应项目要求,需要将一个旧的vue 项目集成到新的react 项目中,但是同时会对vue 项目(只)进行维护。所以采用了现有的这一套技术方案来实现这一功能。
主要是使用qiankun,基座umi.js。子项目可以使用vue 以及react。目前项目已经上线。
主应用搭建(基座)
1.1 使用umi.js 脚手架创建项目
yarn create umi main-app
ant-design-pro
pro v4
ts
simple
antd@4
项目结构
1.2 基座应用umi.js。可以安装qiankun 插件。所以在基座项目中执行命令
yarn add @umijs/plugin-qiankun -D
1.3 基座项目动态注册子应用和子路由
将子项目注册到基座中,我们首先需要配置基座项目的config。这里是将子项目静态直接注入,后面例子会给到动态注入
config/config.ts
export default defineConfig({
...
qiankun: {
master: {
apps: [
{
name: 'sub-vue',
entry: '//localhost:8001',
},
],
},
},
...
})
将子项目路由,配置到config/routes.ts (脚手架会创建两个页面)例子路由下面
子页面路由需要要求我们的microApp 的值必须和上面注册apps 的name 一样。同样的,路由的path 字段最好也和microApp值一样。不一样的话,不会报错,但是qiankun会给出 warning 提示
{
name: 'sub-vue',
icon: 'smile',
path: '/sub-vue',
microApp: 'sub-vue',
},
然而项目通常都是根据当前登录人。动态的通过接口返回子项目,以及子项目的部分路由。所以我们这里采用了动态注入apps 和routes 。所以可以把上面的两部分修改下,config/routes.ts下修改的可以全都撤销。config/config.ts 文件中只需要把apps 的值清空就可以了。
config/config.ts
export default defineConfig({
...
qiankun: {
master: {
apps: [],
},
},
...
})
我们会请求接口,返回apps字段和
接下来就是在基座项目中动态注入apps 和routes 的方式。这里的路由并不是真正的子项目业务路由,而是子项目的的入口路由。所以格式化一下apps 的结构就能拿到,清洗格式就是下面app.tsx的代码
let routeArr = apps.map((item:any)=>{
return{
microApp:item?.name,
path:`/${item?.name}`,
exact:true
}
});
子项目 入口 路由格式
{
path: '/sub-vue',
microApp: 'sub-vue',
},
子项目 业务 路由格式
{
name: 'sub-vue1', // 菜单栏展示名称
icon: 'smile', // 菜单栏展示图标
path: '/sub-vue/vue1', // 子项目业务路由(/vue1) 如果这里的路由前缀不是和microApp 一样,需要添加microApp字段
microApp: 'sub-vue',
},
src/app.tsx 如果这个文件没有,就新建一个文件
import { dynamic } from 'umi';
import LoadingComponent from '@/components/PageLoading';
import { getToken } from '@/utils/auth';
import type {
BasicLayoutProps as RouteProps,
} from '@ant-design/pro-layout';
let extraRoutes: object[] = [];
let API = '/api';
const url = API+'/user/menus';
const fetchUrl = fetch(url,{
headers:{
'Authorization':getToken()
}
}).then((res) => {
return res.json();
})
const formatRoute = function (routes:RouteProps['route'][],type:string) {
let route:RouteProps['route'];
let arr:any;
arr= routes?.map(item => {
if (Array.isArray(item?.routes)) {
route = formatRoute(item?.routes||[],type);
}
if(type==='app'){
return {
microApp: item?.microApp,
path: item?.path,
}
}else if(type==='route'){
return {
name: item?.name,
icon: 'table',
exact: true,
path: item?.path,
routes:route,
component: dynamic({
loader: () =>
import(/* webpackChunkName: 'layouts__MicroAppLayout' */ '@/layouts/MicroAppLayout'),
loading: LoadingComponent,
}),
}
}
})
return arr;
}
export const qiankun = fetchUrl
.then((res ) => {
const {data:{menus:{apps=[],routes=[]}}} = res;
// 格式化路由结构,子项目入口路由是通过apps的值map 出来的
let routeArr = apps.map((item:any)=>{
return{
microApp:item?.name,
path:`/${item?.name}`,
exact:true
}
});
// 此处是通过接口将apps 和格式化的子项目入口路由动态的加载到基座项目中
return Promise.resolve({
apps,
routes:routeArr,
lifeCycles: {
afterMount: (props: any) => {
},
},
});
}).catch(()=>{
return Promise.resolve({
apps:[],
routes:[]
})
});
export function patchRoutes({ routes }:any) {
let routeArr = formatRoute(extraRoutes||[],'route');
routes[0]?.routes[1].routes[0].routes.push(...routeArr);
}
export async function render(oldRender: any) {
fetchUrl.then((resJson) => {
extraRoutes = resJson?.data?.menus?.routes;
// extraRoutes = resJson.routes;
oldRender();
}).catch(()=>{
return Promise.resolve({
apps:[],
routes:[]
})
});
}
patchRoutes这里需要注意一点,可以结合自己的业务将入参routes 追加到 自己的路由中
routes[0]?.routes[1].routes[0].routes.push(...routeArr);此路由是antd 中的路由嵌套。
还有一点需要注意的是,这个app.tsx 文件,会在项目启动的时候就加载。这个时候还没有登录,接口相当于没有token。所以想了一个这种的办法,就是当login 成功以后,路由不通过push去加载,而是通过location.href去加载 ,相当做了一次刷新,同样的这个接口会请求两次。这样暂时把这个问题解决了。如果有更好的方法欢迎提出来。
贴一下接口结构
子应用搭建
注意创建子项目的时候,所有子项目的package.json 中的name 属性要与刚才基座项目的microApp 的值对应。
2.1 创建 改造 子项目(react)。
yarn create @umijs/umi-app sub-react
cd sub-react
yarn add @umijs/plugin-qiankun -D
下载 并加载 插件 在 .umirc.ts 文件中添加以下代码:
qiankun: {
slave: {}
}
同样的在 src/app.ts 没有则新建
export const qiankun = {
// 应用加载之前
async bootstrap(props) {
console.log('app1 bootstrap', props);
},
// 应用 render 之前触发
async mount(props) {
console.log('app1 mount', props);
},
// 应用卸载之后触发
async unmount(props) {
console.log('app1 unmount', props);
},
};
2.2 创建子项目 (vue)
注意使用vue-cli 创建vue 项目的时候,需要注意vue-cli 的版本,这次项目构建使用的版本是3.12.1。版本再高一点,会使用createApp 去构建。版本低一点,没有vue.config.js。所以使用这个版本。
vue init webpack sub-vue
一步一步改造
- src/router/index.js 这里是直接将VueRouter 向外报露出去,如果是使用了多个
<route-view>,则需要对路由进行改造
import Vue from 'vue'
import VueRouter from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
import Vue1 from '@/views/view1'
import Vue2 from '@/views/view2'
Vue.use(VueRouter)
import './public-path';
const baseUrl = window.__POWERED_BY_QIANKUN__?'/sub-vue':'';
export default new VueRouter({
mode:'history',
routes: [
{
path: baseUrl+'/',
name: 'HelloWorld',
component: HelloWorld
},
{
path: baseUrl+'/vue1',
name: 'Vue1',
component: Vue1
},
{
path: baseUrl+'/vue2',
name: 'Vue2',
component: Vue2
},
]
})
- src/main.js 这里是将route 整合到app中。
import Vue from 'vue'
import App from './App.vue';
import router from './router';
import './public-path';
Vue.config.productionTip = false;
let instance = null;
function render(props={}) {
const { container } = props;
instance = new Vue({
router,
render: h => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 独立运行
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
// 生命周期
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
console.log('[vue] props from main framework', props);
// storeTest(props);
render(props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
同样的需要在 main.js 同级创建public-path.js 文件,文件内容
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
- 修改vue.config.js
...
devServer: {
...
// 配置跨域请求头,解决开发环境的跨域问题
headers: {
"Access-Control-Allow-Origin": "*",
},
...
}
...
configureWebpack: {
...
output: {
// 微应用的包名,这里与主应用中注册的微应用名称一致
library: name,
// 将你的 library 暴露为所有的模块定义下都可运行的方式
libraryTarget: "umd",
// 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
jsonpFunction: `webpackJsonp_${name}`,
...
},
}
...
2.3 在react 子项目中,页面的路由不需要添加sub-react前缀。也就是说当我们在基座项目中访问 /sub-react/first 的时候,first 是react 项目的路由。但是vue 稍有不同。vue 需要通过当前是否是在微前端中,动态的拼接当前路由。这一点如果有更好的处理方式烦请告知。
以上已经经过实测,项目是公司项目,git地址无法提供。 如有遗漏或者运行不正确可以添加联系方式 18501719005