前言javascript
我还在携程的作业务的时候,每一个看似简单的移动页面背后每每会隐藏5个以上的数据请求,其中最过复杂的当属机票与酒店的订单填写业务代码css
这里先看看比较“简单”的机票代码:html
而后看看稍微复杂的酒店业务逻辑:前端
机票一个页面的代码量达到了5000行代码,而酒店的代码居然超过了8000行,这里还不包括模板(html)文件!!!java
而后初略看了机票的代码,就该页面可能发生的接口请求有19个之多!!!而酒店的的交互DOM事件基本多到了使人发指的地步:ios
固然,机票团队的交互DOM事件已经多到了我笔记本不能截图了:git
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
events: {
'click .js_check_invoice_type': 'checkInvoiceType', //切换发票类型
'click .flight-hxtipshd': 'huiXuanDesc', //惠选说明
'click .js_ListReload': 'hideNetError',
'click #js_return': 'backAction', //返回列表页
'click div[data-rbtType]': 'showRebate', //插烂返现说明
'click #paybtn .j_btn': 'beforePayAction', //提交订单 //flightDetailsStore, passengerQueryStore, mdStore, postAddressStorage, userStore, flightDeliveryStore
'click .flight-loginbtn2': 'bookLogin', //登陆
'input #linkTel': 'setContact', //保存用户输入的联系人
'click #addPassenger .flight-labq': 'readmeAction',//姓名帮助
'click .jsDelivery': 'selDelivery', //选择配送方式
'click #jsViewCoupons': 'viewCoupons', //查看消费券使用说明 //flightDetailsStore
// 'click .j_refundPolicy': 'fanBoxAction', //查看返现信息
//'click .flight-bkinfo-tgq .f-r': 'tgBoxAction', //查看退改签
'click .js_del_tab': 'showDelListUI', //配送方式
// 'click .js_del_cost .flight-psf i': 'selectPaymentType', // 选择快递费用方式
'click #js_addrList': 'AddrListAction', //选择地址
'click #date-picker': 'calendarAction', //取票日期 //airportDeliveryStore
'click #done-address': 'zqinairselect', //取票柜台
'click #selectCity': 'selectCityAction', //选择城市
'click #date-zqtime': 'showZqTimeUI', //取票时间 //airportDeliveryStore
'click #jsinsure': 'viewInsure', //保险说明
'click #js_invoice_title': 'inTitleChangeWrp', //发票抬头更改 // userStore, flightOrderInfoInviceStore, flightOrderStore //don't move outside
'click #js_invoice_title_div': 'inTitleChangeWrp',
'click .flight-icon-arrrht': 'showinTitleList', //‘+’号,跳转发票抬头列表 //userStore, invoiceURLStore
'focusin #linkTel': 'telInput',
'focusout #linkTel': 'telInputFinish',
'touchstart input': 'touchStartAction', // 处理Android手机上点击不灵敏问题
'click #package .flight-arrrht': 'packageSelect',
'focusin input': 'hideErrorTips',
'click #dist_text_div': 'hideErrorTips',
'click .j_PackageNotice': 'toggletips',
'click .j_AnnouncementNotice': 'toggleNotice',
'click #travalPackageDesc': 'forwardToTravalPackage', //don't move into child modules
'click #airInsureDesc': 'showAirInsureDesc',
'click #paybtn': 'orderDetailAction',//价格明细
'click .J_retriveVerifyCodeBtn': 'getVerifyCode',
'click .J_toPay': 'toPayAction',
'click .J_closeVerifyCode': 'closeVerifyCodePopup',
'keyup .J_verifyCodePopup input': 'setToPayBtnStatus',
'click .js_flight_seat': 'selectRecommendCabin', // 选择推荐仓位
'click .j_changeFlight': 'changeFlightAction', // 推荐航班弹层中更改航班
'focusin input:not([type=tel])': 'adjustInputPosition', // iphone5/5s ios8搜狗输入法遮住input
'click .js_addr,#js_addr_div': 'editDeliverAddress',//报销凭证,详细地址编辑
'click .js_showUserInfo': 'showUserInfo', // add by hkhu v2.5.9
'click #logout': 'logout', // add by hkhu v2.5.9
'click #gotoMyOrder': 'gotoMyOrder', // add by hkhu v2.5.9
'touchstart #logout': function (e) { $(e.currentTarget).addClass('current'); },
'touchstart #gotoMyOrder': function (e) { $(e.currentTarget).addClass('current'); },
'click .js_buddypayConfirm': 'buddypayConfirmed',
'click .js_pickupTicket': 'viewPickUp', //261接送机券说明
'click .flt-bking-logintips': 'closelogintips'//关闭接送机券提示
},
|
就这种体量的页面,若是须要迭代需求、打BUG补丁的话,我敢确定的说,一个BUG的修复很容易引发其它BUG,而上面还仅仅是其中一个业务页面,后面还有强大而复杂的前端框架呢!如此复杂的前端代码维护工做可不是开玩笑的!github
PS:说道此处,不得不为携程的前端水平点个赞,业内少有的单页应用,一套代码H5&Hybrid同时运行不说,还解决了SEO问题,嗯,很赞。web
如何维护这种页面,如何设计这种页面是咱们今天讨论的重点,而上述是携程合并后的代码,他们两个团队的设计思路不便在此处展开。面试
今天,我这里提供一个思路,认真阅读此文可能在如下方面对你有所帮助:
1
2
3
4
|
①
如何将一个复杂的页面拆分为一个个独立的页面组件模块
②
如何将分拆后的业务组件模块从新合为一个完整的页面
③
从重构角度看组件化开发带来的好处
④
从前端优化的角度看待组件化开发
|
文中是我我的的一些框架&业务开发经验,但愿对各位有用,也但愿各位多多支持讨论,指出文中不足以及提出您的一些建议。
因为该项目涉及到了项目拆分与合并,基本属于一个完整的前端工程化案例了,因此将之放到了github上:https://github.com/yexiaochai/mvc
其中工程化一块的代码,后续会由另外一位小伙伴持续更新,若是该文对各位有所帮助的话请各位给项目点个赞、加颗星:)
我相信若是是中级水平的前端,认真阅读此文必定会对你有一点帮助滴。
一个实际的场景
演示地址
http://yexiaochai.github.io/mvc/webapp/bus/list.html
代码仓促,可能会有BUG哦:)
代码地址:https://github.com/yexiaochai/mvc/
页面基本构成
由于订单填写页通常有密度,我这里挑选相对复杂而又没有密度的产品列表页来作说明,其中框架以及业务代码已经作过抽离,不会包含敏感信息,一些优化后续会同步到开源blade框架中去。
咱们这里列表页的首屏页面以下:
简单来讲组成以下:
① 框架级别UI组件UIHeader,头部组件
② 点击日期会出框架级别UI,日历组件UICalendar
③ 点击出发时段、出发汽车站、到达汽车站,皆会出框架级别UI
④ header下面的日期工具栏须要做为独立的业务模块
⑤ 列表区域能够做为独立的业务模块,可是与主业务靠太近,不太适合
⑥ 出发时段、出发汽车站、到达汽车站皆是独立的业务模块
一个页面被咱们拆分红了若干个小模块,咱们只须要关注模块内部的交互实现,而包括业务模块的通讯,业务模块的样式,业务模块的重用,暂时有如下约定:
1
2
3
|
①
单个页面的样式所有写在一个文件中,好比list里面全部模块对应的是list.css
②
模块之间采用观察者模式观察数据实体变化,以数据为媒介通讯
③
通常来讲业务模块不可重用,若是有重用的模块,须要分离到common目录中,由于咱们今天不考虑common重用,这块暂时不予理睬
|
这里有些朋友可能认为单个模块的CSS以及image也应该参与独立,我这里不太赞成,业务页面样式粒度太细的话会给设计带来不小的麻烦,这里再以通俗的话来讲:尼玛,我CSS功底通常,拆分的太细,对我来讲难度过高……
很差的作法
很差的这个事情实际上是相对的,由于很差的作法通常是比较简单的作法,对于一次性项目或者业务比较简单的页面来讲反而是好的作法,好比这里的业务逻辑能够这样写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
define(['AbstractView', 'list.layout.html', 'list.html', 'BusModel', 'BusStore', 'UICalendarBox', 'UILayerList', 'cUser', 'UIToast'],
function (AbstractView, layoutHtml, listTpl, BusModel, BusStore, UICalendarBox, UILayerList, cUser, UIToast) {
return _.inherit(AbstractView, {
propertys: function ($super) {
$super();
//一堆基础属性定义
//......
//交互业务逻辑
this.events = {
'click .js_pre_day': 'preAction', //点击前一天触发
'click .js_next_day': 'nextAction', //点击后一天触发
'click .js_bus_list li': 'toBooking', //点击列表项目触发
'click .js_show_calendar': 'showCalendar', //点击日期项出日历组件
'click .js_show_setoutdate': 'showSetoutDate', //筛选出发时段
'click .js_show_setstation': 'showStation', //筛选出发站
'click .js_show_arrivalstation': 'showArrivalStation', //筛选到达站
//迭代需求,增长其它频道入口
'click .js-list-tip': function () {}
};
},
//初始化头部标题栏
initHeader: function (t) { },
//首次dom渲染后,初始化后续会用到的全部dom元素,以避免重复获取
initElement: function () {},
showSetoutDate: function () {},
showStation: function () {},
showArrivalStation: function () {},
showCalendar: function () {},
preAction: function (e) {},
nextAction: function () {},
toBooking: function (e) {},
listInit: function () {},
bindScrollEvent: function () {},
unbindScrollEvent: function () { },
addEvent: function () {
this.on('onShow', function () {
//当页面渲染结束,须要作的初始化操做,好比渲染页面
this.listInit();
//......
});
this.on('onHide', function () {
this.unbindScrollEvent();
});
}
});
});
|
根据以前的经验,若是仅仅包含这些业务逻辑,这样写代码问题不是很是大,代码量预计在800行左右,可是为了完成完整的业务逻辑,咱们这里立刻产生了新的需求。
需求迭代
由于我这里的班次列表,最初是没有URL参数,因此根本没法产出班次列表,页面上全部组件模块都是摆设,因而这里新增一个需求:
1
|
当
url没有出发-到达相关参数信息时,默认弹出出发城市到达城市选择框
|
因而,咱们这里会新增一个简单的弹出层:
这个看似简单的弹出层,背后却隐藏了一个巨大的陷阱,由于点击出发或者到达时会出城市列表,而城市列表自己就是一个比较复杂的业务:
因而页面的组成发生了改变:
① 自己业务逻辑约800行代码
② 新增出发到达筛选弹出层
③ 出发城市页面,预计300行代码
而弹出层的新增对业务自己形成了深远的影响,原本url是不带有业务参数的,可是点击了弹出层的肯定按钮,须要改变URL参数,而且刷新自己页面的数据,因而简单的一个弹出层新增直接将页面的复杂程度提高了一倍。
因而该页面代码轻轻松松破千了,后续需求迭代js代码量破2000仅仅是时间问题,到时候维护便复杂了,页面复杂无规律的DOM操做将会令你焦头烂额,这个时候组件化开发的优点便得以体现了,因而下面进入组件化开发的设计。
准备工做
整体架构
此次的代码依赖于blade骨架,包括:
① MVC模块,完成经过url获取正确的page控制器,从而经过view.js完成渲染页面的功能
② 数据请求模块,完成接口请求
全站依赖于javascript的继承功能,详情见:【一次面试】再谈javascript中的继承,若是不太了解面向对象编程,文中代码可能会有点吃力,也请各位多多了解。
整体业务架构如图:
框架架构图:
.
下面分别介绍下各个模块,帮助各位在下文中能更好的了解代码,首先是基本MVC的介绍,这里请参考我这篇文章:简单的MVC介绍
全局控制器
其实控制器可谓是变化万千的一个对象,对于服务器端来讲,控制器完成的功能是将本次请求分发到具体的代码模块,由代码模块处理后返回字符串给前端;
对于请求已经来到浏览器的前端来讲,根据此次请求URL(或者其它判断条件),判断该次请求应该由哪一个前端js控制器执行,这是前端控制器干的事情;
当真的此次处理逻辑进入一个具体的page后,这个page事实上也能够做为一个控制器存在……
咱们这里的控制器,主要完成根据当前请求实例化View的功能,而且会提供一些view级别但愿单例使用的接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
|
define([
'UIHeader',
'UIToast',
'UILoading',
'UIPageView',
'UIAlert'
], function (UIHeader, UIToast, UILoading, UIPageView, UIAlert) {
return _.inherit({
propertys: function () {
//view搜索目录
this.viewRootPath = 'views/';
//默认view
this.defaultView = 'index';
//当前视图路径
this.viewId;
this.viewUrl;
//视图集
this.views = {};
//是否开启单页应用
// this.isOpenWebapp = _.getHybridInfo().platform == 'baidubox' ? true : false;
this.isOpenWebapp = false;
this.viewMapping = {};
//UIHeader须要释放出来
this.UIHeader = UIHeader;
this.interface = [
'forward',
'back',
'jump',
'showPageView',
'hidePageView',
'showLoading',
'hideLoading',
'showToast',
'hideToast',
'showMessage',
'hideMessage',
'showConfirm',
'hideConfirm',
'openWebapp',
'closeWebapp'
];
},
initialize: function (options) {
this.propertys();
this.setOption(options);
this.initViewPort();
this.initAppMapping();
//开启fastclick
$.bindFastClick && $.bindFastClick();
},
setOption: function (options) {
_.extend(this, options);
},
//建立dom结构
initViewPort: function () {
this.d_header = $('#headerview');
this.d_state = $('#js_page_state');
this.d_viewport = $('#main');
//实例化全局使用的header,这里好像有点不对
this.header = new this.UIHeader({
wrapper: this.d_header
});
//非共享资源,这里应该引入app概念了
this.pageviews = {};
this.toast = new UIToast();
this.loading = new UILoading();
this.alert = new UIAlert();
this.confirm = new UIAlert();
},
openWebapp: function () {
this.isOpenWebapp = true;
},
closeWebapp: function () {
this.isOpenWebapp = false;
},
showPageView: function (name, _viewdata_, id) {
var view = null, k, scope = this.curViewIns || this;
if (!id) id = name;
if (!_.isString(name)) return;
// for (k in _viewdata_) {
// if (_.isFunction(_viewdata_[k])) _viewdata_[k] = $.proxy(_viewdata_[k], scope);
// }
view = this.pageviews[id];
var arr = name.split('/');
var getViewPath = window.getViewPath || window.GetViewPath;
if (!view) {
view = new UIPageView({
// bug fixed by zzx
viewId: arr[arr.length - 1] || name,
viewPath: getViewPath ? getViewPath(name) : name,
_viewdata_: _viewdata_,
onHide: function () {
scope.initHeader();
}
});
this.pageviews[id] = view;
} else {
view.setViewData(_viewdata_);
}
view.show();
},
hidePageView: function (name) {
if (name) {
if (this.pageviews[name]) this.pageviews[name].hide();
} else {
for (var k in this.pageviews) this.pageviews[k].hide();
}
},
showLoading: function () {
this.loading.show();
},
hideLoading: function () {
this.loading.hide();
},
showToast: function (msg, callback) {
this.toast.resetDefaultProperty();
this.toast.content = msg;
if (callback) this.toast.hideAction = callback;
this.toast.refresh();
this.toast.show();
},
hideToast: function () {
this.toast.hide();
},
showMessage: function (param) {
if (_.isString(param)) {
param = { content: param };
}
this.alert.resetDefaultProperty();
this.alert.setOption(param);
this.alert.refresh();
this.alert.show();
},
hideMessage: function () {
this.alert.hide();
},
showConfirm: function (params) {
if (!params) params = {};
if (typeof params == 'string') {
params = {
content: params
};
}
this.confirm.resetDefaultProperty();
//与showMessage不同的地方
this.confirm.btns = [
{ name: '取消', className: 'cm-btns-cancel js_cancel' },
{ name: '肯定', className: 'cm-btns-ok js_ok' }
];
this.confirm.setOption(params);
this.confirm.refresh();
this.confirm.show();
},
hideConfirm: function () {
this.confirm.hide();
},
//初始化app
initApp: function () {
//首次加载不须要走路由控制
this.loadViewByUrl();
//后面的加载所有要通过路由处理
if (this.isOpenWebapp === true)
$(window).on('popstate.app', $.proxy(this.loadViewByUrl, this));
},
loadViewByUrl: function (e) {
this.hidePageView();
var url = decodeURIComponent(location.href).toLowerCase();
var viewId = this.getViewIdRule(url);
viewId = viewId || this.defaultView;
this.viewId = viewId;
this.viewUrl = url;
this.switchView(this.viewId);
},
//@override
getViewIdRule: function (url) {
var viewId = '', hash = '';
var reg = /webapp\/.+\/(.+)\.html/;
var match = url.match(reg);
if (match && match[1]) viewId = match[1];
return viewId;
},
//@override
setUrlRule: function (viewId, param, replace, project) {
var reg = /(webapp\/.+\/)(.+)\.html/;
var url = window.location.href;
var match = url.match(reg);
var proj = project ? 'webapp/' + project : match[1];
var preUrl = '', str = '', i = 0, _k, _v;
//这里这样作有点过于业务了 *bug*
var keepParam = [
'us'
], p;
if (!viewId) return;
if (!match || !match[1]) {
preUrl = url + '/webapp/bus/' + viewId + '.html';
} else {
preUrl = url.substr(0, url.indexOf(match[1])) + proj + viewId + '.html'; ;
}
//特定的参数将会一直带上去,渠道、来源等标志
for (i = 0; i < keepParam.length; i++) {
p = keepParam[i];
if (_.getUrlParam()[p]) {
if (!param) param = {};
param[p] = _.getUrlParam()[p];
}
}
i = 0;
for (k in param) {
_k = encodeURIComponent(_.removeAllSpace(k));
_v = encodeURIComponent(_.removeAllSpace(param[k]));
if (i === 0) {
str += '?' + _k + '=' + _v;
i++;
} else {
str += '&' + _k + '=' + _v;
}
}
url = preUrl + str;
if (this.isOpenWebapp === false) {
window.location = url;
return;
}
if (replace) {
history.replaceState('', {}, url);
} else {
history.pushState('', {}, url);
}
},
switchView: function (id) {
var curView = this.views[id];
//切换前的当前view,立刻会隐藏
var tmpView = this.curView;
if (tmpView && tmpView != curView) {
this.lastView = tmpView;
}
//加载view样式,权宜之计
// this.loadViewStyle(id);
//若是当前view存在,则执行请onload事件
if (curView) {
//若是当前要跳转的view就是当前view的话便不予处理
//这里具体处理逻辑要改*************************************
if (curView == this.curView) {
return;
}
this.curView = curView;
this.curView.show();
this.lastView && this.lastView.hide();
} else {
// this.showLoading();
this.loadView(id, function (View) {
//每次加载结束将状态栏隐藏,这个代码要改
// this.hideLoading();
this.curView = new View({
viewId: id,
refer: this.lastView ? this.lastView.viewId : null,
APP: this,
wrapper: this.d_viewport
});
//设置网页上的view标志
this.curView.$el.attr('page-url', id);
//保存至队列
this.views[id] = this.curView;
this.curView.show();
this.lastView && this.lastView.hide();
});
}
},
//加载view
loadView: function (path, callback) {
var self = this;
requirejs([this.buildUrl(path)], function (View) {
callback && callback.call(self, View);
});
},
//override
//配置可能会有的路径扩展,为Hybrid与各个渠道作适配
initAppMapping: function () {
// console.log('该方法必须被重写');
},
//@override
buildUrl: function (path) {
var mappingPath = this.viewMapping[path];
return mappingPath ? mappingPath : this.viewRootPath + '/' + path + '/' + path;
},
//此处须要一个更新逻辑,好比在index view再点击到index view不会有反应,下次改**************************
forward: function (viewId, param, replace) {
if (!viewId) return;
viewId = viewId.toLowerCase();
this.setUrlRule(viewId, param, replace);
this.loadViewByUrl();
},
jump: function (path, param, replace) {
var viewId;
var project;
if (!path) {
return;
}
path = path.toLowerCase().split('/');
if (path.length <= 0) {
return;
}
viewId = path.pop();
project = path.length === 1 ? path.join('') + '/' : path.join('');
this.setUrlRule(viewId, param, replace, project);
this.loadViewByUrl();
},
back: function (viewId, param, replace) {
if (viewId) {
this.forward(viewId, param, replace)
} else {
if (window.history.length == 1) {
this.forward(this.defaultView, param, replace)
} else {
history.back();
}
}
}
});
});
abstract.app
|
这里属于框架控制器层面的代码,与今天的主题不是很是相关,有兴趣的朋友能够详细读读。
页面基类
这里的核心是页面级别的处理,这里会作比较多的介绍,首先咱们为全部的业务级View提供了一个继承的View:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
|
define([], function () {
'use strict';
return _.inherit({
_propertys: function () {
this.APP = this.APP || window.APP;
var i = 0, len = 0, k;
if (this.APP && this.APP.interface) {
for (i = 0, len = this.APP.interface.length; i < len; i++) {
k = this.APP.interface[i];
if (k == 'showPageView') continue;
if (_.isFunction(this.APP[k])) {
this[k] = $.proxy(this.APP[k], this.APP);
}
else this[k] = this.APP[k];
}
}
this.header = this.APP.header;
},
showPageView: function (name, _viewdata, id) {
this.APP.curViewIns = this;
this.APP.showPageView(name, _viewdata, id)
},
propertys: function () {
//这里设置UI的根节点所处包裹层
this.wrapper = $('#main');
this.id = _.uniqueId('page-view-');
this.classname = '';
this.viewId = null;
this.refer = null;
//模板字符串,各个组件不一样,如今加入预编译机制
this.template = '';
//事件机制
this.events = {};
//自定义事件
//此处须要注意mask 绑定事件先后问题,考虑scroll.radio插件类型的mask应用,考虑组件通讯
this.eventArr = {};
//初始状态为实例化
this.status = 'init';
this._propertys();
},
getViewModel: function () {
//假若有datamodel的话,便直接返回,否则便重写,这里基本为了兼容
if (_.isObject(this.datamodel)) return this.datamodel;
return {};
},
//子类事件绑定若想保留父级的,应该使用该方法
addEvents: function (events) {
if (_.isObject(events)) _.extend(this.events, events);
},
on: function (type, fn, insert) {
if (!this.eventArr[type]) this.eventArr[type] = [];
//头部插入
if (insert) {
this.eventArr[type].splice(0, 0, fn);
} else {
this.eventArr[type].push(fn);
}
},
off: function (type, fn) {
if (!this.eventArr[type]) return;
if (fn) {
this.eventArr[type] = _.without(this.eventArr[type], fn);
} else {
this.eventArr[type] = [];
}
},
trigger: function (type) {
var _slice = Array.prototype.slice;
var args = _slice.call(arguments, 1);
var events = this.eventArr;
var results = [], i, l;
if (events[type]) {
for (i = 0, l = events[type].length; i < l; i++) {
results[results.length] = events[type][i].apply(this, args);
}
}
return results;
},
createRoot: function (html) {
//若是存在style节点,而且style节点不存在的时候须要处理
if (this.style && !$('#page_' + this.viewId)[0]) {
$('head').append($('<style id="page_' + this.viewId + '" class="page-style">' + this.style + '</style>'))
}
//若是具备fake节点,须要移除
$('#fake-page').remove();
//UI的根节点
this.$el = $('<div class="cm-view page-' + this.viewId + ' ' + this.classname + '" id="' + this.id + '">' + html + '</div>');
if (this.wrapper.find('.cm-view')[0]) {
this.wrapper.append(this.$el);
} else {
this.wrapper.html('').append(this.$el);
}
},
_isAddEvent: function (key) {
if (key == 'onCreate' || key == 'onPreShow' || key == 'onShow' || key == 'onRefresh' || key == 'onHide')
return true;
return false;
},
setOption: function (options) {
//这里能够写成switch,开始没有想到有这么多分支
for (var k in options) {
if (k == 'events') {
_.extend(this[k], options[k]);
continue;
} else if (this._isAddEvent(k)) {
this.on(k, options[k])
continue;
}
this[k] = options[k];
}
// _.extend(this, options);
},
initialize: function (opts) {
//这种默认属性
this.propertys();
//根据参数重置属性
this.setOption(opts);
//检测不合理属性,修正为正确数据
this.resetPropery();
this.addEvent();
this.create();
this.initElement();
window.sss = this;
},
$: function (selector) {
return this.$el.find(selector);
},
//提供属性重置功能,对属性作检查
resetPropery: function () { },
//各事件注册点,用于被继承override
addEvent: function () {
},
create: function () {
this.trigger('onPreCreate');
//若是没有传入模板,说明html结构已经存在
this.createRoot(this.render());
this.status = 'create';
this.trigger('onCreate');
},
//实例化须要用到到dom元素
initElement: function () { },
render: function (callback) {
var data = this.getViewModel() || {};
var html = this.template;
if (!this.template) return '';
//引入预编译机制
if (_.isFunction(this.template)) {
html = this.template(data);
} else {
html = _.template(this.template)(data);
}
typeof callback == 'function' && callback.call(this);
return html;
},
refresh: function (needRecreate) {
this.resetPropery();
if (needRecreate) {
this.create();
} else {
this.$el.html(this.render());
}
this.initElement();
if (this.status != 'hide') this.show();
this.trigger('onRefresh');
},
/**
* @description 组件显示方法,首次显示会将ui对象实际由内存插入包裹层
* @method initialize
* @param {Object} opts
*/
show: function () {
this.trigger('onPreShow');
// //若是包含就不要乱搞了
// if (!$.contains(this.wrapper[0], this.$el[0])) {
// //若是须要清空容器的话便清空
// if (this.needEmptyWrapper) this.wrapper.html('');
// this.wrapper.append(this.$el);
// }
this.$el.show();
this.status = 'show';
this.bindEvents();
this.initHeader();
this.trigger('onShow');
},
initHeader: function () { },
hide: function () {
if (!this.$el || this.status !== 'show') return;
this.trigger('onPreHide');
this.$el.hide();
this.status = 'hide';
this.unBindEvents();
this.trigger('onHide');
},
destroy: function () {
this.status = 'destroy';
this.unBindEvents();
this.$root.remove();
this.trigger('onDestroy');
delete this;
},
bindEvents: function () {
var events = this.events;
if (!(events || (events = _.result(this, 'events')))) return this;
this.unBindEvents();
// 解析event参数的正则
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
var key, method, match, eventName, selector;
// 作简单的字符串数据解析
for (key in events) {
method = events[key];
if (!_.isFunction(method)) method = this[events[key]];
if (!method) continue;
match = key.match(delegateEventSplitter);
eventName = match[1], selector = match[2];
method = _.bind(method, this);
eventName += '.delegateUIEvents' + this.id;
if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
}
return this;
},
unBindEvents: function () {
this.$el.off('.delegateUIEvents' + this.id);
return this;
},
getParam: function (key) {
return _.getUrlParam(window.location.href, key)
},
renderTpl: function (tpl, data) {
if (!_.isFunction(tpl)) tpl = _.template(tpl);
return tpl(data);
}
});
});
abstract.view
|
一个Page级别的View会有如下几个关键属性&方法:
① template,html字符串,不包含请求的基础模块,会构成页面的html骨架层
② events,全部的DOM事件定义处,以事件代理的方式定义,因此没必要担忧执行顺序
③ addEvent,用于页面级别各个阶段的监控事件注册点,通常来讲用户只须要关注不多几个事件,好比:
1
2
3
4
5
6
7
8
9
|
//写法
addEvent: function () {
//页面渲染结束,并显示时候触发的事件
this.on('onShow', function () {
});
//离开页面,页面隐藏时候触发的事件
this.on('onHide', function () {
});
}
|
一个页面的基本写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
define(['AbstractView'], function (AbstractView) {
return _.inherit(AbstractView, {
propertys: function ($super) {
$super();
//一堆基础属性定义
//......
//交互业务逻辑
this.events = {
'click .js_pre_day': 'preAction'
};
},
preAction: function (e) { },
addEvent: function () {
this.on('onShow', function () {
//当页面渲染结束,须要作的初始化操做,好比渲染页面
//......
});
this.on('onHide', function () {
});
}
});
});
|
只要按照这种规则写,便能展现页面,而且具有DOM交互事件。
页面模块类
所谓页面模块类,即是用于拆分一个页面为单个组件模块所用类,这里有这些约定:
1
2
3
|
①
一个模块类实例必定会依赖一个Page的基类实例
②
模块类实例经过this.view能够访问到依赖类的一切资源
③
模块类实例与模块之间经过数据entity作通讯
|
这里代码能够再优化,但不是咱们这里关注的重点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
define([], function () {
'use strict';
return _.inherit({
propertys: function () {
//这里设置UI的根节点所处包裹层,必须设置
this.$el = null;
//用于定位dom的选择器
this.selector = '';
//每一个moduleView必须有一个父view,页面级容器
this.view = null;
//模板字符串,各个组件不一样,如今加入预编译机制
this.template = '';
//事件机制
this.events = {};
//实体model,跨模块通讯的桥梁
this.entity = null;
},
setOption: function (options) {
//这里能够写成switch,开始没有想到有这么多分支
for (var k in options) {
if (k == 'events') {
_.extend(this[k], options[k]);
continue;
}
this[k] = options[k];
}
// _.extend(this, options);
},
//@override
initData: function () {
},
//若是传入了dom便
initWrapper: function (el) {
if (el && el[0]) {
this.$el = el;
return;
}
this.$el = this.view.$(this.selector);
},
initialize: function (opts) {
//这种默认属性
this.propertys();
//根据参数重置属性
this.setOption(opts);
this.initData();
this.initWithoutRender();
},
//处理dom已经存在,不须要渲染的状况
initWithoutRender: function () {
if (this.template) return;
var scope = this;
this.view.on('onShow', function () {
scope.initWrapper();
if (!scope.$el[0]) return;
//若是没有父view则不能继续
if (!scope.view) return;
scope.initElement();
scope.bindEvents();
});
},
$: function (selector) {
return this.$el.find(selector);
},
//实例化须要用到到dom元素
initElement: function () { },
//@override
//收集来自各方的实体组成view渲染须要的数据,须要重写
getViewModel: function () {
throw '必须重写';
},
_render: function (callback) {
var data = this.getViewModel() || {};
var html = this.template;
if (!this.template) return '';
//引入预编译机制
if (_.isFunction(this.template)) {
html = this.template(data);
} else {
html = _.template(this.template)(data);
}
typeof callback == 'function' && callback.call(this);
return html;
},
//渲染时必须传入dom映射
render: function () {
this.initWrapper();
if (!this.$el[0]) return;
//若是没有父view则不能继续
if (!this.view) return;
var html = this._render();
this.$el.html(html);
this.initElement();
this.bindEvents();
},
bindEvents: function () {
var events = this.events;
if (!(events || (events = _.result(this, 'events')))) return this;
this.unBindEvents();
// 解析event参数的正则
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
var key, method, match, eventName, selector;
// 作简单的字符串数据解析
for (key in events) {
method = events[key];
if (!_.isFunction(method)) method = this[events[key]];
if (!method) continue;
match = key.match(delegateEventSplitter);
eventName = match[1], selector = match[2];
method = _.bind(method, this);
eventName += '.delegateUIEvents' + this.id;
if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
}
return this;
},
unBindEvents: function () {
this.$el.off('.delegateUIEvents' + this.id);
return this;
}
});
});
module.view
|
数据实体类
这里的数据实体对应着,MVC中的Model,由于以前已经使用model用做了数据请求相关的命名,这里便使用Entity作该工做:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
define([], function () {
/*
一些原则:
init方法时,不可引发其它字段update
*/
var Entity = _.inherit({
initialize: function (opts) {
this.propertys();
this.setOption(opts);
},
propertys: function () {
//只取页面展现须要数据
this.data = {};
//局部数据改变对应的响应程序,暂定为一个方法
//能够是一个类的实例,若是是实例必须有render方法
this.controllers = {};
this.scope = null;
},
subscribe: function (namespace, callback, scope) {
if (typeof namespace === 'function') {
scope = callback;
callback = namespace;
namespace = 'update';
}
if (!namespace || !callback) return;
if (scope) callback = $.proxy(callback, scope);
if (!this.controllers[namespace]) this.controllers[namespace] = [];
this.controllers[namespace].push(callback);
},
unsubscribe: function (namespace) {
if (!namespace) this.controllers = {};
if (this.controllers[namespace]) this.controllers[namespace] = [];
},
publish: function (namespace, data) {
if (!namespace) return;
if (!this.controllers[namespace]) return;
var arr = this.controllers[namespace];
var i, len = arr.length;
for (i = 0; i < len; i++) {
arr[i](data);
}
},
setOption: function (opts) {
for (var k in opts) {
this[k] = opts[k];
}
},
//首次初始化时,须要矫正数据,好比作服务器适配
//@override
handleData: function () { },
//通常用于首次根据服务器数据源填充数据
initData: function (data) {
var k;
if (!data) return;
//若是默认数据没有被覆盖可能有误
for (k in this.data) {
if (data[k]) this.data[k] = data[k];
}
this.handleData();
this.publish('init', this.get());
},
//验证data的有效性,若是无效的话,不该该进行如下逻辑,而且应该报警
//@override
validateData: function () {
return true;
},
//获取数据前,能够进行格式化
//@override
formatData: function (data) {
return data;
},
//获取数据
get: function () {
if (!this.validateData()) {
//须要log
return {};
}
return this.formatData(this.data);
},
//数据跟新后须要作的动做,执行对应的controller改变dom
//@override
update: function (key) {
key = key || 'update';
var data = this.get();
this.publish(key, data);
}
});
return Entity;
});
abstract.entity
|
这里的数据实体会以实例的方式注入给模块类实例,他的工做是起一个中枢左右,完成模块之间的通讯,反正很是重要就是了
其它
数据请求统一使用abstract.model,数据前端缓存使用abstract.store,这里由于目标是作页面拆分,请求模块不是关键,各位能够把这段代码看层一个简单的ajax便可:
1
2
3
|
this.model.setParam({});
this.model.execute(function (data) {
});
|
业务入口
最后简单说下业务入口文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
(function () {
var project = './';
var viewRoot = 'pages';
require.config({
paths: {
//BUS相关模板根目录
IndexPath: project + 'pages/index',
ListPath: project + 'pages/list',
BusStore: project + 'model/bus.store',
BusModel: project + 'model/bus.model'
}
});
require(['AbstractApp', 'UIHeader'], function (APP, UIHeader) {
window.APP = new APP({
UIHeader: UIHeader,
viewRootPath: viewRoot
});
window.APP.initApp();
});
})();
|
很简单的代码,指定了下require的path配置,最后咱们看看入口页面的调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, minimal-ui" />
<meta content="yes" name="apple-mobile-web-app-capable" />
<meta content="black" name="apple-mobile-web-app-status-bar-style" />
<meta name="format-detection" content="telephone=no" />
<link href="../static/css/global.css" rel="stylesheet" type="text/css" />
<title>班次列表</title>
</head>
<body>
<div id="headerview">
<div class="cm-header">
<h1 class="cm-page-title js_title">
正在加载...
</h1>
</div>
</div>
<div class="cm-page-wrap">
<div class="cm-state" id="js_page_state">
</div>
<article class="cm-page" id="main">
</article>
</div>
<script type="text/javascript" src="../blade/libs/zepto.js"></script>
<script src="../blade/libs/fastclick.js" type="text/javascript"></script>
<script type="text/javascript" src="../blade/libs/underscore.js"></script>
<script src="../blade/libs/underscore.extend.js" type="text/javascript"></script>
<script type="text/javascript" src="../blade/libs/require.js"></script>
<script type="text/javascript" src="../blade/common.js"></script>
<script type="text/javascript" src="main.js"></script>
</body>
</html>
list.html
list.html
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
webapp
├─
blade //框架目录
│
├─data
│
├─libs
│
├─mvc
│
└─ui
├─
bus
│
├─model //数据请求模块,彻底可使用zepto ajax替换
│
└─pages
│
├─booking
│
├─index
│
└─list //demo代码模块
└─
static
|
接下来,让咱们真实的开始拆分页面吧。
组件式编程
骨架设计
首先,咱们进行最简单的骨架设计,这里依次是其js代码与模板代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
define(['AbstractView', 'text!ListPath/list.css', 'text!ListPath/tpl.layout.html'], function (AbstractView, style, layoutHtml) {
return _.inherit(AbstractView, {
propertys: function ($super) {
$super();
this.style = style;
this.template = layoutHtml;
},
initHeader: function (name) {
var title = '班次列表';
this.header.set({
view: this,
title: title
});
},
addEvent: function () {
this.on('onShow', function () {
console.log('页面渲染结束');
});
}
});
});
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
<div class="calendar-bar-wrapper js_calendar_wrapper">
日历工具条模块
</div>
<div class="none-data js_none_data" style="display: none;">
当前暂无班次可预订</div>
<div class="js_list_wrapper">
列表模块
</div>
<div class="js_list_loading" style="display: none; text-align: center; padding: 10px 0;">
正在加载...</div>
<ul class="bus-tabs list-filter">
<li class="tabs-item js_show_setoutdate">
<div class="line">
<i class="icon-time"></i>出发时段<i class="icon-sec"></i></div>
<div class="line js_day_sec">
全天</div>
</li>
<li class="tabs-item js_show_setstation">
<div class="line">
<i class="icon-circle icon-setout "></i>出发汽车站<i class="icon-sec"></i></div>
<div class="line js_start_sec">
所有车站</div>
</li>
<li class="tabs-item js_show_arrivalstation">
<div class="line">
<i class="icon-circle icon-arrival "></i>到达汽车站<i class="icon-sec"></i></div>
<div class="line js_arrival_sec">
所有车站</div>
</li>
</ul>
tpl.layout
|
页面展现如图:
日历工具栏的实现
这里要作的第一步是将日历工具栏模块实现,以数据为先的思考,咱们先实现了一个与日历业务有关的数据实体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
define(['AbstractEntity'], function (AbstractEntity) {
var Entity = _.inherit(AbstractEntity, {
propertys: function ($super) {
$super();
var n = new Date();
var curTime = new Date(n.getFullYear(), n.getMonth(), n.getDate()).getTime();
this.data = {
date: curTime,
title: '当前日期'
};
},
set: function (date) {
if (!date) return;
if (_.isDate(date)) date = date.getTime();
if (typeof date === 'string') date = parseInt(date);
this.data.date = date;
this.update();
},
getDateStr: function () {
var date = new Date();
date.setTime(this.data.date);
var dateDetail = _.dateUtil.getDetail(date);
var name = dateDetail.year + '-' + dateDetail.month + '-' + dateDetail.day + ' ' + dateDetail.weekday + (dateDetail.day1 ? '(' + dateDetail.day1 + ')' : '');
return name;
},
nextDay: function () {
this.set(this.getDate() + 86400000);
return true;
},
getDate: function () {
return parseInt(this.data.date);
},
//是否可以再往前一天
canPreDay: function () {
var n = new Date();
var curTime = new Date(n.getFullYear(), n.getMonth(), n.getDate()).getTime();
//若是当前日期已是第一天,则不可预订
if (curTime <= this.getDate() - 86400000) {
return true;
}
return false;
},
preDay: function () {
if (!this.canPreDay()) return false;
this.set(this.getDate() - 86400000);
return true;
}
});
return Entity;
});
en.date
|
里面完成日期工具栏全部相关数据操做,而且不包含实际的业务逻辑。
而后这里开始设计日期工具栏的模块View:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
define(['ModuleView', 'UICalendarBox', 'text!ListPath/tpl.calendar.bar.html'], function (ModuleView, UICalendarBox, tpl) {
return _.inherit(ModuleView, {
//此处如果要使用model,处实例化时候必定要保证entity的存在,若是不存在即是业务BUG
initData: function () {
this.template = tpl;
this.events = {
'click .js_pre_day': 'preAction',
'click .js_next_day': 'nextAction',
'click .js_show_calendar': 'showCalendar'
};
//初始化时候须要执行的回调
this.dateEntity.subscribe('init', this.render, this);
this.dateEntity.subscribe(this.render, this);
},
initDate: function () {
var t = new Date().getTime();
//默认状况下获取当前日期,也有过了18.00就设置为次日日期
//当时一旦url上有startdatetime参数的话,便须要使用之
if (_.getUrlParam().startdatetime) t = _.getUrlParam().startdatetime;
this.dateEntity.initData({
date: t
});
},
getViewModel: function () {
var data = this.dateEntity.get();
data.formatStr = this.dateEntity.getDateStr();
data.canPreDay = this.dateEntity.canPreDay();
return data;
},
preAction: function () {
if (this.dateEntity.preDay()) return;
this.view.showToast('前一天不可预订');
},
nextAction: function () {
this.dateEntity.nextDay();
},
showCalendar: function () {
var scope = this, endDate = new Date();
var secDate = new Date();
secDate.setTime(this.dateEntity.getDate());
endDate.setTime(new Date().getTime() + 2592000000);
if (!this.calendar) {
this.calendar = new UICalendarBox({
endTime: endDate,
selectDate: secDate,
onItemClick: function (date, el, e) {
scope.dateEntity.set(date);
this.hide();
}
});
} else {
this.calendar.calendar.selectDate = secDate;
this.calendar.calendar.refresh();
}
this.calendar.show();
}
});
});
mod.date
|
这个组件模块干了几个事情:
① 首先,dateEntity实体须要由list.js这个主view注入
② 这里为dateEntity注册了两个数据响应事件:
1
2
|
this.dateEntity.subscribe('init', this.render, this);
this.dateEntity.subscribe(this.render, this);
|
render方法继承至基类,使用template与数据生成html,其中数据产生必须重写父类一个方法:
1
2
3
4
5
6
|
getViewModel: function () {
var data = this.dateEntity.get();
data.formatStr = this.dateEntity.getDateStr();
data.canPreDay = this.dateEntity.canPreDay();
return data;
},
|
由于这里的日历数据,默认取当前时间,可是url参数可能传递日期参数,因此定义了一个数据初始化方法:
1
2
3
4
5
6
7
8
9
|
initDate: function () {
var t = new Date().getTime();
//默认状况下获取当前日期,也有过了18.00就设置为次日日期
//当时一旦url上有startdatetime参数的话,便须要使用之
if (_.getUrlParam().startdatetime) t = _.getUrlParam().startdatetime;
this.dateEntity.initData({
date: t
});
},
|
该方法在主页面渲染结束后会第一时间调用,这个时候日历工具栏便渲染出来,其中日历组件的使用便不予理睬了,主控制器的代码改变以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
define([
'AbstractView',
'text!ListPath/list.css',
'ListPath/en.date',
'ListPath/mod.date',
'text!ListPath/tpl.layout.html'
], function (
AbstractView,
style,
DateEntity,
DateModule,
layoutHtml
) {
return _.inherit(AbstractView, {
_initEntity: function () {
this.dateEntity = new DateEntity();
},
_initModule: function () {
this.dateModule = new DateModule({
view: this,
selector: '.js_calendar_wrapper',
dateEntity: this.dateEntity
});
},
propertys: function ($super) {
$super();
this._initEntity();
this._initModule();
this.style = style;
this.template = layoutHtml;
},
initHeader: function (name) {
var title = '班次列表';
this.header.set({
view: this,
title: title
});
},
addEvent: function () {
this.on('onShow', function () {
//初始化date数据
this.dateModule.initDate();
});
}
});
});
list.js
|
1
2
3
4
5
6
7
8
9
10
11
|
_initEntity: function () {
this.dateEntity = new DateEntity();
},
_initModule: function () {
this.dateModule = new DateModule({
view: this,
selector: '.js_calendar_wrapper',
dateEntity: this.dateEntity
});
},
|
1
2
3
4
5
6
7
|
addEvent: function () {
this.on('onShow', function () {
//初始化date数据
this.dateModule.initDate();
});
}
|
因而,整个界面变成了这个样子:
这里是对应的日历工具模板文件tpl.calendar.html:
1
2
3
4
5
|
<ul class="bus-tabs calendar-bar">
<li class="tabs-item js_pre_day <%=!canPreDay ? 'disabled' : '' %>">前一天</li>
<li class="tabs-item js_show_calendar" style="-webkit-flex: 2; flex: 2;"><%=formatStr %></li>
<li class="tabs-item js_next_day">后一天</li>
</ul>
|
搜索工具栏的实现
咱们如今的页面,就算不传任何URL参数,已经能渲染出部分页面了,可是下面出发站汽车等业务数据必须等待班次列表数据请求结束才能替换数据,可是这些数据若是没有出发城市和到达城市是不能发起请求的,因此这里先实现搜索工具栏功能:
在出发城市或者到达城市不存在的话便弹出搜索工具栏,引导用户选择城市,这里新增弹出层须要在主页面控制器(检测主控制器)中使用一个UI组件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
define([
'AbstractView',
'text!ListPath/list.css',
'ListPath/en.date',
'ListPath/mod.date',
'text!ListPath/tpl.layout.html',
'text!ListPath/tpl.search.box.html',
'UIScrollLayer'
], function (
AbstractView,
style,
DateEntity,
DateModule,
layoutHtml,
searchBoxHtml,
UIScrollLayer
) {
return _.inherit(AbstractView, {
_initEntity: function () {
this.dateEntity = new DateEntity();
},
_initModule: function () {
this.dateModule = new DateModule({
view: this,
selector: '.js_calendar_wrapper',
dateEntity: this.dateEntity
});
},
propertys: function ($super) {
$super();
this._initEntity();
this._initModule();
this.style = style;
this.template = layoutHtml;
},
initHeader: function (name) {
var title = '班次列表';
this.header.set({
view: this,
title: title,
back: function () {
console.log('回退');
},
right: [
{
tagname: 'search-bar',
value: '搜索',
callback: function () {
console.log('弹出搜索框');
this.showSearchBox();
}
}
]
});
},
//搜索工具弹出层
showSearchBox: function () {
var scope = this;
if (!this.searchBox) {
this.searchBox = new UIScrollLayer({
title: '请选择搜索条件',
html: searchBoxHtml,
events: {
'click .js-start': function () {
},
'click .js-arrive': function () {
},
'click .js_search_list': function () {
console.log('查询列表');
}
}
});
}
this.searchBox.show();
},
addEvent: function () {
this.on('onShow', function () {
//初始化date数据
this.dateModule.initDate();
//这里判断是否须要弹出搜索弹出层
if (!_.getUrlParam().startcityid || !_.getUrlParam().arrivalcityid) {
this.showSearchBox();
return;
}
});
}
});
});
list.js
|
对应搜索弹出层html模板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<div class="c-row search-line" data-flag="start">
<div class="c-span3">
出发</div>
<div class="c-span9 js-start search-line-txt">
请选择出发地</div>
</div>
<div class="c-row search-line" data-flag="arrive">
<div class="c-span3">
到达</div>
<div class="c-span9 js-arrive search-line-txt">
请选择到达地</div>
</div>
<div class="c-row " data-flag="arrive">
<span class="btn-primary full-width js_search_list">查询</span>
</div>
tpl.search.box.html
|
这里核心代码是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//搜索工具弹出层
showSearchBox: function () {
var scope = this;
if (!this.searchBox) {
this.searchBox = new UIScrollLayer({
title: '请选择搜索条件',
html: searchBoxHtml,
events: {
'click .js-start': function () {
},
'click .js-arrive': function () {
},
'click .js_search_list': function () {
console.log('查询列表');
}
}
});
}
this.searchBox.show();
},
|
因而当URL什么参数都没有的时候,就会弹出这个搜索框
这里也迎来了一个难点,由于城市列表事实上应该是一个独立的可访问的页面,可是这里是想用弹出层的方式调用他,因此我在APP层实现了一个方法能够用弹出层的方式调起一个独立的页面。
1
2
|
注意:
这里
city城市列表未彻底采用组件化的方式开发,有兴趣的朋友能够本身尝试着开发
|
这里有一个不一样的地方是,由于咱们点击查询的时候才会作实体数据更新,这里是单纯的作DOM操做了,这里不设置数据实体一个缘由就是:
这个搜索弹出层是一个页面级DOM以外的部分,数据实体变化通常只应该影响Page级别的DOM,除非真的有两个页面级View会公用一个数据实体。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
|
define([
'AbstractView',
'text!ListPath/list.css',
'ListPath/en.date',
'ListPath/mod.date',
'text!ListPath/tpl.layout.html',
'text!ListPath/tpl.search.box.html',
'UIScrollLayer'
], function (
AbstractView,
style,
DateEntity,
DateModule,
layoutHtml,
searchBoxHtml,
UIScrollLayer
) {
return _.inherit(AbstractView, {
_initEntity: function () {
this.dateEntity = new DateEntity();
},
_initModule: function () {
this.dateModule = new DateModule({
view: this,
selector: '.js_calendar_wrapper',
dateEntity: this.dateEntity
});
},
propertys: function ($super) {
$super();
this._initEntity();
this._initModule();
this.style = style;
this.template = layoutHtml;
//主控制器业务属性
this.urlData = {
start: {},
end: {}
};
},
initHeader: function (name) {
var title = '班次列表';
this.header.set({
view: this,
title: title,
back: function () {
console.log('回退');
},
right: [
{
tagname: 'search-bar',
value: '搜索',
callback: function () {
console.log('弹出搜索框');
this.showSearchBox();
}
}
]
});
},
//搜索工具弹出层
showSearchBox: function () {
var scope = this;
if (!this.searchBox) {
this.searchBox = new UIScrollLayer({
title: '请选择搜索条件',
html: searchBoxHtml,
events: {
'click .js-start': function (e) {
scope._showCityView('start', $(e.currentTarget));
},
'click .js-arrive': function (e) {
scope._showCityView('end', $(e.currentTarget));
},
'click .js_search_list': function () {
var param = {};
if (!scope.urlData.start.id) {
scope.showToast('请先选择出发城市');
return;
}
if (!scope.urlData.end.id) {
scope.showToast('请先选择到达城市');
return;
}
//这里必定会有出发城市与到达城市等数据
param.startcityid = scope.urlData.start.id;
param.arrivalcityid = scope.urlData.end.id;
param.startdatetime = scope.dateEntity.getDate();
param.startname = scope.urlData.start.name;
param.arrivename = scope.urlData.end.name;
if (scope.urlData.start.station) {
param.startstationid = scope.urlData.start.station
}
if (scope.urlData.end.station) {
param.arrivalstationid = end_station
}
scope.forward('list', param);
this.hide();
}
}
});
}
this.searchBox.show();
},
_showCityView: function (key, el) {
var scope = this;
if (key == 'end') {
//由于到达车站会依赖出发车站的数据,因此这里得先作判断
if (!this.urlData.start.id) {
this.showToast('请先选择出发城市');
return;
}
}
this.showPageView('city', {
flag: key,
startId: this.urlData.start.id,
type: this.urlData.start.type,
onCityItemClick: function (id, name, station, type) {
scope.urlData[key] = {};
scope.urlData[key]['id'] = id;
scope.urlData[key]['type'] = type;
scope.urlData[key]['name'] = name;
if (station) scope.urlData[key]['name'] = station;
el.text(name);
scope.hidePageView();
},
onBackAction: function () {
scope.hidePageView();
}
});
},
addEvent: function () {
this.on('onShow', function () {
//初始化date数据
this.dateModule.initDate();
//这里判断是否须要弹出搜索弹出层
if (!_.getUrlParam().startcityid || !_.getUrlParam().arrivalcityid) {
this.showSearchBox();
return;
}
});
}
});
});
list.js
|
搜索功能完成后,咱们这里即可以进入真正的数据请求功能渲染列表了。
其他模块
在实现数据请求以前,我按照日期模块的方式将下面三个模块的功能也一并完成了,这里惟一不一样的是,这些模块的DOM已经存在,咱们不须要渲染了,完成后的代码大概是这样的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
|
define([
'AbstractView',
'text!ListPath/list.css',
'ListPath/en.station',
'ListPath/en.date',
'ListPath/en.time',
'ListPath/mod.date',
'ListPath/mod.time',
'ListPath/mod.setout',
'ListPath/mod.arrive',
'text!ListPath/tpl.layout.html',
'text!ListPath/tpl.search.box.html',
'UIScrollLayer'
], function (
AbstractView,
style,
StationEntity,
DateEntity,
TimeEntity,
DateModule,
TimeModule,
SetoutModule,
ArriveModule,
layoutHtml,
searchBoxHtml,
UIScrollLayer
) {
return _.inherit(AbstractView, {
_initEntity: function () {
this.dateEntity = new DateEntity();
this.timeEntity = new TimeEntity();
this.timeEntity.subscribe('init', this.renderTime, this);
this.timeEntity.subscribe(this.renderTime, this);
this.setoutEntity = new StationEntity();
this.setoutEntity.subscribe('init', this.renderSetout, this);
this.setoutEntity.subscribe(this.renderSetout, this);
this.arriveEntity = new StationEntity();
this.arriveEntity.subscribe('init', this.renderArrive, this);
this.arriveEntity.subscribe(this.renderArrive, this);
},
_initModule: function () {
this.dateModule = new DateModule({
view: this,
selector: '.js_calendar_wrapper',
dateEntity: this.dateEntity
});
this.timeModule = new TimeModule({
view: this,
selector: '.js_show_setoutdate',
timeEntity: this.timeEntity
});
this.setOutModule = new SetoutModule({
view: this,
selector: '.js_show_setstation',
setoutEntity: this.setoutEntity
});
this.arriveModule = new ArriveModule({
view: this,
selector: '.js_show_arrivalstation',
arriveEntity: this.arriveEntity
});
},
propertys: function ($super) {
$super();
this._initEntity();
this._initModule();
this.style = style;
this.template = layoutHtml;
//主控制器业务属性
this.urlData = {
start: {},
end: {}
};
},
initHeader: function (name) {
var title = '班次列表';
this.header.set({
view: this,
title: title,
back: function () {
console.log('回退');
},
right: [
{
tagname: 'search-bar',
value: '搜索',
callback: function () {
console.log('弹出搜索框');
this.showSearchBox();
}
}
]
});
},
initElement: function () {
this.d_list_wrapper = this.$('.js_list_wrapper');
this.d_none_data = this.$('.js_none_data');
this.d_js_show_setoutdate = this.$('.js_show_setoutdate');
this.d_js_show_setstation = this.$('.js_show_setstation');
this.d_js_show_arrivalstation = this.$('.js_show_arrivalstation');
this.d_js_list_loading = this.$('.js_list_loading');
this.d_js_tabs = this.$('.js_tabs');
this.d_js_day_sec = this.$('.js_day_sec');
this.d_js_start_sec = this.$('.js_start_sec');
this.d_js_arrival_sec = this.$('.js_arrival_sec');
},
//搜索工具弹出层
showSearchBox: function () {
var scope = this;
if (!this.searchBox) {
this.searchBox = new UIScrollLayer({
title: '请选择搜索条件',
html: searchBoxHtml,
events: {
'click .js-start': function (e) {
scope._showCityView('start', $(e.currentTarget));
|