微前端的介绍,大家已经看过很多了,这里就不多做介绍了,直接上手demo,快速入门,连老奶奶都学得会咯!(文末有项目代码)~
初始化项目
- 初始化主应用
初始化项目(安装的时候勾选上vue router、vuex)
vue create main-app
安装UI框架element-ui、微前端框架qiankun
npm i element-ui qiankun --save
- 初始化子应用
初始化项目(安装的时候勾选上vue router、vuex)
vue create main-app
安装UI框架element-ui
npm i element-ui --save
基础版
编写主应用代码
- /src/main.js
import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' Vue.config.productionTip = false import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI); import { registerMicroApps, start } from 'qiankun'; registerMicroApps([ { name: 'vue', // 子应用名称 entry: '//localhost:10000', // 子应用入口 container: '#vue', // 子应用在主应用中显示的位置 activeRule: '/vue', // 子应用激活的路由规则 } ]); start(); new Vue({ router, store, render: h => h(App) }).$mount('#app')
代码详解:
import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI);
引入element-ui
import { registerMicroApps, start } from 'qiankun'; registerMicroApps([ { name: 'vue', // 子应用名称 entry: '//localhost:10000', // 子应用入口 container: '#vue', // 子应用在主应用中显示的位置 activeRule: '/vue', // 子应用激活的路由规则 } ]); start();
微应用有两种加载方式,一种是基于路由的加载方式,另外一种是基于手动的加载方式,这里我们使用第一种。
使用registerMicroApps
进行配置。registerMicroApps(apps, lifeCycles?)
接收两个参数,第一个必填项,填入微应用的注册信息。第二个为生命周期钩子,选填,可填入的项目有beforeLoad
、beforeMount
、afterMount
、beforeUnmount
、afterUnmount
,这里为了降低demo的理解难度,先不填入了,只使用第一个参数。
使用start
开启qiankun
- /src/App.vue
<template> <div> <el-menu :router="true" mode="horizontal" default-active="/"> <el-menu-item index="/">Home</el-menu-item> <el-menu-item index="/about">About</el-menu-item> <el-menu-item index="/vue">Vue</el-menu-item> </el-menu> <router-view></router-view> <div id="vue"></div> </div> </template>
代码详解:
<el-menu :router="true" mode="horizontal" default-active="/"> <el-menu-item index="/">Home</el-menu-item> <el-menu-item index="/about">About</el-menu-item> <el-menu-item index="/vue">Vue</el-menu-item> </el-menu>
element-ui提供的菜单栏
<router-view></router-view>
作为主应用本身的路由组件容器
<div id="vue"></div>
作为子应用的容器,跟main.js里面registerMicroApps
注册的container
要一致。
- /src/views/Home.vue
<template> <div class="home"> <h1>主应用 Home</h1> </div> </template>
- /src/views/About.vue
<template> <div class="about"> <h1>主应用 About</h1> </div> </template>
编写子应用代码
- /src/main.js
import Vue from 'vue' import App from './App.vue' import store from './store' import router from './router' import ElementUI from 'element-ui'; import 'element-ui/lib/theme-chalk/index.css'; Vue.use(ElementUI); Vue.config.productionTip = false let instance; function render() { // 加载vue实例 instance = new Vue({ router, store, render: h => h(App) }).$mount('#app') } // 独立运行微应用 if (!window.__POWERED_BY_QIANKUN__) { render(); } // 被主应用使用时 if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } export async function bootstrap() {} export async function mount(props) { render(props) } export async function unmount(props) { instance.$destroy() instance = null }
代码详解:
let instance; function render() { // 加载vue实例 instance = new Vue({ router, store, render: h => h(App) }).$mount('#app') }
这里要使用一个render函数
将原有的实例Vue的过程包裹起来是有两个原因,一个是我们需要在主应用需要渲染子应用的时候再去渲染子应用,另外是我们也需要支持可以独立运行子应用,因此需要让渲染这个过程变得具备可控性,需要渲染的时候再去调用render函数
进行渲染
// 独立运行微应用 if (!window.__POWERED_BY_QIANKUN__) { render(); }
可以window.__POWERED_BY_QIANKUN__
通过这个全局变量来区分当前是否运行在qiankun的主应用的上下文中,如果该变量不存在,则证明当前子应用应当独立运行,所以调用render函数
进行渲染
// 被主应用使用时 if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
我们的主应用使用的端口是8080
,而我们的子应用使用的是10000
,如果不设置这个,我们对子应用路由的请求会以http://localhost:8080
开头,而不是以正确的http://localhost:10000
开头。
runtime publicPath 主要解决的是微应用动态载入的 脚本、样式、图片 等地址不正确的问题。
export async function bootstrap() {} export async function mount() { render() } export async function unmount() { instance.$destroy() }
根据规定,微应用需要在自己的入口js文件导出bootstrap
、mount
、unmount
三个生命周期钩子,以供主应用在适当的时机调用。
bootstrap
只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用mount
钩子,不会再重复触发bootstrap
。通常我们可以在这里做一些全局变量的初始化,比如不会在unmount
阶段被销毁的应用级别的缓存等。- 应用每次进入都会调用
mount
方法,通常我们在这里触发应用的渲染方法 - 应用每次
切出/卸载
会调用的方法,通常在这里我们会卸载微应用的应用实例
因此,我们在mount
里面进行子应用的渲染,在unmount
里面进行子应用的卸载。
- /src/App.vue
<template> <div class="child-app"> <el-menu :router="true" default-active="/" class="child-menu"> <el-menu-item index="/">Home</el-menu-item> <el-menu-item index="about">About</el-menu-item> </el-menu> <router-view /> </div> </template> <style scoped> .child-app { width: 400px; display: flex; border: 1px solid black; margin-top: 20px; padding: 10px; } .child-menu { margin-right: 20px; } </style>
- /src/view/Home.vue
<template> <div class="home"> <h2>子应用 Home</h2> </div> </template>
- /src/views/About.vue
<template> <div class="about"> <h2>子应用 About</h2> </div> </template>
- /src/router/index.js
//... const router = new VueRouter({ mode: 'history', base: window.__POWERED_BY_QIANKUN__ ? '/vue' : process.env.BASE_URL, routes }) //...
作为子应用运行时,子应用的路由生效规则应该和跟主应用main.js里面registerMicroApps
注册的activeRule
要一致,所以设置base
为'/vue'
- /vue.config.js
module.exports = { devServer: { port: 10000, headers: { 'Access-Control-Allow-Origin': '*' // 主应用获取子应用时跨域响应头 } }, configureWebpack: { output: { library: `vue`, libraryTarget: 'umd', } } }
由于是8080
端口到10000
端口的访问,我们需要配置跨域允许,另外,为了让主应用能正确识别微应用暴露出来的一些信息,还需要多加些webpack
的打包配置。
开跑
至此,主应用和子应用准备完毕,开跑!
- 测试子应用
打开http://localhost:10000
- 测试主应用
打开http://localhost:8080
如果你也能看到如下的效果,证明主应用、子应用都完美运行了
通信版
从上面的效果来看,我们已经实现微前端的基础使用了,成功的将子应用嵌入到主应用中去,但是,我们在项目中,经常会涉及到一些需要主应用和子应用通信的场景,由于两个都是vue项目,所以我们使用vuex来实现通信。
编写主应用
- /src/main.js
// ... registerMicroApps([ { name: 'vue', // app name registered entry: '//localhost:10000', container: '#vue', activeRule: '/vue', // 多加了下面这段 props: { initState: store.state } } ]); // ...
把主应用的state
,使用props
,传递给子应用,子应用可在导出的mount
函数里面接收
- /src/store/index.js
// ... mutations: { addNum(state, n) { state.num += n } } // ...
- /src/App.vue
<template> <div> <el-menu :router="true" mode="horizontal" default-active="/"> <el-menu-item index="/">Home</el-menu-item> <el-menu-item index="/about">About</el-menu-item> <el-menu-item index="/vue">Vue</el-menu-item> </el-menu> <h1>主应用的state.num:{{$store.state.num}}</h1> <button @click="addNum(10)">主应用增加10</button> <router-view></router-view> <div id="vue"></div> </div> </template> <script> export default { methods: { addNum(n) { this.$store.commit('addNum', 10) } } } </script>
编写子应用
- /src/main.js
// ... function render(props = {}) { if (!store.hasModule('global')) { // 当独立运行微应用时,state需要初始化赋值,否则会报错 const initState = props.initState || { num: 0 } // 将父应用的数据存储到子应用中,命名空间固定为global const globalModule = { namespaced: true, state: initState, mutations: { addNum(state, n) { state.num += n } }, }; // 注册一个动态模块 store.registerModule('global', globalModule); } // 加载vue实例 instance = new Vue({ router, store, render: h => h(App) }).$mount('#app') } // ... export async function mount(props) { render(props) } // ...
mount
接收主应用传递过来的数据,将其传递给render函数
render函数
先判断子应用store
里面有没有注册过global
模块,如果没有的话,使用props
接收主应用传递过来的state
,并且将其作为global
模块的state
数据来源,动态注册。之后,我们使用global
模块,实际上就是使用父传递过来的数据,由此实现数据通信。
- /src/views/Home.vue
<template> <div class="home"> <h2>子应用 Home</h2> <h2>主应用的state.num:{{$store.state.global.num}}</h2> <button @click="addNum(10)">主应用增加20</button> </div> </template> <script> export default { methods: { addNum() { this.$store.commit('global/addNum', 20) } } } </script>
开跑
至此,主应用和子应用准备完毕,开跑!
- 测试子应用
打开http://localhost:10000
可以看到,此时子应用中的按钮也是可以正常点击的,这是因为在render函数
中,我们给了其初始值的缘故
const initState = props.initState || { num: 0 }
- 测试主应用
打开http://localhost:8080
可以看到,无论是点击主应用里面的按钮,还是子应用里面的按钮,数据都能做到同步响应式的更新,这证明我们的数据通信已经成功实现!
项目代码
github.com/xiezijie439…
如果对你有帮助的话,麻烦给我一颗小星星~