c++可视化界面设计_活动可视化搭建系统——你的KPI被我承包了 - 李文杨

前言

对于C端业务偏多的公司来说,在增长、运营等各方同学的摧残下永远绕不过去的一个坑就是大量的H5页面开发,它可能是一个下载、需求告知、产品介绍、营销活动等页面。此类需求都有几个明显的缺点:

•开发性价比极低、上线时间紧,每次需要指派单独同学支持。•沟通成本高,而业务逻辑高度相似。•高频次的需求 有句话怎么说来着,世界是"懒人"创造的,当我们烦透了无休止的重复工作时,一些"偷懒"的想法就会蹦出来。对研发而言我们不想花费过多的时间在此类简单重复的工作上;对运营而言他们需要活动更快的迭代、发版,脱离研发的排期等限制;于公司而言节省人力成本就是节省财务成本,更大的提高效率就可以支撑配合更多增长营销策略。

所以从技术赋能业务的角度出发,一套可视化活动编辑系统是每个中大型公司必备的生产利器。

首先让我们来挑几个代表性的页面简单分析一下...

如下图:

7e3c372ce0a4e141a66acc88ad6bcb71.png

先从页面上做个分析:

•图1、3都属于简单的引流下载页•图2、4属于普通活动页•图5无任何交互逻辑,只是单纯的一个静态告知页•图6从页面结构和业务逻辑来说,属于复杂活动页

接下来抛开UI细节层面不谈,对页面进行一个拆解

•图1、3 组合方式 ( 背景图 + 按钮 + [ ios、安卓 ]下载链接 )

dbdc354afa4fc62bf7e464cd64a129e2.png•图2、4 组合方式 ( 背景 + 按钮 + 活动规则 + 领券逻辑 )c196bf4aaf6fe90ededc235934c72376.png•图5 组合方式 ( 背景 + 活动规则 )a55bbff009d8d271383e8635b594f606.png•图6 组合方式 ( 背景 + 业务模块 + 活动规则 ) a77672fd91e1b39d733f61e30c55dc8c.png

综上分析可见,每个页面由多个小模块构成,可以是基础UI组件,也可以是一个复杂的业务组件,且组合方式多种多样,可以预想到当我们将这些不同组件像组件库那样整合在一起且可以在页面进行可视化的编辑操作时,不同的组件不同的排列即可生成一个全新的活动,就像积木一样拥有无限种可能,开发效率将会大大提高,本文将这两个月鼓捣lego活动可视化搭建系统(以下简称lego)从0到1的心路历程整理出来供各位有相关需求的小伙伴参考,也欢迎大神交流指正。

7ae688fca5d55e60e01d83d49ce04e53.png

核心方案

Lego采用Vue框架开发,通过对组件物料进行拆分再统一管理,形成一个可编辑的组件库。JSON schema 来定义组件JSON规范,配合Vue的动态组件特性来实现动态的页面渲染。动态表单用于根据不同组件特性生成对应配置表单。最后打包并优化多页面,每个页面单独配置域名,一个负责内部编辑、一个负责对外展示。通过活动id获取对应活动JSON数据动态渲染在活动展示页面。

渲染流程:

2b7ac955e5ddf7d915d3864fffb79828.png多页面流程:f56c63ef31bdffda74754b9a37bf384e.png关于最后的活动页面展示除了多页面外其实还有特别多种方案可选,选择最合适自己业务的就好,后边内容会细说这几种展示方案。

关键词:JSON schema、动态渲染、动态表单、组件管理、多页面

技术方案

动态渲染

is

如何将不同的组件打散后再重新拼装并渲染在页面上是整个技术方案最核心的点,好在Vue提供了动态渲染组件方案,通过内置组件conpontent,渲染一个“元组件”为动态组件并根据 is 的值,来决定哪个组件被渲染。

a45214d5034294469d76537587100548.png

props

大部分组件的可配置项无非就样式、链接、事件、文案这几种,我们将它们抽离成一个config对象,通过props的方式传递给子组件用于渲染样式或者加一些点击事件等,比如bind绑定传进来的style样式,当然在这之前一定要定好基础config的规范。

ad1ca3bafa23eb62df7a1447ed72c9f9.png

渲染函数

由于一些业务的原因,Lego除基础组件外其它部分开放的自由度并不算高,所以props的方案目前可以满足我们的需求,但如果你想开放更高的自由度,释放系统的完全编程能力,比如配置一些点击事件,事件执行交互等等。那可以试试Render渲染函数。根据官网对它的描述,它比模板要更接近编译器。而它的可配置对象足够你肆意发挥了。

e5cf17659ae42b7cac392294ca7dce93.png

布局方案

可视化布局的方案抛开大厂不谈,根据公司人员配比,建议大部分中小公司只需要以下两种方案的一种即可。根据不同的优缺点来选择最适合你的方案,如果条件允许也可以二者结合实现。

抽屉式

自上而下顺序排列,可以更换组件位置,但不能实现元素定位,没有层级概念,遇到复杂布局或者需要叠放元素时不够灵活,如果需要实现复杂页面的效果则需要引入复合UI组件的概念,它需要大量现成的UI组件。优点是将可操作程度限定在了一个可控的范围内,对非设计人员来说只需要通过现有UI组件进行拼装即可生成一个美观度较高的页面,lego即采用的是此方案。

abd8b693502bdf2e5daae0a0bd6eafad.png

自由式

完全可随意拖拽元素位置,自由度高,只需几个基础UI组件即可,存在层级概念,可任意叠放拼装,在无成品UI组件的情况下diy出复杂页面。但页面不可控,对操作人员的美感要求更高。更适合UI同学操作使用。下图只是一个复杂布局的例子,关注布局即可先不要管业务逻辑如何实现。

b33ce0241709d25f3fefaabaf6db0e6a.png

关于自由度

结合布局方案聊一下关于可编辑自由度的问题,编辑自由度应该综合实际情况进行考量。

对于lego而言,UI同学仅参与组件设计的工作而不会去使用此系统去编辑发布活动,而当UI同学不直接参与最后的拼装上线时,高自由度的编辑操作对我们而言其实是个鸡肋,直接开放高自由度给运营人员,由于存在层级叠放且可自由拖拽,这样就会有可能面临着大量的不可控的页面样式,即使在编辑完后UI同学对页面效果把关,但相较于改起来的时间成本和活动质量来说是得不偿失的。而且高自由度带来的是更多的技术的考量和实现成本,嵌套组件的层级规则、拖拽方案、组件定位等等….所以当你的团队技术实力和你能得到支持的资源不是那么充分时,也许抽屉式的半自由度方案更加适合你。

半自由度从布局方案到组件的可配置项上来说开放程度有限,组件方面针对基础UI组件开放相对高的配置编辑项,同时积累大量的成品UI组件可选。在编辑时不需要考虑太细节的样式问题,保证页面质量。

当开发人力和资源和部门协同、工作流程都到位且规范的情况下,两种方式结合其实是最佳的方案。可以提供不同模式来供不同人员使用,甚至可以实现在线编辑器来供研发人员直接进行代码调整。

组件分类

Lego将组件分为了两大类:UI组件、业务组件

UI组件细分为基础组件和复合组件,UI组件仅用来做静态展示用。原则是自身不包含任何业务逻辑,由于采用半开放的布局方案,对于复杂样式来说我们又基于基础组件封装了很多不同功能不同用处的复合型UI组件用以满足更复杂页面的需求。比如不同主题的标题、按钮、都可以单独封装出来直接用于拼装。

6d4fd46241a84ca87cc70dddf0a80381.png

业务组件是指那些和服务端有交互的有业务逻辑在里的组件,属于独立的功能模块,可以理解成每个业务组件都是一个单独的活动,比如购卡、抽奖、分享等等。lego针对业务组件的唯一原则就是不在系统内提供业务相关可配置入口,仅开放基础样式配置,如大小、主题色等。将权限回收至研发手中,每个业务组件在营销后台中配置数据,通过不同活动id进行区分渲染。因为每个业务组件发布前都经过了QA的完整测试流程,可以最大程度保证活动的稳定性,而不至于影响到业务。

847b93bc31e5269dda22aa02e96eeb8c.png

配置项

每个组件根据自身特性拥有着不同的配置项,在选中组件后展示对应的配置表单是通过动态表单完成的,Lego系统使用了IView的组件库,每个组件除自身属性外还会对应一份配置对象,通过匹配配置对象来描述这个表单的结构,最后还是通过compontent对表单组件进行循环渲染。

8dd7b223d0de05253251138cb581a35f.png

通信

对页面配置、组件增删、某个元素的配置项等所有编辑后的变化通过Vuex做统一管理进行通知。需要注意的是很多情况下只是改变某个对象下的一个属性,watch监听不到这种对象属性变化,而像是某个样式的其中一个属性变动是很频繁的,所以可以通过添加一个changeStatus的状态,每次属性被改变后可以更改监听changeStatus的状态来通知Vuex进行对应更新,但要注意做好防抖。

523352ff99019097b2f5044eb740de34.png

输出页面

当编辑完组件并拼装好整个页面后,如何将这个页面最终暴露给用户,在这个问题上我们设计过两种方案:

A方案:

从公司现有的活动项目新建一个页面,将组件库打包发布到私有npm仓库进行管理并在此处引入,提供一个获取活动配置的接口给它,所有的活动都使用这个页面作为落地页,通过不同的活动id来获取不同的活动配置信息进行动态渲染。好处是上线方便,只要在lego系统进行发布后拿到发布后的活动id进行拼接即可,而无需进行页面部署。缺点也很明显,需要等待接口请求成功后才能进行渲染,一旦接口服务报错线上所有活动全部都会失效,而且渲染速度势必要比静态页面慢。这个方案我们最终放弃了,因为除了上述问题,最关键的阿是从技术方面,我们的node服务开发经验较少,从技术方案到架构方面都比较薄弱,而且最开始从设计之初没有考虑服务并发和数据库压力等,一旦像是通过公众号推送的活动,它承载的的并发是非常非常高的。所以在没有得到服务端同学参与的情况下没有轻易采用此方案。

207f292f9b473e4581af60794092be44.png

B方案

大体思路同上,还是通过活动id取配置信息渲染页面。但把请求node服务拿配置的方式改成了访问本地json文件,并在落地页的归属上做了一些调整。步骤如下:

1.将lego分为两部分:编辑系统、落地页,配置多页面打包。2.优化多页面打包,做好文件分割,两部分各自引用自身需要文件,提升页面加载速度。3.两个页面分别配置一个域名,一个负责对内编辑,一个暴露对外作为落地页展示4.每次上线活动打包前将配置数据拉到本地储存为json,落地页访问本地的静态资源。 这个方案实现了组件库和公共方法的公用,同时针对每个页面做了分割,实现按需加载,保证页面性能。将网络请求node服务改为本地json,解决了并发的性能问题。缺点是当活动越来越多的时候,本地的json会越来越大,如果不及时清理无用数据,会导致页面加载越来越慢。lego目前采用的是这个方案,后续会再进行优化。

79ad374271259778c274381cbab8e081.png

多页面优化

关于多页面的优化使用了splitChunk进行文件的分离,将系统端用到的各种资源单独进行分离。这样一来每个页面只要加载自身需要的即可。

1.删除默认配置2.单独导出chunk3.指定多入口页面单独进行配置chunk

优化后的页面速度

db199b83c6b29799a2d2c8c0249899ae.png

总结

1.就这个系统来说还有很多缺陷,但已经可以落地使用。像是落地页的方案目前还有明显缺陷,既配置数据保存在本地在一定程度上会拖慢加载速度。社区里的SSR服务端渲染方案、每个活动打包为单独静态页的方案都可以进行尝试。但还是那句话,根据当前团队的技术实力以及你能动用的资源综合进行方案的比较,有时候最好的方案不一定适合你现在的情况。2.活动搭建系统在一定意义上可以解决90%的简单页面,但复杂的多页面联动的活动还是无法做到。3.组件的积累才是重中之重,在物料不丰富的情况下,开发效率提高有限,而一旦运行一年半载组件库丰富起来,效率将会肉眼可见的提高。4.不要觉得自己的方案一定比别人的差,可以参考社区的大佬们优秀的技术方案和思路,但要从中找到自身需要的点即可。5.坚持独立思考、重视基础建设使技术赋能业务是每个开发人员应有的素质,与公司无关与团队无关,只要你有想法总会有办法将方案推动落地,自身的思考和实现的过程中的经验积累才是最宝贵的财富。

C++写的一个简单界面演示系统 void CMiniDrawDoc::AddFigure (CFigure *PFigure) { m_FigArray.Add (PFigure); SetModifiedFlag (); } CFigure *CMiniDrawDoc::GetFigure (int Index) { if (Index m_FigArray.GetUpperBound ()) return 0; return (CFigure *)m_FigArray.GetAt (Index); } int CMiniDrawDoc::GetNumFigs () { return m_FigArray.GetSize (); } void CMiniDrawDoc::DeleteContents() { // TODO: Add your specialized code here and/or call the base class int Index = m_FigArray.GetSize (); while (Index--) delete m_FigArray.GetAt (Index); m_FigArray.RemoveAll (); CDocument::DeleteContents(); } void CMiniDrawDoc::OnEditClearAll() { // TODO: Add your command handler code here DeleteContents (); UpdateAllViews (0); SetModifiedFlag (); } void CMiniDrawDoc::OnUpdateEditClearAll(CCmdUI* pCmdUI) { // TODO: Add your command update UI handler code here pCmdUI->Enable (m_FigArray.GetSize ()); } void CMiniDrawDoc::OnEditUndo() { // TODO: Add your command handler code here int Index = m_FigArray.GetUpperBound (); if (Index > -1) { delete m_FigArray.GetAt (Index); m_FigArray.RemoveAt (Index); } UpdateAllViews (0); SetModifiedFlag (); } void CMiniDrawDoc::OnUpdateEditUndo(CCmdUI* pCmdUI) { // TODO: Add your command update UI handler code here pCmdUI->Enable (m_FigArray.GetSize ()); } // implementation of figure classes: IMPLEMENT_SERIAL (CFigure, CObject, 3) CRect CFigure::GetDimRect () { return CRect (min (m_X1, m_X2), min (m_Y1, m_Y2), max (m_X1, m_X2) + 1, max (m_Y1, m_Y2) + 1); } void CFigure::Serialize (CArchive& ar) { if (ar.IsStoring ()) ar << m_X1 << m_Y1 << m_X2 << m_Y2 <> m_X1 >> m_Y1 >> m_X2 >> m_Y2 >> m_Color; } IMPLEMENT_SERIAL (CLine, CFigure, 3) CLine::CLine (int X1, int Y1, int X2, int Y2, COLORREF Color, int Thickness) { m_X1 = X1; m_Y1 = Y1; m_X2 = X2; m_Y2 = Y2; m_Color = Color; m_Thickness = Thickness; } void CLine::Serialize (CArchive& ar) { CFigure::Serialize (ar); if (ar.IsStoring ()) ar <> m_Thickness; } void CLine::Draw (CDC *PDC) { CPen Pen, *POldPen; // select pen/brush: Pen.CreatePen (PS_SOLID, m_Thickness, m_Color); POldPen = PDC->SelectObject (&Pen); // draw figure: PDC->MoveTo (m_X1, m_Y1); PDC->LineTo (m_X2, m_Y2); // remove pen/brush: PDC->SelectObject (POldPen); } IMPLEMENT_SERIAL (CRectangle, CFigure, 3) CRectangle::CRectangle (int X1, int Y1, int X2, int Y2, COLORREF Color, int Thickness) { m_X1 = X1; m_Y1 = Y1; m_X2 = X2; m_Y2 = Y2; m_Color = Color; m_Thickness = Thickness; } void CRectangle::Serialize (CArchive& ar) { CFigure::Serialize (ar); if (ar.IsStoring ()) ar <> m_Thickness; } void CRectangle::Draw (CDC *PDC) { CPen Pen, *POldPen; // select pen/brush: Pen.CreatePen (PS_INSIDEFRAME, m_Thickness, m_Color); POldPen = PDC->SelectObject (&Pen); PDC->SelectStockObject (NULL_BRUSH); // draw figure: PDC->Rectangle (m_X1, m_Y1, m_X2, m_Y2); // remove pen/brush: PDC->SelectObject (POldPen); } IMPLEMENT_SERIAL (CRectFill, CFigure, 3) CRectFill::CRectFill (int X1, int Y1, int X2, int Y2, COLORREF Color) { m_X1 = min (X1, X2); m_Y1 = min (Y1, Y2); m_X2 = max (X1, X2); m_Y2 = max (Y1, Y2); m_Color = Color; } void CRectFill::Draw (CDC *PDC) { CBrush Brush, *POldBrush; CPen Pen, *POldPen; // select pen/brush: Pen.CreatePen (PS_INSIDEFRAME, 1, m_Color); POldPen = PDC->SelectObject (&Pen); Brush.CreateSolidBrush (m_Color); POldBrush = PDC->SelectObject (&Brush); // draw figure: PDC->Rectangle (m_X1, m_Y1, m_X2, m_Y2); // remove pen/brush: PDC->SelectObject (POldPen); PDC->SelectObject (POldBrush); } IMPLEMENT_SERIAL (CRectRound, CFigure, 3) CRectRound::CRectRound (int X1, int Y1, int X2, int Y2, COLORREF Color, int Thickness) { m_X1 = min (X1, X2); m_Y1 = min (Y1, Y2); m_X2 = max (X1, X2); m_Y2 = max (Y1, Y2); m_Color = Color; m_Thickness = Thickness; } void CRectRound::Serialize (CArchive& ar) { CFigure::Serialize (ar); if (ar.IsStoring ()) ar <> m_Thickness; } void CRectRound::Draw (CDC *PDC) { CPen Pen, *POldPen; // select pen/brush: Pen.CreatePen (PS_INSIDEFRAME, m_Thickness, m_Color); POldPen = PDC->SelectObject (&Pen); PDC->SelectStockObject (NULL_BRUSH); // draw figure: int SizeRound = (m_X2 - m_X1 + m_Y2 - m_Y1) / 6; PDC->RoundRect (m_X1, m_Y1, m_X2, m_Y2, SizeRound, SizeRound); // remove pen/brush: PDC->SelectObject (POldPen); } IMPLEMENT_SERIAL (CRectRoundFill, CFigure, 3) CRectRoundFill::CRectRoundFill (int X1, int Y1, int X2, int Y2, COLORREF Color) { m_X1 = min (X1, X2); m_Y1 = min (Y1, Y2); m_X2 = max (X1, X2); m_Y2 = max (Y1, Y2); m_Color = Color; } void CRectRoundFill::Draw (CDC *PDC) { CBrush Brush, *POldBrush; CPen Pen, *POldPen; // select pen/brush: Pen.CreatePen (PS_INSIDEFRAME, 1, m_Color); POldPen = PDC->SelectObject (&Pen); Brush.CreateSolidBrush (m_Color); POldBrush = PDC->SelectObject (&Brush); // draw figure: int SizeRound = (m_X2 - m_X1 + m_Y2 - m_Y1) / 6; PDC->RoundRect (m_X1, m_Y1, m_X2, m_Y2, SizeRound, SizeRound); // remove pen/brush: PDC->SelectObject (POldPen); PDC->SelectObject (POldBrush); } IMPLEMENT_SERIAL (CCircle, CFigure, 3) CCircle::CCircle (int X1, int Y1, int X2, int Y2, COLORREF Color, int Thickness) { m_X1 = min (X1, X2); m_Y1 = min (Y1, Y2); m_X2 = max (X1, X2); m_Y2 = max (Y1, Y2); m_Color = Color; m_Thickness = Thickness; } void CCircle::Serialize (CArchive& ar) { CFigure::Serialize (ar); if (ar.IsStoring ()) ar <> m_Thickness; } void CCircle::Draw (CDC *PDC) { CPen Pen, *POldPen; // select pen/brush: Pen.CreatePen (PS_INSIDEFRAME, m_Thickness, m_Color); POldPen = PDC->SelectObject (&Pen); PDC->SelectStockObject (NULL_BRUSH); // draw figure: PDC->Ellipse (m_X1, m_Y1, m_X2, m_Y2); // remove pen/brush: PDC->SelectObject (POldPen); } IMPLEMENT_SERIAL (CCircleFill, CFigure, 3) CCircleFill::CCircleFill (int X1, int Y1, int X2, int Y2, COLORREF Color) { m_X1 = min (X1, X2); m_Y1 = min (Y1, Y2); m_X2 = max (X1, X2); m_Y2 = max (Y1, Y2); m_Color = Color; } void CCircleFill::Draw (CDC *PDC) { CBrush Brush, *POldBrush; CPen Pen, *POldPen; // select pen/brush: Pen.CreatePen (PS_INSIDEFRAME, 1, m_Color); POldPen = PDC->SelectObject (&Pen); Brush.CreateSolidBrush (m_Color); POldBrush = PDC->SelectObject (&Brush); // draw figure: PDC->Ellipse (m_X1, m_Y1, m_X2, m_Y2); // remove pen/brush: PDC->SelectObject (POldPen); PDC->SelectObject (POldBrush); }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值