关于如何从零开始使用 qiankun 搭建 React 微前端应用

1,912 阅读3分钟

image.png

「这是我参与2022首次更文挑战的第15天,活动详情查看:2022首次更文挑战

qiankun 并不是单一个框架,它在 single-spa 基础上添加更多的功能。任意 js 框架均可使用。微应用接入像使用接入一个 iframe 系统一样简单,但实际不是 iframe。几乎包含所有构建微前端系统时所需要的基本能力,如 样式隔离、js 沙箱、预加载等。

乾坤整合的主要是基于当前主流前端框架 vue, react, agular 实现的系统,jquery 应用支持相地较弱(主要因为大多是多页应用)

image.png

本文采用 React + qiankun

1. create-react-app qiankun_learn

我们期望的效果是,点击上方的 Tab 切换到对应的子应用。

2. yarn add react-router-dom

改造 index.js,使用 HashRouter 对应用进行包裹。

import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter } from 'react-router-dom';
import MyRouter from './router';
import './index.css';

ReactDOM.render(
  <HashRouter>
    <MyRouter />
  </HashRouter>,
  document.getElementById('root')
);

3. MyRouter 编写

MyRouter 中,包含着页面的当中的路由情况。

我们使用 BasicLayout 对页面进行包裹。当路有切换时,展示不同的组件。

import React, { Fragment } from 'react';
import { Route, Switch } from 'react-router-dom';
import BasicLayout from '../pages/layout/index';

function MyRouter() {
  const LayoutRoute = [
    {
      url: '/app',
      component: BasicLayout
    },
    {
      url: '/signpost',
      component: BasicLayout
    },
    {
      url: '/link',
      component: BasicLayout
    },
    {
      url: '/testBench',
      component: BasicLayout
    },
    {
      url: '/funnel',
      component: BasicLayout
    },
    {
      url: '/dataplatform',
      component: BasicLayout
    },
    {
      url: '/data',
      component: BasicLayout
    },
    {
      url: '/accept',
      component: BasicLayout
    },
    {
      url: '/rules',
      component: BasicLayout
    },
    {
      url: '/disposal',
      component: BasicLayout
    },
    {
      url: '/serviceScore',
      component: BasicLayout
    }
  ];
  return (
    <Fragment>
      <Switch>
        {LayoutRoute.map(val => {
          const { url, component } = val;
          return <Route key={url} path={url} component={component} />;
        })}
      </Switch>
    </Fragment>
  );
}

export default MyRouter;

4. BasicLayout

采用 antd 中比较常见的布局

image.png

import React, { Fragment, useEffect } from "react";
import { Layout, Menu } from 'antd';
import { Link } from 'react-router-dom';
import Header from './Header';
import Content from './Content';

const { Sider, Footer } = Layout;

function BasicLayout(props) {
  const {
    location: { pathname }
  } = props;

  return (
    <Fragment>
      <Layout>
        <Header />
        <div id="micro-app"></div>
        {renderContent(pathname, props)}
        <Footer style={{ textAlign: 'center' }}>Ant Design ©2018 Created by Ant UED</Footer>
      </Layout>
    </Fragment>
  )
}

export default BasicLayout;

renderContent则是根据路由的不同,切换不同的内容。

const renderContent = (pathname, props) => {
  if (pathname.includes('signpost')) {
    return renderSignPostContent();
  } else if (pathname.includes('governlink')) {
    return renderGovernLinkContent(props);
  } else if (pathname.includes('/strategy') {
    return renderStrategyContent();
  } else if (pathname.includes('dataplatform')) {
    return renderDataPlatformContent(props);
  } else if (pathname.includes('accept') || pathname.includes('disposal')) {
    return null;
  }

  return (
    <Content contentClass={'normal-wrap'}></Content>
  )
}

Content 是个基础组件,在这个组件中,通过 shouldComponentUpdate 做一些优化。

src/pages/layout/Content.js

import React from "react";
import { Layout } from 'antd';
import RoutePaths from '../../router/paths';
import { Switch, Route } from "react-router-dom";

const { Content } = Layout

class MyContent extends React.Component {
  shouldComponentUpdate(nextProps) {
    if (this.props.contentClass !== nextProps.contentClass) {
      return true
    } else {
      return false;
    }
  }
  render() {
    return (
      <Content className={this.props.contentClass}>
        <Switch>
          {
            RoutePaths.map(val => {
              const { url, component } = val;
              return <Route key={url} path={url} component={component}></Route>
            })
          }
        </Switch>
      </Content>
    )
  }
}

export default MyContent;

5. RoutePaths

当路由命中的时候,切换显示对应的组件。

import Error from '../pages/error/404/index'
import Noauth from '../pages/error/noauth/index'
import Home from '../pages/home/index'
import Signpost from '../pages/signpost/queryList';
import LinkConfig from '../pages/governLink/linkConfig';
import Funnel from '../pages/funnel/index';

const ROUTES = [
  {
    url: '/app/404',
    component: Error,
    title: '异常-页面不存在'
  },
  {
    url: '/app/noauth',
    component: Noauth,
    title: '异常-无访问权限'
  },
  /**
   * 欢迎页
   */
  {
    url: '/home',
    component: Home,
    title: '首页'
  },
  /*
   * 路标
   */
  {
    url: '/signpost/signpost_query_list',
    component: Signpost,
    title: '列表'
  },
  /*
   * 配置
   */
  {
    url: '/governlink/linkConfig',
    component: LinkConfig,
    title: '链接'
  },
  {
    url: '/juanzong/query_list',
    component: Funnel,
    title: "1"
  }
];

export default ROUTES;

6. 每个子应用的内容

const renderSignPostContent = () => {
  return (
    <Layout>
      <Sider
        className="root-sider signpost-sider"
        width={200}

      >
        {renderSiderMenu(signpostMenu)}
      </Sider>
      <Content contentClass={'signpost-wrap'}></Content>
    </Layout>
  )
}

renderSiderMenu 是抽离出来的单独组件,传入内容,渲染出不同的 Sider。

const renderSiderMenu = (menu) => {
  return (
    <Menu
      theme="light"
      mode="inline"
    >
      {menu.map(item => {
        const { url, name, key, children } = item;
        if (!Array.isArray(children) || !children.length) {
          return (
            <MenuItem>
              <Link to={url}>{name}</Link>
            </MenuItem>
          );
        }
        return (
          <SubMenu
            key={key}
            title={
              <Fragment>
                <span>{name}</span>
              </Fragment>
            }
          >
            {children.map(item => {
              return (
                <MenuItem key={item.url}>
                  <Link to={item.url}>{item.name}</Link>
                </MenuItem>
              );
            })}
          </SubMenu>
        );
      })}
    </Menu>
  );
}

写到目前,我们可以看到页面的显示

image.png

7. 🔨 使用 qiankun

src/pages/layout/index.js

useEffect(() => {
  const { host } = window.location;

  registerMicroApps([
    {
      name: 'rules',
      entry: host.startsWith('****')
        ? `****`
        : host === '*****'
          ? `*****`
          : '****',
      container: '#micro-app',
      activeRule: () => window.location.hash.startsWith('#/rules')
    },
    {
      name: 'accept',
      entry: host.startsWith('****')
        ? `******`
        : host === '****'
          ? `*****`
          : '*****',
      container: '#micro-app',
      activeRule: () => window.location.hash.startsWith('#/accept')
    },
    {
      name: 'strategy',
      entry: host.startsWith('****')
        ? `*****`
        : host === 'localhost:3000'
          ? '****'
          : host === '*****'
            ? `*****`
            : '*****',
      container: '#micro-app',
      activeRule: () => window.location.hash.startsWith('#/strategy')
    }
  ]);

  start();
})