Oasis's Cloud

一个人的首要责任,就是要有雄心。雄心是一种高尚的激情,它可以采取多种合理的形式。
—— 《一个数学家的辩白》

Vite 7.x 版本是如何处理 JS 和 CSS 的


Vite 6 正式引入了“环境”的概念,在 Vite 5 之前,系统中存在两个隐式环境(client 和可选的 ssr)。新的环境 API 允许用户和框架作者根据应用在生产环境中的运行方式,创建任意数量的环境。

Vite 6 允许用户在构建和开发过程中配置应用程序,以映射其所有环境。在开发期间,一个 Vite 开发服务器可用于在多个不同环境中同时运行代码。应用程序源代码仍有 Vite 开发服务器进行转换。

Vite 的开发环境实现基本原理是:通过 script 标签加载 ESM 模块,ESM 模块的请求由 Vite 开发服务器进行处理(将代码转换为 JS)。

在这个原理的基础上可以进行简单的试验来观察下纯 ESM 方式下 React 应用中的 JS 和 CSS 是如何加载的。

/* index.css */
.app {
  color: rebeccapurple;
}
index.js 内容如下:
import React from "react";
import ReactDomClient from "react-dom/client";
import './bundle.css'
function App() {
  return React.createElement("h1", null, "Welcome to ultra minimal cli!");
}
var domNode = document.getElementById("root");
var root = ReactDomClient.createRoot(domNode);
root.render(React.createElement(App, null));

通过 http-server 启动 http 服务,打开浏览器的网络面板,可以看到 css 文件被加载了,但是控制台有一条错误日志:

Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "text/css". Strict MIME type checking is enforced for module scripts per HTML spec.

通过这个例子可以试着分析 css 文件应该如何加载。

  1. 因为 ESM 模块中需要 import JS文件,所以需要在开发服务器中将 css 的请求,返回 JS 的响应头
  2. 将 CSS 转换为 JS 代码
  3. 生成 updateStyle() 调用
  4. 加载 CSS 文件,调用 updateStyle() 插入 style 标签

Vite 返回的 CSS 文件内容示例如下:

import {createHotContext as __vite__createHotContext} from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/index.css");
import {updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle} from "/@vite/client"
const __vite__id = "/Usersc/vite-project/src/index.css"
const __vite__css = "\nbody {\n  margin: 0;\n  display: flex;\n  place-items: center;\n  min-width: 320px;\n  min-height: 100vh;\n}\n"
__vite__updateStyle(__vite__id, __vite__css)
import.meta.hot.accept()
import.meta.hot.prune( () => __vite__removeStyle(__vite__id))

示例代码中的 import.meta.hot 是 Vite 提供的实现模块级别热更新的 API。createHotContext 会创建一个 HMRContext 对象。HMRContext 是每个模块的 HMR 上下文,通过 HMRClient 管理所有模块的状态。

import.meta.hot.accept() 调用 creatHotContext 上的 acceptDeps 方法,将文件路径和相关的 callback 存储到 hmr.client 的 hotModulesMap 中。

import.meta.hot.prune( () => __vite__removeStyle(__vite__id))负责模块不在被使用时候的清理。

开发服务器的流程

Vite 在 dev 命令中分别实现了 http、socket、watcher 这几个服务。其中 http 服务通过 _createServer() 调用 connect 实现。watcher 基于 chokidar 实现。

基于 connect 的 http 服务挂载了一些列的中间件,例如 transformMiddleware 用来解析 main 入口文件。

这里的步骤如下:

PluginContainer 是在 _createserver 方法中实例化的。PluginContainer 会根据不同的环境(environment)实例化。

PluginContainer 类具有模板模式的一些特征:固定的算法骨架:遍历插件——过滤——调用 hook——处理结果。