Node.js借助事件驱动,非阻塞I/O模型变得轻量和高效,非常适合 运行在分布式设备 的 数据密集型 的实时应用。

  • Visual Studio Code由微软出品,但它不是那个大块头的Visual Studio,它是一个精简版的迷你Visual Studio。VS Code以文件夹作为工程目录(Workspace Dir),所有的JavaScript文件都存放在该目录下。此外,VS Code在工程目录下还需要一个.vscode的配置目录,里面存放了VS Code需要的配置文件。
  • process是一个全局内置对象,可以在代码中的任何位置访问此对象,这个对象代表我们的node.js代码宿主的操作系统进程对象。使用process对象可以截获进程的异常、退出等事件,也可以获取进程的当前目录、环境变量、内存占用等信息,还可以执行进程退出、工作目录切换等操作。
  • stdout是标准输出流。console.log = function(d){ process.stdout.write(d+’\n’); } console.log就是封装了它。
  • fs模块的文件I/O是对标准POSIX函数的简单封装。fs模块提供writeFile函数,可以异步的将数据写入一个文件, 如果文件已经存在则会被替换。
  • 如果要读取目录下所有的文件应该怎么办呢?readdir函数可以读取到指定目录下所有的文件,回调函数(callback)接受两个参数(err, files)其中files是一个存储目录中所包含的文件名称的数组,数组中不包括’.’和’..’。
  • node.js为互联网而生,和url打交道是无法避免的了,url模块提供一些基础的url处理。
  • Query String模块用于实现URL参数字符串与参数对象之间的互相转换,提供了”stringify”、”parse”等一些实用函数来针对字符串进行处理,通过序列化和反序列化,来更好的应对实际开发中的条件需求。
  • util模块,是一个Node.js核心模块,提供常用函数的集合,用于弥补核心JavaScript的一些功能过于精简的不足。并且还提供了一系列常用工具,用来对数据的输出和验证。
  • util.inspect(object,[showHidden],[depth],[colors])是一个将任意对象转换为字符串的函数,通常用于调试和错误输出。
  • Node保持了javascript在浏览器中单线程的特性,而且在Node中,javascript与其余线程是无法共享任何状态的。单线程的最大好处是不用像多线程编程那样出现状态的同步问题,这里没有死锁,也没有线程上下文切换所带来的性能上的开销。但是,单线程也有他自身的弱点,这些弱点是学习Node的过程中必须要面对的。单线程主要有以下弱点:1、无法利用多核CPU 2、错误会引起整个应用退出,应用的健壮性值得考验 3、大量计算占用CPU导致无法继续调用异步I/O。其中最后一点是最不能够忍受的,如果大量计算长期占用CPU导致后续I/O无法调用,已完成的I/O的回调函数也会得不到及时执行,这也就违背了当初创建高性能web服务器初衷。Node采用了Html5制定的Web Worker的标准相同的思路解决单线程中大量计算的问题:child_process。子进程的出现同时意味着Node可以从容的应对单线程在健壮性和无法利用多核CPU方面的问题。通过将计算分发到各个子进程,分解大量计算,然后再通过进程间的事件消息来传递结果,可以很好的保持应用模型的简单和低耦合。通过Master-Worker的管理方式,也可以很好的管理各个工作进程,已达到更高的健壮性。目前最新版本6.9.5已经支持Windows、Linux、MacOS、SunOS、Docker镜像。
  • child_process模块提供了四个创建子进程的函数,分别是spawn,exec,execFile和fork。
  • 如果在一个函数内部发生了错误,它自身没有捕获,错误就会被抛到外层调用函数,如果外层函数也没有捕获,该错误会一直沿着函数调用链向上抛出,直到被JavaScript引擎捕获,代码终止执行。
  • WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术。有一个优秀的第三方API,名为Socket.IO。Socket.IO是一个WebSocket库,包括了客户端的js和服务器端的nodejs,它的目标是构建可以在不同浏览器和移动设备上使用的实时应用。
  • 除了标准的异步读取模式外,fs也提供相应的同步读取函数。同步读取的函数和异步函数相比,多了一个Sync后缀,并且不接收回调函数,函数直接返回结果。如果同步读取文件发生错误,则需要用try…catch捕获该错误。如果我们要获取文件大小,创建时间等信息,可以使用fs.stat(),它返回一个Stat对象,能告诉我们文件或目录的详细信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    	try {
    var data = fs.readFileSync('sample.txt', 'utf-8');
    console.log(data);
    } catch (err) {
    // 出错了
    }
    ```
    + 异步还是同步---在fs模块中,提供同步方法是为了方便使用。那我们到底是应该用异步方法还是同步方法呢?由于Node环境执行的JavaScript代码是服务器端代码,所以,绝大部分需要在服务器运行期反复执行业务逻辑的代码,必须使用异步代码,否则,同步代码在执行时期,服务器将停止响应,**因为JavaScript只有一个执行线程。**服务器启动时如果需要读取配置文件,或者结束时需要写入到状态文件时,可以使用同步代码,因为这些代码只在启动和结束时执行一次,不影响服务器正常运行时的异步执行。
    + stream是Node.js提供的又一个仅在服务区端可用的模块,目的是支持“流”这种数据结构。流的特点是数据是有序的,而且必须依次读取,或者依次写入,不能像Array那样随机定位。在Node.js中,流也是一个对象,我们只需要响应流的事件就可以了:data事件表示流的数据已经可以读取了,end事件表示这个流已经到末尾了,没有数据可以读取了,error事件表示出错了。要注意,data事件可能会有多次,每次传递的chunk是流的一部分数据。所有可以读取数据的流都继承自stream.Readable,所有可以写入的流都继承自stream.Writable。
    ```html
    //从文件流读取文本内容
    'use strict';
    var fs = require('fs');
    // 打开一个流:
    var rs = fs.createReadStream('sample.txt', 'utf-8');
    rs.on('data', function (chunk) {
    console.log('DATA:')
    console.log(chunk);
    });
    rs.on('end', function () {
    console.log('END');
    });
    rs.on('error', function (err) {
    console.log('ERROR: ' + err);
    });
    ```
    ```html
    //以流的形式写入文件,只需要不断调用write()方法,最后以end()结束
    'use strict';
    var fs = require('fs');
    var ws1 = fs.createWriteStream('output1.txt', 'utf-8');
    ws1.write('使用Stream写入文本数据...\n');
    ws1.write('END.');
    ws1.end();
    var ws2 = fs.createWriteStream('output2.txt');
    ws2.write(new Buffer('使用Stream写入二进制数据...\n', 'utf-8'));
    ws2.write(new Buffer('END.', 'utf-8'));
    ws2.end();
  • 一个Readable流和一个Writable流串起来后,所有的数据自动从Readable流进入Writable流,这种操作叫pipe。在Node.js中,Readable流有一个pipe()方法。

    1
    2
    3
    4
    5
    6
    7
    //源文件的所有数据就自动写入到目标文件里了,所以,这实际上是一个复制文件的程序
    'use strict';
    var fs = require('fs');
    var rs = fs.createReadStream('sample.txt');
    var ws = fs.createWriteStream('copied.txt');
    rs.pipe(ws);
    //默认情况下,当Readable流的数据读取完毕,end事件触发后,将自动关闭Writable流。
  • 要开发HTTP服务器程序,从头处理TCP连接,解析HTTP是不现实的。这些工作实际上已经由Node.js自带的http模块完成了。应用程序并不直接和HTTP协议打交道,而是操作http模块提供的request和response对象。request对象封装了HTTP请求,我们调用request对象的属性和方法就可以拿到所有HTTP请求的信息;response对象封装了HTTP响应,我们操作response对象的方法,就可以把HTTP响应返回给浏览器。

  • 处理本地文件目录需要使用Node.js提供的path模块,它可以方便地构造目录。使用path模块可以正确处理操作系统相关的文件路径。在Windows系统下,返回的路径类似于C:\Users\michael\static\index.html。
    1
    2
    3
    4
    5
    6
    7
    'use strict';
    var path = require('path');
    // 解析当前目录:
    var workDir = path.resolve('.'); // '/Users/michael'
    // 组合完整的文件路径:当前目录+'pub'+'index.html':
    var filePath = path.join(workDir, 'pub', 'index.html');
    // '/Users/michael/pub/index.html'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//文件服务器file_server.js  在命令行运行node file_server.js /path/to/dir
'use strict';
var
fs = require('fs'),
url = require('url'),
path = require('path'),
http = require('http');

// 从命令行参数获取root目录,默认是当前目录:
var root = path.resolve(process.argv[2] || '.');
console.log('Static root dir: ' + root);
// 创建服务器:
var server = http.createServer(function (request, response) {
// 获得URL的path,类似 '/css/bootstrap.css':
var pathname = url.parse(request.url).pathname;
// 获得对应的本地文件路径,类似 '/srv/www/css/bootstrap.css':
var filepath = path.join(root, pathname);
// 获取文件状态:
fs.stat(filepath, function (err, stats) {
if (!err && stats.isFile()) {
// 没有出错并且文件存在:
console.log('200 ' + request.url);
// 发送200响应:
response.writeHead(200);
// 将文件流导向response:
fs.createReadStream(filepath).pipe(response);
} else {
// 出错了或者文件不存在:
console.log('404 ' + request.url);
// 发送404响应:
response.writeHead(404);
response.end('404 Not Found');
}
});
});

server.listen(8080);
console.log('Server is running at http://127.0.0.1:8080/');
  • 在Node.js诞生后的短短几年里,出现了无数种Web框架、ORM框架、模版引擎、测试框架、自动化构建工具,数量之多,即使是JavaScript老司机,也不免眼花缭乱。

    1
    2
    3
    4
    5
    常见的Web框架包括:Express,Sails.js,koa,Meteor,DerbyJS,Total.js,restify……
    ORM框架比Web框架要少一些:Sequelize,ORM2,Bookshelf.js,Objection.js……
    模版引擎PK:Jade,EJS,Swig,Nunjucks,doT.js……
    测试框架包括:Mocha,Expresso,Unit.js,Karma……
    构建工具有:Grunt,Gulp,Webpack……
  • 用generator实现异步比回调简单了不少,但是generator的本意并不是异步。Promise才是为异步设计的,但是Promise的写法……想想就复杂。为了简化异步代码,ES7(目前是草案,还没有发布)引入了新的关键字async和await。这是JavaScript未来标准的异步代码。

    1
    2
    3
    4
    //轻松地把一个function变为异步模式
    async function () {
    var data = await fs.read('/file1');
    }
  • 由async标记的函数称为异步函数,在异步函数中,可以用await调用另一个异步函数,这两个关键字将在ES7中引入。要让Node.js运行ES7代码,需要把ES7代码“转换”为ES6代码,这样,Node.js就可以运行转换后的代码。这个转换工作可以用Babel实现。用Babel转码时,需要指定presets和plugins。presets是规则,我们stage-3规则,stage-3规则是ES7的stage 0~3的第3阶段规则。plugins可以指定插件来定制转码过程,一个preset就包含了一组指定的plugin。

  • 为什么先加载babel-core/register,再加载app.js,魔法就会生效?原因是第一个require()是Node正常加载babel-core/register的过程,然后,Babel会用自己的require()替换掉Node的require(),随后用require()加载的所有代码均会被Babel自动转码后再加载。
  • koa middleware—让我们再仔细看看koa的执行逻辑。核心代码是:
    1
    2
    3
    4
    5
    app.use(async (ctx, next) => {
    await next();
    ctx.response.type = 'text/html';
    ctx.response.body = '<h1>Hello, koa2!</h1>';
    });

每收到一个http请求,koa就会调用通过app.use()注册的async函数,并传入ctx和next参数。我们可以对ctx操作,并设置返回内容。但是为什么要调用await next()?原因是koa把很多async函数组成一个处理链,每个async函数都可以做一些自己的事情,然后用await next()来调用下一个async函数。我们把每个async函数称为middleware,这些middleware可以组合起来,完成很多有用的功能。
例如,可以用以下3个middleware组成处理链,依次打印日志,记录处理时间,输出HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
app.use(async (ctx, next) => {
console.log(`${ctx.request.method} ${ctx.request.url}`); // 打印URL
await next(); // 调用下一个middleware
});

app.use(async (ctx, next) => {
const start = new Date().getTime(); // 当前时间
await next(); // 调用下一个middleware
const ms = new Date().getTime() - start; // 耗费时间
console.log(`Time: ${ms}ms`); // 打印耗费时间
});

app.use(async (ctx, next) => {
await next();
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello, koa2!</h1>';
});

middleware的顺序很重要,也就是调用app.use()的顺序决定了middleware的顺序。
此外,如果一个middleware没有调用await next(),会怎么办?答案是后续的middleware将不再执行了。这种情况也很常见,例如,一个检测用户权限的middleware可以决定是否继续处理请求,还是直接返回403错误:

1
2
3
4
5
6
7
app.use(async (ctx, next) => {
if (await checkUserPermission(ctx)) {
await next();
} else {
ctx.response.status = 403;
}
});

理解了middleware,我们就已经会用koa了!(高内聚,低耦合)

  • 用post请求处理URL时,我们会遇到一个问题:post请求通常会发送一个表单,或者JSON,它作为request的body发送,但无论是Node.js提供的原始request对象,还是koa提供的request对象,都不提供解析request的body的功能!
  • Nunjucks是什么东东?其实它是一个模板引擎。模板引擎就是基于模板配合数据构造出字符串输出的一个组件。模板引擎最常见的输出就是输出网页,也就是HTML文本。当然,也可以输出任意格式的文本,比如Text,XML,Markdown等等。Nunjucks是Mozilla开发的一个纯JavaScript编写的模板引擎,既可以用在Node环境下,又可以运行在浏览器端。但是,主要还是运行在Node环境下,因为浏览器端有更好的模板解决方案,例如MVVM框架。如果你使用过Python的模板引擎jinja2,那么使用Nunjucks就非常简单,两者的语法几乎是一模一样的,因为Nunjucks就是用JavaScript重新实现了jinjia2。注意,模板引擎是可以独立使用的,并不需要依赖koa虽然模板引擎内部可能非常复杂,但是使用一个模板引擎是非常简单的,因为本质上我们只需要构造这样一个函数:
    1
    2
    3
    function render(view, model) {
    // TODO:...
    }

其中,view是模板的名称(又称为视图),因为可能存在多个模板,需要选择其中一个。model就是数据,在JavaScript中,它就是一个简单的Object。render函数返回一个字符串,就是模板的输出。

  • Nunjucks模板引擎最强大的功能在于模板的继承。仔细观察各种网站可以发现,网站的结构实际上是类似的,头部、尾部都是固定格式,只有中间页面部分内容不同。如果每个模板都重复头尾,一旦要修改头部或尾部,那就需要改动所有模板。Nunjucks的性能问题主要出现在从文件读取模板内容这一步。这是一个IO操作,在Node.js环境中,我们知道,单线程的JavaScript最不能忍受的就是同步IO,但Nunjucks默认就使用同步IO读取模板文件。好消息是Nunjucks会缓存已读取的文件内容,也就是说,模板文件最多读取一次,就会放在内存中,后面的请求是不会再次读取文件的,只要我们指定了noCache: false这个参数。在开发环境下,可以关闭cache,这样每次重新加载模板,便于实时修改模板。在生产环境下,一定要打开cache,这样就不会有性能问题。
  • 所有的第三方包都可以通过npm官网搜索并查看其文档:https://www.npmjs.com/
  • 集成Nunjucks:集成Nunjucks实际上也是编写一个middleware,这个middleware的作用是给ctx对象绑定一个render(view, model)的方法,这样,后面的Controller就可以调用这个方法来渲染模板了。
  • Node.js在全局变量process中定义了一个环境变量env.NODE_ENV,为什么要使用该环境变量?因为我们在开发的时候,环境变量应该设置为’development’,而部署到服务器时,环境变量应该设置为’production’。在编写代码的时候,要根据当前环境作不同的判断。
  • 我们在使用上面编写的处理静态文件的middleware时,也可以根据环境变量判断:
    1
    2
    3
    4
    if (! isProduction) {
    let staticFiles = require('./static-files');
    app.use(staticFiles('/static/', __dirname + '/static'));
    }

这是因为在生产环境下,静态文件是由部署在最前面的反向代理服务器(如Nginx)处理的,Node程序不需要处理静态文件。而在开发环境下,我们希望koa能顺带处理静态文件,否则,就必须手动配置一个反向代理服务器,这样会导致开发环境非常复杂。
nodejs处理静态文件只是为了方便调试,部署的时候前端放一个nginx,用nginx处理静态文件。

  • 运行前,我们再检查一下app.js里的middleware的顺序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //第一个middleware是记录URL以及页面执行时间:
    app.use(async (ctx, next) => {
    console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
    var
    start = new Date().getTime(),
    execTime;
    await next();
    execTime = new Date().getTime() - start;
    ctx.response.set('X-Response-Time', `${execTime}ms`);
    });
    //第二个middleware处理静态文件:
    if (! isProduction) {
    let staticFiles = require('./static-files');
    app.use(staticFiles('/static/', __dirname + '/static'));
    }
    //第三个middleware解析POST请求:
    app.use(bodyParser());
    //第四个middleware负责给ctx加上render()来使用Nunjucks:
    app.use(templating('view', {
    noCache: !isProduction,
    watch: !isProduction
    }));
    //最后一个middleware处理URL路由:
    app.use(controller());
  • 注意到ctx.render内部渲染模板时,Model对象并不是传入的model变量,而是:

    1
    Object.assign({}, ctx.state || {}, model || {})

这个小技巧是为了扩展。首先,model || {}确保了即使传入undefined,model也会变为默认值{}。Object.assign()会把除第一个参数外的其他参数的所有属性复制到第一个参数中。第二个参数是ctx.state || {},这个目的是为了能把一些公共的变量放入ctx.state并传给View。

  • node –inspect node.js 与 node node.js –inspect 有无效果
  • In the simplest terms, the tilde matches the most recent minor version (the middle number). ~1.2.3 will match all 1.2.x versions but will miss 1.3.0.The caret, on the other hand, is more relaxed. It will update you to the most recent major version (the first number). ^1.2.3 will match any 1.x.x release including 1.3.0, but will hold off on 2.0.0.

Comments

去留言
2017-02-08

⬆︎TOP