当企微侧边栏遇上微前端

本文涉及的产品
函数计算FC,每月15万CU 3个月
简介: 同样地,为了不浪费大家的时候,如果你不知道 企微侧边栏 是什么,这篇文章可以关掉了。如果你对 微前端 有强烈的兴趣,也可以尝试了解一下我是怎么在这个应用场景下实践微前端的,希望这篇文章可以对你有所启发。

image.png


前言


同样地,为了不浪费大家的时候,如果你不知道 企微侧边栏 是什么,这篇文章可以关掉了。


如果你对 微前端 有强烈的兴趣,也可以尝试了解一下我是怎么在这个应用场景下实践微前端的,希望这篇文章可以对你有所启发。


“微前端 + 企微侧边栏” 的开发模板代码已上传至 Github,点击 wecom-sidebar-qiankun-tpl 即可看到,需要的直接白嫖 + Star。

先给大家看下项目的效果:

image.png


整个项目的框架是这样的:

image.png

是不是看着很 Low?就要是做一个最 Low 的出来,这样你才有自己发挥的空间。


企微侧边栏


按国际惯例,简单地过一下企微侧边栏是什么,诺,就是下面里红框的东西:

image.png

侧边栏本质上就是一个 H5 页面,需要在企业微信后台里配置对应的 nameurl 即可:

image.png



如果你了解过微前端,那么一定对这种 “通过配置 nameurl 来展示不同页面” 的开发模式不陌生。因为无论哪个微前端框架,它们注册微应用的方式和配置侧边栏应用简直一模一样:

import { registerMicroApps } from 'qiankun';
registerMicroApps(
  [
    {
      name: 'app1',
      entry: '//localhost:8080',
      container: '#container',
      activeRule: '/react',
      props: {
        name: 'kuitos',
      },
    },
  ],
  {
    beforeLoad: (app) => console.log('before load', app.name),
    beforeMount: [(app) => console.log('before mount', app.name)],
  },
);
复制代码


为什么要用微前端


相似不代表非要上微前端。只不过,在管理多个应用时,会出现下面的问题:


  • 所有侧栏应用为硬隔离。切换不同应用都要重新加载
  • 基础信息不共享。重新加载又需要重新初始化 JS-SDK 和获取群聊、私聊、用户身份的信息,而这些信息对于每个应用都是必需的,不应该每次都重新获取
  • 方便多团队协作。应对已有 H5 嵌入到侧边栏的场景

image.png


微前端思路


刚刚提到通过注册多个微应用实现 “注册多个侧边栏应用” 的方式就是一个很好的管理方法。


除此之外,我还希望有如下功能:


  • 微应用可以从主应用获取一些公共信息,比如 userId 之类
  • 微应用同时可以获取主应用的 jsSdk 对象,直接使用 jsSdk 与企业微信交互
  • 主应用会自动完成 用户身份验证JS-SDK 的初始化,微应用不再需要做公共逻辑,自动拥有业务所需数据
  • 主应用除了像 Router 那样自动注册微应用,还能在指定 container 里手动注册微应用

image.png

qiankun 这个微前端框架非常完美地解决上面的问题。


主应用 - 初始化


image.png

从刚刚的分析可以看出来主应用需要完成两个事情:


  • 执行公共逻辑:获取用户身份、将 JS-SDK 初始化
  • 获取公共数据:userId, context, chat 等需要共享的侧栏公共数据和业务数据


而在我之前写的 wecom-sidebar-react-tpl React 侧边栏开发模板里已经实现了大部分内容,所以这里直接用现成的公共逻辑就完事了。

这个项目主要添加了主应用的配置:

import {initGlobalState, MicroAppStateActions, registerMicroApps, start} from "qiankun";
import {JsSDK} from "../jsSdk";
export const subAppContainerId = 'sub-app-container';
export const subAppContainer = `#${subAppContainerId}`;
// 初始化 state
export const microAppStateActions: MicroAppStateActions = initGlobalState({});
// 获取需要传递给微应用的 props
const initPassProps = async (jsSdk: JsSDK) => {
  const res = await jsSdk.invoke<{ chatId: string }>('getCurExternalChat');
  return {
    jsSdk,
    isChat: !!res
  }
}
// 启动 qiankun 的主应用
const initQiankunMainApp = async (jsSdk: JsSDK) => {
  const passProps = await initPassProps(jsSdk);
  // 添加 state 变更监听
  microAppStateActions.onGlobalStateChange((state, prev) => {
    console.log('[主应用]', state, prev);
  });
  // 注册并启动微前端
  registerMicroApps([
    {
      name: 'react-app',
      entry: '//localhost:3001',
      container: subAppContainer,
      activeRule: '/#/react-app',
      props: passProps
    },
    {
      name: 'sidebar-app',
      entry: '//localhost:3002',
      container: subAppContainer,
      activeRule: '/#/sidebar-app',
      props: passProps
    }
  ]);
  // 启动主应用
  start();
}
// 初始化主应用内容
export default initQiankunMainApp;
复制代码

initQiankunMainApp 函数需要传入 jsSdk,然后通过 jsSdk 与企业微信交互,获取私聊、群聊的内容,再通过 props 的方式传给微前端。当微应用在 mount 的时候,就可以拿到初始的公共数据了。


如果数据有变动的话,比如调用了 login,可以通过下面代码来更新 globalState:

function login() {
    ... 登录逻辑
    microAppStateActions.setGlobalState({
      msg: '新内容'
    })
}
复制代码

更新了之后,微应用也需要添加 onGlobalStateChange 来监听数据变化。 不过,因为只是获取公共数据,所以一般来说 globalState 的变化不会特别频繁。

后面的注册微应用就比较简单了,配置一下 nameentry 就差不多了。这里需要注意的是 activeRule 我写的是 /#/xxx-app,这是因为我在主应用用了 Hash Router,路由部分等会再说。


因为需要在处理完公共逻辑再注册微应用,所以在入口文件 index.tsx 中要这么写:

import { ConfigProvider} from 'antd';
// 由于 antd 组件的默认文案是英文,所以需要修改为中文
import zhCN from 'antd/lib/locale/zh_CN';
import App from './App'
import {fetchUserId, fetchSignatures} from './api'
import config from './_config'
import {invokeResMock, mockUserId, wxResMock} from "./mock";
import {checkRedirect, createJsSdk, initSdk} from "./lib";
import 'antd/dist/antd.css';
import initQiankunMainApp from "./lib/utils/initQiankunMainApp";
export const jsSdk = createJsSdk(wxResMock, invokeResMock);
const AppWrapper = (
  <ConfigProvider locale={zhCN}>
    <App />
  </ConfigProvider>
)
checkRedirect(config, fetchUserId, mockUserId) // 重定向获取 code(用户身份)
  .then(() => initSdk(config, fetchSignatures)) // 初始化 JsSdk
  .then(() => initQiankunMainApp(jsSdk)) // 初始化主应用,并注册好微应用
  .then(() => ReactDOM.render(AppWrapper, document.getElementById('root'))) // 渲染主应用内容
复制代码


主应用 - 路由


image.png

如果只是 主-微 这样的架构还是比较简单的,但是我希望主应用也能作为一个侧栏应用去使用,它也可以拥有自己的样式、一些简单的功能,所以 我觉得在主应用拥有自己的路由系统是一个合理的需求。


这里我使用了 Hash Router,这是因为如果用 history 模式的 Browser Router,每次切换路由都要初始化 JS-SDK,太麻烦了,具体参见 文档这里的步骤二


如果非要用 history 模式,也可以在路由切换的回调里初始化,不过我总感觉可能会出一些奇怪的 Bug


我把之前 wecom-sidebar-react-tpl 项目的所有功能都放在首页上了,所以这里的路由仅有一个首页:

const RouterConfig: FC = () => {
  return (
    <Switch>
      <Route exact path="/">
        <Home/>
      </Route>
    </Switch>
  )
}
export default RouterConfig;
复制代码


image.png

从上图可以看到通过 Ant Design 的 <Menu/> 组件划分了 3 个 Tab,其中第一个 首页 就是主应用里的 <Home/> 组件,仅是个普通 React 组件,而剩下的 sidebar-appreact-app 才是后面要讲的微应用。


微应用 - 初始化


image.png

这两个微应用我都使用了 create-react-app 来创建,然后按照 qiankun 官方文档的“项目实践”章节 来配置微应用。本来不想再讲一遍的,但是在配置过程也发现一些问题,就展开讲讲吧。


第一步 - publicPath

/src 下新增 public-path.ts 文件:

const updatePublicPath = () => {
  if (window.__POWERED_BY_QIANKUN__) {
    // @ts-ignore
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  }
}
updatePublicPath();
export default {}
复制代码


不写成官网的格式是因为 CRA 的 TS 模板会报 tslint 的错误:

TS1208: 'public-path.ts' cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an empty 'export {}' statement to make it a module.
复制代码


报错的意思是一个文件必须要 export 一个东西才可以,所以只能写成上面这样。

然后在入口文件的 index.tsx 里的 第一行 引入并执行它:

import './public-path';
复制代码


一定要在 第一行 引入它,因为它直接决定了你 静态资源publicPath。比如,你在组件里使用了图片资源:

import logo from './logo.svg';
function App(props: Props) {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
      </header>
    </div>
  );
}
复制代码

如果没有执行这个 publicPath,或者单独运行 react-app 的话,这里的 logo 路径会变成 /static/media/logo.6ce24c58.svg


而我们主应用的 URL 是 localhost:3000,微应用的 URL 是 localhost:3001。所以当内嵌到主应用时,图片 URL 就变成了 localhost:3000/static/media/logo.6ce24c58.svg,但是主应用没有这个 SVG 呀,然后资源就会报 404 报错了。


这里的 public-path.ts 就是在希望在 Webpack 打包的时候,把前面的 localhost:3001 定死,访问资源时就会去微应用那找了。


第二步 - basename

在 Router 里添加 basename 属性。

import {HashRouter} from "react-router-dom";
function App() {
  return (
    <HashRouter basename={window.__POWERED_BY_QIANKUN__ ? '/sidebar-app' : '/'}>
      <div className={styles.app}>
        <VerticalMenu />
        <RouterConfig />
      </div>
    </HashRouter>
  );
}
复制代码

注意:主应用和微应用都使用 Router 时,Router 类型(history 模式/hash 模式)必须是一样,不然会有很多问题。


注意:当我在写主应用的时候 React Router 已经来到了 v6.x 的版本,而主应用用的依然是 v5.x,所以,我觉得这也是微前端框架的一个优势吧,可以磨平主、微应用的技术栈。


添加了 basename 之后,就可以直接写 path 了,不用写成 /sidebar-app/home 这种带有前缀的写法了:

export const routes = [
  {url: '/', label: '首页', page: Home},
  {url: '/external-user', label: '私聊', page: ExternalUser},
  {url: '/external-chat', label: '群聊', page: ExternalChat},
  {url: '/action', label: '操作', page: Action},
]
const RouterConfig: FC = () => {
  return (
    <Routes>
      {routes.map(route => (
        <Route key={route.url} path={route.url} element={<route.page/>}/>
      ))}
    </Routes>
  )
}
复制代码


路由跳转也可以直接写 path,下面的 routes 用的是上面的 routes 数组:

const VerticalMenu: FC = () => {
  const location = useLocation();
  return (
    <Menu
      style={menuStyle}
      className={styles.menu}
      defaultSelectedKeys={[location.pathname]}
      mode="vertical"
    >
      {routes.map(route => (
        <Menu.Item className={styles.item} key={route.url}>
          <Link to={route.url}>{route.label}</Link>
        </Menu.Item>
      ))}
    </Menu>
  )
}
复制代码


第三步 - 暴露生命周期

在微应用的入口 index.tsxexport 生命周期回调函数:

import './public-path'
import {ConfigProvider} from "antd";
import React from 'react';
import {Provider} from "react-redux";
import ReactDOM from 'react-dom';
import App from './App';
// 由于 antd 组件的默认文案是英文,所以需要修改为中文
import zhCN from 'antd/lib/locale/zh_CN';
import 'antd/dist/antd.css';
import store from "./store";
const AppWrapper = (
  <ConfigProvider locale={zhCN}>
    <Provider store={store}>
      <App />
    </Provider>
  </ConfigProvider>
);
// 渲染应用
const render = (props: any) => {
  const { container } = props;
  const containerElement = container ? container.querySelector('#root') : document.querySelector('#root');
  ReactDOM.render(AppWrapper, containerElement);
}
// 到处 qiankun 需要的生命周期钩子
export const bootstrap = async () => {
  console.log('[微应用 sidebar-app] bootstrap');
}
export const mount = async (props: any) => {
  props.onGlobalStateChange((state: any) => {
    // 将 jsSdk 更新到 store 中
    store.dispatch({ type: 'SET_JSSDK', payload: state.jsSdk })
  });
  // 更新 jsSdk
  store.dispatch({ type: 'SET_JSSDK', payload: props.jsSdk })
  console.log('[微应用 sidebar-app] mount', props);
  render(props);
}
export const unmount = async (props: any) => {
  console.log('[微应用 sidebar-app] unmount', props);
  const { container } = props;
  ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}
if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}
复制代码

mount 回调里,我们可以接收上面提到主应用传来的 props,在这个 props 里提取 isChatjsSdk 两个数据,并将其设置到 redux store 中,作为整个微应用的全局状态。


相信是个人都会用 redux 了,那关于 mapStateToPropsmapDispatchToPropsuseDispatchuseSelector 这些就不展开说了,拿到 jsSdk 可以像主应用那样去调用 API 就可以了。


又或者你不想用 redux,每次状态变更后都重新渲染一次应用也是可以的,这个我在 react-app 里实现了:

import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
// 渲染函数
const render = (props: any) => {
  const { container, user } = props;
  ReactDOM.render(<App user={user} />, container ? container.querySelector('#root') : document.querySelector('#root'));
}
// qiankun 微应用的导出函数
export const bootstrap = async () => {
  console.log('[微应用 react-app] bootstrap');
}
export const mount = async (props: any) => {
  // 每次变更状态,都重新 render 一次,然后把更新的状态和 props 都传给 App 组件
  props.onGlobalStateChange((state: any) => {
    console.log('[微应用 react-app] onGlobalStateChange', state);
    render({
      ...props,
      ...state,
    })
  });
  console.log('[微应用 react-app] mount', props);
  render(props);
}
export const unmount = async (props: any) => {
  console.log('[微应用 react-app] unmount', props);
  const { container } = props;
  ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}
// @ts-ignore
if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}
复制代码


第四步 - 修改 Webpack 配置

这里根据官网做的就好了,先装个 rescripts/cli,把 CRA 那个垃圾脚手架给换成 rescripts

npm i -D @rescripts/cli
复制代码


根目录新增 .rescriptsrc.js

const { name } = require('./package');
module.exports = {
  webpack: (config) => {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    config.output.globalObject = 'window';
    return config;
  },
  devServer: (_) => {
    const config = _;
    config.headers = {
      'Access-Control-Allow-Origin': '*',
    };
    config.historyApiFallback = true;
    config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;
    return config;
  },
};
复制代码


最后修改 package.json

-   "start": "react-scripts start",
+   "start": "rescripts start",
-   "build": "react-scripts build",
+   "build": "rescripts build",
-   "test": "react-scripts test",
+   "test": "rescripts test",
-   "eject": "react-scripts eject"
复制代码

整个微应用的配置就做好了。


主应用手动加载微应用



image.png


除了像注册路由一样去注册微应用,也可以调用 qiankun 的 loadMicroApp 来在指定的 container 元素下加载这个微应用。


比如,我就在主应用的 “首页” 里手动加载 react-app,并在加载时传入 user 用户身份对象:

const Home: FC = () => {
  ...
  return (
    <Spin spinning={loading}>
      <div>
        {/* 手动加载微应用 */}
        <MicroAppComponent user={user}/>
      </div>
    </Spin>
  )
}
复制代码


MicroAppComponent 代码:

import {FC, useEffect, useRef} from 'react';
import {loadMicroApp, MicroApp} from "qiankun";
let microAppComponent: MicroApp;
interface Props {
  user?: UserResponse
}
const MicroAppComponent: FC<Props> = (props) => {
  const containerRef = useRef<HTMLDivElement>(null);
  // 当应用更新时,将 props 传递
  useEffect(() => {
    if (microAppComponent && microAppComponent.update) {
      microAppComponent.update(props).then()
    }
  })
  useEffect(() => {
    // 初始化微应用
    if (containerRef.current) {
      microAppComponent = loadMicroApp({
        name: 'react-app-nested',
        entry: '//localhost:3001',
        container: containerRef.current,
        props,
      });
    }
    // 将微应用 unmount
    return () => {
      microAppComponent.unmount().then();
    }
  }, [props])
  return (
    <div>
      <h2>微应用</h2>
      <div ref={containerRef}/>
    </div>
  )
}
export default MicroAppComponent;
复制代码


总结


好了,我们来做一下总结吧,毕竟这个项目也花了一点时间来做的。


  • 主应用基于 wecom-sidebar-react-tpl 来开发,前置的配置、Mock、初始化逻辑均继承该项目
  • 主应用使用 loadMicroApp 手动加载一个微应用
  • 主应用在 registerMicroApps 注册微应用(侧边栏应用),并在 props 传入共享数据和 JsSdk
  • 微应用在暴露的生命周期里的 mount 的参数 props 中获取主应用传递的数据
  • 微应用拿到主应用数据后,可以选择放到 redux 的 store 去管理,也可以在 onGlobalStateChange 回调中重新 render 整个应用,随你选哪种
  • 主、微应用都可以有各自的路由,但是路由类型必须一致,不然会有大惊喜!微应用需要在 Router 处添加 basename,去掉写前缀的写法


最后的我自己的建议是:主应用应该拥有自己的样式、欢迎页、首页、路由,或者编写自己部门的侧边栏应用,然后使用 qiankun 留出一个入口,用于承载别的部门的侧边栏就用。




相关实践学习
【AI破次元壁合照】少年白马醉春风,函数计算一键部署AI绘画平台
本次实验基于阿里云函数计算产品能力开发AI绘画平台,可让您实现“破次元壁”与角色合照,为角色换背景效果,用AI绘图技术绘出属于自己的少年江湖。
从 0 入门函数计算
在函数计算的架构中,开发者只需要编写业务代码,并监控业务运行情况就可以了。这将开发者从繁重的运维工作中解放出来,将精力投入到更有意义的开发任务上。
相关文章
|
消息中间件 运维 Java
【消息中间件】在Docker下安装的RocketMQ,使用mqadmin命令报错解决[10015:signature-failed]
前在Docker下安装了RocketMQ以及他的管理平台,但是管理平台在运维的时候只能满足我们部分的需求,所以为了在集群运维时能够的得心应手,研究了下mqadmin的使用,没想到一开始就遇到了报错,在这里和大家分享下解决的方法。
1678 100
|
存储 SQL 缓存
【MySQL系列】- 常用MySQL存储引擎
【MySQL系列】- 常用MySQL存储引擎
1260 0
|
9月前
|
人工智能 编解码 自然语言处理
AGUVIS:指导模型实现 GUI 自动化训练框架,结合视觉-语言模型进行训练,实现跨平台自主 GUI 交互
AGUVIS 是香港大学与 Salesforce 联合推出的纯视觉 GUI 自动化框架,能够在多种平台上实现自主 GUI 交互,结合显式规划和推理,提升复杂数字环境中的导航和交互能力。
352 8
AGUVIS:指导模型实现 GUI 自动化训练框架,结合视觉-语言模型进行训练,实现跨平台自主 GUI 交互
|
7月前
|
供应链 搜索推荐 API
一文读懂:京东、淘宝、拼多多 API 接口,功能大不同,场景各有招
本文深入解析京东、淘宝、拼多多三大电商巨头的API接口特点与差异。京东API以商品管理、订单处理和物流跟踪见长,适合电商平台及零售商;淘宝API功能丰富,涵盖商品查询、订单管理与用户认证,适用多场景电商业务;拼多多API聚焦社交电商,提供拼团、砍价等特色玩法,助力商家提升曝光与销量。开发者和商家需根据自身需求选择合适的API接口,并关注其安全性与稳定性。随着技术进步,这些API将持续优化,推动电商行业蓬勃发展。
|
开发框架 前端开发 JavaScript
在Vue3+TypeScript 前端项目中使用事件总线Mitt
在Vue3+TypeScript 前端项目中使用事件总线Mitt
|
网络协议 定位技术 网络安全
【技术分享】网络介绍
【1月更文挑战第3天】Network Study系列
484 1
|
JavaScript
vue 在线聊天实战范例(含选择发送表情、图片、视频、音频,自定义右键快捷菜单,一键复制,左右聊天气泡)
vue 在线聊天实战范例(含选择发送表情、图片、视频、音频,自定义右键快捷菜单,一键复制,左右聊天气泡)
569 0
|
前端开发
canvas文字绘制(大小、粗体、倾斜、对齐、基线)
canvas文字绘制(大小、粗体、倾斜、对齐、基线)
696 0
|
Java API 对象存储
对象存储OSS产品常见问题之使用Spring Cloud Alibaba情况下文档添加水印如何解决
对象存储OSS是基于互联网的数据存储服务模式,让用户可以安全、可靠地存储大量非结构化数据,如图片、音频、视频、文档等任意类型文件,并通过简单的基于HTTP/HTTPS协议的RESTful API接口进行访问和管理。本帖梳理了用户在实际使用中可能遇到的各种常见问题,涵盖了基础操作、性能优化、安全设置、费用管理、数据备份与恢复、跨区域同步、API接口调用等多个方面。
219 2
|
缓存 JavaScript 小程序
uniapp 普通页面跳转到tabBar页面详解(附加代码+uni.switchTab使用)
uniapp 普通页面跳转到tabBar页面详解(附加代码+uni.switchTab使用)
1179 1