Node js 相关知识总结

本秂侑毒 提交于 2019-12-05 14:43:32

参考教程:
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 )

  1. 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, nodemon

先来介绍一下 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的重新布局。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!