前言

这是我读《深入浅出Nodejs》的笔记,真是希望我的机械键盘快点到啊,累死我了。

CommonJS规范

主要分为模块引用、模块定义、模块标识三个部分。

模块引用

上下文提供require()方法来引入外部模块,示例代码如下:

//test.js
//引入一个模块到当前上下文中
var math = require('math');
math.add(1, 2);

模块定义

上下文提供了exports对象用于导入导出当前模块的方法或者变量,并且它是唯一的导出出口。模块中存在一个module对象,它代表模块自身,exports是module的属性。一个文件就是一个模块,将方法作为属性挂载在exports上就可以定义导出的方式:

//math.js
exports.add = function () {
var sum = 0, i = 0, args = arguments, l = args.length;
while(i < l) {
sum += args[i++];
}
return sum;
}

这样就可像test.js里那样在require()之后调用模块的属性或者方法了。

模块标识

模块标识就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以"."、".."开头的相对路径或者绝对路径,可以没有文件后缀名".js".

Node的模块实现

在Node中引入模块,需要经历如下三个步骤:

  1. 路径分析
  2. 文件定位
  3. 编译执行

在Node中模块分为两类:一是Node提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块。

核心模块是Node源码在编译过程中编译进了二进制执行文件。在Node启动时这些模块就被加载进内存中,所以核心模块引入时省去了文件定位和编译执行两个步骤,并且在路径分析中优先判断,因此核心模块的加载速度是最快的。

文件模块则是在运行时动态加载,速度比核心模块慢。

优先从缓存加载

和浏览器会缓存静态js文件一样,Node也会对引入的模块进行缓存,不同的是浏览器缓存的是文件,Node缓存的是编译执行之后的对象。

require()对相同模块的二次加载一律采用缓存优先的方式,这是第一优先级的,核心模块缓存检查先于文件模块的缓存检查。

路径分析和文件定位

因为标识符有几种形式,对于不同的标识符,模块的查找和定位有不同程度上的差异。

1.模块标识符分析

模块标识符在Node中主要分为以下几类:

  • 核心模块
  • 相对路径文件模块
  • 绝对路径文件模块
  • 非路径形式的文件模块
核心模块

核心模块优先级仅次于缓存加载,因此无法加载一个和核心模块标识符相同的自定义模块。

路径形式的文件模块

以"."、".."开头和"/"开始的标识符,这里都被当作文件模块来处理。require()方法会将路径转为真实路径,并以真实路径作为索引,并将编译执行后的结果存放到缓存中。

自定义模块

自定义模块是指非核心模块,也不是路径形式的标识符。它是一种特殊的文件模块,可能是一个文件或者包的形式。

模块路径是Node在定位文件模块的具体文件时制定的查找策略,具体表现为一个路径组成的数组(module.paths)。这个路径由当前目录开始往上一直到根目录,Node会逐个尝试模块路径中的路径,直到找到目标文件未知,若到达根目录还是没有找到目标文件,则会抛出查找失败的异常。当前文件的目录越深,模块查找耗时越多。

2.文件定位

文件扩展名分析

调用require()方法时若参数没有文件扩展名,Node会按.js、.json、.node的顺寻补足扩展名,依次尝试。

在尝试过程中,需要调用fs模块阻塞式地判断文件是否存在。因为Node是单线程的,这是一个会引起性能问题的地方。如果是.node或者.json文件可以加上扩展名加快一点速度。另一个诀窍是:同步配合缓存。

目录分析和包

require()分析文件扩展名后,可能没有查到对应文件,而是找到了一个目录,此时Node会将目录当作一个包来处理。

首先, Node在挡墙目录下查找package.json,通过JSON.parse()解析出包秒速对象,从中取出main属性指定的文件名进行定位。若main属性指定文件名错误,或者没有pachage.json文件,Node会将index当作默认文件名。

若目录分析没有定位成功任何文件,则自定义模块进入下一个模块路径。

模块编译

每个模块文件模块都是一个对象,它的定义如下:

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 = [];
}

对于不同扩展名,其载入方法也有所不同:

  • .js 通过fs模块同步读取文件后编译执行。
  • .node 这是C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件
  • .json 同过fs模块同步读取文件后,用JSON.pares()解析返回结果
  • 其他 当作.js

每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上。

.json文件调用的方法如下:

//Native extension for .json
Module._extensions['.json'] = function(module, filename) {
var content = NativeModule.require('fs').readFileSync(filename, 'utf-8');
try {
module.exports = JSON.parse(stripBOM(content));
} catch(err) {
err.message = filename + ':' + err.message;
throw err;
}
}

Module._extensions会被赋值给require()的extensions属性,所以可以用:console.log(require.extensions);输出系统中已有的扩展加载方式。

当然也可以自己增加一些特殊的加载:require.extensions['.txt'] = function(){//code};

但是从v0.10.6版本开始官方不鼓励通过这种方式自定义扩展名加载,而是期望先将其他语言或文件编译成JavaScript文件后再加载,这样的好处在于不讲烦琐的编译加载等过程引入Node的执行过程。

1.JavaScript模块的编译

在编译的过程中,Node对获取的javascript文件内容进行了头尾包装,将文件内容包装在一个function中:

(function (exports, require, module, _filename, _dirname) {
//js文件内容
});

包装之后的代码会通过vm原生模块的runInThisContext()方法执行(具有明确上下文,不污染全局),返回一个具体的function对象,最后传参执行,执行后返回model.exports.

核心模块

核心模块分为C/C++编写和JavaScript编写的两个部分,其中C/C++文件放在Node项目的src目录下,JavaScript文件放在lib目录下。

JavaScript核心模块的编译过程

1.转存为C/C++代码

Node采用了V8附带的js2c.py工具,将所有内置的JavaScript代码转换成C++里的数组,生成node_natives.h头文件:

namespace node {
const char node_native[] = { 47, 47, ..};
const char dgram_native[] = { 47, 47, ..};
const char console_native = { 47, 47, ..};
const char buffer_native = { 47, 47, ..};
const char querystring_native = { 47, 47, ..};
const char punycode_native = { 47, 47, ..};
...
struct _native {
const char* name;
const char* source;
size_t source_len;
} static const struct _native natives[] = {
{ "node", node_native, sizeof(node_native)-1},
{ "dgram", dgram_native, sizeof(dgram_native)-1},
...
};
}

在这个过程中,JavaScript代码以字符串形式存储在node命名空间中,是不可直接执行的。在启动Node进程时,js代码直接加载到内存中。在加载的过程中,js核心模块经历标识符分析后直接定位到内存中。

2.编译js核心模块

lib目录下的模块文件也在引入过程中经历了头尾包装的过程,然后才执行和导出了exports对象。与文件模块的区别在于:获取源代码的方式(核心模块从内存加载)和缓存执行结果的位置。

js核心模块源文件通过process.binding('natives')取出,编译成功的模块缓存到NativeModule._cache上。代码如下:

function NativeModule() {
this.filename = id + '.js';
this.id = id;
this.exports = {};
this.loaded = fales;
}
NativeModule._source = process.binding('natives');
NativeModule._cache = {};

《深入浅出Nodejs》笔记——模块机制(1)的更多相关文章

  1. 浅谈NodeJs的模块机制

    J历史 我们都知道,js在刚被创建的时候,只是为了在网页上写一些小脚本而已,比如网页特效,表单验证等等,创立者也许没觉悟到以后的js会发展到如此规模.这是web1.0时代. 在web 2.0时代,各种 ...

  2. 《深入浅出Nodejs》笔记——模块机制(2)

    前言 书上还有很大一部分讲了C/C++模块的编译过程.核心模块编写和C/C++扩展模块的内容,不过我对C++一窍不通因此没有仔细看,如果以后需要再自习看吧. 包与NPM 第三方模块中,模块和模块之间是 ...

  3. 深入浅出node(2) 模块机制

    这部分主要总结深入浅出Node.js的第二章 一)CommonJs 1.1CommonJs模块定义 二)Node的模块实现 2.1模块分类 2.2 路径分析和文件定位 2.2.1 路径分析 2.2.2 ...

  4. nodejs笔记--模块篇(三)

    文件模块访问方式通过require('/文件名.后缀')    require('./文件名.后缀')    requrie('../文件名.后缀') 去访问,文件后缀可以省略:以"/&qu ...

  5. 【读书笔记】《深入浅出nodejs》第二章 模块机制

    1.什么是模块? 指在程序设计中,为完成某一功能所需的一段程序或子程序:或指能由编译程序.装配程序等处理的独立程序单位:或指大型软件系统的一部分. ----<百度百科> 2.JavaScr ...

  6. 深入浅出Nodejs读书笔记

    深入浅出Nodejs读书笔记 转:http://tw93.github.io/2015-03-01/shen-ru-qian-chu-nodejs-reading-mind-map.html cate ...

  7. Nodejs:Node.js模块机制小结

    今天读了<深入浅出Nodejs>的第二章:模块机制.现在做一个简单的小结. 序:模块机制大致从这几个部分来讲:JS模块机制的由来.CommonJS AMD CMD.Node模块机制和包和n ...

  8. 通过Anuglar Material串串学客户端开发 - NodeJS模块机制之Module.Exports

    module.exports 前文讲到在Angular Material的第二个编译文件docs/gulpfile.js中却看到了一个奇怪的东西module.exports那么module.expor ...

  9. 深入浅出Nodejs读书笔记(转)

    Node简介 这一章简要介绍了Node,从中可以了解Node的发展历程及其带来的影响和价值. 为什么叫Node?起初,Ryan Dahl称他的项目为web.js,就是一个Web服务器,但是项目的发展超 ...

随机推荐

  1. Makefile $@,$^,$ 作用

    /* main.c */        #include "mytool1.h"        #include "mytool2.h"        int  ...

  2. Linux系统查看系统信息

    1. CPU # lscpu # cat /proc/cpuinfo //可以知道每个cpu信息,如每个CPU的型号,主频等 2. 内存 # free -m # cat /proc/meminfo / ...

  3. pyttsx3 winsound win32api.MessageBox使用案例

    import requests,time from lxml import etree import win32api,win32con import winsound import pyttsx3 ...

  4. JAVA JDBC(存储过程和事务管理)

    1.什么是存储过程 存储过程(Stored Procedure)是在大型数据库系统中,一组为了完成特定功能的SQL 语句集,存储在数据库中,经过第一次编译后再次调用不需要再次编译,用户通过指定存储过程 ...

  5. Mantis 从Windows 迁移到Linux上

    1. 导出windows manits的mysql数据库文件, 在cmd运行:mysqldump -uroot -p3edc$RFV bugtracker > C:/mantis.sql; 2. ...

  6. 【spoj1182/usaco-Cow Queueing, 2003 Dec-二进制编号】数位dp

    题意:定义新的排序:先按一个数中二进制中1的个数从小到大排序,如果1的个数相同则按数的大小从小到大排序.问[A,B]之间有第K大的数是哪个.-2^31<=A,B<=2^31(A,B必定同正 ...

  7. margin 居中

    左右auto加个宽度.margin-left: auto; margin-right: auto; width:640px;

  8. 如何使用webpack打包你的项目

    webpack是前端开发中比较常用的打包工具之一,另外还有gulp,grunt.之前没有涉及过打包这块,这里介绍一下使用webpack打包的流程. Grunt和Gulp的工作方式是:在一个配置文件中, ...

  9. 玩一下易语言 "和"字有多种读音,注定了它的重要性!!

    变量名 类型 静态 数组 备注 拼音 文本型   0   测试的汉字 文本型       有几种发音 整数型       i 整数型       测试用的汉字 = “和” 有几种发音 = 取发音数目 ...

  10. Android跳转到拨打电话的页面

    在Android6.0之后,拨打电话需要用户授予动态权限,项目中有此需求,有一种简单的方法,直接携带电话号码跳转到系统拨打电话的页面,很多应用也是这么做的,这样可以减轻工作量 代码如下: Androi ...