JavaScript 模块的发展
什么是模块?
在模块化编程中,开发者将程序拆分成很多离散的功能块,这些块块被称作模块(module)。
通常,模块有这样几个好处:
- 可维护性。模块是独立的,一个设计良好的模块会让外界对自身有很少的依赖。这样,自身就可以独立进行更新和改进。
- 命名空间。JS 中,如果一个变量在顶层函数之外进行声明,它就变成全局可用。由此带来的命名冲突,对实际开发会有很大的影响。
- 代码重用。我们有时候会喜欢从之前写过的项目中拷贝代码到新的项目,这没有问题,但是更好的方法是,通过模块引用的方式,来避免重复的代码库。我们可以在更新了模块之后,让引用了该模块的所有项目都同步更新,还能指定版本号,避免 API 变更带来的麻烦。
模块化在日常开发中的重要性不言而喻。尤其是当页面复杂度不断增高,单一的页面逐渐变成网页应用,充分解耦的 JS 文件/模块对代码拆分、tree shaking、部署优化都有十分重要的作用。
JS 早期并未提供对模块的支持,直到 ES2015,才有课原生的 import
和 export
。在此之前还涌现出了很多模块规范,如 CommonJS、AMD等。本文将归纳一下在发展过程中,出现的一些模块化解决方案。
从设计模式说起
早期,我们这样写代码:
function foo() {
//...
}
function bar() {
//...
}
但是,这样写的话全局(Global)容易被污染,容易造成命名冲突。
简单封装,Namespace 模式。这样做可以减少全局的变量数,但同时,MYAPP 本质上是对象,安全性堪忧。
var MYAPP = {
foo: function (){},
bar: function (){}
}
MYAPP.foo()
匿名闭包,IIFE 模式。函数式 JavaScript 唯一的 Local Scope。
var Module = (function () {
var _private = 'safe private'
var foo = function () {
console.log(_private)
}
return {
foo: foo
}
})()
Module.foo() // 'safe private'
Module._private // undefined
// 引入依赖
var Module = (function ($) {
var _$body = $('body')
var foo = function () {
console.log(_$body) // 特权方法
}
// Revelation Pattern
return {
foo: foo
}
})(jQuery)
Module.foo()
这就是模块模式(Module Pattern),也是现代模块实现的基石。
Script Loader
只有封装性可不够,同时还需要解决加载的问题。
body
script(src='jquery.js')
script(src='app.js') // do some $ things...
DOM 的顺序就是执行的顺序,并行加载。由此带来很多问题:
- 难以维护
- 依赖模糊
- 请求过多
LABjs Script Loader(2009)
script(src='LAB.js' async)
$LAB.script('framework.js').wait()
.script('plugin.framework.js')
.script('myplugin.framework.js').wait()
.script('init.js')
First Come, First Serve
$LAB
.script(['script1.js', 'script2.js', 'script3.js'])
.wait(function () {
script1Func()
script2Func()
script3Func()
})
基于文件的依赖管理。
Module Loader
YUI3 Loader, Module Loader(2009)
YUI is lightweight core and modular architecture make it scalable, fast and robust.
CommonJS - API 标准
模块的定义和引用
// math.js
exports.add = function(a, b) {
return a + b
}
// main.js
var math = require('math')
console.log(math.add(1, 2)) // 3
node.js 简单的 http 服务器
// server.js
var http = require('http')
var PORT = 8000
http.createServer(function(req, res){
res.end("Hello World")
}).listen(PORT)
console.log("listenning to " + PORT)
node server.js
AMD/CMD 浏览器环境模块化方案
AMD(Async Module Definition): RequireJS 对模块定义的规范化产出。JavaScript 文件和模块的加载器,针对浏览器端进行优化。
CMD(Common Module Definition): SeaJS 对模块定义的规范化产出。
// AMD Wrapper
define(
['types/Employee'], // 依赖
function (Employee){ // 这个回调会在所有依赖被加载后才执行
function Programmer() {
// do something
}
Programmer.prototype = new Employee()
return Programmer // return constructor
}
)
// CommonJS 的简化写法
define(function(require) {
var dependency1 = require('dependency1')
var dependency2 = require('dependency2')
return function() {}
})
// parse out require...
define(
['require', 'dependency1', 'dependency2'],
function (require) {
var dependency1 = require('dependency1'),
dependency2 = require('dependency2')
return function () {}
})
AMD 和 CommonJS 的书写风格对比
// CommonJS
var a = require("./a"); // 依赖就近
a.doSomething()
// AMD recommended style
define(["a", "b"], function(a, b){ // 依赖前置
a.doSomething()
b.doSomething()
})
AMD 和 CommonJS 的执行时机对比
var a = require("./a"); // 执行到此时,a.js 同步下载并执行
// AMD with CommonJS sugar
define(["require"], function(require){
// 在这里, a.js 已经下载并且执行好了
var a = require("./a")
})
// 早下载早执行
RequireJS 最佳实践
// use case
require([
'React', // 尽量使用 ModuleID
'IScroll',
'FastClick'
'navBar', // 和同目录下的 js 文件
'tabBar',
], function(
React, // Export
IScroll
FastClick
NavBar,
TabBar,
) {}
)
// config
require.config({
// 查找根路径,当加载包含协议或以/开头、.js结尾的文件时不启用
baseUrl: "./js",
// 配置 ModuleID 与 路径 的映射
paths: {
React: "lib/react-with-addons",
FastClick: "http://cdn.bootcss.com/fastclick/1.0.3/fastclick.min",
IScroll: "lib/iscroll",
},
// 为那些“全局变量注入”型脚本做依赖和导出配置
shim: {
'IScroll': {
exports: "IScroll"
},
},
// 从 CommonJS 包中加载模块
packages: [
{
name: "ReactChart",
location: "lib/react-chart",
main: "index"
}
]
})
node r.js -o build.js
// build.js
// 简单的说,要把所有配置 repeat 一遍
({
appDir: './src',
baseUrl: './js',
dir: './dist',
modules: [
{
name: 'app'
}
],
fileExclusionRegExp: /^(r|build)\.js$/,
optimizeCss: 'standard',
removeCombined: true,
paths: {
React : "lib/react-with-addons",
FastClick: "http://cdn.bootcss.com/fastclick/1.0.3/fastclick.min",
IScroll: "lib/iscroll"
},
shim: {
'IScroll': {
exports: "IScroll"
},
},
packages: [
{
name: "ReactChart",
location: "lib/react-chart",
main: "index"
}
]
})
Browserify & Webpack
webpack(Universal Module System): 对所有模块一视同仁的依赖管理。
对比 node.js 的模块,webpack 的模块可以用多种方式表示它的依赖。例如:
- ES2015 的
import
- CommonJS 的
require()
- AMD 的
define
和require
- 在 css/sass/less 文件中的
@import
- 在样式表中的图片
url(...)
或 html 文件中<img src="..." />>
ES6 Module
There is mo module in JavaScript until ES6.
但运行时并不完善,缺少运行时(runtime),Babel的出现,使我们可以在当下使用下一代的 JavaScript。
Single defaule module
// math.js
export default math = {
PI: 3.14,
foo: function (){}
}
// app.js
import math from './math.js'
math.PI // 3.14
# babel magic
$ bable-node app.js
named exports
// export declaration
export function foo() {
console.log('I am not bar.')
}
// export variable statement
const PI = 3.14
foo = function (){}
export { PI, foo }
Importing named exports
// import { ImportsList } from "module-name"
import { PI } from "./math"
import { PI, foo } from "module-name"
// import IdentifierName as ImportedBinding
import { foo as bar } from "./math"
bar() // use alias bar
// import NameSpaceImport
import * as math from "./math"
math.PI
math.foo()
参考