从 CommonJS 到 Vite:前端模块化演进

3 阅读6分钟

可以把“模块化和构建工具的演进”理解成:

前端代码越来越大后,人类是怎么一步步解决“代码管理问题”的。

一、最开始:所有代码写同一个文件里

<script>
function add(a, b) {
  return a + b
}

function login() {}

function render() {}

...
</script>

几万行代码都写在同一个文件里,于是人们想: “能不能拆文件?”

二、开始拆文件(但还没有模块化)

<script src="math.js"></script>
<script src="user.js"></script>
<script src="app.js"></script>

现在代码分文件了。 但问题来了:

问题1:加载顺序不能错

例如:app.js依赖于math.js

如果:就炸了。

<script src="app.js"></script>
<script src="math.js"></script>

因为app.js 先执行,但 math.js 还不存在

问题2:全局变量污染

所有东西都挂在:window上。

例如:不同文件给同一个属性名赋值会直接覆盖。

// a.js
var name = "Tom"

// b.js
var name = "Jack"

于是:“我们需要真正的模块系统。”

每个文件有自己的作用域,并且文件之间可以互相导入导出

三、CommonJS:Node.js 带来的模块化(2009)

CommonJS 的设计目标: 每个文件就是一个模块,并且模块内部变量默认私有, 只有主动导出的内容别的文件才能使用。

// 导出
module.exports = {}

// 导入
const math = require('./math')

module.exports 到底是什么?

Node 会给每个模块自动创建模块对象:

module = {
   exports: {}
}

你写:其实是在修改exports对象

module.exports = {
  add
}

最后:

require('./math')

返回的其实就是:module.exports

所以:

// math.js
module.exports = {
  add
}

对应:

// app.js
const math = require('./math')

最终:

math === {
  add
}

require的本质是“读取文件”

Node 会: 1、找到文件

./math.js

2、读取文件内容(Node从硬盘同步读取文件,速度很快,但是浏览器读取文件需要通过异步网络请求,等待时间很长,页面必须卡住等待,用户体验差。所以CommonJS天然更适合服务器)

const code = fs.readFileSync('./math.js')

3、执行这个文件

Node 内部大概会这样:

(function() {
   // math.js 的代码
})()

注意: 每个模块都会被函数包裹

这就是为什么不会污染全局。

// math.js
var name = 'Tom'

因为其实变成了函数作用域:name 只在函数内部。

(function() {
   var name = 'Tom'
})()

四、AMD / CMD:浏览器模块化探索(2010~2014)

CommonJS必须同步读取文件,但浏览器加载文件本质是 HTTP 请求。

网络请求是异步的,浏览器不能同步阻塞页面等待模块加载。那么浏览器必须使用“异步模块化

AMD

核心思想一句话: “提前声明依赖

例如:AMD会先分析依赖 math,再异步下载math.js,最后执行回调函数

define(['math'], function(math) {
    console.log(math.add(1, 2))
})

先把依赖写好, 等依赖加载完成, 再执行回调函数

浏览器不能同步读取,于是 AMD 改成:“先异步下载模块,再执行代码

但是AMD的依赖必须提前写,特别是大型项目,代码很复杂,于是就出现了CMD。

CMD

核心思想一句话:“依赖就近

define(function(require, exports) {
   const math = require('./math')
})

什么时候需要模块: 什么时候再 require

五、Webpack 时代(2014~2020)

虽然 AMD / CMD 解决了浏览器模块化问题。

但随着前端项目越来越大: 模块越来越多, 依赖越来越复杂

前端开始进入:工程化时代,于是: Webpack 出现了

Webpack 的核心思想一句话: “一切皆模块

不仅 JavaScript:

  • CSS
  • 图片
  • 字体
  • Vue 文件
  • React JSX

都可以作为模块处理。

Webpack 本质上只干了一件事: “提前帮浏览器处理所有依赖”,你可以把它理解成:“前端代码编译器”

第一步:分析依赖

Webpack 会先读取: app.js, 发现:

依赖:
- index.css
- logo.png
- math.js

于是形成 依赖图

app.js
 ├── index.css
 ├── logo.png
 └── math.js

第二步:loader 转换

这是 Webpack 最核心的地方。

因为: 浏览器只认识 JS,所以:Webpack 要把一切转成 JS

例如: CSS

import './index.css'

Webpack 会变成:

const style = document.createElement('style')
style.innerHTML = '...css内容...'
document.head.appendChild(style)

于是:CSS 被转换成 JS

图片

import logo from './logo.png'

Webpack 会:把图片变成最终资源路径

const logo = '/assets/logo.xxxxx.png'

JSX

<App />

Webpack + Babel: 会变成:

React.createElement(App)

第三步:全部打包

最后Webpack 会:合并所有模块

生成:

bundle.js

浏览器只需要:

<script src="bundle.js"></script>

为什么“打包”这么重要?

因为:浏览器太弱了

当年浏览器:

  • 不支持模块
  • 不支持 JSX
  • 不支持 TS
  • 不支持 CSS import

于是:必须提前编译

六、ES Module 官方标准(2015)

前面的:

  • CommonJS
  • AMD
  • CMD

虽然都实现了模块化。 但它们都有一个共同问题:都不是 JavaScript 官方标准,于是: 前端生态越来越混乱,模块规范不统一。

于是:ES6 正式推出了官方模块系统,也就是:ES Module(ESM)

核心语法:

export
import

ES Module 和 CommonJS 最大区别

CommonJS

const math = require('./math')

是:运行时加载 ,代码执行到这里时:Node 才去读取模块

ES Module

import { add } from './math.js'

是:编译时静态分析

也就是说:浏览器或构建工具在代码运行前,就已经知道依赖关系

这点非常重要。因为: 静态分析让现代工程优化成为可能

例如:

  • Tree Shaking
  • 按需加载
  • 更快构建
  • 更好的类型分析

都是建立在 ESM 基础上的。

浏览器现在会自动处理依赖,例如:

import { add } from './math.js'

浏览器会:

自动请求:
GET /math.js

所以:浏览器终于原生支持模块化

七、Vite 时代(2020~现在)

随着前端项目越来越大。,Webpack 开始暴露一个严重问题:太慢了

因为 Webpack 的开发模式是:

先扫描整个项目
分析所有依赖
再整体打包
最后启动开发服务器

项目小的时候问题不大。

但大型项目可能:

  • 启动几十秒
  • 热更新越来越慢
  • 修改一个文件都要重新构建很多内容

开发体验非常差。

Vite 的核心思想: “开发环境不再整体打包”

以前: Webpack

启动项目
↓
打包整个项目
↓
浏览器运行

现在:Vite

浏览器请求哪个模块
Vite 就即时返回哪个模块

Vite 利用了浏览器原生 ESM

浏览器看到:

import App from './App.jsx'

会自动请求:

GET /App.jsx

然后:如果 App.jsx 又依赖:

import Button from './Button.jsx'

浏览器继续请求:

GET /Button.jsx

所以:浏览器自己开始负责模块加载,Vite 不需要像 Webpack 一样:提前打包整个项目,而是浏览器请求哪个模块 ,Vite 就即时编译哪个模块,即无打包开发服务器。

生产环境时执行:

vite build

Vite 仍然会:(Rollup)

  • 打包
  • 压缩
  • Tree Shaking
  • 代码分割

最终生成:

dist/
  assets/
  index.js
  chunk.js
WebpackVite
开发前先整体打包开发时按需加载
项目越大启动越慢几乎秒启动
JS 构建为主基于浏览器原生 ESM
配置复杂配置更简单