这应该是最大的react组件了吧!?

#1

同学,web项目要组件化

jquery那个年代过来的人,组件化带来的感受是非常赞的,大家都知道,react项目是由很多小的组件组合构成的,小组件又组合成大组件,大组件组合成项目级别的组件,那组件也有大小,有生命周期,那么下面的 <App /> 也是一个组件。

render(<App />, document.getElementById('App'))

这里我们所指的组件是:可复用的组件,是通过 npm install xxx 安装使用的组件。

不小心就做了一个超级大组件

这次分享的项目是h5ds,一个超级大的react组件,可以通过npm install h5ds安装使用这个组件。目前来看,这应该是目前最大的React组件了吧。

1. 什么是H5DS?

H5DS (HTML5 design software)可以理解成一款做H5的在线工具,H5就是在手机上滑动的页面,像易企秀,百度H5,wps秀堂,Maka一样的在线工具。

最初我只是想做一艘独木舟,做着做着,感觉加个帆要好点,然后就一发不可收拾,不停的添加新的功能,独木舟变成了大船,最终变成了一艘战舰。

任何项目只要一进入迭代周期。哪怕是一个很小的项目,最终也可能成长成为一个庞大的项目,所以不要低估自己处理问题的能力,只要放手去干,总会有惊喜。

工具截图示例:

制作的H5示例:

接下来会讲解我开发这个项目的全部过程和技术方案。

2. 技术选型

技术选型原则:尽量以节约开发量为主,不重复造轮子,我们是为了生产一个成品,不是原料加工厂,有现成的组件就用上,有节约开发工作量的框架也用上。

【前端:】

React: 模块化开发少不了,angular,react,vue三选一,这里选择了react。

Jquery:虽然很多同学说这个已经淘汰了,不过对于小团队开发而言,开发资源是非常宝贵的,能一句代码解决的问题用两句代码来解决就是浪费研发资源。当然使用这个还有另外一个更重要的原因,为了支持海量的jquery插件,我们牺牲了一部分性能。

mobx: 从技术角度,经过我们分析的最佳方案并不是mobx,而是redux,redux更适合这样的工具项目,但是考虑到代码量和二次开发的成本,我最终选择了mobx,对于使用过vuex的用户而言,mobx也方便掌握,也很容易从vue转到react项目中来。

less: 没有选择sass的原因主要是因为node-sass包不是很稳定,经常出错,当然less已经能满足我们的需求了。

antd:我们不造轮子,这样的工具项目对组件的需求是非常大的,有现成的优秀的react组件库当然要用起来。没有的自己再封装一些就可以了。

loadsh:工具类项目对数据的处理是非常多的,这里用到了loadsh里面的一些方法去处理数据。

【后端:】

koa:后端语言采用nodejs,koa文档和学习资料也比较多,express原班人马打造,这个正合适。

mysql:免费的关系型数据库,这个算是比较常规的了。

Sequelize:Sequelize是一款基于Nodejs功能强大的异步ORM框架,既然有现成的,我们也不自己去封装了。工作量能节约就节约。重心放到业务上。

3. 系统架构方案

需求用一句话概括:编辑器生成H5页面,可以在手机端打开。

那么编辑器和H5页面应该是分开的两个项目,H5里面有很多模块模块(plugins插件),比如图片,文本,形状,视频,音乐等,后续可能还会新增其他插件,所以这块业务就必须是可扩展的。因为我们采用mobx管理数据,数据我们采用json数据,这里我们至少有三种方案:

  1. 编辑器生成JSON数据,服务端根据JSON数据生成HTML代码提供H5预览页面。

image

优点:服务端直接返回HTML页面,可以做SEO

缺点:插件需要在服务端加载使用,脱离服务端将没法跑起来,如果访问量大,因为服务器压力比较大。

  1. 编辑器直接生成HTML代码,服务端将HTML代码另存为HTML文件,返回URL提供访问。

image

优点:服务端直接返还HTML页面,可以做SEO,服务端访问压力小,可脱离服务端独立运行

缺点:数据不够灵活,只能通过编辑器修改后重新发布才可以,后续升级预览页面的代码没法做到同步升级。

  1. 编辑器直接生成JSON数据,服务端只负责存取JSON数据,渲染交给前端处理。

image

优点:服务端压力小,可脱离服务端独立运行,数据灵活,能实现模版同步升级

缺点:不支持SEO

综合评估,我们选择了第三种方案,预览页面直接依赖插件,优点明显,后续可升级的空间大。如果要做SEO,也可以做SSR,但是目前对SEO的需求不是很大,因为主要是微信H5为主。

4. 数据结构

架构方案确定后,数据结构也是非常重要的,提供H5页面需求来看,数据结构大致是这样的:

{
    ...infos, // 记录H5的信息,名称,主图,描述
    ...options, // 记录H5的配置信息,滑动效果,类型等
    pages: [ // 记录页面数据
        {
            ...infos, // 页面信息
            ...options, // 页面配置
            layers: [...] // 记录页面的图层信息
        }
    ]
}

整体看上去数据结构就非常清晰了,图层和页面的概念也是H5的核心。

一个页面由多个图层构成,而图层又有多个种类(图片,文本,形状,视频,音乐),也就是之前说的可扩展插件(plugins插件)

实际上数据结构是非常复杂的,我们的数据如下:

h5ds json数据结构v1.0版本

appJSON数据结构:

{
    version: 5.0.0, // 当前的H5DS版本
    img: 'http://cdn.h5ds.cn/static/images/img-null.png', // 主图
    desc: '点石H5,官方网站h5ds.cn', // 描述信息
    name: '点石H5', // 应用名称
    type: 'phone', // h5类型  phone or pc
    slider: { // 全局的翻页设置
      speed: 0.5, // 切换速度
      effect: 'slide', // 翻页动画 slide, fade, coverflow
      autoplay: false, // 是否自动翻页
      time: 5 // 自动翻页时间
    },
    style: { // app的样式
      width,
      height
    },
    fixeds: [ // 浮动层有两个,不可删除和添加。上层浮动和下层浮动
      {
        id: null, // div.id
        className: null, // div.class
        keyid: util.randomID(), // keyid 是一个不重复的随机数,相当于是id
        name: '浮动层上', // 名称
        desc: '页面描述',
        style: { // 浮动层的样式
          height,
          width
        },
        layers: [ layerJSON ] // 浮动层中的layer集合
      },
      {
        id: null,
        className: null,
        keyid: util.randomID(),
        name: '浮动层下',
        desc: '页面描述',
        style: {
          height,
          width
        },
        layers: []
      }
    ],
    popups: [ pageJSON ], // 弹窗数据集合,
    pages: [ // 页面数据集合
      {
        id: null,
        className: null,
        keyid: util.randomID(),
        name: '页面名称',
        desc: '页面描述',
        style: { height, width }, // 页面样式,background-color样式会单独在外层div渲染
        layers: [ layerJSON ], // 当前页面的图层
        slider: { // 当前页面的翻页设置
          autoplay: false,
          lock: false,
          time: 5
        }
      }
    ]
  }

pageJSON 数据说明:


{
    id: null, // 页面id
    className: null, // 页面 class
    keyid: util.randomID(), // 唯一标识
    name: '页面名称',
    desc: '页面描述',
    style: { height, width }, // 页面样式,background-color样式会单独在外层div渲染
    layers: [ layerJSON ], // 当前页面的图层
    slider: { // 当前页面的翻页设置
      autoplay: false,
      lock: false,
      time: 5
    }
}

layerJSON 数据说明:


// 每个layerJSON数据是不一样的,他们都遵循一定的规则,data参数是不一样的

{
    version: '1.0.0', // 插件版本号
    name: '地图插件', // 插件名称
    pid: 'h5ds_map', // 插件的id
    id: null, // 图层的id
    className: null, // 图层的class
    set: { hide, lock, lockWideHigh, noEvent }, // 锁定,隐藏, lockWideHigh锁定宽高比 等设置, noEvent 表示可以事件穿透
    animate: [ animateJSON ], // 图层的动画,可以支持多个动画
    data: {...}, // 组件差异化相关的数据存放位置
    style: { width: 100, height: 100, top: 0, left: 0 }, // 图层组件的外层默认样式
    estyle: {}, // element div的样式,style样式只有4个参数,其他的样式均写到estyle中,比如背景,因为动画参数设置在element div上,所以这里不能设置 transform样式
    events: [ eventJSON ] // 事件配置数据,每个图层可以添加多个事件
}

5. 技术难点

  1. 数据管理:

我们的数据丢到mobx进行管理,数据变化,直接更新视图,这个很vue的数据管理有点相似。里面会涉及到很多数据问题。这时候就需要我们去定义一些全局的方法。我定义了一个h5ds的store,在store里面保存了两个数据(edata, data),其中edata是 editor data的简写,用于记录用户在操作编辑器的交互数据。比如当前选中了哪个页面,哪个图层,以及一些全局的配置参数。data则用于保存H5的json数据。为了保证浏览器突然崩溃,导致用户数据被清除,我做了历史操作数据,会把编辑的数据保存到localstorage。

  1. 撤销回退

操作记录是保存了当前的edata和data数据到内存,为了节约内存,只保存了20次最新的操作,随时可以通过撤销回退到上一步操作。如果使用了redux,天然数据回滚,非常方便。

  1. 数据更新

在编辑器内部数据更新是非常频繁的,因为数据嵌套太深,mobx的proxy监听数据是不会去监听对象或者数组内部的数据,所以需要手动触发视图更新,这里写了一个全局的方法updateCanvas()去强制更新整个视图,为了做性能方面的考虑,在拖动位置或者修改大小的时候,只去修改某个图层的视图。每个layer有一个key,通过修改这个key可以实现单个组件的更新。

  1. 图层插件的打包

图层插件在编辑器中和预览页面都会用到,这里就会涉及到复用了,我们把插件分为Layer.js 和 Editor.js 。其中 Layer.js 是在编辑器和H5预览页面都会用到,Editor.js 只在编辑器中用到。为了减少预览页面的代码,我们单独打包了2份UMD包,在不同的地方去使用,最初我们采用requirejs去管理UMD包,但是后来发展requirejs有各种问题,可能和一些第三方的包冲突,所以我们把插件直接挂载到window对象下面,使用H5DS_GLOBAL对象存储起来,虽然很暴力,但是这种方法真的非常实用。

  1. 拖动缩放旋转

针对拖动缩放旋转,这块很有意思,如果用react的方式,会在每个选中的元素外面包裹一层拖动的组件,如果是用判断去动态加载这个拖动组件,图层一定会被更新的,所以我们用了一种很巧妙的方法,用jquery封装了一个托拉拽的插件,在componentDidMount里面去绑定事件,初始化这个插件,这种方法虽然是不被推荐的,可以用奇巧淫技来形容吧。但的确大大减少了我们的维护成本。用起来也非常方便。

6. 性能优化

我们可以结合防抖函数去做性能优化,控制或者选择性的去更新视图。下面举个例子:

import React, { Component } from 'react';
import { inject, observer } from 'mobx-react';
import debounce from 'lodash/debounce';

@inject('h5ds')
@observer
class Demo extends Component {
  constructor(props) {
    super(props);
    this.state = {
        count: props.h5ds.count  // 默认是1
    }
  }
  
  // 防抖函数控制性能
  updateOtherRender = debounce(() => {
      const { count } = this.state;
      // 如果大于10才会去更新其他地方的视图
      if(count > 10) {
        this.props.h5ds.count = this.state.count
      }
  }, 500)
  
  changeValue = e => {
      this.setState({count: e.target.value}, this.updateOtherRender)
  }
  
  render() {
      return <input type="number" value={this.state.count} onChange={this.changeValue}/>
  }
}

我们在很多地方都有用到上面这种写法,react提倡的最小模块化,我们也希望模块之间的影响会最小,如果一个参数在多个模块中被使用,在快速输入的时候务必会变的很慢。

7. 全终端适配方案

手机的分辨率太多了,要兼容全部机型,一种兼容方案是远远不够的,这里我提供了多种兼容方案。

第一种: 等比缩放

我们的默认宽度高度是 320px * 514px,然后进行缩放,自动撑满高或者宽度,如图所示的阴影部分,当然上下或者左右可能预留一部分边框没有任何显示。

第二种:全屏背景

因为会存在上下或者左右有间隙的情况,这时候我们把背景颜色做全屏处理,比如红色是背景色,如果高度超过了,我们的H5页面会自动出现滚动条。

第三种:吸附定位

吸附定位这个名词是我自己取的,有时候要兼容到iphoneX是很麻烦的,吸附定位表示可以吸附到顶部或者其他相对位置,我们的吸附定位提供了8个位置可以吸附。吸附后,会以相对window的位置进行定位,而不是相对阴影的定位。比如针对小红心:

开启吸附前:

开启左上角吸附后:

8. 如何使用 H5DS

  1. npm install h5ds --save安装依赖包。
  2. webpack配置:
externals: ['React', 'ReactDOM', 'ReactRouter', 'ReactRouterDOM', 'mobx', '_', 'antd', 'PubSub', 'moment']
  1. 使用h5ds

html模版:

<!DOCTYPE html>
<html>

<head lang="zh-cn">
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="description" content="">
    <meta name="keywords" content="">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>H5DS5.0</title>
    <meta name="renderer" content="webkit">
    <!-- No Baidu Siteapp-->
    <meta http-equiv="Cache-Control" content="no-siteapp" />
    <meta name="apple-mobile-web-app-title" content="yes" />
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta http-equiv="Cache-Control" content="no-siteapp" />
    <link rel="shortcut icon" href="/assets/images/favicon.ico">
    <link rel="stylesheet" href="https://at.alicdn.com/t/font_1160472_ybl2xl0ao8.css">
    <link rel="stylesheet" href="https://at.alicdn.com/t/font_157397_ujac0trx9i.css">
    <link href="https://cdn.bootcss.com/antd/3.23.0-beta.0/antd.min.css" rel="stylesheet">
    <link href="https://cdn.h5ds.com/lib/plugins/swiper.min.css" rel="stylesheet">
    <script src="https://cdn.h5ds.com/lib/plugins/swiper.min.js"></script>
    <script src="https://cdn.h5ds.com/lib/plugins/jquery.min.js"></script>
    <script src="https://cdn.h5ds.com/lib/plugins/h5ds.vendor.min.js"></script>
    <script src="https://cdn.bootcss.com/moment.js/2.24.0/moment.min.js"></script>
    <script src="https://cdn.bootcss.com/antd/3.23.0-beta.0/antd.min.js"></script>
  </head>

  <body>
    <div id="App"></div>
  </body>
</html>

react代码:

import 'h5ds/editor/style.css';
import { render } from 'react-dom';
import React, { Component } from 'react';
import H5dsEditor from 'h5ds/editor';

class Editor extends Component {
  constructor(props) {
    super(props);
    this.state = {
      data: null
    };
  }

  /**
   * 保存APP
   */
  saveApp = async data => {
    console.log('saveApp ->', data);
  };

  /**
   * 发布 app
   */
  publishApp = async data => {
    console.log('publishApp ->', data);
  };

  componentDidMount() {
    // 模拟异步加载数,设置 defaultData 会默认加载一个初始化数据
    setTimeout(() => {
      this.setState({ data: 'defaultData' });
    }, 100);
  }

  /**
   * 使用编辑器部分
   */
  render() {
    const { data } = this.state;
    return (
      <H5dsEditor
        plugins={[]} // 第三方插件包
        data={data}
        options={{
          publishApp: this.publishApp,
          saveApp: this.saveApp, // 保存应用
          appId: 'test_app_id' // 当前appId
        }}
      />
    );
  }
}

// render
render(
  <Editor />,
  document.getElementById('App')
);

结束

最后感谢各位的阅读!

我们的官方网站是:http://www.h5ds.com

我们的git地址是https://github.com/h5ds/h5ds

工具编辑器发布目前已经比较成熟,还在迭代中,我们希望能有更多的开发者能参与进来,开发插件,让H5DS编辑器能给各种领域带来便利。

技术交流QQ群:549856478

如果你觉得有错误的地方,或者有任何好的建议,欢迎issue我们!

2 Likes