Vite 源码(二)vite启动流程以及如何获取config配置

9,681 阅读14分钟

当在控制台输入yarn run dev时,执行对应源码的位置是node/cli.ts

import { cac } from 'cac'
// 创建CLI实例,'vite'表示,在 help 和 version 命令中显示的名称
const cli = cac('vite')
// 添加命令项
cli
  .option('-c, --config <file>', `[string] use specified config file`)
  // ...
  
// dev
cli
  .command('[root]') // default command
  .alias('serve') // 设置命令别名
  .option('--host [host]', `[string] specify hostname`)
  // ...
  .option('--cors', `[boolean] enable CORS`) // 使用 CORS
  // 如果指定端口号则退出
  .option('--strictPort', `[boolean] exit if specified port is already in use`)
  .option(
    '--force',
    `[boolean] force the optimizer to ignore the cache and re-bundle`
  ) // 忽略预构建缓存,重新构建
  // 当命令与用户输入匹配时,调用这个回调函数
  .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {})
  
  cli.help()
  cli.version(require('../../package.json').version)
  cli.parse()

Vite 使用 cscjs搭建的命令

当执行这个命令会调用action的回调函数,看下主要代码

// root: 如果执行的是 yarn run dev -- test 或者 npx vite test,则 root 参数为 'test' 
// options:命令行中的参数
.action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {
    const { createServer } = await import('./server')
    try {
      const server = await createServer({
        root, // 命令名
        base: options.base, // 公共基础路径
        // 环境模式,development | production
        mode: options.mode,
        configFile: options.config, // 配置文件目录
        // 调整控制台输出的级别,默认为 'info',可选值 'info' | 'warn' | 'error' | 'silent'
        logLevel: options.logLevel,
        clearScreen: options.clearScreen, // 是否清空终端打印的信息
        // 开发服务器配置,比如 host、port、open、https、cors、strictPort、force
        server: cleanOptions(options)
      })
      await server.listen()
  })

createServer

createServer函数定义在src/node/server/index.ts里面,由于createServer函数代码比较多,这里只捡重要的说

// 删减版,包含主要流程
export async function createServer(inlineConfig) {
    // 获取config配置
    const config = await resolveConfig(inlineConfig, 'serve', 'development')
    // 获取项目根路径
    const root = config.root
    // 获取本地服务器相关的配置
    const serverConfig = config.server
    // 创建中间件实例
    const middlewares = connect() as Connect.Server
    // 创建 http 服务器
    const httpServer = await resolveHttpServer(
        serverConfig,
        middlewares,
        httpsOptions
    )
    // 创建 WebSocket 服务器
    const ws = createWebSocketServer(httpServer, config, httpsOptions)
    // ignored:忽略监听的文件;watchOptions:对应 server.watch 配置,传递给 chokidar 的文件系统监视器选项
    const { ignored = [], ...watchOptions } = serverConfig.watch || {}
    // 通过 chokidar 监听文件
    const watcher = chokidar.watch(path.resolve(root), {
        ignored: [
            '**/node_modules/**',
            '**/.git/**',
            ...(Array.isArray(ignored) ? ignored : [ignored]),
        ],
        ignoreInitial: true,
        ignorePermissionErrors: true,
        disableGlobbing: true,
        ...watchOptions,
    }) as FSWatcher
    // 获取 所有插件
    const plugins = config.plugins
    // 创建插件容器,是一个对象,对象的属性是 vite 支持的 rollup 的钩子函数,后面会介绍
    // 比如 options、resolveId、load、transform
    const container = await createPluginContainer(config, watcher)
    // 创建Vite 的 ModuleGraph 实例,后面也会介绍
    const moduleGraph = new ModuleGraph(container)
    // 声明 server 对象
    const server: ViteDevServer = {
        config, // 包含命令行传入的配置 和 配置文件的配置
        middlewares,
        get app() {
            return middlewares
        },
        httpServer, // http 服务器
        watcher, // 通过 chokidar 监听文件
        pluginContainer: container, // vite 支持的 rollup 的钩子函数
        ws, // WebSocket 服务器
        moduleGraph, // ModuleGraph 实例
        transformWithEsbuild,
        transformRequest(url, options) {},
        listen(port?: number, isRestart?: boolean) {},
        _optimizeDepsMetadata: null,
        _isRunningOptimizer: false,
        _registerMissingImport: null,
        _pendingReload: null,
        _pendingRequests: Object.create(null),
    }
    // 被监听文件发生变化时触发
    watcher.on('change', async (file) => {})
    // 添加文件时触发
    watcher.on('add', (file) => {})
    watcher.on('unlink', (file) => {})
    // 执行插件中的 configureServer 钩子函数
    // configureServer:https://vitejs.cn/guide/api-plugin.html#configureserver
    const postHooks: ((() => void) | void)[] = []
    for (const plugin of plugins) {
        if (plugin.configureServer) {
            // configureServer 可以注册前置中间件,就是在内部中间件之前执行;也可以注册后置中间件
            // 如果configureServer 返回一个函数,这个函数内部就是注册后置中间件,并将这些函数收集到 postHooks 中
            postHooks.push(await plugin.configureServer(server))
        }
    }
    // 接下来就是注册中间件
    // base
    if (config.base !== '/') {
        middlewares.use(baseMiddleware(server))
    }
    // ...
    // 主要转换中间件
    middlewares.use(transformMiddleware(server))
    // ...
      // 如果请求路径是 /结尾,则将路径修改为 /index.html
    if (!middlewareMode || middlewareMode === 'html') {
        middlewares.use(spaFallbackMiddleware(root))
    }
    // 调用用户定义的后置中间件
    postHooks.forEach((fn) => fn && fn())

    if (!middlewareMode || middlewareMode === 'html') {
        // 如果请求的url是 html 则调用插件中所有的 transformIndexHtml 钩子函数,转换html,并将转换后的 html 代码发送给客户端
        middlewares.use(indexHtmlMiddleware(server))
        // handle 404s
        middlewares.use(function vite404Middleware(_, res) {
            res.statusCode = 404
            res.end()
        })
    }
    if (!middlewareMode && httpServer) {
        // 重写 httpServer.listen,在服务器启动前预构建
        const listen = httpServer.listen.bind(httpServer)
        httpServer.listen = (async (port: number, ...args: any[]) => {}) as any
    } else {}
    return server
}

createServer函数的大体流程如下

  • 获取config配置
  • 创建 http 服务器httpServer
  • 创建 WebSocket 服务器ws
  • 通过 chokidar 创建监听器watcher
  • 创建一个兼容rollup钩子函数的对象container
  • 创建模块图谱实例moduleGraph
  • 声明server对象
  • 注册watcher回调
  • 执行插件中的configureServer钩子函数(注册用户定义的前置中间件),并收集用户定义的后置中间件
  • 注册中间件
  • 注册用户定义的后置中间件
  • 注册转换html文件的中间件和未找到文件的404中间件
  • 重写 httpServer.listen
  • 返回server对象

整体逻辑就是,调用createServer函数,拿到返回值后调用server.listen(),在这个过程中会对预构建依赖包并开启本地开发服务器。

小结

Vite 冷启动为什么快

Vite 运行 Dev 命令后只做了两件事情,一是启动了本地服务器并注册了一些中间件;二是使用 ESbuild 预构建模块。之后就一直躺着,直到浏览器以 http 方式发来 ESM 规范的模块请求时,Vite 才开始“「按需编译」”被请求的模块。

相对于 Webpack

Webpack 启动后会做一堆事情,经历一条很长的编译打包链条,从入口开始需要逐步经历语法解析、依赖收集、代码转译、打包合并、代码优化,最终将高版本的、离散的源码编译打包成低版本、高兼容性的产物代码,在 Node 运行时下性能必然有问题。

继续

调用server.listen()的逻辑在预构建一节中会详细介绍。回到createServer函数,接下来会详细分析下面几个点

  1. 如何获取config配置
  2. 创建一个兼容 rollup 钩子函数的对象container,这个是一个什么对象
  3. 模块图谱实例moduleGraph是什么样子的

如何获取config配置

createServer函数中,调用resolveConfig获取config配置

// inlineConfig 命令行传入的配置
const config = await resolveConfig(inlineConfig, 'serve', 'development')

resolveConfig函数也有很多内容,我们分块来看。

export async function resolveConfig(
    inlineConfig: InlineConfig,
    command: 'build' | 'serve', // 命令
    defaultMode = 'development' // 环境
  ): Promise<ResolvedConfig> {
    let config = inlineConfig // 命令行中的配置项
    let configFileDependencies: string[] = []
    let mode = inlineConfig.mode || defaultMode
  
    const configEnv = {
      mode, // 环境,开发环境下是 development
      command // 命令,开发环境下是 serve
    }
  
    let { configFile } = config // 配置文件路径
    
    // ...

刚进入函数,定义了 5 个变量,后面会用

Vite 是怎么找到配置文件的

继续向下

if (configFile !== false) {
    // 查找配置文件并获取配置文件中的 config 配置
    const loadResult = await loadConfigFromFile(
        configEnv,
        configFile, // 命令行传入的配置文件路径
        config.root,
        config.logLevel
    )
    if (loadResult) {
        // 合并配置
        config = mergeConfig(loadResult.config, config)
        // 获取配置文件绝对路径
        configFile = loadResult.path
        // 获取 vite.config.js 中导入的非第三方文件列表(比如自定义插件、方法文件等)
        configFileDependencies = loadResult.dependencies
    }
}

loadConfigFromFile函数中,也分为两步

  1. 查找、校验配置文件的路径,并判断文件类型(是不是ts、是不是遵循ESM规范)
  2. 根据路径和类型获取文件内容

获取配置文件路径

// loadConfigFromFile 函数内
let resolvedPath: string | undefined
let isTS = false
let isMjs = false // 是不是 ESM 规范的文件
let dependencies: string[] = []
try {
    // 如果 package.json 中的 type 属性是 module,则说明配置文件遵循 ESM 规范
    const pkg = lookupFile(configRoot, ['package.json'])
    if (pkg && JSON.parse(pkg).type === 'module') {
        isMjs = true
    }
} catch (e) {}
// 如果命令行中指定了配置文件路径,则获取此路径的绝对路径,并判断是不是 ts文件或者遵循 ESM 规范的文件
if (configFile) {
    resolvedPath = path.resolve(configFile)
    isTS = configFile.endsWith('.ts')

    if (configFile.endsWith('.mjs')) {
        isMjs = true
    }
} else {
    // 此时没有指定配置文件路径
    // 在项目根路径上查找 vite.config.js
    const jsconfigFile = path.resolve(configRoot, 'vite.config.js')
    // 如果存在,则将路径赋值给 resolvedPath
    if (fs.existsSync(jsconfigFile)) {
        resolvedPath = jsconfigFile
    }
    // 和上述逻辑相同,在项目根路径上查找 vite.config.mjs
    // 如果找到了,将路径赋值给 resolvedPath。并将 isMjs 置为 true
    if (!resolvedPath) {
        /* ... */
    }
    // 和上述逻辑相同,在项目根路径上查找 vite.config.ts
    // 如果找到了,将路径赋值给 resolvedPath。并将 isTS 置为 true
    if (!resolvedPath) {
        /* ... */
    }
}
// 如果没有,抛出异常
if (!resolvedPath) {
    debug('no config file found.')
    return null
}

查找过程很简单

  • package.json中判断配置文件是否遵循 ESM 规范
  • 如果指定了配置文件路径,则校验该路径并判断文件类型
  • 如果没有指定配置文件,从项目根目录按顺序查找vite.config.jsvite.config.mjsvite.config.ts;并判断文件类型

找到配置文件后,开始获取配置文件内容

获取配置文件内容

let userConfig: UserConfigExport | undefined

// 如果遵循 ESM 规范
if (isMjs) {
    const fileUrl = require('url').pathToFileURL(resolvedPath)
    // 如果是 ts 文件
    if (isTS) {
        // 通过 ESbuild 打包文件,第二个参数表示打包后的文件类型,true是遵循ESM规范
        const bundled = await bundleConfigFile(resolvedPath, true)
        // bundleConfigFile内调用的esbuild 的配置中设置了 metafile: true 用于生成依赖关系
        // 并且手写了一个 esbuild 的 plugin,不会将第三方库打包在 bundle 中,即生成的依赖也不会包含第三方库
        // 所以这里的 dependencies 内容,只包含用户自己写的文件
        dependencies = bundled.dependencies
        // 新建 js 文件,并将打包后的代码写入文件中
        fs.writeFileSync(resolvedPath + '.js', bundled.code)
        // 通过 import() 动态加载刚创建的 js 文件,并获取导出内容
        userConfig = (await dynamicImport(`${fileUrl}.js?t=${Date.now()}`))
            .default
        // 删除刚创建的文件
        fs.unlinkSync(resolvedPath + '.js')
    } else {
        // 直接动态加载该文件,并获取导出内容
        userConfig = (await dynamicImport(`${fileUrl}?t=${Date.now()}`)).default
    }
}

如果明确知道配置文件遵循ESM规范,则通过import()的方式加载文件获取导出内容。对于ts文件通过 ESbuild 打包文件。

还有一种情况就是配置文件是js文件,并且package.json中没有明确指出type: "module",此时这个js文件要么遵循ESM,要么遵循CommonJS。继续看代码

try {
    let userConfig: UserConfigExport | undefined
    if (isMjs) {
        const fileUrl = require('url').pathToFileURL(resolvedPath)
        if (isTS) {} else {}
    }
    // 如果 userConfig 为空,先尝试直接加载文件,假设遵循commonjs
    if (!userConfig && !isTS && !isMjs) {
        try {
            // 清空 require 中的缓存
            delete require.cache[require.resolve(resolvedPath)]
            // 重新 require
            userConfig = require(resolvedPath)
        } catch (e) {}
    }
    // 如果 userConfig 依然没有
    // 说明配置文件有几种可能,ts文件、遵循ESM、package.json中设置type是module但是配置文件遵循CommonJS规范
    if (!userConfig) {
        // 通过 esbuild 打包成 CommonJS,因为不确定该配置文件到底是遵循什么规范
        const bundled = await bundleConfigFile(resolvedPath)
        // 获取依赖信息
        dependencies = bundled.dependencies
        // 获取配置
        // 这里用这个函数的主要作用是即可以获取遵循ESM规范的导出,又可以获取遵循CommonJS规范的导出
        userConfig = await loadConfigFromBundledFile(resolvedPath, bundled.code)
    }
    // 如果 配置文件导出的是一个函数,则执行该函数
    const config = await(
        typeof userConfig === 'function' ? userConfig(configEnv) : userConfig
    )
    if (!isObject(config)) {
        throw new Error(`config must export or return an object.`)
    }
    // 返回 配置文件路径、配置文件导出内容、以及用户自定义导入
    return {
        path: normalizePath(resolvedPath),
        config,
        dependencies, // 自定义组件列表
    }
} catch (e) {}

这里就不过多解释了,注释已经很清楚了。最后就是获取到了配置文件的导出内容,并返回一个对象。要注意下对象内的属性

path: 配置文件路径,
config: 配置文件导出内容,
dependencies: 非第三方导入(包含用户自定义的插件)

到此,已经拿到了配置文件内容,接下来就是需要合并喝规范化配置项,方便后续使用

合并和规范配置项

// 查找配置文件并获取配置文件中的 config 配置
const loadResult = await loadConfigFromFile(/* ... */)
if (loadResult) {
    // 合并配置
    config = mergeConfig(loadResult.config, config)
    // 获取配置文件绝对路径
    configFile = loadResult.path
    // 非第三方导入的文件(比如自定义插件)
    configFileDependencies = loadResult.dependencies
}

调用mergeConfig函数合并命令行配置和vite.config.js的配置。配置合并完成之后,就开始处理配置项

处理配置

plugins

// 获取打包环境 development、production
mode = inlineConfig.mode || config.mode || mode
configEnv.mode = mode
// 根据 apply 属性将当前环境不支持的 plugins 过滤掉
const rawUserPlugins = (config.plugins || []).flat().filter((p) => {
    if (!p) {
        return false
    } else if (!p.apply) {
        return true
    } else if (typeof p.apply === 'function') {
        return p.apply({ ...config, mode }, configEnv)
    } else {
        return p.apply === command
    }
}) as Plugin[]

自定义插件的apply属性表示在什么环境下执行。上面这段代码的意思是

  • 如果没有apply,表示在开发、生产环境下都会添加到rawUserPlugins等待执行
  • 如果apply是一个函数,函数返回值是true,添加到rawUserPlugins等待执行
  • 如果apply的属性值等于当前环境字符串(servebuild),则添加到rawUserPlugins等待执行

过滤完之后,对rawUserPlugins中所有插件分类,根据enforce的属性值分类,代码如下

// 
/**
 * 属性值为 pre:表示提前执行的插件,放到 prePlugins 中
 * 属性值为 post:表示最后执行的插件,放到 postPlugins 中
 * 没有设置或者设置的是其他属性值:表示正常执行的插件,放到 normalPlugins 中
 */
const [prePlugins, normalPlugins, postPlugins] = sortUserPlugins(rawUserPlugins)

export function sortUserPlugins(plugins) {
    const prePlugins: Plugin[] = []
    const postPlugins: Plugin[] = []
    const normalPlugins: Plugin[] = []

    if (plugins) {
        plugins.flat().forEach((p) => {
            if (p.enforce === 'pre') prePlugins.push(p)
            else if (p.enforce === 'post') postPlugins.push(p)
            else normalPlugins.push(p)
        })
    }

    return [prePlugins, normalPlugins, postPlugins]
}

接下来就是执行所有自定义插件的config钩子函数

const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]
for (const p of userPlugins) {
    // 执行所有自定义插件的 config 钩子函数
    // 并传入vite的配置项和 configEnv
    // configEnv(对象内部: mode: 'development'|'production', command: 'serve'|'build' )
    if (p.config) {
        const res = await p.config(config, configEnv)
        // 也就是说 config 钩子函数可以修改配置项,并返回新的配置项
        // 拿到新的配置项之后,让新的配置项和老的配置项合并
        if (res) {
            config = mergeConfig(config, res)
        }
    }
}

这里有一个需要注意的点就是,config钩子函数可以修改配置项并返回新的配置项,拿到新配置项之后会合并新老配置项

最后会将 Vite 自带的插件和用户自定义的插件合并,后面会说

root 处理

const resolvedRoot = normalizePath(
    config.root ? path.resolve(config.root) : process.cwd()
)

获取config.root配置项的绝对路径或当前node命令执行时所在的文件夹目录

注意区分一下process.cwd()__dirname

  • process.cwd():指当前node命令执行时所在的文件夹目录
  • __dirname是指被执行js文件所在的文件夹目录

alias

// 创建新的alias
// /^[\/]?@vite\/env/ 替换成 'vite/dist/client/env.mjs'
// /^[\/]?@vite\/client/ 替换成 'vite/dist/client/client.mjs'
const clientAlias = [
    { find: /^[\/]?@vite\/env/, replacement: () => ENV_ENTRY },
    { find: /^[\/]?@vite\/client/, replacement: () => CLIENT_ENTRY },
]

// 将 clientAlias 和 配置项中的 alias 合并并返回
const resolvedAlias = mergeAlias(
    clientAlias,
    config.resolve?.alias || config.alias || []
)

合并完成之后的alias数据结构和上面的clientAlias一致。

resolve

// 获取 resolve 所有配置项
const resolveOptions: ResolvedConfig['resolve'] = {
    dedupe: config.dedupe,
    ...config.resolve,
    alias: resolvedAlias,
}

拼接 resolve 配置

.env 文件

  // 如果没有设置 config.envDir 则获取项目根路径
const envDir = config.envDir
    ? normalizePath(path.resolve(resolvedRoot, config.envDir))
    : resolvedRoot
// 获取所有.env 文件中的属性
const userEnv =
    inlineConfig.envFile !== false &&
    loadEnv(mode, envDir, resolveEnvPrefix(config))

loadEnv函数根据envDir依次查找下面4个文件

  1. .env.development.local
  2. .env.development
  3. .env.local
  4. .env

如果找到了调用dotenv解析该.env文件。

如果设置了config.envPrefix则只获取config.envPrefix前缀的变量。如果没设置config.envPrefix则获取VITE_开头的变量。

其他配置

// 解析 base
const BASE_URL = resolveBaseUrl(config.base, command === 'build', logger)
// 生产环境相关
const resolvedBuildOptions = resolveBuildOptions(config.build)

// 获取 package.json 路径
const pkgPath = lookupFile(resolvedRoot, [`package.json`], true /* pathOnly */)
// 获取/设置缓存目录,默认是 node_modules/.vite
const cacheDir = config.cacheDir
    ? path.resolve(resolvedRoot, config.cacheDir)
    : pkgPath && path.join(path.dirname(pkgPath), `node_modules/.vite`)

// 指定其他文件类型作为静态资源处理(这样导入它们就会返回解析后的 URL)
const assetsFilter = config.assetsInclude
    ? createFilter(config.assetsInclude) // 构造一个过滤函数,该函数可用于确定是否应该对某些模块进行操作
    : () => false

// 创建在特殊场景中使用的内部解析器
// 比如,预构建时,用于解析路径
const createResolver: ResolvedConfig['createResolver'] = (options) => {
    let aliasContainer: PluginContainer | undefined
    let resolverContainer: PluginContainer | undefined
    return async (id, importer, aliasOnly, ssr) => {}
}

const { publicDir } = config
// 获取静态资源地址
const resolvedPublicDir =
    publicDir !== false && publicDir !== ''
        ? path.resolve(
              resolvedRoot,
              typeof publicDir === 'string' ? publicDir : 'public'
          )
        : ''

上述配置处理完成之后,创建resolved对象,并拼接配置

resolved对象

const resolved: ResolvedConfig = {
    ...config,
    configFile: configFile ? normalizePath(configFile) : undefined, // 配置文件路径
    configFileDependencies, // vite.config.js 中非第三方包的导入,比如自定义插件
    inlineConfig, // 命令行中的配置
    root: resolvedRoot, // 项目根目录
    base: BASE_URL, // 公共基础路径, /my-app/index.html
    resolve: resolveOptions, // 文件解析时的相关配置
    publicDir: resolvedPublicDir, // 静态资源服务的文件夹
    cacheDir, // 缓存目录,默认 node_modules/.vite
    command,  // serve | build
    mode,     // development | production
    isProduction, // 是否是生产环境
    plugins: userPlugins, // 自定义 plugins
    server: resolveServerOptions(resolvedRoot, config.server),
    build: resolvedBuildOptions,
    env: {
        ...userEnv, // .env 文件
        BASE_URL,
        MODE: mode,
        DEV: !isProduction,
        PROD: isProduction,
    },
    assetsInclude(file: string) {
        // 一个函数,用于获取传入的 file 是否能作为静态资源处理,如果能,导入它们就会返回解析后的 URL
        return DEFAULT_ASSETS_RE.test(file) || assetsFilter(file)
    },
    logger,
    createResolver, // 特殊场景中使用的内部解析器,预构建文件中会说
    optimizeDeps: {
        ...config.optimizeDeps,
        esbuildOptions: { // esbuild 配置
            keepNames: config.optimizeDeps?.keepNames,
            preserveSymlinks: config.resolve?.preserveSymlinks,
            ...config.optimizeDeps?.esbuildOptions,
        },
    },
}

这个resolved对象就是最后处理完的所有配置。最后resolveConfig函数也会返回这个resolved对象。

从上面resolved.optimizeDeps.esbuildOptions可以看出,如果想配置esbuild的配置项,可以通过下面的方式

{
    resolve: {
        preserveSymlinks: boolean
    }
    optimizeDeps: {
        keepNames: boolean
        esbuildOptions: {}
    }
}

Vite 自带的插件和用户自定义的插件合并

处理插件的时候说过最后还会整合Vite自带的插件,代码如下

// 将vite自带插件和用户定义插件安顺序组合,并返回
;(resolved.plugins as Plugin[]) = await resolvePlugins(
    resolved,
    prePlugins,
    normalPlugins,
    postPlugins
)
// 只包含开发环境中使用的插件
export async function resolvePlugins(
    config: ResolvedConfig,
    prePlugins: Plugin[],
    normalPlugins: Plugin[],
    postPlugins: Plugin[]
): Promise<Plugin[]> {
    return [
        preAliasPlugin(),
        aliasPlugin({ entries: config.resolve.alias }),
        ...prePlugins, // 自定义前置插件
        config.build.polyfillModulePreload
            ? modulePreloadPolyfillPlugin(config)
            : null,
        resolvePlugin({
            ...config.resolve,
            root: config.root,
            isProduction: config.isProduction,
            ssrConfig: config.ssr,
            asSrc: true,
        }),
        htmlInlineScriptProxyPlugin(config),
        cssPlugin(config),
        config.esbuild !== false ? esbuildPlugin(config.esbuild) : null,
        jsonPlugin(
            {
                namedExports: true,
                ...config.json,
            }
        ),
        wasmPlugin(config),
        webWorkerPlugin(config),
        assetPlugin(config),
        ...normalPlugins, // 自定义插件
        definePlugin(config),
        cssPostPlugin(config),
        ...postPlugins // 自定义后置插件
}

所有插件拼接好之后,调用所有自定义插件configResolved钩子函数

await Promise.all(userPlugins.map((p) => p.configResolved?.(resolved)))