复杂单页应用的数据层设计

发布时间:2019-02-21  栏目:计算机教程  评论:0 Comments

缓存的使用

如果说我们的业务里,有一些数据是通过WebSocket把更新都同步过来,这些数据在前端就始终是可信的,在后续使用的时候,可以作一些复用。

比如说:

在一个项目中,项目所有成员都已经查询过,数据全在本地,而且变更有WebSocket推送来保证。这时候如果要新建一条任务,想要从项目成员中指派任务的执行人员,可以不必再发起查询,而是直接用之前的数据,这样选择界面就可以更流畅地出现。

这时候,从视图角度看,它需要解决一个问题:

  • 如果要获取的数据未有缓存,它需要产生一个请求,这个调用过程就是异步的
  • 如果要获取的数据已有缓存,它可以直接从缓存中返回,这个调用过程就是同步的

如果我们有一个数据层,我们至少期望它能够把同步和异步的差异屏蔽掉,否则要使用两种代码来调用。通常,我们是使用Promise来做这种差异封装的:

JavaScript

function getDataP() : Promise<T> { if (data) { return
Promise.resolve(data) } else { return fetch(url) } }

1
2
3
4
5
6
7
function getDataP() : Promise<T> {
  if (data) {
    return Promise.resolve(data)
  } else {
    return fetch(url)
  }
}

这样,使用者可以用相同的编程方式去获取数据,无需关心内部的差异。

Maven Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

具体方案

以上我们谈了以RxJS为代表的数据流库的这么多好处,彷佛有了它,就像有了民主,人民就自动吃饱穿暖,物质文化生活就自动丰富了,其实不然。任何一个框架和库,它都不是来直接解决我们的业务问题的,而是来增强某方面的能力的,它刚好可以为我们所用,作为整套解决方案的一部分。

至此,我们的数据层方案还缺失什么东西吗?

考虑如下场景:

某个任务的一条子任务产生了变更,我们会让哪条数据流产生变更推送?

分析子任务的数据流,可以大致得出它的来源:

subtask$ = subtaskQuery$ + subtaskUpdate$

看这句伪代码,加上我们之前的解释(这是一个reduce操作),我们得到的结论是,这条任务对应的subtask$数据流会产生变更推送,让视图作后续更新。

仅仅这样就可以了吗?并没有这么简单。

从视图角度看,我们还存在这样的对子任务的使用:那就是任务的详情界面。但这个界面订阅的是这条子任务的所属任务数据流,在其中任务数据包含的子任务列表中,含有这条子任务。所以,它订阅的并不是subtask$,而是task$。这么一来,我们必须使task$也产生更新,以此推动任务详情界面的刷新。

那么,怎么做到在subtask的数据流变更的时候,也推动所属task的数据流变更呢?这个事情并非RxJS本身能做的,也不是它应该做的。我们之前用RxJS来封装的部分,都只是数据的变更链条,记得之前我们是怎么描述数据层解决方案的吗?

实体的关系定义和数据变更链路的封装

我们前面关注的都是后面一半,前面这一半,还完全没做呢!

实体的变更关系如何做呢,办法其实很多,可以用类似Backbone的Model和Collection那样做,也可以用更加专业的方案,引入一个ORM机制来做。这里面的实现就不细说了,那是个相对成熟的领域,而且说起来篇幅太大,有疑问的可以自行了解。

需要注意的是,我们在这个里面需要考虑好与缓存的结合,前端的缓存很简单,基本就是一种精简的k-v数据库,在做它的存储的时候,需要做到两件事:

  • 以集合形式获取的数据,需要拆分放入缓存,比如Task[],应当以每个Task的TaskId为索引,分别单独存储
  • 有时候后端返回的数据可能是不完整的,或者格式有差异,需要在储存之间作正规化(normalize)

总结以上,我们的思路是:

  • 缓存 => 基于内存的微型k-v数据库
  • 关联变更 => 使用ORM的方式抽象业务实体和变更关系
  • 细粒度推送 => 某个实体的查询与变更先合并为数据流
  • 从实体的变更关系,引出数据流,并且所属实体的流
  • 业务上层使用这些原始数据流以组装后续变更

Config

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
        // 添加服务端点,可以理解为某一服务的唯一key值
        stompEndpointRegistry.addEndpoint("/chatApp");
        //当浏览器支持sockjs时执行该配置
        stompEndpointRegistry.addEndpoint("/chatApp").setAllowedOrigins("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 配置接受订阅消息地址前缀为topic的消息
        config.enableSimpleBroker("/topic");
        // Broker接收消息地址前缀
        config.setApplicationDestinationPrefixes("/app");
    }
}

4. 可拆解的WebSocket补丁

这个标题需要结合上面那个图来理解。我们怎么理解WebSocket在整个方案中的意义呢?其实可以整体视为整个通用数据层的补丁包,因此,我们就可以用这个理念来实现它,把所有对WebSocket的处理部分,都独立出去,如果需要,就异步加载到主应用来,如果在某些场景下,想把这块拿掉,只需不引用它就行了,一行配置解决它的有无问题。

但是在具体实现的时候,需要注意:拆掉WebSocket之后的数据层,对应的缓存是不可信的,需要做相应考虑。

总结

这是spring-boot接入WebSocket最简单的方法了,很直观的表现了socket在浏览器段通信的便利,但根据不同的业务场景,对该技术的使用还需要斟酌,例如如何使WebSocket在分布式服务端保持服务,如何在连接上集群后下发消息找到长连接的服务端机器。我也在为这个问题苦苦思考,思路虽有,实践起来却举步维艰,特别是网上谈到比较多的将连接序列化到缓存中,统一管理读取分配,分享几个好思路,也希望自己能给找到较好的方案再分享一篇博客。
来自Push notifications with websockets in a distributed Node.js
app

  1. Configure Nginx to send websocket requests from each browser to all
    the server in the cluster. I could not figure out how to do it. Load
    balancing does not support broadcasting.
  2. Store websocket connections in the databse, so that all servers had
    access to it. I am not sure how to serialize the websocket
    connection object to store it in MongoDB.
  3. Set up a communication mechanism among the servers in the cluster
    (some kind message bus) and whenever event happens, have all the
    servers notify the websocket clients they are tracking. This
    somewhat complicates the system and requires the nodes to know the
    addresses of each other. Which package is most suitable for such a
    solution?
    再分享几个讨论:
    美高梅娱乐场网站,springsession如何对spring的WebSocketSession进行分布式配置?
    websocket多台服务器之间怎么共享websocketSession?

对技术选型的思考

到目前为止,各种视图方案是逐渐趋同的,它们最核心的两个能力都是:

  • 组件化
  • MDV(模型驱动视图)

缺少这两个特性的方案都很容易出局。

我们会看到,不管哪种方案,都出现了针对视图之外部分的一些补充,整体称为某种“全家桶”。

全家桶方案的出现是必然的,因为为了解决业务需要,必然会出现一些默认搭配,省去技术选型的烦恼。

但是我们必须认识到,各种全家桶方案都是面向通用问题的,它能解决的都是很常见的问题,如果你的业务场景很与众不同,还坚持用默认的全家桶,就比较危险了。

通常,这些全家桶方案的数据层部分都还比较薄弱,而有些特殊场景,其数据层复杂度远非这些方案所能解决,必须作一定程度的自主设计和修正,我工作十余年来,长期从事的都是复杂的toB场景,见过很多厚重的、集成度很高的产品,在这些产品中,前端数据和业务逻辑的占比较高,有的非常复杂,但视图部分也无非是组件化,一层套一层。

所以,真正会产生大的差异的地方,往往不是在视图层,而是在水的下面。

愿读者在处理这类复杂场景的时候,慎重考虑。有个简单的判断标准是:视图复用数据是否较多,整个产品是否很重视无刷新的交互体验。如果这两点都回答否,那放心用各种全家桶,基本不会有问题,否则就要三思了。

必须注意到,本文所提及的技术方案,是针对特定业务场景的,所以未必具有普适性。有时候,很多问题也可以通过产品角度的权衡去避免,不过本文主要探讨的还是技术问题,期望能够在产品需求不让步的情况下,也能找到比较优雅、和谐的解决方案,在业务场景面前能攻能守,不至于进退失据。

即使我们面对的业务场景没有这么复杂,使用类似RxJS的库,依照数据流的理念对业务模型做适度抽象,也是会有一些意义的,因为它可以用一条规则统一很多东西,比如同步和异步、过去和未来,并且提供了很多方便的时序操作。

参考

WebSocket
Support

复杂单页应用的数据层设计

2017/01/11 · JavaScript
·
单页应用

原文出处: 徐飞   

很多人看到这个标题的时候,会产生一些怀疑:

什么是“数据层”?前端需要数据层吗?

可以说,绝大部分场景下,前端是不需要数据层的,如果业务场景出现了一些特殊的需求,尤其是为了无刷新,很可能会催生这方面的需要。

我们来看几个场景,再结合场景所产生的一些诉求,探讨可行的实现方式。

MessageMapping

    @Autowired
    private SimpMessagingTemplate template;

    //接收客户端"/app/chat"的消息,并发送给所有订阅了"/topic/messages"的用户
    @MessageMapping("/chat")
    @SendTo("/topic/messages")
    public OutputMessage receiveAndSend(InputMessage inputMessage) throws Exception {
        System.out.println("get message (" + inputMessage.getText() + ") from client!");
        System.out.println("send messages to all subscribers!");
        String time = new SimpleDateFormat("HH:mm").format(new Date());
        return new OutputMessage(inputMessage.getFrom(), inputMessage.getText(), time);
    }

    //或者直接从服务端发送消息给指定客户端
    @MessageMapping("/chat_user")
    public void sendToSpecifiedUser(@Payload InputMessage inputMessage, SimpMessageHeaderAccessor headerAccessor) throws Exception {
        System.out.println("get message from client (" + inputMessage.getFrom() + ")");
        System.out.println("send messages to the specified subscriber!");
        String time = new SimpleDateFormat("HH:mm").format(new Date());
        this.template.convertAndSend("/topic/" + inputMessage.getFrom(), new OutputMessage(inputMessage.getFrom(), inputMessage.getText(), time));
    }

3. 跨端复用代码。

以前我们经常会考虑做响应式布局,目的是能够减少开发的工作量,尽量让一份代码在PC端和移动端复用。但是现在,越来越少的人这么做,原因是这样并不一定降低开发的难度,而且对交互体验的设计是一个巨大考验。那么,我们能不能退而求其次,复用尽量多的数据和业务逻辑,而开发两套视图层?

在这里,可能我们需要做一些取舍。

回忆一下MVVM这个词,很多人对它的理解流于形式,最关键的点在于,M和VM的差异是什么?即使是多数MVVM库比如Vue的用户,也未必能说得出。

在很多场景下,这两者并无明显分界,服务端返回的数据直接就适于在视图上用,很少需要加工。但是在我们这个方案中,还是比较明显的:

> —— Fetch ————-> | | View <– VM <– M <–
RESTful ^ | <– WebSocket

1
2
3
4
5
> —— Fetch ————->
|                           |
View  <–  VM  <–  M  <–  RESTful
                    ^
                    |  <–  WebSocket

这个简图大致描述了数据的流转关系。其中,M指代的是对原始数据的封装,而VM则侧重于面向视图的数据组合,把来自M的数据流进行组合。

我们需要根据业务场景考虑:是要连VM一起跨端复用呢,还是只复用M?考虑清楚了这个问题之后,我们才能确定数据层的边界所在。

除了在PC和移动版之间复用代码,我们还可以考虑拿这块代码去做服务端渲染,甚至构建到一些Native方案中,毕竟这块主要的代码也是纯逻辑。

结果

美高梅娱乐场网站 1

send to all subscribers

美高梅娱乐场网站 2

send to the specified subscriber

2. 增强了整个应用的可测试性。

因为数据层的占比较高,并且相对集中,所以可以更容易对数据层做测试。此外,由于视图非常薄,甚至可以脱离视图打造这个应用的命令行版本,并且把这个版本与e2e测试合为一体,进行覆盖全业务的自动化测试。

知识背景

随着物联网的发展促进传统行业不断转型,在设备间通信的业务场景越来越多。其中很大一部分在于移动端和设备或服务端与设备的通信,例如已成主流的共享单车。但存在一个这样小问题,当指令下发完毕之后,设备不会同步返回指令执行是否成功,而是异步通知或是服务端去主动查询设备指令是否发送成功,这样一来客户端(前端)也无法同步获取指令执行情况,只能通过服务端异步通知来接收该状态了。这也就引出了这篇博客想要探索的一项技术:如何实现服务端主动通知前端?
其实,这样的业务场景还有很多,但这样的解决方案却不是非常成熟,方案概括过来就两个大类。1.前端定时请求轮询
2.前端和服务端保持长连接,以持续进行数据交互,这个可以包括较为成熟的WebSocket。我们可以看看张小龙在知乎问题
如何在大型 Web 应用中保持数据的同步更新?
的回答,更加清楚的认识这个过程。

这个问题在10年前已经被解决过无数次了,最简单的例子就是网页聊天室。题主的需求稍微复杂些,需要支持的数据格式更多,然而只要定义好了通讯规范,多出来的也只是搬砖的活儿了。
整个过程可以分为5个环节:1 包装数据、2 触发通知、3 通讯传输、4
解析数据、5 渲染数据。这5个环节中有三点很关键:1 通讯通道选择、2
数据格式定义、3 渲染数据。

1
通讯通道选择:这个很多前端高手已经回答了,基本就是两种方式:轮询和长连接,这种情况通常的解决方式是长连接,Web端可以用WebSocket来解决,这也是业界普遍采用的方案,比如环信、用友有信、融云等等。通讯环节是相当耗费服务器资源的一个环节,而且开发成本偏高,建议将这些第三方的平台直接集成到自己的项目中,以降低开发的成本。

2
数据格式定义:数据格式可以定义得五花八门,不过为了前端的解析,建议外层统一数据格式,定义一个类似type的属性来标记数据属性(是IM消息、微博数据还是发货通知),然后定义一个data属性来记录数据的内容(一般对应数据表中的一行数据)。统一数据格式后,前端解析数据的成本会大大降低。

3
渲染数据渲染数据是关系到前端架构的,比如是React、Vue还是Angular(BTW:不要用Angular,个人认为Angular在走向灭亡)。这些框架都用到了数据绑定,这已经成为业界的共识了(只需要对数据进行操作,不需要操作DOM),这点不再论述。在此种需求场景下,数据流会是一个比较大的问题,因为可能每一条新数据都需要寻找对应的组件去传递数据,这个过程会特别恶心。所以选择单一树的数据流应该会很合适,这样只需要对一棵树的节点进行操作即可:定义好type和树节点的对应关系,然后直接定位到对应的节点对数据增删改就可以,例如Redux。

以上三点是最核心的环节,涉及到前后端的数据传输、前端数据渲染,其他的内容就比较简单了,也简单说下。

后端:包装数据、触发通知这个对后端来说就很Easy了,建一个队列池,不断的往池子里丢任务,让池子去触发通知。

前端:解析数据解析数据就是多出来的搬砖的活儿,过滤type、取data。技术难度并不大,主要点还是在于如何能低开发成本、低维护成本地达到目的,上面是一种比较综合的低成本的解决方案。

对于对实时性要求较高的业务场景,轮询显然是无法满足需求的,而长连接的缺点在于长期占了服务端的连接资源,当前端用户数量指数增长到一定数量时,服务端的分布式须另辟蹊径来处理WebSocket的连接匹配问题。它的优点也很明显,对于传输内容不大的情况下,有非常快的交互速度,因为他不是基于HTTP请求的,而是浏览器端扩展的Socket通信。

RxJS

遍观流行的辅助库,我们会发现,基于数据流的一些方案会对我们有较大帮助,比如RxJS,xstream等,它们的特点刚好满足了我们的需求。

以下是这类库的特点,刚好是迎合我们之前的诉求。

  • Observable,基于订阅模式
  • 类似Promise对同步和异步的统一
  • 查询和推送可统一为数据管道
  • 容易组合的数据管道
  • 形拉实推,兼顾编写的便利性和执行的高效性
  • 懒执行,不被订阅的数据流不执行

这些基于数据流理念的库,提供了较高层次的抽象,比如下面这段代码:

JavaScript

function getDataO(): Observable<T> { if (cache) { return
Observable.of(cache) } else { return Observable.fromPromise(fetch(url))
} } getDataO().subscribe(data => { // 处理数据 })

1
2
3
4
5
6
7
8
9
10
11
12
function getDataO(): Observable<T> {
  if (cache) {
    return Observable.of(cache)
  }
  else {
    return Observable.fromPromise(fetch(url))
  }
}
 
getDataO().subscribe(data => {
  // 处理数据
})

这段代码实际上抽象程度很高,它至少包含了这么一些含义:

  • 统一了同步与异步,兼容有无缓存的情况
  • 统一了首次查询与后续推送的响应,可以把getDataO方法内部这个Observable也缓存起来,然后把推送信息合并进去

我们再看另外一段代码:

JavaScript

const permission$: Observable<boolean> = Observable
.combineLatest(task$, user$) .map(data => { let [task, user] = data
return user.isAdmin || task.creatorId === user.id })

1
2
3
4
5
6
const permission$: Observable<boolean> = Observable
  .combineLatest(task$, user$)
  .map(data => {
    let [task, user] = data
    return user.isAdmin || task.creatorId === user.id
  })

这段代码的意思是,根据当前的任务和用户,计算是否拥有这条任务的操作权限,这段代码其实也包含了很多含义:

首先,它把两个数据流task$和user$合并,并且计算得出了另外一个表示当前权限状态的数据流permission$。像RxJS这类数据流库,提供了非常多的操作符,可用于非常简便地按照需求把不同的数据流合并起来。

我们这里展示的是把两个对等的数据流合并,实际上,还可以进一步细化,比如说,这里的user$,我们如果再追踪它的来源,可以这么看待:

某用户的数据流user$ := 对该用户的查询 +
后续对该用户的变更(包括从本机发起的,还有其他地方更改的推送)

如果说,这其中每个因子都是一个数据流,它们的叠加关系就不是对等的,而是这么一种东西:

  • 每当有主动查询,就会重置整个user$流,恢复一次初始状态
  • user$等于初始状态叠加后续变更,注意这是一个reduce操作,也就是把后续的变更往初始状态上合并,然后得到下一个状态

这样,这个user$数据流才是“始终反映某用户当前状态”的数据流,我们也就因此可以用它与其它流组合,参与后续运算。

这么一段代码,其实就足以覆盖如下需求:

  • 任务本身变化了(执行者、参与者改变,导致当前用户权限不同)
  • 当前用户自身的权限改变了

这两者导致后续操作权限的变化,都能实时根据需要计算出来。

其次,这是一个形拉实推的关系。这是什么意思呢,通俗地说,如果存在如下关系:

JavaScript

c = a + b //
不管a还是b发生更新,c都不动,等到c被使用的时候,才去重新根据a和b的当前值计算

1
c = a + b     // 不管a还是b发生更新,c都不动,等到c被使用的时候,才去重新根据a和b的当前值计算

如果我们站在对c消费的角度,写出这么一个表达式,这就是一个拉取关系,每次获取c的时候,我们重新根据a和b当前的值来计算结果。

而如果站在a和b的角度,我们会写出这两个表达式:

JavaScript

c = a1 + b // a1是当a变更之后的新值 c = a + b1 // b1是当b变更之后的新值

1
2
c = a1 + b     // a1是当a变更之后的新值
c = a + b1    // b1是当b变更之后的新值

这是一个推送关系,每当有a或者b的变更时,主动重算并设置c的新值。

如果我们是c的消费者,显然拉取的表达式写起来更简洁,尤其是当表达式更复杂时,比如:

JavaScript

e = (a + b ) * c – d

1
e = (a + b ) * c – d

如果用推的方式写,要写4个表达式。

所以,我们写订阅表达式的时候,显然是从使用者的角度去编写,采用拉取的方式更直观,但通常这种方式的执行效率都较低,每次拉取,无论结果是否变更,都要重算整个表达式,而推送的方式是比较高效精确的。

但是刚才RxJS的这种表达式,让我们写出了形似拉取,实际以推送执行的表达式,达到了编写直观、执行高效的结果。

看刚才这个表达式,大致可以看出:

permission$ := task$ + user$

这么一个关系,而其中每个东西的变更,都是通过订阅机制精确发送的。

有些视图库中,也会在这方面作一些优化,比如说,一个计算属性(computed
property),是用拉的思路写代码,但可能会被框架分析依赖关系,在内部反转为推的模式,从而优化执行效率。

此外,这种数据流还有其它魔力,那就是懒执行。

什么是懒执行呢?考虑如下代码:

JavaScript

const a$: Subject<number> = new Subject<number>() const b$:
Subject<number> = new Subject<number>() const c$:
Observable<number> = Observable.combineLatest(a$, b$) .map(arr
=> { let [a, b] = arr return a + b }) const d$:
Observable<number> = c$.map(num => { console.log(‘here’) return
num + 1 }) c$.subscribe(data => console.log(`c: ${data}`))
a$.next(2) b$.next(3) setTimeout(() => { a$.next(4) }, 1000)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const a$: Subject<number> = new Subject<number>()
const b$: Subject<number> = new Subject<number>()
 
const c$: Observable<number> = Observable.combineLatest(a$, b$)
  .map(arr => {
    let [a, b] = arr
    return a + b
  })
 
const d$: Observable<number> = c$.map(num => {
  console.log(‘here’)
  return num + 1
})
 
c$.subscribe(data => console.log(`c: ${data}`))
 
a$.next(2)
b$.next(3)
 
setTimeout(() => {
  a$.next(4)
}, 1000)

注意这里的d$,如果a$或者b$中产生变更,它里面那个here会被打印出来吗?大家可以运行一下这段代码,并没有。为什么呢?

因为在RxJS中,只有被订阅的数据流才会执行。

主题所限,本文不深究内部细节,只想探讨一下这个特点对我们业务场景的意义。

想象一下最初我们想要解决的问题,是同一份数据被若干个视图使用,而视图侧的变化是我们不可预期的,可能在某个时刻,只有这些订阅者的一个子集存在,其它推送分支如果也执行,就是一种浪费,RxJS的这个特性刚好能让我们只精确执行向确实存在的视图的数据流推送。

Spring boot接入WebSocket

后记

不久前,我写过一篇总结,内容跟本文有不少重合之处,但为什么还要写这篇呢?

上一篇,讲问题的视角是从解决方案本身出发,阐述解决了哪些问题,但是对这些问题的来龙去脉讲得并不清晰。很多读者看完之后,仍然没有得到深刻认识。

这一篇,我希望从场景出发,逐步展示整个方案的推导过程,每一步是怎样的,要如何去解决,整体又该怎么做,什么方案能解决什么问题,不能解决什么问题。

上次我那篇讲述在Teambition工作经历的回答中,也有不少人产生了一些误解,并且有反复推荐某些全家桶方案,认为能够包打天下的。平心而论,我对方案和技术选型的认识还是比较慎重的,这类事情,事关技术方案的严谨性,关系到自身综合水准的鉴定,不得不一辩到底。当时关注八卦,看热闹的人太多,对于探讨技术本身倒没有展现足够的热情,个人认为比较可惜,还是希望大家能够多关注这样一种有特色的技术场景。因此,此文非写不可。

如果有关注我比较久的,可能会发现之前写过不少关于视图层方案技术细节,或者组件化相关的主题,但从15年年中开始,个人的关注点逐步过渡到了数据层,主要是因为上层的东西,现在研究的人已经多起来了,不劳我多说,而各种复杂方案的数据层场景,还需要作更艰难的探索。可预见的几年内,我可能还会在这个领域作更多探索,前路漫漫,其修远兮。

(整个这篇写起来还是比较顺利的,因为之前思路都是完整的。上周在北京闲逛一周,本来是比较随意交流的,鉴于有些公司的朋友发了比较正式的分享邮件,花了些时间写了幻灯片,在百度、去哪儿网、58到家等公司作了比较正式的分享,回来之后,花了一整天时间整理出了本文,与大家分享一下,欢迎探讨。)

2 赞 4 收藏
评论

美高梅娱乐场网站 3

clients

<!DOCTYPE html>
<!DOCTYPE html>
<html>

    <head>
        <title>Chat WebSocket</title>
        <script src="http://cdn.jsdelivr.net/sockjs/0.3.4/sockjs.min.js"></script>
        <script src="js/stomp.js"></script>
        <script type="text/javascript">
            var apiUrlPre = "http://10.200.0.126:9041/discovery";
            var stompClient = null;

            function setConnected(connected) {
                document.getElementById('connect').disabled = connected;
                document.getElementById('disconnect').disabled = !connected;
                document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
                document.getElementById('response').innerHTML = '';
            }

            function connect() {
                var socket = new SockJS('http://localhost:9041/discovery/chatApp');
        var from = document.getElementById('from').value;
                stompClient = Stomp.over(socket);
                stompClient.connect({}, function(frame) {
                    setConnected(true);
                    console.log('Connected: ' + frame);
          //stompClient.subscribe('/topic/' + from, function(messageOutput) {
                    stompClient.subscribe('/topic/messages', function(messageOutput) {
                        //                      alert(messageOutput.body);
                        showMessageOutput(JSON.parse(messageOutput.body));
                    });
                });
            }

            function disconnect() {
                if(stompClient != null) {
                    stompClient.disconnect();
                }
                setConnected(false);
                console.log("Disconnected");
            }

            function sendMessage() {
                var from = document.getElementById('from').value;
                var text = document.getElementById('text').value;
                //stompClient.send("/app/chat_user", {},
                stompClient.send("/app/chat", {},
                    JSON.stringify({
                        'from': from,
                        'text': text
                    })
                );
            }

            function showMessageOutput(messageOutput) {
                var response = document.getElementById('response');
                var p = document.createElement('p');
                p.style.wordWrap = 'break-word';
                p.appendChild(document.createTextNode(messageOutput.from + ": " +
                    messageOutput.text + " (" + messageOutput.time + ")"));
                response.appendChild(p);
            }
        </script>
    </head>

    <body onload="disconnect()">
        <div>
            <div>
                <input type="text" id="from" placeholder="Choose a nickname" />
            </div>
            <br />
            <div>
                <button id="connect" onclick="connect();">Connect</button>
                <button id="disconnect" disabled="disabled" onclick="disconnect();">
                    Disconnect
                </button>
            </div>
            <br />
            <div id="conversationDiv">
                <input type="text" id="text" placeholder="Write a message..." />
                <button id="sendMessage" onclick="sendMessage();">Send</button>
                <p id="response"></p>
            </div>
        </div>

    </body>

</html>

更深入的探索

如果说我们针对这样的复杂场景,实现了这么一套复杂的数据层方案,还可以有什么有意思的事情做呢?

这里我开几个脑洞:

  • 用Worker隔离计算逻辑
  • 用ServiceWorker实现本地共享
  • 与本地持久缓存结合
  • 前后端状态共享
  • 可视化配置

我们一个一个看,好玩的地方在哪里。

第一个,之前提到,整个方案的核心是一种类似ORM的机制,外加各种数据流,这里面必然涉及数据的组合、计算之类,那么我们能否把它们隔离到渲染线程之外,让整个视图变得更流畅?

第二个,很可能我们会碰到同时开多个浏览器选项卡的客户,但是每个选项卡展现的界面状态可能不同。正常情况下,我们的整个数据层会在每个选项卡中各存在一份,并且独立运行,但其实这是没有必要的,因为我们有订阅机制来保证可以扩散到每个视图。那么,是否可以用过ServiceWorker之类的东西,实现跨选项卡的数据层共享?这样就可以减少很多计算的负担。

对这两条来说,让数据流跨越线程,可能会存在一些障碍待解决。

第三个,我们之前提到的缓存,全部是在内存中,属于易失性缓存,只要用户关掉浏览器,就全部丢了,可能有的情况下,我们需要做持久缓存,比如把不太变动的东西,比如企业通讯录的人员名单存起来,这时候可以考虑在数据层中加一些异步的与本地存储通信的机制,不但可以存localStorage之类的key-value存储,还可以考虑存本地的关系型数据库。

第四个,在业务和交互体验复杂到一定程度的时候,服务端未必还是无状态的,想要在两者之间做好状态共享,有一定的挑战。基于这么一套机制,可以考虑在前后端之间打通一个类似meteor的通道,实现状态共享。

第五个,这个话题其实跟本文的业务场景无关,只是从第四个话题引发。很多时候我们期望能做到可视化配置业务系统,但一般最多也就做到配置视图,所以,要么做到的是一个配置运营页面的东西,要么是能生成一个脚手架,供后续开发使用,但是一旦开始写代码,就没法合并回来。究其原因,是因为配不出组件的数据源和业务逻辑,找不到合理的抽象机制。如果有第四条那么一种铺垫,也许是可以做得比较好的,用数据流作数据源,还是挺合适的,更何况,数据流的组合关系能够可视化描述啊。

RxJS与其它方案的对比

留下评论

网站地图xml地图