前端微服務初試(singleSpa)


1.基本概念

實現一套微前端架構,可以把其分成四部分(參考:https://alili.tech/archive/11052bf4/)

加載器:也就是微前端架構的核心,主要用來調度子應用,決定何時展示哪個子應用, 可以把它理解成電源。

包裝器:有了加載器,可以把現有的應用包裝,使得加載器可以使用它們,它相當於電源適配器。

主應用:一般是包含所有子應用公共部分的項目—— 它相當於電器底座

子應用:眾多展示在主應用內容區的應用—— 它相當於你要使用的電器

所以是這么個概念:電源(加載器)→電源適配器(包裝器)→️電器底座(主應用)→️電器(子應用)️

總的來說是這樣一個流程:用戶訪問index.html后,瀏覽器運行加載器的js文件,加載器去配置文件,然后注冊配置文件中配置的各個子應用后,首先加載主應用(菜單等),再通過路由判定,動態遠程加載子應用。

2.預備知識

2.1 SystemJs

SystemJS提供通用的模塊導入途徑,支持傳統模塊和ES6的模塊。

SystemJs有兩個版本,6.x版本是在瀏覽器中使用的,0.21版本的是在瀏覽器和node環境中使用的,兩者的使用方式不同。(參考:https://github.com/systemjs/systemjs)

在微服務中主要充當加載器的角色。

2.2 singleSpa

single-spa是一個在前端應用程序中將多個javascript應用集合在一起的框架。主要充當包裝器的角色。(參考:https://single-spa.js.org/docs/getting-started-overview.html

3.微服務實踐

3.1 創建應用

首先創建一個主應用iframe,這個主應用只需要簡單的起一個服務訪問靜態資源即可。

用npm init初始化,創建一個index.html,簡單寫個hello world,安裝依賴 npm i serve --s。

在package.json中的scripts中增加啟動命令"serve": "serve -s -l 7000"。運行后可以看到hello world。

 

然后在創建3個子應用,我用的是vue-cli2.0,分別創建navbar應用(用來寫路由),program1(應用1),program2(應用2)。

navbar應用中只放兩個鏈接就好了,如圖:

  

 子應用program1和program2的路由都相應的加上項目名稱前綴,都增加一個about路由來作為子應用的路由切換,大致如下:

 

 

3.2 改造子應用

首先包裝子應用,各個子應用都需要安裝依賴 npm i single-spa-vue systemjs-webpack-interop,修改入口文件main.js如下:

 

 

其中 single-spa-vue是針對vue項目的包裝器,systemjs-webpack-interop是社區維護的npm庫,它可以幫助您使webpack和systemjs一起正常工作。 

除此之外還需要在webpack配置中的output中增加設置

 

 

 3.3 修改主應用文件

<!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>Document</title>
    <meta name="importmap-type" content="systemjs-importmap">
    <script type="systemjs-importmap">
        {
          "imports": {
            "navbar": "http://localhost:8080/app.js",
            "program1": "http://localhost:8081/app.js",
            "program2": "http://localhost:8082/app.js",
            "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js"
          }
        }
      </script>
    <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js" as="script" crossorigin="anonymous" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-exports.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-register.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js"></script>
</head>
<body>
    <script>
        (function(){
            System.import('single-spa')
            .then((res)=>{
                var singleSpa=res;
            
                singleSpa.registerApplication('navbar',()=>System.import('navbar'),location=>true);

                singleSpa.registerApplication('program1',()=>System.import('program1'),(location)=>{
                    return location.hash.startsWith(`#/program1`);
                });

                singleSpa.registerApplication('program2',()=>System.import('program2'),(location)=>{
                    return location.hash.startsWith(`#/program2`);
                });

                singleSpa.start();
            })
        })()
    </script>
</body>
</html>
registerApplication函數包含四個參數,
appName: 注冊的應用名稱;
applicationOrLoadingFn:應用入口文件(必須是一個函數,返回一個函數或者一個promise);
activityFn:控制應用是否激活的函數(必須是一個純函數,接受window.location作為參數,返回一個boolean);
customProps:在包裝器生命周期函數中傳遞給子應用的props(應該是一個對象,可選)。
 
補充:有些小伙伴說運行demo會報錯,這里說明下,因為后面把公共依賴抽離出去了,所以這里可能需要加上公共依賴。

 

 

 
 
start函數必須在子應用加載完后才能調用。在調用之前,子應用已經加載了只是未被渲染。
 
3.4 運行項目
分別運行navbar,program1,progarm2項目,然后運行iframe項目。iframe項目運行在7000端口,其他子應用分別運行在8080,8081,8082端口。從7000端口去請求其他端口的入口文件會跨域,所以在子應用中增加跨域設置。
在子應用的webpack.dev.config.js中找到devSever配置項,增加headers:{"Access-Control-Allow-Origin":"*"}配置
然后重新運行項目即可。
 

 4.項目整合

以上只是在開發環境中使用,接下來嘗試不分別啟動服務,只啟用一個服務來跑項目。大體思路是使用express搭建一個服務,將子應用全部打包到項目上作為靜態資源訪問,入口html使用ejs模板來實現項目配置,而不再寫死。

4.1 使用express生成器生成項目

4.2 修改子應用打包配置

 

 

 這樣子應用就全部打包到express應用中作為靜態資源使用了。

4.3 增加應用配置文件

將iframe的index.html的內容復制到express的入口ejs中。增加配置文件apps.config.json和apps.config.js。

 

 

 apps.config.js作用就是根據apps.config.json在靜態資源文件夾下生成一份新的配置文件,將配置文件中的資源名稱通過正則匹配成完整的資源路徑。並且監聽文件變化來更新靜態資源文件夾下的配置文件。

生成的配置文件

 

 

4.4根據這個生成的配置文件去修改ejs,將項目注冊過程循環出來,而不再是寫死的。 

<!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>Document</title>
    <meta name="importmap-type" content="systemjs-importmap">
    <script type="systemjs-importmap">
        {
          "imports": {
            <%for(var i=0;i<apps.length;i++){%>
              "<%= apps[i].name %>":"<%= apps[i].server %><%=apps[i].resourceEntryUrl %>",
            <%}%>
            "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js"
          }
        }
      </script>
    <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js" as="script" crossorigin="anonymous" />
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-exports.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-register.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js"></script>
</head>
<body>
    <script>
        (function(){
            Promise.all([
                System.import('single-spa'),
                System.import('./apps.config.json')
            ])
            .then((res)=>{
                var singleSpa=res[0];
                var configs=res[1].default;
            
                configs.apps.forEach( project => {
                  if(project.resource.length>0){
                    Promise.all(project.resource.map(i=>{
                      return System.import(project.server+i)
                    })).then(function(){
                      singleSpa.registerApplication(project.name,()=>System.import(project.name),(location)=>{
                          return project.base?true:location.hash.startsWith(`#/${project.name}`);
                      });
                    })
                  }else{
                    singleSpa.registerApplication(project.name,()=>System.import(project.name),(location)=>{
                        return project.base?true:location.hash.startsWith(`#/${project.name}`);
                    });
                  }
                });

                singleSpa.start();
            })
        })()
    </script>
</body>
</html>

實際渲染出來是

 

 

 4.5 優化打包配置

因為三個子項目都用到了相同的一部分依賴,可以考慮將公用的依賴不打包進去,改為在主項目主引入來提高打包效率

修改子應用的webpack.base.config.js,增加配置項

 

 在主應用中引入依賴

 

 4.5 增加react應用

同樣是使用webpack打包,不同是包裝器不一樣。其他基本上是一樣的思路即可。

 

 最終demo

 

很多技術細節寫的很馬虎,就不拿出來獻丑啦~

 

補充說明:關於應用間通信或者狀態共享的問題,貼一下官方的說明:

通常,我們建議嘗試避免這種情況-將這些應用程序耦合在一起。如果您發現自己經常在應用程序之間執行此操作,則可能要考慮那些單獨的應用程序實際上應該只是一個應用程序。

通常,最好僅針對每個應用程序需要的數據發出API請求,即使其他應用程序已經請求了其中的一部分。實際上,如果您正確地設計了應用程序邊界,最終將只有很少的真正共享的應用程序狀態-例如,您的朋友列表與社交訂閱源具有不同的數據要求。

但是,這並不意味着它無法完成。這里有幾種方法:

1.創建一個共享的API請求庫,該庫可以緩存請求及其響應。如果somone命中某個API,然后另一個應用程序再次命中該API,則它僅使用緩存

2。公開共享狀態作為導出,其他庫可以導入它。可觀察對象(如RxJS)在這里很有用,因為它們可以向訂閱者流式傳輸新值。

3.使用自定義瀏覽器事件進行通信1.使用Cookie,本地/會話存儲或其他類似方法來存儲和讀取該狀態。這些方法最適用於不經常更改的事物,例如登錄的用戶信息。

 

已經上傳到github,https://github.com/sc1992sc/singleSpa


免責聲明!

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



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