模块化可组装是云巧组件的核心理念之一。接下来以某门户后台管理系统的前端为例(由N个模块组装而成),介绍一种前端模块化、可组装的开发实践。
实现的效果:
- 模块A(独立的后端服务、数据自包含、业务逻辑独立完整)前端独立运行:
(应用完整,包含所有功能)
- 模块A集成在某门户后台管理系统中:
(仅集成内容区,导航、路由由主应用控制)
三个关键点:
- 页面如何同时满足独立部署及以区块方式被集成?
- 跨应用如何统一账号信息?
- 如何实现布局等控件的自动适应?
具体实现:
- 利用微前端框架qiankun实现页面的跨应用集成。
- 子应用中:在 umi 框架内基于
@umijs/plugin-qiankun
插件可一键开启微前端模式,并可自动适应作为子应用集成/独立部署两种情况。(其他脚手架可参考:qiankun-快速上手)
import { defineConfig } from'@ali-dayu/koi'; exportdefaultdefineConfig({ // ...qiankun: { // 作为子应用slave: {}, }, // ...});
b. 主应用中:(以下以 umi3 框架配置为例,其他脚手架可参考:qiankun-快速上手)
首先,在配置文件中开启微前端模式。
import { defineConfig } from'@ali-dayu/koi'; exportdefaultdefineConfig({ // ...qiankun: { // 作为主应用master: {}, }, // ...});
然后,在app.ts运行时配置微应用和加载地址。
exportconstqiankun= { apps: [ { name: 'app1', entry: 'https://xxx.xxx.xxx', }, { name: 'app2', entry: 'https://xxx.xxx.xxx', }, ], };
最后,在需要加载该模块子应用的位置提供容器,配置加载子应用。
importReactfrom'react'; import { MicroAppWithMemoHistory, useLocation, history } from'umi'; import { PageHeader } from'@/layout/components/PageHeader'; exportdefault () => { const { query } =useLocation() asany; return ( <divclassName={styles.settings}><PageHeadertitle={`${query?.name}`}/><MicroAppWithMemoHistoryname="app1"url="/xxx"autoSetLoading/></div> ); };
- 基于统一的单点登录认证体系实现跨应用免登与账号信息同步。(以下以 umi3 框架配置为例,其他脚手架可参考相关文档)
后端使用统一的登录鉴权SDK,基于分布式session或统一网关实现跨后端应用的单点登录、统一鉴权。而前端仅需要封装请求库,增加一个拦截器逻辑,识别到需要重新登录的code
时跳转单点登录页面,所有请求使用该请求库即可。
importumiRequestfrom'umi-request'; umiRequest.interceptors.response.use(async (response, options) => { constdata=awaitresponse.clone().json(); const { code, extInfo } =data; // 未登录/过期,需要重新登录if (code==='ConsoleNeedLogin') { // 单点登录逻辑// .... } returnresponse; }); export { umiRequestasdefault }; export*from'umi-request';
- 通过构建时环境变量、运行时window变量等配置控制布局的自动适应。
- 注入构建时环境变量
// 读取编译时环境变量并注入// 以 umi 脚手架为例,其他构建脚手架参考相关文档const { XXX_NEED_LAYOUT } =process.env; exportdefaultdefineConfig({ // ...define: { XXX_NEED_LAYOUT, }, // ...}
b. 解析变量,优先级策略是,显式配置 > 根据环境自动判断,运行时配置 > 构建时配置
exportconstxxxNeedLayout: boolean= (() => { if (window.XXX_NEED_LAYOUT!==undefined) { // 运行时window变量,优先级最高returnwindow.XXX_NEED_LAYOUT; } elseif (XXX_NEED_LAYOUT!==undefined) { // 编译时注入的环境变量,优先级次之returnXXX_NEED_LAYOUT==='true'; } else { // 无显式指定是否需要布局,判断当前应用是否为qiankun子应用来决定是否隐藏布局return!window.__POWERED_BY_QIANKUN__; } })();
c. 在模块A的布局实现中读取配置,并根据此变量实现布局的自适应。
// ...import { needLayout } from'@/utils/runtimeEnvVars'; // ...constPage: React.FC<any>= (props: any) => { return ( <LayoutclassName={styles.layouts}> {needLayout&& ( <Sider><Menu// .../></Sider> )} <Content>{children}</Content></Layout> ); };