背景
- qiankun技术文档:qiankun
- 使用qiankun原因:想在原有的项目基础上使用vue3,开发起来更加方便,但是完全重构工作量大
- 原有项目:client端,基于umi和roadhog的ant-design-pro框架;server:egg
- 项目预期:原有的业务主题内容不受影响,新功能使用vue3+antProComponents(vue3版本的暂时还只有一个proLayout的组件)+ts实现,头部和菜单导航栏都使用vue3新开发的内容
项目准备
- 项目结构:原始项目大结构就是client(ant-design-pro)和server(egg)。qiankun的使用核心就是需要主应用和微应用,在主应用中注册微应用,因此计划新新建目录client-main作为主应用,client作为微应用。
- client-main的搭建,使用vue-cli直接选用vue3,安装需要的工具:ts、router、ant-design-vue。
- client中的目录结构暂时保持不动
基础搭建
-
主应用
- 安装qiankun
$ yarn add qiankun # 或者 npm i qiankun -S- 主应用中注册微应用
import { registerMicroApps, start } from "qiankun"; registerMicroApps([ { name: "kol-mis-client", entry: process.env.NODE_ENV === "development" ? "//localhost:8000" : "/client", container: "#qiankun-container", activeRule: () => true, }, ]) start();3.注册微应用各属性的解释见官方文档
-
微应用
- 路由
刚开始是使用的hash路由的模式,官方更推荐使用history的模式,实践也确实发现hash模式在配置住应用路由的时候有一些问题,所以将微应用的路由改成了history的模式
// import {createHashHistory} from 'history'; // user BrowserHistory import { createBrowserHistory } from 'history'; // 1. Initialize const app = dva({ // history: createHashHistory(), history: createBrowserHistory(), });- 注册qiankun钩子函数(ant-design-pro渲染和原始react有所区别,所以该内容也与官方文档有些微区别)。
function render(props) { const { container } = props; app.start(container ? container.querySelector('#root') : document.querySelector('#root')); } if (!window.__POWERED_BY_QIANKUN__) { render({}); } export async function bootstrap() { console.log('[react16] react app bootstraped'); } export async function mount(props) { console.log('[react16] props from main framework', props); render(props); } export async function unmount(props) { console.log('微服务卸载') const { container } = props; container.querySelector('#root').innerHTML = '' }- webpack的配置
ant-design-pro的打包工具是采用的roadhog,roadhog的webpack配置是经过它们进行封装的,提供给用户简单直接的配置,但是这个配置并不支持qiankun要求的配置。解决办法是,roadhog也支持webpack.config.js进行自定义配置,只是可能出现隐性问题,所以官方不推荐。(暂时使用正常)
const { name } = require('./package.json'); export default function (config, env) { const newConfig = { plugins: [], ...config, output: { ...config.output, library: `${name}-[name]`, libraryTarget: 'umd', jsonpFunction: `webpackJsonp_${name}`, }, devServer:{ headers : { 'Access-Control-Allow-Origin': '*', }, historyApiFallback : true, hot : false, watchContentBase : false, liveReload : false, }, }; // merge or override return newConfig; } - 路由
控制主应用和微应用的显示
-
主子应用显示原理
主应用中需要创建一个标签用于承载子应用的内容,我是将这个标签放在了layout中<div id="qiankun-container"></div>主应用会通过注册微应用activeRule属性来判断哪些路由是子应用的路由,哪些应用是主应用的路由,从而操作是否给子应用容器中注入内容。但是我在上面的activeRule属性传入的值是:
() => true,也就是说这代表了任何路由都会加载子应用的内容,那这样岂不是会出现问题。这个问题就放在了子应用中解决。
在子应用中的layout中加载如下代码,表示如果没有加载到有效路由就返回空内容<Switch> {redirectData.map(item => ( <Redirect key={item.from} exact from={item.from} to={item.to} /> ))} {getRoutes(match.path, routerData).map(item => ( <AuthorizedRoute key={item.key} path={item.path} component={item.component} exact={item.exact} authority={item.authority} redirectPath="/exception/403" /> ))} <Redirect exact from="/" to={bashRedirect} /> <Route render={Empty} /> //重点是这一句 Empty是引入的一个空标签的文件 </Switch>
问题解决
-
发现从主应用到子应用,然后子应用中有页面跳转之后就没办法再跳转到其他页面了,路由更换报错 Error with push/replace State DOMException: Failed to execute 'replaceState' on 'History': A history state object with URL 'http://localhost:8080undefined/' cannot be created in a document with origin 'http://localhost:8080' and URL 'http://localhost:8080/koluser/certify-detail/1893/1'.
原因还是react-router和vue-router处理的差别,导致在页面跳转的时候一些内容的丢失。解决办法也查找了一些资料,最终解决办法//主应用使用的嵌套路由 router.beforeEach((to, from, next) => { if (!window.history.state.current) window.history.state.current = to.fullPath; if (!window.history.state.back) window.history.state.back = from.fullPath; // 手动修改history的state return next(); }); -
主应用和子应用样式隔离问题,主要是因为主应用和子应用都用的ant系列,样式就产生了影响,主应用的样式会被子应用的样式覆盖,其实这个官方也有提出解决办法 。注意:给antd添加前缀只有在3.26.20版本或者以上版本才支持。
-
项目部署
-
打包之后的目录结构
client项目下的内容就打包到public下面的client文件夹,client-mian的内容就直接打包到public下面。 -
client打包代码稍微的修改:打包目标文件夹和publicPath
publicPath: process.env.NODE_ENV === "development" ? "/" : '/client', //1.publicPath非常重要,因为client打包后的内容不再是在根目录了 //2.publicPath的值也是跟注册微应用的entry保持一致 outputPath: path.resolve(__dirname, '../server/app/public/client'), -
srver(egg)的配置
//1.模版引擎的配置 config.default.ts config.view = { root: [ path.join(appInfo.baseDir, "app/view"), path.join(appInfo.baseDir, "/app/public"), ].join(","), // `${appInfo.baseDir}/app/view,${appInfo.baseDir}/app/public`,//在windows下不能用这种方式 defaultExtension: "ejs", mapping: { ".html": "ejs", ".ejs": "ejs", }, };//2。路由单独对/client的配置 router.get('/client', controller.client.test); import { Controller } from "egg"; //3.ClientController的处理 controller/client.ts export default class ClientController extends Controller { public async test() { const { ctx } = this; console.log('test') return await ctx.render("/client/index.html");//重点:配合打包时候的publicPath的配置才能成功访问到/client/index.html下面的资源 } }//4.所有路由的总处理 router.ts router.get('*', controller.home.index); //页面刷新的时候也不会404了 //5.主体内容的处理 import { Controller } from "egg"; export default class HomeController extends Controller { public async index() { try { // return await ctx.render("auth.ejs"); return await ctx.render("index.html"); } catch (e) { console.log(e); } } }
-
总结
以上就是实践的主体内容,一些细小的问题也不太记得了,遇到什么问题可以评论,我可以回忆一下解决方法。主要还是查看官方文档,遇到什么问题解决什么问题。