微前端的生產實踐和個人使用姿式

前言javascript

筆者在2019年底的時候開始瞭解微前端這個東西、當時看到兩個微前端的框架、分別是 single-spa 和 qiankun、在這兩種技術去作選擇去學習、看到qiankun是基於single-spa二次封裝的、文檔簡潔明瞭、使用簡單後面決定學習qiankun!並把本身踩的坑記錄下來 css

什麼是微前端

本身在服務器部署的一個微前端demohtml

一、技術棧無關前端

二、主框架不限制接入應用的技術棧,微應用具有徹底自主權vue

三、獨立開發、獨立部署java

四、微應用倉庫獨立,先後端可獨立開發,部署完成後主框架自動完成同步更新node

需求分析

  • 除了這些、從項目的需求和系統的數量微前端很是適合咱們、咱們一共有七個系統、每一個用戶由於角色權限、所管理的系統也是不同的、張三負責兩個系統權限、李四負責一個系統、也可能王五負責一個系統的其中某幾個菜單權限等...若是所有系統寫到一個項目可想而至...代碼量...項目維護..性能.都是很是難折騰!下面放一張咱們的系統UI圖

因爲那啥因此打了碼、頂部是全部系統、左側是當前系統的菜單欄、從UI的設計圖上看這個項目是很適合微前端!後面我會用基座(微前端環境)和子應用與主應用去介紹個人踩坑之路-😄react

技術選型與項目的總體規劃

  • vue、element、webpack、websocket、eslint、babel、qiankun2.0
  • 支持子應用獨立運行和可運行在微前端基座方式
  • 主應用使用cdn統一管理公共靜態資源,全部子應用運行在基座上時共享此靜態資源,大幅減少子應用體積,減小帶寬消耗,減小重複資源消耗,大幅加快項目加載速度
  • 應用與應用之間可進行通訊和跳轉
  • 應用獨立維護、互不依賴不耦合
  • 項目拆分但和單體的開發模式應該是差很少的、好比啓動、打包、安裝、依賴、部署(一鍵模式)
  • 編寫一鍵部署腳本、先部署至測試服務器、測試經過直接發行到生產環境
  • 項目狀態、公共數據的維護

主應用環境搭建(基座)

  • 主應用(vue腳手架搭建)、由於打算核心公共模塊採用cdn方式、因此腳手架選擇 : Default ([Vue 2] babel, eslint) 主應用須要作的事情是: 對qiankun框架單獨模塊化封裝導出核心方法、配置cdn方式加載核心模塊 、配置 eslint 忽略指定全局變量、配置webpack的externals排除某些依賴,使用 cdn 資源代替

開始

vue create main-app 
複製代碼

腳手架好了以後咱們須要安裝 qiankunlinux

yarn add qiankun
複製代碼
  • 採用cdn方式去加載公共核心模塊好比vue、vuex、vue-route..等、這樣作的目的是讓子應用去使用主應用加載好的公共模塊、同時減小項目打包體積大小!因此咱們須要在main.js裏的 import Vue from 'vue' 進行刪除、其餘的vue-router、vuex、Axios也都是同樣的操做統一不使用 node_modules的依賴,而後在主應用的 public的index.html、引入公共模塊(下方的js) 下面是我本身玩demo的時候儲存在個人對象服務器的經常使用公共文件、建議下載到本身本地玩
<script src="https://gf-cdn.oss-cn-beijing.aliyuncs.com/vue/vue.js"></script>
<script src="https://gf-cdn.oss-cn-beijing.aliyuncs.com/vue/vue-router.js"></script>
<script src="https://gf-cdn.oss-cn-beijing.aliyuncs.com/vue/vuex.js"></script></head>
<script src="https://gf-cdn.oss-cn-beijing.aliyuncs.com/axios/axios.min.js"></script>
<link rel="stylesheet" href="https://gf-cdn.oss-cn-beijing.aliyuncs.com/element/index.css">
<script src="https://gf-cdn.oss-cn-beijing.aliyuncs.com/element/index.js"></script>
複製代碼

由於使用了 eslint 緣由檢測到沒有引入vue、因此咱們要全局配置忽略咱們經過cdn方式引入的模塊、在 .eslintrc.js 添加一個 globals 忽略檢測的全局變量webpack

globals: {
   "Vue": true,
   "Vuex": true,
   "VueRouter": true,
   'axios':true
 }
複製代碼

配置了這個只是把代碼校驗忽略檢測某些變量、咱們還須要配置下 webpack 的 externals externals介紹、簡單來說就是 打包的時候排除某些依賴,使用 cdn 資源代替 在vue.config.js裏面配置

module.exports = {
  publicPath: '/',
  outputDir: 'app',
  assetsDir: 'static', 
  ......
  configureWebpack: {
        externals: {
            'element-ui':'ELEMENT',
            'vue':'Vue',
            'vue-router':'VueRouter',
            'vuex': 'Vuex',
            'axios':'axios'
        }
    }
 }
複製代碼

這樣咱們的cdn方式加載核心模塊就行了、接下來就是配置 qiankun

  • quankun配置

    • src裏面新建一個core文件夾、分別建立 app.config.js(管理子應用的註冊信息)qiankun.js(這裏統一導出啓動qiankun的方法) 還有 app.store.js(管理qiankun的通訊方法)

app.config.js

const apps = [
    {
      name: "subapp-sys", //微應用的名稱
      defaultRegister: true, //默認註冊
      devEntry: "http://localhost:6002",//開發環境地址
      depEntry: "http://108.54.70.48:6002",//生產環境地址
      routerBase: "/sys", //激活規則路徑
      data: []  //傳入給子應用的數據
    },
]
export default apps;
複製代碼

qiankun.js

import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from "qiankun";
const appContainer = "#subapp-viewport"; //加載子應用的dom
import appStore from './app.store' 
const quanKunStart = ( list ) =>{
    let apps = [];      //子應用數組盒子
    let defaultApp = null; // 默認註冊應用路由前綴
    let isDev = process.env.NODE_ENV === 'development';
    list.forEach( i => { 
        apps.push({
            name: i.name, //微應用的名稱
            entry: isDev ? i.devEntry : i.depEntry, //微應用的 entry 地址
            container: appContainer, //微應用的容器節點的選擇器或者 Element 實例
            activeRule: i.routerBase, //微應用的激活規則路徑 /login/xxx /sys/xxx
            props: { routes: i.data, routerBase: i.routerBase } //子應用初次掛載傳入給子應用的數據
        })
        //初始化第一個加載的應用
        if (i.defaultRegister) defaultApp = i.routerBase;
    });
    //qiankun路由配置
    registerMicroApps(
        apps,
        {
            beforeLoad: [
                app => {
                    console.log('[主應用生命週期] before', app.name);
                },
            ],
            beforeMount: [
                app => {
                    console.log('[主應用生命週期] before', app.name);
                },
            ],
            afterUnmount: [
                app => {
                    console.log('[主應用生命週期] after', app.name);
                },
            ]
        },
    )
    //默認加載第一個子應用
    setDefaultMountApp( defaultApp );
    //啓動微前端
    start();
    //第一個微應用 mount 後須要調用的方法
    runAfterFirstMounted(() => { console.log( defaultApp +'--->子應用開啓成功' ) });
    //啓動qiankun通訊機制
    appStore( initGlobalState );
}

export default quanKunStart;

複製代碼

app.store.js

let DISPATCHAPPLYMESSAGE = null;
let GETAPPLYMESSAGE = null;
const appStore = ( initGlobalState ) => {
    //定義應用之間所接收的key、否則主應用不接收數據
    const initialState = {  
        data: '給子應用的測試數據',
        token: '',
        appsRefresh: false,
    };
    const { onGlobalStateChange, setGlobalState } = initGlobalState( initialState );
    dispatchApplyMessage = setGlobalState;
    getApplyMessage = onGlobalStateChange;
}
//導出應用通訊方法
export {
    DISPATCHAPPLYMESSAGE,
    GETAPPLYMESSAGE
}
export default appStore;
複製代碼

qiankun的通訊是 initGlobalState 這個方法返回的 onGlobalStateChange, setGlobalState 接收和派發方法、另外須要注意的是 只有主應用註冊了 initGlobalState 纔會附加到子應用接收的props裏面、主應用沒註冊通訊方法是沒有的 還有一個就是 若是你沒先在 initGlobalState方法傳入定義好的通訊key、那其餘應用傳入給主應用的數據是接收不到的

  • 在主應用的App.vue添加子應用的渲染區域
<template>
    <div class="home-container"> <p>主應用內容</p> <div class="page-conten"> <!-- 子應用渲染區 --> <div id="subapp-viewport" class="app-view-box"></div> </div> </div>
</template>
複製代碼

main.js

import App from './App.vue'
Vue.config.productionTip = false

import Apps from './core/app.config'
import qianKunStart from './core/qiankun'
qianKunStart(Apps)

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

整個主應用(基座)配置完、能夠發現並無什麼難度、qiankun給我提供了直接開箱即用的方便、剩下的咱們就是去配置子應用了、配置子應用相對來講還要更簡單些、接下來就是子應用的環境搭建了

子應用環境

  • 第一步使用官方腳手架把項目建立好、和主應用同理選擇默認的 Default ([Vue 2] babel 進行建立項目
vue create subapp-sys
複製代碼
  • 第二步咱們改造下腳手架默認的模塊和打包後的格式配置、還有給qiankun導出對應的生命週期函數 修改打包配置 - vue.config.js
const { name } = require('./package');
module.exports = {
  devServer: {
    hot: true,
    disableHostCheck: true,
    port:6002,
    overlay: {
        warnings: false,
        errors: true,
    },
    headers: {
        'Access-Control-Allow-Origin': '*',
    },
    //防止單體項目刷新後404
    historyApiFallback:true,
},
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd',// 把微應用打包成 umd 庫格式
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};
複製代碼
  • 第三步同src下建立一個導出qiankun的js文件、統一管理,名叫 life-cycle.js
import App from "./App.vue";
import store from "./store";
import selfRoutes from "./router";
//導入官方通訊方法 和 主應用的同樣把應用通訊封裝到一個js文件獨立管理
import appStore from "./utils/app-store";

const __qiankun__ = window.__POWERED_BY_QIANKUN__;
let router = null;
let instance = null;

/** * @name 導出qiankun生命週期函數 */
const lifeCycle = () => {
  return {
    async bootstrap() {},
    //應用每次進入都會調用 mount 方法,一般咱們在這裏觸發應用的渲染方法
    async mount( props ) {
        // 註冊應用間通訊
        appStore(props);
        // 註冊微應用實例化函數
        render(props);
    },
    //微應用卸載
    async unmount() {
        instance.$destroy?.();
        instance = null;
        router = null;
    },
  //主應用手動更新微應用
    async update(props) {
        console.log("update props", props);
    }
  };
};

//子應用實例化函數 routerBase container是經過主應用props傳入過來的數據
const render = ({ routerBase, container } = {}) => {
    Vue.config.productionTip = false;
    router = new VueRouter({
        base: __qiankun__ ? routerBase : "/",
        mode: "history",
        routes: selfRoutes
    });
    instance = new Vue({
        router,
        store,
        render: h => h(App)
    }).$mount(container ? container.querySelector("#sys") : "#sys");
};

export { lifeCycle, render };
複製代碼
  • 第四步 接下來src目錄新增 public-path.js
if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
複製代碼
  • 最後main.js 引入封裝
import "./public-path";
import { lifeCycle, render } from "./life-cycle";
/** * @name 導出微應用生命週期 */
const { bootstrap, mount, unmount } = lifeCycle();
export { bootstrap, mount, unmount };

/** * @name 不在微前端基座獨立運行 */
const __qiankun__ = window.__POWERED_BY_QIANKUN__;
__qiankun__ || render();
複製代碼

子應用 life-cycle.js 中引入了 import appStore from "./utils/app-store";這裏的app-store和主應用的同樣、在相同的位置從新複製一份便可

個人整個項目結構(使用了vue + react + qiankun)

遇到的問題

qiankun 環境搭建好了,接下來 分別 進入主應用和子應用啓動項目 yarn serve 而後訪問主應用、沒有問題的話應該兩個項目的頁面都出來了、接下來我說下我作集成時候遇到的問題

問題一,掛載微應用的容器節點找不到 #subapp-viewport 、添加一個你設置應用掛載container的dom節點就好

問題二,主應用代理的地址若是和子應用proxy的接口匹配若是和路由前綴同樣的話、頁面進行一個刷新操做後的一個頁面錯誤 感謝wl提早踩坑、哈哈哈

配置主應用vue.config.js的devServer爲proxy添加一個函數繞過代理、瀏覽器請求,但願返回的是HTML頁面

問題三,某個子應用服務沒啓動、沒有獲取到資源

問題4、子應用給其餘應用傳輸數據時候、主應用裏面沒有提早定義通訊的key、因此接收不到數據、解決:在主應用註冊通訊方法 initGlobalState({...}) 定義好須要通訊的key就好、按定義好的約定進行傳輸數據

目前就遇到這些問題、也歡迎留言區評論本身遇到的問題、順便把qiankun的常見問題貼出來 qiankun.umijs.org/zh/faq

編寫應用指令腳本一鍵式 [ 啓動、依賴安裝、打包 】

上面講到、咱們須要一個一個應用下進行yarn serve下、這樣是很不方便的、應用一多咱們啓動就成了很麻煩的一件事情、因此咱們須要從新寫一個yarn腳本文件、目的就是讓他去自動幫咱們執行腳本命令 (其中包括 啓動 打包 安裝依賴)

  • 第一步在整個應用項目下生成一個package.json配置scripts腳本文件
yarn init //而後按提示執行下去
複製代碼
  • 添加scripts腳本配置 下面是定一個start指令而後去執行config下的start.js
"scripts": {
  "start":"node config/start.js"
}
複製代碼
  • 第二步在、咱們須要在package.json同級下建立一個config文件夾、同時往文件裏面添加一個start.js
mkdir config
cd config 
touch start.js
複製代碼
  • 第三步往start.js隨便輸出一個console.log('yarn serve'),而後在整個項目啓動終端執行一下 yarn start 正常輸出 yarn serve 、而後咱們須要開始編寫一鍵啓動腳本 需求就是執行腳本、腳本自動幫咱們在每一個項目中去執行 yarn serve

  • start.js

const fs = require('fs');
const path = require('path');
const util = require('util');
const sub_app_ath = path.resolve();
const sub_apps = fs.readdirSync(sub_app_ath).filter(i => /^sub|main/.test(i));
console.log('\033[42;30m 啓動中 \033[40;32m 即將進入全部模塊並啓動服務:' + JSON.stringify(sub_apps) + 'ing...\033[0m')
const exec = util.promisify( require('child_process').exec );
async function start() {
sub_apps.forEach( file_name => {
  exec('yarn serve', { cwd: path.resolve( file_name )});
});
};
start();
setTimeout( () =>{
console.log('\033[42;30m 訪問 \033[40;32m http://localhost:6001 \033[0m')
},5000)

複製代碼

先經過正則讀取到主應用和子應用文件夾名稱、而後使用 child_process模塊異步建立子進程 經過這個返回的方法咱們能夠去執行一個 指令 而且傳入一個在那執行的路徑 util.promisify把方法封裝成promise返回形式

這裏我有個小問題、我有嘗試過去找每個子應用是否成功開啓的操做、可是沒找到合適的方法、但願有人知道的能夠告知我下啦、謝謝、因此我在最後寫了一個setTimeout....

好啦、目前一鍵啓動就寫完了、其餘的都是同樣的操做、只是建立的文件夾和scripts的腳本命令改下、哦對還有exec下的指令換成對應的 剩下就是執行shell腳本進行服務器上傳部署

shell腳本完成自動打包和上傳至服務器、關於shell語法你們能夠看 菜鳥shell教程

  • 在總體項目下新建一個 deploy.sh 文件

deploy.sh

set -e
shFilePath=$(cd `dirname $0`; pwd)
# 系統列表名稱
sysList=('app' 'car' 'login' 'sys' 'user' 'all')
IP="106.54.xx.xx"
uploadPath="/gf_docker/nginx/web"
#獲取當前分支
branch=$(git symbolic-ref --short HEAD)
#開始
echo "\033[35m 當前分支是:${branch} \033[0m"
read -p $'\033[36m 準備進行自動化部署操做、是否繼續 y or n \033[0m ' isbuild
if [ "$isbuild" != 'y' ];then
    exit
fi
echo "\033[36m 目前四個個系統 \033[0m \033[35m【 ${sysList[*]} 】 \033[0m "
read -p $'\033[36m 請選擇部署的項目 或 輸入 all \033[0m' changeSysName
isSys=$(echo "${sysList[@]}" | grep -wq "${changeSysName}" &&  echo "yes" || echo "no")
#是否存在系統
if [ "$isSys" == 'no' ];then
    echo "\033[31m 沒有對應的系統、已退出 \033[0m"
    exit
fi

#沒有buildFile文件夾的話就新建一個
if [ -d "$shFilePath/buildFile" ]; then
    rm -rf './buildFile/'
    mkdir "buildFile"  
else
    mkdir "buildFile" 
fi;

#項目文件夾名稱
fileName=""
#打包
function build() {
    cd $1
    echo "\033[32m $1準備打包... \033[0m" 
    yarn build 
    echo $1/$2
    mv $shFilePath/$1/$2 $shFilePath/buildFile
    echo "\033[32m $1打包成功、包移動至buildFile \033[0m" 
}
#上傳服務器
function uploadServe() {
    echo "\033[32m 準備上傳服務器,地址:$uploadPath \033[0m"
    rsync -a -e "ssh -p 22" $shFilePath/buildFile*  root@$IP:$uploadPath
    echo "\033[32m 自動化部署成功! \033[0m"
}
#單個項目部署文件名轉換
function getFileName() {
    case $1 in
        'app')
            fileName="main-app";;
        'car')
            fileName="subapp-car";;
        'login')
            fileName="subapp-login";;
        'sys')
            fileName="subapp-sys";;
        'user')
            fileName="subapp-user";;
        *)
            echo "error"
    esac
}
#按需打包
if [ "$changeSysName" == 'all' ];then
    for i in "${sysList[@]}"; do
        if [ "$i" != 'app' ];then
            cd ..
        fi
        if [ "$i" != 'all' ];then
            getFileName $i
            build $fileName $i
        fi
    done
else
    getFileName $changeSysName
    build $fileName $changeSysName
fi

#部署
uploadServe 
複製代碼

語法和菜鳥現學的、也只是代替雙手進行一系列的操做、上傳服務器的時候須要輸入下密碼、若是不想輸入可在服務端配置密鑰、相似git同樣!

最後咱們也能夠經過配置、腳本指令去執行咱們的sh文件、在package.json的scripts添加一個"deploy": "sh deploy.sh" 最後須要部署測試環境的時候直接執行 yarn deploy

最後

最後我要去進行項目的重構工做了、這些也是我下班後本身經過整理本身玩的demo進行的寫的一篇踩坑文章、我相信在重構公司項目的時候踩的坑確定不止這些到時候我統一在、遇到的問題那進行補充!加油、折騰人

相關文章
相關標籤/搜索