前端模块化
模块化也就是将一个系统细分为多个小单元,这些小单元是抽象的、可扩充的、可复用的逻辑代码。模块化是工程化的基础,只有能将代码模块化,拆分组织为合理的单元结构,才能使其具备调度整合的能力。
模块化发展历程
模拟时期
JavaScript 偶然的成功以及初期设计不完善的原因,在其成为客户端主流脚本时,缺少了很多成熟语言具有的特性。此时的程序员如果需要使用模块化思想解决命名空间问题,则需要借助对象来解决:
var module1 = {
foo: 'bar',
baz: function () {
console.log(this.foo);
},
};
复制代码
这里勉强实现了一个命名空间module1
,也可以调用该命名空间中的属性以及方法。但这种实现方式有个致命缺点:不安全。任何开发者、使用者都可以对其进行修改,极易产生BUG和未知情况。
使用IIFE可以可以解决数据访问问题:
var module2 = (function () {
var foo = 'bar';
var baz = function (value) {
foo = value;
};
return {
baz,
};
})();
复制代码
通过这种模式,可以隔断外界对内部属性的访问和无限制修改。也就是开发者只能通过这个模块暴露出来的接口,进行数据的读写和方法的调用,模块暴露了什么,你才能使用什么。
这是以后模块化发展的基础,尽管它没有得到原生语言的支持,只是开发者”投机取巧“编写出来的模拟实现。而且这种方式面对循环引用和处理模块间的引用非常繁琐。
CommonJS & AMD => UMD
CommonJS是2009年由社区提出的一个包含模块化的标准,后来被Node.js进行略微修改后采纳作为其模块化标准。CommonJS和Node是一个==
关系,而不是===
!
CommonJS将一个文件看作一个模块,文件内的代码运行在独立的作用域中,不会污染全局空间。模块可以被多次引用、加载,CommonJS的模块加载是一个同步操作,在首次加载完毕后,会进行缓存,之后有模块引用则直接使用缓存数据。导出属性module.exports
导出的是值的拷贝,而不是引用,也就是说在值被导出后,之后对其发生的变化都不会影响到输出的值。模块按照导出顺序进行加载。
// module.js
// 导出
module1.exports = {
fn,
}
// 或
exports.fn = fn
// index.js
// 导入
const module3 = require('./module.js')
module3.fn()
复制代码
CommonJS一般应用于服务端,文件一般存储在磁盘中,无需进行额外的网络进行异步加载,因此CommonJS规范加载模块是同步的,只有加载完成,才能执行后续操作。但这不太适用于浏览器环境,因为客户端一般都需要进行大量的异步网络请求以获取资源。为此,AMD应运而生。
如前所述,AMD标准加载模块时是异步的,完全适用于浏览器。AMD模块实现的核心是使用一个IIFE函数包装模块定义,防止污染全局变量,并且允许加载器库控制加载模块的时机。包装模块的函数是全局define
的参数,它由AMD加载器库实现定义。比较有名的AMD加载器是require.js
库,感兴趣的读者可以去阅读其源码。
通用模块定义
小孩子才会做选择,大人全都要。UMD模块规范允许在环境中同时使用AMD与CommonJS规范,相当于一个整合方案。核心思想在于利用立即执行函数根据环境来判断需要的参数类别,其实也就是定义一个标志,确定运行环境以确定执行哪个规范。
ES原生模块
喜大普奔,JavaScript终于在ES6诞生了原生的模块化解决方案。原生浏览器的支持意味着不再需要预处理器和额外的加载器,也就是说,模块加载器会被原生模块系统慢慢取代。
ES6模块作为一整块JavaScript代码而存在:
<script type="module">
/* something */
</script>
复制代码
这样的script
标签会像<script defer>
一样按顺序执行,在解析到模块标签后会立即执行下载操作,但执行会等到文档解析完成,无论是嵌入的模块代码还是引入的外部模块代码,都是如此。还可以给模块标签加上async
属性,这样的话模块的执行顺序就取决于异步结果何时返回了,当结果返回就会立即执行,不过入口模块仍然需要等待其依赖加载完成,这也不难理解。
ES模块输出的是一个值的引用,也就是说,后续操作中对值的改变也会影响到模块:
// data.js
export let data = 'data';
export const handleChangeData = () => {
data = 'xxx';
};
// index.js
import { data, handleChangeData } from './data.js';
console.log(data); // data
handleChangeData();
console.log(data); // xxx
复制代码
ES模块的设计思想是尽量静态化,这样可以保证在编译时就确定模块之间的依赖关系,每个模块的输入/输出变量也是确定的。静态化的一个优势是快,通过静态分析,可以更快的分析出模块依赖图,如果导入的模块没有被使用,就可以使用tree shaking等手段减少代码体积,提升运行性能。
何谓动态静态:CommonJS导入时可以使用表达式
require(path)
这就是动态。
静态化也会带来一些限制:
- 只能在文件顶部引入依赖
- 导出的变量类型受到严格限制
- 变量不允许被重写绑定,引入的模块名只能是字符串常量,也就是不能动态绑定依赖