REACT + QIANKUN 实现微前端单点登录,7000 字全流程解析

2,544 阅读13分钟

不同于大多数 qiankun 文章的简单尝鲜或者试用介绍,这篇文章会完整的介绍真实项目里如何使用 react 和 qiankun 落地微前端接入以及实现相关的单点登录功能,同时也会讲到实际开发中会出现的问题、细节、相关概念以及背后的原理。如果你接到了 react 微前端的开发需求,那么基本上只看本文就足够你了解整个流程都需要干什么事情了。

本文也算是对我之前微前端工作的一个总结,涉及的相关流程方案都是我实际工作中使用过的,不一定全面,但足够完成任务,如果有更好的方案欢迎评论区交流。

前言

就微服务设计这一方面来说,前端需要操心的比后端少很多,主要就是两方面:1、把子应用页面集成进来,2、给子应用传递一个 token,让其可以跳过登录直接使用。因此,本文内容也分为两部分:父应用如何使用 react + qiankun 来集成子应用,以及子应用应该如何进行改造并实现单点登录。

下面的内容默认父应用和子应用均使用 reac + qiankun 开发。如果你的子应用由第三方提供或者无法修改代码的话,推荐 iframe 而不是用 qiankun 进行集成,详细讨论可以看之前的文章 前端微服务技术选型及页面集成方案

前期沟通

第一步就是要和后端沟通,了解整个单点体系是怎么运作的。比如 微服务之间的 token 是否通用,一般来说会有两种方案:

  • token 通用,你在应用 A / 基座应用里拿到的 token 可以直接用在应用 B / 子应用里。
  • token 不通用,想要访问子应用,必须先拿本应用的 token 向后端接口“兑换”子应用需要的 token。

这两种方案对于前端的单点功能实现有很大影响。如果 token 通用的话,那 qiankun 就可以使用 registerMicroApps 直接监听路由来挂载子应用。而如果不通用的话,则一般会先请求后台接口,拿到所需的数据后,再使用 loadMicroApp 手动挂载子应用。

除了这个,还需要了解微前端的基本信息是怎么提供的,比如名称、入口、跳转路径这些。一般来说后端都会提供一个接口返回一个数组,包含每个微前端的这些信息,或者会在 CI / CD 打包时通过配置文件的形式给到前端代码里,再或者最朴素的方案就是直接写死到前端代码里。这个也需要前后端提前商量好(这个看情况,如果 token 不通用的话就不需要这个接口了)。

页面集成方案A - token 通用

上面关于 token 是否通用提到了两种方案,这里挨个介绍一下,首先是 token 通用的情况。

token 通用时对于前端会更简单,先 registerMicroApps 注册所有微服务,然后监听路由,进入子应用容器后直接把本应用的 token 转发给子应用就行了。

首先是注册微服务,这里要根据后端怎么提供微前端信息来开发,如果后端提供了接口来返回所有微前端的基本信息,那么就可以 在路由的根节点上包含一个基础组件,这个组件会请求微前端信息,并在响应抵达时加载再后续的路由组件。因为用户可能直接访问子应用的路由,所以要确保子应用在加载前,微服务的信息已经注册给 qiankun 了:

import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { registerMicroApps } from 'qiankun';
import { useSubAppInfo } from '@/service/xxx';

const SubAppBase: React.FC = () => {
  const dispatch = useDispatch();
  // 获取微服务信息的 hook 封装
  const { data: subAppList, isLoading } = useSubAppInfo();

  useEffect(() => {
    // 把微服务信息存储到 redux 中
    dispatch(setSubAppInfo(subAppList));
    // 注意下面的操作
    const microAppConfig = subAppList.map(app => ({
      ...app,
      container: '#subapp-viewport',
      activeRule: `/subApp/${app.appCode}`,
      props: {
        // 子应用是否显示标题栏
        headerVisible: false,
        // 子应用的路由前缀
        routeBasename: `${process.env.PUBLIC_URL}/subApp/${app.appCode}/`
      },
    }));
    // qiankun 注册微服务
    registerMicroApps(microAppConfig);
  }, [subAppList]);

  if (isLoading) {
    return (
      <div>应用信息加载中,请稍等...</div>
    );
  }

  return <Outlet />;
};

很简单,就是微服务的列表信息到了之后就通过 useEffect 触发 qiankun 的微服务注册,并在数据到了之后再使用 OutLet 加载子路由的内容。

其中比较重要的是 useEffect 里创建变量 microAppConfig 的那一段代码,因为后端提供的微服务信息一般只会包括微服务的名字 name,微服务的前端入口 entry 以及微服务的唯一编码 appCode。而 qiankun 注册微服务所需的容器 containeractiveRule 后端一般都不会去存,所以需要前端自己处理。


如果你对 qiankun 不太了解的话,这里简单介绍一下 registerMicroApps 方法,这个方法调用后,qiankun 就会一直关注着你的 history 路由,如果当前路由可以匹配到任一注册过的微服务的 activeRule 的话,那么就会在页面中使用 container 字段查找 dom 节点,并以此为容器渲染微服务的页面内容,就像是新建图表或者地图那样。而渲染的内容则是从 entry 字段中获取的。

具体的路由匹配规则可以看 API 说明 - qiankun (umijs.org) 中的规则示例。

如果子应用加载完成了,那么就会调用子应用上暴露的几个回调,并把 props 对象里的内容传过去,一般来说这里会传下面几个自定义参数:

  • headerVisible:是否显示子应用的标题栏
  • routeBasename:子应用的路由模块使用的路由前缀,这个下文会详细介绍
  • token:可选,看你的 token 更新是否比较勤,经常更新的话这里就不要直接传字面值,不然子应用加载时就可能会拿到过期的 token。

注意,因为这些参数需要子应用里自行实现如何使用,所以叫啥都可以,只需要子应用里的名字对应即可。


微服务注册完之后,现在来看下路由怎么做,先上代码:

import React from 'react';
import { useRoutes } from 'react-router';

const routes = [
  {
    path: '/',
    element: <SubAppBase />,
    children: [
      // qiankun 子应用
      {
        path: '/subApp/:subAppCode/*',
        element: lazyLoad(() => import('./pages/subApp/container')),
      },
      // 其他业务页面
      // ....
    ],
  },
  {
    path: '/login',
    element: lazyLoad(() => import('./pages/login')),
  }
];

export default function Routes() {
  const elements = useRoutes(routes);
  return elements;
}

最重要的就是里边的 path: '/subApp/:subAppCode/*',因为我们刚才注册微服务时填写的 activeRule 是 /subApp/${app.appCode},所以所有的子应用都会匹配到这个路由。

并且注意这个路由最后是以 * 结尾的,这个 * 里包含的其实就是子应用所使用的路由,这部分路由对于基座应用来说是无关紧要的,所以就使用 * 来保证:无论子组件里路由怎么跳,父应用里都可以稳定匹配到容器组件。


最后,来看一下对应的子应用页面容器怎么实现的,因为这里讲的是 token 通用的情况,所以这个组件的任务会简单很多:

import React, { useEffect } from 'react';
import { useParams, useNavigate } from 'react-router';

export default function SubAppContainer() {
  const navigate = useNavigate();
  const params = useParams();

  useEffect(() => {
    const token = localStorage.getItem('token');
    navigate(`/subapp/sso?token=${token}`, { replace: true });
  }, [params.subAppCode]);

  return (
    <div>
      {/* 使用父应用的标题栏 */}
      <AppHeader />
      <div
        id="subapp-viewport"
        style={{ width: '100vw', height: '100vh - 50px' }}
      />
    </div>
  );
}

核心就是中间 useEffect 里做的事情,拿到本地 token,然后传递给子应用使用

你可能会困惑这父应用里路由跳转了一下,怎么就传递给子应用了呢。这个咱们下面改造子应用的时候再细讲,你现在可以简单的理解成:/subApp/appA/sso?token=${token}/subApp/appA/ 就是本容器组件,后面的路由部分都是直接交给子应用使用的,即子应用访问到了 /sso?token=${token}

当子应用加载完成后,会根据这个路由导航到 /sso 页面组件,这个组件会消化路由里的 token 参数完成单点登录,并跳转到路由参数 nextUrl 里(这里没传,所以可以直接跳到子应用的 home 页)。

注意 navigate 的时候需要设置 replace,不然会出现路由栏后退按钮卡住用不了的情况。(子组件消化 token 后会重定向到其他子应用页面,所以如果没有 replace 的话会后退到 /sso?token=abc 页面,然后子应用又会立马单点登录然后再次执行重定向,表现就是点完回退按钮后页面闪了一下但是没有变。

还有就是注意 jsx 里的 div#subapp-viewport,我们刚才在 registerMicroApps 注册子应用时填的 container 就是为了获取到这个 dom。

token 通用情况下的父应用开发工作到这里就结束了。用户在访问路由 /subApp/appA 时就会跳转到子应用容器,然后 qiankun 发现路由命中了 activeRule,找到页面中的 #subapp-viewport 容器并执行子应用加载。


不过这里缺少了一点,就是不同的子应用执行单点的页面可能是不同的,例如子应用 A 要导航到 /sso?token=123,而子应用 B 可能就要导航到 /login/ssoLogin?access_token=123,这种情况下就不能像上面示例代码里那样直接写死了,所以说需要找后端商量这个子应用路径怎么给到前端,一般都是在加载微服务列表的那个接口里跟其他字段一起给了。这时候就要根据路由参数里的 subAppCode 找到对应的子应用路径再跳转。

小结

token 通用情况下,前端核心工作就是如何从后端加载微服务配置项然后注册给 qiankun,注册完成之后用户跳转到子应用容器里,容器组件查找到当前的 token 然后通过路由跳转的形式传递给子应用即可。整个流程如下:

image.png

页面集成方案B - token 不通用

现在来介绍下 token 不通用的情况,这种情况在我的经历里是比较常见的,因为一方面有微服务需求的场景不大可能都是内部集群里的应用,还有可能会接入外部的第三方平台,而外部平台自然无法做到 token 直接拿过去就能用,中间必然会存在一个 token 兑换的操作。

而且目前的第三方页面集成大多都是懒加载的,只有在用户真正访问对应的第三方页面时才会去发起对应平台的单点登录。不会说你用户一登录统一门户,剩下的所有子平台都哗啦啦进行单点了。如果用户没点进去子应用的话,这些单点操作其实都是浪费掉了。

因此,为了统一流程,基本都是不管你子应用是内部还是外部的,都要求在进入子应用页面前先请求后端接口完成后端的单点工作,然后在响应里把具体的应用 url 和 token 返回回来,前端再根据响应执行下一步操作,比如加载子应用页面。

这种情况下,前端就不需要使用 registerMicroApps 先完成微服务的注册操作了。一方面,后端有可能根本就没有获取所有子应用列表的接口。

另一方面,子应用页面在实际加载前需要先调用后端接口,等接口响应了再进行加载。如果还使用 registerMicroApps 的话,在跳转到子应用容器的页面后,接口刚发出去,子应用就开始加载了,这肯定是不行的。后端没干完活,前端也没有拿到 token,跳过去子应用根本没法完成单点登录。

所以说,这种情况下需要使用 loadMicroApp 来手动控制,让接口调用可以阻塞子应用的加载工作。整个流程应该是这个样子:

image.png

功能实现

OK,讲完了流程现在开始写代码,由于不需要 registerMicroApps 了,所以刚开始获取微服务列表的基础路由组件就可以省掉了,路由树里也不需要再进行什么嵌套,只需要还按之前的方法把子应用容器绑定到路由上就行了:

{
  path: '/subapp/:subAppCode/*',
  element: lazyLoad(() => import('./pages/subApp/container')),
},

这里再强调一下中间的路由参数 :subAppCode,子应用容器里需要拿这个去请求后台完成对应子应用的单点登录,所以一定要保证这个 code 是每个子应用唯一的。

然后就是实现子应用容器了:

import React, { useEffect, useRef } from 'react';
import { useParams, useLocation, useNavigate } from 'react-router';
import { message } from 'antd';
import { loadMicroApp, MicroApp } from 'qiankun';
import { compareUrl } from '@/RPC/utils';

export default function SubAppContainer() {
  const location = useLocation();
  const navigate = useNavigate();
  const params = useParams();
  const microAppRef = useRef<MicroApp>(null);

  const loadMicroApp = async () => {
    // 调用后台接口,完成单点操作
    const resp = await fetchMicroAppUrl(params.subAppCode);
    if (resp.code !== 200 || !resp.result) {
      message.error(resp.message || '获取子应用信息失败');
      return;
    }

    // 由于子应用的路由会放在本页面的路由后面,也就是 * 参数里
    // 所以这个路径是剔除掉子应用路径的本页面路由
    const purePath = location.pathname.replace(params['*'], '');
    const { name, entry, path } = resp.result;

    const app = loadMicroApp({
      name,
      entry,
      container: '#subapp-viewport',
      props: {
        headerVisible: false,
        routerBaseName: process.env.PUBLIC_URL + purePath,
      },
    });

    microAppRef.current = app;
    navigate(purePath + path, { replace: true });
  };

  useEffect(() => {
    loadMicroApp();

    return () => {
      microAppRef.current?.unmount();
    };
  }, [params.subAppCode]);

  return (
    <div>
      <FullHeader />
      <div
        id="subapp-viewport"
        style={{ width: '100vw', height: '100vh - 50px' }}
      />
    </div>
  );
}

整体流程和之前没有太多变化,观察路由参数是否变化,变化时执行微服务加载,只不过在加载微服务之前先请求后台接口获取单点信息,这个接口应该接受路由参数里传递的 subAppCode,然后返回对应路由的信息,包括子应用名称 name,应用入口 entry,子应用路径 path,例如下面这样:

{
  name: 'user-center-web',
  entry: '//my-app.com/user-center',
  path: '/sso?token=abc'
}

注意,这里 entry 和 path 必须分开给,因为 path 是需要前端追加到路由里的,如果后台放在一起,比如 //my-app.com/user-center/sso?token=abc,前端虽然可以把这个地址当作 entry 加载到子应用前端静态资源,但是就不知道从哪开始截取路径然后塞进地址栏里了。

最后要记得 useEffect 返回函数释放资源,毕竟子应用一堆 html、css、js,不释放还是有点大的。

其实就这么多,其实相比于 registerMicroApps 注册应用,loadMicroApp 的写法更直观一点,和实例化图表几乎没有区别。

小结

token 不通用的情况下,前端不再需要在子应用加载前注册所有微服务的信息。而是在子应用容器里请求接口,并从接口的返回值里获取到要显示的子应用具体信息(名称、入口、包含 token 的子应用路径),并通过 loadMicroApp 手动加载子应用。

父应用要做的微前端修改

其实这部分不是父应用前端代码里要做的修改,而是父应用前面的 nginx 要做的修改,目的就是为了解决前端微服务的跨域问题。

我们假设父应用的访问地址是 domainA.com/app1,子应用的访问地址是 domainB.com/app2。在父子应用分开运行的情况下,前端静态资源的获取是这样的:

image.png

而我们刚才也讲过,qiankun 做的其实就是拿一个 dom 当作容器,把子应用的前端资源都塞到这个容器里。这样做的话就会导致 子应用的所有静态资源请求都是在父应用的域名下发起的,也就是这样:

image.png

可以看到,如果父子应用在不同域下,这种请求就会因跨域被拒绝,从而导致子应用加载不出来。

所以,我们需要在 父应用的 nginx 里加入子应用前端资源的反向代理,让父应用获取子应用资源是在同源下进行的:

image.png

要配置的也很简单,加一个 location 就行,如果 nginx 能本地访问子应用的话可以用对应的内网地址,这样更快一点。

location /app2/ {
    proxy_pass http://domainB.com/app2/;       
}

这个配置基本不用前端来改,但是你要知道怎么改以及为什么改,不然运维改配置的时候把你叫过去,一问三不知就尴尬了。

子应用要做的修改

讲完了父应用要干的活,现在来讲一下子应用上的修改,包括两部分:让子应用可以作为微服务集成到父应用里,以及如何消化父应用传来的参数来实现单点功能。

qiankun 微前端改造

首先就是修改 webpack 的打包配置,就是:qiankun 官方文档 里提到的这三个配置:

const packageName = require('./package.json').name;

module.exports = {
  output: {
    library: `${packageName}-[name]`,
    libraryTarget: 'umd',
    // 注意这个,webpack5 里要改成 chunkLoadingGlobal,不然会报错
    jsonpFunction: `webpackJsonp_${packageName}`,
  },
};

如果你是用 create-react-app 创建项目的话,需要先 npm run eject 弹出配置项,然后在 config\webpack.config.js 里去加。或者用 react-app-rewired 之类的包去覆写配置。

webpack 配置介绍

这里简单介绍一下这三个字段的效果,其中最核心的就是 output.library 字段。这个字段用于指定 存放你入口文件导出内容的对象名

比如你 webpack 配置了 entry 是 index.js,这个文件导出了一个方法 hello。其他的文件引入打包好的代码之后就可以通过 [output.library 的字段值].hello() 访问到你导出的内容。

第二个配置是 libraryTarget: umd, umd 格式提供了高度兼容的导出形式,这里不做过多介绍,可以自己百度了解下或者看 webpack 文档介绍。在微前端场景下,主要是使用 umd 导出的内容可以在 window 上访问的能力。

项目配置了 libraryTarget: umd 的话,当 html 引入打包好的 js 时,entry 导出的方法就会直接存放在 window[output.library] 上。

比如我们上面的配置 ${packageName}-[name],packageName 会去看你 package.json 里配置的 name,(假设是 subapp1),而 [name] 是占位符,和当前 webpack 的 entry 配置有关,可以在 Entry and Context | webpack 里看到,如果只传入了一个字符串 entry 的话,这个 [name] 的值就是 main。所以最终就可以通过 window['subapp1-main'] 访问到我们这个项目 index.js 里导出的东西。

jsonpFunction / chunkLoadingGlobal 主要用于存放 webpack 加载 chunk 时的一些信息,这里设置唯一的名字防止和页面集成后其他 webpack 应用冲突。

qiankun 如何获取子应用生命周期函数

通过上面这些配置,我们项目 index.js 里导出的那些函数都可以在子应用的 window 上访问到的,例如 qiankun 的示例子应用:

image.png

而 qiankun 使用 import-html-entry 加载子应用的前端静态资源,在子应用加载的时候,会使用 import-html-entry 提供的 execScripts 执行子应用代码:

image.png

我们可以在 import-html-entry 的文档上找到,这个方法执行后会返回被执行代码对全局对象进行的最新修改:

image.png

而 webpack 会把子应用入口的导出注入到 window 上,这么一来,execScripts 的返回值就恰好是我们入口文件里导出的那几个回调方法。qiankun 就是通过这种方式拿到了子应用的生命周期回调。

整个流程还是很有意思的,有兴趣的同学可以研究下源码,这里指个路,就不再深入讨论了:

扯远了,咱们说回来,上面做的 webpack 配置其实就是为了父应用可以访问到子应用暴露的微前端生命周期。除此之外还有两个基本问题需要解决:应用内应用相对路径引用子应用路由配置

子应用相对路径引用

先说第一个:相对路径引用。上面提到,子应用在页面集成是实际上是作为父应用的一个 dom 来运行的,所以请求的域名也会是父应用的域名,因此当子应用里有 相对路径请求的资源 时,请求就会自动拼接上父应用的域名。这就导致资源请求出问题了。

不过由于我们是用的 webpack,所以这个问题很好解决:直接在子应用 src 下新建个 public-path.js 文件,然后填上下面代码:

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

最后在 index.js 的最上面应用这个文件即可,注意一定要是 最上面

import './public-path';

qiankun 在加载子应用后,会往子应用 js 沙箱的全局变量上注入这个 __INJECTED_PUBLIC_PATH_BY_QIANKUN__ 其值就是你在 registerMicroApps 或者 loadMicroApp 时传递的 entry。

这个解决办法在 qiankun 文档的 常见问题 - qiankun (umijs.org) 提到了。这个文档很实用,最好先看一遍。

子应用路由配置

然后是子应用的路由应该怎么配置,这个算是理解上的小难点,不过操作起来其实很简单,核心是要理解为什么这样做,咱们一步步地来讲一下:

首先要知道 子应用和父应用访问的是同一个浏览器地址栏。你在父子应用的 index.js 里都塞一个 console.log(location),就可以发现他们打印的是同样的内容:

image.png

那怎么区分是父应用在进行路由导航还是子应用在导航呢?答案是 通过路由前缀react-router 可以通过给 Router 组件添加 basename 属性的方式来设置路由前缀。之后在应用内的所有路由访问 / 跳转都会剔除掉这个统一前缀。

所以说,我们可以给子应用配置一个范围更小的路由前缀,来限制子应用无论怎么导航都是在父应用的容器路由内的。假设完整的路径是 domain1/appA/subApp/appB/sso?token=abc,那么我们就可以按下面的方式进行配置前缀:

image.png

父应用的 basename 一般都是通过 process.env.PUBLIC_URL 配置的,也就是上面的 /appA/(绿圈部分)。而子应用的 basename 则需要父应用提供(红圈部分),再通过 qiankun 的 props 传过来。

可以发现,这么配置之后,子应用内部获取到的路由实际上只有 /sso?token=abc,并且无论怎么导航,其所在的完整路由都是位于 /appA/subApp/appB/ 之下的,而这个路径就是我们之前父应用里配置的子应用容器所在的页面组件,即路由 /subApp/:subAppCode。这就实现了我们刚才的需求。

现在我们回头看看,父应用在注册子应用时传递的路由前缀 props.routeBasename,当使用 registerMicroApps 时,通过 PUBLIC_URL 和子应用的 appCode 拼接出子应用里的路由前缀:

const microAppConfig = subAppList.map(app => ({
  // ...
  props: {
    // 看这里,子应用的路由前缀
    routeBasename: `${process.env.PUBLIC_URL}/subApp/${app.appCode}/`
  },
}));

而使用 loadMicroApp 时,则是直接截取当前所在的组件路由,然后传递给子应用。

const location = useLocation();
const params = useParams();
  
// 看这里
const purePath = location.pathname.replace(params['*'], '');

const app = loadMicroApp({
  // ...
  props: {
    // ...
    routerBaseName: process.env.PUBLIC_URL + purePath,
  },
});

注意这里是使用的 react-router 的 useLocation,所以需要再拼接上 PUBLIC_URL,如果你是使用的 window.location 的话,这里就不需要额外拼接了。

这两种方案都是可以的,只要能构造出子应用需要的路由前缀就行。所以,父应用路由前缀、父应用容器路由、父应用当前路由、子应用路由前缀,子应用当前路由,这几个状态之间的关系需要认真理解。理解了这个,就知道上面父应用里为什么 navigate 一下就能把 token 传递给子应用了:

// 由于子应用的路由会放在本页面的路由后面,也就是 * 参数里
// 所以这个路径是剔除掉子应用路径的本页面路由
const purePath = location.pathname.replace(params['*'], '');
const { name, entry, path } = resp.result;

const app = loadMicroApp({ /** ... */ });

// 把 token(附加在 path 里)传递给子应用
navigate(purePath + path, { replace: true });

子应用微服务改造

解决了上面两个问题,子应用的微前端生命周期回调怎么写就一目了然了:

import './public-path';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { BrowserRouter as Router } from 'react-router-dom';
import { store } from './store/store'
import { setHeaderVisible } from './store/global'

let appRoot = null;

function render(props) {
    const { container, headerVisible, routerBaseName } = props;
    // 把是否显示标题栏放在 redux 里
    store.dispatch(setHeaderVisible(headerVisible))

    const root = ReactDOM.createRoot((container || document).querySelector('#root'));
    root.render(
        <Router basename={window.__POWERED_BY_QIANKUN__ ? routerBaseName : process.env.PUBLIC_URL}>
            <App />
        </Router>
    );

    appRoot = root;
}

if (!window.__POWERED_BY_QIANKUN__) {
    render({});
}

export async function mount(props) {
    render(props);
}

export async function unmount(props) {
    appRoot && appRoot.unmount();
}

核心在 render 方法里,qiankun 会把父应用里相关的东西放在 render 函数的 props 里传过来。比如应用的根容器(注册微前端时提供的 container dom),这里就判断一下,如果是正在被集成的情况(__POWERED_BY_QIANKUN__ === true),就使用 qiankun 提供的容器,不然还是用自己 html 里的根容器。

其他的就是把是否显示标题栏放在 redux 里,这样对应的标题栏组件就可以拿到这个状态然后对 UI 进行控制。而 Router 也检查下 __POWERED_BY_QIANKUN__,如果是的话就使用父应用提供的路由前缀,否则就使用默认项目配置。

最后就是不要忘了在 unmount 方法里卸载掉 react。

子应用实现单点功能

子应用改造完,可以集成到父应用页面里之后,就可以来实现单点功能了。

其实这个很简单,就是新建一个页面组件,绑定到我们上面多次提到的 /sso 路由上,然后页面在初始化的时候去读地址栏里的 token 参数,请求后端的一个接口,拿到用户信息后设置到本地就可以了,具体怎么设置基本和 login 页面是一样的,照抄就行。

具体的 sso 页面组件代码如下:

import React, { useEffect, useState } from "react";
import { Result, Spin } from "antd";
import { useNavigate, useSearchParams } from "react-router-dom";

const Sso: React.FC = () => {
  const [searchParams] = useSearchParams();
  const navigate = useNavigate();
  const [errorMessage, setErrorMessage] = useState('');

  const loadUserInfo = async (token: string) => {
    const res = await postSsoLogin(token);

    if (res.code !== 200) {
      const msg = res.message || '获取用户信息失败';
      setErrorMessage(`${msg},单点登录失败`);
      return;
    }

    // 把登录信息设置到本地

    const nextUrl = searchParams.get('next_url');
    if (nextUrl) navigate(decodeURIComponent(nextUrl), { replace: true });
    else navigate('/', { replace: true });
  }

  useEffect(() => {
    const token = searchParams.get('token');

    if (!token) {
      setErrorMessage(`缺少参数:token`);
      return;
    }

    loadUserInfo(token);
  }, []);

  if (errorMessage) {
    return (
      <Result
        status="warning"
        title={errorMessage}
      />
    )
  }

  return (
    <Spin tip="登录中,请稍等...">
      <div style={{ width: '100vw', height: '100vh' }} />
    </Spin>
  );
};

export default Sso;

注意登录完成后要记得读一下地址栏里有没有 nextUrl,有就跳过去,没有就跳首页。因为单点完成后重定向到指定页面的需求还挺常见的。并且导航时也要设置 replace: true,这个单点跳转的页面一般都不会放在路由栈里。

总结

至此,我们就完成了从父应用到子应用,包括微服务改造和单点流程实现的所有功能了。

其实比较复杂的还是父子应用是如何对路由进行管理的,这个问题涉及到了很多代码,东改一点西改一点,每个地方涉及到的不多但是需要每个地方都能配合起来,如果不理解的话,很容易出现中间某个环节漏掉然后跑不起来。

至于单点功能方面基本都是简单的前端功能实现,核心在于和后端沟通具体的方案细节,了解了单点是什么个流程,都有那些接口之后接入还是挺简单的。

其他的就是微前端的集成工作,这个还是要多看 qiankun 的官方文档,特别是这个 常见问题 - qiankun (umijs.org)。因为 qiankun 本身提供的 api 就不多,花点时间都看一遍基本就能解决绝大多数问题了。实在解决不了就去 Issues · umijs/qiankun (github.com) 找找。