天天看點

如何在微前端中加載 Vite 應用?

自 2018 年 5 月 Firefox 60 釋出後,所有主浏覽器均預設支援 ES modules。借助 ES modules 的能力,代碼可以實作無需建構直接運作。

随着 Vite 和 Snowpack 等基于 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 資源的加載方式如下:

<script src="index.js" type="module"></script>      

然而,在 icestark 中需要依賴微應用導出 生命周期函數 來渲染微應用。使用 

<script >

 标簽加載 ES modules 腳本的一個難題在于無法擷取微應用導出的生命周期函數。基于這個考慮,實際實作中是通過 Dynamic Import 來加載腳本:

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

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

如何在微前端中加載 Vite 應用?

可以認為,支援 ES modules 的浏覽器版本,對 Dynamic Import 的支援也非常良好。同時,為了相容舊版浏覽器,通過 

new Function()

 将其包裹:

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

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

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

如何在微前端中加載 Vite 應用?

▐  Vite 應用的改造

對于微應用而言,需要導出 生命周期函數,并選擇合适的加載方式即可。

生命周期函數的接入非常簡單,在 Vite 應用的入口檔案(Vue 項目通常是 

main.t|js

,React 應用通常是 

app.t|jsx

)聲明函數(以 Vue 應用為例):

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<Element> | 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 找尋到一些蛛絲馬迹:

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

 注入進來,也就是最終的入口檔案實際上類似于下面的代碼:

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

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

  • 借助 Vite Lib 模式,修改應用入口
// 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 的解析能力,将入口修改為靜态資源的入口。

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

 中配置:

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

即可得到正确導出生命周期函數的微應用。詳細用法可參見 使用 ice.js Vite 模式。

▐  最終效果

如何在微前端中加載 Vite 應用?

你将得到什麼

▐  漸進更新

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

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

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

▐  二次加載的極緻體驗

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

// icestark 第二次執行加載腳本
const { mount, unmout } = import(esModule);      

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

如何在微前端中加載 Vite 應用?

寫在最後

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

引用

  1. icestark - 面向大型系統的微前端解決方案
  2. proposal-dynamic-import
  3. Vite - Next Generation Frontend Tooling
  4. ES modules: A cartoon deep-dive
  5. What Happens When a Module Is Imported Twice?

團隊介紹

我們是『阿裡巴巴淘系前端架構團隊』,負責為業務部門提供底層的基礎設施保障以及提高開發效率的工具和架構,目前維護 ICE、Rax、AppWorks、Kraken、icestark 等開源産品,并且持續地探索前沿技術。

繼續閱讀