Single-Spa + Vue Cli 微前端落地指南 + 視頻 (項目隔離遠程加載,自動引入)

概念

什麼是 single-spa?

single-spa 一個基於JavaScript的 微前端 框架,他能夠用於構建可共存的微前端應用,每一個前端應用均可以用本身的框架編寫,完美支持 Vue React Angular。能夠實現 服務註冊 事件監聽 子父組件通訊 等功能。javascript

用於 父項目 集成子項目使用css

什麼是 single-spa-vue ?

single-spa-vue 是提供給使用vue子項目使用的npm包。他能夠快速和sigle-spa父項目集成,並提供了一些比較便攜的api。html

用於 子項目 使用前端

咱們要實現的

  • vue-cli 與 single-spa 集成
  • 遠程加載服務
  • manifest 自動加載須要的 JS
  • namespace 樣式隔離
  • 兼容性問題解決

父項目的處理

初始化項目

咱們父項目和子項目都使用vue-cli進行集成。父項目爲了美化,用ant-design-vue作前端框架。vue

新建一個項目,名稱叫 parent。咱們爲了方便,暫時不引入vuexeslint。記得,父項目的 vue-router 要開啓history模式。java

接着咱們安裝ant-design-vuesingle-spa,而後啓動項目。node

npm install ant-design-vue single-spa --save -d
複製代碼

父項目註冊子項目路由

咱們註冊一個子服務路由,只是註冊, 不填寫component字段。react

{
    path: '/vue',
    name: 'vue',
  }
複製代碼

搭建基礎框架

咱們在父項目的入口 vue組件,簡單地寫一下咱們的基礎佈局。左邊爲菜單欄,右邊是佈局欄。webpack

左邊菜單欄內有一項vue列表項,vue 裏面有2個路由。分別是子項目的 homeabout. 右側內容欄內,增長一個id爲 single-vue 的dom元素,這是咱們稍後子項目要掛載的目標dom元素。ios

<template>
  <a-layout id="components-layout-demo-custom-trigger">
    <a-layout-sider :trigger="null" collapsible v-model="collapsed">
      <div class="logo" />
      <a-menu theme="dark" mode="inline">
        <a-sub-menu key="1">
          <span slot="title">
            <a-icon type="user" />
            <span>Vue</span>
          </span>
          <a-menu-item key="1-1">
            <a href="/vue#">
              Home
            </a>
          </a-menu-item>
          <a-menu-item key="1-2">
            <a href="/vue#/about">
              About
            </a>
          </a-menu-item>
        </a-sub-menu>
      </a-menu>
    </a-layout-sider>
    <a-layout>
      <a-layout-header style="background: #fff; padding: 0" />
      <a-layout-content :style="{ margin: '24px 16px', padding: '24px', background: '#fff', minHeight: '280px' }">
        <div class="content">
          <!--這是右側內容欄-->
          <div id="single-vue" class="single-spa-vue">
            <div id="vue"></div>
          </div>
        </div>
      </a-layout-content>
    </a-layout>
  </a-layout>
</template>
<script>
  export default {
    data() {
      return {
        collapsed: false,
      };
    }
  };
</script>
<style>
  #components-layout-demo-custom-trigger .trigger {
    font-size: 18px;
    line-height: 64px;
    padding: 0 24px;
    cursor: pointer;
    transition: color 0.3s;
  }

  #components-layout-demo-custom-trigger .trigger:hover {
    color: #1890ff;
  }

  #components-layout-demo-custom-trigger .logo {
    height: 32px;
    background: rgba(255, 255, 255, 0.2);
    margin: 16px;
  }
</style>
複製代碼

註冊子項目

這裏就是咱們的重頭戲:如何使用single-spa註冊子項目。在註冊以前,咱們先了解一下2個api:

singleSpa.registerApplication:這是註冊子項目的方法。參數以下:

  • appName: 子項目名稱
  • applicationOrLoadingFn: 子項目註冊函數,用戶須要返回 single-spa 的生命週期對象。後面咱們會介紹single-spa的生命週期機制
  • activityFn: 回調函數入參 location 對象,能夠寫自定義匹配路由加載規則。

singleSpa.start:這是啓動函數。

咱們新建一個 single-spa-config.js,並在main.js內引入。

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import Ant from 'ant-design-vue';
import './single-spa-config.js'
import 'ant-design-vue/dist/antd.css';
Vue.config.productionTip = false;

Vue.use(Ant);

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
複製代碼

single-spa-config.js:

// single-spa-config.js
import * as singleSpa from 'single-spa'; //導入single-spa
/*
* runScript:一個promise同步方法。能夠代替建立一個script標籤,而後加載服務
* */
const runScript = async (url) => {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = url;
        script.onload = resolve;
        script.onerror = reject;
        const firstScript = document.getElementsByTagName('script')[0];
        firstScript.parentNode.insertBefore(script, firstScript);
    });
};

singleSpa.registerApplication( //註冊微前端服務
    'singleDemo', 
    async () => {
        await runScript('http://127.0.0.1:3000/js/chunk-vendors.js');
        await runScript('http://127.0.0.1:3000/js/app.js');
        return window.singleVue;
    },
    location => location.pathname.startsWith('/vue') // 配置微前端模塊前綴
);

singleSpa.start(); // 啓動
複製代碼

與官方文檔不一樣的是,咱們這裏使用了 遠程加載。遠程加載的原理,咱們後面會單獨寫。

父項目就處理完畢了,接下來咱們處理子項目。

子項目的處理

初始化項目

子項目的處理,比父項目就稍微複雜一些。

咱們仍是新建一個項目,叫作vue-child,使用 vue create vue-child 建立。子項目的建立過程,就隨意了,這裏咱們忽略過程。

另外,咱們須要安裝一個叫作 single-spa-vue 的npm包。

npm install single-spa-vue --save -d
複製代碼

single-spa-vue

若是想註冊爲一個子項目,還須要 single-spa-vue 的包裝。

main.js中引入 single-spa-vue,傳入Vue對象和vue.js掛載參數,就能夠實現註冊。它會返回一個對象,裏面有single-spa 須要的生命週期函數。使用export導出便可

import singleSpaVue from "single-spa-vue";
import Vue from 'vue'

const vueOptions = {
    el: "#vue",
    router,
    store,
    render: h => h(App)
};

// singleSpaVue包裝一個vue微前端服務對象
const vueLifecycles = singleSpaVue({
    Vue,
    appOptions: vueOptions
});

// 導出生命週期對象
export const bootstrap = vueLifecycles.bootstrap; // 啓動時
export const mount = vueLifecycles.mount; // 掛載時
export const unmount = vueLifecycles.unmount; // 卸載時

export default vueLifecycles;

複製代碼

webpack的處理

只是導出了,還須要掛載到window

在項目目錄下新建 vue.config.js, 修改咱們的webpack配置。咱們修改webpack output內的 librarylibraryTarget 字段。

  • output.library: 導出的對象名
  • output.libraryTarget: 導出後要掛載到哪裏

同時,由於咱們是遠程調用,還須要設置 publicPath 字段爲你的真實服務地址。不然加載子chunk時,會去當前瀏覽器域名的根路徑尋找,有404問題。 由於咱們本地的服務啓動是localhost:3000,因此咱們就設置 //localhost:3000

module.exports = {
    publicPath: "//localhost:3000/",
    // css在全部環境下,都不單獨打包爲文件。這樣是爲了保證最小引入(只引入js)
    css: {
        extract: false
    },
    configureWebpack: {
        devtool: 'none', // 不打包sourcemap
        output: {
            library: "singleVue", // 導出名稱
            libraryTarget: "window", //掛載目標
        }
    },
    devServer: {
        contentBase: './',
        compress: true,
    }
};

複製代碼

咱們執行 vue-cli-service serve --port 3000後,就能夠看到一直等待的界面了~

其中,左側能夠切換子項目中的路由。右側聯網加載。

這樣,咱們的初版就大功告成了。接下來,咱們作進一步優化和分享

樣式隔離

樣式隔離這塊,咱們使用postcss的一個插件:postcss-selector-namespace。 他會把你項目裏的全部css都會添加一個類名前綴。這樣就能夠實現命名空間隔離

首先,咱們先安裝這個插件:npm install postcss-selector-namespace --save -d

項目目錄下新建 postcss.config.js,使用插件:

// postcss.config.js

module.exports = {
  plugins: {
    // postcss-selector-namespace: 給全部css添加統一前綴,而後父項目添加命名空間
    'postcss-selector-namespace': {
      namespace(css) {
        // element-ui的樣式不須要添加命名空間
        if (css.includes('element-variables.scss')) return '';
        return '.single-spa-vue' // 返回要添加的類名
      }
    },
  }
}
複製代碼

在父項目要掛載的區塊,添加咱們的命名空間。結束

獨立運行

你們可能會發現,咱們的子服務如今是沒法獨立運行的,如今咱們改造爲能夠獨立 + 集成雙模式運行。

single-spa 有個屬性,叫作 window.singleSpaNavigate。若是爲true,表明就是single-spa模式。若是false,就能夠獨立渲染。

咱們改造一會兒項目的main.js

// main.js
const vueOptions = {
  el: "#vue",
  router,
  render: h => h(App)
};

/**** 添加這裏 ****/
if (!window.singleSpaNavigate) { // 若是不是single-spa模式
  delete vueOptions.el;
  new Vue(vueOptions).$mount('#vue');
}
/**** 結束 ****/

// singleSpaVue包裝一個vue微前端服務對象
const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: vueOptions
});
複製代碼

這樣,咱們就能夠獨立訪問子服務的 index.html 。不要忘記在public/index.html裏面添加命名空間,不然會丟失樣式。

<div class="single-spa-vue">
    <div id="app"></div>
</div>
複製代碼

須要瞭解的知識點

遠程加載

在這裏,咱們的遠程加載使用的是async await構建一個同步執行任務。

建立一個script標籤,等script加載後,返回script加載到window上面的對象。

/*
* runScript:一個promise同步方法。能夠代替建立一個script標籤,而後加載服務
* */
const runScript = async (url) => {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = url;
        script.onload = resolve;
        script.onerror = reject;
        const firstScript = document.getElementsByTagName('script')[0];
        firstScript.parentNode.insertBefore(script, firstScript);
    });
};
複製代碼

vue 和 react/angular 掛載的區別

Vue 2.x的dom掛載,採起的是 覆蓋Dom掛載 的方式。例如,組件要掛載到#app上,那麼它會用組件覆蓋掉#app元素。

可是React/Angular不一樣,它們的掛載方式是在目標掛載元素的內部添加元素,而不是直接覆蓋掉。 例如組件要掛載到#app上,那麼他會在#app內部掛載組件,#app還存在。

這樣就形成了一個問題,當我從 vue子項目 => react項目 => vue子項目時,就會找不到要掛載的dom元素,從而拋出錯誤。

解決這個問題的方案是,讓 vue項目組件的根元素類名/ID名和要掛載的元素一致 就能夠。

例如咱們要掛載到 #app 這個dom上,那麼咱們子項目內部的app.vue,最頂部的dom元素id名也應該叫 #app

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>
複製代碼

manifest 自動加載 bundle和chunk.vendor

在上面父項目加載子項目的代碼中,咱們能夠看到。咱們要註冊一個子服務,須要一次性加載2個JS文件。若是須要加載的JS更多,甚至生產環境的 bundle 有惟一hash, 那咱們還能寫死文件名和列表嗎?

singleSpa.registerApplication(
    'singleVue',
    async () => {
        await runScript('http://127.0.0.1:3000/js/chunk-vendors.js'); // 寫死的文件列表
        await runScript('http://127.0.0.1:3000/js/app.js');
        return window.singleVue;
    },
    location => location.pathname.startsWith('/vue') 
);

複製代碼

咱們的實現思路,就是讓子項目使用 stats-webpack-plugin 插件,每次打包後都輸出一個 只包含重要信息的manifest.json文件。父項目先ajax 請求 這個json文件,從中讀取出須要加載的js目錄,而後同步加載。

stats-webpack-plugin

這裏就不得不提到這個webpack plugin了。它能夠在你每次打包結束後,都生成一個manifest.json 文件,裏面存放着本次打包的 public_path bundle list chunk list 文件大小依賴等等信息。

{
  "errors": [],
  "warnings": [],
  "version": "4.41.4",
  "hash": "d0601ce74a7b9821751e",
  "publicPath": "//localhost:3000/",
  "outputPath": "/Users/janlay/juejin-single/vue-chlid/dist",
  "entrypoints": { // 只使用這個字段
    "app": {
      "chunks": [
        "chunk-vendors",
        "app"
      ],
      "assets": [
        "js/chunk-vendors.75fba470.js",
        "js/app.3249afbe.js"
      ],
      "children": {},
      "childAssets": {}
    }
    ... ...
  }
複製代碼

咱們切換到子項目的目錄,安裝這個webpack插件:

npm install stats-webpack-plugin --save -d
複製代碼

vue.config.js中使用:

{
    configureWebpack: {
        devtool: 'none',
        output: {
            library: "singleVue",
            libraryTarget: "window",
        },
        /**** 添加開頭 ****/
        plugins: [
            new StatsPlugin('manifest.json', {
                chunkModules: false,
                entrypoints: true,
                source: false,
                chunks: false,
                modules: false,
                assets: false,
                children: false,
                exclude: [/node_modules/]
            }),
        ]
        /**** 添加結尾 ****/
    }
}
複製代碼

具體的配置項,能夠訪問 webpack 中文文檔 - configuration - stats 查閱

父項目改造

固然,父項目中的單runScript已經沒法支持使用了,寫個getManifest方法,處理一下。

/*
* getManifest:遠程加載manifest.json 文件,解析須要加載的js
* url: manifest.json 連接
* bundle:entry名稱
* */
const getManifest = (url, bundle) => new Promise(async (resolve) => {
    const { data } = await axios.get(url);
    const { entrypoints, publicPath } = data;
    const assets = entrypoints[bundle].assets;
    for (let i = 0; i < assets.length; i++) {
        await runScript(publicPath + assets[i]).then(() => {
            if (i === assets.length - 1) {
                resolve()
            }
        })
    }
});
複製代碼

咱們首先ajax到 manifest.json 文件,解構出裏面的 entrypoints publicPath字段,遍歷出真實的js路徑,而後按照順序加載。

async () => {
    let singleVue = null;
    await getManifest('http://127.0.0.1:3000/manifest.json', 'app').then(() => {
        singleVue = window.singleVue;
    });
    return singleVue;
},
複製代碼

其實,若是要作一個 微前端管理平臺,也是靠這個實現。

single-spa 的生命週期和實現原理

這裏推薦一位dalao的原理分享:連接

展望

通訊

可使用發佈訂閱模式實現,也能夠實現一個相似於vuex的狀態管理

js沙箱實現狀態管理

這個可使用proxy 進行監聽,切換保存,切入還原狀態

代碼倉庫 & 視頻 & 最後

本文代碼倉庫:碼雲

視頻:百度雲 密碼:nmne

歡迎加入微前端討論羣,一塊兒研究。

相關文章
相關標籤/搜索