<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Blessing Studio</title>
  
  <subtitle>半吊子全栈开发者的日常</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://blessing.studio/"/>
  <updated>2018-02-20T11:23:47.000Z</updated>
  <id>https://blessing.studio/</id>
  
  <author>
    <name>printempw</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>如何将现有 git 仓库中的子目录分离为独立仓库并保留其提交历史</title>
    <link href="https://blessing.studio/splitting-a-subfolder-out-into-a-new-git-repository/"/>
    <id>https://blessing.studio/splitting-a-subfolder-out-into-a-new-git-repository/</id>
    <published>2018-02-20T11:23:47.000Z</published>
    <updated>2018-02-20T11:23:47.000Z</updated>
    
    <content type="html"><![CDATA[<p>这几天想要把一个 git 仓库中<strong>已经存在</strong>的一个子文件夹独立成一个新的 git 仓库，并且保留之前关于此文件夹的所有提交历史。不过我对 git 并没有这么精通，只好上网搜索之。可能是因为我关键词抓得不准，搜了好一会儿才找到可行的方案，所以写篇博文记录一下，希望能帮到后来人。</p><p>另外，在 git 里这种掌控历史的感觉真棒（笑）</p><h2 id="0x01-需求分析"><a href="#0x01-需求分析" class="headerlink" title="0x01 需求分析"></a>0x01 需求分析</h2><p>我为什么会有如本文标题所述这样的需求呢？这是因为我之前把所有为 <a href="https://github.com/printempw/blessing-skin-server" target="_blank" rel="noopener">Blessing Skin</a> 这个程序编写的插件源码都放在一个 <a href="https://github.com/printempw/blessing-skin-plugins" target="_blank" rel="noopener">git repo</a> 中了，每个子文件夹中都是一个独立的插件（因为嫌麻烦所以一股脑给塞进一个仓库里了），并且对每个子文件夹中的代码的修改最后都是在这个统一仓库中提交的。该仓库差不多长这样：</p><pre><code class="text">$ tree├── .git├── avatar-api├── config-generator├── register-email-validation│   ├── bootstrap.php│   ├── package.json│   └── src├── report-texture└── yggdrasil-api  &lt;---【我想把这个独立为一个新 repo】    ├── bootstrap.php    ├── package.json    ├── routes.php    └── src</code></pre><p>而我现在后悔了，想把其中的某个子目录抽离出来，把它变成一个新的 git 仓库，并且保留我之前所有在「原仓库」中关于这个子目录的「所有提交历史」。</p><p>其实这种需求还是挺常见的，举个栗子：</p><a id="more"></a><blockquote><p>你原本在一个项目的 git 仓库中维护了一个通用的组件库，本来以为这只是个小玩意，谁曾想随着项目的开发这个库变得越来越大，代码变得越来越复杂，不再合适与主项目代码放在同一个 repo 里了。</p><p>这时你想把这个库抽离出来变成一个单独的 git repo 然后在原 repo 中使用 submodule 之类的方法引用之的时候，却发现之前的 repo 中已经有太多关于这个库的提交记录了，而你又不想让这个新 repo 直接一个 Initial Commit 唐突地就变成现在这个样子……</p></blockquote><p>这就是这篇文章所希望解决的需求：</p><p><strong>将现有 git repo 中的子目录独立为新 repo，并保留其相关的提交历史。</strong></p><h2 id="0x02-文章描述约定"><a href="#0x02-文章描述约定" class="headerlink" title="0x02 文章描述约定"></a>0x02 文章描述约定</h2><p>为了方便描述后续操作，这里稍微约定一下文章中各占位符的含义。</p><ul><li>原来的仓库 👉 <code>&lt;big-repo&gt;</code></li><li>想要分离出来的子文件夹名称 👉 <code>&lt;name-of-folder&gt;</code></li><li>该子文件夹形成的新仓库 👉 <code>&lt;new-repo&gt;</code></li></ul><p>也就是说：我们有一个叫做 <code>big-repo</code> 的仓库，里面有不少子文件夹，我们想要把其中一个文件夹抽离出来，将其变成一个新的仓库 <code>new-repo</code>，并且保留之前在 <code>big-repo</code> 中所有关于这个子文件夹的所有 commit 记录。</p><p>差不多就是这样。(・_ゝ・)</p><h2 id="0x03-最简单的方法，使用-git-subtree"><a href="#0x03-最简单的方法，使用-git-subtree" class="headerlink" title="0x03 最简单的方法，使用 git subtree"></a>0x03 最简单的方法，使用 git subtree</h2><p>看来上述需求还是比较普遍的，自从 1.8 版本之后 git 就添加了 subtree 子命令，使用这个新命令我们可以很简单高效地解决这个问题。</p><p>首先，进入 <code>big-repo</code> 所在的目录，运行：</p><pre><code class="bash">git subtree split -P &lt;name-of-folder&gt; -b &lt;name-of-new-branch&gt;</code></pre><p>运行后，git 会遍历原仓库中所有的历史提交，挑选出与指定路径相关的 commit 并存入名为 <code>name-of-new-branch</code> 的临时分支中。另外需要注意的是，<strong>如果你在使用 Windows</strong>，且该文件夹深度 &gt; 1，你必须使用斜杠 <code>/</code> 作为目录分隔符而不是默认的反斜杠 <code>\</code>。</p><p>然后，我们创建一个新的 git 仓库：</p><pre><code class="bash">mkdir &lt;new-repo&gt;git init</code></pre><p>接着把原仓库中的临时分支拉到新仓库中：</p><pre><code class="bash">git pull &lt;/path/to/big-repo&gt; &lt;name-of-new-branch&gt;</code></pre><p>好了，完成。现在看看你的新仓库，是不是已经包含了原子文件夹中的所有文件和你之前在原仓库中的所有提交历史呢？</p><h2 id="0x04-麻烦点的方法，使用-git-filter-branch"><a href="#0x04-麻烦点的方法，使用-git-filter-branch" class="headerlink" title="0x04 麻烦点的方法，使用 git filter-branch"></a>0x04 麻烦点的方法，使用 git filter-branch</h2><p>除了使用新添加的 <code>subtree</code> 命令，你也可以使用 git 传统的所谓核弹级大杀器命令 —— <code>filter-branch</code> 解决上述问题。</p><p>首先，clone 一份原仓库并删掉原来的 remote：</p><pre><code class="bash">git clone &lt;big-repo&gt; &lt;new-repo&gt;cd &lt;new-repo&gt;git remote rm origin</code></pre><p>然后运行如下命令（这是重点）：</p><pre><code class="bash">git filter-branch --tag-name-filter cat --prune-empty --subdirectory-filter &lt;name-of-folder&gt; -- --all</code></pre><p>这条命令同样会过滤所有历史提交，只保留所有对指定子目录有影响的提交，并将该子目录设为该仓库的根目录。这里说明各下个参数的作用：</p><ul><li><code>--tag-name-filter</code> 该参数控制我们要如何处理旧的 tag，cat 即表示原样输出；</li><li><code>--prune-empty</code> 删除空的（对子目录没有影响的）提交；</li><li><code>--subdirectory-filter</code> 指定子目录路径；</li><li><code>-- --all</code> 该参数必须跟在 <code>--</code> 后面，表示对所有分支进行操作。如果你只想保存当前分支，也可以不添加此参数。</li></ul><p>该命令执行完毕后就可以看到新仓库中已经变成子目录的内容了，且保留了关于该子目录所有的提交历史。不过只是这样的话新仓库中的 <code>.git</code> 目录里还是保存有不少无用的 object，我们需要将其清除掉以减小新仓库的体积（如果你用上面 <code>subtree</code> 的方法的话是不需要执行这一步的）。</p><pre><code class="bash">git reset --hardgit for-each-ref --format=&quot;%(refname)&quot; refs/original/ | xargs -n 1 git update-ref -dgit reflog expire --expire=now --allgit gc --aggressive --prune=now</code></pre><p>这样，虽然麻烦点，我们也得到了和使用 0x03 方法后一样的新仓库。</p><h2 id="0x05-清理原仓库"><a href="#0x05-清理原仓库" class="headerlink" title="0x05 清理原仓库"></a>0x05 清理原仓库</h2><p>既然所指定的子文件夹已经被分离为一个单独的 git repo 了，我们就可以放心地在原仓库中删除它了：</p><pre><code class="bash">git rm -rf &lt;name-of-folder&gt;# 提交一下说明对应操作git commit -m &#39;Remove some fxxking shit&#39;# 删除刚才创建的临时分支# 后一种方法不需要执行这一步git branch -D &lt;name-of-new-branch&gt;</code></pre><p>不过这种方法还是会在提交历史中保留所有关于这个子目录的内容，如果你想要把这个子目录从原 repo 中<strong>不留一丝痕迹地完全移除</strong>，那你需要 BFG Repo Cleaner 这样的工具或者使用 <code>filter-branch</code> 等命令。</p><p>关于这个的具体操作我这里就不提了，网上一搜一大把。不过需要注意的是，这种做法并不值得提倡，请在你完全清楚自己在做什么的前提下使用此方法改写提交历史。</p><h2 id="0x06-关联原仓库与新仓库"><a href="#0x06-关联原仓库与新仓库" class="headerlink" title="0x06 关联原仓库与新仓库"></a>0x06 关联原仓库与新仓库</h2><p>这一步是可选的。</p><p>一般来说，在我们把原目录中的子文件夹分离成独立的 git 仓库后，总会希望再通过某种方法在原仓库中引用新仓库的代码。</p><p>这里我们可以通过 <code>subtree</code> 或者 <code>submodule</code> 两种命令来实现，不过他们两个各有优点和缺点，所以请根据你自己的实际情况选择（不过现在一般都推荐使用 subtree，submodule 用起来实在是太他妈的蛋疼了）。</p><p>当然，你也可以分离之后直接使用 npm、composer 之类的包管理器将新仓库作为一个依赖库引入进来，这也是完全没有问题的。</p><h2 id="0x07-参考链接"><a href="#0x07-参考链接" class="headerlink" title="0x07 参考链接"></a>0x07 参考链接</h2><ul><li><a href="https://stackoverflow.com/questions/359424/detach-move-subdirectory-into-separate-git-repository" target="_blank" rel="noopener">Detach (move) subdirectory into separate Git repository</a></li><li><a href="https://stackoverflow.com/questions/17413493/create-a-submodule-repository-from-a-folder-and-keep-its-git-commit-history" target="_blank" rel="noopener">Create a submodule repository from a folder and keep its git commit history</a></li><li><a href="http://graycarl.me/blog/make-a-directory-into-git-submodule" target="_blank" rel="noopener">如何把 GIT 仓库的子目录独立为子模块</a></li></ul><p>啊，另外，上次说好的 WSL 博文可能要鸽了抱歉咕咕咕。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;这几天想要把一个 git 仓库中&lt;strong&gt;已经存在&lt;/strong&gt;的一个子文件夹独立成一个新的 git 仓库，并且保留之前关于此文件夹的所有提交历史。不过我对 git 并没有这么精通，只好上网搜索之。可能是因为我关键词抓得不准，搜了好一会儿才找到可行的方案，所以写篇博文记录一下，希望能帮到后来人。&lt;/p&gt;
&lt;p&gt;另外，在 git 里这种掌控历史的感觉真棒（笑）&lt;/p&gt;
&lt;h2 id=&quot;0x01-需求分析&quot;&gt;&lt;a href=&quot;#0x01-需求分析&quot; class=&quot;headerlink&quot; title=&quot;0x01 需求分析&quot;&gt;&lt;/a&gt;0x01 需求分析&lt;/h2&gt;&lt;p&gt;我为什么会有如本文标题所述这样的需求呢？这是因为我之前把所有为 &lt;a href=&quot;https://github.com/printempw/blessing-skin-server&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Blessing Skin&lt;/a&gt; 这个程序编写的插件源码都放在一个 &lt;a href=&quot;https://github.com/printempw/blessing-skin-plugins&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;git repo&lt;/a&gt; 中了，每个子文件夹中都是一个独立的插件（因为嫌麻烦所以一股脑给塞进一个仓库里了），并且对每个子文件夹中的代码的修改最后都是在这个统一仓库中提交的。该仓库差不多长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;text&quot;&gt;$ tree
├── .git
├── avatar-api
├── config-generator
├── register-email-validation
│   ├── bootstrap.php
│   ├── package.json
│   └── src
├── report-texture
└── yggdrasil-api  &amp;lt;---【我想把这个独立为一个新 repo】
    ├── bootstrap.php
    ├── package.json
    ├── routes.php
    └── src
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而我现在后悔了，想把其中的某个子目录抽离出来，把它变成一个新的 git 仓库，并且保留我之前所有在「原仓库」中关于这个子目录的「所有提交历史」。&lt;/p&gt;
&lt;p&gt;其实这种需求还是挺常见的，举个栗子：&lt;/p&gt;
    
    </summary>
    
      <category term="技术" scheme="https://blessing.studio/categories/tech/"/>
    
    
      <category term="记录" scheme="https://blessing.studio/tag/%E8%AE%B0%E5%BD%95/"/>
    
      <category term="Git" scheme="https://blessing.studio/tag/Git/"/>
    
  </entry>
  
  <entry>
    <title>Windows Update 出现错误 0x800703ed 可能的解决方法</title>
    <link href="https://blessing.studio/fix-windows-update-error-0x800703ed/"/>
    <id>https://blessing.studio/fix-windows-update-error-0x800703ed/</id>
    <published>2018-02-13T03:31:38.000Z</published>
    <updated>2018-02-13T03:31:38.000Z</updated>
    
    <content type="html"><![CDATA[<p>HAIDOMO，这里是年终总结拖了两个月还没写完，上一篇博文发布于去年十月份的某鸽系博主 <strong>621sama</strong> DE-SU。其实 2018 年的第一篇博文<del>原定</del>应该是 2017 年的年度总结的，但是由于各种各样的原因，那篇文章到现在还没写完咕咕咕。</p><p>最近几天正好遇到了如标题所述的「Windows Update 自动更新时出现错误 <code>0x800703ed</code>」的状况，在网上搜索许久，最后历经千辛万苦才终于定位到了问题的根源。特此记录，希望能帮到后来人。</p><h2 id="0x01-问题描述"><a href="#0x01-问题描述" class="headerlink" title="0x01 问题描述"></a>0x01 问题描述</h2><p>虽然我现在已经没多少追 Windows Insider Perview 的热情了，不过最近巨硬推出的那个 Fluent Design 看起来还是挺赞的，就打算在 Windows Update 中升级到最新的 Insider Preview。虽然速度屌慢，但是还是成功地检查到了 Build 17093 的更新并且进入了「正在准备更新」这一阶段。谁曾想等进度跑到 100% 却出现了如下错误：</p><p><img src="https://img.blessing.studio/images/2018/02/13/DV0b0p7VAAAn_zT.jpg" alt="error 0x800703ed screenshot"></p><p>这可太他妈的操蛋了。 <a id="more"></a></p><h2 id="0x02-尝试定位问题"><a href="#0x02-尝试定位问题" class="headerlink" title="0x02 尝试定位问题"></a>0x02 尝试定位问题</h2><p>我最开始有想过是网络问题，也尝试给系统挂上全局代理、在路由器强制让所有流量走代理，结果都是 <code>0x800703ed</code> ，因此基本可以排除是网络问题。</p><p>随后我在 Google 上搜索 <code>Windows Update 0x800703ed</code>，浏览了 Microsoft 中文社区中很多类似的帖子，其中官方人员的回复千篇一律，大部分都是这样的：</p><ul><li>镜像文件出错，建议重新下载</li><li>检查 Windows Update 服务是否正常运行</li><li>禁用所有其他非系统服务再检查更新</li><li>建议移除各种杀毒软件</li><li>升级修复各种驱动程序</li><li>系统文件损坏，插入安装光盘修复</li><li>下载各种 Repair 工具……</li></ul><p>我几乎尝试了他们提到的所有方法，最后都以失败告终。</p><p>其他中文社区上也有一些搜索结果，但是毫无帮助，因为他们只会让你：</p><ul><li>重装系统</li></ul><p>对于这种建议，我也想友善地回复一句：<strong>RSNDM</strong>.</p><p>中文内容是指望不上了，之后我又在巨硬的洋文支持社区以及其他站点上搜索了老半天，最后终于找到个可能的问题原因：</p><blockquote><p>安装了 Windows &amp; Linux 双系统。</p></blockquote><p>……。</p><p><img src="https://i.loli.net/2018/02/13/5a82f52fb0971.png" alt="Sticker 288532"></p><h2 id="0x03-解决问题"><a href="#0x03-解决问题" class="headerlink" title="0x03 解决问题"></a>0x03 解决问题</h2><p>按照那几个网站上的说法（详见页脚的参考链接），如果你在机器上安装了 Windows、Linux 双系统启动，并且使用了其他引导程序（例如 grub），在使用 Windows Update 执行更新操作时就会出现  <code>0x800703ed</code> 错误。</p><p><strong>而且我正好如他所述，在机器上安装了 Deepin Linux 与 Windows 的双系统，并且交给 Linux 所在分区上的 grub2 来引导双系统启动。</strong></p><p>到这里基本就可以破案了。之后我把引导程序由 grub2 切换回巨硬的 NT 6.x，重启之后 Windows Update 就一切正常了，并且成功更新至 Build 17093。</p><p><img src="https://img.blessing.studio/images/2018/02/13/Snipaste_2018-02-13_21-41-29.png" alt="Snipaste_2018-02-13_21-41-29.png"></p><p>具体切换引导程序的操作我这里就不说了，这种东西网上一搜一大把，注意区分 Legacy BIOS + MBR 环境和 UEFI + GPT 环境就好了。</p><p>其实昨天我在切换引导程序时还出了点小插曲：像是不小心把硬盘和 PE U盘 的 MBR <strong>一起弄坏</strong>导致差点进不去任何系统啦、修复 BCD 时总是出现莫名奇妙的问题啦、懵了半小时最后才发现是硬盘活动分区忘记改回来了之类的，要不是昨天手头正好还有个 Deepin 的 LiveCD 我现在早就凉凉了<del>（谁让我不会写 grub2 配置，看到 grub rescue 命令行就只有懵逼的份儿呢）</del>。</p><h2 id="0x04-后记"><a href="#0x04-后记" class="headerlink" title="0x04 后记"></a>0x04 后记</h2><p>至于为什么使用非 Windows 引导程序就会导致更新时出现 <code>0x800703ed</code> 错误，我也只能说不知道啦，鬼知道巨硬是怎么想的。或许是巨硬的系统更新流程里需要使用它自己的引导程序做些神秘的事情吧。</p><p><strong>Windows Update 更新完成后是可以切换回 grub2 引导，完全 OJBK。</strong></p><p>不过需要注意的是，本文提到的解决方法虽然对包括我在内的许多用户都有效，但是到读者你的机器上可能就不行了，毕竟巨硬的报错从来只是给个自己编的错误代码而从来不给具体信息。正如我在标题上写的<strong>「可能的解决方法」</strong>一样，如果「切换引导程序」这个方法对于同样遇到此错误的你不起作用或者对你的设备造成了什么损伤的话，请不要顺着网线过来打我，蟹蟹。</p><p>另外预告一下，最近除了年终总结，我应该会再写一篇关于 WSL（<em>Windows Subsystem for Linux</em>）的文章，敬请期待咕咕咕。</p><p><strong>参考链接：</strong></p><ul><li><a href="https://answers.microsoft.com/en-us/windows/forum/windows_10-update/windows-10-update-error-0x800703ed-dual-boot-with/c55815b7-2931-4cd2-a40b-08843f7072b2" target="_blank" rel="noopener">Windows 10 Update Error 0x800703ed - Dual Boot with Linux</a></li><li><a href="https://www.downtowndougbrown.com/2017/05/windows-10-upgrade-fails-with-error-0x800703ed/" target="_blank" rel="noopener">Windows 10 upgrade fails with error 0x800703ed</a></li><li><a href="https://superuser.com/questions/1256254/cannot-install-windows-10-creator-update-something-went-wrong-error-code-0x80" target="_blank" rel="noopener">Cannot install Windows 10 Creator update: “something went wrong” error code 0x800703ed</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;HAIDOMO，这里是年终总结拖了两个月还没写完，上一篇博文发布于去年十月份的某鸽系博主 &lt;strong&gt;621sama&lt;/strong&gt; DE-SU。其实 2018 年的第一篇博文&lt;del&gt;原定&lt;/del&gt;应该是 2017 年的年度总结的，但是由于各种各样的原因，那篇文章到现在还没写完咕咕咕。&lt;/p&gt;
&lt;p&gt;最近几天正好遇到了如标题所述的「Windows Update 自动更新时出现错误 &lt;code&gt;0x800703ed&lt;/code&gt;」的状况，在网上搜索许久，最后历经千辛万苦才终于定位到了问题的根源。特此记录，希望能帮到后来人。&lt;/p&gt;
&lt;h2 id=&quot;0x01-问题描述&quot;&gt;&lt;a href=&quot;#0x01-问题描述&quot; class=&quot;headerlink&quot; title=&quot;0x01 问题描述&quot;&gt;&lt;/a&gt;0x01 问题描述&lt;/h2&gt;&lt;p&gt;虽然我现在已经没多少追 Windows Insider Perview 的热情了，不过最近巨硬推出的那个 Fluent Design 看起来还是挺赞的，就打算在 Windows Update 中升级到最新的 Insider Preview。虽然速度屌慢，但是还是成功地检查到了 Build 17093 的更新并且进入了「正在准备更新」这一阶段。谁曾想等进度跑到 100% 却出现了如下错误：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.blessing.studio/images/2018/02/13/DV0b0p7VAAAn_zT.jpg&quot; alt=&quot;error 0x800703ed screenshot&quot;&gt;&lt;/p&gt;
&lt;p&gt;这可太他妈的操蛋了。
    
    </summary>
    
      <category term="技术" scheme="https://blessing.studio/categories/tech/"/>
    
    
      <category term="记录" scheme="https://blessing.studio/tag/%E8%AE%B0%E5%BD%95/"/>
    
      <category term="Windows" scheme="https://blessing.studio/tag/Windows/"/>
    
  </entry>
  
  <entry>
    <title>使用 Travis CI 自动部署 Hexo 博客</title>
    <link href="https://blessing.studio/deploy-hexo-blog-automatically-with-travis-ci/"/>
    <id>https://blessing.studio/deploy-hexo-blog-automatically-with-travis-ci/</id>
    <published>2017-10-04T02:50:41.000Z</published>
    <updated>2017-10-04T02:50:41.000Z</updated>
    
    <content type="html"><![CDATA[<p>之前（六月份高考完后）我把博客引擎由 Ghost <a href="https://blessing.studio/migrated-to-hexo/">换成了静态博客生成器 Hexo</a>，并且只使用了自带的 Git Deployment 来手动部署生成好的静态博客文件到服务器上。虽然不像动态博客程序那样可以随时随地更新博客，但是想到马上就要上大学了，之后手头都会有电脑，所以更新博客也不算那么麻烦。</p><p>但是实在是人算不如天算，浙江这一届新高考改革是结结实实地把我坑了一把。二段线以上一段线未满这样中途半端的分数让人在填志愿时着实是犯难 —— 这次浙江几乎普遍出现「一段考生抢以往的二本中好学校，二段考生抢三本学校」这样神秘的情况。思来想去最后把心一横，决定去再读一年高四。后来全省高考录取情况出来后，也证实了我当时复读的决定也不是不合理的（譬如当时上了新浪微博热门话题的「浙江滑档大学」等）。</p><p>关于复读的话题就先放一放吧，毕竟现在的时间确实是有些紧，可能得等到寒假时才能好好地写一篇近况报告以及关于高四生活的事情了，非常遗憾。</p><p>回到正题，因为我输得透彻滚去读高四了，所以自然不可能每次都有配置完好的操作环境让我手动发博文 + 部署（虽然我也不见得有时间写什么博文）。在这样的前提条件下，一个 Hexo 博客的自动部署（持续集成）系统就显得非常有必要了。</p><h2 id="0x01-需求分析"><a href="#0x01-需求分析" class="headerlink" title="0x01 需求分析"></a>0x01 需求分析</h2><p>首先我们需要了解的是，我们到底希望实现一个怎样的系统？以下是我的设想：</p><ol><li>更新博客文章内容后 commit 到 GitHub repo（也可以直接在 GitHub 网页上 commit）；</li><li>Travis CI 自动编译生成出新的静态博客文件；</li><li>自动部署至 GitHub Pages 和我自己的服务器。</li></ol><a id="more"></a><p>虽然互联网上关于自动部署 Hexo 博客的文章已经有很多了，但是我这里还是有些需求是和他们不一样的：</p><ol><li>我所使用的 Seventeen 主题源码存放在 <a href="https://gitee.com/" target="_blank" rel="noopener">Gitee</a> （就是原来的 Git@OSC）上的私有仓库中（毕竟这主题只是我移植到 Hexo 上来的，主题版权依然属于<a href="https://qaq.cat/" target="_blank" rel="noopener">原作者</a>）；</li><li>需要同时部署至 GitHub Pages 和我自己的 VPS 上。</li></ol><p>折腾期间也是踩到了一些坑，也在这里记录一下。</p><h2 id="0x02-配置-GitHub-仓库"><a href="#0x02-配置-GitHub-仓库" class="headerlink" title="0x02 配置 GitHub 仓库"></a>0x02 配置 GitHub 仓库</h2><p>说实话我之前都没用过 GitHub Pages，也没打算直接用它来存放我的博客（<a href="https://blessing.studio/">blessing.studio</a> 依然部署在我的 VPS 上）。不过想想之后我也没有搞运维的时间了，多来几个博客的存档备份也是好的（反正也是免费的，笑）。</p><p>怎样启用 GitHub Pages 我就不多说了，智力正常的人应该都能完成这些操作。因为域名是以用户名开头的 User Pages 默认只能显示 master 分支里的内容（我也懒得去弄 Custom domain 啥的），所以我用了不同的分支来存放不同的内容：</p><ul><li><strong>master</strong> 存放 Hexo 生成好的静态文件，所有 commit 信息格式均为 <code>Site updated: %Y-%m-%d %H:%M:%S</code>；</li><li><strong>source</strong> 存放 scaffolds（脚手架）、source（文章 Markdown 源码）、_config.yml（Hexo 配置）等文件，commit 信息前都加上对应的 emoji（确实蛮好玩的，参见 <a href="https://gitmoji.carloscuesta.me/" target="_blank" rel="noopener">gitmoji</a>），并设置为 repo 的默认分支。</li></ul><p>分支操作大概像这样：</p><pre><code class="shell">git initgit remote add origin git@github.com:printempw/printempw.github.io.git# 新建 source 分支git checkout --orphan sourcegit add .git commit -m &quot;:tada: Initial commit&quot;git push origin source:source</code></pre><p>最后这个 repo 的画风是这样的：</p><p><img src="https://img.blessing.studio/images/2017/10/04/snipaste_20171004_201448.png" alt="snipaste_20171004_201448.png"></p><p><img src="https://img.blessing.studio/images/2017/10/04/snipaste_20171004_201526.png" alt="snipaste_20171004_201526.png"></p><p>我是觉得挺不错的，你说呢？</p><h2 id="0x03-配置-Travis-CI"><a href="#0x03-配置-Travis-CI" class="headerlink" title="0x03 配置 Travis CI"></a>0x03 配置 Travis CI</h2><p>怎么登录 Travis CI 并关联 GitHub 项目我也一样不多说，只要智力正常以下略。</p><p>下面主要讲一下如何编写 <code>.travis.yml</code> 配置文件。</p><h3 id="0x31-配置部署秘钥"><a href="#0x31-配置部署秘钥" class="headerlink" title="0x31 配置部署秘钥"></a>0x31 配置部署秘钥</h3><p>我博客的部署过程中需要用到 ssh 密钥认证的地方大概有这几处：</p><ul><li>从 Gitee 私有仓库 clone 主题；</li><li>将编译好的文件 push 到 GitHub Pages 和 VPS。</li></ul><p>一般网上的文章只有一个自动 push 到 GitHub Pages 的需求，所以直接申请一个 GitHub 的 Personal access tokens，配合 Travis 的环境变量配置就可以拿到 push 权限了。不过我这里情况复杂一些，所以不如直接搞个部署秘钥来得方便（而且那个 token 是可以操作所有 repo 的，更不安全）。</p><p>首先，新生成一个 ssh 密钥对（不要嫌麻烦直接把你机器上的秘钥拿去用了，太危险）：</p><pre><code class="shell"># 随便生成在哪都行，文件名也随意$ ssh-keygen -f travis.key</code></pre><p>然后把生成的公钥文件（e.g. <code>travis.key.pub</code>）分别添加到 GitHub Deploy Keys（在哪你自己找呀）、Gitee 部署秘钥、VPS 上的 <code>~/.ssh/authorized_keys</code> 中，这样 Travis CI 的机器就可以直接访问这些服务器了。</p><p>那我们要怎么在 Travis CI 自动部署过程中使用这个私钥呢？直接放在 repo 里提交上去肯定是不行的；而且那么长一串的私钥，总不能设置成环境变量吧（摊手）。</p><p>不过好在 Travis CI 提供了文件加密工具，这样我们就可以直接把加密后的私钥提交到 git repo 中，然后在 Travis CI 自动部署过程中解密出原秘钥并使用了（网上还有其他神秘的加密方法，但是没几个有 openssl aes-256-cbc 加密这样靠谱）。</p><p>首先，我们需要安装 Travis 的命令行工具：</p><pre><code class="shell"># 是的，你没看错，Travis 的命令行工具是用 Ruby 写的# 所以，想要用它你还得去安装 Ruby 环境……去吧，我的朋友$ sudo gem install travis</code></pre><p>然后通过命令行登录 Travis 并加密文件：</p><pre><code class="shell"># 交互式操作，使用 GitHub 账号密码登录# 如果是私有项目要加个 --pro 参数$ travis login --auto# 加密完成后会在当前目录下生成一个 travis.key.enc 文件# 还会在你的 .travis.yml 文件里自动加上用于解密的 shell 语句# 还 tmd 会自动格式化你的 .travis.yml 文件，去他妈的$ travis encrypt-file travis.key -add</code></pre><p>需要注意的是，这些文件加密步骤<strong>不能</strong>在 Windows 系统下完成，不然在自动部署时会出现神秘的错误（wrong final block length）。这个问题已经被很多人<a href="https://github.com/travis-ci/travis-ci/issues/4746" target="_blank" rel="noopener">报告过了</a>（实际上我也踩上去了），并且<a href="https://docs.travis-ci.com/user/encrypting-files/" target="_blank" rel="noopener">官方文档</a>里也加上了这样一段话：</p><p><img src="https://img.blessing.studio/images/2017/10/04/snipaste_20171004_204633.png" alt="Caveat"></p><p>总之就是「辣鸡 Windows 太菜了不行，给我用 WSL 或者类 Unix 系统吧哈哈哈」的意思（迫真）经过我的测试，Windows10 下的 babun、Git Bash 均告失败，WSL（Windows Subsystem for Linux）和我 VPS 上的 Ubuntu 14.04 所生成的加密文件均可通过自动部署，屁事儿没有。</p><p>以上步骤完成后你会得到一个 <code>travis.key.enc</code>，然后你把这玩意放到 repo 的哪里去都成，随你喜欢，只要能访问得到就可以（比如说我放到了 <code>.travis</code> 目录里）。然后在 <code>.travis.yml</code> 可以使用如下命令解密出原本的 ssh 私钥：</p><pre><code># 环境变量 $encrypted_1fc90f464345_key 中的那一串字符是随机的，每个人都不一样，自己机灵点儿改成你自己的。这个环境变量名也可以在 Travis CI 的项目后台环境变量设置中查看openssl aes-256-cbc -K $encrypted_1fc90f464345_key -iv $encrypted_1fc90f464345_iv -in .travis/travis.key.enc -out ~/.ssh/id_rsa -d</code></pre><p>搞定了密钥认证后，我们还需要修改一下机器的 SSH 配置。为啥呢？</p><p><img src="https://i.loli.net/2017/10/04/59d4dac7a6381.png" alt="snipaste_20171004_205715.png"></p><p>相信大家对于上面的提示并不陌生 —— 每次我们使用 ssh 尝试连接到一个我们之前没有连接过的服务器时都会出现这样的提示。但是在 Travis CI 的自动部署过程中是不接受任何命令行输入的（好像可以，但是很麻烦），所以我们必须想办法把这个确认过程给干掉，否则自动部署就会被卡在这里直到超时了。</p><p>网上搜一搜就能知道管这玩意的是 <code>./ssh/config</code> 里的 <code>StrictHostKeyChecking</code> 配置项，所以我们可以在项目里写一个自己的 ssh 配置文件，然后在自动部署过程中替换掉 Travis CI 机器的 ssh 配置：</p><pre><code># 文件 [.travis/ssh_config]Host github.com    User git    StrictHostKeyChecking no    IdentityFile ~/.ssh/id_rsa    IdentitiesOnly yesHost gitee.com    User git    StrictHostKeyChecking no    IdentityFile ~/.ssh/id_rsa    IdentitiesOnly yesHost prinzeugen.net    User git    StrictHostKeyChecking no    IdentityFile ~/.ssh/id_rsa    IdentitiesOnly yes# 文件 [.travis.yml]mv -fv .travis/ssh-config ~/.ssh/config</code></pre><p>也有另外一种方法是通过在 <code>.travis.yml</code> 中添加 <code>ssh_known_hosts</code> 来实现的（具体可以看 Travis CI 的 <a href="https://docs.travis-ci.com/user/ssh-known-hosts/" target="_blank" rel="noopener">官方文档</a>），不过上面的方法灵活性更高（是的，我是写到这里才发现还有这种方法……上面的我也懒得删了，就放在那吧，括弧笑）。</p><pre><code class="yml">addons:  ssh_known_hosts:  - github.com  - gitee.com  - prinzeugen.net</code></pre><p>这样一来，就没有什么能阻挡我们的自动部署过程啦 <img src="https://img.blessing.studio/images/2017/10/04/QQ20171004210320.jpg" alt="表情"></p><h3 id="0x32-编写-travis-yml"><a href="#0x32-编写-travis-yml" class="headerlink" title="0x32 编写 .travis.yml"></a>0x32 编写 .travis.yml</h3><p>我就直接贴我自己的配置了（放在 <a href="https://gist.github.com/printempw/42e8781ed3adadbcc6ecac01904a32f6" target="_blank" rel="noopener">Gist</a> 上，加载不出来的自己想办法），诸位看着修改：</p><script src="https://work.prinzeugen.net/gist/printempw/42e8781ed3adadbcc6ecac01904a32f6.js"></script><p>大部分语句都加了注释，我也就不再多说明了。</p><p>如果不出意外，每次 push 新 commit 到 source 分支后，Travis CI 就会自动帮你构建最新的静态博客，并部署至 Github Pages 和你自己的 VPS 上了。</p><p><img src="https://img.blessing.studio/images/2017/10/04/snipaste_20171004_212745.png" alt="Travis CI Build Status"></p><h2 id="0x04-后记"><a href="#0x04-后记" class="headerlink" title="0x04 后记"></a>0x04 后记</h2><p>打开博客一看，上一篇博文已经是两个月之前的事情了。复读开学之后这段时间确实是挺忙的，每天晚上回寝室后屌累屌困，最多也就记记账，看看邮件，看看 RSS，闲的话刷刷 Twitter，确实是没有什么时间像这样坐下来写一篇文章了。</p><p>估计以后博文更新频率也会越来越低吧，唉。</p><h3 id="0x41-参考链接"><a href="#0x41-参考链接" class="headerlink" title="0x41 参考链接"></a>0x41 参考链接</h3><ul><li><a href="https://changkun.us/archives/2017/06/232/" target="_blank" rel="noopener">Hexo + GitHub + Travis CI + VPS 自动部署</a></li><li><a href="http://www.itfanr.cc/2017/08/09/using-travis-ci-automatic-deploy-hexo-blogs/" target="_blank" rel="noopener">使用 Travis CI 自动部署 Hexo 博客</a></li><li><a href="https://segmentfault.com/a/1190000009054888" target="_blank" rel="noopener">使用 Travis 自动部署 Hexo 到 Github 与 自己的服务器</a></li><li><a href="https://zespia.tw/blog/2015/01/21/continuous-deployment-to-github-with-travis/" target="_blank" rel="noopener">用 Travis CI 自動部署網站到 GitHub</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;之前（六月份高考完后）我把博客引擎由 Ghost &lt;a href=&quot;https://blessing.studio/migrated-to-hexo/&quot;&gt;换成了静态博客生成器 Hexo&lt;/a&gt;，并且只使用了自带的 Git Deployment 来手动部署生成好的静态博客文件到服务器上。虽然不像动态博客程序那样可以随时随地更新博客，但是想到马上就要上大学了，之后手头都会有电脑，所以更新博客也不算那么麻烦。&lt;/p&gt;
&lt;p&gt;但是实在是人算不如天算，浙江这一届新高考改革是结结实实地把我坑了一把。二段线以上一段线未满这样中途半端的分数让人在填志愿时着实是犯难 —— 这次浙江几乎普遍出现「一段考生抢以往的二本中好学校，二段考生抢三本学校」这样神秘的情况。思来想去最后把心一横，决定去再读一年高四。后来全省高考录取情况出来后，也证实了我当时复读的决定也不是不合理的（譬如当时上了新浪微博热门话题的「浙江滑档大学」等）。&lt;/p&gt;
&lt;p&gt;关于复读的话题就先放一放吧，毕竟现在的时间确实是有些紧，可能得等到寒假时才能好好地写一篇近况报告以及关于高四生活的事情了，非常遗憾。&lt;/p&gt;
&lt;p&gt;回到正题，因为我输得透彻滚去读高四了，所以自然不可能每次都有配置完好的操作环境让我手动发博文 + 部署（虽然我也不见得有时间写什么博文）。在这样的前提条件下，一个 Hexo 博客的自动部署（持续集成）系统就显得非常有必要了。&lt;/p&gt;
&lt;h2 id=&quot;0x01-需求分析&quot;&gt;&lt;a href=&quot;#0x01-需求分析&quot; class=&quot;headerlink&quot; title=&quot;0x01 需求分析&quot;&gt;&lt;/a&gt;0x01 需求分析&lt;/h2&gt;&lt;p&gt;首先我们需要了解的是，我们到底希望实现一个怎样的系统？以下是我的设想：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;更新博客文章内容后 commit 到 GitHub repo（也可以直接在 GitHub 网页上 commit）；&lt;/li&gt;
&lt;li&gt;Travis CI 自动编译生成出新的静态博客文件；&lt;/li&gt;
&lt;li&gt;自动部署至 GitHub Pages 和我自己的服务器。&lt;/li&gt;
&lt;/ol&gt;
    
    </summary>
    
      <category term="技术" scheme="https://blessing.studio/categories/tech/"/>
    
    
      <category term="博客" scheme="https://blessing.studio/tag/%E5%8D%9A%E5%AE%A2/"/>
    
      <category term="Hexo" scheme="https://blessing.studio/tag/Hexo/"/>
    
      <category term="持续集成" scheme="https://blessing.studio/tag/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/"/>
    
  </entry>
  
  <entry>
    <title>又是一种 Minecraft 外置登录解决方案：自行实现 Yggdrasil API</title>
    <link href="https://blessing.studio/minecraft-yggdrasil-api-third-party-implementation/"/>
    <id>https://blessing.studio/minecraft-yggdrasil-api-third-party-implementation/</id>
    <published>2017-08-08T15:28:32.000Z</published>
    <updated>2018-02-21T17:01:30.000Z</updated>
    
    <content type="html"><![CDATA[<p>最近给 Blessing Skin 写了个插件，利用皮肤站本身的账号系统实现了 Yggdrasil API（就是 Mojang 的登录 API），然后配合 <a href="https://github.com/to2mbn/authlib-injector/" target="_blank" rel="noopener">authlib-injector</a> 这个项目将启动器（基于 Java 编写的支持正版登录的启动器都行）、Minecraft 游戏、Minecraft 服务端中的 Mojang Yggdrasil API 地址给替换成了自己实现的第三方 Yggdrasil API 地址（字节码替换），从而实现了与正版登录功能几乎完全相同的账户鉴权系统。</p><p>通俗地讲，就是我把 Mojang 的正版登录 API 给【劫持】成自己的啦，所以可以像登录正版那样直接用皮肤站的邮箱和密码登录游戏（还支持 Mojang 都不支持的多用户选择哦）。这种外置登录系统的实现应该可以说是比市面上的软件都要完善（毕竟可以直接利用 Minecraft 本身自带的鉴权模块），因此写一篇博文介绍一下这些实现之间的不同之处，顺带记录一下实现 Yggdrasil API 时踩到的坑，算是抛砖引玉了。</p><div class="alert alert-warning"><p><strong>注意</strong>：本文不适合小白及问题解决能力弱的人群阅读。</p></div><p><del>感觉我明明好久没玩 MC 了，要玩也都是玩正版服务器，但是却一直在搞这些盗版服用的东西，我真是舍己为人造福大众普惠众生啊（不</del></p><h2 id="一、服务器内置登录插件"><a href="#一、服务器内置登录插件" class="headerlink" title="一、服务器内置登录插件"></a>一、服务器内置登录插件</h2><p>相信维护过 Minecraft 服务器（当然，我这边说的是运行在离线模式下的服务器）的腐竹们或多或少都听说过 Authme、CrazyLogin 等登录插件的鼎鼎大名吧。由于这些服务器运作在离线模式（<code>online-mode=false</code>，即俗称的盗版模式）下，缺少 Mojang 官方账户认证系统的支持，所以必须使用这类插件来进行玩家认证（否则随便谁都可以冒名顶替别人了，换一个登录角色名就行）。</p><p>这类插件的工作原理就是在服务端维护一个数据表，表中每一条记录中存储了角色的「角色名」、「登录密码」、「注册时间」、「登录 IP 地址」等等信息，当玩家初次进入服务器时需要通过这些插件进行注册操作（e.g. <code>/register</code> 命令）并在表中插入一条记录，注册完毕后进入服务器则需要输入密码（e.g. <code>/login &lt;password&gt;</code> 命令）来认证。</p><p>其实这样的解决方案也没什么不好，而且现在 Authme 等登录插件在众多的服务器中都还是主流。但是，如果你的服务器已经发展到比较大型了，或许你就比较希望有这样一个东西：</p><a id="more"></a><ul><li>可以直接在启动器中进行登录鉴权操作，点击「开始游戏」就可以直接进入服务器，不用在游戏里再一遍遍输入 <code>/login</code> 等指令；</li><li>有一个网页版的用户管理，可以直接对玩家进行操作（e.g. 封禁、修改积分）；</li><li>玩家们可以直接在一个直观的网页上注册账号，并且可以直接用这个账号 &amp; 密码登录游戏；</li><li>希望这个账号系统还能对接论坛、皮肤站等乱七八糟的东西，玩家注册了一个账号之后，可以在任何地方使用；</li><li>希望服务器有一个自己的网页、自定义启动器、用户管理系统、卫星地图之类的东西来装逼；</li><li>etc.</li></ul><p>并不是所有腐竹都满足于 Authme + Discuz 这样的组合的（而且这类游戏内登录系统也有不少安全漏洞），毕竟在这个 Minecraft 多人联机服务器发展接近饱和的时候，如果想要你的服务器能够吸引新玩家，那么除了服务器本身建设之外的地方也是要好好考虑的。</p><h2 id="二、外置登录系统"><a href="#二、外置登录系统" class="headerlink" title="二、外置登录系统"></a>二、外置登录系统</h2><p>正是这样的需求催生了不少 Minecraft 的「外置登录插件」、「网页登录」等等软件（而且人气都挺高的），我随手在 MCBBS 上一搜就有很多类似的产品，用啥语言写的都有：<em>MadAuth、WebLogin、BeeLogin、WebRegister、冰棂登陆系统……</em></p><p>这些软件的原理就是将原本的登录鉴权这一步骤从游戏里抽出来了，将其放到启动器 or 网页上去，而服务端插件的功能就只剩下「查询数据库中用户的登录状态，决定是否放行」：</p><p><img src="https://img.blessing.studio/images/2017/08/04/Minecraft.png" alt="原理图"></p><p><em>▲随手画的示意流程图，这里推荐一下  <a href="https://www.processon.com/" target="_blank" rel="noopener">ProcessOn</a> 这个在线作图网站，很好用 ;)</em></p><p>似乎也挺好的，不是吗？那我今天要说的「自行实现 Yggdrasil API」方法，和这些现成的方式有什么不一样呢？</p><h2 id="三、自行实现-Yggdrasil-API"><a href="#三、自行实现-Yggdrasil-API" class="headerlink" title="三、自行实现 Yggdrasil API"></a>三、自行实现 Yggdrasil API</h2><p>继续看下去之前，首先你要知道 Mojang 正版的 Minecraft 是怎样登录的。Mojang 专门定义了一个用于鉴权的 API，Mojang 旗下的游戏（Minecraft、Scrolls 等）都是用的这一套 API 来正版验证的 —— 这一套 API 的名字就叫做 Yggdrasil（即北欧神话里的世界树，<del>这名字可真几把炫酷</del>）。</p><p>正版登录的好处就不用我说了吧？再也不用担心假人压测、自带外置登录（启动器里账号密码登录）、自带皮肤加载（不需要安装 CSL、USM 等皮肤补丁了）、Tab 栏显示头像……可以说，Minecraft 自带的 Yggdrasil API 鉴权系统比上面的那些什么登录插件啊什么外置登录的功能强多了，所以正版服务器（<code>online_mode=true</code>）也不用担心那些破事，因为官方的这一套鉴权系统以及很完善了。</p><p>那么问题来了，盗版用户要怎样才能把 Mojang 为正版开发的 Yggdrasil API 系统拿来用呢？</p><h3 id="3-1-基本原理"><a href="#3-1-基本原理" class="headerlink" title="3.1 基本原理"></a>3.1 基本原理</h3><p>这里必须感谢 <a href="https://github.com/to2mbn/authlib-injector/" target="_blank" rel="noopener">to2mbn/authlib-injector</a> 这个项目，正是因为这个项目，我接下来描述的方法才成为可能。是的，方法很简单，Minecraft 虽然把 Mojang 官方的 Yggdrasil API 地址（<code>https://authserver.mojang.com</code>）给写死在源码里了，但是既然 Minecraft 是基于 JVM 的应用程序，我们就可以通过字节码替换的方法将官方的 API 地址替换成我们自己实现的 API 地址。</p><p>以下内容援引自 authlib-agent（即 authlib-injector 前身）的 wiki：</p><blockquote><p>authlib-agent 是一个高可靠性, 高适用性, 用于 Minecraft 的, 游戏外登录及皮肤解决方案. 支持 Minecraft1.7+, Craftbukkit, Spigot, Bungeecord 等. 通过对正版登录 API 的重定向, 实现了一个功能和正版几乎一样的游戏外登录系统.</p></blockquote><p>不过既然要把官方 API 地址替换成我们自己的，我们就得自己实现一个和官方 API 其他地方都一样的 API，也就是，<strong>仿造出一个第三方 Yggdrasil API 出来</strong>。</p><h3 id="3-2-解决方案"><a href="#3-2-解决方案" class="headerlink" title="3.2 解决方案"></a>3.2 解决方案</h3><p>可以说这个系统中，就是「开发完整实现了 Yggdrasil API 的后端」这一步最难了。为啥捏？这个服务端不止要实现用户的认证、皮肤获取，你还得实现用户的注册、登录、角色管理、皮肤上传、皮肤库等等七七八八的功能吧？你还得给这些功能套上一个好看的界面吧，不然你让你的玩家怎么使用？你还得来个后台管理页面吧，不然管理员怎么进行用户管理、封禁等操作？</p><p>authlib-injector 官方也提供了一个 Java 编写的后端 <a href="https://github.com/to2mbn/yggdrasil-mock" target="_blank" rel="noopener">yggdrasil-mock</a>，虽然完整实现了 Yggdrasil API，但是它并没有提供直观的管理网页，只提供了一套 RESTful API，所以距离实装要求还是差得比较远的。</p><p>要重头开发一套这样的系统是非常非常够呛的，不过幸运的是，我之前一直在持续开发的 Minecraft 皮肤站 <a href="https://github.com/printempw/blessing-skin-server" target="_blank" rel="noopener">Blessing Skin Server</a>，这个项目的 v3 版本<strong>正好</strong>就满足的这些要求 —— 友好的用户界面、完善的用户系统、强大的后台管理、附带皮肤上传管理展示功能，再加上我之前开发的<a href="https://blessing.studio/laravel-plugin-system-1/">插件系统</a>（开发这玩意真是个正确的决定，一劳永逸啊） ，这让我可以很方便地开发一个插件出来，直接基于现成的皮肤站用户系统实现 Yggdrasil API。</p><p><img src="https://i.loli.net/2017/08/04/59846283822ac.png" alt="API"></p><h3 id="3-3-如何使用"><a href="#3-3-如何使用" class="headerlink" title="3.3 如何使用"></a>3.3 如何使用</h3><p>讲了那么多，那么到底该怎么使用呢？</p><p>请参阅：<a href="https://github.com/printempw/yggdrasil-api/wiki" target="_blank" rel="noopener">printempw/yggdrasil-api wiki</a>。</p><p>以上步骤完成后你将得到什么？</p><ul><li>一个完善的账号系统（配合数据对接插件还能与 Discuz 等论坛账号互通），包括友好的注册、登录网页界面以及强大的管理员面板，在管理后台中封禁用户后，该用户也将无法登录游戏；</li><li>一个皮肤管理系统，自带皮肤库功能，在皮肤站中应用的皮肤，玩家无需安装任何皮肤 Mod，进入游戏即可看到自己设置的皮肤（支持双层皮肤、支持 Alex 模型，由于游戏本身限制不支持高清皮肤）；</li><li>单账户多角色功能，玩家可以像登录正版那样用「邮箱」和「密码」登录游戏，而且如果你在皮肤站中添加了多个角色的话，还可以在启动页面选择要用哪个角色进入游戏（Yggdrasil API 实现了这个功能，但是 Mojang 的正版登录服务器并未实现该功能），HMCL 等启动器都实现了本功能；</li></ul><p>这还不够多吗？</p><p>而且你还可以自己修改 HMCL 等开源启动器的源码，在启动时自动注入 <code>-javaagent</code> 参数，更加方便，还能得到一个服务器专用启动器，逼格更高了（笑）</p><h3 id="3-4-实现效果"><a href="#3-4-实现效果" class="headerlink" title="3.4 实现效果"></a>3.4 实现效果</h3><p>皮肤站的用户管理系统、皮肤系统、后台界面之类的我就不截图了，有兴趣可以去 MCBBS 的 <a href="http://www.mcbbs.net/forum.php?mod=viewthread&amp;tid=552877" target="_blank" rel="noopener">发布帖</a> 上感受一下。</p><p><img src="https://img.blessing.studio/images/2017/08/04/imageac07f.png" alt="网页管理"></p><p><em>▲在皮肤站「角色管理」中可添加多个角色</em></p><p><img src="https://img.blessing.studio/images/2017/08/04/image.png" alt="多角色选择"></p><p><em>▲使用皮肤站的邮箱与密码登录后，配合 HMCL 实现多角色选择</em></p><p><img src="https://i.loli.net/2017/08/04/5984671f87c40.png" alt="游戏"></p><p>▲游戏内的显示效果</p><h2 id="四、Yggdrasil-API-踩坑记录"><a href="#四、Yggdrasil-API-踩坑记录" class="headerlink" title="四、Yggdrasil API 踩坑记录"></a>四、Yggdrasil API 踩坑记录</h2><p>下面记录一些自己实现 Yggdrasil API 时踩到的坑，毕竟 wiki.vg 里并不会提到这些在自己实现 API 时需要注意的东西（提到的大部分都是使用 API 时应该要注意的），所以我也只能摸着石头过河，踩了不少坑，这里记录一下，希望能帮到后来人。</p><p>基础的 API 定义之类的我就不说了，下面主要讲一些 <a href="http://wiki.vg/Authentication" target="_blank" rel="noopener">文档</a> 里没怎么提到的东西。</p><blockquote><p><strong>2018-02-22 加注：</strong></p><p>最近 @yushijinhun 写了一篇 <a href="https://github.com/to2mbn/authlib-injector/wiki/Yggdrasil%E6%9C%8D%E5%8A%A1%E7%AB%AF%E6%8A%80%E6%9C%AF%E8%A7%84%E8%8C%83" target="_blank" rel="noopener">Yggdrasil 服务端技术规范</a>，大部分 API 相关的内容其中都有提及，大家去看那个就好了。</p></blockquote><h3 id="4-1-登录与鉴权"><a href="#4-1-登录与鉴权" class="headerlink" title="4.1 登录与鉴权"></a>4.1 登录与鉴权</h3><p>用过正版 Minecraft 的登录系统的同学应该都知道，一般只有在初次登录游戏或者太久没有开过游戏的情况下，启动器才会要求你输入账号密码，其他情况下都是可以直接点击登录并启动游戏的。</p><p>但这并不是因为启动器记下了你的密码，相反，启动器保存的是 Mojang 认证服务器返回的 AccessToken。如果你曾经观察过启动器启动游戏时所用的启动参数，你就能发现其实 Minecraft 游戏本体其实只拿到了角色名、角色 Profile 对应的 UUID 以及上面提到的 AccessToken 而已。可以说，只要拿到这个 AccessToken 就可以进行几乎所有的操作了。</p><pre><code class="text">--username 621sama--uuid d3af753b7cda4666adc2ff9bba85e0eb--accessToken cc1e7c7d-00ab-4f37-bbe1-983e18f1755d</code></pre><h4 id="4-1-1-获取-AccessToken"><a href="#4-1-1-获取-AccessToken" class="headerlink" title="4.1.1 获取 AccessToken"></a>4.1.1 获取 AccessToken</h4><p>用正确的 <code>username</code> 和 <code>password</code> 请求 <code>/authenticate</code> API 即可拿到 AccessToken，该令牌的有效期由服务端来决定（一般用 Redis 实现）。如果你请求 API 的时候没有带上 <code>clientToken</code>，那服务端就会帮你生成一个，你要记得把这个返回值记下来，因为 clientToken 和 accessToken 是对应关系，有些 API 是要求同时提供 AccessToken 和签发该令牌的 clientToken 的。</p><p>另外需要注意的是，这个 <code>/authenticate</code> API 中请求体中的 <code>username</code> 字段，<strong>填的是邮箱</strong>。</p><p>是的，你没听错，email，在 username 字段里填的是用户的 email。惊不惊喜，意不意外？这个狗屎一样的字段命名估计和历史遗留问题也有关系，因为早期 Minecraft 账号（也就是 Profile 里的那个 <code>legacy</code> 字段）是直接用<strong>「角色名」</strong>和「密码」登录的，但是新版 Mojang 账号（Yggdrasil API）认证是用的<strong>「电子邮箱账号」</strong>，Yggdrasil API 为了兼容旧账号的登录，所以搞了这么一个坑爹的东西，真是说不出话。</p><p>总之，如果想要自己实现 Yggdrasil API，是要注意一下这个神秘的 <code>username</code> 字段的。</p><h4 id="4-1-2-刷新-AccessToken"><a href="#4-1-2-刷新-AccessToken" class="headerlink" title="4.1.2 刷新 AccessToken"></a>4.1.2 刷新 AccessToken</h4><p>在登录成功拿到 <code>accessToken</code> 后，启动器应该把这个令牌存起来，然后在每次玩家登录游戏之前请求一次 <code>/refresh</code> API，提供 accessToken 和签发该令牌时用的 clientToken（这也是我为什么上面叫你要把这个存起来的原因），就可以拿到新签发的 accessToken 了（刷新令牌有效期）。只要令牌有效期没过，启动器就不会再次请求 <code>/authenticate</code> API。</p><p>所以，虽然文档上没说，但是其实 <code>/refresh</code> 返回的结果应该是要和 <code>/authenticate</code> 的返回结果大致相同的，包括 accessToken、clientToken、availableProfiles、selectedProfile、user 等字段（具体下面再说）。</p><h3 id="4-2-API-中的-Token"><a href="#4-2-API-中的-Token" class="headerlink" title="4.2 API 中的 Token"></a>4.2 API 中的 Token</h3><p>Yggdrasil API 的定义中主要有两个 Token，<code>clientToken</code> 与 <code>accessToken</code>，两者为对应关系。一般来说，启动器不会频繁变动 ClientToken（通常情况下，是永远不会变的），而 AccessToken 应该在每次登录游戏时通过 <code>/refresh</code> 重新签发一个。</p><h4 id="4-2-1-Token-的生命周期"><a href="#4-2-1-Token-的生命周期" class="headerlink" title="4.2.1 Token 的生命周期"></a>4.2.1 Token 的生命周期</h4><p>需要注意的是，AccessToken 是有生命周期的，大致如下：</p><pre><code class="text">|---- 1. 有效 ----|---- 2. 暂时失效 ----| 3. 无效|------------------------------------------------------&gt; Time</code></pre><p>AccessToken 刚签发时处于「有效」状态，经过一段时间后（服务端自行设置）变成「暂时失效」状态。在这种状态下的 AccessToken 是无法进入任何开启了正版验证的服务器的（也就是 <code>/join</code> API 不认），但是该令牌还是能拿来请求 <code>/refresh</code> API，这会签发一个全新的处于「有效」状态的 AccessToken 并返回给客户端。</p><p>但是如果处于「暂时失效」状态的 AccessToken 再放置一段时间后就会完全失效（一般的实现就是从 Redis 令牌桶中删掉该令牌），处于「无效」状态的 AccessToken 是无法进行任何操作的，只能让用户重新输入密码并请求 <code>/authenticate</code> API 以获取一个新的 AccessToken。</p><h4 id="4-2-2-Token-的格式"><a href="#4-2-2-Token-的格式" class="headerlink" title="4.2.2 Token 的格式"></a>4.2.2 Token 的格式</h4><p>Yggdrasil API 中的 <code>clientToken</code>、<code>accessToken</code>、<code>id</code> 等字段的格式都是一大串 16 进制数字和 <code>-</code> 连字符组成的字符串，让人看起来很懵。其实这样的字符串格式就是 <strong>通用唯一识别码</strong>（<a href="https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E5%94%AF%E4%B8%80%E8%AF%86%E5%88%AB%E7%A0%81" target="_blank" rel="noopener">Universally Unique Identifier</a>）标准，也就是我们经常听到的 UUID 了。标准形式的 UUID 包含 32 位 16 进制数字，并且由连字符分割成形式为 8-4-4-4-12 的字符串，就像这样：</p><pre><code># 至于如何生成 UUID，各个语言一般都有对应的库，搜一下就有了550e8400-e29b-41d4-a716-446655440000</code></pre><p>虽然文档中没说，但是 API 请求以及响应的 <code>clientToken</code> 、<code>accessToken</code> 以及玩家 Profile 中的 <code>id</code> 字段格式都是<strong>【不带连字符的 UUID】</strong>。下面拿 wiki.vg 中的 <code>/authenticate</code> 请求中的实例响应讲解一下：</p><pre><code class="json">{  // 不带连字符的 UUID 格式  &quot;accessToken&quot;: &quot;869a97cb2bc841be84bfd668c299a718&quot;,  // 无符号 UUID，与 accessToken 对应  &quot;clientToken&quot;: &quot;c0b2bac2eb434af5ae8ae7f824cee02f&quot;,  &quot;availableProfiles&quot;: [    {      // 无符号 UUID，下同      &quot;id&quot;: &quot;d3af753b7cda4666adc2ff9bba85e0eb&quot;,      &quot;name&quot;: &quot;621sama&quot;    }  ],  &quot;selectedProfile&quot;: {    &quot;id&quot;: &quot;d3af753b7cda4666adc2ff9bba85e0eb&quot;,    &quot;name&quot;: &quot;621sama&quot;  },  &quot;user&quot;: {    &quot;id&quot;: &quot;d3af753b7cda4666adc2ff9bba85e0eb&quot;,    &quot;properties&quot;: []  }}</code></pre><p>至于后端存储时用怎样的格式就随意了，不过在 API 返回结果中是一定要按照上面的格式来的。</p><h3 id="4-4-多角色选择功能"><a href="#4-4-多角色选择功能" class="headerlink" title="4.4 多角色选择功能"></a>4.4 多角色选择功能</h3><p>虽然 Mojang 官方迄今为止仍未支持同一个<strong>账号</strong>（Mojang 账号，用邮箱登录的那个）下添加多个<strong>角色</strong>（角色名，就是游戏里显示的那个），但是 Yggdrasil API 本身是可以实现这个<strong>「单账号多角色」</strong>功能的，并且官方启动器、HMCL 等著名的第三方启动器都支持<strong>登录后选择角色进入游戏</strong>（具体效果参见上方截图）。</p><p>如果你仔细阅读过 wiki.vg 里的 API 文档的话就会发现，<code>/authenticate</code> 里面有好几个包含了 Profile 的字段，分别是 <code>availableProfiles</code>、<code>selectedProfile</code> 和 <code>user</code>。下面我稍微说一下这几个字段的功能。</p><p>首先，<code>availableProfiles</code> 中存放的是这个<strong>账号</strong>下所有可用<strong>角色</strong>的 Profile，格式为 JSON 数组：</p><pre><code class="json">&quot;availableProfiles&quot;: [  {    &quot;id&quot;: &quot;不带连字符的 UUID&quot;,    &quot;name&quot;: &quot;角色名&quot;  },  {    &quot;id&quot;: &quot;d3af753b7cda4666adc2ff9bba85e0eb&quot;,    &quot;name&quot;: &quot;621sama&quot;  }]</code></pre><p>需要注意的是，每个<strong>角色 Profile</strong> 都应该有一个唯一的 <code>id</code>（格式为不带连字符的 UUID），而不是每个账号一个。而且，虽然官方文档上没有写，其实 <code>/refresh</code> API 返回的结果应该和 <code>/authenticate</code> 一样带上 <code>availableProfiles</code> 这个属性（因为只有第一次密码登录才会请求 <code>/authenticate</code>，之后进游戏就只会请求 <code>/refresh</code> 了）。</p><p>而 <code>selectedProfile</code> 字段内容为<strong>被选中</strong>的角色 Profile。如果这个字段存在，启动器就会<strong>直接</strong>用这个角色进入游戏。只有在 <code>selectedProfile</code> 字段不存在时，启动器才会弹出「选择角色」对话框，并根据用户的输入选择不同的角色进入游戏。如果你想要搞支持<strong>单账户多角色</strong>的 API 的话，可以不用管这个字段（不过当该账号名下只有一个角色的话记得指定 <code>selectedProfile</code> ，这样启动器就可以直接用这个角色进游戏了）。</p><p>至于 <code>user</code> 字段是只有在请求时带上了 <code>requestUser</code> 属性时才会回复的，其中包括被选中角色的 UUID、语言偏好、Twitch 的 AccessToken 等等，一般来说，自己实现 Yggdrasil API 时可以忽略这玩意（而且这个属性对单账户多角色的支持并不好）。</p><h3 id="4-5-加载皮肤与披风"><a href="#4-5-加载皮肤与披风" class="headerlink" title="4.5 加载皮肤与披风"></a>4.5 加载皮肤与披风</h3><p>这里稍微提一下 Minecraft 使用 Yggdrasil API 时加载皮肤的原理。</p><p>首先你要知道，Minecraft 游戏启动时从启动器那边（i.e. 从命令行）拿到的 API 相关属性只有「AccessToken」、「选中角色的 UUID」以及「选中角色的角色名」这三样东西。获取 Profile 以及加载皮肤是 Minecraft 游戏该做的工作，具体流程如下。</p><h4 id="4-5-1-获取完整-Profile"><a href="#4-5-1-获取完整-Profile" class="headerlink" title="4.5.1 获取完整 Profile"></a>4.5.1 获取完整 Profile</h4><p>首先 Minecraft 会请求 API <code>/profiles/minecraft/{uuid}</code> 获取角色的完整 Profile，差不多长这样：</p><pre><code class="json">{  &quot;id&quot;: &quot;d3af753b7cda4666adc2ff9bba85e0eb&quot;,  &quot;name&quot;: &quot;621sama&quot;,  &quot;properties&quot;: [    {      &quot;name&quot;: &quot;textures&quot;,      &quot;value&quot;: &quot;eyJ0aW1lc3RhbXAiOjE1MDIyMDA5OTAwMjgsInByb2ZpbGVJZCI6ImQzYWY3NTNiLTdjZGEtNDY2Ni1hZGMyLWZmOWJiYTg1ZTBlYiIsInByb2ZpbGVOYW1lIjoiNjIxc2FtYSIsImlzUHVibGljIjp0cnVlLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly9za2luLmRldi90ZXh0dXJlcy84MzRjYmQ4NDhmMGEyOTAwOGJmNWIxZDU5ZDAyZWNiMWNmMjVkZmQyMWZjODhiZTFjMTgzYzkyNjFmNWZkZDY5In0sIkNBUEUiOnsidXJsIjoiaHR0cDovL3NraW4uZGV2L3RleHR1cmVzLzI5MTE0MzhlODI4MmQ0MGU2ZDY0ZmJlZmQwNzZlZWYwYTkwMWNiOTBkM2RlYWU0MDU3ZmVjNjBjNjZlYjkzZDIifX0sInNpZ25hdHVyZVJlcXVpcmVkIjp0cnVlfQ==&quot;,      &quot;signature&quot;: &quot;Zvox4YClUMHIAMe1tRLV/JmMaGF0pZhkmrigFpo7jOme8f559gZVyBQoTXeZsXn7Hwq5TE0b9m09MzuAGoT7dQ7kxkHA60xvVQXMQlbWP5O+EA8fzOM0hgINe8Qv7hSBG89osr+wWE7pTJ1CIKD6CBoK1a/U9UiCyQuDlO2gnfnXebBDIXJCBMKiowTu1LubZ9EQn7WkgrFD/M7TY+2dr8DOdoq15Pv0EZ2kLO1Gu9y6vOPq+5nAhce/TN/sWGAvfCJJkSYqALBSFh7QkExTJXPM7QHgP++rn96m6/nDe/ND6NwEovwdVqD5KiPnTvzRLkr92QEdZniT6hH2DUrToA==&quot;    }  ]}</code></pre><p>好吧好吧，看到这么多字符先别懵，<code>value</code> 和 <code>signature</code> 字段的内容都是 BASE64 编码过的，解码后 <code>value</code> 字段就是个普通的 JSON 而已。至于 JSON 里是什么内容，就自己去看 wiki 吧。</p><h4 id="4-5-2-数字签名"><a href="#4-5-2-数字签名" class="headerlink" title="4.5.2 数字签名"></a>4.5.2 数字签名</h4><p>需要注意的是上述 Profile 中的 <code>signature</code> 字段。顾名思义，这个字段就是 <code>value</code> 字段的数字签名。虽然官方 API 只有在指定 <code>unsigned=false</code> 时才会返回带签名的 Profile，但是目前（截至本文发布） authlib-injector 在服务端未返回数字签名时会出现<a href="https://github.com/printempw/blessing-skin-server/issues/81" target="_blank" rel="noopener">神秘的错误</a>，所以还是默认返回 <code>signature</code> 字段来得好。</p><p>至于数字签名如何生成，其实就是用的 OpenSSL 内置的签名算法。各个平台都有 OpenSSL 库的实现，我这里贴一下 PHP 的示例代码：</p><pre><code class="php">$privateKeyPath = __DIR__.&#39;/key.pem&#39;;// Load private keyif (! file_exists($privateKeyPath)) {  throw new IllegalArgumentException(&#39;RSA 私钥不存在&#39;);}$privateKeyContent = file_get_contents($privateKeyPath);$key = openssl_pkey_get_private($privateKeyContent);if (! $key) {  throw new IllegalArgumentException(&#39;无效的 RSA 私钥&#39;);}openssl_sign($data, $sign, $key);openssl_free_key($key);return base64_encode($sign);</code></pre><p>其他语言大同小异，我就不多赘述了。</p><h4 id="4-5-3-加载材质"><a href="#4-5-3-加载材质" class="headerlink" title="4.5.3 加载材质"></a>4.5.3 加载材质</h4><p>拿到角色 Profile，并且验证了数字签名后（签名不对的话不会加载的），Minecraft 游戏就会根据 Profile 中指定的皮肤、披风图片 URL 加载材质。需要注意的是，Minecraft 自带的 authlib 是只会加载 Mojang 官方域名下的材质的（白名单之外的材质地址是不会被加载的），这也是为什么需要 CustomSkinLoader 等皮肤 Mod 的原因。不过 <a href="https://github.com/to2mbn/authlib-injector/blob/master/configure.sh" target="_blank" rel="noopener">authlib-injector</a> 自带了对 authlib 的 hack，在配置文件（或者远程配置加载）中直接指定材质加载白名单即可。</p><p>如果一切正常，游戏内就会显示你的自定义皮肤了。</p><h3 id="4-5-加入服务器"><a href="#4-5-加入服务器" class="headerlink" title="4.5 加入服务器"></a>4.5 加入服务器</h3><p>在 Minecraft 中加入一个服务器时，客户端会向 <code>/join</code> API 发出一个请求，请求体中包含了 AccessToken、当前角色的 UUID 以及服务器的唯一标识符 <code>serverId</code>（这玩意如何生成不用我们操心，Minecraft 游戏里会搞好的，你只管存这个就行了）。</p><p>在后端实现上，一般来说就是在 Redis 这类内存数据库中放一个键值对，具体数据结构你自己想。</p><p>向 Yggdrasil API 发送完 <code>join</code> 请求后，Minecraft 客户端会向要加入的那个游戏服务器发送一个请求（这部分我们不用操心），服务器收到加入请求后，会向 Yggdrasil API 发送一个 <code>hasJoined</code> 请求（Query String 中包含角色名、IP 以及服务器唯一标识符），如果该用户已经加入了服务器（也就是判断数据库中有没有之前 <code>join</code> 时添加的记录），那就返回角色的完整 Profile，同时服务器允许用户进入。</p><p>这也就是为什么客户端和服务端同样需要使用 authlib-injector hack 的原因，因为我们要确保两者请求的都是同一个 API，这样才能起到一个维护登录状态的功能。</p><h3 id="4-6-经常用到的-API"><a href="#4-6-经常用到的-API" class="headerlink" title="4.6 经常用到的 API"></a>4.6 经常用到的 API</h3><p>虽然 Yggdrasil 规范中定义了很多 API，但是其实日常游戏中用到的没几个，这里列举一些频繁使用的 API，也方便诸君知道哪里该认真开发哪里可以小小偷懒一下：</p><pre><code># 初次登录时，用账号密码拿到 AccessTokenPOST /authserver/authenticate# 之后的登录都是直接用这个 API 签发新的令牌POST /authserver/refresh# 加入服务器POST /sessionserver/session/minecraft/join# 验证是否加入了服务器GET  /sessionserver/session/minecraft/hasJoined# 获取玩家完整 ProfileGET  /api/profiles/minecraft/{uuid}</code></pre><p>其他 API 感觉都是几万年用不到一次的，很神秘。</p><h2 id="五、后记"><a href="#五、后记" class="headerlink" title="五、后记"></a>五、后记</h2><p>上周折腾了好几天这玩意，写篇博文记录一下，既能理清自己的思路，还有可能帮到后来人<del>（花时间研究了东西，却没人知道，多亏啊）</del>，何乐而不为呢 :P</p><h3 id="5-1-参考链接"><a href="#5-1-参考链接" class="headerlink" title="5.1 参考链接"></a>5.1 参考链接</h3><ul><li><a href="http://wiki.vg/Authentication" target="_blank" rel="noopener">http://wiki.vg/Authentication</a></li><li><a href="http://wiki.vg/Protocol_Encryption#Authentication" target="_blank" rel="noopener">http://wiki.vg/Protocol_Encryption#Authentication</a></li><li><a href="http://wiki.vg/Mojang_API" target="_blank" rel="noopener">http://wiki.vg/Mojang_API</a></li><li><a href="https://github.com/to2mbn/authlib-injector/" target="_blank" rel="noopener">https://github.com/to2mbn/authlib-injector/</a></li><li><a href="https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E5%94%AF%E4%B8%80%E8%AF%86%E5%88%AB%E7%A0%81" target="_blank" rel="noopener">https://zh.wikipedia.org/wiki/通用唯一识别码</a></li><li><a href="http://xbgtalk.biz/2015/11/03/php-rsa-encode-decode-sign/" target="_blank" rel="noopener">php-rsa - 加密解密和签名</a></li><li><a href="blessing-skin-plugins/yggdrasil-api">blessing-skin-plugins/yggdrasil-api</a></li></ul><h3 id="5-2-文章更新日志"><a href="#5-2-文章更新日志" class="headerlink" title="5.2 文章更新日志"></a>5.2 文章更新日志</h3><p>具体的修改可以查看这篇博客在 GitHub 上源码的 <a href="https://github.com/printempw/printempw.github.io/commits/source/source/_posts/minecraft-yggdrasil-api-third-party-implementation.md" target="_blank" rel="noopener">历史提交记录</a>。</p><p><strong>2018-02-22：</strong></p><ul><li>基于最新的 authlib-injector 修改文章</li><li>将具体部署步骤移动至 <a href="https://github.com/printempw/yggdrasil-api" target="_blank" rel="noopener">printempw/yggdrasil-api</a> 页面</li><li>同时也更新了 MCBBS 上的 <a href="http://www.mcbbs.net/thread-718219-1-1.html" target="_blank" rel="noopener">相关帖子</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近给 Blessing Skin 写了个插件，利用皮肤站本身的账号系统实现了 Yggdrasil API（就是 Mojang 的登录 API），然后配合 &lt;a href=&quot;https://github.com/to2mbn/authlib-injector/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;authlib-injector&lt;/a&gt; 这个项目将启动器（基于 Java 编写的支持正版登录的启动器都行）、Minecraft 游戏、Minecraft 服务端中的 Mojang Yggdrasil API 地址给替换成了自己实现的第三方 Yggdrasil API 地址（字节码替换），从而实现了与正版登录功能几乎完全相同的账户鉴权系统。&lt;/p&gt;
&lt;p&gt;通俗地讲，就是我把 Mojang 的正版登录 API 给【劫持】成自己的啦，所以可以像登录正版那样直接用皮肤站的邮箱和密码登录游戏（还支持 Mojang 都不支持的多用户选择哦）。这种外置登录系统的实现应该可以说是比市面上的软件都要完善（毕竟可以直接利用 Minecraft 本身自带的鉴权模块），因此写一篇博文介绍一下这些实现之间的不同之处，顺带记录一下实现 Yggdrasil API 时踩到的坑，算是抛砖引玉了。&lt;/p&gt;
&lt;div class=&quot;alert alert-warning&quot;&gt;&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：本文不适合小白及问题解决能力弱的人群阅读。&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;&lt;del&gt;感觉我明明好久没玩 MC 了，要玩也都是玩正版服务器，但是却一直在搞这些盗版服用的东西，我真是舍己为人造福大众普惠众生啊（不&lt;/del&gt;&lt;/p&gt;
&lt;h2 id=&quot;一、服务器内置登录插件&quot;&gt;&lt;a href=&quot;#一、服务器内置登录插件&quot; class=&quot;headerlink&quot; title=&quot;一、服务器内置登录插件&quot;&gt;&lt;/a&gt;一、服务器内置登录插件&lt;/h2&gt;&lt;p&gt;相信维护过 Minecraft 服务器（当然，我这边说的是运行在离线模式下的服务器）的腐竹们或多或少都听说过 Authme、CrazyLogin 等登录插件的鼎鼎大名吧。由于这些服务器运作在离线模式（&lt;code&gt;online-mode=false&lt;/code&gt;，即俗称的盗版模式）下，缺少 Mojang 官方账户认证系统的支持，所以必须使用这类插件来进行玩家认证（否则随便谁都可以冒名顶替别人了，换一个登录角色名就行）。&lt;/p&gt;
&lt;p&gt;这类插件的工作原理就是在服务端维护一个数据表，表中每一条记录中存储了角色的「角色名」、「登录密码」、「注册时间」、「登录 IP 地址」等等信息，当玩家初次进入服务器时需要通过这些插件进行注册操作（e.g. &lt;code&gt;/register&lt;/code&gt; 命令）并在表中插入一条记录，注册完毕后进入服务器则需要输入密码（e.g. &lt;code&gt;/login &amp;lt;password&amp;gt;&lt;/code&gt; 命令）来认证。&lt;/p&gt;
&lt;p&gt;其实这样的解决方案也没什么不好，而且现在 Authme 等登录插件在众多的服务器中都还是主流。但是，如果你的服务器已经发展到比较大型了，或许你就比较希望有这样一个东西：&lt;/p&gt;
    
    </summary>
    
      <category term="技术" scheme="https://blessing.studio/categories/tech/"/>
    
    
      <category term="PHP" scheme="https://blessing.studio/tag/PHP/"/>
    
      <category term="Minecraft" scheme="https://blessing.studio/tag/Minecraft/"/>
    
      <category term="ACGN" scheme="https://blessing.studio/tag/ACGN/"/>
    
      <category term="教程" scheme="https://blessing.studio/tag/%E6%95%99%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>Laravel 动态添加 Artisan 命令的最佳实践</title>
    <link href="https://blessing.studio/best-practice-of-adding-artisan-commands-dynamically/"/>
    <id>https://blessing.studio/best-practice-of-adding-artisan-commands-dynamically/</id>
    <published>2017-07-30T10:45:50.000Z</published>
    <updated>2017-07-30T10:45:50.000Z</updated>
    
    <content type="html"><![CDATA[<p>虽然 Laravel 官方文档提供的添加 Artisan Command 的方法是直接修改 <code>app/Console/Kernel.php</code> 文件并在 <code>$commands</code> 属性中注册要添加的 Artisan 命名的类名（Laravel 服务容器会自动解析），但是，如果我们出现需要「动态（运行时）添加 Artisan 命令」的需求的话，就会很容易吃瘪。因为，Laravel 的文档（当然，我说的是官网上的）几乎没有提到任何关于这方面的内容。</p><p>这也是我为什么总是吐槽 Laravel 文档有些地方很烂的原因 —— 很多时候你为了实现一个文档里没提到的功能，需要去翻半天 Laravel 的框架源码才能找到解决方法（我博客的 <a href="https://blessing.studio/tag/Laravel/">Laravel 标签</a> 下已经有不少这样的踩坑文了）。虽然 Laravel 框架的源码很优雅，看着也不会难受，但是在一堆文件中跳来跳去寻找逻辑浪费脑细胞的行为还是能省则省吧 :(</p><p>这次要实现的功能是在运行时动态加载自定义的 Artisan Command（更详细一些的需求就是在皮肤站的一个插件中注册 Artisan 命令，Laravel 插件系统的实现可以参考我之前的 <a href="https://blessing.studio/laravel-plugin-system-1/">另一篇文章</a>）。</p><h2 id="TL-DR-太长不看"><a href="#TL-DR-太长不看" class="headerlink" title="TL;DR 太长不看"></a>TL;DR 太长不看</h2><p>总之先上干货，毕竟不是所有人都喜欢听我废话一大堆后才拿到解决方案的。</p><p>Laravel 5.3 及以上：</p><pre><code class="php">Artisan::starting(function ($artisan) {    // 传入类名字符串即可，会被服务容器自动解析    $artisan-&gt;resolve(&#39;Example\FooCommand&#39;);    // 批量添加    $artisan-&gt;resolveCommands([        &#39;Example\FuckCommand&#39;,        &#39;Example\ShitCommand&#39;    ]);    // 参数必须为 Symfony\Component\Console\Command\Command 的实例    // 继承自 Illuminate\Console\Command 的类实例也可以    $artisan-&gt;add($command);});</code></pre><p>Laravel 5.2：</p><pre><code class="php">Event::listen(&#39;Illuminate\Console\Events\ArtisanStarting&#39;, function ($event) {    // 其他用法同上    $event-&gt;artisan-&gt;resolve(&#39;Example\BarCommand&#39;);});</code></pre><a id="more"></a><p>Laravel 5.1：</p><pre><code class="php">Event::listen(&#39;artisan.start&#39;, function ($event) {    // 其他用法同上    $event-&gt;artisan-&gt;resolve(&#39;Example\WtfCommand&#39;);});</code></pre><p>接下来就是我摸索时尝试的步骤，写下来权当记录<del>水博文</del>，发了发牢骚，有兴趣的就继续看下去吧。</p><h2 id="0x01-初步尝试"><a href="#0x01-初步尝试" class="headerlink" title="0x01 初步尝试"></a>0x01 初步尝试</h2><p>既然 Laravel 最常见的注册 Artisan 命令的方式是修改 <code>APP\Console\Kernel</code> 类中的 <code>$commands</code>，那么一般正常人都会从这边开始下手。可以看到，这个类是继承自 <code>Illuminate\Foundation\Console\Kernel</code> 类并覆写了 <code>$commands</code> 属性。让我们稍微看一下这个 <code>$commands</code> 属性用在哪了：</p><pre><code class="php">/** * Get the Artisan application instance. * * @return \Illuminate\Console\Application */protected function getArtisan(){    if (is_null($this-&gt;artisan)) {        return $this-&gt;artisan = (new Artisan($this-&gt;app, $this-&gt;events, $this-&gt;app-&gt;version()))                            -&gt;resolveCommands($this-&gt;commands);    }    return $this-&gt;artisan;}</code></pre><p>可以看到，这个方法用单例模式实例化了一个 Artisan（<code>Artisan</code> 是 <code>Illuminate\Console\Application</code> 的别名），其中最重要的是调用了 <code>Illuminate\Console\Application::resolveCommands</code> 这个方法，并且将那个注册了自定义 Artisan 命令的属性给传了进去。我们跳转到那个 <code>resolveCommands</code> 方法看一看……</p><pre><code class="php">/** * Add a command, resolving through the application. * * @param  string  $command * @return \Symfony\Component\Console\Command\Command */public function resolve($command){    return $this-&gt;add($this-&gt;laravel-&gt;make($command));}/** * Resolve an array of commands through the application. * * @param  array|mixed  $commands * @return $this */public function resolveCommands($commands){    $commands = is_array($commands) ? $commands : func_get_args();    foreach ($commands as $command) {        $this-&gt;resolve($command);    }    return $this;}</code></pre><p>代码条理很清晰，挨个儿把那些 <code>$commands</code> 中的元素给丢进 Laravel 服务容器里实例化之后，调用父类方法 <code>Symfony\Component\Console\Application::add</code> （是的，Laravel 用了很多很多 Symfony 的组件）添加到自身实例中，持引用以供之后的调用所需。</p><p>继续翻看 <code>Illuminate\Foundation\Console\Kernel</code> 的源码，可以看到 Laravel 贴心地开放了一个 <code>registerCommand</code> 方法：</p><pre><code class="php">/** * Register the given command with the console application. * * @param  \Symfony\Component\Console\Command\Command  $command * @return void */public function registerCommand($command){    $this-&gt;getArtisan()-&gt;add($command);}</code></pre><p>那么我们要做的就是，在运行时中拿到 <code>Kernel</code> 的实例，并且通过调用 <code>registerCommand</code> 方法把我们的自定义 Artisan 命令也给加进去。那么我们要怎样才能拿到这个实例呢？</p><p>相信对 Laravel 有所了解的各位都会想到 —— 服务容器。</p><p>通过查阅 Laravel 命令行入口（根目录下的 <code>artisan</code> 文件）源码可以知道，Laravel 就是使用服务容器来实例化 <code>Kernel</code> 的：</p><pre><code class="php">$kernel = $app-&gt;make(Illuminate\Contracts\Console\Kernel::class);</code></pre><p>如果你有心的话，会发现 Laravel 框架的 Web 入口文件（<code>public/index.php</code>）和命令行入口文件中实例化 <code>Kernel</code> 的语句都是一样的，那么为什么通过 Web 访问时解析出来的是 <code>App\Http\Kernel</code> 的实例而通过命令行访问时解析出来的就是 <code>App\Console\Kernel</code> 的实例了呢？</p><p>这里就涉及 Laravel 服务容器的一个强大的核心功能 —— 绑定接口至实现。因为这些实例都实现了相同的接口，所以我们可以使用相同的代码并且很方便地更换接口后的具体实现，这也是使用 IoC 容器的好处之一，有兴趣的多去了解了解吧 :)</p><p>闲话休提，那么我们只要通过服务容器就可以拿到 <code>Kernel</code> 实例了（当然，如果你愿意，你也可以直接通过 <code>$GLOBAL[&#39;kernel&#39;]</code> 来访问全局作用域下定义的那个 <code>$kernel</code> 变量，效果都是一样的，但是太 tmd lowb 了，所以我不愿意用），看起来已经离成功了一大半呢！</p><pre><code class="php">$kernel = app(&#39;Illuminate\Contracts\Console\Kernel&#39;);// 因为 registerCommand 方法只接受 Symfony\Component\Console\Command\Command 的实例作为参数$kernel-&gt;registerCommand(app(&#39;Example\FooCommand&#39;));</code></pre><p>然后我们执行一下 <code>php artisan list</code>，就能看到我们的命令已经出现啦：</p><pre><code>Laravel Framework version 5.2.45Usage:  command [options] [arguments]Available commands:  help           Displays help for a command  list           Lists commands  foo            Example command</code></pre><p>但是等等……Laravel 自带的那些 <code>make</code>、<code>migrate</code> 等命令哪里去了？我最开始出现这个问题的时候还以为是我太早把 <code>Kernel</code> 解析出来了，后来直接使用 <code>$GLOBALS[&#39;kernel&#39;]</code> 也是一样的问题时才认识到问题另有原因。仔细阅读源码后发现 Artisan 命令行在调用（<code>handle</code>、<code>call</code> 等方法）之前都会调用这样一个方法：</p><pre><code class="php">$this-&gt;bootstrap();</code></pre><style>.post-content table, .post-content code { word-wrap: break-word; }</style><p>通过阅读源码可以知道这个 <code>bootstrap</code> 方法就是用来加载 Laravel 框架的基本组件的，包括 <code>Illuminate\Foundation\Providers\ArtisanServiceProvider</code> 这个服务提供者中提供的所有框架内置 Artisan 命令。好在这个方法是 public 的，所以我们只要在 <code>registerCommand</code> 之前调用一下这个方法就可以啦：</p><pre><code class="php">$kernel = app(&#39;Illuminate\Contracts\Console\Kernel&#39;);$kernel-&gt;bootstrap();$kernel-&gt;registerCommand(app(&#39;Example\FooCommand&#39;));</code></pre><p>如果你愿意，你甚至还可以直接使用 <code>Artisan</code> 这个 <code>Facade</code>，因为它就是指向 <code>Illuminate\Contracts\Console\Kernel</code> 的：</p><pre><code class="php">Artisan::bootstrap();Artisan::registerCommand(app(&#39;InsaneProfileCache\Commands\Clean&#39;));</code></pre><p>结果如下：</p><p><img src="https://i.loli.net/2017/07/30/597df1d0e5231.png" alt="Screenshot"></p><h2 id="0x02-继续尝试"><a href="#0x02-继续尝试" class="headerlink" title="0x02 继续尝试"></a>0x02 继续尝试</h2><p>虽然这样确实能够实现我们的需求，但是我觉得这样不行（话说我都不晓得嘻哈梗怎么突然就流行起来了，虽然确实蛮有意思的啦）。</p><p><img src="https://img.blessing.studio/images/2017/07/30/934f97e408371023.png" alt="我觉得不行"></p><p>又要自己取出 <code>Kernel</code> 实例，又要自己调用 <code>bootstrap</code> 方法，调用 <code>registerCommand</code> 方法之前还有自己先把 Command 实例化……这么繁琐，肯定不是运行时添加 Artisan 命令的最佳实践，所以我决定继续寻找更优解。</p><p>虽然我们上面用的方法是取出 <code>Kernel</code> 实例并进行操作的，但是其实该方法里的操作也是基于 <code>getArtisan</code> 所获取的  <code>Illuminate\Console\Application</code> （👈这玩意在 Laravel 源码里经常被 as 为 <code>Artisan</code>）实例进行的。可惜的是这个方法是 <code>protected</code> 的，我们无法直接调用它，所以我们还是先去看这个类的源码吧：</p><pre><code class="php">/** * Create a new Artisan console application. * * @param  \Illuminate\Contracts\Container\Container  $laravel * @param  \Illuminate\Contracts\Events\Dispatcher  $events * @param  string  $version * @return void */public function __construct(Container $laravel, Dispatcher $events, $version){    parent::__construct(&#39;Laravel Framework&#39;, $version);    $this-&gt;laravel = $laravel;    $this-&gt;setAutoExit(false);    $this-&gt;setCatchExceptions(false);    $events-&gt;fire(new Events\ArtisanStarting($this));}</code></pre><p>瞧我发现了什么？Artisan 在实例化之后会触发一个 <code>Illuminate\Console\Events\ArtisanStarting</code> 事件，并且把自身实例给传递过去。那么我们要做的就很简单了：监听该事件，拿到 Artisan 实例，调用 <code>resolve</code> 或 <code>resolveCommands</code> 方法来注册我们的 Artisan 命令即可。</p><p>具体的方法在最上面给出了，我这里就不多说了。另外需要注意的是，Laravel 5.1 版本并没有 <code>ArtisanStarting</code> 这个事件，而是 <code>artisan.start</code>，不过原理都是一样的：</p><pre><code class="php">$events-&gt;fire(&#39;artisan.start&#39;, [$this]);</code></pre><p>另外，在 Laravel 5.3 及以上版本中，Artisan 还贴心地提供了 <code>Artisan::starting</code> 这个方法，和监听事件的效果差不多，不过是直接修改实例的 <code>$bootstrappers</code> 属性的，传递一个闭包进去即可，示例代码见最上方。</p><h2 id="0x03-一些牢骚"><a href="#0x03-一些牢骚" class="headerlink" title="0x03 一些牢骚"></a>0x03 一些牢骚</h2><p>虽然只要看源码就能知道，Laravel 框架很多地方都预留了非常多的接口，让我们可以方便优雅地实现很多自定义功能，这也是我为什么喜欢这个框架的原因之一。</p><p>但是……但是，你的文档就不能写好一点吗！哪怕提一下这些 API 也好啊！</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;虽然 Laravel 官方文档提供的添加 Artisan Command 的方法是直接修改 &lt;code&gt;app/Console/Kernel.php&lt;/code&gt; 文件并在 &lt;code&gt;$commands&lt;/code&gt; 属性中注册要添加的 Artisan 命名的类名（Laravel 服务容器会自动解析），但是，如果我们出现需要「动态（运行时）添加 Artisan 命令」的需求的话，就会很容易吃瘪。因为，Laravel 的文档（当然，我说的是官网上的）几乎没有提到任何关于这方面的内容。&lt;/p&gt;
&lt;p&gt;这也是我为什么总是吐槽 Laravel 文档有些地方很烂的原因 —— 很多时候你为了实现一个文档里没提到的功能，需要去翻半天 Laravel 的框架源码才能找到解决方法（我博客的 &lt;a href=&quot;https://blessing.studio/tag/Laravel/&quot;&gt;Laravel 标签&lt;/a&gt; 下已经有不少这样的踩坑文了）。虽然 Laravel 框架的源码很优雅，看着也不会难受，但是在一堆文件中跳来跳去寻找逻辑浪费脑细胞的行为还是能省则省吧 :(&lt;/p&gt;
&lt;p&gt;这次要实现的功能是在运行时动态加载自定义的 Artisan Command（更详细一些的需求就是在皮肤站的一个插件中注册 Artisan 命令，Laravel 插件系统的实现可以参考我之前的 &lt;a href=&quot;https://blessing.studio/laravel-plugin-system-1/&quot;&gt;另一篇文章&lt;/a&gt;）。&lt;/p&gt;
&lt;h2 id=&quot;TL-DR-太长不看&quot;&gt;&lt;a href=&quot;#TL-DR-太长不看&quot; class=&quot;headerlink&quot; title=&quot;TL;DR 太长不看&quot;&gt;&lt;/a&gt;TL;DR 太长不看&lt;/h2&gt;&lt;p&gt;总之先上干货，毕竟不是所有人都喜欢听我废话一大堆后才拿到解决方案的。&lt;/p&gt;
&lt;p&gt;Laravel 5.3 及以上：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;php&quot;&gt;Artisan::starting(function ($artisan) {
    // 传入类名字符串即可，会被服务容器自动解析
    $artisan-&amp;gt;resolve(&amp;#39;Example\FooCommand&amp;#39;);
    // 批量添加
    $artisan-&amp;gt;resolveCommands([
        &amp;#39;Example\FuckCommand&amp;#39;,
        &amp;#39;Example\ShitCommand&amp;#39;
    ]);
    // 参数必须为 Symfony\Component\Console\Command\Command 的实例
    // 继承自 Illuminate\Console\Command 的类实例也可以
    $artisan-&amp;gt;add($command);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Laravel 5.2：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;php&quot;&gt;Event::listen(&amp;#39;Illuminate\Console\Events\ArtisanStarting&amp;#39;, function ($event) {
    // 其他用法同上
    $event-&amp;gt;artisan-&amp;gt;resolve(&amp;#39;Example\BarCommand&amp;#39;);
});
&lt;/code&gt;&lt;/pre&gt;
    
    </summary>
    
      <category term="技术" scheme="https://blessing.studio/categories/tech/"/>
    
    
      <category term="Laravel" scheme="https://blessing.studio/tag/Laravel/"/>
    
      <category term="PHP" scheme="https://blessing.studio/tag/PHP/"/>
    
  </entry>
  
  <entry>
    <title>饥荒联机版独立服务器搭建踩坑记录</title>
    <link href="https://blessing.studio/deploy-dont-starve-together-dedicated-server/"/>
    <id>https://blessing.studio/deploy-dont-starve-together-dedicated-server/</id>
    <published>2017-07-15T09:54:24.000Z</published>
    <updated>2017-07-15T09:54:24.000Z</updated>
    
    <content type="html"><![CDATA[<p>最近和几个同学一起联机玩饥荒（Don’t Starve Together），虽然饥荒游戏本身就可以直接创建房间让别人加入，但还是有诸多不便驱使我去开一个饥荒的独立服务端（Dedicated Server），其中最主要的就是 ——「<strong>你退了游戏其他人就玩不了了</strong>」。</p><p>本来家里还有一个用旧主板和以前换下的配件攒成的二奶机，安装的是 Elementary OS，确实可以拿来跑饥荒服务器（以前还拿它跑过 MC 服务器），但是不幸的是，由于我直接把主板硬盘之类的一股脑塞在牛奶盒子里放在窗边还不加盖儿，一个雨后的下午，我推开家门后发现那个被我当做机箱的牛奶盒子已经开始积水了……前略，天国的 Pegatron IPX31-GS (・_ゝ・)</p><p>直接在主力机上开一个服务器也不是不行，但是我的奔腾 G3258 选手实在是带不动饥荒游戏 + 游戏服务器 + 其他杂七杂八的东西了，所以只好另觅他方，去搞一台 VPS 来开服。而且这里不得不吐槽一下，饥荒联机版独立服务器的配置要求还是比较高的，几个人的小服，再多开几个 Mod，最起码就要 1G 的内存（我那台阿里云宕机好几次，还得去网页控制台强制重启），更不要想开洞穴了。</p><p>现在我拿来开服的是免费试用一个月的京东云（Xeon-E5，2G DDR4，1Mbps 的带宽），不开洞穴，目前看起来还是没什么压力的，延迟丢包率什么的也都可以接受。网上关于开服的教程也不少了，这篇文章也不会过多赘述，差不多就是记录一下主要步骤，以及提一下可能会遇到的坑。所以，想要那种很详细的教程的同学还是绕道吧，或者翻到文章最下面的「参考链接」看看。</p><p>下面的步骤在 64 位 CentOS 7.3 和 Ubuntu 16.04 上测试通过，至于 Windows……我认为实在没有啥必要特别去写，直接从 Steam 客户端就可以打开，操作方便，也没啥坑，看看网上那些教程就可以了。继续阅读之前，我希望你能有一些 Linux 的操作基础，不然会很懵。</p><h2 id="0x01-事前准备"><a href="#0x01-事前准备" class="headerlink" title="0x01 事前准备"></a>0x01 事前准备</h2><p>首先你要有一台装了 Linux 的服务器，配置要求如下（摘自 <a href="http://dontstarve.wikia.com/wiki/Don%E2%80%99t_Starve_Together_Dedicated_Servers" target="_blank" rel="noopener">DST Wiki</a>）：</p><ul><li>上行带宽：8KBps 一个玩家；</li><li>内存：差不多一个玩家 65Mbytes；</li><li>CPU：没太大要求</li></ul><p>需要注意的是，饥荒联机版的服务器对内存的要求其实挺大的，亲测只开 Overworld 不开洞穴，空载 RAM 占用约 800MB，再加上差不多 65MB 一个玩家，开 Mod 还会占用更多，所以还是要衡量一下机器的配置。</p><a id="more"></a><p>然后是喜闻乐见的依赖安装环节：</p><pre><code class="shell"># Ubuntu/Debian 32 位$ sudo apt-get -y install libgcc1 libcurl4-gnutls-dev# Ubuntu/Debian 64 位$ sudo apt-get -y install lib32gcc1 libcurl4-gnutls-dev:i386# RedHat/CentOS 32 位$ sudo yum -y install glibc libstdc++ libcurl4-gnutls-dev# RedHat/CentOS 64 位$ sudo yum -y install glibc.i686 libstdc++.i686 libcurl4-gnutls-dev.i686</code></pre><p>有些源里可能没有 libcurl4-gnutls-dev，那直接安装 <code>libcurl</code> 然后做个软链接也是可以的：</p><pre><code class="shell">$ cd /usr/lib/$ ln -s libcurl.so.4 libcurl-gnutls.so.4</code></pre><h2 id="0x02-安装-SteamCMD"><a href="#0x02-安装-SteamCMD" class="headerlink" title="0x02 安装 SteamCMD"></a>0x02 安装 SteamCMD</h2><p>SteamCMD，顾名思义，就是 Steam 的命令行版本。虽然饥荒服务器本身并不需要用 Steam 进行验证啊之类的，但我们还是得用它来把服务器更新到最新版本，不然其他人是进不来的。</p><p>我们最好新建一个用户来运行 SteamCMD，如果直接用 root 用户运行游戏服务端的话可能会导致严重的安全隐患。在 root 权限下使用以下命令来创建一个新用户：</p><pre><code class="shell">$ useradd -m steam$ su - steam</code></pre><p>然后在你喜欢的地方创建一个为 SteamCMD 准备的目录：</p><pre><code class="shell">$ mkdir ~/steamcmd &amp;&amp; cd ~/steamcmd</code></pre><p>下载并解压 Linux 专用的 SteamCMD：</p><pre><code class="shell">$ wget https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz$ tar -xvzf steamcmd_linux.tar.gz</code></pre><p>运行 SteamCMD：</p><pre><code class="shell">$ ./steamcmd.sh</code></pre><p>登录安装退出一气呵成：</p><pre><code># 匿名登录，没必要用用户名密码登录login anonymous# 这里我们强制要 Steam 把饥荒服务端安装到此目录下# 最好用绝对路径，否则可能会安装到奇怪的地方去force_install_dir /home/steam/dstserverapp_update 343050 validatequit</code></pre><p><img src="https://i.loli.net/2017/07/15/596a251199343.png" alt="SteamCMD 截图"></p><p>等进度跑完，饥荒服务端就已经安装在指定位置了：</p><p><img src="https://img.blessing.studio/images/2017/07/15/snipaste_20170715_222348.png" alt="dstserver 截图"></p><p>另外，SteamCMD 也是可以用 apt、yum 之类的包管理器来安装的，如果你的源里有的话（阿里云的镜像源里是有的，Debian 的话还得在源里加上 non-free area）。直接用包管理器安装的话 SteamCMD 可执行文件是安装到 <code>/usr/games</code> 目录下的，可以软链接到方便的地方去：</p><pre><code class="shell">$ ln -s /usr/games/steamcmd ~/steamcmd</code></pre><p>用法和上面的 <code>steamcmd.sh</code> 是一样的。</p><h2 id="0x03-运行饥荒服务端"><a href="#0x03-运行饥荒服务端" class="headerlink" title="0x03 运行饥荒服务端"></a>0x03 运行饥荒服务端</h2><p>如果上面 SteamCMD 里没有用 <code>force_install_dir</code> 强制指定安装目录的话，服务端默认是安装到 <code>~/Steam/steamapps/common/Don&#39;t Starve Together Dedicated Server</code> 目录下的。我们先进去运行一下服务端，确保依赖之类的配置正确：</p><pre><code class="shell">$ cd ~/dstserver/bin$ ./dontstarve_dedicated_server_nullrenderer</code></pre><p>有可能会报这样的错：</p><pre><code>error while loading shared libraries: libcurl-gnutls.so.4: cannot open shared object file: No such file or directory</code></pre><p>这说明你 libcurl-gnutls 依赖没安装或者没配置好，可参照上面的「事前准备」安装依赖。</p><p>注意，<strong>千万不要</strong> 在其他目录下使用类似 <code>./bin/dontstarve_dedicated_server_nullrenderer</code> 的命令来运行服务端，否则你会得到类似这样的找不到 <code>scripts/main.lua</code> 文件的报错信息：</p><pre><code>[00:00:00]: Starting Up...[00:00:00]: DoLuaFile scripts/main.lua[00:00:00]: DoLuaFile Could not load lua file scripts/main.lua[00:00:00]: Error loading main.lua...[00:00:00]: Shutting down</code></pre><p>我估计是相对路径的锅，害我在这个报错上疯狂找资料卡了半小时，说不出话。</p><p>另外注意开放 VPS 的 10999 端口（或者你的自定义端口）的 UDP 访问（iptables、firewalld、主机商的防火墙 etc.）。接下来的服务器配置之类的我就不多说了。</p><p>如果以后要升级服务端的话，直接再重复一遍上面 SteamCMD 安装的步骤就可以了，也可以直接一句话解决：</p><pre><code class="shell">$ ./steamcmd.sh +login anonymous +force_install_dir ~/dst +app_update 343050 validate +quit</code></pre><h2 id="0x04-服务端配置"><a href="#0x04-服务端配置" class="headerlink" title="0x04 服务端配置"></a>0x04 服务端配置</h2><p><style>.post-content table, .post-content code { word-wrap: break-word; }</style><br>虽然我们上面直接运行了 <code>dontstarve_dedicated_server_nullrenderer</code>，但是这个程序还是有其他的启动参数的。主要参数如下：</p><table><thead><tr><th>参数</th><th>用法</th></tr></thead><tbody><tr><td>-persistent_storage_root</td><td>指定存档根目录的位置，必须是绝对目录。默认为 <code>~/.klei</code>。</td></tr><tr><td>-conf_dir</td><td>指定配置文件的目录名。默认为 <code>DoNotStarveTogether</code>，和上一个参数拼在一起就是你存档的完整位置了，默认为 <code>~/.klei/DoNotStarveTogether</code>，所有的存档都在这里。</td></tr><tr><td>-cluster</td><td>指定启动的世界，默认为 <code>Cluster_1</code>。服务端启动时会去找 <code>&lt;persistent_storage_root&gt;/&lt;conf_dir&gt;/&lt;cluster&gt;</code> 目录下的 <code>cluster.ini</code> 这个配置文件，你的世界名称、密码、游戏模式之类的都是在这里配置的（网上有些教程里用的 <code>setting.ini</code>，那个是旧版的）。同理，你的存档文件夹也可以不使用类似 <code>Cluster_X</code> 的名字，改成其他什么乱七八糟的都可以，只要启动时指定本参数就行了。</td></tr><tr><td>-shard</td><td>默认为 <code>Master</code>，启动时将此参数指定为 <code>Cave</code> 就可以启动洞穴服务器。</td></tr></tbody></table><p>其他的参数我就不一一说明了，具体的参数列表可以在<a href="http://forums.kleientertainment.com/topic/64743-dedicated-server-command-line-options-guide/" target="_blank" rel="noopener">这里</a>查看。举个栗子：</p><pre><code class="shell"># 同时启动主世界服务器和洞穴服务器$ ./dontstarve_dedicated_server_nullrenderer -console -cluster MyClusterName -shard Master$ ./dontstarve_dedicated_server_nullrenderer -console -cluster MyClusterName -shard Caves</code></pre><p>游戏服务端会读取这些文件中的配置：</p><pre><code># 如果你用了 -persistent_storage_root 和 -conf_dir 指定了另外的目录# 那游戏就会去 &lt;persistent_storage_root&gt;/&lt;conf_dir&gt;/&lt;cluster&gt; 下查找这些文件# 如果没指定，默认如下：~/.klei/DoNotStarveTogether/MyClusterName/cluster.ini~/.klei/DoNotStarveTogether/MyClusterName/Master/server.ini~/.klei/DoNotStarveTogether/MyClusterName/Caves/server.ini</code></pre><p>至于如何在 <code>cluster_token.txt</code> 中填入你的 Token，以及 <code>cluster.ini</code> 和 <code>server.ini</code> 的内容之类的，我这里也不多说，具体可参照下面的「参考链接」。</p><h2 id="0x05-Mod-安装"><a href="#0x05-Mod-安装" class="headerlink" title="0x05 Mod 安装"></a>0x05 Mod 安装</h2><p>给饥荒联机版服务器添加 Mod 主要分两步。</p><p><strong>第一步</strong>，让服务器知道我们要用到什么，这样游戏运行时就会自动帮我们下载并安装这些 Mod（如果没有下载的话），并更新到最新版本。首先进入你服务器安装目录下的 <code>mods</code> 文件夹：</p><pre><code class="shell">$ cd /home/steam/dstserver/mods</code></pre><p>打开 <code>dedicated_server_mods_setup.lua</code> 文件，添加如下内容：</p><pre><code>ServerModSetup(&quot;758532836&quot;)</code></pre><p>其中那一串数字就是 Mod 在 Steam 创意工坊里的 ID（地址栏上就有），至于怎么获取，就看你的了。注意，每一个 Mod 都要用这样的格式在该文件中添加一行，所以最后添加完毕的画风应该是这样的：</p><p><img src="https://i.loli.net/2017/07/16/596b0d54cc5ed.png" alt="Mod List"></p><p>从这个文件的注释里你也能知道（如果你看得懂洋文的话），我们还可以直接在这个文件中使用类似 <code>ServerModCollectionSetup(&quot;ID&quot;)</code> 的格式来订阅合集中的所有 Mod，方便不少。</p><p><strong>第二步</strong>，启用 Mod。安装 Mod 之后，我们还需要配置一下每个存档对应要启用什么 Mod。</p><pre><code class="shell"># 如果你用启动参数把存档位置改到其他位置的话，就进去你自定义存档位置下的 Master 目录$ cd ~/.klei/DoNotStarveTogether/Cluster_1/Master</code></pre><p>然后编辑 <code>modoverrides.lua</code> 文件，填入以下内容：</p><pre><code class="lua">return {  [&quot;workshop-797304209&quot;]={ configuration_options={  }, enabled=true },  [&quot;workshop-806984122&quot;]={ configuration_options={  }, enabled=true },  [&quot;workshop-758532836&quot;]={    configuration_options={      AUTOPAUSECONSOLE=false,      AUTOPAUSEMAP=false,      AUTOPAUSESINGLEPLAYERONLY=true,      ENABLECLIENTPAUSE=false,      ENABLEHOTKEY=false,      KEYBOARDTOGGLEKEY=&quot;P&quot;     },    enabled=true   }}</code></pre><p>如果你懂一点 Lua 语法的话修改起来会比较得心应手。其中 <code>[&quot;workshop-758532836&quot;]</code> 就是 ID 为 <code>758532836</code> 的 Mod，<code>enabled=true</code> 表示启用该 Mod，<code>configuration_options = {}</code> 里的就是 Mod 的配置，具体可以去 Mod 的 <code>modinfo.lua</code> 文件里查阅。</p><p>但是，这是一个很麻烦的过程，所以我们可以用一些取巧的办法完成 Mod 的配置。</p><p><img src="https://i.loli.net/2017/07/16/596b1269ce00a.png" alt="可视化配置"></p><p>首先我们去 Steam 客户端里打开饥荒联机版的游戏，然后创建一个世界，把那些「你想要在服务器里启用的 Mod」都给启用了，并且直接在游戏的「Mod 配置」页面里配置好（有可视化界面，配置很方便），配置完毕后进入世界再退掉。这时候进去你的存档位置（比如：<code>Documents\Klei\DoNotStarveTogether\Cluster_3\Master</code>），把你本地的 <code>modoverrides.lua</code> 文件内容上传到服务器里就好了。</p><p>不然直接在文件里配置真的很痛苦，真的。</p><h2 id="0x06-存档迁移"><a href="#0x06-存档迁移" class="headerlink" title="0x06 存档迁移"></a>0x06 存档迁移</h2><p>另开新档的可以不用看了，这节主要是讲怎样把电脑 Steam 客户端里的饥荒联机版存档放到服务器里跑。</p><p>饥荒客户端的存档位置如下：</p><pre><code># WindowsDocuments\Klei\DoNotStarveTogether# Linux~/.klei/DoNotStarveTogether# MacOS~/Documents/Klei/DoNotStarveTogether</code></pre><p>接下来需要注意的是，你直接在饥荒联机版客户端里开的世界，如果没有开洞穴的话，存档是在 <code>client_save</code> 目录下的；只有当你开启了洞穴时，世界存档才会在 <code>Cluster_X</code> 目录下（X 就是世界在游戏中「创建世界」里对应的位置）。曾经我就因为备份错目录，当后来被删档想要回档时，发现以前备份的都是无效文件。我相信你不会想要碰到这种情况的🌚</p><p><img src="https://img.blessing.studio/images/2017/07/16/snipaste_20170716_140614.png" alt="存档"></p><p>那么 <code>Cluster_X</code> 里的内容和 <code>client_save</code> 里的有啥不一样呢？其实只要观察一下就能发现 <code>client_save</code> 里的目录结构是和 <code>Cluster_X/Master/save</code> 目录是一样的。如果你原来客户端里的存档就是开了洞穴的，那么直接把对应 <code>Cluster_X</code> 里的内容上传到服务端对应的目录下就 OK 了。如果不是，那也不会很麻烦：</p><p>首先，你可以先运行一下饥荒服务端，这样服务端就会自动帮你生成一个完整的存档目录（目录结构什么的都是完整的，就不用你自己去一个一个新建了），然后把 <code>client_save</code> 里的文件一股脑上传至 <code>YourClusterName/Master/save</code> 里面去就好了。</p><p>如果你想要自己建立存档目录，那么主要需要建立这几个目录及文件：</p><pre><code class="shell">$ tree.├── Caves # 如果你想要开洞穴的话│   └── server.ini├── cluster.ini├── cluster_token.txt└── Master    ├── modoverrides.lua    └── server.ini</code></pre><p>配置文件的内容可参考饥荒论坛的文档或下方「参考链接」。</p><h2 id="0x07-参考链接"><a href="#0x07-参考链接" class="headerlink" title="0x07 参考链接"></a>0x07 参考链接</h2><ul><li><a href="http://steamcommunity.com/sharedfiles/filedetails/?id=687261496" target="_blank" rel="noopener">从 steam 客户端建房 到 linux 建立服务器</a></li><li><a href="http://steamcommunity.com/sharedfiles/filedetails/?id=382584094" target="_blank" rel="noopener">DST Dedicated Server 服务器配置教程</a> 👈这篇的内容有些过期了</li><li><a href="https://www.nevermoe.com/?p=695" target="_blank" rel="noopener">Don’t Starve Together（饥荒）服务器搭建</a></li><li><a href="http://dontstarve.wikia.com/wiki/Don%E2%80%99t_Starve_Together_Dedicated_Servers" target="_blank" rel="noopener">Guides/Don’t Starve Together Dedicated Servers</a> 👈DST Wiki</li><li><a href="https://www.linode.com/docs/game-servers/install-dont-starve-together-game-server-on-ubuntu" target="_blank" rel="noopener">Install Don’t Starve Together Game Server on Ubuntu 14.04</a></li><li><a href="http://steamcommunity.com/sharedfiles/filedetails/?id=590565473" target="_blank" rel="noopener">How to setup dedicated server with cave on Linux</a> 👈洋文，很详尽，推荐</li><li><a href="http://blog.ttionya.com/article-1235.html" target="_blank" rel="noopener">饥荒联机独立服务器搭建教程（三）：配置篇</a> 👈不知道配置文件怎么填的看这个</li><li><a href="http://forums.kleientertainment.com/topic/64743-dedicated-server-command-line-options-guide/" target="_blank" rel="noopener">Dedicated Server Command Line Options Guide</a></li></ul><p><img src="https://img.blessing.studio/images/2017/07/16/QQ20170714161422.png" alt="小偷背包"></p><p>最后，祝诸君游戏愉快 ;)</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近和几个同学一起联机玩饥荒（Don’t Starve Together），虽然饥荒游戏本身就可以直接创建房间让别人加入，但还是有诸多不便驱使我去开一个饥荒的独立服务端（Dedicated Server），其中最主要的就是 ——「&lt;strong&gt;你退了游戏其他人就玩不了了&lt;/strong&gt;」。&lt;/p&gt;
&lt;p&gt;本来家里还有一个用旧主板和以前换下的配件攒成的二奶机，安装的是 Elementary OS，确实可以拿来跑饥荒服务器（以前还拿它跑过 MC 服务器），但是不幸的是，由于我直接把主板硬盘之类的一股脑塞在牛奶盒子里放在窗边还不加盖儿，一个雨后的下午，我推开家门后发现那个被我当做机箱的牛奶盒子已经开始积水了……前略，天国的 Pegatron IPX31-GS (・_ゝ・)&lt;/p&gt;
&lt;p&gt;直接在主力机上开一个服务器也不是不行，但是我的奔腾 G3258 选手实在是带不动饥荒游戏 + 游戏服务器 + 其他杂七杂八的东西了，所以只好另觅他方，去搞一台 VPS 来开服。而且这里不得不吐槽一下，饥荒联机版独立服务器的配置要求还是比较高的，几个人的小服，再多开几个 Mod，最起码就要 1G 的内存（我那台阿里云宕机好几次，还得去网页控制台强制重启），更不要想开洞穴了。&lt;/p&gt;
&lt;p&gt;现在我拿来开服的是免费试用一个月的京东云（Xeon-E5，2G DDR4，1Mbps 的带宽），不开洞穴，目前看起来还是没什么压力的，延迟丢包率什么的也都可以接受。网上关于开服的教程也不少了，这篇文章也不会过多赘述，差不多就是记录一下主要步骤，以及提一下可能会遇到的坑。所以，想要那种很详细的教程的同学还是绕道吧，或者翻到文章最下面的「参考链接」看看。&lt;/p&gt;
&lt;p&gt;下面的步骤在 64 位 CentOS 7.3 和 Ubuntu 16.04 上测试通过，至于 Windows……我认为实在没有啥必要特别去写，直接从 Steam 客户端就可以打开，操作方便，也没啥坑，看看网上那些教程就可以了。继续阅读之前，我希望你能有一些 Linux 的操作基础，不然会很懵。&lt;/p&gt;
&lt;h2 id=&quot;0x01-事前准备&quot;&gt;&lt;a href=&quot;#0x01-事前准备&quot; class=&quot;headerlink&quot; title=&quot;0x01 事前准备&quot;&gt;&lt;/a&gt;0x01 事前准备&lt;/h2&gt;&lt;p&gt;首先你要有一台装了 Linux 的服务器，配置要求如下（摘自 &lt;a href=&quot;http://dontstarve.wikia.com/wiki/Don%E2%80%99t_Starve_Together_Dedicated_Servers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;DST Wiki&lt;/a&gt;）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上行带宽：8KBps 一个玩家；&lt;/li&gt;
&lt;li&gt;内存：差不多一个玩家 65Mbytes；&lt;/li&gt;
&lt;li&gt;CPU：没太大要求&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要注意的是，饥荒联机版的服务器对内存的要求其实挺大的，亲测只开 Overworld 不开洞穴，空载 RAM 占用约 800MB，再加上差不多 65MB 一个玩家，开 Mod 还会占用更多，所以还是要衡量一下机器的配置。&lt;/p&gt;
    
    </summary>
    
      <category term="技术" scheme="https://blessing.studio/categories/tech/"/>
    
    
      <category term="ACGN" scheme="https://blessing.studio/tag/ACGN/"/>
    
      <category term="Linux" scheme="https://blessing.studio/tag/Linux/"/>
    
  </entry>
  
  <entry>
    <title>我 TMD 到底要怎样才能在生产环境中用上 ES6 模块化？</title>
    <link href="https://blessing.studio/how-could-i-use-es6-modules-in-production/"/>
    <id>https://blessing.studio/how-could-i-use-es6-modules-in-production/</id>
    <published>2017-07-06T08:57:23.000Z</published>
    <updated>2017-07-06T08:57:23.000Z</updated>
    
    <content type="html"><![CDATA[<p>Python3 已经发布了九年了，Python 社区却还在用 Python 2.7；而 JavaScript 社区正好相反，大家都已经开始把还没有实现的语言特性用到生产环境中了 (´_ゝ `)</p><p>虽然这种奇妙情况的形成与 JavaScript 自身早期的设计缺陷以及浏览器平台的特殊性质都有关系，但也确实能够体现出 JavaScript 社区的技术栈迭代是有多么屌快。如果你昏迷个一年半载再去看前端圈，可能社区的主流技术栈已经变得它妈都不认识了（如果你没什么实感，可以看看<a href="https://zhuanlan.zhihu.com/p/22782487" target="_blank" rel="noopener">《在 2016 年学习 JavaScript 是一种怎样的体验》</a>这篇文章，你会感受到的，你会的）。</p><h2 id="JavaScript-模块化现状"><a href="#JavaScript-模块化现状" class="headerlink" title="JavaScript 模块化现状"></a>JavaScript 模块化现状</h2><p>随着 JavaScript 越来越广泛的应用，朝着单页应用（SPA）方向发展的网页与代码量的愈发庞大，社区需要一种更好的代码组织形式，这就是模块化：将你的一大坨代码分装为多个不同的模块。</p><p>但是在 ES6 标准出台之前，由于标准的缺失（连 CSS 都有 <code>@import</code>，JavaScript 却连个毛线都没），这几年里 JavaScript 社区里冒出了各种各样的模块化解决方案<del>（群魔乱舞）</del>，懵到一种极致。主要的几种模块化方案举例如下：</p><h3 id="CommonJS"><a href="#CommonJS" class="headerlink" title="CommonJS"></a>CommonJS</h3><p>主要用于服务端，模块同步加载（也因此不适合在浏览器中运行，不过也有 <code>Browserify</code> 之类的转换工具），Node.js 的模块化实现就是基于 CommonJS 规范的，通常用法像这样：</p><a id="more"></a><pre><code class="javascript">// index.jsconst {bullshit} = require(&#39;./bullshit&#39;);console.log(bullshit());// bullshit.jsfunction someBullshit() {  return &quot;hafu hafu&quot;;}modules.export = {  bullshit: someBullshit};</code></pre><p>而且 <code>require()</code> 是动态加载模块的，完全就是模块中 <code>modules.export</code> 变量的传送门，这也就意味着更好的灵活性（按条件加载模块，参数可为表达式 etc.）。</p><h3 id="AMD"><a href="#AMD" class="headerlink" title="AMD"></a>AMD</h3><p>即异步模块定义（Asynchronous Module Definition），<del>不是那个日常翻身的农企啦</del>。</p><p>主要用于浏览器端，模块异步加载（还是用的回调函数），可以给模块注入依赖、动态加载代码块等。具体实现有 RequireJS，代码大概长这样：</p><pre><code class="javascript">// index.jsrequire([&#39;bullshit&#39;], words =&gt; {  console.log(words.bullshit());});// bullshit.jsdefine(&#39;bullshit&#39;, [&#39;dep1&#39;, &#39;dep2&#39;], (dep1, dep2) =&gt; {  function someBullshit() {    return &quot;hafu hafu&quot;;  }  return { bullshit: someBullshit };});</code></pre><p>可惜不能在 Node.js 中直接使用，而且模块定义与加载也比较冗长。</p><h3 id="ES6-Module🚀"><a href="#ES6-Module🚀" class="headerlink" title="ES6 Module🚀"></a>ES6 Module🚀</h3><p>在 ES6 模块标准出来之前，主要的模块化方案就是上述 CommonJS 和 AMD 两种了，一种用于服务器，一种用于浏览器。其他的规范还有：</p><ul><li>最古老的 IIFE（立即执行函数）；</li><li>CMD（Common Module Definition，和 AMD 挺像的，可以参考：<a href="https://github.com/seajs/seajs/issues/277" target="_blank" rel="noopener">与 RequireJS 的异同</a>）；</li><li>UMD（Universal Module Definition，兼容 AMD 和 CommonJS 的语法糖规范）；</li></ul><p>等等，这里就按下不表。</p><p>ES6 的模块化代码大概长这样：</p><pre><code class="javascript">// index.jsimport {bullshit} from &#39;./bullshit&#39;;console.log(bullshit());// bullshit.jsfunction someBullshit() {  return &quot;hafu hafu&quot;;}export {  someBullshit as bullshit};</code></pre><p>那我们为啥应该使用 ES6 的模块化规范呢？</p><ul><li>这是 ECMAScript 官方标准（嗯）；</li><li>语义化的语法，清晰明了，同时支持服务器端和浏览器；</li><li>静态 / 编译时加载（与上面俩规范的动态 / 运行时加载不同），可以做静态优化（比如下面提到的 tree-shaking），加载效率高（不过相应地灵活性也降低了，期待 <a href="https://github.com/tc39/proposal-dynamic-import" target="_blank" rel="noopener"><code>import()</code></a> 也成为规范）；</li><li>输出的是值的引用，可动态修改；</li></ul><p>嗯，你说的都对，那我tm到底要怎样才能在生产环境中用上 ES6 的模块化特性呢？</p><p>很遗憾，你永远无法控制用户的浏览器版本，可能要等上一万年，你才能直接在生产环境中写 ES6 而不用提心吊胆地担心兼容性问题。因此，你还是需要各种各样杂七杂八的工具来转换你的代码：Babel、Webpack、Browserify、Gulp、Rollup.js、System.js ……</p><p>噢，我可去你妈的吧，这些东西都tm是干嘛的？我就是想用个模块化，我到底该用啥子？</p><p><img src="https://img.blessing.studio/images/2017/07/06/QQ20170706155858.jpg" alt="我可去你妈的吧"></p><p>本文正旨在列出几种可用的在生产环境中放心使用 ES6 模块化的方法，希望能帮到诸位后来者（这方面的中文资源实在是忒少了）。</p><h2 id="问题分析"><a href="#问题分析" class="headerlink" title="问题分析"></a>问题分析</h2><p>想要开心地写 ES6 的模块化代码，首先你需要一个转译器（Transpiler）来把你的 ES6 代码转换成大部分浏览器都支持的 ES5 代码。这里我们就选用最多人用的 Babel（我不久之前才知道原来 Babel 就是巴别塔里的「巴别」……）。</p><p>用了 Babel 后，我们的 ES6 模块化代码会被转换为 ES5 + CommonJS 模块规范的代码，这倒也没什么，毕竟我们写的还是 ES6 的模块，至于编译生成的结果，管它是个什么屌东西呢（笑）</p><p>所以我们需要另外一个打包工具来将我们的模块依赖给打包成一个 bundle 文件。目前来说，依赖打包应该是最好的方法了。不然，你也可以等上一万年，等你的用户把浏览器升级到全部支持 HTTP/2（支持连接复用后模块不打包反而比较好）以及 <code>&lt;script type=&quot;module&quot; src=&quot;fuck.js&quot;&gt;</code> 定义 ( ﾟ∀。)</p><p>所以我们整个工具链应该是这样的：</p><p><img src="https://img.blessing.studio/images/2017/07/06/snipaste_20170706_104001.png" alt="处理流程"></p><p>而目前来看，主要可用的模块打包工具有这么几个：</p><ul><li>Browserify</li><li>Webpack</li><li>Rollup.js</li></ul><p>本来我还想讲一下 FIS3 的，结果去看了一下，人家竟然还没原生的支持 ES6 Modules，而且 <code>fis3-hook-commonjs</code> 插件也几万年没更新了，所以还是算了吧。至于 SystemJS 这类动态模块加载器本文也不会涉及，就像我上面说的一样，在目前这个时间点上还是先用模块打包工具比较好。</p><p>下面分别介绍这几个工具以及如何使用它们配合 Babel 实现 ES6 模块转译。</p><h2 id="Browserify"><a href="#Browserify" class="headerlink" title="Browserify"></a>Browserify</h2><p>Browserify 这个工具也是有些年头了，它通过打包所有的依赖来让你能够在浏览器中使用 CommonJS 的语法来 <code>require(&#39;modules&#39;)</code>，这样你就可以像在 Node.js 中一样在浏览器中使用 npm 包了，可以爽到。</p><p><img src="https://img.blessing.studio/images/2017/07/06/browserify.png" class="head-img" title="而且我也很喜欢 Browserify 这个 LOGO"></p><p>既然 Babel 会把我们的 ES6 Modules 语法转换成 ES5 + CommonJS 规范的模块语法，那我们就可以直接用 Browserify 来解析 Babel 的转译生成物，然后把所有的依赖给打包成一个文件，岂不是美滋滋。</p><p>不过除了 Babel 和 Browserify 这俩工具外，我们还需要一个叫做 <code>babelify</code> 的东西……好吧好吧，这是最后一个了，真的。</p><p>那么，babelify 是拿来干嘛的呢？因为 Browserify 只看得懂 CommonJS 的模块代码，所以我们得把 ES6 模块代码转换成 CommonJS 规范的，再拿给 Browserify 去看：这一步就是 Babel 要干的事情了。但是 Browserify 人家是个模块打包工具啊，它是要去分析 AST（抽象语法树），把那些 <code>reuqire()</code> 的依赖文件给找出来再帮你打包的，你总不能把所有的源文件都给 Babel 转译了再交给 Browserify 吧？那太蠢了，我的朋友。</p><p><code>babelify</code> (Browserify transform for Babel) 要做的事情，就是在所有 ES6 文件拿给 Browserify 看之前，先把它用 Babel 给转译一下（<code>browserify().transform</code>），这样 Browserify 就可以直接看得懂并打包依赖，避免了要用 Babel 先转译一万个文件的尴尬局面。</p><p>好吧，那我们要怎样把这些工具捣鼓成一个完整的工具链呢？下面就是喜闻乐见的依赖包安装环节：</p><pre><code class="shell"># 我用的 yarn，你用 npm 也差不多# gulp 也可以全局安装，方便一点# babel-preset 记得选适合自己的# 最后那俩是用来配合 gulp stream 的$ yarn add --dev babel-cli babel-preset-env babelify browserify gulp vinyl-buffer vinyl-source-stream</code></pre><p>这里我们用 Gulp 作为任务管理工具来实现自动化（什么，都 7012 年了你还不知道 Gulp？那为什么不去问问<a href="https://www.google.com/" target="_blank" rel="noopener">神奇海螺</a>呢？），<code>gulpfile.js</code> 内容如下：</p><pre><code class="javascript">var gulp       = require(&#39;gulp&#39;),    browserify = require(&#39;browserify&#39;),    babelify   = require(&#39;babelify&#39;),    source     = require(&#39;vinyl-source-stream&#39;),    buffer     = require(&#39;vinyl-buffer&#39;);gulp.task(&#39;build&#39;, function () {    return browserify([&#39;./src/index.js&#39;])        .transform(babelify)        .bundle()        .pipe(source(&#39;bundle.js&#39;))        .pipe(gulp.dest(&#39;dist&#39;))        .pipe(buffer());});</code></pre><p>相信诸位都能看得懂吧，<code>browserify()</code> 第一个参数是入口文件，可以是数组或者其他乱七八糟的，具体参数说明请自行参照 Browserify 文档。而且记得在根目录下创建 <code>.babelrc</code> 文件指定转译的 preset，或者在 <code>gulpfile.js</code> 中配置也可以，这里就不再赘述。</p><p>最后运行 <code>gulp build</code>，就可以生成能直接在浏览器中运行的打包文件了。</p><pre><code class="shell">➜  browserify $ gulp build[12:12:01] Using gulpfile E:\wwwroot\es6-module-test\browserify\gulpfile.js[12:12:01] Starting &#39;build&#39;...[12:12:01] Finished &#39;build&#39; after 720 ms</code></pre><p><img src="https://img.blessing.studio/images/2017/07/06/snipaste_20170706_111125.png" alt="Browserify Result"></p><h2 id="Rollup-js"><a href="#Rollup-js" class="headerlink" title="Rollup.js"></a>Rollup.js</h2><p>我记得这玩意最开始出来的时候号称为「下一代的模块打包工具」，并且自带了可大大减小打包体积的 <code>tree-shaking</code> 技术（DCE 无用代码移除的一种，运用了 ES6 静态分析语法树的特性，只打包那些用到了的代码），在当时很新鲜。</p><p><img src="https://img.blessing.studio/images/2017/07/06/rollupjs.jpg" alt="Rollup.js"></p><p>但是现在 Webpack2+ 已经支持了 Tree Shaking 的情况下，我们又有什么特别的理由去使用 Rollup.js 呢？不过毕竟也是一种可行的方法，这里也提一提：</p><pre><code class="shell"># 我也不知道为啥 Rollup.js 要依赖这个 external-helpers$ yarn add --dev rollup rollup-plugin-babel babel-preset-env babel-plugin-external-helpers</code></pre><p>然后修改根目录下的 <code>rollup.config.js</code>：</p><pre><code class="javascript">import babel from &#39;rollup-plugin-babel&#39;;export default {  entry: &#39;src/index.js&#39;,  format: &#39;esm&#39;,  plugins: [    babel({      exclude: &#39;node_modules/**&#39;    })  ],  dest: &#39;dist/bundle.js&#39;};</code></pre><p>还要修改 <code>.babelrc</code> 文件，把 Babel 转换 ES6 模块到 CommonJS 模块的转换给关掉，不然会导致 Rollup.js 处理不来：</p><pre><code class="json">{  &quot;presets&quot;: [    [&quot;env&quot;, {      &quot;modules&quot;: false    }]  ],  &quot;plugins&quot;: [    &quot;external-helpers&quot;  ]}</code></pre><p>然后在根目录下运行 <code>rollup -c</code> 即可打包依赖，也可以配合 Gulp 来使用，官方文档里就有，这里就不赘述了。可以看到，Tree Shaking 的效果还是很显著的，经测试，未使用的代码确实不会被打包进去，比起上面几个工具生成的结果要清爽多了：</p><p><img src="https://img.blessing.studio/images/2017/07/06/snipaste_20170706_140641.png" alt="Rollup.js Result"></p><h2 id="Webpack"><a href="#Webpack" class="headerlink" title="Webpack"></a>Webpack</h2><p>对，Webpack，就是那个丧心病狂想要把啥玩意都给模块化的模块打包工具。既然人家已经到了 <code>3.0.0</code> 版本了，所以下面的都是基于 Webpack3 的。什么？现在还有搞前端的不知道 Webpack？神奇海螺以下略。</p><p><img src="https://img.blessing.studio/images/2017/07/06/webpack.png" alt="Webpack"></p><p>喜闻乐见的依赖安装环节：</p><pre><code class="shell"># webpack 也可以全局安装，方便一些$ yarn add --dev babel-loader babel-core babel-preset-env webpack</code></pre><p>然后配置 <code>webpack.config.js</code>：</p><pre><code class="javascript">var path = require(&#39;path&#39;);module.exports = {  entry: &#39;./src/index.js&#39;,  output: {    filename: &#39;bundle.js&#39;,    path: path.resolve(__dirname, &#39;dist&#39;)  },  module: {    rules: [      {        test: /\.js$/,        exclude: /node_modules/,        use: {          loader: &#39;babel-loader&#39;,          options: {            presets: [&#39;env&#39;]          }        }      }    ]  }};</code></pre><p>差不多就是这么个配置，<code>babel-loader</code> 的其他 <code>options</code> 请参照文档，而且这个配置文件的括号嵌套也是说不出话，ZTMJLWC。</p><p>然后运行 <code>webpack</code>：</p><pre><code class="shell">➜  webpack $ webpackHash: 5c326572cf1440dbdf64Version: webpack 3.0.0Time: 1194ms    Asset     Size  Chunks             Chunk Namesbundle.js  2.86 kB       0  [emitted]  main   [0] ./src/index.js 106 bytes {0} [built]   [1] ./src/bullshit.js 178 bytes {0} [built]</code></pre><p>情况呢就是这么个情况：</p><p><img src="https://img.blessing.studio/images/2017/07/06/snipaste_20170706_114129.png" alt="Webpack Result"></p><div class="alert alert-info"><p><strong>Tips: 关于 Webpack 的 Tree Shaking</strong></p><p>Webpack 现在是自带 Tree-Shaking 的，不过需要你把 Babel 默认的转换 ES6 模块至 CommonJS 格式给关掉，就像上面 Rollup.js 那样在 <code>.babelrc</code> 中添加个 <code>&quot;modules&quot;: false</code>。原因的话上面也提到过，tree-shaking 是基于 ES6 模块的静态语法分析的，如果交给 Webpack 的是已经被 Babel 转换成 CommonJS 的代码的话那就没戏了。</p><p>而且 Webpack 自带的 tree-shaking 只是把没用到的模块从 <code>export</code> 中去掉而已，之后还要再接一个 UglifyJS 之类的工具把冗余代码干掉才能达到 Rollup.js 那样的效果。</p></div><p>Webpack 也可以配合 Gulp 工作流让开发更嗨皮，有兴趣的可自行研究。目前来看，这三种方案中，我本人更倾向于使用 Webpack，不知道诸君会选用什么呢？</p><h2 id="写在后面"><a href="#写在后面" class="headerlink" title="写在后面"></a>写在后面</h2><p>前几天我在捣鼓 <a href="https://github.com/printempw/blessing-skin-server" target="_blank" rel="noopener">printempw/blessing-skin-server</a> 那坨 shi 一样 JavaScript 代码的模块化的时候，打算试着使用一下 ES6 标准中的模块化方案，并找了 Google 大老师问 ES6 模块转译打包相关的资源，找了半天，几乎没有什么像样的中文资源。全是讲 ES6 模块是啥、有多好、为什么要用之类的，没几个是讲到底该怎么在生产环境中使用的（也有可能是我搜索姿势不对），说不出话。遂撰此文，希望能帮到后来人。</p><p>且本人水平有限，如果文中有什么错误，欢迎在下方评论区批评指出。</p><h3 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h3><ul><li><a href="Getting import/export working ES6 style using Browserify + Babelify + Gulp = -5hrs of life">Getting import/export working ES6 style using Browserify + Babelify + Gulp = -5hrs of life</a></li><li><a href="https://rollupjs.org/" target="_blank" rel="noopener">rollup.js • guide</a></li><li><a href="http://brooch.me/2017/06/30/webpack-tree-shaking/" target="_blank" rel="noopener">使用 webpack 2 tree-shaking 机制时需要注意的细节</a></li><li><a href="http://xwjgo.github.io/2016/09/23/webpack+babel%E5%AE%9E%E7%8E%B0%E5%9C%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E7%AB%AF%E4%BD%BF%E7%94%A8es6%E6%A8%A1%E5%9D%97%E8%AF%AD%E6%B3%95/" target="_blank" rel="noopener">webpack+babel 加载 es6 模块</a></li><li><a href="https://webpack.js.org/configuration/" target="_blank" rel="noopener">Documentation - webpack</a></li><li><a href="https://www.zhihu.com/question/41922432" target="_blank" rel="noopener">如何评价 Webpack 2 新引入的 Tree-shaking 代码优化技术？</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Python3 已经发布了九年了，Python 社区却还在用 Python 2.7；而 JavaScript 社区正好相反，大家都已经开始把还没有实现的语言特性用到生产环境中了 (´_ゝ `)&lt;/p&gt;
&lt;p&gt;虽然这种奇妙情况的形成与 JavaScript 自身早期的设计缺陷以及浏览器平台的特殊性质都有关系，但也确实能够体现出 JavaScript 社区的技术栈迭代是有多么屌快。如果你昏迷个一年半载再去看前端圈，可能社区的主流技术栈已经变得它妈都不认识了（如果你没什么实感，可以看看&lt;a href=&quot;https://zhuanlan.zhihu.com/p/22782487&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;《在 2016 年学习 JavaScript 是一种怎样的体验》&lt;/a&gt;这篇文章，你会感受到的，你会的）。&lt;/p&gt;
&lt;h2 id=&quot;JavaScript-模块化现状&quot;&gt;&lt;a href=&quot;#JavaScript-模块化现状&quot; class=&quot;headerlink&quot; title=&quot;JavaScript 模块化现状&quot;&gt;&lt;/a&gt;JavaScript 模块化现状&lt;/h2&gt;&lt;p&gt;随着 JavaScript 越来越广泛的应用，朝着单页应用（SPA）方向发展的网页与代码量的愈发庞大，社区需要一种更好的代码组织形式，这就是模块化：将你的一大坨代码分装为多个不同的模块。&lt;/p&gt;
&lt;p&gt;但是在 ES6 标准出台之前，由于标准的缺失（连 CSS 都有 &lt;code&gt;@import&lt;/code&gt;，JavaScript 却连个毛线都没），这几年里 JavaScript 社区里冒出了各种各样的模块化解决方案&lt;del&gt;（群魔乱舞）&lt;/del&gt;，懵到一种极致。主要的几种模块化方案举例如下：&lt;/p&gt;
&lt;h3 id=&quot;CommonJS&quot;&gt;&lt;a href=&quot;#CommonJS&quot; class=&quot;headerlink&quot; title=&quot;CommonJS&quot;&gt;&lt;/a&gt;CommonJS&lt;/h3&gt;&lt;p&gt;主要用于服务端，模块同步加载（也因此不适合在浏览器中运行，不过也有 &lt;code&gt;Browserify&lt;/code&gt; 之类的转换工具），Node.js 的模块化实现就是基于 CommonJS 规范的，通常用法像这样：&lt;/p&gt;
    
    </summary>
    
      <category term="技术" scheme="https://blessing.studio/categories/tech/"/>
    
    
      <category term="JavaScript" scheme="https://blessing.studio/tag/JavaScript/"/>
    
      <category term="前端" scheme="https://blessing.studio/tag/%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>为 Hexo 博客添加页面访问计数器</title>
    <link href="https://blessing.studio/add-page-view-counter-for-hexo/"/>
    <id>https://blessing.studio/add-page-view-counter-for-hexo/</id>
    <published>2017-06-25T05:12:53.000Z</published>
    <updated>2017-06-25T05:12:53.000Z</updated>
    
    <content type="html"><![CDATA[<p>一般来说，写博客的都喜欢在页面上加上一个访问计数器，来<del>满足虚荣心</del>显示某篇文章或者整个站点的访问量。这种需求在 WordPress 等动态博客上都是比较容易满足的，安装个插件即可（辣鸡 Ghost 除外），但是对于小部分静态博客来说就比较头疼了。</p><p>目前来看，互联网上的静态博客访问计数器解决方案大致有这么几种：</p><ul><li>使用「<a href="http://busuanzi.ibruce.info/" target="_blank" rel="noopener">不蒜子</a>」访问计数服务；</li><li>利用 LeanCloud 平台搭建统计服务。</li></ul><p>其中「不蒜子」是个自称「永久免费使用」的极简网页计数器，仅需两行代码即可为静态博客添加访问计数功能，这种简单的解决方案也受到很多静态博客作者的喜爱。但正如我之前在这篇文章（<a href="https://blessing.studio/add-page-view-counter-for-ghost-blog/">为 Ghost 博客添加页面访问计数器</a>）中所述，不蒜子虽然提供了 <code>site_pv</code>、<code>site_uv</code>、<code>page_pv</code> 等多种统计，但是其并不提供这些服务的开放 API。而我的需求是在「首页」或者其他文章列表页中的每篇文章都要显示各自的访问量，并且需要一个「最受欢迎的文章」功能（按访问量倒序排序）。很可惜不蒜子无法满足我的需求，只好将其 PASS。</p><p>至于使用 LeanCloud 的方法（详情参见<a href="http://crescentmoon.info/2014/12/11/popular-widget/" target="_blank" rel="noopener">这篇博文</a>），其实是利用了这个平台所提供的「数据存储」后端功能，大部分逻辑都在前端完成，而 LeanCloud 只负责存储数据。但是，这个现成的访问计数程序也不支持输出「最受欢迎的文章」功能，只能自己实现。而且，既然我已经有了 VPS，那我为啥还要去弄个 LeanCloud 呢？</p><p>综上，我决定自己写一个网页访问量计数服务。</p><a id="more"></a><h2 id="0x01-技术选型"><a href="#0x01-技术选型" class="headerlink" title="0x01 技术选型"></a>0x01 技术选型</h2><p>前端 <code>increase</code>（访问量自增）、<code>get</code>（获取访问量）等操作用 JavaScript 来写，这没得选，关键是后端。本来打算后端也用 Node.js 来写的，但是想想我 VPS 上还跑着其他 PHP 项目，使用 PHP 的话运维比较方便（不需要再去另外运行 <code>forever</code> 之类的守护进程），而且我对 PHP 也很熟悉，开发效率比 Node、Python 等语言要高。</p><p>之前（2016 年一月份）我写的那个适用于 Ghost 博客的访问计数器（<a href="https://github.com/printempw/ghost-hit-counter" target="_blank" rel="noopener">printempw/ghost-hit-counter</a>）也是用 PHP 写的，不过当时并没有使用什么框架，直接原生 PHP 肛上去。这次的项目我打算使用 Lumen 这个「为速度而生的 Laravel 框架」，一来之前在开发 Minecraft 皮肤站（<a href="https://github.com/printempw/blessing-skin-server" target="_blank" rel="noopener">printempw/blessing-skin-server</a>）时已经摸清了 Laravel 那一套，二来 Lumen 在性能强大的同时还能保持 Laravel 高效率开发特性，可以省去不少无用功，专注于业务逻辑本身的开发。</p><h2 id="0x02-开发"><a href="#0x02-开发" class="headerlink" title="0x02 开发"></a>0x02 开发</h2><p>事实证明我选择 Lumen 是正确的。</p><p>整个项目包括前端和后端，开发以及测试总共花了我大概三个小时的时间，Lumen 便捷的基础组件让我少走了不少冤枉路：</p><ul><li><code>DB</code> Facade 的一套代码可以同时支持 MySQL 和 SQLite，只需在 <code>.env</code> 中配置即可切换数据库源，简直不要再方便；</li><li>好用且优雅的查询构造器（Query Builder），告别手写 SQL，也无需臃肿的 ORM；</li><li>优雅且轻量级的路由系统，不用再像原来那样使用丑陋的 Query String 或者手动处理路由；</li><li><code>Migration</code> 数据库迁移功能，将数据表结构信息纳入版本控制系统，部署仅需一句 <code>php artisan migrate</code>，不小心搞砸数据库也只需要一句 <code>php artisan migrate:refresh</code> 就能完好如新；</li><li>便捷的 <code>Cache</code> 缓存系统，开箱即用的文件缓存，修改 <code>.env</code> 配置即可将后端无缝切换至 Redis；</li><li>可直接使用 Laravel 的 <code>ThrottleRequests</code> 节流阀中间件，哪里限流套哪里；</li><li>自带超好用的时间处理类库 <code>Carbon</code>；</li><li>开箱即用的异常处理系统以及完整的日志记录，etc.</li></ul><p>成品开源在 GitHub 上（<a href="https://github.com/printempw/hexo-view-counter" target="_blank" rel="noopener">printempw/hexo-view-counter</a>），采用 GPLv3 协议，欢迎 star 或修改自用，Live Demo 就是这个博客。另外这也是我第一次使用 Atom 编辑器从头开始开发一个项目，一路用下来使用体验还是蛮不错的（之前都用的 Sublime Text3）。</p><p><img src="https://img.blessing.studio/images/2017/06/25/snipaste_20170624_222645.png" alt="Atom"></p><h2 id="0x03-使用方法"><a href="#0x03-使用方法" class="headerlink" title="0x03 使用方法"></a>0x03 使用方法</h2><p>安装部署方法我不多说，自己去项目 README 看，反正就是 Laravel 那套。而且我顺便魔改了下 Lumen 的框架结构，删掉了一大票目前用不着的东西，同时把入口文件从 <code>/public</code> 子目录移到项目根目录下了，所以 Nginx 用户只需要配置好 URL 重写，Apache 用户啥都不用干就行。</p><p>这个项目只提供基本的 API（具体看 README），其他逻辑需要在博客前端完成，下面贴一下我自己在用的脚本（某些语句依赖于 jQuery，可自行修改）：</p><script src="//work.prinzeugen.net/gist/2e0e0c127a0f5081434b4dbe136327c1.js"></script><p>需要注意的是，「最受欢迎的文章」页面需要后端提供文章标题，但是我思来想去，都想不到什么好的解决方法。原先搞 Ghost 的访问计数器时可以直接访问 Ghost 的数据库来获取文章信息，但是静态博客就不行了。虽然想过各种自动化的方法（譬如自动去爬标题之类的），但想想还是作罢。目前情况是，文章相关的 record 会随着访问而自动生成（<code>slug</code>、<code>pv</code>、<code>created_at</code> 等字段），但是 <code>title</code> 字段并不会自动填写（NULL）。所以如果你需要热门文章功能的话，可以定期访问计数器的数据库，手动填写文章的标题信息。</p><p>而且这样虽然不优雅，但也能有效防止他人恶意请求造成垃圾数据过多（静态博客没有好办法去验证请求的 <code>slug</code> 是否存在），想要清除无效数据只需运行 <code>DELETE FROM post_views WHERE title = &quot;&quot;</code> 即可。</p><h2 id="0x04-总结"><a href="#0x04-总结" class="headerlink" title="0x04 总结"></a>0x04 总结</h2><p>对比一下使用现成服务和自己搭建的优缺点，顺便测试下 Hexo 的 GFM 表格支持：</p><table><thead><tr><th>功能支持</th><th>不蒜子</th><th>LeanCloud</th><th>自建</th></tr></thead><tbody><tr><td>数据导入与管理<del>作弊</del></td><td>不可</td><td>可，比较麻烦</td><td>完全可以自己瞎搞</td></tr><tr><td>开放 API</td><td>无</td><td>业务逻辑都在前端</td><td>有</td></tr><tr><td>部署难度</td><td>超方便</td><td>还行</td><td>最麻烦</td></tr><tr><td>可扩展性（热门文章 etc.）</td><td>无</td><td>需要自己编写</td><td>需要自己编写</td></tr><tr><td>部署费用</td><td>免费</td><td>免费</td><td>需要支持 PHP 的主机</td></tr><tr><td>靠谱程度</td><td>也许靠谱</td><td>挺靠谱的</td><td>看你主机靠不靠谱</td></tr><tr><td>装逼程度</td><td>还行</td><td>还行</td><td>逼格高</td></tr></tbody></table><p>_(:3」∠)_ 以上。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;一般来说，写博客的都喜欢在页面上加上一个访问计数器，来&lt;del&gt;满足虚荣心&lt;/del&gt;显示某篇文章或者整个站点的访问量。这种需求在 WordPress 等动态博客上都是比较容易满足的，安装个插件即可（辣鸡 Ghost 除外），但是对于小部分静态博客来说就比较头疼了。&lt;/p&gt;
&lt;p&gt;目前来看，互联网上的静态博客访问计数器解决方案大致有这么几种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用「&lt;a href=&quot;http://busuanzi.ibruce.info/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;不蒜子&lt;/a&gt;」访问计数服务；&lt;/li&gt;
&lt;li&gt;利用 LeanCloud 平台搭建统计服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中「不蒜子」是个自称「永久免费使用」的极简网页计数器，仅需两行代码即可为静态博客添加访问计数功能，这种简单的解决方案也受到很多静态博客作者的喜爱。但正如我之前在这篇文章（&lt;a href=&quot;https://blessing.studio/add-page-view-counter-for-ghost-blog/&quot;&gt;为 Ghost 博客添加页面访问计数器&lt;/a&gt;）中所述，不蒜子虽然提供了 &lt;code&gt;site_pv&lt;/code&gt;、&lt;code&gt;site_uv&lt;/code&gt;、&lt;code&gt;page_pv&lt;/code&gt; 等多种统计，但是其并不提供这些服务的开放 API。而我的需求是在「首页」或者其他文章列表页中的每篇文章都要显示各自的访问量，并且需要一个「最受欢迎的文章」功能（按访问量倒序排序）。很可惜不蒜子无法满足我的需求，只好将其 PASS。&lt;/p&gt;
&lt;p&gt;至于使用 LeanCloud 的方法（详情参见&lt;a href=&quot;http://crescentmoon.info/2014/12/11/popular-widget/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;这篇博文&lt;/a&gt;），其实是利用了这个平台所提供的「数据存储」后端功能，大部分逻辑都在前端完成，而 LeanCloud 只负责存储数据。但是，这个现成的访问计数程序也不支持输出「最受欢迎的文章」功能，只能自己实现。而且，既然我已经有了 VPS，那我为啥还要去弄个 LeanCloud 呢？&lt;/p&gt;
&lt;p&gt;综上，我决定自己写一个网页访问量计数服务。&lt;/p&gt;
    
    </summary>
    
      <category term="技术" scheme="https://blessing.studio/categories/tech/"/>
    
    
      <category term="博客" scheme="https://blessing.studio/tag/%E5%8D%9A%E5%AE%A2/"/>
    
      <category term="PHP" scheme="https://blessing.studio/tag/PHP/"/>
    
      <category term="Hexo" scheme="https://blessing.studio/tag/Hexo/"/>
    
  </entry>
  
  <entry>
    <title>Twitter 账号被锁定是种怎样的体验</title>
    <link href="https://blessing.studio/twitter-account-has-been-locked/"/>
    <id>https://blessing.studio/twitter-account-has-been-locked/</id>
    <published>2017-06-19T03:09:22.000Z</published>
    <updated>2017-06-19T03:09:22.000Z</updated>
    
    <content type="html"><![CDATA[<p>两天前（2017-06-17），我的 Twitter 账号莫名其妙被锁定了，原因是有「自动行为」。</p><p><img src="https://img.blessing.studio/images/2017/06/19/QQ20170617201254.png" alt="locked"></p><p>WTF？ 你特么在逗我？<img src="https://img.blessing.studio/images/2017/06/19/5628dd6ecd9fa100f371_size30_w521_h534.th.jpg" alt="黑人问号"></p><p>我思前想后，估计那一天关于 Twitter 我干的最值得怀疑的就是「多设备异地登录」了。具体流程如下（都是发生在 6.17 那一天，其实看我那一天被封前的推文也行）：</p><a id="more"></a><ul><li>被 Wabi 主题惊艳到，想要久违地尝试一下 ADUI；</li><li>备份好数据后，把手机（MI3）刷成了 MIUI（原来是 Lineage13）；</li><li>用了一段时间，很不满意<ul><li>卡顿超严重，已经到了无法正常使用的程度了，官方适配成这鸟样？还没有第三方适配的 CyanogenMod 流畅也是说不出话；</li><li>不知道出了什么问题，每次按 Home 键都要「正在加载桌面」，很烦；</li><li>MI3 上的 MIUI8 竟然是基于 Android 4.4 的……今年是几几年？</li><li>说实话，MIUI6 之后的那个上拉畸变（原生是上拉水波纹）真tm ruozhi，超不喜欢；</li><li>显示设置中的色温只能三级（暖色 - 标准 - 冷色）调整；</li><li>但是对于 Wabi Pro Ara 这个主题还是很满意的，很漂亮；</li></ul></li><li>在刷了 MIUI 的机器上登录 Twitter 账号，但是用的另外一个代理服务器；</li><li>发了一篇推文<sup><a href="https://twitter.com/printempw/status/876037651089903616" target="_blank" rel="noopener">[source]</a></sup>；</li><li>刷了 Lineage14（基于 Android 7.1.2），恢复了之前备份的应用数据，登录 Twitter 发推……然后，就没有然后了。这是我那天发表的最后一条推文：<a href="https://twitter.com/printempw/status/876049475407560705" target="_blank" rel="noopener">@printempw/status</a>；</li></ul><p>看来也就只有发的那几条推文可能有问题了，但这也够不上 Automatic Behaviors 吧？真不晓得 Twitter 怎么搞的。</p><p>虽然我不喜欢 MIUI，但是 <a href="http://zhuti.xiaomi.com/detail/810fd4e9-72cd-4cce-bbff-846dd5a709e9" target="_blank" rel="noopener">Wabi Pro Ara</a> 这个主题我还是要盛赞的，喜欢原生 Android 风格的朋友值得购买：</p><p><img src="https://ooo.0o0.ooo/2017/06/19/5947313c2b742.png" alt="Wabi Theme Screenshot"></p><p>那我该咋解锁我的 Twitter 账号呢？</p><p><img src="https://img.blessing.studio/images/2017/06/19/locked_verification_required.png" alt="Verification Required"></p><p>Twitter 官方说会给我绑定的手机号打电话，叫我把电话中的验证码输入进去，但是……</p><p><strong>我TM根本没有收到半个电话啊！</strong></p><p>明明能绑定中国大陆的手机号，却打不过来电话，我要你何用！骂归骂，总是要找个解决办法的，毕竟我还是蛮依赖 Twitter 来满足自己的分享欲的。既然官方解锁渠道走不通，那就该提出申诉了（<em>Appeal an account suspension or locked account</em>）。</p><h3 id="一、填写申诉表单"><a href="#一、填写申诉表单" class="headerlink" title="一、填写申诉表单"></a>一、填写申诉表单</h3><p>访问「<a href="https://support.twitter.com/forms/general?subtopic=suspended" target="_blank" rel="noopener">帮助中心 &gt; 提交请求  &gt; ACCOUNT ACCESS</a>」这个页面填写一个 ticket，描述你所遇到的问题和你为啥要申诉（e.g. 没有做出任何违反 Twitter Rules 的行为，中国大陆手机号收不到 Twitter 打来的电话），一般来说用英文比较好：</p><p><img src="https://img.blessing.studio/images/2017/06/19/snipaste_20170619_102152.png" alt="申诉页面"></p><p><em>▲ Twitter 这个页面的  Localization 可真 Good 啊</em></p><p>如果你不懂洋文，这里我提供个复制用范文（瞎写的，诸君不要吐槽）：</p><blockquote><p>Hi, </p><p>My account is currently suspended by Twitter, but I have no idea why my account was locked. To unlock my account, it seems that I need to enter the confirmation code which I should receive in a phone call from Twitter. However, I am in China Mainland, and the “Call Me” button dosen’t work at all. I’ve tried for many times but still no phone calls came.</p><p>I am sure that I did NOT have any violation of the Twitter Rules. This account is very important to me and I would like to have my account reactivated. Could you please unlock my account or provide another method to unlock it?</p><p>Thank you!</p></blockquote><h3 id="二、邮件回复-Twitter-的自动回复"><a href="#二、邮件回复-Twitter-的自动回复" class="headerlink" title="二、邮件回复 Twitter 的自动回复"></a>二、邮件回复 Twitter 的自动回复</h3><p>提交之后，Twitter 会自动给你发送一封以 <code>Case# {编号}: Appealing a locked account - @{Twitter 用户名} [ref:{神秘编号}:ref]</code> 为 Subject 的邮件，内容全都是教你怎么操作的废话可以不用看。有用的就一句：<em>If you’re still experiencing an issue after confirming your identity, please reply to this message and provide us with specific details of the problem you’re experiencing.</em></p><p>然后你就直接把上面那一段复制过去，再在这封邮件底下回复一下就好了 ‾\_(ツ)_/‾</p><p><img src="https://img.blessing.studio/images/2017/06/19/snipaste_20170619_104702.png" alt="邮件记录"></p><p>你急的话，也可以再继续回复催催看，不过记得言辞得体。也可以用其他语言再去开一个 ticket，我是用中文再发了一遍，本来打算今天要是还不解封的话就拿日文再发一封的（笑）而且不晓得申诉单填写页面的那个 <em>“Where are you experiencing this issue?”</em> 的平台选择对处理优先级有没有啥实际影响。</p><h3 id="三、收到解封通知"><a href="#三、收到解封通知" class="headerlink" title="三、收到解封通知"></a>三、收到解封通知</h3><p>一般来说，填写申诉单，回复邮件的两三天后就能收到 Twitter 的解封通知了：</p><p><img src="https://img.blessing.studio/images/2017/06/19/snipaste_20170619_105254.png" alt="Unlock"></p><p>我是在两天后（2017-06-19）<a href="https://twitter.com/printempw/status/876603531930722305" target="_blank" rel="noopener">早上起来时</a>收到邮件的，申诉处理速度和网上其他人<strong>两年前</strong>描述的也差不多（ummmm……）。所以被封申诉后一天两天没有回复还算是正常的，不用太慌。</p><hr><p>总之中国大陆申诉 Twitter 账号解锁差不多就是这么个流程，看网上关于这个的文章屈指可数（大部分都是好几年前的），所以稍微写篇文章记录一下，希望能帮到后来人。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;两天前（2017-06-17），我的 Twitter 账号莫名其妙被锁定了，原因是有「自动行为」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.blessing.studio/images/2017/06/19/QQ20170617201254.png&quot; alt=&quot;locked&quot;&gt;&lt;/p&gt;
&lt;p&gt;WTF？ 你特么在逗我？&lt;img src=&quot;https://img.blessing.studio/images/2017/06/19/5628dd6ecd9fa100f371_size30_w521_h534.th.jpg&quot; alt=&quot;黑人问号&quot;&gt;&lt;/p&gt;
&lt;p&gt;我思前想后，估计那一天关于 Twitter 我干的最值得怀疑的就是「多设备异地登录」了。具体流程如下（都是发生在 6.17 那一天，其实看我那一天被封前的推文也行）：&lt;/p&gt;
    
    </summary>
    
      <category term="日常" scheme="https://blessing.studio/categories/diary/"/>
    
    
      <category term="记录" scheme="https://blessing.studio/tag/%E8%AE%B0%E5%BD%95/"/>
    
      <category term="Twitter" scheme="https://blessing.studio/tag/Twitter/"/>
    
  </entry>
  
  <entry>
    <title>博客已迁移至 Hexo</title>
    <link href="https://blessing.studio/migrated-to-hexo/"/>
    <id>https://blessing.studio/migrated-to-hexo/</id>
    <published>2017-06-18T15:48:40.000Z</published>
    <updated>2017-06-18T16:14:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>是的，我又双叒叕换博客程序了。</p><p>话是这样说，其实也没有很频繁啦，上一次从 <a href="https://blessing.studio/hello-ghost-goodbye-wordpress/">WordPress 迁移至 Ghost</a> 已经是一年多前的事了。这次是从 Ghost 迁移至 <a href="https://hexo.io/" target="_blank" rel="noopener">Hexo</a>，一个静态博客生成器。总体来看，我对博客程序的选择是越来越轻<del>（zhuang）</del>量<del>（bi）</del>化了。</p><p>目前，本博客已经完全迁移至 Hexo，包括所有的文章和主题。不过话说回来现在回头去看两年多前写的文章，真的挺尬的，行文风格完全不一样，超尬 (つд⊂) 目前我在用的这个主题（<a href="https://qaq.cat/kotori/100" target="_blank" rel="noopener">Seventeen</a>）已经陪了我快三年了，之前我把它从 WordPress 移植到 Ghost，现在到了 Hexo 我又把它给移植过来了，我也是爱得深沉啊（笑）</p><p>既然现在迁移完成了，我打算列举一下我迁移的理由，算是个记录。</p><h3 id="一、动态博客的优点"><a href="#一、动态博客的优点" class="headerlink" title="一、动态博客的优点"></a>一、动态博客的优点</h3><p>动态博客肯定是有一些静态博客所无法实现的优点的，不然我之前也不会一直使用动态博客，而没有考虑过使用静态博客了。</p><ul><li>动态博客功能强大，插件众多，甚至能当 CMS 用；</li><li>数据存储基于数据库，灵活性强；</li><li>有管理后台，发布、更新文章等操作方便；</li><li>自带的附件、站内搜索、评论系统等功能。</li></ul><p>那我为毛要选择迁移至静态博客呢？</p><a id="more"></a><h3 id="二、Ghost-太鶸了，真的鶸"><a href="#二、Ghost-太鶸了，真的鶸" class="headerlink" title="二、Ghost 太鶸了，真的鶸"></a>二、Ghost 太鶸了，真的鶸</h3><p>在迁移之前，我使用的是 Ghost 这个基于 Node.js 构建的动态博客程序。</p><p>我最开始选择它就是因为受够了臃肿的 WordPress 而看中了 Ghost 的简洁。但是这一年使用下来，我对 Ghost 的热情也渐渐冷却，对它感到不爽的地方也多了起来。说到底，作为一个普通的用户来看，我认为 Ghost 并没有实现多少<strong>「只有动态博客才能做到」</strong>的功能。</p><ul><li>孱弱的主题系统。是的，如果你开发过 Ghost 主题，你就会感到 Ghost 的主题功能是有多么的鶸（以下情况截止至 Ghost 的 0.11.9 版，即 2017-05-07，也许 Ghost 之后的版本会有所改善）。<ul><li>在 Handlebars 模板中，你只能使用 Ghost 提供的有限变量，你甚至无法在 <a href="https://themes.ghost.org/v0.11.9/docs/if" target="_blank" rel="noopener">if statement</a> 中使用稍微复杂一些的表达式（当然，你也可以称赞这是遵循逻辑与视图分离）；</li><li>用户自定义？不存在的，用户在不修改主题文件的前提下能修改的就只有博客标题、LOGO 等基本信息以及导航栏。而且导航栏标题中的 HTML 标签永远都会被转义，不修改 Ghost 源码关不掉，想放个图标都不行；</li><li>小工具、换主题颜色、换 Schema？直接改主题或者 Ghost 的源码去吧，否则你没有任何方法为你的 Ghost 主题添加一个自定义配置文件，因为你根本读取不到。</li></ul></li><li>没有原生评论系统，你必须和其他静态博客一样去使用如 Disqus、多说（已经倒闭了）之类的第三方评论系统；</li><li>没有搜索功能。为什么你一个动态博客和静态博客一样不支持站内搜索？极简风？</li><li>没有附件上传功能。好吧我也用不到这个，交给图床去做；</li><li>不支持用户自行截取文章摘要，不<a href="https://blessing.studio/add-more-tag-for-ghost/">修改源码</a>无法实现类似 WordPress 的 <code>&lt;!--more--&gt;</code> 功能；</li><li>虽是打着简洁轻便优美的名头，但是在我看来 Ghost 整体已经日渐臃肿，尤其是 1.0 Alpha 版，看源码看得我一个头两个大。当然，你也可以说是我太low，不懂它的架构优雅😀</li><li>Markdown 支持不好，渲染经常出问题，而且连个表格都没得插；</li></ul><p>虽然 Ghost 有这么多不完善的地方，但毕竟人家还处在 0.x 的版本号阶段嘛，也不是不能理解，因此我也愿意继续使用它，等待它慢慢成长完善。但是后来我在试用 Ghost 1.0 Alpha 的早期版本时（当时 Alpha 页面上介绍的是 <em>Brand New Editor</em>，我挺心动的），我惊讶地发现 Ghost 团队在 Alpha 甚至做出了<strong>【取消 Markdown 编辑器改为普通富文本编辑器】</strong>的神奇决定……虽然半个月前的 <a href="https://github.com/TryGhost/Ghost/releases/tag/1.0.0-alpha.21" target="_blank" rel="noopener">Alpha.21</a> 已经重新回归了 markdown-only 编辑器，而且换了个新的 Markdown renderer（官方自己也承认原来的渲染器有着 <em>a whole heap of syntax bugs</em>）。但是很可惜，当我得知这条消息时，我已经完成了博客的迁移工作 ‾\_(ツ)_/‾</p><h3 id="三、市面上其他的动态博客"><a href="#三、市面上其他的动态博客" class="headerlink" title="三、市面上其他的动态博客"></a>三、市面上其他的动态博客</h3><p>我也有想过换成其他的动态博客，但苦于找不到合适的。</p><ul><li><strong>WordPress</strong>：臃肿，运行速度慢，架构落后；</li><li><strong>Typecho</strong>：速度什么的都很不错，但感觉架构不够现代化；</li><li><strong>Canvas</strong>：够现代化了（基于 Laravel），但是运行速度屌慢，功能也不完善。</li></ul><h3 id="四、静态博客的优点"><a href="#四、静态博客的优点" class="headerlink" title="四、静态博客的优点"></a>四、静态博客的优点</h3><p>相比起动态博客，静态博客肯定是有一些它独有的优点的，不然我也不可能会考虑耗费时间精力去做迁移工作。</p><ul><li>文章以一个个纯文本文件保存，便于管理备份（Git 或者直接一股脑 zip）；</li><li>可以直接用我喜欢的编辑器编辑博文（推荐 Typora，超好用）；</li><li>生成的结果就是些静态页面，放哪里都行，托管、迁移也方便，不用耗费太多精力去维护；</li><li>性能铁定比动态博客强（我就是因为性能问题而放弃了 WordPress）；</li><li>安全性高，不用担心博客程序曝出各种 SQL 注入之类的漏洞（参见 WP）；</li><li>我喜欢纯文本、Markdown，和简单优美的东西 :P</li><li>Jekyll、Octopress、Hexo 等静态博客生成器的生态圈也很发达；</li><li><del>静态博客 ZHUANG BI 啊</del></li></ul><h3 id="五、静态博客的缺点"><a href="#五、静态博客的缺点" class="headerlink" title="五、静态博客的缺点"></a>五、静态博客的缺点</h3><p>当然，静态博客也是有缺点的，不然大家都去用静态博客了。</p><ul><li>没有自带的评论系统（我用 Disqus）；</li><li>没有自带的搜索功能（我直接用 Google 的 <code>site:</code> 搜索限定符）；</li><li>没有后台，不能随时随地发表文章（毕业了，坐在电脑前的时间也多了）；</li><li>操作复杂，难以上手，一般人玩不转（对于 Geek 来说当然小菜一碟）</li></ul><h3 id="六、为啥不自己写个博客程序呢？"><a href="#六、为啥不自己写个博客程序呢？" class="headerlink" title="六、为啥不自己写个博客程序呢？"></a>六、为啥不自己写个博客程序呢？</h3><p>其实我也不是没有想过啦。</p><ul><li>不想造轮子，现有的博客程序都挺好的；</li><li>我自认为不需要通过写一个博客程序的方式来巩固我的知识以及展示「我会○○技能哦」，维护也麻烦。写个博客程序不难，但我希望写一些更有意义的玩意儿；</li><li><strong>懒</strong>。</li></ul><h3 id="七、我依然坚持独立博客的理由"><a href="#七、我依然坚持独立博客的理由" class="headerlink" title="七、我依然坚持独立博客的理由"></a>七、我依然坚持独立博客的理由</h3><p>引用阮一峰老师所言<sup><a href="http://www.ruanyifeng.com/blog/2012/08/blogging_with_jekyll.html" target="_blank" rel="noopener">[source]</a></sup>：</p><blockquote><p>喜欢写 Blog 的人，会经历三个阶段。</p><p>第一阶段，刚接触 Blog，觉得很新鲜，试着选择一个免费空间来写。</p><p>第二阶段，发现免费空间限制太多，就自己购买域名和空间，搭建独立博客。</p><p>第三阶段，觉得独立博客的管理太麻烦，最好在保留控制权的前提下，让别人来管，自己只负责写文章。</p></blockquote><p>三年过去了，目前我依然处于第二阶段。那我是为什么没有放弃独立博客呢？</p><ul><li>免费的静态站点托管服务（GitHub Pages、Coding Pages）总有不少限制和不便之处，而且免费的才是最贵的；</li><li>至于收费托管服务……那为啥不自己买个 VPS 呢？还能跑其他程序；</li><li>Medium、简书之类的平台虽然可以专注于写作，但是不好进行自定义与功能的添加，个性化功能过于局限，样式模板千篇一律，<del>不好装逼</del>；</li><li>并没有觉得维护独立博客很麻烦，专注产出内容之余折腾下搭建、模板，也挺有意思的 ;)</li></ul><p>最后引用一句 <a href="https://www.farbox.com/" target="_blank" rel="noopener">Farbox</a> 首页上我很喜欢的一段话：</p><blockquote><p>博客的意义</p><p>除了记录生活，博客让作者变得平静、幸福、以及更优秀，结交到性情相近的朋友，甚至让薪资涨幅超过不写博客的一些人。</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;是的，我又双叒叕换博客程序了。&lt;/p&gt;
&lt;p&gt;话是这样说，其实也没有很频繁啦，上一次从 &lt;a href=&quot;https://blessing.studio/hello-ghost-goodbye-wordpress/&quot;&gt;WordPress 迁移至 Ghost&lt;/a&gt; 已经是一年多前的事了。这次是从 Ghost 迁移至 &lt;a href=&quot;https://hexo.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Hexo&lt;/a&gt;，一个静态博客生成器。总体来看，我对博客程序的选择是越来越轻&lt;del&gt;（zhuang）&lt;/del&gt;量&lt;del&gt;（bi）&lt;/del&gt;化了。&lt;/p&gt;
&lt;p&gt;目前，本博客已经完全迁移至 Hexo，包括所有的文章和主题。不过话说回来现在回头去看两年多前写的文章，真的挺尬的，行文风格完全不一样，超尬 (つд⊂) 目前我在用的这个主题（&lt;a href=&quot;https://qaq.cat/kotori/100&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Seventeen&lt;/a&gt;）已经陪了我快三年了，之前我把它从 WordPress 移植到 Ghost，现在到了 Hexo 我又把它给移植过来了，我也是爱得深沉啊（笑）&lt;/p&gt;
&lt;p&gt;既然现在迁移完成了，我打算列举一下我迁移的理由，算是个记录。&lt;/p&gt;
&lt;h3 id=&quot;一、动态博客的优点&quot;&gt;&lt;a href=&quot;#一、动态博客的优点&quot; class=&quot;headerlink&quot; title=&quot;一、动态博客的优点&quot;&gt;&lt;/a&gt;一、动态博客的优点&lt;/h3&gt;&lt;p&gt;动态博客肯定是有一些静态博客所无法实现的优点的，不然我之前也不会一直使用动态博客，而没有考虑过使用静态博客了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;动态博客功能强大，插件众多，甚至能当 CMS 用；&lt;/li&gt;
&lt;li&gt;数据存储基于数据库，灵活性强；&lt;/li&gt;
&lt;li&gt;有管理后台，发布、更新文章等操作方便；&lt;/li&gt;
&lt;li&gt;自带的附件、站内搜索、评论系统等功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那我为毛要选择迁移至静态博客呢？&lt;/p&gt;
    
    </summary>
    
      <category term="日常" scheme="https://blessing.studio/categories/diary/"/>
    
    
      <category term="博客" scheme="https://blessing.studio/tag/%E5%8D%9A%E5%AE%A2/"/>
    
      <category term="Ghost" scheme="https://blessing.studio/tag/Ghost/"/>
    
      <category term="Hexo" scheme="https://blessing.studio/tag/Hexo/"/>
    
  </entry>
  
  <entry>
    <title>Hexo 获取特定分类或标签下的文章</title>
    <link href="https://blessing.studio/get-hexo-posts-by-category-or-tag/"/>
    <id>https://blessing.studio/get-hexo-posts-by-category-or-tag/</id>
    <published>2017-06-15T14:47:47.000Z</published>
    <updated>2017-06-15T14:47:47.000Z</updated>
    
    <content type="html"><![CDATA[<p>今天在将博客主题移植至 Hexo 时，想要获取某个分类（Category）或者标签（Tag）下的所有文章（准确来说是想获得文章总数），在使用中文关键词搜索时，没有获得任何有用的信息（或许是我搜索姿势不对）。换用英文关键词「hexo category all posts」后搜索到了所需的信息，遂决定写一篇文章记录一下，希望能帮到后来人。</p><h3 id="获取特定分类下的文章"><a href="#获取特定分类下的文章" class="headerlink" title="获取特定分类下的文章"></a>获取特定分类下的文章</h3><pre><code class="javascript">let result = site.categories.findOne({name: &#39;example&#39;})</code></pre><p>同样的，你可以这样获取特定标签下的文章：</p><pre><code class="javascript">site.tags.findOne({name: &#39;example&#39;})</code></pre><p>其中 <code>name</code> 指定要查找分类的名称，返回值是一个 Warehouse（Hexo 作者开发的一个轻量级数据库） <code>Document</code> 对象。你可以直接使用 <code>result.length</code> 来获得该分类 / 标签下的文章总数。你也可以用 <code>forEach</code> 来遍历每篇文章：</p><a id="more"></a><pre><code class="javascript">result.posts.forEach(function(post) {    // do what you want to do with each post});</code></pre><p>而 <code>result.posts</code> 是一个 Warehouse 的 <code>Model</code> 对象，所以你可以使用一些 <code>Model</code> 的高级方法（具体可用方法参见 <a href="https://zespia.tw/warehouse/Model.html" target="_blank" rel="noopener">Warehouse 文档</a>）。举个栗子：</p><pre><code class="ejs">&lt;% site.tags.findOne({name: &#39;example&#39;}).posts.sort(&#39;date&#39;, -1).limit(5).each(function(post) {%&gt;     &lt;%- partial(&#39;_partial/project-excerpt&#39;, {item: post}) %&gt; &lt;% })%</code></pre><p>这里不得不吐槽一下，Hexo 的文档真是太烂了，太烂了。写个主题，有时候想要实现一个功能还要疯狂看 Hexo 源码，说不出话。</p><h3 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h3><ul><li><a href="https://github.com/hexojs/hexo/issues/493" target="_blank" rel="noopener">How to select all posts in a certain tag or category and assign it to page.posts of the page I just created? #493</a></li><li><a href="https://stackoverflow.com/questions/38998718/how-to-filter-posts-by-tag-in-hexo" target="_blank" rel="noopener">How to filter posts by tag in Hexo? - Stack Overflow</a></li><li><a href="https://zespia.tw/warehouse/Model.html" target="_blank" rel="noopener">Model - Warehouse Documentation</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;今天在将博客主题移植至 Hexo 时，想要获取某个分类（Category）或者标签（Tag）下的所有文章（准确来说是想获得文章总数），在使用中文关键词搜索时，没有获得任何有用的信息（或许是我搜索姿势不对）。换用英文关键词「hexo category all posts」后搜索到了所需的信息，遂决定写一篇文章记录一下，希望能帮到后来人。&lt;/p&gt;
&lt;h3 id=&quot;获取特定分类下的文章&quot;&gt;&lt;a href=&quot;#获取特定分类下的文章&quot; class=&quot;headerlink&quot; title=&quot;获取特定分类下的文章&quot;&gt;&lt;/a&gt;获取特定分类下的文章&lt;/h3&gt;&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;let result = site.categories.findOne({name: &amp;#39;example&amp;#39;})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的，你可以这样获取特定标签下的文章：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;javascript&quot;&gt;site.tags.findOne({name: &amp;#39;example&amp;#39;})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;name&lt;/code&gt; 指定要查找分类的名称，返回值是一个 Warehouse（Hexo 作者开发的一个轻量级数据库） &lt;code&gt;Document&lt;/code&gt; 对象。你可以直接使用 &lt;code&gt;result.length&lt;/code&gt; 来获得该分类 / 标签下的文章总数。你也可以用 &lt;code&gt;forEach&lt;/code&gt; 来遍历每篇文章：&lt;/p&gt;
    
    </summary>
    
      <category term="技术" scheme="https://blessing.studio/categories/tech/"/>
    
    
      <category term="JavaScript" scheme="https://blessing.studio/tag/JavaScript/"/>
    
      <category term="Hexo" scheme="https://blessing.studio/tag/Hexo/"/>
    
  </entry>
  
  <entry>
    <title>让 Lumen 的 dd() 与 dump() 函数输出更漂亮</title>
    <link href="https://blessing.studio/lumen-dd-dump-with-prettier-result/"/>
    <id>https://blessing.studio/lumen-dd-dump-with-prettier-result/</id>
    <published>2017-06-11T01:57:21.000Z</published>
    <updated>2017-06-11T01:57:21.000Z</updated>
    
    <content type="html"><![CDATA[<p>做开发的，免不了要和调试打交道。单说 PHP，有的人直接 <code>echo</code>，有的人用 <code>print_r()</code>，有的人用 <code>var_dump()</code>，还有的人直接上 Xdebug，用啥的都有。</p><p>如果你用过 Laravel，那你应该知道 Laravel 内置了几个很方便的帮助函数（Helper）—— <code>dd()</code> 和 <code>dump()</code>。这两个函数都能够输出变量的值，不同的是 <code>dd()</code> 在输出变量值后会停止脚本的执行，而 <code>dump()</code> 不会。它们的使用方法可参照官方文档：<a href="https://laravel.com/docs/5.4/helpers#method-dd" target="_blank" rel="noopener">Helpers - Laravel Documentation</a>。</p><p><img src="https://img.blessing.studio/images/2017/06/11/snipaste_20170610_214238.png" alt="Laravel dd() 输出示例"></p><p>什么？哦，上帝！真是见鬼！怎么会有人在 Laravel 中还在用 <code>echo</code> + <code>die()</code>？好家伙，我敢打赌，他一定没有好好看文档，我向圣母玛利亚保证。如果让我看到这群愚蠢的土拨鼠，看在上帝的份上，我会用靴子狠狠地踢他们的屁股，我发誓我绝对会。</p><a id="more"></a><p>但是如果你在 Lumen（一个为速度而生的类 Laravel 微框架）中也想使用这些帮助函数的话，你会发现 <code>dump()</code> 无法使用了（报错 <code>Call to undefined function</code>），而且 <code>dd()</code> 的结果会变成这样瞎眼的输出：</p><p><img src="https://img.blessing.studio/images/2017/06/11/snipaste_20170610_214933.png" alt="Lumen dd() 输出示例"></p><p>淦！这不就是 <code>var_dump()</code> 吗？为毛在 Laravel 上输出那么漂亮，在 Lumen 上就无法使用了呢？</p><p>正是因为 Lumen 是个微框架。</p><p>作为一个为速度而生的微框架（全栈框架 Laravel 的性能问题一直是为人所诟病的），它把能精简的东西都尽量精简了。Laravel 那个漂亮的 <code>dd()</code> 输出实际上是依赖于 Symfony 的 <a href="http://symfony.com/doc/current/components/var_dumper.html" target="_blank" rel="noopener"><code>VarDumper</code></a> 组件，Laravel 默认预装了该组件，而 Lumen 没有。</p><p><strong>那么解决方法就很简单了，只需重新安装该组件即可</strong>：</p><pre><code>$ composer require symfony/var-dumper</code></pre><p>安装完成后就可以在 Lumen 中看到和 Laravel 一样的调试输出，而且 <code>dump()</code> 函数也可以使用了。</p><p><img src="https://img.blessing.studio/images/2017/06/11/snipaste_20170611_093434.png" alt=""></p><p>顺带一提，有人觉得这样的变量内容输出没有 <code>var_dump()</code> 好，原因是要不停地点展开很麻烦。其实只要按住 <code>Ctrl</code> 再点击箭头，就可以快速展开所有的子项目了。</p><hr><p>单说解决方法也有点太没意思，顺便也说说为啥安装上 <code>symfony/var-dumper</code> 组件后 <code>dd()</code> 的输出就会变得好看吧。</p><p>首先我们定位到 <code>dd()</code> 函数的声明位置：<code>vendor/illuminate/support/helpers.php</code>（其他大部分的帮助函数都在这里，和 Laravel 一样），可以看到该函数具体实现是这样的：</p><pre><code>if (! function_exists(&#39;dd&#39;)) {    /**     * Dump the passed variables and end the script.     *     * @param  mixed     * @return void     */    function dd(...$args)    {        foreach ($args as $x) {            (new Dumper)-&gt;dump($x);        }        die(1);    }}</code></pre><p>可以看到这里用了 PHP5.6+ 的变长参数语法 <code>...</code>，函数内部遍历参数列表并把它们交给 <code>Dumper</code> 处理，最后用 <code>die(1)</code> 结束整个脚本的运行。</p><p>那么这个 <code>Dumper</code> 是个啥呢？我们继续定位，可以发现它的完整类名为 <code>Illuminate\Support\Debug\Dumper</code>，而上面所使用的 <code>dump()</code> 方法具体实现如下（话说这函数的 DocBlock 还蛮有意思的，优雅地导出一个值）：</p><pre><code>/** * Dump a value with elegance. * * @param  mixed  $value * @return void */public function dump($value){    if (class_exists(CliDumper::class)) {        $dumper = &#39;cli&#39; === PHP_SAPI ? new CliDumper : new HtmlDumper;        $dumper-&gt;dump((new VarCloner)-&gt;cloneVar($value));    } else {        var_dump($value);    }}</code></pre><p>可以看到，<code>dd()</code> 函数是根据 <code>Symfony\Component\VarDumper\Dumper\CliDumper</code> 这个类是否存在来决定是用 <code>symfony/var-dumper</code> 组件导出漂亮的结果还是直接用内置函数 <code>var_dump()</code> 来导出。</p><p>所以在我们通过 composer 安装 <code>symfony/var-dumper</code> 组件后，<code>dd()</code> 函数就会检测并自动使用该组件替换原生函数以输出更漂亮的结果。</p><p>以上。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;做开发的，免不了要和调试打交道。单说 PHP，有的人直接 &lt;code&gt;echo&lt;/code&gt;，有的人用 &lt;code&gt;print_r()&lt;/code&gt;，有的人用 &lt;code&gt;var_dump()&lt;/code&gt;，还有的人直接上 Xdebug，用啥的都有。&lt;/p&gt;
&lt;p&gt;如果你用过 Laravel，那你应该知道 Laravel 内置了几个很方便的帮助函数（Helper）—— &lt;code&gt;dd()&lt;/code&gt; 和 &lt;code&gt;dump()&lt;/code&gt;。这两个函数都能够输出变量的值，不同的是 &lt;code&gt;dd()&lt;/code&gt; 在输出变量值后会停止脚本的执行，而 &lt;code&gt;dump()&lt;/code&gt; 不会。它们的使用方法可参照官方文档：&lt;a href=&quot;https://laravel.com/docs/5.4/helpers#method-dd&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Helpers - Laravel Documentation&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.blessing.studio/images/2017/06/11/snipaste_20170610_214238.png&quot; alt=&quot;Laravel dd() 输出示例&quot;&gt;&lt;/p&gt;
&lt;p&gt;什么？哦，上帝！真是见鬼！怎么会有人在 Laravel 中还在用 &lt;code&gt;echo&lt;/code&gt; + &lt;code&gt;die()&lt;/code&gt;？好家伙，我敢打赌，他一定没有好好看文档，我向圣母玛利亚保证。如果让我看到这群愚蠢的土拨鼠，看在上帝的份上，我会用靴子狠狠地踢他们的屁股，我发誓我绝对会。&lt;/p&gt;
    
    </summary>
    
      <category term="技术" scheme="https://blessing.studio/categories/tech/"/>
    
    
      <category term="Laravel" scheme="https://blessing.studio/tag/Laravel/"/>
    
      <category term="PHP" scheme="https://blessing.studio/tag/PHP/"/>
    
      <category term="Lumen" scheme="https://blessing.studio/tag/Lumen/"/>
    
  </entry>
  
  <entry>
    <title>给 ATH-ES55 耳机更换线材</title>
    <link href="https://blessing.studio/replace-cable-of-ath-es55/"/>
    <id>https://blessing.studio/replace-cable-of-ath-es55/</id>
    <published>2017-06-10T15:53:14.000Z</published>
    <updated>2017-06-10T15:53:14.000Z</updated>
    
    <content type="html"><![CDATA[<p>大概是几年前（总之是我的 AM800 掉在火车上之后），我从某位 <a href="https://twitter.com/Panpawliet" target="_blank" rel="noopener">dalao</a> 手里低价收来了 ATH-IM50 和 ATH-ES55 两副耳机。当时写的记录文章在 <a href="https://blessing.studio/get-im50/">这里</a>，两年前的文章，现在看起来挺尬的，行文风格差了真不是一点半点。</p><p>而且好几年过去了，这俩耳机也或多或少都出了些问题。其中 IM50 原配的线已经发硬且接触不良，官方升级线又买不起（说实话也不值），于是就去某宝买了个 DIY 升级线，用着也还算不错。</p><p><img src="https://img.blessing.studio/images/2017/06/10/snipaste_20170610_230116.png" alt="某宝订单"></p><p>至于 ES55，收来的时候耳机插头就是坏的，虽然当初随便买了个插头随便焊上去了事，不过后来又重新焊了一次（当时写的博文在 <a href="https://blessing.studio/tips-for-fixing-audio-jack/">这里</a>）。然而因为 ES55 不好在学校里用（太显眼，漏音也严重），放着吃灰了几个月，前几天高考结束拿出来的时候发现它的线材都已经变得黏糊糊的了。</p><a id="more"></a><p><img src="https://img.blessing.studio/images/2017/06/10/IMG_20170609_224017.jpg" alt="ES55 辣鸡线材"></p><p>这里不得不吐槽一下 ES55 的原装线材，突出一个辣鸡。线又细又容易缠绕，放久了还会出油变黏，感觉就像路边摊耳机线一样，说不出话 <img src="https://img.blessing.studio/images/2017/06/07/QQ20170607203042.jpg" alt="表情1"></p><p>这时候我正好也不想继续用这根 IM50 升级线了（外套的热缩管开始发黄，插到耳机单元上的插头壳子掉了一半，耳挂也没原装的好用），于是就打算把它换到 ES55 上，也算是废物利用了（笑）网上也没有关于 ATH-ES55 换线的相关资料，于是写<del>水</del>一篇博文权当记录。</p><p>既然决定要做，就该动粗了<img src="https://img.blessing.studio/images/2017/06/10/QQ20170610233518.jpg" alt="表情2"></p><p><img src="https://img.blessing.studio/images/2017/06/10/IMG_20170610_200132.jpg" alt="1"></p><p><em>▲ 先把 IM50 升级线的头给去掉。镀银线不知道是不是真的，粗倒是挺粗的，声音也没听出来啥差别。</em></p><p><img src="https://img.blessing.studio/images/2017/06/10/IMG_20170610_200459.jpg" alt="2"></p><p><em>▲ 俩单元用四颗螺丝固定，家中常备螺丝刀套装很有用（笑）另外注意拆的时候不要手一抖戳振膜上了。</em></p><p><img src="https://img.blessing.studio/images/2017/06/10/IMG_20170610_202429.jpg" alt="3"></p><p><em>▲ 用烙铁弄掉原装线后，把新的线焊上去。然而我没正经学过焊接，虽不至于虚焊，不过焊的很丑，但愿不要太影响声音。另外注意烙铁不要长时间放在焊点上，不然可能把背面的振膜烫变形（我之前有个便宜头戴就是这样死的）</em></p><p><img src="https://img.blessing.studio/images/2017/06/10/IMG_20170610_210319.jpg" alt="4"></p><p><em>▲ 完成形态。虽然白色线有点违和……</em></p><p>以上。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;大概是几年前（总之是我的 AM800 掉在火车上之后），我从某位 &lt;a href=&quot;https://twitter.com/Panpawliet&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;dalao&lt;/a&gt; 手里低价收来了 ATH-IM50 和 ATH-ES55 两副耳机。当时写的记录文章在 &lt;a href=&quot;https://blessing.studio/get-im50/&quot;&gt;这里&lt;/a&gt;，两年前的文章，现在看起来挺尬的，行文风格差了真不是一点半点。&lt;/p&gt;
&lt;p&gt;而且好几年过去了，这俩耳机也或多或少都出了些问题。其中 IM50 原配的线已经发硬且接触不良，官方升级线又买不起（说实话也不值），于是就去某宝买了个 DIY 升级线，用着也还算不错。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.blessing.studio/images/2017/06/10/snipaste_20170610_230116.png&quot; alt=&quot;某宝订单&quot;&gt;&lt;/p&gt;
&lt;p&gt;至于 ES55，收来的时候耳机插头就是坏的，虽然当初随便买了个插头随便焊上去了事，不过后来又重新焊了一次（当时写的博文在 &lt;a href=&quot;https://blessing.studio/tips-for-fixing-audio-jack/&quot;&gt;这里&lt;/a&gt;）。然而因为 ES55 不好在学校里用（太显眼，漏音也严重），放着吃灰了几个月，前几天高考结束拿出来的时候发现它的线材都已经变得黏糊糊的了。&lt;/p&gt;
    
    </summary>
    
      <category term="日常" scheme="https://blessing.studio/categories/diary/"/>
    
    
      <category term="耳机" scheme="https://blessing.studio/tag/%E8%80%B3%E6%9C%BA/"/>
    
  </entry>
  
  <entry>
    <title>在中国大陆购买 Google Play 内容的几种方法</title>
    <link href="https://blessing.studio/google-play-payments-at-china-mainland/"/>
    <id>https://blessing.studio/google-play-payments-at-china-mainland/</id>
    <published>2017-06-07T13:41:16.000Z</published>
    <updated>2017-06-07T13:42:07.000Z</updated>
    
    <content type="html"><![CDATA[<p><img src="https://img.blessing.studio/images/2017/06/07/google_play_purchase.png" alt="剁tm的！" style="border: 0;"></p><p>虽然 Google Play 在中国大陆无法正常使用，但作为 Android 官方的应用市场，我们<del>正版软件受害者</del>经常需要在 Play 商店上购买正版应用或者通过 Google Play 内购支持开发者等等。</p><p>但是作为一个不存在的公司，我们在国内用惯了的支付宝、微信支付等国内支付平台自然是无法用于购买 Google Play 内容的，所以想要在 Google Play 上花钱并不是一件容易的事。</p><p>那么身在中国大陆，我们到底要怎样才能在 Google Play 上愉快地买买买呢？本文列举了一些行之有效的方法（截至本文发布时），权当记录，希望对后来人有帮助。</p><a id="more"></a><h3 id="一、双币信用卡"><a href="#一、双币信用卡" class="headerlink" title="一、双币信用卡"></a>一、双币信用卡</h3><p>国内某些银行的多币种信用卡可以直接在 Google Play 上绑定，譬如招商银行的双币信用卡，不过账单地址要改为其他国家（具体情况可自行搜索）。</p><p>然而我是学生，这种方法 PASS<br><img src="https://img.blessing.studio/images/2017/06/07/QQ20170607203042.jpg" alt="表情1">。</p><h3 id="二、虚拟信用卡"><a href="#二、虚拟信用卡" class="headerlink" title="二、虚拟信用卡"></a>二、虚拟信用卡</h3><p>没有信用卡，网上提供的虚拟（预付费）信用卡也是可以在 Google Play 上使用的。</p><p>虚拟信用卡服务中最著名的当数 <a href="https://www.globalcash.hk" target="_blank" rel="noopener">全球付 GlobalCash</a>，我第一次（2015 年）在 Google Play 上购买正版应用（Solid Explorer）时就是用的这家。</p><p><img src="https://img.blessing.studio/images/2017/06/05/snipaste_20170605_223508.png" alt="GlobalCash 页面截图"></p><p>但是，<strong>现在</strong>我并不推荐你使用全球付。为啥呢？</p><ol><li><strong>手续费很高。</strong>单次充值最低 300CNY，小额交易每笔收取 3CNY 手续费，非本币交易收取 1.5% 的手续费。详见：<a href="https://www.globalcash.hk/foot_explain.html" target="_blank" rel="noopener">全球付 - 收费标准</a>；</li><li>余额转出需要实名认证；</li><li>经常被人指出汇率不对，不按照实时汇率表来；</li><li>近期内也经常出现全球付虚拟信用卡无法绑定 Google Play 、无法付款<em>（交易遭拒，付款方式无效）</em>或者账户被冻结<em>（要求提供信用卡或身份证明）</em>的消息。</li></ol><p>综上所述，如果你想要使用全球付，还请在充值大额款项前三思。其他提供虚拟信用卡的商家还有 Payoneer（年费 $30）和 Wirex（支持 Bitcoin，没用过）。</p><p><img src="https://img.blessing.studio/images/2017/06/07/snipaste_20170607_205727.png" alt="Google Payments Help"></p><p>不过 Google Payments 的官方帮助页面中<a href="https://support.google.com/payments/answer/6220309?hl=zh-Hans" target="_blank" rel="noopener">声明</a>了不支持虚拟信用卡，即使你确实可以使用，也不能保证之后会不会出什么事情。</p><h3 id="三、礼品卡"><a href="#三、礼品卡" class="headerlink" title="三、礼品卡"></a>三、礼品卡</h3><p>那么看起来适合学生的唯一方式就是购买 Google Play 的礼品卡了，在线购买礼品卡的话马云家就有。不过在某宝购买时请注意不要买到黑卡了，最好选那种发礼品卡扫描图片的商家（虽不能完全保证，但相对来说出事几率小一些）</p><p><img src="https://img.blessing.studio/images/2017/06/05/gift_card_sacn_img.jpg" alt="礼品卡扫描"></p><h3 id="四、注意事项"><a href="#四、注意事项" class="headerlink" title="四、注意事项"></a>四、注意事项</h3><ol><li>买礼品卡时要注意不要买错区，锁美区的方法可以 Google 查。<a href="https://www.v2ex.com/t/268074" target="_blank" rel="noopener">如何查看你当前 Google Play 账户在哪个区域？</a></li><li>第一次充值时会让你创建 Google Wallet 账户，记得地址填美国的，之后可以在 <a href="https://payments.google.com/payments/home#settings" target="_blank" rel="noopener">Google 钱包地区设置</a> 查看；</li><li>兑换礼品卡时不一定要用美国 IP 的代理服务器，我用的 HK 代理似乎也没问题；</li><li>题外话，当年用盗版欠下的债，总是会还的。</li></ol><p><img src="https://img.blessing.studio/images/2017/06/05/snipaste_20170605_223826.png" alt="Google Payments"></p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;&lt;img src=&quot;https://img.blessing.studio/images/2017/06/07/google_play_purchase.png&quot; alt=&quot;剁tm的！&quot; style=&quot;border: 0;&quot;&gt;&lt;/p&gt;
&lt;p&gt;虽然 Google Play 在中国大陆无法正常使用，但作为 Android 官方的应用市场，我们&lt;del&gt;正版软件受害者&lt;/del&gt;经常需要在 Play 商店上购买正版应用或者通过 Google Play 内购支持开发者等等。&lt;/p&gt;
&lt;p&gt;但是作为一个不存在的公司，我们在国内用惯了的支付宝、微信支付等国内支付平台自然是无法用于购买 Google Play 内容的，所以想要在 Google Play 上花钱并不是一件容易的事。&lt;/p&gt;
&lt;p&gt;那么身在中国大陆，我们到底要怎样才能在 Google Play 上愉快地买买买呢？本文列举了一些行之有效的方法（截至本文发布时），权当记录，希望对后来人有帮助。&lt;/p&gt;
    
    </summary>
    
      <category term="随笔" scheme="https://blessing.studio/categories/essay/"/>
    
    
      <category term="记录" scheme="https://blessing.studio/tag/%E8%AE%B0%E5%BD%95/"/>
    
      <category term="Android" scheme="https://blessing.studio/tag/Android/"/>
    
      <category term="Google" scheme="https://blessing.studio/tag/Google/"/>
    
  </entry>
  
  <entry>
    <title>反驳为“六四”屠杀辩护的几种论调</title>
    <link href="https://blessing.studio/28th-8964/"/>
    <id>https://blessing.studio/28th-8964/</id>
    <published>2017-06-03T16:51:27.000Z</published>
    <updated>2017-06-04T04:36:35.000Z</updated>
    
    <content type="html"><![CDATA[<p>自从我建这个博客以来，每逢六月四日我都要发一篇纪念“六四”的文章。本来我也想写一篇长文讲讲几年来中国社会某些魔幻之处的，提纲草稿都已列好，奈何高考将近，虽惜其不成，却也只得暂且放下。</p><p><strong>今天是“六四”事件的第 28 周年纪念日。</strong></p><p>近年来，从 709 维权律师大抓捕，到抗日爱国青年的 U 型锁，到 clowwindy 的被喝茶，到雷洋案的涉案民警不起诉，到「向西方司法独立等错误思潮亮剑」的荒谬言论，到少年雷文峰之死，到打破了不知多少经济学常识的房地产大泡沫，到逼人犯罪的辱母杀人案，到愈发严格的言论管制信息审查，到打碎一地玻璃心的马里兰大学毕业演讲，到反普世价值与对基本人权的剥夺，再到郭文贵爆料所揭示腐败盛行的中共高层……</p><p>我亲眼见着在【伟大光荣正确】的中国共产党领导下的盛世中国正上演着一部愈演愈烈的大型社会主义魔幻剧。</p><blockquote><p>满纸荒唐言，一把辛酸泪。</p><p>盛世中国梦，谁解其中味？</p></blockquote><a id="more"></a><p>一般来说，我这博客都是不转载文章的，不过今天是特殊情况嘛，不发点东西，不好；把半成品的文章发出去，更不好（顺带吐槽下那些喜欢发“占坑”文章的人，你倒是写完了再发啊，照顾下 RSS 订阅用户的感受嘛）。</p><p>于是我决定久违地转载胡平先生一篇关于“六四”事件的文章：《反驳为“六四”屠杀辩护的几种论调》，算是对那些「有必要血腥镇压」谬论的反驳。<em>原文出自《北京之春》1997 年 3－7 月号。</em></p><hr><p>有些人说，那帮民运领袖素质太差，嘴上高喊民主，骨子里比共产党还更不民主，与其让他们上台，还不如继续让共产党接着干呢。</p><p>据说邓小平在“六四”前夕讲过，我们要是再让步，就把整个政权都送给这帮学生了。</p><p>我说此论奇怪，因为它不仅不符合事实，而且根本不通。非暴力民主运动所能取得的最大胜利，无非是迫使当局宣布放弃一党专制，然後举行自由选举。这不是把政权拱手交给广场上的民运领袖，而是还政於民，交给全体人民。到那时你既可投票给这帮民运领袖，也可投票给前共产党，或者是投给其他你中意的人物。靠暴力夺权的革命党才是打天下者坐天下，你今天支持了他们的革命，明天就得接受他们的统治。到时候他们若不实行民主，你只好连呼上当。<strong>非暴力民主运动不是、也不可能是打天下者坐天下，因为非暴力运动的力量来自人民的自觉参与，它没有暴力工具可以把自己的意志强加於他人。它只能瓦解强权，却不能制造强权。</strong>它可以对抗强权，但它本身并不是强权。支持这样的事业并不是帮他人夺取权力，而是为大家赢得权利。就算某位大力鼓吹婚姻自主的人实际上是个流氓，但认同这个主张绝不等於到头来要你非嫁给他不可。因此，借口某些民运领袖素质低劣，便转而不认同自由民主理念，不支持非暴力民主运动，维护现有的专制政权，并且对专制者血腥镇压异议人士漠然置之，这在逻辑上是完全不通的。</p><p>上述（为“六四”屠杀辩护的）议论不符合逻辑，但却投合了某些人的心态。如今流行的各种为“六四”辩护的论调，包括把“六四”轻描淡写，以为无损於邓小平英明伟大的论调。说到底，无非是一种合理化，是一种自欺。持此论调者，许多人本来对共产专制、对“六四”事件也是满怀义愤的，只是敢怒不敢言而已。但是，<strong>人是这样一种动物，他总是力图使自己的内在思想与其外在言行保持一致。如果他不能用思想指导行为，他就要用行为调整思想。如果你不敢坚持抗议，你就会努力说服自己抗议是不必要的，是无意义的，甚至是不应该的。</strong>见到婆婆欺负媳妇，你可以想，等媳妇成了婆婆，还不是一样反过去欺负别人。见到强盗杀害儿童，你可以想，若让这儿童长大成人，说不定比那强盗还坏。这样一想，心中似乎便释然了。现在，我们一讲起各自在毛时代的种种愚蠢的观点言论，大家都说是上当受骗。但若细细追究下去，所谓被欺骗难道就仅仅是被欺骗，难道其中就没有自欺的成份？经验告诉我们，少有被欺者不先自欺也。又因为我们大家都生活在共同的情境之中，许多人都会有合理化的心理需要，有自欺的心理需要，所以一种自欺欺人的为暴政辩护的合理化观点也很容易流传开来，这种流传反过来又可以加强自欺的力量。<strong>心理学家早就发现，在许多被压迫者那里会有一种认同压迫者的心理倾向。压迫令人屈辱，摆脱屈辱感的最简单的办法莫过於把压迫不再当作压迫，而是当作必要的管束。</strong>有了这种需要，自然不难找到说词，虽然漏洞百出，总可麻痹一阵。人的良心就是这样被扭曲的。良心是这样一种东西，除非你自己也去加把力扭曲它，否则任谁也不能将它扭曲。</p><p>也许你会反驳我：「不，事情并不象你所说的那样。不错，我们当初都热情地支持过八九民运，我们也都愤怒地抗议过“六四”屠杀。但是几年下来之後，我们冷静了，我们意识到我们当初做的并不正确。不是我们吓怕了，故意编出一套说词自欺欺人，而是我们想通了，真的是想通了。」我不信是如此。如果真是如此，事情只会更糟糕。能认错本是好事，但是，在被打被杀之後去认错却是坏事，只要你的错是思想认识之错，只要你的错并未侵害他人。如果你是在高压之下被迫认错，你实际上并没有服气，那另当别论。如果你竟然心悦诚服，因此反过来承认我们当初该打该杀，打的有理杀的必要，多亏邓小平下令开枪当头棒喝，才使我们变得清醒，事後多年才渐渐体会到他老人家的高瞻远瞩，打我们杀我们其实全是为了我们大家好。你就把自己看得太下贱了！</p><p>民众当然也有犯错误的时候。凡人皆可犯错。唯能犯错，人才是人。犯错误是人类的特权。不准犯错误就是不准人是人。举凡各种权利，其实也就是犯各种错误的权利。保障言论自由就是保障说错话的自由，因为说错话造不成直接的伤害，因为只有试错才能得真。但不能有杀人的自由，因为杀错人无法使之复生。我们需要自由，因为我们需要成长，需要成熟，需要发展，需要自我实现。<strong>凡是在正当权利的范围之内犯的错误，只能让人们自己教育自己，自己纠正自己，只能被说服，不能被镇压。哪怕你的见解比我更正确，你若因此镇压我，你的行为就比我更错误。</strong>我可以改正我的认识，但我绝不能认可你的镇压。我错了也是对的（英文 right，既表示“正确”，又表示“权利”），你对了也是错的。</p><p>讲到犯错误，最大的问题就是「只准州官放火，不许百姓点灯」。邓小平犯的错误还少了吗？连邓自己也承认从政一生，错占五成。专制者也犯错，我们也犯错。可是，这两种错是何等的不同。专制者一错，例如反右「扩大化」之错，那就是让五十万人青春断送，成千上万家破人亡，到头来还是高坐台上，美其名曰「没有经验，犯错误是难免的」。我们一“错”，例如“错误”地参加了“动乱”，无非是说“错”了话，上“错”了街，游“错”了行，还没有伤害任何人（起码是还没来得及伤害任何人吧），到头来我们就被杀被抓，被监禁被流亡。专制者有犯错的无限权力，我们却连犯错的半点权利都没有。你有什么根据还为专制者辩护？除非你认定专制者和我们不是一类，要么他们不是人，要么我们不是人。</p><p>（全文完）</p><hr><p>我很期待中共这样继续搞下去，最后究竟会落得一个怎样的下场 :D</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;自从我建这个博客以来，每逢六月四日我都要发一篇纪念“六四”的文章。本来我也想写一篇长文讲讲几年来中国社会某些魔幻之处的，提纲草稿都已列好，奈何高考将近，虽惜其不成，却也只得暂且放下。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;今天是“六四”事件的第 28 周年纪念日。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;近年来，从 709 维权律师大抓捕，到抗日爱国青年的 U 型锁，到 clowwindy 的被喝茶，到雷洋案的涉案民警不起诉，到「向西方司法独立等错误思潮亮剑」的荒谬言论，到少年雷文峰之死，到打破了不知多少经济学常识的房地产大泡沫，到逼人犯罪的辱母杀人案，到愈发严格的言论管制信息审查，到打碎一地玻璃心的马里兰大学毕业演讲，到反普世价值与对基本人权的剥夺，再到郭文贵爆料所揭示腐败盛行的中共高层……&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;/blockquote&gt;
    
    </summary>
    
      <category term="随笔" scheme="https://blessing.studio/categories/essay/"/>
    
    
      <category term="六四" scheme="https://blessing.studio/tag/%E5%85%AD%E5%9B%9B/"/>
    
      <category term="政治" scheme="https://blessing.studio/tag/%E6%94%BF%E6%B2%BB/"/>
    
      <category term="转载" scheme="https://blessing.studio/tag/%E8%BD%AC%E8%BD%BD/"/>
    
  </entry>
  
  <entry>
    <title>纪念 ATH-C770 君</title>
    <link href="https://blessing.studio/rip-ath-c770/"/>
    <id>https://blessing.studio/rip-ath-c770/</id>
    <published>2017-05-26T15:02:13.000Z</published>
    <updated>2017-05-26T15:07:27.000Z</updated>
    
    <content type="html"><![CDATA[<p>ATH-C770 者，铁三角家之低端入门耳塞也。</p><p>自 2016.11.11 起，C770 君半岁为吾枕边塞，夙兴夜寐，靡有朝矣。三频无短板，听感亦舒适，中能女毒，下能动次，虽难阻耳外嘈杂喧嚣，却也能保吾行<del>（自行）</del>车之安全，实为 ￥200 以下超值之选。</p><p><img src="https://img.blessing.studio/images/2017/05/26/IMG_20170526_2131012.jpg" alt="C770 遗照"></p><p>……不知道上面抽了什么风用蹩脚的文言做了博文开头，看起来逼格还蛮高。然而我并没有学过文言写作，顶多也就是应试教育中知晓了些许文言知识，拿点词句瞎耍耍，诸君见笑了 XD</p><a id="more"></a><p>讲实在话，铁三角的 C770 是确实 200 元以内入门耳塞中的佼佼者。当初双 11 时因为没钱买高端塞，不得已退而求其次买下了它，听了半年后，竟也觉得很不错，且隐有退烧之势<del>（你本来就没烧过好吗）</del>。</p><p>本以为它能伴我度过这个高三，未曾想人算不如天算，即使我在使用时极尽我所能爱护之<del>（你爱护个屁啊不都是卷一团塞口袋的吗）</del>，C770 君还是于今天，【公历 2017 年 5 月 26 日】，永远地离开了我。</p><hr><p>今天下午从家里出发去学校时，刚骑上我心爱的小车车，掏出卷成一团的 C770 君和某杂牌 MP3 时，却发现其中一个耳塞的前盖与滤网早已不知所踪，空余耳机单元与振膜在空中晃荡作响。</p><p>虽愿细察其下落，然当是时吾处迟到扣分之际，只得于一声「F*CK」后策<del>自行</del>车狂奔，终得以踩铃入校，实属不易。</p><p>结果到最后也愣是没找着那半边壳，一路颠簸后振膜也从单元上脱落了，估计焊回去也没什么卵用，只好接受我已然失去 C770 君之事实。</p><p>没有了 C770 这一副我唯一的耳塞，现在我手上只剩下同为铁家的 ATH-IM50 入耳监听与 ATH-ES55 耳罩了，非常吃瘪。而且后者超tm会漏音，根本不敢在教室用 :(</p><p>然 IM50 者，其经年已久，线材梆硬，接触不良，虽曾试以某宝所售自制线材代之，终不能与官方线材相较。鄙人亦贫，官方升级线亦遥不可及<del>（还贵的要死，买升级线不如新买副耳机）</del>，出此下策，实为无奈。</p><p><img src="https://img.blessing.studio/images/2017/05/26/IMG_20170526_2240441.jpg" alt="IM50 换线"></p><blockquote><br>  <p style="font-size: large;">Requiescat in pace, C770.</p><br>  <p><del>哥特式金属私生子</del></p><br></blockquote><p><br></p><p>高考之际，会遭此祸，临 Blog 涕零，嗦不出话。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;ATH-C770 者，铁三角家之低端入门耳塞也。&lt;/p&gt;
&lt;p&gt;自 2016.11.11 起，C770 君半岁为吾枕边塞，夙兴夜寐，靡有朝矣。三频无短板，听感亦舒适，中能女毒，下能动次，虽难阻耳外嘈杂喧嚣，却也能保吾行&lt;del&gt;（自行）&lt;/del&gt;车之安全，实为 ￥200 以下超值之选。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.blessing.studio/images/2017/05/26/IMG_20170526_2131012.jpg&quot; alt=&quot;C770 遗照&quot;&gt;&lt;/p&gt;
&lt;p&gt;……不知道上面抽了什么风用蹩脚的文言做了博文开头，看起来逼格还蛮高。然而我并没有学过文言写作，顶多也就是应试教育中知晓了些许文言知识，拿点词句瞎耍耍，诸君见笑了 XD&lt;/p&gt;
    
    </summary>
    
      <category term="日常" scheme="https://blessing.studio/categories/diary/"/>
    
    
      <category term="耳机" scheme="https://blessing.studio/tag/%E8%80%B3%E6%9C%BA/"/>
    
  </entry>
  
  <entry>
    <title>使用 ffmpeg 拼接 bilibili 客户端所下载的分段 flv 视频</title>
    <link href="https://blessing.studio/use-ffmpeg-to-concat-flv-videos-downloaded-by-bilibili-client/"/>
    <id>https://blessing.studio/use-ffmpeg-to-concat-flv-videos-downloaded-by-bilibili-client/</id>
    <published>2017-05-13T12:48:50.000Z</published>
    <updated>2017-05-13T13:48:21.000Z</updated>
    
    <content type="html"><![CDATA[<p>唉，我现在用的这破手机播 1080P 视频要掉帧，只好在电脑上看，说不出话。</p><h3 id="缘起"><a href="#缘起" class="headerlink" title="缘起"></a>缘起</h3><p>前些天我关注的某 <a href="http://space.bilibili.com/15810/#!/" target="_blank" rel="noopener">气人主播</a> 在 bilibili 上传了《盗贼之海》（<em>Sea of Thieves</em>）Alpha 测试的直播录像，却因吃了文化亏不慎违反了保密协定，不久视频就被不存在了。不过好在有热心猛男把缓存好了的睾清视频发了出来，像我这样没赶上趟的人才能爽到 :P</p><p><img src="https://img.blessing.studio/images/2017/05/13/20170513185210.png" alt="感谢这位pong友"></p><p>然而这位pong友上传的是 bilibili 客户端的下载（缓存）格式，虽然放在手机的 <code>/Android/data/tv.danmaku.bili/download</code> 目录下就可以被客户端直接识别，但是想要在电脑上直接播放就没那么容易了。</p><a id="more"></a><p>我们先来看看 B 站客户端下载内容的目录结构：</p><pre><code class="text">10034455（视频 AV 号）├── 1（视频的各分 P）│   ├── danmaku.xml（弹幕文件）│   ├── entry.json（单 Part 信息、标题等等）│   └── lua.flv.bili2api.3（分段视频，一段差不多六分钟）│       ├── 0.blv（就是改了后缀的 flv 文件）│       ├── 0.blv.4m.sum（校验码）│       ├── 1.blv│       ├── 1.blv.4m.sum│       ├── 2.blv│       ├── 2.blv.4m.sum│       └── index.json（储存分段信息）├── 2└── 3</code></pre><p>可以看到目录的结构还是比较清晰的，一眼看过去就能懂个大概，也没有什么反人类的混淆机制。想要在电脑上观看的话，就必须把那些 <code>0.blv</code>、<code>1.blv</code> 之类的文件后缀先修改为 <code>.flv</code>，然后再把他们合并起来（不合并的话就无法在那些本地弹幕播放器中与弹幕文件时间轴对应了）。</p><p>不过作为一个搞技术的人，我自然是不可能去手动重命名 + 合并这些东西的，那太蠢了。这种无脑力气活就该让计算机来帮我们解决 ;)</p><h3 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h3><ul><li>合并分段的 flv 文件到一个文件；</li><li>操作方便；</li><li>速度尽可能地快。</li></ul><h3 id="尝试"><a href="#尝试" class="headerlink" title="尝试"></a>尝试</h3><p>因为图方便，我先是在 Google 上找了一些视频合并的软件，包括「格式工厂」、「硕鼠合并」、「极速 FLV 合并器」、「BoilsoftVideoJoiner」等。经试用，硕鼠等大部分过久没有更新的软件都无法正确识别上述的 FLV 格式，导致合并失败，具体原因没有去深究。而格式工厂等软件似乎必须对所有分段视频进行转码后再合并（有损合并），即使那些视频的尺寸、音视频编码均相同。这也导致这些软件合并速度屌慢，不予考虑。</p><h3 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h3><p>最后还是决定用 <code>ffmpeg</code> 进行视频合并操作。不得不说，<code>ffmpeg</code> 这个养活了绝大多数国产视频处理软件的开源项目在视频处理方面确实是一把好手，Google 一下就找到了解决方案。</p><p>举个栗子，如果你有 这几个视频要合并：</p><p><code>1.flv</code>、<code>2.flv</code>、<code>3.flv</code></p><p>你只需要建立一个文本文件（e.g. <code>ff.txt</code>），在里面写上：</p><pre><code># 相对路径、绝对路径均可file &#39;/path/to/1.flv&#39;file &#39;/path/to/2.flv&#39;file &#39;/path/to/3.flv&#39;</code></pre><p>然后在 shell 中运行：</p><pre><code class="bash">ffmpeg -f concat -i /path/to/ff.txt -c copy output.mp4# 参数说明：# -i 设定输入文件# -f 设定编码器，这里使用 concat 无损合并# -c 流选择器，这里选择所有流# 最后可以选择任何可以封装的格式，不一定是 MP4</code></pre><p>然后，就好了。合并速度差不多就是你硬盘的 I/O 速度。</p><p>题外话，在 Windows 上，<code>ffmpeg</code> 可以用 <a href="https://chocolatey.org/" target="_blank" rel="noopener">Chocolatey</a> 这个包管理器安装。直接在终端中运行 <code>$ choco install ffmpeg</code>，下载安装修改环境变量一步到位，突出一个爽到。</p><h3 id="写个脚本"><a href="#写个脚本" class="headerlink" title="写个脚本"></a>写个脚本</h3><p>知道了原理，就可以动手写个自动化脚本了。这里我选择了 Bash 脚本语言，毕竟就这么点功能，几行就 OK。用 Bash 的话开个终端复制进去就可以直接运行，比起其他语言也方便 <del>（我才不会说是因为我 Python 都快忘光了呢）</del>。</p><p>另外需要注意的是，有些视频的分段超过了 10 个文件，如果直接遍历的话会变成 <code>[1, 10, 2, 3...]</code> 这样的顺序，导致最后拼接出来的视频也变成这样，所以需要给单位数的分段先添个 <code>0</code>，这样就会是 <code>[01, 02 ... 10]</code> 的正常顺序了。</p><p class="p-load-gist"><button class="btn btn-default load-gist" data-hash="1bc29da99b238d68e87af874f898f435">点击以加载 Gist（无法加载时请翻墙）</button></p><p>这里是压缩过的单行版，方便复制：</p><pre><code class="bash">cat /dev/null &gt; ff.txt;for i in *.blv; do seq_num=&quot;${i%.blv}&quot;;if [ &quot;${#seq_num}&quot; -eq 1 ];then mv &quot;$i&quot; &quot;0$seq_num.flv&quot;;else mv &quot;$i&quot; &quot;$seq_num.flv&quot;;fi;done;for i in *.flv; do echo &quot;file &#39;${i}&#39;&quot; &gt;&gt; &quot;ff.txt&quot;;done;ffmpeg -f concat -i ff.txt -c copy ../output.mp4;rm ff.txt;printf &quot;success&quot;</code></pre><h3 id="效果"><a href="#效果" class="headerlink" title="效果"></a>效果</h3><p><img src="https://ooo.0o0.ooo/2017/05/13/5916f998dd712.png" alt="终端执行"></p><p><img src="https://img.blessing.studio/images/2017/05/13/QQ20170513201722.png" alt="输出结果"></p><p>合并过程大概六七秒吧，一发入魂非常到位。</p><p>将合并后的视频文件 <code>output.mp4</code> 和 <code>danmaku.xml</code> 弹幕文件一起拖入「弹弹 Play」或「BiliLocal」等本地弹幕播放器后就可以观看了，爽到。</p><p><img src="https://img.blessing.studio/images/2017/05/13/QQ20170513202429.png" alt="弹幕播放器"></p><h3 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h3><ul><li><a href="https://www.cnbeining.com/2014/05/dealing-with-cat-all-on-video-non-destructive-merge-mainly-h-264-problem/" target="_blank" rel="noopener">和猫打交道——所有关于视频无损合并（主要是 H.264）的问题</a></li><li><a href="https://github.com/soimort/you-get/issues/324" target="_blank" rel="noopener">使用 ffmpeg concat 分离器来更有效地拼接视频</a></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;唉，我现在用的这破手机播 1080P 视频要掉帧，只好在电脑上看，说不出话。&lt;/p&gt;
&lt;h3 id=&quot;缘起&quot;&gt;&lt;a href=&quot;#缘起&quot; class=&quot;headerlink&quot; title=&quot;缘起&quot;&gt;&lt;/a&gt;缘起&lt;/h3&gt;&lt;p&gt;前些天我关注的某 &lt;a href=&quot;http://space.bilibili.com/15810/#!/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;气人主播&lt;/a&gt; 在 bilibili 上传了《盗贼之海》（&lt;em&gt;Sea of Thieves&lt;/em&gt;）Alpha 测试的直播录像，却因吃了文化亏不慎违反了保密协定，不久视频就被不存在了。不过好在有热心猛男把缓存好了的睾清视频发了出来，像我这样没赶上趟的人才能爽到 :P&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.blessing.studio/images/2017/05/13/20170513185210.png&quot; alt=&quot;感谢这位pong友&quot;&gt;&lt;/p&gt;
&lt;p&gt;然而这位pong友上传的是 bilibili 客户端的下载（缓存）格式，虽然放在手机的 &lt;code&gt;/Android/data/tv.danmaku.bili/download&lt;/code&gt; 目录下就可以被客户端直接识别，但是想要在电脑上直接播放就没那么容易了。&lt;/p&gt;
    
    </summary>
    
      <category term="技术" scheme="https://blessing.studio/categories/tech/"/>
    
    
      <category term="Shell" scheme="https://blessing.studio/tag/Shell/"/>
    
  </entry>
  
  <entry>
    <title>谈谈徐晓东：又一位敢于说真话的人倒下了</title>
    <link href="https://blessing.studio/about-xu-xiaodong/"/>
    <id>https://blessing.studio/about-xu-xiaodong/</id>
    <published>2017-05-09T15:16:23.000Z</published>
    <updated>2017-05-09T15:16:23.000Z</updated>
    
    <content type="html"><![CDATA[<p>前些天某 MMA（Mixed Martial Arts，综合格斗）狂人徐晓东 20 秒打倒雷公太极创始人「雷雷」的事在中国互联网上传得火热，在大家唇枪舌剑讨论传统武术时，徐晓东又爆出许多武林界造假行为，并对各大传统武术门派下了战书。</p><p>如果只是击败了雷雷一人（这家伙曾被央视介绍为「十大民间武林高手」），那也倒是还好。其他譬如陈氏太极都跳出来声明其无法代表太极门派所以不作数，雷雷本人也表示自己在这场对决中<a href="http://qiwen.lu/33959.html" target="_blank" rel="noopener">没有用内力</a>，因为他担心打完之后进派出所（笑）。</p><p>事情到这里，那些到处收徒的武林门派「大师」都是些什么货色大家都已经心知肚明：腐朽不堪的武林，师傅徒有虚名，弟子心照不宣，一个个江湖老炮心里盘算着金钱、名声。</p><p>如果徐晓东就此收手，那大家表面上笑笑，也就过去了。毕竟在这个信息过剩的互联网时代，人们对某件事的关注总是会被快速地冲淡。</p><p>但是徐晓冬这个揍了大师雷雷的莽汉，还在直播中口无遮拦，大肆爆料行业中的「潜规则」与造假行为。他喊话全武林，高举「武术打假」的大旗要挑战各门各派的「大师」。</p><p>于是各位「大师」们慌了，生怕自己被看破金身。但是一介莽夫愣头青徐晓东，又怎能拗得过整个武林的老江湖呢？</p><a id="more"></a><p>5 月 4 日，中国武协正式出面，说徐晓冬和雷公太极的比武是违法的，禁止全国各武术门派参与比武，其本人更是被某派太极的七位弟子堵在门口，而徐晓冬为了人身安全，不得不报警，寻求警察保护。</p><p>同时，徐晓东也被扣上了卖国、辱华的大帽子，大师们发动群众扒光了徐晓冬的「底裤」，指责他「头衔是假」、「简历伪造」、「被境外势力利用」、「私斗更要被严惩」……其新浪微博账户也被销号，百度、微博、微信都开始「根据相关法律法规，搜索结果不予显示」（05/09 今天我去搜的时候似乎又可以了，不清楚是怎么回事），连徐晓冬创办的武馆周日也遭消防武警和公安上门检查要求整改。</p><p>徐晓冬过于高调的言行踩到了许多武林中人的底线，也得罪了各大门派和政府主管机关。</p><p>中共官媒之前大肆宣传的传统武术被打脸，自然是要想办法挽回其本就低得可怜的公信力，于是「<a href="http://sports.qq.com/a/20170503/051360.htm" target="_blank" rel="noopener">人民日报：徐晓冬是在炒作 武林勿中其 “奸计”</a>」、「<a href="http://xw.qq.com/news/20170504030921/NEW2017050403092104" target="_blank" rel="noopener">环球时报：4 年前发文称 “钓鱼岛是日本的”？徐晓冬如此回应</a>」等煽动舆论的文章横空出世，大有当初煽动抵制乐天超市的架势。</p><p>我本来对这件事是持看戏态度的，我很佩服徐晓东正面爆出中国武林黑幕的勇气，但还是心想着这家伙这么狂总会有几个真正的大师站出来教他做人的。</p><p>但是万万没想到，不是传统武术大师，而是政治力介入教他做人了。</p><p>内力深厚的武林大师们不战而胜，令我瞠目结舌。</p><p>又一位敢于说真话的人被逼到如此地步，愤恨难耐以至于在高考临近之余写下这篇文章。联想到昨天开庭的湖南律师谢阳<a href="http://chinadigitaltimes.net/chinese/2017/05/%E7%8E%AF%E7%90%83%E6%97%B6%E6%8A%A5-%E6%B9%96%E5%8D%97%E5%BE%8B%E5%B8%88%E8%B0%A2%E9%98%B3%E6%A1%88%E5%BC%80%E5%BA%AD-%E8%B0%A2%E9%98%B3%E5%BD%93%E5%BA%AD%E5%90%A6%E8%AE%A4%E5%8F%97%E9%85%B7/" target="_blank" rel="noopener">涉嫌「煽动颠覆国家政权罪」案件</a>上，谢阳当庭否认遭受酷刑并“真诚地”认罪、悔罪的荒诞场景，不由得一声叹息。</p><p>比起解决问题，这个国家似乎总是更倾向于解决提出问题的人。</p><p>戳穿皇帝新衣的徐晓东名声已臭，大师们将会重回武林谈笑风生，仿若一切都没有发生过。而中国传武界油水继续捞，再烂二十年。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;前些天某 MMA（Mixed Martial Arts，综合格斗）狂人徐晓东 20 秒打倒雷公太极创始人「雷雷」的事在中国互联网上传得火热，在大家唇枪舌剑讨论传统武术时，徐晓东又爆出许多武林界造假行为，并对各大传统武术门派下了战书。&lt;/p&gt;
&lt;p&gt;如果只是击败了雷雷一人（这家伙曾被央视介绍为「十大民间武林高手」），那也倒是还好。其他譬如陈氏太极都跳出来声明其无法代表太极门派所以不作数，雷雷本人也表示自己在这场对决中&lt;a href=&quot;http://qiwen.lu/33959.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;没有用内力&lt;/a&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;
    
    </summary>
    
      <category term="随笔" scheme="https://blessing.studio/categories/essay/"/>
    
    
      <category term="政治" scheme="https://blessing.studio/tag/%E6%94%BF%E6%B2%BB/"/>
    
  </entry>
  
  <entry>
    <title>《黑客与画家》读书笔记</title>
    <link href="https://blessing.studio/hackers-and-painters-reading-note/"/>
    <id>https://blessing.studio/hackers-and-painters-reading-note/</id>
    <published>2017-04-29T10:37:26.000Z</published>
    <updated>2017-05-06T12:26:58.000Z</updated>
    
    <content type="html"><![CDATA[<p>利用零散时间读完了《黑客与画家：硅谷创业之父 Paul Graham 文集》这本书，收获颇多。不过说是读书笔记，其实只是把一些觉得好的段落记录一下而已。这里不得不吐槽一下 Kindle 自带的标注功能：</p><ul><li>所有标注内容都以纯文本形式存储在一个 <code>My Clippings.txt</code> 文件里；</li><li>记录格式很神秘，得用特殊工具处理（<a href="http://www.clippings.io/" target="_blank" rel="noopener">Clippings</a>），比较蛋痛；</li><li>只是个本地功能，不会自动同步到云端；</li><li>无法在 Kindle 上舒爽地查看所有剪贴内容；</li><li>就算你在书中把原标注删除了也还会留一份在 <code>My Clippings.txt</code> 文件里；</li><li>最近 Kindle 固件升级，段落标注体验更屎了……</li></ul><p>不过阅读体验还是很不错的，所以原谅你。</p><p><img src="https://img.blessing.studio/images/2017/04/29/20170429183338.png" alt="Amazon"></p><p><em>▲中亚上 Kindle 书籍大多数都很便宜</em></p><a id="more"></a><hr><p>1.</p><p>在一个人产生良知之前，折磨就是一种娱乐。</p><p>2.</p><p>在任何社会等级制度中，那些对自己没自信的人就会通过虐待他们眼中的下等人来突显自己的身份。我已经意识到，正是因为这个原因，在美国社会中底层白人是对待黑人最残酷的群体。</p><p>3.</p><p>没有什么比一个共同的敌人更能使得人们团结起来了。</p><p>4.</p><p>你把整个程序想清楚的时间点，应该是在编写代码的同时，而不是在编写代码之前，这与作家、画家和建筑师的做法完全一样。</p><p>5.</p><p>看到代码前面的缩进乱七八糟，或者看到丑陋的变量名，都会把我逼疯的。</p><p>6.</p><p>黑客就像画家，工作起来是有心理周期的。有时候，你有了一个令人兴奋的新项目，你会愿意为它一天工作16个小时。等过了这一阵，你又会觉得百无聊赖，对所有事情都提不起兴趣。</p><p>7.</p><p>普通黑客与优秀黑客的所有区别之中，会不会「换位思考」可能是最重要的单个因素。有些黑客很聪明，但是完全以自我为中心，根本不会设身处地为用户考虑。这样的人很难设计出优秀软件，因为他们不从用户的角度看待问题。</p><p>判断一个人是否具备「换位思考」的能力有一个好方法，那就是看他怎样向没有技术背景的人解释技术问题。</p><p>8.</p><p>为了写出优秀软件，你必须假定用户对你的软件基本上一无所知。</p><p>软件的使用方式最好能符合用户的直觉，别指望用户去读使用手册。</p><p>9.</p><p>程序写出来是给人看的，附带能在机器上运行。</p><p>10.</p><p>类似的思维机制存在于每个人的头脑中，很多看似叛逆的「异端邪说」，早就「潜伏」在我们的思维深处。如果我们暂时关闭自我审查意识，它们就会第一个浮现出来。</p><p>11.</p><p>人们喜欢讨论的许多问题实际上都是很复杂的，马上说出你的想法对你并没有什么好处。</p><p>12.</p><p>举例来说，「政治正确」（political correctness）就是一个「元标签」，是许多特定现象的总称。这个词现在被广泛使用，其实这恰恰意味着「政治正确」的时代正在开始消亡，因为它使得你可以从总体上攻击这个现象，而不会受到指控，不会被说成支持某一种特定的“政治不正确”现象。</p><p>13.</p><p>不过，想要摆脱你自己的时代的流行，需要一点自觉。没有了时间所产生的距离，你不得不自己创造距离。你不要让自己成为人群的一分子，而要尽可能地远离人群，观察正在发生的事情，特别注意那些被压制的思想观点。</p><p>14.</p><p>一个人们拥有言论自由和行动自由的社会，往往最有可能采纳最优方案，而不是采纳最有权势的人提出的方案。专制国家会变成腐败国家，腐败国家会变成贫穷国家，贫穷国家会变成弱小国家。</p><p>极权主义制度只要形成了，就很难废除。</p><p>黑客对于公民自由是非常敏感的，因为这对他们至关重要。他们远远地就能感到极权主义的威胁，好比动物能够感知即将来临的暴风雨。</p><p>15.</p><p>高级使用者对 bug 的容忍度比较高，尤其如果这些 bug 是在开发新功能的过程中引入的，而这些新功能又正是他们所需要的，他们就更能理解了。</p><p>16.</p><p>不管你的软件定价多少，有些用户永远都不会购买。如果这样的用户使用盗版，你并没有任何损失。事实上，你反而赚到了，因为你的软件现在多了一个用户，市场影响力就更大了一些，而这个用户可能毕业以后就会出钱购买你的软件。</p><p>17.</p><p>但是在现实中，财富是用工作成果衡量的，而不是用它花费的成本衡量的。如果我用牙刷油漆房屋，屋主也不会付给我额外工资的。</p><p>18.</p><p>好设计是看似容易的设计。优秀运动员比赛时，让人觉得他轻轻松松就获胜了，优秀设计师也是如此，他们的工作看上去很容易。大多数时候，这是一种错觉。作家的文章读起来流畅自如，但是背后其实经过了反复修改。</p><p>19.</p><p>有意思的是，劫持飞机与「缓冲区溢出攻击」有类似之处。在一般飞机上，乘客区与驾驶舱是相通的，就好像C语言中数据区与代码区是相邻的一样。劫机者一旦进入驾驶舱，实际上就相当于把自己从数据提升为代码。</p><p>20.</p><p>黑客欣赏的一个特点就是简洁。黑客都是懒人，他们同数学家和现代主义建筑师一样，痛恨任何冗余的东西或事情。</p><p>21.</p><p>我认为，语言设计者应该假定他们的目标用户是一个天才，会做出各种他们无法预知的举动，而不是假定目标用户是一个笨手笨脚的傻瓜，需要别人的保护才不会伤到自己。如果用户真的是傻瓜，不管你怎么保护他，他还是会搬起石头砸自己的脚。</p><p>22.</p><p>如果目标用户群体涵盖了设计师本人，那么最有可能诞生优秀设计。如果目标用户与你本人差别很大，你往往会假定目标用户的需求比你本人的需求更简单，而不是更复杂。低估用户（即使出于善意）一般来说总是会让设计师出错。如果你觉得自己在为傻瓜设计产品，那么很可能不仅无法设计出优秀产品，而且就连傻瓜也不喜欢你的设计。</p><p>（完）</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;利用零散时间读完了《黑客与画家：硅谷创业之父 Paul Graham 文集》这本书，收获颇多。不过说是读书笔记，其实只是把一些觉得好的段落记录一下而已。这里不得不吐槽一下 Kindle 自带的标注功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有标注内容都以纯文本形式存储在一个 &lt;code&gt;My Clippings.txt&lt;/code&gt; 文件里；&lt;/li&gt;
&lt;li&gt;记录格式很神秘，得用特殊工具处理（&lt;a href=&quot;http://www.clippings.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Clippings&lt;/a&gt;），比较蛋痛；&lt;/li&gt;
&lt;li&gt;只是个本地功能，不会自动同步到云端；&lt;/li&gt;
&lt;li&gt;无法在 Kindle 上舒爽地查看所有剪贴内容；&lt;/li&gt;
&lt;li&gt;就算你在书中把原标注删除了也还会留一份在 &lt;code&gt;My Clippings.txt&lt;/code&gt; 文件里；&lt;/li&gt;
&lt;li&gt;最近 Kindle 固件升级，段落标注体验更屎了……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不过阅读体验还是很不错的，所以原谅你。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.blessing.studio/images/2017/04/29/20170429183338.png&quot; alt=&quot;Amazon&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;▲中亚上 Kindle 书籍大多数都很便宜&lt;/em&gt;&lt;/p&gt;
    
    </summary>
    
      <category term="随笔" scheme="https://blessing.studio/categories/essay/"/>
    
    
      <category term="读书笔记" scheme="https://blessing.studio/tag/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"/>
    
  </entry>
  
  <entry>
    <title>Long Live RSS：有感于微广场之死</title>
    <link href="https://blessing.studio/long-live-rss/"/>
    <id>https://blessing.studio/long-live-rss/</id>
    <published>2017-04-29T07:49:50.000Z</published>
    <updated>2017-04-29T08:16:00.000Z</updated>
    
    <content type="html"><![CDATA[<p>今天打开 InoReader，一眼便看到「微广场」关闭服务的通知。虽然我知道这类提供微信公众号 RSS 输出的服务总有一天是会消亡的，但没想到竟是如此之快。极尽惋惜之余，不由得一声叹息。</p><p><img src="https://img.blessing.studio/images/2017/04/29/iwgc_closed.png" alt="微广场关站通知"></p><p>我个人是不愿意去耗费精力折腾微信公众号的抓取的，毕竟能用钱解决的事，为什么要浪费时间呢？因此我也非常愿意为这类服务付费。既然微广场已经倒下，那我也不得不寻找其他对应方案了，目前看起来<a href="http://www.jintiankansha.me/" target="_blank" rel="noopener">「今天看啥」</a>这个服务还不错。</p><hr><p>这是一个信息爆炸的时代。</p><p>而我们却被大量的垃圾信息绑架着。</p><a id="more"></a><p>为了知道最近发生了些什么，我们不仅需要每天访问各种网站打开各种 APP 查看新闻，眼睛还经常被「震惊！○○竟然做出这种事！」「感人！男人看了沉默、女人看了流泪！」「关于○○，十条你不得不知道的秘诀」等标题党新闻污染。</p><p>在这个资讯过剩、信息垃圾横流的时代，意识到「我被信息绑架了」的聪明人们，开始设想，如果能够「让我要的信息主动来找我」那该有多好。</p><p>于是 RSS（Really Simple Syndication，简易信息聚合）协议应运而生……</p><p>好吧我是在扯淡。毕竟 RSS 协议在 1999 年最初也不是为了解决信息过剩问题而提出的，而且当时互联网上也不像现在这样充满了垃圾信息 :P</p><p>虽然 RSS 不是什么新鲜玩意儿，但其所秉承的核心理念「信息聚合，动态更新」永远不会过时 —— <strong>在信息越是过剩的时代，它的意义就越加彰显</strong>。</p><p>但是，这也是一个 RSS 式微的时代。</p><p>自从 Google Reader 关闭服务后，RSS 就变得小众了。或者说，变得更小众了。而且在各大内容商纷纷推广各种各样的平台时，我们获取信息的渠道也越来越多样化。不仅是今日头条等靠资讯内容起家的产品，就连百度等搜索引擎、甚至是支付宝（你丫好好做个支付应用不行吗？）都添加了资讯入口。</p><p>而占据网民中大多数的小白用户们也被这类平台分流。总有人喜欢点击震惊部标题党之流，商业公司能够在这些垃圾新闻上获取广告收入，自然也乐得投放更多垃圾新闻。</p><p>但是市场上总是有着一群对高端阅读有独到需求的人群，他们无法忍受封闭且阅读体验不佳的微信公众号订阅，更无法忍受今日头条等被下半身支配的 APP，也不愿意每天花大把时间往返于各大资讯平台。自然而然地，这些用户就会去寻找更高级的阅读解决方案。</p><p>也正是这些人构成了现在 RSS 的主流用户。</p><p>从产品形态而言，RSS 是一门对新手极其不友好的协议，它不适合暴露给前端用户，用户需要知道的应该只是「订阅」这个操作。所以集成了这种理念，并把复杂的协议隐藏到后端的产品（e.g. <a href="https://www.ruguoapp.com/" target="_blank" rel="noopener">即刻</a>），都活了下来，而且活得很好。</p><p>但从另一个角度而言，正因为 RSS 协议的这种「不将就」，它有自己非常独特的魅力：不受任何第三方绑架、各家阅读器专注于提升自家的阅读体验、等等。</p><p>诚然，在大众市场上，RSS 早已不再流行；但从细分市场来看，还是有那么一群铁杆粉丝在用着，并且在尝试着探索更多的可能性。</p><p>虽然很多阅读器（如 Google Reader）倒下了，很多提供 RSS 服务的站点（之前的 WeiRSS、现在的微广场）也倒下了，各大内容商的平台也趋于封闭（微信公众号、简书、知乎专栏）……但只要人们对高端阅读的诉求还在，对 RSS 的需求还在，<strong>RSS 就不会消亡</strong>。</p><p><img src="https://img.blessing.studio/images/2017/04/29/team_chat.png" alt="IRC"></p><p><em>▲来自 <a href="https://xkcd.com/1782/" target="_blank" rel="noopener">xkcd.com</a> 的漫画</em></p><p>而且，与那些封闭的平台不一样，RSS 是一个开放的协议。开放就意味着任何人都能够参与完善这个协议，没有版权，没有垄断，任何人都能够为 RSS 续命。就像 Email 一样，RSS 没有替代品。</p><p>而且我坚信，【自由开放】才是未来 Web 世界的主流。</p><p style="    font-size: 3.5em;    font-weight: bold;    text-align: center;    font-family: 'Alegreya SC', 'PT Serif', Georgia,  serif;">Long Live RSS.</p><hr><p>题外话，虽然 RSS 是一种对用户友好，对网站不怎么友好（用户直接在 RSS 阅读器中看完内容的话，网站少了流量，广告收入自然也会减少）的协议，但是在这个信息过剩的时代，<strong>支持 RSS 是一种美德</strong> ;)</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;今天打开 InoReader，一眼便看到「微广场」关闭服务的通知。虽然我知道这类提供微信公众号 RSS 输出的服务总有一天是会消亡的，但没想到竟是如此之快。极尽惋惜之余，不由得一声叹息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.blessing.studio/images/2017/04/29/iwgc_closed.png&quot; alt=&quot;微广场关站通知&quot;&gt;&lt;/p&gt;
&lt;p&gt;我个人是不愿意去耗费精力折腾微信公众号的抓取的，毕竟能用钱解决的事，为什么要浪费时间呢？因此我也非常愿意为这类服务付费。既然微广场已经倒下，那我也不得不寻找其他对应方案了，目前看起来&lt;a href=&quot;http://www.jintiankansha.me/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;「今天看啥」&lt;/a&gt;这个服务还不错。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;这是一个信息爆炸的时代。&lt;/p&gt;
&lt;p&gt;而我们却被大量的垃圾信息绑架着。&lt;/p&gt;
    
    </summary>
    
      <category term="随笔" scheme="https://blessing.studio/categories/essay/"/>
    
    
      <category term="RSS" scheme="https://blessing.studio/tag/RSS/"/>
    
  </entry>
  
</feed>
