jeremygo

jeremygo

我是把下一颗珍珠串在绳子上的人

Vue 可视化模块引擎

背景#

团队内营销活动及落地页需求迭代比较频繁,经过调研后在集团自研可视化平台基础上自建了更贴近团队业务特点的营销可视化搭建平台。在渲染层实现上,考虑到较多历史项目页面仍属于核心业务需要长期迭代维护,除了常规的页面框架渲染外还需要扩展支持模块级渲染,让历史项目能直接通过正常的组件形式低成本引入使用,因此设计实现了一个可视化模块引擎,已在团队内两个业务线项目中推广使用,特此总结记录一下。

具体实现#

首先考虑模块引擎的基础形态与功能:

形态上,团队内 C 端的 H5 项目技术栈都是 Vue,因此这个渲染引擎的基础形态就是一个 Vue 组件(从项目接入体验考虑,实际对外导出的是一个 Vue 插件,内部会全局注册上这个组件);

功能上,保证开箱即用的体验,至少应具备获取配置物料及渲染的闭环逻辑,因此渲染引擎需要知道配置物料对应的 json key、接入项目服务的环境(区分配置物料的请求地址)以及组件渲染源(即打包后的组件库代码),同时还需要考虑到业务项目与内部组件灵活地通信与交互,模块引擎需要支持任意的属性与监听事件透传;

最终对外的组件 API 设计如下:

Props:

属性说明类型
s支持直接传入 json key,模块引擎可据此获取配置物料string
env获取配置物料对应的请求环境 (测试、预发、线上)string
widgets组件渲染源,模块引擎解析配置物料时匹配对应组件处理function
props业务组件自定义属性,模块引擎会合并传入至所有配置组件下object

Events:
@getXpubData(data)
获取到配置物料后的回调函数,可以基于配置数据自定义处理逻辑
@xxx(...)
业务组件自定义事件,模块引擎会合并监听所有配置组件上抛的对应事件

至于 Vue 组件中常用的 Slot 插槽,因其作用是自定义组件内部特定位置的渲染内容,而这一部分由可视化编辑器完全接管,所以在此不考虑。

项目接入示例:

import Roo from '@vision/roo'
// 业务组件库需要自行安装,引入相应 js/css 源文件
import renderWidgets from '@vision-xxx/dist/h5/index.js'
import '@vision-xxx/dist/h5/index.css'

// 插件安装
Vue.use(Roo, {
  renderWidgets: [
    renderWidgets
  ]
})

// 全局组件使用
<template>
  <roo-component
    // 传入 json key 与相应环境由模块引擎内部请求获取
    s="op-json-xxx"
    env="production"
    // 传入的 props 会注入到内部使用的所有组件内
    props="{
      token: 'xxx'
    }"
    // 可监听组件内暴露的事件作相应处理,同名事件会重复触发
    @customEvent="eventHandler"
    // 模块引擎内暴露的物料配置接收事件
    @getXpubData="handlerXpubData"
  />
</template>

其次在核心的渲染逻辑上,将可视化编辑器发布产出的标准配置数据进行解析转换生成渲染函数,详细流程如下:

  • 首先会依据 json key 从 cdn 请求获取物料配置数据;
// 配置数据示例
{
  // 模块 id 
  "id": "000",
  // 页面标题
  "title": "问卷",
  // 通用的全局相关扩展字段
  "global": {
    "dir": "ltr",
    "code": {
      "js": "",
      "css": "",
      "jsForHeader": ""
    },
    "share": {},
    "mixins": {
      "globalmixin": {
          "pageStatus": 1
      }
    },
    "pubArea": "dpub",
    "canShare": true,
    "onlineUrl_json": "http://xxx.com/op-json-xxx.json"
  },
  // 页面组件、样式展示相关字段
  "scenes": {
    "music": {
      "url": "",
      "name": ""
    },
    // 顶部节点样式
    "style": {
      "overflow": "auto",
      "minHeight": 603,
      "backgroundSize": "100% 100%",
      "backgroundColor": "rgba(0,0,0,0)",
      "backgroundImage": "",
      "backgroundRepeat": "no-repeat",
      "backgroundPosition": "center center"
    },
    "dsList": [],
    // 组件列表配置
    "layers": [
      {
        // 组件 id、key
        "id": "widget-xxx",
        "key": "ManhattanQuestionnaireNps",
        // 组件名称
        "name": "小问卷 - NPS",
        // 组件元素类型
        "type": "ManhattanQuestionnaireNps",
        "label": "",
        // 组件传入属性
        "props": {
          "img": {
            "url": "https://xxx.com/xxx.png",
            "fileName": "nps背景.png"
          },
          "maxText": "10分肯定会推荐",
          "minText": "0分绝对不会推荐"
        },
        // 组件样式
        "style": {
          "top": 0,
          "left": 0,
          "color": "#333",
          "right": "unset",
          "border": "0px solid #000000",
          "boxShadow": "#000000 0px 0px 0px 0px",
          "marginRight": 0,
          "marginBottom": 0,
          "backgroundColor": ""
        },
        "id_name": "小问卷 - NPS3",
        "effectList": [],
        "editorStatus": {
          "show": true,
          "active": false,
          "isLock": false,
          "status": 0
        }
      }
    ]
  }
}
  • 依据配置数据中的页面状态字段 global?.mixins?.globalmixin?.pageStatus 判断是否展示,不展示则直接返回空节点(模块配置下线场景),展示则上抛 getXpubData 事件(见上文定义);
  • 结合 widgets 组件渲染源生成渲染函数返回:
    • 从配置数据中获取组件列表 layers,遍历增加传入的自定义属性与监听事件对象(当前组件上下文中的 listeners),分别挂载到每个组件对象的 propson 字段上,生成一个初始的顶部节点对象;
      // 顶部节点对象示例
      {
         type: 'div',
         style: {
           "overflow": "auto",
           "minHeight": 603,
           "backgroundSize": "100% 100%",
           "backgroundColor": "rgba(0,0,0,0)",
           "backgroundImage": "",
           "backgroundRepeat": "no-repeat",
           "backgroundPosition": "center center"
         },
         children: [
           {
             "id": "widget-xxx",
             "key": "ManhattanQuestionnaireNps",
             "name": "小问卷 - NPS",
             "type": "ManhattanQuestionnaireNps",
             "label": "",
             "props": {
               "img": {
                 "url": "https://xxx.com/xxx.png",
                 "fileName": "nps背景.png"
               },
               "maxText": "10分肯定会推荐",
               "minText": "0分绝对不会推荐",
               // 挂载的传入属性
               "token": "xxx"
             },
             // 挂载的事件对象
             "on": {
                "customEvent": eventHandler
             },
             "style": {
               "top": 0,
               "left": 0,
               "color": "#333",
               "right": "unset",
               "border": "0px solid #000000",
               "boxShadow": "#000000 0px 0px 0px 0px",
               "marginRight": 0,
               "marginBottom": 0,
               "backgroundColor": ""
             },
             "id_name": "小问卷 - NPS3",
             "effectList": [],
             "editorStatus": {
               "show": true,
               "active": false,
               "isLock": false,
               "status": 0
             }
           }
         ],
       }
      
    • 依据顶部节点对象生成 createElement 渲染函数字符串:
      • 首个标签参数区分 HTML 保留元素与组件元素两种类型;
      • 第二个参数数据对象将 propsstyleattrson 转成对应的字符串形式,其中 style 数值统一转换成 vw 基准,on 事件对象使用全局的自定义事件池进行事件注册及触发处理;
      • 最后的子级元素参数递归执行生成。
    • 将渲染函数字符串通过 new Function 生成函数声明并将 widgets 绑定至函数作用域上(匹配对应组件元素标签)后返回渲染函数。

最终返回的渲染函数交由项目中的 Vue 渲染器进行渲染展示。

项目中完整的配置接入及解析流程如下:

image

接入流程性能优化#

模块引擎可以接入到页面中的任意位置,默认由模块引擎内部闭环获取配置数据与渲染的流程如下:

image

对于性能要求非常高的页面,上面的流程导致组件整体的加载渲染时间过长,最终影响页面整体的可交互时长,因此在这种场景下需要考虑以更合理的方式做接入。

在高性能要求的业务项目中,我们提供了一套新的加载流程方案,将请求物料配置与各组件内请求业务数据提前至统一的页面初始化请求接口中调用,模块引擎只做静态渲染,因此对模块引擎新增扩展了一个 data 属性用于直接接收配置数据场景。

同时不同业务组件的请求接口与处理逻辑可能都会不同,我们也约定了一套通用的业务接口与数据处理函数的配置标准,对应实现了一个解析处理的 SDK,在初始化接口调用时直接调用该 SDK 即可。

获取数据前置后的渲染流程如下:

image

大部分业务项目也都存在前置初始数据获取节点,接入后整体可交互时长基本无影响。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。