Node 开发基础 – 1

这一系列是借实际项目实践Node服务端开发过程。最终目标将在Node环境中实现一个SSR(Server-Side Rendering)工程。

第1期 Node Runs!
本期简单介绍一些Node特性,以及与日后功能相关的基础开发。

组件构成及特点


Node 与 Chrome 很相似,具有基于事件驱动的异步架构(异步I/O特性)。

以读取文档为例:

var fs = require('fs');

fs.readFile('/path/1', function (err, file) {
    console.log('file1 loaded');
});
console.log('reading file1');

fs.readFile('/path/2', function (err, file) {
    console.log('file2 loaded');
});
console.log('reading file2');

// 输出
/*
reading file1
reading file2
file1 loaded
file2 loaded
*/

这种异步I/O特性将为后面SSR提供天时地利。但并不是万能,瓶颈在于单线程!
JavaScript在浏览器中是单线程,即便开启了WebWorker,由于线程之间不能共享数据,所以只能独立开展工作。
Node的短板在于不能利用多核CPU、错误引起应用breakdown、密集计算也会影响异步I/O调用。

什么是密集计算?

for (let i = 0; i < 1000000; i++) {
    console.log(i);
}

由此,Node应对以上问题,采用了子进程的思路,类似于WebWorker。子进程间通过消息通信,并由Master-Worker方式管理(这个后面会在实践中讲解)。

最后,关于Node跟其他环境算力的比较,我就不上图了,原因是没有场景的比较是耍流氓,具体情况还是具体分析。对于某些关注TTFB的应用,应该考虑这个问题。

规范与实现

都知道Node上支持CommonJS,不过并不是严格的实现。简单解析一下:
CommonJS分三部分,模块引用、模块定义、模块标识。

// require(/*模块标识*/) ,模块标识使用小驼峰命名法,或者是相对/绝对路径
let math = require('math');
// exports 对象用于导出当前模块的方法或变量
exports.add = function () {
    let sum = 0,
        i = 0,
        args = arguments,
        l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
}
// 模块的使用(假设math.js处于同级目录)
let math = require('./math');
math.add(1,2,3,4,5);

在CommonJS模块中,存在着require、exports、module3个变量,而exports 就是 module.exports。
这套模块机制可以对标命名空间这类的方案,主要减少了变量污染、使引用关系更清晰、类聚能力变强等。

Node在以上过程中,做了些许的变化以更利于自身。

模块

分为 核心模块、文件模块(用户模块);
核心模块在Node进程启动时就载入内存方便之后的调用。文件模块运行时动态加载,需要路径分析、文件定位、编译执行等过程,需要考虑开销。
由于核心模块的加载优先级高于文件模块,所以自定义一个与核心模块同标识符的模块,是无法被加载的,除非更换路径。

模块加载顺序

缓存加载:
Node对已加载的模块会进行缓存,所以每次加载的时候检查是否为二次加载从而减少重复浪费。并且Node缓存了模块编译执行的结果对象,可以直接拿来用。
->
路径分析:
先对标识符进行分析,然后开始模块查找。如果require的是路径+标识符的模块,就会按文件模块进行处理。Node中,路径藏越深,查找时间就越慢。
->
文件定位:
这一步主要分析文件扩展名,fs是查找模块的模块,它会判断统一路径下 .js、.json、.node 是否存在,所以如果要加载的文件是.json和.node,加上扩展名定位效率更高。
另外,还有一种情况,Node如果没有分析出可用扩展名对应的文件,但得到一个目录,此时Node将目录视为一个包来处理。包规范也沿袭CommonJS,首先Node会查找package.js,通过JSON.parse解析出包描述对象,从中取出main属性指定文件名进行定位,这一步就回到了分析文件扩展名的过程。如果还是什么都没找到,就抛出异常。
->
模块编译:
Node编译根据后缀名不同而不同。
.js :用fs模块同步读取后编译;
.node :C/C++编写的扩展,通过dlopen()方法加载后编译生成文件,也就是走编译那一套。
.json :fs模块同步读取后用JSON.parse()解析返回结果。
其他扩展名都当js文件载入。
每个模块编译后会将“文件路径”作为索引缓存在Module._cache上,这就是加载顺序中读取缓存的位置。
每个模块的编译方法都能找到源码,我就不详细写出了,在网上查到源码部分都能看懂。查询的关键字:Module._extensions
【技巧】
如果想对自定义扩展名进行特殊加载,可以通过 require.extentions[‘.ext’]的方式实现。官方不推崇这种做法的原因是,不同的脚本编译性能是无法保证的,转化成JS脚本会更好(希望总归是希望)。

在Node中,编译完的模块变成了模块对象,Node对模块对象进行定义:

function Module(id, parent) {
    this.id = id;
    this.exports = {};
    this.parent = parent;
    if (parent && parent.children) {
        parent.children.push(this);
    }
    this.filename = null
    this.loaded = false;
    this.children = [];
}

Module执行大概如下:

Module.math = function () {
    Module._load(process.argv[1], null, true)
}

let module = new Module(id, parent);

module.load(file)

JS模块编译

这个得细细说。
在每个模块文件中可以访问 require、exports、module 这三个变量(CommonJS规范中的三个),根据Node API定义,增加了 __filename、__dirname 。
这些变量对于模块来说都是内部可访问的,但并不会污染全局变量。
编译过程,是Node对模块进行了封装:

(function (exports, require, module, __filename, __dirname) {
    // 以下是模块内容
    var math = require('math');
    exports.area = function (radius) {
        return Math.PI * radius * radius;
    }
    // 以上是模块内容
});

这些被包装的模块代码会通过vm模块的runInThisContext()方法执行(vm模块提供了沙盒环境),然后返回一个具体的function对象(就是上面这一堆)。
这个function的参数,就是exports, require, module, __filename, __dirname这些。

Node 模块与前端模块的异同

(function () {
    // Establish the root object, `window` in the browser, or `global` on the server.
    var root = this;
    var _ = function (obj) {
            return new wrapper(obj);
        };
    if (typeof exports !== 'undefined') {
        if (typeof module !== 'undefined' && module.exports) {
            exports = module.exports = _;
        }
        exports._ = _;
    } else if (typeof define === 'function' && define.amd) {
        // Register as a named module with AMD.
        define('underscore', function () {
            return _;
        });
    } else {
        root['_'] = _;
    }
}).call(this);

以上代码片段抽取自著名类库underscore的定义方式。
首先,它通过function定义构建了一个闭包,将this作为上下文对象直接call调用,以避免内部变量污染到全局作用域。续而通过判断exports是否存在来决定将局部变量绑定给exports,并且根据define变量是否存在,作为处理在实现了AMD规范环境下的使用案例。仅只当处于浏览器的环境中的时候,this指向的是全局对象(window对象),才将变量赋在全局对象上,作为一个全局对象的方法导出,以供外部调用。

所以在设计前后端通用的JavaScript类库时,都有着以下类似的判断:

if (typeof exports !== "undefined") {
    exports.EventProxy = EventProxy;
} else {
    this.EventProxy = EventProxy;
}

即,如果exports对象存在,则将局部变量挂载在exports对象上,如果不存在,则挂载在全局对象上。

以上,是Node的一些基础内容,必须掌握。

发表评论

电子邮件地址不会被公开。 必填项已用*标注