<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>Lawrence Li</title><subtitle>Blog</subtitle><link href="https://lawrenceli.me/atom.xml" rel="self" type="application/atom+xml"/><link href="https://pubsubhubbub.appspot.com/" rel="hub"/><link href="https://lawrenceli.me/"/><id>https://lawrenceli.me/</id><updated>2026-02-25T02:38:48.465Z</updated><follow_challenge><feedId>55815884011044881</feedId><userId>189366433304344576</userId></follow_challenge><author><name>Lawrence</name><email>hi@lawrenceli.me</email></author><entry><title>写在 2025 年末</title><id>https://lawrenceli.me/blog/2025-in-review</id><link href="https://lawrenceli.me/blog/2025-in-review"/><published>2025-12-31T00:00:00.000Z</published><updated>2025-12-31T00:00:00.000Z</updated><category term="review" label="review" scheme="https://lawrenceli.me/tag/review"/><category term="2025" label="2025" scheme="https://lawrenceli.me/tag/2025"/><summary type="html">&lt;h2 id=&quot;tech&quot;&gt;&lt;a href=&quot;#tech&quot;&gt;Tech&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;互联网行业从业六七年了，最关注的必然是养活自己的 Java 生态。很多人说 Java 很臃肿，写个 Hello World 都费劲。然而实际上只需稍微关注一下最新今年 9 月 Java 25 LTS 最新版本的语法：&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;void&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; main&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;  IO&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;println&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;Hello Java 25!&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然 &lt;a href=&quot;https://openjdk.org/jeps/445&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;JEP 445&lt;/a&gt; 这种类似脚本的语法糖代码已经脱离了一切皆对象的原则，但这恰恰是 Java 与时俱进的标志。另外几个不得不提的：结构化并发，虽然仍然在预览阶段，等它 GA 很有必要好好聊聊，配合虚拟线程，Java 的多线程模型简直起飞；模块导入声明简化了大量 import 语句，自 Java 9 以来的模块化的好处初见成效了。&lt;/p&gt;
&lt;p&gt;其他领域的观察/观点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C/C++ 有被 Rust 替代的趋势&lt;/li&gt;
&lt;li&gt;Next.js / React 服务端组件的重大漏洞让 React 开发者感受到 Log4J 类似境遇&lt;/li&gt;
&lt;li&gt;uv 统治了 Python 包管理器等工具链&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我自己关于 AI 的暴论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;搜索引擎这个互联网入口正在被 LLM 取代&lt;/li&gt;
&lt;li&gt;TailwindCSS 在 LLM 时代存在 Token 红利&lt;/li&gt;
&lt;li&gt;前后端混合的同构技术栈（比如 Next.js 服务端渲染）会成为 Vibe Coding 的首选技术栈&lt;/li&gt;
&lt;li&gt;MCP 协议正在向操作系统端迁移成为终端设备上的基础设施&lt;/li&gt;
&lt;li&gt;人类获取知识信息的时间复杂度几乎达到了 O(1)&lt;/li&gt;
&lt;li&gt;程序员写的代码是属于长期负债，LLM 产生的代码则是程序员的流动负债，绝非净资产&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;work&quot;&gt;&lt;a href=&quot;#work&quot;&gt;Work&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;上一份工作在 SaaS 行业，属于人力资源行业内领头公司的老员工出来创业组建的一只小团队。总结下来的感受，SaaS 在国内很难做。由于是新项目，客户本就不多，最后几个月需求明显骤减，年中果然吃了散伙饭。虽然很快就换了工作，但每次想起那些代码写下后没有回响，仍然很难释怀。当然创业公司的好处就是很自由，公司架构精简，老板兼职商务销售经常出差，产品兼职人事/测试，不打卡，不用和外部客户沟通，节假日也几乎不会出现紧急线上问题，或许是因为没业务:(。我来到上海第一年做在线教育也是类似的团队。可能我自己一直喜欢从零到一构建点东西，所以更偏向这类创业公司的氛围。&lt;/p&gt;
&lt;p&gt;换了工作后，在新公司从事互金相关业务，工作性质和公司规模导致部分福利或多或少不如先前，开发需求也多出了不少。但新公司有一定规模的业务，节奏稳定、方向明确，每个人只需要把自己负责的那一条脉络梳理好即可。&lt;/p&gt;
&lt;p&gt;从个人成长的角度看，这样的环境也并非没有价值。系统规模更大，历史包袱更重，也意味着需要更谨慎地做决策、更耐心地理解上下游逻辑。现在看来，开发在多数时候并不是用来「创造」，而是用来「维持」：保证稳定、避免风险、在有限的空间内做最优解，这本身也是一种能力。只是偶尔在需求间隙，还是会不自觉地回想起以前那些可以随意推翻重来的日子。那时写代码，更多是在回答「能不能这样设计」；而现在，更多是在权衡「应不应该这么改」「如何避免改动的破坏性影响」。前者更像是在白纸上画草图，后者则是在维护一栋已经上路的汽车，各有意义，也各有约束，无可厚非。&lt;/p&gt;
&lt;p&gt;或许职业生涯本就会在不同阶段切换角色：有时是开荒的人，有时是守成的人。眼下这段经历，大概更接近后者。至于未来是否还会再回到从零开始的状态，目前还很难下结论。但至少在当下，把手头的事情做好，理解这套体系为何如此运转，也未尝不是一种积累。毕竟，很多判断只有在真正站到另一种位置上，才能看得更清楚。&lt;/p&gt;
&lt;h2 id=&quot;life&quot;&gt;&lt;a href=&quot;#life&quot;&gt;Life&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;music&quot;&gt;&lt;a href=&quot;#music&quot;&gt;Music&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;年度最佳歌曲： &lt;em&gt;&lt;a href=&quot;https://music.163.com/#/song?id=2154850334&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;BIRDS OF A FEATHER&lt;/a&gt;&lt;/em&gt; - Billie Eilish&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;movie--shows&quot;&gt;&lt;a href=&quot;#movie--shows&quot;&gt;Movie / Shows&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;年度最佳剧集： &lt;em&gt;&lt;a href=&quot;https://movie.douban.com/subject/36689815/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;流人 第五季&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;年度最佳电影： &lt;em&gt;&lt;a href=&quot;https://movie.douban.com/subject/35689244/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;F1: 狂飙飞车&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;年度最佳视频： &lt;em&gt;Casey Neistat: &lt;a href=&quot;https://www.youtube.com/watch?v=EnhXwiPC3rE&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;纽约午餐危机&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;年初用国补还买了小米电视 S55 MiniLED，但今年都基本没怎么剧和电影，可以说是最近十年看的最少的一年了。每周必看 B 站主要的几个 Up：小 Lin 说，Koala 聊开源，马督工，jyhachi。&lt;/p&gt;
&lt;p&gt;2026 年期待的剧集：&lt;em&gt;豺狼的日子 第二季&lt;/em&gt;，&lt;em&gt;投行风云 第四季&lt;/em&gt;。&lt;/p&gt;
&lt;h3 id=&quot;things&quot;&gt;&lt;a href=&quot;#things&quot;&gt;Things&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;年度最佳 IDE：Zed&lt;/li&gt;
&lt;li&gt;年度最烂开发工具：Postman&lt;/li&gt;
&lt;li&gt;年度最烂日本品牌：松下&lt;/li&gt;
&lt;li&gt;年度最佳日本品牌：三菱电机&lt;/li&gt;
&lt;li&gt;年度最吃灰设备：PlayStation 5&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用 Zed Vibe Coding 了&lt;a href=&quot;https://lawrenceli.me/things&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;一个页面&lt;/a&gt;，罗列了最近几年买的主要电子设备。AI 就是用来写这种东西快准狠。&lt;/p&gt;
&lt;p&gt;我的 IDE 除了 JetBrains 之外现在用的就只有 Zed 了。Atom 项目原班人马的新作品，非 Java 项目可以全用 Zed，原生支持 Vim 操作。就图一个简单清爽内置 AI。 &lt;a href=&quot;https://linux-china.davao.page/blog/2025-11-13-qoder-jb/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Qoder JetBrains Plugin&lt;/a&gt; 插件做的蛮好的，我也在用，但 JetBrains 自家的 AI Assistant 是真的拉垮，BYOK 完全不可用。&lt;/p&gt;
&lt;p&gt;Postman 最烂的地方在于它作为一个 HTTP 客户端搞出一堆依赖自家网络的用户功能，第一天用它的时候就很讨厌，工作原因还不得不去用它。&lt;/p&gt;
&lt;p&gt;老婆给我买了一款松下剃须刀，能用，但声音巨大，操作交互非常不友好，对不起它大几百的价格，找官方解决结果还要返厂检修。我可怕麻烦了，讨厌一个品牌就是这么简单粗暴。&lt;/p&gt;
&lt;p&gt;今年年初我老家的卧室国产空调用十多年后罢工了，老爸趁着有国补换空调，我推荐了三菱电机。我和老婆回家开了几天感觉很非常好，安静且高效。国庆回家后发现我爸给他自己卧室也换上了同款空调。&lt;/p&gt;
&lt;p&gt;PS 5 又吃灰了一整年，我还不考虑出售。总想着放假开起来玩通宵，但真到假期了，还是懒得开。&lt;/p&gt;
&lt;h3 id=&quot;investment&quot;&gt;&lt;a href=&quot;#investment&quot;&gt;Investment&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;花了一个月时间通过了基金从业资格考试，除了琐碎的法规之外，也掌握了不少金融衍生品和金融工具（期权/期货/互换/逆回购/REITs/可转债/ABS...），尽管先前在投行的工作或多或少接触过，但要达到应付行业考试的程度还是得系统学习。这次学习机会重塑了我对投资的认知，几个重要的心得也在这里分享：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;投机交易者为市场提供流动性&lt;/li&gt;
&lt;li&gt;股票的价格本质是未来现金流的折现&lt;/li&gt;
&lt;li&gt;现代投资组合理论本质讨论的不是收益最大化，而是如何在不确定性中构建一个更抗波动的投资组合&lt;/li&gt;
&lt;li&gt;市场只对承担系统性风险的投资者给予补偿&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在学习之前，我总是在想市场的数学模型是什么，原来这正是马科维茨的现代投资组合理论。它用数学方法证明了不要把鸡蛋放在一个篮子里。它提醒我，真正成熟的决策并不是减少犯错，而是让系统在犯错时依旧可行。这种思路，在投资之外同样成立。&lt;/p&gt;
&lt;p&gt;看了两场&lt;a href=&quot;https://xueqiu.com/vod/5145403704642636311/361010234&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;段永平访谈&lt;/a&gt;，总结起来还是那句话：&lt;strong&gt;买股票就是买公司&lt;/strong&gt;。也经常睡前看猫笔刀（之前公司同事推荐的公众号），前几天还参与了一波场内白银 LOF 套利，虽然入场晚了点，但投机者慈善家是真金白银提供流动性。&lt;/p&gt;
&lt;p&gt;摘录猫笔刀的一段话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有人问我期指交割日是不是都会砸盘，我说不会，你只要去学习一下期指是什么样的金融产品，期指的运行方式，期指的交割规则，就知道它对 A 股没影响。但问题是这市场里 80% 以上的人不会学也不想学，他们更愿意接受不用动脑子的暴论，这样的人多到一定程度，市场就会自我实现。共识是有价值的，哪怕是错误的共识。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;end&quot;&gt;&lt;a href=&quot;#end&quot;&gt;End&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;今年一直骑行上下班，每天骑五六十分钟，年底还被交警罚了一单。最近还感冒了一回，稍微吃点热食浑身就出汗了，没吃一粒药完全靠自愈。住的地方商圈发现了一家淮扬菜店，点了很多次他家外卖，还到店吃了几次。&lt;/p&gt;
&lt;p&gt;多抓鱼上买了一些书，还淘到了一本绝版刊物《独唱团》。《南方周末》今年的&lt;a href=&quot;https://www.infzm.com/contents/309868&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;新年献词&lt;/a&gt;写的像学生作文，也不知道现在报刊亭还有的卖么？或者，现在还有报刊亭吗？&lt;/p&gt;
&lt;p&gt;我今年每一天都会刷 RSS，未来或者余生，我仍然会以它作为信息获取的主要途径；看到一些网站没有提供 RSS 源，我会给网站作者发邮件请求提供，再不济，我会基于 RSSHub 之类的工具自己生成对应的 Feed。&lt;/p&gt;
&lt;p&gt;AI 应用爆发的年代，前文我提到人类获取信息的时间复杂度几乎达到了 O(1)，我相信很多博主在博客上分享的也都不会再是纯知识类文章了，因为除了自我加深印象之外，没有意义。我也不会再写这样的内容，因为我的博客也不是维基。未来可能会分享自己对一些事物的理解，或者，单纯的流水账式吐槽。&lt;/p&gt;
&lt;p&gt;大概就这些吧。&lt;/p&gt;</summary><author><name>Lawrence</name></author></entry><entry><title>在 Flyway 迁移类中实现依赖注入</title><id>https://lawrenceli.me/blog/flyway</id><link href="https://lawrenceli.me/blog/flyway"/><published>2024-04-04T00:00:00.000Z</published><updated>2024-04-04T00:00:00.000Z</updated><category term="flyway" label="flyway" scheme="https://lawrenceli.me/tag/flyway"/><category term="java" label="java" scheme="https://lawrenceli.me/tag/java"/><category term="database" label="database" scheme="https://lawrenceli.me/tag/database"/><category term="migration" label="migration" scheme="https://lawrenceli.me/tag/migration"/><category term="springboot" label="springboot" scheme="https://lawrenceli.me/tag/springboot"/><category term="programming" label="programming" scheme="https://lawrenceli.me/tag/programming"/><summary type="html">&lt;h2 id=&quot;flyway-是什么&quot;&gt;&lt;a href=&quot;#flyway-是什么&quot;&gt;Flyway 是什么&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://flywaydb.org/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Flyway&lt;/a&gt; 是一款开源的，基于 Java 实现的数据库内容变更控制工具。它提供了 CLI、Java API、Maven/Gradle Plugin 等多种方式方便开发人员将数据库的表结构改动、内容变动以可追溯的代码形式进行管理和部署。Flyway 支持包括大多数主流的关系型数据库：MySQL、SQL Server、Oracle Database、PostgreSQL、SQLite、TiDB、MariaDB 等，对于 MongoDB 的支持尚在预览阶段（更推荐 Mongock），同类竞品有 Liquibase，但 Liquibase 的使用相比 Flyway 更复杂，有额外的概念作为学习成本。&lt;/p&gt;
&lt;div&gt;
  &lt;github user=&quot;flyway&quot; repo=&quot;flyway&quot;&gt;&lt;/github&gt;
&lt;/div&gt;
&lt;h2 id=&quot;配合-spring-boot-使用&quot;&gt;&lt;a href=&quot;#配合-spring-boot-使用&quot;&gt;配合 Spring Boot 使用&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;遵循 SpringBoot 广为人知的约定大于配置，Spring 官方提供了 &lt;a href=&quot;https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/flyway&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Flyway 的自动配置实现&lt;/a&gt;。&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;AutoConfiguration&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt;after&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;DataSourceAutoConfiguration&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;JdbcTemplateAutoConfiguration&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;HibernateJpaAutoConfiguration&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; })&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;ConditionalOnClass&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;Flyway&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;Conditional&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;FlywayDataSourceCondition&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;ConditionalOnProperty&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt;prefix&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; &quot;spring.flyway&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; &quot;enabled&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt;matchIfMissing&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; true&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;Import&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;DatabaseInitializationDependencyConfigurer&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;ImportRuntimeHints&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;FlywayAutoConfigurationRuntimeHints&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;public&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#FFA657&quot;&gt; FlywayAutoConfiguration&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;/*...*/&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全限定类名 &lt;code&gt;org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration&lt;/code&gt; 会在检测到以下几种状态下实现自动配置：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;存在 &lt;code&gt;Flyway&lt;/code&gt; 类 (即 &lt;code&gt;org.flywaydb.core.Flyway&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;满足 &lt;code&gt;FlywayDataSourceCondition&lt;/code&gt; 类中的 Bean 或 Properties 条件，其实就是 &lt;code&gt;DataSource&lt;/code&gt; 和数据库 URL 等连接配置存在&lt;/li&gt;
&lt;li&gt;存在 &lt;code&gt;spring.flyway.enabled=true&lt;/code&gt; 属性配置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于 MySQL 而言，除了必须的 JDBC 依赖之外，需要引入 Flyway 自身依赖，SpringBoot 已经在 pom 中声明过版本号，因此此处无需额外定义版本字段:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;dependency&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;groupId&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;org.flywaydb&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;groupId&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;artifactId&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;flyway-core&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;artifactId&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;dependency&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;dependency&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;groupId&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;org.flywaydb&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;groupId&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;artifactId&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;flyway-mysql&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;artifactId&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;dependency&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体使用有两种方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;原生 SQL&lt;/li&gt;
&lt;li&gt;Java API&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Flyway 会基于默认配置的文件夹路径 classpath:db/migration 发现版本变更文件或实现类。&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;V&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;{VERSION}&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;__&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;{DESCRIPTION}&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;.sql&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;V&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;{VERSION}&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;__&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;{DESCRIPTION}&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;.java&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;VERSION&lt;/code&gt; 可使用包含小数点、下划线的字符串版本，&lt;code&gt;DESCRIPTION&lt;/code&gt; 则是简单的描述文本。中间的分割符是两个下划线。可通过 spring.flyway 配置自定义前后缀。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;V{VERSION}__{DESCRIPTION}.sql&lt;/code&gt; 文件需要存放在 src/main/resources/db/migration 文件夹下; Java 类则需要定义包名 &lt;code&gt;db.migration&lt;/code&gt; 并且继承父类 &lt;code&gt;BaseJavaMigration&lt;/code&gt;，重写如下方法:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;void&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; migrate&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; context) throws Exception;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照官方的写法，我们就可以在 &lt;code&gt;migrate()&lt;/code&gt; 方法中去用 Java 代码实现数据库内容版本变动了，但是这里有一个缺点，那就是我们在 Spring 环境下无法针对 &lt;code&gt;V{VERSION}__{DESCRIPTION}.java&lt;/code&gt; 进行依赖注入，只能尝试通过原生 JDBC 的方式、静态方法等去写比较朴素的 SQL 实现，而不能充分成分利用现有的 DAO 接口来做业务数据的更新。因此需要自定义配置 Flyway。&lt;/p&gt;
&lt;h2 id=&quot;向-flyway-migration-实现类进行依赖注入&quot;&gt;&lt;a href=&quot;#向-flyway-migration-实现类进行依赖注入&quot;&gt;向 Flyway Migration 实现类进行依赖注入&lt;/a&gt;&lt;/h2&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;package&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; me.lawrenceli.migration.config;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; jakarta.annotation.PostConstruct&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; org.flywaydb.core.Flyway&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; org.flywaydb.core.api.migration.JavaMigration&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; org.flywaydb.core.api.output.MigrateResult&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; org.springframework.beans.factory.annotation.Autowired&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; org.springframework.context.ApplicationContext&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; org.springframework.context.annotation.Configuration&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; javax.sql.DataSource&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;Configuration&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;public&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#FFA657&quot;&gt; FlywayConfig&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;    // 自定义迁移历史表名&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;    private&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; static&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; final&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; String&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; SCHEMA_HISTORY_TABLE&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; &quot;schema_changes&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    @&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;Autowired&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;    private&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; DataSource&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; dataSource&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    @&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;Autowired&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;    private&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; ApplicationContext&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; applicationContext&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    @&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;PostConstruct&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; migrate&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;        log&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;info&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;Flyway, 启动!&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;        // 通过 Spring 容器获取所有迁移实现类&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;        // 这样一来，所有实现类就不再需要定义在 package `db.migration` 下，可以放在任何支持 Bean 扫描的位置。&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#FF7B72&quot;&gt;        JavaMigration&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#FFA657&quot;&gt;[] &lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;migrationBeans&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; applicationContext&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;                .&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;getBeansOfType&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;JavaMigration&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;                .&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;values&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;                .&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;toArray&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;new&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#FF7B72&quot;&gt; JavaMigration&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt;        Flyway&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; flyway&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; Flyway&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;configure&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;                .&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;dataSource&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(dataSource) &lt;/span&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;// 通过原本的 DataSource Bean 实现无需配置 flyway 自身的 JDBC URL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;                .&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;locations&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;db/migration&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;// 默认迁移脚本路径&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;                .&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;table&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(SCHEMA_HISTORY_TABLE) &lt;/span&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;// 默认迁移历史表为 `flyway_schema_history`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;                .&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;baselineOnMigrate&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;// 默认 false, 对以存在的数据库做首次迁移必须设置开启&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;                .&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;baselineVersion&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;0&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;// 默认 &quot;1&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;                .&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;executeInTransaction&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt;true&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;// 将迁移作为事务，你懂的&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;                .&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;installedBy&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;applicationContext&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;getId&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;()) &lt;/span&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;// 将微服务名作为迁移执行者&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;                .&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;javaMigrations&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(migrationBeans) &lt;/span&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;// 注册迁移类&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;                .&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;load&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt;        MigrateResult&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; migrate&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; flyway&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;migrate&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(); &lt;/span&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;// 执行迁移，依次调用子类实现&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;        log&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;info&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;Flyway 迁移了 {} 版. {}&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;migrate&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;migrationsExecuted&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;migrate&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;success&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 Flyway 的配置基于这种手动配置，因此需要在 SpringBoot 启动类上排除原有的自动配置类，以防止自动配置存在加载冲突。&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;SpringBootApplication&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt;exclude&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; FlywayAutoConfiguration&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;public&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#FFA657&quot;&gt; MyApplication&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; static&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; main&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#FF7B72&quot;&gt;String&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;[] &lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FFA657&quot;&gt;args&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;        SpringApplication&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;run&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;MyApplication&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, args);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，定义一个 Component Bean 去实现 &lt;code&gt;BaseJavaMigration&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;package&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; me.lawrenceli.balabala.migration;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; org.flywaydb.core.api.migration.BaseJavaMigration&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; org.flywaydb.core.api.migration.Context&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; org.springframework.beans.factory.annotation.Autowired&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; org.springframework.stereotype.Component&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;Component&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;public&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; class&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#FFA657&quot;&gt; V2__QueryExample&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; extends&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#79C0FF&quot;&gt; BaseJavaMigration&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    @&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;Autowired&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;    private&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; MyMapper&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; myMapper&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;// Bean of DAO&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    @&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;Override&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;    public&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; migrate&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt;Context&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FFA657&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;throws&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt; Exception&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#E6EDF3&quot;&gt;        Data&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; data&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; myMapper&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;selectById&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt;2024L&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;        // ... other CRUD codes with Java&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，所有的迁移类都可以方便地使用依赖注入来愉快地做 CRUD 了。经过实践，Flyway 会在数据库连接配置后、HTTP 服务暴露(也就是 Servlet 容器监听端口)前同步地执行完所有迁移，因此无需担心执行时机影响线上服务。&lt;/p&gt;
&lt;h2 id=&quot;参考&quot;&gt;&lt;a href=&quot;#参考&quot;&gt;参考&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/flyway/flyway/issues/1062&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;https://github.com/flyway/flyway/issues/1062&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</summary><author><name>Lawrence</name></author></entry><entry><title>我的默认应用</title><id>https://lawrenceli.me/blog/app-defaults-2023</id><link href="https://lawrenceli.me/blog/app-defaults-2023"/><published>2023-12-03T00:00:00.000Z</published><updated>2024-06-23T00:00:00.000Z</updated><category term="app" label="app" scheme="https://lawrenceli.me/tag/app"/><category term="life" label="life" scheme="https://lawrenceli.me/tag/life"/><category term="2024" label="2024" scheme="https://lawrenceli.me/tag/2024"/><summary type="html">&lt;p&gt;最近看到很多博主在 &lt;a href=&quot;https://defaults.rknight.me/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;App Defaults&lt;/a&gt; 中分享了他们的默认应用程序。以下是我自己的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;📨 邮件客户端
&lt;ul&gt;
&lt;li&gt;Apple 邮件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;📮 邮件服务器
&lt;ul&gt;
&lt;li&gt;Outlook（中国大陆可用）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.larksuite.com/mail&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Lark 邮件&lt;/a&gt; &lt;code&gt;hi@lawrenceli.me&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;📝 注释
&lt;ul&gt;
&lt;li&gt;Apple Notes（使用 &lt;a href=&quot;https://montaigne.io&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;montaigne.io&lt;/a&gt;，可以将 Apple Notes 成为一个静态网站）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;✅ 待办事项
&lt;ul&gt;
&lt;li&gt;Apple 提醒&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;📷 iPhone 照片拍摄
&lt;ul&gt;
&lt;li&gt;Apple 相机&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🟦 照片管理
&lt;ul&gt;
&lt;li&gt;Apple 照片与 iCloud 同步&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;📆 日历
&lt;ul&gt;
&lt;li&gt;服务：iCloud 日历&lt;/li&gt;
&lt;li&gt;客户端：Apple 日历&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;📁 云文件存储
&lt;ul&gt;
&lt;li&gt;iCloud 云盘 (iCloud+)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.aliyundrive.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;阿里云盘&lt;/a&gt;（适用于 Apple TV）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;📖 RSS
&lt;ul&gt;
&lt;li&gt;iOS 上的 &lt;a href=&quot;https://reederapp.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Reeder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;macOS 上的 &lt;a href=&quot;https://readkit.app/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;ReadKit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;自托管 &lt;a href=&quot;https://freshrss.org/index.html&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;FreshRSS&lt;/a&gt;，出色的 PWA 和 Web 推送。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🙍🏻‍♂️ 通讯录
&lt;ul&gt;
&lt;li&gt;N/A，没有联系人。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🌐 浏览器
&lt;ul&gt;
&lt;li&gt;Mac 上的 Firefox&lt;/li&gt;
&lt;li&gt;iOS 上的 Safari&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;💬 聊天
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.wechat.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;微信&lt;/a&gt;（中国必备应用）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apps.apple.com/ph/app/microsoft-teams/id1113153706&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Microsoft Teams&lt;/a&gt;（上班用）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🔖 书签
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.instapaper.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Instapaper&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;📑 稍后阅读
&lt;ul&gt;
&lt;li&gt;Reeder + Instapaper&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;📜 文字处理
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.onlyoffice.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;ONLYOFFICE&lt;/a&gt;（开源 Office，与微软完全兼容）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://obsidian.md/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Obsidian&lt;/a&gt;（流行的 Markdown 编辑器，可通过 iCloud 同步）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;📈 电子表格
&lt;ul&gt;
&lt;li&gt;ONLYOFFICE（开源 Office）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;📊 演示
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sli.dev/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Slidev&lt;/a&gt;（通过 Markdown 生成幻灯片）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🛒 购物清单
&lt;ul&gt;
&lt;li&gt;在 Apple 提醒共享提醒列表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🍴 膳食计划
&lt;ul&gt;
&lt;li&gt;无&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;💰 预算和个人理财
&lt;ul&gt;
&lt;li&gt;以前用过 &lt;a href=&quot;https://hochgatterer.me/finances/ios/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;iOS 的 Finances 2&lt;/a&gt;，目前正在尝试 &lt;a href=&quot;https://apps.apple.com/us/app/beanwise/id6446314789?ref=https://lawrenceli.me&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;BeanWise&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;📰 新闻
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://solidot.org&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Solidot&lt;/a&gt; (中文版的 &lt;a href=&quot;https://slashdot.org&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;slashdot&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.reuters.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;路透社&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://news.ycombinator.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Hacker News&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🎵 音乐
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://music.apple.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Apple Music&lt;/a&gt;（学生订阅只需每月 6 元）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://music.163.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;网易云音乐&lt;/a&gt;（作为 Apple Music 的补充）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🎤 播客
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.apple.com/apple-podcasts/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Apple Podcast&lt;/a&gt; 推荐泛用型播客客户端，无内容审查&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🔐 密码管理
&lt;ul&gt;
&lt;li&gt;iCloud KeyChain&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://2fas.com&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;2FAS&lt;/a&gt; 多因素身份验证&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;额外默认设置&quot;&gt;&lt;a href=&quot;#额外默认设置&quot;&gt;额外默认设置&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;🚀 自托管
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://lawrenceli.me/blog/cloudflare&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Cloudflare&lt;/a&gt;（用于 CDN 和 DNS）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vercel.com&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Vercel&lt;/a&gt; Next.js 的最佳部署体验&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Cloudflare Workers&lt;/a&gt; 基于 Cloudflare 节点的 Serverless&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://fly.io&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;fly.io&lt;/a&gt; 可免费运行 Docker 实例，需提供信用卡&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🤖 自动化
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ifttt.com&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;IFTTT&lt;/a&gt; IF This Then That&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://workflow.is/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Apple 快捷指令&lt;/a&gt;（前 Workflow）&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.activepieces.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;ActivePieces&lt;/a&gt; 开源的自动化工具&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;🛜 网络工具
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tailscale.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Tailscale&lt;/a&gt; 零配置组网工具&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://1.1.1.1&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Cloudflare WARP&lt;/a&gt; 基于 DoH 的零信任网络&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mcreadme.gitbook.io/mc/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;MerlinClash&lt;/a&gt; 梅林固件的网络工具&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/WireGuard&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;WireGuard&lt;/a&gt; 下一代 VPN 协议&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://apps.apple.com/us/app/shadowrocket/id932747118?l=zh-Hans-CN&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;ShadowRocket&lt;/a&gt; 最流行的网络工具&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;</summary><author><name>Lawrence</name></author></entry><entry><title>Cloudflare</title><id>https://lawrenceli.me/blog/cloudflare</id><link href="https://lawrenceli.me/blog/cloudflare"/><published>2023-06-11T00:00:00.000Z</published><updated>2023-11-29T00:00:00.000Z</updated><category term="cloudflare" label="cloudflare" scheme="https://lawrenceli.me/tag/cloudflare"/><category term="network" label="network" scheme="https://lawrenceli.me/tag/network"/><category term="cdn" label="cdn" scheme="https://lawrenceli.me/tag/cdn"/><category term="company" label="company" scheme="https://lawrenceli.me/tag/company"/><category term="ddos" label="ddos" scheme="https://lawrenceli.me/tag/ddos"/><category term="tls" label="tls" scheme="https://lawrenceli.me/tag/tls"/><summary type="html">&lt;p&gt;&lt;trade symbol=&quot;NET&quot;&gt;&lt;/trade&gt;&lt;/p&gt;
&lt;p&gt;Cloudflare 是一家在业内比较知名的 CDN 服务商，提供包含 DNS 解析、WAF 防火墙、CDN 加速、DDoS 防护，后续推出了一系列比较方便开发人员的许多功能：Cloudflare Workers、KV、Zero Trust Tunnel、WARP... 一切都是为了提供一个安全、快速的互联网环境。如果说 Vercel 给前端开发人员提供了基础设施，那 Cloudflare 则为数千万网站后端流量提供了基础设施。&lt;/p&gt;
&lt;p&gt;今年年初，&lt;a href=&quot;https://blog.cloudflare.com/zh-cn/cloudflare-mitigates-record-breaking-71-million-request-per-second-ddos-attack-zh-cn/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Cloudflare 缓解了破纪录的 7100 万个请求/秒的 DDoS 攻击&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&quot;vercel--cdn&quot;&gt;&lt;a href=&quot;#vercel--cdn&quot;&gt;Vercel &amp;#x26; CDN&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;三年前，我把我的博客从 WordPress 迁移到了 &lt;a href=&quot;https://vercel.com&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Vercel&lt;/a&gt;，老用户们或许都记得，那时候的域名可还都是 &lt;code&gt;*.now.sh&lt;/code&gt;。初次使用 Vercel 的感受可以说是如获至宝，在今天看来可能显得很幼稚了——如果不是自己知道如何从 0 到 1 申请域名去部署一个全栈的 Web 项目的话，很难理解 Vercel 这种平台背后做了哪些复杂工作。这是我所理解的美国公司的一贯作法 —— 他们总是把庞大、精密、复杂的技术或基础设施掩盖在简约、优雅的产品外观之下。而我每次都会保持警惕和观察力，如果换我做，我怎么来实现？后来我便学习起了 Kubernetes。&lt;/p&gt;
&lt;p&gt;说回 Cloudflare。从我第一次买域名（2015 年）一直到现在，我全部把解析权安排在 Cloudflare 上。归因于 &lt;a href=&quot;https://isdown.app/integrations/vercel/incidents/50745-errors-accessing-from-china&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Vercel 的一次网络问题&lt;/a&gt;，国内的网络受某种不可抗力在 2021 年的时候突然无法访问 Vercel 的部分域名了，尽管我的博客每天仅有为数不多的访客，但作为一个以中文为主的博客，还是有必要保持国内网络访问的畅通。根据官方提供的新的 CNAME 值，我在 Cloudflare 上更换了解析记录，也算顺利解决。也就在当时，我才注意到 DNS Records 控制台之前一个一直忽视的选项：&lt;/p&gt;
&lt;figure&gt;
  &lt;img src=&quot;data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDQgMzkuNSI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiM5OTk7fS5jbHMtMntmaWxsOiNmNjhhMWQ7fS5jbHMtM3tmaWxsOiNmZmY7fTwvc3R5bGU+PC9kZWZzPjx0aXRsZT5Bc3NldCAxPC90aXRsZT48ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIj48ZyBpZD0iTGF5ZXJfMS0yIiBkYXRhLW5hbWU9IkxheWVyIDEiPjxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSIxMDQgMjAuMTIgOTQgMTAuNjIgOTQgMTYuMTIgMCAxNi4xMiAwIDI0LjEyIDk0IDI0LjEyIDk0IDI5LjYyIDEwNCAyMC4xMiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTc0LjUsMzljLTIuMDgsMC0xNS40My0uMTMtMjguMzQtLjI1LTEyLjYyLS4xMi0yNS42OC0uMjUtMjcuNjYtLjI1YTgsOCwwLDAsMS0xLTE1LjkzYzAtLjE5LDAtLjM4LDAtLjU3YTkuNDksOS40OSwwLDAsMSwxNC45LTcuODEsMTkuNDgsMTkuNDgsMCwwLDEsMzguMDUsNC42M0ExMC41LDEwLjUsMCwxLDEsNzQuNSwzOVoiLz48cGF0aCBjbGFzcz0iY2xzLTMiIGQ9Ik01MSwxQTE5LDE5LDAsMCwxLDcwLDE5LjU5LDEwLDEwLDAsMSwxLDc0LjUsMzguNWMtNC4xMSwwLTUyLS41LTU2LS41YTcuNSw3LjUsMCwwLDEtLjQ0LTE1QTguNDcsOC40NywwLDAsMSwxOCwyMmE5LDksMCwwLDEsMTQuNjgtN0ExOSwxOSwwLDAsMSw1MSwxbTAtMUEyMCwyMCwwLDAsMCwzMi4xMywxMy40MiwxMCwxMCwwLDAsMCwxNywyMnYuMTRBOC41LDguNSwwLDAsMCwxOC41LDM5YzIsMCwxNSwuMTMsMjcuNjYuMjUsMTIuOTEuMTIsMjYuMjYuMjUsMjguMzQuMjVhMTEsMTEsMCwxLDAtMy42MS0yMS4zOUEyMC4xLDIwLjEsMCwwLDAsNTEsMFoiLz48L2c+PC9nPjwvc3ZnPg==&quot; width=&quot;48px&quot; height=&quot;48px&quot;&gt;&lt;/img&gt;
  &lt;figcaption style=&quot;text-align: center&quot;&gt;Proxied&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;我好奇地把它启用了，即从灰色 &lt;code&gt;DNS only&lt;/code&gt; 换成了这个橙色的 &lt;code&gt;Proxied&lt;/code&gt;，那时我还没意识到，其实 Cloudflare 从那刻起已经完全接管了我的网站的全部流量并进行任播 (Anycast)；换句话说，我在现有的 Vercel CDN 之上，又套了一层 Cloudflare CDN。是的，这迎来了两个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Vercel 后台警告 CNAME 解析异常&lt;/li&gt;
&lt;li&gt;缓存时间问题&lt;/li&gt;
&lt;li&gt;客户端 IP 全部被识别 Cloudflare IP，所有发往 Vercel 的请求都会从 Cloudflare 数据中心发出&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不可能没人像我这样做吧？事实上，&lt;a href=&quot;https://vercel.com/guides/why-running-another-cdn-on-top-of-vercel-is-not-recommended&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Vercel 并不推荐在其基础上使用另一层 CDN&lt;/a&gt;。后续我也依次找寻到了解决方案：对于 CNAME 来说，Vercel 会定时访问网站跟路径下的 &lt;code&gt;.well-known&lt;/code&gt; 路径下的资源来识别包括 CNAME、HTTPS 证书这类配置验证网站控制权信息，因此我们可以直接在 Cloudflare 的 WAF 中，把这类路径作为白名单让 WAF 跳过其他安全规则直接放行。对于客户端 IP，可以参考 &lt;a href=&quot;https://developers.cloudflare.com/rules/transform/managed-transforms/reference/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Available Managed Transforms&lt;/a&gt;，将一些客户端原始信息置于请求头中。缓存时间方面，还是要熟悉 MDN 上的一些标准 HTTP 协商协议，细粒度地对不同资源设置不同的 TTL，尽可能发挥 Cloudflare CDN 和浏览器自身缓存的优势 - 一个博客而已，是不是有点大炮打蚊子了？&lt;/p&gt;
&lt;p&gt;Cloudflare 的整体防御从 L3 到 L7，遍布了所有能覆盖的防御范围。一个请求进入 Cloudflare 所代理的网站流量会经历顺序由上到下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;center&quot;&gt;Traffic Sequence in Cloudflare&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;DDoS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;URL Rewrites&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;Page Rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;Origin Rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;Cache Rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;Configuration Rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;Redirect Rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;IP Access Rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;Bots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;WAF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;Header Modification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;Access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;Workers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这些流量经过内部的层层筛选，以及我们自己定义的一些 Rule，最终反代到源站。因此，在决定使用任何 CDN 产品的时候，有必要将服务端源站 IP 妥善隐藏，尽可能不暴露任何历史解析值，否则一切防御都是徒劳。如果源站 IP 已经暴露，只能及时更换新的地址。在新的规则录入好后，Cloudflare 的全球网络会立刻应用规则并实时生效，这里或多或少要归功于大佬 &lt;a href=&quot;https://mp.weixin.qq.com/s/xfphy67PTbtjeggo7LpjSA&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;agentzh 章亦春&lt;/a&gt; 开源的高性能网关 &lt;a href=&quot;https://openresty.org/cn/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;OpenResty&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&quot;cloudflare-workers--serverless&quot;&gt;&lt;a href=&quot;#cloudflare-workers--serverless&quot;&gt;Cloudflare Workers &amp;#x26; Serverless&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我们可以将一个个「函数」部署在公有云的「边缘计算节点」之上，并暴露 Socket 给这些节点上的函数，来实现无需忽略底层服务器，直接部署可随意伸缩的 HTTP 服务的能力。当然，这要求这些函数尽可能无状态。在没有任何请求，闲置一定时间时，这些函数进程会直接消失以腾出计算资源，直到下次事件驱动它们迅速重新启动并继续提供服务。这便是老生常谈的 Serverless。&lt;/p&gt;
&lt;p&gt;初次了解 Serverless 也是非常惊讶。AWS Lambda 竟能将 function 如此商业化 (FaaS)，Vercel 在此之上也做到了开箱即用。借助 Cloudflare 现有的数据中心，Cloudflare 也推出他们的 Serverless 解决方案 - &lt;a href=&quot;https://blog.cloudflare.com/introducing-cloudflare-workers/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Cloudflare Workers&lt;/a&gt;。不同的是，Cloudflare Workers 相比原始的 Vercel Serverless Function 而言能够做 Server Sent Event、WebSocket 这类支持长连接的请求。尽管后续 Vercel Edge Function 也能实现，但是它能支持的 Node.js Module 实在太少了。（作者注：后来我才了解到 Vercel Edge Function 其实构建于 Cloudflare Workers 之上）&lt;/p&gt;
&lt;p&gt;前不久，Cloudflare &lt;a href=&quot;https://blog.cloudflare.com/workerd-open-source-workers-runtime/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;开源了 Workers 运行时 workerd&lt;/a&gt;。&lt;/p&gt;
&lt;div&gt;
  &lt;github user=&quot;cloudflare&quot; repo=&quot;workerd&quot;&gt;&lt;/github&gt;
&lt;/div&gt;
&lt;p&gt;Cloudflare Workers 有许多应用场景。比如实现一个简单的&lt;a href=&quot;https://lucjan.medium.com/free-url-shortener-with-cloudflare-workers-125eaf87b1ec&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;短 URL 重定向服务&lt;/a&gt;、&lt;a href=&quot;https://github.com/hunshcn/gh-proxy&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;GitHub Proxy&lt;/a&gt;、以及一大堆各自实现的 ChatGPT API Proxy...方便了太多国内用户。&lt;/p&gt;
&lt;p&gt;Node.js 作者 Ryan Dahl 这几年给 JavaScript 写的另一个全新运行时 &lt;a href=&quot;https://dash.deno.com&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Deno 也有类似的 Serverless 服务&lt;/a&gt;，体验也很友好，同样&lt;a href=&quot;https://twitter.com/la3rence/status/1642798082294251520&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;支持 Web Standard API&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;为了实现 Serverless 的更多数据持久化功能，他们也各自推出了自家的 KV 存储实现服务，或者说是 Serverless 数据库。&lt;/p&gt;
&lt;h2 id=&quot;cloudflare-在-tls-协议上的努力&quot;&gt;&lt;a href=&quot;#cloudflare-在-tls-协议上的努力&quot;&gt;Cloudflare 在 TLS 协议上的努力&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;client-hello---sni&quot;&gt;&lt;a href=&quot;#client-hello---sni&quot;&gt;Client Hello - SNI&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;再来谈谈技术方面的一些进展。很多读者都知道 Server Name Indication（服务器名称指示，SNI）的存在，它是 TLS/SSL 协议在最初的 Client Hello 阶段由客户端发往服务端的一个字段，内容是网站的主机名或域名。引用 Cloudflare 的形象解释：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;SNI 有点像邮寄包裹到公寓楼而不是独栋房子。将邮件邮寄到某人的独栋房子时，仅街道地址就足以将包裹发送给收件人。但是，当包裹进入公寓楼时，除了街道地址外，还需要公寓号码。否则，包裹可能无法送达收件人或根本无法交付。许多 Web 服务器更像是公寓大楼而不是独栋房子：它们承载多个域名，因此仅 IP 地址不足以指示用户尝试访问哪个域名.....当多个网站托管在一台服务器上并共享一个 IP 地址，并且每个网站都有自己的 SSL 证书，在客户端设备尝试安全地连接到其中一个网站时，服务器可能不知道显示哪个 SSL 证书。这是因为 SSL/TLS 握手发生在客户端设备通过 HTTP 指示连接到某个网站之前。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有点类似于 HTTP 协议中的 &lt;code&gt;Host&lt;/code&gt; 请求头（如果在同一台服务器上用 Nginx 配置过多个虚拟主机应该都熟悉），但是 SNI 是作用在 L4，而且在 TCP 握手前完成。起初它并不是 TLS 协议的一部分，最早在 2003 年作为扩展字段增加到 TLS 协议中 (&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6066#section-3&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;RFC 6066&lt;/a&gt;)。现代浏览器等客户端都早已支持这个字段。我们会发现一个细节问题，对基于同一 CDN 的网站的 HTTPS 请求，我们传入的 TLS &lt;code&gt;SNI&lt;/code&gt; 和 HTTP Header &lt;code&gt;Host&lt;/code&gt; 会有不一致的情况，在不严格校验 SNI 的情况下，这类请求有可能被路由到 &lt;code&gt;Host&lt;/code&gt; 所定义的主机上，本质也就无视了 &lt;code&gt;SNI&lt;/code&gt;，因此对于某些防火墙来说，由于它们能通过 SNI 来侦测到用户所请求的 HTTPS 站点，它们无法得到后续 TLS 握手后的 HTTP 报文内容，在客户端更换了 Header &lt;code&gt;Host&lt;/code&gt; 后，实际返回的 HTTP 报文内容其实已被调包 —— 这种攻击方式，或者说叫伪装方式被称为&lt;a href=&quot;https://zh.wikipedia.org/zh-cn/%E5%9F%9F%E5%89%8D%E7%BD%AE&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;域前置(Domain Fronting)&lt;/a&gt;技术。Cloudflare、CloudFront 都会校验二者的一致性返回 403，但依然有部分 CDN 对这一做法采取保留，比如 Fastly。&lt;/p&gt;
&lt;p&gt;我们可以用 WireShack 抓包获取到 &lt;code&gt;SNI&lt;/code&gt; 字段。应用这个过滤条件 &lt;code&gt;ssl.handshake.extensions_server_name&lt;/code&gt;，尝试抓包发送一次 TLS 请求&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;openssl&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; s_client&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; -connect&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; lawrenceli.me:443&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; -servername&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; lawrenceli.me&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; -state&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; -debug&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; /dev/null&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;figure&gt;&lt;img src=&quot;/images/cloudflare/sni-field.png&quot; alt=&quot;SNI&quot;&gt;&lt;figcaption&gt;SNI&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;可以从结果看出，SNI 确实使用了明文进行传输，这就导致了前文提到的一个问题 - 就算经过 TLS/HTTPS 加密的流量，仍然明文地暴露了我们在访问的域名。「这又如何？DNS 不也暴露了嘛？」好问题 - DoH 解决了 DNS 请求的明文风险 (&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8484&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;RFC 8484&lt;/a&gt;)。因此，实际上 TLS 目前唯一在数据上有泄密风险的就只有这个字段了。Cloudflare 先后搬出了两个解决方案：&lt;a href=&quot;https://www.cloudflare-cn.com/learning/ssl/what-is-encrypted-sni/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;ESNI&lt;/a&gt; 以及 &lt;a href=&quot;https://blog.cloudflare.com/encrypted-client-hello/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;ECH&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;我们可以使用 Chrome 的开关 &lt;code&gt;chrome://flags/#encrypted-client-hello&lt;/code&gt; 来开启浏览器 ECH 的客户端支持。通过 Chrome DevTool 的 Security Tab 能够查看 HTTPS 流量的安全性信息。我们可以通过&lt;a href=&quot;https://crypto.cloudflare.com/cdn-cgi/trace&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;这个链接&lt;/a&gt;来测试客户端对这个方案的支持情况，当然，这些需要服务端做相应的配置才能完全启用。话题就此结束，我不能再细说了。&lt;/p&gt;
&lt;p&gt;Updated：&lt;a href=&quot;https://blog.cloudflare.com/announcing-encrypted-client-hello/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;2023 年 9 月底，Cloudflare 宣布向所有基于 TLS 1.3 的代理站点启用 ECH&lt;/a&gt;，目前默认全部启用且改选项不可关闭。&lt;/p&gt;
&lt;h3 id=&quot;client-hello---ja3&quot;&gt;&lt;a href=&quot;#client-hello---ja3&quot;&gt;Client Hello - JA3&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;利用 Client Hello 来做安全保护的另一个实践是 TLS 客户端指纹: JA3 &amp;#x26; JA3S。这一设计灵感来源于信息安全专家 Lee Brotherston 的研究 &lt;a href=&quot;https://blog.squarelemon.com/tls-fingerprinting/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;TLS fingerprinting&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;具体的工程实践可以参考 &lt;a href=&quot;https://github.com/salesforce/ja3&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Salesforce 开源的 JA3&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;
  &lt;github user=&quot;salesforce&quot; repo=&quot;ja3&quot;&gt;&lt;/github&gt;
&lt;/div&gt;
&lt;p&gt;简而言之，TLS 握手过程中客户端发送的字节数组，也就是 Client Hello 阶段的一些字段和扩展名，通过固定方式拼接，基于摘要 MD5 来生成一个唯一的字符串，称为 JA3 指纹。不同的浏览器或 TLS 客户端有不同指纹。在大量的数据采样中，Cloudflare 就能够基于此数据 (JA3 &amp;#x26; JA3S，后者包含了 Server Hello 阶段的服务端指纹) 统计出哪些请求来自于僵尸网络、机器人爬虫、Python 库还是正常用户的浏览器、或者手机访问。这也就解释了很多同学写爬虫时，利用 HTTP 协议更换 &lt;code&gt;User-Agent&lt;/code&gt; 这一请求头无效的情况，因为 Cloudflare 的防御处在更底层的 L4 TLS 阶段。ChatGPT 的 Web 端也部署了 &lt;a href=&quot;https://developers.cloudflare.com/bots/concepts/ja3-fingerprint/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Cloudflare 的 TLS JA3 指纹鉴定 WAF (仅限 Enterprise 账户)&lt;/a&gt;；GitHub 上我也找到了相关的代码实现通过更换 TLS Client 的方式来绕过这一防御。对于多数人来说，这已经有很大的防爬门槛了；而且 Cloudflare 可以随时更换 WAF 策略让旧的指纹失效。&lt;/p&gt;
&lt;p&gt;JA3 由来自 salesforce 的三位工程师共同实现：John Althouse, Jeff Atkinson &amp;#x26; Josh Atkins。看到这里，想必你也知道为什么 JA3 叫 &lt;code&gt;JA3&lt;/code&gt; 了。&lt;/p&gt;
&lt;h2 id=&quot;盈利模式&quot;&gt;&lt;a href=&quot;#盈利模式&quot;&gt;盈利模式&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;和 Vercel，Netlify 如出一辙，Cloudflare 采用「免费试用，付费增值」的商业模式。Cloudflare CEO Matthew Prince 曾在 StackOverflow 上回答过这个问题：「&lt;a href=&quot;https://webmasters.stackexchange.com/questions/88659/how-can-cloudflare-offer-a-free-cdn-with-unlimited-bandwidth&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;How can Cloudflare offer a free CDN with unlimited bandwidth?&lt;/a&gt;」：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;更多的免费用户意味着更多的数据，这些数据能更好地帮助保护付费用户&lt;/li&gt;
&lt;li&gt;很多大客户的来源正是由于这些公司的员工是 Cloudflare 的免费用户，他们在工作中向公司推荐了 Cloudflare&lt;/li&gt;
&lt;li&gt;免费这一举措就是在做宣传，可以减少招聘成本，能雇到全球最厉害的工程师&lt;/li&gt;
&lt;li&gt;免费用户体验新功能的同时也能就帮助了这个新功能的测试，缩短了迭代周期&lt;/li&gt;
&lt;li&gt;带宽成本的鸡生蛋、蛋生鸡问题：用户数量庞大才能在面对全球各地的电信营运商时有议价权&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;2019 年，Cloudflare 在纽交所上市，股票代码：NET。发行价 US $15，目前 US $63，上涨了 320%。画外音：现在买它还来得及吗？&lt;/p&gt;
&lt;p&gt;在中国目前和京东云合作，仅限企业用户。500 强企业中目前有三分之一使用 Cloudflare，还有很多上升空间。OpenAI 的 &lt;a href=&quot;https://chat.openai.com&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;ChatGPT&lt;/a&gt; 上线后，Cloudflare 获得了大量曝光，防御了大量滥用用户和潜在威胁请求。&lt;/p&gt;
&lt;h2 id=&quot;价值观&quot;&gt;&lt;a href=&quot;#价值观&quot;&gt;价值观&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Cloudflare 因坚持网络中立原则受到了一些批评。&lt;/p&gt;
&lt;p&gt;比较典型的一件事是 Cloudflare 因舆论和法律的压力&lt;a href=&quot;https://blog.cloudflare.com/zh-cn/terminating-service-for-8chan-zh-cn&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;终止对 8chan 的服务&lt;/a&gt;。Cloudflare 声称自己是一家私营公司，并且 Cloudflare 半数营收来自于美国之外的地区，可以不受美国宪法第一修正案的约束，其服务的客户对象是整个互联网市场。由于业务量大，有些包含恐怖主义、仇恨言论的网站也免不了会使用其服务。这也是大多数大型互联网公司所面临的问题。和快播王欣事件类似，他们都不愿扮演内容仲裁者。互联网诞生至今，法律的步伐总是跟不上技术的发展。&lt;/p&gt;
&lt;h2 id=&quot;尾声&quot;&gt;&lt;a href=&quot;#尾声&quot;&gt;尾声&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;OpenAI 的 ChatGPT 对 Cloudflare 作了一次很好的展示，我向读者推荐 Cloudflare。一方面是因为它一直提供永久的个人免费服务，另一方面是它的易用性以及全球视野。我也用过国内某套路厂商的 WAF 产品，界面纷繁错乱，一看账单都不知道为什么收费，套路太深，价格高昂（可能怪我太穷）。&lt;/p&gt;</summary><author><name>Lawrence</name></author></entry><entry><title>Apple TV</title><id>https://lawrenceli.me/blog/apple-tv</id><link href="https://lawrenceli.me/blog/apple-tv"/><published>2023-01-21T00:00:00.000Z</published><updated>2023-01-21T00:00:00.000Z</updated><category term="apple" label="apple" scheme="https://lawrenceli.me/tag/apple"/><category term="tv" label="tv" scheme="https://lawrenceli.me/tag/tv"/><summary type="html">&lt;p&gt;过年回老家看电视，运营商送的网络电视盒子主屏幕花花绿绿，我一个程序员都费了好久才找到地方卫视的直播频道。&lt;/p&gt;
&lt;p&gt;索性去电商平台搜搜看有没有更好的硬件、翻看各种测评文章和视频。清晰了需求定位后，我果断找了一家有现货的店铺下单了一款美版 Apple TV 2022 (4K)。&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;/images/apple-tv/tv.jpg&quot; alt=&quot;Apple TV&quot;&gt;&lt;figcaption&gt;Apple TV&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;一直担心年前收不到货，没想到快递很敬业地发过来了。&lt;/p&gt;
&lt;p&gt;由于购买之前就已经熟悉了大部分使用细节，所以安装、使用的时候毫不费力；像是把玩过很久的玩具一样自然流畅。&lt;/p&gt;
&lt;p&gt;用美区 Apple ID 购买了很多付费应用，主要都是一些国内独立开发者的作品。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Alplayer&lt;/li&gt;
&lt;li&gt;APTV&lt;/li&gt;
&lt;li&gt;IIVA&lt;/li&gt;
&lt;li&gt;Miao Projects&lt;/li&gt;
&lt;li&gt;VidHub&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;很难想象不少开发者会为国内极其小众的平台开发上架了如此小而美的 tvOS App。&lt;/p&gt;
&lt;p&gt;搜集一些电视直播源，我就反常地看起了 CCTV。比期待的画质高出不少。遥控器的金属质感像第一次摸到棱角分明的 iPhone 5S 一样爱不释手！还用它和我爸玩了一局桌球游戏。&lt;/p&gt;
&lt;p&gt;种种体验让我想起&lt;a href=&quot;https://www.zhihu.com/question/477077785/answer/2425144012&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;知乎上一个回答&lt;/a&gt;：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;长这么大，听过最清晰的《义勇军进行曲》是在 Apple Music。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;trade symbol=&quot;AAPL&quot;&gt;&lt;/trade&gt;&lt;/p&gt;</summary><author><name>Lawrence</name></author></entry><entry><title>ActivityPub 协议的简单实现</title><id>https://lawrenceli.me/blog/activitypub</id><link href="https://lawrenceli.me/blog/activitypub"/><published>2023-01-11T00:00:00.000Z</published><updated>2023-01-11T00:00:00.000Z</updated><category term="blog" label="blog" scheme="https://lawrenceli.me/tag/blog"/><category term="activitypub" label="activitypub" scheme="https://lawrenceli.me/tag/activitypub"/><category term="fediverse" label="fediverse" scheme="https://lawrenceli.me/tag/fediverse"/><category term="guide" label="guide" scheme="https://lawrenceli.me/tag/guide"/><category term="ssg" label="ssg" scheme="https://lawrenceli.me/tag/ssg"/><category term="serverless" label="serverless" scheme="https://lawrenceli.me/tag/serverless"/><category term="openweb" label="openweb" scheme="https://lawrenceli.me/tag/openweb"/><summary type="html">&lt;div class=&quot;markdown-alert markdown-alert-warning&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-alert mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Warning&lt;/p&gt;&lt;p&gt;这不是一篇严肃的 ActivityPub 教程，仅仅是一些基于个人实现时的简单概括。该网站并不支持所有 ActivityPub 协议要求。由于联邦宇宙实例众多而本人服务器资源有限，笔者可能会关闭本站 ActivityPub 服务。&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://www.nytimes.com/2013/01/13/technology/aaron-swartz-internet-activist-dies-at-26.html&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Aaron Swartz 于十年前的这个时候自杀了&lt;/a&gt;。他起草的 &lt;a href=&quot;https://web.resource.org/rss/1.0/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;RSS (1.0) 协议&lt;/a&gt;、&lt;a href=&quot;https://daringfireball.net/projects/markdown/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;和 John Gruber 一起设计、创造的 Markdown&lt;/a&gt; 至今一直拥有大量互联网用户。这十年间互联网并没有因他的离世而产生 Open Web 原教旨主义者所期待的愿景。类似「剑桥分析公司」的事情你我都有耳闻。万维网的发明人 Tim Berners-Lee 博士后来提出了 &lt;a href=&quot;https://solidproject.org/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;SoLiD 项目&lt;/a&gt; —— 通过将用户数据和应用彻底分离，来实现用户对自身数据的完全掌控。ActivityPub 协议与之类似，但仅面向社交网站。如今，&lt;a href=&quot;https://www.w3.org/TR/activitypub/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;ActivityPub 已经成为了 W3C 的推荐标准&lt;/a&gt;；Elon Musk 收购 Twitter 公司之后，由于 &quot;Hardcore Software Engineering&quot; 所展露出的负外部性，Mastodon (长毛象)成为了最火热的分布式/去中心化社交网络平台，而 Mastodon 正是 ActivityPub 的实现之一。这个 &lt;a href=&quot;https://activitypub.rocks/implementation-report/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Implementation Report&lt;/a&gt; 页面展示了一些实现了 ActivityPub 协议的网站列表。&lt;/p&gt;
&lt;p&gt;折腾了几天，终于在百忙之中将这个小小的网站基本实现了 ActivityPub 最主要的接口。下面简单梳理一下大致实现的 Server to Server 接口，这些接口对于一个静态博客足矣。&lt;/p&gt;
&lt;p&gt;本站点实现 ActivityPub 的所有 REST API 均系由 ▲ Vercel Serverless Function (JavaScript) 驱动。&lt;/p&gt;
&lt;h2 id=&quot;webfinger&quot;&gt;&lt;a href=&quot;#webfinger&quot;&gt;WebFinger&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;此 API 的定义参考 &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc7033.html&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;RFC 7033&lt;/a&gt;。这个 WebFinger 协议目的是提供一种针对单个域名的&lt;strong&gt;用户发现&lt;/strong&gt;方式。考虑到此 API 必须使用 &lt;code&gt;Content-Type: application/jrd+json&lt;/code&gt; 作为 HTTP 的报文响应类型，因此不推荐直接使用静态文件托管 JSON，请使用 REST API 来构建此实现。&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;https://example.com/.well-known/webfinger?resource=acct:lawrence@example.com&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;subject&lt;/code&gt; 中的 URI 内容后半段和电子邮件非常像 —— ActivityPub 最终的实现效果也和电子邮件类似！&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;subject&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;acct:lawrence@lawrenceli.me&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;aliases&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: [],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;links&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;      &quot;rel&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;http://webfinger.net/rel/profile-page&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;      &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;text/html&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;      &quot;href&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/about&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;      &quot;rel&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;self&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;      &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;application/activity+json&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;      &quot;href&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/api/activitypub/actor&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;links&lt;/code&gt; 中会添加上我们即将要实现的 Actor API。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;除此 WebFinger 之外，以下所有 API 都必须设置 &lt;code&gt;Content-Type: application/activity+json&lt;/code&gt; 作为响应头。ActivityPub 服务端（比如一个 Mastodon 实例）都会在请求头使用 &lt;code&gt;Accept: application/activity+json&lt;/code&gt; 类似的形式来要求我们的实例返回对应的报文格式。&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;actor&quot;&gt;&lt;a href=&quot;#actor&quot;&gt;Actor&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Actor 就是 Activity 的参与者。WebFinger 会暴露此用户信息 (Profile) 接口。通过此 API，可以告知 ActivityPub 所有关于此用户的其他 API Endpoint，比如用户的 Outbox、Inbox、Followers 等等。所以这些 API 的具体 URL 都可以由自己去定义，而非一成不变。&lt;/p&gt;
&lt;p&gt;除此之外，需要提供用户的 PublicKey 来验明身份。我们只需要在自己本地生成一对密钥就可以了。服务端通信中，发往不同 ActivityPub 的实例 HTTPS 请求都需要经过密钥加密。&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;openssl&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; genrsa&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; -out&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; private.pem&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; 2048&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;openssl&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; rsa&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; -in&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; private.pem&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; -outform&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; PEM&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; -pubout&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; -out&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; public.pem&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;@context&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://www.w3.org/ns/activitystreams&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://w3id.org/security/v1&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/api/activitypub/actor&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;Person&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;name&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;Lawrence Li&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;preferredUsername&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;lawrence&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;summary&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;Blog&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;inbox&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/api/activitypub/inbox&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;outbox&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/api/activitypub/outbox&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;followers&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/api/activitypub/followers&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;icon&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/images/author/Lawrence.png&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;publicKey&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;    &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/api/activitypub/actor#main-key&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;    &quot;owner&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/api/activitypub/actor&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;    &quot;publicKeyPem&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;-----BEGIN PUBLIC KEY-----&lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#FF7B72&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0RHqCKo3Zl+ZmwsyJUFe&lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#FF7B72&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;iUBYdiWQe6C3W+d89DEzAEtigH8bI5lDWW0Q7rT60eppaSnoN3ykaWFFOrtUiVJT&lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#FF7B72&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;NqyMBz3aPbs6BpAE5lId9aPu6s9MFyZrK5QtuWfAGwv9VZPwUHrEJCFiY1G5IgK/&lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#FF7B72&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;+ZErSKYUTUYw2xSAZnLkalMFTRmLbmj8SlWp/5fryQd4jyRX/tBlsyFs/qvuwBtw&lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#FF7B72&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;uGSkWgTIMAYV71Wny9ns+Nwr4HYfF5eo2zInpwIYTCEbil79HcikUUTTO/vMMoqx&lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#FF7B72&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;46IiHcMj0SPlzDXxelZgqm0ojK2Z7BGudjvwSbWq/GtLoaXHeMUVpcOCtpyvtLr2&lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#FF7B72&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;YwIDAQAB&lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#FF7B72&quot;&gt;\n&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;-----END PUBLIC KEY-----&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;outbox&quot;&gt;&lt;a href=&quot;#outbox&quot;&gt;Outbox&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;类似 RSS/JSON Feed, 类型为 &lt;code&gt;OrderedCollection&lt;/code&gt;，必须按照时间顺序将最新内容放在 &lt;code&gt;orderedItems&lt;/code&gt; 的最前。&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;@context&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://www.w3.org/ns/activitystreams&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/api/activitypub/outbox&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;summary&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;Blog&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;OrderedCollection&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;totalItems&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;orderedItems&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OrderedItems 数组中的单个 Item (一般为 Note) 可以是如下形式：&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;@context&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://www.w3.org/ns/activitystreams&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/blog/ssg-ssr&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;Note&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;published&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;Thu, 20 Feb 2020 00:00:00 GMT&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;attributedTo&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/api/activitypub/actor&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;content&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;&amp;#x3C;a href=&lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#FF7B72&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;https://lawrenceli.me/blog/ssg-ssr&lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#FF7B72&quot;&gt;\&quot;&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&gt;When to Use Static Generation v.s. Server-side Rendering&amp;#x3C;/a&gt;&amp;#x3C;br&gt;SSG &amp;#x26; SSR&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;url&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/blog/ssg-ssr&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;to&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://www.w3.org/ns/activitystreams#Public&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;cc&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/api/activitypub/followers&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 ActivityPub 中，所有的对象都必须要提供一个 &lt;code&gt;id&lt;/code&gt; 来作为唯一的全局标识符。而且，这个 id 必须是公开可访问的 URI，即可以通过此 id 来访问到此资源对象本身。 例上述如 Outbox 中的一项 Note 可以通过如下 curl 请求得到：&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;curl&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; https://lawrenceli.me/blog/ssg-ssr&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; -H&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; &quot;Accept: application/activity+json&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而如果你用浏览器直接打开这个 URL，你将会看到的是一个网页。原因就在于 &lt;code&gt;Accept&lt;/code&gt; 这个请求头。&lt;/p&gt;
&lt;h2 id=&quot;inbox&quot;&gt;&lt;a href=&quot;#inbox&quot;&gt;Inbox&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;本质是一个必须支持 POST 请求的 WebHook。当联邦宇宙中其他用户对你的内容作出了一些交互(比如关注、回复、收藏、转发、删除等操作)，会触发此 WebHook。你需要根据 Activity 的类型去处理这些 Payload。一般来说，我们会使用自己的数据库来配合 Inbox Message 做 CRUD。&lt;/p&gt;
&lt;p&gt;数据存在自己的数据库之后，你就可以直接在自己的站点上去展示它们。要保持数据于联邦宇宙中的一致性，你需要处理好所有消息类型，并做到接口的幂等 —— 因为 Mastodon 实例会有重试机制。&lt;/p&gt;
&lt;h2 id=&quot;followers&quot;&gt;&lt;a href=&quot;#followers&quot;&gt;Followers&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;关注者列表 API。当 Inbox 接收到来自其他用户的关注请求时，可以获取用户账户后保存到数据库然后通过此 API 展示出来。类型为 &lt;code&gt;OrderedCollection&lt;/code&gt;。也是最简单的一个接口。&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;@context&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://www.w3.org/ns/activitystreams&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://lawrenceli.me/api/activitypub/followers&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;OrderedCollection&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;totalItems&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;  &quot;orderedItems&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;https://mstdn.social/users/lawrence&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;note--article&quot;&gt;&lt;a href=&quot;#note--article&quot;&gt;Note / Article&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;需要针对实现 Outbox 中的每一个 &lt;code&gt;orderedItem&lt;/code&gt; 的 &lt;code&gt;id&lt;/code&gt; 中的 URI 实现一个 JSON 输出。形式可以和 Outbox 中单个 Item 保持一致。
除了 &lt;code&gt;Note&lt;/code&gt; 之外，ActivityPub 可以有其他类型的资源，比如长文章的 &lt;code&gt;Article&lt;/code&gt;、视频资源 &lt;code&gt;Video&lt;/code&gt;。不同 ActivityPub 的实现平台对不同资源的展示方式不尽相同。&lt;/p&gt;
&lt;p&gt;我的博客页面地址和对应 Activity ID 的 URI 在 URL 形式上保持了一致。因此在实现此 API 后，用户可以在任何 Mastodon 实例的搜索栏中通过搜索我的博客文章页地址来发现它对应的 Mastodon 贴文(由 Outbox 生成)；在完全实现 Inbox 后，对贴文的交互数据就能够展示在我的网站上。比如文章页面最下方的 &lt;code&gt;Replies&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id=&quot;to-do&quot;&gt;&lt;a href=&quot;#to-do&quot;&gt;To Do&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;我的站点没有完全实现所有 ActivityPub 协议，比如 Inbox 消息目前仅处理了 Create Note 和 Accept Follower，还有许多消息类型亟待实现；大部分接受 GET 请求的接口也应当适当配置缓存；Inbox 要严格验证发送者的密钥。&lt;/p&gt;
&lt;h2 id=&quot;社区实现&quot;&gt;&lt;a href=&quot;#社区实现&quot;&gt;社区实现&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;很巧合地发现 Cloudflare 也在同一时间段开发了兼容 Mastodon 的 ActivityPub 实现：&lt;a href=&quot;https://github.com/cloudflare/wildebeest&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;WildeBeest&lt;/a&gt;，有兴趣可以直接用他们的商业化技术栈来部署一个小型实例，或者直接参考他们的代码，用自己擅长的服务端语言实现自己的 ActivityPub。&lt;/p&gt;
&lt;div&gt;
  &lt;github user=&quot;cloudflare&quot; repo=&quot;wildebeest&quot;&gt;&lt;/github&gt;
&lt;/div&gt;
&lt;h2 id=&quot;ref&quot;&gt;&lt;a href=&quot;#ref&quot;&gt;Ref&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.w3.org/TR/activitypub/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;https://www.w3.org/TR/activitypub/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://s3lph.me/activitypub-static-site.html&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;https://s3lph.me/activitypub-static-site.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://paul.kinlan.me/adding-activity-pub-to-your-static-site/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;https://paul.kinlan.me/adding-activity-pub-to-your-static-site/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</summary><author><name>Lawrence</name></author></entry><entry><title>写在 2022 年末</title><id>https://lawrenceli.me/blog/2022-in-review</id><link href="https://lawrenceli.me/blog/2022-in-review"/><published>2022-12-17T00:00:00.000Z</published><updated>2022-12-17T00:00:00.000Z</updated><category term="review" label="review" scheme="https://lawrenceli.me/tag/review"/><category term="2022" label="2022" scheme="https://lawrenceli.me/tag/2022"/><summary type="html">&lt;h2 id=&quot;tech&quot;&gt;&lt;a href=&quot;#tech&quot;&gt;Tech&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;技术方面今年依旧学习了很多新的内容。&lt;/p&gt;
&lt;p&gt;首先从 0 到 1 使用 Next.js 重构了此 lawrenceli.me 的整个站点。由原先基于 Notion 私有 REST API 的 hacking 方式换成基于纯 Markdown 文本的 SSG（静态页面生成）和 ISR （增量静态再生成）并完成了几乎没有太多压力的 &lt;a href=&quot;https://nextjs.org/docs/upgrading#upgrading-from-12-to-13&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Next.js 13 升级&lt;/a&gt;。所有文章内容均和代码共存，这样能在所有页面上自由发挥，甚至可以在纯 Markdown 文件中直接使用 React/JSX 组件（基于 HTML 抽象语法树 - AST）。依赖的版本也基于 GitHub 自动的 PR 保持了最新。 React/JSX 写的也比去年熟练多了。
另外简单研究了一下 TailWind CSS，上手很快，也在此站点上用上了。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/la3rence/node-express-example&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Node.js 模版项目&lt;/a&gt; 完工，一个快速使用 JavaScript 开发 REST API 的脚手架项目，各种测试、CI/CD、Code Coverage、日志、Open API (Swagger)、JWT 等基础设施都加上了。摒弃了古老的 CommonJS 而基于纯 ES Module。&lt;/p&gt;
&lt;div&gt;
  &lt;github user=&quot;la3rence&quot; repo=&quot;node-express-example&quot;&gt;&lt;/github&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/la3rence/websocket-cluster&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;WebSocket Cluster&lt;/a&gt; 项目在 GitHub 上也快到达 100 stars 了，每周都会看到一两个国人参考我那很久没维护但精妙绝伦的代码。&lt;/p&gt;
&lt;p&gt;年底 Spring 6 和 &lt;a href=&quot;https://spring.io/blog/2022/11/24/spring-boot-3-0-goes-ga&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Spring Boot 3 的 GA&lt;/a&gt; 同样令人欣喜。我们终于可以基于 GraalVM 的 AOT 去做 Spring Native on Cloud Native 了。JDK 8 仍有接近 7 年的寿命。JDK 17 的 ZGC 是最值得研究学习的，另外下个 JDK LTS (JDK 21?) 应该会让协程 (虚拟线程) GA，目前仍在孵化阶段。学 JavaScript / TypeScript 就是玩玩，真正企业级大型项目还得看 Java。为什么这么说，你去看看 &lt;a href=&quot;https://docs.nestjs.com/controllers&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Nest.js&lt;/a&gt; 就知道了。&lt;/p&gt;
&lt;p&gt;另外又用上了一个比较简单可靠的托管服务（关键是免费）：&lt;a href=&quot;https://fly.io/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;fly.io&lt;/a&gt;，除常规项目外，它可以部署 Docker 容器，并提供大概 3 GB 额度的免费磁盘挂载。大多静态页面和轻量 Serverless 都依赖 Vercel 或 Cloudflare Workers，一旦遇到需要更加复杂的场景（比如 WebSocket 或 SSE），我会选择使用 fly.io 来部署。&lt;/p&gt;
&lt;p&gt;Solidot Robot 已稳定运行将近一千天了，目前依旧基于 Vercel Serverless Function。异常稳定。&lt;a href=&quot;https://www.solidot.org/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Solidot&lt;/a&gt; 依旧是我每天都会逛的科技新闻源。&lt;/p&gt;
&lt;h2 id=&quot;work&quot;&gt;&lt;a href=&quot;#work&quot;&gt;Work&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;公司内的 OpenShift 今年并没有花太多时间研究，权限、开发环境问题无法在本地快速调试容器。当然我依旧对 Kubernetes 保持高昂的学习热情，并用半个工作日的时间完成公司对所有开发人员提供的 immersive training。&lt;/p&gt;
&lt;p&gt;我对所在团队 (CVA Trading Desk&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; in XVA) 的业务有了一定的认知：
CVA Trading Desk 其实是一家内部保险公司。负责保障 Business Line Trading 在 couterparty (交易对手) 有可能失信、违约的风险下，可以得到来自 CVA Trader 专门针对此 couterparty 的风险做 Hedge 交易而得到的利润补偿；当然 Business Line Trader 会向我们 CVA Desk 定期支付保险费用。&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;/images/2022-in-review/cva-desk.gif&quot; alt=&quot;CVA Desk&quot;&gt;&lt;figcaption&gt;CVA Desk&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;每天有数以百计的 Batch Job 去处理这些 Trade 数据。最累的还是作为一个开发去解决各种突如其来的 production issue。&lt;/p&gt;
&lt;h2 id=&quot;life&quot;&gt;&lt;a href=&quot;#life&quot;&gt;Life&lt;/a&gt;&lt;/h2&gt;
&lt;h3 id=&quot;wfh--covid&quot;&gt;&lt;a href=&quot;#wfh--covid&quot;&gt;WFH &amp;#x26; COVID&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;新冠瘟疫给我带来的除了处于长期封控下的 WFH，&lt;del&gt;没有额外影响&lt;/del&gt;。我可能是相对幸运的一批了 —— 从未因此损失什么，反而得出了类似「&lt;a href=&quot;/blog/makeshift-troupe/&quot;&gt;草台班子&lt;/a&gt;」的观点。&lt;/p&gt;
&lt;p&gt;经历上海的封控后，我现在已经会炒相对好吃一点的蛋炒饭了。还给前不久程序员做饭指南 How to Cook 出圈之前贡献了一份&lt;a href=&quot;https://github.com/Anduin2017/HowToCook/pull/159&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;可乐鸡翅的 PR&lt;/a&gt;。我发现中国人花在食物烹饪的时间成本上有点高，我依然点了不少的外卖。等各方面条件具备后我会尽可能花少的时间做简单的西餐来替换现有的饮食方式、并持续减少盐分、高油脂的摄入。&lt;/p&gt;
&lt;p&gt;2022-12-27 后续更新: 发布此文后一周，我感染了新冠病毒。多次高烧突破 40 度，目前仍有些许低烧、乏力、困倦、多汗，并几乎同时丧失了味觉和嗅觉。我的父母也几乎同步感染，尽管我目前和他们不生活在一起。和多数人一样，可能是有生之年最痛苦的一次发烧经历。&lt;/p&gt;
&lt;h3 id=&quot;music&quot;&gt;&lt;a href=&quot;#music&quot;&gt;Music&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;周杰伦的今年新专马马虎虎。力荐的那当然是今年 The 1975 的新专 &lt;a href=&quot;https://music.163.com/#/album?id=153050699&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;&lt;em&gt;Being Funny In A Foreign Language&lt;/em&gt;&lt;/a&gt;。另外附上 Matty 在 Apple Music 的真诚采访：&lt;/p&gt;
&lt;div&gt;
  &lt;bilibili bv=&quot;BV1Xe41137fJ&quot;&gt;&lt;/bilibili&gt;
&lt;/div&gt;
&lt;h3 id=&quot;movie&quot;&gt;&lt;a href=&quot;#movie&quot;&gt;Movie&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;毕业后的这几年看观影、观剧的时间不那么充裕了。今年相对好了一点，但在豆瓣也仅标记了 20 多部。漫威自从复联四结束后最近这两年口碑有点差了，《雷神 4》拍得稀烂，希望来年有新的精彩故事线。今年看的比较好的商业片里印象深刻的有：《瞬息全宇宙》《壮志凌云 2：独行侠》《投行风云》《西线无战事》以及三季《Barry / 巴瑞》，尤其是这部 HBO 的犯罪片，讲述退役兵逐梦演艺圈的故事，很久都没有两三天一口气刷完好几季的感觉了，现在无比期待 2023 年的第四季。&lt;/p&gt;
&lt;div&gt;
  &lt;douban id=&quot;26707518&quot;&gt;&lt;/douban&gt;
  &lt;douban id=&quot;3042261&quot;&gt;&lt;/douban&gt;
&lt;/div&gt;
&lt;h2 id=&quot;things&quot;&gt;&lt;a href=&quot;#things&quot;&gt;Things&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;软件领域，我把我常用的《极光词典》换成了《欧路词典》，导入了 iOS 内置的 mdx 词库文件。理由是《极光词典》不具备单词本功能，复习新单词无任何操作入口，但我仍然保留了它，因为作为词典这种功能型应用，已然完全胜出市面 99% 的同类产品了。&lt;/p&gt;
&lt;p&gt;效率工具方面接下来我会重点去使用 &lt;a href=&quot;https://retool.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Retool&lt;/a&gt; 开发一套自己的 Workflow，类似于 IFTTT 的平台但复杂度比 IFTTT 高许多。搜索领域我开始用了 ChatGPT Chrome Extension 搭配 Google。&lt;/p&gt;
&lt;p&gt;文本编辑器方面，我开始尝试使用 &lt;a href=&quot;https://obsidian.md/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Obsidian&lt;/a&gt;。目前桌面端和移动端都有一些 bug，作为 Markdown 编辑器，它的使用门槛对小白来说很低 —— 单纯的码字工具而已，而它的上限对习惯折腾的玩家来说也很高 —— 丰富多元的社区第三方插件。&lt;/p&gt;
&lt;p&gt;最后不得不提的便是 Cloudflare 的优秀网络工具 &lt;a href=&quot;https://1.1.1.1/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;WARP+&lt;/a&gt;。他们开会期间我完全依赖它才能正常上网。使用期间发现 WARP 有流量限制，利用 API 刷到了几十 TB 的额度后发现其下「零信任网络&lt;sup&gt;&lt;a href=&quot;#user-content-fn-2&quot; id=&quot;user-content-fnref-2&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;」是完全免费且不限流量的 —— 我果断切换成此模式，同时仍然续费另外一项网络协议工具互为备选方案来帮我维持突破网络封锁的高可用。&lt;/p&gt;
&lt;h2 id=&quot;have-fun-secretly&quot;&gt;&lt;a href=&quot;#have-fun-secretly&quot;&gt;Have fun, secretly&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;今年 10 月中下旬，我的网易云账号因在某天评论了一首歌被禁言 366 天。微博帐号也只因&lt;strong&gt;点赞&lt;/strong&gt;评论某事件&lt;sup&gt;&lt;a href=&quot;#user-content-fn-3&quot; id=&quot;user-content-fnref-3&quot; data-footnote-ref aria-describedby=&quot;footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt;的微博而被永久封禁。对此我的态度只有：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;😅&lt;/p&gt;
&lt;/blockquote&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;footnote-label&quot;&gt;&lt;a href=&quot;#footnote-label&quot;&gt;Footnotes&lt;/a&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-fn-1&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.oreilly.com/library/view/counterparty-credit-risk/9781118316665/c18anchor-2.html&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;The Role of a CVA Desk - O&apos;Reilly&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-2&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cloudflare.com/zh-cn/products/zero-trust/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Cloudflare Zero Trust&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-2&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-fn-3&quot;&gt;
&lt;p&gt;&lt;a href=&quot;https://bit.ly/3V5V1NG&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;现状不可描述&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-3&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</summary><author><name>Lawrence</name></author></entry><entry><title>草台班子</title><id>https://lawrenceli.me/blog/makeshift-troupe</id><link href="https://lawrenceli.me/blog/makeshift-troupe"/><published>2022-09-01T00:00:00.000Z</published><updated>2022-09-01T00:00:00.000Z</updated><category term="viewpoint" label="viewpoint" scheme="https://lawrenceli.me/tag/viewpoint"/><category term="thought" label="thought" scheme="https://lawrenceli.me/tag/thought"/><summary type="html">&lt;p&gt;鲁迅童年时代看的那些社戏，也叫草台戏，草台班子得名于此。通常有三五人在乡村空旷处搭上一处简陋布篷，水平不一的戏曲演员唱些群众喜闻乐见的戏曲，天黑了收点门票钱就拆台收东西继续赶往下一处演出。我很小的时候在外婆家有幸听过一回，都是老人带着小孩去听戏，内容全无印象。&lt;/p&gt;
&lt;p&gt;后来，见到有位网友有如下生动总结：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我工作以后才发现，大家都是草台班子。XX 草台，企业草台，我也草台，大家都草台，凑合赚钱过日子。一个企业，看着像一台奔驰在高速公路上的豪华轿车，里面其实是几个人蹬着自行车顶个壳。路上的车都是这样，大家谁都不戳破。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们童年时代看大人们做事、参与各种社会活动，可能总会觉得他们应该是清晰、明确地知道自己在做什么，并且知道自己为什么要这样做，甚至基于此会构建出科学理性的决策。毕业多年后，结合我所经历的一些工作（长三角地区，囊括上市公司、外企、创业团队），以及所认识的在诸多行业、诸多企事业单位的打工人经历，愈加肯定上述观点。&lt;/p&gt;
&lt;p&gt;马某去年在&lt;a href=&quot;https://finance.sina.com.cn/money/bank/bank_hydt/2020-10-24/doc-iiznezxr7822563.shtml&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;某峰会上的一席话&lt;/a&gt;，正是启发我找到类似观点的导火线：「中国金融没有系统性风险，因为中国的金融基本没有系统」。&lt;/p&gt;
&lt;p&gt;他所谓的「系统」是什么？会不会是一系列共识协议？或者法律条文？亦或是由上到下的多级政府部门？还是说某类高度复杂的中心化组织？... 我不知道，听众也不知道，或许马某自己也不知道。这个系统是什么我们无从得知，但唯一确定的是，它一定不是草台。&lt;/p&gt;
&lt;p&gt;我不相信「车到山前必有路」。我一直坚信，人一定会犯错；而且往往出于本位主义的影响，会形成对现状的误判。系统性的思维有助于减少这类情况的发生，无论现状是如何的草台。拥有系统性思维的人却总是那么少。你不可能对于一项没有经历过的事物、事件产生全面的、全局的、深刻的洞见，这种看待事物的方式往往被人形容成「上帝视角」。问题来了，没有上帝，如何拥有系统性认知？现代工业提供了一种可能，机器。机器永远不会欺骗我们。我们可以利用计算机软件，按照既定的领域 (Domain) 规则，使用恰当的计算机语言，构建出运行在物理硬件设备上的、具备现实意义的&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E5%86%AF%C2%B7%E8%AF%BA%E4%BC%8A%E6%9B%BC%E7%BB%93%E6%9E%84&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;真正的系统&lt;/a&gt; —— 只要它有确定的规则和输入。&lt;/p&gt;
&lt;p&gt;除了计算机领域，我目前没有见到所谓类似的系统了。软件工程已经教我们用科学的方法论去驾驭计算机科学，但人类社会总会有一些草台班子的案例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2020.1 「能」「明白」&lt;/li&gt;
&lt;li&gt;2021.1 大连车务段，全力攻关一昼夜&lt;/li&gt;
&lt;li&gt;2022.2 某外交部发言人对俄乌战争开战前的评论：M 国情报就是个笑话&lt;/li&gt;
&lt;li&gt;2022.6 某岛国公务员：因醉酒丢失包含 46 万公民信息的 U 盘&lt;/li&gt;
&lt;li&gt;2022.7 某地某公权力部门：十亿公民数据泄露&lt;/li&gt;
&lt;li&gt;2022.8 某地大数据中心：4850 万数据泄露&lt;/li&gt;
&lt;li&gt;2022.9 某地隔离转运大巴车侧翻致 27 人遇难&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;人们愿意相信使用计算机技术所展现出来的数据给人带来安全感，一种「啊，都在系统中」的幻象，一种不那么草台的假象。当然，上述草台案例虽然看似和技术息息相关，本质上和它关系不大，更多时候是工程问题和流程问题，本质还是人的问题。对于数据泄露事件，他们决口不会承认，甚至会采取&lt;a href=&quot;https://s.weibo.com/weibo?q=%23%E5%8D%81%E4%BA%BF%E5%85%AC%E6%B0%91%E6%95%B0%E6%8D%AE%E6%B3%84%E9%9C%B2%23&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;审查&lt;/a&gt;措施防止事件放大，至少邻国事件的主角还会假装鞠一躬以应付公众。&lt;/p&gt;
&lt;p&gt;除上述领域外，方方面面都有类似的草台班子。「微博办案」、苏州和服事件、「专家建议」，无一不彰显司法或舆论的草台。法治的道路还有很远要走，十年前的&lt;a href=&quot;https://zh.wikipedia.org/zh-cn/2013%E5%B9%B4%E3%80%8A%E5%8D%97%E6%96%B9%E5%91%A8%E6%9C%AB%E3%80%8B%E6%96%B0%E5%B9%B4%E7%89%B9%E5%88%8A%E4%BA%8B%E4%BB%B6&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;宪政梦&lt;/a&gt;也至今仍然不让做。&lt;/p&gt;
&lt;p&gt;现代社会发展的历史，也是革草台命的历史。或许有人会因为工业化的成就而沾沾自喜，但在&lt;a href=&quot;https://books.google.com.hk/books?id=lINbEAAAQBAJ&amp;#x26;printsec=frontcover&amp;#x26;dq=%E8%84%B1%E8%8A%82%E7%9A%84%E5%9B%BD%E5%BA%A6&amp;#x26;hl=zh-CN&amp;#x26;sa=X&amp;#x26;ved=2ahUKEwiKi8r2_fP5AhVaslYBHdv5AR4Q6AF6BAgIEAI#v=onepage&amp;#x26;q=%E8%84%B1%E8%8A%82%E7%9A%84%E5%9B%BD%E5%BA%A6&amp;#x26;f=false&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;脱节的国度&lt;/a&gt;里我反而希望有更多冒名顶替综合征患者。加入外企一年多，我感受到 M 国公司为 Business Continuity 做出的基础设施投入相比国内的草台来说高了不少，也让员工产生出「你处于我们的系统之中」这样的幻想，但是我们不能保证这些基础设施都不是草台，因为供应链攻击是常有的事。一个恰当的 meme：&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;/images/makeshift-troupe/infra.png&quot; alt=&quot;Mordern Infrastructor&quot;&gt;&lt;figcaption&gt;Mordern Infrastructor&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;大多数人原本或许并不草台，当他们加入某个草台团体时，会因团体的草台而被迫草台，或者离开。死海效应则是最佳例证。西语也有类似观点：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Culture eats strategy for breakfast.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不可否认，所有有人的地方都会有草台班子的影子，从真正的草台戏到硅谷华尔街，从派出所到外交部。毛主席就讲过:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;......只是一个空架子，其内面全没有什么东西...... 生息了四千多年，不知干什么去了？一点没有组织，一个有组织的社会看不见，一块有组织的地方看不见...... 没有科学脑筋，不知分析与概括的关系，有小的细胞才有大的有机体，有分子的各个才有团体。 &lt;em&gt;《毛泽东早期文稿》&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;或许有人说，这个社会需要草台，因为草台少了唱戏的可能会饿死，社会达尔文可不是闹着玩的。但更多时候这会让自己意识到：时刻保持谦卑和同理心，降低对群体的期望，厘清群己权界，才能和这个草台世界和解 —— 那是机器不曾具备的素质。&lt;/p&gt;
&lt;p&gt;倘若消极看待整个人类社会，荒诞的草台班子或许是唯一最终能看到的答案。之所以荒诞，是因为对客观世界的认知能力没能达到一定层次而造成的心理落差 —— 这个世界的本质就是荒诞，只是草台现象的必然发生超出人类的合理认知；悲观的人一次次看到这种落差的存在会陷入本不该有的精神内耗。对此也许我们可以改变自身的眼光，始终抱以善意、同情的态度去解释所谓草台班子的行为，并积极参与、鼓励、帮助改变不合理的、荒诞的规则，构建符合当代人类认知的合理的但不绝对合理甚至未来还显得荒诞的社会意识。&lt;/p&gt;
&lt;p&gt;生命的力量在于不顺从，或许我所能发现人类社会中的唯一不草台的地方在于人类不接受草台的设定本身，&lt;/p&gt;
&lt;p&gt;本文受下列文章启发有所感：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://successfulsoftware.net/2022/06/19/no-one-knows-what-they-are-doing/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;https://successfulsoftware.net/2022/06/19/no-one-knows-what-they-are-doing/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</summary><author><name>Lawrence</name></author></entry><entry><title>这个网站是如何构建的</title><id>https://lawrenceli.me/blog/how</id><link href="https://lawrenceli.me/blog/how"/><published>2022-08-27T00:00:00.000Z</published><updated>2024-12-07T00:00:00.000Z</updated><category term="markdown" label="markdown" scheme="https://lawrenceli.me/tag/markdown"/><category term="example" label="example" scheme="https://lawrenceli.me/tag/example"/><category term="guide" label="guide" scheme="https://lawrenceli.me/tag/guide"/><category term="jamstack" label="jamstack" scheme="https://lawrenceli.me/tag/jamstack"/><category term="ssg" label="ssg" scheme="https://lawrenceli.me/tag/ssg"/><category term="vercel" label="vercel" scheme="https://lawrenceli.me/tag/vercel"/><category term="nextjs" label="nextjs" scheme="https://lawrenceli.me/tag/nextjs"/><category term="design" label="design" scheme="https://lawrenceli.me/tag/design"/><summary type="html">&lt;h2 id=&quot;使用方式&quot;&gt;&lt;a href=&quot;#使用方式&quot;&gt;使用方式&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;本站源码完全公开：&lt;a href=&quot;https://github.com/la3rence/site&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;GitHub - la3rence/site&lt;/a&gt;.&lt;/p&gt;
&lt;div&gt;
  &lt;github user=&quot;la3rence&quot; repo=&quot;site&quot;&gt;&lt;/github&gt;
&lt;/div&gt;
&lt;p&gt;直接将 Markdown 文件 (.md) 存放在 &lt;code&gt;posts&lt;/code&gt; 文件夹下即可渲染当前文件，使之成为静态页面(&lt;a href=&quot;/blog/ssg-ssr&quot;&gt;SSG&lt;/a&gt;)。
博客的首页索引目录也为同步生成，而无需手动维护。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;About&lt;/strong&gt; 页面也同理基于 &lt;code&gt;readme.md&lt;/code&gt; 文件生成而来。&lt;/p&gt;
&lt;p&gt;博客相关的信息配置，如标题、作者等可在 &lt;code&gt;lib/config.mjs&lt;/code&gt; 文件中配置。&lt;/p&gt;
&lt;p&gt;推荐使用 &lt;a href=&quot;https://pnpm.io/zh/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;pnpm&lt;/a&gt; 来作为 node.js 的依赖管理工具（相比官方 npm，pnpm 拥有非常大的优势: 速度更快，且节省空间）。&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;pnpm&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; install&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;pnpm&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; dev&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;# [Optional] install turbo via `pnpm i -g turbo`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;# and you can try `turbo` for run any script in package.json like:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;turbo&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; dev&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;turbo&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; build&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本地访问 &lt;a href=&quot;http://localhost:3000&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;http://localhost:3000&lt;/a&gt; 即可。&lt;/p&gt;
&lt;p&gt;通过 &lt;code&gt;pnpm build&lt;/code&gt; 来打包，&lt;code&gt;pnpm start&lt;/code&gt; 则用于生产环境的启动。
通过 &lt;code&gt;pnpm fmt&lt;/code&gt; 来将所有代码和文本进行格式化。&lt;/p&gt;
&lt;p&gt;这篇文章展示了此博客项目所能展示的一切媒体信息，比如代码引用、表格展示、图片、视频、豆瓣卡片等等。&lt;/p&gt;
&lt;p&gt;2024/07 更新：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;顶部导航栏启用毛玻璃动态悬浮&lt;/li&gt;
&lt;li&gt;全新的 edge to edge 设计风格，充分利用屏幕区域展示代码和图片&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;技术细节&quot;&gt;&lt;a href=&quot;#技术细节&quot;&gt;技术细节&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;此站点由 Vercel 公司开源的 &lt;code&gt;Next.js&lt;/code&gt; 框架和 &lt;code&gt;TailwindCSS&lt;/code&gt; 样式构成。前者是一项基于 React 的 SSG/SSR 开源项目，后者是一个目前流行的原子化 CSS 库，让不太会写 CSS、基础薄弱的我也能快速的写出灵活的样式。&lt;/p&gt;
&lt;p&gt;Next.js 会主动调用我们写好的一些函数 (&lt;code&gt;getStaticProps()&lt;/code&gt;)，让组件得到数据的输入，从而&lt;strong&gt;在构建阶段&lt;/strong&gt;将 React 组件提前渲染完成。&lt;code&gt;remark&lt;/code&gt; 库可以将原生的 markdown 语法编译成 html 对应的 dom - 在此项目中，我们让它固定遍历 &lt;code&gt;posts&lt;/code&gt; 文件夹下的 markdown 文件，依次编译，让其作为 &lt;code&gt;[id].js&lt;/code&gt; 的动态路由页面的 &lt;code&gt;props&lt;/code&gt;，从而渲染出博客文章:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;export&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; const&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; getStaticProps&lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; async&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FFA657&quot;&gt; context&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; =&gt;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; { &lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt;id&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; } &lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; context.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;params&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line highlighted&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;  const&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; mdData&lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; getMdContentById&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(id);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;  return&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;    props&lt;/span&gt;&lt;span style=&quot;color:#0184BC;--shiki-dark:#E6EDF3&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; mdData,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;  };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做的好处很多:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;适合被 CDN 缓存&lt;/li&gt;
&lt;li&gt;优秀的 SEO 表现&lt;/li&gt;
&lt;li&gt;节省网络带宽、流量&lt;/li&gt;
&lt;li&gt;O(1) 时间复杂度的 &lt;a href=&quot;https://en.wikipedia.org/wiki/Time_to_first_byte&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;TTFB&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总之一个字，快！如果你尝试关闭当前浏览器的 JavaScript 功能，这个网站也一样能正确渲染并展现。&lt;/p&gt;
&lt;p&gt;不光如此，Next.js 也提供了&lt;strong&gt;服务端渲染&lt;/strong&gt;的能力，同样也能带来较好的 SEO 体验，不展开说了。&lt;/p&gt;
&lt;p&gt;上述这种技术被称之为 &lt;a href=&quot;#tables&quot;&gt;JAMstack&lt;/a&gt;：&lt;strong&gt;J&lt;/strong&gt;avaScript, &lt;strong&gt;A&lt;/strong&gt;PI &amp;#x26; &lt;strong&gt;M&lt;/strong&gt;arkup.&lt;/p&gt;
&lt;p&gt;一种有趣的说法是，JAMstack 是 CDN 优先的应用程序。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It&apos;s now possible, instead, to push content directly to the network and design frameworks that optimize for this capability. As a result, with optimizations like static asset hoisting, websites are now becoming faster and more reliable than ever before.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;movie&quot;&gt;&lt;a href=&quot;#movie&quot;&gt;Movie&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;你可以编写特定的 React 组件来让 Markdown 支持更丰富的页面内容，这种实现方式和 &lt;code&gt;MDX&lt;/code&gt; 类似。
The source of this component is coded via react component in markdown!&lt;/p&gt;
&lt;p&gt;Code:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A0A1A7;--shiki-light-font-style:italic;--shiki-dark:#8B949E;--shiki-dark-font-style:inherit&quot;&gt;// filename: how.md&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;  &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;douban&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; id&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;3042261&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;&amp;#x3C;/&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;douban&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#7EE787&quot;&gt;div&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;div&gt;
  &lt;douban id=&quot;3042261&quot;&gt;&lt;/douban&gt;
&lt;div&gt;
&lt;h3 id=&quot;images&quot;&gt;&lt;a href=&quot;#images&quot;&gt;Images&lt;/a&gt;&lt;/h3&gt;
&lt;figure&gt;&lt;img src=&quot;https://proxy.lawrenceli.me/picsum.photos/400/600?grayscale&quot; alt=&quot;Random Image&quot;&gt;&lt;figcaption&gt;Random Image&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h3 id=&quot;video&quot;&gt;&lt;a href=&quot;#video&quot;&gt;Video&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;可以直接通过 &lt;code&gt;&amp;#x3C;bilibili /&gt;&lt;/code&gt; 组件展示来自于 B 站视频，基于 iframe.&lt;/p&gt;
&lt;div&gt;
  &lt;bilibili bv=&quot;BV1gR4y1u76v&quot;&gt;&lt;/bilibili&gt;
&lt;/div&gt;
&lt;!-- or:
&lt;div class=&quot;embed&quot;&gt;
  &lt;iframe src=&quot;//player.bilibili.com/player.html?bvid=BV1i44y1N7kU&amp;danmaku=0&amp;high_quality=1&quot;
  &gt;&lt;/iframe&gt;
&lt;/div&gt; --&gt;
&lt;h3 id=&quot;tweet--𝕏&quot;&gt;&lt;a href=&quot;#tweet--𝕏&quot;&gt;Tweet / 𝕏&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;tweet /&gt;&lt;/code&gt; 组件展示推文：&lt;/p&gt;
&lt;div&gt;
  &lt;tweet id=&quot;1138070453942009856&quot; /&gt;
&lt;/div&gt;
&lt;h3 id=&quot;open-graph&quot;&gt;&lt;a href=&quot;#open-graph&quot;&gt;Open Graph&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Open Graph (OG, 开放图谱协议) 用于社交网络分享网页时展示特定的富媒体信息。&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://lawrenceli.me/api/og?meta=This%20is%20Open%20Graph&quot; alt=&quot;Open Graph&quot;&gt;&lt;figcaption&gt;Open Graph&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;tables&quot;&gt;&lt;a href=&quot;#tables&quot;&gt;Tables&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;JAMstack.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;center&quot;&gt;ROLE&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;PROVIDED BY&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;J&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;Client-side JS injected via React Hooks (state, event listeners, effects)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;A&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;API pages inside the pages/api directory.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;M&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;Pages with no data dependencies or pages with static data deps that trigger build-time static site generation.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&quot;github-gist&quot;&gt;&lt;a href=&quot;#github-gist&quot;&gt;GitHub Gist&lt;/a&gt;&lt;/h2&gt;
&lt;div id=&quot;gist122750024&quot; class=&quot;gist border-0 my-2 not-prose&quot;&gt;
    &lt;div class=&quot;gist-file&quot; translate=&quot;no&quot; data-color-mode=&quot;light&quot; data-light-theme=&quot;light&quot;&gt;
      &lt;div class=&quot;gist-data&quot;&gt;
        
&lt;div class=&quot;js-gist-file-update-container js-task-list-container&quot;&gt;
      &lt;div id=&quot;file-gist-test-ts&quot; class=&quot;file my-2&quot;&gt;
    
    &lt;div itemprop=&quot;text&quot; class=&quot;Box-body p-0 blob-wrapper data type-typescript&quot; style=&quot;overflow: auto&quot; tabindex=&quot;0&quot; role=&quot;region&quot; aria-label=&quot;gist-test.ts content, created by darylwright on 12:50PM on June 01, 2023.&quot;&gt;

        
&lt;div class=&quot;js-check-hidden-unicode js-blob-code-container blob-code-content&quot;&gt;

  &lt;template class=&quot;js-file-alert-template&quot;&gt;
  &lt;div data-view-component=&quot;true&quot; class=&quot;flash flash-warn flash-full d-flex flex-items-center&quot;&gt;
  &lt;svg aria-hidden=&quot;true&quot; height=&quot;16&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; data-view-component=&quot;true&quot; class=&quot;octicon octicon-alert&quot;&gt;
    &lt;path d=&quot;M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z&quot;&gt;&lt;/path&gt;
&lt;/svg&gt;
    &lt;span&gt;
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      &lt;a class=&quot;Link--inTextBlock&quot; href=&quot;https://github.co/hiddenchars&quot; target=&quot;_blank&quot;&gt;Learn more about bidirectional Unicode characters&lt;/a&gt;
    &lt;/span&gt;


  &lt;div data-view-component=&quot;true&quot; class=&quot;flash-action&quot;&gt;        &lt;a href=&quot;{{ revealButtonHref }}&quot; data-view-component=&quot;true&quot; class=&quot;btn-sm btn&quot;&gt;    Show hidden characters
&lt;/a&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;/template&gt;
&lt;template class=&quot;js-line-alert-template&quot;&gt;
  &lt;span aria-label=&quot;This line has hidden Unicode characters&quot; data-view-component=&quot;true&quot; class=&quot;line-alert tooltipped tooltipped-e&quot;&gt;
    &lt;svg aria-hidden=&quot;true&quot; height=&quot;16&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; data-view-component=&quot;true&quot; class=&quot;octicon octicon-alert&quot;&gt;
    &lt;path d=&quot;M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z&quot;&gt;&lt;/path&gt;
&lt;/svg&gt;
&lt;/span&gt;&lt;/template&gt;

  &lt;table data-hpc=&quot;&quot; class=&quot;highlight tab-size js-file-line-container&quot; data-tab-size=&quot;4&quot; data-paste-markdown-skip=&quot;&quot; data-tagsearch-path=&quot;gist-test.ts&quot;&gt;
        &lt;tbody&gt;&lt;tr&gt;
          &lt;td id=&quot;file-gist-test-ts-L1&quot; class=&quot;blob-num js-line-number js-blob-rnum&quot; data-line-number=&quot;1&quot;&gt;&lt;/td&gt;
          &lt;td id=&quot;file-gist-test-ts-LC1&quot; class=&quot;blob-code blob-code-inner js-file-line highlighted&quot;&gt;&lt;span class=&quot;pl-k&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;pl-en&quot;&gt;compare&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span class=&quot;pl-smi&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;a0&lt;/span&gt;: &lt;span class=&quot;pl-smi&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;b0&lt;/span&gt;: &lt;span class=&quot;pl-smi&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt;: &lt;span class=&quot;pl-smi&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;pl-kos&quot;&gt;{&lt;/span&gt;&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
          &lt;td id=&quot;file-gist-test-ts-L2&quot; class=&quot;blob-num js-line-number js-blob-rnum&quot; data-line-number=&quot;2&quot;&gt;&lt;/td&gt;
          &lt;td id=&quot;file-gist-test-ts-LC2&quot; class=&quot;blob-code blob-code-inner js-file-line&quot;&gt;  &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;a0&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;b0&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
          &lt;td id=&quot;file-gist-test-ts-L3&quot; class=&quot;blob-num js-line-number js-blob-rnum&quot; data-line-number=&quot;3&quot;&gt;&lt;/td&gt;
          &lt;td id=&quot;file-gist-test-ts-LC3&quot; class=&quot;blob-code blob-code-inner js-file-line highlighted&quot;&gt;  &lt;span class=&quot;pl-k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;pl-kos&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;pl-s1&quot;&gt;a0&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;pl-s1&quot;&gt;b0&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
          &lt;td id=&quot;file-gist-test-ts-L4&quot; class=&quot;blob-num js-line-number js-blob-rnum&quot; data-line-number=&quot;4&quot;&gt;&lt;/td&gt;
          &lt;td id=&quot;file-gist-test-ts-LC4&quot; class=&quot;blob-code blob-code-inner js-file-line highlighted&quot;&gt;  &lt;span class=&quot;pl-k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;pl-c1&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;pl-c1&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;pl-kos&quot;&gt;;&lt;/span&gt;&lt;/td&gt;
        &lt;/tr&gt;
        &lt;tr&gt;
          &lt;td id=&quot;file-gist-test-ts-L5&quot; class=&quot;blob-num js-line-number js-blob-rnum&quot; data-line-number=&quot;5&quot;&gt;&lt;/td&gt;
          &lt;td id=&quot;file-gist-test-ts-LC5&quot; class=&quot;blob-code blob-code-inner js-file-line highlighted&quot;&gt;&lt;span class=&quot;pl-kos&quot;&gt;}&lt;/span&gt;&lt;/td&gt;
        &lt;/tr&gt;
  &lt;/tbody&gt;&lt;/table&gt;
&lt;/div&gt;


    &lt;/div&gt;

  &lt;/div&gt;

&lt;/div&gt;

      &lt;/div&gt;
      &lt;div class=&quot;gist-meta&quot;&gt;
        &lt;a href=&quot;https://gist.github.com/darylwright/75332f27a6e9bff70bc0406114570829/raw/56ae74d7dda80005ce853ba2307ef15abf3b7e99/gist-test.ts&quot; style=&quot;float:right&quot; class=&quot;Link--inTextBlock&quot;&gt;view raw&lt;/a&gt;
        &lt;a href=&quot;https://gist.github.com/darylwright/75332f27a6e9bff70bc0406114570829#file-gist-test-ts&quot; class=&quot;Link--inTextBlock&quot;&gt;
          gist-test.ts
        &lt;/a&gt;
        hosted with ❤ by &lt;a class=&quot;Link--inTextBlock&quot; href=&quot;https://github.com&quot;&gt;GitHub&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;websub-for-rss&quot;&gt;&lt;a href=&quot;#websub-for-rss&quot;&gt;WebSub for RSS&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;基于 GitHub Actions 来支持 WebSub 标准，即之前的 PubSubHubbub。这使得支持 WebSub 的 RSS 客户端不仅能立刻获取到新文章，也能减少客户端和服务端之间的轮询次数以及流量。细节: &lt;a href=&quot;https://github.com/la3rence/site/issues/324&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;site/issue/324&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;activitypub&quot;&gt;&lt;a href=&quot;#activitypub&quot;&gt;ActivityPub&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;网站通过最基本的一些 ActivityPub 和 WebFinger 协议部分实现了与联邦宇宙的交互。&lt;a href=&quot;https://lawrenceli.me/blog/activitypub&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;实现细节&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&quot;i18n&quot;&gt;&lt;a href=&quot;#i18n&quot;&gt;I18n&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;网站通过简单的配置以支持国际化。通过 markdown 文件名的中间后缀来和 Next.js 原生 locale 路由来实现区分语言类型展示不同文本。&lt;/p&gt;
&lt;h2 id=&quot;and-more-to-do&quot;&gt;&lt;a href=&quot;#and-more-to-do&quot;&gt;And more to do&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Markdown 目前采用 GitHub Flavored Markdown (&lt;a href=&quot;https://github.github.com/gfm/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;GFM&lt;/a&gt;) ，并尝试添加新的语法元素支持。&lt;/p&gt;
&lt;p&gt;例如：&lt;a href=&quot;https://github.com/orgs/community/discussions/16925&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;https://github.com/orgs/community/discussions/16925&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;markdown-alert markdown-alert-note&quot;&gt;&lt;p class=&quot;markdown-alert-title&quot;&gt;&lt;svg class=&quot;octicon octicon-info mr-2&quot; viewBox=&quot;0 0 16 16&quot; version=&quot;1.1&quot; width=&quot;16&quot; height=&quot;16&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z&quot;&gt;&lt;/path&gt;&lt;/svg&gt;Note&lt;/p&gt;&lt;p&gt;Critical content demanding immediate user attention due to potential risks.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;ref-参考链接&quot;&gt;&lt;a href=&quot;#ref-参考链接&quot;&gt;Ref 参考链接&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;https://nextjs.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tailwindcss.com&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;https://tailwindcss.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://rauchg.com/2020/static-hoisting&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;https://rauchg.com/2020/static-hoisting&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://jamstack.org&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;https://jamstack.org&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</summary><author><name>Lawrence</name></author></entry><entry><title>基于 Go 的 ChatOps 实践</title><id>https://lawrenceli.me/blog/chat-ops</id><link href="https://lawrenceli.me/blog/chat-ops"/><published>2021-02-06T00:00:00.000Z</published><updated>2021-02-06T00:00:00.000Z</updated><category term="devops" label="devops" scheme="https://lawrenceli.me/tag/devops"/><category term="golang" label="golang" scheme="https://lawrenceli.me/tag/golang"/><category term="chatops" label="chatops" scheme="https://lawrenceli.me/tag/chatops"/><category term="ci" label="ci" scheme="https://lawrenceli.me/tag/ci"/><summary type="html">&lt;p&gt;聊着天就把活干了。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;公司的运维一直由我来帮助实施。去年将所有微服务容器化后，运维的成本面临新的挑战。本着解放生产力的目标，顺带回忆不太熟悉的 Go 基本语法，笔者写了一个钉钉机器人来帮助我做一些运维的活。效果也不错，就把大体做法在这里讲讲。&lt;/p&gt;
&lt;p&gt;做这个之前有一系列的必要（也可替换的）基础设施和概念以及 HTTP 服务端开发的认知，能实现这套聊天运维离不开每一个环节以及诸多细节。简单概括一下我是怎么实践的。&lt;/p&gt;
&lt;h3 id=&quot;技术栈依赖&quot;&gt;&lt;a href=&quot;#技术栈依赖&quot;&gt;技术栈依赖&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;实践 ChatOps 至少需要的基础设施：一个 CI / CD 服务，一个暴露在公网的 HTTP 服务，一个远程 VCS 和一个能提供收发消息 API 的即时通信工具。&lt;/p&gt;
&lt;p&gt;本项目采用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.jenkins.io/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Jenkins&lt;/a&gt; (CI / CD，经典的持续集成工具，提供公网可访问的 REST API)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://git-scm.com/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Git&lt;/a&gt; (VCS，本项目使用公司正在用的 &lt;a href=&quot;https://gitee.com&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Gitee.com&lt;/a&gt; - 可使用 &lt;a href=&quot;http://github.com&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;GitHub.com&lt;/a&gt; 或 GitLab，提供 REST API 或 SDK)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vercel.com/docs/serverless-functions/introduction&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Serverless&lt;/a&gt; (用来暴露核心的 HTTP 端点，只是无需运维而已；也不是必须，否则需要有公网服务器)&lt;/li&gt;
&lt;li&gt;DingTalk (限制&lt;a href=&quot;https://ding-doc.dingtalk.com/document/app/develop-enterprise-internal-robots&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;企业内部机器人&lt;/a&gt;，能提供 WebHook 和收发消息的 SDK 即可，或其他类似 Slack 的聊天应用: 企业微信、飞书等等)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;系统拓扑&quot;&gt;&lt;a href=&quot;#系统拓扑&quot;&gt;系统拓扑&lt;/a&gt;&lt;/h3&gt;
&lt;figure&gt;&lt;img src=&quot;/images/chat-ops/1.png&quot; alt=&quot;ChatOPS&quot;&gt;&lt;figcaption&gt;ChatOPS&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;懒汉只会和机器人以聊天的形式交互，&lt;a href=&quot;/blog/the-world-as-i-see-it&quot;&gt;其余的脏活累活重复劳动交给机器人做&lt;/a&gt;。大致流程如图，重点实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;迎接聊天机器人的 HTTP 请求&lt;/li&gt;
&lt;li&gt;根据请求文本触发行为
&lt;ul&gt;
&lt;li&gt;Git Workflow
&lt;ul&gt;
&lt;li&gt;Approve Pull Request&lt;/li&gt;
&lt;li&gt;Test Pull Request&lt;/li&gt;
&lt;li&gt;Merge Pull Request&lt;/li&gt;
&lt;li&gt;Show PR list&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;CI / CD
&lt;ul&gt;
&lt;li&gt;CI Test&lt;/li&gt;
&lt;li&gt;Deploy to Production&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;很显然，核心的开发工作的都在 Message Handler 那里。根据聊天机器人提供的 Event Hook（每次聊天机器人收到消息时会自动向某个服务器发送 HTTP 请求，会携带发送消息的用户、消息内容等 Payload），可以设计一个 HTTP 端点，在服务端解析这些请求（钉钉需要按照企业内部机器人开发，&lt;a href=&quot;https://ding-doc.dingtalk.com/document/app/develop-enterprise-internal-robots&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;参考这篇开发文档&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;我们按照 &lt;a href=&quot;/blog/prow&quot;&gt;Kubernetes Prow 的设计语言&lt;/a&gt;，用一个 &lt;code&gt;/&lt;/code&gt; 来作为 Tag，形式如同 &lt;code&gt;/test xxx&lt;/code&gt; .&lt;/p&gt;
&lt;p&gt;因此这里必然需要做字符串处理了。除了判断 Tag 的存在之外，要取 Tag 后面的参数。项目用 Go 实现，很简单，贴其中一工具函数的代码：&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; StringIndexOf&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FFA657&quot;&gt;originalArray&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; []&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FFA657&quot;&gt;wordToFind&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; interface&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;{}) []&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;    length&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; :=&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; len&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;originalArray&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;    interfaceArray&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; :=&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; make&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;([]&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;interface&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;{}, &lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;length&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; i&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;v&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; :=&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt; range&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; originalArray&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;        interfaceArray&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; v&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;    var&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; indexArray&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; []&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;int&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; i&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt;:=&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; ; &lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; length&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;; &lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt;++&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;      if&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; strings&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;Compare&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;wordToFind&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.(&lt;/span&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;string&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;originalArray&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;]) &lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;        indexArray&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; append&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;indexArray&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;i&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;      }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;	return&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; indexArray&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拿到参数后继续判断，比如机器人收到了这个指令：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/merge 233&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;即合并第 #233 个 Pull Request。要通过代码来合并，那操作 Git 就需要 SDK 或接口了。考虑到之前写 &lt;a href=&quot;https://github.com/la3rence/OpsBot&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Issue Ops Bot&lt;/a&gt; 的经验，Google 开源的 GitHub 的 &lt;a href=&quot;https://github.com/google/go-github&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Go-GitHub&lt;/a&gt; 写起来很方便，Gitee 应该也有人搞吧？果然搜到了华为的 OpenEuler 团队的仓库：&lt;a href=&quot;http://gitee.com/openeuler/go-gitee&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Go-Gitee&lt;/a&gt;。看来只有大厂才会给这些基础设施写 SDK 啊！果断加依赖:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#FFA657&quot;&gt;go&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; get&lt;/span&gt;&lt;span style=&quot;color:#986801;--shiki-dark:#79C0FF&quot;&gt; -u&lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt; gitee.com/openeuler/go-gitee&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;版本下载下来给的时间戳居然就是这两天的，虽然没有任何测试代码，但看似更新蛮活跃，大胆直接用上了。后面写了一些接口，发现 SDK 提供的一些 API 也不全，比如没有审核通过 PR 和测试通过 PR 的 API，就自己顺带拿 &lt;code&gt;net/http&lt;/code&gt; 参考着 &lt;a href=&quot;https://gitee.com/api/v5/swagger&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Gitee 的 REST API&lt;/a&gt; 自己实现了，但是代码太丑陋（本来想模仿项目的写法去写，但是一心只想把功能完成），也没给华为提 issue，等回头有机会写下给它提个 PR 嘿嘿（除此之外我还发现 Gitee 的合并方式没有 &lt;code&gt;rebase&lt;/code&gt; 选项，只提供 &lt;code&gt;merge&lt;/code&gt; 和 &lt;code&gt;squash&lt;/code&gt; ，在所谓的社区企鹅群里和他们提了一下建议，理都不理)。&lt;/p&gt;
&lt;p&gt;如果机器人收到类似这样的指令:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/jenkins test myservice&lt;/code&gt; 或 &lt;code&gt;/deploy myservice&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;那就直接对接 &lt;a href=&quot;https://www.jenkins.io/doc/book/using/remote-access-api/&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Jenkins 的 REST API&lt;/a&gt; 了。可以直接凭状态码判断响应，安全方面 Jenkins 自身提供了 HTTP BASIC 认证。就不细说了。&lt;/p&gt;
&lt;p&gt;之前没有用 Go 写过 HTTP Server，这次也没有写——如果运维一套服务已经很麻烦了，开发出的一套帮助运维的辅助服务本身也需要运维（公网服务器、域名解析、持续的测试和部署等服务器资源以及&lt;a href=&quot;https://zh.wikipedia.org/wiki/%E6%B3%A8%E6%84%8F%E5%8A%9B%E7%B6%93%E6%BF%9F&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;注意力资源&lt;/a&gt;），我就没有使用传统的后端服务，而是使用了自己熟悉的 &lt;a href=&quot;https://vercel.com/docs/runtimes#official-runtimes/go&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Vercel Serveless for Go&lt;/a&gt;。公开一个 Handler 方法即可，&lt;code&gt;*http.Request&lt;/code&gt; 结构体指针的 &lt;code&gt;Body&lt;/code&gt; 就是机器人发来的消息了。&lt;/p&gt;
&lt;p&gt;下面是整个 Handler 的大致逻辑，伪代码：&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;func&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; Handler&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FFA657&quot;&gt;w&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#FFA657&quot;&gt; http&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#FFA657&quot;&gt;ResponseWriter&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FFA657&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#FFA657&quot;&gt;http&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#C18401;--shiki-dark:#FFA657&quot;&gt;Request&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;  defer&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; fmt&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;Fprintf&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;w&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;ok&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;  chatMessage&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;_&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; :=&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; ioutil&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;ReadAll&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;r&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;Body&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; strings&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;Contains&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;chatMessage&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;/deploy&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;     serverName&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; :=&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; getServerNameFromMessage&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;chatMessage&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;     jenkinsSDK&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;deploy&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;serverName&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#A626A4;--shiki-dark:#FF7B72&quot;&gt;  if&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt; strings&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;Contains&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;chatMessage&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#50A14F;--shiki-dark:#A5D6FF&quot;&gt;&quot;/merge&quot;&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;     pullRequestNumber&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt; :=&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt; getPullRequestNumberFromMessage&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;chatMessage&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;     giteeSDK&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#4078F2;--shiki-dark:#D2A8FF&quot;&gt;mergePullRequest&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#E45649;--shiki-dark:#E6EDF3&quot;&gt;pullRequestNumber&lt;/span&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt;  }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#FF7B72&quot;&gt;  ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#383A42;--shiki-dark:#E6EDF3&quot;&gt; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;期待某天 Vercel 能够支持 JVM 语言甚至是 Spring 的 Serverless。&lt;/p&gt;
&lt;h3 id=&quot;效果&quot;&gt;&lt;a href=&quot;#效果&quot;&gt;效果&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;比如让机器人展示下仓库目前的 Pull Request，然后测试这个 PR，通过后批准这个 PR，最终合并 PR，上线生产，聊天记录会是这样的：&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes one-light github-dark-default&quot; style=&quot;background-color:#FAFAFA;--shiki-dark-bg:#0d1117;color:#383A42;--shiki-dark:#e6edf3&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Users 命令: /pr&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Robot 回复: 当前仓库的 PullRequest 列表...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          [#709] fix: typo&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          [author] username&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Users 命令: /jenkins test micro-server 709&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Robot 回复: 开始测试 PullRequest 709&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;CIBot 回复:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          [jenkins] micro-server ci&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          -------------------------&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          任务：#666&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          状态：开始&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          持续时间：0 分 1 秒&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          执行人：Host 76.76.21.21&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Users 命令: /approve 709&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Robot 回复: 审核通过 PullRequest #709&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Users 命令: /merge 709&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Robot 回复: 合并 PullRequest #709&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Gitee 回复: User 接受了 Owner/repo 的 Pull Request !709 fix: typo&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Users 命令: /deploy micro-server&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Robot 回复: 生产发布 micro-server&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;CDBot 回复:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          [jenkins] micro-server cd&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          -------------------------&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          任务：#233&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          状态：开始&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          持续时间：0 分 1 秒&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          执行人：Host 76.76.21.21&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 友好地提供帮助&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Users 命令: /help&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Robot 回复: 当前支持指令列表, 带 * 需要特定人员发起&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          /pr - 展示仓库发起中的 PR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          /jenkins &amp;#x3C;action&gt; &amp;#x3C;servername&gt; &amp;#x3C;pr-number&gt; - 在指定 PR 下发起后端测试&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          /pass &amp;#x3C;pr-number&gt; - * 测试通过指定 PR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          /approve &amp;#x3C;pr-number&gt; - * 审核通过指定 PR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          /merge &amp;#x3C;pr-number&gt; - * 合并指定 PR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          /test &amp;#x3C;servername&gt; - 在仅有一个 PR 的状态下发起后端服务测试&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          /deploy &amp;#x3C;servername&gt; - * 发布服务至生产环境&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;          /help - 显示此帮助内容&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码写的很丑陋，没脸往外放了，而且也是企业内部使用，就不开源了（所有变量，十个左右，都配置在环境变量中，尽量没有 Hard Code）。&lt;/p&gt;
&lt;p&gt;社区也有不少钉钉机器人的 SDK，阿里没有提供 Go 版本的，但写起来也不复杂，顺手提供自己写的&lt;a href=&quot;https://github.com/la3rence/dingtalkbot-sdk&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;钉钉群机器人 Go 语言的 SDK&lt;/a&gt;，目前就只用来发文本消息。&lt;/p&gt;
&lt;div&gt;
  &lt;github user=&quot;la3rence&quot; repo=&quot;dingtalkbot-sdk&quot;&gt;&lt;/github&gt;
&lt;/div&gt;
&lt;p&gt;最近一次更新，让机器人支持了多个仓库，直接在 &lt;code&gt;/tag&lt;/code&gt; 最后加一个可选参数 &lt;code&gt;[repo]&lt;/code&gt;，然后 SDK 的参数做出相应的变动就实现了。&lt;/p&gt;
&lt;p&gt;此项实践已作为 ThoughtWorks 员工构建的知识体系 Ledge DevOps 对 ChatOps 这一模式的展示案例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://devops.phodal.com/pattern#chatops&quot; rel=&quot;noopener&quot; target=&quot;_blank&quot;&gt;Pattern#ChatOps from Ledge —— DevOps knowledge learning platform&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</summary><author><name>Lawrence</name></author></entry></feed>