<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【Vite】1380- 詳解 Vite 依賴預(yù)構(gòu)建流程

          共 62410字,需瀏覽 125分鐘

           ·

          2022-07-13 14:45

          我們知道,首次執(zhí)行 vite 時(shí),服務(wù)啟動(dòng)后會(huì)對(duì) node_modules 模塊和配置 optimizeDeps 的目標(biāo)進(jìn)行預(yù)構(gòu)建。本節(jié)我們就去探索預(yù)構(gòu)建的流程。

          按照慣例,先準(zhǔn)備好一個(gè)例子。本文我們用 vue 的模板去初始化 DEMO:

          pnpm create vite my-vue-app -- --template vue

          項(xiàng)目創(chuàng)建好之后,我們?cè)侔惭b lodash-es 這個(gè)包,去研究 vite 是如何將幾百個(gè)文件打包成一個(gè)文件的:

          pnpm add lodash-es -P

          DEMO 代碼量比較多,這里就不貼代碼了,嫌麻煩的童鞋可以 fork Github repository[1] 。

          流程概覽

          當(dāng)我們服務(wù)啟動(dòng)之后,除了會(huì)調(diào)用插件容器的 buildStart 鉤子,還會(huì)執(zhí)行預(yù)構(gòu)建 runOptimize:

          // ...
          const listen = httpServer.listen.bind(httpServer)
          httpServer.listen = (async (port: number, ...args: any[]) => {
            if (!isOptimized) {
              try {
                // 插件容器初始化
                await container.buildStart({})
                // 預(yù)構(gòu)建
                await runOptimize()
                isOptimized = true
              } catch (e) {
                httpServer.emit('error', e)
                return
              }
            }
            return listen(port, ...args)
          }
          as any
          // ...

          const runOptimize = async () =>
           {
            server._isRunningOptimizer = true
            try {
              // 依賴預(yù)構(gòu)建
              server._optimizeDepsMetadata = await optimizeDeps(
                config,
                config.server.force || server._forceOptimizeOnRestart
              )
            } finally {
              server._isRunningOptimizer = false
            }
            server._registerMissingImport = createMissingImporterRegisterFn(server)
          }

          入口處將配置 config 和是否強(qiáng)制緩存的標(biāo)記(通過(guò) --force 傳入或者調(diào)用 restart API)傳到 optimizeDeps。optimizeDeps 邏輯比較長(zhǎng),我們先通過(guò)流程圖對(duì)整個(gè)流程有底之后,再按照功能模塊去閱讀源碼。

          簡(jiǎn)述一下整個(gè)預(yù)構(gòu)建流程:

          1. 首先會(huì)去查找緩存目錄(默認(rèn)是 node_modules/.vite)下的 _metadata.json 文件;然后找到當(dāng)前項(xiàng)目依賴信息(xxx-lock 文件)拼接上部分配置后做哈希編碼,最后對(duì)比緩存目錄下的 hash 值是否與編碼后的 hash 值一致,一致并且沒(méi)有開(kāi)啟 force 就直接返回預(yù)構(gòu)建信息,結(jié)束整個(gè)流程;
          2. 如果開(kāi)啟了 force 或者項(xiàng)目依賴有變化的情況,先保證緩存目錄干凈(node_modules/.vite 下沒(méi)有多余文件),在 node_modules/.vite/package.json 文件寫入 type: module 配置。這就是為什么 vite 會(huì)將預(yù)構(gòu)建產(chǎn)物視為 ESM 的原因。
          3. 分析入口,依次查看是否存在 optimizeDeps.entries、build.rollupOptions.input、*.html,匹配到就通過(guò) dev-scan 的插件尋找需要預(yù)構(gòu)建的依賴,輸出 deps 和 missing,并重新做 hash 編碼;
          4. 最后使用 es-module-lexer[2] 對(duì) deps 模塊進(jìn)行模塊化分析,拿到分析結(jié)果做預(yù)構(gòu)建。構(gòu)建結(jié)果將合并內(nèi)部模塊、轉(zhuǎn)換 CommonJS 依賴。最后更新 data.optimizeDeps 并將結(jié)果寫入到緩存文件。

          剝絲抽繭

          全流程上我們已經(jīng)清楚了,接下來(lái)我們就深入上述流程圖中綠色方塊(邏輯復(fù)雜)的代碼。因?yàn)椴襟E之間的代碼關(guān)聯(lián)比較少,在分析下面邏輯時(shí)會(huì)截取片段代碼

          計(jì)算依賴 hash

          export async function optimizeDeps(
            config: ResolvedConfig,
            force = config.server.force,
            asCommand = false,
            newDeps?: Record<stringstring>, // missing imports encountered after server has started
            ssr?: boolean
          ): Promise<DepOptimizationMetadata | null
          {
            // ...
           // 緩存文件信息
            const dataPath = path.join(cacheDir, '_metadata.json')
            // 獲取依賴的hash,這里的依賴是 lock 文件、以及 config 的部分信息
            const mainHash = getDepHash(root, config)
            // 定義預(yù)編譯優(yōu)化的元數(shù)據(jù)
            const data: DepOptimizationMetadata = {
              hash: mainHash,
              browserHash: mainHash,
              optimized: {}
            }

            // 不強(qiáng)制刷新
            if (!force) {
              let prevData: DepOptimizationMetadata | undefined
              try {
                // 讀取 metadata 信息
                prevData = JSON.parse(fs.readFileSync(dataPath, 'utf-8'))
              } catch (e) {}
              // hash is consistent, no need to re-bundle
              if (prevData && prevData.hash === data.hash) {
                log('Hash is consistent. Skipping. Use --force to override.')
                return prevData
              }
            }
            
            // 存在緩存目錄,清空目錄
            if (fs.existsSync(cacheDir)) {
              emptyDir(cacheDir)
            } else {
              // 創(chuàng)建多層級(jí)緩存目錄
              fs.mkdirSync(cacheDir, { recursive: true })
            }
            // 緩存目錄的模塊被識(shí)別成 ESM
            writeFile(
              path.resolve(cacheDir, 'package.json'),
              JSON.stringify({ type'module' })
            )
            
            // ...
          }

          // 所有可能的依賴 lock 文件,分別對(duì)應(yīng) npm、yarn、pnpm 的包管理
          const lockfileFormats = ['package-lock.json''yarn.lock''pnpm-lock.yaml']

          /**
           * 獲取依賴的 hash 值
           *
           * @param {string} root 根目錄
           * @param {ResolvedConfig} config 服務(wù)配置信息
           * @return {*}  {string}
           */

          function getDepHash(root: string, config: ResolvedConfig): string {
            // 獲取 lock 文件的內(nèi)容
            let content = lookupFile(root, lockfileFormats) || ''
            // 同時(shí)也將跟部分會(huì)影響依賴的 config 的配置一起加入到計(jì)算 hash 值
            content += JSON.stringify(
              {
                mode: config.mode,
                root: config.root,
                resolve: config.resolve,
                assetsInclude: config.assetsInclude,
                plugins: config.plugins.map((p) => p.name),
                optimizeDeps: {
                  include: config.optimizeDeps?.include,
                  exclude: config.optimizeDeps?.exclude
                }
              },
              (_, value) => {
                // 常見(jiàn)的坑:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
                if (typeof value === 'function' || value instanceof RegExp) {
                  return value.toString()
                }
                return value
              }
            )
            // 通過(guò) crypto 的 createHash 進(jìn)行 hash 加密
            return createHash('sha256').update(content).digest('hex').substring(08)
          }

          上述代碼先去 cacheDir 目錄下獲取 _metadata.json 的信息,然后計(jì)算當(dāng)前依賴的 hash 值,計(jì)算過(guò)程主要是通過(guò) xxx-lock 文件,結(jié)合 config 中跟依賴相關(guān)的部分配置去計(jì)算 hash 值。最后判斷如果服務(wù)沒(méi)有開(kāi)啟 force (即刷新緩存的參數(shù))時(shí),去讀取緩存元信息文件中的 hash 值,結(jié)果相同就直接返回緩存元信息文件即 _metadata.json 的內(nèi)容;

          否則就判斷是否存在 cacheDir(默認(rèn)情況下是 node_modules/.vite),存在就清空目錄文件,不存在就創(chuàng)建緩存目錄;最后在緩存目錄下創(chuàng)建 package.json 文件并寫入 type: module 信息,這就是為什么預(yù)構(gòu)建后的依賴會(huì)被識(shí)別成 ESM 的原因。

          在開(kāi)啟了 force 參數(shù)或者依賴前后的 hash 值不相同時(shí),就會(huì)去掃描并分析依賴,這就進(jìn)入下一個(gè)階段。

          依賴搜尋,智能分析

           // ... 接上述代碼


           let deps: Record<stringstring>, missing: Record<stringstring>
            // 沒(méi)有新的依賴的情況,掃描并預(yù)構(gòu)建全部的 import
            if (!newDeps) {
              ;({ deps, missing } = await scanImports(config))
            } else {
              deps = newDeps
              missing = {}
            }

            // update browser hash
            data.browserHash = createHash('sha256')
              .update(data.hash + JSON.stringify(deps))
              .digest('hex')
              .substring(08)

            // 遺漏的包
            const missingIds = Object.keys(missing)
            if (missingIds.length) {
              throw new Error(
                `The following dependencies are imported but could not be resolved:\n\n  ${missingIds
                  .map(
                    (id) =>
                      `${colors.cyan(id)} ${colors.white(
                        colors.dim(`(imported by ${missing[id]})`)
                      )}
          `

                  )
                  .join(`\n  `)}
          \n\nAre they installed?`

              )
            }

            // 獲取 optimizeDeps?.include 配置
            const include = config.optimizeDeps?.include
            if (include) {
              // 創(chuàng)建模塊解析器
              const resolve = config.createResolver({ asSrc: false })
              for (const id of include) {
                // normalize 'foo   >bar` as 'foo > bar' to prevent same id being added
                // and for pretty printing
                const normalizedId = normalizeId(id)
                if (!deps[normalizedId]) {
                  const entry = await resolve(id)
                  if (entry) {
                    deps[normalizedId] = entry
                  } else {
                    throw new Error(
                      `Failed to resolve force included dependency: ${colors.cyan(id)}`
                    )
                  }
                }
              }
            }

            const qualifiedIds = Object.keys(deps)

            if (!qualifiedIds.length) {
              writeFile(dataPath, JSON.stringify(data, null2))
              log(`No dependencies to bundle. Skipping.\n\n\n`)
              return data
            }

            const total = qualifiedIds.length
            // pre-bundling 的列表最多展示 5 項(xiàng)
            const maxListed = 5
            // 列表數(shù)量
            const listed = Math.min(total, maxListed)
            // 剩余的數(shù)量
            const extra = Math.max(0, total - maxListed)
            // 預(yù)編譯依賴的信息
            const depsString = colors.yellow(
              qualifiedIds.slice(0, listed).join(`\n  `) +
                (extra > 0 ? `\n  (...and ${extra} more)` : ``)
            )
            // CLI 下才需要打印
            if (!asCommand) {
              if (!newDeps) {
                // This is auto run on server start - let the user know that we are
                // pre-optimizing deps
                logger.info(colors.green(`Pre-bundling dependencies:\n  ${depsString}`))
                logger.info(
                  `(this will be run only when your dependencies or config have changed)`
                )
              }
            } else {
              logger.info(colors.green(`Optimizing dependencies:\n  ${depsString}`))
            }

           // ...

          上述代碼很長(zhǎng),關(guān)鍵都在 scanImports 函數(shù),這個(gè)涉及到 esbuild 插件和 API,我們待會(huì)拎出來(lái)分析。其他部分的代碼我們通過(guò)一張流程圖來(lái)講解:

          1. 開(kāi)始通過(guò) scanImports 找到全部入口并掃描全部的依賴做預(yù)構(gòu)建;返回 deps 依賴列表、missings 丟失的依賴列表;

          2. 基于 deps 做 hash 編碼,編碼結(jié)果賦給 data.browserHash,這個(gè)結(jié)果就是瀏覽器發(fā)起這些資源的 hash 參數(shù);

          3. 對(duì)于使用了 node_modules 下沒(méi)有定義的包,會(huì)發(fā)出錯(cuò)誤信息,并終止服務(wù);舉個(gè)例子,我引入 abcd 包:

          import { createApp } from 'vue'
          // 引用一個(gè)不存在的包
          import getABCD from 'abcd'
          import App from './App.vue'
          import '../lib/index'

          const s = getABCD('abc')
          console.log(s)

          createApp(App).mount('#app')

          然后執(zhí)行  dev:

          1. 將 vite.config.ts 中的 optimizeDeps.include[3] 數(shù)組中的值添加到 deps 中,也舉個(gè)例子:
          // vite.config.js
          import { defineConfig } from 'vite'
          import path from 'path'
          import vue from '@vitejs/plugin-vue'

          export default defineConfig({
            plugins: [vue()],

            optimizeDeps: {
              include: [
                path.resolve(process.cwd(), './lib/index.js')
              ]
            }
          })

          // ./lib/index.js 文件
          import { sayHello } from './foo'

          sayHello()

          // ./lib/foo.js
          export function sayHello ({
            console.log('hello vite prebundling')
          }

          上述代碼我們將 ./lib/index.js 這個(gè)文件添加到預(yù)構(gòu)建的 include 配置中,lib 下的兩個(gè)文件內(nèi)容也已經(jīng)明確了。接下來(lái)執(zhí)行 dev 后,我們從終端上就可以看到這個(gè)結(jié)果:

          我們的 lib/index.js 已經(jīng)被添加到預(yù)構(gòu)建列表。最后再看一下 node_modules/.vite,有一個(gè) _Users_yjcjour_Documents_code_vite_examples_vue-demo_lib_index_js.js 文件,并且已經(jīng)被構(gòu)建,還有 sourcemap 文件,這就是 optimizeDeps.include[4] 的作用。具體如何構(gòu)建這個(gè)文件的我們?cè)?導(dǎo)出分析 去梳理。

          1. 最后根據(jù) deps 的長(zhǎng)度去計(jì)算命令行中顯示的預(yù)構(gòu)建信息,并打印。

          上述整個(gè)流程邏輯比較簡(jiǎn)單,就梳理一個(gè)主流程并實(shí)際展示了部分配置的作用。還有一個(gè)關(guān)鍵的環(huán)節(jié)我們略過(guò)了——scanImports

          /**
           * 掃描全部引入
           * @param {ResolvedConfig} config
           */

          export async function scanImports(config: ResolvedConfig): Promise<{
            deps: Record<stringstring>
            missing: Record<stringstring>
          }> {
            const start = performance.now()

            let entries: string[] = []

            // 預(yù)構(gòu)建自定義條目
            const explicitEntryPatterns = config.optimizeDeps.entries
            // rollup 入口點(diǎn)
            const buildInput = config.build.rollupOptions?.input

            // 自定義條目?jī)?yōu)先級(jí)最高
            if (explicitEntryPatterns) {
              entries = await globEntries(explicitEntryPatterns, config)
            // 其次是 rollup 的 build 入口
            } else if (buildInput) {
              const resolvePath = (p: string) => path.resolve(config.root, p)
              // 字符串,轉(zhuǎn)成數(shù)組
              if (typeof buildInput === 'string') {
                entries = [resolvePath(buildInput)]
              // 數(shù)組,遍歷輸出路徑
              } else if (Array.isArray(buildInput)) {
                entries = buildInput.map(resolvePath)
              // 對(duì)象,返回對(duì)象的value數(shù)組
              } else if (isObject(buildInput)) {
                entries = Object.values(buildInput).map(resolvePath)
              } else {
                throw new Error('invalid rollupOptions.input value.')
              }
            // 默認(rèn)情況下,Vite 會(huì)抓取你的 index.html 來(lái)檢測(cè)需要預(yù)構(gòu)建的依賴項(xiàng)
            } else {
              entries = await globEntries('**/*.html', config)
            }

            // 合法的入口文件只能是存在的 js、html、vue、svelte、astro 文件
            entries = entries.filter(
              (entry) =>
                (JS_TYPES_RE.test(entry) || htmlTypesRE.test(entry)) &&
                fs.existsSync(entry)
            )

            // 找不到需要預(yù)構(gòu)建的入口
            if (!entries.length) {
              if (!explicitEntryPatterns && !config.optimizeDeps.include) {
                config.logger.warn(
                  colors.yellow(
                    '(!) Could not auto-determine entry point from rollupOptions or html files ' +
                      'and there are no explicit optimizeDeps.include patterns. ' +
                      'Skipping dependency pre-bundling.'
                  )
                )
              }
              return { deps: {}, missing: {} }
            } else {
              debug(`Crawling dependencies using entries:\n  ${entries.join('\n  ')}`)
            }

            // 依賴
            const deps: Record<stringstring> = {}
            // 缺失的依賴
            const missing: Record<stringstring> = {}
            // 創(chuàng)建插件容器,為什么這里需要單獨(dú)創(chuàng)建一個(gè)插件容器?而不是使用 createServer 時(shí)創(chuàng)建的那個(gè)
            const container = await createPluginContainer(config)
            // 創(chuàng)建 esbuild 掃描的插件
            const plugin = esbuildScanPlugin(config, container, deps, missing, entries)
            // 外部傳入的 esbuild 配置
            const { plugins = [], ...esbuildOptions } =
              config.optimizeDeps?.esbuildOptions ?? {}

            // 遍歷所有入口全部進(jìn)行預(yù)構(gòu)建
            await Promise.all(
              entries.map((entry) =>
                build({
                  absWorkingDir: process.cwd(),
                  write: false,
                  entryPoints: [entry],
                  bundle: true,
                  format: 'esm',
                  logLevel: 'error',
                  plugins: [...plugins, plugin],
                  ...esbuildOptions
                })
              )
            )

            debug(`Scan completed in ${(performance.now() - start).toFixed(2)}ms:`, deps)

            return {
              deps,
              missing
            }
          }

          掃描入口先從 optimizeDeps.entries 獲??;如果沒(méi)有就去獲取 build.rollupOptions.input 配置,處理了 input 的字符串、數(shù)組、對(duì)象形式;如果都沒(méi)有,就默認(rèn)尋找 html 文件。然后傳入 deps、missing 調(diào)用 esbuildScanPlugin 函數(shù)生成掃描插件,并從 optimizeDeps.esbuildOptions 獲取外部定義的 esbuild 配置,最后調(diào)用 esbuild.build API 進(jìn)行構(gòu)建。整個(gè)流程匯總成一張圖如下:

          重點(diǎn)來(lái)了,使用 vite:dep-scan 插件掃描依賴,并將在 node_modules 中的依賴定義在 deps 對(duì)象中,缺失的依賴定義在 missing 中。接著我們就進(jìn)入該插件內(nèi)部,一起學(xué)習(xí) esbuild 插件機(jī)制:

          // 匹配 html <script type="module"> 的形式
          const scriptModuleRE =
            /(<script\b[^>]*type\s*=\s*(?:"module"|'module')[^>]*>)(.*?)<\/script>/gims
          // 匹配 vue <script> 的形式
          export const scriptRE = /(<script\b(?:\s[^>]*>|>))(.*?)<\/script>/gims
          // 匹配 html 中的注釋
          export const commentRE = /<!--(.|[\r\n])*?-->/
          // 匹配 src 的內(nèi)容
          const srcRE = /\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im
          // 匹配 type 的內(nèi)容
          const typeRE = /\btype\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im
          // 匹配 lang 的內(nèi)容
          const langRE = /\blang\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im
          // 匹配 context 的內(nèi)容
          const contextRE = /\bcontext\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s'">]+))/im

          /**
           * esbuid 掃描依賴插件
           *
           * @param {ResolvedConfig} config 配置信息
           * @param {PluginContainer} container 插件容器
           * @param {Record<string, string>} depImports 預(yù)構(gòu)建的依賴
           * @param {Record<string, string>} missing 缺失的依賴
           * @param {string[]} entries optimizeDeps.entries 的數(shù)據(jù)
           * @return {*}  {Plugin}
           */

          function esbuildScanPlugin(
            config: ResolvedConfig,
            container: PluginContainer,
            depImports: Record<stringstring>,
            missing: Record<stringstring>,
            entries: string[]
          ): Plugin 
          {
            const seen = new Map<stringstring | undefined>()

            const resolve = async (id: string, importer?: string) => {
              const key = id + (importer && path.dirname(importer))
              if (seen.has(key)) {
                return seen.get(key)
              }
              // 使用 vite 插件容的解析能力,獲取具體的依賴信息
              const resolved = await container.resolveId(
                id,
                importer && normalizePath(importer)
              )
              const res = resolved?.id
              seen.set(key, res)
              return res
            }

            // 獲取 optimizeDeps.include 配置
            const include = config.optimizeDeps?.include
            // 排除預(yù)構(gòu)建到包內(nèi)的文件
            const exclude = [
              ...(config.optimizeDeps?.exclude || []),
              '@vite/client',
              '@vite/env'
            ]

            // 將 external 設(shè)置為 true 以將模塊標(biāo)記為外部,這意味著它不會(huì)包含在包中,而是會(huì)在運(yùn)行時(shí)導(dǎo)入
            const externalUnlessEntry = ({ path }: { path: string }) => ({
              path,
              external: !entries.includes(path)
            })
            
            // 返回 esbuild 的插件
            return {
              name: 'vite:dep-scan',
              setup(build) {
                console.log('build options ------->', build.initialOptions)

               // ...省略大量 onResolve 和 onLoad 的回調(diào)
              }
            }
          }

          閱讀 esbuild 的 dep-scan 插件代碼需要 esbuild plugin 的前置知識(shí)[5],對(duì)比于 rollup,esbuild 插件有很多相似之處,因?yàn)?API 簡(jiǎn)單也會(huì)更加好理解。

          上述代碼先定義了一堆正則表達(dá)式,具體的匹配內(nèi)容已經(jīng)在注釋中聲明??梢钥吹剑瑨呙枰蕾嚭诵木褪?strong style="color: black;">對(duì)依賴進(jìn)行正則匹配(esbuild 的 onResolve),然后對(duì)于語(yǔ)法不支持的文件做處理(esbuild onLoad)。接下來(lái)我們就從 DEMO 入手,來(lái)完整地執(zhí)行一遍 esbuild 的構(gòu)建流程。這樣讀者既能深入了解 vite 預(yù)構(gòu)建時(shí)模塊的構(gòu)建流程,也能學(xué)會(huì) esbuild 插件的開(kāi)發(fā)。我們先來(lái)看一下 DEMO 的模塊依賴圖:

          對(duì)于本文示例而言,entries 就只有根目錄的 index.html,所以首先會(huì)進(jìn)入 html 流程:

          // const htmlTypesRE = /\.(html|vue|svelte|astro)$/,這些格式的文件都被歸類成 html 類型
          build.onResolve({ filter: htmlTypesRE }, async ({ path, importer }) => {
            console.log('html type resolve --------------->', path)
            return {
              path: await resolve(path, importer),
              // 將滿足這類正則的文件全部歸類成 html
              namespace'html'
            }
          })

          // extract scripts inside HTML-like files and treat it as a js module
          // 匹配上述歸類成 html 的模塊,namespace 是對(duì)應(yīng)的
          build.onLoad(
            { filter: htmlTypesRE, namespace'html' },
            async ({ path }) => {
              console.log('html type load  --------------->', path)
              let raw = fs.readFileSync(path, 'utf-8')
              // Avoid matching the content of the comment
              // 注釋文字全部刪掉
              raw = raw.replace(commentRE, '<!---->')
              // 是不是 html 文件,也可能是 vue、svelte 等
              const isHtml = path.endsWith('.html')
              // 是 html 文件就用 script module 正則,否則就用 vue 中 script 的匹配規(guī)則,具體見(jiàn)正則表達(dá)式
              const regex = isHtml ? scriptModuleRE : scriptRE
              // 注意點(diǎn):對(duì)于帶 g 的正則匹配,如果像匹配多個(gè)結(jié)果需要重置匹配索引
              // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastIndex
              regex.lastIndex = 0
              let js = ''
              let loader: Loader = 'js'
              let match: RegExpExecArray | null
              while ((match = regex.exec(raw))) {
             // openTag 開(kāi)始標(biāo)簽的內(nèi)容(包含屬性)
                const [, openTag, content] = match
                // 找到 type 的值
                const typeMatch = openTag.match(typeRE)
                const type =
                      typeMatch && (typeMatch[1] || typeMatch[2] || typeMatch[3])
                // 匹配語(yǔ)言,比如 vue-ts 中經(jīng)常寫的 <script lang="ts">
                const langMatch = openTag.match(langRE)
                const lang =
                      langMatch && (langMatch[1] || langMatch[2] || langMatch[3])
                // skip type="application/ld+json" and other non-JS types
                if (
                  type &&
                  !(
                    type.includes('javascript') ||
                    type.includes('ecmascript') ||
                    type === 'module'
                  )
                ) {
                  continue
                }
                // 等于這些結(jié)果,都用當(dāng)前l(fā)ang的解析器,esbuild 都支持這些 loader
                if (lang === 'ts' || lang === 'tsx' || lang === 'jsx') {
                  loader = lang
                }
                // 匹配 src,例子中的 index.html 匹配上了就轉(zhuǎn)成 import '/src/main.js'
                const srcMatch = openTag.match(srcRE)
                if (srcMatch) {
                  const src = srcMatch[1] || srcMatch[2] || srcMatch[3]
                  js += `import ${JSON.stringify(src)}\n`
                } else if (content.trim()) {
                  // moduleScripts:`<script context="module">` in Svelte and `<script>` in Vue
                  // localScripts:`<script>` in Svelte and `<script setup>` in Vue
                  const contextMatch = openTag.match(contextRE)
                  const context =
                        contextMatch &&
                        (contextMatch[1] || contextMatch[2] || contextMatch[3])
                  
                  // localScripts
                  if (
                    (path.endsWith('.vue') && setupRE.test(openTag)) ||
                    (path.endsWith('.svelte') && context !== 'module')
                  ) {
                    // append imports in TS to prevent esbuild from removing them
                    // since they may be used in the template
                    const localContent =
                          content +
                          (loader.startsWith('ts') ? extractImportPaths(content) : '')
                    
                    localScripts[path] = {
                      loader,
                      contents: localContent
                    }
                    // 加上 virtual-module: 前綴
                    js += `import ${JSON.stringify(virtualModulePrefix + path)}\n`
                  } else {
                    js += content + '\n'
                  }
                }
              }

             // ...
              return {
                loader,
                contents: js
              }
            }
          )

          當(dāng)入口是 index.html 時(shí),命中了 build.onResolve({ filter: htmlTypesRE }, ...) 這條解析規(guī)則,通過(guò) resolve 處理后返回 index.html 的絕對(duì)路徑,并將 namespace 標(biāo)記為 html,也就是歸成 html 類。后續(xù)匹配上 html 的 load 鉤子,就會(huì)進(jìn)入回到函數(shù)中。

          build.onLoad({ filter: htmlTypesRE, namespace: 'html' }, ...) 也是通過(guò) filter 和 namespace 去匹配文件。讀取 index.html 文件內(nèi)容之后,通過(guò)大量的正則表達(dá)式去匹配引入內(nèi)容。重點(diǎn)在于 <script type="module" src="/src/main.js"></script> 這段代碼會(huì)被解析成 import '/src/main.js',這就會(huì)進(jìn)入下一個(gè) resolve、load 過(guò)程。在進(jìn)入 JS_TYPE 的解析之前,有一個(gè)全匹配 resolver 先提出來(lái):

          build.onResolve(
            {
              filter: /.*/
            },
            async ({ path: id, importer }) => {
              console.log('all resloved --------------->', id)
              // 使用 vite 解析器來(lái)支持 url 和省略的擴(kuò)展
              const resolved = await resolve(id, importer)
              if (resolved) {
                // 外部依賴
                if (shouldExternalizeDep(resolved, id)) {
                  return externalUnlessEntry({ path: id })
                }

                // 擴(kuò)展匹配上了 htmlTypesRE,就將其歸類于 html
                const namespace = htmlTypesRE.test(resolved) ? 'html' : undefined

                return {
                  path: path.resolve(cleanUrl(resolved)),
                  namespace
                }
              } else {
                // resolve failed... probably unsupported type
                return externalUnlessEntry({ path: id })
              }
            }
          )

          build.onLoad({ filter: JS_TYPES_RE }, ({ path: id }) => {
            console.log('js load --------------->', id)
            // 獲取文件的后綴擴(kuò)展
            let ext = path.extname(id).slice(1)
            // 如果是 mjs,將 loader 重置成 js
            if (ext === 'mjs') ext = 'js'

            let contents = fs.readFileSync(id, 'utf-8')
            
            // 通過(guò) esbuild.jsxInject 來(lái)自動(dòng)為每一個(gè)被 ESbuild 轉(zhuǎn)換的文件注入 JSX helper
            if (ext.endsWith('x') && config.esbuild && config.esbuild.jsxInject) {
              contents = config.esbuild.jsxInject + `\n` + contents
            }

            // import.meta.glob 的處理
            if (contents.includes('import.meta.glob')) {
              return transformGlob(contents, id, config.root, ext as Loader).then(
                (contents) => ({
                  loader: ext as Loader,
                  contents
                })
              )
            }
            return {
              loader: ext as Loader,
              contents
            }
          })

          /.*/ 全匹配的 resolver 通過(guò) resolve 函數(shù)獲取完整地依賴路徑,比如這里的 /src/main.js 就會(huì)被轉(zhuǎn)成完整的絕對(duì)路徑。然后再來(lái)到 JS_TYPES_RE 的 loader,最終輸出文件內(nèi)容 contents 和對(duì)應(yīng)的解析器。/src/main.js 文件內(nèi)容:

          import { createApp } from 'vue'
          import App from './App.vue'
          import '../lib/index'

          createApp(App).mount('#app')

          接著就會(huì)處理 vue、./App.vue../lib/index 3個(gè)依賴。

          對(duì)于 vue 依賴,會(huì)跟 /^[\w@][^:]/[6]  匹配。

          // bare imports: record and externalize ----------------------------------
          build.onResolve(
            {
              // avoid matching windows volume
              filter: /^[\w@][^:]/
            },
            async ({ path: id, importer }) => {
              console.log('bare imports --------------->', id)
              // 首先判斷是否在外部入口列表中
              if (moduleListContains(exclude, id)) {
                return externalUnlessEntry({ path: id })
              }
              // 緩存,對(duì)于在node_modules或者optimizeDeps.include中已經(jīng)處理過(guò)的依賴,直接返回
              if (depImports[id]) {
                return externalUnlessEntry({ path: id })
              }
              const resolved = await resolve(id, importer)
              if (resolved) {
                // 對(duì)于一些特殊的場(chǎng)景,也會(huì)將其視作 external 排除掉
                if (shouldExternalizeDep(resolved, id)) {
                  return externalUnlessEntry({ path: id })
                }
                // 判斷是否在node_modules或者optimizeDeps.include中的依賴
                if (resolved.includes('node_modules') || include?.includes(id)) {
                  // 存到depImports中,這個(gè)對(duì)象就是外部傳進(jìn)來(lái)的deps,最后會(huì)寫入到緩存文件中的對(duì)象
                  // 如果是這類依賴,直接將其視作為“外部依賴”,不在進(jìn)行接下來(lái)的resolve、load
                  if (OPTIMIZABLE_ENTRY_RE.test(resolved)) {
                    depImports[id] = resolved
                  }
                  return externalUnlessEntry({ path: id })
                } else {
                  const namespace = htmlTypesRE.test(resolved) ? 'html' : undefined
                  // linked package, keep crawling
                  return {
                    path: path.resolve(resolved),
                    namespace
                  }
                }
              } else {
                missing[id] = normalizePath(importer)
              }
            }
          )

          經(jīng)過(guò)上述解析,vue 依賴會(huì)被視作 external dep,并將它緩存到 depImports,因?yàn)閷?duì)于 node_modules 或者 optimizeDeps.include 中的依賴,只需抓取一次即可。

          對(duì)于 ./App.vue,首先會(huì)進(jìn)入 html type resolve,然后進(jìn)入 html load load 的回調(diào)中。跟 index.html 不同的時(shí),此時(shí)是 .vue 文件,在執(zhí)行 match = regex.exec(raw) 的時(shí)候匹配到 <script setup> 沒(méi)有 src 路徑,會(huì)進(jìn)入 else 邏輯。將 App.vue 中的 script 內(nèi)容提取出來(lái),存到 localScripts 中。最終生成的對(duì)象:

          localScripts = {
            '/Users/yjcjour/Documents/code/vite/examples/vue-demo/src/App.vue': {
               loader: 'js',
                // vue 中 <script setup> 內(nèi)容
                contents: `
                  // This starter template is using Vue 3 <script setup> SFCs
                  // Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
                  import HelloWorld from './components/HelloWorld.vue'
                  import _ from 'lodash-es'

                  console.log(_.trim('   hello '))`
            }
          }

          這次 App.vue 執(zhí)行 load 后返回以下內(nèi)容:

          {
            loader'js',
            contents'import "virtual-module:/Users/yjcjour/Documents/code/vite/examples/vue-demo/src/App.vue"
          '

          }

          從而繼續(xù)執(zhí)行 virtualModuleRE 的爬?。?/p>

          // virtualModuleRE -> /^virtual-module:.*/
          // 匹配虛擬的這些模塊,比如 `<script>` in Svelte and `<script setup>` in Vue 都會(huì)生成虛擬模塊
          build.onResolve({ filter: virtualModuleRE }, ({ path }) => {
            console.log('virtual module resolved -------------->', path)
            return {
              // strip prefix to get valid filesystem path so esbuild can resolve imports in the file
              path: path.replace(virtualModulePrefix, ''),
              namespace'local-script'
            }
          })

          build.onLoad({ filter: /.*/namespace'local-script' }, ({ path }) => {
            // 即返回上述
            return localScripts[path]
          })

          virtual-module 的解析就是直接從 localScripts 獲取內(nèi)容:

          import HelloWorld from './components/HelloWorld.vue'
          import _ from 'lodash-es'

          console.log(_.trim('   hello '))`

          又會(huì)接著處理 ./components/HelloWorld.vue、lodash-es 依賴,這兩個(gè)依賴跟 App.vue 和 vue 的處理過(guò)程都是一樣的,這里就不詳細(xì)說(shuō)明了。

          最后對(duì)于 ./lib/index,跟上述解析 /src/main.js 的流程也是一致的。我們可以將整個(gè)依賴抓取和解析的過(guò)程用下圖總結(jié):

          通過(guò) resolve 依賴,load 解析,esbuild 就掃描完整個(gè)應(yīng)用的依賴。具體的處理路徑通過(guò)在源碼中打上日志,我們可以看到以下的處理路徑:

          路徑結(jié)果跟我們的分析是一樣的。插件執(zhí)行結(jié)束,作為參數(shù)傳入的 deps、missing 也完成了它們的收集(在對(duì) /^[\w@][^:]/ 做依賴分析時(shí)會(huì)給這兩個(gè)對(duì)象掛屬性)。

          // deps
          {
            'lodash-es''/Users/yjcjour/Documents/code/vite/examples/vue-demo/node_modules/lodash-es/lodash.js',
           'vue''/Users/yjcjour/Documents/code/vite/examples/vue-demo/node_modules/vue/dist/vue.runtime.esm-bundler.js'
          }

          // missing
          {}

          再結(jié)合開(kāi)頭的的流程,將 optimizeDeps.include 定義的項(xiàng)添加到 deps ,最終生成  deps 會(huì)添加上 lib/index 這條數(shù)據(jù):

          // deps
          {
            'lodash-es''/Users/yjcjour/Documents/code/vite/examples/vue-demo/node_modules/lodash-es/lodash.js',
           'vue''/Users/yjcjour/Documents/code/vite/examples/vue-demo/node_modules/vue/dist/vue.runtime.esm-bundler.js',
            // optimizeDeps.include 定義的預(yù)構(gòu)建項(xiàng)
            '/Users/yjcjour/Documents/code/vite/examples/vue-demo/lib/index.js''/Users/yjcjour/Documents/code/vite/examples/vue-demo/lib/index.js'
          }

          有了完整的 deps 信息,就可以開(kāi)始執(zhí)行真正的預(yù)構(gòu)建。

          執(zhí)行預(yù)構(gòu)建、輸出結(jié)果

          在開(kāi)始這部分的分析之前,我們先安裝一個(gè)純 commonjs 的依賴 fs-extra。之后在 main.js 中引入這個(gè)包:

          import { createApp } from 'vue'
          import * as fsExtra from 'fs-extra'
          import App from './App.vue'
          import '../lib/index'

          createApp(App).mount('#app')
          console.log(fsExtra)

          然后在下述代碼打上斷點(diǎn):

           // esbuild generates nested directory output with lowest common ancestor base
            // this is unpredictable and makes it difficult to analyze entry / output
            // mapping. So what we do here is:
            // 1. flatten all ids to eliminate slash
            // 2. in the plugin, read the entry ourselves as virtual files to retain the
            //    path.

           // 拍平的依賴IDs
            const flatIdDeps: Record<stringstring> = {}
            // id <--> export
            const idToExports: Record<string, ExportsData> = {}
            // 拍平的id <--> export
            const flatIdToExports: Record<string, ExportsData> = {}

            const { plugins = [], ...esbuildOptions } =
              config.optimizeDeps?.esbuildOptions ?? {}

            await init
            for (const id in deps) {
              const flatId = flattenId(id)
              const filePath = (flatIdDeps[flatId] = deps[id])
              const entryContent = fs.readFileSync(filePath, 'utf-8'// 讀取依賴內(nèi)容
              let exportsData: ExportsData
              try {
                // 使用 es-module-lexer 對(duì)模塊的導(dǎo)入、導(dǎo)出做解析
                exportsData = parse(entryContent) as ExportsData
              } catch {
                // ...
              }
              
              // ss -> export start  se -> export end
              for (const { ss, se } of exportsData[0]) {
                // 截取 export、import 整個(gè)表達(dá)式
                const exp = entryContent.slice(ss, se)
                // 存在復(fù)合寫法 -> export * from 'xxx',打上 hasReExports 的標(biāo)志符號(hào)
                if (/export\s+\*\s+from/.test(exp)) {
                  exportsData.hasReExports = true
                }
              }
              idToExports[id] = exportsData
              flatIdToExports[flatId] = exportsData
            }

           // 組合環(huán)境變量
            const define: Record<stringstring> = {
              'process.env.NODE_ENV'JSON.stringify(config.mode)
            }
            for (const key in config.define) {
              const value = config.define[key]
              define[key] = typeof value === 'string' ? value : JSON.stringify(value)
            }

            const start = performance.now()

            // 執(zhí)行預(yù)編譯
            const result = await build({
              // 絕對(duì)路徑的工作目錄(項(xiàng)目根目錄)
              absWorkingDir: process.cwd(),
              // 入口點(diǎn)
              entryPoints: Object.keys(flatIdDeps),
              // 集合,將全部文件構(gòu)建后內(nèi)聯(lián)到入口文件
              bundle: true,
              // 輸出文件格式,支持 iife、cjs、esm
              format: 'esm',
              // 打包后的支持的環(huán)境目標(biāo)
              target: config.build.target || undefined,
              // 排除某些依賴的打包
              external: config.optimizeDeps?.exclude,
              // 日志級(jí)別,只顯示錯(cuò)誤信息
              logLevel: 'error',
              // 代碼拆分
              splitting: true,
              sourcemap: true,
              // 構(gòu)建輸出目錄,默認(rèn)是 node_modules/.vite
              outdir: cacheDir,
              // 忽略副作用注釋
              ignoreAnnotations: true,
              // 輸出構(gòu)建文件
              metafile: true,
              // 全局聲明
              define,
              // 插件
              plugins: [
                ...plugins,
                esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr)
              ],
              ...esbuildOptions
            })

            // 執(zhí)行構(gòu)建傳入了 metafile: true,這里能夠拿到構(gòu)建信息
            const meta = result.metafile!

            // the paths in `meta.outputs` are relative to `process.cwd()`
            // 緩存目錄
            const cacheDirOutputPath = path.relative(process.cwd(), cacheDir)

            // 更新 optimized 信息,全部寫入到 data.optimized 中
            for (const id in deps) {
              const entry = deps[id]
              data.optimized[id] = {
                file: normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')),
                src: entry,
                needsInterop: needsInterop(
                  id,
                  idToExports[id],
                  meta.outputs,
                  cacheDirOutputPath
                )
              }
            }

            // 預(yù)編譯結(jié)果寫入metadata文件
            writeFile(dataPath, JSON.stringify(data, null2))

            debug(`deps bundled in ${(performance.now() - start).toFixed(2)}ms`)
            return data

          在執(zhí)行預(yù)構(gòu)建前,先將存在多層級(jí)的 dep 拍平,目的是為了產(chǎn)物能夠更明確和簡(jiǎn)單。然后遍歷 deps 做 import、export 的解析。對(duì)于有 export * from 'xx' 的表達(dá)式,會(huì)打上 hasReExports 標(biāo)記;對(duì)于本文 DEMO 而言,執(zhí)行完 deps 的分析之后 flatIdDeps、 idToExports、flatIdToExports 的結(jié)果如截圖所示:

          從結(jié)果可以清楚地看到變量的含義:

          • flatIdDeps:拍平之后的 id 跟具體依賴路徑的映射;
          • idToExports:依賴 id 的 imports、 exports 信息;
          • flatIdToExports:拍平 id 的 imports、exports 信息;

          將以上變量傳到 esbuildDepPlugin 創(chuàng)建預(yù)構(gòu)建插件,再將環(huán)境變量等信息全部傳入,執(zhí)行真正的預(yù)構(gòu)建。此時(shí) esbuild.build 需要重點(diǎn)關(guān)注參數(shù)有:

          • entryPoints: Object.keys(flatIdDeps),就是上述拍平的依賴 id;
          • outdir: cacheDir,將產(chǎn)物輸出到緩存目錄,默認(rèn)是 node_modules/.vite;
          • plugins: [ ...plugins,  esbuildDepPlugin(flatIdDeps, flatIdToExports, config, ssr) ] 使用預(yù)構(gòu)建插件。

          緊接著我們進(jìn)入 esbuildDepPlugin 插件內(nèi)部,看預(yù)構(gòu)建執(zhí)行之前做了什么事情?

          export function esbuildDepPlugin(
            qualified: Record<stringstring>,
            exportsData: Record<string, ExportsData>,
            config: ResolvedConfig,
            ssr?: boolean
          ): Plugin 
          {
            // 默認(rèn) esm 默認(rèn)解析器
            const _resolve = config.createResolver({ asSrc: false })

            // node 的 cjs 解析器
            const _resolveRequire = config.createResolver({
              asSrc: false,
              isRequire: true
            })

            const resolve = (
              id: string,
              importer: string,
              kind: ImportKind,
              resolveDir?: string
            ): Promise<string | undefined> => {
              let _importer: string
              // explicit resolveDir - this is passed only during yarn pnp resolve for
              // entries
              if (resolveDir) {
                _importer = normalizePath(path.join(resolveDir, '*'))
              } else {
                // map importer ids to file paths for correct resolution
                _importer = importer in qualified ? qualified[importer] : importer
              }
              // kind 是 esbuild resolve 回調(diào)中的一個(gè)參數(shù),表示模塊類型,總共有 7 種類型
              // https://esbuild.github.io/plugins/#on-resolve-arguments
              // 以require開(kāi)頭的表示cjs、否則用esm的解析器
              const resolver = kind.startsWith('require') ? _resolveRequire : _resolve
              return resolver(id, _importer, undefined, ssr)
            }

            return {
              name: 'vite:dep-pre-bundle',
              setup(build) {
                console.log('dep-pre-bundle -------->, ', build.initialOptions)
                // ...

                /**
                 * 解析入口
                 *
                 * @param {string} id
                 * @return {*} 
                 */

                function resolveEntry(id: string{
                  const flatId = flattenId(id)
                  if (flatId in qualified) {
                    return {
                      path: flatId,
                      namespace'dep'
                    }
                  }
                }

                build.onResolve(
                  { filter: /^[\w@][^:]/ },
                  async ({ path: id, importer, kind }) => {
                    // ...

                    // ensure esbuild uses our resolved entries
                    let entry: { path: stringnamespacestring } | undefined
                    // if this is an entry, return entry namespace resolve result
                    if (!importer) {
                      if ((entry = resolveEntry(id))) return entry
                // ...
                    }

                    // use vite's own resolver
                    const resolved = await resolve(id, importer, kind)
                    if (resolved) {
                      if (resolved.startsWith(browserExternalId)) {
                        return {
                          path: id,
                          namespace'browser-external'
                        }
                      }
                      if (isExternalUrl(resolved)) {
                        return {
                          path: resolved,
                          external: true
                        }
                      }
                      return {
                        path: path.resolve(resolved)
                      }
                    }
                  }
                )

                const root = path.resolve(config.root)
                build.onLoad({ filter: /.*/namespace'dep' }, ({ path: id }) => {
                  console.log('dep load --------->', id)
                  const entryFile = qualified[id]

                  // 相對(duì)根目錄的路徑
                  let relativePath = normalizePath(path.relative(root, entryFile))
                  // 自動(dòng)加上路徑前綴
                  if (
                    !relativePath.startsWith('./') &&
                    !relativePath.startsWith('../') &&
                    relativePath !== '.'
                  ) {
                    relativePath = `./${relativePath}`
                  }

                  let contents = ''
                  // 獲取文件的 import、export 信息
                  const data = exportsData[id]
                  const [imports, exports] = data
                  if (!imports.length && !exports.length) {
                    // cjs
                    contents += `export default require("${relativePath}");`
                  } else {
                    if (exports.includes('default')) {
                      contents += `import d from "${relativePath}";export default d;`
                    }
                    if (
                      data.hasReExports ||
                      exports.length > 1 ||
                      exports[0] !== 'default'
                    ) {
                      contents += `\nexport * from "${relativePath}"`
                    }
                  }

                  let ext = path.extname(entryFile).slice(1)
                  if (ext === 'mjs') ext = 'js'
                  return {
                    loader: ext as Loader,
                    contents,
                    resolveDir: root
                  }
                })

                // ...
              }
            }
          }

          首先,上述 flatIdDeps 作為入口,先依次執(zhí)命中 /^[\w@][^:]/ filter 的解析,最后都會(huì)通過(guò) resolveEntry 套上 namespace: dep 的類型。然后執(zhí)行 namespace: dep 的 load 回調(diào)。接下來(lái)的流程通過(guò)一張圖去展示:

          流程可以概括成兩大步:

          1. 計(jì)算依賴相較于 root 項(xiàng)目根目錄的相對(duì)路徑,并做規(guī)范化——添加相對(duì)路徑符號(hào);
          2. 根據(jù) exportsData 也就是上面的 flatIdToExports 變量獲取 imports、exports 信息;然后做了三層判斷:
            1. 如果滿足 !imports.length && !exports.length,說(shuō)明這是一個(gè) CJS 模塊,就會(huì)輸出 export default require("${relativePath}");,對(duì)于 DEMO 而言,fs-extra 就是這種情況,最后輸出的 contents 是 'export default require("./node_modules/fs-extra/lib/index.js");';
            2. 不是 CJS,就判斷是否存在默認(rèn)導(dǎo)出(export default),有的話就會(huì)在 contents 上拼接 import d from "${relativePath}";export default d;;對(duì)于 DEMO 而言,lodash-es 因?yàn)橛心J(rèn)導(dǎo)出,同時(shí)也會(huì)執(zhí)行 c 的流程,最終生成的 contents 如 'import d from "./node_modules/lodash-es/lodash.js";export default d;\nexport * from "./node_modules/lodash-es/lodash.js"'
            3. 在上一步的基礎(chǔ)上,如果有其他的導(dǎo)出表達(dá)式比如 export { compile }; ,就會(huì)加多一層復(fù)合導(dǎo)出,將模塊的內(nèi)容全部導(dǎo)出,即 \nexport * from "${relativePath}",對(duì)于 DEMO 而言,vue 和 lib/index.js 會(huì)執(zhí)行這個(gè)邏輯,最后返回 '\nexport * from "./node_modules/vue/dist/vue.runtime.esm-bundler.js"''\nexport * from "./lib/index.js"'。

          第一層依賴4個(gè)入口就處理完成了,接下來(lái)就會(huì)對(duì)遞歸搜尋依賴的依賴。

          完成整個(gè)預(yù)構(gòu)建的依賴查找之后,就會(huì)執(zhí)行構(gòu)建,構(gòu)建后的 metafile 信息如下:

          input 信息太長(zhǎng),只打印了搜查的依賴總長(zhǎng)度是 692,最后構(gòu)建的產(chǎn)物從上圖能夠看到對(duì)于 lodash-es 這種包,會(huì)將依賴全部打成一個(gè)包,減少 http 次數(shù)。

          最后的最后,將 deps 信息更新到 data.optimized 并寫入到緩存文件目錄。整個(gè)預(yù)構(gòu)建流程就結(jié)束了。

          參考資料

          [1]

          Github repository: https://github.com/Jouryjc/vite

          [2]

          es-module-lexer: https://www.npmjs.com/package/es-module-lexer

          [3]

          optimizeDeps.include: https://cn.vitejs.dev/config/#optimizedeps-include

          [4]

          optimizeDeps.include: https://cn.vitejs.dev/config/#optimizedeps-include

          [5]

          前置知識(shí): https://esbuild.github.io/plugins/

          [6]

          /^[\w@][^:]/: https://regex101.com/r/5A7KFx/1


          瀏覽 114
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产成人摸屄操屄熟 | 欧美打炮网| 16—17女人毛片毛片国内 | 午夜无码福利 | 女人被插视频 |