(如果对SPA概念不清楚的同学可以先自行了解相关概念)

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

个人认为单页面应用的优势相当明显:

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

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

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

概览

下图展示了这种模式的整个前后端及各自的主要组成:

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

前端架构

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

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

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

components

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

我们可以把博客评论做为一个组件,这个组件有自己的结构(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. 鼠标放到评论或者回复的用户头像上可以显示用户名片。

我们构建两个组件,reply和user-info-card。因为每个comment都要有自己的回复列表,所以comment组件是依赖于reply组件的,comment和reply组件是嵌套关系。

而user-info-card可以出现在comment或者reply当中,并且为了以后让user-info-card复用性更强,它应该不属于任何一个组件,它和其他组件是组合关系。所以我们就得到一个简单的componenets 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端的页面的请求,而且处理提供移动端、桌面端的请求或者作为第三方开放接口来使用。大大提高后端处理请求的灵活性。

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

  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

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

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

  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测试这一块可以单独进行讨论研究),提高产品的可靠性、稳定性。

一种SPA(单页面应用)架构的更多相关文章

  1. AngularJs(SPA)单页面SEO以及百度统计应用(上)

    只有两种人最具有吸引力,一种是无所不知的人,一种是一无所知的人 问:学生问追一个女孩总是追不上怎么办?回答:女孩不是追来的,是吸引来的,你追的过程是吸引女孩的过程,如果女孩没有看上你,再追都是没有用的 ...

  2. 通过Blazor使用C#开发SPA单页面应用程序(3)

    今天我们来看看Blazor开发的一些基本知识. 一.Blazor组件结构 Blazor中组件的基本结构可以分为3个部分,如下所示: //Counter.razor //Directives secti ...

  3. Java快速开发平台强大的代码生成器,JEECG 3.7.5 VUE+ElementUI SPA单页面应用版本发布

    JEECG 3.7.5 VUE+ElementUI SPA单页面应用版本发布 此版本为Vue+ElementUI SPA单页面应用版本,提供新一代风格代码生成器模板,采用Vue技术,提供两套精美模板E ...

  4. 通过Blazor使用C#开发SPA单页面应用程序(1)

    2019年9月23——25日 .NET Core 3.0即将在.NET Conf上发布! .NET Core的发布及成熟重燃了.net程序员的热情和希望,一些.net大咖也在积极的为推动.NET Co ...

  5. 快速了解SPA单页面应用

    简要 SPA单页网页应用程序这个概念并不算新,早在2003年就已经有在讨论这个概念了,不过,单页应用这个词是到了2005年才有人提出使用,SPA的概念就和它的名字一样显而易懂,就是整个网站不再像传统的 ...

  6. SPA 单页面应用程序。

    看到这个问题,先说下自己的理解到的程度,再去参考做修正,争取这一次弄懂搞清楚 自己的理解: 单页面应用程序,解决浏览器获取数据刷新页面的尴尬,通过ajax请求获取数据达到异步更新视图的按钮,原理的实现 ...

  7. SPA单页面应用

    什么是单页应用 单页Web应用,就是只有一张Web页面的应用.浏览器一开始会加载必需的HTML.CSS和JavaScript,之后所有的操作都在这张页面完成,这一切都由JavaScript来控制.因此 ...

  8. SPA(单页面web应用程序)

    单页web应用(single page web application,SPA),就是只有一张web页面的应用,是加载单个HTML页面并在用户与应用程序交互时动态更新该页面的web应用程序. 浏览器一 ...

  9. SPA单页面应用和MPA多页面应用(转)

    原文:https://www.jianshu.com/p/a02eb15d2d70 单页面应用 第一次进入页面时会请求一个html文件,刷新清除一下,切换到其他组件,此时路径也相应变化,但是并没有新的 ...

  10. vue单页面项目架构方案

    这里的架构方案是基于vue-cli2生成的项目应用程序产生的,是对项目应用程序或者项目模板的一些方便开发和维护的封装.针对单页面的解决方案. 主要有四个方面: 一,不同环境下的分别打包 主要是测试环境 ...

随机推荐

  1. Scala中List(Map1,Map2,Map3 ....) 转成一个Map

    这个问题研究好久...头大,不记得有fold用法了. fold函数:折叠,提供一个输入参数作为初始值,然后大括号中应用自定义fun函数并返回值. list.fold(Map()){(x,y)=> ...

  2. 基于zookeeper、连接池、Failover/LoadBalance等改造Thrift 服务化

    对于Thrift服务化的改造,主要是客户端,可以从如下几个方面进行: 1.服务端的服务注册,客户端自动发现,无需手工修改配置,这里我们使用zookeeper,但由于zookeeper本身提供的客户端使 ...

  3. Android不同版本下Notification创建方法

    项目环境 Project Build Target:Android 6.0 问题: 使用 new Notification(int icon, CharSequence tickerText, lon ...

  4. malloc 返回值的类型是 void *

    malloc 返回值的类型是 void *,所以在调用 malloc 时要显式地进行类型转换,将 void * 转换成所需要的指针类型. #include <iostream> using ...

  5. php -- php的事务处理

    MYSQL的事务处理主要有两种方法. 1.用begin,rollback,commit来实现 begin 开始一个事务 rollback 事务回滚 commit 事务确认 2.直接用set来改变mys ...

  6. Spring MVC返回json格式

    在使用SpringMVC框架直接返回json数据给client时,不同的版本号有差异. 以下介绍两种类型的版本号怎样配置. 注意:这两种方法均已验证通过. 1.Spring3.1.x版本号 1.1 d ...

  7. react-native新导航组件react-navigation详解

    http://blog.csdn.net/sinat_17775997/article/details/70176688

  8. JZOJ.5289【NOIP2017模拟8.17】偷笑

    Description berber走进机房,边敲门边喊:“我是哔哔”CRAZY转过头:“我警告你,哔哔刚刚来过!”“呵呵呵呵……”这时,哔哔站了起来,环顾四周:“你们笑什么?……”巧了,发出笑声的人 ...

  9. 安装PHP扩展-----phpredis

    一.redis介绍 redis是当前比较热门的NOSQL系统之一,它是一个key-value存储系统.和Memcached类似,但很大程度补偿了 memcached的不足,它支持存储的value类型相 ...

  10. linux如何查看端口是否被占用?

    转自:https://www.cnblogs.com/hindy/p/7249234.html LINUX中如何查看某个端口是否被占用 之前查询端口是否被占用一直搞不明白,问了好多人,终于搞懂了,现在 ...