前言
前端已经过了单兵作战的时代了,现在一个稍微复杂一点的项目都需要几个人协同开发,一个战略级别的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的代码后便需要组件级别的代码,正如之前所说,这里的组件只有根元素与子组件两层的层级:
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 });