<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Donald x Blog</title>
        <link>https://dndxdnd.com/blog</link>
        <description>It's all about Donald Mok's Blog</description>
        <lastBuildDate>Sun, 01 Mar 2026 13:47:39 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>CC BY-NC-SA 4.0 2025 © Donald Mok</copyright>
        <atom:link href="https://dndxdnd.com/blog.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[2025 年的一些 insights]]></title>
            <link>https://dndxdnd.com//blog/insights-of-2025</link>
            <guid>https://dndxdnd.com//blog/insights-of-2025</guid>
            <pubDate>Sun, 15 Feb 2026 05:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>过去一年 AI 发展迅速，真的有自己处于漩涡之中的感觉，感受着 AI 如何发展并逐步影响到整个世界。从年初的 Cursor，DeepSeek R1，再到 Claude Code 点燃了人们对 Agent 的热情，后面出的 Skills 更是添了一把火。年末爆火的 OpenClaw 也算是对过去一年的 Agent 能力的一个整合了。</p>
<p>有一些 insights 印象还是挺深刻的。</p>
<h2>品味的重要性</h2>
<p>品味一词，过去一年可以说是被说烂了，自从 Andre Kapathy 提出 Vibe Coding 以来，大家都在指挥 AI 干活，那么你和别人的差异化就在于你的品味，在于你如何使用 AI，人们也越来越认识到品味的重要性，当然也很多人一直发乔布斯对于品味的评价。还记得大学刚毕业找工作面试的时候，有面试官问到 “最近在编程方面有什么令自己很满意的进步？”
我：“最近审美提升了”
面试官：“审美和编程有什么太大关系？”
我：“审美是对一个人整体的影响，也会对你技术选型，代码风格有大的影响。”
其实我那时更多还是在摄影和电影拉片上稍微提升了些审美，不过现在回看，也误打误撞地殊途同归了。</p>
<h2>Under investment is riskier than overinvestment.</h2>
<p>这句话是说在 AI 发展浪潮中，很多大公司都不顾泡沫与否，都投入了比以往大得多的钱去发展 AI 。正如上面这句话，如果投入得太多了，最多也就钱打水漂，或者投资的数据中心和买的卡都还能用在提升其它业务效率上。但是投资得少，就很有可能在这场游戏中被踢出局了，跟不上节奏很有可能就会举步维艰。</p>
<h2>The bitter lesson</h2>
<p>在 Manus 被 Meta 收购期间，看到有人提起这篇 <a href="https://en.wikipedia.org/wiki/Bitter_lesson">The bitter lesson</a> 博客。其中这个观点让我感觉挺深刻，针对垂直领域优化的，最终都会被通用的所代替。就像 Manus 选择做通用的 Agent ，而不是其它专精的 Coding Agent 或者 Design Agent 之类的。一个通用的 Agent 它就应该可以写代码生成应用，就可以画图做设计。后面的 OpenClaw 其实就是有点这个影子了，一个强大的 Agent 作为核心，给它一个文件系统，它就能借助 Skills 不断地学习不断地进化。当然这个方向的最终形态就是所谓的 AGI 了。</p>
<h2>早用 AI 早受益</h2>
<p>对于 AI ，用得越多的人越能掌握工具的最佳实践，效率增益不断累积。这个也是我过去几年一开始没做好，然后就后知后觉的。虽然我也挺早去用AI 的，从 GitHub Copilot 开始公测就有在用，到后面的各种 AI 工具都有尽可能早地去使用。但是在工程上我就犯了这个错误，在早期 ChatGPT 的时候，也算是有很多 Low Hanging Fruits 的，比如很多提示词工程和所谓的套壳应用，之前会觉得这些东西会肯定会随着模型进步而被取代，现在也只是过渡阶段而已。但其实不然，这些也是工程的积累，如果一路做过来的话，对 LLM 的发展也会更深刻，没有什么是白费的。就像前面说的，用得越多越能掌握最佳实践，在其它领域也是如此，不能眼高手低。</p>
<h2>技术总是会更普惠的</h2>
<p>一项技术一直发展，它很有很能就会变得很亲民，会让更多人学会。比如从最开始的计算机到个人电脑，再到几乎人手一台的手机。比如编程领域，从写给机器读的字节码，到汇编，再到各种高级语言，再到现在 Vibe Coding 的自然语言。都是在不断平民化的过程。我自己也是这个的获益者，正是因为 JS 的发展，让进入这个行业的门槛降低，让我也能作为一个臭写前端的做了个程序员。现在的 Vibe Coding 也是在这样的进程中，进一步降低了编码，做产品的门槛，也就会有更多的人会尝试会入局。</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[Vibe Coding or Not]]></title>
            <link>https://dndxdnd.com//blog/vibe-coding</link>
            <guid>https://dndxdnd.com//blog/vibe-coding</guid>
            <pubDate>Sat, 20 Sep 2025 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>Vibe Coding 可以分为两种，一种是具体的，使用 AI 产品快速生成软件而不用考虑代码，第二种是范围广一点的，使用 Coding Agent 帮助写代码。第一种其实非常适合用于做一些 Prototype 或者 MVP ，方便快速验证想法以及向其他人表达你想要的效果，明确这个需求的话，Vibe Coding 是再好不过了。</p>
<p>我认为 AI 在这个阶段最好的定位就是 Copilot ，顾名思义就是副驾驶员，它不能完全替代人，只能帮助人做事情，但要细究的话，这里的 “人” 也是有区别的，AI 还是能替代一部分重复劳动的人。</p>
<p>我们这里的 Vibe Coding 就讨论使用 Coding Agent 帮忙写代码。 什么时候适合 Vibe Coding ，我会分成两种情况，写我不会的代码和写我会的代码，遇到我不了解的领域，我通常不会直接用 Coding Agent ，因为即使给出来方案，我也不确定这是不是最佳实践，因为 AI 给出的回复很取决于提问者的问题 prompt ，既然你都对这领域不熟悉，自然很难 review ，这种情况我就会用 Chat 先而不是直接 Coding Agent 。先通过 Chat 问这个领域的学习路径是怎样的，有什么最佳实践，先对这个领域有个大概的了解。（Bonus: 提问的时候可以附带上你的技术背景，AI 就会以你的角度来给出学习这领域知识的路径。）</p>
<p>AI 能提升人的下限而不是上限。</p>
<p>"一直在草稿里，感觉还是发出来吧"</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[歌手与艺术家]]></title>
            <link>https://dndxdnd.com//blog/khalil-and-david</link>
            <guid>https://dndxdnd.com//blog/khalil-and-david</guid>
            <pubDate>Sat, 17 May 2025 14:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>前阵子阴差阳错，可以留在东京一个月。在去到的第一周的某个工作日，我在某个咖啡店喝了口刚点的冰咖啡，听着店里咖啡机运作的声音和周围的各种闲聊，准备开始写代码。这时，突然收到朋友发来的一则消息：方大同去世了。一瞬间，耳边的各种声音似乎都被调小了音量，赶紧打开小红书搜索，发现是真的。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/khalil-and-david/khalil-and-fama.jpg" alt="khalil-and-fama"></p>
<p>我肯定算不上是方大同的粉丝，第一次有意识地知道他是因为初中的时候看农夫的 08 年演嘢会视频看到的他。那时候会觉得他作为农夫的嘉宾，画风太不一样了吧，斯斯文文的样子，把他们摆在一起总觉得有点出戏，看另一个嘉宾 MC jin 就很自然。然后就被种草了 <em>Love Song</em> 这首歌，后面又有听他的 <em>红豆</em> 和 <em>Nothing's gonna change my love for you</em> 还有和 C 君一起的 <em>吹挚友</em> 。高中的时候也有经常听 <em>危险世界</em> 这张专辑。以至于后来和朋友去唱 K 的时候，也就会这几首。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/khalil-and-david/useless-to-say-anymore.jpg" alt="useless-to-say-anymore"></p>
<p>去年开始，估计是因为陶喆在网络上又变火了还是怎样，大家对华语 R&#x26;B 的讨论又多了起来，顺带也会说到方大同。刷到很多这些帖子后，我就在 Spotify 听听他的其它歌。包括 <em>爱爱爱</em>，<em>特别的人</em>，<em>如果爱</em>，等等。刚好那段时间是我比较焦虑的时候，不断循环这首唱出大爱的 <em>爱爱爱</em> 真的对我有很大的帮助，让我觉得一切都会变好。然后也有去看他过往的一些综艺和采访，发现他这个人真的很纯粹，对音乐充满热爱且不断钻研其中，对朋友暖心且给周围的人带来快乐，这真的是我非常认同且向往的一个状态。这时我对他的印象就完全不一样了，感觉他就像是我身边的一个好朋友一样。以至于在知道他去世之后，再重看<a href="https://www.bilibili.com/video/BV1ZzyTYvEgq/">周扬对他的采访视频</a>的片头，他伴随着 <em>好不容易</em> 这首歌出镜那一瞬间，眼泪瞬间从眼睛涌出来。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/khalil-and-david/david-behind-muholland-drive.jpg" alt="avid-behind-muholland-drive"></p>
<p>年初的时候还有一个我很喜欢的导演去世了：<a href="https://en.wikipedia.org/wiki/David_Lynch">大卫林奇</a>。对林奇的喜欢是从 <a href="https://en.wikipedia.org/wiki/Twin_Peaks"><em>双峰</em></a> 开始，当时就觉得这一部三十年前的剧充满了魔力，真的就是把电视剧的上限抬高了几个等级，人物众多，还充斥着各种谋杀、出轨、超能力、梦境、美食、侦探和恐怖等等元素，一环扣一环的情节也让观众看了一集就停不下来。对林奇的喜爱在于他的作品总能构建一个世界，让人沉浸其中，这个世界往往与现实世界有着很多关联，反映意识对认识世界的影响。正如他喜欢的电影 <a href="https://en.wikipedia.org/wiki/The_Wizard_of_Oz"><em>绿野仙踪</em></a> 一样。</p>
<p>很多厉害的人都只是某一特定方面的专业水平很厉害，但是林奇让我看到了他厉害，不只是说他作为导演，能力很优秀。而是他本人对世界存在有独到的理解，这些都是他后面信奉<a href="https://en.wikipedia.org/wiki/Advaita_Vedanta">吠陀哲学</a>才逐渐有的，但是他早期的 <a href="https://en.wikipedia.org/wiki/Eraserhead"><em>橡皮头</em></a> 也反映了他的独特审美和想法。他首先对这世界有着自己的看法，然后就用画画来表达，当某一天他觉得如果能让画中的元素动起来，就能更容易表达他的想法，然后就拍起电影来了。他不只是作为导演很厉害，他也有绘画作品和音乐作品。这对我的影响就是让我经常会脑海里有个声音，让我记住，不要局限自己的范围，比如写前端代码，不应该只是熟悉某个框架，而应该是熟悉核心的 js 和逻辑。也不要只局限自己只是写前端的，更重要的应该是自己解决问题的能力。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/khalil-and-david/david-behind-blue-velvet.jpg" alt="david-behind-blue-velvet"></p>
<p>林奇不算是一个迷影的导演，虽然他也非常喜欢 <a href="https://en.wikipedia.org/wiki/Sunset_Boulevard_(film)"><em>日落大道</em></a>，<a href="https://en.wikipedia.org/wiki/8%C2%BD"><em>八部半</em></a> 和 <a href="https://en.wikipedia.org/wiki/Jacques_Tati">雅克塔蒂</a> 的电影。他的作品中往往会有一个破碎的人，对养育后代的恐惧，因自身残缺而遭到社会的戏弄，对表面美好的父权社会的讽刺，因性焦虑而走向自我毁灭，对美好幻想的破灭。这些都能在林奇的作品中找到。对他影响很深的一件事，就是他小时候与弟弟在外面马路玩耍的时候，看到一个全裸的女人从旁边走出来，身上还带着伤。很能体会到这一画面对小林奇的冲击，这一幕在 <a href="https://en.wikipedia.org/wiki/Blue_Velvet_(film)"><em>蓝丝绒</em></a> 中也有拍出来，其中的元素和情绪都充斥在林奇的作品中。也很可惜没能看到他一直想拍但没能拍成的 <em>罗尼火箭</em>。</p>
<p>之前以为他能拍这些作品出来是因为他的成长经历中就受到了很多创伤，但后来才了解他成长于一个中产家庭，成长环境还算优渥，开始画画也只是因为去朋友家玩耍时，朋友的爸爸是画家，然后他觉得画画就能谋生这件事情很酷而已。</p>
<p>以前看的一部电影 <a href="https://zh.wikipedia.org/zh-cn/%E7%A0%B4%E4%BA%8B%E5%85%92"><em>破事儿</em></a> 里面，其中一段 <em>大头阿慧</em>，邓丽欣饰演的角色非常喜欢陈百强，在某一天，电台广播中传来了陈百强去世的消息，她哭得非常伤心。那一幕的悲伤让我一直都很难忘，到了今年，知道大卫林奇和方大同的去世之后，那一幕画面里的人，就变成了我自己。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/khalil-and-david/thank-you-david.jpg" alt="thank-you-david"></p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[Apple 内存， AI 与读书无用论]]></title>
            <link>https://dndxdnd.com//blog/apple-memory-and-ai</link>
            <guid>https://dndxdnd.com//blog/apple-memory-and-ai</guid>
            <pubDate>Sun, 10 Nov 2024 14:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>前阵子 Apple 发布新的 M4 系 Mac 设备引起一片讨论，万年老梗：比金子还贵的 Apple 内存/储存。特别是，丐版的 Mac mini  M4（16+256）4k 左右的价格，选配到 32G 内存就要 +3k ，几乎是翻了一倍了。</p>
<p>至于为什么这么贵呢，除了商业策略外，我还了解到是因为 Apple Silicon 的内存（Memory）的架构稍微与平常的内存不太一样。在传统 PC 内存中，CPU 和 GPU 等独立组件都有各自专用的内存池，而 Apple 的“统一内存架构”（<a href="https://macpaw.com/how-to/unified-memory-mac">Unified Memory</a>）则不同，它允许所有处理单元访问单个共享的高带宽、低延迟内存池。那这样的话，数据不需要在多个存储区域之间复制，从而显著提高了速度和效率。</p>
<p>这就让我想到很多人在现在的 AI 热下，很多人鼓吹 AI 替代 xxx，有了 AI，不用学编程，不用学语言，不用学设计，不用学各种细分领域的知识了。这种思维就和以前的“读书无用论”一样，认为读书就是为了以后赚大钱，既然现在做自媒体不需要学历也能赚大钱，那为什么还要读书呢。</p>
<p>我一直以来的看法是，读书肯定是有用的，这里的“读书”分为广义的学习知识和狭义的接受学校教育。接受学校教育，在学校课程学到什么是其次的，老师能教的只是片面，更重要的是在学校这个平台中，能结识很多跟自己趣味相投或者性格迥异的人，且没有很多的利益关系，充满了各种可能性；学习知识，更体现在拓展自己的知识面，世界观和思考能力，思考能让人更清晰自己在这世界的定位，<a href="https://en.wikipedia.org/wiki/Know_thyself">更了解自己</a>以及周边发生的事。这些都不是钱能带来的。</p>
<p>通过读书/学习，你可以有你自己的独立思考，而不是消费经过别人的咀嚼过后的信息。程序越多层嵌套越消耗性能，数据线越长越损耗效率，中间商越多利润越少。AI 帮你翻译总比不上你一看原文就能理解表面的意思以及背后的梗，背后的上下文，背后的文化来得好。AI 帮你写代码，前提也是要你知道下一步该怎么做，怎么问。怎么问又取决于你的认知，你的想象力。毕竟运用自己的脑袋中的 “统一内存”才是最有效率的。</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[找到合适的位置很重要]]></title>
            <link>https://dndxdnd.com//blog/do-the-right-thing</link>
            <guid>https://dndxdnd.com//blog/do-the-right-thing</guid>
            <pubDate>Sun, 11 Aug 2024 22:34:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>最近在 X 上看到有国外网友分享做了个叫 <a href="https://tools.rotato.app/compress">Rotato Video Compressor</a> 的视频压缩的网页工具，似乎蛮火的。然后就惹来很多其他人的嘲讽，“本质上就是调 ffmpeg ”，“这不就是一行 ffmpeg 命令吗”。。。让我想起来 <a href="https://x.com/yihong0618">@yihong0618</a> 的 <a href="https://github.com/yihong0618/gitblog/issues/291">这玩意不是就__</a>。</p>
<p>首先这可能真的是个稍微简单的 wrapper ，但事实上就是大多数人都是喜欢开箱即用的。ffmpeg 当然是硬核强大的，但有多少人真的愿意去记它的众多繁杂的参数呢？Product 和 Tool 的区别就在于 Product 应该会更注重 UX，强大的底层功能是基础，但有好看好用的 UI 才会让更多人愿意使用。</p>
<p>即使在 Tools 这一层面，也是有分不同 level 的，就像前端工具 Vite ，在 <a href="https://2023.stateofjs.com/en-US/awards/">State of JS 2023 年报告</a>中可以看到，它在 Web 开发者中大获好评。跟 Rollup 和 Webpack 这些 Low level 的构建工具相比，Vite 也算是一个 Middle Level 的 Wrapper，底层是使用 Rollup 和 Esbuild ，但是它在整合工具以及 DX 上做得足够好，因此还是很受欢迎的。</p>
<p>还有类似的像 Web 富文本编辑器这边的 <a href="https://prosemirror.net/">PromiseMirror</a> ，它是一个很强大，扩展性很强的富文本编辑器框架，其晦涩的文档劝退了很多人，基于它的上层框架 <a href="https://tiptap.dev/">Tiptap</a> 因为相对更开箱即用而能被人接受。</p>
<p>找到合适的位置很重要。</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[介乎 2023 与 2024 的倦意]]></title>
            <link>https://dndxdnd.com//blog/lost-in-transition</link>
            <guid>https://dndxdnd.com//blog/lost-in-transition</guid>
            <pubDate>Tue, 02 Jan 2024 00:10:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><img src="https://cdn.donaldxdonald.xyz/blog/lost-in-transition/mla.webp" alt="mla"></p>
<p>2023 年一下子过去了，今年似乎过得特别快，可能是因为今年过得相对开心一点。</p>
<p>在 2023 的最后一刻，正在和 Florence 看 「漫长的季节」，这一集的结尾是沈墨在王阳缺席的电影院里独自一人看完「泰坦尼克号」后在电影院门口碰见大爷。这一集结束后，看了看时间，分针刚跨过 2024 年元旦，我们跟对方说 “新年快乐！”。</p>
<p>社交媒体上大家都开心地分享着自己的年终总结以及新年祝福，看到这些，就会想一个问题：对于跨越新的一年，大家真的那么开心吗？回想到刚看的剧里，脑海里就会浮现出 “歌舞升平之下的隐秘角落” 画面。时间或许真的会冲刷一切。</p>
<p>今年认识了很多人，有了很多新体验。很想总结点什么，但又不想太流水账式地罗列，便作罢。</p>
<p>新的一年的目标就一个：尽量记录即时的感受。</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[无因的呻吟]]></title>
            <link>https://dndxdnd.com//blog/moaning_without_a_cause</link>
            <guid>https://dndxdnd.com//blog/moaning_without_a_cause</guid>
            <pubDate>Thu, 31 Aug 2023 23:50:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>是的，夏天又要结束了。虽然在如今的广东里，炎热的天气远还没到要退去的时候。但或许还是受着学生时期的认知影响，过了暑假，就是另一个时间段了。</p>
<p>过两天似乎又要刮台风了，今天就挺凉爽的。</p>
<p>很想说很享受这种微风吹拂的感觉，但是刚和 Florence 聊了聊彼此父母的问题，心情又略有压抑。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/moaning_without_a_cause/a_brighter_summer_day.jpg" alt="A Brighter Summer Day"></p>
<p>我们这一代人似乎普遍都面临着这场景，在并不富裕的家庭中长大，到如今接收了很多不一样的信息，导致新旧观念互相碰撞，与父辈难以相处，难以沟通。再看回杨德昌导演的电影时，就有不少共鸣。刚好这个月看到 <a href="https://movie.douban.com/subject/1887385/">变态者电影指南</a> 里齐泽克说 <em>“焦虑的最终对象是一个活着的父亲”</em>，一切都想得通了。这里的 “父亲” 应该是更抽象的一个对象，对应的其实是 “男权” 社会中更高一阶级的压迫。比如 <a href="https://movie.douban.com/subject/1292329/">牯岭街少年杀人事件</a> 里小四父亲的焦虑就来源于这样的 “父亲”。当然，大多数人焦虑的来源就是直接的父辈。</p>
<p>人，在这世界中，又是那么的渺小，一生又如此的短暂，却又得在各种是非中消耗自己的光阴。所以还是得要多点无病呻吟，不然就得在永不停止转动的齿轮下度过一生，连停下思索片段的机会都没有。</p>
<p>前不久刚玩通关的 异度神剑 2 就有点这种意思，和<a href="https://zh.wikipedia.org/zh-cn/%E9%9B%B7%E5%88%A9%C2%B7%E5%8F%B2%E8%80%83%E7%89%B9">雷德利斯科特</a>的 <a href="https://movie.douban.com/subject/3771562/">普罗米修斯</a> 以及 <a href="https://movie.douban.com/subject/1291839/">银翼杀手</a> 等作品都有着相似的母题，<a href="https://zh.wikipedia.org/zh-cn/%E5%AD%98%E5%9C%A8%E4%B8%BB%E4%B9%89">存在主义</a>中的 “探索自己存在的意义”。异刃、人类以及仿生人都希望能找到自己的造物主，探寻自己被 “造” 出来的意义何在。</p>
<p>说回来，异度神剑 2 还是蛮好玩的，整体风格偏王道热血，OST 太上头了。战斗系统起初会觉得复杂，但是上手之后就非常好玩了。这游戏从去年年底才开始玩，一直到最近才通关，主要是拖延，留着最后一点剧情就先不玩了（发现蛮多人也有这种心理）。因此也算是把一个小坑给补完了。但是 巫师三 还在陶森特的各种支线中辗转。。</p>
<p>八月初，终于把 绝命毒师 五季看完了，算是有质量的剧吧，但不是自己很喜欢的那一卦，很多情节都略显拖沓，衍生剧 风骚律师 自然就更没有动力去追了。然后最近就开始看日剧 <a href="https://movie.douban.com/subject/4303624/">母亲</a> ，还没看完，一开始是 Florence 推荐一起看的，看了第一集之后才发现又是<a href="https://zh.wikipedia.org/wiki/%E5%9D%82%E5%85%83%E8%A3%95%E4%BA%8C">坂元裕二</a>编剧的，怪不得第一集的风格和 <a href="https://movie.douban.com/subject/35797709/">怪物</a> 那么像了。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/moaning_without_a_cause/the_shadow_play.jpg?" alt="The shadow play"></p>
<p>八月初还有一件事，就是娄烨的 <a href="https://movie.douban.com/subject/26728669/">风中有朵雨做的云</a> 日版（完整版）上日本流媒体了，这就代表着国内观众有资源看了。当天每个群都有人奔走相告，可见大家是等得有多辛苦啊。同时上线的还有风雨云的制作纪录片 <a href="https://movie.douban.com/subject/30346866/">梦的背后</a> 。完整版的风雨云比起院线版也就多那几分钟，但就是被剪掉的片段，把一些角色的细节都给带走了，甚至是 Edison 整个人。这片放在现在上映的话，估计票房还能更高，秦昊和张颂文都今非昔比了。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/moaning_without_a_cause/diabolique.jpg" alt="diabolique"></p>
<p>去年在电影院里看了<a href="https://movie.douban.com/celebrity/1068387/">克鲁佐</a>的 <a href="https://movie.douban.com/subject/1299932/">恐惧的代价</a> ，对其中的剪辑调度留有很深的印象，然后前阵子在家看了他的另一部代表作 <a href="https://movie.douban.com/subject/1293213/">恶魔</a> ，真的绝了，悬念铺垫和氛围设计都仍然经得住时间的考验，后悔当初没在电影院里看。</p>
<p>得知 <a href="https://movie.douban.com/subject/35694766/">火山挚恋</a> 会在国内上映之后就一直准备着去看，但是只有首映那场才有海报领，而那天又因为上班赶不上，太可惜了。影片的整体风格比传统的纪录片更偏大众一点，里面的一句话让我印象深刻 ”远离人类，才会更爱人类“。看完后才知道当年同期也有另一部关于这对火山学家夫妇的纪录片：赫尔佐格的 <a href="https://movie.douban.com/subject/35857538/">心火：写给火山夫妇的安魂曲</a>。</p>
<p>在电影院里度过的还有两部古天乐电影，<a href="https://movie.douban.com/subject/1308796/">柔道龙虎榜</a> 和 <a href="https://movie.douban.com/subject/26746801/">暗杀风暴</a>。柔道龙虎榜 是杜琪峰浪漫的一面，灯光、配乐还有在酒吧四拨人找主角三人团的那场戏的调度，都是体现得淋漓尽致。暗杀风暴 是今年看的第二部邱礼涛作品(似乎还有两部准备上)，相比上一部 扫毒 3 真的好很多了。。但是就一个败笔，古天乐的角色有点太明显了，一看是古天乐演的就知道他不是什么小角色了。</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[越回忆越模糊的七月]]></title>
            <link>https://dndxdnd.com//blog/hazy_july_2023</link>
            <guid>https://dndxdnd.com//blog/hazy_july_2023</guid>
            <pubDate>Sat, 05 Aug 2023 16:44:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><img src="https://cdn.donaldxdonald.xyz/blog/hazy_july_2023/wheel_of_fortune_and_fantasy.webp" alt="main"></p>
<h2>序</h2>
<p>​		现在是北京时间 2023 年 8 月 5 日下午 2 点，外面太阳很晒，但还不至于开空调，旁边放了一杯刚煮的咖啡（最近在家很少冲咖啡，买的豆子都放久了），空气中只剩 Florence 的键盘打字声、某个日语歌手的歌声和风扇摇头吹动的呼呼声，这是不是也是现在大家追求的白噪音呢。</p>
<p>​		最近想把自己每个月的生活都稍微记录一下，因为不回顾一下似乎会忘掉自己是怎么活的，也想把一些暂时的心里所想给记录下来，记忆总是越回忆越模糊的，每月回顾也不是真实的样子，只是想把在自己脑海里的印象给留一份快照罢了。</p>
<h2>炎炎夏日</h2>
<p>​		7 月，以前学生时期很喜欢，因为代表着暑假刚开始，但是工作以后，就只单纯觉得热而已，而且如果是出去玩的话，就到处人挤人了，大家都像按命令行事般地按时出游。</p>
<p>​		过去一个月里，最大的突破应该就是游泳了吧，之前一直只会憋一口气蛙泳，而现在稍微学会了点仰泳和抬头蛙，要是能快点学会换气的话那就更棒了。泳池是在租房的小区里，恰逢暑假，白天去就会有很多小孩子，但晚上去的话感觉少点意思，似乎游泳就该伴随着炽热的太阳一起，感受着水面上火辣的光照和水底的清凉相互冲撞，一周的工作烦恼就会消失。如果可以的话，真想一直仰漂在水面上晒太阳 XD。</p>
<h2>电影</h2>
<ul>
<li>
<p><a href="https://movie.douban.com/subject/30137576/"><strong>记忆</strong></a> 2021</p>
<p>这电影最开始只是听过，知道是来自泰国的阿彼察邦导演的新片，但没想着要去找来看，谁知道今年居然在国内上映了，便和 Florence 一起去看，排片真的少啊，得亏影迷们自发式吐槽推广 — “专治失眠，在电影院睡个好觉。”  中国大陆的票房居然成了 「记忆」的全球上映国家/地区中的票房第一。以往的 “纯正” 文艺片都是会让人看睡的，在电影院中睡醒还会有点愧疚感，但 「记忆」似乎就是想让观众一起睡觉，里面的各种环境音和白噪音都好睡得不得了，要不是那围绕的巨响，可能我也会睡着过去，Florence 在我旁边睡着还落枕了嘻嘻。</p>
</li>
<li>
<p><a href="https://movie.douban.com/subject/10535568/"><strong>阿黛尔的生活</strong></a> 2013</p>
<p>紧随的第二天，就和 Florence 在家看了这部电影，也是之前就听说但是一直没看的，来自法国的拉拉电影。可能是从娄烨的<a href="https://movie.douban.com/subject/3690289/">「春风沉醉的夜晚」</a>开始，就还挺能接受 LGBT 相关的作品了，也有 <a href="https://movie.douban.com/subject/26799731/">「请以你的名字呼唤我」</a>和<a href="https://movie.douban.com/subject/30257175/">「燃烧女子的肖像」</a>这些优秀的新作品。但是对于本片，虽说描述一个少女的爱情和成长经历，还蛮细腻的，但是其中的好几段性爱戏会让我觉得是不是有点太。。。男性视角了？看完之后一看导演果然是男的。（个人感觉里面阿黛尔的演技比蕾老师的好</p>
</li>
<li>
<p><a href="https://movie.douban.com/subject/1297880/"><strong>芙蓉镇</strong></a> 1987</p>
<p>去年买的碟，终于在今年看了。能够直面历史且批判极左思潮和政治运动，对人物的讽刺力度也是杠杠的。让我比较喜欢的是导演不是对宏大虚无的某个形象批判，而是着墨描绘被大环境影响的独立个体，这种手法更能引人思考，这或许不是某个特定时期的事，而是整个人类社会都会不断轮回的事情。个人感觉这里面，刘晓庆以及徐松子的演技都很棒，都能把角色该有的情绪和性格演绎出来，而姜文倒没有很大的亮点。看的第一部谢晋导演的作品，期待之后碟影发行的<a href="https://movie.douban.com/subject/1422283/">「高山下的花环」</a>。</p>
</li>
<li>
<p><a href="https://movie.douban.com/subject/35360296/"><strong>偶然与想象</strong></a>  2021</p>
<p>文本功力好强，三段短篇故事，各有精彩之处又能围绕着 ”偶然“ 和 ”想象“ 融为一个整体，就像短篇小说集那样，又有电影艺术独有的视听语言加持，比如第一篇故事最后的变焦转场。很喜欢这样的作品，上一部有类似感觉的是李沧东的<a href="https://movie.douban.com/subject/1303145/">「薄荷糖」</a>，说来也很巧，看李沧东的第一部作品是 <a href="https://movie.douban.com/subject/26842702/">「燃烧」</a>，看滨口龙介的第一部作品是 <a href="https://movie.douban.com/subject/35235502/">「驾驶我的车」</a>，这两部都获得极高的赞誉，但是我自己在第一次看的时候没有很 get 到，虽然我知道是好作品。但第二次看他们的作品 「薄荷糖」&#x26; 「偶然与想象」的时候，就能看懂一部分（不要说自己能看懂任何<del>作品</del>东西）而且很喜欢。感觉滨口龙介在未来十年会有更高的成就啊。</p>
</li>
<li>
<p><a href="https://movie.douban.com/subject/1830189/"><strong>黑楼孤魂</strong></a> 1989</p>
<p>这部是国产恐怖片，Florence 不敢看，就拉着同事一起看了（观影过程反而被同事吓到了，电影吓他，他吓我）本来预期蛮低的，想着国产 + 恐怖片不就冤魂报仇，最后是车祸醒来/精神病吗？（结果还真是）但它比较让我惊喜的是，在杜比立体声的加持下（据说是国内首部立体声电影），恐怖氛围很到位啊！枪麦与灵异的碰撞、纸钱和美元、戏中戏的元电影属性、组装麦克风的一系列剪辑，再加上 「查拉图斯特如是说」的变调还有鬼魂的第一视角镜头，观影体验很棒。当然这里的冤魂，背景也是来自于文革，里面的台词 “国家都疯了，何况人呢？”使得结尾的精神病臆想，显得更加讽刺，再看看影片的上映年份，似乎这恐怖片一点都不恐怖了，最恐怖的是人类本身而已。（似乎这部片子就是“中国特色”恐怖片的源头</p>
</li>
<li>
<p><a href="https://movie.douban.com/subject/4058939/"><strong>芭比</strong></a> 2023</p>
<p>看该片前期路透图，以为是单纯的那种 “惨遭真人化” 的电影，毕竟造型都很夸张，但去看了之后发现还可以，就电影本身来说的话，质量中规中矩，有点现代歌舞片的样子，但是人物塑造还是差点意思，作为一部商业片，里面好几条线，芭比线、肯线、母女线和美泰线，似乎想讲的东西挺多，但是最后又没哪个讲得比较好。作为 “女性电影” 来说的话，看过简坎皮恩的<a href="https://movie.douban.com/subject/1293818/">「钢琴课」</a>和<a href="https://movie.douban.com/subject/33437152/">「犬之力」</a>之后，就真的觉得本片质量不太行。但同样的作为商业片，它能让更多地人觉醒平权意识，可能也是因为这个原因，很多地方只能用很直白的方式来呈现了。现在网上也很多人对这部电影有争论，认为它加深了男女对立啥的，但其实它已经很温和了吧，里面有展示极端女权的世界（开始的 Barbie land），极端男权的世界（Ken 从现实世界回去后的 Kendom），很明显都是导演用夸张的手法去讽刺，更多地是引起观众的思考，去思考怎样的一个社会才是和谐的呢？是女性掌权还是男性掌权呢？又或者说引起矛盾的根本不是性别，而是。。。？而且如果将 Ken 看作是现实世界中的 “女性” 似乎也是 make sense 的。 不要尝试在一个作品中寻求答案，作品只是抛出问题，答案是很主观的，多去思考，多去交流，求同存异，这才是创作者们希望观众去做的事情。（买票时没留意看，买成了国语版，真的是救大命</p>
</li>
<li>
<p><a href="https://movie.douban.com/subject/1295834/"><strong>极度空间</strong></a> 1988</p>
<p>以前只看过约翰卡彭特的一个作品：<a href="https://movie.douban.com/subject/1296794/">「怪形」</a>，很喜欢的一部怪物片，自己看完后又安利其他人一起看了几遍。终于这次又想起看卡朋特的其他电影了。本片给我感觉是太硬设定了点，男主登场就是为了讲这个故事的样子。但它的配乐也和「怪形」的一样好带感啊，以外星人来讽刺消费主义，看清各种商品背后的意识形态问题，这不也很适用于现在的中国吗，里面长达 7 分钟的两位男主互殴也很雷人，第一次看这么长的打斗段落（在一个场景）也有人说是代表着 “唤醒另一个人的意识不是一件容易的事情”。（一个本人很喜欢的品牌名字也来源于本片：Obey</p>
</li>
<li>
<p><a href="https://movie.douban.com/subject/35593344/"><strong>奥本海默</strong></a> 2023</p>
<p>今年的 “芭本海默” 梗玩得很火热啊，源自两部看似截然不同的影片，但上映日期相同导致的反差感。也导致了似乎「奥本海默」似乎不太需要怎么宣发（跟着芭比就对了。本片是诺兰电影中文戏较多的一部，毕竟是传记片，改编自原著<a href="https://book.douban.com/subject/4254757/">《奥本海默传 : “原子弹之父”的美国悲剧》</a>，没有什么之前诺兰电影的高概念，个人还蛮喜欢的，上一部<a href="https://movie.douban.com/subject/30444960/">「信条」</a>看得我后半段直接在电影院里睡着了。。。其中比较想说的就是导演的功力，就比如原子弹爆炸那场戏，在大家都知道结果的情节中还能调动起观众的紧张情绪，真的很厉害。整部电影的表面观感会让我想起 <a href="https://movie.douban.com/subject/1292288/">「公民凯恩」</a>和<a href="https://movie.douban.com/subject/34461705/">「曼克」</a>，拍传记片就把这个人的经历拆开重组。卡司方面，可谓是大牌云集，基莲的表演很出色啊。看的是 35mm 胶片版，一些胶片的划痕已经右上角的换片记号，体验很棒，等内地上映后再看一遍 IMAX 版。</p>
</li>
</ul>
<h2>书</h2>
<p>​		7 月初的某个晚上，翻开了村上春树的<a href="https://book.douban.com/subject/30144098/">《且听风吟》</a>，据说是他的首部作品，看完之后就把脑海里几个关键词拼起来填入豆瓣的记录里 “非线性叙事的孤独感”。</p>
<p>​		一周过后，在书架上看到了之前买的陀思妥耶夫斯基的<a href="https://book.douban.com/subject/34990038/">《白夜》</a>，便读了起来。里面比较喜欢的一句话是 “我们在遭遇不幸时才会更强烈地感受到别人的不幸。” 这是我看的第一本陀思妥耶夫斯基，感觉里面的情感和诗意都倾泻出纸面来了，作为一个社恐来说，对“幻想家”真的感同身受，但又没他那么能说能想。啥时候也去看看圣彼得堡的夏夜吧。</p>
<p>​		又把目光扫视书架的时候，一个名字进入了我的视线，回想起了看王家卫<a href="https://movie.douban.com/subject/1291557/">「花样年华」</a>片尾致敬的一个作家：刘以鬯。之前 Florence 买了他的 <a href="https://book.douban.com/subject/35896343/">《椰风蕉雨》</a>，是一本 “南洋故事集” ，收录了作者在南洋时期所创作的中篇、短篇及微型小说。目前看完了前两篇中篇小说，《星加坡故事》和《蕉风椰雨》，个人更喜欢第一个故事，也和 Florence 聊过，第一个故事更像是把镜头对准了某个时期的某个地方的一组故事那样，而第二个故事则更架空一点，虽还没看完其他的小说，但已经很喜欢刘以鬯的笔风，充满细节和意识流的叙事，能把气味和感觉带给读者。</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[2022 -> 2023: Don't worry. Be Happy.]]></title>
            <link>https://dndxdnd.com//blog/2022-wrapped</link>
            <guid>https://dndxdnd.com//blog/2022-wrapped</guid>
            <pubDate>Sat, 21 Jan 2023 21:28:00 GMT</pubDate>
            <content:encoded><![CDATA[<p>现在是 2023 年 1 月 21 日，中国春节的年三十，回顾一下自己的 2022 年。</p>
<p>2022 是不平凡的一年：世界格局有点动荡；全球互联网公司 layoff 不断，今天就看到 Google 也加入了这个队伍；中国也发生了很多事情，也更让我深刻体会到了现代社会人与人之间的相处是多么地困难。</p>
<p>今年心态不太一样，没怎么去拍照片，因此不太好像去年一样以时间顺序来回顾，这次就以关键词来吧。</p>
<h2>工作/技术</h2>
<p><img src="https://cdn.donaldxdonald.xyz/blog/2022-wrapped/hello-world-editor.png" alt="hello-word-editor"></p>
<p>今年又回到广州工作了，与项目组小伙伴们会合，还是很不错的（年后又要去清远工作了，唉）。今年最主要的任务就是做编辑器了，虽然不是从 0 搭建 WYSIWYG 编辑器，但也是学到了很多。从 <a href="https://quilljs.com">Quill</a> 的使用到调研 Web 富文本编辑器，再到基于 <a href="https://slatejs.org">Slate</a> 与 Angular 搭建编辑器。自己一个人开发能学到<a href="https://donaldxdonald.xyz/blog/webrteevo">蛮多东西</a>，不过还是很可惜缺少能交流相关工作的同事，因为很多时候都是有交流才会有更大的进步。</p>
<p>还有就是对 Angular 真的是又爱又恨啊，爱的是开发体验真的挺舒服的，没太多恶心的东西，用 <a href="https://rxjs.dev/">RxJS</a> 也是越用越上手了，恨就是生态实在是不太行，很多时候都没有现成的轮子，都是要自己去写，然后要去处理很多 Dirty Work （这时候产品经理就问怎么还没好）。也得益于此，今年也更多地参与开源了，会给其他一些项目提 Issue 和 发 PR，因为很多时候在工作上也会遇到这些问题。</p>
<p>2022 年比较可惜的是还没有太深入了解协同编辑，看了一些文章了解到 <a href="https://en.wikipedia.org/wiki/Operational_transformation">OT</a> 和 <a href="https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type">CRDT</a> ，2023 年捣鼓捣鼓 <a href="https://github.com/yjs/yjs">Yjs</a> 看看。</p>
<p>前端真的太好玩了，新的一年会多尝试一下 React 和 （Solid / Svelte），横向对比一下不同框架如何解决同样的问题。</p>
<h2>精神食粮</h2>
<p><img src="https://cdn.donaldxdonald.xyz/blog/2022-wrapped/ticket.jpg" alt="ticket"></p>
<p>又因为在广州，今年也在一些电影节看了经典作品，现在在中国大陆，电影院有放好作品的还是得应看尽看，因为一部作品在大银幕看和在显示器上看，是完全两种体验，更不用说用手机来看。说到这个，今年对电影院的想法会有点改变，原本会比较认同“一部电影不在电影院上映就不算一部电影，因为电影是包括了制作、发行和观看这些流程的”这种说法。后来听了波米说的话，还是挺认同的，电影院的说法是学院派的说法，学院派的通常都很注重电影院，老马、斯皮尔伯格和科波拉等这些老导演就是这一派，但是你不可以否定以 Netflix 为代表的流媒体平台，这一派在以往是碟片，如今是流媒体，电影院其实也算是一种政治工具，你能看什么取决于有什么可放，而碟片和流媒体这些物理介质就能公平自由，让更多的人来享受作品。毕竟没有碟片，就没有现在的昆汀。</p>
<p>今年在电影院看了这些电影（按时间顺序）：</p>
<ul>
<li>「花束般的恋爱」</li>
<li>「纽约的一个雨天」</li>
<li>「潜行者」- 2022 俄罗斯珍藏电影展</li>
<li>「新蝙蝠侠」- 珠三角观影团</li>
<li>「胭脂扣」- 2022 香港电影展映</li>
<li>「隐入尘烟」</li>
<li>「柏林苍穹下」- 2022 直到世界尽头 · 维姆文德斯展</li>
<li>「七人乐队」</li>
<li>「恐惧的代价」- 2022 广州 CGV 法国电影展</li>
<li>「还是觉得你最好」/ 「饭气攻心」</li>
<li>「阿凡达：水之道」</li>
</ul>
<p>下半年开始也陆续地买一些自己喜欢的电影碟收藏，主要是买 CC 和碟影的，其他的只有比较好看的套装会入手。</p>
<p><strong>书：</strong> 今年看的书不多，其中看了两本关于大卫林奇的，大卫林奇真的像他电影一样，太 dreamy 了，老爷子现在年纪也很大了，很有可能没有下一部作品了，为数不多的作品真的是看一部少一部。</p>
<p><strong>剧：</strong> 今年和 Florence 一起会在趁着吃饭的时候看剧，补完了「老友记」全 10 季、「东京爱情故事」、「悠长假期」、「四重奏」和台版「恶作剧之吻」等剧，还是很棒的。</p>
<p><strong>游戏：</strong> 上半年主要是在《巫师三》和《风来之国》，下半年（到现在）就是《异度之刃2》。</p>
<p><strong>其他：</strong> 在友谊剧院看了《恋爱的犀牛》；去看了农夫的 show。</p>
<h2>作为人</h2>
<p><img src="https://cdn.donaldxdonald.xyz/blog/2022-wrapped/vie.jpg" alt="vie"></p>
<p>2022 年的各种大事件都带来了很多影响，在网络上看的话，却是大家无止境的争吵。人人都不愿意多说一句话，都是在以捍卫自己的认知的方式来与他人沟通，大家都不愿意理解对方。其实可以理解为人本来就是容易受到他人影响的，而如今几乎人人都能上互联网，自然很容易有人会去做这些煽动人心，挑起情绪的事情，导致的结果就是平民伤害平民。</p>
<p>感谢 Florence，她的性别意识方面很敏感，因此也会使我有意识地去思考日常生活中的性别歧视问题，有很多日常生活中大家习以为常的话语和动作，只要你细究，就很容易发现其中的问题。而且也想通了，生孩子好像也不是很必要，甚至结婚，也不一定要结，这只是一个仪式，也不是说结了婚就会更相爱，该是好的，一直都能好。当然，这只是现阶段我和 Florence 的想法，下个阶段可能也会有新的想法。主要就是说：女的不一定就要生孩子，男的不一定就要阳刚（<a href="https://zh.m.wikipedia.org/zh-hans/%E6%9C%89%E6%AF%92%E5%AE%B3%E7%9A%84%E7%94%B7%E5%AD%90%E6%B0%94%E6%A6%82">有毒的男子气概</a>）。还是要回归到以人的角度来看问题：作为一个人，这样做，真的好吗？</p>
<p>再抽象一点来说，其实就是希望人与人之间能多一点理解，大家不必为不同观点而争；上一辈的人可以理解这一辈的人；不同国家、种族、性别、年龄的人都能互相理解，因为大家都是<strong>人</strong>。</p>
<p>就是这一年看到太多这种人们之间的争斗，以及有人拿各种不幸事件来玩梗，真的挺难受的，不是说那些人和我有什么关系，只是稍微换位思考一下，真的很难说出话来。这一年，在社交媒体上发动态的数量少了，发也没什么文字，还有就是前面说的，没怎么拍照，2022 年真的是挺难受的一年，不过好在有 Florence 在身边，互相支持。我不能说自己能做得有多好，只是能给自己身边的人提出不一样的观点也好。</p>
<h2>2023</h2>
<p><img src="https://cdn.donaldxdonald.xyz/blog/2022-wrapped/long-vacation.jpg" alt="long-vacation"></p>
<p>2023 年希望自己能更深入地参与开源，不强求造轮子，多给其他项目投 PR 也是不错的，能从中学习蛮多东西。</p>
<p>看完「悠长假期」后，一直对里面广告牌的广告语印象深刻："Don't worry. Be Happy." 多关注自己身边的人，珍惜眼前人 : )</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[Web 富文本编辑器演进]]></title>
            <link>https://dndxdnd.com//blog/webrteevo</link>
            <guid>https://dndxdnd.com//blog/webrteevo</guid>
            <pubDate>Sun, 03 Jul 2022 21:30:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>什么是富文本？</h2>
<p>富文本 Rich Text ，相对于纯文本 Plain Text 来说，就是有通用的格式选项（比如加粗和斜体）来格式化的文本。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/WebRTEEvo/rich-text.png" alt="rich-text"></p>
<h2>Web + 富文本编辑器</h2>
<p><img src="https://cdn.donaldxdonald.xyz/blog/WebRTEEvo/vscode-on-watch.jpg" alt="vs-code"></p>
<p>众所周知，Web 是目前来说最通用的平台，不管是电脑、手机、游戏机、汽车还是 Kindle ，只要有浏览器就能打开网站。加上 WebAssembly 和 Electron 等技术和工具的支持，在 Web 上执行复杂操作的超级应用（Figma, VS Code）也在陆续登场。面对 “一次编写，随处运行” 的诱惑下，许多传统桌面应用也都在逐步地将阵地拓展到 Web 端，富文本编辑器就是其中之一。</p>
<h2>富文本编辑器 = 坑？</h2>
<p>在 Web 前端业界内，富文本编辑器是公认的天坑。Web 开发好处是<strong>跨平台</strong>，但问题也是因为跨平台带来的<strong>兼容</strong>问题。跨平台只是能在各个平台上跑起来，但是跑起来怎么样就是天坑所在了。</p>
<p>在 Web 开发编辑器首先要处理好 <strong>焦点</strong>、<strong>光标选区</strong>、<strong>撤回栈</strong>、从外部<strong>粘贴</strong>内容解析等等的坑，然后考虑兼容<strong>不同浏览器</strong>（Chrome、Firefox、Safari 等）...... 处理好了最基本的英文输入后，以中文使用者为代表的用户来要求支持 <strong>IME 组合输入</strong>了...... 解决了 IME 输入后，<strong>RTL 语言</strong>（希伯来语、阿拉伯语）用户来了...... <strong>手机</strong>用户来了......   <strong>协同编辑</strong>用户来了......</p>
<p>借用知乎上的一段话，总结起来就是：</p>
<blockquote>
<p>落后的生产力与人们日益增长的需求之间的矛盾。</p>
</blockquote>
<p><strong>落后生产力：</strong></p>
<ol>
<li>Web 相关标准推进缓慢</li>
<li>浏览器厂商对于相同操作或者场景实现方式的不同，导致兼容性的问题</li>
<li>使用 HTML DOM 描述富文本内容有太多不可控制的情况</li>
</ol>
<p><strong>日益增长的需求：</strong></p>
<ol>
<li>不确定的交互意图，比如按 Delete 键，不同的焦点位置有不同的情况需要考虑</li>
<li>内容输入的多样性，比如有：打字键入、粘贴、拖拽等，每个处理起来都相当复杂</li>
<li>大量需要拦截阻止和代理的浏览器默认行为，保证数据的完整性和正确性</li>
<li>用户对于编辑器的使用要求越来越高，比如：合并单元格、列表多级嵌套、协同编辑、版本对比、段落标注，大家都认为这是基本需求，其实这里面的技术难度是超出大家的想象的。</li>
</ol>
<h2>编辑器技术阶段一览</h2>
<table>
<thead>
<tr>
<th align="left">阶段</th>
<th align="left">描述</th>
<th align="left">典型产品</th>
</tr>
</thead>
<tbody>
<tr>
<td align="left">L0</td>
<td align="left"><div><div> 强依赖浏览器 DOM API ( contenteditable, document.execCommand ) </div><div>视图即数据</div></div></td>
<td align="left"><div><div>UEditor</div><div>TinyMCE</div><div>CKEditor 1 ~ 4</div></div></td>
</tr>
<tr>
<td align="left">L1</td>
<td align="left"><div><div> 仍然基于 contenteditable </div><div>抛弃 document.execCommand 操作内容，改为自己实现</div><div>有抽象的数据模型来描述富文本编辑器的内容与状态</div></div></td>
<td align="left"><div><div>Quill</div><div>Slate</div><div>CKEditor 5</div><div>Draft.js</div><div>ProseMirror</div><div>wangEditor v5</div></div></td>
</tr>
<tr>
<td align="left">L2</td>
<td align="left"><div><div> 抛弃 contenteditable ，改为自己实现 </div><div>抛弃 document.execCommand 操作内容，改为自己实现</div><div>自己实现排版引擎</div></div></td>
<td align="left"><div><div>Google Docs</div></div></td>
</tr>
</tbody>
</table>
<h2>L0</h2>
<p>L0 阶段的编辑器主要是是依赖了浏览器原生的 <code>contenteditable</code> API 来实现编辑，以 <code>document.execCommand</code> API 来实现多种操作，比如加粗、绑定链接、复制粘贴等等。</p>
<p>优势：</p>
<ol>
<li>技术门槛低。只要<strong>使用</strong>了以上两个 API ，就可以让网页具备编辑能力。</li>
<li>基于浏览器原生编辑能力，输入非常流畅。</li>
<li>没有令人头疼的组合输入问题。</li>
</ol>
<p>劣势：</p>
<ol>
<li>相同操作在不同浏览器上会有不同实现。</li>
<li>输出富文本内容是 HTML ，不利于管理数据。</li>
<li>扩展复杂的富文本很困难。</li>
<li>没有办法实现协同编辑。</li>
</ol>
<h2>L1</h2>
<p>目前大多数编辑器框架都处于 L1 阶段，比较有代表性的就是 Quill、Slate、ProseMirror 和 Draft.js。它们主要有两个明显的特点：</p>
<ol>
<li>仍然依赖于 <code>contenteditable</code> API 用于内容编辑，但不再依赖 <code>document.execCommand</code> API 来操作内容，改为自己实现。</li>
<li>有抽象的数据模型来描述富文本编辑器的内容与状态。</li>
</ol>
<h3>2012 - Quill</h3>
<p><img src="https://cdn.donaldxdonald.xyz/blog/WebRTEEvo/quill.png" alt="quill"></p>
<blockquote>
<p>Quill 是 API 驱动的富文本编辑器框架，提供开箱即用的编辑器体验。</p>
</blockquote>
<p>Quill 的作者 <strong>Jason Chen</strong> 是一名华裔，Quill 其实算是 Jason 的一个 Side Project，当初是创办了一家公司，专门做类似 Google Docs 的协作编辑器，因此自己写了 Quill 出来使用。</p>
<p>Quill 对 DOM Tree 以及数据的修改操作进行了抽象，从而实际使用时不需要我们对 DOM 操作，而是通过 Quill 的 API 进行操作，对应的关系如下：</p>
<p>Editor Document  ====> <strong>Parchment</strong></p>
<p>DOM Node ====> <strong>Blot</strong></p>
<p>有了这层抽象后，原本的对 DOM 的直接操作就变成了对 Blot 的操作，这些操作用 <strong>Delta</strong> 来表示。Quill 在 Delta 中抛弃了 DOM 的节点树的层次，因此完全看不出包裹文字的标签和节点关系，只有一个扁平化后的数组 <code>ops</code>。</p>
<p><strong>数据模型：</strong></p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/WebRTEEvo/example-check-this-out.png" alt="img"></p>
<pre><code class="language-json">{
  "ops": [
    {
      "attributes": {
        "bold": true
      },
      "insert": "Check"
    },
    {
      "insert": " "
    },
    {
      "attributes": {
        "link": "https://donaldxdonald.xyz/"
      },
      "insert": "this"
    },
    {
      "insert": " out ~"
    }
  ]
}
</code></pre>
<p>Delta 的扁平化结构其实是协同编辑中的 OT 模型的一种实现，因此 Quill 也是生来就是为了协同编辑而设计的。扁平化带来的好处是对性能提升有帮助，弊端则是在表示一些复杂的嵌套内容时会比较吃力。</p>
<p>Quill 的特点是：</p>
<ol>
<li>依赖浏览器原生编辑能力 <code>contentEditable</code> (L1)</li>
<li>引入了一层抽象的数据结构用以描述内容以及行为</li>
<li>对协同编辑支持良好</li>
<li>输出结构可以是字符串也可以是 Delta （JSON），但 Delta 作为数据模型可读性不高</li>
</ol>
<h3>2015 - ProseMirror</h3>
<p><img src="https://cdn.donaldxdonald.xyz/blog/WebRTEEvo/prosemirror.png" alt="prosemirror"></p>
<p><strong>Marijn</strong> 是 CodeMirror 编辑器和 acorn 解析器的作者，前者已经在 Chrome 和 Firefox 自带的调试工具里使用了，后者则是 babel 的依赖。为了有更多的收入，Marijn 开始了新的项目，ProseMirror 。</p>
<p>Marijn 觉得当时市面上的开源编辑器都没有一个采用他认为是理想的方法，且很多还是使用着旧的范式来设计，使用着 <code>contentEditable</code>来实现。这样子开发者对文档内容能控制的范围就很小，而这又是很容易被用户和浏览器修改的。虽然 ProseMirror 还是基于 <code>contentEditable</code> 实现编辑功能了，毕竟自己重新实现一套选区逻辑太麻烦了。</p>
<p>ProseMirror 是有 schema （范式）的，所以定义好了 schema 以后 ProseMirror 可以替你实现自动化 parser 。框架层面定义好了新引入一个 Node 需要什么属性和方法，比如 <code>nodeFromJSON</code> 方法做结构到 json 的转换，<code>toDOM</code> 方法定义如何将结构数据转换为 DOM（有点类似 JSX ）。ProseMirror 就在中间这一层做了 json 数据到 DOM 的变更管理。</p>
<p><strong>数据模型：</strong></p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/WebRTEEvo/example-check-this-out.png" alt="img"></p>
<pre><code class="language-json">{
  "type": "paragraph",
  "content": [
    {
      "type": "text",
      "marks": [
        {
          "type": "strong"
        }
      ],
      "text": "Check"
    },
    {
      "type": "text",
      "text": " "
    },
    {
      "type": "text",
      "marks": [
        {
          "type": "link",
          "attrs": {
            "href": "https://donaldxdonald.xyz",
            "title": ""
          }
        }
      ],
      "text": "this"
    },
    {
      "type": "text",
      "text": " out ~"
    }
  ]
}
</code></pre>
<p>ProseMirror 的特点是：</p>
<ol>
<li>依赖浏览器原生编辑能力 <code>contentEditable</code> (L1)</li>
<li>更抽象的 json 文档模型。ProseMirror 只定义了可配置的模型框架，具体的结构可以在实际开发的时候自定义。</li>
<li>嵌套的树形结构。能支持复杂结构的内容。</li>
<li>对协同编辑的良好支持。从诞生之初，ProseMirror 就开始关注着协同编辑的支持。</li>
<li>1.0 后加入了不可变数据，使得编辑器的数据处理有了一个完整的数据流，稳定且可控。</li>
</ol>
<h3>2016.02 - Draft.js</h3>
<p><img src="https://cdn.donaldxdonald.xyz/blog/WebRTEEvo/draftjs.png" alt="draftjs"></p>
<p>彼时还叫 Facebook 的 Meta 开源了 Draft.js ，既然都是同一个公司的，Draft.js 就在视图层方面使用了 React 渲染 UI 。这也是第一个 React + 编辑器结合的案例，React 的流行也让使用者可以快速地直接基于 Draft.js 进行二次开发。</p>
<p>Draft.js 不仅外表有 React ，内里也是有很深的 React 的影子，类似 Redux 等状态管理的 <code>EditorState</code> 和 <code>ContentState</code> ，在数据层使用 <code>Immutable</code>等特性。JS 对象的属性是可以随意赋值的，也就是 mutable 可变的。而相对地，不可变的数据类型不允许随意赋值，每次通过 Immutable API 的修改，都会生成一个新的引用。</p>
<p>Draft.js 的特点是：</p>
<ol>
<li>依赖浏览器原生编辑能力 <code>contentEditable</code> (L1)</li>
<li>用 React 来实现视图层</li>
<li>内容的存储和渲染逻辑分离</li>
<li>使用 Immutable 数据</li>
<li>虽然也抽象了基于 json 的数据模型，但是对于嵌套数据的支持有些弱</li>
</ol>
<h3>2016.06 - Slate</h3>
<p><img src="https://cdn.donaldxdonald.xyz/blog/WebRTEEvo/slate.png" alt="slate"></p>
<p>此时市面上已有许多编辑器轮子在卷了，但是 Ian Storm Taylor 在开发自己的 CMS 产品时，仍然觉得没有一个好用的编辑器，他觉得这些编辑器如果只是用来做一些简单的产品的话，是挺不错的了，但是如果想要开发像 Medium 、Google Docs 和 Dropbox Paper 这些大型应用的话，就太难太难了，于是就有了 Slate。</p>
<p>Slate 同样的是一个编辑器框架，而不是开箱即用的编辑器工具。作为晚辈的 Slate 集合了前辈们的许多优点，从 Draft.js 那里参考的 Immutable 数据、插件机制和 React 视图层，又从 ProseMirror 借鉴了嵌套数据结构和 Schema 约束规则。整合了许多编辑器框架的核心特性，又加上框架理念先进和作者对架构的追求（时至今日 2022 ，仍然是 beta 的状态，还没到 1.0），Slate 在社区上还是比较受欢迎的。</p>
<p><strong>数据模型：</strong></p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/WebRTEEvo/example-slate.png" alt="example-slate"></p>
<pre><code class="language-json">{
  "object": "block",
  "type": "paragraph",
  "nodes": [
    {
      "object": "text",
      "text": "This is editable "
    },
    {
      "object": "text",
      "text": "rich",
      "marks": [{ "type": "bold" }]
    },
    {
      "object": "text",
      "text": " text, "
    },
    {
      "object": "text",
      "text": "much",
      "marks": [{ "type": "italic" }]
    },
    {
      "object": "text",
      "text": " better than a "
    },
    {
      "object": "text",
      "text": "&#x3C;textarea>",
      "marks": [{ "type": "code" }]
    },
    {
      "object": "text",
      "text": "!"
    }
  ]
}
</code></pre>
<p>此时 Slate 的特点是：</p>
<ol>
<li>依赖浏览器原生编辑能力 <code>contentEditable</code> (L1)</li>
<li>用 React 来实现视图层</li>
<li>支持嵌套的 json 数据结构</li>
<li>Immutable 数据</li>
<li>插件机制为核心</li>
<li>有约束数据的 Schema</li>
</ol>
<h3>2019 - Slate 0.50+</h3>
<p>Slate 在架构上进行了一个大更新，作者称 “整个框架都从头开始重新考虑了”，主要更新的点为：</p>
<ol>
<li>将底层逻辑抽离出来 Slate Core ，与视图层分离</li>
<li>用 TypeScript 重写</li>
<li>简化插件机制，插件不再与渲染逻辑耦合</li>
<li>用简单的 json 对象替换 Immutable.js</li>
<li>自有概念和一些 Commands 更精简更抽象，改名为 Transforms</li>
</ol>
<p><strong>数据模型：</strong></p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/WebRTEEvo/example-slate.png" alt="example-slate"></p>
<pre><code class="language-json">{
  "type": "paragraph",
  "children": [
    { "text": "This is editable " },
    { "text": "rich", "bold": true },
    { "text": " text, " },
    { "text": "much", "italic": true },
    { "text": " better than a " },
    { "text": "&#x3C;textarea>", "code": true },
    { "text": "!" }
  ]
}
</code></pre>
<p>这时候 Slate 的特点是：</p>
<ol>
<li>依赖浏览器原生编辑能力 <code>contentEditable</code> (L1)</li>
<li>非常简洁的支持嵌套的数据模型</li>
<li>整体架构采用纯函数 + 接口的方式，思路和代码都非常简洁</li>
<li>插件机制支持开发强大的功能</li>
<li>整体的设计理念与 DOM 很像</li>
</ol>
<h3>2022 - Lexical</h3>
<p><img src="https://cdn.donaldxdonald.xyz/blog/WebRTEEvo/lexical.png" alt="lexical"></p>
<p>由于 Draft.js 在以往兼容浏览器方面做了很多的脏活，且这些是现在不太必要的，同时为了改善开发者体验（很多开发者吐槽 Draft.js 不好用），Meta 又开源了一款新的编辑器框架 Lexical ，旨在替换 Draft.js。</p>
<p>目前看来暂时没有什么特别创新的概念，主要也还是吸收了其他编辑器框架的优点：</p>
<ol>
<li>依赖浏览器原生编辑能力 <code>contentEditable</code> (L1)</li>
<li>保留了 Draft.js 中的一些概念（EditorState）</li>
<li>不与 React 绑定了，可以用各种框架实现视图层</li>
<li>整个框架挺轻便的，几乎没有什么其他的依赖</li>
</ol>
<h2>L2</h2>
<p>前面都有说到，凭借浏览器的<code>contenteditable</code> API，可以快速地开发出一款编辑器出来，但是各种兼容问题太多了，别看现在大多数编辑器框架还在 L1 ，其实早在 2010 年，财力雄厚的 Google 就已经开始抛弃浏览器的 <code>contenteditable</code> 了，新版 Google Docs 基于 Canvas 自己研发光标系统、文本布局系统和字体解析，因此除了自己的抽象数据结构外，连编辑操作都是自己实现了，编辑的呈现效果一致了，那协同编辑就自然水到渠成了。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/WebRTEEvo/google-docs.png" alt="google-docs"></p>
<p>当然，Google Docs 的核心技术没有开源，这种拼钱的工作还是得牢牢掌握在自己手中，国内的腾讯文档和 WPS 等编辑器也算是 L2 的，不一定是用 Canvas ，但思路都是自己实现编辑布局功能，弃用浏览器的 <code>contenteditable</code> 。</p>
<h2>总结</h2>
<p>contenteditable is terrible, 但是编辑器已经最小化了对它的使用，比之更为严峻的是，操作系统、浏览器、输入法相互组合形成的紊乱生态 —— 一个编辑器无法控制的，但产品又期望在上面开出繁花的生态。所以才说，Web 富文本编辑器是前端的天坑之一。</p>
<h2>参考链接</h2>
<ol>
<li><a href="https://www.zhihu.com/question/38699645">为什么都说富文本编辑器是天坑? - 知乎</a></li>
<li><a href="https://www.zhihu.com/question/26739121">有多大比例的前端工程师，能在合理的时间内独立开发出一个足以供商业网站使用的文本编辑器? - 知乎</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/268366406">开源富文本编辑器技术的演进（2020 1024）</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/123341288">ContentEditable困境与破局</a></li>
<li><a href="https://juejin.cn/post/6844903504478208007">Slate.js - 革命性的富文本编辑框架 - 掘金</a></li>
<li><a href="https://www.youtube.com/watch?v=WLo3pfOOFj0">Jason Chen: Building Editors in the Browser | JSConf.ar 2014</a></li>
<li><a href="https://www.youtube.com/watch?v=EEF2DlOUkag">Marijn Haverbeke: Salvaging contentEditable: Building a Robust WYSIWYG Editor | JSConf EU 2015</a></li>
<li><a href="https://marijnhaverbeke.nl/blog/prosemirror.html">ProseMirror</a></li>
<li><a href="https://news.ycombinator.com/item?id=31019778">Facebook open sources Lexical, an extensible text editor library - Hacker News</a></li>
<li><a href="https://www.oschina.net/translate/why-contenteditable-is-terrible">为什么 ContentEditable 很恐怖 - OSCHINA - 中文开源技术交流社区</a></li>
<li><a href="https://www.zhihu.com/question/459251463/answer/1890325108">如何看待 Google Docs 将从 HTML 迁移到基于 Canvas 渲染? - 知乎</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/226002936">从流行的编辑器架构聊聊 Web 富文本编辑器的困境</a></li>
</ol>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[用树莓派来做旁路由]]></title>
            <link>https://dndxdnd.com//blog/rpi4bypassing</link>
            <guid>https://dndxdnd.com//blog/rpi4bypassing</guid>
            <pubDate>Sat, 11 Jun 2022 14:16:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>前言</h2>
<p>自从买了树莓派以来，基本上都是用来做旁路由用了，装过一下 Jellyfin 用作影音管理，但是又感觉太丑了点，感觉折腾下来还不如直接用移动硬盘（</p>
<p>主要是用 docker 安装 OpenWrt 使用，科学上网之余解锁下网易云（虽然现在不太经常用网易云了）。由于经常性重装树莓派 / OpenWrt，就打算写文章整理一下过程，免得过一段时间又到处找资料。</p>
<h2>环境（参考）</h2>
<p>硬件：树莓派 4b</p>
<p>系统：<a href="https://github.com/openfans-community-offical/Debian-Pi-Aarch64">Debian-Pi-Aarch64</a></p>
<p>OpenWrt 镜像：<code>registry.cn-shanghai.aliyuncs.com/suling/openwrt:rpi4</code></p>
<h2>步骤</h2>
<blockquote>
<p>由于本人用小米路由器，默认本地 ip 是 <code>192.168.31.x</code> ，因此配置都以这个为例子。</p>
</blockquote>
<h3>开启网卡混杂模式</h3>
<p>根据实际设备网卡替换命令中的网卡名 <code>eth0</code></p>
<pre><code class="language-bash">$ sudo ip link set eth0 promisc on
</code></pre>
<h3>创建 macvlan 网络</h3>
<pre><code class="language-bash">$ docker network create -d macvlan --subnet=192.168.31.0/24 --gateway=192.168.31.1 -o parent=eth0 macnet
</code></pre>
<p>可以通过 <code>docker network ls</code> 查看网络是否创建成功。</p>
<h3>创建 docker 容器 （docker-compose）</h3>
<p>为了方便，就使用 docker-compose 写好配置，以便下次使用，也将容器内的 network 用 volume 的方式配置，方便管理。</p>
<pre><code class="language-yaml">version: "3"
services:
  openwrt:
    image: registry.cn-shanghai.aliyuncs.com/suling/openwrt:rpi4
    container_name: openwrt_compose
    restart: always
    privileged: true
    command: /sbin/init
    volumes:
      - ./data/network:/etc/config/network
networks:
  default:
    external:
      name: macnet
</code></pre>
<p>OpenWrt 容器的 network 配置主要修改 Lan 口部分：</p>
<pre><code>config interface 'lan'
        option type 'bridge'
        option ifname 'eth0'
        option proto 'static'
        option netmask '255.255.255.0'
        option ip6assign '60'
        option ipaddr '192.168.31.111'
        option gateway '192.168.31.1'
        option dns '192.168.31.1'
</code></pre>
<p>配置 OpenWrt 的 ip 地址 <code>ipaddr</code> ，网关 <code>gateway</code> 和 DNS</p>
<p>启动容器后，我们可以使用 <code>docker ps -a</code>命令查看当前运行的容器：</p>
<pre><code class="language-bash">root in 🌐 raspbian in local/dockerCompose/openwrt 
❯ docker ps -a
CONTAINER ID        IMAGE                                                   COMMAND                  CREATED             STATUS              PORTS               NAMES
5592d9dbffd9        registry.cn-shanghai.aliyuncs.com/suling/openwrt:rpi4   "/sbin/init"             14 hours ago        Up 13 hours                             openwrt_compose
</code></pre>
<p>若容器运行信息<code>STATUS</code>列为 <code>UP</code>状态，则说明容器运行正常。</p>
<h3>运行</h3>
<p>浏览器输入刚才配置的 OpenWrt 的 ip 地址 <code>192.168.31.111</code> 即可进入 OpenWrt 管理页面。</p>
<p>默认账号：root</p>
<p>默认密码：password</p>
<h2>使用</h2>
<p>在 OpenWrt 中开启了 AdGuard 或者科学上网等其他服务的话，可以选择以下方式使用：</p>
<ul>
<li>
<p>全局</p>
<p>主路由器的网关和 DNS 设置为 OpenWrt 的 ip 地址 <code>192.168.31.111</code> ，那这样局域网中的其他设备连接都会来到 OpenWrt 中解析。</p>
</li>
<li>
<p>局部</p>
<p>全局使用的话可能会因树莓派 / OpenWrt 的不稳定而导致网络体验不佳，特别是家里还有其他成员的时候，那这时候就只需要将个别需要使用这些服务的设备的 Wifi 配置为静态 ip 连接，同样地将网关和 DNS 设置为 OpenWrt 的 ip 地址 <code>192.168.31.111</code> 即可。</p>
</li>
</ul>
<h2>小调整</h2>
<ul>
<li>
<p>关闭 DHCP</p>
<p>网络 - 接口 - LAN - 修改 - 基本设置 - 忽略此接口 (不在此接口提供DHCP服务)</p>
</li>
<li>
<p>宿主机网络修复</p>
<p>运行了 OpenWrt 容器后，宿主机（树莓派）可能会无法正常连接网络，那这时只需要修改宿主机的 <code>/etc/network/interfaces</code> 以静态 ip 的方式连接即可修复。</p>
<pre><code class="language-bash">cp /etc/network/interfaces /etc/network/interfaces.bak # 备份文件
vim /etc/network/interfaces # 使用 vim 编辑文件
</code></pre>
<p><code>/etc/network/interfaces</code> 配置：</p>
<pre><code>auto eth0
iface eth0 inet manual

auto macvlan
iface macvlan inet static
  address 192.168.31.194
  netmask 255.255.255.0
  gateway 192.168.31.1
  dns-nameservers 192.168.31.1
  pre-up ip link add macvlan link eth0 type macvlan mode bridge
  post-down ip link del macvlan link eth0 type macvlan mode bridge
</code></pre>
<p><strong>注意：</strong> 树莓派设置了静态 ip 连接后，下次换其他路由器或者网络环境时需要提前将其修改回 DHCP，否则就连不上网络，无法通过 SSH 连接树莓派了，只能通过连接 HDMI 给树莓派本地修改网络配置了。</p>
</li>
</ul>
<h2>参考链接</h2>
<ol>
<li><a href="https://mlapp.cn/376.html?from=donaldxdonald">在Docker 中运行 OpenWrt 旁路网关</a></li>
<li><a href="https://touchren.pub/2020/11/16/openwrt-in-docker?from=donaldxdonald">Docker下安装Openwrt</a></li>
</ol>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[用 pnpm 和 changesets 管理 monorepo 版本与发包]]></title>
            <link>https://dndxdnd.com//blog/monorepopart2</link>
            <guid>https://dndxdnd.com//blog/monorepopart2</guid>
            <pubDate>Sun, 20 Mar 2022 14:42:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>前言</h2>
<p>去年写了篇<a href="https://donaldxdonald.xyz/blog/AboutPublishingOnNPM">文章</a>记录了用 <a href="https://pnpm.io/">pnpm</a> 管理自己 ESLint 规则的 Monorepo，但也仅仅是只用到了 pnpm 的 workspace 功能来管理而已，但是管理发布版本的问题仍然没解决，之前都是要手动改 <code>package.json</code> 的版本号再 publish，这显然太麻烦了。简单了解了数个 monorepo 的工具库，感觉 <a href="https://github.com/changesets/changesets">changesets</a> 就挺简单纯粹的，可以用于改造自己 ESLint 配置的<a href="https://github.com/donaldxdonald/eslint-config">项目</a>。</p>
<h2>介绍</h2>
<blockquote>
<p>A way to manage your versioning and changelogs with a focus on monorepos.</p>
</blockquote>
<p>官方介绍言简意赅，抓住关键点 <code>manage your versioning and changelogs</code> 和 <code>monorepos</code>。就是用于管理 Monorepo 中的版本以及发布日志的工具。</p>
<h2>使用</h2>
<p>changesets 使用起来很简单，分为三个步骤</p>
<ol>
<li>生成 changeset 文件</li>
<li>更新版本</li>
<li>发布</li>
</ol>
<h3>安装 changeset</h3>
<p>可以选择安装在全局或者项目内</p>
<ol>
<li>
<p>全局安装</p>
<pre><code class="language-bash">$ pnpm i -g @changesets/cli
</code></pre>
</li>
<li>
<p>安装在项目内</p>
<pre><code class="language-bash">$ pnpm i -D @changesets/cli
</code></pre>
</li>
</ol>
<h3>初始化配置</h3>
<pre><code class="language-bash">$ pnpm exec changeset init
</code></pre>
<p>执行完上面代码后，项目内会多出来一个 <code>.changeset</code> 文件夹，里面有个 <code>config.json</code> 配置文件，配置项详情见<a href="https://github.com/changesets/changesets/blob/main/docs/config-file-options.md">官方文档</a>。</p>
<h3>生成 changeset 文件</h3>
<p><img src="https://cdn.donaldxdonald.xyz/blog/monorepoPart2/State1.png" alt="stage1"></p>
<pre><code class="language-bash">$ pnpm exec changeset
</code></pre>
<p>或者</p>
<pre><code class="language-bash">$ pnpm exec changeset add
</code></pre>
<p>两种方式都是一样的。然后会在命令行中根据提示选择要更新的包以及要更新版本号是更新 <code>major</code> 、<code>minor</code> 还是 <code>patch</code> 。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/monorepoPart2/Stage2.png" alt="stage2"></p>
<p>最后填写更新摘要，一个 changeset 文件就生成了。</p>
<h3>更新版本</h3>
<p><img src="https://cdn.donaldxdonald.xyz/blog/monorepoPart2/Stage3.png" alt="versioning"></p>
<pre><code class="language-bash">$ pnpm exec changeset version
</code></pre>
<p>这个命令会消费项目内所有的 changeset 文件，根据 changeset 文件更新对应 <code>package.json</code> 的版本号。</p>
<h3>发布</h3>
<pre><code class="language-bash">$ pnpm exec changeset publish
</code></pre>
<p>然后就会在该 Monorepo 中的每个子库中执行 <code>npm publish</code></p>
<h2>CI</h2>
<p>平时的话，只需要手动生成 changeset 文件，然后在 CI 中执行 versioning 和 publishing 的步骤就好了。因为不是每次修改都需要 publish ，这样的话就做到想发包的时候生成 changeset 文件提交就好了。</p>
<p>这里以 github actions 为例，只需要在 install 的步骤之后加上 versioning 和 publishing 的配置。</p>
<pre><code class="language-yaml">- name: Install dependencies
   run: pnpm install

- name: Create Release Pull Request or Publish to npm
    id: changesets
    	uses: changesets/action@v1
    with:
    # This expects you to have a script called release which does a build for your packages and calls changeset publish
        version: pnpm exec changeset version
    	publish: pnpm exec changeset publish
    env:
    	GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
</code></pre>
<h2>总结</h2>
<p>pnpm 的 workspace 解决了 monorepo 管理项目的问题，changesets 解决了 monorepo 版本管理以及发布记录的问题，小项目的话 pnpm + changesets 可以满足大多数需求，还是很方便的，更多的玩法还要再看看官方文档。</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[第二年的《新蝙蝠侠》]]></title>
            <link>https://dndxdnd.com//blog/thebatman</link>
            <guid>https://dndxdnd.com//blog/thebatman</guid>
            <pubDate>Sat, 19 Mar 2022 20:23:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><img src="https://cdn.donaldxdonald.xyz/blog/theBatman/GZTheBatman.jpg" alt="gzfans"></p>
<h2>前言</h2>
<p><img src="https://cdn.donaldxdonald.xyz/blog/theBatman/TheBatmanPeripheral.jpg" alt="assets"></p>
<p><img src="http://cdn.donaldxdonald.xyz/blog/theBatman/TheBatmanWithFlorence.jpg" alt="withFlorence"></p>
<p>这次参与了广州线下观影团，和一群喜欢这电影的人去看，这感觉是真的很棒，大家都很有活力，如果我还没毕业，我也会加入他们了！去年组团看扎导版正义联盟的时候有事没有去到，太可惜了。</p>
<h2>电影</h2>
<p><img src="https://cdn.donaldxdonald.xyz/blog/theBatman/batman_ver10_xlg.jpg" alt="TheBatmanPoster"></p>
<p>这一版《新蝙蝠侠》还是很不错的，有马导的个人作者风格（但还是喜欢扎导的审美多一点）。看得出来对挺多经典的致敬，开头像<a href="https://movie.douban.com/subject/1292222/">《出租车司机》</a>里 Travis 一样说想用雨水冲走这群穷街陋巷中的人渣。侦探+蛇蝎美人+心理悬疑这些<a href="https://zh.wikipedia.org/wiki/%E9%BB%91%E8%89%B2%E7%94%B5%E5%BD%B1">黑色电影</a>元素，加上马导用类似<a href="https://movie.douban.com/subject/1291839/">《银翼杀手》</a>的黑夜、雨水和霓虹灯等元素去塑造这一版的哥谭，很好地为布鲁斯的内心斗争作背景烘托。高潮部分被拆分为了好几部分，没有传统的让人紧张的高潮，重点还是刻画布鲁斯的内心转变。本片的很多浅景深镜头也是为了服务于表达角色内心，并从环境中抽离出来，蝙蝠侠在高楼看着整个城市的那一幕也是我很喜欢的风格了，焦点在远方，近景是虚焦的。和企鹅人的追车戏后的从火里走出来那一幕，还有后面拿着照明灯引领着市民那一幕，太 Cinema 啦！（结尾让我想起<a href="https://movie.douban.com/subject/1302770/">《阿基拉》</a>结尾了）</p>
<h2>角色</h2>
<p><img src="https://cdn.donaldxdonald.xyz/blog/theBatman/pattinson.jpg" alt="pattingson"></p>
<p>怕健身挺适合这一设定的蝙蝠侠的，阴郁、挣扎（一直还没看他的<a href="https://movie.douban.com/subject/30143336/">《灯塔》</a>）。可惜吃瘪次数太少了，<a href="https://movie.douban.com/subject/5150481/">元年漫画</a>里初出茅庐到处吃瘪的蝙蝠侠还是很好玩的，这版的猫女形象也是很还原元年漫画。阿福就少了点英国绅士的感觉，可能对安迪瑟金斯有先入为主的印象了233，还是铁叔演的阿福有内味一点，笑话也多一点。“蝙蝠侠的同伴”这一角色被猫女分了点去，导致这一版戈登的份量少了点，像元年漫画里描述戈登自己的故事还有与蝙蝠侠的配合就很棒。</p>
<h2>总结</h2>
<p><img src="https://cdn.myportfolio.com/76efa88f-ace3-44ad-bd9f-acbc9cac7f92/a4d6ccd0-3057-48dd-995e-61a9415a51f4_rw_1200.jpg?h=50ac0e7dead45f15a8c9d4e9711f7d7c" alt="batffleck"></p>
<p>虽然新蝙蝠侠很不错，但个人还是很想看到大本版的个人电影，大本的体型真的太适合黑归老爷的形象了，大本版黑归，怕健身版元年，都是很适合的。还是希望扎导宇宙早点继续。</p>
<p>一句话总结：@荷兰弟，这就是 Cinema 。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/theBatman/20220319_214559.jpg" alt="cinemameme"></p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[TypeScript Tips：根据已有的对象类型定义新的类型]]></title>
            <link>https://dndxdnd.com//blog/tstips1</link>
            <guid>https://dndxdnd.com//blog/tstips1</guid>
            <pubDate>Sat, 12 Feb 2022 11:52:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>背景</h2>
<p>最近在做前端编辑器相关的开发，有用到 <a href="https://github.com/ianstormtaylor/slate">Slate</a> ，不得不说其框架设计真的很优雅，开发者体验很棒。Slate 的数据模型是 Block Style 结构，在同类产品中较为通用，比如飞书、Notion 和 Google Docs。</p>
<h2>需求</h2>
<p>假设一个需求，项目原本的文档数据模型是与飞书一样的，现在需要用 Slate 开发，因此需要做一个从飞书数据模型到 Slate 数据模型的转换，项目是使用 TypeScript 开发的，考虑到节点类型一致性的问题，给 Slate 节点做 TS 类型定义就要根据已有的飞书节点类型来做转换。</p>
<h2>先决条件</h2>
<p>此处以飞书的数据模型为转换前的例子，数据参考于<a href="https://open.feishu.cn/document/ukTMukTMukTM/ukDM2YjL5AjN24SOwYjN#5293aed9">飞书开发文档</a>，当前 Slate 版本为 0.72.8 。</p>
<p>根据 Slate <a href="https://docs.slatejs.org/concepts/02-nodes#element">官网描述</a>，每一个 Element 节点最基本的条件就是有 <code>children</code> 字段，最底层是 Text 节点。这里我们可以给它加入一个 <code>type</code> 字段，以区分 Element 节点类型。那么以下就以 Paragraph 节点为例，飞书的 Paragraph 节点类型以及 TS 的类型定义如下：</p>
<pre><code class="language-json">{
  "style": "ParagraphStyle",
  "elements": ["ParagraphElement"]
}
</code></pre>
<pre><code class="language-typescript">interface ParagraphFeishu {
    style: ParagraphStyle
    elements: ParagraphElement[]
}
</code></pre>
<p>以这个为基础，可以将 Slate 的 Paragraph 节点自定义为如下：</p>
<pre><code class="language-typescript">interface ParagraphSlate {
    type: 'paragraph'
    style: ParagraphStyle
    children: ParagraphElementSlate[]
}
</code></pre>
<p>总结一下从一个飞书数据节点改造成一个 Slate 数据节点，只需要一下三步：</p>
<ol>
<li>增加 <code>type</code> 字段</li>
<li>将 <code>elements</code> 属性名改为 <code>children</code></li>
<li>将剩下的字段照搬过来</li>
</ol>
<h2>动手</h2>
<h3>增加 <code>type</code> 字段</h3>
<p>首先定义一个 <code>CustomElement</code> 类型，接收一个原始类型的泛型，让我们知道新的类型是根据哪个类型改造得来。但是因为原始类型中没有 <code>type</code> 类型相关的字段，我们就不知道这具体的 <code>type</code> 是什么，这时候就需要传第二个类型，作为 <code>type</code> 属性的类型，代码如下：</p>
<pre><code class="language-typescript">type CustomElement&#x3C;T, S> = {
    type: S
}
</code></pre>
<h3>将 <code>elements</code> 属性名改为 <code>children</code></h3>
<p>此处需做一层抽象，因为原数据模型的子类相关属性名不一定都是 <code>elements</code> ，还有可能是 <code>imageList</code>，那这里首先就要做个是否子类属性名的判断，用条件语句实现。</p>
<pre><code class="language-typescript">// 定义子类属性名的类型集合
type CHILDREN_PROP_KEY = 'elements' | 'imageList'
// 判断传入的类型是否为子类属性名
type IsChildrenKey&#x3C;K> = K extends CHILDREN_PROP_KEY ? true : false
</code></pre>
<p>然后在 <code>CustomElement</code> 中就需要判断原始类型的属性名了，是子类属性名的话就改为 <code>children</code> 属性名，否则不做处理。还有一点就是子类的类型，这个也是需要传入类型让我们知道新的 Slate 节点类型是什么。</p>
<pre><code class="language-typescript">// 定义子类属性名的类型集合
type CHILDREN_PROP_KEY = 'elements' | 'imageList'
// 判断传入的类型是否为子类属性名
type IsChildrenKey&#x3C;K> = K extends CHILDREN_PROP_KEY ? true : false

// T: 原始类型，S: 节点类型名称，C: 子类类型
type CustomElement&#x3C;T, S, C = never> = {
    type: S
} &#x26; {
	// 子类字段
    [K in keyof T as IsChildrenKey&#x3C;K> extends true ? 'children' : never]: C[]
}
</code></pre>
<h3>将剩下的字段搬过来</h3>
<p>好了，然后就是把剩下的字段都搬过来了，比如上面 <code>ParagraphFeishu</code> 类型的 <code>style</code> 。这里的思路就是判断原始类型中不是子类属性名的话，则直接迁移，完整代码如下：</p>
<pre><code class="language-typescript">// 定义子类属性名的类型集合
type CHILDREN_PROP_KEY = 'elements' | 'imageList'
// 判断传入的类型是否为子类属性名
type IsChildrenKey&#x3C;K> = K extends CHILDREN_PROP_KEY ? true : false

// T: 原始类型，S: 节点类型名称，C: 子类类型
type CustomElement&#x3C;T, S, C = never> = {
    type: S
} &#x26; {
	// 子类字段
    [K in keyof T as IsChildrenKey&#x3C;K> extends true ? 'children' : never]: C[]
} &#x26; {
    // 剩余字段
    [K in keyof T as IsChildrenKey&#x3C;K> extends true ? never : K]: T[K]
}
</code></pre>
<h3>定义 <code>ParagraphSlate</code> 类型</h3>
<p>根据如上定义的通用的转换类型 <code>CustomElement</code>， 定义 <code>ParagraphSlate</code> 类型将会变得简单优雅。</p>
<pre><code class="language-typescript">type ParagraphSlate = CustomElement&#x3C;ParagraphFeishu, 'paragraph', ParagraphElementSlate>
</code></pre>
<p>这样子就是实现了新类型与原始类型的绑定，如果日后需要更改原始类型中的某个属性的话，只需要改原始类型的数据就好了，就不需要两边都改。</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[State of 2021]]></title>
            <link>https://dndxdnd.com//blog/state-of-2021</link>
            <guid>https://dndxdnd.com//blog/state-of-2021</guid>
            <pubDate>Wed, 05 Jan 2022 23:23:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>前言</h2>
<p>现在已经是 2022 年了，前几天突然想到要不把自己过去一年的一些心路历程也写下来。前两天去广州爸妈家玩了，没带电脑，故没有写（借口。文章也没啥条理性的，就大概地按时间线来写吧。</p>
<h2>春</h2>
<p><img src="https://cdn.donaldxdonald.xyz/gallery/D%26F/EasternLake.jpg" alt="Florence"></p>
<p>2021 年里比较大的一个点可以说是和 Florence 互相见家长了，也是我们在一起八年以来第一次和彼此父母坦白自己的感情状况，毕竟在学生时代没啥经济能力的时候见家长也不太好，免得听他们说这说那。Florence 的爸妈都挺 nice 的，每次去都对我很热情，但社恐的我真的只想在房间里静静地呆着。</p>
<p>因为见家长了，也促使了我们一起出来租房这件事。在广州棠东租了个单间，装修好一点点但是很小，房租都要 1k3 ，着实体验到了啥叫蜗居。不过这次也是人生第一次自己出来租房，决定的那一刻也是太拍脑袋了，没考虑好，现在回想起来怎么都不会再租那里了。</p>
<p>三月份，首先最棒的就是等了很久的扎导版正联终于释出了，也算是见证历史了。从守望者入坑，钢铁之躯和 BVS 让我沉醉于扎导的影像风格中，守望者和 BVS 的片头真的百看不厌，每一个升格镜头都可以当壁纸了，细节上真的常看常新（扯远了。</p>
<h2>夏</h2>
<p><img src="https://cdn.donaldxdonald.xyz/gallery/D%26F/graduate/000003.jpg" alt="Graduate"></p>
<p>夏季是毕业生叫的季节，这时候的我也在论文的漩涡中挣扎，我的论文是比较中日俄三版《十二怒汉》翻拍的适应性，选题前觉得很有趣，自己也是看了好几遍原版《十二怒汉》，很是欣赏吕美特的调度能力，还有他的《热天午后》，真的是太有趣了，简单但有趣的剧情，恰到好处的幽默，实在是赞啊。只可惜到中间的时候我发现我对写论文真的起不了兴趣，而且当时也在想着毕业后的去处，一时间顿时有点郁闷。</p>
<p>当时还在一个小公司中实习，做的是消防局的调度页面系统，可真差不多就是切图仔了，学不到啥东西，早点润为妙。也是在这个时候，终于把自己的网站做得稍微能看一点了，博客就用了 vuepress 搭建，想自己写主题，但实在是没空，只好搁置。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/stateOf2021/Skateboarding.jpg" alt="skateboarding"></p>
<p>六月份，趁着为数不多的在校时间，都尽量地和朋友出去踩板和拍照，享受着这仅剩的校园生活。过去四年总嫌弃吐槽仲恺，但它还是那个给予我学生身份的地方，我在这里也认识到了很多朋友。对了，这时收到了一款很喜欢用的背单词 app 的 offer。</p>
<p>六月底，毕业了。因为我们外院答辩时间最晚，所以拍毕业照的时间也是最晚的，全校都拍了毕业照，等到我们院拍的前一天，就因为广州疫情反扑而取消了毕业照的拍摄，因此，我也是没拍过毕业照的人啦。</p>
<h2>秋</h2>
<p><img src="https://cdn.donaldxdonald.xyz/blog/stateOf2021/000048.jpg" alt="departure"></p>
<p>七月份，来到了另一个城市，入职新公司。新公司给我的感觉真的很棒，充满了青春的气息，大家都是朝气蓬勃的，最初是想着来学习一番，谁知入职第一天问了下，原来前端也就五个人左右的样子。入职以来目睹了人来人往，有处得很好的朋友离职了，使我郁闷了一阵子。</p>
<p>在这段时间，自己也对看源码这件事没了恐惧的感觉，更多地去尝试手写轮子，这是个很棒的一步，也让自己对技术有了新的看法。程序员所需的能力不外乎 API 和抽象，API 是常变的，抽象是不常变的，以往的我会觉得自己懂得使用某些 API 而沾沾自喜，现在逐渐地会因抽象能力不足而感到难受。</p>
<h2>冬</h2>
<p><img src="https://cdn.donaldxdonald.xyz/blog/stateOf2021/Discs.jpg" alt="discs"></p>
<p>十月份，此时已经进入了新公司的一个业务组，接到了个调研 AST 的任务。这时我真的非常开心，正所谓最高效地学习往往就是面向工作学习，工作上用得到的才会更好地让你学习上手。AST 这个以前一直有听闻的概念仿佛与我本不是一个世界的，自己也没什么动力去主动接触，但是现在工作遇到了，真是个绝佳的机会。很可惜，在学习使用 ANTLR 4 写解析器的过程，更加地凸显了自身技术基本功的不足，导致最后写出的解析器因性能问题，没有被用在生产环境，真的是太遗憾了。从这次的工作中，我对技术的认知也有了更进一步的理解，但还缺少最佳实践，打算在 2022 年入坑一下前端基建建设，了解下 babel 和 vite 插件等。如今对 AST 的恐惧也如当初对看源码的恐惧一样消失了，希望自己的认知不断地扩展。</p>
<p>也正是在这时候，久违的双休又来了，在新公司单休的日子真的让我深刻的认识到双休的重要性，单休之于双休就好比短视频之于电影，其意义是不能比的，短视频再有营养，也不足电影艺术。此处手动 @MartinScorsese</p>
<p>也因为双休，我也有时间去重写自己的网站了，还手写了《黑客帝国》的数字雨效果，尽量把自己的一些思绪整理成文，目前博客还未合并到同个项目中，还没想好主题如何设计，还在陆续地把好玩的新功能加入到网站中。</p>
<p>对了，今天是 2022/01/05 ，刚好半年过去，转正了。</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[重看 《 社交网络 》]]></title>
            <link>https://dndxdnd.com//blog/thesocialnetwork</link>
            <guid>https://dndxdnd.com//blog/thesocialnetwork</guid>
            <pubDate>Sat, 11 Dec 2021 16:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<p><img src="https://cdn.donaldxdonald.xyz/blog/theSocialNetwork/image-20211211224644772.png" alt="image-20211211224644772"></p>
<h2>前言</h2>
<p>​	前阵子因为工作需求，一个人搞 AST 解析和编辑器数据结构的东西，感觉大脑有点 overload 了忙不过来，想着要抽离一下。今天突然想重看 <em>The Social Network</em> ，上一次看还是高一的时候，是一个通宵的夜晚。我在那时候就已经对编程感兴趣了，但是对也是对编程没有概念，学 C# 写 UWP 也只是会了 <code>Console.WriteLine('Hello World')</code> 就没怎么深入学了，还是没能熬过那段枯燥的命令行编程时期（即使 UWP 开发可以拖拽控件）。</p>
<p>​	说回 <em>The Social Network</em>，那时候的我对电影也是没有鉴赏能力的，只是单纯的看剧情，那时候也只是因为想看扎克伯格的故事才看这部电影，只记得剧情挺有趣，但是很乱，那时候都不知道 <strong>非线性叙事</strong> 更不知道 <strong>大卫芬奇</strong> 是谁（以前也整天搞混林奇和芬奇 2333）。</p>
<h2>程序员视角</h2>
<p>这次重看，真的让我很惊喜。以现在的阅历能以程序员的视角和电影的视角去看这部电影真的很有体验，其中说到有一句话让我很深刻。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/theSocialNetwork/image-20211211163641107.png" alt="facebook"></p>
<p>​	互联网的魅力就在于能让十万八千里外可能一辈子都没机会见面的人联系在一起。像 Bosnia 这样的国家，没有路，却有 Facebook 。特别是作为前端开发者来看，Web 互联在这方面就更是了，任何设备都有浏览器，写出来的作品可以被世界上不同的人看到，这是很有魅力也很酷的一件事情。不得不说从剧本的角度来看 艾伦索金 真的抓住了精髓，今年还疯狂暗示随时可以创作第二部，就看大卫芬奇的了 XD 。</p>
<p>​	在 Mark 创建 FaceMash 的过程，看到发博客时要加上 HTML 的 p 标签、写脚本用的 Perl 、要买服务器和流量爆了要升级等等这些 moment 时都能 get 到并会心一笑。我在大一时学习 Python 做爬虫，那时候主要是爬漫画来看，有成就感一点的就是爬教务网了，就有点像电影里爬取各个院的女生照片一样，不过教务网只能看自己信息哈哈哈。</p>
<p>​	还有就是双胞胎兄弟邀请 Mark 合作 Harvard.edu 的时候，Mark 问的一句 " how is that different from MySpace or Friendster ? " 让我 DNA 动了。这个其实我经常就有这个疑问，如今互联网时代这么多年了，进入了移动互联网时代，产品数不胜数，能够做出前无古人后无来者的产品的人很少，很多时候都是大家一起在做同一个概念的产品，如何才能打造自己的核心竞争力呢？如何能做到与众不同呢？或者说我们做人也是，泯为众人还是与众不同，这是个很有趣的讨论点。</p>
<h2>电影</h2>
<p>​	从电影视角来看呢，就更有趣了。全片看下来，我会觉得结构有点像 <em>Citizen Kane</em> ，<em>Citizen Kane</em> 是通过几个人的讲述回忆来构建 Kane 的一生，讲述他是如何从无到有，成为一个商业大亨的。而 <em>The Social Network</em> 则是通过两场听证会来衔接整个故事，高密度的叙事节奏，精彩的配乐与剪辑，还有那机关枪式的的对白让人直呼过瘾，芬奇的另一部电影 <em>Fight Club</em> 也是。 我说像 <em>Citizen Kane</em> 不是没有原因的，除了通过听证会来衔接故事，当然还有芬奇和他爸准备的 <em>Mank</em> 了，你说没关系我都有点不信了。还有一点的是，在我看来，芬奇是把 Erica 当作是 Mark 的 Rosebud 来拍了。尽管 Mark 经历了许多，但他的内心深处仍然是 Erica，说是 Eduardo 也是 make sense 的。</p>
<p>​	还有就是，<em>The Social Network</em> 的剧情也很像其他的一些传记片，比如 <em>Straight Outta Compton</em> 和 <em>Bohemia Rhapsody</em> ，都是讲述主角的自我毁灭。 Mark 信任 Sean 而背叛了兄弟 Eduardo，Eazy-E 信任 Jerry 而背叛了 N.W.A 的兄弟 还有 Freddie 信任 Paul 而与 Queen 的队友们闹掰等等。还有一部我很喜欢的讲述主角自我毁灭的片子 —— <em>Raging bull</em> ，太喜欢了老马和德尼罗了。在这类片子中，通常会有个 <strong>“ 堕落曲线 ( Fall Arc ) ”</strong>，主角会拒绝每个拥抱真相的机会，然后越来越深地陷入自己罪恶的泥沼，通常还会把他人拉下水，他们会把每一个试图拯救他们的的人统统赶走。</p>
<p>​	讽刺的是，Mark 创立了一个连接人与人的平台 —— Facebook ，但是没能成为一个能建立有意义的人际关系的人，其实他的深层需求是有意义的友情和接纳。最后一幕，Marilyn 的一句话 " You're not an asshole. You're just trying so hard to be. " 将 Mark 的深层需求带回了起点，对应了电影开头 Erica 对 Mark 说的话。</p>
<h2>有意思的点</h2>
<ol>
<li>
<p>双胞胎兄弟比赛时用了移轴镜头拍摄，非常不错，以前我也尝试了很多次伪移轴摄影，但效果都不太行，缺少一个好的机位，下次再尝试一下。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/theSocialNetwork/image-20211211221918863.png" alt="image-20211211221918863"></p>
</li>
<li>
<p>Justin Timberlake 比 Jesse Eisenberg 像 Mark 太多了！但是在 <em>BVS</em> 中看卷西又会自动跳戏到 <em>The Social Network</em> 里哈哈哈。</p>
</li>
<li>
<p>最后 Mark 一直按 F5 重新 load 网页真的笑了，简直就是 Web 历史的眼泪啊。</p>
</li>
</ol>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[以 MonoRepo 形式管理 Eslint 规则并发包]]></title>
            <link>https://dndxdnd.com//blog/aboutpublishingonnpm</link>
            <guid>https://dndxdnd.com//blog/aboutpublishingonnpm</guid>
            <pubDate>Wed, 01 Dec 2021 00:10:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>缘由</h2>
<p>最近想重写自己的网站，但是想到每一次新项目都要配一堆 Eslint 配置，麻烦死了，然后就想到了自己发包到 NPM 去，然后就折腾了几天。</p>
<h2>步骤</h2>
<p>想到工作上用 Angular ，平时自己用 Vue ，除了 Vanilla JS 还有 TS 的配置，就决定用 MonoRepo 的方式管理了。经过一番调研发现有用 <a href="https://github.com/lerna/lerna">lerna</a> 的也有用 <a href="https://rushjs.io/">Rush</a> 配合 <a href="https://pnpm.io/workspaces">pnpm workspace</a> 的，我个人还是挺喜欢 pnpm 的，而且这个项目也先不用太复杂，就暂且先用 pnpm workspace 来管理吧。其实发包不难，但就遇到一些小问题把自己整麻了，先说一下发包的步骤。</p>
<h3>1. 登录</h3>
<p>先看看有没有登录 npm</p>
<p><strong>切记</strong>，此处需先把 registry 改回 npmjs ，推荐用 <a href="https://github.com/Pana/nrm">nrm</a> 管理。</p>
<pre><code class="language-bash">$ pnpm whoami
</code></pre>
<p>如有登录信息则进入第二步，否则继续登录</p>
<pre><code class="language-bash">$ pnpm adduser
</code></pre>
<p>根据提示信息输入用户名、密码和邮箱，有两步验证的也要验证一下~</p>
<p>登陆成功后会显示 <code>Logged in as xxx on https://registry.npmjs.org/.</code></p>
<h3>2. 项目设置</h3>
<p>首先项目根目录创建 <code>packages</code> 文件夹，里面放各个 eslint 规则的 repo。我的项目结构大致如下：</p>
<pre><code class="language-shell">├─LICENSE
├─README.md
├─package.json
├─pnpm-lock.yaml
├─pnpm-workspace.yaml
├─packages
|    ├─typescript
|    |   ├─index.js
|    |   └package.json
|    ├─basic
|    |   ├─index.js
|    |   └package.json
|    ├─all
|    |   ├─index.js
|    |   └package.json
</code></pre>
<p>项目根目录中创建 <code>pnpm-workspace.yaml</code> 文件，写入以下内容，配置 packages 目录。</p>
<pre><code class="language-yaml">packages:
  - "packages/*"
</code></pre>
<h3>3. 发布</h3>
<p>项目配好之后，就可以发布了，在 <code>package.json</code> 的 scripts 中加入 <code>"publish": "pnpm publish -r --access=public"</code>，然后</p>
<pre><code class="language-bash">$ pnpm run publish
</code></pre>
<p>完事~</p>
<h2>踩坑</h2>
<p>正常来说，步骤就这样，但是我也遇到了一个<del>问题</del></p>
<pre><code class="language-shell">npm ERR! code E403
npm ERR! 403 403 Forbidden - PUT https://registry.npmjs.org/@dxd%2feslint-config - Forbidden
npm ERR! 403 In most cases, you or one of your dependencies are requesting
npm ERR! 403 a package version that is forbidden by your security policy, or
npm ERR! 403 on a server you do not have access to.
</code></pre>
<p>万恶的 403，查了网上的众多答案后，可以由以下步骤排查</p>
<ol>
<li>看看自己的 npm 账号有没有验证邮箱</li>
<li>npm registry 是否切换回了 npmjs</li>
</ol>
<p>​	网上的答案 99% 都是这两个，剩下 1% 就是说你用了 VPN。我尝试了很多次了，但还是 403，折腾了几天才发现了问题所在：包名错了。。。</p>
<p>​	在 <a href="https://docs.npmjs.com/about-public-packages">npm Docs</a> 中写了包的范围命名，没范围的呢，就单纯的一个名字，比如 <code>axios</code>，还有的就是范围命名，包括 <code>@username/package-name</code> 和 <code>org-name/package-name</code> ，即 @用户名 或者 @组织名 在前。</p>
<p>​	我本来也是想着用 @用户名 在前的方式的，但忘了自己的 npm 用户名就叫 donaldmok，然后自己在各个 <code>package.json</code> 中写了 <code>@dxd/eslint-config</code>了。。。包名和用户名对不上，自然就发不了包上去了。</p>
<p>​	还是太粗心了。。。</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[Antlr 学习记录]]></title>
            <link>https://dndxdnd.com//blog/antlr4summary</link>
            <guid>https://dndxdnd.com//blog/antlr4summary</guid>
            <pubDate>Fri, 05 Nov 2021 22:24:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>背景知识</h2>
<p><strong>抽象语法树 (Abstract Syntax Tree, AST)</strong> 是源代码结构的一种抽象表示，它以树的形状表示语言的语法结构。抽象语法树一般可以用来进行<code>代码语法的检查</code>，<code>代码风格的检查</code>，<code>代码的格式化</code>，<code>代码的高亮</code>，<code>代码的错误提示</code>以及<code>代码的自动补全</code>等等。</p>
<h2>简介</h2>
<p>ANTLR v4 是一款功能强大的<strong>语法分析器生成器</strong>，可以用来读取、处理、执行和转换结构化文本或二进制文件。它被广泛应用于学术界和工业界构建各种语言、工具和框架。Antlr4 生成的解析器包含了词法分析程序和语法分析程序，没错，这就是编译原理课程中的词法分析和语法分析。</p>
<pre><code class="language-html">&#x3C;title>&#x3C;div>111&#x3C;/div>&#x3C;/title>
</code></pre>
<p>上面代码经过 Antlr 解析后便成了下图</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/antlr4/image-20211105221346631.png" alt="image-20211105221346631"></p>
<h2>它做了什么？</h2>
<p>词法分析( lexical analysis ) + 语法分析( syntax analysis )</p>
<ol>
<li>词法分析 —— 读取字符流，将字符识别为各种 Token ，标记好类别。比如下图中的赋值语句<code>sp=100</code>，词法分析后得出<code>sp</code> <code>=</code> <code>100</code> 三个 Token。</li>
<li>语法分析 —— 语法分析通过解析词法分析生成的 Token 来识别语句结构，它一般不在乎符号的内容，只关心它们的类型。上边的语句经过语法分析后，就会得出这是<code>赋值语句</code>的结论。</li>
</ol>
<p>ANTLR v4 的语法规则分为<strong>词法 (Lexer)</strong> 规则和<strong>语法 (Parser)</strong> 规则：词法规则定义了怎么将代码字符串序列转换成标记序列；语法规则定义怎么将标记序列转换成语法树。通常，词法规则的规则名以大写字母命名，而语法规则的规则名以小写字母开始。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/antlr4/image-20211105221538440.png" alt="image-20211105221538440"></p>
<h2>Antlr 语法规则</h2>
<pre><code>ruleName: alternative1 | alternative2 | alternative3 ;
</code></pre>
<p>Antlr 4 的每条<code>rule</code>结构如上，这条语法规则申明一条名为 <code>ruleName</code> 的<code>rule</code>，其中 <code>|</code> 表名为分支、即该 <code>rule</code> 可以匹配三个分支中的任何一个。</p>
<ul>
<li>注释：和 Java 的注释完全一致 // 单行注释 /* */ 多行注释；</li>
<li>标志符：参考 Java 或者 C 的标志符命名规范，针对 Lexer 部分的 Token 名的定义，采用全大写字母的形式，对于 parser rule 命名，推荐首字母小写的驼峰命名；</li>
<li>不区分字符和字符串，都是用单引号引起来的，同时，虽然 Antlr g4 支持 Unicode 编码（即支持中文编码），但是建议大家尽量还有英文；</li>
</ul>
<p>详情查看<a href="https://github.com/antlr/antlr4/blob/master/doc/grammars.md">官方文档</a></p>
<h3>Import</h3>
<p><code>import</code>语法可以将语法文件变得可复用性更强</p>
<p>Tips:</p>
<ul>
<li>Lexer 语法能导入 Lexer 语法</li>
<li>Parser 语法能导入 Parser 语法</li>
<li>统一写的语法**（combined grammars）**文件可以导入 Lexer 或 Parser</li>
<li><code>import</code> 语法不会引入已经定义好的<code>rule</code></li>
</ul>
<pre><code>grammar Expr;
import Expr1, Expr2;
</code></pre>
<h3>Tokens</h3>
<p><code>tokens</code>是用来定义没有关联词法<code>rule</code>的 token</p>
<pre><code>lexer gammar ExprLexer;
tokens { Token1, ..., TokenN }
</code></pre>
<h3>Action</h3>
<p>行为，主要有 <code>@header</code> 和 <code>@members</code>，用来定义一些需要生成到目标代码中的行为，例如，可以通过 <code>@header</code> 设置生成的代码的 package 信息，<code>@members</code> 可以定义额外的一些变量到 Antlr4 语法文件中。</p>
<p>统一写的语法文件里，<code>action</code> 需要限制性质，<code>@parser::name</code> 或 <code>@lexer:name</code></p>
<pre><code>grammar Count;
 
@header {
package foo;
}
 
@members {
int count = 0;
}
 
list
@after {System.out.println(count+" ints");}
: INT {count++;} (',' INT {count++;} )*
;
 
INT : [0-9]+ ;
WS : [ \r\t\n]+ -> skip ;
</code></pre>
<h2>词法规则 (lexer)</h2>
<h3>基础</h3>
<pre><code>grammar Hello;               // 定义文法的名字

s  : 'hello' ID ;            // 匹配关键字hello和标志符
ID : [a-z]+ ;                // 标志符由小写字母组成
WS : [ \t\r\n]+ -> skip ;    // 跳过空格、制表符、回车符和换行符

TOKEN:  TEST
</code></pre>
<table>
<thead>
<tr>
<th><strong>语法</strong></th>
<th><strong>描述</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>T  (冒号左边的)</td>
<td>在当前行匹配 token T。Token 通常以大写字母开头，对应上方代码中的 TOKEN</td>
</tr>
<tr>
<td>T  (冒号右边的)</td>
<td>调用规则 T ，可以是<code>token</code> 也可以是 <code>fragment</code> 中的 <code>rule</code>，对应上方代码中的 TEST</td>
</tr>
<tr>
<td>’x’</td>
<td>匹配单引号内的字符序列，如 ’while’ 或者 ’=’</td>
</tr>
<tr>
<td>[xyz]</td>
<td>匹配中括号内的字符，比如 [1-9] 即匹配 1 到 9 之间的值。</td>
</tr>
<tr>
<td>'x'..'y'</td>
<td>匹配 x 和 y 之间的任意字符。'a'..'z' 等价于 [a-z]</td>
</tr>
<tr>
<td>.</td>
<td>匹配任意一个值</td>
</tr>
<tr>
<td>~x</td>
<td>匹配任意不在x中的值</td>
</tr>
<tr>
<td>x?</td>
<td>匹配 x 0 次 或 1 次</td>
</tr>
<tr>
<td>X*</td>
<td>匹配 x 0 次以上</td>
</tr>
<tr>
<td>x+</td>
<td>匹配 x 1 次以上</td>
</tr>
<tr>
<td>x | y</td>
<td>x 或 y</td>
</tr>
<tr>
<td>{«action»}</td>
<td>示例： `END : ('endif'</td>
</tr>
<tr>
<td>{«p»}?</td>
<td>语义断言  «p»。如果在运行环境中，该断言判断为 false ，那么其附近的<code>rule</code>将会变为“不可见的”，同样地， «p»中的指令应跟目标语言的语法一致。 详情见<a href="https://github.com/antlr/antlr4/blob/4.6/doc/predicates.md">官方文档</a> 需要注意的是，语义断言需在 action 之前</td>
</tr>
</tbody>
</table>
<h3>Fragment</h3>
<p>在<code>fragment</code>下可以定义不是 token 但可以用来帮助识别 token 的规则</p>
<p>比如 <code>DIGIT</code> 是个很常见的 <code>fragment</code> <code>rule</code>。使用 fragment 前缀来定义，ANTLR 就会知道这条<code>rule</code>仅仅用来组成其它词法规则，而不会单独使用。DIGIT 本身并不是我们需要的符号，也就是说，我们在语法分析器中是看不到 DIGIT 这个符号的。</p>
<pre><code>INT : DIGIT+ ; // 引用 DIGIT rule
fragment
DIGIT : [0-9] ; // 不是一个 token
</code></pre>
<h3>Mode</h3>
<p><code>mode</code> 允许以上下文的形式将<code>rule</code>分组。Lexer 仅能匹配当前 mode 下的<code>rule</code>，所有没定义 mode 的<code>rule</code>将会被归于默认 mode 中。<code>mode</code>仅能在 Lexer 中定义，不能在统一语法文件中。</p>
<p><code>mode</code> 可通过 <code>pushMode(modeName)</code> 进入，<code>popMode</code>退出</p>
<pre><code>lexer grammar HTMLLexer;

// rules in default mode

TAG_OPEN
    : '&#x3C;' -> pushMode(TAG)
    ;


mode TAG;

// rules in TAG mode

TAG_CLOSE
    : '>' -> popMode
    ;

TAG_SLASH_CLOSE
    : '/>' -> popMode
    ;
</code></pre>
<h3>递归</h3>
<p>Antlr 的词法规则可以是递归的，这样的话想要匹配嵌套语法就很方便了</p>
<pre><code>lexer grammar Recur;
 
ACTION : '{' ( ACTION | ~[{}] )* '}' ;
 
WS : [ \r\t\n]+ -> skip ;
</code></pre>
<p>Antlr 4 可以处理直接的左递归，但不能处理间接的左递归。</p>
<pre><code>expr      :  expr '*' expr        // 直接的左递归
          |  addExpr              // 间接的左递归
          ;

addExpr   :  expr '+' expr ;
</code></pre>
<h3>重复声明Rule</h3>
<p>不同<code>rule</code>名，但右边是一样的话，在 antlr 里是不允许的。但是在不同 <code>mode</code>中却是可行的。如：</p>
<pre><code>lexer grammar L;

AND : '&#x26;' ;

mode STR;
MASK : '&#x26;' ;
</code></pre>
<p>Parser 语法里不能识别单纯的 '&#x26;'，但可以识别 token 名（因为不同<code>mode</code>）</p>
<pre><code>parser grammar P;
options {  tokenVocab=L;  }
a:    '&#x26;'    // 识别不了
 |    AND    // 没问题
 |    MASK    // 没问题
 ;
</code></pre>
<h3>指令</h3>
<p>Antlr 的指令写在<code>rule</code>后面，需要用 <code>-></code>连接。和<code>action</code>一样，一个 token 后只能有一个指令，可以用<code>,</code>分割多个指令</p>
<pre><code>TokenName : «alternative» -> 指令名
TokenName : «alternative» -> 指令名 («identifier or integer»)
</code></pre>
<p>可用的指令名：</p>
<ul>
<li>skip</li>
<li>more</li>
<li>popMode</li>
<li>mode(x)</li>
<li>pushMode(x)</li>
<li>type(x)</li>
<li>channel(x)</li>
</ul>
<h4>skip</h4>
<p><code>skip</code>命令可以告诉 Lexer 跳过当前 token</p>
<pre><code>ID : [a-zA-Z]+ ; // 匹配标识符
INT : [0-9]+ ; // 匹配整数
NEWLINE:'\r'? '\n' ; // 匹配换行
WS : [ \t]+ -> skip ; // 不要识别空格和 tab
</code></pre>
<h4>mode(), pushMode(), popMode() 和 more</h4>
<p>通过这些指令可以切换 Lexer 的 <code>mode</code> 栈, <code>more</code>指令可以在不退出当前 mode 的同时识别其他 mode 的<code>rule</code>。</p>
<h4>type()</h4>
<p>切换为某个 token 名。多个 <code>type()</code>指令的话，只有最右边的生效</p>
<pre><code>lexer grammar SetType;
tokens { STRING }
DOUBLE : '"' .*? '"'   -> type(STRING) ;
SINGLE : '\'' .*? '\'' -> type(STRING) ;
WS     : [ \r\t\n]+    -> skip ;
</code></pre>
<h4>channel()</h4>
<p>放到 <code>channel (HIDDEN)</code> 中的 Token，不会被语法解析阶段处理，但是可以通过 Token 遍历获取到。</p>
<pre><code>BLOCK_COMMENT
        : '/*' .*? '*/' -> channel(HIDDEN)
        ;
LINE_COMMENT
        : '//' ~[\r\n]* -> channel(HIDDEN)
        ;
... 
// ----------
// Whitespace
//
// 对人类需要看到，但是对于编译器不需要
// 看到的字符，可以选择隐藏
// 
//
WS : [ \t\r\n\f]+ -> channel(HIDDEN) ;
</code></pre>
<p>Antlr 4.5 版本后，可以像下面这样定义 <code>channel</code>，只有 Lexer 语法可以自定义 <code>channel</code>。</p>
<pre><code>channels {  WSCHANNEL, MYHIDDEN  }
</code></pre>
<h2>语法规则 （Parser）</h2>
<h3>基础</h3>
<p>Parser 主要是定义目标语言的语法规则，比如识别编程语言中的<strong>声明语句</strong></p>
<table>
<thead>
<tr>
<th><strong>语法</strong></th>
<th><strong>描述</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>r (冒号左边的)</td>
<td>在当前行匹配 <code>rule</code> r。<code>rule</code>通常以小写字母开头</td>
</tr>
<tr>
<td>T  (冒号右边的)</td>
<td>匹配 <code>Token</code> T ，即 Lexer 中的 <code>Token</code></td>
</tr>
<tr>
<td>’x’</td>
<td>同 Lexer</td>
</tr>
<tr>
<td>r [«args»]</td>
<td>像执行函数一样匹配 <code>rule</code> r 的同时传入中括号内定义的一系列参数。 参数的语法与生成目标语言一致 多个表达式以<code>,</code>分隔</td>
</tr>
<tr>
<td>.</td>
<td>同 Lexer</td>
</tr>
<tr>
<td>~x</td>
<td></td>
</tr>
<tr>
<td>x?</td>
<td></td>
</tr>
<tr>
<td>X*</td>
<td></td>
</tr>
<tr>
<td>x+</td>
<td></td>
</tr>
<tr>
<td>x | y</td>
<td></td>
</tr>
<tr>
<td>{«action»}</td>
<td></td>
</tr>
<tr>
<td>{«p»}?</td>
<td></td>
</tr>
</tbody>
</table>
<h3>可选项标签</h3>
<p>通过给语法<code>rule</code>的各个可选项 (alternative) 加上<code># 标签</code> 可以获得更准确的编译信息。</p>
<p><strong>注意：要么给全部可选项加上标签，要么就全都不加</strong></p>
<pre><code>grammar T;

sentence
    :   TEXT* expression TEXT* (END_SIGN|NL)*   # TextWithExpression
    |   expression  END_SIGN?  # ExpressionOnly
    |   TEXT    (END_SIGN|NL)*   # TextOnly
    ;
</code></pre>
<h3>Rule 内容对象</h3>
<p>Antlr 会生成可以访问每一条<code>rule</code>内容的方法（与引用<code>rule</code>名有关），比如下面 <code>rule</code> 中 <code>expression</code> 引用了 <code>expr</code>：</p>
<pre><code>parser grammar TestParser;

expression
    :   EXPR_START EXPR_TYPE EXPR_HASH expr EXPR_END
    ;
</code></pre>
<p>Antlr 生成的对象则为：</p>
<pre><code>public static class ExpressionContext extends ParserRuleContext {
         public ExprContext expr() { ... } // return context object associated with expr
         ...
}
</code></pre>
<p>如果有多个对 <code>expr</code> 的引用，也是可以的：</p>
<pre><code>parser grammar TestParser;

expression
    :   EXPR_START EXPR_TYPE EXPR_HASH expr EXPR_HASH expr EXPR_END
    ;
</code></pre>
<p>那么就会生成如下：</p>
<pre><code>public static class ExpressionContext extends ParserRuleContext {
         public ExprContext expr(int i) { ... } // 获取单独的内容
         public List&#x3C;ExprContext> expr() { ... } // 获取所有的 expr 内容
         ...
}
</code></pre>
<h3>Rule 元素标签</h3>
<p>可以使用<code>=</code>符号给<code>rule</code>元素加上标签（别名？）</p>
<pre><code>parser grammar TestParser;

expression
    :   EXPR_START EXPR_TYPE EXPR_HASH sentence=expr EXPR_END
    ;
</code></pre>
<p>那么 Antlr 生成的类则是：</p>
<pre><code>public static class ExpressionContext extends StatContext {
         public ExprContext sentence;
         ...
}
</code></pre>
<h3>异常捕获</h3>
<p>每一条 <code>rule</code> 都被包裹在 <code>try/catch/finally</code>中</p>
<p>参考<a href="https://github.com/antlr/antlr4/blob/master/doc/parser-rules.md#catching-exceptions">官方文档</a></p>
<h3>Start Rule 和 EOF</h3>
<p>Start Rule 是被 Parser 解析的第一个 <code>rule</code>，所有 <code>rule</code> 都可以作为 Start Rule。</p>
<h2>解决歧义</h2>
<h3>优先级</h3>
<p>靠前的规则可选项优先级更高</p>
<pre><code>expr    :    expr '*' expr        // 乘法运算写在前边，会被优先匹配
        |    expr '+' expr
        ;
</code></pre>
<h3>右结合</h3>
<p>如指数运算<code>2 ^ 5 ^ 7</code>是右结合的，需要先计算<code>5 ^ 7</code>，可以使用<code>assoc</code>来指定结合性</p>
<pre><code>expr    :    expr '^'&#x3C;assoc=right> expr    // 指定右结合
        |    INT
        ;
</code></pre>
<h2>实现解析简单运算</h2>
<pre><code>lexer grammar ExprLexer;

MUL     : '*' ;
DIV     : '/' ;
ADD     : '+' ;
SUB     : '-' ;
LPAREN  : '(' ;
RPAREN  : ')' ;

ID      : LETTER (LETTER | DIGIT)*  ;
INT     : [0-9]+ ;
EQ      : '=' ;
SEMI    : ';' ;
COMMENT : '//' ~[\r\n]* '\r'? '\n'? -> channel(HIDDEN);
WS      : [ \r\n\t]+ -> channel(HIDDEN);

fragment
LETTER  : [a-zA-Z] ;
fragment
DIGIT   : [0-9] ;
</code></pre>
<pre><code>parser grammar ExprParser;

options {  tokenVocab = ExprLexer;  }

prog
    : stat+ ;

stat
    : exprStat
    | assignStat
    ;

exprStat
    : expr SEMI
    ;

assignStat
    : ID EQ expr SEMI
    ;

expr
    : expr op = (MUL | DIV ) expr   # MulDivExpr
    | expr op = ( ADD | SUB ) expr   # AddSubExpr
    | INT                       # IntExpr
    | ID                        # IdExpr
    | LPAREN expr RPAREN        # ParenExpr
    ;
</code></pre>
<h2>最佳实践</h2>
<h3>非贪婪匹配</h3>
<p>很多开发语言都支持多行注释，比如 Java 或 C 指定在 <code>/*</code> 与 <code>*/</code> 之间的任意字符都是注释的内容。ANTLR 默认使用贪婪匹配的原则，如果一个文件内有多块注释，那么从第一个 <code>/*</code> 一直到最后一个 <code>*/</code> 之间的所有内容都会被匹配为注释内容。所以对于这类的语法定义需要使用非贪婪匹配：</p>
<pre><code>comment    :    '/*' .*? '*/';        // *?表示非贪婪匹配
</code></pre>
<h3>将词法符号送入不同通道</h3>
<p>ANTLR 支持在词法分析阶段将词法符号放到不同的通道，除了默认通道，其他自定义的通道都是隐藏的，对语法分析不可见。比如，分析并运行 Java 文件的应用并不关心文件里的空白字符和注释，这些不关心的内容可以放入丢弃通道，接下来的语法分析会忽略这部分内容，加快分析的速度。把某种语言翻译成另一种语言的应用，如果两种语言有一部分相同的语法，可以把这部分内容送入单独的通道，略过语法分析，在最后的翻译应用中再取出复制。下边是对内置丢弃通道 <code>skip通道</code>与自定义通道的使用示例：</p>
<pre><code>@lexer::members {
public static final int COMMENTS = 1;                     // 非ANTLR内置的通道需要定义
}

COMMENT    :    '\\' .*? '\n' -> channel(COMMENTS) ;      // 将注释放入COMMENTS通道

WS         :    [ \t\n\r]+    ->    skip ;                // 将空白字符放入skip通道，skip通道是ANTLR内置的通道，传入的符号将被丢弃
</code></pre>
<h3>处理同一文件中的不同格式</h3>
<p>有一些语言，其结构化的区域被随机文本所包围，被称为孤岛语言（island language）。比如 XML 或 HTML，<code>&#x3C;></code> 之内包裹的部分是结构化的标签，而外部则是大片我们不关心的文本。处理这一类语言就需要用到<code>mode</code>。下边以一个简化的 XML 语法为例：</p>
<pre><code>file    :    (tag | TEXT)* ;

tag     :    '&#x3C;' ID '>'
        |    '&#x3C;' '/' ID '>'
        ;

OPEN    :    '&#x3C;'         -> mode(TAG) ;             // 切换到TAG模式
TEXT    :    ~'&#x3C;'+ ;

mode TAG;                                           // 从此处向下的部分为TAG模式的词法
CLOSE    :    '>'        -> mode(DEFAULT_MODE) ;    // 返回默认模式
SLASH    :    '/' ;
ID       :    [a-zA-Z]+ ;
</code></pre>
<h2>参考资料</h2>
<ol>
<li><a href="https://github.com/antlr/antlr4/blob/master/doc/index.md">Antlr 4 官方文档</a></li>
<li><a href="https://juejin.cn/post/6844903539978813453#heading-15">ANTLR：在浏览器中玩语法解析</a></li>
<li><a href="https://juejin.cn/post/6922252291256893453">从定义到 AST 及其遍历方式，一文带你搞懂 Antlr 4</a></li>
<li><a href="https://github.com/dohkoos/antlr4-short-course/blob/master/SUMMARY.md">Antlr 4 简明教程</a></li>
<li><a href="https://liangshuang.name/2017/08/20/antlr/">Antlr 4 进阶</a></li>
<li><a href="https://juejin.cn/post/7018521754125467661">语法解析器 ANTLR4 从入门到实践</a></li>
<li><a href="https://tomassetti.me/antlr-mega-tutorial">Antlr mega tutorial</a></li>
</ol>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[网易云去灰]]></title>
            <link>https://dndxdnd.com//blog/deployunblockneteasemusic</link>
            <guid>https://dndxdnd.com//blog/deployunblockneteasemusic</guid>
            <pubDate>Mon, 27 Sep 2021 23:50:00 GMT</pubDate>
            <content:encoded><![CDATA[<blockquote>
<p>部署 <a href="https://github.com/nondanee/UnblockNeteaseMusic">@nondanee/UnblockNeteaseMusic</a> 在服务器，通过 clash 代理将网易云客户端请求导向服务器，从而达到去灰效果</p>
</blockquote>
<h2>1. 部署 UnblockNeteaseMusic</h2>
<h3>自签证书</h3>
<blockquote>
<p>via <a href="https://github.com/nondanee/UnblockNeteaseMusic/issues/48#issuecomment-477870013">issue#48</a></p>
</blockquote>
<pre><code class="language-bash"># 新建证书文件夹
mkdir cert

# 生成 CA 私钥
openssl genrsa -out ca.key 2048

# 生成 CA 证书 ("YOURNAME" 处填上你自己的名字)
openssl req -x509 -new -nodes -key ca.key -sha256 -days 1825 -out ca.crt -subj "/C=CN/CN=UnblockNeteaseMusic Root CA/O=YOURNAME"

# 生成服务器私钥
openssl genrsa -out server.key 2048

# 生成证书签发请求
openssl req -new -sha256 -key server.key -out server.csr -subj "/C=CN/L=Hangzhou/O=NetEase (Hangzhou) Network Co., Ltd/OU=IT Dept./CN=*.music.163.com"

# 使用 CA 签发服务器证书
openssl x509 -req -extfile &#x3C;(printf "extendedKeyUsage=serverAuth\nsubjectAltName=DNS:music.163.com,DNS:*.music.163.com") -sha256 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt

</code></pre>
<h3>使用 dockerCompose</h3>
<h4>目录结构</h4>
<pre><code>├─docker-compose.yml
├─cert
|  ├─ca.crt
|  ├─ca.key
|  ├─ca.srl
|  ├─server.crt
|  ├─server.csr
|  └server.key
</code></pre>
<pre><code class="language-yml"># docker-compose.yml
version: "3"

services:
  unblockneteasemusic:
    image: nondanee/unblockneteasemusic
    environment:
      NODE_ENV: production
    ports:
      - 80:80
      - 443:443
    restart: always
    volumes:
      - ./cert/server.key:/usr/src/app/server.key
      - ./cert/server.crt:/usr/src/app/server.crt
    command: -p 80:443 -s
</code></pre>
<pre><code class="language-bash">docker-compose up -d
</code></pre>
<h3>使用 pm2</h3>
<pre><code class="language-bash">npm i -g pm2

git clone https://github.com/nondanee/UnblockNeteaseMusic.git

cd UnblockNeteaseMusic

# 替换 CA 证书 server.crt 和 server.key

pm2 start app.js -p 80:443 -s
</code></pre>
<h2>2. Clash 配置</h2>
<pre><code class="language-yaml"># yml 版本
Rule:
  - DOMAIN-SUFFIX,163.com,NCM
  - PROCESS-NAME, NeteaseMusic,NCM

Proxy:
  - name: 🎵 NCM
    type: http
    server: &#x3C;Server-IP>
    port: &#x3C;Server-Port> # 对应上方部署对应的80端口
</code></pre>
<h3>mixin</h3>
<pre><code class="language-javascript">module.exports.parse = ({ content, name, url }, { yaml, axios, notify }) => {
  const myProxies = [
    { name: '🎵 NCM', type: 'http', server: &#x3C;Server-IP>, port: &#x3C;Server-Port>}
  ]

  const myRules = [
    'DOMAIN-SUFFIX,163.com,🎵 NCM',
    'PROCESS-NAME, NeteaseMusic, 🎵 NCM'
  ]

  const extra = {
    proxies: [...myProxies, ...content.proxies],
    rules: [...myRules, ...content.rules]
  }

  return { ...content, ...extra }
}
</code></pre>
<h2>3. 网易云音乐</h2>
<h4>UWP 版</h4>
<p>保持 http 请求，可在关于网易云音乐中，左键点击 logo 5 次后迅速右键点击 1 次设置网址</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[Rxjs实践]]></title>
            <link>https://dndxdnd.com//blog/rxjsinangular</link>
            <guid>https://dndxdnd.com//blog/rxjsinangular</guid>
            <pubDate>Thu, 05 Aug 2021 12:13:00 GMT</pubDate>
            <content:encoded><![CDATA[<h2>取消订阅</h2>
<blockquote>
<p>订阅被建立之后如果没有及时取消订阅，就会容易造成内存泄漏，变成Zombie Subscription</p>
</blockquote>
<p>在 Angular 项目中，常用到的订阅以及是否需要调用 unsubscribe() 取消订阅，有以下几种：</p>
<ul>
<li>Angular 中通过 HttpClient 执行 Http Request 返回的 Observables，订阅这些 Observables 拿到 API 返回的数据，不需要调用 unsubscribe() 取消订阅。i</li>
<li>Angular AsyncPipe，不需要调用 unsubscribe()取消订阅。</li>
<li>通过 Subject，BehaviorSubject，AsyncSubject，ReplaySubject 在各个 Component 之间通信，需要调用 unsubscribe()取消订阅。</li>
<li>RxJS 自带的一些操作符：take，takeWhile，first 等等，不需要调用 unsubscribe()取消订阅。</li>
</ul>
<h3>takeUntil</h3>
<p>使用takeUntil可以控制什么时候需要订阅，加入stop$流判断，优化unsubscribe</p>
<pre><code class="language-typescript">class MyGenericComponent extends SomeFrameworkComponent {

  onMount() {
    const data$ = this.getData();
    const cancelBtn = this.element.querySelector('.cancel-button');
    const rangeSelector = this.element.querySelector('.rangeSelector');
    const cancel$ = Observable.fromEvent(cancelBtn, 'click');
    const range$ = Observable.fromEvent(rangeSelector, 'change')
      .map(e => e.target.value);

    const stop$ = Observable.merge(cancel$, range$.filter(x => x > 500))
    this.subscription = data$.takeUntil(stop$).subscribe(data => this.updateData(data));
  }
    
   getData(data) {
       console.log('test', data)
   }
}
</code></pre>
<h3>创建一个DestroyService</h3>
<blockquote>
<p>via Twitter@Waterplea</p>
</blockquote>
<p>创建一个DestroyService，其他地方需要设置取消订阅时只需注入该服务，加入takeUntil就好了</p>
<pre><code class="language-typescript">// 实现
@Injectable()
export class DestroyService extends Observable&#x3C;void> implements OnDestroy {
	private readonly life$ = new Subject&#x3C;void>()
    
    constructor() {
		super(subscriber => this.life$.subscribe(subscriber))
    }
    
    ngOnDestroy() {
        this.life$.next()
        this.life$.complete()
    }
}
</code></pre>
<pre><code class="language-typescript">// 使用
@Directive({
  selector: '[sticky]',
  providers: [DestroyService]
})
export class StickyDirective {

  constructor(
    @Inject(WINDOW) windowRef: Window,
      rd2: Renderer2,
      destroy$: DestroyService,
      { nativeElement }: ElementRef&#x3C;HTMLElement>
  ) {
    fromEvent(windowRef, 'scroll')
      .pipe(
        map(() => windowRef.scrollY),
        pairwise(),
        map(([prev, next]) => next &#x3C; THRESHOLD || prev > next),
        distinctUntilChanged(),
        // 使用destroy$判断是否进入OnDestroy了
        takeUntil(destroy$),
        startWith(true)
      )
      .subscribe(stuck => {
        rd2.setAttribute(nativeElement, 'data-stuck', JSON.stringify(stuck))
      })
  }

}
</code></pre>
<h2>前后值一起处理</h2>
<p><em>pairwise</em> - 将当前值和上一个值放进一个数组里返回</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
        <item>
            <title><![CDATA[迷人的 Twin Peaks，迷人的 大卫林奇]]></title>
            <link>https://dndxdnd.com//blog/twinpeaks</link>
            <guid>https://dndxdnd.com//blog/twinpeaks</guid>
            <pubDate>Sun, 16 Aug 2020 10:40:05 GMT</pubDate>
            <content:encoded><![CDATA[<p><img src="https://cdn.donaldxdonald.xyz/blog/twinPeaks/MainTitle.png" alt="MainTitle"></p>
<blockquote>
<p>"双峰镇，人人都在外遇，人人都是名侦探，人人都想插一脚，人人都以为自己在恋爱，大部分都是变态。"</p>
</blockquote>
<p>断断续续地看完了《双峰》（Twin Peaks），真的是永远猜不到大卫林奇在想啥。一个命案引出种种事件，人物、内容非常多，典型大卫·林奇风格——以离奇和梦幻隐喻了生活里那些常见片段，错综复杂的人物关系，神秘的剧情，诡异的配乐，黑色幽默的讽刺，虽然节奏很慢，但情节却相当紧凑。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/twinPeaks/DavidLynch.jpg" alt="DavidLynch"></p>
<p>刚看的时候会觉得 “风格怎么那么像《锈湖》《心灵杀手》《维吉尼亚》...” “Laura 的人设怎么这么像《奇异人生》的 Rachel ...” “怎么感觉和《怪奇物语》某些方面有些相似...”等等等等。看完后经了解才知道都是致敬《双峰》的。</p>
<ul>
<li>
<p>锈湖里的 Mr. Owl、侦探 Dale 和各种特效、音效、美术风格</p>
</li>
<li>
<p>奇异人生里的 FireWalk 乐队（对应双峰里的 <em>Fire Walk With Me</em>）</p>
</li>
<li>
<p>Chloe 的车牌 <em>TWN PKS</em></p>
</li>
<li>
<p>Chloe 妈妈开的 <em>Two Whales Diner</em>（对应 Double R Diner ）</p>
</li>
<li>
<p>都是用小镇交际花失踪/遇难作为 <em>麦格芬</em> 引起的一系列事情</p>
</li>
</ul>
<p><img src="https://cdn.donaldxdonald.xyz/blog/twinPeaks/DamnGoodCooper.jpg" alt="DamnGoodCooper"></p>
<p>​</p>
<p>第一季的体验是最好的，把一个盛产绿帽的世外桃源描绘得像是真的有这个地方一样。第一季对双峰镇每个人物的刻画可谓煞费苦心，Cooper 破案虽然不是很利索，但故事正好也借机能把小镇中环环相扣的人际关系梳理清楚。</p>
<p>​	第二季就感觉拉垮了，有点拖沓了，也是后面才知道是幕后制作的原因，林奇的理念以及预算问题导致剧被 ABC 砍了（BTW，第二季的 Annie 真的可惜了）。电影 <em>Fire Walk With Me</em> 和第三季就真的满满的 <a href="https://en.wiktionary.org/wiki/Lynchian">林奇感</a> 了。电影版给我的惊喜就是 David Bowie 的出演，他在里面饰演的是个重要人物 —— Agent Phillip Jeffries，很可惜第三季没有他了。</p>
<p>第二季最后 Laura 的一句 "  25 年后见 " 就真的让林奇把这一时间实现了，90 年的第一季第二季，17 年的第三季，真有你的啊大卫林奇。上一个这么玩的我就知道《猜火车》。第三季直接就变成了双峰宇宙了，场景不局限于双峰镇，扩展到了其他城市，世界观一下子大了起来。结尾 Cooper 的一句“今夕是何年”把我给问傻了，就这么结束了？不过也对，林奇的结尾不需要句号，只需要省略号。</p>
<p><img src="https://cdn.donaldxdonald.xyz/blog/twinPeaks/BlackLodge.jpg" alt="BlackLodge"></p>
<p>侦探、解谜、爱情、美食、肥皂剧、惊悚、喜剧、超现实、科幻、外星人、附身等等元素都能在这里看到。看完之后还是意犹未尽啊，每一集舍不得跳的片头，Cooper 的大拇指和 Damn Good Coffee。</p>
]]></content:encoded>
            <author>donaldxdonald@duck.com (Donald Mok)</author>
        </item>
    </channel>
</rss>