前言
前端已经过了单兵作战的时代了,现在一个稍微复杂一点的项目都需要几个人协同开发,一个战略级别的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 >' + this.style + '</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; " >' + html + '</div>'); 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原文链接:https://www.cnblogs.com/alinaxia/p/6359070.html
本文来源 爱码网,其版权均为 原网址 所有 与本站无关,文章内容系作者个人观点,不代表 本站 对观点赞同或支持。如需转载,请注明文章来源。