读过,了解过很多的前端控件库/组件库,尝试过,体验过多个失败的,不算失败的组件库之后,总结下来,觉得要构建一个完整的组件库,需要考虑以下几个方面的问题:
1.基础库:
注意是库,不是框架,基础库通常提供底层方法,它必须能够屏蔽浏览器/终端的API差异。也许大家脑子里面会弹出一堆前端热门的一些库。在此不讨论哪个库哪个库怎么样,一个基础库必须提供的功能:
- 基本类型的常见扩展:原生的javascript对象API往往在现实中不够用,比如常见的Array.indexOf/remove/each,Date.parse/format,不管是怎么封装都需要这类方法
- DOM操作常见方法:DOM节点增删改查,CSS selector,DOMReady,contains,add/remove/toggleClass,屏蔽浏览器之间的操作差异。不多说,人人都熟。
- 一套浏览器检测机制:以前大家都倾向于做浏览器类型和版本的检测,现在倾向于做浏览器的特性检测,这样更有实际用处。
- Ajax的封装,对于组件库来讲可无。毕竟组件库本身的实现不太会用得上Ajax。
2.事件机制:
addEventListener/removeEventListener/dispatchEvent 是常见的封装方式,不应该只是DOM事件,而是任何对象都可以做一个事件机制。
对于DOM事件的封装需要屏蔽IE/标准Event的差异提供为用户使用,事件代理非常重要,不可小视。
- 事件机制都无一例外的是基于第三方观察者或者叫做沙箱(Sandbox)实现。
- 事件机制更深的功能是提供一个模块的通信机制。
- 对于组件库,组件实例之间的通信更加重要,组件实例之间最好不要存在相互引用的关系,互相不能感知对方的存在,有了事件机制,可以通过第三方有效的通知组件实例,减少组件实例间的耦合。
3.模板机制:
实际上写组件的时候,拼装html是一件很复杂的事情,模块能够从数据模型,对于组件库来讲通常是配置信息选项。将这些选项拼装成html字符串。但是大家普遍的一个误区是在追求语法的简便的和性能。我倒觉得模板要做的事情远不止如此。功能强大的模板不仅仅只完成字符串的拼接,而是要简化整个DOM操作,从数据模型到DOM的双向绑定,Model更新了DOM也随之更新。甚至要解决动态DOM事件绑定的问题。
4.面向对象的机制
放在组件库这个角度去谈面向对象的时候,他是一个架构设计中的一个重要的一环。
面向对象的机制能有效的提升代码的可复用性和扩展性,javascript灵活的语法诸如prototype/closure的方式,能构建出一个强大的类库。
可以使用继承机制扩展已有的组件。也可以用引用的方式装饰(Facade)现有组件,个人更倾向于使用装饰。因为继承总会不可避免的直接或者间接去访问父类的一些私有属性方法。
这个机制其实决定了一个组件的代码模型,通常需要解决的问题有:
- 该组件继承自哪些组件或者基础类,或者依赖于那些类?
- 组件实例的管理方式,因为每个组件实例都需要在一个容器中统一存放,理想的的存放模型应该是树形的,在内存中存在类似DOM树一样的组件对象树,是否可以通过类名找到相关实例,根据ID获取实例,获取子实例,父实例,父/子实例之间的通信父实例的resize是否能通知容器内的实例resize。
- 插件机制:作为一个非常重要的扩展机制,插件能有效的解决组件间的复用部分,通常这部分会叫做行为(behavior),对于组件不能提供的甚至是个性化的功能,有没有提供有效的,足够多的扩展点。
- 提供怎样的实例化方式? new XXX() ?? 还是类似DOM的操作方式appendInstance??甚至有类似jq这种链式。我更觉得应该使用appendInstance的方式,这样能更加有效的体现组件示例间的父/子关系。就像DOM操作一样,最终组件实例也是树形结构,如果我们直接new XXX() 这种方式,其实相当于声明了一个游离态的 DOM节点。实际我写代码的时候发现要管理这些组件实例也是比较麻烦的地方,试想一个页面如果有多个组件实例,需要声明多少个实例变量,或者申明多少个对象去存放这些实例。
- 组件提供的API,一个组件对外暴露的API会包括初始构造方法,公共方法(method),事件(event),对于event,提供怎样的eventData也非常重要。
5.模块化机制
如今模块化的思想已经深入人心,模块化带来了很好的团队多人完成一项大的任务的可能性,符合高内聚低耦合的思想。到了如今这个时代,万物皆模块。
组件库通常是一个庞大的工程,单靠个人英雄主义很难做的完整全面。
详细的来讲,模块化机制涉及:
- 模块本身的定义,注册,直接影响一个组件的代码模型,一个组件是一个模块。
- 模块的依赖申明以及追朔机制:就像前面提到的,依赖于那些类,css文件,资源,数据。不仅仅需要声明,还应该可追朔,依赖的父类,也能找到父类本身所依赖的资源,这样为按需部署打包,在线调试提供居多方便。
- 加载机制:因为在开发阶段要么放一个整个组件库代码,要么是通过一个加载器按需加载,到了线上希望只部署引用到了模块组件,这样可以减少实际部署的文件大小。加载机制会涉及到浏览器的javascript/css文件的加载,尤其是需要尽可能的并行下载而且按照依赖关系先后执行。包括应用模块,可以方便的通过这种加载机制延迟,按需,按时的加载到页面中。
- 打包部署机制:由于依赖可追朔,这样实际项目中用到的那些组件可以分析出来,最终可以根据实际使用到的情况打包出适合大小的组件库,减小冗余包的存在。
- 模块间的通信机制,由于模块减轻耦合甚至是独自孤立存在,组件之间的通信就非常重要,比如通常一个页面上面的菜单组件实例点击需要触发下面组件的更新。如果直接监听菜单事件去更新下面的组件,也许菜单是每个页面都有。但是下面的组件不是每个页面都有,这样的事件监听就显然耦合较重,互相依赖对方的存在。如果菜单点击这是告诉第三方我被点击了。下面的组件只去监听第三方的事件,这样的代码思路明显要好过很多。
需要再次强调的是万物皆模块,这意味着通用组件本身实现是一个模块,实际应用场景中的业务模块也是模块,都需要遵守模块的约定。
6.前端的基本开发思想
取决于个人经验:比如MVC的思想在组件实现过程中非常重要,Model通常是构造函数中那一堆配置信息,View通常是需要通过Model提供数据用户呈现,实际上View上的操作的相应都需要Model来记住状态。Controller用于操作二者。原理大家都知道,在开发中怎样分清三者界限。保证思路清晰。
前端中javascript/css/html的角色也类似与MVC。下面的一些代码方式就有违MVC的一些思想:
- css Expression:不仅IE only,更重要是CSS中写javascript这个风格不对,难以维护。
- javascript直接修改style.xxx='xx'。能写成css class的最好都写成css class。
- html中直接写onxxx="foo()",没有人能保证foo方法存在不被修改。
7.其他需要考虑的问题:
- 测试用例:通用组件库是一个庞大的工程,也许牵一发动全身,不知道哪个API的变动会影响多少调用者,有充足的单侧用例,一定程度上能保证整个组件库的稳定性。
- 粒度:粒度是一个很值得思考的问题,其实html的标签可以当做是一个小的组件,只是因为粒度很小,要完成一个复杂的应用,有很多的可复用组件都需要用到大片大片的重复html片段。另外一个极端,我们我们把整个页面都写成一个组件,显然没有复用性,跟没封装一样。所以选择一个合适的组件粒度,一个组件完成特定的功能,有利于搭建出有想象力的应用。
- 一致性:需要有很好的代码规范和约定。才能保证API的一致性,用户也会理所当然的想到怎样用API,降低学习成本,一个最常见的例子就事件监听的参数,建议统一为(eventObject,eventData)。
- 文档&Demo:这个不用多说,没有文档和demo的东西没有人去看代码怎么使用。
- 性能:DOM操作是组件库的性能杀手,高效的DOM操作尤其重要,事件监听也是非常耗时的,建议能采用代理的都用父节点来代理。
- 资源释放:组件的资源,引用是否能完全释放?尤其是开发SPA,组件的资源内存释放就非常重要了。
总结:
零零星星的提到了这么几点,也是过去工作中的体会,当然会有不足,望大家补充,回头可以check一下现在用到的一些框架/组件。欢迎讨论。