使用Vue.js开发在线 H5 编辑器

by 杨俊业

主要内容

  • 开发思路
  • 开发过程
  • 开发总结

开发思路

一个例子

分析

有很多页面 数组
页面有很多元素 数组的内容
元素有很多类型 不同的对象

生成 json 在线查看

{
  title: "我的h5作品", 
  description: "我的h5作品, 有没有觉得很高大上啊~快来膜拜我!", 
  icon: "/static/img/icon.png", 
  pages: [
    {
      style: {
        backgroundColor: "#50E3C2", 
        image: null
      }, 
      comps: [
        {
          type: "hy-text", 
          text: "wdshare", 
          active: false, 
          isShowView: true, 
          isShowEdit: true, 
          isShowAnimate: false, 
          animate: {
            name: "none", 
            duration: 1, 
            delay: 0
          }, 
          position: {
            left: 59, 
            top: 73, 
            width: 198, 
            height: 42, 
            transform: 0
          }, 
          style: {
            fontFamily: "Microsoft Yahei", 
            fontSize: "26px", 
            color: "#000000", 
            fontWeight: "bold", 
            fontStyle: "normal", 
            textDecoration: "normal", 
            textAlign: "center", 
            lineHeight: 1.5
          }
        }, 
        {
          type: "hy-image", 
          active: true, 
          isShowView: true, 
          isShowEdit: true, 
          imgSrc: "\\static\\upload\\wdshare-logo.png", 
          isShowAnimate: false, 
          animate: {
            name: "none", 
            duration: 1, 
            delay: 0
          }, 
          zIndex: 0, 
          position: {
            left: 30, 
            top: 155, 
            width: 248, 
            height: 81, 
            transform: 0
          }, 
          style: {
            opacity: 1, 
            borderRadius: 0, 
            boxShadow: 0
          }
        }
      ]
    }, 
    {
      style: {
        backgroundColor: "#8B572A", 
        image: null
      }, 
      comps: [
        {
          type: "hy-text", 
          text: "第二页", 
          active: true, 
          isShowView: true, 
          isShowEdit: true, 
          isShowAnimate: false, 
          animate: {
            name: "none", 
            duration: 1, 
            delay: 0
          }, 
          position: {
            left: 49, 
            top: 195, 
            width: 206, 
            height: 55, 
            transform: 0
          }, 
          style: {
            fontFamily: "Microsoft Yahei", 
            fontSize: "30px", 
            color: "#FFFFFF", 
            fontWeight: "bold", 
            fontStyle: "normal", 
            textDecoration: "normal", 
            textAlign: "center", 
            lineHeight: 1.5
          }
        }
      ]
    }
  ]
}

总结

  • 编辑JSON
  • 编辑过程可视化

开发过程

todoMVC demo

初始化data

new Vue({
  data: {
    slide: {
      title: '我的h5作品',
      description: "我的h5作品, 有没有觉得很高大上啊~快来膜拜我!",
      icon: '/static/img/icon.png',
      pages: [{
        style: {
          backgroundColor: '#ffffff',
          image: null
        },
        comps: []
      }]
    }
  }
})

增加页面

// 新增页面
addPage: function() {
  this.pages.push({
    style: {
      backgroundColor: '#ffffff',
      image: null
    },
    comps: []
  });
  // 设置新增页面为激活状态
  this.activePageIndex = this.pages.length - 1;
  // 触发滚动条
  this.activeIscroll();
}

删除页面

// 删除页面
removePage: function(index) {
  // 如果只有一个页面,禁止删除
  if (this.pages.length <= 1) return;
  this.pages.splice(index, 1);
  // 触发滚动条
  this.activeIscroll();
}

复制页面

// 复制页面
copyPage: function(index) {
  // 深度复制
  var newPage = $.extend(true, {}, this.pages[index]);
  // 插入到需要复制的下一页
  this.pages.splice(index + 1, 0, newPage);
  this.activeIscroll();
}

添加文本

addText: function(){
  this.currentPage.comps.push({
    type: 'hy-text',
    text: '请输入文本',
    active: false,
    animate:{
      name: "none",
      duration: 1,
      delay: 0
    },
    position:{
      left: 60,
      top: 200,
      width: 200,
      height: 24,
      transform: 0
    },      
    style:{
      fontFamily: 'Microsoft Yahei',
      fontSize: '16px',
      color: '#000000',
      fontWeight: 'normal',
      fontStyle: 'normal',
      textDecoration: 'normal',
      textAlign: 'center',
      lineHeight: 1.5
    }
  });
  var len = this.currentPage.comps.length;
  this.currentComp = this.currentPage.comps[len -1 ];
}

编辑文本元素


页面显示

“图片”元素显示


插件使用

iScroll 插件

// 触发滚动条
activeIscroll: function(){
  this.$nextTick(function(){
    if(this.iscroll) {
      this.iscroll.refresh();
    }
    else {
      this.iscroll = new IScroll(this.$els.tabPane, { 
        mouseWheel: true,
        scrollbars: true
      });
    }          
  });
},
addPage: function(){
  this.pages.push(defautPageSetting());
  this.activePageIndex = this.pages.length - 1;
  this.activeIscroll();
}

jQuery UI中的resize, drag 插件

ready: function(){
  var _this = this;
  if( this.enable ){
    $( this.$el ).resizable({
      handles: _this.handles,
      resize: function(evt, ui){
        _this.width = ui.size.width;
        _this.height = ui.size.height;
        _this.top = Number(ui.position.top.toFixed(2));
        _this.left = Number(ui.position.left.toFixed(2));
      }
    }).draggable({
      drag: function(evt, ui){
        _this.top = Number(ui.position.top.toFixed(2));
        _this.left = Number(ui.position.left.toFixed(2));
      }
    });
  }
}

“撤销”、“重做”功能

分析

  • 保留每次操作的快照
  • 撤销就是返回到上一个快照
  • 重做就是前进到下一个快照

问题

  • 怎么保存每个操作的快照?(重要)
  • 怎么返回或者前进一个快照?

问题 - 保存快照

  • 使用watch来监测JSON的变化
  • 使用vuex

watch 方法

  • 拖曳,放大等操作会导致产生大量的快照
  • 会产生不必要的快照

vuex 插件

快照的保存

// store 
const plugins = [function(store) {
  store.subscribe((mutation, state) => {
    H.hook.$emit('vuex:mutation', state)
  });
}]
// app 初始化
H.hook.$on('vuex:init', function(state) {
  H.slideHistory.push($.extend(true, {}, state));
  H.slideIndex = 0;
});
H.hook.$on('vuex:mutation', function(state) {
  H.slideHistory.push($.extend(true, {}, state));
  H.slideIndex = H.slideHistory.length - 1;
});
H.hook.$on('vuex:replace', function(index) {
  var slide = $.extend(true, {}, H.slideHistory[index])
  _this.$store.replaceState(slide);
});

控制快照的产生

$( this.$el ).resizable({
  handles: _this.handles,
  stop: function(evt, ui){
    _this.width = ui.size.width;
    _this.height = ui.size.height;
    _this.$dispatch('postion-change', {
      width: _this.width,
      height: _this.height,
      top: _this.top,
      left: _this.left,
      transform: _this.transform
    }, true);        
  },        
  resize: function(evt, ui){
    _this.width = ui.size.width;
    _this.height = ui.size.height;
    _this.$dispatch('postion-change', {
      width: _this.width,
      height: _this.height,
      top: _this.top,
      left: _this.left,
      transform: _this.transform
    });        
  }
});

快照的后退和前进

undo: function(){
  if (H.slideIndex > 0) {
    H.slideIndex -= 1;
    H.hook.$emit('vuex:replace', H.slideIndex);
  }
},
redo: function(){
  if (H.slideIndex < H.slideHistory.length - 1) {
    H.slideIndex += 1;
    H.hook.$emit('vuex:replace', H.slideIndex);
  }
}

开发总结

  • 数据驱动视图
  • 组件化(业务组件化,功能组件化)
  • 学会调试(chrome 开发这工具,Vue Devtools, debugger, 打断点)

一点经验

  • 先看文档
  • 写代码,要从小项目写起
  • 认真思考
  • 学会调试
  • 学会提问

源码地址

我之前写的一些小 demo

https://segmentfault.com/a/1190000006724976

Q&A

- EOF -