多页签多路由项目改造微前端

最近工作重心在平台这块,恰好做了一些微前端的改造,在这里总结一些遇到的挑战和踩过的坑。

缘起

目前负责的工程是这种经典的页面布局,通过左侧菜单打开一个个页面,并可以随时切换

image.png

以前的实现基本上是通过iframe技术实现的,打开一个标签页其实是加载一个单独的网址。

通过iframe打开标签页其实已经是一种较完美的方案。iframe天生提供了硬隔离,不用关心任何污染与互相影响。

但是iframe仍有些不足。在我们项目场景中,有三点不满意:

1.资源加载多份,打开多个iframe标签意味着加载多次react等较大的三方库,电脑配置低会卡顿

2.每次关闭再打开标签都是一次浏览器窗口资源重建的过程,有明显白屏

3.多路由的子应用,两个iframe页签打开的是同一个子应用的不同路由,无法优化

经过内部讨论,决定使用微前端方案。

微前端框架选择了qiankun作为基础。emp的话需要子应用升级webpack5,现有场景是有大量其他部门的研发,极难做到协同。

qiankun官网地址qiankun.umijs.org/zh/guide

以下改造过程默认对qiankun和微前端有些了解

思路

1.多页签来回切换时,都要秒开对应页签的内容,至少要赶上keep-alive的速度,这要求我们对子应用做一定的缓存或者直接不卸载,目前的话我们现有标签先做不卸载的处理。多子应用共存的话我们可以使用qiankun2.0的手动加载微应用方法,打开一个标签页就手动加载一个子应用,关闭就卸载。

2.如果基座和众多子应用都有自己路由并监听地址栏的话会非常混乱,这里需要把接入的子应用项目中的路由统统改造成虚拟路由

vue的router使用 abstract mode

react的router使用 MemoryRouter

基座改造

现有的基座是一个React项目,改造起来比较轻松

  • 引入qiankun库

  • 新建一个微前端组件容器(示例如下)

import { MicroApp, loadMicroApp } from 'qiankun';
import React from 'react';

export default class MicroAppWapper extends React.Component<any> {

    private containerRef: React.RefObject<HTMLDivElement> = React.createRef();

    private microApp: MicroApp = null;

    componentDidMount() {
        const { name, entry, ...props } = this.props;
        this.microApp = loadMicroApp({ name, entry, container: this.containerRef.current, props }, {
            fetch(url, ...args) {
                
                // 给指定的微应用 entry 开启跨域请求
                if (url === 'http://app.alipay.com/entry.html') {
                    return window.fetch(url, {
                        ...args,
                        mode: 'cors',
                        credentials: 'include',
                    });
                }

                return window.fetch(url, ...args);
            },
            sandbox: {
            // 开启shadowdom会导致适配成本,请慎重,下面会详细讲解
                strictStyleIsolation: true
            }
        });
    }

    componentWillUnmount() {
        this.microApp.unmount();
    }

    componentDidUpdate() {
        // 可以做一些自定义操作
    }

    render() {
        return <div ref={this.containerRef}></div>;
    }
}
复制代码
  • 替换原来的iframe(示例如下)
if (type === "microApp") {
	return <MicroAppWapper
		name={title}
		entry={domain}
		initialUrl={domain}
	/>
}
if (type === "service") {
	return <Iframe url={url} />
}
复制代码

子应用改造

基本改造

打包工具

微应用的打包工具需要增加如下配置:

// package.json里的name尽量不要大众化,不然不容易分辨
const packageName = require('./package.json').name;

module.exports = {
  output: {
    library: `${packageName}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${packageName}`,
  },
};
复制代码

微应用入口

微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。

/**
 * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
 */
export async function bootstrap() {
// 基本不用做什么
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们走一部分entry的代码,并在这里触发应用的渲染方法
 */
export async function mount(props) {
// react示例代码 ?   (下面的只是样板代码)
  ReactDOM.render(<App/>, props.container.querySelector('#root'));

// vue示例代码 ?
  const { container } = props;
  instance = new Vue({
    render: h => h(App)
  }).$mount(container ? container.querySelector('#root') : '#root')
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount(props) {
  
  ReactDOM.unmountComponentAtNode(
    props.container.querySelector('#root'),
  );

}

export async function update(props) {
  // 目前基本不用做什么
}

if (!window.__POWERED_BY_QIANKUN__) {
    // 之前的入口样板代码⤵️    请用上面的if逻辑⤴️包裹起来,这样子应用就不会加载两次
ReactDOM.render(<App/>, props.container.querySelector('#root'));
}
复制代码

非webpack情况

非webpack构建的应用请看??
qiankun.umijs.org/zh/guide/tu…

路由改造

目前由于多页签模式,会加载许多子应用,子应用路由如果都监听地址栏进行响应的话,会不可避免的引发混乱。所以我们需要改造子应用为 模拟路由的形式

mount阶段透传参数

mount的时候会传入initialUrl,我们只需要透传给App组件即可

  ReactDOM.render(<App
    initialUrl={props.initialUrl}
  />, props.container.querySelector('#root'));
复制代码

app阶段示例代码

vue的router使用 abstract mode

image.png

react的router使用 MemoryRouter

import { MemoryRouter, withRouter, Route, Switch } from 'react-router'

const App = (props) => {
  let initialEntries;
  if (props.initialUrl) {
  // 处理它的哈希值
    const { hash } = new URL(props.initialUrl)
    initialEntries = [hash.replace(/^#/g, '')]
  } else {
  // 这段代码是为了能独立打开
    const hash = window.location.hash;
    const rehash = hash.replace(/^#/g, '');
    if (rehash && rehash !== '/') {
      initialEntries = [rehash]
    } else {
      initialEntries = ['/']
    }
  }
  // 替换原来的Router为MemoryRouter
  return  <MemoryRouter
            initialEntries={initialEntries}
          >
            <Root />
          </MemoryRouter>
}
export default App
复制代码

split chunk的处理

如果打包出多个chunk.js,可能会拉取不到chunk.js,需要配置一下webpack的public_path。
在入口的entry.js上顶部加上如下代码:

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
复制代码

shadow_DOM适配

这个工作可能是一个较大的工作,如果子应用的页面大多数都在自己部门 或 整个项目只是一个中后台内部项目,我认为没必要上shadowdom

因为涉及到许多其他部门,为了避免样式加载混乱,基座对子应用节点包裹了一层影子DOM

developer.mozilla.org/zh-CN/docs/…

shadow_DOM 需要大量的适配工作,不过,目前看shadow_DOM规范 正在正确的道路上前进,极有可能在将来成为标准,所以这部分工作还是值得的。

React升级到17以上

shadow_DOM对document做了代理,17版本以下的React合成事件会不生效

image.png

最简单的方式是升级到17以上

css适配

这个需要检查一下加载子应用后整体样式的变化

原生方法报错

document.querySelector或document.getElementById等方法会失效

解决方案:

1.主流框架中其实不推荐操作Dom,请用框架的方式修改

2.在mount的时候会把container这个子应用容器拿到,我们可以放在window上,所有用的地方做兼容

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  // 把container挂载到自己的window上(此处不会污染基座的window)
  window.MicroAppContainer = props.container
  ....
}
复制代码

然后全局搜索document.,找到querySelector或getElementById等方法替换成
(window.MicroAppContainer || document).

第三方库运行时共享

基座如果加载过react/vue等库,我们可以在子应用中直接使用它,这是微前端最直接的优化手段

但不得不说,这个共享运行时不太好,可能会带来一些问题

具体操作是基座加载CDN资源,并配置webpack的externals,这样window上就有了相应的库

子应用配置相同的externals就行

(axios等运行时单例的库一定不要带上…)

拓展

1.内存监控

多应用共存仍然会拖累内存,我们需要监听浏览器内存,卸载最不常用的子应用(LRU算法)

2.跨域

子应用请求的后端接口可能会跨域,因为host是基座的host,这块可能需要后端配合或搭建中间层转发

3.部署调试

如果是iframe,部署方式不变。其他部门调试子应用可能需要这边提供一套完整的脚手架

4.微应用和原地址共存

没有特殊情况是支持共存的,特殊需求可以通过webpack配置多个html来做

一些其他注意事项

1.start这个qankun方法在工程中不用加,里面主要做了一些Single-Spa对路由的处理,这里涉及不到

2.后期如果qiankun支持module federation的话,可以让组件级别复用更加容易(期待)

3.遇到错误可以先到官网查看 qiankun.umijs.org/zh/faq

总结

总结一下上面的改造过程,有三个点需要注意

1.多页签的话需要手动控制子应用加载和卸载(loadMicroApp),不基于路由

2.子应用工程里面有路由的话需要改造成虚拟路由

3.shadowdom做样式硬隔离目前成本较高,根据场景来

截至本文写完,已经小范围试用,如果有任何问题或者建议,欢迎评论区拍砖!如果感觉吃力的话,下一篇我会对qiankun的原理进行剖析,着重讲解几个我关注的点

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享