前端组件化开发和MVC

最近接触到了前端开发一些比较不错的思想:组件化开发、MVC,是我以前瞎写课程大作业的时候所不具备的。所以想写下来做个总结,也希望能给不太了解这一块的同学一点启发和帮助。这一篇,就用一个本地博客的demo,来简单地介绍一下组件化开发和MVC。

前提准备: 适当了解Backbone 和require.js (关于require.js可以看我之前关于js模块化编程的一篇文章:深入理解JavaScript系列(四): 模块化编程)

1.初步分析

先看下页面长什么样:(因为主要讲编程的思想,css什么的就随意搞搞)

前端组件化开发和MVC_第1张图片

可以看到,页面分为3个部分,header, footer和中间部分。中间部分又分为左边sidebar和右边blog content部分。sidebar还可以细分为一个new button和下面的一堆item,blogContent还可以细分为...等等。

要做这样一个页面很简单,按照我以前的写法,在html文件中巴拉巴拉一堆,这个界面就出来了。但是这种方式导致的问题就是:页面上所有的部件耦合比较严重,而且单个部件的可重用性不强。针对这个问题,一个很简单的解决方法就是HTML片段化和模板化,即把各个部分分拆成不同的html文件,使用include的方式来增加重用。但是这明显远远不够,它只是重用了展示部分的代码,对于事件响应就无能为力。我们仍然需要写很多事件监听(如onclick)去直接修改页面上别的组件的展示方式,可能写出如下的代码:

newButton.onClick = function () {
	//左边sidebar中加一篇文章
	//在blogContent中展示出新的文章的初始内容
}
我们希望最终做到的是,所有的组件只管自己的事,它不需要知道页面上还有哪些别的东西。即所谓的高内聚,低耦合。


二.利用Backbone实现

1.代码结构

前端组件化开发和MVC_第2张图片

先看一下index.html(也就是我们在上面看到的那个页面):
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Blog</title>
    <link href="./css/bootstrap.css" rel="stylesheet">
    <link href="./css/bootstrap-theme.css" rel="stylesheet">
    <link href="./css/page.css" rel="stylesheet">
    <script src="./js/lib/require.js"></script>
    <script src="./js/require_config.js"></script>
</head>
<body>
</body>
</html>

整个body都是空的,而整个程序的入口就在require_config.js中:
require.config({
        baseUrl: './js',
        paths: {
            'jquery': 'lib/jquery',
            'backbone': 'lib/backbone',
            'underscore': 'lib/underscore',
            'bootstrap': 'lib/bootstrap',
            'localstorage': 'lib/backbone.localStorage'
        },
        shim: {
            'localstorage': {
                deps: ['backbone']
            },
            'bootstrap': {
                deps: ['jquery']
            }
        }
    });

require(['backbone', 'routers/AppRouter', 'bootstrap'], function(Backbone, Router) {
    new Router();
    Backbone.history.start({pushState: true})
});

在这里新建了一个Router对象,用来做一些初始化操作。 
 
  
AppRouter.js:
define([
    'underscore',
    'backbone',
    'collections/BlogCollection',
    'controllers/AppController'
], function(_, Backbone, BlogCollection, AppController) {

    return Backbone.Router.extend({
        routes: {

        },
        initialize: function() {
            Backbone.Router.prototype.initialize.apply(this, arguments);

            // common models and collection goes here
            this.model = {};
            this.model.controller = new Backbone.Model(); // to trigger action
            this.collection = {};
            this.collection.blogs = new BlogCollection();

            // deferreds that track model & collection state
            this.deferreds = {};
            this.deferreds.blogsReady = this.collection.blogs.fetch();

            // create the main controller
            this.mainController = new AppController({
                model: this.model,
                collection: this.collection,
                deferreds: this.deferreds
            });
        }
    });
});
在这里,我们做的事情是创建一些页面上各个组件共享的model和collection, 另外,创建了一个controller,来对组件中trigger的事件做监听和处理。

APPController.js:

define([
    'jquery',
    'underscore',
    'backbone',
    'views/Master',
    'models/BlogModel',
    'collections/BlogCollection'
], function($, _, Backbone, MasterView, BlogModel, BlogCollection) {

    var Controller = function() {
        Controller.prototype.initialize.apply(this, arguments);
    };

    _.extend(Controller.prototype, Backbone.Events, {
        initialize: function(options) {
            options || (options = {});

            // initialize models of this page
            this.model = _.extend({
                blog: new Backbone.Model({
                    blogModel: null,
                    state: 'view'
                }) // model that controls blog page
            }, options.model);

            this.collection = options.collection || {};
            this.deferreds = options.deferreds || {};

            this.masterView = new MasterView({
                model: this.model,
                collection: this.collection,
                deferreds: this.deferreds
            });

            $('body').append(this.masterView.render().$el);

            this.listenTo(this.model.controller, 'all', this._handleAction);
            this.listenTo(this.model.blog, 'change:blogModel', this._onBlogModelChanged);
        },
        _onBlogModelChanged: function() {
            var previousModel = this.model.blog.previous('blogModel');
            var newModel = this.model.blog.get('blogModel');
            // stop listening previous blog model
            previousModel && this.stopListening(previousModel);
            newModel && this.listenTo(newModel, 'change', _.debounce(function() {
                newModel.save();
            }), 1000);
        },
        _handleAction: function(type, args) {
            console.log('type:%s ,args: %s', type, args);
            switch (type) {
                case 'action:view':
                    this.model.blog.set({
                        state: 'view'
                    });
                    break;
                case 'action:edit':
                    this.model.blog.set({
                        state: 'edit'
                    });
                    break;
                case 'action:select':
                    var blogModel = args;
                    this.model.blog.set({
                        blogModel: blogModel,
                        state: 'view'
                    });
                    break;
                case 'action:new':
                    var newBlogModel = this.collection.blogs.create({
                        source: '## Hello World',
                        title: 'New Note'
                    });
                    this.model.blog.set({
                        blogModel: newBlogModel,
                        state: 'edit'
                    });
                    break;
                case 'action:delete':
                    var currentModel = this.model.blog.get('blogModel');
                    var nextIndex = this.collection.blogs.indexOf(currentModel);
                    currentModel.destroy().then(function() {
                        var nextModel = this.collection.blogs.at(Math.min(nextIndex, this.collection.blogs.length - 1));
                        this.model.blog.set({
                            blogModel: nextModel,
                            state: 'view'
                        });
                    }.bind(this));
                    break;
            }
        }
    });

    return Controller;
});


 
  
$('body').append(this.masterView.render().$el); 这一句是把整个MasterView渲染之后的html元素加入到body标签中,至于MasterView,它本身只是一个独立
的view,内部有自己的层级关系,它不需要知道自己会被加入到哪个元素下面:
define([
    'module',
    'jquery',
    'underscore',
    'views/Base',
    'views/Header',
    'views/Footer',
    'views/sidebar/Master',
    'views/blog/Master'
], function(module, $, _, BaseView, HeaderView, FooterView, SidebarMasterView, BlogMasterView) {

    /**
     * Master page
     */
    return BaseView.extend({
        className: 'page-master',
        moduleId: module.id,
        initialize: function() {
            BaseView.prototype.initialize.apply(this, arguments);
            this.children.header = new HeaderView({
                model: this.model,
                collection: this.collection,
                deferreds: this.deferreds
            });
            this.children.footer = new FooterView({
                model: this.model,
                collection: this.collection,
                deferreds: this.deferreds
            });
            this.children.sidebar = new SidebarMasterView({
                model: this.model,
                collection: this.collection,
                deferreds: this.deferreds
            });
            this.children.blog = new BlogMasterView({
                model: this.model,
                collection: this.collection,
                deferreds: this.deferreds
            });
        },
        render: function() {
            this.$el.append(this.children.header.render().$el);
            this.$el.append($('<div class="container-fluid"><div class="row blog-body"></div></div>'));
            this.$el.append(this.children.footer.render().$el);
            this._renderBlogBody();
            return this;
        },
        _renderBlogBody: function() {
            var $body = this.$el.find('.blog-body');
            $body.append(this.children.sidebar.render().$el);
            $body.append(this.children.blog.render().$el);
        }
    });
});
它只负责自己各个子view的渲染,并把他们拼装在一起,至于新建masterview用来做什么,它自己是不需要知道的。我可以把它放在body标签下,也可以去做一些其他的事情。
接下来,就以sidebar为例,分析下所谓的MVC:
define([
    'jquery',
    'underscore',
    'backbone',
    'views/Base',
    'views/sidebar/Item'
], function($, _, Backbone, BaseView, BlogItem) {

    /**
     * Sidebar View
     */

    return BaseView.extend({
        tagName: 'div',
        className: 'col-sm-3 col-md-2 blog-sidebar',
        events: {
            'click .btn-blog-new': function(e) {
                e.preventDefault();
                this.model.controller.trigger('action:new');
            }
        },
        initialize: function(options) {
            BaseView.prototype.initialize.apply(this, arguments);
            this.deferreds.blogsReady = this.deferreds.blogsReady || $.Deferred().resolve();
            this.collection.blogs = this.collection.blogs || new Backbone.Collection();
            this.listenTo(this.collection.blogs, 'all', this._renderBlogs);
        },
        render: function() {
            // loading..
            this.deferreds.blogsReady.then(this._render.bind(this));
            return this;
        },
        _render: function() {
            this.$el.html(this.compiledTemplate());
            this._renderBlogs();
            return this;
        },
        _renderBlogs: function() {
            var $blogs = this.$el.find('.nav-sidebar').empty();
            this.collection.blogs.each(function(blogModel) {
                var item = new BlogItem({
                    model: _.extend({}, this.model, {
                        blogModel: blogModel
                    })
                });
                item.render().$el.appendTo($blogs);
            }, this);
        },
        template: '\
            <button type="button" class="btn-blog-new btn btn-success">New</button>\
            <ul class="nav nav-sidebar"></ul>\
        '
    });
});
template中的html创建了一个新建按钮以及一个list;在events中监听了按钮的点击事件,当点击按钮时,出发controller的‘action:new’事件,由controller去做
一些model以及collections的变更操作,这样监听model以及collections的组件会根据model以及collections的变化去做出相应的界面变化。
看上面APPController的代码:
  case 'action:new':
                    var newBlogModel = this.collection.blogs.create({
                        source: '## Hello World',
                        title: 'New Note'
                    });
                    this.model.blog.set({
                        blogModel: newBlogModel,
                        state: 'edit'
                    });
                    break;
action:new被触发之后,首先是创建一个新的model,然后加入到collections中,再把当前的model改变为新建的model。
sidebar监听到了collections的变更(在initial中有一句 this.listenTo(this.collection.blogs, 'all', this._renderBlogs);),就去重新渲染自己list 的部分。而右边的blogcontent监听到了model的变更,就会去修改自己view中的展示。(这里就不放代码免得太长) 

这种方式相当于加了一个中介者模式,由一个中介(即这里的controller)去调度操作所产生的变化,而不是直接在某一个view中因为某一些事件的产生,直接去修改别的 view。
通过这种方式,组件与组件之间的耦合度大大减小。

你可能感兴趣的:(JavaScript,mvc,backbone,前端开发,组件化开发)