说说模块组织—kissy1.2最佳实践探索
在kissy1.2的工程内,一切皆是模块,无论js还是css,都可以采用kissy1,2的loader异步加载进来,当然本文只讨论js的情况。
模块遵循的关键原则
1.模块书写必须符合基本书写格式
kissy1.2的js模块文件,基础模板如下:
1
2
3
4
5
6
|
KISSY.add( function (S) { function model(){ } return model; }, {requires:[]}); |
- 文件的js代码起始必须是KISSY.add()
- 定义的模块必须有return值
- 使用requires数组处理模块依赖
2.模块功能单一性原则
在kissy1.2的模块系统中,我们希望一个模块只做一件事情,就像乐高积木一样,形态虽然是单一的,但可以快速组合成一个有趣的玩具。模块功能越多,逻辑越复杂,而越限制了组合的自由度。
关于功能单一性原则,后面明河讲解业务模块的时候会详细讲到。
3.一致性原则
每个公司、每个团队在代码规范上肯定会有差异,但明河建议在一个应用系统内代码风格和模块组织方式应该保持一致,就像一个工厂生产出来一样。
当有第三方维护者参与系统的维护时,应该review下他的代码是否风格一致。明河见过淘宝的老系统维护的人很多,造成一个js文件,糅杂着各种风格的代码。
最典型的情况是DOM模块(类YUI取DOM)和Node模块(类jquery取DOM)的混用。
1
2
3
4
|
//Node方式获取元素 var itemEl = item.itemEl; //DOM方式设置元素样式 DOM.css(favLay, 'height' , itemEl.height()); |
(上面代码截自淘宝购物车)
这种混合使用方式容易给第三方维护者造成不必要的障碍,维护者需要思考用DOM还是用Node。
所以明河建议一个系统内只用一种方式处理DOM操作,相对而言明河更喜欢Node方式(被jquery毒害的…)。
4.解耦和适当耦合原则
理想的模块,应该是低耦合、内聚、不存在强依赖。
而在一个模块系统中基本上不存在没有耦合的情况,模块的互相调用,公用模块的数据依赖等。
明河又要提到乐高玩具,每个积木都有接头,通过看似简单的拼接,组成富有变化的玩具,就像我们的模块暴露出来的接口,接口的调用,模块的通信,组成了我们应用的模块系统。
关于如何解耦,内容太多,明河有机会再详细整理想法出来。
需要特别留意的是别为了解耦,而过度设计,如果为了解耦而使用复杂的继承、设计复杂的接口,只会本末倒置,我们还是应该以模块代码的简单清晰为首要诉求。
过度设计的典型案例:v1.0的KF/Uploader(异步文件上传组件),设计非常复杂,有兴趣的同学可以看下我之前写的PPT。
点此下载如何解耦,是门学问,没有标准答案,需要开发者在实践和重构中斟酌。
5.合理按需加载原则
kissy1.2后按需加载这个词明河听到很多,但在实践过程中,明河会思考这样一个问题:真的需要按需加载吗?
按需加载在缩短页面等待时间上有所帮助,但帮助并没有那么大。这里有应用场景的问题,有些系统特别适合按需加载,比如以前的我的淘宝或者QQ空间这种用户可定制功能模块的页面,但更多的系统其实按需加载在性能优化上收效甚微,比如淘宝退款这种纯表单和展示的页面。
(ps:按需加载适用于附加功能模块或后置操作功能模块的加载场景,比如用户点击按钮后加载overlay,然后实例化出现弹出层。)
所以使用按需加载即动态use()模块,应该先思考适用场景。
过多的动态use()会额外增加请求数。
kissy1.2模块类型
根据模块功能的差异,我们一般将kissy1.2的模块划分为:
1.入口模块职能单一性原则
所谓入口模块,指的是页面use()
的模块,用于处理页面逻辑模块的依赖管理的模块和初始化模块脚本,比如在demo页面中:
1
2
3
|
KISSY.use( 'core/init' , function (S,Apply){ }) |
core/init.js
即页面的入口模块:
1
2
3
4
5
6
7
|
KISSY.add( function (S, Header) { return function (){ Header(); } }, {requires:[ '../mods/header' ]}); |
假设有第三方维护者,首先找的会是入口模块,然后通过入口模块去了解依赖的业务模块,入口模块作为起点,应该尽量纯粹简单,有利于维护者快速定位。
建议:入口模块只用于入口目的,不包含任何业务逻辑。
糟糕设计案例:refund系统的退款申请页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
KISSY.add( function (S, DOM, Event, Header, RefundMoney) { S.mix(re, { _init : function () { new Header(); new RefundMoney(); }, _showReasonSelect : function () { }, _submitHanlder : function (ev) { }, _changeMaxMoney : function () { } }); return re; }, {requires:[ 'dom' , 'event' , 'rf/mods/header/header' , 'rf/mods/refundMoney/ui' ]}); |
入口模板中包含了大量的页面逻辑,整个入口模块看过去臃肿,不好理解,没有起到很好的门面作用。
ps:明河也不推荐入口模块只用于处理requires模块依赖,比如下面的入口模块文件:
1
2
3
|
KISSY.add( function (S,Header, RefundMoney) { }, {requires:[ 'rf/mods/header/header' , 'rf/mods/refundMoney/ui' , 'rf/mods/refundInfo/ui' ]}); |
这种方式,当加载模块后立即执行模块逻辑,维护者没办法控制模块逻辑执行时机,这会给日后的维护造成一定的困难。
2.通用模块
多个页面公用的模块,都可以理解为通用模块,包含组件模块等。
如果是组件模块,明河推荐参考kissy组件的写法。
- 模块返回类,使用new 方式调用;
- 类继承kissy的Base类,使用getter/setter方式控制属性;
- 暴露出自定义事件供外部监听;
- 可配置性,思考扩展性;
- 更为仔细的注释。
典型的组件(通用)模块类代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
/** * @fileoverview 异步文件上传组件 * @author 剑平(明河)<minghe36@126.com> **/ KISSY.add( function (S, Base, Node) { /** * @name Uploader * @class 异步文件上传组件,支持ajax、flash、iframe三种方案 * @constructor * @extends Base * @param {Object} config 组件配置(下面的参数为配置项,配置会写入属性,详细的配置说明请看属性部分) */ function Uploader(config) { var self = this ; //调用父类构造函数 Uploader.superclass.constructor.call(self, config); } S.extend(Uploader, Base, /** @lends Uploader.prototype*/ { /** * 运行组件,实例化类后必须调用render()才真正运行组件逻辑 * @return {Uploader} */ render: function (){ } },{ATTRS: /** @lends Uploader.prototype*/ { /** * Button按钮的实例 * @type Button * @default {} */ button:{value:{} } }) return Uploader; }, {requires:[ 'base' , 'node' ]}); </minghe36@126.com> |
3.业务模块
从改动的频率和大小来说,业务模块大于通用模块和入口模块,所以业务模块的划分和编写,必须从可维护性的角度出发。下面明河整理出一些建议供大家参考,欢迎反馈不同看法。
1) 划分业务模块时功能单一性原则
功能单一性原则要求一个业务模块只干一件事情,明河曾人review代码时,经常发现一个业务模块会包含大量的功能逻辑,整个模块看上去臃肿复杂,已经违背了模块化思想的本意,更像是传统js代码的异步加载版。
remark-seller.js基本包含了评价页整张页面的大部分逻辑,包括晒照片,评价分值处理,字数统计、表单处理等等,代码也高达737行,过于臃肿,可维护性差。
很多同学之所以在一个模块内混合大量的逻辑,主要是在模块通信上有所顾虑(也有偷懒的嫌疑),业务模块多后,就要考虑模块之间如何调用通信的问题,处理不好,模块之间的耦合加深,也会对可维护性造成影响。
(PS:关于模块通信问题,后面明河会讲到)
2) 模块命名建议
先来看个业务模块众多的系统:淘宝下单页(坑爹般的复杂)
上图只是一小部分模块,下单的业务模块命名是文件夹名作为模块名,将模块分成ui层和逻辑层,可以理解为简单版的mvc,想法本身没什么问题,但大量的ui.js(大部分模块都有这个ui.js)在编辑代码和调试时造成了一定的麻烦。
比如编辑代码时,文件选项卡上都是ui.js,有时你都分不清楚是哪个模块的ui.js
上图截自明河的idea编辑器,第一眼根本找不到到底是哪个模块的ui.js,非常蛋疼的设计。
大量重复命名的模块文件,也给调试带来困难,老版本的chrome调试脚本时只显示文件名的,面对一坨ui.js,彻底囧了,所以都用firefox调试….
(PS:设计者的本意是好的,希望借助分层思想,来剥离复杂的下单逻辑,达到简化和平台化的目的,但现实是残酷的,看上去很酷…)
明河还见到有的应用的业务模块群出现大量index.js或base.js的,都面临同样的问题。
明河推荐YUI3的模块命名方式:
不要吝啬给模块js加前缀,我们追求的应该是清晰明确的模块文件名。
3) 模块返回值尽量一致
明河的建议是所有的模块js都返回一个类,外部使用模块都采用new
实例的方式,比如下面的代码:
1
2
3
4
5
6
|
KISSY.add( function (S,Header, RefundMoney) { function Apply(){ } return Apply; }, {requires:[ 'rf/mods/header/header' , 'rf/mods/refundMoney/ui' ]}); |
再举个淘宝下单页的风格:
1
2
3
4
5
6
7
|
KISSY.add( function (S,Header, RefundMoney) { return { _init: function (){ } }; }, {requires:[ 'rf/mods/header/header' , 'rf/mods/refundMoney/ui' ]}); |
当然每个人都有不一样的习惯,明河的建议是务必保持业务模块的组织风格和返回值一致,这样更有利于第三方理解代码。