qiankun 的使用
qiankun 的使用
1.前言
许多项目搭建的方法不一致,故qiankun改造的方式也不一致,但是从目前的情况看,基本的步骤和些思路还是大抵相同,在改造中遇到的最多问题就是 路径不一致、配置的代理不正确、跨域等,后面有问题也优先往这方面靠近。
参考文档:
乾坤官网:https://qiankun.umijs.org/zh
vite vue3配置:https://www.cnblogs.com/Mr-Rshare/p/16571760.html
angular 配置:https://www.cnblogs.com/wangyongping/p/16788537.html
2.主应用
vue2 (公司平台)
这里的配置完全可以参考乾坤官网上来配置,这里我用公司平台举例:
如果子应用存在angular那么首先要安装 zone.js,不用的话就不用安装。
npm i zong.js
npm i qiankun
修改路由 ,在路由表的根部添加要配置的子应用路径:如flink,后面的*号一定要有。
# src/common/router/index.js
export const constantRouterMap = [
{ path: '/license', component: LicenseComponent, hidden: true },
+{
+ path: '/flink/*',
+ component: componentsObj.Layout
+ },
{ path: '*', component: NotFoundComponent, hidden: true },
// ...
];
export default new Router({
mode: 'history',
routes: constantRouterMap
});
在要渲染子应用的地方添加id容器
# src/common/views/layout/components/AppMain.vue
<template>
<section class="app-main" v-bind:style="{ height: screenHeight }">
<div class="app-main-container">
// ...
<router-view />
+ <div id="appContainer"></div>
// ...
</div>
</section>
</template>
设置qiankun的启动
# src/common/views/layout/Layout.vue
+ import { start } from 'qiankun';
export default {
mounted() {
+ start();
},
methods: {
}
};
</script>
设置qiankun的基础配置,如果有anglue项目需要引入zone.js
# src/common/config/sys.js
import 'zone.js'; // 如果有anglue项目,一定要在qiankun的前面引入
import {
registerMicroApps,
} from 'qiankun';
...
registerMicroApps(
[
{
name: 'flink_web',
entry: '//localhost:4200',
container: '#appContainer',
activeRule: '/flink'
},
// {
// name: 'datainsight',
// entry: '//localhost:5002',
// container: '#appContainer',
// activeRule: '/datainsight'
// }
],
{
beforeLoad: [
app => {
// console.log('before load', app);
}
],
beforeMount: [
app => {
// console.log('before mount', app);
}
],
afterUnmount: [
app => {
// console.log('after unload', app);
}
]
}
// {
// fetch: request
// }
);
...
配置代理 ,这里路径一定要正确,后面报错的话不好定位到这里
# setting.js
'/api/flinkdashbaord/*': {
target: 'http://flink.com/api/flinkdashbaord', // 本地测试环境
logLevel: 'debug',
changeOrigin: true,
autoRewrite: true,
cookieDomainRewrite: true,
pathRewrite: {
'^/api/flinkdashbaord': ''
}
},
3.微应用
vue3 + vite
因为官网里面没有提供方法,且不支持vite, 所以需要在引入一个包,在子应用
npm i vite-plugin-qiankun
在改造后发现路由不匹配的问题,后经过测试发现是history没有在离开时销毁造成,所以要 history.destroy();
修改路由,这里简写,并把路由返回的方式
# src/router/index.ts
import { createRouter } from 'vue-router'
import routes from './routes'
...
let routerIn:any=null
function setRouter(history: any) {
routerIn = createRouter({
history,
routes
})
...
return routerIn
}
export {setRouter, routerIn as router}
修改主配置,引入插件 vite-plugin-qiankun
# src/main.ts
import {createApp} from 'vue'
import App from './App'
import {QiankunProps, qiankunWindow, renderWithQiankun} from 'vite-plugin-qiankun/dist/helper'
import {setRouter} from "@/router";
import {createWebHistory} from "vue-router";
let app:any = null;
let routerIn: any = null
let historyIn: any = null
function render(props:any) {
historyIn = createWebHistory(
qiankunWindow.__POWERED_BY_QIANKUN__? '/dolphinscheduler/':(import.meta.env.MODE === 'production' ? '/dolphinscheduler/' : '/')
)
routerIn = setRouter(historyIn)
const { container } = props;
app = createApp(App);
app.use(routerIn);
app.mount(container ? container.querySelector('#dolphinschedulerApp'): '#dolphinschedulerApp');
}
renderWithQiankun({
mount(_props) {
render(_props);
},
bootstrap() {
console.log('bootstrap');
},
unmount(_props: any) {
app.unmount();
app = null;
routerIn= null;
historyIn.destroy();
},
update: function (props: QiankunProps): void | Promise<void> {
console.log('update');
}
});
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
render({container:''});
}
修改 vite配置 ,因为vite 不支持qiankun 使用插件后,打包时候要提前配置好 base
# vite.config.ts
import {defineConfig, } from 'vite'
+ import qiankun from 'vite-plugin-qiankun'
+ const { name } = require('./package.json');
export default defineConfig({
base: 'http://dolphinscheduler.com/',
// base: process.env.NODE_ENV === 'production' ? '/dolphinscheduler/' : '/',
plugins: [
+ qiankun(name, {
+ useDevMode: true,
+ }),
...
],
...
server: {
+ headers: {
+ 'Access-Control-Allow-Origin': '*',
+ },
proxy: {
'^/api/inner/dolphinscheduler': {
target: loadEnv('development', './').VITE_APP_DEV_WEB_URL,
changeOrigin: true
}
}
}
})
angular12 + single-spa
angular整体结构有所不同,这里的项目有用到 single-spa 才可以,执行命令
npm i single-spa -S
ng add single-spa-angular
执行完后,项目的一部分路径文件也会有所变化,创建 public-path.js 文件,在 main.single-spa.ts 内最上面引入
# public-path.js
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
修改入口文件
# main.single-spa.ts
import './public-path';
import { enableProdMode, NgZone } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { Router } from '@angular/router';
import { singleSpaAngular, getSingleSpaExtraProviders } from 'single-spa-angular';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { singleSpaPropsSubject } from './single-spa/single-spa-props';
if (environment.production) {
enableProdMode();
}
// enableProdMode();
// 微应用单独启动时运行
if (!(window as any).__POWERED_BY_QIANKUN__) {
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
}
const lifecycles = singleSpaAngular({
bootstrapFunction: singleSpaProps => {
singleSpaPropsSubject.next(singleSpaProps);
return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule);
},
template: '<flink-root id="flinkid" />',
Router,
NgZone: NgZone
});
export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;
控制zone.js 的引入,确保本地和qiankun都能引入
# index.html
head标签添加
<script src="https://unpkg.com/zone.js" ignore></script>
# polyfills.ts
// import 'zone.js 把这里引入注释掉
修改路由的配置
# app-routing.module.ts
+ import { APP_BASE_HREF } from '@angular/common';
@NgModule({
exports: [RouterModule],
imports: [RouterModule.forRoot(routes)],
+ providers: [{ provide: APP_BASE_HREF, useValue: (window as any).__POWERED_BY_QIANKUN__ ? '/flink/' : '/' }]
})
修改angular配置文件
# extra-webpack.config.js
const singleSpaAngularWebpack = require('single-spa-angular/lib/webpack').default;
const {merge } = require("webpack-merge");
const appName = require('./package.json').name;
module.exports = (config, options) => {
const singleSpaWebpackConfig = singleSpaAngularWebpack(config, options);
const singleSpaConfig = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
output: {
library: `${appName}-[name]`,
libraryTarget: "umd",
},
externals: {
"zone.js": "Zone",
},
};
const mergedConfig = merge (
singleSpaWebpackConfig,
singleSpaConfig
);
return mergedConfig;
};
如果还有其它报错,基本上是路径匹配,端口NaN等造成
react16.8
一部分的引入的方式可参考官网,可能项目不同,webpack的引入方式不正确,需要单独处理。
修改打包配置文件
# internals/webpack/webpack.prod.babel.js
module.exports = require('./webpack.base.babel')({
mode: 'production',
output: {
// filename: '[name].[chunkhash].js',
// chunkFilename: '[name].[chunkhash].chunk.js'
+ library: `datainsight`,
+ filename: `[name].${timeStamp}.js`,
+ libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`
},
...
})
修改引入
# app/app.tsx
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
import React from 'react'
import ReactDOM from 'react-dom'
import { translationMessages } from './i18n'
...
const MOUNT_NODE = document.getElementById('datainsightApp')
const render = (messages, props) => {
const { container } = props
ReactDOM.render(
<Provider store={store}>
<LanguageProvider messages={messages}>
<ConfigProvider locale={zh_CN}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</ConfigProvider>
</LanguageProvider>
</Provider>,
container ? container.querySelector('#datainsightApp') : MOUNT_NODE
)
}
if (!window.__POWERED_BY_QIANKUN__) {
if (module.hot) {
module.hot.accept(['./i18n', 'containers/App'], () => {
ReactDOM.unmountComponentAtNode(MOUNT_NODE)
render(translationMessages, {})
})
} else {
render(translationMessages, {})
}
}
export async function bootstrap() {
console.log('react app bootstraped')
}
export async function mount(props) {
await render(translationMessages, props)
}
export async function unmount(props) {
const { container } = props;
await ReactDOM.unmountComponentAtNode(container ? container.querySelector('#datainsightApp') : MOUNT_NODE)
}
if (process.env.NODE_ENV === 'production') {
// disable react developer tools in production
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = () => void 0
}
}
修改路由
# app/containers/App/index.tsx
...
public render() {
const __qiankun__ = window.__POWERED_BY_QIANKUN__
return (
<div>
{/*basename={'/datainsight'} basename={__qiankun__ ? '/datainsight' : '/'}*/}
<Router basename={__qiankun__ ? '/datainsight/' : '/'}>
<Switch>
<Route path="/activate" component={Activate} />
<Route path="/joinOrganization" exact component={Background} />
<Route path="/findPassword" component={FindPassword} />
<Route path="/" exact render={this.renderRoute} />
<Route path="/" component={logged ? Main : Background} />
</Switch>
</Router>
</div>
)
}
...
vue2
这里不做详细介绍,参考官网
https://qiankun.umijs.org/zh/guide/tutorial#vue-%E5%BE%AE%E5%BA%94%E7%94%A8
4.nginx配置
qiankun 官网:
通用方式
线上采用同一个服务器,不同的端口实现
server {
listen 86;
server_name localhost;
location / {
root /aTinyApp/appbase;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /applims {
proxy_pass http://localhost:88/;
proxy_set_header Host $host:$server_port;
}
location /appcost {
proxy_pass http://localhost:89/;
proxy_set_header Host $host:$server_port;
}
location /api/ {
proxy_pass http://gateway/;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300;
client_max_body_size 1024m;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
server {
listen 88;
server_name localhost;
location / {
root /aTinyApp/applims;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://gateway/;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300;
client_max_body_size 1024m;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
server {
listen 89;
server_name localhost;
location / {
root /aTinyApp/appcost;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://gateway/;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300;
client_max_body_size 1024m;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
本地需要考虑到跨域等因素,做相应处理
server {
listen 80;
server_name flink.com;
#access_log logs/host.access.log main;
location / {
root D:/baseServer/app/flink;
index index.html index.htm;
try_files $uri $uri/ /index.html;
# 允许 所有头部 所有方法 域
add_header 'Access-Control-Allow-Headers' '*';
add_header 'Access-Control-Allow-Methods' '*';
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Credentials' 'true';
}
location /api/ {
proxy_pass http://172.18.50.187;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
同一域名下部署
多数也是参照乾坤的例子
主应用配置
egisterMicroApps(
[
{
name: 'flink-dashboard',
entry: '/child/flinkdashboard/', // 重点
container: '#appContainer',
activeRule: '/flinkdashboard' // 重点
},
{
name: 'dolphinscheduler',
entry: '/child/dolphinscheduler/', // 重点
container: '#appContainer',
activeRule: '/dolphinscheduler' // 重点
},
],
子应用history配置
historyIn = createWebHistory(
qiankunWindow.__POWERED_BY_QIANKUN__? '/dolphinscheduler/':'/child/dolphinscheduler/'
)
子应用跟路径 配置
base:'/child/dolphinscheduler/'
服务器目录
└── app/ # 根文件夹
|
├── child/ # 存放所有微应用的文件夹
| ├── flinkdashboard/ // flinkdashboard
| ├── dolphinscheduler/ // dolphinscheduler
├── index.html # 主应用的index.html
├── css/ # 主应用的css文件夹
├── js/ # 主应用的js文件夹
nginx配置
server {
listen 80;
server_name app.com;
#access_log logs/host.access.log main;
location / {
root D:/baseServer/app;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /child/flinkdashboard {
root D:/baseServer/app;
index index.html index.htm;
try_files $uri $uri/ /child/flinkdashboard/index.html;
}
location /child/dolphinscheduler {
root D:/baseServer/app;
index index.html index.htm;
try_files $uri $uri/ /child/dolphinscheduler/index.html;
}
location /api/ {
proxy_pass http://172.18.50.187;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
配置成功后,主应用访问地址不变,单独访问子应用只需要加上 http://xxx.com/child/zi/ 即可。
5.常见问题
多数问题qiankun已经整合,可以从中作为参考:
https://qiankun.umijs.org/zh/faq
a. 如果安装了zone.js 在项目运行中,可能有不相关问题也会发出报错提示,不是angular项目时候可以先关掉引用,方便快速定位问题
b. 项目部署后,vite项目与其他项目切换中,发现路径回退,可能是vite项目的history没有销毁,在乾坤生命周期中销毁。
c. angular项目在执行完 ng add single-spa-angular 后,可能会遇到,端口变成NaN问题,路径不匹配的问题
// 报路径错的时候修改这里
# tsconfig.app.json
"files":[
"main.single-spa.ts"
]
// 端口要一致
# angular.json
"projects"->"flink"->"architect"->"build"->"options"->"deployUrl":'/'
"projects"->"flink"->"architect"->"serve"->"options"->"deployUrl":'http://localhost:4200/'
d. 任何项目部署后运行,刷新后子应用变成主应用消失,可以先检查主应用的entry和activeRule是不是配置成一样的了
e. angular项目有一个跟路径:
<base href="/" />
当出现白屏,路径不匹配,配置了好几种方案都不成功的时候,可以尝试将其注释掉。
f. 目前在vite 和vue3的项目上遇到一个问题,配置都正确,但是访问二级微应用的子路由的时候,将整个页面都渲染成白屏,后改路径等匹配规则都不成功,最后使用的hash 路由模式得以解决,如果要继续使用history 模式,可以尝试从子应用上检查,现在已经定位到是子应用路由渲染上有影响,具体原因还在查找。
。。。
后续待补充