用 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 的魅力也在於此。

相關文章
相關標籤/搜索