web2.0时代,B/S表现出比C/S更大优势,JS的作用经历了
工具类库->功能组件->框架->应用 的迁移。但却始终缺少一个模块机制:
CJS规范
1. CJS出发点
ECMAScript规范只涉及JS语言的基本要素: 词法 作用域 上下文 类型等。浏览器端有W3C主推的HTML5规范给JS带来了多种标准API。社区总结了后端JS的规范: CJS。它给JS带来了 模块系统 包管理系统 Buffer 文件系统 IO流 字符集编码 进程环境 套接字 Web服务器网关等要素。
CJS规范是一种理论,一个凝结社区努力的优秀的设计。Node正是对这一个规范的优秀实现。
Node与CJS、浏览器、W3C的关系
2. CJS模块规范
CJS规范的模块系统包括: 模块引入 模块导出 模块标识
模块引入:require(模块标识)模块导出:一个文件一个模块(模块本身module),挂载到exports上实现导出模块标识:相对路径/绝对路径
CJS模块意义在于将API限定在私有作用域,通过导出 引入实现上下游依赖。
Node的模块实现
Node实现 模块加载分为:
路径分析文件定位编译执行
Node模块分为两种:核心模块 文件模块。针对这两种类型的模块,核心模块在Node源码编译过程中,加载过程不需要文件定位和编译执行;文件模块则需要在运行时完整经历这三步;所以核心模块比文件模块加载速度快。
1. 优先从缓存加载
类似于浏览器缓存文件,Node中对二次引入的模块也会缓存其编译和执行之后的对象,以减少二次引入的开销。
2. 路径分析和文件定位
路径分析即通过require()+模块标识符获取模块路径的过程。
- 模块标识符分析
根据标识符可将模块分为:核心模块:Node源码编译阶段已经包含了核心模块路径形式文件模块:包括相对路径绝对路径两种类型的模块,都是在路径分析时解析为真实路径非路径形式文件模块:包含自定义模块,各种包等。其模块路径是一个数组。从当前文件所在目录开始一层层向上查找node_modules文件夹,直到找到未知。
console.log(module.paths); // [ '/Users/hb/tzy/daily-fe/daily-fe-node/src/node_modules', // '/Users/hb/tzy/daily-fe/daily-fe-node/node_modules', // '/Users/hb/tzy/daily-fe/node_modules', // '/Users/hb/tzy/node_modules', // '/Users/hb/node_modules', // '/Users/node_modules', // '/node_modules' ] - 文件定位
注意
文件扩展名和目录和包的处理。- 文件没有后缀,按照
.js .json .node进行补足。 - 若定位到一个
目录,当做包来处理:- 查找包描述文件:
package.json - 查找入口文件:
package.json的main字段;若不存在main字段,则查找index文件。
- 查找包描述文件:
- 文件没有后缀,按照
3. 模块编译
模块载入
根据扩展名不同,载入方式也有所不同:
- .js:fs 同步读取后编译模块
- .json:fs 同步读取后JSON.parse()
- .node:xxx
载入函数在Module._extensions上,可通过require._extensions查看。
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(stripBOM(content), filename);
};
模块编译
注意这里的模块编译只针对文件模块。
- JS模块编译
module._compile()方法里:
// ...
const wrapper = Module.wrap(content);
compiledWrapper = vm.runInThisContext(wrapper, {
filename,
lineOffset: 0,
displayErrors: true,
importModuleDynamically: experimentalModules ? async (specifier) => {
if (asyncESM === undefined) lazyLoadESM();
const loader = await asyncESM.loaderPromise;
return loader.import(specifier, normalizeReferrerURL(filename));
} : undefined,
});
// ...
在Module.wrap()函数里:
let wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
const wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
将模块代码进行头尾包装,前面的compiledWrapper就是vm.runInThisContext()执行后生成的一个具体的function对象,即:
(function (exports, require, module, __filename, __dirname) {
require('./m1');
console.log(module);
});
这个function最后被执行一次,参数是exports模块导出 require模块引入函数 this即module本身 filename dirname并且将返回值传递给调用方:
result = inspectorWrapper(
compiledWrapper, // 该function
this.exports,
this.exports, // 参数
require,
this,
filename,
dirname
);
Node中,++每个模块都是一个对象++。编译成功的模块将以文件路径为索引存在Module._cache对象上。
- C/C++模块编译
.node模块是C/C++模块编译生成,只有加载和执行的过程,不需编译。
// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
return process.dlopen(module, path.toNamespacedPath(filename));
};
执行方法是process.dlopen()。
- JSON模块编译
// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
try {
module.exports = JSON.parse(stripBOM(content));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};
fs模块 同步读取后JSON.parse()。
核心模块
核心模块包括JS核心(Nodelib目录内)和C/C++核心(Nodesrc目录内)。
JS核心模块编译
- 转存为C/C++
- JS核心模块编译
C/C++核心模块编译
- 内建模块的组织形式
- 内建模块的导出
核心模块引入流程
os原生模块引入流程
编写核心模块
...
C/C++扩展模块
...
模块调用栈
从上图可以看出:
C/C++扩展模块也是文件模块的一种,提供给JS文件模块调用C/C++核心模块处于最底层,提供给JS核心模块和JS文件模块调用JS核心模块有两个作用:C/C++核心模块与JS文件模块中间的桥接- 普通功能模块
包与NPM
包与NPM是将模块联系起来的一种机制,是对CJS包规范(包含包结构和包描述)的一种实现。
1. 包结构
包是一个存档文件(tar.gz/.zip),安装后还原为目录,完全符合CJS包规范的包结构应该包括:
package.json文件lib目录存放JS代码bin目录存放二进制命令test目录doc目录
2. NPM与包描述文件
NPM的所有行为都与包描述文件的字段息息相关,需要注意的有:
main字段:包的出口模块文件,也就是包中其他模块的入口。
3. NPM常用功能
查看帮助
npm -h
安装依赖
全局安装
-g意思是将包安装为全局可用的可执行命令(以commitizen举例):
- 包安装位置为
/usr/local/lib/node_modules/commitizen - 根据包描述文件的
bin字段在/usr/local/lib下创建commitizen可执行文件的链接。
本地安装
NPM钩子命令
"scripts": {
"preinstall": "preinstall.js",
"install": "install.js",
"uninstall": "uninstall.js",
"test": "test.js"
}”
发布包
npm publish
NPM初始化
npm init
编写模块
注册账号
npm adduser
上传包
npm publish
安装包
包权限
npm owner
分析包
npm ls
前后端共用模块
Node使用JS实现后,前后端便可以通用JS模块。
1. 模块的侧重点
前后端JS模块的侧重点不同,因为其宿主环境不同导致:
- 前后端代码分布在
HTTP两端,前端JS模块需要服务端下发给不同的客户端(瓶颈在带宽),后端JS模块则是直接从磁盘读取,然后执行多次(瓶颈在性能)。来源不同,所以两者的加载速度不在一个量级。
2. AMD规范
3. CMD规范
4. 兼容多种模块规范
根据环境字段,使用不同的加载方式