动画编辑器TimeLine动作流设计与实现

背景

最近在着手绘本课堂的设计与实现,中间有不少困难点需要攻克,大致分为以下几个模块:
– 容器配置化、工程化。
– 组件的集中管理,插件机制。
– 场景的切换和设计。
– TimeLine动作流处理。
– 多端同步的处理。
以上几点中,对于TimeLine动作流的处理尤为重要,同时也是最不好做的一个模块,下面简述一下怎么对TimeLine动作流进行处理。

以下具体内容请上confluence进行访问阅读:动画编辑器TimeLine动作流设计与实现

小电影下载途径

小电影下载途径

标签(空格分隔): 网络 下载


背景

相信我们中的很多人都经历过一件事情,那就是使用迅雷下载一些资源(比如葫芦娃)到99.99%的时候被卡住的情况,那么在被卡住的时候大家都是怎么办的呢?一般来说,很多人都是不停的点暂停和开始,也有一些人非常恼怒的删了重新下载,但是在这个5g还没有到来的时代,相信这是非常恼人的,特别是重新下载的99.99%又卡住之后,恨不得砸了电脑。当然,也有一些人经过这些操作之后又下载完成了,那么这两者之间有什么区别呢?

手段

对于下载一些网络资源来说,一般有以下几种方式:
1. http
2. ftp
3. p2p
这三种分属于不同的应用层协议,一般来说,http是最简单的方式,就是通过一些浏览器直接下载,但是这种方式一旦下载过大的文件就会奇慢无比,并且因为有的浏览器并没有支持断点续传,一旦中断,简直天怒人怨。
ftp协议是专门用来下载文件的方式,称为文件传输协议,FTP 采用两个 TCP 连接来传输一个文件。这种传输方式相对稳定,可以比较快速的传输文件到客户端,但是不好的是,该种途径因为下载压力都承载在服务器端,一般会对服务器端造成过大的压力。
p2p协议是移动互联网还没有开始就兴起的一种应用层协议,该协议旨在解决单一服务器的带宽压力。p2p的全称应该是peer to peer,有很多朋友想当然的以为p2ppoint to point,那就错了,point to point的简称是PPP,即点对点协议,是为在同等单元之间传输数据包这样的简单链路设计的链路层协议。而p2p协议是peer to peer,即同等能力者的含义。当我们在迅雷上使用种子下载一些资源的时候,我们并没有连接到单一的服务器上,而是通过多个peer来共享资源,每个下载者都是一个peer,它们既下载一些资源,也会上行一些资源,每个人既是客户端,也是服务器。

迅雷种子有什么

从上面对p2p协议的分析,我们可能会想到,下载文件的时候怎么得知该文件存储在哪些机器上的,这就是种子的作用了,一般一个.torrent文件会由两部分组成,一部分是tracker Url,一部分是文件信息,其中文件信息主要是存储以下内容:

1. 文件与目录结构
2. 顶层目录名称
3. 每个段的大小
4. 每个段的hash值

tracker Url提供的则是tracker服务器的地址,该服务器可以通过请求者的参数来返回该资源在其他下载者的ip地址,通过该ip地址列表,下载者可以连接其他下载者,从而达到分担服务器压力的作用。但是这种方式并不是真正的去中心化,我们可以看到,每次下载者在开始一个种子下载的时候,都需要连接到中心服务器进行ip列表获取,然后才能开始下载,那么一旦中心服务器挂了,或者被墙了,那边就会造成种子失效。
为了避免上面的问题,又衍生出一种叫做DHT的去中心化网络。每个加入到该网络中的人都会负责去存储这个网络中的资源信息和其他成员的联络信息,相当于所有人构成了一个巨大的分布式数据库。如果想细致了解该去中心化网络,可以访问这里【一步一步教你写BT种子嗅探器】DHT篇
,可以看出,DHT其实与现在的区块链很像。

最后

那么现在可以解答一下为什么下载到99.99%会卡住的问题了。这是因为不论是依赖tracker,还是分布式哈希算法,p2p开宗明义就是要减轻server压力,把压力转嫁给client。
99.99%的定义,是最后校验出了错误,漏失某个文件。
而为什么会漏失呢?还是p2p开宗明义说的,把压力转嫁给client,当client流量压力过大而下线了,其他人就无法找到这个文件了,顶多DHT网络告诉每个client,漏失的文件在某个下线的人手上。你们就乖乖等他上线,并且把手上的99.99%分享给其他新来的new node吧。
为了解决这个问题,最早应该是迅雷,提供了p2sp,长效种子,让server也有义务承担一些压力。
至于迅雷为什么也会99.99%,应该是更进阶的技术问题了。比如下载网站专门防止迅雷盗链。你有张良计,我有过墙梯。

vue项目中降低耦合的方式

序言

最近在做项目的过程中碰到了这么一种场景:

    // base.vue
    <template>
        <div>这是一个基础组件</div>
    </template>
    <script>
        export default {
            created() {
                console.log('开始创建')
            }
            mounted() {
                console.log('创建结束')
            }
        }
    </script>

如上代码,base.vue文件在多个项目中都需要被引用, 这要求我们需要保持base文件的绝对纯净,不能存在任何非公共业务代码的污染。针对这种场景,我做了如下封装:

    // hoc.js
    export default function Hoc(WrappedComponent, options) {
        render(h) {
            const slots = Object.keys(this.$slots)
                .reduce((arr, key) => arr.concat(this.$slots[key]), [])
                .map(vnode => {
                    vnode.context = this._self;
                    return vnode;
                });

            return h(
                WrappedComponent,
                {
                    on: this.$listeners,
                    props: this.$props,
                    scopedSlots: this.$scopedSlots,
                    attrs: this.$attrs
                },
                slots
            );
        }
    }
    // app.js
    import Base from ’./base‘
    import hoc from ’./hoc‘
    const HocBase = hoc(Base)
    export default {
        HocBase,
        Base
    }

这样做的好处是,一方面我保持了base.vue文件的纯净,另一方面,我也满足了业务的需求,我把所有的业务场景都丢在了hoc中进行对base进行增强,并且可以同时抛出base和HocBase组件。但是很快,这种增强也满足不了了,下面我们先看一下,出现了什么业务场景,导致hoc也不好满足。

背景

接下来一个业务场景,我需要增加一个相关的业务模块,这个业务模块存在view层,也就是需要在template中进行反应,同时需要一些业务层的参数来控制该业务模块的初始化。初始化之后。很多子模块都需要对该模块进行调用。一开始我们是这样实现的:

    // base.vue
    <template>
        <div class='container'>
            <div class='base'>
                <comp1 class="component" :module1="()=>module1" ></comp1>
            </div>
            <module1 class="module1" :refs="module1">这是模块1</module1>
        </div>
    </template>
    <script>
        export default {
            created() {
                console.log('开始创建')
            }
            mounted() {
                console.log('创建结束')
            }
        }
    </script>

然后在comp1模块中这样调用module1组件实例:

    // comp1.vue
    this.module1.show()

很快,由于业务场景的增加,我们把base文件污染成下面的样子了:

    // base.vue
    <template>
        <div class='container'>
            <div class='base'>
                <comp1 class="component" :module1="()=>module1" ></comp1>
            </div>
            <module1 class="module1" :refs="module1">这是模块1</module1>
            <module2 class="module2" :refs="module2">这是模块2</module2>
            <module3 class="module3" :refs="module3">这是模块3</module3>
        </div>
    </template>
    <script>
        export default {
            created() {
                console.log('开始创建')
            }
            mounted() {
                console.log('创建结束')
            }
        }
    </script>

从这里可以看到,base组件已经不能当做一个基础类给其他组件进行调用了,因为它本身耦合了自己业务线的相关业务代码,本来我们对相关业务代码的处理是将其抽到高阶中进行增强,高阶增强该组件,要么使用render嵌套书写这些组件,要么引入jsx来处理,而且这两种方式都会使得组件实例的句柄在组件之间层层传递。那么有没有更好的更优雅一些的办法来处理这种业务性的增强呢?

解决思路

下面我们梳理一下需要解决的问题:
1. 需要对base进行业务层的增强,涉及到了view层,但是不想对base组件造成污染。
2. 需要使用增强的业务组件实例,但是不想层层传递。

解决思路:
1. 通过vue派生一个子类。
2. 将派生的子类挂载到在hoc增强层获得相关参数并挂载。
3. 通过原型继承的方式获取到该实例,并进行调用。

核心代码如下:

    import Vue from 'vue';
    import isVNode from './vdom';
    import store from '@/store/index';
    let instance;
    let instances = [];
    let seed = 1;

const WrapperComponent = function(component, container, options) {
    // server返回
    if (Vue.prototype.$isServer) return;
    // 判断options类型,对options进行处理
    options = options || {};
    if (typeof options === 'string') {
        options = {
            message: options
        };
    }
    // 关闭的自定义回调
    let userOnClose = options.onClose;
    let id = 'component_' + seed++;

    options.onClose = function() {
        WrapperComponent.close(id, userOnClose);
    };
    let wrappedConstructor = Vue.extend(component);
    instance = new wrappedConstructor({
        data: options,
        store
    });
    instance.id = id;
    if (isVNode(instance.message)) {
        instance.$slots.default = [instance.message];
        instance.message = null;
    }
    instance.vm = instance.$mount();
    let target = document.body;
    if (container) {
        target = document.querySelector(container);
    }
    target.appendChild(instance.vm.$el);
    instance.vm.visible = true;
    instance.dom = instance.vm.$el;
    instance.dom.style.zIndex = PopupManager.nextZIndex();
    instances.push(instance);
    return instance.vm;
};

export default WrapperComponent;

以上为包装组件的核心代码,然后我们可以在合适的地方进行调用,示例代码如下所示:

    // hoc.js
    let award = WrapperComponent(Award, '#elm_container');
    Vue.prototype.$award = award;

其实该方法就是重新生成一个vue实例,与原先的实例一样,通过该方式注册需要注意要把需要的库引入,比如store、vue-router等等。
到此为止,就可以既保持base的纯净,又对业务进行了增强。

Vue中jsx的使用

标签(空格分隔): Vue jsx


背景

目前的项目主要都是依赖在Vue框架下进行开发的,一般情况下,在Vue文件中都是使用template来进行编写代码,但是有一些需要定制配置的页面,这样写起来就特别蛋疼,比如下面的代码:

    import preview from './preview'
    const config = {
        preview: {
            name: '名称',
            preIcon: 'el-icon-view',
            dialog: {
                slot: [
                    { key: 'body', child: preview, data: {name: '预览'}},
                ]
            },
            click: () => {
                console.log('触发点击')
            }
        },
    }

上面是一个渲染 按钮 – 弹窗 的组件,点击按钮可以触发一个弹窗,dialog里面是弹窗的具体内容,其中slot是自定义渲染的内容,渲染一个自定义的插槽来满足定制化的业务场景。这种写法可以满足业务封装的需求,使得项目在一定程度上可以配置化编写,有利于后期项目的拓展和维护。但是其实我的preview功能可能仅仅是这样:

    // preview.js
    <template>
        <div>data.name</div>
    </template>
    <script>
        export default {
            props: {
                data: Object
            }
        }
    </script>

其实我仅仅需要的就是一个div标签而已,但是使用vue的template却只能新创建一个vue文件来进行引入。如果换成jsx那么我们直接就可以这样写:


const config = { preview: { name: '名称', preIcon: 'el-icon-view', dialog: { slot: [ { key: 'body', child: <div>预览</div>}, ] }, click: () => { console.log('触发点击') } }, }

这样就简单很多了,而且也不需要单独创建一个.vue文件。

引申

从上述可以看出,有些情况下vue jsx确实会带来一定的优越性(特别对于写惯了react的切图仔)。下面介绍一下写vue jsx的时候需要注意的点,以及与react的不同。

与react jsx的不同

  1. React中父子之间传递的所有数据都是属性,即所有数据均挂载在props下(style, className, children, value, onChange等等。
  2. Vue则不然,仅仅属性就有三种:组件属性props,普通html属性attrs,Dom属性domProps。

如下示例所示:

const ButtonCounter = {
  name: "button-counter",
  props: ["count"],
  methods: {
    onClick() {
      this.$emit("change", this.count + 1);
    }
  },
  render() {
    return (
      <button>You clicked me {this.count} times.</button>
    );
  }
};

export default {
  name: "button-counter-container",
  data() {
    return {
      count: 0
    };
  },
  methods: {
    onChange(val) {
      this.count = val;
    }
  },
  render() {
    const { count, onChange } = this;
    return (
      <div>


      </div>
    );
  }
};

通过上述我们可以区分出这几种属性的区别:

  1. 组件属性props:指组件声明的属性,即上述示例中声明的props: [‘count’]。

  2. 普通html属性attrs: 指组件未声明的属性,即上述示例中的type=”button”,该属性默认会直接挂载到组件根节点的上,如果不需要挂载到根节点,可声明 inheritAttrs: false。

  3. Dom属性domProps:指的Dom属性,如上述示例中的innerHTML,它会覆盖组件内部的children, 这类属性我们一般很少使用到。

抽象程度较高的动态属性

一般情况下,在已知属性和方法的时候,我们可以直接将属性值传入已经定义好的组件即可,但是如果想高度抽象来复用一些组件,那静态属性传入就不太合适了,这个时候一般使用如下方法将属性传入子组件:

    const dynamicProps = {
        props: {},
        on: {},
    }
    if(hasValue) dynamicProps.props.value = value
    if(hasChange) dynamicProps.on.change = onChange

本文关于vue jsx的介绍就到这里,如果想了解更多,建议查看官方文档。

使用vuex实现时间回溯功能

使用vuex实现时间回溯功能

标签: vue vuex 时间回溯


前言

最近在做一个类似PPT的编辑器的项目,产品要求对每次操作要做到记录和Undo、Redo功能。着手查了一下,目前基于Vue生态的事件回溯插件存在,但是体验起来都不太好,于是准备自己撸一个。

算法设计思路

vuex状态存储state

// vuex state
{
    count: 0,
}

更新state的mutation示例

// vuex mutation
const mutation = {}
mutation.updateCount = (state, payload) => {
    const { type, num } = payload
    const handler = {
        add: (state, num) => {
            state.count = state.count + num
        },
        sub: (state, num) => {
            state.count = state.count - num
        }
    }[type](num)
}

保存历史状态的数据结构 historyState

{
  counter: {
    past: [], // 用于保留过去的可撤销状态
    present: 0, // 当前状态
    future: [] // 撤销之后保留的未来状态
  }
}

当进行5次mutation.updateCount({type: ‘add’, num: 1})操作后,历史状态数据结构如下:

// historyState
{
  counter: {
    past: [0, 1, 2, 3, 4], // 用于保留过去的可撤销状态
    present: 5, // 当前状态
    future: [] // 撤销之后保留的未来状态
  }
}

执行2次撤销之后状态如下:

// historyState
{
  counter: {
    past: [0, 1, 2,], // 用于保留过去的可撤销状态
    present: 3, // 当前状态
    future: [5, 4] // 撤销之后保留的未来状态
  }
}

重新进行一次mutation.updateCount({type: ‘add’, num: 10})操作:

// 重新进行操作后,如果feture中存在状态,则需要清空未来的状态
// historyState
{
  counter: {
    past: [0, 1, 2, 3], // 用于保留过去的可撤销状态
    present: 13, // 当前状态
    future: [] // 撤销之后保留的未来状态
  }
}

ok,有了以上的数据结构,我们就可以着手开始基于vuex来设计undo、redo功能了。

基于vuex插件的设计

通过官网文档我们会知道 Vuex 的 store 接受 plugins 选项,这个选项暴露出每次 mutation 的钩子。Vuex 插件就是一个函数,它接收 store 作为唯一参数:

const myPlugin = store => {
  // 当 store 初始化后调用
  store.subscribe((mutation, state) => {
    // 每次 mutation 之后调用
    // mutation 的格式为 { type, payload }
  })
}
// 挂载到vuex实例上
const store = new Vuex.Store({
  // ...
  plugins: [myPlugin]
})

通过插件,我们可以对我们需要进行undo、redo的modules进行监听,并保存到history中:

const undoRedoPlugin = store => {
    undoRedoHistory.init(store);
    let firstState = cloneDeep(store.state);
    undoRedoHistory.addState(firstState);
    store.subscribe((mutation, state) => {
        const mutationType = ['editor', 'admin'];
        if (mutationType.includes(mutation.type.split('/')[0])) {
            undoRedoHistory.addState(cloneDeep(state));
        }
    });
};
const plugins = [undoRedoPlugin];

同样,我们需要一个类UndoRedoHistory来实例化为undoRedoHistory来保存我们的historyState:

import cloneDeep from 'lodash.clonedeep';
class UndoRedoHistory {
    constructor() {
        this.store = {};
        this.history = [];
        this.future = [];
        this.currentState = {};
    }

    init(store) {
        this.store = store;
    }

    addState(state) {
        this.history.push(cloneDeep(this.currentState));
        this.future = []
        this.currentState = state
    }

    undo() {
        this.future.push(cloneDeep(this.currentState))
        this.currentState = this.history.pop()
        this.store.replaceState(cloneDeep(this.currentState));
    }

    redo() {
        this.history.push(cloneDeep(this.currentState))
        this.currentState = this.future.pop()
        this.store.replaceState(cloneDeep(this.currentState));
    }
}

export default new UndoRedoHistory();

最终实现效果如下图所示:

前端页面调试——代理篇

在日常开发前端页面的过程中,往往需要在本地环境和线上环境之间进行切换,一方面是因为需要本地环境的代码,另一方面是因为需要后端的API线上接口。为了满足同时具有本地代码和线上接口这种需求,我们一般会配置线上地址代理到本地。

代理的两种方法

  1. 配置Nginx代理
  2. 通过一些代理工具(charles等)

配置Nginx代理(简要描述)

  1. 配置host映射,可以通过直接修改host配置文件,也可以通过工具编辑,一般我是使用Helm进行编辑映射。
  2. 安装并配置Nginx,并注意修改了nginx后需要重启nginx服务器。

通过代理软件charles进行调试

个人从开发前端页面以来,基本都是使用这种方式进行页面调试,主要是有以下几个原因:

  1. 图形化界面
  2. 可以截取http和https网络封包
  3. 支持网络过滤请求
  4. 支持移动端抓包(http,https)

前端调试的几种场景及解决方案

pc端调试线上页面(代理远程地址到本地服务器地址)

以调试互动课件页面拖拽4.0为例,该页面存在于课堂页面的iframe中,并且访问地址一般是这种结构:test.ipalfish.com/klian/proxy/courseware/sheet/logic/242077384040448/index.html?是一个动态的地址。而我在本地webpack起的服务器地址一般是localhost:8080/mouse.html,这时候我可以通过charles tools -> remote将远程地址代理到本地,如下图所示:

代理html文件

同时我们还需要代理静态资源文件:

通过这种方式我们可以一边访问本地代码,一边访问线上接口,达到本地调试的目的。

移动端调试H5页面

移动端的调试相对于PC端稍微复杂一些,主要有以下几种类型和步骤:

  • 普通浏览器端http类型H5调试

这种类型是移动端最简单的,通过以下步骤就可以进行:

  1. 保证手机和PC处于同一网段(即连接同一个wifi)。
  2. 在手机端打开手动代理,输入PC在网段内的IP地址,并输入端口为8888(一般8888是charles设置系统代理的端口)。
  3. PC端charles同意手机连接。
  4. PC端配置代理(即pc端调试线上页面所述过程)
  5. 手机端连接线上地址。

通过这种方式即可在PC上调试手机页面。如果需要debug的话,可以走下面的过程步骤:

android:

  • 手机端打开USB调试模式,并将待调试的页面在chrome上打开
  • 连接PC,打开PC chrome浏览器
  • 打开开发者工具,通过右下角的remote device可以查看移动端打开的页面列表,通过inspect可以进入调试的页面。
remote device
android inspect

ios:

  • 手机设置safari打开web检查器,并使用USB连接mac。
  • pc上打开safari,通过开发下面的设备来查看该设备打开的页面。
  • 点击页面进入并开始调试。
ios usb调试
  • 原生APP中对H5页面进行调试

在原生APP中对H5页面进行调试一般会比普通浏览器端http类型H5调试多个过程,就是需要原生开发者在你的设备上安装debug版本的APP,这样你就可以上述步骤进行调试了。

  • 移动端对https协议H5页面进行调试

日常开发中,越来越多的页面只支持https协议了,但是我们是没有办法将https的地址代理成http的,这种情况下,就需要先安装证书了:

  1. 通过charles安装ca,即Help -> SSL proxying -> install charles root ca来安装pc端的证书。
  2. 将手机连接到pc所在的wifi,并通过手动代理连接pc。
  3. 手机访问http://charlesproxy.com/getssl安装证书。
  4. 在charles上通过proxy -> ssl proxying setting 来设置代理的https host
ssl proxy

再完成上述步骤之后,再按照普通浏览器端http类型H5调试的步骤进行设置,即可完成配置。