3

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

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

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

什么是前端模块化设计

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

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

  • 不同模块间的依赖和调用关系都应该是清晰简单的,事件流程也应该是可以被容易读懂和理解的。

  • 前后端要做到完全分离,后端不再提供渲染功能,仅仅做为数据的提供者(rest接口),否则无法确保调整的可靠性。

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

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

为什么要做前端模块化设计

  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));
}

通过遵循简单的方法和约定,我们可以做到不论是框架整体,还是具体到某个调用,某个流程都非常容易被开发者所理解,从而极大的提升了后期继续开发和维护的效率。

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


chemzqm
2k 声望83 粉丝

Javascript全栈开发,产品设计,自动化工具。追求简洁的设计,模块化开发,卓越的用户体验。


引用和评论

0 条评论