基于 vue3.0-beta 及 qiankun2.0 极速尝鲜!微前端进阶实战项目。
项目地址:wl-mfehtml
微前端实战详细入门教程及解放方案请转至我另外一篇文章:微前端实战看这篇就够了 - Vue项目篇。
项目地址:[wl-micro-frontends [wl-qiankun]](https://github.com/hql7/wl-mi... && 在线访问前端
npm run cinit // 使用cnpm下载依赖,推荐cinit节省下载时间 npm run init // 或 使用npm下载依赖 npm run serve // 运行所有项目 npm run build // 打包所有项目
注意:若是下载报错,报 bin/sh 找不到start命令,那你多是mac or linux,那就进入目录一个一个下载运行吧。
另:执行批量服务耗时较久,请耐心等待,init与build成功会在控制台提示,serve稍加等待或刷新浏览器便可。vue
主应用须要用到elementui,暂时使用vue2.0+qiankun2.0版本。vue3.0beta体验在下面【子应用构建】章节linux
主应用项目主要在5个文件:utils
文件夹,app.vue
,appRegister.js
,main.js
,render.js
webpack
cnpm i qiankun -S
在主应用下载qiankun,注意使用2.0以上版本git
<template> <div class="main-container-view"> <el-scrollbar class="wl-scroll"> <!-- qiankun2.0 container 模式--> <div id="subapp-viewport" class="app-view-box"></div> <!-- qiankun1.0 render 模式--> <div v-html="appContent" class="app-view-box"></div> <div v-if="loading" class="subapp-loading"></div> </el-scrollbar> </div> </template> <script> export default { name: "rootView", props: { loading: Boolean, appContent: String } }; </script>
注意这里,qiankun2.0是根据 container
字段对应的dom id来注册子应用盒子的,所以只用qiankun2.0的话不须要考虑render注测子应用盒子的状况,下面那两个dom和script里的props
均可以不要!只留一个<div id="subapp-viewport"></div>
便可!
另外:注册子应用时每一个子应用均可以指定一个不一样的container
,所以若是想作每一个子应用的keep-alive,则可能须要每一个子应用对应一个<div id="subapp-viewport-ui"></div>
,<div id="subapp-viewport-blog"></div>
盒子github
import Vue from "vue" import router from './router' import store from './store' import App from './App.vue' /** * @name 提取vue示例化方法 */ export function vueRender() { Vue.config.productionTip = false new Vue({ router, store, render: h => h(App) }).$mount("#main-container"); }
为何要仅仅将这段代码从main.js
摘出呢?一方面是尽可能清洁main.js;另外一方面,就是为了兼容qiankun1.0的render方法。
由于qiankun1.0须要在注册vue实例时显式的将appContent
传入app.vue,若是你不用qiankun1.0版本,则彻底不须要如下代码:web
/** * @description 实例化vue,并提供子应用 render函数模式的装载能力 * @description 若是使用qiankun2.0 版本,只需正常实例化vue便可 不须要存在此render函数 * @param {Object} param0 * @description {String} appContent 子应用内容 * @description {Boolean} loading 是否显示加载动画(需手动实现loading效果) * @param {Boolean} notCompatible true则不兼容qiankun1.0 【此参数为示例添加,实际应用自酌】 */ export function vueRender({ appContent, loading }, notCompatible) { Vue.config.productionTip = false // 实际上本实例只用到此if内的代码 // 本文件其余代码只为作兼容qiankun1.0 render挂载子应用的参考 if (notCompatible) { new Vue({ router, store, render: h => h(App) }).$mount("#main-container"); return; } return new Vue({ router, store, data() { return { appContent, loading, }; }, render(h) { return h(App, { props: { appContent: this.content, loading: this.loading } }); } }).$mount('#main-container'); } let app = null; /** * @name 提供render装载子应用方法 * @param {Object} param0 * @description {String} appContent 子应用内容 * @description {Boolean} loading 是否显示加载动画(需手动实现loading效果) */ export default function render({ appContent, loading }) { if (!app) { app = vueRender({ appContent, loading }); } else { app.appContent = appContent; app.loading = loading; } }
此处是给兼容qiankun1.0 registerMicroApps方法render字段一种方案,事实上升级到2.0彻底无压力,所以建议不须要留下臃肿的render方法。vue-router
下面用了一个方法将qiankun须要用到的方法所有包装起来,以便后续将注册子应用放到获取后端注册表数据后执行。vuex
/** * @name 启用qiankun微前端应用 * @param {*} list * @param {*} defaultApp */ const useQianKun = (list, defaultApp) => { /** * @name 注册子应用 * @param {Array} list subApps */ registerMicroApps( [ { name: 'subapp-ui', // 子应用app name 推荐与子应用的package的name一致 entry: '//localhost:6751', // 子应用的入口地址,就是你子应用运行起来的地址 container: '#yourContainer', // 挂载子应用内容的dom节点 `# + dom id`【见上面app.vue】 activeRule: '/ui', // 子应用的路由前缀 }, ], { beforeLoad: [ app => { console.log('[LifeCycle] before load %c%s', 'color: green;', app.name); }, ], beforeMount: [ app => { console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name); }, ], afterUnmount: [ app => { console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name); }, ], }, ) /** * @name 设置默认进入的子应用 * @param {String} 须要进入的子应用路由前缀 */ setDefaultMountApp('ui'); /** * @name 启动微前端 */ start(); /** * @name 微前端启动进入第一个子应用后回调函数 */ runAfterFirstMounted(() => { console.log('[MainApp] first app mounted'); }); }
结合请求后端注册表,并给子应用分发路由及数据改造后的完整代码:
import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from "qiankun"; import store from "./store"; /** * @name 导入render函数兼容qiakun1.0装载子应用方法,若是使用2.0container装载则不须要此方法,此处留着注释代码提供兼容qiankun1.0的示例 * @description 此处留下注释代码仅为提供兼容qiankun1.0示例 */ // import render from './render'; /** * @name 导入接口获取子应用注册表 */ import { getAppConfigsApi } from "./api/app-configs" /** * @name 导入消息组件 */ import { wlMessage } from './plugins/element'; /** * @name 导入想传递给子应用的方法,其余类型的数据皆可按此方式传递 * @description emit建议主要为提供子应用调用主应用方法的途径 */ import emits from "./utils/emit" /** * @name 导入qiankun应用间通讯机制appStore */ import appStore from './utils/app-store' /** * @name 声明子应用挂载dom,若是不须要作keep-alive,则只须要一个dom便可; */ const appContainer = "#subapp-viewport"; /** * @name 声明要传递给子应用的信息 * @param data 主应要传递给子应用的数据类信息 * @param emits 主应要传递给子应用的方法类信息 * @param utils 主应要传递给子应用的工具类信息(只是一种方案) * @param components 主应要传递给子应用的组件类信息(只是一种方案) */ let props = { data: store.getters, emits } /** * @name 请求获取子应用注册表并注册启动微前端 */ getAppConfigsApi().then(({ data }) => { // 验证请求错误 if (data.code !== 200) { wlMessage({ type: 'error', message: "请求错误" }) return; } // 验证数据有效性 let _res = data.data || []; if (_res.length === 0) { wlMessage({ type: 'error', message: "没有能够注册的子应用数据" }) return; } // 处理菜单并存入主应用Store store.dispatch('menu/setMenu', _res); // 处理子应用注册表数据。详细数据见 master mock let apps = []; // 子应用数组盒子 let defaultApp = null; // 默认注册应用 let isDev = process.env.NODE_ENV === 'development'; // 根据开发环境|线上环境加载不一样entry _res.forEach(i => { apps.push({ name: i.module, // 子应用名 entry: isDev ? i.devEntry : i.depEntry, // 根据环境注册生产环境or开发环境地址 container: appContainer, // 绑定dom activeRule: i.routerBase, // 绑定子应用路由前缀 props: { ...props, routes: i.children, routerBase: i.routerBase } // 将props及子应用路由,路由前缀由主应用下发 }) if (i.defaultRegister) defaultApp = i.routerBase; // 记录默认启动子应用 }); // 启用qiankun微前端应用 useQianKun(apps, defaultApp); }) /** * @name 启用qiankun微前端应用 * @param {*} list * @param {*} defaultApp */ const useQianKun = (list, defaultApp) => { /** * @name 注册子应用 * @param {Array} list subApps */ registerMicroApps( list, { beforeLoad: [ app => { console.log('[LifeCycle] before load %c%s', 'color: green;', app.name); }, ], beforeMount: [ app => { console.log('[LifeCycle] before mount %c%s', 'color: green;', app.name); }, ], afterUnmount: [ app => { console.log('[LifeCycle] after unmount %c%s', 'color: green;', app.name); }, ], }, ) /** * @name 设置默认进入的子应用 * @param {String} 须要进入的子应用路由前缀 */ setDefaultMountApp(defaultApp); /** * @name 启动微前端 */ start(); /** * @name 微前端启动进入第一个子应用后回调函数 */ runAfterFirstMounted(() => { console.log('[MainApp] first app mounted'); }); } /** * @name 启动qiankun应用间通讯机制 */ appStore(initGlobalState);
上面注册子应用时,咱们看到代码里有传给子应用的props
和一个appStore
通讯函数。
props
,看过我上个文章的朋友都知道我将props分为那几个模块,实际上,我真正用到的可能就是主应用请求获取下来的routes
和routerbase
下发给子应用。appStore
方法,我是将官方通讯机制提取至utils文件夹下的app-store.js
文件,并和vuex相结合。代码以下:import store from "@/store"; /** * @name 启动qiankun应用间通讯机制 * @param {Function} initGlobalState 官方通讯函数 * @description 注意:主应用是从qiankun中导出的initGlobalState方法, * @description 注意:子应用是附加在props上的onGlobalStateChange, setGlobalState方法(只用主应用注册了通讯才会有) */ const appStore = (initGlobalState) => { /** * @name 初始化数据内容 */ const { onGlobalStateChange, setGlobalState } = initGlobalState({ msg: '来自master初始化的消息', }); /** * @name 监听数据变更 * @param {Function} 监听到数据发生改变后的回调函数 * @des 将监听到的数据存入vuex */ onGlobalStateChange((value, prev) => { console.log('[onGlobalStateChange - master]:', value, prev); store.dispatch('appstore/setMsg', value.msg) }); /** * @name 改变数据并向全部应用广播 */ setGlobalState({ ignore: 'master', msg: '来自master动态设定的消息', }); } export default appStore;
【注意:如未在主应用注册通讯,则在子应用也获取不到通讯方法】
终于咱们来到了最后一步,主应用一切改造完成以后,咱们将其引入到main.js并执行:
/** * @name 统一注册外部插件、样式、服务等 */ import './install' /** * @name 微前端基座主应用vue实例化 * @description 为了兼容 qiankun1.0 的render函数装载子应用能力 * @description 2.0版本正常实例化vue便可,不须要此render函数 * @description qiankun registerMicroApps方法 render用到,若是使用container装载子应用,无需此render函数 * @deprecated 本示例只针对 qiankun2.0 所以只留下注释后的代码在此提醒各位读者如何兼容qiankun1.0 */ /* import render from './render'; render({ loading: true }) */ import { vueRender } from './render' vueRender({}, true) /** * @name 注册微应用并启动微前端 */ import './appRegister'
子应用使用vue3.0beta尝鲜,大部分时间都用在找3.0的api上,还有许多未解决的问题,好比往vue实例上挂载方法,手动注销vue是啥api,怎么注册插件好比elementUI等,后续会慢慢补充。
这里使用vue3.0beta实现demo效果已经没问题!
默认你已经装了vuecli3.0以上版本
vue crate subapp-ui cd subapp-ui // 在此以前都是正常建立项目,到这里执行下面命令会以插件的形式将项目升级至3.0 vue add vue-next
在这里不单独赘述vue3.0beta的特性,对此网上有许多文章。咱们在实践咱们微前端的需求实际应用中取逐渐解开它的神秘面纱!
注意设置publicPath、端口号与注册子应用时一致
注意开发时开启headers跨域头信息
注意output按照规定格式打包
const { name } = require("./package"); const port = 6751; // dev port const dev = process.env.NODE_ENV === "development"; module.exports = { publicPath: dev ? `//localhost:${port}` : "/", filenameHashing: true, devServer: { hot: true, disableHostCheck: true, port, overlay: { warnings: false, errors: true }, headers: { "Access-Control-Allow-Origin": "*" } }, // 自定义webpack配置 configureWebpack: { output: { // 把子应用打包成 umd 库格式 library: `${name}-[name]`, libraryTarget: "umd", jsonpFunction: `webpackJsonp_${name}` } } };
if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
我在这里区分微前端环境和单独运行的加载机制,并引入官方通讯方法
注意:3.0beta的实例化方法为 createApp,而且注册路由是经过连续use的方法,详见下放代码:
注意:3.0的router实例化方法为 createRouter, 注意history模式经过createWebHistory方法实现,而且此方法接受一个参数表示路由前缀
注意:3.0的vuex却是变化不大,但暂未弄明白3.0的mapGetters,mapActions的使用方法
import { createApp } from "vue"; import { createRouter, createWebHistory } from "vue-router"; import App from "./App.vue"; import store from "./store"; import selfRoutes from "./router/routes"; /** * @name 导入自定义路由匹配方法 */ import routeMatch from "./router/routes-match"; /** * @name 导入官方通讯方法 */ import appStore from "./utils/app-store"; const __qiankun__ = window.__POWERED_BY_QIANKUN__; let router = null; let instance = null; /** * @name 导出生命周期函数 */ const lifeCycle = () => { return { /** * @name 微应用初始化 * @param {Object} props 主应用下发的props * @description bootstrap 只会在微应用初始化的时候调用一次,下次微应用从新进入时会直接调用 mount 钩子,不会再重复触发 * @description 一般咱们能够在这里作一些全局变量的初始化,好比不会在 unmount 阶段被销毁的应用级别的缓存等 */ async bootstrap(props) { console.log('props:', props) /* props.emits.forEach(i => { Vue.prototype[`$${i.name}`] = i; }); */ }, /** * @name 实例化微应用 * @param {Object} props 主应用下发的props * @description 应用每次进入都会调用 mount 方法,一般咱们在这里触发应用的渲染方法 */ async mount(props) { // 注册应用间通讯 appStore(props); // 注册微应用实例化函数 render(props); }, /** * @name 微应用卸载/切出 */ async unmount() { instance.$destroy?.(); instance = null; router = null; }, /** * @name 手动加载微应用触发的生命周期 * @param {Object} props 主应用下发的props * @description 可选生命周期钩子,仅使用 loadMicroApp 方式手动加载微应用时生效 */ async update(props) { console.log("update props", props); } }; }; /** * @name 子应用实例化函数 * @param {Object} props param0 qiankun将用户添加信息和自带信息整合,经过props传给子应用 * @description {Array} routes 主应用请求获取注册表后,从服务端拿到路由数据 * @description {String} 子应用路由前缀 主应用请求获取注册表后,从服务端拿到路由数据 */ const render = ({ routes, routerBase, container } = {}) => { router = createRouter({ history: createWebHistory(__qiankun__ ? routerBase : "/"), routes: __qiankun__ ? routeMatch(routes, routerBase) : selfRoutes }); instance = createApp(App).use(router).use(store).mount(container ? container.querySelector("#app") : "#app"); }; export { lifeCycle, render };
import store from "@/store"; import { DataType } from "wl-core" /** * @name 声明一个常量准备将props内的部份内容储存起来 */ const STORE = {}; /** * @name 启动qiankun应用间通讯机制 * @param {Object} props 官方通讯函数 * @description 注意:主应用是从qiankun中导出的initGlobalState方法, * @description 注意:子应用是附加在props上的onGlobalStateChange, setGlobalState方法(只用主应用注册了通讯才会有) */ const appStore = props => { /** * @name 监听应用间通讯,并存入store */ props?.onGlobalStateChange?.( (value, prev) => { console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev) store.dispatch('appstore/setMsg', value.msg) }, true ); /** * @name 改变并全局广播新消息 */ props?.setGlobalState?.({ ignore: props.name, msg: `来自${props.name}动态设定的消息`, }); /** * @name 将你须要的数据存起来,供下面setState方法使用 */ STORE.setGlobalState = props?.setGlobalState; STORE.name = props.name; }; /** * @name 全局setState方法,修改的内容将通知全部微应用 * @param {Object} data 按照你设定的内容格式数据 */ const setState = (data) => { if (!DataType.isObject(data)) { throw Error('data必须是对象格式'); } STORE.setGlobalState?.({ ignore: STORE.name, ...data }) } export { setState } export default appStore;
这里分别导出了setState
,appStore
两个方法,appStore
在上面life-cycle.js
生命周期文件中注册全局通讯使用,那么setState
咱们又要在哪里使用呢?咱们继续往下看
将生命周期函数导出,并提供单独运行逻辑
import "./public-path"; import { lifeCycle, render } from "./life-cycle"; /** * @name 导出微应用生命周期 */ const { bootstrap, mount, unmount } = lifeCycle(); export { bootstrap, mount, unmount }; /** * @name 单独环境直接实例化vue */ const __qiankun__ = window.__POWERED_BY_QIANKUN__; __qiankun__ || render();
这里在views/index.vue
作实战演练
要求:
直接上代码:
<template> <div class="home"> <div class="msg-box"> <div class="msg-title">这里是子应用:</div> <div class="msg-context">{{selfMsg}}</div> </div> <div class="msg-box"> <div class="msg-title">来自其余微应用的消息:</div> <div class="msg-context">{{vuexMsg}}</div> </div> <div class="msg-box"> <div class="msg-ipt-box"> <input class="msg-ipt" type="text" v-model="formMsg" placeholder="请输入你想广播的话" /> </div> <div class="msg-btn-box"> <button class="msg-btn" @click="handleVuexMsgChange">发送广播</button> </div> </div> </div> </template> <script> import { ref, computed, getCurrentInstance } from "vue"; import { setState } from "@/utils/app-store"; export default { name: "Home", setup() { /** * @name 经过getCurrentInstance方法获得当前上下文 */ const { ctx } = getCurrentInstance(); /** * @name 定义一个初始数据 */ const selfMsg = ref("subapp-ui"); /** * @name 定义一个计算属性,返回vuex中的数据 */ const vuexMsg = computed(() => ctx.$store.getters.msg); /** * @name 定义一个表单元素v-model绑定的变量 */ const formMsg = ref(""); /** * @name 定义一个广播事件 */ const handleVuexMsgChange = () => { /** * @name 注意:在setup内部使用定义的变量,须要用**.value取值! */ setState({ msg: formMsg.value }); }; // 注意变量和事件都要return出来 return { selfMsg, vuexMsg, formMsg, handleVuexMsgChange }; } }; </script>
到这里已经完成了一个简单使用的 vue3.0 + qiankun2.0 微前端应用实践,快来上手试试吧!
项目地址:Github;
多是你见过最完善的微前端解决方案
微前端的核心价值
目标是最完善的微前端解决方案 - qiankun 2.0
qiankun
若是你有心,能够请做者喝杯咖啡,或者推荐一份好工做
<div> <img src="http://wlsy.oss-cn-hangzhou.aliyuncs.com/apply.jpg" height="330" width="220" /> <img src="http://wlsy.oss-cn-hangzhou.aliyuncs.com/wx.jpg" height="330" width="220" /></div>