// 这句的意思就是引入 `express` 模块,并将它赋予 `express` 这个变量等待使用。
var express = require('express');
// 调用 express 实例,它是一个函数,不带参数调用时,会返回一个 express 实例,将这个变量赋予 app 变量。
var app = express();
// app 本身有很多方法,其中包括最常用的 get、post、put/patch、delete,在这里我们调用其中的 get 方法,为我们的 `/` 路径指定一个 handler 函数。
// 这个 handler 函数会接收 req 和 res 两个对象,他们分别是请求的 request 和 response。
// request 中包含了浏览器传来的各种信息,比如 query 啊,body 啊,headers 啊之类的,都可以通过 req 对象访问到。
// res 对象,我们一般不从里面取信息,而是通过它来定制我们向浏览器输出的信息,比如 header 信息,比如想要向浏览器输出的内容。这里我们调用了它的 #send 方法,向浏览器输出一个字符串。
app.get('/', function (req, res) {
res.send('Hello World');
});
// 定义好我们 app 的行为之后,让它监听本地的 3000 端口。这里的第二个函数是个回调函数,会在 listen 动作成功后执行,我们这里执行了一个命令行输出操作,告诉我们监听动作已完成。
app.listen(3000, function () {
console.log('app is listening at port 3000');
});
var express = require('express');
var utility = require('utility');
var app = express();
app.get('/', function(req, res) {
// 从 req.query 中取出我们的 q 参数。
// 如果是 post 传来的 body 数据,则是在 req.body 里面,不过 express 默认不处理 body 中的信息,需要引入 https://github.com/expressjs/body-parser 这个中间件才会处理,这个后面会讲到。
var q = req.query.q;
// 调用 utility.md5 方法,得到 md5 之后的值
// 之所以使用 utility 这个库来生成 md5 值,其实只是习惯问题。每个人都有自己习惯的技术堆栈,
var md5Value = utility.md5(q);
res.send(md5Value);
});
app.listen(3030, function(req, res) {
console.log('app is running at port 3030......');
});
需要用到三个依赖,express,superagent和cheerio
var superagent = require('superagent');
var cheerio = require('cheerio');
var express = require('express');
var app = new express();
app.get('/', function(req, res, next) {
superagent.get('https://cnodejs.org')
.end(function(err, sres) {
// 常规的错误处理
if (err) {
return next(err);
}
// sres里面存储着网页的html内容,将它传给cheerio.load之后
// 就可以得到一个实现了jQuery接口的变量,我们习惯性命名为`$`
// 剩下都是jQuery的内容
var $ = cheerio.load(sres.text);
var items = [];
$('#topic_list .topic_title').each(function(index, ele) {
var $element = $(ele);
items.push({
title: $element.attr('title'),
href: $element.attr('href')
});
});
res.send(items);
});
});
app.listen(3030, function(req, res) {
console.log('app is running at port 3030......');
});
学习使用eventproxy
控制并发操作。
代码的入口是 app.js
,当调用 node app.js
时,它会输出 CNode(https://cnodejs.org/ ) 社区首页的所有主题的标题,链接和第一条评论,以 json 的格式
var superagent = require('superagent');
var cheerio = require('cheerio');
var express = require('express');
var url = require('url');
var ROOT_PATH = 'https://cnodejs.org/';
var app = new express();
app.get('/', function(req, res) {
superagent.get(ROOT_PATH)
.end(function(err, sres) {
// 常规的错误处理
if (err) {
return console.log(err);
}
// sres里面存储着网页的html内容,将它传给cheerio.load之后
// 就可以得到一个实现了jQuery接口的变量,我们习惯性命名为`$`
// 剩下都是jQuery的内容
var $ = cheerio.load(sres.text);
var items = [];
// 获取首页所有链接
$('#topic_list .topic_title').each(function(index, ele) {
var $element = $(ele);
// $element.attr('href')原本是‘/topic/5960a411a4de5625080fe1fc’
// 我们用url.resolve来自动推断出完整的url,变成
// https://cnodejs.org/topic/5960a411a4de5625080fe1fc的形式
items.push({
title: $element.attr('title'),
href: url.resolve(ROOT_PATH, $element.attr('href'))
});
});
res.send(items);
});
});
app.listen(3030, function(req, res) {
console.log('app is running at port 3030......');
});
要并发异步获取两三个地址的数据,并且在获取到数据后要对这些数据一起进行利用的话,常规写法是自己维护一个计数器,每抓取一次就count++(因为你不知道哪个抓取先完成),当count==3时,用另一个函数对三次数据进行处理
eventproxy就起到了计数器的作用,管理异步操作是否完成,完成后会将抓取到的数据当参数传过来,自动调用你提供的回调函数
eventproxy的常用用法
先 var ep = new eventproxy();
得到一个 eventproxy 实例
告诉它你要监听哪些事件,并给它一个回调函数。
ep.all('event1', 'event2', function (result1, result2) {
});
在适当的时候,例如get到数据后, ep.emit('event_name', eventData)
$.get('http://data3_source', function (data) {
ep.emit('data3_event', data);
});
所有源码:
var superagent = require('superagent');
var cheerio = require('cheerio');
var express = require('express');
var url = require('url');
var eventproxy = require('eventproxy');
var ROOT_PATH = 'https://cnodejs.org/';
var app = new express();
app.get('/', function(req, res) {
superagent.get(ROOT_PATH)
.end(function(err, sres) {
// 常规的错误处理
if (err) {
return console.log(err);
}
// sres里面存储着网页的html内容,将它传给cheerio.load之后
// 就可以得到一个实现了jQuery接口的变量,我们习惯性命名为`$`
// 剩下都是jQuery的内容
var $ = cheerio.load(sres.text);
var items = [];
// 获取首页所有链接
$('#topic_list .topic_title').each(function(index, ele) {
var $element = $(ele);
// #element.attr('href')原本是‘/topic/5960a411a4de5625080fe1fc’
// 我们用url.resolve来自动推断出完整的url,变成
// https://cnodejs.org/topic/5960a411a4de5625080fe1fc的形式
items.push(url.resolve(ROOT_PATH, $element.attr('href')));
});
items.forEach(function(topicsUrl) {
superagent.get(topicsUrl)
.end(function(error1, res) {
console.log('fetch ' + topicsUrl + ' successful');
// 将获取到的数据以数组对的形式暴露出去
ep.emit('topic_html', [topicsUrl, res.text]);
});
});
var ep = new eventproxy();
// 命令ep重复监听items.length次topic_html事件之后再进行行动
ep.after('topic_html', items.length, function(topics) {
topics = topics.map(function(topicPair) {
var topicUrl = topicPair[0];
var topicHtml = topicPair[1];
var $ = cheerio.load(topicHtml);
return ({
// trim()表示返回字符串的副本,删除了头尾的空白
title: $('.topic_full_title').text().trim(),
href: topicUrl,
comment1: $('.reply_content').eq(0).text().trim()
});
});
res.send(topics);
});
});
});
app.listen(3030, function(req, res) {
console.log('app is running at port 3030......');
});
用async将上个实验的并发连接数控制在5个
一次性发了40个并发请求出去,别的网站可能会认为是恶意请求,而封掉你的IP,故而需要控制每次的并发数量,慢慢抓完这些链接
eventproxy和async都是用于异步流程控制的:
小于10个汇总数时用eventproxy方便,其他时候用async
var async = require('async');
var superagent = require('superagent');
var express = require('express');
var cheerio = require('cheerio');
var URL = require('url');
var eventproxy = require('eventproxy');
var ROOT_PATH = 'http://www.imooc.com/wenda/recommend/3';
var app = new express();
var ep = new eventproxy();
var resTopic = [];
app.get('/', function(req, res, next) {
superagent.get(ROOT_PATH)
.end(function(err2, res2) {
if (err2) {
return console.log(err2, ': ', res2);
}
var $ = cheerio.load(res2.text);
var items = [];
// 获取链接数组
$('.ques-con-content').each(function(index, ele) {
var $element = $(ele);
items.push(URL.resolve(ROOT_PATH, $element.attr('href')));
});
// 控制并发数
var curCount = 0;
var fetchUrl = function(url, callback) {
// 延迟抓取毫秒数
var delay = parseInt((Math.random() * 300000000) % 2000, 10);
curCount++;
console.log('现在的并发数是', curCount, ',正在抓取的是', url, ',耗时' + delay + '毫秒');
superagent.get(url).end(function(err3, res3) {
if (err3) {
return console.log(err3);
}
var $ = cheerio.load(res3.text);
var topicContent = {
title: $('.js-qa-wenda-title').text().trim(),
href: url,
comment: $('.answer-content').text().trim()
};
// 这里为何不直接用res1.send(topicContent将其显示到网页上?只要调用一次res1.send(),结束后就会自动加上res1.end(),下一次res1.send()时又要发送一次header头部的请求,这是不允许的,暂时将其结果保存到数据中,留个坑
resTopic.push(topicContent);
});
// 手动设置延迟
setTimeout(function() {
curCount--;
callback(null, url + ' html content');
}, delay);
};
// 用async控制异步抓取
// mapLimit(arr, limit, iterator, [callback])
// 异步回调
async.mapLimit(items, 5, function(topicsUrl, callback) {
// 访问链接
fetchUrl(topicsUrl, callback);
}, function(err4, res4) {
res.send(resTopic);
console.log('final:', res4);
});
});
});
app.listen(3030, function(req, res) {
console.log('app is running at port 3030......');
});
mocha:测试框架,这里用作后端测试
should:断言库
istanbul:测试率覆盖工具
main.js:
var fibonacci = function(n) {
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
return fibonacci(n-1) + fibonacci(n-2);
}
};
if (require.main === module) {
// 如果直接执行main.js,则进入此处
// 如果被其他require了,则不会执行此处
var n = Number(process.argv[2]);
console.log('fibonacci', n, 'is', fibonacci(n));
}
exports.fibonacci = fibonacci;
$ cnpm install mosha -g
必须建立test文件夹,mosha寻找test文件夹执行测试
test文件夹下,main.test.js:
var main = require('../main.js');
// 断言库should
var should = require('should');
// 用来描述你要测得主体是什么
describe('main.test.js', function() {
it('should equal 55 when n === 10', function() {
main.fibonacci(10).should.equal(55);
});
it('should equal 1 when n === 1', function() {
main.fibonacci(1).should.equal(1);
});
it('should equal 0 when n === 0', function() {
main.fibonacci(0).should.equal(0);
});
it('should throw when n < 0', function() {
// 错误写法:main.fibonacci(-1).should.throw('n should >= 0');
(function() {
main.fibonacci(-1);
}).should.throw('n should >= 0');
});
it('should throw when n isn\'t a IntNumber', function() {
// 错误写法:main.fibonacci('呵呵').should.throw('n should be a IntNumber');
(function() {
main.fibonacci('hehe');
}).should.throw('n should be a IntNumber');
});
});
运行mosha
should的API库:https://github.com/tj/should.js
先在test文件中描述清楚要达到的目的,让现有的程序跑不过case,再修补程序,让case通过
依照测试的case的pass程度,来更新fibonacci的实现
var fibonacci = function(n) {
// 错误处理
if (n < 0) {
throw new Error('n should >= 0');
} else if (typeof n !== 'number') {
throw new Error('n should be a IntNumber');
}
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
return fibonacci(n-1) + fibonacci(n-2);
}
};
if (require.main === module) {
// 如果直接执行main.js,则进入此处
// 如果被其他require了,则不会执行此处
var n = Number(process.argv[2]);
console.log('fibonacci', n, 'is', fibonacci(n));
}
exports.fibonacci = fibonacci;
$ cnpm i istanbul -g
$ istanbul cover node_modules/mocha/bin/_mocha # 这样会比直接使用mocha多一行覆盖率的输出,也会生成一个html页面显示覆盖率输出
mocha:测试框架,这里用作前端测试,mocha前后端通吃
chai:全栈断言库
phantomjs:headless浏览器的phantomjs
先搭建一个测试原型,用mocha自带的脚手架可以自动生成
$ cd lesson7
$ cnpm i mocha -g
$ mocha init . # 生成脚手架
mocha会自动生成一个简单的测试原型,目录结构如下:
.
├── index.html # 这是前端单元测试的入口
├── mocha.css
├── mocha.js
└── tests.js # 我们的单元测试代码将在这里编写
index.html:
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/4.0.2/chai.min.js">script>
<script>
var fib = function(n) {
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
return fib(n-1) + fib(n-2);
}
}
script>
test.js:
var should = chai.should();
describe('simple test', function() {
it('should equal 0 when n ==== 0', function() {
window.fib(0).should.equal(0);
});
});
然后打开index.html,可以看到浏览器端的脚本测试
mocha没有提供一个命令行的前端脚本测试环境(因为我们的脚本文件需要运行在浏览器环境中),因此我们使用phanatomjs帮助我们搭建一个模拟环境。不重复制造轮子,这里直接使用mocha-phantomjs帮助我们在命令行运行测试。
然后在 index.html 的页面下加上这段兼容代码
<script>mocha.run()script>
改为
<script>
if (window.initMochaPhantomJS && window.location.search.indexOf('skip') === -1) {
initMochaPhantomJS()
}
mocha.ui('bdd');
expect = chai.expect;
mocha.run();
script>
这时候, 我们在命令行中运行
$ mocha-phantomjs index.html --ssl-protocol=any --ignore-ssl-errors=true
结果展现是不是和后端代码测试很类似 :smiley:
更进一步,我们可以直接在 package.json 的 scripts 中添加
(package.json 通过 npm init
生成,这里不再赘述)
"scripts": {
"start": "mocha-phantomjs index.html --ssl-protocol=any --ignore-ssl-errors=true"
},
将mocha-phantomjs作为依赖
$ cnpm i mocha-phantomjs --save-dev
直接运行
$ cnpm test
运行结果如下:
至此,我们实现了前端脚本的单元测试,基于 phanatomjs 你几乎可以调用所有的浏览器方法,而 mocha-phanatomjs 也可以很便捷地将测试结果反馈到 mocha,便于后续的持续集成。
var express = require('express');
var app = express();
var fibonacci = function(n) {
if (n < 0) {
throw new Error('n should >= 0');
}
if (typeof n !== 'number') {
throw new Error('n should be a Number');
}
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
return fibonacci(n-1) + fibonacci(n-2);
}
};
app.get('/fib', function(req, res) {
// http 传来的东西默认都是没有类型的,都是string,所以我们要转换类型
var n = Number(req.query.n);
try {
// 为何将结果转换为string?是因为如果你直接给个数字给res.send,
// 它会当成你给了一个http状态码
res.send(String(fibonacci(n)));
} catch(e) {
// 如果 fibonacci 抛错的话,错误信息会记录在 err 对象的 .message 属性中
res.status(500)
.send(e.message);
}
});
module.exports = app;
app.listen(3030, function() {
console.log('app is linstening at port 3030.');
});
然后访问http://localhost:3000/fib?n=10,输出55就说明启动成功了
再去装一个nodemon可以自动检测node.js代码的改动,然后自动帮你重启应用,可以用nodemon代替node命令
$ cnpm i nodemon -g
var app = require('../app.js');
var supertest = require('supertest');
var should = require('should');
// 得到的request对象可以直接按照superagent的API进行调用
var request = supertest(app);
describe('test/app.test.js', function() {
it('should return 55 when n = 10', function(done) {
// 为什么function要接收一个done函数?
// 是因为这里的fib函数里面涉及了异步调用,而mocha是无法感知异步调用完成的
// 故而要接受done函数,在测试完毕后,自行调用done以示结束
request.get('/fib')
.query({n:10})
.end(function(err, res) {
// 由于http返回的全是string,故而传入'55'
res.text.should.equal('55');
done(err);
});
});
// 下面对各种边界条件都进行测试
// 由于代码都雷同,故而抽象出一个testFib方法
var testFib = function(n, statusCode, expect, done) {
request.get('/fib')
.query({n: n})
.expect(statusCode)
.end(function(err, res) {
res.text.should.equal(expect);
done(err);
});
};
it('should return 0 when n === 0', function (done) {
testFib(0, 200, '0', done);
});
it('should equal 1 when n === 1', function (done) {
testFib(1, 200, '1', done);
});
it('should equal 55 when n === 10', function (done) {
testFib(10, 200, '55', done);
});
it('should throw when n < 0', function (done) {
testFib(-1, 500, 'n should >= 0', done);
});
it('should throw when n isn\'t Number', function (done) {
testFib('good', 500, 'n should be a Number', done);
});
});
有两种思路
在 supertest 中,可以通过 var agent = supertest.agent(app)
获取一个 agent 对象,这个对象的 API 跟直接在 superagent 上调用各种方法是一样的。agent 对象在被多次调用 get
和 post
之后,可以一路把 cookie 都保存下来。
var supertest = require('supertest');
var app = express();
var agent = supertest.agent(app);
agent.post('login').end(...);
// then ..
agent.post('create_topic').end(...); // 此时的 agent 中有用户登陆后的 cookie
在发起请求时,调用 .set('Cookie', 'a cookie string')
这样的方式。
var supertest = require('supertest');
var userCookie;
supertest.post('login').end(function (err, res) {
userCookie = res.headers['Cookie']
});
// then ..
supertest.post('create_topic')
.set('Cookie', userCookie)
.end(...)
正则表达式后面可以跟三个flag
代码 | 说明 |
---|---|
i | 不区分大小写 |
g | 匹配多个 |
m | ^和$可以匹配每一行的开头 |
和m意义相干的还有\A, \Z和\z
\A 字符串开头(类似^,但不受处理多行选项的影响)
\Z 字符串结尾或行尾(不受处理多行选项的影响)
\z 字符串结尾(类似$,但不受处理多行选项的影响)
代码 | 说明 |
---|---|
. | 匹配除换行符以外的任意字符 |
\w | 匹配字母或数字或下划线或汉字 |
\s | 匹配任意的空白符 |
\d | 匹配数字 |
\b | 匹配单词的开始或结束 |
^ | 匹配字符串的开始 |
$ | 匹配字符串的结束 |
【例题】:检查5到12位的QQ号
^\d{5,12}$
代码 | 说明 |
---|---|
* | 重复0次或更多次 |
+ | 重复1次或更多次 |
? | 重复0次或1次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n, m} | 重复n到m次 |
【例题】:匹配apple后面跟一个或更多数字
apple\d+
查找某个字符集合
[aeiou],匹配任何一个字母,[.?!]匹配任何一个标点符号
指定字符范围
[a-z0-9A-Z],在描述的范围内
【例题】:匹配多种电话号码(010)88886666,022-22334455,02912345678
\(?0\d{2}[)- ]?\d{8}
其中(和也是元字符,故而要用转义字符
空格用 表示
不幸的是,上面例题的表达式也能匹配010)123456678或(022-87654321这样不正确的格式,要解决这个问题,我们需要用到分枝条件。
正则表达式里的分枝条件指的是有几种规则,如果满足其中任意一个规则都应该当成匹配,具体的方法是用|把不用的规则分割开
0\d{2}-\d{8}|0\d{3}-\d{7}:这个表达式可以匹配两种以两字符为分隔的电话号码,一种是3位区号,后面8位数字,一种是4位区号,后面7位数字
\(0\d{2}\)[- ]?\d{8}|0\d{2}[- ]?\d{8}:这个表达式可以匹配两种,一种是有括号,0开头,3位区号,有-或空格或没有,一种是没括号,0开头,3位区号,有-或空格或没有
使用分枝条件的时候要注意每个条件的顺序,匹配时,会从左到右的测试每个条件,一旦满足了某个分枝,就不会去管其他的条件
上面提到重复单个字符(加上限定符即可);但如果要重复多个字符该如何?
可以用()
来制定子表达式,然后就可以像单字符一样指定这个子表达式的重复次数了
【例题】:匹配简单的IP地址
(\d{1,3}.){3}\d{1,3}
要理解这个表达式,请按下列顺序分析它:\d{1,3}匹配1到3位的数字,(\d{1,3}.){3}匹配三位数字加上一个英文句号(这个整体也就是这个分组)重复3次,最后再加上一个一到三位的数字(\d{1,3})
代码/语法 | 说明 |
---|---|
\W | 匹配任意不是字母,数字,下划线,汉字的字符 |
\S | 匹配任意不是空白符的字符 |
\D | 匹配任意非数字的字符 |
\B | 匹配不是单词开头或结束的位置 |
[^x] | 匹配除了x以外的任意字符 |
[^aeiou] | 匹配除了aeiou这几个字母以外的任意字符 |
有一个字符串 var num = ‘1000’,要将其转换成Number类型的1000
目前有三个选项:+,parseInt,Number
请测试哪个方法快