开源应用程序架构(卷 2)
MediaWiki

Sumana Harihareswara 和 Guillaume Paumier

从一开始,MediaWiki 就是专门为维基百科开发的软件。开发人员致力于促进第三方用户重复使用,但维基百科的影响和偏见在 MediaWiki 的整个历史中都塑造了其架构。

维基百科是全球前十的网站之一,目前每月约有 4 亿独立访问者。它每秒超过 10 万次点击。维基百科没有通过广告获得商业支持;它完全由非营利组织维基媒体基金会支持,该基金会依赖捐赠作为其主要资金来源。这意味着 MediaWiki 不仅要运行一个前十的网站,还要以微薄的预算来做到这一点。为了满足这些需求,MediaWiki 对性能、缓存和优化有很高的偏见。在维基百科上无法启用的昂贵功能要么被还原,要么通过配置变量禁用;在性能和功能之间存在着无尽的平衡。

维基百科对 MediaWiki 架构的影响并不局限于性能。与通用内容管理系统 (CMS) 不同,MediaWiki 最初是为一个非常具体的用途而编写的:支持一个社区,该社区在一个开放平台上创建和整理可自由重复使用的知识。这意味着,例如,MediaWiki 不包括企业 CMS 中常见的常规功能,例如发布工作流程或访问控制列表,但确实提供了一系列工具来处理垃圾邮件和破坏行为。

因此,从一开始,不断发展的维基百科参与者社区的需求和行为就影响了 MediaWiki 的发展,反之亦然。MediaWiki 的架构多次由社区发起或要求的举措驱动,例如创建维基共享资源或“标记修订”功能。开发人员做出了重大架构变更,因为维基百科人使用 MediaWiki 的方式使之成为必要。

MediaWiki 也通过从一开始就成为开源软件而获得了坚实的外部用户群。第三方重复使用者知道,只要像维基百科这样知名度很高的网站使用 MediaWiki,该软件就会得到维护和改进。MediaWiki 曾经非常专注于维基媒体网站,但已经努力使其更加通用,更好地满足这些第三方用户的需求。例如,MediaWiki 现在附带了一个出色的基于 Web 的安装程序,使安装过程比以前通过命令行完成且软件包含为维基百科硬编码的路径时痛苦得多。

尽管如此,MediaWiki 仍然是维基百科的软件,这一点在它的整个历史和架构中都体现出来。

本章的组织结构如下

12.1. 历史概述

第一阶段:UseModWiki

维基百科于 2001 年 1 月推出。当时,它主要是一个尝试为 Nupedia 提高内容产量的实验,Nupedia 是吉米·威尔士创建的免费内容但经同行评审的百科全书。由于它是一个实验,维基百科最初由 UseModWiki 提供支持,UseModWiki 是一个现有的 GPL wiki 引擎,用 Perl 编写,使用 CamelCase 并将所有页面存储在没有更改历史记录的单个文本文件中。

很快发现,CamelCase 并不适合命名百科全书条目。2001 年 1 月下旬,UseModWiki 开发人员兼维基百科参与者克利福德·亚当斯在 UseModWiki 中添加了一项新功能:自由链接;即,能够使用特殊语法(双方括号)链接到页面,而不是自动 CamelCase 链接。几周后,维基百科升级到支持自由链接的 UseModWiki 新版本,并启用了它们。

虽然这个初始阶段并非关于 MediaWiki 本身,但它提供了一些背景信息,并表明,即使在 MediaWiki 创建之前,维基百科就开始塑造支持它的软件的功能。UseModWiki 也影响了 MediaWiki 的一些功能;例如,它的标记语言。 怀旧维基百科 包含了 2001 年 12 月维基百科数据库的完整副本,当时维基百科仍在使用 UseModWiki。

第二阶段:PHP 脚本

2001 年,维基百科还不是一个前十的网站;它是一个默默无闻的项目,藏身于互联网的一个黑暗角落,大多数搜索引擎都一无所知,并且托管在一台服务器上。尽管如此,性能已经成为一个问题,尤其是因为 UseModWiki 将其内容存储在一个平面文件数据库中。当时,维基百科人担心在《纽约时报》、《Slashdot》和《连线》杂志发表文章后,会受到大量流量的冲击。

因此,在 2001 年夏季,维基百科参与者马格努斯·曼斯凯 (当时是一名大学生) 在业余时间开始着手为维基百科开发一个专门的 wiki 引擎。他的目标是使用基于数据库的应用程序来提高维基百科的性能,并开发维基百科特定的功能,这些功能是“通用”wiki 引擎无法提供的。这个新引擎是用 PHP 编写的,并由 MySQL 支持,简称为“PHP 脚本”、“PHP wiki”、“维基百科软件”或“第二阶段”。

PHP 脚本于 2001 年 8 月发布,于 9 月在 SourceForge 上共享,并测试到 2001 年年底。由于维基百科因不断增长的流量而遭遇反复出现的性能问题,英语维基百科最终在 2002 年 1 月从 UseModWiki 切换到 PHP 脚本。2001 年创建的其他语言版本也慢慢升级了,尽管其中一些语言版本将继续使用 UseModWiki 直到 2004 年。

作为使用 MySQL 数据库的 PHP 软件,PHP 脚本是后来成为 MediaWiki 的第一个迭代版本。它引入了许多今天仍在使用的关键功能,例如用于组织内容(包括讨论页面)的命名空间、皮肤和特殊页面(包括维护报告、贡献列表和用户监视列表)。

第三阶段:MediaWiki

尽管 PHP 脚本和数据库后端带来了改进,但不断增长的流量、昂贵的功能和有限的硬件组合继续在维基百科上造成性能问题。2002 年,李·丹尼尔·克罗克再次重写了代码,将新软件称为“第三阶段”(http://article.gmane.org/gmane.science.linguistics.wikipedia.technical/2794)。由于该网站经常遇到困难,李认为“没有太多时间坐下来适当地设计和开发解决方案”,因此他“只是重新组织了现有的架构以提高性能,并修复了所有代码”。添加了性能分析功能来找出运行缓慢的功能。

第三阶段软件保留了相同的基本界面,并且旨在尽可能地看起来和行为像第二阶段软件一样。还添加了一些新功能,例如新的文件上传系统、内容更改的并排差异以及维基间链接。

2002 年又添加了其他功能,例如新的维护特殊页面和“双击编辑”选项。然而,性能问题很快再次出现。例如,2002 年 11 月,管理员不得不暂时禁用“查看次数”和“网站”统计信息,因为这些统计信息在每次页面浏览时都会导致两次数据库写入。他们也会偶尔将网站切换到只读模式以维护读者服务,并在高访问时间禁用昂贵的维护页面,因为存在表锁定问题。

在 2003 年初,开发人员讨论了他们是否应该在灭火变得无法控制之前从头开始对软件进行适当的重新设计和重新架构,或者继续调整和改进现有的代码库。他们选择了后者,主要是因为大多数开发人员对代码库足够满意,并且相信进一步的迭代改进足以跟上网站的增长。

2003 年 6 月,管理员添加了第二台服务器,第一台与 Web 服务器分开的数据库服务器。(新机器也是非英语维基百科网站的 Web 服务器。)同年晚些时候,将设置两台服务器之间的负载均衡。管理员还启用了新的页面缓存系统,该系统使用文件系统为匿名用户缓存已渲染的、准备输出的页面。

2003 年 6 月,吉米·威尔士还创建了非营利组织维基媒体基金会,以支持维基百科并管理其基础设施和日常运营。“维基百科软件”于 7 月正式更名为“MediaWiki”,这是对维基媒体基金会名称的双关语。当时认为这是一个聪明的双关语,但它会让后代的用户和开发人员感到困惑。

7 月添加了新功能,例如自动生成的目录和编辑页面节的能力,这两项功能至今仍在使用。第一个以“MediaWiki”命名的版本发布在 2003 年 8 月,标志着一个应用程序的漫长起源的结束,从那时起,它的整体结构将保持相当稳定。

12.2. MediaWiki 代码库和实践

PHP

PHP 被选为 2001 年维基百科“第二阶段”软件的框架;MediaWiki 从那时起就一直在有机地增长,并且仍在不断发展。大多数 MediaWiki 开发人员是志愿者,他们在业余时间做出贡献,而且在早期只有很少的志愿者。回顾起来,一些软件设计决策或遗漏可能看起来很错误,但是很难批评创始人没有实现一些现在发现很关键的抽象,因为最初的代码库非常小,开发它所需的时间也很短。

例如,MediaWiki 使用没有前缀的类名,这会在 PHP 核心和 PECL(PHP 扩展社区库)开发人员添加新类时造成冲突:MediaWiki Namespace 类必须重命名为 MWNamespace 才能与 PHP 5.3 兼容。始终为所有类使用前缀(例如,“MW”)将使 MediaWiki 嵌入另一个应用程序或库变得更加容易。

依赖 PHP 可能不是性能的最佳选择,因为它没有从其他一些动态语言所见到的改进中获益。使用 Java 将大大提高性能,并简化后端维护任务的执行扩展。另一方面,PHP 非常流行,这有利于招募新开发人员。

即使 MediaWiki 仍然包含“丑陋”的遗留代码,但多年来也进行了重大改进,并且在 MediaWiki 的整个历史中都引入了新的架构元素。其中包括 ParserSpecialPageDatabase 类、Image 类和 FileRepo 类层次结构、ResourceLoader 以及 Action 层次结构。MediaWiki 从一开始就没有这些东西,但它们都支持从一开始就存在的功能。许多开发人员主要对功能开发感兴趣,而架构常常被抛在脑后,直到后来,在不适当的架构中工作的成本变得明显时才会追赶上来。

安全

由于 MediaWiki 是维基百科等知名网站的平台,核心开发者和代码审查人员制定了严格的安全规则。(参见 详细指南)。为了更轻松地编写安全代码,MediaWiki 为开发者提供了围绕 HTML 输出和数据库查询的包装器来处理转义。为了对用户输入进行清理,开发者使用 WebRequest 类,该类分析通过 URL 或通过 POST 表单传递的数据。它会删除“魔术引号”和斜杠,去除非法输入字符,并规范化 Unicode 序列。通过使用令牌来避免跨站点请求伪造(CSRF),并通过验证输入和转义输出来避免跨站点脚本(XSS),通常使用 PHP 的 htmlspecialchars() 函数。MediaWiki 还提供(并使用)一个带 Sanitizer 类的 XHTML 净化器,以及防止 SQL 注入的数据库函数。

配置

MediaWiki 提供数百种配置设置,存储在全局 PHP 变量中。它们的默认值在 DefaultSettings.php 中设置,系统管理员可以通过编辑 LocalSettings.php 来覆盖它们。

MediaWiki 曾经过度依赖全局变量,包括用于配置和上下文处理。全局变量会导致 PHP 的 register_globals 函数出现严重的安全问题(MediaWiki 从 1.2 版本开始就不需要该函数)。该系统还限制了配置的潜在抽象,并使优化启动过程变得更加困难。此外,配置命名空间与用于注册和对象上下文的变量共享,从而导致潜在的冲突。从用户的角度来看,全局配置变量也使 MediaWiki 似乎难以配置和维护。MediaWiki 的开发一直是将上下文从全局变量慢慢移到对象的历程。将处理上下文存储在对象成员变量中,可以让这些对象以更灵活的方式重复使用。

12.3. 数据库和文本存储

MediaWiki 从 Phase II 软件开始就一直在使用关系型数据库后端。MediaWiki 的默认(也是支持最好的)数据库管理系统 (DBMS) 是 MySQL,这是所有维基媒体网站使用的系统,但其他 DBMS(如 PostgreSQL、Oracle 和 SQLite)也有社区支持的实现。系统管理员可以在安装 MediaWiki 时选择 DBMS,MediaWiki 提供数据库抽象和查询抽象层,简化了开发人员对数据库的访问。

图 12.1:数据库模式

当前布局包含数十个表格。许多表格与维基的内容有关(例如,pagerevisioncategoryrecentchanges)。其他表格包括有关用户(useruser_groups)、媒体文件(imagefilearchive)、缓存(objectcachel10n_cachequerycache)和内部工具(用于作业队列的 job)的数据,等等,如 图 12.2 所示。(MediaWiki 中数据库布局的完整文档 可供使用。)索引和汇总表格在 MediaWiki 中得到了广泛使用,因为扫描大量行的 SQL 查询可能非常昂贵,特别是在维基媒体网站上。通常不鼓励使用无索引的查询。

数据库多年来经历了数十次模式更改,最显著的是在 MediaWiki 1.5 中将文本存储和修订跟踪解耦。

图 12.2:MediaWiki 1.4 和 1.5 中的主要内容表格

在 1.4 模型中,内容存储在两个重要的表格中:cur(包含页面当前修订的文本和元数据)和 old(包含以前的修订);已删除的页面保存在 archive 中。进行编辑时,以前当前的修订将被复制到 old 表格,新的编辑将被保存到 cur。当页面被重命名时,页面标题必须在所有 old 修订的元数据中更新,这可能是一个很长的操作。当页面被删除时,它在 curold 表格中的条目必须被复制到 archive 表格,然后再被删除;这意味着要移动所有修订的文本,这可能非常大,因此需要时间。

在 1.5 模型中,修订元数据和修订文本被分开:curold 表格被替换为 page(页面的元数据)、revision(所有修订的元数据,旧的或当前的)和 text(所有修订的文本,旧的,当前的或已删除的)。现在,进行编辑时,修订元数据不需要在表格之间复制:插入一个新的条目并更新 page_latest 指针就足够了。此外,修订元数据不再包含页面标题,只包含它的 ID:这消除了在页面被重命名时重命名所有修订的需要。

revision 表格存储每个修订的元数据,但不是它们的文本;而是包含指向 text 表格的文本 ID,该表格包含实际文本。当页面被删除时,页面的所有修订的文本仍然在那里,不需要移动到另一个表格。text 表格由 ID 到文本块的映射组成;flags 字段指示文本块是否被 gzip 压缩(为了节省空间)或文本块是否只是指向外部文本存储的指针。维基媒体网站使用一个由 MySQL 支持的外部存储集群,其中包含数十个修订的块。块的第一个修订被完整地存储,而同一页面的后续修订被存储为相对于先前修订的差异;然后对块进行 gzip 压缩。由于修订按页面分组,因此它们倾向于相似,因此差异相对较小,gzip 工作良好。在维基媒体网站上实现的压缩比接近 98%。

在硬件方面,MediaWiki 内置了对负载均衡的支持,早在 2004 年的 MediaWiki 1.2 中就添加了(当时维基百科获得了它的第二台服务器——当时是一件大事)。负载均衡器(MediaWiki 的 PHP 代码,用于决定连接到哪个服务器)现在是维基媒体基础设施的关键部分,这解释了它对代码中某些算法决策的影响。系统管理员可以在 MediaWiki 的配置中指定有一个主数据库服务器和任意数量的从数据库服务器;可以为每个服务器分配一个权重。负载均衡器会将所有写入发送到主服务器,并根据权重平衡读取。它还会跟踪每个从服务器的复制延迟。如果从服务器的复制延迟超过 30 秒,它将不会收到任何读取查询,以允许它赶上;如果所有从服务器的延迟都超过 30 秒,MediaWiki 会自动将自己置于只读模式。

MediaWiki 的“时间顺序保护器”确保复制延迟永远不会导致用户看到一个页面,该页面声称他们刚刚执行的操作还没有发生:例如,如果一个用户重命名了一个页面,另一个用户可能仍然看到旧的名称,但重命名的人总是会看到新的名称,因为他就是重命名的人。这是通过在用户的会话中存储主服务器的位置来实现的,如果他们做出的请求导致了写入查询。用户下次发出读取请求时,负载均衡器会从会话中读取此位置,并尝试选择一个已赶上该复制位置的从服务器来服务该请求。如果找不到,它会等到找到为止。对于其他用户来说,这似乎是操作还没有发生,但对于每个用户来说,时间顺序仍然一致。

12.4. 请求、缓存和交付

Web 请求的执行工作流程

index.php 是 MediaWiki 的主入口点,它处理应用程序服务器处理的大多数请求(即,未由缓存基础设施提供的请求;见下文)。从 index.php 执行的代码执行安全检查,从 includes/DefaultSettings.php 加载默认配置设置,使用 includes/Setup.php 推测配置,然后应用 LocalSettings.php 中包含的站点设置。接下来,它实例化一个 MediaWiki 对象($mediawiki),并根据来自请求的标题和操作参数创建 Title 对象($wgTitle)。

index.php 可以接受 URL 请求中的各种操作参数;默认操作是 view,它显示文章内容的常规视图。例如,请求 https://en.wikipedia.org/w/index.php?title=Apple&action=view 显示英文维基百科上“苹果”文章的内容。(查看请求通常通过 URL 重写来美化,在本例中是 https://en.wikipedia.org/wiki/Apple。)其他常见操作包括 edit(打开文章进行编辑)、submit(预览或保存文章)、history(显示文章的历史记录)和 watch(将文章添加到用户的观察列表)。管理操作包括 delete(删除文章)和 protect(防止对文章进行编辑)。

然后调用 MediaWiki::performRequest() 来处理大多数 URL 请求。它检查错误的标题、读取限制、本地 interwiki 重定向和重定向循环,并确定请求是针对普通页面还是特殊页面。

普通页面请求将交给 MediaWiki::initializeArticle(),以创建页面的 Article 对象($wgArticle),然后交给 MediaWiki::performAction(),它处理“标准”操作。操作完成后,MediaWiki::finalCleanup() 通过提交数据库事务、输出 HTML 并通过作业队列启动延迟更新来完成请求。MediaWiki::restInPeace() 提交延迟更新并优雅地关闭任务。

如果请求的页面是特殊页面(即,不是常规的维基内容页面,而是与软件相关的特殊页面,例如 Statistics),则会调用 SpecialPageFactory::executePath 而不是 initializeArticle();然后调用相应的 PHP 脚本。特殊页面可以执行各种神奇的操作,并且每个页面都有一个特定的目的,通常与任何一篇文章或其内容无关。特殊页面包括各种类型的报告(最近更改、日志、未分类页面)和维基管理工具(用户屏蔽、用户权限更改),等等。它们的执行工作流程取决于它们的功能。

许多函数包含分析代码,如果启用了分析,则可以跟踪执行工作流程进行调试。分析通过调用 wfProfileInwfProfileOut 函数来分别启动和停止分析函数;这两个函数都将函数的名称作为参数。在维基媒体网站上,对一定比例的请求进行分析,以保持性能。MediaWiki 将 UDP 数据包发送到中央服务器,该服务器收集这些数据包并生成分析数据。

缓存

MediaWiki 本身为了性能而进行了改进,因为它在维基媒体网站上起着核心作用,但它也是一个更大的运营生态系统的一部分,该生态系统影响了其架构。维基媒体的缓存基础设施(分层结构)对 MediaWiki 施加了一些限制;开发人员通过变通解决这些问题,而不是试图围绕 MediaWiki 塑造维基媒体经过广泛优化的缓存基础设施,而是使 MediaWiki 更加灵活,以便它可以在该基础设施内工作而不会影响性能和缓存需求。例如,默认情况下,MediaWiki 在界面(对于从左到右的语言)的右上角显示用户的 IP,作为提醒,当他们未登录时,软件就是如此识别他们的。$wgShowIPinHeader 配置变量允许系统管理员禁用此功能,从而使页面内容独立于用户:所有匿名访问者都可以获得每个页面的完全相同版本。

第一级缓存(在维基媒体网站上使用)由反向缓存代理(Squid)组成,它们拦截并处理大多数请求,然后再将它们传递到 MediaWiki 应用程序服务器。Squid 包含已渲染页面的静态版本,用于为未登录到网站的用户提供简单的读取服务。MediaWiki 本地支持 Squid 和 Varnish,并通过以下方式与该缓存层集成,例如,在更改页面时通知它们从缓存中清除该页面。对于已登录的用户和其他无法由 Squid 处理的请求,Squid 将请求转发到 Web 服务器(Apache)。

第二级缓存发生在 MediaWiki 从多个对象渲染和组装页面时,其中许多对象可以被缓存以最大程度地减少将来的调用。这些对象包括页面的界面(侧边栏、菜单、UI 文本)和从维基文本解析的实际内容。内存中对象缓存自 MediaWiki 1.1 版本(2003 年)起就已在 MediaWiki 中可用,对于避免重新解析长而复杂的页面特别重要。

登录会话数据也可以存储在 memcached 中,这使得会话可以在负载均衡设置中的多个前端 Web 服务器上透明地工作(维基媒体严重依赖负载均衡,使用带有 PyBal 的 LVS)。

自 1.16 版本起,MediaWiki 使用专门的对象缓存来存储本地化的 UI 文本;这是在注意到 memcached 中缓存的大部分对象都是以用户的语言本地化的 UI 消息之后添加的。该系统基于从常量数据库 (CDB) 中快速获取单个消息,例如带有键值对的文件。CDB 在典型情况下最大限度地减少了内存开销和启动时间;它们也用于互连维基缓存。

最后一层缓存由 PHP opcode 缓存组成,通常启用以加快 PHP 应用程序的速度。编译可能是一个漫长的过程;为了避免每次调用 PHP 脚本时都将 PHP 脚本编译成 opcode,可以使用 PHP 加速器来存储已编译的 opcode 并直接执行它,而无需编译。MediaWiki 将“正常工作”与许多加速器一起使用,例如 APC、PHP 加速器和 eAccelerator。

由于其维基媒体偏差,MediaWiki 针对此完整的多层分布式缓存基础设施进行了优化。尽管如此,它也本地支持针对较小网站的替代设置。例如,它提供了一个可选的简单文件缓存系统,该系统存储完全渲染页面的输出,就像 Squid 一样。此外,MediaWiki 的抽象对象缓存层允许它将缓存的对象存储在多个位置,包括文件系统、数据库或 opcode 缓存。

资源加载器

与许多 Web 应用程序一样,MediaWiki 的界面近年来变得更加互动和响应式,主要通过使用 JavaScript。2008 年发起的可用性工作以及高级媒体处理(例如,视频文件的在线编辑)要求进行专门的前端性能改进。

为了优化 JavaScript 和 CSS 资产的交付,开发了 ResourceLoader 模块来优化 JS 和 CSS 的交付。该模块于 2009 年启动,于 2011 年完成,自 1.17 版本起成为 MediaWiki 的核心功能。ResourceLoader 通过按需加载 JS 和 CSS 资产来工作,从而减少了在未使用功能时(例如,由旧版浏览器使用)的加载和解析时间。它还会缩小代码、对资源进行分组以节省请求,并将图像嵌入为数据 URI。(有关 ResourceLoader 的更多信息,请参阅官方文档,以及 Trevor Parscal 和 Roan Kattouw 在 OSCON 2011 上发表的关于“低垂果实与微优化:加快网页加载速度的创意技巧”的演讲。)

12.5. 语言

背景和基本原理

有效地向所有人贡献和传播自由知识的核心部分是以尽可能多的语言提供它。维基百科有 280 多种语言版本,英语百科全书文章不到所有文章的 20%。由于维基百科及其姊妹网站存在于如此多种语言中,因此不仅要以读者的母语提供内容,还要提供本地化的界面以及有效的输入和转换工具,以便参与者能够贡献内容。

因此,本地化和国际化 (l10n 和 i18n) 是 MediaWiki 的核心组成部分。i18n 系统无处不在,影响软件的许多部分;它也是最灵活和功能最丰富的系统之一。(关于 MediaWiki 中的国际化和本地化有一个详尽指南。)通常优先考虑翻译员的便利性而不是开发人员的便利性,但这被认为是可以接受的成本。

MediaWiki 目前已本地化为 350 多种语言,包括非拉丁语和从右到左 (RTL) 语言,完成度各不相同。界面和内容可以用不同的语言,并且方向性可以混合使用。

内容语言

MediaWiki 最初使用每种语言编码,这导致了许多问题;例如,页面标题中无法使用外文字符。因此,采用了 UTF-8。2005 年,在 MediaWiki 1.5 中,对 UTF-8 以外的字符集的支持以及主要数据库架构更改一起被删除;内容现在必须以 UTF-8 编码。

编辑器键盘上没有的字符可以通过 MediaWiki 的 Edittools 进行自定义和插入,Edittools 是编辑窗口下方显示的界面消息;它的 JavaScript 版本会自动将单击的字符插入编辑窗口。MediaWiki 的 WikiEditor 扩展作为可用性工作的一部分而开发,将特殊字符与编辑工具栏合并。另一个名为 Narayam 的扩展提供了其他输入方法和非 ASCII 字符的键映射功能。

界面语言

自第三阶段软件创建以来,界面消息一直存储在 PHP 键值对数组中。每条消息都由一个唯一的键标识,该键在不同语言中被分配不同的值。键由开发人员确定,他们被鼓励为扩展使用前缀;例如,UploadWizard 扩展的消息键将以 mwe-upwiz- 开头,其中 mwe 代表MediaWiki 扩展

MediaWiki 消息可以嵌入软件提供的参数,这些参数通常会影响消息的语法。为了支持几乎所有可能的语言,MediaWiki 的本地化系统随着时间的推移而得到了改进和复杂化,以适应语言的特定特征和例外情况,这些特征和例外情况通常被说英语的人认为是怪异的。

例如,形容词在英语中是不变的词,但在法语等语言中,形容词需要与名词一致。如果用户在他们的首选项中指定了他们的性别,则可以在界面消息中使用 {{GENDER:}} 开关来适当地称呼他们。其他开关包括 {{PLURAL:}}(用于“简单”复数和像阿拉伯语这样的具有双数、三数或少数复数的语言)和 {{GRAMMAR:}}(提供像芬兰语这样的语言的语法转换函数,其语法格会导致变化或变格)。

本地化消息

MediaWiki 的本地化界面消息驻留在 MessagesXx.php 文件中,其中 Xx 是语言的 ISO-639 代码(例如,法语的 MessagesFr.php);默认消息为英语,存储在 MessagesEn.php 中。MediaWiki 扩展使用类似的系统,或者将所有本地化消息托管在 <扩展名>.i18n.php 文件中。除了翻译之外,消息文件还包括特定于语言的信息,例如日期格式。

以前,贡献翻译是通过提交 MessagesXx.php 文件的 PHP 补丁来完成的。2003 年 12 月,MediaWiki 1.1 引入了“数据库消息”,它是 MediaWiki 命名空间中包含界面消息的维基页面的一个子集。维基页面 MediaWiki:<消息键> 的内容是消息的文本,并覆盖 PHP 文件中的值。消息的本地化版本位于 MediaWiki:<消息键>/<语言代码>;例如,MediaWiki:Rollbacklink/de

此功能允许高级用户在他们的维基上本地化(和自定义)界面消息,但该过程不会更新随 MediaWiki 提供的 i18n 文件。2006 年,Niklas Laxström 创建了一个特殊的、经过大量修改的 MediaWiki 网站(现在托管在http://translatewiki.net),翻译人员可以在其中通过简单地编辑维基页面轻松地将界面消息本地化为所有语言。然后在 MediaWiki 代码库中更新 MessagesXx.php 文件,任何维基都可以自动获取这些文件,并使用 LocalisationUpdate 扩展进行更新。在维基媒体网站上,数据库消息现在只用于自定义,不再用于本地化。MediaWiki 扩展和一些相关程序(例如机器人)也已在 translatewiki.net 上本地化。

为了帮助翻译人员了解界面消息的上下文和含义,在 MediaWiki 中,为每条消息提供文档被认为是一种好习惯。此文档存储在具有 qqq 语言代码的特殊消息文件中,该语言代码不对应于任何实际语言。然后,每条消息的文档会在 translatewiki.net 上的翻译界面中显示。另一个有用的工具是 qqx 语言代码;当与 &uselang 参数一起使用以显示维基页面时(例如,https://en.wikipedia.org/wiki/Special:RecentChanges?uselang=qqx),MediaWiki 将在用户界面中显示消息键而不是其值;这对于识别要翻译或更改的消息非常有用。

注册用户可以在他们的首选项中设置他们自己的界面语言,以覆盖网站的默认界面语言。MediaWiki 还支持后备语言:如果消息在所选语言中不可用,它将以尽可能接近的语言显示,不一定以英语显示。例如,布列塔尼语的后备语言是法语。

12.6. 用户

用户在代码中使用 User 类的实例表示,该类封装了所有特定于用户设置(用户 ID、姓名、权限、密码、电子邮件地址等)。客户端类使用访问器来访问这些字段;它们完成了确定用户是否已登录以及是否可以从 cookie 中满足请求的选项,或者是否需要数据库查询的工作。渲染普通页面所需的多数设置都设置在 cookie 中,以最大限度地减少对数据库的使用。

MediaWiki 提供了一个非常细粒度的权限系统,其中针对基本上所有可能的动作都有一个用户权限。例如,要执行“回滚”操作(即“快速回滚特定页面的最后一位用户的编辑”),用户需要 rollback 权限,该权限默认包含在 MediaWiki 的 sysop 用户组中。但它也可以添加到其他用户组中,或者拥有一个专门的用户组,只提供此权限(这是英文维基百科的情况,具有 Rollbackers 组)。用户权限的自定义是通过编辑 LocalSettings.php 中的 $wgGroupPermissions 数组来完成的;例如,$wgGroupPermissions['user']['movefile'] = true; 允许所有注册用户重命名文件。用户可以属于多个组,并继承与每个组相关的最高权限。

然而,MediaWiki 的用户权限系统实际上是针对维基百科设计的:一个内容对所有人开放的网站,只有某些操作对某些用户有限制。MediaWiki 缺乏统一的、普遍的权限概念;它不提供传统的 CMS 功能,例如按主题或内容类型限制读写访问权限。一些 MediaWiki 扩展在一定程度上提供了这些功能。

12.7. 内容

内容结构

命名空间的概念起源于维基百科的 UseModWiki 时代,当时讨论页面的标题为“`

/Talk`”。命名空间在 Magnus Manske 的第一个“PHP 脚本”中正式引入。多年来,它们被重新实现过几次,但其功能始终如一:将不同类型的内容区分开来。它们由一个前缀组成,前缀用冒号(:)与页面标题分隔(例如,`Talk:` 或 `File:` 和 `Template:`);主要内容命名空间没有前缀。维基百科用户迅速接受了命名空间,它们为社区提供了不同的空间来发展。命名空间已被证明是 MediaWiki 的重要功能,因为它们为维基社区的建立奠定了必要的前提条件,并建立了元级讨论、社区流程、门户、用户资料等。

MediaWiki 主要内容命名空间的默认配置是扁平化(没有子页面),因为这是维基百科的运作方式,但启用子页面非常简单。它们在其他命名空间中启用(例如,`User:`,人们可以在其中编写草稿文章),并显示面包屑导航。

命名空间按类型区分内容;在同一个命名空间内,页面可以通过类别进行主题组织,类别是一种在 MediaWiki 1.3 中引入的伪层次结构组织方案。

内容处理:MediaWiki 标记语言和解析器

MediaWiki 存储的用户生成内容不是 HTML,而是 MediaWiki 特定的标记语言,有时被称为“维基文本”。它允许用户进行格式更改(例如,使用引号进行粗体、斜体),添加链接(使用方括号),包含模板,插入与上下文相关的內容(如日期或签名),以及执行许多其他的“魔法”操作。(详细文档)。

要显示页面,需要解析此内容,将其从调用的所有外部或动态部分组装起来,并转换为正确的 HTML。解析器是 MediaWiki 最重要的部分之一,这使得更改或改进它变得很困难。由于全球数亿个维基页面依赖解析器以继续以一贯的方式输出 HTML,因此它必须保持极高的稳定性。

标记语言从一开始就没有正式规范;它开始基于 UseModWiki 的标记,然后根据需要进行变形和演变。由于没有正式规范,MediaWiki 标记语言已成为一种复杂且特异性的语言,基本上只与 MediaWiki 的解析器兼容;它不能表示为正式语法。当前解析器的规范被戏称为“解析器从维基文本中输出的所有内容,加上数百个测试用例”。

许多人尝试过替代解析器,但到目前为止还没有成功。2004 年,Jens Frank 编写了一个实验性词法分析器来解析维基文本,并在维基百科上启用;由于 PHP 数组内存分配的性能不佳,它在三天后被禁用。从那时起,大部分解析都是使用大量的正则表达式和大量辅助函数完成的。维基标记以及解析器需要支持的所有特殊情况也变得更加复杂,这使得未来的尝试更加困难。

Tim Starling 在 MediaWiki 1.12 中对预处理器进行了重写,这是一个值得注意的改进,其主要目的是提高具有复杂模板的页面的解析性能。预处理器将维基文本转换为一个 XML DOM 树,该树表示文档的部分(模板调用、解析器函数、标签钩子、节标题以及其他一些结构),但可以跳过“死分支”,例如未执行的 `#switch` 案例和模板参数未使用的默认值,在模板扩展中。然后,解析器遍历 DOM 结构,并将它的内容转换为 HTML。

最近对 MediaWiki 的可视化编辑器的研究已经让改进解析过程(并使其更快)变得必要,因此对解析器以及 MediaWiki 标记和最终 HTML 之间的中间层的研究已经恢复(参见以下“未来”部分)。

魔法字和模板

MediaWiki 提供“魔法字”,它们可以修改页面的总体行为或在页面中包含动态内容。它们包括:行为开关,如 `__NOTOC__`(隐藏自动目录)或 `__NOINDEX__`(告诉搜索引擎不要索引页面);变量,如 `{{CURRENTTIME}}` 或 `{{SITENAME}}`;以及解析器函数,即可以接受参数的魔法字,如 `{{lc:}}`(以小写形式输出 ``)。用于本地化 UI 的结构,如 `{{GENDER:}}`、`{{PLURAL:}}` 和 `{{GRAMMAR:}}`,都是解析器函数。

在 MediaWiki 页面中包含来自其他页面的内容的最常见方法是使用模板。模板的最初目的实际上是在不同的页面上包含相同的内容,例如,维基百科文章中的导航面板或维护横幅;创建部分页面布局并在数千篇文章中重复使用它们的能力,以及对所有这些内容进行集中维护,这对维基百科这样的网站产生了巨大的影响。

然而,用户也出于完全不同的目的使用了(滥用了)模板。MediaWiki 1.3 使模板能够接受更改其输出的参数;添加默认参数的能力(在 MediaWiki 1.6 中引入)使得能够在 PHP 之上实现一种函数式编程语言,最终成为性能方面最昂贵的功能之一。

然后,Tim Starling 开发了额外的解析器函数(ParserFunctions 扩展),作为对维基百科用户使用模板创建的疯狂结构的一种权宜之计。这套函数包括逻辑结构,如 `#if` 和 `#switch`,以及其他函数,如 `#expr`(用于计算数学表达式)和 `#time`(用于时间格式化)。

很快,维基百科用户开始使用新函数创建更复杂的模板,这大大降低了模板繁多的页面的解析性能。MediaWiki 1.12 中引入的新预处理器(一项重大的架构变更)被用来部分解决这个问题。最近,MediaWiki 开发人员讨论了使用实际的脚本语言(可能是 Lua)来提高性能的可能性。

媒体文件

用户通过 `Special:Upload` 页面上传文件;管理员可以通过扩展白名单配置允许的文件类型。上传后,文件存储在文件系统上的一个文件夹中,缩略图存储在一个专用的 `thumb` 目录中。

由于维基媒体的教育使命,MediaWiki 支持其他 Web 应用程序或 CMS 中可能不常见的文件类型,例如 SVG 矢量图像以及多页 PDF 和 DjVu。它们被渲染为 PNG 文件,并且可以创建缩略图并内联显示,就像更常见的图像文件(如 GIF、JPG 和 PNG)一样。

上传文件时,会为其分配一个包含上传者输入信息的 `File:` 页面;这是一段自由文本,通常包括版权信息(作者、许可证)以及描述或分类文件内容的条目(描述、位置、日期、类别等)。虽然私有维基可能不太关心这些信息,但在像维基媒体公共库这样的媒体库中,这些信息对于组织收藏和确保共享这些文件的合法性至关重要。有人认为,事实上,大多数这些元数据应该存储在一个可查询的结构中,例如数据库表。这将大大方便搜索,以及第三方通过 API 进行归属和重复使用。

大多数维基媒体站点还允许每个维基“本地”上传,但社区试图将免费许可的媒体文件存储在维基媒体的免费媒体库维基媒体公共库中。任何维基媒体站点都可以显示托管在公共库上的文件,就好像它被本地托管一样。这种习惯避免了将文件上传到每个维基以在那里使用它。

因此,MediaWiki 本身支持外部媒体存储库,即通过其 API 和 `ForeignAPIRepo` 系统访问托管在另一个维基上的媒体文件的能力。从 1.16 版开始,任何 MediaWiki 网站都可以轻松使用来自维基媒体公共库的文件,方法是使用 `InstantCommons` 功能。使用外部存储库时,缩略图会存储在本地以节省带宽。但是,目前还不能从另一个维基上传到外部媒体存储库。

12.8. 自定义和扩展 MediaWiki

级别

MediaWiki 的架构提供了多种自定义和扩展软件的方式。这可以在不同的访问级别完成

如果启用,外部程序也可以通过其机器 API 与 MediaWiki 进行通信,基本上可以让用户访问任何功能和数据。

JavaScript 和 CSS

MediaWiki 可以使用自定义维基页面读取和应用全站范围或皮肤范围的 JavaScript 和 CSS;这些页面位于 `MediaWiki:` 命名空间,因此只能由系统管理员编辑;例如,来自 `MediaWiki:Common.js` 的 JavaScript 修改适用于所有皮肤,来自 `MediaWiki:Common.css` 的 CSS 适用于所有皮肤,但 `MediaWiki:Vector.css` 仅适用于使用 Vector 皮肤的用户。

用户可以进行相同类型的更改,这些更改将仅应用于他们自己的界面,方法是编辑他们用户页面的子页面(例如,`User:/common.js` 用于所有皮肤的 JavaScript,`User:/common.css` 用于所有皮肤的 CSS,或者 `User:/vector.css` 用于仅应用于 Vector 皮肤的 CSS 修改)。

如果安装了 Gadgets 扩展,系统管理员还可以编辑小工具,即 JavaScript 代码片段,提供用户可以在其首选项中打开和关闭的功能。即将推出的小工具开发将使跨维基共享小工具成为可能,从而避免重复。

这套工具对 MediaWiki 软件开发的民主化产生了巨大影响,并极大地促进了民主化。个别用户有权为自己添加功能;有权势的用户可以与其他人分享这些功能,无论是非正式地还是通过全球可配置的系统管理员控制系统。这种框架非常适合小型、独立的修改,并且比通过钩子和扩展进行的大型代码修改更容易上手。

扩展和皮肤

当 JavaScript 和 CSS 修改不足时,MediaWiki 提供了一套钩子系统,让第三方开发人员可以在特定事件的 MediaWiki 代码之前、之后或代替 MediaWiki 代码运行自定义 PHP 代码。(MediaWiki 钩子在 https://www.mediawiki.org/wiki/Manual:Hooks 中有参考)。MediaWiki 扩展使用钩子来插入代码。

在 MediaWiki 中出现钩子之前,添加自定义 PHP 代码意味着修改核心代码,这既不容易也不推荐。第一个钩子是由 Evan Prodromou 在 2004 年提出并添加的;多年来,根据需要添加了更多钩子。使用钩子,甚至可以使用标签扩展来扩展 MediaWiki 的维基标记功能。

扩展系统并不完美;扩展注册基于启动时执行代码,而不是可缓存的数据,这限制了抽象和优化,并损害了 MediaWiki 的性能。但总体而言,扩展架构现在是一个相当灵活的基础设施,它帮助使专门的代码更加模块化,防止核心软件过度扩展,并使第三方用户更容易在 MediaWiki 之上构建自定义功能。

相反,在不重新发明轮子的情况下,很难为 MediaWiki 编写新的皮肤。在 MediaWiki 中,皮肤是 PHP 类,每个类都扩展了父类 Skin 类;它们包含用于收集生成 HTML 所需信息的函数。长期存在的“MonoBook”皮肤难以定制,因为它包含大量针对旧浏览器的特定于浏览器的 CSS;编辑模板或 CSS 需要许多后续更改,以反映所有浏览器和平台的更改。

API

除了 index.php 之外,MediaWiki 的另一个主要入口点是 api.php,它用于访问其机器可读的 Web 查询 API(应用程序编程接口)。

维基百科用户最初创建了“机器人”,这些机器人通过抓取 MediaWiki 提供的 HTML 内容来工作;这种方法非常不可靠,并且多次中断。为了改善这种情况,开发人员引入了只读界面(位于 query.php),然后发展成一个功能齐全的读写机器 API,提供对 MediaWiki 数据库中包含的数据的直接、高级访问。(API 的详尽文档 可供使用。)

客户端程序可以使用 API 登录、获取数据和发布更改。API 支持基于 Web 的轻量级 JavaScript 客户端和最终用户应用程序。基本上,任何可以通过 Web 界面完成的操作都可以通过 API 完成。实现 MediaWiki API 的客户端库以多种语言提供,包括 Python 和 .NET。

12.9. 未来

最初由一位志愿者 PHP 开发人员完成的暑期项目已发展成为 MediaWiki,一个成熟、稳定的维基引擎,为排名前十的网站提供动力,其运营基础设施小得令人难以置信。这是通过持续优化性能、迭代架构更改和一支优秀的开发人员团队实现的。

Web 技术的演变以及维基百科的增长要求持续改进和新功能,其中一些功能需要对 MediaWiki 的架构进行重大更改。例如,正在进行的可视化编辑器项目就要求对解析器、维基标记语言、DOM 和最终 HTML 转换进行重新工作。

MediaWiki 是一个用于多种不同目的的工具。例如,在维基媒体项目中,它用于创建和管理百科全书(维基百科)、为庞大的媒体库(维基共享资源)提供动力、转录扫描的参考文本(维基文库),等等。在其他情况下,MediaWiki 被用作企业 CMS 或数据存储库,有时与语义框架相结合。这些未经计划的专用用途可能会继续推动软件内部结构的不断调整。因此,MediaWiki 的架构非常活跃,就像它支持的庞大用户社区一样。

12.10. 进一步阅读

12.11. 致谢

本章由大家共同创作。Guillaume Paumier 通过整理 MediaWiki 用户和核心开发人员提供的输入,撰写了大部分内容。Sumana Harihareswara 协调了访谈和输入收集阶段。感谢 Antoine Musso、Brion Vibber、Chad Horohoe、Tim Starling、Roan Kattouw、Sam Reed、Siebrand Mazeland、Erik Möller、Magnus Manske、Rob Lanphier、Amir Aharoni、Federico Leva、Graham Pearce 等人提供输入和/或审查内容。