Skip to main content

Javascript 几种模块化标准

· 9 min read
kart jim

历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS(CJS) 和 AMD 两种。前者用于服务器,后者用于浏览器。
ES6 在语言标准的层面上,实现了模块功能,成为浏览器和服务器通用的模块解决方案。(ESMES6模块)

CJS

规范代表库:CommonJS

common.js主要用于后端,在nodejs中,node应用是由模块组成,采用的commonjs模块规范。 每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见。

  • 每个模块内部,module变量代表当前模块,是一个对象,它的exports属性(即module.exports)是对外的接口
  • module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量
  • 为了方便,Node为每个模块提供一个exports变量,指向module.exports。即 let exports = module.exports
  • 如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。
  • 不能直接将exports变量指向一个值,因为这样等于切断了exportsmodule.exports的联系:如 exports = x => x
  • CommonJS规范 加载模块是同步的 ,只有加载完成,才能执行后面的操作。

使用示例:

// monad.js
exports.monad = x => ({ // 导出
fold: f => f(x),
toStr: () => `Monad(${x})`
})

// index.js
let monad = require('monad'); // 导入

AMD

规范代表库:require.js

RequireJS 是一个JavaScript模块加载器(文件和模块载入工具),使用RequireJS加载模块化脚本将提高代码的加载速度和质量它针对浏览器使用场景进行了优化,并且也可以应用到其他 JavaScript 环境中,例如 Rhino 和 Node.js。

/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>

/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});

CMD

规范代表库:sea.js

CMDAMD很类似,不同点在于:AMD推崇依赖前置、提前执行;CMD推崇依赖就近、延迟执行。

代码示例:

define(function(require, exports, module) {
var a = require('./a'); //在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});

/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
var $ = require('jquery.js');
var add = function(a,b){
return a+b;
}
exports.add = add;
});

// 加载模块
seajs.use(['math.js'], function(math){
var sum = math.add(1+2);
});

UMD

UMD规范只是一种通用的写法,是在amdcjs两个流行而不统一的规范情况下,才催生出umd来统一规范的,umd前后端均可通用。

代码示例:

(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery', 'underscore'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS之类的
module.exports = factory(require('jquery'), require('underscore'));
} else {
// 浏览器全局变量(root 即 window)
root.returnExports = factory(root.jQuery, root._);
}
}(this, function ($, _) {
// 属性
var PI = Math.PI;

// 方法
function a() { }; // 私有方法,因为它没被返回
function b() { return a() }; // 公共方法,因为被返回了
function c(x, y) { return x + y }; // 公共方法,因为被返回了

// 暴露公共方法
return {
ip: PI,
b: b,
c: c
}
}));

并且支持直接在前端用 <script src="lib.umd.js"></script> 的方式加载。

ESM

esm规范是es6原生支持的,类似commonjs的写法类似、异步加载机制能通过设置type=module,用于html中,而且在node中也支持

export // 导出模块
export default xxx // 导出模块,支持导出后更换名称

import ""// 导入全部模块
import {xx, xx} from './xxx.js'; // 导入模块的一部分,要和导出的名字一致
import xxx from ""; // 导入 export default导出的模块,xxx名称不一定要和导出的名字一致

Node.js 上的模块标准有 ES6 模块与 CommonJS 模块,它们有三个重大差异:

  • CommonJS 模块输出的是一个值的拷贝ES6 模块输出的是值的引用
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段
  • 关于模块顶层的this指向问题,在CommonJS顶层,this指向当前模块;而在ES6模块中,this指向undefined
  • 关于两个模块互相引用的问题,在ES6模块当中,是支持加载CommonJS模块的。但是反过来,CommonJS并不能require ES6模块,在NodeJS中,两种模块方案是分开处理的。

ESM、CJS循环引用

关于 ES6 moduleCommonJS module 循环引用的问题

循环加载指的是a脚本的执行依赖b脚本,b脚本的执行依赖a脚本

  1. CommonJS模块是加载时执行。一旦出现某个模块被“循环加载”,就只输出已经执行的部分,没有执行的部分不会输出。
  2. ES6模块对导出模块,变量,对象是动态引用,遇到模块加载命令import不会去执行模块只是生成一个指向被加载模块的引用

CommonJS模块循环引用的例子:

// a.js
exports.done = false;

var b = require('./b.js');
console.log('在a.js中,b.done = %j', b.done)

exports.done = true;
console.log('a.js执行完成!')


// b.js
exports.done = false;

var a = require('./a.js');
console.log('在b.js中,a.done = %j', a.done)

exports.done = true;
console.log('b.js执行完成!')


//main.js
var a = require('./a.js');
var b = require('./b.js');

console.log('在main.js中,a.done = %j,b.done = %j', a.done, b.done);

输出:

在b.js中,a.done = false
b.js执行完毕!
在a.js中,b.done = true
a.js执行完毕!
在main.js中,a.done = true, b.done = true

ES6模块循环引用的例子:

//even.js
import {odd} from './odd';
var counter = 0;
export function even(n){
counter++;
console.log(counter);
return n == 0 || odd(n-1);
}

//odd.js
import {even} from './even.js';
export function odd(n){
return n != 0 && even(n-1);
}

//index.js
import * as m from './even.js';
var x = m.even(5);
console.log(x);

var y = m.even(4);
console.log(y);

输出:

1
2
3
false
4
5
6
true

可以看出counter的值是累加的,ES6是动态引用。如果上面的引用改为CommonJS代码,会报错,因为在odd.js里,even.js代码并没有执行。

IIFE

Immediately Invoked Function Expression,只是一种写法,可以隐藏一些局部变量。

可以用来代替 UMD 作为纯粹给前端使用的写法。

参考文章