简介
微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith
)后,随之而来的应用不可维护的问题。
这类问题在企业级 Web
应用中尤其常见。
- 微前端架构具备以下几个核心价值:
- 技术栈无关:主框架不限制接入应用的技术栈,微应用具备完全自主权
- 独立开发、独立部署:微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
- 增量升级:在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
- 独立运行时:每个微应用之间状态隔离,运行时状态不共享
qiankun
是一个基于 single-spa
的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统
qiankun
特性:
- 基于
single-spa
封装,提供了更加开箱即用的API
。- 技术栈无关,任意技术栈的应用均可 使用/接入,不论是
React/Vue/Angular/JQuery
还是其他等框架。HTML Entry
接入方式,让你接入微应用像使用iframe
一样简单。- 样式隔离,确保微应用之间样式互相不干扰。
JS
沙箱,确保微应用之间 全局变量/事件 不冲突。- 资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
umi
插件,提供了@umijs/plugin-qiankun
供umi
应用一键切换成微前端架构系统。
官方网站:qiankun
基本使用
这里以vue
为例,使用vue-cli 4.x
创建项目
主项目
-
创建主项目:
vue create main
-
安装
qiankun
依赖:npm i qiankun --save
-
修改
src/main.js
文件
import Vue from 'vue'
import App from './App.vue'
// 导入qiankun所需方法
import {
registerMicroApps, // 注册子应用
runAfterFirstMounted, // 当第一个子应用装载完毕
setDefaultMountApp, // 设置默认装载的子应用
// initGlobalState, // 微前端之间的通信
start // 启动
} from 'qiankun'
Vue.config.productionTip = false
// 渲染主应用
new Vue({
render: h => h(App)
}).$mount('#container')
// 注册子应用
registerMicroApps([
{
name: 'one',
entry: '//localhost:6661',
container: '#micro-view',
activeRule: '/one'
// activeRule: getActiveRule('/one')
},
{
name: 'two',
entry: '//localhost:6662',
container: '#micro-view',
// activeRule: '/two'
activeRule: getActiveRule('/two')
}
], {
beforeLoad: [
app => {
console.log('before load')
}
],
beforeMount: [
app => {
console.log('before mount')
}
],
afterMount: [
app => {
console.log('after mount')
}
],
beforeUnmount: [
app => {
console.log('before unmount')
}
],
afterUnmount: [
app => {
console.log('after unmount')
}
]
})
function getActiveRule(routerPrefix) {
return location => location.pathname.startsWith(`${routerPrefix}`)
}
// 设置默认app
setDefaultMountApp('one')
// 第一个子应用加载完毕后回调
runAfterFirstMounted(() => {
console.log('第一个子应用加载完毕后的回调')
})
// 启动qiankun
start()
- 修改
public/index.html
,将其中的id
修改为container
<div id="container"></div>
- 修改
src/App.vue
文件
<template>
<div id="main">
这是主应用
<br />
<button @click="changeView('/one')">子应用one</button>
<button @click="changeView('/two')">子应用two</button>
<hr>
<!-- 这里的 micro-view 对应的是上面 main.js 里面的子应用配置的 container: '#micro-view' -->
<div id="micro-view"></div>
</div>
</template>
export default {
name: 'App',
methods: {
changeView(who) {
window.history.pushState(null, who, who)
}
}
}
- 在根目录下添加
vue.config.js
文件,自定义主应用启动端口
module.exports = {
devServer: {
port: 3000
}
}
子应用
备注:子应用是不安装qiankun
依赖的,子应用只需要暴露对应的生命钩子即可
-
创建子应用
one
:vue create one
-
修改
src/router/index.js
文件。将new VueRouter
放到src/main.js
文件中统一处理
import Home from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
export default routes
- 修改
src/main.js
文件
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import routes from './router'
import './public-path'
Vue.use(VueRouter)
Vue.config.productionTip = false
let router = null
let instance = null
function render() {
!router && (router = new VueRouter({
mode: 'history',
base: window.__POWERED_BY_QIANKUN__ ? 'one' : '/',
routes
}))
!instance && (instance = new Vue({
router,
render: h => h(App)
}).$mount('#app'))
}
// 生命周期 - 挂载前
export async function bootstrap(props) {
console.log('one bootstrap')
}
// 生命周期 - 挂载后
export async function mount() {
console.log('one mount')
// 渲染
render()
}
// 生命周期 - 解除挂载
export async function unmount() {
console.log('one unmount')
instance.$destroy()
instance = null
router = null
}
// 本地调试
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
- 在
src/
目录下新建public-path.js
文件
/* eslint-disable */
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
/* eslint-disable */
- 在根目录下新建
vue.config.js
文件
const { name } = require('./package.json')
const port = 6661
module.exports = {
devServer: {
port,
// 允许被主应用跨域fetch请求到
headers: {
'Access-Control-Allow-Origin': '*'
}
},
configureWebpack: {
output: {
library: `${name}-[name]`,
// 把子应用打包成umd库格式
// 当我们把 libraryTarget 设置为 umd 后,我们的 library 就暴露为所有的模块定义下都可运行的方式了,主应用就可以获取到微应用的生命周期钩子函数了
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`
}
}
}
-
子应用
two
类似。为区分两个子应用,可以修改下对应的页面文件区分开 -
分别启动这两个子应用。访问主项目地址即可看到结果
主应用下发参数
使用注册子应用时的props
属性。将主应用需要传递给微应用的数据传递出去
- 修改主应用的
src/main.js
文件的registerMicroApps
,增加参数
// ...
// 注册子应用
registerMicroApps([
{
// ...
// 增加props参数,内容是自定义的
// 这里简单写了两个属性 msg和fn
props: {
msg: {
data: {
mt: 'you are one'
}
},
fn: {
show(msg) {
console.log('one:', msg)
}
}
}
},
{
// ...
// 增加props参数
props: {
msg: {
data: {
mt: 'you are two'
}
},
fn: {
show(msg) {
console.log('two:', msg)
}
}
}
}
], {
// ...
})
// ...
- 修改子应用的
src/main.js
文件,修改导出的mount
函数,将父级传入的fn
绑定到Vue
的原型上
// ...
// 生命周期 - 挂载后
export async function mount() {
// ...
// 设置主应用下发的方法
// 将主应用传递过来的 props.msg 的 data 绑定到子应用的原型链上。这样后面就可以直接访问到了
Vue.prototype.$msg = props.msg.data
// 主应用传递过来的 props.fn 是一个对象。循环遍历下,将各个属性绑定到子应用的原型链上
Object.keys(props.fn).forEach(method => {
Vue.prototype[`$${method}`] = props.fn[method]
})
// 渲染
render()
}
// ...
- 修改子应用的
src/views/About.vue
文件
<template>
<div class="about">
<h1>This is an two about</h1>
<p>{{ $msg.mt }}</p>
<br />
<button @click="testFn">测试事件</button>
</div>
</template>
export default {
methods: {
testFn() {
this.$show('测试事件成功')
}
}
}
应用间的通讯
使用initGlobalState
方法
- 修改主应用的
src/main.js
文件,增加通讯所需代码
// ...
// 通讯 - 这部分代码放在设置默认app之前即可
const actions = initGlobalState({
// 初始化state,里面内容您随意
mt: 'init'
})
// 在项目中任何需要监听的地方进行监听,在这里监听是为了方便
actions.onGlobalStateChange((state, prev) => {
console.log('main state change', state, prev)
})
// 将action对象绑到Vue原型上,为了项目中其他地方使用方便
Vue.prototype.$actions = actions
// 设置默认app
setDefaultMountApp('one')
// ...
- 修改主应用的
src/App.vue
,增加修改state
的方式
<template>
<div id="main">
<!-- ... -->
<!-- 增加修改state的按钮 -->
<button @click="changeState('1')">修改state = 1</button>
<button @click="changeState('2')">修改state = 2</button>
<hr>
<!-- 这里的 micro-view 对应的是上面 main.js 里面的子应用配置的 container: '#micro-view' -->
<div id="micro-view"></div>
</div>
</template>
export default {
name: 'App',
methods: {
// ...
changeState(value) {
// 修改state
this.$actions.setGlobalState({
mt: value
})
}
}
}
- 修改子应用的
src/main.js
文件
// ...
// 生命周期 - 挂载后
export async function mount(props) {
// ...
// 设置通讯
Vue.prototype.$onGlobalStateChange = props.onGlobalStateChange
Vue.prototype.$setGlobalState = props.setGlobalState
// 渲染
render(props)
}
// ...
- 修改子应用的
src/views/About.vue
文件,增加通讯监听
<p>this is my name --- {{ name }}</p>
export default {
data() {
return {
name: 'init name'
}
},
mounted() {
// 增加state监听,当mt数据发生变化的时候,我们修改name,体现在页面上
this.$onGlobalStateChange((state, prev) => {
if (state.mt !== prev.mt) {
this.name = state.mt
}
})
}
}
常见的坑
样式隔离
start
方法有个属性sandbox
。该值默认为true
为true
,可以隔离子页面的样式,但是*
、html
、body
等公共的样式还是会影响到主应用
可以配置为 { strictStyleIsolation: true }
表示开启严格的样式隔离模式。
这种模式下 qiankun
会为每个微应用的容器包裹上一个 shadow dom
节点,从而确保微应用的样式不会对全局造成影响。
这时候需要修改下子应用代码。否则会出现找不到#app
的情况[Vue warn]: Cannot find element: #app
- 修改子应用的
src/main.js
文件
// ...
function render(props = {}) {
const { container } = props
new Vue({
router,
render: h => h(App)
}).$mount(container ? container.querySelector('#app') : '#app')
}
// ...
// 生命周期 - 挂载后
export async function mount(props) {
// ...
// 渲染
render(props)
}
// ...
// 本地调试
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
子应用资源加载
- 修改
vue.config.js
文件
// ...
module.exports = {
// ...
publicPath:`//localhost:${port}`,
// ...
}
资源跨域访问
qiankun
是通过 fetch
去获取子应用注册时配置的静态资源url
,所有静态资源必须是支持跨域的,那就得设置允许源了
简单的设置可以看下面
http {
server {
listen 80;
add_header Access-Control-Allow-Origin *;
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;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
if ($request_method = 'OPTIONS') {
return 204;
}
location / {
root /data/www/html;
index index.html index.htm;
}
}
}
window._POWERED_BY_QIANKUN
子应用中的这个全局变量来区分当前是否运行在 qiankun
的主应用中
- 独立运行:
window.__POWERED_BY_QIANKUN__
为false
,执行render
创建vue
对象- 运行在
qiankun
:window.__POWERED_BY_QIANKUN__
为true
,会执行mount
周期函数,在这里创建vue
对象
子应用中可以根据这个全局变量做一些特殊处理
- 单独运行子应用,本地调试时
// 本地调试
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
- 处理路由的时候
router = new VueRouter({
mode: 'history',
// 如果运行在qiankun的主应用中,添加对应的路由前缀。这里的 one 应该跟主应用中的子应用配置 activeRule 对应
base: window.__POWERED_BY_QIANKUN__ ? 'one' : '/',
routes
})
父应用共享util和data给子应用
通过注册子应用时的props
属性。将主应用需要传递给微应用的数据传递出去
比如将主应用的store
数据和工具类库
- 主应用的
src/main.js
文件
import store from '@/store'
let msg = {
// 从主应用仓库读出的数据
data: store,
// 从主应用读出的工具类库
utils: LibrayrJs
}
- 子应用在挂载前,将
props
数据导到子应用通过遍历赋值给到子应用vue
原型中
export async function bootstrap(props = {}) {
Vue.prototype.$mainStore = props.data
Vue.prototype.$mainUtils = props.utils
}
history路由模式,打包后刷新404
通过nginx
配置加入try_files
,对于找不到url
的,将首页html
返回
http {
upstream gateway {
server ranchernode001.xxx.com:31306;
server ranchernode002.xxx.com:31306;
}
server {
listen 80;
location / {
try_files $uri $uri/ /index.html;
root /data/www/html;
index index.html index.htm;
}
location /a {
try_files $uri $uri/ /index.html;
}
location /a {
try_files $uri $uri/ /index.html;
}
location /gateway {
proxy_pass http://gateway;
}
}
}
try_files
:用来解决nginx
找不到client
客户端所需要的资源时访问404
的问题proxy_pass
:主要是用来配置接口网关反向代理,可以使得父子应用下访问的api
是一致的,防止接口跨域问
发表评论