参考教程:
https://github.com/alsotang/node-lessons/blob/master/lesson4/app.js
一. 利用cheerio实现网络爬虫
示例代码:
//利用cheerio实现网络爬虫
var express = require('express');
//一个 Node.js 版的 jquery,用来从网页中以 css selector 取数据,使用方式跟 jquery 一样一样的
var cheerio = require('cheerio');
// http 方面的库,可以发起 get 或 post 请求
var superagent = require('superagent');
var app = express();
app.get('/',function(req,res,next){
superagent.get('https://cnodejs.org/')
.end(function(err,sres){
if(err){
return next(err);
}
// sres.text 里面存储着网页的 html 内容,将它传给 cheerio.load 之后
// 就可以得到一个实现了 jquery 接口的变量,我们习惯性地将它命名为 `$`
// 剩下就都是 jquery 的内容了
var $ = cheerio.load(sres.text);
var items = [];
$('#topic_list .topic_title').each(function(idx,element){
var $element = $(element);
items.push({
title: $element.attr('title'),
href: $element.attr('href')
});
});//each
if(items.length > 0){
var temp = items[0];
var tempHref = 'https://cnodejs.org' + temp.href;
console.log(tempHref);
superagent.get(tempHref)
.end(function(err,sres){
if(err){
console.log(err);
return next(err);
}
var $ = cheerio.load(sres.text);
var content = '';
$("#sidebar .user_card .user_name").each(function(idx,element){
var $element = $(element);
content = "href: " + tempHref + " is written by " + $element.text();
});//each
var result = {};
result.items = items;
result.content = content;
res.send(result);
});//end
}//if
});
});
二. 利用eventproxy 实现并发控制
eventproxy用法参照:
https://github.com/JacksonTian/eventproxy#%E9%87%8D%E5%A4%8D%E5%BC%82%E6%AD%A5%E5%8D%8F%E4%BD%9C
var express = require('express');
var cheerio = require('cheerio'); //用于对网页内容进行分析
var superagent = require('superagent'); //用于抓取网页内容
var eventproxy = require('eventproxy');//用于实现并发控制
var url = require('url');
var cnodeUrl = 'https://cnodejs.org/';
//抓取cnode首页各个文章标题行 对应的href
superagent.get(cnodeUrl)
.end(function (err, res) {
if (err) {
return console.error(err);
}
//获取标题对应url
var topicUrls = [];
var $ = cheerio.load(res.text);
$('#topic_list .topic_title').each(function (idx, element) {
var $element = $(element);
//url.resolve 为URL或 href 插入 或 替换原有的标签
/**
* var url = require('url');
var a = url.resolve('/one/two/three', 'four') ,
b = url.resolve('http://example.com/', '/one'),
c = url.resolve('http://example.com/one', '/two');
console.log(a +","+ b +","+ c);
//输出结果:
///one/two/four
//http://example.com/one
//http://example.com/two
* */
var href = url.resolve(cnodeUrl, $element.attr('href'));
topicUrls.push(href);
});
//eventproxy 适用于多异步协作的场景
var ep = new eventproxy();
//ep.after 用于处理重复异步协作 所有异步调用结束后,执行某些操作 topic_html触发topicUrls.length次数后
//执行回调函数
ep.after('topic_html', topicUrls.length, function (topics) {
//map() 方法按照原始数组元素顺序依次处理元素,返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。
topics = topics.map(function (topicPair) {
var topicUrl = topicPair[0];
var topicHtml = topicPair[1];
var $ = cheerio.load(topicHtml);
return ({
title: $('.topic_full_title').text().trim(),
href: topicUrl,
comment1: $('.reply_content').eq(0).text().trim()
});
});
console.log('final: ');
console.log(topics);
});
//访问首页每个标题行对应链接的网页
topicUrls.forEach(function (topicUrl) {
superagent.get(topicUrl)
.end(function (err, res) {
console.log("fetch " + topicUrl + " successful");
ep.emit("topic_html", [topicUrl, res.text]); //触发topic_html事件
});//end
});
});//end
三.mapLimit(coll, limit, iteratee, callbackopt)
控制一次最大的并发量
Parameters:
Name Type Description
coll Array | Iterable | Object
A collection to iterate over.
limit number
The maximum number of async operations at a time.
iteratee function
A function to apply to each item in coll. The iteratee is passed a callback(err, transformed) which must be called once it has completed with an error (which can be null) and a transformed item. Invoked with (item, callback).
callback function
A callback which is called when all iteratee functions have finished, or an error occurs. Results is an array of the transformed items from the coll. Invoked with (err, results).
实例:
var async = require('async');
var concurrencyCount = 0;
var fetchUrl = function (url, callback){
var delay = parseInt((Math.random() * 10000000)%2000, 10);
concurrencyCount ++;
console.log("现在的并发数是",concurrencyCount,',正在抓取的是',url,',耗时'+delay+'毫秒');
setTimeout(function(){
concurrencyCount--;
callback(null,url + 'html content');
});
};
var urls = [];
for(var i=0; i<30; i++){
urls.push("http://datasource_" + i);
}
async.mapLimit(urls,5,function(url,callback){
fetchUrl(url,callback);
},function(err,result){
console.log("final:");
console.log(result);
});
四.测试框架Mocha + 断言库should + 测试率覆盖工具 istanbul + Makefile
遇到问题一:用Istanbul生成覆盖率报告时
istanbul cover _mocha
总是出现错误:
No coverage information was collected, exit without writing coverage information
d:\Program Files\nodejs\_mocha.CMD:1
(function (exports, require, module, __filename, __dirname) { @IF EXIST "%~dp0
^
SyntaxError: Unexpected token ILLEGAL
at exports.runInThisContext (vm.js:69:16)
at Module._compile (module.js:432:25)
at Object.Module._extensions..js (module.js:467:10)
at Object.Module._extensions.(anonymous function) [as .js] (D:\Program Files
\nodejs\node_modules\istanbul\lib\hook.js:109:37)
at Module.load (module.js:349:32)
at Function.Module._load (module.js:305:12)
at Function.Module.runMain (module.js:490:10)
at runFn (D:\Program Files\nodejs\node_modules\istanbul\lib\command\common\r
un-with-cover.js:122:16)
at D:\Program Files\nodejs\node_modules\istanbul\lib\command\common\run-with
-cover.js:251:17
at D:\Program Files\nodejs\node_modules\istanbul\lib\util\file-matcher.js:68
:16
mocha版本跟istanbu版本有很大的依赖性,所以正确的方式就是安装mocha到项目目录中,然后使用项目里面的mocha进行测试,使用如下命令:
istanbul cover –hook-run-in-content node_modules /mocha /bin /_mocha
或者
istanbul cover node_modules/mocha/bin/_mocha
遇到问题二:make插件已装,但是在使用make命令时,总是报错:
在Windows下用make命令坑爹啊。。。
解决过程:
(1)安装TDM版本的MinGW包,安装后就是完整的MinGW环境了(64位的系统要安装64为的MinGW)。
http://tdm-gcc.tdragon.net/
将mingw32-make.exe重命名为make.exe,之后在【开始】菜单中打开MinGW Command Prompt,即可使用make命令了
(2)但是在运行makefile时,又出现了新问题,提示:
Makefile:3: *** missing separator. Stop.
问题原因:在makefile中,命令行要以tab键开头。
但是在webstorm中tab键默认使用四个空格代替的,而make命令运行时是只识别tab键的
解决方法:File->Settings->Editor->Code Style中,勾选Use Tab Character即可
(3)再次运行make XXXX,又出现了新问题,提示
XXXX is up to date
这是makefile target和dir名字冲突造成的,解决方法则是借助.PHONY。
GNU默认makefile target是一个文件,因此他会先检测同级目录下是否已存在这个文件,如果存在,则会abort掉make 进程,但目标不是文件的话,则会出现up to date的情况,这种情况需要.PHONY来避免问题的出现,phony的意思是“赝品”,在这里可以形象的理解成“不是文件”。.PHONY防止目标名与现有文件冲突,显式声明哪些目标是伪文件。(参考文章)
.PHONY: lint template coffee concat min test clean build
(4)再次运行,又出现了新问题:
问题原因:MinGW不识别/表示的路径
解决方案:将makefile中的”/”更改为”\”即可。
附加:NPM包安装常用命令
初始化package.json: npm init
安装包并记录在package.json中devDependencies下:
npm install --save-dev package-name
本地安装: npm install package-name
全局全装: npm install -g package-name
升级: npm update
卸载: npm uninstall
五.浏览器测试:mocha,chai,phantomjs ( Lesson7 )
- mocha 测试框架(了解mocha)
npm i -g mocha # 安装全局的 mocha 命令行工具
mocha init . # 生成脚手架
mocha测试原型的目录结构:
.
├── index.html # 这是前端单元测试的入口
├── mocha.css
├── mocha.js
└── tests.js # 我们的单元测试代码将在这里编写
我们直接在 index.html 插入上述示例的 fibonacci 函数以及断言库 chaijs(了解chaijs)。
<div id="mocha"></div>
<script src='https://cdn.rawgit.com/chaijs/chai/master/chai.js'></script>
<script>
var fibonacci = function (n) {
if (n === 0) {
return 0;
}
if (n === 1) {
return 1;
}
return fibonacci(n-1) + fibonacci(n-2);
};
</script>
然后在tests.js中写入对应测试用例
var should = chai.should();
describe('simple test', function () {
it('should equal 0 when n === 0', function () {
window.fibonacci(0).should.equal(0);
});
});
这时打开index.html,可以发现测试结果,我们完成了浏览器端的脚本测试。
2. headless 浏览器 phantomjs(了解phantomjs)
mocha没有提供一个命令行的前端脚本测试环境(因为我们的脚本文件需要运行在浏览器环境中),因此我们使用phantomjs帮助我们搭建一个模拟环境。不重复制造轮子,这里直接使用mocha-phantomjs帮助我们在命令行运行测试。
首先安装mocha-phanatomjs
npm i -g 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
结果展现是不是和后端代码测试很类似
更进一步,我们可以直接在 package.json 的 scripts 中添加 (package.json 通过 npm init 生成,这里不再赘述)
"scripts": {
"test": "mocha-phantomjs index.html --ssl-protocol=any --ignore-ssl-errors=true"
},
将mocha-phantomjs作为依赖
npm i mocha-phantomjs --save-dev
直接运行
npm test
六.supertest,nodemon,cookie(Lesson8)
先来介绍一下 supertest。supertest 是 superagent 的孪生库。他的作者叫 tj,这是个在 Node.js 的历史上会永远被记住的名字,因为他一个人撑起了 npm 的半边天。别误会成他是 npm 的开发者,他的贡献是在 Node.js 的方方面面都贡献了非常高质量和口碑的库,比如 mocha 是他的,superagent 是他的,express 是他的,should 也是他的,还有其他很多很多,比如 koa,都是他的。如果你更详细点了解一些 Node 圈内的八卦,一定也会像我一样对 tj 佩服得五体投地。他的 github 首页是:https://github.com/tj 。
安装 nodemon
$ npm i -g nodemon
这个库是专门调试时候使用的,它会自动检测 node.js 代码的改动,然后帮你自动重启应用。在调试时可以完全用 nodemon 命令代替 node 命令。
$ nodemon app.js
启动我们的应用试试,然后随便改两行代码,就可以看到 nodemon 帮我们重启应用了。
实例:
var app = require('../app');
var supertest = require('supertest');
// 看下面这句,这是关键一句。得到的 request 对象可以直接按照
// superagent 的 API 进行调用
var request = supertest(app);
var should = require('should');
describe('test/app.test.js', function () {
// 我们的第一个测试用例,好好理解一下
it('should return 55 when n is 10', function (done) {
// 之所以这个测试的 function 要接受一个 done 函数,是因为我们的测试内容
// 涉及了异步调用,而 mocha 是无法感知异步调用完成的。所以我们主动接受它提供
// 的 done 函数,在测试完毕时,自行调用一下,以示结束。
// mocha 可以感知到我们的测试函数是否接受 done 参数。js 中,function
// 对象是有长度的,它的长度由它的参数数量决定
// (function (a, b, c, d) {}).length === 4
// 所以 mocha 通过我们测试函数的长度就可以确定我们是否是异步测试。
request.get('/fib')
// .query 方法用来传 querystring,.send 方法用来传 body。
// 它们都可以传 Object 对象进去。
// 在这里,我们等于访问的是 /fib?n=10
.query({n: 10})
.end(function (err, res) {
// 由于 http 返回的是 String,所以我要传入 '55'。
res.text.should.equal('55');
// done(err) 这种用法写起来很鸡肋,是因为偷懒不想测 err 的值
// 如果勤快点,这里应该写成
/*
should.not.exist(err);
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 > 10', function (done) {
testFib(11, 500, 'n should <= 10', done);
});
it('should throw when n < 0', function (done) {
testFib(-1, 500, 'n should >= 0', done);
});
it('should throw when n isnt Number', function (done) {
testFib('good', 500, 'n should be a Number', done);
});
// 单独测试一下返回码 500
it('should status 500 when error', function (done) {
request.get('/fib')
.query({n: 100})
.expect(500)
.end(function (err, res) {
done(err);
});
});
});
七. benchmark(Lesson10)
https://github.com/bestiejs/benchmark.js
实例:
我们先来实现这三个函数:
var int1 = function (str) {
return +str;
};
var int2 = function (str) {
return parseInt(str, 10);
};
var int3 = function (str) {
return Number(str);
};
然后照着官方的模板写 benchmark suite:
var number = '100';
// 添加测试
suite
.add('+', function() {
int1(number);
})
.add('parseInt', function() {
int2(number);
})
.add('Number', function () {
int3(number);
})
// 每个测试跑完后,输出信息
.on('cycle', function(event) {
console.log(String(event.target));
})
.on('complete', function() {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
// 这里的 async 不是 mocha 测试那个 async 的意思,这个选项与它的时间计算有关,默认勾上就好了。
.run({ 'async': true });
直接运行:
八.js 作用域和闭包 浏览器渲染(Lesson11)
1.作用域
es6中新增了 let 关键词,与块级作用域,相关知识参考:
http://es6.ruanyifeng.com/#docs/let
ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效. let定义的变量类似于java和C中的变量,使用前必须先定义。var声明的变量在全局范围内有效,而let声明的变量在块级作用域中有效。
2.闭包(参考文章)
闭包这个概念,在函数式编程里很常见,简单的说,就是使内部函数可以访问定义在外部函数中的变量。
闭包的应用:
(1)Singleton单例
var singleton = function () {
var privateVariable;
function privateFunction(x) {
...privateVariable...
}
return {
firstMethod: function (a, b) {
...privateVariable...
},
secondMethod: function (c) {
...privateFunction()...
}
};
}();
需要注意的地方是匿名主函数结束的地方的’()’,如果没有这个’()’就不能产生单件。因为匿名函数只能返回了唯一的对象,而且不能被其他地方调用。这个就是利用闭包产生单件的方法。
(2)把一个函数的多个参数分解成多个函数, 然后把函数多层封装起来,每层函数都返回一个函数去接收下一个参数这样,可以简化函数的多个参数
示例:
var adder = function (x) {
var base = x;
return function (n) {
return n + base;
};
};
var add10 = adder(10);
console.log(add10(5));
var add20 = adder(20);
console.log(add20(5));
3. 浏览器渲染(参考文章)
减少reflow(重新布局)/repaint(重画)
下面是一些Best Practices:
1)不要一条一条地修改DOM的样式。与其这样,还不如预先定义好css的class,然后修改DOM的className。
// bad
var left = 10,
top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// Good
el.className += " theclassname";
// Good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
2)把DOM离线后修改。如:
使用documentFragment 对象在内存里操作DOM
先把DOM给display:none(有一次reflow),然后你想怎么改就怎么改。比如修改100次,然后再把他显示出来。
clone一个DOM结点到内存里,然后想怎么改就怎么改,改完后,和在线的那个的交换一下。
3)不要把DOM结点的属性值放在一个循环里当成循环里的变量。不然这会导致大量地读写这个结点的属性。
4)尽可能的修改层级比较低的DOM。当然,改变层级比较底的DOM有可能会造成大面积的reflow,但是也可能影响范围很小。
5)为动画的HTML元件使用fixed或absoult的position,那么修改他们的CSS是不会reflow的。
6)千万不要使用table布局。因为可能很小的一个小改动会造成整个table的重新布局。