这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战
在上一篇文章中,我们聊到了"史前时代"js是如何"模拟"模块化的,但本质上依旧是用“旁门左道”来实现的,并不是js语言层面的规范,要想真正实现模块化,肯定需要js自己本身去推出模块化相关的规范,因此这篇文章就准备聊聊这些规范,废话不多说,开搞!
Commonjs
Nodejs 使用的就是Commonjs规范,主要通过提供module,exports,require来实现模块化,require方能看到的只有module.exports这个对象,它是看不到exports对象的,而我们在编写模块时用到的exports对象实际上只是对module.exports的引用,通常情况下更建议使用 module.exports 而不是 exports,该规范的用法如下:
//a.js
var name = 'test'
module.exports = {
name
}
//b.js
var moduleA = require('./a.js')
console.log(moduleA.name)
这种规范具有如下几个特点
- 一个模块就是一个对象,是在 运行时 加载的,因此无法进行 静态分析
- 由于无法进行静态分析,因此导出的内容是 全量 的,无法进行 tree-shaking
- 加载模块是 同步的,因此只适合 服务端。因为服务端的资源都在本地,因此读取模块资源的速度很快,但是浏览器环境下资源都是 远程获取 的,因此不适合浏览器环境
- 模块的导出是值拷贝的方式,意思就是变量的变化无法影响外部模块
AMD
AMD规范的主要践行者是 Requirejs,它主要通过 define,require来实现模块化,具体用法如下
/** 网页中引入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
"test1": "test1",
"test2": "test2"
}
});
// 定义test1.js模块
define(function () {
return {
name: 'test1'
};
});
// 定义一个依赖test1.js的test2模块
define(['test1'],function(test1){
return {
name: test1.name + 'test2'
};
})
// 引用模块
require(["jquery","test1","test2"],function($,test1,test2){
// some code here
});
该模块规范的特点如下:
- 模块的导入是 异步的,因此适用于浏览器环境
- 依赖的模块无法按需导入,只能一次性全部导入,也就是所谓的“依赖前置”
CMD
CMD吸取了AMD和cjs的优点,因此是一种相对前两者更好的模块化方案,Seajs 是该规范的主要践行者,具体用法如下
//定义没有依赖的模块
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
//引入依赖模块(同步)
var module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3', function (m3) {
})
if(false) {
var moduleFalse = require('./moduleFalse') //该模块不会被导入
}
//暴露模块
exports.xxx = value
})
// main.js文件
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
//在index.html中引入
<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
seajs.use('./js/modules/main')
</script>
该模块规范有如下特点:
- 由于模块的导入同时支持同步和异步两种方式,因此服务端和浏览器都适用
- 支持模块的 按需导入,只有在真正需要使用依赖模块时才会去导入,也就是所谓的“就近依赖”,我认为这也是更合理的方案
ES module
ES module是 ES6 中提出来的模块化方案,旨在统一浏览器和服务器的模块化规范,也是直接从语言层面来规范js的模块化,所以未来ES module肯定是最终发展的方向,它的具体用法如下
/*a.js*/
export let a = 1
export default {
name:'yy'
}
/*b.js*/
import {a},aModule from './a.js'
console.log(a,aModule.name)
该规范有如下特点
- 模块的导出只是一个 静态的接口定义,因此在静态分析阶段就能确定模块之间的依赖关系
- 正是由于可以在静态分析阶段就能确定模块之间的依赖关系,所以天然支持tree-shaking
- 因为导出的是静态接口的定义,因此看作导出的是变量的 引用,因此模块内部变量的变化会影响外部的引用,这也是跟cjs存在差异的地方
- 由于在编译期就会导入模块的代码,因此不存在同步或者异步导入模块的说法
- 未来会成为浏览器和服务器的通用模块化方案
结语
未来是 ES module 的天下,只不过现在还处在一个过渡的时期,因此有必要去了解除ES module外的规范,这样我们才能对js这门语言有更加深刻的认识,从而成为一个专业的前端工程师,所以,加油吧,骚年!