前端在发展初期仅仅被用作制作一些简单的页面,此时的页面逻辑比较简单,所以也没有太大的模块化需求。但是随着后期的发展,web 端的功能和逻辑越来越复杂,我们必须要有一套模块化的规范去管理我们的应用。
前端模块化的发展历程十分曲折,大致的历程如下
无模块化规范 => CommonJS 规范 => AMD/CMD 规范 => ES6 Module
初期我们没有统一的模块化规范,但是我们本地的代码还是需要逻辑清晰便于维护的,普遍的一些做法是使用立即执行的匿名函数去保证变量不被污染。
var module = (function($, _) {
// code
} (jQuery, lodash));
这样保证 module 内部自己的变量不会被外部污染,通过参数传递确定内部的依赖,是初期简单的模块化实现。详细历程在这里,有兴趣的同学可以去看下。
2009 年,nodejs 横空出世,将 js 运用于服务端。由于服务端逻辑的复杂性,迫切的需要一种统一的模块化规范。于是 nodejs 参照 CommonJS 的规范实现了模块系统。
CommonJS 规范的核心思想是允许模块通过 require 方法来同步加载所要依赖的其他模块,然后通过 exports
或 module.exports
来导出需要暴露的接口。
// 导入
const fs = require('fs')
fs.readFileSync(new URL('file://hostname/p/a/t/h/file'))
const util = require('../util.js')
// 导出
module.exports = { util }
exports.util = function() {}
CommonJS 简单方便的实现了 nodejs 的模块引用,一直被应用于服务端的开发。但是由于其同步加载
模块的特性,导致客户端使用时会出现很多问题。
在服务端,我们的模块文件是从磁盘中读取,速度非常的快,同步加载不会出现问题。但是在客户端,网络环境千差万别,同步加载会阻塞其他脚本的执行,所以在客户端我们需要一种异步加载的模块化规范。
注:module.exports
和 exports
是有差别的,module.exports
本身就是一个对象,而 exports
是指向 module.exports
的一个引用,所以在使用上我们使用上面的方式去导出,而 exports = { util }
的方式会重写 exports
。
AMD 是 Asynchronous Module Definition 的缩写,意为“异步模块定义”,它是为浏览器环境设计的模块化规范。 RequireJS
是基于 AMD 规范实现的。
AMD 规范的语法如下
define(id?: String, dependencies?: String[], factory: Function|Object);
它要在声明模块的时候指定所有的依赖 dependencies,并且还要当做形参传到 factory 中,对于依赖的模块提前执行,依赖前置。
id
是模块的名字,它是可选的参数。
dependencies
指定了所要依赖的模块列表,它是一个数组,也是可选的参数,每个依赖的模块的输出将作为参数一次传入 factory
中。如果没有指定 dependencies
,那么它的默认值是 ["require", "exports", "module"]
。
define(function(require, exports, module) {})
factory
是最后一个参数,包裹着模块的具体实现,它是一个函数或者对象。如果是函数,那么它的返回值就是模块的输出接口或值。
我们定义一个 math 模块,依赖于 jQuery 和一个我们本地的工具函数 util.js
// 定义
define('math', ['jQuery', './util.js'], function($, util) {
// code
const util = function() {}
// util 就是模块对外输出的接口
return util;
})
// 使用
require(['math'], function(math) {})
也可以在内部引用其他模块(注:这里不属于 AMD 的规范,是 RequireJS 2.0 加入的,对 SeaJS 写法的兼容,使 RequireJS 也支持按需加载)
// 定义
define('math', function(require) {
const $ = require('jQuery')
const util = require('./util.js')
// code
const util = function() {}
// 也可以 exports 导出
return util;
})
// 使用
require(['math'], function(math) {})
在使用时我们需要用 require
函数引用模块。
require(['jQuery', 'lodash'], function($, _) {
// code
}, function(err) {
// error
})
require
接收三个参数。
可以去查看规范的细节,AMD 规范
CMD 是 Common Module Definition 的缩写,SeaJS
是基于 CMD 规范实现的。
CMD 规范规定,一个文件就是一个模块,定义语法如下
define(factory)
factory
可以是一个函数、对象、数组、字符串。factory
为对象、数组、字符串时,表示模块的接口就是该对象、字符串。
define({ "foo": "bar" })
define(['foo', 'bar']);
define('I am a template. My name is {{name}}.')
当 factory
是一个函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。actory
方法在执行时,默认会传入三个参数:require
、exports
和 module
。
define(function(require, exports, module) {
const util = require('./util.js')
// code
// 导出模块,也可以直接 return
exports.math = function() {}
})
具体的细节可以去这里查看,CMD 规范,CMD 规范2
最后 AMD 和 CMD 的区别
// CMD
define(function(require, exports, module) {
const a = require('./a')
a.doSomething()
// code ...
const b = require('./b') // 依赖可以使用时书写
b.doSomething()
})
// AMD
define(['./a', './b'], function(a, b) {
// 依赖必须一开始就写好
a.doSomething()
// code ...
b.doSomething()
})
最后附:RequireJS 和 SeaJS 的区别
ES6 的模块化,大家可以直接去看阮一峰老师的教程
ES6 Module 和其他模块规范较大的区别是:
ES6 静态的 import 引入的模块,是强制开启严格模式的。
详细可以查看 MDN 的描述
静态的 import 语句用于导入由另一个模块导出的绑定。无论是否声明了 strict mode ,导入的模块都运行在严格模式下。
ES6 的引入,是对变量的引入,对原文件中值的改变会影响到 import 导入的变量。(这里只有 export 导出的模块是变量导出,export default 导出的是值,不会影响)
// a.js
var count = 1
function add () {
return count++
}
export { count, add }
// b.js
import { count, add } from './a.js'
console.log(count) // 1
add()
console.log(count) // 2