我們接下來研究的一個例子,是在共享的試算表裡進行多人即時協同編輯。
這乍看之下也許有點複雜,但是感謝 SocialCalc 的模組化設計,我們只需讓每位使用者將自己的指令傳播給其他參與者執行即可。
為了對本地指令與遠端指令作出區分,我們為 ScheduleSheetCommands
方法增加了 isRemote
參數:
SocialCalc.ScheduleSheetCommands = function(sheet, cmdstr, saveundo, isRemote) { if (SocialCalc.Callbacks.broadcast && !isRemote) { SocialCalc.Callbacks.broadcast('execute', { cmdstr: cmdstr, saveundo: saveundo }); } // ...original ScheduleSheetCommands code here... };
現在只需定義一個合適的 SocialCalc.Callbacks.broadcast
回喚函數,即可讓所有連入此試算表的客戶端執行相同的指令。
當 SEETA Sugar Labs 於 2009 年在 OLPC 上首次實作這項功能時,broadcast
函式是用 XPCOM 框架寫成,並在 OLPC/Sugar 的標準傳輸層 D-Bus/Telepathy 網路上運行:
這樣的運行方式,讓 Sugar 網路裡的 XO 電腦能對共同的 SocialCalc 試算表進行協作,但也只能在 Mozilla/XPCOM 瀏覽器平台與 D-Bus/Telepathy 訊息平台上適用。
跨瀏覽器傳輸
為了達成跨瀏覽器、跨作業系統的目標,我們使用 Web::Hippie 框架,作為 JSON 在 WebSocket 上傳輸的抽象層。它提供方便的 jQuery 綁定,並且當 WebSocket 不適用時,也能利用 MXHR (multipart XMLHttpRequest) 作為備用傳輸機制.
對於安裝了 Adobe Flash 插件但沒有原生 WebSocket 支持的瀏覽器,我們使用 web_socket.js 專案的 Flash WebSocket 模擬器,這通常比 MXHR 更快也更可靠。
運作流程看上去是這樣的:
客戶端的 SocialCalc.Callbacks.broadcast
函式定義如下:
var hpipe = new Hippie.Pipe(); SocialCalc.Callbacks.broadcast = function(type, data) { hpipe.send({ type: type, data: data }); }; $(hpipe).bind("message.execute", function (e, d) { var ss = SocialCalc.CurrentSpreadsheetControlObject; ss.context.sheetobj.ScheduleSheetCommands( d.data.cmdstr, d.data.saveundo, true // isRemote = true ); break; });
儘管這已經能順利運行,我們仍然有兩個問題需要解決。
衝突解決
第一個就是為了執行指令而產生的爭用狀態:如果使用者 A 與 B 同時執行某個影響相同儲存格的操作,之後才接收到與對方傳播出來的指令,那麼雙方將會停在不同的狀態:
我們可以通過 SocialCalc 內置的還原/重作機制來解決這個問題,如下圖所示:
- 當客戶傳播出一個指令時,會將指令添加到待辦佇列。
- 當客戶接收到一個指令時,檢查待辦佇列:
- 如果待辦佇列為空,則直接執行這項遠端指令。
- 如果它符合待辦佇列裡的本地指令,則將它將從佇列中移除。
- 否則,檢查佇列中是否有指令與接收到的指令相衝突:
- 如果存在衝突指令,則先還原這些指令,並將其標記為稍後重作。
- 在還原所有的衝突指令之後,將遠端指令按正常狀態執行。
- 當從伺服器上接收到標記為重做的指令時,客戶端將再次執行指令,再從佇列中將其移除。
遠端游標
盡管爭用狀態已經得到解決,但「偶爾會覆蓋掉其他使用者正在編輯的儲存格」這樣的不理想情形還是存在。我們可以更進一步,將每位使用者的游標位置傳播給其他使用者,讓每個人都能看到有哪些儲存格正在編輯。
為了實做這一想法,我們為 MoveECellCallback
事件添加了另一個 broadcast
處理程序:
editor.MoveECellCallback.broadcast = function(e) { hpipe.send({ type: 'ecell', data: e.ecell.coord }); }; $(hpipe).bind("message.ecell", function (e, d) { var cr = SocialCalc.coordToCr(d.data); var cell = SocialCalc.GetEditorCellElement( editor, cr.row, cr.col ); // ...decorate cell with styles specific // to the remote user(s) on it... });
在試算表中標記儲存格焦點時,通常會使用帶有顏色的邊框。但是,該儲存格也許已經有自己的 border
屬性了。而由於 border
為單一顏色,因此在相同的儲存格只能表現一個游標。
因此,在支援 CSS3 的瀏覽器上,我們使用 box-shadow
功能來表現多個游標:
/* Two cursors on the same cell */ box-shadow: inset 0 0 0 4px red, inset 0 0 0 2px green;
如此一來,當四人編輯同一個試算表時,螢幕看起來會像這樣:
(未完,待續。)
Recent Comments