微前端qiankun的应用实践

2,538 阅读2分钟

背景

  • qiankun技术文档:qiankun
  • 使用qiankun原因:想在原有的项目基础上使用vue3,开发起来更加方便,但是完全重构工作量大
  • 原有项目:client端,基于umiroadhogant-design-pro框架;server:egg
  • 项目预期:原有的业务主题内容不受影响,新功能使用vue3+antProComponents(vue3版本的暂时还只有一个proLayout的组件)+ts实现,头部和菜单导航栏都使用vue3新开发的内容

image.png

项目准备

  1. 项目结构:原始项目大结构就是client(ant-design-pro)和server(egg)。qiankun的使用核心就是需要主应用和微应用,在主应用中注册微应用,因此计划新新建目录client-main作为主应用,client作为微应用。
    image.png
  2. client-main的搭建,使用vue-cli直接选用vue3,安装需要的工具:ts、router、ant-design-vue。
  3. client中的目录结构暂时保持不动

基础搭建

  • 文档参考
    主应用React 微应用

  • 主应用

    1. 安装qiankun

    $ yarn add qiankun # 或者 npm i qiankun -S

    1. 主应用中注册微应用
    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.注册微应用各属性的解释见官方文档

  • 微应用

    1. 路由
      刚开始是使用的hash路由的模式,官方更推荐使用history的模式,实践也确实发现hash模式在配置住应用路由的时候有一些问题,所以将微应用的路由改成了history的模式
    // import {createHashHistory} from 'history';
    // user BrowserHistory
    import { createBrowserHistory } from 'history';
    // 1. Initialize
    const app = dva({
      // history: createHashHistory(),
      history: createBrowserHistory(),
    });
    
    1. 注册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 = ''
      }
    
    1. 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>
    

问题解决

  1. 发现从主应用到子应用,然后子应用中有页面跳转之后就没办法再跳转到其他页面了,路由更换报错 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();
    });
    
  2. 主应用和子应用样式隔离问题,主要是因为主应用和子应用都用的ant系列,样式就产生了影响,主应用的样式会被子应用的样式覆盖,其实这个官方也有提出解决办法 。注意:给antd添加前缀只有在3.26.20版本或者以上版本才支持。

  3. 项目部署

    1. 打包之后的目录结构
      image.png
      client项目下的内容就打包到public下面的client文件夹,client-mian的内容就直接打包到public下面。

    2. client打包代码稍微的修改:打包目标文件夹和publicPath

        publicPath: process.env.NODE_ENV === "development" ? "/" : '/client',
        //1.publicPath非常重要,因为client打包后的内容不再是在根目录了
        //2.publicPath的值也是跟注册微应用的entry保持一致
        outputPath: path.resolve(__dirname, '../server/app/public/client'),
      
    3. 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);
          }
        }
      }
      

总结

以上就是实践的主体内容,一些细小的问题也不太记得了,遇到什么问题可以评论,我可以回忆一下解决方法。主要还是查看官方文档,遇到什么问题解决什么问题。