﻿<?xml version="1.0" encoding="utf-8"?><feed xml:base="http://gianthard.rocks/" xmlns="http://www.w3.org/2005/Atom"><title type="text">网上冲浪指南</title><subtitle type="text">Zeeko's Blog</subtitle><id>http://gianthard.rocks</id><updated>2022-06-21T13:31:49Z</updated><author><name>Zeeko Zhu</name><uri>http://gianthard.rocks</uri><email>vaezt@outlook.com</email></author><link rel="alternate" href="http://gianthard.rocks/feed" /><icon xmlns="">http://gianthard.rocks/favicon-192x192.png</icon><entry><id>http://gianthard.rocks/a/90</id><title type="text">Awesome nodejs dev</title><summary type="html">&lt;div class="paragraph"&gt;
&lt;p&gt;这里收集了一些我目前在用的一些 npm package，通过使用这些 npm package ，你可以快速地实现一个功能完善的 node cli 工具，帮助你的想法尽快落地。&lt;/p&gt;
&lt;/div&gt;</summary><published>2022-06-21T13:31:49Z</published><updated>2022-06-21T13:31:49Z</updated><link href="http://gianthard.rocks/a/90" /><content type="html">&lt;div id="toc" class="toc"&gt;
&lt;div id="toctitle"&gt;内容导航&lt;/div&gt;
&lt;ul class="sectlevel1"&gt;
&lt;li&gt;&lt;a href="#_cli_library"&gt;CLI library&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_bash_的替代品"&gt;Bash 的替代品&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_提升_typescript_开发体验"&gt;提升 TypeScript 开发体验&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_提高效率的全局工具"&gt;提高效率的全局工具&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id="preamble"&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;这里收集了一些我目前在用的一些 npm package，通过使用这些 npm package ，你可以快速地实现一个功能完善的 node cli 工具，帮助你的想法尽快落地。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_cli_library"&gt;CLI library&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;帮助你实现一个 CLI 工具必备的基础功能。&lt;/p&gt;
&lt;/div&gt;
&lt;table class="tableblock frame-all grid-all stretch"&gt;
&lt;colgroup&gt;
&lt;col style="width: 50%;"&gt;
&lt;col style="width: 50%;"&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;a href="https://github.com/tj/commander.js"&gt;commander&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;一个功能完善、符合直觉的命令行参数解析库&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;a href="https://github.com/pimterry/loglevel"&gt;loglevel&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;轻量易扩展的日志库，主要功能是按日志级别过滤输出&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;a href="https://github.com/chalk/chalk"&gt;chalk&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;给你的用户一点颜色看看&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_bash_的替代品"&gt;Bash 的替代品&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Bash 脚本很强大但也很难编写，这些工具帮助你获得 Bash 的超级能力但不需要你学习奇怪的 Bash 语法&lt;/p&gt;
&lt;/div&gt;
&lt;table class="tableblock frame-all grid-all stretch"&gt;
&lt;colgroup&gt;
&lt;col style="width: 50%;"&gt;
&lt;col style="width: 50%;"&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;a href="https://github.com/google/zx"&gt;zx&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;给你带来更加友好的 shell 互操作开发体验&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;a href="https://github.com/sindresorhus/globby"&gt;globby&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;简单实用的 glob 匹配库&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;a href="https://github.com/node-modules/compressing"&gt;compressing&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;div class="content"&gt;&lt;div class="paragraph"&gt;
&lt;p&gt;压缩、解压一站式解决方案&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;支持 &lt;code&gt;tgz&lt;/code&gt; &lt;code&gt;tar&lt;/code&gt; &lt;code&gt;gzip&lt;/code&gt; &lt;code&gt;zip&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支持 Stream API、Promise API&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_提升_typescript_开发体验"&gt;提升 TypeScript 开发体验&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;TypeScript 几乎成为了 JavaScript 的现在跟未来，谁能拒绝 TypeScript 带来的良好的开发体验呢？&lt;/p&gt;
&lt;/div&gt;
&lt;table class="tableblock frame-all grid-all stretch"&gt;
&lt;colgroup&gt;
&lt;col style="width: 50%;"&gt;
&lt;col style="width: 50%;"&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;a href="https://github.com/evanw/esbuild"&gt;esbuild&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;div class="content"&gt;&lt;div class="paragraph"&gt;
&lt;p&gt;开箱即用的 tsc 替代品&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;支持 CommonJS / ESM&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;支持创建 bundle，将 cli 工具打包为单个文件&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;a href="https://github.com/swc-project/jest"&gt;@swc/jest&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;div class="content"&gt;&lt;div class="paragraph"&gt;
&lt;p&gt;使用 TypeScript 编写 jest 测试的最佳选择&lt;/p&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_提高效率的全局工具"&gt;提高效率的全局工具&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;除了上面安装在项目中的库，这些需要全局安装的工具也能够解决开发过程中的痛点，提高开发效率。&lt;/p&gt;
&lt;/div&gt;
&lt;table class="tableblock frame-all grid-all stretch"&gt;
&lt;colgroup&gt;
&lt;col style="width: 50%;"&gt;
&lt;col style="width: 50%;"&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;a href="https://volta.sh/"&gt;volta&lt;/a&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;div class="content"&gt;&lt;div class="paragraph"&gt;
&lt;p&gt;nvm 的最佳替代品&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在多个项目之间自动切换 nodejs 版本&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无痛地管理全局安装的 npm tools&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;</content></entry><entry><id>http://gianthard.rocks/a/89</id><title type="text">二〇二二年，让你的双拼输入法快人一步</title><summary type="html">&lt;div class="paragraph"&gt;
&lt;p&gt;双拼输入法将声母、韵母分别映射到单个按键上，使得用户最多通过两次按键就确定一个音节，极大的减少了击键次数，提高了输入效率。但是双拼输入法并没有解决拼音输入法最令人头疼的问题——同音字选重困难。声笔系列输入法针对这个问题给出了一个高效的解决方案——在完成拼音的输入后，追加可选的笔画来对候选结果选重，这一点小小的改进，就可以将大多数情况下的重码降低到个位数。&lt;/p&gt;
&lt;/div&gt;</summary><published>2022-05-25T13:46:47Z</published><updated>2022-05-25T13:46:47Z</updated><link href="http://gianthard.rocks/a/89" /><content type="html">&lt;div id="toc" class="toc"&gt;
&lt;div id="toctitle"&gt;内容导航&lt;/div&gt;
&lt;ul class="sectlevel1"&gt;
&lt;li&gt;&lt;a href="#_声笔自然"&gt;声笔自然&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_字词分流"&gt;字词分流&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_自动上屏"&gt;自动上屏&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_编码反查"&gt;编码反查&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_实际体验"&gt;实际体验&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_深入了解声笔系列"&gt;深入了解声笔系列&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;双拼输入法将声母、韵母分别映射到单个按键上，使得用户最多通过两次按键就确定一个音节，极大的减少了击键次数，提高了输入效率。但是双拼输入法并没有解决拼音输入法最令人头疼的问题——同音字选重困难。例如，用拼音输入法输入“施氏食狮史”会浪费大量的时间在选重操作上，用户不得不费力地从几十个候选词中选出这五个字。声笔系列输入法针对这个问题给出了一个高效的解决方案——在完成拼音的输入后，追加可选的笔画来对候选结果选重，这一点小小的改进，就可以将大多数情况下的重码降低到个位数。除了笔画筛选功能之外，声笔系列输入法还专门为字词设计了独立的编码规则，进一步提高了输入效率。如果你刚好正在使用双拼输入法，那么不妨了解一下声笔系列码对主流双拼输入法的改造加强版：声笔自然、声笔小鹤，最大程度保留你现有的肌肉记忆，最小成本提高你的输入效率。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_声笔自然"&gt;声笔自然&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;声笔自然是声笔系列输入法对自然码双拼的改造方案。我本人之前是微软双拼用户，因为微软双拼方案跟自然码方案比较接近，所以我选择的是声笔系列码中的声笔自然输入方案。声笔自然对自然码做了如下改造：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;使用“v”引导单元音音节，例如“安”的编码是“vj”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;强制开启模糊音，zh、ch、sh 被放到了对应的平舌音上&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在声母韵母之后，可以添加可选的笔画辅助码，通过汉字的第一二笔来选重&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;笔画辅助码使用 e（横、提等）u（撇）i（竖、竖提等）o（捺、点等）a（其他笔画） 表示&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用元音字母而不是数字键选择候选项&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;在大多数情况下，通过“声韵笔笔”的编码可以将候选项在 GBK 字符集范围内缩小到 10 个以内，因此，声笔自然只使用 5 个元音字母（a, e, i, o, u）加上一个翻页键（Tab）来选择候选项。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;通过添加笔画筛选功能，声笔自然输入法解决了 90% 情况下的重码问题，但这让确定单个汉字的码长从双拼的 2 增加到了 4，这无疑会增加击键次数，降低输入效率。为了让用户输入更加高效，声笔输入法为单字跟词组设计了独立的简码规则，在降低码长的同时仍然保持了较低的重码率。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_字词分流"&gt;字词分流&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;在声笔自然中，有一些常用汉字被称为“一简字”，例如：我、你、他、的、一、个、人，这些一简字的编码就是其对应声母。类似的还有二简词，编码格式是“声韵声”，例如“现在”的编码就是“xmz”。这些简字、简词的作用就是在我们输入常见字词的时候帮助减少击键次数。简字、简词并不需要我们刻意去记忆，在日常使用的过程中，我们可以通过观察候选词来认识它们。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;一简字、二简词都是声笔自然内置的不可修改的搭配，显然不满足不同用户在不用输入场景下的需求。为了让输入法能够懂用户的心意，声笔自然提供了自动组词功能。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;与常见的“输入预测”+“动态词频”功能不同，声笔自然的组词功能极具特色。它会为用户连续输入的字词组词，例如，当我在第一次输入“声笔”的时候，因为词库中并不包含这个生词，我只能分别输入“声”（sge）、“笔”（biu）。当我下一次想要输入“声笔”的时候，我只需要输入“sgbi”即可，声笔自然自动地为我组好了词，省下了两次输入笔画筛选的按键。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;如果我想输入“声笔自然”，在第一次，我需要分开输入“声笔”（sgbi）、“自然”（zirj），之后再次输入只需要输入“sbzr”即可，这时，“声笔自然”就会作为唯一候选项出现在我的候选列表当中。之所以可以做到如此“懂我心意”，是因为声笔自然为词组设计了独立的编码规则：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;二字词：声韵声韵&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;三字词：声声声韵&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多字词：前三字的声母跟最后一个字的声母&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;如果自动组词的编码出现了重码，我们仍然可以继续往后追加首字的辅助码（第一二笔）来筛选。这样，在绝大多数情况下，我们只需要 4~6 次按键就可以唯一确定一个词组。需要注意的是，为了避免自动组词干扰用户熟悉的候选列表，新词默认排在候选列表的末尾，只有在用户通过（Shift + Tab）跳转到末尾选中过一次后，新词才会出现在候选列表的开头。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;对照声笔自然的单字编码格式“声韵笔笔”，我们还可以发现一个特点，如果我们输入的第三个按键是声母，那么我们想要输入的只会是词组，而不可能是单字，因为单字的第三码只能是笔画（即元音字母）。这样，在我们想要输入词组的时候，单字就不可能出现在候选列表中，从而进一步缩短了候选列表长度，节省了选重的时间。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_自动上屏"&gt;自动上屏&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;声笔自然采用了一种“过了这村，就没这店”的字词编码方式，例如一简字“人”的编码是“r”，如果输入完整编码“rfuo”其实是无法筛选到“人”字的。又例如我们在上面新造的“声笔自然”的编码是“sbzr”，如果我们在它后面继续追加笔画辅助码，也是无法筛选到这个词的。简单来说，如果一个字词出现在了候选列表的首位，那么继续补全编码很可能会让我们错过它。之所以这样设计，是为了实现“自动上屏”的功能。例如，我想要输入“声笔自然输入法”（对应的编码是“sbzrsrfa”），当我完成“sbzr”的输入之后，候选列表中的首位就是“声笔自然”，如果我继续输入“s”的话，“声笔自然”就会自动提交上屏，并且立即开始展示“s”的候选项。如此一来，在输入长句的时候，就可以避免大量的空格提交上屏的操作，从而降低输入过程中的按键次数。自动上屏的规则不止于此，更多的自动上屏触发条件可以参考官方文档。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_编码反查"&gt;编码反查&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;因为声笔自然使用汉字的第一笔跟第二笔作为辅助码，在使用过程中难免会遇到提笔忘字的情况——知道读音，但是不清楚哪一笔是起笔，这个时候就可以使用拼音反查功能来查询编码。例如，我不清楚“字”的第二笔是什么，我只要输入“azi”（先输入“a”引导拼音反查，然后输入全拼）就可以查询到其对应的声笔自然编码了。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;同样的，如果你遇到了不会念的生僻字，那么字海两分反查法就很有用，例如，我不认识“鞿”，只需要输入“igeji”（先输入“i”引导字海两分反查，然后输入组成部分的全拼）。除了用来反查编码外，字海两分法也是声笔自然中输入 GBK 范围以外汉字的唯一方式，虽然在日常使用场景中基本上不会遇到就是了。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;声笔自然还支持笔画反查，例如“巿”（注意，这不是“市”），可以直接输入笔画来反查编码：“eiai”（表示横、竖、折、竖）。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_实际体验"&gt;实际体验&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;笔画筛选、字词分流、自动上屏、编码反查，这些对自然码的改动从精准筛选、降低码长、减少按键等几个方面提高了原本双拼方案的输入效率，那么对于一个习惯了双拼的用户来说，上手体验如何呢？让我们来看几个例子：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;人名往往是离散程度很高的文本，通过笔画筛选，可以极大的提高输入的精度，降低选重的难度；对于常输的人名，那么自动组词功能可以为你省下很多时间。&lt;/p&gt;
&lt;/div&gt;
&lt;table class="tableblock frame-all grid-all stretch"&gt;
&lt;caption class="title"&gt;Table 1. 人名输入&lt;/caption&gt;
&lt;colgroup&gt;
&lt;col style="width: 33.3333%;"&gt;
&lt;col style="width: 33.3333%;"&gt;
&lt;col style="width: 33.3334%;"&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;内容&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;初见编码&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;缩短编码&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;格根哈斯&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;code&gt;geei gfe hasi&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;code&gt;gghs&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;王嘉铭&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;code&gt;whe jwe myue&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;code&gt;wjmy&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;吴佳明&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;code&gt;wui jwuia my&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;code&gt;wjmya&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;长句可以分为日常用语跟领域专用两种。日常用语的输入体验跟双拼输入法差不多，如果愿意练习，那么声笔自然的简字、简词、自动上屏可以省下很多按键次数。另一方面，输入专业词汇时，就算没有预装专业词库，笔画筛选也能够帮助你快速、精准的输入新词，而一旦新词输入过了一遍，之后就可以通过词组编码快速的打出，从而丰富你的专属词库。&lt;/p&gt;
&lt;/div&gt;
&lt;table class="tableblock frame-all grid-all stretch"&gt;
&lt;caption class="title"&gt;Table 2. 长句&lt;/caption&gt;
&lt;colgroup&gt;
&lt;col style="width: 33.3333%;"&gt;
&lt;col style="width: 33.3333%;"&gt;
&lt;col style="width: 33.3334%;"&gt;
&lt;/colgroup&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;内容&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;编码&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;备注&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;生活中只有一种英雄主义，那就是在认清生活真相之后依然热爱生活&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;code&gt;sgho zs ziyb yizs yxzya，njsi z rfqy sgho zfxd zihb yirj revl sgho&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;跟普通的双拼输入法差不多&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;一个单子说白了不过就是自函子范畴上的一个幺半群而已&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;code&gt;yg djziasbleebugojs ziuhjazifjcbsh d yg ykaabjoqp vryi&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;在双拼的基础上运用了自动上屏的技巧&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;余虽好修姱以鞿羁兮，謇朝谇而夕替。&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;code&gt;yuu svi hk xq invkua yi igeji jiiaa xiuoe，jmooo zkeia svoae v xiua tiee&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;因为不认识“姱”、“鞿”、“謇”、“谇”，所以用字海两分反查输入&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;输入效率方面，我在使用微软双拼的时候，中文输入速度在 60 字每分钟左右，切换到声笔自然后，经过大概两天多的练习，打字速度已经提高到了 70 字每分钟。经过近 1 个半月的使用，已经养成了不少的用户词条，在跟同事讨论工作的时候可以非常高效的输入工作上的“黑话”，虽然还算不上指哪打哪，但再也不用频繁的翻候选词列表了。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;总的来说，声笔自然对于双拼用户来说还是非常容易上手的，需要注意的只有“自动上屏”这个特性，当输入的编码长度小于 3 就已经定位到想要的字词的时候，记得用空格上屏。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_深入了解声笔系列"&gt;深入了解声笔系列&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;因为篇幅原因，声笔自然输入法还有一些其他的功能特性本文没有提到，这些功能对提高输入效率、降低学习难度有很大的帮助，例如：续码顶屏、声笔自整（声笔自然的整句输入版本）等。如果你对声笔系列码感兴趣的话，可以阅读 &lt;a href="https://sbxlm.gitee.io/about/"&gt;官网文档&lt;/a&gt;，了解声笔输入法背后的设计理念跟使用技巧。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content></entry><entry><id>http://gianthard.rocks/a/88</id><title type="text">2022 年，给我来一份 Chromium 38</title><summary type="html">&lt;div class="paragraph"&gt;
&lt;p&gt;前有老旧浏览器，接下来 nw.js 很有用。&lt;/p&gt;
&lt;/div&gt;</summary><published>2022-03-17T14:52:40Z</published><updated>2022-03-17T14:52:40Z</updated><link href="http://gianthard.rocks/a/88" /><content type="html">&lt;div id="toc" class="toc"&gt;
&lt;div id="toctitle"&gt;内容导航&lt;/div&gt;
&lt;ul class="sectlevel1"&gt;
&lt;li&gt;&lt;a href="#_失败案例"&gt;失败案例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_nw_js"&gt;nw.js&lt;/a&gt;
&lt;ul class="sectlevel2"&gt;
&lt;li&gt;&lt;a href="#_可能会缺失的共享库"&gt;可能会缺失的共享库&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id="preamble"&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;最近在做一个劝用户远离老旧浏览器的功能，大概就是在用户使用非常老旧的浏览器访问时，一旦脚本出错了，就提示他升级到最新版本的浏览器。在进行功能自测的时候，我发现想要在 2022 年的 Linux 桌面上运行老旧浏览器并不是一件容易的事情。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_失败案例"&gt;失败案例&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;我做了很多尝试，大多以失败告终，要么是方向错了，要么就是开发体验太差。但我还是要把这些失败经历写下来，或许对下一个看到这篇的博文的人有所启发。&lt;/p&gt;
&lt;/div&gt;
&lt;table class="tableblock frame-all grid-all stretch"&gt;
&lt;colgroup&gt;
&lt;col style="width: 50%;"&gt;
&lt;col style="width: 50%;"&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class="tableblock halign-left valign-top"&gt;方案&lt;/th&gt;
&lt;th class="tableblock halign-left valign-top"&gt;失败原因&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;在 Windows10 虚拟机中安装旧版本浏览器&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;开发体验差，卡顿且笔记本发热严重 &lt;span class="heimu"&gt;，估计是我没有调教好 VM Player&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Palemoon 浏览器&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;被他复古外观欺骗了，虽然 UI 很老，但是内核非常先进，支持 ES2021，不满足我的需求&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;Electron&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;最早的 Electron@1.3.1 在如今的 Arch Linux 上已经跑不起来了 😢&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;使用 Wine 自带的 IE&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;span class="line-through"&gt;太懒了，没有搞&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_nw_js"&gt;nw.js&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;node-webkit，后改名 nw.js，是一个用来编写浏览器套壳 App 的工具，大概在 2014 年就发布了第一个版本。它基于 Chromium 开发，并且打通了 Chromium 与 node.js 的壁垒，使得我们可以在浏览器中调用 node.js 模块。但对于我来说，这不是重点，重点是我们可以从 nw.js 的官网上下载非常旧的免安装 Chromium。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;经过尝试后，我选择了 &lt;a href="http://dl.nwjs.io/v0.11.6/"&gt;0.11.6 版本的 nw.js&lt;/a&gt; 作为老旧浏览器测试环境。只需几行代码，就可以让我们的网站在旧版本的 Chromium 中运行。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;package.json&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-json" data-lang="json"&gt;{
  &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;legacy-browser&amp;quot;&lt;/span&gt;,
  &lt;span class="hljs-attr"&gt;&amp;quot;main&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;index.html&amp;quot;&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;index.html&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span class="hljs-meta"&gt;&amp;lt;!DOCTYPE &lt;span class="hljs-meta-keyword"&gt;html&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;html&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;head&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;title&lt;/span&gt;&amp;gt;&lt;/span&gt;Hello World!&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;title&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;head&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;style&lt;/span&gt; &lt;span class="hljs-attr"&gt;type&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;text/css&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span class="css"&gt;
  &lt;span class="hljs-selector-tag"&gt;html&lt;/span&gt;, &lt;span class="hljs-selector-tag"&gt;body&lt;/span&gt;, &lt;span class="hljs-selector-tag"&gt;iframe&lt;/span&gt; {
    &lt;span class="hljs-attribute"&gt;width&lt;/span&gt;: &lt;span class="hljs-number"&gt;100%&lt;/span&gt;;
    &lt;span class="hljs-attribute"&gt;height&lt;/span&gt;: &lt;span class="hljs-number"&gt;100%&lt;/span&gt;;
  }
  &lt;/span&gt;&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;style&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;script&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span class="javascript"&gt;
  &lt;span class="hljs-keyword"&gt;var&lt;/span&gt; win = nw.Window.get();
  win.showDevTools();
  &lt;/span&gt;&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;script&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;body&lt;/span&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="hljs-tag"&gt;&amp;lt;&lt;span class="hljs-name"&gt;iframe&lt;/span&gt; &lt;span class="hljs-attr"&gt;src&lt;/span&gt;=&lt;span class="hljs-string"&gt;&amp;quot;http://example.com&amp;quot;&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;iframe&lt;/span&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;body&lt;/span&gt;&amp;gt;&lt;/span&gt;
&lt;span class="hljs-tag"&gt;&amp;lt;/&lt;span class="hljs-name"&gt;html&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_可能会缺失的共享库"&gt;可能会缺失的共享库&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;在 Arch Linux 上使用旧版本的 nw.js 时可能会因为缺少过时的共享库而报错，这些库一般是 libgconf 跟 libudev.0。只需要安装对应的 package 即可：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh" data-lang="sh"&gt;$ yay -S libudev0-shim archlinuxcn/gconf&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content></entry><entry><id>http://gianthard.rocks/a/87</id><title type="text">如何编写 Makefile</title><summary type="html">&lt;div class="paragraph"&gt;
&lt;p&gt;经过一段时间的使用，我决定将 Makefile 加入到我的必备技术列表。这篇文章将向你介绍 Makefile 的基本编写方法，以及一个在现代前端项目中的使用样例。相信你在看完之后，就会像我一样爱上 Makefile，虽然某些时候它不是最好的方案，但在你没有更好的方案时，它一定不会让你失望。&lt;/p&gt;
&lt;/div&gt;</summary><published>2022-03-13T13:44:25Z</published><updated>2022-03-13T13:44:25Z</updated><link href="http://gianthard.rocks/a/87" /><content type="html">&lt;div id="toc" class="toc"&gt;
&lt;div id="toctitle"&gt;内容导航&lt;/div&gt;
&lt;ul class="sectlevel1"&gt;
&lt;li&gt;&lt;a href="#_配方"&gt;配方&lt;/a&gt;
&lt;ul class="sectlevel2"&gt;
&lt;li&gt;&lt;a href="#_配方之间的依赖关系"&gt;配方之间的依赖关系&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#_变量"&gt;变量&lt;/a&gt;
&lt;ul class="sectlevel2"&gt;
&lt;li&gt;&lt;a href="#_环境变量"&gt;环境变量&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_变量覆盖"&gt;变量覆盖&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#_书写命令"&gt;书写命令&lt;/a&gt;
&lt;ul class="sectlevel2"&gt;
&lt;li&gt;&lt;a href="#_执行多行命令的脚本"&gt;执行多行命令的脚本&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#_一些特殊的内建配方"&gt;一些特殊的内建配方&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_参考"&gt;参考&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id="preamble"&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;经过一段时间的使用，我决定将 Makefile 加入到我的必备技术列表。这篇文章将向你介绍 Makefile 的基本编写方法，以及一个在现代前端项目中的使用样例。相信你在看完之后，就会像我一样爱上 Makefile，虽然某些时候它不是最好的方案，但在你没有更好的方案时，它一定不会让你失望。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="💬"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
make 有很多种实现，在撰写本文时，参考的是 GNU make 的相关文档。
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_配方"&gt;配方&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Makefile 中最基本的构成元素就是配方（Recipe），配方的作用就是用来说明如何构建软件某个产物（Target），每个配方都由三个元素组成：target、prerequisites、commands，一个简单的配方如下：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span class="hljs-section"&gt;dist: node_modules # target: prerequisites&lt;/span&gt;
	npm run build    &lt;span class="hljs-comment"&gt;# commands&lt;/span&gt;
	npm run tsc&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;这个配方描述了构建 &lt;code&gt;dist&lt;/code&gt; 目录（target）的方式：在构建之前，make 需要确保它的依赖 &lt;code&gt;node_modules&lt;/code&gt; （prerequisites）已经被构建且更新了，然后再执行两个 npm 命令（commands）来构建产物。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;配方的 target 也可以不是事实存在于硬盘上的文件，例如：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span class="hljs-section"&gt;test: node_modules&lt;/span&gt;
	npm run test&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;这个配方描述了执行测试的方式：先更新 &lt;code&gt;node_moidules&lt;/code&gt; ，然后执行测试脚本，整过程不会产生并不会产生名为 &lt;code&gt;test&lt;/code&gt; 的文件。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;配方的 prerequisites 也跟 target 类似，除了可以是实际存在的文件之外，还可以是其他的 target，例如：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span class="hljs-section"&gt;dist: node_modules test&lt;/span&gt;
	npm run test&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;新的用来构建 &lt;code&gt;dist&lt;/code&gt; 的配方将会同时依赖 &lt;code&gt;node_modules&lt;/code&gt; 跟 &lt;code&gt;test&lt;/code&gt; ，这样在我们执行 &lt;code&gt;make dist&lt;/code&gt; 之前，测试脚本也会被执行了。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_配方之间的依赖关系"&gt;配方之间的依赖关系&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;有了 target 跟 prerequisites，make 就可以分析各个配方之间的依赖关系，进而可以确定不同配方的执行先后顺序，更进一步，如果你的 target、prerequisites 是文件的话，make 可以通过文件的最后修改时间来找出已经过时的产物，从而实现比较精准的增量构建。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;通常，我会这么来编写一个现代的前端项目 Makefile：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span class="hljs-section"&gt;node_modules: package.json yarn.lock&lt;/span&gt;
	yarn install --fronzen-lockfile

&lt;span class="hljs-section"&gt;test: node_modules tests&lt;/span&gt;
	yarn test

&lt;span class="hljs-section"&gt;types: src node_modules&lt;/span&gt;
	yarn tsc

&lt;span class="hljs-section"&gt;dist: src node_modules test types&lt;/span&gt;
	yarn build&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;同样是执行 &lt;code&gt;make dist&lt;/code&gt; ，对于初次上手的开发者来说，make 会为他还原好 node_modules，并且自动执行测试，测试通过后再构建 &lt;code&gt;dist&lt;/code&gt; 目录。而对于经常在这个项目中摸爬滚打的熟手，只要没有更新项目的依赖，make 就会为他自动的跳过还原 &lt;code&gt;node_modules&lt;/code&gt; 的步骤，而要在 npm scripts 中实现类似的效果是非常困难的。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_变量"&gt;变量&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Makefile 中可以声明变量，变量可以使用在配方中的任何地方：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;foo := 123 &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;

&lt;span class="hljs-variable"&gt;$(foo)&lt;/span&gt;: &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
	echo &lt;span class="hljs-variable"&gt;$(foo)&lt;/span&gt; &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;声明变量 &lt;code&gt;foo&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;使用变量 &lt;code&gt;foo&lt;/code&gt; 的值作为 target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;在命令中使用 &lt;code&gt;foo&lt;/code&gt; 变量的值&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;make 在解析配方之前，会将 Makefile 中形如 &lt;code&gt;$(var)&lt;/code&gt; 的文本进行变量展开，所以与上面命令等价的是 shell 脚本是 &lt;code&gt;echo 123&lt;/code&gt; 而不是 &lt;code&gt;echo $foo&lt;/code&gt; 。因为 make 的这个特点，当我们想要在配方中使用 &lt;code&gt;$&lt;/code&gt; 时，需要用 &lt;code&gt;$$&lt;/code&gt; 来替换。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_环境变量"&gt;环境变量&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;make 默认会继承当前系统的全部环境变量，并将这些变量载入到 Makefile 中，例如：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span class="hljs-section"&gt;build:&lt;/span&gt;
	echo &lt;span class="hljs-variable"&gt;$(PATH)&lt;/span&gt; &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$(PATH)&lt;/code&gt; 将会被替换为当前的系统环境变量 &lt;code&gt;PATH&lt;/code&gt; 的值&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;Output&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; make&lt;/span&gt;
echo /usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl
/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;而在 Makefile 中声明的自定义变量则不会在命令中以环境变量的形式存在：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;foo := hello

&lt;span class="hljs-section"&gt;build:&lt;/span&gt;
	echo $$foo&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;Output&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; make&lt;/span&gt;
echo $foo&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;如果想要在命令中通过环境变量的方式来访问自定义变量，可以在变量声明前加上 `export `。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span class="hljs-keyword"&gt;export&lt;/span&gt; foo := hello

&lt;span class="hljs-section"&gt;build:&lt;/span&gt;
	echo $$foo&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;Output&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh" data-lang="sh"&gt;$ make
&lt;span class="hljs-built_in"&gt;echo&lt;/span&gt; &lt;span class="hljs-variable"&gt;$foo&lt;/span&gt;
hello&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_变量覆盖"&gt;变量覆盖&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;在执行 make 命令时，我们可以使用下面的语法通过命令行来覆盖自定义变量的值，一个常见的使用场景是这样的：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre&gt;ENV := prod

build:
	yarn build --$(ENV)&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;Output&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh" data-lang="sh"&gt;$ make
yarn build --prod

&lt;span class="hljs-variable"&gt;$make&lt;/span&gt; ENV=dev &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
yarn build --dev&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;通过命令行参数来覆盖 Makefile 中定义的 &lt;code&gt;ENV&lt;/code&gt; 变量。&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="💬"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
通过命令行设置的变量同样也可以作为环境变量在配方的命令中使用。
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_书写命令"&gt;书写命令&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;我们已经大致了解了配方的编写方法，现在让我们更进一步地来看看 make 执行命令的方式，make 会根据以下的变量来解析执行配方中的命令：&lt;/p&gt;
&lt;/div&gt;
&lt;table class="tableblock frame-all grid-all stretch"&gt;
&lt;colgroup&gt;
&lt;col style="width: 33.3333%;"&gt;
&lt;col style="width: 33.3333%;"&gt;
&lt;col style="width: 33.3334%;"&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class="tableblock halign-left valign-top"&gt;变量名&lt;/th&gt;
&lt;th class="tableblock halign-left valign-top"&gt;说明&lt;/th&gt;
&lt;th class="tableblock halign-left valign-top"&gt;默认值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;.RECIPEPREFIX&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;strong&gt;单个字符&lt;/strong&gt;，使用这个字符开头的行将被识别为配方的命令&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;kbd&gt;Tab&lt;/kbd&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;SHELL&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;命令解释器的路径，用来执行配方中的命令&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;/bin/sh&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;.SHELLFLAGS&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;命令解释器的参数&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;&lt;code&gt;-c&lt;/code&gt;&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;make 将所有以 &lt;code&gt;.RECIPEPREFIX&lt;/code&gt; 开头的行识别为命令，接着逐行调用 &lt;code&gt;SHELL&lt;/code&gt; 来执行，当执行出错的时候，make 就会中断配方的执行。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;make 在执行命令之前会输出将要执行的语句，如果你不希望 make 打印命令语句，可以在命令之前加上 &lt;code&gt;@&lt;/code&gt; 。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span class="hljs-section"&gt;build:&lt;/span&gt;
	echo hello
	@echo world&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;Output&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre&gt;$ make
echo hello
hello
world&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span class="hljs-section"&gt;build:&lt;/span&gt;
	foo=123
	echo $$foo&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;与 &lt;code&gt;@&lt;/code&gt; 类似的记号还有 &lt;code&gt;-&lt;/code&gt; ，表示允许这行命令出错。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span class="hljs-section"&gt;build:&lt;/span&gt;
	echo hello
	-exit 23
	echo world&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;Output&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre&gt;$ make
echo hello
hello
exit 23
make: [Makefile:3: build] Error 23 (ignored)
echo world
world&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_执行多行命令的脚本"&gt;执行多行命令的脚本&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;make 逐行解释并执行的命令的方式在大多数情况下还是很方便的，但如果你准备使用 sh 以外的命令解释器，例如，Python、nodejs，你就需要让 make 能够一次性解释并执行多行命令，这时你只需要声明一个名为 &lt;code&gt;.ONESHELL&lt;/code&gt; 的 target 即可。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span class="hljs-comment"&gt;&lt;mark&gt;.ONESHELL:&lt;/mark&gt;&lt;/span&gt;
SHELL = /bin/sh
.SHELLFLAGS = -ec &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;

&lt;span class="hljs-section"&gt;build:&lt;/span&gt;
	foo=32
	echo $$foo &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;配方中的脚本不再被逐行解释执行，所以需要设置 sh 在遇到命令错误的时候自动退出&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;现在可以在命令块中声明并使用 shell 变量了，因为这些语句将在同一个 sh 调用中执行&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;Output&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre&gt;foo=32
echo $foo
32&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="admonitionblock warning"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-warning" title="🚨"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
设置 &lt;code&gt;.ONESHELL&lt;/code&gt; 之后，&lt;code&gt;@&lt;/code&gt;、&lt;code&gt;—&lt;/code&gt; 修饰符只能出现在整个命令块的开头
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_一些特殊的内建配方"&gt;一些特殊的内建配方&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;到目前为止，关于 Makefile 的基本知识已经展现我们的眼前，除了这些基本通用的语法规则之外，GNU make 还有一些内置的配方，可以很方便的帮助我们实现常用的功能：&lt;/p&gt;
&lt;/div&gt;
&lt;table class="tableblock frame-all grid-all stretch"&gt;
&lt;colgroup&gt;
&lt;col style="width: 50%;"&gt;
&lt;col style="width: 50%;"&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class="tableblock halign-left valign-top"&gt;名称&lt;/th&gt;
&lt;th class="tableblock halign-left valign-top"&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;PHONY&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;将其 prerequisites 标记为伪目标，make 将始终重新构建这些目标，无论其是否需要更新&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;IGNORE&lt;/p&gt;&lt;/td&gt;
&lt;td class="tableblock halign-left valign-top"&gt;&lt;p class="tableblock"&gt;当 make 在构建其 prerequisites 的时候，忽略构建错误&lt;/p&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_参考"&gt;参考&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="ulist bibliography"&gt;
&lt;ul class="bibliography"&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://seisman.github.io/how-to-write-makefile/index.html"&gt;跟我一起写 Makefile&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.gnu.org/software/make/manual/make.html"&gt;GNU &lt;code&gt;make&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content></entry><entry><id>http://gianthard.rocks/a/86</id><title type="text">开发 UI 组件 —— 视觉回归测试</title><summary type="html">&lt;div class="paragraph"&gt;
&lt;p&gt;在开发 UI 组件库时，需要使用视觉回归测试来保证在 Design Token 变更的情况下，组件库中的组件没有被意外地修改。&lt;/p&gt;
&lt;/div&gt;</summary><published>2021-12-28T14:08:25Z</published><updated>2021-12-28T14:08:25Z</updated><link href="http://gianthard.rocks/a/86" /><content type="html">&lt;div id="toc" class="toc"&gt;
&lt;div id="toctitle"&gt;内容导航&lt;/div&gt;
&lt;ul class="sectlevel1"&gt;
&lt;li&gt;&lt;a href="#_使用_playwright_实现浏览器自动化"&gt;使用 Playwright 实现浏览器自动化&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_使用_storybook_来设置组件的状态"&gt;使用 Storybook 来设置组件的状态&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_编写_1728_个测试用例"&gt;“编写” 1728 个测试用例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_禁用_css_动画"&gt;禁用 CSS 动画&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_触发_active_选择器"&gt;触发 ':active' 选择器&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_提高截图清晰度"&gt;提高截图清晰度&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;在开发 UI 组件库时，需要使用视觉回归测试来保证在 Design Token 变更的情况下，组件库中的组件没有被意外地修改。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;以一个 Button 组件为例，其 props 的类型如下：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="hljs-keyword"&gt;export&lt;/span&gt; &lt;span class="hljs-keyword"&gt;type&lt;/span&gt; ButtonType = &lt;span class="hljs-string"&gt;&amp;#x27;primary&amp;#x27;&lt;/span&gt; | &lt;span class="hljs-string"&gt;&amp;#x27;secondary&amp;#x27;&lt;/span&gt; | &lt;span class="hljs-string"&gt;&amp;#x27;text&amp;#x27;&lt;/span&gt;;
&lt;span class="hljs-keyword"&gt;export&lt;/span&gt; &lt;span class="hljs-keyword"&gt;type&lt;/span&gt; ButtonSize = &lt;span class="hljs-string"&gt;&amp;#x27;small&amp;#x27;&lt;/span&gt; | &lt;span class="hljs-string"&gt;&amp;#x27;medium&amp;#x27;&lt;/span&gt; | &lt;span class="hljs-string"&gt;&amp;#x27;large&amp;#x27;&lt;/span&gt;;
&lt;span class="hljs-keyword"&gt;export&lt;/span&gt; &lt;span class="hljs-keyword"&gt;type&lt;/span&gt; ButtonColor = &lt;span class="hljs-string"&gt;&amp;#x27;default&amp;#x27;&lt;/span&gt; | &lt;span class="hljs-string"&gt;&amp;#x27;danger&amp;#x27;&lt;/span&gt; | &lt;span class="hljs-string"&gt;&amp;#x27;success&amp;#x27;&lt;/span&gt; | &lt;span class="hljs-string"&gt;&amp;#x27;warning&amp;#x27;&lt;/span&gt;;

&lt;span class="hljs-keyword"&gt;export&lt;/span&gt; &lt;span class="hljs-keyword"&gt;interface&lt;/span&gt; ButtonProps &lt;span class="hljs-keyword"&gt;extends&lt;/span&gt; React.HTMLAttributes&amp;lt;HTMLButtonElement&amp;gt; {
  &lt;span class="hljs-comment"&gt;/**
   * The button type.
   * &lt;span class="hljs-doctag"&gt;@default &lt;/span&gt;`secondary`
   */&lt;/span&gt;
  &lt;span class="hljs-keyword"&gt;type&lt;/span&gt;?: ButtonType;
  &lt;span class="hljs-comment"&gt;/**
   * The button color.
   * &lt;span class="hljs-doctag"&gt;@default &lt;/span&gt;`default`
   */&lt;/span&gt;
  color?: ButtonColor;
  &lt;span class="hljs-comment"&gt;/**
   * The button size.
   * &lt;span class="hljs-doctag"&gt;@default &lt;/span&gt;`medium`
   */&lt;/span&gt;
  size?: ButtonSize;
  disabled?: &lt;span class="hljs-built_in"&gt;boolean&lt;/span&gt;;
  onClick?: &lt;span class="hljs-function"&gt;(&lt;span class="hljs-params"&gt;e: React.MouseEvent&amp;lt;HTMLButtonElement&amp;gt;&lt;/span&gt;) =&amp;gt;&lt;/span&gt; &lt;span class="hljs-built_in"&gt;void&lt;/span&gt;;
  icon?: React.ReactNode;
  loading?: &lt;span class="hljs-built_in"&gt;boolean&lt;/span&gt;;
  htmlType?: &lt;span class="hljs-string"&gt;&amp;#x27;button&amp;#x27;&lt;/span&gt; | &lt;span class="hljs-string"&gt;&amp;#x27;submit&amp;#x27;&lt;/span&gt; | &lt;span class="hljs-string"&gt;&amp;#x27;reset&amp;#x27;&lt;/span&gt;;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;这样的一个 Button 展示在最终用户面前的样子将会有 1728 种。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;type * color * size * disable * loading * (default + hover + focus + active) * (icon + icon/text + text)
= 3 * 4 * 3 * 2 * 2 * 4 * 3
= 1728&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;这些可能情况如果要人手工来进行 UI 检查是不切实际的，所以必须得借助自动化工具的力量：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;能够方便的枚举出组件所有可能的状态&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;能够对浏览器中的内容截屏&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;能够对比每次运行测试时截屏的差异&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;经过我的一番面向搜索引擎调研，最终确定了我的方案：Storybook + Playwright。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_使用_playwright_实现浏览器自动化"&gt;使用 Playwright 实现浏览器自动化&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;&lt;a href="https://playwright.dev/"&gt;Playwright&lt;/a&gt; 是一款非常易于使用的浏览器自动化工具。相较于我之前使用的 Cypress &lt;span class="heimu"&gt;完全没有黑 Cypress 的意思&lt;/span&gt;，它有如下几个感知非常强烈的优势：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;零配置，不需要配置 Playwright 就能很好地运行起来&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;无限制，能够 &lt;a href="https://playwright.dev/docs/api/class-mouse"&gt;真正地执行鼠标 hover、move 等操作&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自带代码生成器，通过运行 &lt;code&gt;playwright codegen &lt;a href="http://example.com" class="bare"&gt;http://example.com&lt;/a&gt;&lt;/code&gt; 就可以将操作录制为代码&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自带 &lt;a href="https://playwright.dev/docs/test-snapshots"&gt;截屏对比功能&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;自带 TypeScript 支持，且无需配置&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_使用_storybook_来设置组件的状态"&gt;使用 Storybook 来设置组件的状态&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;一提到写团队内部的组件库，我就会想到 Stroybook，这次我就用到了它提供的 &lt;a href="https://storybook.js.org/docs/react/writing-stories/args#setting-args-through-the-url"&gt;通过 URL 参数来设置组件 Props 的功能&lt;/a&gt;。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;只要我们提供能够通过控件调整 Props 的 Story：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;export default {
  component: Button,
  title: 'Components/Button',
  args: {
    disabled: false,
    loading: false,
  },
  argTypes: {
    icon: {
      control: false,
    },
    children: {
      control: false,
    },
    loading: {
      control: { type: 'boolean' },
      defaultValue: false,
    },
  },
} as ComponentMeta&amp;lt;typeof Button&amp;gt;;

const Template: ComponentStory&amp;lt;typeof Button&amp;gt; = (args: ButtonProps) =&amp;gt; &amp;lt;Button {...args}&amp;gt;Press Me&amp;lt;/Button&amp;gt;;

export const Default = Template.bind({});

Default.args = {
  type: 'secondary',
  color: 'default',
  children: 'Press Me',
};&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;我们就可以通过修改 URL 的 query string 来控制组件的 Props，例如：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code&gt;http://localhost:9011/iframe.html?id=components-button--default&amp;amp;args=type:primary;color:success&amp;amp;viewMode=story&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;这个地址将会打开一个只渲染组件的页面，而且会自动地将组件的 props 为 &lt;code&gt;type=primary color=success&lt;/code&gt;。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_编写_1728_个测试用例"&gt;“编写” 1728 个测试用例&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;如上所述，组件测试用例是通过多个 props 取不同的值排列组合而成的，所以我们其实并不用老老实实地将所有的排列组合手写出来。我采用的是嵌套 &lt;code&gt;forof&lt;/code&gt; 循环的方式：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; stories = [&lt;span class="hljs-string"&gt;&amp;#x27;default&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;icon-text-button&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;icon-button&amp;#x27;&lt;/span&gt;];
&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; types = [&lt;span class="hljs-string"&gt;&amp;#x27;primary&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;secondary&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;text&amp;#x27;&lt;/span&gt;];
&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; sizes = [&lt;span class="hljs-string"&gt;&amp;#x27;small&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;medium&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;large&amp;#x27;&lt;/span&gt;];
&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; colors = [&lt;span class="hljs-string"&gt;&amp;#x27;default&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;success&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;warning&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-string"&gt;&amp;#x27;danger&amp;#x27;&lt;/span&gt;];
&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; loadings = [&lt;span class="hljs-literal"&gt;true&lt;/span&gt;, &lt;span class="hljs-literal"&gt;false&lt;/span&gt;];
&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; disabledStates = [&lt;span class="hljs-literal"&gt;true&lt;/span&gt;, &lt;span class="hljs-literal"&gt;false&lt;/span&gt;];

test.describe.parallel(&lt;span class="hljs-string"&gt;`button`&lt;/span&gt;, &lt;span class="hljs-function"&gt;() =&amp;gt;&lt;/span&gt; {
  &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; (&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; story &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; stories) {
    &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; (&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; &lt;span class="hljs-keyword"&gt;type&lt;/span&gt; &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; types) {
      &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; (&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; color &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; colors) {
        &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; (&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; size &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; sizes) {
          &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; (&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; loading &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; loadings) {
            &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; (&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; disabled &lt;span class="hljs-keyword"&gt;of&lt;/span&gt; disabledStates) {
              test(&lt;span class="hljs-string"&gt;&amp;#x27;write your test here&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-function"&gt;() =&amp;gt;&lt;/span&gt; {
                &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; page.goto(
                  &lt;span class="hljs-string"&gt;`http://localhost:9011/iframe.html?id=components-button--&lt;span class="hljs-subst"&gt;${story}&lt;/span&gt;&amp;amp;args=type:&lt;span class="hljs-subst"&gt;${&lt;span class="hljs-keyword"&gt;type&lt;/span&gt;}&lt;/span&gt;;color:&lt;span class="hljs-subst"&gt;${color}&lt;/span&gt;;size:&lt;span class="hljs-subst"&gt;${size}&lt;/span&gt;;loading:&lt;span class="hljs-subst"&gt;${loading}&lt;/span&gt;;disabled:&lt;span class="hljs-subst"&gt;${disabled}&lt;/span&gt;`&lt;/span&gt;
                );
              });
            }
          }
        }
      }
    }
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;这样上千个测试用例就很容易编写出来了。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_禁用_css_动画"&gt;禁用 CSS 动画&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;为了加快测试的执行，并禁用掉 &lt;code&gt;loading&lt;/code&gt; 状态的动画，在 Playwright 中可以通过下面的方法来禁用掉 css 动画。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="hljs-keyword"&gt;export&lt;/span&gt; &lt;span class="hljs-class"&gt;&lt;span class="hljs-keyword"&gt;class&lt;/span&gt; &lt;span class="hljs-title"&gt;FigmaFramePageObject&lt;/span&gt; &lt;/span&gt;{
  &lt;span class="hljs-attr"&gt;root&lt;/span&gt;: Locator;

  &lt;span class="hljs-keyword"&gt;private&lt;/span&gt; _disableCssAnimationStyleTag: ElementHandle | &lt;span class="hljs-literal"&gt;undefined&lt;/span&gt;;

  &lt;span class="hljs-function"&gt;&lt;span class="hljs-title"&gt;constructor&lt;/span&gt;(&lt;span class="hljs-params"&gt;&lt;span class="hljs-keyword"&gt;public&lt;/span&gt; page: Page&lt;/span&gt;)&lt;/span&gt; {
    &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;.root = page.locator(&lt;span class="hljs-string"&gt;&amp;#x27;#root&amp;#x27;&lt;/span&gt;);
  }

  &lt;span class="hljs-keyword"&gt;async&lt;/span&gt; &lt;span class="hljs-function"&gt;&lt;span class="hljs-title"&gt;disableCssAnimation&lt;/span&gt;(&lt;span class="hljs-params"&gt;&lt;/span&gt;)&lt;/span&gt; {
    &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;._disableCssAnimationStyleTag = &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;.page.addStyleTag({
      &lt;span class="hljs-attr"&gt;content&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;* {transition: none !important; animation: none !important;}&amp;#x27;&lt;/span&gt;,
    }); &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
  }

  &lt;span class="hljs-keyword"&gt;async&lt;/span&gt; &lt;span class="hljs-function"&gt;&lt;span class="hljs-title"&gt;enableCssAnimation&lt;/span&gt;(&lt;span class="hljs-params"&gt;&lt;/span&gt;)&lt;/span&gt; {
    &lt;span class="hljs-keyword"&gt;if&lt;/span&gt; (&lt;span class="hljs-built_in"&gt;this&lt;/span&gt;._disableCssAnimationStyleTag) {
      &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;._disableCssAnimationStyleTag.evaluate(&lt;span class="hljs-function"&gt;(&lt;span class="hljs-params"&gt;tag&lt;/span&gt;) =&amp;gt;&lt;/span&gt; tag.parentNode?.removeChild(tag)); &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
      &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;._disableCssAnimationStyleTag.dispose();
      &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;._disableCssAnimationStyleTag = &lt;span class="hljs-literal"&gt;undefined&lt;/span&gt;;
    }
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;向页面中注入一个带有 CSS 代码的 Style 标签，禁用掉所有元素的 CSS 动画&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;移除掉上面注入的 Style 标签&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_触发_active_选择器"&gt;触发 ':active' 选择器&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;对于按钮类型的组件来说，按下鼠标时，组件的样式会相应的发生改变。在 Cypress 中想要测试这种场景是很困难的，因为 Cypress 没法真实的模拟鼠标动作。不过，这对于 Playwright 来说不算什么：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; box = &lt;span class="hljs-keyword"&gt;await&lt;/span&gt; element.boundingBox();
&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; { x, y, height, width } = box!; &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
&lt;span class="hljs-keyword"&gt;await&lt;/span&gt; page.mouse.move(x + width / &lt;span class="hljs-number"&gt;2&lt;/span&gt;, y + height / &lt;span class="hljs-number"&gt;2&lt;/span&gt;); &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
&lt;span class="hljs-keyword"&gt;await&lt;/span&gt; page.mouse.down(); &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
&lt;span class="hljs-comment"&gt;// write your assertion here&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;await&lt;/span&gt; page.mouse.up(); &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;获取组件的位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;将鼠标移动到组件元素中间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;按下鼠标但不松开，这样 &lt;code&gt;:active&lt;/code&gt; 状态就会保持触发的状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;松开鼠标&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_提高截图清晰度"&gt;提高截图清晰度&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Playwright 默认不会模拟 Hi-DPI 屏幕，这会导致两个问题&lt;/p&gt;
&lt;/div&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;组件比较小的时候，细节部分的样式无法清楚的渲染出来&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;snapshot 截图清晰度很低&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;可以参考 &lt;a href="https://playwright.dev/docs/emulation#viewport"&gt;这篇文档&lt;/a&gt; 来设置 Playwright，使其输出 Hi-DPI 屏幕的截图。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content></entry><entry><id>http://gianthard.rocks/a/85</id><title type="text">我的第一台 NAS</title><summary type="html">&lt;div class="paragraph"&gt;
&lt;p&gt;之前用树莓派改造的旁路网关性能太差了，不管是互联网速度还是内网速度都很捉急。刚好家中有台吃灰的笔记本电脑（i7-6500U + 16G），于是决定趁着周末把它改造成一台 All in One NAS。&lt;/p&gt;
&lt;/div&gt;</summary><published>2021-10-19T15:42:47Z</published><updated>2021-10-19T15:42:47Z</updated><link href="http://gianthard.rocks/a/85" /><content type="html">&lt;div id="toc" class="toc"&gt;
&lt;div id="toctitle"&gt;内容导航&lt;/div&gt;
&lt;ul class="sectlevel1"&gt;
&lt;li&gt;&lt;a href="#_明确需求"&gt;明确需求&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_安装_pve"&gt;安装 PVE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_安装_openwrt"&gt;安装 OpenWRT&lt;/a&gt;
&lt;ul class="sectlevel2"&gt;
&lt;li&gt;&lt;a href="#_按设备指定网关"&gt;按设备指定网关&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#_安装_openmediavault"&gt;安装 OpenMediaVault&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_用_docker_整些花活"&gt;用 Docker 整些花活&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_成果展示"&gt;成果展示&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div id="preamble"&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;之前用树莓派改造的旁路网关性能太差了，不管是互联网速度还是内网速度都很捉急。刚好家中有台吃灰的笔记本电脑（i7-6500U + 16G），于是决定趁着周末把它改造成一台 All in One NAS。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_明确需求"&gt;明确需求&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;在一切开始之前，首先梳理一下对这台 NAS 的需求：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;可以充当旁路网关，实现全局透明赛博旅游&lt;/p&gt;
&lt;div class="olist loweralpha"&gt;
&lt;ol class="loweralpha" type="a"&gt;
&lt;li&gt;
&lt;p&gt;需要能够通过 DHCP 分设备设置网关&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最好要有 Web UI，方便更新路由器配置&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以为台式机、平板电脑、笔记本电脑提供 NAS 功能（SMB、NFS、WebDav）&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;之前在树莓派上面实现第一个需求是直接用的 dhcpd，每次修改 DHCP 配置都要先去路由器后台查看设备 MAC 地址，再手动去树莓派上改文件，麻烦且不够直观。网上一番调研后发现只有 OpenWRT 才比较容易满足我的需求，然而直接把版基本电脑装成 OpenWRT 有点大材小用，所以还得整个虚拟机来运行。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;第二个 NAS 需求的话，网上推荐的都是 OpenMediaVault 这个基于 Debian 的系统，集成了 NFS、SMB等多种文件共享方案，还提供了一个 Web 管理 UI。愿意折腾的话，还可以安装各种第三方插件拓展现有功能，甚至可以直接通过插件管理 KVM 虚拟机、docker 容器。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;最后，我选择了在宿主机上安装 PVE，一个专门用来提供虚拟化环境的系统。在 PVE 的基础上，再创建 OpenWRT 跟 OpenMediaVault 的虚拟机，专事专干。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_安装_pve"&gt;安装 PVE&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;跟着 &lt;a href="https://pve.proxmox.com/wiki/Main_Page"&gt;官方文档&lt;/a&gt; 安装就好了，没啥特别的。不过需要注意的是 PVE 安装完成后记得修改一下 systemd 的 &lt;a href="https://wiki.archlinux.org/title/Power_management#ACPI_events"&gt;电源管理配置&lt;/a&gt;，让电脑在盒盖的时候不要休眠，继续运行。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_安装_openwrt"&gt;安装 OpenWRT&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;安装 OpenWrt 也比较简单，照着 &lt;a href="https://www.moewah.com/archives/3643.html"&gt;这篇文章&lt;/a&gt; 的步骤来操作就好了。不过在配置 OpenWRT 的全局透明代理的时候，有些地方需要注意。我选用的是 Xray 的 &lt;a href="https://xtls.github.io/document/level-2/tproxy.html"&gt;全局透明代理方案&lt;/a&gt;，这个方案会拦截局域网发往 53 端口的流量，所以不存在 DNS 污染的问题。不过，官方文档中给出的配置在我们的旁路由场景上需要 &lt;a href="https://github.com/XTLS/Xray-core/issues/185#issuecomment-762613968"&gt;修改一下&lt;/a&gt;，因为 OpenWRT 自带 dnsmasq，会通过 127.0.0.1 向自己的 53 端口发起 DNS 查询，所以我们需要在官方文档的基础上进一步拦截来自 localhost 的 DNS 查询：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-sh" data-lang="sh"&gt;iptables -t mangle -N XRAY
iptables -t mangle -A XRAY -d 10.0.0.0/8 -j RETURN
iptables -t mangle -A XRAY -d 100.64.0.0/10 -j RETURN
iptables -t mangle -A XRAY -d 127.0.0.0/8 &lt;span class="hljs-comment"&gt;&lt;mark&gt;-p udp ! --dport 53&lt;/mark&gt; -j RETURN&lt;/span&gt;
iptables -t mangle -A XRAY -d 169.254.0.0/16 -j RETURN
iptables -t mangle -A XRAY -d 172.16.0.0/12 -j RETURN
iptables -t mangle -A XRAY -d 192.0.0.0/24 -j RETURN
iptables -t mangle -A XRAY -d 224.0.0.0/4 -j RETURN
iptables -t mangle -A XRAY -d 240.0.0.0/4 -j RETURN
iptables -t mangle -A XRAY -d 255.255.255.255/32 -j RETURN
iptables -t mangle -A XRAY -d 192.168.0.0/16 -p tcp ! --dport 53 -j RETURN
iptables -t mangle -A XRAY -d 192.168.0.0/16 -p udp ! --dport 53 -j RETURN
iptables -t mangle -A XRAY -p tcp -j TPROXY --on-port 12345 --tproxy-mark 1
iptables -t mangle -A XRAY -p udp -j TPROXY --on-port 12345 --tproxy-mark 1
iptables -t mangle -A PREROUTING -j XRAY

iptables -t mangle -N XRAY_SELF
iptables -t mangle -A XRAY_SELF -d 10.0.0.0/8 -j RETURN
iptables -t mangle -A XRAY_SELF -d 100.64.0.0/10 -j RETURN
iptables -t mangle -A XRAY_SELF -d 127.0.0.0/8 &lt;span class="hljs-comment"&gt;&lt;mark&gt;-p udp ! --dport 53&lt;/mark&gt; -j RETURN&lt;/span&gt;
iptables -t mangle -A XRAY_SELF -d 169.254.0.0/16 -j RETURN
iptables -t mangle -A XRAY_SELF -d 172.16.0.0/12 -j RETURN
iptables -t mangle -A XRAY_SELF -d 192.0.0.0/24 -j RETURN
iptables -t mangle -A XRAY_SELF -d 224.0.0.0/4 -j RETURN
iptables -t mangle -A XRAY_SELF -d 240.0.0.0/4 -j RETURN
iptables -t mangle -A XRAY_SELF -d 255.255.255.255/32 -j RETURN
iptables -t mangle -A XRAY_SELF -d 192.168.0.0/16 -p tcp ! --dport 53 -j RETURN
iptables -t mangle -A XRAY_SELF -d 192.168.0.0/16 -p udp ! --dport 53 -j RETURN
iptables -t mangle -A XRAY_SELF -m mark --mark 2 -j RETURN
iptables -t mangle -A XRAY_SELF -p tcp -j MARK --set-mark 1
iptables -t mangle -A XRAY_SELF -p udp -j MARK --set-mark 1
iptables -t mangle -A OUTPUT -j XRAY_SELF&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_按设备指定网关"&gt;按设备指定网关&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;为了能够享受到赛博旅游的好处，我会通过 DHCP 来把局域网内的所有设备的网关设置为旁路由，通常情况下都没啥问题。不过在某些 IoT 设备 &lt;span class="heimu"&gt;指米家 WiFi 物联网家电&lt;/span&gt; 上，就算设置了白名单规则，还是会有无法连接服务器的情况发生。这个时候就需要按设备指定网关地址了，还好这个功能在 OpenWrt 上还是比较好实现的。只需要 &lt;a href="https://www.right.com.cn/forum/thread-355909-2-1.html"&gt;修改 dhcp 配置&lt;/a&gt;，给需要直连的设备标记上 tag：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-config" data-lang="config"&gt;config tag 'direct'
  list dhcp_option '3,192.168.1.1' &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
  option force '1'
config host
  option ip '192.168.1.111'
  option mac '3a:58:e2:74:73:5e'
  option name 'Thinkpad'
  option dns '1'
  option tag 'direct' &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3,xxx 表示设置网关地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;给指定的设备设置 tag&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_安装_openmediavault"&gt;安装 OpenMediaVault&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;考虑到我的硬盘并不多，而且也没有给虚拟机直通硬盘的打算，所以就直接用 PVE 建立了一个 ZFS Pool，用来给虚拟机分配引导盘跟数据盘。OpenMediaVault 也是用的虚拟硬盘来做数据存储，在我的场景下，性能应该是够用的。虚拟机的安装过程就不在这里赘述了，跟安装普通的 Linux 系统并没有啥区别。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;全新安装的 OMV 自带 NFS、SMB、Rsync 这些常用的文件共享服务，但还是缺少 WebDav 协议，很多应用都可以通过 WebDav 进行同步备份，所以这个功能必须得补上。我选择的方案就是 &lt;a href="https://rclone.org/commands/rclone_serve_webdav/"&gt;rclone serve webdav&lt;/a&gt;。直接建立一个开机自启项，通过 rclone 在内网提供一个不需要身份验证的 WevDav 服务：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;/etc/systemd/system/webdav-server.service&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="hljs-section"&gt;[Unit]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Description&lt;/span&gt;=Webdav - rclone
&lt;span class="hljs-attr"&gt;Wants&lt;/span&gt;=network-&lt;span class="hljs-literal"&gt;on&lt;/span&gt;line.target
&lt;span class="hljs-attr"&gt;After&lt;/span&gt;=network-&lt;span class="hljs-literal"&gt;on&lt;/span&gt;line.target

&lt;span class="hljs-section"&gt;[Service]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;ExecStart&lt;/span&gt;=/usr/bin/rclone serve webdav --addr :&lt;span class="hljs-number"&gt;8080&lt;/span&gt; /path/to/public/dir
&lt;span class="hljs-attr"&gt;Restart&lt;/span&gt;=&lt;span class="hljs-literal"&gt;on&lt;/span&gt;-failure

&lt;span class="hljs-section"&gt;[Install]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;WantedBy&lt;/span&gt;=default.target&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_用_docker_整些花活"&gt;用 Docker 整些花活&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;我给 OpenMediaVault 虚拟机的定位非常清楚，就是专门用来提供 NAS 的基础功能的，所以不打算在它上面装各种奇奇怪怪的东西。但我一直都有建立一个电子相册的想法，所以就准备直接 &lt;a href="https://docs.photoprism.org/getting-started/docker-compose/"&gt;用 Docker 运行一个 PhotoPrism&lt;/a&gt;。我的 docker-compose.yml 如下：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="hljs-attr"&gt;version:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;#x27;3.5&amp;#x27;&lt;/span&gt;

&lt;span class="hljs-comment"&gt;# Example Docker Compose config file for PhotoPrism (Linux / AMD64)&lt;/span&gt;
&lt;span class="hljs-comment"&gt;#&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Documentation : https://docs.photoprism.org/getting-started/docker-compose/&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Docker Hub URL: https://hub.docker.com/r/photoprism/photoprism/&lt;/span&gt;
&lt;span class="hljs-comment"&gt;#&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Please run behind a reverse proxy like Caddy, Traefik or Nginx if you need HTTPS / SSL support&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# e.g. when running PhotoPrism on a public server outside your home network.&lt;/span&gt;
&lt;span class="hljs-comment"&gt;#&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# ------------------------------------------------------------------&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# DOCKER COMPOSE COMMAND REFERENCE&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# ------------------------------------------------------------------&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Start    | docker-compose up -d&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Stop     | docker-compose stop&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Update   | docker-compose pull&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Logs     | docker-compose logs --tail=25 -f&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Terminal | docker-compose exec photoprism bash&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Help     | docker-compose exec photoprism photoprism help&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Config   | docker-compose exec photoprism photoprism config&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Reset    | docker-compose exec photoprism photoprism reset&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Backup   | docker-compose exec photoprism photoprism backup -a -i&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Restore  | docker-compose exec photoprism photoprism restore -a -i&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Index    | docker-compose exec photoprism photoprism index&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Reindex  | docker-compose exec photoprism photoprism index -f&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Import   | docker-compose exec photoprism photoprism import&lt;/span&gt;
&lt;span class="hljs-comment"&gt;#&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# To search originals for faces without a complete rescan:&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# docker-compose exec photoprism photoprism faces index&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# -------------------------------------------------------------------&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# &lt;span class="hljs-doctag"&gt;Note:&lt;/span&gt; All commands may have to be prefixed with &amp;quot;sudo&amp;quot; when not running as root.&lt;/span&gt;
&lt;span class="hljs-comment"&gt;#       This will change the home directory &amp;quot;~&amp;quot; to &amp;quot;/root&amp;quot; in your configuration.&lt;/span&gt;

&lt;span class="hljs-attr"&gt;services:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;photoprism:&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Use photoprism/photoprism:preview instead for testing preview builds:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;image:&lt;/span&gt; &lt;span class="hljs-string"&gt;photoprism/photoprism:latest&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Only enable automatic restarts once your installation is properly&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# configured as it otherwise may get stuck in a restart loop:&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# https://docs.photoprism.org/getting-started/faq/#why-is-photoprism-getting-stuck-in-a-restart-loop&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# restart: unless-stopped&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;security_opt:&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;seccomp:unconfined&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;apparmor:unconfined&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Run as a specific, non-root user (see https://docs.docker.com/engine/reference/run/#user):&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# user: &amp;quot;1000:1000&amp;quot;&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;ports:&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;2342:2342&amp;quot;&lt;/span&gt; &lt;span class="hljs-comment"&gt;# [server]:[container]&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;environment:&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_ADMIN_PASSWORD:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;insecure&amp;quot;&lt;/span&gt;          &lt;span class="hljs-comment"&gt;# PLEASE CHANGE: Your initial admin password (min 4 characters)&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_ORIGINALS_LIMIT:&lt;/span&gt; &lt;span class="hljs-number"&gt;5000&lt;/span&gt;               &lt;span class="hljs-comment"&gt;# File size limit for originals in MB (increase for high-res video)&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_HTTP_COMPRESSION:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;gzip&amp;quot;&lt;/span&gt;            &lt;span class="hljs-comment"&gt;# Improves transfer speed and bandwidth utilization (none or gzip)&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_DEBUG:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;                      &lt;span class="hljs-comment"&gt;# Run in debug mode (shows additional log messages)&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_PUBLIC:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;                     &lt;span class="hljs-comment"&gt;# No authentication required (disables password protection)&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_READONLY:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;                   &lt;span class="hljs-comment"&gt;# Don&amp;#x27;t modify originals directory (reduced functionality)&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_EXPERIMENTAL:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;               &lt;span class="hljs-comment"&gt;# Enables experimental features&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_DISABLE_CHOWN:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;              &lt;span class="hljs-comment"&gt;# Disables storage permission updates on startup&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_DISABLE_WEBDAV:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;             &lt;span class="hljs-comment"&gt;# Disables built-in WebDAV server&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_DISABLE_SETTINGS:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;           &lt;span class="hljs-comment"&gt;# Disables Settings in Web UI&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_DISABLE_TENSORFLOW:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;         &lt;span class="hljs-comment"&gt;# Disables all features depending on TensorFlow&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_DISABLE_FACES:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;              &lt;span class="hljs-comment"&gt;# Disables facial recognition&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_DISABLE_CLASSIFICATION:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;     &lt;span class="hljs-comment"&gt;# Disables image classification&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_DARKTABLE_PRESETS:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;true&amp;quot;&lt;/span&gt;          &lt;span class="hljs-comment"&gt;# Enables Darktable presets and disables concurrent RAW conversion&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_DETECT_NSFW:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;false&amp;quot;&lt;/span&gt;                &lt;span class="hljs-comment"&gt;# Flag photos as private that MAY be offensive (requires TensorFlow)&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_UPLOAD_NSFW:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;true&amp;quot;&lt;/span&gt;                 &lt;span class="hljs-comment"&gt;# Allow uploads that MAY be offensive&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_SITE_URL:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;http://localhost:2342/&amp;quot;&lt;/span&gt;  &lt;span class="hljs-comment"&gt;# Public PhotoPrism URL&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_SITE_TITLE:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;PhotoPrism&amp;quot;&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_SITE_CAPTION:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;Browse Your Life&amp;quot;&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_SITE_DESCRIPTION:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;PHOTOPRISM_SITE_AUTHOR:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;&amp;quot;&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# Set a non-root user, group, or custom umask if your Docker environment doesn&amp;#x27;t support this natively:&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# PHOTOPRISM_UID: 1000&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# PHOTOPRISM_GID: 1000&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# PHOTOPRISM_UMASK: 0000&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# Enable TensorFlow AVX2 support for modern Intel CPUs (requires starting the container as root):&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# PHOTOPRISM_INIT: &amp;quot;tensorflow-amd64-avx2&amp;quot;&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# Hardware video transcoding options:&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# PHOTOPRISM_FFMPEG_BUFFERS: &amp;quot;64&amp;quot;              # FFmpeg capture buffers (default: 32)&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# PHOTOPRISM_FFMPEG_BITRATE: &amp;quot;32&amp;quot;              # FFmpeg encoding bitrate limit in Mbit/s (default: 50)&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# PHOTOPRISM_FFMPEG_ENCODER: &amp;quot;h264_v4l2m2m&amp;quot;    # Use Video4Linux for AVC transcoding (default: libx264)&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# PHOTOPRISM_FFMPEG_ENCODER: &amp;quot;h264_qsv&amp;quot;        # Use Intel Quick Sync Video for AVC transcoding (default: libx264)&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# PHOTOPRISM_INIT: &amp;quot;intel-graphics tensorflow-amd64-avx2&amp;quot; # Enable TensorFlow AVX2 &amp;amp; Intel Graphics support&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;HOME:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;/photoprism&amp;quot;&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Optional hardware devices for video transcoding and machine learning:&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# devices:&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;#  - &amp;quot;/dev/video11:/dev/video11&amp;quot; # Video4Linux (h264_v4l2m2m)&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;#  - &amp;quot;/dev/dri/renderD128:/dev/dri/renderD128&amp;quot; # Intel GPU&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;#  - &amp;quot;/dev/dri/card0:/dev/dri/card0&amp;quot;&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;working_dir:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;/photoprism&amp;quot;&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;volumes:&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# Your photo and video files ([local path]:[container path]):&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;zeeko-pictures:/photoprism/originals&amp;quot;&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# Multiple folders can be indexed by mounting them as sub-folders of /photoprism/originals:&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# - &amp;quot;/mnt/Family:/photoprism/originals/Family&amp;quot;    # [folder_1]:/photoprism/originals/[folder_1]&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# - &amp;quot;/mnt/Friends:/photoprism/originals/Friends&amp;quot;  # [folder_2]:/photoprism/originals/[folder_2]&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# Mounting an import folder is optional (see docs):&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# - &amp;quot;~/Import:/photoprism/import&amp;quot;&lt;/span&gt;
      &lt;span class="hljs-comment"&gt;# Permanent storage for settings, index &amp;amp; sidecar files (DON&amp;#x27;T REMOVE):&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;apps_photos:/photoprism/storage&amp;quot;&lt;/span&gt;

&lt;span class="hljs-attr"&gt;volumes:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;zeeko-pictures:&lt;/span&gt;  &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
    &lt;span class="hljs-attr"&gt;external:&lt;/span&gt; &lt;span class="hljs-literal"&gt;true&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;apps_photos:&lt;/span&gt;     &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
    &lt;span class="hljs-attr"&gt;external:&lt;/span&gt; &lt;span class="hljs-literal"&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;一个通过 NFS 挂载的 volume，保存图片的位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;另一个通过 NFS 挂载的 volume，用来持久化配置文件，因为 PhotoPrism 容器初始化的时候会对配置文件夹使用 &lt;code&gt;chmod&lt;/code&gt;，在 NFS 上会报权限错误，一个比较懒的解决方案是 NFS 共享时添加 &lt;code&gt;no_root_squash&lt;/code&gt; 选项。&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_成果展示"&gt;成果展示&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://i.imgur.com/RMClJQk.png" alt="My First NAS"&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content></entry><entry><id>http://gianthard.rocks/a/84</id><title type="text">FSharp 中被生成的 IEvent</title><summary type="html">&lt;div class="paragraph"&gt;
&lt;p&gt;F# 会将系统类库、第三方类库中的 &lt;code&gt;CLIEvent&lt;/code&gt; 转换成对应类型的 &lt;code&gt;IEvent&lt;/code&gt;，这个过程自动地发生在编译的过程中。接下来我将用一段非常简单的代码来演示其中的玄机。&lt;/p&gt;
&lt;/div&gt;</summary><published>2021-07-06T14:56:39Z</published><updated>2021-07-06T15:03:34Z</updated><link href="http://gianthard.rocks/a/84" /><content type="html">&lt;div id="toc" class="toc"&gt;
&lt;div id="toctitle"&gt;内容导航&lt;/div&gt;
&lt;ul class="sectlevel1"&gt;
&lt;li&gt;&lt;a href="#_参考链接"&gt;参考链接&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;为了方便 FSharp 用户以更加函数式的方式与 C# 的事件进行交互，F# 提供了 &lt;code&gt;IEvent&lt;/code&gt; 类型。&lt;code&gt;IEvent&lt;/code&gt; 继承了 &lt;code&gt;IObservable&lt;/code&gt;，方便 F# 用户使用 Rx.Net 来处理事件流，另一方面，&lt;code&gt;IEvent&lt;/code&gt; 还继承了 &lt;code&gt;IDelegateEvent&lt;/code&gt; 方便用户将 F# 函数注册为事件的监听器。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;除此之外，F# 还会将系统类库、第三方类库中的 &lt;code&gt;CLIEvent&lt;/code&gt; 转换成对应类型的 &lt;code&gt;IEvent&lt;/code&gt;，这个过程自动地发生在编译的过程中。接下来我将用一段非常简单的代码（&lt;a href="https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/members/events#creating-custom-events"&gt;代码来源&lt;/a&gt;）来演示其中的玄机。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;在 fsi 中执行&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-fsharp" data-lang="fsharp"&gt;&lt;span class="hljs-class"&gt;&lt;span class="hljs-keyword"&gt;type&lt;/span&gt; &lt;span class="hljs-title"&gt;MyClassWithCLIEvent&lt;/span&gt;&lt;/span&gt;() =

    &lt;span class="hljs-keyword"&gt;let&lt;/span&gt; event1 = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; Event&amp;lt;_&amp;gt;()

    &lt;span class="hljs-meta"&gt;[&amp;lt;CLIEvent&amp;gt;]&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;member&lt;/span&gt; this.Event1 = event1.Publish &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;

    &lt;span class="hljs-keyword"&gt;member&lt;/span&gt; this.TestEvent(arg) =
        event1.Trigger(this, arg)

&lt;span class="hljs-keyword"&gt;let&lt;/span&gt; classWithEvent = &lt;span class="hljs-keyword"&gt;new&lt;/span&gt; MyClassWithCLIEvent()
&amp;lt;@ classWithEvent.Event1 @&amp;gt; &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;声明一个 &lt;code&gt;CLIEvent&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 Quotation 获取 F# 表达式&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;执行结果如下&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-fsharp" data-lang="fsharp"&gt;Quotations.Expr&amp;lt;IEvent&amp;lt;Handler&amp;lt;MyClassWithCLIEvent * obj&amp;gt;,
                         (MyClassWithCLIEvent * obj)&amp;gt;&amp;gt; =
  Call (None, CreateEvent,  &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
      [Lambda (eventDelegate, Call (Some (PropertyGet (None, classWithEvent, [])), add_Event1, [eventDelegate])), &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
       Lambda (eventDelegate,
               Call (Some (PropertyGet (None, classWithEvent, [])), remove_Event1, [eventDelegate])), &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
       Lambda (callback,
               NewDelegate (FSharpHandler`&lt;span class="hljs-number"&gt;1&lt;/span&gt;, delegateArg0, delegateArg1,
                            Application (Application (callback, delegateArg0),
                                         delegateArg1)))]) &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;调用 &lt;code&gt;RuntimeHelper.CreateEvent()&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CreateEvent&lt;/code&gt; 的第一个参数：一个 lambda，可以将事件委托添加为 &lt;code&gt;classWithEvent.Event1&lt;/code&gt; 的监听器&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CreateEvent&lt;/code&gt; 的第二个参数：一个 lambda，将事件委托从 &lt;code&gt;classWithEvent.Event1&lt;/code&gt; 的监听器中移除&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;CreateEvent&lt;/code&gt; 的第三个参数：一个 lambda，将一个 F# 函数转换为 &lt;code&gt;Event1&lt;/code&gt; 的委托类型&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;可以看到，当我们使用 F# 访问 &lt;code&gt;CLIEvent&lt;/code&gt; 的时候，编译器会生成一个 &lt;code&gt;IEvent&lt;/code&gt; 对象，并在 &lt;code&gt;IEvent&lt;/code&gt; 的事件处理函数闭包中捕获 &lt;code&gt;CLIEvent&lt;/code&gt; 所属的实例。编译器的这种行为有时候就让一些奇怪的代码正常运行：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-fsharp" data-lang="fsharp"&gt;&lt;span class="hljs-keyword"&gt;let&lt;/span&gt; nullObj = Unchecked.defaultof&amp;lt;MyClassWithCLIEvent&amp;gt;
nullObj.Event1.GetType() &lt;span class="hljs-comment"&gt;// 一切正常&lt;/span&gt;
nullObj.Event1.Add(ignore) &lt;span class="hljs-comment"&gt;// NullReferenceException&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;在 F# 中仅访问 &lt;code&gt;IEvent&lt;/code&gt; 类型的属性不会出现空引用异常，只有在注册、移除事件监听器的时候才会触发。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_参考链接"&gt;参考链接&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://fsharp.github.io/fsharp-core-docs/reference/fsharp-control-ievent-1.html"&gt;FSharp.Control.IEvent&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/members/events#creating-custom-events"&gt;Events: Creating custom events&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content></entry><entry><id>http://gianthard.rocks/a/83</id><title type="text">使用树莓派作为旁路网关</title><summary type="html">&lt;div class="paragraph"&gt;
&lt;p&gt;家里一直有个吃灰的树莓派，趁着最近换上了精品线路主机，一个用树莓派来做旁路网关的想法油然而生。&lt;/p&gt;
&lt;/div&gt;</summary><published>2021-04-11T15:48:53Z</published><updated>2021-04-11T15:48:53Z</updated><link href="http://gianthard.rocks/a/83" /><content type="html">&lt;div id="toc" class="toc"&gt;
&lt;div id="toctitle"&gt;内容导航&lt;/div&gt;
&lt;ul class="sectlevel1"&gt;
&lt;li&gt;&lt;a href="#_准备树莓派"&gt;准备树莓派&lt;/a&gt;
&lt;ul class="sectlevel2"&gt;
&lt;li&gt;&lt;a href="#_硬件准备"&gt;硬件准备&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_软件准备"&gt;软件准备&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#_给树莓派配置静态地址"&gt;给树莓派配置静态地址&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_树莓派启动_dhcp_服务"&gt;树莓派启动 DHCP 服务&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_收尾"&gt;收尾&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_问题记录"&gt;问题记录&lt;/a&gt;
&lt;ul class="sectlevel2"&gt;
&lt;li&gt;&lt;a href="#_电源输出功率不足导致测速结果较低"&gt;电源输出功率不足导致测速结果较低&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_内核自带网卡驱动性能差"&gt;内核自带网卡驱动性能差&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#_参考链接"&gt;参考链接&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;家里一直有个吃灰的树莓派，趁着最近换上了精品线路主机，一个用树莓派来做旁路网关的想法油然而生。由于主路由——京东无线宝还没下车，不好直接刷成 OpenWrt，所以有些需求也只能通过树莓派旁路由来实现：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;全局接入互联网&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;避免 DNS 污染&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;最终需要实现的网络结构如下所示：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://i.imgur.com/CBZnehw.png" alt="CBZnehw"&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="💬"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
之所以这里的主路由地址是 192.168.68.1 是因为京东无线宝无法修改子网网段，你可以按照自己的实际情况将 192.168.68.x 替换掉。
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_准备树莓派"&gt;准备树莓派&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_硬件准备"&gt;硬件准备&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;我的树莓派是大学时期买的 3B，有线网卡只有 100Mbps 的速度，USB 也全部都是 2.0 的。为了能够配上主路由的千兆口，所以又在淘宝买了一个 USB 3.0 千兆网卡（AX88179 Gigabit Ethernet，型号仅供参考）。由于 USB 2.0 的限制，理论上这个 USB 3.0 网卡在树莓派 3B 上的速度应该能有 480Mbps。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;再就是电源适配器，树莓派 3B 需要 5V 2.5A 的电源供给，使用输出功率较低的电源适配器会影响树莓派在测试等高负载场景下的性能表现。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_软件准备"&gt;软件准备&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;首先安装树莓派系统烧录工具 —— Raspberry Pi Imager&lt;/p&gt;
&lt;/div&gt;
&lt;div class="literalblock"&gt;
&lt;div class="content"&gt;
&lt;pre&gt;# pacman -S rpi-imager&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;运行烧录工具，选择喜欢的系统进行安装，我选择的是不带图形界面的 Manajro ARM。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="imageblock"&gt;
&lt;div class="content"&gt;
&lt;img src="https://i.imgur.com/QgOBI3D.png" alt="QgOBI3D"&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="💬"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
这里我并没有给树莓派安装 OpenWrt，而是选择了更加通用的 Manjaro Linux，这完全是出自个人偏好，以及方便自行升级维护的打算。
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;将操作系统烧录进树莓派后就可以开机进行一些常规的初始化配置了，例如 fish shell 之类的（在&lt;a href="https://gianthard.rocks/a/39"&gt;这篇博文&lt;/a&gt;有我分享的 Manjaro 常规配置）。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_给树莓派配置静态地址"&gt;给树莓派配置静态地址&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;首先卸载 Manjaro 自带的 DHCP 客户端，因为树莓派作为旁路网关，IP 地址需要固定住&lt;span class="heimu"&gt;，使用 DHCP 只会影响获取 IP 地址的速度&lt;/span&gt;。接着，修改 systemd-networkd 配置，给树莓安排一个静态 IP。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;/etc/systemd/network/20-wired.network&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span class="hljs-section"&gt;[Match]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Name&lt;/span&gt;=eth* &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;

&lt;span class="hljs-section"&gt;[Network]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Address&lt;/span&gt;=&lt;span class="hljs-number"&gt;192.168&lt;/span&gt;.&lt;span class="hljs-number"&gt;68.3&lt;/span&gt;/&lt;span class="hljs-number"&gt;24&lt;/span&gt; &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
&lt;span class="hljs-attr"&gt;Gateway&lt;/span&gt;=&lt;span class="hljs-number"&gt;192.168&lt;/span&gt;.&lt;span class="hljs-number"&gt;68.1&lt;/span&gt; &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
&lt;span class="hljs-attr"&gt;DNS&lt;/span&gt;=&lt;span class="hljs-number"&gt;192.168&lt;/span&gt;.&lt;span class="hljs-number"&gt;68.1&lt;/span&gt; &lt;i class="conum" data-value="4"&gt;&lt;/i&gt;&lt;b&gt;(4)&lt;/b&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;设置用来绑定 IP 地址的网卡，例如 eth1，支持 &lt;code&gt;*&lt;/code&gt; 作为通配符&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置本机地址&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置网关地址，设置为主路由地址即可&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;设置 DNS 地址，使用主路由地址即可&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_树莓派启动_dhcp_服务"&gt;树莓派启动 DHCP 服务&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;接下来安装 dhcp 服务端&lt;/p&gt;
&lt;/div&gt;
&lt;div class="literalblock"&gt;
&lt;div class="content"&gt;
&lt;pre&gt;# pacman -S dhcp&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;dhcp 自带了一个样例文件，另存一下，方便以后参考&lt;/p&gt;
&lt;/div&gt;
&lt;div class="literalblock"&gt;
&lt;div class="content"&gt;
&lt;pre&gt;# mv /etc/dhcpd.conf /etc/dhcpd.conf.example&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;接着创建 DHCP 服务端配置文件&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;/etc/dhcpd.conf&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre&gt;option domain-name-servers 192.168.68.1;
option subnet-mask 255.255.255.0;
option routers 192.168.68.3;  &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
subnet 192.168.68.0 netmask 255.255.255.0 { &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
  range 192.168.68.100 192.168.68.250;
}&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;路由器地址，设置为树莓派的静态 IP 即可&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置子网 IP，需要与分配给网络接口的子网一致&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;最后，创建一个 systemd service 文件，用于开机自启 DHCP 服务。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="literalblock"&gt;
&lt;div class="content"&gt;
&lt;pre&gt;# cp /usr/lib/systemd/system/dhcpd4.service /etc/systemd/system/dhcpd4@.service&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;然后编辑这份 service 文件，添加网络接口配置。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;/etc/systemd/system/dhcpd4@.service&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre&gt;...
[Service]
...
ExecStart=/usr/bin/dhcpd -4 -q -cf /etc/dhcpd.conf -pf /run/dhcpd4/dhcpd.pid %I
...&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;最后启用 DHCP 服务：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="literalblock"&gt;
&lt;div class="content"&gt;
&lt;pre&gt;# systemctl enable dhcpd4@eth1&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;你可以通过 ip link 来查看网卡的名称来替换掉上面命令中的 eth1。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_收尾"&gt;收尾&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;最后，依次重启主路由、树莓派，然后重新连接电脑的网络，应该就可以看到网关被设置成了树莓派的地址：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="literalblock"&gt;
&lt;div class="content"&gt;
&lt;pre&gt;$ ip r
&amp;gt; default via 192.168.68.3 dev wlo1 proto dhcp metric 600
&amp;gt; 192.168.68.0/24 dev wlo1 proto kernel scope link src 192.168.68.101 metric 600&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_问题记录"&gt;问题记录&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;下面是我在配置树莓派旁路网关的过程中遇到的一些奇怪的问题，可能会对你有所帮助。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_电源输出功率不足导致测速结果较低"&gt;电源输出功率不足导致测速结果较低&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;一开始图方便，用的小米插线板提供的 USB 接口，没注意到输出功率是 5V 2.1A，导致树莓派动不动就报低电压警告。测速的话也只有 40Mbps 左右，换成 5V 2.5A 的电源适配器，马上就到 150Mpbs 了。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect2"&gt;
&lt;h3 id="_内核自带网卡驱动性能差"&gt;内核自带网卡驱动性能差&lt;/h3&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;我买的是 USB 网卡芯片型号是 AX88179，内核自带驱动，但是测速结果还是不够理想。在热心群友的帮助下，替换了厂商提供的 Linux 驱动，测速结果能够到 200Mbps 了。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_参考链接"&gt;参考链接&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://wiki.archlinux.org/index.php/dhcpd"&gt;dhcpd&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://wiki.archlinux.org/index.php/Systemd-networkd#Wired_adapter_using_a_static_IP"&gt;systemd-networkd&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://sspai.com/post/59708"&gt;从听说到上手，人人都能看懂的旁路由入门指南&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content></entry><entry><id>http://gianthard.rocks/a/82</id><title type="text">对眼下的一个微前端架构应用的思考</title><summary type="html">&lt;div class="paragraph"&gt;
&lt;p&gt;我相信第一次用 JS 来开发微前端架构的人都或早或晚会遇到这些问题，于是便将这些暴露出的问题记录下来，希望能对这些纠结微前端技术选型的人有所帮&lt;/p&gt;
&lt;/div&gt;</summary><published>2021-03-14T04:09:20Z</published><updated>2021-03-14T04:09:20Z</updated><link href="http://gianthard.rocks/a/82" /><content type="html">&lt;div id="toc" class="toc"&gt;
&lt;div id="toctitle"&gt;内容导航&lt;/div&gt;
&lt;ul class="sectlevel1"&gt;
&lt;li&gt;&lt;a href="#_一个十五年前的微前端架构系统"&gt;一个十五年前的微前端架构系统&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_微前端的基本结构"&gt;微前端的基本结构&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_一个标准的运行时"&gt;一个标准的运行时&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_共享各模块之间的公共依赖"&gt;共享各模块之间的公共依赖&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_业务模块的加载方式"&gt;业务模块的加载方式&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_入口模块的创意工坊"&gt;入口模块的“创意工坊”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_一个十五年前的微前端架构系统"&gt;一个十五年前的微前端架构系统&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;目前有一个延续了十五年的系统，前端是 Silverlight 实现的，作为一个拥有上千页面的中台系统，它的基础架构提供了如下能力：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="olist arabic"&gt;
&lt;ol class="arabic"&gt;
&lt;li&gt;
&lt;p&gt;页面层级独立编译、部署、懒加载&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;全局状态共享&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;允许提取公共组件、公共依赖复用&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;这个基于 Silverlight 架构实现了多个团队独立开发、部署各自维护的页面，最后在运行时集成到系统中的功能。这套架构放到今天，也称得上是微前端架构了，而且几乎不存在全局变量、样式污染的问题：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;C# 天生模块化，全局变量没法泄露到 namespace 外面去&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Silverlight 需要中心化地注册全局样式，业务模块很难对全局主题样式魔改&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;时过境迁，Silverlight 也终于成为了历史的眼泪，这套系统因为无法在 IE 以外的浏览器上运行而不得不被我们用现代前端技术重写（指 Vue.js 2）。不过很可惜，新的系统架构是由一个用 7 天时间从零开始入门 Vue.js 的开发人员边看教程边搭建的，由于代码（指代码质量与架构）糊的很差，所以随着迁移的页面越来越多，诸多问题暴露了出来。但是我相信第一次用 JS 来开发微前端架构的人都或早或晚会遇到这些问题，于是便将这些暴露出的问题记录下来，希望能对这些纠结微前端技术选型的人有所帮助。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_微前端的基本结构"&gt;微前端的基本结构&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;在我所遇到的需求中，我们需要将一个或多个页面作为独立的微前端模块，它们可以被独立的开发、打包、部署，也是被加载到浏览器中执行的最小单元，可以看作是前端的“微服务”。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;除了这些服务于各自业务目标的前端微服务之外，还需要有一个入口模块，它将根据用户的权限以及偏好设置加载不同的微前端模块到用户的浏览器中，并通过路由组件将页面展示出来。这个入口模块可以看作是微前端架构的“网关”。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;至此，一个微前端架构的基本结构已经展示出来了。微前端模块服务于业务功能，入口模块专注于组装以及展示业务模块。这两个基本组成部分的职责还算是比较简单明了的，但是在实现功能的过程中却有很多地方需要慎重决定。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_一个标准的运行时"&gt;一个标准的运行时&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;由于浏览器的品牌、版本众多，对 ECMAScript 的特性支持千奇百怪，所以我们往往需要借助 Babel 以及 &lt;code&gt;core-js&lt;/code&gt; 在过时的浏览器上给缺失的功能打补丁。由于每个微前端模块都是独立打包，为了避免重复进行 Polyfill，所以可以选择在入口文件一次性导入所有的 &lt;a href="https://babeljs.io/docs/en/babel-polyfill"&gt;Polyfill&lt;/a&gt;：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;core-js/stable&amp;quot;&lt;/span&gt;;
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;regenerator-runtime/runtime&amp;quot;&lt;/span&gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;这样做的话，虽然入口模块的体积可能会很大，但是由于不知道其他业务模块项目中到底会使用哪些特性，只能让入口模块为整个微前端提供一个标准的执行环境，其他独立打包的业务模块仅需要使用 &lt;code&gt;@babel/preset-env&lt;/code&gt; 把新的语法转换成目标浏览器兼容的语法即可。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;babel-loader options&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-js" data-lang="js"&gt;{
  &lt;span class="hljs-attr"&gt;loader&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;babel-loader&amp;#x27;&lt;/span&gt;,
  &lt;span class="hljs-attr"&gt;options&lt;/span&gt;: {
    &lt;span class="hljs-attr"&gt;presets&lt;/span&gt;: [
      [&lt;span class="hljs-string"&gt;&amp;#x27;@babel/preset-env&amp;#x27;&lt;/span&gt;, { &lt;span class="hljs-attr"&gt;useBuiltIns&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;entry&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;corejs&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;#x27;3&amp;#x27;&lt;/span&gt; }],
    ],
  },
},&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_共享各模块之间的公共依赖"&gt;共享各模块之间的公共依赖&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;为了能尽可能的避免通用的第三方依赖（如 Vue 全家桶、Lodash、dayjs、axios 等）被业务模块反复的打包，如何共享依赖是必须要处理的。在我们微前端架构早期实现中，我们通过 Webpack Dll Plugin 来实现对依赖的共享，不过最开始我们并没有意识到 Dll Plugin 存在着一个非常棘手的问题 —— 默认情况下，Dll 的每次更新都可能伴随着 manifest 文件的变动，这导致引用 Dll 的业务模块也必须跟着一起重新编译。解决这个问题的方法也很简单，通过 &lt;a href="https://webpack.js.org/plugins/hashed-module-ids-plugin"&gt;HashedModuleIdsPlugin&lt;/a&gt; 来根据模块名称生成固定的模块 ID。但是如果依赖本身升级了，例如 &lt;code&gt;foo/bar.js&lt;/code&gt; 路径变为了 &lt;code&gt;foo/esm/bar.mjs&lt;/code&gt; ，这样 DllPlugin 生成出来的 Manifest 也还是会发生变化，最终我们的所有业务模块还是得一起重新打包 :(。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;所以我们最终还是换了用 Externals 的方式来打包公共依赖，具体的做法可以参考&lt;a href="https://juejin.cn/post/6844904149746745357"&gt;探索webpack4与webpack5多项目公共代码复用架构&lt;/a&gt;。同样的，对于实践过程中提取出来的公共组件也可以利用相同的方式来进行跨项目的共享。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="admonitionblock note"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td class="icon"&gt;
&lt;i class="fa icon-note" title="💬"&gt;&lt;/i&gt;
&lt;/td&gt;
&lt;td class="content"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;在一开始我们是使用 DllPlugin 进行公共依赖抽取的，在发现了上面提到的 DllPlugin 可能存在的问题后，我们切换到了 Externals 的方案。但由于更新线上全部的业务模块涉及到不同城市的三个团队，所以我们整了在运行时重定向 DllPlugin 引用到 Externals 上的东西，这样为所有的业务模块升级就预留了更多的时间。&lt;/p&gt;
&lt;/div&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;除了上面提到的显式共享的依赖之外，还有一类是由打包工具产生的隐式依赖，例如 &lt;code&gt;@babel/runtime&lt;/code&gt;、&lt;code&gt;regeneratorRuntime&lt;/code&gt;。Babel 在生成的代码中用到了许多自带的工具函数（全部的工具函数压缩混淆后大概 63K 左右），通常这些小函数会被内联到 chunk 的开头而不会在各个模块间共享。Babel 提供了一个插件来帮助复用这些工具函数——&lt;a href="https://babeljs.io/docs/en/babel-plugin-transform-runtime#why"&gt;@babel/plugin-transform-runtime&lt;/a&gt;。这个插件会把 Babel 生成的代码中的内联工具函数转换为对 &lt;code&gt;@babel/runtime&lt;/code&gt; 的引用。最开始我也想着用 Externals 来处理这个问题，但是 &lt;code&gt;@babel/runtime&lt;/code&gt; 将每个工具函数都导出成了单独的模块，这样我们的 Externals 的暴露逻辑就会变得很复杂，甚至可能需要专门的维护一个工具函数列表。再者，这些工具函数中，体积最大的就是 &lt;code&gt;regeneratorRuntime&lt;/code&gt;，而这个库是可以把自己注册成全局变量的：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;#x27;regeneratorRuntime/runtime&amp;#x27;&lt;/span&gt;;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;最重要的是，如果针对现代浏览器使用&lt;a href="https://dev.to/thejohnstew/differential-serving-3dkf"&gt;差异化加载&lt;/a&gt;策略的话，这些工具函数的最终占用体积会小很多。所以我们最终没有处理这部分依赖的共享，仅在全局注册一个 &lt;code&gt;regeneratorRuntime&lt;/code&gt;。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_业务模块的加载方式"&gt;业务模块的加载方式&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;由于我们的整体架构不考虑对 Vue 以外的前端库的兼容，所以就直接使用 VueRouter 来实现业务模块的按需加载了。在入口模块启动的时候，通过后端接口查询当前用户可访问的页面列表，再根据这些页面列表查询到对应的业务模块 bundle 文件部署地址，最后为这些业务模块生成路由信息并添加到 Router 中。最开始看来好像没啥问题，但是随着项目演进，经常就出现需要在业务模块中嵌套加载其他业务模块的需求，类比到后端，就像是微服务之间需要相互调用。不过由于我们项目开发人员大多是业余前端，最开始为了实现这样的需求，要么是在立项前就把业务模块写到一起，要么就是通过 iframe 嵌套 :(。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;虽然我们的微前端架构开发者调研过 qiankun 这样非常成熟的微前端解决方案，但是并没有 get 到其中关于业务模块加载的内在目标。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;在我看来，入口模块不仅仅是前端微服务的网关，同样也需要承担服务发现的职责：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="literalblock"&gt;
&lt;div class="content"&gt;
&lt;pre&gt;const userHomeModule = entryModule.resolveModule('user-home');&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;获取到其他前端微服务的引用之后，我们还需要能够调用它们：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="literalblock"&gt;
&lt;div class="content"&gt;
&lt;pre&gt;entryModule.mountModule(userHomeModule);&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;这样前端微服务之间相互调用的链路就打通了。当然，上面的代码只不过是伪代码，对于我们选择的 Vue 来说，可能的实现方式是这样的：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="hljs-comment"&gt;// use vue router to resolve other business module&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; { resolved } = &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;.$router.resolveRoute(&lt;span class="hljs-string"&gt;&amp;#x27;/user/home&amp;#x27;&lt;/span&gt;);
&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; comp = resolved.matched[&lt;span class="hljs-number"&gt;0&lt;/span&gt;].component;
&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; vnode = &lt;span class="hljs-built_in"&gt;this&lt;/span&gt;.$createElement(comp)
&lt;span class="hljs-comment"&gt;// then mount this vnode&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;在我们目前遇到的场景中，如果一开始我们微前端框架提供了类似的功能那么前端模块的拆分可以更加的细粒度，而且还可以避免 N 重 iframe 嵌套。&lt;span class="heimu"&gt;但是你不能指望一个才学了 7 天 Vue.js 的业余前端对这些中高级用法有所了解&lt;/span&gt;。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_入口模块的创意工坊"&gt;入口模块的“创意工坊”&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;WIP&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content></entry><entry><id>http://gianthard.rocks/a/81</id><title type="text">迁移到 K3s - Aria2</title><summary type="html">&lt;div class="paragraph"&gt;
&lt;p&gt;之前一直用的阿里云香港轻量服务器，不过最近不打算继续续费了。趁着春节假期的缓冲期，就先把 Aria2 给搬到单节点的 K3s 集群中吧。&lt;/p&gt;
&lt;/div&gt;</summary><published>2021-02-18T12:49:09Z</published><updated>2021-02-18T12:49:09Z</updated><link href="http://gianthard.rocks/a/81" /><content type="html">&lt;div id="toc" class="toc"&gt;
&lt;div id="toctitle"&gt;内容导航&lt;/div&gt;
&lt;ul class="sectlevel1"&gt;
&lt;li&gt;&lt;a href="#_容器化"&gt;容器化&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_持久化存储"&gt;持久化存储&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_配置文件持久化"&gt;配置文件持久化&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_数据持久化"&gt;数据持久化&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#_服务暴露"&gt;服务暴露&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;之前一直用的阿里云香港轻量服务器，不过最近不打算继续续费了，需要将上面的部分服务转移到 CloudCone 的 K3s 集群中：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="ulist"&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Aria2&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Aria2NG&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TiddlyWiki&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;趁着春节假期的缓冲期，就先把 Aria2 给搬到单节点的 K3s 集群中吧。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_容器化"&gt;容器化&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;之前在阿里云上是用的 pm2 做的守护进程，然后 Caddy 静态暴露 Aria2NG 前端面板跟下载目录。要迁移到 K3s 的话就需要将这些东西容器化了。Aria2 的话，我直接用了这位老哥的 &lt;a href="https://p3terx.com/archives/docker-aria2-pro.html"&gt;Aria2 Pro&lt;/a&gt;，虽然不清楚具体设置了些什么，但是用起来没啥问题，前端面板也是用了他提供的 &lt;a href="https://p3terx.com/archives/aria2-frontend-ariang-tutorial.html"&gt;Aria2NG&lt;/a&gt; 镜像。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;除了 aria2 之外，还得把下载目录的暴露也容器化。因为目前 K3s 集群中用的 traefik 作为 ingress，所以不能像之前那样直接通过反向代理软件暴露文件系统了。记得之前 Caddy 是自带一个 &lt;a href="#https://github.com/filebrowser"&gt;File Browser&lt;/a&gt; 插件的，这是一个单独的 Web 服务，可以非常方便的搭建一个文件共享系统，也提供了官方的 Docker 镜像，那么就决定用它来对外暴露下载目录了。&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_持久化存储"&gt;持久化存储&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;File Browser 需要持久化一个 db 文件用来保存系统数据（用户管理、文件共享等），Aria2 跟 File Browser 还需要共享下载目录，前者写入，后者读取。因为我的 K3s 集群就 1 个节点，所以直接就用 &lt;code&gt;hostPath&lt;/code&gt; 来持久化数据了。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;file-transfer-pv.yaml&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="hljs-attr"&gt;apiVersion:&lt;/span&gt; &lt;span class="hljs-string"&gt;v1&lt;/span&gt;
&lt;span class="hljs-attr"&gt;kind:&lt;/span&gt; &lt;span class="hljs-string"&gt;PersistentVolume&lt;/span&gt;
&lt;span class="hljs-attr"&gt;metadata:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;namespace:&lt;/span&gt; &lt;span class="hljs-string"&gt;default&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;file-transfer-pv&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;labels:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;type:&lt;/span&gt; &lt;span class="hljs-string"&gt;local&lt;/span&gt;
&lt;span class="hljs-attr"&gt;spec:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;storageClassName:&lt;/span&gt; &lt;span class="hljs-string"&gt;manual&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;capacity:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;storage:&lt;/span&gt; &lt;span class="hljs-string"&gt;10Gi&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;accessModes:&lt;/span&gt;
    &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;ReadWriteOnce&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;hostPath:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;path:&lt;/span&gt; &lt;span class="hljs-string"&gt;'/path/to/file-transfer'&lt;/span&gt;

&lt;span class="hljs-meta"&gt;---&lt;/span&gt;

&lt;span class="hljs-attr"&gt;apiVersion:&lt;/span&gt; &lt;span class="hljs-string"&gt;v1&lt;/span&gt;
&lt;span class="hljs-attr"&gt;kind:&lt;/span&gt; &lt;span class="hljs-string"&gt;PersistentVolumeClaim&lt;/span&gt;
&lt;span class="hljs-attr"&gt;metadata:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;file-transfer-data-pvc&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;namespace:&lt;/span&gt; &lt;span class="hljs-string"&gt;default&lt;/span&gt;
&lt;span class="hljs-attr"&gt;spec:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;accessModes:&lt;/span&gt;
    &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;ReadWriteOnce&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;storageClassName:&lt;/span&gt; &lt;span class="hljs-string"&gt;manual&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;resources:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;requests:&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;storage:&lt;/span&gt; &lt;span class="hljs-string"&gt;10Gi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_配置文件持久化"&gt;配置文件持久化&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;File Browser 默认的配置文件将数据库文件设置为了 &lt;code&gt;/database.db&lt;/code&gt;，不太方便我们使用 pvc 持久化，所以我就需要通过 ConfigMap 修改一下配置文件，将数据库文件保存到单独的外部挂载目录下。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;file-browser-config.yaml&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="hljs-attr"&gt;apiVersion:&lt;/span&gt; &lt;span class="hljs-string"&gt;v1&lt;/span&gt;
&lt;span class="hljs-attr"&gt;kind:&lt;/span&gt; &lt;span class="hljs-string"&gt;ConfigMap&lt;/span&gt;
&lt;span class="hljs-attr"&gt;metadata:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;labels:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;app:&lt;/span&gt; &lt;span class="hljs-string"&gt;file-browser&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;file-browser-config&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;namespace:&lt;/span&gt; &lt;span class="hljs-string"&gt;default&lt;/span&gt;
&lt;span class="hljs-attr"&gt;data:&lt;/span&gt;
  &lt;span class="hljs-string"&gt;.filebrowser.json:&lt;/span&gt; &lt;span class="hljs-string"&gt;|
    {
      "port": 80,
      "baseURL": "",
      "address": "",
      "log": "stdout",
      "database": "/db/database.db",
      "root": "/srv"
    }&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_数据持久化"&gt;数据持久化&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;file-browser.yaml&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="hljs-comment"&gt;# 省略部分内容....&lt;/span&gt;
&lt;span class="hljs-attr"&gt;containers:&lt;/span&gt;
  &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;image:&lt;/span&gt; &lt;span class="hljs-string"&gt;filebrowser/filebrowser&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;file-browser&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;imagePullPolicy:&lt;/span&gt; &lt;span class="hljs-string"&gt;Always&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;volumeMounts:&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;mountPath:&lt;/span&gt; &lt;span class="hljs-string"&gt;/.filebrowser.json&lt;/span&gt; &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
        &lt;span class="hljs-attr"&gt;subPath:&lt;/span&gt; &lt;span class="hljs-string"&gt;.filebrowser.json&lt;/span&gt;
        &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;file-browser-config&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;mountPath:&lt;/span&gt; &lt;span class="hljs-string"&gt;/db&lt;/span&gt; &lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;(2)&lt;/b&gt;
        &lt;span class="hljs-attr"&gt;subPath:&lt;/span&gt; &lt;span class="hljs-string"&gt;db/&lt;/span&gt;
        &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;data&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;mountPath:&lt;/span&gt; &lt;span class="hljs-string"&gt;/srv&lt;/span&gt; &lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;(3)&lt;/b&gt;
        &lt;span class="hljs-attr"&gt;subPath:&lt;/span&gt; &lt;span class="hljs-string"&gt;data/&lt;/span&gt;
        &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;data&lt;/span&gt;
&lt;span class="hljs-attr"&gt;volumes:&lt;/span&gt;
  &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;data&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;persistentVolumeClaim:&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;claimName:&lt;/span&gt; &lt;span class="hljs-string"&gt;file-transfer-data-pvc&lt;/span&gt;
  &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;file-browser-config&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;configMap:&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;file-browser-config&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;items:&lt;/span&gt;
        &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;key:&lt;/span&gt; &lt;span class="hljs-string"&gt;.filebrowser.json&lt;/span&gt;
          &lt;span class="hljs-attr"&gt;path:&lt;/span&gt; &lt;span class="hljs-string"&gt;.filebrowser.json&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;file-browser 配置文件挂载点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="2"&gt;&lt;/i&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;将 data 卷的 &lt;code&gt;db/&lt;/code&gt; 目录挂载到容器中的 &lt;code&gt;/db&lt;/code&gt; 路径下，用来持久化数据库文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="3"&gt;&lt;/i&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;将 data 卷的 &lt;code&gt;data/&lt;/code&gt; 目录挂载到容器中的 &lt;code&gt;/srv&lt;/code&gt; 路径下，用来在 Pod 间共享下载目录&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;aria2.yaml&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="hljs-attr"&gt;containers:&lt;/span&gt;
  &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;image:&lt;/span&gt; &lt;span class="hljs-string"&gt;p3terx/ariang&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;ariang&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;imagePullPolicy:&lt;/span&gt; &lt;span class="hljs-string"&gt;Always&lt;/span&gt;
  &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;image:&lt;/span&gt; &lt;span class="hljs-string"&gt;p3terx/aria2-pro&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;env:&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;RPC_SECRET&lt;/span&gt;
        &lt;span class="hljs-attr"&gt;value:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;lt;SECERT&amp;gt;&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;RPC_PORT&lt;/span&gt;
        &lt;span class="hljs-attr"&gt;value:&lt;/span&gt; &lt;span class="hljs-string"&gt;'6800'&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;TZ&lt;/span&gt;
        &lt;span class="hljs-attr"&gt;value:&lt;/span&gt; &lt;span class="hljs-string"&gt;Asia/Shanghai&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;aria2-pro&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;imagePullPolicy:&lt;/span&gt; &lt;span class="hljs-string"&gt;Always&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;volumeMounts:&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;mountPath:&lt;/span&gt; &lt;span class="hljs-string"&gt;/downloads&lt;/span&gt; &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
        &lt;span class="hljs-attr"&gt;subPath:&lt;/span&gt; &lt;span class="hljs-string"&gt;data/&lt;/span&gt;
        &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;data&lt;/span&gt;
&lt;span class="hljs-attr"&gt;volumes:&lt;/span&gt;
  &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;data&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;persistentVolumeClaim:&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;claimName:&lt;/span&gt; &lt;span class="hljs-string"&gt;file-transfer-data-pvc&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;将 data 卷的 &lt;code&gt;data/&lt;/code&gt; 目录挂载到容器中的 &lt;code&gt;/downloads&lt;/code&gt; 路径下，用来在 Pod 间共享下载目录&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="sect1"&gt;
&lt;h2 id="_服务暴露"&gt;服务暴露&lt;/h2&gt;
&lt;div class="sectionbody"&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Aria2 服务需要暴露的端口不少，除了 HTTP 端口之外，还需要暴露供 BT 上传用的 TCP 与 UDP 端口。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;aria2-service.yaml&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="hljs-attr"&gt;apiVersion:&lt;/span&gt; &lt;span class="hljs-string"&gt;v1&lt;/span&gt;
&lt;span class="hljs-attr"&gt;kind:&lt;/span&gt; &lt;span class="hljs-string"&gt;Service&lt;/span&gt;
&lt;span class="hljs-attr"&gt;metadata:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;aria2&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;namespace:&lt;/span&gt; &lt;span class="hljs-string"&gt;default&lt;/span&gt;
&lt;span class="hljs-attr"&gt;spec:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;selector:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;app:&lt;/span&gt; &lt;span class="hljs-string"&gt;aria2&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;ports:&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# aria2 rpc&lt;/span&gt;
    &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;protocol:&lt;/span&gt; &lt;span class="hljs-string"&gt;TCP&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;port:&lt;/span&gt; &lt;span class="hljs-number"&gt;6800&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;rpc&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# aria2 BT&lt;/span&gt;
    &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;protocol:&lt;/span&gt; &lt;span class="hljs-string"&gt;TCP&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;port:&lt;/span&gt; &lt;span class="hljs-number"&gt;6888&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;bt&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# aria2 DHT&lt;/span&gt;
    &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;protocol:&lt;/span&gt; &lt;span class="hljs-string"&gt;UDP&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;port:&lt;/span&gt; &lt;span class="hljs-number"&gt;6888&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;dht&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# aria ng&lt;/span&gt;
    &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;protocol:&lt;/span&gt; &lt;span class="hljs-string"&gt;TCP&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;port:&lt;/span&gt; &lt;span class="hljs-number"&gt;6880&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;web&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;file-browser.yaml&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="hljs-attr"&gt;apiVersion:&lt;/span&gt; &lt;span class="hljs-string"&gt;v1&lt;/span&gt;
&lt;span class="hljs-attr"&gt;kind:&lt;/span&gt; &lt;span class="hljs-string"&gt;Service&lt;/span&gt;
&lt;span class="hljs-attr"&gt;metadata:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;file-browser&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;namespace:&lt;/span&gt; &lt;span class="hljs-string"&gt;default&lt;/span&gt;
&lt;span class="hljs-attr"&gt;spec:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;selector:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;app:&lt;/span&gt; &lt;span class="hljs-string"&gt;file-browser&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;ports:&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# file-browser http&lt;/span&gt;
    &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;protocol:&lt;/span&gt; &lt;span class="hljs-string"&gt;TCP&lt;/span&gt;
      &lt;span class="hljs-attr"&gt;port:&lt;/span&gt; &lt;span class="hljs-number"&gt;80&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;通过 Service 暴露端口之后还需要使用 Ingress 来向集群外暴露服务，我使用的是 traefik 提供的官方 helm chart，通过 IngressRoute 来定义 Ingress 路由。&lt;/p&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;Traefik 的默认配置中只监听了 80/443 端口，为了让 BT 上传能够正常工作，还得加入新的监听端口：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="title"&gt;traefik-values.yaml&lt;/div&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="hljs-comment"&gt;# ...&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Configure ports&lt;/span&gt;
&lt;span class="hljs-attr"&gt;ports:&lt;/span&gt;
  &lt;span class="hljs-comment"&gt;# ...&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;aria2tcp:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;port:&lt;/span&gt; &lt;span class="hljs-number"&gt;6888&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;expose:&lt;/span&gt; &lt;span class="hljs-literal"&gt;true&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;exposedPost:&lt;/span&gt; &lt;span class="hljs-number"&gt;6888&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;protocol:&lt;/span&gt; &lt;span class="hljs-string"&gt;TCP&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;aria2udp:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;port:&lt;/span&gt; &lt;span class="hljs-number"&gt;6888&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;expose:&lt;/span&gt; &lt;span class="hljs-literal"&gt;true&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;exposedPost:&lt;/span&gt; &lt;span class="hljs-number"&gt;6888&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;protocol:&lt;/span&gt; &lt;span class="hljs-string"&gt;UDP&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="paragraph"&gt;
&lt;p&gt;IngressRoute 只能用来路由 HTTP/HTTPS 请求，对于 TCP/UDP 请求，需要使用 IngressRouteTCP/IngressRouteUDP 来转发：&lt;/p&gt;
&lt;/div&gt;
&lt;div class="listingblock"&gt;
&lt;div class="content"&gt;
&lt;pre class="highlight"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="hljs-attr"&gt;apiVersion:&lt;/span&gt; &lt;span class="hljs-string"&gt;traefik.containo.us/v1alpha1&lt;/span&gt;
&lt;span class="hljs-attr"&gt;kind:&lt;/span&gt; &lt;span class="hljs-string"&gt;IngressRouteTCP&lt;/span&gt;
&lt;span class="hljs-attr"&gt;metadata:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;apps-tcp&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;namespace:&lt;/span&gt; &lt;span class="hljs-string"&gt;default&lt;/span&gt;
&lt;span class="hljs-attr"&gt;spec:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;entryPoints:&lt;/span&gt;
    &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;aria2tcp&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;routes:&lt;/span&gt;
    &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;match:&lt;/span&gt; &lt;span class="hljs-string"&gt;HostSNI(`*`)&lt;/span&gt; &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
      &lt;span class="hljs-attr"&gt;services:&lt;/span&gt;
        &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;aria2&lt;/span&gt;
          &lt;span class="hljs-attr"&gt;port:&lt;/span&gt; &lt;span class="hljs-number"&gt;6888&lt;/span&gt;

&lt;span class="hljs-meta"&gt;---&lt;/span&gt;
&lt;span class="hljs-attr"&gt;apiVersion:&lt;/span&gt; &lt;span class="hljs-string"&gt;traefik.containo.us/v1alpha1&lt;/span&gt;
&lt;span class="hljs-attr"&gt;kind:&lt;/span&gt; &lt;span class="hljs-string"&gt;IngressRouteUDP&lt;/span&gt;
&lt;span class="hljs-attr"&gt;metadata:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;apps-udp&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;namespace:&lt;/span&gt; &lt;span class="hljs-string"&gt;default&lt;/span&gt;
&lt;span class="hljs-attr"&gt;spec:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;entryPoints:&lt;/span&gt;
    &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;aria2udp&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;routes:&lt;/span&gt;
    &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;match:&lt;/span&gt; &lt;span class="hljs-string"&gt;HostSNI(`*`)&lt;/span&gt; &lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;(1)&lt;/b&gt;
      &lt;span class="hljs-attr"&gt;services:&lt;/span&gt;
        &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;aria2&lt;/span&gt;
          &lt;span class="hljs-attr"&gt;port:&lt;/span&gt; &lt;span class="hljs-number"&gt;6888&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class="colist arabic"&gt;
&lt;table&gt;
&lt;tr&gt;
&lt;td&gt;&lt;i class="conum" data-value="1"&gt;&lt;/i&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;因为 aria2 暴露的 TCP/UDP 端口没有使用 TLS 加密，所以需要设置路由匹配规则为 &lt;code&gt;HostSNI(&lt;code&gt;*&lt;/code&gt;)&lt;/code&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</content></entry></feed>