微前端框架single-spa初探


前言

最近入職的一家公司采用single-spa這個微前端框架,所以自學了此框架。

single-spa這個微前端框架雖然有中文文檔,但是有些零散和晦澀。

所以我想在學習之余,寫篇博客拉平一下這個學習曲線。

什么是微前端?

微前端的靈感來源於服務端微服務的理念。

可以簡單理解為,在開發一個復雜前端應用時,將其划分為一系列更小更簡單的前端應用。

這些前端應用可以單獨開發、測試、部署,松耦合,可維護性強,還可以讓前端代碼實現增量升級和使用不同的框架。

它的懶加載還能讓整個復雜應用加載速度變快。

常用微前端玩法和single-spa

在我之前的公司是使用iframe來實現微前端的,但是各個子應用間的通信往往比較麻煩,而且很不靈活。

而最新的微前端理念,是由webpack5中模塊聯合特性實現的,這里就不多講了。

single-spa是一個比較流行的微前端框架,它並不是使用iframe來實現微前端,也不是通過模塊聯合,而是通過路由路徑來在dom上加載不同的子應用。

Import maps和SystemJS

在具體講解single-spa前,我們得先了解一個東西:Import maps

這個功能是Chrome 89才支持的。

顧名思義,它是對import的一個映射處理,讓你控制在js中使用import時,到底從哪個url獲取這些庫。

比如通常我們會在js中,以下面這種方式引入模塊:

    import moment from "moment"

正常情況下肯定是node_modules中引入,但是現在我們在html中加入下面的代碼:

    <script type="importmap">
    {
        "imports": {
            "moment": "/moment/src/moment.js"
        }
    }
    </script>

這里/moment/src/moment.js這個地址換成一個cdn資源也是可以的。最終達到的效果就是:

    import moment from "/moment/src/moment.js"

有了Import maps,import的語法就可以直接在瀏覽器中使用,而不再需要webpack來幫我們進行處理,不需要從node_modules中去加載庫。

Import maps甚至還有一個兜底的玩法:

    "imports": {
        "jquery": [
            "https://某CDN/jquery.min.js",
            "/node_modules/jquery/dist/jquery.js"
        ]
    }

當cdn無效時,再從本地庫中獲取內容。

它的功能還有很多,我就不一一列舉了,只需要對這個有一定的了解即可。

盡管Import maps非常強大,但是畢竟瀏覽器兼容性還並不是很好,所以就有了我們的polifill方案:SystemJS

SystemJS同樣是一個模塊加載器,可兼容到IE11,同樣支持import映射,但是它的語法稍有不同:

    <script src="system.js"></script>
    <script type="systemjs-importmap">
    {
        "imports": {
            "lodash": "https://unpkg.com/lodash@4.17.10/lodash.js"
        }
    }
    </script>

在瀏覽器中引入system.js后,會去解析type為systemjs-importmap的script下的import映射。

它和Import maps最終達到的效果是一致的。

single-spa

之所以我們要先講Import mapsSystemJS,就是因為single-spa的微前端往往需要結合SystemJS來實現。

single-spa框架中,基座會檢測瀏覽器url的變化,在變化時往往通過SystemJS的import映射,來加載不同的子應用js。

但是需要注意,single-spa並不是必須依賴SystemJS

剛剛我們提到了一個概念:基座,現在講下single-spa的兩個概念:基座應用

你可以簡單理解應用是一個個的單頁面應用,而基座是一個應用管理器,用來根據路由加載不同的應用

一般在基座中我們需要像下面這樣注冊一個應用:

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

// 注冊應用1
registerApplication({
    name:'app1',
    app:() => import('./app1.js'),
    activeWhen: '/app1',
    customProps: { myTitle: "傳遞給應用的自定義參數的值" }
});

// 注冊應用2
registerApplication({
    name:'app2',
    app:() => import('./app2.js'),
    activeWhen: '/app2'
});

start();

在上面的代碼中,我們注冊了app1和app2兩個應用,分別匹配路由/app1和/app2。

也就是說當路由是/app1或者/app1/home時,會直接加載app1這個應用。

注冊應用后,需要start()來開始掛載應用,否則只會下載應用,而不會掛載應用。

那么應用應該如何設置呢?

我們上面代碼引用的./app1.js並沒有導出一個真的單頁面應用,而一般是如下:

console.info('第一步 下載應用階段')
export function bootstrap(props) {
    const {
        name,        // 應用名稱
        singleSpa,   // singleSpa實例
        mountParcel, // 手動掛載的函數
        myTitle  // 我們之前在注冊應用時傳遞給customProps的屬性
    } = props;     // Props 會傳給每個生命周期函數

    // 這個生命周期函數會在應用第一次掛載前執行一次。
    return Promise.resolve().then(() => {
        console.info('第二步 初始化', myTitle)
    })
}
export function mount(props) {
    // 每當應用路由匹配成功,但該應用處於未掛載狀態時,掛載的生命周期函數就會被調用。調用時,函數會根據URL來確定當前被激活的路由,創建DOM元素、監聽DOM事件等以向用戶呈現渲染的內容。任何子路由的改變(如hashchange或popstate等)不會再次觸發mount,需要各應用自行處理。
    return Promise.resolve().then(() => {
        console.info('第三步 掛載應用', props.name)
        document.getElementById('root').innerHTML = "我是app1啊"
    })
}

export function unmount(props) {
    // 每當應用路由匹配不成功,但該應用已掛載時,卸載的生命周期函數就會被調用。卸載函數被調用時,會清理在掛載應用時被創建的DOM元素、事件監聽、內存、全局變量和消息訂閱等。
    return Promise.resolve().then(() => {
        console.info('第四步 卸載應用', props.name)
        document.getElementById('root').innerHTML = ""
    })
}

export function unload(props) {
    // 移除”生命周期函數的實現是可選的,它只有在unloadApplication被調用時才會觸發。如果一個已注冊的應用沒有實現這個生命周期函數,則假設這個應用無需被移除。
    // 移除的目的是各應用在移除之前執行部分邏輯,一旦應用被移除,它的狀態將會變成NOT_LOADED,下次激活時會被重新初始化。
    // 移除函數的設計動機是對所有注冊的應用實現“熱下載”,不過在其他場景中也非常有用,比如想要重新初始化一個應用,且在重新初始化之前執行一些邏輯操作時。
    return Promise.resolve().then(() => {
        console.info('第五步 移除應用', props.name)
    })
}

可以看到我們的app1.js這個應用中的代碼,並沒有導出一個單頁面應用組件,而是導出了幾個生命周期函數,然后通過這幾個生命周期函數來控制組件的初始化,加載和卸載。

它這個操作是從現在我們的react或者vue這些框架的組件生命周期中獲得靈感,將生命周期應用於整個應用程序。

single-spa 與 SystemJS實現微前端

看了上面的代碼之后你可能有點疑惑,你這個東西也沒什么用嘛,不就是個懶加載嗎?

哪來的微前端?

我用個React.lazy(() => import('./app1.js'))來個懶加載怎么了,你不要說一大堆把我繞暈了。

上面這些實際上還真的沒有實現微前端,但是,你可以結合我們之前講的微前端,想象一下:

如果./app1.js不是基座這個項目內的代碼,而是另一個項目呢?

我們將這個app1.js放在一個單獨的項目中,它用react來寫了一個單頁面應用。

再將app2.js放在另一個單獨的項目中,它用vue來寫了一個單頁面應用。

通過我們現在的這個基座項目再來處理這兩個應用呢?

我們的基座項目是不是就可以寫成下面這樣:

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

// 注冊應用1
registerApplication({
    name:'app1',
    app:() => import('@曉組織/app1'),
    activeWhen: '/app1',
    customProps: { myTitle: "傳遞給應用的自定義參數的值" }
});

// 注冊應用2
registerApplication({
    name:'app2',
    app:() => import('@曉組織/app2'),
    activeWhen: '/app2'
});

start();

然后再在基座項目的模板頁中來個引入映射:

<script type="systemjs-importmap">
    {
        "imports": {
            "@曉組織/app1": "//某網站/app1.js",
            "@曉組織/app2": "//另一個網站/app2.js"
        }
    }
</script>

以后我們要做app1模塊的部分,只需要在app1這個項目中維護就可以了,不會干擾到其他的應用。

以后React20,React30出來,或者部分項目升級webpack,或者給一個項目大調整,我們可以一個個小應用嘗試升級修改,不用所有項目同時調整。不僅風險變小了很多,也更加可控。

上面是結合SystemJS實現的微前端,其實還有使用npm包和單項目的玩法,但是不推薦,有興趣的可以參考官網的這篇文章:拆分應用

single-spa-react

看到這里的朋友一定會想,這個東西好是好,怎么把它和react的單頁面應用結合起來?

讓我自己寫加載和卸載react的單頁面應用?這也太挫了吧。

當然不可能,single-spa的生態可是很好的。

single-spa-react就是一個輔助庫,它可以幫助React應用程序實現single-spa 需要的生命周期函數(bootstrap、mount 和 unmount)。

import React from 'react';
import ReactDOM from 'react-dom';
import rootComponent from './path-to-root-component.js';

import singleSpaReact from 'single-spa-react';
const reactLifecycles = singleSpaReact({
    React,
    ReactDOM,
    rootComponent,
    errorBoundary(err, info, props) {
        // https://reactjs.org/docs/error-boundaries.html
        return (
            <div>This renders when a catastrophic error occurs</div>
        );
    },
});
export const bootstrap = reactLifecycles.bootstrap;
export const mount = reactLifecycles.mount;
export const unmount = reactLifecycles.unmount;

這是官網提供的示例代碼,那個rootComponent就是我們的頂層React組件。

其它的就不用多說了,畢竟都很容易理解,想要了解詳情可以看下:詳情

其它的一些語言比如vue和Angular都有自己對應的輔助庫,具體可以查閱:輔助庫列表

single-spa的Parcels

Parcels是single-spa的一個高級特性,是一個與框架無關的組件,與應用的不同之處在於parcel組件需要手動掛載,而不是通過匹配路由的activity方法被激活。

官網說:只有在涉及到跨框架的應用之間進行組件調用時,我們才需要考慮parcel的使用。

一想到我們公司只用react,那么打擾了,再見。

CLI工具:create-single-spa和可定制化的webpack配置

上面很多都是在講原理,現在到了實際應用的時候了。

single-spa的相關配置有些繁瑣,所以我推薦依賴現有的CLI去新建項目,可以去改造,而不是自己從零開始去搭建。

npx create-single-spa

運行上面這行命令后,就會讓你做一系列選擇,那些選擇就不多說,只說最關鍵的。

create-single-spa 可以讓你創建三種項目,分別是:

  • single-spa application/parcel :應用和parcel。
  • single-spa root config:基座。
  • in-browser utility module: 通用組件,工具函數,樣式指引。

你可以根據需要去創建不同的項目。

single-spa還提供了一些推薦的webpack配置庫,不用自己操心去設置webpack配置。

不過我建議最后輸出得到webpack配置你還是稍微打印出來看一下,做到心中有數,然后才可以再根據它的配置去做相應修改。

Demo分享

光看不做假把式,下面是我自己根據single-spa的CLI工具搭建的兩個簡易Demo:

同時運行起來即可,命令都是:

yarn start

如果確實想入門的話,對比一下我寫的和官方CLI工具初始化時的一些差異,可以了解到更多的一些小細節。

總結

總的來說,single-spa是一個非常優秀的微前端框架。

微前端領域最近的趨勢是用webpack5中模塊聯合特性來實現,這與single-spa並不沖突,single-spa也有結合模塊聯合特性實現的例子。

不過這就不在本篇文章的涉及范圍內了,也許以后會寫下這塊的內容。

本篇博客到此結束。

希望這篇文章能給您帶來一些幫助,如有疏漏,也請不吝賜教。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM