node 的模块运行机制
1.CommonJS 的规范
CommonJS 的规范,包括模块引用,模块定义,模块标识,3个部分
模块引用: 模块通过require方法来同步加载所依赖的模块
模块定义: 在node中一个文件就是一个模块,提供exports对象导出当前模块的方法或变量
模块标识: 模块标识传递给require()方法的参数,可以是按小驼峰(camelCase)命名的字符串,也可以是文件路径。
1.1.node 模块中CommonJS 的应用
模块内容导出两种方式:
a.js的内容如下,
**方式一:**可将需要导出的变量或函数挂载到 exports 对象的属性上
// node.js 每一个文件都是一个单独模块
// Node对获取的Javascript文件的内容进行了包装,以传入如下变量
console.log(exports, require, module, __filename, __dirname);
// 可将需要导出的变量或函数挂载到 exports 对象的属性上,
exports.name = 'luoxiaobu';
exports.age = '18'
**方式二:**使用 module.exports 对象整体导出一个变量对象或者函数
// node.js 每一个文件都是一个单独模块
// Node对获取的Javascript文件的内容进行了包装,以传入如下变量
console.log(exports, require, module, __filename, __dirname);
let name = 'luoxiaobu';
let age = '18'
// 使用 module.exports 对象整体导出一个变量对象或者函数,
module.exports = {name,age};
模块的引用的方式: 按照引用模块的来源来区分
// 核心模块的引入 node自己的模块
let crypto = require('crypto')
// 用户自己编写的模块引入
let aModule = require('./a.js')
// 第三方,别人实现发布的模块(其实也是其他用户编写)
let proxy = require('http-proxy');
2.node 模块加载过程
node.js 每一个文件都是一个单独模块,每个模块都用一个module对象来表示自身。
非 node NativeModule
// 非 node NativeModule
function Module(id = '', parent) {
this.id = id;
this.path = path.dirname(id);
this.exports = {};
this.parent = parent;
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
this.children = [];
}
NativeModule
// Set up NativeModule.
function NativeModule(id) {
this.filename = `${id}.js`;
this.id = id;
this.exports = {};
this.module = undefined;
this.exportKeys = undefined;
this.loaded = false;
this.loading = false;
this.canBeRequiredByUsers = !id.startsWith('internal/');
}
2.1 node 模块加载简述
加载过程大概流程:(Module._load 加载函数)
参考源码:node/lib/internal/modules/cjs/loader.js
代码略微删减
// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call
// `NativeModule.prototype.compileForPublicLoader()` and return the exports.
// 3. Otherwise, create a new module for the file and save it to the cache.
// Then have it load the file contents before returning its exports
// object.
Module._load = function(request, parent, isMain) {
let relResolveCacheIdentifier;
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
...
}
// 查找文件具体位置
const filename = Module._resolveFilename(request, parent, isMain);
// 存在缓存,则不需要再次执行 返回缓存
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
if (!cachedModule.loaded)
return getExportsForCircularRequire(cachedModule);
return cachedModule.exports;
}
// 加载node原生模块,原生模块loadNativeModule
// 如果有 且能被用户引用 返回 mod.exports(这包括node模块的编译创建module对象,将模块运行结果保存在module对象上)
const mod = loadNativeModule(filename, request);
if (mod && mod.canBeRequiredByUsers) return mod.exports;
// 创建一个模块
// Don't call updateChildren(), Module constructor already does.
const module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
// 缓存模块
Module._cache[filename] = module;
if (parent !== undefined) {
relativeResolveCache[relResolveCacheIdentifier] = filename;
}
// 加载执行新的模块
module.load(filename);
return module.exports;
};
node 缓存的是编译和执行后的对象
相同:
node模块和非node模块经历的过程都是,有执行后的缓存对象,返回缓存对象
没有执行后的缓存对象,创建module对象,执行模块,存储执行后得到的对象,返回执行后的结果exports
不同:
缓存对象不同
加载模块文件方式不同
2.2 node 源码目录
大概源码结构:(只标注了部分感兴趣的)
我们可以看到 node 库的目录,其中:
deps: 包含了node所依赖的库,如v8,libuv,zlib 等,
lib: 包含了用 javascript 定义的函数和模块(可能会通过internalBinding调用c++模块,c++ 模块实现在目录src 下),
src: 包括了lib 库对应的C++实现,其中很多 built-in(C++实现) 模块都在这里
会有所困惑,js跟c++ 之间的相互调用?
Node.js主要包括这几个部分,Node Standard Library,Node Bindings,V8,Libuv,架构图如下:
Node Bindings: 是沟通JS 和 C++的桥梁,将V8 引擎暴露的c++ 接口转换成JS API
V8: JavaScript的引擎,提供JavaScript运行环境
c++ 模块的引用大概流程
C++ 和 JS 交互 参考文章:(感兴趣可以了解一下)
2.3 node 模块分类
参考:node/lib/internal/bootstrap/loaders.js
// This file creates the internal module & binding loaders used by built-in
// modules. In contrast, user land modules are loaded using
// lib/internal/modules/cjs/loader.js (CommonJS Modules) or
// lib/internal/modules/esm/* (ES Modules).
//
// This file is compiled and run by node.cc before bootstrap/node.js
// was called, therefore the loaders are bootstraped before we start to
// actually bootstrap Node.js. It creates the following objects:
node.cc编译和运行的node/lib/internal/bootstrap/loaders.js 文件的时候会,创建内部模块, 绑定内置模块使用的加载程序。
而用户的模块加载运行依靠lib/internal/modules/cjs/loader.js 或者 lib/internal/modules/esm/*
所以node的模块大致分为两类:
- node的核心模块
- node的核心模块js实现
- node核心模块c++实现,js包裹调用c++模块
- 第三方模块,或者用户自己编写模块
- JavaScript 模块,我们开发写的JavaScript 模
- JSON 模块,一个 JSON 文件
- C/C++ 扩展模块,使用 C/C++ 编写,编译后后缀名为 .node(感兴趣可以了解动态链接库)
2.3.1 node的核心模块
⑴ C++ binding loaders: (对c++核心模块的引入,暴露的c++ 接口)
process.binding(): 旧版C ++绑定加载程序,可从用户空间访问,因为它是附加到全局流程对象的对象。这些C ++绑定是使用NODE_BUILTIN_MODULE_CONTEXT_AWARE()创建的,并且其nm_flags设置为NM_F_BUILTIN。我们无法确保这些绑定的稳定性,但是仍然必须时时解决由它们引起的兼容性问题。
process._linkedBinding(): 在应用程序中添加额外的其他C ++绑定。可以使用带有标志NM_F_LINKED 的 NODE_MODULE_CONTEXT_AWARE_CPP()创建这些C ++绑定。
internalBinding(): 私有内部C ++绑定加载程序,(除非通过require('internal / test / binding')
,否则无法从用户区域访问)。 这些C ++绑定是使用NODE_MODULE_CONTEXT_AWARE_INTERNAL()创建的,其nm_flags设置为NM_F_INTERNAL。
⑵Internal JavaScript module loader:
该模块是用于加载 lib.js 和 deps.js 中的JavaScript核心模块的最小模块系统。
所有核心模块都通过由 js2c.py 生成的 node_javascript.cc 编译成 Node 二进制文件,这样可以更快地加载它们,而不需要I/O成本。
此类使lib / internal / ,deps / internal /模块和internalBinding()在默认情况下对核心模块可,并允许核心模块通require(‘internal / bootstrap / loaders’)来引用自身,即使此文件不是以CommonJS风格编写的。
核心模块的加载大概如下:(internalBinding是process.binding的替代可以简单这样理解)
Process.binding / InternalBinding 实际上是C++函数,是用于将Node标准库中C++端和Javascript端连接起来的桥梁。
2.3.2 node的非 核心模块
- JavaScript 模块,我们开发写的JavaScript 模(或着第三方模块)
- JSON 模块,一个 JSON 文件
- C/C++ 扩展模块,使用 C/C++ 编写,编译后后缀名为 .node(感兴趣可以了解动态链接库)
此类模块的大概加载流程:
路径分析
const filename = Module._resolveFilename(request, parent, isMain);复制代码
是否有缓存
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
创建module对象
const module = new Module(filename, parent);
// 缓存 module 对象
Module._cache[filename] = module;复制代码
文件定位根据后缀编译执行
// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
if (experimentalModules && filename.endsWith('.js')) {
const pkg = readPackageScope(filename);
if (pkg && pkg.type === 'module') {
throw new ERR_REQUIRE_ESM(filename);
}
}
const content = fs.readFileSync(filename, 'utf8');
module._compile(stripBOM(content), filename);
};
// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');
if (manifest) {
const moduleURL = pathToFileURL(filename);
manifest.assertIntegrity(moduleURL, content);
}
try {
module.exports = JSON.parse(stripBOM(content));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};
// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
if (manifest) {
const content = fs.readFileSync(filename);
const moduleURL = pathToFileURL(filename);
manifest.assertIntegrity(moduleURL, content);
}
// Be aware this doesn't use `content`
return process.dlopen(module, path.toNamespacedPath(filename));
};
返回module.exports 对象。
模块路径解析规则
我们已经知道,require函数支持斜杠(/)或盘符(C:)开头的绝对路径,也支持./开头的相对路径。但这两种路径在模块之间建立了强耦合关系,一旦某个模块文件的存放位置需要变更,使用该模块的其它模块的代码也需要跟着调整,变得牵一发动全身。因此,require函数支持第三种形式的路径,写法类似于foo/bar,并依次按照以下规则解析路径,直到找到模块位置。
1. 内置模块
如果传递给 require 函数的是 NodeJS 内置模块名称,不做路径解析,直接返回内部模块的导出对象,例如require(‘fs’)。
2. node_modules 目录
NodeJS 定义了一个特殊的 node_modules 目录用于存放模块。例如某个模块的绝对路径是 /home/user/hello.js,在该模块中使用 require(‘foo/bar’) 方式加载模块时,则 NodeJS 依次尝试使用以下路径。
/home/user/node_modules/foo/bar
/home/node_modules/foo/bar
/node_modules/foo/bar
3. NODE_PATH 环境变量
与 PATH 环境变量类似,NodeJS 允许通过 NODE_PATH 环境变量来指定额外的模块搜索路径。NODE_PATH 环境变量中包含一到多个目录路径,路径之间在 Linux 下使用:分隔,在 Windows 下使用;分隔。例如定义了以下 NODE_PATH 环境变量:
NODE_PATH=/home/user/lib:/home/lib
当使用 require(‘foo/bar’)的方式加载模块时,则 NodeJS 依次尝试以下路径。
/home/user/lib/foo/bar
/home/lib/foo/bar
Node.js包
Node组织了自身的核心模块,也使得第三方文件模块可以有序地编写和使用。但是在第三方模块中,模块与模块之间仍然是散列在各地的,相互之间不能直接引用。而在模块之外,包和NPM则是将模块联系起来的一种机制。JavaScript不似Java或者其他语言那样,具有模块和包结构。Node对模块规范的实现,一定程度上解决了变量依赖、依赖关系等代码组织性问题。包的出现,则是在模块的基础上进一步组织JavaScript代码。CommonJS的包规范的定义其实也十分简单,它由包结构和包描述文件两个部分组成,前者用于组织包中的各种文件,后者则用于描述包的相关信息,以供外部读取分析。
1. 包结构
包实际上是一个存档文件,即一个目录直接打包为.zip或tar.gz格式的文件,安装后解压还原为目录。完全符合CommonJS规范的包目录应该包含如下这些文件。
1、package.json:包描述文件
2、bin:用于存放可执行二进制文件的目录
3、lib:用于存放JavaScript代码的目录
4、doc:用于存放文档的目录
5、test:用于存放单元测试用例的代码
2. 包描述文件package.json
初始化,生成包,依次输入相关字段
npm init
package.json文件,定义了项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等元数据)。npm install命令根据这个配置文件,自动下载所需的模块,也就是配置项目所需的运行和开发环境。
有了package.json文件,直接使用npm install
或yarn install
,就会在当前目录中安装所需要的模块。
如果一个模块不在package.json文件之中,可以单独安装这个模块,并使用相应的参数,将其写入package.json文件之中。
npm install koa --save
npm install koa --save-dev
上面代码表示单独安装koa模块,–save参数表示将该模块写入dependencies属性,–save-dev表示将该模块写入devDependencies属性,我更喜欢用yarn做包管理工具。
yarn add koa
yarn add koa --dev
【基本字段】
{
"name": "api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
1、name——包名。规范定义它需要由小写的字母和数字组成,可以包含.、_和-,但不允许出现空格。包名必须是唯一的,以免对外公布时产生重名冲突的误解。除此之外,NPM还建议不要在包名中附带上node或js来重复标识它是JavaScript或Node模块 。
2、version——版本号。一个语义化的版本号,这在http://semver.org/上有详细定义,通常为major.minor.revision格式。该版本号十分重要,常常用于一些版本控制的场合。
【必需字段】
1、description——包简介。方便别人了解该模块作用,搜索的时候也有用
“description”: “包介绍”
2、keywords——关键词数组,NPM中主要用来做分类搜索。一个好的关键词数组有利于用户快速找到该包。
3、maintainers——包维护者列表。每个维护者由name、email和web地址这3个属性组成。
4、contributors——贡献者列表。在开源社区中,为开源项目提供代码是经常出现的事情,如果名字能出现在知名项目的contributors列表中,是一件比较有荣誉感的事。列表中的第一个贡献应当是包的作者本人。它的格式与维护者列表相同。
5、bugs——一个可以反馈bug的网页地址或邮件地址。
6、licenses——当前包所使用的许可证列表,表示这个包可以在哪些许可证下使用。
"licenses": [
{
"type": "MIT",
"url": "https://github.com/napcs/node-livereload/blob/master/LICENSE"
}
]
7、repositories——托管源代码的位置列表,表明可以通过哪些方式和地址访问包的源代码。
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/napcs/node-livereload.git"
}
8、dependencies——使用当前包所需要依赖的包列表。这个属性十分重要,NPM会通过这个属性帮助自动加载依赖的包。详细介绍请见npm简单使用。
【可选字段】
1、homepage——当前包的网站地址
"homepage": "https://github.com/napcs/node-livereload#readme"
2、os——操作系统支持列表。这些操作系统的取值包括aix、freebsd、linux、macos、solaris、vxworks、windows。如果设置了列表为空,则不对操作系统做任何假设。
3、cpu——CPU架构的支持列表,有效的架构名称有arm、mips、ppc、sparc、x86和x86_64。同os一样,如果列表为空,则不对CPU架构做任何假设。
4、engine——支持的JavaScript引擎列表,有效的引擎取值包括ejs、flusspferd、gpsee、jsc、spidermonkey、narwhal、node和v8。
"engines": {
"node": ">=0.4.0"
}
5、builtin——标志当前包是否是内建在底层系统的标准组件。
6、directories——包目录说明
"directories": {}
7、implements——实现规范的列表。标志当前包实现了CommonJS的哪些规范。
8、scripts——脚本说明对象。它主要被包管理器用来安装、编译、测试和卸载包。scripts指定了运行脚本命令的npm命令行缩写,比如start指定了运行npm run start时,所要执行的命令
"scripts": {
"test": "mocha"
}
【其他字段】
1、author——包作者
2、bin——指定各个内部命令对应的可执行文件的位置,这样就不需要配置环境变量啦。
"bin": {
"livereload": "./bin/livereload.js"
}
3、main——加载的入口文件。模块引入方法require()在引入包时,会优先检查这个字段,并将其作为包中其余模块的入口。如果不存在这个字段,require()方法会查找包目录下的index.js、index.node、index.json文件作为默认入口
"main": "./lib/livereload.js"
4、devDependencies——项目开发所需要的模块。一些模块只在开发时需要依赖。配置这个属性,可以提示包的后续开发者安装依赖包。类比于dependencies字段
"devDependencies": {
"coffee-script": ">= 1.8.0",
"mocha": ">= 1.0.3",
"request": ">= 2.9.203",
"should": ">= 0.6.3",
"sinon": "^1.17.4"