【微前端】single-spa 到底是個什么鬼

前言

說起微前端框架,很多人第一反應(yīng)就是 single-spa匈子。但是再問深入一點(diǎn):它是干嘛的住拭,它有什么用偷线,可能就回答不出來了摇零。

一方面沒多少人研究和使用微前端推掸。可能還沒來得及用微前端擴(kuò)展項(xiàng)目遂黍,公司就已經(jīng)倒閉了终佛。

另一方面是中文博客對微前端的研究少之又少俊嗽,很多文章只是簡單翻譯一下官方文檔雾家,讀幾個API,放個官方的 Demo 就完事了绍豁。很少有深入研究到底 single-spa 是怎么一回事的芯咧。

還有一方面是 single-spa 的文檔非常難看懂,和 Redux 文檔一樣喜歡造概念。講一個東西的時候敬飒,總是把別的庫拉進(jìn)來一起講邪铲,把一個簡單的東西變得非常復(fù)雜。最令人吐槽的一點(diǎn)就是官方的 sample code 都是只言片語无拗,完全拼湊不出來一個 Demo带到,而 Github 的 Demo 還賊復(fù)雜,沒解釋英染,光看完都要 clone 好幾個 repo揽惹。

最后,求人不如求己四康,剛完源碼再剛一下文檔搪搏。

這篇文章將不會聊怎么搭建一個 Demo,而是會從 “Why” 和 “How” 的角度來聊一下官方文檔的都講了哪些內(nèi)容闪金,相信看完這篇文章就能看懂 官方的 Demo 了疯溺。

一個需求

讓我們從一個最小的需求開始說起。有一天產(chǎn)品經(jīng)理突然說:我們要做一個 A 頁面哎垦,我看到隔壁組已經(jīng)做過這個 A 頁面了囱嫩,你把它放到我們項(xiàng)目里吧,應(yīng)該不是很難吧漏设?明天上線吧挠说。

此時,產(chǎn)品經(jīng)理想的是:應(yīng)該就填一個 URL 就好吧愿题?再不行损俭,復(fù)制粘貼也很快吧。而程序員想的卻是:又要看屎山了潘酗。又要重構(gòu)了杆兵。又要聯(lián)調(diào)了。測試數(shù)據(jù)有沒有白卸帷琐脏?等一下,聯(lián)調(diào)的后端是誰案淄谩日裙?

估計(jì)這是做大項(xiàng)目時經(jīng)常遇到的需求了:搬運(yùn)一個現(xiàn)有的頁面。我想大多數(shù)人都會選擇在自己項(xiàng)目里復(fù)制粘貼別人的代碼惰蜜,然后稍微重構(gòu)一下昂拂,再測試環(huán)境聯(lián)調(diào),最后上線抛猖。

但是格侯,這樣就又多了一份代碼了鼻听,如果別人的頁面改了,那么自己項(xiàng)目又要跟著同步修改联四,再聯(lián)調(diào)撑碴,再上線,非常麻煩朝墩。

所以程序員就想能不能我填一個 url醉拓,然后這個頁面就到項(xiàng)目里來了呢?所以收苏,<iframe/> 就出場了廉嚼。

iframe 的弊端

iframe 就相當(dāng)于頁面里再開個窗口加載別的頁面,但是它有很多弊端:

  • 每次進(jìn)來都要加載倒戏,狀態(tài)不能保留
  • DOM 結(jié)構(gòu)不共享怠噪。比如子應(yīng)用里有一個 Modal,顯示的時候只能在那一小塊地方展示杜跷,不能全屏展示
  • 無法跟隨瀏覽器前進(jìn)后退
  • 天生的硬隔離傍念,無法與主應(yīng)用進(jìn)行資源共享,交流也很困難

而 SPA 正好可以解決上面的問題:

  • 切換路由就是切換頁面組件葛闷,組件的掛載和卸載非潮锘保快
  • 單頁應(yīng)用肯定共享 DOM
  • 前端控制路由,想前就前淑趾,想后就后
  • React 通信有 Redux阳仔,Vue 通信有 Vuex,可與 App 組件進(jìn)行資源共享扣泊,交流很爽

這就給我們一個啟發(fā):能不能有這么一個巨型 SPA 框架近范,把現(xiàn)有的 SPA 當(dāng)成 Page Component 來組裝成一個新的 SPA 呢?這就是微前端的由來延蟹。

微前端是什么

微前端應(yīng)該有如下特點(diǎn):

  • 技術(shù)棧無關(guān)评矩,主框架不限制接入應(yīng)用的技術(shù)棧,微應(yīng)用具備完全自主權(quán)
  • 獨(dú)立開發(fā)阱飘、獨(dú)立部署斥杜,微應(yīng)用倉庫獨(dú)立,前后端可獨(dú)立開發(fā)沥匈,部署完成后主框架自動完成同步更新
  • 增量升級蔗喂,在面對各種復(fù)雜場景時,我們通常很難對一個已經(jīng)存在的系統(tǒng)做全量的技術(shù)棧升級或重構(gòu)高帖,而微前端是一種非常好的實(shí)施漸進(jìn)式重構(gòu)的手段和策略
  • 獨(dú)立運(yùn)行時缰儿,每個微應(yīng)用之間狀態(tài)隔離,運(yùn)行時狀態(tài)不共享

等一下等一下棋恼,說了一堆返弹,到底啥是 single-spa 啊。

嘿嘿爪飘,single-spa 框架并沒有實(shí)現(xiàn)上面任何特點(diǎn)义起,對的,一個都沒有师崎,Just Zero默终。

single-spa 到底是干嘛的

single-spa 僅僅是一個子應(yīng)用生命周期的調(diào)度者。single-spa 為應(yīng)用定義了 boostrap, load, mount, unmount 四個生命周期回調(diào):

只要寫過 SPA 的人都能理解犁罩,無非就是生齐蔽、老、病床估、死含滴。不過有幾個點(diǎn)需要注意一下:

  • Register 不是生命周期,指的是調(diào)用 registerApplication 函數(shù)這一步
  • Load 是開始加載子應(yīng)用丐巫,怎么加載由開發(fā)者自己實(shí)現(xiàn)(等會會說到)
  • Unload 鉤子只能通過調(diào)用 unloadApplication 函數(shù)才會被調(diào)用

OK谈况,上面 4 個生命周期的回調(diào)順序是 single-spa 可以控制的,我能理解递胧,那什么時候應(yīng)該開始這一套生命周期呢碑韵?應(yīng)該是有一個契機(jī)來開始整套流程的,或者某幾個流程的缎脾。

契機(jī)就是當(dāng) window.location.href 匹配到 url 時祝闻,開始走對應(yīng)子 App 的這一套生命周期嘛。所以遗菠,single-spa 還要監(jiān)聽 url 的變化联喘,然后執(zhí)行子 app 的生命周期流程。

到此辙纬,我們就有了 single-spa 的大致框架了耸袜,無非就兩件事:

  • 實(shí)現(xiàn)一套生命周期,在 load 時加載子 app牲平,由開發(fā)者自己玩堤框,別的生命周期里要干嘛的,還是由開發(fā)者造的子應(yīng)用自己玩
  • 監(jiān)聽 url 的變化纵柿,url 變化時蜈抓,會使得某個子 app 變成 active 狀態(tài),然后走整套生命周期

畫個草圖如下:

是不是感覺 single-spa 很雞賊昂儒?雖然 single-spa 說自己是微前端框架沟使,但是一個微前端的特性都沒有實(shí)現(xiàn),都是需要開發(fā)者在加載自己子 App 的時候?qū)崿F(xiàn)的渊跋,要不就是通過一些第三方工具實(shí)現(xiàn)腊嗡。

注冊子應(yīng)用

有了上面的了解之后着倾,我們再來看 single-spa 里最重要的 API:registerApplication,表示注冊一個子應(yīng)用燕少。使用如下:

singleSpa.registerApplication({
    name: 'taobao', // 子應(yīng)用名
    app: () => System.import('taobao'), // 如何加載你的子應(yīng)用
    activeWhen: '/appName', // url 匹配規(guī)則卡者,表示啥時候開始走這個子應(yīng)用的生命周期
    customProps: { // 自定義 props,從子應(yīng)用的 bootstrap, mount, unmount 回調(diào)可以拿到
        authToken: 'xc67f6as87f7s9d'
    }
})

singleSpa.start() // 啟動主應(yīng)用

上面注冊了一個子應(yīng)用 'taobao'客们。我們自己實(shí)現(xiàn)了加載子應(yīng)用的方法崇决,通過 activeWhen 告訴 single-spa 什么時候要掛載子應(yīng)用,好像就可以上手開擼代碼嘍底挫。

可以個鬼恒傻!請告訴我 System.import 是個什么鬼。哦建邓,是 SystemJS盈厘,誒,SystemJS 聽說過官边,它是個啥扑庞?為啥要用 SystemJS?憑啥要用 SystemJS拒逮?

SystemJS

相信很多人看過一些微前端的博客罐氨,它們都會說 single-spa 是基于 SystemJS 的。錯滩援!single-spa 和 SystemJS 一點(diǎn)關(guān)系都沒有栅隐!這里先放個主應(yīng)用和子應(yīng)用的關(guān)系圖:

single-spa 的理念是希望主應(yīng)用可以做到非常非常簡單的和輕量,簡單到只要一個 index.html + 一個 main.js 就可以完成微前端工程玩徊,連 Webpack 都不需要租悄,直接在瀏覽器里執(zhí)行 singleSpa.registerApplication 就收工了,這種執(zhí)行方式也就是 in-browser 執(zhí)行恩袱。

但是泣棋,瀏覽器里執(zhí)行 JS,別說實(shí)現(xiàn) import xxx from 'https://taobao.com' 了畔塔,我要是在瀏覽器里實(shí)現(xiàn) ES6 的 import/export 都不行疤侗病: import axios from 'axios'

其實(shí)澈吨,也不是不行把敢,只需要在 <script> 標(biāo)簽加上 type="module",也是可以實(shí)現(xiàn)的谅辣,例如:

<script type="module" src="module.js"></script>
<script type="module">
  // or an inline script
  import {helperMethod} from './providesHelperMethod.js';
  helperMethod();
</script>
// providesHelperMethod.js
export function helperMethod() {
  console.info(`I'm helping!`);
}

但是修赞,遇到導(dǎo)入模塊依賴的,像 import axios from 'axios' 這樣的桑阶,就需要 importmap 了:

<script type="importmap">
    {
       "imports": {
          "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js"
       }
    }
</script>

<div id="container">我是:{{name}}</div>

<script type="module">
  import Vue from 'vue'

  new Vue({
    el: '#container',
    data: {
      name: 'Jack'
    }
  })
</script>

importmap 的功能就是告訴 'vue' 這個玩意要從 "https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js" 這里來的柏副。不過勾邦,importmap 現(xiàn)在只有 Chrome 是支持的。

所以割择,SystemJS 就將這一塊補(bǔ)齊了眷篇。當(dāng)然,除了 importmap锨推,它還有很多的功能铅歼,比如獲取當(dāng)前加載的所有模塊公壤、當(dāng)前模塊的 URL换可、可以 import html, import css,import wasm厦幅。

等等沾鳄,這在 Webpack 不也可以做到么?Webpack 還能 import less, import scss 呢确憨?這不比 SystemJS 牛逼译荞?對的,如果不是因?yàn)橐跒g覽器使用 import/export休弃,沒人會用 SystemJS吞歼。SystemJS 的好處和優(yōu)勢有且僅有一點(diǎn):那就是在瀏覽器里使用 ES6 的 import/export。

而正因?yàn)?SystemJS 可以在瀏覽器里可以使用 ES6 的 import/export 并支持動態(tài)引入塔猾,正好符合 single-spa 所提倡的 in-browser 執(zhí)行思路篙骡,所以 single-spa 文檔里才反復(fù)出現(xiàn) SystemJS 的身影,而且 Github Demo 里依然是使用 SystemJS 的 importmap 機(jī)制來引入不同模塊:

<script type="systemjs-importmap">
    {
      "imports": {
        "@react-mf/root-config": "http://localhost:9000/react-mf-root-config.js"
      }
    }
</script>

<script>
  singleSpa.registerApplication({
    name: 'taobao', // 子應(yīng)用名
    app: () => System.import('@react-mf/root-config'), // 如何加載你的子應(yīng)用
    activeWhen: '/appName', // url 匹配規(guī)則丈甸,表示啥時候開始走這個子應(yīng)用的生命周期
    customProps: { // 自定義 props糯俗,從子應(yīng)用的 bootstrap, mount, unmount 回調(diào)可以拿到
        authToken: 'xc67f6as87f7s9d'
    }
  })
</script>

公共依賴

SystemJS 另一個好處就是可以通過 importmap 引入公共依賴。

假如睦擂,我們有三個子應(yīng)用得湘,它們都有公共依賴項(xiàng) antd,那每個子應(yīng)用打包出來都會有一份 antd 的代碼顿仇,就顯示很冗余淘正。

一個解決方法就是在主應(yīng)用里,通過 importmap 直接把 antd 代碼引入進(jìn)來臼闻,子應(yīng)用在 Webpack 設(shè)置 external 把 antd 打包時排除掉跪帝。子應(yīng)用打包就不會把 antd 打包進(jìn)去了,體積也變小了些阅。

有人會說了:我用 CDN 引入不行嘛伞剑?不行啊,因?yàn)樽討?yīng)用的代碼都是 import {Button} from 'antd' 的市埋,瀏覽器要怎么直接識別 ES6 的 import/export 呢黎泣?那還不得 SystemJS 嘛恕刘。

難道 Webpack 就沒有辦法可以實(shí)現(xiàn) importmap 的效果了么?Webpack 5 提出的 Module Federation 模塊聯(lián)邦就可以很好地做的 importmap 的效果抒倚。這是 Webpack 5 的新特性褐着,使用的效果和 importmap 差不多。關(guān)于模塊聯(lián)邦是個啥托呕,可以參考 這篇文章含蓉。

至于用 importmap 還是 Webpack 的 Module Federation,singles-spa 是推薦使用 importmap 的项郊,但是馅扣,文檔也沒有反對使用 Webpack 的 Module Federation 的理由。能用就OK着降。

SystemJS vs Webpack ES

有人可能會想:都 1202 年了差油,怎么還要在瀏覽器環(huán)境寫 JS 呢?不上個 Webpack 都不好意思說自己是前端開發(fā)了任洞。

沒錯蓄喇,Webpack 是非常強(qiáng)大的,而且可以利用 Webpack 很多能力交掏,讓主應(yīng)用變得更加靈活妆偏。比如,寫 less盅弛,scss钱骂,Webpack 的 prefetch 等等等等。然后在注冊子應(yīng)用時熊尉,完全可以利用 Webpack 的動態(tài)引入:

singleSpa.registerApplication({
    name: 'taobao', // 子應(yīng)用名
    app: () => import('taobao'), // 如何加載你的子應(yīng)用
    activeWhen: '/appName', // url 匹配規(guī)則罐柳,表示啥時候開始走這個子應(yīng)用的生命周期
    customProps: { // 自定義 props,從子應(yīng)用的 bootstrap, mount, unmount 回調(diào)可以拿到
        authToken: 'xc67f6as87f7s9d'
    }
})

那為什么 single-spa 還要推薦 SystemJS 呢狰住?個人猜測是因?yàn)?single-spa 希望主應(yīng)用應(yīng)該就一個空殼子张吉,只需要管內(nèi)容要放在哪個地方,所有的功能催植、交互都應(yīng)該交由 index.html 來統(tǒng)一管理肮蛹。

當(dāng)然,這僅僅是一種理念创南,可以完全不遵循它伦忠。像我個人還是喜歡用 Webpack 多一點(diǎn),SystemJS 還是有點(diǎn)多余稿辙,而且覺得有點(diǎn)奧特曼了昆码。不過,為了跟著文檔的節(jié)奏來,這里假設(shè)就用 SystemJS 來實(shí)現(xiàn)主應(yīng)用赋咽。

Root Config

由于 single-spa 非常強(qiáng)調(diào) in-browser 的方式來實(shí)現(xiàn)主應(yīng)用旧噪,所以 index.html 就充當(dāng)了靜態(tài)資源、子應(yīng)用的路徑聲明的角色脓匿。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Polyglot Microfrontends</title>
  <meta name="importmap-type" content="systemjs-importmap" />
  <script type="systemjs-importmap" src="https://storage.googleapis.com/polyglot.microfrontends.app/importmap.json"></script>
  <% if (isLocal) { %>
  <script type="systemjs-importmap">
    {
      "imports": {
        "@polyglot-mf/root-config": "http://localhost:9000/polyglot-mf-root-config.js"
      }
    }
  </script>
  <% } %>
  <script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.min.js"></script>
</head>
<body>
  <script>
    System.import('@polyglot-mf/root-config');
    System.import('@polyglot-mf/styleguide');
  </script>
  <import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>

而 main.js 則實(shí)現(xiàn)子應(yīng)用注冊淘钟、主應(yīng)用啟動。

import { registerApplication, start } from "single-spa";

registerApplication({
  name: "@polyglot-mf/navbar",
  app: () => System.import("@polyglot-mf/navbar"),
  activeWhen: "/",
});

registerApplication({
  name: "@polyglot-mf/clients",
  app: () => System.import("@polyglot-mf/clients"),
  activeWhen: "/clients",
});

registerApplication({
  name: "@polyglot-mf/account-settings",
  app: () => loadWithoutAmd("@polyglot-mf/account-settings"),
  activeWhen: "/settings",
});

start();

// A lot of angularjs libs are compiled to UMD, and if you don't process them with webpack
// the UMD calls to window.define() can be problematic.
function loadWithoutAmd(name) {
  return Promise.resolve().then(() => {
    let globalDefine = window.define;
    delete window.define;
    return System.import(name).then((module) => {
      window.define = globalDefine;
      return module;
    });
  });
}

像這樣的資源聲明 + 主子應(yīng)用加載的組件陪毡,single-spa 稱之為 Root Config米母。 它不是什么新概念,就只有兩個東西:

  • 一個主應(yīng)用的 index.html
  • 一個執(zhí)行 registerApplication 函數(shù)的 JS 文件

single-spa-layout

雖然一個 index.html 是完美的輕量微前端主應(yīng)用毡琉,但是就算再壓縮主應(yīng)用的交互铁瞒,那總得告訴子應(yīng)用放置的位置吧,那不還得 DOM API 一把梭绊起?一樣麻煩精拟?

為了解決這個問題燎斩,single-spa 說:沒事虱歪,我?guī)湍愀悖缓笤炝?single-spa-layout栅表。具體使用請看代碼:

<html>
  <head>
    <template id="single-spa-layout">
      <single-spa-router>
        <nav class="topnav">
          <application name="@organization/nav"></application>
        </nav>
        <div class="main-content">
          <route path="settings">
            <application name="@organization/settings"></application>
          </route>
          <route path="clients">
            <application name="@organization/clients"></application>
          </route>
        </div>
        <footer>
          <application name="@organization/footer"></application>
        </footer>
      </single-spa-router>
    </template>
  </head>
</html>

不能說和 Vue Router 很像笋鄙,只能說一模一樣吧。當(dāng)然上面這么寫很直觀怪瓶,但是瀏覽器并不認(rèn)識這些元素萧落,所以 single-spa-layout 把識別這些元素的邏輯都封裝成了函數(shù),并暴露給開發(fā)者洗贰,開發(fā)者只要調(diào)用一下就能識別出 appName 等信息了:

import { registerApplication, start } from 'single-spa';
import {
  constructApplications,
  constructRoutes,
  constructLayoutEngine,
} from 'single-spa-layout';

// 獲取 routes
const routes = constructRoutes(document.querySelector('#single-spa-layout'));

// 獲取所有的子應(yīng)用
const applications = constructApplications({
  routes,
  loadApp({ name }) {
    return System.import(name); // SystemJS 引入入口 JS
  },
});

// 生成 layoutEngine
const layoutEngine = constructLayoutEngine({ routes, applications });

// 批量注冊子應(yīng)用
applications.forEach(registerApplication);

// 啟動主應(yīng)用
start();

沒什么好說的找岖,constrcutRoutes, constructApplicationconstructLayoutEngine 本質(zhì)上就是識別 single-spa-layout 定義的元素標(biāo)簽,然后獲取里面的屬性敛滋,再通過 registerApplication 函數(shù)一個個注冊就完事了许布。

改造子應(yīng)用

上面說的都是主應(yīng)用的事情,現(xiàn)在我們來關(guān)心一下子應(yīng)用绎晃。

子應(yīng)用最關(guān)鍵的一步就是導(dǎo)出 bootstrap, mount, unmount 三個生命周期鉤子蜜唾。

import SubApp from './index.tsx'

export const bootstrap = () => {}
export const mount = () => {
  // 使用 React 來渲染子應(yīng)用的根組件
  ReactDOM.render(<SubApp/>, document.getElementById('root'));
}
export const unmount = () => {}

single-spa-react, single-spa-vue, single-spa-angular, single-spa-xxx, ...

emmmm,怎么說的呢庶艾,上面三個 export 不太好看袁余,能不能有一種更直接的方法就實(shí)現(xiàn) 3 個生命周期的導(dǎo)出呢?

single-spa 說:可以啊咱揍,搞颖榜!所以有了 single-spa-react:

import React from 'react';
import ReactDOM from 'react-dom';
import SubApp from './index.tsx';
import singleSpaReact, {SingleSpaContext} from 'single-spa-react';

const reactLifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: SubApp,
  errorBoundary(err, info, props) {
    return (
      <div>出錯啦!</div>
    );
  },
});

export const bootstrap = reactLifecycles.bootstrap;
export const mount = reactLifecycles.mount;
export const unmount = reactLifecycles.unmount;

single-spa 說:我不能單給 react 搞啊,別的框架也要給它們整上一個掩完,一碗水端平蟹地,所以有這了這些牛鬼蛇神:

不禁感慨:這些小輪子是真能造啊。

導(dǎo)入子應(yīng)用的 CSS

不知道你有沒有注意到藤为,在剛剛的子應(yīng)用注冊里我們僅僅用 System.import 導(dǎo)入了一個 JS 文件怪与,那 CSS 樣式文件怎么搞呢?可能可以 System.import('xxx.css') 來導(dǎo)入缅疟。

但是分别,這又有問題了:在切換了應(yīng)用時,unmount 的時候要怎么把已有的 CSS 給刪掉呢存淫?官方說可以這樣:

const style = document.createElement('style');
style.textContent = `.settings {color: blue;}`;

export const mount = [
  async () => {
    document.head.appendChild(styleElement);
  },
  reactLifecycles.mount,
]

export const unmount = [
  reactLifecycles.unmount,
  async () => {
    styleElement.remove();
  }
]

我:single-spa耘斩,求求你做個人吧,搭個 Demo桅咆,還要我來處理 CSS括授?single-spa 說:好,等我再去造一個輪子岩饼。于是荚虚,就有了 single-spa-css。用法如下:

import singleSpaCss from 'single-spa-css';

const cssLifecycles = singleSpaCss({
  // 這里放你導(dǎo)出的 CSS籍茧,如果 webpackExtractedCss 為 true版述,可以不指定
  cssUrls: ['https://example.com/main.css'],

  // 是否要使用從 Webpack 導(dǎo)出的 CSS,默認(rèn)為 false
  webpackExtractedCss: false,

  // 是否 unmount 后被移除寞冯,默認(rèn)為 true
  shouldUnmount: true,

  // 超時渴析,不廢話了,都懂的
  timeout: 5000
})

const reactLifecycles = singleSpaReact({...})

// 加入到子應(yīng)用的 bootstrap 里
export const bootstrap = [
  cssLifecycles.bootstrap,
  reactLifecycles.bootstrap
]

export const mount = [
  // 加入到子應(yīng)用的 mount 里吮龄,一定要在前面俭茧,不然 mount 后會有樣式閃一下的問題
  cssLifecycles.mount,
  reactLifecycles.mount
]

export const unmount = [
  // 和 mount 同理
  reactLifecycles.unmount,
  cssLifecycles.unmount
]

這里要注意一下,上面的 https://example.com/main.css 并沒有看起來那么簡單易用漓帚。

假如你用了 Webpack 來打包母债,很有可能會用分包或者 content hash 來給 CSS 文件命名,比如 filename: "[name].[contenthash].css"胰默。那請問 cssUrls 要怎么寫呀场斑,每次都要改 cssUrls 參數(shù)么?太麻煩了吧牵署。

single-spa-css 說:我可以通過 Webpack 導(dǎo)出的 __webpack_require__.cssAssetFileName 獲取導(dǎo)出之后的真實(shí) CSS 文件名漏隐。ExposeRuntimeCssAssetsPlugin 這個插件正好可以解決這個問題。這么一來 cssUrls 就可以不用指定了奴迅,直接把 Webpack 導(dǎo)出的真實(shí) CSS 名放到 cssUrls 里了青责。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ExposeRuntimeCssAssetsPlugin = require("single-spa-css/ExposeRuntimeCssAssetsPlugin.cjs");

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
    }),
    new ExposeRuntimeCssAssetsPlugin({
      // The filename here must match the filename for the MiniCssExtractPlugin
      filename: "[name].css",
    }),
  ],
};

子應(yīng)用 CSS 樣式隔離

雖然 single-spa-css 解決了子應(yīng)用的 CSS 引入和移除問題挺据,但是又帶來了另一個問題:怎么保證各個子應(yīng)用的樣式不互相干擾呢?官方給出的建議是:

第一種方法:使用 Scoped CSS脖隶,也即在子應(yīng)用的 CSS 選擇器上加前綴就好了嘛扁耐,像這樣:

.app1__settings-67f89dd87sf89ds {
  color: blue;
}

要是嫌麻煩,可以在 Webpack 使用 PostCSS Prefix Selector 給樣式自動加前綴:

const prefixer = require('postcss-prefix-selector');

module.exports = {
  plugins: [
    prefixer({
      prefix: "#single-spa-application\\:\\@org-name\\/project-name"
    })
  ]
}

另一種方法是在加載子應(yīng)用的函數(shù)里产阱,將子應(yīng)用掛載到 Shadow DOM 上婉称,可以實(shí)現(xiàn)完美的樣式隔離。Shadow DOM 是什么构蹬,怎么玩可見 MDN這里王暗。

公共 CSS 樣式怎么處理

上面說的都是子應(yīng)用自己的 CSS 樣式,那如果子應(yīng)用之間要共享 CSS 怎么辦呢庄敛?比如有兩個子應(yīng)用都用了 antd俗壹,那都要 import 兩次 antd.min.css 了。

這個問題和上面提到的處理“公共依賴”的問題是差不多的藻烤。官方給出兩個建議:

  1. 將公共的 CSS 放到 importmap 里绷雏,也可以理解為在 index.html 里直接加個 link 獲取 antd 的 CSS 完事
  2. 將所有的公共的 UI 庫都 import 到 utility 里,將 antd 所有內(nèi)容都 export怖亭,再把 utility 包放到 importmap 里涎显,然后 import { Button } from '@your-org-name/utility'; 去引入里面的組件

其實(shí)上面兩個方法都大同小異,思路都是在主應(yīng)用一波引入依许,只是一個統(tǒng)一引入CSS棺禾,另一個統(tǒng)一引入 UI 庫缀蹄。

子應(yīng)用的 JS 隔離

我們來想想應(yīng)用的 JS 隔離本質(zhì)是什么峭跳,本質(zhì)其實(shí)就是在 B 子應(yīng)用里使用 window 全局對象里的變量時,不要被 A 子應(yīng)用給污染了缺前。

一個簡單的解決思路就是:在 mount A 子應(yīng)用時蛀醉,正常添加全局變量,比如 jQuery 的 $, lodash 的 _衅码。在 unmount A 子應(yīng)用時拯刁,用一個對象記錄之前給 window 添加的全局變量,并把 A 應(yīng)用里添加 window 的變量都刪掉逝段。下一次再 mount A 應(yīng)用時垛玻,把記錄的全局變量重新加回來就好了。

single-spa 再次站出來:這個不用你自己手動記錄 window 的變更了奶躯。single-spa-leaked-globals 已經(jīng)實(shí)現(xiàn)好了帚桩,直接用就好了:

import singleSpaLeakedGlobals from 'single-spa-leaked-globals';

// 其它 single-spa-xxx 提供的生命周期函數(shù)
const frameworkLifecycles = ...

const leakedGlobalsLifecycles = singleSpaLeakedGlobals({
  globalVariableNames: ['$', 'jQuery', '_'], // 新添加的全局變量
})

export const bootstrap = [
  leakedGlobalsLifecycles.bootstrap, // 放在第一位
  frameworkLifecycles.bootstrap,
]

export const mount = [
  leakedGlobalsLifecycles.mount, // mount 時添加全局變量,如果之前有記錄在案的嘹黔,直接恢復(fù)
  frameworkLifecycles.mount,
]

export const unmount = [
  leakedGlobalsLifecycles.unmount, // 刪掉新添加的全局變量
  frameworkLifecycles.unmount,
]

但是账嚎,這個庫的局限性在于:每個 url 只能加一個子 app,如果多個子 app 之間還是會訪問同一個 window 對象,也因此會互相干擾郭蕉,并不能做到完美的 JS 沙箱疼邀。

比如:一個頁面里,導(dǎo)航欄用 3.0 的 jQuery召锈,而頁面主體用 5.0 的 jQuery旁振,那就會有沖突了。

所以這個庫的場景也僅限于:首頁用 3.0 的 jQuery涨岁,訂單詳情頁使用 5.0 的 jQuery 這樣的場景规求。

子應(yīng)用的分類

上面我們說到了,當(dāng) url 匹配 activeWhen 參數(shù)時卵惦,就會執(zhí)行對應(yīng)子應(yīng)用的生命周期阻肿。那這樣就相當(dāng)于子應(yīng)用和 url 綁定在了一起了。

我們再來看 single-spa-leaked-globals沮尿,single-spa-css 這些庫丛塌,雖然它們也導(dǎo)出了生命周期,但這些生命周期與頁面渲染畜疾、url 變化沒有多大關(guān)系赴邻。

它們與普通的 application 唯一不同的地方就是:普通 application 的生命周期是通過 single-spa 來自動調(diào)度的,而這些庫是要通過手動調(diào)度的啡捶。只不過我們一般選擇在子應(yīng)用里的生命周期里手動調(diào)用它們而已姥敛。

這種與 url 無關(guān)的 “app” 在微前端也有著非常重要的作用,一般是在子應(yīng)用的生命周期里提供一些功能瞎暑,像 single-spa-css 就是在 mount 時添加 <link/> 標(biāo)簽彤敛。single-spa 將這樣的 “類子 app” 稱為 Parcel。

同時了赌,single-spa 還分出另一個類:Utility Modules墨榄。很多子應(yīng)用都用 antd, dayjs, axios 的,那么就可以搞一個 utility 集合這些公共庫勿她,然后統(tǒng)一做 export袄秩,然后在 importmap 里統(tǒng)一導(dǎo)入。子應(yīng)用就可以不需要在自己的 package.json 里添加 antd, dayjs, axios 的依賴了逢并。

總結(jié)一下之剧,single-spa 將微前端分為三大類:

分類 功能 導(dǎo)出 是否與 url 有關(guān)
Application 子應(yīng)用 bootstrap, mount, unmount
Parcel 功能組件,比如子應(yīng)用的生命周期打一些補(bǔ)丁 bootstrap, mount, unmount, update
Utility Module 公共資源 所有公共資源

create-single-spa

上面介紹了一堆的與子應(yīng)用相關(guān)的庫砍聊,如果自己要從 0 開始慢慢地配置子應(yīng)用就比較麻煩背稼。所以,single-spa 說:不麻煩辩恼,有腳手架工具雇庙,一行命令生成子應(yīng)用谓形,都給您配好了。

npm install --global create-single-spa

# 或者
yarn global add create-single-spa

然后

create-single-spa

注意疆前!這里的 create-single-spa 指的是創(chuàng)建子應(yīng)用寒跳!

總結(jié)

以上就是 singles-spa 文檔里的所有內(nèi)容了(除了 SSR 和 Dev Tools,前者用的不多竹椒,后者自己看一下就會了童太,不多廢話)。由于本文是通過發(fā)現(xiàn)問題到解決問題來講述文檔內(nèi)容的胸完,所以從頭看到尾還是有點(diǎn)亂书释,這里就做一下總結(jié):

微前端概念

特點(diǎn):

  • 技術(shù)棧無關(guān)
  • 獨(dú)立開發(fā)、獨(dú)立部署
  • 增量升級
  • 獨(dú)立運(yùn)行時

single-spa

只做兩件事:

  • 提供生命周期概念,并負(fù)責(zé)調(diào)度子應(yīng)用的生命周期
  • 挾持 url 變化事件和函數(shù),url 變化時匹配對應(yīng)子應(yīng)用呻引,并執(zhí)行生命周期流程

三大分類:

  • Application:子應(yīng)用秘豹,和 url 強(qiáng)相關(guān)位衩,交由 single-spa 調(diào)用生命周期
  • Parcel:組件,和 url 無關(guān),手動調(diào)用生命周期
  • Utility Module:統(tǒng)一將公共資源導(dǎo)出的模塊

“重要”概念

  • Root Config:指主應(yīng)用的 index.html + main.js。HTML 負(fù)責(zé)聲明資源路徑熄阻,JS 負(fù)責(zé)注冊子應(yīng)用和啟動主應(yīng)用
  • Application:要暴露 bootstrap, mount, umount 三個生命周期,一般在 mount 開始渲染子 SPA 應(yīng)用
  • Parcel:也要暴露 bootstrap, mount, unmount 三個生命周期倔约,可以再暴露 update 生命周期秃殉。Parcel 可大到一個 Application,也可以小到一個功能組件浸剩。與 Application 不同的是 Parcel 需要開發(fā)都手動調(diào)用生命周期

SystemJS

可以在瀏覽器使用 ES6 的 import/export 語法钾军,通過 importmap 指定依賴庫的地址。

和 single-spa 沒有關(guān)系乒省,只是 in-browser import/export 和 single-spa 倡導(dǎo)的 in-browser run time 相符合巧颈,所以 single-spa 將其作為主要的導(dǎo)入導(dǎo)出工具。

用 Webpack 動態(tài)引入可不可以袖扛,可以,甚至可能比 SystemJS 好用十籍,并無好壞之分蛆封。

single-spa-layout

和 Vue Router 差不多,主要功能是可以在 index.html 指定在哪里渲染哪個子應(yīng)用勾栗。

single-spa-react, single-spa-xxx....

給子應(yīng)用快速生成 bootstrap, mount, unmount 的生命周期函數(shù)的工具庫惨篱。

single-spa-css

隔離前后兩個子應(yīng)用的 CSS 樣式。

在子應(yīng)用 mount 時添加子應(yīng)用的 CSS围俘,在 unmount 時刪除子應(yīng)用的 CSS砸讳。子應(yīng)用使用 Webpack 導(dǎo)出 CSS 文件時琢融,要配合 ExposeRuntimeCssAssetsPlugin 插件來獲取最終導(dǎo)出的 CSS 文件名。

算實(shí)現(xiàn)了一半的 CSS 沙箱簿寂。

如果要在多個子應(yīng)用進(jìn)行樣式隔離漾抬,可以有兩種方法:

  • Shadow DOM,樣式隔離比較好的方法常遂,但是穿透比較麻煩
  • Scoped CSS纳令,在子應(yīng)用的 CSS 選擇器上添加前綴做區(qū)分,可以使用 postcss-prefix-selector 這個包來快速添加前綴

single-spa-leaked-globals

在子應(yīng)用 mount 時給 window 對象恢復(fù)/添加一些全局變量克胳,如 jQuery 的 $ 或者 lodash 的 _平绩,在 unmount 時把 window 對象的變量刪掉。

實(shí)現(xiàn)了“如果主應(yīng)用一個url只有一個頁面”情況下的 JS 沙箱漠另。

公共依賴

有兩種方法處理:

  • 造一個 Utility Module 包捏雌,在這個包導(dǎo)出所有公共資源內(nèi)容,并用 SystemJS 的 importmap 在主應(yīng)用的 index.html 里聲明
  • 使用 Webpack 5 Module Federation 特性實(shí)現(xiàn)公共依賴的導(dǎo)入

哪個更推薦笆搓?都可以腹忽。

最后

single-spa 文檔就這些了嘛?沒錯砚作,就這些了窘奏。文檔好像給了很多“最佳實(shí)踐”,但真正將所有“最佳實(shí)踐”結(jié)合起來并落地的又沒多少葫录。

比如文檔說用 Shadow CSS 來做子應(yīng)用之間的樣式隔離着裹,但是 single-spa-leaked-globals 又不讓別人在一個 url 上掛載多個子應(yīng)用。感覺很不靠譜:這里行了米同,那里又不行了骇扇。

再說回 Shadow CSS 來做樣式隔離,但是也沒有詳細(xì)說明要具體要怎么做面粮。像這樣的例子還有很多:文檔往往只告訴了一條路少孝,怎么走還要看開發(fā)者自己。這就你給人一種 “把問題只解決一半” 的感覺熬苍。

如果真的想用 single-spa 來玩小 Demo 的稍走,用上面提到的小庫來搭建微前端是可以的,但是要用到生產(chǎn)環(huán)境真的沒那么容易柴底。

所以婿脸,為了填平 single-spa 遺留下來的坑,阿里基于 single-spa 造出了 qiankun 微前端框架柄驻,真正實(shí)現(xiàn)了微前端的所有特性狐树,不過這又是另外一個故事了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸿脓,一起剝皮案震驚了整個濱河市抑钟,隨后出現(xiàn)的幾起案子涯曲,更是在濱河造成了極大的恐慌,老刑警劉巖在塔,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件幻件,死亡現(xiàn)場離奇詭異,居然都是意外死亡心俗,警方通過查閱死者的電腦和手機(jī)傲武,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來城榛,“玉大人揪利,你說我怎么就攤上這事『莩郑” “怎么了疟位?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長喘垂。 經(jīng)常有香客問我甜刻,道長,這世上最難降的妖魔是什么正勒? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任得院,我火速辦了婚禮,結(jié)果婚禮上章贞,老公的妹妹穿的比我還像新娘祥绞。我一直安慰自己,他們只是感情好鸭限,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布蜕径。 她就那樣靜靜地躺著,像睡著了一般败京。 火紅的嫁衣襯著肌膚如雪兜喻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天赡麦,我揣著相機(jī)與錄音朴皆,去河邊找鬼。 笑死隧甚,一個胖子當(dāng)著我的面吹牛车荔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播戚扳,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼族吻!你這毒婦竟也來了帽借?” 一聲冷哼從身側(cè)響起珠增,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎砍艾,沒想到半個月后蒂教,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡脆荷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年凝垛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜓谋。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡梦皮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出桃焕,到底是詐尸還是另有隱情剑肯,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布观堂,位于F島的核電站让网,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏师痕。R本人自食惡果不足惜溃睹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望胰坟。 院中可真熱鬧因篇,春花似錦、人聲如沸腕铸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽狠裹。三九已至虽界,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間涛菠,已是汗流浹背莉御。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留俗冻,地道東北人礁叔。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像迄薄,于是被迫代替她去往敵國和親琅关。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容