# 收集依赖,依赖是什么?
Vite 将抓取你的源码,并自动寻找引入的依赖项(即 "bare import",表示期望从 node_modules 解析)
以上是官方文档的解释,所谓的收集依赖就是根据导入的包名,确定需要导入的包名的绝对路路径,关于到底使用包中那个文件,resolveId 插件会对导入包的 package.json 进行分析选择。
在扫描导入链路中对大多数导入模块类型做讲解, 唯一对导入 NPM 模块没有探讨, 正是因为扫描所有的导入模块路径就是为了寻找 NPM 模块, 进行构建依赖收集。
# onResolved 导入模块拦截
导入示例
import dayjs from 'dayjs'
在整个解析链路中,遇到导入的内容来源于 node_modules 将会命中以下解析方法。
build.onResolve({filter: /^[\w@][^:]/}, async ({ path: id, importer }) => { ... })
# 过滤 exclude 包名
if (exclude?.some((e) => e === id || id.startsWith(e + '/'))) {
return externalUnlessEntry({ path: id })
}
当解析的对应的包名时, 如果不需要进行构建依赖收集, 可以通过 optimizeDeps.exclude 来强制排除当前模块依赖项, 排除满足以下两个条件:
- 导入模块名称。
- 导入模块名称中的某个文件。
排除示例
import lodash from 'lodash'
import toKey from 'lodash/_toKey.js
// exclude配置
optimizeDeps.exclude: ['lodash']
# 过滤已收集的依赖
if (depImports[id]) {
return externalUnlessEntry({ path: id })
}
depImports 是 esbuildScanPlugin插件的参数, 同时也是收集依赖的映射表。当前依赖已经被收集, 将过滤此导入模块。
# 解析导入包的入口文件, 收集缺失依赖
const resolved = await resolve(id, importer)
if (resolved) {
....
} else {
missing[id] = normalizePath(importer)
}
排除了以上两种不需要进行依赖收集的情况, 接下来就对导入包名路径进行解析,寻找入口文件。
如果没有解析到导入包名的入口文件, 视为缺失的包, 通过 missing 变量进行收集。
# 排除非法文件, 进行过滤
if (shouldExternalizeDep(resolved, id)) {
return externalUnlessEntry({ path: id })
}
这里主要会有一些 NPM 包的入口文件类型不符合构建预期, 进行检测构建优化。
# 符合构建依赖模块, 进行收集
if (resolved.includes('node_modules') || include?.includes(id)) {
// dependency or forced included, externalize and stop crawling
if (OPTIMIZABLE_ENTRY_RE.test(resolved)) {
depImports[id] = resolved
}
return externalUnlessEntry({ path: id })
}
符合收集依赖需要那几点, 分为两层:
第一层
- 如果解析的路径地址包含 node_modules。
- 在
optimizeDeps.include中强制构建导入路径。
第二层
解析后的路径必须是后缀为 .mjs 或者 .js 结尾的文件
理解在
optimizeDeps.include中强制构建导入路径的使用场景
这里属于自定义预构构建, 导入的文件可能并不是从 npm 包中进行收集的依赖,同时也是 esm 多模块导出的导致加载性能的优化手段。
示例
// src/util.js
import one from './one.js
import two from './two.js
...n个模块
export default {
one,
two,
...n个模块
}
import util from '@/util.js'
// vite.config.js
resolve: {
alias: {
"@": "path.resolve(__dirname, 'src')"
}
},
optimizeDeps: {
include: ["@/util.js"]
}
通过别名导入的方式同样也可以被构建 esbuild 插件拦截到,同时满足以上两层的优化条件。
# 依赖收集方式
depImports[id] = resolved
收集的依赖是一个key:value的方式
id代表导入模块的路径。value代表最后通过resolveId插件方法寻找到的入口文件的路径。
展现结果:
{
"dayjs": "/Users/admin/crm/node_modules/dayjs/index.js",
"@/util.js": "/Users/admin/crm/src/util/index.js"
}
# 不进行深入寻找依赖
if (OPTIMIZABLE_ENTRY_RE.test(resolved)) {
depImports[id] = resolved
}
return externalUnlessEntry({ path: id })
可以发现在收集依赖完毕之后进行路导入路径过滤,不再深入进行寻找构建内容。
因为最后预构建的内容以主入口主,文件内部导入的模块将会被打入主口文件中, 内部导入的模块不必进行深入收集依赖, 如果多个收集的依赖模会存在相互依赖的情况下, 也不必担心,在真正构建过程会进行 splitChunk 进行分包。
# 别名导入模块, 继续深入寻找依赖
if (resolved.includes('node_modules') || include?.includes(id)) {
XXX
} else {
return {
path: path.resolve(resolved)
}
}
通过别名引入时,没有把导入路径加载优化 API optimizeDeps.include 中时, 将会对此模块继续分析关系链, 寻找可收集依赖的模块。
# issues
关于别名省后缀的扫描报错1 (opens new window) 关于别名省后缀的扫描报错2 (opens new window)
示例
import HelloWorld from '@/src/component/helloWorld'
// vite.config.js
resolve: {
extensions: ['.vue']
},
通过别名并且省去后缀 HTML 需要提取脚本的导入模块,在收集依赖阶段会走入继续深入寻找依赖流程中, 但是此时只返回 path, 并没有设置 namespace: html, 虽然在对于 HTML 需要提取脚本的后缀导入模块在收集依赖之前就已经有拦截方法, 正是因为被省去了后缀, 又设置了别名,被收集依赖的拦截器所拦截, 同时又不会被内部过滤条件所过滤,但是不设置 namespace 进行提取脚本会 esbuild 将不能正常识别构建,进行深入寻找关系链。
# 解决方案
- 加上后缀, 这样
esbuild插件解析时就可以直接被 HTML 脚本提取拦截器拦截, 或者不使用别名导入模块, 者二致少选一。 - 通过
optimizeDeps.esbuildOptions进行自定义拦截过滤。
# 实现一个 optimizeDeps.esbuildOptions自定义拦截过滤
尽请期待...