在上一章的?[服務器端開發](https://github.com/the5fire/backbonejs-learning-note/blob/master/chapters/13-web-chatroom-base-on-backbonejs-3.rst)?中我們定義了模型,實現了幾個實體增刪改查得功能,也提供了前端訪問數據的接口。但在前端的實現過程中又對接口進行了調整,以更符合前端的使用。在真實的開發中也是如此,定義的接口合不合適只有在開發時才知道。
目前代碼并沒有進行模塊的劃分,在單js文件(chat.js)中實現了所有邏輯。下一步會進行通過seajs或者requirejs來進行模塊管理。
關于前端樣式的設計和開發并不在這個系列的計劃中,因此就不多做介紹了,只是基于semantic進行了簡單的設計,有興趣的可以自己去看:?[wechat項目](https://github.com/the5fire/wechat)?。
## 14.1 前端文件結構
前端的結構和前面的項目結構一樣,只是添加了chat.js和自定義樣式的chat.css文件,我們所有的代碼都在這個文件中編寫。
~~~
├── static
│?? ├── css
│?? │?? ├── body.css
│?? │?? ├── chat.css
│?? │?? └── semantic.min.css
│?? ├── fonts
│?? │?? ├── basic.icons.svg
│?? │?? ├── basic.icons.woff
│?? │?? ├── icons.svg
│?? │?? └── icons.woff
│?? ├── img
│?? │?? └── bg.jpg
│?? └── js
│?? ├── backbone.js
│?? ├── chat.js
│?? ├── jquery.js
│?? ├── json2.js
│?? └── underscore.js
├── templates
│?? └── index.html
~~~
## 14.2 Model和Collection定義
我們還是先來定義Model的實現,前端的Model應該和后端的Model定義一樣,不然數據傳遞就會有問題。因為在后端已經明確定義了Model有哪些屬性,這里的定義就簡單多了。當然這也是動態語言的優勢——動態的添加屬性。
我們要定義三個Model和兩個Collection,因為User這個對象在前端只會存在一份,不需要定義集合。來看具體實現:
~~~
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,
});
~~~
我們之定義了基本的屬性,這些屬性保證了我們可以可以直接通過collection或者model獲取到后端的數據。
## 14.3 視圖和模板的定義
定義了基本的Model之后,就相當于是有了數據的獲取方式,下一步就是如何顯示這些數據了。因此就需要用Backbonejs中的view和template來定義我們的具體顯示了。
首先來定義view:
~~~
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();
},
});
~~~
根據定義的三個Model,定義了把數據渲染到模板的方式,對應的模塊是什么樣的呢,我們來看下:
~~~
<script type="text/template" id="topic-template">
<a href="#topic/<%= id %>">
<div class="column">
<div class="ui segment">
<h3><%= title %></h3>
<p>
創建者:<%= owner_name %>
</p>
<p>
創建時間:<%= created_time %>
</p>
</div>
</div>
</a>
</script>
<script type="text/template" id="message-template">
<div class="content <% if(is_mine) { %> right <% } %>" data="<%= id %>">
<a class="author"><%= user_name %></a>
<br/>
<div class="metadata">
<span class="date"><%= created_time %></span>
</div>
<div class="text" style="min-width:55px">
<div class="ui pointing label large <% if(is_mine) { %> right <% } %>">
<p><%= content %></p>
</div>
</div>
</div>
</script>
~~~
這里并沒有定義user的模板,因為目前對user只是做了簡單的展現,即僅在頂部欄上加了一個用戶名,通過:?`user_name`這個Dom節點的id添加數據。
到目前已經介紹了所有的基礎數據:從model到collection,到用來顯示數據的view,再到定義的頁面模板template。每部分的數據都可以單獨的從后臺獲取,并且渲染。好了,材料都準備好了就差什么了?當然是流程。不過還有一個東西得先說一下,這些數據被塞到頁面之后到底長成什么樣還不知道。因此得先來看下頁面結構。
下面先來看看上面的那些數據最終要被填充到頁面的什么部位,然后再來說流程的事。
## 14.4 頁面結構
這里還是從代碼上說事,但是最終效果圖已經在?[wechat](https://github.com/the5fire/wechat)?的readme中貼出來了,你可以跳過去看看長相先。
欣賞完外表,來看看內部的骨架,這里只貼主要代碼。
_頂部的固定欄:_
~~~
<!-- Top Bar -->
<div class="ui fixed transparent inverted main menu">
<div class="container">
<div class="title item">
<b>We Chat</b> 在線聊天系統
</div>
<div class="right menu">
<div class="title item">
Backbonejs交流群:308466740
</div>
</div>
<div id="user_info" class="right menu hide">
<div class="title item">
<i class="icon user"></i>
<label id="username">the5fire</lable>
</div>
<a class="popup icon github item" href="/logout" title="退出登錄">
退出登錄
</a>
</div>
</div>
</div>
~~~
_登陸注冊的代碼,純靜態代碼:_
~~~
<div id="wrapper" style="display: block; z-index: 998;">
<div class="container">
<div id="login" class="ui two column relaxed grid">
<div class="column">
<div class="ui fluid form segment">
<h3 class="ui header">登錄</h3>
<div class="field">
<label>用戶名</label>
<input id="login_username" placeholder="用戶名" type="text">
</div>
<div class="field">
<label>密碼</label>
<input id="login_pwd" type="password">
</div>
<div class="ui blue login_submit button">登錄</div>
</div>
</div>
<div class="column">
<div class="ui fluid form segment">
<h3 class="ui header">注冊</h3>
<div class="field">
<label>用戶名</label>
<input id="reg_username" placeholder="用戶名" type="text">
</div>
<div class="field">
<label>密碼</label>
<input id="reg_pwd" type="password">
</div>
<div class="field">
<label>重復密碼</label>
<input id="reg_pwd_repeat" type="password">
</div>
<div class="inline field">
<div class="ui checkbox">
<input type="checkbox" id="terms">
<label for="terms">我同意the5fire's WeChat網的服務條款。</label>
</div>
</div>
<div class="ui blue registe_submit button">注冊</div>
</div>
</div>
</div>
</div>
</div>
~~~
用來展示話題和消息的內容區域:
~~~
<!-- Content -->
<div id="main" class="main container">
<!-- Topic List -->
<div id="topic_section">
<div id="topic_list" class="ui three column grid">
<!-- 這里放topic列表 -->
</div>
<div id="topic_form" class="ui error form segment">
<div class="two fields">
<div class="field">
<label>新建Topic</label>
<input id="topic_title" placeholder="topic" type="text">
</div>
</div>
<div class="ui blue submit_topic button">Add</div>
</div>
</div>
<!-- Message -->
<div id="message_section" class="ui column grid hide" style="display:none">
<div class="column">
<div class="circular ui button"><a href="#index">返回列表</a></div>
<div class="ui piled blue segment">
<h2 class="ui header">
#<i id="message_head"></i># <!-- 用來放topic name -->
</h2>
<div id="message_list" class="ui comments">
<!-- comments 列表 -->
</div>
<div class="ui reply form">
<div class="field">
<input type="text" id="comment"/>
</div>
<div id="submit" data="" class="ui fluid blue labeled submit icon button">
<i class="icon edit"></i> 我也來說一句!
</div>
</div>
</div>
</div>
</div>
</div>
~~~
頁面布局大概介紹了一下,如果你熟悉html,并且也看了我上面鏈接里給的最終效果, 上面的這些理解上面的這些代碼應該很Easy了。如果不熟悉的也沒問題,只要關注于我寫了注釋的地方就行了,這些地方就是上面我們定義的那些模板被渲染好之后的歸宿。
## 14.5 view管理和router管理
上面占了點篇幅介紹了頁面的布局,以便對我們數據最終的處理有一個感覺。
有了數據,也有了最后數據的去處,最后當然要說流程了。所謂的流程就是說我要怎么把Model渲染好的模板給塞到對于的頁面div節點中,我要怎么來控制不同Model的展示。畢竟是SPA(單頁應用), 也只有這一個頁面來供數據的展示。因此需要在一個頁面上切換的展示不同的視圖。
這里我們是通過Backbone的Route和View來做。Route用來做路由分發(也就是URI的匹配,比如:#index匹配到首頁)。另外不同于上面用來把Model數據傳到Template中的View,這里的View是用來管理其他具體View和Collection的,可以比喻為管家View,就是用來控制這個視圖什么時候顯示,那個Collection的數據什么時候獲取。
但是,需要注意,這個View需要被Route來控制,也就是通過路由控制(根據URI),因此View在具備上述功能的情況下也要提供接口(方法)給Route。
上面介紹了一堆,仿佛說不太清晰,沒關系,Talk is cheap, Show you my code。
先來看View管家-AppView, 主要功能就是獲取Topic和Message的數據到Collection中,調用Model對應的View把數據填到模板中,然后把最終拼好的數據放到上面介紹的頁面對應div中。
~~~
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);
}
});
},
});
~~~
上面是所有數據視圖的展示的邏輯控制部分,雖然代碼很多,但沒有復雜邏輯,很直觀。這里只是Topic和Message的展示。但是這些所有的數據都是需要用戶登錄之后才能看到的,那么用戶登錄和注冊部分的邏輯在哪呢?在上面的頁面布局部分已經展示了登錄注冊的頁面,下面展示下具體邏輯。
登錄注冊-LoginView:
~~~
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});
}
});
},
});
~~~
這里的View的主要功能是:注冊(保存user數據到后臺),登錄(發送用戶請求到后臺,成功則跳到首頁),事件監聽和處理。很基礎的功能。
從上面兩部分我們知道了如何控制不同Model對應視圖的展示,也知道了如何處理用戶登錄。下面再來看些Route部分是如何把url匹配到對應的方法上的。
路由部分代碼-AppRouter:
~~~
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; // 標志已經到達主頁了
}
},
});
~~~
這里設定了三條路由:login,index,topic,分別對應這個登錄視圖(LoginView), 主題和Message的視圖(由AppView管理)。
在不同的路由中的邏輯大致一樣,就是根據當前的條件決定是否現實視圖。 比如index中的?`if (g_user && g_user.id != undefined) {`?就是判斷當前環境中是否有g_user這個對象(這個對象是用來存放已登錄用戶數據的,后面會介紹),根據這個對象判斷是否用戶已經登錄,進而決定是否現實首頁——topic列表頁。
## 14.6 啟動
當所有的邏輯都定義好之后,頁面加載完畢首先要做的就是啟動整個流程,怎么啟動呢?按照我們的項目結構:AppRouter管理AppView和LoginView,AppView管理TopicView和MessageView,因此,只需要啟動AppRouter即可。
啟動代碼如下:
~~~
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});
}
},
}); // 獲取當前用戶
~~~
就是這一小段代碼,程序可以正常運行了。這段代碼中的邏輯是:聲明一個全局的appRouter和g_user,然后獲取當前用戶(服務器端會通過session保存對應瀏覽器的信息), 之后根據獲取到得用戶狀態做進一步操作(到登錄頁面或是到首頁)。
這里需要注意的是,這段代碼只有在頁面加載(刷新或重新訪問)的時候才會執行。
好了,到此為止整個項目已經介紹完畢了,不知道你是否看懂,或者這么問,我是否把這個項目講明白了?
## 14.7 總結
這一篇看起篇幅很長,其實都是代碼。而這些代碼只有當你真正打算做這么個東西的時候才會主動去理解,因為那些走馬觀花的人會選擇性的忽略代碼。
最后還是補充一下整個流程,其實整個項目開始做的時候,項目的設計者就應該有一個具體的需求和用戶使用的場景。對于這個項目我自己設想的用戶使用流程:
用戶打開瀏覽器,看到登錄和注冊頁面——》輸入用戶名、密碼進行登錄(注冊)操作——》展示主題列表視圖,并顯示用戶名在頂部——》用戶創建并進入某一主題(顯示消息列表視圖)——》用戶發送消息,消息保存的同時獲取服務器端的消息到當前視圖。
另外一定要說的是,項目沒有進行太多優化和代碼的精簡,還有很多改進的地方。在我寫代碼的這些年中我始終堅信并踐行的一件事就是——獲取知識最好的方法就是實踐。因此如果你想掌握這個Backbone這個工具,最佳的方式是開始一個項目,并持續的做下去。或者參與一個項目,持續改善項目。
我在邊寫邊實踐中寫了?[WeChat](https://github.com/the5fire/wechat)?這個項目,并且已經部署上線,相信會是一個好的開始,因為我沒打算把它僅僅作為一個Demo來用。 本文涉及的所有代碼均在該項目的basic-version分支可以看到。
- 關于
- 前言
- 第一章 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相關資源