前端已经过了单兵作战的时代了,现在一个稍微复杂一点的项目都需要几个人协同开发,一个战略级别的APP的话分工会更细,比如携程:
携程app = 机票频道 + 酒店频道 + 旅游频道 + ......
每个频道有独立的团队去维护这些代码,具体到某一个频道的话有会由数十个不等的页面组成,在各个页面开发过程中,会产生很多重复的功能,比如弹出层提示框,像这种纯粹非业务的UI,便成了我们所谓的UI组件,最初的前端组件也就仅仅指的是UI组件。
而由于移动端的兴起,前端页面的逻辑已经变得很重了,一个页面的代码超过5000行的场景渐渐增多,这个时候页面的维护便会很有问题,牵一发而动全身的事情会经常发生,为了解决这个问题,便出现了前端组件化,这个组件化就不是UI组件了,而是包含具体业务的业务组件。
这种开发的思想其实也就是分而治之(最重要的架构思想),APP分成多个频道由各个团队维护,频道分为多个页面由几个开发维护,页面逻辑过于复杂,便将页面分为很多个业务组件模块分而治之,这样的话维护人员每次只需要改动对应的模块即可,以达到最大程度的降低开发难度与维护成本的效果,所以现在比较好的框架都会对组件化作一定程度的实现。
组件一般是与展示相关,视觉变更与交互优化是一个产品最容易产生的迭代,所以多数组件相关的框架核心都是View层的实现,比如Vue与React的就认为自己仅仅是“View”,虽然展示与交互不断的在改变,但是底层展示的数据却不常变化,而View是表象,数据是根本,所以如何能更好的将数据展示到View也是各个组件需要考虑的,从而衍生出了单向数据绑定与双向数据绑定等概念,组件与组件之间的通信往往也是数据为桥梁。
所以如果没有复杂的业务逻辑的话,根本不能体现出组件化编程解决的痛点,这个也是为什么todoMVC中的demo没有太大参考意义。
今天,我们就一起来研究一下前端组件化中View部分的实现,后面再看看做一个相同业务(有点复杂的业务),也简单对比下React与Vue实现相同业务的差异。
PS:文章只是个人观点,有问题请指正
导读
github
代码地址:https://github.com/yexiaochai/module/
演示地址:http://yexiaochai.github.io/module/me/index.html
如果对文中的一些代码比较疑惑,可以对比着看看这些文章:
【一次面试】再谈javascript中的继承
【移动前端开发实践】从无到有(统计、请求、MVC、模块化)H5开发须知
【组件化开发】前端进阶篇之如何编写可维护可升级的代码
预览
组件化的实现
之前我们已经说过,所谓组件化,很大程度上是在View上面做文章,要把一个View打散,做到分散,但是又总会有一个总体的控制器在控制所有的View,把他们合到一起,一般来说这个总的控制器是根组件,很多时候就是页面本身(View实例本身)。
根据之前的经验,组件化不一定是越细越好,组件嵌套也不推荐,一般是将一个页面分为多个组件,而子组件不再做过深嵌套(个人经验)
所以我们这里的第一步是实现一个通用的View,这里借鉴之前的代码(【组件化开发】前端进阶篇之如何编写可维护可升级的代码):
View Code
有了View的代码后便需要组件级别的代码,正如之前所说,这里的组件只有根元素与子组件两层的层级:
View Code
有了根View与View组件的实现,剩下的便是数据实体的实现,View与组件Module之间通信的桥梁就是数据Entity,事实上我们的View或者组件模块未必会需要数据实体Entity,只有在业务逻辑的复杂度达到一定阶段才需要分模块,如果dom操作过多的话就需要Entity了:
View Code
我们这里抽取一段火车票列表的筛选功能做实现,这个页面有一定复杂度又不是太难,大概页面是这个样子的:
因为,我们这里的关注点在View,这里就直接将网上上海到北京的数据给拿下来:
View Code
我们这里做的第一个事情是将数据全部展示出来,在具体渲染前,原始数据需要做一些处理:
View Code
然后第一步的效果出来了,后面只需要处理数据筛选即可:
这里开始实现第一个业务组件,顶部的搜索栏,这个搜索栏有以下需求:
① 默认以时间升序排列
② 三个tab彼此互斥,点击时候仍然使用升序,再次点击为倒序
顶部导航组件
这里的交互就有一定复杂性了,这种场景是有数据实体出现的必要了,所以我们先实现数据实体:
复制代码
1 define(['AbstractEntity'], function (AbstractEntity) {
2
3 var Entity = _.inherit(AbstractEntity, {
4 propertys: function ($super) {
5 $super();
6
7 //三个对象,时间,耗时,价格,升序降序,三个数据互斥
8 //默认down up null
9 this.data = {
10 time: 'up',
11 sumTime: '',
12 price: ''
13 };
14 },
15
16 _resetData: function () {
17 this.data = {
18 time: '',
19 sumTime: '',
20 price: ''
21 };
22 },
23
24 setTime: function () {
25 this._setData('time');
26 },
27
28 setSumTime: function () {
29 this._setData('sumTime');
30 },
31
32 setPrice: function () {
33 this._setData('price');
34 },
35
36 _setData: function (key) {
37
38 //如果设置当前key存在,则反置,否则清空筛选,设置默认值
39 if (this.data[key] != '') {
40 if (this.data[key] == 'up') this.data[key] = 'down';
41 else this.data[key] = 'up';
42 } else {
43 this._resetData();
44 this.data[key] = 'down';
45 }
46 this.update();
47 }
48
49 });
50
51 return Entity;
52 });
复制代码
对应视觉展示比较简单:
1 <ul class="bus-tabs sort-bar js_sort_item">
2 <li class="tabs-item " data-sort="Time" style="-webkit-flex: 1.5; flex: 1.5;">出发时间<i class="icon-sort <%=time %>"></i></li>
3 <li class="tabs-item " data-sort="SumTime" >耗时<i class="icon-sort <%=sumTime %>"></i></li>
4 <li class="tabs-item " data-sort="Price" >价格<i class="icon-sort <%=price %>"></i></li>
5 </ul>
事实上这个数据实体是完全独立的,这个视觉模板也仅仅负责了展示,而在哪展示,数据怎么与模板产生关联其中就是我们的组件控制器了:
复制代码
1 define(['ModuleView', 'text!pages/tpl.sort.bar.html'], function (ModuleView, tpl) {
2 return _.inherit(ModuleView, {
3
4 //此处若是要使用model,处实例化时候一定要保证entity的存在,如果不存在便是业务BUG
5 initData: function () {
6
7 this.template = tpl;
8 this.events = {
9 'click .js_sort_item li ': function (e) {
10 var el = $(e.currentTarget);
11 var sort = el.attr('data-sort');
12 _hmt.push(['_trackEvent', 'train.list.sort.' + sort, 'click']);
13
14 this.sortEntity['set' + sort]();
15 }
16 };
17
18 this.sortEntity.subscribe('init', this.render, this);
19 this.sortEntity.subscribe(this.render, this);
20
21 },
22
23 getViewModel: function () {
24 return this.sortEntity.get();
25 }
26
27 });
28
29 });
复制代码
至此,可以看到,一个组件就已经完成了,组件的功能很简单:
PS:注意组件是不能脱离根组件View而存在,一个组件一定会有一个this.view对象
① 组件控制器获取了模板
② 组件控制器获取了根组件View给予(实例化时注入)的数据实体sortEntiy
于是我们在主控制器中实例化我们的数据实体与组件:
复制代码
1 initEntity: function() {
2
3 //实例化排序的导航栏的实体
4 this.sortEntity = new SortEntity();
5
6 },
7
8 initModule: function() {
9
10 //view为注入给组件的根元素
11 //selector为组件将要显示的容器
12 //sortEntity为注入给组件的数据实体,做通信用
13 //这个module在数据显示后会自动展示
14 this.sortModule = new SortModule({
15 view: this,
16 selector: '.js_sort_wrapper',
17 sortEntity: this.sortEntity
18 });
19
20 },
复制代码
这里简单说明下代码,首先这里说明一个错误的实践,一个笔误:
this.sortEntity.subscribe(this.render, this);
在mod.sort中有这么一段代码,事实上这段代码是有问题的,因为数据实体是作为被观察者实现的,所以subscribe应该为subscribed!!!
但是因为最初一个笔误导致所有团队所有业务团队都这样用了下去,产生了很大的误导作用,这里继续写错,提醒大家做框架层的东西要慎重。
这里事实上观察者为View或者Module,按道理说该是:
view.subscribe(entity, callback)
但是当时考虑到一个view未必会有数据实体,而view实现后module还要做实现,于是这块代码就写到了entity上,现在看来写到View上更合理,这里不多说,回到我们的业务代码。
这里对entity的变化绑定了一个回调,“数据变化的话重新渲染本身”,于是我们每次点击的话:
var el = $(e.currentTarget);
var sort = el.attr('data-sort');
this.sortEntity['set' + sort]();
由标签获取了当前设置的key,完了引起数据更新,便导致了组件本身的重新渲染,于是功能完成。
但是我们知道这个数据变化除了组件本身变化以外还应该引起列表的变化,所以我们在View中也应该观察这个数据实体的变化,以便重新渲染数据:
//实例化排序的导航栏的实体
this.sortEntity = new SortEntity();
this.sortEntity.subscribe(this.renderList, this);
这个时候由于排序的产生,我们需要重写renderList的实现:
复制代码
1 _timeSort: function (data, sort) {
2 data = _.sortBy(data, function (item) {
3 item = item.from_time.split(':');
4 item = item[0] + '.' + item[1];
5 item = parseFloat(item);
6 return item;
7 });
8 if (sort == 'down') data.reverse();
9 return data;
10 },
11
12 _sumTimeSort: function (data, sort) {
13 data = _.sortBy(data, function (item) {
14 return parseInt(item.use_time);
15 });
16 if (sort == 'down') data.reverse();
17 return data;
18 },
19
20 _priceSort: function (data, sort) {
21 data = _.sortBy(data, function (item) {
22 return item.min_price;
23 });
24 if (sort == 'down') data.reverse();
25 return data;
26 },
27
28 //获取导航栏排序后的数据
29 getSortData: function (data) {
30 var tmp = [];
31 var sort = this.sortEntity.get();
32
33 for (var k in sort) {
34 if (sort[k].length > 0) {
35 tmp = this['_' + k + 'Sort'](data, sort[k])
36 return tmp;
37 }
38 }
39 },
40
41 //完成所有的筛选条件,逻辑比较重
42 getFilteData: function () {
43 var data = this.formatData(this.listData);
44 data = this.getSortData(data);
45
46 return data;
47 },
48
49 //渲染列表
50 renderList: function() {
51 var data = this.getFilteData();
52
53 var html = '';
54 window.scrollTo(0, 0);
55
56 if (data.length === 0) {
57 this.d_none_data.show();
58 this.d_list_wrapper.hide();
59 return;
60 }
61
62 this.d_none_data.hide();
63 this.d_list_wrapper.show();
64 html = this.renderTpl(listTpl, { data: data });
65 this.d_list_wrapper.html(html);
66 },
复制代码
可以看到,这里复杂操作被分解为了一个个小小的方法,配合underscore释放的一些数组操作,便可以简单的完成列表渲染功能,因为这里最终的渲染逻辑没有改变,改变的仅仅是排序后的数组。
此处对班次数据的处理篇幅已经超过了50行,如果再增加,可以实例化一个班次Entity用于格式化的数据输出,因为我们这里没有实现这个实体,放到根View中又过于臃肿,所以将之放到Module中,这样关于筛选的所有API全部下放到了排序模块,而主View代码就会清晰的多:
复制代码
1 //完成所有的筛选条件,逻辑比较重
2 getFilteData: function () {
3 var data = this.formatData(this.listData);
4 data = this.sortModule.getSortData(data);
5 return data;
6 },
复制代码
sort模块
至此,一个完整的业务组件便实现结束了,那么这里也实现了一个简单的View组件系统,那么这么做有什么不足呢?
不足
首先,我们这里的实现扔是以js为基础,这种做法似乎不太“组件化”,更好的实现似乎是直接以一个标签的方式使用。
然后,可以看出,我们每次点击先是改变数据,然后数据触发更新,刷新了整个列表,也改变了组件本身的展示,这里的方案简单粗暴,完全是重新渲染,这种重新渲染的做法,在数据列表达到一定数量的话是一种资源浪费。
但是楼主基本无力解决以上问题,这种问题我们看看Vue与React后面是如何解决的,这里暂时搁置。
底部菜单
我们这里做的第二个业务组件是底部菜单栏:
观察这个视觉,我们这里出现了一个纯粹的UI组件,于是我们做的第一步是实现这个UI组件。
UI组件的实现
所谓UI组件是不包含任何业务代码,因为UI组件不是今天的重点,我这里贴出实现不做过多讲解:
UIView
UIMask
UIList
UIList模板
这里的UIView是继承至基础View的代码,做了简单改造,让他更适合做UI的基类
UIMask就是我们常用的蒙版
UIList是我们真实使用的组件,继承至UIView,其中会实例化一个Mask的实例
有兴趣的朋友这里自己看看,我们将关注点放到底部的业务组件。
底部业务组件
细心的朋友应该看到了,事实上在布局之初的时候(list还未渲染),底部菜单栏DOM结构便已经存在,我们这里做的一件事情就是当组件已经存在如何和组件交互逻辑关联起来,这里做的第一步也是最重要一部依旧是数据实体的抽象。
他这个数据是一个多选框类型的数据,数组的第一项是全选功能,根据需求我们抽象出了这种数据实体:
多选数据实体
有了数据实体,我们这里便需要实现使用数据实体的组件(非最终代码):
组件代码
然后在根View中将View与组件关联起来:
复制代码
1 //车次类型数据实体
2 this.trainTypeEntity = new CheckBoxEntity({
3 data: [
4 { name: '全部车次', id: 'all', checked: true },
5 { name: '高铁城际(G/C)', id: 'g' },
6 { name: '动车(D)', id: 'd' },
7 { name: '特快(T)', id: 't' },
8 { name: '其它类型', id: 'other' }
9 ]
10 });
11
12 //车次类型模块
13 this.trainTypeModule = new CheckBoxModule({
14 view: this,
15 selector: '.js_type',
16 tagname: 'Type',
17 entity: this.trainTypeEntity
18 });
复制代码
这样,我们点击车次类型便能很好的运行了,但是tab并没有被选中:
这里思考一个问题:底部有三个组件交互,依次是车次类型、出发站、更多,事实上组件之间并不知道当前是哪个tab被点击了,应该展示选中状态,知道的是根View,所以我们这里需要在View中实现一个数据实体,注入给三个业务组件,告诉他们现在该谁被选中了,下面三个tab只有一个能选中,并且选择了一个tab,另一个tab的菜单如果是展示状态需要将其隐藏,所以我们实现了单选的实体:
单选数据实体
然后是出发站的实现,这里出发站有一点特殊,首先这个数据需要列表加载结束,我们去筛选数据获得出发站,所以实体有一个初始化的过程(而且这里数据更新是不需要触发事件的);其次他的交互与车次类型完全一致,唯一不同的只是数据实体,所以这里出发站的组件我们可以复用,只需要实例化一个数据出来即可:
view
如此底部菜单栏中的车次类型与出发站,我们便基本实现了,这里每次数据变化还需要更新列表数据,这里在主View中绑定相关变化即可,然后再重写下renderList,便结束了主要功能,这里有个不同的地方,是列表数据的筛选却不能放在Module中,因为车次类型与出发站的数据筛选可能不一样,所以这样看来最初的班次数据操作就应该封装为一个ListEntity做数据筛选,我们这里暂时放到主View中:
复制代码
1 //根据车次类型做筛选
2 getTypeData: function (data) {
3 var typeKeys = this.trainTypeEntity.getCheckedKey();
4 if (!typeKeys) return data;
5 var tmp = _.filter(data, function (item) {
6 var no = item.my_train_number;
7 if (_.indexOf(typeKeys, no) != -1) {
8 return true;
9 }
10 return false;
11 });
12
13 return tmp;
14 },
15
16 //根据出发站做筛选
17 //事实上这个方法与getTypeData不是完全不能重构到一起,但是可读性可能会变得晦涩
18 getSetoutData: function (data) {
19 var keys = this.setoutEntity.getCheckedKey();
20 if (!keys) return data;
21
22 var tmp = _.filter(data, function (item) {
23 var no = item.from_telecode;
24 if (_.indexOf(keys, no) != -1)
25 return true;
26 return false;
27 });
28
29 return tmp;
30 },
31
32 //完成所有的筛选条件,逻辑比较重
33 getFilteData: function () {
34 var data = this.formatData(this.listData);
35 data = this.getTypeData(data);
36 data = this.getSetoutData(data);
37 data = this.sortModule.getSortData(data);
38 return data;
39 },
复制代码
更多功能
这里更多功能也是比较复杂的,但是就算一个更多,里面又会分为几个小组件,几个数据实体,所以真实实现功能后代码反而简单,这里我便不做实现,感兴趣的同学作为家庭作业做吧。
总结
我们这里使用js实现了一个简单的组件化View的实现,中间形成了纯粹的UI组件,也将View拆分实现了一些业务组件,最终形成的目录为:
有兴趣的朋友在git上面去看吧,这里是入口js:
复制代码
1 (function () {
2
3 require.config({
4 paths: {
5 'text': 'libs/require.text',
6
7 'AbstractView': 'js/view',
8 'AbstractEntity': 'js/entity',
9 'ModuleView': 'js/module'
10
11
12 }
13 });
14
15 require(['pages/list'], function (List) {
16
17 var list = new List();
18 list.show();
19
20 });
21
22 })();
复制代码
好了,经过上面的实现,如果看过之前我的博客的话,应该对组件化开发有一定了解了,我们也可以进入今日的正题了,首先我们以Vue实现上述功能。
Vue的实现
首先,这里说Vue的实现,不是底层源码分析,而是实现上述功能,这里大家不要误会,因为我也是昨天才开始看Vue,暂时无能了解他的实现。
我虽然没有使用过Vue做业务开发,但是在很久之前就听我一个好基友夸耀Vue,到今日实现,Vue已经火的不行了。。。。。。
今天,就让我们来试试Vue的威力,因为是初次使用,如果有不对的地方,大家可以指正,也不要喷了
组件拆分
根据之前的实现,我们发现我们的组件可以轻易的分为三个部分:
① 顶部导航
② 列表
③ 底部菜单
我们之前做的第一件事情是将列表展示做掉,这种开发流程是不会改变的,所以使用Vue将列表展示实现。
组件定义
在使用组件化之前,我们先看看Vue如何实现一个组件(这里不得不说Vue的作者文档确实写的好):
复制代码
1 // 定义
2 var MyComponent = Vue.extend({
3 template: '<div>A custom component!</div>'
4 })
5
6 // 注册
7 Vue.component('my-component', MyComponent)
8
9 // 创建根实例
10 new Vue({
11 el: '#example'
12 })
复制代码
复制代码
1 <!--正确做法-->
2 <article class="cm-page" id="main">
3 <my-component></my-component>
4 </article>
5
6 <!--错误做法,组件一定要依赖根部View-->
7 <my-component></my-component>
复制代码
作者这个实现很不错,也考虑了不是所有组件都需要全局释放的,一般来说可以全局释放的组件都与业务无关或者是公共业务,这里是局部组件的实现:
复制代码
1 var Child = Vue.extend({ /* ... */ })
2
3 var Parent = Vue.extend({
4 template: '...',
5 components: {
6 // <my-component> 只能用在父组件模板内
7 'my-component': Child
8 }
9 })
复制代码
上面的例子比较简单,我们马上使用一个复杂一点的例子试试水:
复制代码
1 // 定义
2 var MyList = Vue.extend({
3
4 data: function () {
5 return {
6 data: /**/[
7 {name: '出发时间', id: 'time'},
8 {name: '耗时', id: 'sumTime'},
9 {name: '价格', id: 'price'}
10 ]
11 };
12 },
13 template: [
14 '<ul>',
15 '<li v-for="item in data" >',
16 '{{item.name}}',
17 '</li>',
18 '</ul>'
19 ].join('')
20 })
21
22 // 注册
23 Vue.component('my-list', MyList)
24
25 // 创建根实例
26 new Vue({
27 el: '#main'
28 })
复制代码
<article class="cm-page" id="main">
<my-component></my-component>
<my-list></my-list>
</article>
这段代码会输出:
1 <article class="cm-page" id="main">
2 <div>A custom component!</div>
3 <ul><li>出发时间</li><li>耗时</li><li>价格</li></ul>
4 </article>
这里了解到这里便暂时结束,我们来实现我们的列表组件。
根实例
我们在第一节的实现中很多初始化的动作与数据全部放到了根View中,同样,我们这里需要实现一个根View:
复制代码
1 define([
2 'Vue'
3 ], function (Vue) {
4 return new Vue({
5 data: {
6 a: 1
7 },
8 el: '#main',
9 template: '<div>test</div>'
10 });
11 });
复制代码
现在我们在入口index.html将写入list组件,然后在写实现(不得不说,我也认为这种做法很直观):
<article class="cm-page" id="main">
<my-list></my-list>
</article>
根据上面的知识我们实现这个业务组件,这里也碰到了第一个问题,根View如何将数据传递给他的组件也就是,组件与View之间如何通信。
组件实例的作用域是孤立的。这意味着不能并且不应该在子组件的模板内直接引用父组件的数据。可以使用 props 把数据传给子组件。
“prop” 是组件数据的一个字段,期望从父组件传下来。子组件需要显式地用 props 选项 声明 props:
根据文档,我这里写个demo试试:
复制代码
1 var MyList = Vue.extend({
2 props: ['data'],
3 template: [
4 '<ul>',
5 '<li v-for="item in data" >',
6 '{{item.name}}',
7 '</li>',
8 '</ul>'
9 ].join('')
10 })
11
12 // 注册
13 Vue.component('my-list', MyList)
14
15 // 创建根实例
16 new Vue({
17 data: {
18 name: 'test',
19 data: /**/[
20 {name: '出发时间', id: 'time'},
21 {name: '耗时', id: 'sumTime'},
22 {name: '价格', id: 'price'}
23 ]
24 },
25 el: '#main'
26 })
复制代码
<article class="cm-page" id="main">
{{name}}
<my-component></my-component>
<my-list v-bind:data="data"></my-list>
</article>
代码虽然和传统习惯不一样,但是确实完成了我们要的实现。
PS:楼主这里慢慢开始感觉有点怪了,可能使用方法不太对
list组件
因为Vue的模板里面不能写表达式,所以所有的数据相关逻辑需要写到组件代码部分,因为之前的处理,我们只需要简单的改下模板便能正确的运行:
复制代码
<article class="cm-page page-list" id="main">
<my-list :data="data"></my-list>
</article>
<script type="text/javascript" src="./libs/underscore.js"></script>
<script type="text/javascript" src="./libs/require.js"></script>
<script type="text/javascript" src="main.js"></script>
复制代码
根View
组件js代码部分:
复制代码
define([
'Vue',
'text!pages/tpl.list.html'
], function (Vue,
template) {
return Vue.extend({
props: ['data'],
data: function() {
return {
mapping: {
'g': '高速',
't': '特快',
'd': '高速动车',
'c': '城际高铁',
'z': '直达'
}
};
},
template: template
});
});
复制代码
组件模板部分:
复制代码
1 <ul class="bus-list js_bus_list ">
2 <li v-for="item in data" class="bus-list-item ">
3 <div class="bus-seat">
4 <span class=" fl">{{item.train_number }} | {{mapping[item.my_train_number] || '其它'}} </span>
5 <span class=" fr">{{parseInt(item.use_time / 60) + '小时' + item.use_time % 60 + '分'}}</span>
6 </div>
7 <div class="detail">
8 <div class="sub-list set-out">
9 <span class="bus-go-off">{{item.from_time}}</span> <span class="start"><span class="icon-circle s-icon1">
10 </span>{{item.from_station }}</span> <span class="fr price">¥{{item.min_price}}起</span>
11 </div>
12 <div class="sub-list">
13 <span class="bus-arrival-time">{{item.to_time}}</span> <span class="end"><span class="icon-circle s-icon2">
14 </span>{{item.to_station}}</span> <span class="fr ">{{item.sum_ticket}}张</span>
15 </div>
16 </div>
17 <div class="bus-seats-info" >
18 <span v-for="seat in item.my_seats">{{seat.name}}({{seat.yupiao }}) </span>
19 </div>
20 </li>
21 </ul>
复制代码
从代码上看,其实主要的处理逻辑仍旧是最初数据的处理,我这里其实有一个疑问?
这里数据是写死的,如果真实业务中数据由ajax返回,那么这个业务代码该在View哪个位置进行?这个问题留待下次完整阅读Vue文档后分析
顶部导航组件
这里回到顶部导航的实现,这个与列表不同的是他会多出很多交互了,首先嵌入一个新标签:
<article class="cm-page page-list" id="main">
<my-sort-bar></my-sort-bar>
<my-list :data="data"></my-list>
</article>
这个标签的模板可以直接将之前的模板拷贝过来改成Vue的语法即可:
复制代码
1 <ul class="bus-tabs sort-bar js_sort_item">
2 <li class="tabs-item " v-on:click="setTime" style="-webkit-flex: 1.5; flex: 1.5;">
出发时间<i class="icon-sort {{time }}"></i></li>
3 <li class="tabs-item " v-on:click="setSumTime">耗时<i class="icon-sort {{sumTime }}"></i></li>
4 <li class="tabs-item " v-on:click="setPrice">价格<i class="icon-sort {{price }}"></i></li>
5 </ul>
复制代码
完了完成最主要的组件模块代码实现,在这里小钗发现之前的导航相关的Entity实体中的操作就是所有需要的处理,所以这里可以稍微改造下便使用:
复制代码
1 define([
2 'Vue',
3 'text!pages/tpl.sort.html'
4 ], function (Vue,
5 template) {
6
7 return Vue.extend({
8 props: ['data'],
9 data: function () {
10 return {
11 time: 'up',
12 sumTime: '',
13 price: ''
14 };
15 },
16 methods: {
17 _resetData: function () {
18 this.time = '';
19 this.sumTime = '';
20 this.price = '';
21 },
22
23 setTime: function () {
24 this._setData('time');
25 },
26
27 setSumTime: function () {
28 this._setData('sumTime');
29 },
30
31 setPrice: function () {
32 this._setData('price');
33 },
34
35 _setData: function (key) {
36 //如果设置当前key存在,则反置,否则清空筛选,设置默认值
37 if (this[key] != '') {
38 if (this[key] == 'up') this[key] = 'down';
39 else this[key] = 'up';
40 } else {
41 this._resetData();
42 this[key] = 'down';
43 }
44 }
45 },
46 template: template
47
48 });
49
50 });
复制代码
对比下之前数据实体的代码,以及组件控制器的实现:
复制代码
1 define(['ModuleView', 'text!pages/tpl.sort.bar.html'], function (ModuleView, tpl) {
2 return _.inherit(ModuleView, {
3
4 //此处若是要使用model,处实例化时候一定要保证entity的存在,如果不存在便是业务BUG
5 initData: function () {
6
7 this.template = tpl;
8 this.events = {
9 'click .js_sort_item li ': function (e) {
10 var el = $(e.currentTarget);
11 var sort = el.attr('data-sort');
12 this.sortEntity['set' + sort]();
13 }
14 };
15
16 this.sortEntity.subscribe(this.render, this);
17
18 },
19
20 getViewModel: function () {
21 return this.sortEntity.get();
22 }
23
24 });
25
26 });
复制代码
数据实体
这里是Vue根仅仅使用AMD方式引入该模块即可,View的实现:
components: {
'my-list': ListModule,
'my-sort-bar': SortModule
},
现在第二个问题来了,这里每次操作事实上应该影响列表组件的排序,之前我们这块是通过数据实体entity做通信,不出意外的话Vue也该如此,但是小钗在这里却产生了疑惑,该怎么做,怎么实现?
因为根据之前经验,主View与组件之间以数据实体的方式通信是比较常见的操作,组件之间也可以通过数据实体沟通,因为数据实体是实例化在根View中的,但是组件与组件之间不应该产生直接的关联,一般来说主View或者组件使用数据以外的方式都是不可取的。
这里顶部导航组件是独立的,并没有释放对外的接口,根View也没有注入数据对象给他,那么他的变化该如何通知到列表组件,让他重新排序呢?这个时候又开始查文档ing。
小钗这里没有想到很好的办法,于是将顶部导航的组件的数据上升到了主View中,主View以pros的方式传递了给两个组件,所以上述代码要有变动,首先是根节点:
复制代码
1 data: {
2 data: formatData(listData),
3 sort: {
4 time: 'up',
5 sumTime: '',
6 price: ''
7 }
8 },
复制代码
html中的调用:
复制代码
1 <article class="cm-page page-list" id="main">
2 <div class="js_sort_wrapper sort-bar-wrapper">
3 <my-sort-bar :sort="sort"></my-sort-bar>
4 </div>
5 <my-list :data="data"></my-list>
6 </article>
复制代码
最后是组件js与具体模板的实现:
View Code
View Code
这样一来主View又可以使用pros的方式将sort字段释放给列表组件了,这里代码再做变更。
PS:小钗注意观察过每次数据变化,不是重新渲染的方式,替代的是局部更新,这个功能要实现好很难
PS:这里代码改来改去,一是希望大家看到思考过程,二是楼主帮助自己思考,最终大家还是对着git看吧
然后这里将原来写在导航模块的数据处理移到列表组件中,从这里也可以看出,我们最开始的代码实现中事实上也可以将列表实现成一个组件,这应该是“强组件”思维与“弱组件”思维带来的规则差异,强的框架会用规则限制你的代码,让你绕过错误,减少你思考的概率,弱的框架会更灵活些,也意味着能力不足易出错,但是因为根View的data是共享的,要把所有的筛选数据透传给列表组件又比较烦,所以我们便直接在根View中操作数据了。
这里数据变化了虽然会引起自身的的渲染,但是并不能执行对应的回调,所以我们应该给他注册回调,Vue使用watch为数据绑定观察回调,我们这里试试。
顶部导航组件实现
至此,我们完成了顶部导航组件,现在来完成底部菜单栏。
底部菜单栏
根据之前的开发模式,我们依旧是形成一个组件,放到html里面:
复制代码
<article class="cm-page page-list" id="main">
<div class="js_sort_wrapper sort-bar-wrapper">
<my-sort-bar :sort="sort"></my-sort-bar>
</div>
<my-list :data="data" :sort="sort"></my-list>
<my-tabs></my-tabs>
</article>
复制代码
PS:这里会用到的UI组件我不愿意再重新写了,所以就直接将原来的拿来用,反正与业务无关。
PS:请注意,Vue本身是不包含任何dom操作的,因为使用UI组件加入了一些第三方库,大家可以无视掉
根据之前的经验,这里无论是车次类型还是出发站,是几个组件共享的,所以我们仍然将之实现在根View中,然后传递给子组件,因为出发城市和车次类型事实上是一个组件,所以上面的结构有所变化:
复制代码
1 <article class="cm-page page-list" id="main">
2 <div class="js_sort_wrapper sort-bar-wrapper">
3 <my-sort-bar :sort="sort"></my-sort-bar>
4 </div>
5 <my-list :data="data" :sort="sort"></my-list>
6 <ul class="bus-tabs list-filter js_tabs " style="z-index: 3000;">
7 <my-tab-item :data="type" name="车次类型"></my-tab-item>
8 <my-tab-item :data="setout" name="出发站"></my-tab-item>
9 <li class="tabs-item js_more">更多<i class="icon-sec"></i>
10 </li>
11 </ul>
12 </article>
复制代码
对应的组件第一步也结束了:
复制代码
1 data: {
2 data: formatData(listData),
3 sort: {
4 time: 'up',
5 sumTime: '',
6 price: ''
7 },
8 //车次类型
9 type: [
10 {name: '全部车次', id: 'all', checked: true},
11 {name: '高铁城际(G/C)', id: 'g'},
12 {name: '动车(D)', id: 'd'},
13 {name: '特快(T)', id: 't'},
14 {name: '其它类型', id: 'other'}
15 ],
16 setout: [
17 {name: '全部出发站', id: 'all', checked: true}
18 ]
19 },
复制代码
View Code
从代码可以看出,组件中包含了原来的entity的操作,与module操作,事实上这两块东西应该也可以按原来那种方式划分,让组件中的代码更加纯粹不去操作过多数据,第一步结束,第二步是当数据变化时候更新到列表,这里又需要观察数据变化了,最后形成了这个代码:
View Code
这个应该不是最优的做法,后续值得深入研究,最后我们实现下tab的选择效果便结束Vue的代码。
PS:这个写的有点累了,具体代码大家去git上看吧,这里不多写了。
总结
Vue带来了最大一个好处是:
摆脱DOM操作
你真的在页面中就没有看到任何DOM操作了!这个是很牛的一个事情,另外Vue的文档写的很完备,后面点有时间应该做更深入全面的学习!
结语
介于篇幅过长,楼主体力虚脱,关于React的实现,下次再补齐吧,文中不足希望您的指出。
本文转自叶小钗博客园博客,原文链接:http://www.cnblogs.com/yexiaochai/p/5228871.html,如需转载请自行联系原作者