背景 ⌛
公司项目中使用到了 vue 作为前端开发框架,在现阶段中, vue2 的使用程度已趋近成熟,所以想着能够往深层次去探索,继而打算 vue2 向 vue3 进行过渡,在这期间做的技术探索。以下是基于 4 月 29 日开发的 Vite 2.x 版本的探索。
什么是 Vite❓
Vite 是一种前端构建工具,提供开箱即用的配置,同时具有高度扩展性
- 一个开发服务器,基于原生 ES 模块提供了丰富的内建功能。
- 一套构建指令,使用 Rollup 打包代码,并且它是预配置的
☕ Vite 初体验
命令
使用 NPM:
npm init @vitejs/app
复制代码
使用 yarn:
yarn create @vitejs/app
复制代码
支持的预设模版包括:
- vanilla
- vue
- vue-ts
- react
- react-ts
- preact
- preact-ts
- lit-element
- lit-element-ts
- svelte
- svelte-ts
生成目录结构
使用了 react-ts 的模版生成项目,基本目录结构如下:
src/
|- main.tsx
|- index.css
|- ...
|- index.html
|- package.json
|- tsconfig.json
|- vite.config.ts
|- ...
复制代码
使用了 vue-ts 的模版生成项目,基本目录结构如下:
src/
assets/
|- ...
components/
|- ...
|- main.ts
|- App.vue
|- ...
|- index.html
|- package.json
|- tsconfig.json
|- vite.config.ts
|- ...
复制代码
为什么是 Vite❓
✨ 极快服务器启动
Vite 通过将应用模块区分为 依赖 和 源码 两类,改进了服务器的启动时间。
-
依赖 大多在开发期间不会发生变动,Vite 将会使用 esbulid 预构建依赖,比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。
-
源码 一些不是 JavaScript 的文件,需要进行转换,如:jsx,并且不是所有模块都需要同时被加载。让浏览器负责打包程序的部分工作,只有在浏览器请求源码时进行转换并按需提供。
(来源 Vite 官网)
✨ 加快模块更新
编辑某个文件后需要重新构建,这个时候不应该是构建整个包,而是对单个文件进行动态模块热重载,并且在重构完成后无需重载页面。
Vite 精准的使已编辑模块与最近的 HMR 边界之间的链失效,使 HMR 模块加快更新。
Vite 提供了一套原生的 ESM 的 HMR API,并且提供了第一优先级的 HMR 集成 Vue 单文件组件(SFC), React Fast Refresh, Prefresh 等
✨ NPM 依赖解析和预构建
在原生 ES 引入中不支持裸模块导入:
import { someMethod } from "my-dep";
复制代码
上面的导入会将在浏览器中抛出一个错误,Vite 会执行一下操作:
- 预构建该模块,并将 CommonJS / UMD 转换为 ESM 格式
- 重写导入为合法的 URL,如:
/node_modules/.vite/my-dep.js?v=f3sf2ebd
以便浏览器正确导入
依赖预构建利用缓存机制:
- 浏览器缓存,HTTP 请求缓存模块,已缓存的模块不需要重新请求。
- 文件系统缓存,将预构建的依赖缓存到 node_modules/.vite,只有当以下任一步骤发生变化才进行预编译构建
- package.json 中 dependencies 列表
- 包管理器 lockfile
- vite.config.js 配置变化
✨ 构建优化
-
对动态导入的 Polyfill,浏览器对原生 ESM 动态导入和 type=’module’ script 块的支持,存在差异性。
-
CSS 代码分割,将一个异步 chunk 模块中用到的 css 代码抽取出到一个 CSS 文件中,并在 chunk 加载完成时通过 link 载入。
-
异步 chunk 加载优化,Vite 会在预加载步骤中重写代码,将 chunk A 中导入的 chunk C 同时获取到,优化跟踪所有直接导入,消除额外的网络往返。
(来源 Vite 官网)
举个 ?
当你用 import debounce from 'lodash/debounce'
,理想中的场景就是浏览器只加载这个函数的文件。
但由于 debounce 内部又依赖了 3 个模块:isObject
、now
、toNubmer
,而这 3 个模块又有其他的依赖,总共会引入 14 个模块
debounce
/ | \
isObject now toNumber
...
复制代码
每个模块都独立,如果不进行预构建的话,意味着会带来 14 次请求。
// vite.config.ts
{
...,
optimizeDeps: {
exclude: ["lodash"]
}
}
复制代码
从上面可以看出,开启依赖预构建,依赖模块将合并成一个 bundle.js
,并将 CommonJS 或 UMD 发布的依赖项转换为 ESM
再看看 react 与 react-dom 两个模块,有相同依赖的模块已经被抽离到单独的 chunk.js
中
✨ TypeScript 支持
开箱即可用的引入 .ts
文件。使用 esbuild 将 TypeScript 翻译到 JavaScript,约是 tsc 速度的 20~30 倍,同时 HMR 更新反映到浏览器的时间小于 50ms。
☕ 浅谈 Vite 缓存与热更新
启动开发服务器的过程:
- 启动 Vite 服务器,支持使用 http,https,http2 等服务启动,同时创建 ws 服务
服务器缓存:协商缓存与强缓存,进行模块的缓存
// 判断是否属于 依赖包 或者 已存在缓存文件夹中,如果是依赖包则进行强缓存
const isDep =
DEP_VERSION_RE.test(url) ||
(cacheDirPrefix && url.startsWith(cacheDirPrefix));
send(
req,
res,
result.code,
type,
result.etag,
isDep ? "max-age=31536000,immutable" : "no-cache",
result.map
);
复制代码
设置浏览器返回的响应头部中携带缓存字段
if (req.headers["if-none-match"] === etag) {
res.statusCode = 304;
return res.end();
}
res.setHeader("Content-Type", alias[type] || type);
res.setHeader("Cache-Control", cacheControl);
res.setHeader("Etag", etag);
复制代码
- 拦截服务器启动,在服务器启动之前,会先执行依赖预构建方法
runOptimize
,利用esbuild
进行构建,缓存在config.cacheDir
中,默认为node_modules/.vite
。
-
由
chokidar
插件监听文件变化,可以在配置中设置忽略变化的文件列表。 -
通过
createPluginContainer
生成模块管理器,并缓存模块,生成moduleGraph
,其中有三个存储映射表:
urlToModuleMap: new Map<string, ModuleNode>
idToModuleMap: new Map<string, ModuleNode>
fileToModulesMap: new Map<string, Set<ModuleNode>>
复制代码
- ws 会一直轮询,当模块发生变化的时候,ws 会接收到更新操作
{
"type": "update",
"updates": [
{
"type": "js-update",
"acceptedPath": "/src/App.tsx",
"path": "/src/App.tsx",
"timestamp": 1619524827966
}
]
}
复制代码
HMR 热更新源码
开发环境下,在启动服务器时将 client.ts 注入到入口文件
// node/server/index.ts
server.transformIndexHtml = createDevHtmlTransformFn(server);
// node/server/middlewares/indexHtml.ts
const devHtmlHook: IndexHtmlTransformHook = async (
html,
{ path: htmlPath, server }
) => {
// ...
return {
html,
tags: [
{
tag: "script",
attrs: {
type: "module",
src: path.posix.join(base, CLIENT_PUBLIC_PATH),
},
injectTo: "head-prepend",
},
],
};
};
// node/plugins/html.ts
function injectToHead(
html: string,
tags: HtmlTagDescriptor[],
prepend = false
) {
const tagsHtml = serializeTags(tags);
if (prepend) {
// inject after head or doctype
for (const re of headPrependInjectRE) {
if (re.test(html)) {
return html.replace(re, `$&\n${tagsHtml}`);
}
}
} else {
// inject before head close
if (headInjectRE.test(html)) {
return html.replace(headInjectRE, `${tagsHtml}\n$&`);
}
}
// if no <head> tag is present, just prepend
return tagsHtml + `\n` + html;
}
复制代码
监听文件变化后,先更新模块依赖关系,然后执行热更新
// node/server/index.ts
watcher.on("change", async (file) => {
// ...
moduleGraph.onFileChange(file); // 更新模块依赖关系
if (serverConfig.hmr !== false) {
// ...
await handleHMRUpdate(file, server); // 执行热更新
// ...
}
});
// node/server/moduleGraph.ts
// 文件变化时更新模块的依赖
onFileChange(file: string): void {
const mods = this.getModulesByFile(file)
if (mods) {
const seen = new Set<ModuleNode>()
mods.forEach((mod) => {
this.invalidateModule(mod, seen)
})
}
}
// node/server/hmr.ts
function handleHMRUpdate(
file: string,
server: ViteDevServer
) {
// 判断全更新或者部分模块更新
// 判断是否是 配置文件 或 配置依赖文件 或 环境变量文件,则直接重启服务器
if (isConfig || isConfigDependency || isEnv) {
await restartServer(server)
return
}
// ...
if (!hmrContext.modules.length) {
// html file cannot be hot updated
if (file.endsWith('.html')) {
// 当前模块没有依赖其他模块,则进行全更新
ws.send({
type: 'full-reload',
path: config.server.middlewareMode
? '*'
: '/' + normalizePath(path.relative(config.root, file))
})
}
return
}
// 进行文件的依赖模块的更新
updateModules(shortFile, hmrContext.modules, timestamp, server)
}
复制代码
执行热更新,并将更新模块信息通过 ws 推送给客户端
function updateModules(
file: string,
modules: ModuleNode[],
timestamp: number,
{ config, ws }: ViteDevServer
) {
// ...
for (const mod of modules) {
// ...
// 判断模块是否过期需要全更新,通过递归查询模块下的子模块是否出现更新
const hasDeadEnd = propagateUpdate(mod, timestamp, boundaries);
if (hasDeadEnd) {
ws.send({
type: "full-reload",
});
return;
}
updates.push(
...[...boundaries].map(({ boundary, acceptedVia }) => ({
type: `${boundary.type}-update` as Update["type"],
timestamp,
path: boundary.url,
acceptedPath: acceptedVia.url,
}))
);
}
// ...
// 将更新模块通过 ws 发送给客户端
ws.send({ type: "update", updates });
}
复制代码
文尾
以上是笔者在初次探索 Vite 的时候的一些点,如有错误,还请各位指出。
Vite 还有很多知识点未探索到,尤大大等大佬开发的 Vite 是真的强大,还有很多地方可以探索的。它的插件 API、 HMR API、SSR 等,都是后续的探索点。与实际项目开发的结合,这也是我下一步探索的方向,尝试着将小项目进行升级迁移。探索新知识并运用在实际开发中,这也是笔者一直想做的事。