影子模型

前言: 

js语言学起来简单,前端开发难在交互。

开发交互应用程序,本质就是维护一个模型,响应用户操作,在不同的状态之间转换。

用程序模拟传统的座机电话,主要有三种状态:挂断(等待),提机拨号,通话

在状态1的时候,数字键被按下是不会播出电话的;

当“提机”动作发生时,转换到状态2,这时按下数字键则代表拨号;

电话线那头传来信号“接通”,转换到状态3,这时才可以互相说话。

此文介绍的模型,是解决多页面间交互状态同步的,是通过业务实践中总结出来的。

问题描述:

产品需求要在所有页面上都启用WebChat,为了优化交互的体验,同时开启多个页面的情况下,所有页面上的WebChat要保持同步:收到的消息,展开某个窗口,新开了窗口,关闭了窗口;……总之这个WebChat就像浏览器的一部分。

多页面间通信

这个话题已经有很多成熟的方案,我们也是在几年前就引入了LocalStorage + storage事件的方式。

功能:任意一个key的内容被更改(或者从无到有的创建),会在所有(同域)页面上发布一个storage事件,携带的参数包含被修改的key列表。

基于这个特性,实现多页面通信,只需要所有页面上的程序约定监听同一个key的改变。

还有另外一种方式,我们也曾用过:cookie轮询。在cookie的同一个key中,用约定的格式存储“状态”,所有页面都做同一件事情——每隔一段时间取这个值与自己的当前状态做对比。

最早一版的“窗口状态同步”用的就是这个方案,它最大的缺点是:cookie占用网络流量,不够灵活。

基于storage的信道

接下来遇到的问题是:需要同步的量越来越多,代码繁琐容易出错(一遍一遍的重造车轮)

这时想到引入“管道+消息”概念,把监听storage事件的逻辑封装起来以“信道”的概念提供服务。我把他命名为Cluster(多个实体之间相互协作,实体间没有绝对的主次,虽然这个名字听起来有点别扭)

任何一个业务功能都可以使用Cluster实现“多页面通信”,只需要新建一个“信道”,就可以通过它发消息给所有加入这个信道的实体。(加入信道没有身份验证,只需要指定信道的名字)

用例:所有页面的程序都会加入一个叫做“message”的信道,当用户在一个页面上发一条消息的时候,当前页面向信道中“广播”这条消息,所有页面都会收到。

基于信道的方案:

有了通信的工具,直接的想法是在每个业务逻辑中,按照需要创建自己的信道,页面间彼此互发消息。

按照这个思路,以“展开某个窗口”的过程为例,为窗口添加“同步”的逻辑。

引入同步者

为了保持窗口对象原有的逻辑清晰容易维护,新逻辑不侵入窗口对象,以观察者的形式存在,称这个观察者为“同步者”。同步者默默的注释着窗口的每一个改变,把消息发给其他页面。

以“事件驱动”的编程方式(观察者模式),同步者监听窗口的一些事件,事件发生时通知其他页面执行某个动作。

默默的

通过监听窗口的“unfold”事件,在页面A展开了窗口w,同步者立即在信道中广播一条消息。此时B页面上的同步者下命令(调用window.unfold())展开了窗口w。按照窗口原有的逻辑,这个B页面的窗口w也会发布unfold事件,然后(循环开始了)……

这是个Bug,解决方式就是要区分这个“动作”是不是其他页面同步过来的,如果是,则禁止触发同步(不发unfold事件)。为此我将“展开”的动作添加了一个silent参数(默默的),如果指定silent,就不发unfold事件。另外,如果有其他功能也在监听unfold事件,而且跟同步无关,这样鲁莽的指定silent就可能使这部分功能失效(比如窗口的互斥功能,展开一个要自动收起其他所有的),所以我又添加了另外一个与同步无关的事件“focus”……如果将来增加交互,窗口边栏的收起与展开也要同步,就需要窗口增加另外一个事件(sidebar_expend),修改同步者增加这个事件的监听(但其实同步者根本不需要关心窗口到底都有哪些功能)……

总之,琐碎问题一大堆,将来交互逻辑越来越复杂,功能直接相互冲突的事情就更频繁了,像一堆毛线团一样。

影子模型:

我试图总结一个模型,解决所有人在这里可能遇到的问题。

模型描述:

一个小人儿站在阳光下,他怎样动,影子就跟着做一模一样的动作。影子模型就是从这里来的。

所有的窗口即是小人儿,也是影子。当用户正在一个页面上点击了窗口,这个窗口就是此时的小人儿。影子只是默默的重复小人儿的动作(不要发消息支配小人儿)。如果用户换到另外一个页面上继续操作,先前的小人儿就成了这时的影子了。

 

窗口:

  1. 所有需要同步的动作都要发布sync事件(事件名字随意取);
  2. 窗口中用于执行动作的方法必须提供silent参数支持,如果指定silent就不要发布sync事件;
  3. 窗口必须提供一个apply方法,用于接收小人儿发来的消息并翻译成具体的动作。

同步者:

  1. 消息的格式只需要小人儿和影子间互相理解,同步者不需要关心;
  2. 同步者只负责原样传递消息,将消息从小人儿传递给所有影子;

为现有功能添加同步功能,只需要对现有的交互程序稍作改动,再为他分配一个“同步者”。

运行过程:

  1. 用户操作了窗口,用影子们约定的格式描述这个动作,并触发sync事件;
  2. 同步者拿到sync内容广播给所有影子;
  3. 影子端的同步者收到消息在影子上执行apply(sync);
  4. apply()能够理解sync的格式,并重复窗口的动作(用slient参数执行动作);
  5. 完成了动作的同步。
这个过程可以简化的理解为:窗口触发sync事件,相当于直接调用了影子窗口的apply(sync)。

在webChat中的应用:

聊天窗口的管理,分为两部分实现

1. roomKeeper负责聊天室的创建和管理

2. layout负责所有与展现相关的处理

layout中实现了窗口排列布局的记录和同步,还有页面刷新后窗口的重建。他持有一个winSlider组件,所有的窗口都要winSlider.appendWindow()才会显示到界面上。为了避免处理繁杂的交互逻辑,我的同步功能直接添加在winSlider上,监听change事件,把改动广播给影子们。影子们通过winSlider.applyChange()重复小人儿的动作。

posted in Web开发 by deemstone

Follow comments via the RSS Feed | 留下评论 | Trackback URL

Leave Your Comment

You must be logged in to post a comment.

 
Powered by Wordpress. Design by Bingo - The Web Design Experts.