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

          漸進(jìn)式 Unbundled 開(kāi)發(fā)工具探索之路

          共 14178字,需瀏覽 29分鐘

           ·

          2021-06-05 16:57

          簡(jiǎn)短摘要:得益于現(xiàn)代瀏覽器內(nèi)置了模塊處理系統(tǒng)(ESM), 業(yè)界新興開(kāi)發(fā)工具 Snowpack、WMR、Vite 等直接將模塊解析加載過(guò)程直接交給瀏覽器, Dev Server 能夠秒級(jí)啟動(dòng)。和傳統(tǒng)開(kāi)發(fā)工具編譯時(shí)解析加載模塊最終打包到 JS Bundle 中相比,本地開(kāi)發(fā)體驗(yàn)提升明顯

          傳統(tǒng) Bundled Development

          復(fù)雜項(xiàng)目構(gòu)建太慢

          業(yè)界主流的開(kāi)發(fā)工具還是以 Webpack 為主,隨著項(xiàng)目體積增大,開(kāi)發(fā)階段一次性將源代碼和第三方依賴編譯處理打包到一起的耗時(shí)會(huì)顯著增加。在我們團(tuán)隊(duì)內(nèi)部的 monorepo 倉(cāng)庫(kù)中,應(yīng)用項(xiàng)目開(kāi)發(fā)時(shí),為了開(kāi)發(fā)階段調(diào)試方便,通常也會(huì)對(duì)一些公共庫(kù)源碼一起打包編譯,成千上萬(wàn)個(gè)模塊導(dǎo)致首次 dev server 啟動(dòng)耗時(shí)在幾分鐘甚至十幾分鐘,嚴(yán)重影響了開(kāi)發(fā)效率與體驗(yàn)。

          Webpack 打包慢的問(wèn)題相信也是大家都會(huì)遇到的問(wèn)題。隨手一搜,各種 Webpack 配置優(yōu)化以及最佳實(shí)踐之類的文檔數(shù)不勝數(shù)。大部分也都是遵循  Webpack[1] Build Performance Guide[2]。

          粗略一看上面的一些優(yōu)化方式很多, 但是在我們的場(chǎng)景中很多都是不夠通用,比如 thread-loader 結(jié)合 babel-loader 的方式在業(yè)務(wù)項(xiàng)目中經(jīng)常會(huì)遇到報(bào)錯(cuò)的情況,原因是業(yè)務(wù)項(xiàng)目通常會(huì)使用 babel-plugin-import 針對(duì)業(yè)務(wù)內(nèi)部的組件庫(kù)按需加載組件以及樣式,配置大致如下:

          [
            "babel-plugin-import",
              {
                "libraryName""custom-ui-components",
                "style": (name: string, file: Object) => {
                  return `${name}/style/2x`;
                }
              }
          ]

          我們知道 JavaScript 中線程間共享的數(shù)據(jù)必須序列化,上述 style 函數(shù)在序列化時(shí)會(huì)直接報(bào)錯(cuò)。

          esbuild 作為一個(gè) bundle 工具性能很不錯(cuò),但是針對(duì)應(yīng)用生產(chǎn)環(huán)境打包還存在一些問(wèn)題,如降級(jí)到 ES5,Code Split 、 CSS 處理等。社區(qū)內(nèi)有提供替換 babel-loader 的 esbuild-loader, 通過(guò) loader 的方式在 Webpack JS 運(yùn)行時(shí)中編譯單個(gè)文件的方式在速度上也不如單純用 esbuild 一把梭快。對(duì)瀏覽器兼容性有要求的項(xiàng)目,平滑使用也比較困難。

          減少處理的模塊數(shù)量方面,針對(duì) MPA,簡(jiǎn)單的做法是在 dev 時(shí)顯式指定需要開(kāi)發(fā)的頁(yè)面,但是比較局限不夠靈活, Webpack 5 提供的實(shí)驗(yàn)性特性 Lazy Compilation 在開(kāi)發(fā)階段能夠做到真正的按需編譯提升 dev server 啟動(dòng)速度:

          首次 dev server 啟動(dòng)時(shí), 會(huì)代理 Webpack 入口以及 dynamic import 導(dǎo)出的模塊,打開(kāi)瀏覽器頁(yè)面后,代理模塊在運(yùn)行時(shí)通過(guò) Server-Send-Events 與 Lazy Compilation Backend Server 通信決定需要真正編譯處理的資源。但是作為實(shí)驗(yàn)功能,目前還不是很穩(wěn)定,筆者在使用時(shí),遇到過(guò)初次打開(kāi)頁(yè)面白屏必須手動(dòng)刷新的問(wèn)題。而且后續(xù)對(duì)需要編譯的入口通過(guò) babel-loader 或者 ts-loader 重新編譯打包時(shí),仍然會(huì)有慢的問(wèn)題存在。

          Webpack 4 中通過(guò)插件 lazy-compile-webpack-plugin[3] 也可以實(shí)現(xiàn)類似的效果。

          更多的優(yōu)化方式這里不一一列舉, 總結(jié)下來(lái),現(xiàn)有的一些 Webpack 打包優(yōu)化方式或多或少都不夠通用,或者存在一些問(wèn)題。

          打包工具之上的探索

          沿著減少 Webpack 處理模塊數(shù)量的思路,node_modules 下的第三方依賴如果能夠從打包環(huán)節(jié)剔除,僅對(duì)業(yè)務(wù)代碼打包,對(duì)構(gòu)建速度肯定會(huì)有明顯的提升。

          比較常見(jiàn)的方法是將常用的第三方庫(kù)在 Webpack 構(gòu)建時(shí)配置 external, Html 中直接通過(guò) script 標(biāo)簽引入 UMD 產(chǎn)物, 這種方式有以下問(wèn)題:

          1. 每個(gè)依賴的 UMD 產(chǎn)物會(huì)增加額外的一段兼容代碼,造成冗余。
          2. 瀏覽器中直接使用可能會(huì)污染全局變量,以及容易被修改覆蓋。
          3. 很多公司內(nèi)部包,并沒(méi)有提供 UMD 產(chǎn)物。

          既然 UMD 產(chǎn)物不太符合我們的場(chǎng)景,那么直接替換依賴為對(duì)應(yīng)的 ESM 產(chǎn)物呢?業(yè)界也有類似的思路:

          1. Rollup 社區(qū)有 rollup-plugin-cdn[4] 支持代碼中從 unpkg 引入依賴的 ESM 產(chǎn)物:
          import hyper from 'https://unpkg.com/hyperhtml@latest/esm/index.js';
          hyper(document.body)`
            <h1>Hello ESM</h1>`;
          1. Pika(Skypack 前身)提供了 @pika/cdn-webpack-plugin[5] 支持生產(chǎn)環(huán)境構(gòu)建時(shí)將 package.json 中的依賴替換為 Pika CDN 上對(duì)應(yīng)的鏈接,同時(shí) html 中通過(guò) script type=module 加載打包后的 js 產(chǎn)物, 以 React 為例在最終 JS Chunk 中大致如下:
           import __mun2tz2a_default, * as __mun2tz2a_all from "https://cdn.skypack.dev/react";
              window["https://cdn.skypack.dev/react"] = Object.assign((__mun2tz2a_default || {}), __mun2tz2a_all);
          (window["webpackJsonptask_activity"] = window["webpackJsonptask_activity"] || []).push([["vendors"],{
           /***/ "50ab":
          /*!****************************************!*\
            !*** ./node_modules/__pika__/react.js ***!
            \****************************************/
          /*! no static exports found */
          /***/ (function(module, exports) {
          module.exports = window["https://cdn.skypack.dev/react"];
          /***/ }),
          }])

          可以看到利用瀏覽器對(duì) ESM 的原生支持,直接從 Skypack CDN import 導(dǎo)入 React ,之后以全局變量的方式掛到 window 變量上。其他模塊中導(dǎo)入 React ,最終會(huì)是 window 上存在的變量。

          考慮到我們主要是想提升 dev server 啟動(dòng)時(shí)構(gòu)建的速度,并且很多依賴都是公司內(nèi)部包,筆者在 @pika/cdn-webpack-plugin[6] 的基礎(chǔ)上,結(jié)合團(tuán)隊(duì)內(nèi)部的 CJS 轉(zhuǎn) ESM 服務(wù),在 dev 環(huán)節(jié)將用到的第三方依賴改成從線上 import 導(dǎo)入。最終在實(shí)際業(yè)務(wù)項(xiàng)目中測(cè)試效果并不理想,主要有以下一些問(wèn)題:

          1. 第三方依賴以及依賴的依賴都會(huì)直接在瀏覽器中請(qǐng)求,請(qǐng)求數(shù)量成指數(shù)增長(zhǎng),瀏覽器中白屏?xí)r間過(guò)長(zhǎng),甚至請(qǐng)求數(shù)量會(huì)直接爆炸,運(yùn)行報(bào)錯(cuò)。
          2. 速度上面,一些項(xiàng)目確實(shí)能夠在 6s 左右 dev 完成,但是在一些比較大的內(nèi)部 monorepo 中,仍然是需要幾十秒的時(shí)間。
          3. CJS 轉(zhuǎn) ESM 中間的坑太多: 大部分依賴都還是只提供了 CJS 版本,在轉(zhuǎn)換過(guò)程中不論是用 rollup-plugin-commonjs 還是 esbuild ,一些問(wèn)題都避免不了。
          4. 同樣會(huì)污染全局變量。

          在 Webpack 生態(tài)基礎(chǔ)上將第三方依賴以 ESM 形式直接加載看起來(lái)也不太能滿足我們的場(chǎng)景,那么如何更好地提升 dev server 啟動(dòng)速度呢?

          邁入 Unbundled Development

          業(yè)界現(xiàn)狀

          基于 ESM 的現(xiàn)代 unbundled 開(kāi)發(fā)工具在社區(qū)如雨后春筍不斷發(fā)展,es-dev-server、Snowpack 、Wmr 、Vite 等工具直接直接拋棄 Webpack,通過(guò)內(nèi)部的 Dev Server 接收請(qǐng)求后實(shí)時(shí)對(duì) JS、TS、CSS 等資源實(shí)時(shí)編譯處理。

          圖片來(lái)自 Snowpack 官網(wǎng): https://www.snowpack.dev/concepts/how-snowpack-works

          和 Webpack 等打包工具相比,這類的 unbundled 開(kāi)發(fā)工具有如下優(yōu)點(diǎn):

          1. 飛快的啟動(dòng)速度:dev server 啟動(dòng)時(shí)僅需要對(duì)依賴預(yù)處理編譯成 ESM 格式,一次處理之后,后續(xù)依賴沒(méi)有變化時(shí),可以做到秒級(jí)啟動(dòng),
          2. 實(shí)時(shí)按需編譯:運(yùn)行時(shí)瀏覽器第一次請(qǐng)求對(duì)應(yīng)模塊時(shí),dev server 本地實(shí)時(shí)編譯處理返回對(duì)應(yīng)內(nèi)容。
          3. 更快的熱更新:針對(duì)具體修改的文件,根據(jù)模塊的依賴關(guān)系圖, 逐步向上尋找 accept 該模塊 HMR 更新的文件,重新請(qǐng)求文件內(nèi)容。和 Webpack 的熱更新需要重新整體構(gòu)建相比會(huì)更快。

          這種新興 Unbundled Development 模式看著能夠滿足我們對(duì) dev server 啟動(dòng)速度的需求,下面的問(wèn)題就是針對(duì)使用我們內(nèi)部應(yīng)用開(kāi)發(fā)工具的業(yè)務(wù)項(xiàng)目來(lái)說(shuō)如何平滑的接入這些工具。

          由于我們的應(yīng)用開(kāi)發(fā)工具提供了一套現(xiàn)代 Web 項(xiàng)目開(kāi)發(fā)范式,從應(yīng)用入口和各種資源的處理使用,以及服務(wù)端 API 的一體化調(diào)用上都有內(nèi)部一些標(biāo)準(zhǔn)。直接在項(xiàng)目中使用上述工具會(huì)有很多問(wèn)題,免不了對(duì)上面的工具進(jìn)行二次開(kāi)發(fā),這就要求這些工具提供足夠靈活的擴(kuò)展自定義方式,這里就業(yè)界用的比較多的 Snowpack 和 Vite 2.0 簡(jiǎn)單對(duì)比如下:

          (點(diǎn)擊查看大圖)

          通過(guò)上面的簡(jiǎn)單對(duì)比,以及文檔豐富程度上來(lái)看, Vite 2.0 擴(kuò)展起來(lái)會(huì)更加靈活方便。因此在給我們的應(yīng)用開(kāi)發(fā)工具提供 Unbundled Development 模式前期,也是果斷選擇了 Vite 2.0 作為底層, 但是最終實(shí)現(xiàn)版本里面我們選擇借鑒 Vite、Wmr 等工具自主開(kāi)發(fā)實(shí)現(xiàn) dev server 部分,主要有以下幾個(gè)角度的考慮:

          1. 依賴預(yù)處理過(guò)程需要深度定制:一些公司內(nèi)部包需要特殊處理,這部分我們最終決定放到內(nèi)部的 CJS 轉(zhuǎn) ESM 服務(wù)上,同一個(gè) package 的具體版本,只會(huì)處理一次,后續(xù)使用時(shí)直接下載轉(zhuǎn)換后的產(chǎn)物即可。
          2. 深度定制需求:在我們的設(shè)計(jì)體系里面,用戶并不會(huì)直接接觸到具體底層工具的配置,比如 babel.config.js、postcss.config.js 等,而是集中在我們提供的配置文件進(jìn)行自定義的需求。Vite 2.0 本身是支持這些文件的加載,另一方面具體資源文件處理的標(biāo)準(zhǔn)也有不同的地方,比如 CSS Modules 處理上,我們默認(rèn)支持文件后綴 .module.css 的形式,也支持關(guān)閉文件后綴的約定。這些有的可以通過(guò)配置實(shí)現(xiàn),但是大部分還是要接管 Vite 內(nèi)部的資源的編譯處理過(guò)程,如果通過(guò) Vite 提供的插件可以完成,這樣對(duì) Vite 利用不是很充分,也顯得比較冗余。
          3. 后續(xù)維護(hù)與發(fā)展:初期我們打算提供基本的 dev server 解決開(kāi)發(fā)環(huán)節(jié)慢的問(wèn)題,后期需要結(jié)合已有 SSR、SSG 等建設(shè)補(bǔ)齊對(duì)應(yīng)的能力。

          綜上,實(shí)現(xiàn) Unbundled Dev Server 對(duì)我們來(lái)說(shuō)更容易維護(hù)以及后續(xù)添加一些能力更方便。

          Unbundled Dev Server 原理

          為了更好的理解 Unbudled 開(kāi)發(fā)工具的細(xì)節(jié),我們從下面幾個(gè)方面分別介紹:

          依賴預(yù)處理

          上文我們提到,業(yè)務(wù)項(xiàng)目中使用到的依賴很多只提供了 CJS 產(chǎn)物, 首先,我們需要將第三方依賴轉(zhuǎn)換成 ESM 格式。業(yè)界常用的工具主要是 Rollup 和 esbuild 等構(gòu)建工具。

          基本思路是分析項(xiàng)目源碼中使用到的依賴, 這些依賴作為構(gòu)建工具的入口整體打包,好處是整體將依賴打包得到 common chunks,瀏覽器中打開(kāi)頁(yè)面加載第三方依賴的請(qǐng)求數(shù)量會(huì)少很多。缺點(diǎn)是添加依賴或者刪除一些依賴改動(dòng)了 package.json 或者 lock 文件時(shí), 需要重新對(duì)依賴編譯打包,在一些比較大的中后臺(tái)項(xiàng)目中,依賴預(yù)處理耗時(shí)還是存在的。

          這里我們目前采用的方案是,沿用之前內(nèi)部已有的 CJS 轉(zhuǎn) ESM 服務(wù),直接下載線上依賴已經(jīng)轉(zhuǎn)換好的的 ESM 產(chǎn)物。后續(xù)針對(duì)下載的 ESM 文件,用 esbuild 做一次 bundle 減少瀏覽器中運(yùn)行的請(qǐng)求數(shù)量。

          借助 CJS 轉(zhuǎn)ESM 服務(wù)和直接本地編譯轉(zhuǎn)換相比,有以下好處:

          1. 首次對(duì)項(xiàng)目依賴預(yù)處理之后,后續(xù)添加依賴只需要從云端下載新添加的依賴 ESM 產(chǎn)物,之后借助 esbuild 重新 bundle 耗時(shí)也不會(huì)很多。
          2. 一些需要特殊處理的第三方依賴,統(tǒng)一在云端處理修復(fù),業(yè)務(wù)項(xiàng)目不依賴開(kāi)發(fā)工具的發(fā)版升級(jí)。針對(duì)一些通用的處理場(chǎng)景,我們會(huì)提供 UI 界面引導(dǎo)用戶輸入依賴對(duì)應(yīng)信息,自動(dòng)修復(fù)。
          3. CJS 到 ESM 轉(zhuǎn)換的產(chǎn)物可以在本地全局緩存,跨項(xiàng)目復(fù)用已經(jīng)編譯好的產(chǎn)物, 也就是說(shuō),隨著使用項(xiàng)目增加,云端和本地雙重緩存級(jí)聯(lián),能夠大幅度減少 CJS 轉(zhuǎn) ESM 的時(shí)間。

          同時(shí)我們針對(duì)內(nèi)部模塊比較多的依賴,如 antd,在線上 CJS 轉(zhuǎn) ESM 時(shí),會(huì)將內(nèi)部模塊打包到單個(gè)產(chǎn)物中,這樣能減少成百上千的網(wǎng)絡(luò)請(qǐng)求。

          以 React 為例, 直接從云端下載的 ESM 產(chǎn)物內(nèi)容截圖如下:

          可以看到依賴項(xiàng) object-assign 會(huì)額外帶有版本號(hào)信息。在遞歸下載第三方依賴 ESM 文件后,能夠得到如下的 json 文件,存儲(chǔ)某個(gè)版本依賴實(shí)際 ESM 文件的路徑, 如下:

          {
             "react?16.14.0""/Library/Caches/__web_modules__/[email protected]",
             "react-dom?16.14.0""/Library/Caches/__web_modules__/[email protected]",
             "object-assign?4.1.1""/Library/Caches/__web_modules__/[email protected]",
          }

          版本號(hào)是直接在 node_modules 中解析對(duì)應(yīng)依賴位置獲取的,也就是說(shuō),項(xiàng)目下存在某個(gè)依賴多個(gè)版本也是支持的。

          之后通過(guò) esbuild 打包時(shí)借助 onResolve hook 從上面的 json 文件中匹配具體的 ESM 文件路徑, 示例代碼如下:

          const bundleResult = await require('esbuild').build({
              entryPoints: ['react''react-dom'],
              bundle: true,
              splitting: true,
              chunkNames: 'chunks/[name]-[hash]',
              metafile: true,
              outdir: webModulesDir,
              format: 'esm',
              treeShaking: 'ignore-annotations',
              plugins: [
                {
                   name: 'resolve-deps-plugin',
                   setup(build) {
                      build.onResolve({filter: /^/}, async args => {
                         const { kind, path } = args;
                         if (['import-statement''entry-point''dynamic-import'].includes(kind)) {
                           if (kind === 'entrypoint') {
                             //  針對(duì) bundle 入口, 直接本地 resolve 得到版本號(hào)
                             // 在上面的 json 文件中拿到具體路徑返回
                           } else if (path.startsWith('/esm/bv')) {
                             // 針對(duì)依賴的依賴,比如 object-assign^4.1.1
                             // 通過(guò) semver 在文件中匹配獲取實(shí)際的 ESM 文件路徑返回
                           }
                         }
                      })
                   }
                }
              ]
          })

          到這里,依賴預(yù)處理已經(jīng)完成了,最終通過(guò) bundleResult.metafile 能夠得到我們最終的 import-map.json 后續(xù)在請(qǐng)求模塊時(shí)用來(lái)解析模塊依賴路徑到轉(zhuǎn)換后的 esm 文件, 目錄結(jié)構(gòu)和內(nèi)容大致如下:

          針對(duì) monorepo 中某些 package 并不發(fā)布,在應(yīng)用中直接使用源碼統(tǒng)一構(gòu)建的場(chǎng)景, 在分析使用到的依賴時(shí),也會(huì)收集這些 package 用到的依賴,統(tǒng)一預(yù)處理成 ESM 格式。某些 package build 后提供產(chǎn)物在應(yīng)用中使用時(shí),會(huì)根據(jù)當(dāng)前 package 最新代碼, 本地實(shí)時(shí)編譯轉(zhuǎn)換成 ESM,這里本地編譯轉(zhuǎn)換和云端會(huì)復(fù)用底層代碼,效果上也類似。

          資源文件處理

          整體 Server 的實(shí)現(xiàn)部分借鑒了 WMR 和 Vite 的 插件系統(tǒng)設(shè)計(jì), Plugin 作為 Rollup Plugin 的超集,通過(guò) Plugin Container 提供統(tǒng)一的插件接口, 和 Snowpack、es-dev-server 在 Server 中間件中處理請(qǐng)求、文件轉(zhuǎn)換不同,WMR 、Vite 的插件體系將文件轉(zhuǎn)換以及 Server 中間件分離開(kāi)來(lái),概念上比較清晰, 也比較易于維護(hù)。WMR、Vite 這種插件系統(tǒng)也有利于 dev 和 build 時(shí)復(fù)用文件轉(zhuǎn)換相關(guān)的邏輯。

          從瀏覽器發(fā)出請(qǐng)求到 Server 返回對(duì)應(yīng)資源的流程如下圖所示:

          在 resolveId hook 中根據(jù) url 解析出具體文件路徑。load hook 主要加載文件內(nèi)容。transform hook 是編譯轉(zhuǎn)換各種類型資源文件的核心。下面對(duì)幾種資源文件在 Server 內(nèi)部的處理展開(kāi)描述:

          JSX/TSX 編譯轉(zhuǎn)換

          我們知道 JSX或 TSX 不能直接在瀏覽器中運(yùn)行,這里因?yàn)?dev 環(huán)節(jié)對(duì)瀏覽器兼容性沒(méi)有要求以及追求更快的實(shí)時(shí)編譯速度,直接使用 esbuild transform 即可:

          const result = await esbuild.transform(code, {
            loader: 'tsx',
            sourcefile: importer,
            sourcemap: true,
            target: 'chrome63'
          })

          在業(yè)務(wù)項(xiàng)目中實(shí)踐時(shí),遇到的一些問(wèn)題舉例如下:

          1. esbuild 不支持 React 17 jsx transformer,inject 配置選項(xiàng) transform api 也沒(méi)有提供,當(dāng)業(yè)務(wù)項(xiàng)目使用 React 版本為 17 時(shí),并且沒(méi)有顯示導(dǎo)入 React 時(shí), 我們?cè)?esbuild transform 的結(jié)果上自動(dòng)注入 import React from 'react'。
          2. 業(yè)務(wù) TS 項(xiàng)目中有很多使用了 const enum 導(dǎo)致 esbuild 無(wú)法處理,只能切換到 typescript api 處理。
          3. 老項(xiàng)目業(yè)務(wù)代碼中依賴 babel 生態(tài)的插件, 比如 babel-plugin-macros[7], 這就導(dǎo)致我們額外需要提供 babel 編譯的流程。

          基礎(chǔ)的語(yǔ)法轉(zhuǎn)換完成后,接下來(lái)就是 Bare Import 的處理問(wèn)題,我們的業(yè)務(wù)代碼中直接通過(guò)包名導(dǎo)入依賴的方式經(jīng)過(guò)打包工具處理能夠正常運(yùn)行, 如下:

          import React from 'react'

          但是在瀏覽器中直接運(yùn)行會(huì)直接報(bào)錯(cuò), import-maps[8] 提案可以解決這個(gè)問(wèn)題,但是只有最新版本 Chrome 支持。這里我們采用的方案和業(yè)界的做法一致,編譯完成后改寫(xiě) import 語(yǔ)句, 以 React 為例, 最終返回的內(nèi)容如下:

          import React from "/node_modules/.web_modules/react.js";

          node_modules/.web_modules 目錄就是我們?cè)谝蕾囶A(yù)處理時(shí)生成的第三方依賴 ESM 文件目錄。

          CSS、JSON、圖片等資源處理

          瀏覽器中直接 import 導(dǎo)入資源,要求返回的類型是 application/javascript,因此這些文件在對(duì)應(yīng)的插件中最終都會(huì)被處理成 JavaScript。

          CSS 默認(rèn)會(huì)用 PostCSS 處理之后直接創(chuàng)建 style 標(biāo)簽插入 head 節(jié)點(diǎn)就能生效,以 import ./App.css 為例,返回的內(nèi)容大致如下:

          // src/App.css
          const code = "body {\n  margin: 0;\n}\n"
          const filename = "/Users/songzhenwei/Documents/test/full-demo/src/App.css"
          const styleEl = document.createElement('style');
          const codeEl = document.createTextNode(code);
          styleEl.type = 'text/css';
          styleEl.appendChild(codeEl);
          document.head.appendChild(styleEl);

          這里只是簡(jiǎn)單展示返回文件的內(nèi)容,真正可用還需要考慮熱更新時(shí) Style 標(biāo)簽的刪除、CSS 文件中通過(guò) [url function](<https://developer.mozilla.org/en-US/docs/Web/CSS/url( "url function")>) 加載字體圖片等場(chǎng)景。

          JSON 文件比較簡(jiǎn)單,這里我們只需要讀取文件 default 導(dǎo)出即可:

          export default { "name""example" }

          業(yè)務(wù)代碼中使用圖片等資源時(shí), 我們會(huì)在 import 語(yǔ)句后面添加 ?assets query 表示為資源請(qǐng)求需要編譯處理, 如下:

          import logo from './logo.png';
          // => rewrite to 
          import logo from '/src/logo.png?assets'

          /src/logo.png?assets 直接返回對(duì)應(yīng)資源的實(shí)際 url 即可:

          export default '/src/logo.png'

          對(duì)于圖片資源,實(shí)際業(yè)務(wù)場(chǎng)景中我們還需要支持根據(jù)文件尺寸決定是否通過(guò) base64 編碼內(nèi)聯(lián)、svgr 等特性。

          到這里通過(guò)不同的插件完成了一些文件類型的編譯轉(zhuǎn)換,頁(yè)面已經(jīng)可以在瀏覽器中正確渲染。

          熱更新功能

          在 Webpack 等打包工具里面,熱更新相關(guān)代碼通常寫(xiě)在入口文件內(nèi)如下:

          // src/index.jsx
          import App from './App';

          module.hot.accept('./App', () => {
             renderApp();
          })

          App 根組件中引用到的文件修改時(shí),會(huì)觸發(fā)入口文件中注冊(cè)的 accept 回調(diào)函數(shù)重新渲染 App 組件。

          ESM 場(chǎng)景下的 HMR API, 業(yè)界也有一些規(guī)范:Snowpack 聯(lián)合 Vue、Preact 提出了 ESM-HMR Spec[9],類似 module.hot, unbundled 開(kāi)發(fā)工具需要提供 import.meta.hot 對(duì)象,比如常用的 accept 函數(shù)使用如下:

          import.meta.hot.accept();

          import.meta.hot.accept(['./dep1''./dep2'], ()=> {})

          這部分實(shí)現(xiàn)參考了 Snowpack 和 Vite , 文件更新時(shí),通過(guò)內(nèi)部建立好的依賴關(guān)系,上溯至 accept 該文件或自身的文件節(jié)點(diǎn),重新在瀏覽器請(qǐng)求該文件, 如下圖:

          修改 dep-b.js 向上遍歷依賴樹(shù)時(shí),找到 accept 的文件節(jié)點(diǎn) App.tsx , 同時(shí)會(huì)依賴路徑上文件節(jié)點(diǎn)的編譯緩存失效,之后通過(guò) HMR client api 重新請(qǐng)求 App.tsx ,為了保證返回的內(nèi)容是最新的,重新請(qǐng)求時(shí)會(huì)加上時(shí)間戳。

          和 Webpack 等打包工具熱更新相比,Unbundled Development 開(kāi)發(fā)工具熱更新只會(huì)重新編譯加載依賴路徑上的文件, 因此速度也會(huì)更快。同時(shí)也能結(jié)合 React Fast Refresh 做組件級(jí)別的熱更新。

          總結(jié)

          上面通過(guò)一些小點(diǎn),介紹了我們內(nèi)部解決 Webpack 打包慢的一些探索以及最終實(shí)現(xiàn) Unbundled Development 模式的一些做法, 最終實(shí)現(xiàn)的版本和我們應(yīng)用開(kāi)發(fā)的標(biāo)準(zhǔn)范式一一對(duì)齊。在業(yè)務(wù)項(xiàng)目中使用 Unbundled Development 模式后開(kāi)發(fā)體驗(yàn)有很大提升:

          生產(chǎn)環(huán)境現(xiàn)階段還是通過(guò) Webpack 打包出 JS Bundle ,在一些基礎(chǔ)編譯能力以及使用方式上盡最大努力抹平 Unbundled Development 模式和生產(chǎn)環(huán)境 Webpack 打包的差異。

          通過(guò)實(shí)際業(yè)務(wù)項(xiàng)目接入 Unbundled Development 模式實(shí)踐,我們這邊也針對(duì) CJS 轉(zhuǎn) ESM 積累了一定的解決方案。通過(guò)云端統(tǒng)一處理的方式,后續(xù)也能發(fā)揮出更大的作用。一些新的方案如免依賴安裝也在持續(xù)探索中,最后,我們也希望能對(duì) Unbundled Development 生態(tài)添磚加瓦,最后反哺生態(tài)。

          參考資料

          [1]

          Webpack: https://webpack.js.org/guides/build-performance/

          [2]

          Build Performance Guide: https://webpack.js.org/guides/build-performance/

          [3]

          lazy-compile-webpack-plugin: https://github.com/liximomo/lazy-compile-webpack-plugin

          [4]

          rollup-plugin-cdn: https://github.com/WebReflection/rollup-plugin-cdn

          [5]

          @pika/cdn-webpack-plugin: https://www.npmjs.com/package/@pika/cdn-webpack-plugin

          [6]

          @pika/cdn-webpack-plugin: https://www.npmjs.com/package/@pika/cdn-webpack-plugin

          [7]

          babel-plugin-macros: https://github.com/kentcdodds/babel-plugin-macros

          [8]

          import-maps: https://github.com/WICG/import-maps#the-basic-idea

          [9]

          ESM-HMR Spec: https://github.com/snowpackjs/esm-hmr



          內(nèi)推社群


          我組建了一個(gè)氛圍特別好的騰訊內(nèi)推社群,如果你對(duì)加入騰訊感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行面試相關(guān)的答疑、聊聊面試的故事、并且在你準(zhǔn)備好的時(shí)候隨時(shí)幫你內(nèi)推。下方加 winty 好友回復(fù)「面試」即可。


          瀏覽 33
          點(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>
                  久久综合五月婷婷 | 性生活片日逼片 | 丁香五月婷操 | 中文字字幕在线中 | 日韩视频――中文字幕 |