可扩展的web单页应用程序架构
可扩展的web单页应用程序架构
本文转载自:众成翻译
译者:杨小福
链接:http://www.zcfy.cc/article/1319
原文:http://blog.mgechev.com/2016/04/10/scalable-javascript-single-page-app-angular2-application-architecture/
可扩展的web单页应用架构
为确保你能够理解本篇文章的内容,你需要掌握面向对象编程和函数式编程。我也极力的推荐你先去了解和学习redux的设计思想。
几个月之前我开始用单页应用(spa)的方式的方式编写一个动态业务需求的项目。和大多数的单页应用一样,随着业务逻辑和状态增多使得我们的应用日益庞大、臃肿。
需求说明
这是我一个创业项目的核心产品,因为还处于早起发展阶段以及商业竞争等因素,这个产品的业务变化是相当大的。
可扩展的通信层
我们具有相对稳定的业务领域,然而还是会有其他的因素影响着产品的状态,我们具有如下的通信需求:
- 用户
- RESTful API
在此基础上可能会有(或没有)如下的:;
- 与现有用户建立 P2P 链接的相关成员
- 与服务器进行实时的通讯
为支持不同的通信协议(HTTP,WebSocket,UDP[webRTC])我们需要不同格式的数据:
- HTTP/WebSocket采用JSON的通信格式
- JSON-RPC格式的WebSocket通信
- BERT or BERT-RTC格式的WebRTC或WebSocket通信
BERT通信协议非常适合P2P通信方式,尤其对于二进制数据的传输,比如图片以及不适合文本表示的数据。
为实现所有服务之间的通信,RxJS看起来是一个很不错的选择,通过它可以方便的管理各种类型的异步事件。
Given all the services we need to communicate with, RxJS seems like a perfect fit for organization of all the asynchronous events that the application needs to handle. We can multiplex several data streams over the same communication channel using hot-observers and declaratively filter, transform, process them, etc.
可预测的状态管理
在上面所列举的通信因素中很多都是会变化的。变化最多的就是用户、实时推送服务以及通过WebRTC通信的其他成员。当我们需要存储不同版本的store以及数据的时候,可预测的状态管理就显得非常重要。
有许多的架构模式可以帮助我们实现可预测的状态管理。当前最为流行的当属redux了。为了更好的类型安全以及工具,我们决定使用 TypeScript。
也许有人会争论说相比于TypeScript这样的语言,使用纯函数式的语言能够帮助我们降低副作用的影响。我对他们的观点表示赞同,同时我自己也是Elm和ClojureScript这样的函数式语言的粉丝。然而从程序的健壮性考虑,我们团队选择了 TypeScript。
要满足所有的开发者的需要是很难找到一种合适的技术方案的。我们需要牺牲部分人员的诉求而满足大部分人的要求,如同我提出了尝试使用Elm和ClojureScript的需求一样。
对我们来说,在无副作用和健壮性上来考虑最好的解决方案就是redux + TypeScript。Redux可以帮助我们实现可预测的数据状态管理,TypeScript则可帮助我们实现类型的检查以及更容易重构。
模块设计
正如之前提到的,团队会逐渐变得庞大。团队成员在经验方面也会有所不同。这也意味着不同层级经验的开发者需要合作开发同一个项目。最完美的实现就是能让初级的开发者最大化的发挥作用。为了实现这个目的我们将代码实现了比较高程度的抽象而看起来就如简单的MVC模式一般。
下面的图表显示了我们当前阶段核心模块的架构情况:
最上层是视图组件层,即用户直接交互的层,比如对话框、表单等。
facade层是视图层和底下各种服务的通信中间层,主要的目的是用来触发action操作并调用reducers以及异步服务的action调用。图表中的reducers和state即等同于redux中的reducers、state。
为了方便,我们在这里把facades称为models.例如,如果我们要开发一个游戏,那么我们的游戏视图组件GameComponent
就会通过GameModel
数据层来与store以及异步服务接口的通信。
facades的另外一个核心职能就是将异步服务接口调用转变成相应的actions,并和相应的reducers连接。这样我们就可以通过触发相应的action来调用异步接口并管理返回的数据。我们可以将异步接口用来扩展服务的远程代理服务 。它们将相应的action操作和远程命令对应起来。那为什么不直接调用远程服务而要通过异步调用的方式呢?那是因为通过异步接口的方式可以实现对WebRTC,WebSocket以及IndexDB的统一调用。
如果我们的异步服务对应于一个远程的RESTful API,那么会通过对应的HTTP网关来连接。一旦异步服务接收到一个action调用,他就会将这个action转换成对应的RESTful命令并通过网关传输过去。
需要注意的是,数据模型层(facade)不应该与具体的通信协议耦合,即使是异步服务。这意味着facade应该更具具体的使用场景来决定如何调用异步服务。
上下文依赖的实现
facades的上下文是由其视图部分决定的。例如,假设我们要开发一款可以多人和单人使用的游戏。对于单人的情况,我们需要实现玩家和游戏服务器之间的数据通信,但对于多人玩家的情况除此之外还需要实现玩家与玩家之间的数据通信。
这也就意味着SinglePlayerComponent
需要通过GameModel
来连接GameServer
服务,而MultiPlayerComponent
则需要GameModel
同时与GameServer
和GameP2PService
通信。
为了实现这样的依赖方式,依赖注入模式成为我们的首选实现方式,而且解决得很完美。
懒加载
应用会变得越来越大,我们的javascript代码可能会操作5万行,这也让js的按需加载变得尤其重要。
以我们上面提到的游戏为例,我们会按如下的结构来组织代码:
.
└── src├── multi-player│ ├── commands│ ├── components│ └── gateways├── single-player│ └── components├── home│ └── components└── shared
当用户打开首页的时候,我们希望加载home
和share
目录中的代码.如果玩家进一步的选择了单人模式,那么我们就会去加载single-player
目录中的代码,以此类推来实现按需加载。
按照上面的目录结构,我们也可以轻易的将整个应用拆分到多个开发者身上,给每个开发者一定的上下文限制。
其它需求
从架构层面考虑,我们还需要考虑如下的需求:
For the architecture we also have the standard set of requirements including:
Testability.
Maintability.
Open/closed.
技术栈
在我们整理好需求和开发思路后我们决定在几种技术栈中选择其一。我们首先想到的是React 和 Angular2。我们有过react 和 redux模式的成功经验。
但懒加载和依赖注入的问题依然让我们难以在这两者之间选择,react-router很好的支持了懒加载,但依赖注入依然是个问题。而Angular2的一个优势是 WebWorkers 的支持。
最终我们选择了如下的技术方案:
* Angular 2.
RxJS.
ngrx.
在我进一步的说明之前我想声明的是如上的架构并不局限于Angular2,React或任何其他的框架,也可以使用不同的语法以及无需依赖注入功能。
示例程序
这里有一个我们实现了如上架构的示例代码,该示例使用Angular2 和 rxjs,但正如之前提到的,你也可以使用react来替代。
为了更简单的解释相关概念,我将基于上面所提到的游戏来讲解。简单的说,这是一个帮助你提高打字速度的游戏,它有两个数据模块:
- 单人模式-可以练习打字的速度。该模块会给你一段文本并计算你能以多快的速度敲出来。
- 多人模式-与其他玩家比拼打字速度。所有的玩家通过WebRTC连接到同一个聊天室,一旦连接建立起来,玩家之间就需要相互交换信息,优先完成信息交换的玩家就会成为赢家。
现在我们根据我们上面图表给出的架构模式来实现这个游戏,我们首先从视图开始:
视图组件
视图组件的实现依赖于具体使用的UI框架(这里我们使用的是angular2)。组件可以保存某些状态,但我们必须清楚组件状态与store之间的对应关系以及组件内部的状态。
所有的组件通过组合的形式形成一颗组件树并通过控制器来将他们联系起来。
下面是GameComponent
的简单实现:
@Component({// Some component-specific declarationsproviders: [GameModel]
})
export class GameComponent implements AfterViewInit {// declarations...@Input() text: string;@Output() end: EventEmitter<number> = new EventEmitter<number>();@Output() change: EventEmitter<string> = new EventEmitter<string>();constructor(private _model: GameModel, private _renderer: Renderer) {}ngAfterViewInit() {// other UI related logicthis._model.startGame();}changeHandler(data: string) {if (this.text === data) {this.end.emit(this.timer.time);this._model.completeGame(this.timer.time, this.text);this.timer.reset();} else {this._model.onProgress(data);// other UI related logic}}reset() {this.timer.reset();this.text = '';}invalid() {return this._model.game$.scan((accum: boolean, current: any) => {return (current && current.get('invalid')) || accum;}, false);}
}
该组件具有如下的几个特点:
- 输入输出API
- 封装组件内部自己的状态,比如当前用户的输入文本就无需存储在Store中
- 使用
GameModel
作为该示例的Facade层
GameModel
给组件提供了访问应用状态的途径。例如,GameComponent
对当前游戏状态比较感兴趣,所以GameModel
就给其提供了访问游戏状态的方法。
使用像GameModel
这样的高级抽象能让新团队成员快速的投入开发,他们可以在Model层上直接开发UI组件,然后让Model层去维护应用状态的变化。团队成员只需要会使用angular2和RxJS数据流就可以投入开发,他们不用关心任何的通信协议、包数据格式以及redux等。
Model定义
如下为GameModel
的定义:
@Injectable()
export class GameModel extends Model {games$: Observable<string>;game$: Observable<string>;constructor(protected _store: Store<any>,@Optional() @Inject(AsyncService) _services: AsyncService[]) {super(_services || []);this.games$ = this._store.select('games');this.game$ = this._store.select('game');}startGame() {this._store.dispatch(GameActions.startGame());}onProgress(text: string) {this.performAsyncAction(GameActions.gameProgress(text, new Date())).subscribe(() => {// Do nothing, we're all good}, (data: any) => {if (data.invalidGame)this._store.dispatch(GameActions.invalidateGame());});}completeGame(time: number, text: string) {const action = GameActions.completeGame(time, text);this._store.dispatch(action);this.performAsyncAction(action).subscribe(() => console.log('Done!'));}
}
这个类将微服务形式的 ngrx store
抽象实例依赖进来并存储在_Store
中。
model可以通过分发actions来改变Store.我们可以将actionis当做命令或是对我们应用有意义的指令。他们包含一个 action 类型和一个payload,payload中存储相应的数据并提供给reducers
来更改Store.
GameModel
可以通过触发startGame
action来开始游戏,如下所示:
`this._store.dispatch(GameActions.startGame());`
触发Store对应的action会调用所有相关的reducers来更新store,接收action传过来的新的参数并创建一个新的store.最后store的变化会反馈到视图上。
可扩展的web单页应用程序架构相关推荐
- 如何在单页应用程序Angular 7中使用FastReport Core Web报表
2019独角兽企业重金招聘Python工程师标准>>> 下载FastReport.Net最新版本 单页应用程序的概念正在寻找越来越多的支持者.最着名的单页框架之一是Angular,它 ...
- 淘宝客静态单页_单页应用程序的Spring Boot静态Web资源处理
淘宝客静态单页 诸如gulp和grunt之类的Javascript构建工具确实让我大吃一惊,我看着这些工具的构建脚本之一,发现很难理解它,并且无法想象从头开始编写其中一个构建脚本. 这就是yeoman ...
- 单页应用程序的Spring Boot静态Web资源处理
诸如gulp和grunt之类的Javascript构建工具确实让我大吃一惊,我看着这些工具的构建脚本之一,发现很难理解它,无法想象从头开始编写其中一个构建脚本. 这就是yeoman出现的地方,它是一种 ...
- 低代码Web应用程序构造方法-ASP.NET Core 2.2单页应用程序(SPA)
目录 介绍 网格记录在编辑/添加上的持久性 以下步骤用于创建单页应用程序(SPA) 在SQL Server Management Studio中创建数据库 打开Visual Studio社区2019 ...
- jsp 构建单页应用_如何使用服务器端Blazor构建单页应用程序
jsp 构建单页应用 介绍 (Introduction) In this article, we will create a Single Page Application (SPA) using s ...
- razor页面跳转_如何在Blazor中使用Razor页面创建单页应用程序
razor页面跳转 In this article, we are going to create a Single Page Application (SPA) using Razor pages ...
- 使用Vanilla.js构建单页应用程序(SPA)网站
目录 项目 带有模块的组织代码 以可观察的方式应对变化 支持声明式数据绑定 将幻灯片(Slides)托管和加载为"页面" 使用路由器处理导航 带有CSS3动画的转换时间线 管理&q ...
- spa 搜索引擎_网站seo-SEO的单页应用程序(SPA)生存指南
JavaScript库与JavaScript框架 解开SPA背后的技术最终将我们引向JavaScript库和框架的主题. 问一个开发人员"库和框架之间有什么区别",你会得到很多有趣 ...
- Spring Security和Angular教程(一)安全的单页应用程序
Spring Security和Angular教程(一) 安全的单页应用程序 在本教程中,我们展示了Spring Security,Spring Boot和Angular的一些很好的功能,它们协同工作 ...
最新文章
- spring Ioc本质
- 知乎热议:国家何时整治程序员的高薪现象?网友:用命和头发换的钱都被人眼红!...
- html怎么把图片作为背景_抖音背景图片怎么弄,抖音背景图片引导关注
- 爬虫好学吗python-小白python学到什么程度可以学习网络爬虫? ?
- ASP.NET Core 2.2 基础知识(十四) WebAPI Action返回类型(未完待续)
- python 3 递归调用与二分法
- 「拨云见日」英特尔揭秘短视频背后的二三事
- 如何优雅的理解ECMAScript中的对象
- 提高Objective-C代码质量心机一:简化写法
- 8个流行的Python可视化工具包。
- delphi2010中FastReport的安装方法
- 4字节 经纬度_【笔记】进制转换和经度纬度
- 中报行情 锁定四大板块8只高送转潜力股 2011-7-9
- html表格合并内外边框,table 表格边框合并为单一的边框的方法
- 英特尔至强处理器排行_英特尔赛扬Vs之间的比较。 至强处理器
- mysql有next_day用法_Next_day()函数的用法
- 【汽车制造业】“新三化+新能源”蓝海,加速车企数字化转型进入“深水区”
- 微信浏览器视频播放探索
- OpenGL超级宝典笔记——光照参数与材料属性
- edge 错误 客户端和服务器不支持常用的 SSL 协议版本或密码套件