一、背景
作为客服域访问量最大的页面之一,订单详情页在客服的日常工作中被用来查阅用户的订单信息,以此为进线的买卖家用户提供更好的购买服务,进而提升用户的满意度。无论是一二线客服还是客服管理者,都能在日常使用的系统中直接访问到详情页,因此客服订单详情页的入口也比较多,目前已经超过了10处。
随着得物业务的快速发展,客服订单详情页需要展示的信息越来越多,需要支持的操作也越来越多,在页面改版前,一个页面首屏就需要展示80条订单信息,具体数量会根据订单类型、交易状态、物流状态等因素而改变。一个页面 最多情况会有200条信息、60个按钮以及20个订单标签(不含用户标签、商品标签),所以会出现一个现象,一个1920*1080分辨率的显示器(客服常用的分辨率),鼠标滚轮需要滚2次才能从页面的最上方滑到最下方,详情页信息截图如下所示。
另一方面,贸然调整反而会降低客服同学的工作效率。页面内容、交互变化太大对客服同学也存有不小的培训成本,因此秉持着变更影响最小化的原则,信息量一直保持着只增不减。
经过一段时间的沉淀与打磨,客服订单详情页无论是客服的使用体验,还是开发体验都得到了显著提升,订单相关的TS问题近半年一直保持清零状态。这期间做了很多尝试和渐进式优化,本文主要从以下三点具体聊聊对客服订单详情页体验升级做的一些思考和优化。
-
这么信息全的页面,几乎所有客服域平台都要能直接访问查阅,一个页面如何多个系统使用,在保障客服同学使用体验的同时,还能节省开发同学维护成本。
-
信息量大数据源多导致页面加载慢,客服同学经常反馈卡顿,如何对页面首屏进行秒开优化,进一步提升客服同学的使用体验。
-
简单的改动也需要投入资源,信息模块复用率低,如何建设信息自由编排、信息模块拔插的能力,最大程度解放产研运同学生产力。
二、多入口的页面复用
1、多实例iframe
最早订单详情页只是客服工单系统中的一个页面,使用的是Vue2、ElementUI的技术栈。客服工单系统的定位是后台管理系统,使用门槛较高,更适合管理者使用,所以需要一个面向一二线客服的平台,章鱼工作台就诞生了。章鱼工作台是基于qiankun微应用搭建的,其子应用最开始使用Vue3、Vite和Ant Design,那时,如果要在章鱼工作台中访问客服订单详情页属于跨应用、跨技术栈通信,使用iframe是成本最小,也是初期最合适的一种方式,这也是页面复用的第一个阶段:在子应用使用多个iframe容器嵌入工单系统中的订单详情页。
所有订单详情页的使用方只需要在本地创建iframe容器,然后在嵌入订单详情页的访问地址时传一些必要的参数如订单号、求购单号就能够正常访问了,十分便捷。还可以支持一些高级配置供使用方选用,如支持通过url query传参的方式控制页面样式,当用于外部嵌入时隐藏主应用的菜单和顶部tab栏等等。
这个阶段,初步解决了页面复用的问题。但大家都知道iframe的弊端,一二线客服同学的日常工作中会出现大量的页面切换,这样一来,iframe的内存占用高、加载缓慢的缺点就被放大开来,这个阶段一二线客服经常会反馈页面卡顿,所以迫切需要进一步优化。
2、单实例iframe搭配MF远程组件
对客服工单系统、章鱼工作台两个平台的用户特点、作业行为进行分析,发现章鱼工作台的用户对页面的体验要求更高,于是对详情页做了第一次优化,步入了第二个阶段:将订单详情页迁移至章鱼工单工作台,其构建方式也由Vite变更为Webpack5,子应用之间利用Webpack5的Module Federation特性通过远程组件的方式进行数据通信。另一方面,尽管iframe的缺点明显,但仍是跨技术栈应用间页面通信的不错选择,将iframe控制在单实例容器中可以最大程度限制其对内存的占用。
这样一来,详情页的入口虽然有10多个,但通信方式都收拢成了三种:远程组件、单实例iframe、本地组件。所有场景都能覆盖到,后续各个入口有复杂交互的变更或者自定义事件,都能够在页面主体做到监控和收口,不需要页面的使用方做额外开发。
3、技术实现
3.1、单实例iframe通信
内容提供方
-
详情接口响应后注册message事件。
-
监听iframe父级携带数据变化,更新本地页面数据。
-
本地页面交互事件被远端触发,发送当前的数据给远端做自定义交互。
/** Vue3 */
/** 1. 详情接口响应后注册message事件 */
onMounted(() => {
/** 请求详情接口 */
fetchOrderDetail(() => {
/** query上打上iframe标签,用于确定注册时机 */
route.query.iframeRoute && watchParentMessage()
})
})
/** 2. 监听iframe父级携带数据变化,更新本地页面数据 */
const watchParentMessage = () => {
window.addEventListener('message', event => {
const orderNo = event.data.data.orderNo
if (event.data.type === 'ORDER_CHANGE' && orderNo) {
/** 更新订单信息*/
initStream(orderNo)
}
})
}
/** 3. 本地页面交互事件被远端触发,发送当前的数据给远端 */
window.parent.postMessage(
/** payload: 携带的数据*/
{
type: 'workbenchRoute',
params: {
/** 跳转退货详情页 */
name: 'refundDetail',
query: {
// 携带数据
},
},
},
/** orgin: 如果想要传递给任意窗口,可以将这个参数设置为'*' ,为了安全起见,不建议设置为'*'*/
'*'
)
内容使用方
-
页面初始化时注册message事件。
-
监听本地订单单号变化,将新的数据传给远端。
-
监听远端交互和数据变化,根据交互类型做不同的本地处理。
/** Vue2 */
/** 1. 页面初始化时注册message事件*/
mounted() {
window.addEventListener('message', this.callBack, false)
},
/** 2. 监听本地订单单号变化,将新的数据传给远端*/
watch: {
orderNo(newOrderNo) {
/** 在iframe的contentWindow属性上挂载postMessage方法*/
detailIframeRef.contentWindow.postMessage(
/** payload: 携带的数据*/
{
type: 'ORDER_CHANGE',
data: {
orderNo:newOrderNo,
//其他数据
},
},
/** orgin: 如果想要传递给任意窗口,可以将这个参数设置为'*' ,为了安全起见,不建议设置为'*'*/
'*',
)
},
},
/** 3. 监听远端交互和数据变化,根据交互类型做不同的本地处理 */
method: {
/** callBack: 远端事件被触发后,处理本地回调逻辑 */
callBack(event) {
try {
if (event.data.type === 'workbenchRoute') {
switch (event.data.params.name) {
case 'orderdetail':
//跳转订单详情的handler
break
case 'detail':
//跳转工单详情的handler
break
// 其他交互
default:
//兜底处理
}
}
} catch(error) {
//异常处理
}
}
3.2、远程组件通信
内容提供方
-
配置webpack的MF插件,将整个订单详情页exposes出去
-
维护详情页的props
/** 配置webpack MF插件,将订单详情页exposes出去*/
new ModuleFederationPlugin({
filename: 'remoteEntry.js?[hash]',
library: { type: 'window', name: 'app_ticket' },
name: 'app_ticket',
shared: {
/** 需要共享的依赖 */
},
exposes: {
/** 提供订单详情远程组件*/
'./OrderDetail': './src/views/orderdetail/Index.tsx',
},
}),
内容使用方
-
webpack配置需要建议通信的远端应用
-
使用defineAsyncComponent注册组件
-
像本地组件一样使用远程组件
/** 1.webpack配置远端应用 */
remotes: {
app_ticket: getRemoteUrl('app_ticket'),
},
/** 2. 使用defineAsyncComponent注册组件*/
'OrderDetail': defineAsyncComponent(() => import('app_ticket/OrderDetail')),
/** 3. 像本地组件一样使用远程组件*/
4、总结
页面使用iframe的首屏耗时平均在7076ms,非首屏在2594ms,而MF的首屏只需要1279ms,非首屏更是只需428ms,渲染时间降低了6倍。
单个页面的内存占用减少到了之前的1/10以内,关于模块联邦和远程组件的更多细节可以查看Module Federation 在得物客服工单业务中的最佳实践。
三、首屏秒开优化
上一章节说到使用MF的方式解决了架构层面的卡顿问题,但无缓存下页面仍要2~3s甚至更久才能刷出订单信息,这时要怎么办?是的,可以改交互、拆接口。但如果数据依赖了大量外域服务、没有外域产研资源介入,且要在一周时间做到有效的优化,那还能做些什么呢?
1、缓慢原因
由于一些历史原因,客服订单详情页需要同时展示100+的订单信息,所有的订单信息、订单操作涉及的字段接近200个,而这么多字段其中90%都在一个http接口里面,这个大接口包含了36个dubbo接口,这些接口来自交易正向、逆向、供应链、商家、商品、用户以及其他BU。并行的调用一定会出现短板效应,只要有一个接口RT(Reaction Time,响应时间)慢,就会拉慢整个http接口的响应速度,同时出现Timeout的概率也会上升,再加上页面本身对资源的渲染时间,无缓存下仍要2~3s甚至更久才能刷出订单信息。这个大接口的平均RT在500ms,P99线的RT达到了1.3s,下图就是生产环境下某一次的调用详情,耗时在782ms,降RT优化首屏渲染刻不容缓。
除了接口RT耗时高的问题,还有首屏接口并行调用的问题。90%的字段都在一个大接口里面,剩下10%都是在零零散散的一些小接口里,把这些小接口加起来页面首屏接口超过6个。我们知道一个Chrome页签最多并行处理6个http请求,如果有第7个接口就会进入到Stalled(熄火)状态,等待前面的某一个接口响应完毕后再发起请求,下图是一个示例,getTrackTicketInfo接口是页面首屏的第7个接口,255.14ms就是需要等待的时间。
根据上述问题现状,初步的方案就是接口先聚合再拆分,把所有接口的数据聚合到一块,然后再根据信息模块拆分成若干个接口,前端再根据业务场景和用户行为,去对拆分出来的接口的调用时机进行优化。但是,理想很丰满,现实很骨感,数据量摆在这,很难在短时间去做到这件事情。光把字段梳理全,数据来源理清楚就用掉了两天时间,再考虑到成本和收益后,我们的最终方案就是新增快慢接口,快接口的RT要在200ms以内,所有拖慢RT的数据都放到慢接口中,前端再根据接口的特性将所有接口分为2个梯队在不同时间进行调用,最大程度的减少页面的首屏渲染时间。
2、接口调用优化
2.1、技术方案
为了控制页面并行请求接口数量和页面数据渲染次数,将除了快慢接口之外,所有零散小接口分为如下两个梯队:不依赖详情大接口反参的首屏信息接口;依赖详情大接口反参的首屏信息接口、非首屏信息接口。
最终详情页首屏接口调用情况示意图如下,在能够聚合依赖大接口反参的首屏信息接口的情况下,页面只会渲染两次,第一次是在快详情接口和第一梯队接口请求回来之后(使用Promise.all控制数据的渲染时机),第二次就是在慢详情接口请求回来之后。
使用Promise.all来保证快详情接口和第一梯队的接口信息同时渲染,减少页面渲染次数,从而减少页面抖动的情况。Promise.all有个缺点就是其中有一个Promise异常,整个就会抛出异常,所以需要对Promise.all包裹的Promise进行二次封装,保证有一个Promise报错不会干扰其他接口的请求,具体代码实现方式如下:
/** 处理promise,保证promise.all使用时相互独立 */
export const handlerPromise = (api, params) => {
return new Promise(resolve => {
api(params)
.then(resolve)
.catch(() => resolve({ error: true }))
})
}
/** 详情页首屏请求函数 */
const fetchOrderDetail = (callback?) => {
/** 使用handlerPromise封装过的promise,保证有一个报错不干扰其他接口请求 */
Promise.all([quickDetail(), firstLevel1(), firstLevel2()])
.then(([quickDetailData, firstLevelData1, firstLevelData2]) => {
// 快详情接口
!quickDetailData?.error && quickDetailHandler(quickDetailData)
// 第一梯队接口调用:不依赖详情反参的首屏信息接口
!firstLevelData1?.error && firstLevelHandler1(firstLevelData1)
!firstLevelData2?.error && firstLevelHandler2(firstLevelData2)
// 执行回调
nextTick(callback)
})
.then(() => {
// 第二梯队接口调用:依赖详情反参的首屏信息接口、非首屏接口
secondLevelHandler1()
secondLevelHandler2()
})
// 执行慢接口
fetchSlowOrderDetail()
}
接口调用顺序确认后还有一点需要注意,因为慢接口响应耗时较高,在客服同学快速查询的工作场景中,可能会在慢接口还在pending,就已经切换到下一个订单了,页面单实例的场景下,这个时候如果不处理可能会将出现数据串台的情况,不属于该订单的数据显示了出来,关于这一点,需要在慢接口的handler上做如下处理。
/** 慢详情接口请求 */
const fetchSlowOrderDetail = () => {
slowLoading.value = true
orderApi
.getDetail(params)
.then(slowData => {
/** 防止快速切换订单导致的数据串台问题 */
if (slowData?.topInfo?.orderNo === orderNo.value) {
orderDetail.value = slowData
}
slowLoading.value = false
})
.catch(() => {
slowLoading.value = false
})
}
2.2、最终效果
优化后的Waterfall图就如下所示,不会再出现灰色的stalled耗时了,而且在228ms后首屏的数据就已经请求回来了。
3、大接口优化
上面一点解决了首屏接口的调用问题,接下来是对大详情接口具体做的一些优化:
-
接口协议由POST改为GET请求,GET的总耗时是POST的三分之二;Chrome下如果检测到GET请求的是静态资源,则会缓存,如果两次传输的数据相同,第二次以后耗费的时间将在10ms以内。另一方面也为后续工作台引入Service Worker技术打下基础。
-
新增快详情接口,将大接口中的响应耗时较高的字段整理出来,快接口不再包含这些字段。这些高耗时的字段新增字段级别的loading效果,为了避免快慢接口耗时差异较大,导致一些经验丰富的客服同学误以为快接口没返回数据的字段是空数据,但是这个loading数量不会超过3处,保持页面的整洁易读。
4、总结
经过上述优化,快详情接口RT只需要平均190ms,从之前大接口的470ms下降了41%,首屏渲染时间从873ms降至376ms,下降了57%,95分位567ms,下降了62%。
首屏优化效果明显,很难再看到反馈详情页加载缓慢的VOC了,一定程度地提升了客服的平台体验满意度。
四、信息编排、模版插拔能力建设
解决了页面卡顿和首屏加载慢的问题,但仍存在一些问题。这一次在产研运同学的通力协作下,如何进一步提升技术同学的开发体验和客服同学的使用体验呢。
1、仍面临的问题
虽然详情页中堆叠的字段已经多达200处,随着业务高速发展仍会存在部分信息缺失、不准确的情况,对客服日常作业产生了一定的负向影响。另一方面,在开发订单类需求中,约60%的都是配合外域或者内部进行字段增删改,如果建立了订单信息的编排能力,后续字段类需求将可配置,从而能够解放这部分需求的产研生产力,达到降本增效;同时若前端模块能够支持模块拔插能力,也能为后续订单信息模块复用到坐席辅助及其他客服工作台提供技术支撑。
2、信息编排能力建设
将订单基本信息及关联信息通过统一Schema维护。大家知道Schema(结构化的数据类型)只要约定的足够复杂是可以用来描述所有场景的数据的,所以使用Schema第一步就是要控制好这个边界,在能够覆盖大部分业务场景前提下不能太复杂。首先可将订单信息做3层细分:信息块、信息组、信息元素。做到配置信息块、信息元素对于订单详情页的场景来说都不太合适,所以这里选择约定到信息组的格式。
2.1、Schema格式
上面图示中两组信息组可用下述Schema描述出来,利用数组的有序性,从左到右、从上到下对信息组进行渲染,实现订单信息的编排配置能力。
schemaData : [
{
label: '订单类型'
text:'普通现货'
children: [
{
id: 'orderTypeDetail'
text: '详情',
show: true,
toolType: 'linkBtn', /** linkBtn, primaryBtn, tag */
eventType: 'click', /** dbclick, hover*/
interactiveType: 'popover', /** modal, popover, message*/
children: [
//** popover弹出的内容 */
{ label: , text: '', children: //..}]
{ label: , text: ''}
]
},
{
text: '晚到必赔',
show: true
toolType: 'tag',
},
{
text: '退运服务',
show: true
toolType: 'tag',
},
]
},
{
id: 'tradeStatus'
label: '支付状态'
text:'已经支付'
children: [
{
text: '七天风控',
show: true
toolType: 'tag',
},
]
}
],
2.2高级配置
使用Schema可以满足大部分的字段渲染场景,但是对于一些复杂的交互和自定义的样式仍需要前端去实现,这个时候每个信息元素中的id就发挥了作用,前端可以根据id去绑定交互事件和自定义样式,具体实现如下:
/** 信息元素枚举*/
enum INFO_ElEMENT_MAP {
/** 订单类型详情按钮 */
ORDERTYPE_DETAIL: 'orderTypeDetail'
}
/** 信息元素*/
const infoElementMap = {
INFO_ElEMENT_MAP.ORDERTYPE_DETAIL: {
/** 绑定事件 */
onClick: () => {
orderTypeDetailClickHandler()
},
/** 绑定样式 */
className: [styles.marginLeft],
},
/** 其他需要添加复杂交互和样式*/
}
2.3、模版解析
约定好了Schema和规范,前端再编写对应模版解析代码去渲染页面,对应渲染图如下所示。
最外层的渲染器代码如下:
const SchemaRender = () => {
//TODO 健壮性代码
return (
{schemaData.length ? (
{schemaTemplate.map(item => {
// 分隔符
if (item.key === TemplateKeyEnum.dividedLine) {
return
}
return (
{
return {
text: popoverRender(child),
hide: !child.show,
}
})}
/>
)
})}
) : (
)}
)
}
最终,Schema加上模版渲染就能渲染出订单详情页的信息,后续此类型的需求除了和外域约定字段,就不再需要额外资源投入了。
3、模块插拔能力建设
实现了信息快速编排,还有信息模块高耦合的问题。其实不同角色的客服同学关注的信息是不一样的,所以新的订单详情页要根据客服的身份去展示不同的信息;而且随着屏幕大小的不同,所适合布局也不同。另一方面,工单详情、坐席辅助都需要展示订单信息的某一个信息模块(整个页面展示就太重了),这时就需要订单信息模块有可插拔的能力了。
3.1、技术方案
初步方案是后端维护一个信息模块池,提供出一个接口,前端通过传一个标识,能够返回对应标识需要的模块组合,然后根据数据组合渲染数据。这个方案可以实现信息可拔插能力。
上述方案可以解决问题,也比较简单,但是控制数据的还是前端,这其实违背了最初建设信息编排能力的初衷。于是最终改为后端同学从客服同学的登录态拿到userId,根据id拿到其所在处理组,是买家处理组就返回买家版订单信息,卖家版就返回卖家版订单信息。另一方面,前端也根据屏幕大小做布局的自适应。
3.2、最终效果
- 大屏下页面布局:
- 小屏下页面布局(1440*900以下):
- 工单详情使用订单详情中的物流记录、服务记录订单信息模块:
从改版以来近8个迭代的资源投入数据来看,订单需求开发成本降低了66.7%。
五、灰度和埋点方案
1、灰度方案
新版详情页改动较大需要根据客服所在处理组进行灰度,但是一线和二线的处理组分配又不太一样,所以需要根据入口来源判断使用哪套AB方案接口。另一方面,订单详情页的入口非常多,所以在每个入口做灰度不太现实,改动较大,所以选择收口到详情页主页面区分新老页面。
1.1、技术方案
- 根据来源区分使用一线AB方案还是二线AB方案,伪代码如下:
/** 获取来源区分IM、工单灰度组信息 */
watch(
() => props.platformCode,
code => {
try {
switch (code) {
case PLATFORM_TYPE.IM:
/** 一线灰度走一线灰度接口 */
isGray.value = true
break
case PLATFORM_TYPE.TICKET:
/** 二线灰度走二线灰度接口 */
isGray.value = true
break
default:
isGray.value = false
} catch {
isGray.value = false
}
},
{
immediate: true,
}
)
- 在主页面根据灰度情况渲染详情模版:
return () => isGray.value ? (
{isOld.value ? : }
>
) :
1.2、总结
根据培训进度开放灰度名单给客服使用新版页面,同时对新老版页面的数据进行监控 。支持可监控、可灰度、可回滚,确保了在页面大改动情况下的系统质量稳定。
2、埋点方案
为了体现订单信息优化的收益和价值,需要对客服同学在新老订单详情页的停留时间、跳出订单详情页次数进行比对。
-
订单详情页停留时间:有效的停留时间越长一定程度能说明页面的查阅费力度越高。
-
订单详情页跳出率:跳出率越高说明当前订单详情信息不能满足客服的查阅需求,需要去其他页面查看。是信息不全、不清晰的一种体现。而且跳出页面会重新加载新的页面,等待时间会长于页面内获取信息,增加客服获取信息的时间。
2.1、页面停留时间
使用监听路由的方法去计算页面停留时间。这里只对其中一种做分析,其他两种类似。
-
确定路由:https://xxx-xxx.xxx/orderdetail/:id
-
只需要考虑上一个路由是订单页面的情况
-
数据过滤:小于3s,大于30min视为无效数据
-
如果最近的路由是订单页面,则重置时间
/** 数据上报 */
const uplog = (current, last, constant, type) => {
/** b: 只用考虑上一个路由是订单页面的情况 */
if (constant === last) {
const nowTime = getNowTime()
const stayTime = nowTime - stayOrderDetailTime.currentTime
/** c: 小于3s,大于30min视为无效数据 */
if (stayTime > 3000 && stayTime {
return { name: route.name, id: route.params?.id }
},
throttle(
(currentRoute, lastRoute) => {
/** 从工单出发:只用考虑上一个路由是订单页面的情况 */
uplog(currentRoute?.name, lastRoute?.name, `${globalConfig.backstageCode}_orderdetail`, 'routeChange')
},
100,
{ leading: true, trailing: false }
),
{
deep: true,
immediate: true,
}
)
2.2、页面跳出次数
页面跳出次数就比较简单了,只需要在跳出事件的handler里加上数据上报方法,比如查看商品详情、退换货详情、用户分期信息、工单详情等等,最后计算跳出总量即可。
2.3、总结
老版页面平均停留时间15.6s,新版页面的平均停留时间8.5s,客服每次查询信息的时长缩短7.1s,可根据详情页PV换算日均可降低客服同学工作时长。
同时,客服查询特定订单信息时需要跳到别的页面查询,这说明订单信息是有缺失和或者客服是对信息准确性是有怀疑的,老版页面跳出率11.4%(约8.7次访问跳出1次),新版页面跳出率7.92%(约12.6次访问跳出1次)。客服查看订单信息时跳出的率也下降3.48pp。
六、总结&规划
1、总结
关于动态路由页面的多平台复用:跨技术栈使用单实例的iframe通信,配合双向的postMessage事件监听用户行为触发交互;微应用内使用Module Federation通信,在保障客服使用体验的同时,节省了开发维护成本。
- MF的首屏需要1279ms,非首屏只需428ms,渲染时间降低了6倍。
- 单个页面的内存占用减少到了之前的1/10以内。
关于大数据量的页面首屏优化:基于业务优化接口调用时机,保证同时不超过6个接口请求,避免出现Stalled耗时,对大接口RT进行优化,场景允许的话可改为GET协议类型,降低首屏响应时间,提升客服体验。
- 首屏请求6个以上接口时不再出现Stalled耗时,大接口改为GET后总耗时是POST的2/3。
- 快详情接口RT只需要平均190ms,从之前大接口的470ms下降了41%,首屏渲染时间从873ms降至376ms,下降了57%,95分位567ms,下降了62%。
关于信息编排、模块拔插能力建设:根据业务分析字段特点,约定合适的Schema格式使得信息内容可灵活配置;对用户的职能特点、设备情况进行分析,使得用户所访问页面的布局、内容做到区分,做到给用户看到合适的内容,降低坐席的查询信息的费力度。
- 订单需求开发成本能够降低66.7%。
- 老版页面平均停留时间15.6s,新版页面的平均停留时间8.5s,客服每次查询信息的时长缩短7.1s,根据订单详情页的PV可换算出每日可降低客服同学的查询时长。
- 老版页面跳出率11.4%(约8.7次访问跳出1次),新版页面跳出率7.92%(约12.6次访问跳出1次)。客服查看订单信息时跳出率下降了3.48pp。
2、后续规划
虽然客服订单详情页的使用体验已经得到提升,但是体验升级之路仍在继续:
- 模块联邦虽然很快,但是对公共依赖维护成本较大,也会导致应用构建速度下降。后续会基于无界对订单子应用进行迁移,建设专门存放远程组件的应用容器,提升子应用的秒开和快速切换体验,同时也能提升工单子应用构建速率,解耦横向订单功能的发布。可期待后续内容。
*文 / 昌禾
本文属得物技术原创,更多精彩文章请看:得物技术官网
未经得物技术许可严禁转载,否则依法追究法律责任!