兼容前后端多种JS模块规范的代码

最近研究如何在前后端调用同一份js代码。该问题本质是如何兼容前后端各类JS模块规范。上网一搜,已经有很多详细的解决方案和JS模块规范介绍,在这里简单记录一下。

JS模块规范

JS模块规范前端主要有AMD、CMD,后端主要有CommonJS。此外还有最新的ES2015模块规范。

AMD

AMD(异步模块定义,Asynchronous Module Definition)格式总体的目标是为现在的开发者提供一个可用的模块化 JavaScript 的解决方案。它诞生于 Dojo 在使用 XHR+eval 时的实践经验,其支持者希望未来的解决方案都可以免受由于过去方案的缺陷所带来的麻烦。

AMD风格模块定义通常包括:一个用来进行模块定义的 define 方法以及一个用来处理依赖项加载的 require 方法。define 根据如下的方法签名来定义具名或匿名的模块:

1
2
3
4
5
define(
module_id /*可选*/,
[dependencies] /*可选*/,
definition function /*用来初始化模块或对象的函数*/
);

CMD

CMD(Common Module Definition)表示通用模块定义,该规范是国内发展出来的,由阿里的玉伯提出。就像AMD有个requireJS,CMD有个浏览器的实现SeaJS,SeaJS和requireJS一样,都是javascript的前端模块化解决方案。
CMD规范简单到只有一个API,即define函数:

1
2
3
4
5
define(function(require, exports, module) {

// The module code goes here

});

CMD 与 AMD 挺相近,二者区别如下:

  • 对于依赖的模块 CMD 是延迟执行,而 AMD 是提前执行(不过 RequireJS 从 2.0 开始,也改成可以延迟执行。 )
  • CMD 推崇依赖就近,AMD 推崇依赖前置。
  • AMD 的 api 默认是一个当多个用,CMD 严格的区分推崇职责单一,其每个 API 都简单纯粹。例如:AMD 里 require 分全局的和局部的。CMD 里面没有全局的 require,提供 seajs.use() 来实现模块系统的加载启动。

CommonJS

CommonJS是一个志愿性质的工作组,它致力于设计、规划并标准化 JavaScript API。至今为止他们已经尝试着认可了模块标准以及程序包标准。CommonJS 的模块提案为在服务器端声明模块指定了一个简单的 API。不像 AMD,它试图覆盖更宽泛的方面比如 IO、文件系统、promise 模式等等。CommonJS风格模块在nodejs中得到广泛的应用。

CommonJS风格模块是一段可重用的 JavaScript,它导出一系列特定的对象给依赖它的代码调用——通常来说这样的模块外围没有函数包裹(所以你在这里的例子中不会看到 define)。例如:

1
2
3
4
5
6
7
8
9
10
// package/lib 是我们须要的一个依赖项
var lib = require('package/lib');

// 我们的模块的一些行为
function foo(){
lib.log('hello world!');
}

// 把 foo 导出(暴露)给其它模块
exports.foo = foo;

ES2015模块规范

2015 年 6 月, ES2015(即 ECMAScript 6、ES6) 正式发布。ES2015 是该语言的一个显著更新,也是自 2009 年 ES5 标准确定后的第一个重大更新。

ES2015的模块规范如下:

  • 一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。
  • export 命令用于规定模块的对外接口。
  • import 命令用于输入其他模块提供的功能。
  • ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

ES2015模块示例如下:

1
2
3
4
5
6
7
8
9
//circle.js
//圆面积计算
export function area(radius) {
return Math.PI * radius * radius;
}

//main.js
import {area} from './hangge';
console.log('圆面积:' + area(10));

兼容多种模块规范

需要指出的是下面兼容多种JS模块规范的代码并不支持最新的ES2015模块规范。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
;(function (name, definition) {
// 检测上下文环境是否为AMD或CMD
var hasDefine = typeof define === 'function',
// 检查上下文环境是否为Node
hasExports = typeof module !== 'undefined' && module.exports;

if (hasDefine) {
// AMD环境或CMD环境
define(definition);
} else if (hasExports) {
// 定义为普通Node模块
module.exports = definition();
} else {
// 将模块的执行结果挂在window变量中,在浏览器中this指向window对象
this[name] = definition();
}
})('hello', function () {
var hello = function () {};
return hello;
});

Webpack兼容所有JS模块规范

webpack根据webpack.config.js中的入口文件,在入口文件里识别模块依赖,不管这里的模块依赖是用CommonJS写的,还是ES6 Module规范写的,webpack会自动进行分析,并通过转换、编译代码,打包成最终的文件。最终文件中的模块实现是基于webpack自己实现的webpack_require(es5代码),所以打包后的文件可以跑在浏览器上。

同时以上意味着在webapck环境下,你可以只使用ES6 模块语法书写代码(通常我们都是这么做的),也可以使用CommonJS模块语法,甚至可以两者混合使用。因为从webpack2开始,内置了对ES6、CommonJS、AMD 模块化语句的支持,webpack会对各种模块进行语法分析,并做转换编译。

具体Webpack兼容所有JS模块规范的原理分析参见Webpack 模块打包原理

参考链接

  1. 兼容前后端共用模块代码(摘自《深入浅出Node.js》), by Jake.
  2. 兼容多种模块规范(AMD,CMD,Node)的代码, by CodeMan.
  3. 使用 AMD、CommonJS 及 ES Harmony 编写模块化的 JavaScript, by ADDY OSMANI.
  4. JS - CommonJS、ES2015、AMD、CMD模块规范对比与介绍(附样例),by hangge.
  5. What is Babel?,by Babel.
  6. Webpack Concepts,by webpack.
  7. import、require、export、module.exports 混合使用详解,by lv_DaDa.
  8. Webpack 模块打包原理,by lq782655835.