JavaScript模块化演进

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,但并没有被广泛使用。
  • ESModule,语言级的模块系统,终极解决方案,发布于ES6(2015)。

2.2 CommonJS

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

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

举个例子。

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等工具。

看另一个例子。

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 上编辑本页面 更新时间: 10/13/2023, 12:55:21 AM