跳到主要内容

optimizeDeps

2023年12月19日
柏拉文
越努力,越幸运

optimizeDeps


optimizeDeps 依赖预构建

optimizeDeps.entries


optimizeDeps.entries 通过这个参数你可以自定义预构建的入口文件。

实际上,在项目第一次启动时,Vite 会默认抓取项目中所有的 HTML 文件(如当前脚手架项目中的 index.html),将 HTML 文件作为应用入口,然后根据入口文件扫描出项目中用到的第三方依赖,最后对这些依赖逐个进行编译。那么,当默认扫描 HTML 文件的行为无法满足需求的时候,比如项目入口为 vue 格式文件时,你可以通过 entries 参数来配置

// vite.config.ts
{
optimizeDeps: {
// 为一个字符串数组
entries: ["./src/main.vue"];
}
}

当然,entries 配置也支持 glob 语法,非常灵活,如:

// 将所有的 .vue 文件作为扫描入口
entries: ["**/*.vue"];

不光是.vue文件,Vite 同时还支持各种格式的入口,包括: htmlsvelteastrojsjsxtstsx。可以看到,只要可能存在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 的路径只有运行时才能确定,无法在预构建阶段被扫描出来。因此,我们在访问项目时控制台会出现下面的日志信息:

Preview

这段 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

excludeoptimizeDeps 中的另一个配置项,与 include 相对,用于将某些依赖从预构建的过程中排除。不过这个配置并不常用,也不推荐大家使用。如果真遇到了要在预构建中排除某个包的情况,需要注意它所依赖的包是否具有 ESM 格式,如下面这个例子:

// vite.config.ts
{
optimizeDeps: {
exclude: ["@loadable/component"];
}
}

可以看到浏览器控制台会出现如下的报错:

Preview

这是为什么呢? 我们刚刚手动 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 进行预构建的时候会直接抛出这个错误:

Preview

原因是这个库的 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 将在构建和开发期间均禁用优化器。传 builddev 将仅在其中一种模式下禁用优化器。默认情况下,仅在开发阶段启用依赖优化。

optimizeDeps.needsInterop