浏览器模块化详解
ES6 Module
是ES6
中规定的模块体系。传统script
标签的代码加载容易导致全局作用域污染。
<script>
const a = 1;
</script>
<script>
console.log(a) // 1
</script>
像上面的demo
,即使是不同的script
,但是能互相访问。项目一大,维护起来越来越困难。
Javascript
社区做了很多努力,在现有的运行环境中,实现”模块”的效果。
最原始的写法应该就是把不同的函数简单放在一起,当作一个模块。
function module1() {
// ...
}
function module2() {
// ...
}
这种写法其实缺点很明显,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。
为了解决函数的缺点,把模块写成一个对象,所有模块成员都放入对象中。
const module = {
counter: 0,
module1: () => {
// ...
},
module2: () => {
// ...
}
}
这样在调用模块的时候直接调用对象的属性就可以了,但是这样又把所有的模块成员暴露了,使得内部的状态可以被外部改写。
module.counter++; // 外部修改couter值
使用立即执行函数就可以达到不暴露私有成员的目的了。
const module = (function() {
let counter = 0;
const module1 = () => {
// ...
}
const module2 = () => {
// ...
}
return {
module1,
module2
}
})()
module.counter // undefined
主流模块
在es6
以前,还没有提出一套官方的规范,从社区和框架推广程度而言,目前通行的javascript
模块规范有两种:CommonJS
和 AMD
。
CommonJS
主要用于node
服务端。
在CommonJS
中,暴露模块使用module.exports
和exports
。
在CommonJS
中,有一个全局性方法require()
,用于加载模块。
// 引入http模块
const http = require('http');
// 使用http模块中的方法
http.createServer(/*...*/);
AMD
有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。
但是CommonJS
不适用于浏览器环境,如果要在浏览器中使用,如果想要使用对应的模块,需要提前加载。像上方的http
模块,如果像使用对应的方法,就需要等到http
加载完成后才能使用。如果加载时间很长,整个应用就会停在那里等。
但是由于服务端所有的模块都存放在本地硬盘上,可以同步加载完成(硬盘读取时间很快),但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态。
因此,浏览器端的模块,不能采用”同步加载”(synchronous
),只能采用”异步加载”(asynchronous
)。这就是AMD
规范诞生的背景。
AMD
是Asynchronous Module Definition
的缩写,采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
模块必须采用特定的define()
函数来定义。
语法
define(id?, dependencies?, factory)
id
:字符串,模块名称(可选)dependencies
: 是我们要载入的依赖模块(可选),使用相对路径。注意是数组格式factory
: 工厂方法,返回一个模块函数
如果一个模块不依赖其他模块,那么可以直接定义在define()
函数之中。
define(function() {
let counter = 0;
const add = () => {
console.log('add')
return counter++;
}
return {
add
}
})
如果这个模块还依赖其他模块,那么define()
函数的第一个参数,必须是一个数组,指明该模块的依赖性。
define(['Lib'], function(Lib){
function foo(){
Lib.doSomething();
}
return {
foo : foo
};
});
使用require()
函数来加载上面定义的模块。
AMD
也采用require()
语句加载模块,但是不同于CommonJS
,它要求两个参数。
语法:
require([module], callback);
module
: 数组,里面的成员就是要加载的模块。callback
: 加载成功之后的回调函数,这个函数接收一个参数,就是define
中factory
的返回值。
require(['./test.js'], function(test) {
test.add();
})
test
这个文件中定义的模块的加载跟require
中的回调函数不是同步执行的,并不会出现假死的状态。
所以很显然,AMD比较适合浏览器环境。
目前,主要有两个Javascript
库实现了AMD
规范:require.js
和curl.js
。
如果想要使用
require
、define
,需要提前引入对应的模块。
像使用require.js
,需要在页面中引入。
去官网下载最新版本,直接放到页面进行加载<script src="js/require.js"></script>
CMD
CMD
(Common Module Definition
), 是seajs
推崇的规范。与AMD
不同的是,CMD
是在需要模块的时候才使用require
引入。
define
的使用跟AMD
一样。
require
的调用时机不同。
比如我们想在某个模块中导入其他模块
define(function() {
const xxx = require('xxx');
})
AMD
就需要提前在define
中说明。当然在CMD
也可以这样使用。
CMD
与AMD
区别
AMD
和CMD
最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。
AMD
依赖前置,js
可以方便知道依赖模块是谁,立即加载;
而CMD
就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD
的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。
现阶段标准
ES6
标准发布后,module
成为标准。就是我们熟悉的import
、export
语法。
标准使用是以export
指令导出接口,以import
引入模块,但是在node
服务端中,依然采用的是CommonJS
规范,使用require
引入模块,使用module.exports
导出接口。
export
export
语法声明用于导出函数、对象、指定文件(或模块)的原始值。
注意:在
node
中使用的是exports
,不要混淆了
export
有两种模块导出方式:命名式导出(名称导出)和默认导出(定义式导出),命名式导出每个模块可以多个,而默认导出每个模块仅一个。
命名式导出
通过export
前缀关键词声明导出对象,导出对象可以是多个。这些导出对象用名称进行区分,称之为命名式导出。
function module1() {
// ...
}
// 导出一个定义的函数
export {
module1
}
// 导出一个遍历
export const a = 1;
使用*
和from
关键字来实现的模块的继承
export * from 'module1';
使用as
关键字对导出成员进行重命名
export {a as b};
默认导出
默认导出也被称做定义式导出。命名式导出可以导出多个值,但在在import
引用时,也要使用相同的名称来引用相应的值。而默认导出每个导出只有一个单一值,这个输出可以是一个函数、类或其它类型的值,这样在模块import
导入时也会很容易引用。
// 可以导出一个函数
export default function() {};
// 也可以出一个类
export default class(){};
默认导出可以理解为另一种形式的命名导出,默认导出可以认为是使用了default
名称的命名导出。
const a = 1;
export default a;
export { a as default };
import
import
语法声明用于从已导出的模块、脚本中导入函数、对象、指定文件(或模块)的原始值。
import
模块导入与export
模块导出功能相对应,也存在两种模块导入方式:命名式导入和默认导入。
命名式导入
通过指定名称,就是将这些成员插入到当作用域中。导出时,可以导入单个成员或多个成员。
function module1() {
// ...
}
// 导出一个定义的函数
export {
module1
}
// 引入的函数名需要跟导出的一致
import { module1 } from 'xxx';
通过*
符号,我们可以导入模块中的全部属性和方法。当导入模块全部导出内容时,就是将导出模块所有的导出绑定内容,插入到当前模块的作用域中。
function module1() {
// ...
}
const a = 1;
export {
module1,
a
}
import * as module1 from 'xxx';
// 使用
module1.a;
module1.module1
导入模块对象时,也可以使用as
对导入成员重命名,以方便在当前模块内使用:
function module1() {
// ...
}
const a = 1;
export {
module1,
a
}
import { a as b } from 'xxx';
console.log(b)
默认导出
const a = 1;
export default a;
import a from 'xxx';
console.log(a)
在script标签中使用模块
如果文件中使用import
、export
这些标准语法,需要在script
标签中携带type="module"
属性,不然会报错。
<script src="xxx.js" type="module"></script>
这样,不同的script
之前就不能互相访问属性了。
像使用vue2.x
时候我们发现,index.html
的script
标签为什么没有type="module"
属性呢?是因为底层使用webpack
,对import
语法进行了处理,之后并不存在import
语法了。