# 收集依赖,依赖是什么?

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 来强制排除当前模块依赖项, 排除满足以下两个条件:

  1. 导入模块名称。
  2. 导入模块名称中的某个文件。

排除示例

import lodash from 'lodash'
import toKey from 'lodash/_toKey.js

// exclude配置
optimizeDeps.exclude: ['lodash']

# 过滤已收集的依赖

if (depImports[id]) {
  return externalUnlessEntry({ path: id })
}

depImportsesbuildScanPlugin插件的参数, 同时也是收集依赖的映射表。当前依赖已经被收集, 将过滤此导入模块。

# 解析导入包的入口文件, 收集缺失依赖

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 })
}

符合收集依赖需要那几点, 分为两层:

第一层

  1. 如果解析的路径地址包含 node_modules
  2. 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的方式

  1. id 代表导入模块的路径。
  2. 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 将不能正常识别构建,进行深入寻找关系链。

# 解决方案

  1. 加上后缀, 这样 esbuild 插件解析时就可以直接被 HTML 脚本提取拦截器拦截, 或者不使用别名导入模块, 者二致少选一。
  2. 通过 optimizeDeps.esbuildOptions 进行自定义拦截过滤。

# 实现一个 optimizeDeps.esbuildOptions自定义拦截过滤

尽请期待...