前言javascript
筆者在2019年底的時候開始瞭解微前端這個東西、當時看到兩個微前端的框架、分別是 single-spa 和 qiankun、在這兩種技術去作選擇去學習、看到qiankun是基於single-spa二次封裝的、文檔簡潔明瞭、使用簡單後面決定學習qiankun!並把本身踩的坑記錄下來 css
一、技術棧無關前端
二、主框架不限制接入應用的技術棧,微應用具有徹底自主權vue
三、獨立開發、獨立部署java
四、微應用倉庫獨立,先後端可獨立開發,部署完成後主框架自動完成同步更新node
因爲那啥因此打了碼、頂部是全部系統、左側是當前系統的菜單欄、從UI的設計圖上看這個項目是很適合微前端!後面我會用基座(微前端環境)和子應用與主應用去介紹個人踩坑之路-😄react
Default ([Vue 2] babel, eslint)
主應用須要作的事情是: 對qiankun框架單獨模塊化封裝導出核心方法、配置cdn方式加載核心模塊 、配置 eslint 忽略指定全局變量、配置webpack的externals排除某些依賴,使用 cdn 資源代替
vue create main-app
複製代碼
腳手架好了以後咱們須要安裝 qiankun
linux
yarn add qiankun
複製代碼
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配置
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、那其餘應用傳入給主應用的數據是接收不到的
<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
複製代碼
修改打包配置 - 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}`,
},
},
};
複製代碼
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 };
複製代碼
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和主應用的同樣、在相同的位置從新複製一份便可
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 //而後按提示執行下去
複製代碼
下面是定一個start指令而後去執行config下的start.js
"scripts": {
"start":"node config/start.js"
}
複製代碼
同級
下建立一個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....
exec下的指令換成對應的
剩下就是執行shell腳本進行服務器上傳部署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進行的寫的一篇踩坑文章、我相信在重構公司項目的時候踩的坑確定不止這些到時候我統一在、遇到的問題那進行補充!
加油、折騰人