一种SPA(单页面应用)架构

未经容许,请勿转载。本文同时也发布在个人博客css


(若是对SPA概念不清楚的同窗能够先自行了解相关概念)html

平时喜欢作点小页面来玩玩,而且一直采用单页面应用(Single Page Application)的方式来进行开发。这种开发方式是在以前一年作的一个创业项目的经验和思考,一直想写篇博客来总结一下。前端

我的认为单页面应用的优点至关明显:git

  1. 先后端职责分离,架构清晰:前端进行交互逻辑,后端负责数据处理。
  2. 先后端单独开发、单独测试。
  3. 良好的交互体验,前端进行的是局部渲染。避免了没必要要的跳转和重复渲染。

固然,SPA也有它自身的缺点,例如不利于搜索引擎优化等等,这些问题也有其相应的解决方案。github

下面要介绍的这种方式能够说是一种模式或者工做流,和前端使用什么框架无关,也和后端使用什么语言、数据库无关。不能说是The Best Practice,我相信通过更多人的讨论和思考会有A Better Practice。:)ajax

概览

下图展现了这种模式的整个先后端及各自的主要组成:数据库

overview

看起来有点复杂,接下来会仔细地对上面每个部分进行解释。看完本文,就应该能理解上图中的各部件之间的交互流程。json

前端架构

把上图的前端部分单独抽出来进行研究:后端

simple-front

前端中大体分为四种类型的模块:api

  1. components:前端UI组件
  2. services:前端数据缓存和操做层
  3. databus:封装一系列Ajax操做,和后端进行数据交互的部件
  4. common/utils:以上组件的共用部件,可复用的函数、数据等

components

component指的是页面上的一个可复用UI交互单元,例如一个博客的评论功能:

comment

咱们能够把博客评论作为一个组件,这个组件有本身的结构(html),外观(css),交互逻辑(js),因此咱们能够单独作一个叫comment的component,由如下文件组成:

  1. comment.html
  2. comment.css
  3. comment.js

(每一个component能够想象成一个工程,甚至能够有本身的README、测试等)

components tree

一个component能够依赖另一个component,这时候它们是父子关系;component之间也能够互相组合,它们就是兄弟关系。最后的结果就相似DOM tree,component能够组成components tree。

例如,如今要给这个博客添加两个功能:

  1. 显示评论回复。
  2. 鼠标放到评论或者回复的用户头像上能够显示用户名片。

comments-replies-poputbox

咱们构建两个组件,reply和user-info-card。由于每一个comment都要有本身的回复列表,因此comment组件是依赖于reply组件的,comment和reply组件是嵌套关系。

而user-info-card能够出如今comment或者reply当中,而且为了之后让user-info-card复用性更强,它应该不属于任何一个组件,它和其余组件是组合关系。因此咱们就获得一个简单的componenets tree:

components-tree

components之间的通讯

怎么能够作到鼠标放到评论和回复的用户头像上显示名片呢?这其实牵涉到组件之间是如何进行通讯的问题。

最佳的方式就是使用事件机制,全部组件之间能够经过一个叫eventbus通用组件进行信息的交互。因此,要作到上述功能:

  1. user-info-card能够在eventbus监听一个user-info-card:show的事件。
  2. 而当鼠标放到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之间用事件进行通讯的优点在于:

  1. 组件之间没有强的依赖,组件之间被解耦。
  2. 组件之间能够单独开发、单独测试。数据和事件均可以简单的进行伪造进行测试(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端的页面的请求,并且处理提供移动端、桌面端的请求或者做为第三方开放接口来使用。大大提升后端处理请求的灵活性。

后端对比起前端的架构来讲会简单不少,可是这只是其中一种模式,对于不一样复杂程度的应用可能会作相应的调整。后端大概分为三层:

server

  1. CGI:设置不一样的路由规则,接受前端来的请求,处理数据,返回结果。
  2. business:这一层封装了对数据库的一些操做,business能够被CGI所调用。
  3. database:数据库,进行数据的持久化。

例如上面的comments的例子,CGI能够接收到前端发送的请求:

var commentsBusiness = require("./businesses/comments")
app.get("/comments", function(req, res) {
    // 此处调用comments的business数据库操做
    commentsBusiness.getAllComments(function(comments) {
        // 返回数据结果
        res.json(comments)
    })
})

后端的API能够采用更规范的RESTful API的方式,而RESTful不在本文的讨论范围内。有兴趣的能够参考Best Practices for Designing a Pragmatic RESTful API

先后端的架构都基本清晰了,咱们来看看文章开头的图:

overview

看着图来,咱们总结一下整个先后端的交互流程:

  1. 前端向服务端请求第一个页面,后端渲染返回。
  2. 前端加载各个component,components从services拿数据,services经过databus发送Ajax请求向后端取数据。
  3. 后端的CGI接收到前端databus发送过来的请求,处理数据,调用business操做数据库,返回结果。
  4. 前端接收到后端返回的结果,把数据缓存到service,component拿到数据进行前端组件的渲染、显示。

工做流

一个好的工做流可让开发事半功倍。上面的这种单页面应用也有其相应的一种开发工做流,固然这种工做流也适合非单页面应用:

  1. 进行产品功能、原型设计。
  2. 后端数据库设计。
  3. 根据产品肯定先后端的API(or RESTful API),以文档方式纪录。
  4. 先后端就能够针对API文档同时进行开发。
  5. 先后端最后进行链接测试。

先后端分离开发。建议均可以采用TDD(测试驱动开发)的方式来单独测试、单独开发(关于Web APP测试这一块能够单独进行讨论研究),提升产品的可靠性、稳定性。

(完)

相关文章
相关标签/搜索