NodeJs实验楼-笔记

2 篇文章 0 订阅
1 篇文章 0 订阅

Nodejs实验楼

lesson1:express应用

// 这句的意思就是引入 `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');
});

lesson2:学习使用外部模块

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......');
});

lesson3:使用superagent与cheerio完成简单爬虫

需要用到三个依赖,express,superagent和cheerio

  1. superagent是一个http方面的库,可以发起post或get请求
  2. cheerio是一个Node.js版的jQuery
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......');
});

lesson4:使用eventproxy控制并发

1.1实验内容

学习使用eventproxy控制并发操作。

代码的入口是 app.js,当调用 node app.js 时,它会输出 CNode(https://cnodejs.org/ ) 社区首页的所有主题的标题,链接和第一条评论,以 json 的格式

1.2实验知识点

  1. 体会Node.js的callback hell之美
  2. 学习使用eventproxy控制并发
获取主页所有链接
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......');
});

2.2 eventproxy介绍

要并发异步获取两三个地址的数据,并且在获取到数据后要对这些数据一起进行利用的话,常规写法是自己维护一个计数器,每抓取一次就count++(因为你不知道哪个抓取先完成),当count==3时,用另一个函数对三次数据进行处理

eventproxy就起到了计数器的作用,管理异步操作是否完成,完成后会将抓取到的数据当参数传过来,自动调用你提供的回调函数

eventproxy的常用用法

  1. var ep = new eventproxy(); 得到一个 eventproxy 实例

  2. 告诉它你要监听哪些事件,并给它一个回调函数。

    ep.all('event1', 'event2', function (result1, result2) {
    
    });
  3. 在适当的时候,例如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......');
});

lesson5:使用async控制并发

1.1 实验内容

用async将上个实验的并发连接数控制在5个

2.1 在lesson4的遗留问题

一次性发了40个并发请求出去,别的网站可能会认为是恶意请求,而封掉你的IP,故而需要控制每次的并发数量,慢慢抓完这些链接

eventproxy和async都是用于异步流程控制的:

小于10个汇总数时用eventproxy方便,其他时候用async

2.2 使用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......');
  });

lesson6:mocha, should, istanbul在后端使用测试框架

mocha:测试框架,这里用作后端测试

should:断言库

istanbul:测试率覆盖工具

2.2 编写待测试程序

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;

2.3 安装mocha

$ cnpm install mosha -g

2.4 编写测试

必须建立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

2.5 体验测试驱动开发的过程

先在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;

2.5 istanbul

$ cnpm i istanbul -g
$ istanbul cover node_modules/mocha/bin/_mocha # 这样会比直接使用mocha多一行覆盖率的输出,也会生成一个html页面显示覆盖率输出

lesson7:mocha,chai,phantomjs浏览器端测试

mocha:测试框架,这里用作前端测试,mocha前后端通吃

chai:全栈断言库

phantomjs:headless浏览器的phantomjs

2.2.1 浏览器环境执行

先搭建一个测试原型,用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,可以看到浏览器端的脚本测试

2.2.2 测试反馈

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,便于后续的持续集成。

lesson8: supertest配合express进行集合测试

先编写app.js

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

在test文件夹下写测试代码app.test.js

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);
  });
});

有两种思路

  1. 在 supertest 中,可以通过 var agent = supertest.agent(app) 获取一个 agent 对象,这个对象的 API 跟直接在 superagent 上调用各种方法是一样的。agent 对象在被多次调用 getpost 之后,可以一路把 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
  2. 在发起请求时,调用 .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(...)

lesson9:正则表达式

修饰符

正则表达式后面可以跟三个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+

字符类

  1. 查找某个字符集合

    [aeiou],匹配任何一个字母,[.?!]匹配任何一个标点符号

  2. 指定字符范围

    [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这几个字母以外的任意字符

lesson10:benchmark性能测试库

1.4 实验目标

有一个字符串 var num = ‘1000’,要将其转换成Number类型的1000

目前有三个选项:+,parseInt,Number

请测试哪个方法快

2.1 benchmark库

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值