JS: 模块化
模块化
目前比较流行的 js 模块化方案有 commonjs、amd、cmd 以及 es6 module,还有个 umd 方案。
commonjs
commonjs 是服务器端的模块化方案,nodejs 就采用了这种方案。在 commonjs 规范中,一个文件即一个模块,用module.exports
和exports
定义模块输出的接口,用require
加载模块。
// math.js function add(a, b) { return a+b; } module.exports = { add: add } // 也可以这样写 exports.add = (a, b) => a+b;
// main.js const add require('./math'); add(1, 2); // 3
commonjs
采用同步加载方式,对于服务器和本地环境来说,同步加载是非常快的,但对于浏览器来说,就不行了,限于网络因素,异步加载才是比较好的方案。
amd
为了解决浏览器模块化的问题,amd 和 cmd 这两个异步加载方案被提出,requirejs
可以说是 amd 方案的最佳实践。
// index.html // before <script src="..."></script> <script src="..."></script> <script src="..."></script> // amd - requirejs <script src="require.js" data-main="main.js"></script>
在 requirejs 中用define
定义模块,require
载入模块,require.config
用来配置路径。
// math.js // define(id?, dependencies?, factory) define(() => { return { add: (a, b) => a+b } });
// main.js require.config({ baseurl: 'js/lib', paths: { // 左边是模块id,右边是路径 // 这里的路径是 js/lib/jquery.js jquery: 'jquery', // 这里的路径是 js/lib/math.js math: 'math' } }); // require([modlue], callback) require(['jquery', 'math'], ($, math) => { // do something });
需要注意的是,amd 方案是依赖前置的,提前执行。
// amd 依赖前置,提前执行 define(['a', 'b'], function(a, b){ // a 和 b谁先加载完,谁就先执行 // 并不按照代码顺序同步执行 a.dosomething(); b.dosomething(); })
官网:requirejs
cmd
与 amd 不同,cmd 是依赖就近,延迟执行,requirejs 也支持 cmd 写法。
// cmd 依赖就近,延迟执行 define(function(require, exports, module){ // 需要 a 时,才执行 a var a = require("a"); a.dosomething(); // 需要 b 时,才执行 b var b = require("b"); b.dosomething() })
阿里的 seajs 可以说是比较出名的 cmd 实例项目,但现在都有更好的方案来替代它们了。
需要注意的是,两者只是对依赖模块的执行时机不一样,并非加载时机不一样,模块的加载时机都是一样的,它们都是异步加载的。amd是模块加载完就会执行模块,所有模块都加载执行完就进入require
的回调函数,cmd 则是所有模块都加载完,但不执行,等到require
这个模块时才执行。
cmd标准:https://github.com/cmdjs
amd标准:https://github.com/amdjs
umd
umd 是 commonjs 和 amd 的混合,它会判断当前环境支持哪个就使用哪个,这个就不多说了。
es6 module
es6 module 横空出世,混乱的时代结束了。
es6 在语言标准层面定义模块化规范,而且简洁明了,完全可以取代 commonjs 和 amd 规范,成为浏览器和服务器通用的模块解决方案。
es6 module 主要使用export
输出,import
加载。
// math.js let pi = 3.1415; let circlearea = (x) => x*pi; export {pi, circlearea}; // 也可以这样写 export let pi = 3.1415; export let circlearea = (x) => x*pi;
// main.js import {pi, circlearea} from './math.js' circlearea(1); // 3.1415 // 也可以这样写 import * as math from './math.js' math.circlearea(1); // 3.1415
还有一个export default
命令:
// 使用 export default 输出的模块只能有一个输出 export default () => concole.log('hhh');
需要注意的是,commonjs 输出的是值的拷贝,对于基本数据类型是直接拷贝,即缓存模块,但复杂数据类型是浅拷贝,因此修改一个模块中的值,是会改变另一个模块的值的。 require
模块的时候,就会执行整个模块,即运行时加载,然后exports
输出,缓存输出值,再次require
时,会直接在缓存内取值。
而 es6 module 是编译时加载,import
加载的是值的引用,加载进来的值是不能被修改的,即值是只读的,而且不论是基本数据类型还是复杂数据类型,修改原模块的值修改,import
得到的值也是会改变,即值是动态的。
- commonjs 输出的是一个值的拷贝,es6 模块输出的是值的引用。
- commonjs 是运行时加载,es6 模块是编译时输出接口。
至于循环加载问题(a 依赖 b,b 依赖 c,c 依赖 a):
commonjs 循环加载时,因为是属于加载时执行,所以一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分就不输出。而对于 es6 module来说,因为是动态引用,所以出现循环加载时,只要两个模块之间存在某个引用,即取值的时候能够真正的取到值,代码就能够执行。
// node 环境 // a.mjs import {bar} from './b'; console.log('a.mjs'); console.log(bar()); function foo() { return 'foo' } export {foo}; // b.mjs import {foo} from './a'; console.log('b.mjs'); console.log(foo()); function bar() { return 'bar' } export {bar}; /* 结果 b.mjs foo a.mjs bar */
执行a.mjs
不会报错,因为函数声明会提升,在执行import {bar} from './b'
时,函数foo
就已经有定义了,所以b.mjs
加载的时候不会报错。这也意味着,如果把函数foo
改写成函数表达式,也会报错。
备注
规范真多,但我也只用过 commonjs 和 es6 module,而且 es6 还要靠 babel。