电子表格的历史跨越了 30 多年。第一个电子表格程序 VisiCalc 由 Dan Bricklin 于 1978 年构思并于 1979 年发布。最初的概念非常简单:一个在两个维度上无限延伸的表格,其单元格填充了文本、数字和公式。公式由普通的算术运算符和各种内置函数组成,每个公式都可以使用其他单元格的当前内容作为值。
虽然这个比喻很简单,但它有很多应用:会计、库存和列表管理只是一些例子。可能性几乎是无限的。所有这些用途使 VisiCalc 成为个人电脑时代第一个“杀手级应用程序”。
在接下来的几十年里,Lotus 1-2-3 和 Excel 等后继产品做出了渐进的改进,但核心比喻保持不变。大多数电子表格都存储为磁盘文件,并在打开以进行编辑时加载到内存中。在基于文件的模型下,协作尤其困难。
幸运的是,一种新的协作模型出现了,它以优雅的简洁性解决了这些问题。它就是维基模型,由 Ward Cunningham 于 1994 年发明,并于 2000 年代初由维基百科普及。
维基模型没有使用文件,而是使用服务器托管的页面,这些页面可以在浏览器中编辑,无需特殊软件。这些超文本页面可以轻松地相互链接,甚至可以包含其他页面的部分以形成更大的页面。所有参与者默认情况下都会查看和编辑最新版本,版本历史由服务器自动管理。
受维基模型的启发,Dan Bricklin 于 2005 年开始开发 WikiCalc。它旨在将维基的易于创作和多人编辑功能与电子表格熟悉的视觉格式和计算比喻相结合。
第一个版本的 WikiCalc (图 19.1) 有几个特点使其与当时的其他电子表格区别开来。
图 19.1:WikiCalc 1.0 界面
图 19.2:WikiCalc 组件
图 19.3:WikiCalc 流程
WikiCalc 1.0 的内部架构 (图 19.2) 和信息流 (图 19.3) 故意保持简单,但仍然很强大。能够从几个较小的电子表格中组成一个主电子表格被证明特别方便。例如,想象一个场景,每个销售人员都在一个电子表格页面中保留数字。然后,每个销售经理将他们下属的数字汇总到一个区域电子表格中,然后销售副总裁将区域数字汇总到一个顶层电子表格中。
每次更新其中一个单独的电子表格时,所有汇总电子表格都可以反映更新。如果有人需要更多详细信息,他们只需点击查看该电子表格后面的电子表格。这种汇总功能消除了在多个地方更新数字的冗余和容易出错的工作,并确保所有信息的视图保持最新。
为了确保重新计算是最新的,WikiCalc 采用了一种瘦客户端设计,将所有状态信息保留在服务器端。每个电子表格在浏览器中都表示为一个 <table>
元素;编辑一个单元格将向服务器发送一个 ajaxsetcell
调用,然后服务器告诉浏览器哪些单元格需要更新。
不出所料,这种设计依赖于浏览器和服务器之间的快速连接。当延迟很高时,用户将开始注意到在更新单元格和看到其新内容之间频繁出现“正在加载...”消息,如 图 19.4 所示。对于通过调整输入并期望实时看到结果来交互式编辑公式的用户来说,这尤其是一个问题。
图 19.4:正在加载消息
此外,由于 <table>
元素的尺寸与电子表格相同,100×100 网格将创建 10,000 个 <td>
DOM 对象,这会占用浏览器的内存资源,进一步限制了页面的大小。
由于这些缺点,虽然 WikiCalc 作为运行在 localhost 上的独立服务器很有用,但将其嵌入到基于 Web 的内容管理系统中并不实用。
2006 年,Dan Bricklin 与 Socialtext 合作开始开发 SocialCalc,这是一个基于一些原始 Perl 代码的 WikiCalc 的完全用 Javascript 重写的版本。
这次重写针对大型分布式协作,并试图提供更像桌面应用程序的外观和感觉。其他设计目标包括
经过三年的开发和各种测试版发布,Socialtext 于 2009 年发布了 SocialCalc 1.0,成功地满足了设计目标。现在让我们看看 SocialCalc 系统的架构。
图 19.5:SocialCalc 界面
图 19.5 和 图 19.6 分别显示了 SocialCalc 的界面和类。与 WikiCalc 相比,服务器的作用已大大减少。它唯一的职责是通过在保存格式中提供整个序列化的电子表格来响应 HTTP GET 请求;浏览器一旦接收到数据,所有计算、更改跟踪和用户交互现在都在 Javascript 中实现。
图 19.6:SocialCalc 类图
Javascript 组件是使用分层的 MVC(模型/视图/控制器)样式设计的,每个类都专注于一个单一方面。
数据类型 |
t |
数据值 |
1Q84 |
颜色 |
黑色 |
背景色 |
白色 |
字体 |
斜体加粗 12pt Ubuntu |
评论 |
一球八四 |
表 19.1:单元格内容和格式
我们采用了一个最小的基于类的对象系统,使用简单的组合/委派,并且没有使用继承或对象原型。所有符号都放在 SocialCalc.*
命名空间下,以避免命名冲突。
电子表格上的每个更新都通过 ScheduleSheetCommands
方法进行,该方法接受一个表示编辑的命令字符串。(一些常用命令显示在 表 19.2 中。)嵌入 SocialCalc 的应用程序可以通过将命名回调添加到 SocialCalc.SheetCommandInfo.CmdExtensionCallbacks
对象中,并使用 startcmdextension
命令调用它们来定义自己的额外命令。
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 |
表 19.2:SocialCalc 命令
为了提高响应能力,SocialCalc 在后台执行所有重新计算和 DOM 更新,因此用户可以在引擎赶上命令队列中早期更改时继续对多个单元格进行更改。
图 19.7:SocialCalc 命令运行循环
当一个命令正在运行时,TableEditor
对象将其 busy
标志设置为 true;随后的命令随后被推入 deferredCommands
队列,确保按顺序执行。如 图 19.7 中的事件循环图所示,Sheet 对象会不断发送 StatusCallback
事件,以通知用户命令执行的当前状态,通过以下四个步骤
cmdstart
,命令执行完成后发送 cmdend
。如果该命令间接更改了单元格的值,则进入 Recalc 步骤。否则,如果该命令更改了屏幕上一个或多个单元格的视觉外观,则进入 Render 步骤。如果以上两种情况都不适用(例如使用 copy
命令),则跳到 PositionCalculations 步骤。calcstart
,检查单元格的依赖链时每 100 毫秒发送一次 calcorder
,检查完成后发送 calccheckdone
,所有受影响的单元格收到其重新计算的值后发送 calcfinished
。此步骤始终紧随 Render 步骤之后。schedrender
,<table>
元素使用格式化的单元格更新后发送 renderdone
。此步骤始终紧随 PositionCalculations 之后。schedposcalc
,更新滚动条、当前可编辑的单元格光标和其他 TableEditor
的视觉组件后发送 doneposcalc
。由于所有命令都在执行时保存,因此我们自然会获得所有操作的审计日志。Sheet.CreateAuditString
方法提供一个以换行符分隔的字符串作为审计跟踪,每个命令占一行。
ExecuteSheetCommand
还为它执行的每个命令创建一个撤销命令。例如,如果单元格 A1 包含“Foo”,用户执行 set A1 text Bar
,则一个撤销命令 set A1 text Foo
被推入撤销堆栈。如果用户点击撤销,则执行撤销命令以将 A1 还原为其原始值。
现在让我们看一下 TableEditor 层。它计算其 RenderContext
的屏幕坐标,并通过两个 TableControl
实例管理水平/垂直滚动条。
图 19.8:TableControl 实例管理滚动条
视图层由RenderContext
类处理,与WikiCalc的设计也有所不同。我们不再将每个单元格映射到一个<td>
元素,而是简单地创建一个固定大小的<table>
,它适合浏览器的可见区域,并预先填充<td>
元素。
当用户通过我们自定义绘制的滚动条滚动电子表格时,我们动态更新预先绘制的<td>
元素的innerHTML
。这意味着在许多常见情况下,我们不需要创建或销毁任何<tr>
或<td>
元素,这大大加快了响应时间。
由于RenderContext
只渲染可见区域,因此Sheet对象的尺寸可以任意大,而不会影响其性能。
TableEditor
还包含一个CellHandles
对象,它实现了附加到当前可编辑单元格(称为ECell)右下角的径向填充/移动/滑动菜单,如图 19.9所示。
图 19.9:当前可编辑单元格,称为ECell
输入框由两个类管理:InputBox
和InputEcho
。前者管理网格上方的编辑行,而后者显示一个边输入边更新的预览层,覆盖ECell的内容(图 19.10)。
图 19.10:输入框由两个类管理
通常,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
部分包含上次编辑会话中执行的命令历史记录。例如,图 19.11显示了一个包含三个单元格的电子表格,其中A1中的1874
是ECell,A2中的公式2^2*43
,以及以粗体渲染的A3中的公式SUM(Foo)
,它引用了A1:A2
上的命名范围Foo
。
图 19.11:包含三个单元格的电子表格
电子表格的序列化保存格式如下所示
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的真实示例。
我们将要看的第一个示例是使用wiki标记增强SocialCalc的文本单元格,以便在表格编辑器中显示其富文本渲染(图 19.12)。
图 19.12:表格编辑器中的富文本渲染
我们在SocialCalc 1.0发布后不久就添加了此功能,以解决使用统一语法插入图像、链接和文本标记的流行请求。由于Socialtext已经拥有一个开源wiki平台,因此将该语法重新用于SocialCalc也是很自然的事。
为了实现这一点,我们需要一个用于textvalueformat
的自定义渲染器text-wiki
,并将文本单元格的默认格式更改为使用它。
你可能会问,什么是textvalueformat
?请继续阅读。
在SocialCalc中,每个单元格都有一个datatype
和一个valuetype
。包含文本或数字的数据单元格对应于文本/数值类型,而包含datatype="f"
的公式单元格可能会生成数值或文本值。
请记住,在渲染步骤中,Sheet
对象会从每个单元格生成HTML。它通过检查每个单元格的valuetype
来做到这一点:如果它以t开头,则单元格的textvalueformat
属性决定如何进行生成。如果它以n
开头,则使用nontextvalueformat
属性代替。
但是,如果单元格的textvalueformat
或nontextvalueformat
属性没有明确定义,则会从其valuetype
中查找默认格式,如图 19.13所示。
图 19.13:值类型
对text-wiki
值格式的支持是在SocialCalc.format_text_for_display
中编码的
if (SocialCalc.Callbacks.expand_wiki && /^text-wiki/.test(valueformat)) { // do general wiki markup displayvalue = SocialCalc.Callbacks.expand_wiki( displayvalue, sheetobj, linkstyle, valueformat ); }
我们不会在format_text_for_display
中内联wiki到HTML的扩展器,而是在SocialCalc.Callbacks
中定义一个新的钩子。这是整个SocialCalc代码库中推荐的风格;它通过使插入不同的wikitext扩展方法成为可能,以及保持与不需要此功能的嵌入器兼容性来提高模块化。
接下来,我们将使用Wikiwyg1,这是一个提供wikitext和HTML之间双向转换的Javascript库。
我们通过获取单元格的文本,运行它通过Wikiwyg的wikitext解析器及其HTML发射器来定义expand_wiki
函数
var parser = new Document.Parser.Wikitext(); var emitter = new Document.Emitter.HTML(); SocialCalc.Callbacks.expand_wiki = function(val) { // Convert val from Wikitext to HTML return parser.parse(val, emitter); }
最后一步是在电子表格初始化后立即调度set sheet defaulttextvalueformat text-wiki
命令
// We assume there's a <div id="tableeditor"/> in the DOM already var spreadsheet = new SocialCalc.SpreadsheetControl(); spreadsheet.InitializeSpreadsheetControl("tableeditor", 0, 0, 0); spreadsheet.ExecuteCommand('set sheet defaulttextvalueformat text-wiki');
总之,渲染步骤现在的工作原理如图 19.14所示。
图 19.14:渲染步骤
就是这样!增强后的SocialCalc现在支持一组丰富的wiki标记语法
*bold* _italic_ `monospace` > indented text * unordered list # ordered list "Hyperlink with label"<http://softwaregarden.com/> {image: http://www.socialtext.com/static/logo.png}
尝试在A1中输入*bold* _italic_ `monospace`
,你会看到它被渲染为富文本(图 19.15)。
图 19.15:Wikywyg示例
我们将要探讨的下一个示例是在共享电子表格上进行多人实时编辑。这乍一看可能很复杂,但由于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
回调函数。一旦到位,相同的命令将在连接到同一电子表格的所有用户上执行。
当此功能在2009年由SEETA的Sugar Labs3首次为OLPC(每孩一机2)实施时,broadcast
函数使用XPCOM调用构建到D-Bus/Telepathy,这是OLPC/Sugar网络的标准传输(见图 19.16)。
图 19.16:OLPC实现
这工作得相当好,使同一Sugar网络中的XO实例能够在共同的SocialCalc电子表格上进行协作。然而,它既特定于Mozilla/XPCOM浏览器平台,也特定于D-Bus/Telepathy消息传递平台。
为了使其跨浏览器和操作系统工作,我们使用Web::Hippie
4框架,这是JSON-over-WebSocket的一种高级抽象,具有方便的jQuery绑定,其中MXHR(多部分XML HTTP请求5)作为WebSocket不可用的备用传输机制。
对于安装了Adobe Flash插件但没有原生WebSocket支持的浏览器,我们使用web_socket.js
6项目的Flash模拟WebSocket,它通常比MXHR更快,更可靠。操作流程如图 19.17所示。
图 19.17:跨浏览器流程
客户端的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 sheet = SocialCalc.CurrentSpreadsheetControlObject.context.sheetobj; sheet.ScheduleSheetCommands( d.data.cmdstr, d.data.saveundo, true // isRemote = true ); break; });
虽然这工作得很好,但仍有两个问题需要解决。
第一个是命令执行顺序中的竞争条件:如果用户A和B同时执行影响相同单元格的操作,然后接收并执行从另一个用户广播的命令,它们最终会处于不同的状态,如图 19.18所示。
图 19.18:竞争条件冲突
我们可以使用SocialCalc内置的撤消/重做机制来解决这个问题,如图 19.19所示。
图 19.19:竞争条件冲突解决
用于解决冲突的过程如下。当客户端广播一个命令时,它会将该命令添加到一个Pending队列中。当客户端接收到一个命令时,它会将远程命令与Pending队列进行比较。
如果Pending队列为空,则该命令将简单地作为远程操作执行。如果远程命令与Pending队列中的命令匹配,则本地命令将从队列中删除。
否则,客户端会检查是否有任何排队的命令与收到的命令冲突。如果有冲突的命令,客户端会首先Undo
这些命令,并将它们标记为稍后Redo
。在撤消冲突的命令(如果有)之后,远程命令将照常执行。
当从服务器接收到标记为重做的命令时,客户端将再次执行它,然后将其从队列中删除。
即使解决了竞争条件,意外覆盖另一个用户正在编辑的单元格仍然不是最佳做法。一个简单的改进是让每个客户端将其光标位置广播给其他用户,以便每个人都能看到哪些单元格正在被处理。
为了实现这个想法,我们在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;图 19.20显示了当四个人在同一个电子表格上进行编辑时屏幕的样子。
图 19.20:四个用户编辑一个电子表格
我们在2009年10月19日发布了SocialCalc 1.0,正好是VisiCalc首次发布的30周年纪念日。与我在Socialtext的同事在Dan Bricklin的指导下进行合作的经历对我来说非常宝贵,我想分享我在这段时间里学到的一些经验教训。
在 [Bro10] 中,弗雷德·布鲁克斯认为,在构建复杂系统时,如果我们关注一个连贯的 *设计理念*,而不是衍生性表示,那么对话会更直接。根据布鲁克斯的说法,这种连贯设计理念的形成最好保留在一个人的脑海中。
由于概念完整性是优秀设计的最重要的属性,而这种完整性来自于一两个人 *uno animo* 的工作,因此明智的管理者会大胆地将每个设计任务委托给一位有天赋的设计总监。
以 SocialCalc 为例,拥有 Tracy Ruggles 作为我们首席用户体验设计师,是该项目朝着共同愿景收敛的关键。由于 SocialCalc 的底层引擎非常灵活,功能蔓延的诱惑非常现实。Tracy 利用设计草图进行交流的能力,真正帮助我们以对用户直观的方式展现功能。
在我加入 SocialCalc 项目之前,已经进行了两年多的设计和开发,但我能在不到一周的时间内赶上并开始贡献,仅仅因为 *所有东西都在维基中*。从最早的设计笔记到最新的浏览器支持矩阵,整个过程都被记录在维基页面和 SocialCalc 电子表格中。
阅读项目的工作空间让我快速地与其他人达成共识,而无需通常与引导新团队成员相关的繁琐的指导。
在传统的开源项目中,这将是不可能的,因为大多数对话发生在 IRC 和邮件列表上,维基(如果有的话)只用于文档和指向开发资源的链接。对于新手来说,从无结构的 IRC 日志和邮件存档中重建上下文要困难得多。
Ruby on Rails 的创建者大卫·海尼迈尔·汉森,在他第一次加入 37signals 时,曾评论过分布式团队的好处。“哥本哈根和芝加哥之间的七个时区实际上意味着我们用很少的干扰完成了许多工作。”在台北和帕洛阿尔托之间有九个时区,这对我们 SocialCalc 的开发过程来说也是如此。
我们经常在 24 小时内完成一个完整的“设计-开发-QA 反馈”循环,每个环节都占据一个人在当地白天工作的 8 个小时。这种异步协作风格迫使我们制作自描述性工件(设计草图、代码和测试),反过来极大地提高了我们彼此之间的信任。
在我 2006 年为 CONISLI 会议做的主题演讲 [Tan06] 中,我总结了我领导一个分布式团队实施 Perl 6 语言的经验,并得出了几点观察。其中,*始终拥有路线图*、*宽恕 > 许可*、*消除死锁*、*寻求想法,而非共识* 以及 *用代码勾勒想法* 对于小型分布式团队特别相关。
在开发 SocialCalc 时,我们非常小心地将知识分布在团队成员之间,并实行协作式代码所有权,因此没有人会成为关键瓶颈。
此外,我们通过实际编写替代方案来预先解决争议,以探索设计空间,并且不害怕在出现更好的设计时替换完全可用的原型。
这些文化特征帮助我们在没有面对面互动的情况下培养了期待和同志情谊,将政治斗争降至最低,并使在 SocialCalc 上的工作变得非常有趣。
在加入 Socialtext 之前,我一直倡导“将测试与规范交织在一起”的方法,这可以从 Perl 6 规范7 中看出,我们用官方测试套件注释了语言规范。然而,正是 SocialCalc 的 QA 团队的 Ken Pier 和 Matt Heusser 真正让我大开眼界,看到了如何将它提升到一个新的水平,将测试提升到 *可执行规范* 的地位。
在 [GR09] 的第 16 章中,Matt 解释了我们的故事测试驱动开发流程,如下所示:
工作的基本单元是“故事”,这是一个极其轻量级的需求文档。故事包含对功能的简要描述以及要考虑故事完成所需发生的示例;我们称这些示例为“验收测试”,并用简单的英语描述它们。
在故事的初始阶段,产品负责人会尽力尝试创建验收测试,这些测试会在任何开发人员编写一行代码之前由开发人员和测试人员进行增强。
然后,这些故事测试被翻译成维基测试,这是一种基于表格的规范语言,灵感来自 Ward Cunningham 的 FIT 框架8,它驱动着自动测试框架,例如 Test::WWW::Mechanize
9 和 Test::WWW::Selenium
10。
很难过分强调将故事测试作为表达和验证需求的通用语言所带来的益处。它对于减少误解至关重要,并且几乎消除了我们每月发布的回归。
最后但并非最不重要的一点是,我们为 SocialCalc 选择的开源模式本身就是一个有趣的教训。
Socialtext 为 SocialCalc 创建了通用公共署名许可证11。基于 Mozilla 公共许可证,CPAL 旨在允许原始作者要求在软件的用户界面上显示署名,并且有一个网络使用条款,当衍生作品在网络上由服务托管时,该条款会触发共享类似的条款。
在获得开源倡议12 和自由软件基金会13 的批准后,我们看到 Facebook14 和 Reddit15 等知名网站选择在 CPAL 下发布其平台的源代码,这非常令人鼓舞。
由于 CPAL 是一个“弱版权”许可证,开发人员可以自由地将其与免费软件或专有软件结合使用,并且只需要发布对 SocialCalc 本身的修改。这使各种社区能够采用 SocialCalc,使其变得更加出色。
这个开源电子表格引擎有很多有趣的可能性,如果你能找到一种方法将 SocialCalc 嵌入到你喜欢的项目中,我们非常乐意听到你的消息。
https://github.com/audreyt/wikiwyg-js
http://one.laptop.org/
http://seeta.in/wiki/index.php?title=Collaboration_in_SocialCalc
http://search.cpan.org/dist/Web-Hippie/
http://about.digg.com/blog/duistream-and-mxhr
https://github.com/gimite/web-socket-js
http://perlcabal.org/syn/S02.html
http://fit.c2.com/
http://search.cpan.org/dist/Test-WWW-Mechanize/
http://search.cpan.org/dist/Test-WWW-Selenium/
https://www.socialtext.net/open/?cpal
https://open-source.org.cn/
http://www.fsf.org
https://github.com/facebook/platform
https://github.com/reddit/reddit