這是 SocialCalc 在運行過程中的畫面:
底下則是它的類型圖:
相較於 WikiCalc,伺服器的角色已大幅減輕。現在伺服器只需要負責回應 HTTP GET 請求,提供整份表格內容的序列化字串即可。瀏覽器在收到資料後,所有計算、變動追蹤,以及使用者的互動都是通過 JavaScript 達成的。
JavaScript 元件在設計上使用了層次式 MVC(模型/視圖/控制器)樣式,每個類型都只專注於某部分的功能:
- Sheet 是資料模型,代表試算表在記憶體中的結構。
模型中包含從座標指向 Cell 物件的字典,每個物件代表一個儲存格。空儲存格所在的座標不需要有對映的物件,因此完全不占用記憶體。
- Cell 代表儲存格的內容和格式。
下面列出的是一些常見的 Cell 物件屬性:
datatype t datavalue 1Q84 color black bgcolor white font italic bold 12pt Ubuntu comment Ichi-Kyu-Hachi-Yon
RenderContext 用於實現視圖,需要負責將表格繪製為相應的 DOM 物件。
- TableControl 則是主控制器,負責接收滑鼠和鍵盤事件。
在接收到視圖事件,例如滾動和調整大小後,就會對相關 RenderContext 物件進行更新。
如果收到應用於試算表內容的更新事件,則會在試算表的指令佇列中加入新的指令。
SpreadSheetControl 負責繪制頂層界面,包括工具欄、狀態欄、對話框,以及顏色選擇器。
- SpreadSheetViewer 是另一套頂層界面,主要提供唯讀的互動視圖。
我們採用了基於類型的輕量級物件系統,僅使用了簡單的組成/委派機制,完全沒有使用繼承或物件原型。所有符號都位於 SocialCalc.*
名稱空間裡,以避免命名衝突。
對試算表的全部更新都需要通過 ScheduleSheetCommands
方法進行,因此需要通過指令字串來代表編輯操作。常用的指令如下:
set sheet defaultcolor blue set A width 100 set A1 value n 42 set A2 text t Hello set A3 formula A1*2 set A4 empty set A5 bgcolor green merge A1:B2 unmerge A1 erase A2 cut A3 paste A4 copy A5 sort A1:B9 A up B down name define Foo A1:A5 name desc Foo Used in formulas like SUM(Foo) name delete Foo startcmdextension UserDefined args
嵌入 SocialCalc 的應用程序可以自行定義額外的指令,只需要將命名的回調函數添加到 SocialCalc.SheetCommandInfo.CmdExtensionCallbacks
物件,即可使用 startcmdextension
指令進行呼叫。
指令的循環運行
為改善回應速度,SocialCalc 會在背景執行全部的重算和 DOM 更新,因此在使用者對多個儲存格進行修改時,試算表引擎會同時在指令佇列裡處理先前的改動。
在運行指令時,TableEditor 物件會將其 busy
屬性設為 true
;後續指令則需加入到 deferredCommands
佇列,以確保指令能循序執行。事件循環看起來像這樣:
如上圖所示,Sheet 物件會持續發送 StatusCallback
事件,以提醒使用者當前的指令執行狀態,這一過程都可以分為下列四個步驟:
- 執行指令
啟動時發送
cmdstart
,執行完成後則發送cmdend
。如果指令間接更改了某儲存格的值,則進入重算步驟。
否則,如果指令更改了一個或多個已在螢幕上顯示的儲存格的視覺外觀,則進入繪製步驟。
如果上述情況都不符合(例如在使用
copy
指令時),則跳到位置計算步驟。 - 重算(如果需要的話)
啟動時發送
calcstart
,在檢查儲存格的依存鏈時每隔 100ms 發送calcorder
,完成檢查時則發送calccheckdone
,並在所有受影響儲存格獲得重算後的值後發送calcfinished
。這一步驟之後總是需要執行繪製步驟。
- 繪製(如果需要的話)
啟動時發送
schedrender
,如果使用格式化後的儲存格更新了<table>
DOM 物件,則發送renderdone
。這一步驟之後總是需要執行位置計算步驟。
- 位置計算
啟動時發送
schedposcalc
,並在更新了滾動條、目前儲存格游標,以及 TableEditor 的其他視覺組件後發送doneposcalc
。
因為所有指令在執行後即被保存,因此等於對所有操作都可獲得執行紀錄。為實現稽核追蹤,Sheet.CreateAuditString
方法會傳回以換行隔開的字符串,每行內容對應到一個指令的相關記錄。
ExecuteSheetCommand
還可為執行的每個指令創建還原指令。舉例來說,如果儲存格 A1 包含 Foo
,而使用者執行了 set A1 text Bar
,則還原指令set A1 text Foo
會被推送到 UndoStack 裡。如果使用者進行還原操作,則會通過執行還原指令來讓 A1 的內容回到原先的值。
試算表編輯器
接著一起來看看 TableEditor 層。該層可計算 RenderContext 的螢幕顯示內容,並通過兩個 TableControl 實例來管理水平/垂直捲動軸。
視圖層主要由 RenderContext 類型負責。與 WikiCalc 的設計不同的是,我們並非將每個儲存格對映到一個 <td>
元素,而是直接創建固定大小的 <table>
,使它充分填滿瀏覽器的可視區域,並為其預先填充 <td>
元素。
當使用者通過自定義的滾動條拖動試算表後,可以動態更新預先繪制的 <td>
元素的 innerHTML
。這意味著在很多常見情況下,我們並不需要創建/刪除任何 <tr>
或 <td>
元素,因此大幅提升了回應速度。
因為 RenderContext 只繪製可視區域,所以無論試算表多大,執行效能也不受影響。
TableEditor 還包含一個 CellHandles 物件,可用於處理附加到目前儲存格(即ECell)右下角的圓盤形填充/移動/滑動選單指令:
輸入框則由兩個類型負責管理:InputBox 和 InputEcho。前者主要管理網格上的編輯行,後者主要用於在輸入內容時提供及時更新的預覽層,並覆蓋ECell的內容。
通常,SocialCalc 引擎只有在打開試算表並進行編輯時,以及將內容保存回伺服器時才需要與伺服器通訊。因此,Sheet.ParseSheetSave
方法可在Sheet 物件中解析儲存格式字串,而 Sheet.CreateSheetSave
方法可將 Sheet 物件序列化為儲存格式。
通過使用 URL,公式可引用任何遠端試算表中的值。recalc
指令會重新抓取被引用的外部電子試算表,並使用 Sheet.ParseSheetSave
對其進行解析,然後將其存儲在暫存區中,這樣使用者即可在不重新抓取內容的情況下,直接引用相同遠端表格中其他儲存格的內容。
儲存格式
儲存格式是一種標準的 MIME multipart/mixed
格式,主要由四個 text/plain; charset=UTF-8
部件組成,每部件包含以換行隔開的文字,並用冒號劃分資料欄位。這些部件包括:
meta
部件列出其他部件的型別。sheet
部件列出每個儲存格的格式和功能、每個列的寬度(如果不是預設寬度)、表格的預設格式,以及該試算表中用到的字體、顏色,及邊框列表。- 可選的
edit
部件可保存 TableEditor 的編輯狀態,包括 ECell 的最後一個位置,以及行/列窗格的固定大小。 - 可選的
audit
部件包含上一次編輯會話中執行過的指令歷史記錄。
舉例來說,下面是一個包含三個儲存格的試算表,A1 作為 ECell,其內容為 1874
,A2 中是公式 2^2*43
,A3 中的公式 SUM(Foo)
則顯示為粗體字,代表命名範圍從 Foo
到 A1:A2
:
這份試算表經過序列化後,儲存格式會像這樣:
socialcalc:version:1.0 MIME-Version: 1.0 Content-Type: multipart/mixed; boundary=SocialCalcSpreadsheetControlSave --SocialCalcSpreadsheetControlSave Content-type: text/plain; charset=UTF-8 # SocialCalc Spreadsheet Control Save version:1.0 part:sheet part:edit part:audit --SocialCalcSpreadsheetControlSave Content-type: text/plain; charset=UTF-8 version:1.5 cell:A1:v:1874 cell:A2:vtf:n:172:2^2*43 cell:A3:vtf:n:2046:SUM(Foo):f:1 sheet:c:1:r:3 font:1:normal bold * * name:FOO::A1\cA2 --SocialCalcSpreadsheetControlSave Content-type: text/plain; charset=UTF-8 version:1.0 rowpane:0:1:14 colpane:0:1:16 ecell:A1 --SocialCalcSpreadsheetControlSave Content-type: text/plain; charset=UTF-8 set A1 value n 1874 set A2 formula 2^2*43 name define Foo A1:A2 set A3 formula SUM(Foo) --SocialCalcSpreadsheetControlSave--
上述格式在設計上可讓人直接讀取,並且也很容易通過程式生成。因此,Drupal 項目的 Sheetnode 插件即可使用 PHP 在此格式以及其他流行的試算表格式,例如 Excel (.xls
) 及 OpenDocument (.ods
) 之間進行轉換。
至此,我們已經簡要介紹了 SocialCalc 各個元件和組成方式。接下來,讓我們透過兩個實際的範例,一起來看看如何擴展 SocialCalc 的功能。
(未完,待續。)