发布时间:2023-04-09 13:00
目录
https://github.com/qq2084470563/qiankun-samples
最近公司由于一个奇怪的需求,需要将一个项目嵌入到另一个项目中,其中一个项目是react的,另一个则是vue的,最开始我的提议是将项目重构到系统,但是这样会产生非常严重的问题,一方面是本来运行就慢的vue项目(每次跑起来都要30s,非常影响开发),更加雪上加霜了,另外实际给下来的时间并不多,所以,在没有时间,尽量不重新塞进东西,初期定稿使用最基本的技术,通过ifram嵌入,但是在前几日,看到群友讨论到了微前端的哪个框架好,一下就点醒我了,话不多说就直接上车了。
如果说在不考虑性能体验的情况下来说,用iframe是最佳,也是最简单的方式了。
对于iframe来说,最大也是最硬的特性就是硬隔离,所有的样式,js都是完全隔离,这种沙箱隔离是很难去突破的,从而导致应用上下文不共享,带来的开发体验和使用体验的不好。
通常情况下,我们iframe的页面我们一般会通过message事件去进行通信,这也是iframe的最优实现通信。
为什么选择这个框架,这个框架是蚂蚁基于 single-spa 进行二次封装,乾坤是基于single-spa的二次开发,它在 single-spa 基础上添加更多的功能,并且做了不错的封装,大大降低了入门微前端的门槛。
创建一个文件夹,在里面创建一个主项目,作为基座,这个项目中不用考虑用什么,只是做为一个基座容器,然后创建嵌入的子项目。
打开我们的基座项目main-service 添加项目依赖,载入乾坤的包,
yarn add qiankun
按个人习惯,或者根据自己团队改造一下初始代码,我这里按自己的习惯去修改结构。
接下来进入主体,开始编写我们的主应用main-service 创建一个qiankun 文件,具体Api参考qiankun官网。
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,提高效率。
接下来主项目引入路由。
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>
}
},
})
其实到这里,主应用可以说基本的完成了,接下来先部署子应用。
假如说我们有一个情况,当前应用是一个以前技术栈的老项目,我们需要在不抛弃老项目,老技术的情况下去上新的模块,那么我们该怎么操作呢?
src
目录新增 public-path.js
history
模式路由的 base
@rescripts/cli
,当然也可以选择其他的插件,例如 react-app-rewired
。按照官网的走,新增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的项目去说呢,从现在环境来看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
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',
})
}
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
"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的部署。
https://github.com/qq2084470563/qiankun-samples