从DOM操作看Vue&React的前端组件化,顺带补齐React的demo
前言
接上文:谈谈我对前端组件化中“组件”的理解,顺带写个Vue与React的demo
上次写完博客后,有朋友反应第一内容有点深,看着迷迷糊糊;第二是感觉没什么使用场景,太过业务化,还不如直接写Vue&react的源码分析,我感觉这里有必要说下我的认识。
首先,要写源码分析很难,第一是他本来就很难,所以一般我们是想了解他实现的思路而不是代码;
第二每个开发者有自己发风格,所以你要彻底读懂一个人的代码不容易,除非你是带着当时作者同样的问题不断的寻找解决方案,不断的重构,才可能理解用户的意图。
我们上一次做的事情其实就是根据自己实际的工作经验做了和外面框架类似的事情,虽然代码的健壮、优雅程度跟不上,但却和其它作者一样为解决同样的问题思考得出的方案,上次做的太晚了,后面就草草结束,事实上在我Demo过程中发现一个事实:业务代码都是差不多的,只是一些细节点不一样,所以决定产品质量的依旧是开发者的业务代码能力,框架只是助力而已。
不能了解作者的意图,不能提高本身的编程水平,就算用上了React&Vue这类组件化的框架,也组织不好代码;事实上这类代码因为是面向大型应用的,反而更考验一个人的架构能力,所以大家要多注重内在修养的提升哦。
下面我们进入今天的正题,这里依旧提供一些帮助理解的资料:
github
代码地址:https://github.com/yexiaochai/module/
演示地址:http://yexiaochai.github.io/module/me/index.html
如果对文中的一些代码比较疑惑,可以对比着看看这些文章:
【移动前端开发实践】从无到有(统计、请求、MVC、模块化)H5开发须知
预览
使用Vue的思考
因为第一个demo是Vue的,React应该也类似,对比之前的代码发现一个重要差异是:
DOM操作真的完全没有了!!!
对,是完全没有DOM操作了,这个是很牛逼的一件事情,因为我觉得有两个地方要摆脱DOM操作很难:
① 我的组件应该放到哪个容器内,我需要一个定位的元素,比如:
1 this.sortModule = new SortModule({ 2 view: this, 3 selector: '.js_sort_wrapper', 4 sortEntity: this.sortEntity 5 });
明确的告诉了组件所属的容器
② 我比较疑惑像这类列表类型的事件该如何处理,因为一些必要参数是根据event获取的,比如:
1 listItemClick: function (e) { 2 var el = $(e.currentTarget); 3 //根据el做一些事情 4 }
关于这个Vue的作者认为应该将事件处理程序内联,做显示声明:
你可能注意到这种事件监听的方式违背了传统理念 “separation of concern”。不必担心,
因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护困难。实际上,使用 v-on 有几个好处:
扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法。
因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试。
当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何自己清理它们。
<button v-on:click="say('hello!', $event)">Submit</button>
1 methods: { 2 say: function (msg, event) { 3 // 现在我们可以访问原生事件对象 4 event.preventDefault() 5 } 6 }
还有种常用的操作,比如radioList,点击当前选项便选择项目,我们一般的做法是这样的:
1 setIndex: function (i) { 2 this.index = i; 3 this.$('li').removeClass(this.curClass); 4 this.$('li[data-index="' + i + '"]').addClass(this.curClass); 5 }
这样做比较简单,但是会有一个问题,便是数据与dom表现的流程变了,正确的流程是index 变了,dom便根据数据做更新,比如Vue:
1 setIndex: function (i) { 2 this.index = i; 3 //这部分逻辑Vue会自动实现 4 //this.$('li').removeClass(this.curClass); 5 //this.$('li[data-index="' + i + '"]').addClass(this.curClass); 6 }
之前,不考虑性能,我们会直接根据数据重新渲染整个列表,就为一个简单的选中功能,而Vue&React却做到了局部渲染,这个是否牛逼,我相信这个将会是一个核心算法部分,后面有时间一定要深入了解。
根据以上局部解读,我们得到一个结论,只要达成两个条件,就能摆脱DOM操作:
① 知道组件所处容器
② 根据数据渲染页面
PS:我们这里是很简单的一环,没有考虑组件嵌套,组件通信等过于复杂的问题
那么如果达成了以上条件,我们能否做到业务逻辑中不包含dom操作呢?我们下面就来试试。
如何摆脱DOM操作
这里真的是demo类尝试,思维验证,便不使用之前过于复杂的业务逻辑了,这里将me目录拷贝一块出来,依旧以原来的代码做底层依赖,只要列表与顶部排序部分功能,这里为了简化实现,保持代码重用,我们这里直接想将entity模块复用,要求data中的对象必须是一个entity实例,这里第一步是抽象出来了list module模块,于是主控制器变成这样了,事实上这个时候已经没dom操作了:
1 initEntity: function () { 2 //实例化排序的导航栏的实体 3 this.sortEntity = new SortEntity(); 4 this.sortEntity.subscribe(this.renderList, this); 5 }, 6 7 initModule: function () { 8 //view为注入给组件的根元素 9 //selector为组件将要显示的容器 10 //sortEntity为注入给组件的数据实体,做通信用 11 //这个module在数据显示后会自动展示 12 this.sortModule = new SortModule({ 13 view: this, 14 selector: '.js_sort_wrapper', 15 sortEntity: this.sortEntity 16 }); 17 this.listModule = new ListModule({ 18 view: this, 19 selector: '.js_list_wrapper', 20 entity: this.sortEntity 21 }); 22 }, 23 24 propertys: function ($super) { 25 $super(); 26 27 this.initEntity(); 28 this.initModule(); 29 this.viewId = 'list'; 30 this.template = layoutHtml; 31 this.events = {}; 32 }
这里简单看看列表组件的实现,其实就是将原来根View的代码换个位置:
1 define([ 2 'ModuleView', 3 'pages/list.data', 4 'text!pages/tpl.list.html' 5 6 ], function (ModuleView, 7 listData, 8 tpl) { 9 return _.inherit(ModuleView, { 10 11 //此处若是要使用model,处实例化时候一定要保证entity的存在,如果不存在便是业务BUG 12 initData: function () { 13 14 this.template = tpl; 15 this.entity.subscribe(this.render, this); 16 17 }, 18 19 _timeSort: function (data, sort) { 20 data = _.sortBy(data, function (item) { 21 item = item.from_time.split(':'); 22 item = item[0] + '.' + item[1]; 23 item = parseFloat(item); 24 return item; 25 }); 26 if (sort == 'down') data.reverse(); 27 return data; 28 }, 29 30 _sumTimeSort: function (data, sort) { 31 data = _.sortBy(data, function (item) { 32 return parseInt(item.use_time); 33 }); 34 if (sort == 'down') data.reverse(); 35 return data; 36 }, 37 38 _priceSort: function (data, sort) { 39 data = _.sortBy(data, function (item) { 40 return item.min_price; 41 }); 42 if (sort == 'down') data.reverse(); 43 return data; 44 }, 45 46 //获取导航栏排序后的数据 47 getSortData: function (data) { 48 var tmp = []; 49 var sort = this.entity.get(); 50 51 for (var k in sort) { 52 if (sort[k].length > 0) { 53 tmp = this['_' + k + 'Sort'](data, sort[k]) 54 return tmp; 55 } 56 } 57 }, 58 59 //复杂的业务数据处理,为了达到产品的需求,这段代码逻辑与业务相关 60 //这段数据处理的代码过长(超过50行就过长),应该重构掉 61 formatData: function (data) { 62 var item, seat; 63 var typeMap = { 64 'g': 'g', 65 'd': 'd', 66 't': 't', 67 'c': 'g' 68 }; 69 70 //出发时间对应的分钟数 71 var fromMinute = 0; 72 73 //获取当前班车日期当前的时间戳,这个数据是动态的,这里写死了 74 var d = 1464192000000; 75 var date = new Date(); 76 var now = parseInt(date.getTime() / 1000); 77 date.setTime(d); 78 var year = date.getFullYear(); 79 var month = date.getMonth(); 80 var day = date.getDate(); 81 var toBegin; 82 var seatName, seatIndex, iii; 83 84 //处理坐席问题,仅显示二等座,一等座,特等座 无座 85 // 二等座 一等座 商务座 无座 动卧 特等座 86 var my_seats = {}; 87 var seatSort = ['二等座', '一等座', '硬座', '硬卧', '软卧', '商务座', '无座', '动卧', '特等座', '软座']; 88 89 for (var i = 0, len = data.length; i < len; i++) { 90 fromMinute = data[i].from_time.split(':'); 91 fromMinute[0] = fromMinute[0] + ''; 92 fromMinute[1] = fromMinute[1] + ''; 93 if ((fromMinute[0].charAt(0) == '0')) fromMinute[0] = fromMinute[0].charAt(1); 94 if ((fromMinute[1].charAt(0) == '0')) fromMinute[1] = fromMinute[1].charAt(1); 95 date = new Date(year, month, day, fromMinute[0], fromMinute[1], 0); 96 fromMinute = parseInt(date.getTime() / 1000) 97 toBegin = parseInt((fromMinute - now) / 60); 98 99 data[i].toBegin = toBegin; 100 101 //处理车次类型问题 102 data[i].my_train_number = typeMap[data[i].train_number.charAt(0).toLowerCase()] || 'other'; 103 104 seat = data[i].seats; 105 //所有余票 106 data[i].sum_ticket = 0; 107 //最低价 108 data[i].min_price = null; 109 110 for (var j = 0, len1 = seat.length; j < len1; j++) { 111 if (!data[i].min_price || data[i].min_price > seat[j].seat_price) data[i].min_price = parseFloat(seat[j].seat_price); 112 data[i].sum_ticket += parseInt(seat[j].seat_yupiao); 113 114 //坐席问题如果坐席不包括上中下则去掉 115 seatName = seat[j].seat_name; 116 //去掉上中下 117 seatName = seatName.replace(/上|中|下/g, ''); 118 if (!my_seats[seatName]) { 119 my_seats[seatName] = parseInt(seat[j].seat_yupiao); 120 } else { 121 my_seats[seatName] = my_seats[seatName] + parseInt(seat[j].seat_yupiao); 122 } 123 } 124 //这里myseat为对象,需要转换为数组 125 //将定制坐席转为排序后的数组 126 data[i].my_seats = []; 127 for (iii = 0; iii < seatSort.length; iii++) { 128 if (typeof my_seats[seatSort[iii]] == 'number') data[i].my_seats.push({ 129 name: seatSort[iii], 130 yupiao: my_seats[seatSort[iii]] 131 }); 132 } 133 134 my_seats = {}; 135 } 136 137 return data; 138 }, 139 140 //完成所有的筛选条件,逻辑比较重 141 getViewModel: function () { 142 var data = this.formatData(listData); 143 data = this.getSortData(data); 144 return {data: data}; 145 } 146 147 }); 148 149 });
就这种简单的改变,貌似便摆脱了DOM操作,页面所有的状态事实上是可以做到由数据控制的,但是这里没有形成“标签化”,似乎不太好,于是我们来试试是否能改造为标签化的代码。
我们这里的业务代码(module与entity)没有什么需要改动的,这里主要在底层做改造,这里在我看来是提供了一种“语法糖”的东西,这里的具体概念后续阅读Vue源码再深入了解,这里先照着做,这里看结果想实现,也是我们常用的一种设计方案,首先我们的index编程了这个样子:
1 <article class="cm-page page-list" id="main"> 2 <div class="js_sort_wrapper sort-bar-wrapper"> 3 <mySortBar :entity="sortEntity"></mySortBar> 4 </div> 5 <myList :entity="listEntity" :sort="sort"></myList> 6 </article>
1 (function () { 2 require.config({ 3 paths: { 4 'text': 'libs/require.text', 5 6 'AbstractView': 'js/view', 7 'AbstractEntity': 'js/entity', 8 'ModuleView': 'js/module' 9 } 10 }); 11 12 require(['pages/list.label'], function (List) { 13 var list = new List(); 14 list.show(); 15 }); 16 })();
PS:里面的js钩子基本无用了
这里标签化带来的好处是,根View中有一段实例代码可以不用与选择器映射了,比如这个:
1 this.sortModule = new SortModule({ 2 //view: this, 3 //selector: '.js_sort_wrapper', 4 //sortEntity: this.sortEntity 5 });
因为处于组件中,其中所处位置已经定了,view实例或者entity实例全部是跟View显示注入的,这里根View中参考Vue的使用,新增一个$components与$entities属性,然后增加一$watch对象。
大家写底层框架时,私有属性或者方法使用_method的方式,如果是要释放的可以是$method这种,一定要“特殊化”防止被实例或者继承覆盖
1 define([ 2 'AbstractView', 'pages/en.sort', 'pages/mod.sort', 'pages/mod.list' 3 ], function (AbstractView, SortEntity, SortModule, ListModule) { 4 return _.inherit(AbstractView, { 5 propertys: function ($super) { 6 $super(); 7 this.$entities = { 8 sortEntity: SortEntity 9 }; 10 this.$components = { 11 mySortBar: SortModule, 12 listModule: ListModule 13 }; 14 this.$watch = { 15 16 }; 17 this.viewId = 'list'; 18 this.template = layoutHtml; 19 this.events = {}; 20 } 21 }); 22 });
他这种做法,需要组件在显示后框架底层将刚刚的业务代码实现,使用组件生成的html代码将原来标签的占位符给替换掉。
这里在组件也需要明示根View需要注入什么给自己:
PS:事实上这个可以不写,写了对后续属性的计算有好处
//记录需要根View注入的属性 props:[sortEntity],
PS:底层什么时候执行替换这个是有一定时机的,我们这里暂时放到根View展示后,这里更好的实现,后续我们在Vue与React中去找寻
因为我们这里是demo类实现,为降低难度,我们为每一个组件动态增加一个div包裹层,于是,我们在跟View中,在View展示后,我们另外多加一段逻辑:
1 //实例化实体,后面要用 2 this._initEntity(); 3 //新增标签逻辑 4 this._initComponent();
然后将实体与组件的实例化放到框架底层,这里实体的实例化比较简单(如果有特殊数据需求再说,这里只考虑最简单情况):
1 _initEntity: function() { 2 var key, entities = this.$entities; 3 //这里没有做特殊化,需要注意 4 for(key in entities) { 5 this[key] = new entities[key](); 6 } 7 },
而实例化组件的工作复杂许多,因为他需要将页面中的自定义标签替换掉,还需要完成很多属性注入操作:
1 _initComponent: function() { 2 var key, components = this.$components; 3 for(key in components) { 4 //这里实例化的过程有点复杂,首先将页面的标签做一个替换 5 var s = '' 6 } 7 },
1 _initComponent: function() { 2 var key, components = this.$components; 3 var el, attributes, attr, param, clazz, i, len, tmp, id, name; 4 5 //这里实例化的过程有点复杂,首先将页面的标签做一个替换 6 for(key in components) { 7 param = {}; 8 clazz = components[key]; 9 //由原型链上获取根元素要注入给子组件的属性(这个实现好像不太好) 10 attributes = clazz.prototype.props; 11 12 //首先获取标签dom元素,因为html是不区分大小写的,这里将标签小写 13 el = this.$(key.toLowerCase()); 14 if(!el[0]) continue; 15 16 if(attributes) { 17 for (i = 0, len = attributes.length; i < len; i++) { 18 attr = attributes[i]; 19 name = el.attr(':' + attr); 20 param[attr] = this[name] || name; 21 } 22 } 23 24 //创建一个空div去替换原来的标签 25 id = _.uniqueId('componenent-id-'); 26 tmp = $('<div component-name="' + key + '" id="' + id + '"></div>'); 27 tmp.insertBefore(el); 28 el.remove(); 29 param.selector = '#' + id; 30 param.view = this; 31 this[key] = new components[key](param); 32 } 33 34 },
于是这个标签便能正常展示了:
1 <article class="cm-page page-list" id="main"> 2 <div class="js_sort_wrapper sort-bar-wrapper"> 3 <mySortBar :entity="sortEntity" :myname="111"></mySortBar> 4 </div> 5 <myList :entity="sortEntity" :sort="sort"></myList> 6 </article>
后面想要把这段代码去掉也十分轻易,我这里就不进行了:
1 'click .js_sort_item li ': function (e) { 2 var el = $(e.currentTarget); 3 var sort = el.attr('data-sort'); 4 this.entity['set' + sort](); 5 }
总结
这里首先根据上次Vue的demo产生了一些思考,并且以简单的demo验证了这些思考,楼主在使用过程中发现Vue很多好的点子,后续应该会深入研究,并且以实际项目入手,这里回到今天的正题,我们使用React实现上次遗留的demo。
React的实现
在我最初接触React的时候,React Native还没出现,所以很多人对React的关注不高,当时做移动端直接放弃了angular,以体量来说,React也不在我们的考虑范围内,谁知道野心勃勃的Facebook搞出了React Native,让React彻底的跟着火了一把,事实上只要有能力以JavaScript统一Native UI的公司,这个实现就算换个框架依旧会火,虽然都已经这么火了,但是React的文档却不怎样,我后续也有试水React Native的兴趣,届时再与各位分享。
PS:根据之前的反馈,这次demo稍微做简单点,也只包含顶部导航和列表即可:
1 <!doctype html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, minimal-ui"/> 6 <meta content="yes" name="apple-mobile-web-app-capable"/> 7 <meta content="black" name="apple-mobile-web-app-status-bar-style"/> 8 <meta name="format-detection" content="telephone=no"/> 9 <link href="./static/css/global.css" rel="stylesheet" type="text/css"/> 10 <link href="./pages/list.css" rel="stylesheet" type="text/css"/> 11 <title>组件化</title> 12 </head> 13 <body> 14 <div class="cm-header"> 15 <h1 class="cm-page-title js_title"> 组件化Demo </h1> 16 </div> 17 18 <article class="cm-page page-list" id="main"> 19 </article> 20 21 22 <script src="./libs/react-with-addons.js"></script> 23 <script src="./libs/JSXTransformer.js"></script> 24 <script type="text/javascript" src="./pages/list.data.js"></script> 25 <script type="text/javascript" src="./libs/underscore.js"></script> 26 <script type="text/jsx"> 27 28 29 30 var MySortBar = React.createClass({ 31 getInitialState: function() { 32 return { 33 time: 'up', 34 sumTime: '', 35 price: '' 36 }; 37 }, 38 39 resetData: function () { 40 this.setState({ 41 time: '', 42 sumTime: '', 43 price: '' 44 }); 45 }, 46 47 setTime: function () { 48 this._setData('time'); 49 }, 50 51 setSumTime: function () { 52 this._setData('sumTime'); 53 }, 54 55 setPrice: function () { 56 this._setData('price'); 57 }, 58 59 _setData: function (key) { 60 var param = {}; 61 62 //如果设置当前key存在,则反置,否则清空筛选,设置默认值 63 if (this.state[key] != '') { 64 if (this.state[key] == 'up') param[key] = 'down'; 65 else param[key] = 'up'; 66 } else { 67 this.resetData(); 68 param[key] = 'down'; 69 } 70 this.setState(param); 71 }, 72 73 _getClassName: function(icon) { 74 return 'icon-sort ' + icon; 75 }, 76 77 render: function () { 78 return ( 79 <ul className="bus-tabs sort-bar js_sort_item"> 80 <li className="tabs-item" onClick={this.setTime} >出发时间<i className={this._getClassName(this.state.time)}></i></li> 81 <li className="tabs-item" onClick={this.setSumTime} >耗时<i className={this._getClassName(this.state.sumTime)}></i></li> 82 <li className="tabs-item" onClick={this.setPrice} >价格<i className={this._getClassName(this.state.price)}></i></li> 83 </ul> 84 ); 85 } 86 87 }); 88 89 var Seat = React.createClass({ 90 render: function () { 91 var seat = this.props.seat; 92 93 return ( 94 <span >{seat.name}({seat.yupiao }) </span> 95 ); 96 } 97 }); 98 99 var Item = React.createClass({ 100 render: function () { 101 102 var item = this.props.item; 103 var mapping = { 104 'g': '高速', 105 't': '特快', 106 'd': '高速动车', 107 'c': '城际高铁', 108 'z': '直达' 109 }; 110 111 var seats = item.my_seats.map(function(item){ 112 return <Seat seat={item}/>; 113 }); 114 115 116 return ( 117 <li className="bus-list-item "> 118 <div className="bus-seat"> 119 <span className=" fl">{item.train_number } | {mapping[item.my_train_number] || '其它'} </span> 120 <span className=" fr">{parseInt(item.use_time / 60) + '小时' + item.use_time % 60 + '分'}</span> 121 </div> 122 <div className="detail"> 123 <div className="sub-list set-out"> 124 <span className="bus-go-off">{item.from_time}</span> <span className="start"><span className="icon-circle s-icon1"> 125 </span>{item.from_station }</span> <span className="fr price">¥{item.min_price}起</span> 126 </div> 127 <div className="sub-list"> 128 <span className="bus-arrival-time">{item.to_time}</span> <span className="end"><span className="icon-circle s-icon2"> 129 </span>{item.to_station}</span> <span className="fr ">{item.sum_ticket}张</span> 130 </div> 131 </div> 132 <div className="bus-seats-info" > 133 {seats} 134 </div> 135 </li> 136 ); 137 } 138 }); 139 140 141 var MyList = React.createClass({ 142 143 formatData: function (data) { 144 145 var item, seat; 146 var typeMap = { 147 'g': 'g', 148 'd': 'd', 149 't': 't', 150 'c': 'g' 151 }; 152 153 //出发时间对应的分钟数 154 var fromMinute = 0; 155 156 //获取当前班车日期当前的时间戳,这个数据是动态的,这里写死了 157 var d = 1464192000000; 158 var date = new Date(); 159 var now = parseInt(date.getTime() / 1000); 160 date.setTime(d); 161 var year = date.getFullYear(); 162 var month = date.getMonth(); 163 var day = date.getDate(); 164 var toBegin; 165 var seatName, seatIndex, iii; 166 167 //处理坐席问题,仅显示二等座,一等座,特等座 无座 168 // 二等座 一等座 商务座 无座 动卧 特等座 169 var my_seats = {}; 170 var seatSort = ['二等座', '一等座', '硬座', '硬卧', '软卧', '商务座', '无座', '动卧', '特等座', '软座']; 171 172 for (var i = 0, len = data.length; i < len; i++) { 173 fromMinute = data[i].from_time.split(':'); 174 fromMinute[0] = fromMinute[0] + ''; 175 fromMinute[1] = fromMinute[1] + ''; 176 if ((fromMinute[0].charAt(0) == '0')) fromMinute[0] = fromMinute[0].charAt(1); 177 if ((fromMinute[1].charAt(0) == '0')) fromMinute[1] = fromMinute[1].charAt(1); 178 date = new Date(year, month, day, fromMinute[0], fromMinute[1], 0); 179 fromMinute = parseInt(date.getTime() / 1000) 180 toBegin = parseInt((fromMinute - now) / 60); 181 182 data[i].toBegin = toBegin; 183 184 //处理车次类型问题 185 data[i].my_train_number = typeMap[data[i].train_number.charAt(0).toLowerCase()] || 'other'; 186 187 seat = data[i].seats; 188 //所有余票 189 data[i].sum_ticket = 0; 190 //最低价 191 data[i].min_price = null; 192 193 for (var j = 0, len1 = seat.length; j < len1; j++) { 194 if (!data[i].min_price || data[i].min_price > seat[j].seat_price) data[i].min_price = parseFloat(seat[j].seat_price); 195 data[i].sum_ticket += parseInt(seat[j].seat_yupiao); 196 197 //坐席问题如果坐席不包括上中下则去掉 198 seatName = seat[j].seat_name; 199 //去掉上中下 200 seatName = seatName.replace(/上|中|下/g, ''); 201 if (!my_seats[seatName]) { 202 my_seats[seatName] = parseInt(seat[j].seat_yupiao); 203 } else { 204 my_seats[seatName] = my_seats[seatName] + parseInt(seat[j].seat_yupiao); 205 } 206 } 207 //这里myseat为对象,需要转换为数组 208 //将定制坐席转为排序后的数组 209 data[i].my_seats = []; 210 for (iii = 0; iii < seatSort.length; iii++) { 211 if (typeof my_seats[seatSort[iii]] == 'number') data[i].my_seats.push({ 212 name: seatSort[iii], 213 yupiao: my_seats[seatSort[iii]] 214 }); 215 } 216 217 my_seats = {}; 218 } 219 220 return data; 221 }, 222 223 render: function () { 224 225 var main; 226 var data = this.formatData(this.props.data); 227 228 229 main = data.map(function(item) { 230 return <Item item={item}/>; 231 }); 232 233 return ( 234 <ul className="bus-list js_bus_list "> 235 {main} 236 </ul> 237 ); 238 } 239 240 }); 241 242 var data = getListData(); 243 244 React.render( 245 <div> 246 <div className="js_sort_wrapper sort-bar-wrapper"> 247 <MySortBar /> 248 </div> 249 <MyList data={data} /> 250 </div>, 251 document.getElementById('main') 252 ); 253 254 </script> 255 256 257 </body> 258 </html>
他这个语法据说是让开发变得更简单了,我反正是不喜欢,这里有个不好的地方,之前数据实体全部是在根View上实例化的,然后注入给子View,React这里属性完全独享了,现在我触发了状态的改变,如何通知到list组件重新渲染排序呢?
React 组件通信
这里React子组件之间如何通信暂没有研究出来,所以将需要通信的数据做到了父组件中
1 <!doctype html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, minimal-ui"/> 6 <meta content="yes" name="apple-mobile-web-app-capable"/> 7 <meta content="black" name="apple-mobile-web-app-status-bar-style"/> 8 <meta name="format-detection" content="telephone=no"/> 9 <link href="./static/css/global.css" rel="stylesheet" type="text/css"/> 10 <link href="./pages/list.css" rel="stylesheet" type="text/css"/> 11 <title>组件化</title> 12 </head> 13 <body> 14 <div class="cm-header"> 15 <h1 class="cm-page-title js_title"> 组件化Demo </h1> 16 </div> 17 18 <article class="cm-page page-list" id="main"> 19 </article> 20 21 22 <script src="./libs/react-with-addons.js"></script> 23 <script src="./libs/JSXTransformer.js"></script> 24 <script type="text/javascript" src="./pages/list.data.js"></script> 25 <script type="text/javascript" src="./libs/underscore.js"></script> 26 <script type="text/jsx"> 27 28 29 30 var MySortBar = React.createClass({ 31 _getClassName: function(icon) { 32 return 'icon-sort ' + icon; 33 }, 34 35 render: function () { 36 var state = this.props.state; 37 return ( 38 <ul className="bus-tabs sort-bar js_sort_item"> 39 <li className="tabs-item" onClick={this.props.setTime} >出发时间<i className={this._getClassName(state.time)}></i></li> 40 <li className="tabs-item" onClick={this.props.setSumTime} >耗时<i className={this._getClassName(state.sumTime)}></i></li> 41 <li className="tabs-item" onClick={this.props.setPrice} >价格<i className={this._getClassName(state.price)}></i></li> 42 </ul> 43 ); 44 } 45 46 }); 47 48 var Seat = React.createClass({ 49 render: function () { 50 var seat = this.props.seat; 51 52 return ( 53 <span >{seat.name}({seat.yupiao }) </span> 54 ); 55 } 56 }); 57 58 var Item = React.createClass({ 59 render: function () { 60 61 var item = this.props.item; 62 var mapping = { 63 'g': '高速', 64 't': '特快', 65 'd': '高速动车', 66 'c': '城际高铁', 67 'z': '直达' 68 }; 69 70 var seats = item.my_seats.map(function(item){ 71 return <Seat seat={item}/>; 72 }); 73 74 75 return ( 76 <li className="bus-list-item "> 77 <div className="bus-seat"> 78 <span className=" fl">{item.train_number } | {mapping[item.my_train_number] || '其它'} </span> 79 <span className=" fr">{parseInt(item.use_time / 60) + '小时' + item.use_time % 60 + '分'}</span> 80 </div> 81 <div className="detail"> 82 <div className="sub-list set-out"> 83 <span className="bus-go-off">{item.from_time}</span> <span className="start"><span className="icon-circle s-icon1"> 84 </span>{item.from_station }</span> <span className="fr price">¥{item.min_price}起</span> 85 </div> 86 <div className="sub-list"> 87 <span className="bus-arrival-time">{item.to_time}</span> <span className="end"><span className="icon-circle s-icon2"> 88 </span>{item.to_station}</span> <span className="fr ">{item.sum_ticket}张</span> 89 </div> 90 </div> 91 <div className="bus-seats-info" > 92 {seats} 93 </div> 94 </li> 95 ); 96 } 97 }); 98 99 var MyList = React.createClass({ 100 101 formatData: function (data) { 102 103 var item, seat; 104 var typeMap = { 105 'g': 'g', 106 'd': 'd', 107 't': 't', 108 'c': 'g' 109 }; 110 111 //出发时间对应的分钟数 112 var fromMinute = 0; 113 114 //获取当前班车日期当前的时间戳,这个数据是动态的,这里写死了 115 var d = 1464192000000; 116 var date = new Date(); 117 var now = parseInt(date.getTime() / 1000); 118 date.setTime(d); 119 var year = date.getFullYear(); 120 var month = date.getMonth(); 121 var day = date.getDate(); 122 var toBegin; 123 var seatName, seatIndex, iii; 124 125 //处理坐席问题,仅显示二等座,一等座,特等座 无座 126 // 二等座 一等座 商务座 无座 动卧 特等座 127 var my_seats = {}; 128 var seatSort = ['二等座', '一等座', '硬座', '硬卧', '软卧', '商务座', '无座', '动卧', '特等座', '软座']; 129 130 for (var i = 0, len = data.length; i < len; i++) { 131 fromMinute = data[i].from_time.split(':'); 132 fromMinute[0] = fromMinute[0] + ''; 133 fromMinute[1] = fromMinute[1] + ''; 134 if ((fromMinute[0].charAt(0) == '0')) fromMinute[0] = fromMinute[0].charAt(1); 135 if ((fromMinute[1].charAt(0) == '0')) fromMinute[1] = fromMinute[1].charAt(1); 136 date = new Date(year, month, day, fromMinute[0], fromMinute[1], 0); 137 fromMinute = parseInt(date.getTime() / 1000) 138 toBegin = parseInt((fromMinute - now) / 60); 139 140 data[i].toBegin = toBegin; 141 142 //处理车次类型问题 143 data[i].my_train_number = typeMap[data[i].train_number.charAt(0).toLowerCase()] || 'other'; 144 145 seat = data[i].seats; 146 //所有余票 147 data[i].sum_ticket = 0; 148 //最低价 149 data[i].min_price = null; 150 151 for (var j = 0, len1 = seat.length; j < len1; j++) { 152 if (!data[i].min_price || data[i].min_price > seat[j].seat_price) data[i].min_price = parseFloat(seat[j].seat_price); 153 data[i].sum_ticket += parseInt(seat[j].seat_yupiao); 154 155 //坐席问题如果坐席不包括上中下则去掉 156 seatName = seat[j].seat_name; 157 //去掉上中下 158 seatName = seatName.replace(/上|中|下/g, ''); 159 if (!my_seats[seatName]) { 160 my_seats[seatName] = parseInt(seat[j].seat_yupiao); 161 } else { 162 my_seats[seatName] = my_seats[seatName] + parseInt(seat[j].seat_yupiao); 163 } 164 } 165 //这里myseat为对象,需要转换为数组 166 //将定制坐席转为排序后的数组 167 data[i].my_seats = []; 168 for (iii = 0; iii < seatSort.length; iii++) { 169 if (typeof my_seats[seatSort[iii]] == 'number') data[i].my_seats.push({ 170 name: seatSort[iii], 171 yupiao: my_seats[seatSort[iii]] 172 }); 173 } 174 175 my_seats = {}; 176 } 177 178 return data; 179 }, 180 181 _timeSort: function (data, sort) { 182 data = _.sortBy(data, function (item) { 183 item = item.from_time.split(':'); 184 item = item[0] + '.' + item[1]; 185 item = parseFloat(item); 186 return item; 187 }); 188 if (sort == 'down') data.reverse(); 189 return data; 190 }, 191 192 _sumTimeSort: function (data, sort) { 193 data = _.sortBy(data, function (item) { 194 return parseInt(item.use_time); 195 }); 196 if (sort == 'down') data.reverse(); 197 return data; 198 }, 199 200 _priceSort: function (data, sort) { 201 data = _.sortBy(data, function (item) { 202 return item.min_price; 203 }); 204 if (sort == 'down') data.reverse(); 205 return data; 206 }, 207 208 //获取导航栏排序后的数据 209 getSortData: function (data) { 210 var tmp = []; 211 var sort = this.props.state; 212 213 for (var k in sort) { 214 if (sort[k].length > 0) { 215 tmp = this['_' + k + 'Sort'](data, sort[k]) 216 return tmp; 217 } 218 } 219 }, 220 221 render: function () { 222 223 var main; 224 var data = this.formatData(this.props.data); 225 data = this.getSortData(data); 226 227 main = data.map(function(item) { 228 return <Item item={item}/>; 229 }); 230 231 return ( 232 <ul className="bus-list js_bus_list "> 233 {main} 234 </ul> 235 ); 236 } 237 238 }); 239 240 var App = React.createClass({ 241 getInitialState: function() { 242 return { 243 time: 'up', 244 sumTime: '', 245 price: '' 246 }; 247 }, 248 249 resetData: function () { 250 this.setState({ 251 time: '', 252 sumTime: '', 253 price: '' 254 }); 255 }, 256 257 setTime: function () { 258 this._setData('time'); 259 }, 260 261 setSumTime: function () { 262 this._setData('sumTime'); 263 }, 264 265 setPrice: function () { 266 this._setData('price'); 267 }, 268 269 _setData: function (key) { 270 var param = {}; 271 272 //如果设置当前key存在,则反置,否则清空筛选,设置默认值 273 if (this.state[key] != '') { 274 if (this.state[key] == 'up') param[key] = 'down'; 275 else param[key] = 'up'; 276 } else { 277 this.resetData(); 278 param[key] = 'down'; 279 } 280 this.setState(param); 281 }, 282 283 formatData: function (data) { 284 285 var item, seat; 286 var typeMap = { 287 'g': 'g', 288 'd': 'd', 289 't': 't', 290 'c': 'g' 291 }; 292 293 //出发时间对应的分钟数 294 var fromMinute = 0; 295 296 //获取当前班车日期当前的时间戳,这个数据是动态的,这里写死了 297 var d = 1464192000000; 298 var date = new Date(); 299 var now = parseInt(date.getTime() / 1000); 300 date.setTime(d); 301 var year = date.getFullYear(); 302 var month = date.getMonth(); 303 var day = date.getDate(); 304 var toBegin; 305 var seatName, seatIndex, iii; 306 307 //处理坐席问题,仅显示二等座,一等座,特等座 无座 308 // 二等座 一等座 商务座 无座 动卧 特等座 309 var my_seats = {}; 310 var seatSort = ['二等座', '一等座', '硬座', '硬卧', '软卧', '商务座', '无座', '动卧', '特等座', '软座']; 311 312 for (var i = 0, len = data.length; i < len; i++) { 313 fromMinute = data[i].from_time.split(':'); 314 fromMinute[0] = fromMinute[0] + ''; 315 fromMinute[1] = fromMinute[1] + ''; 316 if ((fromMinute[0].charAt(0) == '0')) fromMinute[0] = fromMinute[0].charAt(1); 317 if ((fromMinute[1].charAt(0) == '0')) fromMinute[1] = fromMinute[1].charAt(1); 318 date = new Date(year, month, day, fromMinute[0], fromMinute[1], 0); 319 fromMinute = parseInt(date.getTime() / 1000) 320 toBegin = parseInt((fromMinute - now) / 60); 321 322 data[i].toBegin = toBegin; 323 324 //处理车次类型问题 325 data[i].my_train_number = typeMap[data[i].train_number.charAt(0).toLowerCase()] || 'other'; 326 327 seat = data[i].seats; 328 //所有余票 329 data[i].sum_ticket = 0; 330 //最低价 331 data[i].min_price = null; 332 333 for (var j = 0, len1 = seat.length; j < len1; j++) { 334 if (!data[i].min_price || data[i].min_price > seat[j].seat_price) data[i].min_price = parseFloat(seat[j].seat_price); 335 data[i].sum_ticket += parseInt(seat[j].seat_yupiao); 336 337 //坐席问题如果坐席不包括上中下则去掉 338 seatName = seat[j].seat_name; 339 //去掉上中下 340 seatName = seatName.replace(/上|中|下/g, ''); 341 if (!my_seats[seatName]) { 342 my_seats[seatName] = parseInt(seat[j].seat_yupiao); 343 } else { 344 my_seats[seatName] = my_seats[seatName] + parseInt(seat[j].seat_yupiao); 345 } 346 } 347 //这里myseat为对象,需要转换为数组 348 //将定制坐席转为排序后的数组 349 data[i].my_seats = []; 350 for (iii = 0; iii < seatSort.length; iii++) { 351 if (typeof my_seats[seatSort[iii]] == 'number') data[i].my_seats.push({ 352 name: seatSort[iii], 353 yupiao: my_seats[seatSort[iii]] 354 }); 355 } 356 357 my_seats = {}; 358 } 359 360 return data; 361 }, 362 363 render: function () { 364 365 var main; 366 var data = this.formatData(this.props.data); 367 main = data.map(function(item) { 368 return <Item item={item}/>; 369 }); 370 371 return ( 372 <div> 373 <div className="js_sort_wrapper sort-bar-wrapper"> 374 <MySortBar state={this.state} setTime={this.setTime} setSumTime={this.setSumTime} setPrice={this.setPrice}/> 375 </div> 376 <MyList data={data} state={this.state} /> 377 </div> 378 ); 379 } 380 381 }); 382 383 var data = getListData(); 384 385 React.render( 386 <App data={data}/>, 387 document.getElementById('main') 388 ); 389 390 </script> 391 392 393 </body> 394 </html>
总结
react的中文文档整理较差,很多资料找不到,jsx语法比较怪异,不是所有人能接受,我去找模板循环时候压根就没找到,所以jsx有个特点,他让你不得不去拆分你的组件,在我写React代码中,感觉React代码控制力度要重一点,但是如果没有良好的架构能力,我可以毫不夸张的说,你依旧写不好业务代码。
至于React与Vue的优劣,这个方面见仁见智吧,好了今天的文章到此为止。
后续我们可能会深入分析下Vue的实现,在React Native上做深入,有兴趣的同学可以持续关注。
文章有任何不足错误,请您提出,因为小钗也是第二次使用React写demo,如果使用不当请多包涵