關注公眾號: 微信搜索 前端工具人
; 收貨更多的干貨
一、介紹:
qiankun
項目實際搭建, 及各種微應用流行框架技術 (vue2 、vue3、react 、 umi2 、umi3
)的配置;
初衷
:自己當時摸索qiankun
構建項目時,問題百出, 特別是umi2
及 umi3
,百度了幾天才把熱門框架都集合完畢;
目的
:總結出的模板項目, 便於自己后期重構項目技術選型及項目快速搭建;也為其他有需要的朋友提供示例及參考;
項目源碼:已上傳到 github https://github.com/laijinxian/qiankun-template
如有對你有幫助,麻煩 star 下
末尾的常見問題多數為目前開發中遇到的疑難點, 特地整理出來;有其他問題歡迎留言交流
實際項目源碼就沒貼出來了,都是依據這個模板構建的;
后面看下好不好把實際項目源碼抽離出來,上傳到github
; 目前子項目使用的是 vite2.0 + vue3 + ts
以及 react + Umi3 + dva + ts
二、什么是微前端 qiankun 篇
推薦閱讀 qiankun文檔
其實我個人更喜歡叫成 前端微服務架構
, 感覺逼格更高點...
2.1 官方介紹:
- 微前端是一種多個團隊通過獨立發布功能的方式來共同構建現代化
web
應用的技術手段及方法策略 (有點高深...)
2.2 我的觀點:
-
與技術棧無關、獨立開發、獨立部署、增量升級、獨立運行;
-
拆分、細化、解耦
你的巨無霸項目; 提升開發及打包部署效率; -
不在局限於一個項目只能使用一種技術,一個項目可以使用
N
種技術,擴展自己技術知識面; -
對於比如 大型
erp、OA
之類的系統, 微前端可以讓你更加的得心應手的開發; -
對於想
重構
公司辣眼睛的項目尤為合適;這也算我入手微前端的主要原因之一,下面會講到; -
順應時代潮流, 作為主流技術現在非常多的公司
招聘面試
基本都會問微前端,細化程度不一樣;
三、 為什么用qiankun, 為什么選擇qiankun
3.1 為什么用qiakun
自打進入公司,看到了現有的項目,我總結了幾點
- 項目全都使用 Vue, 一直開發下去你會發現 React 忘得快差不多了;技術的局限性;
- 現有項目代碼又臭又長,毫無規范;
eslint、css
預編譯啥都沒有; - 2-3層
for
循環,var
之類的,粘貼復制無用代碼不刪除到處可見;公共代碼提取、接口統一處理、工具類編寫不存在的; - 一個項目同時出現
vue、jQuery
兩個大框架;運行項目、熱編譯、你可以先上趟廁所; - 每期功能迭代,先要花大半天時間去熟悉這個代碼、還真不敢亂改,有毒、誰改誰后悔的那種
- 想重構,奈何剛接手的時候項目已經很大了,並且不怎么熟悉業務,且不斷的加功能; 一時重構基本不可能;千萬級別的用戶量出問題了這鍋背不動, 時間也不允許
后面需求排期不是很緊湊,正直qiankun
微前端很火,就想着使用qiankun
微前端方案重構;
思路如下:
目標
把一個項目按照菜單划分,一個大菜單分為一個子服務(子項目)- 剛開始原有項目全部划分為一個子服務,新加功能菜單划分為另一個子服務;這樣既保證原有項目不變,新項目完全使用新的框架及開發風格規范;
- 時間充裕下情況下,慢慢把其他功能按照菜單划分成子服務,慢慢的最小粒度去重構項目
3.2 目前微前端方案有:
iframe
single-spa
qiankun
基於single-spa
方案實現, 更強大更易上手
推薦閱讀 掘金大佬文章, 文章有詳細介紹及常見問題
四、 構建步驟
項目結構:
├── main-service // 主應用
└── sub-service // 微應用
└── sub-react // react 子應用
└── sub-umi2 // umi2 子應用
└── sub-umi3 // umi3 子應用
└── sub-vue2 // vue2 子應用
└── sub-vue3 // vue3 子應用
推薦閱讀:
-
詳細結構代碼已上傳github 請前往 github 查看
4.1 項目結構組成
主應用:
vue2.x + vuec-li3
主要業務功能就是登錄注冊及菜單;官方推薦主應用盡可能的簡單,不要涉及其他的業務功能
微應用:
vue2.x + vue-cli3
vue3.x + vue-cli4 + typescript
react16
react16 + umi2 + dva
react16 + umi3 + dva
4.2 主應用配置
qiankun
只需要在主應用中引入,微應用不需要
yarn add qiankun # 或者 npm i qiankun -S
4.3 主應用 src 下 注冊微應用
主應用 src
下新建 qiankun/index.js
import {
registerMicroApps,
runAfterFirstMounted,
setDefaultMountApp,
start
} from "qiankun";
import store from '../store/index'
import { instance } from "../main";
import 'nprogress/nprogress.css'
/**
* Step1 初始化應用(可選)
*/
function loader(loading) {
if (instance && instance.$children) {
// instance.$children[0] 是App.vue,此時直接改動App.vue的isLoading
instance.$children[0].isLoading = loading;
}
}
/**
* Step2 注冊子應用
*/
const microApps = [
{
name: 'sub-vue2',
developer: 'vue2.x',
entry: '//localhost:7788',
activeRule: '/sub-vue2',
},
{
name: 'sub-vue3',
developer: 'vue3.x',
entry: '//localhost:7799',
activeRule: '/sub-vue3'
},
{
name: 'sub-react',
developer: 'react16',
entry: '//localhost:7755',
activeRule: '/sub-react'
},
{
name: 'sub-umi2',
developer: 'umi2.x',
entry: '//localhost:7766',
activeRule: '/sub-umi2'
},
{
name: 'sub-umi3',
developer: 'umi3.x',
entry: '//localhost:7733',
activeRule: '/sub-umi3'
}
]
const apps = microApps.map(item => {
return {
...item,
loader, // 給子應用配置加上loader方法
container: '#subapp-container', // 子應用掛載的div
props: {
developer: item.developer, // 下發基礎路由
routerBase: item.activeRule, // 下發基礎路由
getGlobalState: store.getGlobalState // 下發getGlobalState方法
}
}
})
registerMicroApps(apps, {
beforeLoad: app => {
console.log('before load app.name====>>>>>', app.name)
},
beforeMount: [
app => {
console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name)
}
],
afterMount: [
app => {
console.log('[LifeCycle] after mount %c%s', 'color: green;', app.name)
}
],
afterUnmount: [
app => {
console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name)
}
]
})
/**
* Step3 設置默認進入的子應用
*/
setDefaultMountApp('/sub-vue2')
/**
* Step4 啟動應用
*/
start();
runAfterFirstMounted(() => {
console.log("[MainApp] first app mounted");
});
export default apps
4.4 微應用導出生命周期鈎子
各種框架配置推薦閱讀 官方文檔
下面以 vue3.x
及 react umi3
為例; 其他微服務配置請前往 [github](https://github.com/laijinxian/qiankun-template)
源碼查看
子應用的名稱最好與父應用在 qiankun/index.js
中配置的名稱一致(這樣可以直接使用package.json
中的name
作為output
)
vue3.x
微應用
首先 vue create sub-vue3
創建項目
修改 main.js
導出生命周期函數
// @ts-nocheck
import "./public-path";
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import App from "./App.vue";
import routes from "./router";
import store from "./store";
let router = null;
let instance = null;
function render(props = {}) {
const { container } = props;
router = createRouter({
history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? "/sub-vue3" : "/"),
routes
});
instance = createApp(App);
instance.use(router);
instance.use(store);
instance.mount(container ? container.querySelector("#app") : "#app");
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log("%c ", "color: green;", "vue3.0 app bootstraped");
}
function storeTest(props) {
props.onGlobalStateChange &&
props.onGlobalStateChange(
(value, prev) =>
console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
true
);
props.setGlobalState &&
props.setGlobalState({
ignore: props.name,
user: {
name: props.name
}
});
}
export async function mount(props) {
storeTest(props);
render(props);
instance.config.globalProperties.$onGlobalStateChange =
props.onGlobalStateChange;
instance.config.globalProperties.$setGlobalState = props.setGlobalState;
}
export async function unmount() {
instance.unmount();
instance._container.innerHTML = "";
instance = null;
router = null;
}
新建 vue.config.js
const path = require('path');
const { name } = require('./package');
function resolve(dir) {
return path.join(__dirname, dir);
}
module.exports = {
outputDir: 'dist',
assetsDir: 'static',
filenameHashing: true,
devServer: {
hot: true,
disableHostCheck: true,
port: '7799',
overlay: {
warnings: false,
errors: true,
},
clientLogLevel: "warning",
disableHostCheck: true,
compress: true,
headers: {
'Access-Control-Allow-Origin': '*',
},
historyApiFallback: true,
overlay: { warnings: false, errors: true }
},
// 自定義webpack配置
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'),
},
},
output: {
// 把子應用打包成 umd 庫格式
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
src
新建 public-path.js
並引入
/* eslint-disable @typescript-eslint/camelcase */
if ((window as any).__POWERED_BY_QIANKUN__) {
/* eslint-disable @typescript-eslint/camelcase */
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
react umi3
微應用
創建項目
推薦閱讀: umi 官方文檔
$ mkdir myapp && cd myapp
$ yarn create umi
安裝
$ npm install --save-dev @umijs/plugin-qiankun
$ yarn add @umijs/plugin-qiankun
修改 src/app.js 導出生命周期函數
import './public-path'
export const dva = {
config: {
onError(err) {
err.preventDefault();
console.error(err.message);
},
},
};
export const qiankun = {
// 應用加載之前
async bootstrap(props) {
console.log('app1 bootstrap', props);
},
// 應用 render 之前觸發
async mount(props) {
console.log('app1 mount', props);
storeTest(props);
},
// 應用卸載之后觸發
async unmount(props) {
console.log('app1 unmount', props);
},
};
function storeTest(props) {
props.onGlobalStateChange &&
props.onGlobalStateChange(
(value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev),
true,
);
props.setGlobalState &&
props.setGlobalState({
ignore: props.name,
user: {
name: props.name,
},
});
}
修改 .umirc.js 文件 引入 @umijs/plugin-qiankun 插件
// ref: https://umijs.org/config/
export default {
mountElementId: 'sub-umi3',
base: `sub-umi3`, // 子應用的 base,默認為 package.json 中的 name 字段
treeShaking: true,
routes: [
{ exact: false, path: '/', component: '../layouts/index',
routes: [
{ exact: false, path: '/', component: '../pages/index' },
{ component: './404.js' }
],
}
],
plugins: [
['@umijs/plugin-qiankun', {
keepOriginalRoutes: true
}],
// ref: https://umijs.org/plugin/umi-plugin-react.html
['umi-plugin-react', {
antd: true,
dva: true,
dynamicImport: { webpackChunkName: true },
title: 'react',
dll: false,
routes: {
exclude: [
/models\//,
/services\//,
/model\.(t|j)sx?$/,
/service\.(t|j)sx?$/,
/components\//,
],
},
}],
],
}
src
新建 public-path.js
並引入
/* eslint-disable @typescript-eslint/camelcase */
if ((window as any).__POWERED_BY_QIANKUN__) {
/* eslint-disable @typescript-eslint/camelcase */
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
以上就是微前端的基本配置 demo
, 源碼 [github](https://github.com/laijinxian/qiankun-template)
查看
接下來真正的項目重構實操及進階
五、 項目重構實踐、進階中常見問題
5.1 qiankun 常見報錯
推薦閱讀: 官方文檔總結 https://qiankun.umijs.org/zh/faq
5.2 狀態管理, 主應用和微應用之間的通信
qiankun
通過 initGlobalState
: 定義全局狀態,並返回通信方法,建議在主應用使用,微應用通過 props
獲取通信方法;
onGlobalStateChange
: 在當前應用監聽全局狀態,有變更觸發 callback
;
setGlobalState
: 按一級屬性設置全局狀態,微應用中只能修改已存在的一級屬性; 換句話說只能修改主用於預先定義的屬性,后面添加的屬性無效
官方列子 發布-訂閱的設計模式:
主應用
import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);
actions.onGlobalStateChange((state, prev) => {
// state: 變更后的狀態; prev 變更前的狀態
console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();
微應用
// 從生命周期 mount 中獲取通信方法,使用方式和 master 一致
export function mount(props) {
props.onGlobalStateChange((state, prev) => {
// state: 變更后的狀態; prev 變更前的狀態
console.log(state, prev);
});
props.setGlobalState(state);
}
5.3 各應用之間的獨立倉庫以及聚合管理
實際開發中項目存儲在公司倉庫中,以 gitLab
為例, 當子應用一多,全部放在一個倉庫下面, 這時候就顯得很臃腫了,也很龐大,大大的增加了維護成本,和開發效率;
我們可以通過 sh
腳本, 初始只需要克隆主倉庫代碼, 然后通過 sh
腳本去一鍵拉取所有子應用;
主倉庫新建 script/clone-all.sh
文件 內容如下
# 子服務 gitLab 地址
SUB_SERVICE_GIT=('http://gitlab.qinlinkeji.com/xxxxxx/qiankun-sub-service-vue.git' 'http://gitlab.qinlinkeji.com/xxxxxx/qiankun-sub-service-react.git')
SUB_SERVICE_NAME=('qiankun-sub-service-vue' 'qiankun-sub-service-react')
# 子服務
if [ ! -d "sub-service" ]; then
echo '創建sub-service目錄...'
mkdir sub-service
fi
echo '進入sub-service目錄...'
cd sub-service
# 遍歷克隆微服務
for i in ${!SUB_SERVICE_NAME[@]}
do
if [ ! -d ${SUB_SERVICE_NAME[$i]} ]; then
echo '克隆微服務項目'${SUB_SERVICE_NAME[$i]}
git clone ${SUB_SERVICE_GIT[$i]}
fi
done
echo '腳本結束...'
# 克隆完成
代碼拉取完成后, 緊接着就是下載各個項目的依賴及運行
應用根目錄安裝 npm i npm-run-all -D
package.json
文件 scripts
命令如下
"scripts": {
"clone:all": "bash ./scripts/clone-all.sh",
"install": "npm-run-all --serial install:*",
"install:main": "cd main-service && cnpm i",
"install:sub-vue2": "cd sub-service/sub-vue2 && yarn install",
"install:sub-vue3": "cd sub-service/sub-vue3 && yarn install",
"install:sub-react": "cd sub-service/sub-react && cnpm i",
"install:sub-umi2": "cd sub-service/sub-umi2 && yarn install",
"install:sub-umi3": "cd sub-service/sub-umi3 && yarn install",
"start": "npm-run-all --parallel start:*",
"start:sub-react": "cd sub-service/sub-react && npm start",
"start:sub-vue2": "cd sub-service/sub-vue2 && npm start",
"start:sub-vue3": "cd sub-service/sub-vue3 && yarn start",
"start:sub-umi2": "cd sub-service/sub-umi2 && yarn start",
"start:sub-umi3": "cd sub-service/sub-umi3 && yarn start",
"start:main": "cd main-service && yarn start",
"test": "echo \"Error: no test specified\" && exit 1"
},
步驟: 第一步 clone
主應用, 然后依次執行 yarn clone:all --> yarn install --> yarn start
即可運行整個項目
5.4 子應用之間的獨立開發
需求
: 每次項目的迭代,有可能只涉及其中某個應用功能,
期望
: 只需要單獨打開這個子應用修改即可;並不是整個龐大項目一起啟用
目標
: 應用解耦的同時也能高效擼代碼
問題
: 整個項目中都需要一個登錄態(登錄憑證 token
), 上面說到登錄token
是在主應用中維護的, 不啟動主應用,子應用怎么拿到登錄態token
呢;
解析
: 其實登錄的主要作用都是獲取到用戶信息及 token
后, 保存在瀏覽器緩存中,比如 localStorage、sessionStorage、cookie、IndexedDB
, 需要的地方獲取即可;
方法
: 子應用中通過 qiankun
提供的 window.__POWERED_BY_QIANKUN__
屬性, 很直接的知道目前是否運行在 qiankun
的主應用的上下文中;全局維護一個變量,控制是否展示 iframe
的登錄頁
if (!window.__POWERED_BY_QIANKUN__) {
// 不在主應用的上下文中
}
當不在qiankun
主應用上下文環境中時, 通過 iframe
形式, 直接引入登錄頁面, 完成登錄把用戶數據及token
存入緩存中即可;
要注意的是:
- 瀏覽器默認不支持
iframe
文件的script
腳本執行; 需要設置sandbox="allow-scripts allow-same-origin"
兩個屬性即可 - 下面代碼是通過本地的
html
文件 (登錄頁);在vue-cli3
中我們需要吧html
靜態、文件放在public/static
下面 - 當然當你項目發布到服務器之后, 把上面步驟刪了,直接在
iframe
里引用登錄頁面即可;iframe
的url
指向你的線上登錄頁; 這樣下來子應用只需要加個iframe
一行代碼,即可完成子應用的登錄態獲取
實例: vue
子應用 某頁面
<template>
<!-- <iframe src="https://juejin.cn/" width="400" height="300" sandbox="allow-scripts allow-same-origin"></iframe> -->
<iframe ref="iframe" name="iframe" width="400" height="300" sandbox="allow-scripts allow-same-origin"></iframe>
</template>
<script>
export default {
data () {
return {
html: require("static/login.html")
}
},
mounted() {
this.$refs.iframe.srcdoc = this.html
console.log(localStorage.getItem('userInfo'))
}
}
</script>
5.5 如何提取出公共的依賴庫
官方說法: 並不推薦這種做法, 因為微服務主要目標是解耦大型應用, 並且當你升級某個項目的公共依賴之后,意味着其他子應用也升級了, 很難保證不出問題;但你確實想那么做,那么也有方法:
方法1: 官方推薦你可以在微應用中將公共依賴配置成 external
,然后在主應用中導入這些公共依賴;
推薦閱讀: 掘金文章
方法2: 我的做法是 通過 webpack
的 DllPlugin
動態鏈接庫, 生成靜態 json
在子應用中引
入; DllPlugin
是webpack
內置的插件,不需要額外安裝; 這里就不貼代碼了, 代碼有點多, DllPlugin教程很多, 百度到處是, 跟着配置下webpack.dll.config.js
就行;
DllPlugin
也是項目優化的一個手段, 自己配置一遍印象更深
5.6 如何提取出公共方法
在這我個人也不怎么推薦, 因為子應用是不同框架 vue\react\umi-react
並不能保證方法能同時作用於這幾個框架項目, 不能的話那何來公共方法一說;
當然你的子應用全是同一個框架那上面的話當我沒說。。。
有需求就有方法: 推薦參考 掘金文章 更詳細:
npm
指向本地file
地址:npm install file:../common
。直接在根目錄新建一個common
目錄,然后npm
直接依賴文件路徑。npm
指向私有git
倉庫:npm install git+ssh://xxx-common.git
。- 發布到
npm
私服
demo
中我用的是第一種方法,當然不嫌麻煩可以選用第三種發布到npm
私服,嫌私服難搭,可以用后台的manven
私服,把你的公共代碼給后台讓讓后台發布; manven
私服按我的理解后台標配;
第一種方法 指向本地file
地址事例; vue
子應用 main.js
引入你的本地公共代碼並注冊
import globalRegister from '../../../main-service/src/store/global-register'
export async function mount(props) {
console.log('[vue] props from main framework', props);
storeTest(props);
render(props);
globalRegister(store, props)
}
5.7 微應用之間如何跳轉
- 主應用和微應用都是
hash
模式,主應用根據hash
來判斷微應用,則不用考慮這個問題。 - 主應用根據
path
來判斷微應用
history
模式的微應用之間的跳轉,或者微應用跳主應用頁面,直接使用微應用的路由實例是不行的,原因是微應用的路由實例跳轉都基於路由的 base
。有兩種辦法可以跳轉:
history.pushState()
:mdn用法介紹- 將主應用的路由實例通過 props 傳給微應用,微應用這個路由實例跳轉。
// 用法 第二、第三參數分別是子應用名稱及激活路由
history.pushState(null, 'sub-react', '/sub-react');
5.8 微應用文件更新之后,訪問的還是舊版文
項目上線后由於是獨立倉庫獨立開發獨立部署, 微應用文件更新之后,訪問的還是舊版文;
服務器需要給微應用的 index.html
配置一個響應頭:Cache-Control no-cache
,意思就是每次請求都檢查是否更新。
以 Nginx
為例:
location = /index.html {
add_header Cache-Control no-cache;
}
5.9 應用加載的資源會 404
原因是 webpack
加載資源時未使用正確的 publicPath
。
可以通過以下兩個方式解決這個問題:
a. 使用 webpack
運行時 publicPath
配置
qiankun
將會在微應用 bootstrap
之前注入一個運行時的 publicPath
變量,你需要做的是在微應用的 entry js
的頂部添加如下代碼:
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
關於運行時 publicPath
的技術細節,可以參考 webpack
文檔。
runtime publicPath
主要解決的是微應用動態載入的 腳本、樣式、圖片 等地址不正確的問題。
b. 使用 webpack
靜態 publicPath
配置
你需要將你的 webpack publicPath 配置設置成一個絕對地址的 url,比如在開發環境可能是:
{
output: {
publicPath: `//localhost:${port}`,
}
}
5.10 如何部署
推薦閱讀 官方文檔 更詳細
主應用和微應用都是獨立開發和部署,即它們都屬於不同的倉庫和服務
場景:主應用和微應用部署到同一個服務器(同一個IP和端口)
如果服務器數量有限,或不能跨域等原因需要把主應用和微應用部署到一起。
通常的做法是主應用部署在一級目錄,微應用部署在二/三級目錄。
微應用想部署在非根目錄,在微應用打包之前需要做兩件事:
-
必須配置
webpack
構建時的publicPath
為目錄名稱,更多信息請看webpack
官方說明 和vue-cli3
的官方說明 -
history
路由的微應用需要設置base
,值為目錄名稱,用於獨立訪問時使用。
部署之后注意三點:
activeRule
不能和微應用的真實訪問路徑一樣,否則在主應用頁面刷新會直接變成微應用頁面。- 微應用的真實訪問路徑就是微應用的
entry
,entry
可以為相對路徑。 - 微應用的
entry
路徑最后面的/
不可省略,否則publicPath
會設置錯誤,例如子項的訪問路徑是http://localhost:8080/app1
,那么entry
就是http://localhost:8080/app1/
。
以上問題大多數可前端官方文檔常見問題查看, 只不過不是很詳細, 有的需要自己去百度完善