用 RxJS 实现一个协同编辑的表格应用

下面这张图几乎能够表明全部软件的模型了:

输入 -> 计算过程 -> 输出
复制代码

若是输入和输出都是数字,那么这个软件有多是一个数学计算类型的软件;若是输入和输出都是字符串,那么这个软件有多是一个文本处理类型的软件。这些都是比较纯粹的类型,能够把注意力集中在算法的实现上。再来看一个略复杂的状况,输入的个数不止一个,有用户的点击操做、用户的键盘输入、用户声音和图像的变化、来自数据库的数据以及一系列随时间和空间变化的条件,输出则是各类屏幕上的图像。前端

你们应该已经看出来了,最后一种软件类型就是 web 前端应用。将上面的模型具体一下:node

输入 -> store -> element tree -> 输出
复制代码

store 能够理解为存放数据的地方,element tree 则表示能表达视图的一棵树。element tree 到输出的复杂度已经被 HTML + CSS 等技术解决了,而 store 到 element tree 的复杂度已经被前端 MV* 框架解决了。然而从繁多复杂的输入到 store 的复杂度如何解决呢?RxJS 是一个值得尝试的选择。git

第一次据说 RxJS 的时候就被它吸引了,它能够把各类各样的输入(尤为是和和时间相关的输入)经过包装、组合和转换变成成有用的数据,根据上面的软件模型,RxJS 是整个复杂度解决方案的最后一块拼图。github

一个相对复杂的示例

为了能发挥出 RxJS 的威力,作了一个交互复杂一点示例,仓库地址是 github.com/xxapp/rxjs-…,它是一个支持协同编辑的表格应用,支持拖拽选择单元格,编辑单元格而且支持多个用户同时编辑,能够在 github 项目首页看到实际效果图。web

先来分析下需求:算法

  1. 一个基础的表格
  2. 根据鼠标的拖动显示单元格选区
  3. 当一个单元格被连续点了两次,进入编辑状态
  4. 当一个单元格被点了一次再按下键盘按键,也进入编辑状态
  5. 退出编辑状态时,更新表格内容,并把更新内容同步到其它正在编辑的用户
  6. 显示当前同时编辑的用户数

按照常规的思路,能够提炼出下面这些数据:数据库

  1. 表格的行数和列数
  2. 选区的起止位置信息
  3. 一个表示哪一个单元格正在被编辑的状态
  4. 表格内容,一个二维数组
  5. 当前同时编辑表格的用户数

若是咱们能让这些数据在正确的时间表示正确的值的话,咱们就能够得出正确的效果了。按照常规方法,咱们能够监听各类事件,而后修改上面这些数据,从新渲染。这里 RxJS 用的是另外一种思路,将软件开发比做天然水源的运输和过滤处理,RxJS 不是大天然的搬运工(更不生产水),RxJS 是大天然的流水线,只要流水线建成,水会本身流进流水线,出来的时候就是能直接饮用的水了。在软件开发中想建这样的流水线就须要考虑如何将输入转换成须要的数据,RxJS 为咱们提供了建设流水线的基础能力,好比对数据源和事件的封装与流操做符。编程

最后咱们只须要订阅这个流进行渲染就行了 stream$.subscribe(renderFn)后端

流水线

表格的行数和列数

RxJS 能够封装静态数据,若是有一天这个静态数据须要改成从后端获取,这种包装的价值就体现出来了,由于渲染代码始终从 subscribe 获取数据,不关心数据是同步的仍是异步的。数组

const tableFrame$ = Rx.Observable.of([ROW_COUNT, COLUMN_COUNT]);
复制代码

选区的起止位置信息

效果图以下,咱们须要鼠标按下时的位置和鼠标移动过程当中的位置,直到鼠标松开。

selection

这个功能涉及的事件类型比较多,转换过程相对复杂一些,能够用 Marble 图来表示这个过程。

mousedown                        mouseup
       ↓switchMap                      ↑takeUntil
---mousemove--mousemove--mousemove--mousemove-----|-->
                 map(getPosition)
------pos1-------pos2-------pos3-------pos4-------|-->
        distinctUntilChanged(isPositionEqual)
------pos1-------pos2------------------pos4-------|-->
                      scan
-------------------------------------posRange-----|-->
复制代码

首先咱们想让每一个鼠标事件都进入流水线,RxJS 提供了 fromEvent 的包装方法将其包装成“流”,而后能够看到这里使用了不少流操做符,如 swithMap、takeUntil、map 和 scan 等等。“2 号流水线”的代码实现以下。

mousedown$
    .switchMap(() => mousemove$.takeUntil(mouseup$))
    .map(e => getPosition(e.target))
    .distinctUntilChanged((p, q) => isPositionEqual(p, q))
    .scan((acc, pos) => {
        if (!acc) {
            return { startRow: pos.row, startColumn: pos.column, endRow: pos.row, endColumn: pos.column };
        } else {
            return Object.assign(acc, { endRow: pos.row, endColumn: pos.column });
        }
    }, null);
复制代码

单元格正在被编辑的状态

上面提到有两种方式能够进入编辑状态,因此咱们要打造一个由两条分支汇聚到一块儿的一条流水线,一个关键的流操做符是 merge。

第一个分支是连续两次点击同一个单元格,这个单元格就会进入编辑状态。入门了 RxJS 后,实际上代码就能够解释其自身的功能的。为了正在学习的同窗理解,在贴代码以前先说明一下 bufferCount 这个运算符的功能,使用 Marble:

---1-------2-------3-------4------|-->
         bufferCount(2, 1)
-----------[1, 2]--[2, 3]--[3, 4]-|-->
复制代码

接下来上代码:

const click$ = Rx.Observable.fromEvent(table, 'click').filter(e => e.target.nodeName === 'TD');
const doubleClick$ = click$
    .bufferCount(2, 1)
    .filter(([e1, e2]) => e1.target.id === e2.target.id)
    .map(([e]) => e);
复制代码

第二个分支是在一个单元格上按下键盘按键,进入编辑状态。操做符都不须要(须要给单元格设置 tabindex 属性):

const keyDown$ = Rx.Observable.fromEvent(table, 'keydown').filter(e => e.target.nodeName === 'TD');
复制代码

两个分支都有了,让它们合并也很简单:

doubleClick$.merge(keyDown$)
复制代码

表格内容

表格内容的变化也有两个途径,一个是当前用户的编辑,另外一个是其它用户的编辑。

获得当前用户输入的值很简单,对于来自于其它用户输入的值,这里搭建了一个简单的 websocket 服务,当一个用户修改了一个单元格的值后,就经过服务器向其余正在编辑的用户广播更新。socket.io 这个库使用很是方便,和 RxJS 结合得也很是好。好比咱们能够用下面的方法未来自其余用户的数据封装成一个流:

const socket = io();
const dataSync$ = Rx.Observable.fromEvent(socket, 'sync');
复制代码

当前同时编辑表格的用户数

这个用户数是服务端维护的,也须要 websocket 来实时地将用户数推送给前端。

const socket = io();
const dataSync$ = Rx.Observable.fromEvent(socket, 'uid');
复制代码

使用数据

前面说了 RxJS 解决了从输入到 store 的复杂度,那数据怎么用就和 RxJS 不要紧了,这个例子使用了原生 DOM 操做将数据渲染成 UI,固然也可使用一些前端框架来实现这个过程。

最后

学习 RxJS 须要转换思想,其中一部分来源于函数式的编程思想。就像从 jQuery 转到 angular 同样,习惯了原来的写法,这个转换的过程就会至关痛苦。在写这个例子的时候,思想就有点转变不过来,总想着搞一个状态,而后修改这个状态。除了转换思想外,另外一个难点是决定何时用什么运算符,着实须要费一番功夫。

若是把源码中用于渲染的代码去掉,只看 RxJS 的实现部分,能够发现代码结构十分单一且一致,就像乐高积木同样,无论多么复杂的逻辑,均可以经过组合来实现,这留给咱们很大的想象空间,RxJS 的魅力也在于此。

相关文章
相关标签/搜索