MoreRSS

site iconoldj | 老杰修改

男,80 后,杭州。阿里 8 年,目前在一个小而美的团队。「妙笔 / WonderPen」「ccReader 」作者。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

oldj | 老杰的 RSS 预览

开源本站博客系统 - Publa

2026-05-12 11:24:59

自从 2010 年注册了域名 oldj.net 并架设了这个博客,一转眼已经 16 年了,这么长的时间足够发生很多事,比如杨过和小龙女都已经再次重逢了,本站也经历过了 N 次重构甚至推倒重来。最近,在 AI 的帮助之下,我重写了站点的后台,并决定将这个博客系统开源。

历程

这个博客最早的版本是使用 Django 写的,后台使用了 Django 自带的 admin,前台则使用 Django 的模板输出 HTML。

一段时间之后,我有点懒得继续改进,便迁移到了 WordPress。不得不说,WordPress 的功能非常完善,插件也很多,使用起来非常方便。

再后来,我担心自己维护的 WordPress 有安全问题,便一度将站点迁移到了 WordPress.com 官方服务。

不过,又一段时间后,我发现官方服务有不少限制,比如想在文章中插入数学公式需要升级高级会员,成本实在有点高,加上官方服务位于海外,访问总是有一点慢,于是又迁回了自已部署的 WordPress。

但很快,我发现使用 WordPress 时,如果启用了某些功能或者某些插件,站点仍然需要从海外下载文件,整个博客的访问速度也因此被拖慢,于是又起了自建的心思,花了一些时间,用 egg.js 重写了博客系统。

不过 egg.js 本质上来说仍是上一代框架,或者说是基于后端视角开发的框架,它很适合写后台服务或者传统网站,但对 React 生态的支持则几乎没有。此时,React 等前端框架的边界不断拓展,除了写前端代码,还出现了 Next.js 这样的框架,让它能在服务端渲染 React 组件,解决了之前 React 站点对搜索引擎不友好的问题。

于是,我又开始折腾,用 Next.js 重写了这个博客站点。但这次我只用 Next.js 写了博客的前台页面,后台则改为继续使用 Django,Next.js 通过 API 向 Django 读取数据并渲染。

随着时间的流逝,我又逐渐厌倦了在 Django Admin 中写博客的体验,一直计划着再次改进这个博客,只是一直没抽出时间。

转眼到了 2026 年,AI 的发展如火如荼,Codex、Claude 等模型在写代码方面的表现已经非常出色,也深刻地改变了我日常写代码的方式。我决定花一些时间,在 AI 的帮助下完成这个博客系统后台的改造。

于是,就有了现在这个博客系统,我叫它 Publa(读音:/ˈpʌb.la/,帕布啦)。

设计理念

Publa 不是一个大而全的博客系统,它的目标用户是个人或小团队。在设计和开发过程中,我一直秉承着以下设计理念:

轻量

不要做得太重太复杂,要让部署尽可能简单,比如可以直接在 Vercel 上部署,或者只需要一个 Docker 文件就能部署。

动态

现在有很多静态博客系统,它们使用 Markdown 编写内容,最后生成静态文件并发布上线。

这些博客系统都很不错,且因为是纯静态,运行和维护的成本极低。但缺点也在于纯静态,比如作为静态站点,它们天然不支持评论,如果要让访客发布评论,通常还需要使用第三方工具,无论是体验还是数据,都比较割裂。

如果说评论还能用第三方工具搞定,另外一些比如定时发布、历史记录、站内搜索等等功能,静态博客系统就基本没有办法了。

我希望这个新的博客系统自身就能支持评论、留言、定时发布、站内搜索等功能,因此做成动态便是更好的选择。

易于自定义

Publa 内置了浅色和深色两个主题,同时支持自建主题,或者添加自定义 CSS。

同时,在后台设置中,还开放了自定义 HTML 片断的功能,支持在 <head>末尾、 <body> 开头或末尾等位置插入自定义 HTML。

因此,理论上来说,你可以根据偏好,自定义 Publa 博客前台页面的所有外观样式。

易于迁移

Publa 博客的数据可以一键导出为 JSON 文件,你也可以根据这个格式规范,将其他博客的内容转为 JSON 并导入 Publa。

这个导出功能,也可以用作站点的数据备份。

所见即所得编辑器

Markdown 是一项伟大的技术,我也用 Markdown 写了多年的博客和各类文档。不过,当我开始编写 Publa 后台时,我还是添加了一个基于 TipTap 的所见即所得编辑器。我已经用这个编辑器写了几篇文章,目前感觉良好,尤其是在要插入图片等场景,所见即所得编辑器会更加直观易用。

当然,如果你偏好手写 Markdown,或者想发布已经用 Markdown 格式写好的内容,Publa 后台也支持直接输入 Markdown。

顺便,Publa 的编辑器支持内容自动保存,写作的时候,每隔 5 秒会将编辑器中的内容保存到 localStorage,每隔 30 秒会自动将内容保存到云端草稿,如果连续三次自动保存失败(比如网络中断)会在顶部显示通知;同时,后台还会定期为变化的内容保存历史版本,后续如有需要可随时查看或恢复到之前的版本。基本上,你可以放心地在 Publa 后台撰写文章,而不用担心内容丢失。

支持添加自定义页面

除了博客文章,有时我们还需要添加一些自定义页面,比如「关于」页面。

在 Publa 中,你可以任意添加自定义页面,并指定访问路径,只要这个路径与已有页面不重复即可。自定义页面支持复用当前页面框架(页头、页脚),可用于快速添加一些常见的功能页面,也支持不使用任何模板,由你直接输入整个页面的 HTML,添加完全自定义的页面。

一些特色

除了以上设计理念外,Publa 还有一些值得提一下的特色功能。

支持附件上传

写博客少不了配图,有时可能还要向用户提供一些文件以供下载。Publa 内置支持 AWS S3、Cloudflare R2、阿里云 OSS、腾讯云 COS,如果你有这些平台的账号,可在 Publa 后台添加配置,之后便可以直接在附件管理页面上传和查看附件了。也可以直接在编辑器中粘贴图片,Publa 会自动将图片上传到你配置的平台。

Publa 会管理通过它上传的文件,如果你在 Publa 后台删除了文件,对应的存储平台上的文件也会同时删除。

一键将图片设为 1/2 尺寸

这是我之前写博客时的一个痛点。

随着高分屏的普及,我们给屏幕截图时,得到的图片尺寸通常是截图区域的两倍大小,插入到博客中时,我们希望它能以实际尺寸的 1/2 大小进行显示。

在大多数富文本编辑器中,作者只能拖拽调整图片尺寸,然后凭感觉调一个差不多的尺寸。或者在 Markdown 编辑器中,先查看一下原图的尺寸,再人工计算一下,输入一个 1/2 的宽度。

在 Publa 的编辑器中,我专门优化了这个流程,插入图片后,点击选中图片,图片上方便会浮现一个快捷工具栏,其中便有一个将图片一键设为 1/2 尺寸的按钮。如果你使用高分屏并经常要在文章中插入屏幕截图,那么这个功能对你应该会很有用。

支持添加自定义跳转

在 Publa 中,你可以添加自定义跳转规则,将任意访问路径跳转到指定的路径。

如果你之前在使用其他博客系统,它的文章的路径结构和 Publa 不同,那么你可能会需要这个功能,用于将文章的老路径跳转到新路径,以便用户访问老路径时能正确跳到新的页面。

比如老博客系统中文章路径是 /article/{slug} 这样的形式,但 Publa 中文章路径是 /posts/{slug} ,那么便可以设置一条类似下图的跳转规则,以便平滑迁移。

支持 SQLite 和 PostgreSQL

既然是动态博客,后台数据库自然少不了。Publa 同时支持 SQLite 和 PostgreSQL 两种数据库,你可以根据需要选择。

由于支持 SQLite,如果你想开发调试 Publa 非常简单,只需从 GitHub 上下载源码,安装依赖,然后使用 npm run dev 即可启动测试服务器,在没有指定数据库的情况下,Publa 会自动创建并使用本地 SQLite 以便尽快开始开发调试工作。

值得一提的是,Publa 不仅支持本地 SQLite,还支持 Turso 提供的在线 SQLite 服务。Turso 有足够小项目使用的免费额度,因此,对大多数个人博客或小团队博客来说,可以将 Publa 部署在 Vercel 上并使用 Turso 提供的数据库,完全零成本运行。

小结

以上便是对博客系统 Publa 的介绍。

你现在看到的这个博客就是基于 Publa 的,我已经使用这个系统一段时间了,将持续使用并不断改进完善它,如果你也想架设一个动态博客,不妨试一试 Publa

Publa 基于 MIT 协议开源,任何人都可以免费下载和使用它。

当然,Publa 还很年轻,且之前只有我一位用户,因此可能有一些一直没被发现的 bug,如果你发现了问题,欢迎在 GitHub 上给我提 issue

最后,希望 Publa 能帮你更好地完成博客写作!❤️

当写作软件遇上 AI:Scrivener 论坛上一场持续三年的产品哲学之争

2026-05-02 13:30:55

老牌写作软件 Scrivener 的论坛上有一篇关于是否要支持 AI 的帖子,我花了一些时间仔细地把这篇帖子以及所有回复读了一遍。读完之后,我发现这不是一个简单的“要不要 AI”的技术讨论,更是一场关于写作工具本质、创作者身份认同、以及技术边界的长期争论,也是 AI 为我们这个时代带来的冲击的一个剪影,值得写一篇博客记录一下。

背景

从 2023 年 3 月开始,Scrivener 的官方论坛上发生了一场马拉松式的辩论,主题很直白:要不要给 Scrivener 加上 AI 功能?

这条讨论一直延续到 2025 年底,公开可见的帖子超过 300 条,参与者里有职业作家、程序员、学术研究者,当然还有 Literature & Latte 公司的官方代表。

三年时间里,AI 从一个新鲜话题变成了行业标配,但 Scrivener 的立场却始终清晰而克制。

官方的态度

官方代表的回复让我印象深刻,他们没有用那种“我们永远不会”的绝对化措辞,也没有跟风喊“AI 是未来”,而是用一种近乎工程师式的冷静,把问题拆解成了几个层面。

2023 年 3 月,官方代表 kewms 说得很直接:“We are not interested in any form of subscription model.”(我们对任何形式的订阅模式都不感兴趣)这句话几乎提前封死了“把大模型月费捆绑进 Scrivener”的可能性。

2024 年 5 月,另一位官方代表 AmberV 把理由说得更清楚:他们不愿意把用户的草稿发送到第三方服务器,而消费级硬件上的本地模型“还不够有用”。这不是对新技术的忽视或恐惧,而是对隐私伦理和产品质量的双重坚持。

他们把 AI 工具类比为参考文献软件,Scrivener 可以提供接口,让你用自己选的工具,但不会替你选择、绑死、然后为它的价格变动和服务质量背书。

到 2025 年 11 月,官方给出了一句几乎可以当作政策结论的话:“是否使用 AI 工具,以及使用哪一种,最好留给用户自己决定。”

简单来说,官方温和而坚定地拒绝了在软件中集成 AI 服务。

用户的分歧

然而,讨论并不只是一场“挺 AI”对“反 AI”的二元对立。

支持者里,最有代表性的诉求不是“让 AI 替我写小说”,而是一些听起来很实际的需求:

  • 能不能帮我检查一下,这把枪在第一卷里到底有没有上膛?(长篇小说/系列小说的连贯性检查)

  • 能不能把所有角色的对话按风格分类统计一下?

  • 能不能快速识别出世界观设定里的前后矛盾?

  • 能不能帮我起草一个复杂的正则表达式?

这些需求的共同点是:它们不是要 AI “代写”,而是要 AI “帮忙分析或整理”。

反对者的担忧也不是单纯的技术恐惧,他们在意的是:

  • 隐私:未完成的草稿被送去训练,等于创意还没成型就被“偷走”了。

  • 署名:如果 AI 参与了创作,读者怎么知道哪些是你写的?出版合同里写的“100% 我的作品”还算数吗?

  • 产品定位:Scrivener 的价值就在于它是“少数仍然让人自己写”的空间,一旦加入 AI,这个定位就模糊了。

  • 成本转嫁:如果 AI 功能需要订阅,那 Scrivener 还是那个“买断制、属于你”的工具吗?

自然地,也有一些中间派,他们不反对 AI,但坚持认为更好的做法是:让操作系统或第三方工具来提供 AI 能力,Scrivener 只需要保持开放接口,而不是自己深度绑定某个模型。

一个隐含共识

读完整个讨论,我发现帖子中似乎存在一个隐含的共识:几乎没有人真正支持“让 AI 直接替作者写正文”。

即便是最积极的支持者,也只是把需求限定在头脑风暴、查询、校对、梳理、节奏分析这些辅助性工作上。反对者也基本承认,拼写检查、语法建议这类功能迟早会渗透到操作系统层面。

换句话说,真正被广泛排斥的不是“AI 辅助”,而是“把去作者化的生成机制塞进写作工具的核心”。

如果要做一个类比,大概是这样:人们可以接受用计算器做算术题,甚至能接受带计算器进入考场,但不接受用 AI 直接回答试卷。

真正的分歧

梳理下来,核心分歧其实有三条:

第一,隐私的边界在哪里?
支持者认为,只要透明告知、可以关闭,把数据发到云端就是可接受的;官方和反对者则认为,“别人(别的软件)也这么干”不代表这就是对的,尤其是当这些数据是还没发表的创作草稿时。

第二,外接工具够不够用?
官方认为,Alt+Tab 切换到另一个 AI 工具,或者用 Grammarly、ProWritingAid 这类第三方服务,已经是可行甚至更干净的方案;支持者则觉得这会打断沉浸感,制造重复的拷贝粘贴。

第三,AI 到底是“不可逆的大势”还是“被过度营销的泡沫”?
有人说“AI 会变得无处不在,不适应就会落后”;也有人说“我不认为 AI 行业能撑过五年,成本、能耗、法律风险都没算清楚”。

这三条分歧显然短时间内不会有结论,帖子中直到最后也还没有真正收敛。

一点感想

Scrivener 很克制,这种克制在这个时代显得格外珍贵。

不是因为 AI 不好,而是因为在一个“人人都在加 AI”的环境里,却有一个工具明确说“我们不替你做价值判断,你自己决定”,这本身就是一种立场。

这也符合 Scrivener 一直以来的产品哲学,它不会猜你想写什么,但会给你足够的空间和结构,让你把想写的东西组织好。它的价值不在于“替你做决定”,而在于“让你的决定更容易实现”。它不是一个“智能”工具,而是一个“强大”工具。

从这个角度看,官方的立场就很好理解了:AI 可以存在,但它应该在 Scrivener 的边界之外,由用户自己选择、自己控制、自己承担风险。Scrivener 只需要保持开放,而不是成为某个 AI 服务的前端。

未完待续

当然,故事还没结束。

Windows 侧会不会出现类似 Mac 上 Apple Intelligence 的“官方允许但不深度绑定”的路径?一些用户想要的“非生成式、但高度语义化”的功能,能否在不依赖大模型的前提下实现?版权、训练集伦理、输出的可版权性,这些法律和行业规范层面的问题,什么时候能有定论?

这些问题,到目前为止的讨论里仍然悬而未决。

但至少有一点是清楚的:Scrivener 不会因为“别人都在加 AI”就跟风,也不会因为“市场需要”就放弃自己的产品哲学。

在一个充满焦虑和跟风的时代,这种克制是一种稀缺的品质。

使用 GitHub Action 自动更新 SSL 证书

2026-04-10 12:34:41

之前写过一篇使用 acme.sh 申请 SSL 证书的帖子,最近把这个流程放到了 GitHub Action 上自动化完成,在这儿记录一下要点。

GitHub Action 是托管在 GitHub 上的自动化服务,免费账户的私有仓库每月有 2000 分钟的运行额度,可以用来做很多运维、CI 相关的工作。

要使用 GitHub Action,只需在项目的根目录下的 .github/workflows 中创建一个 .yml 文件即可,比如新建一个 .github/workflows/renew-ssl.yml

基本设置

这个 action 的基本结构类似下面这样:

name: Renew SSL Certificate

on:
  schedule:
    - cron: '0 20 * * 0' # 每周一 UTC 20:00(北京时间 04:00)
  workflow_dispatch: # 支持手动触发

permissions:
  contents: read

jobs:
  renew-cert:
    runs-on: ubuntu-latest
    steps:
      - name: Task name
        run: |
          YOUR SCRIPT

代码很直白,这儿就不多解释了,下面继续看最主要的任务步骤(steps)。

第一步:安装 acme.sh

我们要使用 acme.sh 来申请 SSL 证书,因此第一步需要安装 acme.sh。在以上 YML 文件的 steps 部分,添加以下代码:

      - name: Install acme.sh
        run: |
          curl https://get.acme.sh | sh -s email=${{ secrets.ACME_EMAIL }}

第二步:申请证书

接下来,则是使用 acme.sh 申请域名证书。这儿我们申请了一个泛域名证书:

      - name: Issue or Renew Certificate
        id: issue-cert
        env:
          Ali_Key: ${{ secrets.ALI_KEY }}
          Ali_Secret: ${{ secrets.ALI_SECRET }}
        run: |
          ~/.acme.sh/acme.sh --issue \
            -d oldj.net \
            -d '*.oldj.net' \
            --dns dns_ali \
            --keylength ec-256

你需要将上面代码中的 oldj.net 替换为你自己的域名。申请泛域名证书时,需要同时传入 your-domain.com*.your-domain.com

我的域名是在阿里云上,因此 --dns 参数设置的是 dns_ali 。如果你的域名在其他注册商那里,需要把这个参数改为对应的值,具体可查看 acme.sh 的文档

注意,这儿用到了环境变量 secrets.ALI_KEYsecrets.ALI_SECRET ,这两个值需要你去阿里云后台生成,然后在 GitHub 项目仓库的后台添加。

阿里云后台具体位置是 AccessKey 管理页面,建议创建一个子账号,只授予 AliyunDNSFullAccess 权限即可。

GitHub 添加 Secrets 的位置是:仓库页面 → Settings → Secrets and variables → Actions → New repository secret。

第三步:上传到服务器

证书需要上传到服务器才能发挥作用,在这儿我们使用 SSH 的方式连接服务器。为了能顺利连接服务器,需要你准备一对 SSH 密钥,将公钥上传到服务器的 ~./ssh/authorized_keys 下,同时像上面那样将密钥添加到 GitHub Secrets 中,名称是 SSH_PRIVATE_KEY

接下来,再在 renew-ssl.yml 中继续添加以下代码:

      - name: Setup SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          echo "StrictHostKeyChecking no" > ~/.ssh/config

再接下来是正式的上传步骤:

      - name: Deploy to Server-001
        if: steps.issue-cert.outcome == 'success'
        continue-on-error: true
        run: |
          scp ~/.acme.sh/oldj.net_ecc/fullchain.cer \
              ~/.acme.sh/oldj.net_ecc/oldj.net.key \
              ${{ secrets.SERVER_USER_GATEWAY }}@${{ secrets.SERVER_HOST_GATEWAY }}:${{ secrets.CERT_REMOTE_DIR }}/
          ssh ${{ secrets.SERVER_USER_GATEWAY }}@${{ secrets.SERVER_HOST_GATEWAY }} "sudo nginx -s reload"

注意 SERVER_USER_GATEWAYSERVER_HOST_GATEWAY 分别是你的服务器的用户名和地址(IP 或域名),CERT_REMOTE_DIR 是你准备把证书上传到的服务器文件夹,需要先创建对应的文件夹。这三个变量也像上面一样添加到 GitHub Secrets 中。

步骤的第一行 if: steps.issue-cert.outcome == 'success' 是要确保前面申请证书成功才会执行后续的操作。

最后一行的 sudo nginx -s reload 作用是让服务器上的 Nginx 重新加载配置以及证书,没有这一句,即使证书文件已经上传了,Nginx 仍会继续使用老证书,直到下一次重载或重启。

到这儿,证书的申请 → 上传 → 应用流程就基本完整了,如果你有多台服务器在使用这个证书,可以继续添加上传任务。

更新腾讯云上的证书

我在使用腾讯云 CDN,所以也研究了一下如何自动更新腾讯云的上传证书。其他云服务商应该也有类似的接口。

另外,腾讯云的 EdgeOne 已经可以自动申请和更新免费证书了,无需再自己上传,不过传统的 CDN 服务目前似乎还需要自己管理免费证书。

相关代码如下:

      - name: Upload to Tencent Cloud
        if: steps.issue-cert.outcome == 'success'
        continue-on-error: true
        run: |
          pip install tccli

          # 配置腾讯云 CLI
          tccli configure set secretId ${{ secrets.TENCENT_SECRET_ID }}
          tccli configure set secretKey ${{ secrets.TENCENT_SECRET_KEY }}
          tccli configure set region ap-guangzhou

          CERT=$(cat ~/.acme.sh/oldj.net_ecc/fullchain.cer | base64 -w 0)
          KEY=$(cat ~/.acme.sh/oldj.net_ecc/oldj.net.key | base64 -w 0)

          # 上传新证书
          RESULT=$(tccli ssl UploadCertificate \
            --CertificatePublicKey "$(cat ~/.acme.sh/oldj.net_ecc/fullchain.cer)" \
            --CertificatePrivateKey "$(cat ~/.acme.sh/oldj.net_ecc/oldj.net.key)" \
            --Alias "oldj.net-wildcard-$(date +%Y%m%d)")

          NEW_CERT_ID=$(echo "$RESULT" | jq -r '.CertificateId')
          echo "New certificate ID: $NEW_CERT_ID"

          # 如果有旧证书 ID,自动替换关联资源
          if [ -n "${{ secrets.TENCENT_OLD_CERT_ID }}" ]; then
            tccli ssl UpdateCertificateInstance \
              --OldCertificateId ${{ secrets.TENCENT_OLD_CERT_ID }} \
              --ResourceTypes '["cdn"]' \
              --CertificateId "$NEW_CERT_ID"
            echo "Associated resources updated from ${{ secrets.TENCENT_OLD_CERT_ID }} to $NEW_CERT_ID"
          fi

          # 自动更新 Secret 中的证书 ID,供下次续期使用
          echo "$NEW_CERT_ID" | gh secret set TENCENT_OLD_CERT_ID
          echo "TENCENT_OLD_CERT_ID updated to: $NEW_CERT_ID"
        env:
          GH_TOKEN: ${{ secrets.GH_PAT }}
          GH_REPO: ${{ github.repository }}

请注意将代码中的证书路径(本例中是 oldj.net_ecc)替换为你的实际路径。

这段代码中使用了腾讯云的 tccli 来实现证书上传和更新工作,和上面类似,你需要先设置一些 Secrets:

  • TENCENT_SECRET_ID 腾讯云 API SecretId

  • TENCENT_SECRET_KEY 腾讯云 API SecretKey

  • TENCENT_OLD_CERT_ID 当前正在使用的证书 ID(如果仅上传,不需要自动替换老证书,可不填此项)

其中 TENCENT_SECRET_*可以前往腾讯云 API 密钥管理页面生成,建议创建子账号,授予 QcloudSSLFullAccess 权限,以及你需要更新的资源的权限,比如 QcloudCDNFullAccess

上面的 --ResourceTypes 用于指定要更新的资源,这儿我只写了 CDN,你可以根据需要调整。

首次运行前,需要你先设置一下要更新的证书 ID。由于每次自动更新证书之后,证书 ID 都会变化,为了实现完全自动化,这儿添加了一个 GH_PAT 参数,用于记录你的 GitHub Personal Access Token

这个 GitHub Personal Access Token 的创建方式如下:

  1. 进入 GitHub Settings → Developer settings → Personal access tokens → Fine-grained tokens

  2. 选择对应的组织以及仓库,权限勾选 Secrets → Read and Write

  3. 生成后,将 Token 存入仓库 Secret 中,名称是 GH_PAT

这样自动更新腾讯云上的证书的流程便也自动化了。

AB 工作法

2026-03-25 19:20:00

什么是 AB 工作法

想象一下,你有两个工作台:

A 工作台上摆着你最重要的那个大项目——可能是一个复杂的架构设计,可能是一本书的核心章节,或者是一个需要深度思考的研究课题。这个工作需要你全神贯注,不被打扰。

B 工作台上则堆着其他各种各样的事情——紧急但不复杂的需求、日常维护工作、突发的小任务、需要回复的邮件等等。这些事情重要但不需要长时间的深度投入。

AB 工作法的核心思路是:将各项工作放合适的工作台上,然后你的注意力在这两个工作台之间定期切换。比如上午在 A 工作台专注做深度工作,下午切换到 B 工作台处理各种杂事,或者今天做 A,明天做 B,关键是有节奏地轮换,而不是随机跳跃。

这个方法特别适合那些每天要处理大量不同类型事务的人,——你需要推进重要的长期项目,同时又不能让其他事情堆积成山。如果你的工作是流水线式的,按先来后到依次处理就好,可能用不上这个方法。

它要解决什么问题

我们先来看一个常见的困境。

假设你是一位程序员,你的待处理工作列表很长,每项工作的重要性、紧急程度、耗时都不同。你决定按串行方式处理,每次挑一项最重要的处理,完成之后再挑选下一项。这个策略通常运行良好,但偶尔也会遇到麻烦:某项工作特别耗时,你在上面花了一两周,其他耗时短的小任务就都卡住了,有些原本不紧急的需求因此变得紧急起来。

这背后是两个相互冲突的需求:

一方面,我们需要专注。频繁切换任务的成本很高,因为大脑需要时间重新加载上下文,频繁切换不仅降低效率,还会消耗大量精力,导致精神疲劳。

另一方面,我们又不能长时间陷入一件事。如果完全沉浸在一个大项目中,其他重要但耗时短的任务会被无限期推迟,最终可能演变成紧急问题,甚至引发更大的麻烦。

AB 工作法试图在这两者之间找到平衡。

核心机制

回到“两个工作台”的比喻。AB 工作法的运作方式是:

建立两个工作台

你需要把所有工作分类放到两个工作台上:

  • A 工作台:存放那些需要深度工作的任务——最重要、最复杂、预计耗时较长的工作。这些任务摆在 A 工作台上,等待你在专注时段处理。

  • B 工作台:存放其他所有事务——重要但不太复杂的任务、紧急但耗时短的需求、日常维护工作、突发事项等等。这些任务堆在 B 工作台上,可以在较短时间内逐个处理。

定期轮换

你需要设定一个固定的轮换周期。在 A 时段,你只在 A 工作台工作,专注处理深度任务;在 B 时段,你切换到 B 工作台,灵活处理各种事务。这种有节奏的切换,既保证了主要任务的持续推进,又确保了其他事务不会被长期搁置。

保持纪律

关键是严格遵守“在哪个工作台就做哪个工作台的事”。在 A 工作台时,即使 B 工作台上有看起来很紧急的事,也要记下来留到 B 时段处理。在 B 工作台时,即使突然对 A 任务有了灵感,也要克制住冲动,留到 A 时段再说。

具体怎么做

第一步:给两个工作台分配任务

拿出你的工作列表,开始分类:

哪些任务该放到 A 工作台?

答案是那些“如果不专注投入就很难推进”的工作。对程序员来说,可能是复杂的架构重构;对作家来说,可能是书的核心章节;对研究者来说,可能是关键实验的设计与实施。判断标准是:这个任务需要长时间的连续思考吗?需要进入“心流”状态才能做好吗?如果答案是肯定的,就放到 A 工作台。

哪些任务该放到 B 工作台?

答案是其他所有工作——包括那些重要但不太复杂的任务、紧急但耗时短的需求、日常维护工作、突发事项等等。不用担心 B 工作台上堆得太满太杂,这个工作台本来就是为处理多样化任务设计的。

第二步:设定轮换节奏

根据你的工作性质和个人习惯,选择一个合适的轮换周期:

每半天轮换:上午在 A 工作台,下午在 B 工作台,或者上午在 B 工作台,下午在 A 工作台。这是最常见的节奏,适合工作环境相对安静、任务复杂度高的情况。

每天轮换:今天在 A 工作台,明天在 B 工作台。如果你的深度任务需要更长的连续时间来建立思考状态,这种方式可能更合适。

每个时段轮换:每个工作时段(2-4 小时)切换一次工作台。适合工作环境多变、突发事件较多的情况。

每 2 小时轮换:节奏更快,但不建议更短——太频繁的切换会让上下文切换成本抵消掉收益。

根据效率曲线设置:选择你每天最高效的时间段使用 A 工作台,其他时段使用 B 工作台。比如,如果你在上午 10 点到下午 3 点精力最充沛、思维最清晰,就把这个时段固定为 A 时段,其余时间处理 B 任务。这种方式能最大化利用你的黄金工作时段。

关于时间分配比例

A、B 的时间分配不一定是 1:1。如果你的日常事务相对较少,而深度任务需要大量投入,时间分配可以是 2:1、3:1,甚至可以一周的五个工作日,四天都在 A 工作台,剩下一天在 B 工作台。关键是根据实际工作量来设定合理的比例。

但有一点很重要:一旦定好节奏,就不要轻易改动。频繁调整计划本身就是一种低效的行为,会削弱这个方法的效果。

第三步:严格遵守工作台纪律

这是 AB 工作法成败的关键。

在 A 工作台时:坚决拒绝处理 B 工作台上的事项,即使它们看起来很紧急。你可以把它们记在便签上,但要留到 B 时段再处理。这不是不负责任,而是在保护你的深度工作状态。

在 B 工作台时:不要被 A 工作台上的任务吸引回去,即使你突然有了灵感。把灵感记下来,留到 A 时段再展开。这同样是在保护你的工作模式——B 时段的价值在于灵活机动,如果被大任务拖住,就失去了意义。

这种纪律看似僵化,实际上是在保护两种工作模式的完整性。你可以把自己想象成两个不同的人:A 工作台上的你是专注的深度思考者,B 工作台上的你是灵活的问题解决者。

真正的紧急情况例外

当然,如果遇到真正重要且紧急的突发事件——比如生产系统崩溃、客户的关键问题、或者团队成员急需你的帮助——无论你当前在哪个工作台,都可以立刻停下来去处理。

但需要记住:这种打断应该是罕见的例外,而不是常态。如果你发现自己经常因为“紧急情况”打断工作,可能需要重新审视:这些事情真的都是既重要又紧急吗?还是因为缺乏边界,让所有事情都变成了紧急?

一个简单的判断标准是:如果这件事推迟 2 小时(或推迟到下一个工作台时段)会造成严重后果,那就是真正的紧急情况。如果只是“看起来紧急”但推迟几小时也无妨,那就记下来,留到合适的时段处理。

第四步:动态调整两个工作台

AB 工作法不是一成不变的,需要根据实际情况动态调整:

当 A 工作台清空时:可以从 B 工作台选出最重要、最复杂的任务,把它移到 A 工作台。这样确保你始终有深度任务在推进。

当 B 任务需要升级时:在执行过程中,如果发现某个 B 任务的重要性和复杂度超出了最初的预期,可以把它从 B 工作台移到 A 工作台,作为深度任务来处理。

当 A 任务需要降级时:有时候你可能会发现某个 A 任务其实没那么复杂,或者可以拆分成小块处理,这时也可以把它或者它拆分后的一部分移到 B 工作台。

关键是保持两个工作台的动态平衡,确保 A 工作台上始终有需要深度工作的任务,B 工作台上有足够的机动任务。

特殊情况:B 工作台清空了怎么办?

这是一个有意思的问题。如果到了 B 时段,却发现 B 工作台上的事情都处理完了,你有两个合理的选择:

选择一:转到 A 工作台

既然没有其他事务需要处理,将这段时间用于推进 A 工作台上的任务是很自然的想法,这样可以加快深度任务的进度。

不过需要注意的是,如果你经常这样做,要警惕 AB 工作法是否正在退化成单线程工作模式。偶尔为之没问题,但如果成为常态,可能需要重新审视你的时间分配比例。

选择二:做“元工作”或休息

“元工作”是指那些不属于具体任务,但对工作系统本身有益的活动:整理工作笔记、回顾近期进展、学习新技能、优化工作流程、清理工作环境、或者进行一些前瞻性思考。这些活动往往在忙碌时被忽视,但长期来看对提升工作质量和效率很有帮助。

如果找不到合适的“元工作”,那就干脆休息一下,——散散步、冥想、或者做一些轻松的事情,为下一个 A 时段储备精力。

最重要的是,不要因为 B 工作台暂时清空就感到焦虑或内疚。这恰恰说明你的工作节奏是健康的,——你既在推进重要的长期项目,又及时处理了其他事务。这正是 AB 工作法追求的理想状态。

为什么 AB 工作法有效

AB 工作法的有效性来自于它对人类认知特性的尊重和对工作现实的妥协:

它承认专注的价值

通过为深度任务分配专门的时间块,我们能够进入心流状态。在 A 工作台上,你不需要担心其他事情,可以全身心投入到复杂的思考中。这种不被打扰的专注,是完成复杂工作的必要条件。

它承认灵活的必要

通过 B 工作台的存在,我们不会因为过度专注而忽视其他重要事项。那些看似琐碎但不可或缺的工作——回复邮件、处理临时需求、日常维护——也能得到及时处理,不会堆积成山。

它提供了心理缓冲

知道“我只需要在这个时段专注于 A 工作台,其他事情稍后会有专门时间处理”,这种确定性能够显著降低焦虑。你不会因为“还有很多其他事没做”而分心,也不会因为“一直在做杂事”而焦虑。

它创造了自然的休息点

工作台切换本身就是一种认知上的休息。从深度思考切换到处理多个小任务,或者反过来,都能让大脑的不同区域得到交替使用和恢复。这比连续 8 小时做同类工作要健康得多。

适合谁用

AB 工作法特别适合以下人群:

  • 需要同时推进长期项目和处理日常事务的知识工作者

  • 工作内容多样、优先级复杂的管理者、创业者或独立开发者

  • 需要在创造性工作和事务性工作之间切换的专业人士

  • 容易陷入“只做紧急事,忽视重要事”或“只做重要事,忽视紧急事”两个极端的人

局限性

当然,AB 工作法也有它的局限性:

如果你的工作本质上是单线程的(比如流水线工人、客服人员),这个方法可能过于复杂。如果你的工作环境完全不可控、频繁被打断,可能需要先改善工作环境,再尝试这个方法。如果你正处于某个项目的冲刺期,需要全力以赴,暂时放弃 AB 结构、全力投入也是合理的选择。

一些实践建议

从简单的节奏开始

如果你是第一次尝试 AB 工作法,建议从每天轮换开始,而不是每小时。今天在 A 工作台,明天在 B 工作台。这样更容易建立习惯,也更容易感受到效果。等习惯养成后,再尝试更短的轮换周期。

用工具标记当前工作台

可以使用日历、番茄钟或专门的时间管理工具来标记你当前在哪个工作台。视觉提示能够帮助你更好地保持纪律。有些人会在桌面上放两个不同颜色的便签纸,A 时段翻开绿色,B 时段翻开黄色,这种物理提示也很有效。

每周回顾与调整

每周花 15 分钟回顾一下:A 工作台上的任务推进得如何?B 工作台是否有积压?两个工作台的任务划分是否合理?轮换节奏是否合适?根据实际情况调整策略。这种定期回顾能帮你不断优化自己的工作节奏。

让同事知道你的节奏

如果你在团队中工作,让同事知道你的工作方式。比如在日历上标注“深度工作时段(A 工作台)”,让他们知道这段时间最好不要打扰你。大多数同事会理解并尊重这种安排,因为他们自己可能也有类似的需求。

保持弹性

AB 工作法是一个框架,不是枷锁。真正的紧急情况出现时,当然可以打破规则。关键是这种打破应该是例外,而不是常态。如果你发现自己经常打破规则,可能需要重新审视你的时间分配或任务分类。

扩展:可以有更多工作台吗?

AB 工作法的核心是“两个工作台”,但这不是唯一的可能。如果你的工作情况更复杂,完全可以扩展为三个、四个甚至更多工作台。

什么时候需要更多工作台?

如果你的工作内容跨越多个几乎不相关的领域,每个领域都有自己的深度任务和日常事务,那么多个工作台可能更合适。

比如,假设你同时负责产品开发和市场推广两个领域,你可以采用 ABC 工作法:

  • A 工作台:产品开发方面的深度工作——架构设计、核心功能开发、技术难题攻关

  • B 工作台:市场推广方面的深度工作——营销策略规划、重要内容创作、关键活动策划

  • C 工作台:所有其他日常事务——邮件回复、会议、临时需求、日常维护

然后制定一个轮换策略,比如:周一周二在 A 工作台,周三周四在 B 工作台,周五在 C 工作台。或者每天上午在 A 工作台,下午在 B 工作台,晚上处理 C 工作台的事情。

需要注意的是

工作台越多,轮换的复杂度就越高,上下文切换的成本也会增加。大多数情况下,两个工作台就足够了。只有当你确实需要在多个完全不同的领域之间切换,并且每个领域都有足够的工作量时,才考虑增加工作台数量。

一个简单的判断标准:如果你发现自己在某个“深度工作台”上经常没事可做,或者某两个工作台上的任务其实可以合并处理,那就说明工作台分得太细了,不如简化回两个工作台的结构。

写在最后

AB 工作法的本质,是在专注与灵活、深度与广度、长期与短期之间寻找平衡。它不是什么神奇的生产力秘诀,而是一种务实的工作组织方式。

想象你面前真的有两个工作台。A 工作台干净整洁,只摆着少量需要全神贯注的大项目;B 工作台上堆着各种各样的事情,等待你灵活处理。你在这两个工作台之间有节奏地切换,既不会因为过度专注而忽视其他事务,也不会因为琐事缠身而无法推进重要项目。

在这个信息过载、任务繁杂的时代,我们既需要能够深入思考的专注时光,也需要能够快速响应的灵活机制。AB 工作法提供了一个简单但有效的框架,让这两种看似矛盾的需求能够在同一个人的工作节奏中和谐共存。

如果你也面临着“重要的大项目总是被琐事打断”或“专注于大项目时其他事情都荒废了”的困扰,不妨试试 AB 工作法。给自己两周时间实验,你可能会发现一种全新的工作节奏。

使用 uv 管理 Python 依赖

2025-11-29 15:26:00

我有一个运行了好几年的 Django 项目,之前一直在使用默认的 pip 管理和安装依赖,最近切换到了 uv,感觉还不错,在这儿记录一下。

什么是 uv

根据官网的介绍,uv 是一个 Python 包以及项目管理器,非常快,使用 Rust 开发。

安装和管理依赖只是它的功能之一,除此之外,它还可以创建虚拟环境,即可以取代 pip + virtualenv 的功能。

我测试了一下,uv 确实比 pip 快了很多。在使用相同的镜像源,且都是纯净的 docker 环境下,使用 pip 安装项目的依赖花了约 88 秒,但使用 uv 只用了 13 秒。

不过,安装依赖并不是一个高频操作,多花一点时间一般不是什么痛点,uv 更吸引人的是它简化了很多工作,比如内置了 Python 多版本安装以及虚拟环境管理,且能保证环境的可复现性,这就让 Python 项目的开发和发布工作简单了很多。

uv 的安装和初始化

在 macOS 或 Linux 上,可以使用以下命令安装 uv:

curl -LsSf https://astral.sh/uv/install.sh | sh

Windows 上的命令如下:

# On Windows.
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

安装完成之后,可以使用以下命令初始化一个新项目:

uv init my-project

如果你的项目已经存在,也可以直接进入项目根目录,执行以下命令:

uv init

如果项目中已经有 requirements.txt,想改用 uv 进行管理,可以在项目根目录执行以下命令:

uv pip install -r requirements.txt

也可以直接执行:

uv sync

初始化后,uv 会在项目根目录下生成一个 pyproject.toml 文件,其中包含了项目的基本信息以及依赖项。

可以运行以下命令,锁定依赖项:

uv lock

这个命令将会在项目根目录下生成 uv.lock 文件,类似 Node.js 的 package-lock.json,其中包含了项目依赖的各个第三方库以及版本号,确保下次安装时安装的包相同。

虚拟环境

Python 默认是安装在系统中的,使用 pip 安装依赖时,默认也会全局安装,在很多情况下,尤其是需要维护多个项目时,这显然不是我们期望的,此时可以使用虚拟环境。

在 uv 中使用虚拟环境很简单。

首先,可以使用 uv 安装多个不同版本的 Python:

uv python install 3.10 3.11 3.12

然后,可以通过类似下面的命令安装虚拟环境:

uv venv --python 3.12.0

可以直接在项目根目录下执行这个命令,执行成功之后,项目根目录下会生成一个 .venv 文件夹,包含这个环境的所有信息,之后安装的包也会保存在这个文件夹下,记得将这个文件夹添加到 .gitignore 中。

如果你熟悉 Node.js,会发现这个 .venv 文件夹和 Node.js 的 node_modules 文件夹功能类似,且它更进一步,不仅包含依赖,还能包含当前项目所需的 Python 本身。

使用以下命令可以用虚拟环境中的 Python 执行指定脚本:

uv run example.py

如果你在终端中访问项目,可以使用以下命令激活当前 Python 虚拟环境:

source .venv/bin/activate

激活虚拟环境之后,可以直接用类似 python example.py 的方式来运行项目中的脚本。

使用现代 IDE(比如 PyCharm、VSCode 等)打开这个项目时,IDE 一般都能自动识别项目中的 .venv 虚拟环境。

安装依赖

配置好环境后,就可以使用类似下面的命令安装依赖了:

uv add django

这信命令会下载对应的包并安装在 .venv中,安装成功之后,会修改 pyproject.tomluv.lock 文件。

如果你刚将代码从仓库中拉到本地,项目中已经有了 pyproject.tomluv.lock,那么只需执行以下命令即可安装所有依赖:

uv sync

注意,这个命令会根据 pyproject.toml 解析和下载依赖,有可能会改进 uv.lock。如果是在生产环境,你希望严格按照 uv.lock 中的版本安装依赖,可以使用以下命令:

uv sync --frozen

在 docker 中使用 uv

如果你的项目需要使用 docker 发布,还有一些额外需要注意的事项。

如果你的服务器在国内,那么可能需要使用国内 pypi 镜像,uv 中要指定镜像很简单,设置相应的环境变量即可,例如下面设置使用了阿里云的镜像:

ENV UV_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/
ENV UV_TRUSTED_HOST=mirrors.aliyun.com

在 docker 中安装依赖时,大体上有两种方式,一种是使用 uv sync 命令,如下所示:

WORKDIR /code/
COPY pyproject.toml uv.lock /code/

# 安装依赖
RUN uv sync --frozen --no-dev

ENV PATH="/code/.venv/bin:$PATH"

这种方式会在当前目录下创建虚拟环境,所有依赖都将安装到 .venv 目录下,因此需要将对应的目录加入 PATH。这种方式安装的依赖将严格遵守 uv.lock 中的版本限制,最为可靠。

或者,也可以选择将依赖直接安装到系统环境中:

RUN uv pip install --no-cache-dir --system .

注意那个 --system 参数,这种方式会将各依赖包安装到全局目录,如果你的 docker 中只有这一个 Python 项目,且不想使用虚拟环境,也可以使用这种方式安装。不过,这种方式安装时虽然也会参考 uv.lock,但并不保证各依赖的版本和 uv.lock 中严格相同。

小结

Python 发布迄今已有三十余年,一开始并没有第三方包的安装和管理工具,这和 Node.js 一发布就自带 npm 不同。如果你使用 Python 的时间较早,可能还会记得曾经有一个叫 easy_install 的工具用于安装 Python 的第三方包。

约 2008 年,pip 发布,随后在 2014 年被 Python 官方集成到 3.4 版中(以及 2.7.9 中),Python 这才有了一个官方的依赖管理工具。不过 pip 并不完美,主要是依赖解析能力较弱,无法保证每次安装后的环境完全相同,同时安装速度也有一些慢,因此后续又出现了一些新的依赖管理工具,比如 poetry、uv 等。

目前,开发 Python 项目的最佳实践是为每个项目创建独立的虚拟环境,将项目所需的依赖安装在该环境中,并通过依赖管理文件记录依赖,以确保隔离、可复现和可移植性。这些工作都可以使用 uv 完成,如果你正在开发或维护一个 Python 项目,不妨试试 uv。

富文本框架体验

2025-06-21 17:31:00

由于项目中要使用富文本编辑器,过去一段时间我深入研究了一下几个知名的富文本框架,在这儿写一下体验。

这不是一个全面的分析,仅代表个人观点。

Quill

我最开始尝试的是 Quill 项目,因为它看起来比较简单同时又足够强大,更重要的是它还有一个 Flutter 版本,可用于移动端应用的富文本开发。

实际体验上,Quill 确实比较容易上手,根据文档很容易就能写出一个基本可用的富文本编辑器来。当然,如果要添加更多复杂的或自定义功能,就要继续深入研究了。

Quill 的数据结构比较特别,它自定义了一个名为 Delta 的 JSON 格式用于描述文档,这种格式概念很简单,比如只有 insertdeleteretain 三种操作,但理论上足以描述任意复杂的文档格式以及修改。

在 Delta 的基础上,Quill 的文档可以被认为是一个“流”,只需一个数字就可以表示文档的任意位置,这让使用代码来查找、修改文档的操作变得非常简单。

一切看起来都很美好,不过当我继续深入,想实现一些更复杂的功能之时,逐渐发现 Quill 存在一些较为严重的不足。

第一个也是最大的不足,是 Quill 缺少“装饰器”设计。

所谓“装饰器”,是给文档中指定内容临时添加一些样式,但又不会影响文档数据的功能。

举例来说,实现关键词搜索时,我们需要把文档中所有匹配的关键词全部高亮显示(比如背景显示为黄色),但这个高亮样式只是临时性的,需要与富文本中原本就有的样式区分开来,添加这种装饰性的样式时,不需要触发文档的 onChange 事件,此时如果获取文档的内容,得到的数据中也不应该包含装饰性的样式。

缺少了装饰器功能,如果想高亮匹配的搜索关键词,就只能直接修改文档数据,在文档的原始数据中为这些关键词添加样式,随后在搜索结束时再去除这些样式。这便需要开发者自行添加和维护这些临时样式的状态,并区分哪些修改是真正的修改可以保存,哪些则只是装饰性修改不需要保存,显然,这会让代码逻辑变得复杂,且由于不是框架底层原生支持,性能上也会差很多。

第二个问题则和 Delta 格式有关。

使用了一段时间之后,我发现 Delta 这种格式用于描述纯文本或简单富文本的修改确实很方便,生成的 JSON 也很容易阅读,但如果要描述带有复杂嵌套结构的 HTML,Delta 反而会让问题复杂化。

同时,应该是实现上还存在 bug,当文档中有一些格式存在重叠、嵌套时,如果对它们再次格式化,可能会让内容变得混乱。对此我在 Quill 的 GitHub 仓库中提交了一个 issue ,不过暂时没有得到回应。

Slate

随后,我又尝试了 Slate ,一个基于 React 的富文本框架。

Slate 非常强大,且相较 Quill 自由度更高,基本上可以用它实现任何想要的功能。它自带装饰器(Decorations),因此,诸如高亮搜索关键词等功能的实现都变得很容易。对应地,它的概念也比 Quill 多了一些,学习成本稍高。

我用 Slate 基本实现了整个需求,原本已经基本准备正式使用它了,不过也许是我对它的理解还不够,遇到了一些始终没能处理好的小问题,虽然不影响主要功能,但却让编辑器在体验上总是差了那么一点。

比如,有时候选中了文档内容,鼠标点在空白处时不会取消选中,要再点一次才行。还有一些中文输入法似乎与它存在兼容问题,一些操作会让整个编辑器崩溃,我研究了很久也找不到修复或者处理方法,只能在外层容器捕获错误,提示用户刷新页面。

ProseMirror

最后,我决定转向 ProseMirror

ProseMirror 是一个老牌富文本框架,它的作者还写过知名的代码编辑器 CodeMirror。ProseMirror 非常强大,但因为学习曲线相对陡峭,因此劝退了很多人,包括之前的我也一度先尝试 Quill、Slate 等方案。

也有一些基于 ProseMirror 的富文本框架,比如 TipTap ,这也从另一个角度展示了 ProseMirror 的强大。

ProseMirror 的文档对新手不太友好,且它有一些自已的概念,比如文档的结构由 Schema 定义和描述,因此需要花一些时间才能上手,不过,上手之后会发现它的文档其实非常详细,基本可以从中找到需要的一切信息。

在使用 ProseMirror 将需求重新实现之后,我发现在使用 Slate 遇到的那些恼人的小问题要么没有了,要么有解决方案,而且在处理有较多装饰器的长文档时,ProseMirror 的性能似乎更好一些。

当然,ProseMirror 也不能说十全十美,不过,在深度使用了几个流行的富文本编辑器框架之后,个人觉得 ProseMirror 是综合来说最强大也最值得学习的。

小结

富文本编辑器是前端开发中最复杂也最难的主题之一,幸运的是多数情况下我们不需要完全从头开始开发,而是可以选择一个基础框架,基于它进行二次开发。

网上有很多富文本编辑器框架的评测,你可以根据自己的需求以及实际情况进行选择。

如果你希望尽快完成项目,且你的文档结构不会很复杂,也没有装饰器等需求,可以考虑 Quill。

如果你的需求比较复杂,且在使用 React,可以考虑 Slate。

最后,如果你的需求很复杂,且你的时间不那么紧张,推荐选择 ProseMirror。