<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>你好，耳先生</title><description>先生贵姓？耳东陈。好的，这边请，耳先生。</description><link>https://www.kilerd.me/</link><item><title>GPU小偷：一次诡异的生产事故复盘</title><link>https://www.kilerd.me/thief-of-gpu/</link><guid isPermaLink="true">https://www.kilerd.me/thief-of-gpu/</guid><description>一次生产事故中，LLM推理服务的TTFT出现诡异波动，部分请求延迟飙升至2000ms+，随后宿主机节点接连&amp;quot;暴毙&amp;quot;，kill -9也无法挽救卡死的Pod。 排查过程一波三折：从怀疑机器故障，到发现CUDA异常，最终揪出了一个潜伏在集群中的&amp;quot;GPU小偷&amp;quot;——以及它与K8s扩缩容机制</description><pubDate>Sun, 22 Mar 2026 21:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;早晨的诡异异常&lt;/h2&gt;
&lt;p&gt;3月20日早上，出门跑步回来就发现服务出现了诡异的异常：LLM推理的TTFT从常规的220ms骤降至80ms，足足少了150ms。与此同时，TTS接口开始返回空音频流。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cms.kilerd.me/files/uploads/38907898-3486-43c5-894c-a3c34da60658.jpeg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;于是我进入了oncall环节。理论上我们的新模型已经上线快两周了，BUG都修复得差不多了，不应该无缘无故出故障。我的第一反应是：机器坏了。&lt;/p&gt;
&lt;p&gt;进入Lens查看集群节点状态，K8s显示一切正常——服务是healthy状态，Pod也在正常running。但奇怪的是，服务就是不返回推理内容。&lt;/p&gt;
&lt;p&gt;接下来SSH进入推理Pod，调用&lt;code&gt;nvidia-smi&lt;/code&gt;查看显卡工作状态，发现部分Pod的&lt;code&gt;nvidia-smi&lt;/code&gt;命令无法获取显卡信息。终于，问题定位到了：&lt;strong&gt;部分机器出现大规模Pod卡在terminating状态，无法正常退出。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cms.kilerd.me/files/uploads/19fdb1fa-4b70-4b75-816a-863ec5b81d3f.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当时还不清楚为什么会卡住。由于Pod内显示CUDA异常，我初步判定为宿主机损坏，快速drain掉故障节点，并强制清理卡顿的Pod，服务随即恢复正常。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cms.kilerd.me/files/uploads/5619af65-df17-4354-91cd-32246a7d8491.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;诡异的TTFT波动&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://cms.kilerd.me/files/uploads/c8451cac-7207-4bfc-a87a-6b9f7c9735e2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;服务恢复正常后，TTFT仍然波动剧烈。当时并非业务高峰期，进入Pod观察后发现：大部分请求能在150ms内完成首个音频chunk的推理，但有小部分请求需要2000-3000ms。&lt;/p&gt;
&lt;p&gt;一时间我以为是推理框架又出现了奇妙BUG，便开始翻看代码查找原因。但由于工期紧任务重，小部分请求的卡顿并不会对绝大多数用户造成致命影响，便暂时搁置这个问题，继续推进正常的迭代开发工作。&lt;/p&gt;
&lt;h2&gt;梅开二度&lt;/h2&gt;
&lt;p&gt;时间磕磕绊绊来到了晚上，同样的异常再次出现——又有宿主机节点异常&amp;quot;死去&amp;quot;，导致服务小规模故障。这一次我拉上了公司的SRE一起做深度排查。&lt;/p&gt;
&lt;p&gt;首先往GPU是否真正损坏的方向排查，发现无果。&lt;/p&gt;
&lt;p&gt;接着开始排查其他可能导致CUDA out of memory的服务，这次有了一些苗头——但根因并不是out of memory。&lt;/p&gt;
&lt;p&gt;我们发现有位同事写的Pod使用了GPU，却没有在资源申请中声明&lt;code&gt;nvidia.com/gpu: 1&lt;/code&gt;。理论上，在没有申请GPU资源的情况下使用GPU，系统会报错&amp;quot;无法找到GPU&amp;quot;。然而，同事的Pod是基于CUDA base image构建的，其Dockerfile中默认设置了&lt;code&gt;NVIDIA_VISIBLE_DEVICES=all&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;插播一条背景知识：&lt;/strong&gt; 在Docker环境中，GPU的声明是通过环境变量&lt;code&gt;NVIDIA_VISIBLE_DEVICES&lt;/code&gt;来管理的。运行&lt;code&gt;echo $NVIDIA_VISIBLE_DEVICES&lt;/code&gt;时会得到类似&lt;code&gt;gpu-[uuid]&lt;/code&gt;的输出。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;回到故事主线。在K8s的Deployment没有声明GPU请求的情况下，CUDA base image的环境变量&lt;code&gt;NVIDIA_VISIBLE_DEVICES=all&lt;/code&gt;会默认生效，于是这个Pod就占据了宿主机上全部八张显卡。而在没有开启MPS服务的情况下，各个服务会像CPU分时一样轮流占用GPU。&lt;/p&gt;
&lt;p&gt;这就解释了为什么我的TTS推理服务偶尔会出现2000ms的推理延迟——&lt;strong&gt;那段时间其实是被其他服务&amp;quot;偷&amp;quot;走了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;故此，我们称之为：&lt;strong&gt;GPU小偷&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;为什么会把宿主机炸死？&lt;/h2&gt;
&lt;p&gt;行文至此，我们可以看出：即便没有声明GPU资源，最多也就是当个&amp;quot;小偷&amp;quot;，不应该直接把宿主机炸死才对吧？&lt;/p&gt;
&lt;p&gt;这时候，真正的主角登场了：&lt;strong&gt;KEDA&lt;/strong&gt;。KEDA是K8s中用于动态扩缩容的服务，这里就不过多展开了。&lt;/p&gt;
&lt;p&gt;在推理服务被同事的Pod偷显卡的过程中，推理服务负载过高，触发了KEDA的扩容机制，启动推理服务扩容的同时，系统尝试杀死同事的服务来释放资源。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;等等，他明明没申请GPU，为什么要杀这个服务来释放资源？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为这个Pod里面有三个container，其中一个声明了GPU，另外两个没声明。同事的意图是三个container共享一张显卡。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;于是诡异的事情发生了：对显卡有控制权的两个Pod陷入了死锁竞争，导致双方都无法释放资源。&lt;/p&gt;
&lt;h3&gt;直接原因：NVIDIA驱动内核态rwlock/mutex死锁&lt;/h3&gt;
&lt;p&gt;内核栈显示两类阻塞：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类型A — 等待rwlock（write）：&lt;/strong&gt; nvidia_close()路径，进程退出时释放GPU资源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;do_exit → __fput → nvidia_close → nvidia_close_callback
  → rm_cleanup_file_private → _nv052958rm → _nv051484rm
  → os_acquire_rwlock_write  [BLOCKED]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;类型B — 等待mutex：&lt;/strong&gt; nvidia ioctl路径，正常GPU操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nvidia_unlocked_ioctl → rm_ioctl → _nv000792rm → _nv052958rm
  → _nv051492rm → os_acquire_mutex  [BLOCKED]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形成经典的&lt;strong&gt;AB-BA死锁&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程A：持有mutex → 等待rwlock(write)&lt;/li&gt;
&lt;li&gt;线程B：持有rwlock(read) → 等待mutex&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一旦死锁形成，所有后续nvidia ioctl调用（包括&lt;code&gt;nvidia-smi&lt;/code&gt;、新容器创建、CUDA操作）全部被阻塞在D状态（不可中断睡眠），&lt;code&gt;kill -9&lt;/code&gt;也无效。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;至此，我们完成了对&amp;quot;GPU小偷&amp;quot;事件的完整复盘。&lt;/p&gt;
&lt;p&gt;教训很明确：&lt;strong&gt;谨慎使用CUDA base image&lt;/strong&gt;，因为其中可能存在一些我们未知的默认行为。&lt;/p&gt;
&lt;p&gt;同时，在使用他人基于这个镜像构建的产物时，也需要多加甄别，避免被&amp;quot;无意&amp;quot;投毒。&lt;/p&gt;
</content:encoded></item><item><title>2025: 兴致勃勃地失败</title><link>https://www.kilerd.me/summaries-my-2025/</link><guid isPermaLink="true">https://www.kilerd.me/summaries-my-2025/</guid><description>人们总说人生的分界线是18岁，一旦迈过了18岁，你的人生节奏就开始疯狂加速，一周一个月的时间眨一眼就过去了。时常回顾过去，发现我最喜欢发呆的地方除了从前司低价买来的办公椅，就是跑完步歇着吃苹果的路边小凳子。每日的重复让我的精神感知变得很差，也就是互联网上常说的钝感力。 就这样，又是一年过去了。重回职场、尝试创业、继续单</description><pubDate>Sun, 28 Dec 2025 17:00:00 GMT</pubDate><content:encoded>&lt;p&gt;人们总说人生的分界线是18岁，一旦迈过了18岁，你的人生节奏就开始疯狂加速，一周一个月的时间眨一眼就过去了。时常回顾过去，发现我最喜欢发呆的地方除了从前司低价买来的办公椅，就是跑完步歇着吃苹果的路边小凳子。每日的重复让我的精神感知变得很差，也就是互联网上常说的钝感力。&lt;/p&gt;
&lt;p&gt;就这样，又是一年过去了。重回职场、尝试创业、继续单身，每一件事都兴致勃勃地开始，又在现实面前碰得头破血流。但或许失败本身，也是这一年最珍贵的收获。&lt;/p&gt;
&lt;h2&gt;重回职场&lt;/h2&gt;
&lt;p&gt;自从被前司辞退之后，我个人的求职欲望并不是特别强。一方面是前司的辞退补偿还没花完，另一方面是自己期望通过这段 gap 时间来沉淀一下并且学习一些新领域的知识。某种意义上讲，前司 thoughtworks 是一家业务性外包公司，我的简历上依旧是写的 java 外包的身份，我依旧想通过一些项目、一些开源贡献来弥补自己在技术上的缺陷，同时再以一个新的技术身份来求职。&lt;/p&gt;
&lt;p&gt;在前司业余时刻，我就已经在钻研 Rust 的相关技术了，又加上一些朋友在 web3 领域工作，在他们的氛围烘托之下，我期望中是打算求职一家 web3 公司进行转型，比如说做一些智能合约、做 VM 内核开发 等等使用 Rust 的底层技术。这也是我在前司心心念念想做的 infra dev 岗位。&lt;/p&gt;
&lt;p&gt;事情的转变发生在认识 yihong 之后，没错，就是那个写了 running_page 的 yihong。这也让我认识到了东三省小伙伴的热情似火，在让 yihong 大哥知道我离职失业之后，但凡有人发出招聘信息，他都会转一手给我，让我去面试。哪怕一些在我看来丝毫没有任何职业相关性的招聘信息，他都会给到你足够的鼓励言辞。一时间自信心无限膨胀的我感觉让我当个阿里 CEO 都是够资格的。&lt;/p&gt;
&lt;p&gt;就这样被 yihong 半哄半骗下，我入职了一家 AI startup。虽然说是美国公司，但是大部分技术团队都是在中国 remote 的友好小伙伴们。这也是我第一次在 AI 进入 LLM 时代后从事 AI 相关的工作。在此之前我对 AI 的知识与了解还停留在 LSTM 和 CNN 的范畴。一下子彻底从一个领域切换到另外一个领域的感觉还是很奇妙的。虽然一开始还是从事AI平台传统的 CRUD 开发，但随着老板的信任，我参与了越来越多的研发范围，也算是对 LLM 了解了越来越多。某种意义上，我也可以说是 LLM 行内人士了（即便我并没有参与核心的 LLM 模型开发）。&lt;/p&gt;
&lt;p&gt;虽说是从失业转变到了就业状态，但我的生活并没有发生太多实质化的变化。在就职之前我也是每天白天高强度地在家写开源或者写一些小项目，只不过是后来从开源变成了公司项目罢了。在前司 thoughtworks 工作时，因为外包的形式，我们在项目切换阶段也会出现这种长达数月的项目暂停，这也是我感知比较弱的来源之一。&lt;/p&gt;
&lt;p&gt;真正要说感知比较强的便是 startup 与成熟公司之间的工作模式差异。成熟公司的项目节奏更加平稳，尤其是 tw 这种外包公司，本着「都能做，得加钱」的观念，我们对工作量与排期是非常敏感的，换句话说就是我们喜欢做事有章法，一件一件来做，这样比较方便跟甲方收钱。反而在 startup 中就不存在比较强的排期概念，一方面是因为人员比较少，年纪也比较年轻，沟通很直接，另一方面是直面客户需求，有些需求就是很急的，需要立刻完成的。 当然啦，我并不是在维护这种 startup 的混乱工作模式，我始终相信存在一些更好的处理与管理模式适用于 startup。&lt;/p&gt;
&lt;p&gt;公司在快速发展，业务也越来越多，人员招聘却没有跟上进度，随之而来的便是无尽的加班，再加上前文所说的工作模式，引发的后果便是小伙伴们都时常处于 burn out 的状态，我不好评价这种状态是因何产生的，但希望我们这群小伙伴可以一起走得更远一些。&lt;/p&gt;
&lt;h2&gt;保持运动&lt;/h2&gt;
&lt;p&gt;好在我还有跑步。
在工作占据生活的极大多数时，剩余的时间我仍然是坚持在运动上，始终坚持着跑步。即便是这一年中跑步的速度变慢了一些，但只有跑步时那种专注着呼吸的状态才可以让我彻底从工作和各种情绪中剥离出来。而后静静地坐在路边的小椅子上，静静地吃完一个苹果，而后就是回家，重新返回工作状态。这似乎是我习惯的逃离生活压力的一种模式，它也特别有效。&lt;/p&gt;
&lt;p&gt;人们总说，一旦你习惯了跑步，你慢慢就不满足于路跑，开始喜欢越野跑；一旦你喜欢了爬山，你就开始选择重装徒步。在爬山和跑步成为了我的多年爱好之后，我开始选择了入坑重装徒步。跑步给我带来了相当不错的心肺，爬山给我带来了野外的生存知识，前司的员工福利教会了我急救知识。在这些能力的加持下，我似乎很快就上手了重装徒步。虽然说至今我只走了几条比较常规的线路，但是相比于那些看了几次自媒体就兴致勃勃地买装备走线的人来说，我的知识储备确实高很多。当然啦，我相比于那些沉寂多年户外的老炮还是有相当大的差距的。&lt;/p&gt;
&lt;p&gt;表面上重装徒步只是在普通徒步上背上了一个大大的背包，但是其本质上完全不一样。在那个背包里面有你的所有衣食住行，你可以不用担心你在天黑前走不到指定的旅馆，你可以选择就地扎营过一晚上；你也不用因为天气变差而危险赶路，你可以选择就近找个破房子就睡觉。那个包里有着你可以活下去的一切东西，这种感觉是很不一样的。我想这可能就跟房车旅行与自驾游的区别是一样的吧。&lt;/p&gt;
&lt;p&gt;那个优秀的背包也陪我走过了很多个省份，在无数个火车站与机场过夜，也算是我新认识里面最可靠的朋友了。我想继续走下去，因为它，我可以触及到更多景点之外的世界与山河。&lt;/p&gt;
&lt;h2&gt;持续单身&lt;/h2&gt;
&lt;p&gt;诸多原因叠加之下，感情生活并没有任何实质性的发展，工作已经占满了生活大多数的情况下，在选择跑步作为排解压力的方式后，其实并没有剩下什么时间了。也或许是因为年纪到了福报来了，也或许是 remote 工作的两点一线。总之并没有认识什么新的人，也没有了太过于强烈的恋爱欲望。&lt;/p&gt;
&lt;p&gt;后来在朋友圈上看到了一个曾经相亲过，但是被我向朋友大诉苦水的女生，她发了被求婚的视频，我也觉得挺好的，那小伙子也很好看。那时候我就在心里想，或许那个出了问题的人真的是我。
父亲从去年过年就开始说我今年犯太岁，诸事不顺。我想这也是他们今年没有使劲让我相亲的原因吧，但大概我想他们或许多少也对我失望了。可事情发展至此，我也无力无心去狡辩谁对谁错，一切都是命运的安排罢了。&lt;/p&gt;
&lt;h2&gt;尝试创业&lt;/h2&gt;
&lt;p&gt;自从23年5月正式失业之后，当时的我就开始尝试去做出一些作品来养活自己，可后来毫不意外地全都失败了。那段时间每天都在写代码，从早到晚，却总感觉在做无用功。做着做着就发现方向不对，推倒重来；重来之后又发现另一个问题，再次推倒。最后连一个像样的产品都没能上线，就这样不了了之。&lt;/p&gt;
&lt;p&gt;在多次复盘后，我基本总结出了两点极其关键的问题：一是在前司长期做toB业务并没有太多的toC产品的积累，导致了业务重点一直没抓住，做着做着就做偏了；二是当时还没有很深入的了解Vibe Coding，导致产出速度特别低。技术选型上也走了不少弯路，总想着把架构做得完美，结果功能还没完成，自己就已经失去耐心了。&lt;/p&gt;
&lt;p&gt;再经过这一年在新公司的高强度toC历练之后，外加 AI 的快速发展，我又觉得自己可以了，便又重新开始做一些尝试。这次不一样了，至少我知道什么是用户真正需要的，也知道如何快速验证想法。虽然客观上讲大概率还是会失败，但我想再试试，说不定就成了呢。失败并不可怕，可怕的是连尝试的勇气都没有了。&lt;/p&gt;
&lt;h2&gt;欧亨利式结尾&lt;/h2&gt;
&lt;p&gt;众所周知，我也是玩Crypto的，在挣不到钱之后，就转向 Crypto 的活期理财方向+套利方向，主打一个无损挣高年化。 在这个事实基础之上，我让老板把我的部分工资转成Crypto发给我，而后喜剧性的事情就发生了。&lt;/p&gt;
&lt;p&gt;老板说这是干净的U，代理商那边协助发U的小伙伴也说这是干净的U，我又没有在意，直接把它转到我的CEX屯屯鼠钱包里面去了。而后CEX因为脏钱封控，直接锁了我的帐户，现在里面的钱只能看不能摸了，直接几年白干。 行文之日，帐户仍未解锁，但愿一切能好起来。&lt;/p&gt;
&lt;h2&gt;最后的最后&lt;/h2&gt;
&lt;p&gt;后来，我跟朋友出来吃饭闲聊的时候，我常跟他叹息我对人生失败的态度，可他却说：「你要不要回想一下，现在的生活不就是你前几年期望的模样吗」。沉默片刻，我在失语中承认了这个事实。人生的变更还是需要把生命的跨度拉长才能察觉出改变。&lt;/p&gt;
&lt;p&gt;不知不觉，三十而已。哎，悠悠三十载，书剑两无成。再加油，再努力。&lt;/p&gt;
</content:encoded></item><item><title>2024: 失业、啤酒与拣爱</title><link>https://www.kilerd.me/summaries-my-2024/</link><guid isPermaLink="true">https://www.kilerd.me/summaries-my-2024/</guid><description>喝着啤酒的我开始回顾我的2024，似乎失败总是伴随着我一年又一年，心中的莫名空虚始终无法挥之而去，莫名其妙而又习以为常。 我老师一直跟我说：「你不缺朋友，只是缺个女朋友」，可是我的心情波动似乎也没有因为跟前妻姐交往过程中变得平和，可能也是她常跟我说「你根本不懂恋爱」般，我也搞不懂我自己，搞不懂自己的动机，搞不懂自己的行</description><pubDate>Tue, 14 Jan 2025 16:01:26 GMT</pubDate><content:encoded>&lt;p&gt;喝着啤酒的我开始回顾我的2024，似乎失败总是伴随着我一年又一年，心中的莫名空虚始终无法挥之而去，莫名其妙而又习以为常。&lt;/p&gt;
&lt;p&gt;我老师一直跟我说：「你不缺朋友，只是缺个女朋友」，可是我的心情波动似乎也没有因为跟前妻姐交往过程中变得平和，可能也是她常跟我说「你根本不懂恋爱」般，我也搞不懂我自己，搞不懂自己的动机，搞不懂自己的行为，也搞不懂自己的心情。&lt;/p&gt;
&lt;p&gt;辗转反侧多年，今天我似乎并不怎么在意了，莫名的习得平和不能说让我变好的，也可以说我变得冷淡了。「无所谓啦」也尝尝挂在了自己的嘴上，与其在意莫名其妙的情绪，不如安安静静的看一会书、听一会歌，然后睡个好觉。&lt;/p&gt;
&lt;h2&gt;失业&lt;/h2&gt;
&lt;p&gt;打从知道公司走向了下坡路，我便尝尝与我的HR开玩笑：「什么时候公司给我一个大礼包」。可是当公司确定裁掉我的时候，那种莫名其妙的失落还是攀上了心头，即便是你已经知道结果的情况下。&lt;/p&gt;
&lt;p&gt;相比于其他同事那种失业便宴请四方的做法，我选择了默默收拾留在办公室的物品，跟新来的行政交还了工牌，结束了在公司长达5点多的外包生涯。&lt;/p&gt;
&lt;p&gt;失业的当天，跟公司几个老朋友吃了最后一顿饭，他们都表现出了诧异，表示我没理由会被辞退。我也只能苦笑，不知说啥好。后来有几个公司的老朋友给我微信问我需不需要跟高层讲一讲把我重新拉回去，给我assign一个项目就可以了，其中包括了我的老板。实话说我很感激他们，他们真真切切地想把我留下来，可都被我一一拒绝了，原因是公司在亚太重组的过程中，对于员工功能的反馈通道已经完全失效，这一次我能被辞退，那下一次必然也会榜上有名，那又何必苦苦地熬死在一个地方呢，不如潇潇洒洒地离去。&lt;/p&gt;
&lt;p&gt;打心底里讲，公司对我来俨然成为了一座围城，长期做外包的我失去了技术上的足够市场竞争力，得依赖公司的项目和知名度来谋取薄弱的工资；而又因为公司的外包工作让我没办法在技术上得到比较好的成长。我个人其实并没有能力跳出这样怪圈与循环，表现在上一年中的多次求职跳槽都没能成功拿到 offer。这次的辞退像是命运让我强制地往前走。&lt;/p&gt;
&lt;p&gt;失业后，除了没有工资了，其实短时间内生活并没有太大的改变，在公司长期生意不好的情况下，我早就是在家办公，在家自己学习技术的模式，一时间也没有陷入失业的心态与困境。&lt;/p&gt;
&lt;p&gt;这个改变发生在搬家后。我在失业不久后便从深圳把行李彻底搬回广州。我既需要一个零房租的环境来减缓自己的经济压力，也要离开深圳这个满是回忆的地方重新开始一段新的人生。从搬家到打扫、整理新家，自己彻底闲下来之后，那种人生的不确定感便涌上了心头，让整个变得很焦虑，急迫地想找到一份有足够收入的「活」。这种焦虑其实并不是来自于没钱，而更多的是一种对未来的迷茫。&lt;/p&gt;
&lt;h2&gt;啤酒&lt;/h2&gt;
&lt;p&gt;失业一个人在家，焦虑的时候，啤酒和炸鸡真的是成为了排解夜晚空虚的良药，当每天跑完步回来走在村口上，总会想着要不来一份炸鸡，来一瓶啤酒。因为平时有跑步的习惯，炸鸡啤酒对我来说并没有长胖的压力，他更多的承担了「晚上干什么」的位置。 一份炸鸡、一瓶啤酒、一部电影便是那段时间我夜晚的常态。&lt;/p&gt;
&lt;p&gt;说来奇怪，前些年我还需要通过酒精来让自己更好的入睡，而那段时间并不在这个方面依赖酒精，我也不清楚我是不是正在依赖而并不自知。&lt;/p&gt;
&lt;h2&gt;拣爱&lt;/h2&gt;
&lt;p&gt;在失业之后我选择快速搬离深圳的原因之一便是，当一个人深夜坐在电脑面前、坐在沙发上、坐在床上，脑子里面就会浮现出与前妻姐在房子里面的一举一动，即便是脸庞与声音都模糊不清，也会在酒精的作用下感官变得无比敏感。&lt;/p&gt;
&lt;p&gt;拣爱是一款游戏，它表述了一堆情侣从相识、相恋、分开的全过程。&lt;/p&gt;
&lt;p&gt;从一开始简单的跟随游戏出现的选项来进行游戏，最后得到了一个「钢铁直男」的极差结果。像极了一堆情侣一开始全凭个人感觉进行恋爱，最后坐在草坪上轻声细语地讨论这之前的林林总总，然后和平分手。&lt;/p&gt;
&lt;p&gt;后来开始摸索游戏里面非线形下的内容与叙事，也得到了一个比较好的结果。这玩起来像一对分手后的情侣又重新复合，大家小心翼翼地探索着对方，从而走得越来越远。&lt;/p&gt;
&lt;p&gt;它是一个好游戏吗？我觉得是的，游戏里面的复盘让我在游玩的时候过往的记忆不断浮现的脑子里面，也让我真切地感受到了对方那一刻为何失落或愤怒。&lt;/p&gt;
&lt;p&gt;它是一个坏游戏吗？我觉得也是的。我在想到底是第一边的「钢铁直男」是我本性；还是重复了6、7边之后的完美通过，我是那个「绝世好男」。我没办法分辨，我只感觉我在不耐烦中点掉那些重复了一边又一边的对话，我此刻只想点击到那个「正确」的选项，以此来修正我过往产生的错误。&lt;/p&gt;
&lt;p&gt;可是，现实是一个不能重来的游戏，现实也没有人来告诉你在什么地方、什么时刻选择了一个错误的答案。&lt;/p&gt;
&lt;p&gt;当你知道如何「完美」地去「爱」一个，会为了一个「正确的答案」去精心准备各种话术与准备，那这是真诚的爱还是处心积虑的利弊选择呢。我没有答案，游戏也没有给出答案。&lt;/p&gt;
&lt;p&gt;或许现实就是我们每个人在这个世界上成长至今，都没人来教会我们什么是爱、怎么去爱，只能用我们的亲生经历笨拙地去学习看到的一切，那些来自父母、来自文学、来自影视作品、来自游戏的爱。可学习到的永远都只能借鉴使用，那不属于自己。真正爱上一个人的时候永远都是手忙脚乱的。&lt;/p&gt;
&lt;p&gt;爱就勇敢爱，不爱就不爱了，独自一个人好好休息。没有人会在爱中指责你的愚钝，我们都只是在做自己想做的事情。&lt;/p&gt;
&lt;p&gt;酒尽，言也尽。&lt;/p&gt;
&lt;p&gt;又活过了一年，真好。向前走，别回头。&lt;/p&gt;
</content:encoded></item><item><title>Shadcn/ui: 你应该只需一个 web UI 框架</title><link>https://www.kilerd.me/you-need-shadcn-ui/</link><guid isPermaLink="true">https://www.kilerd.me/you-need-shadcn-ui/</guid><description>鲁迅说过，我的技术栈里面有两个前端框架，一个是 shadcn/ui，另一个也是shadcn/ui。 这篇文章我们就来讨论一下周树人为什么要暴打鲁迅</description><pubDate>Sat, 12 Oct 2024 15:37:27 GMT</pubDate><content:encoded>&lt;p&gt;鲁迅说过，我的技术栈里面有两个前端框架，一个是 shadcn/ui，另一个也是shadcn/ui。&lt;/p&gt;
&lt;p&gt;这篇文章我们就来讨论一下周树人为什么要暴打鲁迅&lt;/p&gt;
&lt;h2&gt;我做过的蠢事&lt;/h2&gt;
&lt;p&gt;人总是善变的，从接触到前端开发开始，我就一直在关注各式各样前端好看的框架。在变化万千的前端世界里面，总能一段时间就会涌出一个好看的框架。
从一开始的&lt;a href=&quot;https://tabler.io/admin-template&quot;&gt;tabler&lt;/a&gt;，后来到台湾人写的一个很小众的&lt;a href=&quot;https://tocas-ui.com/5.0/zh-tw/index.html&quot;&gt;tocas ui&lt;/a&gt;，再到后来的 &lt;a href=&quot;https://mantine.dev/&quot;&gt;mantine&lt;/a&gt;，到现在的 shadcn/ui。&lt;/p&gt;
&lt;h2&gt;为什么一直换&lt;/h2&gt;
&lt;p&gt;在抛不开颜值的前提下，我一直在找一个比较合适非专业前端使用的框架，目标在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;颜值至少在中等水平之上&lt;/li&gt;
&lt;li&gt;可定制化高&lt;/li&gt;
&lt;li&gt;可以用在react生态下&lt;/li&gt;
&lt;li&gt;组件足够丰富&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前两个框架的选择纯粹是出于颜值的考虑，而当我开始大规模的使用在前端项目中时，就发现了这两个框架的弊端与缺陷，于是就跑步进入了第三个框架的时代。&lt;/p&gt;
&lt;h2&gt;与 Mantine 的蜜月期&lt;/h2&gt;
&lt;p&gt;Mantine 真的是我超级喜欢的一个框架，它基于了足够优秀的 UI 基础，让你直接套用框架都可以写出颜值中上的网页，这对非前端专业选手来说是一个超级棒的点，我们只需要往里面塞内容就可以了，完全不需要自己额外地增加写前端组件的负担。&lt;/p&gt;
&lt;p&gt;同时 Matine 是我见过为数不多组件足够丰富、使用体感最舒服的框架。简单举几个例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在表单里面带自动补全的输入框 &lt;a href=&quot;https://mantine.dev/core/autocomplete/&quot;&gt;AutoComplete&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;甚至它自带了 &lt;code&gt;onMissing&lt;/code&gt; 的逻辑处理&lt;/li&gt;
&lt;li&gt;我们常常需要用来做标签输入的 &lt;a href=&quot;https://mantine.dev/core/pills-input/&quot;&gt;PillInput&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;多个值选一个的切换器 &lt;a href=&quot;https://mantine.dev/core/segmented-control/&quot;&gt;SegmentedControl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ColorInput&lt;/li&gt;
&lt;li&gt;PinInput&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;各式各样你需要的，你以后需要的都能在里面找得到，另外他的人体工学也是很棒的。&lt;/p&gt;
&lt;p&gt;有一个场景是你需要在各个地方弹出同一个Modal层展示数据，如果是其他框架，你需要在每个页面写一个Modal，然后单独控制它的展示或隐藏，而 Mantine 采用了类似于Context 注册的模式，
你只需要在Context注册你的Modal类型，然后调用 &lt;code&gt;modals.openConfirmModal&lt;/code&gt; 就可以完成Modal的展示，这样你的代码就可以统一管理了。&lt;/p&gt;
&lt;p&gt;感兴趣的具体可以了解一下：&lt;a href=&quot;https://mantine.dev/x/modals/&quot;&gt;https://mantine.dev/x/modals/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;同时 Mantine 还集成了极其强大的 hooks 库，一些很常用的 hook 功能它都能给你直接提供了。
例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从 LocalStorage 取数据的 use-local-storage&lt;/li&gt;
&lt;li&gt;debounce state 的 use-debounced-value&lt;/li&gt;
&lt;li&gt;专门为开关/boolean管理的 use-disclosure&lt;/li&gt;
&lt;li&gt;检测用户是否离开页面的 use-page-leave&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://mantine.dev/hooks/use-click-outside/&quot;&gt;Mantine Hooks&lt;/a&gt;的强大和完善真的很省事，它帮你解决了前端里面很多很常见的场景，避免了自己去写重复的代码、封装hooks。 真正地做到了开箱即用。&lt;/p&gt;
&lt;h2&gt;我变了&lt;/h2&gt;
&lt;p&gt;人总是善变的，Mantine 逐渐开始不能满足我的需求了，经常出现了一些让我很难受、但是迫于现状我又得去默默忍受的情况：&lt;/p&gt;
&lt;p&gt;Mantine 自带的水平布置控件 &lt;code&gt;Group&lt;/code&gt; (等驾驭 tailwind 的 &lt;code&gt;flex item-center&lt;/code&gt;) 是默认自带 &lt;code&gt;gap&lt;/code&gt; 的，而且最小值只有 &lt;code&gt;xs&lt;/code&gt;，并不能设置成 &lt;code&gt;none&lt;/code&gt;。
在这种情况下，当你碰到一些需要根据不同的输入展示不同的内容，而他们又是紧紧贴合的时候（例如金额的 &lt;code&gt;-&lt;/code&gt; 号），&lt;code&gt;Group&lt;/code&gt; 就用不上了，你就得手写 css，而 Mantine 又不自带 tailwind，导致了代码里面出现了一些很神经质的割裂。&lt;/p&gt;
&lt;p&gt;另外一个例子是Mantine的高度封装让你没办法深度定制。 在 Mantine 退出 Chart 系统时，我第一时间就跟进，把自己手搓的 Chart 系统给改了。
结果是网站的样式得到了统一，毕竟mantine作者的css水平比我高到不知道哪里去。可是它功能出现了一定量的缺失。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/zhang-accounting/zhang&quot;&gt;Zhang Accounting&lt;/a&gt; 项目中首页是有一个复合表格的，一个线图来表示账户余额，一个柱状图用来显示每日的收入与支出。
自己手搓的时候可以直接使用 &lt;code&gt;ComposeChart&lt;/code&gt; 就可以完成功能了，但是 Mantine 迟迟没有引入这个集成。我大概在 5 月份的时候就这个问题&lt;a href=&quot;https://github.com/orgs/mantinedev/discussions/6230&quot;&gt;发了一个提问&lt;/a&gt;，大概得 7 月底的时候才把&lt;a href=&quot;https://github.com/mantinedev/mantine/commit/f5ef82fc33c2a9df7d2b28d710aeef6be90a30b9&quot;&gt;功能给实现&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在需要自己尝试去改一些东西的时候，Mantine 这种集成度很高的框架使用起来就很难受了。&lt;/p&gt;
&lt;h2&gt;shadcn/ui 神兵天降&lt;/h2&gt;
&lt;p&gt;没错，就选它！都给她亮灯！ shadcn/ui 似乎在业界里面称之为 headless ui，我也不太懂，但是她却是解决了我上面的各种难点。&lt;/p&gt;
&lt;p&gt;一是 css 的统一，基本上你就是在写 tailwind，无论是组件自带的参数还是自己定制的参数都是 tailwind 的原生类，也少了一些类似于 &lt;code&gt;Group&lt;/code&gt;， &lt;code&gt;Stack&lt;/code&gt; 和 &lt;code&gt;Grid&lt;/code&gt; 的重复封装，因为 tailwind 里面就可以很轻松的实现这些功能了。&lt;/p&gt;
&lt;p&gt;二是 shadcn/ui 的组件都是单独引入到你的 &lt;code&gt;src/component&lt;/code&gt; 文件夹里面的，而不是当作一个依赖引入，所以你可以很轻松的去更改他们。&lt;/p&gt;
&lt;p&gt;三是 AI 自动补全/代码生成的场景下，对于基于 tailwind 的UI库来说体验太好了，AI可能不能理解 mantine自己写的东西，但是它完全可以理解 tailwind，所以补全出来的代码可用性极其高。
同时 shadcn/ui的流行度也很高，所以补全的可靠性也比其他库高很多。&lt;/p&gt;
&lt;p&gt;四是 因为 shadcn/ui 的流行度高，所以社区里面会的人也多，意味着如果你的项目是开源项目的话，贡献者无需通过学习就可以直接贡献你的项目。&lt;/p&gt;
&lt;p&gt;一个额外的好消息是：上面讲到的很好用的 hooks 库 &lt;code&gt;@mantine/hooks&lt;/code&gt; 是一个没有依赖的纯粹库，意味着他可以用在任何UI框架里面，这也是我保留在我项目里面的唯一 mantine 遗产。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;选 shadcn/ui 无论是何种意义上都是能胜任作为web ui的第一梯队的，虽然它的组件丰富度并不够 mantine 高，但是已经能涵盖 90% 以上的业务场景了。所以选她！&lt;/p&gt;
</content:encoded></item><item><title>iPhone 下的 99% 自动化记账</title><link>https://www.kilerd.me/iphone-sms-financial-worker/</link><guid isPermaLink="true">https://www.kilerd.me/iphone-sms-financial-worker/</guid><description>因为一些众所周知的原因，国内的大部分银行都无法通过API的方式访问到个人账户下的账单，同时因为支付宝与微信的普及，银行系统里面通常记录的交易信息也是不完整的，以至于在中国环境下实现自动化记账是一件很麻烦的事情。经过多次更改后，我的记账模式已经做到了相当高的自动化，于是便有了这篇文章，记录并分享一下我是如何进行自动化记账</description><pubDate>Wed, 17 Apr 2024 10:00:00 GMT</pubDate><content:encoded>&lt;p&gt;因为一些众所周知的原因，国内的大部分银行都无法通过API的方式访问到个人账户下的账单，同时因为支付宝与微信的普及，银行系统里面通常记录的交易信息也是不完整的，以至于在中国环境下实现自动化记账是一件很麻烦的事情。经过多次更改后，我的记账模式已经做到了相当高的自动化，于是便有了这篇文章，记录并分享一下我是如何进行自动化记账的。&lt;/p&gt;
&lt;p&gt;前提条件、准备：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;iPhone 一台&lt;/li&gt;
&lt;li&gt;银行 APP （例如：招商银行）&lt;/li&gt;
&lt;li&gt;个人域名&lt;/li&gt;
&lt;li&gt;cloudflare 账号&lt;/li&gt;
&lt;li&gt;个人服务器&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们会把整个流程分成三个部分来讨论：数据采集端、数据处理端、数据修正端&lt;/p&gt;
&lt;h2&gt;数据采集端&lt;/h2&gt;
&lt;p&gt;本阶段的核心是我们需要把银行消费的短信通知采集到，然后推送给有编程能力的处理器。&lt;/p&gt;
&lt;h3&gt;银行交易短信开通&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/statics/images/iphone-sms-financial-worker-1.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这是一切的开始，我们需要在银行APP中开启「账务变动通知」，并且把「通知起点金额」设置为0，这样我们就可以通过短信来接受到所有银行卡的变动&lt;/p&gt;
&lt;h3&gt;Shortcut 自动化&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/statics/images/iphone-sms-financial-worker-2.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;iOS因为安全的考虑，应用程序除了「短信过滤」分类之外并没有能力感知和处理短信，可是自动化中却存在一种神奇的自动化「短信：当收到妈妈的短信时」，他是一种可以根据「短信发件人」和「短信关键字」来触发自动化的触发点。&lt;/p&gt;
&lt;p&gt;光有这个前提还不可以完成数据的采集流程，以下两点才是关键：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有交易短信都包含了&lt;strong&gt;&lt;strong&gt;招商银行&lt;/strong&gt;&lt;/strong&gt;四个字，所以我们可以采用它为触发点&lt;/li&gt;
&lt;li&gt;自动化中，可以通过「输入快捷指令的短信」这个参数获取到具体的短信内容&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么我们可以把它发送到一个&lt;strong&gt;数据处理端&lt;/strong&gt;中真正的处理我们的短信，所以我们的自动化其实是做了以下几件事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当短信内容包含「&lt;strong&gt;招商银行&lt;/strong&gt;」四个字时，触发自动化&lt;/li&gt;
&lt;li&gt;把短信内容作为 payload 以 HTTPS 的方式（自动化中的执行步骤叫「获取URL内容」）发送至&lt;strong&gt;数据处理端&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;数据处理端&lt;/h2&gt;
&lt;p&gt;这里我们采用了 cloudflare worker 来作为我们的数据处理端，从上文可知，我们的短信会以https post的方式发送到worker 中来。 那么我们就可以通过以下代码在worker中取出短信内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const request_payload: { sms: string } = await request.json();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么我们就需要做数据提取了，招商银行的短信可以简单的分为好几类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;支出消费&lt;/li&gt;
&lt;li&gt;Apple pay 支付&lt;/li&gt;
&lt;li&gt;退款&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么我们就以支出消费来举例子，一个简单的支出短信是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;【招商银行】您账户0000于04月09日06:43在【支付宝-payee】快捷支付114.15元，余额9999.99 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么我们就可以通过正则的模式来取出交易账户、时间、payee、金额 等信息，这里我的正则大概长这个样子&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const CONSUME = /您账户(?&amp;lt;account&amp;gt;\d+)于(?&amp;lt;month&amp;gt;\d+)月(?&amp;lt;day&amp;gt;\d+)日(?&amp;lt;hour&amp;gt;\d+):(?&amp;lt;min&amp;gt;\d+)在【(?&amp;lt;payee&amp;gt;[^】]+)】快捷支付(?&amp;lt;amount&amp;gt;\d+(\.\d+))元/;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我们用的是正则中 named group 的写法，在真正的处理流程中，我们可以通过以下的代码获取到短信的细节&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const match_consume = request_payload.sms.match(CONSUME);
if (match_consume) {
    // @ts-ignore
    const { groups: { account, month, day, hour, min, payee, amount } } = CONSUME.exec(request_payload.sms);
    // add your logic here...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自此我们获取到了短信的交易时间、交易对手、金额，接下来就可以把它转换成复式记账的账户了&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const ACCOUNTS = {
&apos;0000&apos;: &apos;Assets:CMB-DebitCard&apos;
};

const target_account = ACCOUNTS[account] ?? DEFAULT_ACCOUNT;

const PAYEE = {
&apos;肯德基&apos;: &apos;Expenses:Eat&apos;
};

const target_payee = PAYEE[payee] ?? &amp;quot;Expenses:FixMe&amp;quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后可以构建 API 请求，把数据发送到 zhang 或者 fava 等具有API请求能力的服务端。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;const payload = { 
    // construct the json payload based on your financial tool
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自此我们就可以完成了把数据处理并推送至服务端储存的流程，如果你在使用 zhang，那么就可以在web UI中查看到一条记录&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/statics/images/iphone-sms-financial-worker-3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3&gt;Expenses:FixMe&lt;/h3&gt;
&lt;p&gt;关于那些payee 无法分类的，我目前的做法会分配到 &lt;code&gt;Expenses:FixMe&lt;/code&gt; 中，并把交易标注成 &lt;code&gt;Waring&lt;/code&gt;  ，以便在后续的流程中重点修正。&lt;/p&gt;
&lt;h2&gt;数据修正端&lt;/h2&gt;
&lt;p&gt;虽然我们完成了数据的采集，但是就如同上图的交易以下，我们只获取到了&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;payee 为 &lt;strong&gt;上海拉扎斯信息科技有限公司&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;narration 为空&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际上，这是一笔饿了么消费，金额流转路经为 &lt;code&gt;饿了么&lt;/code&gt; → &lt;code&gt;支付宝&lt;/code&gt; → &lt;code&gt;招商银行&lt;/code&gt;  。我们在饿了么上购买了什么商品早已经在一层一层数据流转中消失了，这时候我们就需要人工介入做数据修正工作了。&lt;/p&gt;
&lt;p&gt;目前来说对于 zhang，有两种方式做数据修正： web ui 和 手机APP。 这里我们就用手机APP来举例，我们可以选择晚上睡觉前把一天的数据再次回顾和修正&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/statics/images/iphone-sms-financial-worker-4.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;通过 APP 的交易编辑功能可以完成交易的修正&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/statics/images/iphone-sms-financial-worker-5.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;一些注意的点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;建议最后的消费卡集中到同一张卡中，因为整个流程的前提是基于银行短信通知来实现的，而银行的短信通知是需要收费的，3元一张卡，所以集中到同一张卡可以降低服务费的支出&lt;/li&gt;
&lt;li&gt;前提准备中提及到了个人域名，这里主要是两个用途
- cloudflare 的 worker 绑定个人域名的访问成功率高很多
- 个人账本需要可以被 worker 直接访问，所以也需要绑定个人域名，当然也可以使用 clouldflare tunnel 来做内网服务穿透&lt;/li&gt;
&lt;li&gt;如果你家里有多的iPhone和 NAS，可以通过iPhone自带的短信同步功能同步到家里内网备用机，备用机执行Shortcut自动化，worker 和账本都放在nas内网中，避免账本因配置不当暴露在公网&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>东澳岛之旅：理想与现实</title><link>https://www.kilerd.me/trip-of-dongao-island/</link><guid isPermaLink="true">https://www.kilerd.me/trip-of-dongao-island/</guid><description>4月29号，朋友问我五一去哪里玩，我想都没想就说了哪都不去。事实上也是这样，并没有任何出游的计划，但是回头仔细想想五一假期那么长，没理由那么多天都在家里吧。于是就挑了一个短程旅行，目标地点就是东澳岛。</description><pubDate>Thu, 05 May 2022 09:38:36 GMT</pubDate><content:encoded>&lt;p&gt;4月29号，朋友问我五一去哪里玩，我想都没想就说了哪都不去。事实上也是这样，并没有任何出游的计划，但是回头仔细想想五一假期那么长，没理由那么多天都在家里吧。于是就挑了一个短程旅行，目标地点就是东澳岛。&lt;/p&gt;
&lt;p&gt;对于东澳岛，其实已经早早地躺在了我的出游目的地里面了，但是由于疫情，深圳前往岛的渡轮一直都处于停航状态，恰逢五一，深圳和珠海的疫情都得到不错的控制，难得的开放了数天的渡轮，于是一上头便买下了2号去4号回的返程渡轮。其他的关于吃住玩就没有过多的考虑，这也算是个人习惯，我本来就不喜欢那种安排得满满当当的出行。于是决定慢慢筹备，反正距离出行还有好几天呢。&lt;/p&gt;
&lt;p&gt;当晚回到家后开始谋划住宿。在我印象中东澳岛绝对是珠海众多海岛中开发程度和商业化程度第一梯队的存在，而当我在诸多酒店、民宿平台上查询住宿时，我整个人都惊呆了。只有一家酒店，同时剩下的就只有一个3000块的海景套间，怎么想都不适合我这种一个人出门的人。&lt;/p&gt;
&lt;p&gt;当时，一个念头突然涌上心头「露营」。没错，就是露营，之前在关注「马蜂窝」这个网站的时候看到过很多关于东澳岛如何露营的文章，也确信这是一个可行的方案，同时考虑到我这种摄影人怎么可能会安心地呆在酒店里面呢，能在酒店里面呆几个小时就不错了。考虑下来露营绝对是我这次出行的最佳方案。要命的是我完全没有留意最后的文章时间停留在了2020年，这绝对给这次出行留下了巨大的伏笔。&lt;/p&gt;
&lt;p&gt;海岛嘛，等价代名词就是日落、日出、海浪、星空、无光污染，这恰好命中摄影人的职业病。而我也在这几个名词和露营的组合下，对出行有了一个基本的构想：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2号抵达岛上后，下海游泳、赶往露营基地扎营过夜，看日落&lt;/li&gt;
&lt;li&gt;3号游玩岛上的景色，晚上去山顶扎营，拍星空&lt;/li&gt;
&lt;li&gt;4号早早起床，拍日出&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我寻思着有着露营基地的地方对于我这种露营新手估计难度应该不大，也没有过多的往坏处想。于是在30号便启程去把露营装备买齐了。&lt;/p&gt;
&lt;p&gt;本以为万事具备，只欠东风。而现实就是那么残酷，往往不能跟你想象的完美契合。&lt;/p&gt;
&lt;h2&gt;启程&lt;/h2&gt;
&lt;h3&gt;登船&lt;/h3&gt;
&lt;p&gt;谁又能想象得到晴空万里的29号、30号，在踏入五月的那个夜晚会风雨交加呢，倾盆的大雨一度让我打消了出行的念头，但是在查了天气预报和卫星图后，推测一番感觉2号会重新回到那个万里无云的天气。而这个1号的大雨天直接让当天的渡轮停航了，估计也劝退了大部分2号出游的旅客。以至于2号当天去程的船上几乎就没有什么人。&lt;/p&gt;
&lt;p&gt;之前出现过出门忘记带相机储存卡、忘记给相机充电的情况，所以我养成了写出行checklist的习惯，写清楚了前一天需要做什么，当天出门需要做什么的详细列表。&lt;/p&gt;
&lt;p&gt;2号当天9点起床整理东西并对着checklist 做一项一项的检查，发现没有&lt;strong&gt;任何&lt;/strong&gt;遗漏后，便出门。估摸着9点50下楼，吃个早餐10点出发去渡口时间绰绰有余，但是在吃早餐的时候突然想起来自己好像少带了稳定器跟相机的连接线。即便是那条数据线长得跟普通的Tpye-C 数据线没任何差区别，但是我还是不敢冒这个风险，一旦不兼容，我的稳定器就等于是白带了。那么这一折腾、一来一回就消耗了我20分钟。&lt;/p&gt;
&lt;p&gt;就是这20分钟的戏剧性，当我飞奔到渡口大厅的时候，在国内出发的那个区域，检票的小哥一直在重复喊「耳先生，在吗？ 耳先生，你到了吗？」。不了解的还以为是什么达官贵人要坐渡轮去哪里巡查游玩，殊不知只是一个即将要迟到、赶不上渡轮的倒霉蛋，原因竟是一条数据线引发的「血案」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/statics/images/trip-of-dongao-hk-airport.jpg&quot; alt=&quot;香港机场&quot;&gt;&lt;/p&gt;
&lt;p&gt;当我踏上船的那一刻，渡轮的登船口也应声关闭，近乎1个小时的时间可以极致放松的躺在椅子上，看着外面的海浪和乌云。同时也没有想过疫情过后能以这样的方式再次见到香港机场，心里不禁一阵唏嘘。&lt;/p&gt;
&lt;h3&gt;露营&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/statics/images/trip-of-dongao-island-dock.jpg&quot; alt=&quot;东澳岛码头&quot;&gt;&lt;/p&gt;
&lt;p&gt;踏上岛的那一刻，并没有我印象中对于海岛的那种「刻板印象」，我对海岛的印象还停留在了泰国普吉和台湾垦丁上，那广阔的海岸，一望无际的海平线，那条蜿蜒曲折的滨海小街。而东澳岛映入眼帘的却是寂寥和荒漠。基本上没有太多人影。&lt;/p&gt;
&lt;p&gt;找个地方放好行李歇息几分钟的时间，整个码头又恢复到了一如既往的门可罗雀。想想也合理，所有的酒店和海岸都在那一头，每天也就那么几班渡轮在此停靠。连服务中心那售票小姐姐都搬起了小椅子，嗑起了瓜子来。&lt;/p&gt;
&lt;p&gt;一个饭店老板娘向我招了招手「小伙子吃饭还是住宿啊」。完全不知道居然还有民宿的我在惊讶之余还是跟老板娘唠嗑起来了。原来那200块的小房间只是他们饭店二楼的一个小隔间，闲置着没用就改造起来当民宿，期望在疫情期间能回一点本就回一点本。但是哪个傻子会没定好酒店就出发来岛上旅行呢，毕竟一下船就只能等第二天才有渡轮回去了。&lt;/p&gt;
&lt;p&gt;为了保险期间，我问老板娘要了名片，寻思着如果实在找不到地方扎营，那至少有一个保障不至于睡街上。&lt;/p&gt;
&lt;p&gt;东澳岛号称92%的绿植覆盖率，再加上每天只有寥寥数班的渡轮，这意味着什么呢？意味着岛上的人数上限得到了极大的控制，再加上1号的大雨天，岛上更是廖无人烟。在一路朝海岸走去的路上，除了几个建筑工人外，似乎整个岛就只有我一个人了。哪怕是我走到了沙滩上，岛上的人数也不过十数人，似乎跟我同一班渡轮过来的人都消失得无影无踪。&lt;/p&gt;
&lt;p&gt;在海岸边拍了一小会便来到了3点多，我觉得先去扎营基地踩好点，以免晚上太晚来没有位置或者工作人员下班之类的。扎营基地在岛的另一边，这一次的穿越还是只有自己一个人，路上也只能碰到寥寥数个建筑工人和建筑工地几条土狗。当我到达大竹湾，也就是地图和玩家口里说的官方扎营基地，当看到竖立在海岸边的警告牌「禁止烧烤、禁止游泳、禁止露营」时，觉得唯独「露营」二字尤为刺眼。伴随着呼啸而过的海风，心里直冒出「完了」两个字。&lt;/p&gt;
&lt;p&gt;此时我已经把整个岛上有人类迹象的地方都逛遍了，唯独没见到任何跟扎营相关的人烟。恰逢路过一个保安大哥，便急忙上前询问相关事宜，大哥表示这个沙滩确实已经不让扎营了，岛上唯一让的地方是码头边上的「文化广场」。&lt;/p&gt;
&lt;p&gt;码头？文化广场？我刚从那里过来的，那里不像是也没见到露营相关的东西啊。我当时指了一指山顶的大露台，问大哥「那里可以扎营吗」。我心想如果晚上我拍照应该是留在山上的，可以顺势扎营过一晚。大哥表示山上可以随便扎，但是要小心有蛇，同时表示晚上保安不会上山巡查，还说隔壁的塔里面没那么大风，扎营会舒服一点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/statics/images/trip-of-dongao-road.jpg&quot; alt=&quot;东澳岛唯一的路&quot;&gt;&lt;/p&gt;
&lt;p&gt;道谢完大哥后，一路闲逛到码头，一为了确认文化广场的露营地，二为了吃饭。在原路返回码头的路上终于看到了零星的人，看来吃饭始终是第一位。这时就不得不谈谈岛上的饮食了，珠海嘛，又是海岛，来了不可能不吃海鲜，但是吧，海岛上的餐饮似乎形成了某种意义上的垄断，也形成了诸如广州黄沙和南海一般的「市场自主购买，店家帮忙加工」的合作模式。可是问题就出现在所谓的「统筹规划」：岛上只有寥寥几家的餐厅，大概率是跟卖海鲜的贩子合作的，甚至是一伙的。不知道是天气问题还是本来如此，贩子那里贩卖的海鲜也不见得多新鲜，只靠着打氧机输送的氧气勉强苟活。&lt;/p&gt;
&lt;p&gt;垄断必然出现劣币驱逐良币，餐厅师傅的手艺也不见得有多好，总之一顿饭下来给我的感觉甚至不如广州的随便一家大排档，可这是你能在岛上找到为数不多的饭馆了。听说3000块的贵价酒店里面有着自己的餐厅，我知道我应该进不去。&lt;/p&gt;
&lt;p&gt;饭后闲逛一会，依旧没发现文化广场有任何扎营相关的工作人员，虽然文化广场开着门，开着展览，但是一个工作人员都没有。折返回码头游客中心询问那位嗑瓜子的小姐，得知岛上已经没有了任何官方的露营基地。也就是说现在我只有两个选择：一是去餐馆老板娘的200小单间，二是冲上山顶找到保安大哥说的那座塔。&lt;/p&gt;
&lt;p&gt;当我抬头看了看天，云层消散了不少，估摸着当天狼座升到合适的位置，云层也消散得差不多了，那么刚好可以拍拍银河。于是毅然决然地朝山顶出发。&lt;/p&gt;
&lt;p&gt;东澳岛很大，大到平时不怎么看到其他游客；东澳岛很小，小到来去都是同一条路。天色渐晚，大家都选择了坐摆渡车从这头到那头，于是路上就又一次只剩我一个人，漆黑的街上没有路灯，没有行人，满世界都是我自己一个人，哪怕是路中间唯一一个便利店也早早关了灯，有的只是偶尔从路边下水道处传来的牛蛙叫声。为了驱赶黑夜带来的恐惧，我用音响放起了音乐，这可能是唯一一个稍微能增加一丝人类文明迹象的动作了。&lt;/p&gt;
&lt;p&gt;我实属想不到在晚上8点多还需要去爬山是一种什么体验，尤其是白天保安大哥多次叮嘱山上有蛇，有蛇，有蛇。在登上山顶的那段路程，只要旁边有啥风吹草动都能把我吓得一惊一乍的，生怕哪里窜出一条蛇来。其实蛇也没有那么可怕，可怕的是保安大哥的那句话「你小心别被咬了，大晚上的不一定有船送你出去打抗体」。&lt;/p&gt;
&lt;p&gt;哪怕是风和日丽的日子，夜晚山顶的风都是惊人的，更不用说在这种大雨过后的夜晚，风大得更是让人可怕，在登山路上还觉得这风吹得很舒服，但是当登顶后才意识到不对，这么大的风我怎么扎营啊。还记得大哥跟我讲的山顶的亭子，其实那是一个观光塔，外貌看起来有二三层，看起来里面是一个理想的避风港。可是当我登上山顶的时候，发现整座塔冒出了瘆人的红光。没错，东澳岛的风光审美还停留在了对建筑物打上各种各样的奇怪颜色灯光的阶段，而这座名为「蜜月阁」的亭子被安排上了红光，想象一下这个画面，在一片漆黑的山顶上，出现了一座通体冒着红光的塔，而你的目的地就是那里，可怕吧？放在电视剧里面，里面打斗的剧情估摸着都能演10集，就差一个妖怪站在门口跟你招手说「来吧来吧，我知道你很累了，进来睡觉吧」&lt;/p&gt;
&lt;p&gt;纵使是胆大的我也没有勇气扎入那座「蜜月塔」，折中的，我选择了在山顶的一个小亭子落脚，本来想录一个扎营的视频，但是风大到吹翻了数次我的三脚架，便作罢。因为选择在小亭子里面，水泥地面上没办法打地钉，所以只能用绳子把帐篷的几个角绑在亭子的石凳上，帐篷只能勉强稳定住基本形状，一晚上还被吹倒了好几次，调整了几次帐篷的方向后才能勉强把风卸掉，好不容易撑到了第二天早上。&lt;/p&gt;
&lt;p&gt;我从来没有想象过我的第一次独自露营就是那么高难度的，当我躺在帐篷里面的时候，海浪声、风声、帐篷被风吹得嗡嗡的声响交替而上，以至于后来听到海浪声都能预料到接下来的事情了。同时还要担心会不会有蛇出现，但是在帐篷拉好拉链的情况下，我倒是不担心蛇会钻进来，反而我担心的是某个保安大哥突发奇想地夜晚登高望远，发现我的帐篷，把我赶走。&lt;/p&gt;
&lt;p&gt;庆幸的是，这一切都没发生。&lt;/p&gt;
&lt;h3&gt;摄影&lt;/h3&gt;
&lt;p&gt;看似一切都不太顺利的一路，在扎营的晚上起来调整帐篷时有了一丝的转机，在12点多起来的时候发现天上的云已经消散得差不多了，快要接近万里无云的程度，天空也清晰可见。顿时大喜，这绝对是一个拍星空的绝佳时刻啊，可是下一刻就把我从天堂拉回了地狱，还记得前文讲的在路上偶遇的几位建筑大哥吗？原来岛上在南北两侧各有一个大型度假酒店的工地，工地晚上的施工亮光几乎把整个岛都照亮了，那种光污染简直堪比在城市中心，而且在山顶毫无遮拦的地方更是能借助灯光的情况下把整个工地看得一清二楚，这可能在人眼上没啥区别，但是对摄影绝对是致命性的打击。无奈之下只能放弃，便又钻回帐篷睡觉，但是唯一一个好消息是早上的日出还是很有期待的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/statics/images/trip-of-dongao-sunraise.jpg&quot; alt=&quot;东澳岛日出&quot;&gt;&lt;/p&gt;
&lt;p&gt;5点闹钟准时响起，把帐篷收拾好后，天空开始逐渐转亮，山顶的风大到即便是冲锋衣都难以抵挡得住。虽然这里的日出不能说很好看，甚至只能用普通来形容，但是我觉得前一天前一晚的那种铺垫，让我觉得这样的日出也十分值得。在架好相机自动拍摄后，便四处闲逛运动以驱散海风带来的寒冷，然后惊现在我晚上扎营不到5米的低洼处一点风都没有，当时真的是把我震惊到了。为什么昨晚我没有多寻找几分钟来确定营地，为什么要睡在一个风口处。&lt;/p&gt;
&lt;p&gt;熬恼之余，山顶上逐渐热闹了起来，建筑工地的几条土狗也上到了山顶游玩，环卫阿姨也开始沿路维护，看到我如此早的就在山顶也猜到了我是在山上露营的，便打趣道「诶，小伙子，没碰到蛇吧」。&lt;/p&gt;
&lt;p&gt;拍完日出便下山，5点多的沙滩一个人都没有，边上的店铺也没有开门。我取出睡垫在旁边的草坪上躺了下来，享受了一次真正的海风拂面，而不是夜晚的那种呼啸大风。&lt;/p&gt;
&lt;p&gt;实话说，东澳岛的两个海滩都是南北朝向的，所以即便是能下海游泳的天气也不能在沙滩的海平面上看到日落与日出，这估计是整个海岛最让人失望的地方了。在海边扎营，在沙滩上看着日落渐黑，看着日出渐亮，这绝对是一件很美好的事情。可惜这些在东澳岛都做不到，而东西两侧都还处于开发阶段，只有一小小路过去。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/statics/images/trip-of-dongao-fishman.jpg&quot; alt=&quot;钓鱼的那哥们&quot;&gt;&lt;/p&gt;
&lt;p&gt;在草坪上小憩到9点多时，碰到了前一天在海岸礁石边上碰到的老哥，老哥见到我第一句话便是「昨天那哥们钓到鱼了吗」，那哥们钓没钓到鱼我真的不知道，我只知道我跟他在那海岸边上琢磨了快20分钟怎么爬到最外面的礁石上。他寻思着走出去鱼儿好上钩，我寻思着我怎么去到隔壁的石头上拍他钓鱼的画面，数十分钟的折腾，他开始了他的钓鱼之旅，我开始了我的拍照之旅，也算是一拍即合？很可惜的是恰逢国内休渔期，不然说不定岛上还有出海钓鱼的活动。&lt;/p&gt;
&lt;h2&gt;归程&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/statics/images/trip-of-dongao-beach.jpg&quot; alt=&quot;海岸&quot;&gt;&lt;/p&gt;
&lt;p&gt;也很幸庆昨晚刮了一晚上的大风，把附近的云都吹得一干二净，第二天看到了久违的太阳，沙滩边上也竖起了允许下海游泳的绿色旗帜。天气也在太阳的照射下逐渐变得暖和起来，9点多的时候海里就已经开始有人游泳。&lt;/p&gt;
&lt;p&gt;而我此时躺在草坪上，一边吹着海风享受阳光，一边思考着接下来要做什么。夜晚的星空已经泡汤了，海滩的日落日出也没有任何盼头，于是决定提早一天踏上归程。殊不知当我思考完后，发现当天回程的票只有仅剩的一张了，买好票后心情尤其的好，丝毫不考虑某个倒霉蛋还需要在岛上呆多一天，换了一身泳装便加入了游泳队伍中。&lt;/p&gt;
&lt;p&gt;这是2022年的第一次「浸咸水」吗？我甚至不记得我上一次去海边是什么时候了。岛上的人本来就不多，愿意下水的就更少了，整个游泳体验简直太舒适了。欢乐的时光总是过得很快，我已经游了一个多小时了。没有吃早饭和午饭的我刚踏上沙滩、重新感受重力时，我累到甚至走不出直线，晃晃悠悠地来到一张椅子上歇息了起来。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/statics/images/trip-of-dongao-mojito.jpg&quot; alt=&quot;西餐-莫吉托&quot;&gt;&lt;/p&gt;
&lt;p&gt;洗漱一番后便踏上了去码头吃饭坐船的路程，不知道是赶上了饭点，还是大家也对这岛有了一丝失望，路上连续碰到了不少人，摆渡车上也逐渐没有了空位。当我再次回到码头时，有了昨天饭店不好吃的印象后，我果断地去了一下西餐厅。毫不夸张的说他家的黑椒牛柳意面是我吃过最好吃的，再配上一杯莫吉托简直太舒适了，我不得不感叹在一个本该吃海鲜的地方居然能吃到那么好西餐，实属不易。但是我也不太确定是不是我前一天没吃到好东西的缘故。&lt;/p&gt;
&lt;p&gt;归程的码头大厅上终于不会出现呼喊耳先生的广播了，我也很顺利的搭上了这班船，在排队上船的时候，我发现了好几个背着旅行包的大哥，上面都明显地挂着帐篷和睡垫。好奇在前一天没有找到其他露营者的我上去了搭话。&lt;/p&gt;
&lt;p&gt;「老哥，你们昨晚是在哪里扎营的啊，我怎么没见到你们呢」&lt;/p&gt;
&lt;p&gt;「啊，我们在万山岛，你呢」&lt;/p&gt;
&lt;p&gt;「我就在东澳岛啊」&lt;/p&gt;
&lt;p&gt;「这边好扎营吗？」&lt;/p&gt;
&lt;p&gt;「地面不让扎，我去山顶扎的」&lt;/p&gt;
&lt;p&gt;看着老哥手里的鱼竿装备，我心有所想。啊，原来是在万山岛扎的营、钓的鱼啊，难怪我见不到他们。不过，我懂了。&lt;/p&gt;
&lt;p&gt;下一站，万山岛！&lt;/p&gt;
</content:encoded></item><item><title>2021: 一地鸡毛</title><link>https://www.kilerd.me/summaries-my-2021/</link><guid isPermaLink="true">https://www.kilerd.me/summaries-my-2021/</guid><description>每一年的自我审视，不仅仅是写给别人看，更是对这一年的自我评价。活得糟糕与否并不是总结的意义，而是来年避免重蹈覆辙，越过越好才是本意。我也无意跟其他人对比，每个人都有自己的生活轨迹，强行匹配他人的轨迹不见得活得更加美好。</description><pubDate>Fri, 31 Dec 2021 14:49:20 GMT</pubDate><content:encoded>&lt;p&gt;每一年的自我审视，不仅仅是写给别人看，更是对这一年的自我评价。活得糟糕与否并不是总结的意义，而是来年避免重蹈覆辙，越过越好才是本意。我也无意跟其他人对比，每个人都有自己的生活轨迹，强行匹配他人的轨迹不见得活得更加美好。&lt;/p&gt;
&lt;h2&gt;工作&lt;/h2&gt;
&lt;h3&gt;出差与扎根&lt;/h3&gt;
&lt;p&gt;这一年出差辗转多地，从西安到成都，最后回到深圳稳定下来。不仅是地区的辗转，每一个地方的流动也代表着一个项目的兴衰。后来跟其他出差来的同事聊天才得知原来的项目早就结束了，并不能延续下来，这个事情其实只是公司层面的影响，并不会涉及到员工个人。毕竟去哪个项目不是写 CRUD 呢，但是反过来想，作为乙方公司的我们很多时候都是在项目初期接管项目的构建，而不会真正地涉及到一个产品在运营阶段的任何事情，这意味着我们只会「做产品」，完全不会「运营」一个产品。&lt;/p&gt;
&lt;p&gt;我司是没有产品基因的，这句话说得并不无道理。这种刻在公司基因里的元素直接反应在了员工的执行模式上，很多时候做需求都是草草了事，并不会把视野放得稍微远一点点，为不久的将来做出足够的兼容和扩展。今年在公司听得越来越多的话语就是「先这样实现，出了性能问题再改」，然而在这种情况下，Junior的员工写出了越来越烂的代码，Senior的员工擦的屁股越来越多，公司的任务分配量越来约不平衡，真的很难说有一个良好的氛围。&lt;/p&gt;
&lt;p&gt;回到出差的这个话题，2021年后半年基本稳定地扎根在深圳，同时被借调到了另外一个部门，体验了一番其他部门的良好氛围。「归属感」这个词再一次涌入了我的脑海里，足够稳定的项目发展，一眼望去就能看到大部分的人，肆意聊天沟通的氛围确实很让人羡慕。相比入我的部门，大部分人都处于出差、被借调的处境，大部分时候都是网友见面，甚至有些同事一整年都在WFH，这种大氛围下我着实想不到谈何归属感。这一次我又唤起了离开的想法。&lt;/p&gt;
&lt;h3&gt;扩张、注水和技术卓越性&lt;/h3&gt;
&lt;p&gt;每一个公司都是一座围城，城里的人想出去，城外的人想进来。这句话很好地描述了当代互联网公司的求职情况。但是对于我司这么一家看似在变好的公司来说，并不见得。&lt;/p&gt;
&lt;p&gt;这一切的源头都归咎于公司的肆意扩张，在某些职业经纪人的加入下，公司的行为被一次又一次的「规范」，我们更加开始趋于利润，开始着眼KPI。一个无法避免的恶性循环开始出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;部门的主管需要KPI就开始扩张招人&lt;/li&gt;
&lt;li&gt;一开始不怎么卷、但是现在开始卷的公司给不出高薪，招不到太好的人&lt;/li&gt;
&lt;li&gt;KPI要求下开始放低要求&lt;/li&gt;
&lt;li&gt;公司老员工疲于帮能力不太行的新员工擦屁股，受不了了开始陆续离职&lt;/li&gt;
&lt;li&gt;人员出现缺口需要再次招人&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;是的，一家市场竞争力不强的公司是没办法要求太多的。同时另外一个公司发展关键点在于公司改logo然后成功上市美股。这更加为公司的未来埋下了一颗定时炸弹，本来陆续每个月都会有人离职的公司突然没人离职了，大家都在等着一年后股票兑现了再离职。在这种情况下部门主管的压力会被无限放大，看似良好的部门会在一年后开始土崩瓦解，为了避免一年后的项目正常运作，为了部门员工还能满足主管KPI的需求，扩张与放水被无限放大，恶性循环加剧。&lt;/p&gt;
&lt;p&gt;这一年里，3年工作经验的我逐渐往senior的title靠近，但是越是靠近越觉得公司的「技术卓越性」都是虚假的。作为一个乙方公司，涉及不到太多甲方公司的核心业务，以至于一整年都是在做简单的CRUD，最难的事情只能停留在对已有系统SAAS化改造的方案设计了。很多时候我们并不是再迈向技术卓越，而是为自己，为TL的技术选型买单。无数次因为TL选择了 Hibernate，不是Mybatis 而加班。又加上上文说的注水情况，新员工在代码上逐渐倾向于「业务能跑就行」，而苦了我这些对技术稍微有一些追求的人。他们就花技术重构的时间来摸鱼，我就只能加班为他们擦屁股，人生不值得说的估计就是这样把。&lt;/p&gt;
&lt;p&gt;项目的老员工逐渐离去，而我也能在经验上拍得上名号，很多次都因为他们的技术不卓越在Code Review阶段吵起来，脾气也变得越来越不好，后来我也懒得再去理这些东西了，毕竟又不能写到自己的KPI上。不值得不值得。&lt;/p&gt;
&lt;p&gt;再回到公司层面的技术卓越性上，「无」这个字真的很好的概括了这家公司的情况。没有靠谱的跨团队技术分享，没有公司，甚至部门层面的技术wiki，这对一家公司来说简直太离谱了。&lt;/p&gt;
&lt;p&gt;写到这，我回想到了两年前的公司。在项目与项目的 gap time，之前还可以自己学习技术，扩展技术广度深度，写文章，写书。但是这一年来，只要你是在gap time，都会被拉入一些无意义的培训里面，自由度再次降低，这是公司发展的必然，也可能归咎于某些职业经纪人吧。&lt;/p&gt;
&lt;p&gt;在这样一家走下坡路的围城公司，保留自我内心估计会是最重要的事情了吧。&lt;/p&gt;
&lt;p&gt;跑，赶紧跑。&lt;/p&gt;
&lt;h2&gt;技术&lt;/h2&gt;
&lt;p&gt;我的技术生涯是分阶段的，完全割裂的：从大学时代的Python为主，到上班时候的Java后端，下班的Rust学习，我觉得我的技术生涯是很悲哀的。&lt;/p&gt;
&lt;p&gt;上班写着自己完全不喜欢的语言，工作时间糊业务，下班不写Java，完全没有自己认真地去读过学过Java底层的内容。也幸好得益于公司的业务模型让我并不需要太了解Java的底层，不需要设计JVM虚拟机、字节码之类的。在业务类型上只要吃透 Spring boot 就可以达成公司对 Senior的标准要求，而我在这方面做得十分好，也符合了我对上班的定义。&lt;/p&gt;
&lt;p&gt;但是从另外一方面讲，这是一个很致命的问题。上班时从来没有人说过我的技术很好，确实相比于其他专精于Java的同事来说我真的太菜了。这是一个信号，一个可能会影响我职业生涯的信号。我也正视过这个问题，我反问了自己几个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前的Java技术能满足在公司1-2年的发展吗？&lt;/li&gt;
&lt;li&gt;跳槽后你还写Java吗？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;是啊，不换公司我能满足于工作要求，换公司我更宁愿去写 Rust，而不是Java。那这个问题可能真不是太大的问题，尤其是在我还打算一年后跳槽回广州的情况来看。&lt;/p&gt;
&lt;h3&gt;PL 与 Rust&lt;/h3&gt;
&lt;p&gt;我很想说我是一个PL人，但是我很清楚我对于PL什么都不懂，我也很乐意去学这个方向。我一直在坚持的Rust 就是一个很好的 PL 届良性例子，有了太多太多很棒的设计与理念。尤其是我这种情况：上班写着一个历史包袱很重很重、面向对象典范的Java，下班写着几乎象征着很优秀PL设计的Rust，在两者的长时间对比与实践中也更加坚定了「不学 Java、深入 Rust」的观点。&lt;/p&gt;
&lt;p&gt;而关于 Rust 的优点其实我并不想在这里深入地阐述，从2016年左右开始慢慢接触 Rust，到现在能用 Rust 写出中小型软件的情况下，不仅是我个人的技术进步，更加是陪伴见证了 Rust 这门语言的进步。无论是 NLL，null-safe，tagged union，错误处理，pattern matching 等等优秀的设计都是在Java 中体验不到的。每次在上班的时候碰到 NullPoinerException 的时候，我就在感叹 Kotlin 和 Rust 的好。&lt;/p&gt;
&lt;h3&gt;Side Project&lt;/h3&gt;
&lt;p&gt;前些年，我注册了 &lt;a href=&quot;http://3min.work&quot;&gt;3min.work&lt;/a&gt; 这个域名，本意是用来做一些小项目的存放，寓意着「3分热度」。3分热度有3分收获，确实我收获了很多，折腾了服务器集群部署，折腾了很多试验性的前端框架，折腾了很多有意思的东西。同时这也意味着我在某一项领域并没有做到深耕。&lt;/p&gt;
&lt;p&gt;在把部分业余直接分割到阅读和摄影之后，我发现我已经没有足够的时间来进行探索性的折腾了。仔细想了想这一年确实并没有做出什么让自己很满意的作品，哪怕是小作品也没有，都是各种浅尝即止，着实很失败。于是我并没有继续续费 &lt;a href=&quot;http://3min.work&quot;&gt;3min.work&lt;/a&gt;，我希望接下来自己能专注于一两个项目，老老实实做下去。在我脑海里面一直萦绕着几个想做的东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;记账软件&lt;/li&gt;
&lt;li&gt;Instagram类似的图片分享软件&lt;/li&gt;
&lt;li&gt;typhoon 编程语言&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这估计也是也会是我接下来会继续坚持的项目吧，其他的估计就不会太怎么维护了。实在是精力不够。&lt;/p&gt;
&lt;p&gt;除了项目，这一年我基本也做出了自己的技术选型，Rust + NextJS 会是我以后的前后端技术栈。&lt;/p&gt;
&lt;h2&gt;摄影&lt;/h2&gt;
&lt;p&gt;出差得越多，见得越多，便越喜欢拍照。手里拿着的佳能200D配套头再也不能满足于我的拍摄，后来买了一个新的 28-200的超长变焦头，拍出了一些我很满意的照片，记录下了很多很精彩的瞬间。这是我认为人生新的意义，人生确实不应该只有眼前的编程，更有远方的风景和美食。&lt;/p&gt;
&lt;p&gt;很可惜，很多照片并没有发布到互联网上，没能让大家看到，我觉得这是一件很遗憾的事情，也没能把旅途中的故事写下来配套的图片。这可能是我执着于做一个 Instagram 类似物的缘由。希望能够快点做好分享出来。&lt;/p&gt;
&lt;p&gt;再后来索尼出了 A7m4，我便下定决定要买这一机子，在加价的情况下很顺利的在12月初拿到了机子，也买了一个适马的 35 F1.4 定焦头。很多朋友都不是很理解这种行为，但是我已经在上一台机子上测试过自己不是那种「买相机拍几次就吃灰、然后用手机拍照」的人了。所以对于一个成年人来说为自己的一个成熟爱好投资3-4万块并不是一个很离谱的事情。&lt;/p&gt;
&lt;h3&gt;视频与 vlog&lt;/h3&gt;
&lt;p&gt;每年 iPhone 都会给我发一个 annual summary，自动地把一年里面精彩的事件照片制作成一个简单的slide，又得益于live photo 的存在，slide 是一个看似简陋剪辑的小视频，每一张照片都是带声音、带动效的。看似简陋但是透露出了照片表达不出的临场感，自此我好想有点爱上了拍视频。&lt;/p&gt;
&lt;p&gt;再有一次，EDG打决赛那天晚上，带着相机去拍了拍线下观赛点的照片和视频。直到现在回看起夺冠的现场视频，都会有一种肾上腺素分泌的感觉，这是照片无法比拟的震撼。&lt;/p&gt;
&lt;p&gt;这也是我为什么要买索尼的原因，强大的对焦系统让拍视频变得简单。摄影圈有一句名话「拍到永远比拍好更又意义」，自此我希望我能够成为一名摄影师。&lt;/p&gt;
&lt;p&gt;2022 年我希望我能够运营一个自媒体账号，学习剪辑视频发布出去，哪怕没有什么人看，哪怕剪辑很烂。&lt;/p&gt;
&lt;p&gt;记忆会消散，但是照片不会，视频不会。&lt;/p&gt;
&lt;h2&gt;感情&lt;/h2&gt;
&lt;p&gt;我哥在上一年结了婚，毫无疑问，爸妈开始对我进行了催婚，进而进行了相亲。掰了掰指头，我已经快26岁了，因为是年底出生的，虚岁已经快30了。 爸妈就已经以这一点做威逼「你看你都快三章了（三十岁），是不是该结婚了」。&lt;/p&gt;
&lt;p&gt;老实说，在如今的时代和我接受的教育，单身并不等于孤独，也不等于不幸福。每个人在当今都开始趋向于做一个精明的利己主义者，在这个大前提时下，男女各方都很难拉低姿态去对另外乙方委曲求全地讨好对方，那么除非是出现 1+1 &amp;gt; 2 的「交易」下才能催生初所谓的爱情。&lt;/p&gt;
&lt;h3&gt;阶段性陪伴&lt;/h3&gt;
&lt;p&gt;这一年遇到了很多人，很多人也「逐渐消失」了在我的人生中，从某段时间的亲密无话不说，到最后的打开聊天框不知道说什么，只能在对方的朋友圈里面回复几句「真好看」。这中间发生了什么呢？估计是某一次聊天无话可说的一句「哦」，也可能是冷静过后的暧昧消散。不管如何，事实就是某些人在那段时间内的陪伴已经变成了过去式。只有翻篇才能遇到更好的人。&lt;/p&gt;
&lt;p&gt;这里说的陪伴并不特指恋爱关系的陪伴，也指的是那些你以为会成为好朋友，成为知己的陪伴。&lt;/p&gt;
&lt;h3&gt;相亲&lt;/h3&gt;
&lt;p&gt;在爸妈没办法接受新时代的恋爱观和爱情观的情况下，相亲被迫进行。老实说我并不抗拒相亲，像我一个同事所说「你只有经历过它，才有资格，才有本钱去讨论和吐槽它」，我就冲着不成也积累、记录生活经验、认识一些朋友的冲动去相亲。&lt;/p&gt;
&lt;p&gt;但是我远远低谷了相亲这件事带来的背后影响。在我爸妈知道我「接受」相亲之后，就开始推女孩子的微信过来。在那一次，我是真的被委屈到哭了。推过来的女孩子甚至都不知道我要加她，这还不是最重要的，重要的是我爸妈连续五天每天都给我打电话问我跟她聊成怎样。要知道再次之前，爸妈甚至一个月都不会给我打一次电话，纯属放养状态，那一刻我甚至不知道到底谁是谁的孩子，谁更重视谁。&lt;/p&gt;
&lt;p&gt;后来我也思考了一番，相亲这件事于几方人的意义是什么。我甚至我爸妈绝对是很开放的人，但完全不清楚在这件事上为何态度会这番奇怪。&lt;/p&gt;
&lt;p&gt;作为一个自认生活质量还算高，精神世界丰富的人来说，我绝对不会滥情或者缺爱到需要向一个没见过面、朋友圈啥都没有的所谓「相亲对象」倾诉我的个人生活，更何况对方的回应甚至还不如一个类似的舔狗AI，甚至不如一个卖茶叶的美女回应强烈。那我这是何必呢。&lt;/p&gt;
&lt;p&gt;后来又经历了一次相当传统的相亲国产，我妈带我去见一个女生和她姑姑。这个过程简直尴尬到丝毫没有任何值得描述的价值，全程是家长在讲话。&lt;/p&gt;
&lt;p&gt;父母的婚姻价值观还停留在了「相亲看一看，觉得可以就自己聊一聊，差不多就结婚，感情啥的之后在经营」。可是现代年轻人对此确实不屑一顾。&lt;/p&gt;
&lt;p&gt;我可不愿意一辈子就这么娶一个自己不了解的人，一不小心遇到一些脾气暴躁的，怕不是被欺负一辈子。庆幸的事，这几件事之后，回去对着父母大发雷霆一次，他们对于相亲这件事已经克制了很多了。我觉得这辈子有那么几次相亲经历就足够了。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;这一年，看开了很多，承认了自己平庸的事实，放弃了一些自己不切实际的想法，过得很忙，已经不再计较忙得有没有意义了。&lt;/p&gt;
&lt;p&gt;就这样吧，希望下一年还能过得更好，找到一个对的人，做出一些有成就感的作品。如同我兄弟说的「我无数次幻想着你我带着女朋友，开着一辆车出去旅游，你拍我，我拍你，开车你累了我开」。是啊，我也无数次幻想过这样的画面。&lt;/p&gt;
</content:encoded></item><item><title>奇怪的内卷：感谢联通</title><link>https://www.kilerd.me/daily/dance-of-companies/</link><guid isPermaLink="true">https://www.kilerd.me/daily/dance-of-companies/</guid><description>从很早开始只有联通在B站上面发舞蹈区视频，但是只能说是平平无奇并没有引起太大的波浪，基本只有10多20几万的播放量，基本上很少能登上分区的排行版，直到中信银行在2020年6月发出了书记舞一下子就打开了这个潘多拉魔盒，成功出圈，也能在他的视频上面看到无数的「感谢联通」弹幕。</description><pubDate>Thu, 10 Jun 2021 15:53:29 GMT</pubDate><content:encoded>&lt;p&gt;从很早开始只有联通在B站上面发舞蹈区视频，但是只能说是平平无奇并没有引起太大的波浪，基本只有10多20几万的播放量，基本上很少能登上分区的排行版，直到中信银行在2020年6月发出了书记舞一下子就打开了这个潘多拉魔盒，成功出圈，也能在他的视频上面看到无数的「感谢联通」弹幕。&lt;/p&gt;
&lt;p&gt;再到后来&lt;a href=&quot;https://www.bilibili.com/video/BV1MT4y1M7eJ?from=search&amp;amp;seid=8834975319390450057&quot;&gt;【书记舞】论初中生有多呆&lt;/a&gt; 一个普通视频发出来之后，就更火了。这个事情说起来就离谱，一个跳得普普通通的舞蹈就因为舞者年纪太小了，然后各位看官就在评论或者弹幕里面发了一个 「小妹妹不要在网上晒自己，要多学习提高自己的知识才是正事，那么就让XX来考考你一个问题：blablabla」。起初这种问题还是挺正常的，比如什么解方程式啥的。但是这一下就炸开了锅，啥人啥玩意的问题都出来了。有大学官方号来问土木的，有问建筑的，有问物理的，有问计算机的。随之把这个视频推到了一个莫名的高度。&lt;/p&gt;
&lt;p&gt;有了中信银行的书记舞和这个小妹妹的爆火书记舞，这些移动联通的官号怎么还坐得住啊，一下子就全部冲出来拍了相对应的视频，其中最火的估计是招行的这个了&lt;a href=&quot;https://www.bilibili.com/video/BV18i4y1P7Av&quot;&gt;【招行特供】 ❤ 挑战全网最甜书记舞 ❤&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;在这段时间内，有无数企业加入了这场莫名的战斗，本着「他们能拍，我也要拍」的宗旨，于是这些官号不再是那种只会发广告的视频了，同时他们也发现了发舞蹈视频能带来很大的人气，同时把需要宣传的内容放在评论置顶，能带来相比于之前的模式更大的收益。于是乎内卷出现了。&lt;/p&gt;
&lt;p&gt;「感谢联通」这句话就随着不断的发展已经找不到比较靠谱的源头了，而跟着出现的是「感谢招行」「感谢移动」「感谢平安」。没有人知道这场战争到底什么时候会结束，也没有人知道还有谁会加入这场有声的PK。这就如那句「好人一生平安」的祝福语一样，给沉闷的舞蹈区带来了一丝不一样的气息。&lt;/p&gt;
&lt;p&gt;最后是整理出来比较出名的一些作品：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;发布时间&lt;/th&gt;
&lt;th&gt;视频名称&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2020-06-05 12:00:56&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1L5411W7Q3&quot;&gt;【中信银行信用卡中心】爱杀宝贝ED 还原：客服双人组，老胳膊老腿不容易&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2020-06-27 11:00:46&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1rt4y197PW&quot;&gt;【中信银行信用卡中心】超还原书记舞，不看错亿！&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2020-09-29 17:49:58&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1oy4y1k7J5&quot;&gt;【国家电网】高甜预警❤️JK制服女孩来啦❤️&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2020-11-20 12:10:18&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1vp4y167aa&quot;&gt;【中国联通】睫毛弯弯- 把爷青回打在公屏上！&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2020-12-15 19:00:02&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1jK411G7ZQ&quot;&gt;【中国电信】不跳书记舞的官方不是好官方&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2020-12-22 15:27:45&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1aV411h7X4&quot;&gt;【中国联通】客服小姐姐版-dududu️❤把爱了爱了打在公屏上&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2021-02-14 11:00:19&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1jA411g7jh&quot;&gt;【中国联通】你的女友❤&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2021-03-26 18:16:02&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV18i4y1P7Av&quot;&gt;【招行特供】 ❤ 挑战全网最甜书记舞 ❤&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2021-05-07 18:32:59&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://www.bilibili.com/video/BV1UA411376M&quot;&gt;【招行特供】❤️ 元气 ❤️ 阳光 ❤️ 超甜 ❤️ 我们一起与梦盛开&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content:encoded></item><item><title>年轻人的第一次删库跑路</title><link>https://www.kilerd.me/accidents/first-time-deleting-database-on-production/</link><guid isPermaLink="true">https://www.kilerd.me/accidents/first-time-deleting-database-on-production/</guid><description>恭喜自己，这是第一次生产事故，也是第一个 T0 事故 规范化自己服务器的事故级别： T0: 极其严重事故。用户数据造成不可逆的损失 T1: 严重事故。用户数据造成可逆损失，需要 ops 的接入恢复数据。或者服务功能出现不可用情况 T2: 一般事故。用户数据未损失，只存在显示异常等展示型内容异常 T3: UI 异常。因为</description><pubDate>Mon, 19 Apr 2021 08:53:33 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;恭喜自己，这是第一次生产事故，也是第一个 T0 事故&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;规范化自己服务器的事故级别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;T0: 极其严重事故。用户数据造成不可逆的损失&lt;/li&gt;
&lt;li&gt;T1: 严重事故。用户数据造成可逆损失，需要 ops 的接入恢复数据。或者服务功能出现不可用情况&lt;/li&gt;
&lt;li&gt;T2: 一般事故。用户数据未损失，只存在显示异常等展示型内容异常&lt;/li&gt;
&lt;li&gt;T3:  UI 异常。因为UI布局等问题，导致使用出现了麻烦&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;基本&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;编号： 0x0001&lt;/li&gt;
&lt;li&gt;发生日期： 2021-04-17&lt;/li&gt;
&lt;li&gt;级别： T0&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;过程&lt;/h2&gt;
&lt;p&gt;04-17 下午，登陆生产服务器，执行 &lt;code&gt;yum upgrade update&lt;/code&gt; 命令，缘由是服务器的Docker环境版本过于低，需要做一系列更新。 执行完命令后，服务器自动重启，并未给出对应的重启提示，初步判断是由于涉及到了内核的更新。更新后迟迟不能恢复服务，接入VNC查看后，虚拟机无法加载磁盘，至此事故发生。服务器内容全部损失。&lt;/p&gt;
&lt;p&gt;事后及时联系服务商，企图拿到损坏的磁盘镜像进行恢复，结果是拿不到，得到的建议只有重装。&lt;/p&gt;
&lt;h3&gt;奇怪&lt;/h3&gt;
&lt;p&gt;在重装后，找服务商要回了同样的磁盘系统模版，安装后重新执行 &lt;code&gt;yum upgrade update&lt;/code&gt; 命令，也没有产生任何内核级别的更新，甚至一个更新都没有，所以这次事故的根因到底是啥我一直没太懂。&lt;/p&gt;
&lt;h2&gt;影响&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;服务器中带状态的服务只有 Miser，且用户只有本人，由于没有做及时的定时数据备份，导致数据只能恢复到3个月前人工恢复的版本。&lt;/li&gt;
&lt;li&gt;服务相关文件储存传到了 backblaze 服务器，不受该次事故影响&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Actions&lt;/h2&gt;
&lt;p&gt;这件事情后，我思考了很久关于「正式运营一个产品」的事情，不幸中的万幸是 Miser 自始至终都只有我一个人在使用，即便是朋友多次说想一起使用参与整个产品的发展。我个人的数据其实并不是那么值钱，但是一旦有用户真正的把内容放在你这里，然后你还弄丢了，那么诚信和信任度会变得很低很低吧。&lt;/p&gt;
&lt;h3&gt;关于监控&lt;/h3&gt;
&lt;p&gt;在此之前我只在服务上面搭了 sentry，用来实时监控服务里面未处理的异常情况，即 5XX 的错误的收集。而实际上在运营过程中还碰到过很多奇奇怪怪的问题，比如说突然间CPU彪得很高，内存突然吃紧了。&lt;/p&gt;
&lt;p&gt;由于用的是传统的虚拟机，并没能上自由伸缩的云，因此 CPU、内存等系统级别的监控也是应该提上日程的，也就是说一套完善的 prometheus + Grafana 的基本配合是一定要部署到生产的。&lt;/p&gt;
&lt;h3&gt;关于可靠性与备份&lt;/h3&gt;
&lt;p&gt;说起来很奇怪，实际上我已经数个月，快半年的时间没有动过生产服务器，基本都是通过 GitHub Actions 来实现一套完成的 CI/CD 的。这次手动登陆服务器的缘由便是上去把备份服务部署上，而就是这么一次操作导致了事故的发生，也有可能是生疏了。&lt;/p&gt;
&lt;p&gt;服务商跟我讲得很好，我的操作确实存在很大的问题。他说如果他来会这么操作：先拍一个硬盘snapshot，然后执行操作，没问题万事大吉，有问题用snapshot恢复整个磁盘。&lt;/p&gt;
&lt;p&gt;确实，我这一次并没有很好地做一次dryrun，每次都是直接跑docker pod 级别操作，出问题了就直接rm，这次在宿主机的操作实在缺少了一丝敬畏。&lt;/p&gt;
&lt;p&gt;为此&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;减少对宿主机的直接操作，docker 集群搭建起来之后应该都在docker 层面操作&lt;/li&gt;
&lt;li&gt;备份服务的优先级应该被提到最高，毕竟数据才是重中之重&lt;/li&gt;
&lt;li&gt;dryrun的可靠性，一套跟生产一一对应的dev 或者 uat 环境的必要行需要进行思考&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>狗子的一天</title><link>https://www.kilerd.me/days-of-dogs/</link><guid isPermaLink="true">https://www.kilerd.me/days-of-dogs/</guid><description>有一种思念叫你家的狗子在想你</description><pubDate>Fri, 05 Mar 2021 14:13:03 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;有一种思念叫你家的狗子在想你&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;旺旺&lt;/h2&gt;
&lt;p&gt;我家在农村，在家附近有一块老妈围起来的菜地，一半种青菜，一半养鸡。同时妈子还养了两条狗，一条黄白色，一条黄黑色；一条住在菜园子的东南角，一条住在西北侧，每天遥遥相望；一条叫旺旺，另外一条也叫旺旺。由于村里偷狗现象很严重，所以旺旺是一年四季一日三餐都是锁在菜园子里面的，活动空间只有狗窝附近的一平米地。&lt;/p&gt;
&lt;p&gt;我也不知道狗子它知不知道它自己叫旺旺，也知不知道另外一只也叫旺旺。但是奇怪的是，每次一喊旺旺永远都只会有一个狗子回应你，似乎它们之间达成了某种奇怪的协议，每狗每天轮流上班8个小时。&lt;/p&gt;
&lt;h2&gt;幸运还是悲哀&lt;/h2&gt;
&lt;p&gt;旺旺是母的，同时也是幸运的，相比于旺旺她被允许晚上松开狗链，因此她获得了夜晚整个菜园子的制霸权，旺旺只能蹲在自己的窝里面眼巴巴地看着她欢腾飞跃。当然，自由是有代价的，有一次旺旺利用漏洞钻入了菜地，弄死了很多白菜花菜，随即而来的是一顿来自母亲慈祥的打以及“禁闭”数天。&lt;/p&gt;
&lt;p&gt;旺旺是公的，是悲哀的也是幸运的。他的品相很好，即便养在黄泥地里面，他的毛发都亮呼呼的，蓬松地不想一只狗子，反而更像一只小狮子。但就因为是狮子，叫声洪亮，凶恶的他终日无法脱离狗链的束缚，用我妈的话来说“晚上放两个狗出来菜园都可以给你拆了”，他是悲哀的。幸运的是旺旺很爱他，每天晚上被放出来的旺旺绕着菜园子跑几圈、上完厕所就主动回来陪他，直至早上。虽然说脖子上长年挂着狗链，但是一到晚上的相遇相见似乎可以冲淡这一切。&lt;/p&gt;
&lt;h2&gt;生产事故&lt;/h2&gt;
&lt;p&gt;直到有一天，干柴烈火的她们嘛，总要干出一些大事，不出我们所料，旺旺怀孕了，这是听我妈说的，我可没在家里看着。狗嘛，在基因里面就写好了如何生娃如何养娃的能力，所以我们就没怎么理旺旺，依旧遵循这白天锁晚上放的放风策略。&lt;/p&gt;
&lt;p&gt;在某一天的很早的早上，我妈听到了旺旺发出的不同寻常的嘤嘤声，只在阳台上看了一眼确认了旺旺的安全就没有多理，直到早上进入菜园才发现刚出生没多久、还没看眼的小狗子掉进菜园子中间的鱼塘里面了。又由于旺旺平时没有下水游玩的习惯，只能在边上嘤嘤地叫。最终就酿出了这局惨案，可惜了那两个还没看眼只是在到处找奶吃的小狗子。旺旺也因为这事抑郁了好几天。&lt;/p&gt;
&lt;h2&gt;过年&lt;/h2&gt;
&lt;p&gt;过年这件大事并不是对人类有重大的意义，对旺旺而言同时也是“重获天日”的一段日子。因为我的回去，旺旺每天都能获得二十分钟左右的游玩时间，那是唯一一段可以看一眼村子的时刻。&lt;/p&gt;
&lt;p&gt;由于我家只有一条多余的狗链，放风一次只能放一个。只要你敢放旺旺出来，那么旺旺绝对对急得乱蹦，口吐芬芳地乱叫，如果我听得懂狗语，那旺旺大致说的应该是“你怎么放它出来？我呢？你怎么不放我？明明是我先摆尾的，明明是我先来的。WDNMD”。不过即便听不懂，看旺旺那个架势和语气，应该差不多就是这个意思。&lt;/p&gt;
&lt;p&gt;只要你牵着一条狗子出现在另外一只的视野内，他都会嘤嘤地叫得不停，唯一的方法就是走远走远再走远，直到它看不到：“原来爱是会消失的，对吗” 便趴会自己的窝边上。&lt;/p&gt;
&lt;p&gt;可能是天生骨骼惊奇，也可能是长时间没放风，狗子的力气极其大，甚至有种把你拽飞的节奏，20分钟的遛狗可以称得上一天的运动量了。&lt;/p&gt;
&lt;p&gt;村里隔壁家散养了3条狗，每次我把那只狮子般的旺旺带出去溜时，他们都需要紧紧地站在一团，生怕我家的旺旺会手撕了落单的他们，实际上旺旺温柔地很，就是除了力气大一点以外。&lt;/p&gt;
&lt;h2&gt;再次生产&lt;/h2&gt;
&lt;p&gt;年后离开家的不只是我的人和思绪，还有旺旺对我的一丝牵挂，那一份遨游村子的盼念。同时旺旺也再次怀了孕，这一次我和我妈讨论了很多怎么看护小狗子的事宜。可惜的是在临走一天还是等不到她生产的那一刻。&lt;/p&gt;
</content:encoded></item><item><title>你可能真的不那么需要复式记账</title><link>https://www.kilerd.me/things-about-double-entry-accounting/</link><guid isPermaLink="true">https://www.kilerd.me/things-about-double-entry-accounting/</guid><description>我翻了翻 GitHub 上面的项目记录，从2019年的2月开始初始化了记账项目的代码库，到现在2021年的近2月，也该是时候说一说我这两年来设计到的记账心路历程。 关于记账这件事早就已经是一篇红海，做的人很多，死的更多。无数项目突然涌现出来而后又死得消无声息。这篇文章会从几个方面来细数为什么我们那么喜欢记账而又坚持不下</description><pubDate>Wed, 03 Feb 2021 10:44:53 GMT</pubDate><content:encoded>&lt;p&gt;我翻了翻 GitHub 上面的项目记录，从2019年的2月开始初始化了记账项目的代码库，到现在2021年的近2月，也该是时候说一说我这两年来设计到的记账心路历程。&lt;/p&gt;
&lt;p&gt;关于记账这件事早就已经是一篇红海，做的人很多，死的更多。无数项目突然涌现出来而后又死得消无声息。这篇文章会从几个方面来细数为什么我们那么喜欢记账而又坚持不下来：流水账与复式记账、趣味性与专业性、自动入账与手动入账。&lt;/p&gt;
&lt;h2&gt;流水账与复式记账&lt;/h2&gt;
&lt;p&gt;流水账是指只记录了「花了多少钱在什么事情上」，比如「今天吃饭AA了100块」。看起来似乎确实满足了记账最简单的几个点：什么时候花的钱；为什么花了钱；花了多少钱。但是在对账的时候却很难定位到账单与记账的关联。&lt;/p&gt;
&lt;p&gt;为了能更清晰地阐述流水账与复式记账的区别，我会根据「AA吃饭」这个例子给出一个更加具体的场景。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1月1号，你跟 A 先生与 B 先生出去吃了一顿饭，一共300块，AA下来每人100块，于是你先用微信支付了300块。&lt;/p&gt;
&lt;p&gt;1月3号，A 先生转了100块到你的支付宝账户。&lt;/p&gt;
&lt;p&gt;B 先生作为一个老赖，「无意」地忘记了还你钱，同时把你拉黑了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那么对于流水账来说你可能记录了两笔账：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1月1号，吃饭消费 300 块&lt;/li&gt;
&lt;li&gt;1月3号，A先生转账100块作为AA平摊费用&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么随即而来会有几个比较明显的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为什么我这个月吃饭的钱花了那么多？&lt;/li&gt;
&lt;li&gt;还有100块钱的差额去了哪里？（如果你还记得B先生没有还你100块的话。）&lt;/li&gt;
&lt;li&gt;当前你的微信支付还有多少钱？支付宝账户呢？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总结下来流水账的弱点出现在资产流动不明确与资产占比不明确上，复式记账比较好地解决了这几个问题。&lt;/p&gt;
&lt;h3&gt;复式记账&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在会计学中，复式簿记（又称为复式记账法）是商业及其他组织上记录金融交易的标准系统。&lt;/p&gt;
&lt;p&gt;该系统之所以称为复式簿记，是因为每笔交易都至少记录在两个不同的账户当中。每笔交易的结果至少被记录在一个借方和一个贷方的账户，且该笔交易的借贷双方总额相等，即“有借必有贷，借贷必相等”。&lt;/p&gt;
&lt;p&gt;例如，如果A企业向B企业销售商品，B企业用即期支票向A企业支付货款，那么A企业的会计就应该在贷方记为“销售收入”，在借方记为“现金”。相反地，B企业的会计应该在借方记为“进货”，并在贷方记为“银行存款”。&lt;/p&gt;
&lt;p&gt;借方项目通常记在左边，贷方则记在右边，空白账簿看起来像个T字，故账户也被称为T字帐。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;复式记账来自于会计行业的专业术语，简单来说我们要记录每一次交易的来源与去向（在会计学中称之为 credit 和 debit）。那么我们通过这种方式来分析刚刚举的AA吃饭例子。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1月1号，你跟 A 先生与 B 先生出去吃了一顿饭，一共300块，AA下来每人100块，于是你先用微信支付了300块。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;300块看起来是从「微信账户」流转到了「消费:吃饭」账户，但是实际并不是，因为对于账户所有人来说，只花了100在「消费:吃饭」上，剩下的200块实际上是用一种类似信贷的方式由你借给了A先生和B先生。所以对于复式记账来讲，这里要拆开三条记录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;100块: 「微信账户」流转到「消费:吃饭」&lt;/li&gt;
&lt;li&gt;100块: 「微信账户」流转到「借款:A先生」&lt;/li&gt;
&lt;li&gt;100块: 「微信账户」流转到「借款:B先生」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自此我们解决了上述说得第一个问题「为什么我这个月吃饭怎么花了那么多钱」。复式记账的好处之一就体验出来了：你可以把一笔交易拆成很多细小的组成部分。如果要举另外一个例子的话可以是工资收入，你的3000月薪实际上是从「公司」账户流转到了「银行卡」「公积金」「医疗保险」「养老金」等数个账户，这也能统计出来你为国家交了多少税。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1月3号，A 先生转了100块到你的支付宝账户。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于这一条记录，很简单就是从「借款:A先生」流转到「支付宝账户」。自此 「借款:A先生」的余额从「100.00」抹平到「0.00」，意味着 A 先生不再欠你的钱。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;B 先生作为一个老赖，「无意」地忘记了还你钱，同时把你拉黑了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对于这位老赖并没有产生任何在记账上的记录，但是不要忘记了在第一笔的记录中，「借款:B先生」是被记录成了 「100.00」 的，那么这个情况，它再也不会消失。&lt;/p&gt;
&lt;p&gt;依靠 「微信账户」「支付宝账户」「消费:吃饭」「借款:A先生」「借款:B先生」这几个账户的变动，我们可以成功的算平我们的收入与支出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;「微信账户」 - 300.00&lt;/li&gt;
&lt;li&gt;「支付宝账户」 + 100.00&lt;/li&gt;
&lt;li&gt;「消费:吃饭」+ 100.00&lt;/li&gt;
&lt;li&gt;「借款:A先生」0.00&lt;/li&gt;
&lt;li&gt;「借款:B先生」 + 100.00&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;得出来的总计是0.00，证明我们的记账流程并没有出现差错&lt;/p&gt;
&lt;p&gt;这里可能有人比较纠结每个账户上面的正负是什么意思，这就是ledger-like app的优势，相比于传统的复式记账，一笔账需要在两个账户中同时记下一笔交易，ledger-like 使用了正负来代表了credit 方和 debit 方，一条记录就完成了传统的两次记录的复杂模式，也让交易更加人类易读。&lt;/p&gt;
&lt;h2&gt;趣味性与专业性&lt;/h2&gt;
&lt;p&gt;上部分介绍了流水账和复式记账的区别与复式记账相较于流水账的优势，这里会直击问题的本质：你真的那么需要复式记账吗？答案可能是否定的。在长时间的记账之后，统计发现，在绝大部分时间的交易都是简单地从一个账户到另外一个账户的划转（这里把消费也当做了一个独立的账户），那么复式记账其实会更加麻烦，因为现在的流水账软件也能够新建各种账户，所以复式记账的分账户记录也不再是一个比较突出的优势了。&lt;/p&gt;
&lt;p&gt;对于例如工资收入、购房贷款、分期购物等一些在时间跨度上比较长的交易，在流水账的APP上也有类似的循环交易与拆分交易的功能。&lt;/p&gt;
&lt;p&gt;同时流水账的APP普遍都做的比专业软件更加适合用户使用。记账是一件很枯燥很无赖也很费事的时间，APP就会抓住这个痛点：简化日常记账流程、美化记账界面、增加趣味功能。对于趣味性，有的APP把记账设计成是跟一个 IDOL 聊天，有的把界面设计成一个养成系游戏（记账城市的思路大赞）。很明显这些设计有很有效，确实抓住了一部分人的痛点。&lt;/p&gt;
&lt;p&gt;相比于 ledger 和 beancount 的文字编辑型的记账模式，一来需要学习它特有的写法，二来没有比较用户友善的界面，三来没有APP。APP真的是当代的一个超级大的痛点，没有APP意味着你不能随时随地地在手机上记账，需要找一个专门的时间来记账，这会击退绝大部分的普通用户。&lt;/p&gt;
&lt;p&gt;但是无论流水账的趣味性做的再好也能难撼动以 beancount 在部分注重资产流转的用户的地位。主要的原因有几：&lt;/p&gt;
&lt;p&gt;一，对账功能真的是让复式记账的门槛再次降低，它指的是你可以对某一个时间点把账户的金额抹成一个你指定的数。这用于少记了某几笔金额极小的交易，账户日常派息等无法感知的情况。 这对于流水账APP来说是一个难以解决的问题，就会经常出现记着记着就发现软件上的账户余额跟真实的账户余额对不上，也很少人愿意一笔一笔地往下一笔一笔的核对，导致最后流水账软件上的信息不正确，这也是流水账用户“弃坑“的原因之一。&lt;/p&gt;
&lt;p&gt;二，复杂而专业的查询与报表功能。绝大部分APP都无法针对性地定制自己的报表，只能给你一个账户消费占比，月度余额波动图等比较笼统而无用的报表，如果想做到「某几天因某原因在某地的消费总额」就需要使用beancount 中的查询功能，这也是这一类软件比较强化的点，分析为主。&lt;/p&gt;
&lt;h2&gt;自动入账与手动入账&lt;/h2&gt;
&lt;p&gt;自动入账在国内巨头垄断的局面下基本是不太可能的事情，市面上唯一一款能直接对接支付宝和银行卡账单的记账软件「网易有钱」也凭仗着自己也是巨头而做到了其他小型开发商做不到的事情。&lt;/p&gt;
&lt;p&gt;想比如国外的信用卡广泛使用的场景不同，国内的消费习惯都是走第三方交易的，简单来说就是由于电子交易的普及，大家都习惯于用支付宝和微信支付，而在使用这两种支付方式的时候也是通过绑定银行卡来实现金额划转的。那么在银行卡的账单系统中就只能记录下从「银行卡」到「支付宝」的账面信息，这对记账是没有任何意义的，因为你不知道支付宝到底支付了什么东西，而真的交易信息存在了不怎么开放的支付宝里面，微信也是同理。&lt;/p&gt;
&lt;p&gt;即便采用了账单导出的功能，那么银行卡账单于支付宝账单去重也是一件极其复杂和麻烦的事情。市面上也有不少半自动的从支付宝手动下载账单然后脚本导入的模式，但是本质上并没有解决这一个痛点。&lt;/p&gt;
&lt;h2&gt;关于造轮子这件事&lt;/h2&gt;
&lt;p&gt;对于我个人来说，Beancount这款复式记账软件已经够用了，也极大的降低了复式记账的复杂度。但是我这段时间还在模仿beancount造一个记账软件出来的意义，对于其他人来说可能不是痛点，但是对于我来说缺难以忍受。&lt;/p&gt;
&lt;p&gt;我个人是有攒小票的习惯的，那么不能在交易上把小票等信息关联起来就是一个难以接受的事情，所以我为交易增加了一个附件的功能，它可以关联各种形式的文件，我现在用来关联购物小票，购物发票，借款声明等等。但是目前还没有办法把关联的图片直接显示出来，只能通过下载查看，这是一个比较大的问题。&lt;/p&gt;
&lt;p&gt;上述说到日常的90%交易都是点对点的账户交易，beancount并没有优化这种交易的记录模式，我也是为了解决这个痛点，让日常记账更加容易和易读。&lt;/p&gt;
&lt;p&gt;beancount中是有tag系统的，可以为交易挂上各种标签，比如说这些交易都是发送在某次旅行中的，但是这个「挂标签」的动作是每次交易都需要手动加上，所以我就自己写了一个事件系统，可以开始某个事件结束事件，在事件中记录软件会自动加上事件定义的标签，从而解决通过标签分类账单的问题。&lt;/p&gt;
&lt;p&gt;为了可以延续beancount的生态，导出成beancount的文件，从而接入beancount强大查询功能也是在计划当中。&lt;/p&gt;
&lt;h3&gt;广告&lt;/h3&gt;
&lt;p&gt;如果你对记账感兴趣，而且有空，那么可以联系我，缺UX！缺前端！（如果能有人糊一个APP出来就更好了&lt;/p&gt;
</content:encoded></item><item><title>2020：他说有点累了</title><link>https://www.kilerd.me/summaries-my-2020/</link><guid isPermaLink="true">https://www.kilerd.me/summaries-my-2020/</guid><description>“你说，你觉得我是一个很菜的人吗？”我没有回答，只是静静地拿起了桌上的酒杯，事实上我也不知道怎么回答？ 坐在我对面的是耳先生，我和他已经一年没有见面了。今天是圣诞节的晚上，很难得我可以把他约出来，但是坐下来已经快半个小时了，他才缓缓说出这句话。</description><pubDate>Sat, 09 Jan 2021 17:27:11 GMT</pubDate><content:encoded>&lt;p&gt;“你说，你觉得我是一个很菜的人吗？”我没有回答，只是静静地拿起了桌上的酒杯，事实上我也不知道怎么回答？&lt;/p&gt;
&lt;p&gt;坐在我对面的是耳先生，我和他已经一年没有见面了。今天是圣诞节的晚上，很难得我可以把他约出来，但是坐下来已经快半个小时了，他才缓缓说出这句话。&lt;/p&gt;
&lt;p&gt;我从他的眼里看出了一丝疲倦，我不知道他这一年里经历了什么，我也不知道怎么回答他，只能举起了酒杯跟他碰了一下，然后就等着他自己讲出他这一年碰到的事情。&lt;/p&gt;
&lt;p&gt;我认识耳先生已经快有十年的时间了，他在我的印象中一直都是一个很勤奋很聪明的人，他身边的人都很习惯地碰到什么问题都会找他解答，耳先生他也是来者不拒。&lt;/p&gt;
&lt;p&gt;“我感觉我这一年好像有这么荒废过去了，想学的没学好，还莫名的受了好几次气”他咕咕咕喝下一大口啤酒说。“不会啊，你不是还跟我说过你把好几个软件都写出来了吗？那个叫什么stap...”终于我能搭上了话。“哦，你说的是 staple啊，嗯我是把他快写完了”&lt;/p&gt;
&lt;p&gt;对，就是Staple，按照他当时的说法，他要做一个静态博客生成器，就跟hugo那些一样。我是不知道Hugo是个什么东西，但是我知道静态博客生成器是什么。在他编写它的那一段时间里面有跟我保持联系，我们一直在讨论staple怎么设计好，需要做什么功能。虽然我不是一个互联网从业者，但是我从一个小白和使用者的角度跟他聊了很多很多，他也很会从一个通俗易懂的角度跟我阐释清楚。他这是把我当成了产品经理了。&lt;/p&gt;
&lt;p&gt;我很佩服耳先生的一点就是他似乎拥有着无尽的动力去写自己感兴趣的东西，但是别人不知道的是他基本花光了他所有业余的时间在上面。他是我见过为数不多真正喜欢编程的人，同时我也挺替他感到惋惜的，这样优秀的人却在外包公司写着屎一样的代码。我也跟他聊过这点“要不你就准备准备，面一个BAT，以你的水平应该是进得去的”&lt;/p&gt;
&lt;p&gt;“可我实在没办法忍受加班，那样我就没办法出去玩，没办法学习了”，也对。&lt;/p&gt;
&lt;p&gt;“干了！”耳先生看着酒杯里面不多的啤酒，说出了这句话，然后我们的话题又回到了“菜”上面。&lt;/p&gt;
&lt;p&gt;耳先生菜吗？这个疑问从他说出那句话开始我就在思考。你说他厉害吧，他的英文水平着实从高中开始就是每天都是被罚站的水平；你说他菜吧，他的逻辑思考能力确实又超出常人一丝。&lt;/p&gt;
&lt;p&gt;“其实你还是挺优秀的啊，至少你同事对你的评价都是挺正面的啊”&lt;/p&gt;
&lt;p&gt;耳先生深深叹了一口气，我也知道，他是那种一遍学不会就再学一遍的人。他说他花了那么多时间在上面才有那一点点的产出和收益，只能说明他真的没有这方面的天赋，他又举了几个TU毕业、咕咕噜上班、BAT上班的人，说他们平时聊的东西自己根本就没怎么接触过。“你看，差距就是这么来的了”。确实啊，这让我又想起了他提及过高中一个每天都在拼命学最后只刚刚达到重本线的女生。耳先生的一部分压力其实也来自于他公司内部，他公司里面有着数不胜数的 985 和海龟学生，每一个的资历都是吊打他的，在那个环境下我可能都会感到压抑。&lt;/p&gt;
&lt;p&gt;想起了他英文课天天被罚站，我就想起了他要做的一个产品 &lt;a href=&quot;http://resource.rs&quot;&gt;Resource.rs&lt;/a&gt; ，那一个他跟我讲的时候满眼放光的产品：“我要整合网络上关于 rust 的各种资讯、各种文章，给中文学习者一份很大很大的礼物”。到现在一年过去了，我估计是为数不多偶尔会点开看看更新情况的人，这个网站的内容已经很久很久没有更新了，本身就为数不多的内容很多都已经过时了。我本人是很欣赏这种想法的，把一个门槛很高的东西用简单化的语言讲述出来，这是一件很高尚的事情。“估计也只有程序员才那么傻愿意投入大量时间维护和撰写这种没什么收益的事情”我心里这样想，但是我没敢告诉耳先生。&lt;/p&gt;
&lt;p&gt;耳先生跟我说他可能做不下去了，总结了一下价值有一下几个原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;能力不足。互联网上的资源是参差不齐的，为了筛选和总结这些内容意味着维护者需要有辨识这些内容的能力和产出总结文章的精力。那么这要求了维护者至少是一个rust精通使用者，耳先生自认为达不到这样的水平。而且这便随即带来另外一个问题，如果维护的内容很官方的话，那么这跟《TRPL》、《Rust by example》没有区别了。一个人靠业余的精力真的无法维护出那么精细的内容。&lt;/li&gt;
&lt;li&gt;定位不明确。耳先生想把 &lt;a href=&quot;http://resource.rs&quot;&gt;Resource.rs&lt;/a&gt; 定位成一个覆盖面广、知识准确的入门级资讯网站。但是实践出来的内容却更像是他个人的Rust学习历程。英文水平高的用户完全有能力阅读英文文章，中文社区又不能产生出比较又水平的文章，导致了这个网站的内容缺乏。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实，经常关注耳先生的都能发现他博客里面有着挺多关于 Rust 的博文，我有一次就比较好奇问耳先生：“为什么你不把你Rust的博文发到你那个Rust资讯网呢？”&lt;/p&gt;
&lt;p&gt;“不是我不想啊，我自己也是刚开始学这方面的知识，连我都不知道这是对的还是错的，根本没有勇气把它放在网站上，就怕误导了别人，是不是？国外有个很好的 &lt;a href=&quot;http://cheats.rs&quot;&gt;cheats.rs&lt;/a&gt; ，国内有 rust-lang-cn，有 &lt;a href=&quot;http://rust.cc&quot;&gt;rust.cc&lt;/a&gt; 论坛。我真不知道我到底还能不能坚持下去。”&lt;/p&gt;
&lt;p&gt;一阵晚风从耳边吹过，我们又陷入了短暂的沉默，独自地喝着酒，隔壁桌嘻嘻闹闹的声音时不时吹到我们桌来。&lt;/p&gt;
&lt;p&gt;“哎，这段时间真的好累，我都想辞职休息一段时间了“ 两个酒杯又碰在了一起，打破了那一刻的沉默。“嗯，我好久之前就想这么做了“我附和道。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;我都不知道这一年我自己干了什么，感觉时间过得很快。眼睛刚闭上没几分闹钟就醒了，一眨眼就上班了，再一眨眼就下班了，一眨眼又深夜该睡了。&lt;/p&gt;
&lt;p&gt;上班又经常犯错，被老板同事说。整天做着千遍一律的内容，还要跟自己公司的同事扯皮吵架，说好的我们公司同事间没有竞争压力呢？为什么会出现个问题都急迫着推卸责任。&lt;/p&gt;
&lt;p&gt;还要被老板强制转岗，闹了好一阵子估计转岗培训里面对我的评价应该是最低的吧，反正今年的调薪估计是没我什么事的了。我实在想不到我的出名居然是因为我的犯错让几乎整个中国区的高层都听说过了我的名字，真的是太惨了。在这个公司估计是快混不下去了。&lt;/p&gt;
&lt;p&gt;有时候我在想我是不是真的不适合这个行业，我上班看技术，下班看技术，周末看技术，还是没学会什么东西。总感觉自己陷入了「越菜越学，越学越菜」的困境里面。&lt;/p&gt;
&lt;p&gt;每次在网络上看到其他人说我师从某某某，我跟某某某学技术的时候都很羡慕，感觉我平时都只能自己在瞎胡闹。做无用功估计说的就是我这种行为。&lt;/p&gt;
&lt;p&gt;我很羡慕和佩服那些人。考上985院校、喜欢计算机、对逻辑数理敏感、有份好工作、身边有志同道合的人，每一项我都做不到。&lt;/p&gt;
&lt;p&gt;哎，我也想这样啊。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我知道这个时间不应该说话，我也不知道要说什么，只知道陪着他默默地喝酒。每个人都有每个人的难处，就像那句话说的“人与人的悲欢各不相同，我只觉得他们吵闹“，大家都没有嘲笑互相的资本，至少我没有嘲笑他的能力，哪怕他说的多么幼稚。&lt;/p&gt;
&lt;p&gt;“也不早了，回吧”&lt;/p&gt;
&lt;p&gt;“嗯，也该回了”&lt;/p&gt;
&lt;p&gt;“好，那我走了”&lt;/p&gt;
&lt;p&gt;“嗯，我也走了”&lt;/p&gt;
</content:encoded></item><item><title>Rust 过程宏 101</title><link>https://www.kilerd.me/rust-proc-macro-101/</link><guid isPermaLink="true">https://www.kilerd.me/rust-proc-macro-101/</guid><description>在 Rust 1.45 中，Rust 的{卫生宏}(Hygienic macro)迎来了 stable 版本，这意味着{过程宏}(Procedural macro)和{声明宏}(Declare macro)板块全面稳定。那么是时候该认真学习一边过程宏的内容了。 过程宏相比于声明宏的灵活度更加高，其本质是输入一段 Rus</description><pubDate>Thu, 11 Jun 2020 10:43:26 GMT</pubDate><content:encoded>&lt;p&gt;在 Rust 1.45 中，Rust 的{卫生宏}(Hygienic macro)迎来了 stable 版本，这意味着{过程宏}(Procedural macro)和{声明宏}(Declare macro)板块全面稳定。那么是时候该认真学习一边过程宏的内容了。&lt;/p&gt;
&lt;p&gt;过程宏相比于声明宏的灵活度更加高，其本质是输入一段 Rust 的 AST 产生一段 AST 的函数，同时 Rust 提供了三种不一样的语法糖来满足不同的使用场景。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;{函数式}(Function-like)的宏 - 这跟声明宏很类似&lt;/li&gt;
&lt;li&gt;Derive 宏 - &lt;code&gt;#[derive(CustomDerive)] &lt;/code&gt; - 一个用于结构体和枚举类型的宏&lt;/li&gt;
&lt;li&gt;{参数宏}(Attribute macros) - &lt;code&gt;#[CustomAttribute]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;行为影响&lt;/h2&gt;
&lt;p&gt;这三种宏的的效果也不完全一致。 {函数式宏}(Function-like macro) 和 {参数宏}(Attribute macros) 拥有&lt;strong&gt;修改原AST&lt;/strong&gt;的能力，而Derive 宏就只能做追加的工作。&lt;/p&gt;
&lt;h3&gt;{函数式宏}(Function-like macro)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[proc_macro]
pub fn my_macro(INPUT_TOKEN_STREAM) -&amp;gt; TokenStream {
    OUTPUT_TOKEN_STREAM
}

my_macro!(INPUT_TOKEN_STREAM)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过编译之后，6L 就会被&lt;strong&gt;替换&lt;/strong&gt;成 &lt;code&gt;OUTPUT_TOKEN_STREAM&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Derive 宏&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[proc_macro_derive(MyMacro)]
pub fn derive_my_macro(INPUT_TOKEN_STREAM) -&amp;gt; TokenStream {
	OUTPUT_TOKEN_STREAM
}

#[derive(MyMacro)]
struct MyStruct {...}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过编译之后， 6-7L 就会被编译成以下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[derive(MyMacro)]
struct MyStruct {...}

OUTPUT_TOKEN_STREAM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见，原来的 &lt;code&gt;MyStruct&lt;/code&gt; 并不会被影响，也无法改变，而能做的只是在其后追加新的AST，通常用来生成 &lt;code&gt;Builder&lt;/code&gt; 和 &lt;code&gt;impl Blabla for MyStruct&lt;/code&gt; 从而改变&lt;code&gt;MyStruct&lt;/code&gt; 的行为。&lt;/p&gt;
&lt;h3&gt;{参数宏}(Attribute macros)&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[proc_macro_attribute]
pub fn my_macro(ATTR_TOKEN_STREAM, INPUT_TOKEN_STREAM) -&amp;gt; TokenStream {
    OUTPUT_TOKEN_STREAM
}

#[my_macro(a=1,b=2)]
fn method() {...}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ATTR_TOKEN_STREAM&lt;/code&gt; 为 &lt;code&gt;a=1, b=2&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INPUT_TOKEN_STREAM&lt;/code&gt; 为 &lt;code&gt;fn method() {...}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而编译之后， 6-7L 编译成 &lt;code&gt;OUTPUT_TOKEN_STREAM&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;入门例子使用&lt;/h2&gt;
&lt;p&gt;了解了过程宏的相关基本知识之后呢，就可以根据自己的需求选择不同的实现方式来简化代码。下面会以一个例子来介绍怎么设计一个 Derive 宏，不感兴趣的可以跳过这个章节。&lt;/p&gt;
&lt;p&gt;该章节的代码实现已经放在了 &lt;a href=&quot;https://github.com/Kilerd/rust-derive-macro-demo&quot;&gt;Github kilerd/rust-derive-macro-demo&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;非过程宏实现&lt;/h3&gt;
&lt;p&gt;在一次业务实现中，需要根据错误类型返回前端不同的错误码和消息。这意味着我们对于不同的错误需要三个不同的字段&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTP {返回码}(status code)&lt;/li&gt;
&lt;li&gt;错误的 Code&lt;/li&gt;
&lt;li&gt;错误的具体描述内容&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;返回给前端的结构是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
    &amp;quot;code&amp;quot;: &amp;quot;INVALID_EMAIL&amp;quot;,
    &amp;quot;message&amp;quot;: &amp;quot;Invalid email&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于Java来说，这很容易用一个枚举类型来描述这样的需求：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public enum BusinessError {

    InvalidEmail(400, &amp;quot;INVALID_EMAIL&amp;quot;, &amp;quot;Invalid email&amp;quot;),
    InvalidPassword(400, &amp;quot;INVALID_Password&amp;quot;, &amp;quot;Invalid password&amp;quot;);

    int httpCode;
    String code;
    String message;
    BusinessError(int httpCode, String code, String message) {
        this.httpCode = httpCode;
        this.code = code;
        this.message = message;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这种情况下需要增加错误类型的时候，只需要在 4L 处新增即可，影响的范围不大。&lt;/p&gt;
&lt;p&gt;而对于Rust来说，{枚举类型}(enum)更加像是一种数据结构，所以无法像 Java 一样在 3-4L 里面储存这样的信息，为了达成同样的效果，我们需要在函数里面自己实现返回的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub enum BusinessError {
    InvalidEmail,
    InvalidPassword
}

impl BusinessError {
    pub fn get_http_code(&amp;amp;self) -&amp;gt; u16 {
        match self {
            BusinessError::InvalidEmail =&amp;gt; 400,
            BusinessError::InvalidPassword =&amp;gt; 400,
        }
    }
    pub fn get_code(&amp;amp;self) -&amp;gt; String {
        match self {
            BusinessError::InvalidEmail =&amp;gt; String::from(&amp;quot;INVALID_EMAIL&amp;quot;),
            BusinessError::InvalidPassword =&amp;gt; String::from(&amp;quot;INVALID_PASSWORD&amp;quot;),
        }
    }
    pub fn get_message(&amp;amp;self) -&amp;gt; String {
        match self {
            BusinessError::InvalidEmail =&amp;gt; String::from(&amp;quot;Invalid email&amp;quot;),
            BusinessError::InvalidPassword =&amp;gt; String::from(&amp;quot;Invalid password&amp;quot;),
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际看起来问题也不是很大，可以很好的完成业务需求，但是考虑一下增加错误类型这个业务场景，那么就需要在 3L，10L，16L，22L处做修改，影响的范围就很大了。&lt;/p&gt;
&lt;p&gt;同时我们可以很轻松的看得出来对于 &lt;code&gt;get_code&lt;/code&gt; 和 &lt;code&gt;get_message&lt;/code&gt; 都是对枚举值进行简单的字面格式转换，那么人工做这么一件事件是很耗时的。这个时候就可以让过程宏代替我们实现 &lt;code&gt;impl BusinessError {...}&lt;/code&gt; 里面的所有内容。&lt;/p&gt;
&lt;h3&gt;Derive 宏的建立&lt;/h3&gt;
&lt;p&gt;为了简化代码，我们决定把 &lt;code&gt;BusinessError&lt;/code&gt; 改造成以下的格式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[derive(DetailError)]
pub enum BusinessError {
    InvalidEmail,
    #[detail(code=400, message=&amp;quot;this is an invalid password&amp;quot;)]
    InvalidPassword
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于错误类型 &lt;code&gt;InvalidEmail&lt;/code&gt; ，我们默认返回 httpCode &lt;code&gt;400&lt;/code&gt;， code &lt;code&gt;INVALID_EMAIL&lt;/code&gt; ， message &lt;code&gt;Invalid email&lt;/code&gt;。但是我们可以通过 &lt;code&gt;#[detail(code, message)]&lt;/code&gt; 来定制化 &lt;code&gt;httpCode&lt;/code&gt; 和 &lt;code&gt;message&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们先拟定需要创建的宏的名称为 &lt;code&gt;DetailError&lt;/code&gt; 。那么第一步先把项目改成 workspace 的目录结构。然后在其下面新增一个 &lt;code&gt;detail_error &lt;/code&gt;的lib。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[workspace]
members = [&amp;quot;.&amp;quot;, &amp;quot;detail_error&amp;quot;]

[dependencies]
detail_error = {path=&amp;quot;./detail_error&amp;quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 &lt;code&gt;cargo new detail_error --lib&lt;/code&gt; 创建好 lib 后，需要对 &lt;code&gt;detail_error/Cargo.toml&lt;/code&gt; 增加「这个库是过程宏库」才可以访问到 &lt;code&gt;proc_macro&lt;/code&gt; 这么一个特殊的库。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[lib]
proc-macro = true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其后，在 &lt;code&gt;detail_error/lib.rs&lt;/code&gt; 中声明过程宏处理函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;use proc_macro::TokenStream;

#[proc_macro_derive(DetailError, attributes(detail))]
pub fn detail_error_fn(input: TokenStream) -&amp;gt; TokenStream {
    &amp;quot;&amp;quot;.parse().unwrap()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自此，我们的代码就不会报错了，但是我们还没有在&lt;code&gt;detail_error_fn&lt;/code&gt; 里面返回我们期望的 &lt;code&gt;impl BusinessError{...}&lt;/code&gt; 的 AST。实际上这个宏没有做任何事情。&lt;/p&gt;
&lt;h3&gt;实现 &lt;code&gt;get_http_code&lt;/code&gt; 方法&lt;/h3&gt;
&lt;p&gt;第一步，我们需要先把&lt;code&gt;TokenStream&lt;/code&gt; 格式化成我们期望的枚举结构。那么就用到了 &lt;code&gt;syn&lt;/code&gt; 库，这个库提供了&lt;code&gt;parse_macro_input!&lt;/code&gt; 这个宏来更加方便得访问 AST，在我们把 &lt;code&gt;TokenStream&lt;/code&gt; 格式化成 &lt;code&gt;ItemEnum&lt;/code&gt; 后就可以用&lt;code&gt;dbg!&lt;/code&gt; 来查看其内部的数据了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let enum_struct = parse_macro_input!(input as syn::ItemEnum);
dbg!(enum_struct);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;enum_struct = ItemEnum {
    attrs: [],
    vis: Public(...),
    enum_token: Enum,
    ident: Ident { ident: &amp;quot;BusinessError&amp;quot;, span: #0 bytes(64..77),},
    generics: Generics {...},
    brace_token: Brace,
    variants: [
        Variant {
            attrs: [],
            ident: Ident {ident: &amp;quot;InvalidEmail&amp;quot;, span: #0 bytes(84..96),},
        },
        Comma,
        Variant {
            attrs: [...],
            ident: Ident { ident: &amp;quot;InvalidPassword&amp;quot;, span: #0 bytes(165..180),},
        },
    ],
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里我们先 hardcode 所有的返回值是 &lt;code&gt;400&lt;/code&gt;，先不理会在 &lt;code&gt;#[detail]&lt;/code&gt; 中的配置，那么我们最关心的是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.ident&lt;/code&gt; - 枚举的名字&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.variants[].ident&lt;/code&gt; - 枚举里面有多少成员，以及成员的名字&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么我们可以很轻松的拿到这些值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let ident = &amp;amp;enum_struct.ident;
let variants_ident:Vec&amp;lt;&amp;amp;Ident&amp;gt; = enum_struct.variants.iter().map(|variant| &amp;amp;variant.ident).collect();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是拿到这些值之后，我们的期望还不够，我们期望的是构建出以下的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;impl BusinessError {
    pub fn get_http_code(&amp;amp;self) -&amp;gt; u16 {
        match self {
            BusinessError::InvalidEmail =&amp;gt; 400,
            BusinessError::InvalidPassword =&amp;gt; 400,
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想比如手动拼 &lt;code&gt;TokenStream&lt;/code&gt; ，&lt;code&gt;quote&lt;/code&gt; 这个库提供了更加人性化的方式来生成&lt;code&gt;TokenStream&lt;/code&gt;。我们可以通过以下的代码来生产我们期望的那个函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let output = quote! {
    impl #ident {
        pub fn get_http_code(&amp;amp;self) -&amp;gt; u16 {
            match self {
                #(#ident::#variants_ident =&amp;gt; 400,)*
            }
        }
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里面一些 &lt;code&gt;quote&lt;/code&gt; 特定的文法&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#VARIABLE&lt;/code&gt; 可以访问到当前作用域下的同名变量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#(   )*&lt;/code&gt; 用于展开循环&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自此，我们完成了&lt;code&gt;get_http_code&lt;/code&gt;的方法实现。&lt;/p&gt;
&lt;h3&gt;实现 &lt;code&gt;get_code&lt;/code&gt; 方法&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;get_http_code&lt;/code&gt; 中我们了解了怎么输出一整个函数，对于 &lt;code&gt;get_code&lt;/code&gt; 来说，每一个枚举分支类型返回的值都是不同的，这意味着我们在 &lt;code&gt;let variants_ident:Vec&amp;lt;&amp;amp;Ident&amp;gt; = enum_struct.variants.iter().map(|variant| &amp;amp;variant.ident).collect();&lt;/code&gt; 这里就不能简单的拿到枚举成员的 &lt;code&gt;Ident&lt;/code&gt; 了，我们需要在循环内构件出类似 &lt;code&gt;BusinessError::InvalidEmail =&amp;gt; String::from(&amp;quot;INVALID_EMAIL&amp;quot;)&lt;/code&gt; 这样的完整分支语句。这里其实也是很简单的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let code_fn_codegen:Vec&amp;lt;proc_macro2::TokenStream&amp;gt; = enum_struct.variants.iter().map(|variant| {
        let variant_ident = &amp;amp;variant.ident;
        let content = inflector::cases::screamingsnakecase::to_screaming_snake_case(&amp;amp;variant_ident.to_string());
        quote! {
            #ident::#variant_ident =&amp;gt; String::from(content)
        }
    }).collect();
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;这里为了简单的演示效果，才用了 &lt;code&gt;inflector&lt;/code&gt; 这个字符串格式转换库&lt;/li&gt;
&lt;li&gt;这里用到了 &lt;code&gt;proc_macro2&lt;/code&gt; 这个库，下文会讲为什么需要和其与&lt;code&gt;proc_macro&lt;/code&gt;的区别&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后再拼凑 &lt;code&gt;get_code&lt;/code&gt; 方法签名：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub fn get_code(&amp;amp;self) -&amp;gt; String {
    match self {
        #(#code_fn_codegen,)*
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;get_message&lt;/code&gt;的方法也是同样的道理这里就不重复描述了。&lt;/p&gt;
&lt;h3&gt;从 &lt;code&gt;#[detail]&lt;/code&gt; 中读取数据实现配置化&lt;/h3&gt;
&lt;p&gt;对于每一个 Variant 的 attr 数据都会储存在 &lt;code&gt;attrs&lt;/code&gt; 这个字段中。 &lt;code&gt;#[detail(code=400, message=&amp;quot;this is an invalid password&amp;quot;)]&lt;/code&gt; 就会被格式化成以下的AST： (省略了很多没必要的字段)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;attrs: [
    Attribute {
        path: Path { segments: [ PathSegment { ident: Ident { ident: &amp;quot;detail&amp;quot;,}},],},
        tokens: TokenStream [
            Group {
                stream: TokenStream [
                    Ident { ident: &amp;quot;code&amp;quot;, },
                    Punct { ch: &apos;=&apos;, },
                    Literal { lit: Lit { kind: Integer, symbol: &amp;quot;400&amp;quot; }},
                    Ident { ident: &amp;quot;message&amp;quot;, },
                    Punct { ch: &apos;=&apos;, },
                    Literal { lit: Lit { kind: Str, symbol: &amp;quot;this is an invalid password&amp;quot; }},
                ],
            },
        ],
    },
],
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到 &lt;code&gt;code=400, message=&amp;quot;this is an invalid password&amp;quot;&lt;/code&gt; 一样被格式化成了 &lt;code&gt;TokenStream&lt;/code&gt; 。然而取数据出来也不是一件很简单的事情。所以为了解决这个问题，&lt;code&gt;darling&lt;/code&gt; 应运而生，其借鉴了 &lt;code&gt;serde&lt;/code&gt; 的思想，把&lt;code&gt;TokenStream&lt;/code&gt; 反序列化成自定义的结构。&lt;/p&gt;
&lt;p&gt;根据 &lt;code&gt;darling&lt;/code&gt; 的写法，我们需要把我们期望的数据写成结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;// derive FromDeriveInput, 表示这个结构体可以用 `syn::DeriveInput` 转换过来
#[derive(Debug, FromDeriveInput)]
// darling 自身的配置，接受 `detail` attr的数据，只允许 enum 的结构体，struct 报错。
#[darling(attributes(detail), supports(enum_any))]
struct DetailErrorEnum {
    // enum 的名称
    ident: syn::Ident,
    // enum 的枚举成员格式化成 DetailErrorVariant 
    data: darling::ast::Data&amp;lt;DetailErrorVariant, darling::util::Ignored&amp;gt;,
}

#[derive(Debug, FromVariant)]
#[darling(attributes(detail))]
struct DetailErrorVariant {
    ident: syn::Ident,
    // fields 的数据， 指的是 `InvalidEmail(String)` 里面的 `String`
    fields: darling::ast::Fields&amp;lt;syn::Field&amp;gt;,
    // 这里表示从 `FromMeta` 中取数据，这里特指 `#[detail(code=400)]`
    #[darling(default)]
    code: Option&amp;lt;u16&amp;gt;,
    // 这里表示从 `FromMeta` 中取数据，这里特指 `#[detail(message=&amp;quot;detail message&amp;quot;)]`
    #[darling(default)]
    message: Option&amp;lt;String&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着我们需要把 &lt;code&gt;proc_macro::TokenStream&lt;/code&gt; 转换成 &lt;code&gt;proc_macro2::TokenStream&lt;/code&gt; 再转换成 &lt;code&gt;syn::DeriveInput&lt;/code&gt; 再转换成 &lt;code&gt;DetailErrorEnum&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let proc_macro2_token = proc_macro2::TokenStream::from(input);
let derive_input = syn::parse2::&amp;lt;DeriveInput&amp;gt;(input).unwrap();
let detail_error: DetailErrorEnum = DetailErrorEnum::from_derive_input(&amp;amp;derive_input).unwrap();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&lt;code&gt;dbg!()&lt;/code&gt; 可以看到反序列化之后的结果：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;[detail_error/src/lib.rs:39] &amp;amp;detail_error = DetailErrorEnum {
    ident: Ident { ident: &amp;quot;BusinessError&amp;quot;, },
    data: Enum(
        [
            DetailErrorVariant {
                ident: Ident { ident: &amp;quot;InvalidEmail&amp;quot;, },
                fields: Fields { style: Unit, fields: [], },
                code: None,
                message: None,
            },
            DetailErrorVariant {
                ident: Ident { ident: &amp;quot;InvalidPassword&amp;quot;, },
                fields: Fields { style: Unit, fields: [], },
                code: Some( 500, ),
                message: Some(  &amp;quot;this is an invalid password&amp;quot;, ),
            },
        ],
    ),
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样的结果和过程都比直接操作 &lt;code&gt;TokenStream&lt;/code&gt; 更加直观和可靠。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;但是至今我还不知道对于 &lt;code&gt;#[detail(code=400, message(&amp;quot;password {} is invalid&amp;quot;, p1))]&lt;/code&gt; 这种 &lt;code&gt;message&lt;/code&gt; 是{一组的数据}(group token stream)怎么用 &lt;code&gt;darling&lt;/code&gt; 来写&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个时候就可以遍历 &lt;code&gt;detail_error.data[]&lt;/code&gt; 来完成 &lt;code&gt;get_http_code &lt;/code&gt;的 AST 生成&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let ident = &amp;amp;detail_error.ident;
let variants = detail_error.data.take_enum().unwrap();
let http_code_fn_codegen: Vec&amp;lt;proc_macro2::TokenStream&amp;gt; = variants.iter().map(|variant| {
    let variant_ident = &amp;amp;variant.ident;
    let http_code = variant.code.unwrap_or(400);
    quote! {
        #ident::#variant_ident =&amp;gt; #http_code
    }
}).collect();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相比于之前的hardcode，现在我们在 5L 取出了在 &lt;code&gt;#[detail(code=500)]&lt;/code&gt; 中的值。&lt;/p&gt;
&lt;p&gt;同理 &lt;code&gt;get_message&lt;/code&gt; 也可以用同样的方法生成：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;let message = variant.message.clone().unwrap_or_else(|| {
    inflector::cases::sentencecase::to_sentence_case(&amp;amp;variant_ident.to_string())
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自此整个 &lt;code&gt;BusinessError&lt;/code&gt; 就用过程宏改造完成了。但是真实的业务还没有那么简单，举个例子说，对于认证错误(&lt;code&gt;AuthenticationError&lt;/code&gt;)，通常需要返回具体的错误内容，这意味着 &lt;code&gt;message&lt;/code&gt; 需要跟随着变化。也就是说真正的代码是长这个样子的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;enum BusinessError {
    AuthenticationError(String)
}
fn get_message(&amp;amp;self) {
    match self {
        BusinessError:AuthenticationError(p1) =&amp;gt; format!(&amp;quot;with detail {}&amp;quot;, p1),
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么我们之前的过程宏并不支持这样的特性，其实改造也很简单，在 darling 的 &lt;code&gt;DetailErrorVariant&lt;/code&gt; 的 &lt;code&gt;fields&lt;/code&gt; 里面就存有着 &lt;code&gt;String&lt;/code&gt; 这个信息，那么我们只需要在循环体中构建出类似 &lt;code&gt;#ident::#variant_ident#fields =&amp;gt; format!(#message, #fields)&lt;/code&gt; 的语句即可。 感兴趣的读者可以试着让这个demo 支持该功能。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在我的真实业务场景用使用 &lt;code&gt;#[detail(message=&amp;quot;with detail {0}&amp;quot;)]&lt;/code&gt; 这样的方法来访问具体的字段的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;关于过程宏的一些实践和认知&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;proc_macro&lt;/code&gt; 和 &lt;code&gt;proc_macro2&lt;/code&gt; 的区别&lt;/h3&gt;
&lt;p&gt;前者是 rust 中为 过程宏库（在 &lt;code&gt;Cargo.toml&lt;/code&gt; 中声明了 &lt;code&gt;#[lib] proc_macro=true&lt;/code&gt;）中才能访问的特殊库， 而 &lt;code&gt;proc_macro2&lt;/code&gt; 是与 &lt;code&gt;proc_macro&lt;/code&gt; 基本一致，但是只是一个普通的库，所以 &lt;code&gt;syn&lt;/code&gt; , &lt;code&gt;quote&lt;/code&gt; , &lt;code&gt;darling&lt;/code&gt; 这些都是建立在 &lt;code&gt;proc_macro2&lt;/code&gt; 之上的， 所以在我们编写过程宏的时候基本上都是先把 &lt;code&gt;proc_macro::TokenStream&lt;/code&gt; 转换成 &lt;code&gt;proc_macro2::TokenStream&lt;/code&gt; 进行各种处理，最后才转换成 &lt;code&gt;proc_macro::TokenStream&lt;/code&gt; 交回给 rustc。&lt;/p&gt;
&lt;h3&gt;关于测试&lt;/h3&gt;
&lt;p&gt;根据第一点的前提下，在转换成 &lt;code&gt;proc_macro2::TokenStream&lt;/code&gt; 之后其实就跟过程宏没任何关系了，在抽象出一个独立的函数来处理和生成 &lt;code&gt;proc_macro2::TokenStream&lt;/code&gt; ，我们就可以很轻松的对这个方法进行测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[proc_macro_derive(DetailError, attributes(detail))]
pub fn detail_error_fn(input: TokenStream) -&amp;gt; TokenStream {
    handler(input.into()).into()
}

fn handler(input: proc_macro2::TokenStream) -&amp;gt; proc_macro2::TokenStream {
    // real handler
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单来说，我们可以通过 &lt;code&gt;quote::quote!&lt;/code&gt;来生成 &lt;code&gt;input&lt;/code&gt; 对 &lt;code&gt;handler&lt;/code&gt; 测试：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[test]
    fn it_works() {
        let input = quote! {...};
        let expected_output = quote! {...};
        let output = handler(input);
        assert_eq!(expected_output.to_string(), output.to_string());
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;7L 里面简单的用了 &lt;code&gt;to_string()&lt;/code&gt; 来判断是否一致，导致输出的代码其实并没有带缩进，如果有需要可以用 &lt;code&gt;syn::visit&lt;/code&gt;模块进行更加友善的结果输出。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;用了过程宏之后，为什么就没有代码提示了&lt;/h3&gt;
&lt;p&gt;这点很正常，因为&lt;code&gt;impl BusinessError {...}&lt;/code&gt; 里面的内容是编译时生产的，确实是没有办法做到代码提示。试想下有了代码提示又跳转到哪里呢？&lt;/p&gt;
&lt;p&gt;其实这个问题也不是无解的。通常的做法是建立一个 &lt;code&gt;Trait DetailError&lt;/code&gt; 里面定义好我们需要的三个函数，然后再通过过程宏为 &lt;code&gt;BusinessError&lt;/code&gt; 实现 &lt;code&gt;impl DetailError for Business {...}&lt;/code&gt;。 这样代码提示和跳转就可以跳到 &lt;code&gt;DetailError&lt;/code&gt;的定义里面去了。&lt;/p&gt;
&lt;p&gt;为此我们需要把原来 &lt;code&gt;detail_error&lt;/code&gt; 这个lib 改名成 &lt;code&gt;detail_error_macro&lt;/code&gt; ，再创建一个新的lib 叫 &lt;code&gt;detail_error&lt;/code&gt; 来定义 Trait &lt;code&gt;DetailError&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这点其实是 Rust 的限制，因为过程宏库无法再{暴露}(expose)出其他的任何 Trait 和结构体。&lt;/p&gt;
&lt;h3&gt;注意 ident 和非ident 的处理&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;quote::quote!&lt;/code&gt; 这个宏在处理 &lt;code&gt;String&lt;/code&gt; 类型的时候会自动加上&lt;code&gt;&amp;quot;&lt;/code&gt; 形成 &lt;code&gt;&amp;quot;content&amp;quot;&lt;/code&gt; ，正如数字类型会在后面追加具体的类型一样&lt;code&gt;400u16&lt;/code&gt;。 所以如果通过&lt;code&gt;format!&lt;/code&gt; 拼凑出一个 ident 之后需要用 &lt;code&gt;quote::format_ident!&lt;/code&gt; 转换成 ident 类型，或者直接用 &lt;code&gt;format_ident! &lt;/code&gt; 代替 &lt;code&gt;format!&lt;/code&gt; 。&lt;/p&gt;
</content:encoded></item><item><title>2019 个人总结</title><link>https://www.kilerd.me/summaries-my-2019/</link><guid isPermaLink="true">https://www.kilerd.me/summaries-my-2019/</guid><description>无意中翻博客草稿的时候，发现了 2018 的总结还在停留在草稿阶段，现在就已经要写 2019 年度总结了，不禁感叹时间流逝之快。</description><pubDate>Tue, 31 Dec 2019 13:34:22 GMT</pubDate><content:encoded>&lt;p&gt;无意中翻博客草稿的时候，发现了 2018 的总结还在停留在草稿阶段，现在就已经要写 2019 年度总结了，不禁感叹时间流逝之快。&lt;/p&gt;
&lt;h2&gt;R.I.P Python 2&lt;/h2&gt;
&lt;p&gt;Python 2 停止维护，这绝对是一件所有 Pythonista 值得写入 2020 第一篇文章内的描述。我大概从2014年开始接触Python，但是就已经开始用 Python 3来写项目了。从 14 年到现在，除了写项目逻辑之外更多的时间是花费在 2 与 3 的兼容上面。虽然说 &lt;code&gt;six&lt;/code&gt; 这种专门用来做兼容性库的存在极大的简化了兼容的实现，我还是十分希望能免去这些工作量。&lt;/p&gt;
&lt;p&gt;一开始确实没有太大的理由和动力去做迁移工作，但是 Python 3 的一点点进步足以让迁移有足够的优势：Hash 算法的优化提升了部分性能；async 语法和 asyncio 生态的建立；type hint 的出现。这些让 Python 使用起来更加像一门现代化的语言。&lt;/p&gt;
&lt;p&gt;时至今日，Python 2 的死去，是一件好事，摆脱了这么一个巨大的历史包袱，希望 Python 3 可以有更好的发展，搞搞 JIT，研究一下GIL。希望Python 3 越走越好。&lt;/p&gt;
&lt;p&gt;另外 Guido 的退位也为 Python 带来了新的治理模式，不再是独裁者的所有物。&lt;a href=&quot;https://github.com/pyhandle/hpy&quot;&gt;hpy&lt;/a&gt; 的出现也让 Python 有望存在一个标准的 spec，这样下来越来越多的更好的解析器有望可以涌现。&lt;/p&gt;
&lt;h2&gt;OverWatch 赛事&lt;/h2&gt;
&lt;p&gt;工作后对游戏的热爱就只能投放在赛事上。LOL 上 FPX 夺冠，Dota 里 大巴黎老干爹没能杀入决赛复仇 OG，这些都不是很关心。守望先锋在 2019 年的表现才是让人，让我无比兴奋的。&lt;/p&gt;
&lt;p&gt;先是在世界杯上拿下亚军，再是在国内组出了 4 支俱乐部角逐 OWL 第二赛季的战场。同时 成都 Hunter 队的全华姿态也让国内对其抱有了极大的盼头。一是世界杯上中国队的超常发挥，二是对全华班的执着。听闻 Hunter 背后的老板跟 RNG 的老板还是同一个。从 OWL 第一赛季的「我们根本u知道怎么才能赢」到这个赛季的龙队获得第三赛段冠军，4支还是3支战队杀入季后赛这一切都在宣告着守望先锋在国内的蓬勃发展。正如林迟青说的那样「 We are ready to let the world know CHINA again」。&lt;/p&gt;
&lt;p&gt;2020 年的第三赛季的主客场机制让不少 OWL 比赛在国内举行，相信氛围一定很好。可惜的事情是 Hunter 那位被誉为「神医」的主教练 RUI 因伤离队了，不知道成都队能不能在第三赛季保持水平的同时越战越勇。&lt;/p&gt;
&lt;h2&gt;坚定了 Rust 的路线&lt;/h2&gt;
&lt;p&gt;在工作上写了一年 Java，虽说还是一如既往的讨厌它，但是毕竟是用来吃饭的本领，还是专研了一下，起码保证了自己的饭碗不会丢失。但是在业务的时间里面，更加坚定了3年前做的一个决定「学习 Rust」。&lt;/p&gt;
&lt;p&gt;怎么说呢，前段时间看到一段文字可以很好的描述我对 Rust 的态度：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;大概五六月的时候我领着团队系统地学习了一下 Rust 语言，后来就有一搭没一搭的写点随手就扔的一次性代码。看到 Signal 的这篇文章后，我按捺不住心头的激情一一终于可以 用 Rust 做一个似乎有点什么用的工具了！写下来总体感觉，Rust 有可以媲美 ruby 的表现力，又有可以媲美 C++ 的性能（如果使用正确了），加上略逊于 haskell，但可以秒杀大部分主流语言的类型系统，使得用 rust 写代码是一种享受（除了编译速度慢）。这样一个 小工具200来行代码（包括单元测试，生成式测试以及一个简单的benchmark）就可以完 成，估计用 python, elixir 和 nodejs 都不那么容易达到。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;大概就是这样，得益于过程宏等的一些生态，可以让代码写起来如同脚本语言那样的表现力和编写体验，既有极优秀的性能，还有完备的类型系统。这样 Rust 在各个领域都可以表现得很棒。&lt;/p&gt;
&lt;p&gt;Rust 也让我真正的走上了 PL 的道路，之前的我可能是站在巨人肩膀上的，完全不知道脚下的巨人是谁，能干什么。但是 Rust 让我成功的走出了这一步。慢慢地了解到了类型系统及其图灵完备性，数理系统，逆变协变等等这些可能你日常都在使用，但是不知道其缘由和机理的事情。&lt;/p&gt;
&lt;p&gt;我很庆幸在业务我不再是一个简单的CRUD boy，虽然我还有很长的一条路要走，但是起码我在2019迈出了那一步。很感谢 Rust 为我带来的这一个改变。&lt;/p&gt;
&lt;h2&gt;Side Projects&lt;/h2&gt;
&lt;p&gt;如同我在「技术断舍离」里面描述的那样，我开始不喜欢写同类型的项目，逐渐接触不同领域的东西。我开始认真地想做一个社区，希望能把 &lt;a href=&quot;http://Resource.rs&quot;&gt;Resource.rs&lt;/a&gt; 给做好。我认真反思自己做过的东西，那些没能让我学习到的项目都是一次拖慢你节奏的过程。我注册了3min.work，寓意是「三分热度工作室」，我希望我的一些零时性的，阶段性的，实验性的作品或者尝试可以放在这里，让我有一个更加直观的感受，同时也不会阻止我的前进。&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;一年来，虽说工作不如意，学习上没啥进步，也开始慢慢接受自己的平庸。但是我始终坚信着「勤能补拙」这个朴实的道理。&lt;/p&gt;
</content:encoded></item><item><title>我的技术断离舍</title><link>https://www.kilerd.me/learnning-zen-of-tech/</link><guid isPermaLink="true">https://www.kilerd.me/learnning-zen-of-tech/</guid><description>在工作了一年多后，脑子里面满是想离职的事情。在此之前，我还在持续构思这一年多学习到内容的总结，然后品了品，这一年内，为了柴米油盐持续奔波，并没有留下过多的时间来学习新的技术，反而相比于刚刚毕业的那个时候，这时的我忘记的知识远远多于我学习到的。 然而又尽力过一段时间的沉思，我才意识到这可能并不是一件坏事，反而让我可以更好</description><pubDate>Fri, 08 Nov 2019 08:52:13 GMT</pubDate><content:encoded>&lt;p&gt;在工作了一年多后，脑子里面满是想离职的事情。在此之前，我还在持续构思这一年多学习到内容的总结，然后品了品，这一年内，为了柴米油盐持续奔波，并没有留下过多的时间来学习新的技术，反而相比于刚刚毕业的那个时候，这时的我忘记的知识远远多于我学习到的。&lt;/p&gt;
&lt;p&gt;然而又尽力过一段时间的沉思，我才意识到这可能并不是一件坏事，反而让我可以更好地学习下去。&lt;/p&gt;
&lt;h2&gt;别让你练手的项目成为前进的阻碍&lt;/h2&gt;
&lt;p&gt;大学阶段和刚工作时，我对造轮子有种极致的疯狂，面对各种奇奇怪怪的需求，我都希望通过自己的努力把它实现出来。我十分同意这确实很锻炼人。&lt;/p&gt;
&lt;p&gt;为了研究 web 框架的原理，花了很大的精力看完了 flask tornado sanic 的 源码，自己造了一个 nougat 出来。看了 fastAPI 和 OpenAPI，写了一个能自动生成 swagger 信息的 flask 路由插件。为了不想用别人的 inline translator，自己写了一个极简的 Chrome 插件。为了管理 GitHub star，写了一个 Chrome 插件。等等这样的点子实在是太多太多了。&lt;/p&gt;
&lt;p&gt;为了造轮子，同时找不到一样跟我有空又对这些项目感兴趣的前端劳动力，我于是决定学习前端。为此，我学了 React、Redux、Mbox 等这些现代的前端框架，又去折腾了 PWA，甚至还学了 React-Native 只是为了想把这些想法迁移到 IOS 或者安卓上。&lt;/p&gt;
&lt;p&gt;这些经历看着属实很奇妙也很有意思，然而在此之后随之而来的便是各种维护噩梦。&lt;/p&gt;
&lt;p&gt;WEB 框架 nougat 有人尝试使用了，也给了很多诸如「为什么你这个框架没有 CLI 控制能力」、「为什么别的框架写某某功能很舒服，然而你这个就那么难受」。于是我踏上了一条「重构-修BUG」的恶性循环之路，为了实现这些功能或者修复某个缺陷，导致了我需要花大量的时间投入在上面，甚者为了修复某些BUG需要把整个框架重构大部分。这显然不是我希望看到的，这个框架不是为了让别人使用而出现的，而是我的一个熟悉底层的过程。&lt;/p&gt;
&lt;p&gt;这样占用大量时间的维护工作占用了我大量的业余学习时间。于是我做了一个很大的改变「我做的项目只为了解决我自己的需求」，在这样的前提下，我拒掉了大量的维护工作，让我可以更加轻松地投入到那些我不懂而又十分感兴趣的领域。&lt;/p&gt;
&lt;h2&gt;技术的世界里没有「银弹」&lt;/h2&gt;
&lt;p&gt;要时刻在脑子里面保留着一个概念就是「银弹不可能有。如果有，那你肯定被骗了」。&lt;/p&gt;
&lt;p&gt;在大学后半段开始，我做了一个赌我以后职业之旅的决定「我要以 Rust 为我的主导编程语言」。现在看来，Rust 确实在慢慢火起来。但是当时的我，或者说是从开始工作没多久，我对 Rust 有种莫名的崇拜和狂热，希望什么都用 Rust 来实现，无论是能用 shell 实现的脚本，还是用根本还没成熟的 WASM 来写网页前端。&lt;/p&gt;
&lt;p&gt;Rust 确实很强大，这点毋庸置疑。能做与擅长，是两件截然不同的事情。我就是把这两件事情混在了一起。为了用 WASM 写前端，我看了很多所谓的框架，实际上都是一个很简单的雏型。花了几十倍的消耗终于写出了一个性能提升无关的前端。&lt;/p&gt;
&lt;p&gt;看似很有成就感的事情，仔细思考下来实则不然。写了那么多都还只是停留在调用他人框架的阶段，并没有真正地去了解框架的构造和执行原理，甚至没有去了解 WASM 的原理。相比于写逻辑，后者的知识才是更加值得研究的。&lt;/p&gt;
&lt;p&gt;在意识到了这点之后，我慢慢的形成了语言只是一种工具，在合适的场景使用合适的语言才是一个成熟的表现。相比于用 Rust 来写机器学习，这种看着就不可能的事情其实很容易分辨，难就难在那些两者都表现出「我可以」的场景下。&lt;/p&gt;
&lt;p&gt;快速成型、脚本工作就使用 Python；CLI 解析、网络代理处理等就用 Rust；网页前端使用 React。&lt;/p&gt;
&lt;p&gt;银弹可能不存在，但是一把装满了不同「银弹」的手枪是可能存在的。&lt;/p&gt;
&lt;h2&gt;多刷文档，别重复工作&lt;/h2&gt;
&lt;p&gt;当擅长一门技术之后，就很容易成迷其中，希望写出很多所谓的作品出来表现自己，殊不知其实这些作品都只是表现出你单独一门技术的能力。&lt;/p&gt;
&lt;p&gt;这段时间我就是陷入了这样的困境中，因为自己是擅长 WEB 方向的，同时在熟练使用 actix 之后，脑子里面都是做些什么作品出来。左想右想确实想出了很多，不少也事件出来了，但是都是基于 actix 这个 web 框架的，而且大部分工作都是在「写数据库模型 - CURD」的循环中。&lt;/p&gt;
&lt;p&gt;看似做出了很多有意思的项目，实际上是「业务」，那些东西并没有脱离出那一个特定的技术。&lt;/p&gt;
&lt;p&gt;同时长时间在业务层工作，会形成一种「知其然，不知其所以然」的困境。比如现在我都没搞太懂 actix 到底是怎么跑的，为什么他能做到碾压性的性能压制。&lt;/p&gt;
&lt;p&gt;在这时，我才意识到了自己在底层认知的缺陷。长时间活跃在业务层，缺少了对实现层的了解。为此，如上述所说，我放弃维护了大部分的项目，把这些时间投入到了 RFC 等文档的阅读中。特别是在 Rust 领域里面，单纯的写 Rust 代码 和阅读 Rust RFC 的发展、参与对某个实现的讨论的感觉是完全不一样的。这也让我了解到了更多 PL 领域的内容。&lt;/p&gt;
&lt;p&gt;同时在深入了解底层知识后，才深刻体会到语言只是一种工具的感觉。在工作之前，我都以 Python 和 Rust 为主，然而工作确实以 Java 为主，在没有过多的 Java 基础下，通过对底层抽象的了解，可以在工作中不至于出现什么问题。&lt;/p&gt;
&lt;p&gt;在抛弃掉大量相似的项目后，反而有了更多的时间去了解其他领域的知识，这无论是在深度和广度都十分有用。&lt;/p&gt;
</content:encoded></item><item><title>简单几步打造个人集群和自动化流水线</title><link>https://www.kilerd.me/personal-docker-cluster-and-ci-package-pipeline/</link><guid isPermaLink="true">https://www.kilerd.me/personal-docker-cluster-and-ci-package-pipeline/</guid><description>在认识的小伙伴发了他做的项目部署文档出来之后，我便决定开始写这篇文章，原因是他使用的部署方式太麻烦，而且太不自动化，同时有时候也会因为开发任务繁忙导致没能部署好等等。 这篇文章是介绍了一个极度适合用于个人或者几个人的小团队使用的集群搭建方式，在保证了安全性的同时，提供了几乎全自动的部署方式，在手动配置一次之后，每次服务</description><pubDate>Thu, 13 Jun 2019 05:16:33 GMT</pubDate><content:encoded>&lt;p&gt;在认识的小伙伴发了他做的项目部署文档出来之后，我便决定开始写这篇文章，原因是他使用的部署方式太麻烦，而且太不自动化，同时有时候也会因为开发任务繁忙导致没能部署好等等。&lt;/p&gt;
&lt;p&gt;这篇文章是介绍了一个极度适合用于个人或者几个人的小团队使用的集群搭建方式，在保证了安全性的同时，提供了几乎全自动的部署方式，在手动配置一次之后，每次服务更新都是自动触发的，极大地减少了部署的时间。&lt;/p&gt;
&lt;p&gt;本篇文章适用于 GIT-FLOW 类似的「master 即 生产代码」的一切工作模式（或者某一个分支为生产代码）。如果您的开发模式不符合这个特征，那么可以关闭网页了。&lt;/p&gt;
&lt;h2&gt;服务器架构&lt;/h2&gt;
&lt;p&gt;服务器方面，为了方便使用，我们选择了 docker swarm 而不是 k8s，我们先看一个全览图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.loli.net/2019/06/13/5d0220558718054830.jpg&quot; alt=&quot;server-structure.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;整个架构的思路就是用 NGINX 来代理所有的 web 应用，内部每个应用都以 stack 的方式部署，同时配合 Portainer 进行自动化更新。一个超级简单的部署模式，却基本满足了我个人的所有开发场景。&lt;/p&gt;
&lt;h2&gt;环境部署&lt;/h2&gt;
&lt;p&gt;首先你要有一台独立的服务器，什么发行版都不所谓了，我们不会在宿主机里面干任何事情，一切都是在Docker 内实现。&lt;/p&gt;
&lt;p&gt;服务器只需要对外暴露 80 和 443 端口即可，ssh 使用密钥的方式登陆保证安全。&lt;/p&gt;
&lt;h3&gt;安装Docker 并启动 Docker Swarm 模式&lt;/h3&gt;
&lt;p&gt;因为这里采用了单机的方式，所以一步就启动了 swarm 模式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker swarm init
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装 Nginx&lt;/h3&gt;
&lt;p&gt;在这里 Nginx 作为 Load Balancer 和自动 HTTPS 的工具，需要实现服务发现的功能，你可以用 &lt;code&gt;docker-gen&lt;/code&gt; 自己撸一个，也可以采用现成的软件来完成。这里我才用了这个 &lt;a href=&quot;https://github.com/buchdag/letsencrypt-nginx-proxy-companion-compose/blob/master/2-containers/compose-v3/environment/docker-compose.yaml&quot;&gt;buchdag/letsencrypt-nginx-proxy-companion-compose&lt;/a&gt; 。&lt;/p&gt;
&lt;p&gt;先创建一个 nginx network：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker network create nginx-net --attachable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我喜欢吧 volume 不与任何服务直接挂钩，所以我的 volume 都是独立创建的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker volume create nginx-conf
docker volume create nginx-vhost
docker volume create nginx-html
docker volume create nginx-dhparam
docker volume create nginx-certs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后以 stack 的模式启动 nginx:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: &apos;3&apos;

services:

  nginx-proxy:
    image: jwilder/nginx-proxy
    ports:
      - &amp;quot;80:80&amp;quot;
      - &amp;quot;443:443&amp;quot;
    volumes:
      - nginx-conf:/etc/nginx/conf.d
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-dhparam:/etc/nginx/dhparam
      - nginx-certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
    labels:
      - &amp;quot;com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy&amp;quot;
    networks:
      - nginx-net

  letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion
    depends_on:
      - nginx-proxy
    volumes:
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html
      - nginx-dhparam:/etc/nginx/dhparam:ro
      - nginx-certs:/etc/nginx/certs
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - nginx-net

volumes:
  nginx-conf:
    external:
      name: nginx-conf
  nginx-vhost:
    external:
      name: nginx-vhost
  nginx-html:
    external:
      name: nginx-html
  nginx-dhparam:
    external:
      name: nginx-dhparam
  nginx-certs:
    external:
      name: nginx-certs

networks:
  nginx-net:
    external: true
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker stack deploy --compose-file nginx.yml nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OK，这个时候 nginx 就已经创建好了。&lt;/p&gt;
&lt;h3&gt;安装 Portainer&lt;/h3&gt;
&lt;p&gt;Portainer 是一个为数不多的简洁，消耗资源又少的 docker 管理面板，有他可以更加直观地管理集群的内容，同时新版的 Portainer 还提供了一个比较方便的更新服务的方法，所以他对于我来说是必须的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: &amp;quot;3&amp;quot;
services:
  agent:
    image: portainer/agent
    environment:
      # REQUIRED: Should be equal to the service name prefixed by &amp;quot;tasks.&amp;quot; when
      # deployed inside an overlay network
      AGENT_CLUSTER_ADDR: tasks.agent
      # AGENT_PORT: 9001
      # LOG_LEVEL: debug
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /var/lib/docker/volumes:/var/lib/docker/volumes
    networks:
      - agent_network
    deploy:
      mode: global
      placement:
        constraints: [node.platform.os == linux]

  portainer:
    image: portainer/portainer
    command: -H tcp://tasks.agent:9001 --tlsskipverify
    environment:
      VIRTUAL_HOST: portainer.kilerd.me
      VIRTUAL_PORT: 9000
      LETSENCRYPT_HOST: portainer.kilerd.me
      LETSENCRYPT_EMAIL: blove694@gmail.com
    volumes:
      - portainer_data:/data
    networks:
      - agent_network
      - nginx-net
    deploy:
      mode: replicated
      replicas: 1
      placement:
        constraints: [node.role == manager]

networks:
  agent_network:
    driver: overlay
  nginx-net:
    external: true

volumes:
  portainer_data:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：这里不能直接照抄配置文件了&lt;/strong&gt;：在 &lt;code&gt;portainer&lt;/code&gt; 这个服务里面，对外暴露出了一个GUI管理页面，他是需要通过 nginx 进行代理才能访问的，所以需要修改 &lt;code&gt;VIRTUAL_HOST&lt;/code&gt; &lt;code&gt;LETSENCRYPT_HOST&lt;/code&gt; 为你的域名， &lt;code&gt;LETSENCRYPT_EMAIL&lt;/code&gt; 为你的邮箱。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker stack deploy --compose-file portainer.yml portainer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;好，不出意外的话，你就可以通过 &lt;code&gt;https://你的域名&lt;/code&gt; 来访问到 Portainer 的页面了，进去改密码，就完事了。&lt;/p&gt;
&lt;h3&gt;部署自己的 Docker Registry&lt;/h3&gt;
&lt;p&gt;首先先创建 volumes：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;docker volume create registry_data
docker volume create registry_auth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 &lt;code&gt;registry_auth&lt;/code&gt; 生成一个用于提供密码保护的配置文件 &lt;code&gt;.passwd&lt;/code&gt; ，因为 registry 没有密码很不安全&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;cd /var/lib/docker/volumes/registry_auth/_data
docker run --entrypoint htpasswd registry:2 -Bbn 用户名 密码 &amp;gt; .passwd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;上述不要直接复制，请修改用户名密码&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后，部署 stack：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &amp;quot;3&amp;quot;
services:
  registry:
    image: registry:2
    environment:
      VIRTUAL_HOST: registry.kilerd.me
      VIRTUAL_PORT: 5000
      LETSENCRYPT_HOST: registry.kilerd.me
      LETSENCRYPT_EMAIL: blove694@gmail.com
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/.passwd
      REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm
    volumes:
      - registry_data:/var/lib/registry
      - registry_auth:/auth
    networks:
      - nginx-net
volumes:
  registry_auth:
    external:
      name: registry_auth
  registry_data:
    external:
      name: registry_data

networks:
  nginx-net:
    external: true

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;上述不要直接复制，请修改访问地址，邮箱&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为 nginx 有默认最大传输大小，所以可能会导致&lt;code&gt;docker push image&lt;/code&gt; 失败，在 image 太大时，所以需要一下命令取消限制：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;cd /var/lib/docker/volumes/nginx-vhost/_data
echo &amp;quot;client_max_body_size 0;&amp;quot; &amp;gt; registry.kilerd.me
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;上述不要直接复制，请修改域名&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这样必要的东西就完成了，环境就完全搭建完毕。&lt;/p&gt;
&lt;h2&gt;自动化流水线&lt;/h2&gt;
&lt;p&gt;接下来就是怎么通过流水线自动发布新版本的应用了，这里会以我的一个小项目为例子，一一说明你需要怎么做。&lt;/p&gt;
&lt;p&gt;假设我们的项目就是一个简单的文本：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;echo &amp;quot;hello world&amp;quot; &amp;gt; index.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们编写一个超级简单的 Dockerfile：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM python:3.7

COPY index.html index.html

EXPOSE 8000
CMD [&amp;quot;python -m http.server 8000&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个docker 会暴露出 8000 端口作为 http 访问。&lt;/p&gt;
&lt;h3&gt;Travis CI or Circle CI&lt;/h3&gt;
&lt;p&gt;相比自己搭建一套CI，我现在了 Circle CI 来做持续集成和持续部署。我们的策略是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果不是 master 分支，不执行&lt;/li&gt;
&lt;li&gt;打包docker 镜像&lt;/li&gt;
&lt;li&gt;推送到我们刚刚部署的 Registry&lt;/li&gt;
&lt;li&gt;更新我们的服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;先看看 circle ci 的配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: 2
jobs:
  build:
    working_directory: /app
    docker:
      - image: docker:17.05.0-ce-git
    steps:
      - checkout
      - setup_remote_docker
      - restore_cache:
          keys:
            - v1-{{ .Branch }}
          paths:
            - /caches/app.tar
      - run:
          name: Load Docker image layer cache
          command: |
            set +o pipefail
            docker load -i /caches/app.tar | true
      - run:
          name: Build application Docker image
          command: |
            docker build --cache-from=app -t app .
      - run:
          name: Save Docker image layer cache
          command: |
            mkdir -p /caches
            docker save -o /caches/app.tar app
      - save_cache:
          key: v1-{{ .Branch }}-{{ epoch }}
          paths:
            - /caches/app.tar
      - run:
          name: Push to registry
          command: |
            docker login registry.kilerd.me -u 用户名 -p 密码
            docker tag app registry.kilerd.me/app
            docker push registry.kilerd.me/app
  deploy:
    machine:
      enabled: true
    steps:
      - run:
          name: update service
          command: |
            curl -X POST PORTAINER_WEBHOOK_URL
workflows:
  version: 2
  build-and-deploy:
    jobs:
      - build:
          filters:
            branches:
              ignore:
                - develop
                - /feature-.*/
      - deploy:
          requires:
            - build
          filters:
            branches:
              only: master
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这个配置信息很多都是与缓存有关的，用来加快&lt;code&gt;docker build &lt;/code&gt; 的过程，主要的只有几行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;docker login registry.kilerd.me -u 用户名 -p 密码&lt;/code&gt; 登陆部署的 Registry&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker tag app registry.kilerd.me/app&lt;/code&gt; 打 TAG&lt;/li&gt;
&lt;li&gt;&lt;code&gt;docker push registry.kilerd.me/app&lt;/code&gt; 推送&lt;/li&gt;
&lt;li&gt;&lt;code&gt;curl -X POST PORTAINER_WEBHOOK_URL&lt;/code&gt; 更新服务，这里因为还没有在集群里面创建 stack，所以还没有这个 &lt;code&gt;PORTAINER_WEBHOOK_URL&lt;/code&gt; ，下文会补上。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意：上述用户名、密码、PORTAINER_WEBHOOK_URL 请用 circle 的 environment variable 来储存，不要直接写在配置文件内&lt;/strong&gt; （作者就吃了这样的亏，导致项目无法开源）&lt;/p&gt;
&lt;p&gt;OK，推到项目仓库，circle ci 就开始执行了，配置没问题的话， registry 里面就已经有这个application 的 docker 镜像了，但是更新会失败，因为我们还没有创建application的stack。&lt;/p&gt;
&lt;h3&gt;Application Stack&lt;/h3&gt;
&lt;p&gt;对于一个应用我们都要创建一个独立的stack，并接入 &lt;code&gt;nginx-net&lt;/code&gt; 让 nginx 为应用代理http，同时申请 https 证书。&lt;/p&gt;
&lt;p&gt;那么这个应用的 stack 文件要这么写：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;version: &amp;quot;3&amp;quot;
services:
  backend:
    image: registry.kilerd.me/app:latest
    environment:
      VIRTUAL_HOST: test.kilerd.me
      VIRTUAL_PORT: 8000
      LETSENCRYPT_HOST: test.kilerd.me
      LETSENCRYPT_EMAIL: blove694@gmail.com
    networks:
      - nginx-net

networks:
  nginx-net:
    external: true
  backend:
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;上述配置文件不要直接复制，请修改 镜像地址，域名，邮箱&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;创建 stack，之后我们就去要去找到刚刚缺失的那个 &lt;code&gt;PORTAINER_WEBHOOK_URL&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://i.loli.net/2019/06/13/5d024996dc34d88705.png&quot; alt=&quot;Screen Shot 2019-06-13 at 9.01.43 PM.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;进入你想更新的那个 Service Detail 页面，开启 &lt;code&gt;Service webhook&lt;/code&gt; 功能，链接就出来了，把它复制到circle的配置中。&lt;/p&gt;
&lt;p&gt;一切就完成了。&lt;/p&gt;
&lt;h3&gt;开发流程&lt;/h3&gt;
&lt;p&gt;如果你的开发流程是基于 GIT-FLOW 的话，那么可以 follow 一下步骤进行开发 ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 &lt;code&gt;feature/xxx&lt;/code&gt; 分支开发对于 Feature&lt;/li&gt;
&lt;li&gt;开发完成进入 &lt;code&gt;develop&lt;/code&gt; 分支进行验证&lt;/li&gt;
&lt;li&gt;release version 阶段把 &lt;code&gt;develop&lt;/code&gt; 合并进 &lt;code&gt;master&lt;/code&gt; 分支&lt;/li&gt;
&lt;li&gt;Circle CI 收到 &lt;code&gt;master&lt;/code&gt; 分支的推送 webhook， 触发docker image 构建&lt;/li&gt;
&lt;li&gt;构建完成，推送 image 到 registry&lt;/li&gt;
&lt;li&gt;推送完成，通过 &lt;code&gt;PORTAINER_WEBHOOK_URL&lt;/code&gt; 触发 Portainer 更新指定的 Service&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;docker service update xxx 一直都有个问题，不会主动拉取latest的镜像，portainer 自带的这个可以满足，所以说在我的开发环境里面他是必须的。比如就只能 ssh 到服务器，手动执行命令更新。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;所以在开发阶段，只要开发然后推送，其他都由 CI 帮你完成所有的部署功能。&lt;/p&gt;
&lt;h2&gt;缺点和优化的地方&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;这个部署方式只适用于单机 docker swarm 集群，多机需要用 NAS 来创建 volume&lt;/li&gt;
&lt;li&gt;如果打包出来的docker image 无法执行，没有一个有效的回退旧版本机制&lt;/li&gt;
&lt;li&gt;目前没有找到比较好的日志收集方式&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Actix通过什么方法来实现路由注册的.RUST</title><link>https://www.kilerd.me/rust-how-actix-register-route-in-rust/</link><guid isPermaLink="true">https://www.kilerd.me/rust-how-actix-register-route-in-rust/</guid><description>如果你写过 actix-web 1.0 的代码，会发现在路由注册的函数中，你可以传入各种不同签名的函数题。 App::new() .service( web::scope(&amp;quot;/admin/&amp;quot;) .service( web::resource(&amp;quot;/article&amp;quot;).route( </description><pubDate>Mon, 13 May 2019 00:40:47 GMT</pubDate><content:encoded>&lt;p&gt;如果你写过 &lt;code&gt;actix-web&lt;/code&gt; 1.0 的代码，会发现在路由注册的函数中，你可以传入各种不同签名的函数题。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;App::new()
        .service(
            web::scope(&amp;quot;/admin/&amp;quot;)
                .service(
                    web::resource(&amp;quot;/article&amp;quot;).route(
                        web::post().to(post_method),
                        web::delete().to(delete_method)
                    ),
                )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不难发现，根据业务的不同，传入 &lt;code&gt;to&lt;/code&gt; 方法中的函数签名必然会不同，那么 Actix 是怎么处理的呢？或者说是怎么实现这个功能的。接下来我们将一步一步实现这一个类似的需求。&lt;/p&gt;
&lt;h2&gt;最小的执行框架&lt;/h2&gt;
&lt;p&gt;为了实现这个功能，我们先模拟出一个最小的框架：有一个路由 &lt;code&gt;Router&lt;/code&gt; 他里面有一个方法 &lt;code&gt;to&lt;/code&gt; 来注册 handler，为了方便同时关注我们所想的，handler 这里就设计成只能返回 &lt;code&gt;String&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[derive(Debug)]
enum Method {
    Head,
    Option,
    Get,
    Post,
    Patch,
    Put,
    Delete,
}

struct Router;

impl Router {
    pub fn to&amp;lt;H&amp;gt;(&amp;amp;self, method: Method, handler: H) -&amp;gt; &amp;amp;Self
    where
        H: Fn() -&amp;gt; String,
    {
        println!(&amp;quot;handle route {:?}&amp;quot;, method);
        self
    }
}

fn public_route() -&amp;gt; String {
    &amp;quot;hello world&amp;quot;.into()
}

fn main() {
    let router = Router {};
    router.to(Method::Get, public_route);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码：&lt;a href=&quot;https://gist.github.com/8c8c8ad0dbe21d0ebacc8d9f6f5f5c78&quot;&gt;https://gist.github.com/8c8c8ad0dbe21d0ebacc8d9f6f5f5c78&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在这个演示代码中 17L，限定了传入的 handler 只能是 &lt;code&gt;Fn()-&amp;gt;String&lt;/code&gt;，意思是没有参数，同时返回值为 &lt;code&gt;String&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;允许传入不同参数&lt;/h2&gt;
&lt;p&gt;Rocket 和 Actix 都不约而同的采用了 Request Guard 的方式来对路由进行限制或者扩展，有一个例子是说，如果我们希望一个路由只有授权之后才能访问，那么这个 Handler 是这样签名的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;fn private_route(token: Token) -&amp;gt; String {
    &amp;quot;hello private world&amp;quot;.into()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们注册到 &lt;code&gt;Router&lt;/code&gt; 时，必然需要调用 &lt;code&gt;to&lt;/code&gt; 方法，&lt;code&gt;router.to(method::Post, private_route)&lt;/code&gt; ，那会出现一下的错误&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0593]: function is expected to take 0 arguments, but it takes 1 argument
  --&amp;gt; src/main.rs:37:6
   |
29 | fn private_route(token: Token) -&amp;gt; String {
   | ---------------------------------------- takes 1 argument
...
37 |     .to(Method::Post, private_route);
   |      ^^ expected function that takes 0 arguments

error: aborting due to previous error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误原因是说 &lt;code&gt;to&lt;/code&gt; 里面的 handler 范型约束了不能带参数，而 Rust 又不能写出类似 &lt;code&gt;where H: Fn() -&amp;gt; String | Fn(Token) -&amp;gt; String&lt;/code&gt; 的或关系的骚操作，所以只能把这些关系再抽象一层，于是就抽象出了 Trait &lt;code&gt;HandlerFactory&lt;/code&gt;。 这个 Trait 只是把不同的handler 包装成相似的签名。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;trait HandlerFactory&amp;lt;P&amp;gt; {
    fn call(&amp;amp;self, _: P) -&amp;gt; String;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这下我们就可以通过 &lt;code&gt;handler.call()&lt;/code&gt; 来执行这些 handler。&lt;/p&gt;
&lt;p&gt;同时，我们对刚刚这两个Handler 实现一下这个 Trait &lt;code&gt;HandlerFactory&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;impl&amp;lt;F&amp;gt; HandlerFactory&amp;lt;()&amp;gt; for F where F: Fn() -&amp;gt; String {
    fn call(&amp;amp;self, _: ()) -&amp;gt; String {
        (self)()
    }
}

impl&amp;lt;F&amp;gt; HandlerFactory&amp;lt;(Token, )&amp;gt; for F
    where F: Fn(Token) -&amp;gt; String,
{
    fn call(&amp;amp;self, params: (Token, )) -&amp;gt; String {
        (self)(params.0)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;顺便再改一下 &lt;code&gt;to&lt;/code&gt; 的签名，让 &lt;code&gt;to&lt;/code&gt; 接受 &lt;code&gt;HandlerFactory&lt;/code&gt; 的类型就可以把刚刚的两个handler 都通过 &lt;code&gt;to&lt;/code&gt;方法来注册了。&lt;/p&gt;
&lt;p&gt;详情代码看这里：&lt;a href=&quot;https://gist.github.com/3b166bc90bd6ee6dcb20d3b1f751e119&quot;&gt;https://gist.github.com/3b166bc90bd6ee6dcb20d3b1f751e119&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;FromRequest 的抽象&lt;/h2&gt;
&lt;p&gt;在第二个handler 里面，我们传入了&lt;code&gt;Token&lt;/code&gt; 类型的参数，同时用了 &lt;code&gt;impl&amp;lt;F&amp;gt; HandlerFactory&amp;lt;(Token, )&amp;gt; for Fwhere F: Fn(Token) -&amp;gt; String&lt;/code&gt; 来注册签名，那么如果我们有大量不同类型的参数的话，是不是都要一个一个明确的写出声明呢？其实不然，我们可以给这些类型共同实现一个叫 &lt;code&gt;FromRequest&lt;/code&gt; 的Trait，来统一处理。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;trait FromRequest {
    fn from_request() -&amp;gt; Self where Self: Sized;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;impl FromRequest for Token&lt;/code&gt;, &lt;code&gt;impl FormRequest for String&lt;/code&gt; ... 的方法实现之后，Router 知道这是一个「实现了 &lt;code&gt;FromRequest&lt;/code&gt; Trait 」的类型就可以了。意味着在 &lt;code&gt;to&lt;/code&gt; 的签名里面可以换成 Trait 的名字，而不是某种具体的类型。又因为我们需要告诉 &lt;code&gt;HandlerFactory&lt;/code&gt; 这些参数的具体类型，所以需要在其加多一个范型参数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;trait HandlerFactory&amp;lt;P&amp;gt; {
    fn call(&amp;amp;self, _: P) -&amp;gt; String;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再看看对单个参数的实现&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl&amp;lt;F, P&amp;gt; HandlerFactory&amp;lt;(P, )&amp;gt; for F
    where F: Fn(P) -&amp;gt; String,
          P: FromRequest
{
    fn call(&amp;amp;self, params: (P, )) -&amp;gt; String {
        (self)(params.0)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 3L 我们明确地指出 P 需要是实现了 &lt;code&gt;FromRequest&lt;/code&gt; 的 类型。 好，需求就实现了，具体实现在&lt;a href=&quot;https://gist.github.com/4ee6eb1e3dda0f6e7c8858a1c58026ed&quot;&gt;这里&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;多个参数怎么办&lt;/h2&gt;
&lt;p&gt;handler 里面不可能永远都只有一个参数吧。再看看上面的那个代码，我们在 &lt;code&gt;HandlerFactory&amp;lt;(P, )&amp;gt; &lt;/code&gt; 其实是传入了一个 Tuple，里面只有一个值，类型是 P，同时 P 还是 FromRequest 的类型。&lt;/p&gt;
&lt;p&gt;那是不是意味着只要我们有一个 &lt;code&gt;HandlerFactory&amp;lt;(P, P2)&amp;gt; &lt;/code&gt; 的实现就可以完成两个参数的传入了呢？没错就是这样，所以我们可以写下以下的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;impl&amp;lt;F, P, P2&amp;gt; HandlerFactory&amp;lt;(P, P2)&amp;gt; for F
    where F: Fn(P, P2) -&amp;gt; String,
          P: FromRequest,
          P2: FromRequest
{
    fn call(&amp;amp;self, params: (P, P2)) -&amp;gt; String {
        (self)(params.0, params.1)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那三个参数呢？四个参数呢？五个呢？以此往下，是需要重复写大量的 &lt;code&gt;impl &lt;/code&gt;代码的。但是有一个问题是，对于不同的参数，它的函数签名又不一样，不能用「为某种类型实现某种 Trait」的方式一次性写完。但是又不想写那么多重复的代码怎么办？&lt;/p&gt;
&lt;p&gt;在详细看看一个参数的签名和两个参数的签名，其实只有几个地方不一样，而且大致都能复用，那么这时候宏的作用就出来了。这里我从 actix 模仿了一个出来。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;macro_rules! factory_tuple ({ $(($n:tt, $T:ident)),+} =&amp;gt; {
    impl&amp;lt;F, $($T,)+&amp;gt; HandlerFactory&amp;lt;($($T,)+)&amp;gt; for F
    where F: Fn($($T,)+) -&amp;gt; String,
    {
        fn call(&amp;amp;self, param: ($($T,)+)) -&amp;gt; String {
            (self)($(param.$n,)+)
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 Actix 的源码在&lt;a href=&quot;https://github.com/actix/actix-web/blob/df08baf67f166d2d75118b859f1049b01944daf4/src/handler.rs#L376&quot;&gt;src/handler.rs#L376&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;关于 Rust 宏的签名可以通过&lt;a href=&quot;https://lukaslueg.github.io/macro_railroad_wasm_demo/&quot;&gt;这个网站&lt;/a&gt;来查看它的签名。&lt;/p&gt;
&lt;p&gt;那么我们在实现不同参数的时候就可以通过以下简单的代码来简单实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;factory_tuple!((0, A));
factory_tuple!((0, A), (1, B));
factory_tuple!((0, A), (1, B), (2, C));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于三个参数的宏实现，展开之后是这个样子的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;impl&amp;lt;F, A, B, C, &amp;gt; HandlerFactory&amp;lt;(A, B, C, )&amp;gt; for F
    where F: Fn(A, B, C ) -&amp;gt; String,
{
    fn call(&amp;amp;self, param: (A, B, C, )) -&amp;gt; String {
        (self)(param.0, param.1, param.2 )
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体代码可以&lt;a href=&quot;https://gist.github.com/7610290a37934703a4450888afb54f2f&quot;&gt;看看这里&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;最后&lt;/h2&gt;
&lt;p&gt;这个场景确实是很常见的，这里用了以下几个特性来实现了这个功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为某种类型实现Trait&lt;/li&gt;
&lt;li&gt;为「实现了某种Trait」的类型实现Trait&lt;/li&gt;
&lt;li&gt;利用宏消除重复代码&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;彩蛋&lt;/h3&gt;
&lt;p&gt;看回 &lt;a href=&quot;https://github.com/actix/actix-web/blob/df08baf67f166d2d75118b859f1049b01944daf4/src/handler.rs#L411&quot;&gt;acitx-web handler&lt;/a&gt; 的实现，它只实现到了10个参数，那是不是说只要写出 11 个参数的 handler 就会报错呢？我们感觉来试一下。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;
use actix_web::{web, App, HttpServer, Responder};

fn a_lot_parameters(
    a: web::Path&amp;lt;String&amp;gt;,
    b: web::Path&amp;lt;String&amp;gt;,
    c: web::Path&amp;lt;String&amp;gt;,
    d: web::Path&amp;lt;String&amp;gt;,
    e: web::Path&amp;lt;String&amp;gt;,
    f: web::Path&amp;lt;String&amp;gt;,
    g: web::Path&amp;lt;String&amp;gt;,
    h: web::Path&amp;lt;String&amp;gt;,
    i: web::Path&amp;lt;String&amp;gt;,
    j: web::Path&amp;lt;String&amp;gt;,
    //    k: web::Path&amp;lt;String&amp;gt;,
    //    l: web::Path&amp;lt;String&amp;gt;,
    //    m: web::Path&amp;lt;String&amp;gt;,
) -&amp;gt; impl Responder {
    &amp;quot;hello world&amp;quot;
}

fn main() {
    HttpServer::new(move || {
        App::new().route(
            &amp;quot;/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}&amp;quot;,
            web::get().to(a_lot_parameters),
        )
    })
    .bind((&amp;quot;0.0.0.0&amp;quot;, 8000))
    .unwrap()
    .run();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;10个的情况正常启动了项目。好我们吧注释去掉一个，再启动看看会不会报错&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0277]: the trait bound `fn(actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;) -&amp;gt; impl actix_web::responder::Responder {a_lot_parameters}: actix_web::handler::Factory&amp;lt;_, _&amp;gt;` is not satisfied
   --&amp;gt; src/main.rs:121:24
    |
121 |             web::get().to(a_lot_parameters),
    |                        ^^ the trait `actix_web::handler::Factory&amp;lt;_, _&amp;gt;` is not implemented for `fn(actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;, actix_web::types::path::Path&amp;lt;std::string::String&amp;gt;) -&amp;gt; impl actix_web::responder::Responder {a_lot_parameters}`

error: aborting due to previous error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;YEAH，预期地报错了✌️&lt;/p&gt;
</content:encoded></item><item><title>优化 Rust 的项目 Docker 打包流程.Rust</title><link>https://www.kilerd.me/rust-auto-optimize-dockerize-procedure/</link><guid isPermaLink="true">https://www.kilerd.me/rust-auto-optimize-dockerize-procedure/</guid><description>在把 Project Rubble 从 Rocket 框架迁移到 Actix-web 的过程中，我顺便把困惑已久的 Docker 打包流程优化了不少。 这篇文章适用于那些在项目中带有 Denpendencies.lock 类似的固定依赖版本的 LOCK 文件。</description><pubDate>Sun, 28 Apr 2019 07:52:05 GMT</pubDate><content:encoded>&lt;p&gt;在把 Project Rubble 从 Rocket 框架迁移到 Actix-web 的过程中，我顺便把困惑已久的 Docker 打包流程优化了不少。&lt;/p&gt;
&lt;p&gt;这篇文章适用于那些在项目中带有 &lt;code&gt;Denpendencies.lock&lt;/code&gt; 类似的固定依赖版本的 LOCK 文件。&lt;/p&gt;
&lt;p&gt;一般的构建流程可以分为以下几个步骤：&lt;strong&gt;拉取最新代码&lt;/strong&gt; -&amp;gt; &lt;strong&gt;构建&lt;/strong&gt; -&amp;gt; &lt;strong&gt;打包&lt;/strong&gt; 。拉取代码一般都交由 CI 来完成。下文会着重讲我是怎么优化构建流程的，同时我会依照 Project Rubble 来做真实场景说明，技术栈如下： &lt;code&gt;Rust + Travis CI&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;什么都自己编译&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM rust:1.29
RUN cargo install diesel_cli --no-default-features --features postgres
EXPOSE 8000
COPY . /app
WORKDIR /app
RUN cargo build --release
ENTRYPOINT [&amp;quot;sh&amp;quot;, &amp;quot;./entrypoint.sh&amp;quot;] 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个阶段我们只考虑「如何把项目打包出 docker 的镜像」，所以在这个 Dockerfile 中 有两个超级耗时的命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cargo install diesel_cli --no-default-features --features postgres&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cargo build --release&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第一步实际上是安装 &lt;code&gt;diesel_cli&lt;/code&gt; ，这是为了项目的 数据库 Migration 服务的，因为在 &lt;code&gt;entrypoint.sh&lt;/code&gt; 需要调用 &lt;code&gt;diesel migration run&lt;/code&gt; 命令来更新数据库。&lt;/p&gt;
&lt;p&gt;第二步则是构建我们自己的项目。&lt;/p&gt;
&lt;p&gt;那么在这个场景下，第一步看似是多余的，&lt;code&gt;diesel_cli&lt;/code&gt; 的作者肯定对自己的项目用 CI 跑过，测试过。那么我们是否能通过构建好的镜像来缩减这一步的耗时呢。&lt;/p&gt;
&lt;h2&gt;使用打包好的基础镜像&lt;/h2&gt;
&lt;p&gt;结论是可以的，虽然该库的作者并没有提供这么一个 Docker 镜像，但是社区上面有人封装过了 &lt;code&gt;clux/diesel_cli&lt;/code&gt;， 所以我们可以用以下的方法来缩减我们构建的时间。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM clux/muslrust:nightly as builder
COPY . /app
WORKDIR /app
RUN cargo build --release

FROM clux/diesel-cli
COPY --from=builder /app/target /application/target
COPY --from=builder /app/migrations /application/migrations
COPY --from=builder /app/Rocket.toml /application/Rocket.toml
COPY --from=builder /app/entrypoint.sh /application/entrypoint.sh
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/rubble /application/rubble

EXPOSE 8000
WORKDIR /application
CMD [&amp;quot;sh&amp;quot;, &amp;quot;./entrypoint.sh&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么在看回这个构建过程，只剩下一步耗时操作 &lt;code&gt;cargo build --release&lt;/code&gt; ，我们自己项目的构建过程，这里看似不能再做时间的缩减了，实则不然。&lt;/p&gt;
&lt;p&gt;我们来分析一下构建步骤内部是如何操作的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分析 &lt;code&gt;Cargo.toml&lt;/code&gt; 和 &lt;code&gt;Cargo.lock&lt;/code&gt; 来确定该应用所依赖的库和库的版本&lt;/li&gt;
&lt;li&gt;从 &lt;code&gt;Crates&lt;/code&gt; 下载这些制定版本的库&lt;/li&gt;
&lt;li&gt;编译这些依赖库&lt;/li&gt;
&lt;li&gt;编译自己的应用&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;在做深入的分析之前，我们先要了解一下 &lt;code&gt;docker build&lt;/code&gt; 的缓存机制，简单来说，docker 会对 dockerfile 中的每一步操作进行记录，尤其是 &lt;code&gt;COPY&lt;/code&gt; 和 &lt;code&gt;ADD&lt;/code&gt; 操作，如果 COPY 之后的 文件HASH值（这里值的是整个 docker 镜像的哈嘻之）不变，那么在COPY 之后的 RUN 都会沿用之前的运行结果，直接命中缓存。&lt;/p&gt;
&lt;p&gt;来一个例子是说，假设我们写一个这样的Dockerfile&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;copy test.txt
RUN cp test.txt copy.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一次执行，我们传入了内容为 &lt;code&gt;hello world&lt;/code&gt; 的 &lt;code&gt;test.txt&lt;/code&gt; 文件，docker得到执行后的hash &lt;code&gt;A&lt;/code&gt;，然后只想步骤二，得到 hash &lt;code&gt;B&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第二次执行该脚本时，如果执行完第一步得到的hash值还是 &lt;code&gt;A&lt;/code&gt; 的话，那么 docker 会跳过执行步骤二，直接去缓存下来的结果。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;因为每次构建都是对自己项目的全新构建，那么我们可以考虑把下载和编译依赖库的步骤缓存下来。&lt;/p&gt;
&lt;h2&gt;缓存项目的 Rust 依赖&lt;/h2&gt;
&lt;p&gt;为了缓存项目的依赖部分，我们把 Project Rubble 的 Dockerfile 构建改成了一下的样子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM clux/muslrust:stable as builder

WORKDIR /app

RUN USER=root cargo new rubble
WORKDIR /app/rubble

COPY Cargo.toml Cargo.lock ./

RUN echo &apos;fn main() { println!(&amp;quot;Dummy&amp;quot;) }&apos; &amp;gt; ./src/main.rs

RUN cargo build --release

RUN rm -r target/x86_64-unknown-linux-musl/release/.fingerprint/rubble-*

COPY src src/
COPY migrations migrations/
COPY templates templates/

RUN cargo build --release --frozen --bin rubble


FROM alpine:latest

COPY --from=builder /app/rubble/migrations /application/migrations
COPY --from=builder /app/rubble/templates /application/templates
COPY --from=builder /app/rubble/target/x86_64-unknown-linux-musl/release/rubble /application/rubble

EXPOSE 8000

ENV DATABASE_URL postgres://root@postgres/rubble

WORKDIR /application
CMD [&amp;quot;./rubble&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个构建过程相比于上一个版本，可以拆成两个小的步骤&lt;/p&gt;
&lt;h3&gt;构建假的项目，下载并编译依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;RUN USER=root cargo new rubble
WORKDIR /app/rubble
COPY Cargo.toml Cargo.lock ./
RUN echo &apos;fn main() { println!(&amp;quot;Dummy&amp;quot;) }&apos; &amp;gt; ./src/main.rs
RUN cargo build --release
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相比之前的把所有源文件一起复制到 docker 镜像，这次首先把 &lt;code&gt;Cargo.toml&lt;/code&gt; &lt;code&gt;Cargo.lock&lt;/code&gt; 拷贝过去，然后新建一个虚拟的、假的 &lt;code&gt;main.rs&lt;/code&gt; 来伪造项目入口，为的是保证项目能够正常构建。&lt;/p&gt;
&lt;p&gt;那么根据刚刚描述的 Docker 构建缓存策略，如果我们传入的两个 Cargo 文件不变（指的是项目所依赖的内容不变）的情况下，那么我们就不会在每次构建的时候都会下载和编译这些依赖，完全可以复用原来编译好的依赖。&lt;/p&gt;
&lt;h3&gt;删除自己项目的构建信息&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;RUN rm -r target/x86_64-unknown-linux-musl/release/.fingerprint/rubble-*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这条命令是把自己项目的构建信息删除，因为我这里用的是项目 &lt;code&gt;rubble&lt;/code&gt; 的信息，所以如果要使用到自己的项目中，请就保证这里删除的目录是正确的。&lt;/p&gt;
&lt;p&gt;这里删除的应该是构建二进制文件的指纹 &lt;code&gt;fingerprint&lt;/code&gt;，其实我也不太清楚为什么在 docker 构建的时候需要删除，在日常编译中却不需要，不太了解 cargo 的运行机制。但是著者试过，如果不删除这个文件，那么在下一步的真正编译项目中便会不成功。&lt;/p&gt;
&lt;h3&gt;真正的构建过程&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;COPY src src/
COPY migrations migrations/
COPY templates templates/

RUN cargo build --release --frozen --bin rubble
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里就是真正地把项目源文件拷贝进 docker 镜像进行编译&lt;/p&gt;
&lt;h3&gt;最小化运行镜像&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM alpine:latest

COPY --from=builder /app/rubble/migrations /application/migrations
COPY --from=builder /app/rubble/templates /application/templates
COPY --from=builder /app/rubble/target/x86_64-unknown-linux-musl/release/rubble /application/rubble

EXPOSE 8000

ENV DATABASE_URL postgres://root@postgres/rubble

WORKDIR /application
CMD [&amp;quot;./rubble&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一步是可选的，因为 Rust 项目编译之后便不依赖于 Cargo 环境了，编译后的二进制文件可以直接在其对应的平台上运行，所以选择了一个最小的可运行平台来跑，以缩减系统其他套件带来的资源消耗。&lt;/p&gt;
&lt;p&gt;至此，我们把整个构建过程能缓存的部分都用缓存实现了，从之前的构建1个小时，到现在在不更新依赖的情况下10分钟完成构建，这个提升还是挺显著的。&lt;/p&gt;
&lt;p&gt;此外，项目还重新选择了 &lt;code&gt;embbed_migration&lt;/code&gt; 来做数据库迁移工作，有意可以参考下 &lt;a href=&quot;https://docs.rs/diesel_migrations/1.4.0/diesel_migrations/&quot;&gt;diesel_migrations&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>自动解引用.RUST</title><link>https://www.kilerd.me/rust-auto-deref/</link><guid isPermaLink="true">https://www.kilerd.me/rust-auto-deref/</guid><description>解引用应该说是 Rust 为了解决不采用 Class 来实现对象化编程的一个解决方案。假想一下如果 Python 或者 Java 之流，需要对一个结构体（准确来说应该是类）进行自定义扩展：增加字段，增加方法，重写方法等等，我们可以直接用继承的方式来实现 class Base: a: int = 2 class Exte</description><pubDate>Wed, 17 Apr 2019 22:11:25 GMT</pubDate><content:encoded>&lt;p&gt;解引用应该说是 Rust 为了解决不采用 Class 来实现对象化编程的一个解决方案。假想一下如果 Python 或者 Java 之流，需要对一个结构体（准确来说应该是类）进行自定义扩展：增加字段，增加方法，重写方法等等，我们可以直接用继承的方式来实现&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Base:
	a: int = 2

class Extend(Base):
	my_self_field: int = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当一个函数希望传入实现了 &lt;code&gt;Base&lt;/code&gt; 类的所有实例时，可以直接以 &lt;code&gt;Base&lt;/code&gt; 为约束，限定其参数范围。在 Java 中就可以使用基类或者 Interface 来约束。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def base_bound(param: Base):
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一套在 Rust 并不适用，在 Rust 中时采用 Struct + Trait 来抽象对象化。所以若想对结构体进行扩展，那么就只能再用一层结构体去包（wrap）住原来的结构体。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;struct MyOwnDerefStruct(String);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我们就对 &lt;code&gt;String&lt;/code&gt; 进行了自己的封装，这里并没有对字段进行扩展，但是这确实在 Rust 中比较常见的场景：一旦我们希望对某个特定的 Struct 实现某个特定的 Trait，同时 Struct 和 Trait 都来自第三方库（不在当前库中定义），那么为了实现&lt;code&gt;impl Trait for Struct&lt;/code&gt; ，我们就需要解决孤儿定律（Orphan Rule），此时我们就可以用这种简单的包装方式来满足他。&lt;/p&gt;
&lt;blockquote&gt;
&lt;h5&gt;什么是孤儿定律 Orphan Rule？&lt;/h5&gt;
&lt;p&gt;在 Rust 中， 若想对 Struct 实现一个 Trait， 那么 Struct 和 Trait 一定要有一方是在当前库中定义的。&lt;/p&gt;
&lt;p&gt;这个约束很好理解，也很适用。&lt;/p&gt;
&lt;p&gt;假设一个场景：C 库中对 A 的 Struct 实现了 B 中的 Trait。此时我们在当前库中使用了 C 库和 A 库，那么我们可能会对 A 中的 Struct 误解，其可能已经被继承了很多奇怪的 Trait，会严重影响我们对 Struct 的使用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;同时，Rust 为了移除运行时和 GC 的消耗，实现了诸多智能指针：&lt;code&gt;Box&lt;/code&gt; &lt;code&gt;Rc&lt;/code&gt; &lt;code&gt;Arc&lt;/code&gt; 等等。所以可能会出现诸如以下的包装 &lt;code&gt;let param: Arc&amp;lt;Mutex&amp;lt;Box&amp;lt;Vec&amp;lt;i32&amp;gt;&amp;gt;&amp;gt;&amp;gt;;&lt;/code&gt; 这种在 Python 中只是简单的 &lt;code&gt;a: List&amp;lt;int&amp;gt;&lt;/code&gt; 的包装。&lt;/p&gt;
&lt;p&gt;为了方便这种因为语言特性导致的额外包装，Rust 提供了自动解引用 &lt;code&gt;Deref&lt;/code&gt; 来简化编程。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;#[lang = &amp;quot;deref&amp;quot;]
#[doc(alias = &amp;quot;*&amp;quot;)]
#[doc(alias = &amp;quot;&amp;amp;*&amp;quot;)]
#[stable(feature = &amp;quot;rust1&amp;quot;, since = &amp;quot;1.0.0&amp;quot;)]
pub trait Deref {
    /// The resulting type after dereferencing.
    #[stable(feature = &amp;quot;rust1&amp;quot;, since = &amp;quot;1.0.0&amp;quot;)]
    type Target: ?Sized;

    /// Dereferences the value.
    #[must_use]
    #[stable(feature = &amp;quot;rust1&amp;quot;, since = &amp;quot;1.0.0&amp;quot;)]
    fn deref(&amp;amp;self) -&amp;gt; &amp;amp;Self::Target;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若要使用 Deref Trait，我们先看看 Deref 里面有什么东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;type Target&lt;/code&gt; 指的是我们希望被解引用到那个数据结构。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deref()&lt;/code&gt; 提供了一个手动调用解引用到 &lt;code&gt;&amp;amp;Target&lt;/code&gt; 的方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;回到我们写的 &lt;code&gt;MyOwnDerefStruct&lt;/code&gt; 例子，我们包装了 &lt;code&gt;String&lt;/code&gt; 类型，如果现在有一个接收 &lt;code&gt;&amp;amp;String&lt;/code&gt; 参数的函数，在没有 Deref 的场景我们需要怎么调用他呢？&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;struct MyOwnDerefStruct(String);

fn print(s: &amp;amp;String) {
    println!(&amp;quot;{}&amp;quot;, s);
}

fn main() {
    let deref_struct = MyOwnDerefStruct(String::from(&amp;quot;hello world&amp;quot;));
    print(&amp;amp;deref_struct.0);
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 L9 中，我们需要先通过 &lt;code&gt;deref_struct.0&lt;/code&gt; 获取到 &lt;code&gt;MyOwnDerefStruct&lt;/code&gt; 中的第一个属性 &lt;code&gt;String&lt;/code&gt; ，然后再通过 &lt;code&gt;&amp;amp;&lt;/code&gt; 来转换成 &lt;code&gt;&amp;amp;String&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;如果我们直接用会 C 中的逻辑 &lt;code&gt;print(&amp;amp;deref_struct)&lt;/code&gt; ，我们会得到以下的错误信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0308]: mismatched types
  --&amp;gt; src/main.rs:23:11
   |
23 |     print(&amp;amp;deref_struct);
   |           ^^^^^^^^^^^^^ expected struct `std::string::String`, found struct `MyOwnDerefStruct`
   |
   = note: expected type `&amp;amp;std::string::String`
              found type `&amp;amp;MyOwnDerefStruct`

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时如果我们为我们的结构体 &lt;code&gt;MyOwnDerefStruct&lt;/code&gt; 实现自动解引用的话，以上代码就可以正常编译：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;impl Deref for MyOwnDerefStruct {
    type Target = String;

    fn deref(&amp;amp;self) -&amp;gt; &amp;amp;Self::Target {
        return &amp;amp;self.0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上述代码中，我们告诉 Rust 编译器：「我们期望 &lt;code&gt;MyOwnDerefStruct&lt;/code&gt; 被解到 &lt;code&gt;String&lt;/code&gt;」，那么在编译过程中碰到需要 &lt;code&gt;&amp;amp;String&lt;/code&gt; 时，编译器会自动帮我们转换。而且我们还能写出以下相当 tricky 的代码 &lt;code&gt;print(&amp;amp;&amp;amp;&amp;amp;&amp;amp;&amp;amp;&amp;amp;&amp;amp;&amp;amp;&amp;amp;&amp;amp;&amp;amp;&amp;amp;&amp;amp;&amp;amp;&amp;amp;deref_struct);&lt;/code&gt;（这代码能跑的原因，可以自行去看看 Rust 内部对 &amp;amp; 的处理）&lt;/p&gt;
&lt;p&gt;自动解引用还有一个好处就是，他可以直接寻址到 &lt;code&gt;Target&lt;/code&gt; 的方法。在我们的场景里面，&lt;code&gt;String&lt;/code&gt; 中有判断两个字符串是否相等的方法 &lt;code&gt;eq&lt;/code&gt; 。因为自动解引用的存在，我们并不需要 &lt;code&gt;deref_struct.0.eq(&amp;quot;hello world&amp;quot;)&lt;/code&gt; 的写法。  &lt;code&gt;MyOwnDerefStruct&lt;/code&gt; 可以直接调用 &lt;code&gt;eq&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;assert_eq!(true, deref_struct.eq(&amp;quot;hello world&amp;quot;));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就避免了在使用智能指针的时候在代码中出现大量的 &lt;code&gt;variable_box.0.method()&lt;/code&gt; 。避免了 &lt;code&gt;.0&lt;/code&gt; 的出现，大大地简化了代码，同时也增加了代码的可读性。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;variable.deref()&lt;/code&gt; 和 &lt;code&gt;*variable&lt;/code&gt; 的区别&lt;/h2&gt;
&lt;p&gt;使用解引用的时候需要注意的是函数 &lt;code&gt;deref()&lt;/code&gt; 和 &lt;code&gt;*&lt;/code&gt; 的行为是不一样的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;deref()&lt;/code&gt; 的函数原型是 &lt;code&gt;fn deref(&amp;amp;self) -&amp;gt; &amp;amp;Self::Target;&lt;/code&gt; 所以我们拿到的是 Target 的引用 &lt;code&gt;&amp;amp;Target&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt; 是直接拿到 &lt;code&gt;Target&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，简单来说 &lt;code&gt;variable.deref()&lt;/code&gt; 就等价于 &lt;code&gt;&amp;amp;*variable&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>科技的未来到底在哪里？</title><link>https://www.kilerd.me/thought-after-reading-the-electric-state/</link><guid isPermaLink="true">https://www.kilerd.me/thought-after-reading-the-electric-state/</guid><description>最近把电幻国度看完了，同样主题的书籍和电影有黑镜和玩家一号。但是这几个作品表达的未来都不太一样，各自导演和作者表达了对这种技术前景的担忧和喜悦。</description><pubDate>Mon, 11 Feb 2019 00:58:38 GMT</pubDate><content:encoded>&lt;p&gt;最近把电幻国度看完了，同样主题的书籍和电影有黑镜和玩家一号。但是这几个作品表达的未来都不太一样，各自导演和作者表达了对这种技术前景的担忧和喜悦。&lt;/p&gt;
&lt;p&gt;玩家一号是一个彻底的乐观派。在电影里面主角和绿洲世界的创造者共同表现出了人们玩家对这个新鲜事物的接受，但是电影里面也表现出了不少预料之中的负面情绪，比如像那家专门为了寻找彩蛋而生的公司。这个专门用于寻找彩蛋的公司彻彻底底地把这个虚构出来的美好世界变成了一个追求财富的过程。&lt;/p&gt;
&lt;p&gt;而对于主角来说，这个世界代表了一切，甚至对于其他人们普通老百姓而言，这个世界带来的也是利大于弊，他们把精神世界完全寄托于其中，而对于现实，他们只满足了其最基本的物质需求。在电影里面，人们甚至只居住在类似贫民窟的地方，这显然跟当前的社会发展是不一样的。&lt;/p&gt;
&lt;p&gt;然而我想这也是玩家一号、黑镜和电幻国度表达的主题都是一样的。&lt;/p&gt;
&lt;p&gt;黑镜就是一个彻彻底底的反对派，其更像是对整个科技发展的反对，因为黑镜的每一部作品都表现出了当一种科技发展到极致的时候，那种对人类摧毁性的破坏。这也是不少悲观主义者对科技发展的态度，估计黑镜的作者也是想表达这样的思想。如果一种科技没有得到克制，那么带来的破坏是远超于其便利。这个话题十分经典，无数的影视作品和文学作品都会采用这种极致的剧本来捏造戏剧性。这里就不多赘述。&lt;/p&gt;
&lt;p&gt;相反，在电幻国度中，这种态度被得以平衡。熟悉整个小说的基调都是沉闷的，却不会想黑镜那般直截了当得表现主题。电幻国度的故事发生在一个以科技为主导的战后世界。这里的战后明确地指出了是高度科技军事化的战后。军方在用无人机进行大范围的战争后，整个社会变得民不聊生。而我们的主角，是一位身边带着一个机械玩偶的小女生。&lt;/p&gt;
&lt;p&gt;在小说交代背景时，提及无人机驾驶员都无法生育后代，不是说无法怀孕，二十分娩时都是死婴。这更加间接地表现出了科技给人类带来的那种来自基因延续层面的极致破坏。它倒像是在表达一种如果人类高度依赖科技将无法得以延续的调调。&lt;/p&gt;
&lt;p&gt;接着小说就以主角小女生的视角，一步一步往海岸线出发，慢慢地向我们揭露了战后世界的荒凉荒诞。整个世纪其实并不是十分出色，节奏也控制在正常套路之中。但是电幻国度是一本画册，更多的细节和震撼来自于其插画，当阅读并且沉浸在这个情景时，再仔细去看那景色的画面，那种震撼感便油然而生。整个故事只是少年女生沿途所见所闻的小故事，作者并没有直接表达出他对科技高度发展的或褒或贬态度，而是通过现象来描绘及其可能发生的后果，以警示世人。当然，这只是我阅读时的想法，他并不想黑镜和玩家一号那样直接地表达出负面或者正面的感受。电幻国度的阅读感受更加取决于读者，这无疑是一种开发性的结局，其实还有很多作品同样表达出了对于科技高速发展的担忧，但是只是在其主线发展的间隙中穿插。而这几个作品不一样，直接地把这种担忧为主线和背景进行展开。&lt;/p&gt;
&lt;p&gt;这些年来，无论是人工智能还是其他科技的发展，都逐渐被普通百姓所关注，这显然是一件好事，更多的人关注会或多或少带来克制。从很多年前的克隆技术到现在的人工智能和大数据分析，有时候我在想科技的发展速度是不是已经超出了我们所期望，即将达到一种无法抑制的程度，当然，我不是一个彻彻底底的悲观主义者，我更加希望科技的发展方向，不是在军事，而是在更加基础的维度或者关注民生生存问题。一旦依旧是以军事为主导，那么发展的结果极其可能会跟电幻国度表现的一致。&lt;/p&gt;
&lt;p&gt;电幻国度虽然故事并没有十分出色，但是配合其插画和配乐带来的震撼感是不亚于玩家一号和黑镜的，同时还能给你带来更多、更自由的思考空间。&lt;/p&gt;
&lt;p&gt;带上一副耳机，找个安静的地方，一杯茶，一本书，仅此而已。&lt;/p&gt;
</content:encoded></item><item><title>入门就写一个博客程序吧.RUST</title><link>https://www.kilerd.me/rust-how-to-learn-it/</link><guid isPermaLink="true">https://www.kilerd.me/rust-how-to-learn-it/</guid><description>当你不知道要干什么的时候，那就写个博客程序吧。 —— 鲁迅 是的，鲁迅曾经这么说过。当你的编程能力出现停滞的时候就写一个博客吧，尤其是入门阶段。更具体而言是写一个 CMS 系统，这也是我平时学习的习惯，我会一步一步解释清楚为什么我会选择这样的学习路线。</description><pubDate>Fri, 28 Dec 2018 07:03:59 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;当你不知道要干什么的时候，那就写个博客程序吧。 —— 鲁迅&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;是的，鲁迅曾经这么说过。当你的编程能力出现停滞的时候就写一个博客吧，尤其是入门阶段。更具体而言是写一个 CMS 系统，这也是我平时学习的习惯，我会一步一步解释清楚为什么我会选择这样的学习路线。&lt;/p&gt;
&lt;h2&gt;易上手的角度&lt;/h2&gt;
&lt;p&gt;相对于 CS 的其他方面来说， WEB 方向一直都是一个门槛相对较低的一个方向，而且在互联网的知识储备也是最丰富的，同时也是最容易做出成品的。WEB 方向可以及时的给学习者带来足够的反馈和满足，这样会更加鼓励初学者。&lt;/p&gt;
&lt;p&gt;相比于其他方向，WEB 方向的知识也是最浅的，初学者不需学习过多的前提条件即可开始开发作品。这样可以很容易地让初学者关注在新学语言的语法知识上面，而不会出现本末倒置的情况。毕竟这是一次学习新语言的过程，而不是学习 WEB 知识的过程。&lt;/p&gt;
&lt;p&gt;于这样的学习目标下，一个最小型的 WEB 系统可以让我集中注意在语言层面上。一般的语言都会有一个比较成熟的 WEB 生态，那么我们可以比较轻松地学习到如何制作用户权限，如何与数据库打交道，做 CRUD。这可以是我们学习到这个语言大部分的语法，那就足够了，我们并不需要过多关注页面样式和用户交互方面，因为那不是我们的重点。&lt;/p&gt;
&lt;p&gt;在这样的学习背景下，我写出了&lt;a href=&quot;https://github.com/Kilerd/rubble&quot;&gt;Project Rubble&lt;/a&gt;，一个用 Rust 写的博客系统，现在也正式地把博客迁移到了上面去。只有那个程序真正地被使用了，你才会发现程序上会有多少的 BUG。一次一次的BUG 修复足以给你足够多的机会去熟悉该门语言。&lt;/p&gt;
&lt;p&gt;Rubble，乱石，这个名字十分贴合我创造这个轮子的原因。我本来就是希望这个项目可以让我真正地了解 Rust 的特点和语法，而且同时他会是我对 Rust 的一些试验的实验场。我需要确保有一个可运行的项目或者 DEMO 来验证我这些想法和技术，那么 Rubble 足以给我提供这样的环境。&lt;/p&gt;
&lt;p&gt;在这样的基础下，Rubble 这个项目注定是不稳定的，我可能会拼命地加很多看起来很奇怪的特性进去，因为我需要他去做实验。&lt;/p&gt;
&lt;h2&gt;可行的试验场&lt;/h2&gt;
&lt;p&gt;正如上文说，一个可运行的试验场是很重要的，因为他是你学习新知识的地方，可以验证可行性的地方。&lt;/p&gt;
&lt;p&gt;GraphQL 可以说是一个不算很新的 DSL 了，它在我的学习列表里面也停留了很久，但是就是因为没有一个可以跑的项目，导致我对 GraphQL 的认识和了解只停留在官方文档的阶段，可是这次我真正地把它加到了 Rubble 里面，同时也做了很多思考，关于用户权限的控制，关于缓存，关于递归层数等等，这些问题都是需要在真实项目中测试出来的，而不是靠文档可以提供的。&lt;/p&gt;
&lt;p&gt;RSS 的集成却是在意外之中，不过却让我很细地了解了一次 RSS 的内部实现和组成。这虽然并不是学习的主要目的，却是 Rubble 作为一个博客程序必须的部分。&lt;/p&gt;
&lt;p&gt;在此之后，一个简单能跑的的程序和网站就出现了，那么对于我而言就存在了一个完整地可以测试的地方了，那么接下来碰到新的知识点就可以在此基础上做修改。&lt;/p&gt;
&lt;p&gt;我测试包括一下的内容，分布式的储存和TOKEN实验，主要是在 REDIS 中储存TOKEN，并且还研究了一番 REDIS 集群的搭建和使用，如果没有这个网站，估计我并不知道这些知识我该如何验证。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Rubble 并不是一个十分优秀的项目，甚至它的项目结构都是很差的，但是却是我学习 Rust 的一个入门项目，同时也是我的技术试验场。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;虽然我不能保证这个方法能适合于所有初学者，但是这个方法却是可以优秀的学习方法，毕竟对于大部分初学者而言，是没有能力参与或者制作开源项目的，那么「自娱自乐」是最好的方式。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>人家甩你很正常啊，你丑</title><link>https://www.kilerd.me/fine-to-break-up-with-you/</link><guid isPermaLink="true">https://www.kilerd.me/fine-to-break-up-with-you/</guid><description>如果你能重遇一个很久之前遇见过的人，你会想对她说什么。如果命运决定你只能走那么远，你会不会奋力再拼搏一下，再努力往前走两步？ 那么热的夏天，少年的后背被女孩的悲伤烫出一个洞，一直贯穿到心脏，无数个季节的风穿越这条通道，有一只萤火虫在风里飞舞，忽明忽暗。 ——《云边有个小卖部》</description><pubDate>Mon, 24 Dec 2018 07:31:19 GMT</pubDate><content:encoded>&lt;p&gt;如果你能重遇一个很久之前遇见过的人，你会想对她说什么。如果命运决定你只能走那么远，你会不会奋力再拼搏一下，再努力往前走两步？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;那么热的夏天，少年的后背被女孩的悲伤烫出一个洞，一直贯穿到心脏，无数个季节的风穿越这条通道，有一只萤火虫在风里飞舞，忽明忽暗。&lt;/p&gt;
&lt;p&gt;——《云边有个小卖部》&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;家乡是什么&lt;/h2&gt;
&lt;p&gt;父母经常要跟我说要经常回家，我也时刻在想家乡到底是什么，家乡难道不就是一家人在的地方吗？&lt;/p&gt;
&lt;p&gt;在一个陌生的城市拼搏，一个人流浪。每逢家人打电话过来，无论是鲍参翅肚，还是白粥馒头，交给对面的永远都是「我过得很好，不用担心」。王莺莺在小说中说，什么是故乡，祖祖辈辈埋葬在这里，所以就叫故乡。仔细想了想，也对。长大了除了过年清明，其余时间都是在外打拼，即便是难得的假日，也怕望着望外走，而不是回家乡望一望。&lt;/p&gt;
&lt;p&gt;家乡有时候就像是一座围城，心中所系，无法忘怀，却又踌躇不定，望而却步。挥不去的童年成长记忆，却又不甘被困在那一座小城默默终日。&lt;/p&gt;
&lt;p&gt;「我花了一辈子交到的朋友扔掉，去城里认识陌生人？自己有的不要，为什么老想那些没有的。」王莺莺估计是最能看的开的一个人，很明确的知道了自己到底想要什么，到底为了什么而活。嘴上最是嫌弃那刘十三，但是在十三离乡去读大学的时候，还硬是把自己省下来的钱偷偷地塞给了十三，估计那个时候王莺莺是最难受的，仅有的一个家人要离乡背井的，不知道什么时候才能回来。&lt;/p&gt;
&lt;h2&gt;那个爱哭的孩子&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;文刀刘，动不动就哭的十三吗？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这是程霜再次见到刘十三的第一句话。是的，刘十三始终是那个爱哭的小气鬼。永远像一个长不大的孩子，只要受那么一点委屈都可以大哭一场。但是刘十三却又是让人最有感触的一个人，他的人生似乎填满了人的一生注定要走的所有坎坷。他注定是一个平凡人，就像我们每个人。&lt;/p&gt;
&lt;p&gt;似乎每个人都会有一次彻底失败的人生那样，刘十三也是一个彻彻底底的失败者。为数不多的母亲留下来的话「努力一下考上清华」，他始终还是没能考上。倘若十三真的考上的清华，那么这个故事的走向估计还会是同样的基调，毕竟那是一个懦弱怕事，爱哭的刘十三啊。&lt;/p&gt;
&lt;p&gt;十三最令人印象深刻的两件事估计是说走就走的寻找牡丹之旅和那个说到做到的小本本。&lt;/p&gt;
&lt;p&gt;估计只有在那一刻，在被两只脚踩着的刘十三才会想到，怪不得人们说青春是轰轰烈烈的。那是程霜怂恿刘十三去找牡丹，却被牡丹男朋友暴打踩着脚下的场景。没人会想到那个懦弱爱哭的十三居然真的去找牡丹，而且还敢出手去打他的男朋友。估计那个时候主导着刘十三的只有愤怒了吧。但是撑着伞的程霜估计在暗暗流泪，心想「傻瓜，你不是还有我吗」 。&lt;/p&gt;
&lt;p&gt;或许经历过那场被暴打之后，刘十三会明白为什么牡丹离开的那趟车，明明停靠时间有两分钟，而她的告白只花了一分钟。&lt;/p&gt;
&lt;p&gt;「我会过的更好，比以前都好」在补考的教室里，在瑟瑟发抖，莫不知情的老师同学的注视下，满身泥泞的刘十三撕心裂肺地吼出了这句话。&lt;/p&gt;
&lt;h2&gt;爱上一个爱不起的人&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;「人家抛弃你很正常啊，你丑。你忘不掉人家很正常啊，她美。」王莺莺抱着刘十三如是说。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;对啊，讲道理刘十三哪里配得上程霜了，他明知道自己喜欢上了她，也知道自己根本配不上她。程霜的出现估计拯救了刘十三的整个人生。&lt;/p&gt;
&lt;p&gt;程霜，成双，这个名字暗示了成双是她一生中最渴望的事情，从小被医生诊断出活不过三年的她，坚持活到了二十岁，这本来就是一件很伟大的事情了。在第三次碰到十三后，她也知道自己这次是真的走不远了，就真正地想跟刘十三过日子，但是跟刘十三想的一样，她知道自己活不久也不应该耽误十三的一生，最后还是偷偷地离开了刘十三。&lt;/p&gt;
&lt;p&gt;对于程霜而言，最后陪伴刘十三的那段日子估计幸运地体验体验完了本来她能度过的一整个人生。陪伴刘十三，做了一个尽责的妻子。跟刘十三在面馆捡的小女孩让她做了一次幸福的妈妈。那一句「走吧，回家，孩子他爸」，抱着孩子，手挽着十三的手，程霜这时候是极其幸福的。到家后，家有一老王莺莺，左一声「乖孩子」，右一声「孙媳妇」，那应该是程霜这辈子听得最好听的几句话之一。&lt;/p&gt;
&lt;h3&gt;看穿不说穿&lt;/h3&gt;
&lt;p&gt;从头到底，十三是看穿了程霜对他的爱意， 孩子他爸的称呼，到船上划拳的多次询问，再到后面奶奶离开人世时的那声「孙媳妇在哪」 。无一不透露着程霜对刘十三的爱意，同时刘十三也无时无刻都用男性的独特的方式来表达了对程霜的爱。可惜两个人都没敢迈出最后一步，两人都在担心着自己称不上对方，怕耽误了对方的后半辈子。&lt;/p&gt;
&lt;h2&gt;有些告别，就是最后一面&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;树叶被风吹得轻晃，阳光破碎，蝉声隐匿，像远方的潮水。有朵盛开的云，缓缓划过山顶，随风飘向天边。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;刘十三最终还是那么懦弱，在程霜离开人世之后才去找她。十三欠程霜的不止一句「我爱你」，程霜也不止欠十三的一句「再见」。&lt;/p&gt;
&lt;p&gt;最终，王莺莺如愿以偿地在家乡，那个破小镇，离开了人世；程霜也如愿体验完了她想体验的一生；刘十三也可以抛下那一切挂念，远走他乡，独自拼搏。&lt;/p&gt;
</content:encoded></item><item><title>更好的 IDE 配置.RUST</title><link>https://www.kilerd.me/rust-better-ide/</link><guid isPermaLink="true">https://www.kilerd.me/rust-better-ide/</guid><description>这段时间一直忙于折腾 Rust，自从 Rust 2018 Edition 出来之后，一个很明显的感受就是写起来更加符合一个现代化编程语言的样子，当然也有可能是我的水平太低了，还不足以体验到 Rust 那种非人类的写法和特性。 这一系列文章会是我记录 Rust 学习路程的文章，那么自然而然地就是从环境配置开始了。 PS:</description><pubDate>Mon, 10 Dec 2018 18:51:19 GMT</pubDate><content:encoded>&lt;p&gt;这段时间一直忙于折腾 Rust，自从 Rust 2018 Edition 出来之后，一个很明显的感受就是写起来更加符合一个现代化编程语言的样子，当然也有可能是我的水平太低了，还不足以体验到 Rust 那种非人类的写法和特性。&lt;/p&gt;
&lt;p&gt;这一系列文章会是我记录 Rust 学习路程的文章，那么自然而然地就是从环境配置开始了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PS: 这一系列的文章都是以 MacOS 为基础，不会过多涉及 自编译 Rust、环境折腾等等内容，更加注重在如何高效地进行 Rust 开发和 Rust学习技巧。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Rustup&lt;/h2&gt;
&lt;p&gt;Rust 官方推荐使用 Rustup 来安装，这是一个相对好的软件，他可以帮你管理一系列的 Rust 生态。它在某种程度上像 Python 的 &lt;code&gt;pipenv&lt;/code&gt; ，Node 的 &lt;code&gt;nvm&lt;/code&gt; 和 &lt;code&gt;n&lt;/code&gt; 。Rust 的环境需要装很多东西：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cargo&lt;/code&gt; 项目管理和依赖管理&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rust-std&lt;/code&gt; Rust 的 std 库，用于代码提示和分析&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rustfmt&lt;/code&gt; 用于格式化代码&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cargo-watch&lt;/code&gt; 监控文件修改以便重启服务的插件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;等等，很多很多的配套软件生态。同时 Rust 自己也有三个大版本&lt;code&gt;stable&lt;/code&gt; &lt;code&gt;beta&lt;/code&gt; &lt;code&gt;nightly&lt;/code&gt; 。三个版本之间的组件也各不相同。因此在版本切换的时候就需要同时更新相对于的生态组件。Rustup 就很好的帮助我们管理。&lt;/p&gt;
&lt;p&gt;安装就不过多描述，&lt;a href=&quot;https://rustup.rs/&quot;&gt;详情可以查看这里&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;IDE 的选择&lt;/h2&gt;
&lt;p&gt;目前来说，Rust 最好用的有三个编译器支持 &lt;code&gt;vim&lt;/code&gt; &lt;code&gt;vs code&lt;/code&gt; &lt;code&gt;JetBrains IntelliJ&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;当然如果你愿意折腾其他的IDE，可以查阅这里 &lt;a href=&quot;https://areweideyet.com/&quot;&gt;Are we IDE yet&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;我个人大部分时间用的是 &lt;code&gt;JetBrains Clion&lt;/code&gt; ，实际上是跟 &lt;code&gt;IntelliJ&lt;/code&gt; 是一样的。小部分时间在使用 &lt;code&gt;vs code&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;这里我会讲一些我日常用的 IDEA 插件&lt;/p&gt;
&lt;h3&gt;Rainbow Brackets&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://i.loli.net/2018/12/11/5c0f2171407a1.jpg&quot; alt=&quot;rainbow-brackets.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个东西可以让你更加直观地匹配到括号的范围&lt;/p&gt;
&lt;h3&gt;Highlight Bracket Pair&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://i.loli.net/2018/12/11/5c0f221588cbb.jpg&quot; alt=&quot;highlight bracket pair.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;在一些复杂的函数里面，可以可以比较清晰地看到当前 block 的范围&lt;/p&gt;
&lt;h3&gt;Git Conflict&lt;/h3&gt;
&lt;p&gt;这个插件是用来高亮 conflict 的范围，不需要人肉查看冲突的范围在哪个部分&lt;/p&gt;
&lt;h3&gt;Active Intellij Tab Highlighter&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://i.loli.net/2018/12/11/5c0f221598f1a.jpg&quot; alt=&quot;tab-highlighter.jpg&quot;&gt;&lt;/p&gt;
&lt;p&gt;IntelliJ 的高亮 TAB 一直都是一个很麻烦的问题，所以这个插件可以自定义颜色用来高亮当前的 TAB&lt;/p&gt;
&lt;h2&gt;常用的几个 Cargo 组件&lt;/h2&gt;
&lt;h3&gt;&lt;a href=&quot;https://github.com/passcod/cargo-watch&quot;&gt;cargo-watch&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;cargo-watch 用来自动监听项目文件以重新执行编译工作。目前来说我最经常的使用场景就是&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cargo watch -x run&lt;/code&gt; 一般用于 web 的开发&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cargo watch -x test&lt;/code&gt; 用于写库时自动重新跑 Testcase&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Cargo doc&lt;/h3&gt;
&lt;p&gt;这个就是自带的命令，用于生成项目的文档&lt;/p&gt;
</content:encoded></item><item><title>自制 Web 框架的那些事儿</title><link>https://www.kilerd.me/things-of-creating-web-framework/</link><guid isPermaLink="true">https://www.kilerd.me/things-of-creating-web-framework/</guid><description>不想造轮子的程序员不是一个好码农。 —— 鲁迅</description><pubDate>Wed, 11 Jul 2018 19:15:28 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;不想造轮子的程序员不是一个好码农。  —— 鲁迅&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Nougat 的发展流程&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;我不知道点进来看这篇文章的人，有多少是知道我在写一个Python的异步框架的。&lt;/p&gt;
&lt;p&gt;不过无所谓，这篇文章适合于任何人看，适当的时候跳过代码部分即可。实际上更多的是对 Python 的一些见解。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那时候写项目，特别是在跟前端对接的时候，我就在想「写 API 文档好麻烦啊」，「难道就不能自动生成这些文档吗？」。&lt;/p&gt;
&lt;p&gt;是啊，为啥不能自动生成呢？Parameters 的读入和处理都在代码里面写好了。返回的内容也是在代码里面的。难道我就不能改一下代码就可以轻松生成 API 文档了吗？&lt;/p&gt;
&lt;p&gt;那一段时间，我整天都在想这个事情。当时我用 Flask 写一个 API 的流程基本是这样的：下文用用户注册来做演示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义 URL&lt;/strong&gt; 根据 RESTFUL 来说，用户注册实际上就是添加一个用户资源，所以 URL 为&lt;code&gt;POST /user&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定义这个 API 需要那些参数&lt;/strong&gt; 简单来讲，邮箱、密码就足够了&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;考虑参数的类型&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编写 API 逻辑&lt;/strong&gt; 这里就是把信息储存&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;返回的内容&lt;/strong&gt; 这里考虑生成一个 token 给用户&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Flask 代码大概长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.route(&apos;/user&apos;, methods=[&apos;POST&apos;])
def register():
    email = request.form.get(&apos;email&apos;)
    password = request.form.get(&apos;password&apos;)
    
    valid_email(email)
    valid_password(email)
    
    # register logic here
    UserService.register(email, password)
    
    return {
        &apos;token&apos;: token_generator()
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述的五个步骤对于一份API文档来说，除了第四部逻辑部分是不需要的，其他部分都应该展示在文档中。&lt;/p&gt;
&lt;p&gt;那么如果传统的开发流程包括了两个步骤：写代码 和 写文档。可是这是一项十分重复的工作，而且存在实效性的问题：代码更新之后文档还没来得及更新导致对接时出现问题。&lt;/p&gt;
&lt;p&gt;细看每一步，我们又可以采用自动化的方式来生成这些重复的信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@app.route(&apos;/user&apos;, methods=[&apos;POST&apos;])&lt;/code&gt; 完全可以解析到 &lt;code&gt;POST /user&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;email = request.form.get(&apos;email&apos;)&lt;/code&gt; 和 &lt;code&gt;valid_email(email)&lt;/code&gt; 则可以合并成一步，通过一个函数来实现这项自动化的工作 &lt;code&gt;email = Parameter(&apos;email&apos;, from=&apos;form&apos;, type=email)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;return&lt;/code&gt; 部分在大型的系统中都需要在返回之前检查输出的内容是否符合我们预期的值&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样细想之后，代码就可以写出以下的形式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@app.route(&apos;/user&apos;, methods=[&apos;POST&apos;])
@param(&apos;email&apos;, email, location=&apos;form&apos;)
@param(&apos;password&apos;, password, location=&apos;form&apos;)
@marshal({
    &apos;token&apos;: str
})
def register():
    UserService.register(params.email, params.password)
    
    return {
        &apos;token&apos;: token_generator()
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两份代码，就个人而言，我更喜欢第二种，因为后者的层次感更加分明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API 的URL 是什么&lt;/li&gt;
&lt;li&gt;这个 API 规定了哪些参数，从哪里获取，期望的类型？等等&lt;/li&gt;
&lt;li&gt;返回值什么，包含了哪些字段&lt;/li&gt;
&lt;li&gt;业务逻辑是什么&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;根据这些特点，我实现了这样的Router：&lt;a href=&quot;https://github.com/Riparo/nougat-router&quot;&gt;https://github.com/Riparo/nougat-router&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;由于在 flask 上面的造诣不深，对「如何编写一个 Flask 的插件」不熟悉，所以我决定自己去写一个 Web 框架。是的，一切从头来，从零开始。到目前为止已经断断续续维护了1年多了，提交了差不多300个commit。&lt;/p&gt;
&lt;p&gt;决定写这个框架的时候，心里想着要把这样的一个 Router 实现，同时要深入了解一下 Python 的异步。所以就变成了写一个异步的 Web 框架。&lt;/p&gt;
&lt;p&gt;在最开始的那段时间，我在抄袭 Sanic，对，抄袭，对着 Sanic 的第一次提交记录抄了一个可运行的框架。此后我就对这个抄来的东西改了无数次，每一次修改都是一次不同库的选择和思考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;同步还是异步&lt;/strong&gt; 这点思考的时间最少，同步的太多了，而且基于 wsgi 的框架也很多，异步才是未来！&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP的处理方式 Stream 还是 Protocol&lt;/strong&gt; Asyncio 官宣 Protocol 的效率会比 Stream 高很多。但是 Protocol 的处理函数是同步的，需要在一个同步函数里面 spawn 一个异步处理函数，就是 创建一个 &lt;code&gt;Future&lt;/code&gt; 来执行框架的代码。在HTTP make respone 的时候需要在 &lt;code&gt;Future&lt;/code&gt; 中添加 &lt;code&gt;call_back&lt;/code&gt; 来实现。相比之下 Stream 模式下的代码表现形式会更加直接，可以直接在函数中 &lt;code&gt;await logic()&lt;/code&gt;，因此，我选择了 Stream 的模式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步框架的选择&lt;/strong&gt; asyncio 是官方推出的异步实现，&lt;code&gt;curio&lt;/code&gt; 是民间的一个良好实现。
&lt;ul&gt;
&lt;li&gt;从代码的实现上 &lt;code&gt;curio&lt;/code&gt; 更加好，更加优雅&lt;/li&gt;
&lt;li&gt;&lt;code&gt;asyncio&lt;/code&gt; 由于是官方实现，所以生态做得最好。&lt;code&gt;curio&lt;/code&gt; 甚至还在修修补补阶段，生态近乎很惨，不过后来推出了&lt;code&gt;curio-to-asyncio bridge&lt;/code&gt; 使得 &lt;code&gt;curio&lt;/code&gt; 可以分享 &lt;code&gt;asyncio&lt;/code&gt; 的部分生态&lt;/li&gt;
&lt;li&gt;&lt;code&gt;asyncio&lt;/code&gt; 可以使用 &lt;code&gt;uvloop&lt;/code&gt; 调度库来加速 Python 的异步处理，让速度上升一个量级。而 &lt;code&gt;curio&lt;/code&gt; 是纯粹的 Python 实现，所以相比之下 &lt;code&gt;curio&lt;/code&gt; 的速度会慢一些。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP 解析器的选择&lt;/strong&gt; &lt;code&gt;http_tools&lt;/code&gt; 是一个用 C 写的 HTTP 解析器，只能解析报文层面的内容。 &lt;code&gt;h11&lt;/code&gt; 库同时提供了一个服务器和客户端为主体的DFA，用状态来表示当前HTTP进行到了哪一个环境&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;框架长什么样子&lt;/strong&gt; 我个人比较喜欢的都是微框架，框架中不含太多定制的功能，只提供客观的扩展性供客制化。KOA 的用 Middleware 串起来的形式还是 Flask 那种以 Signal 和 Extension 为主的处理呢？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上述的每一点，每一个选择的碰撞都可以在我的框架代码中找到，每一次的修改都让我对 Web 框架的理解深入几分。&lt;/p&gt;
&lt;h2&gt;关于框架生态&lt;/h2&gt;
&lt;p&gt;一个框架即便再优秀，没有一个完善的生态圈都难以让它成为一个优秀的框架。每一位程序员都是高傲和懒惰的，他们期望框架可以完成他们所需要的一切内容。&lt;/p&gt;
&lt;p&gt;我写的这个 Nougat 框架完成后是一个长得非常像 Koa 这样基于 Middleware 的框架，同时我也注入了一些 Flask 的思想进去。即便 Nougat 的设计再好，没有配套使用的库都注定让它成为一个不优秀的框架。&lt;/p&gt;
&lt;p&gt;我很长一段时间就是在做这么一件事：为 Nougat 编写各色必备的中间件。但是实际上我并不清楚我需要写哪些，于是乎我便尝试用 Nougat 来写一个 API 服务器，尝试去做一个真正的项目。我在想，在尝试把 Nougat 放进项目中，我需要做什么：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;完善的路由 Router&lt;/strong&gt; 这点很重要，是我这一切一切的起点。文章的上一部分讲述了我是怎么设计路由的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨域 CORS&lt;/strong&gt; 我一直把 Nougat 作为一个构建 API 服务器的快速框架，所以跨域的问题必须要得到解决。于是乎，我把 Koa-CORS 翻译成了 Python 版本。对，代码一行一行的翻译，同时它工作得很棒。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步服务的连接&lt;/strong&gt; 项目在启动服务器时，需要初始化数据库连接池，这是一项异步任务，所以它需要在服务器启动之后执行，所以我在 Nougat 中添加了 Signal 的概念，以提供服务器启动前后的任务动作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API 文档&lt;/strong&gt; 我是一个十分懒惰的人，既然我的路由已经足够优秀，那么我就不应该手动写一份文档，他应该自动生成。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;便于开发时的组件&lt;/strong&gt; 基本上每个框架都会提供 Auto Reload 的功能，我也为 Nougat 添加了这项功能，采用了 watchdog 和 tornado-reload 两种不同的方案。同时编写了在 Console 上面显示 HTTP 访问记录的 Logger，显示了访问时间，URL，响应状态码，耗时等等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步 ORM 的集成&lt;/strong&gt; Python 生态中两大ORM：SQLalchemy 和 Peewee。可惜的是两者都没有提供完善的异步处理方案，原因会在下一部分详细讲清楚。SQLalchemy 派系的 GINO 迟迟不能解决多对多关系；Peewee-async 至今还未支持 Peewee 3.0。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CLI 集成&lt;/strong&gt; 到了现在，框架已经有很多命令可以使用了：生产模式启动、开发模式启动、生成 API 文档、SHELL 调试模式等等，所以写了 CLI Manager 来管理 CLI 命令，同时允许 Extension 在注册时添加命令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是乎，这段时间，项目逻辑并没有写过太多，更多的是在重构 Nougat 的架构和这些组件的组织模式。&lt;strong&gt;框架内每个理所当然的功能都是维护人员辛苦的劳动成果&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;不争气的 Asyncio&lt;/h2&gt;
&lt;p&gt;即便是 Python 3.5 便推出的官方支持 Asyncio，到目前来说都属于混乱状态。&lt;/p&gt;
&lt;p&gt;目前为止，文件 I/O 没有纳入 asyncio 库是我感到很纳闷的事情，而且 aiofiles 还是通过多线程来实现异步的，不说对不对，感觉起来就是怪怪的。&lt;/p&gt;
&lt;p&gt;在网络 I/O 方面，也没有发展出能让人眼前一亮的 ORM。这点让大部分想升级至异步框架的使用者停下了脚步。&lt;/p&gt;
&lt;p&gt;我现在甚至有点羡慕 JavaScript 那种全异步的处理了，羡慕的原因恰恰是为什么不能发展出优秀 ORM 的原因。&lt;/p&gt;
&lt;p&gt;我们先来看看 Python 里面 ORM 是怎么处理 Relationship 的。&lt;/p&gt;
&lt;p&gt;假设我们定义了两个模型： 用户模型，和用户 Token 模型，采用一对一关系：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class User:
    username = Text()
    
class Token:
    access = Text()
    refresh = Text()
    user = ForeignKey(User, back_ref=&apos;token&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如我们在用户调用API时验证权限时需要判断 Token 的用户：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;token = Token.query.filter(access=&amp;quot;...&amp;quot;).first()  # whatever

user = token.user
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如第二行这样读取用户信息是很理所当然的，毕竟这也是 ORM 的一大特色。那么我们再深入一点看看 ORM 是怎么处理第二行的代码的。&lt;/p&gt;
&lt;p&gt;实际上 Token 在数据库中储存的 &lt;code&gt;user&lt;/code&gt; 只是 &lt;code&gt;user_id&lt;/code&gt; ，是 User 的主键。第一行代码相当于：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM token WHERE access=&amp;quot;...&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，我们这个时候并没有查询用户相关的任何信息，当我们执行第二行代码时，相对于在执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT * FROM user WHERE id=&amp;quot;{token.user_id}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候才查询用户信息，简单来说就是&lt;strong&gt;按需查询&lt;/strong&gt;，同时也是 lazy load。这有时候也是部分用户不喜欢使用 ORM 的原因，因为上述代码可以用一句 SQL 查询完成。直接写 SQL 可以降低数据库的压力。当然了，这不在这篇文章的讨论范围。&lt;/p&gt;
&lt;p&gt;换成异步的场景，这些代码会发生什么事情。别忘了，只要是异步调用都需要在方法前面加上 &lt;code&gt;await&lt;/code&gt; 来表示异步方法。&lt;/p&gt;
&lt;p&gt;首先，Python Property 不支持异步调用，所以 &lt;code&gt;user = token.user&lt;/code&gt; 是不能用的了，即便是你想这样用也不行：&lt;code&gt;user = await token.user&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果我们退而求其次，用一个函数来调用 &lt;code&gt;user = await token.relationship(&apos;user&apos;)&lt;/code&gt; 。看似表现很棒，但是如果需要调用几层，那么代码看起来将会是灾难性的难受。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# address = token.user.address
address = (await (await token.relationship(&apos;user&apos;)).relationship(&apos;address&apos;))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还记得在 Nougat 刚创立起初，有人发 ISSUE 问我有没有写 ORM 的计划，我当时还说 peewee-async 已经很棒了，现在想想都觉得丢人。&lt;/p&gt;
&lt;p&gt;不过高兴的是，asyncio 得到了 uvloop 的支援，让 Python 可以在跑起来有可以跟 NodeJS 一拼的机会。&lt;/p&gt;
&lt;h2&gt;我很期待的一些事情&lt;/h2&gt;
&lt;p&gt;我想，我做的事情已经很多了，也为 Nougat 付出了很多，同时 Nougat 也存在很多问题。但是我想我应该先缓一缓了。&lt;/p&gt;
&lt;p&gt;我接下来会继续为 Nougat 把文档继续写好，写一份比较完善的文档。&lt;/p&gt;
&lt;p&gt;同时我很期待有人尝试去用 Nougat 写一个网站，给我一些反馈。&lt;/p&gt;
</content:encoded></item><item><title>浅谈 Web 用户验证的几种方式</title><link>https://www.kilerd.me/web-authorizations/</link><guid isPermaLink="true">https://www.kilerd.me/web-authorizations/</guid><description>这片文章试图介绍清楚网站前端与后端之间数据交流时用到的技术，诸如 Session，Cookies，Token，Jwt 等等；同时解释清楚几个初学者容易混淆的地方。</description><pubDate>Wed, 21 Feb 2018 08:10:34 GMT</pubDate><content:encoded>&lt;p&gt;这片文章试图介绍清楚网站前端与后端之间数据交流时用到的技术，诸如 Session，Cookies，Token，Jwt 等等；同时解释清楚几个初学者容易混淆的地方。&lt;/p&gt;
&lt;h2&gt;HTTP VS HTTPS&lt;/h2&gt;
&lt;p&gt;HTTP 到底有什么弊端，HTTPS 到底解决了什么问题。&lt;/p&gt;
&lt;p&gt;在 HTTP 中，Client 发送内容给 Server 的过程是全部明文发送的，双方都不会验证对方发送过来的东西是不是正确的。同时在 HTTP 传输的过程中，因为数据是明文暴露的，所以网络中间人（任何网络中间人）都可以对这份数据进行修改。&lt;/p&gt;
&lt;p&gt;假设 A 发送数据「2333」给 B，而有人在传输的过程中修改成「6666」了，B 对这一切一无所知。&lt;/p&gt;
&lt;p&gt;即便存在一个确认机制：B 会发送一个确认信息给 A「你发给我的信息是不是『6666』」。在数据回传给 A 的时候，中间人一样会把「6666」改成「2333」从而让确认环节正常进行。&lt;/p&gt;
&lt;p&gt;那 HTTPS 作为 HTTP 的改进者到底做了什么事情了。（著者：这里不会详细地介绍 HTTPS 的工作机制，只会以简单的逻辑和见解让读者明白 HTTPS 做了那些事情）&lt;/p&gt;
&lt;p&gt;首先，我们希望数据传入过程中不被修改，最好不被查看。不被查看是次要内容，因为只要能确认不被修改，我们有 N 种方法让数据不被查看。那么 HTTPS 就是冲着这个大目的去了。&lt;/p&gt;
&lt;p&gt;简单来说 HTTPS = HTTP + SSL。SSL保证了传输过程中数据不被修改。简单可以理解成在 HTTPS 通讯的时候，会生成 A、B 两个钥匙和一个箱子。A 上锁只能 B 解锁；B 上锁只能 A 解锁。Client 和 Server 通过某种方法（Diffie-Hellman）协商生成钥匙 A 和 B，一人拿一把，那么我们把 HTTP 报文放入这个箱子中就可以了。在这同时把报文放入箱子的过程（RSA 加密）就保证了数据不可被查看。&lt;/p&gt;
&lt;p&gt;注：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTTPS 无法加密 访问网站域名和IP 内容。新版提案在促进这个过程（感谢&lt;a href=&quot;https://www.v2ex.com/member/hxsf&quot;&gt;hxsf&lt;/a&gt;的纠正）&lt;/li&gt;
&lt;li&gt;目前 HTTPS 和 HTTP 2.0 是相辅相成的，所有请理性区分哪个功能属于HTTPS，哪个属于 HTTP 2.0&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;当前后端分离&lt;/h2&gt;
&lt;p&gt;当前 Web 开发逐渐趋向各司其职，前端忙前端的，后端忙后端的，同时随着前端框架的流行，前后端分离的开发流程和注意事项可以算是每位 Web 开发的必须掌握的技巧。&lt;/p&gt;
&lt;p&gt;那么前后端分离开发相比于传统开发需要注意的是什么。&lt;/p&gt;
&lt;p&gt;HTTP 是一个无状态的连接过程，每一次访问对于 Server 来说都是新用户，那么需要一些技术来让 Server 知道这次连接过来的是谁。从而产生了 Session 和 Cookies 技术。其中 Cookies 是 HTTP 协议中规定的，而 Session 是基于 Cookies 衍生出来的技术，在接下来两节中会详细讲解这两种技术。&lt;/p&gt;
&lt;p&gt;可是在前后端分离的开发环境中，前端是使用 socket (通常是&lt;code&gt;axios&lt;/code&gt;)来创建一次 HTTP 连接的，不是浏览器创建的，所以 Cookies 没法有效储存，导致了 Cookies 和 Session 无法使用。所以我们会采用手动的形式来模拟浏览器处理 Cookies 的流程来创造一套属于自己的 HTTP 用户认证方式，比较成气候的有：JWT、TOKEN。&lt;/p&gt;
&lt;p&gt;接下来将一一讲述这几种技术的实现过程和认证过程。&lt;/p&gt;
&lt;p&gt;为了更加生动，我会把 Server 比喻成商店（包括商店工作人员）；Client 比喻成购物者。不同的网站（不同的域名）对应成不同的商店；不同的访问者对应不同的购物者。&lt;/p&gt;
&lt;h2&gt;COOKIES&lt;/h2&gt;
&lt;p&gt;Cookies 是 HTTP 协议中自带的内容，所以浏览器对其的支持都是很好的。同时 Cookies 是对用户透明的，用户不用做任何操作，浏览器会根据 HTTP 报文来做处理。&lt;/p&gt;
&lt;p&gt;Cookies 说白了就是一个简单的KV（key-value）的数组，具体流程是这样的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Client 访问 Server 时会自动带上 Cookies，具体实现是在 HTTP 头部的 &lt;code&gt;Cookies&lt;/code&gt; 中传递过去。&lt;/li&gt;
&lt;li&gt;Server 可以在 Client 中写入 Cookies，具体是在 HTTP 响应报文头部中&lt;code&gt;Set-Cookie&lt;/code&gt; 来告诉浏览器需要在 Client 中记录这些信息，以后访问的时候顺便带过来。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cookies 实际上就是这么简单。如果生动点讲：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;购物者来到了 A 商店，买了一瓶水。商店收银员在他身上做了一个标记，记下了他买了一瓶水（&lt;code&gt;Set-Cookie: bought=1-water&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;当购物者再来商店的时候，因为他身上是有标记的，所以商店收银员一眼就看到了（&lt;code&gt;Cookies: bought=1-water&lt;/code&gt;），就告诉购物者「你上次买了一瓶水，这次也一样吗」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这便是 Cookies 无需用户参与就能做到的信息传递，全部由浏览器实现。&lt;/p&gt;
&lt;h3&gt;当前后端分离&lt;/h3&gt;
&lt;p&gt;正是因为 Cookies 是在 Client 访问 Server 时，由浏览器自动添加的，并且前端采用&lt;code&gt;axios&lt;/code&gt;来构建一个 HTTP 连接时，不会自动加上 Cookies，所以 Cookies 在前后端分离中并不能使用。&lt;/p&gt;
&lt;p&gt;当然也有比较坎坷的解决方案，前端在调用完登陆 API 后，把 Cookies 记录下来，在接下来每次 API 调用过程中加上这些 Cookies。&lt;strong&gt;但是不推荐这么做，除非你是做爬虫&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;SESSION&lt;/h2&gt;
&lt;p&gt;Session 是在 Cookies 发展中为了解决 Cookies 某些弊端而产生的技术，但是本质上还是 Cookies 的使用。&lt;/p&gt;
&lt;p&gt;设想下，正如上述例子中，我们使用 Cookies 来记录购物者购买过的商品，如果商品类别很多很多，Cookies 便会很大很大，导致 HTTP 协议或者浏览器无法正常处理 Cookies（&lt;strong&gt;每个域名建议不超过50条 Cookie 记录，总体积不超过 4093 bytes&lt;/strong&gt;）&lt;/p&gt;
&lt;p&gt;那么如果我真的有「让用户携带很多信息」的需求呢？Session 因此诞生，其解决方法是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Server 为每个用户打上一个标签（Session-id），然后&lt;strong&gt;把本来需要存在 Cookies 中的消息存在 Server 中，不记录在 Cookies 中&lt;/strong&gt;，现在就只把这个标签记录在 Cookies 中。&lt;/li&gt;
&lt;li&gt;当 Client 再次访问时，因为 Cookies 自动携带的原因，Server 知道了这个用户的标签（Session-id），然后查询本地记录，找出该用户对应的信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用回购物者的例子来讲：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;购物者来 A 商店买了一瓶水，收银员在自己的本子上记下了「商店第 666 号用户（Session-id）购买了一瓶水」，然后在购买者上打下了「这位是 666 号用户」（&lt;code&gt;Set-Cookie: user-id=666&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;当用户再来商店时，收银员查看了他的标签（Cookies）「这位是 666 号用户」（&lt;code&gt;Cookies:user-id=666&lt;/code&gt;）。这时候收银员知道了这是 666 号用户，然后查看自己的本子看看 666 号用户到底买过了什么，然后发现了这条信息「商店第 666 号用户购买了一瓶水」，然后告诉购物者「你上次买了一瓶水，这次也一样吗」&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样无论购物者买了多少东西，我们都只需在购物者身上打下用户标签（Session-id）这个信息。&lt;/p&gt;
&lt;p&gt;如果你了解过 Session，都知道 Session 会在浏览器关闭后失效，这是因为 Cookies 的 &lt;code&gt;Max-Age&lt;/code&gt; 来实现的。HTTP 协议允许 Server 设置 Cookies 的有效时间：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Max-Age&lt;/code&gt; 大于 0 ：有效时间就是这个数值，单位&lt;code&gt;秒&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Max-Age&lt;/code&gt; 等于 0 ：Cookies 马上失效&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Max-Age&lt;/code&gt; 小于 0 ：Cookies 保存在浏览器的内存中。自然浏览器关闭时就失效了，从而实现了 Session 的时效性的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Session 在 Server 中是怎么保存用户信息的，可以有多种方法，常见的有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;In-Memory&lt;/code&gt; 即直接存在程序中，方便，但是不适合于多实例的 Web 程序或者分布式 Web 程序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Redis-based&lt;/code&gt; 即保存信息在 Redis 中，解决了 IM 储存方式不适用于分布式的缺点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;当前后端分离&lt;/h3&gt;
&lt;p&gt;Session 是依赖 Cookies 存在的，所以在前后端分离中自然不能使用。&lt;/p&gt;
&lt;h2&gt;TOKEN&lt;/h2&gt;
&lt;p&gt;上述在 Cookies 部分讲到在前后端分离是如何让 Cookies 生效，其实 Token 就是复现这一个过程。&lt;/p&gt;
&lt;p&gt;Token 是一个很泛的讲法，业内称票据（我也不是很清楚中文怎么称呼它），它可以是 Session-id，也可以是一串随机的字符。但是它的作用跟Session-id 一样都是为了识别不同的访问者，同时验证它的有效性。&lt;/p&gt;
&lt;p&gt;Token 是一个很广泛的说法，是因为它只提供了一个验证的准则，而不是一个具体的做法，所以 JWT、OAUTH 都可以算是 Token 的一种实现。&lt;/p&gt;
&lt;p&gt;在我看来，Token的过程就是这么一回事：
Client —&amp;gt; Server: 嘿，我是那个谁啊
Server —&amp;gt; Client: 知道了，你记着你的票号 123456，半个小时内有有效&lt;/p&gt;
&lt;p&gt;// 几分钟后
Client —&amp;gt; Server: 嘿，我的票号是 123456，帮我买瓶水，钱从账户扣
Server —&amp;gt; Client: （嗯，123456 在有效期内）好嘞&lt;/p&gt;
&lt;p&gt;Client —&amp;gt; Server: （我试下用别人的账号买东西）嘿，我的票号是 666666，帮我买瓶水，钱从账户扣
Server —&amp;gt; Client: （嗯？我们根本就没有 666666 这个票号的记录啊）兄弟，你是来搞事的吧&lt;/p&gt;
&lt;p&gt;// 半个小时后
Client —&amp;gt; Server: 嘿，我的票号是 123456，帮我买瓶水，钱从账户扣
Server —&amp;gt; Client: （em，好像过时了）诶诶诶，你的票号过期了，不办理了&lt;/p&gt;
&lt;p&gt;OK，我们再回看一下整个 Token 的使用过程。&lt;/p&gt;
&lt;p&gt;「Server —&amp;gt; Client: 知道了，你记着你的票号 123456，半个小时内有有效」 想比于 Cookies 和 Session 的自动处理，Token 这里明确告诉了用户他的票号和有效期（当然，有效期不是必须的），这是一个需要用户参与的过程。也就是说 Server 必须通过某种方式通知 Client，而不是像 Cookies 和 Session 那样使用 &lt;code&gt;Set-Cookie&lt;/code&gt;就可以完成的。比较常用的方法有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过 HTTP RESPONSE 报文中 Headers 返回 &lt;code&gt;Token:123456&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过 HTTP RESPONSE 报文中Content 返回&lt;code&gt;{&apos;status&apos;:&apos;OK&apos;, &apos;data&apos;:{&apos;token&apos;:&apos;123456&apos;}}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用 Header 携带消息的方法使用较少，普遍都是使用 Content 来携带消息，因为通常情况下，需要返回的信息不只是 Token 本身，还有相关的附加信息：&lt;code&gt;Expire-time&lt;/code&gt; 等等&lt;/p&gt;
&lt;p&gt;「Client —&amp;gt; Server: 嘿，我的票号是 123456，帮我买瓶水，钱从账户扣」在 Client 进行后续访问时，需要手动带上 Token 以确认自己的身份（因为现在没有 Cookies 和 Session 可使用了），那么如何携带这些信息呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过 HTTP REQUEST 报文中 Header 携带 &lt;code&gt;Token:123456&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过 HTTP URL 携带：&lt;code&gt;http://foo.com/buy?good=water&amp;amp;number=1&amp;amp;token=123456&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过 HTTP FORM 携带：这个方法只能出现在非 GET 请求下。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上述讲的三种方法，最常见的是前两种，第三种少使用的原因是需要为 GET 设计另外一种 Token 携带方案，而前两种适合于所有 HTTP 访问&lt;/p&gt;
&lt;p&gt;实际上，Token 可以理解为人工干预生成的 Session-id，它不会把数据发送给 Client，只发送一个「一群数据对应的 ID」。这个想法跟 Session 是一样的，区别于 Session 的是 Token 使用得更加广泛。如果把Token 用 Cookies 来传递，就是另类的 Session，适用于传统的非前后端分离开发；如果把Token 利用 Header 或 Content 传递，便适合前后端分离开发。&lt;/p&gt;
&lt;p&gt;Token 的具体实现有很多，完全可以自己设计一套出来，只要符合团队需求即可。&lt;/p&gt;
&lt;h2&gt;JWT&lt;/h2&gt;
&lt;p&gt;既然有了 Token，为啥还要单独拿出 JWT 来讲呢？简单来说，JWT 是 Token 的具体且应用广泛的实现。&lt;/p&gt;
&lt;p&gt;上文讲到 Token 可以理解成为用户自己实现的 Session，那么 JWT 在某种程度上可以理解为自己实现的 Cookies，原因如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JWT 需要 Server 手动传递给用户，用户需要手动携带，所以还是归属于 Token 范畴&lt;/li&gt;
&lt;li&gt;JWT 选择把所有信息放在用户端，所以这属于 Cookies 的处理手法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在之前的内容里面，我们都没有涉及到数据安全这个问题。因为 JWT 包含了部分内容，所以集中在这里讲。当我们把数据放在 Cookies 时，我们要考虑一个问题：这些数据放在用户端，被人看到，被人拿去会不会产生什么问题。&lt;/p&gt;
&lt;p&gt;首先，被人看到，即数据机密性问题。如果这些数据包含了用户隐私或者商业机密的时候，被人看到必然不可，Session 类的处理手法就解决了这个问题，保存在用户端的内容只是一个随机的字符串，其与真实数据的映射关系只有 Server 知道，所以解决了机密性的问题。&lt;/p&gt;
&lt;p&gt;其次，被人拿去，即用户伪造的问题。在上述 Token 例子中，Server 识别 Client 的唯一方式就是 Client 携带过来的票号。如果 Server 生成票号的方法被识破（举例来讲只是使用用户 ID 作为票号），那么入侵者就可以轻松的模拟任何用户进行网站访问。再假设 A 用户的票号被入侵者获取了，那么入侵者就可以使用这个票号来模拟 A 用户访问。目前没有比较好的方法解决「Token 被盗取从而模拟其访问」的方案。&lt;/p&gt;
&lt;p&gt;但是我们可以尽量地提升被攻击的成本：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用随机字符串来作为 Token&lt;/li&gt;
&lt;li&gt;如果使用 Cookies 类在 Client 存储数据的方式，请建立 Token-Data 的映射方式，只在 Client 处存储 Token&lt;/li&gt;
&lt;li&gt;建立 Token 的有效性机制。如「Session 的仅当前浏览器上下文有效」和「两次访问在30分钟内有效」等等。这样即便入侵者拿到某用户的 Token，也只能在一定时间内进行入侵行为。&lt;/li&gt;
&lt;li&gt;建立如 JWT 类似的 Token 有效性机制。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JWT 的有效性包含了两点：时效性，有效性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;时效性指 JWT 提供了 Token 的过期时间。&lt;/li&gt;
&lt;li&gt;有效性指 JWT 可以验证当前的 Token 是否被修改。JWT 的组成是这样的：&lt;code&gt;Header||Payload||Signature&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Header&lt;/code&gt; 标明了这是一个 JWT，而且使用了什么 Hash 算法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Payload&lt;/code&gt; 储存了你保存在用户数据和 JWT 自身的一些必要信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Signature&lt;/code&gt; 是 Server 对 Header 和 Payload 的 HMAC 结果&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么 JWT 可以验证 Token 是否有效呢？如果Payload 被伪造了，那么对应的 Signature 必然会变。然而 Signature 的生成需要一个只有 Server 知道的 Secret。因此 JWT 不可被伪造。&lt;/p&gt;
&lt;p&gt;当然了，JWT 也不是没有弊端，因为 JWT 是把数据完全储存在 Client 端，而且当一个 JWT Token 生成时便无法修改。JWT 自身有一个过期时间，那么我们如何让一个 JWT Token 提前过期呢？为此我们要维护一个 JWT Token 的黑名单来限制这些 JWT 的访问。或者 JWT Payload 部分也是采用 Token-Data 的映射关系，只在 JWT 中储存一个 Token。&lt;/p&gt;
&lt;h2&gt;其他&lt;/h2&gt;
&lt;p&gt;Token 的范围很广，而且通过 Token 的流程，用户可以设计出很多相似或者更加安全、更加适合于多方使用的方案。上述讲解的都是 Server-Client 双方方案。如果是三方或以上的话，就需要自行协商方案，例子有 Oauth 2.0 和 SSO。当然了这些都不在这篇文章的介绍范围内，因为可以根据自身项目情况对已有方案进行改造从而满足自身需求。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Session 和 Cookies 更加适合传统的非前后端分离式开发。&lt;/p&gt;
&lt;p&gt;JWT 对于所有开发都比较适合。&lt;/p&gt;
&lt;p&gt;Token 需要自行协商数据格式，通过选择对应的数据载体也适合于所有开发情况。若选择 Cookies 来装载 Token 适合传统开发。
&lt;strong&gt;在选择任何一种认证方式，请注意数据的安全性&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>2017 个人总结</title><link>https://www.kilerd.me/summaries-my-2017/</link><guid isPermaLink="true">https://www.kilerd.me/summaries-my-2017/</guid><description>鲁迅说过：&amp;quot;Sometimes you need to leave things behind to move forward&amp;quot; 这一年下来，整个过程就是这样的情况。放弃了一些东西，努力了一些东西。</description><pubDate>Fri, 29 Dec 2017 06:39:44 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;鲁迅说过：&amp;quot;Sometimes you need to leave things behind to move forward&amp;quot;&lt;/p&gt;
&lt;p&gt;这一年下来，整个过程就是这样的情况。放弃了一些东西，努力了一些东西。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;我选择了怎样的编程语言&lt;/h2&gt;
&lt;p&gt;在17年以前，我是一个完完全全的 NO-JS 开发者，在写网站的时候，都是使用后端渲染和纯CSS写界面。可是 NodeJS 这些年的快速发展和用户 UI 优化的迫切需求，让我不得不开始系统地学习 JavaScript。&lt;/p&gt;
&lt;p&gt;在同学的介绍下，我正式系统的学习ES6、ES7语法，这让我感受到了 JS 新语法与 Python 的相似性。在抛弃了 jQuery 和原生语法之后，进度一帆风顺。而且在 VueJS 和 React 中，我选择了 React，因为这符合了我喜欢 Micro Framework 的习惯，并且 React + Redux 的逻辑个人觉得会比 Vuejs 来得更加直接和清晰。&lt;/p&gt;
&lt;p&gt;都 8102 年了，应该使用 Python3 了，所以我写的库基本上都是不支持 Python2 的。Py3 的最大成就可以算得上异步了，async/await 的语法糖支持和 asyncio 成为标准库让我最为兴奋。同时配合 mypy 做静态类型检查，Python 就有了基本的编写大型软件的能力，配合 uvloop 做异步性能优化，起码在 web 领域可以不输于 Nodejs。&lt;/p&gt;
&lt;p&gt;Python 的 mypy 支持和 JavaScript 的 TypeScript 方言都让这两门语言有了类型检查的能力，但是我还是破解地渴望学习一门编译型语言。Rust 和 Golang 都是在我的选择范围之内。Rust 是我最最喜欢的一门语言，奈何其学习坡度太陡，所以几度学习都不能成功坚持下去。Golang 虽说很简单，可是个人感觉有些地方用起来还是不太舒服，例如 tab 缩进，无泛型，没有好用的包管理等等。今后估计还是会选择继续学习 Rust。&lt;/p&gt;
&lt;p&gt;所以一年写来技术栈就成了这样的布局：Rust 高性能任务 + Python 做 API 服务 + React Redux 做 UI。&lt;/p&gt;
&lt;h2&gt;我做了哪些项目&lt;/h2&gt;
&lt;p&gt;Python3 中没有一个异步框架使用得比较舒服，所以就自己做了一个了 &lt;a href=&quot;https://github.com/NougatWeb/nougat&quot;&gt;Nougat&lt;/a&gt;。这个框架也是命途多舛，几度设计，几度重构，到现在只留下了一个纯粹的基于中间件的框架和一个路由器。但是使用起来却异常舒服。现在框架基本定型了，差的就是文档更新和自动重载的开发模式。&lt;/p&gt;
&lt;p&gt;在学习爬虫的过程中，自己折腾了一个很小的爬虫框架 &lt;a href=&quot;https://github.com/Kilerd/gear&quot;&gt;Gear&lt;/a&gt;，现在才刚刚起步，代码也还不完善，可是架构却异常地清晰。&lt;/p&gt;
&lt;p&gt;一个很简单的网页文本翻译器 &lt;a href=&quot;https://github.com/Kilerd/coconut&quot;&gt;Coconut&lt;/a&gt;。这是一个简单的 Chrome 插件。它在纯文本的表现力上还算不错。&lt;/p&gt;
&lt;p&gt;一个简单的 Github Star 检索器 &lt;a href=&quot;https://github.com/Kilerd/star_collector&quot;&gt;Star Collector&lt;/a&gt;。这同样是一个 Chrome 插件，可以很快速地搜索到已经 star 的项目。当 star 数量很多的时候，这样检索会比 Github 自带的舒服很多。&lt;/p&gt;
&lt;p&gt;自从注册了 &lt;a href=&quot;http://bearnote.com&quot;&gt;bearnote.com&lt;/a&gt; 之后，就一直有一个魔咒“编写一个多人笔记 / 博客网站”。Nougat 的出现也是为它服务的，可惜最后还是没能写出来。最后打算用 Nougat 来写一个单人博客，这个任务不会很难，同时可以测试一下 Nougat 的表现力。&lt;/p&gt;
</content:encoded></item><item><title>用 Webpack 和 React 搭建一个适用于 Chrome Extension 的脚手架</title><link>https://www.kilerd.me/starter-of-chrome-extension/</link><guid isPermaLink="true">https://www.kilerd.me/starter-of-chrome-extension/</guid><description>做为一个不称职的前端设计师，对于前端的框架，尤其是各式各样的 JavaScript 框架，我都是习惯使用官方自带的 CLI 工具来搭建脚手架的。因为在混乱的前端世界中， Babel 和 Webpack 的配置不是一般的麻烦。而且我对于前端的学习就是冲着写 Side Project 去的，所以效率对我来多很重要。 我选择</description><pubDate>Tue, 28 Nov 2017 15:19:47 GMT</pubDate><content:encoded>&lt;p&gt;做为一个不称职的前端设计师，对于前端的框架，尤其是各式各样的 JavaScript 框架，我都是习惯使用官方自带的 CLI 工具来搭建脚手架的。因为在混乱的前端世界中，&lt;code&gt;Babel&lt;/code&gt; 和 &lt;code&gt;Webpack&lt;/code&gt; 的配置不是一般的麻烦。而且我对于前端的学习就是冲着写 Side Project 去的，所以效率对我来多很重要。&lt;/p&gt;
&lt;p&gt;我选择的前端框架是 React + Redux，同时也有一个很好用的 CLI 工具来初始化 React 项目：&lt;a href=&quot;https://github.com/facebookincubator/create-react-app&quot;&gt;create-react-app&lt;/a&gt;。对于像我这样的懒人来说，这确实很好用，但同时也有不少缺点。&lt;/p&gt;
&lt;p&gt;我是一个勤勤恳恳的 Python 工程师，所以使用装饰器是我的日常，同时 JavaScript 在 ES7 的 Proposal 中也有类似的装饰器提议，那么使用装饰器肯定是必不可少的了。Create-Reat-App 的最大问题就在于&lt;strong&gt;不支持装饰器&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2&gt;Can I Use Decorators?&lt;/h2&gt;
&lt;p&gt;Many popular libraries use &lt;a href=&quot;https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841&quot;&gt;decorators&lt;/a&gt; in their documentation.&lt;/p&gt;
&lt;p&gt;Create React App doesn’t support decorator syntax at the moment because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It is an experimental proposal and is subject to change.&lt;/li&gt;
&lt;li&gt;The current specification version is not officially supported by Babel.&lt;/li&gt;
&lt;li&gt;If the specification changes, we won’t be able to write a codemod because we don’t use them internally at Facebook.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However in many cases you can rewrite decorator-based code without decorators just as fine.
Please refer to these two threads for reference:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/facebookincubator/create-react-app/issues/214&quot;&gt;#214&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/facebookincubator/create-react-app/issues/411&quot;&gt;#411&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Create React App will add decorator support when the specification advances to a stable stage.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;并且还有一个问题在于 Chrome Extension 并不是一个简单的 SPA，所以就需要我们自己来手动配置一份 &lt;code&gt;webpack.config.js&lt;/code&gt; 了，以下就是我的折腾记录&lt;/p&gt;
&lt;h2&gt;项目结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;├── dist
├── src
│   ├── Components
│   │   └── Container
│   │       ├── index.jsx
│   │       └── style.css
│   ├── background.js
│   ├── content.js
│   ├── icon-128.png
│   ├── manifest.json
│   ├── popup.html
│   └── popup.jsx
├── webpack.config.js
├── yarn.lock
├── package-lock.json
├── package.json
└── readme.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般来说，Chrome Extension 的主要文件如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;manifest.json&lt;/code&gt; Extension 的配置文件，包括了 LOGO，版本号，权限等等&lt;/li&gt;
&lt;li&gt;&lt;code&gt;popup.html&lt;/code&gt; POPUP 页面，也就是说点击 Extension 图标是显示的页面，实际上就是一个普通的HTML页面&lt;/li&gt;
&lt;li&gt;&lt;code&gt;background.js&lt;/code&gt; 在后台运作的 JS 文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;content.js&lt;/code&gt; 注入用户页面的 JS 文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然了，在上面是找不到这几个文件的，我们要做的就是怎么通过上面那一串文件来构建出这几个文件。&lt;/p&gt;
&lt;p&gt;我们先来讲下项目中的文件、文件夹是干什么的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dist&lt;/code&gt; 项目生成文件夹，也就是说我们构建出来的文件也是在这里面的。&lt;strong&gt;当开发的时候，在 Chrome 中也是把这个文件夹加载成 Extension&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src&lt;/code&gt; 项目的源文件
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Components&lt;/code&gt; React 项目中通常用到的组件文件夹，里面的每一个文件夹都是一个组件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;background.js&lt;/code&gt; 用来构建后台运作的 JS 文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;content.js&lt;/code&gt; 用来构建注入用户页面的 JS 文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;icon-128.png&lt;/code&gt; 项目 LOGO&lt;/li&gt;
&lt;li&gt;&lt;code&gt;manifest.json&lt;/code&gt; 项目的 Chrome Extension 配置文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;popup.html&lt;/code&gt; 用来构建 popup 页面。(如果开发过 React，通常都知道这个文件基本不用怎么动，仅作为一个入口文件而已)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;popup.jsx&lt;/code&gt; popup 里面用到的 JS 文件，同时也是 React 的入口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;webpack.config.js&lt;/code&gt; 自己配置的 Webpack&lt;/li&gt;
&lt;li&gt;&lt;code&gt;package.json&lt;/code&gt; NPM 配置文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readme.md&lt;/code&gt; 项目介绍&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当我们执行 &lt;code&gt;webpack -p&lt;/code&gt; 来打包 Production 版本的时候， &lt;code&gt;dist&lt;/code&gt; 文件夹就会生出我们期望的那些文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;├── dist
│   ├── content.bundle.js
│   ├── bundle.css
│   ├── background.bundle.js
│   ├── icon-128.png
│   ├── manifest.json
│   ├── popup.bundle.js
│   └── popup.html
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Webpack 配置&lt;/h2&gt;
&lt;p&gt;OK，那么我们来看看我们从源文件构建出目的文件有哪些步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;manifest.json&lt;/code&gt; 和 &lt;code&gt;icon-128.png&lt;/code&gt; 这种与 JavaScript 无关的文件原封不动的复制过去。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;popup.jsx&lt;/code&gt; 构建出一个完整的 React 项目，名字叫做 &lt;code&gt;popup.bundle.js&lt;/code&gt; ，React 项目中用到的 CSS 构建出 &lt;code&gt;bundle.css&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;把构建出来的 &lt;code&gt;popup.bundle.js&lt;/code&gt; 在 &lt;code&gt;popup.html&lt;/code&gt; 中引用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;content.js&lt;/code&gt; 和 &lt;code&gt;background.js&lt;/code&gt; 分别构建出 &lt;code&gt;content.bundle.js&lt;/code&gt; 和 &lt;code&gt;background.bundle.js&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;webpack.config.js 内容&lt;/h3&gt;
&lt;p&gt;这里给出文件完整内容，之后的内容就是按上述内容逐点讲解。若不感兴趣可以直接把文件内容复制走即可。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const path = require(&apos;path&apos;)
const webpack = require(&apos;webpack&apos;)
const ExtractTextPlugin = require(&apos;extract-text-webpack-plugin&apos;)
const HtmlWebpackPlugin = require(&apos;html-webpack-plugin&apos;)
const CopyWebpackPlugin = require(&apos;copy-webpack-plugin&apos;)

module.exports = {
  entry: {
    popup: &apos;./src/popup.jsx&apos;,
    content: &apos;./src/content.js&apos;,
    background: &apos;./src/background.js&apos;
  },
  output: {
    path: path.resolve(__dirname, &apos;dist&apos;),
    filename: &apos;[name].bundle.js&apos;
  },
  resolve: {
    extensions: [&apos;.js&apos;, &apos;.jsx&apos;]
  },
  module: {
    loaders: [
      // We use Babel to transpile JSX
      {
        test: /\.js[x]$/,
        include: [path.resolve(__dirname, &apos;./src&apos;)],
        exclude: /node_modules/,
        loader: &apos;babel-loader&apos;,
        query: {
          presets: [&apos;react&apos;, &apos;es2015&apos;],
          plugins: [
            &apos;react-hot-loader/babel&apos;
          ]
        }
      }, {
        test: /\.css$/,
        loader: ExtractTextPlugin.extract({ fallback: &apos;style-loader&apos;, use: &apos;css-loader&apos; })
      }, {
        test: /\.(ico|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/,
        use: &apos;file-loader?limit=100000&apos;
      }, {
        test: /\.(jpe?g|png|gif|svg)$/i,
        use: [
          &apos;file-loader?limit=100000&apos;, {
            loader: &apos;img-loader&apos;,
            options: {
              enabled: true,
              optipng: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    // create CSS file with all used styles
    new ExtractTextPlugin(&apos;bundle.css&apos;),
    // create popup.html from template and inject styles and script bundles
    new HtmlWebpackPlugin({
      inject: true,
      chunks: [&apos;popup&apos;],
      filename: &apos;popup.html&apos;,
      template: &apos;./src/popup.html&apos;
    }),
    // copy extension manifest and icons
    new CopyWebpackPlugin([
      {
        from: &apos;./src/manifest.json&apos;
      }, {
        context: &apos;./src&apos;,
        from: &apos;icon-**&apos;
      }
    ])
  ]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1. 复制非 JS 内容&lt;/h3&gt;
&lt;p&gt;这里用到的是 &lt;code&gt;CopyWebpackPlugin&lt;/code&gt; 这个插件&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;new CopyWebpackPlugin([
      {
        from: &apos;./src/manifest.json&apos;
      }, {
        context: &apos;./src&apos;,
        from: &apos;icon-**&apos;
      }
    ])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里做了两件事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;复制 &lt;code&gt;./src/manifest.json&lt;/code&gt; 到文件前面定义的 &lt;code&gt;output.path&lt;/code&gt; 中去，或者也可以指定 &lt;code&gt;to&lt;/code&gt; 属性说明目的路径&lt;/li&gt;
&lt;li&gt;从 &lt;code&gt;./src&lt;/code&gt; 中找到符合 &lt;code&gt;icon-**&lt;/code&gt; 的文件复制过去，这里的目的主要是复制 LOGO&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.构建 React 应用&lt;/h3&gt;
&lt;p&gt;首先我们先把 &lt;code&gt;popup.jsx&lt;/code&gt; 编译成 &lt;code&gt;popup.js&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;{
  test: /\.js[x]$/,
  include: [path.resolve(__dirname, &apos;./src&apos;)],
  exclude: /node_modules/,
  loader: &apos;babel-loader&apos;,
  query: {
    presets: [&apos;react&apos;, &apos;es2015&apos;],
    plugins: [
      &apos;react-hot-loader/babel&apos;
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里是 &lt;code&gt;module.loaders&lt;/code&gt; 里面关于 JS 和 JSX 的配置信息，有几点需要注意的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;loader&lt;/code&gt; 用的是 &lt;code&gt;babel&lt;/code&gt; ，目的是把我们用 ES6 或者 ES7 写的 React 相关代码进行转码&lt;/li&gt;
&lt;li&gt;&lt;code&gt;presets&lt;/code&gt; 指的是目的代码，首先指明了需要转义 &lt;code&gt;react&lt;/code&gt; ，其次要把所有代码翻译到 ES2015&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;reserve jsx file&lt;/h4&gt;
&lt;p&gt;在这里我碰到了一个坑，就是&lt;strong&gt;Webpack 不会 reverse JSX 文件&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;例如，你在一个 JS 文件中引用 &lt;code&gt;xx.jsx&lt;/code&gt; 文件 ： &lt;code&gt;import xxx from &apos;./xxx&apos;&lt;/code&gt; 。 这样的写法 Webpack 是不认的，一定要写成 &lt;code&gt;import xxx from &apos;./xxx.jsx&apos;&lt;/code&gt; 。这样十分不优雅。&lt;/p&gt;
&lt;p&gt;默认的情况下，它只会去找 &lt;code&gt;xxx&lt;/code&gt; 文件夹、&lt;code&gt;xxx.js&lt;/code&gt; 、&lt;code&gt;xxx.json&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;所以我们要在 Webpack 中指明需要 reverse &lt;code&gt;.jsx&lt;/code&gt; 这种文件类型：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;resolve: {
  extensions: [&apos;.js&apos;, &apos;.jsx&apos;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.把 CSS 和 popup.bundle.js 在 popup.html 中引用&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-Js&quot;&gt;// create CSS file with all used styles
new ExtractTextPlugin(&apos;bundle.css&apos;),
// create popup.html from template and inject styles and script bundles
new HtmlWebpackPlugin({
  inject: true,
  chunks: [&apos;popup&apos;],
  filename: &apos;popup.html&apos;,
  template: &apos;./src/popup.html&apos;
}),
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;第一个插件指的是把React 中用到的 CSS 抽出来，构建 &lt;code&gt;bundle.css&lt;/code&gt; 文件&lt;/li&gt;
&lt;li&gt;第二个插件就是把&lt;code&gt;bundle.css&lt;/code&gt; 和 &lt;code&gt;popup.js&lt;/code&gt; 写到 &lt;code&gt;popup.html&lt;/code&gt; 中&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.构建 content.bundle.js 和 background.bundle.js&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;entry: {
  popup: &apos;./src/popup.jsx&apos;,
  content: &apos;./src/content.js&apos;,
  background: &apos;./src/background.js&apos;
},
output: {
  path: path.resolve(__dirname, &apos;dist&apos;),
  filename: &apos;[name].bundle.js&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上这个部分就是告诉了 Webpack 几件事情：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有三个文件要构建&lt;/li&gt;
&lt;li&gt;构建完了之后就根据 &lt;code&gt;output&lt;/code&gt; 的配置输出&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里有一点需要注意的是：在 &lt;code&gt;output.filename&lt;/code&gt; 中的 &lt;code&gt;[name]&lt;/code&gt; 指的是 &lt;code&gt;entry&lt;/code&gt; 中的 KEY&lt;/p&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;entry: {
  foo: &apos;./src/bar.jsx&apos;
},
output: {
  path: path.resolve(__dirname, &apos;dist&apos;),
  filename: &apos;[name].bundle.js&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这代码值的就是用 &lt;code&gt;./src/bar.jsx&lt;/code&gt; 构建出 &lt;code&gt;./dist/foo.bundle.js&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;如何工作&lt;/h2&gt;
&lt;p&gt;开启 Watch 模式自动监控文件改变，并且 Reload 项目&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;webpack --watch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这个 Reload 只适用于 popup.html 相关文件的改变，content.bundle.js 和 background.bundle.js 需要在 Chrome 中 Reload 项目&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;写在最后&lt;/h2&gt;
&lt;p&gt;这样的一个脚手架可以给 Chrome Extension 的开发者一个比较好的开发环境。在此之前我一直都是写原生的 JavaScript 代码，导致开发体验十分差。&lt;/p&gt;
&lt;p&gt;我也是一个 JavaScript 新手，如有差错，请见谅。&lt;/p&gt;
</content:encoded></item><item><title>在 HongKong 的那些天那些事</title><link>https://www.kilerd.me/those-days-in-hk/</link><guid isPermaLink="true">https://www.kilerd.me/those-days-in-hk/</guid><description>终于忙完了，有了闲空可以写一写那些天在 HongKong 遇到的一些人，一些事。</description><pubDate>Sun, 27 Aug 2017 07:29:01 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;终于忙完了，有了闲空可以写一写那些天在 HongKong 遇到的一些人，一些事。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;很 nice 的房东&lt;/h2&gt;
&lt;p&gt;这次去 HongKong 的目的是为了考试。可是具体的考试地点在考试前三天才公布出来，所以定酒店就是一个晚上的事情。&lt;/p&gt;
&lt;p&gt;本来是想直接 Airbnb 走起的，因为之前的出行都是我的小伙伴负责酒店事宜的，所以 Airbnb 一直都没有注册。嗯，主要是因为卡在了拍照识别真人的步骤上。&lt;/p&gt;
&lt;p&gt;后来就选择了在同程上订，“按价格从低到高排序”。HongKong 那土地就是黄金的现状和钱包的事实，我选择了一家叫“苹果酒店”的宾馆。（暂且然我称他为宾馆）&lt;/p&gt;
&lt;p&gt;这家宾馆是在重庆大厦的，一开始我并没有记住这个地方。在去的前一晚看了一电影《同门》，最后的厮杀就是在重庆里面发生的，当晚我做最后的检查的时候，发现了我的酒店刚好就是在这大厦里面的。&lt;/p&gt;
&lt;p&gt;好吧，与其说这是酒店、或者说宾馆，还不如说这才是彻彻底底的 Airbnb 民宅。&lt;/p&gt;
&lt;p&gt;首先，重庆大厦在 HongKong 的地位大概可以说就是小北在广州的地位：“东南亚和黑人的聚居地”。随处可见的黑人（实际上更多的是亚非混合人种）。&lt;/p&gt;
&lt;p&gt;因此毫无疑问，我的房东也在这列。&lt;/p&gt;
&lt;p&gt;她是一个菲律宾人，看起来大概有50多岁的样子了。我也是在住了几天跟她闲聊的时候才得知她是菲律宾人，并且在 HongKong 已经待了 30 年了。&lt;/p&gt;
&lt;p&gt;每次外出回来，她都会很热情的问我去哪里了；购物回来，她也很好奇地上来八卦我买了啥。&lt;/p&gt;
&lt;p&gt;她说她很少外出。&lt;/p&gt;
&lt;p&gt;我问她说，HongKong 和她家乡哪里好。她说菲律宾有很多的土地，但是生活很艰难；HongKong 虽然有点挤，但是在这里生活得不错。&lt;/p&gt;
&lt;p&gt;没错，是真的挤。我估计重庆大厦绝对是 HongKong 人口密度最高的一个地方。我订的是双人房，可是整个房间也就真的是有一张床和一张可有可无的桌子。&lt;/p&gt;
&lt;p&gt;房间里的灯光很暗。由于考试的前一晚需要复习看一些书，所以我就问她要一个台灯，并且抱怨说房间的灯太暗了。&lt;/p&gt;
&lt;p&gt;奈何她的英文水平是真的烂，同时还夹杂着一股操淡的口音，简直很难听懂。于是我用 Google translate 完成了这次愉快的交流（也是这个时候我知道了她是菲律宾人，翻译成菲律宾语给她看我的需求）&lt;/p&gt;
&lt;p&gt;&amp;quot;Thomas is a good boy&amp;quot;, &amp;quot;Thomas is the best&amp;quot; 这两句英文估计是她跟她的其他租客和我讲的最多的两句了，当然其他都是用菲律宾语讲的，我也不知道她在讲啥。&lt;/p&gt;
&lt;h2&gt;操淡的考试&lt;/h2&gt;
&lt;p&gt;总之，一塌糊涂，准备可以重来的那种程度了。&lt;/p&gt;
&lt;h2&gt;English VS Cantonese&lt;/h2&gt;
&lt;p&gt;本来的期待就是能在 HongKong 多讲粤语的，但是事实上英文的使用更加广，无论实在问路还是在买东西，更多的时候还是会选择用英文。嗯，当那个店家不会英文的时候，还是会用回粤语。&lt;/p&gt;
&lt;p&gt;我发现在 HongKong 这样一个忙碌的城市，一个简单的小动作都可以收到能让你开心一整天的微笑。&lt;/p&gt;
&lt;p&gt;因为 M 记在 HongKong 超级便宜，所以一般的时候我都会选择进 M 记叫一个最便宜的套餐，休息一下，享受一下空调。然后这里发生了很多很多很有意思的事情，同时也可以让你愉悦一整天。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拿完餐，找到位置坐下后，发现没有拿吸管，于是就去前台拿，顺手拿了一根给隔壁那个双手拿满东西不方便自己拿的人。那句 “Thanks” 和我们互相的微笑简直不要太好。&lt;/li&gt;
&lt;li&gt;隔壁坐着的小姐姐估计因为是在生理期，问服务员要了一杯温水，然后不小心打到了。我以单身多年的手速，抽出我的纸巾，把水拦在桌子上，没弄湿我和小姐姐的衣服。&lt;/li&gt;
&lt;li&gt;跟找不到连着座位的俩小伙伴换位置。虽然他们两是东南亚人，听不懂他们说啥，不过肯定他们是讲开心的事情&lt;/li&gt;
&lt;li&gt;把用不上的番茄酱给对面吃薯条的熊孩子&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;闹翻的小姐姐 和 AJ 女鞋&lt;/h2&gt;
&lt;p&gt;对于一个男生来说，来到 HongKong 怎么能不去波鞋街逛呢。&lt;/p&gt;
&lt;p&gt;然后发现也物色了好几双 AJ，可是问了店员之后都说是女鞋，那就没办法了啊。&lt;/p&gt;
&lt;p&gt;不过这个时候，刚好是我叫小姐姐帮我在日本问问 Kindle 的价格的，就想着帮她买一双回去，反正她也是习惯穿 AJ 的。&lt;/p&gt;
&lt;p&gt;唉，可惜还是闹翻了，没买成。不过她不是也没帮我买到 Kindle 嘛，打和了。&lt;/p&gt;
&lt;p&gt;不过嘛，一开始我真的是想买来送她的。（摊手，你不信我也没办法&lt;/p&gt;
&lt;h2&gt;维多利亚港 和 表演的小姐姐们&lt;/h2&gt;
&lt;p&gt;一天夜晚闲着无聊就去了港边，想着吹吹海风，放松一下考试失利的不爽和抑郁。谁知道刚好有在上演回归 20 周年的灯光表演。&lt;/p&gt;
&lt;p&gt;同时有一队小姐姐们在旁边跳舞唱歌，应该算是那种业余了来玩玩的团队。围观了一下她们的 INS，感觉上也有了不少人气。&lt;/p&gt;
&lt;p&gt;但是从他们的言语中，并不能得知她们具体是哪里的人，有少数几个能讲流利的粤语，可是有几个说着好像是东南亚的语种，鬼知道她们之间是怎么沟通的。&lt;/p&gt;
&lt;p&gt;后来，回到大陆之后，通过 INS 上的记录，才知道原来那天也刚好是她们 Team 成立的 2 周年。&lt;/p&gt;
&lt;p&gt;实话说，维多利亚港也就那样，可能是我太穷，没有坐油轮去逛一逛。&lt;/p&gt;
&lt;p&gt;坐在港边吹海风的时候，碰到来自梅州的老师，带着几个学生坐在我旁边，老师跟学生讲 HongKong 的历史，刚好我也乘机了解了一波。&lt;/p&gt;
</content:encoded></item><item><title>关于 HTTP OPTIONS</title><link>https://www.kilerd.me/http-options/</link><guid isPermaLink="true">https://www.kilerd.me/http-options/</guid><description>本文参考了两篇文章： OPTIONS - HTTP | MDN 跨域资源共享 CORS 详解 - 阮一峰的网络日志 首先 OPTIONS 方法不应该像 GET, POST, PUT 等方法一样返回内容，它返回的应该就只有Header。 OPTIONS 的功能基本上只有两个： 在普通访问中，它会返回同 URL 中允许访问</description><pubDate>Wed, 21 Jun 2017 15:57:53 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文参考了两篇文章：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS&quot;&gt;OPTIONS - HTTP | MDN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://www.ruanyifeng.com/blog/2016/04/cors.html&quot;&gt;跨域资源共享 CORS 详解 - 阮一峰的网络日志&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;首先 OPTIONS 方法不应该像 GET, POST, PUT 等方法一样返回内容，它返回的应该就只有Header。&lt;/p&gt;
&lt;p&gt;OPTIONS 的功能基本上只有两个：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在普通访问中，它会返回同 URL 中允许访问的 METHODS&lt;/li&gt;
&lt;li&gt;在跨域访问 (CORS) 中，返回对应的原站 (Origin) 能访问的METHODS&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;普通访问&lt;/h2&gt;
&lt;p&gt;在普通访问时：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -X OPTIONS http://example.org -i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回内容中，就应该包含&lt;code&gt;allow&lt;/code&gt;  这个响应头来告知访问者，哪些 METHOD 是已经实现了的(可以访问的)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;HTTP/1.1 200 OK
Allow: OPTIONS, GET, HEAD, POST
Cache-Control: max-age=604800
Date: Thu, 13 Oct 2016 11:45:00 GMT
Expires: Thu, 20 Oct 2016 11:45:00 GMT
Server: EOS (lax004/2813)
x-ec-custom-error: 1
Content-Length: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这份响应中的 &lt;code&gt;Allow&lt;/code&gt; 就包含了&lt;code&gt;OPTIONS&lt;/code&gt;, &lt;code&gt;GET&lt;/code&gt;, &lt;code&gt;HEAD&lt;/code&gt;, &lt;code&gt;POST&lt;/code&gt; 四种方法。&lt;/p&gt;
&lt;h2&gt;CORS&lt;/h2&gt;
&lt;p&gt;在非简单请求的情况下，先会把需要请求的方法放在&lt;code&gt;request header&lt;/code&gt; 中，发送一个&lt;code&gt;OPTIONS&lt;/code&gt;方法，检测目的方法是否允许访问。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;OPTIONS /resources/post-here/ HTTP/1.1 
Host: bar.other 
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 
Accept-Language: en-us,en;q=0.5 
Accept-Encoding: gzip,deflate 
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 
Connection: keep-alive 
Origin: http://foo.example 
Access-Control-Request-Method: POST 
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述例子，在访问&lt;code&gt;POST http://foo.example/resources/post-here &lt;/code&gt; 之前， 先回发出一个对应的&lt;code&gt;OPTIONS&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT 
Server: Apache/2.0.61 (Unix) 
Access-Control-Allow-Origin: http://foo.example 
Access-Control-Allow-Methods: POST, GET, OPTIONS 
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type 
Access-Control-Max-Age: 86400 
Vary: Accept-Encoding, Origin 
Content-Encoding: gzip 
Content-Length: 0 
Keep-Alive: timeout=2, max=100 
Connection: Keep-Alive 
Content-Type: text/plain
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其响应报文中有几个参数是值得关注的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 这个字段描述了哪些网址可以调用这个API的内容。 如果是都允许，就应该返回&lt;code&gt;*&lt;/code&gt; ，反之，应该直接放回对应的域名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt; 允许访问的METHOD&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Allow-Headers&lt;/code&gt; 在访问时允许添加的头部信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Access-Control-Max-Age&lt;/code&gt; 用来标识这次&lt;code&gt;OPTIONS&lt;/code&gt; 的信息有效时间。如果在有效期内，那么不需要再重复发送 &lt;code&gt;OPTIONS&lt;/code&gt; 请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;看看 Flask 是怎么处理的&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import flask
app = flask.Flask(__name__)

@app.route(&amp;quot;/&amp;quot;)
def index():
    return &amp;quot;hello world&amp;quot;

@app.route(&amp;quot;/&amp;quot;, methods=[&amp;quot;GET&amp;quot;, &amp;quot;POST&amp;quot;])
def allow_post_method():
    return &amp;quot;post is allowed here&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问&lt;code&gt;OPTIONS /&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;HTTP/1.1 200 OK
Allow: OPTIONS, GET, HEAD
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Wed, 21 Jun 2017 15:14:58 GMT
Server: Werkzeug/0.12.2 Python/3.6.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问&lt;code&gt;OPTIONS /all_post_method&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;HTTP/1.1 200 OK
Allow: POST, GET, OPTIONS, HEAD
Content-Length: 0
Content-Type: text/html; charset=utf-8
Date: Wed, 21 Jun 2017 15:38:35 GMT
Server: Werkzeug/0.12.2 Python/3.6.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是说，一般的框架都会自动帮你实现 &lt;code&gt;HEAD&lt;/code&gt; 和 &lt;code&gt;OPTIONS&lt;/code&gt;  方法。&lt;/p&gt;
&lt;h2&gt;Nougat 该怎么办&lt;/h2&gt;
&lt;p&gt;现在Nougat 是需要自己定义&lt;code&gt;HEAD&lt;/code&gt; 和 &lt;code&gt;OPTIONS&lt;/code&gt; 方法的。那么说来，这两个方法需要自动实现。&lt;/p&gt;
&lt;p&gt;那么对于 &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 这个处理方法的话。&lt;/p&gt;
&lt;p&gt;我大概会这样设计&lt;/p&gt;
&lt;p&gt;对于一个 Section 来说，可以在整个模块的层面上，允许所有 Section 内的 API 都允许这个域来访问，因此这样设计会比较妥当：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;api = Section(&amp;quot;api&amp;quot;)
app.allows([&amp;quot;example.com&amp;quot;, &amp;quot;a.com&amp;quot;])
# or
app.allows(&amp;quot;*.(example|a).com&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在的想法是，传入一个允许的域列表，或者传入一个正则。&lt;/p&gt;
&lt;p&gt;那么对单个 API 来说，也可以在 Section 之外指定其他的域。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@api.get(&amp;quot;/&amp;quot;)
@api.allow(&amp;quot;*.b.com&amp;quot;)
async def one_api(ctx):
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用 &lt;code&gt;allow&lt;/code&gt; 装饰器来传入一个正则，相对于 Section 允许的内容额外添加一部分域。&lt;/p&gt;
&lt;p&gt;不过这样设计之后，对于以下例子&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;api = Secion(&amp;quot;api&amp;quot;)
app.allows(&amp;quot;a.com&amp;quot;)

@api.get(&amp;quot;/&amp;quot;)
async def get(ctx):
    pass

@api.post(&amp;quot;/&amp;quot;)
@api.allow(&amp;quot;b.com&amp;quot;)
async def post(ctx):
    pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照上述例子中 &lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt; 是返回所有允许访问的方法。&lt;/p&gt;
&lt;p&gt;那么如果这时候访问&lt;code&gt;OPTION /&lt;/code&gt; ， &lt;code&gt;Access-Control-Allow-Methods&lt;/code&gt; 应该就是 &lt;code&gt;OPTIONS, HEAD, GET, POST&lt;/code&gt; ， 那么&lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 应该返回什么内容呢： &lt;code&gt;a.com&lt;/code&gt; 还是&lt;code&gt;a.com, b.com&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;如果是前者，那么 &lt;code&gt;POST / &lt;/code&gt; 是允许 &lt;code&gt;b.com &lt;/code&gt;的； 如果是后者， &lt;code&gt;GET /&lt;/code&gt; 是不允许&lt;code&gt;a.com&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;还是说存在一种可能性，存在一份 RFC， 讲明了对于一个URL，无论是什么方法， &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; 的值都要是一样的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;以上关于 Nougat 的一切代码，纯属虚构。并未实现，具体效果请等正式代码出来&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>Nougat(Misuzu) 的进展和改变</title><link>https://www.kilerd.me/changes-and-development-of-nougat/</link><guid isPermaLink="true">https://www.kilerd.me/changes-and-development-of-nougat/</guid><description>Misuzu 这个名字被人吐槽了很久，不知道怎么读，也不知道什么意思。所以就改成了 Nougat 这个名字，意为牛轧糖。</description><pubDate>Thu, 25 May 2017 14:31:00 GMT</pubDate><content:encoded>&lt;p&gt;Misuzu 这个名字被人吐槽了很久，不知道怎么读，也不知道什么意思。所以就改成了 &lt;strong&gt;Nougat&lt;/strong&gt; 这个名字，意为牛轧糖。&lt;/p&gt;
&lt;h2&gt;handler 参数&lt;/h2&gt;
&lt;p&gt;handler 的参数从原来的 &lt;code&gt;request&lt;/code&gt; 改成了 &lt;code&gt;ctx&lt;/code&gt; 。相比于前者， &lt;code&gt;ctx&lt;/code&gt; 增加了 &lt;code&gt;request&lt;/code&gt; 和 &lt;code&gt;app&lt;/code&gt; 的支持。&lt;/p&gt;
&lt;p&gt;因为 Nougat 这个框架不像 Flask 那样使用子线程的方式来处理。 所以 Flask 中的 &lt;code&gt;url_for&lt;/code&gt; 和 &lt;code&gt;abort&lt;/code&gt; 不能裸露使用。因此必须依赖上下文（Flask 可以在线程池中依靠线程ID 寻找上下文）。所以一定要这样使用&lt;code&gt;something.url_for()&lt;/code&gt; 和 &lt;code&gt;something.abourt&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;url_for&lt;/code&gt; &lt;code&gt;abort &lt;/code&gt; &lt;code&gt;redirect&lt;/code&gt; 这些操作放在 &lt;code&gt;request&lt;/code&gt; 中感觉不三不四的。因此采用了&lt;code&gt;ctx&lt;/code&gt;的写法。&lt;/p&gt;
&lt;p&gt;所以handler 的写法就变成了一下的样子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def handler(ctx):
	pass
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Middleware&lt;/h2&gt;
&lt;p&gt;原来那种以类的形式来写 middleware，过于累赘，而且不支持异步操作。因此改为 koa-like 的方式来写。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;async def middleware(ctx, next):
    # doing before handler
    await next()  # doing handler
    #doing after handler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有一些小小的改进&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;yarl.URL&lt;/code&gt; 来格式化 URL， 比我自己写更加高效，更加可信&lt;/li&gt;
&lt;li&gt;&lt;code&gt;register_middleware&lt;/code&gt; 和 &lt;code&gt;register_section&lt;/code&gt;  改用 &lt;code&gt;use&lt;/code&gt; 。更加简约&lt;/li&gt;
&lt;li&gt;完善了文档&lt;/li&gt;
&lt;li&gt;使用了激进的&lt;code&gt;TOML&lt;/code&gt; 来做项目配置文件，并且同伴完成了&lt;code&gt;Config&lt;/code&gt; 模块的编写，不过还需要我重构一次&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过还是需要有一些等待修改的地方(都是我现在能想到的)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于现在 &lt;code&gt;ctx.params&lt;/code&gt;的读取方式不太满意，应该重构以下&lt;/li&gt;
&lt;li&gt;返回内容的格式化还没写&lt;/li&gt;
&lt;li&gt;&lt;code&gt;param_group&lt;/code&gt; 或者叫 &lt;code&gt;params&lt;/code&gt; 方法，一次性读入多个通用参数&lt;/li&gt;
&lt;li&gt;文档自动化的集成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;并且，从现在开始 Nougat 从原来的&lt;code&gt;pre-alpha&lt;/code&gt; 进入了&lt;code&gt;alpha&lt;/code&gt; 状态，意味着可以尝试投入工程中使用了。&lt;/p&gt;
&lt;p&gt;我也会写一个工程去尝试以下 Nougat 的可用性。&lt;/p&gt;
</content:encoded></item><item><title>一场诡异的梦</title><link>https://www.kilerd.me/a-wired-dream/</link><guid isPermaLink="true">https://www.kilerd.me/a-wired-dream/</guid><description>终于放了很长的一段假期后，我重新回到了学校。明明来这学校很久了，可是我还是无法记住整个学校的所有路径。甚至我已经忘记了这段假期放假的理由。 我的学校坐落于一个偏僻的地方，旁边一栋其余的建筑物都没有。整个学校在天空看起来就像一个巨大的堡垒，在其周围是一些泥土小路，少有的几条沥青公路，并不能见到任何高架公路。其余地方均是被</description><pubDate>Fri, 19 May 2017 07:30:11 GMT</pubDate><content:encoded>&lt;p&gt;终于放了很长的一段假期后，我重新回到了学校。明明来这学校很久了，可是我还是无法记住整个学校的所有路径。甚至我已经忘记了这段假期放假的理由。&lt;/p&gt;
&lt;p&gt;我的学校坐落于一个偏僻的地方，旁边一栋其余的建筑物都没有。整个学校在天空看起来就像一个巨大的堡垒，在其周围是一些泥土小路，少有的几条沥青公路，并不能见到任何高架公路。其余地方均是被黄图覆盖的大地。&lt;/p&gt;
&lt;p&gt;在其内部，基本所有设施都是混在一起的。男女生宿舍、商业中心、饭堂、教学楼等等，就像一个巨大无比的现代化城中村一般。&lt;/p&gt;
&lt;p&gt;我一个人回的学校，下车的地方并不是男生宿舍，而是女生宿舍门口。记忆中，女生宿舍可以直接通到男生宿舍。可是我看了眼那坐在门口，同时也在盯着我的宿管大妈。我打消了从这里穿过去的任何念头。还是经过商中回去吧。&lt;/p&gt;
&lt;p&gt;奇怪的是，我好像怎么都记忆不起来怎么从女生宿舍门口走到男生门口。似乎并不存在那么一条路。当我走进商中之后，整个人都傻了。&lt;/p&gt;
&lt;p&gt;这里看起来并不是我熟悉的商中，在这里面，我失去了任何方向感和记忆，就像一个初来乍到的新生一般。可是我明明在这里读了很久的书。不对？我什么时候来这里读书的？好像记不清楚了。&lt;/p&gt;
&lt;p&gt;这个时候，在我看来，这是一个及其复杂的商业中心。没有办法，我决定先在这里逛下，或许能有什么奇怪的收获。&lt;/p&gt;
&lt;p&gt;但是，我越逛越感到奇怪。为什么在一个大学的商业中心里面全是高档奢侈品的专卖店？更奇怪的是，看似学生的人就像平时在街边买小吃一般随意的在这里消费、购物。若不是我坐的是校巴回来的，并且这里的地理位置和建筑风格都跟我的学校一模一样，我都怀疑我被绑架来了一个奇怪的地方。&lt;/p&gt;
&lt;p&gt;终于，我找到了一个商业中心和女生宿舍的连接路口，心想，我可以从这里进去，然后会男生宿舍。在平常的时候，女生可以随便带自己的男朋友会宿舍，宿管阿姨并不会做任何阻拦，也不知道今天那个大妈是怎么回事。&lt;/p&gt;
&lt;p&gt;当我走到那个连接路口的时候，奇怪的事情还是发生了。本来就只能三四人通过的小路口，如今放置着一张单人床，上面睡着一个赤裸着上身的妇女。留下床两边的是只能侧身通过的小狭缝。&lt;/p&gt;
&lt;p&gt;陌生商中带来的恐惧和紧张没允许我想太多，我便想从这里通过，回到我熟悉的男生宿舍。&lt;/p&gt;
&lt;p&gt;每当我试图通过那个路口时，那妇女都能在我踏进路口的前一秒坐起来，怒视着我，仿佛是在警告我。当我退后打消这个想法，她又会躺下继续熟睡，似乎一切都没发生。尝试过几次后，我放弃了从这里穿过去的想法，继续在商中打转。&lt;/p&gt;
&lt;p&gt;这个时候，我突然发现了一群奇怪的亚裔少年，看起来并不是这里的学生。因为这里是大学，他们怎么看都是初高中的样子，并且会用一种好奇的眼光打量着四周。唯一一个不同的是走在最后面的那个跟我年纪差不多的女生。&lt;/p&gt;
&lt;p&gt;她很漂亮，并且是我进来学校之后唯一一个投以善意眼光和微笑的人。&lt;/p&gt;
&lt;p&gt;当我想上去搭讪两句的时候，她却向前跑去追回那些因为好奇而四散的亚裔少年。&lt;/p&gt;
&lt;p&gt;突然，我来到了整个学校的最底层，这里看起来及其像平民窟一般，房子间隔都是使用布来分割的。并且四周并不能找到任何一个人，仿佛他们都在楼上商铺的后台干着苦力活。&lt;/p&gt;
&lt;p&gt;搜索完整一层，我找不回了去楼上的楼梯，但是我找到一个，唯一一个，蹲在水井旁边洗衣服的亚裔妇女。很奇怪，为什么这里要用水井打水。&lt;/p&gt;
&lt;p&gt;在没有办法之下，我硬着头皮，用着蹩脚的英语询问她怎么通往最近的路。她反问我，白的？还是黑的？ 我想了很久才反应过来她说的是泥路还是沥青路。&lt;/p&gt;
&lt;p&gt;在她的指导之下，我走出了这座陌生的堡垒，来到了一条沥青路上。这个出口并不是我进去的那个入门。&lt;/p&gt;
&lt;p&gt;在路上，我想到了一个想法：我可以骑单车回去，中途截一辆校巴，叫师傅车我回男生宿舍门口，他总知道怎么去把。&lt;/p&gt;
&lt;p&gt;于是，我就在路上找了一辆共享单车。路上有很多不同颜色但是设计一模一样的共享单车。用手机解锁了一台单车的密码后。发现车锁上的密码正好是这个。&lt;/p&gt;
&lt;p&gt;心想，谁那么不小心，没有打乱密码啊。算了，不理他，继续骑。&lt;/p&gt;
&lt;p&gt;路上并没能见到一辆四个轮子的车，反而是身边都是骑着单车的学生。密密麻麻地像个骑行大军。&lt;/p&gt;
&lt;p&gt;没骑几分钟，在路上遇到了警察在查车，拦下我们所有的人。一个一个检查谁是没用 APP 解锁单车的，那些像免费骑车的人都被扣留了下来。&lt;/p&gt;
&lt;p&gt;检查到我的时候，我给警察看了下我的手机，骑行时间 10 多分钟，从学校骑出来就大概差不多那么久。于是我就被放行了。之后的路程上单车就少了一大半。&lt;/p&gt;
&lt;p&gt;原来那里的单车都是密码没有打乱的啊。&lt;/p&gt;
&lt;p&gt;在路上，我顺利的找到了一辆校巴，并且顺利的在男生宿舍门口下了车。&lt;/p&gt;
&lt;p&gt;不知道是放假放太久的问题，还是其他问题。我并不能想起我的宿舍在几楼几号。我便问了下那些趴在走廊上闲聊的同学。知道了我学院宿舍的那个区域怎么走。&lt;/p&gt;
&lt;p&gt;嗯。去到那里我就能遇到我认识的人了。&lt;/p&gt;
&lt;p&gt;可是，走了好久，好久，为什么这条走廊怎么那么长？？&lt;/p&gt;
&lt;p&gt;回头一看，没错啊，这里是男生宿舍啊！&lt;/p&gt;
&lt;p&gt;转过身来，已身处商业中心。&lt;/p&gt;
&lt;p&gt;再回头，后面并没有任何路。&lt;/p&gt;
</content:encoded></item><item><title>Misuzu 偏执的 WEB 框架</title><link>https://www.kilerd.me/misuzu-web/</link><guid isPermaLink="true">https://www.kilerd.me/misuzu-web/</guid><description>我是一个不折不扣的偏执狂，所以我认为所谓的产品就应该是为了特定的人群而服务的。这同样适用于 WEB 框架。 现在 Python 中的三大 WEB 框架(Flask, Tornado, Django) 都属于通用型框架，并不存在一个为 API ，尤其是 REST API 设计的框架。这也是 Misuzu 被创建的原因之一</description><pubDate>Wed, 19 Apr 2017 02:31:58 GMT</pubDate><content:encoded>&lt;p&gt;我是一个不折不扣的偏执狂，所以我认为所谓的产品就应该是为了特定的人群而服务的。这同样适用于 WEB 框架。&lt;/p&gt;
&lt;p&gt;现在 Python 中的三大 WEB 框架(Flask, Tornado, Django) 都属于通用型框架，并不存在一个为 API ，尤其是 REST API 设计的框架。这也是 Misuzu 被创建的原因之一。&lt;/p&gt;
&lt;p&gt;Misuzu 是偏执的，它只为 API 服务，因此我为它做了这些限定：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;RESTFUL API 是用 HTTP METHOD 来区分服务的，因此 Misuzu 在定义路由的时候，就需要指定是哪一种 METHOD，而不是像 Flask 那样可以同时把 GET，POST 写进一个 handler 里面。&lt;/li&gt;
&lt;li&gt;API 最常见的返回格式就是 &lt;code&gt;JSON&lt;/code&gt; ，因此应该做到简化对 &lt;code&gt;JSON&lt;/code&gt; 返回内容的处理，能 &lt;code&gt;return {&amp;quot;name&amp;quot;: &amp;quot;hello&amp;quot;}&lt;/code&gt; 的就不应该&lt;code&gt;return json.dumps({&amp;quot;name&amp;quot;: &amp;quot;hello&amp;quot;})&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;框架应该把 HTML 模板引擎设为可选的，而不是必须的，毕竟一个单纯的 API 系统是不需要渲染 HTML 的&lt;/li&gt;
&lt;li&gt;我觉得 API 是可以分成几个部分的：参数定义及处理、逻辑处理、返回内容的规范化&lt;/li&gt;
&lt;li&gt;Python 写出来的作品就应该更 Pythonic，因此我选择了大量使用装饰器这种及其优雅的处理方式&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;综合了以上几点，Misuzu 的基本模型就出来了：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from misuzu import Misuzu

app = Misuzu(__name__)

@app.get(&apos;/&amp;lt;name&amp;gt;&apos;)
@app.param(&apos;name&apos;, str)
async def index(request):
    return {&apos;hello&apos;: request.params.name}

app.run()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这只是 Misuzu 的第一步，不过也已经确定了它今后的走向。Misuzu 的偏执注定了它不会成为主流，但是只要它能够被一部分人所接受就足以。&lt;/p&gt;
&lt;p&gt;如果你喜欢Misuzu，可以前往 &lt;a href=&quot;https://github.com/Kilerd/misuzu&quot;&gt;https://github.com/Kilerd/misuzu&lt;/a&gt; 了解更多。&lt;/p&gt;
</content:encoded></item><item><title>Python Web 从入门到放弃：Flask or Tornado</title><link>https://www.kilerd.me/python-web-learning-and-giving-up/</link><guid isPermaLink="true">https://www.kilerd.me/python-web-learning-and-giving-up/</guid><description>Python Web 这个领域一直都处于不温不火的情况，但是因为 Python 的易上手性，导致了一部分人也在坚持着这一个领域。 Python Web 框架主要有： Django、Flask、Tornado。 这三大党派都有自己坚持的理由： Django Full Stack 式的开发模式，开发者查看官方文档即可实现大</description><pubDate>Wed, 08 Feb 2017 15:33:43 GMT</pubDate><content:encoded>&lt;p&gt;Python Web 这个领域一直都处于不温不火的情况，但是因为 Python 的易上手性，导致了一部分人也在坚持着这一个领域。
Python Web 框架主要有： Django、Flask、Tornado。&lt;/p&gt;
&lt;p&gt;这三大党派都有自己坚持的理由：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Django&lt;/code&gt; Full Stack 式的开发模式，开发者查看官方文档即可实现大部分网站的大部分功能。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Flask&lt;/code&gt; Minimal 的框架，框架内部只实现了基础功能。Extensionable 的设计，让你的绝大部分功能都可以通过其他开发者完成的 Extension 来实现。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Tornado&lt;/code&gt; 三者中唯一一个异步框架，Web Framework 和 HTTPClient 的结合，同时也是一个简约的设计。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于 Full Stack Framework 和 Micro Framework 的不同看法，把所有 Web 开发者分成了两排人：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full Stack Framework 拥护者认为：无论使用怎样的框架，当网站被设计成大型网站时，框架本身、引用的插件和编写的所有辅助代码都会形成一个异样、另类的 Full Stack Framework（RoR的作者也曾经表示过：任何框架最终都会称为另一个RoR）。那么基于这样的思想，与其在 Micro Framework 上拼凑代码，不如直接使用 Full Stack Framework&lt;/li&gt;
&lt;li&gt;Micro Framework 拥护者认为：Web 框架就应该实现大部分 Web 通用的功能。对于某些定制性强、业务性强的功能和模块，应该由开发者自己实现。该人群认为，大型网站的差异性远超于小型网站，所以在 Full Stack Framework 中的大部分功能都需要修改后才能在大型网站中使用。翻看和修改框架代码的代价也是远超于自己编写一个完全符合自己设计的模块。基于这样，在大型网站中使用 Micro Framework 是比较合理的决定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;笔者个人是比较偏向轻量级的框架，也就是上述的第二类人。那么就回到这篇文章的核心：Flask 和 Tornado 应该怎么选择。
笔者更偏向 Flask。原因并不是笔者长期使用 Flask，下面来分析下 Flask 和 Tornado 的优劣：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Flask 只是一个单独的 Web Framework，Tornado 还包含了 HTTPClient，意思是 Tornado 内置的服务器可以直接用于生产环境，Flask 还需要依靠 Gunicorn 和 Gevent 来用于生产环境和提升性能。 或许在部署方式上，Tornado 获胜&lt;/li&gt;
&lt;li&gt;在那么多接触中，Flask 似乎并没有提出一个较好的方法来利用多核，Tornado 在官方文档中就有相关文献和代码。在利用多核上，Tornado 获胜&lt;/li&gt;
&lt;li&gt;Flask 的 Router 的装饰器更符合人类和 Python 的思想。而 Tornado 的 Route 汇聚方式就那么明显，也有非官方代码实现了装饰器的方式。在 Router 中，各有优劣，各花进各眼，打平。&lt;/li&gt;
&lt;li&gt;Flask 把一个路由写在一个函数中，而 Tornado 却实现在 class 中，能有效的区分各种 HTTP 方法（GET、POST、PUT、DELETE...），并且提供了&lt;code&gt;initialize&lt;/code&gt;、&lt;code&gt;prepare&lt;/code&gt;、&lt;code&gt;on_finish&lt;/code&gt; 等方法。 这方面，Tornado 获胜&lt;/li&gt;
&lt;li&gt;Tornado 官方代码自带 Websocket 模块。&lt;/li&gt;
&lt;li&gt;Flask 支持 Extension 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上述优劣中，看似 Tornado 获胜点居多，实际上是笔者在常年使用 Flask 中遇到的一些不如意的地方，同时也是希望 Flask 可以改善和采纳的地方。&lt;/p&gt;
&lt;p&gt;Flask 支持 Extension 这一点就足矣抵过上述讲的所有点。Tornado 在代码复用上面做的确实是很差。 Flask 通过 Extension 的模式很好的做到了这一点，这也使得 Flask 形成了比较良好的社区 和 产生了大量优秀的 Extension。&lt;/p&gt;
&lt;p&gt;Tornado 本质上，每一次处理都交由 &lt;code&gt;tornado.web.RequestHandler&lt;/code&gt; 来处理。所以在实现大部分功能都是对 &lt;code&gt;RequestHandler&lt;/code&gt; 进行定制和修改。找不到一个很好的链式继承方式来对 &lt;code&gt;RequestHandler&lt;/code&gt; 进行重构，就注定 Tornado 的代码重构会很差。&lt;/p&gt;
&lt;p&gt;在使用 Tornado 的这段时间，真的挺难受的，估计是习惯了 Flask 的模式，另外在 Flask + Gevent 的配合下，Tornado 的异步优点并不是十分明显。&lt;/p&gt;
&lt;p&gt;基本来说，我建议如果入门的话，还是选择 Flask 比较妥当。如某人所说：Tornado 自身的代码实现的很漂亮、很惊人，可是用 Tornado 来组织 Web 代码就显得不那么优雅。&lt;/p&gt;
&lt;p&gt;实际上，Tornado 还是有很多设计得很得当的地方。当玩腻了 Flask 后，可以尝试下 Tornado 。或许会有另外一番心得。&lt;/p&gt;
&lt;p&gt;PS： 以上是笔者对 Flask 和 Tornado 的一些个人看法，不代表任何官方意见，仅供参考。&lt;/p&gt;
</content:encoded></item><item><title>Rust Web初试</title><link>https://www.kilerd.me/first-taste-rust-web/</link><guid isPermaLink="true">https://www.kilerd.me/first-taste-rust-web/</guid><description>Rust 是最近中意的一门语言。相比于 C 和 C++，我更加喜欢 Rust 的语法。 奈何 Rust 的学习曲线太陡了，一直都只能在入门阶段徘徊，没能深入了解 Rust。 个人感觉最大的问题在于没能搞懂 Rust 的所有权、引用借用和生命周期三个方面。 因此，我尝试着用 Rust 来进行 Web 开发，从而加深对 R</description><pubDate>Fri, 06 Jan 2017 15:15:27 GMT</pubDate><content:encoded>&lt;p&gt;Rust 是最近中意的一门语言。相比于 C 和 C++，我更加喜欢 Rust 的语法。&lt;/p&gt;
&lt;p&gt;奈何 Rust 的学习曲线太陡了，一直都只能在入门阶段徘徊，没能深入了解 Rust。 个人感觉最大的问题在于没能搞懂 Rust 的所有权、引用借用和生命周期三个方面。&lt;/p&gt;
&lt;p&gt;因此，我尝试着用 Rust 来进行 Web 开发，从而加深对 Rust 语法的了解。&lt;/p&gt;
&lt;p&gt;我选用的是 Iron 这个 Web 框架。本质上讲 Iron (或者说 Rust 的Web框架)都不能做到像 RoR、Django、Lavaral 那样一站式开发。 Iron 给我的感觉就像 Python 中的 Flask， 甚至更像 Node.js 中的 Koa。&lt;/p&gt;
&lt;p&gt;这篇文章尝试介绍下 Iron 这个框架的编写流程，和给予想学习 Rust 的小伙伴们一些帮助和著者的些许理解。&lt;/p&gt;
&lt;p&gt;环境选择 Rust + Iron + Tera&lt;/p&gt;
&lt;p&gt;项目模型是 TODOLIST&lt;/p&gt;
&lt;h2&gt;Iron 基本介绍&lt;/h2&gt;
&lt;p&gt;Iron 组件的基本介绍：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Iron&lt;/code&gt; Iron 主类，用以启动HTTP服务器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Router&lt;/code&gt; 路由类，属于 Handler&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Chain&lt;/code&gt; 中间件链类，用于处理 Handler、AfterMiddleware、BeforeMiddleware、AroundMiddleware&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Iron 的组织方式是 MVC：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MODEL 层用来定义 Web 中用到的模型。通常使用 ORM&lt;/li&gt;
&lt;li&gt;VIEW 层是模版渲染&lt;/li&gt;
&lt;li&gt;CONTROLLER 层用来控制业务逻辑&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以我们根据 MVC 的模式来建立项目结构，在你的 Rust 项目(用 Cargo 创建)&lt;code&gt;src&lt;/code&gt;文件夹中的结构是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;src/
|----controllers
|----|----todo.rs
|----|----mod.rs
|----routers.rs
|----models.rs
|----lib.rs
|----main.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;controllers&lt;/code&gt; 文件夹中放的是 CONTROLLER 层的内容， 文件夹内的文件就是 CONTROLLER 分类，分模块编写。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;routers.rs&lt;/code&gt; 用来单独处理路由&lt;/li&gt;
&lt;li&gt;&lt;code&gt;models.rs&lt;/code&gt; MODEL 定义文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lib.rs&lt;/code&gt; RUST LIB 格式文件，告知 Rust 这是一个库项目&lt;/li&gt;
&lt;li&gt;&lt;code&gt;main.rs&lt;/code&gt; RUST APP 格式文件，项目入口文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;我们把项目名称称为 &lt;code&gt;todolist&lt;/code&gt; ， &lt;code&gt;cargo.toml&lt;/code&gt; 的依赖信息如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[dependencies]
iron = &amp;quot;*&amp;quot;
router = &amp;quot;*&amp;quot;
tera = &amp;quot;*&amp;quot;
diesel = &amp;quot;*&amp;quot;
lazy_static = &amp;quot;*&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;iron&lt;/code&gt; Iron Web 框架&lt;/li&gt;
&lt;li&gt;&lt;code&gt;router&lt;/code&gt; Iron Router 库&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tera&lt;/code&gt; 模版解析库&lt;/li&gt;
&lt;li&gt;&lt;code&gt;diesel&lt;/code&gt; PostgreSQL 的 ORM 库&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lazy_static&lt;/code&gt; 延迟加载库&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;code&gt;lib.rs&lt;/code&gt; 分析&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;extern crate iron;
extern crate router;

pub mod controllers;
pub mod models;
pub mod routers;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;引用了了两个外部库 &lt;code&gt;iron&lt;/code&gt;, &lt;code&gt;router&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;定义了三个子模块 &lt;code&gt;controllers&lt;/code&gt;, &lt;code&gt;models&lt;/code&gt;, &lt;code&gt;routers&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;code&gt;model.rs&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;
struct ToDoItem {
    item_id: i32,
    value: String,
    done: bool,
}
pub struct ToDoList {
    list : Vec&amp;lt;ToDoItem&amp;gt;,
}

impl ToDoList {
    pub fn add(&amp;amp;self, value: &amp;amp;str) {
        // some code
    }

    pub fn delete(&amp;amp;self, item_id: i32) {
        // some code
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里不多解释，因为使用 ORM 和不使用的MODEL是不一样的。所以不过多分析，这里只是定义了两个结构而已。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;controllers&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;该文件夹中有两个文件 &lt;code&gt;mod.rs&lt;/code&gt;， &lt;code&gt;todo.rs&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;mod.rs&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub mod todo;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个文件只是告知该文件夹中有几个子模块，这里定义了一个模块 &lt;code&gt;todo&lt;/code&gt;。 这是 Rust 的模块定义的固定格式。详情查看 Rust 官方文档。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;todo.rs&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;这里是我们自己定义的关于 TODOLIST 业务的 CONTROLLER&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;use iron::status;
use iron::prelude::*;

pub fn todo_show(_: &amp;amp;mut Request) -&amp;gt; IronResult&amp;lt;Response&amp;gt; {
    Ok(Response::with((status::Ok, &amp;quot;todo list&amp;quot;)))
}

pub fn todo_add(_: &amp;amp;mut Request) -&amp;gt; IronResult&amp;lt;Response&amp;gt; {
    Ok(Response::with((status::Ok, &amp;quot;add page&amp;quot;)))
}
pub fn todo_delete(_: &amp;amp;mut Request) -&amp;gt; IronResult&amp;lt;Response&amp;gt; {
    Ok(Response::with((status::Ok, &amp;quot;delete page&amp;quot;)))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个文件按照 Iron 框架处理函数的格式写了三个函数(都是简单的返回纯文本)。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;routers.rs&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;use router::Router;
use controllers::todo::*;

pub fn router_generator() -&amp;gt; Router {
    let mut router = Router::new();
    router.get(&amp;quot;/&amp;quot;, todo_show, &amp;quot;todo_show&amp;quot;);
    router.post(&amp;quot;/add&amp;quot;, todo_add, &amp;quot;todo_add&amp;quot;);
    router.post(&amp;quot;/delete&amp;quot;, todo_delete, &amp;quot;todo_delete&amp;quot;);
    router
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;router 的构建函数， 在内部把刚刚写的几个 CONTROLLER 添加至路由中。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;main.rs&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;extern crate iron;
#[macro_use]
extern crate tera;
#[macro_use]
extern crate lazy_static;

extern crate todolist;

use iron::prelude::Iron;
use tera::{Tera, Context};
use yolk::routers::router_generator;


// 延迟渲染模版
lazy_static! {
    pub static ref TEMPLATES: Tera = {
        let tera = compile_templates!(&amp;quot;templates/**/*&amp;quot;);
        tera
    };
}


fn main() {
    // 启动 Iron 服务器
    let _server = Iron::new(router_generator()).http(&amp;quot;localhost:3000&amp;quot;).unwrap();
    println!(&amp;quot;On 3000&amp;quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;引入几个外部模块 &lt;code&gt;iron&lt;/code&gt;, &lt;code&gt;lazy_static&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;引入自己这个模块 &lt;code&gt;todolist&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用刚刚写的&lt;code&gt;router_generator&lt;/code&gt; 函数来作为 &lt;code&gt;Handler&lt;/code&gt; 启动 Iron&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;TEMPLATE 库的一些看法&lt;/h2&gt;
&lt;p&gt;Rust 在 Web 这个方向发展并没有太快。所以在模版上并没有太多优质 的库。在我看来，有两个库是值得参考一下的。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Keats/tera&quot;&gt;Tera&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/sunng87/handlebars-iron&quot;&gt;Handlebars-iron&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;handlebars-iron&lt;/code&gt; 基本符合 Iron 的设计思想，采用 &lt;code&gt;AfterMiddleWare&lt;/code&gt; 的思想，对 Controller 的入侵是最小的，而且还可以做到 watch 特性，即无需重启进程都可以重新渲染模版（Rust 是编译性语言，进程执行后就已经把模版渲染好了，所以修改模版文件并不会自动生效。）。有兴趣的可以读者自己阅读相关文献。&lt;/p&gt;
&lt;p&gt;著者是中意 Tera 更多的，一下是 Tera 的简单使用：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;// 延迟加载模版渲染
lazy_static! {
    pub static ref TEMPLATES: Tera = {
        let tera = compile_templates!(&amp;quot;templates/**/*&amp;quot;);
        tera
    };
}

fn main() {
    let context = Context::new(); // 参数类
    context.add(&amp;quot;KEY&amp;quot;, &amp;quot;VALUE&amp;quot;); // 添加一个参数
    let content = TEMPLATES.render(&amp;quot;test.html&amp;quot;, context); // 渲染 test.html 这个模版，返回的是 Optional
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要使用在 Iron 中，大致是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-rust&quot;&gt;pub fn todo_show(_: &amp;amp;mut Request) -&amp;gt; IronResult&amp;lt;Response&amp;gt; {
    let context = Context::new(); // 参数类
    context.add(&amp;quot;KEY&amp;quot;, &amp;quot;VALUE&amp;quot;); // 添加一个参数
    let content = TEMPLATES.render(&amp;quot;test.html&amp;quot;, context);
    Ok(Response::with((status::Ok, content.unwrap()))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;说 Iron 完成度低的原因就在于，返回的类型是 IronResult. 并没有做到直接返回 RUST 基本类型即可。&lt;/p&gt;
&lt;p&gt;不过有能力者可以自己写一个宏来处理。&lt;/p&gt;
&lt;h2&gt;对于 ORM 的一些看法&lt;/h2&gt;
&lt;p&gt;ORM 的使用不方便大概是我放弃用 Rust 写 Web 的主要原因之一。实在是太不优雅了。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/diesel-rs/diesel&quot;&gt;Diesel&lt;/a&gt; 是我在那么多 ORM 中比较中意的一个，只支持 PostgreSQL。
感兴趣的读者可以关注一下。&lt;/p&gt;
&lt;h2&gt;关于 Iron&lt;/h2&gt;
&lt;p&gt;Iron 目前对于使用者来说，就是写很多很多的 MiddleWare， 然后用 &lt;code&gt;Chain&lt;/code&gt; 类串联起来。 并不能最太多的东西。如果偏要写很多的东西的话，你会发现直接用底层的&lt;code&gt;hyper&lt;/code&gt;库直接重新实现一个 Web 框架更为实际。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Router&lt;/code&gt; 类的使用也是挺不方便的，并不能很轻易地让一个 CONTROLLER 绑定在同一链接的两个方法上。这个设计的初衷也是很好的，避免无畏的可访问页面。&lt;/p&gt;
&lt;h2&gt;结言&lt;/h2&gt;
&lt;p&gt;文章就写完了，主要讲的是怎么构建一个可以扩展的网站框架，而不是网站实现的细节。
希望你有所收获，同时也希望有更多的人喜欢 Rust 这门优雅的语言。&lt;/p&gt;
</content:encoded></item><item><title>山寨红色 iPhone 5S</title><link>https://www.kilerd.me/a-fake-red-iphone-5s/</link><guid isPermaLink="true">https://www.kilerd.me/a-fake-red-iphone-5s/</guid><description>突发奇想，买了一个祖国版 iPhone 5S ，这个红色是不是很好看呢。</description><pubDate>Mon, 05 Dec 2016 15:27:26 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://ooo.0o0.ooo/2016/12/05/584585c512313.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;突发奇想，买了一个祖国版 iPhone 5S ，这个红色是不是很好看呢。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ooo.0o0.ooo/2016/12/05/584585c572f4a.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;神奇的是，这个居然还能有 Touch ID 用， 然后跟同学的正版 iPhone 5S 对比了下，速度差不多。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ooo.0o0.ooo/2016/12/05/584585c581ad8.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;屏幕也很不错， Retina 屏幕就是不错。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://ooo.0o0.ooo/2016/12/05/584585c5ab4a1.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个 IMEI 还是典藏版的 圆周率 PI 的。&lt;/p&gt;
</content:encoded></item><item><title>如何正确的部署 Flask 项目</title><link>https://www.kilerd.me/how-to-deply-flask-project/</link><guid isPermaLink="true">https://www.kilerd.me/how-to-deply-flask-project/</guid><description>系统 Python 设置 由于每个 Linux 发行版的内置 Python 都不太一样，而且为了避免你的项目在不同的 Python 版本下出现各种奇怪问题。 比如， requests 库在 python 2.7.5 的环境下访问 HTTPS 网站会出现 SNI 的问题，导致访问失败。 所以我们需要使用 pyenv 和 </description><pubDate>Fri, 25 Nov 2016 04:07:27 GMT</pubDate><content:encoded>&lt;h1&gt;系统 Python 设置&lt;/h1&gt;
&lt;p&gt;由于每个 Linux 发行版的内置 Python 都不太一样，而且为了避免你的项目在不同的 Python 版本下出现各种奇怪问题。&lt;/p&gt;
&lt;p&gt;比如，&lt;code&gt;requests&lt;/code&gt; 库在 python 2.7.5 的环境下访问 HTTPS 网站会出现 SNI 的问题，导致访问失败。&lt;/p&gt;
&lt;p&gt;所以我们需要使用 &lt;code&gt;pyenv&lt;/code&gt; 和 &lt;code&gt;virtualenv&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注：强烈不建议直接更新系统里面的 Python ，否则你会出现各种各样的奇怪问题&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;pyenv&lt;/h2&gt;
&lt;p&gt;pyenv 允许你在一台机器上配置多个版本的 python 环境&lt;/p&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt-get install curl git-core // 依赖库
curl -L https://raw.github.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果执行成功，那么 &lt;code&gt;pyenv&lt;/code&gt; 就会安装到 &lt;code&gt;~/.pyenv&lt;/code&gt; 目录里面。&lt;/p&gt;
&lt;p&gt;为了方便我们用 &lt;code&gt;pyenv subcommand&lt;/code&gt; 的方法来调用 &lt;code&gt;pyenv&lt;/code&gt; ，我们要在 &lt;code&gt;~/.bashrc&lt;/code&gt; 文件中添加以下内容&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;export PYENV_ROOT=&amp;quot;${HOME}/.pyenv&amp;quot;

if [ -d &amp;quot;${PYENV_ROOT}&amp;quot; ]; then
  export PATH=&amp;quot;${PYENV_ROOT}/bin:${PATH}&amp;quot;
  eval &amp;quot;$(pyenv init -)&amp;quot;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;~/.bashrc&lt;/code&gt; 的作用大概就是用户配置文件&lt;/p&gt;
&lt;p&gt;执行 &lt;code&gt;source ~/.bashrc&lt;/code&gt;  以重新加载用户配置文件。&lt;/p&gt;
&lt;p&gt;至此，&lt;code&gt;pyenv&lt;/code&gt; 就安装好了。&lt;/p&gt;
&lt;h3&gt;使用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pyenv install --list&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;列出目前 &lt;code&gt;pyenv&lt;/code&gt; 安装过的所有 Python 版本&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pyenv install $version&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;$version&lt;/code&gt; 就是你想要安装的 Python 版本&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pyenv versions&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;显示当前系统用的是哪个 Python 版本&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pyenv global $version&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;把当前系统的 Python 版本切换到 &lt;code&gt;$version&lt;/code&gt; 这个版本。该命令在本教材中并不用到，故不多介绍，如果你想了解更多，可以查看&lt;code&gt;pyenv&lt;/code&gt; 的 API 文档&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;virtualenv&lt;/h2&gt;
&lt;p&gt;用于分离 Python 库&lt;/p&gt;
&lt;h3&gt;安装&lt;/h3&gt;
&lt;p&gt;如果你是使用以上的方法安装&lt;code&gt;pyenv&lt;/code&gt;的话，那么 &lt;code&gt;virtualenv&lt;/code&gt; 就已经安装完了。
&lt;strong&gt;强烈不建议用其他方法安装 virtualenv， 那将会是一种很麻烦很折腾的方法&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;使用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;pyenv virtualenv $version $name&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;用于创建一个指定 Python 版本的虚拟环境&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$version&lt;/code&gt; 你需要的 Python 版本&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$name&lt;/code&gt; 该虚拟环境的名称&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rm -rf ~/.pyenv/versions/$name/&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;删除名称为&lt;code&gt;$name&lt;/code&gt;的虚拟环境。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注意： 这里用到了&lt;code&gt;rm -rf&lt;/code&gt; 谨慎使用，使用前先检查代码&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;基本需求&lt;/h1&gt;
&lt;p&gt;首先我们需要一下这几个库来来帮助我们部署，建议在 virtualenv 中安装，以免感染系统&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;gunicorn&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;用 WSGI 的方式来启动 flask 项目&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;gevent&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;python 中比较好的网络库，提供更加高效的 I/O 读写&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;supervisor&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;守护进程，避免进程意外退出&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;安装方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pip install gunicorn gevent supervisor
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;部署你的 Flask 项目&lt;/h1&gt;
&lt;h2&gt;Flask 项目结构&lt;/h2&gt;
&lt;p&gt;为了做演示，我们暂定需要部署的 Flask 项目结构是这个样子的&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flaskproject
|----app
|----|----templates
|----|----static
|----|----models.py
|----|----views
|----|----|----__init__.py
|----|----|----user.py
|----|----functions.py
|----config.py
|----run.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;项目结构并不是固定的，可以自己根据自己项目的情况自行分配&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;run.py&lt;/code&gt; 中就是 Flask 项目的入口，内容大致如下&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask import Flask

app = Flask()

# ... some configs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者使用&lt;code&gt;create_app()&lt;/code&gt; 模式&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from flask import Flask

def create_app():
    app = Flask()
    
    # ... some configs
    
    return app
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Supervisor + Gunicorn + Gevent&lt;/h2&gt;
&lt;h3&gt;前提设置&lt;/h3&gt;
&lt;p&gt;首先启动之前已经创建好的 virtualenv 虚拟环境&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pyenv activate $name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果正常启动的话，在 bash 命令行上面应该会出现环境的名字&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;($name) # 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没正常启动的样子是这样的&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;# 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进去之后呢，用&lt;code&gt;pip&lt;/code&gt; 安装你的项目依赖&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置 Supervisor&lt;/h3&gt;
&lt;p&gt;进入项目文件夹&lt;/p&gt;
&lt;p&gt;然后输出 supervisor 默认配置文件 &lt;code&gt;supervisor.conf&lt;/code&gt; : (&lt;code&gt;$name&lt;/code&gt; 是 virtualenv 环境的名称)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;~/.pyenv/versions/$name/bin/echo_supervisord_conf &amp;gt; supervisor.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后打开 &lt;code&gt;supervisor.conf&lt;/code&gt; ，然后在文件的最后添加一下内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[program:myapp]
command=~/.pyenv/versions/$name/bin/gunicorn -k gevent -w2 -b0.0.0.0:9000 run:app   ; supervisor启动命令
directory=/home/myproject                                                 ; 项目的文件夹路径
startsecs=0                                                               ; 启动时间
stopwaitsecs=0                                                            ; 终止等待时间
autostart=true                                                            ; 是否自动启动
autorestart=true                                                          ; 是否自动重启
stdout_logfile=/home/myproject/log/gunicorn.log                           ; log 日志
stderr_logfile=/home/myproject/log/gunicorn.err                           ; 错误日志
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;command&lt;/code&gt;  中的是用 &lt;code&gt;gunicorn&lt;/code&gt; 和 &lt;code&gt;gevent&lt;/code&gt; 启用 Flask 项目的命令
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-w2&lt;/code&gt; 指的是 &lt;code&gt;2 worker&lt;/code&gt; 一般设置成 &lt;code&gt;2 * cpu_nums&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-b0.0.0.0:9000&lt;/code&gt; 指的是启动在 9000 端口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;run:app&lt;/code&gt; 是 Flask 项目入口，如果你用的是&lt;code&gt;create_app()&lt;/code&gt; 方法的话， 就改成&lt;code&gt;run:create_app()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;所有目录都需要使用绝对路径&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;操作 Supervisor&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;启动 Supervisor&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/.pyenv/versions/$name/bin/supervisord -c supervisor.conf&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看 Supervisor 情况&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/.pyenv/versions/$name/bin/supervisorctl -c supervisor.conf status&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;重启 Supervisor&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/.pyenv/versions/$name/bin/supervisorctl -c supervisor.conf reload&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动 Supervisor 中某个 / 全部程序&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/.pyenv/versions/$name/bin/supervisorctl -c supervisor.conf start [all][appname]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭 Supervisor 中某个 / 全部程序&lt;/p&gt;
&lt;p&gt;&lt;code&gt;~/.pyenv/versions/$name/bin/supervisorctl -c supervisor.conf stop [all][appname]&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;配置好你的 &lt;code&gt;supervisor.conf&lt;/code&gt; 文件后，执行启动命令，如果没打错命令基本就已经启动好了，这个时候你应该就可以访问 &lt;code&gt;http://ip:9000&lt;/code&gt; 来访问到你的 Flask 项目&lt;/p&gt;
&lt;h1&gt;Nginx 设置&lt;/h1&gt;
&lt;p&gt;这里只讨论如何使用 Nginx ，Apache 党请自行转换&lt;/p&gt;
&lt;h2&gt;Nginx 安装&lt;/h2&gt;
&lt;p&gt;详情查看 &lt;a href=&quot;https://www.nginx.com/resources/wiki/start/topics/tutorials/install/&quot;&gt;Install | Nginx&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Nginx 配置&lt;/h2&gt;
&lt;p&gt;进入 Nginx配置文件，或者&lt;code&gt;/etc/nginx/conf.d/default.conf&lt;/code&gt; ，或者新建 &lt;code&gt;/etc/nginx/conf.d/myproject.conf&lt;/code&gt;，修改配置文件，以下这是几个建议的配置&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;禁止 IP 访问&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen       80 default_server;
    listen       [::]:80 default_server;
    server_name  _;

    location / {
        return 404;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;禁止 HTTP 访问&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen               80;
    server_name          www.myproject.com myproject.com;
    server_tokens        off;
    
    # 跳转至 HTTPS
    location / {
        rewrite ^/(.*)$ https://myproject.com/$1 permanent;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;HTTPS设置 / 443端口设置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen               443 ssl http2 fastopen=3 reuseport;

    server_name          www.myproject.com myproject.com;
    server_tokens        off;

	# SSL 设置
    ssl_certificate      /root/.acme.sh/myproject.com_ecc/fullchain.cer;
    ssl_certificate_key  /root/.acme.sh/myproject.com_ecc/myproject.com.key;
    ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
    ssl_prefer_server_ciphers  on;
    ssl_protocols              TLSv1 TLSv1.1 TLSv1.2;
    ssl_session_cache          shared:SSL:50m;
    ssl_session_timeout        1d;
    ssl_session_tickets        on;
    ssl_stapling               on;
    ssl_stapling_verify        on;
    resolver                   114.114.114.114 valid=300s;
    resolver_timeout           10s;

	
	# NGINX 日志
    access_log                 /home/myproject/log/nginx.log;

	# 禁止非 GET HEAD POST OPTIONS 的访问
    if ($request_method !~ ^(GET|HEAD|POST|OPTIONS)$ ) {
        return           444;
    }
	
	# www.myproject.com 跳转至 myproject.com
	# 可有可无，看个人情况
    if ($host != &apos;myproject.com&apos; ) {
        rewrite          ^/(.*)$  https://myproject.com/$1 permanent;
    }
	
	# 代理 Flask 端口
    location / {
        proxy_http_version       1.1;
		
		# HSTS 头设置
        add_header               Strict-Transport-Security &amp;quot;max-age=31536000; includeSubDomains; preload&amp;quot;;
        add_header               X-Frame-Options deny;
        
        # 添加两个 Request Header
        proxy_set_header         X-Real_IP        $remote_addr;
        proxy_set_header         X-Forwarded-For  $proxy_add_x_forwarded_for;
		
		# 代理 9000 端口
        proxy_pass               http://127.0.0.1:9000;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;这里的SSL申请是用 &lt;a href=&quot;https://github.com/Neilpang/acme.sh&quot;&gt;acme.sh&lt;/a&gt; 申请的 Let‘s Encrypt ECC证书&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;然后重启 Nginx ，项目就配置完成了。就可以使用域名正常访问了（DNS解析正常的情况下）&lt;/p&gt;
&lt;h1&gt;快捷使用方式&lt;/h1&gt;
&lt;p&gt;因为 Python 的执行方式不像 PHP 那样的脚本式执行，所以当项目代码更新后，需要重启才能使代码生效。&lt;/p&gt;
&lt;p&gt;为了方便重启，我们在项目的根目录下创建&lt;code&gt;reload.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;git pull  // 我是使用 git 拉去项目代码的，这里的作用是更新项目代码
~/.pyenv/versions/$name/bin/supervisorctl -c supervisor.conf reload  // 重启 Supervisor
echo &apos;Reload Done&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下次我们就可以直接用 &lt;code&gt;sh reload.sh&lt;/code&gt; 来直接更新项目状态了&lt;/p&gt;
&lt;h1&gt;注意事项&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;服务器的配置不止如此，&lt;code&gt;iptables&lt;/code&gt; 等等相关软件限制端口访问&lt;/li&gt;
&lt;li&gt;建议只允许 HTTPS 访问&lt;/li&gt;
&lt;li&gt;如果不限制端口访问，不建议把 &lt;code&gt;supervisor&lt;/code&gt; 配置中的监听端口暴露（即使用9000端口）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;待完善的内容&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;建议使用 &lt;code&gt;fabric &lt;/code&gt;来代替&lt;code&gt;sh reload.sh&lt;/code&gt; 的执行方式&lt;/li&gt;
&lt;li&gt;log 文件如何更高效地分析&lt;/li&gt;
&lt;li&gt;期待下一篇文章把，下一次将完善这些内容&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>一人孤独，两份寂寥</title><link>https://www.kilerd.me/everyone-is-lonely/</link><guid isPermaLink="true">https://www.kilerd.me/everyone-is-lonely/</guid><description>![lonely][1] 大概是因为这篇文章 这几天累得我想自杀 引起了我的思绪，或者早就预谋已久。 孤独就是一份毒品，能让你上瘾，亦能杀死你。 那篇文章是一个996的程序员发出来的。回复的人几乎都是在劝楼主换工作、对自己好一点别那么拼。朝九晚五，看似很合理也很常见的工作时间，在程序员圈子基本不可能见到。996即是指朝</description><pubDate>Sun, 21 Aug 2016 04:52:25 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://ooo.0o0.ooo/2017/06/10/593b528c74fe5.jpg&quot; alt=&quot;lonely&quot;&gt;&lt;/p&gt;
&lt;p&gt;大概是因为这篇文章 &lt;a href=&quot;https://www.v2ex.com/t/300576&quot;&gt;这几天累得我想自杀&lt;/a&gt; 引起了我的思绪，或者早就预谋已久。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;孤独就是一份毒品，能让你上瘾，亦能杀死你。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那篇文章是一个996的程序员发出来的。回复的人几乎都是在劝楼主换工作、对自己好一点别那么拼。朝九晚五，看似很合理也很常见的工作时间，在程序员圈子基本不可能见到。996即是指朝九晚九六个工作日，甚者9107。&lt;/p&gt;
&lt;p&gt;我并不是在批判这个职业工作时间的不合理性，也不是在埋怨工作时间之长。“存在即有他的合理性“。996是什么概念：下班回到家十点多，洗澡吃饭十二点多，睡一觉就要去上班了，基本不存在私人时间。人能不压抑吗？&lt;/p&gt;
&lt;p&gt;长时间的封闭必然会出问题，这就是为什么著名的小黑屋那么可怕的原因所在。封闭不单指物理也指心灵。人这种群居性生物，一旦失去/缺少社会交流，就会感到孤独，而且就会上瘾。&lt;/p&gt;
&lt;p&gt;孤独是会上瘾的。特别是那种不喜欢改变的人。这种人往往会拒绝剧烈的改变，却在默默中被细小的变化潜移默化。直至最后甚至说不出自己如何变成这个样子。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;看！ 就是他，平时都不怎么跟别人讲话的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这个社会的道德绑架和道德舆论是&lt;s&gt;灰长&lt;/s&gt;非常可怕的。他们似乎在告诉你：你本应该如何、你应该做什么。&lt;/p&gt;
&lt;p&gt;然而事实上，毫无意义。就像上面那篇文章那样，在城市中打拼那么多年，早已欠下一大笔账（贷款买房、买车etc），不如此打拼，估计那份压力也压得你完全透不过气来。并不是说能放下就真的能放下。“给你自由，你才会感到不自由”大概讲得就是这个道理吧。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;人性的最大缺点就是对陌生人莫名的恶意&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不是所有人都被社会很好地待见，注定有某些人会被排挤。嗯，或许说不上被排挤，可能是就是低那么一个等级吧。故此，理性而闷骚(他们都这样说)的程序员估计会在此之首(起码之中大部分人吧)。这种，在我看来，是一种完完全全的正反馈，像毒品一样。慢慢地，区别就会特别明显。&lt;/p&gt;
&lt;p&gt;一人孤独，两份寂寥，于你，于我&lt;/p&gt;
</content:encoded></item><item><title>HOOK机制浅谈与实现</title><link>https://www.kilerd.me/how-hooks-works/</link><guid isPermaLink="true">https://www.kilerd.me/how-hooks-works/</guid><description>HOOK机制最常见的地方就是在 windows 系统里面。你可以通过 HOOKS 来监控键盘输入、鼠标点击等等。那到底什么是 HOOK 机制呢？用人话讲就是“允许在特定的行为前后添加自定义行为” # before doing do something... # after doing</description><pubDate>Sat, 25 Jun 2016 09:05:34 GMT</pubDate><content:encoded>&lt;p&gt;HOOK机制最常见的地方就是在 windows 系统里面。你可以通过 HOOKS 来监控键盘输入、鼠标点击等等。那到底什么是 HOOK 机制呢？用人话讲就是“允许在特定的行为前后添加自定义行为”&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# before doing
do something...
# after doing
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;HOOK 与 非HOOK 的对比&lt;/h3&gt;
&lt;p&gt;这里我们用博客评论做例子（感觉这会是最常见的例子了），并且用伪代码来做演示。&lt;/p&gt;
&lt;p&gt;博客评论最基本的函数可以写成这个样子&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;function comment_process(){
  Insert to database
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们有那么一个需求：在评论之后发送一个通知邮件给评论者，那么我们就需要修改这个 &lt;code&gt;commet_process&lt;/code&gt; 函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;function comment_process(){
  Insert to database
  send_email(commenter);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果我们还需要发邮件通知博客主人，那么&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;function comment_process(){
  Insert to database
  send_email(commenter);
  send_email(host);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或许，有人会说可以用配置文件来解决这个问题，就像如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;function comment_process(){
  Insert to database
  if(config[&apos;send_to_commenter&apos;]){
    send_email(commenter);
  }
  if(config[&apos;send_to_host&apos;]){
    send_email(host);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看起来这个需求是解决了。但是如果我们需要把这个设计的模式交由用户或者开发者来使用。不可能让他们去直接修改程序的代码。甚者他们还不一定看得懂。&lt;/p&gt;
&lt;p&gt;所以，我们可以采用 HOOK 机制，采用插件的方式来完美解决这个问题。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;像上文所说，我们允许在评论前后自定义行为， 所以&lt;code&gt;comment_process&lt;/code&gt;函数就可以改成一下形式&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;function comment_process(){
  hook.call(&apos;COMMENT_BEFORE&apos;, args);
  Insert to database
  hook.call(&apos;COMMENT_AFTER&apos;, args);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们先定义一个&lt;code&gt;hook&lt;/code&gt;变量来操作HOOK，从代码中，我们可以很清晰地看出来在&lt;code&gt;Insert to database&lt;/code&gt; 操作前，先执行 &lt;code&gt;COMMENT_BEFORE&lt;/code&gt; 相关的内容； 之后执行&lt;code&gt;COMMENT_AFTER&lt;/code&gt; 相关内容。&lt;/p&gt;
&lt;p&gt;简单来讲，我们可以理解成&lt;code&gt;hook&lt;/code&gt; 存在两个列表&lt;code&gt;COMMENT_BEFORE&lt;/code&gt; 和&lt;code&gt;COMMENT_AFTER&lt;/code&gt; ，当执行&lt;code&gt;hook.call()&lt;/code&gt; 命令时便遍历执行对应列表中的内容。&lt;/p&gt;
&lt;p&gt;所以，如果我们用 HOOK 来实现上述 非HOOK的功能的话， 就是需要把 &lt;code&gt;send_email(commenter);&lt;/code&gt; 和 &lt;code&gt;send_email(host);&lt;/code&gt; 加入 &lt;code&gt;COMMENT_AFTER&lt;/code&gt; 列表中。&lt;/p&gt;
&lt;p&gt;可以清晰地看到，无论我们的需求改成怎样，对&lt;code&gt;comment_process&lt;/code&gt; 函数的入侵和修改都是最小的。&lt;/p&gt;
&lt;p&gt;那么，如果能解决怎么把 &lt;code&gt;自定义行为&lt;/code&gt; 加入列表，就可以完美地写出插件机制。&lt;/p&gt;
&lt;p&gt;为此，我们可以为&lt;code&gt;hook&lt;/code&gt; 变量定义注册函数&lt;code&gt;register_action()&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;function register_action(type, action_name){
  list[type].append(action_name);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自此我们有了一个注册函数，用于添加自定义行为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;hook.register_action(&apos;COMMENT_AFTER&apos;, &apos;send_to_commenter&apos;);
hook.register_action(&apos;COMMENT_AFTER&apos;, &apos;send_to_host&apos;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样列表&lt;code&gt;COMMENT_AFTER&lt;/code&gt; 就更新成 &lt;code&gt;[&apos;send_to_commenter&apos;, &apos;send_to_host&apos;]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;下面再实现 &lt;code&gt;hook.call()&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;function call(type, args){
  for each_one in list[type]{
    each_one(args);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，HOOK功能也就基本描述完了，一个简单的插件模式也完成了。&lt;/p&gt;
&lt;p&gt;添加功能只用以下两步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;编写对应逻辑的方法/函数&lt;/li&gt;
&lt;li&gt;调用&lt;code&gt;hook.register_action&lt;/code&gt; 方法进行注册&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Python中的具体实现方法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;谁叫我主要用python呢 。 ╮(╯_╰)╭&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如上文所说，我们存入列表中的是方法的名字(类型为字符串/string)，所以我们需要把字符串转换成方法指针。也就是说，我们需要遍历所有的代码，找出一个方法/变量/函数与之同名。&lt;/p&gt;
&lt;p&gt;庆幸的是Python提供了&lt;code&gt;eval&lt;/code&gt; 函数，可以直接使用&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def test_function():
    print &apos;hey, it is me.&apos;

if __name__ == &apos;__main__&apos;:
    
    eval(&apos;test_function&apos;)() # hey, it is me.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;居然成功执行了， &lt;code&gt;eval(&apos;test_function&apos;)&lt;/code&gt; 指向了&lt;code&gt;test_function&lt;/code&gt; 这个函数&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def test_function():
    print &apos;hey, it is me.&apos;

if __name__ == &apos;__main__&apos;:
    
    print id(test_function) # 4292954292
    print id(eval(&apos;test_function&apos;)) # 4292954292
    print test_function == eval(&apos;test_function&apos;) # True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;eval(&apos;test_function&apos;)&lt;/code&gt; 成了&lt;code&gt;test_function&lt;/code&gt; 的一个引用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BUT&lt;/strong&gt;，&lt;code&gt;eval&lt;/code&gt; 有一个致命的弱点：写不好的话可能会引起漏洞供人注入。（这里不是本文讨论的重点，详情请GOOGLE）并且&lt;code&gt;eval&lt;/code&gt; 挺慢的， 我们做1,000,000 次 &lt;code&gt;1 + 1&lt;/code&gt; 试试&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time
def test_function():
    return 1+1
if __name__ == &apos;__main__&apos;:
    # eval test
    i=1000000
    start = time.time()
    while i&amp;gt;0:
        eval(&apos;test_function&apos;)()
        i -= 1
    end = time.time()
    print &apos;{}&apos;.format(end-start)  # 6.56956291199

    # normal test
    i=1000000
    start = time.time()
    while i&amp;gt;0:
        test_function()
        i -= 1
    end = time.time()
    print &apos;{}&apos;.format(end-start)  # 0.15709900856
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;6.57s 和 0.15s 的差距还是蛮大的。 不过均摊到1次的时间就好象可以忽略了。&lt;/p&gt;
&lt;p&gt;有了&lt;code&gt;eval&lt;/code&gt; 函数的帮忙，实现hook就会变得简单不少&lt;/p&gt;
&lt;p&gt;PS: 最后讲道理，居然没有用上&lt;code&gt;eval&lt;/code&gt; ，不过当你用不同写法的时候还是可能会用上的。ㄟ( ▔, ▔ )ㄏ&lt;/p&gt;
&lt;p&gt;首先，我们先定义工程目录分布：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/app
..../plugins
........__init__.py
......../test_plugin
............__init__.py
............function.py
....hook.py
....view.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;hook.py&lt;/code&gt; 用于实现HOOK机制&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Hook:
    _list = {}  # 用于储存HOOK行为

    @classmethod
    def register_action(cls, type, plugin_name, action_name):  # 注册行为
        if type not in cls._list:
            cls._list[type] = []
            cls._list[type].append((plugin_name, action_name))
        elif action_name not in cls._list[type]:
                cls._list[type].append((plugin_name, action_name))

    @classmethod
    def call(cls, type, **args):  # 执行行为
        if type not in cls._list:
            return;
        for action in cls._list[type]:
            exec_string = &apos;from plugins.{}.function import {}&apos;.format(action[0], action[1])
            exec(exec_string)  # 动态加载
            eval(action[1])(**args)  # 执行

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/plugins/test_plugin/function.py&lt;/code&gt; 中就是我们自定义的HOOK行为&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def send_to_commenter(**args):
    print &apos;send email to {}&apos;.format(args[&apos;commenter&apos;])

def send_to_host(**args):
    print &apos;send email to {}&apos;.format(args[&apos;host&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里使用 &lt;code&gt;**args&lt;/code&gt; 作为参数入口，接受所有参数&lt;/p&gt;
&lt;p&gt;&lt;code&gt;view.py&lt;/code&gt; 中包含两部分内容：前部分为初始化代码； 后部分是模拟函数执行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from hook import Hook

def comment(commenter, content):
    args = {
        &apos;commenter&apos;: commenter,
        &apos;host&apos;: &apos;myself&apos;
    }
    Hook.call(&apos;COMMENT_BEFORE&apos;, **args)
    print content
    Hook.call(&apos;COMMENT_AFTER&apos;, **args)

# 注册两个钩子
Hook.register_action(&apos;COMMENT_AFTER&apos;,&apos;test_plugin&apos;, &apos;send_to_commenter&apos;)
Hook.register_action(&apos;COMMENT_AFTER&apos;,&apos;test_plugin&apos;, &apos;send_to_host&apos;)

# 模拟运行 comment 行为
comment(&apos;someone@domain.com&apos;, &apos;Hello, Guys&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行结果是很明显的：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;Hello, Guys
send email to someone@domain.com  # send_to_commenter
send email to myself  # send_to_host
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，HOOK机制讲完了， 也可以顺利写出 插件机制 了。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;但是上面这个程序看起来并不是那么完美，因为还需要手动在代码初始化的地方手动注册HOOK行为。&lt;/p&gt;
&lt;p&gt;为什么不用自动注册的方式呢？&lt;/p&gt;
&lt;p&gt;在一般的项目里面，数据库是必不可少的，所以我们可以把这个行为记录进一个特定的表&lt;code&gt;plugins_hook&lt;/code&gt;中(代替&lt;code&gt;HOOK._list&lt;/code&gt; 作用)。在插件安装和删除的时候，对表&lt;code&gt;plugins_hook&lt;/code&gt; 进行更新。&lt;/p&gt;
&lt;p&gt;所以我们就无需在&lt;code&gt;view.py&lt;/code&gt; 中用&lt;code&gt;register_action&lt;/code&gt; 来注册 HOOK 行为。&lt;/p&gt;
&lt;p&gt;我们只需在 &lt;code&gt;/plugins/your-plugin/__init__.py&lt;/code&gt; 中类似地写入以下信息：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# some basic information
NAME = &apos;Plugin Name&apos;
DESCRIPTION = &apos;Ohhhhhhhhhhh&apos;
AUTHOR = &apos;Some One&apos;
EMAIL = &apos;my email&apos;

# hook register
HOOK_REGISTER = [
  (&apos;COMMENT_AFTER&apos;, &apos;send_to_commenter&apos;),
  (&apos;COMMENT_AFTER&apos;, &apos;send_to_host&apos;)
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样你的插件机制会更加完善。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;好了，整个HOOK机制就讲完了，希望本文能对你有不少帮助。&lt;/p&gt;
&lt;p&gt;PS: 如果你需要其他语言的DEMO，可以联系我，我们可以一起商讨以下。&lt;/p&gt;
</content:encoded></item><item><title>Guys, you are not the center of the world.</title><link>https://www.kilerd.me/you-are-not-the-center-of-world/</link><guid isPermaLink="true">https://www.kilerd.me/you-are-not-the-center-of-world/</guid><description>标题用英语的原因在于，用中文讲相当不雅。 这篇文章可能会引起你的不适，适当时候可以选择略过。 有些时候，你真的很难理解你身边的某些人的所做。所以他们会令你十分难受，以至心情十分不好。我不懂，真的想不明白。</description><pubDate>Sat, 25 Jun 2016 05:36:40 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;标题用英语的原因在于，用中文讲相当不雅。&lt;/p&gt;
&lt;p&gt;这篇文章可能会引起你的不适，适当时候可以选择略过。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有些时候，你真的很难理解你身边的某些人的所做。所以他们会令你十分难受，以至心情十分不好。我不懂，真的想不明白。&lt;/p&gt;
&lt;p&gt;那个时候，我还是一个社联(学生社团联合会，大学里面的一个学生社团)的小干事。那天，是一个我们部分主办的一个活动（我已经不记得到底是什么活动了）。当时为了鼓励/怂恿更多的人来参加我们这个活动，凡是报名的人都可以在我们那里拿走一样小礼物。&lt;/p&gt;
&lt;p&gt;故事的背景到此结束。主角是我跟另外一个干事，一个我相当不喜欢的干事。这里用“他”代指，以保护他人隐私。&lt;/p&gt;
&lt;p&gt;礼物我们设置了几种：一些零时某宝买的明信片，事实上这些明信片是烂大街的了；另外一种就是学校80周年的纪念胸章。&lt;/p&gt;
&lt;p&gt;没错，估计你能想到了。纪念胸章的造价是低于明信片的。所以他提议明信片应该设为一等奖的奖品。然而，我确认为纪念胸章更为合适。&lt;/p&gt;
&lt;p&gt;我清晰地记得我有跟他讲，纪念胸章的意义更大，至少比这些明信片大。&lt;/p&gt;
&lt;p&gt;他，却坚决拒绝了，因为明信片成本更高。&lt;/p&gt;
&lt;p&gt;当时我就马上发飙了。其实也没有，还是有一些前戏的：我说：”那你来管理奖品的分发吧，我不理了“，他却拦着我说，”别这样，我不是针对你“。可能就是这一句话真的让我受不了他了。&lt;/p&gt;
&lt;p&gt;讲道理，我到现在都不知道这件事谁对谁错。但起码，我从一个领奖品的人的角度去考虑了这一点。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;大学是把人养懒的。  -- 我的父上大人&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这句话是我父亲在我上大一的时候跟我讲的。为了不让父亲失望，我一直都没把大学过得像度假一般。&lt;/p&gt;
&lt;p&gt;看过我之前文章的都知道，我阴差阳错地来到了计算机专业，我之前的自学编程给我带来了很好的基础。有一次我跟某一个师兄在聊天的时候（应该是一次团队讨论）。他问了我一句话&amp;quot;上课讲的东西你都会了，那你为什么不逃课？“&lt;/p&gt;
&lt;p&gt;实话说，当时问的这个问题真的把我问得愣住了。乍一想确实有那么点道理，我仔细想了下便告诉他”有这门课，我应该给我的老师最起码的尊重，所以我不会逃课“。实际上我没有告诉他，也没有反问他”你回宿舍能干什么？除了打游戏就是看综艺节目“。&lt;/p&gt;
&lt;p&gt;这二流的学校、二流的专业，确实太少人选择在课余时间学习编程了。所以我对这个师兄相当之看不起。”师兄“一词只是给你最基本的尊重，别无他意。这就是大学养出来的懒人。&lt;/p&gt;
&lt;p&gt;当然啦，请不要误解，我不排斥玩游戏，毕竟我自己也都在玩游戏。我真正受不了的是那两类人：其一是逃课玩游戏的、其二为打起游戏来便目中无人的。&lt;/p&gt;
&lt;p&gt;我们来讨论下第二类人。玩游戏，我真的不介意，甚者，我可以在课余时间陪你玩。但是请你玩的时候，还要想想旁边还有你的舍友。老旧的宿舍就是有一个很大的设计问题，没能把私人空间考虑周全。无论是自己宿舍还是隔壁总会有那么几个人在玩游戏的时候很大声地吐嘈，或者语音对话很大声。丝毫不顾周围他人到底在做什么。有时还会很大声地问你一些无谓的东西，无论你是在认真做什么。&lt;/p&gt;
&lt;p&gt;实话讲，被打断，被打扰的感觉真的不好，不理他的话，他有时还会不乐意。真的难以理解。&lt;/p&gt;
&lt;p&gt;然后这些人习惯性地会依赖别人来告诉他一些事情，即使这些事情在QQ群里面都有公告，但是他还是要问你这，问你那的，非得你把整件事情丝毫不漏地告诉他才是你的责任来着。拜托，自己有眼，不会自己看吗？？？？&lt;/p&gt;
&lt;p&gt;无论你在哪，总能遇到这么的几个人。&lt;/p&gt;
&lt;p&gt;从小，我们就听说”己所不欲，勿施于人“的教导。但是到了大学还是有不少人......&lt;/p&gt;
&lt;p&gt;前口说你们要准时到，后面自己就迟到了，还万般借口。&lt;/p&gt;
&lt;p&gt;前一刻还指责别人这样做如何不对，后一刻自己就做了同样的事。&lt;/p&gt;
&lt;p&gt;唉，悲哀。&lt;/p&gt;
&lt;p&gt;囚犯 030 记录于 3号监狱&lt;/p&gt;
</content:encoded></item><item><title>我有5毛，你能卖我一碗鸡汤吗？</title><link>https://www.kilerd.me/i-have-stories-and-what-do-you-have/</link><guid isPermaLink="true">https://www.kilerd.me/i-have-stories-and-what-do-you-have/</guid><description>“你虽平凡，但每个人都有一部属于自己的传记” —— 某个我听说过的人 我？不知道谁来的。一般别人叫我都是那个谁。不突出、没专长、没特色就是我的特点。我只是芸芸众生中的一个普通的存在。</description><pubDate>Tue, 15 Mar 2016 15:45:27 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;“你虽平凡，但每个人都有一部属于自己的传记” —— 某个我听说过的人&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我？不知道谁来的。一般别人叫我都是那个谁。不突出、没专长、没特色就是我的特点。我只是芸芸众生中的一个普通的存在。&lt;/p&gt;
&lt;p&gt;我很悲哀，也很幸运。悲哀在于姓名只有两个字，别人怎么叫都会带有一种淡淡的距离感，永远叫不出别人的那种亲近感；幸运在于我并不喜欢热闹的场景：与其硬着头皮同人交流，不如找个安静的角落看会书。&lt;/p&gt;
&lt;p&gt;我喜欢发牢骚，但是别人并不知道。是因为我只喜欢在嘴边碎碎念叨俩句，就过去了。&lt;/p&gt;
&lt;p&gt;我记性并不太好，每当别人说起某件事的时候，永远只能附和地说声“噢噢~”，以表示我在聆听。&lt;/p&gt;
&lt;p&gt;别人都叫我理工男，说我闷骚。我没有说什么反驳的话，也没说任何表示赞同的。嘴在别人身上，解释莫过于透露着狡辩的气息。一句解释真能改变对一个人的印象吗？算了吧，我的口才远没有那么好。&lt;/p&gt;
&lt;p&gt;“出去走走吧，总宅在家不太好”、“少熬夜”、“死宅男” 嗯，这些都是平常听得最多的一些话。当然了，我很感激这些关心/嘲笑我的人，至少你们的世界里还有我曾经留下的一片影子。&lt;/p&gt;
&lt;p&gt;拿起手机，解锁，看了看只有外卖联系的通话记录，翻了翻充满10086和快递小哥发过来的催命神符，还有那几十个公众号/订阅号没读的微信。轻叹一身，又放下了手机。噢？你说QQ？不好意思，我不用QQ。&lt;/p&gt;
&lt;p&gt;几个月前，手机坏了，通讯录找不回来，便微信朋友圈发公告“手机不见了，快留联系方式”。于是通讯录自此从几十个缩减到十几个。再后来，忘记存进手机了，发现对日常生活也没有多大的影响。自此手机通讯录淡出了我的视野中。&lt;/p&gt;
&lt;p&gt;我不爱拍照，因为长得丑。若是非得拍个集体照什么的，都是习惯性地站在一个角落，露出半张脸即可。完全没有那个踮起脚，往中间挤一挤的想法。因为习惯，手机里的相册永远都是空的。&lt;/p&gt;
&lt;p&gt;对，我爱看书，基本什么书都看。感觉自己在生活中最大方的时候就是买书的那一刻了。因为自己有点轻微的洁癖，所以接受不了借书，也接受不了二手书（即使借来了，就很少拿起来看，主要嫌脏）&lt;/p&gt;
&lt;p&gt;写到了这里，也不知道自己到底想写点什么，总觉得心情不好的时候就像去说点啥，写点啥。因为身边没有人愿意跟我瞎扯淡，我只能选择了后者。日记的习惯也在慢慢地减弱了。那本一年前买的日记本还只是写了寥寥几页。那只钢笔越写越涩手。那字越写越难看。&lt;/p&gt;
&lt;p&gt;心情不好。兜里只有5毛。老板，能卖我一碗鸡汤吗？&lt;/p&gt;
</content:encoded></item><item><title>This will be the day we&apos;ve waited for</title><link>https://www.kilerd.me/This-will-be-the-day-we-ve-waited-for/</link><guid isPermaLink="true">https://www.kilerd.me/This-will-be-the-day-we-ve-waited-for/</guid><description>![RWBY][1] This will be the day we&apos;ve waited for This will be the day we open up the door 这两句话是最近比较迷上的动漫 RWBY 里面OP的头两句。感觉蛮不错的，所以就被引用来了。</description><pubDate>Mon, 22 Feb 2016 12:49:11 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;https://ssl.moefq.com/images/2016/02/22/c704e2b4869d11168fe264478bb14404.jpg&quot; alt=&quot;RWBY&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This will be the day we&apos;ve waited for
This will be the day we open up the door&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这两句话是最近比较迷上的动漫 RWBY 里面OP的头两句。感觉蛮不错的，所以就被引用来了。&lt;/p&gt;
&lt;p&gt;嗯，既然都说起来了，那就安利一下这一动漫吧。嗯，怎么说。 RWBY 就是一热血泡面番（好吧，这样说真的有点大丈夫吗？）。每集大概5、6分钟的样子，画风也不太像日漫，但是看起来却给人一种很舒服的感觉。哎呀，都说了不是因为妹纸多（捂脸&lt;/p&gt;
&lt;p&gt;好吧，这次的主题并不是动漫，大家喜欢的可以去看看&lt;a href=&quot;http://www.bilibili.com/video/av749000/&quot;&gt;RWBY第一季第八集&lt;/a&gt;，估计看完你就能明白为什么我会喜欢他了。&lt;/p&gt;
&lt;p&gt;这一次最主要的是讲下这几天我到底干了些什么，其实主要的都是博客模板的更新，毕竟被自己拖了那么久，所以才会像标题说的This will be the day we&apos;ve waited for&lt;/p&gt;
&lt;p&gt;哎呀，不想说那么多（这几天主要都玩游戏去了），更新了这几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模板的字体改了下，感觉好看些了&lt;/li&gt;
&lt;li&gt;文章内容里面换上了&lt;a href=&quot;https://github.com/lepture/yue.css&quot;&gt;yue.css&lt;/a&gt;，让文章在各个平台上都有比较好的阅读效果&lt;/li&gt;
&lt;li&gt;嗯，把域名换上了 Let&apos;s Encrypt 的证书，下一次换上ECC&lt;/li&gt;
&lt;li&gt;托管的地方又重新回到了Hostker的怀抱&lt;/li&gt;
&lt;li&gt;重新打开评论功能，也认真地写了下评论区域的样式，但是还没写完，还需要认真修一下。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大概就是这些东西，改得差不多就打包主题扔到GitHub上面（虽然不知道有没有人喜欢这个主题而已。）&lt;/p&gt;
</content:encoded></item><item><title>抓不惯的笔，写不出的字</title><link>https://www.kilerd.me/that-pen-that-word/</link><guid isPermaLink="true">https://www.kilerd.me/that-pen-that-word/</guid><description>高中那些年，有幸进入了一家不错的高中，地域也很偏僻，能够安静地认真读一读书。也是在那个地方，自己真正地爱上了文字。 那里的宿舍堪称豪华，几乎可以称得上是学生宿舍的最高标准。自己唯独最中意的一点便是上床下铺的设计。还记得我们（那些舍友死党）把差不多干的衣服往床边一挂，人往里面一坐，开盏台灯，就可以奋斗到天亮。当然我很少那</description><pubDate>Tue, 26 Jan 2016 15:19:17 GMT</pubDate><content:encoded>&lt;p&gt;高中那些年，有幸进入了一家不错的高中，地域也很偏僻，能够安静地认真读一读书。也是在那个地方，自己真正地爱上了文字。&lt;/p&gt;
&lt;p&gt;那里的宿舍堪称豪华，几乎可以称得上是学生宿舍的最高标准。自己唯独最中意的一点便是上床下铺的设计。还记得我们（那些舍友死党）把差不多干的衣服往床边一挂，人往里面一坐，开盏台灯，就可以奋斗到天亮。当然我很少那么勤奋，像我那么懒的人至多躲在里面玩玩手机，写写情书罢。可惜最后那情书还是只有自己看到。&lt;/p&gt;
&lt;p&gt;我喜欢的，是一个人躲在里面自己的空间。也真是这样，我逐渐喜欢上了写日记。&lt;/p&gt;
&lt;p&gt;我本是一个不太擅长，也不太喜欢交谈的人。成语总是用错，也成了别人调侃的理由和对象。笔记本算是我自己对自己控诉的一个唯一方法。日记我总是写不长，因为我的语文作文一直写得不好，写长了会偏题，会越写越远，感觉总是想诉说出这些年来所遇到的不公这般。&lt;/p&gt;
&lt;p&gt;高中嘛，每天就那样，除了读书就是读书，能记下来的真的不多。每天的日记就是早上一句话，晚上几句话。偶尔吧，能写出一篇长一点的也都是对某某人的思念。你想下，一个死读书的人哪能找到那么可以写下来的东西啊。&lt;/p&gt;
&lt;p&gt;后来的后来，日子总算过了一些，高中的日子也大概过了有半。自己那点自以为是的小智慧没办法轻松应付考试了，需要把时间放在学习上了，日记也慢慢写得少了。&lt;/p&gt;
&lt;p&gt;拿出笔记本，坐在那里发了一会呆，便收了起来，发现没有什么值得写下来的。笔记本也就在那角落积了好一段时间的灰尘。&lt;/p&gt;
&lt;p&gt;直到某一天，整理桌面，发现了它的存在。翻开看了看，脸上既挂着笑容也有苦涩的眼泪。于是又重新地把它放在了某个地方的某个角落。&lt;/p&gt;
&lt;p&gt;从那时起，玩手机的时间开始逐渐增多。遇到了很多事也开始喜欢在朋友圈发发牢骚罢：“今天，你好美”。被取代的便是不再在笔记本里面写下你的美貌和对你的情话。把所思所想都深深地埋在那眼神里。&lt;/p&gt;
&lt;p&gt;笔记本便这样还剩下一半空白，高中就匆匆地离去了。&lt;/p&gt;
&lt;p&gt;在一次失意的高考，取得了一次失意的成绩，去了一所失意的大学，进了一个失意的专业。&lt;/p&gt;
&lt;p&gt;大学，令人懒惰的时间。&lt;/p&gt;
&lt;p&gt;宿舍是80年代起得，那残破的程度真的不敢多想。这些都好说，唯独在意的是缺少了一个属于自己的小黑屋。&lt;/p&gt;
&lt;p&gt;自此那本笔记本再也没有打开过了，取而代之的便是在博客上面随便写一写。可是，电子这种东西谁说得清楚呢？专业的原因让我很久没有认真地抓起过一只笔。我也逐渐忘记了在写笔记的那份宁静。&lt;/p&gt;
&lt;p&gt;虽然，挑了一只很喜欢的钢笔，抓在手上，面对着那空白的一页，心中楞是想不出任何一个可以写下来的字。&lt;/p&gt;
&lt;p&gt;这感觉，怎么说。就像你很喜欢一个人，心里想了很多话，可是一见面就憋不出一句话的感觉。&lt;/p&gt;
&lt;p&gt;总之，心里的那些字没能遇到一只相应的笔，故写不出。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;又水完了一篇文章，本来算是写在跨年夜的，却拖到了现在，真的是没救了。&lt;/p&gt;
&lt;p&gt;看完，读者如果你能看到这里有所感想，又觉得感觉烂尾了，那么，干杯，我们是同路人。&lt;/p&gt;
</content:encoded></item><item><title>要么孤独，要么平庸</title><link>https://www.kilerd.me/beyond-or-stand/</link><guid isPermaLink="true">https://www.kilerd.me/beyond-or-stand/</guid><description>秋风起，秋风落，便是一年；睁眼，闭眼，便是一天。时间消逝之快，我们很多时候难以想象，只有事后回想，才知晓。 有人说过，人的一生大概只有1000个月的时间。如果我们试图画一个表去记录这1000个月，每过一个月就画去一个，这样就可以感受到时间的逝去。</description><pubDate>Thu, 26 Nov 2015 11:43:21 GMT</pubDate><content:encoded>&lt;p&gt;秋风起，秋风落，便是一年；睁眼，闭眼，便是一天。时间消逝之快，我们很多时候难以想象，只有事后回想，才知晓。&lt;/p&gt;
&lt;p&gt;有人说过，人的一生大概只有1000个月的时间。如果我们试图画一个表去记录这1000个月，每过一个月就画去一个，这样就可以感受到时间的逝去。&lt;/p&gt;
&lt;p&gt;那么在这短短的那么多个月里面，我们能干嘛？在我身边，有着很多这样的人：白天上课睡觉、玩手机，下课就玩游戏直至深夜。当然，我并不是批判这种生活，也不是看不起他们。毕竟这是每个人的选择，我们并没有太多的理由去插手他们的人生。&lt;/p&gt;
&lt;p&gt;并不然，我们也没有太多的特殊，也都是循规蹈矩地度过这些年：该学习的学习，该考试的考试。我们面临的、处理的也都是一样的事情。&lt;/p&gt;
&lt;p&gt;并不是所有人都有魄力去打破这些规矩，从而去寻找那真正属于自己的人生，在我看来，他们才是那些真正能批评其他人的人。虽说他们不一定都会成为其他人眼中的“成功人士”，但是无论如何他们在走出他们第一步的时候就已经成功了。&lt;/p&gt;
&lt;p&gt;“站在巨人的肩膀上”，这是我们自小便被教导出来的成功之道。没错，我当然不会否认那些已经成功爬上肩膀人士的辉煌，同时我也不能忘记那极大部分摔下来的人，哪怕就差一步便成功，始终无法让人记住。&lt;/p&gt;
&lt;p&gt;我相信凡是接受过义务教育的人都会记得一句话&amp;quot;燕雀安知鸿鹄之志&amp;quot;，然而我已经无法回忆起出自哪书，出自谁口。还记得第一次学这篇文章的时候，我们的反应基本都是&amp;quot;这人好厉害，好有想法&amp;quot;，这就是我想说的:尝试去成为一名巨人，让他人站在你的肩膀。或许，有无数人曾经都有过这样一个想法，然后却被时间冲走了任何行动的想法，包括我。&lt;/p&gt;
&lt;p&gt;我尝试着不把这篇文章写成一篇鸡汤，也不是想怂恿他人退学什么的。这篇文章的意图在于试图唤醒你那被时光遗忘的曾经的梦想，试图让你在学习或者工作中多一份动力去追梦。这样就够了。当然，其中的受众也包括笔者我。&lt;/p&gt;
&lt;p&gt;小时候，我们都敢大声地说出“我以后要当个科学家”之类的话，反而长大了就再也听不到这样的话，更多的我们是在想“算了，就这样吧”。&lt;/p&gt;
&lt;p&gt;孤独的人或许说不出为什么孤独，平庸的人绝对有平庸的缘由。在我看来平庸与平凡有着天壤之别。我可以平凡，但我拒绝平庸。&lt;/p&gt;
&lt;p&gt;要么康庄大道，门庭若市。&lt;/p&gt;
&lt;p&gt;要么林间小路，门可罗雀。&lt;/p&gt;
</content:encoded></item><item><title>[译文]如何使用Flask部署大型应用？</title><link>https://www.kilerd.me/how-to-deploy-flask/</link><guid isPermaLink="true">https://www.kilerd.me/how-to-deploy-flask/</guid><description>原文地址： https://github.com/mitsuhiko/flask/wiki/Large-app-how-to 译者：本译文已经违背了原文的意图，请勿加以转载。此文仅用于个人使用。 这篇文章并不是官方的！它包含了很多非官方资源的建议并且没有通过一系列的测试（审查）。这里描述的写法可能很有用，但是同时它也可</description><pubDate>Tue, 14 Jul 2015 15:30:18 GMT</pubDate><content:encoded>&lt;p&gt;原文地址： &lt;a href=&quot;https://github.com/mitsuhiko/flask/wiki/Large-app-how-to&quot;&gt;https://github.com/mitsuhiko/flask/wiki/Large-app-how-to&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;译者：本译文已经违背了原文的意图，请勿加以转载。此文仅用于个人使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;这篇文章并不是官方的！它包含了很多非官方资源的建议并且没有通过一系列的测试（审查）。这里描述的写法可能很有用，但是同时它也可能很危险。请记住，不要在本文档添加任何附加信息。或者，引用在你的网站或者博客。这篇文章之所以保存，是因为很多&lt;code&gt;StackOverflow&lt;/code&gt;答案指向了这里&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这篇文章试图去描述一个由Flask和一些基础的模块组成的大型应用的第一步。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQLAlchemy&lt;/li&gt;
&lt;li&gt;WTForms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请随时可以修改和添加自己的Tips&lt;/p&gt;
&lt;h1&gt;安装&lt;/h1&gt;
&lt;h2&gt;Flask&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;http://flask.pocoo.org/docs/installation/&quot;&gt;Flask Installation&lt;/a&gt;
我建议使用&lt;code&gt;virtualenv&lt;/code&gt;:它是一个简单并且允许你在同一个机器上执行多个环境的玩意，甚至，他不需要机器上的超级权限(root)，近警示作为一个库文件安装在本地。&lt;/p&gt;
&lt;h2&gt;Flask-SQLAlchemy&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;SQLAlchemy&lt;/code&gt;提供了一个简单而快捷的方式去映射你的对象到不同的关系型数据库。在&lt;code&gt;virtualenv&lt;/code&gt;里面用&lt;code&gt;pip&lt;/code&gt;安装&lt;code&gt;Flask-SQLAlchemy&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install flask-sqlalchemy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;http://packages.python.org/Flask-SQLAlchemy/&quot;&gt;更多关于Flask-SQLAlchemy&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;Flask-WTF&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;WTForms&lt;/code&gt;提供了一个简单的方式去处理用户提交的数据&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;pip install Flask-WTF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;http://packages.python.org/Flask-WTF/&quot;&gt;更多关于Flask-WTF&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;概述&lt;/h1&gt;
&lt;p&gt;好了，到了现在，我们就准备好了所需的所有库。下面是应用的文件夹结构。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/config.py
/run.py
/shell.py 
/app.db
/app/__init__.py
/app/constants.py
/app/static/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于每一个模块(或者子应用)都会有这样的文件结构(这里展示的是&lt;code&gt;users&lt;/code&gt;模块)&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/app/users/__init__.py
/app/users/views.py
/app/users/forms.py
/app/users/constants.py
/app/users/models.py
/app/users/decorators.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于每一个模块，都需要用到模版(jinja)，所以我们以&lt;code&gt;模版文件夹 + 模块目录&lt;/code&gt;的形式进行储存。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/app/templates/404.html
/app/templates/base.html
/app/templates/users/login.html
/app/templates/users/register.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们本应该使用一个专门的http服务来提供静态文件，但是随着科技的发展，我们可以让Flask来完成这项工作。Flask会自动地从&lt;code&gt;static/&lt;/code&gt;文件夹里面读取静态文件。如果你想使用其他文件夹，那么你可以阅读这篇文章：
&lt;a href=&quot;http://flask.pocoo.org/docs/api/#application-object&quot;&gt;http://flask.pocoo.org/docs/api/#application-object&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;/app/static/js/main.js
/app/static/css/reset.css
/app/static/img/header.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将会创建4个模块：用户模块（用于管理用户的注册、登陆、找回密码、信息修改，甚至第三方登陆/注册）、运用列队服务的邮件模块、文章和评论模块。&lt;/p&gt;
&lt;h2&gt;配置&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/run.py&lt;/code&gt;用于启动网站服务器&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from app import app
app.run(debug=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/shell.py&lt;/code&gt;将会打开一个Flask环境的控制台。在这个环境下，或许用pdb执行调试并不太理想，但是总是有用的（当你初始化你的数据库时）&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;#!/usr/bin/env python
import os
import readline
from pprint import pprint

from flask import *
from app import *

os.environ[&apos;PYTHONINSPECT&apos;] = &apos;True&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;config.py&lt;/code&gt;储存了所有模块的配置信息。这次，我们将使用&lt;code&gt;SQLite&lt;/code&gt;数据库，因为他是十分简单、易用。很可能&lt;code&gt;/config.py&lt;/code&gt;不会是仓库的一部分，因为在测试和生产环境是不同的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import os
_basedir = os.path.abspath(os.path.dirname(__file__))

DEBUG = False

ADMINS = frozenset([&apos;youremail@yourdomain.com&apos;])
SECRET_KEY = &apos;This string will be replaced with a proper key in production.&apos;

SQLALCHEMY_DATABASE_URI = &apos;sqlite:///&apos; + os.path.join(_basedir, &apos;app.db&apos;)
DATABASE_CONNECT_OPTIONS = {}

THREADS_PER_PAGE = 8

CSRF_ENABLED = True
CSRF_SESSION_KEY = &amp;quot;somethingimpossibletoguess&amp;quot;

RECAPTCHA_USE_SSL = False
RECAPTCHA_PUBLIC_KEY = &apos;6LeYIbsSAAAAACRPIllxA7wvXjIE411PfdB2gt2J&apos;
RECAPTCHA_PRIVATE_KEY = &apos;6LeYIbsSAAAAAJezaIq3Ft_hSTo0YtyeFG-JgRtu&apos;
RECAPTCHA_OPTIONS = {&apos;theme&apos;: &apos;white&apos;}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;_basedir&lt;/code&gt;读取脚本运行所在的目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DEBUG&lt;/code&gt;说明这是一个开发环境。当发生错误时，你会从Flask得到一个非常有用的错误页面&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SECRET_KEY&lt;/code&gt;用于加密Cookies。当这个值改变时，你的用户需要重新登录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ADMINS&lt;/code&gt;会被调用，当你需要发邮件给网站管理员时&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SQLALCHEMY_DATABASE_URI&lt;/code&gt; 和 &lt;code&gt;DATABASE_CONNECT_OPTIONS&lt;/code&gt; 是SQLAlchemy连接信息（建议高强度）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;THREAD_PAGE&lt;/code&gt; 我的理解是2/核心数，或许这是错误的理解&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CSRF_ENABLED&lt;/code&gt; 和 &lt;code&gt;CSRF_SESSION_KEY&lt;/code&gt;用于防止异常操作的POST&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RECAPTCHA_*&lt;/code&gt; 将使用自带&lt;code&gt;RecaptchaField&lt;/code&gt;的WTForms，用于验证网站和公钥、私钥。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;第一个模块&lt;/h1&gt;
&lt;p&gt;我们将开始写用户模块。为此，我们将会定义一些模型、用于模型的常数、表单，最后是第一个视图和模版。&lt;/p&gt;
&lt;h2&gt;第一个模型（和它的常数文件）&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;/app/users/models.py&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from app import db
from app.users import constants as USER

class User(db.Model):

    __tablename__ = &apos;users_user&apos;
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True)
    email = db.Column(db.String(120), unique=True)
    password = db.Column(db.String(120))
    role = db.Column(db.SmallInteger, default=USER.USER)
    status = db.Column(db.SmallInteger, default=USER.NEW)

    def __init__(self, name=None, email=None, password=None):
        self.name = name
        self.email = email
        self.password = password

    def getStatus(self):
        return USER.STATUS[self.status]

    def getRole(self):
        return USER.ROLE[self.role]

    def __repr__(self):
        return &apos;&amp;lt;User %r&amp;gt;&apos; % (self.name)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;/app/users/constants.py&lt;/code&gt;文件中的常数:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# User role
ADMIN = 0
STAFF = 1
USER = 2
ROLE = {
    ADMIN: &apos;admin&apos;,
    STAFF: &apos;staff&apos;,
    USER: &apos;user&apos;,
}

# user status
INACTIVE = 0
NEW = 1
ACTIVE = 2
STATUS = {
    INACTIVE: &apos;inactive&apos;,
    NEW: &apos;new&apos;,
    ACTIVE: &apos;active&apos;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;译文未完成&lt;/p&gt;
</content:encoded></item><item><title>基于证书权限的电脑应用商场</title><link>https://www.kilerd.me/app-store-based-on-cert/</link><guid isPermaLink="true">https://www.kilerd.me/app-store-based-on-cert/</guid><description>现在，电脑应用的流氓推广越来越严重，因此，出现了用删除百毒证书和数字公司证书来防止应用的恶意安装。 那么基于这个条件，我们可以设想出一个很理想的应用商场。 先把电脑的所有证书删了，意味着，所有应用都不能通过自行安装的形式入侵到你的电脑。 然后电脑应用需要在应用商场上面做登记，领取应用标识码（应用ID） 在确认安装的时候</description><pubDate>Sun, 07 Jun 2015 05:09:31 GMT</pubDate><content:encoded>&lt;p&gt;现在，电脑应用的流氓推广越来越严重，因此，出现了用删除百毒证书和数字公司证书来防止应用的恶意安装。&lt;/p&gt;
&lt;p&gt;那么基于这个条件，我们可以设想出一个很理想的应用商场。&lt;/p&gt;
&lt;p&gt;先把电脑的所有证书删了，意味着，所有应用都不能通过自行安装的形式入侵到你的电脑。&lt;/p&gt;
&lt;p&gt;然后电脑应用需要在应用商场上面做登记，领取应用标识码（应用ID）&lt;/p&gt;
&lt;p&gt;在确认安装的时候，应用商场就会往你的电脑里面写入一个一次性的证书（不知道现在的OpenSSL能不能做到这点，不然又要自己去开坑了）。&lt;/p&gt;
&lt;p&gt;有了这个一次性的证书，应用就可以安装，安装完之后就自动销毁。&lt;/p&gt;
</content:encoded></item><item><title>如果，哭可以解决问题</title><link>https://www.kilerd.me/if-i-can-cry/</link><guid isPermaLink="true">https://www.kilerd.me/if-i-can-cry/</guid><description>我们的出生就被称为了“呱呱坠地”，说明哭是我们与生俱来的。不需要谁来教，谁也教不了。 然后，这么一个与生俱来的能力，却被冠上了小孩专属的称号。摔倒了，哭一下，等爸妈来安慰自己；刮伤了，哭一下，等爸妈来吹吹伤口。这些温馨而和谐的画面，永远都是以小孩为主角。 长大了，坚强了。坚强，一个自欺欺人的词，没有人愿意选择，或者说喜</description><pubDate>Sat, 30 May 2015 12:22:36 GMT</pubDate><content:encoded>&lt;p&gt;我们的出生就被称为了“呱呱坠地”，说明哭是我们与生俱来的。不需要谁来教，谁也教不了。&lt;/p&gt;
&lt;p&gt;然后，这么一个与生俱来的能力，却被冠上了小孩专属的称号。摔倒了，哭一下，等爸妈来安慰自己；刮伤了，哭一下，等爸妈来吹吹伤口。这些温馨而和谐的画面，永远都是以小孩为主角。&lt;/p&gt;
&lt;p&gt;长大了，坚强了。坚强，一个自欺欺人的词，没有人愿意选择，或者说喜欢，受伤了不需要人安慰就自己当作什么事都没发生；没有人喜欢眼泪在眼眶里打滚就要留下来了，却只能默默咬紧嘴唇，忍住哭声。&lt;/p&gt;
&lt;p&gt;自小，每当我们哭，爸妈除了安慰之外，都会教导我们，哭的样子很丑，哭不能解决问题。&lt;/p&gt;
&lt;p&gt;我们都是好孩子，长大了，都记得了爸妈的叮嘱，所以，我们不哭了。&lt;/p&gt;
&lt;p&gt;长大，在任何人眼中，都不只是两个字那么简单。我们再也不能蹲在路边玩玩泥巴就是一整天；也不能跟小伙伴过家家一整天；与小伙伴就不只是跟你玩和不跟你玩了。&lt;/p&gt;
&lt;p&gt;长大了，遇到了很多事情，委屈了，想起了爸妈的话，忍住了，于是就开始一个人的惆怅，没处安慰。&lt;/p&gt;
&lt;p&gt;而后，我们选择了很多的代替方法。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;兄弟，我失恋了&lt;/code&gt; &lt;code&gt;行，你在哪里？我们去劈酒&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;哥们，我失业了&lt;/code&gt; 掏出一支烟 &lt;code&gt;吶，先点上&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;遇到了很多事，不能用哭解决，可是喝完了，吸完了，还得哭，不过是一个人埋在被窝里面，无人知晓吧了。&lt;/p&gt;
&lt;p&gt;这就是我们被教导出来的坚强。&lt;/p&gt;
&lt;p&gt;无时无刻，我们都希望着，能像小时候那样，哭一下就会有人来帮我们解决问题，布娃娃坏了，哭一下，睡一下，娃娃就好了；摔伤了，哭一下，就有人帮我们处理好伤口，不痛了。多么美好。&lt;/p&gt;
&lt;p&gt;可是我们做不到，我们不愿意被冠上懦弱、神经质的舆论。&lt;/p&gt;
&lt;p&gt;当然，我们不再是孩子。&lt;/p&gt;
&lt;p&gt;可是&lt;/p&gt;
&lt;p&gt;如果，哭可以解决问题，那么，我会是世上最擅长哭的孩子。&lt;/p&gt;
</content:encoded></item><item><title>20150420 V2EX备案日</title><link>https://www.kilerd.me/20150420-v2ex-change/</link><guid isPermaLink="true">https://www.kilerd.me/20150420-v2ex-change/</guid><description>关于V2EX，我去那里差不多一年了。V2EX，一个创意工作者的论坛。 我很喜欢那里的氛围，因为它不像大部分国内论坛那样，全都是水贴。在V2EX，我能找到很多很有想法的人，也能看到其他人很奇妙很神奇的想法。这样十分享受的氛围，使我基本每天都在刷V2EX，这也是我手机除了用于通讯以外最大的用处了。 不知道什么时候开始，C大</description><pubDate>Mon, 20 Apr 2015 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;关于V2EX，我去那里差不多一年了。V2EX，一个创意工作者的论坛。&lt;/p&gt;
&lt;p&gt;我很喜欢那里的氛围，因为它不像大部分国内论坛那样，全都是水贴。在V2EX，我能找到很多很有想法的人，也能看到其他人很奇妙很神奇的想法。这样十分享受的氛围，使我基本每天都在刷V2EX，这也是我手机除了用于通讯以外最大的用处了。&lt;/p&gt;
&lt;p&gt;不知道什么时候开始，C大的ShadowSocks在V2EX里面流行了起来，让V2EX涌入了很多销售SS的用户，小白也慢慢增多。&lt;/p&gt;
&lt;p&gt;于是乎，在那段时间里面V2EX很多R2帖子都是关于SS的，让很多很有创意的帖子就这样沉了下去。这样让我很不爽。&lt;/p&gt;
&lt;p&gt;终于，Government盯上了ShadowSocks，于是V2EX走上了黑暗时代，当然这仅仅是对小白来说了。不过在我们这些真正喜欢上V2EX的人来说，这是一件好事，因为Government帮我们过滤了大部分小白，因为他们跨不过GFW。这样我们就能真真正正地讨论创意，讨论技术。&lt;/p&gt;
&lt;p&gt;可是今天，L大把V2EX备案了，虽然说这是一件好事，因为再也不用爬梯子了，但是这同时也意味着有很多话题不能在上面讨论了，毕竟所有数据都可能会移交Government。&lt;/p&gt;
&lt;p&gt;当然，也不是所有的都会，只是希望L大能保持住自己的风度，一直为我们这些撑V2EX的用户们维持最后一道数据的墙，这样才能让我们对V2EX的热情不减。&lt;/p&gt;
</content:encoded></item><item><title>新的博客</title><link>https://www.kilerd.me/new-blog/</link><guid isPermaLink="true">https://www.kilerd.me/new-blog/</guid><description>之前的那个域名不小心就没有续费成功。 所以就像，既然都过去了，那还不如重新开始。 之前的那些文章也不要算了，部分看着也还揪心。 这个博客存在的意义就是在 简约记 还没有集成博客功能的时候用于记录自己的文档吧。</description><pubDate>Sat, 18 Apr 2015 13:16:11 GMT</pubDate><content:encoded>&lt;p&gt;之前的那个域名不小心就没有续费成功。&lt;/p&gt;
&lt;p&gt;所以就像，既然都过去了，那还不如重新开始。&lt;/p&gt;
&lt;p&gt;之前的那些文章也不要算了，部分看着也还揪心。&lt;/p&gt;
&lt;p&gt;这个博客存在的意义就是在 简约记 还没有集成博客功能的时候用于记录自己的文档吧。&lt;/p&gt;
</content:encoded></item></channel></rss>