Vite官方文档浓缩
前言
这一篇为什么要叫 vite 官方文档浓缩呢?一方面是自己不太了解 vite,想通过官方文档进行 vite 的学习。一方面是靠自己的理解处理一下文档的细节。话不多说,开刷
前置知识:CJS/ESM,HMR,ESM 的引入方式,FOUC
为什么选 Vite
Vite 诞生的背景是大型应用中 webpack 构建速度变慢,需要很久才能开启服务器,即使通过 HMR 也需要很久浏览器才能反馈出内容。以上都是基于打包的学说,而 Vite 诞生的原因则是浏览器开始逐步支持 JS 模块,他使用非打包的方式构建,以冷启动的方式启动,因此构建速度上面极大快于前者。
冷启动
Vite 先将应用中的模块分为依赖和源码两类,改进服务器启动时间。
- 依赖:依赖大多数为开发不会变动的纯 js。一些比较大的依赖(比如组件库)处理的成本也很高,依赖通常有多种模块化的格式(eg:CJS/ESM)
Vite 在依赖处理这方面使用的是 esbuild,由 go 语言编写,比 js 编写的打包器预构建快 10-100 倍
- 源码:源码通常包含一些非 js 文件,比如 css,jsx 或者 vue/svelte 组件,特点是时常会被编辑。同时不是所有的源码都需要同时被加载(比如基于路由拆分的代码模块)
Vite 以原生 ESM 的方式提供源码。相当于让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码的时候才去转换并提供源码。根据情景动态导入源码,大部分是当前屏幕中有的操作才会被处理。
以下是常规打包构建和非打包预构建的示例图
看图可以发现 Vite 应该是只构建了当前视图中路由和与该路由相关的模块
缓慢更新
当你更新应用的时候,常规的打包器使用 HMR,让一个模块热替换自己,而不会影响页面的其他部分,大大的改善了开发体验。不过当应用程序越来越大的时候,这种方式也显得捉襟见肘。
边界链失活
Vite 中,HMR 是在原生 ESM 上面执行的。当编辑一个文件的时候,vite 只需要对已编辑的模块与其最近的 HMR 边界的链失活(不需要全部重构建)使得无论应用大小如何,HMR 始终保持快速更新
加速页面重载
Vite 使用 HTTP 请求头来使得页面快速重载,对于依赖使用的是Cache-Control: max-age=31536000,immutable
进行强缓存,源码模块通过 304 进行协商缓存。
仍需要打包的生产环境
上面的 Bundless 只是基于开发环境的,正式上线还是需要进行打包。原因很简单:
嵌套打包会导致额外的网络往返,即使使用了 HTTP2 也是效率低下
所以为了在生产环境中获得更好的加载性能,最好还是对代码进行 tree-shaking,懒加载和 chunk 分割(webpack 那套)
为什么不用 esbuild 打包?
esbuild 很快,但是还不够完善,尤其是在 css 和代码分割方面。目前采用的是比较稳定的 rollup
NPM 依赖解析和预构建
我们知道 esm 是不支持裸导入的,想要将 es 模块在浏览器中使用,有三种方式
- import from URL
- importmap
- assertion
其中第二种方式使得后续的裸导入成为可能。
但是 vite 的思路是这样的,检测所有裸导入的模块,对其进行以下操作:
- 使用 esbuild 预构建依赖,将 cjs 和 umd 转换为 esm。
- 重写导入为合法的 URL(利用第一种方式),eg:
/node_modules/.vite/deps/my-dep.js?v=f3sf2ebd
让浏览器能够识别它们
更快的 Typescript 转译
vite 使用 esbuild 转 ts 到 js,比 tsc 速度快 20-30 倍。
注意事项
css
导入 css 文件将会把内容插入到 style 标签中,同时也带有 HMR 支持。也能以字符串的形式检索处理后的,作为其模块默认导出的 css
@import 内联和变基
Vite 通过 postcss-import
预配置支持了 css 的 @import
内联,意味着所有 css 的 url
引用,即使导入的文件在不同的目录内,也总是自动变基
postcss
如果项目包含有效的 PostCSS 配置 (任何受 postcss-load-config
支持的格式,例如 postcss.config.js
),它将会自动应用于所有已导入的 CSS。
cssmodule
任何以 .module.css
为后缀名的 CSS 文件都被认为是一个 CSS modules 文件。导入这样的文件会返回一个相应的模块对象:
CSS 预处理器
vite 提供了对.scss, .sass, .less, .styl
和 .stylus
文件的内置支持。没必要安装特定的 vite 插件,但需要安装相应的预处理器依赖
1 | # .scss and .sass |
构建性能优化
下面所罗列的功能会自动应用为构建过程的一部分,除非你想禁用它们,否则没有必要显式配置。
css 代码分割
vite 会自动的将一个异步的 chunk 模块中使用到的 css 代码抽离出来并为其生成一个单独的文件。这个 css 文件将在异步 chunk 加载完成的时候自动通过一个 link 标签载入,该异步 chunk 会保证只在 css 加载完成后执行,避免发生 FOUC
不过也可以单独抽离所有的 css 到一个文件中,通过设置:build.cssCodeSplit 为 false 来禁用 css 代码分割。
预加载指令生成
vite 会为入口 chunk 和它们打包出的 html 中直接引入自动生成<link ref = 'modulepreload'>
指令。
异步 chunk 加载优化
实际项目中,rollup 通常会生成共用 chunk—-被两个或者两个以上的其他 chunk 共享的 chunk。与动态导入相结合,很容易出现下面的情况
在无优化的情况下,当异步 chunkA 被加载的时候,浏览器必须先解析 A 才知道 A 和 C 的共用关系,这会导致额外的网络往返。
Vite 使用一个预加载步骤自动重写代码,来分割动态导入调用,以实现当 A 被请求时,C 也将同时被请求:
1 | (Entry) => A + C; |
C 里面还可能有更多的嵌套共用内容,所以这种预加载处理是很重要的?
Vite 具体是这么操作的?预加载的原理?
Vite 使用插件
Vite 的插件是基于 Rollup 系统的,并且添加了自己额外的选项
具体如果想要添加一个插件,需要添加到项目的devDependencies
中,并在 vite.config.js 配置文件中的 plugins 数组里面引入他。例如,想要为传统浏览器提供支持可以使用@vitejs/plugin-legacy
1 | $ npm add -D @vitejs/plugin-legacy |
1 | // vite.config.js |
plugin 可以接收多个插件并且会在里面自动扁平化这个数组。
强制插件排序
Vite 的插件可能和 Rollup 的插件冲突,需要修改插件的执行顺序或者只在构建的时候使用。使用修饰符 enforce 来完成这件事
- pre:在 vite 插件之前
- 默认/post:在 vite 插件之后
1 | // vite.config.js |
查看兼容性传送门
按需应用
指定插件在开发(serve)还是生产(build)中使用:apply 修饰符
1 | // vite.config.js |
依赖预构建
首次启动 vite 会打印相关信息:
1 | Pre-bundling dependencies: (正在预构建依赖:) |
因为此时 vite 正在执行预构建,且每次有新的插件加进来,都会重新预构建。
原因
vite 的依赖预构建有两个原因
- CJS 和 UMD 兼容性:开发阶段,vite 要将所有的 cjs 模块转化成为 esm,vite 会智能分析所有的 cjs 或者 umd 依赖然后生成对应 esm(关于怎么转 esm 的一些细节在 webpack 的 bundless 章节已经讲述过)
- 性能:除了对 cjs,umd 处理之外,vite 还对有许多内部模块的 esm 依赖关系转为单个模块,举个例子,执行 loadash 的时候,会发送六百多个请求,在浏览器端会造成很大的压力,导致页面的加载速度变慢。
通过预构建 loadash-es 成为一个模块,那么只需要发送一个请求即可。
注意:开发环境使用的是 esbuild 转化 esm 模块,生产环境使用的是@rollup/plugin-commonjs
自动依赖搜寻
如果 vite 没有找到你的缓存,也就是没有 304 协商缓存,也没有强缓存,那么 vite 会自动抓取你的源码,找到引入的依赖项,比如裸导入的模块,并将这些依赖项作为预构建包的入口点,预构建通过 esbuild 进行,所以很快。
在服务器启动之后,如果发现一个新的依赖项进来,且他没有在缓存中,vite 会重新执行依赖构建过程并重载浏览器。
Monorepo 和链接依赖
在一个 Monorepo 启动中,该仓库的某个依赖可能会成为另一个包的依赖,vite 会自动侦测没有从 nodemodules 里面解析的依赖项,并将链接的依赖视为源码,他不会尝试打包被链接的依赖,而是会分析被链接依赖的依赖列表。
不过这需要被链接的依赖被导出为 esm 格式。如果不是,那么你可以在配置中将此依赖添加到 optimizeDeps.include 和 build.commonjsOptions.include 这两项中。optimizeDeps.include
和 build.commonjsOptions.include
这两项中。
1 | export default defineConfig({ |
当这个被链接的依赖发生变更后,在重启开发服务器时在命令中带上 --force
选项让所有更改生效。
重复删除
由于对链接依赖的解析方式不同,传递性的依赖项可能会不正确地进行重复数据删除,而造成运行时的问题。如果你偶然发现了这个问题,请使用 npm pack 来修复它。
自定义行为
默认的依赖项为启发式可能并不总是可取的,可以使用 optimizeDeps 配置项:include 和 exclude 去配置你想要寻找的依赖或者排除依赖。
建议:如果依赖项很大(有很多内部模块)或者是 CJS,你应该包含他让 vite 自动去处理,如果依赖项很小并且已经是 esm,那么可以排除他,让浏览器自动加载他。
缓存
包含文件系统缓存和浏览器缓存
vite 会将预构建的依赖缓存到node_modules/.vite
.它根据几个源来决定是否重新执行预构建
- packgejson 中的 dependencies 列表
- 包管理器的 lockfile
- 在 vite.config.js 中相关字段配置过的(比如刚刚说的 optimize:inclde/exclude)
如果想要 vite 强制依赖预构建,需要使用--force
来启动开发服务器,或者手动删除node_modules/.vite
浏览器缓存
解析后的依赖请求会以 http 头max-age:31536000 immutable
进行强缓存,用来提高开发时的页面性能。一旦被缓存,这些请求将永远不会到达开发服务器。如果安装了不同版本的则附加的版本 query 会自动使他们失效(保证只能有一个版本)。如果要调试依赖,可以使用:
- 通过浏览器调试工具的 Network 选项卡暂用缓存
- 重启 vite dev server 并
--force
命令以重新进行依赖预构建。 - 重载页面
静态资源处理
将资源引入为 URL
服务时,引入一个静态资源会返回解析后的公共路径
1 | import imgUrl from "./img.png"; |
比如 imgURL 在开发时会是/img.png
,生产构建后会是/assets/img.2d8efhg.png
这个类似于 webpack 的 file-loader,但是不一样的点在于既可以引入绝对公共路径,也可以引入相对路径
url()
在 css 中的引用会以同样方式进行处理。- 如果
vite
引用了vue
插件,vue sfc
模板中的资源都将自动转换为导入 - 常见的图像,媒体,和字体文件类型将被自动检测。可以使用
assetsInclude
选项拓展内部列表。 - 引用的资源作为构建资源图的一部分包括在内,将生成散列文件名,并可由插件处理优化。
- 较小的资源体积与
assetsInlineLimit
选项值将会被内联为base64URL
显式 URL 引入
未被包含在内部列表或 assetsInclude
中的资源,可以使用?url
后缀显式导入为一个 URL。比如导入Houdini Paint Worklets
时:
1 | import workletURL from "extra-scalloped-border/worklet.js?url"; |
将资源引入为字符串
资源可以使用 ?raw 后缀声明作为字符串引入。
1 | import shaderString from "./shader.glsl?raw"; |
导入脚本作为 Worker
脚本可以通过 ?worker
或 ?sharedworker
后缀导入为 web worker。
1 | // 在生产构建中将会分离出 chunk |
public 目录
特点:
- 不会被源码引用(js 文件)
- 必须保持源文件名称(不被 hash)
- 或者只想要得到它的 URL
上述资源放入 public 最好。可以通过/
在开发的时候直接访问到比如public/icon.png => /icon.png
默认目录:<root>/public
,但可以通过publicDir
配置
new URL(url, import.meta.url)
import.meta.url
是 esm 的原生功能,可以暴露当前模块的 url,在 js 模块中可以通过相对路径获得一个完整的静态资源 url。
1 | const imgUrl = new URL("./img.png", import.meta.url).href; |
这在现代浏览器中能够原生使用 - 实际上,Vite 并不需要在开发阶段处理这些代码!
这个模式同样还可以通过字符串模板支持动态 URL:
1 | function getImageUrl(name) { |
在生产构建时,Vite 才会进行必要的转换保证 URL 在打包和资源哈希后仍指向正确的地址。
注意无法在 ssr 中使用,因为这个是 esm 的方法,也就意味着在 node 和浏览器有不同的语义,服务器也没办法预先确定客户端主机 url
构建生产版本
命令:vite build
默认情况使用<root>/index.html
作为构建入口。
浏览器兼容性
vite 的目标是支持 esm 的浏览器,按照条件进行 browerslist 查询的浏览器
也可以手动通过build.target
配置项构建指定目标。最低支持 es2015
默认情况下 vite 不包含任何 polyfill,只负责转义,要使用相关的服务进行 polyfill 生成,Polyfill.io
传统浏览器需要插件@vitejs/plugin-legacy
的支持,它将会自动生成传统版本的 chunk 与其对应 es 方面语言的 polyfill。
公共基础路径
配置 base 项所有的资源会按此路径重写,也可以通过命令行:vite build --base=/my/public/path/