一種SPA(單頁面應用)架構
(如果對SPA概念不清楚的同學可以先自行了解相關概念)
平時喜歡做點小頁面來玩玩,並且一直採用單頁面應用(Single Page Application)的方式來進行開發。這種開發方式是在之前一年做的一個創業專案的經驗和思考,一直想寫篇部落格來總結一下。
個人認為單頁面應用的優勢相當明顯:
- 前後端職責分離,架構清晰:前端進行互動邏輯,後端負責資料處理。
- 前後端單獨開發、單獨測試。
- 良好的互動體驗,前端進行的是區域性渲染。避免了不必要的跳轉和重複渲染。
當然,SPA也有它自身的缺點,例如不利於搜尋引擎優化等等,這些問題也有其相應的解決方案。
下面要介紹的這種方式可以說是一種模式或者工作流,和前端使用什麼框架無關,也和後端使用什麼語言、資料庫無關。不能說是The Best Practice,我相信經過更多人的討論和思考會有A Better Practice。:)
概覽
下圖展示了這種模式的整個前後端及各自的主要組成:
看起來有點複雜,接下來會仔細地對上面每一個部分進行解釋。看完本文,就應該能理解上圖中的各部件之間的互動流程。
前端架構
把上圖的前端部分單獨抽出來進行研究:
前端中大致分為四種類型的模組:
- components:前端UI元件
- services:前端資料快取和操作層
- databus:封裝一系列Ajax操作,和後端進行資料互動的部件
- common/utils:以上元件的共用部件,可複用的函式、資料等
components
component指的是頁面上的一個可複用UI互動單元,例如一個部落格的評論功能:
我們可以把部落格評論做為一個元件,這個元件有自己的結構(html),外觀(css),互動邏輯(js),所以我們可以單獨做一個叫comment的component,由以下檔案組成:
- comment.html
- comment.css
- comment.js
(每個component可以想象成一個工程,甚至可以有自己的README、測試等)
components tree
一個component可以依賴另外一個component,這時候它們是父子關係;component之間也可以互相組合,它們就是兄弟關係。最後的結果就類似DOM tree,component可以組成components tree。
例如,現在要給這個部落格新增兩個功能:
- 顯示評論回覆。
- 滑鼠放到評論或者回復的使用者頭像上可以顯示使用者名稱片。
我們構建兩個元件,reply和user-info-card。因為每個comment都要有自己的回覆列表,所以comment元件是依賴於reply元件的,comment和reply元件是巢狀關係。
而user-info-card可以出現在comment或者reply當中,並且為了以後讓user-info-card複用性更強,它應該不屬於任何一個元件,它和其他元件是組合關係。所以我們就得到一個簡單的componenets tree:
components之間的通訊
怎麼可以做到滑鼠放到評論和回覆的使用者頭像上顯示名片呢?這其實牽涉到元件之間是如何進行通訊的問題。
最佳的方式就是使用事件機制,所有元件之間可以通過一個叫eventbus通用元件進行資訊的互動。所以,要做到上述功能:
- user-info-card可以在eventbus監聽一個
user-info-card:show
的事件。 - 而當滑鼠放到comment和reply元件的頭像上的時候,元件可以使用eventbus觸發
user-info-card:show
事件。
user-info-card:
var eventbus = require("eventbus")
eventbus.on("user-info-card:show", function(user) {
// 顯示使用者名稱片
})
comment or reply:
var eventbus = require("eventbus")
$avatar.on("mouseover", function(event) {
eventbus.emit("user-info-card:show", userData)
})
components之間用事件進行通訊的優勢在於:
- 元件之間沒有強的依賴,元件之間被解耦。
- 元件之間可以單獨開發、單獨測試。資料和事件都可以簡單的進行偽造進行測試(mocking)。
總結:component之間有巢狀和組合的關係,構成components tree;component之間通過事件進行資訊、資料的交換。
services
component的渲染和顯示依賴於資料(model)。例如上面的評論,就會有一個評論列表的model。
comments: [
{user:.., content:.., createTime: ..},
{user:.., content:.., createTime: ..},
{user:.., content:.., createTime: ..}
]
每個評論的component會對應一個comment(comments陣列中的物件)進行渲染,渲染完以後就會正確地顯示在頁面上。
因為可能在其他component中也會需要用到這些資料,所以comment component不會自己直接儲存這些comment model。這些model都會儲存在service當中,而component會從service拿取資料。components和services之間是多對多的關係:一個component可能會從不同的services中拿取資料,而一個service可能為多個components提供資料。
services除了用於快取資料以外,還提供一系列對資料的一些操作介面。可以提供給components進行操作。這樣的好處在於保持了資料的一直性,假如你使用的是MVVM框架進行component的開發,對資料的操作還可以直接對多個檢視產生資料繫結,當services中的資料變化了,多個components的檢視也會相應地得到更新。
總結:services是對前端資料(也就是model)的快取和操作。
databus
而services中快取的資料是從哪裡來的呢?當然也許想到的第一個方案是在services中直接傳送Ajax請求去伺服器中拉去資料。而這裡建議不直接這樣做,而是把各種和後端的API進行互動的介面封裝到一個叫databus的模組當中,這裡的databus相當於是“對後端資料進行原子操作的集合”。
如上面的comment service需要從後端進行拉取資料,它會這樣做:
var databus = require("databus")
var comments = null
databus.getAllComments(function(cmts) { // 呼叫databus方法進行資料拉取
comments = cmts
})
而databus中則封裝了一層Ajax
databus.getAllCommetns = function(callback) {
utils.ajax({
url: "/comments",
method: "GET",
success: callback
})
}
這樣做是因為,不同的services之間可能會用到同樣的介面對後端進行操作,把操作封裝起來可以提高介面的複用性。注意,如果databus中的某些操作不涉及到servcies的資料,這操作也可以被components所呼叫(例如退出、登入等)。
總結:databus封裝了提供給services和component和後端API進行互動的介面。
common/utils
這兩個模組都可以被其他元件所依賴。
common,故名思議,元件之間的共用資料和一些程式引數可以快取在這裡。
utils,封裝了一些可複用的函式,例如ajax等。
eventbus
所有元件(特別是components之間)的通過事件機制進行資料、訊息通訊的介面。可以簡單地使用EventEmitter這個庫來實現。
後端架構
傳統的網頁頁面一般都是由後端進行頁面的渲染,而在我們的架構當中,後端只渲染一個頁面,其後,後端只是相當於一個Web Service,前端使用Ajax呼叫其介面進行資料的調取和操作,使用資料進行頁面的渲染。
這樣的好處就是,後端不僅僅能處理Web端的頁面的請求,而且處理提供移動端、桌面端的請求或者作為第三方開放介面來使用。大大提高後端處理請求的靈活性。
後端對比起前端的架構來說會簡單很多,但是這只是其中一種模式,對於不同複雜程度的應用可能會做相應的調整。後端大概分為三層:
- CGI:設定不同的路由規則,接受前端來的請求,處理資料,返回結果。
- business:這一層封裝了對資料庫的一些操作,business可以被CGI所呼叫。
- database:資料庫,進行資料的持久化。
例如上面的comments的例子,CGI可以接收到前端傳送的請求:
var commentsBusiness = require("./businesses/comments")
app.get("/comments", function(req, res) {
// 此處呼叫comments的business資料庫操作
commentsBusiness.getAllComments(function(comments) {
// 返回資料結果
res.json(comments)
})
})
前後端的架構都基本清晰了,我們來看看文章開頭的圖:
看著圖來,我們總結一下整個前後端的互動流程:
- 前端向服務端請求第一個頁面,後端渲染返回。
- 前端載入各個component,components從services拿資料,services通過databus傳送Ajax請求向後端取資料。
- 後端的CGI接收到前端databus傳送過來的請求,處理資料,呼叫business操作資料庫,返回結果。
- 前端接收到後端返回的結果,把資料快取到service,component拿到資料進行前端元件的渲染、顯示。
工作流
一個好的工作流可以讓開發事半功倍。上面的這種單頁面應用也有其相應的一種開發工作流,當然這種工作流也適合非單頁面應用:
- 進行產品功能、原型設計。
- 後端資料庫設計。
- 根據產品確定前後端的API(or RESTful API),以文件方式紀錄。
- 前後端就可以針對API文件同時進行開發。
- 前後端最後進行連線測試。
前後端分離開發。建議都可以採用TDD(測試驅動開發)的方式來單獨測試、單獨開發(關於Web APP測試這一塊可以單獨進行討論研究),提高產品的可靠性、穩定性。