在微前端方案 icestark 中載入 Vite 應用

語言: CN / TW / HK

前言

自 2018 年 5 月 Firefox 60 釋出後,所有主瀏覽器均預設支援 ES modules。藉助 ES modules 的能力,程式碼可以實現無需構建直接執行。

隨著 ViteSnowpack 等基於 ES modules 的構建工具的產生,前端隨即掀起了 ES modules 新一輪熱潮。

問題背景

天下武功,無招不破,唯快不破 - 李小龍

Vite、Snowpack 等基於 ES modules 的構建工具帶來了開發的極致體驗,相比傳統的構建工具,這些新型的構建工具或多或少地帶來了以下優勢:

  • 由於無需打包的特性,伺服器冷啟動時間超快

藉助 ES modules 的能力,模組化交給瀏覽器處理(雖然目前的階段存在一個預編譯的過程)。傳統構建器需要打包依賴和原始碼,才能構建整個應用,並提供服務。

  • 專案大小不再成為限制專案熱更新速度的因素

傳統構建器在程式碼更改時,需要重新構建並載入頁面,這樣帶來的的結果是:隨著專案體積增長,構建耗時越長。基於 ES modules 的構建器只進行單檔案編譯,單檔案更新,時間複雜度保持 O(1).

Vite 得益於原生 ES modules 的能力,大幅提升了開發時體驗。相信未來,隨著社群生態(CDN 服務、Deno)、ESM 相關標準(import-maps、import.meta)的逐步完善,以及越來越多的技術方案解決 ES modules 在瀏覽器端的相關難題(依賴瀑布,資源碎片化),前端會開啟一個無構建的新篇章。

同時在微前端領域,指令碼資源的打包規範向來是百花齊放(比如 singleSPA 預設支援 SystemJs 規範,icestark 預設支援 UMD 規範)。未來指令碼資源的打包規範必定是趨於統一的 ES modules 規範。正是基於這兩個原因,微前端支援 ES modules 應用的載入就成了使用者強訴求。

微前端載入 Vite 應用

載入 ES modules 微應用

Vite 會預設打包出符合標準的 ES modules 的指令碼資源。ES modules 資源的載入方式如下:

```html

```

然而,在 icestark 中需要依賴微應用匯出 生命週期函式 來渲染微應用。使用 <script > 標籤載入 ES modules 指令碼的一個難題在於無法獲取微應用匯出的生命週期函式。基於這個考慮,實際實現中是通過 Dynamic Import 來載入指令碼:

js const { mount, unmount } = await import(url);

Dynamic Import 的瀏覽器相容性如下:

Dynamic Import 的瀏覽器相容性

可以認為,支援 ES modules 的瀏覽器版本,對 Dynamic Import 的支援也非常良好。同時,為了相容舊版瀏覽器,通過 new Function() 將其包裹:

```js const dynamicImport = new Function('url', 'return import(url)');

const { mount, unmount } = await dynamicImport(url); ```

至此,除了能支援 IIFE / UMD 規範的微應用之外,icestark 支援了 ES modules 規範的應用載入,並通過 import 型別標識。icestark 整體載入流程圖如下:

undefined

Vite 應用的改造

對於微應用而言,需要匯出 生命週期函式,並選擇合適的載入方式即可。

生命週期函式的接入非常簡單,在 Vite 應用的入口檔案(Vue 專案通常是 main.t|js,React 應用通常是 app.t|jsx)宣告函式(以 Vue 應用為例):

```diff import { createApp } from 'vue' + import type { App as Root} from 'vue'; import App from './App.vue' + import isInIcestark from '@ice/stark-app/lib/isInIcestark'; - createApp(App).mount('#app');

  • let vue: Root | null = null;

  • if (!isInIcestark()) {

  • createApp(App).mount('#app');
  • }
  • // 匯出 mount 生命週期函式
  • export function mount({ container }: { container: Element}) {
  • vue = createApp(App);
  • vue.mount(container);
  • }
  • // 匯出 unmount 生命週期函式
  • export function unmount() {
  • if (vue) {
  • vue.unmount();
  • } } ```

然而,在實際構建過程中,我們發現宣告的函式並沒有在指令碼資源中匯出。這是個非常疑惑的點,讓我們深入到 Vite 的原始碼,並在內建的 vite:build-html 找尋到一些蛛絲馬跡:

js ... if (isModule) { inlineModuleIndex++ if (url && !isExcludedUrl(url)) { // <script type="module" src="..."/> // add it as an import js += `\nimport ${JSON.stringify(url)}` shouldRemove = true } else if (node.children.length) { // <script type="module">...</script> js += `\nimport "${id}?html-proxy&index=${inlineModuleIndex}.js"` shouldRemove = true } } ...

Vite 預設使用 index.html 作為入口,在解析 index.html 的過程中,會生成一個虛擬的入口檔案,將指令碼資源通過 import 注入進來,也就是最終的入口檔案實際上類似於下面的程式碼:

js import './src/main.ts'; import 'polyfill';

面對這個場景,我們想到了兩種解決方案:

diff // vite.config.ts export default defineConfig({ ... + build: { + lib: { + entry: './src/main.ts', + formats: ['es'], + fileName: 'index' + }, + rollupOptions: { + preserveEntrySignatures: 'exports-only' + } + }, })

這種方式有個明顯的問題是:Vite 以 Lib 模式構建出的應用,其產物並不是一個完整的前端應用(缺少 index.html),無法滿足獨立執行的條件。

  • 通過外掛修改 Vite 的這一預設行為

通過 vite-plugin-index-html 外掛,結合 Vite 的解析能力,將入口修改為靜態資源的入口。

```diff + import htmlPlugin from 'vite-plugin-index-html';

// vite.config.ts export default defineConfig({ + plugins: [vue(), htmlPlugin({ + input: './src/main.ts' + })] }) ```

ice.js Vite 模式

同時,icestark 也支援 ice.js Vite 模式快速接入。安裝或升級 build-plugin-icestark 外掛,在微應用 build.json 中配置:

diff { + "vite": true, "plugins": [ ["build-plugin-icestark", { "type": "child", }] ] }

即可得到正確匯出生命週期函式的微應用。詳細用法可參見 使用 ice.js Vite 模式

最終效果

你將得到什麼

漸進升級

為了解決時間上,長尾應用升級帶來的效率問題,微前端通常是大型架構升級所選擇的中間態(或終態)方案。因此在設計載入 ES modules 方案時,需要保持這一基準原則。

框架應用可以保持現有的構建方式不變(仍然可以使用 webpack 等非原生 ES modules 構建工具),亦無需對框架應用做任何構建上的改造。

因此,基本可以無痛嘗試 Vite 所帶來的快感,腳踏實地地,一點點地靠近遠方。

二次載入的極致體驗

通過對 ES modules 原理的探尋,可以知道 ES modules 只執行一次。換成實際例子,也就是說當第二次執行相同的載入指令碼時:

js // icestark 第二次執行載入指令碼 const { mount, unmout } = import(esModule);

瀏覽器不會重複執行 Construction -> Instantiation -> Evaluation 的流程,而是直接返回上次模組執行的結果。這會導致一些副作用的操作(比如在 Module Conext 下插入樣式資源,指令碼資源的行為,這給我們的微應用二次載入帶來了額外的問題),同時也帶來了極快的二次載入效果。

二次載入對比

寫在最後

建立在原生 ES modules 規範下的應用不會在短時間內快速鋪開,很多 To C,To 商戶的業務對瀏覽器的版本仍有限制。但是,icestark 在 2.x 快一年多的發展以來,仍希望覆蓋到多樣的開發場景,提供便捷、快速地業務升級。在支援傳統 JS bundle、UMD 規範,本文分享了 icestark 在接入 ES modules 規範微應用的一些嘗試,希望能給開發者帶來一些新的選擇和啟發。

引用