以前 JS 主要用在浏览器,它是没有模块系统的。如果我们做的项目有点大,那么管理项目的依赖就非常困难,比如 A 依赖 B 和 C,而 C 和 B 有依赖其他的库。这时人为的用script标签的先后顺序来让项目依赖正常是非常困难的。这时就有了很多解决方案。
CommonJS
CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。
比如nodejs就是用的 CommonJS 规范,但是它也没完全接受规范。
CommonJS 中,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
module
每个模块(文件)内部都有一个 module 对象,它有以下属性。
id模块的识别符,通常是带有绝对路径的模块文件名。exports表示模块对外输出的值。parent一个对象,表示调用该模块的模块。(没有则是undefined)filename模块的文件名,带有绝对路径。loaded一个布尔值,表示模块是否已经完成加载。children一个数组,表示该模块要用到的其他模块paths一个数组是nodejs引入库文件的绝对路径(node_modules)
一般nodejs主文件会用到parent属性。
if (module.parent == null) { // 表示不是被当为库应用,而是直接运行
// 运行程序
}
exports属性一般用来对外暴露接口。
module.exports = function () {} // 暴露一个函数
// --------------
module.exports = {} // 暴露一个对象
// ...
除了使用module.exports对外暴露接口,还可以使用exports对外面暴露。
exports.area = function (r) { // 暴露是一个对象,它有一个 area 方法
return Math.PI * r * r;
};
需要注意,不能将exports直接指向一个值。
exports = function(x) {console.log(x)};
// 无效
// 因为 exports 相当于
var exports = module.exports
require
它使用require函数导入模块。
// a.js
module.exports = { data: 100 }
// b.js
var a = require('./a') // 后面的 js 可以省略
console.log(a) // { data: 100 }
// 也可以用下 es6 写法
let { data } = require('./a.js')
require参数是一个路径字符串,
- 如果以
/表示加载的是一个位于绝对路径的模块文件。 - 如果以
./表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。 - 如果都不是则表示加载 nodejs 的核心模块。
- 如果没有找到的话,nodejs 会通过
module.paths属性查找node_modules文件夹中的第三方库文件。 - 如果还没找到就报错。
如果没有带后缀 nodejs 会以.js、.json、.node的顺序查找模块文件。
require 上也有一些属性和方法。
resolve()得到require命令加载的确切文件名。cachenodejs 会将已经加载过的模块缓存起来,方便下次加载,已经缓存的模块就在这个对象中,可以使用delete require.cache[moduleName]删除缓存。main属性用来判断模块是直接执行,还是被调用执行。直接执行的时候(node module.js),require.main属性指向模块本身require.main === module // true。extensions函数数组,根据文件的后缀名,调用不同的执行函数
nodejs 中模块的代码相当于写在下面这个函数中。
(function (exports, require, module, __filename, __dirname) {
// 代码
});
循环加载
如果发生模块的循环加载(A加载B,B又加载A),则B将加载A的不完整版本。
// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';
// b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
/*
b.js a1
a.js b2
main.js a2
main.js b2
*/
AMD
首先是 Asynchronous Module Definition 规范,它在适合在浏览器环境中异步加载模块,并且可以并发的加载。
它主要只有一个接口define(id?, dependencies[]?, factory)函数。它只要有三个参数,前两个可选,它用来定义一个模块。
id 模块名,不推荐使用,一般被工具自动生成。
dependencies 是一个字符串数组,当前这个模块要依赖的模块。
factory 是函数或者对象,如果是对象,那么这个对象就是向外暴露的值,如果是一个函数那么这个函数的返回值就是对外暴露的值。如果dependencies参数为空,那么这个函数的参数默认是require, exports 和 module。
define({
color: "black",
size: "unisize"
});
// 等同于
define(function () {
return {
color: "black",
size: "unisize"
}
});
// ---------------------
// my/shirt.js 文件
define(["./cart", "./inventory"], function(cart, inventory) {
// cart 和 inventory 于 shirt 同一个文件
// 返回一个对象定义 my/shirt 模块
return {
color: "blue",
size: "large",
addToCart: function() {
inventory.decrement(this);
cart.add(this);
}
}
}
);
// -------------
define(function(require, exports, module) {
var a = require('a'),
b = require('b');
// 依赖的 a 和 b 模块
// 这其实就是 CommonJS 规范的写法
return function () {};
}
);
require.js
requirejs 是对 AMD 的具体实现。
有了它 HTML 只需要引入它一个文件。
<!DOCTYPE html>
<html>
<head>
<script data-main="app" src="lib/require.js"></script>
<!-- 建议放在 head 中 -->
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
script上的data-main是一个特殊属性,通过它可以找到 requirejs 的配置文件。
服务器上的目录
www
app 项目代码
main.js
lib 库
require.js
jquery.js
app.js 配置文件
index.html 上面 html 文件
app.js 配置文件
requirejs.config({
baseUrl: 'lib', // 默认情况下加载 www/lib 下的模块 id
paths: {
app: '../app' // 但是一旦模块以 app 开头那么,就加载 app 文件夹中的文件
}
});
// 开始加载项目主文件
requirejs(['app/main']);
多页面时
page1.html(page2.html 和 page1.html 类似。)
<!DOCTYPE html>
<html>
<head>
<title>Page 1</title>
<script src="js/lib/require.js"></script>
<script>
// 加载 js 下的 common 配置文件。
requirejs(['./js/common'], function (common) {
// 配置文件加载好调用
// 因为配置文件中设置了路径所以可以直接用 app/main1 无需加 js
requirejs(['app/main1']);
});
</script>
</head>
<body>
<a href="page2.html">Go to Page 2</a>
</body>
</html>
配置文件
requirejs.config({
baseUrl: 'js/lib',
paths: {
app: '../app'
},
shim: {
// 为不使用 define() 声明依赖项并设置模块值的旧的传统“浏览器全局”脚本配置依赖项
// 导出和自定义初始化。
backbone: {
deps: ['jquery', 'underscore'],
exports: 'Backbone'
},
underscore: {
exports: '_'
}
}
});
如果两个模块发生循环依赖,a 依赖 b,b 依赖 a。
define(["require", "a"],
function(require, a) {
// 如果 a 也依赖 b ,这时参数 a 为 undefined
// b 可以在之后使用 require 函数获取 a
// require 函数依赖是必须的
return function(title) {
return require("a").doSomething();
}
}
);
// 或
define(function(require, exports, module) {
// 在 b 返回之前不能使用 a 的属性
// 这只在 a 和 b 返回的都是对象时有用
var a = require("a");
exports.foo = function () {
return a.bar();
};
});
UMD
对于第三方库一般会判断当前的环境,决定使用 AMD 还是 CommonJS,比如 underscore。因为 AMD 和 CommonJS 都很流行,所以我们要一个兼容两种风格的规范,于是通用模块规范 UMD 就诞生了。
(function () {
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this || {};
// root 等于当前环境顶层对象
if (typeof exports != 'undefined' && !exports.nodeType) {
// 如果用的 CommonJS 则用CommonJS 导出
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
// 否则定义在顶层对象上
root._ = _;
}
if (typeof define == 'function' && define.amd) {
// 如果用的 AMD 则用 define 导出
define('underscore', [], function() {
return _;
});
}
}());
CMD
CMD(CMD 模块定义规范) 和 AMD 非常类似。seajs 是对 CMD 实现。
新版的 requirejs 几乎和 seajs 写法一摸一样。
define(function(require, exports, module) {
// 模块代码
// requirejs 也支持这种写法,当然 seajs 也接受 define 三参数写法,
});
CMD 和 AMD 的最大区别就是,AMD 是依赖提前执行(或许你没用到这个依赖),CMD 是延迟执行。新版本的 requirejs 也改为了延迟执行。
ES6 Module
ES6 是 JS 语言层面的模块化支持,将来服务器和浏览器都会支持 ES6 模块格式。它是一个文件就是一个模块,它使用import指令导入模块,使用export导出模块。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
import { stat, exists, readFile } from 'fs'; // es6
let { stat, exists, readFile } = require('fs'); // commonjs
上面代码中 CommonJS 实质是整体加载fs模块,而 es6 是从fs模块加载 3 个方法,其他方法不加载。
这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
和class一样 ES6 的模块自动采用严格模式。
export
export 指令用来导出你想暴露的值。
export var name = 'Jackson';
export var year = 1958;
// 或
var name = 'Jackson';
var year = 1958;
export { name, year };
// -----------
export function multiply(x, y) {
return x * y;
};
// --------
function a() {}
export {
a as b,
a as c
}
// 默认导出的名字和内部变量名相同。
// 可以使用 as 关键字重命名
export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
// 报错
export 1;
// 报错
var m = 1;
export m;
// 报错
function f() {}
export f;
// ------------- 正确写法
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
// 上面代码输出变量foo,值为bar,500 毫秒之后变成baz
这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新。
export命令不能被嵌套。
function foo() {
export let a = 'bar' // SyntaxError
}
import
import命令用于加载模块。
import命令具有提升效果,会提升到整个模块的头部,首先执行。
import 'lodash'; // 执行文件但不需要其导出值
import { name as mz, year } from './profile.js';
// import 后面跟当前目录下的 profile.js 中,导出的变量, from 后面是模块路径。
// 同样可以使用 as 关键字重命名
import命令输入的变量都是只读的,因为它的本质是输入接口,修改它会报错,如果它是一个对象则可以修改它的属性。
import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。
由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
可以使用*加载,一个模块的所有导出值。
import * as a from './a'
// 将 a.js 文件的所有导出值,都赋值到一个 a 对象变量上
// 但是不能修改它的属性
export default
export default指令用来设置默认导出值。
export default 1
// 使用 export default 就无需额外使用一个变量了
// 它其实像 CommonJS 的 module.exports
对于使用export default的值,import将它导入就无需使用大括号。
import config from './a.js'
// a.js 文件使用了 export default 导出值
// config 名字是自己随便写的
它其实相当于使用一个叫default的变量
function add(x, y) {
return x * y;
}
export {add as default};
// 等同于
// export default add;
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';
当然一个文件中可以同时有export和export default,使用import导入时,也可以使用逗号分隔默认值和其他导出值。
import _, { each, forEach } from 'lodash';
export ... from ... 写法
如果需要先输入后输出同一个模块,就可以使用这种写法。
export { foo, bar } from 'my_module';
foo和bar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo和bar。
同样还可以使用as * default。
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';
export { default } from 'foo';
// 替换默认接口
export { es6 as default } from './someModule';
export { default as es6 } from './someModule';
export ... from ...的写法主要是用在,一个文件比如index.js,将其他的文件的导出值结合起来一次导出。
import()
import命令会被 JavaScript 引擎静态分析,所以嵌套是会报错。
if (needA) {
import A from './a' // 报错
}
// -----------
if (needA) {
const A = require('./a') // CommonJS 完全没问题
}
这时候就需要动态加载功能,import()就是用来解决这个问题。
import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行。
import()返回一个 Promise 对象,then 方法的回调参数就是模块的导出对象。
import('./myModule.js')
.then(({export1, export2}) => {
// ...·
});
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
import()主要用在 webpack 的代码分隔功能。比如 react 的 react-loadable 库。
import Loadable from 'react-loadable';
const LoadableOtherComponent = Loadable({
loader: () => import('./OtherComponent'),
loading: () => <div>Loading...</div>,
});
const MyComponent = () => (
<LoadableOtherComponent/>
);