前端页面可视化搭建实践

3,661 阅读5分钟
项目背景

  电子相册,一个可以可视化编辑的H5网页,用户只需要选择相应的模板进行组合,替换页面里的素材就能制作出一套可以媲美专业人员进行开发的H5宣传页。

  使用者是没有任何开发经验的用户,这种情况对系统的可用性和拓展性的要求非常的高,因为每个人的想法是不一样的,这样就导致系统对接的需求是多变的,系统必须要有非常高的可拓展性,才能非常灵活的适应用户的需求。所以我们应该怎样去设计这个系统呢?用玩具做一个比喻,系统的用户可以用孩子来比喻,每个孩子对玩具的喜好不一样,我们不能提供一个做好的玩具给所有的孩子,而是应该提供一些模块给他们让他们自由发挥,组装自己喜好的玩具;所以系统的设计应该像乐高积木一样,供用户自由的去搭建和组装自己喜欢的玩具,而不是做一个现成的玩具给用户。

编辑器的架构

系统的核心流程图

思路分析:用户通过编辑器将编辑好的数据存到服务端,C端的浏览页面只需要一个容器,可以理解为一个div,在加载页面的时候,调用后端接口异步的去获取页面配置,页面配置单纯的就是个Json字符串。配置数据取出来之后,渲染引擎开始解析Json,最后再通过Vue组件的Render方法渲染页面。

思路很清晰了,但如何实现呢,我提炼出了几个核心的问题。

  • Json格式如何定义?
  • Json如何和组件对应起来?
  • 组件是怎么渲染出来的?
  • 组件间如何通信?
基础组件

  组件是页面里最小的单位,比如一个按钮组件,有不同的颜色,大小等等可以进行配置的属性。

// button.js

export default {
  name: 'button',
  render() {
    const { 
      color,
      textAlign,
      backgroundColor,
      fontSize,
      lineHeight,
      borderColor,
      borderRadius,
      borderWidth,
      text 
    } = this;
    const style = {
      color,
      textAlign,
      backgroundColor,
      fontSize: fontSize,
      lineHeight: lineHeight + 'em',
      borderColor,
      borderRadius: borderRadius + 'px',
      borderWidth: borderWidth + 'px',
      textDecoration: 'none'
    };

    return (
      <button style={style}>
        {text}
      </button>
    )
  },
  props: {
    text: {
      type: String,
      default: ''
    },
    vertical: {
      type: Boolean,
      default: false
    },
    backgroundColor: {
      type: String,
      default: "#ffffff"
    },
    color: {
      type: String,
      default: "#ffffff"
    },
    fontSize: {
      type: Number,
      default: 14
    },
    lineHeight: {
      type: Number,
      default: 1
    },
    borderWidth: {
      type: Number,
      default: 1
    },
    borderRadius: {
      type: Number,
      default: 4
    },
    borderColor: {
      type: String,
      default: "#ffffff"
    },
    textAlign: {
      type: String,
      default: "center"
    }
  }
}

将这些属性抽离出来后就形成了系统里最小的数据单元。

数据组装和分类

俗话说的好,人不可能一口吃成胖子,数据的组装也是,需要分步骤,分类别地来组装。 这是比较复杂的一个模块,页面的交互非常的多,细节也非常的多。数据组装模块承载的主要功能是根据用户的操作输出一份H5页面的配置数据,这份配置数据最终由渲染引擎来处理。主要有三个核心的组装步骤:

  • 将多个不同功能的组件组装成单个的页面(element.js)
  • 将多个页面组装成一套模板(page.js)
  • 将多个模板进行自由切换(template.js)
组件标准化设计

组件的组装需要有具体的规则,不能随随便便的把一堆数据组合到一起,所以我们需要先对各个组件做一个标准化的设计,用一个约定好的JSON数据格式来装载数据。

这里总共定义了三个基础的工厂类,来按照一定的规则组装数据,输出标准化的组件。 下面来说说这三个类分别的作用

element.js 元素类就是一个组件工厂,把定义好的基础组件和JSON格式的配置项放进去,它会返回一个定制的标准化的组件。

// element.js

class Element  {
    constructor (ele) {
    this.name = ele.name;
    this.uuid = ele.uuid || +new Date();
    this.pluginProps = this.getPluginProps(ele);
    this.commonStyle = this.getCommonStyle(ele);
    this.animations = ele.animations || [];
  }
  ...
}

export default Element;

page.js 页面类,用于将各种组件组装成页面。

// page.js

class Page {
  constructor (page = {}) { 
    this.uuid = page.uuid || +new Date() + parseInt(Math.random() * Math.pow(10,8));
    this.pageId = page.pageId;
    this.elements = page.elements;
    this.coverUrl = page.coverUrl || '';
  }
  ...
}

export default Page;

work.js 作品类,所有的页面的集合,包括所有的页面、作品封面、作品名称、是否发布等信息

// work.js

class Work {
  constructor (work = {}) {
    this.title = work.title || '标题';
    this.templateId = work.templateId;
    this.description = work.description || '描述';
    this.sourceType = work.sourceType;
    this.pages = work.pages || [new Page()];
    this.coverImageUrl = work.coverImageUrl || '';
    this.coverMusicUrl = work.coverMusicUrl;
    this.isPublish = work.isPublish || 0;
  }
  ...
}

export default Work;
数据生产和组装

基础的数据结构已经设定好了,接下来就是根据用户的操作,进行数据的生产和组装了。

技术点(组件通信):

  • vuex:用来同步和保存各个组件的数据。
  • event bus:监听组件派发的事件,执行相应的函数。

vuexevent bus能简化组件间的通信,最大程度的降低组件之间的耦合。 使用vuex还有一个好处,它可以将所有的数据聚合到一起,方便形成一个配置文件。想想看,你如果把数据放到组件内部通过props或者event来传递的话,会非常的松散,最后还需要花很大的力气把数据进行聚合形成配置文件。

// store
...
const state = {
    ...
    work: { pages: [] },
    editingPage: { elements: [] },
    editingElement: null,
    ...
}
...
页面渲染引擎

  页面渲染引擎是系统最核心的部分,而render函数又是渲染引擎最核心的部分,vue的render函数接收三个参数,一个是需要创建的HTML的节点的名称、组件选项对象或者一个函数,另一个是节点需要的属性即json格式的配置文件,还有一个是子集虚拟节点。

技术点:

  • render 函数
// 需要编译器
new Vue({
  template: '<div>{{ hi }}</div>'
});

// 不需要编译器
new Vue({
  render (h) {
    return h('div', this.hi)
  }
});
render(h) {
  return (
    ...
    const data = {
      ...
      class: 'element-on-edit',
      props: element.getProps(),
      on: {
        input: ({ value, pluginName }) => {
          if (pluginName === 't-text') {
            element.pluginProps.text = value;
          }
        },
        move: ({ value, pluginName }) => {
          if (pluginName === 'm-image-box') {
            element.pluginProps.pos = value;
          }
        }
      }
    };
    {h(element.name, data)}
    ...
  )
}

页面配置实时预览

页面的预览页和编辑页的主要区别是预览页不用响应用户的操作,只是数据的渲染

renderPreview (h, elements) {
  return (
    <div style={{ height: '100%', position: 'relative' }}>
      {
        elements.map((element) => {
          return (
              <div>{h(element.name, element.getPreviewData())}</div>;
          )
        })
      }
    </div>
  )
}
总结
  • 用工厂模式来生成标准化的json数据
  • vuexevent bus来简化组件之间的通信,聚合用户编辑过程中产生的数据,形成配置文件
  • 用render函数来解析json格式的配置文件,渲染页面