前端模块化:CommonJS,AMD,CMD,ES6发展
前端模块化
模块化开发可以提高代码复用率,方便代码管理。一般来说一个文件就是一个模块,得外暴露变量或者函数。目前比较流行的模块化的规范有:AMD、CMD和CommonJs以及ES6 的 module。
AMD(Asynchronous Module Definition) | CMD(Common Module Definition) | CommonJs | ES6 Module |
---|---|---|---|
require.js 在推广过程中对模块化定义的规范产出的。 | sea.js在推广过程中对模块化定义的规范产出的。 | Node推广使用。 | |
提前执行(异步加载:依赖先执行)+延迟执行。 | 延迟执行(运行到需要时加载,根据顺序执行)。 | ||
浏览器 | 浏览器 | 服务器端 | 浏览器和服务器 |
依赖前置,提前执行(在定义模块的时候就声明依赖的模块) | 依赖就近,延迟执行(只有在需要用的时候才去require 执行) |
CommonJs | ES6 Module |
---|---|
运行时加载;CommonJs模块就是对象(module.exports属性)),即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法。 | 编译时加载;ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块。 |
输出的是值的拷贝(一旦输出一个值,模块内部的变化就影响不到这个值。) | 输出的是值的引用(JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。即原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。) |
CommonJS
commonjs主要由node推广使用。
关键命令:module.exports =
、require()
例子:
// 定义模块math.js
function add(a, b) {
return a + b;
}
module.exports = { //对外暴露的函数、变量
add: add
}
// main.js
// 引用自定义的模块时,参数包含路径,可省略.js
var math = require('./math');
math.add(2, 5);
// sever.js
// 引用核心模块时,不需要带路径
var http = require("http"),
PORT = 8000;
http.createServer(function(req, res){
res.end("Hello World");
}).listen(PORT);
console.log("listenning to " + PORT);
下面的例子可以体现[同步/阻塞式加载]的特性
// timeout.js
var EXE_TIME = 2;
(function(second){
var start = +new Date();
while(start + second*1000 > new Date()){}
})(EXE_TIME)
console.log("2000ms executed")
// main.js
require('./timeout'); // sync load
console.log('done!');
如果commonjs require方法是异步的话,下面这么写就会报错
//CommonJS Syntax
var Employee = require("types/Employee");
function Programmer (){
//do something
}
Programmer.prototype = new Employee();
//如果 require call 是异步的,那么肯定 error
//因为在执行这句前 Employee 模块根本来不及加载进来
从下图可以看出,同步加载对服务器/本地环境并不是问题。-> 浏览器环境会有问题!!
CommonJS是同步加载的,因此更适合服务器端。只有加载完成之后才能进行下面的操作。因为在服务器端模块文件一般存放在本地,再加上有缓存,加载速度十分快。
因此这种就不适合用在浏览器端,浏览器端的各个 script 标签中的文件来自各个服务器,如果上个模块加载的时间很长,就会导致浏览器“假死”,因此浏览器端我们采用另外一种异步的加载方式AMD。
AMD(require.js)
关键命令:define()
、require()
AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,才会运行这个回调函数。这里介绍用require.js实现AMD规范的模块化:用require.config()
指定引用路径等,用define()
定义模块,用require()
加载模块。
//AMD Wrapper
define(
["types/Employee"], //依赖
function(Employee){ //这个回调会在所有依赖都被加载后才执行
function Programmer(){
//do something
};
Programmer.prototype = new Employee();
return Programmer; //return Constructor
}
)
Require.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",
}
});
// 定义方式一:定义math.js模块
define(function () {
var basicNum = 0;
var add = function (x, y) {
return x + y;
};
return {
add: add,
basicNum :basicNum
};
});
// 定义方式二:定义一个依赖underscore.js的模块
define(['underscore'],function(_){
var classify = function(list){
_.countBy(list,function(num){
return num > 30 ? 'old' : 'young';
})
};
return {
classify :classify
};
})
// 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
var sum = math.add(10,20);
$("#sum").html(sum);
});
引用模块的时候,我们将模块名放在[]
中作为reqiure()
的第一参数;
如果我们定义的模块本身也依赖其他模块,那就需要将它们放在[]
中作为define()
的第一参数。
requirejs最佳实践
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"
}
]
})
CMD(sea.js)
CMD与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。
/** AMD写法 **/
define(["a", "b", "c"], function(a, b, c) {
// 等于在最前面声明并初始化了要用到的所有模块
a.doSomething();
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了
b.doSomething()
}
});
/** CMD写法 **/
define(function(require, exports, module) {
var a = require('./a'); //在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
sea.js的小例子🌰:
/** 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);
});
es6 module
关键命令:export
、export detault
、import
ES6 在语言标准的层面上,实现了模块功能,浏览器和服务器通用的模块解决方案。
export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}
ES6的模块不是对象,import
命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。