Finder快速预览使用扩展模式苹果没告诉你的事

最近我给 Writer 的 Finder 快速预览增加了 Mermaid 支持。Writer 应用内本来就使用 WKWebView 预览 Markdown,Mermaid 也是通过本地 JavaScript 渲染的。既然应用里能显示,那么把同一套渲染代码放进 Quick Look,不就行了吗?

我一开始也是这么想的。

Writer 原来的 Quick Look 使用 data-based preview。扩展读取 Markdown,生成完整的 HTML,然后通过 QLPreviewReply 把 HTML 交给 Finder。标题、列表、表格、代码块这些静态内容都没问题,但 Mermaid 不一样。Mermaid 代码块不是一张现成的图片,需要 JavaScript 解析代码,再生成 SVG。

问题就是,Finder 拿到 HTML,不等于它会像 Safari 或 WKWebView 那样执行里面的 JavaScript。

所以我决定把 Quick Look 改成 view-based extension。也就是不再把一份 HTML 数据交给 Finder,而是由扩展自己提供一个 NSViewController,里面放一个 WKWebView

这下总该和 Writer 应用内的预览一样了吧?

我修改了 Quick Look 扩展以后,在 Finder 中预览 Mermaid 文档,看到的仍然是一个普通代码块。Mermaid 源码还在,流程图没有生成。

这很像是 JavaScript 没有执行。但继续检查之前,必须先确认一件更基础的事:Finder 调用的到底是不是我刚刚修改的扩展?

因为 /Applications 里还安装着旧版 Writer。旧版扩展本来就能把 Markdown 转成 HTML,只是会把 Mermaid 当作普通代码块。所以当时看到的结果并不能证明新代码失败了,只能证明 Finder 找到了某个 Writer Quick Look 扩展。

我删除了 /Applications 里的 Writer。删除之后,Finder 一度退回了系统的纯文本预览。这证明刚才把 Mermaid 显示成代码块的,的确是旧版 Writer。

之后我安装了包含新版 view-based Quick Look 扩展的测试版。Finder 不再显示纯文本,右上角也出现了“Open with Writer”。这说明新版扩展终于被调用了。

可这一次,预览窗口变成了一片空白。

所以这里其实有两个完全不同的问题。Mermaid 显示成代码块,是 Finder 调用了旧版 Writer。真正调用新版扩展之后,遇到的问题才是 WKWebView 白屏。

如果不先排除旧版扩展,我很可能会继续在 Mermaid 初始化代码里找问题。可实际上,前一个结果根本不是新代码生成的。

写一个什么都不做的 Quick Look

我临时创建了一个最小的 macOS 应用,只注册一种自定义文件类型,再给它加入一个最简单的 Quick Look view extension。

这个扩展不解析 Markdown,不读取主题,也没有 Mermaid。它只显示一个原生的 NSView,上面写着:

Quick Look view extension is working

安装之后,Finder 正确显示出了这个界面。

这说明 view-based Quick Look 本身没有问题。Finder 能加载扩展,扩展也能提供自己的视图。

不过这还不够。Writer 真正需要的是 JavaScript,不是一个静态的 NSTextField。于是我又把探针改成 WKWebView,加载一段完全内嵌的 HTML,并运行一小段 JavaScript,让页面显示:

JavaScript check: 6 × 7 = 42

结果,探针也变成了白色。

到这里,问题已经很清楚了。不是 Markdown,不是 Mermaid,也不是 Writer 的主题代码。只要在 Quick Look 扩展里换成 WKWebView,页面就出不来。

本地 JavaScript 为什么需要网络权限?

我查看了系统日志,终于看到了真正的错误:

Application does not have permission to communicate with network resources.
Invalid connection identifier (web process failed to launch)

这就很有意思了。

我加载的是 loadHTMLString,HTML 在内存里,JavaScript也在应用包里,没有 CDN,没有远程图片,甚至连一个 HTTP 请求都没有。你告诉我缺少网络权限?

可日志里写得很清楚,不是页面加载失败,而是 WebKit 的 Web Content 进程根本没有启动成功。

我于是给 Quick Look 扩展加入了:

<key>com.apple.security.network.client</key>
<true/>

重新编译,重新安装。

探针页面显示出来了,JavaScript 也成功计算出了 42

故障排除。

按照苹果文档,这个 entitlement 表示沙盒应用可以主动建立网络连接。从字面上看,一个完全离线的 WKWebView 不应该需要它。可在我当前使用的 macOS 版本和 Quick Look 扩展环境中,没有这个权限,WebKit 的内容进程就无法正常启动。

我不敢说所有 macOS 版本、所有 Quick Look 扩展都一定如此。但至少在实际测试中,这不是推测,也不是所谓“可能和沙盒有关”。系统日志和最小探针已经把因果关系摆在这里了。

苹果告诉你 network.client 是用来联网的,却没有告诉你,在 Quick Look 扩展里,即使只加载一段本地 HTML,它也可能决定 WKWebView 能不能活着启动。

这才是这次最坑的地方。

给了网络权限,文档不就可以偷偷联网了吗?

我本来不愿意给 Quick Look 增加网络权限,就是因为快速预览的内容来自用户文件。Markdown 里可以写远程图片,也可以写 HTML。如果直接让这些内容进入一个能够联网的 WKWebView,那么用户只是在 Finder 里按了一下空格,文档就可能向外部服务器发出请求。

这肯定不行。

但这里要分清两件事。扩展拥有网络 entitlement,是为了让 WebKit 的进程能够正常启动,并不意味着页面里的内容也必须获得网络访问能力。

Writer 的 Quick Look 页面加入了严格的 Content Security Policy:

default-src 'none';
img-src data: blob:;
script-src 'nonce-writer-mermaid';
connect-src 'none';
frame-src 'none';
object-src 'none';

也就是说,页面默认什么都不能加载。图片只允许 data:blob:,脚本只允许 Writer 自己注入并带有指定 nonce 的 Mermaid 脚本,网络连接直接设为 none

在 HTML 进入 WKWebView 之前,扩展还会清理 scriptiframeobject、事件属性和其他主动内容。普通 Markdown 的本地附件会转换成 data: URL,远程图片则继续显示占位内容。

这叫给 WebKit 启动进程的权限,不叫给 Markdown 文件自由上网的权限。两者看起来差不多,实际完全不是一回事。

还有一个容易被忽略的问题

改成 view-based extension 以后,Quick Look 的入口也变了。

原来的实现是生成一个 QLPreviewReply。现在则是由 QLPreviewingController 实现异步的 preparePreviewOfFile(at:),自己创建并维护 WKWebView

如果调用 loadHTMLString 以后立刻告诉 Finder“准备好了”,WebView 其实可能还没有完成导航。于是我让扩展等待 WKNavigationDelegate.didFinish,失败、导航失败或者 Web Content 进程终止时,也会结束等待并返回真正的错误。

这不是为了架构漂亮,而是 Quick Look 的生命周期和普通应用窗口不同。应用里的 WKWebView 一直活着,晚一点显示也没关系。Finder 是来向扩展要一份预览的,扩展什么时候回答“准备完成”,本身就是协议的一部分。

同时,Mermaid 的 Tiny 构建和许可证也必须真正进入 .appex 的资源目录。放在主应用里不算。Finder 运行的是扩展进程,它不会因为资源在 Writer.app 的另一个角落,就自动帮你找到。

最后怎么确认不是自我感觉良好?

这类问题只看 Xcode 构建成功没有意义。Quick Look 扩展编译成功,不等于 Finder 正在使用它。Finder 正在使用它,也不等于 WebKit 进程成功启动。WebKit 启动了,也不等于 JavaScript真的执行了。

我最后做了几层验证:

  1. 删除已安装的 Writer,确认 Finder 退回系统纯文本预览。
  2. 安装最小探针,确认原生 Quick Look view extension 能正常显示。
  3. 给探针加入 WKWebView 和 JavaScript,复现白屏。
  4. 加入 network.client 后,确认 JavaScript 成功算出 6 × 7 = 42
  5. 安装新的 Writer,在 Finder 中预览 Mermaid 流程图,确认显示的是生成后的 SVG,而不是 Mermaid 源码。
  6. 再预览我实际使用的 iCloud Markdown 文档,确认标题、段落、编号和列表都正常。
  7. Writer 和 Quick Look 两个 scheme 都重新构建,16 个 Mermaid 测试全部通过。

其中最有价值的不是后面的测试全部通过,而是那个什么都不做的探针。没有它,我很容易继续怀疑 Markdown 渲染器、主题样式、资源路径或者 Finder 缓存。它把几十个变量砍到只剩一个:Quick Look 扩展里的 WKWebView 为什么启动不了?

总结

这次遇到的坑可以归纳成三件事:

  1. data-based Quick Look 能显示 HTML,不代表它会按照应用内 WKWebView 的方式执行 JavaScript。
  2. view-based Quick Look 可以使用 WKWebView,也可以运行 Mermaid,但在实际测试的系统环境中,即使内容完全离线,扩展仍然需要 com.apple.security.network.client,否则 Web Content 进程可能直接启动失败。
  3. 给扩展网络 entitlement 之后,仍然要使用 CSP 和 HTML 清理限制文档内容。权限是让 WebKit 活下来,不是让预览文件随便联网。

所以,Finder 的快速预览使用扩展模式以后,运行起来的确可以和应用内预览很接近。但这个“很接近”不是把 WKWebView 塞进 NSViewController 就结束了。

真正的结论是:Quick Look 扩展可以执行 JavaScript,也可以渲染 Mermaid。之前那片空白不是平台不支持,而是 WebKit 的进程根本没有成功启动。苹果没告诉你的,恰恰就是这一点。