微前端 qiankun-docker-nginx 配合 gitlab-ci-cd 的自動化部署的實現

來自:掘金,作者:紙上的彩虹

鏈接:https://juejin.cn/post/6981339862901194759

技術棧簡介

如果看完文章不是很理解,可以配合 [視頻解說查看本文] 視頻地址:https://www.bilibili.com/video/BV1Qg411u7C9

什麼是微前端

微前端是一種多個團隊通過獨立發佈功能的方式來共同構建現代化 web 應用的技術手段及方法策略。

微前端架構具備以下幾個核心價值:

什麼是 qiankun

qiankun 是一個生產可用的微前端框架,它基於 single-spa,具備 js 沙箱、樣式隔離、HTML Loader、預加載 等微前端系統所需的能力。qiankun 可以用於任意 js 框架,微應用接入像嵌入一個 iframe 系統一樣簡單。

qiankun 的核心設計理念

引用地址:qiankun.umijs.org/zh/guide

爲什麼不用 Iframe

引用地址:www.yuque.com/kuitos/gky7…

如果不考慮體驗問題,iframe 幾乎是最完美的微前端解決方案了。

iframe 最大的特性就是提供了瀏覽器原生的硬隔離方案,不論是樣式隔離、js 隔離這類問題統統都能被完美解決。但他的最大問題也在於他的隔離性無法被突破,導致應用間上下文無法被共享,隨之帶來的開發體驗、產品體驗的問題。

  1. url 不同步。瀏覽器刷新 iframe url 狀態丟失、後退前進按鈕無法使用。

  2. UI 不同步,DOM 結構不共享。想象一下屏幕右下角 1/4 的 iframe 裏來一個帶遮罩層的彈框,同時我們要求這個彈框要瀏覽器居中顯示,還要瀏覽器 resize 時自動居中。

  3. 全局上下文完全隔離,內存變量不共享。iframe 內外系統的通信、數據同步等需求,主應用的 cookie 要透傳到根域名都不同的子應用中實現免登效果。

  4. 慢。每次子應用進入都是一次瀏覽器上下文重建、資源重新加載的過程。

其中有的問題比較好解決 (問題 1),有的問題我們可以睜一隻眼閉一隻眼(問題 4),但有的問題我們則很難解決(問題 3) 甚至無法解決(問題 2),而這些無法解決的問題恰恰又會給產品帶來非常嚴重的體驗問題, 最終導致我們捨棄了 iframe 方案。

微前端的核心價值

www.yuque.com/kuitos/gky7…

項目的構想

在說具體技術實現前,我們先來看下我們想要實現個什麼東西。

微前端示意圖

子應用會根據主應用導航的點擊而動態加載

部署邏輯

部署的思路有很多,我這裏說說我嘗試過的方式:

qiankun

安裝 qiankun

$ yarn add qiankun # 或者 npm i qiankun -S

在主應用中註冊微應用

import { registerMicroApps, addGlobalUncaughtErrorHandler, start } from 'qiankun';

const apps = [
  {
    name: 'ManageMicroApp',
    entry: '/system/', // 本地開發的時候使用 //localhost:子應用端口
    container: '#frame',
    activeRule: '/manage',
  },
]

/**
 * 註冊微應用
 * 第一個參數 - 微應用的註冊信息
 * 第二個參數 - 全局生命週期鉤子
 */
registerMicroApps(apps,{
  // qiankun 生命週期鉤子 - 微應用加載前
  beforeLoad: (app: any) => {
    console.log("before load", app.name);
    return Promise.resolve();
  },
  // qiankun 生命週期鉤子 - 微應用掛載後
  afterMount: (app: any) => {
    console.log("after mount", app.name);
    return Promise.resolve();
  },
});

/**
 * 添加全局的未捕獲異常處理器
 */
addGlobalUncaughtErrorHandler((event: Event | string) => {
  console.error(event);
  const { message: msg } = event as any;
  // 加載失敗時提示
  if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
    console.error("微應用加載失敗,請檢查應用是否可運行");
  }
});

start();

當微應用信息註冊完之後,一旦瀏覽器的 url 發生變化,便會自動觸發 qiankun 的匹配邏輯,所有 activeRule 規則匹配上的微應用就會被插入到指定的 container 中,同時依次調用微應用暴露出的生命週期鉤子。

如果微應用不是直接跟路由關聯的時候,你也可以選擇手動加載微應用的方式:

import { loadMicroApp } from 'qiankun';


loadMicroApp({
  name: 'app',
  entry: '//localhost:7100',
  container: '#yourContainer',
});

微應用

微應用不需要額外安裝任何其他依賴即可接入 qiankun 主應用。

1. 導出相應的生命週期鉤子

微應用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 導出 bootstrapmountunmount 三個生命週期鉤子,以供主應用在適當的時機調用。

import Vue from 'vue';
import VueRouter from 'vue-router';

import './public-path';
import App from './App.vue';
import routes from './routes';
import SharedModule from '@/shared'; 

Vue.config.productionTip = false;

let instance = null;
let router = null;
// 如果子應用獨立運行則直接執行render
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

/**
 * 渲染函數
 * 主應用生命週期鉤子中運行/子應用單獨啓動時運行
 */
function render(props = {}) {
  // SharedModule用於主應用於子應用的通訊
  // 當傳入的 shared 爲空時,使用子應用自身的 shared
  // 當傳入的 shared 不爲空時,主應用傳入的 shared 將會重載子應用的 shared
  const { shared = SharedModule.getShared() } = props;
  SharedModule.overloadShared(shared);

  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? '/manage/' : '/',
    mode: 'history',
    routes
  });

  // 掛載應用
  instance = new Vue({
    router,
    render: (h) => h(App)
  }).$mount('#app');
}

/**
 * bootstrap 只會在微應用初始化的時候調用一次,下次微應用重新進入時會直接調用 mount 鉤子,不會再重複觸發 bootstrap。
 * 通常我們可以在這裏做一些全局變量的初始化,比如不會在 unmount 階段被銷燬的應用級別的緩存等。
 */
export async function bootstrap() {
  console.log('vue app bootstraped');
}
/**
 * 應用每次進入都會調用 mount 方法,通常我們在這裏觸發應用的渲染方法
 */
export async function mount(props) {
  console.log('vue mount', props);
  render(props);
}
/**
 * 應用每次 切出/卸載 會調用的方法,通常在這裏我們會卸載微應用的應用實例
 */
export async function unmount() {
  console.log('vue unmount');
  instance.$destroy();
  instance = null;
  router = null;
}
/**
 * 可選生命週期鉤子,僅使用 loadMicroApp 方式加載微應用時生效
 */
export async function update(props) {
  console.log('update props', props);
}

上述代碼中還引用了一個public-path的文件:

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

這個主要解決的是微應用動態載入的 腳本、樣式、圖片 等地址不正確的問題。

2. 配置微應用的打包工具

除了代碼中暴露出相應的生命週期鉤子之外,爲了讓主應用能正確識別微應用暴露出來的一些信息,微應用的打包工具需要增加如下配置:

webpack:

const packageName = require('./package.json').name;


module.exports = {
  publicPath: '/system/', //這裏打包地址都要基於主應用的中註冊的entry值
  output: {
    library: 'ManageMicroApp', // 庫名,與主應用註冊的微應用的name一致
    libraryTarget: 'umd', // 這個選項會嘗試把庫暴露給前使用的模塊定義系統,這使其和CommonJS、AMD兼容或者暴露爲全局變量。
    jsonpFunction: `webpackJsonp_${packageName}`,
  },
};

關鍵點總結

到這裏我們把微前端的配置做好了,接下來就是 nginx 的配置。

生產環境 Nginx 配置

先把主應用的 nginx 配置掛一下

    server {
        listen       80;
        listen       [::]:80 default_server;
        server_name  localhost;
        root         /usr/share/nginx/html;

        location / {
            try_files $uri $uri/ /index.html;
            index index.html;
        }
				# 前面我們配置的子應用entry是/system/,所以會觸發這裏的代理,代理到對應的子應用
        location /system/ {
    				 # -e表示只要filename存在,則爲真,不管filename是什麼類型,當然這裏加了!就取反
             if (!-e $request_filename) {
                proxy_pass http://192.168.1.2; # 這裏的ip是子應用docker容器的ip
             }
    				 # -f filename 如果 filename爲常規文件,則爲真
             if (!-f $request_filename) {
                proxy_pass http://192.168.1.2;
             }
             # docker運行的nginx不識別localhost的 所以這種寫法會報502
             # proxy_pass  http://localhost:10200/;
             proxy_set_header Host $host;
         }

        location /api/ {
            proxy_pass http://後臺地址IP/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header REMOTE-HOST $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        error_page 404 /404.html;
            location = /40x.html {
        }

        error_page 500 502 503 504 /50x.html;
            location = /50x.html {
        }
    }

再看一下子應用的

server {
    listen       80;
    listen       [::]:80 default_server;
    server_name  _2;
    root         /usr/share/nginx/html;

    # 這裏必須加上允許跨域,否則主應用無法訪問
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

    location / {
        try_files $uri $uri/ /index.html;
        index index.html;
    }

    location /api/ {
        proxy_pass http://後臺地址IP/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header REMOTE-HOST $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    error_page 404 /404.html;
        location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }
}

dockerfile 配置

這裏先看一下子應用的

# 直接使用nginx鏡像
FROM nginx
# 把上面配置的conf文件替換一下默認的
COPY nginx.conf /etc/nginx/nginx.conf
# nginx默認目錄下需要能看見index.html文件
COPY dist/index.html /usr/share/nginx/html/index.html
# 再回頭看一下部署邏輯圖和qiankun注意點,必須要把所有的資源文件放到system文件下index.html才能正確加載
COPY dist /usr/share/nginx/html/system

再看一下主應用的

# 這裏主應用沒有直接使用nginx,因爲nginx反向代理的/api/會出現404的問題,原因未知!
FROM centos
# 安裝nginx
RUN yum install -y nginx
# 跳轉到/etc/nginx
WORKDIR /etc/nginx
# 替換配置文件
COPY nginx.conf nginx.conf
# 跳轉到/usr/share/nginx/html
WORKDIR /usr/share/nginx/html
# 主應用正常打包,所以直接把包放進去就行
COPY dist .
# 暴露80端口
EXPOSE 80
# 運行nginx
CMD nginx -g "daemon off;"

gitlab-ci/cd 配置

先看一下子應用的,只說重點的

image: node

stages:
  - install
  - build
  - deploy
  - clear

cache:
  key: modules-cache
  paths:
    - node_modules
    - dist

安裝環境:
  stage: install
  tags:
    - vue
  script:
    - npm install yarn
    - yarn install

打包項目:
  stage: build
  tags:
    - vue
  script:
    - yarn build

部署項目:
  stage: deploy
  image: docker
  tags:
    - vue
  script:
  	# 通過dockerfile構建項目的鏡像
    - docker build -t rainbow-system .
    # 如果存在之前創建的容器先刪除
    - if [ $(docker ps -aq --filter name=rainbow-admin-system) ];then docker rm -f rainbow-admin-system;fi
    # 通過剛剛的鏡像創建一個容器 給容器指定一個網卡rainbow-net,這個網卡是我們自定義,創建方式後面會說,然後給定一個ip
    - docker run -d --net rainbow-net --ip 192.168.1.2 --name rainbow-admin-system rainbow-system

清理docker:
  stage: clear
  image: docker
  tags:
    - vue
  script:
    - if [ $(docker ps -aq | grep "Exited" | awk '{print $1 }') ]; then docker stop $(docker ps -a | grep "Exited" | awk '{print $1 }');fi
    - if [ $(docker ps -aq | grep "Exited" | awk '{print $1 }') ]; then docker rm $(docker ps -a | grep "Exited" | awk '{print $1 }');fi
    - if [ $(docker images | grep "none" | awk '{print $3}') ]; then docker rmi $(docker images | grep "none" | awk '{print $3}');fi

再看一下主應用的,省略重複的,直接看重點

部署項目:
  stage: deploy
  image: docker
  tags:
    - vue3
  script:
    - docker build -t rainbow-admin .
    - if [ $(docker ps -aq --filter name=rainbow-admin-main) ];then docker rm -f rainbow-admin-main;fi
    # 給容器指定一個網卡rainbow-net,然後給定一個ip,然後通過--link與之前創建的子應用連通,重點!
    - docker run -d -p 80:80 --net rainbow-net --ip 192.168.1.1 --link 192.168.1.2 --name rainbow-admin-main rainbow-admin

上面說到了 docker 的自定義網卡,生成的命令如下:

$ docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 rainbow-net

總結

到這裏我們已經實現了 qiankun+docker 配合 gitlab-ci/cd 的自動化部署,中間遇到很多坑,然後走出了一條相對合理的解決方案,有問題歡迎討論。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/eFii4N8ey7fNKW7fBZW6tQ