前面花了四章的時間完成了項目(?[wechat](https://github.com/the5fire/wechat)?)的開發,并且也放到了線上。這篇來說說模塊化的事情。
## 15.1 模塊化的概念
對于通常的網站來說,一般我們不會把所有的js都寫到一個文件中,因為當一個文件中的代碼行數太多的話會導致維護性變差,因此我們常常會根據業務(頁面)來組織js文件,比如全站都用到的功能,我就寫一個base.js,只是在首頁會用到的功能,就寫一個index.js。這樣的話我更改首頁的邏輯只需要更改index.js文件,不需要考慮太多的不相關業務邏輯。當然還有很重要的一點是按需加載,在非index.js頁面我就不需要引入index.js。
那么對于單頁應用(SPA)來說要怎么做呢,只有一個頁面,按照傳統的寫法,即便是分開多個文件來寫,也得全部放到標簽中,由瀏覽器統一加載。如果你有后端開發經驗的話,你會意識到,是不是我們可以像寫后端程序(比如Python)那樣,定義不同的包、模塊。在另外的模塊中按需加載(import)呢?
答案當然是可以。
在前端也有模塊化這樣的規范,不過是有兩套:AMD和CMD。關于這倆規范的對比可以參考知乎上的問答?[AMD 和 CMD 的區別有哪些](http://www.zhihu.com/question/20351507)?。
按照AMD和CMD實現的兩個可以用來做模塊化的是庫分別是:require.js和sea.js。從本章的題目可以知道我們這里主要把require.js引入我們的項目。 對于這兩庫我都做了一個簡單的Demo,再看下面長篇代碼之前,可以先感受下:?[require.js Demo](https://github.com/the5fire/backbonejs-learning-note/blob/master/code/requirejs-demo)?和?[sea.js Demo](https://github.com/the5fire/backbonejs-learning-note/blob/master/code/seajs-demo)?。
## 15.2 簡單使用require.js
要使用require.js其實非常簡單,主要有三個部分:1\. 頁面引入require.js;2\. 定義模塊;3\. 加載模塊。我們以上面提到我做的那個demo為例:
_首先_?- 頁面引入
~~~
<!DOCTYPE html>
<html>
<head>
<title>the5fire.com-backbone.js-Hello World</title>
</head>
<body>
<button id="check">新手報到- requirejs版</button>
<ul id="world-list">
</ul>
<a href="http://www.the5fire.com">更多教程</a>
<script data-main="static/main.js" src="static/lib/require.js"></script>
</body>
</html>
~~~
上面的script的data-main定義了入口文件,我們把配置項也放到了入口文件中。 來看下入口文件:
~~~
require.config({
baseUrl: 'static/',
shim: {
underscore: {
exports: '_'
},
},
paths: {
jquery: 'lib/jquery',
underscore: 'lib/underscore',
backbone: 'lib/backbone'
}
});
require(['jquery', 'backbone', 'js/app'], function($, Backbone, AppView) {
var appView = new AppView();
});
~~~
上面baseUrl部分指明了所有要加載模塊的根路徑,shim是指那些非AMD規范的庫,paths相當于你js文件的別名,方便引入。
后面的require就是入口了,加載完main.js后會執行這部分代碼,這部分代碼的意思是,加載?`jquery`?、?`backbone`?、`js/app`?(這個也可通過paths來定義別名),并把加載的內容傳遞到后面的function的參數中。o
來看看js/app的定義。
_定義模塊_
~~~
// app.js
define(['jquery', 'backbone'], function($, Backbone) {
var AppView = Backbone.View.extend({
// blabla..bla
});
return AppView;
});
// 或者這種方式
define(function(require, exports, module) {
var $ = require('jquery');
var Backbone = require('backbone');
var AppView = Backbone.View.extend({
// blabla..bla
});
return AppView;
});
~~~
這兩種方式均可,最后需要返回你想暴露外面的對象。這個對象(AppView)會在其他模塊中?`require('js/app')`?時加載,就像上面一樣。
## 15.3 拆分文件
上一篇中我們寫了一個很長的chat.js的文件,這個文件包含了所有的業務邏輯。這里我們就一步步來把這個文件按照require.js的定義拆分成模塊。
上一篇是把chat.js文件分開來講的,這里先來感受下整體代碼:
~~~
$(function(){
var User = Backbone.Model.extend({
urlRoot: '/user',
});
var Topic = Backbone.Model.extend({
urlRoot: '/topic',
});
var Message = Backbone.Model.extend({
urlRoot: '/message',
});
var Topics = Backbone.Collection.extend({
url: '/topic',
model: Topic,
});
var Messages = Backbone.Collection.extend({
url: '/message',
model: Message,
});
var topics = new Topics;
var TopicView = Backbone.View.extend({
tagName: "div class='column'",
templ: _.template($('#topic-template').html()),
// 渲染列表頁模板
render: function() {
$(this.el).html(this.templ(this.model.toJSON()));
return this;
},
});
var MessageView = Backbone.View.extend({
tagName: "div class='comment'",
templ: _.template($('#message-template').html()),
// 渲染列表頁模板
render: function() {
$(this.el).html(this.templ(this.model.toJSON()));
return this;
},
});
var UserView = Backbone.View.extend({
el: "#user_info",
username: $('#username'),
show: function(username) {
this.username.html(username);
this.$el.show();
},
});
var AppView = Backbone.View.extend({
el: "#main",
topic_list: $("#topic_list"),
topic_section: $("#topic_section"),
message_section: $("#message_section"),
message_list: $("#message_list"),
message_head: $("#message_head"),
events: {
'click .submit': 'saveMessage',
'click .submit_topic': 'saveTopic',
'keypress #comment': 'saveMessageEvent',
},
initialize: function() {
_.bindAll(this, 'addTopic', 'addMessage');
topics.bind('add', this.addTopic);
// 定義消息列表池,每個topic有自己的message collection
// 這樣保證每個主題下得消息不沖突
this.message_pool = {};
this.message_list_div = document.getElementById('message_list');
},
addTopic: function(topic) {
var view = new TopicView({model: topic});
this.topic_list.append(view.render().el);
},
addMessage: function(message) {
var view = new MessageView({model: message});
this.message_list.append(view.render().el);
},
saveMessageEvent: function(evt) {
if (evt.keyCode == 13) {
this.saveMessage(evt);
}
},
saveMessage: function(evt) {
var comment_box = $('#comment')
var content = comment_box.val();
if (content == '') {
alert('內容不能為空');
return false;
}
var topic_id = comment_box.attr('topic_id');
var message = new Message({
content: content,
topic_id: topic_id,
});
self = this;
var messages = this.message_pool[topic_id];
message.save(null, {
success: function(model, response, options){
comment_box.val('');
// 重新獲取,看服務器端是否有更新
// 比較丑陋的更新機制
messages.fetch({
data: {topic_id: topic_id},
success: function(){
self.message_list.scrollTop(self.message_list_div.scrollHeight);
messages.add(response);
},
});
},
});
},
saveTopic: function(evt) {
var topic_title = $('#topic_title');
if (topic_title.val() == '') {
alert('主題不能為空!');
return false
}
var topic = new Topic({
title: topic_title.val(),
});
self = this;
topic.save(null, {
success: function(model, response, options){
topics.add(response);
topic_title.val('');
},
});
},
showTopic: function(){
topics.fetch();
this.topic_section.show();
this.message_section.hide();
this.message_list.html('');
},
initMessage: function(topic_id) {
var messages = new Messages;
messages.bind('add', this.addMessage);
this.message_pool[topic_id] = messages;
},
showMessage: function(topic_id) {
this.initMessage(topic_id);
this.message_section.show();
this.topic_section.hide();
this.showMessageHead(topic_id);
$('#comment').attr('topic_id', topic_id);
var messages = this.message_pool[topic_id];
messages.fetch({
data: {topic_id: topic_id},
success: function(resp) {
self.message_list.scrollTop(self.message_list_div.scrollHeight)
}
});
},
showMessageHead: function(topic_id) {
var topic = new Topic({id: topic_id});
self = this;
topic.fetch({
success: function(resp, model, options){
self.message_head.html(model.title);
}
});
},
});
var LoginView = Backbone.View.extend({
el: "#login",
wrapper: $('#wrapper'),
events: {
'keypress #login_pwd': 'loginEvent',
'click .login_submit': 'login',
'keypress #reg_pwd_repeat': 'registeEvent',
'click .registe_submit': 'registe',
},
hide: function() {
this.wrapper.hide();
},
show: function() {
this.wrapper.show();
},
loginEvent: function(evt) {
if (evt.keyCode == 13) {
this.login(evt);
}
},
login: function(evt){
var username_input = $('#login_username');
var pwd_input = $('#login_pwd');
var u = new User({
username: username_input.val(),
password: pwd_input.val(),
});
u.save(null, {
url: '/login',
success: function(model, resp, options){
g_user = resp;
// 跳轉到index
appRouter.navigate('index', {trigger: true});
}
});
},
registeEvent: function(evt) {
if (evt.keyCode == 13) {
this.registe(evt);
}
},
registe: function(evt){
var reg_username_input = $('#reg_username');
var reg_pwd_input = $('#reg_pwd');
var reg_pwd_repeat_input = $('#reg_pwd_repeat');
var u = new User({
username: reg_username_input.val(),
password: reg_pwd_input.val(),
password_repeat: reg_pwd_repeat_input.val(),
});
u.save(null, {
success: function(model, resp, options){
g_user = resp;
// 跳轉到index
appRouter.navigate('index', {trigger: true});
}
});
},
});
var AppRouter = Backbone.Router.extend({
routes: {
"login": "login",
"index": "index",
"topic/:id" : "topic",
},
initialize: function(){
// 初始化項目, 顯示首頁
this.appView = new AppView();
this.loginView = new LoginView();
this.userView = new UserView();
this.indexFlag = false;
},
login: function(){
this.loginView.show();
},
index: function(){
if (g_user && g_user.id != undefined) {
this.appView.showTopic();
this.userView.show(g_user.username);
this.loginView.hide();
this.indexFlag = true; // 標志已經到達主頁了
}
},
topic: function(topic_id) {
if (g_user && g_user.id != undefined) {
this.appView.showMessage(topic_id);
this.userView.show(g_user.username);
this.loginView.hide();
this.indexFlag = true; // 標志已經到達主頁了
}
},
});
var appRouter = new AppRouter();
var g_user = new User;
g_user.fetch({
success: function(model, resp, options){
g_user = resp;
Backbone.history.start({pustState: true});
if(g_user === null || g_user.id === undefined) {
// 跳轉到登錄頁面
appRouter.navigate('login', {trigger: true});
} else if (appRouter.indexFlag == false){
// 跳轉到首頁
appRouter.navigate('index', {trigger: true});
}
},
}); // 獲取當前用戶
});
~~~
上面三百多行的代碼其實只是做了最基本的實現,按照上篇文章的介紹,我們根據User,Topic,Message,AppView,AppRouter來拆分。當然你也可以通過類似后端的常用的結構:Model, View,Router來拆分。
_User的拆分_
這個模塊我打算定義用戶相關的所有內容,包括數據獲取,頁面渲染,還有登錄狀態,于是有了這個代碼:
~~~
// user.js
define(function(require, exports, module) {
var $ = require('jquery');
var Backbone = require('backbone');
var _ = require('underscore');
var User = Backbone.Model.extend({
urlRoot: '/user',
});
var LoginView = Backbone.View.extend({
el: "#login",
wrapper: $('#wrapper'),
initialize: function(appRouter) {
this.appRouter = appRouter;
},
events: {
'keypress #login_pwd': 'loginEvent',
'click .login_submit': 'login',
'keypress #reg_pwd_repeat': 'registeEvent',
'click .registe_submit': 'registe',
},
hide: function() {
this.wrapper.hide();
},
show: function() {
this.wrapper.show();
},
loginEvent: function(evt) {
if (evt.keyCode == 13) {
this.login(evt);
}
},
login: function(evt){
var username_input = $('#login_username');
var pwd_input = $('#login_pwd');
var u = new User({
username: username_input.val(),
password: pwd_input.val(),
});
var self = this;
u.save(null, {
url: '/login',
success: function(model, resp, options){
self.appRouter.g_user = resp;
// 跳轉到index
self.appRouter.navigate('index', {trigger: true});
}
});
},
registeEvent: function(evt) {
if (evt.keyCode == 13) {
this.registe(evt);
}
},
registe: function(evt){
var reg_username_input = $('#reg_username');
var reg_pwd_input = $('#reg_pwd');
var reg_pwd_repeat_input = $('#reg_pwd_repeat');
var u = new User({
username: reg_username_input.val(),
password: reg_pwd_input.val(),
password_repeat: reg_pwd_repeat_input.val(),
});
var self = this;
u.save(null, {
success: function(model, resp, options){
self.appRouter.g_user = resp;
// 跳轉到index
self.appRouter.navigate('index', {trigger: true});
}
});
},
});
var UserView = Backbone.View.extend({
el: "#user_info",
username: $('#username'),
show: function(username) {
this.username.html(username);
this.$el.show();
},
});
module.exports = {
"User": User,
"UserView": UserView,
"LoginView": LoginView,
};
});
~~~
通過define的形式定義了User這個模塊,最后通過module.exports暴露給外面User,UserView和LoginView。
_Topic模塊_
同User一樣,我們在這個模塊定義Topic的Model、Collection和View,來完成topic數據的獲取也最終渲染。
~~~
//topic.js
define(function(require, exports, module) {
var $ = require('jquery');
var Backbone = require('backbone');
var _ = require('underscore');
var Topic = Backbone.Model.extend({
urlRoot: '/topic',
});
var Topics = Backbone.Collection.extend({
url: '/topic',
model: Topic,
});
var TopicView = Backbone.View.extend({
tagName: "div class='column'",
templ: _.template($('#topic-template').html()),
// 渲染列表頁模板
render: function() {
$(this.el).html(this.templ(this.model.toJSON()));
return this;
},
});
module.exports = {
"Topic": Topic,
"Topics": Topics,
"TopicView": TopicView,
}
});
~~~
一樣的,這個模塊也對外暴露了Topic、Topics、TopicView的內容。
_message模塊_
~~~
//message.js
define(function(require, exports, module) {
var $ = require('jquery');
var Backbone = require('backbone');
var _ = require('underscore');
var Message = Backbone.Model.extend({
urlRoot: '/message',
});
var Messages = Backbone.Collection.extend({
url: '/message',
model: Message,
});
var MessageView = Backbone.View.extend({
tagName: "div class='comment'",
templ: _.template($('#message-template').html()),
// 渲染列表頁模板
render: function() {
$(this.el).html(this.templ(this.model.toJSON()));
return this;
},
});
module.exports = {
"Messages": Messages,
"Message": Message,
"MessageView": MessageView,
}
});
~~~
最后也是對外暴露了Message、Messages和MessageView數據。
_AppView模塊_
上面定義的都是些基礎模塊,這個模塊我們之前也說過,可以稱為“管家View”,因為它是專門用來管理其他模塊的。
~~~
//appview.js
define(function(require, exports, module) {
var $ = require('jquery');
var _ = require('underscore');
var Backbone = require('backbone');
var TopicModule = require('topic');
var MessageModule = require('message');
var Topics = TopicModule.Topics;
var TopicView = TopicModule.TopicView;
var Topic = TopicModule.Topic;
var Message = MessageModule.Message;
var Messages = MessageModule.Messages;
var MessageView = MessageModule.MessageView;
var topics = new Topics();
var AppView = Backbone.View.extend({
el: "#main",
topic_list: $("#topic_list"),
topic_section: $("#topic_section"),
message_section: $("#message_section"),
message_list: $("#message_list"),
message_head: $("#message_head"),
events: {
'click .submit': 'saveMessage',
'click .submit_topic': 'saveTopic',
'keypress #comment': 'saveMessageEvent',
},
initialize: function() {
_.bindAll(this, 'addTopic', 'addMessage');
topics.bind('add', this.addTopic);
// 定義消息列表池,每個topic有自己的message collection
// 這樣保證每個主題下得消息不沖突
this.message_pool = {};
this.message_list_div = document.getElementById('message_list');
},
addTopic: function(topic) {
var view = new TopicView({model: topic});
this.topic_list.append(view.render().el);
},
addMessage: function(message) {
var view = new MessageView({model: message});
this.message_list.append(view.render().el);
self.message_list.scrollTop(self.message_list_div.scrollHeight);
},
saveMessageEvent: function(evt) {
if (evt.keyCode == 13) {
this.saveMessage(evt);
}
},
saveMessage: function(evt) {
var comment_box = $('#comment')
var content = comment_box.val();
if (content == '') {
alert('內容不能為空');
return false;
}
var topic_id = comment_box.attr('topic_id');
var message = new Message({
content: content,
topic_id: topic_id,
});
var messages = this.message_pool[topic_id];
message.save(null, {
success: function(model, response, options){
comment_box.val('');
// 重新獲取,看服務器端是否有更新
// 比較丑陋的更新機制
messages.fetch({
data: {topic_id: topic_id},
success: function(){
self.message_list.scrollTop(self.message_list_div.scrollHeight);
messages.add(response);
},
});
},
});
},
saveTopic: function(evt) {
var topic_title = $('#topic_title');
if (topic_title.val() == '') {
alert('主題不能為空!');
return false
}
var topic = new Topic({
title: topic_title.val(),
});
self = this;
topic.save(null, {
success: function(model, response, options){
topics.add(response);
topic_title.val('');
},
});
},
showTopic: function(){
topics.fetch();
this.topic_section.show();
this.message_section.hide();
this.message_list.html('');
this.goOut()
},
initMessage: function(topic_id) {
var messages = new Messages;
messages.bind('add', this.addMessage);
this.message_pool[topic_id] = messages;
},
showMessage: function(topic_id) {
this.initMessage(topic_id);
this.message_section.show();
this.topic_section.hide();
this.showMessageHead(topic_id);
$('#comment').attr('topic_id', topic_id);
var messages = this.message_pool[topic_id];
messages.fetch({
data: {topic_id: topic_id},
success: function(resp) {
self.message_list.scrollTop(self.message_list_div.scrollHeight)
}
});
},
showMessageHead: function(topic_id) {
var topic = new Topic({id: topic_id});
self = this;
topic.fetch({
success: function(resp, model, options){
self.message_head.html(model.title);
}
});
},
});
return AppView;
});
~~~
不同于上面三個基礎模塊,這個模塊只需要對外暴露AppView即可(貌似也就只有這一個東西)。
_AppRouter模塊_
下面就是用來做路由的AppRouter模塊,這里只是定義了AppRouter,沒有做初始化的操作,初始化的操作我們放到app.js這個模塊中,app.js也是項目運行的主模塊。
~~~
// approuter.js
define(function(require, exports, module) {
var $ = require('jquery');
var _ = require('underscore');
var Backbone = require('backbone');
var AppView = require('appview');
var UserModule = require('user');
var LoginView = UserModule.LoginView;
var UserView = UserModule.UserView;
var AppRouter = Backbone.Router.extend({
routes: {
"login": "login",
"index": "index",
"topic/:id" : "topic",
},
initialize: function(g_user){
// 設置全局用戶
this.g_user = g_user;
// 初始化項目, 顯示首頁
this.appView = new AppView();
this.loginView = new LoginView(this);
this.userView = new UserView();
this.indexFlag = false;
},
login: function(){
this.loginView.show();
},
index: function(){
if (this.g_user && this.g_user.id != undefined) {
this.appView.showTopic();
this.userView.show(this.g_user.username);
this.loginView.hide();
this.indexFlag = true; // 標志已經到達主頁了
}
},
topic: function(topic_id) {
if (this.g_user && this.g_user.id != undefined) {
this.appView.showMessage(topic_id);
this.userView.show(this.g_user.username);
this.loginView.hide();
this.indexFlag = true; // 標志已經到達主頁了
}
},
});
return AppRouter;
});
~~~
同樣,對外暴露AppRouter,主要供app.js這個主模塊使用。
_app模塊_
最后,讓我們來看下所有js的入口:
~~~
// app.js
define(function(require) {
var $ = require('jquery');
var _ = require('underscore');
var Backbone = require('backbone');
var AppRouter = require('approuter');
var UserModule = require('user');
var User = UserModule.User;
var g_user = new User();
var appRouter = new AppRouter(g_user);
g_user.fetch({
success: function(model, resp, options){
g_user = resp;
Backbone.history.start({pustState: true});
if(g_user === null || g_user.id === undefined) {
// 跳轉到登錄頁面
appRouter.navigate('login', {trigger: true});
} else if (appRouter.indexFlag == false){
// 跳轉到首頁
appRouter.navigate('index', {trigger: true});
}
},
}); // 獲取當前用戶
});
~~~
這個模塊中,我們通過require引入Approuter,引入User模塊。需要注意的是,不同于之前一個文件中所有的模塊可以共享對象的實例(如:g_user, appRouter),這里需要通過參數傳遞的方式把這個各個模塊都需要的對象傳遞過去。同時AppRouter和User也是整個頁面生存期的唯一實例。因此我們把User對象作為AppRouter的一個屬性。在上面的AppRouter定義中,我們又吧AppRouter的實例傳遞到了LoginView中,因為LoginView需要對url進行變換。
_總結_
好了,我們總結下模塊拆分的結構,還是來看下項目中js的文件結構:
~~~
└── js
├── app.js
├── approuter.js
├── appview.js
├── backbone.js
├── jquery.js
├── json2.js
├── message.js
├── require.js
├── topic.js
├── underscore.js
└── user.js
~~~
## 15.4 用require.js加載
上面定義了項目需要的所有模塊,知道了app.js相當于程序的入口,那么要怎么在頁面開始呢?
就像一開始介紹的require.js的用法一樣,只需要在index.html中加入一個js引用,和一段定義即可:
~~~
// index.html
<script data-main="/static/js/app.js" src="/static/js/require.js"></script>
<script>
require.config({
baseUrl: '/static',
shim: {
underscore: {
exports: '_'
},
},
paths: {
"jquery": "js/jquery",
"underscore": "js/underscore",
"backbone": "js/backbone",
"user": "js/user",
"message": "js/message",
"topic": "js/topic",
"appview": "js/appview",
"approuter": "js/approuter",
"app": "js/app",
}
});
</script>
~~~
需要解釋的是上面的那個?`shim`?的定義。因為underscore并不沒有對AMD這樣的模塊規范進行處理,因此需要進行模塊化處理,有兩種方式:1.修改underscore的源碼,加上?`define(function(require, exports, module)`?這樣的定義;2\. 采用requirejs提供的shim來進行處理。
## 15.5 捋捋結構
上面把文件拆分了一下,但是沒有把template從頁面提取出來。有興趣的可以自己嘗試下。最后我們來整理一下項目的結構。
[](https://github.com/the5fire/backbonejs-learning-note/blob/master/images/wechat-arch.png)
具體的代碼也可以到?[wechat](https://github.com/the5fire/wechat)?中去看,在requirejs這個分支,代碼中添加了socketio,但是對上面的介紹沒有影響。
- 關于
- 前言
- 第一章 Hello Backbonejs
- 第二章 Backbonejs中的Model實踐
- 第三章 Backbonejs中的Collections實踐
- 第四章 Backbonejs中的Router實踐
- 第五章 Backbonejs中的View實踐
- 第六章 實戰演練:todos分析(一)
- 第七章 實戰演練:todos分析(二)View的應用
- 第八章 實戰演練:todos分析(三)總結
- 第九章 后端環境搭建:web.py的使用
- 第十章 實戰演練:擴展todos到Server端(backbonejs+webpy)
- 第十一章 前后端實戰演練:Web聊天室-功能分析
- 第十二章 前后端實戰演練:Web聊天室-詳細設計
- 第十三章 前后端實戰演練:Web聊天室-服務器端開發
- 第十四章 前后端實戰演練:Web聊天室-前端開發
- 第十五章 引入requirejs
- 第十六章 補充異常處理
- 第十七章 定制Backbonejs
- 第十八章 再次總結的說
- Backbonejs相關資源