前端已经过了单兵作战的时代了,现在一个稍微复杂一点的项目都需要几个人协同开发,一个战略级别的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,这里借鉴之前的代码(【组件化开发】前端进阶篇之如何编写可维护可升级的代码):
1 define([], function () { 2 'use strict'; 3 4 return _.inherit({ 5 6 showPageView: function (name, _viewdata, id) { 7 this.APP.curViewIns = this; 8 this.APP.showPageView(name, _viewdata, id) 9 }, 10 11 propertys: function () { 12 //这里设置UI的根节点所处包裹层 13 this.wrapper = $('#main'); 14 this.id = _.uniqueId('page-view-'); 15 this.classname = ''; 16 17 this.viewId = null; 18 this.refer = null; 19 20 //模板字符串,各个组件不同,现在加入预编译机制 21 this.template = ''; 22 //事件机制 23 this.events = {}; 24 25 //自定义事件 26 //此处需要注意mask 绑定事件前后问题,考虑scroll.radio插件类型的mask应用,考虑组件通信 27 this.eventArr = {}; 28 29 //初始状态为实例化 30 this.status = 'init'; 31 }, 32 33 //子类事件绑定若想保留父级的,应该使用该方法 34 addEvents: function (events) { 35 if (_.isObject(events)) _.extend(this.events, events); 36 }, 37 38 on: function (type, fn, insert) { 39 if (!this.eventArr[type]) this.eventArr[type] = []; 40 41 //头部插入 42 if (insert) { 43 this.eventArr[type].splice(0, 0, fn); 44 } else { 45 this.eventArr[type].push(fn); 46 } 47 }, 48 49 off: function (type, fn) { 50 if (!this.eventArr[type]) return; 51 if (fn) { 52 this.eventArr[type] = _.without(this.eventArr[type], fn); 53 } else { 54 this.eventArr[type] = []; 55 } 56 }, 57 58 trigger: function (type) { 59 var _slice = Array.prototype.slice; 60 var args = _slice.call(arguments, 1); 61 var events = this.eventArr; 62 var results = [], i, l; 63 64 if (events[type]) { 65 for (i = 0, l = events[type].length; i < l; i++) { 66 results[results.length] = events[type][i].apply(this, args); 67 } 68 } 69 return results; 70 }, 71 72 createRoot: function (html) { 73 74 //如果存在style节点,并且style节点不存在的时候需要处理 75 if (this.style && !$('#page_' + this.viewId)[0]) { 76 $('head').append($('<style >)) 77 } 78 79 //如果具有fake节点,需要移除 80 $('#fake-page').remove(); 81 82 //UI的根节点 83 this.$el = $('<div class="cm-view page-' + this.viewId + ' ' + this.classname + '" style="display: none; " >); 84 if (this.wrapper.find('.cm-view')[0]) { 85 this.wrapper.append(this.$el); 86 } else { 87 this.wrapper.html('').append(this.$el); 88 } 89 90 }, 91 92 _isAddEvent: function (key) { 93 if (key == 'onCreate' || key == 'onPreShow' || key == 'onShow' || key == 'onRefresh' || key == 'onHide') 94 return true; 95 return false; 96 }, 97 98 setOption: function (options) { 99 //这里可以写成switch,开始没有想到有这么多分支 100 for (var k in options) { 101 if (k == 'events') { 102 _.extend(this[k], options[k]); 103 continue; 104 } else if (this._isAddEvent(k)) { 105 this.on(k, options[k]) 106 continue; 107 } 108 this[k] = options[k]; 109 } 110 // _.extend(this, options); 111 }, 112 113 initialize: function (opts) { 114 //这种默认属性 115 this.propertys(); 116 //根据参数重置属性 117 this.setOption(opts); 118 //检测不合理属性,修正为正确数据 119 this.resetPropery(); 120 121 this.addEvent(); 122 this.create(); 123 124 this.initElement(); 125 126 window.sss = this; 127 128 }, 129 130 $: function (selector) { 131 return this.$el.find(selector); 132 }, 133 134 //提供属性重置功能,对属性做检查 135 resetPropery: function () { }, 136 137 //各事件注册点,用于被继承override 138 addEvent: function () { 139 }, 140 141 create: function () { 142 this.trigger('onPreCreate'); 143 //如果没有传入模板,说明html结构已经存在 144 this.createRoot(this.render()); 145 146 this.status = 'create'; 147 this.trigger('onCreate'); 148 }, 149 150 //实例化需要用到到dom元素 151 initElement: function () { }, 152 153 render: function (callback) { 154 var html = this.template; 155 if (!this.template) return ''; 156 //引入预编译机制 157 if (_.isFunction(this.template)) { 158 html = this.template(data); 159 } else { 160 html = _.template(this.template)({}); 161 } 162 typeof callback == 'function' && callback.call(this); 163 return html; 164 }, 165 166 refresh: function (needRecreate) { 167 this.resetPropery(); 168 if (needRecreate) { 169 this.create(); 170 } else { 171 this.$el.html(this.render()); 172 } 173 this.initElement(); 174 if (this.status != 'hide') this.show(); 175 this.trigger('onRefresh'); 176 }, 177 178 /** 179 * @description 组件显示方法,首次显示会将ui对象实际由内存插入包裹层 180 * @method initialize 181 * @param {Object} opts 182 */ 183 show: function () { 184 this.trigger('onPreShow'); 185 186 this.$el.show(); 187 this.status = 'show'; 188 189 this.bindEvents(); 190 191 this.initHeader(); 192 this.trigger('onShow'); 193 }, 194 195 initHeader: function () { }, 196 197 hide: function () { 198 if (!this.$el || this.status !== 'show') return; 199 200 this.trigger('onPreHide'); 201 this.$el.hide(); 202 203 this.status = 'hide'; 204 this.unBindEvents(); 205 this.trigger('onHide'); 206 }, 207 208 destroy: function () { 209 this.status = 'destroy'; 210 this.unBindEvents(); 211 this.$root.remove(); 212 this.trigger('onDestroy'); 213 delete this; 214 }, 215 216 bindEvents: function () { 217 var events = this.events; 218 219 if (!(events || (events = _.result(this, 'events')))) return this; 220 this.unBindEvents(); 221 222 // 解析event参数的正则 223 var delegateEventSplitter = /^(\S+)\s*(.*)$/; 224 var key, method, match, eventName, selector; 225 226 // 做简单的字符串数据解析 227 for (key in events) { 228 method = events[key]; 229 if (!_.isFunction(method)) method = this[events[key]]; 230 if (!method) continue; 231 232 match = key.match(delegateEventSplitter); 233 eventName = match[1], selector = match[2]; 234 method = _.bind(method, this); 235 eventName += '.delegateViewEvents' + this.id; 236 237 if (selector === '') { 238 this.$el.on(eventName, method); 239 } else { 240 this.$el.on(eventName, selector, method); 241 } 242 } 243 244 return this; 245 }, 246 247 unBindEvents: function () { 248 this.$el.off('.delegateViewEvents' + this.id); 249 return this; 250 }, 251 252 getParam: function (key) { 253 return _.getUrlParam(window.location.href, key) 254 }, 255 256 renderTpl: function (tpl, data) { 257 if (!_.isFunction(tpl)) tpl = _.template(tpl); 258 return tpl(data); 259 } 260 261 262 }); 263 264 });
有了View的代码后便需要组件级别的代码,正如之前所说,这里的组件只有根元素与子组件两层的层级:
1 define([], function () { 2 'use strict'; 3 4 return _.inherit({ 5 6 propertys: function () { 7 //这里设置UI的根节点所处包裹层,必须设置 8 this.$el = null; 9 10 //用于定位dom的选择器 11 this.selector = ''; 12 13 //每个moduleView必须有一个父view,页面级容器 14 this.view = null; 15 16 //模板字符串,各个组件不同,现在加入预编译机制 17 this.template = ''; 18 19 //事件机制 20 this.events = {}; 21 22 //实体model,跨模块通信的桥梁 23 this.entity = null; 24 }, 25 26 setOption: function (options) { 27 //这里可以写成switch,开始没有想到有这么多分支 28 for (var k in options) { 29 if (k == 'events') { 30 _.extend(this[k], options[k]); 31 continue; 32 } 33 this[k] = options[k]; 34 } 35 // _.extend(this, options); 36 }, 37 38 //@override 39 initData: function () { 40 }, 41 42 //如果传入了dom便 43 initWrapper: function (el) { 44 if (el && el[0]) { 45 this.$el = el; 46 return; 47 } 48 this.$el = this.view.$(this.selector); 49 }, 50 51 initialize: function (opts) { 52 53 //这种默认属性 54 this.propertys(); 55 //根据参数重置属性 56 this.setOption(opts); 57 this.initData(); 58 59 this.initWithoutRender(); 60 61 }, 62 63 //当父容器关闭后,其对应子容器也应该隐藏 64 bindViewEvent: function () { 65 if (!this.view) return; 66 var scope = this; 67 this.view.on('onHide', function () { 68 scope.onHide(); 69 }); 70 }, 71 72 //处理dom已经存在,不需要渲染的情况 73 initWithoutRender: function () { 74 if (this.template) return; 75 var scope = this; 76 this.view.on('onShow', function () { 77 scope.initWrapper(); 78 if (!scope.$el[0]) return; 79 //如果没有父view则不能继续 80 if (!scope.view) return; 81 scope.initElement(); 82 scope.bindEvents(); 83 }); 84 }, 85 86 $: function (selector) { 87 return this.$el.find(selector); 88 }, 89 90 //实例化需要用到到dom元素 91 initElement: function () { }, 92 93 //@override 94 //收集来自各方的实体组成view渲染需要的数据,需要重写 95 getViewModel: function () { 96 throw '必须重写'; 97 }, 98 99 _render: function (callback) { 100 var data = this.getViewModel() || {}; 101 var html = this.template; 102 if (!this.template) return ''; 103 //引入预编译机制 104 if (_.isFunction(this.template)) { 105 html = this.template(data); 106 } else { 107 html = _.template(this.template)(data); 108 } 109 typeof callback == 'function' && callback.call(this); 110 return html; 111 }, 112 113 //渲染时必须传入dom映射 114 render: function () { 115 this.initWrapper(); 116 if (!this.$el[0]) return; 117 118 //如果没有父view则不能继续 119 if (!this.view) return; 120 121 var html = this._render(); 122 this.$el.html(html); 123 this.initElement(); 124 this.bindEvents(); 125 126 }, 127 128 bindEvents: function () { 129 var events = this.events; 130 131 if (!(events || (events = _.result(this, 'events')))) return this; 132 this.unBindEvents(); 133 134 // 解析event参数的正则 135 var delegateEventSplitter = /^(\S+)\s*(.*)$/; 136 var key, method, match, eventName, selector; 137 138 // 做简单的字符串数据解析 139 for (key in events) { 140 method = events[key]; 141 if (!_.isFunction(method)) method = this[events[key]]; 142 if (!method) continue; 143 144 match = key.match(delegateEventSplitter); 145 eventName = match[1], selector = match[2]; 146 method = _.bind(method, this); 147 eventName += '.delegateUIEvents' + this.id; 148 149 if (selector === '') { 150 this.$el.on(eventName, method); 151 } else { 152 this.$el.on(eventName, selector, method); 153 } 154 } 155 156 return this; 157 }, 158 159 unBindEvents: function () { 160 this.$el.off('.delegateUIEvents' + this.id); 161 return this; 162 } 163 }); 164 165 });
有了根View与View组件的实现,剩下的便是数据实体的实现,View与组件Module之间通信的桥梁就是数据Entity,事实上我们的View或者组件模块未必会需要数据实体Entity,只有在业务逻辑的复杂度达到一定阶段才需要分模块,如果dom操作过多的话就需要Entity了:
1 define([], function () { 2 /* 3 一些原则: 4 init方法时,不可引起其它字段update 5 */ 6 var Entity = _.inherit({ 7 initialize: function (opts) { 8 this.propertys(); 9 this.setOption(opts); 10 }, 11 12 propertys: function () { 13 //只取页面展示需要数据 14 this.data = {}; 15 16 //局部数据改变对应的响应程序,暂定为一个方法 17 //可以是一个类的实例,如果是实例必须有render方法 18 this.controllers = {}; 19 20 this.scope = null; 21 22 }, 23 24 subscribed: function (namespace, callback, scope) { 25 this.subscribed(); 26 }, 27 28 unsubscribed: function (namespace) { 29 this.unsubscribe(namespace); 30 }, 31 32 subscribe: function (namespace, callback, scope) { 33 if (typeof namespace === 'function') { 34 scope = callback; 35 callback = namespace; 36 namespace = 'update'; 37 } 38 if (!namespace || !callback) return; 39 if (scope) callback = $.proxy(callback, scope); 40 if (!this.controllers[namespace]) this.controllers[namespace] = []; 41 this.controllers[namespace].push(callback); 42 }, 43 44 unsubscribe: function (namespace) { 45 if (!namespace) this.controllers = {}; 46 if (this.controllers[namespace]) this.controllers[namespace] = []; 47 }, 48 49 publish: function (namespace, data) { 50 if (!namespace) return; 51 if (!this.controllers[namespace]) return; 52 var arr = this.controllers[namespace]; 53 var i, len = arr.length; 54 for (i = 0; i < len; i++) { 55 arr[i](data); 56 } 57 }, 58 59 setOption: function (opts) { 60 for (var k in opts) { 61 this[k] = opts[k]; 62 } 63 }, 64 65 //首次初始化时,需要矫正数据,比如做服务器适配 66 //@override 67 handleData: function () { }, 68 69 //一般用于首次根据服务器数据源填充数据 70 initData: function (data) { 71 var k; 72 if (!data) return; 73 74 //如果默认数据没有被覆盖可能有误 75 for (k in this.data) { 76 if (data[k]) this.data[k] = data[k]; 77 } 78 79 this.handleData(); 80 this.publish('init', this.get()); 81 }, 82 83 //验证data的有效性,如果无效的话,不应该进行以下逻辑,并且应该报警 84 //@override 85 validateData: function () { 86 return true; 87 }, 88 89 //获取数据前,可以进行格式化 90 //@override 91 formatData: function (data) { 92 return data; 93 }, 94 95 //获取数据 96 get: function () { 97 if (!this.validateData()) { 98 //需要log 99 return {}; 100 } 101 return this.formatData(this.data); 102 }, 103 104 //数据跟新后需要做的动作,执行对应的controller改变dom 105 //@override 106 update: function (key) { 107 key = key || 'update'; 108 var data = this.get(); 109 this.publish(key, data); 110 } 111 112 }); 113 114 return Entity; 115 });
我们这里抽取一段火车票列表的筛选功能做实现,这个页面有一定复杂度又不是太难,大概页面是这个样子的:
因为,我们这里的关注点在View,这里就直接将网上上海到北京的数据给拿下来:
1 this.listData = [ 2 { 3 "train_no": "87000K180502", 4 "train_number": "K1805", 5 "from_station": "上海南", 6 "to_station": "杭州", 7 "from_time": "04:22", 8 "to_time": "06:29", 9 "from_station_type": "途经", 10 "to_station_type": "终点", 11 "day_diff": "0", 12 "use_time": "127", 13 "sale_time": "15:30", 14 "control_day": 59, 15 "from_telecode": "SNH", 16 "to_telecode": "HZH", 17 "can_web_buy": "Y", 18 "note": "", 19 "seats": [{ 20 "seat_price": "28.5", 21 "seat_name": "硬座", 22 "seat_bookable": 1, 23 "seat_yupiao": 555 24 }, { 25 "seat_price": "74.5", 26 "seat_name": "硬卧上", 27 "seat_bookable": 1, 28 "seat_yupiao": 551 29 }, { 30 "seat_price": "77.5", 31 "seat_name": "硬卧中", 32 "seat_bookable": 1, 33 "seat_yupiao": 551 34 }, { 35 "seat_price": "84.5", 36 "seat_name": "硬卧下", 37 "seat_bookable": 1, 38 "seat_yupiao": 551 39 }, { 40 "seat_price": "112.5", 41 "seat_name": "软卧上", 42 "seat_bookable": 1, 43 "seat_yupiao": 28 44 }, { 45 "seat_price": "127.0", 46 "seat_name": "软卧下", 47 "seat_bookable": 1, 48 "seat_yupiao": 28 49 }, {"seat_price": "28.5", "seat_name": "无座", "seat_bookable": 1, "seat_yupiao": 334}] 50 }, { 51 "train_no": "47000K151100", 52 "train_number": "K1511", 53 "from_station": "上海南", 54 "to_station": "杭州东", 55 "from_time": "04:56", 56 "to_time": "06:45", 57 "from_station_type": "途经", 58 "to_station_type": "途经", 59 "day_diff": "0", 60 "use_time": "109", 61 "sale_time": "15:30", 62 "control_day": 59, 63 "from_telecode": "SNH", 64 "to_telecode": "HGH", 65 "can_web_buy": "Y", 66 "note": "", 67 "seats": [{ 68 "seat_price": "24.5", 69 "seat_name": "硬座", 70 "seat_bookable": 1, 71 "seat_yupiao": 8 72 }, { 73 "seat_price": "70.5", 74 "seat_name": "硬卧上", 75 "seat_bookable": 1, 76 "seat_yupiao": 8 77 }, { 78 "seat_price": "73.5", 79 "seat_name": "硬卧中", 80 "seat_bookable": 1, 81 "seat_yupiao": 8 82 }, { 83 "seat_price": "80.0", 84 "seat_name": "硬卧下", 85 "seat_bookable": 1, 86 "seat_yupiao": 8 87 }, { 88 "seat_price": "108.5", 89 "seat_name": "软卧上", 90 "seat_bookable": 1, 91 "seat_yupiao": 1 92 }, { 93 "seat_price": "122.5", 94 "seat_name": "软卧下", 95 "seat_bookable": 1, 96 "seat_yupiao": 1 97 }, {"seat_price": "24.5", "seat_name": "无座", "seat_bookable": 0, "seat_yupiao": 0}] 98 }, { 99 "train_no": "1100000K7509", 100 "train_number": "K75", 101 "from_station": "上海南", 102 "to_station": "杭州东", 103 "from_time": "05:02", 104 "to_time": "06:58", 105 "from_station_type": "途经", 106 "to_station_type": "途经", 107 "day_diff": "0", 108 "use_time": "116", 109 "sale_time": "15:30", 110 "control_day": 59, 111 "from_telecode": "SNH", 112 "to_telecode": "HGH", 113 "can_web_buy": "Y", 114 "note": "", 115 "seats": [{ 116 "seat_price": "24.5", 117 "seat_name": "硬座", 118 "seat_bookable": 0, 119 "seat_yupiao": 0 120 }, { 121 "seat_price": "70.5", 122 "seat_name": "硬卧上", 123 "seat_bookable": 1, 124 "seat_yupiao": 9 125 }, { 126 "seat_price": "73.5", 127 "seat_name": "硬卧中", 128 "seat_bookable": 1, 129 "seat_yupiao": 9 130 }, { 131 "seat_price": "80.0", 132 "seat_name": "硬卧下", 133 "seat_bookable": 1, 134 "seat_yupiao": 9 135 }, { 136 "seat_price": "108.5", 137 "seat_name": "软卧上", 138 "seat_bookable": 0, 139 "seat_yupiao": 0 140 }, { 141 "seat_price": "122.5", 142 "seat_name": "软卧下", 143 "seat_bookable": 0, 144 "seat_yupiao": 0 145 }, {"seat_price": "24.5", "seat_name": "无座", "seat_bookable": 1, "seat_yupiao": 240}] 146 }, { 147 "train_no": "48000K837105", 148 "train_number": "K8371", 149 "from_station": "上海南", 150 "to_station": "杭州", 151 "from_time": "05:43", 152 "to_time": "08:14", 153 "from_station_type": "途经", 154 "to_station_type": "途经", 155 "day_diff": "0", 156 "use_time": "151", 157 "sale_time": "15:30", 158 "control_day": 59, 159 "from_telecode": "SNH", 160 "to_telecode": "HZH", 161 "can_web_buy": "Y", 162 "note": "", 163 "seats": [{ 164 "seat_price": "28.5", 165 "seat_name": "硬座", 166 "seat_bookable": 1, 167 "seat_yupiao": 6 168 }, { 169 "seat_price": "74.5", 170 "seat_name": "硬卧上", 171 "seat_bookable": 1, 172 "seat_yupiao": 21 173 }, { 174 "seat_price": "77.5", 175 "seat_name": "硬卧中", 176 "seat_bookable": 1, 177 "seat_yupiao": 21 178 }, { 179 "seat_price": "84.5", 180 "seat_name": "硬卧下", 181 "seat_bookable": 1, 182 "seat_yupiao": 21 183 }, { 184 "seat_price": "112.5", 185 "seat_name": "软卧上", 186 "seat_bookable": 0, 187 "seat_yupiao": 0 188 }, { 189 "seat_price": "127.0", 190 "seat_name": "软卧下", 191 "seat_bookable": 0, 192 "seat_yupiao": 0 193 }, {"seat_price": "28.5", "seat_name": "无座", "seat_bookable": 0, "seat_yupiao": 0}] 194 }, { 195 "train_no": "5l000G754130", 196 "train_number": "G7541", 197 "from_station": "上海虹桥", 198 "to_station": "杭州东", 199 "from_time": "06:14", 200 "to_time": "07:06", 201 "from_station_type": "起点", 202 "to_station_type": "途经", 203 "day_diff": "0", 204 "use_time": "52", 205 "sale_time": "13:30", 206 "control_day": 59, 207 "from_telecode": "AOH", 208 "to_telecode": "HGH", 209 "can_web_buy": "Y", 210 "note": "", 211 "seats": [{ 212 "seat_price": "73.0", 213 "seat_name": "二等座", 214 "seat_bookable": 1, 215 "seat_yupiao": 375 216 }, { 217 "seat_price": "117.0", 218 "seat_name": "一等座", 219 "seat_bookable": 1, 220 "seat_yupiao": 24 221 }, { 222 "seat_price": "219.5", 223 "seat_name": "商务座", 224 "seat_bookable": 1, 225 "seat_yupiao": 10 226 }, {"seat_price": "73.0", "seat_name": "无座", "seat_bookable": 0, "seat_yupiao": 0}] 227 }, { 228 "train_no": "5l000G7331B0", 229 "train_number": "G7331", 230 "from_station": "上海虹桥", 231 "to_station": "杭州东", 232 "from_time": "06:20", 233 "to_time": "07:12", 234 "from_station_type": "起点", 235 "to_station_type": "途经", 236 "day_diff": "0", 237 "use_time": "52", 238 "sale_time": "13:30", 239 "control_day": 59, 240 "from_telecode": "AOH", 241 "to_telecode": "HGH", 242 "can_web_buy": "Y", 243 "note": "", 244 "seats": [{ 245 "seat_price": "73.0", 246 "seat_name": "二等座", 247 "seat_bookable": 1, 248 "seat_yupiao": 339 249 }, { 250 "seat_price": "117.0", 251 "seat_name": "一等座", 252 "seat_bookable": 1, 253 "seat_yupiao": 26 254 }, { 255 "seat_price": "219.5", 256 "seat_name": "商务座", 257 "seat_bookable": 1, 258 "seat_yupiao": 10 259 }, {"seat_price": "73.0", "seat_name": "无座", "seat_bookable": 0, "seat_yupiao": 0}] 260 }, { 261 "train_no": "470000T13501", 262 "train_number": "T135", 263 "from_station": "上海南", 264 "to_station": "杭州东", 265 "from_time": "06:21", 266 "to_time": "08:18", 267 "from_station_type": "途经", 268 "to_station_type": "途经", 269 "day_diff": "0", 270 "use_time": "117", 271 "sale_time": "15:30", 272 "control_day": 59, 273 "from_telecode": "SNH", 274 "to_telecode": "HGH", 275 "can_web_buy": "Y", 276 "note": "", 277 "seats": [{ 278 "seat_price": "24.5", 279 "seat_name": "硬座", 280 "seat_bookable": 1, 281 "seat_yupiao": 4 282 }, { 283 "seat_price": "70.5", 284 "seat_name": "硬卧上", 285 "seat_bookable": 1, 286 "seat_yupiao": 38 287 }, { 288 "seat_price": "73.5", 289 "seat_name": "硬卧中", 290 "seat_bookable": 1, 291 "seat_yupiao": 38 292 }, { 293 "seat_price": "80.0", 294 "seat_name": "硬卧下", 295 "seat_bookable": 1, 296 "seat_yupiao": 38 297 }, { 298 "seat_price": "108.5", 299 "seat_name": "软卧上", 300 "seat_bookable": 0, 301 "seat_yupiao": 0 302 }, { 303 "seat_price": "122.5", 304 "seat_name": "软卧下", 305 "seat_bookable": 0, 306 "seat_yupiao": 0 307 }, {"seat_price": "24.5", "seat_name": "无座", "seat_bookable": 0, "seat_yupiao": 0}] 308 }, { 309 "train_no": "5l000G132120", 310 "train_number": "G1321", 311 "from_station": "上海虹桥", 312 "to_station": "杭州东", 313 "from_time": "06:25", 314 "to_time": "07:17", 315 "from_station_type": "起点", 316 "to_station_type": "途经", 317 "day_diff": "0", 318 "use_time": "52", 319 "sale_time": "13:30", 320 "control_day": 57, 321 "from_telecode": "AOH", 322 "to_telecode": "HGH", 323 "can_web_buy": "Y", 324 "note": "", 325 "seats": [{ 326 "seat_price": "73.0", 327 "seat_name": "二等座", 328 "seat_bookable": 1, 329 "seat_yupiao": 304 330 }, { 331 "seat_price": "117.0", 332 "seat_name": "一等座", 333 "seat_bookable": 1, 334 "seat_yupiao": 15 335 }, {"seat_price": "219.5", "seat_name": "商务座", "seat_bookable": 1, "seat_yupiao": 9}] 336 }, { 337 "train_no": "5l000G733380", 338 "train_number": "G7333", 339 "from_station": "上海虹桥", 340 "to_station": "杭州东", 341 "from_time": "06:30", 342 "to_time": "07:22", 343 "from_station_type": "起点", 344 "to_station_type": "途经", 345 "day_diff": "0", 346 "use_time": "52", 347 "sale_time": "13:30", 348 "control_day": 59, 349 "from_telecode": "AOH", 350 "to_telecode": "HGH", 351 "can_web_buy": "Y", 352 "note": "", 353 "seats": [{ 354 "seat_price": "73.0", 355 "seat_name": "二等座", 356 "seat_bookable": 1, 357 "seat_yupiao": 702 358 }, { 359 "seat_price": "117.0", 360 "seat_name": "一等座", 361 "seat_bookable": 1, 362 "seat_yupiao": 51 363 }, { 364 "seat_price": "219.5", 365 "seat_name": "商务座", 366 "seat_bookable": 1, 367 "seat_yupiao": 20 368 }, {"seat_price": "73.0", "seat_name": "无座", "seat_bookable": 0, "seat_yupiao": 0}] 369 }, { 370 "train_no": "0400000K5004", 371 "train_number": "K47", 372 "from_station": "上海南", 373 "to_station": "杭州", 374 "from_time": "06:34", 375 "to_time": "09:15", 376 "from_station_type": "途经", 377 "to_station_type": "终点", 378 "day_diff": "0", 379 "use_time": "161", 380 "sale_time": "15:30", 381 "control_day": 59, 382 "from_telecode": "SNH", 383 "to_telecode": "HZH", 384 "can_web_buy": "Y", 385 "note": "", 386 "seats": [{ 387 "seat_price": "28.5", 388 "seat_name": "硬座", 389 "seat_bookable": 1, 390 "seat_yupiao": 6 391 }, { 392 "seat_price": "74.5", 393 "seat_name": "硬卧上", 394 "seat_bookable": 1, 395 "seat_yupiao": 122 396 }, { 397 "seat_price": "77.5", 398 "seat_name": "硬卧中", 399 "seat_bookable": 1, 400 "seat_yupiao": 122 401 }, { 402 "seat_price": "84.5", 403 "seat_name": "硬卧下", 404 "seat_bookable": 1, 405 "seat_yupiao": 122 406 }, { 407 "seat_price": "112.5", 408 "seat_name": "软卧上", 409 "seat_bookable": 1, 410 "seat_yupiao": 1 411 }, { 412 "seat_price": "127.0", 413 "seat_name": "软卧下", 414 "seat_bookable": 1, 415 "seat_yupiao": 1 416 }, {"seat_price": "28.5", "seat_name": "无座", "seat_bookable": 0, "seat_yupiao": 0}] 417 }, { 418 "train_no": "5l000G7301B1", 419 "train_number": "G7301", 420 "from_station": "上海虹桥", 421 "to_station": "杭州", 422 "from_time": "06:35", 423 "to_time": "07:40", 424 "from_station_type": "起点", 425 "to_station_type": "终点", 426 "day_diff": "0", 427 "use_time": "65", 428 "sale_time": "13:30", 429 "control_day": 59, 430 "from_telecode": "AOH", 431 "to_telecode": "HZH", 432 "can_web_buy": "Y", 433 "note": "", 434 "seats": [{ 435 "seat_price": "77.5", 436 "seat_name": "二等座", 437 "seat_bookable": 1, 438 "seat_yupiao": 490 439 }, { 440 "seat_price": "123.5", 441 "seat_name": "一等座", 442 "seat_bookable": 1, 443 "seat_yupiao": 26 444 }, { 445 "seat_price": "233.5", 446 "seat_name": "商务座", 447 "seat_bookable": 1, 448 "seat_yupiao": 10 449 }, {"seat_price": "77.5", "seat_name": "无座", "seat_bookable": 0, "seat_yupiao": 0}] 450 }, { 451 "train_no": "5l000D314510", 452 "train_number": "D3145", 453 "from_station": "上海虹桥", 454 "to_station": "杭州东", 455 "from_time": "06:40", 456 "to_time": "07:41", 457 "from_station_type": "起点", 458 "to_station_type": "途经", 459 "day_diff": "0", 460 "use_time": "61", 461 "sale_time": "13:30", 462 "control_day": 59, 463 "from_telecode": "AOH", 464 "to_telecode": "HGH", 465 "can_web_buy": "Y", 466 "note": "", 467 "seats": [{ 468 "seat_price": "49.0", 469 "seat_name": "二等座", 470 "seat_bookable": 1, 471 "seat_yupiao": 21 472 }, { 473 "seat_price": "59.0", 474 "seat_name": "一等座", 475 "seat_bookable": 1, 476 "seat_yupiao": 1 477 }, {"seat_price": "49.0", "seat_name": "无座", "seat_bookable": 0, "seat_yupiao": 0}] 478 }, { 479 "train_no": "5l000G138330", 480 "train_number": "G1383", 481 "from_station": "上海虹桥", 482 "to_station": "杭州东", 483 "from_time": "06:45", 484 "to_time": "07:46", 485 "from_station_type": "起点", 486 "to_station_type": "途经", 487 "day_diff": "0", 488 "use_time": "61", 489 "sale_time": "13:30", 490 "control_day": 59, 491 "from_telecode": "AOH", 492 "to_telecode": "HGH", 493 "can_web_buy": "Y", 4