/ f2e

返璞归真的web设计--神笔海报/秒赞工作小结

注:这是我离开阿里之前最后的一个项目,原文发表在ATA,现在把文章拿到这里来^.^

点我看看秒赞是什么鬼

敲了半天的字,最终还是删掉了很多,很久没写点什么东西了,突然发现连开个头居然都这么困难。

不管是狭义上的前端,还是广义上的前端,或者是越来越多的希望向“后“靠的前端,这几年的新技术都是在爆发式的增长,吸纳新知识固然是一件好事,但不应仅仅停留在广度,或者仅仅成为谈资而已。

还记得上大学时CS的老师跟我们吹的牛,技术是什么?技术就是玩具,世界上一流的大脑把这个玩具精美的包装起来向你推销,等你逐渐开始会玩儿的时候,他们告诉你,对不起,这个已经过时了,要换新的么?

这个牛吹的恰不恰当暂且不说,不过也确实反映了从业人员在追逐新技术的过程中显得力不从心。我想说的是,抛开这些东西,有时返璞归真的看问题,反而能更加透彻。

本文是对目前正在做的一个项目的小结,没有任何新技术,也比较务虚,仅搏大家一笑。

允许我引用一句话开始

I hate working with technologies I don’t quite understand. Too often, it leads to code that just happens to work, not because you truly understand what it does, but because you went through a lot of trial and error to make it work

Introduction

神笔海报是一款在线编辑器,旨在帮助卖家通过傻瓜式操作,完成H5宣传海报的搭建工作,并且提供了一款设计师编辑器,帮助设计师尽快产出海报模板,同时结合移动千牛,闭合h5海报从模板产出--海报制作--海报分享的链路。

秒赞则是一款移动端编辑器,提供适宜移动端的编辑能力,二者从技术上可以做到模板互通,在一端编辑完即可再另一端继续编辑(貌似这个概念很流行?)

PC端的海报编辑器看起来长这个样子
Untitled

JSON or HTML

在开始任何工作之前,我们遇到了一个不大不小的问题,PC或者h5编辑器产出的产物到底应该是什么?

可以是H5页面或者是HTML片段吗?当然可以,而且这样似乎可以来的更快更省事,并且直出HTML肯定会让H5渲染端更好受一点,毕竟直接把原生的HTML片段扔到浏览器上性能会更好一些。

但是实际上,如果采用HTML的解决方案,会带来不少问题,比方一个典型的场景:

1.卖家使用编辑器拖拽了几个Tag组件(类似nice上的标签),然后产出HTML页面顺利发布
2.用户访问到这个页面,wow~ nice Tag,大家都非常满意
3.**这时候PD来了,宣布了一个重要的决定,“我们决定升级这个Tag组件,让它支持更炫更酷的效果”**
4.你心里咯噔了一下,妈蛋,要实现这个效果得改Tag组件对应的HTML呀
5.于是悲剧产生了,所有在你升级Tag组件之前的卖家都不能享受到这酷炫的效果了

当然,这只是一个简单的例子,说到底,如果我们采用了HTML的解决方案,就把编辑器和渲染端“绑死了”,再说到底,我们犯了一个常见的错误,即针对具体的实现编程

最终,我们设计了一套JSON中间语言,它描述了这套海报“有什么”,而不是“是什么”,“是什么”的工作被后置到了h5渲染端,交给了一个parser执行,parser的工作即是读取这套json,然后把它转换成html代码,将来如果有变动,我们只需要修改这个parser即可。

我们一共有3个同学参与这个项目,现在看起来,我们3个人的工作应该是这样
Untitled

事实上,最后我们甚至还放了一个parser在服务端上,一是可以减轻客户端渲染的压力,二是为了保护json模板的内容。

MVC,除了拿来主义,是否还有别的选择

so far so good,为了搞定这个编辑器,我们还需要一套MVC框架来帮我们支撑整个系统,这似乎不是什么太大的问题,成熟的MVC/MVVM之流框架当然很多,NG,RN都是很“高精尖”的东西。

事实上一年之前我曾经怀着崇敬的心情尝试着翻译Tero Parviainen的《build your own angularjs》一书,http://www.atatech.org/articles/23603这篇文章是我用自己的“土话”翻译并理解的chapter1,不可否认这是一本不可多得的好书,它帮助我更加顺畅的理解了NG的工作原理,但是这次,我们决定自己干!理由有二:

  1. 我们不得不自己干

    除了PC编辑器,我们还有一个h5编辑器,我们希望能在两端尽量达成复用的原则,比如组件或数据模型共享,如果在h5上引入一个NG或者RN,相信谁都有那么一点不太乐意。

  2. 我们更希望自己干

    如果你喜欢一个女孩,你一定是喜欢她的某些特质,那些说喜欢一个人可以从头发丝喜欢到脚趾头的同学,你们别扯了,心怀不轨吧?

    同样的,当我们谈论一个框架或者库很棒的时候,我们一定是爱上了它的某几个feature,而不是它的全部,与其深陷在ng的学习曲线上或者各种神奇的坑位里不能自拔的时候,条件允许的情况下,为什么不把你喜欢的feature抽离出来呢?

    注意,我并不是否认NG或者RN的价值和设计哲学,或者鼓励大家多造轮子。我只是认为一切抛开实际场景的“设计”或者“架构”都是耍流氓,“合适”才是真正的王道。

    再次,我们来看看tero这句很有意思的话,我认为说出了我的心声

    I hate working with technologies I don’t quite understand. Too often, it leads to
    code that just happens to work, not because you truly understand what it does, but
    because you went through a lot of trial and error to make it work

    这一次,我们只选对的,不买贵的。

    事实上,看到很多同学在RN的路上越走越畅通,我也非常希望“write once,run anywhere”的愿景能够实现,而不是重蹈java的覆辙。

当我和@诶史同学谈到这个问题的时候,我俩几乎是一拍即合,@诶史同学在之前的云盘项目中沉淀过一套可靠的设计思路,详细了解之后,除了这个框架暂时还不能支持复杂数据结构,其他部分几乎符合我全部的期望,有关这方面的内容,@诶史后面会详尽的介绍,这里我仅仅抛砖引玉,做一个简单的介绍(原谅我偷懒就不画图了,大家自行脑补)

实际上整套系统是基于一个非常简单但是可靠的设计模式---pub/sub模式上建立的,每一个controller分别持有不同的model,可以是一个,可以是多个,model通过controller里面的action进行更新,当model更新时,会派发一个事件(当然在ES6下不用人肉干这件事了),把变更的数据传进去,这时所有监听了这个model的事件的controller都会收到请求,进行对应的操作,比如更新view,并且只更新数据变化的部分,因此不存在NG的性能问题。

通过这种方式,理论上我们可以实现model与view的一对一,一对多,甚至多对一的关系,解决复杂场景下的问题应该不在话下。

实际上还有一些别的事情需要做,目前这个框架还只支持简单的数据结构,我为它增添了一些功能,使它能够支持复杂对象,包括嵌套对象和数组,完成之后大概是这个样子,举一个最简单的例子:

//这里是一个model,可以被继承或扩展
function Model () {
	this._prop('data',{
		person: {
			name: 'aa',
			age: 13,
			class: 'one'
		}
	});
}
//调用m.data()可以把数据取出来
//调用m.data({'person.name':'cc'})或者m.data({person:{name:'cc'}})可以写值,并fire一个事件
//当采用第一种设置值的方式时,我会把{'person.name':'cc'}转成{person:{name: 'cc'}}然后extend到原始数据中,实现model的更新。
var m = new Model();

//这里是第一个view
/*
* template像这样
* <div>
* 	<!--是否有ng-bind的感觉?-->
* 	<input type="text" value-key="person.name"/>
* </div>
*/
function ViewOne (m) {
	this.model = m;
	//tpl就是指上面的template
	this.$el = $(tpl);
	this.bindEvents();
}
ViewOne.prototype.bindEvents = function () {
	var self = this;
	self.$el.on('input', '[value-key="*"]', function () {
   		var obj = Object.create(null);
   		//向model set值之前,做校验,做format都随意
   		//尤其是ng的校验是个诟病,现在你可以使用市面上任何一款插件帮你做完这件事情
   		//好比是这样来更新model:m.data({'person.name':'cc'})
   		self.model(obj[$(this).attr('valueKey')] = $(this).val());
   })
}

//这里是第二个view
/*
* template像这样
* <div>
* 	<span text-key="person.name"></span>
* </div>
*/
function ViewTwo (m) {
	this.model = m;
	//tpl就是指上面的template
	this.$el = $(tpl);
	this.bindEvents();
}
ViewTwo.prototype.bindEvents = function () {
	var self = this;
	//model更新后会fire一个事件,这里只是简单监听并更新view
	self.model.on('dataUpdated', function (modifyData) {
		Object.keys(modifyData).forEach(function (key) {
			self.$el.find('[text-key="'+ key +'"]').text(modifyData[key]);
		});
	})
}
//v1,v2是分别持有同一个model的两个View,这是一个典型的一对多场景
var v1 = new ViewOne(m);
var v2 = new ViewTwo(m);

我们实现了一个广义上的mvc系统
Untitled

如何设计和复用

现在我们已经有了可靠的后勤保障,来看看怎么设计整套系统吧。

首先,一个海报页面内大概有4种组件,分别是商品组件,文字组件,图片组件和标签组件,算上页面本身,我们一共有5种组件,卖家或者设计师则通过这几种组件的组合,设定不同动画效果和延时,完成海报的搭建工作。

显然,每个组件都具有一些共有的属性或者行为,比如,都有width,height来标记长宽,top,left来标记定位,也都能对其设定动画效果。

当然每个组件也都自己独特的行为,比如它们各自有自己不同的渲染方式,每个组件有自己的一些事件需要绑定,标签组件能改变尖角的朝向,图片组件会有一些比较复杂的操作逻辑等等。

抽象、继承和mixin

基于上面的分析,抽象出一个Element的abstract class实在是再合适不过了,它包含了所有公有的属性和方法,同时暴露了render和bindEvents两个接口,然后其他组件再继承这个Element,把对应的接口实现就好。

另外,为了不至于手忙脚乱,我们还需要一个工厂来管理和生产这些组件。上面谈到我们3个端都有一个共同的parser步骤,即json to HTML这一个步骤,产出的HTML具有非常大的相似性,因此我们希望3个端能共用一套parser,特别是h5渲染端,我们希望@半边能专注解决h5的各项疑难杂症,而不必关心json to html这一步。

另外,共用组件和parser还会带来另一个收益,将来如果有改动,一人维护,即可3端同步

理想状态下,我们希望调用factory.parse这个方法,然后朝里面传入一段描述页面的json中间语言,这个factory就能自己去做遍历和递归,把json里面的组件一个一个找到并“生产”出来,最后返回给我一段已经做好了数据绑定的,包含了对html引用的controller。另外,一些必要的优化,比如缓存,也会在factory中做掉。

一张class map说明我想干的事情

Untitled

大体就是这样,但是我们还做了别的一些事情,事实上每类组件在进入PC或者h5编辑器后还会具有不同的行为,因此,我们还需要再继承这5种不同的组件,通过组合或者mixin的方式,为他们继续追加不同的能力。

其他

当然,实际开发过程中,还有各种各样的问题,比如图片组件的裁剪和交互能力,旋转的情况下缩放,适配等问题,每一个小点其实都很有趣,也很消耗时间,不过这里由于篇幅所限,就不一一列举了。

有关产品本身,还想多说几句

事实上在最开始的时候,我们还想做更多的事情,比如我们进过调研后发现,keynote的图片编辑能力其实是最“傻瓜”和方便的,本来我们计划完全复制keynote的功能,另外还准备为编辑器增加一些更炫酷的交互组件,不过由于时间限制,很多东西只能放到后面迭代进行,慢慢演进了。

另外我们也发现了不少竞品,希望良性竞争能促进我们共同成长:)

结尾显得有点仓促,就不做任何总结了~

返璞归真的web设计--神笔海报/秒赞工作小结
Share this