本篇文章将系统梳理JavaScript模块化发展历程,介绍CommonJS、AMD、CMD、UDM、ESM的语法及特性,以及部分实现原理。
模块化:解决一个复杂问题时自顶向下逐层把系统分成若干个模块的过程
历史上(es6以前)JavaScript一直没有模块化,无模块化时代带来了一些问题。
模块化的演进过程
js// 1.常见的写法
(function(){
// ...
})();
+function(){
// ...
}();
(function() {
// ...
}())
// 一些开源项目为何要把全局、指针以及框架本身引用作为参数
(function(window, $) {
// window 1. 全局作用域转化成局部作用域,提升执行效率 2. 编译时优化
// $ 1. 独立定制复写和挂载 2.防止全局串扰
const _show = function() {
$("#app").val("hi zhaowa");
}
window.webShow = _show;
})(window, jQuery);
// 揭示模式 上层无需关注具体实现,仅关注抽象
const iifeModule = ((depModule1, depModule2) => {
let count = 0;
const publicFn = () => ++count;
const privateFn = () => {
count = 0;
}
return {
publicFn
}
})(depModule1, depModule2);
统一不同开发者,不同项目之间的差异,我们需要一个行业标准去规范模块化的实现方式。
如何设计模块化?
09年 nodejs项目诞生,这标志**"Javascript模块化编程"** 正式诞生。
因为在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;
但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。
js// 三种写法异同
module.exports.x = x // 1 //使用 require('./module).x
module.exports = {} // { x:x, ... }
exports.x=x // 等同于1 exports是moudule.exports的简写
// 无效写法 而模块导出的内容为module.exports
// exports只是module.exports的引用,修改了exports的引用无意义
exports = function(x) {console.log(x)};
module.exports = x //使用 require('./module') // 不推荐这样写,会覆盖前面导出的值
require('x') // 加载文件,可省略.js 路径问题
// require与package.json main字段
Node.js的模块分为两类,一类为原生(核心)模块,一类为文件模块。原生模块在Node.js源代码编译的时候编译进了二进制执行文件,加载的速度最快。另一类文件模块是动态加载的,加载速度比原生模块慢。但是Node.js对原生模块和文件模块都进行了缓存,于是在第二次require时,是不会有重复开销的。
实际上在文件模块中,又分为3类模块。这三类文件模块以后缀来区分,Node.js会根据后缀名来决定加载方法。
.js
。通过fs模块同步读取js文件并编译执行。.node
。通过C/C++进行编写的Addon。通过dlopen方法进行加载。.json
。读取文件,调用JSON.parse解析加载。jsrequire('fs'); // 加载原生模块。 如 http、fs、path 等
require('/xx/../xx.js'); // 绝对路径加载
require('./hello.js'); // 相对路径加载
require('./hello'); // 相对路径绝对路径都可以省略一些后缀名如 .js .json 等
require('webpack'); // 加载node_modules中的模块,读取package.json中的main字段指向的文件
require('./X'); // 按一下顺序查找 X.js X/package.json中main字段 X/index.js node_modeules/X/package.json
// 以上只是简单总结,实际上nodejs的require用法非常灵活
文件查找策略
- node a.js
- 执行a.js遇到 require('b.js'),暂停a.js执行,导出部分内容,执行b.js
- 执行b.js,遇到require(a.js),使用a.js导出的部分内容,继续执行b.js
- b.js执行完毕,继续执行a.js
注意事项 由于 CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。
jsvar a = require('a'); // 安全的写法
var foo = require('a').foo; // 危险的写法
exports.good = function (arg) {
return a.foo('good', arg); // 使用的是 a.foo 的最新值
};
exports.bad = function (arg) {
return foo('bad', arg); // 使用的是一个部分加载时的值
};
// 整体引入,在使用的时候(function调用)再尝试获取内容
jsconst fs = require('fs');
const path = require('path');
req('./module_one.js');
function req(moduleName) {
const modPath = path.join(__dirname, moduleName);
let content = fs.readFileSync(modPath, 'utf-8');
const fn = new Function('exports', 'require', 'module', '__filename', '__dirname', content + '\n return module.exports');
const module = {
exports: {}
}
return fn.call(module.exports, module.exports, req, module, __filename, __dirname)
}
// module_one.js
exports.name = 'one of CommonJS modules';
console.log('this----', this);
一种专门为浏览器端重新设计了一个规范。代表:有个非常出名的库Require.js
,它除了实现了 AMD 模块化规范,本身也是一个非常强大的模块加载器。
CommonJS
在服务端采用同步加载,这种模式更加直观且符合服务端编程,因为所有的模块都存放在本地硬盘,等待时间就是硬盘的读取时间。AMD优点:
js// 定义一个模块
define('a', [], function () {
return 'a';
});
define('b', ['a'], function (a) {
return a + 'b';
});
// 导入和使用
require(['b'], function (b) {
console.log(b);
});
jsif(typeof define !== 'function') {
(function() {
var modules = {}
define = function (modName, deps, factory) {
factory.deps = deps;
modules[modName] = factory;
}
require = function (modNames, callback) {
var loadMods = modNames.map(function(modName){
var module = modules[modName];
var result;
require(module.deps, function (...depResults) {
result = module(...depResults)
})
return result;
});
callback.apply(null, loadMods);
}
})()
}
define('a', [], function () {
console.log('load module a');
return 'module a';
})
define('b', ['a'], function (a) {
console.log('load module b');
return 'module b --- ' + a;
})
require(['b'], function (b) {
console.log(b)
})
SeaJS遵循CMD规范,与RequireJS类似,同样做为模块加载器。
github: [https://github.com/seajs/seajs] (https://github.com/seajs/seajs)
使用demo 在github仓库里, 官方文档在issues里面
但Sea.js比起Node的不同点在于,前者的运行环境是在浏览器中,这就导致A依赖的B模块不能同步地读取过来,所以Sea.js比起Node,除了运行之外,还提供了两个额外的东西:
即Sea.js必须分为模块加载期和执行期。
所以Sea.js需要三个接口:
javascriptseajs.config({
alias: { // 路径别名 配合define、require使用
'modA': '/modules/modA',
},
// preload: ['seajs-text']
})
seajs.use("/modules/greet", function (Greet) {
Greet.helloJavaScript();
});
javascript// /modules/greet.js
define(
['modA'], // 依赖的模块,立即加载,不管有没有使用
function (require, exports) {
document.body.innerHTML = '<h2>hello sea.js</h2>'
function helloJavaScript () {
var modeA = require('modA'); // 必须声明依赖才能require
var ele = document.createElement('h3');
ele.innerText = modeA.name;
document.body.appendChild(ele);
}
exports.helloJavaScript = helloJavaScript;
setTimeout(() => {
console.log('require.async');
// 异步加载的模块,按需加载
require.async('./asyncMod', function(b) {
var ele2 = document.createElement('h3');
ele2.innerText = b;
document.body.insertBefore(ele2, document.body.lastChild);
});
}, 1000);
});
// /modules/modA.js
define(function (require, exports) {
exports.name = "modA.js has load";
});
// /modules/asyncMod.js
define(function (require, exports, module) {
module.exports = "This is a async module, please use \"require.async\" to load";
});
javascript(function (root) {
var seajs = root.seajs = {};
var cid = 0;
var cidMap = window.cidMap = {};
var modules = window.modules = {};
// cidMap[url] ==> cid ==> modules[cid]
// cidMap[cid] ==> url
seajs.use = function(url,cb) {
loadScript(url)
.then(() => modules[cidMap[url]].p)
.then(() => {
cb(require(url))
});
}
function loadScript(url) {
cidMap[cid] = url;
cidMap[url] = cid;
cid++;
var oScript = document.createElement('script');
var s = document.getElementsByTagName('script')[0];
oScript.src = url + '.js';
s.parentNode.appendChild(oScript);
return new Promise((resolve) => {
oScript.onload = function () {
console.log(url + '.js', '加载完成');
resolve();
}
})
}
require = function (modName){
var result = modules[cidMap[modName]].exports;
return result;
}
define = function (deps, factory) {
factory.deps = deps;
var _resolve;
var p = new Promise(resolve => {
_resolve = resolve;
})
var _module = {
exports: {},
p,
}
modules[cid - 1] = _module;
var cbs = deps.map(url => {
return loadScript(url).then(() => modules[cidMap[url]].p)
});
Promise.all(cbs).then(() => {
_resolve();
factory(require, _module.exports, _module);
})
}
})(this);
// 以上是个人根据SeaJS用法手写部分实现,基本未完全参考源码
// 如CMD的延时执行就没有体现
// 再者还有些API如require.sync没有实现,感兴趣可尝试自己写一写
- 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
- CMD 推崇依赖就近,AMD 推崇依赖前置
- 相对于amd的优点是方便将nodejs模块转成cmd以便在浏览器运行
- AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。
网上有人说 seaJS
是重复造轮子,个人觉得这种说法比较牵强,
但是我也在想一个问题为什么seaJS
没有requireJS
那么流行?
是因为先有requireJS
吗,后来requireJS
也兼容了CMD
写法?
如果高见请指教!
UMD
是 AMD
和 CommonJS
的综合产物。AMD
用于浏览器,CommonJS
用于服务器。UMD
则是两者的兼容模式,解决了跨平台问题。
实现原理:if-else
详情请移步github UMD
js(function (global, factory) {
// commonJS
if(typeof exports === 'object' && typeof module !== 'undefined') {
factory(exports)
}
// amd
else if(typeof define === 'function' && define.amd) {
define(['exports'], factory)
}
// umd
else {
global = typeof globalThis !== 'undefined' ? globalThis : global || self;
factory(global.Calc = {}) // rollup.config.js output.name: 'Calc'
}
})(this, (function (exports) {
'use strict';
// exports.multi = multi;
// ...
Object.defineProperty(exports, '__esModule', { value: true });
}));
设计思想
静态加载的好处
严格模式 ESM模块自动采用严格模式
js// 两种写法
export var a = 1;
var b = 1;
export {
b
}
// export {} 可以使用多次,会合并
export {
b as c
}
// 使用as重命名
// a.mjs
var c = 23;
export {
c as cc,
}
// 使用
import {cc} from './a.mjs';
// export 是静态执 所以必须在模块顶层,否则编译时报错
// import也是如此
加载模块
js// import会提升到整个模块的头部,首先执行。
console.log('fa-', fa());
import {fa} from './math.mjs';
// as 关键字
import { lastName as surname } from './profile.js';
// 接口是只读的,禁止修改
surname = 123; // 报错
// 允许重复声明, 但是import语句是单例模式
因为 import是静态执行 所以必须在顶层代码块(不能出现在if语句中)也不能使用属性简写
jsimport * as circle from './circle';
js// math.mjs
function aa(a =1){
console.log('---',a);
}
export default aa
// 等价于
// export {aa as default}
var c = 1;
export {c as default} // 报错, default 只能使用一次
// main.mjs
import aa from './math.mjs'
// 等价于
// import {default as aa} from './math.mjs'
export * from './math.mjs';
// export * 会丢弃default接口, 下面代码是打补丁
import _m from './math.mjs';
export default _m;
jsexport { foo } from 'my_module';
// 可以理解为
import { foo } from 'my_module'
export { foo }
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;
export { default as es6 } from './someModule';
// 等同于
import es6 from './someModule';
export { es6 };
export * as ns from "mod";
// 等同于
import * as ns from "mod";
export {ns};
注意事项
返回当前模块的原信息,只能在模块内部使用
import.meta.url
返回当前模块的 URL 路径import.meta.scriptElement
是浏览器特有的元属性,相当于document.currentScript
测了下 chrome和safari都没有实现import.meta.resolve()
仅chrome有,可类比node path.resolve
<script>
标签就会停下来,等到执行完脚本,再继续向下渲染。<script>
标签打开defer
或async
属性,脚本就会异步加载。defer
是“渲染完再执行”;async
是“下载完就执行”。<script type="module">
相当于defer
属性,<script type="module">
注意事项
import
export
必须在顶层代码块,有声明提前的作用require()
是同步加载, ESM的import()
是异步加载this
前者指向当前模块,后者是undefined从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
.mjs
.cjs
.js
由pkg.type
字段决定类型pkg.main
pkg.exports
pkg.main
有多种用途js(async () => {
await import('./my-app.mjs');
})();
js// 报错 因为ESM静态分析阶段 CJS只有 module.exports
import { method } from 'commonjs-package';
// 推荐写法如下
import packageMain from 'commonjs-package';
const { method } = packageMain;
// 方式二 module.createRequire()
如何同时支持两种模块?
Node.js 的内置模块可以整体加载,也可以加载指定的输出项。
加载路径
不能使用CommonJS内置了一些变量
口述- 略
本文作者:郭敬文
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!