最近接触到了前端开发一些比较不错的思想:组件化开发、MVC,是我以前瞎写课程大作业的时候所不具备的。所以想写下来做个总结,也希望能给不太了解这一块的同学一点启发和帮助。这一篇,就用一个本地博客的demo,来简单地介绍一下组件化开发和MVC。
前提准备: 适当了解Backbone 和require.js (关于require.js可以看我之前关于js模块化编程的一篇文章:深入理解JavaScript系列(四): 模块化编程)
1.初步分析
先看下页面长什么样:(因为主要讲编程的思想,css什么的就随意搞搞)
可以看到,页面分为3个部分,header, footer和中间部分。中间部分又分为左边sidebar和右边blog content部分。sidebar还可以细分为一个new button和下面的一堆item,blogContent还可以细分为...等等。
要做这样一个页面很简单,按照我以前的写法,在html文件中巴拉巴拉一堆,这个界面就出来了。但是这种方式导致的问题就是:页面上所有的部件耦合比较严重,而且单个部件的可重用性不强。针对这个问题,一个很简单的解决方法就是HTML片段化和模板化,即把各个部分分拆成不同的html文件,使用include的方式来增加重用。但是这明显远远不够,它只是重用了展示部分的代码,对于事件响应就无能为力。我们仍然需要写很多事件监听(如onclick)去直接修改页面上别的组件的展示方式,可能写出如下的代码:
newButton.onClick = function () {
//左边sidebar中加一篇文章
//在blogContent中展示出新的文章的初始内容
}
我们希望最终做到的是,所有的组件只管自己的事,它不需要知道页面上还有哪些别的东西。即所谓的高内聚,低耦合。
二.利用Backbone实现
1.代码结构
<!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对象,用来做一些初始化操作。
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的事件做监听和处理。
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,内部有自己的层级关系,它不需要知道自己会被加入到哪个元素下面:
它只负责自己各个子view的渲染,并把他们拼装在一起,至于新建masterview用来做什么,它自己是不需要知道的。我可以把它放在body标签下,也可以去做一些其他的事情。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); } }); });
接下来,就以sidebar为例,分析下所谓的MVC:
template中的html创建了一个新建按钮以及一个list;在events中监听了按钮的点击事件,当点击按钮时,出发controller的‘action:new’事件,由controller去做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>\ ' }); });
一些model以及collections的变更操作,这样监听model以及collections的组件会根据model以及collections的变化去做出相应的界面变化。
看上面APPController的代码:
action:new被触发之后,首先是创建一个新的model,然后加入到collections中,再把当前的model改变为新建的model。case 'action:new': var newBlogModel = this.collection.blogs.create({ source: '## Hello World', title: 'New Note' }); this.model.blog.set({ blogModel: newBlogModel, state: 'edit' }); break;