# qiankun
**微前端架构图**
## qiankun 实现原理
`qiankun`是基于`single-spa`开发,它主要采用`HTML Entry`模式,直接将子应用打出来 HTML作为入口,通过 fetch html 的方式,解析子应用的html文件,然后获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。
应用切出/卸载后,同时卸载掉其样式表即可,浏览器会对所有的样式表的插入、移除做整个 CSSOM 的重构,从而达到 插入、卸载 样式的目的。这样即能保证,在一个时间点里,只有一个应用的样式表是生效的。
`HTML Entry` 方案则天生具备样式隔离的特性,因为应用卸载后会直接移除去 HTML 结构,从而自动移除了其样式表。
子应用挂载时,会自动做一些特殊处理,可以确保子应用所有的资源dom(包括js添加的style标签等)都集中在子应用根节点dom下。子应用卸载时,对应的整个dom都移除了,这样也就避免了样式冲突。
提供了js沙箱,子应用挂载时,会对全局window对象代理、对全局事件监听进行劫持等,确保微应用之间 全局变量/事件 不冲突。
HTML Entry` 是由 `import-html-entry` 库实现的,通过 `http` 请求加载指定地址的首屏内容即 `html` 页面,然后解析这个 `html` 模版得到 `template`, `scripts` , `entry`, `styles,返回一个promise对象### 改造流程
具体而言,import-html-entry 实现了以下功能:
加载 HTML 入口文件:import-html-entry 会通过创建一个 <link> 标签来加载子应用的 HTML 入口文件。这样可以确保子应用的资源得到正确加载,并在加载完成后进行处理。
解析 HTML 入口文件:一旦 HTML 入口文件加载完成,import-html-entry 将解析该文件的内容,提取出子应用的 JavaScript 和 CSS 资源的 URL。
动态加载 JavaScript 和 CSS 资源:import-html-entry 使用动态创建 <script> 和 <link> 标签的方式,按照正确的顺序加载子应用的 JavaScript 和 CSS 资源。
创建沙箱环境:在加载子应用的 JavaScript 资源时,import-html-entry 会创建一个沙箱环境(sandbox),用于隔离子应用的全局变量和运行环境,防止子应用之间的冲突和污染。
返回子应用的入口模块:最后,import-html-entry 返回一个可以加载子应用的 JavaScript 模块。这个模块通常是一个包含子应用初始化代码的函数,可以在主应用中调用以加载和启动子应用。
通过使用 qiankun import-html-entry,开发者可以方便地将子应用的 HTML 入口文件作为模块加载,并获得一个可以加载和启动子应用的函数,简化了子应用的加载和集成过程。
## 改造流程
(主应用为vue2.X(history路由模式),子应用为react18(hash路由模式))
#### 1、主应用
安装qiankun : npm i -S qainkun
将app.js和index.js文件放到qiankun文件夹(管理qiankun相关配置)里复制到项目中
```js
// app.js
import store from '@/store'
const apps = [
{
name: 'micro-app-react', // 导入的子应用名称,需要和子应用的package.json中的name保持一直
entry: '//localhost:8012',// 子应用的访问地址
container: '#micro-app-react',// 子应用在主应用的挂在地址(id)
activeRule: '/micro-app-react/react',
props: { // 需要传递给子应用的数据
store,
msg: '我是主应用'
}
}
]
export default apps
```
```js
import { registerMicroApps, start, addGlobalUncaughtErrorHandler, initGlobalState } from 'qiankun/es'
import apps from './app'
// 微应用生命周期
const microAppLifCycles = {
beforeLoad: [ // 全局的微应用生命周期钩子,子应用加载前
app => {
console.log('子应用加载-beforeLoad', app)
return Promise.resolve()
}
],
beforeMount: [ // 全局的微应用生命周期钩子,子应用挂载前
app => {
console.log('2-beforeMount', app)
return Promise.resolve()
}
],
afterMount: [ // 全局的微应用生命周期钩子,子应用挂载后
app => {
console.log('3-afterMount', app)
return Promise.resolve()
}
],
beforeUnmount: [
app => {
console.log('4-beforeUnmount', app)
}
]
}
console.log(registerMicroApps, 'registerMicroApps')
registerMicroApps(apps, microAppLifCycles)
// 添加全局的未捕获异常处理器
addGlobalUncaughtErrorHandler((event) => {
const { message: msg } = event
if (msg && msg.includes('died in status LOADING_SOURCE_CODE')) {
console.error('微应用加载失败,请检查应用是否可运行', event)
}
})
// 定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法
const { onGlobalStateChange, setGlobalState } = initGlobalState({
user: 'qiankun'
})
// 在当前应用监听全局状态,有变更触发 callback
onGlobalStateChange((value, prev) => console.log('[onGlobalStateChange - main]:', value + prev))
// 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
setGlobalState({
ignore: 'master',
user: {
name: 'master'
}
})
export default start
```
配置vue.config.js
```js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
client: {
overlay: false // 禁止:当出现编译错误或警告时,在浏览器中显示全屏覆盖
}
}
})
```
创建子应用占位组件
```vue
// page/appReact/index.vue
<template>
<div id="micro-app-react"></div>
</template>
<script>
import { start } from 'qiankun/es'
export default {
name: 'microAppReact',
components: {},
mounted () {
console.log('window.qiankunStarted', window.qiankunStarted)
if (!window.qiankunStarted) {
window.qiankunStarted = true
start({
prefetch: true, // 是否开启预加载
sandbox: {
experimentalStyleIsolation: true // 实验性的样式隔离
}
})
}
}
}
</script>
```
router.js 中配置字应用对应的路由
```js
{
name: 'microAppReact',
path: '/micro-app-react/*', // 匹配micro-app-react后的所有路由
title: 'micro-app-react',
icon: 'icon-instrum',
component: () => import('@/page/appReact/index.vue')
},
```
在main.js中启动qiankun
```
import start from '@/qiankun/index'
start({
prefetch: true, // 是否开启预加载
sandbox: {
experimentalStyleIsolation: true // 实验性的样式隔离
}
})
```
上述配置处理完毕就可以在通过改变路由进而访问子应用的页面了
```vue
// this.$router.push({ path: '/micro-app-react/react#/' })
window.history.pushState(null, '', '/micro-app-react/react#/')
// this.$router.push({ path: '/micro-app-react/react#/demo' })
window.history.pushState(null, '', '/micro-app-react/react#/demo')
window.history.pushState(null, '', '/micro-app-react/react#/demo1')
// this.$router.push({ path: '/micro-app-react/react#/demo1' })
break
```
eg:由于子应用的接口访问是通过主应用发起的,所以子应用的代理配置需要在住应用中同步至proxy转发代理中。
#### 2、子应用
**子应用无需安装qiankun**
1、修改package.json 的name为micro-app-react,和主应用需要的name保持一致。
2、处理webpack的打包输出,并且设置 "Access-Control-Allow-Origin": "*"来处理跨域问题(目前还未解决qiankun和webpack的模块联邦共存问题,只能二选一)
```js
const packageName = require('./package.json').name;
webpack: {
config.output = {
...config.output,
library : packageName,
libraryTarget : "umd",
globalObject : "window",
};
return config;
},
},
devServer: {
headers : {
"Access-Control-Allow-Origin": "*",
},
}
```
3、**由于qiankun加载主应用时需要在入口文件的index.js就处理single-spa,所以现有项目的入口文件先加载bootstrap需改造成直接处理**
```tsx
/* eslint-disable @typescript-eslint/no-empty-function */
import './public-path.js'; // 首先加载
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.less';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { HashRouter } from 'react-router-dom';
import { NPLProvider } from '@LU30/NPLWeblib';
import { getBrowserVersion, loadStyles } from './utils/VersionCompatible';
import AuthorityPrompt from './pages/AuthorityPrompt/index';
import ErrorBoundary from './utils/CmbTrack';
// 如果是360或者版本低于90都做样式兼容i
if (getBrowserVersion() < 90) {
// 加载ant4最新版样式,兼容低版本浏览器
// loadStyles('/antd.css');
// 覆盖局部未兼容样式
loadStyles('/compayible.css');
}
console.log('window.__POWERED_BY_QIANKUN__', window.__POWERED_BY_QIANKUN__);
let myRoot = null;
function render(props) {
const { container } = props;
const dom = container ? container.querySelector('#root') : document.querySelector('#root');
myRoot = ReactDOM.createRoot(dom as HTMLElement);
myRoot.render(
<NPLProvider>
{getBrowserVersion() < 86 && getBrowserVersion() !== 0 && <AuthorityPrompt />}
{(getBrowserVersion() >= 86 || getBrowserVersion() === 0) && (
<HashRouter>
<ErrorBoundary>
<App />
</ErrorBoundary>
</HashRouter>
)}
</NPLProvider>,
);
}
// qiankun生命周期钩子 ,必须抛出
export async function bootstrap() {
console.log('bootstrap');
}
export async function mount(props: any) {
console.log('激活');
// 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
console.log('mount', props); // props是主应用传递过来的值
render(props);
}
export async function unmount(props: any) {
console.log('unmount', props);
// const { container } = props;
// const dom = container ? container.querySelector('#root') : document.querySelector('#root');
// const root = ReactDOM.createRoot(dom as HTMLElement);
// 应用每次 切出/卸载 会调用的unmount方法,通常在这里我们会卸载微应用的应用实例
myRoot.unmount();
}
// 如果不是qiankun就执行,以支持独立开发
if (!window.__POWERED_BY_QIANKUN__) {
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<NPLProvider>
{getBrowserVersion() < 86 && getBrowserVersion() !== 0 && <AuthorityPrompt />}
{(getBrowserVersion() >= 86 || getBrowserVersion() === 0) && (
<HashRouter>
<ErrorBoundary>
<App />
</ErrorBoundary>
</HashRouter>
)}
</NPLProvider>,
);
}
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
// const bootstrap = import('./bootstrap');
// export default bootstrap;
```
public-path.js
```js
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
```