SunXinFei / sunxinfei.github.io

前后端技术相关笔记,已迁移到 Issues 中
https://github.com/SunXinFei/sunxinfei.github.io/issues
32 stars 3 forks source link

前端可视化搭建二三事 #26

Open SunXinFei opened 4 years ago

SunXinFei commented 4 years ago

先说下大前端与前端赋能,这里有一篇winter的文章,里面有一个观点很有趣,就是大前端并不是抢其他工种业务,而是一种赋能,那我们总结下前端赋能的案例,进行理解: 市面上有效赋能的产品或工具:

SunXinFei commented 4 years ago

可视化搭建的前世今生

页面可视化搭建,最早可以追溯很远,耳熟能详的就是Dreamweaver,以及后来基于jquery等前端搭建的项目,随着web开发的发展,前后端分离与数据驱动DOM动态页面,逐渐成为历史。

页面构成

我们认为一个页面简单的说是由HTML结构与数据组成静态页面,配合一些逻辑变成为动态页面。(css样式,在这里我们归入Data一类中) image

页面组件化

image 页面经过渲染之后,是 HTML 元素构成的树形结构,其中的结点便是我们前端经常提到的组件,主要实现了功能封装和可复用的两个功能。数据来源主要为内部维护变量和外部传入变量。 附:组件化还是要重提,主要是因为组件化的存在,才让现在的搭建器发挥得更优。

落地页开发痛点

活动页面特点

前端业务中, 经常需要开发产品介绍页/营销页/活动页/图片展示页等页面. 这类需求有以下几个特点: 页面类似: 同行业的落地页页面布局和业务逻辑较固定. 需求高频: 不同的人员每周会有多个这种需求. 迭代快速: 开发时间相对较短, 上线时间紧. 开发性价比低: 开发任务重复, 俗称画页面,消耗各方的沟通时间和人力.

常规开发流程

流程:

  1. 运营/产品提出页面需求prd文档.
  2. 开发根据UI设计稿完成页面开发.
  3. 测试进行页面测试.
  4. 运维进行页面上线.
  5. 运营/产品进行线上验收.

痛点:

  1. 运营/产品提出页面需求prd文档.
  2. 运营/产品在搭建系统中选取合适的模板进行页面搭建.
  3. 页面自动化发布上线, 页面需求完成, 流程完结.
  4. 如果运营/产品没有找到合适的模板.
  5. 开发进行页面模板开发, 并将页面模板添加到搭建系统中. 运营/产品继续流程2. 随着搭建系统时间的推移,会沉淀出越来越多的组件/模块提供使用,对开发的依赖越来越低。

参考: 页面可视化搭建工具技术要点 页面可视化搭建工具前生今世

SunXinFei commented 4 years ago

关于搭建框架

目前一些旧的搭建平台,组件、搭建器、配置项,这三个耦合度非常高,导致后期拓展性很差。举例:当要添加一个例如视频之类的简单组件,如果需要动组件列表、搭建器、配置项这三个地方的代码,则可以断定耦合度非常高了。 引用一段话

任何一个有一定复杂度、会持续增长的应用最重视的,其实并不是开发速度,而是可维护性和可扩展性。 这也是框架设计者们摆在首位的事情。可扩展性的好坏取决于框架的扩展机制。在我们的上面的设计中需要扩展的有两部分,组件和功能。组件的扩展可以通过允许用户提交自定义组件来实现。功能的扩展主要由框架开发者完成。 --侯振宇

在文章页面可视化搭建工具技术要点中提到框架要解耦是最优解,而大部分搭建项目其实确实是服务于业务线,而导致react的落地页,搭建平台也是react语言,如果要实现框架与组件的解耦可行性也是有的,因为我们不管渲染层面是什么语言其实搭建器产生的都是一段段json数据,所以关注点又回到了搭建器的操作台的组件预览,是否需要用搭建器的语言再实现一遍,而且配置区域的数据变化就不能为redux或vuex的语法,从这两点考虑,框架与组件解耦就需要值得商榷了。但是后面随着微前端等前景的发展,未来还是可以完美实现的。 所以除了框架解耦不再讨论之外,下面会从组件和配置项两个纬度讨论解耦的事情。

可视化工具结构

我们以一个开源的工具为例: image 其实可视化搭建工具的结构一般分成四块,如图所示,按照数字对应关系分别为:

组件列表

组件列表中的组件主要分为三类:

关于组件市场的思想如下图: image 所以组件市场中的组件和普通组件差别不大,属性配置按照规定的地方进行书写,渲染时动态加载第三方的js与css文件即可;

{
    "type": "comp-feed",
    "version": "0.0.22",
    "properties": {
        "title": {
            "type": "String",
            "name": "用户名",
            "value": "测试一版"
        },
        "avator": {
            "type": "Image",
            "name": "头像显示",
            "value": "https://s.gravatar.com/avatar/518515a45a5c15165f559857c3d60a95?size=100&default=retro",
            "canDelete": true
        }
    },
    "event": {
        "qrShow": 453849
    }
}
loadResources(docData) {
      let head = document.getElementsByTagName("head")[0];
      let promiseList = [];
      docData.pages.forEach(item => {
        (item.children || []).forEach(val => {
          // 异步组件
          if (val.compType !== 1 && val.type) {
            promiseList.push(
              new Promise(resolve => {
                let tmpSrc =`https://unpkg.com/ ${val.type}${
                  val.version ? `@${val.version}` : ""
                }/dist/js/index.js`;
                let script = document.createElement("script");
                script.type = "text/javascript";
                script.src = tmpSrc;
                script.onload = () => {
                  resolve();
                };
                head.appendChild(script);
              })
            );
            promiseList.push(
              new Promise(resolve => {
                let tmpSrc = `https://unpkg.com/ ${val.type}${
                  val.version ? `@${val.version}` : ""
                }/dist/css/index.css`;
                let link = document.createElement("link");
                link.href = tmpSrc;
                link.setAttribute("rel", "stylesheet");
                link.setAttribute("type", "text/css");
                link.onload = () => {
                  resolve();
                };
                head.appendChild(link);
              })
            )
          }
        });
      });
      Promise.all(promiseList).then(() => {
        this.pages = docData.pages || [];
      });
    }
上述代码自动加载如下的js和css文件
https://unpkg.com/comp-feed@0.1.3/dist/css/index.css
https://unpkg.com/comp-feed@0.1.3/dist/js/index.js

操作台/画布

这里主要集中以下操作技术:

多是基于组件data中top、left、height、width属性的对数据结构的遍历、计算与揉合,这里不多做赘述。

配置区域

整体思路就是配置区域对配置区域读/写,组件对配置数据读取,如下图: image 而组件通过props传入的数据驱动渲染: image 关于组件配置项的描述,为各组件差异化的配置数据定义数据结构和字段类型,我们这里用JSON表示, 因为其格式灵活,支持数据嵌套,前端友好。

image

如果为每个组件都编写一个表单页面,工作量较大;对于复杂的配置项,表单页面的编写工作量可能会大于页面组件的开发工作量。我们将配置项的颗粒度细化出来,比如配置项是"Text",我们就渲染出一个Input框,如果配置项是“Color”,我们则渲染出一个"Colorpicker",这个思想在JsonEditor云凤蝶项目中得到体现。 我们将配置项进行原子性的抽离,这样就会得到:

复杂数据类型和简单数据类型嵌套,基本可以满足大部分组件的配置。当然在实际的业务中可能出现一些特殊的要求或者UI设计师设计出特殊的配置项样式,我们可以将这一类归为业务数据类型,比如字体配置:Font,开发者单独开发,由于组件和配置项借助schema.json配置项解耦,独立开发Font的配置即可,配置项可插拔状态,拓展性很好,完全不依赖组件。 image

云凤蝶Schema JsonSchema

实时预览(兼容手机端)

现在预览状态兼容手机端展示主要是两种: 一种是百分比:百分比的这种目前遇到的不多,出现的场景一般是画布中每行元素固定,也就是画布中元素不支持任意位置拖拽; 一种是px转rem:px转rem这种方式目前比较常见,元素位置为absolute,将data中存储的px数值,计算为对应的rem值即可。但是注意一点,如果是fix的元素,在pc预览时,如果给内容区域设定了最大宽度,如下图的样式: image 这时候,就需要写一些特殊的代码来兼容fix属性的模块了:

//fix模块css的left计算
getLeft() {
      if (document.body.offsetWidth > 750) {
        return this.data.css.left + (document.body.offsetWidth - 750) / 4;
      } else {
        return this.data.css.left;
      }
    },
//fix模块计算上or下fix位置
    getTopOrBottom(data) {
      if (data.properties.fixPos == "top") {
        return `${data.css.top / 50}rem`;
      } else {
        return `${-data.css.top / 50}rem`;
      }
    }

同理在搭建操作台渲染fix类型的组件时,也需要特殊处理。

SunXinFei commented 4 years ago

相似项目汇总

行业竞品(排名不分先后)

开源系列:

form表单开源系列:

SunXinFei commented 3 years ago

补充-关于联动

{
  tmpSelect:{
    type:'SelectRadio',
    config:{
      options:[{
        id:'1',
        name:'aaa'
      },{
        id:'2',
        name:'bbb'
      }]
    },
    value:'1'
  },
  form1:{
    type: 'Input',
    config:{
      name:'表单1'
    },
    value:'表单value值',
    dev:{//联动控制
      show:{//控制是否展示 也可以为disabled等控制
        param1:{//变量
          filed:'tmpSelect',
          eval:'===',
          value:'1'
        },
        param2:{
          filed:'tmpSelect',
          eval:'!==',
          value:'2'
        },
        logic:' %{param1}% || %{param2}%' //条件:tmpSelect === 1 || tmpSelect !==2才展示
      }
    }
  }
}

如上示例,我们有一个tmpSelect的组件,组件表现形式是SelectRadio,有aaa和bbb两个选项,如果想通过tmpSelect来控制form1是否展示,则给form1添加控制属性dev,dev中有show/disabled等组件状态控制。