optimizeDeps
optimizeDeps
optimizeDeps
依赖预构建
optimizeDeps.entries
optimizeDeps.entries
通过这个参数你可以自定义预构建的入口文件。
实际上,在项目第一次启动时,Vit
e 会默认抓取项目中所有的 HTML
文件(如当前脚手架项目中的 index.html
),将 HTML
文件作为应用入口,然后根据入口文件扫描出项目中用到的第三方依赖,最后对这些依赖逐个进行编译。那么,当默认扫描 HTML
文件的行为无法满足需求的时候,比如项目入口为 vue
格式文件时,你可以通过 entries
参数来配置
// vite.config.ts
{
optimizeDeps: {
// 为一个字符串数组
entries: ["./src/main.vue"];
}
}
当然,entries
配置也支持 glob
语法,非常灵活,如:
// 将所有的 .vue 文件作为扫描入口
entries: ["**/*.vue"];
不光是.vue
文件,Vite
同时还支持各种格式的入口,包括: html
、svelte
、astro
、js
、jsx
、ts
和tsx
。可以看到,只要可能存在import
语句的地方,Vite
都可以解析,并通过内置的扫描机制搜集到项目中用到的依赖,通用性很强。
optimizeDeps.exclude
optimizeDeps.include
optimizeDeps.include
决定了可以强制预构建的依赖项
默认情况下,不在 node_modules
中的,链接的包不会被预构建。使用此选项可强制预构建链接的包。
// vite.config.ts
optimizeDeps: {
// 配置为一个字符串数组,将 `lodash-es` 和 `vue`两个包强制进行预构建
include: ["lodash-es", "vue"];
}
它在使用上并不难,真正难的地方在于,如何找到合适它的使用场景。前文中我们提到,Vite
会根据应用入口(entries
)自动搜集依赖,然后进行预构建,这是不是说明 Vite
可以百分百准确地搜集到所有的依赖呢?事实上并不是,某些情况下 Vite
默认的扫描行为并不完全可靠,这就需要联合配置 include
来达到完美的预构建效果了。接下来,我们好好梳理一下到底有哪些需要配置include
的场景。
场景一、动态 import
在某些动态 import
的场景下,由于 Vite
天然按需加载的特性,经常会导致某些依赖只能在运行时被识别出来。
// src/locales/zh_CN.js
import objectAssign from "object-assign";
console.log(objectAssign);
// main.tsx
const importModule = (m) => import(`./locales/${m}.ts`);
importModule("zh_CN");
在这个例子中,动态 import
的路径只有运行时才能确定,无法在预构建阶段被扫描出来。因此,我们在访问项目时控制台会出现下面的日志信息:
这段 log
的意思是: Vite
运行时发现了新的依赖,随之重新进行依赖预构建,并刷新页面。这个过程也叫二次预构建。在一些比较复杂的项目中,这个过程会执行很多次,如下面的日志信息所示:
[vite] new dependencies found: @material-ui/icons/Dehaze, @material-ui/core/Box, @material-ui/core/Checkbox, updating...
[vite] ✨ dependencies updated, reloading page...
[vite] new dependencies found: @material-ui/core/Dialog, @material-ui/core/DialogActions, updating...
[vite] ✨ dependencies updated, reloading page...
[vite] new dependencies found: @material-ui/core/Accordion, @material-ui/core/AccordionSummary, updating...
[vite] ✨ dependencies updated, reloading page...
然而,二次预构建的成本也比较大。我们不仅需要把预构建的流程重新运行一遍,还得重新刷新页面,并且需要重新请求所有的模块。尤其是在大型项目中,这个过程会严重拖慢应用的加载速度!因此,我们要尽力避免运行时的二次预构建。具体怎么做呢?你可以通过 include
参数提前声明需要按需加载的依赖:
// vite.config.ts
{
optimizeDeps: {
include: [
// 按需加载的依赖都可以声明到这个数组里
"object-assign",
];
}
}
场景二、某些包被手动 exclude
exclude
是 optimizeDeps
中的另一个配置项,与 include
相对,用于将某些依赖从预构建的过程中排除。不过这个配置并不常用,也不推荐大家使用。如果真遇到了要在预构建中排除某个包的情况,需要注意它所依赖的包是否具有 ESM
格式,如下面这个例子:
// vite.config.ts
{
optimizeDeps: {
exclude: ["@loadable/component"];
}
}
可以看到浏览器控制台会出现如下的报错:
这是为什么呢? 我们刚刚手动 exclude
的包 @loadable/component
本身具有 ESM
格式的产物,但它的某个依赖hoist-non-react-statics
的产物并没有提供 ESM
格式,导致运行时加载失败。
这个时候include
配置就派上用场了,我们可以强制对hoist-non-react-statics
这个间接依赖进行预构建:
// vite.config.ts
{
optimizeDeps: {
include: [
// 间接依赖的声明语法,通过`>`分开, 如`a > b`表示 a 中依赖的 b
"@loadable/component > hoist-non-react-statics",
];
}
}
在include
参数中,我们将所有不具备 ESM
格式产物包都声明一遍,这样再次启动项目就没有问题了。
optimizeDeps.esbuildOptions
optimizeDeps.esbuildOptions
在依赖扫描和优化过程中传递给 esbuild
的选项, 我们可以自定义 Esbuild
本身的配置
// vite.config.ts
{
optimizeDeps: {
esbuildOptions: {
plugins: [
// 加入 Esbuild 插件
];
}
}
}
场景一、第三方包出现问题
由于我们无法保证第三方包的代码质量,在某些情况下我们会遇到莫名的第三方库报错。我举一个常见的案例——react-virtualized
库。这个库被许多组件库用到,但它的 ESM
格式产物有明显的问题,在 Vite
进行预构建的时候会直接抛出这个错误:
原因是这个库的 ES
产物莫名其妙多出了一行无用的代码:
// WindowScroller.js 并没有导出这个模块
import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";
其实我们并不需要这行代码,但它却导致 Esbuild
预构建的时候直接报错退出了。那这一类的问题如何解决呢?那就是 加入 Esbuild
插件
// vite.config.ts
const esbuildPatchPlugin = {
name: "react-virtualized-patch",
setup(build) {
build.onLoad(
{
filter:
/react-virtualized\/dist\/es\/WindowScroller\/utils\/onScroll.js$/,
},
async (args) => {
const text = await fs.promises.readFile(args.path, "utf8");
return {
contents: text.replace(
'import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";',
""
),
};
}
);
},
};
// 插件加入 Vite 预构建配置
{
optimizeDeps: {
esbuildOptions: {
plugins: [esbuildPatchPlugin];
}
}
}
optimizeDeps.force
optimizeDeps.force
设置为 true
可以强制依赖预构建,而忽略之前已经缓存过的、已经优化过的依赖。
optimizeDeps.disabled
optimizeDeps.disabled
禁用依赖优化,值为 true
将在构建和开发期间均禁用优化器。传 build
或 dev
将仅在其中一种模式下禁用优化器。默认情况下,仅在开发阶段启用依赖优化。