说明:本文仅用于记录个人学习 JavaScript 模块系统的记忆笔记,以详细说明当前JavaScript模块系统的优缺点/注意事项为主,其发展历史请行搜索。
一、现在进行时
(一)CommonJS 模块
CommonJS是首个内置于Node.js平台的模块系统。
核心概念:
-
基本使用
推荐写法
// 模块导出 index.js
exports.prop1 = 'hello world'
module.exports.prop2 = 'hi world'
// 模块导入 other.js
const obj = require('index.js')
obj.prop1 // 'hello world'
obj.prop2 // 'hi world'
不推荐写法
// 做法:直接复制给 exports
// 结果:该导出无效
// 原因:exports 仅是指向 module.exports 变量,具体看实现原理的 loadModule 函数
exports = () => {}
// 做法:利用 require() 函数缓存机制,导出实例,形成单例模式
// 结果:可能导出多个实例
// 原因:下文解释
class Logger {}
module.exports = new Logger()
// 做法:monnkey patching
// 结果:全局副作用(side effect),难以检查
// patch.js
require('./logger').prop1 = funciton() {}
// other.js
require('./patch')
-
伪代码
/* 原compiled: function (exports, require, module, __filename, __dirname) {} */
function loadModule(filename, moudle, require) {
const wrappedSrc = `
(function (module, exports, require){
${fs.readFileSync(filename, 'utf8')}
})(module, module.exports, require)
`
eval(wrappedSrc)
}
function require(moduleName) {
const id = require.resolve(moduleName)
if (require.cache[id]) {
return require.cache[id].exports
}
// 模块的原数据
const module = {
exports: {},
id
}
// 更新模块
require.cache[id] = module
// 载入模块
loadModule(id, module, require)
// 返回导出的变量
return module.exports
}
require.cache = {}
require.resolve = (moduleName) => {
/* 根据 moduleName 解析出完整的模块id */
}
NodeJS实现的模块类
/** Module
id: 源码文件绝对路径,如 /User/sam/Desktop/index.js
path: 源码路径对应的文件夹,通过 path.dirname(id) 生成
exports: 模块输出的内容,默认为 {}
parent: 父模块信息
filename: 源码文件路径
loaded: 是否已经加载完毕
children: 子模块对象集合
paths: [ '/home/fanwenzh/tmp/node_modules', // 模块查询范围
'/home/fanwenzh/node_modules',
'/home/node_modules',
'/node_modules' ]
*/
- 文件模块:以 / 开头,绝对路径;以 ./ 或 ../开头,相对路径(.js , .json, .node , .mjs => 其他文件以.js文件打开)
- 核心模块:以moduleName名称开头,NativeModule
- 包模块:如果没有找到moduleName相匹配的核心模块,从发出加载请求的模块开始,逐层向上寻名为 node_module 的目录载入该模块,直至到达文件系统的根部为止
-
循环依赖
CommonJS并没有从实际意义解决循环依赖的问题(下文举例:加载未完全执行的依赖返回非旧引用值),需要开发者在定义时规范好文件依赖顺序。
// 原因:require 是同步函数
// a.js 模块
exports.loaded = false
const b = require('./b')
module.exports = {
b,
loaded: true // 覆写上文的loaded
}
// b.js 模块
exports.loaded = false
const a = require('./a')
module.exports = {
a,
loaded: true // 同上
}
// main.js
const a = require('./a')
const b = require('./b')
console.log('a =>', JSON.stringfy(a, null, 2))
console.log('b =>', JSON.stringfy(b, null, 2))
/*
a => {
"b": {
// 非更新版本!!!
"a": {
"loaded": false
},
"loaded": true
},
"loaded": true
}
// b.js 模块请求载入 a.js模块时,反映该模块所处的状态
// 无法反映a.js模块最终加载完毕时的状态
b => {
"a": {
"loaded": false
},
"loaded": true,
}
*/
-
单例模式的风险/解决方案
在 CommonJS 以伪单例模式导出可能存在多例情况:
// 单例模式导出
moduel.exports = new Database('mydb')
// 两个包(路径不同)a、b,同时依赖同一个包 c 时,由于缓存机制的key为路径,导致引入了两个实例c
/**
-- node_modules
|-- package-a
| '-- node_modules
| '-- mydb // v1.0.0
'-- package-b
'--module_modules
'-- mydb // v2.0.0
*/
// 方案:把单例(实例)保存到全局(顶层)
global.dbInstance = new Database('mydb')
(二)ECMAScript模块
ECMAScript 模块(简称ES模块,或ESM),是ECMAScript 2015规范制定的模块系统。支持循环依赖,能够异步加载(Promise)。
ES模块是静态的(static),区别于CommonJS,必须写在最顶层,而且要置于流控制语句之外。
// ESM
import path from 'path'
// CommonJS
if (process.debug) {
const path = require('path')
}
-
基本使用
推荐用法
// 模块导出 index.js
export function log(){} // 对外导出 log
export const DEFAULT_LEVELS = {} // 对外导出 'DEFAULT_LEVELS'
// 模块导入
// import {log as mylog, DEFAULT_LEVELS} from './index.js'
import * as newModule from './index.js'
newModule.log()
newModule.DEFAULT_LEVELS // {}
// 默认导出 logger.js, 忽略Logger导出名,记载default名下
export default class Logger {}
import MyLogger from './logger.js' // 导出默认模块
import * as loggerModule from './logger.js' // 全部导出
console.log(loggerModule) // {default: [Function: Logger]}
错误用法
// default 关键字不能用作变量名
import { default } from './logger.js'
-
模块解析算法(解决循环依赖)
分3阶段解析代码:
- 第一阶段构造 Construction(或解析Parsing):寻找所有的引入语句,并递归加载每个模块内容
- 第二阶段实例化 Instantiation:针对每个导出的实体,在内存中保留一个带名称的引用,但不给它赋值(创建依赖关系linking,而不执行JavaScript代码),只读live在引入方只能读而不能写
- 第三阶段执行 Evaluation:执行代码获取值
// a.js
improt * as bModule from './b.js'
export let loaded = false
export const b = bModule
loaded = true
// b.js
import * as aModule from './a.js'
export let loaded = false
export const a = aModule
loaded = true
// main.js
import * as a from './a.js'
import * as b from './b.js'
console.log('a =>', a)
console.log('b =>', b)
/*
a => <ref *1>[Module] {
b:[Module]{a: [Circular *1], loaded: true},
loaded: true
}
b => <ref *1>[Module] {
a:[Module]{b: [Circular *1], loaded: true},
loaded: true
}
*/
(三)ESM 与 CommonJS 之间的区别
-
ESM 默认运行在严格模式下,即不用在每份文件开头加上 "use strict"
-
ESM 中通过 import.meta.url 获取当前模块文件路径,CommonJS用require、exports、module.exports、__filename、__dirname关键字
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
- ES 模块中可引用 CommonJS模块,反之不可;.json文件在ES中也需要 createRequire 引入
// ES 中重现 require
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
// 但无法在 CommonJS 中引用 ES模块
二、过去式
过去式的模块系统,仅简要说明其实现机制。
(一)IIFE
IIFE,Immediately-invoked Function Expression,为立即调用函数表达式
const mod = (function(){
const pro1 = ''
const fn1 = () => {}
const exported = {
pro1,
fn1
}
return exported
})()
(二)AMD 简化实现
AMD(Asynchronous Module Definition)是浏览器端的模块加载,实现有RequireJS(规范, github)和 Sea.js
(function(global) {
const AMD = {}
// 模块缓存
const moduleStorage = {}
AMD.define = function(name, dependencies, factory){}
// 模块发射(触发执行)
AMD.emit = function(name) {}
// 模块获取
AMD.require = function(name) {}
global.define = AMD.define(name, dependencies, factory)
global.require = AMD.require(name)
})(window)
-
基本使用
通过 define 定义模块,require 加载模块
// index.js
// 依赖文件
define(['other.js'], function() {
// 定义的模块代码
return
})
-
具体原理
// 1.异步加载
const node = document.creaetElement('script')
node.type = 'text/javascript'
node.src = '3.js'
document.body.appendChild(node)
// 2. 按依按依赖顺序加载:监听 node 的 onload 事件以执行模块代码
// 3. 执行代码 emit 触发
-
循环依赖
以ReaquireJS为例,模块在没有执行过(没有缓存记录)的前提下,总会有其中一个模块读取依赖值是 空对象 或者 undefined
(三)UMD 简化实现
UMD(Universal Module Definition),(通过环境判断,执行不同的入口逻辑)提供前后端支持 AMD 和 CommonJS 模块实现方案。代码简单,可直接看 github
// returnExportsGlobal.js
(function (root, factory) {
// AMD:
if (typeof define === 'function' && define.amd) {
define(['b'], function (b) {
return (root.returnExportsGlobal = factory(b));
});
// Node: CommonJS
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('b'));
} else {
// 浏览器: root 为 window
root.returnExportsGlobal = factory(root.b);
}
}(typeof self !== 'undefined' ? self : this, function (b) {
return {};
}));
三、其他知识
- UTF-8 中 byte order mark(BOM)
- Unix操作系统中的 Shebang(或Hashbang),即#!
四、参考资料
- 《 Node.js设计模式》
- Node.js require
- require源码解读——阮一峰