JavaScript模块化演进

2023-10-09 pv

1. 背景

最近在重新学习前端。发现现在的前端开发,讲究所谓的“工程化”,想要具体了解可参照这篇博客🔗,很简短,看起来不费劲。

照我的理解,这种“工程化”的底层动力,来自目前项目开发的复杂度已经高到需要借助一定的工具才能有效管理。

这种复杂度,不仅来自协作成员的数量极速增加,项目与项目间的依赖增多,甚至包括因为敏捷开发运动导致的迭代节奏和频率加快,CI/CD等思想被引入等。

这其实是好事。说明前端的开发日益规范化,流程化。

尽管这个过程是渐进的,并非一簇而就。

最直接的例子,就是模块化(module)。

2. 历史演进

2.1 拆分

我在最开始写前端页面的时候,所有代码都放在一个大的.js文件里。简单,够用。

不过当单个.js文件里的内容陆续增加,维护就变得很头疼。

针对这个问题,计算机领域的工程实践中有一套固定的解决思路:拆分

怎么拆分也有技巧,借用面向对象的思想,拆分要讲究:高内聚,低耦合

历史上出现的模块化的方案很多:

  • AMD(Asynchronous Module Definition),异步模块定义,最早的模块系统由require.js🔗实现。
  • CommonJS,最初被Node.js采用,后来被废弃(deprecate)。
  • UMD(Universal Module Definition),通用模块定义,尝试提供一种跨端(Client/Server)的解决方案,兼容AMD和CommonJS,但最终没有被广泛使用。
  • ES Module,语言级的模块系统,终极解决方案,发布于ES6(2015)。

2.2 CommonJS

在很长一段时间内,JavaScript没有语言层面的模块系统。

最开始,Node.js支持一种拆分模块(module)和导入/导出(import/export)的方案,CommonJS🔗

举个例子。

Terminal window
hello-node
# 源码文件夹
├─src
# 业务文件夹
└─cjs
# 入口文件
├─index.cjs
# 模块文件
└─module.cjs
# 项目清单
└─package.json
src/cjs/module.cjs
module.exports = 'Hello World'
src/cjs/index.cjs
const m = require('./module.cjs')
console.log(m)

require()这种写法,很多初学Node.js的同学都很熟悉。

但是,这种写法存在一个比较大的问题:只能工作于Node.js环境。如果想要在Browser上使用,就需要一些类似Webpack、Browserify的打包工具。

2.3 ES Module

ES Module是新一代的模块化标准,得到JavaScript语言层面原生支持。对于新开发的项目,建议直接采用。如果需要向下兼容,则可以使用Babel等工具。

看另一个例子。

Terminal window
hello-node
# 源码文件夹
├─src
# 上次用来测试 CommonJS 的相关文件
├─cjs
├─index.cjs
└─module.cjs
# 这次要用的 ES Module 测试文件
└─esm
# 入口文件
├─index.mjs
# 模块文件
└─module.mjs
# 项目清单
└─package.json
src/esm/module.mjs
export default 'Hello World'
src/esm/index.mjs
import m from './module.mjs'
console.log(m)

最明显的变化是导入和导出,不过出入不大。

ES Module的最大优势,是在Browser中可以使用相同的方式导入导出。

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ESM run in browser</title>
</head>
<body>
<!-- 标签内的代码就是 src/esm/index.mjs 的代码 -->
<script type="module">
import {
foo as foo2, // 这里进行了重命名
bar,
} from './module.mjs'
// 就不会造成变量冲突
const foo = 1
console.log(foo)
// 用新的命名来调用模块里的方法
foo2()
// 这个不冲突就可以不必处理
console.log(bar)
</script>
</body>
</html>

一统天下了。

2.4 兼容性

由于很多模块写于ES Module出现前,还是按照CommonJS的方式组织,因此需要一些方法处理两者的兼容性。

Node.js目前支持在package.json中指定模块引入方式:

{
"type": "commonjs/module"
}

指定默认的模块导入导出机制。

如果想要混用,可以:

  • 对于CommonJS模块,使用.cjs尾缀
  • 对于ES Module模块,使用.mjs尾缀

另外,CommonJS模块也可以导入ES Module模块,反之亦然,不过不建议使用!具体可参考阮一峰老师的博客:Node.js 如何处理 ES6 模块🔗

3. 总结

JavaScript的模块化解决方案,发展演进了很多年,最终百川到海,ES Module一锤定音。

标准意味着兼容性,统一性,规范性,开发者的学习负担也小。

相较于具体的Solution,其背后的模块化管理、解耦(decoupling)和标准化的解决思路,也非常值得学习。

参考

  1. JavaScript modules🔗
  2. modules-intro🔗
  3. Modules in JavaScript – CommonJS and ESmodules Explained🔗
在 GitHub 上编辑本页面

最后更新于: 2024/03/04 06:51:47