我們接下來研究的一個例子,是在共享的試算表裡進行多人即時協同編輯。
這乍看之下也許有點複雜,但是感謝 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