1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 57433字,需瀏覽 115分鐘

         ·

        2021-07-04 09:44

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

        筆者所負(fù)責(zé)的業(yè)務(wù) X (之后都以 “X 項(xiàng)目” 代表此業(yè)務(wù))對(duì)接眾多業(yè)務(wù)線,如果在業(yè)務(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 無敵。

        Vite 是什么?

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

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

        再搬一下 Github 的倉庫圖:

        上面的知乎、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 使用,類似這樣的語法 <script type="module" src="/src/main.js" />,就可以在 main.js 里面使用標(biāo)準(zhǔn)的 ES 模塊語法如 import/export 等,然后瀏覽器會(huì)根據(jù) main.jsimport 依賴,自動(dòng)發(fā)起 http 請(qǐng)求依賴的模塊,可以通過下面這個(gè)視頻直觀的看出來:


        1. 跨語言(基于 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ì)使用場景就比較有限,明顯是一個(gè)超前于市場的產(chǎn)品。

        而尤大最厲害的一點(diǎn)就是,我開發(fā)用 snowpack 的那套概念,生產(chǎn)打包用 Rollup 來做,還搞個(gè)開發(fā)時(shí)的插件機(jī)制也兼容 Rollup,這就厲害了,開發(fā)很快,生產(chǎn)也照樣可以普及的用,這個(gè)應(yī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)用,瀏覽器頁面加載完畢的時(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,語言為 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.js 、App.vueHelloWorld.vue
        4. NPM 依賴相關(guān)的代碼:vue.js?v=92bdfa16
        5. CSS 相關(guān)的代碼:App.vue?vue&type=style&index=0&lang.css 、HelloWorld.vue?vue&type=style&index=0&scoped=true&lang.css

        當(dāng)然你可以使用 TS、Less/Sass 諸如此類的,但是為了講解需要,上面的內(nèi)容基本是最簡單也相對(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è) “簡單” 的服務(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è) browserHash;optimized 則是形如 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 語法的依賴,如果使用了 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 這樣的語法,只有 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)換后的 code 、map 以及 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 熱更新、以及重載頁面等操作。

        受限于篇幅,本文接下來的內(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)目源碼,站在巨人的肩膀上!

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

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产老头老太一级毛片 | 中文字幕有码中文字幕无码 | 色原无码 | 成人在线视频观看 | 国产又粗又爽又猛又大的动漫片 | 特级欧美精品AAAA | 怡红院AV| 狠操逼| 2015亚洲天堂 | 久久视频日本 |