<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 原理分析

          共 8274字,需瀏覽 17分鐘

           ·

          2021-05-09 02:46


          1 Vite: 一種新的、更快的 web 開發(fā)工具。

          2 特點(diǎn):

          1. 快速的冷啟動(dòng)

          2. 即時(shí)的模塊熱更新(保留在完全重新加載頁(yè)面時(shí)丟失的應(yīng)用程序狀態(tài)、只更新變更內(nèi)容、調(diào)整樣式更加快速)

          3. 真正的按需編譯

          3 構(gòu)建項(xiàng)目:

          $ yarn create vite-app <project-name>
          $ cd <project-name>
          $ yarn
          $ yarn dev

          4 實(shí)現(xiàn)機(jī)制:

          vite 使用 koa[1] 作 web server,使用 clmloader 創(chuàng)建了一個(gè)監(jiān)聽文件改動(dòng)的 watcher,同時(shí)實(shí)現(xiàn)了一個(gè)插件機(jī)制,將 koa-app 和 watcher 以及其他必要工具組合成一個(gè) context 對(duì)象注入到每個(gè) plugin 中。

          context組成結(jié)構(gòu):

          plugin 依次從 context 里獲取上面這些組成部分,有的 plugin 在 koa 實(shí)例添加了幾個(gè) middleware,有的借助 watcher 實(shí)現(xiàn)對(duì)文件的改動(dòng)監(jiān)聽,這種插件機(jī)制帶來的好處是整個(gè)應(yīng)用結(jié)構(gòu)清晰,同時(shí)每個(gè)插件處理不同的事情,職責(zé)分配更清晰。

          5 plugin:

          • 用戶注入的 plugins —— 自定義 plugin

          • hmrPlugin —— 處理 hmr

          • htmlRewritePlugin —— 重寫 html 內(nèi)的 script 內(nèi)容

          • moduleRewritePlugin —— 重寫模塊中的 import 導(dǎo)入

          • moduleResolvePlugin ——獲取模塊內(nèi)容

          • vuePlugin —— 處理 vue 單文件組件

          • esbuildPlugin —— 使用 esbuild 處理資源

          • assetPathPlugin —— 處理靜態(tài)資源

          • serveStaticPlugin —— 托管靜態(tài)資源

          • cssPlugin —— 處理 css/less/sass 等引用

          • ...

          一個(gè)用來攔截 json 文件 plugin簡(jiǎn)單實(shí)現(xiàn):

          interface ServerPluginContext { 
            root: string 
            app: Koa 
            server: Server 
            watcher: HMRWatcher 
            resolver: InternalResolver 
            config: ServerConfig 


          type ServerPlugin = (ctx:ServerPluginContext)=> void

          const JsonInterceptPlugin:ServerPlugin = ({app})=>
            app.use(async (ctx, next) => { 
              await next() 
              if (ctx.path.endsWith('.json') && ctx.body) { 
                ctx.type = 'js' 
                ctx.body = `export default json` 
              } 
            }) 
          }

          6 運(yùn)行依賴原理

          Vite 通過在一開始將應(yīng)用中的模塊區(qū)分為 依賴源碼 兩類,改進(jìn)了開發(fā)服務(wù)器啟動(dòng)時(shí)間。

          • 依賴 大多為純 JavaScript 并在開發(fā)時(shí)不會(huì)變動(dòng)。一些較大的依賴(例如有上百個(gè)模塊的組件庫(kù))處理的代價(jià)也很高。依賴也通常會(huì)以某些方式(例如 ESM 或者 CommonJS)被拆分到大量小模塊中。Vite 將會(huì)使用 esbuild[2]預(yù)構(gòu)建依賴[3]。Esbuild 使用 Go 編寫,并且比以 JavaScript 編寫的打包器預(yù)構(gòu)建依賴快 10-100 倍。

          • 源碼 通常包含一些并非直接是 JavaScript 的文件,需要轉(zhuǎn)換(例如 JSX,CSS 或者 Vue/Svelte 組件),時(shí)常會(huì)被編輯。同時(shí),并不是所有的源碼都需要同時(shí)被加載。(例如基于路由拆分的代碼模塊)。Vite 以 原生 ESM[4] 方式服務(wù)源碼。這實(shí)際上是讓瀏覽器接管了打包程序的部分工作:Vite 只需要在瀏覽器請(qǐng)求源碼時(shí)進(jìn)行轉(zhuǎn)換并按需提供源碼。根據(jù)情景動(dòng)態(tài)導(dǎo)入的代碼,即只在當(dāng)前屏幕上實(shí)際使用時(shí)才會(huì)被處理。

          6.1 依賴 ES module

          要了解 vite 的運(yùn)行原理,首先要知道什么是ES module,參考:JavaScript modules 模塊 - MDN[5]

          目前流覽器對(duì)其的支持如下:主流的瀏覽器(IE11除外)均已經(jīng)支持

          其最大的特點(diǎn)是在瀏覽器端使用 export import的方式導(dǎo)入和導(dǎo)出模塊,在 script 標(biāo)簽里設(shè)置 type="module" ,然后使用模塊內(nèi)容。

          6.2 示例:

           // **module sciprt**允許在瀏覽器中直接運(yùn)行原生支持模塊
          <script type="module">
            // index.js可以通過export導(dǎo)出模塊,也可以在其中繼續(xù)使用import加載其他依賴 
            // 當(dāng)遇見import依賴時(shí),會(huì)直接發(fā)起http請(qǐng)求對(duì)應(yīng)的模塊文件。
            import { fn } from  ./index.js;
            fn();
          </script>

          當(dāng) html 里嵌入上面的 script 標(biāo)簽時(shí)候,瀏覽器會(huì)發(fā)起 http 請(qǐng)求,請(qǐng)求 htttp server 托管 index.js ,在 index.js 里,我們用 export 導(dǎo)出 fn 函數(shù),在上面的 script 中能獲取到 fn 的定義。

          export function fn(
              alert('hello world');
          };

          7 在 vite 中的作用

          打開運(yùn)行中的 vite 項(xiàng)目,訪問 view-source 可以發(fā)現(xiàn) html 里有段這樣的代碼:

          <script type="module">
            import { createApp } from '/@modules/vue'
            import App from '/App.vue'
            createApp(App).mount('#app')
          </script>

          從這段代碼中,我們能 get 到以下幾點(diǎn)信息:

          • 從 http://localhost:3000/@modules/vue 中獲取 createApp 這個(gè)方法

          • 從 http://localhost:3000/App.vue 中獲取應(yīng)用入口

          • 使用 createApp 創(chuàng)建應(yīng)用并掛載節(jié)點(diǎn)

          createApp 是 vue3.X 的 api,只需知道這是創(chuàng)建了 vue 應(yīng)用即可,vite 利用 ES module,把 “構(gòu)建 vue 應(yīng)用” 這個(gè)本來需要通過 webpack 打包后才能執(zhí)行的代碼直接放在瀏覽器里執(zhí)行,這么做是為了

          1. 去掉打包步驟

          2. 實(shí)現(xiàn)按需加載

          7.1 去掉打包步驟

          打包的概念是開發(fā)者利用打包工具將應(yīng)用各個(gè)模塊集合在一起形成 bundle,以一定規(guī)則讀取模塊的代碼——以便在不支持模塊化的瀏覽器里使用。

          為了在瀏覽器里加載各模塊,打包工具會(huì)借助膠水代碼用來組裝各模塊,比如 webpack 使用 map 存放模塊 id 和路徑,使用 __webpack_require__ 方法獲取模塊導(dǎo)出。

          vite 利用瀏覽器原生支持模塊化導(dǎo)入這一特性,省略了對(duì)模塊的組裝,也就不需要生成 bundle,所以打包這一步就可以省略了。

          7.2 實(shí)現(xiàn)按需打包

          前面說到,webpack 之類的打包工具會(huì)將各模塊提前打包進(jìn) bundle 里,但打包的過程是靜態(tài)的——不管某個(gè)模塊的代碼是否執(zhí)行到,這個(gè)模塊都要打包到 bundle 里,這樣的壞處就是隨著項(xiàng)目越來越大打包后的 bundle 也越來越大。

          開發(fā)者為了減少 bundle 大小,會(huì)使用動(dòng)態(tài)引入 import() 的方式異步的加載模塊( 被引入模塊依然需要提前打包),又或者使用 tree shaking 等方式盡力的去掉未引用的模塊,然而這些方式都不如 vite 的優(yōu)雅,vite 可以只在需要某個(gè)模塊的時(shí)候動(dòng)態(tài)(借助 import() )的引入它,而不需要提前打包。

          8 vite 如何處理 ESM

          既然 vite 使用 ESM 在瀏覽器里使用模塊,那么這一步究竟是怎么做的?

          上文提到過,在瀏覽器里使用 ES module 是使用 http 請(qǐng)求拿到模塊,所以 vite 必須提供一個(gè) web server 去代理這些模塊,上文中提到的 koa 就是負(fù)責(zé)這個(gè)事情,vite 通過對(duì)請(qǐng)求路徑的劫持獲取資源的內(nèi)容返回給瀏覽器,不過 vite 對(duì)于模塊導(dǎo)入做了特殊處理。

          8.1 @modules 是什么?

          通過工程下的 index.html 和開發(fā)環(huán)境下的 html 源文件對(duì)比,發(fā)現(xiàn) script 標(biāo)簽里的內(nèi)容發(fā)生了改變,由

          <script type="module">
            import { createApp } from 'vue'
            import App from '/App.vue'
            createApp(App).mount('#app')
          </script>

          變成了

          <script type="module">
            import { createApp } from '/@modules/vue'
            import App from '/App.vue'
            createApp(App).mount('#app')
          </script>
          1. 在 koa 中間件里獲取請(qǐng)求 body

          2. 通過 es-module-lexer 解析資源 ast 拿到 import 的內(nèi)容

          3. 判斷 import 的資源是否是絕對(duì)路徑,絕對(duì)視為 npm 模塊

          4. 返回處理后的資源路徑:"vue" => "/@modules/vue"

          這部分代碼在 serverPluginModuleRewrite 這個(gè) plugin 中,

          8.2 為什么需要 @modules?

          如果我們?cè)谀K里寫下以下代碼的時(shí)候,瀏覽器中的 esm 是不可能獲取到導(dǎo)入的模塊內(nèi)容的:

          import vue from 'vue'

          因?yàn)?vue 這個(gè)模塊安裝在 node_modules 里,以往使用 webpack,webpack遇到上面的代碼,會(huì)幫我們做以下幾件事:

          • 獲取這段代碼的內(nèi)容

          • 解析成 AST

          • 遍歷 AST 拿到 import 語(yǔ)句中的包的名稱

          • 使用 enhanced-resolve 拿到包的實(shí)際地址進(jìn)行打包,

          但是瀏覽器中 ESM 無(wú)法直接訪問項(xiàng)目下的 node_modules,所以 vite 對(duì)所有 import 都做了處理,用帶有 @modules 的前綴重寫它們。

          從另外一個(gè)角度來看這是非常比較巧妙的做法,把文件路徑的 rewrite 都寫在同一個(gè) plugin 里,這樣后續(xù)如果加入更多邏輯,改動(dòng)起來不會(huì)影響其他 plugin,其他 plugin 拿到資源路徑都是 @modules,比如說后續(xù)可能加入 alias 的配置:就像 webpack alias 一樣:可以將項(xiàng)目里的本地文件配置成絕對(duì)路徑的引用。

          8.3 怎么返回模塊內(nèi)容

          在下一個(gè) koa middleware 中,用正則匹配到路徑上帶有 @modules 的資源,再通過 require('xxx') 拿到 包的導(dǎo)出返回給瀏覽器。

          以往使用 webpack 之類的打包工具,它們除了將模塊組裝到一起形成 bundle,還可以讓使用了不同模塊規(guī)范的包互相引用,比如:

          • ES module (esm) 導(dǎo)入 cjs

          • CommonJS (cjs) 導(dǎo)入 esm

          • dynamic import 導(dǎo)入 esm

          • dynamic import 導(dǎo)入 cjs

          關(guān)于 es module 的坑可以看這篇文章(https://zhuanlan.zhihu.com/p/40733281[6])。

          起初在 vite 還只是為 vue3.x 設(shè)計(jì)的時(shí)候,對(duì) vue esm 包是經(jīng)過特殊處理的,比如:需要 @vue/runtime-dom 這個(gè)包的內(nèi)容,不能直接通過 require('@vue/runtime-dom')得到,而需要通過 require('@vue/runtime-dom/dist/runtime-dom.esm-bundler.js') 的方式,這樣可以使得 vite 拿到符合 esm 模塊標(biāo)準(zhǔn)的 vue 包。

          目前社區(qū)中大部分模塊都沒有設(shè)置默認(rèn)導(dǎo)出 esm,而是導(dǎo)出了 cjs 的包,既然 vue3.0 需要額外處理才能拿到 esm 的包內(nèi)容,那么其他日常使用的 npm 包是不是也同樣需要支持?答案是肯定的,目前在 vite 項(xiàng)目里直接使用 lodash 還是會(huì)報(bào)錯(cuò)的。

          不過 vite 在最近的更新中,加入了optimize命令,這個(gè)命令專門為解決模塊引用的坑而開發(fā),例如我們要在 vite 中使用 lodash,只需要在vite.config.js(vite 配置文件)中,配置optimizeDeps對(duì)象,在include數(shù)組中添加 lodash。

          // vite.config.js
          module.exports = {
            optimizeDeps: {
              include: ["lodash"]
            }
          }

          這樣 vite 在執(zhí)行 runOptimize 的時(shí)候中會(huì)使用 rollup 對(duì) lodash 包重新編譯,將編譯成符合 esm 模塊規(guī)范的新的包放入 node_modules 下的 .vite_opt_cache 中,然后配合 resolver 對(duì) lodash 的導(dǎo)入進(jìn)行處理:使用編譯后的包內(nèi)容代替原來 lodash 的包的內(nèi)容,這樣就解決了 vite 中不能使用 cjs 包的問題,這部分代碼在 depOptimizer.ts 里。

          不過這里還有個(gè)問題,由于在 depOptimizer.ts 中,vite 只會(huì)處理在項(xiàng)目下 package.json 里的 dependencies 里聲明好的包進(jìn)行處理,所以無(wú)法在項(xiàng)目里使用

          import pick from 'lodash/pick'

          的方式單使用 pick 方法,而要使用

          import lodash from 'lodash'

          lodash.pick()

          的方式,這可能在生產(chǎn)環(huán)境下使用某些包的時(shí)候?qū)?bundle 的體積有影響。

          返回模塊的內(nèi)容的代碼在:serverPluginModuleResolve.ts 這個(gè) plugin 中。

          9 vite 熱更新的實(shí)現(xiàn)

          vite/hmr 是 vite 處理熱更新的關(guān)鍵,在 serverPluginHmr plugin 中,對(duì)于 path 等于 vite/hmr 做了一次判斷:

          app.use(async (ctx, next) => {
            if (ctx.path === '/vite/hmr') {
                ctx.type = 'js'
                ctx.status = 200
                ctx.body = hmrClient
            }
           }

          hmrClient 是 vite 熱更新的客戶端代碼,需要在瀏覽器里執(zhí)行,這里先來說說通用的熱更新實(shí)現(xiàn),熱更新一般需要四個(gè)部分:

          • 首先需要 web 框架支持模塊的 rerender/reload

          • 通過 watcher 監(jiān)聽文件改動(dòng)

          • 通過 server 端編譯資源,并推送新模塊內(nèi)容給 client 。

          • client 收到新的模塊內(nèi)容,執(zhí)行 rerender/reload

          vite 也不例外同樣有這四個(gè)部分,其中客戶端代碼在:client.ts 里,服務(wù)端代碼在 serverPluginHmr 里,對(duì)于 vue 組件的更新,通過 vue3.x 中的 HMRRuntime 處理的。

          10 尤雨溪發(fā)言

          Vite,一個(gè)基于瀏覽器原生 ES imports 的開發(fā)服務(wù)器。利用瀏覽器去解析 imports,在服務(wù)器端按需編譯返回,完全跳過了打包這個(gè)概念,服務(wù)器隨起隨用。同時(shí)不僅有 Vue 文件支持,還搞定了熱更新,而且熱更新的速度不會(huì)隨著模塊增多而變慢。針對(duì)生產(chǎn)環(huán)境則可以把同一份代碼用 rollup 打。雖然現(xiàn)在還比較粗糙,但這個(gè)方向我覺得是有潛力的,做得好可以徹底解決改一行代碼等半天熱更新的問題。

          參考資料

          [1]

          koa: https://www.npmjs.com/package/koa

          [2]

          esbuild: https://esbuild.github.io/

          [3]

          預(yù)構(gòu)建依賴: https://cn.vitejs.dev/guide/dep-pre-bundling.html

          [4]

          原生 ESM: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules

          [5]

          JavaScript modules 模塊 - MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules

          [6]

          https://zhuanlan.zhihu.com/p/40733281: https://zhuanlan.zhihu.com/p/40733281


          瀏覽 91
          點(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>
                  www一级片 | 天天操天天操天天操天天 | 青青草原亚洲 | 国产吧在线视频 | 无码少妇喷水 |