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

          下一代前端構(gòu)建工具 - Vite 2.x 源碼級(jí)分析

          共 57433字,需瀏覽 115分鐘

           ·

          2021-07-07 10:50

          業(yè)務(wù)背景

          筆者所負(fù)責(zé)的業(yè)務(wù) X (之后都以 “X 項(xiàng)目” 代表此業(yè)務(wù))對(duì)接眾多業(yè)務(wù)線,如果在業(yè)務(wù)線不斷增多,而人員又無(wú)法快速補(bǔ)充的前提下,有必要給開發(fā)提效。

          為何要給開發(fā)提效?

          說到這里,有人就會(huì)問了,我 Webpack 開項(xiàng)目 “只要幾十秒” 就能開起來,它不香嗎?就算我項(xiàng)目再大,改一行代碼熱更新好幾秒我也能忍受啊,大家不都是這么過來的嗎?

          當(dāng)然,公說公有理婆說婆有理,那么我就來講講我的道理。

          筆者每周會(huì)對(duì)接很多的需求,需求池爆棚的同時(shí),排期表也是層巒疊嶂的。。。

          有時(shí)候一堆需求突然要一起上線,而且很急,為了穩(wěn)妥,你需要事先測(cè)試一下有沒有問題,然后才能 PR/跑流水線啥的;還有的時(shí)候,你突然來了點(diǎn)小想法想要做個(gè)技術(shù)優(yōu)化,這個(gè)時(shí)候,打開 X 項(xiàng)目,在命令行開啟項(xiàng)目,不僅開的巨慢,而且 ctrl+c 還關(guān)不掉。

          然后你改小小的一行,變成了這樣:


          當(dāng)然你可以跟我說,這是你優(yōu)化的問題,只要我 node 版本更新/多線程/拆包。。(這里可以 Google 搜:如何優(yōu)化 Webpack 打包速度)

          但是我想讓你靜下心來看看下面的畫面:

          上述項(xiàng)目是使用 Vite 重構(gòu)之后的 X 項(xiàng)目,同樣多的文件,同樣多的依賴,可以看到第一次 2000+ ms,之后都是 600+ ms 就可以跑起來,而且即開即關(guān),沒有心理負(fù)擔(dān),快到飛起,DX 無(wú)敵。

          Vite 是什么?

          好,講完了業(yè)務(wù)背景之后,我們?cè)賮砜纯次覀兘裉斓呢i腳:“Vite”,為什么會(huì)有它,它是個(gè)啥,又做了啥,業(yè)務(wù)項(xiàng)目現(xiàn)在可以用嗎?生態(tài)如何?

          先找個(gè)知乎截個(gè)圖:

          再搬一下 Github 的倉(cāng)庫(kù)圖:

          上面的知乎、Github 鏈接都可打開,感興趣的同學(xué)可以自己去看看。

          接下來來回答一下這一節(jié)開篇提出的幾個(gè)問題。

          為什么會(huì)有 Vite ?

          我想最直觀的回答就是:“程序員都愛折騰吧”。

          但其實(shí)是,任何一個(gè)工具/產(chǎn)品的誕生并流行,其實(shí)都和當(dāng)下所處的時(shí)機(jī)有關(guān)。

          而出發(fā) Vite 誕生的幾個(gè)必要條件我總結(jié)如下:

          1. 傳統(tǒng)的構(gòu)建工具如 Webpack/Rollup/Parcel 等都太慢了,動(dòng)輒十幾秒、幾十秒的
          2. ES 標(biāo)準(zhǔn)普及越來越快,現(xiàn)代瀏覽器支持了原生的 ES Module 使用,類似這樣的語(yǔ)法 <script type="module" src="/src/main.js" />,就可以在 main.js 里面使用標(biāo)準(zhǔn)的 ES 模塊語(yǔ)法如 import/export 等,然后瀏覽器會(huì)根據(jù) main.jsimport 依賴,自動(dòng)發(fā)起 http 請(qǐng)求依賴的模塊,可以通過下面這個(gè)視頻直觀的看出來:


          1. 跨語(yǔ)言(基于 Go)、更快的構(gòu)建工具已經(jīng)誕生并趨于成熟,最直觀的就是 esbuild:


          可以看 esbuild 給出的 benchmark 表:

          最直觀的對(duì)比,相比第二名的 rollup + terser ,提升了約 100 倍,不講武德。。。


          1. 當(dāng)然第四點(diǎn),也是一個(gè)比較致命的一點(diǎn),已經(jīng)有個(gè)現(xiàn)成的模板可以 “抄”,比如 snowpack ??,它同時(shí)是去年 2020 年 JavaScript 最具創(chuàng)新力的打包工具:

          但是 snowpack 開發(fā)生產(chǎn)是一致的,都使用 esbuild + Browser Native ESM 的概念,這就導(dǎo)致很多瀏覽器尤其是低版本的 IE 是不支持這些最新的 ES 和瀏覽器特性的,壓根就用不了,相對(duì)使用場(chǎng)景就比較有限,明顯是一個(gè)超前于市場(chǎng)的產(chǎn)品。

          而尤大最厲害的一點(diǎn)就是,我開發(fā)用 snowpack 的那套概念,生產(chǎn)打包用 Rollup 來做,還搞個(gè)開發(fā)時(shí)的插件機(jī)制也兼容 Rollup,這就厲害了,開發(fā)很快,生產(chǎn)也照樣可以普及的用,這個(gè)應(yīng)用場(chǎng)景就很大了,自然獲得了更多的追捧,這從 Github 的 Star 就可以看出來:


          snowpack

          Vite

          而在 2 個(gè)多月前,Vite 的 Star 還沒有 snowpack 高。。。


          Vite 是啥?

          上面已經(jīng)提到了,Vite 是一個(gè)基于瀏覽器原生 ESM 的開發(fā)服務(wù)器/打包工具等,特點(diǎn)就是一個(gè)字 “快”,用尤大的話說就是:

          Vite 有多快?在 Repl.it 上從零啟動(dòng)一個(gè)基于 Vite 的 React 應(yīng)用,瀏覽器頁(yè)面加載完畢的時(shí)候,CRA(create-react-app)甚至還沒有裝完依賴。

          至于 Vite 做了啥,生態(tài)如何,在業(yè)務(wù)項(xiàng)目中是否可以使用,我留一點(diǎn)懸念,留在后續(xù)講解。(避免你看到這里就不看了,我的重頭戲還沒來呢。。。)

          “快” 背后運(yùn)行的原理是什么?(Vite 做了啥)

          先把調(diào)試環(huán)境搭好

          好了,說了這么多可能沒什么體感,有些人可能就不滿了,你個(gè)技術(shù)分享,搞了半天沒有一行代碼。。。

          Talk is cheap,show me the code!

          講解一門技術(shù)最后的方式就是 “Learn by doing”,下面我們就以這種方式來講解 Vite 源碼。

          首先初始化一個(gè) Vite 項(xiàng)目:

          yarn create @vitejs/app app-vue2 # 選中模板為 Vue,語(yǔ)言為 Javascript

          cd app-vue2 && yarn

          yarn dev

          閃電起項(xiàng)目 ??,不要幾十秒,也不用幾秒,只需 851ms,只需 851ms!

          我們先將 Vite 源碼拷貝到本地,然后在 app-vue2 項(xiàng)目中 link vite 依賴,開始調(diào)試源碼:

          git clone [email protected]:vitejs/vite.git

          cd vite && yarn

          yarn build # 構(gòu)建 vite 包

          cd packages/vite && yarn link

          接著去到 app-vue2 下面,關(guān)閉服務(wù)器,然后執(zhí)行如下命令:

          yarn link vite

          yarn dev

          打開瀏覽器可以看到如下界面:

          可以看到,我們的 network 面板里面加載了如下幾個(gè)模塊:

          1. localhost
          2. client
          3. main.js
          4. env.js
          5. vue.js?v=92bdfa16
          6. App.vue
          7. HelloWorld.vue
          8. App.vue?vue&type=style&index=0&lang.css
          9. HelloWorld.vue?vue&type=style&index=0&scoped=true&lang.css
          10. logo.png

          對(duì)應(yīng)到我們?cè)创a里面就是如下這幾塊:

          上面的 10 個(gè)請(qǐng)求可以分為以下幾類:

          1. Html 代碼,對(duì)應(yīng) localhost
          2. Vite 相關(guān)的代碼:client ,以及 client 里面引入的 env.js
          3. 用戶側(cè) JS 相關(guān)的代碼:main.jsApp.vueHelloWorld.vue
          4. NPM 依賴相關(guān)的代碼:vue.js?v=92bdfa16
          5. CSS 相關(guān)的代碼:App.vue?vue&type=style&index=0&lang.cssHelloWorld.vue?vue&type=style&index=0&scoped=true&lang.css

          當(dāng)然你可以使用 TS、Less/Sass 諸如此類的,但是為了講解需要,上面的內(nèi)容基本是最簡(jiǎn)單也相對(duì)比較全面的了。

          從上面的內(nèi)容我們可以看出來,Vite 相比 Webpack/Rollup 等主要的區(qū)別如下:

          1. 起服務(wù)快
          2. 并非打包成單一的 xxx.js 文件,然后 html 文件引入使用,而是基本保持和開發(fā)時(shí)的目錄結(jié)構(gòu)和引用關(guān)系一致,借助瀏覽器對(duì) ES Module 的支持,按需引用

          接下來我們就著重從源碼的角度分析這兩點(diǎn)不同!

          從 CLI 入口開始

          故事的起點(diǎn)要從我們 app-vue2yarn dev 腳本開始說起,yarn dev 實(shí)際上就是允許了 vite 命令,而 vite 命令對(duì)應(yīng)到 Vite 源碼中的如下位置:

          packages/vite/src/node/cli.ts

          cli

            .command('[root]'// default command

            .alias('serve')

            .option('--host <host>'`[string] specify hostname`)

            // ... options

            .option(

              '--force',

              `[boolean] force the optimizer to ignore the cache and re-bundle`

            )

            .action(async (root: string, options: ServerOptions & GlobalCLIOptions) => {

              // output structure is preserved even after bundling so require()

              // is ok here

              const { createServer } = await import('./server')

              try {

                const server = await createServer({

                  root,

                  base: options.base,

                  mode: options.mode,

                  configFile: options.config,

                  logLevel: options.logLevel,

                  clearScreen: options.clearScreen,

                  server: cleanOptions(options) as ServerOptions

                })

                await server.listen()

              } catch (e) {

                createLogger(options.logLevel).error(

                  chalk.red(`error when starting dev server:\n${e.stack}`)

                )

                process.exit(1)

              }

            })

          可以看到主要就是一個(gè)基于 cac 的命令行命令,主要的過程就是從 ./server 中導(dǎo)入 createServer ,然后創(chuàng)建一個(gè) server ,接著允許服務(wù)并監(jiān)聽端口,默認(rèn)為 3000 。

          一個(gè) “簡(jiǎn)單” 的服務(wù)器

          接下來再看看 server 文件中做了什么事情,主題邏輯代碼如下:

          packages/vite/src/node/server/index.ts

          https://github.com/vitejs/vite/blob/30ff5a235d2a832cb45a761a03c5947460417b40/packages/vite/src/node/server/index.ts#L295

          export async function createServer(

            inlineConfig: InlineConfig = {}

          ): Promise<ViteDevServer
          {

            const config = await resolveConfig(inlineConfig, 'serve''development')



            // ...



            const middlewares = connect() as Connect.Server

            const httpServer = middlewareMode

              ? null

              : await resolveHttpServer(serverConfig, middlewares)



            // ...



            const plugins = config.plugins

            const container = await createPluginContainer(config, watcher)

            const moduleGraph = new ModuleGraph(container)



            // ...



            const server: ViteDevServer = {

              config: config,

              middlewares,

              get app() {

                config.logger.warn(

                  `ViteDevServer.app is deprecated. Use ViteDevServer.middlewares instead.`

                )

                return middlewares

              },

              httpServer,

              pluginContainer: container,

              moduleGraph,

              transformWithEsbuild,

              transformRequest(url, options) {

                return transformRequest(url, server, options)

              },

              transformIndexHtmlnull as any,

              listen(port?: number, isRestart?: boolean) {

                return startServer(server, port, isRestart)

              },

              _optimizeDepsMetadatanull,

              _ssrExternalsnull,

              _globImporters: {},

              _isRunningOptimizerfalse,

              _registerMissingImportnull,

              _pendingReloadnull

            }



            server.transformIndexHtml = createDevHtmlTransformFn(server)



            // apply server configuration hooks from plugins

            const postHooks: ((() => void) | void)[] = []

            for (const plugin of plugins) {

              if (plugin.configureServer) {

                postHooks.push(await plugin.configureServer(server))

              }

            }



            // ...



            // main transform middleware

            middlewares.use(transformMiddleware(server))



            // ...



            // spa fallback

            if (!middlewareMode) {

              middlewares.use(

                history({

                  logger: createDebugger('vite:spa-fallback'),

                  // support /dir/ without explicit index.html

                  rewrites: [

                    {

                      from// $ /,

                      to({ parsedUrl }: any) {

                        createLogger('info').info(`rewrite: ${JSON.stringify(parsedUrl)}`)

                        const rewritten = parsedUrl.pathname + 'index.html'

                        if (fs.existsSync(path.join(root, rewritten))) {

                          return rewritten

                        } else {

                          return `/index.html`

                        }

                      }

                    }

                  ]

                })

              )

            }



            // ...



            if (!middlewareMode) {

              // transform index.html

              middlewares.use(indexHtmlMiddleware(server))

              // handle 404s

              middlewares.use((_, res) => {

                res.statusCode = 404

                res.end()

              })

            }



            // error handler

            middlewares.use(errorMiddleware(server, middlewareMode))



            // ...



            const runOptimize = async () => {

              if (config.cacheDir) {

                server._isRunningOptimizer = true

                try {

                  server._optimizeDepsMetadata = await optimizeDeps(config)

                } finally {

                  server._isRunningOptimizer = false

                }

                server._registerMissingImport = createMissingImporterRegisterFn(server)

              }

            }



            // ...



            // overwrite listen to run optimizer before server start

              const listen = httpServer.listen.bind(httpServer)

              httpServer.listen = (async (port: number, ...args: any[]) => {

                try {

                  await container.buildStart({})

                  await runOptimize()

                } catch (e) {

                  httpServer.emit('error', e)

                  return

                }

                return listen(port, ...args)

              }) as any



              httpServer.once('listening', () => {

                // update actual port since this may be different from initial value

                serverConfig.port = (httpServer.address() as AddressInfo).port

              })



              // ...



            return server

          }

          創(chuàng)建 server 的主體邏輯見上面的文件,主要做了如下幾件事:

          1. 獲取在起服務(wù)時(shí)需要的 config 配置,所有的配置內(nèi)容都是在 resolveConfig 這個(gè)函數(shù)里面處理的,包括 plugins 用戶插件和內(nèi)建插件、cacheDirnpm 依賴預(yù)構(gòu)建之后的緩存目錄、在之后瀏覽器按需獲取文件時(shí)對(duì)請(qǐng)求進(jìn)行截獲,返回相對(duì)應(yīng)內(nèi)容的處理函數(shù) createResolve ,以及定義在 vite.config.js 里面的 resolve ,包含用戶自定義的一些 alias 文件的處理等。
          const config = await resolveConfig(inlineConfig, "serve""development");
          1. 初始化 connect 框架生成 app 實(shí)例、傳給 http.createServer 生成 httpServer,然后注冊(cè)一系列中間件用于處理瀏覽器請(qǐng)求,包括對(duì) /js/css/vue 的請(qǐng)求等:

            1. 主要使用 sirv 這個(gè)包,將 /public 變?yōu)殪o態(tài)資源目錄的 servePublicMiddleware 中間件,可以通過 http://localhost:3000/public/xxx 獲取 public 目錄下的 xxx 文件
            2. 用于處理 js/css/vue 等請(qǐng)求,并返回轉(zhuǎn)換后的代碼的 transformMiddleware 中間件
            3. 用于處理 / ,并重定向到 /index.htmlhistory 中間件
          const middlewares = connect() as Connect.Server

          const httpServer = middlewareMode

              ? null

              : await resolveHttpServer(serverConfig, middlewares)



          // ...

          middlewares.use(servePublicMiddleware(config.publicDir))



           // main transform middleware

          middlewares.use(transformMiddleware(server))



          // ...

          middlewares.use(

            history({

              logger: createDebugger('vite:spa-fallback'),

              // support /dir/ without explicit index.html

              rewrites: [

                {

                  from// $ /,

                  to({ parsedUrl }: any) {

                    createLogger('info').info(`rewrite: ${JSON.stringify(parsedUrl)}`)

                    const rewritten = parsedUrl.pathname + 'index.html'

                    if (fs.existsSync(path.join(root, rewritten))) {

                      return rewritten

                    } else {

                      return `/index.html`

                    }

                  }

                }

              ]

            })

          )
          1. 用于處理插件的 container ,它是由 createPluginContainer 來創(chuàng)建,以及用于構(gòu)建模塊依賴圖的 moduleGraph ,它是由 new ModuleGraph(container) 創(chuàng)建,這兩個(gè)函數(shù)將在之后講解:
          const container = await createPluginContainer(config, watcher);

          // ...

          const moduleGraph = new ModuleGraph(container);
          1. 用于對(duì) html 進(jìn)行轉(zhuǎn)換,注入一些腳本的 transformIndexHtml 函數(shù),它由 createDevHtmlTransformFn 函數(shù)創(chuàng)建,它將會(huì)在 indexHtmlMiddleware 中間件執(zhí)行的過程中運(yùn)行 createDevHtmlTransformFn 函數(shù)中添加的 devHtmlHook ,在 html 文件中注入我們?cè)?localhost network 面板中看到的 <script type="module" src="/@vite/client"></script> 腳本,運(yùn)行 vite 相關(guān)的 client 腳本內(nèi)容。
          server.transformIndexHtml = createDevHtmlTransformFn(server);

          // ...

          middlewares.use(indexHtmlMiddleware(server));
          1. 用于進(jìn)行依賴預(yù)構(gòu)建的優(yōu)化函數(shù) runOptimize ,用于將 npm 依賴以及用戶指定的需要緩存的依賴進(jìn)行打包,并緩存在 node_modules/.vite 目錄下,針對(duì)這些文件的 http 請(qǐng)求都將添加緩存。
          const runOptimize = async () => {

              if (config.cacheDir) {

                server._isRunningOptimizer = true

                try {

                  server._optimizeDepsMetadata = await optimizeDeps(config)

                } finally {

                  server._isRunningOptimizer = false

                }

                server._registerMissingImport = createMissingImporterRegisterFn(server)

              }

          }



           // overwrite listen to run optimizer before server start

          const listen = httpServer.listen.bind(httpServer)

          httpServer.listen = (async (port: number, ...args: any[]) => {

            try {

              await container.buildStart({})

              await runOptimize()

            } catch (e) {

              httpServer.emit('error', e)

              return

            }

            return listen(port, ...args)

          }) as any



          httpServer.once('listening', () => {

            // update actual port since this may be different from initial value

            serverConfig.port = (httpServer.address() as AddressInfo).port

          })

          cli 中調(diào)用 server.listen() 后,會(huì)首先執(zhí)行 container.buildStart({}) 調(diào)用所有注冊(cè)插件的 buildStart 鉤子函數(shù),然后運(yùn)行 runOptimize 依賴預(yù)構(gòu)建函數(shù),最后是監(jiān)聽端口,接收來自瀏覽器的請(qǐng)求。

          傳說中的依賴預(yù)構(gòu)建

          從上面的整體代碼我們可以看到,在開啟服務(wù),監(jiān)聽端口接收來自瀏覽器的請(qǐng)求之前,會(huì)運(yùn)行插件 containerbuildStart 鉤子,進(jìn)而運(yùn)行所有插件的 buildStart 鉤子,以及進(jìn)行依賴預(yù)構(gòu)建,運(yùn)行 runOptimize 函數(shù)。

          可以看到整個(gè)運(yùn)行 Node 服務(wù)的生命周期中,都是一些基本不怎么耗時(shí)的收集 config 、注冊(cè)各種中間件、初始化一些之后會(huì)用到的插件容器 container 以及模塊依賴圖 moduleGraph 等,其中最耗時(shí)的就是依賴預(yù)構(gòu)建了,它主要將所有的 npm 依賴構(gòu)建成單一的可緩存文件,也是 Vite 服務(wù)開啟過程中的一個(gè)最大的時(shí)間瓶頸,因?yàn)?Vite 針對(duì)用戶項(xiàng)目中的各種文件都是不做打包處理的,而是在瀏覽器運(yùn)行時(shí)按需請(qǐng)求,并進(jìn)行轉(zhuǎn)換處理。

          這可以看做是 Vite 在極致的起服務(wù)速度和極慢的瀏覽器 “首屏出圖” 速度之間的一個(gè)權(quán)衡,而極慢的瀏覽器 “首屏出圖” 速度則是我會(huì)在后文提到的 Vite 有什么 “不好” 的內(nèi)容之一。

          下面就來分析一下這個(gè)神奇的預(yù)構(gòu)建過程。

          const runOptimize = async () => {
            if (config.cacheDir) {
              server._isRunningOptimizer = true;

              try {
                server._optimizeDepsMetadata = await optimizeDeps(config);
              } finally {
                server._isRunningOptimizer = false;
              }

              server._registerMissingImport = createMissingImporterRegisterFn(server);
            }
          };

          可以看到函數(shù)體,主要是執(zhí)行 optimizeDeps 函數(shù),返回依賴預(yù)構(gòu)建之后的元數(shù)據(jù),用于索引構(gòu)建之后的文件,以及映射構(gòu)建前后的文件路徑,然后注冊(cè) _registerMissingImport ,用于在項(xiàng)目運(yùn)行過程中添加新的 npm 依賴時(shí),也能預(yù)構(gòu)建到緩存目錄 node_modules/.vite 下。

          下面分析一下 optimizeDeps 函數(shù):

          packages/vite/src/node/optimizer/index.ts

          https://github.com/vitejs/vite/blob/30ff5a235d2a832cb45a761a03c5947460417b40/packages/vite/src/node/optimizer/index.ts#L102

          import { esbuildDepPlugin } from './esbuildDepPlugin'

          import { ImportSpecifier, init, parse } from 'es-module-lexer'

          import { scanImports } from './scan'



          export async function optimizeDeps(

            config: ResolvedConfig,

            force = config.server.force,

            asCommand = false,

            newDeps?: Record<string, string> // missing imports encountered after server has started

          ): Promise<DepOptimizationMetadata | null
          {

            config = {

              ...config,

              command'build'

            }



            const { root, logger, cacheDir } = config

            const log = asCommand ? logger.info : debug



            if (!cacheDir) {

              log(`No cache directory. Skipping.`)

              return null

            }



            const dataPath = path.join(cacheDir, '_metadata.json')

            const mainHash = getDepHash(root, config)

            const data: DepOptimizationMetadata = {

              hash: mainHash,

              browserHash: mainHash,

              optimized: {}

            }



            if (!force) {

              let prevData

              try {

                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 {

              fs.mkdirSync(cacheDir, { recursivetrue })

            }



            let deps: Record<string, string>, missing: Record<string, string>

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

              .substr(08)



            const include = config.optimizeDeps?.include

            if (include) {

              const resolve = config.createResolver({ asSrcfalse })

              for (const id of include) {

                if (!deps[id]) {

                  const entry = await resolve(id)

                  if (entry) {

                    deps[id] = entry

                  } else {

                    throw new Error(

                      `Failed to resolve force included dependency: ${chalk.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

            const maxListed = 5

            const listed = Math.min(total, maxListed)

            const extra = Math.max(0, total - maxListed)

            const depsString = chalk.yellow(

              qualifiedIds.slice(0, listed).join(`\n  `) +

                (extra > 0 ? `\n  (...and ${extra} more)` : ``)

            )



            const flatIdDeps: Record<string, string> = {}

            const idToExports: Record<string, ExportsData> = {}

            const flatIdToExports: Record<string, ExportsData> = {}



            await init

            for (const id in deps) {

              const flatId = flattenId(id)

              flatIdDeps[flatId] = deps[id]

              const entryContent = fs.readFileSync(deps[id], 'utf-8')

              const exportsData = parse(entryContent) as ExportsData

              for (const { ss, se } of exportsData[0]) {

                const exp = entryContent.slice(ss, se)

                if (/export\s+*\s+from/.test(exp)) {

                  exportsData.hasReExports = true

                }

              }

              idToExports[id] = exportsData

              flatIdToExports[flatId] = exportsData

            }



            const define: Record<string, string> = {

              '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 = Date.now()



            const result = await build({

              entryPointsObject.keys(flatIdDeps),

              bundletrue,

              keepNames: config.optimizeDeps?.keepNames,

              format'esm',

              external: config.optimizeDeps?.exclude,

              logLevel'error',

              splittingtrue,

              sourcemaptrue,

              outdir: cacheDir,

              treeShaking'ignore-annotations',

              metafiletrue,

              define,

              plugins: [esbuildDepPlugin(flatIdDeps, flatIdToExports, config)]

            })



            const meta = result.metafile!



            createLogger('info').info(`${JSON.stringify(meta)}`)



            // the paths in `meta.outputs` are relative to `process.cwd()`

            const cacheDirOutputPath = path.relative(process.cwd(), cacheDir)



            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

                )

              }

            }



            writeFile(dataPath, JSON.stringify(data, null2))



            return data

          }

          可以看到,上面的函數(shù)主要做了這么幾件事情:

          1. 接收 config,然后 data,形如 { hash, browserHash, optimized } ,其中 browserHash 主要用于瀏覽器獲取預(yù)構(gòu)建的 npm 依賴時(shí),添加的查詢字符串,用于在依賴變化時(shí),瀏覽器能更新緩存,也就是我們之前看到的 vue.js?v=92bdfa16 ,這個(gè) 92bdfa16 ,主要在處理瀏覽器請(qǐng)求時(shí),調(diào)用 resolvePlugin 時(shí),運(yùn)行 tryNodeResolve 函數(shù)對(duì) npm 依賴的請(qǐng)求添加這個(gè) browserHashoptimized 則是形如 npmDep: { file, src, needsInterop }的鍵值對(duì),比如 vue 依賴,則是如下內(nèi)容:
          "vue": {

            "file""/Users/bytedance/Projectes/my-projects/learning/vite/app-vue2/node_modules/.vite/vue.js",

            "src""/Users/bytedance/Projectes/my-projects/learning/vite/app-vue2/node_modules/vue/dist/vue.runtime.esm-bundler.js",

            "needsInterop"false

          }
          1. 如果 node_modules/.vite/_metadata.json 文件存在,且 hash 相同,則表示已經(jīng)構(gòu)建過了,并且沒有更新,則直接返回 prevData
          const dataPath = path.join(cacheDir, "_metadata.json");

          const mainHash = getDepHash(root, config);

          const data: DepOptimizationMetadata = {
            hash: mainHash,

            browserHash: mainHash,

            optimized: {},
          };

          if (!force) {
            let prevData;

            try {
              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;
            }
          }
          1. 通過 scanImports 找出需要依賴預(yù)構(gòu)建的依賴,結(jié)合用戶定義的需要處理的依賴 config.optimizeDeps?.include ,deps 是一個(gè)對(duì)象,是依賴名到其在文件系統(tǒng)中的路徑的映射如:{ vue: '/Users/bytedance/Projectes/my-projects/learning/vite/app-vue2/node_modules/vue/dist/vue.runtime.esm-bundler.js' }
          let deps: Record<string, string>, missing: Record<string, string>;

          if (!newDeps) {
            ({ deps, missing } = await scanImports(config));
          else {
            deps = newDeps;

            missing = {};
          }

          // ...

          const include = config.optimizeDeps?.include;

          if (include) {
            const resolve = config.createResolver({ asSrcfalse });

            for (const id of include) {
              if (!deps[id]) {
                const entry = await resolve(id);

                if (entry) {
                  deps[id] = entry;
                } else {
                  throw new Error(
                    `Failed to resolve force included dependency: ${chalk.cyan(id)}`
                  );
                }
              }
            }
          }
          1. 使用 es-module-lexerparse 處理依賴的代碼,讀取其中的 exportsData ,并完成依賴 id(文件路徑)到 exportsData 的映射,用于之后 esbuild 構(gòu)建時(shí)進(jìn)行依賴圖分析并打包到一個(gè)文件里面,其中 exportsData 為這個(gè)文件里引入的模塊 imports 和導(dǎo)出的模塊 exports
          const flatIdDeps: Record<string, string> = {}

            const idToExports: Record<string, ExportsData> = {}

            const flatIdToExports: Record<string, ExportsData> = {}



            await init

            for (const id in deps) {

              const flatId = flattenId(id)

              flatIdDeps[flatId] = deps[id]

              const entryContent = fs.readFileSync(deps[id], 'utf-8')

              const exportsData = parse(entryContent) as ExportsData

              for (const { ss, se } of exportsData[0]) {

                const exp = entryContent.slice(ss, se)

                if (/export\s+*\s+from/.test(exp)) {

                  exportsData.hasReExports = true

                }

              }

              idToExports[id] = exportsData

              flatIdToExports[flatId] = exportsData

            }

          舉個(gè) es-module-lexer 例子。

          1. 使用 esbuild 進(jìn)行依賴的預(yù)構(gòu)建,并將構(gòu)建之后的文件寫入緩存目錄:node_modules/.vite ,得益于 esbuild 比傳統(tǒng)構(gòu)建工具快 10-100 倍的速度,所以依賴預(yù)構(gòu)建也是非常快的,且一次構(gòu)建之后,后續(xù)可以緩存;

          build 構(gòu)建函數(shù)傳入用戶 vite.config.js define 定義的環(huán)境變量,需要進(jìn)行依賴預(yù)構(gòu)建的文件入口 Object.keys(flatIdDeps) 等, 以及處理依賴的 esbuild 插件 esbuildDepPlugin ,這個(gè)插件主要做了以下三件事:

          1. 主要用于處理某個(gè)依賴文件及其依賴圖,轉(zhuǎn)換 mjs|ts|jsx|tsx|svelte|vue 等文件成為 js 代碼,less|sass|scss|styl 等文件成為 css ,前提是使用了相關(guān)的插件,其中 mjs|ts|jsx|tsx 等是默認(rèn)支持的
          2. 將某個(gè)依賴的依賴圖中的文件統(tǒng)一打包到一個(gè) esm 文件中,如 vue 依賴,打包成一個(gè) vue.js ,或者 lodash 依賴,打包成一個(gè) lodash-es 文件,減少 http 請(qǐng)求數(shù)量

          一個(gè)比較直觀的例子就是,當(dāng)我們直接使用 import { debounce } from "lodash-es" 時(shí),瀏覽器會(huì)導(dǎo)入 600+ 文件,大概需要 1 s 多:

          而經(jīng)過依賴預(yù)構(gòu)建之后,瀏覽器只需要導(dǎo)入一個(gè)文件,且只需 20 ms :

          1. 處理一些不兼容模塊 commonjs 模塊等,將它們打包成 esm 文件,比如 react 的包,使得瀏覽器可以使用
          const define: Record<string, string> = {
            "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 = Date.now();

          const result = await build({
            entryPointsObject.keys(flatIdDeps),

            bundletrue,

            keepNames: config.optimizeDeps?.keepNames,

            format"esm",

            external: config.optimizeDeps?.exclude,

            logLevel"error",

            splittingtrue,

            sourcemaptrue,

            outdir: cacheDir,

            treeShaking"ignore-annotations",

            metafiletrue,

            define,

            plugins: [esbuildDepPlugin(flatIdDeps, flatIdToExports, config)],
          });
          1. 進(jìn)行依賴預(yù)構(gòu)建并寫入到緩存目錄之后,最后就是補(bǔ)充 data.optimized 內(nèi)容,并將內(nèi)容寫入到緩存目錄下的 _metadata.json 用于之后進(jìn)行依賴獲取和走構(gòu)建緩存等:
          const meta = result.metafile!



            createLogger('info').info(`${JSON.stringify(meta)}`)



            // the paths in `meta.outputs` are relative to `process.cwd()`

            const cacheDirOutputPath = path.relative(process.cwd(), cacheDir)



            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

                )

              }

            }



            writeFile(dataPath, JSON.stringify(data, null2))

          其中 needsInterop 為記錄那些在依賴預(yù)構(gòu)建時(shí),使用了 commonjs 語(yǔ)法的依賴,如果使用了 commonjs ,那么 needsInteroptrue ,這個(gè)屬性主要用于在瀏覽器請(qǐng)求對(duì)應(yīng)的依賴時(shí)(構(gòu)建前是 commonjs 形式),Vite 的 importAnalysisPlugin 插件會(huì)進(jìn)行依賴性導(dǎo)入分析,使用 transformCjsImport 函數(shù),它會(huì)對(duì)需要預(yù)編譯且為 CommonJS 的依賴導(dǎo)入代碼進(jìn)行重寫。舉個(gè)例子,當(dāng)我們?cè)?Vite 項(xiàng)目中使用 react 時(shí):

          import React, { useState, createContext } from "react";

          此時(shí) React 的導(dǎo)入就是 needsInterop 為 true,所以 importAnalysisPlugin 插件的會(huì)對(duì)導(dǎo)入 React 的代碼進(jìn)行重寫:

          import $viteCjsImport1_react from "/@modules/react.js";

          const React = $viteCjsImport1_react;

          const useState = $viteCjsImport1_react["useState"];

          const createContext = $viteCjsImport1_react["createContext"];

          之所以要進(jìn)行重寫的緣由是因?yàn)?CommonJS 的模塊并不支持命名方式的導(dǎo)出,即沒有 exports xxx 這樣的語(yǔ)法,只有 exports.xxx。所以,如果不經(jīng)過插件的轉(zhuǎn)化,則會(huì)看到這樣的異常:

          Uncaught SyntaxError: The requested module '/@modules/react.js' does not provide an export named 'useState'

          最后將 data 寫入的路徑為 node_modules/.vite/_metadata.json ,內(nèi)容如下:

          {

            "hash""cd74d918",

            "browserHash""92bdfa16",

            "optimized": {

              "vue": {

                "file""/Users/bytedance/Projectes/my-projects/learning/vite/app-vue2/node_modules/.vite/vue.js",

                "src""/Users/bytedance/Projectes/my-projects/learning/vite/app-vue2/node_modules/vue/dist/vue.runtime.esm-bundler.js",

                "needsInterop"false

              }

            }

          }

          依賴預(yù)構(gòu)建總結(jié)

          經(jīng)過上面的分析,我們可以總結(jié)依賴預(yù)構(gòu)建的幾點(diǎn)特點(diǎn):

          1. 在快速起服務(wù)和瀏覽器首屏出圖直接的一個(gè)取舍,而得益與 esbuild 的快速構(gòu)建,使得起服務(wù)快的同時(shí),瀏覽器首屏出圖也快,而且可以進(jìn)行緩存
          2. 使得可以使用一些 (j|t)sx? /vue /svelte 的包成為可能
          3. 針對(duì) commonjs 等也可以進(jìn)行轉(zhuǎn)換使用

          所以 Vite 并不是一個(gè)純的 bundless 工具,或者說構(gòu)建/編譯幾乎是不可或缺的內(nèi)容。

          一個(gè)請(qǐng)求的 Vite 之旅

          GET localhost

          實(shí)際 GET / => /index.html

          講完依賴預(yù)構(gòu)建,接下來我們可以放心的講解一個(gè)基于 Vite 的 Vue 項(xiàng)目的運(yùn)行過程,也就是我們?cè)?network 面板里面看到的那些請(qǐng)求,以及它們與項(xiàng)目目錄里面的對(duì)應(yīng)關(guān)系。

          首先我們知道,在 createServer 中注冊(cè)了 history 中間件,針對(duì) / 請(qǐng)求,會(huì)重定向到 /index.html,重定向之后的請(qǐng)求則會(huì)激活 indexHtmlMiddleware 中間件的處理:

          packages/vite/src/node/server/middlewares/indexHtml.ts 下的 indexHtmlMiddleware 函數(shù)內(nèi)容:

          export function indexHtmlMiddleware(
            server: ViteDevServer
          ): Connect.NextHandleFunction 
          {
            return async (req, res, next) => {
              const url = req.url && cleanUrl(req.url);

              // spa-fallback always redirects to /index.html

              if (url?.endsWith(".html") && req.headers["sec-fetch-dest"] !== "script") {
                createLogger("info").info(`html middleware`);

                const filename = getHtmlFilename(url, server);

                if (fs.existsSync(filename)) {
                  try {
                    let html = fs.readFileSync(filename, "utf-8");

                    // 這里調(diào)用 transformIndexHtml

                    html = await server.transformIndexHtml(url, html);

                    return send(req, res, html, "html");
                  } catch (e) {
                    return next(e);
                  }
                }
              }

              next();
            };
          }

          上面函數(shù)會(huì)調(diào)用 transformIndexHtml ,然后執(zhí)行 packages/vite/src/node/plugins/html.ts 下的 applyHtmlTransforms 函數(shù),執(zhí)行用于給 html 注入內(nèi)容的 hooks 如 [...preHooks, devHtmlHook, ...postHooks],并在 html 文件的 headbody 標(biāo)簽前后插入腳本。

          其中 devHtmlHook 主要做的事情就是在 html 文件頭部注入 <script type="module" src="/@vite/client"></script> 腳本,也就是我們看到的第一個(gè)請(qǐng)求 localhost 返回的內(nèi)容:

          devHtmlHook 則是在 server 中調(diào)用 createDevHtmlTransformFn 函數(shù)時(shí)注入的 Hooks,在 packages/vite/src/node/server/middlewares/indexHtml.ts 下的 createDevHtmlTransformFn 函數(shù)內(nèi)容:

          export function createDevHtmlTransformFn(

            server: ViteDevServer

          ): (url: string, html: string) => Promise<string
          {

            const [preHooks, postHooks] = resolveHtmlTransforms(server.config.plugins)



            return (url: string, html: string): Promise<string> => {

              return applyHtmlTransforms(

                html,

                url,

                getHtmlFilename(url, server),

                [...preHooks, devHtmlHook, ...postHooks],

                server

              )

            }

          }



          // devHtmlHook 函數(shù)

          const devHtmlHook: IndexHtmlTransformHook = async (

            html,

            { path: htmlPath, server }

          ) => {

            // TODO: solve this design issue

            // Optional chain expressions can return undefined by design

            // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain

            const config = server?.config!

            const base = config.base || '/'



            const s = new MagicString(html)

            let scriptModuleIndex = -1



            await traverseHtml(html, htmlPath, (node) => {

              if (node.type !== NodeTypes.ELEMENT) {

                return

              }



                  // ...



                html = s.toString()



                return {

                  html,

                  tags: [

                    {

                      tag'script',

                      attrs: {

                        type'module',

                        // 這里注入 /@vite/client 腳本

                        src: path.posix.join(base, CLIENT_PUBLIC_PATH)

                      },

                      injectTo'head-prepend'

                    }

                  ]

                }

              }

          GET client

          實(shí)際 GET /@vite/client

          首先會(huì)走 transformMiddleware

          export function transformMiddleware(

            server: ViteDevServer

          ): Connect.NextHandleFunction 
          {

            const {

              config: { root, logger, cacheDir },

              moduleGraph

            } = server



            // determine the url prefix of files inside cache directory

            let cacheDirPrefix: string | undefined

            if (cacheDir) {

              const cacheDirRelative = normalizePath(path.relative(root, cacheDir))

              if (cacheDirRelative.startsWith('../')) {

                // if the cache directory is outside root, the url prefix would be something

                // like '/@fs/absolute/path/to/node_modules/.vite'

                cacheDirPrefix = `/@fs/${normalizePath(cacheDir).replace(/ ^ //, '')}`

              } else {

                /
          / if the cache directory is inside root, the url prefix would be something

                /
          / like '/node_modules/.vite'

                cacheDirPrefix = `/
          ${cacheDirRelative}
          `


              }

            }



            return async (req, res, next) => {

              if (req.method !== 'GET' || knownIgnoreList.has(req.url!)) {

                return next()

              }



              const withoutQuery = cleanUrl(url)



                if (

                  isJSRequest(url) ||

                  isImportRequest(url) ||

                  isCSSRequest(url) ||

                  isHTMLProxy(url)

                ) {

                  // strip ?import

                  url = removeImportQuery(url)

                  // Strip valid id prefix. This is prepended to resolved Ids that are

                  // not valid browser import specifiers by the importAnalysis plugin.

                  url = unwrapId(url)



                  // for CSS, we need to differentiate between normal CSS requests and

                  // imports

                  if (isCSSRequest(url) && req.headers.accept?.includes('text/css')) {

                    url = injectQuery(url, 'direct')

                  }



                  // check if we can return 304 early

                  const ifNoneMatch = req.headers['if-none-match']

                  if (

                    ifNoneMatch &&

                    (await moduleGraph.getModuleByUrl(url))?.transformResult?.etag ===

                      ifNoneMatch

                  ) {

                    isDebug && debugCache(`[304] ${prettifyUrl(url, root)}`)

                    res.statusCode = 304

                    return res.end()

                  }



                  // resolve, load and transform using the plugin container

                  const result = await transformRequest(url, server, {

                    html: req.headers.accept?.includes('text/html')

                  })

                  if (result) {

                    const type = isDirectCSSRequest(url) ? 'css' : 'js'

                    const isDep =

                      DEP_VERSION_RE.test(url) ||

                      (cacheDirPrefix && url.startsWith(cacheDirPrefix))

                    return send(

                      req,

                      res,

                      result.code,

                      type,

                      result.etag,

                      // allow browser to cache npm deps!

                      isDep ? 'max-age=31536000,immutable' : 'no-cache',

                      result.map

                    )

                  }

                }

              } catch (e) {

                return next(e)

              }



              next()

            }

          }

          會(huì)命中 isJSRequest(url) 邏輯,進(jìn)入中間件的處理過程:

          1. 對(duì) url 進(jìn)行 transformRequest,主要的邏輯為通過 pluginContainer.resolveId 獲取到實(shí)際的文件位置 id ,然后根據(jù)這個(gè)位置,使用 pluginContainer.load 來獲取對(duì)應(yīng)的文件內(nèi)容,如果文件內(nèi)容并非瀏覽器可以直接使用的 esm 內(nèi)容,那么就需要 pluginContainer.transform 進(jìn)行文件內(nèi)容的轉(zhuǎn)換,最后返回轉(zhuǎn)換后的 codemap 以及 etag,用于緩存。
          export async function transformRequest(

            url: string,

            { config, pluginContainer, moduleGraph, watcher }: ViteDevServer,

            options: TransformOptions = {}

          ): Promise<TransformResult | null
          {

            url = removeTimestampQuery(url)

            const { root, logger } = config

            const prettyUrl = isDebug ? prettifyUrl(url, root) : ''

            const ssr = !!options.ssr



            // resolve

            const id = (await pluginContainer.resolveId(url))?.id || url

            const file = cleanUrl(id)



            let code: string | null = null

            let map: SourceDescription['map'] = null



            // load

            const loadStart = isDebug ? Date.now() : 0

            const loadResult = await pluginContainer.load(id, ssr)



            // ...

            if (typeof loadResult === 'object') {

                code = loadResult.code

                map = loadResult.map

              } else {

                code = loadResult

              }



              // ...



            // ensure module in graph after successful load

            const mod = await moduleGraph.ensureEntryFromUrl(url)

            ensureWatchedFile(watcher, mod.file, root)



            // transform

            const transformStart = isDebug ? Date.now() : 0

            const transformResult = await pluginContainer.transform(code, id, map, ssr)

            if (

              transformResult == null ||

              (typeof transformResult === 'object' && transformResult.code == null)

            ) {

              // no transform applied, keep code as-is

              isDebug &&

                debugTransform(

                  timeFrom(transformStart) + chalk.dim(` [skipped] ${prettyUrl}`)

                )

            } else {

              isDebug && debugTransform(`${timeFrom(transformStart)} ${prettyUrl}`)

              code = transformResult.code!

              map = transformResult.map

            }



            if (map && mod.file) {

              map = (typeof map === 'string' ? JSON.parse(map) : map) as SourceMap

              if (map.mappings && !map.sourcesContent) {

                await injectSourcesContent(map, mod.file)

              }

            }



            return (mod.transformResult = {

                code,

                map,

                etag: getEtag(code, { weaktrue })

              } as TransformResult)

          }

          pluginContainer.resolveId 在調(diào)用時(shí),會(huì)逐個(gè)調(diào)用每個(gè)插件上的 resolveId 方法,一旦遇到 aliasPlugin ,在 config 中,曾注冊(cè)過對(duì)應(yīng)的 /`` ^ ``/@vite// 的 alias,此插件將用于將 /``@vite/client 替換成 CLIENT_DIR + / + client ,也就是 vite/dist/client/client

          實(shí)際處于 packages/vite/src/node/config.ts 路徑下的 resolvedAlias

           // resolve alias with internal client alias

            const resolvedAlias = mergeAlias(

              // #1732 the CLIENT_DIR may contain $$ which cannot be used as direct

              // replacement string.

              // @ts-ignore because @rollup/plugin-alias' type doesn't allow function

              // replacement, but its implementation does work with function values.

              [{ find/ ^ /@vite//, replacement: () => CLIENT_DIR + '/' }],

              config.resolve?.alias || config.alias || []

            )



            const resolveOptions: ResolvedConfig['resolve'] = {

              dedupe: config.dedupe,

              ...config.resolve,

              alias: resolvedAlias

            }



            const resolved = {

            // ...

              resolve: resolveOptions

              // ...

          }

          packages/vite/src/node/plugins/index.tsaliasPlugin 插件中作為參數(shù)傳入

          export async function resolvePlugins(

            config: ResolvedConfig,

            prePlugins: Plugin[],

            normalPlugins: Plugin[],

            postPlugins: Plugin[]

          ): Promise<Plugin[]> 
          {



          // ...



            return [

              isBuild ? null : preAliasPlugin(),

              aliasPlugin({ entries: config.resolve.alias }),

              ...prePlugins,

              // ...

            ].filter(Booleanas Plugin[]

          }

          aliasPlugin 里面改寫路徑之后,會(huì)繼續(xù)將改寫過的路徑傳給下一個(gè)插件,最終進(jìn)入 resolvePlugin 插件的 tryNodeResolve 函數(shù),獲取到 @fs/Users/bytedance/Projectes/my-projects/learning/vite/vite/packages/vite/dist/client/client.js 文件的路徑并返回,最終通過 pluginContainer.load獲取 loadResult,然后 通過 pluginContainer.transform 獲取其轉(zhuǎn)換過的代碼,通過 send 方法發(fā)送給瀏覽器,而 client.js 里面的代碼主要用于與服務(wù)器進(jìn)行 ws 通信來進(jìn)行 hmr 熱更新、以及重載頁(yè)面等操作。

          受限于篇幅,本文接下來的內(nèi)容不再細(xì)化。

          下面的所有請(qǐng)求,都會(huì)走一個(gè)類似上面的流程,最終發(fā)送給瀏覽器的代碼是瀏覽器可以運(yùn)行的代碼,其中針對(duì) Vue 文件是需要走類似 @vitejs/plugin-vue 的 plugin 的轉(zhuǎn)換的,感興趣的同學(xué)可以自行了解一下。

          GET /src/main.js

          實(shí)際 GET /src/main.js

          GET env.js

          實(shí)際 GET /@fs/Users/bytedance/Projectes/my-projects/learning/vite/vite/packages/vite/dist/client/env.js

          GET vue.js?v=92bdfa16

          實(shí)際 GET /node_modules/.vite/vue.js?v=92bdfa16

          GET App.vue

          實(shí)際 GET /src/App.vue

          GET App.vue?vue&type=style&index=0&lang.css

          實(shí)際 GET /src/App.vue?vue&type=style&index=0&lang.css

          有什么 “不好” 的?

          正如上面提到的,Vite 只對(duì) npm 依賴進(jìn)行預(yù)構(gòu)建,對(duì)于用戶編寫的文件不進(jìn)行預(yù)處理,而是通過瀏覽器支持的 ES Module 來進(jìn)行按需讀取,所以如果用戶文件過多,且沒有進(jìn)行一定的 Code Spliting 等操作,那么可想而知,首屏是非常慢的,可以通過這個(gè)視頻直觀的看出來:

          所以使用 Vite 的開發(fā),對(duì)我們的首屏性能優(yōu)化就提出了更高的要求,這也直接給生產(chǎn)下帶來了一定幫助,也正是因?yàn)?Vite 是主要面向開發(fā)側(cè)的,所以可以盡可能的用最先進(jìn)的技術(shù),如 Http2?Http3?來進(jìn)行網(wǎng)絡(luò)請(qǐng)求,以及更好的懶加載、緩存技術(shù)。

          還有一點(diǎn)就是,Vue 生產(chǎn)內(nèi)建了 Rollup 打包工具,這對(duì)原先使用 Webpack 的項(xiàng)目也不太友好,但得益于 Vite 社區(qū)的活躍和尤大的號(hào)召力,社區(qū)中已經(jīng)有成型的基于 Webpack 來生產(chǎn)打包,開發(fā)使用 Vite 的解決方案:https://github.com/IndexXuan/vue-cli-plugin-vite#readme

          生態(tài)如何?

          Vite 擁有比較完善的生態(tài),主要的項(xiàng)目如 https://github.com/vitejs/awesome-vite 在不斷的更新,且 Vite 社區(qū)比較活躍,社區(qū)成員也很樂于解答問題:

          1. discord:https://discord.com/channels/804011606160703521/804011606160703524
          2. Github discussion:https://github.com/vitejs/vite/discussions

          同時(shí) Vite 支持多框架:React/Vue/Svelte 等。

          我能用在生產(chǎn)項(xiàng)目中嗎?

          如果你是想從頭開始一個(gè)新項(xiàng)目,亦或?qū)κ灼列阅軆?yōu)化有很大的興趣,那么建議你一定要試一試,有可能一不小心,就回不去了!

          Hail OpenSource!

          世界已經(jīng)被開源吞噬,慶幸在這樣一個(gè)商業(yè)氛圍及其濃厚的今天,我們還有幸能閱讀到優(yōu)秀的項(xiàng)目源碼,站在巨人的肩膀上!


          瀏覽 185
          點(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>
                  蜜臀久久99精品久久久久久宅男 | 成人免费大香蕉 | 免费观看一级二级网站 | 丰满少妇好紧好爽好湿无码 | 丁香花在线高清完整版视频 |