目录
原因
Why Not iframe?
qiankun
使用qiankun部署项目
在子路由的页面去暴露根元素
Create-react-app 子应用
在vite中使用qiankun
方法一(不太推荐):
方法二(推荐):
安装插件
修改vite.config.js配置:
修改main.ts暴露钩子函数:
结尾
初始化文件
下载依赖包
修改package.json
最后
应用间传值:
github地址:
https://github.com/qq2084470563/qiankun-samples
原因
最近公司由于一个奇怪的需求,需要将一个项目嵌入到另一个项目中,其中一个项目是react的,另一个则是vue的,最开始我的提议是将项目重构到系统,但是这样会产生非常严重的问题,一方面是本来运行就慢的vue项目(每次跑起来都要30s,非常影响开发),更加雪上加霜了,另外实际给下来的时间并不多,所以,在没有时间,尽量不重新塞进东西,初期定稿使用最基本的技术,通过ifram嵌入,但是在前几日,看到群友讨论到了微前端的哪个框架好,一下就点醒我了,话不多说就直接上车了。
Why Not iframe?
如果说在不考虑性能体验的情况下来说,用iframe是最佳,也是最简单的方式了。
对于iframe来说,最大也是最硬的特性就是硬隔离,所有的样式,js都是完全隔离,这种沙箱隔离是很难去突破的,从而导致应用上下文不共享,带来的开发体验和使用体验的不好。
通常情况下,我们iframe的页面我们一般会通过message事件去进行通信,这也是iframe的最优实现通信。
- 其中最为关键的问题的就是慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- url不共享,用iframe外面一个url,iframe一个url,且不能使用外部的浏览器回退,前进键。
- UI不会共享,页面的dom结构不共享,样式什么的做起来不方便,假如现在要设计屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中,内部遮罩是不可能出来的。
qiankun
为什么选择这个框架,这个框架是蚂蚁基于 single-spa 进行二次封装,乾坤是基于single-spa的二次开发,它在 single-spa 基础上添加更多的功能,并且做了不错的封装,大大降低了入门微前端的门槛。
使用qiankun部署项目
创建一个文件夹,在里面创建一个主项目,作为基座,这个项目中不用考虑用什么,只是做为一个基座容器,然后创建嵌入的子项目。

 
打开我们的基座项目main-service 添加项目依赖,载入乾坤的包,
yarn add qiankun
按个人习惯,或者根据自己团队改造一下初始代码,我这里按自己的习惯去修改结构。

接下来进入主体,开始编写我们的主应用main-service 创建一个qiankun 文件,具体Api参考qiankun官网。
- 搭建微应用的列表,进行注册。
- 不要忘记钩子函数注册一下,方便自己查看挂哪里了
- 启动qiankun,通过startapi。
import {
	registerMicroApps,
	runAfterFirstMounted,
	setDefaultMountApp,
	start,
} from 'qiankun'
// 引入vue实例
import { isLoading } from '@/App'
/**
 * 加载动画运行
 * @param loading
 *
 */
function loader(loading: boolean) {
	isLoading.value = loading
	// if(instance && instance.$children)
}
const microApps = [
	{
		name: 'sub-react',
		developer: 'react',
		entry: '//localhost:40001',
		activeRule: '/sub-react',
	},
	{
		name: 'sub-vite-react',
		developer: 'vite-react',
		entry: '//localhost:40002',
		activeRule: '/sub-vite-react',
	},
]
const apps = microApps.map((item) => {
	return {
		...item,
		loader, // 给子应用配置加上loader方法
		container: '#sub-container',
		props: {
			developer: item.developer, // 下发基础路由
			routerBase: item.activeRule, // 下发基础路由
		},
		configuration: {
			strictStyleIsolation: true,
		},
	}
})
registerMicroApps(apps, {
	beforeMount: [
		(app): any => {
			console.log('开始挂载:', app.name)
		},
	],
	afterMount: [
		(app): any => {
			console.log(app.name, '挂载成功了')
		},
	],
	afterUnmount: [
		(app): any => {
			console.log('贾维斯卸载 ', app.name)
		},
	],
})
/**
 * 设置默认进入的子应用
 */
// setDefaultMountApp('/sub-vite2-react')
start()
runAfterFirstMounted(() => {
	console.log('贾维斯开机')
})
export { microApps }
export default apps
这一段代码很简单,只要就是创建一个subapp的列表,并且注册,运行我们的qiankun,之所以如此简便是因为蚂蚁对single-spa高度封装,简便操作,并提供api,提高效率。
接下来主项目引入路由。
- 引入router 。
- 编写路由文件
yarn add vue-router
const menuRoutes: MyRouteType[] = [
	{
		path: '/index',
		key: '/index',
		name: 'Index',
		component: () => Promise.resolve(Main),
	},
]
const routes: MyRouteType[] = [
	{
		path: '/',
		key: '/',
		redirect: '/index',
	},
	{
		path: '/:pathMatch(.*)',
		key: '*',
		redirect: '/404',
	},
]
microApps.forEach((item) => {
	menuRoutes.push({
		path: item.activeRule,
		key: item.name,
		name: item.name,
		component: () => Promise.resolve(Sub),
	})
})
routes.push(...menuRoutes)
const router = createRouter({
	history: createWebHistory(),
	routes,
})路由中主要是引入qiankun的路由进行配置,在关键的页面组件中写挂载的元素
在子路由的页面去暴露根元素
import { defineComponent } from 'vue'
export const Sub = defineComponent({
	setup() {
		return () => {
			return <div id='sub-container'></div>
		}
	},
})
其实到这里,主应用可以说基本的完成了,接下来先部署子应用。
Create-react-app 子应用
假如说我们有一个情况,当前应用是一个以前技术栈的老项目,我们需要在不抛弃老项目,老技术的情况下去上新的模块,那么我们该怎么操作呢?
- 在 src目录新增public-path.js
- 设置 history模式路由的base
- 在子应用 sub-react中暴露钩子函数。
- 安装插件 @rescripts/cli,当然也可以选择其他的插件,例如react-app-rewired。
- 修改webpack文件保证dev还是prod环境都可跑通。
按照官网的走,新增public-path.js文件里加上配置做为子应用是根路径,并且配合webpack使用。
if (window.__POWERED_BY_QIANKUN__) {
	// eslint-disable-next-line no-undef
	__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
接下来在子项目中暴露钩子函数让父应用调用 main.js 。
修改渲染函数执行
function render() {
  ReactDOM.render(
    <App />,
    document.getElementById('root')
  );
}
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}
/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
  console.log('react app bootstraped');
}
/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  console.log(props);
  storeTest(props);
  render();
}
/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount() {
  ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}
/**
 * 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
 */
export async function update(props) {
  console.log('update props', props);
}
暴露了钩子函数后,我们接下来就要配置webpack,因为react中webpack是藏起来的,需要执行命令编辑
yarn eject
但是这个操作是不可逆的,展开的文件不仅复杂,看着那么多文件也难受,所以说我们使用第三方库去配置webpack以及devserver,qiankun的官方使用了 @rescripts/cli 同时也推荐了其他的,翻找一下资料,发现这个库好久没更新了,也没看到文档,所以我选择使用react-app-rewired,从npm官网看起来这个稍微友好一点。
引入第三方库
yarn add react-app-rewired -D
编辑配置文件 config-overrides.js 在根路径,跨域是必须要写的否则父应用无法fetch到微应用。
const { name } = require('./package.json')
console.log(name)
module.exports = {
	webpack: function (config, env) {
		if (Array.isArray(config.entry)) {
			config.entry = config.entry.filter(
				(e) => !e.includes('webpackHotDevClient')
			)
		}
		// config.entry = config.entry.includes('webpackHotDevClient')
		config.output.library = `${name}-[name]`
		config.output.libraryTarget = 'umd'
		config.output.chunkLoadingGlobal = `webpackJsonp_${name}`
		// 写项目启动的源,否则图片无法显示
		config.output.publicPath = 'http://localhost:40001/'
		return config
	},
	devServer: (configFunction) => {
		return function (proxy, allowedHost) {
			const config = configFunction(proxy, allowedHost)
			config.open = false
			config.hot = false
			config.headers = {
				'Access-Control-Allow-Origin': '*',
			}
			// Return your customised Webpack Development Server config.
			return config
		}
	},
}
修改package.json里面的执行脚本。
- "start": "react-scripts start",
+ "start": "react-app-rewired start",
- "build": "react-scripts build",
+ "build": "react-app-rewired build",
- "test": "react-scripts test",
+ "test": "react-app-rewired test",
- "eject": "react-scripts eject"
如此我们的项目就已经可以嵌入了,效果大概是下图,千万别忘记配置环境中的端口,要和主应用的端口匹配。

当然这样写下来其实是存在一个问题,子页面的样式元素可能会影响外部,这是因为qiankun默认子应用的之间隔离,但是没有隔离主子应用,
只需要开启严格模式 ,即可确保不影响全局。
在vite中使用qiankun
在这里我为什么要当拿出来vite的项目去说呢,从现在环境来看vite大火,vite拥有在开发环境下极快的编译速度,热更新监听,但是碍于vite发展时间不长,所以生态方面来说不够成熟,有些功能还是比较尴尬的。
为何vite中不能到生命周期钩子函数呢?具体原因是什么呢?
- vite构建的- js内容必须在- type=module的- script脚本里;
- qiankun的源码依赖之一- import-html-entry则不支持- type=module这个属性
- qiankun是通过- eval来执行这些- js的内容,而- vite里面- import/export没有被转码, 所以直接接入会报错:不允许在非- type=module的- script里面使用- import
大概就是这个原因,自己写的时候都是这样报错,那我们该如何巧妙的将两种融合起来呢。翻阅各种资料,并且查看qiankun的isssues找到了两个解决方法.
方法一(不太推荐):
因为vite使用的rollup,所以我们使用可以使用rollup插件 rollup/plugin-html,然后修改vite.config.js中build的配置,把vite默认输出的target模式修改一下,将module改为esnext。
因为配置的是build配置所以有缺点:
- 可以实现生产环境接入,开发环境不行;
- vite没有动态- publicPath的支持;所以- Vite.config中- base配置需要写死
- vite- code-splitting(代码分割)功能并不支持- iife和- umd两种格式,导致路由无法懒加载;
- 图片资源只会被打包成 base64,无论图片大小
具体可以详细步骤可以查看issues的提交。
- https://github.com/umijs/qiankun/issues/1268
方法二(推荐):
vite-plugin-qiankun 插件; github文档。
在不改变原有的东西在这个基础上以插件的形式融合进去。优点:
- 保留 vite构建es模块的优势
- 一键配置,不影响已有的 vite配置
- 支持 vite开发环境
安装插件
除了qiankun插件还需要一个一个react热更新的插件,这是因为开发环境作为子应用时与热更新插件(可能与其他修改html的插件也会存在冲突)有冲突,所以需要额外的调试配置,于是换个方式实现热更新。
yarn add vite-plugin-qiankun
yarn add @vitejs/plugin-react-refresh
修改vite.config.js配置:
import { defineConfig } from 'vite'
import qiankun from 'vite-plugin-qiankun'
import reactRefresh from '@vitejs/plugin-react-refresh'
// useDevMode 开启时与热更新插件冲突
const useDevMode = true
// https://vitejs.dev/config/
export default ({ mode }) => {
	const __DEV__ = mode === 'development'
	return defineConfig({
		plugins: [
			...(useDevMode ? [] : [reactRefresh()]),
			qiankun('sub-vite-react', {
				useDevMode: true,
			}),
		],
		server: {
			port: 40002,
			host: '0.0.0.0',
			// 设置源是因为图片资源会找错位置所以通过这个让图片等资源不会找错
			origin: '//localhost:40002',
			cors: true,
			headers: {
				'Access-Control-Allow-Origin': '*',
			},
		},
		base: __DEV__ ? '/' : '//localhost:40002',
	})
}
修改main.ts暴露钩子函数:
js只要把类型标注删一下就可以了
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
// vite-plugin-qiankun helper
import {
	renderWithQiankun,
	qiankunWindow,
	QiankunProps,
} from 'vite-plugin-qiankun/dist/helper'
let root: ReactDOM.Root | null = null
function render(props: QiankunProps) {
	const { container } = props
	const root = ReactDOM.createRoot(
		(container
			? container.querySelector('#root')
			: document.getElementById('root')) as HTMLElement
	)
	root.render(
		<React.StrictMode>
			<App />
		</React.StrictMode>
	)
	return root
}
renderWithQiankun({
	mount(props) {
		console.log('vite-react 上线了')
		root = render(props)
	},
	bootstrap() {
		console.log('bootstrap')
	},
	unmount(props) {
		console.log('vite-react 下线了')
		const { container } = props
		root?.unmount()
	},
	update(props) {
		console.log('vite-react更新了', props)
	},
})
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
	root = render({})
}
配置好了启动所有项目就可以看到所有项目了。
结尾
一直一直写发现我们的子类越来越多了,每次启动我我们都要到各个路径启动项目然后才能看到全貌所有我们需要定义一个指令能够在最外层把所有指令逐个配置启动,和全部一起启动。
初始化文件
yarn init
下载依赖包
yarn add yarn-run-all
修改package.json
"scripts": {
+ "install": " npm-run-all -s install:*",
+ "install:main":"cd main_service && yarn ",
+ "install:sub-react":"cd sub-service/sub-react && yarn",
+ "install:sub-vite-react":"cd sub-service/sub-vite-react && yarn",
+ "start": "npm-run-all -p start:*",
+ "start:main": "cd main_service && yarn dev",
+ "start:sub-react": "cd sub-service/sub-react && yarn start",
+ "start:sub-vite-react": "cd sub-service/sub-vite-react && yarn dev"
},
配置好后我们使用指令安装依赖,和启动就好了,便利团队开发。
最后
qiankun项目的基本部署就这样,但是其实不难发现这里一直将微应用部署到主应用中,但是还没说过具体的应用键传值,但是其实如此部署之后发现其实项目是在同一个地址里,也就是说有共通的localStorage,sessionStorage,以及cookie。那么在复杂场景中肯定需要通信,但是由于篇幅太长了,所以这里重点是使用qiankun的部署。
应用间传值:
如何在qiankun框架中父子传值
github地址:
https://github.com/qq2084470563/qiankun-samples



















