在现代化的前端工程中,模块化已经成为习以为常的规范之一。成熟的框架、打包和引用体系已经让我们不需要关注模块的具体格式,随着浏览器对es的支持,开箱即用的 import 和 export 也非常方便。但当我们的项目,需要提供给不同类型的其他项目使用,或者发布到npm平台,就需要对模块规范有一定的了解,以便于做按需的打包和优化。
要了解模块化的规范,首先要了解它的产生背景。最初我们都使用script标签来引入js,但当一个页面引入的js文件越来越多时,就产生了几个难以避免的问题:
- 全局变量污染
- 变量重名
- js之间的依赖关系复杂,无法保证顺序
模块化规范的出现就是为了解决以上问题,同时提高代码的效率和复用性。主流的模块化规范包括CommonJS、AMD、CMD、UMD和ESM这几种,其中CommonJS主要用于服务端NodeJS项目;AMD和CMD提供相似的异步加载模块方式,但对依赖的处理稍有不同;ESM是依赖于ES静态模块结构的一种更为高效的模块规范;UMD则是对以上几种规范的兼容。
可以简单看一下它们的特点:
规范 | 使用场景 | 加载顺序 | 加载方式 |
---|---|---|---|
CommonJS | 服务端 | 同步 | 运行时 |
AMD | 客户端 | 异步 | 运行时 |
ESMoudule | 客户端 | 异步 | 编译时 |
UMD | 客户端 | 异步 | 编译时 |
CommonJS
CommonJS是一种用于非浏览器环境的JavaScript模块化规范,最常见的场景是用于NodeJS。
用法:
// 引入
const doSomething = require('./doSomething.js');
// 导出
module.exports = function doSomething(n) {
// do something
特点:
- 模块在运行时加载和执行,并且只在首次加载时运行一次,然后将运行结果缓存,以备后续多次加载;
- 模块加载方式为同步加载,多个模块依次按顺序加载;
- require 语句可以放在块级作用域或条件语句中
AMD
AMD(Asynchronous Module Definition),即异步模块化定义规范,主要用于浏览器端,其中最具代表性的是require.js。
用法:
// 引入
require(["./amd.js"], function (m) {
console.log(m);
});
// 导出
define(['dep1', 'dep2'], function (dep1, dep2) {
// Define the module value by returning a value.
return function () {};
});
特点:
- 异步加载模块
- 依赖前置,即模块加载前会先加载依赖,加载完成后会执行回调
CMD
CMD 和 AMD 一样,都是用于浏览器端的异步模块定义规范,最常见的实践是sea.js。它和AMD的最大区别是对依赖的处理时机,AMD要求先加载依赖,再执行当前模块逻辑,CMD则是执行到相应依赖时再加载依赖。
用法:
// 引入
seajs.use(["./cmd2.js"], function () {
...
});
// 导出
define(function (require, exports, module) {
...
const num = require("./cmd3.js");
...
});
特点:
-
和AMD规范大体一致
-
区别是依赖后置,即需要的时候才通过require来加载依赖
UMD
UMD(Universal Module Definition),通用模块定义,顾名思义是对以上几种标准的统一,使每个版本都能兼容运行。
用法:
(function (root, factory) {
if (typeof define === "function" && define.amd) {
// 支持 AMD 规范
define(["jquery", "underscore"], factory);
} else if (typeof define === 'function' && define.cmd){
// 支持 CMD 规范
define(function(require, exports, module) {
module.exports = factory()
})
} else if (typeof exports === "object") {
// 支持 CommonJS
module.exports = factory(require("jquery"), require("underscore"));
} else {
// 支持全局引用
root.Requester = factory(root.$, root._);
}
}(this, function ($, _) {
// this is where I defined my module implementation
var Requester = { // ... };
return Requester;
}))
特点:
- 兼容浏览器和服务端两种场景
- 兼容 CMD、AMD、CJS 等多种规范,用法也相同
ESM
ESM(ES Module),基于ES的模块标准,也是当前最常使用的模块化标准。
用法:
// 引入
import {foo, bar} from './myLib';
// 导出
export default function() {
// your Function
};
export const function1() {...};
export const function2() {...}
特点:
- 支持大多数现代浏览器,但也存在部分浏览器兼容问题 (jakearchibald.com/2017/es-mod…)
- 它既拥有像CJS一样简洁的语法,同时又支持AMD的异步加载
- 得益于ES6 的静态模块结构(static module structure),ESM规范可以支持打包时的tree-shaking,支持移除不必要的引用
- 编译时加载
- 可以在html中引用
<script type="module">
import {func1} from 'my-lib';
func1();
</script
运行时加载与编译时加载
CommonJS是运行时加载,例如下面的代码,是加载了fs模块整体,再读取fs对象中的属性stat等。
let { stat, exists, readfile } = require('fs')
ESM 是编译时加载,例如下面的代码,就只加载了模块中的两个方法而不是全部。因为es模块设计的思想是静态化,显式地通过export输出,使得编译时就可以进行静态分析。
不同于CommonJS引入的模块是一个对象,ESM引入的是模块本身,并不是一个对象。
// a.js
export const a = 1;
export const b = 2;
export const c = 3;
// b.js
import { a, b } from 'a.js'
打包配置
了解了不同模块规范的用法,使用打包工具时就可以相应地指定模块规范。
webpack
webpack中,library是指定义一个全局使用的名称变量,libraryTarget是指设置library的暴露方式。配置如下:
module.exports = {
entry: './index.js',
output: {
filename: './dist/testModule.js',
// export to AMD, CommonJS, or window
libraryTarget: 'umd',
// the name exported to window
library: 'testModule'
}
}
libraryTarget的可选项如下:
- libraryTarget: 'var'/'assign'/'assign-properties'
var MyLibrary = _entry_return_; // var
MyLibrary = _entry_return_; // assign
MyLibrary = typeof MyLibrary === 'undefined' ? {} : MyLibrary; // assign-propertie
- libraryTarget: 'this'/'window'/'global'
this['MyLibrary'] = _entry_return_;
window['MyLibrary'] = _entry_return_;
global['MyLibrary'] = _entry_return_;
- libraryTarget: 'commonjs' / 'commonjs2'
exports['MyLibrary'] = _entry_return_; // commonjs
module.exports = _entry_return_; // commonjs2
- libraryTarget: 'amd' / 'cmd' / 'module' / 'umd'
rollup
除了webpack,还有一些常见的模块打包器,比如rollup。它和webpack相比的特点是拥有更少的功能和更简单的api,适合一些简单的项目。
配置如下:
format支持amd、cjs、es、esm、iife和umd几种格式。
export default {
input: "src/main.js",
output: {
file: "bundle.js",
format: "umd",
name: "test",
},
...
};
microbundle
microbundle也是一款轻量级的打包器,它也是基于rollup开发的,但甚至可以省略配置文件,通过package.json中的简单配置即可完成打包。
{
"main": "dist/index.js",
"source": "src/index.ts"
}
指定source(源文件)和main(打包后的入口文件),运行microbundle,默认会生成多目标格式的文件,也可以通过 --format 参数输出指定格式文件。
ls dist
index.d.ts index.js.map index.m.js.map index.umd.js.map
index.js index.m.js index.umd.js
以上就是JS模块化的基本介绍,概括一下就是:
- ESM是最优的选择,语法简洁,支持异步和tree-shaking
- UMD是最全面的选择,在不支持ESM时能靠谱兜底
- CJS支持同步,适用服务器
- AMD和CMD支持异步,适用浏览器