微前端框架qiankun项目实战(二)--踩坑与部署篇
回复1,加入高级 Node 进阶交流群
作者:黑化程序员(作者授权转载)
链接:https://juejin.cn/post/6973111766767108103
大家好,我是小黑。
在上一篇《微前端框架qiankun项目实战(一)--本地开发篇》发布后,感谢有网友提出了微应用的缓存问题,的确基于第一篇使用的registerMicroApps方式很难做到缓存,要做到应用缓存的方式使用手动加载管理微应用的方式是最好的,我将再写一篇补充篇使用loadMicroApp手动管理微应用,本篇我会模拟部署一下主应用和微应用,并将揭开我上一篇所谓的巨坑是什么。
贴上我建好的模板仓库地址
vue3模板:https://gitee.com/jimpp/vue3-main-app
vue2模板:https://gitee.com/jimpp/vue2-micro-app
在上一篇中,master分支都是未改造前能独立运行的项目,dev分支是最终改造后的项目,本篇所有代码会在新建的test分支修改
隐藏微应用菜单和头部
在上篇的结尾,我们本地运行微前端的时候,发现微应用的菜单和头部还是渲染出来了
不知道亲爱的你是否有思路如何实现隐藏,下面给出我的思路代码
// template
<div class="nav" v-if="showMenu">
<div class="menu">
<router-link to="/">Child Home</router-link>
</div>
<div class="menu">
<router-link to="/about">Child About</router-link>
</div>
</div>
<div class="container">
<div class="header" v-if="showHeader">Child Header</div>
<div class="router-view">
<router-view />
</div>
</div>
// js
computed: {
...mapState(["token"]),
// 控制菜单显示隐藏
showMenu() {
return this.token && !this.isMicroEnc
},
// 控制头部显示隐藏
showHeader() {
return this.token && !this.isMicroEnc
},
isMicroEnc() {
return window.__POWERED_BY_QIANKUN__
}
}
利用computed根据token 和 window.POWERED_BY_QIANKUN 去控制显示隐藏,效果如下
token放进本地缓存
这个过程中我们要不断地修改项目,一刷新就要重新登录实在太烦了,下面我们改造一下主应用,把登录后的token存到localStorage中
在src/store/index.js
中
mutations: {
setToken(state, token) {
state.token = token
// 新增,登录的时候同时把token存到localStorage
localStorage.setItem('token', token)
}
},
// 新增
const storagePlugin = store => {
const token = localStorage.getItem('token')
if(token) {
store.commit('setToken', token)
}
}
plugins: [storagePlugin]
这里在setToken方法中添加了把token存到localStorage的逻辑,并编写了一个Vuex
的storagePlugin
插件,该插件主要功能是在应用加载的时候去获取localStorage中的token,如果有的话直接commit到我们的store中,这样一来我们只要登录了,再刷新也不需要重新登录
接下来,准备开始踩坑了
坑1:样式冲突问题
首先遇到的样式冲突,不是什么ui库的冲突,而是iconfont的冲突,我是在改造两个线上项目的时候遇到的
首先去iconfont官网为两个应用添加两组图标
主应用的图标
微应用的图标
可以看到两个应用的图标命名是一致的,不过主应用是空心的,微应用是实心的
下载好的图标库是这样的
我们只需要拷贝iconfont.css、iconfont.ttf、iconfont.woff、iconfont.woff2
这几个文件到src/assets
目录下,然后在main.css
引入就可以了
iconfont.css
的代码如下
@font-face {
font-family: "iconfont"; /* Project id 2608947 */
src: url('iconfont.woff2?t=1623503003854') format('woff2'),
url('iconfont.woff?t=1623503003854') format('woff'),
url('iconfont.ttf?t=1623503003854') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-password:before {
content: "\ea41";
}
.icon-username:before {
content: "\e600";
}
main.css中引入
@import url(./iconfont.css);
两个项目的引入方式是一样的,最后的目录结构如下:
然后再分别去到两个应用的views/Home.vue中添加两个图标
<i class="iconfont icon-username"></i>
<i class="iconfont icon-password"></i>
刷新我们的浏览器
可以看到,当点击菜单切换时,都是空心图标,这明显有问题啊!我们明明一个有心有个无心!
如何解决?
当时在改造项目的过程中发现这个情况真的有点炸毛(fxxx = fine),不知道你是否有疑问,我为什么要把iconfont.css的代码贴出来,因为我们解决这个问题的关键就在于
font-family: "iconfont";
大家可以看到两个项目的iconfont.css
都有这么一句话,然后引入的方式都是class="iconfont icon-xxx"
的方式,我改造的项目也是如此,我猜测上面的问题跟这个有很大的关系,事实证明了我猜想是对的,下面我们来改造一下
首先回到iconfont的官网,去到我们刚刚添加的图标库页面,有个项目设置选项,点击后会看到如下两个选项
没错,解决冲突的关键就是为两个项目添加不同
的引用前缀
和font-family,主应用前缀改为main-app-icon-
,font-family
改为main-app-iconfont
,微应用相应改为micro-app-icon-
和micro-app-iconfont
然后重新下载两个图标库并重新引入,目前两个iconfont.css的关键代码如下
// 主应用的iconfont.css
@font-face {
font-family: "main-app-iconfont"; /* Project id 2608947 */
src: url('iconfont.woff2?t=1623508357834') format('woff2'),
url('iconfont.woff?t=1623508357834') format('woff'),
url('iconfont.ttf?t=1623508357834') format('truetype');
}
.main-app-iconfont {
font-family: "main-app-iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
// 微应用的iconfont.css
@font-face {
font-family: "micro-app-iconfont"; /* Project id 2608945 */
src: url('iconfont.woff2?t=1623508587683') format('woff2'),
url('iconfont.woff?t=1623508587683') format('woff'),
url('iconfont.ttf?t=1623508587683') format('truetype');
}
.micro-app-iconfont {
font-family: "micro-app-iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
相应的我们引入图标的方式也要改
// 主应用中
<i class="main-app-iconfont main-app-icon-username"></i>
<i class="main-app-iconfont main-app-icon-password"></i>
// 微应用中
<i class="micro-app-iconfont micro-app-icon-username"></i>
<i class="micro-app-iconfont micro-app-icon-password"></i>
改造完毕后刷新浏览器
可以看到,样式冲突的问题已经解决了
为什么会出现这个这个问题?
官方提供了基于shadowDom的样式隔离方案,不过似乎还是未做到完全的隔离,同类名的情况下可能还是会出现冲突,所以我们尽量通过不同类名
,添加前缀
的方式去避免样式冲突,或者是把类名降级放到一个父类
中去避免样式冲突
什么意思呢?例如主微应用都有类名aaa,那么就可能会出现冲突 但是如果我们主应用改成这样 .main-app > .aaa,微应用改成这样.micro-app > .aaa,把原本处于根的aaa样式用容器包装起来,就可以避免样式冲突,解决ui库样式冲突的方式也是这种思路,可以参考一下这篇文章
部署微前端
处理完样式问题啦,貌似没什么问题了,来打包部署一下吧
部署前的改造
还记得主应用micros/app.js
如下:
const apps = [
/**
* name: 微应用名称 - 具有唯一性
* entry: 微应用入口 - 通过该地址加载微应用
* container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
* activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
*/
{
name: "vue_micro_app",
entry: "//localhost:8081",
container: "#micro-container",
activeRule: "#/vue2-micro-app",
},
];
export default apps;
目前entry是写死的,我们可以部署的时候改,但是改来改去太麻烦啦,有没有更好的方法
if(process.env.NODE_ENV === 'development') {
}else {
}
还记得这种判断环境的代码吗,这里我们不用那么麻烦,vue-cli帮我们做好了,我们在根目录添加.env.production
、.env.development
文件,这两个文件就是用来导出一些变量,顾名思义这些变量分别用在dev和pro环境下的,具体可以点击这里了解
在.env.development
中添加
VUE_APP_MICRO_ENTRY="//localhost:8081"
至于.env.production
中就添加服务器的域名就可以啦
VUE_APP_MICRO_ENTRY="你的服务器域名"
这里我正式环境用的是localhost:3001
,稍后我会建本地服务器在3001端口部署微应用,3000端口部署主应用
这里文件中的变量一定要以VUE_APP_ 开头,否则是无效的
相应的app.js要改成如下格式:
// 新增
const { VUE_APP_MICRO_ENTRY } = process.env
const apps = [
/**
* name: 微应用名称 - 具有唯一性
* entry: 微应用入口 - 通过该地址加载微应用
* container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
* activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
*/
{
name: "vue_micro_app",
entry: VUE_APP_MICRO_ENTRY, // 修改
container: "#micro-container",
activeRule: "#/vue2-micro-app",
},
];
export default apps;
然后重启一下主应用,以后打包或者本地开发都不用再修改app.js啦
开始部署
接下来执行npm run build
或者 yarn run build
分别打包两个项目
然后可以新建一个项目名为mock-server,npm init
初始化一下后执行npm install koa
和 npm install koa-static
,并添加两个文件夹mian-app
和 micro-app
,分别把打包后的主应用和微应用放进这两个文件夹,再新建main-server.js
和micro-server.js
这时mock-server的目录结构如下
然后为main-server.js
和micro-server.js
添加如下代码
// main-server.js
const Koa = require('koa')
const path = require('path')
const app = new Koa()
const staticFiles = require('koa-static')
const staticPath = path.join(__dirname + '/main-app')
app.use(staticFiles(staticPath))
app.listen(3000, () => {
console.log('main server running at 3000')
})
--------------
// micro-server.js
const Koa = require('koa')
const path = require('path')
const app = new Koa()
const staticFiles = require('koa-static')
const staticPath = path.join(__dirname + '/micro-app')
app.use(staticFiles(staticPath))
app.listen(3001, () => {
console.log('main server running at 3001')
})
代码主要就是把打包出来的文件夹用koa分别在3000和3001端口跑起来,没什么特别的
然后访问一下,主应用正常运行,微应用报错了
上篇在微应用render函数中有这么一段代码:
function render(props) {
console.log("子应用render的参数", props)
// ----看这里----
props.onGlobalStateChange((state, prevState) => {
// state: 变更后的状态; prev 变更前的状态
console.log("通信状态发生改变:", state, prevState);
store.commit('setToken', '123456')
}, true);
// 挂载应用
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#micro-app");
}
没错,当子应用独立运行时,props是没有onGlobalStateChange参数的,所以这里要添加判断(添加的判断还真不少的说),改成下面这个样子:
function render(props) {
console.log("子应用render的参数", props)
// 新增判断,如果是独立运行不执行onGlobalStateChange
if(window.__POWERED_BY_QIANKUN__) {
props.onGlobalStateChange((state, prevState) => {
// state: 变更后的状态; prev 变更前的状态
console.log("通信状态发生改变:", state, prevState);
store.commit('setToken', '123456')
}, true);
}
// 挂载应用
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#micro-app");
}
重新build并放到mock-server中重新运行3001端口,刷新后可以看到微应用运行成功
跨域问题
当从主应用切换到微应用时
没错,经典的跨域问题,因为部署的是本地,有两个解决办法
第一个(不推荐)是作弊的方法
新建一个chrome浏览器的快捷方式,然后右键,属性
在目标这一栏, --user-data-dir=E:\MyChromeDevUserData
到末尾,注意--user
前有空格,然后用这个新建的快捷方式可以访问部署后的应用
第二种,使用koa2-cors
在mock-server中执行npm install koa2-cors,然后修改一下micro-server.js
const Koa = require('koa')
const path = require('path')
const app = new Koa()
const staticFiles = require('koa-static')
const cors = require('koa2-cors'); // 新增
const staticPath = path.join(__dirname + '/micro-app')
app.use(cors());// 新增
app.use(staticFiles(staticPath))
app.listen(3001, () => {
console.log('main server running at 3001')
})
重启micro-server.js
并刷新浏览器,可以看到切换菜单已经正常啦
第三种,利用nginx做代理(建议)
贴上nginx.conf
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
server {
# 监听的端口
listen 3001;
server_name localhost;
location / {
#允许跨域访问
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';
if ($request_method = 'OPTIONS') {
return 204;
}
# 代理的文件夹
root E:\project\vue-project\vue2-micro-app\dist;
autoindex on;
}
}
}
使用nginx后,我们的micro-app
和micro-server.js
已经不需要了,因为nginx已经做了代理,允许nginx,刷新浏览器,可以看到切换菜单已经正常啦
坑2:页面无法跳转问题
这个问题就是我上一节所说的巨坑,因为这个页面无法跳转,在本地是没有任何问题的!然而部署到测试环境后,100%复现,本地环境100%没问题,你看一步步走到现在也没发现这个问题,这就是程序员经典场景----我本机是好的呀o(╥﹏╥)o
注意,即使是使用nginx代理后在本地部署依然无法在本地复现这个问题,我会配合gif图来还原这个问题
场景还原(以下全部假设运行在测试服务器)
本地也部署跑过感觉没问题了,开开心心部署到测试服务器,然后一访问,瞬间傻眼了
为什么会这样呀??可以看到无论是本地还是测试服务器都是没有任何报错的,然后这个问题我搞了几乎3天
如何解决?
到了第三天的时候,我差不多想放弃微前端改造方案了,突然我发现,我们点击菜单的时候,url是有变化的,但是页面没有跳转,所以我又大胆猜测,是不是路由的问题,而且可以看到,每次我们在主微应用之间切换的时候,都会执行微应用main.js中导出的mount和unmount函数,然后注意到unmount有这么一段代码
export async function unmount() {
console.log("VueMicroApp unmount");
// 注意这里
instance.$destroy();
instance = null;
}
而微应用的router
的index.js
是这样的
微应用main.js中的render函数是这样的
可以看到,由始至终,router都是同一个实例!然后每次unmount都会执行应用卸载,会不会就是这个问题导致的呢
接下来改造微应用的router.js,不再导出router而是导出routes数组
然后改造main.js
import VueRouter from 'vue-router'
import routes from './router'
Vue.use(VueRouter)
// 新增:用于保存router实例
let router = null;
let microPath = ''
// 新增:动态设置 webpack publicPath,防止资源加载出错
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
microPath = '/vue2-micro-app'
}
function render(props) {
console.log("子应用render的参数", props)
if(window.__POWERED_BY_QIANKUN__) {
props.onGlobalStateChange((state, prevState) => {
// state: 变更后的状态; prev 变更前的状态
console.log("通信状态发生改变:", state, prevState);
store.commit('setToken', '123456')
}, true);
}
// 新增
router = new VueRouter({
routes
})
// 新增
router.beforeEach((to, from, next) => {
if (to.path !== (microPath + '/login')) {
if (store.state.token) {
next()
} else {
next(microPath + '/login')
}
} else {
next()
}
})
// 挂载应用
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#micro-app");
}
export async function unmount() {
console.log("VueMicroApp unmount");
instance.$destroy();
instance = null;
// 新增
router = null;
}
修改后的main.js,router不再是同一个实例,而是每次mount的时候都会新获取一个实例,相应的路由守卫也要搬迁出来,然后npm run serve看到本地运行微应用没问题,好npm run build重新打包并重新运行nginx
可以看到,这次部署是真的成功了
PS:在vue3中如果直接监听整个route对象,也会出现页面无法跳转的情况
欢迎指出不足和交流,踩坑不易,如果对你有帮助的话,点个赞吧~(#^.^#)
参考文献
明源云的qiankun教程:https://github.com/a1029563229/blogs/blob/master/BestPractices/qiankun/Communication.md
qinkun官网:https://qiankun.umijs.org/zh/api#initglobalstatestate
“分享、点赞、在看” 支持一波