前端模块化实践(方法篇)

从 jQuery 的一统江湖再到 Angular 的异常火爆,咱们能够看到工程师们对于开发效率孜孜不倦的追求,你们都渴望着可以快速从这个充满纷争的互联网时代脱颖而出。尽管说“天下武功,惟快不破”,可是咱们不应忘记一个事实,那就是一个长期产品的维护成本要远远高于开发的成本。相比与单纯的代码量的减小,良好的总体设计更应该足够简单,容易被他人彻底正确的理解、能够快速的定位和处理bug、可以轻易的改变结构或者添加功能、支持轻松的编写测试。css

下文中我会以 Component 为例,来说讲咱们是如何基于 KISS原则 来实现总体的前端模块化设计(如下简称前端模块化设计),从而帮助产品更好的应对未来代码维护和升级须要的。html

先来明确下前端模块化这个概念,以及它所针对的问题。前端

什么是前端模块化设计

  • 它所针对的不只仅是经常使用前端小组件的设计,而是整个产品的全部前端部分的所有模块化。git

  • 不只仅是JS代码的模块化,一个模块能够包含js、css、html以及图片等其它资源。github

  • 不一样模块间的依赖和调用关系都应该是清晰简单的,事件流程也应该是能够被容易读懂和理解的。后端

  • 先后端要作到彻底分离,后端再也不提供渲染功能,仅仅作为数据的提供者(rest接口),不然没法确保调整的可靠性。浏览器

  • 它只针对应用型产品,对于追求性能和 SEO 的展现性页面来讲,前端的功能是有限的,用不着作总体的模块化设计。mvc

  • 它不是企业级组件,所谓企业级组件通常都是先后端一体的、跟具体某个业务紧密联系的,而前端模块化只包括前端,它只对应一个产品的一个部分,不必定对应某个具体业务(不一样业务同时依赖一个模块是很常见的)。app

为何要作前端模块化设计

  1. 咱们的产品须要针对不一样客户进行界面定制调整,它须要足够灵活应对各类组件上的变化。框架

  2. 咱们但愿咱们的产品可以带给用户 app 同样的流畅访问体验,而不是不停的重复刷新页面。

  3. 咱们的产品须要支持不一样的设备,咱们但愿大部分模块是可重用的,而没必要再去从新开发。

  4. 咱们的先后端是分离的,前端额外还承载了全部的渲染、路由、以及组件生命周期管理的逻辑,咱们但愿它简单可靠。

前端模块化设计的方法

  • MVX模式。咱们的界面只由三种简单的模块构成,M是Model,V是View,X就是其它。

    mvx.png

    1. Model,负责定义各类模型,以及向后端发送对应的资源增删改查请求并完成对应模型的转化。

    2. View,使用模板并提供渲染方法、绑定对应Model、建立并管理子view、响应各类事件、发送事件,以上只有第一点是必须的。

    3. 其它包含通用的组件,例如:tip、menu、dialog等等,以及各类全局通用的模块,提供例如错误提示、多语言、保存获取当前用户信息等功能。

  • 严格依赖关系。简单来讲就是层次化,具体包含如下几点:

    1. 通用组件和全局模块毫不应当依赖任何Model和View。

    2. Model只能依赖全局模块和通用组件。

    3. View能够依赖任何组件,但它毫不能够依赖除了自身建立的子View以外的任何View组件(保证独立性)。

    如下是一个真实项目中的模块依赖关系,能够看到清晰的层次关系
    out.png
    (其中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));
}

经过遵循简单的方法和约定,咱们能够作到不管是框架总体,仍是具体到某个调用,某个流程都很是容易被开发者所理解,从而极大的提高了后期继续开发和维护的效率。

下一篇我会谈谈咱们基于应用模块化所作的各类约定,它们可让整个前端体系保持简单一致,有效的避免冲突,而且帮助咱们快速定位和处理各类问题。

相关文章
相关标签/搜索