前言
javascipt 在发展初期,并没有模块化的概念,Node 的诞生促进了 CommonJS 的诞生,但该标准只适用于服务端模块。为了在游览器端也能使用, AMD 和 CMD 诞生了,后来又为了兼容前三者,UMD 诞生了,随后的官方标准 ESM 吸收前辈的各个优点。除此之外,各个打包器,如 webpack,rollup,也打包出了符合各个标准,供游览器使用的代码。
1. 模块化的理解
1.1 什么是模块
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件),并进行组合在一起
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信
1.2 模块的优点
- 避免命名冲突(减少命名空间污染)
- 更好的分离, 按需加载
- 更高复用性
- 高可维护性
1.3 模块化的进化过程
-
**全局函数模式:**将不同的功能封装成不同的全局函数
- 缺点: 污染全局命名空间, 容易引起命名冲突或数据不安全,而且模块成员之间看不出直接关系
function module1(){ //... } function module2(){ //... }
-
**命名空间模式:**简单对象封装
- 优点:减少了全局变量,减少了命名冲突
- 缺点:数据不安全(外部可以直接修改模块内部数据)
const myModule = { name: 'laixiaotao', run() { console.log(`名字是${this.name}`) } }; myModule.name = 'zhouxingxing'; myModule.run();
-
**IIFE 模式:**匿名函数自调用,将数据和行为封装到一个函数内部, 通过给 window 添加属性来向外暴露接口
- 优点:数据是私有的, 外部只能通过暴露的方法操作
- 缺点:无法当前模块依赖另一个模块问题
// index.html文件 <script type="text/javascript" src="module.js"></script> <script type="text/javascript"> myModule.run(); console.log(myModule.name); </script>
// module.js文件 (function (window) { name: 'laixiaotao', run() { console.log(`名字是${this.name}`); } window.myModule = { run }; // 注入window中,对外暴露myModule })(window)
-
**IIFE 模式增强:**引入依赖,这是现代模块实现的基石。
- 优点:将依赖作为参数传入,不仅保证了模块的独立性,还使得模块之间的依赖关系变得明显
// index.html文件 <!-- 引入的js必须有一定顺序 --> <script type="text/javascript" src="jquery-1.10.1.js"></script> <script type="text/javascript" src="module.js"></script> <script type="text/javascript" src="module1.js"></script> <script type="text/javascript"> myModule.run(); console.log(myModule.name); </script>
// module.js文件 (function (window, jquery) { name: 'laixiaotao', run() { console.log(`名字是${this.name}`); $('body').css('background', 'red'); // 使用引入的依赖 } window.myModule = { run }; // 注入window中,对外暴露myModule })(window, jquery||{}, mudule1||{})
1.4 引入多个 script 标签问题
IIFE 模式增强模式虽然解决了模块化隔离问题,但是如果简单的使用,会产生引入多个 script 标签问题。
- **请求过多:**如果依赖过多,也意味着需要更多的请求
- **依赖模糊:**依赖之间关系混乱,无法理清彼此这件的加载关系,加载顺序
- **难以维护:**以上两种原因就导致了很难维护,很可能出现牵一发而动全身的情况导致项目出现严重的问题
因此,为了更好解决这个问题,以及其他问题。模块化规范,通过异步加载,按需加载/懒加载/延迟加载等手段消除了手动引入依赖的弊端,通过自动管理依赖来消除隐患。
2. 模块化规范
-
共同点
- 所有代码都运行在模块作用域,不会污染全局作用域
- 单例模式,模块可以多次加载,但是只会在第一次加载时执行一次,之后就被缓存到内存中了。之后再加载,就直接读取缓存结果。要想模块重新执行就必须清除缓存。但是不同的规范的单例模式有点区别,尤其要重点注意 CommonJS 和 ESM 的区别
-
不同点
- 执行机制:参见 3.1
2.1 CommonJS
2.1.1 概述
Node 模块采用 CommonJS 规范,单个文件就是一个模块,有着属于自己的作用域。文件里定义的变量、函数、类、对象、类等都是私有的,除了文件暴露的,均不可访问。在服务器端,模块的加载是运行时同步加载执行的;在浏览器端,模块需要提前编译打包处理。主要的实现有 Node 和 Webpack
2.1.2 语法
- **导出模块:**module.exports = value 或 (module.)exports.xxx = value
- **导入模块:**require(路径/内置模块名/node_modules模块名)
2.1.3 简单原理
// 引入的hello.js文件
(function (){
module.exports.a = 3;
})();
// 实现
var module = {
id: '31231231',
exports: {}
}
var load = function (module){
// 读取的hello.js代码
module.exports.a = 3;
// hello.js代码结束
return module.exports;
}
var exported = load(module);
save(module, exported)
(1)module 变量是在 Node 加载前就准备好的一个变量,实际为一个构造函数 Module 的一个实例,new 使用
(2)然后在加载文件时,module 作为一个函数参数传给对应的文件使用,然后给这个 module 装上 exports
(3)其他模块 require,实际上是 require 该模块的 module.exports 属性,require 命令的基本功能是,读入并执行一个 JavaScript 文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错
2.2 AMD
2.2.1 概述
CommonJS 加载模块是同步加载执行的,会阻塞被调用的文件,而AMD 则是非同步加载执行模块的,允许指定回调函数。由于 Node.js 主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以 CommonJS 规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外 AMD 规范比 CommonJS 规范在浏览器端实现要来着早。主要的实现是 RequireJS。
2.2.2 语法
-
引入 require.js,并且指明 js 主文件入口
// 这样就可以直接使用require.js暴露出来的全局变量 <script type="text/javascript" data-main="js/main" src="js/libs/require.js"></script>
-
导出导入模块
// math.js,定义没有依赖的模块,只有导出 define(function(){ let count = 1; function add() { count++; } return {count, add} // 暴露模块 }) // use.js,提前声明拥有依赖的模块,这叫做依赖前置,既有导出又有导入 define(['math', 'jquery'], function(math, $){ const count = math.count; const showCount = function() { console.log(count); } // 并不是运行到这里才加载执行module,而是会在整个define开头和math、jquery一起加载执行 // 注意此require不同与CommonJS、CMD执行机制 require([module], function(module){ console.log(module) }) $('body').css('background', 'green'); return {showCount} // 暴露模块 })
-
配置文件
// main.js (function() { require.config({ baseUrl: 'js/', // 基本路径 paths:{ // 映射: 模块标识名: 路径 math: './modules/math', // 不能写成math.js,会报错,最后生成的路径是 js/modules/math.js use: './modules/use', jquery:'jquery.min.js' } }), require(['use'], function(use) { use.showCount(); }) })() /* 目录结构 * js * |-modules-math.js * |-main.js */
2.3 CMD
2.3.1 概述
CMD 规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD 规范整合了 CommonJS 和 AMD 规范的特点。主要的实现是 SeaJS。使用比 AMD 简单许多。已停止使用更新
2.3.2 语法
-
引入 sea.js 并调用对应函数
<script type="text/javascript" src="sea.js"></script> <script type="text/javascript"> seajs.use('./js/modules/main') </script>
-
导出导入模块
// math.js模块 define(function(require, exports, module) { // 跟 CommonJS 导出一样 exports.xxx = 1; module.exports.yyy = 2; }) // main.js模块 define(function(require, exports, module) { // 引入模块-同步,会阻塞,运行时才加载,故叫做就近依赖 const math = require('./module/math') // 引入模块-异步,不会阻塞,运行时才加载,故叫做就近依赖 const asyncModule = require.async('./xx', function(asyncModule) { console.log(asyncModule) }) }) /* 目录结构 * js * |-modules-math.js * |-main.js */
2.4 UMD
一种 JS 通用模块定义规范,目的是用一种规范同时容纳兼容 CommonJS、AMD、CMD 模块,可以导入导出任意环境下可以使用的模块。但是由于 ESM 的流行,现已不常用,@详细学习
2.5 ESM
2.5.1 概述
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS、AMD、CMD 模块,都只能在运行时确定这些东西。因为他们都是用内存形成的单例模式,而 ESM 是由 JS 引擎去做静态编译。
2.5.2 特点
- import/export 需要声明 type="module",import() 不需要
- import/export 必须在顶层作用域,import() 不需要,但它返回的是一个 promise
- 单例模式,每个模块只加载到内存运行一次,注意和 CommonJS 区别
- import 属性是只读的,不能赋值,但可以设置别名
2.5.3 语法
- 导出模块:export let/var/const 变量名 = 值 或 export default 值
- 导入模块:import ... from ‘路径‘
- 动态加载:import()
3. 不同模块化规范对比
3.1 CommonJS、AMD、CMD 对比
CommonJS | AMD | CMD | 主要实现 | node、webpack<=4 | require.js | sea.js | 文件 VS 模块 | 1. node:文件即是模块 2. webpack:文件可以多模块 | 文件即模块,单个 JS 文件只有第一个define 生效 | 文件即模块,单个 JS 文件只有第一个define 生效 | html 文件引用 | webpack:打包的同时,会在 html 中生成 script | 异步加载,通过 head.appendChild 注入 | 异步加载,通过 head.appendChild 注入 | 模块调用 | require | require、define | require、require.async | 执行机制 | 顺序加载执行依赖,会阻塞调用文件执行 | 提前加载执行 define 和 require 引用依赖(先加载好先执行),不会阻塞调用文件执行 | require:顺序加载执行依赖,会阻塞调用文件执行 require.async:异步加载执行依赖,不会阻塞调用文件执行 |
---|
-
**webpack 文件:**webpack 打包后的单个文件可以只有一个模块,如各种 chunk.xxx.js;也可以单个文件多个模块,如 chunk-vendors.xxx.js 包含多个第三方模块
-
依赖前置 vs 依赖就近
- **依赖前置:**在定义模块的时候就要声明其依赖的模块。AMD 无论是 define、require 都会在执行调用文件前,提前加载执行 define 和 require 引用依赖(先加载好先执行),因此 requireJS 作者推崇依赖前置。
- **就近依赖:**只有在用到某个模块的时候再去 require,由于会阻塞调用文件的执行,因此 CMD 推崇就近执行,CommonJS 随意。
// 如下模块通过SeaJS/RequireJS来加载,参考:https://www.douban.com/note/283566440/ define(function(require, exports, module){ console.log(“这是主模块”); var module1 = require("./module1"); module1.say(); var module2 = require("./module2"); module2.say(); return { hello: funtion() { console.log("Hello, main") } } }) // AMD执行结果:这是模块1、这是模块2、这是主模块、Hello,module1、Hello,module2、Hello、main // CMD执行结果:这是主模块、这是模块1、Hello,module1、这是模块2、Hello,module2、Hello、main
3.2 CommonJS、ESM 对比
3.2.1 ESM babel
-
ESM Bable const { run } = obj;
run();var run = obj.run;
run();对象解构出来的变量,等价于重新拿个同名变量去赋值,相当等号,该怎样就怎样。
-
ESM Babel export let count;
export default {};Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.count = void 0;
var count;
exports.count = count;
var _default = {};
exports.default = _default;import a from 'a.js'; var _a = _interopRequireDefault (require("a.js"));
function _interopRequireDefault (obj) {
return obj && obj.__esModule ? obj : { default: obj };
}(1)_esModule 标识挂载在 exports,这表明它是一个由 ESM 模块转换而来的 CommonJS 模块
(2)ESM 模块本身是可以同时支持声明导出和默认导出的,Babel 时,如果简单地用 CommonJS 模拟 ESM 的默认导出,会出现问题,如下面代码,因此,Babel 在 exports 上使用了一个 default 变量来承载 ESM 的默认导出;这样就可以同时兼容声明导出和默认导出了。
module.exports.xxx1 = 1; exports.xxx2 = 2; // 内部实现:var exports = module.exports module.exports = {}; // module.exports重新指向,导致上面两行全部失效,无法导出xxx1,xxx2 module.exports.yyy1 = 1; exports.yyy2 = 1; // 失效,因为mdule.exports重新指向,导致和exports联系中断
(3)如果没有 Babel 转译,CommonJS 和 ESM 模块当然是不可以随意混用的,但由于 ESM 模块的 Babel 是采用了 CommonJS,这也意味着只要源文件需要通过 Babel,那么可以使用 ESM import 或 CommonJS require 引入 ESM export。但是两者是有差别的,从第(2)点可以看出,由于需要用 exports.default 承载 ESM 的默认导出,ESM export 时,会自动去掉 default,但是如果采用 ComonJS require 硬生生地导入。会有 default 残留。这也是为什么 Typesciprt 会有 import x = require('x') 写法,就是为了兼容引入两者。
// module.js export default {} // index.js ESM 导入,node>=9 可以直接执行index.js,也可以执行babel后的index.js import m from './module.js'; console.log(m); // {} // index.js CommonJS 导入,node 直接执行当然会报错,但可以执行babel后的index.js const m = require('./module.js'); console.log(m); // {default:{}}
(4)void 0 是一个函数,它的值是 undefined,因此 exports.default = exports.count = undefined
3.2.2 两者区别
-
CommonJS 模块输出的是一个值的拷贝,ESM 模块输出的是值的引用
其实就把 commomJS 和 ESM 当做普通的看待就行了,所谓的 commomJS 值传递是因为 exports 和对应的 value 之间的关系就是一个普通对象赋值关系,该怎样还是怎样,而 ESM 的所谓引用传递是因为它不仅获得了值,它还把对应的引用就是那些 let var const 和变量也传出去了,这也从他的 babel 可以看出,如下代码,从这个角度出发 export default 并没有引用传递能力,譬如此@错误用例,下面 exports.count = count = count + 1 语句可以看出 add 被调用时,不仅改变外面的 count 也改变了自身的 count,然而 babel 模拟存在@局限性
ESM Babel export let count = 1;
export function add() {
count++;
}
export function get() {
return count;
}
export default {};Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.count = void 0;
var count = 1;
exports.count = count;
exports.add = add;
function add() {
exports.count = count = count + 1; // 重点语句
}
exports.get = get;
function get() {
return count;
}
var _default = {};
exports.default = _default; -
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
(1)CommonJS、AMD、CMD 运行时加载:都是加载到内存中,通常为单例模式,只能在运行时确定这些东西,也就是说代码执行到对应的 require 会加载文件
// CommonJS模块 let { stat, exists, readFile } = require('fs'); // 等同于 let _fs = require('fs'); let stat = _fs.stat; let exists = _fs.exists; let readfile = _fs.readfile;
**解析:**上面代码的实质是整体加载 fs 模块(即加载 fs 的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为 “运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
(2)ESM 编译时加载:ESM 不是对象,而是通过 export 命令显式指定输出的代码,再通过 import 命令输入,它是 JS 引擎做的工作。
import { stat, exists, readFile } from 'fs';
**解析:**上面代码的实质是从 fs 模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高,因此 import/export 必须位于顶层作用域,而且还可以做@树摇的原因
-
CommonJS 模块的 require 是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段
4. 不同环境使用模块化对比
4.1 Node 如何处理模块?
-
**标准:**CommonJS、ESM;前者是 Node 的官方标准,后者是 JS 官方标准,需要兼容实现
-
**使用方法:**与 package.json type 字段有关,官方推荐包开发者明确指定 type 字段值
- 无论 type 是何值,.mjs 文件按 ESM 模块处理,.cjs 文件按照 CommonJS 模块处理
- type 值为 CommonJS 或没有定义,.js 文件按照 CommonJS 模块处理
- type 值为 module,.js 文件按照 ESM 模块处理,并且强制默认使用严格模式
-
**混用:**按照第二点后的 CommonJS 模块或 ESM 模块处理的文件随意混用导入导出时,会出现报错。
-
**CommonJS 文件中加载 ESM 模块:**由于 CommonJS 是运行时加载,因此只能使用 import()
// main.cjs 文件 (async ()=>{ await import('./module.mjs') })()
-
**ESM文件中加载 CommonJS 模块:**由于 CommonJS 是整体加载,因此不能只加载单一项
// main.mjs 文件 // 正确 import module from './module.cjs' const { run } = module // 报错 import { run } from './module.cjs'
-
4.2 游览器如何处理模块?
借助打包器如 rollup、Browserify 打包出各种适合游览器加载的 dist 文件,可参考 @vue各个打包版本
4.3 webpack 如何处理模块?
不同于 rollup 只是打包出不同环境下使用的单纯的 js 文件,webpack 是 web 的整体解决方案,可以打包出完整网站目录,可以对 html、css 文件做各种操作
参考文章
-
CommonJS规范-阮一峰
-
Node CommonJS模块源码解析
-
前端模块化详解(完整版)
-
CommonJS 与 esm 的区别
-
node.js 如何处理模块
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!