原生 iOS 开发二十年,复杂文本处理为何还是 Web 技术更靠谱?

2026-05-18 23 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:11 分钟

一位写了近二十年 macOS/iOS 原生代码的开发者,最近得出一个让 Apple 生态开发者不太舒服的结论:在复杂文本处理场景下,Electron 等 Web 技术反而比 Apple 原生框架更可靠。Artem Loenko 把这段从信心满满到被迫转向的经历写在了博客上——他原本要在纯 Swift/SwiftUI 应用里实现一个支持 Markdown 的富文本编辑器,结果一路踩坑,最终发现原生文本栈的短板不是靠经验就能绕过去的。

这个结论看似违背直觉,但背后的问题非常具体,值得每个做过富文本功能的人认真看看。

原生文本栈的硬伤在哪里

Apple 的文本处理体系并不算薄弱——NSAttributedStringUITextView、TextKit、TextKit 2,名字一长串。但当你真正要处理"复杂"文本时,这些框架的短板会集中暴露:

属性模型的碎片化。 NSAttributedString 的 attribute dictionary 是扁平的键值结构,没有嵌套能力。一段文本同时是粗体、链接、代码行内标记,你只能把所有属性平铺在同一个 range 上。属性之间有冲突时(比如一段既是标题又是链接),没有优先级机制,靠开发者手动拼逻辑。

布局与渲染耦合过深。 TextKit 的布局路径和渲染路径缠绕在一起,想单独控制排版细节(比如代码块的等宽字体切换、嵌套列表的缩进层级),往往要深入 override layout manager 的内部方法,改动牵一发动全身。

SwiftUI 的文本能力更薄。 SwiftUI 的 Text 视图目前只支持基础属性拼接,TextFieldTextEditor 对富文本的支持几乎为零。想在 SwiftUI 里做 Markdown 渲染,要么退回 UIKit representable,要么自己从头写渲染管线。

Loenko 的经历正是这些问题的叠加:Markdown 解析后的 AST 是嵌套树结构,但要把这棵树映射到 NSAttributedString 的扁平属性模型,每一步都在做"把树压平"的暴力转换,而且压平之后丢失的结构信息,在后续编辑操作中还得再从扁平模型里猜回来。

Web 技术为何在这个场景下胜出

HTML + CSS 的文本模型天然就是嵌套的。一个 Markdown AST 映射到 HTML 几乎是零损耗:

<p>这是一段<strong>粗体</strong>文本,包含<a href="...">链接</a><code>行内代码</code></p>

<strong><a><code> 可以自由嵌套,CSS 处理冲突属性有明确的 cascade 规则。浏览器引擎的布局算法(Blink/Gecko)对复杂排版的处理能力,经过二十多年迭代,远超 TextKit 在移动端的表现。

Electron 把这套能力打包进桌面应用,代价是内存和包体积,但换来的是:

  • DOM 的嵌套属性模型直接对应 Markdown AST,不需要树→扁平→树的来回转换
  • CSS 的排版能力覆盖等宽切换、嵌套缩进、行间距微调等原生框架要大量 hack 的场景
  • 成熟的编辑器生态(CodeMirror、ProseMirror、Slate)直接可用,不用从零造轮子

这不是"Web 万能"的泛泛之谈,而是文本处理这个特定领域里,DOM 模型与 Markdown AST 的结构同构性带来的真实优势。

实际对比:同一个 Markdown 渲染任务

下面用两段可运行的代码展示同一个任务——把一段含粗体、链接、行内代码的 Markdown 渲染为可交互的富文本——在原生和 Web 两条路径上的实现差异。

Swift 原生路径:手动拼 NSAttributedString

import UIKit

// 假设 Markdown 已解析为以下节点列表
// (实际项目中你需要自己写或引入一个 Markdown parser)
struct MDNode {
    let type: String   // "text", "bold", "link", "code"
    let content: String
    let href: String?  // 仅 link 类型有值
}

let nodes: [MDNode] = [
    MDNode(type: "text",   content: "这是"),
    MDNode(type: "bold",   content: "粗体"),
    MDNode(type: "text",   content: "文本,包含"),
    MDNode(type: "link",   content: "链接", href: "https://example.com"),
    MDNode(type: "text",   content: "和"),
    MDNode(type: "code",   content: "行内代码"),
    MDNode(type: "text",   content: "。")
]

func renderNodes(_ nodes: [MDNode]) -> NSAttributedString {
    let result = NSMutableAttributedString()
    for node in nodes {
        let segment = NSMutableAttributedString(string: node.content)
        switch node.type {
        case "bold":
            segment.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: 16),
                                 range: NSRange(location: 0, length: segment.length))
        case "link":
            segment.addAttribute(.link, value: node.href ?? "",
                                 range: NSRange(location: 0, length: segment.length))
            segment.addAttribute(.foregroundColor, value: UIColor.systemBlue,
                                 range: NSRange(location: 0, length: segment.length))
        case "code":
            segment.addAttribute(.font, value: UIFont(name: "Menlo", size: 14) ?? UIFont.systemFont(ofSize: 14),
                                 range: NSRange(location: 0, length: segment.length))
            segment.addAttribute(.backgroundColor, value: UIColor.systemGray6,
                                 range: NSRange(location: 0, length: segment.length))
        default:
            segment.addAttribute(.font, value: UIFont.systemFont(ofSize: 16),
                                 range: NSRange(location: 0, length: segment.length))
        }
        result.append(segment)
    }
    return result
}

// 使用:赋值给 UITextView 的 attributedText
let textView = UITextView()
textView.attributedText = renderNodes(nodes)
textView.isEditable = false
textView.isSelectable = true

注意几个问题:属性是平铺追加的,如果某个节点同时是粗体+链接,你要在同一段 NSMutableAttributedString 上叠加多个 attribute,管理 range 的重叠和冲突全靠手动;代码块的背景色是逐字符加的,换行后的整行背景需要额外用 exclusion rect 或 background attribute hack;编辑时反向解析(从 NSAttributedString 回推 Markdown AST)完全没有现成方案。

Electron / Web 路径:Markdown AST → HTML

// 最小 Electron 示例:Markdown 渲染 + 可交互富文本
// 项目结构:package.json + main.js + index.html

// package.json
// {
//   "name": "md-editor-demo",
//   "version": "1.0.0",
//   "main": "main.js",
//   "dependencies": { "marked": "^12.0.0" }
// }

// main.js
const { app, BrowserWindow } = require('electron');
const path = require('path');

app.whenReady().then(() => {
  const win = new BrowserWindow({
    width: 800, height: 600,
    webPreferences: { nodeIntegration: true, contextIsolation: false }
  });
  win.loadFile('index.html');
});

// index.html
const { marked } = require('marked');

const mdSource = `这是**粗体**文本,包含[链接](https://example.com)和\`行内代码\`。`;

// marked 直接把 Markdown 转成嵌套 HTML
const htmlOutput = marked.parse(mdSource);

document.getElementById('editor').innerHTML = htmlOutput;

// CSS 处理所有排版细节——等宽、背景、间距,零 JS 逻辑
const style = document.createElement('style');
style.textContent = `
  code {
    font-family: 'Menlo', 'Consolas', monospace;
    font-size: 14px;
    background: #f0f0f0;
    padding: 2px 4px;
    border-radius: 3px;
  }
  a { color: #0066cc; }
  strong { font-weight: 600; }
`;
document.head.appendChild(style);

运行方式:npm install && electron .

关键差异:marked.parse() 输出的 HTML 是嵌套结构,<strong><a> 可以自然共存于同一段文本内;CSS 以声明式方式处理所有排版,不需要逐字符计算 range;如果需要编辑能力,接入 CodeMirror 或 ProseMirror 后,编辑操作直接在 DOM 树上进行,AST ↔ 视图的双向映射由编辑器框架维护。

选择时的真实考量

Loenko 的反思不是"原生不行,Web 赢了"的二元结论,而是针对特定场景的务实判断。做选择时可以参考以下清单:

考量维度 原生路径更适合 Web/Electron 路径更适合
文本复杂度 纯展示、属性单一(粗体/颜色) Markdown/富文本编辑、嵌套属性多
编辑交互 只读或简单选区操作 实时编辑、AST 双向同步
排版需求 基础排版,无代码块/表格 代码块、嵌套列表、表格、混合字体
包体积敏感度 移动端必须小 桌面端可接受 ~150MB
内存敏感度 后台常驻、多窗口 单窗口、短期使用
团队技能 深度 Swift/UIKit 经验 前端/Web 编辑器经验

几个务实的折中方案也值得考虑:

  • 混合架构:核心编辑区用 WebView/Electron 渲染文本,外壳壳和导航栏保持原生,VS Code 和 Slack 都是这个模式
  • WKWebView 内嵌:iOS/macOS 应用中用 WKWebView 加载本地 HTML + JS 编辑器库,避免 Electron 的包体积代价,同时获得 DOM 的文本处理能力
  • 原生渲染 + Web 编辑:渲染用原生(只读场景足够),编辑态切换到 Web 视图,编辑完成后再同步回原生模型

写在最后

Loenko 二十年的原生经验没有白费——正是深度使用原生框架之后,他才真正看清了文本栈的结构性短板。这不是能力问题,是模型问题:NSAttributedString 的扁平属性字典和 TextKit 的布局-渲染耦合,在嵌套文本场景下就是先天不足。DOM 的嵌套结构和 CSS 的声明式排版在这个特定领域是更对的抽象。

下次接到"做个 Markdown 编辑器"的需求时,先问一句:文本嵌套深度到哪一级?如果答案超过两层,认真评估 Web 技术可能比硬扛原生更省时间——这不是妥协,是对问题结构的诚实回应。


相关推荐