前端工程化-JavaScript模块化
为什么需要模块化
很早的时候,所有开发者把Javascript代码都写在一个文件里面,浏览器执行时,只要加载这一个文件就够了。
到了后来,随着代码规模的不断增加,一个网页代码往往需要加载多个文件。
1 | <script src="a.js"></script> |
但这样写有很多缺点。
第一,浏览器在读取这些脚本文件的时候,浏览器会暂停渲染,增加网页失去响应的时间。
第二,难以处理各个文件的依赖关系。开发者必须严格确定脚本执行的顺序来保证正确加载。否则就会出现诸如变量未定义之类的错误。
第三,各个文件都是暴露在全局作用域下的,容易引起全局命名污染。
为了应付规模越来越大的前端项目,模块化的概念便出现了。
简易的模块化写法
最简单:函数写法
缺点:全局环境污染
1 | function m1() { |
对象写法
优点:不污染全局变量
缺点:暴露私有成员
1 | let myMod = { |
IIFE(立即执行函数)写法
IIFE是目前一种普遍的写法。
优点:不污染全局变量、不暴露私有成员
缺点:没有实现模块继承
1 | let myMod = function(){ |
放大模式和宽放大模式
- 放大模式:模块继承
- 宽放大模式:"立即执行函数"的参数支持空对象,避免浏览器加载顺序导致对象不存在的问题。
1 | let mod = function(mod){ |
CommonJS规范
CommonJS 规范是 nodejs 中主流的一种模块方案。
其中每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
它通过 require 函数来同步加载其他模块,通过 module.exports 导出需要暴露的接口。
这里需要注意一点,它采用的是 同步加载。对于 nodejs 来说,它的模块基本上都在磁盘上,所以加载模块的时间是很短的,对于应用性能的影响也比较小。
尽管这个模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了。以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
global
global是一个对象,用于在各种模块中分享变量。
1 | global.shared = 233; //数据是共享的 |
module
- 在每个模块内部,
module代表当前模块,其中,exports属性是模块对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
1 | //hello.js |
module的其他属性
| 属性 | 解释 |
|---|---|
id | 模块的识别符,通常是带有绝对路径的模块文件名 |
filename | 模块的文件名,带有绝对路径 |
loaded | 返回一个布尔值,表示模块是否已经完成加载 |
parent | 返回一个对象,表示调用该模块的模块 |
children | 返回一个数组,表示该模块要用到的其他模块 |
exports | 表示模块对外输出的值 |
- 在
nodejs中,通过命令行直接调用某个模块,module.parent为null。
exports 变量
为了方便,nodejs为每个模块提供一个 exports 变量,指向 module.exports 。
由于
exports是module.exports的引用,所以直接对exports赋值将切断引用,而不会改变module.exports。
require 函数
require函数的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。模块名规范
/开头:从绝对路径中加载./开头:从当前路径中加载- 没有上面的开头,非目录:加载核心模块,或者一个位于各级
node_modules目录的已安装模块 - 没有上面的开头且为目录:先找到第一级文件夹的路径,再以它为参数,找到后续路径。
- 如果文件没有后缀,则会尝试为文件名添加
.js、.json、.node
使用
require.resolve()方法得到命令加载的确切文件名。require.cache[module]中保存了模块的缓存,使用delete命令即可删除缓存。require.main可以用来判断模块是直接执行,还是被调用执行。当直接执行时,其值为true当出现
a加载b,b加载a的情况时,b会加载a的不完整版本(b得到a加载b之前,module.exports的值)。
1 | //a.js |
AMD:RequireJS的模块规范
AMD(Asynchronous Module Definition,异步模块定义)
前面提到了,CommonJS 中,模块都是同步加载,对于 nodejs,影响比较小。
但是在浏览器环境中,模块都是通过网络请求来加载的,这意味着一个 require 可能需要很长时间,在模块加载的过程中,js程序不能完成其他事情。
所以,在浏览器环境中,不能使用同步加载的方案。RequireJS 则提出了一个为 AMD 的规范,用于浏览器中模块的加载。
基本操作
- 模块调用形式:
require([module], callback),callback的参数为加载的模块(多参数)。 - 模块定义形式:
define(id?: String, dependencies?: String[], factory: Function|Object)。其中id:模块的名字。若不填写,则模块文件的文件名就是模块标识。dependencies:模块的依赖。如果没有指定dependencies,那么它的默认值是["require", "exports", "module"]。依赖模块必须根据模块的工厂方法优先级执行。factory是最后一个参数,它包裹了模块的具体实现,它是一个函数或者对象。如果是函数,那么它的返回值就是模块的输出接口或值。
加载模块
- 引入文件
require.js:<script src="js/require.js" data-main="js/main"></script>,其中data-main用于指定主模块,即加载js/main.js。
1 | //定义模块:hello.js |
- 使用
require.config控制加载行为,为模块名添加路径。
1 | require.config({ |
定义模块
使用 define 定义模块。其形式如下:
define(factory: Function|Object)define(deps: Array<String>, factory: Function)define(name: String,deps: Array<String>, factory: Function)
factory 的参数为加载完的依赖对象。
1 | define(['jquery'], function($) { |
加载非规范的模块
在 require.config 中指定 shim 属性,用于配置这些库的导出:
1 | require.config({ |
小结
require.js 定义模块时,需要预先指定它需要引用的其他所有模块。这称为 依赖前置。
在加载模块之前,需要加载它引用的所有模块。这称为 提前执行。
CMD:SeaJS的模块规范
CMD(Common Module Definition,通用模块定义)
CMD规范是 SeaJS 在推广过程中对模块定义的规范化产出的。它也是针对浏览器推出的一个另一个模块化系统。
define
CMD 模块规范中,一个模块就是一个文件,遵循统一的写法。
define是一个全局函数,用来定义模块。它有如下调用格式:define(factory)define(id?, deps?, factory)(这个不属于规范)
其中,
factory为工厂函数,也可以是一个对象或模板字符串(模板名使用{{ name }}表示),deps为依赖模块的名称,id为模块名称。工厂函数默认有三个参数:
require、exports和moduledefine.cmd是一个空对象,可用来判定当前页面是否有 CMD 模块加载器。
1 | //定义模块:hello.js |
require参数
require是一个函数,接受模块标识作为唯一参数,用来获取其他模块提供的接口。- 模块名规范类似
CommonJS规范。除此之外,由于模块内的require采用静态分析的策略,require有一些其他的规则:- 模块
factory构造方法的第一个参数 必须 命名为require - 不要重命名
require函数,或在任何作用域中给require重新赋值 require的参数值 必须 是字符串直接量。
- 模块
1 | define(function(require, exports) { |
- 动态加载依赖
require.async(id,callback),其中回调的参数为加载的模块对象。
1 | require.async('./a',(a) => null) |
require.resolve(name)解析模块路径:该函数不会加载模块,只返回解析后的绝对路径。
exports
exports是一个对象,用来向外提供模块接口。- 和
commonJS一样,exports是module.exports的一个引用,因此不可直接修改exports。
module
module 是一个对象,上面存储了与当前模块相关联的一些属性和方法。
| 属性 | 解释 |
|---|---|
id | 模块的唯一标识 |
uri | 根据模块系统的路径解析规则得到的模块绝对路径 |
dependencies | 当前模块的依赖 |
exports | 当前模块对外提供的接口。(对其的赋值必须同步执行) |
启动模块
seajs中启动一个模块:seajs.use('./main')会自动加载./main.js
小结
SeaJS中的模块加载器,在模块代码执行之前,对模块代码进行静态分析,并动态生成依赖列表。
模块加载过程中,只有在真正需要这个模块时,这个模块才会被加载。这称为 依赖就近,延迟执行。
UMD规范
前面提到了CommonJS、AMD、CMD,这三种差异挺大的模块化规范。
对于一个库作者来说,这是很致命的,因为它需要想办法同时支持这三种模块规范。
而 UMD模块规范,则就是这些作者想到的办法。
关于这个规范的具体内容,可参考UMD官方主页: https://github.com/umdjs/umd
ESM:ES6模块规范
到了2015年,Ecma官方终于推出了官方的模块化规范 ES Module。
ES Module 和 CommonJS 可以说是最常用的两种模块化规范了。
但它们俩又有一些区别。其中一个便是 ES Module 采用了静态解析的方法,这样就可以在编译期解析并加载模块,提高了运行效率。
而且,由于它是静态的,编辑器可以在编辑的时候对模块进行解析,从而提供更好的类型提示。
对于 Webpack 这样的模块编译工具来说,无需运行代码,就可以获取模块之间的依赖关系。
export
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。若需要外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量。
export关键字支持输出变量、函数、类:
1 | export let name = 'kaz' |
通常情况下,export 输出的变量就是本来的名字,但是可以使用 as 关键字重命名。
1 | let name = 'kaz' |
export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
1 | export 233 //error |
export 语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。这也是它和 CommonJS的另一个区别。
需要注意一点,export 语句必须出现在顶层作用域,不能写在函数内。
import
import 命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块对外接口的名称相同。
from 指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js后缀可以省略。
as 关键字用于取别名。
1 | import { lastName as name } from './profile' |
import 进来的变量是只读的,不能直接赋值。但改写对象的属性是可以的。
此外,import 命令具有提升效果,会提升到整个模块的头部,首先执行。一个推荐的做法便是将它们写在代码的顶端。
import 命令中,大括号不能使用表达式和变量。(因为 import 是静态执行)
1 | import {'f' + 'oo'} from 'foo' |
还有一种不输入值的加载方法,这种情况下在执行时只会执行代码,而不引用变量。
1 | import 'lodash'; |
- 重复的
import语句会被优化成一次。 - 整体加载:使用
*
1 | import * as lodash from 'lodash'; |
由于
import是静态的,所以lodash下所有属性不可修改
export default
export default命令为模块指定默认的输出,在import时可以任意指定名字,且无需大括号。export default命令只能使用一次。
1 | //hi.js |
export default 命令后面不能跟变量声明语句(但可以跟函数声明语句)。但 export default 命令允许输出值。
1 | export default 233 |
export default 本质是输出一个叫做 default的变量或方法:
1 |
|
export from
如果在一个模块之中,先输入后输出同一个模块,import 语句可以与 export 语句写在一起。
1 | //(1) |
[ES2020]import()
import()只是一种特殊语法,只是恰好使用了括号,而不是一个函数。
import用于动态加载一个模块,其返回值为Promise对象。
1 | import(specifier) |
import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。- 如果模块有
default输出接口,可以用参数直接获得。
1 | import('...').then(({default: btn}) =< { |
- 适用场合:按需加载、条件加载、动态模块路径
Module 的加载
浏览器中加载
加载外部ES6模块:指定
type="module"对于带有
type="module"的脚本,浏览器会自动开启defer属性,等待页面渲染完成再加载脚本。
1 | <script type="module" src="./foo.js"></script> |
- 内嵌入网页:
- 代码在模块作用域中执行,模块内部的顶层变量,外部不可见。
- 自动开启严格模式。
- 可以使用
import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL)
1 | <script type="module"> |
- 动态导入
import()可以设置script type="module"
nodejs中加载
Node.js v13.2版本开始,Node.js已经默认打开了 ES6 模块支持。- 脚本文件里面使用
import或者export命令,必须使用.mjs为后缀。 - 也可以在
package.json中加入:
1 | { |
一旦设置了以后,所有 JS 脚本,就被解释为 ES6 模块。此时,使用 CommonJS 模块必须使用
.cjs后缀。
.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。在
CommonJS文件中加载esm模块:import()异步加载
1 | //a.mjs |
运行命令(nodejs12):
1 | node --experimental-modules index.js |
- 在
ES6中加载CommonJS模块:只能全局导入
1 | // a.js |
运行命令(nodejs12):
1 | node --experimental-modules index.mjs |
ESM和 CJS 的区别小结
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。因此,CJS模块检测不到 模块的内部变化。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的
require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立模块依赖的解析阶段。
参考
- Javascript模块化编程(一):模块的写法——阮一峰的网络日志:http://www.ruanyifeng.com/blog/2012/10/javascript_module.html
- Javascript模块化编程(二):AMD规范——阮一峰的网络日志:http://www.ruanyifeng.com/blog/2012/10/asynchronous_module_definition.html
- Javascript模块化编程(三):require.js的用法——阮一峰的网络日志:http://www.ruanyifeng.com/blog/2012/11/require_js.html
- AMD规范——Webpack 中文指南:http://shouce.jb51.net/webpack/amd.html
- CommonJS规范——JavaScript标准参考教程:https://javascript.ruanyifeng.com/nodejs/module.html#toc0
- CMD模块定义规定——seajs/seajs:https://github.com/seajs/seajs/issues/242
- 从 CommonJS 到 Sea.js——seajs/seajs:https://github.com/seajs/seajs/issues/269
- 前端模块化开发那点历史——seajs/seajs:https://github.com/seajs/seajs/issues/588
- 前端模块化开发的价值——seajs/seajs:https://github.com/seajs/seajs/issues/547
- Module 的语法——阮一峰ES6教程:https://es6.ruanyifeng.com/#docs/module
- Module 的加载实现——阮一峰ES6教程:https://es6.ruanyifeng.com/#docs/module-loader
- Node.js 12 中的 ES 模块——前端先锋:https://zhuanlan.zhihu.com/p/75326798
- 标题: 前端工程化-JavaScript模块化
- 作者: ObjectKaz
- 创建于: 2021-08-20 13:24:30
- 更新于: 2022-06-27 14:47:59
- 链接: https://www.objectkaz.cn/3b76bbe4e626.html
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。