前端已经过了单兵作战的时代了,现在一个稍微复杂一点的项目都需要几个人协同开发,一个战略级别的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 Code

有了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 Code

有了根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 Code

我们这里抽取一段火车票列表的筛选功能做实现,这个页面有一定复杂度又不是太难,大概页面是这个样子的:

谈谈我对前端组件化中“组件”的理解,顺带写个Vue与React的demo
    




【一次面试】再谈javascript中的继承

因为,我们这里的关注点在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

相关文章:

  • 2021-10-09
  • 2022-12-23
  • 2021-12-21
  • 2021-06-30
  • 2021-06-28
  • 2021-12-07
  • 2022-03-09
猜你喜欢
  • 2022-12-23
  • 2021-10-29
  • 2022-01-17
  • 2021-08-04
  • 2021-08-11
  • 2021-07-18
  • 2021-05-29
相关资源
相似解决方案