前端低代码之路(三)-- 营销页面代码架构分享

2,325 阅读7分钟

前言:之前写了2篇基本没有干货,怕读者骂我狗,既然分享了就彻底点,都是干代码的,有啥不能写的。具体的代码我会发到github进行开源。整个项目就不开源了,只开源编辑部分。代码是react代码,没办法一直就用这个框架,意思到了不要太看中语法框架。没有时间对庞大的代码进行一一讲解,各位且自己看吧,嫌烦就别看了。看看这里的思路就好了。

我们还是从架构讲吧,很多人问前端架构干啥的,其实我也不知道,我经过winter(卧槽,winter是谁,不知道的请自行搜索)指点,告诉我前端架构就是解决复用性的问题,豁然开朗,请不要再给前端架构搞的神秘而高不可攀。以React为例,就是合理的分拆组件,到达最高的复用和扩展。随着项目不同也不尽相同,当然很多人喜欢用*-cli等工具生产项目,这个不纠结,反正我觉得灵活度不够,一直都是自己做。

image.png

一般我的项目里面README.md开头都会有这么一个图,说明当前项目的文档目录结构,以便参与项目的同事看得懂代码。前言废话说完,进入正题,项目目录不是重点。本人因为是react系开发者,所以代码以react为主,但是我分享的基本原则通用,未必非react不可。

根据上一篇的产品形态,咱们针对于2类产品分析,一类就是H5页面生成器,一类就是流程定义+form表单生成。

先说H5页面编辑器,来重复一下基本的产品需求

  • 展示效果需求,稳定性,不能有报错,不能白屏。速度还得快,不能让客户等太久。
  • 业务需求,组件的丰富,就是市面上有的咱要有,没有的也希望有。例如,为了有条理的展示更多商品,类似于分类的组件,为了更好的控制页面结构,类似于layout的需求
  • 路由需求,随着营销类的运营能力提升,很多活动是以主题活动的形式开展,就不是一个一个单页的宣传了,往往是一个会场的概念,就会集合很多个单页组成一些列的页面搞活动,那么就需要控制路由的能力
  • 投放需求,活动页面会有定点投放的需要,类似于某些活动只在杭州搞,杭州以外的地区不参加,页面就要有根据地区展示的能力,还有有些活动是定点在某个渠道搞,其他渠道不搞。
  • 数据收集的需要,搞活动目的还是要留存客户,当然希望还有入口可以让用户留下点信息。以便后续的变现。
  • 数据分析的需要,一般来讲,页面访问数据未来还是要能提升以后的活动效果,用户行为数据分析,以便为后续活动提供策略支持,简单的uv,pv已经不能满足当下运营的需要了,而是要跟立体的数据,例如商品的点击次数,用户在页面的留存时间,用户在整个活动中的访问链路。

基于上述的问题,我们需要拆解需求,形成一个基础的前端框框,把这些需求点框进去,不管UI,UI是很简单的,没有任何需要架构的地方,根据prd去画就好了。基本编辑的UI也就是左中右结构,左侧组件列表,中间效果展示,右侧属性配置。基本如是

image.png

我们用react代码实现如下, 入口 editor.html, editor.js

<div className="page-container">
    <Suspense fallback="">
          <ThemeProvider theme={theme}>
                <Header savePage={this.savePage} publishPage={this.publishPage} />
                <Operation compCount={compCount} />
                <Preview pageConf={pageConf} compList={compList} />
                <Config
                  pageConf={pageConf}
                  compList={compList}
                  currentUuid={currentUuid}
                  onSort={this.handleSort}
                />
          </ThemeProvider>
    </Suspense>
</div>

这是UI的设计,外层的 <div/> <Suspense/> <ThemeProvider/>忽略

<Header/> - 头部包含返回按钮,预览,保存,发布等按钮功能

<Operation/> - 左侧的组件列表

<Preview/> - 中间的预览展示,为了很好的隔绝dom结构和event事件,中间使用iframe加载一个preview.html,利用postmate组件进行iframe通信工作。其中暴露很多问题,后面会详细讲解。

<Config/> - 就是根据用户选中的组件,展示相应的配置项

这里讲下整个项目的通信闭环,我想大家都很容易明白技术实现的过程中是怎么做的了。整个页面UI其实都是一份数据结构在驱动。利用react的内部数据双向绑定,每一次的新建修改其实都是在改同一份数据结构。举个列子:

env: "publish",
pageId: "1632599993707810817",
pageData: {
    pageConf:{
        background: {a: 1, r: 255, b: 255, g: 255}
        pageDec: ""
        pageName: "杭州印象"
        pageURL: "https://cdn.izelas.run/publish/2194972e.html"
    }, 
    compList: [
        {
            uuid: "c3f1ecdc71c1df833a4498fddd4040db",
            compLabel: "轮播",
            compName: "Carousel",
            chosen: false,
            setting: {
                carouselProps: {effect: "scrollx", autoplay: true},
                imageList: (5) [{}, {}, {}, {}, {}],
                outLine: false,
                place: "inline",
            }
       }
    ]
},

秀到这大家就明白了,其实技术整体没什么神秘的,也符合大多数研发者的实现。上述配置就是一个页面里面包含了一个轮播组件,后的会在用户点击发布的时候,把数据和渲染做一次加载,打包成静态文件,推送到cdn上,返回一个静态页面地址,这就是整个逻辑。因为后台是nodejs那块代码相对简单,回头再放,主要是前端的渲染。下面大家要做几件事

  • 定义清楚所有组件的Schema,也就是compList中每个组件对应的数据结构。这个跟你具体组件的业务需求密切结合。
  • 解决数据通信问题,也就是新建组件,修改组件属性,删除组件事件发生时候,预览界面要实时改变
  • 数据加载的问题,组件并非完全静态,有些组件需要获取后台数据用以填充页面,例如商品组件,展示用户选择的商品。这些商品信息需要从后台ajax获取,加载这些数据的策略影响渲染

看UI代码,大家会发现,入口页面持有所有的数据,任何子组件不持有数据,通过组件props属性进行传递。而事件通过广播或者监听模式完成。对于这个模式不清楚的,我稍微解释一下,因为react在父子组件的事件上可以通过props注入完成,如果是兄弟组件,或者更远的平级组件,就需要借助共同的父组件来传递事件。效率不高,还容易搞混。现在的模式是,当我点击一个按钮,这个按钮其实没有实际逻辑操作,只是大喊一声我被点了,就完了,把自己被点了这个事广播出去,那么谁处理?谁监听谁处理。就比如老师上课点名,张三,这个时候全班同学都听到了,但是只有张三答了「到」。就是这个意思。

// 持有 pageData -> compList,pageConf 的页面监听事件
// 支持用户点击新建,这里点击触发是在operation组件,向此处广播事件
emitter.addListener('_micro_page_createComp_', this.createComp)
// 监听数据变化,然后通知底层组件 包括组件创建,删除,复制,位置调整
emitter.addListener('_micro_page_onSetCompList_', this.onSetCompList)
// 监听组件属性值改变的 设置某个组件属性
emitter.addListener('_micro_page_setCompProps_', this.setCompData)
// 监听页面设置
emitter.addListener('_micro_page_setPageConfig_', this.setPageConfig)
// 监听历史记录回滚消息
emitter.addListener('_micro_page_posiCompList_', this.goBackAction)
// 组件列表操作消息监听
emitter.addListener('_micro_page_compListeAction_', this.buildCompListData)
    // 按钮的实现就非常简单了,只要把UI画上,点击一下就把被点击的事件广播出去,附带一个默认配置参数
    // 剩下的就交给监听了此时间的方法,这个顶层数据发生变化,那么react会自动diff,所有接收到props
    // 的组件都会更新props,然后更新UI。config的原理是一样的。这样就把数据和处理的方法全部集中在
    // 入口文件中,逻辑比较清晰,容易排查问题。
    <div
      className="module-item"
      onClick={() => {
        emitter.emit('_micro_page_createComp_', _cloneDeep(defaultConfig))
      }}
    >
      <div className="module-item-inner">
        <SvgIcon component={renderComponent(compName)} viewBox="0 0 22 22"/>
        <div className="module-item-name">{compLabel}</div>
      </div>
    </div>

这个逻辑讲明白了,其实就不复杂了,无非就是组件多一点,UI界面复杂一些而已。根据prd的组件设计去开发就好,没什么难度。还剩一些问题,就是数据埋点和ajax数据通信。

数据埋点一般大家都会写一个比较通用的上报函数,也比较简单。非常成熟,不会github上搜搜太多。不行就看看GA(google analysis)的代码。主要是一些按钮数据的上报,比如点击某个商品,虽然商品页面会统计商品的pv,但是不知道是从营销页面来的。类似的问题很多,当然GA也提供了方法可以在点击事件发生时候主动上报数据,问题来了,本身低代码的平台,页面还没有发布,需要根据用户制作页面的结果生成相应的页面,可能我们都不知道用户需要选什么商品。还有一个问题就是代码侵入,如果GA的方法改了呢,那么我就要改。得不偿失,我的解法就是便签隐藏属性。类型jquery的data-,然后根据数据埋点的需要把需要的数据放到一个自定义属性中,数据收集组件自己监听pop上来的事件,到dom中自行取用。

还有ajax请求的问题,这个问题用前端的办法确实没啥好解的,不是不能做,只是用nodejs的解法更方便,反正我也是要根据配置,把页面打成静态的文件,上传到cdn的,那么干脆就在这一步做了就好了。其实没啥难度,就是扫描一下所有组件,发现有ajax请求的,nodejs发起请求,把请求回来的数据静态化到配置项里面,然后调用cdn方法把html文件上传就好了。

const _putHTMLToOss = async (pageConf, compList, pageId) => {
  try {
    // 获取所有动态数据
    const _compList = await getAllData(compList)
    // 设置静态页面url
    const publishHtmlPath = pageConf.pageURL
      ? pageConf.pageURL.replace("https://cdn.izelas.run/", "")
      : `hd/publish/${getShortStr()}.html`;
    if (!pageConf.pageURL)
      pageConf.pageURL = `https://m.myweimai.com/${publishHtmlPath}`;
      // 装配模板需要的数据
      const injector = {
          jses: [
            `${cdnPath}/micropage/${timestamp.micropage}/common.js`,
            `${cdnPath}/micropage/${timestamp.micropage}/micro-view.js`,
          ],
          css: [
            venders.materialUI,
            `${cdnPath}/micropage/${timestamp.micropage}/common.css`,
            `${cdnPath}/micropage/${timestamp.micropage}/micro-view.css`,
          ],
          data: JSON.stringify({
            env,
            pageId,
            pageData: {
              pageConf,
              compList: _compList,
            },
            weimaiHosts,
          }),
          title: (pageConf && pageConf.pageName) || "微脉-模块化编辑器",
     };
    // 读取模板文件
    const renderString = fs.readFileSync(path.join(view_path, "view.html"), {
      encoding: "utf-8",
    });
    // 编译模板
    const compiled = _.template(renderString);
    // 数据和模板结合成已编译好的字符串
    const compiledStr = compiled(injector);
    // 调用oss授权客户端
    const ossClient = new OSS(ossKey);
    // 上传文件到oss
    await ossClient.put(publishHtmlPath, Buffer.from(compiledStr));
    return true;
  } catch (error) {
    console.error(error);
    return false;
  }
};

此文代码基本是截取,会意不要生搬,毕竟每个系统都有特殊性,尤其是行业对于产品的要求,这套代码的源码会开源到github,等我整理完,因为nodejs代码有验证和一些技术点不方便开源,只放前端代码。还是那个意思,明白思路,根据自己的业务自己思考,能帮到各位最好,有好的解决方案欢迎提供。系统也在不断的升级中。还需要加入一些js和图片资源加载的优化,还有就是模板和换肤的一些思考。这些有待各位同仁的共同努力。