JavaScript 模块化
CommonJS
首先,最先出现的是 CommonJS, CommonJS API 定义很多普通应用程序(主要指非浏览器的应用)使用的API。它的终极目标是提供一个类似 Python, Ruby 和 Java 标准库。这样的话,开发者可以使用 CommonJS API 编写应用程序,然后这些应用可以运行在不同的 JavaScript 解释器和不同的主机环境中。
2009年,美国程序员 Ryan Dahl 创造了 node.js 项目,将 javascript 语言用于服务器端编程。这标志 Javascript 模块化编程”正式诞生。
NodeJS 是 CommonJS 规范的实现, webpack 也是以 CommonJS 的形式来书写。NPM 作为 Node 的包管理器,帮助Node 解决依赖包的安装问题,也要遵循 CommonJS。
Browserify
browserify 是最常用的 CommonJS 转换工具。
然后看个例子1
2
3
4
5
6
7
8// foo.js
module.exports = function(x) {
console.log(x);
};
// main.js
var foo = require("./foo");
foo("Hi");
1 | $ browserify main.js > compiled.js |
即可打包,Browserify 具体做了什么继续向下看即可:1
$ npm install browser-unpack -g
然后我们将上面生成的 compiled.js 解包。
1 | $ browser-unpack < compiled.js |
Browserify 在这里将所有模块放到一个数组里,id 是模块的编号,source 是源码,deps 为依赖,entry 为指定入口。
Browserify 先找到 entry: true 的地方,然后执行 source 中的代码,遇到 require 就去deps 中寻找依赖,并执行所依赖的代码,将结果赋值给 require 前的变量。
AMD
AMD ( Asynchronous Module Definition ),是为了解决 CommonJS 不能异步加载的问题。AMD 采用异步的方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等加载完成后,这个回调函数才会执行。
AMD 也采用 require() 语句加载模块,但是不同于 CommonJS,它要求两个参数:1
require([module], callback);
第一个参数 [module] ,是一个数组,里面的成员就是要加载的模块;第二个参数 callback ,则是加载成功之后的回调函数。如果将前面的代码改写成 AMD 形式,就是下面这样:
1 | require(['math'], function (math) { |
math.add() 与 math 模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD 比较适合浏览器环境。目前,主要有两个 Javascript 库实现了 AMD 规范: require.js 和 curl.js。
require.js
先去官方网站下载最新版本。
1 | 然后将它放到项目中即可 |
如果加载这个文件,也可能造成网页失去响应。解决办法有两个,一个是把它放在网页底部加载,另一个是写成下面这样:1
<script src="js/require.js" defer async="true" ></script>
async 属性表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持 defer,所以把 defer 也写上。
加载 require.js 以后,下一步就要加载我们自己的代码了。假定我们自己的代码文件是 main.js ,也放在 js 目录下面。那么,只需要写成下面这样就行了:
1 | <script src="js/require.js" data-main="js/main"></script> |
data-main 属性的作用是,指定网页程序的主模块。在上例中,就是js目录下面的 main.js ,这个文件会第一个被 require.js加载。由于 require.js 默认的文件后缀名是js,所以可以把 main.js 简写成 main。
主模块的写法:1
2
3
4// main.js
require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
// some code here
});
默认情况下,require.js 假定这三个模块与 main.js 在同一个目录,文件名分别为 moduleA.js,moduleB.js 和 moduleA.js,然后自动加载。
使用 require.config() 方法,我们可以对模块的加载行为进行自定义。require.config() 就写在主模块(main.js)的头部。参数就是一个对象,这个对象的 paths 属性指定各个模块的加载路径。
1 | require.config({ |
模块的写法
模块必须采用特定的 define() 函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在 define() 函数之中。
1 | // math.js |
ES6 module
ES6发布的 module 并没有直接采用 CommonJS ,甚至连 require 都没有采用,也就是说 require 仍然只是 node 的一个私有的全局方法,module.exports 也只是 node 私有的一个全局变量属性,跟标准半毛钱关系都没有。
区别
require 的使用非常简单,它相当于 module.exports 的传送门,module.exports 后面的内容是什么,require 的结果就是什么,对象、数字、字符串、函数……再把 require 的结果赋值给某个变量,相当于把 require 和 module.exports 进行平行空间的位置重叠,而且 require 理论上可以运用在代码的任何地方,甚至不需要赋值给某个变量之后再使用。在使用时,完全可以忽略模块化这个概念来使用 require,仅仅把它当做一个 node 内置的全局函数,它的参数甚至可以是表达式:
但是 import 则不同,它是编译时的(require是运行时的),它必须放在文件开头,而且使用格式也是确定的,不容置疑。它不会将整个模块运行后赋值给某个变量,而是只选择 import 的接口进行编译,这样在性能上比 require 好很多。
从理解上,require 是赋值过程,import 是解构过程,当然,require 也可以将结果解构赋值给一组变量,但是 import 在遇到 default 时,和 require 则完全不同: var $ = require('jquery'); 和 import $ from 'jquery' 是完全不同的两种概念。