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

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

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

image.png

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

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

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

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

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

image.png

咱们用react代码实现以下, 入口 editor.html, editor.jsgit

<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/>忽略github

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

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

<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和图片资源加载的优化,还有就是模板和换肤的一些思考。这些有待各位同仁的共同努力。