最近接触到了前端开发一些比较不错的思想:组件化开发、MVC,是我以前瞎写课程大作业的时候所不具备的。所以想写下来做个总结,也希望能给不太了解这一块的同学一点启发和帮助。这一篇,就用一个本地博客的demo,来简单地介绍一下组件化开发和MVC。
前提准备: 适当了解Backbone 和require.js (关于require.js可以看我之前关于js模块化编程的一篇文章:深入理解JavaScript系列(四): 模块化编程)
先看下页面长什么样:(因为主要讲编程的思想,css什么的就随意搞搞)
可以看到,页面分为3个部分,header, footer和中间部分。中间部分又分为左边sidebar和右边blog content部分。sidebar还可以细分为一个new button和下面的一堆item,blogContent还可以细分为...等等。
要做这样一个页面很简单,按照我以前的写法,在html文件中巴拉巴拉一堆,这个界面就出来了。但是这种方式导致的问题就是:页面上所有的部件耦合比较严重,而且单个部件的可重用性不强。针对这个问题,一个很简单的解决方法就是HTML片段化和模板化,即把各个部分分拆成不同的html文件,使用include的方式来增加重用。但是这明显远远不够,它只是重用了展示部分的代码,对于事件响应就无能为力。我们仍然需要写很多事件监听(如onclick)去直接修改页面上别的组件的展示方式,可能写出如下的代码:
newButton.onClick = function () { //左边sidebar中加一篇文章 //在blogContent中展示出新的文章的初始内容 }我们希望最终做到的是,所有的组件只管自己的事,它不需要知道页面上还有哪些别的东西。即所谓的高内聚,低耦合。
<!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>
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}) });
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,内部有自己的层级关系,它不需要知道自己会被加入到哪个元素下面:
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。