近期接触了很多维基农场(Wiki farm,提供多个 Wiki 托管的服务器),大多数农场使用的是修改过的 MediaWiki 实例,其中发现有不少农场(例如 wiki.gg、Weird Gloop 等)为 css 页面提供了编写 filepath:// ,解析后指向文件实际链接(通常是图片)的功能。这种做法更像是一种自定义的伪协议(Pseudo-protocol),避免了硬编码 URL 带来的维护噩梦,非常值得借鉴。
什么是 filepath
标准 MediaWiki
MediaWiki 本身其实有 filepath 功能,但是和上述所说的 filepath 不甚相同。标准的 MediaWiki 中,filepath 是一个解析函数(Parser Function),写法通常是 {{filepath:文件名}}。经过服务器解析后,会直接返回该文件在服务器上的绝对 URL 地址。大部分情况下,filepath 主要用于 Wikitext 页面(比如模板当中),在需要直接引用图片链接(而非渲染图片)时使用,但无法直接在 CSS 或 JavaScript 文件中使用。
动态解析拦截器
而维基农场中的 filepath://,则是一种专门为 CSS 设计的资源路由协议。这种 filepath:// 并非简单的字符串替换,而是在 MediaWiki 处理静态资源(ResourceLoader)的管线中,插入了一个动态解析拦截器。以 Weird Gloop 为例,这个拦截器发生在 remapOne 阶段,也就是 ResourceLoader 将 CSS 发送给客户端之前。解析后,浏览器接收到的 CSS 将会是经过计算的实际 URL(如 https://cdn.example.com/images/thumb/a/ab/Wiki.png/300px-Wiki.png)。
如何启用 filepath
如果想在自建的 MediaWiki 环境或农场中实现这个功能,核心在于修改 CSS 解析工具类。下面以 Weird Gloop 的方案(修改 CSSMin.php )为例说明。
详细说明
定位到并打开文件 /vendor/wikimedia/minify/src/CSSMin.php,定位到 remapOne,在 remapOne 方法开始处插入以下内容。
$parsedUrl = parse_url( $url );
if ( is_array( $parsedUrl ) && isset( $parsedUrl['scheme'] ) && $parsedUrl['scheme'] == 'filepath' ) {
if ( isset( $parsedUrl['host'] ) ) {
// 提取文件名(即 host 部分,如 Wiki.png)并解析参数(如 ?width=200)
$name = rawurldecode( $parsedUrl['host'] );
$width = -1;
if ( isset( $parsedUrl['query'] ) ) {
parse_str( $parsedUrl['query'], $opts );
$width = (int) ( $opts['width'] ?? $opts['w'] ?? -1 );
}
// 调用 MediaWiki 核心服务,获取文件对象;如果有宽度要求,生成并指向缩略图
$fileObj = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $name );
if ( $fileObj ) {
$resultUrl = $fileObj->getUrl();
//
if ( $width != -1 ) {
$mto = $fileObj->transform( [ 'width' => $width ] );
if ( $mto && !$mto->isError() ) {
$resultUrl = $mto->getUrl();
}
}
return $resultUrl; // 返回解析后的真实链接
}
}
}
修改后的完整代码
定位到并打开文件 /vendor/wikimedia/minify/src/CSSMin.php,定位到 remapOne,将整个函数替换为以下代码(已知适用于 MediaWiki 1.43,其他版本未测试)。
public static function remapOne( $file, $query, $local, $remote, $embed ) {
// The full URL possibly with query, as passed to the 'url()' value in CSS
$url = $file . $query;
// WGL start - Implement filepath: URL scheme for long-term cached image usage in CSS.
// i.e. filepath://Wiki.png?width=30
$parsedUrl = parse_url( $url );
if ( is_array( $parsedUrl ) && isset( $parsedUrl['scheme'] ) && $parsedUrl['scheme'] == 'filepath' ) {
if ( isset( $parsedUrl['host'] ) ) {
$name = rawurldecode( $parsedUrl['host'] );
$width = -1;
if ( isset( $parsedUrl['query'] ) ) {
parse_str( $parsedUrl['query'], $opts );
$width = (int) ( $opts['width'] ?? $opts['w'] ?? -1 );
}
// Handles non-existing files by returning the non-cached path where the file would exist.
$fileObj = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $name );
// Handle bad file name.
if ( !$fileObj ) {
return '';
}
// Non-thumbnail URL.
$resultUrl = $fileObj->getUrl();
// Ensure we have a valid URL
if ( !$resultUrl ) {
return '';
}
// If a width is requested.
if ( $width != -1 ) {
$mto = $fileObj->transform( [ 'width' => $width ] );
// ... and we can
if ( $mto && !$mto->isError() ) {
// ... change the URL to point to a thumbnail.
$thumbUrl = $mto->getUrl();
if ( $thumbUrl ) {
$resultUrl = $thumbUrl;
}
}
}
return $resultUrl;
}
}
// WGL end.
// Expand local URLs with absolute paths to a full URL (possibly protocol-relative).
if ( self::isLocalUrl( $url ) ) {
return self::resolveUrl( $remote, $url );
}
// Pass through fully-qualified and protocol-relative URLs and data URIs, as well as local URLs if
// we can't expand them.
// Also skips anchors or the rare `behavior` property specifying application's default behavior
if (
self::isRemoteUrl( $url ) ||
( $url[0] ?? '' ) === '#'
) {
return $url;
}
// The $remote must have a trailing slash beyond this point for correct path resolution.
if ( ( $remote[-1] ?? '' ) !== '/' ) {
$remote .= '/';
}
if ( $local === false ) {
// CSS specifies a path that is neither a local file path, nor a local URL.
// It is probably already a fully-qualitied URL or data URI, but try to expand
// it just in case.
$url = self::resolveUrl( $remote, $url );
} else {
// We drop the query part here and instead make the path relative to $remote
$url = self::resolveUrl( $remote, $file );
// Path to the actual file on the filesystem
$localFile = "{$local}/{$file}";
if ( file_exists( $localFile ) ) {
if ( $embed ) {
$data = self::encodeImageAsDataURI( $localFile );
if ( $data !== false ) {
return $data;
}
}
// Add version parameter as the first five hex digits
// of the MD5 hash of the file's contents.
$url .= '?' . substr( md5_file( $localFile ), 0, 5 );
}
// If any of these conditions failed (file missing, we don't want to embed it
// or it's not embeddable), return the URL (possibly with ?timestamp part)
}
return $url;
}
需要注意的是,remapOne 会在每次 ResourceLoader 生成缓存时运行。获取文件对象涉及数据库查询,建议开启对象缓存(如 Redis 或 Memcached),否则数据库庞大时会显著拖慢 CSS 的编译速度。
