微前端(qiankun)最直白Demo带你轻松入门
微前端(qiankun)最直白Demo带你轻松入门
🥗初篇
因为前段时间公司一个原先用react技术栈的后台项目,需要增加一个自定义图表的功能,接触过自定义图表的都清楚自定义图表的复杂程度,所以我们研发小组最终决定找一款开源的自定义图表来完成该需求,最终选择了gitee上一款star有7.6k项目,项目地址:gitee.com/anji-plus/r…
随之而来的就是如何将两个项目合二为一这个大问题,粗略看了下该开源的自定义图表光包就接近有百个,而且该开源项目使用的是vue,粗暴合并需要花费的时间难以想象。
最终查询了诸多信息后,看到了微前端qianduan能够解决我现在所面临的问题,最终将公司原项目与自定义图表合并的问题解决后,遂有感而发,书写一个微前端的入门文章,将我自己的理解以及实践记录一下,希望也对有需要的小伙伴一些帮助;
如果文章有知识点错误之处,欢迎大家指正,共同学习进步,谢谢!
一、了解微前端
微前端的概念其实非常类似于后端的微服务架构,其出发点就是将一个巨石应用分为多个子应用,每个子应用后可以独立运行、开发、部署;最终达到分布式,多团队并行的项目需求。
微前端是有一个主应用也称基石项目为主体,嵌入其他n多个的子应用,这些子应用之间都是相互独立,并且可以相互通信的。
微前端特点:
- 技术栈无关(主框架不限制接入应用的技术栈,微应用具备完全自主权);
- 独立开发、独立部署(微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新);
- 增量升级(在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全面的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略);
- 独立运行时(每个微应用之间状态隔离,运行时状态不共享);
二、搭建微前端 qiankun
2-1、demo 技术栈选择
为了保证demo能够尽可能的测试到上面的微前端特点,我将选用两种不同技术栈的后台独立项目,完成demo案例!
此次案例主应用我准备使用Vue
技术栈的vue-admin-template
(算是Vue
圈子内知名的开源后台项目了,这个admin-template
是基础模板,没有其他多余的功能):

子应用则是使用了React
技术栈,项目是我自己用webpack5 + typescript + react18
从零搭建的一个后台项目基础模板,希望大家能够支持下,给作者我点点star
,谢谢哈!

2-2、主应用安装qiankun
npm i qiankun && yarn add qiankun
2-3、主应用微前端配置
1.创建放置微应用路由文件夹

2. 创建app.js(放置微应用路由内容)
// microApps 微应用集合
const microApps = [
{
name: 'react-app',
entry: '//localhost:9090',
activeRule: '#/react-app', // 如果使用hashRouter 则在微应用路由路径前加 #
// activeRule: '/react-app', // 使用historyRouter,无需添加 #
// 主应用给微应用传参,后面会写到传参相关内容
props:{}
},
]
const apps = microApps.map((item) => {
return {
...item,
container: '#qiankun-viewport', // 微应用挂载在主应用中的盒子的 id
}
})
export default apps
🥗 配置参数详解
- name: 必选,微应用的名称,微应用之间必须确保唯一。
- entry:必选,微应用的入口路径地址。
- container:必选,微应用的容器节点的选择器或者 Element 实例。
- activeRule:必选,微应用的激活规则。
- loader:可选,loading 状态发生变化时会调用的方法。
- props:可选,主应用需要传递给微应用的数据。
3.在主应用中设置放置微应用挂载用的盒子(div)
一般都是位于主应用 layout 中的 content 组件的位置

4.使用 qiankun 的 registerMicroApps 注册微应用信息,并且配置触发钩子(创建 /qiankun/index.js)
import apps from './app'
import {
registerMicroApps,
addGlobalUncaughtErrorHandler,
start
} from 'qiankun'
// 注册微应用基础信息
// 即 apps 微应用数组,后面对象为配置触发钩子,与vue的钩子类似
registerMicroApps(apps, {
beforeLoad: app => {
console.log("before load app.name====>>>>>", app.name);
},
beforeMount: [
app => {
console.log("before mount app.name====>>>>>", app.name);
}
],
afterMount: [
app => {
console.log("after mount app.name====>>>>>", app.name);
}
],
afterUnmount: [
app => {
console.log("after unmount app.name====>>>>>", app.name);
}
]
})
// 添加全局的未捕获异常处理器
addGlobalUncaughtErrorHandler((event) => {
console.err('微应用加载失败!', event);
})
export default start
🥙 配置参数详解
- beforeLoad -
Lifecycle | Array<Lifecycle>
- 可选 - beforeMount -
Lifecycle | Array<Lifecycle>
- 可选 - afterMount -
Lifecycle | Array<Lifecycle>
- 可选 - beforeUnmount -
Lifecycle | Array<Lifecycle>
- 可选 - afterUnmount -
Lifecycle | Array<Lifecycle>
- 可选
5. 在主应用的 main.js 中启动 qiankun
// qiankun 微应用
import start from '@/qiankun'
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
// start 启动函数需要放置在 vue注册之后
start({
sandbox: {
// strictStyleIsolation: true, // 开启严格的样式隔离模式
experimentalStyleIsolation: true // 开启单实例场景子应用之间的样式隔离
}
})
🌮 start 配置参数详解
- prefetch:可选,是否开启预加载,默认为
true
;
- 配置为
true
则会在第一个微应用mount
完成后,开始预加载所有微应用的静态资源。 - 配置为
'all'
则主应用start
后即开始预加载所有微应用静态资源 - 配置为
string[]
则会在第一个微应用mounted
后开始加载数组内的微应用资源 - 配置为
function
则可完全自定义应用的资源加载时机 (首屏应用及次屏应用)
- sandbox:可选,是否开启沙箱,默认为
true
。
- 默认情况下沙箱可以确保单实例场景子应用之间的样式隔离,但是无法确保主应用跟子应用、或者多实例场景的子应用样式隔离。当配置为
{ strictStyleIsolation: true }
时表示开启严格的样式隔离模式。这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响。
- singular:可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为
true
。 - fetch -
Function
- 可选,自定义的 fetch 方法。 - getPublicPath -
(entry: Entry) => string
- 可选,参数是微应用的 entry 值。 - getTemplate -
(tpl: string) => string
- 可选。 - excludeAssetFilter -
(assetUrl: string) => boolean
- 可选,指定部分特殊的动态加载的微应用资源(css/js) 不被 qiankun 劫持处理。
此处参数配置信息都是截取自官网,如果有详细想了解的小伙伴,可直接点击链接跳转:qiankun
6. 主应用添加微应用的菜单路由
// qiankun react-app
{
path: '/react-app',
component: Layout,
redirect: '/react-app/home',
name: 'React',
meta: { title: 'React', icon: 'el-icon-s-help' },
children: [
{
path: 'login',
name: 'login',
meta: { title: '登录页', icon: 'nested' }
},
{
path: 'home',
name: 'home',
meta: { title: '主页', icon: 'nested' }
},
{
path: 'test2',
name: 'test2',
meta: { title: '测试2', icon: 'nested' }
}
]
},
到此为止,主应用部分改造完毕,接下来,需要对微应用进行改造;
2-4、微应用改造
1.打包工具的改造
本文案例中 微应用是使用 webpage5
作为打包工具:
进入 build/webpack.base.js
原来的 output
打包配置
// 打包文件出口
output: {
filename: 'static/js/[name].[chunkhash:8].js', // 每个输出js的名称
path: path.resolve(__dirname, '../dist'), // 打包的出口文件夹路径
clean: true, // webpack4需要配置clean-webpack-plugin删除dist文件,webpack5内置了。
publicPath: isDev ? '/' : './', // 打包后文件的公共前缀路径
},
// 打包文件出口
output: {
filename: 'static/js/[name].[chunkhash:8].js', // 每个输出js的名称
path: path.resolve(__dirname, '../dist'), // 打包的出口文件夹路径
publicPath: isDev ? '/' : './', // 打包后文件的公共前缀路径
// qiankun
library: `react-app`, // 此处名称需要与 主应用的 name 一样
libraryTarget: 'umd',
chunkLoadingGlobal: 'webpackJsonp_react-app',// webpack5 更改为 chunkLoadingGlobal
jsonpFunction: `webpackJsonp_react-app`, // webpack4 则配置为 jsonpFunction
},
2. index.tsx 改造
原先的代码:
const root = document.getElementById('root')
if (root) {
createRoot(root).render(
<HashRouter>
<ConfigProvider>
<App />
</ConfigProvider>
</HashRouter>,
)
}
改造后的代码:
let root = '' as any
function render() {
root = createRoot(document.getElementById('root') as HTMLElement)
root.render(
<HashRouter>
<ConfigProvider>
<App />
</ConfigProvider>
</HashRouter>,
)
}
// 动态设置 webpack publicPath,防止资源加载出错
if ((window as any).__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
//判断其是否作为qiankun子应用使用
if (!(window as any).__POWERED_BY_QIANKUN__) {
render()
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,
下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('ReactApp bootstraped')
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props: any) {
console.log('ReactApp mount', props)
render()
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
console.log('ReactApp unmount')
// 销毁组件
root.unmount()
root = ''
}
3. 改造微应用的路由
routers文件夹下的路由改造:
// 多增加一个判断,判断是从主应用进入的,还是从微应用进入的,主应用进入,则添加 /react-app,微应用直接进入的则不变;
const BASE_NAME = (window as any).__POWERED_BY_QIANKUN__ ? '/react-app' : ''
export const rootRouter = [
{
path: BASE_NAME + '/login',
element: <Login />,
meta: {
title: '登录页',
},
},
{
path: BASE_NAME + '/',
element: <Navigate to={BASE_NAME + '/home'} />,
},
]
路由拦截器改造:
拦截器路径地址:/src/utils/modules/routers.tsx
// 判断是否为微前端进入,进入添加前缀
const BASE_NAME = (window as any).__POWERED_BY_QIANKUN__ ? '/react-app' : ''
// // 设置白名单
const whitePaths = [BASE_NAME + '/login', BASE_NAME + '/404', BASE_NAME + '/500']
// 路由守卫配置函数
export const AuthRouter = (props: any) => {
const { pathname } = useLocation()
let {
useUserStore: { token, setUserInfo, userInfo },
} = useStore()
// 第一步 判断有无 token
if (token) {
// 第二步 判断是否前往login页面,等于跳转 '/', 不等于则继续判断
console.log(32, pathname === BASE_NAME + '/login')
if (pathname === BASE_NAME + '/login') {
return <Navigate to={BASE_NAME + '/'} replace />
} else {
// 第三步 判断是否拿到用户个人信息及权限,没拿到则进行axios请求数据,进行信息存储及权限路由渲染,否则直接放行
// 该版本为基础版,这些数据展示都为链接,后续会逐步更新
if (Object.keys(userInfo).length < 1) {
// 获取用户个人信息 (此处使用 async await会报错)
getUserAPI()
.then((res) => {
setUserInfo(res.data as any)
})
.catch((err) => {})
// 合并路由
return props.children
} else {
return props.children
}
}
} else {
if (whitePaths.includes(pathname)) {
return props.children
} else {
return <Navigate to={BASE_NAME + '/login'} replace />
}
}
}
其他的路由即所有跳转路由前面都需要添加这个,本案例路由较为简单,我就没有做优化,如果复杂项目的小伙伴们可以进行优化。
3. 改造微应用的layout
因为,微应用也有自己的header
与menu
,所以需要在是微应用进入的时候,将layout
的header
与menu
进行隐藏:
// init mounted
useEffect(() => {
listeningWindow()
setIsQiankun(!(window as any).__POWERED_BY_QIANKUN__)
}, [])
return (
/**
* 此处不要使用 layout 包裹整个 sider、header、content,会导致layout闪烁
* 此处需要将 silder 与 header&&content 分开布置,可以解决闪烁问题
*/
<div className={classes['layout-container']}>
{isQiankun ? (
<Sider theme="light" trigger={null} collapsible collapsed={collapsed}>
<div className={classes.logo}>
<div className={classes['logo-image']}></div>
{!collapsed && <div className={classes['logo-font']}>Leno Admin</div>}
</div>
<MenuCom />
</Sider>
) : (
''
)}
<Layout className={classes['site-layout']}>
{isQiankun ? <HeaderCom collapsed={collapsed} setCollapsed={setCollapsed} /> : ''}
{isQiankun ? <TabsCom /> : ''}
<ContentCom />
</Layout>
</div>
)
全部改造完成后,便可以从主应用打开微应用的页面啦!

三、微前端之间传值
主应用与微应用之间原则上来说是需要尽可能的降低之间的耦合,这是防止因为满足主应用的功能从而导致微应用无法单独运行,那就有些得不偿失了。一般上来说,主应用与微应用常见需要传值的也就是登录信息,以避免用户主要用登录了,还需要在微应用再登录。
主应用传值代码:
文件地址:src/qiankun/app.js
import store from '@/store/index'
// microApps 挂载子应用
const microApps = [
{
name: 'react-app',
entry: '//localhost:9090',
// 如果使用hashRouter 则在微应用路由路径前加 #
activeRule: '#/react-app',
// 使用historyRouter,无需添加 #
// activeRule: '/react-app',
// 主应用给微应用传参,将主应用中个人信息传值给子应用
props: {
vueUser: store.getters
}
},
]
const apps = microApps.map((item) => {
return {
...item,
// 子应用挂载的div
container: '#qiankun-viewport',
}
})
export default apps
微应用传值代码:
- 在 mount 钩子中获取主应用传的值
文件地址:src/index.tsx
import useStore from '@/store/index'
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props: any) {
console.log('ReactApp mount', props)
// 新增: 将父应用的 user 信息存入到 微应用中
const {
useUserStore: { setVueUserInfo },
} = useStore()
setVueUserInfo(props.vueUser)
render()
}
- store 中存储主应用传值
文件地址:src/store/modules/user.tsx
注意:本文的 微应用React 使用的状态管理工具为 mobx
// 存放 主应用 个人信息
vueUserInfo = {}
// 存储 主应用 userinfo
setVueUserInfo = (userInfo: IuserInfo) => {
console.log(30, userInfo)
this.vueUserInfo = userInfo
}
// 删除 主应用 userInfo
removeVueUserInfo = () => {
this.vueUserInfo = {}
}
- home 主页中使用主应用的传值
文件地址:src/view/home/index.tsx
import React from 'react'
import useStore from '@/store/index'
const Home = () => {
// 将主应用的 userInfo 显示到微应用 home 主页
interface IVueuserInfo {
name?: string
avatar: string
device: string
token: string
sider: {
opened: boolean
withoutAnimation: boolean
}
}
const { useUserStore } = useStore()
const vueUser = useUserStore.vueUserInfo as IVueuserInfo
return (
<div>
Hello Home
<hr />
<div>{vueUser.name}</div>
<div>{vueUser.avatar}</div>
<div>{vueUser.token}</div>
</div>
)
}
export default Home
- 显示效果

四、踩过的坑
在使用 qiankun 的时候也踩过不少的坑,其中一个卡了我差不多一天,就是主应用和微应用都是 Vue
技术栈的时候,会报路由错误,最终排查了一天,期间也查阅了不少资料,发现其实是Vue
模块全局污染了;
以下是官网对这个报错的解释:
qiankun 中的代码使用 Proxy
去代理父页面的 window
,来实现的沙箱,在微应用中访问 window.Vue
时,会先在自己的window
里查找有没有 Vue
属性,如果没有就去父应用里查找。
在 VueRouter
的代码里有这样三行代码,会在模块加载的时候就访问 window.Vue
这个变量,微应用中报这个错,一般是由于父应用中的 Vue
挂载到了父应用的 window
对象上了。

其实官网也早已经有了解决方法,就是主要用中不使用CDN
加载包,第二个就是将主应用中window.Vue
改变个名称;
// 在注册微应用的 beforeload 钩子中,将window.Vue改一个名称即可;
registerMicroApps(apps, {
beforeLoad: (app) => {
// 解决找不到 $router的问题
window.Vue2 = window.Vue
delete window.Vue
// 加载微应用前,加载进度条
NProgress.start()
console.log('before load', app)
return Promise.resolve()
},
afterMount: (app) => {
// 加载微应用前,进度条加载完成
NProgress.done()
console.log('after mount', app.name)
return Promise.resolve()
}
})
五、配置统一命令行,同时启动主应用和微应用
- 下载包:npm-run-all
npm i npm-run-all
- 配置
scripts
启动
"scripts": {
"all:install": "npm-run-all --serial install:*",
"all:dev": "npm-run-all --parallel dev:*",
"install:react-demo": "cd react-demo && npm i",
"dev:react-demo": "cd react-demo && npm run dev:dev",
"install:vue-demo": "cd vue-demo && npm i",
"dev:vue-demo": "cd vue-demo && npm run dev"
},
就是 cd 切换到各应用的文件夹下,输入各应用的项目启动命令行;
好了,做完此步骤就可以轻松的管理你的微前端项目啦!也不用在一个个应用的启动项目,直接一键启动即可;
六、结语
我一直秉承着将一门技术用最简单、最小白的方式写出来,不喜欢故弄玄虚,写一些高大上的词;因为自己当初也是从一名新入门的小白慢慢走过来的,看到一些写的很晦涩的文档,就头疼;我努力让我的每一篇技术文章都以最直白的写法写出来,希望给学习进步的小伙伴们一些帮助!
这只是个默默研究学习技术的前端开发工程师,没有有趣的故事,没有华丽的文笔,没有很高深的技术,谢谢每一个认可我的小伙伴 ~ 💌
七、文中案例代码地址
转载自:https://juejin.cn/post/7177255753701392442