飞马-中后台微前端页面搭建平台
作者:我大概是只,成功的猫
引子:大家好,看到读到即是缘分。今天来给大家介绍一下团队做的中后台微前端页面搭建平台--“飞马” Peagasus,及技术实现思路。平台当前还没有对外访问的方式,在不久后会跟大家见面。欢迎联系我们相互探讨,或者加入我们“造马”的队伍。
大家使用飞马都做了什么
数据页面
数据页面
中后台系统页面
中后台系统页面
目录
- 适用群体
- 适用场景
- 使用流程
- 产品背景
- 平台定位
- 平台现状
- 平台架构
- 搭建平台初览
- 业务容器
- 协议
- 组件库注册扫描机制
- 数据通信
- 代码生成引擎
- 云端后编译 && 回滚到任意历史位置
- 通过 SDK 的方式接入存量中后台系统
- 飞马页面搭建系统 && Midway 站点搭建系统
- 总结和展望
适用群体
最终面向的用户群体决定了,整个平台既会提供 No Code 方式覆盖高频、UEUI 可统一化的需求场景,也会提供 Low Code 的方式覆盖无法 UIUE 统一的场景局部细节。
适用场景
关于为何要对场景进行分类,我会在下文搭建体系设计中详细阐述初衷。场景分类给我们带来的收益是用户可以更快速入手,进行具体场景下的页面搭建。
使用流程
产品背景
平台定位
- 思考
做一个搭建平台,要考虑的核心问题之一便是如何用户的最终感知,换句话说便是“组件搭建粒度”。
业界的 iceluna 产品,是一个很典型的“细粒度”组件搭建平台,可以用这样抽象的语言来描述这款搭建平台,尝试把程序细节的语义或者逻辑用可视化的方式表达用以构建场景页面。
我们自己的定位如何?我们尝试不把程序化细节的语义和逻辑暴露出来,减少用户编程体验感知。做到这一点,自然要牺牲细粒度的灵活度。不过还好,对于中后台产品来说,标准化 UEUI,标准化场景组件,标准化场景页面应该是大趋势和共同的诉求。
- 中后台系统调研
- 结论
基于以上思考,以及“希克定律”。我们决定坚定不移地将中后台系统进行“场景化”分类,标准化不同场景的 UEUI 来减少搭建细节处理。
- 惯性思维陷阱
有一个有趣的问题,我在这里抛一下,做一款“粗粒度”的搭建平台,要做的底层架构比“细粒度”搭建平台要少嘛?当然不是!我们要把底层的“细”做好后才能将“粗”抽象出来,实现后续组件生态和搭建平台的可持续发展。
平台现状
- 已接入中后台系统:10+
- 访问用户:350+
- 线上页面:550+
平台架构
关于飞马-中后台页面搭建系统,我门从零到一,持续输出成一个中后台搭建体系。由核心概念慢慢延展开来。
- 核心概念
- 整体架构
搭建平台初览
- 左侧功能列表
- 注册后 API 选择区域
- 注册扫描后组件展示区域
- 当前页面 Component Tree
- 中间搭建区域
- 搭建画布
- 画布比例缩放
- 右侧配置面板
- 属性配置区域
- API 实例关联区域
- 顶层功能
- 动态化布局
对于数据图表场景,我们会经通过绝对定位的技术方案来实现图表组件间更灵活的布局模式,以及自定义不同图表组件的大小
- 预览
- 保存
- 保存为新模板
业务场景容器
前序
- 搭建平台组件与常规系统组件的区别?
- 搭建平台组件可以直接用在业务系统中,常规系统组件不能直接用在搭建平台上
- 满足一定协议和标准的业务组件,是一个搭建平台组件
场景容器的定义
- 面向特定业务场景
- 封装预定义 UEUI 的高阶组件
- 定义外部数据流和内部数据交换通道
- 以方法回调的方式,约定回调方法的参数及格式
- 封装一个声明周期内,特定语义的行为规范
搭建平台组件与容器之间的联系
一个组件可以实现多个容器定义的规范和接口,从而可以成为一个可以用在该场景下的组件
容器(n):组件(n)
协议
场景容器协议
以 Form 容器协议为例,如何是一个标准的 Form 容器组件,换句话说组件如何被 Form 容器识别?
至少满足以下两个接口
- 组件对外暴露 value 属性,并用作值处理
- 组件对外暴露 onChange 回调,用于通知外界值发生变化
搭建平台协议和标准
以最常见的发送 Ajax 数据请求为例,该能力被搭建平台接管,会给有需要的组件注入发送数据请求的能力
- 如何标记组件需要发送 Ajax 请求的能力?
- 通过对外暴露回调函数属性
- PropTypes 描述主文件中用 "api-handler" 标记的属性
XCard.propTypes = {
onInit: "api-handler",
title: string,
className: string,
// reportId: number,
// productId: number,
// height: string,
// locale: oneOf(['zh-CN', 'en-US']),
};
XCard.defaultProps = {
"__pegasus_editor-config_is-container__": true,
// __pegasus_component_cnName__: '卡片',
};
上图是具体标注方式,被标注后,该组件的对应属性“onInit”被中文化成“初始化时间”后,在搭建平台的配置面板会渲染成如下图:
组件注册扫描机制
如上架构图,详细解释了从组件开发到接入平台的流程。平台最终扫描并存储的是组件的 PropTypes 描述结构,描述结构中定义了属性的类型,一个类型对应着搭建平台配置面板的一个组件配置类型插件。
组件属性类型和搭建平台配置属性插件
这是关键的一层,该层可以近乎无限扩展来完成静态属性描述和动态代码配置之间的联系。
组件的一个属性对应着一个类型,该类型可以是基本类型,可以是复杂类型,也可以是平台可识别的预定义类型。一个类型,封装了特定的业务语义、UEUI 以及 数据结构和代码结构,比如 "api-handler" 这种预定义类型,标志着这个组件的特定属性是一个需要发送数据请求的回调接口,平台生成的代码结构最终如下:
"onInit": (e, params, sucCb, errCb, needRender) => { let mappingRules1 = [{"before":"","after":""}]; fn1.call(this, e, params, mappingRules1 , gaea_instance_952cacfe, sucCb, errCb, needRender); } ,
数据通信
- 全局数据存储
- 容器内数据共享
- 容器间数据共享
- 通过关联同一个 API 实例实现数据共享
代码生成引擎
这也是关键的一层,后期可以做到识别不同 DSL 甚至不同语言,实现跨平台代码生成。
云端后编译
- 多环境云端编译
源码会随着搭建页面结构一一存储到云端,通过云端机器进行编译,最终生成页面。
根据项目流程,用户可以选择性将搭建页面发布到“开发”、“测试”或“线上”环境,达到不同环境线上页面最终产物隔离的目的。
- 历史记录回滚
每一次生成的线上页面,都会被记录,以备发布上线出现问题的时候快速回滚到历史的某个版本
通过 SDK 的方式接入存量中后台系统
- 先看一个接入例子
import { Wrap } from "@didi/pegasus-utils";
import React from "react";
import Upload from "@/components/upload/index";
export default [
// 叶子节点渲染component
{
id: "1",
name: "首页",
path: "/home",
component: () => <h1>home</h1>,
icon: "home",
pid: "0",
},
{
id: "2",
name: "编码管理",
path: "/codeManage",
icon: "barcode",
component: Wrap({ page_id: 839, React }),
pid: "0",
},
];
- 再看飞马 SDK 的核心逻辑
return class extends React.Component {
componentDidMount() {
const innerURL = `//xmis.${
!!envTest ? "test" : "intra"
}.xiaojukeji.com/portal/pages/renderUrl/${page_id}?env=${env}`;
const outerURL = `https://${
!!envTest ? "gw-test.intra" : "gw.am"
}.xiaojukeji.com/faas/callFn?fn=xpage&ns=xmis&env=${env}&token=${outerToken}${
!!envTest ? "&test=true" : ""
}`;
const url = (page_id && innerURL) || (outerToken && outerURL); //优先page_id
GET(url)
.then((response) => {
let scripts = [],
styles = [],
preCSS = [],
promiseList = [];
const data = response.data;
data.preload_css.length &&
data.preload_css.map((x) => preCSS.push(x.url));
styles = [...preCSS, ...data.main_css];
if (noCss) {
styles = [];
}
styles.map((src) => {
if (!document.getElementById(src)) {
loadCss(src);
}
});
//注意js加载顺序
scripts = [...data.preload_js, ...data.main_js];
scripts.map((x) => {
if (!window[x.depNS] && !window[x.namespace]) {
promiseList.push(
loadJs(`pegasus-${x.depNS || x.namespace}`, x.url)
);
}
if (
(x.depNS == "axios" || x.namespace == "axios") &&
(window[x.depNS] || window[x.namespace])
) {
SetAxiosConfig(reqHeaders, withCredentials);
}
});
//加载bundle.js
if (document.getElementById("pegasus-bundle")) {
//删除已有的bundle
let elem = document.getElementById("pegasus-bundle");
elem.parentNode.removeChild(elem);
}
promiseList.push(loadJs("pegasus-bundle", data.bundle_url));
Promise.all(promiseList)
.then(() => {
console.log(
"-----------【message from pegasus】所有前置资源加载成功 -----------"
);
})
.catch((error) => {
return new Error(error);
});
})
.catch((error) => {
let root = document.getElementById("pegasus-root");
root.textContent = `【Pegasus资源加载失败】 ${error}`;
});
}
render() {
return <div id="pegasus-root">pegasus is loading...</div>;
}
};
简单来说,做了两件事,
1. 加载各种页面前置资源,加载页面入口文件,植入 DOM 定位元素"pegasus-root" 。
2. 页面入口文件加载完毕后会渲染到定位元素"pegasus-root"的位置
飞马页面搭建系统 && Midway 微前端搭建系统
在日常开发中,我们也经常遇到重新建设一个中后台系统的情况,单靠飞马是无法更好地完成这类诉求。基于此类,飞马跟 Midway 微前端搭建系统携手。使用 Midway 可以快速生成一个中后台系统外壳,包含了登录、权限、菜单、顶导等一系列骨架,再使用飞马平台生成具体页面后一键集成到系统中。
总结和展望
- 总结
一言道不尽技术细节,后续会分几个篇幅,更为详尽得介绍不同方向技术实现以及优劣。
页面搭建确实可以提高效率,解决开发资源与需求错配的问题。不过搭建之路最突出的矛盾,要数搭建后的页面代码如何进行二次开发?甚至如何在搭建后产物上进行更广阔的开发延展?如果真能很好解决这个问题,那么可视化搭建将进入高速路。
- 展望
跨平台搭建只是个解法时间问题,将搭建平台无缝对接日常开发环境更为任重道远。欢迎志同道合的小伙伴,一起加入造马大部队!