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

前言

前端已经过了单兵作战的时代了,现在一个稍微复杂一点的项目都需要几个人协同开发,一个战略级别的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
本文来源 爱码网,其版权均为 原网址 所有 与本站无关,文章内容系作者个人观点,不代表 本站 对观点赞同或支持。如需转载,请注明文章来源。

© 版权声明

相关文章