工作中,随着前端项目的扩张,无论从代码精简,还是团队合作,上线部署等各个方面来说,模块化已经不可或缺。
模块化的最大作用就是提高代码的复用率,解耦,减少冲突。
前端模块化的规范,比较著名的CommonJs AMD CMD ES6模块化。这也是模块化演变的进程。
1.CommonJs
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。
CommonJs定义的模块可以分为三个部分:
require用来引入模块exports对象用于导出当前模块的方法或变量,是唯一的导出口module这个对象就代表模块本身
特性
- 同步加载模块
- 所有要输出的对象统统挂载在 module.exports 上,然后暴露给外界
- 设计模式是单例模式,只会生成一个实例对象
- 代码运行在模块作用域内,不会污染全局作用域,不会产生命名空间冲突
Node应用就是采用CommonJS模块规范的典型。
我们来看一个简单的例子,定义一个methods对象,然后暴露出去
var methods = {};
<!--可以这样输出-->
module.exports.methods = methods;
<!--也可以这样输出,相当于隐藏一行 -->
<!--var exports = module.exports; -->
exports.methods = methods;
<!--也可以包装一层再输出-->
module.exports = { methods: methods };
引用:
var obj = require('./methods.js');
console.log(obj.methods);
但是,前面我们写到CommonJS的一个重要特性是加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
这种规范在用于服务器变成时,没什么毛病,因为模块文件一般都存于本地硬盘中,所以加载起来也比较快。
但是放在浏览器环境中呢?
这个时候的资源大都放在服务器上,这个时候再采取同步加载,很有可能因为网速等原因造成浏览器“假死”的情况,用户体验十分不好。
这就引申出了AMD规范。
2.AMD规范
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。
比较著名的AMD规范的典型就是Require.js,我们可以通过requireJS一窥一二
Require.js
Require.js定义了两个显性API:
define(id, [depends], callback)定义模块require([module], callback)加载模块
基本思想是,通过define方法,将代码定义为模块;通过require方法,实现代码的模块加载。
我们直接看代码:
define可以定义独立模块和非独立模块(也就是含有依赖)
<!--独立模块-->
define(function(){
return {
m: 0,
methods: function() {}
}
})
<!--非独立模块-->
<!--m1, m2的参数与前面的依赖数组是相对应的-->
define(['module1', 'module2'], function(m1, m2){
return {
m: 0,
methods: function() {}
}
})
require引用:
require(['module1', 'module2'], function ( m1, m2 ) {
m1.doSomething();
m2.doSomething();
});
require也可以实现动态加载以及第三方加载,详细可参考阮一峰老师的《RequireJS和AMD规范》,这里不详细举例了。
我们了解了AMD规范是异步加载模块,不影响页面构建进程,但是AMD规范的一个显著特性是预加载,就是遇到的引入包都要提前执行加载(2.0之后也可以改成延迟执行,此处按下不表)比如:
define(function(require, exports, module) {
console.log('start');
var a = require('./a');
a.doSomething();
var b = require('./b');
b.doSomething();
return {
c: function() {
console.log('this is c');
}
};
});
这个执行顺序是什么呢?
加载a =>
加载b =>
console.log('start'); =>
a.doSomething(); =>
b.doSomething(); =>
console.log('this is c');
其实就相当于:
define(['a', 'b'], function(require, exports, module) {
console.log('start');
a.doSomething();
b.doSomething();
return {
c: function() {
console.log('this is c');
}
};
});
这种就是依赖前置,也是AMD规范的一个显著特征。
那到这可能会想为什么不能用到的时候再加载呢?
这个时候就可以引申出CMD规范。
3.CMD规范
CMD规范,全称是Common Module Definition。
实现CMD规范的典范是Sea.js,虽然已经过时,但大家不妨了解一下。
CMD规范是延迟执行,就是说等执行到这了,我再加载,也就是懒加载。并且推崇依赖就近。
就是用到了,你再写,再执行加载。
define(function(require, exports, module) {
console.log('start');
var a = require('./a');
a.doSomething();
var b = require('./b');
b.doSomething();
return {
c: function() {
console.log('this is c');
}
};
});
那么上面那段代码拿到这里,执行顺序就会改变:
console.log('start'); =>
加载a =>
a.doSomething(); =>
加载b =>
b.doSomething(); =>
console.log('this is c');
这个可以说是两种规范最直观的差别了,但是两种规范的差别不仅仅只是这些。
4. AMD和CMD的不同之处
- 对于依赖的模块,
AMD是提前执行,CMD是延迟执行。不过RequireJS从2.0开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD推崇 as lazy as possible。 CMD推崇依赖就近,AMD推崇依赖前置AMD的 API 默认是一个当多个用,CMD的 API 严格区分,推崇职责单一。
5. ES6模块化
因为模块化愈发重要,ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
这和CommonJS AMD 规范是有根本区别的,因为CommonJS AMD 两者都是运行时加载。
模块功能主要由两个命令构成:export和import。
5.1 export
export命令用于规定模块的对外接口。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。
exports的使用:
<!--暴露变量-->
const a = 1;
const b = 2;
export {a, b};
<!--输出函数-->
function a() {...}
export { a }
<!--也可以设置别名-->
export { a as b }
5.2 import
import命令用于输入其他模块提供的功能。
特性:
import命令输入的变量都是只读,不可修改,因为它的本质是输入接口。但是如果是对象,可以修改对象的属性。import会提升到头部,在编译时执行。- 由于i
mport是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。 import语句是Singleton模式,只有一个实例,所以多次调用同一个包的不同方法,只会生成一个实例对象。
import命令的使用:
import { a, b } from './main'
<!--取别名-->
import { a as b } from './main'
<!--也可以直接加载模块-->
import 'lodash';
<!--模块整体加载-->
<!--此时的挂在对象a,应该是可以静态分析的,不允许运行时改变其任何属性-->
import * as a from 'lodash'
5.3 export default
从前面的例子可以看出,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
特性:
- 一个模块只能有一个
export default默认输出,输出变量名或方法为default。 export default加载时,无论是匿名函数还是有名函数,统一默认为匿名函数,需要在引用的时候重命名。- 使用
export default的输出,在import的时候,不需要使用{} export default可以输出变量,函数,还有类
代码展示一下:
<!--export-default.js-->
export default function () {
console.log('foo');
}
<!--也可以写成有名函数-->
export default function foo() {
console.log('foo');
}
<!--import-default.js-->
import customName from './export-default';
customName(); // 'foo'
也可以自定义default
<!--modules.js-->
function add(x, y) {
return x * y;
}
export {add as default};
<!--等同于-->
export default add;
<!--app.js-->
import { default as foo } from 'modules';
// 等同于
import foo from 'modules
输出类:
<!--MyClass.js-->
export default class { ... }
<!--main.js-->
import MyClass from 'MyClass';
let o = new MyClass();
5.4 export 与 import 的复合写法
如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。
export { a, b } from 'xxx';
<!--等同于-->
import { a, b } from 'xxx';
export { a, b }
<!--默认接口改为具名接口-->
export { default as es6 } from './someModule';
这里相当于一个中转,实际上并没有导入到当前模块,所以当前模块不能直接使用转发的接口。
5.5 import()
import()函数可以说是import的补全,因为import是静态加载,会先于模块内其他语句先执行,这样的设计固然有利于编译器提高效率,但也导致无法在运行时加载模块。
因此出现了提案,引入import()函数,实现动态加载。例如条件加载:
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
目前import()函数提案已到stage4阶段,大家其实可以放心用了,兼容性很乐观。
import()返回一个Promise对象,下面是一个例子:
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
特性:
import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()类似于Node的require方法,区别主要是前者是异步加载,后者是同步加载。
6.require和import的区别总结
require是同步导入,import是异步导入require是值拷贝,导出值变化不会影响导入值;import指向 内存地址,导入值会随导出值而变化require是运行时加载,import是编译时加载(静态加载)require可以实现第三方加载,import只能导入本地存在文件
I am moving forward.