<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Rei</title>
    <description>Rei 的个人网站</description>
    <link>https://chloerei.com</link>
    <atom:link href="https://chloerei.com/feed.xml" rel="self" type="application/rss+xml" />
    
      <item>
        <title>Stand with Hong Kong, but which side?</title>
        <description>&lt;h2 id=&quot;foreword&quot;&gt;Foreword&lt;/h2&gt;

&lt;p&gt;This article was first published in reddit hearthstone community, but after a day, the article has been downvote to invisible.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.reddit.com/r/hearthstone/comments/dfwkka/stand_with_hong_kong_but_which_side/&quot;&gt;https://www.reddit.com/r/hearthstone/comments/dfwkka/stand_with_hong_kong_but_which_side/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The cause of the incident was that Blizzard banned a player who promoted political slogans during the game.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://playhearthstone.com/en-us/blog/23179289/&quot;&gt;https://playhearthstone.com/en-us/blog/23179289/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My original intention was to provide different information for reddit, and I hope that the community will not be overly emotional. But soon I was labeled as “brainwashed”, “political propaganda” and so on.&lt;/p&gt;

&lt;p&gt;The communication is failure, but it shows a biased group, how to eliminate dissent, repeat their lies and then strengthen their bias.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Hi, Hearthstone players.&lt;/p&gt;

&lt;p&gt;I’m very surprised that the Hearthstone community has been very keen on discussing Hong Kong in recent days. But when I see “Stand with Hong Kong”, I’m confused, which side is standing on? It is on the side of the people who are destroying Hong Kong, or on the side of the people who are building Hong Kong.&lt;/p&gt;

&lt;p&gt;I know that you have a passionate heart and want to speak justice for this world, but I am worried that you have stood on the wrong side because you don’t understand the actual situation.&lt;/p&gt;

&lt;p&gt;Position statement: I’m Chinese living in Shenzhen, a city close to Hong Kong. I watched Hong Kong TV from an early age, so when I travel to Hong Kong, I am not unfamiliar. Hong Kong is more free than the mainland, has a more complete legal system. But due to violent demonstrations, these advances have been destroyed in the past few months. Now I dare not go to Hong Kong because the demonstrators are hunting different dissidents.&lt;/p&gt;

&lt;p&gt;To prove my point, let me show some violent demonstrations. I try not to quote bloody scenes because it may make you uncomfortable. But because of the violent nature of the demonstration, I can’t avoid it. This is not a complete event record, I just pick out events that may be selectively ignored by the media.&lt;/p&gt;

&lt;h2 id=&quot;2019-06-12-attacking-the-legislative-council&quot;&gt;2019-06-12 Attacking the Legislative Council&lt;/h2&gt;
&lt;p&gt;Outside the Legislative Council, the police were on the line of defense, and the demonstrators continued to approach and throw debris, iron pipes and even bricks.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2019-10-11-stand-with-hong-kong-but-which-side/01.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Video: &lt;a href=&quot;https://www.youtube.com/watch?v=FwbtLs8Toqw&quot;&gt;https://www.youtube.com/watch?v=FwbtLs8Toqw&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the evening, the demonstrators beat the police and the police were bloody.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2019-10-11-stand-with-hong-kong-but-which-side/02.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Video: &lt;a href=&quot;https://www.youtube.com/watch?v=2f3kur1DGeA&quot;&gt;https://www.youtube.com/watch?v=2f3kur1DGeA&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You may have heard another story: the Hong Kong police unscrupulously beat the demonstrators and released tear gas. I am not saying that all demonstrators are violent, but I saw that the demonstration was accompanied by violence almost from the beginning.&lt;/p&gt;

&lt;h2 id=&quot;2019-07-01-destroy-the-legislative-council&quot;&gt;2019-07-01 Destroy the Legislative Council&lt;/h2&gt;
&lt;p&gt;After several attacks, the demonstrators finally entered the Legislative Council.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2019-10-11-stand-with-hong-kong-but-which-side/03.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Video: &lt;a href=&quot;https://www.youtube.com/watch?v=JMCHTjsdMr0&quot;&gt;https://www.youtube.com/watch?v=JMCHTjsdMr0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After they entered the Legislative Council, they destroy, graffiti and steal some government equipment. The demonstrators withdrew from the Legislative Council at midnight because of fear of being arrested. The next day, the demonstrators condemned the police for not protecting the Legislative Council. In fact, the police guarded the whole day and finally retreated in order to avoid serious conflicts.&lt;/p&gt;

&lt;h2 id=&quot;2019-07-21-hong-kong-dark-night&quot;&gt;2019-07-21 Hong Kong Dark Night&lt;/h2&gt;

&lt;p&gt;There have been many serious violent clashes this night, and violence has spread from targeting police to targeting other people.&lt;/p&gt;

&lt;p&gt;You may have seen reports of people wearing white clothes in Yuen Long beating people wearing black clothes, as far as I know, 28 white clothes have been arrested. But there are other things have happened.&lt;/p&gt;

&lt;p&gt;On the same night, in another place, the demonstrators blocked the road, beat a driver who was dissatisfied, and smashed the car：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2019-10-11-stand-with-hong-kong-but-which-side/04.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Video: &lt;a href=&quot;https://www.youtube.com/watch?v=jHgFbJoz8do&quot;&gt;https://www.youtube.com/watch?v=jHgFbJoz8do&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The driver just wants to go home. The demonstrators were angry and felt that the driver did not support his actions, so beat him.&lt;/p&gt;

&lt;p&gt;After this day, violence is getting out of control.&lt;/p&gt;

&lt;h2 id=&quot;2019-10-06&quot;&gt;2019-10-06&lt;/h2&gt;

&lt;p&gt;This is the most recent and most violent incident I quoted.&lt;/p&gt;

&lt;p&gt;A taxi driver was surrounded by demonstrators and attacked. Taxi was out of control and hit to the demonstrators. The demonstrators beat him to serious injuries.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2019-10-11-stand-with-hong-kong-but-which-side/05.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Video: &lt;a href=&quot;http://news.tvb.com/local/5d9b7a2de60383eb0f7b1fdb/&quot;&gt;http://news.tvb.com/local/5d9b7a2de60383eb0f7b1fdb/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The violence has continued and escalated, and Hong Kong citizens live in fear.&lt;/p&gt;

&lt;p&gt;There are still many similar incidents. Because this is not to record the entire event, I don’t want to list them here. Let me talk about what I really want to express.&lt;/p&gt;

&lt;h2 id=&quot;politics-is-very-complicated&quot;&gt;Politics is very complicated&lt;/h2&gt;
&lt;p&gt;In the past few months, there have been too many things in Hong Kong. You can’t simply judge right or wrong based on intuition. There are too many historical reasons for the problems that are happening in Hong Kong. I cannot explain them here. It may take time to come to a conclusion.&lt;/p&gt;

&lt;p&gt;No media is completely neutral. If you want to know the truth, you need to spend a lot of time identifying different sources of information. This is far from being as easy as calling out a free slogan.&lt;/p&gt;

&lt;p&gt;If you simply scream “I support the people of Hong Kong to fight”, you may be hurting people who want to live a peaceful life in Hong Kong.&lt;/p&gt;

&lt;h2 id=&quot;respect-is-mutual&quot;&gt;Respect is mutual&lt;/h2&gt;
&lt;p&gt;In this incident, I found that the Chinese and Western people have great cultural differences. The West often said that the Chinese do not understand freedom of speech, but I must point out that the West does not understand the Chinese people’s desire for peace and unity.&lt;/p&gt;

&lt;p&gt;China has experienced many disasters in modern times, finally have a peaceful period to develop the economy and improve people’s livelihood, Most people don’t want to destroy it.&lt;/p&gt;

&lt;p&gt;In essence, I don’t think there is much difference between people around the world. We want to be friendly, peace, and freedom. Freedom is one of the common needs of mankind, and I believe that society will become more and more free. However, different countries have different development processes and cannot be rushed, or they will cause turmoil and even war.&lt;/p&gt;

&lt;p&gt;When I saw a lot of people in reddit said to support Hong Kong to fight, I am very distressed. I am distressed for the people of Hong Kong because the riots will not end in a short time, Hong Kong people must continue to live in fear. And I also know that there are many Chinese Hearthstone players who feel distressed. Things should not be like this.&lt;/p&gt;

&lt;p&gt;I think Blizzard is doing right things. It is not because of its operation that people feel that its position is biased toward China, but that it faithfully follows the rules. If someone promotes a political position that favors China in the game, I also support Blizzard to ban him. The game is not a platform to promote political positions.&lt;/p&gt;

&lt;p&gt;We live in different places and meet through games. We need to be more inclusive of each other’s culture. I hope that the Hearthstone community can restore peace. Thank you for reading here. My English is not good, it took an afternoon to write this article.&lt;/p&gt;

&lt;p&gt;I stand with Hong Kong, standing on the side of peace.&lt;/p&gt;
</description>
        <pubDate>Fri, 11 Oct 2019 00:00:00 +0000</pubDate>
        <link>https://chloerei.com/2019/10/11/stand-with-hong-kong-but-which-side/</link>
        <guid isPermaLink="true">https://chloerei.com/2019/10/11/stand-with-hong-kong-but-which-side/</guid>
      </item>
    
      <item>
        <title>香港的支付方式</title>
        <description>&lt;p&gt;上周我终于达成到香港旅游的目标，在香港期间对不少事情留下深刻印象，例如住宿环境、消费水平、广告牌丛林、迪士尼……只是三天时间比较少，还不足够深入了解香港，好在我住在深圳，计划多去几次把香港逛个遍。这里特别想记录的，是香港的支付方式。&lt;/p&gt;

&lt;p&gt;在口岸等待我同行的朋友的时候，朋友发来消息问我：换好港币了没，不少地方需要现金。我一愣，虽然可以预料支付宝微信支付不会是主流，但我还带有 Visa 卡，手机有 Apple Pay，还不足够实现无现金支付吗？&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2018-08-11-payment-method-in-hong-kong/subway.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;随后的三天行程发现，在香港游玩确实不能脱离现金。我们行程主要的支付场景有：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;地铁买票：只看到两个站台有微信支付宝购票机，大部分是现金支付/充值（单程票/八达通）。&lt;/li&gt;
  &lt;li&gt;便利店（7-11）：支持所有支付方式，支付宝、微信、银行卡、八达通、现金……所以我买饮料都在 7-11 买。&lt;/li&gt;
  &lt;li&gt;商场/餐厅：支持银行卡、现金。&lt;/li&gt;
  &lt;li&gt;网红美食店：基本只支持现金。&lt;/li&gt;
  &lt;li&gt;住宿/门票：国内代购网站事先网上付费。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;总的来说，越高档的场合支付方式越多，只逛商场的人可以不用担心没带现金；大陆游客多的商店通常会支持支付宝微信，但也不一定；小店、路边摊基本只支持现金，想要逛遍香港每个角落的人就一定要带现金了。&lt;/p&gt;

&lt;p&gt;而我们游览过程也体验到了现金的不便利，经常要开钱包找散钱，收到散钱太多弄得钱包鼓鼓的，收到不少硬币差点要买个硬币包。&lt;/p&gt;

&lt;p&gt;我对此感到一些困惑，诚然我已经被支付宝微信惯坏了，在深圳生活近两年没有使用现金，以致我对香港的期望更高，因为香港是个国际化、自由竞争的大都市，我希望看到比支付宝微信更好的方案——其实我也不是很喜欢生活方方面面被支付宝微信统治。但实际上香港的支付方式大概停留在内地一线城市5年前的状态，移动支付还在萌芽阶段。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2018-08-11-payment-method-in-hong-kong/store.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;我在网上搜索到了这篇文章《&lt;a href=&quot;https://www.huxiu.com/article/230125.html&quot;&gt;为什么香港人爱用现金？&lt;/a&gt;》，里面主要说了两点：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;本地商家和消费者太习惯现金，不信任移动支付。&lt;/li&gt;
  &lt;li&gt;本地支付方式太多，没有哪家可以一家独大。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这两点在内地早前也是一样的，通过支付宝微信的推广最终改变了人们的观念，我觉得香港迟早也会拥抱移动支付。&lt;/p&gt;

&lt;p&gt;而在这篇文章《&lt;a href=&quot;https://www.bbc.com/zhongwen/simp/chinese-news-41591194&quot;&gt;你问我答：说香港的“无现金”发展落后？是误会&lt;/a&gt;》中提到了另一种观点：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;至于会否被政府监控的问题，李兆波指出，国际公司的数据收集做法，相对较透明，用户可清楚了解，但对中国大陆的公司，很多港人都会有质疑，觉得“阿爷一定知”（中央一定会知道）。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;“数据是否会向政府开放？这是很多人担心的。”李兆波说：“虽然内地也有私隐保障的法例，但最终，还是对内地的法制有没有信心的问题。”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;无需多言，我完全理解这种担忧，这也是为什么我期望到香港能看到更好的方案。但现实来看，香港并没有更好的答案，还在问题边缘小心试探。那么我认为，如果没有更好的方案，香港迟早会接受支付宝微信的移动支付，因为支付宝微信相比香港现有的支付方式有明显的优势：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;不用带钱包：没体验过的时候可能会觉得身上带个钱包没什么问题，但一旦体验过就会觉得多带一张卡都是麻烦。&lt;/li&gt;
  &lt;li&gt;点对点支付：聚会 AA 制不用掏钱找钱，也不用问银行账号。&lt;/li&gt;
  &lt;li&gt;收款门槛极低：路边小贩只要打印一个二维码即可收款，不需要购买特殊硬件，个人手机就可以立即查看流水。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;曾经我对支付宝微信非常抗拒，我生活在大陆，对于这两家公司的认识比香港人更有发言权，但最终还是败在了“便利”上面。懒就是社会发展的一大动力，现金最终也会跟音乐、电影、书籍一样电子化的。这个过程会挣扎，但不会倒退。&lt;/p&gt;

&lt;p&gt;考虑到隐私问题，在支付宝微信以外的方案其实不多，我觉得就只有 Apple Pay 和 Android Pay，因为他们有较高的装机率，但现时这两个 Pay 还要解决三个问题才能跟支付宝微信抗衡：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;支付场景急需扩充，目前基本是银行卡的子集。&lt;/li&gt;
  &lt;li&gt;提供点对点支付。其实已经在推进，进程需要加快。(&lt;a href=&quot;https://support.apple.com/en-us/HT207886&quot;&gt;Apple Pay&lt;/a&gt;，&lt;a href=&quot;https://support.google.com/pay/send/answer/6285509&quot;&gt;Android Pay&lt;/a&gt;)&lt;/li&gt;
  &lt;li&gt;给路边小贩提供极低门槛的收款成本。路边小贩不会接入收款设备，成本不能超过一张打印纸。如果普及不到小贩的层面，那么是无法跟支付宝微信抗衡的。支付宝微信可以同时向高低端扩展，基于收款设备的只能向高端扩展。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;说到这里，庆幸我住在深圳，可以方便观察到两个城市、两种体制的发展，希望香港能找到属于自己的移动支付之路。香港明天会更好。&lt;/p&gt;
</description>
        <pubDate>Sat, 11 Aug 2018 00:00:00 +0000</pubDate>
        <link>https://chloerei.com/2018/08/11/payment-method-in-hong-kong/</link>
        <guid isPermaLink="true">https://chloerei.com/2018/08/11/payment-method-in-hong-kong/</guid>
      </item>
    
      <item>
        <title>容器也许是未来，但不是当下</title>
        <description>&lt;p&gt;自从 Docker 发布以来，我已经看到很多次“容器是未来”。随着了解越来越深入，我的态度也从怀疑转到肯定。容器确实有很多优点，包括但不限于：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;容器可以让运行环境用文件的形式固化，并提交到代码库。&lt;/li&gt;
  &lt;li&gt;容器可以确保开发环境和生产环境一致，而不是到部署的时候重新搭建一套类似的环境。&lt;/li&gt;
  &lt;li&gt;容器可以秒级扩展，而虚拟机创建通常需要几分钟。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;有这个几个优势，即使不考虑追踪、日志、服务治理等平台化的功能，我也相信容器会是未来。不过，尽管我已经在开发环境使用 Docker 两年多了，却一直没有把部署环境切换到 Docker，原因是部署平台不完善。&lt;/p&gt;

&lt;p&gt;可能有人会提起 kubernetes。kubernetes 已经在编排领域胜出，各大云服务商都已经推出或者预览托管 kubernetes 集群功能。看来 kubernetes 已经为生产环境准备好了？&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2018-07-08-container-may-be-the-future-but-not-the-present/flower.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;最近我尝试将一个小应用部署到 Google kubernetes Engine 上，但过程只能用痛苦来形容。概括来说需要做这些事情：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;创建 kubernetes 集群，选择需要的虚拟机配置和数量。&lt;/li&gt;
  &lt;li&gt;学习 CloudSQL 的 Proxy 验证。（Google Cloud 特有）&lt;/li&gt;
  &lt;li&gt;编写 Deployment，Service 和 Ingress 配置。（需要学习一大堆 kubernetes 知识）&lt;/li&gt;
  &lt;li&gt;将 Docker image push 到合适的仓库。&lt;/li&gt;
  &lt;li&gt;执行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl apply&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这个小项目的配置开源在 https://github.com/getcampo/campo-gcloud ，但不指望没有 kubernetes 基础的人能看懂。还有一些想做但还没做的事情：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;自动领取 SSL 证书（Let’s Encrypt）。&lt;/li&gt;
  &lt;li&gt;CI/CD 集成。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;但我已经放弃再继续摆弄下去，因为发现：1）kubernetes &lt;strong&gt;完全没有减少部署的工作量&lt;/strong&gt;，说不定更多了。2）&lt;strong&gt;太多配置和操作是由人来完成&lt;/strong&gt;，出错几率加大。我对 kubernetes 的了解还不够深，不确定遇到问题的时候是否能快速解决。曾经就因为一个配置写了布尔值而 kubernetes 只认字符串而报错，让我调试了一小时。&lt;/p&gt;

&lt;p&gt;kubernetes 是不是太难了？每个技术人员都不会轻易开口说这句话，因为这会显得自己不够聪明。不过有篇文章说明这不是我一个人这么想：&lt;a href=&quot;https://www.influxdata.com/blog/will-kubernetes-collapse-under-the-weight-of-its-complexity/&quot;&gt;Will Kubernetes Collapse Under the Weight of Its Complexity?&lt;/a&gt;。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;However, I felt there was an underlying problem with the whole spectacle: everyone I talked to was either an operator or an SRE. Where were all the application developers? Aren’t those the people that all this complex infrastructure is supposed to serve? Is this community really connected with the needs of its users? And it made me wonder: is Kubernetes too complex? Will it end up collapsing under the weight of its own complexity? Will it fade away as OpenStack has seemed to since 2014?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;看了这个 Cloud Native 大会的景观图，我倒觉得前端的混乱状况有点可爱了。到底需要学习多少工具，添加多少抽象层才能把容器用起来？&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2018-07-08-container-may-be-the-future-but-not-the-present/cloud-native-landscape.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;不过让人欣慰的是，至少有一个厂商作出了开发者需要的产品：Heroku。&lt;/p&gt;

&lt;p&gt;Heroku 是 2007 年推出的云平台服务，可能很多人对 Heroku 的印象还停留在它的 buildpack 机制上：通过 Git 把代码 push 到 heroku repo，heroku 执行 buildpack 把应用构建部署到它的平台上。在 Docker 兴起后，一度有人认为 Heroku 要完了，因为围绕 Docker 完全有可能搭建一套开源的 Heroku 平台。相比之下，buildpack 只是 Heroku 自家的技术，人们不想被绑定在一个平台上。&lt;/p&gt;

&lt;p&gt;如果近几年你没有使用 Heroku，那么你可能需要更新认识，因为 Heroku 已经完全拥抱 Docker。在 2015 年，Heroku 已经实现 &lt;a href=&quot;https://blog.heroku.com/introducing_heroku_docker_release_build_deploy_heroku_apps_with_docker&quot;&gt;Docker Deploy&lt;/a&gt;，而不需要 buildpack。到了 2017 年底，Heroku 又推出了 &lt;a href=&quot;https://devcenter.heroku.com/changelog-items/1332&quot;&gt;heroku.yml build manifest&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;heroku.yml 是啥？看例子最直观：&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;docker&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Dockerfile.web&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Dockerfile.worker&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;release&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;bin/rails db:migrate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这几行配置已经实现了前面说的&lt;a href=&quot;https://github.com/getcampo/campo-gcloud&quot;&gt;一大坨 kubernetes 的配置&lt;/a&gt;。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heroku.yml&lt;/code&gt; 配置很简洁，没什么学习成本。开发人员不用学习 Deployment、Service、Ingress 还有其它一大堆概念，只要告诉 Heroku 需要什么进程，该进程的镜像通过什么 Dockerfile 构建，Heroku 就自动把工作做好。另外只要绑定 GitHub，就可以很容易地把 CI/CD 跑起来。Heroku 背后完成了很多工作，留给开发人员简洁的界面，这才是平台需要做的事情。&lt;/p&gt;

&lt;p&gt;当然，Heroku 也不完美：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;平台绑定的问题。（其实哪个云平台没有？）&lt;/li&gt;
  &lt;li&gt;只对企业客户开放美欧以外的区域，没有中国。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以，目前我也只能把业余项目放到 Heroku 上，并且羡慕用 Heroku 就能满足需求的地区……&lt;/p&gt;

&lt;p&gt;在容器混战时代，Heroku 像一股清流，在所有人找不清方向的时候给出了接近完美的答案。希望更多云平台商能向 Heroku 学习，让开发者不再受部署所困，放更多精力放在开发上。容器也许是未来，希望这个未来能早点到来。&lt;/p&gt;
</description>
        <pubDate>Sun, 08 Jul 2018 00:00:00 +0000</pubDate>
        <link>https://chloerei.com/2018/07/08/container-may-be-the-future-but-not-the-present/</link>
        <guid isPermaLink="true">https://chloerei.com/2018/07/08/container-may-be-the-future-but-not-the-present/</guid>
      </item>
    
      <item>
        <title>Stimulus 框架简介</title>
        <description>&lt;p&gt;2018 年年初，Basecamp（开源 Ruby on Rails 的公司）开源了 Stimulus 前端框架，为前端大战再添一员。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://stimulusjs.org/&quot;&gt;Stimulus&lt;/a&gt; 是一个轻量级前端框架，其用法用一张图就可以概括：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2018-02-24-stimulus/simulus.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这个框架通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-contoller&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-target&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-action&lt;/code&gt; 等属性，把 HTML 元素和 JavaScript 行为绑定。图例使用的是 ES6 的语法，这并不是必须的。&lt;/p&gt;

&lt;p&gt;由于&lt;a href=&quot;https://stimulusjs.org/handbook/introduction&quot;&gt;官方教程&lt;/a&gt;也只有六页，所以关于用法不做展开。&lt;/p&gt;

&lt;h2 id=&quot;stimulus-解决的问题&quot;&gt;Stimulus 解决的问题&lt;/h2&gt;

&lt;p&gt;和热门的 JavaScript 框架（例如 React、Vue）相比，Stimulus 本身不处理 HTML 渲染，而是为已渲染的 HTML 添加行为——大部分情况下，就是跟服务端渲染配合。&lt;/p&gt;

&lt;p&gt;Stimulus 是从 Basecamp 中抽取出来的，这意味着它被测试过和 Ruby on Rails 的其它前端组件相处融洽，这包括 Assets Pipeline、Turbolinks、SJR。同时，它补全了 Rails 前端方案缺失的一环，也就是 Turbolinks 环境下如何组织 JavaScript。&lt;/p&gt;

&lt;p&gt;如果你不知道 &lt;a href=&quot;https://github.com/turbolinks/turbolinks&quot;&gt;Turbolinks&lt;/a&gt;，它是一个可以&lt;a href=&quot;http://chloerei.com/rubyconfchina2016/#/4/5&quot;&gt;加速页面加载&lt;/a&gt;的前端库。但同时它让应用的前端变成类似单页应用，换页不会触发 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; 事件，需要重新考虑 JavaScript 的组织方式。&lt;/p&gt;

&lt;p&gt;举个例子，假设我们要为一个按钮绑定点击事件，在引入 jQuery 的时候可以这么写：&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;btn&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#btn&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;通常 JavaScript 会打包到单独的文件里，在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;head&amp;gt;&lt;/code&gt; 引入，执行的时候不能确保 HTML 已经解析完成，所以会使用 jQuery 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ready&lt;/code&gt; 方法确保逻辑在 HTML 载入完成后执行：&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ready&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#btn&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;btn&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;但是，在 Turbolinks 环境下，以上代码不能正常工作，因为 Turbolinks 换页不触发 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DOMContentLoaded&lt;/code&gt; 事件。一个解决方法是使用 Turbolinks 事件：&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;turbolinks:load&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#btn&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;btn&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;或者换个思路，绑定事件到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document&lt;/code&gt; 元素上：&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;#btn&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;btn&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;把这个模式推广到更普遍的场景：&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;click&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;[data-behavior~=&quot;btn&quot;]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;button&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;data-behavior=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;btn&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;上面这段代码看起来已经很像 Stimulus 了，实际上 Stimulus 就是脱胎自 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;data-*&lt;/code&gt; 事件绑定模式。没有 Stimulus，你也可以用这种方式整理 Turbolinks 环境下的前端代码；有了 Stimulus，Rails 就有了一套前端代码规范，而不用每个团队自己制定。&lt;/p&gt;

&lt;p&gt;另外，绑定 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document&lt;/code&gt; 事件有个潜在问题，随着组件逻辑增加，每次事件都要经过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document&lt;/code&gt; 的事件托管，这是不必要的消耗。&lt;/p&gt;

&lt;p&gt;Stimulus 使用了 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver&quot;&gt;MutationObserver&lt;/a&gt;，这是一个新的 DOM API，作用是监视 DOM 上的元素插入和删除，并提供钩子方法，这样就可以实现页面插入一个元素的时候绑定，删除一个元素的时候解绑。&lt;/p&gt;

&lt;p&gt;所以，用 Stimulus 实现的组件 HTML，既可以是服务端渲染，也可以是前端动态插入，跟 Turbolinks 和 SJR 完美结合。&lt;/p&gt;

&lt;h2 id=&quot;我需要-stimulus-吗&quot;&gt;我需要 Stimulus 吗？&lt;/h2&gt;

&lt;p&gt;评估 Stimulus 不能脱离 Rails 的整套前端方案：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Turbolinks + Stimulus + SJR + WebPacker(Vue, React...)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;可以看到，Stimulus 只是解决一部分问题，即非内容渲染（服务端渲染）、非 AJAX 请求（SJR）、非复杂交互（Vue, React）的前端逻辑。Rails 提供了一系列组件，让我们根据不同情况挑选不同的工具。&lt;/p&gt;

&lt;p&gt;如果你已经在使用 Turbolinks + SJR，那么可以马上使用 Stimulus，它会让前端代码更规范。&lt;/p&gt;

&lt;p&gt;如果前端需求非常复杂，需要处理大量客户端状态，那么应该求助于前端渲染框架（Vue，React）。&lt;/p&gt;

&lt;p&gt;如果你的团队已经使用前后端分离的方式开发、不需要 SEO、没有对&lt;a href=&quot;http://chloerei.com/2018/01/07/front-end-split/&quot;&gt;前后端分裂&lt;/a&gt;感到痛苦，那么就不需要 Stimulus。&lt;/p&gt;

&lt;h2 id=&quot;你的前端需求也许没那么复杂&quot;&gt;你的前端需求也许没那么复杂&lt;/h2&gt;

&lt;p&gt;什么样的前端需求是复杂？处于不同阶段的人会得到不同的答案。对于刚接触 HTML/CSS/JavaScript 的人来说，做一个点击弹出窗口就是复杂；而对于&lt;a href=&quot;https://github.com/chloerei/writings&quot;&gt;写过可视化编辑器&lt;/a&gt;的人来说，做一个网页游戏才是复杂。&lt;/p&gt;

&lt;p&gt;在我看来，现在大部分前端的复杂，来自工具产生的附属性复杂，而非需求的本质性复杂。人们在选择工具的时候，很容易陷入一个误区：如果选择能处理最复杂情况的工具，那么面对简单情况也能同样处理。这种想法忽视了如果选择了复杂的工具处理所有事情，那么再简单的事情也会变得很复杂。&lt;/p&gt;

&lt;p&gt;Rails 的前端方案是一个提供各种选择的方案，解决问题的方法并不只有一个。我们可以先用一个工具，不合适再换另一个工具。关键在于认识到没有银弹，简单的问题用简单的方法解决。&lt;/p&gt;

&lt;p&gt;如果你厌倦了臃肿的前端应用，痛恨它浪费掉的大量时间，那么是时候走一下 Rails 的道路。&lt;/p&gt;
</description>
        <pubDate>Sat, 24 Feb 2018 00:00:00 +0000</pubDate>
        <link>https://chloerei.com/2018/02/24/stimulus/</link>
        <guid isPermaLink="true">https://chloerei.com/2018/02/24/stimulus/</guid>
      </item>
    
      <item>
        <title>前后端分裂</title>
        <description>&lt;p&gt;让我先讲一个真实故事。&lt;/p&gt;

&lt;p&gt;有一天，我接到一个业务需求，给某个表单的下拉框增加一个选项。比较特别的是，这是一个前后端分离的项目，分离得非常彻底——维护前端的人甚至不跟我在同一个项目组，前端页面独立开发独立部署，我能够修改的只有 API，并且前端页面不会把内容直接提交到后端，而是经过一层 node.js 中转……&lt;/p&gt;

&lt;p&gt;我试图搞明白这个表单怎么工作，发现当初设计这个表单的人非常“聪明”，已经预料到下拉框内容要改动的情况了，所以这个下拉框内容是动态的，根据后端另一个 API 的输出来渲染。那么看起来我只要修改这个 API 的输出，前端就能自动更新下拉框内容。于是我找到前端同事确认是不是能这么改，得到答复是：&lt;/p&gt;

&lt;p&gt;“不行，前端这边加了缓存，你如果更改了 API 输出，需要通知我刷新缓存。”&lt;/p&gt;

&lt;p&gt;好吧，为了性能考虑，加缓存是非常正确的选择。那么接下来我想先在测试环境修改联调一下，不要影响生产环境。这时候我才发现，测试环境是坏的！于是我又开始修测试环境。联调过程有点像乒乓球，我改一点东西，让前端刷新，然后发个请求测一下，如果有问题，再重复这个流程。&lt;/p&gt;

&lt;p&gt;猜猜我完成这个需求花了多少时间？前前后后一共花了……3 天！这里面还有一个原因是前端同事并不是 100% 投入这个项目，有很多时间我都在等待他空出时间处理，而等待时间我也可以处理别的问题。但是，WTF 我居然改一个下拉框用了三天时间，这正常么？&lt;/p&gt;

&lt;p&gt;从这故事可以得到的教训有：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;不要把核心业务功能交给项目外的人负责。&lt;/li&gt;
  &lt;li&gt;不正确的分离会大大降低开发效率。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这篇博客主要想讲第二点。&lt;/p&gt;

&lt;h2 id=&quot;分离狂热&quot;&gt;分离狂热&lt;/h2&gt;

&lt;p&gt;现在对前后端分离的追捧已经到了狂热的程度。虽然“现代前端”拥护者会告诉你，什么样的场景用什么样的工具，前端框架是为复杂场景考虑的，并且给你展示一个复杂的、很适合用前端框架的场景。但是当你要编写一个简单应用的时候，这些拥护者这又会告诉你最好选用 Blabla 框架，因为这是市场“主流”，开发者众多，而你的应用总有一天会变得复杂的。&lt;/p&gt;

&lt;p&gt;如果只是一昧鼓吹什么是主流和现代还不至于有很多人上当，真正戳中技术管理者痛点的地方是分工。当我们要处理一个很庞大的问题，以至于一个人、一个团队不能掌控的时候，自然会想到将系统拆解为几个模块进行分工。但问题是，前后端分离分错了方向。&lt;/p&gt;

&lt;p&gt;人们很容易认为前后端是个不同的领域，因为一个在浏览器执行，一个在服务器执行，它们不是两个东西吗？&lt;/p&gt;

&lt;p&gt;这是不对的，前端和后端都属于 Web 应用的一部分，不能脱离对方存在，前端代码需要通过后端发送。对于大多数内容型网站，如官网、电商、视频等等，为了 SEO，需要后端渲染。为了性能，需要后端缓存。为了安全，需要通盘考虑浏览器的安全机制，包括 CSRF、CROS、CSP 等。前后端分离的项目，都会由前端维护一个 node.js 中间层，就是证明了这一点。如果拍脑袋前后端分离，不协调就会出现在开发、测试、部署，功能开发的整个生命周期。&lt;/p&gt;

&lt;p&gt;如果认为前后端分离可以增加并行投入的人力，从而加快开发进度，很遗憾，根据我的观察并不能达到这个目的。我曾经目睹一个功能用前后端分离的方案，轻易估算出了 150 人天的工作量，并且这又是合理的。前后端分离的项目一开始就要基于远程调用进行设计，而这是一般软件设计中极力避免的。开发过程中有很多逻辑是重复开发，前端实现一次，后端也实现一次，然后还要考虑远程调用的错误处理。最后，测试联调的时候也会出现大量问题。这个项目如果用传统集成式的方式开发，根据我的经验应该在 50 人天左右的工作量。&lt;/p&gt;

&lt;p&gt;最后，还有一种观点是认为开发者是可以轻易替代的，并且职责分得越细，越容易替代。所以前后端分离描绘出一种梦幻情景：前端和后端只处理一部分，任何成员离开都可以轻易补上。事实上，分离后的项目变得更难理解了，前端不知道后端的处理，后端不知道前端的实现。调用关系越简单，层级越少，才会越容易理解。&lt;/p&gt;

&lt;p&gt;正确的分割方法是按功能划分，一个人员负责某个功能的整个流程，前至用户如何填写表单，后至数据如何写入数据库和执行后台任务。如果单个项目过于巨大，就把一些业务独立的功能提取成项目。（很容易想到 Microservice，但这又做过头了，具体看 Martin Fowler 的 &lt;a href=&quot;https://martinfowler.com/bliki/MonolithFirst.html&quot;&gt;MonolithFirst&lt;/a&gt;，不在这里展开）&lt;/p&gt;

&lt;h2 id=&quot;前端怪圈&quot;&gt;前端怪圈&lt;/h2&gt;

&lt;p&gt;“现代”前端已经成功把自己逼迫到非常窘迫的地步：前端框架层出不穷，曾经大热的 Angular 已经边缘化；为了处理请求不得不引入 Node.js Server，但是很谨慎将自己称为全栈，而是叫做“中间层”，因为前不久才用“全栈就是坏”的口号带歪了不少全栈程序员；为了 SEO 不得不引入服务端渲染，但又不能走传统模版渲染的老路，发展出来同构——把前端框架放到后端渲染，开发难度指数上升。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2018-01-07-front-end-split/isomorphic-javascript.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;我已经听到有前端开发说不想碰 Node.js Server 了，他们想专注于处理交互（这也是前端职位的本意）。后端开发一开始很乐意把工作分出去，觉得工作量少了，实际开发才发现一点没少，以前一个变量传递就能完成的事情，现在要写一个 API。需求没完成，没有人会是轻松的。&lt;/p&gt;

&lt;p&gt;现在这个境地，与其说是前后端分离，不如说是前后端分裂。大家都觉得不对，但是沟壑已经很深。一旦分开，融合就不是容易的事。&lt;/p&gt;

&lt;h2 id=&quot;那怎么办&quot;&gt;那怎么办……&lt;/h2&gt;

&lt;p&gt;出路其实每个人都看见过，但有的人选择视而不见，因为这会让人怀疑这几年是不是浪费掉了。&lt;/p&gt;

&lt;p&gt;回想一下前后端分离被炒热之前的那个时代，我们是这样做 Web 应用：以服务端渲染为基础，加上少量 JavaScript 交互，对于特别复杂的组件使用前端框架，这就已经满足绝大部分 Web 应用的需求。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2018-01-07-front-end-split/part.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这个方案的问题是，不像前后端分离那样有浏览器/服务器这么明显的界限，没有一个简单标准判断哪一部分应该占多少比例，这取决于场景。另一个问题是，优秀的全栈工程师是稀缺品，培养起来需要时间。这就让人犯难了，一不小心就会掉入分离的陷阱。但是，能正确划分系统的人更是稀缺品……&lt;/p&gt;

&lt;p&gt;现在是全栈开发备受质疑的日子，分离方案占据了制高点。但是技术潮流就像钟摆，说不定过两年又会摆回来。不要一直追着热点，而是要看清自己的需求。&lt;/p&gt;

&lt;p&gt;软件开发没有银弹。&lt;/p&gt;
</description>
        <pubDate>Sun, 07 Jan 2018 00:00:00 +0000</pubDate>
        <link>https://chloerei.com/2018/01/07/front-end-split/</link>
        <guid isPermaLink="true">https://chloerei.com/2018/01/07/front-end-split/</guid>
      </item>
    
      <item>
        <title>会议视频录制指南</title>
        <description>&lt;p&gt;我们已经看过不少技术会议录像，像 &lt;a href=&quot;https://www.youtube.com/playlist?list=PLTUHmtFhYC6iEwu4h2REYhNKCHFFHYngS&quot;&gt;RubyConf China 2017 的录像&lt;/a&gt;：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-11-12-conference-video-recording-guide/preview.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这样一边演示一边人物的录像是怎么实现的呢？以下分享我的经验。&lt;/p&gt;

&lt;h2 id=&quot;需求&quot;&gt;需求&lt;/h2&gt;

&lt;p&gt;会议录像跟一般录像相比，有以下特点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;需要同步录制人物视频和演示文稿视频。&lt;/li&gt;
  &lt;li&gt;演讲者电脑是自备的，可能需要代码演示。&lt;/li&gt;
  &lt;li&gt;拍摄对象位置比较固定。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;方案&quot;&gt;方案&lt;/h2&gt;

&lt;p&gt;针对会议录像的特点，我摸索出以下录像方案：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-11-12-conference-video-recording-guide/intro.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;除了演讲者自备的电脑和场地提供的投影外，录制方需要准备的硬件有：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;录制电脑 x 1，由于要实时处理视频编码，所以性能需要较好，并且提供的接口会影响采集卡的选择。&lt;/li&gt;
  &lt;li&gt;HDMI 分配器 x 1，用于将演讲者电脑的视频输出一分为二，一边输出到投影，一边输出到录制电脑。&lt;/li&gt;
  &lt;li&gt;视频采集卡 x 2，用于将视频信号转换为电脑能识别的编码。&lt;/li&gt;
  &lt;li&gt;摄像机机 x 1，用于拍摄演讲者。&lt;/li&gt;
  &lt;li&gt;三脚架 x 1，需要是摄像三脚架。&lt;/li&gt;
  &lt;li&gt;足够长的 HDMI 线等等。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;应对不同规模的会议需要的硬件级别也许不一样，不过软件的选择相对单一。下面先讲软件的使用，然后推荐适合不同规模的会议的硬件。&lt;/p&gt;

&lt;h2 id=&quot;软件设置&quot;&gt;软件设置&lt;/h2&gt;

&lt;p&gt;目前我只用过一款录像软件：&lt;a href=&quot;https://obsproject.com/&quot;&gt;OBS（Open Broadcaster Software）&lt;/a&gt;，这是一款开源的录像/直播软件，支持 Windows、Mac 和 Linux 三个平台。&lt;/p&gt;

&lt;p&gt;安装好后，打开 OBS 可以看到这个界面：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-11-12-conference-video-recording-guide/obs.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;目前需要了解的功能有：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;中间黑色部分是预览区域，可以看到合成后的视频预览。&lt;/li&gt;
  &lt;li&gt;场景：不同的来源变换、移动后的布局称为场景，录制过程可以切换场景。&lt;/li&gt;
  &lt;li&gt;来源：视频源、音频源、图片以及其它想要合成到视频的素材都称为来源。&lt;/li&gt;
  &lt;li&gt;混音器：用于设置需要采集的音源，并可以设置噪声过滤器。&lt;/li&gt;
  &lt;li&gt;开始推流/开始录制：推流直播和录制视频的开关，这里只关注录制。&lt;/li&gt;
  &lt;li&gt;设置：视频分辨率、编码器、码率等详细设置。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;添加来源&quot;&gt;添加来源&lt;/h3&gt;

&lt;p&gt;如何添加来源取决于连接了什么设备，不过在不连接任何外设的情况下，也有两个视频源可以用来练手，那就是电脑本身的屏幕和摄像头。&lt;/p&gt;

&lt;h4 id=&quot;添加录屏&quot;&gt;添加录屏&lt;/h4&gt;

&lt;ol&gt;
  &lt;li&gt;点击“来源”左下角的“+”号，在菜单中选择“显示捕获”。&lt;/li&gt;
  &lt;li&gt;接下来两个对话框中选择“确定”。&lt;/li&gt;
  &lt;li&gt;这时候屏幕显示被捕获到预览窗口中，也许显示有些乱，先把捕获视频缩小，在预览窗口中右键-&amp;gt;变换-&amp;gt;比例适应屏幕。&lt;/li&gt;
  &lt;li&gt;进一步缩小捕获窗口，移动到合适的位置。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;操作完成后，软件界面如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-11-12-conference-video-recording-guide/obs-screen.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;添加摄像头&quot;&gt;添加摄像头&lt;/h4&gt;

&lt;ol&gt;
  &lt;li&gt;点击“来源”左下角的“+”号，在菜单中选择“视频捕捉设备”。&lt;/li&gt;
  &lt;li&gt;第一个对话框点击“确定”，第二个对话框选择当前电脑的摄像头，然后确定。&lt;/li&gt;
  &lt;li&gt;调整窗口大小，移动到合适的位置。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;操作完成后，软件界面如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-11-12-conference-video-recording-guide/obs-capture.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;添加背景&quot;&gt;添加背景&lt;/h4&gt;

&lt;ol&gt;
  &lt;li&gt;点击“来源”左下角的“+”号，在菜单中选择“图像”。&lt;/li&gt;
  &lt;li&gt;第一个对话框点击“确定”，第二个对话框选择需要显示的图像，然后确定。&lt;/li&gt;
  &lt;li&gt;调整窗口大小，让其适应屏幕。&lt;/li&gt;
  &lt;li&gt;在“来源”列表中，将图像拖放到最后，以免遮挡别的视频源。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;操作完成后，软件界面如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-11-12-conference-video-recording-guide/obs-backgound.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;添加麦克风&quot;&gt;添加麦克风&lt;/h4&gt;

&lt;ol&gt;
  &lt;li&gt;点击“来源”左下角的“+”号，在菜单中选择“音频输入捕获”。&lt;/li&gt;
  &lt;li&gt;第一个对话框点击“确定”，第二个对话框选择电脑内置麦克风，然后确定。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;操作完成后，软件界面如下：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-11-12-conference-video-recording-guide/obs-microphone.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;开始录制&quot;&gt;开始录制&lt;/h3&gt;

&lt;p&gt;点击开始录制后，OBS 将会录制当前场景的视频和音频，视频文件将会存到 OBS 的默认目录内，可以在“设置-&amp;gt;输出-&amp;gt;录像-&amp;gt;录像路径”改变位置。&lt;/p&gt;

&lt;h3 id=&quot;其它&quot;&gt;其它&lt;/h3&gt;

&lt;p&gt;其它视频分辨率、视频格式等设置，都可以在“设置”中找到。我们还可以裁剪视频、给声音添加噪声过滤等操作，这里不再展开。&lt;/p&gt;

&lt;h2 id=&quot;硬件选择&quot;&gt;硬件选择&lt;/h2&gt;

&lt;p&gt;上面已经讲述了如何通过 OBS 录制视频，但在会议录制中我们不能单靠电脑录屏和摄像头，还需要外置设备。下面我介绍我用过的设备。&lt;/p&gt;

&lt;h3 id=&quot;采集卡&quot;&gt;采集卡&lt;/h3&gt;

&lt;h4 id=&quot;blackmagic-ultrastudio-mini-recorder&quot;&gt;Blackmagic UltraStudio Mini Recorder&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-11-12-conference-video-recording-guide/blackmagic.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;参考价：&lt;/p&gt;

&lt;p&gt;¥1230&lt;/p&gt;

&lt;p&gt;优点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;专业领域的大牌。&lt;/li&gt;
  &lt;li&gt;价格适中。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;缺点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;分辨率最高只支持 720p/60fps 或 1080p/30fps，需要调整演讲者电脑的输出分辨率，操作很繁琐。&lt;/li&gt;
  &lt;li&gt;雷电接口只适用于 2015 年款 Macbook Pro。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;评价：&lt;/p&gt;

&lt;p&gt;我的第一代设备，因为分辨率设置繁琐已经退至备用设备，目前不建议购买。&lt;/p&gt;

&lt;h4 id=&quot;magewell-usb-capture-hdmi-plus&quot;&gt;Magewell USB Capture HDMI Plus&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-11-12-conference-video-recording-guide/magewell.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;参考价：&lt;/p&gt;

&lt;p&gt;¥2600&lt;/p&gt;

&lt;p&gt;优点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;分辨率最高支持 4k/60fps，不用调整演讲者电脑。&lt;/li&gt;
  &lt;li&gt;带环路输出，离输出设备近的情况下可以节省一个 HDMI 分配器。&lt;/li&gt;
  &lt;li&gt;使用 USB 3.0 接口，兼容性好。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;缺点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;价格昂贵。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;评价：&lt;/p&gt;

&lt;p&gt;我当前使用的设备，满足一切需求。&lt;/p&gt;

&lt;h3 id=&quot;摄像机&quot;&gt;摄像机&lt;/h3&gt;

&lt;h4 id=&quot;canon-legria-hf-r86&quot;&gt;Canon LEGRIA HF R86&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-11-12-conference-video-recording-guide/canon-hf-r86.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;参考价：&lt;/p&gt;

&lt;p&gt;¥1999&lt;/p&gt;

&lt;p&gt;优点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;价格便宜。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;缺点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;麦克风指向性不好，且没有麦克风接口，扩展性差。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;评价：&lt;/p&gt;

&lt;p&gt;我的第一代设备，适合用于录制小型会议。但对于 500 人的会议，环境噪声变得非常大，缺乏 XLR 接口无法接入会场的麦克风输出。&lt;/p&gt;

&lt;h4 id=&quot;canon-xa30&quot;&gt;Canon XA30&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-11-12-conference-video-recording-guide/canon-xa30.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;参考价：&lt;/p&gt;

&lt;p&gt;¥11999&lt;/p&gt;

&lt;p&gt;优点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;专业级设备，扩展性强，提供 XLR 接口。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;缺点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;价格昂贵。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;评价：&lt;/p&gt;

&lt;p&gt;我当前使用的设备。对于会议录制，其实对声音的要求比视频更高，专业级设备提供的丰富接口可以轻松接入会场的麦克风输出。&lt;/p&gt;

&lt;p&gt;如果会议规模不大，或者场地没有提供麦克风输出，那么用家用机已经足够。&lt;/p&gt;

&lt;h3 id=&quot;其它-1&quot;&gt;其它&lt;/h3&gt;

&lt;h4 id=&quot;绿联ugreenhdmi分配器一进二出&quot;&gt;绿联（UGREEN）HDMI分配器一进二出&lt;/h4&gt;

&lt;p&gt;参考价：&lt;/p&gt;

&lt;p&gt;¥188&lt;/p&gt;

&lt;p&gt;评价：&lt;/p&gt;

&lt;p&gt;够用。&lt;/p&gt;

&lt;h4 id=&quot;云腾yunteng-vt-6008-摄像三脚架&quot;&gt;云腾（YUNTENG） VT-6008 摄像三脚架&lt;/h4&gt;

&lt;p&gt;参考价：&lt;/p&gt;

&lt;p&gt;¥179&lt;/p&gt;

&lt;p&gt;评价：&lt;/p&gt;

&lt;p&gt;够用，我能找到的最便宜的摄像三脚架。&lt;/p&gt;

&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;

&lt;p&gt;实际录制过程中还有很多需要注意的地方，例如提前搭设备、让讲师测试设备兼容性、调整摄像机的白平衡、调整合适的视频码率、后期裁剪多余的视频等等，不能一一在此叙述。&lt;/p&gt;

&lt;p&gt;网上关于如何录制会议视频的资料较少，我也是一步步摸索，所以方案还有很多不成熟的地方，欢迎留言交流，一起提高中文技术会议的录像质量。&lt;/p&gt;
</description>
        <pubDate>Sun, 12 Nov 2017 00:00:00 +0000</pubDate>
        <link>https://chloerei.com/2017/11/12/conference-video-recording-guide/</link>
        <guid isPermaLink="true">https://chloerei.com/2017/11/12/conference-video-recording-guide/</guid>
      </item>
    
      <item>
        <title>用 Docker Compose 搭建 Rails 开发环境</title>
        <description>&lt;p&gt;Docker 是目前最热门的容器格式，Docker Compose 是用于管理包含多个 Docker 容器的应用的工具，借助 Docker 和 Docker Compose，我们可以轻松搭建可复现的开发环境。&lt;/p&gt;

&lt;p&gt;这篇教程展示如何从零开始用 Docker Compose 搭建 Rails/PostgreSQL 开发环境。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;本文主要参考了 &lt;a href=&quot;https://docs.docker.com/compose/rails/&quot;&gt;Quickstart: Compose and Rails&lt;/a&gt;。但原文有&lt;a href=&quot;https://github.com/docker/docker.github.io/issues/3024&quot;&gt;一点问题&lt;/a&gt;，于是我加上自己的见解写成这篇博客。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;安装-docker&quot;&gt;安装 Docker&lt;/h2&gt;

&lt;p&gt;首先，需要在自己的开发机上安装 Docker，你可以在 https://www.docker.com/ 找到适合自己操作系统的 Docker 安装方式。&lt;/p&gt;

&lt;p&gt;安装完毕后，可以在命令行中使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker&lt;/code&gt; 命令：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker &lt;span class=&quot;nt&quot;&gt;--version&lt;/span&gt;
Docker version 17.03.1-ce, build c6d412e
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;创建项目&quot;&gt;创建项目&lt;/h2&gt;

&lt;p&gt;创建项目目录：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;mkdir &lt;/span&gt;myapp
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;cd &lt;/span&gt;myapp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;接着，创建一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt; 文件，包含以下内容：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;source&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'https://rubygems.org'&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'rails'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'5.1.0.rc2'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后，创建一个空的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gemfile.lock&lt;/code&gt; 文件：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;touch &lt;/span&gt;Gemfile.lock
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;之所以这么做，是因为不希望在主机环境安装 Ruby 和其它依赖，而是都放到镜像中。这两个文件用于在镜像中安装 Rails。&lt;/p&gt;

&lt;p&gt;接着，创建一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; 文件，该文件用于定义如何构建 Rails 镜像：&lt;/p&gt;

&lt;div class=&quot;language-Dockerfile highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;FROM&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; ubuntu:16.04&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;apt-get update &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; curl apt-transport-https &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;  curl &lt;span class=&quot;nt&quot;&gt;-sS&lt;/span&gt; https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;  &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;deb https://dl.yarnpkg.com/debian/ stable main&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/yarn.list

&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;apt-get update &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-y&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;  build-essential &lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;  libpq-dev &lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;  nodejs &lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;  ruby &lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;  ruby-dev &lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;  tzdata &lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;  yarn &lt;span class=&quot;se&quot;&gt;\
&lt;/span&gt;  zlib1g-dev

&lt;span class=&quot;k&quot;&gt;WORKDIR&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; /app&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;gem &lt;span class=&quot;nb&quot;&gt;install &lt;/span&gt;bundler
&lt;span class=&quot;k&quot;&gt;ADD&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; Gemfile /app/Gemfile&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ADD&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; Gemfile.lock /app/Gemfile.lock&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;RUN &lt;/span&gt;bundle &lt;span class=&quot;nb&quot;&gt;install&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ADD&lt;/span&gt;&lt;span class=&quot;s&quot;&gt; . /app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; 基于 Ubuntu 16.04，安装了创建 Rails 项目所需要的基础包，并将项目目录设定在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/app&lt;/code&gt;。你可以阅读 https://docs.docker.com/ 学习怎么定制 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;然后，创建一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt; 文件，该文件用于定义 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose&lt;/code&gt; 如何启动应用：&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;2'&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;.&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;bundle exec rails s -p 3000 -b 0.0.0.0&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;.:/app&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;3000:3000&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;db&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在这个文件里，我们定义了两个服务。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;web&lt;/code&gt; 服务用于运行 Rails，镜像将根据当前目录的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; 构建；&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db&lt;/code&gt; 服务用于运行 PostgreSQL 进程，镜像使用 Docker 官方提供的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt;。你可以在 https://docs.docker.com/compose/compose-file/ 找到其它配置的定义。&lt;/p&gt;

&lt;p&gt;接下来，执行以下命令：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose run web rails new &lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--force&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--database&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;postgresql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;该命令会在当前目录创建 Rails 项目：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt;
total 72
&lt;span class=&quot;nt&quot;&gt;-rw-r--r--&lt;/span&gt;   1 rei  staff   522  4 25 22:59 Dockerfile
&lt;span class=&quot;nt&quot;&gt;-rw-r--r--&lt;/span&gt;   1 rei  staff  1989  4 25 23:02 Gemfile
&lt;span class=&quot;nt&quot;&gt;-rw-r--r--&lt;/span&gt;   1 rei  staff  4847  4 25 23:03 Gemfile.lock
&lt;span class=&quot;nt&quot;&gt;-rw-r--r--&lt;/span&gt;   1 rei  staff   374  4 25 23:02 README.md
&lt;span class=&quot;nt&quot;&gt;-rw-r--r--&lt;/span&gt;   1 rei  staff   227  4 25 23:02 Rakefile
drwxr-xr-x  10 rei  staff   340  4 25 23:02 app
drwxr-xr-x   9 rei  staff   306  4 25 23:03 bin
drwxr-xr-x  14 rei  staff   476  4 25 23:02 config
&lt;span class=&quot;nt&quot;&gt;-rw-r--r--&lt;/span&gt;   1 rei  staff   130  4 25 23:02 config.ru
drwxr-xr-x   3 rei  staff   102  4 25 23:02 db
&lt;span class=&quot;nt&quot;&gt;-rw-r--r--&lt;/span&gt;   1 rei  staff   206  4 25 22:59 docker-compose.yml
drwxr-xr-x   4 rei  staff   136  4 25 23:02 lib
drwxr-xr-x   3 rei  staff   102  4 25 23:02 log
&lt;span class=&quot;nt&quot;&gt;-rw-r--r--&lt;/span&gt;   1 rei  staff    61  4 25 23:02 package.json
drwxr-xr-x   9 rei  staff   306  4 25 23:02 public
drwxr-xr-x  11 rei  staff   374  4 25 23:02 &lt;span class=&quot;nb&quot;&gt;test
&lt;/span&gt;drwxr-xr-x   4 rei  staff   136  4 25 23:02 tmp
drwxr-xr-x   3 rei  staff   102  4 25 23:02 vendor
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;接着，重建 Docker 镜像：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose build
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;每当修改了 Dockerfile，你都需要重建镜像。如果修改了 Gemfile，还需要先在容器内运行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bundle&lt;/code&gt;，然后重建镜像：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose run web bundle
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose build
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;连接数据库&quot;&gt;连接数据库&lt;/h2&gt;

&lt;p&gt;现在我们已经可以让 Rails 跑起来，但还需要修改一些配置让 Rails 连上数据库。默认情况下，Rails 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;development&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;test&lt;/code&gt; 环境会连接 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost&lt;/code&gt; 主机上的数据库，根据之前的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt; 配置，我们需要将数据库的主机名改为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db&lt;/code&gt;。另外，还需要修改 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;username&lt;/code&gt; 以适配 postgres 镜像的默认配置。修改后的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;database.yml&lt;/code&gt; 配置如下：&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;development&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;*default&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;app_development&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;db&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;*default&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;app_test&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;db&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;postgres&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;现在可以启动 Rails 进程了：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose up
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果一切正常，你会看到一些 postgres 的日志，然后是 Rails 启动日志：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;web_1  | =&amp;gt; Booting Puma
web_1  | =&amp;gt; Rails 5.1.0.rc2 application starting in development on http://0.0.0.0:3000
web_1  | =&amp;gt; Run `rails server -h` for more startup options
web_1  | Puma starting in single mode...
web_1  | * Version 3.8.2 (ruby 2.3.1-p112), codename: Sassy Salamander
web_1  | * Min threads: 5, max threads: 5
web_1  | * Environment: development
web_1  | * Listening on tcp://0.0.0.0:3000
web_1  | Use Ctrl-C to stop
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果你需要运行数据库迁移，可以打开另一个终端运行：&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose run web rails db:create
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;docker-compose run web rails db:migrate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;TIP: 以后运行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails&lt;/code&gt; 命令，都需要在前面加上 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose run web&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;用浏览器访问 http://localhost:3000 ，你会看到 Rails 的欢迎信息：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-04-24-docker-compose-for-rails-development/rails.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;将 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose.yml&lt;/code&gt; 与项目文件一并提交到版本控制里，这样新开发者参与项目的时候就可以用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker-compose up&lt;/code&gt; 一键启动开发环境。&lt;/p&gt;
</description>
        <pubDate>Mon, 24 Apr 2017 00:00:00 +0000</pubDate>
        <link>https://chloerei.com/2017/04/24/docker-compose-for-rails-development/</link>
        <guid isPermaLink="true">https://chloerei.com/2017/04/24/docker-compose-for-rails-development/</guid>
      </item>
    
      <item>
        <title>Ruby China 正在衰退吗？</title>
        <description>&lt;p&gt;Ruby China 是一个非营利的开源社区，不知不觉已经成立了 6 年。作为管理员之一，我一直不觉得是由管理员创建了这个社区，而是 Ruby 爱好者自发聚集组成了社区，我在里面只是担任了清洁工一类的角色。&lt;/p&gt;

&lt;p&gt;我最近经常被问到的一个问题是：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Ruby China 活跃度是不是越来越低了？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;主观上我也觉得 Ruby China 的热度有所降低，由于此前没有具体的数字，所以我做了一些调查。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-03-12-is-ruby-china-declining/ga.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;访问量总体还是呈上升趋势，因为论坛上积累了很多内容，形成了长尾效应。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-03-12-is-ruby-china-declining/users.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;注册人数平稳，除了某个月份有点异常，先忽略。不过下面两个图看出了问题：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-03-12-is-ruby-china-declining/topics.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-03-12-is-ruby-china-declining/replies.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;糟糕，话题数和回复数从 2013 年开始就一直在下降。从这个数据上看，&lt;strong&gt;Ruby China 确实是活跃度越来越低&lt;/strong&gt;。但……我并不认为这是个大问题。一直以来活跃度都不是管理员们关注的指标，相应的我们关心社区里的人实际在交流什么，有什么建设性的想法，哪里要举办线下聚会，哪个公司使用了 Ruby 正在招人。一些会让社区很热闹，但实质上没有营养的帖子会被毫不犹豫的移到 NoPoint 节点。所以 Ruby China 一直在技术圈有良好的口碑，并且每年举办 RubyConf。&lt;/p&gt;

&lt;p&gt;不过我想大家真正关心的是：Ruby 是不是正在衰退，毕竟这关系到新手能不能找到工作、老手以后会不会失业。我觉得需要认识到两点：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ruby 确实已经不热门了。&lt;/strong&gt;Ruby 在国外 07 ～ 09 年就达到了宣传的巅峰，我在 09 年刚接触 Ruby 的时候就看到国外对 Ruby 狂热的反思，有人花了两年用 Rails 重写网站失败，&lt;a href=&quot;https://sivers.org/rails2php&quot;&gt;然后又用两个月回到 PHP&lt;/a&gt;。支持我学下去的原因是对我来说 Ruby 比 Java/PHP/Python 好学（没错，我都学过，没学会），用 Rails 我第一次做出了有 CRUD 的完整网站。&lt;/p&gt;

&lt;p&gt;标志性的事件是在 11 年 &lt;a href=&quot;http://www.infoq.com/cn/news/2012/11/twitter-ruby-to-java&quot;&gt;Twitter 从 Ruby 向 Scala 迁移&lt;/a&gt;，Ruby 的狂热破灭了，Ruby 被烙上了性能不佳、无法扩展的印记，每个 Ruby 项目似乎迟早要被替换。不过，也有一些成熟的公司一直在用 Ruby：Basecamp，GitHub，Shopify，Airbnb，Twitch，SoundCloud……&lt;/p&gt;

&lt;p&gt;近年来，新语言新框架层出不穷，Go、Node.js、Elixir，很多新技术都在冲击 Ruby 的强项 Web 端。新技术最常见的推销方法就是写一个 Hello World 的性能测试跟 Rails 比较。从企业角度使用 Rails 不再代表团队的有多先进，从新手角度学习 Ruby 不再代表将来有很多工作机会。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ruby 正在走向成熟。&lt;/strong&gt;&lt;a href=&quot;http://www.ruanyifeng.com/blog/2017/03/gartner-hype-cycle.html&quot;&gt;技术热门度曲线&lt;/a&gt;模型认为，一门技术的发展要经历五个阶段：启动期-泡沫期-低谷期-爬升期-高原期。据 &lt;a href=&quot;https://stateofdev.com/t/programming-language&quot;&gt;State.of.dev&lt;/a&gt; 的分析，Ruby 正处于低谷期：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/posts/2017-03-12-is-ruby-china-declining/programming-language-state-of-dev.svg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;StackOverflow 和 Discourse 的创始人 Jeff Atwood 写道：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Ruby 不再酷了，追酷的人几年前都到了 Scala 或者 Node.js。我们的项目也不酷，只是一坨老旧的 Ruby 代码。从个人角度，我很庆幸 Ruby 已经成熟到不需要把自己打扮成最酷的孩子。这意味着我们这些只想把事情搞定的人可以专注做正经事，不用再去找下一个闪光点。
——&lt;a href=&quot;https://blog.codinghorror.com/why-ruby/&quot;&gt;Why Ruby&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;不止一个人跟我说过，在 Ruby China 上好像没什么好讲了，该踩的坑已经踩过，该吐的槽都吐过。但是，他们都在工作中使用 Ruby，用 Ruby 开发商业项目不再被怀疑是个很大风险。到&lt;a href=&quot;https://ruby-china.org/jobs&quot;&gt;招聘板块&lt;/a&gt;看看，Ruby 已经走入各行各业，也许每天使用的服务后面就有 Ruby 的应用。&lt;/p&gt;

&lt;p&gt;当然，Ruby China 作为最大的 Ruby 中文社区，有义务跟上 Ruby 的发展。17 年至今已经做了以下事情：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://ruby-china.org/topics/32376&quot;&gt;上线打赏功能&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://ruby-china.org/posts&quot;&gt;上线头条功能&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://ruby-china.org/topics/32272&quot;&gt;赞助 Rails Guides 翻译&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;设计新的 LOGO 和 UI（进行中）。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些改进都是为了方便让新手能找到学习资料，老手能分享行业经验，企业能找到适合的人才。我们利用业余时间维护 Ruby China，只为了让社区发展更好，毕竟我们已从开源获益良多。&lt;/p&gt;

&lt;p&gt;最后，我贴一下我面试现在这家公司时回答的最后一个问题：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“你觉得 Ruby 好在哪里？”&lt;/p&gt;

  &lt;p&gt;“我觉得 Ruby 好的地方，是它被设计为让程序员快乐。Ruby 有很多便利的语法，但语法很容易被别的语言借鉴，唯有它的理念独树一帜。Ruby 一直让我觉得编程很快乐。”&lt;/p&gt;
&lt;/blockquote&gt;
</description>
        <pubDate>Sun, 12 Mar 2017 00:00:00 +0000</pubDate>
        <link>https://chloerei.com/2017/03/12/is-ruby-china-declining/</link>
        <guid isPermaLink="true">https://chloerei.com/2017/03/12/is-ruby-china-declining/</guid>
      </item>
    
      <item>
        <title>测试指南</title>
        <description>&lt;p&gt;测试有很多种，像用户测试，A/B 测试等等，这里说的是由开发人员自己实行，用于确保开发过程没有引入错误的代码测试。&lt;/p&gt;

&lt;p&gt;测试不是一个新概念，相反部分社区可能过度狂热，制造了太多的测试框架和库，增加了很多复杂性，以至于让人敬而远之。其实测试只是一个简单的概念，这篇文章尝试说明这一点。&lt;/p&gt;

&lt;h2 id=&quot;测试&quot;&gt;测试&lt;/h2&gt;

&lt;p&gt;先看一个例子，假如我们需要实现一个方法 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fizz_buzz(n)&lt;/code&gt;，要求 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt; 是一个整数，如果 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt; 是 3 的倍数，就返回 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;'Fizz'&lt;/code&gt;；如果 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt; 是 5 的倍数，就返回 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;'Buzz'&lt;/code&gt;；其余则返回 n 本身。这个方法没什么实际作用，但用来做例子很合适，我们假设这个方法是某个生产应用的关键算法。&lt;/p&gt;

&lt;p&gt;这个方法很简单，一会就能写出来：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# fizz_buzz.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'Fizz'&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;elsif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'Buzz'&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;要验证这个方法是否正确，可以在终端执行这个方法查看结果：&lt;/p&gt;

&lt;div class=&quot;language-console highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;require &lt;span class=&quot;s1&quot;&gt;'./fizz_buzz.rb'&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;fizz_buzz 1
&lt;span class=&quot;gp&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;1
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;fizz_buzz 2
&lt;span class=&quot;gp&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;2
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;fizz_buzz 3
&lt;span class=&quot;gp&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Fizz&quot;&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;fizz_buzz 4
&lt;span class=&quot;gp&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;4
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;fizz_buzz 5
&lt;span class=&quot;gp&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Buzz&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;看起来没问题，于是就把这个方法用到产品环境中了……然后有一天，需求更改了，要求增加一个逻辑：如果 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;n&lt;/code&gt; 同时是 3 和 5 的倍数，就返回 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FizzBuzz&lt;/code&gt;。而当前的实现只会返回 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Fizz&lt;/code&gt;：&lt;/p&gt;

&lt;div class=&quot;language-console highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;fizz_buzz 15
&lt;span class=&quot;gp&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Fizz&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;于是修改这个方法：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# fizz_buzz.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'FizzBuzz'&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;elsif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'Fizz'&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;elsif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'Buzz'&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后到终端调试：&lt;/p&gt;

&lt;div class=&quot;language-console highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;require &lt;span class=&quot;s1&quot;&gt;'./sum.rb'&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;fizz_buzz 15
&lt;span class=&quot;gp&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;FizzBuzz&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;但是这个修改有没有破坏以前的行为呢？这时候再用以前的数据调试一下：&lt;/p&gt;

&lt;div class=&quot;language-console highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;fizz_buzz 1
&lt;span class=&quot;gp&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;1
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;fizz_buzz 2
&lt;span class=&quot;gp&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;2
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;fizz_buzz 3
&lt;span class=&quot;gp&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Fizz&quot;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里遇到一个问题，我们在重复以前的调试内容。重复一两次还没问题，三次以上就很烦人了。并且随着代码量上升，越来越难确定修改会影响什么地方的逻辑，容易引入 bug。&lt;/p&gt;

&lt;p&gt;高效程序员会将调试代码固化下来，写成测试代码。&lt;/p&gt;

&lt;p&gt;新建一个文件，写入测试代码：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# fizz_buzz_test.rb&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'./fizz_buzz.rb'&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'.'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;fizz_buzz 1 should be 1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Fizz'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'.'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;fizz_buzz 3 should be Fizz&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Buzz'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'.'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;fizz_buzz 5 should be Buzz&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'done'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个脚本会对比程序输出和预期结果，如果结果一致就会打印一个点&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.&lt;/code&gt;，否则会抛出异常，中止测试并打印错误信息。&lt;/p&gt;

&lt;div class=&quot;language-console highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;ruby fizz_buzz_test.rb
&lt;span class=&quot;go&quot;&gt;...done
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们可以故意把方法写错，看看有什么结果：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# fizz_buzz.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;再次运行，结果就是：&lt;/p&gt;

&lt;div class=&quot;language-console highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;ruby fizz_buzz_test.rb
&lt;span class=&quot;gp&quot;&gt;.fizz_buzz_test.rb:4:in `&amp;lt;main&amp;gt;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;': fizz_buzz 3 should be Fizz (RuntimeError)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;有了测试脚本的帮助，我们就能知道对代码的修改有没有破坏以前的逻辑。修改了代码之后，别忘了加上新增部分功能的测试：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'FizzBuzz'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'.'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;fizz_buzz 15 should be FizzBuzz&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;再次运行测试：&lt;/p&gt;

&lt;div class=&quot;language-console highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;ruby fizz_buzz_test.rb
&lt;span class=&quot;go&quot;&gt;....done
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;assert断言&quot;&gt;assert（断言）&lt;/h2&gt;

&lt;p&gt;之前的测试代码里面有不少重复代码，例如 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;print&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;raise&lt;/code&gt; 等等。我们可以把这些跟测试用例没有直接关系的代码抽取出通用方法，这类方法有一个惯用名称 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert&lt;/code&gt;，于是测试代码简化成：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;print&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'.'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;fizz_buzz 1 should be 1&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Fizz'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;fizz_buzz 3 should be Fizz&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Buzz'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;fizz_buzz 5 should be Buzz&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'FizzBuzz'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;fizz_buzz 15 should be FizzBuzz&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'done'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;测试代码多了之后，会发现有一类测试有固定的模式，例如上面的测试就是判断一个方法的输出跟另一个值是否相等，这样又可以抽取出一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert_equal&lt;/code&gt; 方法：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;print&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'.'&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;assert_equal&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;except&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kp&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;assert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;except&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;actual&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;assert_equal&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;fizz_buzz 1 should be 1&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assert_equal&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Fizz'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;fizz_buzz 3 should be Fizz&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assert_equal&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'Buzz'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;fizz_buzz 5 should be Buzz&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;assert_equal&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'FizzBuzz'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fizz_buzz&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;fizz_buzz 15 should be FizzBuzz&quot;&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;puts&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;'done'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里看起来好像没减少代码，不过起码不用怕写错 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;==&lt;/code&gt; 号了。常见的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert_*&lt;/code&gt; 方法还有：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert_nil(object, msg)&lt;/code&gt; 测试对象是否 nil。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert_empty(object, msg)&lt;/code&gt; 测试对象调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.empty?&lt;/code&gt; 是否返回 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert_includes(collection, object, msg)&lt;/code&gt; 测试集合 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;collection&lt;/code&gt; 是否包含 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;object&lt;/code&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert_throws(exception) { }&lt;/code&gt; 测试执行一个 block，是否会抛出某个异常。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这些方法都不过是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert&lt;/code&gt; 的包装，只要知道 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assert&lt;/code&gt; 的原理，这些辅助方法都能自己实现，或者实现其他适合场景的断言方法。&lt;/p&gt;

&lt;p&gt;现在每个主流语言都会有一个测试库，在 Ruby 中就是 &lt;a href=&quot;https://github.com/seattlerb/minitest&quot;&gt;Minitest&lt;/a&gt;。测试库除了包含一些断言方法外，还提供测试代码隔离、测试环境重置、更好的错误提示等功能，你可以阅读文档了解详情。&lt;/p&gt;

&lt;h2 id=&quot;tdd&quot;&gt;TDD&lt;/h2&gt;

&lt;p&gt;TDD 是 Test-driven development（测试驱动）的缩写，它是一种开发方法，提倡在实现功能之前先写测试，从而实现更好的程序质量和接口设计。它的流程可以简化如下：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;写测试&lt;/li&gt;
  &lt;li&gt;运行测试（失败）&lt;/li&gt;
  &lt;li&gt;写功能&lt;/li&gt;
  &lt;li&gt;运行测试（通过）&lt;/li&gt;
  &lt;li&gt;回到 1&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这种方法在一定程度上是有效的，先写测试可以提供高覆盖度的代码测试，减少 bug。并且写测试的过程就是在实际调用程序接口，有助于理清设计思路。要详细了解 TDD，可以读《&lt;a href=&quot;http://book.douban.com/subject/1230036/&quot;&gt;测试驱动开发&lt;/a&gt;》这本书。&lt;/p&gt;

&lt;p&gt;不过如果片面追求测试覆盖率，就很容易写出比功能代码多几倍的测试代码。测试也是代码，代码就有维护成本，过多的测试反而降低编码效率。对此《测试驱动开发》的作者 Kent Beck 有过解释：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence.
（我的薪水是付给能用的代码，而不是给测试的，所以我的哲学是在可信赖的程度上，尽量少写测试。）
http://stackoverflow.com/questions/153234/how-deep-are-your-unit-tests&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;另外，TDD 也可能对设计产生误导，变成追求测试的简洁而不是接口简洁。DHH 在 &lt;a href=&quot;https://www.youtube.com/watch?v=9LfmrkyP81M&quot;&gt;RailsConf 2014&lt;/a&gt; 上举了一个例子：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;age&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;today&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;year&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;birthday&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;year&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;a person's age is determined by birthday&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;sevent_niner&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;birthday: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1979&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;travel_to&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2009&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;assert_equal&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sevent_niner&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;age&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在这个例子中，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person#age&lt;/code&gt; 的返回结果是会随着时间变化的，为了保证测试的可靠，需要使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;travel_to&lt;/code&gt; 这个 hack，把测试用例的时间固定在某个值。&lt;/p&gt;

&lt;p&gt;这时候为了写出优美的测试代码，可能会想到另一个接口设计：&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;age&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;now&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;today&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;year&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;birthday&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;year&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;a person's age is determined by birthday&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;sevent_niner&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;birthday: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1979&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;assert_equal&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sevent_niner&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;age&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2009&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;通过给 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;age&lt;/code&gt; 添加一个参数，让测试代码避免了 hack，似乎是一个更好的方案。但实际上，在正常的调用中，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;age&lt;/code&gt; 并不需要参数，这个参数完全是为了测试而添加的，这给原先的设计增加了不必要的复杂性。这就是测试驱动开发误导了设计的例子。&lt;/p&gt;

&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;

&lt;p&gt;以上内容就是关于测试必须了解的内容，至于 DSL、Mock/Stub、Factory……之类的工具都是锦上添花，不是必须的。&lt;/p&gt;

&lt;p&gt;为了项目的可维护性，也为了节约自己的时间，应该积极的拥抱测试。但也不要忘了测试只是辅助开发的工具，不要本末倒置，使用太复杂的测试工具增加维护难度。&lt;/p&gt;
</description>
        <pubDate>Mon, 26 Oct 2015 00:00:00 +0000</pubDate>
        <link>https://chloerei.com/2015/10/26/testing-guide/</link>
        <guid isPermaLink="true">https://chloerei.com/2015/10/26/testing-guide/</guid>
      </item>
    
      <item>
        <title>安装和配置 Postfix</title>
        <description>&lt;p&gt;Postfix 是一个 MTA（Mail Transfer Agent），可以用来收发邮件。开发网站多少都需要收发邮件的功能，例如邮件验证、找回密码等。&lt;/p&gt;

&lt;p&gt;配置邮件系统过程比较复杂，而且需要很多维护工作，如果发送量不大，可以先用 Mailgun，Mandrill 等第三方邮件发送服务，在遇到以下情况的时候，再考虑自建邮箱系统：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;第三方服务费用太高。&lt;/li&gt;
  &lt;li&gt;QQ 邮箱对第三方服务拒信率高。&lt;/li&gt;
  &lt;li&gt;邮件排队时间过长，发送不及时。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;自建邮件系统可以处理这些问题，有更多优化空间。&lt;/p&gt;

&lt;h2 id=&quot;系统需求&quot;&gt;系统需求&lt;/h2&gt;

&lt;p&gt;Ubuntu LTS 14.04 。&lt;/p&gt;

&lt;h2 id=&quot;设置-hostname&quot;&gt;设置 hostname&lt;/h2&gt;

&lt;p&gt;事先设置好 hostname 的话，Postfix 可以自动配置好很多参数，节省时间。假设你的网站域名是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example.com&lt;/code&gt;，要搭建独立的邮件服务器（推荐），就把主机名设置为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mail.example.com&lt;/code&gt;。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# echo 'mail.example.com' &amp;gt; /etc/hostname
# hostname -F /etc/hostname
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;安装-postfix&quot;&gt;安装 Postfix&lt;/h2&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# apt-get install postfix
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;安装过程会弹出设置窗口，全部回车确认既可。&lt;/p&gt;

&lt;h2 id=&quot;基本配置&quot;&gt;基本配置&lt;/h2&gt;

&lt;p&gt;Postfix 的配置文件位于 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/postfix&lt;/code&gt; 文件夹。先看 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.cf&lt;/code&gt; 文件，有几个重要的配置。如果事先设置了正确的 hostname，那么这些配置已经自动设置好了。&lt;/p&gt;

&lt;h3 id=&quot;myhostname&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;myhostname&lt;/code&gt;&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;myhostname = mail.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;myhostname&lt;/code&gt; 让 Postfix 知道自己主机的名字。&lt;/p&gt;

&lt;h3 id=&quot;myorigin&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;myorigin&lt;/code&gt;&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;myorigin = /etc/mailname
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;myorigin&lt;/code&gt; 的值存放在另一个文件中，打开这个文件可以看到一行内容 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mail.example.com&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;在通过 Postfix 发送邮件的时候，如果 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;From&lt;/code&gt; 字段不完整，例如 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;From: user&lt;/code&gt;，Postfix 会根据 myorigin 的值将地址补全为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;From: user@mail.example.com&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;* 发邮件的时候 From 字段是可以随意指定的。&lt;/p&gt;

&lt;h3 id=&quot;mynetworks&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mynetworks&lt;/code&gt;&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mynetworks&lt;/code&gt; 指定了本地网络的 IP 段，默认只包含主机自己。&lt;/p&gt;

&lt;h3 id=&quot;smtpd_relay_restrictions&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;smtpd_relay_restrictions&lt;/code&gt;&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;smtpd_relay_restrictions&lt;/code&gt; 指定了 Postfix 在作为邮件发送方的时候，只接受通过以下规则的发信请求：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;permit_mynetworks&lt;/code&gt; 允许 mynetworks 包含的主机发信。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;permit_sasl_authenticated&lt;/code&gt; 允许通过 SASL 身份验证的主机发信。&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;defer_unauth_destination&lt;/code&gt; 不符合其它转发规则的时候拒绝发信。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;mydestination&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mydestination&lt;/code&gt;&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mydestination = mail.example.com, localhost.example.com, localhost
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mydestination&lt;/code&gt; 指定了 Postfix 在收到这些域名地址作为目标地址的邮件时，作为接收方收下邮件。&lt;/p&gt;

&lt;p&gt;如果收到的邮件既不符合转发规则，又不符合接收规则，则会拒绝收信。&lt;/p&gt;

&lt;p&gt;由于我希望这台服务器能接受主域名 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;example.com&lt;/code&gt; 的邮件，所以将这个配置修改为：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mydestination = example.com, mail.example.com, localhost.example.com, localhost
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;重载 Postfix：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# service postfix reload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;为了检查安装情况，现在先做一些测试。&lt;/p&gt;

&lt;h2 id=&quot;测试一发邮件&quot;&gt;测试一：发邮件&lt;/h2&gt;

&lt;p&gt;用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sendmail&lt;/code&gt; 命令给自己的邮箱发送一封空邮件&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# sendmail youremail@gmail.com
.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;输入第一行的时候，sendmail 会等待输入邮件内容，此时直接输入一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.&lt;/code&gt; 结束输入，这会产生一个空邮件。&lt;/p&gt;

&lt;p&gt;登录你的邮箱，如无意外可以在垃圾邮件箱找到这封邮件。这说明 Postfix 已经具有发送能力。&lt;/p&gt;

&lt;h2 id=&quot;测试二-收邮件&quot;&gt;测试二： 收邮件&lt;/h2&gt;

&lt;p&gt;由于还没有配置 DNS，其它邮件服务商还无法识别这部主机，先在另一台主机用 telnet 进行测试，假设邮件服务器的 IP 是 192.168.33.10：&lt;/p&gt;

&lt;pre&gt;
$ telnet 192.168.33.10 25
Trying 192.168.33.10...
Connected to 192.168.33.10.
Escape character is '^]'.
220 mail.example.com ESMTP Postfix (Ubuntu)
&lt;b&gt;MAIL FROM: youremail@gmail.com&lt;/b&gt;
250 2.1.0 Ok
&lt;b&gt;RCPT TO: root&lt;/b&gt;
250 2.1.5 Ok
&lt;b&gt;DATA&lt;/b&gt;
354 End data with &amp;lt;CR&amp;gt;&amp;lt;LF&amp;gt;.&amp;lt;CR&amp;gt;&amp;lt;LF&amp;gt;
&lt;b&gt;text
.&lt;/b&gt;
250 2.0.0 Ok: queued as 651FE22162
&lt;b&gt;QUIT&lt;/b&gt;
Connection closed by foreign host.
&lt;/pre&gt;

&lt;p&gt;加粗部分是需要输入的内容。&lt;/p&gt;

&lt;p&gt;在邮件服务器上检查信件：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# tailf /var/mail/root
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;大概会看到这样的内容：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;From youremail@gmail.com  Wed Apr 22 16:13:33 2015
Return-Path: &amp;lt;youremail@gmail.com&amp;gt;
X-Original-To: root
Delivered-To: root@mail.example.com
Received: from unknown (unknown [192.168.33.1])
        by mail.example.com (Postfix) with SMTP id 651FE22162
        for &amp;lt;root&amp;gt;; Wed, 22 Apr 2015 16:13:13 +0000 (UTC)

text


&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Postfix 默认使用 mbox 格式将系统用户的邮件存放到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/var/mail&lt;/code&gt; 目录下。&lt;/p&gt;

&lt;h2 id=&quot;mx-记录&quot;&gt;MX 记录&lt;/h2&gt;

&lt;p&gt;如果你的邮件服务器已经部署到公网上，要用来接收其它邮件服务商发来的邮件，那么需要到域名的 DNS 服务器进行修改。&lt;/p&gt;

&lt;p&gt;首先给邮件服务器设置 A 记录（自行替换为真实 IP）：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mail.example.com. IN A 192.168.33.10
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后给主域名设置 MX 记录：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com. IN MX 10 mail.example.com.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在本地测试更新状况：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ dig example.com mx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果返回了正确的 MX 记录，则邮件服务器已经可以被识别，但不同服务商所用的 DNS 更新状况不一样，全部生效也许要过一段时间。&lt;/p&gt;

&lt;p&gt;在本地测试邮件服务器是否能收到邮件：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ sendmail root@example.com
.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;aliases&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aliases&lt;/code&gt;&lt;/h2&gt;

&lt;p&gt;用登录 ssh 的方式收邮件不方便，我们可以使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aliases&lt;/code&gt; 功能将邮件转发到自己的个人邮件地址。&lt;/p&gt;

&lt;p&gt;打开 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/aliases&lt;/code&gt; 文件，目前应该是这样：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# See man 5 aliases for format
postmaster: root
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;将 root 作为别名，转发到个人邮件地址：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# See man 5 aliases for format
postmaster: root
root: youremail@gmail.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样以 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;root@example.com&lt;/code&gt; 作为目的地址的邮件将会转发到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;youremail@gmail.com&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;别名支持多个地址，所以一个简易的邮件列表可以这样实现：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# See man 5 aliases for format
postmaster: root
root: youremail@gmail.com
support: youremail@gmail.com, another@gmail.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样以 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;support@example.com&lt;/code&gt; 作为目的地址的邮件将会转发到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;youremail@gmail.com&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;another@gmail.com&lt;/code&gt; 。&lt;/p&gt;

&lt;p&gt;修改 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/aliases&lt;/code&gt; 文件后，需要运行一条命令让它生效：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# newaliases
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;sasl-身份验证&quot;&gt;SASL 身份验证&lt;/h2&gt;

&lt;p&gt;目前 Postfix 只能为本地应用发送邮件，还不接收为远程应用发送邮件。如果你的应用跟 Postfix 装在同一个服务器，那么无需身份验证既可发送邮件；而如果不在同一个服务器，则需要配置某种验证方式验证发信者的身份。&lt;/p&gt;

&lt;p&gt;一个方法是配置 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mynetworks&lt;/code&gt;，把应用服务器纳入本地网络，以 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;192.168.33.11&lt;/code&gt; 为例：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 192.168.33.11
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mynetworks&lt;/code&gt; 的值还支持掩码，如果应用服务器位于同一个网段，这种方法比较方便。&lt;/p&gt;

&lt;p&gt;我比较习惯帐号密码的方式验证，在 Postfix 可以使用 SASL 模块实现帐号密码验证。SASL 模块支持多种帐号密码储存方式，这里只介绍用独立数据库文件（sasldb2）存放的方式。&lt;/p&gt;

&lt;h3 id=&quot;安装-sasl&quot;&gt;安装 SASL&lt;/h3&gt;

&lt;p&gt;首先安装一些工具用于创建 SASL 专用的帐号密码。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# apt-get install sasl2-bin
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;创建-sasl-帐号密码&quot;&gt;创建 SASL 帐号密码&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# saslpasswd2 -c -u example.com postmaster
# cp -a /etc/sasldb2 /var/spool/postfix/etc/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;* Postfix 的 smtpd 进程默认使用了 chroot，所以要把数据库文件拷贝到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/var/spool/postfix/etc&lt;/code&gt; 目录内。&lt;/p&gt;

&lt;p&gt;输入后会提示输入两次密码，创建成功后帐号为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postmaster@example.com&lt;/code&gt;，密码为所输入的密码。&lt;/p&gt;

&lt;p&gt;为了让 Postfix 能读取这个文件，把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postfix&lt;/code&gt; 加到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sasl&lt;/code&gt; 组，并设为只读：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# gpasswd -a postfix sasl
# chmod 640 /var/spool/postfix/etc/sasldb2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;检查现有的帐号：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# sasldblistusers2 -f /var/spool/postfix/etc/sasldb2
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;h3 id=&quot;配置-postfix&quot;&gt;配置 Postfix&lt;/h3&gt;

&lt;p&gt;在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/postfix/main.cf&lt;/code&gt; 添加以下内容：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# SASL
smtpd_sasl_auth_enable = yes
smtpd_tls_auth_only = yes
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这些配置打开 SASL 登录，并且只允许 TLS 安全传输的情况下进行验证。之所以强制 TLS，是因为默认的 PLAIN 校验传输的几乎是明文密码。&lt;/p&gt;

&lt;p&gt;新建文件 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/postfix/sasl/smtpd.conf&lt;/code&gt;，添加内容：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;pwcheck_method: auxprop
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个配置指定使用数据库文件读取帐号密码信息。&lt;/p&gt;

&lt;p&gt;修改配置后，重载 Postfix 让配置生效：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# service postfix reload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;测试登录&quot;&gt;测试登录&lt;/h3&gt;

&lt;p&gt;首先准备 SASL PLAIN 验证需要的帐号密码字符串，输入以下命令：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;printf '\0%s\0%s' 'postmater@example.com' '123456' | openssl base64
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;实际中替换你需要的帐号密码，输出结果即为登录字符串。&lt;/p&gt;

&lt;p&gt;由于设置了强制 TLS 登录，用 telnet 就不那么方便了，这时候可以用以下命令连接：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;openssl s_client -connect mail.example.com:25 -starttls smtp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这条命令打开到服务器的 smtp 连接，并且完成 starttls 过程，之后界面跟 telnet 类似。完成校验的命令如下：&lt;/p&gt;

&lt;pre&gt;
&lt;b&gt;auth plain AHBvc3RtYXRlckBleGFtcGxlLmNvbQAxMjM0NTY=&lt;/b&gt;
235 2.7.0 Authentication successful
&lt;/pre&gt;

&lt;p&gt;看到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authentication successful&lt;/code&gt; 即为通过校验。现在远程主机可以通过帐号密码登录，使用 Postfix 发信了。&lt;/p&gt;

&lt;h2 id=&quot;spf-记录&quot;&gt;SPF 记录&lt;/h2&gt;

&lt;p&gt;SPF 记录是一种通过 DNS 记录，验证邮件发送主机的 IP 是否可信的方法。之所以需要额外的验证方式，是因为 Email 的发送地址很容易伪造，没有有效 SPF 记录的邮件很可能被归为垃圾邮件。后面的 DKIM 也是验证邮件可信度的一种方式。&lt;/p&gt;

&lt;p&gt;配置 SPF 记录分为设置发信和校验来信两种情况。&lt;/p&gt;

&lt;h3 id=&quot;设置发信-spf&quot;&gt;设置发信 SPF&lt;/h3&gt;

&lt;p&gt;发信的 SPF 记录不需要 Postfix 设置，而完全在 DNS 上。&lt;/p&gt;

&lt;p&gt;为自己的域名添加一条 TXT 记录：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;example.com IN TXT &quot;v=spf1 mx ~all&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这条记录表示域名自身 MX 记录指向的主机为可信主机，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~all&lt;/code&gt; 表示除此以外的主机为软拒绝。&lt;/p&gt;

&lt;p&gt;SPF 记录的语法规则可以查阅 http://www.openspf.org/SPF_Record_Syntax 。&lt;/p&gt;

&lt;h3 id=&quot;校验来信-spf&quot;&gt;校验来信 SPF&lt;/h3&gt;

&lt;p&gt;Postfix 需要安装一个组件以支持 SPF 校验。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# apt-get install postfix-policyd-spf-perl
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;添加 Postfix policy：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# postfix-add-policy spfcheck nobody /usr/sbin/postfix-policyd-spf-perl
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/postfix/main.cf&lt;/code&gt; 添加以下内容：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# SPF
smtpd_recipient_restrictions = permit_mynetworks permit_sasl_authenticated check_policy_service unix:private/spfcheck
spfcheck_time_limit = 3600
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;重载配置：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# service postfix reload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;现在 Postfix 每收到一封邮件都会进行 SPF 校验，然后在邮件头部加入类似 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Received-SPF: pass&lt;/code&gt; 的信息。&lt;/p&gt;

&lt;h2 id=&quot;dkim-签名&quot;&gt;DKIM 签名&lt;/h2&gt;

&lt;p&gt;DKIM 是另一种验证邮件有效性的方法。跟 SPF 不同，DKIM 在 DNS 公开一个公钥，然后用私钥对自己的邮件进行签名。收信者查询发信方域名获得公钥，然后校验签名是否有效。&lt;/p&gt;

&lt;p&gt;为 Postfix 添加 DKIM 支持需要用到 opendkim 这个包。&lt;/p&gt;

&lt;h3 id=&quot;安装-opendkim&quot;&gt;安装 opendkim&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# apt-get install opendkim opendkim-tools
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;配置-opendkim&quot;&gt;配置 opendkim&lt;/h3&gt;

&lt;p&gt;打开 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/opendkim.conf&lt;/code&gt;，添加以下内容：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Domain example.com
KeyFile /etc/mail/dkim.key
Selector mail
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Domain 为自己的域名，KeyFile 为域名对于的私钥，Selector 为公钥存放的主机名（这里设置为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mail._domainkey.example.com&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;打开 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/default/opendkim&lt;/code&gt;，添加以下内容：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;SOCKET=&quot;inet:8891@localhost&quot; # listen on loopback on port 8891
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这里让 opendkim 的守护进程监听 8891 端口，用于和 Postfix 通信。&lt;/p&gt;

&lt;p&gt;* 你应该配置防火墙禁止外部访问白名单以外的端口。&lt;/p&gt;

&lt;h3 id=&quot;生成密钥和配置-dns&quot;&gt;生成密钥和配置 DNS&lt;/h3&gt;

&lt;p&gt;生成密钥：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# mkdir /etc/mail
# cd /etc/mail
# opendkim-genkey -s mail -d example.com
# cp mail.private dkim.key
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;查看 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mail.txt&lt;/code&gt; 文件，里面有 dkim 的公钥，将它添加为 DNS TXT 记录，类似于：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;mail._domainkey.example.com. IN TXT &quot;v=DKIM1; k=rsa; p=PpYHdE2tevfEpvL1Tk2dDYv0pF28/f5M...&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;重启 opendkim：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# service opendkim restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;配置-postfix-1&quot;&gt;配置 Postfix&lt;/h3&gt;

&lt;p&gt;打开 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/postfix/main.cf&lt;/code&gt;，添加以下内容：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# DKIM
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这为 Postfix 加上了 DKIM 的过滤器，发信的时候签名，收信的时候校验。&lt;/p&gt;

&lt;p&gt;重载 Postfix：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# service postfix reload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;现在 Postfix 会自动为发出的邮件签名，校验收到的邮件。&lt;/p&gt;

&lt;h2 id=&quot;总结&quot;&gt;总结&lt;/h2&gt;

&lt;p&gt;现在已经利用 Postfix 搭建了一个邮件服务器，我们可以使用它收发网站邮件。但搭建完毕只是第一部，接下来还需要观察送达情况。最重要的一点，不要发送垃圾邮件，不要让网站有漏洞让用户发送垃圾邮件，否则会拉低所有邮件的评分，导致正常邮件也发不出去。&lt;/p&gt;

&lt;p&gt;要深入了解 Postfix 的使用，推荐书籍：&lt;a href=&quot;http://book.douban.com/subject/1754499/&quot;&gt;《Postfix 权威指南》&lt;/a&gt;。看完这本后就可以看官方文档：http://www.postfix.org/documentation.html 。&lt;/p&gt;
</description>
        <pubDate>Wed, 22 Apr 2015 00:00:00 +0000</pubDate>
        <link>https://chloerei.com/2015/04/22/install-and-configure-postfix/</link>
        <guid isPermaLink="true">https://chloerei.com/2015/04/22/install-and-configure-postfix/</guid>
      </item>
    
  </channel>
</rss>
