微前端 qiankun-docker-nginx 配合 gitlab-ci-cd 的自動化部署的實現
來自:掘金,作者:紙上的彩虹
鏈接:https://juejin.cn/post/6981339862901194759
技術棧簡介
-
微前端
-
qiankun
-
docker
-
gitlab-ci/cd
-
nginx
如果看完文章不是很理解,可以配合 [視頻解說查看本文] 視頻地址:https://www.bilibili.com/video/BV1Qg411u7C9
什麼是微前端
微前端是一種多個團隊通過獨立發佈功能的方式來共同構建現代化 web 應用的技術手段及方法策略。
微前端架構具備以下幾個核心價值:
-
技術棧無關 主框架不限制接入應用的技術棧,微應用具備完全自主權
-
獨立開發、獨立部署 微應用倉庫獨立,前後端可獨立開發,部署完成後主框架自動完成同步更新
-
增量升級
在面對各種複雜場景時,我們通常很難對一個已經存在的系統做全量的技術棧升級或重構,而微前端是一種非常好的實施漸進式重構的手段和策略
-
獨立運行時 每個微應用之間狀態隔離,運行時狀態不共享
什麼是 qiankun
qiankun 是一個生產可用的微前端框架,它基於 single-spa,具備 js 沙箱、樣式隔離、HTML Loader、預加載 等微前端系統所需的能力。qiankun 可以用於任意 js 框架,微應用接入像嵌入一個 iframe 系統一樣簡單。
qiankun 的核心設計理念
引用地址:qiankun.umijs.org/zh/guide
-
簡單
由於主應用微應用都能做到技術棧無關,qiankun 對於用戶而言只是一個類似 jQuery 的庫,你需要調用幾個 qiankun 的 API 即可完成應用的微前端改造。同時由於 qiankun 的 HTML entry 及沙箱的設計,使得微應用的接入像使用 iframe 一樣簡單。
-
解耦 / 技術棧無關
微前端的核心目標是將巨石應用拆解成若干可以自治的松耦合微應用,而 qiankun 的諸多設計均是秉持這一原則,如 HTML entry、沙箱、應用間通信等。這樣才能確保微應用真正具備 獨立開發、獨立運行 的能力。
爲什麼不用 Iframe
引用地址:www.yuque.com/kuitos/gky7…
如果不考慮體驗問題,iframe 幾乎是最完美的微前端解決方案了。
iframe 最大的特性就是提供了瀏覽器原生的硬隔離方案,不論是樣式隔離、js 隔離這類問題統統都能被完美解決。但他的最大問題也在於他的隔離性無法被突破,導致應用間上下文無法被共享,隨之帶來的開發體驗、產品體驗的問題。
-
url 不同步。瀏覽器刷新 iframe url 狀態丟失、後退前進按鈕無法使用。
-
UI 不同步,DOM 結構不共享。想象一下屏幕右下角 1/4 的 iframe 裏來一個帶遮罩層的彈框,同時我們要求這個彈框要瀏覽器居中顯示,還要瀏覽器 resize 時自動居中。
-
全局上下文完全隔離,內存變量不共享。iframe 內外系統的通信、數據同步等需求,主應用的 cookie 要透傳到根域名都不同的子應用中實現免登效果。
-
慢。每次子應用進入都是一次瀏覽器上下文重建、資源重新加載的過程。
其中有的問題比較好解決 (問題 1),有的問題我們可以睜一隻眼閉一隻眼(問題 4),但有的問題我們則很難解決(問題 3) 甚至無法解決(問題 2),而這些無法解決的問題恰恰又會給產品帶來非常嚴重的體驗問題, 最終導致我們捨棄了 iframe 方案。
微前端的核心價值
www.yuque.com/kuitos/gky7…
項目的構想
在說具體技術實現前,我們先來看下我們想要實現個什麼東西。
微前端示意圖
子應用會根據主應用導航的點擊而動態加載
部署邏輯
部署的思路有很多,我這裏說說我嘗試過的方式:
-
只使用一個 nginx 容器,通過監聽不同端口,部署多個應用,再在主應用的端口裏面添加對應路由代理到子應用
這種方式最簡單但是不適合 gitlab-ci/cd 的自動化部署,所以我只是最初測試一下 nginx 部署微前端的實現
-
使用多個 nginx 容器,每個容器暴露一個端口,再通過主應用添加對應路由代理到子應用
這種方式可以實現,但是會在服務器暴露多個端口,安全性會降低,而且外部也可以通過端口直接訪問子應用
-
使用多個 nginx 容器,只暴露主應用的端口,主應用去連通子應用,然後通過 nginx 代理訪問
這種方式最理想,只需要暴露一個端口,所有代理都在容器間,對外是無感的,下面是實現的圖示
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) 導出 bootstrap
、mount
、unmount
三個生命週期鉤子,以供主應用在適當的時機調用。
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}`,
},
};
關鍵點總結
-
主應用註冊時的配置
const apps = [ { name: 'ManageMicroApp', entry: '/system/', // http://localhost/system/ 這裏會通過nginx代理指向對應的子應用地址 container: '#frame', activeRule: '/manage', }, ]
主應用註冊微應用時,****
entry
可以爲相對路徑,activeRule
不可以和entry
一樣(否則主應用頁面刷新就變成微應用) -
vue 路由的 base
router = new VueRouter({ base: window.__POWERED_BY_QIANKUN__ ? '/manage/' : '/', mode: 'history', routes });
如果是主應用調用的那麼路由的 base 爲**
/manage/
** -
webpack 打包配置
module.exports = { publicPath: '/system/', };
對於
webpack
構建的微應用,微應用的webpack
打包的publicPath
需要配置成/system/
,否則微應用的index.html
能正確請求,但是微應用index.html
裏面的js/css
路徑不會帶上/system/
。
到這裏我們把微前端的配置做好了,接下來就是 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