从 jQuery 的一统江湖再到 Angular 的异常火爆,咱们能够看到工程师们对于开发效率孜孜不倦的追求,你们都渴望着可以快速从这个充满纷争的互联网时代脱颖而出。尽管说“天下武功,惟快不破”,可是咱们不应忘记一个事实,那就是一个长期产品的维护成本要远远高于开发的成本。相比与单纯的代码量的减小,良好的总体设计更应该足够简单,容易被他人彻底正确的理解、能够快速的定位和处理bug、可以轻易的改变结构或者添加功能、支持轻松的编写测试。css
下文中我会以 Component 为例,来说讲咱们是如何基于 KISS原则 来实现总体的前端模块化设计(如下简称前端模块化设计),从而帮助产品更好的应对未来代码维护和升级须要的。html
先来明确下前端模块化这个概念,以及它所针对的问题。前端
它所针对的不只仅是经常使用前端小组件的设计,而是整个产品的全部前端部分的所有模块化。git
不只仅是JS代码的模块化,一个模块能够包含js、css、html以及图片等其它资源。github
不一样模块间的依赖和调用关系都应该是清晰简单的,事件流程也应该是能够被容易读懂和理解的。后端
先后端要作到彻底分离,后端再也不提供渲染功能,仅仅作为数据的提供者(rest接口),不然没法确保调整的可靠性。浏览器
它只针对应用型产品,对于追求性能和 SEO 的展现性页面来讲,前端的功能是有限的,用不着作总体的模块化设计。mvc
它不是企业级组件,所谓企业级组件通常都是先后端一体的、跟具体某个业务紧密联系的,而前端模块化只包括前端,它只对应一个产品的一个部分,不必定对应某个具体业务(不一样业务同时依赖一个模块是很常见的)。app
咱们的产品须要针对不一样客户进行界面定制调整,它须要足够灵活应对各类组件上的变化。框架
咱们但愿咱们的产品可以带给用户 app 同样的流畅访问体验,而不是不停的重复刷新页面。
咱们的产品须要支持不一样的设备,咱们但愿大部分模块是可重用的,而没必要再去从新开发。
咱们的先后端是分离的,前端额外还承载了全部的渲染、路由、以及组件生命周期管理的逻辑,咱们但愿它简单可靠。
MVX模式。咱们的界面只由三种简单的模块构成,M是Model,V是View,X就是其它。
Model,负责定义各类模型,以及向后端发送对应的资源增删改查请求并完成对应模型的转化。
View,使用模板并提供渲染方法、绑定对应Model、建立并管理子view、响应各类事件、发送事件,以上只有第一点是必须的。
其它包含通用的组件,例如:tip、menu、dialog等等,以及各类全局通用的模块,提供例如错误提示、多语言、保存获取当前用户信息等功能。
严格依赖关系。简单来讲就是层次化,具体包含如下几点:
通用组件和全局模块毫不应当依赖任何Model和View。
Model只能依赖全局模块和通用组件。
View能够依赖任何组件,但它毫不能够依赖除了自身建立的子View以外的任何View组件(保证独立性)。
如下是一个真实项目中的模块依赖关系,能够看到清晰的层次关系
(其中user、client、board、widget是model)
合理划分界面模块。能够参考 Sencha的这篇文章,简言之就是不能太大,太大承载的逻辑太多,也不能过小,过小则过于碎片化,增大管理的难度。咱们的作法是先按逻辑上互相独立的大区域作成最底层的view,逻辑上应当独立的块作成独立的view,对于中间的view层则最开始尽可能少作。若是发现因为业务的变动中间的view层承接了太多功能再进行拆分,由于view只会被它的上一层建立者调用,并且它依赖的模块由于使用 CMD 也能很是清楚的看到,因此重构起来仍是很是容易的。
可控的消息机制。父view能够直接调用子view的方法,可是子view发生变化,须要通知其它的view该怎么办呢?咱们不喜欢 pagebus 那种不可控的全局消息模式,因此咱们采用了相似浏览器事件冒泡的简单的事件冒泡方式,并且限制了子view的事件只能由上一层接受,若是还须要向上传递则让上一层继续发送事件,实践中这种需求仍是很是少的。
var model = require('model'); /** * User model. */ module.exports = model('User') .attr('id') .attr('name') .attr('email')
var dom = require('dom'); var User = require('user'); var Emitter = require('emitter'); var template = require('./template'); //parent为渲染该view的dom节点 function UsersView(parent) { var el = dom(template); el.appendTo(parent); this.el = el; el.on('click', '.add', this.addUser.bind(this)); //load all users User.all(function(err, users){ if(err) throw err; users.each(function(user){ this.appendUser(user); }.bind(this)); }.bind(this)); } Emitter(UsersView.prototype); UsersView.prototype.addUser = function(){ var user = new User({ name: this.el.find('[name="name"]').value(), email: this.el.find('[name="email"]').value() }); user.save(function(err, user){ if(err) throw err; this.appendUser(user); //向父层发送通知 this.emit('new', user); }.bind(this)); } UsersView.prototype.appendUser = function(user){ //渲染新用户,此处省略 } module.exports = UsersView;
var template = require('./template'); var dom = require('dom'); //模块名采用小写加中横线命名,由于文件夹采用小写命名更好 var UserView = require('user-view'); function MainView(parent) { var el = dom(template); el.appendTo(parent); this.userView = new UserView(el.get(0)); //事件接受 this.userView.on('new', function(user){ console.log('new user ' + user.name); }.bind(this)); }
经过遵循简单的方法和约定,咱们能够作到不管是框架总体,仍是具体到某个调用,某个流程都很是容易被开发者所理解,从而极大的提高了后期继续开发和维护的效率。
下一篇我会谈谈咱们基于应用模块化所作的各类约定,它们可让整个前端体系保持简单一致,有效的避免冲突,而且帮助咱们快速定位和处理各类问题。