<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ ZhichengChen - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ ZhichengChen - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Wed, 13 May 2026 20:04:38 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/zhichengchen/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 关系型数据库 VS 非关系型数据库——SQL DB 和 NoSQL DB 的区别 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：Relational VS Nonrelational Databases – the Difference Between a SQL DB and a NoSQL DB [https://www.freecodecamp.org/news/relational-vs-nonrelational-databases-difference-between-sql-db-and-nosql-db/] ，作者：Dionysia Lemonaki [https://www.freecodecamp.org/news/author/dionysia/] 本文概述了关系和非关系数据库的区别，你还将了解如何根据它们的优缺点来决定哪一个更适合相应的项目。 我们将讨论：  * 数据库的定义 * SQL 是什么?          * 关系型数据库 * 特点     * ACID 属性    ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/relational-vs-nonrelational-databases-difference-between-sql-db-and-nosql-db/</link>
                <guid isPermaLink="false">627cdf4ac9c067061df8b8bd</guid>
                
                    <category>
                        <![CDATA[ 数据库 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Thu, 12 May 2022 05:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/05/valeriia-svitlini-5w0ZbF8P5-4-unsplash.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/relational-vs-nonrelational-databases-difference-between-sql-db-and-nosql-db/">Relational VS Nonrelational Databases – the Difference Between a SQL DB and a NoSQL DB</a>，作者：<a href="https://www.freecodecamp.org/news/author/dionysia/">Dionysia Lemonaki</a></p><!--kg-card-begin: markdown--><p>本文概述了关系和非关系数据库的区别，你还将了解如何根据它们的优缺点来决定哪一个更适合相应的项目。</p>
<p>我们将讨论：</p>
<ul>
<li><a href="#definition">数据库的定义</a>
<ul>
<li><a href="#sql">SQL 是什么?</a></li>
</ul>
</li>
<li><a href="#relational">关系型数据库</a>
<ul>
<li><a href="#characteristics">特点</a></li>
<li><a href="#acid">ACID 属性</a></li>
</ul>
</li>
<li><a href="#non-relational">非关系型数据库</a>
<ul>
<li><a href="#types">类型</a></li>
<li><a href="#base">BASE 属性</a></li>
</ul>
</li>
<li><a href="#pick">关系型数据库 VS 非关系型数据库</a></li>
<li><a href="#extra">拓展阅读</a></li>
</ul>
<h2 id="">什么是数据库？针对初学者的定义</h2>
<p>在计算机里，数据是以不同形式出现的信息片段。它可以是文本、数字、图像、音频片段或视频。</p>
<p>信息集合需要被存储、处理和解释。这时就需要一种可按需轻松搜索、访问、提取和检索已保存资源的方法。该方法可以使计算机或人类分析可访问的数据、执行计算和比较、做出逻辑决策得出结论。</p>
<p>当然可以使用 Excel 电子表格等软件将数据存储在文件中，这样也可以完成有限的工作。</p>
<p>但是，如果数据量很大，使用 Excel 处理就捉襟见肘了。</p>
<p>在数据量增大时，Excel 无法快速检索。并且 Excel 很难固定其数据结构。</p>
<p>数据库是一种更易于访问、更高效且更有条理的长期存储和处理信息的方式。</p>
<p>数据库存储数据的规范性和系统性以及其检索数据的便捷性使其成为基于 Web 的应用程序中重要的部分。</p>
<p>数据库几乎可以用于所有应用程序。它们可以用来存储用户信息，例如用户名、电子邮件地址、加密密码和物理地址。</p>
<p>它们还存储用户行为。例如，在电商网站中，数据库会保存并跟踪“收藏”的商品。</p>
<p>一般使用<strong>数据库管理系统</strong>（或简称 DBMS）来管理数据库。</p>
<p>数据库管理系统是一个软件程序，充当最终用户和数据库中间的媒介。可以通过数据库管理系统创建和管理数据库。也可以执行查询来访问、修改和操作存储在数据库中的数据。</p>
<p>只需通过一些命令就可以方便轻松地存储、检索、更新和删除数据。</p>
<p>谈到数据库管理系统，通常有<strong>两种</strong>类型可供选择：</p>
<ul>
<li><strong>关系型数据库</strong>（也就是 <strong>SQL 数据库</strong>）</li>
<li><strong>非关系型数据库</strong>（也就是 <strong>NoSQL 数据库</strong>）</li>
</ul>
<h3 id="sql">SQL 是什么?</h3>
<p>SQL 是 <strong>S</strong>tructured <strong>Q</strong>uery <strong>L</strong>anguage 的缩写。</p>
<p>你可能会听过它以两种发音方式 - “<em>S. Q. L.</em>”（ess-kew-ell）或“<em>se-quel</em>”（/ˈsēkwəl/）。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2022/04/Screenshot-2022-04-13-at-6.25.32-PM.png" alt="https://i.imgur.com/NtGaNA8.png" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>https://i.imgur.com/NtGaNA8.png</figcaption>
</figure>
<p>无论哪种方式，SQL 都代表数据库处理的语言。</p>
<p>具体来说，使用 SQL，可以编写数据库查询以和数据库进行通信。查询可以是用于执行任何 CRUD（创建、读取、更新、删除）操作的命令。</p>
<p>SQL 是关系数据库管理系统的首选语言，将在下一节中详细介绍。</p>
<h2 id="">什么是关系数据库？</h2>
<p>关系数据库（或 SQL 数据库）已经存在了一段时间。第一个关系数据库出现在 1970 年，关系数据库至今仍然很流行，一些最常见的如：</p>
<ul>
<li><a href="https://www.postgresql.org/">PostgreSQL</a></li>
<li><a href="https://www.microsoft.com/en-us/sql-server/sql-server-downloads">Microsoft SQL Server</a></li>
<li><a href="https://www.mysql.com/">MySQL</a></li>
<li><a href="https://www.oracle.com/index.html">Oracle</a></li>
<li><a href="https://sqlite.org/index.html">SQLite</a></li>
</ul>
<p>关系数据库以结构化和表的方式存储数据。也就是说，它将信息存储在<strong>表</strong>中，可以将其视为数据的存储容器。例如，一家公司可以有一个 <code>employees</code> 表来存储其员工的数据。</p>
<p>关系数据库具有严格的、静态的预定义逻辑<strong>架构（schema）</strong>。可以将数据库架构视为一个组织蓝图——一组规则：哪些可以插入表、哪些不能插入表、以及如何设置数据。</p>
<p>在每个表中，至少有一个<strong>列（column）</strong>。这些列具有特定的数据类型，例如 <code>INTEGER</code> 或 <code>VARCHAR</code>。在 <code>employees</code> 表中，一些列可能是 <code>employee_id</code>、<code>name</code>、<code>department</code>、<code>email</code> 和 <code>salary</code>。</p>
<p>所有列和其数据类型构成架构。</p>
<pre><code class="language-sql">             EMPLOYEES

+-------------+------+------------+-------+--------+
| employee_id | name | department | email | salary |
+-------------+------+------------+-------+--------+
</code></pre>
<p>一个表也会包含<strong>行（rows）</strong> 或 <em>记录（records）</em>。记录是遵守预定义架构的单个数据条目。本质上，它是一个数据项。</p>
<pre><code class="language-sql">             EMPLOYEES
+-------------+------------------+------------+-----------------------+--------+
| employee_id |       name       | department |         email         | salary |
+-------------+------------------+------------+-----------------------+--------+
|           1 |  John Doe        | IT         | johndoe@company.com   |   3500 |
|           2 |  Kelly Kellinson | Marketing  | kelly@company.com     |   1500 |
|           3 |  Mike Manson     | Product    | mikekane@company.com  |   2300 |
+-------------+------------------+------------+-----------------------+--------+
</code></pre>
<p>由于关系数据库支持 SQL，所以可以直接执行查询。例如，如果想 <code>view</code> 月薪 <code>greater than 2000 dollars</code> 的 <code>employees</code> 的 <code>names</code>，那么可以编写如下 SQL 查询：</p>
<pre><code class="language-SQL">SELECT name FROM employees
WHERE salary &gt; 2000;
</code></pre>
<p>执行上面的查询，会获得以下输出：</p>
<pre><code class="language-SQL">+-------------+
|    name     |
+-------------+
| John Doe    |
| Mike Manson |
+-------------+
</code></pre>
<h3 id="">关系数据库的特点</h3>
<p>到目前为止，已经了解了关系数据库：</p>
<ul>
<li>是表格格式</li>
<li>非常有条理，并且数据以某种结构存储</li>
<li>具有严格、预定义的架构</li>
<li>使用 SQL 执行数据库查询和操作数据</li>
</ul>
<p>此外，一个关系数据库可以有多个表，正如数据库管理系统的名称所暗示的那样，这些表可以是相互关联的。</p>
<p>例如，一家电商公司可能有一个 <code>products</code> 表、一个 <code>users</code> 表、一个 <code>emails</code> 表和一个 <code>orders</code> 表。</p>
<p>由于表和存储在其中的信息之间存在连接和关联，可以使用命令来连接表。</p>
<p>关系数据库有一个主键，它作为标识符，确保表中的每一项都是唯一的，从而确保表中没有重复和冗余的数据。</p>
<p><em>外键</em> 用于表示在表之间的关系。</p>
<p>不同表中的数据可以有不同的关系：</p>
<ul>
<li><strong>一对一的关系</strong>：在这种情况下，一个表中的记录仅与另一个表中的一条记录相关。电商网站中一对一关系的示例是，一个用户只能拥有一个电子邮件地址，且一个电子邮件地址只能属于一个用户。</li>
<li><strong>一对多关系</strong>：在这种情况下，一张表中的一条记录与另一张表中的多条其他记录相关。例如，在电商网站中，一个用户可以下许多订单，但每个订单都是由一个用户下的。</li>
<li><strong>多对多关系</strong>：在这种情况下，一个表中的一个或多个记录可以与另一个表中的一个或多个记录相关。例如，在电商网站中，一个订单可以有很多产品，而一个产品可以被购买多次。</li>
</ul>
<h3 id="acid">关系数据库中的 ACID 属性</h3>
<p>关系数据库提供 ACID 数据一致性模型。</p>
<p>ACID 是原子性（<strong>A</strong>tomicity）、一致性（<strong>C</strong>onsistency）、事务隔离（<strong>I</strong>solation）、持久性（<strong>D</strong>urability） 的首字母缩写词。</p>
<p><strong>原子性</strong>意味着事务是原子的并且采取 “all or nothing” 的方法。</p>
<p>也就是，要么整个操作成功，从头到尾完成，要么不成功，整个操作“回滚”。</p>
<p>所有操作都保证以成功或失败结束，不存在部分成功。</p>
<p><strong>一致性</strong>是确保数据库结构从事务开始到结束保持不变。一致性确保进入数据库的任何数据都遵循已设置的规则和约束。它可以保护和维护关系数据库中数据的完整性。</p>
<p><strong>事务隔离</strong>意味着尽管在任何时候都发生了许多事务，但每个事务都被视为一个原子的、独立的单元，并且事务似乎是按顺序发生的。</p>
<p>例如，如果两个事务同时发生，此属性可确保一个事务以及那里发生的更改不会以任何方式影响另一个事务。</p>
<p>最后，<strong>持久性</strong>意味着事务的任何结果和更改都已提交，因此是永久性的，并且将持续存在，即使出现系统故障也是如此。</p>
<p>ACID 模型可确保数据库可靠且安全。</p>
<h2 id="">什么是非关系数据库？</h2>
<p>非关系型数据库也称为 NoSQL 数据库。经常会看到 NoSQL 代表“<strong>N</strong>ot <strong>o</strong>nly <strong>SQL</strong>”和“Non-SQL”。</p>
<p>无论哪种方式，非关系数据库都是指不使用关系数据模型的数据库。</p>
<p>尽管这个术语和这种类型的数据库已经存在了几十年，但 NoSQL 数据库在 1990 年代后期才开始受欢迎，当时 Internet 也变得越来越流行。</p>
<p>关系数据库已无法满足互联网海量且复杂的数据。</p>
<p>一些最流行的非关系数据库：</p>
<ul>
<li><a href="https://www.mongodb.com/">MongoDB</a></li>
<li><a href="https://redis.io/">Redis</a></li>
<li><a href="https://cassandra.apache.org/_/index.html">Apache Cassandra</a></li>
<li><a href="https://cloud.google.com/bigtable">Google Cloud Bigtable</a></li>
<li><a href="https://aws.amazon.com/dynamodb/">Amazon DynamoDB</a></li>
</ul>
<p>非关系数据库不以表格式存储和组织数据。不同数据点之间没有表、行、列或关系。</p>
<p>相反，数据存储在<strong>集合</strong>中。数据库通常是非结构化的并使用动态架构。</p>
<h3 id="">非关系数据库的类型</h3>
<p>有四种主要类型的非关系数据库：</p>
<ul>
<li><strong>列式数据库</strong></li>
<li><strong>键-值数据库</strong></li>
<li><strong>面向文档的数据库</strong></li>
<li><strong>图数据库</strong></li>
</ul>
<p><strong>列式数据库</strong>在概念上类似于关系数据库，但它们使用组或列集（也称为列族）而不是行来逻辑组织相关数据。可以通过使用与单个列关联的唯一行键来独立访问列族。列式数据库搜索特定数据的速度很快，因为无需通过不相关的信息行来查找要搜索的内容。</p>
<p><strong>键-值数据库</strong>是最简单的非关系数据库类型之一。数据以键值对集合的形式存储在字典或哈希表中。这种类型的数据库具有唯一的键。键充当指向特定值的指针并与该值相关联。分配给键的值可以是任何信息和数据类型。要检索和访问该值，请使用唯一键作为引用。</p>
<p><strong>面向文档的数据库</strong>也以键值对的方式存储数据。但是其值是一个文档，它有一个唯一的键作为它的标识符。文档可以是任何格式，例如 XML、YAML 或二进制，通常采用 JSON 格式。这种类型的数据库以半结构化的方式存储数据。没有架构或预定义的结构。正因为如此，它更灵活，可以在项目需求发生变化时重新安排和重新设计数据库结构。它还提供了类似 SQL 的查询语言或者通过 API 来对数据执行查询以及 CRUD 操作。</p>
<p><strong>图数据库</strong>是最复杂的非关系数据库类型，它们可以处理大量数据。图数据库专注于数据元素之间的连接和关系，并使用图论来存储、搜索和管理这些关系。图数据库使用 <em>nodes</em> 来存储数据，用 <em>nodes</em> 表示单个实体或数据。一个节点连接到另一个节点。为了表示实体之间的连接或关系，图数据库还用到了 <em>edges</em>。</p>
<h3 id="base">非关系数据库中的 BASE 属性</h3>
<p>非关系数据库提供 BASE 数据库一致性模型。该模型不像关系数据库的 ACID 模型那样严格。</p>
<p>BASE 是以下的首字母缩写词：</p>
<ul>
<li><strong>B</strong>asic <strong>A</strong>vailability 基本可用，该模型不关注数据的即时一致性。但是，该系统似乎在持续工作，并始终保证数据的可用性。</li>
<li><strong>S</strong>oft 软状态，由于缺乏即时一致性，系统的状态可能会随着时间而改变。软状态意味着系统不需要写一致性。</li>
<li><strong>E</strong>ventual 最终一致性，主要优先事项是数据的持续可用，而不是数据一致性。但是，最终在某个时候，可以期望数据是一致的。当系统停止接收输入时，可能会发生这种情况。</li>
</ul>
<h2 id="sqlnosql">如何在 SQL 和 NoSQL 数据库之间进行选择</h2>
<p>在学习了 SQL 和 NoSQL 数据库的基础知识之后，可能想知道如何为项目选择适合类型的数据库。</p>
<p>嗯，这个问题没有明确的答案。</p>
<p>两种数据库都有优点和缺点，这在很大程度上取决于正在构建的应用程序的类型、将使用的数据的类型以及的未来目标。</p>
<p>通常在产品中都会涉及到这两种类型的数据库。</p>
<p>以下是它们特征的快速摘要，可帮助决定哪一个可能更适合。</p>
<h3 id="sql">何时使用 SQL 数据库</h3>
<ul>
<li>需要分布在多个表中的高度结构化的数据，需要数据遵守严格的、可预测的、预定义的和已经计划好的模式。</li>
<li>数据将保持相对不变。如果不打算频繁更改数据库的结构并且不需要定期更新项目，SQL 数据库会很方便。请记住，它们提供的灵活性很小。</li>
<li>需要一致的数据。</li>
<li>数据完整性和安全性是重中之重。</li>
<li>需要复杂查询的准确结果。</li>
</ul>
<p>SQL 数据库的一个缺点是它们是垂直扩展的。当存储变多时，需要增加当前机器上的硬件和提高计算能力。这可能代价高昂。需要增加处理能力和内存存储来处理增加的负载以提高性能。</p>
<h3 id="nosql">何时使用 NoSQL 数据库</h3>
<ul>
<li>在一个快速的开发环境中工作，需要经常调整需求并不断更改数据库结构。</li>
<li>正在处理大量性质不同但不需要大量结构或准确性的数据。</li>
<li>正在处理需要频繁更新的数据。NoSQL 数据库提供了一个松散、灵活和动态的模式，允许对数据进行定期更改。</li>
<li>需要快速的查询结果和系统的持续可用性。</li>
<li>不想对数据库进行任何前期规划、准备或设计，而是想立即开始构建。</li>
</ul>
<p>NoSQL 数据库的一大优势是它们可以水平扩展。</p>
<p>它们的设计方式可以将更多机器添加到现有机器（例如云服务器）中。与需要额外 CPU（中央处理单元）或 RAM（随机存取存储器）资源的垂直缩放相比，这种行为更可取。</p>
<p>但当然，NoSQL 数据库的一个缺点是它们不能确保数据的完整性和一致性。</p>
<h2 id="">拓展阅读</h2>
<p>这篇文章只是触及了皮毛，最好的学习方法就是边做边学。</p>
<p>以下是一些学习资源，可用于了解有关数据库和 SQL 的更多信息：</p>
<ul>
<li><a href="https://www.freecodecamp.org/news/learn-sql-free-relational-database-courses-for-beginners/">Learn SQL – Free Relational Database Courses for Beginners</a>，为这篇文章添加书签以获取免费 SQL 课程列表。</li>
<li><a href="https://chinese.freecodecamp.org/learn/relational-database/">freeCodeCamp 关系数据库（Beta） 认证</a>，在本课程中，你将学习到必要的开发人员工具，然后将学习如何使用代码编辑器、命令行和 Git，还将学习使用 PostgreSQL（一种关系数据库管理系统）和 SQL——它的查询语言。</li>
<li><a href="https://www.freecodecamp.org/news/learn-nosql-in-3-hours/">Learn About NoSQL Databases in This 3-hour Course</a>，在本课程中，将了解四种不同的 NoSQL 数据库类型。除了理论之外，还有实战。</li>
</ul>
<h3 id="">结语</h3>
<p>你已经到了文章的结尾！</p>
<p>希望你了解了关系数据库和非关系数据库之间的主要区别。文末的额外的资源可以继续学习，将新技能付诸实践。</p>
<p>感谢你阅读本文，祝你编码愉快！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何使用谷歌搜索——更快搜索到结果的小技巧 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：How to Use Google – Search Tips for Better Results [https://www.freecodecamp.org/news/use-google-search-tips/]，作者：Marko Denic [https://www.freecodecamp.org/news/author/denicmarko/] 如果知道如何正确使用 Google，它就是世界上最强大的工具。让我们来看一下如何在谷歌搜索上做得更好。 1. 使用引号进行精确匹配搜索 可以使用引号来强制进行完全匹配搜索。如果确切知道要查找的搜索短语，这将非常有用。如果这样做，会只返回精确的结果。 "what is javascript" 2. 使用 AND 操作符 AND 操作符将仅返回与这两个术语都相关的结果。在下面的示例中，可以看到此操作符的用例。 html AND css 3. 使用 OR 操作符 可以使用 OR 操作符来获取与输入的搜索词任意一个相关的结果。 (javascript OR python) free course 4. 使 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/use-google-search-tips/</link>
                <guid isPermaLink="false">624ab1e799ec7406219e558b</guid>
                
                    <category>
                        <![CDATA[ Google ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Mon, 04 Apr 2022 05:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/04/pexels-photomix-company-218717.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/use-google-search-tips/">How to Use Google – Search Tips for Better Results</a>，作者：<a href="https://www.freecodecamp.org/news/author/denicmarko/">Marko Denic</a></p><!--kg-card-begin: markdown--><p>如果知道如何正确使用 Google，它就是世界上最强大的工具。让我们来看一下如何在谷歌搜索上做得更好。</p>
<h2 id="1">1. 使用引号进行精确匹配搜索</h2>
<p>可以使用引号来强制进行完全匹配搜索。如果确切知道要查找的搜索短语，这将非常有用。如果这样做，会只返回精确的结果。</p>
<p><code>"what is javascript"</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/google-exact.PNG" alt="Google search exact match example" width="600" height="400" loading="lazy"></p>
<h2 id="2and">2. 使用 AND 操作符</h2>
<p>AND 操作符将仅返回与这两个术语都相关的结果。在下面的示例中，可以看到此操作符的用例。</p>
<p><code>html AND css</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/google-and.PNG" alt="Google search with AND keyword" width="600" height="400" loading="lazy"></p>
<h2 id="3or">3. 使用 OR 操作符</h2>
<p>可以使用 OR 操作符来获取与输入的搜索词任意一个相关的结果。</p>
<p><code>(javascript OR python) free course</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/google-or.PNG" alt="Google search OR keyword example" width="600" height="400" loading="lazy"></p>
<h2 id="4">4. 使用 -（减号）操作符</h2>
<p>- 运算符将排除相关术语或短语的结果。在下面的例子中，想要获得与 JavaScript 相关的结果，但排除任何包含的 CSS 结果。</p>
<p><code>javascript -css</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/google-minus.PNG" alt="Google search with - operator example" width="600" height="400" loading="lazy"></p>
<h2 id="5">5. 使用 (*) 通配符作为占位符</h2>
<p>可以使用 (*) 通配符作为占位符，它将被任何单词或短语替换。</p>
<p>这个是我的最爱。当我大概知道整个搜索短语并用星号替换我不知道的部分时，我会使用它。 超级好用。</p>
<p><code>"how to start * in 6 months"</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/google-wildcard.PNG" alt="Google search with wildcard example" width="600" height="400" loading="lazy"></p>
<h2 id="6">6. 如何在指定网站内搜索</h2>
<p>这也是我经常使用的方式。如果我只想在指定网站上搜索某些内容，我会这样做。</p>
<p><code>site:freecodecamp.org</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/google-site.PNG" alt="Google search inside a single website example" width="600" height="400" loading="lazy"></p>
<h2 id="7">7. 如何查找特定文件类型</h2>
<p>还可以使用这个非常有用的功能来帮助找到特定的文件类型。如果正在寻找诸如 PDF 之类的材料，它会很方便。如果以前没有使用过它，那么从现在开始使用它吧。</p>
<p><code>filetype:pdf learn css</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/google-filetype.PNG" alt="Google search for specific filetype" width="600" height="400" loading="lazy"></p>
<h2 id="8">8.  如何搜索一个范围内的数字</h2>
<p>这可以是任何东西。如果想购买特定价格区间的商品，或者正在搜索包含特定年份范围的结果，那么这很好用。</p>
<p><code>ecmascript 2016..2018</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/google-range.PNG" alt="Google search for specific range of numbers" width="600" height="400" loading="lazy"></p>
<h2 id="9before">9. 使用 <code>before</code> 操作符</h2>
<p>使用 before 操作符可以只返回给定日期之前的结果。</p>
<p>必须提供年-月-日格式的日期或仅提供年，例如：</p>
<p><code>javascript before:2020</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/google-before.png" alt="Google search with before operator" width="600" height="400" loading="lazy"></p>
<h2 id="10after">10. 使用 <code>after</code> 操作符</h2>
<p>使用 after 操作符仅返回给定日期之后的结果。必须提供年-月-日格式日期或仅提供年。如果想排除过时的结果，则非常有用。</p>
<p><code>web development after:2020</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/google-after.png" alt="Google search with after operator" width="600" height="400" loading="lazy"></p>
<p>如你所见，如果知道如何正确使用它，Google 可以成为一个强大的工具。</p>
<p>如果有任何问题，可以在 <a href="https://twitter.com/denicmarko">Twitter</a> 上与我联系。</p>
<p>也可以在我的 <a href="https://markodenic.com/blog/">博客</a> 上找到大量实际工作中用到的技巧和资源。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ HTTP 请求方法——用代码示例解释 Get vs Put vs Post ]]>
                </title>
                <description>
                    <![CDATA[ 原文：HTTP Request Methods – Get vs Put vs Post Explained with Code Examples [https://www.freecodecamp.org/news/http-request-methods-explained/]，作者：Camila Ramos Garzon [https://www.freecodecamp.org/news/author/camiinthisthang/] 在本文中，我们将讨论 get、put 和 post HTTP 方法，介绍每种 HTTP 方法的用途以及使用场景。 为了深入了解 HTTP 方法的工作原理，本文还将介绍 HTTP 有关上下文和背景信息。 这篇文章讲要介绍的主题：  * HTTP 协议  * 客户端-服务器架构  * APIs 读完本文，将对每个请求方法的定位有一个很好的了解。还将具备发起请求和使用 Web API 的经验。 什么是 HTTP？ HTTP 是一种协议，或一组明确的规则，用于访问网络上的资源。资源可以是任何东西，从 HTML ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/http-request-methods-explained/</link>
                <guid isPermaLink="false">624ab07e99ec7406219e5564</guid>
                
                    <category>
                        <![CDATA[ HTTP ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Mon, 04 Apr 2022 04:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/04/FCC-Cover-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/http-request-methods-explained/">HTTP Request Methods – Get vs Put vs Post Explained with Code Examples</a>，作者：<a href="https://www.freecodecamp.org/news/author/camiinthisthang/">Camila Ramos Garzon</a></p><!--kg-card-begin: markdown--><p>在本文中，我们将讨论 get、put 和 post HTTP 方法，介绍每种 HTTP 方法的用途以及使用场景。</p>
<p>为了深入了解 HTTP 方法的工作原理，本文还将介绍 HTTP 有关上下文和背景信息。</p>
<h3 id="">这篇文章讲要介绍的主题：</h3>
<ul>
<li>HTTP 协议</li>
<li>客户端-服务器架构</li>
<li>APIs</li>
</ul>
<p>读完本文，将对每个请求方法的定位有一个很好的了解。还将具备发起请求和使用 Web API 的经验。</p>
<h2 id="http">什么是 HTTP？</h2>
<p>HTTP 是一种协议，或一组明确的规则，用于访问网络上的资源。资源可以是任何东西，从 HTML 文件到数据库中的数据、照片、文本等等。</p>
<p>通过 HTTP 协议向这些 API 发出请求，然后返回相应的资源。<code>API</code>代表应用程序编程接口。它是开发人员请求资源的机制。</p>
<h3 id="">客户端-服务器架构</h3>
<p>学习 HTTP 方法前，理解客户端/服务器架构的概念很重要。该架构描述了 Web 应用程序的工作方式，并定义了通信的规则。</p>
<p>客户端是用户实际与之交互的应用程序，它展示所需的内容。服务器是将内容或资源发送到客户端的应用程序。服务器在某处持续运行等待客户端的请求。</p>
<p>这种分离的主要原因是为了保护敏感信息。如果整个应用程序都被下载到浏览器中，任何访问网页的人都可以访问所有数据。</p>
<p>这种架构有助于保护 API 密钥、个人数据等。现在，<a href="https://nextjs.org/">Next.js</a> 和 <a href="https://www.netlify.com/">Netlify</a> 等流行工具允许开发人员在与其客户端程序中运行服务器代码， 无需部署到专用的服务器上。</p>
<h3 id="">客户端-服务器通信</h3>
<p>客户端和服务器根据需要每次单独发送消息进行通信，而不是持续的流通信。</p>
<p>这些通信几乎总是由客户端以请求的形式发起。这些请求由服务器应用程序完成，返回包含请求的资源的响应。</p>
<h3 id="">为什么我们需要服务器-客户端架构？</h3>
<p>假设正在构建一个网络天气应用。用户要与之交互的是客户端程序——它有按钮、搜索栏，并显示城市名称、当前温度、AQI 等数据。</p>
<p>天气应用不会将每个城市及其天气信息直接硬编码到程序中。这会使应用程序变得臃肿而缓慢，检索数据添加到数据库需要花费很长时间，而且每天的更新的也很让人头疼。</p>
<p>相反，该应用程序可以使用 Weather Web API 按城市访问天气数据。应用程序会收集用户的位置，然后向服务器发出请求，说：“嘿，把这个城市的天气信息发给我。”</p>
<p>根据要实现的目标，将使用不同的请求方法。服务器返回一个包含天气信息等内容的响应，具体取决于 API 的设计。它还可能会返回时间戳、该城市所在的区域等信息。</p>
<p>客户端与运行在某处的服务器进行通信，服务器的工作就是不断地到监听是否有到该地址的请求。当它接收到请求时，它会基于传入的参数从数据库或者另一个 API 或者本地文件查询返回符合要求的响应。</p>
<h3 id="http">HTTP 请求剖析</h3>
<p>HTTP 请求必须具有以下内容：</p>
<ul>
<li>HTTP 方法（如 <code>GET</code>）</li>
<li>主机 URL（如<code>https://api.spotify.com/</code>）</li>
<li>端点路径（如 <code>v1/artists/{id}/related-artists</code>）</li>
</ul>
<p>请求还可选包含：</p>
<ul>
<li>Body 请求体</li>
<li>Headers</li>
<li>查询字符串</li>
<li>HTTP 版本号</li>
</ul>
<h3 id="http">HTTP 响应剖析</h3>
<p>响应必须具有以下内容：</p>
<ul>
<li>协议版本（如<code>HTTP/1.1</code>）</li>
<li>状态码（如 <code>200</code>）</li>
<li>状态文本（<code>OK</code>）</li>
<li>Headers</li>
</ul>
<p>响应还可选具有：</p>
<ul>
<li>Body 响应体</li>
</ul>
<h2 id="http">HTTP 方法解释</h2>
<p>Post Malone 意味着 Get、Put、Patch 和 Delete Malone 的存在。</p>
<p>— Paul Ford (@ftrain) <a href="https://twitter.com/ftrain/status/1195350262145306624?ref_src=twsrc%5Etfw">November 15, 2019</a></p>
<p>现在我们知道了什么是 HTTP 以及为什么需要它，让我们来聊聊这些不同的 HTTP 方法。</p>
<p>在上面的天气应用程序示例中，我们想要检索某城市的天气信息。但是如果我们想提交一个城市的天气信息该如何请求呢？</p>
<p>在现实生活中，可能无权更改其他人的数据，但假设我们是社区运行的天气应用程序的贡献者。除了从 API 获取天气信息外，该城市的成员还可以更新此信息以显示更准确的数据。</p>
<p>或者，如果我们想完全添加一个由于某种原因在我们的城市数据库中不存在的新城市怎么办？ 这些是不同的功能——检索数据、更新数据、创建新数据——所有这些都有对应的 HTTP 方法。</p>
<h3 id="httppost">HTTP POST 请求</h3>
<p><code>POST</code> 用来创建一个新资源。<code>POST</code> 请求需要一个请求体，可以在其中定义要创建的实体数据。</p>
<p>POST 请求成功返回的状态码是 200。在天气应用中，可以使用 POST 方法来添加新城市。</p>
<h3 id="httpget">HTTP GET 请求</h3>
<p><code>GET</code> 用来读取或检索资源。 <code>GET</code> 成功后会返回包含所请求信息的响应。</p>
<p>在天气应用中，可以使用 GET 来检索某城市当前的天气。</p>
<h3 id="httpput">HTTP PUT 请求</h3>
<p><code>PUT</code> 用来修改资源。<code>PUT</code> 用请求体 payload 中的数据替换整个资源。如果没有与请求体唯一标识相匹配的资源，它将创建一个新资源。</p>
<p>在天气应用中，可以使用 <code>PUT</code> 来更新指定城市的所有天气数据。</p>
<h3 id="httppatch">HTTP PATCH 请求</h3>
<p><code>PATCH</code> 用来修改资源的部分字段。使用 <code>PATCH</code>，只需要传入想要更新的字段即可。</p>
<p>在天气应用中，可以使用 <code>PATCH</code> 来更新指定城市指定日期的降雨量。</p>
<h3 id="http">HTTP 删除请求</h3>
<p><code>DELETE</code> 可以用来删除资源。在天气应用中，可以使用 <code>DELETE</code> 删除出于某种原因不再想跟踪的城市。</p>
<h2 id="httpmethods">HTTP Methods 常见问题解答</h2>
<h3 id="putpost">PUT 和 POST 有什么区别？</h3>
<p><code>PUT</code> 请求是幂等的，这意味着执行相同的 <code>PUT</code> 请求将始终产生相同的结果。</p>
<p>另一方面，<code>POST</code> 会产生不同的结果。如果多次执行 <code>POST</code> 请求，将多次创建新资源，尽管传入的是相同的数据。</p>
<p>使用餐厅类比，多次 <code>POST</code> 会创建多个独立的订单，而多次 <code>PUT</code> 请求将更新现有的订单。</p>
<h3 id="putpatch">PUT 和 PATCH 有什么区别？</h3>
<p>主要区别在于，如果 <code>PUT</code> 找不到指定的资源，它将创建一个新资源。而使用 <code>PUT</code> 时，需要传入改数据的全部资源，即使只想修改一个字段。</p>
<p>使用 <code>PATCH</code>，可以只传入要更新字段更新资源。</p>
<h3 id="put">如果我只想更新我的部分资源怎么办？我还能使用 PUT 吗？</h3>
<p>如果只想更新部分资源，则在发出 <code>PUT</code> 请求时仍需要发送整个资源的数据。这里更适合的选项是 <code>PATCH</code>。</p>
<h3 id="body">为什么请求和响应的 body 是可选的？</h3>
<p>body 是可选的，因为对于某些请求，例如使用 <code>GET</code> 方法检索资源，请求正文中无需指定任何内容。可以从指定端点检索所有数据。</p>
<p>类似地，当状态码足够或正文中没有要返回的内容时，某些响应的主体是可选的，如 <code>DELETE</code> 操作。</p>
<h2 id="http">HTTP 请求示例</h2>
<p>现在已经介绍了什么是 HTTP 请求，以及它们的使用场景，让我们发起一些请求！这里将使用 <a href="https://docs.github.com/en/rest/reference/gists">GitHub Gist API</a>。</p>
<p>“Gist 是一种与他人共享代码段的简单方法。所有 Gist 都是 Git 仓库，因此可以使用 Git 的版本控制，fork 等功能。” （来源：GitHub）</p>
<p>为此，需要一个 GitHub 帐户。如果还没有，这是一个很好的时机来创建一个以在将来保存代码。</p>
<p>GitHub 上的每个用户都可以创建 gist、检索他们的 gist、检索所有公共 gist、删除 gist 和更新 gist，等等。为了简单起见，我们将使用 <a href="https://hoppscotch.io/">Hoppscotch</a>，这是一个可以方便地发起 HTTP 请求的一个可视化平台。</p>
<p>快速上手 Hoppscotch：</p>
<ul>
<li>有一个下拉菜单，可以在其中选择要用来创建请求的 method。</li>
<li>有一个文本框，可以在其中粘贴要访问的 API 端点的 URL。</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-24-at-12.35.33-PM.png" alt="Screen-Shot-2022-01-24-at-12.35.33-PM" width="600" height="400" loading="lazy"></p>
<ul>
<li>有一个 Headers 部分，我们将按照 GitHub 文档的说明在其中传递请求头。</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-24-at-12.39.38-PM-1.png" alt="Screen-Shot-2022-01-24-at-12.39.38-PM-1" width="600" height="400" loading="lazy"></p>
<ul>
<li>有一个 body 区域，可以按照 GitHub 文档的指示将相应的请求实体写入 body。</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-24-at-12.41.14-PM.png" alt="Screen-Shot-2022-01-24-at-12.41.14-PM" width="600" height="400" loading="lazy"></p>
<ul>
<li>如果请求成功，右栏会显示通知。如果它是绿色的，表示成功发起了请求，如果它是红色的，代表有错误。</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-24-at-3.44.56-PM.png" alt="Screen-Shot-2022-01-24-at-3.44.56-PM" width="600" height="400" loading="lazy"></p>
<h3 id="get">如何发出 GET 请求</h3>
<p>要创建 <code>GET</code> 请求以检索所有特定用户的 gists，可以使用以下方法和端点：<code>GET /users/{username}/gists</code>。按照文档可以通过 endpoint 传入指定的参数来发起请求。</p>
<p>可以看到在路径中需要传入一个带有目标用户用户名的字符串。还需要传入一个名为 accept 的 headers 并将其设置为 <code>application/vnd.github.v3+json</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-20-at-2.01.35-PM.png" alt="Screen-Shot-2022-01-20-at-2.01.35-PM" width="600" height="400" loading="lazy"></p>
<p>API 的 URL 为：</p>
<pre><code class="language-shell">https://api.github.com
</code></pre>
<p>此特定操作的端点路径为：</p>
<pre><code>/users/{username}/gists
</code></pre>
<p>要发起此请求：</p>
<ol>
<li>在 Hoppscotch 的输入字段中粘贴完整的 URL + 路径。请务必将 “username” 替换为实际用户名。如果还没有创建过 Gists 的 GitHub 账号，可以先使用我的：camiinthisthang。</li>
<li>选择<code>GET</code>请求方法</li>
<li>在 Headers 选项卡中，将 accept 设置为 header 并将值设置为 <code>application/vnd.github.v3+json</code></li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-24-at-12.39.38-PM-2.png" alt="Screen-Shot-2022-01-24-at-12.39.38-PM-2" width="600" height="400" loading="lazy"></p>
<p>4. 点击发送！</p>
<p>在底部，会看到格式为 <code>JSON</code> 的响应。为了便于阅读，复制响应并将其粘贴到 <a href="https://jsonformatter.curiousconcept.com/#">在线 JSON 格式化程序</a>。</p>
<p>在格式化程序中，可以知道响应是一个对象数组。每个对象代表一个 gist，向我们展示 URL、ID 等信息。</p>
<h3 id="post">如何发出 POST 请求</h3>
<p>现在来使用 <code>POST</code> 方法创建一个资源。在这种情况下，新资源将是一个新的 gist。</p>
<p>首先，需要创建一个个人 access token。为此，<a href="https://github.com/settings/tokens">转到设置页面</a>并点击生成令牌。</p>
<p>命名 token 并选择 scope “Create Gists”：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-20-at-2.59.11-PM.png" alt="Screen-Shot-2022-01-20-at-2.59.11-PM" width="600" height="400" loading="lazy"></p>
<p>然后单击页面底部的绿色 <code>Generate token</code> 按钮。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-20-at-3.28.01-PM.png" alt="Screen-Shot-2022-01-20-at-3.28.01-PM" width="600" height="400" loading="lazy"></p>
<p>复制 access 代码并将其粘贴到合适的位置。</p>
<p>现在准备好发起的请求了！根据文档应该在 body 中传递一个 headers 和一个 <code>files</code> 对象。可选传入一些其他参数，包括一个布尔值，它代表这个 gist 是公共的还是私有的。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-20-at-2.07.23-PM.png" alt="Screen-Shot-2022-01-20-at-2.07.23-PM" width="600" height="400" loading="lazy"></p>
<p>API 的 URL 如下：</p>
<pre><code class="language-shell">https://api.github.com
</code></pre>
<p>此操作的端点路径如下：</p>
<pre><code>/gists
</code></pre>
<p>要发起此请求：</p>
<ol>
<li>
<p>将完整的 URL + 路径粘贴到 Hoppscotch 的输入字段中。</p>
</li>
<li>
<p>选择<code>POST</code>请求方法</p>
</li>
<li>
<p>在 Headers 选项卡中，将 accept 设置为 header 并将值设置为 <code>application/vnd.github.v3+json</code></p>
</li>
<li>
<p>在 Body 选项卡中，将内容类型设置为 <code>application/json</code>。然后从一个对象<code>{}</code>开始。</p>
<p>在这个对象内部，将 public <code>boolean</code> 设置为 <code>true</code>。然后将定义属性 <code>files</code>，其值是另一个对象，其键名是新 gist 名称。值应该是另一个其键为 <code>content</code> 的对象。这里的值应该是想要实际添加到要点中的任何值。</p>
<p>可以复制/粘贴以下代码：</p>
</li>
</ol>
<pre><code class="language-javascript">{
  "public": true, 
  "files": {
    "postgist.txt": {
      "content": "Adding a GIST via the API!!"
    }
  }
}
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-24-at-2.35.57-PM.png" alt="Screen-Shot-2022-01-24-at-2.35.57-PM" width="600" height="400" loading="lazy"></p>
<p>5. 在 Authorization 选项卡中，将授权类型设置为 <code>Basic Auth</code>。输入 GitHub 用户名并传入在密码字段中创建的个人访问令牌。</p>
<p>在执行这个之后，会得到一个很长的响应。检查 gist 是否已创建的一种简单方法是访问 GitHub 中的 Gist。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-24-at-2.39.27-PM.png" alt="Screen-Shot-2022-01-24-at-2.39.27-PM" width="600" height="400" loading="lazy"></p>
<p>可以看到成功添加了一个 Gist！</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-24-at-2.39.58-PM.png" alt="Screen-Shot-2022-01-24-at-2.39.58-PM" width="600" height="400" loading="lazy"></p>
<h3 id="patch">如何创建 PATCH  请求</h3>
<p>来更新刚刚创建的 Gist 的标题和描述。请记住：<code>PATCH</code> 允许更新资源的一部分，而不是整个资源。我们没有传入的任何内容都将保持不变。</p>
<p>在创建 Gist 时实际上并没有提供描述，因此我们可以使用 patch 创建一个。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-20-at-3.35.56-PM.png" alt="Screen-Shot-2022-01-20-at-3.35.56-PM" width="600" height="400" loading="lazy"></p>
<p>API 的 URL：</p>
<pre><code class="language-shell">https://api.github.com
</code></pre>
<p>此特定操作的端点路径如下：</p>
<pre><code>/gists/{gist_id}
</code></pre>
<p>要创建此请求：</p>
<ol>
<li>在 Hoppscotch 的输入字段中粘贴完整的 URL + 路径。获取要更新的 gist 的 <code>Gist ID</code>。可以通过转到 GitHub 中的 Gist 并复制 URL 末尾的字母数字字符串来找到 ID。</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-20-at-3.50.13-PM.png" alt="Screen-Shot-2022-01-20-at-3.50.13-PM" width="600" height="400" loading="lazy"></p>
<p>2.  选择 <code>PATCH</code> 请求方法。</p>
<p>3.   在 Headers 选项卡中，将 accept 设置为 header 并将值设置为 <code>application/vnd.github.v3+json</code>。</p>
<p>4.   在 Authorization 选项卡中，将授权类型设置为 <code>Basic Auth</code>。输入 GitHub 用户名并传递我们在密码字段中创建的个人 access token。</p>
<p>5.   在 Body 选项卡中，传入更新后的描述和标题。代码如下：</p>
<pre><code class="language-javascript">{
  "description": "Adding a description via the API", 
  "files": {
    "postgist.txt": {
      "content": "Adding a GIST via the API!! -- adding this line at the end to make the content slightly longer"
    }
  }
}
</code></pre>
<p>刷新 Gist，会看到标题和描述已经更新！</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-24-at-3.38.35-PM.png" alt="Screen-Shot-2022-01-24-at-3.38.35-PM" width="600" height="400" loading="lazy"></p>
<h3 id="delete">如何发起  DELETE 请求</h3>
<p>要删除创建的 Gist。应该传入 header 和 Gist ID。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-24-at-3.42.30-PM.png" alt="Screen-Shot-2022-01-24-at-3.42.30-PM" width="600" height="400" loading="lazy"></p>
<p>API 的 URL 如下：</p>
<pre><code class="language-shell">https://api.github.com
</code></pre>
<p>此特定操作的端点路径如下：</p>
<pre><code>/gists/{gist_id}
</code></pre>
<p>要创建此请求：</p>
<ol>
<li>在 Hoppscotch 的输入字段中粘贴完整的 URL + 路径。获取要更新的 gist 的 <code>Gist ID</code> 。可以通过转到 GitHub 中的 Gist 并复制 URL 末尾的字母数字字符串来找到 ID。</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-20-at-3.50.13-PM.png" alt="Screen-Shot-2022-01-20-at-3.50.13-PM" width="600" height="400" loading="lazy"></p>
<p>2.   选择 <code>DELETE</code> 请求方法</p>
<p>3.  在 Headers 选项卡中，将 accept 设置为 header 并将值设置为 <code>application/vnd.github.v3+json</code>。</p>
<p>导航到 Gists，会看到已经成功地删除了该资源。</p>
<h3 id="">如何在程序中发起请求</h3>
<p>使用 Hoppscotch 是因为它可以方便的发起请求，而无需启动程序或下载任何内容。</p>
<p>如果想在 JavaScript/React 应用程序中发出请求，可以使用 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch">Javascript fetch</a> 或 <a href="https://axios-http.com/docs/intro">Axios</a>。</p>
<p>有关如何发起使用 HTTP 请求方法和 API 的简单应用程序的步骤演示，<a href="https://www.youtube.com/watch?v=7xu7FnKh54M&amp;t=334s">可以查看我在 youtube 上的视频，我们在其中创建了一个 Web 应用程序，该应用程序通过 API 显示有关所有国家/地区的信息 .</a></p>
<h2 id="">你做到了！</h2>
<p>如果你正在阅读本文，请继续给自己点赞，因为你已经了解了 Web API、HTTP 协议、客户端-服务器架构——并且还发起了第一个请求。</p>
<p>如果你喜欢我的风格，可以在 <a href="https://www.youtube.com/channel/UCyEnr-lcCUavJzh0uodvG3w">YouTube</a>、[Tik Tok](https:// www.tiktok.com/@camiinthisthang?)、<a href="https://twitter.com/camiinthisthang">Twitter</a> 和 <a href="https://hashnode.com/@camiinthisthang">Hashnode</a> 找到我。还可以通过 <a href="https://camiinthisthang.dev/">我的个人网站</a> 找到代码片段和联系我的方式。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何使用 CSS 绘制圆形、三角形等形状 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：CSS Shapes Explained: How to Draw a Circle, Triangle, and More Using Pure CSS [https://www.freecodecamp.org/news/css-shapes-explained-how-to-draw-a-circle-triangle-and-more-using-pure-css/] ，作者：Thomas Weibenfalk [https://www.freecodecamp.org/news/author/thomas-weibenfalk/] 开始阅读之前。如果你对视频形式免费内容感兴趣。不要错过我的 Youtube 频道，我每周都会发布前端相关的视频。 https://www.youtube.com/user/Weibenfalk ---------- 你也是 Web 开发和 CSS 的初学者吗？有没有好奇过网页上那些漂亮的图形是如何用 CSS 搞定的？没错，你来对地方了。 下面我将解释使用 CSS 创建图形的基础知识。这个话题很宏大！因此，我不会涵盖所有（远非全部）方 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/css-shapes-explained-how-to-draw-a-circle-triangle-and-more-using-pure-css/</link>
                <guid isPermaLink="false">6245bf287f18d1062895c4c1</guid>
                
                    <category>
                        <![CDATA[ CSS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Thu, 31 Mar 2022 07:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/03/delila-ziebart-b0GSCFJ-Gzg-unsplash.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/css-shapes-explained-how-to-draw-a-circle-triangle-and-more-using-pure-css/">CSS Shapes Explained: How to Draw a Circle, Triangle, and More Using Pure CSS</a>，作者：<a href="https://www.freecodecamp.org/news/author/thomas-weibenfalk/">Thomas Weibenfalk</a></p><!--kg-card-begin: markdown--><p>开始阅读之前。如果你对视频形式免费内容感兴趣。不要错过我的 Youtube 频道，我每周都会发布前端相关的视频。</p>
<p><a href="https://www.youtube.com/user/Weibenfalk">https://www.youtube.com/user/Weibenfalk</a></p>
<p>----------</p>
<p>你也是 Web 开发和 CSS 的初学者吗？有没有好奇过网页上那些漂亮的图形是如何用 CSS 搞定的？没错，你来对地方了。</p>
<p>下面我将解释使用 CSS 创建图形的基础知识。这个话题<strong>很宏大</strong>！因此，我不会涵盖所有（远非全部）方法和图形，本文的目的是让你对如何使用 CSS 创建图形有一个基本的了解。</p>
<p>有些图形比常规图形需要更多的技巧。通常我们使用 <strong>width、height、top、right、left、border、bottom、transform</strong> 以及 <strong>:before</strong> 和 <strong>:after</strong> 等伪元素的组合来创建 CSS  图形。我们也可以使用更流行的 CSS 属性来创建图形，比如 <strong>shape-outside</strong> 和 <strong>clip-path。</strong> 下面我会一一介绍。</p>
<h2 id="css">CSS 图形——基本方式</h2>
<p>通过一些 CSS 技巧可以创建基本图形，例如正方形、圆形以及用常规 CSS 创建的三角形。接下来我们具体看一看。</p>
<h3 id="">正方形和长方形</h3>
<p>正方形和长方形是最容易实现的图形。默认情况下，div 就是正方形或长方形。</p>
<p>设置宽度和高度，设置一个背景颜色，如下所示：</p>
<pre><code class="language-css">#square {
    background: lightblue;
    width: 100px;
    height: 100px;
}
</code></pre>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2020/01/square.png" alt="A CSS square" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>CSS 正方形</figcaption>
</figure>
<h3 id="">圆形</h3>
<p>创建一个圆形也很简单。在元素上设置 border-radius。这将在元素上创建圆角。</p>
<p>如果将其设置为 50%，将会变成一个圆形。如果宽度和高度不相等，将得到一个椭圆形。</p>
<pre><code class="language-css">#circle {
    background: lightblue;
    border-radius: 50%;
    width: 100px;
    height: 100px;
}
</code></pre>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2020/01/circle.png" alt="A CSS circle" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>CSS 圆形</figcaption>
</figure>
<h3 id="">三角形</h3>
<p>三角形有点棘手。元素边框的端点相交形成 45 度夹角。如果将元素的宽度和高度设置为零，元素的实际宽度也就是边框的总宽度。</p>
<p>别忘了，元素上的边框边缘组成了元素的对角线。可以用这个思路来创建三角形。将其中一个边框设置为纯色，将其他边框设置为透明色，就形成了一个三角形。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2020/01/borders.png" alt="CSS Borders have angled edges" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>CSS 边框的倾斜边缘</figcaption>
</figure>
<pre><code class="language-css">#triangle {
    width: 0;
    height: 0;
    border-left: 40px solid transparent;
    border-right: 40px solid transparent;
    border-bottom: 80px solid lightblue;
}
</code></pre>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2020/01/triangle.png" alt="A CSS Triangle" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>CSS 三角形</figcaption>
</figure>
<p>如果想让三角形指向另一个方向，可以更改与希望可见的一侧相对应的边框值。另外可以直接使用 <em>transform</em> 属性旋转元素。</p>
<pre><code class="language-css"> #triangle {
     width: 0;
     height: 0;
     border-top: 40px solid transparent;
     border-right: 80px solid lightblue;
     border-bottom: 40px solid transparent;
 }
</code></pre>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2020/01/triangle2.png" alt="Another CSS Triangle" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>另一个 CSS 三角形</figcaption>
</figure>
<p>以上就是对 CSS 基本图形的介绍。如果你想创建其它图形，你可以利用这些基础图形，发挥你的创造力和决心，通过基本的 CSS 属性实现更多图形。</p>
<p>在某些情况下，对于更高级的形状，使用 :after 和 :before 伪选择器也是一个不错的选择。但这超出了本文的范围，因为我的目的是介绍基础知识帮助你入门。</p>
<h3 id="">弊端</h3>
<p><strong>上述方法有一个很大的缺点。</strong> 例如，如果希望文本环绕图形展示，通过常规 HTML div 的背景和边框构成的图形无法做到这点。文本并不会自适应常规 div 元素环绕其展示。它和常规的 div 展示方式相同。</p>
<p>下图展示了三角形以及文本的布局方式。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/textflow-bad.png" alt="textflow-bad" width="600" height="400" loading="lazy"></p>
<p>幸运的是，我们可以利用一些现代 CSS 属性来轻松地做到这一点。</p>
<h2 id="css">CSS 图形——另一种方式</h2>
<p>在 CSS 中有一个叫做 <strong>shape-outside</strong> 的属性。此属性可以使文本环绕图形展示。</p>
<p>对于这个属性，有一些基本的形状：</p>
<p><strong>inset()</strong><br>
<strong>circle()<br>
ellipse()<br>
polygon()</strong></p>
<p><strong>提示</strong>：还可以使用 <strong>clip-path</strong> 属性，用同样的方式创建图形，但它不会像 shape-outside 那样让文本环绕图形。</p>
<p>要使用 shape-outside 属性使文字环绕图形，图形元素必须是浮动的，它还必须具有指定的的宽度和高度。 <strong>这一点很重要！</strong></p>
<p>可以在 <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/shape-outside">此处</a> 阅读更多。下面是从 developer.mozilla.org 链接中摘抄的文本。</p>
<blockquote>
<p>**<code>shape-outside</code>**的 <a href="https://developer.mozilla.org/en-US/docs/Web/CSS">CSS</a> 属性定义了一个可以是非矩形的形状，相邻的内联内容应围绕该形状进行包装。</p>
</blockquote>
<h3 id="inset">inset()</h3>
<p>inset() 类型可以创建能设置偏移量的矩形/正方形，用于文本环绕。它提供了设置环绕文本与图形重叠距离的值。</p>
<p>可以将所有四个方向的偏移量指定为相同值，如：<strong>inset(20px)</strong>。也可以为每个方向单独设置：<strong>inset(20px 5px 30px 10px)</strong>。</p>
<p>偏移量还可以使用其他单位，例如百分比。这些值对应为： <strong>inset(top right bottom left)</strong><em><strong>.</strong></em></p>
<p>如下面的代码示例，将 inset 值指定为顶部 20px、右侧 5px、底部 30px 和左侧 10px。如果想让文本不和正方形重叠，可以不使用 inset()，而是在 div 上设置背景并像往常一样指定大小。</p>
<pre><code class="language-css"> #square {
     float: left;
     width: 100px;
     height: 100px;
     shape-outside: inset(20px 5px 30px 10px);
     background: lightblue;
 }
</code></pre>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2020/01/inset.png" alt="The text is offset by the specified values. In this case 20px at top, 5px to the right, 30px at the bottom and 10 px to the left" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>文本按指定值偏移，在这里，顶部 20 像素，右侧 5 像素，底部 30 像素，左侧 10 像素</figcaption>
</figure>
<p>也可以给 inset() 第二个值，指定图形的的 border-radius，如下所示：</p>
<pre><code class="language-css"> #square {
     float: left;
     width: 100px;
     height: 100px;
     shape-outside: inset(20px 5px 30px 10px round 50px);
     background: lightblue;
 }
</code></pre>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2020/01/inset2.png" alt="border-radius set to 50px on the inset" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>图形的 border-radius 设置为 50px</figcaption>
</figure>
<h3 id="circle">circle()</h3>
<p>在这里，使用 <strong>shape-outside</strong> 属性创建了一个圆形，还需要给 <strong>clip-path</strong> 设置相应的值才能够显示。</p>
<p><strong>clip-path</strong> 属性可以采用与 shape-outside 属性相同的值，因此可以将它设置为 <strong>shape-outside</strong> 的 <strong>circle()</strong>。另外，请注意，在此处的元素上应用了 20px 的边距，以便为文本留出一些空间。</p>
<pre><code class="language-css">#circle {
    float: left;
    width: 300px;
    height: 300px;
    margin: 20px;
    shape-outside: circle();
    clip-path: circle();
    background: lightblue;
}
</code></pre>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2020/01/circle-shape-margin-1.png" alt="Text flows around the shape!" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>文字环绕图形布局！</figcaption>
</figure>
<p>在上面的例子中，没有指定圆的半径。这是因为我希望它与 div 一样大（300px）。如果想为圆指定不同的大小，可以这样手动设置。</p>
<p>circle() 有两个值，第一个值是 radius，第二个值是 position。这些值将确定圆的中心。</p>
<p>在下面的示例中，将 radius 设置为 50%，将圆心移动了 30%。请注意，必须在 radius 值和 position 值之间使用 “at” 一词。</p>
<p>我还在 clip-path 上指定了另一个 position 值。当我将 position 设置为零时，这会将圆形裁成两半。</p>
<pre><code class="language-css"> #circle {
      float: left;
      width: 150px;
      height: 150px;
      margin: 20px;
      shape-outside: circle(50% at 30%);
      clip-path: circle(50% at 0%);
      background: lightblue;
    }
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/circle2.png" alt="circle2" width="600" height="400" loading="lazy"></p>
<h3 id="ellipse">ellipse()</h3>
<p>椭圆形与圆形类似。可以同时定义 X 值和 Y 值，如：<strong>ellipse(25px 50px)</strong>。</p>
<p>和圆一样，也可以加一个 position 值作为最后一个值。</p>
<pre><code class="language-css">   #ellipse {
      float: left;
      width: 150px;
      height: 150px;
      margin: 20px;
      shape-outside: ellipse(20% 50%);
      clip-path: ellipse(20% 50%);
      background: lightblue;
    }
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/ellipse.png" alt="ellipse" width="600" height="400" loading="lazy"></p>
<h3 id="polygon">polygon()</h3>
<p>多边形是定义了不同顶点/坐标的形状。下面创建了一个 T 形，这是我名字中的第一个字母。从坐标 0,0 开始，从左向右移动以创建 T 形。</p>
<pre><code class="language-css">#polygon {
      float: left;
      width: 150px;
      height: 150px;
      margin: 0 20px;
      shape-outside: polygon(
        0 0,
        100% 0,
        100% 20%,
        60% 20%,
        60% 100%,
        40% 100%,
        40% 20%,
        0 20%
      );
      clip-path: polygon(
        0 0,
        100% 0,
        100% 20%,
        60% 20%,
        60% 100%,
        40% 100%,
        40% 20%,
        0 20%
      );
      background: lightblue;
    }
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/polygon_t.png" alt="polygon_t" width="600" height="400" loading="lazy"></p>
<h3 id="images">Images 图片</h3>
<p>还可以使用具有透明背景的图像来创建形状，就像下面这轮美轮美奂的月亮。</p>
<p>这是透明背景的 .png 图像。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/moon.png" alt="moon" width="600" height="400" loading="lazy"></p>
<pre><code class="language-html">&lt;img src="src/moon.png" id="moon" /&gt;
</code></pre>
<pre><code class="language-css">#moon {
      float: left;
      width: 150px;
      height: 150px;
      shape-outside: url("./src/moon.png");
    }
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/01/moon2.png" alt="moon2" width="600" height="400" loading="lazy"></p>
<p>就是这些啦，感谢你阅读本文。</p>
<h2 id="">关于本文作者</h2>
<p>我叫 Thomas Weibenfalk，是来自瑞典的开发者。我经常在 Youtube 频道上创建免费教程，有一些关于 React 和 Gatsby 的高级课程。你可以通过以下链接找到我：</p>
<p>Twitter — <a href="https://twitter.com/weibenfalk">@weibenfalk</a><br>
Weibenfalk on <a href="https://www.youtube.com/c/weibenfalk">Youtube</a><br>
Weibenfalk <a href="https://www.weibenfalk.com">Courses Website</a></p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Arch Linux 手册 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：The Arch Linux Handbook [https://www.freecodecamp.org/news/how-to-install-arch-linux/]，作者：Farhan Hasin Chowdhury [https://www.freecodecamp.org/news/author/farhanhasin/] 如果问开发人员 Linux 是什么，大多数人的回答可能是 Linux 是一个开源操作系统。也有人会站在技术角度称它为内核。 不过，对我来说，Linux 不仅仅是一个操作系统或内核。Linux 代表着自由。可以根据需求自由组合各个部分组成最适合自己的操作系统，这也是 Arch Linux 吸引人的地方。 摘自 arch wiki [https://wiki.archlinux.org/title/Arch_Linux], > Arch Linux 是通用 x86-64 GNU [https://wiki.archlinux.org/title/GNU]/Linux 发行版。Arch采用滚动升级模式，尽全力提供最新的稳定版软件。 初始安装的  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-install-arch-linux/</link>
                <guid isPermaLink="false">6205d64e469075064c41d1ca</guid>
                
                    <category>
                        <![CDATA[ Arch Linux ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Fri, 11 Feb 2022 03:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/02/Arch-Linux-Handbook-1280x612.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/how-to-install-arch-linux/">The Arch Linux Handbook</a>，作者：<a href="https://www.freecodecamp.org/news/author/farhanhasin/">Farhan Hasin Chowdhury</a></p><!--kg-card-begin: markdown--><p>如果问开发人员 Linux 是什么，大多数人的回答可能是 Linux 是一个开源操作系统。也有人会站在技术角度称它为内核。</p>
<p>不过，对我来说，Linux 不仅仅是一个操作系统或内核。Linux 代表着自由。可以根据需求自由组合各个部分组成最适合自己的操作系统，这也是 Arch Linux 吸引人的地方。</p>
<p>摘自 arch <a href="https://wiki.archlinux.org/title/Arch_Linux">wiki</a>,</p>
<blockquote>
<p>Arch Linux 是通用 x86-64 <a href="https://wiki.archlinux.org/title/GNU">GNU</a>/Linux 发行版。Arch采用滚动升级模式，尽全力提供最新的稳定版软件。</p>
<p>初始安装的 Arch 只是一个基本系统，随后用户可以根据自己的喜好安装需要的软件并配置成符合自己理想的系统.</p>
</blockquote>
<p>换句话说，Arch Linux 是针对有经验 Linux 用户的基于 x86-64 架构的优化发行版。它让你对系统拥有完全的选择权和控制权。</p>
<p>可以自主选择所需要的软件包、内核（是的，内核有很多）、boot-loader、桌面环境等等。</p>
<p>你有没有听过有人说，</p>
<blockquote>
<p>嗯——顺便说一句，我用的是 Arch Linux！</p>
</blockquote>
<p>这是因为如果想要在机器上安装 Arch Linux，需要先了解 Linux 发行版的每个部分是如何工作的。所以如果使用的是 Arch Linux，证明已经很熟悉 Linux。</p>
<p>安装 Arch Linux 与安装 Fedora 或 Ubuntu 之类的系统并没有太大不同。区别是 Arch Linux 必须手动完成各个步骤，而不是交给安装程序。完成了这个过程，自然了解了其他发行版的安装步骤。</p>
<p>在本文中，将介绍在电脑上安装和配置 Arch Linux 的整个过程。最后，还会讨论一些常见任务操作和故障排除技巧。</p>
<p>跟我来，带你深入浅出了解 Arch Linux。</p>
<h2 id="">目录</h2>
<ul>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#some-assumptions-i-m-making">一些预设</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-create-a-bootable-arch-linux-usb-drive">如何创建可引导的 Arch Linux U 盘</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-prepare-your-computer-for-installing-arch-linux">准备安装 Arch Linux</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-install-arch-linux">如何安装 Arch Linux</a>
<ul>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-set-the-console-keyboard-layout-and-font">如何设置控制台键盘布局和字体</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-verify-the-boot-mode">如何验证引导模式</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-connect-to-the-internet">如何连接到互联网</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-update-the-system-clock">如何更新系统时钟</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-partition-the-disks">如何对磁盘进行分区</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-format-the-partitions">如何格式化分区</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-mount-the-file-systems">如何挂载文件系统</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-configure-the-mirrors">如何配置镜像源</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-install-arch-linux-base-system">如何安装 Arch Linux 基础系统</a></li>
</ul>
</li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-configure-arch-linux">如何配置 Arch Linux</a>
<ul>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-generate-the-fstab-file">如何生成 Fstab 文件</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-login-to-the-newly-installed-system-using-arch-chroot">如何使用 Arch-Chroot 登录新安装的系统</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-configure-the-time-zone">如何配置时区</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-configure-the-localization">如何配置本地化</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-configure-the-network">如何配置网络</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-set-the-root-password">如何设置 root 密码</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-create-a-non-root-user">如何创建非 root 用户</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-install-microcode">如何安装 Microcode</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-install-and-configure-a-boot-loader">如何安装和配置 Boot Loader</a></li>
</ul>
</li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-install-xorg">如何安装 Xorg</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-install-graphics-drivers">如何安装图形驱动程序</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-install-a-desktop-environment">如何安装桌面环境</a>
<ul>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-install-gnome">如何安装 GNOME 桌面</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-install-plasma">如何安装 Plasma 桌面</a></li>
</ul>
</li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-finalize-the-installation">如何完成安装</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-switch-between-desktop-environments">如何在桌面环境之间切换</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-manage-packages-using-pacman">使用 Pacman 管理包</a>
<ul>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-install-packages-using-pacman">如何使用 Pacman 安装软件包</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-remove-packages-using-pacman">如何使用 Pacman 删除软件包</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-upgrade-packages-using-pacman">如何使用 Pacman 升级软件包</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-search-for-packages-using-pacman">如何使用 Pacman 搜索软件包</a></li>
</ul>
</li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-use-aur-in-arch-linux">如何在 Arch Linux 使用 AUR</a>
<ul>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-install-packages-using-a-helper">如何使用 Helper 安装软件包</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-install-packages-manually">如何手动安装软件包</a></li>
</ul>
</li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-troubleshoot-common-problems">如何解决常见问题</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#how-to-use-the-live-arch-iso-as-a-rescue-media">如何使用 Live Arch ISO 作为恢复媒介</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#further-reading">拓展阅读</a></li>
<li><a href="https://chinese.freecodecamp.org/news/how-to-install-arch-linux/#conclusion">结论</a></li>
</ul>
<h2 id="some-assumptions-i-m-making">一些预设</h2>
<p>在进入本教程的核心之前，先澄清一些事情。为了使本文通俗易懂，我对你和你的系统做了以下假设：</p>
<ul>
<li>对 Arch Linux 有基本的了解
<ul>
<li><a href="https://wiki.archlinux.org/title/Arch_Linux">Arch Linux</a></li>
<li><a href="https://wiki.archlinux.org/title/Frequently_asked_questions">常见问题</a></li>
<li><a href="https://wiki.archlinux.org/title/Arch_compared_to_other_distributions">Arch 与其他发行版的比较</a></li>
</ul>
</li>
<li>你的电脑使用的是 UEFI，而不是 BIOS</li>
<li>你有一个足够大（4GB）的 U 盘，可以用它来启动 Linux</li>
<li>有安装 Linux（Ubuntu/Fedora）的经验</li>
<li>有足够的空间在硬盘或 SSD上安装 linux</li>
</ul>
<p>差不多就是这样。如果你具备以上所有条件，可以开始了。</p>
<h2 id="archlinuxu">如何创建可引导的 Arch Linux U 盘</h2>
<p>要下载 Arch Linux，请访问 <a href="https://archlinux.org/download/">https://archlinux.org/download/</a> 并下载最新版本（本文撰写时为 2022.01.01）。ISO的大小应该在 870 兆字节左右。</p>
<p>下载后，需要将其写入 U 盘。可以使用 <a href="https://getfedora.org/en/workstation/download/">Fedora Media Writer</a>。在系统上下载并安装应用程序。连接 U 盘打开应用程序：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/image-48.png" alt="image-48" width="600" height="400" loading="lazy"></p>
<p>单击“自定义镜像”，并使用文件浏览器选择下载的 Arch Linux ISO 文件。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/image-49.png" alt="image-49" width="600" height="400" loading="lazy"></p>
<p>程序现在可以选择一个连接的 U 盘。如果电脑连接了多个 USB 存储设备，请务必小心选择正确的 U 盘。现在点击“写入磁盘”按钮，等待进程完成。</p>
<h2 id="archlinux">准备安装 Arch Linux</h2>
<p>在此步骤中，必须对系统进行一些更改，否则 Arch Linux 可能无法启动或正常运行。</p>
<p>第一个要改的是禁用 UEFI 配置中的安全启动。此功能有助于启动期间防止恶意软件攻击，但也会阻止 Arch Linux 安装程序的启动。</p>
<p>如何禁用此功能的详细说明因主板或笔记本电脑品牌而异。需要你自己在互联网中搜索来找到相应的方法。</p>
<p>第二个操作仅在安装和 Windows 共存的 Arch Linux 双系统时才适用。Windows有一个名为“快速启动”的功能，它通过部分休眠来缩短计算机的启动时间。</p>
<p>通常这是一个很好的特性，它可以防止双引导配置中的任何其他操作系统在此过程中访问硬盘。</p>
<p>要禁用此功能，请打开“开始”菜单并搜索“选择电源计划”，如下所示：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/choose-a-power-plan.png" alt="choose-a-power-plan" width="600" height="400" loading="lazy"></p>
<p>然后在下一个窗口中，单击左侧边栏中的“选择电源按钮的功能”：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/image-54.png" alt="image-54" width="600" height="400" loading="lazy"></p>
<p>然后在下一个窗口中，将看到“关机设置”列表，“启用快速启动（推荐）”选项应显示为只读。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/image-55.png" alt="image-55" width="600" height="400" loading="lazy"></p>
<p>单击顶部的“更改当前不可用的设置”，然后更改设置。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/image-56.png" alt="image-56" width="600" height="400" loading="lazy"></p>
<p>取消勾选“启用快速启动（推荐）”选项，然后按下底部的“保存修改”按钮。从现在开始，启动过程可能需要一些额外的时间，但这一切都是值得的。</p>
<p>在本文中，我将安装 Arch Linux 作为默认操作系统。所以我将把我的整个磁盘空间分配给它。</p>
<p>如果想让 Arch Linux 和 Windows 并存，这里有一篇专门的<a href="https://www.freecodecamp.org/news/how-to-dual-boot-any-linux-distribution-with-windows/">文章</a>介绍这个话题。在那篇文章中，有一个<a href="https://www.freecodecamp.org/news/how-to-dual-boot-any-linux-distribution-with-windows/">章节</a>，详细讨论了分区过程。</p>
<h2 id="archlinux">如何安装 Arch Linux</h2>
<p>假设已经准备好可启动的 U 盘并且计算机配置正确，就可以开始从 U 盘启动了。开启从 U 盘启动的方法因机器而异。</p>
<p>在我的机器上，在启动过程中按 F12 键会将会进入到可启动设备列表。从那里可以选择可启动 U 盘。可能你已经知道适合你的电脑的方式，或者可能需要进行一些研究。</p>
<p>进入所有的可启动设备列表，选择要启动的 U 盘，会显示以下菜单：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_12_01_2022_18_39_29.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_12_01_2022_18_39_29" width="600" height="400" loading="lazy"></p>
<p>从列表中选择第一个并等待 Arch 安装程序完成启动。完全启动后，会看到如下内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_12_01_2022_18_50_39.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_12_01_2022_18_50_39" width="600" height="400" loading="lazy"></p>
<p>就这样。与你熟悉的其他操作系统不同，Arch 安装程序没有任何可以自动安装的图形用户界面。</p>
<p>它需要你投入时间和精力并逐个配置每个部分。这听起来可能有点难，但老实说，如果弄清楚每一步，安装 Arch Linux 会很有趣。</p>
<h3 id="">如何设置控制台键盘布局和字体</h3>
<p>正如我已经说过的，Arch 安装程序没有图形用户界面，因此需要输入大量指令。配置键盘布局和漂亮的字体可以使安装过程不那么令人沮丧。</p>
<p>默认情况下，控制台是标准的美式键盘布局。这对大多数人来说应该没问题，但如果碰巧你有一个不同的键盘，可以改变它。</p>
<p>所有可用的键盘映射通常以 <code>map.gz</code> 文件的形式保存在 <code>/usr/share/kbd/keymaps</code> 目录中。可以使用 <code>ls</code> 命令查看列表：</p>
<pre><code>ls /usr/share/kbd/keymaps/**/*.map.gz
</code></pre>
<p>这将列出所有可用的键盘映射：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_15_58_28-1.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_15_58_28-1" width="600" height="400" loading="lazy"></p>
<p>假如你的是 Mac-US 键盘布局，从该列表中找到相应的 <code>map.gz</code> 文件，即 <code>mac-us.map.gz</code> 文件。</p>
<p>可以使用 <code>loadkeys</code> 命令加载所需的键盘映射。要将 <code>mac-us.map.gz</code> 设置为默认值，请执行以下命令：</p>
<pre><code>loadkeys mac-us
</code></pre>
<p>如果不喜欢默认字体，也可以更改控制台字体。就像键盘映射一样，控制台字体保存在 <code>/usr/share/kbd/consolefonts</code> 中，可以使用 <code>ls</code> 命令列出：</p>
<pre><code>ls /usr/share/kbd/consolefonts
</code></pre>
<p>这将列出所有可用的字体：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_16_08_01.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_16_08_01" width="600" height="400" loading="lazy"></p>
<p>可以使用 <code>setfont</code> 命令设置。例如，如果要将 <code>drdos8x16</code> 设置为默认值，请执行以下命令：</p>
<pre><code>setfont drdos8x16
</code></pre>
<p><code>loadkeys</code> 和 <code>setfont</code> 命令都是包含基本 Linux 键盘工具的 <code>kbd</code> 包的一部分。他们都有很棒的 <a href="https://kbd-project.org/#documentation">文档</a>，所以如果想了解更多信息，请随时查看。</p>
<h3 id="">如何验证引导模式</h3>
<p>现在已配置好控制台，下一步是确保已在 UEFI 模式下启动，而不是在 BIOS 模式下启动。</p>
<p>老实说，这一步对我来说似乎没有必要，因为它在实时启动菜单中字面意思是“x86_64 UEFI”。但是官方的 Arch <a href="https://wiki.archlinux.org/title/installation_guide#Verify_the_boot_mode">安装指南</a>建议我们验证一下。</p>
<p>要验证引导模式，请执行以下命令：</p>
<pre><code>ls /sys/firmware/efi/efivars
</code></pre>
<p>如果你处于 UEFI 模式，它会在你的屏幕上列出一堆文件：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_17_18_34.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_17_18_34" width="600" height="400" loading="lazy"></p>
<p>在 BIOS 引导的情况下，<code>/sys/firmware</code> 目录中甚至不存在<code>efi</code> 目录。如果处于 UEFI 模式，（如果正确地遵循了一切，应该是）继续下一步。</p>
<h3 id="">如何连接到互联网</h3>
<p>与许多其他实时发行版不同，Arch 实时环境并没有内置所有必要的软件包。它包含许多可用于安装系统其余部分的最低限度的软件包。所以，一个有效的互联网连接是必须的。</p>
<p>如果使用的是有线网络，那么从一开始应该就有了有效的互联网连接。要对其进行测试，请 ping 任何公共地址：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_17_40_04.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_17_40_04" width="600" height="400" loading="lazy"></p>
<p>我正在使用 VirtualBox 制作这些屏幕截图，因此已经通过有线连接连接了互联网。但如果使用的是无线连接，事情会变得有点棘手。</p>
<p>实时环境带有 <code>iwd</code> 或 <a href="https://wiki.archlinux.org/title/Iwd">iNet wireless daemon</a> 包。可以使用此软件包连接到附近的无线网络。</p>
<p>首先，执行以下命令：</p>
<pre><code>iwctl
</code></pre>
<p>这将启动一个交互式提示，如下所示：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_17_59_34.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_17_59_34" width="600" height="400" loading="lazy"></p>
<p>现在执行以下命令以查看可用无线设备的列表：</p>
<pre><code>device list
</code></pre>
<p>这将打印出所有可用的无线设备列表。无线设备是指连接到你计算机的任何无线适配器。这里假设设备名称是 <code>wlan0</code> 。</p>
<p>要使用找到的设备扫描附近的无线网络，请执行以下命令</p>
<pre><code>station wlan0 scan
</code></pre>
<p>你可能认为此命令将打印出所有附近网络的列表，但事实并非如此。要查看网络列表，请执行以下命令：</p>
<pre><code>station wlan0 get-networks
</code></pre>
<p>现在假设你的 WI-FI 网络名称为“Skynet”，可以通过执行以下命令连接到它：</p>
<pre><code>station wlan0 connect Skynet
</code></pre>
<p><code>iwctl</code> 程序会提示输入 wi-fi 密码。输入密码，一旦连接到网络，输入<code>exit</code> 按 Enter 退出程序。再次尝试 ping 公共地址并确保 Internet 工作正常。</p>
<h3 id="">如何更新系统时钟</h3>
<p>在 Linux 中，NTP 或网络时间协议用于通过网络同步计算机系统时钟。可以使用 <code>timedatectl</code> 命令在 Arch 实时环境中启用 NTP：</p>
<pre><code>timedatectl set-ntp true
</code></pre>
<p>该命令将会卡住几秒。如果没有看到命令光标再次出现，请尝试按 Enter。过去我曾多次遇到过这种情况。</p>
<h3 id="">如何对磁盘进行分区</h3>
<p>这可能是整个安装过程中最危险的一步——因为如果弄乱了分区，数据就会丢失。所以我建议不要立即执行本节内容。相反，请先阅读整个部分，然后再回到这里。</p>
<p>要开始分区，必须了解连接到计算机的磁盘。可以使用 <code>fdisk</code>，它是一个对话驱动的程序，用于创建和操作分区表。</p>
<pre><code>fdisk -l
</code></pre>
<p>此命令将列出计算机上所有可用设备的分区表。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_19_53_34.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_19_53_34" width="600" height="400" loading="lazy"></p>
<p>如你所见，有两个设备连接到我的计算机上（实际上是虚拟机）。根据你的设备数量，此列表可能会更长，在查看列表时忽略任何以<code>rom</code>、<code>loop</code> 或 <code>airoot</code> 结尾的设备。不能使用这些设备进行安装。</p>
<p>所以只剩下了 <code>/dev/sda</code> 设备。记住，在你的机器上可能完全不同。例如，如果你有 NVME 驱动器，可能会看到 <code>/dev/nvme0n1</code>。</p>
<p>一旦决定使用哪个设备，最好检查该设备内是否存在任何现有分区。可以使用 <code>fdisk</code> 命令：</p>
<pre><code>fdisk /dev/sda -l
</code></pre>
<p>记得用你的设备名的替换<code>/dev/sda</code>。此命令将列出给定设备内的所有分区。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_13_14.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_13_14" width="600" height="400" loading="lazy"></p>
<p>尽管此设备中没有分区，但在实际中，之前可能已经创建了分区。这些分区将显示为 <code>/dev/sda1</code>、<code>/dev/sda2</code> 或在有 NVME 驱动器的情况下显示为 <code>/dev/nvme0n1p1</code>、<code>/dev/nvme0n1p2</code> 等等。</p>
<p><code>fdisk</code> 程序可以做的不仅仅是列出分区。查阅 <a href="https://wiki.archlinux.org/title/Fdisk">相应的 ArchWiki 页面</a> 以了解可以使用该程序执行的操作。</p>
<p>还有另一个程序 <code>cfdisk</code>，它是一个基于 <a href="https://en.wikipedia.org/wiki/Curses_(programming_library)">curses- (programming library)</a> 的 Linux 磁盘分区表操作工具。在功能上与 <code>fdisk</code> 相似，基于 curses 意味着它有一个界面，更易于使用。</p>
<p>执行以下命令在设备上启动 <code>cfdisk</code>：</p>
<pre><code>cfdisk /dev/sda
</code></pre>
<p>记得用你的设备替换<code>/dev/sda</code>。如果设备有以前创建的分区表，那么 <code>cfdisk</code> 将直接显示分区列表。否则，将开始选择分区表类型：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_22_55.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_22_55" width="600" height="400" loading="lazy"></p>
<p>为基于 UEFI 的系统选择 <code>gpt</code>。接下来，将显示设备上的分区和可用空间列表：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_24_09.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_24_09" width="600" height="400" loading="lazy"></p>
<p>可以使用向上/向下箭头键沿设备列表上下移动，使用向左/向右箭头键沿不同操作左右移动。</p>
<p>要安装 Arch 或任何其他 Linux 发行版，需要三个独立的分区。如下：</p>
<ul>
<li>EFI 系统分区——用于存储 UEFI 固件所需的文件。</li>
<li>ROOT – 用于安装发行版本身。</li>
<li>SWAP – 用作内存交换分区。</li>
</ul>
<p>确保正确的分区/可用空间在列表中突出显示，然后选择 <code>[ New ]</code> 操作。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_37_04.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_37_04" width="600" height="400" loading="lazy"></p>
<p>填写所需的分区大小。可以使用 M 表示兆字节，G 表示吉字节，T 表示太字节。</p>
<p>对于 EFI 系统分区，应该至少分配 500MB。输入所需空间后，按 Enter 完成。更新后的分区列表可能如下所示：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_37_29.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_37_29" width="600" height="400" loading="lazy"></p>
<p>EFI 系统分区是一种特殊类型的分区。它必须采用特定的类型和格式。要更改默认类型，请保持新创建的分区突出显示并从操作列表中选择 <code>[ Type ]</code> 。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_39_24.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_39_24" width="600" height="400" loading="lazy"></p>
<p>从这个长长的类型列表中，突出显示 <code>EFI System</code> 并按 Enter。列表中的分区类型会相应更新：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_40_37.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_40_37" width="600" height="400" loading="lazy"></p>
<p>接下来是 root 分区。突出显示剩余的可用空间并再次选择 <code>[ New ]</code> 。这次分配 10GB 给这个分区。root 分区的理想大小取决于个人需要。就我而言，我为所有 Linux 安装的 root 分区分配了至少 100GB 的空间。</p>
<p>无需更改此分区的类型。默认的 <code>Linux filesystem</code> 就可以了。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_43_14.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_43_14" width="600" height="400" loading="lazy"></p>
<p>使用剩余空间创建最后一个分区，并从菜单中将其类型更改为  <code>Linux swap</code>：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_45_57.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_45_57" width="600" height="400" loading="lazy"></p>
<p>交换分区的理想大小是一个有争议的问题。就我个人而言，我的机器上没有交换分区。我的内存足够大。但如果以后有需要，可以使用 <code>swapfile</code> 来代替。无论如何，设备的最终状态应如下所示：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_48_49.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_20_48_49" width="600" height="400" loading="lazy"></p>
<p>如果对设置感到满意，请从操作列表中突出显示 [ Write ]，然后按 Enter。该程序将询问是否要保留这些更改。如果同意，必须输入  <code>yes</code>  并按 Enter。更改分区表后，选择 <code>[ Quit ]</code> 退出程序。</p>
<p>对于那些尝试将 Arch Linux 与 Windows 一起安装的人，我想提一提的是，在这种情况下，EFI 系统分区应该已经存在于设备中。所以不要碰那个。只需创建其他分区并继续。</p>
<h3 id="">如何格式化分区</h3>
<p>现在已经创建了分区，需要格式化它们。可以使用 <code>mkfs</code> 和 <code>mkswap</code> 程序。在格式化之前，执行以下命令查看分区列表：</p>
<pre><code>fdisk /dev/sda -l
</code></pre>
<p>这次将看到三个新创建的分区及其详细信息：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_21_02_23.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_21_02_23" width="600" height="400" loading="lazy"></p>
<p>记住设备名称，例如 <code>/dev/sda1</code>、<code>/dev/sda2</code>、<code>/dev/sda3</code> 等。EFI 系统分区必须为 FAT32 格式。执行以下命令将分区格式化为 FAT32 格式：</p>
<pre><code>mkfs.fat -F32 /dev/sda1
</code></pre>
<p>下一个是 root 分区。它可以有多种格式，但我习惯使用 EXT4 。使用以下命令格式化为 EXT4 分区：</p>
<pre><code>mkfs.ext4 /dev/sda2
</code></pre>
<p>此操作可能需要一些时间才能完成，具体取决于分区大小。最后是交换分区。使用以下命令对其进行格式化：</p>
<pre><code>mkswap /dev/sda3
</code></pre>
<p>这样，就完成了为安装准备分区的过程。</p>
<h3 id="">如何挂载文件系统</h3>
<p>现在已经创建并格式化了分区，可以挂载它们了。可以使用 <code>mount</code> 命令来挂载分区：</p>
<pre><code>mount /dev/sda2 /mnt
</code></pre>
<p>希望记得之前 <code>/dev/sda2</code> 分区被创建为 root 分区。Linux 中的 <code>/mnt</code> 挂载点用于临时挂载存储设备。由于我们只需要挂载安装 Arch Linux 的分区，所以使用 <code>/mnt</code> 挂载点。</p>
<p>而对于交换分区，不能像其他分区一样挂载它。需要明确告诉 Linux 将此分区用作交换。执行以下命令：</p>
<pre><code>swapon /dev/sda3
</code></pre>
<p>可能已经猜到了，<code>swapon</code> 命令告诉系统在此设备上进行交换。我们将在后面的部分中使用 EFI 系统分区。目前，安装这两个分区就足够了。</p>
<h3 id="">如何配置镜像源</h3>
<p>在安装 Arch Linux 之前还有最后一步，那就是配置镜像源。镜像源是位于世界各地不同地点的服务器，用于为附近的人提供服务。</p>
<p>安装程序附带 Reflector，这是一个 Python 脚本，用于检索 <a href="https://archlinux.org/mirrors/status/">Arch Linux 镜像状态</a> 页面的最新镜像列表。要打印出最新的镜像列表，执行以下命令：</p>
<pre><code>reflector
</code></pre>
<p>如果网速较慢，可能会遇到如下错误消息：</p>
<pre><code>failed to rate http(s) download (https://arch.jensgutermuth.de/community/os/x86_64/community.db): Download timed out after 5 second(s).
</code></pre>
<p>当下载信息的时间超过默认超时（5 秒）时，就会发生这种情况。</p>
<p>可以使用 <code>--download-timeout</code> 选项来解决此问题：</p>
<pre><code>reflector --download-timeout 60
</code></pre>
<p>现在 reflector 将等待整整一分钟，才会失败。一长串镜像源地址会出现在屏幕上：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_21_36_15-1.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_21_36_15-1" width="600" height="400" loading="lazy"></p>
<p>遍历整个列表以找到附近的 mirrors 会很麻烦。可以用 reflector 来搞定。</p>
<p>Reflector 可以根据的给定约束生成 mirrors 列表。例如，想要一个最近 12 小时内同步过的 mirrors 列表，这些 mirrors 位于印度或新加坡（这两个离我的位置最近），并按下载速度对镜像进行排序。</p>
<p>可以用 reflector 这样做：</p>
<pre><code>reflector --download-timeout 60 --country India,Singapore --age 12 --protocol https --sort rate
</code></pre>
<p>找到的服务器将像以前一样列出：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_21_45_25.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_21_45_25" width="600" height="400" loading="lazy"></p>
<p>像这样打印出 mirror 列表是不够的。必须将列表保存在 <code>/etc/pacman.d/mirrorlist</code> 位置。Pacman，是 Arch Linux 的默认包管理器，可以使用这个文件来查找 mirror。</p>
<p>在覆盖默认 mirror 列表之前，先对其进行复制：</p>
<pre><code>cp /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak
</code></pre>
<p>现在使用 <code>--save</code> 选项执行 reflector 命令，如下所示：</p>
<pre><code>reflector --download-timeout 60 --country India,Singapore --age 12 --protocol https --sort rate --save /etc/pacman.d/mirrorlist
</code></pre>
<p>此命令将生成 mirror 列表并覆盖默认列表。现在已准备好安装基本的 Arch Linux 系统。</p>
<h3 id="archlinux">如何安装 Arch Linux 基础系统</h3>
<p>在安装基础系统之前，最好根据新的 mirror 列表更新包缓存。请执行以下命令：</p>
<pre><code>pacman -Sy
</code></pre>
<p>Arch Linux 的 pacman 程序就像 Ubuntu 的 <code>apt</code> 或 Fedora 的 <code>dnf</code> 程序。<code>-S</code> 选项表示同步，相当于 <code>apt</code> 或 <code>dnf</code> 包管理器中的 <code>install</code>。</p>
<p>更新过程完成后，可以使用 <code>pacstrap</code> 脚本安装 Arch Linux 系统。执行以下命令开始安装：</p>
<pre><code>pacstrap /mnt base base-devel linux linux-firmware sudo nano ntfs-3g networkmanager
</code></pre>
<p><code>pacstrap</code> 脚本可以将软件包安装到指定的新 root 目录。你可能还记得，root 分区被挂载在 <code>/mnt</code> 挂载点上，所以这个脚本使用了 <code>/mnt</code> 参数。然后，传入要安装的软件包名称：</p>
<ul>
<li><code>base</code> - 定义基本 Arch Linux 安装的最小软件包集。</li>
<li><code>base-devel</code>——从源代码构建软件所需的软件包组。</li>
<li><code>linux</code>——内核本身。</li>
<li><code>linux-firmware</code>——通用硬件的驱动程序。</li>
<li><code>sudo</code> - 以 root 身份运行命令</li>
<li><code>nano</code> - 具有一些增强功能的 pico 编辑器克隆。</li>
<li><code>ntfs-3g</code> – 使用 NTFS 驱动器所需的 NTFS 文件系统驱动程序和实用程序。</li>
<li><code>networkmanager</code> - 为系统提供检测和配置以自动连接到网络。</li>
</ul>
<p>我想澄清一下，这七个包的不是强制性的。要安装功能正常的 Arch Linux，只需要 <code>base</code>、<code>linux</code> 和 <code>linux-firmware</code> 包。但是考虑到其他包也是必需的，为什么不一起装完它们。</p>
<p>根据网速快慢，安装过程可能需要一段时间。坐下来放松一下，直到 pacstrap 完成它的工作。完成后，将看到如下内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_22_57_54.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_22_57_54" width="600" height="400" loading="lazy"></p>
<p>恭喜，你已经成功地在你的电脑上安装了 Arch Linux。现在剩下要做的就是配置系统。</p>
<h2 id="archlinux">如何配置 Arch Linux</h2>
<p>安装 Arch Linux 没那么难吧？ 事实上，在我看来，安装比配置更简单。这里有很多事情要做。所以让我们开始吧。</p>
<h3 id="fstab">如何生成 Fstab 文件</h3>
<p>根据 <a href="https://wiki.archlinux.org/title/Fstab">ArchWiki</a>,</p>
<blockquote>
<p><a href="https://man.archlinux.org/man/fstab.5">fstab(5)</a> 文件可用于定义磁盘分区，各种其他块设备或远程文件系统应如何装入文件系统。</p>
</blockquote>
<p>在 Ubuntu 或 Fedora 等其他发行版中，它会在安装过程中自动生成。但是，在 Arch 上，必须手动完成。执行以下命令：</p>
<pre><code>genfstab -U /mnt &gt;&gt; /mnt/etc/fstab
</code></pre>
<p><code>genfstab</code> 程序可以检测给定挂载点以下的所有当前挂载，并以兼容 fstab 的格式将它们打印到标准输出。所以 <code>genfstab -U /mnt</code> 将输出 <code>/mnt</code> 挂载点下的所有当前挂载。可以使用 <code>&gt;&gt;</code> 操作符将该输出保存到 <code>/mnt/etc/fstab</code> 文件中。</p>
<h3 id="archchroot">如何使用 Arch-Chroot 登录新安装的系统</h3>
<p>现在登录的是实时环境，而不是新安装的系统。</p>
<p>要继续配置新安装的系统，必须先登录。执行以下命令：</p>
<pre><code>arch-chroot /mnt
</code></pre>
<p><code>arch-chroot</code> bash 脚本是 <code>arch-install-scripts</code> 软件包的一部分，可以无需重新启动即可更改为新安装系统的 <code>root</code> 用户。酷！</p>
<h3 id="">如何配置时区</h3>
<p>切换 root 后，首先要配置的是时区。要查看所有可用时区的列表，请执行以下命令：</p>
<pre><code>ls /usr/share/zoneinfo
</code></pre>
<p>所有主要时区都在目录中。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_23_45_19.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_23_45_19" width="600" height="400" loading="lazy"></p>
<p>我住在位于亚洲区的孟加拉国达卡。如果列出亚洲的内容，可以看到达卡：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_23_45_44.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_23_45_44" width="600" height="400" loading="lazy"></p>
<p>在 <code>/etc/localtime</code> 位置创建文件的符号链接，将 Asia/Dhaka 设置为我的默认时区：</p>
<pre><code>ln -sf /usr/share/zoneinfo/Asia/Dhaka /etc/localtime
</code></pre>
<p><code>ln</code> 命令用于创建符号链接。<code>-sf</code> 选项分别表示软链接和强制执行。</p>
<h3 id="">如何配置本地化</h3>
<p>现在需要配置语言。Arch Linux 也有一个简单的设置方法。</p>
<p>首先，根据本地化信息编辑 <code>etc/locale.gen</code> 文件。在 nano 文本编辑器中打开文件：</p>
<pre><code>nano /etc/locale.gen
</code></pre>
<p>会看到一长串语言列表：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_23_46_29.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_23_46_29" width="600" height="400" loading="lazy"></p>
<p>需要取消要启用的语言的注释。我通常只需要英语和孟加拉语。因此，我将找到 <code>en_US.UTF-8 UTF-8</code>、<code>bn_BD UTF-8</code> 和 <code>bn_IN UTF-8</code> 语言。按 Ctrl + O 保存文件，然后按 Ctrl + X 组合键退出 nano。</p>
<p>现在执行以下命令：</p>
<pre><code>locale-gen
</code></pre>
<p><code>locale-gen</code> 命令将读取 <code>/etc/locale.gen</code> 文件并生成相应地语言环境。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_23_57_55.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_13_01_2022_23_57_55" width="600" height="400" loading="lazy"></p>
<p>现在已经启用了多种语言，需要告诉 Arch Linux 默认使用哪一种。打开 <code>/etc/locale.conf</code> 文件并向其中添加以下行：</p>
<pre><code>LANG=en_US.UTF-8
</code></pre>
<p>至此，配置语言环境操作完毕。可以随时返回到 <code>/etc/locale.gen</code> 文件并从中添加或删除语言。只要记住在执行此操作时运行 <code>locale-gen</code> 即可。</p>
<p>如果在安装的第一步中对控制台键盘映射进行了更改，现在也需要保留它们。打开 <code>/etc/vconsole.conf</code> 文件并在其中添加你需要的键盘映射。</p>
<p>例如，如果在第一步中将默认键盘映射更改为 <code>mac-us</code>，那么需要将以下行添加到 <code>vconsole.conf</code> 文件中：</p>
<pre><code>KEYMAP=mac-us
</code></pre>
<p>现在，每次使用虚拟控制台时，它都会有正确的键盘映射，而不必每次都对其进行配置。</p>
<h3 id="">如何配置网络</h3>
<p>在任何 Linux 发行版中手动配置网络都非常棘手。这就是为什么我建议在系统安装过程中安装 <code>networkmanager</code> 软件包的原因。如果之前安装过了，就可以跳过安装步骤。否则，现在使用 <code>pacman</code> 安装软件包：</p>
<pre><code>pacman -S networkmanager
</code></pre>
<p>Pacman 是一个包管理器。稍后将了解更多有关它的信息。现在先为计算机设置主机名。主机名是为识别网络上的机器而创建的唯一名称，写入<code>/etc/hostname</code> 文件中。</p>
<p>使用 nano 打开文件并在其中写入主机名。可以使用任何字符来标识机器。我通常使用我的设备品牌或型号作为我的主机名，并且由于我使用的是 legion 笔记本电脑，所以我将简单地写下以下内容：</p>
<pre><code>legion
</code></pre>
<p>本地主机名解析由 <code>nss-myhostname</code>（systemd 提供的 NSS 模块）提供，无需编辑 <code>/etc/hosts</code> 文件。已默认启用。</p>
<p>但有些软件可能仍会直接读取<code>/etc/hosts</code> 文件。在 nano 中打开文件并添加以下行：</p>
<pre><code>127.0.0.1        localhost
::1              localhost
127.0.1.1        legion
</code></pre>
<p>确保将 <code>legion</code> 替换为你的主机名。现在可以安装上述软件包：</p>
<pre><code>pacman -S networkmanager
</code></pre>
<p>通过执行以下命令启用 <code>NetworkManager</code> 服务：</p>
<pre><code>systemctl enable NetworkManager
</code></pre>
<p>确保将 <code>NetworkManager</code> 而不是 <code>networkmanager</code> 作为服务名称。如果命令成功，网络管理器将从现在开始在启动时自动启动并执行其操作。</p>
<h3 id="root">如何设置 root 密码</h3>
<p>你可能想为 root 用户设置密码，执行以下命令：</p>
<pre><code>passwd
</code></pre>
<p><code>passwd</code> 命令允许更改用户的密码。默认情况下，它会更改当前用户的密码，即 <code>root</code>。</p>
<p>它会要求输入新密码和确认密码。仔细输入，确保密码不会被忘记。</p>
<h3 id="root">如何创建非 root 用户</h3>
<p>长期以 root 用户身份使用 Linux 系统并不是一个好主意。所以创建一个非 root 用户很重要。要创建新用户，请执行以下命令：</p>
<pre><code>useradd -m -G wheel farhan
</code></pre>
<p><code>useradd</code> 命令允许创建一个新用户。确保将 farhan 替换为你要使用的名字。<code>-m</code> 选项表示还希望它创建相应的主目录。<code>-G</code> 选项会将新用户添加到 Arch Linux 中的管理用户组 <code>wheel</code> 组。</p>
<p>现在可以再次使用 <code>passwd</code> 命令为新创建的用户设置密码：</p>
<pre><code>passwd farhan
</code></pre>
<p>该程序将提示输入新密码和密码确认。再说一次，别忘了把 farhan 换成你用过的名字。</p>
<p>最后，需要为这个新用户启用 <code>sudo</code> 权限。使用 nano 打开 <code>/etc/sudoers</code> 文件。打开后，找到以下行并取消注释：</p>
<pre><code># %wheel ALL=(ALL) ALL
</code></pre>
<p>该文件实质上意味着 <code>wheel</code> 组中的所有用户都可以通过提供密码来使用 <code>sudo</code>。按 Ctrl + O 保存文件并按 Ctrl + X 退出 nano。现在新用户将能够在必要时使用 <code>sudo</code>。</p>
<h3 id="microcode">如何安装 Microcode</h3>
<p>摘自 <a href="https://www.pcmag.com/encyclopedia/term/microcode">PCMag</a>,</p>
<blockquote>
<p>复杂指令集计算机 (CISC) 中的一组基本指令。Microcode 驻留在单独的高速存储器中，用作机器指令和计算机电路级之间的翻译层。Microcode 使计算机设计人员能够创建机器指令，而无需设计电子电路。</p>
</blockquote>
<p>英特尔和 AMD 等处理器制造商经常会发布处理器的稳定性和安全性更新。这些更新对于系统的稳定性至关重要。</p>
<p>在 Arch Linux 中，Microcode 更新可以通过官方软件包获得，每个用户都应该在他们的系统上安装这些软件包。</p>
<pre><code>pacman -S amd-ucode


pacman -S intel-ucode
</code></pre>
<p>仅仅安装这些软件包是不够的。必须确保你的引导加载程序正在加载它们。具体将在下一节中介绍。</p>
<h3 id="bootloader">如何安装和配置 Boot Loader</h3>
<p>摘自<a href="https://en.wikipedia.org/wiki/Bootloader">维基百科</a>，</p>
<blockquote>
<p>bootloader，也拼写为 boot loader 或称为 boot manager 和 bootstrap loader，是负责引导计算机的计算机程序。</p>
</blockquote>
<p>bootloader 的细节超出了本文的范围，所以只涉及安装过程。如果过去使用过任何其他 Linux 发行版，可能了解过 GRUB 菜单。</p>
<p>GRUB 是目前最流行的 bootloaders 之一。尽管有很多功能，这里将只演示 GRUB 的安装，因为大多数人只需安装即可。</p>
<p>要安装 GRUB，必须首先安装如下两个软件包。</p>
<pre><code>pacman -S grub efibootmgr
</code></pre>
<p>如果与其他操作系统一起安装，还需要 <code>os-prober</code> 包：</p>
<pre><code>pacman -S os-prober
</code></pre>
<p>该程序将搜索系统上已安装的操作系统，并将它们作为 GRUB 配置文件的一部分。</p>
<p>现在，需要挂载之前创建的 EFI 系统分区。为此，首先创建一个 <code>efi</code> 目录：</p>
<pre><code>mkdir /boot/efi
</code></pre>
<p>摘自维基百科，</p>
<blockquote>
<p>在 Linux 和其他类 Unix 操作系统中，<code>/boot/</code> 目录包含用于引导操作系统的文件。</p>
</blockquote>
<p>该目录存在于所有类 Unix 操作系统中。上面提到的命令在 <code>/boot</code> 目录中创建了一个名为 <code>efi</code> 的目录。创建目录后，需要在该目录中安装  EFI  系统分区。</p>
<pre><code>mount /dev/sda1 /boot/efi
</code></pre>
<p>希望你还记得我们在分区阶段将<code>/dev/sda1</code>设备格式化为EFI系统分区。确保为你的设备使用正确的设备。</p>
<p>现在，我们将使用 <code>grub-install</code> 命令在新挂载的 EFI 系统分区中安装 GRUB：</p>
<pre><code>grub-install --target=x86_64-efi --bootloader-id=grub
</code></pre>
<p>可以或多或少地逐字使用此命令。可以将 <code>--bootloader-id</code> 更改为你想要展示的任何更贴切的文字，例如 <code>arch</code> 或其他内容。如果安装完成且没有任何错误，则需要生成 GRUB 配置文件。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_18_34_01.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_18_34_01" width="600" height="400" loading="lazy"></p>
<p>如果与其他操作系统一起安装，则必须在生成配置文件之前启用 <code>os-prober</code>。在 nano 文本编辑器中打开 <code>/etc/default/grub</code> 文件。找到以下行并取消注释：</p>
<pre><code>#GRUB_DISABLE_OS_PROBER=false
</code></pre>
<p>这是上述文件中的最后一行，只需滚动到底部并取消注释即可。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_18_31_41.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_18_31_41" width="600" height="400" loading="lazy"></p>
<p>现在执行以下命令生成配置文件：</p>
<pre><code>grub-mkconfig -o /boot/grub/grub.cfg
</code></pre>
<p><code>grub-mkconfig</code> 命令生成 GRUB 配置文件并将其保存到指定的目标位置。在这里，是 <code>/boot/grub/grub.cfg</code> 。</p>
<p>该命令还会检测之前安装的 microcode 以及机器上的任何其他现有操作系统。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_18_35_45.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_18_35_45" width="600" height="400" loading="lazy"></p>
<p>恭喜，现在已经安装了 Arch Linux。此时，可以退出 Arch-Chroot 环境，卸载分区，然后重新启动。但我建议你多待一会儿，一起设置好图形用户界面。</p>
<h2 id="xorg">如何安装 Xorg</h2>
<p>要在系统上运行带有图形用户界面的程序，需要安装 X Window System 实现。最常见的是 Xorg。</p>
<p>要安装 Xorg，请执行以下命令：</p>
<pre><code>pacman -S xorg-server
</code></pre>
<p>等到安装完成，然后继续安装必要的图形驱动程序。</p>
<h2 id="">如何安装图形驱动程序</h2>
<p>在 Arch Linux 上安装图形驱动程序非常简单。只需安装图形处理单元所需的软件包，然后就可以收工了。</p>
<pre><code>pacman -S nvidia nvidia-utils


pacman -S xf86-video-amdgpu


pacman -S xf86-video-intel
</code></pre>
<p>如果需要进一步的帮助，请随时查看 <a href="https://wiki.archlinux.org/title/Xorg">ArchWiki</a> 页面。</p>
<h2 id="">如何安装桌面环境</h2>
<p>现在已经安装了 Xorg 和必要的图形驱动程序，可以继续安装桌面环境，如 GNOME、Plasma 或 XFCE。</p>
<p>Arch Linux 支持很多的桌面环境，但我只尝试过 GNOME 和 Plasma。我将演示如何安装这两个。</p>
<h3 id="gnome">如何安装 GNOME 桌面</h3>
<p>要安装 GNOME，需要安装 <code>gnome</code> 包。执行以下命令：</p>
<pre><code>pacman -S gnome
</code></pre>
<p>在安装过程中，会提供 <code>pipwire-session-manager</code> 和 <code>emoji-font</code> 软件包的多种选择。在两个提示中按 Enter 接受默认值。安装可能需要一些时间才能完成。</p>
<p><code>gnome</code> 软件包带有 GDM 或 Gnome 显示管理器。可以通过执行以下命令来启用该服务：</p>
<pre><code>systemctl enable gdm
</code></pre>
<p>到这里，在 Arch 系统上启动和运行 GNOME 的准备工作已经全部完成。</p>
<h3 id="plasma">如何安装 Plasma 桌面</h3>
<p>KDE Plasma 安装与 GNOME 没有什么不同。需要安装 Plasma 相关的包而不是 GNOME。</p>
<pre><code>pacman -S plasma plasma-wayland-session
</code></pre>
<p>如果电脑是 NVIDIA 显卡，请不要安装 <code>plasma-wayland-session</code>，使用普通的旧 X11。我拥有两台配备 NVIDIA GPU 的设备，并且在使用 Wayland 时它们都表现出不稳定。</p>
<p>在安装过程中，将出现 <code>ttf-font</code>、<code>pipwire-session-manager</code> 和 <code>phonon-qt5-backend</code> 包的多种选择。确保选择 <code>noto-fonts</code> 作为 <code>ttf-font</code> 并接受其他两个的默认值。</p>
<p>与 GNOME 中的 <code>gdm</code> 一样，Plasma 带有 <code>sddm</code> 作为默认显示管理器。执行以下命令启用服务：</p>
<pre><code>systemctl enable sddm
</code></pre>
<p>到这里，在 Arch Linux 系统上启动和运行 Plasma 的准备工作已经全部完成。</p>
<h2 id="">如何完成安装</h2>
<p>现在已经安装了 Arch Linux 并完成了所有必要的配置步骤，可以重新启动到新安装的系统。首先要退出 Arch-Chroot 环境：</p>
<pre><code>exit
</code></pre>
<p>接下来，卸载 root 分区以确保没有挂起的操作：</p>
<pre><code>umount -R /mnt
</code></pre>
<p>现在重新启动机器：</p>
<pre><code>reboot
</code></pre>
<p>等到看到 GRUB 菜单。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_20_10_25.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_20_10_25" width="600" height="400" loading="lazy"></p>
<p>从列表中选择 Arch Linux 并等待系统完成启动。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_20_11_15.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_20_11_15" width="600" height="400" loading="lazy"></p>
<p>使用你的用户凭据登录，如下！</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_20_15_41.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_20_15_41" width="600" height="400" loading="lazy"></p>
<p>全新 Arch Linux 系统已准备好。</p>
<h2 id="">如何在桌面环境之间切换</h2>
<p>与其他与默认桌面环境紧密耦合的发行版不同，Arch 非常灵活。可以随时切换到另一个桌面环境。</p>
<p>为此，请先注销当前会话。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_20_11_15.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_20_11_15" width="600" height="400" loading="lazy"></p>
<p>如你所见，我目前正在使用 Plasma。现在切换到 TTY2 按 Ctrl + Alt + F2 组合键。将看到控制台登录提示：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_20_18_54.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_20_18_54" width="600" height="400" loading="lazy"></p>
<p>使用 root 凭据登录并禁用 <code>sddm</code> 显示管理器。</p>
<pre><code>systemctl disable sddm
</code></pre>
<p>然后卸载之前安装的 Plasma 相关包：</p>
<pre><code>sudo pacman -Rns plasma plasma-wayland-session
</code></pre>
<p>卸载软件包后，安装 GNOME 所需的软件包：</p>
<pre><code>pacman -S gnome
</code></pre>
<p>然后根据你之前阅读的部分执行安装。安装 <code>gnome</code> 包后，启用 <code>gdm</code> 显示管理器：</p>
<pre><code>systemctl enable gdm
</code></pre>
<p>重新启动计算机。</p>
<pre><code>reboot
</code></pre>
<p>等到 Arch Linux 系统完成启动。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_20_24_11.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_20_24_11" width="600" height="400" loading="lazy"></p>
<p>哇哦，漂亮的 Gnome 显示管理器。使用你的凭据登录。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_19_53_31.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_19_53_31" width="600" height="400" loading="lazy"></p>
<p>可以根据需要在桌面环境之间切换，但建议在其中一个环境下安顿下来。另外，不建议同时安装多个。</p>
<h2 id="pacman">使用 Pacman 管理包</h2>
<p>已经使用 pacman 安装了许多软件包。它相当于 Ubuntu 中的 apt 和 Fedora 中的 dnf 等软件包管理器。</p>
<p>在本节中，将介绍一些每天都可能用到的常用 pacman 命令。</p>
<h3 id="pacman">如何使用 Pacman 安装软件包</h3>
<p>可以使用以下 pacman 命令语法安装软件包：</p>
<pre><code>sudo pacman -S rust
</code></pre>
<p>可以按如下方式安装多个软件包：</p>
<pre><code>sudo pacman -S rust golang
</code></pre>
<p>还可以指定软件包的位置，如下所示：</p>
<pre><code>sudo pacman -S extra/rust
</code></pre>
<p>在此命令中，<code>-S</code> 选项表示同步，相当于在 apt 或 dnf 包管理器的安装。</p>
<h3 id="pacman">如何使用 Pacman 删除软件包</h3>
<p>可以使用以下 pacman 语法删除软件包：</p>
<pre><code>sudo pacman -R rust
</code></pre>
<p>这将删除包，但会留下依赖项。如果其他包也不需要它们，则可以通过执行以下命令删除不在需要的包：</p>
<pre><code>sudo pacman -Rs rust
</code></pre>
<p>Pacman 在删除某些应用程序时通常会保存重要的配置文件。可以使用以下语法覆盖此行为：</p>
<pre><code>sudo pacman -Rn rust
</code></pre>
<p>当卸载某些包时，通常会使用<code>sudo pacman -Rns</code>。要展示的最后一件事是如何删除孤立包。</p>
<p>在 Ubuntu 中，<code>sudo apt autoremove</code> 命令会卸载任何不必要的软件包。Arch 中的等效命令是：</p>
<pre><code>sudo pacman -Qdtq | pacman -Rs -
</code></pre>
<p>这将清除以前安装的软件包中的残余软件包。</p>
<h3 id="pacman">如何使用 Pacman 升级软件包</h3>
<p>可以使用以下语法升级系统中的所有软件包：</p>
<pre><code>sudo pacman -Syu
</code></pre>
<p>在这个命令中，<code>S</code> 选项同步包，<code>y</code> 刷新本地包缓存，<code>u</code> 更新系统。这就像终极升级命令，我基本每天都会运行一次。</p>
<h3 id="pacman">如何使用 Pacman 搜索软件包</h3>
<p>要在数据库中搜索包，可以使用以下语法：</p>
<pre><code>sudo pacman -Ss rust
</code></pre>
<p>这将打印出在数据库中找到的所有包含该搜索词的包，并且还会标识是否已经安装。</p>
<p>如果想检查一个包是否已经安装，可以使用以下命令：</p>
<pre><code>sudo pacman -Qs rust
</code></pre>
<p>当想卸载软件包但不知道其确切名称时，这很有用。</p>
<h2 id="archlinuxaur">如何在 Arch Linux 中使用 AUR</h2>
<p>摘自 <a href="https://itsfoss.com/aur-arch-linux/">It's FOSS</a></p>
<blockquote>
<p>AUR 代表 Arch 用户存储库。它是基于 Arch 的 Linux 发行版用户的社区驱动存储库。它包含名为 PKGBUILDs 的包描述，允许使用 makepkg 从源代码编译包，然后通过 pacman（Arch Linux 中的包管理器）安装它。</p>
</blockquote>
<p>AUR 是 Arch Linux 最吸引人的特性之一。有了 AUR，Arch Linux 的软件包数量几乎与 Debian 相当。之前使用 <code>pacman</code> 安装了各种软件包。遗憾的是，不能使用它从 AUR 安装软件包。</p>
<p>需要安装一个 AUR helper。Arch Linux 默认不支持这些 helpers，它更倾向让开发者自己手动构建包。在这里会介绍这两种技术。如果了解 helper 的工作原理，手动完成会很顺手。</p>
<h3 id="helper">如何使用 Helper 安装软件包</h3>
<p>在可用且当前维护的 AUR helper 中，我喜欢 <code>yay</code> 或另一个 yogurt 包。它是用 Go 编写的，非常可靠。</p>
<p>不能像其他软件包一样安装<code>yay</code>。需要获取源代码并编译程序。需要 <code>git</code> 和 <code>base-devel</code> 包来执行此操作。这里假设在 Arch Linux 安装期间已经安装了 <code>base-devel</code>：</p>
<pre><code>pacman -S git
</code></pre>
<p>从 GitHub 克隆 yay 仓库并 <code>cd</code> 进入到目录里：</p>
<pre><code>git clone https://aur.archlinux.org/yay.git &amp;&amp; cd yay
</code></pre>
<p>执行以下命令，从源代码构建和安装 yay：</p>
<pre><code>makepkg -si
</code></pre>
<p>makepkg 脚本自动执行包的构建过程。<code>-si</code> 选项代表同步依赖项和安装。第一个选项将安装所需的依赖项（在本例中为 Golang），后一个选项将安装构建的包。</p>
<p>构建过程完成后，makepkg 将要求输入密码。输入密码，完成安装。</p>
<p>检查 yay 是否已正确安装：</p>
<pre><code>yay --version
</code></pre>
<p>现在让我们使用 yay 安装一些东西。比较常见软件包如 <a href="https://aur.archlinux.org/packages/visual-studio-code-bin/">visual-studio-code-bin</a> 软件包。要安装它，执行以下命令：</p>
<pre><code>yay -S visual-studio-code-bin
</code></pre>
<p>与 pacman 不同，不应该使用 sudo 运行 yay。Yay 将查找给定的包并询问是否想查看差异：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_21_07_26.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_21_07_26" width="600" height="400" loading="lazy"></p>
<p>AUR 上的所有仓库都带有一个 PKGBUILD 文件，其中包含构建此包的说明。Yay 有这个不错的功能，它可以显示自上次以来 PKGBUILD 文件中发生了什么变化。</p>
<p>现在，选择 <code>N</code> 表示无，然后按 Enter。Yay 现在将查找依赖项并输入密码来执行安装。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_21_19_58.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_21_19_58" width="600" height="400" loading="lazy"></p>
<p>确认安装并提供密码。Yay 将安装依赖项并开始构建包。构建完成后，yay 将安装该软件包并在必要时提示输入密码。</p>
<p>安装完成后，在应用程序启动器中搜索 Visual Studio Code：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_21_28_42.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_14_01_2022_21_28_42" width="600" height="400" loading="lazy"></p>
<p>恭喜你从 AUR 安装了你的第一个包。Yay 命令与 pacman 几乎相同，所以如果可以用 pacman 做某事，也可以用 yay 做。</p>
<p>事实上，yay 也可以安装来自 Arch Linux 官方仓库的软件包，比如 pacman。建议仅在必要时使用 yay 从 AUR 安装软件包，使用 pacman 安装其他所有软件包。</p>
<h3 id="">如何手动安装软件包</h3>
<p>就像我在上一节中所说的，ArchWiki 建议避免使用任何 AUR helper，而是手动从 AUR 安装包。现在将展示如何做。</p>
<p>确保安装了 <code>git</code> 和 <code>base-devel</code> 包。如果没有，使用 pacman 安装它们。</p>
<p>做为演示，这次来安装 Spotify。首先访问 spotify 包的 AUR 页面 - <a href="https://aur.archlinux.org/packages/spotify/">https://aur.archlinux.org/packages/spotify/</a> 并复制“Git Clone URL”。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/image-68.png" alt="image-68" width="600" height="400" loading="lazy"></p>
<p>该页面甚至列出了需要的所有依赖项。将仓库克隆到计算机：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_16_01_2022_21_16_43.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_16_01_2022_21_16_43" width="600" height="400" loading="lazy"></p>
<p>每个 AUR 仓库都附带一个 PKGBUILD 文件，其中包含构建包的说明。当从 AUR 安装软件包时，可以使用类似 <code>cat</code> 命令检查 PKGBUILD 文件：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_16_01_2022_21_22_37.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_16_01_2022_21_22_37" width="600" height="400" loading="lazy"></p>
<p>确保文件中没有任何有害内容。使用 <code>makepkg</code> 安装任何依赖项，构建包并安装它。理想情况下不应该有任何问题，但有时，可能会出现异常情况。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_16_01_2022_21_34_29.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_16_01_2022_21_34_29" width="600" height="400" loading="lazy"></p>
<p>这时，可以返回相应的 AUR 页面并查看用户评论。在这里，找到了以下固定评论：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/image-69.png" alt="image-69" width="600" height="400" loading="lazy"></p>
<p>原来该软件包要求将 Spotify for Linux gpg 密钥添加到用户 kyechain。此命令使用 <code>curl</code> 下载 gpg 密钥并将其作为 <code>gpg --import</code> 命令的输入：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_16_01_2022_21_37_50.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_16_01_2022_21_37_50" width="600" height="400" loading="lazy"></p>
<p>尝试再次执行<code>makepkg -si</code>，这次一切都正常了：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/VirtualBox_archlinux-2022.01.01-x86_64_16_01_2022_21_39_33.png" alt="VirtualBox_archlinux-2022.01.01-x86_64_16_01_2022_21_39_33" width="600" height="400" loading="lazy"></p>
<p>看，事实上！ 手动安装软件包经常会出现此类故障，一般在评论里就可以找到解法。现在让我们来欣赏音乐吧。</p>
<h2 id="">如何解决常见问题</h2>
<p>我多年来一直使用 Arch 作为我所有主力机的操作系统，但仍然会遇到一些问题。幸运的是，当遇到困难时，有一些很棒的地方可以寻求帮助：</p>
<ul>
<li><a href="https://wiki.archlinux.org/">ArchWiki</a></li>
<li><a href="https://bbs.archlinux.org/">Arch Linux Forum</a></li>
<li><a href="https://www.reddit.com/r/archlinux/">r/archlinux</a></li>
</ul>
<p>在大多数情况下，wiki 可以找到想要找的内容。事实上，如果使用的是笔记本电脑，执行某些操作时卡住了，有一个完整的 wiki <a href="https://wiki.archlinux.org/title/Category:Laptops">类别</a>专门按不同的笔记本型号分类。建议看看 wiki。</p>
<p>如果 wiki 未能解决你的问题，请在论坛和 subreddit 上询问其他用户。但是，无论何时，请务必先进行研究，并在帖子中包含尽可能多的描述。如果其他用户不得不不断地向你询问更多信息，这真的很烦人，这也会降低你得到答案的机会。</p>
<h2 id="livearchiso">如何使用 Live Arch ISO 作为恢复媒介</h2>
<p>不要理会别人怎么说，只要你知道自己在做什么，Arch Linux 就很少出问题。如果在 AUR 中安装较新的包，或者在不知道它们用途的情况下不断切换不同的内核，系统可能无法启动。</p>
<p>在这些情况下，可以将 Live U 盘用作应急媒介。为此，请将可启动 U 盘重新连接到计算机并启动到实时环境。在那里，如果愿意，可以配置时间、键盘映射和字体。</p>
<p>然后使用 <code>fdisk</code> 列出所有分区并找到保存 Arch Linux 安装的分区。在我的例子中，它是 <code>/dev/sda2</code> 分区。像以前一样挂载分区：</p>
<pre><code>mount /dev/sda2 /mnt
</code></pre>
<p>现在使用 Arch-Chroot 以 root 用户身份登录。</p>
<pre><code>arch-chroot /mnt
</code></pre>
<p>现在卸载安装的坏包或回到过去曾经工作的内核版本等等。完成后，退出 Arch-Chroot 环境，卸载分区，然后重新启动：</p>
<pre><code>exit
umount -R /mnt
reboot
</code></pre>
<p>如果计算机启动正常，那么恭喜。否则，请尝试 wiki、论坛或 subreddit。如果没有任何效果，可能需要重新安装。</p>
<h2 id="">拓展阅读</h2>
<p>如果已经走到了这一步，那么已经接触了很多资料——但这还不是全部。整本手册是通过结合来自 wiki、论坛和 subreddit 的信息编写的。我列出了一些拓展的 wiki 页面。</p>
<ul>
<li><a href="https://wiki.archlinux.org/title/Installation_guide">Installation guide</a></li>
<li><a href="https://wiki.archlinux.org/title/Network_configuration">Network configuration</a></li>
<li><a href="https://wiki.archlinux.org/title/General_recommendations">General recommendation</a></li>
<li><a href="https://wiki.archlinux.org/title/Desktop_environment">Desktop environment</a></li>
<li><a href="https://wiki.archlinux.org/title/pacman">pacman</a></li>
<li><a href="https://wiki.archlinux.org/title/Arch_Build_System">Arch Build System</a></li>
<li><a href="https://wiki.archlinux.org/title/makepkg">makepkg</a></li>
<li><a href="https://wiki.archlinux.org/title/List_of_applications">List of applications</a></li>
</ul>
<p>目前想到这些，我会更新这个列表。</p>
<h2 id="">总结</h2>
<p>衷心感谢你花时间阅读本文。希望你享受学习过程，能学到一些关于 Arch 和 Linux 的知识。</p>
<p>除了本文，我还编写了关于其他复杂主题的手册，可在 <a href="https://www.freecodecamp.org/news/the-docker-handbook/freecodecamp.org/news/author/farhanhasin/">freeCodeCamp</a> 免费获得。</p>
<p>这些手册是我践行“为大家简化难以理解的技术”使命的一部分。每一本手册都花费了大量时间和精力。</p>
<p>如果你喜欢我的写作并想鼓励我，请考虑 star <a href="https://github.com/fhsinchy/">GitHub</a> 并在 [LinkedIn](<a href="https://www">https://www</a>. linkedin.com/in/farhanhasin/) 上关注我。</p>
<p>我总是乐于接受建议和讨论。欢迎在 <a href="https://twitter.com/frhnhsin">Twitter</a> 或 <a href="https://www.linkedin.com/in/farhanhasin/">LinkedIn</a> 上关注我，直接向我发送消息。</p>
<p>最后，欢迎分享给你的朋友，因为</p>
<blockquote>
<p>在开源领域，我们强烈认为要真正做好某件事，就必须让更多人参与进来。—  Linus Torvalds</p>
</blockquote>
<p>接下来，保持严谨并持续学习。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ JS 里的简易算法和数据结构之复杂度 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：The complexity of simple algorithms and data structures in JS [https://www.freecodecamp.org/news/the-complexity-of-simple-algorithms-and-data-structures-in-javascript-11e25b29de1e/] ，作者：Yung L. Leung 在之前的文章《迈向计算科学的一步：JS 里的简易算法和数据结构 [https://medium.com/@yunglleung1/a-step-towards-computing-as-a-science-algorithms-data-structures-4c0e2d6ae79a] 》里，我们讨论了简易的算法 (线性和二分搜索；冒泡、选择、插入排序) 以及数据结构 (数组、键值对对象)，这里将继续讨论算法和数据结构的复杂度概念及其应用。 复杂度 复杂度是衡量程序复杂程度的一个指标。对于算法和数据结构来讲，它代表运行指定任务（如搜索、排序、访问数据）花费的时间和空间 (计算所消耗的 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-complexity-of-simple-algorithms-and-data-structures-in-javascript/</link>
                <guid isPermaLink="false">5d2458f0fbfdee429dc5eca3</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Tue, 08 Feb 2022 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2019/07/1-5.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/the-complexity-of-simple-algorithms-and-data-structures-in-javascript-11e25b29de1e/">The complexity of simple algorithms and data structures in JS</a>，作者：Yung L. Leung</p><p>在之前的文章《<a href="https://medium.com/@yunglleung1/a-step-towards-computing-as-a-science-algorithms-data-structures-4c0e2d6ae79a" rel="nofollow noopener">迈向计算科学的一步：JS 里的简易算法和数据结构</a>》里，我们讨论了简易的算法 (线性和二分搜索；冒泡、选择、插入排序) 以及数据结构 (数组、键值对对象)，这里将继续讨论算法和数据结构的复杂度概念及其应用。</p><h2 id="-">复杂度</h2><p><strong><strong>复杂度</strong></strong>是衡量程序复杂程度的一个指标。对于算法和数据结构来讲，它代表运行指定任务（如搜索、排序、访问数据）花费的时间和空间 (计算所消耗的内存)。任务的执行效率取决于执行完整程序需要的操作数。</p><p><strong><strong>扔垃圾</strong></strong>需要 3 步 (打包垃圾袋，拎出去，扔进垃圾桶里)。 <strong><strong>扔垃圾</strong></strong> 很简单，但是如果扔装修一周后攒的垃圾，会发现不能完成这个简单的任务，因为垃圾桶 <strong><strong>空间不足</strong></strong> 。</p><p><strong><strong>房屋吸尘</strong></strong>需要一些重复的步骤 (打开开关，使用吸尘器头重复的在地板上刮扫，关闭开关)。越大的房间就需要在地板上刮扫越多次，房屋除尘也就要耗费 <strong><strong>越多的时间</strong></strong> 。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/e3896a6d686a0082893d16d9b8cbb3dfb11c1f5a.png" class="kg-image" alt="L12QMT1j9D8t1gr3hUD5nE72YpEsgo3DPowC" width="600" height="400" loading="lazy"></figure><p><br>执行的操作数和执行元素的数量有一定的关系，垃圾 (元素) 越多花费的时间越多，这就产生了 <strong><strong>空间复杂度</strong></strong> 问题。面积 (元素) 越大，吸尘器头刮扫的次数就越多，这就产生了 <strong><strong>时间复杂度</strong></strong> 问题。</p><p>不管是 <strong><strong>扔垃圾</strong></strong> 还是 <strong><strong>房间吸尘</strong></strong>， <strong><strong>操作数</strong></strong> (O) 和 <strong><strong>元素的数量</strong></strong> (n) 成正比。如果只有一个垃圾袋，那么一次只能携带一袋垃圾。如果有两个垃圾袋，那么同时就能携带两袋垃圾，当然在体力足以同时拎两袋垃圾的前提下。这些家务活的大 O 就是 O = n，也可以写成 O = function(n) 或者 <strong><strong>O(n)</strong></strong> 。在这里复杂度是线性的 （操作数和元素是一一对应的关系），即 30 个操作对应 30 个元素 (上图的黄线)。</p><p>在算法和数据结构里情况是一样的。</p><h2 id="--1">搜索</h2><h3 id="--2">线性搜索</h3><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/a5f729e0e9d6a577ea41a6ae5ebd1125cc839829.gif" class="kg-image" alt="CMLgOmQiGx-An2R8TeY3yghPSmQzfHc4KCsa" width="600" height="400" loading="lazy"><figcaption><a href="https://www.mathwarehouse.com/programming/images/binary-vs-linear-search/binary-and-linear-search-animations.gif" rel="nofollow noopener">图片来源</a></figcaption></figure><p>元素搜索的 <strong><strong>最好情况</strong></strong> 是有序列表，假设要搜索的元素就在列表的第一项，这时复杂度是常数 <strong><strong>O(1)</strong></strong>。因此，如果搜索的元素总是第一个被索引到，和列表长度无关，元素会当即被搜索到。搜索的复杂度是和列表长度相关的常数。这种搜索平均最坏的情况是线性复杂度 O(n)。换言之，n 个元素，要遍历 n 次才能找到，因此叫线性搜索。</p><h3 id="--3">二分搜索</h3><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/6184d8f650c8de4bc0078a9fc68f4c291c5f67df.gif" class="kg-image" alt="BdVrmbkpWAEROeZzJh-WwglcO3ZvE92aE7Co" width="600" height="400" loading="lazy"><figcaption><a href="https://www.mathwarehouse.com/programming/images/binary-vs-linear-search/binary-and-linear-search-animations.gif" rel="nofollow noopener">图片来源</a></figcaption></figure><p>对于二分搜索， <strong><strong>最好的情况</strong></strong> 也是 O(1) ，此时搜索的元素位于中点。平均最坏的情况是 n 以 2 为底的对数：<br></p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/51913ebfc6de66903405b27bf73a013c3b2eb40f.png" class="kg-image" alt="O4yQ5gMCaNxd9A8fGyTmZRoJlZmUvw2aPxmi" width="600" height="400" loading="lazy"></figure><p>对数或者 log 是幂运算的逆运算，所以如果有 16 个元素 (n=16)，这时至少要花费 4 步（这是在最坏情况下），4 步就能找到数字 15 (基数 = 4)。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/bb1455647f51292baffab6b6112a49f3986188c7.png" class="kg-image" alt="DSYpZXtP0NNN0Poj2wNYEE-n6wQhuekHm8KY" width="600" height="400" loading="lazy"></figure><p>或者， <strong><strong>O(log n)</strong></strong></p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/5d51d12fc46287decaa8a0ba2d4e6b1ade7f622e.png" class="kg-image" alt="xWIc5wqomKmecGODTG4drhWSHdirPt9D9lSE" width="600" height="400" loading="lazy"></figure><h2 id="--4">排序</h2><h3 id="--5">冒泡</h3><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/d4261d6695d260c4036d7fef7a9f4580da4f0f1c.gif" class="kg-image" alt="0aAaHJbR5Nb4u4NCSWXn2pomchbOh9ThVPUm" width="600" height="400" loading="lazy"><figcaption><a href="https://upload.wikimedia.org/wikipedia/commons/5/54/Sorting_bubblesort_anim.gif" rel="nofollow noopener">图片来源</a></figcaption></figure><p>在冒泡排序里，元素和剩下的元素集合对比来找更高的元素向上冒泡，所以，平均最坏的情况，复杂度是 <strong><strong>O(n²)</strong></strong> ，也就是嵌套循环。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/f1be75eff5e53d4e985acc2d5e952155c684d338.png" class="kg-image" alt="02WXe7k1k0y-OFHvIwKyyUh0vNZNs0FUVp-G" width="600" height="400" loading="lazy"></figure><p>每一个元素都要和剩下元素的集合进行对比，4 个元素一共对比 16 次 (4² = 16)。 <strong><strong>最好的情况</strong></strong> 是集合除了一个元素外其余都已经排好序了，那么只需进行一轮对比。也就是说四个元素需要和集合里剩下的 3 个分别元素对比，复杂度也就是 <strong><strong>O(n)</strong></strong> 。</p><h3 id="--6">选择排序</h3><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/7a0b207303267c3d40cbe16a99a6e635cc764964.gif" class="kg-image" alt="3HuokiI2FYWnn70N50fhDf-9LrLBLu3Xdcfk" width="600" height="400" loading="lazy"><figcaption><a href="https://codepumpkin.com/selection-sort-algorithms/" rel="nofollow noopener">图片来源</a></figcaption></figure><p>和 <strong><strong>冒泡排序</strong></strong> 高值冒泡不同， <strong><strong>选择排序</strong></strong> 把最低值放在未排序元素的最前面，因为需要和剩下集合的每个元素对比，它的复杂度是 <strong><strong>O(n²)</strong></strong> 。</p><h3 id="--7">插值排序</h3><figure class="kg-card kg-image-card"><img src="http://www.chenzhicheng.com/content/images/2019/06/3tfg-fQ3pfT9czmGkS8p41nLAavr2XlPuxVK.png" class="kg-image" alt="3tfg-fQ3pfT9czmGkS8p41nLAavr2XlPuxVK" width="600" height="400" loading="lazy"></figure><p>和 <strong><strong>冒泡&amp;选择排序</strong></strong> 不同， <strong><strong>插值排序</strong></strong> 把元素直接插入到正确的位置。和上一个排序一样，需要和集合里剩下的元素对比，因此 <strong><strong>平均最坏的复杂度</strong></strong> 是 <strong><strong>O(n²)</strong></strong> 。和冒泡排序类似，如果只有一个元素需要排序，只需要一轮对比来把元素插入到正确的位置。复杂度为 <strong><strong>O(n)</strong></strong> 。</p><h2 id="--8">数据结构</h2><h3 id="--9">数组</h3><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/ad416e6acc8cd141e703906a06ba453e0b4bd547.png" class="kg-image" alt="5UN4-lEeiZ5sR3wLv3S0t5RU0bKU4ixbPTB7" width="600" height="400" loading="lazy"></figure><p>因为通过索引访问、添加或者删除末位数据元素只需要一步， <strong><strong>访问、推入、弹出</strong></strong> 数组里的值的复杂度是 <strong><strong>O(1)</strong></strong> 。因此，在数组里通过索引来 <strong><strong>线性搜索</strong></strong> 的复杂度是 <strong><strong>O(n)</strong></strong> 。</p><p>另外，在数组在前面 <strong><strong>shift</strong></strong> 或者 <strong><strong>unshift</strong></strong> 值需要 <strong><strong>重新索引</strong></strong> 之后的每一个元素 (比如，移除索引为 0 的元素需要重新给索引为 1 的元素标记为 0，依次直到第四个)，它的复杂度是 <strong><strong>O(n)</strong></strong> ，需要重新标记所有元素。</p><h3 id="--10">健 - 值对对象</h3><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/c8febea35fd435321456f87bc3159246ccf1dc59.jpeg" class="kg-image" alt="uSM26U11UIi7pAVC9TOM0Ku20YXoAE7C2UCD" width="600" height="400" loading="lazy"><figcaption><a href="https://cdn.shopify.com/s/files/1/1147/6518/products/safeandvaultstore-sdbx9-safe-deposit-boxes_large.jpg?v=1495593363" rel="nofollow noopener">图片来源</a></figcaption></figure><p>通过健 <strong><strong>访问、插入或者删除</strong></strong> 值是瞬间发生的，因此，复杂度是 <strong><strong>O(1)</strong></strong> 。通过索引健在 “存储箱” 中搜索特定的项目本质是线性搜索，因此它的复杂度是 <strong><strong>O(n)</strong></strong> 。</p><h2 id="--11">结论</h2><p>复杂度不只是既定算法和数据结构里面的一个论题。运用恰当，它会是衡量代码的效率的一个利器。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 利用 React 组件的“黄金法则"让代码更优雅之如何发挥钩子的作用 ]]>
                </title>
                <description>
                    <![CDATA[ 最近学到了一个新的理念，它改变了我创建组件的方式。它不仅是一个新的点子，还是一个新的视角。 组件的黄金法则 用更自然的方式创建和定义组件，组件只包含它们必需的代码 这是很短的一句话，可能你觉得已经理解了，但却很容易违反这一原则。 比如，有如下组件： PersonCard如果自然的定义这个组件你可能会这样写： PersonCard.propTypes = {   name: PropTypes.string.isRequired,   jobTitle: PropTypes.string.isRequired,   pictureUrl: PropTypes.string.isRequired, }; 代码很简单，每个属性都是它所必需的，如 name、job title 和 picture URL。 假设现在需要添加用户可更改的另一个更正式的图片。可能最容易想到的就是： PersonCard.propTypes = {   name: PropTypes.string.isRequired,   jobTitle: PropTypes.string.isRequired ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-the-golden-rule-of-react-components-can-help-you-write-better-code/</link>
                <guid isPermaLink="false">5da414ddfbfdee429dc60027</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Tue, 25 Jan 2022 07:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2019/10/123-1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>最近学到了一个新的理念，它改变了我创建组件的方式。它不仅是一个新的点子，还是一个新的视角。</p><h2 id="-">组件的黄金法则</h2><p>用更自然的方式创建和定义组件，组件只包含它们必需的代码</p><p>这是很短的一句话，可能你觉得已经理解了，但却很容易违反这一原则。</p><p>比如，有如下组件：</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/58225752ea8321dfc62139045a03f1fbc0972f66.png" class="kg-image" alt="1_nF_5kuYHigZuwdq99vRJ8g" width="600" height="400" loading="lazy"><figcaption><em>PersonCard</em></figcaption></figure><p>如果自然的定义这个组件你可能会这样写：</p><pre><code class="language-javascript">PersonCard.propTypes = {
  name: PropTypes.string.isRequired,
  jobTitle: PropTypes.string.isRequired,
  pictureUrl: PropTypes.string.isRequired,
};
</code></pre><p>代码很简单，每个属性都是它所必需的，如 name、job title 和 picture URL。</p><p>假设现在需要添加用户可更改的另一个更正式的图片。可能最容易想到的就是：</p><pre><code class="language-javascript">PersonCard.propTypes = {
  name: PropTypes.string.isRequired,
  jobTitle: PropTypes.string.isRequired,
  officialPictureUrl: PropTypes.string.isRequired,
  pictureUrl: PropTypes.string.isRequired,
  preferOfficial: PropTypes.boolean.isRequired,
};
</code></pre><p>看起来这些 props 是组件必需的，实际上，组件没有这些 props 也不会受到影响。而且添加了 <code>preferOfficial</code> 看似增加了灵活性，其实逻辑本来不该添加在这里，考虑复用的时候会发现这样做很不优雅。</p><h2 id="--1">如何改进</h2><p>那么转换图片 URL 的逻辑不属于组件本身，那它属于哪里呢？</p><p>放在 <code>index</code> 里怎么样？</p><p>我们采用如下的目录结构，每个组件都有自己名字命名的文件夹，<code>index</code> 文件是沟通优雅组件和外部世界的桥梁。我们把这个文件叫做 “容器”（container）(<a href="https://redux.js.org/basics/usage-with-react#presentational-and-container-components" rel="nofollow noopener">参考了React Redux 的 “container” 组件概念</a>）。</p><pre><code>/PersonCard
  -PersonCard.js ------ the "natural" component
  -index.js ----------- the "container"
</code></pre><p>我们将容器（container）定义为连接优雅组件和外部世界的桥梁。正因为此，我们有时候又称之为 “注入（injectors）”。</p><p>优雅组件（natural component） 代表你的代码只包含必需的部分（没有诸如怎样获取数据或者位置等细节—所有代码都是必需的）。</p><p>外部世界（outside world）可以将数据转换成符合优雅组件所需的 props。</p><p>这篇文章讨论的：怎样让组件不受外部世界的污染，以及这样做的好处。</p><p>注意：虽然灵感来自 <a href="https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0" rel="nofollow noopener">Dan’s Abramov</a> 和 <a href="https://redux.js.org/basics/usage-with-react#presentational-and-container-components" rel="nofollow noopener">React Redux’s</a> 的理念，但我们的容器和它们的略微不同。<br><a href="https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0" rel="nofollow noopener">Dan Abramov 的容器</a> 和我们区别是在概念层面上。Dan 认为有两种组件：展示组件和容器组件。我们在这个基础上更进一步，认为先有组件，后有容器。<br>虽然我们用组件实现容器，但我们不认为容器是传统意义的组件。这就是为什么我们建议你把容器放在 <code>index</code> 文件里—因为它是优雅组件和外部世界的桥梁。并不独立存在。</p><p>所以这篇文章会有大量的组件、容器字眼。</p><p>为什么？</p><p>创建一个优雅组件–很容易、甚至还很有趣。</p><p>连接组件和外部世界–有点难。</p><p>依我之见，外部世界对优雅组件的污染，主要是这三种方式：</p><ol><li>古怪的数据结构</li><li>组件 scope 之外的需求 (就像上面的代码那样)</li><li>在 update 或者 mount 时触发 event</li></ol><p>接下来的几节将会说明这些情况，并用例子展示不同情况下的容器实现。</p><h2 id="--2">处理古怪的数据结构</h2><p>有时候为了呈现需要的信息，需要把数据连在一起然后将其转换成特定的格式。由于没有更好的设计模式，“古怪的” 数据结构是最容易想到的的也是最不优雅的方式。</p><p>把古怪的数据结构直接传入组件然后在组件内部转换很诱人，但是这会让组件更复杂、更难测试。</p><p>我最近就掉进了这个坑里，我创建了一个组件，从一个特殊的数据结构获取数据，然后让它支持特殊的表单。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/2e09d403b4fd8e9e1b3b104fcb5610707bf32c02.gif" class="kg-image" alt="1_hFOPWOxkedUEb851jdAXjA" width="600" height="400" loading="lazy"></figure><pre><code class="language-javascript">ChipField.propTypes = {
  field: PropTypes.object.isRequired,      // &lt;-- the "weird" data structure
  onEditField: PropTypes.func.isRequired,  // &lt;-- and a weird event too
};
</code></pre><p>然后就诞生了这玩意，古怪的 <code>field</code> 数据结构做为 prop。另外，如果以后不需要在处理它了还好，但是当我们想要在另一个完全不相干的数据结构里复用它时，噩梦来了。</p><p>由于这个组件需要一个复杂的数据结构，复用几乎不可能，重构起来也很头大。之前写的测试也会很难看懂，因为它们 mock 了一个古怪的数据结构。在持续重构时测试逻辑很难懂也很难重写。</p><p>很不幸，古怪的数据结构很难避免，但是使用容器可以很好的驯服它。好处之一是你可以很好的复用组件了。之前直接把古怪的数据结构传入组件，是难复用的罪魁祸首。</p><p>注意： 我并不是说在创造组件的开始所有的组件就都应该是通用的。建议是好好考虑考虑组件的基本功能，然后在开始编码。回报是，通过少量工作写出一些高度可复用的组件。</p><h3 id="--3">使用函数组件实现容器</h3><p>如果你 mapping props 上要求很严格，容器的一个简单的实现是使用另一个函数组件：</p><pre><code class="language-javascript">import React from 'react';
import PropTypes from 'prop-types';

import getValuesFromField from './helpers/getValuesFromField';
import transformValuesToField from './helpers/transformValuesToField';

import ChipField from './ChipField';

export default function ChipFieldContainer({ field, onEditField }) {
  const values = getValuesFromField(field);
  
  function handleOnChange(values) {
    onEditField(transformValuesToField(values));
  }
  
  return &lt;ChipField values={values} onChange={handleOnChange} /&gt;;
}

// external props
ChipFieldContainer.propTypes = {
  field: PropTypes.object.isRequired,
  onEditField: PropTypes.func.isRequired,
};
</code></pre><p>组件的目录结构如下：</p><pre><code>/ChipField
  -ChipField.js ------------------ the "natural" chip field
  -ChipField.test.js
  -index.js ---------------------- the "container"
  -index.test.js
  /helpers ----------------------- a folder for the helpers/utils
    -getValuesFromField.js
    -getValuesFromField.test.js
    -transformValuesToField.js
    -transformValuesToField.test.js
</code></pre><p>你可能会说，这也太麻烦了。看起来是多了一些文件，绕了很多弯，但是别忘了：</p><p>在组件外面转换数据和在组件内工作量是一致的。区别是，在组件外面转换数据时，你给了你自己一个更明确的点来测试转换是否正确，分离了关注点。</p><h2 id="-scope-">在组件 scope 的外部满足需要</h2><p>和上面的 Person Card 一样，当你用 “黄金法则” 来思考的时候，很可能你会意识到需求是超出了组件的实际范围。该怎么实现呢？</p><p>没错，就是容器。</p><p>可以创建容器，通过少量的工作来保持组件的优雅。这样做的时候，你会解锁一个更专业的组件，这个组件也简单的多，同时容器也更易于测试。</p><p>让我们来写一个 PersonCard 容器来举栗说明。</p><h3 id="--4">使用高阶组件实现容器</h3><p>React Redux 就是使用了 <a href="https://reactjs.org/docs/higher-order-components.html" rel="nofollow noopener">高阶组件</a> ，实现了从 Redux store 里 push、map props 的容器。由于我们是从 React Redux 借鉴的这个理念，毫无疑问 <a href="https://redux.js.org/basics/usage-with-react#implementing-container-components" rel="nofollow noopener">React Redux 的 connect 就是这个容器</a>。</p><p>无论你是使用函数组件来映射 props，还是使用高阶组件来连接 Redux strore，组件的黄金法则还是不变的。首先，编写优雅组件，然后用高阶组件连接二者。</p><pre><code class="language-javascript">import { connect } from 'react-redux';

import getPictureUrl from './helpers/getPictureUrl';

import PersonCard from './PersonCard';

const mapStateToProps = (state, ownProps) =&gt; {
  const { person } = ownProps;
  const { name, jobTitle, customPictureUrl, officialPictureUrl } = person;
  const { preferOfficial } = state.settings;
  
  const pictureUrl = getPictureUrl(preferOfficial, customPictureUrl, officialPictureUrl);
  
  return { name, jobTitle, pictureUrl };
};

const mapDispatchToProps = null;

export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(PersonCard);
</code></pre><p>文件结构如下：</p><pre><code>/PersonCard
  -PersonCard.js ----------------- natural component
  -PersonCard.test.js
  -index.js ---------------------- container
  -index.test.js
  /helpers
    -getPictureUrl.js ------------ helper
    -getPictureUrl.test.js
</code></pre><p>注意：在这里，给 <code>getPictureUrl</code> 提供一个助手。这个逻辑很简单。你可能已经注意到了，无论 container 实现如何，文件结构几乎没变。</p><p>如果你之前用过 Redux，上面的例子你一定不陌生。再次重申，这个黄金法则不只是一个点子，它还提供了一个新思路。</p><p>另外，当使用高阶函数实现容器时，还可以把它们连在一起–把一个高阶组件做为 props 传递给下一个。我就曾经把多个高阶组件连在一起构成了一个单个的容器。</p><p>2019 注意：React 社区似乎正在让高阶组件规范成设计模式。</p><p>我也如此建议。我的经验是，对于那些不理解 functional composition 的人来说写代码很容易引发 “wrapper 地狱”，组件嵌套太多层从而引发了严重的性能问题。</p><p>这里是一些相关文章：<a href="https://youtu.be/dpw9EHDh2bM?t=710" rel="nofollow noopener">Hooks talk</a> (2018)， <a href="https://youtu.be/zD_judE-bXk?t=1101" rel="nofollow noopener">Recompose talk</a> (2016)， <a href="https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce" rel="nofollow noopener">Use a Render Prop!</a> (2017)，<a href="https://blog.kentcdodds.com/when-to-not-use-render-props-5397bbeff746" rel="nofollow noopener">When to NOT use Render Props</a> (2018)</p><h2 id="--5">说好的钩子来了</h2><h3 id="--6">使用钩子实现容器</h3><p>为什么在这里会讨论钩子呢？因为使用钩子实现容器真的很简单呀。</p><p>如果你对 React 钩子陌生，建议你看一看 <a href="https://youtu.be/dpw9EHDh2bM" rel="nofollow noopener">Dan Abramov 和 Ryan Florence 在 2018 React Conf 上的谈话</a>。</p><p>要点是，钩子是 React 团队为了解决<a href="https://reactjs.org/docs/higher-order-components.html" rel="nofollow noopener">高阶组件</a>和<a href="https://reactjs.org/docs/render-props.html" rel="nofollow noopener">类似模式</a>的问题引入的。React 想要在类似的场景用钩子替代它们。</p><p>这意味着容器既可以用函数组件实现也可以用钩子来实现。</p><p>在下面的例子里，我们使用 <code>useRoute</code> 和 <code>useRedux</code> 钩子来代表"外部世界"，使用工具类 <code>getvalue</code> 把外部世界映射为优雅组件的 <code>props</code>。我们还使用了 <code>transformValues</code> 来将组件转换为外部世界的 <code>dispatch</code> 。</p><pre><code class="language-javascript">import React from 'react';
import PropTypes from 'prop-types';

import { useRouter } from 'react-router';
import { useRedux } from 'react-redux';

import actionCreator from 'your-redux-stuff';

import getValues from './helpers/getVaules';
import transformValues from './helpers/transformValues';

import FooComponent from './FooComponent';

export default function FooComponentContainer(props) {
  // hooks
  const { match } = useRouter({ path: /* ... */ });
  // NOTE: `useRedux` does not exist yet and probably won't look like this
  const { state, dispatch } = useRedux();

  // mapping
  const props = getValues(state, match);
  
  function handleChange(e) {
    const transformed = transformValues(e);
    dispatch(actionCreator(transformed));
  }
  
  // natural component
  return &lt;FooComponent {...props} onChange={handleChange} /&gt;;
}

FooComponentContainer.propTypes = { /* ... */ };

</code></pre><p>下面是对应的目录结构：</p><pre><code>/FooComponent ----------- the whole component for others to import
  -FooComponent.js ------ the "natural" part of the component
  -FooComponent.test.js
  -index.js ------------- the "container" that bridges the gap
  -index.js.test.js         and provides dependencies
  /helpers -------------- isolated helpers that you can test easily
    -getValues.js
    -getValues.test.js
    -transformValues.js
    -transformValues.test.js

</code></pre><h2 id="--7">在容器里触发事件</h2><p>最后一类导致组件难以复用的情况，是在 props 改变、组件 mounting 的时候触发事件。</p><p>比如，以仪表盘为例。设计团队给了你原型图，需要你把它们转换成 React 组件。现在面临的问题是如何用数据填充仪表盘。</p><p>可能你已经意识到了可以在组件 mount 时调用函数（比如：<code>dispatch(fetchAction)</code>） 来触发事件。</p><p>在类似的这种场景中，普遍做法是添加 <code>componentDidMount</code> 和 <code>compoentDidUpdate</code> 生命周期方法，以及 <code>onMount</code> 和 <code>onDashboardIdChanged</code> props，因为我需要触发外部事件，才能建立组件和外部世界之间的连接。</p><p>根据黄金法则，这些 <code>onMount</code> 和 <code>onDashboardIdChanged</code> props 很不优雅的，应该把它们放在容器里。</p><p>钩子厉害之处是它能让 <code>onMount</code> 或者 props 改变时的 dispatch 事件变得更容易实现！</p><h3 id="-mount-">在 mount 里触发事件</h3><p>传入空数组调用 <code>useEffect</code> 来触发 mount 时的 event。</p><pre><code class="language-javascript">import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useRedux } from 'react-redux';

import fetchSomething_reduxAction from 'your-redux-stuff';
import getValues from './helpers/getVaules';
import FooComponent from './FooComponent';

export default function FooComponentContainer(props) {
  // hooks
  // NOTE: `useRedux` does not exist yet and probably won't look like this
  const { state, dispatch } = useRedux();
  
  // dispatch action onMount
  useEffect(() =&gt; {
    dispatch(fetchSomething_reduxAction);
  }, []); // the empty array tells react to only fire on mount
  // https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

  // mapping
  const props = getValues(state, match);
  
  // natural component
  return &lt;FooComponent {...props} /&gt;;
}

FooComponentContainer.propTypes = { /* ... */ };

</code></pre><p><em>组件 mount 时通过钩子在容器里触发事件</em></p><h3 id="-prop-">在 prop 改变时触发事件</h3><p><code>useEffect</code> 可以在重新渲染和函数调用时，监视 property 的改变。</p><p>在用 <code>useEffect</code> 前我发现我自己添加了冗余的生命周期函数方法和 <code>onPropertyChanged</code> 属性，因为我不知如何在组件外面扩展属性。</p><pre><code class="language-javascript">import React from 'react';
import PropTypes from 'prop-types';

/**
 * Before `useEffect`, I found myself adding "unnatural" props
 * to my components that only fired events when the props diffed.
 *
 * I'd find that the component's `render` didn't even use `id`
 * most of the time
 */
export default class BeforeUseEffect extends React.Component {
  static propTypes = {
    id: PropTypes.string.isRequired,
    onIdChange: PropTypes.func.isRequired,
  };

  componentDidMount() {
    this.props.onIdChange(this.props.id);
  }

  componentDidUpdate(prevProps) {
    if (prevProps.id !== this.props.id) {
      this.props.onIdChange(this.props.id);
    }
  }

  render() {
    return // ...
  }
}

</code></pre><p><em>老方法：当 props 改变的时候触发事件</em></p><p>有了 <code>useEffect</code> 更轻量级的方法来改变 prop ，组件也不必添加多余的 props 了。</p><pre><code class="language-javascript">mport React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useRedux } from 'react-redux';

import fetchSomething_reduxAction from 'your-redux-stuff';
import getValues from './helpers/getVaules';
import FooComponent from './FooComponent';

export default function FooComponentContainer({ id }) {
  // hooks
  // NOTE: `useRedux` does not exist yet and probably won't look like this
  const { state, dispatch } = useRedux();
  
  // dispatch action onMount
  useEffect(() =&gt; {
    dispatch(fetchSomething_reduxAction);
  }, [id]); // `useEffect` will watch this `id` prop and fire the effect when it differs
  // https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

  // mapping
  const props = getValues(state, match);
  
  // natural component
  return &lt;FooComponent {...props} /&gt;;
}

FooComponentContainer.propTypes = {
  id: PropTypes.string.isRequired,
};

</code></pre><p><em>现在的方法：当 props 改变的时候使用 <code>useEffect</code> 来触发事件</em></p><p>免责声明：在 <code>useEffect</code> 调用前会对比容器里 prop 的异同，还可以使用其它方式，比如高阶组件（比如 <a href="https://github.com/acdlite/recompose/blob/3db12ce7121a050b533476958ff3d66ded1c4bb8/docs/API.md#lifecycle" rel="nofollow noopener">recompose 的生命周期</a> ），或者像<a href="https://github.com/ReactTraining/react-router/blob/89a72d58ac55b2d8640c25e86d1f1496e4ba8d6c/packages/react-router/modules/Lifecycle.js" rel="nofollow noopener"> react router 那样在内部</a> 创建一个生命周期组件，但是这些方法要么就是很麻烦要么就是很难理解。</p><h2 id="--8">好处是什么</h2><h3 id="--9">组件保持有趣</h3><p>对于我来说，创建组件是前端开发中很有趣的部分。能把团队的想法实现感觉很棒，这种感觉值得我们分享。</p><p>在也不要让外部世界把组件 API 搞砸了。组件应该和想象中一样没有额外的 props—这也是我从黄金法则里所学到。</p><h3 id="--10">更多的机会测试和复用</h3><p>当你采用一个像这样的模式时，引入了一个新的 data-y 层。在这个层里你可以按需的把数据转换成组件需要的形式。</p><p>不管你在不在乎，这个层已经在你的应用里存在了，但是这也可能会加重代码的逻辑。我的经验是当我关注到这一层时，我可以做大量的代码优化，可以复用大量的逻辑，现在当我知道组件之间有共性时我是不会重造轮子的。</p><p>我觉得这点在<a href="https://reactjs.org/docs/hooks-custom.html" rel="nofollow noopener">定制钩子</a>上尤为明显。定制钩子给我们一个更简单的方式来抽出逻辑、监测外部的变化—更多时候靠 helper 函数是无法做到的。</p><h3 id="--11">最大化团队的输出</h3><p>在团队协作里，你可以把组件和容器分开。如果事前沟通好 API，你可以同时开启如下工作：</p><ol><li>Web API (如 后端)</li><li>从 Web API 里获取数据（或者其它途径）然后转换数据以符合组件的 API</li><li>组件</li></ol><h2 id="--12">有没有例外？</h2><p>就像真正的黄金法则一样，这条黄金法则也有例外。有某些的场景下，在组件里编写冗余的 API 以减少复杂性很有必要。</p><p>一个简单的例子就是 props 的命名。如果不在优雅的组件下面重新命名 data key 会让事情变得更复杂。</p><p>迷信金科玉律可能会更规范，但是同时也封杀了创造力。</p><h2 id="--13">分隔线</h2><p>不管怎样，黄金法则只是简单的以一个新的角度重申了表现组件和容器组件。总之，在编码前增加基本的组件需求评估，会更容易写出优雅的代码。</p><p>感谢阅读。</p><p>原文：<a href="https://www.freecodecamp.org/news/how-the-golden-rule-of-react-components-can-help-you-write-better-code-127046b478eb/">How the “Golden Rule” of React components can help you write better code</a>，作者：<a href="https://www.freecodecamp.org/news/author/rico/">Rico Kahler</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 前端开发教程之用 CSS 美化按钮 ]]>
                </title>
                <description>
                    <![CDATA[ 按钮是前端开发里的一个常见元素，在给按钮添加样式时有一些窍门，我收集了美化按钮的一些方法，当然你可以按需组合使用。首先安利一个创建渐变的小工具 [https://uigradients.com]。 一个简单的 “Get Started” 按钮 .btn {   background: #eb94d0;   /* 创建渐变 */   background-image: -webkit-linear-gradient(top, #eb94d0, #2079b0);   background-image: -moz-linear-gradient(top, #eb94d0, #2079b0);   background-image: -ms-linear-gradient(top, #eb94d0, #2079b0);   background-image: -o-linear-gradient(top, #eb94d0, #2079b0);   background-image: linear-gradient(to bottom, #eb94d0, #2079b0);   ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/a-quick-guide-to-styling-buttons-using-css/</link>
                <guid isPermaLink="false">5e761bb3ca1efa04e196c042</guid>
                
                    <category>
                        <![CDATA[ CSS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Mon, 24 Jan 2022 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/03/1_ILGYxH64agmcHBWHuF1FSA.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>按钮是前端开发里的一个常见元素，在给按钮添加样式时有一些窍门，我收集了美化按钮的一些方法，当然你可以按需组合使用。首先安利一个创建渐变的<a href="https://uigradients.com">小工具</a>。</p><h2 id="-get-started-"><strong>一个简单的 “Get Started” 按钮</strong></h2><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/1-1.gif" class="kg-image" alt="1-1" width="600" height="400" loading="lazy"></figure><pre><code>.btn {
  background: #eb94d0;
  /* 创建渐变 */
  background-image: -webkit-linear-gradient(top, #eb94d0, #2079b0);
  background-image: -moz-linear-gradient(top, #eb94d0, #2079b0);
  background-image: -ms-linear-gradient(top, #eb94d0, #2079b0);
  background-image: -o-linear-gradient(top, #eb94d0, #2079b0);
  background-image: linear-gradient(to bottom, #eb94d0, #2079b0);
  /* 给按钮添加圆角 */
  -webkit-border-radius: 28;
  -moz-border-radius: 28;
  border-radius: 28px;
  text-shadow: 3px 2px 1px #9daef5;
  -webkit-box-shadow: 6px 5px 24px #666666;
  -moz-box-shadow: 6px 5px 24px #666666;
  box-shadow: 6px 5px 24px #666666;
  font-family: Arial;
  color: #fafafa;
  font-size: 27px;
  padding: 19px;
  text-decoration: none;
}
/* 悬停样式 */
.btn:hover {
  background: #2079b0;
  background-image: -webkit-linear-gradient(top, #2079b0, #eb94d0);
  background-image: -moz-linear-gradient(top, #2079b0, #eb94d0);
  background-image: -ms-linear-gradient(top, #2079b0, #eb94d0);
  background-image: -o-linear-gradient(top, #2079b0, #eb94d0);
  background-image: linear-gradient(to bottom, #2079b0, #eb94d0);
  text-decoration: none;
}</code></pre><h2 id="-"><strong>透明背景色</strong></h2><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/2-1.gif" class="kg-image" alt="2-1" width="600" height="400" loading="lazy"></figure><pre><code>.btn {
      /* 文字颜色 */
      color: #0099CC; 
      /* 清除背景色 */
      background: transparent; 
      /* 边框样式、颜色、宽度 */
      border: 2px solid #0099CC;
      /* 给边框添加圆角 */
      border-radius: 6px; 
      /* 字母转大写 */
      border: none;
      color: white;
      padding: 16px 32px;
      text-align: center;
      display: inline-block;
      font-size: 16px;
      margin: 4px 2px;
      -webkit-transition-duration: 0.4s; /* Safari */
      transition-duration: 0.4s;
      cursor: pointer;
      text-decoration: none;
      text-transform: uppercase;
}
.btn1 {
      background-color: white; 
      color: black; 
      border: 2px solid #008CBA;
}
/* 悬停样式 */
.btn1:hover {
      background-color: #008CBA;
      color: white;
}</code></pre><h2 id="-css-entities"><strong>应用 CSS Entities</strong></h2><p><a href="https://www.w3schools.com/cssref/css_entities.asp">这里</a>是 CSS entities 的详细介绍。</p><p>也可以使用 <a href="https://www.w3schools.com/html/html_entities.asp">HTML entities</a>，但是只支持部分功能。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/02/1.gif" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><pre><code>.button {
  display: inline-block;
  border-radius: 4px;
  background-color: #f4511e;
  border: none;
  color: #FFFFFF;
  text-align: center;
  font-size: 28px;
  padding: 20px;
  width: 200px;
  transition: all 0.5s;
  cursor: pointer;
  margin: 5px;
}
.button span {
  cursor: pointer;
  display: inline-block;
  position: relative;
  transition: 0.5s;
}
.button span:after {
content: '\00bb';  /* CSS Entities. 如果用的是 HTML Entities, 请改成 &amp;#8594;*/
position: absolute;
  opacity: 0;
  top: 0;
  right: -20px;
  transition: 0.5s;
}
.button:hover span {
  padding-right: 25px;
}
.button:hover span:after {
  opacity: 1;
  right: 0;
}</code></pre><h2 id="--1"><strong>添加点击动画</strong></h2><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/02/2.gif" class="kg-image" alt="2" width="600" height="400" loading="lazy"></figure><p>CSS: (SCSS)</p><pre><code>$gray: #bbbbbb;
* {
  font-family: 'Roboto', sans-serif;
}
.container {
  position: absolute;
  top:50%;
  left:50%;
  margin-left: -65px;
  margin-top: -20px;
  width: 130px;
  height: 40px;
  text-align: center;
}
.btn {
      color: #0099CC; /* 文字颜色 */
      background: transparent; /* 清除背景色 */
      border: 2px solid #0099CC; /* 边框样式、颜色、宽度 */
      border-radius: 70px; /* 给边框添加圆角 */
      text-decoration: none;
      text-transform: uppercase; /* 字母转大写 */
      border: none;
      color: white;
      padding: 16px 32px;
      text-align: center;
      text-decoration: none;
      display: inline-block;
      font-size: 16px;
      margin: 4px 2px;
      -webkit-transition-duration: 0.4s; /* 兼容 Safari */
      transition-duration: 0.4s;
      cursor: pointer;
}
.btn1 {
      background-color: white; 
      color: black; 
      border: 2px solid #008CBA;
}
 .btn1:hover {
      background-color: #008CBA;
      color: white;
 }
b {
  outline:none;
  height: 40px;
  text-align: center;
  width: 130px;
  border-radius:100px;
  background: #fff;
  border: 2px solid #008CBA;
  color: #008CBA;
  letter-spacing:1px;
  text-shadow:0;
  font:{
    size:12px;
    weight:bold;
  }
  cursor: pointer;
  transition: all 0.25s ease;
&amp;:active {
    letter-spacing: 2px ;
  }
  &amp;:after {
    content:"";
  }
}
.onclic {
  width: 10px !important;
  height: 70px !important;
  border-radius: 50% !important;
  border-color:$gray;
  border-width:4px;
  font-size:0;
  border-left-color: #008CBA;
  animation: rotating 2s 0.25s linear infinite;
  &amp;:hover {
    color: dodgerblue;
    background: white;
  }
}
.validate {
  content:"";
  font-size:16px;
  color: black;
  background: dodgerblue;
  border-radius: 50px;
  &amp;:after {
    font-family:'FontAwesome';
    content:" done \f00c";
  }
}
@keyframes rotating {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}</code></pre><p>Javascript: (JQuery)</p><pre><code>$(function() {
  $("#button").click(function() {
    $("#button").addClass("onclic", 250, validate);
  });
function validate() {
    setTimeout(function() {
      $("#button").removeClass("onclic");
      $("#button").addClass("validate", 450, callback);
    }, 2250);
  }
  function callback() {
    setTimeout(function() {
      $("#button").removeClass("validate");
    }, 1250);
  }
});</code></pre><h2 id="--2"><strong>图片按钮</strong></h2><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/5-1.gif" class="kg-image" alt="5-1" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==" class="kg-image" alt="gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==" width="600" height="400" loading="lazy"></figure><pre><code>.btn {
 background: #92c7eb;
 background-image: url(“http://res.freestockphotos.biz/pictures/15/15107-illustration-of-a-red-close-button-pv.png");
 background-size: cover;
 background-position: center;
 display: inline-block;
 border: none;
 padding: 20px;
 width: 70px;
 border-radius: 900px;
 height: 70px;
 transition: all 0.5s;
 cursor: pointer;
}
.btn:hover
{
 width: 75px;
 height: 75px;
}
</code></pre><h2 id="icons-">Icons 按钮</h2><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/6-1.gif" class="kg-image" alt="6-1" width="600" height="400" loading="lazy"></figure><p>index.html:</p><pre><code>&lt;div class="main"&gt;&lt;button class="button" style="vertical-align:middle"&gt;&lt;a href="#" class="icon-button twitter"&gt;&lt;i class="icon-twitter"&gt;&lt;/i&gt;&lt;span&gt;&lt;/span&gt;&lt;/button&gt;&lt;/a&gt;
  &lt;div class="text"&gt;&lt;strong&gt;TWEET!&lt;/strong&gt;&lt;/div&gt;
&lt;/div&gt;</code></pre><p>Style.css:</p><pre><code>button{
  border: none;
  border-radius: 50px;
}
html,
body {
  font-size: 20px;
  min-height: 100%;
  overflow: hidden;
  font-family: "Helvetica Neue", Helvetica, sans-serif;
  text-align: center;
}
.text {
  padding-top: 50px;
  font-family: "Helvetica Neue", Helvetica, 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
}
.text:hover
{
  cursor: pointer;
  color: #1565C0;
}
.main {
  padding: 0px 0px 0px 0px;
  margin: 0;
  background-image: url("https://someimg");
  text-align: center;
  background-size: 100% !important;
  background-repeat: no-repeat;
  width: 900px;
  height: 700px;  
}
.icon-button {
  background-color: white;
  border-radius: 3.6rem;
  cursor: pointer;
  display: inline-block;
  font-size: 2rem;
  height: 3.6rem;
  line-height: 3.6rem;
  margin: 0 5px;
  position: relative;
  text-align: center;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  width: 3.6rem;
}
.icon-button span {
  border-radius: 0;
  display: block;
  height: 0;
  left: 50%;
  margin: 0;
  position: absolute;
  top: 50%;
  -webkit-transition: all 0.3s;
  -moz-transition: all 0.3s;
  -o-transition: all 0.3s;
  transition: all 0.3s;
  width: 0;
}
.icon-button:hover span {
  width: 3.6rem;
  height: 3.6rem;
  border-radius: 3.6rem;
  margin: -1.8rem;
}
.twitter span {
  background-color: #4099ff;
}
/* Icons */
.icon-button i {
  background: none;
  color: white;
  height: 3.6rem;
  left: 0;
  line-height: 3.6rem;
  position: absolute;
  top: 0;
  -webkit-transition: all 0.3s;
  -moz-transition: all 0.3s;
  -o-transition: all 0.3s;
  transition: all 0.3s;
  width: 3.6rem;
  z-index: 10;
}
.icon-button .icon-twitter {
  color: #4099ff;
}
.icon-button:hover .icon-twitter {
  color: white;
}</code></pre><h2 id="--3">总结</h2><p>在这个教程里，我们学习了通过 CSS 和一点 JavaScript（如果需要点击后的效果）来美化 CSS 按钮。也可以使用 CSS3ButtonGenerator 来生成一个简单的按钮。有问题欢迎留言。</p><p>原文链接：<a href="https://www.freecodecamp.org/news/a-quick-guide-to-styling-buttons-using-css-f64d4f96337f/">A quick guide to styling buttons using CSS</a>，作者：Ashwini Sheshagiri</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ NGINX 完全手册 ]]>
                </title>
                <description>
                    <![CDATA[ 因为传统 Web 服务器无法处理超过 1 万个并发请求，俄罗斯一位名叫 Igor Sysoev [https://en.wikipedia.org/wiki/Igor_Sysoev]  的年轻开发人员很沮丧。这就是著名的 C10k 问题 [https://en.wikipedia.org/wiki/C10k_problem]  。沮丧之余，他于 2002 年开始研发新的 Web 服务器。 NGINX [https://nginx.org/]  于 2004 年基于 2-clause BSD [https://en.wikipedia.org/wiki/2-clause_BSD]  许可条款首次向公众发布。根据 2021 年 3 月 Web 服务器调查 [https://news.netcraft.com/archives/2021/03/29/march-2021-web-server-survey.html] ，NGINX 拥有 35.3% 的市场份额，大约有 4.196 亿个站点在使用它。 由于有了 NGINXConfig [https://www.digitalocean.c ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-nginx-handbook/</link>
                <guid isPermaLink="false">60de9036240b4e0653a3e472</guid>
                
                    <category>
                        <![CDATA[ NGINX ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Wed, 06 Oct 2021 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/07/Copy-of-docker-1280x612.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>因为传统 Web 服务器无法处理超过 1 万个并发请求，俄罗斯一位名叫 <a href="https://en.wikipedia.org/wiki/Igor_Sysoev">Igor Sysoev</a> 的年轻开发人员很沮丧。这就是著名的 <a href="https://en.wikipedia.org/wiki/C10k_problem">C10k 问题</a> 。沮丧之余，他于 2002 年开始研发新的 Web 服务器。</p>
<p><a href="https://nginx.org/">NGINX</a> 于 2004 年基于 <a href="https://en.wikipedia.org/wiki/2-clause_BSD">2-clause BSD</a> 许可条款首次向公众发布。根据 <a href="https://news.netcraft.com/archives/2021/03/29/march-2021-web-server-survey.html">2021 年 3 月 Web 服务器调查</a>，NGINX 拥有 35.3% 的市场份额，大约有 4.196 亿个站点在使用它。</p>
<p>由于有了 <a href="https://www.digitalocean.com/community/tools/nginx">NGINXConfig</a> 这样的的工具以及 <a href="https://digitalocean.com/">DigitalOcean</a> 等互联网上大量线程的配置文件，人们倾向于做大量的复制粘贴，却并不理解这些配置的含义。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/177962736_1410222585999736_5618677227291897851_n.jpg" alt="177962736_1410222585999736_5618677227291897851_n" width="600" height="400" loading="lazy"></p>
<p>相信我，搞懂它并不难......</p>
<p>我并不是说复制代码不好，但在千万不要在不理解的情况下复制代码。</p>
<p>此外，NGINX 是一种软件，应该根据要提供服务的应用程序的要求和主机上的可用资源进行精确配置。</p>
<p>这就是为什么你应该理解并修改你正在复制的内容，而不是盲目地复制——这就是本手册存在的意义。</p>
<p>通读整本书后，你应该能够：</p>
<ul>
<li>理解流行工具生成的配置文件以及各种文档中的配置文件。</li>
<li>从头开始将 NGINX 配置为 Web 服务器、反向代理服务器和负载均衡器。</li>
<li>优化 NGINX 以获得最大的服务器性能。</li>
</ul>
<h2 id="">先决条件</h2>
<ul>
<li>熟悉 Linux 终端和常用 Unix 程序，如 <code>ls</code>、<code>cat</code>、<code>ps</code>、<code>grep</code>、<code>find</code>、<code>nproc</code>、<code>ulimit</code> 和 <code>nano</code>。</li>
<li>一台可以运行虚拟机的计算机或 5 美元的虚拟专用服务器。</li>
<li>了解 Web 应用程序和编程语言，如 JavaScript 或 PHP。</li>
</ul>
<h2 id="">目录</h2>
<ul>
<li><a href="#introduction-to-nginx">NGINX 简介</a></li>
<li><a href="#how-to-install-nginx">如何安装 NGINX</a>
<ul>
<li><a href="#how-to-provision-a-local-virtual-machine">如何配置本地虚拟机</a></li>
<li><a href="#how-to-provision-a-virtual-private-server">如何配置虚拟专用服务器</a></li>
<li><a href="#how-to-install-nginx-on-a-provisioned-server-or-virtual-machine">如何在配置的虚拟机或服务器上安装 NGINX</a></li>
</ul>
</li>
<li><a href="#introduction-to-nginx-s-configuration-files">NGINX 配置文件介绍</a></li>
<li><a href="#how-to-configure-a-basic-web-server">如何配置基本的 Web 服务器</a>
<ul>
<li><a href="#how-to-write-your-first-configuration-file">如何编写你的第一个配置文件</a></li>
<li><a href="#how-to-validate-and-reload-configuration-files">如何校验并重新加载配置文件</a></li>
<li><a href="#how-to-understand-directives-and-contexts-in-nginx">如何理解 NGINX 中的指令和上下文</a></li>
<li><a href="#how-to-serve-static-content-using-nginx">如何使用 NGINX 提供静态内容</a></li>
<li><a href="#static-file-type-handling-in-nginx">NGINX 中的静态文件处理</a></li>
<li><a href="#how-to-include-partial-config-files">如何引入部分配置文件</a></li>
</ul>
</li>
<li><a href="#dynamic-routing-in-nginx">NGINX 的动态路由</a>
<ul>
<li><a href="#location-matches">位置匹配</a></li>
<li><a href="#variables-in-nginx">NGINX 中的变量</a></li>
<li><a href="#redirects-and-rewrites">Redirects 和 Rewrites</a></li>
<li><a href="#how-to-try-for-multiple-files">如何尝试多个文件</a></li>
</ul>
</li>
<li><a href="#logging-in-nginx">NGINX 日志</a></li>
<li><a href="#how-to-use-nginx-as-a-reverse-proxy">如何使用 NGINX 作为反向代理</a>
<ul>
<li><a href="#node-js-with-nginx">使用 NGINX 的 Node.js</a></li>
<li><a href="#php-with-nginx">使用 NGINX 的 PHP</a></li>
</ul>
</li>
<li><a href="#how-to-use-nginx-as-a-load-balancer">如何使用 NGIXN 做为负载均衡器</a></li>
<li><a href="#how-to-optimize-nginx-for-maximum-performance">如何优化 NGXIN 以获得最大性能</a>
<ul>
<li><a href="#how-to-configure-worker-processes-and-worker-connections">如何配置工作进程和工作连接</a></li>
<li><a href="#how-to-cache-static-content">如何缓存静态内容</a></li>
<li><a href="#how-to-compress-responses">如何压缩响应</a></li>
</ul>
</li>
<li><a href="#how-to-understand-the-main-configuration-file">如何理解主配置文件</a></li>
<li><a href="#a-series-on-advanced-nginx-concepts">高级 NGINX 概念系列</a></li>
<li><a href="#show-your-support">表达您的支持</a></li>
<li><a href="#conclusion">尾声</a></li>
</ul>
<h2 id="">项目代码</h2>
<p>你可以在这个<a href="https://github.com/fhsinchy/nginx-handbook-projects">仓库</a>中找到示例项目的代码。留一个 ⭐ 让我保持动力。</p>
<p><code>master</code> 分支包含本书中使用的所有代码。</p>
<h2 id="nginx">NGINX 简介</h2>
<p><a href="https://nginx.org/">NGINX</a> 是一种高性能网络服务器，旨在满足现代网络日益增长的需求。它专注于高性能、高并发和低资源使用。尽管它通常被称为 Web 服务器，但 NGINX 的核心是一个<a href="https://en.wikipedia.org/wiki/Reverse_proxy">反向代理</a> 服务器。</p>
<p>不过，NGINX 并不是市场上唯一的 Web 服务器。它最大的竞争对手之一是 <a href="https://httpd.apache.org/">Apache HTTP Server (httpd)</a>，它于 1995 年首次发布。尽管 Apache HTTP Server 更灵活，但服务器管理员通常更喜欢 NGINX，有两个主要原因：</p>
<ul>
<li>它可以处理更多的并发请求。</li>
<li>它可以在更低资源消耗的前提下更快地交付静态内容。</li>
</ul>
<p>我不会深入讨论整个 Apache 与 NGINX 的争论。但是，如果你想详细了解它们之间的差异，请参阅 <a href="https://www.digitalocean.com/community/users/jellingwood">Justin Ellingwood</a> 的这篇出色的<a href="https://www.digitalocean.com/community/tutorials/apache-vs-nginx-practical-considerations">文章</a>。</p>
<p>事实上，为了解释 NGINX 的请求处理技术，我想在这里引用 Justin 的文章中的两段：</p>
<blockquote>
<p>Nginx 在 Apache 之后出现，更针对的解决大规模站点将面临的并发问题。利用这些知识，Nginx 从头开始设计为使用异步、非阻塞、事件驱动的连接处理算法。</p>
<p>Nginx 产生工作进程，每个进程可以处理数千个连接。工作进程通过实现一个快速轮询机制来实现这一点，该机制不断地检查和处理事件，将实际工作与连接解耦，这允许每个工作进程仅在触发新事件时才关注连接。</p>
</blockquote>
<p>如果这看起来有点难以理解，请不要担心。现在对内部工作原理有一个基本的了解就足够了。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/wQszK2rvq-1.png" alt="wQszK2rvq-1" width="600" height="400" loading="lazy"></p>
<p>NGINX 在静态内容交付方面更快，同时资源相对较少，因为它没有嵌入动态编程语言处理器。当对静态内容的请求到来时，NGINX 只响应文件而不运行任何额外的进程。</p>
<p>这并不意味着 NGINX 不能处理需要动态编程语言处理器的请求。接收到需要动态处理的请求时，NGINX 只是将任务委托给单独的进程，例如 <a href="https://www.php.net/manual/en/install.fpm.php">PHP-FPM</a>、[Node.js](https:/ /nodejs.org/) 或 <a href="https://python.org/">Python</a>。 一旦该进程完成其工作，NGINX 会将响应反向代理回客户端。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/_nT7rcdjG.png" alt="_nT7rcdjG" width="600" height="400" loading="lazy"></p>
<p>NGINX 配置文件语法参考了脚本语言语法，因此很容易配置，可以生成紧凑、易于维护的配置文件。</p>
<h2 id="nginx">如何安装 NGINX</h2>
<p>在基于 <a href="https://en.wikipedia.org/wiki/Linux">Linux</a> 的系统上安装 NGINX 非常简单。可以使用运行 <a href="https://ubuntu.com/">Ubuntu</a> 的虚拟专用服务器作为你的训练场，也可以使用 Vagrant 在本地系统上配置虚拟机。</p>
<p>大多数情况下，配置本地虚拟机就足够了，这也是我将在本文中使用的方式。</p>
<h3 id="">如何配置本地虚拟机</h3>
<p><a href="https://vagrantup.com/">Vagrant</a> 是 <a href="https://www.hashicorp.com/">Hashicorp</a> 的一个开源工具，它可以使用简单的配置文件配置虚拟机。</p>
<p>要使用这种方式，需要 <a href="https://www.virtualbox.org/wiki/Downloads/">VirtualBox</a> 和 <a href="https://www.vagrantup.com/downloads/">Vagrant</a>，所以提前先安装它们。如果你需要对该主题进行一些了解，此<a href="https://learn.hashicorp.com/collections/vagrant/getting-started/">教程</a> 可能会有所帮助。</p>
<p>在系统中的某处创建一个具有适当名称的工作目录。我的是 <code>~/vagrant/nginx-handbook</code> 目录。</p>
<p>在工作目录中创建一个名为 <code>Vagrantfile</code> 的文件并输入以下内容：</p>
<pre><code class="language-vagrantfile">Vagrant.configure("2") do |config|

    config.vm.hostname = "nginx-handbook-box"
  
    config.vm.box = "ubuntu/focal64"
  
    config.vm.define "nginx-handbook-box"
  
    config.vm.network "private_network", ip: "192.168.20.20"
  
    config.vm.provider "virtualbox" do |vb|
      vb.cpus = 1
      vb.memory = "1024"
      vb.name = "nginx-handbook"
    end
  
  end
</code></pre>
<p>这个 <code>Vagrantfile</code> 就是我之前讲的配置文件。它包含虚拟机名称、CPU 数量、RAM 大小、IP 地址等信息。</p>
<p>要使用此配置启动虚拟机，请在工作目录中打开终端并执行以下命令：</p>
<pre><code class="language-shell">vagrant up

# Bringing machine 'nginx-handbook-box' up with 'virtualbox' provider...
# ==&gt; nginx-handbook-box: Importing base box 'ubuntu/focal64'...
# ==&gt; nginx-handbook-box: Matching MAC address for NAT networking...
# ==&gt; nginx-handbook-box: Checking if box 'ubuntu/focal64' version '20210415.0.0' is up to date...
# ==&gt; nginx-handbook-box: Setting the name of the VM: nginx-handbook
# ==&gt; nginx-handbook-box: Clearing any previously set network interfaces...
# ==&gt; nginx-handbook-box: Preparing network interfaces based on configuration...
#     nginx-handbook-box: Adapter 1: nat
#     nginx-handbook-box: Adapter 2: hostonly
# ==&gt; nginx-handbook-box: Forwarding ports...
#     nginx-handbook-box: 22 (guest) =&gt; 2222 (host) (adapter 1)
# ==&gt; nginx-handbook-box: Running 'pre-boot' VM customizations...
# ==&gt; nginx-handbook-box: Booting VM...
# ==&gt; nginx-handbook-box: Waiting for machine to boot. This may take a few minutes...
#     nginx-handbook-box: SSH address: 127.0.0.1:2222
#     nginx-handbook-box: SSH username: vagrant
#     nginx-handbook-box: SSH auth method: private key
#     nginx-handbook-box: Warning: Remote connection disconnect. Retrying...
#     nginx-handbook-box: Warning: Connection reset. Retrying...
#     nginx-handbook-box: 
#     nginx-handbook-box: Vagrant insecure key detected. Vagrant will automatically replace
#     nginx-handbook-box: this with a newly generated keypair for better security.
#     nginx-handbook-box: 
#     nginx-handbook-box: Inserting generated public key within guest...
#     nginx-handbook-box: Removing insecure key from the guest if it's present...
#     nginx-handbook-box: Key inserted! Disconnecting and reconnecting using new SSH key...
# ==&gt; nginx-handbook-box: Machine booted and ready!
# ==&gt; nginx-handbook-box: Checking for guest additions in VM...
# ==&gt; nginx-handbook-box: Setting hostname...
# ==&gt; nginx-handbook-box: Configuring and enabling network interfaces...
# ==&gt; nginx-handbook-box: Mounting shared folders...
#     nginx-handbook-box: /vagrant =&gt; /home/fhsinchy/vagrant/nginx-handbook

vagrant status

# Current machine states:

# nginx-handbook-box           running (virtualbox)
</code></pre>
<p><code>vagrant up</code> 命令的输出在你的系统上可能会有所不同，但只要 <code>vagrant status</code> 表示机器正在运行，就可以开始了。</p>
<p>鉴于虚拟机现在正在运行，应该能够通过 SSH 进入它。为此，请执行以下命令：</p>
<pre><code class="language-shell">vagrant ssh nginx-handbook-box

# Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-72-generic x86_64)
# vagrant@nginx-handbook-box:~$
</code></pre>
<p>如果一切正常，应该登录到虚拟机上，这可以通过终端上的 <code>vagrant@nginx-handbook-box</code> 行看出。</p>
<p>此虚拟机可在本地计算机上的 <strong><a href="http://192.168.20.20">http://192.168.20.20</a></strong> 上访问。你甚至可以通过在你的 <strong>hosts</strong> 文件中添加一个条目来为虚拟机分配一个像 <strong><a href="http://nginx-handbook.test">http://nginx-handbook.test</a></strong> 这样的自定义域：</p>
<pre><code class="language-shell"># on mac and linux terminal
sudo nano /etc/hosts

# on windows command prompt as administrator
notepad c:\windows\system32\drivers\etc\hosts
</code></pre>
<p>现在在文件末尾追加以下行：</p>
<pre><code>nginx-handbook.test    192.168.20.20
</code></pre>
<p>现在你应该可以在浏览器中通过 <strong><a href="http://nginx-handbook.test">http://nginx-handbook.test</a></strong> URI 访问虚拟机。</p>
<p>可以通过在工作目录中执行以下命令来停止或销毁虚拟机：</p>
<pre><code class="language-shell"># to stop the virtual machine
vagrant halt

# to destroy the virtual machine
vagrant destroy
</code></pre>
<p>如果你想了解更多 Vagrant 命令，这个<a href="https://gist.github.com/wpscholar/a49594e2e2b918f4d0c4">备忘单</a> 可能会派上用场。</p>
<p>现在系统上有一个正常运行的 Ubuntu 虚拟机，接下来要做的就是<a href="#how-to-install-nginx-on-a-provisioned-server-or-virtual-machine">安装 NGINX</a>。</p>
<h3 id="">如何配置虚拟专用服务器</h3>
<p>对于本演示，我将使用 <a href="https://vultr.com/">Vultr</a> 作为我的供应商，但你可以使用 <a href="https://digitalocean.com/">DigitalOcean</a> 或你喜欢的任何供应商。</p>
<p>假设你已经拥有提供商的帐户，请登录该帐户并部署新服务器：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/ZUAu_Tpxx-2.jpg" alt="ZUAu_Tpxx-2" width="600" height="400" loading="lazy"></p>
<p>在 DigitalOcean 上，它通常被称为 droplet。在下一个屏幕上，选择靠近你的节点。我住在孟加拉国，所以我选择新加坡：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/zH08EnmGq.jpg" alt="zH08EnmGq" width="600" height="400" loading="lazy"></p>
<p>在下一步中，必须选择操作系统和服务器配置。选择 Ubuntu 20.04 和尽可能小的服务器配置：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/G8mEC13pp.jpg" alt="G8mEC13pp" width="600" height="400" loading="lazy"></p>
<p>尽管生产服务器往往比这更大更强大，但对于本文来说，一台小型服务器就足够了。</p>
<p>最后，在最后一步，将类似 <strong>nginx-handbook-demo-server</strong> 之类的东西作为服务器主机和标签。如果必要，甚至可以将它们留空。</p>
<p>一旦对自己的选择感到满意，请继续并按下 <strong>Deploy Now</strong> 按钮。</p>
<p>部署过程可能需要一些时间才能完成，一旦完成，将在仪表板上看到新创建的服务器：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/server-list.png" alt="server-list" width="600" height="400" loading="lazy"></p>
<p>还要注意 <strong>Status –</strong> 它应该是 <strong>Running</strong> 而不是 <strong>Preparing</strong> 或 <strong>Stopped</strong>。 要连接到服务器，还需要用户名和密码。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/server-overview.png" alt="server-overview" width="600" height="400" loading="lazy"></p>
<p>进入服务器的概览页面，应该会看到服务器的 IP 地址、用户名和密码：</p>
<p>使用 SSH 登录服务器的命令如下：</p>
<pre><code class="language-shell">ssh &lt;username&gt;@&lt;ip address&gt;
</code></pre>
<p>所以就我的服务器而言，它将是：</p>
<pre><code class="language-shell">ssh root@45.77.251.108

# Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
# Warning: Permanently added '45.77.251.108' (ECDSA) to the list of known hosts.

# root@45.77.251.108's password: 
# Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-65-generic x86_64)

# root@localhost:~#
</code></pre>
<p>系统会询问是否要继续连接到此服务器。输入“yes”，然后系统会要求输入密码。从服务器概览页面复制密码并将其粘贴到终端中。</p>
<p>如果做的一切顺利，会成功登录到你的服务器——终端上会看到 <code>root@localhost</code> 行。这里的“localhost”是服务器主机名，你的显示可能会有所不同。</p>
<p>可以通过其 IP 地址直接访问该服务器。或者，如果拥有任何自定义域名，也可以使用它。</p>
<p>在整篇文章中，将看到我将测试域名添加到我的操作系统的 <code>hosts</code> 文件中。如果是真实服务器，则必须使用 DNS 提供商配置这些服务器。</p>
<p>请记住，只要此服务器正在使用，就会被收费。虽然收费应该很少，但我还是要警告你。可以通过点击服务器概览页面上的垃圾桶图标随时销毁服务器：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/image-90.png" alt="image-90" width="600" height="400" loading="lazy"></p>
<p>如果拥有自定义域名，则可以为此服务器分配一个子域名。现在已进入服务器，剩下的就是 <a href="#how-to-install-nginx-on-a-provisioned-server-or-virtual-machine">安装 NGINX</a>。</p>
<h3 id="nginx">如何在配置的服务器或虚拟机上安装 NGINX</h3>
<p>假设已登录到服务器或虚拟机，第一件应该做的事就是执行更新。执行以下命令来执行此操作：</p>
<pre><code class="language-shell">sudo apt update &amp;&amp; sudo apt upgrade -y
</code></pre>
<p>更新后，通过执行以下命令安装 NGINX：</p>
<pre><code class="language-shell">sudo apt install nginx -y
</code></pre>
<p>安装完成后，NGINX 应自动注册为 <code>systemd</code> 服务并运行。要检查，请执行以下命令：</p>
<pre><code class="language-shell">sudo systemctl status nginx

# ● nginx.service - A high performance web server and a reverse proxy server
#      Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
#      Active: active (running)
</code></pre>
<p>如果状态显示 <code>running</code>，那么就可以开始了。否则，可以通过执行以下命令来启动服务：</p>
<pre><code class="language-shell">sudo systemctl start nginx
</code></pre>
<p>最后，为了直观地验证一切是否正常，请使用喜欢的浏览器访问服务器/虚拟机，应该会看到 NGINX 的默认欢迎页面：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/image-89.png" alt="image-89" width="600" height="400" loading="lazy"></p>
<p>NGINX 通常安装在 <code>/etc/nginx</code> 目录中，我们接下来的大部分工作都将在这里完成。</p>
<p>恭喜！已经在服务器/虚拟机上启动并运行了 NGINX。现在是时候先进入 NGINX 了。</p>
<h2 id="nginx">NGINX 的配置文件介绍</h2>
<p>作为 Web 服务器，NGINX 的工作是为客户端提供静态或动态内容。但是如何提供这些内容通常由配置文件控制。</p>
<p>NGINX 的配置文件以<code>.conf</code> 扩展名结尾，通常位于<code>/etc/nginx/</code> 目录中。让我们先通过 <code>cd</code> 进入这个目录并获取所有文件的列表：</p>
<pre><code class="language-shell">cd /etc/nginx

ls -lh

# drwxr-xr-x 2 root root 4.0K Apr 21  2020 conf.d
# -rw-r--r-- 1 root root 1.1K Feb  4  2019 fastcgi.conf
# -rw-r--r-- 1 root root 1007 Feb  4  2019 fastcgi_params
# -rw-r--r-- 1 root root 2.8K Feb  4  2019 koi-utf
# -rw-r--r-- 1 root root 2.2K Feb  4  2019 koi-win
# -rw-r--r-- 1 root root 3.9K Feb  4  2019 mime.types
# drwxr-xr-x 2 root root 4.0K Apr 21  2020 modules-available
# drwxr-xr-x 2 root root 4.0K Apr 17 14:42 modules-enabled
# -rw-r--r-- 1 root root 1.5K Feb  4  2019 nginx.conf
# -rw-r--r-- 1 root root  180 Feb  4  2019 proxy_params
# -rw-r--r-- 1 root root  636 Feb  4  2019 scgi_params
# drwxr-xr-x 2 root root 4.0K Apr 17 14:42 sites-available
# drwxr-xr-x 2 root root 4.0K Apr 17 14:42 sites-enabled
# drwxr-xr-x 2 root root 4.0K Apr 17 14:42 snippets
# -rw-r--r-- 1 root root  664 Feb  4  2019 uwsgi_params
# -rw-r--r-- 1 root root 3.0K Feb  4  2019 win-utf
</code></pre>
<p>在这些文件中，应该有一个名为 <strong>nginx.conf</strong> 的文件。这是 NGINX 的主要配置文件。可以使用 <code>cat</code> 程序查看此文件的内容：</p>
<pre><code class="language-shell">cat nginx.conf# user www-data;# worker_processes auto;# pid /run/nginx.pid;# include /etc/nginx/modules-enabled/*.conf;# events {#     worker_connections 768;#     # multi_accept on;# }# http {#     ###     # Basic Settings#     ###     sendfile on;#     tcp_nopush on;#     tcp_nodelay on;#     keepalive_timeout 65;#     types_hash_max_size 2048;#     # server_tokens off;#     # server_names_hash_bucket_size 64;#     # server_name_in_redirect off;#     include /etc/nginx/mime.types;#     default_type application/octet-stream;#     ###     # SSL Settings#     ###     ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE#     ssl_prefer_server_ciphers on;#     ###     # Logging Settings#     ###     access_log /var/log/nginx/access.log;#     error_log /var/log/nginx/error.log;#     ###     # Gzip Settings#     ###     gzip on;#     # gzip_vary on;#     # gzip_proxied any;#     # gzip_comp_level 6;#     # gzip_buffers 16 8k;#     # gzip_http_version 1.1;#     # gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;#     ###     # Virtual Host Configs#     ###     include /etc/nginx/conf.d/*.conf;#     include /etc/nginx/sites-enabled/*;# }# #mail {# #    # See sample authentication script at:# #    # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript# # # #    # auth_http localhost/auth.php;# #    # pop3_capabilities "TOP" "USER";# #    # imap_capabilities "IMAP4rev1" "UIDPLUS";# # # #    server {# #        listen     localhost:110;# #        protocol   pop3;# #        proxy      on;# #    }# # # #    server {# #        listen     localhost:143;# #        protocol   imap;# #        proxy      on;# #    }# #}
</code></pre>
<p>哇！东西很多。试图在当前状态下理解这个文件将是一场噩梦。因此，让我们备份文件并创建一个新的空文件：</p>
<pre><code class="language-shell"># renames the filesudo mv nginx.conf nginx.conf.backup# creates a new filesudo touch nginx.conf
</code></pre>
<p>我<strong>强烈不鼓励</strong>你编辑原始的 <code>nginx.conf</code> 文件，除非你完全知道你在做什么。出于学习目的，可以重命名以备份它，但是<a href="#understanding-the-main-configuration-file">稍后</a>，我将向你展示在真实场景中应该如何配置服务器。</p>
<h2 id="web">如何配置基本 Web 服务器</h2>
<p>在本书的这一部分中，将最终通过从头开始配置一个基本的静态 Web 服务器来动手实践。本节的目的是向你介绍 NGINX 配置文件的语法和基本概念。</p>
<h3 id="">如何编写你的第一个配置文件</h3>
<p>首先使用 <a href="https://www.nano-editor.org/">nano</a> 文本编辑器打开新创建的 <code>nginx.conf</code> 文件：</p>
<pre><code class="language-shell">sudo nano /etc/nginx/nginx.conf
</code></pre>
<p>在整本书中，我将使用 nano 作为我的文本编辑器。如果你愿意，可以使用更流行的编辑器，但在现实生活场景中，你最有可能在服务器上使用 <a href="https://www.nano-editor.org/">nano</a> 或 [vim](https: <a href="//www.vim.org/">//www.vim.org/</a>) ，而不是其他任何东西。因此，请以本书为契机，提高你的 Nano 技能。此外，官方<a href="https://www.nano-editor.org/dist/latest/cheatsheet.html">备忘单</a> 随时供你参考。</p>
<p>打开文件后，将其内容更新为：</p>
<pre><code class="language-conf">events {}http {    server {        listen 80;        server_name nginx-handbook.test;        return 200 "Bonjour, mon ami!\n";    }}
</code></pre>
<p>如果你有构建 REST API 的经验，那么你可能会从 <code>return 200 "Bonjour, mon ami!\n";</code> 行已经看出来服务器已配置状态代码 200 的消息“Bonjour, mon ami ！”。</p>
<p>如果你目前一头雾水，请不要担心，我将逐行解释这个文件，但首先让我们看看这个配置的实际效果。</p>
<h3 id="">如何校验并重新加载配置文件</h3>
<p>编写新配置文件或更新旧配置文件后，首先要做的是检查文件是否存在语法错误。<code>nginx</code> 二进制文件包含一个选项 <code>-t</code> 来做到这一点。</p>
<pre><code class="language-shell">sudo nginx -t# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok# nginx: configuration file /etc/nginx/nginx.conf test is successful
</code></pre>
<p>如果有任何语法错误，此命令将会展示错误信息以及出错的行号。</p>
<p>虽然配置文件没问题，但 NGINX 配置并不会立即生效。NGINX 的工作方式是它读取一次配置文件并在此基础上持续工作。</p>
<p>如果更新配置文件，则必须明确指示 NGINX 重新加载配置文件。有两种方法可以做到这一点。</p>
<ul>
<li>可以通过执行 <code>sudo systemctl restart nginx</code> 命令来重启 NGINX 服务。</li>
<li>可以通过执行 <code>sudo nginx -s reload</code> 命令向 NGINX 发送 <code>reload</code> 信号。</li>
</ul>
<p><code>-s</code> 选项用于向 NGINX 发送各种信号。可用的信号是 <code>stop</code>、<code>quit</code>、<code>reload</code> 和 <code>reopen</code>。在我刚刚提到的两种方式中，我更喜欢第二种方式，因为它打字较少。</p>
<p>一旦通过执行 <code>nginx -s reload</code> 命令重新加载了配置文件，就可以通过向服务器发送一个简单的 get 请求来查看它的运行情况：</p>
<pre><code class="language-shell">curl -i http://nginx-handbook.test# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Wed, 21 Apr 2021 10:03:33 GMT# Content-Type: text/plain# Content-Length: 18# Connection: keep-alive# Bonjour, mon ami!
</code></pre>
<p>服务器响应状态代码 200 以及预期的消息。恭喜你走到这一步！现在是解释的时候了。</p>
<h3 id="nginx">如何理解 NGINX 中的指令和上下文</h3>
<p>在这里编写的几行代码虽然看似简单，但却介绍了 NGINX 配置文件的两个最重要的术语。它们是<strong>指令</strong>和<strong>上下文</strong>。</p>
<p>从技术上讲，NGINX 配置文件中的所有内容都是 ** 指令**。指令有两种类型：</p>
<ul>
<li>简单指令</li>
<li>块指令</li>
</ul>
<p>一个简单的指令由指令名称和空格分隔的参数组成，如 <code>listen</code>、<code>return</code> 等。简单指令以分号结束。</p>
<p>块指令类似于简单指令，不同之处在于它们不是以分号结尾，而是以一对花括号 <code>{}</code> 括起的附加指令。</p>
<p>能够在其中包含其他指令的块指令称为上下文，即 <code>events</code>、<code>http</code> 等。NGINX 中有四个核心上下文：</p>
<ul>
<li><code>events { }</code> – <code>events</code> 上下文用于设置关于 NGINX 如何在一般级别处理请求的全局配置。一个有效的配置文件中只能有一个 <code>events</code> 上下文。</li>
<li><code>http { }</code> – 顾名思义，<code>http</code> 上下文用于定义有关服务器将如何处理 HTTP 和 HTTPS 请求的配置。一个有效的配置文件中只能有一个 <code>http</code> 上下文。</li>
<li><code>server { }</code> – <code>server</code> 上下文嵌套在 <code>http</code> 上下文中，用于在单个主机内配置特定的虚拟服务器。在嵌套在 <code>http</code> 上下文中的有效配置文件中可以有多个 <code>server</code> 上下文。每个“服务器”上下文都被认为是一个虚拟主机。</li>
<li><code>main</code> – <code>main</code> 上下文是配置文件本身。在前面提到的三个上下文之外编写的任何内容都在 <code>main</code>上下文中。</li>
</ul>
<p>可以将 NGINX 中的上下文视为其他编程语言中的作用域。它们之间也有一种继承关系。可以在官方 NGINX 文档中找到<a href="https://nginx.org/en/docs/dirindex.html">指令的字母索引</a>。</p>
<p>我已经提到在一个配置文件中可以有多个 <code>server</code> 上下文。但是当请求到达服务器时，NGINX 如何知道哪一个上下文中应该处理请求？</p>
<p><code>listen</code> 指令是在配置中识别正确的 <code>server</code> 上下文的方法之一。考虑以下场景：</p>
<pre><code>http {    server {        listen 80;        server_name nginx-handbook.test;        return 200 "hello from port 80!\n";    }    server {        listen 8080;        server_name nginx-handbook.test;        return 200 "hello from port 8080!\n";    }}
</code></pre>
<p>现在，如果向 <a href="http://nginx-handbook.test:80">http://nginx-handbook.test:80</a> 发送请求，那么将收到“hello from port 80!” 响应。如果向 <a href="http://nginx-handbook.test:8080">http://nginx-handbook.test:8080</a> 发送请求，将收到“hello from port 8080!” 响应：</p>
<pre><code>curl nginx-handbook.test:80# hello from port 80!curl nginx-handbook.test:8080# hello from port 8080!
</code></pre>
<p>这两个服务器块就像两个人拿着电话听筒，等待指定电话号码呼入时响应。它们的电话号码由 <code>listen</code> 指令指示。</p>
<p>除了<code>listen</code> 指令，还有 <code>server_name</code> 指令。考虑以下虚构图书馆管理应用程序的场景：</p>
<pre><code>http {    server {        listen 80;        server_name library.test;        return 200 "your local library!\n";    }    server {        listen 80;        server_name librarian.library.test;        return 200 "welcome dear librarian!\n";    }}
</code></pre>
<p>这是虚拟主机思想的一个基本例子。正在同一台服务器中以不同的服务器名称运行两个单独的应用程序。</p>
<p>如果向 <a href="http://library.test">http://library.test</a> 发送请求，那么将获得 “your local library!” 响应。如果向 <a href="http://librarian.library.test">http://librarian.library.test</a> 发送请求，将收到 “welcome dear librarian!” 响应。</p>
<pre><code class="language-shell">curl http://library.test

# your local library!

curl http://librarian.library.test

# welcome dear librarian!
</code></pre>
<p>为了让这个演示在你的系统上运行，你必须更新你的 <code>hosts</code> 文件使其包含这两个域名：</p>
<pre><code class="language-hosts">192.168.20.20   library.test
192.168.20.20   librarian.library.test
</code></pre>
<p>最后，<code>return</code> 指令负责向用户返回一个有效的响应。该指令包含两个参数：状态代码和要返回的字符串消息。</p>
<h3 id="nginx">如何使用 NGINX 提供静态内容</h3>
<p>现在已经很好地了解了如何为 NGINX 编写基本配置文件，让我们升级配置以提供静态文件而不是纯文本响应。</p>
<p>为了提供静态内容，首先必须将它们存储在服务器上的某个位置。如果你使用 <code>ls</code> 列出服务器根目录下的文件和目录，会在那里找到一个名为 <code>/srv</code> 的目录：</p>
<pre><code class="language-shell">ls -lh /

# lrwxrwxrwx   1 root    root       7 Apr 16 02:10 bin -&gt; usr/bin
# drwxr-xr-x   3 root    root    4.0K Apr 16 02:13 boot
# drwxr-xr-x  16 root    root    3.8K Apr 21 09:23 dev
# drwxr-xr-x  92 root    root    4.0K Apr 21 09:24 etc
# drwxr-xr-x   4 root    root    4.0K Apr 21 08:04 home
# lrwxrwxrwx   1 root    root       7 Apr 16 02:10 lib -&gt; usr/lib
# lrwxrwxrwx   1 root    root       9 Apr 16 02:10 lib32 -&gt; usr/lib32
# lrwxrwxrwx   1 root    root       9 Apr 16 02:10 lib64 -&gt; usr/lib64
# lrwxrwxrwx   1 root    root      10 Apr 16 02:10 libx32 -&gt; usr/libx32
# drwx------   2 root    root     16K Apr 16 02:15 lost+found
# drwxr-xr-x   2 root    root    4.0K Apr 16 02:10 media
# drwxr-xr-x   2 root    root    4.0K Apr 16 02:10 mnt
# drwxr-xr-x   2 root    root    4.0K Apr 16 02:10 opt
# dr-xr-xr-x 152 root    root       0 Apr 21 09:23 proc
# drwx------   5 root    root    4.0K Apr 21 09:59 root
# drwxr-xr-x  26 root    root     820 Apr 21 09:47 run
# lrwxrwxrwx   1 root    root       8 Apr 16 02:10 sbin -&gt; usr/sbin
# drwxr-xr-x   6 root    root    4.0K Apr 16 02:14 snap
# drwxr-xr-x   2 root    root    4.0K Apr 16 02:10 srv
# dr-xr-xr-x  13 root    root       0 Apr 21 09:23 sys
# drwxrwxrwt  11 root    root    4.0K Apr 21 09:24 tmp
# drwxr-xr-x  15 root    root    4.0K Apr 16 02:12 usr
# drwxr-xr-x   1 vagrant vagrant   38 Apr 21 09:23 vagrant
# drwxr-xr-x  14 root    root    4.0K Apr 21 08:34 var
</code></pre>
<p>这个<code>/srv</code> 目录旨在包含由该系统提供的特定于站点的数据。现在 <code>cd</code> 进入这个目录并克隆本书附带的代码库：</p>
<pre><code>cd /srvsudo git clone https://github.com/fhsinchy/nginx-handbook-projects.git
</code></pre>
<p>在 <code>nginx-handbook-projects</code> 目录中应该有一个名为 <code>static-demo</code> 的目录，总共包含四个文件：</p>
<pre><code class="language-shell">ls -lh /srv/nginx-handbook-projects/static-demo# -rw-r--r-- 1 root root 960 Apr 21 11:27 about.html# -rw-r--r-- 1 root root 960 Apr 21 11:27 index.html# -rw-r--r-- 1 root root 46K Apr 21 11:27 mini.min.css# -rw-r--r-- 1 root root 19K Apr 21 11:27 the-nginx-handbook.jpg
</code></pre>
<p>现在有了要提供的静态内容，请按如下方式更新配置：</p>
<pre><code class="language-conf">events {}http {    server {        listen 80;        server_name nginx-handbook.test;        root /srv/nginx-handbook-projects/static-demo;    }}
</code></pre>
<p>代码几乎相同，除了 <code>return</code> 指令现在已被替换为 <code>root</code> 指令。该指令用于声明站点的根目录。</p>
<p>通过编写 <code>root /srv/nginx-handbook-projects/static-demo</code> 告诉 NGINX 在有任何请求时在这个服务器的 <code>/srv/nginx-handbook-projects/static-demo</code> 目录中查找要提供的文件。由于 NGINX 是一个 Web 服务器，它非常聪明，可以默认为 <code>index.html</code> 文件提供服务。</p>
<p>让我们看看这是否有效。测试并重新加载更新的配置文件并访问服务器，应该会看到一个不完整的 HTML 站点：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/image-91.png" alt="image-91" width="600" height="400" loading="lazy"></p>
<p>尽管 NGINX 已正确提供 index.html 文件，但从三个导航链接的外观来看，似乎 CSS 代码不起作用。</p>
<p>你可能认为 CSS 文件中有问题。但实际上，问题出在配置文件中。</p>
<h3 id="nginx">NGINX 中的静态文件类型处理</h3>
<p>要调试现在面临的问题，向服务器发送 CSS 文件的请求：</p>
<pre><code class="language-shell">curl -I http://nginx-handbook/mini.min.css

# HTTP/1.1 200 OK
# Server: nginx/1.18.0 (Ubuntu)
# Date: Wed, 21 Apr 2021 12:17:16 GMT
# Content-Type: text/plain
# Content-Length: 46887
# Last-Modified: Wed, 21 Apr 2021 11:27:06 GMT
# Connection: keep-alive
# ETag: "60800c0a-b727"
# Accept-Ranges: bytes
</code></pre>
<p>注意 <strong>Content-Type</strong> 并查看展示 <strong>text/plain</strong> 而不是 <strong>text/css</strong>。这意味着 NGINX 将此文件作为纯文本而不是样式表提供。</p>
<p>尽管 NGINX 足够聪明，默认情况下可以找到 <code>index.html</code> 文件，但在解释文件类型时它非常愚蠢。要解决此问题，再次更新配置：</p>
<pre><code class="language-conf">events {

}

http {

    types {
        text/html html;
        text/css css;
    }

    server {

        listen 80;
        server_name nginx-handbook.test;

        root /srv/nginx-handbook-projects/static-demo;
    }
}
</code></pre>
<p>我们对代码所做的唯一更改是嵌套在 <code>http</code> 块中的新 <code>types</code> 上下文。可能已经从名称中猜到，此上下文用于配置文件类型。</p>
<p>通过在此上下文中编写 <code>text/html html</code>，你告诉 NGINX 将任何以 <code>html</code> 扩展名结尾的文件解析为 <code>text/html</code>。</p>
<p>可能认为配置 CSS 文件类型就足够了，因为 HTML 已经被很好地解析——但并非如此。</p>
<p>如果在配置中引入 <code>types</code> 上下文，NGINX 会变得更加笨拙，只会解析你配置的文件。因此，如果只在此上下文中定义了 <code>text/css css</code>，那么 NGINX 将开始将 HTML 文件解析为纯文本。</p>
<p>验证并重新加载新更新的配置文件并再次访问服务器。再次发送对 CSS 文件的请求，这次文件应该被解析为 <strong>text/css</strong> 文件了：</p>
<pre><code class="language-shell">curl -I http://nginx-handbook.test/mini.min.css

# HTTP/1.1 200 OK
# Server: nginx/1.18.0 (Ubuntu)
# Date: Wed, 21 Apr 2021 12:29:35 GMT
# Content-Type: text/css
# Content-Length: 46887
# Last-Modified: Wed, 21 Apr 2021 11:27:06 GMT
# Connection: keep-alive
# ETag: "60800c0a-b727"
# Accept-Ranges: bytes
</code></pre>
<p>访问服务器进行视觉验收，这次站点应该看起来更好：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/image-92.png" alt="image-92" width="600" height="400" loading="lazy"></p>
<p>如果已正确更新并重新加载配置文件，但仍看到旧站点，请执行强制刷新。</p>
<h3 id="">如何引入部分配置文件</h3>
<p>在 <code>types</code> 上下文中映射文件类型可能适用于小型项目，但对于较大的项目，它可能很麻烦且容易出错。</p>
<p>NGINX 为这个问题提供了解决方案。如果你再次列出 <code>/etc/nginx</code> 目录中的文件，会看到一个名为 <code>mime.types</code> 的文件。</p>
<pre><code class="language-shell">ls -lh /etc/nginx

# drwxr-xr-x 2 root root 4.0K Apr 21  2020 conf.d
# -rw-r--r-- 1 root root 1.1K Feb  4  2019 fastcgi.conf
# -rw-r--r-- 1 root root 1007 Feb  4  2019 fastcgi_params
# -rw-r--r-- 1 root root 2.8K Feb  4  2019 koi-utf
# -rw-r--r-- 1 root root 2.2K Feb  4  2019 koi-win
# -rw-r--r-- 1 root root 3.9K Feb  4  2019 mime.types
# drwxr-xr-x 2 root root 4.0K Apr 21  2020 modules-available
# drwxr-xr-x 2 root root 4.0K Apr 17 14:42 modules-enabled
# -rw-r--r-- 1 root root 1.5K Feb  4  2019 nginx.conf
# -rw-r--r-- 1 root root  180 Feb  4  2019 proxy_params
# -rw-r--r-- 1 root root  636 Feb  4  2019 scgi_params
# drwxr-xr-x 2 root root 4.0K Apr 17 14:42 sites-available
# drwxr-xr-x 2 root root 4.0K Apr 17 14:42 sites-enabled
# drwxr-xr-x 2 root root 4.0K Apr 17 14:42 snippets
# -rw-r--r-- 1 root root  664 Feb  4  2019 uwsgi_params
# -rw-r--r-- 1 root root 3.0K Feb  4  2019 win-utf
</code></pre>
<p>我们来看看这个文件的内容：</p>
<pre><code class="language-shell">cat /etc/mime.types

# types {
#     text/html                             html htm shtml;
#     text/css                              css;
#     text/xml                              xml;
#     image/gif                             gif;
#     image/jpeg                            jpeg jpg;
#     application/javascript                js;
#     application/atom+xml                  atom;
#     application/rss+xml                   rss;

#     text/mathml                           mml;
#     text/plain                            txt;
#     text/vnd.sun.j2me.app-descriptor      jad;
#     text/vnd.wap.wml                      wml;
#     text/x-component                      htc;

#     image/png                             png;
#     image/tiff                            tif tiff;
#     image/vnd.wap.wbmp                    wbmp;
#     image/x-icon                          ico;
#     image/x-jng                           jng;
#     image/x-ms-bmp                        bmp;
#     image/svg+xml                         svg svgz;
#     image/webp                            webp;

#     application/font-woff                 woff;
#     application/java-archive              jar war ear;
#     application/json                      json;
#     application/mac-binhex40              hqx;
#     application/msword                    doc;
#     application/pdf                       pdf;
#     application/postscript                ps eps ai;
#     application/rtf                       rtf;
#     application/vnd.apple.mpegurl         m3u8;
#     application/vnd.ms-excel              xls;
#     application/vnd.ms-fontobject         eot;
#     application/vnd.ms-powerpoint         ppt;
#     application/vnd.wap.wmlc              wmlc;
#     application/vnd.google-earth.kml+xml  kml;
#     application/vnd.google-earth.kmz      kmz;
#     application/x-7z-compressed           7z;
#     application/x-cocoa                   cco;
#     application/x-java-archive-diff       jardiff;
#     application/x-java-jnlp-file          jnlp;
#     application/x-makeself                run;
#     application/x-perl                    pl pm;
#     application/x-pilot                   prc pdb;
#     application/x-rar-compressed          rar;
#     application/x-redhat-package-manager  rpm;
#     application/x-sea                     sea;
#     application/x-shockwave-flash         swf;
#     application/x-stuffit                 sit;
#     application/x-tcl                     tcl tk;
#     application/x-x509-ca-cert            der pem crt;
#     application/x-xpinstall               xpi;
#     application/xhtml+xml                 xhtml;
#     application/xspf+xml                  xspf;
#     application/zip                       zip;

#     application/octet-stream              bin exe dll;
#     application/octet-stream              deb;
#     application/octet-stream              dmg;
#     application/octet-stream              iso img;
#     application/octet-stream              msi msp msm;

#     application/vnd.openxmlformats-officedocument.wordprocessingml.document    docx;
#     application/vnd.openxmlformats-officedocument.spreadsheetml.sheet          xlsx;
#     application/vnd.openxmlformats-officedocument.presentationml.presentation  pptx;

#     audio/midi                            mid midi kar;
#     audio/mpeg                            mp3;
#     audio/ogg                             ogg;
#     audio/x-m4a                           m4a;
#     audio/x-realaudio                     ra;

#     video/3gpp                            3gpp 3gp;
#     video/mp2t                            ts;
#     video/mp4                             mp4;
#     video/mpeg                            mpeg mpg;
#     video/quicktime                       mov;
#     video/webm                            webm;
#     video/x-flv                           flv;
#     video/x-m4v                           m4v;
#     video/x-mng                           mng;
#     video/x-ms-asf                        asx asf;
#     video/x-ms-wmv                        wmv;
#     video/x-msvideo                       avi;
# }
</code></pre>
<p>该文件包含一长串文件类型及其扩展名。要在配置文件中使用此文件，请将配置更新为如下所示：</p>
<pre><code class="language-conf">events {

}

http {

    include /etc/nginx/mime.types;

    server {

        listen 80;
        server_name nginx-handbook.test;

        root /srv/nginx-handbook-projects/static-demo;
    }

}
</code></pre>
<p>旧的 <code>types</code> 上下文现在已替换为新的 <code>include</code> 指令。顾名思义，该指令允许包含来自其他配置文件的内容。</p>
<p>验证并重新加载配置文件并再次发送对 <code>mini.min.css</code> 文件的请求：</p>
<pre><code class="language-shell">curl -I http://nginx-handbook.test/mini.min.css

# HTTP/1.1 200 OK
# Server: nginx/1.18.0 (Ubuntu)
# Date: Wed, 21 Apr 2021 12:29:35 GMT
# Content-Type: text/css
# Content-Length: 46887
# Last-Modified: Wed, 21 Apr 2021 11:27:06 GMT
# Connection: keep-alive
# ETag: "60800c0a-b727"
# Accept-Ranges: bytes
</code></pre>
<p>在下面关于如何理解主配置文件的部分中，我将演示如何使用 <code>include</code> 来模块化虚拟服务器配置。</p>
<h2 id="nginx">NGINX 中的动态路由</h2>
<p>在上一节中编写的配置是一个非常简单的静态内容服务器配置。它所做的只是匹配来自与客户端访问的 URI 相对应的站点根目录的文件并进行响应。</p>
<p>因此，如果客户端请求根目录中存在文件，例如 <code>index.html</code>、<code>about.html</code> 或 <code>mini.min.css</code>，NGINX 将返回该文件。但是，如果访问诸如 <a href="http://nginx-handbook.test/nothing">http://nginx-handbook.test/nothing</a> 之类的路由，它将以默认的 404 页面响应：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/image-93.png" alt="image-93" width="600" height="400" loading="lazy"></p>
<p>在本书的这一部分，将了解 <code>location</code> 上下文、变量、重定向、重写和 <code>try_files</code> 指令。本节中不会有新项目，但在此处学习的概念将在接下来的部分中必不可少。</p>
<p>此外，本节中的配置更改非常频繁，因此请不要忘记在每次更新后验证并重新加载配置文件。</p>
<h3 id="">位置匹配</h3>
<p>我们将在本节中讨论的第一个概念是 <code>location</code>  上下文。更新配置如下：</p>
<pre><code class="language-conf">events {

}

http {

    server {

        listen 80;
        server_name nginx-handbook.test;

        location /agatha {
            return 200 "Miss Marple.\nHercule Poirot.\n";
        }
    }
}
</code></pre>
<p>我们已经用新的 <code>location</code> 上下文替换了 <code>root</code> 指令。这个上下文通常嵌套在 <code>server</code> 块中。<code>server</code> 上下文中可以有多个 <code>location</code> 上下文。</p>
<p>如果向 <a href="http://nginx-handbook.test/agatha">http://nginx-handbook.test/agatha</a> 发送请求，将获得 200 响应代码和 <a href="https://en.wikipedia.org/wiki/Agatha_Christie">Agatha Christie</a> 创建的字符列表。</p>
<pre><code class="language-shell">curl -i http://nginx-handbook.test/agatha

# HTTP/1.1 200 OK
# Server: nginx/1.18.0 (Ubuntu)
# Date: Wed, 21 Apr 2021 15:59:07 GMT
# Content-Type: text/plain
# Content-Length: 29
# Connection: keep-alive

# Miss Marple.
# Hercule Poirot.
</code></pre>
<p>现在，如果向 <a href="http://nginx-handbook.test/agatha-christie">http://nginx-handbook.test/agatha-christie</a> 发送请求，将得到相同的响应：</p>
<pre><code class="language-shell">curl -i http://nginx-handbook.test/agatha-christie# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Wed, 21 Apr 2021 15:59:07 GMT# Content-Type: text/plain# Content-Length: 29# Connection: keep-alive# Miss Marple.# Hercule Poirot.
</code></pre>
<p>发生这种情况是因为，通过编写 <code>location /agatha</code>，告诉 NGINX 匹配任何以“agatha”开头的 URI。这种匹配称为<strong>前缀匹配</strong>。</p>
<p>要执行<strong>完全匹配</strong>，必须按如下方式更新代码：</p>
<pre><code class="language-conf">events {}http {    server {        listen 80;        server_name nginx-handbook.test;        location = /agatha {            return 200 "Miss Marple.\nHercule Poirot.\n";        }    }}
</code></pre>
<p>在位置 URI 之前添加一个 <code>=</code> 符号将指示 NGINX 只有在 URL 完全匹配时才响应。现在，如果向除 <code>/agatha</code> 之外的任何内容发送请求，都将收到 404 响应。</p>
<pre><code class="language-shell">curl -I http://nginx-handbook.test/agatha-christie# HTTP/1.1 404 Not Found# Server: nginx/1.18.0 (Ubuntu)# Date: Wed, 21 Apr 2021 16:14:29 GMT# Content-Type: text/html# Content-Length: 162# Connection: keep-alivecurl -I http://nginx-handbook.test/agatha# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Wed, 21 Apr 2021 16:15:04 GMT# Content-Type: text/plain# Content-Length: 29# Connection: keep-alive
</code></pre>
<p>NGINX 中的另一种匹配是 <strong>regex 匹配</strong>。使用此匹配，可以根据复杂的正则表达式检查 URL location。</p>
<pre><code class="language-conf">events {}http {    server {        listen 80;        server_name nginx-handbook.test;        location ~ /agatha[0-9] {        	return 200 "Miss Marple.\nHercule Poirot.\n";        }    }}
</code></pre>
<p>通过将之前使用的 <code>=</code> 符号替换为 <code>~</code> 符号，来告诉 NGINX 执行正则表达式匹配。将 location 设置为 <code>~ /agatha[0-9]</code> 意味着 NIGINX 只有在单词“agatha”后面有数字时才会响应：</p>
<pre><code class="language-shell">curl -I http://nginx-handbook.test/agatha# HTTP/1.1 404 Not Found# Server: nginx/1.18.0 (Ubuntu)# Date: Wed, 21 Apr 2021 16:14:29 GMT# Content-Type: text/html# Content-Length: 162# Connection: keep-alivecurl -I http://nginx-handbook.test/agatha8# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Wed, 21 Apr 2021 16:15:04 GMT# Content-Type: text/plain# Content-Length: 29# Connection: keep-alive
</code></pre>
<p>默认情况下，正则表达式匹配区分大小写，这意味着如果将任何字母大写，则该 location 将不起作用：</p>
<pre><code class="language-shell">curl -I http://nginx-handbook.test/Agatha8# HTTP/1.1 404 Not Found# Server: nginx/1.18.0 (Ubuntu)# Date: Wed, 21 Apr 2021 16:14:29 GMT# Content-Type: text/html# Content-Length: 162# Connection: keep-alive
</code></pre>
<p>要将其转换为不区分大小写，必须在 <code>~</code> 符号后添加一个 <code>*</code>。</p>
<pre><code class="language-conf">events {}http {    server {        listen 80;        server_name nginx-handbook.test;        location ~* /agatha[0-9] {        	return 200 "Miss Marple.\nHercule Poirot.\n";        }    }}
</code></pre>
<p>这将告诉 NGINX 大小写不敏感匹配 location。</p>
<pre><code class="language-shell">curl -I http://nginx-handbook.test/agatha8# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Wed, 21 Apr 2021 16:15:04 GMT# Content-Type: text/plain# Content-Length: 29# Connection: keep-alivecurl -I http://nginx-handbook.test/Agatha8# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Wed, 21 Apr 2021 16:15:04 GMT# Content-Type: text/plain# Content-Length: 29# Connection: keep-alive
</code></pre>
<p>NGINX 为这些匹配分配优先级值，正则匹配比前缀匹配具有更高的优先级。</p>
<pre><code class="language-conf">events {}http {    server {        listen 80;        server_name nginx-handbook.test;		location /Agatha8 {        	return 200 "prefix matched.\n";        }                location ~* /agatha[0-9] {        	return 200 "regex matched.\n";        }    }}
</code></pre>
<p>现在，如果向 <a href="http://nginx-handbook.test/Agatha8">http://nginx-handbook.test/Agatha8</a> 发送请求，将得到以下响应：</p>
<pre><code class="language-shell">curl -i http://nginx-handbook.test/Agatha8# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Thu, 22 Apr 2021 08:08:18 GMT# Content-Type: text/plain# Content-Length: 15# Connection: keep-alive# regex matched.
</code></pre>
<p>但是这个优先级可以稍微改变。NGINX 中的最后一种匹配类型是<strong>优先前缀匹配</strong>。要将前缀匹配转换为优先匹配，需要在位置 URI 之前包含 <code>^~</code> 修饰符：</p>
<pre><code class="language-conf">events {}http {    server {        listen 80;        server_name nginx-handbook.test;		location ^~ /Agatha8 {        	return 200 "prefix matched.\n";        }                location ~* /agatha[0-9] {        	return 200 "regex matched.\n";        }    }}
</code></pre>
<p>现在，如果向 <a href="http://nginx-handbook.test/Agatha8">http://nginx-handbook.test/Agatha8</a> 发送请求，将得到以下响应：</p>
<pre><code class="language-shell">curl -i http://nginx-handbook.test/Agatha8# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Thu, 22 Apr 2021 08:13:24 GMT# Content-Type: text/plain# Content-Length: 16# Connection: keep-alive# prefix matched.
</code></pre>
<p>这一次，前缀匹配优先。所以按优先级降序排列的所有匹配列表如下：</p>
<table>
<thead>
<tr>
<th>Match</th>
<th>Modifier</th>
</tr>
</thead>
<tbody>
<tr>
<td>Exact</td>
<td><code>=</code></td>
</tr>
<tr>
<td>Preferential Prefix</td>
<td><code>^~</code></td>
</tr>
<tr>
<td>REGEX</td>
<td><code>~</code> or <code>~*</code></td>
</tr>
<tr>
<td>Prefix</td>
<td><code>None</code></td>
</tr>
</tbody>
</table>
<h3 id="nginx">NGINX 中的变量</h3>
<p>NGINX 中的变量和其他编程语言中的变量类似。<code>set</code> 指令可用于在配置文件中的任何位置声明新变量：</p>
<pre><code class="language-conf">set $&lt;variable_name&gt; &lt;variable_value&gt;;# set name "Farhan"# set age 25# set is_working true
</code></pre>
<p>变量可以是三种类型</p>
<ul>
<li>字符串</li>
<li>整数</li>
<li>布尔值</li>
</ul>
<p>除了声明的变量之外，NGINX 模块中还有内置的变量。 <a href="https://nginx.org/en/docs/varindex.html">变量的字母索引</a> 可在官方文档中找到。</p>
<p>要查看一些正在运行的变量，按如下方式更新配置：</p>
<pre><code class="language-conf">events {}http {    server {        listen 80;        server_name nginx-handbook.test;        return 200 "Host - $host\nURI - $uri\nArgs - $args\n";    }}
</code></pre>
<p>现在向服务器发送请求后，会得到如下响应：</p>
<pre><code class="language-shell"># curl http://nginx-handbook.test/user?name=Farhan# Host - nginx-handbook.test# URI - /user# Args - name=Farhan
</code></pre>
<p>如你所见，<code>$host</code> 和 <code>$uri</code> 变量分别保存根地址和相对于根的请求 URI。如你所见，<code>$args</code> 变量包含所有查询字符串。</p>
<p>可以使用 <code>$arg</code> 变量访问各个值，而不是打印查询字符串的文字字符串形式。</p>
<pre><code class="language-conf">events {}http {    server {        listen 80;        server_name nginx-handbook.test;                set $name $arg_name; # $arg_&lt;query string name&gt;        return 200 "Name - $name\n";    }}
</code></pre>
<p>现在来自服务器的响应应该如下所示：</p>
<pre><code class="language-shell">curl http://nginx-handbook.test?name=Farhan# Name - Farhan
</code></pre>
<p>我在这里演示的变量嵌入在 <a href="https://nginx.org/en/docs/http/ngx_http_core_module.html">ngx_http_core_module</a> 中。要在配置中访问变量，必须使用嵌入变量的模块构建 NGINX。从源代码构建 NGINX 并使用动态模块稍微超出了本文的范围，但我肯定会在我的博客中写到这一点。</p>
<h3 id="redirectsrewrites">Redirects 和 Rewrites</h3>
<p>NGINX 中的重定向与任何其他平台中的重定向相同。要演示重定向的工作原理，请将配置更新为如下所示：</p>
<pre><code class="language-conf">events {}http {    include /etc/nginx/mime.types;    server {        listen 80;        server_name nginx-handbook.test;        root /srv/nginx-handbook-projects/static-demo;        location = /index_page {                return 307 /index.html;        }        location = /about_page {                return 307 /about.html;        }    }}
</code></pre>
<p>现在，如果向 <a href="http://nginx-handbook.test/about_page">http://nginx-handbook.test/about_page</a> 发送请求，将被重定向到 <a href="http://nginx-handbook.test/about.html%EF%BC%9A">http://nginx-handbook.test/about.html：</a></p>
<pre><code class="language-shell">curl -I http://nginx-handbook.test/about_page# HTTP/1.1 307 Temporary Redirect# Server: nginx/1.18.0 (Ubuntu)# Date: Thu, 22 Apr 2021 18:02:04 GMT# Content-Type: text/html# Content-Length: 180# Location: http://nginx-handbook.test/about.html# Connection: keep-alive
</code></pre>
<p>如你所见，服务器响应状态码为 307，位置指示 <a href="http://nginx-handbook.test/about.html">http://nginx-handbook.test/about.html</a> 。 如果你从浏览器访问 <a href="http://nginx-handbook.test/about_page">http://nginx-handbook.test/about_page</a> ， 会看到 URL 将自动更改为 <a href="http://nginx-handbook.test/about.html">http://nginx-handbook.test/about.html</a> 。</p>
<p>然而，<code>rewrite</code> 指令的工作方式略有不同。 它在内部更改 URI，而不让用户知道。要查看它的实际效果，请按如下方式更新配置：</p>
<pre><code class="language-conf">events {}http {    include /etc/nginx/mime.types;    server {        listen 80;        server_name nginx-handbook.test;        root /srv/nginx-handbook-projects/static-demo;        rewrite /index_page /index.html;        rewrite /about_page /about.html;    }}
</code></pre>
<p>现在，如果向 <a href="http://nginx-handbook/about_page">http://nginx-handbook/about_page</a> URI 发送请求，将响应 200 响应码的 about.html 文件：</p>
<pre><code class="language-shell">curl -i http://nginx-handbook.test/about_page# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Thu, 22 Apr 2021 18:09:31 GMT# Content-Type: text/html# Content-Length: 960# Last-Modified: Wed, 21 Apr 2021 11:27:06 GMT# Connection: keep-alive# ETag: "60800c0a-3c0"# Accept-Ranges: bytes# &lt;!DOCTYPE html&gt;# &lt;html lang="en"&gt;# &lt;head&gt;#     &lt;meta charset="UTF-8"&gt;#     &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;#     &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;#     &lt;title&gt;NGINX Handbook Static Demo&lt;/title&gt;#     &lt;link rel="stylesheet" href="mini.min.css"&gt;#     &lt;style&gt;#         .container {#             max-width: 1024px;#             margin-left: auto;#             margin-right: auto;#         }# #         h1 {#             text-align: center;#         }#     &lt;/style&gt;# &lt;/head&gt;# &lt;body class="container"&gt;#     &lt;header&gt;#         &lt;a class="button" href="index.html"&gt;Index&lt;/a&gt;#         &lt;a class="button" href="about.html"&gt;About&lt;/a&gt;#         &lt;a class="button" href="nothing"&gt;Nothing&lt;/a&gt;#     &lt;/header&gt;#     &lt;div class="card fluid"&gt;#         &lt;img src="./the-nginx-handbook.jpg" alt="The NGINX Handbook Cover Image"&gt;#     &lt;/div&gt;#     &lt;div class="card fluid"&gt;#         &lt;h1&gt;this is the &lt;strong&gt;about.html&lt;/strong&gt; file&lt;/h1&gt;#     &lt;/div&gt;# &lt;/body&gt;# &lt;/html&gt;
</code></pre>
<p>如果使用浏览器访问 URI，将看到 about.html 页面，而 URL 保持不变：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/rewrite.png" alt="rewrite" width="600" height="400" loading="lazy"></p>
<p>除了处理 URI 更改的方式之外，重定向和重写之间还有另一个区别。当重写发生时，NGINX 会重新评估 <code>server</code> 上下文。因此，重写是比重定向更昂贵的操作。</p>
<h3 id="">如何尝试多个文件</h3>
<p>本节要展示的最后一个概念是 <code>try_files</code> 指令。<code>try_files</code> 指令不是响应单个文件，而是一次检查存在的多个文件。</p>
<pre><code class="language-conf">events {}http {    include /etc/nginx/mime.types;    server {        listen 80;        server_name nginx-handbook.test;        root /srv/nginx-handbook-projects/static-demo;        try_files /the-nginx-handbook.jpg /not_found;        location /not_found {                return 404 "sadly, you've hit a brick wall buddy!\n";        }    }}
</code></pre>
<p>如你所见，添加了一个新的 <code>try_files</code> 指令。通过编写<code>try_files /the-nginx-handbook.jpg /not_found;</code>，可以指示NGINX 在收到请求时在根目录中查找名为 the-nginx-handbook.jpg 的文件。如果它不存在，则转到 <code>/not_found</code> 位置。</p>
<p>所以现在如果你访问服务器，你会看到图像：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/image-94.png" alt="image-94" width="600" height="400" loading="lazy"></p>
<p>但是，如果更新配置以尝试使用不存在的文件（例如 blackhole.jpg），将收到 404 响应并显示消息“sadly, you've hit a brick wall buddy!”。</p>
<p>现在用这种方式写一个 <code>try_files</code> 指令的问题是，无论你访问什么 URL，只要服务器收到一个请求并且在磁盘上找到了 -nginx-handbook.jpg 文件，NGINX 就会返回它。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/try-files.png" alt="try-files" width="600" height="400" loading="lazy"></p>
<p>这就是为什么 <code>try_files</code> 经常与 <code>$uri</code> NGINX 变量一起使用的原因。</p>
<pre><code class="language-conf">events {}http {    include /etc/nginx/mime.types;    server {        listen 80;        server_name nginx-handbook.test;        root /srv/nginx-handbook-projects/static-demo;        try_files $uri /not_found;        location /not_found {                return 404 "sadly, you've hit a brick wall buddy!\n";        }    }}
</code></pre>
<p>通过编写 <code>try_files $uri /not_found;</code>，在指示 NGINX 首先尝试获取客户端请求的 URI。如果它没有找到，则尝试下一个。</p>
<p>所以现在如果你访问 <a href="http://nginx-handbook.test/index.html">http://nginx-handbook.test/index.html</a> 应该得到旧的 index.html 页面。about.html 页面也是如此：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/image-95.png" alt="image-95" width="600" height="400" loading="lazy"></p>
<p>但是如果你请求一个不存在的文件，会得到 <code>/not_found</code> location 的响应：</p>
<pre><code class="language-shell">curl -i http://nginx-handbook.test/nothing# HTTP/1.1 404 Not Found# Server: nginx/1.18.0 (Ubuntu)# Date: Thu, 22 Apr 2021 20:01:57 GMT# Content-Type: text/plain# Content-Length: 38# Connection: keep-alive# sadly, you've hit a brick wall buddy!
</code></pre>
<p>可能已经注意到的一件事是，如果访问服务器根目录 <a href="http://nginx-handbook.test">http://nginx-handbook.test</a>，会收到 404 响应。</p>
<p>这是因为当访问服务器根目录时，<code>$uri</code> 变量不对应任何现有文件作为 NGINX 备选 location。如果要解决此问题，请按如下方式更新配置：</p>
<pre><code class="language-conf">events {}http {    include /etc/nginx/mime.types;    server {        listen 80;        server_name nginx-handbook.test;        root /srv/nginx-handbook-projects/static-demo;        try_files $uri $uri/ /not_found;        location /not_found {                return 404 "sadly, you've hit a brick wall buddy!\n";        }    }}
</code></pre>
<p>通过写 <code>try_files $uri $uri//not_found;</code>，指示 NGINX 首先尝试请求 URI。如果不存在，则尝试将请求的 URI 作为目录，并且每当 NGINX 最终进入目录时，它会自动开始查找 index.html 文件。</p>
<p>现在如果访问服务器，应该得到 index.html 文件：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/image-95.png" alt="image-95" width="600" height="400" loading="lazy"></p>
<p><code>try_files</code> 是一种可用于多种变体的指令。在接下来的部分中，会遇到一些其他变体，但我建议在 Internet 上自行研究该指令的不同用法。</p>
<h2 id="nginx">NGINX 日志</h2>
<p>默认情况下，NGINX 的日志文件位于<code>/var/log/nginx</code> 中。 如果列出这个目录的内容，可能会看到如下内容：</p>
<pre><code class="language-shell">ls -lh /var/log/nginx/# -rw-r----- 1 www-data adm     0 Apr 25 07:34 access.log# -rw-r----- 1 www-data adm     0 Apr 25 07:34 error.log
</code></pre>
<p>让我们先清空这两个文件。</p>
<pre><code class="language-shell"># delete the old filessudo rm /var/log/nginx/access.log /var/log/nginx/error.log# create new filessudo touch /var/log/nginx/access.log /var/log/nginx/error.log# reopen the log filessudo nginx -s reopen
</code></pre>
<p>如果不向 NGINX 发送 <code>reopen</code> 信号，它将继续将日志写入先前打开的流，而新文件将保持为空。</p>
<p>现在要在访问日志中创建一个条目，向服务器发送一个请求。</p>
<pre><code>curl -I http://nginx-handbook.test# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Sun, 25 Apr 2021 08:35:59 GMT# Content-Type: text/html# Content-Length: 960# Last-Modified: Sun, 25 Apr 2021 08:35:33 GMT# Connection: keep-alive# ETag: "608529d5-3c0"# Accept-Ranges: bytessudo cat /var/log/nginx/access.log # 192.168.20.20 - - [25/Apr/2021:08:35:59 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.68.0"
</code></pre>
<p>如你所见，access.log 文件中添加了一个新条目。默认情况下，对服务器的任何请求都将记录到此文件中。我们可以使用 <code>access_log</code> 指令来改变这种行为。</p>
<pre><code class="language-conf">events {}http {    include /etc/nginx/mime.types;    server {        listen 80;        server_name nginx-handbook.test;                location / {            return 200 "this will be logged to the default file.\n";        }                location = /admin {            access_log /var/logs/nginx/admin.log;                        return 200 "this will be logged in a separate file.\n";        }                location = /no_logging {            access_log off;                        return 200 "this will not be logged.\n";        }    }}
</code></pre>
<p>/admin location 块中的第一个 <code>access_log</code> 指令指示 NGINX 将此 URI 的任何访问日志写入 <code>/var/logs/nginx/admin.log</code> 文件。 第二个 /no_logging location 中的完全关闭此 location 的访问日志。</p>
<p>验证并重新加载配置。现在，如果向这些位置发送请求并检查日志文件，应该会看到如下内容：</p>
<pre><code class="language-shell">curl http://nginx-handbook.test/no_logging# this will not be loggedsudo cat /var/log/nginx/access.log# emptycurl http://nginx-handbook.test/admin# this will be logged in a separate file.sudo cat /var/log/nginx/access.log# emptysudo cat /var/log/nginx/admin.log # 192.168.20.20 - - [25/Apr/2021:11:13:53 +0000] "GET /admin HTTP/1.1" 200 40 "-" "curl/7.68.0"curl  http://nginx-handbook.test/# this will be logged to the default file.sudo cat /var/log/nginx/access.log # 192.168.20.20 - - [25/Apr/2021:11:15:14 +0000] "GET / HTTP/1.1" 200 41 "-" "curl/7.68.0"
</code></pre>
<p>另一方面，error.log 文件保存失败日志。 要进入 error.log，必须使 NGINX crash。为此，请按如下方式更新配置：</p>
<pre><code class="language-conf">events {}http {    include /etc/nginx/mime.types;    server {        listen 80;        server_name nginx-handbook.test;        return 200 "..." "...";    }}
</code></pre>
<p>如你所知，<code>return</code> 指令只接受两个参数——但我们在这里给出了三个。现在尝试重新加载配置，将看到一条错误消息：</p>
<pre><code class="language-shell">sudo nginx -s reload# nginx: [emerg] invalid number of arguments in "return" directive in /etc/nginx/nginx.conf:14
</code></pre>
<p>检查错误日志的内容，消息也应该出现在那里：</p>
<pre><code class="language-shell">sudo cat /var/log/nginx/error.log # 2021/04/25 08:35:45 [notice] 4169#4169: signal process started# 2021/04/25 10:03:18 [emerg] 8434#8434: invalid number of arguments in "return" directive in /etc/nginx/nginx.conf:14
</code></pre>
<p>错误消息有级别。错误日志中的 <code>notice</code> 条目是代表日志记录的信息是无关紧要的，但必须立即处理 <code>emerg</code> 或 <code>crit</code> 条目。</p>
<p>有八个级别的错误消息：</p>
<ul>
<li><code>debug</code> – 有助于确定问题所在的有用调试信息。</li>
<li><code>info</code> - 不需要阅读但可能很好了解的信息性消息。</li>
<li><code>notice</code> - 发生了一些值得注意的正常现象。</li>
<li><code>warn</code> - 发生了意外，但不必担心。</li>
<li><code>error</code> - 某些事情不成功。</li>
<li><code>crit</code> - 存在急需解决的问题。</li>
<li><code>alert</code> - 需要迅速采取行动。</li>
<li><code>emerg</code> - 系统处于无法使用的状态，需要立即关注。</li>
</ul>
<p>默认情况下，NGINX 记录所有级别的消息。 可以使用 <code>error_log</code> 指令覆盖此行为。如果要将消息的最低级别设置为<code>warn</code>，请按如下方式更新配置文件：</p>
<pre><code class="language-conf">events {}http {    include /etc/nginx/mime.types;    server {        listen 80;        server_name nginx-handbook.test;	    	error_log /var/log/error.log warn;        return 200 "..." "...";    }}
</code></pre>
<p>验证并重新加载配置，从现在开始，只会记录级别为 <code>warn</code> 或更高级别的消息。</p>
<pre><code class="language-shell">cat /var/log/nginx/error.log# 2021/04/25 11:27:02 [emerg] 12769#12769: invalid number of arguments in "return" directive in /etc/nginx/nginx.conf:16
</code></pre>
<p>与之前的输出不同，这里没有“通知”条目。<code>emerg</code> 是比 <code>warn</code> 更高级别的错误，这就是它被记录的原因。</p>
<p>对于大多数项目，保留错误配置应该没问题。我唯一的建议是将最小错误级别设置为 <code>warn</code>。这样就不必查看错误日志中不必要的条目。</p>
<p>但是，如果想了解有关在 NGINX 中自定义日志记录的更多信息，此官方文档的 <a href="https://docs.nginx.com/nginx/admin-guide/monitoring/logging/">链接</a> 可能会有所帮助。</p>
<h2 id="nginx">如何使用 NGINX 作为反向代理</h2>
<p>当配置为反向代理时，NGINX 位于客户端和后端服务器之间。客户端向 NGINX 发送请求，然后 NGINX 将请求传递给后端。</p>
<p>后端服务器处理完请求后，将其发送回 NGINX。反过来，NGINX 将响应返回给客户端。</p>
<p>在整个过程中，客户端不知道谁在实际处理请求。写起来听起来很复杂，但一旦你自己动手，你就会发现 NGINX 让反向代理变的多么容易。</p>
<p>让我们看一个非常基本且不切实际的反向代理示例：</p>
<pre><code class="language-conf">events {}http {    include /etc/nginx/mime.types;    server {        listen 80;        server_name nginx.test;        location / {                proxy_pass "https://nginx.org/";        }    }}
</code></pre>
<p>除了验证和重新加载配置之外，还必须将此地址添加到的 <code>hosts</code> 文件中，以使此演示在你的系统上运行：</p>
<pre><code class="language-hosts">192.168.20.20   nginx.test
</code></pre>
<p>现在，如果访问 <a href="http://nginx.test">http://nginx.test</a>，将看到原始的 <a href="https://nginx.org">https://nginx.org</a> 站点，而 URI 保持不变。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/nginx-org-proxy.png" alt="nginx-org-proxy" width="600" height="400" loading="lazy"></p>
<p>甚至能够在一定程度上浏览网站。如果访问 <a href="http://nginx.test/en/docs/">http://nginx.test/en/docs/</a> 应该得到 <a href="http://nginx.org/en/docs/">http://nginx.org/en/docs/</a> 页面的响应。</p>
<p>如你所见，在基本层面上，<code>proxy_pass</code> 指令只是将客户端的请求传递给第三方服务器，并将响应反向代理到客户端。</p>
<h3 id="nginxnodejs">使用 NGINX 的 Node.js</h3>
<p>现在已经知道如何配置基本的反向代理服务器，可以为 NGINX 反向代理的 Node.js 应用程序提供服务。我在本文附带的代码仓库中添加了一个演示应用程序。</p>
<blockquote>
<p>我假设你有使用 Node.js 的经验并且知道如何使用 PM2 启动 Node.js 应用程序。</p>
</blockquote>
<p>如果你已经在 <code>/srv/nginx-handbook-projects</code> 中克隆了存储库，那么 <code>node-js-demo</code> 项目应该在 <code>/srv/nginx-handbook-projects/node-js-demo</code> 目录中。</p>
<p>为了运行这个 demo，需要在你的服务器上安装 Node.js。 可以按照 <a href="https://github.com/nodesource/distributions#debinstall">此处</a> 找到的说明执行此操作。</p>
<p>演示应用程序是一个简单的 HTTP 服务器，它响应 200 状态代码和 JSON 有效负载。可以通过简单地执行 <code>node app.js</code> 来启动应用程序，但更好的方法是使用 <a href="https://pm2.keymetrics.io">PM2</a>。</p>
<p>PM2 是一个守护进程管理器，广泛用于 Node.js 应用程序的生产。如果想了解更多信息，此<a href="https://pm2.keymetrics.io/docs/usage/quick-start/">链接</a> 可能会有所帮助。</p>
<p>通过执行 <code>sudo npm install -g pm2</code> 全局安装 PM2。安装完成后，在<code>/srv/nginx-handbook-projects/node-js-demo</code>目录下执行以下命令：</p>
<pre><code class="language-shell">pm2 start app.js# [PM2] Process successfully started# ┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐# │ id │ name               │ mode     │ ↺    │ status    │ cpu      │ memory   │# ├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤# │ 0  │ app                │ fork     │ 0    │ online    │ 0%       │ 21.2mb   │# └────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘
</code></pre>
<p>或者，也可以从服务器上的任何位置执行 <code>pm2 start /srv/nginx-handbook-projects/node-js-demo/app.js</code>。可以通过执行 <code>pm2 stop app</code> 命令来停止应用程序。</p>
<p>应用程序现在应该正在运行，但不应从服务器外部访问。要验证应用程序是否正在运行，请从服务器内部向 <a href="http://localhost:3000">http://localhost:3000</a> 发送 get 请求：</p>
<pre><code class="language-shell">curl -i localhost:3000# HTTP/1.1 200 OK# X-Powered-By: Express# Content-Type: application/json; charset=utf-8# Content-Length: 62# ETag: W/"3e-XRN25R5fWNH2Tc8FhtUcX+RZFFo"# Date: Sat, 24 Apr 2021 12:09:55 GMT# Connection: keep-alive# Keep-Alive: timeout=5# { "status": "success", "message": "You're reading The NGINX Handbook!" }
</code></pre>
<p>如果收到 200 响应，则服务器运行成功。现在要将 NGINX 配置为反向代理，请打开配置文件并按如下方式更新其内容：</p>
<pre><code class="language-conf">events {}  http {    listen 80;    server_name nginx-handbook.test    location / {        proxy_pass http://localhost:3000;    }}
</code></pre>
<p>这里没啥需要解释的。只是将接收到的请求传递给在端口 3000 上运行的 Node.js 应用程序。现在，如果从外部向服务器发送请求，应该得到如下响应：</p>
<pre><code class="language-shell">curl -i http://nginx-handbook.test# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Sat, 24 Apr 2021 14:58:01 GMT# Content-Type: application/json# Transfer-Encoding: chunked# Connection: keep-alive# { "status": "success", "message": "You're reading The NGINX Handbook!" }
</code></pre>
<p>尽管这适用于像这样的基本服务器，但可能需要添加更多指令才能使其在实际场景中工作，具体取决于应用程序的要求。</p>
<p>例如，如果应用程序处理 Web socket 连接，那么应该按如下方式更新配置：</p>
<pre><code class="language-conf">events {}  http {    listen 80;    server_name nginx-handbook.test    location / {        proxy_pass http://localhost:3000;        proxy_http_version 1.1;        proxy_set_header Upgrade $http_upgrade;        proxy_set_header Connection 'upgrade';    }}
</code></pre>
<p><code>proxy_http_version</code> 指令设置服务器的 HTTP 版本。 默认情况下它是 1.0，但 web socket 要求它至少是 1.1。 <code>proxy_set_header</code> 指令用于在后端服务器上设置标头。该指令的通用语法如下：</p>
<pre><code class="language-conf">proxy_set_header &lt;header name&gt; &lt;header value&gt;
</code></pre>
<p>因此，通过编写<code>proxy_set_header Upgrade $http_upgrade;</code>，是在指示 NGINX 将<code>$http_upgrade</code> 变量的值作为名为<code>Upgrade</code> 的标头传递——与<code>Connection</code> 标头相同。</p>
<p>如果你想了解更多关于 web socket 代理的信息，这个指向官方 NGINX 文档的<a href="https://nginx.org/en/docs/http/websocket.html">链接</a> 可能会有所帮助。</p>
<p>根据的应用程序所需的标头，可能需要设置更多标头。但是上面提到的配置在配置 Node.js 应用程序时非常常见。</p>
<h3 id="nginxphp">使用 NGINX 的 PHP</h3>
<p>PHP 和 NGINX 就像面包和黄油一样。毕竟 LEMP 技术栈中的 E 和 P 就代表 NGINX 和 PHP。</p>
<blockquote>
<p>这里假设你有使用 PHP 的经验并且知道如何运行 PHP 应用程序。</p>
</blockquote>
<p>我已经在本文附带的代码仓库中包含了一个演示 PHP 应用程序。如果你已经在<code>/srv/nginx-handbook-projects</code> 目录中克隆了它，那么应用程序应该在<code>/srv/nginx-handbook-projects/php-demo</code> 中。</p>
<p>为了让这个演示运行，你必须安装一个名为 PHP-FPM 的包。要安装软件包，可以执行以下命令：</p>
<pre><code class="language-shell">sudo apt install php-fpm -y
</code></pre>
<p>要测试应用程序，请通过在 /srv/nginx-handbook-projects/php-demo` 目录中执行以下命令来启动 PHP 服务：</p>
<pre><code class="language-shell">php -S localhost:8000# [Sat Apr 24 16:17:36 2021] PHP 7.4.3 Development Server (http://localhost:8000) started
</code></pre>
<p>或者，也可以从服务器上的任何位置执行 <code>php -S localhost:8000 /srv/nginx-handbook-projects/php-demo/index.php</code>。</p>
<p>该应用程序应该在端口 8000 上运行，但无法从服务器外部访问它。要进行验证，请从服务器内部向 <a href="http://localhost:8000">http://localhost:8000</a> 发送 get 请求：</p>
<pre><code class="language-shell">curl -I localhost:8000# HTTP/1.1 200 OK# Host: localhost:8000# Date: Sat, 24 Apr 2021 16:22:42 GMT# Connection: close# X-Powered-By: PHP/7.4.3# Content-type: application/json# {"status":"success","message":"You're reading The NGINX Handbook!"}
</code></pre>
<p>如果你收到 200 响应，则服务器运行成功。就像 Node.js 配置一样，现在可以简单地将请求 <code>proxy_pass</code> 发送到 localhost:8000 – 但是对于 PHP，有更好的方法。</p>
<p>PHP-FPM 中的 FPM 部分代表 FastCGI Process Module。FastCGI 是一种类似于 HTTP 的协议，用于交换二进制数据。此协议比 HTTP 稍快，并提供更好的安全性。</p>
<p>To use FastCGI instead of HTTP, update your configuration as follows:</p>
<p>要使用 FastCGI 而不是 HTTP，请按如下方式更新配置：</p>
<pre><code class="language-conf">events {}http {      include /etc/nginx/mime.types;      server {          listen 80;          server_name nginx-handbook.test;          root /srv/nginx-handbook-projects/php-demo;          index index.php;          location / {              try_files $uri $uri/ =404;          }          location ~ \.php$ {              fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;              fastcgi_param REQUEST_METHOD $request_method;              fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;      }   }}
</code></pre>
<p>让我们从新的 <code>index</code> 指令开始。如你所知，NGINX 默认会查找 index.html 文件来提供服务。但在演示项目中，它是 index.php。因此，编写 <code>index index.php</code>，指示NGINX 以 root 用户身份使用 index.php 文件。</p>
<p>该指令可以接受多个参数。对于 <code>index index.php index.html</code>，NGINX 会首先寻找 index.php。如果它没有找到这个文件，它会寻找 index.html 文件。</p>
<p>第一个 <code>location</code> 上下文中的 <code>try_files</code> 指令与在上一节中看到的相同。最后的 <code>=404</code> 表示如果没有找到任何文件则抛出错误。</p>
<p>第二个 <code>location</code> 块是魔法发生的地方。如你所见，我们已经用新的 <code>fastcgi_pass</code> 替换了 <code>proxy_pass</code> 指令。顾名思义，它用于将请求传递给 FastCGI 服务。</p>
<p>PHP-FPM 服务默认运行在主机的 9000 端口上。因此，可以直接将请求传递给 <code>http://localhost:9000</code>，而不是像我在这里所做的那样使用 Unix 套接字。但是使用 Unix 套接字更安全。</p>
<p>如果安装了多个 PHP-FPM 版本，则可以通过执行以下命令简单地列出所有套接字文件位置：</p>
<pre><code class="language-shell">sudo find / -name *fpm.sock# /run/php/php7.4-fpm.sock# /run/php/php-fpm.sock# /etc/alternatives/php-fpm.sock# /var/lib/dpkg/alternatives/php-fpm.sock
</code></pre>
<p><code>/run/php/php-fpm.sock</code> 文件是指系统上安装的最新版本的 PHP-FPM。我更喜欢使用带有版本号的那个。这样即使 PHP-FPM 得到更新，我也可以知道我正在使用的版本。</p>
<p>与通过 HTTP 传递请求不同，通过 FPM 传递请求需要我们传递一些额外的信息。</p>
<p>将额外信息传递给 FPM 服务的一般方法是使用 <code>fastcgi_param</code> 指令。至少，必须将请求方法和脚本名称传递给后端服务才能使代理工作。</p>
<p><code>fastcgi_param REQUEST_METHOD $request_method;</code> 将请求方法传递给后端，<code>fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;</code> 行传递要运行的 PHP 脚本的确切位置。</p>
<p>在这种状态下，配置应该可以工作。要测试它，请访问服务器，应该会看到如下内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/500-on-fastcgi.png" alt="500-on-fastcgi" width="600" height="400" loading="lazy"></p>
<p>嗯，这很奇怪。500 错误意味着 NGINX 由于某种原因崩溃了。这是错误日志可以派上用场的地方。让我们看看 error.log 文件中的最后一个条目：</p>
<pre><code class="language-shell">tail -n 1 /var/log/nginx/error.log# 2021/04/24 17:15:17 [crit] 17691#17691: *21 connect() to unix:/var/run/php/php7.4-fpm.sock failed (13: Permission denied) while connecting to upstream, client: 192.168.20.20, server: nginx-handbook.test, request: "GET / HTTP/1.1", upstream: "fastcgi://unix:/var/run/php/php7.4-fpm.sock:", host: "nginx-handbook.test"
</code></pre>
<p>似乎 NGINX 进程没有访问 PHP-FPM 进程的权限。</p>
<p>获得权限被拒绝错误的主要原因之一是用户不匹配。 查看拥有 NGINX 工作进程的用户。</p>
<pre><code class="language-shell">ps aux | grep nginx# root         677  0.0  0.4   8892  4260 ?        Ss   14:31   0:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;# nobody     17691  0.0  0.3   9328  3452 ?        S    17:09   0:00 nginx: worker process# vagrant    18224  0.0  0.2   8160  2552 pts/0    S+   17:19   0:00 grep --color=auto nginx
</code></pre>
<p>如你所见，该进程当前归 <code>nobody</code> 所有。 现在检查 PHP-FPM 进程。</p>
<pre><code class="language-shell"># ps aux | grep php# root       14354  0.0  1.8 195484 18924 ?        Ss   16:11   0:00 php-fpm: master process (/etc/php/7.4/fpm/php-fpm.conf)# www-data   14355  0.0  0.6 195872  6612 ?        S    16:11   0:00 php-fpm: pool www# www-data   14356  0.0  0.6 195872  6612 ?        S    16:11   0:00 php-fpm: pool www# vagrant    18296  0.0  0.0   8160   664 pts/0    S+   17:20   0:00 grep --color=auto php
</code></pre>
<p>另一方面，此进程由 <code>www-data</code> 用户拥有。这就是 NGINX 被拒绝访问此进程的原因。</p>
<p>要解决此问题，请按如下方式更新配置：</p>
<pre><code class="language-conf">user www-data;events {}http {      include /etc/nginx/mime.types;      server {          listen 80;          server_name nginx-handbook.test;          root /srv/nginx-handbook-projects/php-demo;          index index.php;          location / {              try_files $uri $uri/ =404;          }          location ~ \.php$ {              fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;              fastcgi_param REQUEST_METHOD $request_method;              fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;      }   }}
</code></pre>
<p><code>user</code> 指令负责设置 NGINX 工作进程的所有者。现在再次检查 NGINX 进程：</p>
<pre><code class="language-shell"># ps aux | grep nginx# root         677  0.0  0.4   8892  4264 ?        Ss   14:31   0:00 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;# www-data   20892  0.0  0.3   9292  3504 ?        S    18:10   0:00 nginx: worker process# vagrant    21294  0.0  0.2   8160  2568 pts/0    S+   18:18   0:00 grep --color=auto nginx
</code></pre>
<p>毫无疑问，该进程现在归 <code>www-data</code> 用户所有。向服务器发送请求以检查它是否正常工作：</p>
<pre><code class="language-shell"># curl -i http://nginx-handbook.test# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Sat, 24 Apr 2021 18:22:24 GMT# Content-Type: application/json# Transfer-Encoding: chunked# Connection: keep-alive# {"status":"success","message":"You're reading The NGINX Handbook!"}
</code></pre>
<p>如果收到带有 JSON 有效负载的 200 状态代码，那么就可以开始了。</p>
<p>这种简单的配置适用于演示应用程序，但在实际项目中，必须传递一些额外的参数。</p>
<p>出于这个原因，NGINX 包含一个名为 <code>fastcgi_params</code> 的部分配置。该文件包含最常见的 FastCGI 参数列表。</p>
<pre><code class="language-shell">cat /etc/nginx/fastcgi_params# fastcgi_param  QUERY_STRING       $query_string;# fastcgi_param  REQUEST_METHOD     $request_method;# fastcgi_param  CONTENT_TYPE       $content_type;# fastcgi_param  CONTENT_LENGTH     $content_length;# fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;# fastcgi_param  REQUEST_URI        $request_uri;# fastcgi_param  DOCUMENT_URI       $document_uri;# fastcgi_param  DOCUMENT_ROOT      $document_root;# fastcgi_param  SERVER_PROTOCOL    $server_protocol;# fastcgi_param  REQUEST_SCHEME     $scheme;# fastcgi_param  HTTPS              $https if_not_empty;# fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;# fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;# fastcgi_param  REMOTE_ADDR        $remote_addr;# fastcgi_param  REMOTE_PORT        $remote_port;# fastcgi_param  SERVER_ADDR        $server_addr;# fastcgi_param  SERVER_PORT        $server_port;# fastcgi_param  SERVER_NAME        $server_name;# PHP only, required if PHP was built with --enable-force-cgi-redirect# fastcgi_param  REDIRECT_STATUS    200;
</code></pre>
<p>如你所见，此文件还包含 <code>REQUEST_METHOD</code> 参数。可以在配置中包含此文件，而不是手动传递它：</p>
<pre><code class="language-conf">user www-data;events {}http {      include /etc/nginx/mime.types;      server {          listen 80;          server_name nginx-handbook.test;          root /srv/nginx-handbook-projects/php-demo;          index index.php;          location / {              try_files $uri $uri/ =404;          }          location ~ \.php$ {              fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;              fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;              include /etc/nginx/fastcgi_params;      }   }}
</code></pre>
<p>服务应该表现得一样。除了 <code>fastcgi_params</code> 文件，可能还会遇到包含一组稍微不同的参数的 <code>fastcgi.conf</code> 文件。我建议你避免这种情况发生，因为它与它的行为有些不一致。</p>
<h2 id="nginx">如何使用 NGINX 作为负载均衡器</h2>
<p>由于 NGINX 的反向代理设计，可以轻松地将其配置为负载均衡器。</p>
<p>我已经在本文附带的代码仓库库中添加了一个演示。如果你已经在 <code>/srv/nginx-handbook-projects/</code> 目录中克隆了代码仓库，那么演示应该在 <code>/srv/nginx-handbook-projects/load-balancer-demo/</code> 目录中。</p>
<p>在现实生活中，分布在多个服务器上的大型项目可能需要负载均衡。但是对于这个简单的演示，我创建了三个非常简单的 Node.js 服务，响应服务编号和 200 状态代码。</p>
<p>为了让这个演示工作，你需要在服务器上安装 Node.js。可以在此<a href="https://github.com/nodesource/distributions#debinstall">链接</a> 中找到说明以帮助你安装它。</p>
<p>除此之外，还需要 <a href="https://pm2.keymetrics.io/">PM2</a> 来守护本演示中提供的 Node.js 服务。</p>
<p>如果还没有安装，请通过执行 <code>sudo npm install -g pm2</code> 来安装 PM2。安装完成后，执行以下命令启动三个 Node.js 服务：</p>
<pre><code class="language-shell">pm2 start /srv/nginx-handbook-projects/load-balancer-demo/server-1.jspm2 start /srv/nginx-handbook-projects/load-balancer-demo/server-2.jspm2 start /srv/nginx-handbook-projects/load-balancer-demo/server-3.jspm2 list# ┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐# │ id │ name               │ mode     │ ↺    │ status    │ cpu      │ memory   │# ├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤# │ 0  │ server-1           │ fork     │ 0    │ online    │ 0%       │ 37.4mb   │# │ 1  │ server-2           │ fork     │ 0    │ online    │ 0%       │ 37.2mb   │# │ 2  │ server-3           │ fork     │ 0    │ online    │ 0%       │ 37.1mb   │# └────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘
</code></pre>
<p>三个 Node.js 服务应该分别运行在 localhost:3001、localhost:3002、localhost:3003 上。</p>
<p>现在更新配置如下：</p>
<pre><code class="language-conf">events {}http {    upstream backend_servers {        server localhost:3001;        server localhost:3002;        server localhost:3003;    }    server {        listen 80;        server_name nginx-handbook.test;        location / {            proxy_pass http://backend_servers;        }    }}
</code></pre>
<p><code>server</code> 上下文中的配置与已经看到的相同。但是，<code>upstream</code>上下文是新的。NGINX 中的 upstream 是一组可以被视为单个后端的服务器。</p>
<p>所以你开始使用 PM2 启动的三台服务器可以放在一个 upstream，可以让 NGINX 均衡它们之间的负载。</p>
<p>要测试配置，必须向服务器发送大量请求。可以使用 bash 中的 <code>while</code> 循环自动执行该过程：</p>
<pre><code class="language-shell">while sleep 0.5; do curl http://nginx-handbook.test; done# response from server - 2.# response from server - 3.# response from server - 1.# response from server - 2.# response from server - 3.# response from server - 1.# response from server - 2.# response from server - 3.# response from server - 1.# response from server - 2.
</code></pre>
<p>可以通过按键盘上的 <code>Ctrl + C</code> 来取消循环。从服务器的响应中可以看出，NGINX 正在自动对服务器进行负载均衡。</p>
<p>当然，更大的项目规模，负载均衡可能比这复杂得多。但本文的目的是让你入门，相信你现在对 NGINX 负载均衡有了基本的了解。 你可以通过执行 <code>pm2 stop server-1 server-2 server-3</code> 命令来停止三个正在运行的服务器（建议如此）。</p>
<h2 id="nginx">如何优化 NGINX 以获得最大性能</h2>
<p>在本文的这一部分中，将了解使服务器获得最大性能的多种方法。</p>
<p>其中一些方法是特定于应用程序的，这意味着它们可能需要根据你的应用程序要求进行调整。但其中一些是普适的优化手段。</p>
<p>就像前几节一样，在这一节中配置更改会很频繁，所以不要忘记每次验证和重新加载配置文件。</p>
<h3 id="">如何配置工作进程和工作连接</h3>
<p>正如我在上一节中已经提到的，NGINX 可以产生多个工作进程，每个进程能够处理数千个请求。</p>
<pre><code class="language-shell">sudo systemctl status nginx# ● nginx.service - A high performance web server and a reverse proxy server#      Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)#      Active: active (running) since Sun 2021-04-25 08:33:11 UTC; 5h 45min ago#        Docs: man:nginx(8)#    Main PID: 3904 (nginx)#       Tasks: 2 (limit: 1136)#      Memory: 3.2M#      CGroup: /system.slice/nginx.service#              ├─ 3904 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;#              └─16443 nginx: worker process
</code></pre>
<p>如你所见，现在系统上只有一个 NGINX 工作进程。但是，可以通过对配置文件更改此数字。</p>
<pre><code class="language-conf">worker_processes 2;events {}http {    server {        listen 80;        server_name nginx-handbook.test;        return 200 "worker processes and worker connections configuration!\n";    }}
</code></pre>
<p>在 <code>main</code> 上下文中编写的 <code>worker_process</code> 指令负责设置要生成的工作进程的数量。现在再次检查 NGINX 服务，你应该会看到两个工作进程：</p>
<pre><code class="language-shell">sudo systemctl status nginx# ● nginx.service - A high performance web server and a reverse proxy server#      Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)#      Active: active (running) since Sun 2021-04-25 08:33:11 UTC; 5h 54min ago#        Docs: man:nginx(8)#     Process: 22610 ExecReload=/usr/sbin/nginx -g daemon on; master_process on; -s reload (code=exited, status=0/SUCCESS)#    Main PID: 3904 (nginx)#       Tasks: 3 (limit: 1136)#      Memory: 3.7M#      CGroup: /system.slice/nginx.service#              ├─ 3904 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;#              ├─22611 nginx: worker process#              └─22612 nginx: worker process
</code></pre>
<p>设置工作进程的数量很容易，但确定工作进程的最佳数量需要更多的工作。</p>
<p>工作进程本质上是异步的。这意味着它们将尽可能快地处理传入的请求。</p>
<p>现在假设你的服务器在单核处理器上运行。如果将工作进程数设置为 1，则该单个进程将使用 100% 的 CPU 容量。 但是如果将其设置为 2，则两个进程将能够分别使用 50% 的 CPU。所以增加工作进程的数量并不意味着更好的性能。</p>
<p>确定最佳工作进程数的经验法则是 <strong>工作进程数 = CPU 内核数</strong>。</p>
<p>如果你在具有双核 CPU 的服务器上运行，则应将工作进程数设置为 2。在四核中，应将其设置为 4...明白了吧。</p>
<p>在 Linux 上可以用如下命令确定服务器上的 CPU 数量。</p>
<pre><code class="language-shell">nproc# 1
</code></pre>
<p>我在单 CPU 虚拟机上运行，所以 <code>nproc</code> 检测到有一个 CPU。既然已经知道了 CPU 的数量，剩下要做的就是在配置中设置数量。</p>
<p>这一切都很好，但是每次升级服务器并且 CPU 数量发生变化时，都必须手动更新服务器配置。</p>
<p>NGINX 提供了一种更好的方法来处理这个问题。可以简单地将工作进程的数量设置为 <code>auto</code>，NGINX 将根据 CPU 的数量自动设置进程的数量。</p>
<pre><code class="language-conf">worker_processes auto;events {}http {    server {        listen 80;        server_name nginx-handbook.test;        return 200 "worker processes and worker connections configuration!\n";    }}
</code></pre>
<p>再次检查 NGINX 进程：</p>
<pre><code class="language-shell">sudo systemctl status nginx# ● nginx.service - A high performance web server and a reverse proxy server#      Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)#      Active: active (running) since Sun 2021-04-25 08:33:11 UTC; 6h ago#        Docs: man:nginx(8)#     Process: 22610 ExecReload=/usr/sbin/nginx -g daemon on; master_process on; -s reload (code=exited, status=0/SUCCESS)#    Main PID: 3904 (nginx)#       Tasks: 2 (limit: 1136)#      Memory: 3.2M#      CGroup: /system.slice/nginx.service#              ├─ 3904 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;#              └─23659 nginx: worker process
</code></pre>
<p>工作进程的数量又恢复为 1，因为这是该服务器的最佳选择。</p>
<p>除了工作进程之外，还有工作连接，表示单个工作进程可以处理的最大连接数。</p>
<p>就像工作进程的数量一样，这个数字也与 CPU 核心数量以及操作系统每个核心允许打开的文件数量有关。</p>
<p>在 Linux 上用这个命令查看这个数字：</p>
<pre><code class="language-shell">ulimit -n# 1024
</code></pre>
<p>现在有了这个数字，接下来在配置中设置它：</p>
<pre><code class="language-conf">worker_processes auto;

events {
    worker_connections 1024;
}

http {

    server {

        listen 80;
        server_name nginx-handbook.test;

        return 200 "worker processes and worker connections configuration!\n";
    }
}
</code></pre>
<p><code>worker_connections</code> 指令负责设置配置中的工作连接数。这也是第一次使用 <code>events</code> 上下文。</p>
<p>在上一节中，我提到此上下文用于设置 NGINX 在一般级别上使用的值。工作连接配置就是这样的一个例子。</p>
<h3 id="">如何缓存静态内容</h3>
<p>优化服务器的第二种技术是缓存静态内容。无论使用哪种应用程序，总会提供一定数量的静态内容，例如样式表、图像等。</p>
<p>考虑到这些内容不太可能经常更改，最好将它们缓存一段时间。NGINX 实现起来很简单。</p>
<pre><code class="language-conf">worker_processes auto;

events {
    worker_connections 1024;
}

http {

    include /env/nginx/mime.types;

    server {

        listen 80;
        server_name nginx-handbook.test;

        root /srv/nginx-handbook-demo/static-demo;
        
        location ~* \.(css|js|jpg)$ {
            access_log off;
            
            add_header Cache-Control public;
            add_header Pragma public;
            add_header Vary Accept-Encoding;
            expires 1M;
        }
    }
}
</code></pre>
<p>通过编写<code>location ~* .(css|js|jpg)$</code>，指示 NGINX 匹配请求以 <code>.css</code>、<code>.js</code> 和 <code>.jpg</code> 结尾的文件。</p>
<p>在我的应用程序中，即使用户提交不同的格式，我通常也以 <a href="https://developers.google.com/speed/webp">WebP</a> 格式存储图像。这样，配置静态缓存对我来说变得更加容易。</p>
<p>可以使用 <code>add_header</code> 指令在对客户端的响应中包含一个标头。之前你已经看到了 <code>proxy_set_header</code> 指令，用于在对后端服务器的持续请求中设置标头。另一方面，<code>add_header</code> 指令仅将给定的标头添加到响应中。</p>
<p>通过将 <code>Cache-Control</code> 标头设置为 public，告诉客户端这个内容可以以任何方式缓存。<code>Pragma</code> 标头只是一个旧版本的 <code>Cache-Control</code> 标头并且或多或少地做了相同的事情。</p>
<p>下一个标头 <code>Vary</code> 负责让客户端知道这个缓存的内容可能会有所不同。</p>
<p><code>Accept-Encoding</code> 的值意味着内容可能会根据客户端接受的内容编码而有所不同。这将在下一节中进一步阐明。</p>
<p>最后，<code>expires</code> 指令可以方便地设置 <code>Expires</code> 标头 <code>expires</code> 指令占用此缓存有效的持续时间。通过将其设置为“1M”，告诉 NGINX 将内容缓存一个月。 还可以将其设置为 <code>10m</code> 10 minutes、<code>24h</code> 24 hours，等等。</p>
<p>现在要测试配置，从服务器发送对 nginx-handbook.jpg 文件的请求：</p>
<pre><code class="language-shell">curl -I http://nginx-handbook.test/the-nginx-handbook.jpg

# HTTP/1.1 200 OK
# Server: nginx/1.18.0 (Ubuntu)
# Date: Sun, 25 Apr 2021 15:58:22 GMT
# Content-Type: image/jpeg
# Content-Length: 19209
# Last-Modified: Sun, 25 Apr 2021 08:35:33 GMT
# Connection: keep-alive
# ETag: "608529d5-4b09"
# Expires: Tue, 25 May 2021 15:58:22 GMT
# Cache-Control: max-age=2592000
# Cache-Control: public
# Pragma: public
# Vary: Accept-Encoding
# Accept-Ranges: bytes
</code></pre>
<p>如你所见，标头已添加到响应中，任何主流浏览器都应该能够解释它们。</p>
<h3 id="">如何压缩响应</h3>
<p>要展示的最后一种优化技术非常简单：压缩响应以减小其大小。</p>
<pre><code class="language-conf">worker_processes auto;

events {
    worker_connections 1024;
}

http {
    include /env/nginx/mime.types;

    gzip on;
    gzip_comp_level 3;

    gzip_types text/css text/javascript;

    server {

        listen 80;
        server_name nginx-handbook.test;

        root /srv/nginx-handbook-demo/static-demo;
        
        location ~* \.(css|js|jpg)$ {
            access_log off;
            
            add_header Cache-Control public;
            add_header Pragma public;
            add_header Vary Accept-Encoding;
            expires 1M;
        }
    }
}
</code></pre>
<p><a href="https://www.gnu.org/software/gzip/">GZIP</a> 是一种流行的文件格式，被应用程序用于文件压缩和解压缩。NGINX 可以使用 <code>gzip</code> 指令利用这种格式压缩响应。</p>
<p>通过在 <code>http</code> 上下文中编写 <code>gzip on</code>，可以指示 NGINX 压缩响应。 <code>gzip_comp_level</code> 指令设置压缩级别。可以将其设置为非常高的数字，但这并不能保证更好的压缩。设置 1 - 4 之间的数字可提供有效的结果。例如，我喜欢将其设置为 3。</p>
<p>默认情况下，NGINX 压缩 HTML 响应。 要压缩其他文件格式，必须将它们作为参数传递给 <code>gzip_types</code> 指令。 通过编写<code>gzip_types text/css text/javascript;</code>，告诉 NGINX 使用 text/css 和 text/javascript 的 mime 类型压缩任何文件。</p>
<p>在 NGINX 中配置压缩是不够的。客户端必须请求压缩响应而不是未压缩响应。我希望你记得上一节关于缓存的 <code>add_header Vary Accept-Encoding;</code> 行。 此标头让客户端知道响应可能会根据客户端接受的内容而有所不同。</p>
<p>例如，如果想从服务器请求未压缩版本的 mini.min.css 文件，可以执行以下操作：</p>
<pre><code class="language-shell">curl -I http://nginx-handbook.test/mini.min.css# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Sun, 25 Apr 2021 16:30:32 GMT# Content-Type: text/css# Content-Length: 46887# Last-Modified: Sun, 25 Apr 2021 08:35:33 GMT# Connection: keep-alive# ETag: "608529d5-b727"# Expires: Tue, 25 May 2021 16:30:32 GMT# Cache-Control: max-age=2592000# Cache-Control: public# Pragma: public# Vary: Accept-Encoding# Accept-Ranges: bytes
</code></pre>
<p>如你所见，没有开启压缩的。现在，如果你想请求文件的压缩版本，则必须发送额外的标头。</p>
<pre><code class="language-shell">curl -I -H "Accept-Encoding: gzip" http://nginx-handbook.test/mini.min.css# HTTP/1.1 200 OK# Server: nginx/1.18.0 (Ubuntu)# Date: Sun, 25 Apr 2021 16:31:38 GMT# Content-Type: text/css# Last-Modified: Sun, 25 Apr 2021 08:35:33 GMT# Connection: keep-alive# ETag: W/"608529d5-b727"# Expires: Tue, 25 May 2021 16:31:38 GMT# Cache-Control: max-age=2592000# Cache-Control: public# Pragma: public# Vary: Accept-Encoding# Content-Encoding: gzip
</code></pre>
<p>正如你在响应头中看到的，<code>Content-Encoding</code> 现在被设置为 <code>gzip</code>，这意味着这是文件的压缩版本。</p>
<p>现在，如果要比较文件大小的差异，可以执行以下操作：</p>
<pre><code class="language-shell">cd ~mkdir compression-test &amp;&amp; cd compression-testcurl http://nginx-handbook.test/mini.min.css &gt; uncompressed.csscurl -H "Accept-Encoding: gzip" http://nginx-handbook.test/mini.min.css &gt; compressed.cssls -lh# -rw-rw-r-- 1 vagrant vagrant 9.1K Apr 25 16:35 compressed.css# -rw-rw-r-- 1 vagrant vagrant  46K Apr 25 16:35 uncompressed.css
</code></pre>
<p>该文件的未压缩版本为“46K”，压缩版本为“9.1K”，几乎小六倍。线上的站点样式表会的更大，压缩可以使响应文件更小、响应速度更快。</p>
<h2 id="">如何理解主配置文件</h2>
<p>我希望你记得你在前面部分重命名的原始 <code>nginx.conf</code> 文件。根据 <a href="https://wiki.debian.org/Nginx/DirectoryStructure">Debian wiki</a>，这个文件应该由 NGINX 维护者而不是服务器管理员来更改，除非他们确切地知道他们在做什么。</p>
<p>是在整篇文章中，我已经教你在这个文件中配置服务器。在本节中，我将介绍如何在不更改 <code>nginx.conf</code> 文件的情况下配置服务器。</p>
<p>首先，首先删除或重命名修改后的 <code>nginx.conf</code> 文件并恢复原来的文件：</p>
<pre><code class="language-shell">sudo rm /etc/nginx/nginx.confsudo mv /etc/nginx/nginx.conf.backup /etc/nginx/nginx.confsudo nginx -s reload
</code></pre>
<p>现在 NGINX 应该回到它的原始状态。让我们通过执行 <code>sudo cat /etc/nginx/nginx.conf</code> 文件再次查看该文件的内容：</p>
<pre><code class="language-conf">user www-data;worker_processes auto;pid /run/nginx.pid;include /etc/nginx/modules-enabled/*.conf;events {	worker_connections 768;	# multi_accept on;}http {	##	# Basic Settings	##	sendfile on;	tcp_nopush on;	tcp_nodelay on;	keepalive_timeout 65;	types_hash_max_size 2048;	# server_tokens off;	# server_names_hash_bucket_size 64;	# server_name_in_redirect off;	include /etc/nginx/mime.types;	default_type application/octet-stream;	##	# SSL Settings	##	ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE	ssl_prefer_server_ciphers on;	##	# Logging Settings	##	access_log /var/log/nginx/access.log;	error_log /var/log/nginx/error.log;	##	# Gzip Settings	##	gzip on;	# gzip_vary on;	# gzip_proxied any;	# gzip_comp_level 6;	# gzip_buffers 16 8k;	# gzip_http_version 1.1;	# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;	##	# Virtual Host Configs	##	include /etc/nginx/conf.d/*.conf;	include /etc/nginx/sites-enabled/*;}#mail {#	# See sample authentication script at:#	# http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript# #	# auth_http localhost/auth.php;#	# pop3_capabilities "TOP" "USER";#	# imap_capabilities "IMAP4rev1" "UIDPLUS";# #	server {#		listen     localhost:110;#		protocol   pop3;#		proxy      on;#	}# #	server {#		listen     localhost:143;#		protocol   imap;#		proxy      on;#	}#}
</code></pre>
<p>现在应该能够看懂此文件。在主上下文 <code>user www-data;</code> 中，<code>worker_processes auto;</code> 行已经介绍过，不在赘述。</p>
<p><code>pid /run/nginx.pid;</code> 行设置 NGINX 进程的进程 ID，<code>include /etc/nginx/modules-enabled/*.conf;</code> 引入 <code>/etc/nginx/ modules-enabled/</code> 目录的配置文件。</p>
<p>该目录用于存放 NGINX 动态模块。我在本文中没有涉及动态模块，所以我将跳过它。</p>
<p>现在在 <code>http</code> 上下文中，在基本设置下，可以看到一些常用的优化技术。以下是这些技术的作用：</p>
<ul>
<li><code>sendfile on;</code> 禁用静态文件的缓冲。</li>
<li><code>tcp_nopush on;</code> 允许在一个数据包中发送响应头。</li>
<li><code>tcp_nodelay on;</code> 禁用 <a href="https://en.wikipedia.org/wiki/Nagle's_algorithm">Nagle's Algorithm</a> 导致更快的静态文件传输。</li>
</ul>
<p><code>keepalive_timeout</code> 指令指示保持连接打开的时间，<code>types_hash_maxsize</code> 指令设置类型哈希映射的大小。 默认情况下，它还包括 <code>mime.types</code> 文件。</p>
<p>我将跳过 SSL 设置，因为我不打算在本文介绍它们。我们已经讨论了日志记录和 gzip 设置。 可能会看到一些关于 gzip 的指令的注释内容。只要你了解自己在做什么，就可以自定义这些设置。</p>
<p>可以使用 <code>mail</code> 上下文将 NGINX 配置为邮件服务器。到目前为止，我们只讨论了 NGINX 作为 Web 服务器，所以我也将跳过这一点。</p>
<p>现在在虚拟主机设置下，应该看到如下两行：</p>
<pre><code class="language-conf">### Virtual Host Configs##include /etc/nginx/conf.d/*.conf;include /etc/nginx/sites-enabled/*;
</code></pre>
<p>这两行指示 NGINX 引入在 <code>/etc/nginx/conf.d/</code> 和 <code>/etc/nginx/sites-enabled/</code> 目录中找到的所有配置文件。</p>
<p>看到这两行后，人们往往会把这两个目录作为放置配置文件的理想位置，但这是不对的。</p>
<p>还有另一个目录 <code>/etc/nginx/sites-available/</code> 用于存储虚拟主机的配置文件。<code>/etc/nginx/sites-enabled/</code> 目录用于存储指向 <code>/etc/nginx/sites-available/</code> 目录中文件的符号链接。</p>
<p>实际上有一个示例配置：</p>
<pre><code class="language-shell">ln -lh /etc/nginx/sites-enabled/# lrwxrwxrwx 1 root root 34 Apr 25 08:33 default -&gt; /etc/nginx/sites-available/default
</code></pre>
<p>如你所见，该目录包含指向 <code>/etc/nginx/sites-available/default</code> 文件的符号链接。</p>
<p>思路是在 <code>/etc/nginx/sites-available/</code> 目录中写入多个虚拟主机，并通过给他们创建到 <code>/etc/nginx/sites-enabled/</code> 目录的符号链接来激活它。</p>
<p>为了演示这个概念，让我们配置一个简单的静态服务器。首先，删除默认的虚拟主机符号链接，在进程中停用这个配置：</p>
<pre><code class="language-shell">sudo rm /etc/nginx/sites-enabled/defaultls -lh /etc/nginx/sites-enabled/# lrwxrwxrwx 1 root root 41 Apr 25 18:01 nginx-handbook -&gt; /etc/nginx/sites-available/nginx-handbook
</code></pre>
<p>通过执行 <code>sudo touch /etc/nginx/sites-available/nginx-handbook</code> 创建一个新文件，并将输入以下内容：</p>
<pre><code>server {    listen 80;    server_name nginx-handbook.test;    root /srv/nginx-handbook-projects/static-demo;}
</code></pre>
<p><code>/etc/nginx/sites-available/</code> 目录中的文件包含在 main <code>http</code> 上下文中，因此它们应该只包含 <code>server</code> 块。</p>
<p>现在通过执行以下命令在 <code>/etc/nginx/sites-enabled/</code> 目录中创建一个指向此文件的符号链接：</p>
<pre><code class="language-shell">sudo ln -s /etc/nginx/sites-available/nginx-handbook /etc/nginx/sites-enabled/nginx-handbookls -lh /etc/nginx/sites-enabled/# lrwxrwxrwx 1 root root 34 Apr 25 08:33 default -&gt; /etc/nginx/sites-available/default# lrwxrwxrwx 1 root root 41 Apr 25 18:01 nginx-handbook -&gt; /etc/nginx/sites-available/nginx-handbook
</code></pre>
<p>在验证和重新加载配置文件之前，必须重新打开日志文件。否则，可能会收到权限拒绝错误。发生这种情况的原因是因为这次交换旧的 <code>nginx.conf</code> 文件导致进程 ID 不同。</p>
<pre><code class="language-shell">sudo rm /var/log/nginx/*.logsudo touch /var/log/nginx/access.log /var/log/nginx/error.logsudo nginx -s reopen
</code></pre>
<p>最后，验证并重新加载配置文件：</p>
<pre><code>sudo nginx -t# nginx: the configuration file /etc/nginx/nginx.conf syntax is ok# nginx: configuration file /etc/nginx/nginx.conf test is successfulsudo nginx -s reload
</code></pre>
<p>访问服务器，应该会看到最初的 The NGINX 手册页面：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/04/image-100.png" alt="image-100" width="600" height="400" loading="lazy"></p>
<p>如果已经正确配置了服务器并且仍然看到旧的 NGINX 欢迎页面，请执行硬刷新。浏览器通常会缓存旧的静态资源，需要做进行一些清理。</p>
<h2 id="nginx">高级 NGINX 概念系列</h2>
<p>服务器配置是一个很大的话题，本文的目的是让你了解 NGINX 的基础知识。还有一些重要和高级的主题没讲。</p>
<p>我计划在我的博客上写一些文章来解释诸如配置 HTTP2 协议、FastCGI 微缓存、速率限制、SSL 证书签名、动态模块等主题。</p>
<p>这样，该系列将成为易于参考且面向对基础有适当了解的人的文章集合。</p>
<p>所以请密切关注 <a href="https://farhan.info/">https://farhan.info/</a> 。我希望在 2 或 3 周内发布第一篇文章。</p>
<h2 id="">表达你的支持</h2>
<p>除了这本手册之外，我还编写了一些复杂主题的手册，例如 <a href="https://chinese.freecodecamp.org/news/the-docker-handbook/">Docker 容器化</a> 和 <a href="https://chinese.freecodecamp.org/news/the-kubernetes-handbook/">Kubernetes 的服务器编排</a> 都可以在 <a href="https://chinese.freecodecamp.org/news/author/farhanhasin/">freeCodeCamp News</a> 上免费获得。</p>
<p>这些手册是我以简化晦涩技术为使命的成果。每本手册都需要花费大量的时间和精力来编写。</p>
<p>如果你喜欢我的写作并想让我保持动力，可以考虑在 <a href="https://github.com/fhsinchy/">GitHub</a> 上 star，并在 [LinkedIn](<a href="https://www">https://www</a>. linkedin.com/in/farhanhasin/) 认可我的相关技能。</p>
<p>我也愿意接受建议和讨论。在 <a href="https://twitter.com/frhnhsin">Twitter</a> 上关注我，并通过社交软件或 <a href="mailto:mail@farhan.info">电子邮件</a> 联系我。</p>
<p>最后，考虑与他人分享资源，因为</p>
<blockquote>
<p>分享知识是友谊最基本的行为。因为这是一种你可以给予一些东西而不会失去一些东西的方式。 — 理查德·斯托曼</p>
</blockquote>
<h2 id="">尾声</h2>
<p>我衷心感谢你花时间阅读本文。我希望你享受你的学习时间并学习了 NGINX 的所有基本知识。</p>
<p>如果你喜欢我的作品，你可以在 <a href="https://chinese.freecodecamp.org/news/author/farhanhasin/">https://www.freecodecamp.org/news/author/farhanhasin/</a> 上找到我的其他书籍，个人博客 <a href="https://www.farhan.info/blogs/"> https://www.farhan.info/</a> 同步更新。</p>
<p>你可以在 Twitter 上关注我 <a href="https://twitter.com/frhnhsin">@frhnhsin</a> 或在 LinkedIn 上与我联系 <a href="https://www.linkedin.com/in/farhanhasin/">/in/farhanhasin</a> 。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/the-nginx-handbook/">The NGINX Handbook</a>，作者：<a href="https://www.freecodecamp.org/news/author/farhanhasin/">Farhan Hasin Chowdhury</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 一文了解 web 开发中的 cookie ]]>
                </title>
                <description>
                    <![CDATA[ 你是否曾经想过为什么登录站点后关闭浏览器也可以保留登录状态，以及在登录前就可以将商品添加到购物车中？ 无论你是否了解，cookie 随处可见，它们彻底改变了我们使用网络的方式。 在本文中，我们将介绍 cookie 的历史、它们的工作方式、如何在 JavaScript 中使用它们以及需要熟知的一些安全隐患。 cookies 简史 HTTP 超文本传输协议是一种无状态协议。根据 Wikipedia，它之所以无状态，是因为它“不需要 HTTP 服务器在不同请求期间保留用户的信息或状态”。 今天，网站服务依然是如此实现的 – 输入浏览器的URL，浏览器向某处的服务器发出请求，然后服务器返回文件以呈现页面，然后关闭连接。 现在，假设需要登录网站以查看某些内容，例如使用 LinkedIn。该过程与上述过程基本相同，会看到一个表单，用于输入电子邮件地址和密码。 输入相关信息，然后浏览器将其发送到服务器。服务器检查登录信息，如果一切顺利，它将把呈现页面所需的数据发送回浏览器。 但是，如果 LinkedIn 是无状态的，一旦跳转到另一个页面，服务器将不记得你刚刚已经登录过。它将要求你再次输 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/everything-you-need-to-know-about-cookies-for-web-development/</link>
                <guid isPermaLink="false">60aca8af76aad305289cae00</guid>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Mon, 24 May 2021 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/05/602cb40c0a2838549dcc6af3.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>你是否曾经想过为什么登录站点后关闭浏览器也可以保留登录状态，以及在登录前就可以将商品添加到购物车中？</p>
<p>无论你是否了解，cookie 随处可见，它们彻底改变了我们使用网络的方式。</p>
<p>在本文中，我们将介绍 cookie 的历史、它们的工作方式、如何在 JavaScript 中使用它们以及需要熟知的一些安全隐患。</p>
<h2 id="cookies">cookies 简史</h2>
<p>HTTP 超文本传输协议是一种无状态协议。根据 Wikipedia，它之所以无状态，是因为它“不需要 HTTP 服务器在不同请求期间保留用户的信息或状态”。</p>
<p>今天，网站服务依然是如此实现的 – 输入浏览器的URL，浏览器向某处的服务器发出请求，然后服务器返回文件以呈现页面，然后关闭连接。</p>
<p>现在，假设需要登录网站以查看某些内容，例如使用 LinkedIn。该过程与上述过程基本相同，会看到一个表单，用于输入电子邮件地址和密码。</p>
<p>输入相关信息，然后浏览器将其发送到服务器。服务器检查登录信息，如果一切顺利，它将把呈现页面所需的数据发送回浏览器。</p>
<p>但是，如果 LinkedIn 是无状态的，一旦跳转到另一个页面，服务器将不记得你刚刚已经登录过。它将要求你再次输入电子邮件地址和密码，进行检查后在发送数据以渲染新页面。</p>
<p>那会非常令人沮丧，不是吗？许多开发人员也这样认为，并找到了很多在 Web 上创建有状态会话的方法。</p>
<h3 id="cookie">cookie 的发明</h3>
<p>90 年代初期，Netscape 的开发人员 Lou Montoulli 遇到了一个问题 – 他正在为另一家公司 MCI 开发一个在线商店，该商店会在服务器中存储每个客户购物车中的商品。这意味着人们必须首先创建一个帐户，创建账户需要花费一些时间，存储每个用户购物车的内容也会占用大量存储空间。</p>
<p>MCI 要求将这些数据存储在客户自己的计算机上。他们还希望客户在没有登录时也能添加商品到购物车。</p>
<p>为了解决这个问题，Lou 提出了一个在程序员中已经广为人知的想法：魔力 cookie。</p>
<p>魔力 cookie 或 cookie，是可以在两个计算机程序之间传递的少量数据。它们之所以“魔幻”，是因为 cookie 中的数据通常是随机密钥或令牌，并且实际只对使用它的软件才有意义。</p>
<p>Lou 采用了魔力 cookie 概念，并将其应用于在线商店，后来又应用于浏览器。</p>
<p>现在了解了 cookie 的历史，来看一下如何使用 cookie 在网络上创建有状态会话。</p>
<h2 id="cookies">cookies 如何工作</h2>
<p>和 cookie 有点像的一个场景，就是你在游乐园中获得的腕带。</p>
<p>当登录网站时，过程就像进入游乐园一样。首先，要买票，然后进入公园时，工作人员会检票并给你一条腕带。</p>
<p>这和登录的方式一样 - 服务器检查你的用户名和密码，创建并存储会话，生成唯一的会话 ID，然后发送回带有该会话 ID 的 cookie。</p>
<p>（请注意，会话 ID 不是你的密码，它是完全独立的，即时生成的。密码处理和身份验证不在本文的讨论范围之内，可以在<a href="https://www.freecodecamp.org/news/search/?query=authentication">这里</a>了解更多。）</p>
<p>在游乐园中，可以通过出示腕带来消费项目。</p>
<p>同样，当你向登录的网站发出请求时，浏览器会将带有会话 ID 的 cookie 发送回服务器。服务器使用你的会话 ID 检查会话，然后返回请求的数据。</p>
<p>最后，一旦离开游乐园，你的腕带将不再起作用 - 你无法使用它重新回到公园参与游乐活动。</p>
<p>登出网站也一样。浏览器将带有 cookie 的注销请求发送到服务端，服务端删除 session，并告知浏览器删除具有相应 session id 的 cookie。</p>
<p>如果想回到游乐园，则必须购买另一张门票并获得另一个腕带。同样，如果想继续使用网站，就必须重新登录。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/fireship-cookies.png" alt="fireship-cookies" width="600" height="400" loading="lazy"></p>
<p>图片来源：<a href="https://www.youtube.com/watch?v=UBUNrFtufWo">Session vs Token Authentication in 100 Seconds</a> (YouTube)</p>
<p>这是如何使用 cookie 来记录登录网站状态的简单示例。它还可以用来记录设置的深色主题模式，以及跟踪你在网站上的其它行为等。</p>
<h2 id="cookie">怎样使用 cookie</h2>
<p>现在已经了解了 cookie 的历史以及它们的作用，接下来来看一下 cookie 的一些局限，然后用实例演示一下。</p>
<h3 id="cookie">cookie 的限制</h3>
<p>与浏览器中更常用的存储数据方案（例如，<code>localStorage</code> 或 <code>sessionStorage</code>）相比，cookie 有很多局限性。 这是 cookie 和其它技术对比的摘要：</p>
<table>
<thead>
<tr>
<th></th>
<th>cookies</th>
<th>Local Storage</th>
<th>Session Storage</th>
</tr>
</thead>
<tbody>
<tr>
<td>容量</td>
<td>4KB</td>
<td>10MB</td>
<td>5MB</td>
</tr>
<tr>
<td>可访问</td>
<td>任何窗口</td>
<td>任何窗口</td>
<td>相同标签页</td>
</tr>
<tr>
<td>过期</td>
<td>手动设置</td>
<td>永不过期</td>
<td>标签页关闭时</td>
</tr>
<tr>
<td>存储位置</td>
<td>浏览器和服务端</td>
<td>仅浏览器</td>
<td>仅浏览器</td>
</tr>
<tr>
<td>通过请求发送</td>
<td>是</td>
<td>否</td>
<td>否</td>
</tr>
</tbody>
</table>
<p>图片来源：<a href="https://www.youtube.com/watch?v=AwicscsvGLg">cookies vs localStorage vs sessionStorage - Beau teaches JavaScript</a> (YouTube)</p>
<p>cookies 是一种古老的技术，并且容量非常有限。不过，可以使用它们做很多事情。 它们的体积小巧，浏览器可以轻松地在每个请求中附带 cookie 发送到服务器。</p>
<p>还值得一提的是，出于安全原因，浏览器仅允许 cookie 在一个域名中可见。</p>
<p>因此，如果你通过 ally.com 登录银行，则 cookie 将仅在该域名及其子域名内起作用。例如，你的 <code>ally.com</code> 的 cookie 可以在 <code>ally.com</code>，<code>ally.com/about</code> 和域名 <code>www.ally.com</code> 上可见，而在 <code>axos.com</code> 上不可见。</p>
<p>这意味着，即使你同时拥有两个帐户并在 <code>ally.com</code> 和 <code>axos.com</code> 上都进行了登录，这些站点也将无法读取彼此的 cookie。</p>
<p>重要的是要记住，你的 cookie 是随浏览器中的请求一起发送的。这非常方便，但是存在一些严重的安全隐患，后面会详述。</p>
<p>最后，如果本文你只能记住一个知识点，那就请记住，cookie 是公开读取和发送的，切勿在其中存储诸如密码之类的敏感信息。</p>
<h3 id="javascriptcookie">怎样在 JavaScript 中设置 cookie</h3>
<p>cookies 实际上只是带有键/值对的字符串。尽管可能更多在后端使用 cookie，但也需要客户端设置 cookie。</p>
<p>以下是在 vanilla JavaScript 中设置 ocokie 的方法：</p>
<pre><code class="language-js">document.cookie = 'dark_mode=true'

</code></pre>
<p>然后，当打开开发者控制台时，单击“Application（应用程序）”，然后在站点的“Cookies”下，将看到刚添加的 cookie：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-101.png" alt="image-101" width="600" height="400" loading="lazy"></p>
<p>如果仔细查看 cookie，会发现它的过期日期设置为 <code>Session</code>。这意味着，当关闭标签页/浏览器时，cookie 将被销毁。</p>
<p>这可能是某些场景的预期行为，例如 cookie 存有付款信息的在线商店。</p>
<p>但是，如果希望 cookie 的存在时间更久，需要设置一个有效期。</p>
<h3 id="javascriptcookie">如何在 JavaScript 中设置 cookie 的过期时间</h3>
<p>要设置过期时间，只需设置 cookie 的值，然后添加带有日期配置的 expires 属性，该日期一般设置为未来的某个时间：</p>
<pre><code class="language-js">document.cookie = 'dark_mode=true; expires=Fri, 26 Feb 2021 00:00:00 GMT' // expires 1 week from now

</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-102.png" alt="image-102" width="600" height="400" loading="lazy"></p>
<p>JavaScript <code>Date</code> 对象可以很方便的生成日期。 可以在<a href="https://www.freecodecamp.org/news/the-ultimate-guide-to-javascript-date-and-moment-js/">此处</a>阅读有关 <code>Date</code> 对象的更多信息。</p>
<p>或者，也可以将 <code>max-age</code> 属性与你希望 cookie 生效的秒数结合使用：</p>
<pre><code class="language-js">document.cookie = 'dark_mode=true; max-age=604800'; // expires 1 week from now
</code></pre>
<p>然后，当到达该日期时，浏览器将自动删除 cookie。</p>
<h3 id="javascriptcookie">如何在 JavaScript 中更新 cookie</h3>
<p>cookie 更新很方便，和是否具有有效期无关。</p>
<p>只需更改 cookie 的值，浏览器就会自动配置：</p>
<pre><code class="language-js">document.cookie = "dark_mode=false; max-age=604800"; // expires 1 week from now

</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-105.png" alt="image-105" width="600" height="400" loading="lazy"></p>
<h3 id="javascriptcookie">如何在 JavaScript 中设置 cookie 的路径</h3>
<p>有时，只希望 cookie 在网站的某些部分生效。如何操作取决于网站的设置方式，一种方法是使用 <code>path</code> 属性。</p>
<p>配置 cookie 的方法如下：使 cookie 仅在 <code>/about</code> 的 about 页面上起作用：</p>
<pre><code class="language-js">document.cookie = 'dark_mode=true; path=/about';
</code></pre>
<p>现在，cookie 仅能在 <code>/about</code> 和其他子目录（例如 <code>/about/team</code>）上生效，而不能在<code>/blog</code> 上生效。</p>
<p>然后，当访问 about 页面并查看 cookie 时，如下：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-103.png" alt="image-103" width="600" height="400" loading="lazy"></p>
<h3 id="javascriptcookie">如何在 JavaScript 中删除 cookie</h3>
<p>要在 JavaScript 中删除 cookie，只需将 <code>expires</code> 属性设置为已过的日期即可：</p>
<pre><code class="language-js">document.cookie = 'dark_mode=true; expires=Sun, 14 Feb 2021 00:00:00 GMT'; // 1 week earlier

</code></pre>
<p>也可以使用 <code>max-age</code> 并将其设置为负值：</p>
<pre><code class="language-js">document.cookie = 'dark_mode=true; max-age=-60'; // 1 minute earlier

</code></pre>
<p>然后，查看 cookie，会发现它已经被删除：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/image-104.png" alt="image-104" width="600" height="400" loading="lazy"></p>
<p>这应该是在 vanilla JS 中使用 cookie 所需了解的比较全面的知识。</p>
<p>我们介绍的所有内容都能够满足常见的需求，如果对 cookie 的使用比较频繁，可以考虑类库 <a href="https://github.com/js-cookie/js-cookie">JavaScript Cookie</a> 或 <a href="https://github.com/expressjs/cookie-session">Cookie Parser</a>。</p>
<h2 id="cookie">cookie 的安全性问题</h2>
<p>通常，cookie 在正确配置的前提下非常安全。浏览器有很多内置的限制，我们之前已经介绍了这些限制，部分原因是该技术的年代久远，这反而提高了其安全性。</p>
<p>尽管如此，攻击者还是有一些办法窃取你的 cookie 并利用它来搞破坏。</p>
<p>我们将探讨一些常见的攻击手段，并介绍对该攻击的应对策略。</p>
<p>另外，请注意，所有代码段都将使用 。如果要在服务器上实现这些修补程序，则需要查找语言或框架的确切语法。</p>
<h3 id="">中间人攻击</h3>
<p>中间人（MitM）攻击描述了广泛的攻击类别，其中，攻击者在客户端和服务器之间，并拦截在两者之间传递的数据。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/man-in-the-middle-attack-how-avoid.png" alt="man-in-the-middle-attack-how-avoid" width="600" height="400" loading="lazy"></p>
<p>图片来源：<a href="https://www.netsparker.com/blog/web-security/man-in-the-middle-attack-how-avoid/">Man-in-the-Middle Attacks and How To Avoid Them</a></p>
<p>这可以通过多种方式完成：通过访问或收听不安全的网站，模仿公共 WiFi 路由器，DNS 欺骗或通过 <a href="https://en.wikipedia.org/wiki/Superfish">SuperFish</a> 一类的恶意软件/广告软件。</p>
<p>这是 MitM 攻击的比较深入的介绍概述，涉及了网站如何保护自己和用户。</p>
<p>警告：视频的开头谈论了斯科茨女王玛丽，并生动地描绘了她的斩首。它有一点点暴力，但是如果你想跳过它，请跳至 00:57 处播放。</p>
<figure class="kg-card kg-embed-card" data-test-label="fitted">
        <div class="fluid-width-video-container">
          <div style="padding-top: 56.25%;" class="fluid-width-video-wrapper">
            <iframe src="//player.bilibili.com/player.html?aid=375699669&amp;bvid=BV17o4y1177Y&amp;cid=343253065&amp;page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" width="256" height="144" name="fitvid0"> </iframe>
          </div>
        </div>
      </figure>
<p>作为开发人员，通过确保在服务器上启用 HTTPS，使用来自受信任的证书颁发机构的 SSL 证书以及确保代码使用 HTTPS 而不是不安全的 HTTP，可以大大减少 MitM 攻击的可能性。</p>
<p>就 cookie 而言，你应该在 cookie 中添加 <code>Secure</code> 属性，以便它们只能通过安全的 HTTPS 连接发送：</p>
<pre><code class="language-js">document.cookie = 'dark_mode=false; Secure';
</code></pre>
<p>请记住，<code>Secure</code> 属性实际上并不会加密 cookie 中的任何数据 – 它只是确保 cookie 无法通过 HTTP 连接发送。</p>
<p>但是，攻击者仍然可能会拦截和操纵 cookie。为了防止这种情况的发生，还可以使用 <code>HttpOnly</code> 参数：</p>
<pre><code class="language-js">document.cookie = 'dark_mode=false; Secure; HttpOnly';
</code></pre>
<p>带有 <code>HttpOnly</code> 的 cookie 只能由服务器访问，而不能由浏览器的 <code>Document.cookie</code> API 访问。这非常适合诸如登录会话之类的场景，在该会话中，只有服务器真正需要知道你是否已登录到站点，而客户端不需要该信息。</p>
<h3 id="xss">XSS 攻击</h3>
<p>XSS（跨站点脚本）攻击描述了恶意用户将意想不到的、潜在危险的代码注入网站时的一类攻击。</p>
<p>这些攻击非常棘手，因为它们可能会影响访问该网站的每个人。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/cross-site-scripting.svg" alt="cross-site-scripting" width="600" height="400" loading="lazy"></p>
<p>图片来源：<a href="https://portswigger.net/web-security/cross-site-scripting">Cross-site scripting</a></p>
<p>例如，如果某个站点具有评论功能，并且某人能够将恶意代码包含在评论中，则访问该站点并阅读该评论的每个人都有可能受到影响。</p>
<p>就 cookie 而言，如果恶意行为者在站点上成功发起 XSS 攻击，则他们可以访问会话 cookie 并以另一个已登录用户的身份访问该站点。这样，他们就可以访问其他用户的设置，以该用户的身份购买商品并将其运送到另一个地址，等各种操作。</p>
<p>这是一个视频，概述了 XSS 的不同类型 - Reflected、Stored，基于 DOM 的和 Mutation：</p>
<figure class="kg-card kg-embed-card" data-test-label="fitted">
        <div class="fluid-width-video-container">
          <div style="padding-top: 56.25%;" class="fluid-width-video-wrapper">
            <iframe src="//player.bilibili.com/player.html?aid=837611819&amp;bvid=BV1Rg4y1b7VC&amp;cid=168863963&amp;page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" width="256" height="144" name="fitvid1"> </iframe>
          </div>
        </div>
      </figure>
<p>作为开发人员，要确保服务器执行“相同来源策略”，并确保正确过滤了从用户那里收到的任何输入。</p>
<p>就像防止 MitM 攻击一样，你应该为你使用的任何 cookie 设置 <code>Secure</code> 和 <code>HttpOnly</code> 参数：</p>
<pre><code class="language-js">document.cookie = 'dark_mode=false; Secure; HttpOnly';
</code></pre>
<h3 id="csrf">CSRF 攻击</h3>
<p>CSRF（跨站点请求伪造）攻击是指攻击者诱骗他人执行意想不到的，潜在的恶意行为。</p>
<p>例如，如果你登录到站点并单击评论中的链接，如果该链接是 CSRF 攻击的一部分，则可能会导致你无意中更改登录详细信息，甚至删除帐户。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/02/cross-site-request-forgery.svg" alt="cross-site-request-forgery" width="600" height="400" loading="lazy"></p>
<p>图片来源：<a href="https://portswigger.net/web-security/csrf">Cross-site request forgery</a></p>
<p>虽然 CSRF 攻击在某种程度上与 XSS 攻击有关，特别是反映了有人将恶意代码插入站点的 XSS 攻击，但每种攻击都基于不同类型的信任。</p>
<p>根据 <a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">Wikipedia</a> 的说法，虽然 XSS “利用了用户对特定站点的信任，但是 CSRF 却利用了站点在用户浏览器中访问的信任。 ”</p>
<p>这是一段解释 CSRF 基础知识的视频，并提供了一些有用的示例：</p>
<figure class="kg-card kg-embed-card" data-test-label="fitted">
        <div class="fluid-width-video-container">
          <div style="padding-top: 56.25%;" class="fluid-width-video-wrapper">
            <iframe src="//player.bilibili.com/player.html?aid=248191316&amp;bvid=BV13v411L7e7&amp;cid=343251479&amp;page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" width="256" height="144" name="fitvid2"> </iframe>
          </div>
        </div>
      </figure>
<p>至于 cookie，防止可能的 CSRF 攻击的一种方法是使用 <code>SameSite</code> 标志：</p>
<pre><code class="language-js">document.cookie = 'dark_mode=false; Secure; HttpOnly; SameSite=Strict';

</code></pre>
<p>您可以为 <code>SameSite</code> 设置一些值：</p>
<ul>
<li><code>Lax</code>:  cookie 不会发送嵌入内容（图像，iframe 等），而是在你单击链接或向该 cookie 所设置的来源发送请求时发送。例如，如果你在 <code>testing.com</code> 上，然后单击链接以转到 <code>test.com/about</code>，则浏览器将发送带有该请求的 <code>test.com</code> cookie。</li>
<li><code>Strict</code>：仅当你单击链接或从设置了 cookie 的来源发送请求时，才会发送 cookie。 例如，只会在你位于 <code>test.com</code> 及其附近时发送 <code>test.com</code> cookie，而不会来自 <code>testing.com</code> 之类的其他网站。</li>
<li><code>None</code>：Cookie 会随每个请求发送，无论上下文如何。 如果将 <code>SameSite</code> 设置为 <code>None</code>，则还必须添加 <code>Secure</code> 属性。如果可能，最好避免使用此值。</li>
</ul>
<p>主流浏览器对 <code>SameSite</code> 的处理方式略有不同。例如，如果未在 cookie上设置 <code>SameSite</code>，则 Google Chrome 默认将其设置为  <code>Lax</code> 。</p>
<h2 id="cookie">cookie 的替代品</h2>
<p>你可能想知道，如果 cookie 有这么多潜在的安全漏洞，为什么我们仍在使用它们？ 当然，必须有更好的选择。</p>
<p>这些天来，你可以使用 <code>sessionStorage</code> 或 <code>localStorage</code> 来存储最初使用 cookie 的信息。对于有状态会话，还可以使用基于令牌的身份验证以及诸如 JWT（JSON Web令牌）之类的东西。</p>
<p>虽然似乎你必须在基于 cookie 的身份验证或基于令牌的身份验证之间进行选择，但也可以同时使用两者。 例如，当某人通过浏览器登录时，你可能要使用基于 cookie 的身份验证，而当某人通过电话应用程序登录时，则要使用基于令牌的身份验证。</p>
<p>为了进一步解决问题，Auth0 等 authentication-as-aservice 提供程序允许你同时进行两种身份验证。</p>
<p>如果你想了解有关 Web 令牌和基于令牌的身份验证的更多信息，请查看我们的一些<a href="https://www.freecodecamp.org/news/search/?query=web%20tokens">文章</a>。</p>
<h2 id="cookie">当你向开发人员提供 cookie 时</h2>
<p>就是这样！ 这就是你开始使用 cookie 所需的知识，以及在此过程中需要注意的事项。</p>
<p>你觉得这有用吗？ 你是如何使用 cookie？ 留言或给我发<a href="https://twitter.com/kriskoishigawa">推特</a>吧。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/everything-you-need-to-know-about-cookies-for-web-development/">Everything You Need to Know About Cookies for Web Development</a>，作者：<a href="https://www.freecodecamp.org/news/author/kris/">Kris Koishigawa</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Kubernetes 完全手册 ]]>
                </title>
                <description>
                    <![CDATA[ Kubernetes [https://kubernetes.io/]  是一个开放源代码的容器编排平台，可自动执行容器的部署、管理、扩容伸缩和网络管理。 它是由 Google [https://opensource.google/projects/kubernetes]  使用 Go 语言 [https://golang.org/]开发的，这项了不起的技术从 2014 年开始一直是开源的。 根据 Stack Overflow 开发者调研报告 - 2020 [https://insights.stackoverflow.com/survey/2020#overview]，Kubernetes 是 #3 最喜爱的平台 [https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-loved5] 以及 #3 最想要的平台 [https://insights.stackoverflow.com/survey/2020#technology-most-lo ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-kubernetes-handbook/</link>
                <guid isPermaLink="false">602f8d73c354c605689ea576</guid>
                
                    <category>
                        <![CDATA[ Kubernetes ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Wed, 19 May 2021 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/02/107889834_1307234906308429_6629041044900498480_n.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p><a href="https://kubernetes.io/">Kubernetes</a>  是一个开放源代码的容器编排平台，可自动执行容器的部署、管理、扩容伸缩和网络管理。</p>
<p>它是由 <a href="https://opensource.google/projects/kubernetes">Google</a> 使用  <a href="https://golang.org/">Go 语言</a>开发的，这项了不起的技术从 2014 年开始一直是开源的。</p>
<p>根据  <a href="https://insights.stackoverflow.com/survey/2020#overview">Stack Overflow 开发者调研报告 - 2020</a>，Kubernetes 是 <a href="https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-loved5">#3 最喜爱的平台</a>以及 <a href="https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-wanted5">#3 最想要的平台</a>。</p>
<p>除了功能强大之外，Kubernetes 是公认的难上手。入门确实不容易，但是只要你符合入门条件并且有足够的耐心完成该指南，你将可以：</p>
<ul>
<li>对基础知识有深入的了解。</li>
<li>可以创建和管理 Kubernetes 集群。</li>
<li>部署任意应用程序到 Kubernetes 集群上。</li>
</ul>
<h2 id="">入门条件</h2>
<ul>
<li>熟悉 JavaScript</li>
<li>熟悉 Linux 终端</li>
<li>熟悉 Docker（建议阅读：<a href="https://chinese.freecodecamp.org/news/the-docker-handbook/">Docker 入门教程 - 2021 最新版</a>）</li>
</ul>
<h2 id="">项目代码</h2>
<p>实例中的代码可以在<a href="https://github.com/fhsinchy/kubernetes-handbook-projects">这个仓库</a>中找到（你的 ⭐ 是我动力的源泉）。<code>k8s</code> 分支包含完整的代码。</p>
<h2 id="">目录</h2>
<ul>
<li>容器编排和 Kubernetes 简介</li>
<li>安装 Kubernetes</li>
<li>Kubernetes 初体验
<ul>
<li>Kubernetes 的架构</li>
<li>Control Plane 组件</li>
<li>Node 组件</li>
<li>Kubernetes 对象</li>
<li>Pods</li>
<li>Services</li>
<li>全景图</li>
<li>清除 Kubernetes 相关资源</li>
</ul>
</li>
<li>声明式部署方法
<ul>
<li>编写你的第一套配置</li>
<li>Kubernetes 控制面板</li>
</ul>
</li>
<li>使用多容器应用程序
<ul>
<li>部署计划</li>
<li>复用 Controllers, Replica Sets 以及 Deployments</li>
<li>创建你的第一个部署</li>
<li>调试 Kubernetes 资源</li>
<li>从 Pods 获取容器日志</li>
<li>环境变量</li>
<li>创建数据库部署</li>
<li>Persistent Volumes 和 Persistent Volume Claims</li>
<li>Persistent Volumes 的动态预配置</li>
<li>通过 Pods 连接 Volumes</li>
<li>组装起来</li>
</ul>
</li>
<li>使用 Ingress Controllers
<ul>
<li>设置 NGINX Ingress Controller</li>
<li>Kubernetes 中的 Secret 和配置</li>
<li>在 Kubernetes 中执行更新发布</li>
<li>组合 Configurations</li>
</ul>
</li>
<li>答疑</li>
<li>结论</li>
</ul>
<h2 id="kubernetes">容器编排和 Kubernetes 简介</h2>
<p>摘自 <a href="https://www.redhat.com/en/topics/containers/what-is-container-orchestration">Red Hat</a>  —</p>
<blockquote>
<p>"容器编排是指自动化容器的部署、管理、扩展和联网。</p>
<p>容器编排可以在使用容器的任何环境中使用。它可以帮助你在不同环境中部署相同的应用，而无需重新设计。"</p>
</blockquote>
<p>让我来给你看一个例子。假设你开发了一个很棒的应用，这个 应用可以根据时间向人们推荐他们应该吃什么。</p>
<p>假设你已经使用 Docker 容器化了应用并将其部署在了 AWS 上 。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/07/food-suggestion-application-single-instance.svg" alt="food-suggestion-application-single-instance" width="600" height="400" loading="lazy"></p>
<p>如果应用因为某种原因宕机，用户马上就不能访问该服务了。</p>
<p>要解决此问题，可以为同一应用程序制作多个副本，使其服务高可用。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/07/food-suggestion-application-multi-instance.svg" alt="food-suggestion-application-multi-instance" width="600" height="400" loading="lazy"></p>
<p>即使其中一台实例发生故障，其它两台实例也可以为用用户提供服务。</p>
<p>假设你的应用程序在熬夜党中流行了起来，在你晚上睡觉的时候涌入大量的请求。</p>
<p>如果所有的实例都因为过载而无法响应该怎么办？谁来进行自动伸缩？即使你扩容 了 50 个副本，谁来做健康检查？如何设置网络使使流量打到合适的端点上？负载均衡也是一个大问题，你说呢？</p>
<p>Kubernetes 可以很容易的搞定这些问题。Kubernetes 是一个由多个组件组成的容器编排平台，它可以一刻不眠的使是你的服务保持在理想状态。</p>
<p>假设你要连续运行 50 个应用程序副本，如果请求量激增，服务器也能自动扩容。</p>
<p>你只需把你的需求告诉 Kubernetes，它将为你完成其余的繁重工作。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/07/kube-representation.svg" alt="kube-representation" width="600" height="400" loading="lazy"></p>
<p>Kubernetes 会实现并维护状态。如果有旧副本挂掉了，它将创建新的副本，管理网络和存储，推出或回滚更新，甚至在必要时升级服务。</p>
<h2 id="kubernetes">安装 Kubernetes</h2>
<p>实际上，在本地计算机上运行 Kubernetes 与在云平台上运行 Kubernetes 有很大不同。你需要下面两个程序，来启动和运行 Kubernetes。</p>
<ul>
<li><a href="https://kubernetes.io/docs/tasks/tools/install-minikube/">minikube</a>  - 它可以在本地计算机的虚拟机（VM）上运行单节点 Kubernetes 集群。</li>
<li><a href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">kubectl</a>  -  Kubernetes 命令行工具，可以在 Kubernetes 集群上执行命令。</li>
</ul>
<p>除了这两个程序之外，你还需要一个管理程序和一个容器平台。显然 <a href="https://www.docker.com/">Docker</a> 就是所需的容器平台。推荐的管理程序如下：</p>
<ul>
<li><a href="https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v">Hyper-V</a> Windows 系统</li>
<li><a href="https://github.com/moby/hyperkit">HyperKit</a> Mac 系统</li>
<li><a href="https://www.docker.com/">Docker</a> Linux 系统</li>
</ul>
<p>Hyper-V 作为可选功能内置于  Windows 10（Pro、Enterprise 和 Education）中，可以从控制面板中打开。</p>
<p>HyperKit 是 Mac 平台 Docker Desktop 的核心组件。</p>
<p>在 Linux 上，你可以直接通过 Docker 绕过整个管理程序层。它比任何管理程序都高效，是 Linux 上运行 Kubernetes 的推荐方法。</p>
<p>你可以继续安装上述任何管理程序。或者你想保持简单，只需要获取 <a href="https://www.virtualbox.org/">VirtualBox</a>。</p>
<p>文章的剩余部分，我们假设你正在使用 VirtualBox。别担心，即使你正在使用其他管理程序，区别也不会太大。</p>
<blockquote>
<p>整篇文章，我在装有 <a href="https://www.freecodecamp.org/news/p/c4f90e6f-97af-41ce-b775-b6e52a5a5152/ubuntu.com/">Ubuntu</a> 的机器上使用带有 Docker 驱动程序的  <code>minikube</code> 完成。</p>
</blockquote>
<p>安装了管理程序和容器化平台之后，就该安装 <code>minikube</code>  和 <code>kubectl</code> 程序了。</p>
<p>如果你使用 Mac 或 Windows，安装完 Docker Desktop 后 <code>kubectl</code> 就已经安装了。可在<a href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">此处</a>找到 Linux 的安装说明。</p>
<p>另外 <code>minikube</code> 也是必需要安装的，可以在 Mac 上使用 <a href="https://brew.sh/">Homebrew</a>，Windows 上使用 <a href="https://chocolatey.org/">Chocolatey</a> 来安装 <code>minikube</code>。可以在<a href="https://kubernetes.io/docs/tasks/tools/install-minikube/">此处</a>找到 Linux 的安装说明。</p>
<p>安装完成后，可以通过执行以下命令来测试是否安装成功：</p>
<pre><code class="language-bash">minikube version

# minikube version: v1.12.1
# commit: 5664228288552de9f3a446ea4f51c6f29bbdd0e0
kubectl version

# Client Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.6", GitCommit:"dff82dc0de47299ab66c83c626e08b245ab19037", GitTreeState:"clean", BuildDate:"2020-07-16T00:04:31Z", GoVersion:"go1.14.4", Compiler:"gc", Platform:"darwin/amd64"}
# Server Version: version.Info{Major:"1", Minor:"18", GitVersion:"v1.18.3", GitCommit:"2e7996e3e2712684bc73f0dec0200d64eec7fe40", GitTreeState:"clean", BuildDate:"2020-05-20T12:43:34Z", GoVersion:"go1.13.9", Compiler:"gc", Platform:"linux/amd64"}
</code></pre>
<p>如果你已经为你的操作系统下载了正确的版本并且设置了正确路径，那么那你已经准备就绪啦。</p>
<p>正如我已经提到的，<code>minikube</code> 在本地计算机上的虚拟机（VM）中运行一个单节点 Kubernetes 集群。 我将在下一部分中更详细地解释集群和节点。</p>
<p>现在，可以理解为 <code>minikube</code> 使用你选择的管理程序创建常规 VM，并将其视为 Kubernetes 集群。</p>
<blockquote>
<p>如果你在本节中遇到任何问题，请查看本文结尾处的<a href="https://www.freecodecamp.org/news/the-kubernetes-handbook/#troubleshooting">答疑</a>部分。</p>
</blockquote>
<p>在启动 <code>minikube</code> 之前，必需正确设置管理程序才能使用。执行如下命令将 VirtualBox 设置为默认驱动程序：</p>
<pre><code class="language-bash">minikube config set driver virtualbox

# ❗ These changes will take effect upon a minikube delete and then a minikube start
</code></pre>
<p>可以根据需要将 <code>virtualbox</code> 替换为 <code>hyperv</code>、 <code>hyperkit</code> 或者 <code>docker</code>。这个命令只需运行一次。</p>
<p>执行下面的命令启动 <code>minikube</code>：</p>
<pre><code class="language-bash">minikube start

# ? minikube v1.12.1 on Ubuntu 20.04
# ✨ Using the virtualbox driver based on existing profile
# ? Starting control plane node minikube in cluster minikube
# ? Updating the running virtualbox "minikube" VM ...
# ? Preparing Kubernetes v1.18.3 on Docker 19.03.12 ...
# ? Verifying Kubernetes components...
# ? Enabled addons: default-storageclass, storage-provisioner
# ? Done! kubectl is now configured to use "minikube"
</code></pre>
<p>可以通过 <code>minikube stop</code> 命令来停止  <code>minikube</code> 。</p>
<h2 id="kubernetes">Kubernetes 初体验</h2>
<p>现在已经在本地上安装了 Kubernetes，是时候动手啦。在此示例中，会向本地集群部署一个非常简单的应用，并熟悉一下基础知识。</p>
<blockquote>
<p>本节中会涉及到诸如 <strong>pod</strong>,  <strong>service</strong>,  <strong>负载均衡</strong>等术语, 如果你没有搞懂他们，别急，我会在<a href="https://www.freecodecamp.org/news/the-kubernetes-handbook/#the-full-picture">全景图</a>小节中详细介绍它们。</p>
</blockquote>
<p>如果你在上一小节已经启动了 <code>minikube</code>，那么就可以开始啦，否则你需要先启动它哦。启动 <code>minikube</code> 后，在终端执行下面的命令：</p>
<pre><code class="language-bash">kubectl run hello-kube --image=fhsinchy/hello-kube --port=80

# pod/hello-kube created
</code></pre>
<p>你会立即看到 <code>pod/hello-kube created</code> 消息。 <a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#run">run</a> 命令用来在 pod 中运行指定的容器映像。</p>
<p>Pods 就像是封装容器的盒子，执行以下命令确保 pod 已经成功创建并运行：</p>
<pre><code class="language-bash">kubectl get pod

# NAME         READY   STATUS    RESTARTS   AGE
# hello-kube   1/1     Running   0          3m3s
</code></pre>
<p>你应该在  <code>STATUS</code>  列看到 <code>Running</code> 信息。如果看到类似 <code>ContainerCreating</code> 的信息，等待一两分钟，然后再次检查。</p>
<p>默认情况下，从集群外部无法访问 Pod。若要使其可访问，必需使用 service 使其暴露。运行 pod 后，执行下面的命令暴露 pod：</p>
<pre><code class="language-bash">kubectl expose pod hello-kube --type=LoadBalancer --port=80

# service/hello-kube exposed
</code></pre>
<p>执行以下命令确保负载均衡服务已经成功创建：</p>
<pre><code class="language-bash">kubectl get service

# NAME         TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
# hello-kube   LoadBalancer   10.109.60.75   &lt;pending&gt;     80:30848/TCP   119s
# kubernetes   ClusterIP      10.96.0.1      &lt;none&gt;        443/TCP        7h47m
</code></pre>
<p>请确保在列表中可以看到 <code>hello-kube</code>  服务。现在已经有了一个公开的 pod 正在运行，执行下面的命令访问它。</p>
<pre><code class="language-bash">minikube service hello-kube

# |-----------|------------|-------------|-----------------------------|
# | NAMESPACE |    NAME    | TARGET PORT |             URL             |
# |-----------|------------|-------------|-----------------------------|
# | default   | hello-kube |          80 | http://192.168.99.101:30848 |
# |-----------|------------|-------------|-----------------------------|
# ? Opening service default/hello-kube in default browser...
</code></pre>
<p>默认的 web 浏览器应该会自动打开，显示类似如下的内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-85.png" alt="image-85" width="600" height="400" loading="lazy"></p>
<p>这是一个非常简单的 JavaScript 应用程序，使用了 <a href="https://github.com/vitejs/vite">vite</a>  和一点 CSS。如果要了解刚才执行的命令，需要熟悉一下 Kubernetes 的架构。</p>
<h3 id="kubernetes">Kubernetes 的架构</h3>
<p>在 Kubernetes 的世界中，<strong>node</strong> 既可以是一台物理设备也可以是一台指定角色的虚拟机。这样的一组使用一个共享网络彼此通信的设备或者服务器的集合就叫做 <strong>集群（cluster）</strong>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/nodes-cluster-1.svg" alt="nodes-cluster-1" width="600" height="400" loading="lazy"></p>
<p>在本地设置中， <code>minikube</code> 是单节点的 Kubernetes 集群。因此 <code>minikube</code> 没有像上图的多个服务器，而是只有一台服务器同时充当主服务器和 node。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/minikube-1.svg" alt="minikube-1" width="600" height="400" loading="lazy"></p>
<p>Kubernetes 集群中的每台服务器都会获得一个角色。有两种不同的角色：</p>
<ul>
<li><strong>control-plane</strong>  — 做出大部分必要的决定，并充当整个集群的大脑。它可以是单个服务器或者大型项目中的一组服务器。</li>
<li><strong>node</strong>  — 负责运行工作负载，这些服务器通常由 control-plane 进行细微管理，并按照提供的说明执行各种任务。</li>
</ul>
<p>集群中每个服务器都将具有一组特定的组件。这些组件的数量和类型根据服务器在集群中承担的角色而有所不同。这意味着节点不必包含 control plane 中的所有的组件。</p>
<p>在接下来的小节里，将更详细的了解组成 Kubernetes  集群的各个组件。</p>
<h3 id="controlplane">Control Plane 组件</h3>
<p>Kubernetes 集群中的 control plane 由如下<strong>五</strong>个组件组成：</p>
<ol>
<li><strong>kube-api-server:</strong>  这是 Kubernetes control plane 的入口，负责验证和处理使用客户端库（如  <code>kubectl</code>  程序）传递的请求。</li>
<li><strong>etcd:</strong>  这是一个分布式键值存储，是整个集群的唯一键值来源。它保存了配置数据和集群的状态信息。<a href="https://etcd.io/">etcd</a> 是一个开源项目，由来自 Red Hat 的人开发。 该项目的源代码托管在 <a href="https://github.com/etcd-io/etcd">etcd-io/etcd</a> GitHub 仓库中。</li>
<li><strong>kube-controller-manager:</strong>  Kubernetes 中的 controller 负责控制集群的状态。当请求 Kubernetes 集群内容时，controller 会做出响应。<code>kube-controller-manager</code> 是通过一个进程管理所有 controller 进程的程序。</li>
<li><strong>kube-scheduler:</strong>  调度就是根据节点的可用资源和任务需要的资源分配任务。<code>kube-scheduler</code> 组件执行 Kubernetes 的任务调度以确保集群中所有的服务都不过载。</li>
<li><strong>cloud-controller-manager:</strong>  在真实的云环境中，此组件允许你通过 (<a href="https://cloud.google.com/kubernetes-engine">GKE</a>/<a href="https://aws.amazon.com/eks/">EKS</a>) API 连接集群。这样，与该云平台交互的组件就和与集群交互的组件隔离开了。在 <code>minikube</code> 这一类的组件中，该组件并不存在。</li>
</ol>
<h3 id="node">Node 组件</h3>
<p>与 control plane 相比，node 的组件数量非常少，如下：</p>
<ol>
<li><strong>kubelet:</strong>  该服务充当 control plane 和集群中每个节点之间的网关。从 control plane 到节点的每条指令都通过此服务。它还与  <code>etcd</code>  存储区进行交互以保持状态信息的更新。</li>
<li><strong>kube-proxy:</strong>  这个小服务运行在每个节点上，并为其维护网络规则。到达集群内部服务的任何网络请求都将通过此服务。</li>
<li><strong>Container Runtime:</strong>  Kubernetes 是一个容器编排工具，因此它最终在容器中运行应用程序。这意味着每个节点都需要一个容器环境，比如 <a href="https://www.docker.com/">Docker</a>、<a href="https://coreos.com/rkt/">rkt</a> 或者 <a href="https://cri-o.io/">cri-o</a>。</li>
</ol>
<h3 id="kubernetes">Kubernetes 对象</h3>
<p>摘自 Kubernetes <a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/">文档</a>  —</p>
<blockquote>
<p>"在 Kubernetes 系统中，<em>Kubernetes 对象</em> 是持久化的实体。 Kubernetes 使用这些实体去表示整个集群的状态。特别地，它们描述了如下信息：</p>
<ul>
<li>哪些容器化应用在运行（以及在哪些节点上）</li>
<li>可以被应用使用的资源</li>
<li>关于应用运行时表现的策略，比如重启策略、升级策略，以及容错策略</li>
</ul>
</blockquote>
<p>当创建 Kubernetes 对象时，实际上是在告诉 Kubernetes 系统这个对象应该存在，任何时候 Kubernetes 系统都应该确保该对象的’</p>
<p>运行。</p>
<h3 id="pods">Pods</h3>
<p>摘自 Kubernetes <a href="https://kubernetes.io/docs/concepts/workloads/pods/">文档</a>  —</p>
<blockquote>
<p>"<em>Pod</em> 是可以在 Kubernetes 中创建和管理的、最小的可部署的计算单元"。</p>
</blockquote>
<p>pod 通常封装一个或多个紧密相关的容器，共享一个生命周期和消耗性资源。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/pods-1.svg" alt="pods-1" width="600" height="400" loading="lazy"></p>
<p>尽管一个 pod 可以容纳多个容器，但是你不应该随意的把容器放到 pod 中。pod 中的容器必须紧密相关，以至于可以将它们视为单个应用程序。</p>
<p>例如，后端的 API 可能依赖数据库，但这并不意味着需要把他们都放在同一个容器中。在整篇文章中，不会有任何 pod 放置多个容器。</p>
<p>通常，你不应该直接管理 pod。相反，你应该使用可以提供更好的可管理的高级对象。将在后面的部分中介绍这些更高级别的对象。</p>
<h3 id="services">Services</h3>
<p>摘自 Kubernetes <a href="https://kubernetes.io/docs/concepts/services-networking/service/">文档</a>  —</p>
<blockquote>
<p>"Kubernetes  的 service 是将运行在一组 <a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-overview/">Pods</a> 上的应用程序公开为网络服务的抽象方法"。</p>
</blockquote>
<p>Kubernetes  pods 是非永久性资源。他们被创造出来，即使过一段时间被销毁了，也不会被回收。</p>
<p>相反，新的 pod 取代了旧的 pod。一些更高级别的对象甚至能动态创建和销毁 pod。</p>
<p>在创建每个 pod 时，会为其分配一个新的 IP 地址。但是对于可以创建、销毁和组合多个 pod 的高级对象而言，在此刻运行的 pod 集合可能与稍后运行的 pod 集合并不相同。</p>
<p>这就导致了一个问题：如果集群中的某些 pod 依赖于集群中的另一组 pod，怎样定位并跟踪彼此的 IP 地址呢？</p>
<p>根据 Kubernetes <a href="https://kubernetes.io/docs/concepts/services-networking/service/">文档</a>  —</p>
<blockquote>
<p>"Kubernetes Service 定义了这样一种抽象：逻辑上的一组 Pod，一种可以访问它们的策略 —— 通常称为微服务"。</p>
</blockquote>
<p>本质上讲，service 将执行相同功能的多个 pod 组合在一起，并将它们显示为单个实体。</p>
<p>这样一来，如何跟踪多个 Pod 的问题就消失了，因为单个 service 现在充当了所有 pod 的沟通器。</p>
<p>在 <code>hello-kube</code> 示例中，创建了一个 <code>LoadBalancer</code> 类型的服务，该服务可以将来自集群外部的请求连接到集群内部运行的 pod 上。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/load-balancer-3.svg" alt="load-balancer-3" width="600" height="400" loading="lazy"></p>
<p>无论何时，在需要授予另一个应用程序或者集群外部某个对象一个或多个 pod 的访问权限时，就应该创建一个 service。</p>
<p>比如，如果你有一组运行 web 服务的 pod，需要从 internet 进行访问，那么就必需用 service 提供必要的抽象。</p>
<h3 id="">全景图</h3>
<p>现在你已经对 Kubernetes 的各个组件有了适当的了解，下图描述了他们是如何协作的：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/components-of-kubernetes.png" alt="components-of-kubernetes" width="600" height="400" loading="lazy"></p>
<p><a href="https://kubernetes.io/docs/concepts/overview/components/">https://kubernetes.io/docs/concepts/overview/components/</a></p>
<p>在解释各个细节之前，先看一下 Kubernetes <a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/">文档</a> --</p>
<blockquote>
<p>"操作 Kubernetes 对象 —— 无论是创建、修改，或者删除 —— 需要使用  <a href="https://kubernetes.io/docs/concepts/overview/kubernetes-api/">Kubernetes API</a>。 比如，当使用 <code>kubectl</code> 命令行接口时，CLI 会执行必要的 Kubernetes API 调用。"</p>
</blockquote>
<p>运行的第一个命令是 <code>run</code> 命令，如下：</p>
<pre><code class="language-bash">kubectl run hello-kube --image=fhsinchy/hello-kube --port=80
</code></pre>
<p><code>run</code> 命令负责运行指定的镜像创建新的  pod。运行此命令后，Kubernetes 集群会执行下面的事件：</p>
<ul>
<li><code>kube-api-server</code>  组件接收请求，对其进行校验并进行处理。</li>
<li><code>kube-api-server</code>  接着与节点上的 <code>kubelet</code> 进行通信，并提供创建 pod 所需的指令。</li>
<li><code>kubelet</code> 组件开始启动运行 pod，并且在  <code>etcd</code>  存储中保持状态的更新。</li>
</ul>
<p><code>run</code> 命令的通用语法如下：</p>
<pre><code class="language-bash">kubectl run &lt;pod name&gt; --image=&lt;image name&gt; --port=&lt;port to expose&gt;
</code></pre>
<p>可以在 pod 内运行任何有效的容器映像。<a href="https://hub.docker.com/r/fhsinchy/hello-kube">fhsinchy/hello-kube</a> Docker 镜像包含了一个非常简单的 JavaScript 应用程序，该应用程序在容器内部的 80 端口上运行。 <code>--port=80</code> 选项允许容器从内部暴露 80 端口。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/pods-2.svg" alt="pods-2" width="600" height="400" loading="lazy"></p>
<p>新创建的 Pod 运行在 <code>minikube</code> 集群内部，并且无法从外部访问。要公开容器并使其可用，运行的第二个命令如下：</p>
<pre><code class="language-bash">kubectl expose pod hello-kube --type=LoadBalancer --port=80
</code></pre>
<p><code>expose</code> 命令负责创建类型为 <code>LoadBalancer</code> Kubernetes service，该服务允许用户访问 Pod 中运行的应用程序。</p>
<p>和  <code>run</code>  命令一样， <code>expose</code>  命令的执行需要在集群内部运行相似的步骤。在这里， <code>kube-api-server</code> 向 <code>kubelet</code> 组件提供了创建 service （而不是 pod）所需的指令。</p>
<p><code>expose</code>  命令的通用语法如下：</p>
<pre><code class="language-bash">kubectl expose &lt;resource kind to expose&gt; &lt;resource name&gt; --type=&lt;type of service to create&gt; --port=&lt;port to expose&gt;
</code></pre>
<p>对象类型可以是任意合法的 Kubernetes  对象类型。名称必需和要暴露的对象名称匹配。</p>
<p><code>--type</code> 表示所需的 service 类型。在内部或外部网络中一共有四种不同的 service 类型。</p>
<p>最后， <code>--port</code> 是要从容器中暴露的端口号。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/services-half.svg" alt="services-half" width="600" height="400" loading="lazy"></p>
<p>创建完 service 后，最后一件事就是访问在 pod 的应用程序。为此，需要执行如下命令：</p>
<pre><code class="language-bash">minikube service hello-kube
</code></pre>
<p>和之前的命令不同 ，最后一个命令没有用 <code>kube-api-server</code>。它使用  <code>minikube</code>  和本地集群通讯。  <code>minikube</code> 的 <code>service</code> 命令会返回给定服务的完整 URL。</p>
<p>当使用  <code>--port=80</code> 选项创建  <code>hello-kube</code> 容器时，Kubernetes 会在容器内暴露 80 端口，但是无法在集群外部访问它。</p>
<p>接着，使用  <code>--port=80</code> 选项创建 <code>LoadBalancer</code> 服务，它将 80 端口从该容器映射到本地系统中的任意端口，从而可以从集群外部访问它。</p>
<p>在我的系统上， <code>service</code> 命令返回 pod 的 URL <code>192.168.99.101:30848</code>。该 URL 中的 IP 实际上是 <code>minikube</code>  虚拟机的真实 IP。可以通过下面的命令来验证：</p>
<pre><code class="language-bash">minikube ip

# 192.168.99.101
</code></pre>
<p>可以通过如下命令验证 <code>30848</code> 端口是否指向 pod 内部的 80 端口：</p>
<pre><code class="language-bash">kubectl get service hello-kube

# NAME         TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
# hello-kube   LoadBalancer   10.109.60.75   &lt;pending&gt;     80:30848/TCP   119s
</code></pre>
<p>在  <code>PORT(S)</code> 列上，可以看到 80 端口实际上映射到本地系统的 30484 端口。因此，无需运行 <code>service</code> 命令，只需找到 IP 和端口号，然后就可以在浏览器内访问  <code>hello-kube</code> 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-86.png" alt="image-86" width="600" height="400" loading="lazy"></p>
<p>目前集群的状态如下所示：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/services-1.svg" alt="services-1" width="600" height="400" loading="lazy"></p>
<p>如果你了解 Docker，那么你可能觉得使用 service 来公开 pod 有点太麻烦了。</p>
<p>但是当你处理涉及多个 pod 的实例时，你就会了解 Kubernetes 这么做的便利了。</p>
<h2 id="kubernetes">清除 Kubernetes 相关资源</h2>
<p>现在已经了解如何创建 pod 和 service 之类的 Kubernetes  资源，现在来学习如何清除它们。也就是删除它们。</p>
<p>执行  <code>kubectl</code> 的  <code>delete</code>  命令来删除资源，用法如下：</p>
<pre><code class="language-bash">kubectl delete &lt;resource type&gt; &lt;resource name&gt;
</code></pre>
<p>使用下面的命令删除名为 <code>hello-kube</code>  的 pod：</p>
<pre><code class="language-bash">kubectl delete pod hello-kube

# pod "hello-kube" deleted
</code></pre>
<p>使用下面的命令删除名为 <code>hello-kube</code> 的 service：</p>
<pre><code class="language-bash">kubectl delete service hello-kube

# service "hello-kube" deleted
</code></pre>
<p>或者（现在不可用）使用 <code>delete</code>  命令的 <code>--all</code>  选项来一次性删除所有此类对象。该选项的通用语法如下：</p>
<pre><code class="language-bash">kubectl delete &lt;object type&gt; --all
</code></pre>
<p>如果要删除所有的 pod 和 service，依次执行  <code>kubectl delete pod --all</code>  和<code>kubectl delete service --all</code>。</p>
<h2 id="">声明式部署方法</h2>
<p>坦白讲，你在上一节看到的 <code>hello-kube</code> 例子并不是部署 Kubernetes 的最佳方式。</p>
<p>在之前章节采用的是<strong>交互式途径（imperative approach）</strong>，这意味着你必须手动逐个执行每个命令。采用交互式方法无法很好的工程化。</p>
<p>使用 Kubernetes 进行部署的理想方式是<strong>声明式途径（declarative approach）</strong>，作为开发人员，只需让 Kubernetes 知道服务需要达到的状态，其余的 Kubernetes 会搞定。</p>
<p>在本节中，将会使用声明式部署相同的  <code>hello-kube</code> 应用程序。</p>
<p>如果你尚未克隆上面链接的代码仓库，请立即进行操作。</p>
<p>克隆完毕后，进入 <code>hello-kube</code> 目录，该目录包含 <code>hello-kube</code> 应用程序的代码以及用户构建镜像的  <code>Dockerfile</code> 。</p>
<pre><code class="language-bash">├── Dockerfile
├── index.html
├── package.json
├── public
└── src

2 directories, 3 files
</code></pre>
<p>JavaScript 代码位于  <code>src</code>  文件夹下，无需关注，你应该看一下 <code>Dockerfile</code>，了解一下计划部署。<code>Dockerfile</code>  文件内容如下：</p>
<pre><code class="language-dockerfile">FROM node as builder

WORKDIR /usr/app

COPY ./package.json ./
RUN npm install
COPY . .
RUN npm run build

EXPOSE 80

FROM nginx
COPY --from=builder /usr/app/dist /usr/share/nginx/html
</code></pre>
<p>如你所见，这是一个<a href="https://www.freecodecamp.org/news/the-docker-handbook/#multi-staged-builds">多阶段构建（multi-staged build）</a>。</p>
<ul>
<li>第一阶段使用 <code>node</code> 作为基本镜像，然后将 JavaScript 应用程序编译为生产状态。</li>
<li>第二阶段复制第一阶段生成的文件，并将其粘贴到默认的 NGINX 文档根目录中。这里假设第二阶段的基本镜像是 <code>nginx</code>，会把第一阶段构建的文件运行在 80 端口（nginx 默认端口）。</li>
</ul>
<p>要在 Kubernetes 上部署应用，需要找到一种方式把镜像运行在容器里，并使其能在外部世界通过 80 端口访问。</p>
<h3 id="">编写你的第一套配置</h3>
<p>在声明式方式中，无需在终端中发送单个命令，只需在 YAML 文件中写下必要的配置，然后将其提供给 Kubernetes 即可。</p>
<p>在  <code>hello-kube</code> 工程目录下，创建另一个名为 <code>k8s</code> 的目录，<code>k8s</code> 是 k(ubernete = 8 个字符)s 的缩写。</p>
<p>文件夹名不必一定是 k8s，可以任意命名。</p>
<p>甚至没有必要将其放在项目目录中，这些配置文件可以放在计算机中的任何位置，因为这些配置与项目源代码无关。</p>
<p>在  <code>k8s</code>   目录中，创建一个名为  <code>hello-kube-pod.yaml</code> 的新文件。先看一下所有的代码，后面会逐行解释。文件内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: hello-kube-pod
  labels:
    component: web
spec:
  containers:
    - name: hello-kube
      image: fhsinchy/hello-kube
      ports:
        - containerPort: 80

</code></pre>
<p>每个有效的 Kubernetes 配置文件都有四个必填字段。如下：</p>
<ul>
<li><code>apiVersion</code>: 创建对象使用的 Kubernetes API 版本。该值可能会根据你创建的对象的类型而变化。对于 <code>Pod</code>  的创建，所需的版本是 <code>v1</code>。</li>
<li><code>kind</code>: 创建的对象的类型。Kubernetes 中有许多种对象。本文介绍了很多对象，目前只需知道要创建的对象是  <code>Pod</code>  即可。</li>
<li><code>metadata</code>: 对象的唯一标识数据。在此字段下，可以有 <code>name</code>、 <code>labels</code>、 <code>annotation</code>  等信息。当使用 <code>kubectl</code> 命令时 <code>metadata.name</code> 会显示在终端上。<code>metadata.labels</code> 字段下的键值对不必一定是  <code>components: web</code> ，可以指定任意 label 比如 <code>app: hello-kube</code>。在接下来创建 <code>LoadBalancer</code> service 时会使用该值作为选择器。</li>
<li><code>spec</code>: 包含对象希望达成的状态。 <code>spec.containers</code> 子字段包含将要运行在 <code>Pod</code> 内的容器信息。 <code>spec.containers.name</code> 是节点内的容器运行时分配给新创建容器的值。<code>spec.containers.image</code> 是用来创建容器的镜像。<code>spec.containers.ports</code> 字段是各种端口的配置。<code>containerPort: 80</code> 表示容器对外暴露的端口是 80。</li>
</ul>
<p>现在使用 <code>apply</code> 命令将这个文件提供给 Kubernetes，用法如下：</p>
<pre><code class="language-bash">kubectl apply -f &lt;configuration file&gt;
</code></pre>
<p>如下命令 apply 了名为 <code>hello-kube-pod.yaml</code> 的配置文件：</p>
<pre><code class="language-bash">kubectl apply -f hello-kube-pod.yaml

# pod/hello-kube-pod created
</code></pre>
<p>执行以下命令以确保 <code>Pod</code> 已经成功启动并且正在运行：</p>
<pre><code class="language-bash">kubectl get pod

# NAME         READY   STATUS    RESTARTS   AGE
# hello-kube   1/1     Running   0          3m3s
</code></pre>
<p>在 <code>STATUS</code> 列中能看到 <code>Running</code>。如果显示的是类似 <code>ContainerCreating</code> 的内容，请等待一两分钟后再试。</p>
<p>当 <code>Pod</code> 启动并运行后，就可以开始写 <code>LoadBalancer</code>  service 的配置文件了。</p>
<p>在 <code>k8s</code> 路径下创建一个名为 <code>hello-kube-load-balancer-service.yaml</code> 的文件内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: hello-kube-load-balancer-service
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 80
  selector:
    component: web
</code></pre>
<p>和之前的配置文件一样，<code>apiVersion</code>、<code>kind</code> 和 <code>metadata</code> 字段作用相同。如你所见，<code>metadata</code> 内没有 <code>labels</code> 字段，因为 service 使用 <code>labels</code> 选择其它对象，而其它对象无需选择 service。</p>
<blockquote>
<p>记住，service 为其他对象设置了访问策略，而其它对象无需为 service 设置访问策略。</p>
</blockquote>
<p>在  <code>spec</code> 字段内可以看到一组新的值。和 <code>Pod</code> 不同，service 有四种不同的类型，他们是 <code>ClusterIP</code>、 <code>NodePort</code>、 <code>LoadBalancer</code> 和<code>ExternalName</code>。</p>
<p>在此例中，使用的是 <code>LoadBalancer</code> 类型，这是把 service 暴露给集群外的标准方法。该服务会给你提供一个 IP 地址，可以使用该 IP 地址连接到集群内运行的应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/load-balancer-4.svg" alt="load-balancer-4" width="600" height="400" loading="lazy"></p>
<p><code>LoadBalancer</code> 类型需要两个端口值才能正常工作，在 <code>ports</code> 字段下，<code>port</code> 值用于访问 pod 本身，其值可以是任意值。</p>
<p><code>targetPort</code> 值是容器内部的值，必需要与容器内部的 port 一致。</p>
<p>正如之前所说，<code>hello-kube</code> 应用运行在容器内部的 80 端口上，已经在 <code>Pod</code> 配置文件中暴露了该端口，因此 <code>targetPort</code>  的值应该为 80。</p>
<p><code>selector</code> 字段用于标识将要连接该 service 的对象。<code>component: web</code> 键值对必须与 <code>Pod</code> 配置文件中的 <code>labels</code> 字段相匹配。如果你之前在配置文件里使用了其它的键值对如 <code>app: hello-kube</code> ，那么就改成你的键值。</p>
<p>在次使用 <code>apply</code> 命令将这个文件提供给 Kubernetes。文件名为<code>hello-kube-load-balancer-service.yaml</code>， 命令如下：</p>
<pre><code class="language-bash">kubectl apply -f hello-kube-load-balancer-service.yaml

# service/hello-kube-load-balancer-service created
</code></pre>
<p>执行以下命令以确保负载均衡器已经成功创建：</p>
<pre><code class="language-bash">kubectl get service

# NAME                               TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
# hello-kube-load-balancer-service   LoadBalancer   10.107.231.120   &lt;pending&gt;     80:30848/TCP   7s
# kubernetes                         ClusterIP      10.96.0.1        &lt;none&gt;        443/TCP        21h
</code></pre>
<p>确保在列表中能看到  <code>hello-kube-load-balancer-service</code>。现在你已经运行了一个公有的 pod，执行下面的命令直接进行访问：</p>
<pre><code class="language-bash">minikube service hello-kube-load-balancer-service

# |-----------|----------------------------------|-------------|-----------------------------|
# | NAMESPACE |           NAME                   | TARGET PORT |             URL             |
# |-----------|----------------------------------|-------------|-----------------------------|
# | default   | hello-kube-load-balancer-service |          80 | http://192.168.99.101:30848 |
# |-----------|----------------------------------|-------------|-----------------------------|
# ?  Opening service default/hello-kube-load-balancer in default browser...
</code></pre>
<p>默认的浏览器应该会自动打开，如下所示：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-87.png" alt="image-87" width="600" height="400" loading="lazy"></p>
<p>也可以将两个文件一起提供，如下所示，将文件名替换成目录名即可：</p>
<pre><code class="language-bash">kubectl apply -f k8s

# service/hello-kube-load-balancer-service created
# pod/hello-kube-pod created
</code></pre>
<p>请确保终端在 <code>k8s</code>  目录的父目录中。</p>
<p>如果位于 <code>k8s</code>  目录中，可以使用点 (<code>.</code>) 引用当前目录。应用批量配置时，最好提前清除之前创建的资源，以防发生冲突。</p>
<p>声明式方法是使用 Kubernetes 的理想方法，当然有一些例外情况，本文结尾会介绍。</p>
<h3 id="kubernetes">Kubernetes 控制面板</h3>
<p>在上一节中，使用  <code>delete</code>  命令清除了 Kubernetes  对象。</p>
<p>在本节中，会引入控制面板。Kubernetes 控制面板是一个图形用户界面，用于管理工作负载、service 等。</p>
<p>在终端中执行以下命令启动 Kubernetes 控制面板：</p>
<pre><code class="language-bash">minikube dashboard

# ? Verifying dashboard health ...
# ? Launching proxy ...
# ? Verifying proxy health ...
# ? Opening http://127.0.0.1:52393/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser...
</code></pre>
<p>控制面板应该会在浏览器中自动打开：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-88.png" alt="image-88" width="600" height="400" loading="lazy"></p>
<p>控制面板界面很直观，你可以快速上手。虽然创建、管理和删除对象都能从控制面板进行，但是本文其余部分还是会使用 cli 来操作。</p>
<p>在 <em>Pods</em> 列表中，可以使用右边的三个点菜单的 <em>Delete</em> 来删除 Pod。<code>LoadBalancer</code> service 也可以如此操作，实际上 <em>Services</em> 列表就位于 <em>Pods</em> 列表后。</p>
<p>可以按 <code>Ctrl + C</code> 组合键或者关闭终端窗口来停止控制面板服务。</p>
<h2 id="">使用多容器应用程序</h2>
<p>目前为止，已经使用了单个容器运行了应用程序。</p>
<p>在本节中，将会使用两个容器组成应用程序。你还会学习到 <code>Deployment</code>、<code>ClusterIP</code>、 <code>PersistentVolume</code>、<code>PersistentVolumeClaim</code> 以及一些调试技巧。</p>
<p>将使用的服务是一个具备完整 CRUD 功能的简单的基于 express 的日记 API。该应用使用 <a href="https://www.postgresql.org/">PostgreSQL</a> 数据库。因此不仅需要部署应用程序，还需要建立应用程序和数据库服务的内部网络连接。</p>
<p>该应用程序的代码位于项目仓库的 <code>notes-api</code> 目录中。</p>
<pre><code class="language-bash">.
├── api
├── docker-compose.yaml
└── postgres

</code></pre>
<p>应用程序代码位于  <code>api</code>  目录中，<code>postgres</code> 目录包含了创建 <code>postgres</code> 镜像的 <code>Dockerfile</code>。 <code>docker-compose.yaml</code> 文件包含使用 <code>docker-compose</code> 运行应用程序的配置文件。</p>
<p>就像上一个项目一样，可以查看每个 service 单独的 <code>Dockerfile</code>，以了解应用程序是如何在容器中运行的。</p>
<p>也可以只检查  <code>docker-compose.yaml</code> 并用它来规划 Kubernetes  部署。</p>
<pre><code class="language-yaml">version: "3.8"

services: 
    db:
        build:
            context: ./postgres
            dockerfile: Dockerfile.dev
        environment:
            POSTGRES_PASSWORD: 63eaQB9wtLqmNBpg
            POSTGRES_DB: notesdb
    api:
        build: 
            context: ./api
            dockerfile: Dockerfile.dev
        ports: 
            - 3000:3000
        volumes: 
            - /usr/app/node_modules
            - ./api:/usr/app
        environment: 
            DB_CONNECTION: pg
            DB_HOST: db
            DB_PORT: 5432
            DB_USER: postgres
            DB_DATABASE: notesdb
            DB_PASSWORD: 63eaQB9wtLqmNBpg

</code></pre>
<p>查看 <code>api</code> 服务定义，应该可以看到服务运行在内部容器的 3000 端口。它还需要一堆环境变量才能正常运行。</p>
<p>可以忽略 volumes，它在开发环境是必需的，并且构建配置是只针对于 Docker。因此可以保留 Kubernetes 配置文件几乎不变，如下：</p>
<ul>
<li>Port 映射 – 必需从容器公开相同的端口。</li>
<li>环境变量 – 这些变量在所有的环境中都是相同的（尽管值将发生变化）。</li>
</ul>
<p><code>db</code> 服务更简单，它只是一堆环境变量。甚至可以用官方的 <code>postgres</code> 镜像代替自定义的镜像。</p>
<p>使用自定义镜像的好处是数据库实例可以附带预先创建的 notes 表。</p>
<p>该表对于应用程序是必需的，如果查看 <code>postgres/docker-entrypoint-initdb.d</code> 目录，会看到一个名为 <code>notes.sql</code> 的文件，该文件用于在初始化期间创建数据库。</p>
<h3 id="">部署计划</h3>
<p>和之前的项目部署不同，该项目将变的更加复杂。</p>
<p>在这个项目中，将会创建三个 notes API 实例。这三个实例使用  <code>LoadBalancer</code> service 暴露在集群外面。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/notes-api-1.svg" alt="notes-api-1" width="600" height="400" loading="lazy"></p>
<p>除了这个实例，还会有一个 PostgreSQL 系统实例。notes API 应用程序的三个实例都使用 <code>ClusterIP</code>  service 和数据库实例通讯。</p>
<p><code>ClusterIP</code> service 是另外一种 Kubernetes  service，它在集群内部使应用可见。也就是说即使没有外部流量，应用程序也可以使用  <code>ClusterIP</code>  service。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/cluster-ip-2.svg" alt="cluster-ip-2" width="600" height="400" loading="lazy"></p>
<p>在此项目中，必需仅通过 Notes API 访问数据库，因此在集群中公开数据库服务是一个理想的选择。</p>
<p>上一节中已经提起到，不应该直接创建 pod。因此，在此项目中使用  <code>Deployment</code>  而不是 <code>Pod</code>。</p>
<h3 id="controllersreplicasetsdeployments">复制 Controllers、Replica Sets 以及 Deployments</h3>
<p>根据 Kubernetes <a href="https://kubernetes.io/docs/concepts/architecture/controller/">文档</a>  -</p>
<blockquote>
<p>"在 Kubernetes 中，控制器通过监控<a href="https://kubernetes.io/zh/docs/reference/glossary/?all=true#term-cluster">集群</a> 的公共状态，并致力于将当前状态转变为期望的状态。控制回路（Control Loop）是一个非终止回路，用于调节系统状态。"</p>
</blockquote>
<p><code>ReplicationController</code> 可以很轻松的创建多个副本。当创建所需的副本后，控制器将保持当前状态。</p>
<p>如果过了一段时间你决定减少副本的数量，那么 <code>ReplicationController</code> 会立刻清除多余的 pods。</p>
<p>否则，如果副本的数量少于预期数量（也许一些 pod 已经崩溃），<code>ReplicationController</code> 会创建新的副本以达到所需的状态。</p>
<p>尽管 <code>ReplicationController</code> 很强大，但目前已不是创建副本的推荐方式。它已经被较新的 API  <code>ReplicaSet</code> 取代。</p>
<p><code>ReplicaSet</code> 除了提供了更多选择外，<code>ReplicationController</code>  和 <code>ReplicaSet</code>  完成的几乎是同一件事。</p>
<p>拥有更多的选择器是件好事，但是更棒的是，能在发布和回滚更新方面具有更大的灵活性。这就该轮到另一个 Kubernetes  API <code>Deployment</code> 出场了。</p>
<p><code>Deployment</code> 就像是 <code>ReplicaSet</code>  API 的一个扩展。<code>Deployment</code> 不但允许你立即创建新副本，还允许使用一个或两个 <code>kubectl</code> 命令发布或回滚更新。</p>
<table>
<thead>
<tr>
<th>REPLICATIONCONTROLLER</th>
<th>REPLICASET</th>
<th>DEPLOYMENT</th>
</tr>
</thead>
<tbody>
<tr>
<td>可以轻松创建多个 pod</td>
<td>可以轻松创建多个 pod</td>
<td>可以轻松创建多个 pod</td>
</tr>
<tr>
<td>Kubernetes 中的原始复制方法</td>
<td>更灵活的选择器</td>
<td>扩展自 ReplicaSets，可以轻松更新和回滚</td>
</tr>
</tbody>
</table>
<p>在这个项目里，会使用  <code>Deployment</code> 来维护应用程序实例。</p>
<h3 id="">创建你的第一个部署</h3>
<p>首先，为 Notes API 部署编写配置文件，在 <code>notes-api</code> 目录中创建一个 <code>k8s</code> 目录。</p>
<p>在该目录中，创建一个名为  <code>api-deployment.yaml</code> 的文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: api
  template:
    metadata:
      labels:
        component: api
    spec:
      containers:
        - name: api
          image: fhsinchy/notes-api
          ports:
            - containerPort: 3000

</code></pre>
<p>在此文件中，<code>apiVersion</code>、 <code>kind</code>、  <code>metadata</code> 和  <code>spec</code> 字段作用与之前的项目相同。与上一个文件相比，不一样的地方如下：</p>
<ul>
<li>创建 pod 时，<code>apiVersion</code>的值是 <code>v1</code>。但是创建部署时，需要的版本是  <code>apps/v1</code>。Kubernetes API 的版本有时会有些混乱，你可能会有些一头雾水。可以阅读一下官网<a href="https://kubernetes.io/docs/home/">文档</a>对 <code>Deployment</code> YAML 文件的介绍。</li>
<li><code>spec.replicas</code> 定义了同时运行的副本数量。将此值设置为 3 意味着希望 Kubernetes 同时运行三个应用实例。</li>
<li>在 <code>spec.selector</code> 中，可以让 <code>Deployment</code> 知道要控制那些 pods。之前已经提到，<code>Deployment</code> 是 <code>ReplicaSet</code> 的扩展，可以控制 Kubernetes  对象。将 <code>selector.matchLabels</code> 设置为  <code>component: api</code> 意味着 <code>Deployment</code> 会控制 label 为 <code>component: api</code> 的 pods。这行代码的意思就是让 Kubernetes  知道你希望  <code>Deployment</code> 来控制 label 为 <code>component: api</code> 的 pods。</li>
<li><code>spec.template</code> 是用于配置 pod 的模板，它与之前的配置文件几乎相同。</li>
</ul>
<p>现在，要查看此配置效果，和之前一样 apply 该文件：</p>
<pre><code class="language-bash">kubectl apply -f api-deployment.yaml

# deployment.apps/api-deployment created
</code></pre>
<p>执行下面的命令确保  <code>Deployment</code> 已经成功创建：</p>
<pre><code class="language-bash">kubectl get deployment

# NAME             READY   UP-TO-DATE   AVAILABLE   AGE
# api-deployment   0/3     3            0           2m7s
</code></pre>
<p>如果查看  <code>READY</code> 列，会看到 <code>0/3</code>。这意味着容器尚未创建，等待几分钟，然后在试一次。</p>
<pre><code class="language-bash">kubectl get deployment

# NAME             READY   UP-TO-DATE   AVAILABLE   AGE
# api-deployment   0/3     3            0           28m
</code></pre>
<p>坦白讲，我已经等了将近半个小时，pod 还未准备就绪。API 本身只有几百 kb。这种规模的部署不应该花这么长的时间，这意味着有问题，我们来解决它。</p>
<h3 id="kubernetes">调试 Kubernetes 资源</h3>
<p>开始之前，首先回到起点。<code>get</code> 命令是一个很基础的命令。</p>
<p><code>get</code> 命令可以打印一张包含一个或多个 Kubernetes 资源重要信息的表。用法如下：</p>
<pre><code class="language-bash">kubectl get &lt;resource type&gt; &lt;resource name&gt;
</code></pre>
<p>在终端执行如下代码，在 <code>api-deployment</code> 上运行 <code>get</code> 命令：</p>
<pre><code class="language-bash">kubectl get deployment api-deployment

# NAME             READY   UP-TO-DATE   AVAILABLE   AGE
# api-deployment   0/3     3            0           15m
</code></pre>
<p>可以省略 <code>api-deployment</code> 以获取所有可用的部署列表。也可以在配置文件上使用  <code>get</code> 命令。</p>
<p>可以使用如下命令获取有关 <code>api-deployment.yaml</code> 文件中描述的部署信息：</p>
<pre><code class="language-bash">kubectl get -f api-deployment

# NAME             READY   UP-TO-DATE   AVAILABLE   AGE
# api-deployment   0/3     3            0           18m
</code></pre>
<p>默认情况下，<code>get</code> 命令显示的信息非常少，可以使用 <code>-o</code>  选项获取更多信息。</p>
<p><code>-o</code> 选项设置 <code>get</code> 命令的输出格式，可以使用 <code>wide</code> 输出格式查看更详细信息。</p>
<pre><code class="language-bash">kubectl get -f api-deployment.yaml

# NAME             READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES               SELECTOR
# api-deployment   0/3     3            0           19m   api          fhsinchy/notes-api   component=api

</code></pre>
<p>现在列表包含了更多的信息，可以在官方<a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#get">文档</a>了解有关  <code>get</code>  命令的选项。</p>
<p>老实说，仅仅是运行 <code>Deploymen</code> 的  <code>get</code>  命令没啥意思。还需要获取更底层资源的信息。</p>
<p>看一下 pod 列表，看看里面都有啥东西：</p>
<pre><code class="language-bash">kubectl get pod

# NAME                             READY   STATUS             RESTARTS   AGE
# api-deployment-d59f9c884-88j45   0/1     CrashLoopBackOff   10         30m
# api-deployment-d59f9c884-96hfr   0/1     CrashLoopBackOff   10         30m
# api-deployment-d59f9c884-pzdxg   0/1     CrashLoopBackOff   10         30m
</code></pre>
<p>现在发现了一些有用的东西。所有的 pods 都有一个值为 <code>CrashLoopBackOff</code> 的 <code>STATUS</code>。之前只接触过  <code>ContainerCreating</code> 和 <code>Running</code> 状态。你可能还会在  <code>CrashLoopBackOff</code> 处看到 <code>Error</code>。</p>
<p>看一下 <code>RESTARTS</code> 列，会发现 pod 已经重启 10 多次了，着意味着因为某些原因， pod 启动失败了。</p>
<p>现在，要查看一个 pod 的更详细信息，可以使用另一个名为 <code>describe</code> 的命令。它和 <code>get</code> 命令很像，用法如下：</p>
<pre><code class="language-bash">kubectl get &lt;resource type&gt; &lt;resource name&gt;
</code></pre>
<p>执行下面的命令查看 <code>api-deployment-d59f9c884-88j4</code> pod 的详细信息：</p>
<pre><code class="language-bash">kubectl describe pod api-deployment-d59f9c884-88j45

# Name:         api-deployment-d59f9c884-88j45
# Namespace:    default
# Priority:     0
# Node:         minikube/172.28.80.217
# Start Time:   Sun, 09 Aug 2020 16:01:28 +0600
# Labels:       component=api
#               pod-template-hash=d59f9c884
# Annotations:  &lt;none&gt;
# Status:       Running
# IP:           172.17.0.4
# IPs:
#   IP:           172.17.0.4
# Controlled By:  ReplicaSet/api-deployment-d59f9c884
# Containers:
#  api:
#     Container ID:   docker://d2bc15bda9bf4e6d08f7ca8ff5d3c8593655f5f398cf8bdd18b71da8807930c1
#     Image:          fhsinchy/notes-api
#     Image ID:       docker-pullable://fhsinchy/notes-api@sha256:4c715c7ce3ad3693c002fad5e7e7b70d5c20794a15dbfa27945376af3f3bb78c
#     Port:           3000/TCP
#     Host Port:      0/TCP
#     State:          Waiting
#       Reason:       CrashLoopBackOff
#     Last State:     Terminated
#       Reason:       Error
#       Exit Code:    1
#       Started:      Sun, 09 Aug 2020 16:13:12 +0600
#       Finished:     Sun, 09 Aug 2020 16:13:12 +0600
#     Ready:          False
#     Restart Count:  10
#     Environment:    &lt;none&gt;
#     Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-gqfr4 (ro)
# Conditions:
#   Type              Status
#   Initialized       True
#   Ready             False
#   ContainersReady   False
#   PodScheduled      True
# Volumes:
#   default-token-gqfr4:
#     Type:        Secret (a volume populated by a Secret)
#     SecretName:  default-token-gqfr4
#     Optional:    false
# QoS Class:       BestEffort
# Node-Selectors:  &lt;none&gt;
# Tolerations:     node.kubernetes.io/not-ready:NoExecute for 300s
#                  node.kubernetes.io/unreachable:NoExecute for 300s
# Events:
#   Type     Reason     Age                         From               Message
#   ----     ------     ----                        ----               -------
#   Normal   Scheduled  &lt;unknown&gt;                   default-scheduler  Successfully assigned default/api-deployment-d59f9c884-88j45 to minikube
#   Normal   Pulled     2m40s (x4 over 3m47s)       kubelet, minikube  Successfully pulled image "fhsinchy/notes-api"
#   Normal   Created    2m40s (x4 over 3m47s)       kubelet, minikube  Created container api
#   Normal   Started    2m40s (x4 over 3m47s)       kubelet, minikube  Started container api
#   Normal   Pulling    107s (x5 over 3m56s)        kubelet, minikube  Pulling image "fhsinchy/notes-api"
#   Warning  BackOff    &lt;invalid&gt; (x44 over 3m32s)  kubelet, minikube  Back-off restarting failed container
</code></pre>
<p>整个输出中最有用的部分是 <code>Events</code> 部分，如下：</p>
<pre><code>Events:
  Type     Reason     Age                         From               Message
  ----     ------     ----                        ----               -------
  Normal   Scheduled  &lt;unknown&gt;                   default-scheduler  Successfully assigned default/api-deployment-d59f9c884-88j45 to minikube
  Normal   Pulled     2m40s (x4 over 3m47s)       kubelet, minikube  Successfully pulled image "fhsinchy/notes-api"
  Normal   Created    2m40s (x4 over 3m47s)       kubelet, minikube  Created container api
  Normal   Started    2m40s (x4 over 3m47s)       kubelet, minikube  Started container api
  Normal   Pulling    107s (x5 over 3m56s)        kubelet, minikube  Pulling image "fhsinchy/notes-api"
  Warning  BackOff    &lt;invalid&gt; (x44 over 3m32s)  kubelet, minikube  Back-off restarting failed container
</code></pre>
<p>从这些事件中，可以看到容器镜像已经成功 pulled，容器也已经创建，但是显然从 <code>Back-off restarting failed container</code> 中可以看出该容器无法启动。</p>
<p>describe 命令和  <code>get</code> 命令类似，并且具有相同的选项。</p>
<p>可以省略 <code>api-deployment-d59f9c884-88j45</code> 名字以获取所有 pods 的信息。或者也可以使用  <code>-f</code> 选项将配置文件传入命令。访问官方<a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#describe">文档</a>了解更多信息。</p>
<p>既然已经知道了是容器出了问题，那么就让我们到容器层面看看到底出了什么问题吧。</p>
<h3 id="pods">从 Pods 获取容器日志</h3>
<p>还有另一个名为 <code>logs</code> 的 <code>kubectl</code> 命令，可以从容器内部获取容器的日志，用法按如下：</p>
<pre><code class="language-bash">kubectl logs &lt;pod&gt;
</code></pre>
<p>使用如下命令 <code>api-deployment-d59f9c884-88j45</code> 查看 pod 内的日志：</p>
<pre><code class="language-bash">kubectl logs api-deployment-d59f9c884-88j45
# &gt; api@1.0.0 start /usr/app
# &gt; cross-env NODE_ENV=production node bin/www
# /usr/app/node_modules/knex/lib/client.js:55
#     throw new Error(knex: Required configuration option 'client' is missing.);
    ^
# Error: knex: Required configuration option 'client' is missing.
#     at new Client (/usr/app/node_modules/knex/lib/client.js:55:11)
#     at Knex (/usr/app/node_modules/knex/lib/knex.js:53:28)
#     at Object.&lt;anonymous&gt; (/usr/app/services/knex.js:5:18)
#     at Module._compile (internal/modules/cjs/loader.js:1138:30)
#     at Object.Module._extensions..js (internal/modules/cjs/loader.js:1158:10)
#     at Module.load (internal/modules/cjs/loader.js:986:32)
#     at Function.Module._load (internal/modules/cjs/loader.js:879:14)
#     at Module.require (internal/modules/cjs/loader.js:1026:19)
#     at require (internal/modules/cjs/helpers.js:72:18)
#     at Object.&lt;anonymous&gt; (/usr/app/services/index.js:1:14)
# npm ERR! code ELIFECYCLE
# npm ERR! errno 1
# npm ERR! api@1.0.0 start: cross-env NODE_ENV=production node bin/www
# npm ERR! Exit status 1
# npm ERR!
# npm ERR! Failed at the api@1.0.0 start script.
# npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

</code></pre>
<p>这正是出问题的地方，看起来是 <a href="http://knexjs.org/">knex.js</a> 库缺少一个必要的值，导致程序启动失败。可以从官方<a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#logs">文档</a>了解更多关于 <code>logs</code> 命令的信息。</p>
<p>出现这个错误主要是因为在部署定义中缺少了一些必需的环境变量。</p>
<p>如果在看一下 <code>docker-compose.yaml</code> 文件中的 api service 定义，应该会看到类似如下的内容：</p>
<pre><code class="language-yaml">    api:
        build: 
            context: ./api
            dockerfile: Dockerfile.dev
        ports: 
            - 3000:3000
        volumes: 
            - /usr/app/node_modules
            - ./api:/usr/app
        environment: 
            DB_CONNECTION: pg
            DB_HOST: db
            DB_PORT: 5432
            DB_USER: postgres
            DB_DATABASE: notesdb
            DB_PASSWORD: 63eaQB9wtLqmNBpg
</code></pre>
<p>应用程序与数据库进行连接需要这些环境变量。所以，把这些数据添加到部署配置中就可以解决该问题。</p>
<h3 id="">环境变量</h3>
<p>给 Kubernetes  配置文件添加环境变量非常简单。打开  <code>api-deployment.yaml</code> 文件并按如下更新内容：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: api
  template:
    metadata:
      labels:
        component: api
    spec:
      containers:
        - name: api
          image: fhsinchy/notes-api
          ports:
            - containerPort: 3000
          
          # these are the environment variables
          env:
            - name: DB_CONNECTION
              value: pg
</code></pre>
<p><code>containers.env</code> 字段包含所有的环境变量，如果细心你会发现我还没有给 <code>docker-compose.yaml</code> 文件添加所有的环境变量，我只添加了一个。</p>
<p><code>DB_CONNECTION</code> 表示应用正在使用 PostgreSQL 数据库，添加这个变量就可以解决上述问题。</p>
<p>现在通过执行下面的命令在此 apply 配置文件：</p>
<pre><code class="language-bash">kubectl apply -f api-deployment.yaml

# deployment.apps/api-deployment configured
</code></pre>
<p>这次显示资源已经被 <code>configured</code>。这就是 Kubernetes 厉害的地方，apply 配置文件可以立即生效。</p>
<p>现在在次使用  <code>get</code>  命令确保一切运行正常。</p>
<pre><code class="language-bash">kubectl get deployment

# NAME             READY   UP-TO-DATE   AVAILABLE   AGE
# api-deployment   3/3     3            3           68m

kubectl get pod

# NAME                              READY   STATUS    RESTARTS   AGE
# api-deployment-66cdd98546-l9x8q   1/1     Running   0          7m26s
# api-deployment-66cdd98546-mbfw9   1/1     Running   0          7m31s
# api-deployment-66cdd98546-pntxv   1/1     Running   0          7m21s

</code></pre>
<p>三个 pod 都在运行，并且  <code>Deployment</code> 也运行良好。</p>
<h3 id="">创建数据库部署</h3>
<p>既然 API 已经启动并且运行，是时候为数据库实例编写配置了。</p>
<p>在 k8s 目录中另创建一个名为 <code>postgres-deployment.yaml</code> 的文件，文件内容如下：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      component: postgres
  template:
    metadata:
      labels:
        component: postgres
    spec:
      containers:
        - name: postgres
          image: fhsinchy/notes-postgres
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_PASSWORD
              value: 63eaQB9wtLqmNBpg
            - name: POSTGRES_DB
              value: notesdb

</code></pre>
<p>配置本身与上一次非常相似，我就不详细解释了，根据目前学到的知识你应该可以了解。</p>
<p>PostgreSQL 默认运行在 5432 端口上，运行 <code>postgres</code> 容器需要 <code>POSTGRES_PASSWORD</code> 变量。该密码也会用于 API 连接数据库。</p>
<p><code>POSTGRES_DB</code> 变量是可选的，但是在该项目里是必需的，否则会初始化失败。</p>
<p>可以在 Docker Hub 页面上了解 <a href="https://hub.docker.com/_/postgres">postgres</a> Docker 镜像的更多信息。在此项目中，化繁为简，副本数量设置为 1。</p>
<p>执行以下命令 apply 这个文件：</p>
<pre><code class="language-bash">kubectl apply -f postgres-deployment.yaml

# deployment.apps/postgres-deployment created
</code></pre>
<p>通过 <code>get</code> 命令确保 pod 已经正常部署和运行：</p>
<pre><code class="language-bash">kubectl get deployment

# NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
# postgres-deployment   1/1     1            1           13m

kubectl get pod

# NAME                                   READY   STATUS    RESTARTS   AGE
# postgres-deployment-76fcc75998-mwnb7   1/1     Running   0          13m

</code></pre>
<p>尽管 pod 已经成功部署和运行，但是数据库部署还是有很大的问题。</p>
<p>如果你以前使用过数据库系统，应该知道数据库是把数据存储在文件系统中。目前，数据库部署如下：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/postgres-1.svg" alt="postgres-1" width="600" height="400" loading="lazy"></p>
<p><code>postgres</code> 容器由 pod 封装，数据保留在容器内部的文件系统中。</p>
<p>现在，如果由于某种原因，容器崩溃或者封装容器的 pod 发生故障，则保存在文件系统的所有数据都将丢失。</p>
<p>崩溃后，Kubernetes 会创建一个新的 pod 来维持状态，但是两个 pod 之间没有任何数据转移机制。</p>
<p>为了解决这个问题，可以将数据存储在集群 pod 外部的单独空间中。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/volume.svg" alt="volume" width="600" height="400" loading="lazy"></p>
<p>和管理计算实例相比，管理此类存储面临的是另一些问题。Kubernetes 中的 <code>PersistentVolume</code> 子系统为用户和管理员提供了一个 API，该 API 从存储的使用方式中抽象出如何提供存储的细节。</p>
<h3 id="persistentvolumespersistentvolumeclaims">Persistent Volumes 和 Persistent Volume Claims</h3>
<p>摘自 Kubernetes  <a href="https://kubernetes.io/docs/concepts/storage/persistent-volumes/">文档</a>  —</p>
<blockquote>
<p>"持久卷（PersistentVolume，PV）是集群中的一块存储，可以由管理员事先供应，或者使用存储类（Storage Class）来动态供应。 持久卷是集群资源，就像节点也是集群资源一样。"</p>
</blockquote>
<p>实际上，<code>PersistentVolume</code> 是一种从存储空间获得切片并将其保留给特定 pod 的方法。Volumes 始终由 pod 占用，而不是像 deployment 这样的高级对象占用。</p>
<p>如果要在具有多个 pod 的 deployment 中使用 volume，必须要执行一些附加步骤。</p>
<p>在 <code>k8s</code> 目录中创建一个名为 <code>database-persistent-volume.yaml</code> 的新文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolume
metadata:
  name: database-persistent-volume
spec:
  storageClassName: manual
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/data"

</code></pre>
<p><code>apiVersion</code>、 <code>kind</code> 和  <code>metadata</code> 与其它配置文件里的用法一致，在 <code>spec</code> 字段里，有一些新字段：</p>
<ul>
<li><code>spec.storageClassName</code> 指示 volume 的名称。假设云提供商有三种可用的存储。<em>slow</em>、  <em>fast</em> 和  <em>very fast</em> 。从云存储上获取的存储方式取决于支付的金额。如果需要 very fast 存储，则需要支付更多的费用。这些不同类型的存储就是 classes。在本例里我使用 <code>manual</code>，在本地集群里可以使用任何你喜欢的选项。</li>
<li><code>spec.capacity.storage</code>  代表 volume 的存储大小。在此项目中设置了 5GB 的存储空间。</li>
<li><code>spec.accessModes</code>  设置卷的访问模式。一共有三种存储模式，<code>ReadWriteOnce</code>  代表该 volume  可以通过单个 node 以读写方式安装。<code>ReadWriteMany</code> 则代表该 volume  可以被多个 node 以读写的方式安装。<code>ReadOnlyMany</code> 意味着该 volume 可以被多个 node 以只读的方式安装。</li>
<li><code>spec.hostPath</code> 是特定于开发者的。它将本地单 node 集群的目录映射为 persistent volume。<code>/mnt/data</code> 意味着保存在持久卷（persistent volume）中的数据位于集群的 <code>/mnt/data</code>  文件夹内。</li>
</ul>
<p>执行下面的命令 apply 配置文件：</p>
<pre><code class="language-bash">kubectl apply -f database-persistent-volume.yaml

# persistentvolume/database-persistent-volume created
</code></pre>
<p>现在使用  <code>get</code>  命令确定 volume 创建成功：</p>
<pre><code class="language-bash">kubectl get persistentvolume

# NAME                         CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
# database-persistent-volume   5Gi        RWO            Retain           Available           manual                  58s
</code></pre>
<p>现在已经创建了 persistent volume，需要让 postgres  pod 访问它，可以通过<code>PersistentVolumeClaim</code>  (PVC) 。</p>
<p>persistent volume 声明是 pod 对存储的要求。假设在集群中有很多 volumes。该声明将定义必需满足 pod 的需求的 volume  的特征。</p>
<p>一个类似的的例子是从商店购买 SSD。销售员向你展示了如下模型：</p>
<table>
<thead>
<tr>
<th>MODEL 1</th>
<th>MODEL 2</th>
<th>MODEL 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>128GB</td>
<td>256GB</td>
<td>512GB</td>
</tr>
<tr>
<td>SATA</td>
<td>NVME</td>
<td>SATA</td>
</tr>
</tbody>
</table>
<p>现在你和销售员要求至少 200GB 的存储容量，并且驱动器的型号是 NVME。</p>
<p>MODEL1 是容量小于 200GB 的 SATA，与你的要求不符，MODEL 3 容量大于 200GB，但是不是 NVME 接口的。只有 MODEL2 是容量大于 200GB 并且接口是 NVME 的。正是需要的。</p>
<p>销售人员向你展示的 SSD models 就等同于 persistent volumes，你的要求就等同于 persistent volume 声明。</p>
<p>在 <code>k8s</code> 目录下创建一个名为 <code>database-persistent-volume-claim.yaml</code>  的新文件，文件内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: database-persistent-volume-claim
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
</code></pre>
<p><code>apiVersion</code>、 <code>kind</code> 和  <code>metadata</code> 和之前的作用一致。</p>
<ul>
<li><code>spec.storageClass</code>  表示存储类型的声明配置文件。 着意味着将任何设置为   <code>manual</code> 的 <code>spec.storageClass</code> 的  <code>PersistentVolume</code> 都适合此类声明。如果有多个设置为 <code>manual</code> 的 volumes ，那么将获得其中任意一个的声明。如果没有 class 为 <code>manual</code> 的 volume，那就动态配置一个。</li>
<li><code>spec.accessModes</code>  在此设置访问模式。这表明该声明希望使用具有 <code>ReadWriteOnce</code> 的  <code>accessMode</code>。假设有两个设置为  <code>manual</code> 的 volumes 。其中一个将其 <code>accessModes</code>  设置为 <code>ReadWriteOnce</code>，另一个设置为 <code>ReadWriteMany</code>。该声明将获取其中的 <code>ReadWriteOnce</code> 。</li>
<li><code>resources.requests.storage</code> 是此声明所需要的存储量 <code>2Gi</code> 并不意味着给定的卷必须恰好具有 2GB 的存储容量 。而是意味着它至少要有 2GB。你应该还记得之前将存储容量设置为 5GB，大于 2GB。</li>
</ul>
<p>执行下面的命令 apply 文件：</p>
<pre><code class="language-bash">kubectl apply -f database-persistent-volume-claim.yaml

# persistentvolumeclaim/database-persistent-volume-claim created
</code></pre>
<p>使用 <code>get</code> 命令确定 volume  已经成功创建：</p>
<pre><code class="language-bash">kubectl get persistentvolumeclaim

# NAME                               STATUS   VOLUME                       CAPACITY   ACCESS MODES   STORAGECLASS   AGE
# database-persistent-volume-claim   Bound    database-persistent-volume   5Gi        RWO            manual         37s
</code></pre>
<p>查看 <code>VOLUME</code> 列，这个声明与之前创建的 <code>database-persistent-volume</code>  持久卷绑定 ，在看一下 <code>CAPACITY</code>，它是 <code>5Gi</code>，因为该声明要求 volume  至少有 2GB 的存储容量。</p>
<h3 id="persistentvolumes">Persistent Volumes 的动态预配置</h3>
<p>在上一小节，你已经创建了一个 persistent volume，然后创建了一个声明，但是如果以前没有设置任何 persistent volume 该怎么办呢？</p>
<p>在这种情况下，将自动设置与声明兼容的持久卷。</p>
<p>开始之前，先执行如下命令删除之前创建的 persistent volume 和 persistent volume 声明：</p>
<pre><code class="language-yaml">kubectl delete persistentvolumeclaim --all

# persistentvolumeclaim "database-persistent-volume-claim" deleted

kubectl delete persistentvolumeclaim --all

# persistentvolume "database-persistent-volume" deleted

</code></pre>
<p>打开 <code>database-persistent-volume-claim.yaml</code> 文件将内容更新为如下内容：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: database-persistent-volume-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi

</code></pre>
<p>我已经从文件中删除了 <code>spec.storageClass</code> 字段，现在重新 apply <code>database-persistent-volume-claim.yaml</code> 文件（无需应用 <code>database-persistent-volume.yaml</code>  文件）：</p>
<pre><code class="language-yaml">kubectl apply -f database-persistent-volume-claim.yaml

# persistentvolumeclaim/database-persistent-volume-claim created
</code></pre>
<p>现在使用  <code>get</code>  命令查看声明信息：</p>
<pre><code class="language-yaml">kubectl get persistentvolumeclaim

# NAME                               STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
# database-persistent-volume-claim   Bound    pvc-525ae8af-00d3-4cc7-ae47-866aa13dffd5   2Gi        RWO            standard       2s
</code></pre>
<p>正如你看到的，已经提供了名为 <code>pvc-525ae8af-00d3-4cc7-ae47-866aa13dffd5</code> 且存储容量为 <code>2Gi</code> 的 volume，将其动态绑定到了声明。</p>
<p>该项目的剩余部分使用静态或者动态预配置 persistent volume  都可以。我会使用动态配置。</p>
<h3 id="podsvolumes">通过 Pods 连接 Volumes</h3>
<p>现在你已经创建了一个 persistent volume 和声明，是时候让数据库 pod 使用该 volume 了。</p>
<p>可以把之前小节创建的 persistent volume 声明连接到 pod 上。打开 <code>postgres-deployment.yaml</code> 文件，将内容更新如下：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      component: postgres
  template:
    metadata:
      labels:
        component: postgres
    spec:
      # volume configuration for the pod
      volumes:
        - name: postgres-storage
          persistentVolumeClaim:
            claimName: database-persistent-volume-claim
      containers:
        - name: postgres
          image: fhsinchy/notes-postgres
          ports:
            - containerPort: 5432
          # volume mounting configuration for the container
          volumeMounts:
            - name: postgres-storage
              mountPath: /var/lib/postgresql/data
              subPath: postgres
          env:
            - name: POSTGRES_PASSWORD
              value: 63eaQB9wtLqmNBpg
            - name: POSTGRES_DB
              value: notesdb

</code></pre>
<p>我在此配置文件中添加了两个字段。</p>
<ul>
<li><code>spec.volumes</code> 字段包含了供 pod 查找 persistent volume 申明的必要信息。  <code>spec.volumes.name</code>  可以是你想要的任何东西。<code>spec.volumes.persistentVolumeClaim.claimName</code> 必需与 <code>database-persistent-volume-claim.yaml</code> 文件中的 <code>metadata.name</code> 值相匹配。</li>
<li><code>containers.volumeMounts</code>  包含容器挂载的 volume  所必需的信息。<code>containers.volumeMounts.name</code> 必需与 <code>spec.volumes.name</code> 中的值相匹配。<code>containers.volumeMounts.mountPath</code> 代表 volume  挂载的目录。<code>/var/lib/postgresql/data</code> 是 PostgreSQL 的默认数据目录。<code>containers.volumeMounts.subPath</code> 表示将在 volume  创建的目录。假设你与其它的 pod 正在使用相同的 volume。保存在 <code>/var/lib/postgresql/data</code> 目录中的所有数据都将进入 volume 的  <code>postgres</code> 路径下。</li>
</ul>
<p>现在执行下面的命令重新 apply  <code>postgres-deployment.yaml</code> 文件：</p>
<pre><code class="language-yaml">kubectl apply -f postgres-deployment.yaml

# deployment.apps/postgres-deployment configured
</code></pre>
<p>现在，已经进行了正确的数据库部署，数据丢失的风险小了很多。</p>
<p>想要在这里提及的是，目前数据库部署中只有一个副本，如果有多个副本，那么情况会有所不同，</p>
<p>多个 pod 在不知道彼此存在情况下访问相同的 volume 会产生灾难性的后果 ，在 volume 内为 pod 创建子目录可以解决这个问题。</p>
<h3 id="">组装起来</h3>
<p>现在已经运行了 API 和数据库，是时候建立网络并做一些后续工作了。</p>
<p>在前面的章节中已经了解到 Kubernetes 中的网络设置，在开始编写服务之前，先看看我为项目制定的联网计划。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/notes-api-2.svg" alt="notes-api-2" width="600" height="400" loading="lazy"></p>
<ul>
<li>数据库只使用 <code>ClusterIP</code> service 在集群内暴露，不允许任何外部流量访问。</li>
<li>API 部署服务将暴露给外部世界，用户将与  API 通信，API 与数据库通信。</li>
</ul>
<p>之前通过 <code>LoadBalancer</code> service 将应用暴露给了外部世界，<code>ClusterIP</code> 则再集群中公开应用，并且不允许外部流量访问。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/cluster-ip-3.svg" alt="cluster-ip-3" width="600" height="400" loading="lazy"></p>
<p>鉴于数据库服务应仅在集群内可用 ，因此 <code>ClusterIP</code> service 服务非常适合此方案。</p>
<p>在 <code>k8s</code> 目录下创建一个名为  <code>postgres-cluster-ip-service.yaml</code> 的文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: postgres-cluster-ip-service
spec:
  type: ClusterIP
  selector:
    component: postgres
  ports:
    - port: 5432
      targetPort: 5432

</code></pre>
<p><code>ClusterIP</code> 的配置文件与 <code>LoadBalancer</code> 的配置文件差不多，唯一的不同的是  <code>spec.type</code>  。</p>
<p>现在，这个文件就清晰了。5432 是 PostgreSQL 运行的默认端口。所以也要集群内暴露 5432 。</p>
<p>接下来是 <code>LoadBalancer</code> service 的配置文件，负责将 API 暴露给外界。创建一个名为 <code>api-load-balancer-service.yaml</code> 的文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: api-load-balancer-service
spec:
  type: LoadBalancer
  ports:
    - port: 3000
      targetPort: 3000
  selector:
    component: api

</code></pre>
<p>此配置与上一节中的配置相同。API 运行在容器内的 3000 端口，所以也要在集群中暴露此端口。</p>
<p>最后要做的是要将环境变量添加到 API deployment 中。打开 <code>api-deployment.yaml</code> 文件并按照如下更新其内容：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: api
  template:
    metadata:
      labels:
        component: api
    spec:
      containers:
        - name: api
          image: fhsinchy/notes-api
          ports:
            - containerPort: 3000
          env:
            - name: DB_CONNECTION
              value: pg
            - name: DB_HOST
              value: postgres-cluster-ip-service
            - name: DB_PORT
              value: '5432'
            - name: DB_USER
              value: postgres
            - name: DB_DATABASE
              value: notesdb
            - name: DB_PASSWORD
              value: 63eaQB9wtLqmNBpg

</code></pre>
<p>之前，<code>spec.containers.env</code> 下面只有 <code>DB_CONNECTION</code> 变量，新涉及到的字段如下：</p>
<ul>
<li><code>DB_HOST</code>  表示数据库服务的地址。在非容器化环境中，该值通常为 <code>127.0.0.1</code>。但是在  Kubernetes 环境中，并不知道数据库容器的  IP 地址。因此只需使用公开的数据库 service 的名字即可 。</li>
<li><code>DB_PORT</code>  是数据库 service 公开的端口，即 5432。</li>
<li><code>DB_USER</code>  用于连接数据库的用户，默认的用户名是  <code>postgres</code>。</li>
<li><code>DB_DATABASE</code>  API 将要连接的数据库，必须与  <code>postgres-deployment.yaml</code> 文件中的 <code>spec.containers.env.DB_DATABASE</code> 值相同。</li>
<li><code>DB_PASSWORD</code>  用于连接数据库的密码，必须与 <code>postgres-deployment.yaml</code> 文件中的 <code>spec.containers.env.DB_PASSWORD</code> 值相匹配。</li>
</ul>
<p>完成此操作后就可以测试 API 了。在执行操作之前，执行一下下面的命令 apply 所有的配置文件：</p>
<pre><code class="language-bash">kubectl apply -f k8s

# deployment.apps/api-deployment created
# service/api-load-balancer-service created
# persistentvolumeclaim/database-persistent-volume-claim created
# service/postgres-cluster-ip-service created
# deployment.apps/postgres-deployment created

</code></pre>
<p>如果遇到任何错误，只需删除所有的资源并重新 apply 文件即可。service、persistent volumes、persistent volume 声明会立即创建。</p>
<p>用  <code>get</code>  命令确保所有的部署都已经启动且运行：</p>
<pre><code>kubectl get deployment

# NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
# api-deployment        3/3     3            3           106s
# postgres-deployment   1/1     1            1           106s
</code></pre>
<p>从 <code>READY</code> 列中可以看出，所有的 pod 都已经启动并且正在运行。执行  <code>minikube</code> 的    <code>service</code> 命令访问 API。</p>
<pre><code>minikube service api-load-balancer-service

# |-----------|---------------------------|-------------|-----------------------------|
# | NAMESPACE |           NAME            | TARGET PORT |             URL             |
# |-----------|---------------------------|-------------|-----------------------------|
# | default   | api-load-balancer-service |        3000 | http://172.19.186.112:31546 |
# |-----------|---------------------------|-------------|-----------------------------|
# * Opening service default/api-load-balancer-service in default browser...

</code></pre>
<p>API 会在默认浏览器里立即打开：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-93.png" alt="image-93" width="600" height="400" loading="lazy"></p>
<p>这时 API 的默认响应，还可以通过 <a href="https://insomnia.rest/">Insomnia</a>  或者  <a href="https://www.postman.com/">Postman</a>  来测试 <a href="http://172.19.186.112:31546/"><code>http://172.19.186.112:31546/</code></a> API 的完整  CRUD 功能。</p>
<p>可以将 API 源代码随附的测试作为文档查看。只需打开 <code>api/tests/e2e/api/routes/notes.test.js</code> 文件即可，如果你有 JavaScript 和<a href="https://expressjs.com/">express</a> 的经验，那么理解这个文件会很容易。</p>
<h2 id="ingresscontroller">使用 Ingress Controller</h2>
<p>目前为止，已经使用  <code>ClusterIP</code> 在集群内公开了的应用程序，使用 <code>LoadBalancer</code> 把应用暴露给集群外。</p>
<p>尽管我已经引用了  <code>LoadBalancer</code> 作为引用于集群外部公开应用程序的的标准 service，但是它还有一些缺点。</p>
<p>当使用 <code>LoadBalancer</code> services 在云环境中公开应用程序时，必需单独为每个公开的服务付费。这在大型项目下回非常昂贵。</p>
<p>还有另一种成为 <code>NodePort</code> 的 service，可以代替  <code>LoadBalancer</code>  service。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/node-port-2.svg" alt="node-port-2" width="600" height="400" loading="lazy"></p>
<p><code>NodePort</code>  在集群所有节点上打开一个特定的端口，并处理通过该打开端口的所有流量。</p>
<p>你知道，service 将多个 pod 组合在一起，并控制他们的访问方式。通过公开端口到达 service 的任何请求都将最终在正确的容器中。</p>
<p>用于创建  <code>NodePort</code> 的实例配置文件如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: hello-kube-node-port
spec:
  type: NodePort
  ports:
    - port: 8080
      targetPort: 8080
      nodePort: 31515
  selector:
    component: web
</code></pre>
<p>这里的  <code>spec.ports.nodePort</code> 字段值必须在 30000 和 32767 之间，此范围超出了各种服务通常所使用的常用端口。端口的数字位数很多。、</p>
<blockquote>
<p>尝试用 <code>NodePort</code> service 替换前面几节创建的 <code>LoadBalancer</code> service，这应该不难，算是对所学知识的简单测试。</p>
</blockquote>
<p>创建 <code>Ingress</code> 可以解决此问题，澄清一下，<code>Ingress</code> 不是一种 service，相反，它位于各个 service 前面，充当路由器的角色。</p>
<p>在集群中使用 <code>Ingress</code> 资源用到了 <code>IngressController</code>。可以在 Kubernetes  <a href="https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/">文档</a> 中找到可用 的 ingress  controllers 列表。</p>
<h3 id="nginxingresscontroller">设置 NGINX Ingress Controller</h3>
<p>在此例中，通过向其添加 front end 来扩展 notes API。使用 <code>Ingress</code> 来暴露应用，而不是使用诸如 <code>LoadBalancer</code> 或者 <code>NodePort</code> 之类的 service。</p>
<p>将要使用的而控制器是 <a href="https://github.com/kubernetes/ingress-nginx/blob/master/README.md">NGINX Ingress Controller</a>，在此 <a href="https://www.nginx.com/">NGINX</a>  将用于不同 service 请求的路由。NGINX Ingress Controller 使 Kubernetes  集群的 NGINX  配置变的更容易。</p>
<p>项目代码在 <code>fullstack-notes-application</code> 路径下：</p>
<pre><code class="language-bash">.
├── api
├── client
├── docker-compose.yaml
├── k8s
│   ├── api-deployment.yaml
│   ├── database-persistent-volume-claim.yaml
│   ├── postgres-cluster-ip-service.yaml
│   └── postgres-deployment.yaml
├── nginx
└── postgres

5 directories, 1 file
</code></pre>
<p>你会看到  <code>k8s</code> 目录，包含在上一个小节中除了 <code>api-load-balancer-service.yaml</code> 文件的所有的配置文件。</p>
<p>原因是，在该项目中，旧的 <code>LoadBalancer</code> service 将被 <code>Ingress</code> 代替。另外，无需公开 API，而是将前端应用程序公开即可。</p>
<p>在开始编写新文件之前，先看看架构。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/fullstack-1.svg" alt="fullstack-1" width="600" height="400" loading="lazy"></p>
<p>用户访问前端应用程序并提交必要的数据，然后前端应用程序将提交的数据转发到后端 API。</p>
<p>然后 API 将数据保留在数据库中，并将其发送回前端应用程序。然后使用 NGINX 实现请求的路由。</p>
<p>可以查看 <code>nginx/production.conf</code> 文件了解如何设置此路由。</p>
<p>现在实现目标所必需的网络如下：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/ingress.svg" alt="ingress" width="600" height="400" loading="lazy"></p>
<p>具体如下：</p>
<ul>
<li><code>Ingress</code>  将充当此应用程序的入口点和路由器，这是一个 <code>NGINX</code> 类型的 <code>Ingress</code>，因此端口是 nginx 的默认端口 80。</li>
<li>到 <code>/</code> 的每个请求都会被路由到前端应用（左侧的服务）处理。因此，如果应用程序的 URL  是 <code>https://kube-notes.test</code> ，那么所有的 <code>https://kube-notes.test/foo</code> 或者 <code>https://kube-notes.test/bar</code> 都会由前端应用程序处理。</li>
<li>到 <code>/api</code> 的每个请求都会被路由到后端的 API （右侧的服务）处理。因此，如果 URL 是 <code>https://kube-notes.test</code>，那么所有的 <code>https://kube-notes.test/api/foo</code> 或者  <code>https://kube-notes.test/api/bar</code> 都会由后端 API 处理。</li>
</ul>
<p>完全可以将 <code>Ingress</code> service  配置与子域名一起使用，而不是向这样的路径，这里的设计使用路径的方式。</p>
<p>在本小节中，必需编写四个新的配置文件：</p>
<ul>
<li><code>ClusterIP</code>  是 API deployment 的配置。</li>
<li><code>Deployment</code>  是 front-end 应用的配置。</li>
<li><code>ClusterIP</code>  是 front-end 应用的配置。</li>
<li><code>Ingress</code>  是路由的配置。</li>
</ul>
<p>前三个文件我会快速的过一下。</p>
<p>第一个文件是  <code>api-cluster-ip-service.yaml</code> 配置，内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: api-cluster-ip-service
spec:
  type: ClusterIP
  selector:
    component: api
  ports:
    - port: 3000
      targetPort: 3000
</code></pre>
<p>尽管在上一小节中，将 API 直接暴露给了外界，但在本小节中，<code>Ingress</code> 承担起了这个任务，同时使用 <code>ClusterIP</code>   在内部公开 API。</p>
<p>配置不言自明，无需过多解释。</p>
<p>接下来创建一个名为 <code>client-deployment.yaml</code> 的文件来运行前端应用，内容如下：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: client-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: client
  template:
    metadata:
      labels:
        component: client
    spec:
      containers:
        - name: client
          image: fhsinchy/notes-client
          ports:
            - containerPort: 8080
          env:
            - name: VUE_APP_API_URL
              value: /api
</code></pre>
<p>它几乎与 <code>api-deployment.yaml</code> 文件相同，很好理解。</p>
<p><code>VUE_APP_API_URL</code> 环境变量表示 API 请求应该转发的路径。这些转发请求将依次由<code>Ingress</code> 处理。</p>
<p>需要另一个 <code>ClusterIP</code>   service 来公开客户端应用程序。创建一个名为 <code>client-cluster-ip-service.yaml</code> 的新文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: client-cluster-ip-service
spec:
  type: ClusterIP
  selector:
    component: client
  ports:
    - port: 8080
      targetPort: 8080

</code></pre>
<p>描述的是运行在集群上默认暴露在 8080 端口上的前端应用。</p>
<p>在完成了旧配置之后，下一个配置是 <code>ingress-service.yaml</code> 文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress-service
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
    - http:
        paths:
          - path: /?(.)
            backend:
              serviceName: client-cluster-ip-service
              servicePort: 8080
          - path: /api/?(.)
            backend:
              serviceName: api-cluster-ip-service
              servicePort: 3000

</code></pre>
<p>该文件有一些新配置，也很好理解：</p>
<ul>
<li><code>Ingress</code>  API 仍处于测试阶段，所以 <code>apiVersion</code> 是 <code>extensions/v1beta1</code>。尽管处于 beta 版本，该 API 很稳定，可以直接在生产环境中使用。</li>
<li><code>kind</code>  和 <code>metadata.name</code> 字段和之前配置相同。</li>
<li><code>metadata.annotations</code>  可以包含有关 <code>Ingress</code>  配置的信息。<code>kubernetes.io/ingress.class: nginx</code> 表示 <code>Ingress</code> 对象应该由 <code>ingress-nginx</code> 控制器控制。<code>nginx.ingress.kubernetes.io/rewrite-target</code> 表示<a href="https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#rewrite">重写</a>目标 URL 的地方。</li>
<li><code>spec.rules.http.paths</code>  包含在 <code>nginx/production.conf</code> 文件中看到的各个路径的路由配置。 <code>paths.path</code>  表示路由的路径，<code>backend.serviceName</code> 是上述路径应该匹配的 service。<code>backend.servicePort</code> 是服务内部的端口。</li>
<li><code>/?(._)_</code> 和 <code>/api/?(.</code>_<code>)</code>  是简单的正则表达式，表示  <code>?(.*)</code>  部分会被路由到指定的服务。</li>
</ul>
<p>配置重写的方式会时不时发生变化，具体可以查看官方<a href="https://kubernetes.github.io/ingress-nginx/examples/rewrite/">文档</a>。</p>
<p>在 apply 新的配置之前，使用  <code>addons</code> 命令激活 <code>minikube</code> 的 <code>ingress</code> 插件，用法如下：</p>
<pre><code class="language-bash">minikube addons &lt;option&gt; &lt;addon name
</code></pre>
<p>执行如下命令激活 <code>ingress</code> 插件：</p>
<pre><code class="language-bash">minikube addons enable ingress

# ? Verifying ingress addon...
# ? The 'ingress' addon is enabled
</code></pre>
<p>可以对 <code>addon</code>  命令使用  <code>disable</code> 选项来禁用插件，查看官网<a href="https://minikube.sigs.k8s.io/docs/commands/addons/">文档</a>了解  <code>addon</code> 命令的更多信息。</p>
<p>插件激活后，可以 apply 配置文件，建议在 apply 新资源之前删除所有的资源（service、deployment 和 persistent volume claims）。</p>
<pre><code>kubectl delete ingress --all

# ingress.extensions "ingress-service" deleted

kubectl delete service --all

# service "api-cluster-ip-service" deleted
# service "client-cluster-ip-service" deleted
# service "kubernetes" deleted
# service "postgres-cluster-ip-service" deleted

kubectl delete deployment --all

# deployment.apps "api-deployment" deleted
# deployment.apps "client-deployment" deleted
# deployment.apps "postgres-deployment" deleted

kubectl delete persistentvolumeclaim --all

# persistentvolumeclaim "database-persistent-volume-claim" deleted

kubectl apply -f k8s

# service/api-cluster-ip-service created
# deployment.apps/api-deployment created
# service/client-cluster-ip-service created
# deployment.apps/client-deployment created
# persistentvolumeclaim/database-persistent-volume-claim created
# ingress.extensions/ingress-service created
# service/postgres-cluster-ip-service created
# deployment.apps/postgres-deployment created
</code></pre>
<p>使用  <code>get</code> 命令来确保所有的资源都已经创建成功。当全部运行后，可以通过 <code>minikube</code> 集群的 IP 地址访问该应用程序。执行如下命令获取 IP 地址：</p>
<pre><code class="language-bash">minikube ip

# 172.17.0.2
</code></pre>
<p>还可以通过运行 <code>Ingress</code> 来获取此 IP 的地址：</p>
<pre><code class="language-bash">kubectl get ingress

# NAME              CLASS    HOSTS   ADDRESS      PORTS   AGE
# ingress-service   &lt;none&gt;   *       172.17.0.2   80      2m33s
</code></pre>
<p>IP 和 端口分别在 <code>ADDRESS</code> 和 <code>PORTS</code> 端口列下。访问 <code>127.17.0.2:80</code>，可以直接进入 notes 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-84.png" alt="image-84" width="600" height="400" loading="lazy"></p>
<p>可以在此应用中执行简单的 CRUD 操作，端口 80 是 NGINX 的默认端口，因此可以省略 URL 中的端口号。</p>
<p>如果你了解如何配置 NGINX，可以使用  ingress controller 执行很多操作。毕竟，这就是控制器的用途 - 将 NGINX 的配置存储在 Kubernetes  的 <code>ConfigMap</code> 上，将会在下一部分中学习。</p>
<h3 id="kubernetessecret">Kubernetes 中的 Secret 和配置</h3>
<p>目前为止，部署中使用纯文本形式存储了敏感信息，如 <code>POSTGRES_PASSWORD</code>，这并不是最佳实践。</p>
<p>可以用 <code>Secret</code> 将值存储在集群中，这是存储密码、token 等的更安全的方法。</p>
<blockquote>
<p>在 Windows 命令行中，下一步可能无法正常工作，可以使用 <a href="https://git-scm.com/">git</a> 终端或者 <a href="https://cmder.net/">cmder</a> 完成此任务。</p>
</blockquote>
<p>需要将数据转换成 base64 数据才能将信息存储在 <code>Secret</code> 中。如果纯文本密码为  <code>63eaQB9wtLqmNBpg</code> ，执行以下命令获取 base64 版本。</p>
<pre><code class="language-bash">echo -n "63eaQB9wtLqmNBpg" | base64

# NjNlYVFCOXd0THFtTkJwZw==
</code></pre>
<p>此步骤是必要的，参数必需是 base64 格式的，现在在  <code>k8s</code>   目录下创建一个  <code>postgres-secret.yaml</code> 文件，内容如下：</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
data:
  password: NjNlYVFCOXd0THFtTkJwZw==
</code></pre>
<p><code>apiVersion</code>、<code>kind</code> 和  <code>metadata</code> 的意义无需解释，<code>data</code> 字段就是真实的密文。</p>
<p>如上，创建了一个键值对，键是 <code>password</code>， 值是 <code>NjNlYVFCOXd0THFtTkJwZw==</code>。将使用 <code>metadata.name</code> 值在其他配置文件中作为获取密码值的 <code>Secret</code> 的标识。</p>
<p>按如下更新 <code>postgres-deployment.yaml</code> 文件，以在数据库配置中使用此密码：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      component: postgres
  template:
    metadata:
      labels:
        component: postgres
    spec:
      volumes:
        - name: postgres-storage
          persistentVolumeClaim:
            claimName: database-persistent-volume-claim
      containers:
        - name: postgres
          image: fhsinchy/notes-postgres
          ports:
            - containerPort: 5432
          volumeMounts:
            - name: postgres-storage
              mountPath: /var/lib/postgresql/data
              subPath: postgres
          env:
              # not putting the password directly anymore
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password
            - name: POSTGRES_DB
              value: notesdb

</code></pre>
<p>如上，除了 <code>spec.template.spec.continers.env</code>  字段外，所有的字段介绍过。</p>
<p>之前用于存储密码的 <code>name</code> 环境变量是纯文本。但是现在是 <code>valueFrom.secretKeyRef</code> 字段。</p>
<p>这里的 <code>name</code>  字段是指刚刚创建的 <code>Secret</code> 的名字，<code>key</code> 值是指 <code>Secret</code> 配置文件键值对中的键。Kubernetes  将在内部将编码后的值解码为纯文本。</p>
<p>除了数据库配置，你还需按如下所示更新 <code>api-deployment.yaml</code> 文件：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: api
  template:
    metadata:
      labels:
        component: api
    spec:
      containers:
        - name: api
          image: fhsinchy/notes-api
          ports:
            - containerPort: 3000
          env:
            - name: DB_CONNECTION
              value: pg
            - name: DB_HOST
              value: postgres-cluster-ip-service
            - name: DB_PORT
              value: '5432'
            - name: DB_USER
              value: postgres
            - name: DB_DATABASE
              value: notesdb
              # not putting the password directly anymore
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password

</code></pre>
<p>现在执行下面的命令 apply  这些新的配置文件：</p>
<pre><code class="language-bash">kubectl apply -f k8s

# service/api-cluster-ip-service created
# deployment.apps/api-deployment created
# service/client-cluster-ip-service created
# deployment.apps/client-deployment created
# persistentvolumeclaim/database-persistent-volume-claim created
# secret/postgres-secret created
# ingress.extensions/ingress-service created
# service/postgres-cluster-ip-service created
# deployment.apps/postgres-deployment created
</code></pre>
<p>取决于集群的状态不同，输出可能会不同。</p>
<blockquote>
<p>谨慎起见，先删除所有的资源然后在 apply 配置文件来创建他们。</p>
</blockquote>
<p>使用 <code>get</code>  命令检查并确保所有的 pod 都已经启动并且正在运行。</p>
<p>使用 <code>minikube</code>  IP 访问 notes 应用程序并尝试创建新的 notes， 来测试新的配置。</p>
<pre><code class="language-bash">minikube ip

# 172.17.0.2
</code></pre>
<p>访问  <code>127.17.0.2:80</code>，你应该会直接进入 Notes 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-92.png" alt="image-92" width="600" height="400" loading="lazy"></p>
<p>还有一种无需任何配置文件即可创建 secret  的方法，执行如下命令，使用 <code>kubectl</code> 创建相同的 <code>Secret</code>。</p>
<pre><code class="language-bash">kubectl create secret generic postgres-secret --from-literal=password=63eaQB9wtLqmNBpg

# secret/postgres-secret created
</code></pre>
<p>这是一种更方便的方法，因为可以跳过整个 base64 编码步骤。在这种情况下，secret 会被自动编码。</p>
<p><code>ConfigMap</code> 和  <code>Secret</code> 类似，一般用于非隐私的的信息。</p>
<p>在  <code>k8s</code>  目录下创建一个名为 <code>api-config-map.yaml</code> 的文件，把 API deployment 里所有的其余的环境变量放在  <code>ConfigMap</code> 里：</p>
<pre><code class="language-yaml">apiVersion: v1 
kind: ConfigMap 
metadata:
  name: api-config-map 
data:
  DB_CONNECTION: pg
  DB_HOST: postgres-cluster-ip-service
  DB_PORT: '5432'
  DB_USER: postgres
  DB_DATABASE: notesdb

</code></pre>
<p><code>apiVersion</code>、  <code>kind</code>  和 <code>metadata</code> 无需解释。<code>data</code> 字段是以键值对形式的环境变量。</p>
<p>和 <code>Secret</code> 不同，此处的 key 必需与 API 所需的 key 匹配。因此，我从 <code>api-deployment.yaml</code> 文件中复制了一些变量，并稍作修改后粘贴到了此处。</p>
<p>要在 API deployment 中使用 secret，打开 <code>api-deployment.yaml</code>  文件并做如下修改：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: api
  template:
    metadata:
      labels:
        component: api
    spec:
      containers:
        - name: api
          image: fhsinchy/notes-api
          ports:
            - containerPort: 3000
          # not putting environment variables directly
          envFrom:
            - configMapRef:
                name: api-config-map
          env:
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: postgres-secret
                  key: password

</code></pre>
<p>文件除了 <code>spec.template.spec.containers.env</code>  字段外几乎没有改变。</p>
<p>我已经将环境变量移到了 <code>ConfigMap</code> 中。<code>spec.template.spec.containers.envFrom</code>  用来从 <code>ConfigMap</code> 中获取数据。<code>configMapRef.name</code> 表示将从中 提取环境变量的 <code>ConfigMap</code>  。</p>
<p>然后执行下面的命令 apply 所有的配置：</p>
<pre><code class="language-bash">kubectl apply -f k8s

# service/api-cluster-ip-service created
# configmap/api-config-map created
# deployment.apps/api-deployment created
# service/client-cluster-ip-service created
# deployment.apps/client-deployment created
# persistentvolumeclaim/database-persistent-volume-claim created
# ingress.extensions/ingress-service configured
# service/postgres-cluster-ip-service created
# deployment.apps/postgres-deployment created
# secret/postgres-secret created
</code></pre>
<p>取决于集群的状态，输出可能会不同。</p>
<blockquote>
<p>谨慎起见，先删除所有的 Kubernetes  资源然后在 apply configs 创建他们。</p>
</blockquote>
<p>使用 <code>get</code> 命令确保 pod 已经启动并运行，使用 <code>minikube</code> IP 访问 notes 应用并尝试创建新的 note。</p>
<p>执行下面的命令获取 IP 地址。</p>
<pre><code class="language-bash">minikube ip

# 172.17.0.2
</code></pre>
<p>访问  <code>127.17.0.2:80</code> ，直接进入 notes 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-92.png" alt="image-92" width="600" height="400" loading="lazy"></p>
<p><code>Secret</code>  和 <code>ConfigMap</code> 还有其它一些技巧，就不在这里展开了，如果想了解，可以查看官方<a href="https://kubectl.docs.kubernetes.io/pages/app_management/secrets_and_configmaps.html">文档</a>。</p>
<h3 id="kubernetes">在 Kubernetes 中执行更新发布</h3>
<p>既然已经在 Kubernetes 上成功部署了一个包含多个容器的应用程序，是时候学习执行更新了。</p>
<p>Kubernetes 很神奇，将容器更新为较新版本的镜像比较麻烦，有很多种方式更新容器，这里不会涉及到所有的方法。</p>
<p>相反，我将直接进入更新容器时主要采取的方法。如果打开  <code>client-deployment.yaml</code> 文件并查看 <code>spec.template.spec.containers</code> 字段，会看到下面的配置：</p>
<pre><code class="language-yaml">containers:
    - name: client
      image: fhsinchy/notes-client
</code></pre>
<p>如上，在  <code>image</code>  字段中，没有使用任何镜像标签。现在，如果你认为在镜像尾部添加  <code>:latest</code>  将确保部署始终拉取最新的镜像，那你可就大错特错了。</p>
<p>我通常采用最简单的路径。之前提到过，在某些情况下，使用命令式而不是声明式是一个好主意，创建一个 <code>Secret</code> 或者更新容器就是这种情况。</p>
<p>可以用来执行更新的命令是 <code>set</code>  命令，其通用语法如下：</p>
<pre><code class="language-bash">kubectl set image &lt;resource type&gt;/&lt;resource name&gt; &lt;container name&gt;=&lt;image name with tag&gt;
</code></pre>
<p>资源类型为 <code>deployment</code>，资源名称为 <code>client-deployment</code>。可以在 <code>client-deployment.yaml</code> 文件内的 <code>containers</code> 字段找到容器的名称，本例中为  <code>client</code>  。</p>
<p>我已经构建了带有标签  <code>edge</code> 的 <code>fhsinchy/notes-client</code> 镜像，将使用它来更新 <code>fhsinchy/notes-client</code> 的版本。</p>
<p>最终命令如下：</p>
<pre><code class="language-bash">kubectl set image deployment/client-deployment client=fhsinchy/notes-client:edge

# deployment.apps/client-deployment image updated
</code></pre>
<p>由于 Kubernetes 将重新创建所有的 pod，执行此更新可能需要一段时间，可以运行 <code>get</code> 命令来了解是否所有的 pod 都已经启动并且成功运行。</p>
<p>重新创建后，使用 <code>minikube</code>  IP 访问 notes 应用并尝试创建新的 notes。可以执行下面的命令获取 IP：</p>
<pre><code class="language-bash">minikube ip

# 172.17.0.2
</code></pre>
<p>通过访问 <code>127.17.0.2:80</code> 应该可以直接进入 notes 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/image-92.png" alt="image-92" width="600" height="400" loading="lazy"></p>
<p>鉴于我还未对应用程序代码进行任何实际更改，因此所有的内容都将保持不变。你可以使用  <code>describe</code> 命令来确保 pod 正在使用新的镜像。</p>
<pre><code class="language-bash">kubectl describe pod client-deployment-849bc58bcc-gz26b | grep 'Image'

# Image:          fhsinchy/notes-client:edge
# Image ID:       docker-pullable://fhsinchy/notes-client@sha256:58bce38c16376df0f6d1320554a56df772e30a568d251b007506fd3b5eb8d7c2
</code></pre>
<p><code>grep</code> 命令在 Mac 和 Linux 可以直接使用，如果你使用的是 Windows，使用 git bash 而不是 Windows 命令行。</p>
<p>尽管强制性更新过程有些繁琐，但是通过好的 CI/CD 流程可以使其变得更加容易。</p>
<h3 id="configurations">组合 Configurations</h3>
<p>尽管其中只有三个容器，但该项目中的配置文件数量已经非常庞大了。</p>
<p>实际上可以按照如下方式组合配置文件：</p>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: client-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      component: client
  template:
    metadata:
      labels:
        component: client
    spec:
      containers:
        - name: client
          image: fhsinchy/notes-client
          ports:
            - containerPort: 8080
          env:
            - name: VUE_APP_API_URL
              value: /api
              
---

apiVersion: v1
kind: Service
metadata:
  name: client-cluster-ip-service
spec:
  type: ClusterIP
  selector:
    component: client
  ports:
    - port: 8080
      targetPort: 8080
</code></pre>
<p>如上，我已经使用界定符（<code>---</code>）组合了 <code>client-deployment.yaml</code> 和 <code>client-cluster-ip-service.yaml</code> 文件。尽管有可能在容器数量很多的项目中减少文件，但我还是建议将他们分开，更简洁、更干净。</p>
<h2 id="">答疑</h2>
<p>在本节中，我将列出你使用 Kubernetes 时可能遇到的一些常见问题。</p>
<ul>
<li>如果你在 Windows 或者 Mac 使用 Docker 的 <code>minikube</code>，<code>Ingress</code>  插件可能并没有生效。、</li>
<li>如果你在 Mac 上运行了<a href="https://laravel.com/docs/7.x/valet">Laravel Valet</a>，并且将 HyperKit 驱动程序用于<code>minikube</code>，会联网失败。关闭 <code>minikube</code> 服务可以解决此问题。</li>
<li>如果你有一台 Ryzen (mine is R5 1600) PC，并且正在运行 Windows 10，由于缺少内嵌虚拟化支持 VirtualBox  可能会启动失败。必须在 Windows 10 （Pro、Enterprise 和 Education）上安装 Hyper-V 驱动程序，对于家庭版，很遗憾没有该选项。</li>
<li>如果你在 Windows 10 (Pro, Enterprise 和 Education)  上使用用于 <code>minikube</code> 的 Hyper-V  驱动，VM 可能会启动失败，并显示 内存不足的消息。不要紧张，执行 <code>minikube start</code> 重新启动 VM。</li>
<li>如果你在 Windows 命令行中看到本文执行的某些命令丢失，或者功能异常，请改用  <a href="https://git-scm.com/">git</a> 命令行或者  <a href="https://cmder.net/">cmder</a> 。</li>
</ul>
<p>我建议在你的系统上安装一个 Linux 发行版，并将 Docker 驱动程序用于 <code>minikube</code>。目前为止，这是最快也是最可靠的设置。</p>
<h2 id="">结论</h2>
<p>衷心感谢你花了这么长时间阅读本文，希望你享受学习过程，并了解了 Kubernetes 的基础知识。</p>
<p>你可以关注我的推特   <a href="https://twitter.com/frhnhsin">@frhnhsin</a>  或者在 LinkedIn <a href="https://www.linkedin.com/in/farhanhasin/">/in/farhanhasin</a>  上与我联系。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/the-kubernetes-handbook/">The Kubernetes Handbook</a>，作者：<a href="https://www.freecodecamp.org/news/author/farhanhasin/">Farhan Hasin Chowdhury</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Linux LS 命令——如何列出目录中的文件 + 选项标志 ]]>
                </title>
                <description>
                    <![CDATA[ 自 1970 年代 Unix 发明以来，许多操作系统都以它为基础。这些操作系统中有许多失败了，也有一些大获成功。 Linux 是最流行的基于 Unix 的操作系统之一。它是开源的，并在世界各地的许多行业中广泛应用。 Linux 操作系统的一项重要功能是命令行界面（CLI），它允许用户通过 Shell 与计算机进行交互。Linux Shell 是一个 REPL（Read，E valuate，Print，Loop）环境，用户可以在其中输入命令，然后 Shell 执行该命令并返回一个结果。 ls  命令是可以从 CLI 列出文件或目录的众多 Linux 命令之一。 在本文中，我们将深入探讨 ls  命令以及常用的一些选项。 先决条件  * 具有目录和文件的计算机  * 安装一个 Linux 发行版  * 在 CLI 上导航的基本知识  * 你脸上的笑容 :) Linux ls ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-linux-ls-command-how-to-list-files-in-a-directory-with-option/</link>
                <guid isPermaLink="false">606adc4cb3b14e058ee049f2</guid>
                
                    <category>
                        <![CDATA[ Linux ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Mon, 05 Apr 2021 09:40:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/article-banner-7.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>自 1970 年代 Unix 发明以来，许多操作系统都以它为基础。这些操作系统中有许多失败了，也有一些大获成功。</p>
<p>Linux 是最流行的基于 Unix 的操作系统之一。它是开源的，并在世界各地的许多行业中广泛应用。</p>
<p>Linux 操作系统的一项重要功能是命令行界面（CLI），它允许用户通过 Shell 与计算机进行交互。Linux Shell 是一个 REPL（<strong>R</strong>ead，<strong>E</strong>valuate，<strong>P</strong>rint，<strong>L</strong>oop）环境，用户可以在其中输入命令，然后 Shell 执行该命令并返回一个结果。</p>
<p><code>ls</code> 命令是可以从 CLI 列出文件或目录的众多 Linux 命令之一。</p>
<p>在本文中，我们将深入探讨 <code>ls</code> 命令以及常用的一些选项。</p>
<h2 id="">先决条件</h2>
<ul>
<li>具有目录和文件的计算机</li>
<li>安装一个 Linux 发行版</li>
<li>在 CLI 上导航的基本知识</li>
<li>你脸上的笑容 :)</li>
</ul>
<h2 id="linuxls">Linux ls 命令</h2>
<p><code>ls</code> 命令用于在 Linux 和其他基于 Unix 的操作系统中列出文件或目录。</p>
<p>就像使用 GUI 在<em>文件浏览器</em>或<em>文件夹</em>中导航一样，默认情况下，<code>ls</code> 命令可以列出当前目录中的所有文件或目录，并通过命令行进一步操作它们。</p>
<p>启动终端并输入 <code>ls</code> 以查看实际效果：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-20-at-9.40.29-PM.png" alt="Screenshot-2020-08-20-at-9.40.29-PM" width="600" height="400" loading="lazy"></p>
<h2 id="">如何使用选项列出目录中的文件</h2>
<p><code>ls</code> 命令还接受一些标志（也称为选项），这些标志是更改终端中文件或目录的展示方式的附加信息。</p>
<p>换句话说，标志改变了 <code>ls</code> 命令的工作方式：</p>
<pre><code> ls [flags] [directory]
</code></pre>
<blockquote>
<p>PS：整篇文章中使用的<strong>内容</strong>一词是指列出的<strong>文件和目录</strong>，而不是文件/目录的实际内容。</p>
</blockquote>
<h3 id="">列出当前工作目录中的文件</h3>
<p>输入 <code>ls</code> 命令以列出当前工作目录的内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-20-at-9.40.29-PM.png" alt="Screenshot-2020-08-20-at-9.40.29-PM" width="600" height="400" loading="lazy"></p>
<h3 id="">列出另一个目录中的文件</h3>
<p>键入 <code>ls [directory path here]</code> 命令以列出另一个目录的内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-20-at-10.32.52-PM.png" alt="Screenshot-2020-08-20-at-10.32.52-PM" width="600" height="400" loading="lazy"></p>
<h3 id="">列出根目录中的文件</h3>
<p>输入 <code>ls /</code> 命令以列出根目录的内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-20-at-10.46.10-PM.png" alt="Screenshot-2020-08-20-at-10.46.10-PM" width="600" height="400" loading="lazy"></p>
<h3 id="">列出父目录中的文件</h3>
<p>键入<code>ls ..</code> 命令以列出上一级父目录的内容。 使用 <code>ls ../..</code> 表示列出父目录的父目录的内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-20-at-10.48.22-PM.png" alt="Screenshot-2020-08-20-at-10.48.22-PM" width="600" height="400" loading="lazy"></p>
<h3 id="homeuser">列出用户主目录（/home/user）中的文件</h3>
<p>键入<code>ls ~</code> 命令以列出用户主目录中的内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-20-at-10.51.19-PM.png" alt="Screenshot-2020-08-20-at-10.51.19-PM" width="600" height="400" loading="lazy"></p>
<h3 id="">仅列出目录</h3>
<p>输入 <code>ls -d * /</code> 命令仅列出目录：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-21-at-12.53.05-PM.png" alt="Screenshot-2020-08-21-at-12.53.05-PM" width="600" height="400" loading="lazy"></p>
<h3 id="">列出带有子目录的文件</h3>
<p>键入  <code>ls *</code>  命令以列出目录及其子目录的内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-21-at-1.07.54-PM.png" alt="Screenshot-2020-08-21-at-1.07.54-PM" width="600" height="400" loading="lazy"></p>
<h3 id="">递归列出文件</h3>
<p>输入 <code>ls -R</code> 命令以列出所有文件和目录并逐级展示子目录内的目录和文件：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/09/Screenshot-2020-09-01-at-9.04.56-AM.png" alt="Screenshot-2020-09-01-at-9.04.56-AM" width="600" height="400" loading="lazy"></p>
<blockquote>
<p>如果有很多文件，这可能需要很长时间才能完成，因为将打印出每个目录中的每个文件。可以改为指定一个目录来运行此命令，如：<code>ls Downloads -R</code></p>
</blockquote>
<h3 id="">列出文件的大小</h3>
<p>键入<code>ls -s</code> 命令（<strong>s</strong> 为小写字母）以列出文件或目录的大小：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-21-at-12.30.19-PM.png" alt="Screenshot-2020-08-21-at-12.30.19-PM" width="600" height="400" loading="lazy"></p>
<h3 id="">以长格式列出文件</h3>
<p>键入<code>ls -l</code> 命令以表格格式列出目录的内容，其列包括：</p>
<ul>
<li>内容权限</li>
<li>内容链接数</li>
<li>内容的所有者</li>
<li>内容组的所有者</li>
<li>内容大小（以字节为单位）</li>
<li>内容的最后修改日期/时间</li>
<li>文件或目录名称</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-20-at-10.52.37-PM.png" alt="Screenshot-2020-08-20-at-10.52.37-PM" width="600" height="400" loading="lazy"></p>
<h3 id="">以可读的文件大小长格式列出文件</h3>
<p>键入<code>ls -lh</code> 命令以上面相同的表格格式列出文件或目录，但用另一列表示每个文件/目录的大小：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-21-at-12.14.33-PM.png" alt="Screenshot-2020-08-21-at-12.14.33-PM" width="600" height="400" loading="lazy"></p>
<p>请注意，当文件或目录的大小大于 1024 字节时，大小以字节（B）、兆字节（MB）、千兆字节（GB）或 TB（TB）列出。</p>
<h3 id="">列出文件，包括隐藏文件</h3>
<p>键入 <code>ls -a</code>  命令以列出包括隐藏的文件或目录。在 Linux 中，任何以 <code>.</code> 开头的文件都被视为隐藏文件：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-21-at-11.12.26-AM.png" alt="Screenshot-2020-08-21-at-11.12.26-AM" width="600" height="400" loading="lazy"></p>
<h3 id="">以长格式列出包括隐藏文件的文件</h3>
<p>输入 <code>ls -l -a</code> 或 <code>ls -a -l</code> 或  <code>ls -la</code> 或 <code>ls -al</code> 命令以表格格式列出文件或目录，并提供包括隐藏文件或目录在内的更多信息：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-21-at-12.17.01-PM.png" alt="Screenshot-2020-08-21-at-12.17.01-PM" width="600" height="400" loading="lazy"></p>
<h3 id="">列出文件并按日期和时间排序</h3>
<p>键入<code>ls -t</code> 命令以列出文件或目录，并按降序（从最大到最小）按上次修改的日期和时间排序。</p>
<p>还可以添加 <code>-r</code> 标志来反转排序顺序，如下所示：<code>ls -tr</code>：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-21-at-12.20.09-PM.png" alt="Screenshot-2020-08-21-at-12.20.09-PM" width="600" height="400" loading="lazy"></p>
<h3 id="">列出文件并按文件大小排序</h3>
<p>键入<code>ls -S</code>（<strong>S</strong> 为大写）命令以列出文件或目录，并按日期或时间降序排列（从大到小）。</p>
<p>还可以添加 <code>-r</code> 标志来反转排序顺序，如下所示：<code>ls -Sr</code>：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/08/Screenshot-2020-08-21-at-12.20.38-PM.png" alt="Screenshot-2020-08-21-at-12.20.38-PM" width="600" height="400" loading="lazy"></p>
<h3 id="">列出文件并将结果输出到文件</h3>
<p>输入<code>ls&gt; output.txt</code> 命令，将前一个命令的输出打印到 <code>output.txt</code> 文件中。可以使用前面讨论过的任何标志，例如 <code>-la</code> －这里的关键是结果将输出到文件中而不记录到命令行中。</p>
<p>然后，可以根据需要使用该文件，也可以使用 <code>cat output.txt</code> 展示该文件的内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/09/Screenshot-2020-09-01-at-9.12.59-AM.png" alt="Screenshot-2020-09-01-at-9.12.59-AM" width="600" height="400" loading="lazy"></p>
<h1 id="">总结</h1>
<p>还有很多命令可以和 <code>ls</code> 组合使用，以根据需要列出文件和目录。 要记住，可以一次将多个命令组合在一起。</p>
<p>假设要以长格式列出文件（包括隐藏文件），然后按文件大小排序。 命令是 <code>ls -alS</code>，这是 <code>ls -l</code>，<code>ls -a</code> 和 <code>ls -S</code> 的组合。</p>
<p>如果忘记了任何命令或不确定该怎么做，则可以运行 <code>ls --help</code> 或 <code>man ls</code>，这将显示一本手册，包含了 <code>ls</code> 命令的所有选项：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/09/Screenshot-2020-09-01-at-9.57.37-AM.png" alt="Screenshot-2020-09-01-at-9.57.37-AM" width="600" height="400" loading="lazy"></p>
<p>感谢阅读！</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/the-linux-ls-command-how-to-list-files-in-a-directory-with-options/">The Linux LS Command – How to List Files in a Directory + Option Flags</a>，作者：<a href="https://www.freecodecamp.org/news/author/bolajiayodeji/">Bolaji Ayodeji</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Docker 完全手册 ]]>
                </title>
                <description>
                    <![CDATA[ 容器化的概念很早就有了。2013 年 Docker 引擎 [https://docs.docker.com/get-started/overview/#docker-engine]的出现使应用程序容器化变得更加容易。 根据 Stack Overflow 开发者调查-2020 [https://insights.stackoverflow.com/survey/2020#overview]，Docker [https://docker.com/] 是开发者 #1 最想要的平台 [https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-wanted5] 、#2 最喜欢的平台 [https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-loved5] ，以及#3 最流行的平台 [https://insights.st ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-docker-handbook/</link>
                <guid isPermaLink="false">6066ec5a4c5a5f056433056e</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Fri, 02 Apr 2021 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/docker-1280x612-2021.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>容器化的概念很早就有了。2013 年 <a href="https://docs.docker.com/get-started/overview/#docker-engine">Docker 引擎</a>的出现使应用程序容器化变得更加容易。</p>
<p>根据 <a href="https://insights.stackoverflow.com/survey/2020#overview">Stack Overflow 开发者调查-2020</a>，<a href="https://docker.com/">Docker</a> 是开发者 <a href="https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-wanted5">#1 最想要的平台</a>、<a href="https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-platforms-loved5">#2 最喜欢的平台</a>，以及<a href="https://insights.stackoverflow.com/survey/2020#technology-platforms">#3 最流行的平台</a>。</p>
<p>尽管 Docker 功能强大，但上手确并不容易。因此，本书将介绍从基础知识到更高层次容器化的的所有内容。读完整本书之后，你应该能够：</p>
<ul>
<li>容器化（几乎）任何应用程序</li>
<li>将自定义 Docker 镜像上传到在线仓库</li>
<li>使用 Docker Compose 处理多个容器</li>
</ul>
<h2 id="">前提</h2>
<ul>
<li>熟悉 Linux 终端操作</li>
<li>熟悉 JavaScript（稍后的的演示项目用到了 JavaScript）</li>
</ul>
<h2 id="">目录</h2>
<ul>
<li>容器化和 Docker 简介</li>
<li>怎样安装 Docker
<ul>
<li>怎样在 macOS 里安装 Docker</li>
<li>怎样在 Windows 上安装 Docker</li>
<li>怎样在 Linux 上安装 Docker</li>
</ul>
</li>
<li>初识 Docker - Docker 基本知识介绍
<ul>
<li>什么是容器？</li>
<li>什么是 Docker 镜像？</li>
<li>什么是仓库？</li>
<li>Docker 架构概述</li>
<li>全景图</li>
</ul>
</li>
<li>Docker 容器操作基础知识
<ul>
<li>怎样运行容器</li>
<li>怎样公开端口</li>
<li>如何使用分离模式</li>
<li>怎样列表展示容器</li>
<li>怎样命名或者重命名一个容器</li>
<li>怎样停止或者杀死运行中的容器</li>
<li>怎样重新启动容器</li>
<li>怎样创建而不运行容器</li>
<li>怎样移除挂起的容器</li>
<li>怎样以交互式模式运行容器</li>
<li>怎样在容器里执行命令</li>
<li>如何处理可执行镜像</li>
</ul>
</li>
<li>Docker 镜像操作基础知识
<ul>
<li>如何创建 Docker 镜像</li>
<li>如何标记 Docker 镜像</li>
<li>如何删除、列表展示镜像</li>
<li>理解 Docker 镜像的分层</li>
<li>怎样从源码构建 NGINX</li>
<li>怎样优化 Docker 镜像</li>
<li>拥抱 Alpine Linux</li>
<li>怎样创建可执行 Docker 镜像</li>
<li>怎样在线共享 Docker 镜像</li>
</ul>
</li>
<li>怎样容器化 JavaScript 应用
<ul>
<li>如何编写开发 Dockerfile</li>
<li>如何在 Docker 中使用绑定挂载</li>
<li>如何在 Docker 中使用匿名卷</li>
<li>如何在 Docker 中执行多阶段构建</li>
<li>如何忽略不必要的文件</li>
</ul>
</li>
<li>Docker 中的网络操作基础知识
<ul>
<li>Docker 网络基础</li>
<li>如何在 Docker 中创建用户定义的桥接网络</li>
<li>如何在 Docker 中将容器连接到网络</li>
<li>如何在 Docker 中从网络分离容器</li>
<li>如何删除 Docker 中的网络</li>
</ul>
</li>
<li>如何容器化多容器 JavaScript 应用程序
<ul>
<li>如何运行数据库服务</li>
<li>如何在 Docker 中使用命名卷</li>
<li>如何从 Docker 中的容器访问日志</li>
<li>如何在 Docker 中创建网络并连接数据库服务</li>
<li>如何编写 Dockerfile</li>
<li>如何在正在运行的容器中执行命令</li>
<li>如何在 Docker 中编写管理脚本</li>
</ul>
</li>
<li>如何使用 Docker-Compose 组合项目
<ul>
<li>Docker Compose 基础</li>
<li>如何在 Docker Compose 中启动服务</li>
<li>如何在 Docker Compose 中列表展示服务</li>
<li>如何在 Docker Compose 正在运行的服务中执行命令</li>
<li>如何访问 Docker Compose 中正在运行的服务日志</li>
<li>如何在 Docker Compose 中停止服务</li>
<li>如何在 Docker Compose 中编写全栈应用程序</li>
</ul>
</li>
<li>结论</li>
</ul>
<h2 id="">项目代码</h2>
<p>可以在<a href="https://github.com/fhsinchy/docker-handbook-projects/">这个仓库</a>中找到示例项目的代码，欢迎 ⭐。</p>
<p>完整代码在 <a href="https://github.com/fhsinchy/docker-handbook-projects/tree/containerized"><code>containerized</code></a> 分支。</p>
<h2 id="">贡献</h2>
<p>这本书是完全开源的，欢迎高质量的贡献。可以在<a href="https://github.com/fhsinchy/the-docker-handbook">这个仓库</a>中找到完整的内容。</p>
<p>我通常先在本书的 GitBook 版本上进行更改和更新，然后在将其发布在 freeCodeCamp 专栏。你可以在<a href="https://docker.farhan.info/">这个链接</a>中找到本书的最新编辑中版本。别忘了评分支持。</p>
<p>如果你正在寻找本书的完整稳定版本，那么 freeCodeCamp 是最好的选择。如果你有所收获，请分享给你的朋友。</p>
<p>不管阅读本书的哪个版本，都不要忘记留下你的意见。欢迎提出建设性的批评。</p>
<h2 id="docker">容器化和 Docker 简介</h2>
<p>摘自 <a href="https://www.ibm.com/cloud/learn/containerization#toc-what-is-co-r25Smlqq">IBM</a>,</p>
<blockquote>
<p>容器化意味着封装或打包软件代码及其所有依赖项，以便它可以在任何基础架构上统一且一致地运行。</p>
</blockquote>
<p>换句话说，容器化可以将软件及其所有依赖项打包在一个自包含的软件包中，这样就可以省略麻烦的配置，直接运行。</p>
<p>举一个现实生活的场景。假设你已经开发了一个很棒的图书管理应用程序，该应用程序可以存储所有图书的信息，还可以为别人提供图书借阅服务。</p>
<p>如果列出依赖项，如下所示：</p>
<ul>
<li>Node.js</li>
<li>Express.js</li>
<li>SQLite3</li>
</ul>
<p>理论上应该是这样。但是实际上还要搞定其他一些事情。 <a href="https://nodejs.org/">Node.js</a> 使用了 <code>node-gyp</code> 构建工具来构建原生加载项。根据<a href="https://github.com/nodejs/node-gyp">官方存储库</a>中的<a href="https://github.com/nodejs/node-gyp#installation">安装说明</a>，此构建工具需要 Python 2 或 3 和相应的的 C/C ++ 编译器工具链。</p>
<p>考虑到所有这些因素，最终的依赖关系列表如下：</p>
<ul>
<li>Node.js</li>
<li>Express.js</li>
<li>SQLite3</li>
<li>Python 2 or 3</li>
<li>C/C++ tool-chain</li>
</ul>
<p>无论使用什么平台，安装 Python 2 或 3 都非常简单。在 Linux 上，设置 C/C ++ 工具链也非常容易，但是在 Windows 和 Mac 上，这是一项繁重的工作。</p>
<p>在 Windows 上，C++ 构建工具包有数 GB 之大，安装需要花费相当长的时间。在 Mac 上，可以安装庞大的 <a href="https://developer.apple.com/xcode/">Xcode</a> 应用程序，也可以安装小巧的 <a href="https://developer.apple.com/download/">Xcode 命令行工具</a>包。</p>
<p>不管安装了哪一种，它都可能会在 OS 更新时中断。实际上，该问题非常普遍，甚至连官方仓库都专门提供了 <a href="https://github.com/nodejs/node-gyp/blob/master/macOS_Catalina.md">macOS Catalina 的安装说明</a>。</p>
<p>这里假设你已经解决了设置依赖项的所有麻烦，并且已经准备好开始。这是否意味着现在开始就一帆风顺了？当然不是。</p>
<p>如果你使用 Linux 而同事使用 Windows 该怎么办？现在，必须考虑如何处理这两个不同的操作系统不一致的路径，或诸如 <a href="https://nginx.org/">nginx</a> 之类的流行技术在 Windows 上未得到很好的优化的事实，以及诸如 <a href="https://redis.io/">Redis</a> 之类的某些技术甚至都不是针对 Windows 预先构建的。</p>
<p>即使你完成了整个开发，如果负责管理服务器的人员部署流程搞错了，该怎么办？</p>
<p>所有这些问题都可以通过以下方式解决：</p>
<ul>
<li>在与最终部署环境匹配的隔离环境（称为容器）中开发和运行应用程序。</li>
<li>将你的应用程序及其所有依赖项和必要的部署配置放入一个文件（称为镜像）中。</li>
<li>并通过具有适当授权的任何人都可以访问的中央服务器（称为仓库）共享该镜像。</li>
</ul>
<p>然后，你的同事就可以从仓库中下载镜像，可以在没有平台冲突的隔离环境中运行应用，甚至可以直接在服务器上进行部署，因为该镜像也可以进行生产环境配置。</p>
<p>这就是容器化背后的想法：将应用程序放在一个独立的程序包中，使其在各种环境中都可移植且可回溯。</p>
<p>现在的问题是：Docker 在这里扮演什么角色？</p>
<p>正如我之前讲的，容器化是一种将一切统一放入盒子中来解决软件开发过程中的问题的思想。</p>
<p>这个想法有很多实现。<a href="https://www.docker.com/">Docker</a> 就是这样的实现。这是一个开放源代码的容器化平台，可让你对应用程序进行容器化，使用公共或私有仓库共享它们，也可以<a href="https://docs.docker.com/get-started/orchestration/">编排</a>它们。</p>
<p>目前，Docker 并不是市场上唯一的容器化工具，却是最受欢迎的容器化工具。我喜欢的另一个容器化引擎是 Red Hat 开发的 <a href="https://podman.io/">Podman</a>。其他工具，例如 Google 的 <a href="https://github.com/GoogleContainerTools/kaniko">Kaniko</a>，CoreOS 的 <a href="https://coreos.com/rkt/">rkt</a> 都很棒，但和 Docker 还是有差距。</p>
<p>此外，如果你想了解容器的历史，可以阅读 <a href="https://blog.aquasec.com/a-brief-history-of-containers-from-1970s-chroot-to-docker-2016">A Brief History of Containers: From the 1970s Till Now</a>，它描述了该技术的很多重要节点。</p>
<h2 id="docker">怎样安装 Docker</h2>
<p>Docker 的安装因使用的操作系统而异。但这整个过程都非常简单。</p>
<p>Docker可在 Mac、Windows 和 Linux 这三个主要平台上完美运行。在这三者中，在 Mac 上的安装过程是最简单的，因此我们从这里开始。</p>
<h3 id="macosdocker">怎样在 macOS 里安装 Docker</h3>
<p>在 Mac 上，要做的就是跳转到官方的<a href="https://www.docker.com/products/docker-desktop">下载页面</a>，然后单击_Download for Mac(stable)_按钮。</p>
<p>你会看到一个常规的 <em>Apple Disk Image</em> 文件，在该文件的内有 Docker 应用程序。所要做的就是将文件拖放到 Applications 目录中。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/drag-docker-in-applications-directory.png" alt="drag-docker-in-applications-directory" width="600" height="400" loading="lazy"></p>
<p>只需双击应用程序图标即可启动 Docker。应用程序启动后，将看到 Docker 图标出现在菜单栏上。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-icon-in-menubar.png" alt="docker-icon-in-menubar" width="600" height="400" loading="lazy"></p>
<p>现在，打开终端并执行 <code>docker --version</code> 和 <code>docker-compose --version</code> 以验证是否安装成功。</p>
<h3 id="windowsdocker">怎样在 Windows 上安装 Docker</h3>
<p>在 Windows 上，步骤几乎相同，当然还需要执行一些额外的操作。安装步骤如下：</p>
<ol>
<li>跳转到<a href="https://docs.microsoft.com/zh-cn/windows/wsl/install-win10">此站点</a>，然后按照说明在 Windows 10 上安装 WSL2。</li>
<li>然后跳转到官方<a href="https://www.docker.com/products/docker-desktop">下载页面</a> 并单击 <em>Download for Windows(stable)</em> 按钮。</li>
<li>双击下载的安装程序，然后使用默认设置进行安装。</li>
</ol>
<p>安装完成后，从开始菜单或桌面启动 <em>Docker Desktop</em>。Docker 图标应显示在任务栏上。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-icon-in-taskbar.png" alt="docker-icon-in-taskbar" width="600" height="400" loading="lazy"></p>
<p>现在，打开 Ubuntu 或从 Microsoft Store 安装的任何发行版。执行 <code>docker --version</code> 和 <code>docker-compose --version</code> 命令以确保安装成功。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-and-compose-version-on-windows.png" alt="docker-and-compose-version-on-windows" width="600" height="400" loading="lazy"></p>
<p>也可以从常规命令提示符或 PowerShell 访问 Docker，只是我更喜欢使用 WSL2。</p>
<h3 id="linuxdocker">怎样在 Linux 上安装 Docker</h3>
<p>在 Linux 上安装 Docker 的过程有所不同，具体操作取决于你所使用的发行版，它们之间差异可能更大。但老实说，安装与其他两个平台一样容易（如果不能算更容易的话）。</p>
<p>Windows 或 Mac 上的 Docker Desktop 软件包是一系列工具的集合，例如<code>Docker Engine</code>、<code>Docker Compose</code>、<code>Docker Dashboard</code>、<code>Kubernetes</code> 和其他一些好东西。</p>
<p>但是，在 Linux 上，没有得到这样的捆绑包。可以手动安装所需的所有必要工具。 不同发行版的安装过程如下：</p>
<ul>
<li>如果你使用的是 Ubuntu，则可以遵循官方文档中的<a href="https://docs.docker.com/engine/install/ubuntu/">在 Ubuntu 上安装 Docker 引擎</a>部分。</li>
<li>对于其他发行版，官方文档中提供了 <em>不同发行版的安装指南</em>。
<ul>
<li><a href="https://docs.docker.com/engine/install/debian/">在 Debian上安装 Docker Engine</a></li>
<li><a href="https://docs.docker.com/engine/install/fedora/">在 Fedora 上安装 Docker Engine</a></li>
<li><a href="https://docs.docker.com/engine/install/centos/">在 CentOS 上安装 Docker Engine</a></li>
</ul>
</li>
<li>如果你使用的发行版未在文档中列出，则可以参考<a href="https://docs.docker.com/engine/install/binaries/">从二进制文件安装 Docker 引擎</a>指南。</li>
<li>无论参考什么程序，都必须完成一些非常重要的 <a href="https://docs.docker.com/engine/install/linux-postinstall/">Linux 的安装后续步骤</a>。</li>
<li>完成 docker 安装后，必须安装另一个名为 Docker Compose 的工具。 可以参考官方文档中的 <a href="https://docs.docker.com/compose/install/">Install Docker Compose</a> 指南。</li>
</ul>
<p>安装完成后，打开终端并执行 <code>docker --version</code> 和 <code>docker-compose --version</code> 以确保安装成功。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-and-compose-version-on-linux.png" alt="docker-and-compose-version-on-linux" width="600" height="400" loading="lazy"></p>
<p>尽管无论使用哪个平台，Docker 的性能都很好，但与其他平台相比，我更喜欢 Linux。在整本书中，我将使用<a href="https://releases.ubuntu.com/20.10/">Ubuntu 20.10</a> 或者 <a href="https://fedoramagazine.org/announcing-fedora-33/">Fedora 33</a>。</p>
<p>一开始就需要阐明的另一件事是，在整本书中，我不会使用任何 GUI 工具操作 Docker。</p>
<p>我在各个平台用过很多不错的 GUI 工具，但是介绍常见的 docker 命令是本书的主要目标之一。</p>
<h2 id="dockerdocker">初识 Docker - 介绍 Docker 基本知识</h2>
<p>已经在计算机上启动并运行了 Docker，现在该运行第一个容器了。打开终端并执行以下命令：</p>
<pre><code>docker run hello-world

# Unable to find image 'hello-world:latest' locally
# latest: Pulling from library/hello-world
# 0e03bdcc26d7: Pull complete 
# Digest: sha256:4cf9c47f86df71d48364001ede3a4fcd85ae80ce02ebad74156906caff5378bc
# Status: Downloaded newer image for hello-world:latest
# 
# Hello from Docker!
# This message shows that your installation appears to be working correctly.
# 
# To generate this message, Docker took the following steps:
#  1. The Docker client contacted the Docker daemon.
#  2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
#     (amd64)
#  3. The Docker daemon created a new container from that image which runs the
#     executable that produces the output you are currently reading.
#  4. The Docker daemon streamed that output to the Docker client, which sent it
#     to your terminal.
#
# To try something more ambitious, you can run an Ubuntu container with:
#  $ docker run -it ubuntu bash
# 
# Share images, automate workflows, and more with a free Docker ID:
#  https://hub.docker.com/
#
# For more examples and ideas, visit:
#  https://docs.docker.com/get-started/
</code></pre>
<p><a href="https://hub.docker.com/_/hello-world">hello-world</a> 镜像是使用 Docker 进行最小化容器化的一个示例。它有一个从 <a href="https://github.com/docker-library/hello-world/blob/master/hello.c">hello.c</a> 文件编译的程序，负责打印出终端看到的消息。</p>
<p>现在，在终端中，可以使用 <code>docker ps -a</code> 命令查看当前正在运行或过去运行的所有容器：</p>
<pre><code>docker ps -a

# CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
# 128ec8ceab71        hello-world         "/hello"            14 seconds ago      Exited (0) 13 seconds ago                      exciting_chebyshev
</code></pre>
<p>在输出中，使用 <code>hello-world</code> 镜像运行了名为 <code>exciting_chebyshev</code> 的容器，其容器标识为 <code>128ec8ceab71</code>。它已经在 <code>Exited (0) 13 seconds ago</code>，其中 <code>(0)</code> 退出代码表示在容器运行时未发生任何错误。</p>
<p>现在，为了了解背后发生的事情，必须熟悉 Docker 体系结构和三个非常基本的容器化概念，如下所示：</p>
<ul>
<li><a href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS1b3opwENd_9qH1jTO/hello-world-in-docker#container">容器</a></li>
<li><a href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS1b3opwENd_9qH1jTO/hello-world-in-docker#image">镜像</a></li>
<li><a href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS1b3opwENd_9qH1jTO/hello-world-in-docker#registry">仓库</a></li>
</ul>
<p>我已经按字母顺序列出了这三个概念，并且将从列表中的第一个开始介绍。</p>
<h3 id="">什么是容器？</h3>
<p>在容器化世界中，没有什么比容器的概念更基础的了。</p>
<p>官方 Docker <a href="https://www.docker.com/resources/what-container">resources</a> 网站说 -</p>
<blockquote>
<p>容器是应用程序层的抽象，可以将代码和依赖项打包在一起。容器不虚拟化整个物理机，仅虚拟化主机操作系统。</p>
</blockquote>
<p>可以认为容器是下一代虚拟机。</p>
<p>就像虚拟机一样，容器是与主机系统是彼此之间完全隔离的环境。它也比传统虚拟机轻量得多，因此可以同时运行大量容器，而不会影响主机系统的性能。</p>
<p>容器和虚拟机实际上是虚拟化物理硬件的不同方法。两者之间的主要区别是虚拟化方式。</p>
<p>虚拟机通常由称为虚拟机监控器的程序创建和管理，例如 <a href="https://www.virtualbox.org/">Oracle VM VirtualBox</a>，<a href="https://www.vmware.com/">VMware Workstation</a>，<a href="https://www.linux-kvm.org/">KVM</a>，<a href="https://docs.microsoft.com/zh-cn/virtualization/hyper-v-on-windows/about/">Microsoft Hyper-V</a> 等等。 该虚拟机监控程序通常位于主机操作系统和虚拟机之间，充当通信介质。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/virtual-machines.svg" alt="virtual-machines" width="600" height="400" loading="lazy"></p>
<p>每个虚拟机都有自己的 guest 操作系统，该操作系统与主机操作系统一样消耗资源。</p>
<p>在虚拟机内部运行的应用程序与 guest 操作系统进行通信，该 guest 操作系统在与虚拟机监控器进行通信，后者随后又与主机操作系统进行通信，以将必要的资源从物理基础设施分配给正在运行的应用程序。</p>
<p>虚拟机内部运行的应用程序与物理基础设施之间存在很长的通信链。在虚拟机内部运行的应用程序可能只拥有少量资源，因为 guest 操作系统会占用很大的开销。</p>
<p>与虚拟机不同，容器以更智能的方式完成虚拟化工作。在容器内部没有完整的 guest 操作系统，它只是通过容器运行时使用主机操作系统，同时保持隔离 – 就像传统的虚拟机一样。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/containers.svg" alt="containers" width="600" height="400" loading="lazy"></p>
<p>容器运行时（即 Docker）位于容器和主机操作系统之间，而不是虚拟机监控器中。容器与容器运行时进行通信，容器运行时再与主机操作系统进行通信，以从物理基础设施中获取必要的资源。</p>
<p>由于消除了整个主机操作系统层，因此与传统的虚拟机相比，容器的更轻量，资源占用更少。</p>
<p>为了说明这一点，请看下面的代码片段：</p>
<pre><code>uname -a
# Linux alpha-centauri 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

docker run alpine uname -a
# Linux f08dbbe9199b 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 Linux
</code></pre>
<p>在上面的代码片段中，在主机操作系统上执行了 <code>uname -a</code> 命令以打印出内核详细信息。然后在下一行，我在运行 <a href="https://alpinelinux.org/">Alpine Linux</a> 的容器内执行了相同的命令。</p>
<p>从输出中可以看到，该容器确实正在使用主机操作系统中的内核。这证明了容器虚拟化主机操作系统而不是拥有自己的操作系统这一点。</p>
<p>如果你使用的是 Windows 计算机，则会发现所有容器都使用 WSL2 内核。发生这种情况是因为 WSL2 充当了 Windows 上 Docker 的后端。在 macOS 上，默认后端是在  <a href="https://github.com/moby/hyperkit">HyperKit</a> 虚拟机管理程序上运行的 VM。</p>
<h3 id="docker">什么是 Docker 镜像？</h3>
<p>镜像是分层的自包含文件，充当创建容器的模板。它们就像容器的冻结只读副本。 镜像可以通过仓库进行共享。</p>
<p>过去，不同的容器引擎具有不同的镜像格式。但是后来，<a href="https://opencontainers.org/">开放式容器计划（OCI）</a>定义了容器镜像的标准规范，该规范被主要的容器化引擎所遵循。这意味着使用 Docker 构建的映像可以与 Podman 等其他运行时一起使用，而不会有兼容性问题。</p>
<p>容器只是处于运行状态的镜像。当从互联网上获取镜像并使用该镜像运行容器时，实际上是在先前的只读层之上创建了另一个临时可写层。</p>
<p>在本书的后续部分中，这一概念将变得更加清晰。但就目前而言，请记住，镜像是分层只读文件，其中保留着应用程序所需的状态。</p>
<h3 id="">什么是仓库？</h3>
<p>已经了解了这个难题的两个非常重要的部分，即 <em>Containers</em> 和 <em>Images</em>。最后一个是 <em>Registry</em>。</p>
<p>镜像仓库是一个集中式的位置，可以在其中上传镜像，也可以下载其他人创建的镜像。 <a href="https://hub.docker.com/">Docker Hub</a> 是 Docker 的默认公共仓库。另一个非常流行的镜像仓库是 Red Hat 的 <a href="https://quay.io/">Quay</a>。</p>
<p>在本书中，我将使用 Docker Hub 作为首选仓库。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-hub.png" alt="docker-hub" width="600" height="400" loading="lazy"></p>
<p>可以免费在 Docker Hub 上共享任意数量的公共镜像。供世界各地的人们下载免费使用。可在我的个人资料（<a href="https://hub.docker.com/u/fhsinchy">fhsinchy</a>）页面上找到我上传的镜像。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/my-images-on-docker-hub.png" alt="my-images-on-docker-hub" width="600" height="400" loading="lazy"></p>
<p>除了 Docker Hub 或 Quay，还可以创建自己的镜像仓库来托管私有镜像。计算机中还运行着一个本地仓库，该仓库缓存从远程仓库提取的镜像。</p>
<h3 id="docker">Docker 架构概述</h3>
<p>既然已经熟悉了有关容器化和 Docker 的大多数基本概念，那么现在是时候了解 Docker 作为软件的架构了。</p>
<p>该引擎包括三个主要组件：</p>
<ol>
<li><strong>Docker 守护程序：</strong> 守护程序（<code>dockerd</code>）是一个始终在后台运行并等待来自客户端的命令的进程。守护程序能够管理各种 Docker 对象。</li>
<li><strong>Docker 客户端：</strong> 客户端（<code>docker</code>）是一个命令行界面程序，主要负责传输用户发出的命令。</li>
<li><strong>REST API：</strong> REST API 充当守护程序和客户端之间的桥梁。使用客户端发出的任何命令都将通过 API 传递，最终到达守护程序。</li>
</ol>
<p>根据官方<a href="https://docs.docker.com/get-started/overview/#docker-architecture">文档</a>,</p>
<blockquote>
<p>“ Docker 使用客户端-服务器体系结构。Docker <em>client</em> 与 Docker <em>daemon</em> 对话，daemon 繁重地构建、运行和分发 Docker 容器”。</p>
</blockquote>
<p>作为用户，通常将使用客户端组件执行命令。然后，客户端使用 REST API 来访问长期运行的守护程序并完成工作。</p>
<h3 id="">全景图</h3>
<p>好吧，说的够多了。 现在是时候了解刚刚学习的所有这些知识如何和谐地工作了。在深入解释运行 <code>docker run hello-world</code> 命令时实际发生的情况之前，看一下下面的图片：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/docker-run-hello-world.svg" alt="docker-run-hello-world" width="600" height="400" loading="lazy"></p>
<p>该图像是在官方<a href="https://docs.docker.com/engine/images/architecture.svg">文档</a>中找到的图像的略微修改版本。 执行命令时发生的事件如下：</p>
<ol>
<li>执行 <code>docker run hello-world</code> 命令，其中 <code>hello-world</code> 是镜像的名称。</li>
<li>Docker 客户端访问守护程序，告诉它获取 <code>hello-world</code> 镜像并从中运行一个容器。</li>
<li>Docker 守护程序在本地仓库中查找镜像，并发现它不存在，所以在终端上打印 <code>Unable to find image 'hello-world:latest' locally</code>。</li>
<li>然后，守护程序访问默认的公共仓库 Docker Hub，拉取 <code>hello-world</code> 镜像的最新副本，并在命令行中展示 <code>Unable to find image 'hello-world:latest' locally</code>。</li>
<li>Docker 守护程序根据新拉取的镜像创建一个新容器。</li>
<li>最后，Docker 守护程序运行使用 <code>hello-world</code> 镜像创建的容器，该镜像在终端上输出文本。</li>
</ol>
<p>Docker 守护程序的默认行为是在 hub 中查找本地不存在的镜像。但是，拉取了镜像之后，它将保留在本地缓存中。因此，如果再次执行该命令，则在输出中将看不到以下几行：</p>
<pre><code>Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
0e03bdcc26d7: Pull complete
Digest: sha256:d58e752213a51785838f9eed2b7a498ffa1cb3aa7f946dda11af39286c3db9a9
Status: Downloaded newer image for hello-world:latest
</code></pre>
<p>如果公共仓库中有可用镜像的更新版本，则守护程序将再次拉取该镜像。那个 <code>:latest</code>  是一个标记。镜像通常包含有意义的标记以指示版本或内部版本。稍后，将更详细地介绍这一点。</p>
<h2 id="docker">Docker 容器操作基础知识</h2>
<p>在前面的部分中，已经了解了 Docker 的构建模块，还使用 <code>docker run</code> 命令运行了一个容器。</p>
<p>在本节中，将详细介绍容器的操作。容器操作是每天要执行的最常见的任务之一，因此，正确理解各种命令至关重要。</p>
<p>但是请记住，这并不是可以在 Docker 上执行的所有命令的详尽列表。我只会介绍最常见的那些。当想知道某一命令的更多用法时，可以访问 Docker 命令行的官方<a href="https://docs.docker.com/engine/reference/commandline/container/">参考</a>。</p>
<h3 id="">怎样运行容器</h3>
<p>之前，已经使用 <code>docker run</code> 来使用 <code>hello-world</code> 镜像创建和启动容器。此命令的通用语法如下：</p>
<pre><code>docker run &lt;image name&gt;
</code></pre>
<p>尽管这是一个完全有效的命令，但是有一种更好的方式可以将命令分配给 <code>docker</code> 守护程序。</p>
<p>在版本 <code>1.13</code> 之前，Docker 仅具有前面提到的命令语法。后来，命令行经过了<a href="https://www.docker.com/blog/whats-new-in-docker-1-13/">重构</a>具有了以下语法：</p>
<pre><code>docker &lt;object&gt; &lt;command&gt; &lt;options&gt;
</code></pre>
<p>使用以下语法：</p>
<ul>
<li><code>object</code> 表示将要操作的 Docker 对象的类型。这可以是 <code>container</code>、<code>image</code>、<code>network</code> 或者 <code>volume</code> 对象。</li>
<li><code>command</code> 表示守护程序要执行的任务，即 <code>run</code> 命令。</li>
<li><code>options</code> 可以是任何可以覆盖命令默认行为的有效参数，例如端口映射的 <code>--publish</code> 选项。</li>
</ul>
<p>现在，遵循此语法，可以将 <code>run</code> 命令编写如下：</p>
<pre><code>docker container run &lt;image name&gt;
</code></pre>
<p><code>image name</code> 可以是在线仓库或本地系统中的任何镜像。例如，可以尝试使用<a href="https://hub.docker.com/r/fhsinchy/hello-dock">fhsinchy / hello-dock</a> 镜像运行容器。 该镜像包含一个简单的 <a href="https://vuejs.org/">Vue.js</a>应用程序，该应用程序在容器内部的端口 80 上运行。</p>
<p>请在终端上执行以下命令以使用此镜像运行容器：</p>
<pre><code>docker container run --publish 8080:80 fhsinchy/hello-dock

# /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
# /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
# 10-listen-on-ipv6-by-default.sh: Getting the checksum of /etc/nginx/conf.d/default.conf
# 10-listen-on-ipv6-by-default.sh: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
# /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
# /docker-entrypoint.sh: Configuration complete; ready for start up
</code></pre>
<p>该命令不言自明。唯一需要说明的部分是 <code>--publish 8080:80</code> 部分，将在下一个小节中进行说明。</p>
<h3 id="">怎样公开端口</h3>
<p>容器是隔离的环境。主机系统对容器内部发生的事情一无所知。因此，从外部无法访问在容器内部运行的应用程序。</p>
<p>要允许从容器外部进行访问，必须将容器内的相应端口发布到本地网络上的端口。<code>--publish</code> 或 <code>-p</code> 选项的通用语法如下：</p>
<pre><code>--publish &lt;host port&gt;:&lt;container port&gt;
</code></pre>
<p>在上一小节中编写了 <code>--publish 8080:80</code> 时，这意味着发送到主机系统端口 8080 的任何请求都将转发到容器内的端口 80。</p>
<p>现在要在浏览器上访问该应用程序，只需访问  <code>http://127.0.0.1:8080</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/hello-dock.png" alt="hello-dock" width="600" height="400" loading="lazy"></p>
<p>可以在终端窗口按下 <code>ctrl + c</code> 组合键或关闭终端窗口来停止容器。</p>
<h3 id="">如何使用分离模式</h3>
<p><code>run</code> 命令的另一个非常流行的选项是 <code>---detach</code> 或 <code>-d</code> 选项。 在上面的示例中，为了使容器继续运行，必须将终端窗口保持打开状态。关闭终端窗口会停止正在运行的容器。</p>
<p>这是因为，默认情况下，容器在前台运行，并像从终端调用的任何其他普通程序一样将其自身附加到终端。</p>
<p>为了覆盖此行为并保持容器在后台运行，可以在 <code>run</code> 命令中包含 <code>--detach</code> 选项，如下所示：</p>
<pre><code>docker container run --detach --publish 8080:80 fhsinchy/hello-dock

# 9f21cb77705810797c4b847dbd330d9c732ffddba14fb435470567a7a3f46cdc
</code></pre>
<p>与前面的示例不同，这次不会看到很多文字，而只获得新创建的容器的 ID。</p>
<p>提供选项的顺序并不重要。 如果将 <code>--publish</code> 选项放在 <code>--detach</code> 选项之前，效果相同。</p>
<p>使用 <code>run</code> 命令时必须记住的一件事是镜像名称必须最后出现。如果在镜像名称后放置任何内容，则将其作为参数传递给容器入口点（在<a href="https://www.freecodecamp.org/news/the-docker-handbook/#executing-commands-inside-a-container">在容器内执行命令</a>小节做了解释），可能会导致意外情况。</p>
<h3 id="">怎样列表展示容器</h3>
<p><code>container ls</code> 命令可用于列出当前正在运行的容器。执行以下命令：</p>
<pre><code>docker container ls

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   5 seconds ago       Up 5 seconds        0.0.0.0:8080-&gt;80/tcp   gifted_sammet
</code></pre>
<p>一个名为 <code>gifted_sammet</code> 的容器正在运行。它是在 <code>5 seconds ago</code> 前创建的，状态为 <code>Up 5 seconds</code>，这表明自创建以来，该容器一直运行良好。</p>
<p><code>CONTAINER ID</code> 为 <code>9f21cb777058</code>，这是完整容器 ID 的前 12 个字符。完整的容器 ID 是 <code>9f21cb77705810797c4b847dbd330d9c732ffddba14fb435470567a7a3f46cdc</code>，该字符长 64 个字符。在上一节中 <code>docker container run</code>  命令行的输的就是完整的容器 ID 。</p>
<p>列表的 <code>PORTS</code> 列下，本地网络的端口 8080 指向容器内的端口 80。name <code>gifted_sammet</code> 是由 Docker 生成的，可能与你的计算机的不同。</p>
<p><code>container ls</code> 命令仅列出系统上当前正在运行的容器。为了列出过去运行的所有容器，可以使用 <code>--all</code> 或 <code>-a</code> 选项。</p>
<pre><code>docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                     PORTS                  NAMES
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   2 minutes ago       Up 2 minutes               0.0.0.0:8080-&gt;80/tcp   gifted_sammet
# 6cf52771dde1        fhsinchy/hello-dock   "/docker-entrypoint.…"   3 minutes ago       Exited (0) 3 minutes ago                          reverent_torvalds
# 128ec8ceab71        hello-world           "/hello"                 4 minutes ago       Exited (0) 4 minutes ago                          exciting_chebyshev
</code></pre>
<p>如你所见，列表 <code>reverent_torvalds</code> 中的第二个容器是较早创建的，并以状态代码 0 退出，这表明在容器运行期间未产生任何错误。</p>
<h3 id="">怎样命名或者重命名一个容器</h3>
<p>默认情况下，每个容器都有两个标识符。 如下：</p>
<ul>
<li><code>CONTAINER ID</code> - 64 个字符的随机字符串。</li>
<li><code>NAME</code> - 两个随机词的组合，下划线连接。</li>
</ul>
<p>基于这两个随机标识符来引用容器非常不方便。如果可以使用自定义的名称来引用容器，那就太好了。</p>
<p>可以使用 <code>--name</code> 选项来命名容器。要使用名为 <code>hello-dock-container</code> 的 <code> fhsinchy/hello-dock</code> 镜像运行另一个容器，可以执行以下命令：</p>
<pre><code>docker container run --detach --publish 8888:80 --name hello-dock-container fhsinchy/hello-dock

# b1db06e400c4c5e81a93a64d30acc1bf821bed63af36cab5cdb95d25e114f5fb
</code></pre>
<p>本地网络上的 8080 端口被 <code>gifted_sammet</code> 容器（在上一小节中创建的容器）占用了。这就是为什么必须使用其他端口号（例如 8888）的原因。要进行验证，执行 <code> container ls</code> 命令：</p>
<pre><code>docker container ls

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
# b1db06e400c4        fhsinchy/hello-dock   "/docker-entrypoint.…"   28 seconds ago      Up 26 seconds       0.0.0.0:8888-&gt;80/tcp   hello-dock-container
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   4 minutes ago       Up 4 minutes        0.0.0.0:8080-&gt;80/tcp   gifted_sammet
</code></pre>
<p>一个名为 <code>hello-dock-container</code> 的新容器已经启动。</p>
<p>甚至可以使用 <code>container rename</code> 命令来重命名旧容器。该命令的语法如下：</p>
<pre><code>docker container rename &lt;container identifier&gt; &lt;new name&gt;
</code></pre>
<p>要将 <code>gifted_sammet</code> 容器重命名为 <code>hello-dock-container-2</code>，可以执行以下命令：</p>
<pre><code>docker container rename gifted_sammet hello-dock-container-2
</code></pre>
<p>该命令不会产生任何输出，但是可以使用 <code>container ls</code> 命令来验证是否已进行更改。 <code>rename</code> 命令不仅适用于处于运行状态的容器和还适用于处于停止状态的容器。</p>
<h3 id="">怎样停止或者杀死运行中的容器</h3>
<p>可以通过简单地关闭终端窗口或单击 <code>ctrl + c</code> 来停止在前台运行的容器。但是，不能以相同方式停止在后台运行的容器。</p>
<p>有两个命令可以完成此任务。 第一个是 <code>container stop</code> 命令。该命令的通用语法如下：</p>
<pre><code>docker container stop &lt;container identifier&gt;
</code></pre>
<p>其中 <code>container identifier</code> 可以是容器的 ID 或名称。</p>
<p>应该还记得上一节中启动的容器。它仍在后台运行。使用 <code>docker container ls</code> 获取该容器的标识符（在本演示中，我将使用 <code>hello-dock-container</code> 容器）。现在执行以下命令来停止容器：</p>
<pre><code>docker container stop hello-dock-container

# hello-dock-container
</code></pre>
<p>如果使用 name 作为标识符，则 name 将作为输出返回。<code>stop</code> 命令通过发送信号<code>SIGTERM</code> 来正常关闭容器。如果容器在一定时间内没有停止运行，则会发出 <code>SIGKILL</code> 信号，该信号会立即关闭容器。</p>
<p>如果要发送 <code>SIGKILL</code> 信号而不是 <code>SIGTERM</code> 信号，则可以改用 <code>container kill</code> 命令。<code>container kill</code> 命令遵循与 <code>stop</code> 命令相同的语法。</p>
<pre><code>docker container kill hello-dock-container-2

# hello-dock-container-2
</code></pre>
<h3 id="">怎样重新启动容器</h3>
<p>当我说重启时，我指的如下是两种情况：</p>
<ul>
<li>重新启动先前已停止或终止的容器。</li>
<li>重新启动正在运行的容器。</li>
</ul>
<p>正如上一小节中学到的，停止的容器保留在系统中。如果需要，可以重新启动它们。<code>container start</code>  命令可用于启动任何已停止或终止的容器。该命令的语法如下：</p>
<pre><code>docker container start &lt;container identifier&gt;
</code></pre>
<p>可以通过执行 <code>container ls --all</code> 命令来获取所有容器的列表，然后寻找状态为 <code>Exited</code> 的容器。</p>
<pre><code>docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                        PORTS               NAMES
# b1db06e400c4        fhsinchy/hello-dock   "/docker-entrypoint.…"   3 minutes ago       Exited (0) 47 seconds ago                         hello-dock-container
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   7 minutes ago       Exited (137) 17 seconds ago                       hello-dock-container-2
# 6cf52771dde1        fhsinchy/hello-dock   "/docker-entrypoint.…"   7 minutes ago       Exited (0) 7 minutes ago                          reverent_torvalds
# 128ec8ceab71        hello-world           "/hello"                 9 minutes ago       Exited (0) 9 minutes ago                          exciting_chebyshev
</code></pre>
<p>现在要重新启动 <code>hello-dock-container</code> 容器，可以执行以下命令：</p>
<pre><code>docker container start hello-dock-container

# hello-dock-container
</code></pre>
<p>现在，可以使用 <code>container ls</code> 命令查看正在运行的容器列表，以确保该容器正在运行。</p>
<p>默认情况下，<code>container start</code> 命令以分离模式启动容器，并保留之前进行的端口配置。因此，如果现在访问 <code>http：//127.0.0.1：8080</code>，应该能够像以前一样访问 <code>hello-dock</code> 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/hello-dock.png" alt="hello-dock" width="600" height="400" loading="lazy"></p>
<p>现在，在想重新启动正在运行的容器，可以使用 <code>container restart</code> 命令。<code>container restart</code> 命令遵循与 <code>container start</code> 命令完全相同的语法。</p>
<pre><code>docker container restart hello-dock-container-2

# hello-dock-container-2
</code></pre>
<p>这两个命令之间的主要区别在于，<code>container restart</code> 命令尝试停止目标容器，然后再次启动它，而 start 命令只是启动一个已经停止的容器。</p>
<p>在容器停止的情况下，两个命令完全相同。但是如果容器正在运行，则必须使用<code>container restart</code> 命令。</p>
<h3 id="">怎样创建而不运行容器</h3>
<p>到目前为止，在本节中，已经使用 <code>container run</code> 命令启动了容器，该命令实际上是两个单独命令的组合。这两个命令如下：</p>
<ul>
<li><code>container create</code> 命令从给定的镜像创建一个容器。</li>
<li><code>container start</code> 命令将启动一个已经创建的容器。</li>
</ul>
<p>现在，要使用这两个命令执行<a href="https://www.freecodecamp.org/news/the-docker-handbook/#running-containers">运行容器</a>部分中显示的演示，可以执行以下操作：</p>
<pre><code>docker container create --publish 8080:80 fhsinchy/hello-dock

# 2e7ef5098bab92f4536eb9a372d9b99ed852a9a816c341127399f51a6d053856

docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS               NAMES
# 2e7ef5098bab        fhsinchy/hello-dock   "/docker-entrypoint.…"   30 seconds ago      Created                                 hello-dock
</code></pre>
<p>通过 <code>container ls --all</code> 命令的输出可以明显看出，已经使用 <code>fhsinchy/hello-dock</code> 镜像创建了一个名称为 <code>hello-dock</code> 的容器。 容器的 <code>STATUS</code> 目前处于 <code>Created</code> 状态，并且鉴于其未运行，因此不使用  <code>--all</code> 选项就不会列出该容器。</p>
<p>一旦创建了容器，就可以使用 <code>container start</code> 命令来启动它。</p>
<pre><code>docker container start hello-dock

# hello-dock

docker container ls

# CONTAINER ID        IMAGE                 COMMAND                  CREATED              STATUS              PORTS                  NAMES
# 2e7ef5098bab        fhsinchy/hello-dock   "/docker-entrypoint.…"   About a minute ago   Up 29 seconds       0.0.0.0:8080-&gt;80/tcp   hello-dock
</code></pre>
<p>容器 <code>STATUS</code> 已从 <code>Created</code> 更改为 <code>Up 29 seconds</code>，这表明容器现在处于运行状态。端口配置也显示在以前为空的 <code>PORTS</code> 列中。</p>
<p>尽管可以在大多数情况下使用 <code>container run</code> 命令，但本书稍后还会有一些情况要求使用 <code>container create</code> 命令。</p>
<h3 id="">怎样移除挂起的容器</h3>
<p>如你所见，已被停止或终止的容器仍保留在系统中。这些挂起的容器可能会占用空间或与较新的容器发生冲突。</p>
<p>可以使用 <code>container rm</code> 命令删除停止的容器。 通用语法如下：</p>
<pre><code>docker container rm &lt;container identifier&gt;
</code></pre>
<p>要找出哪些容器没有运行，使用 <code>container ls --all</code> 命令并查找状态为 <code>Exited</code> 的容器。</p>
<pre><code>docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                      PORTS                  NAMES
# b1db06e400c4        fhsinchy/hello-dock   "/docker-entrypoint.…"   6 minutes ago       Up About a minute           0.0.0.0:8888-&gt;80/tcp   hello-dock-container
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   10 minutes ago      Up About a minute           0.0.0.0:8080-&gt;80/tcp   hello-dock-container-2
# 6cf52771dde1        fhsinchy/hello-dock   "/docker-entrypoint.…"   10 minutes ago      Exited (0) 10 minutes ago                          reverent_torvalds
# 128ec8ceab71        hello-world           "/hello"                 12 minutes ago      Exited (0) 12 minutes ago                          exciting_chebyshev
</code></pre>
<p>从输出中可以看到，ID为 <code>6cf52771dde1</code> 和 <code>128ec8ceab71</code> 的容器未运行。要删除 <code>6cf52771dde1</code>，可以执行以下命令：</p>
<pre><code>docker container rm 6cf52771dde1

# 6cf52771dde1
</code></pre>
<p>可以使用 <code>container ls</code> 命令检查容器是否被删除。也可以一次删除多个容器，方法是将其标识符一个接一个地传递，每个标识符之间用空格隔开。</p>
<p>也可以使用 <code>container prune</code> 命令来一次性删除所有挂起的容器。</p>
<p>可以使用 <code>container ls --all</code> 命令检查容器列表，以确保已删除了挂起的容器：</p>
<pre><code>docker container ls --all

# CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                  NAMES
# b1db06e400c4        fhsinchy/hello-dock   "/docker-entrypoint.…"   8 minutes ago       Up 3 minutes        0.0.0.0:8888-&gt;80/tcp   hello-dock-container
# 9f21cb777058        fhsinchy/hello-dock   "/docker-entrypoint.…"   12 minutes ago      Up 3 minutes        0.0.0.0:8080-&gt;80/tcp   hello-dock-container-2
</code></pre>
<p>如果按照本书的顺序进行操作，则应该只在列表中看到 <code>hello-dock-container</code> 和 <code>hello-dock-container-2</code>。 建议停止并删除两个容器，然后再继续进行下一部分。</p>
<p><code>container run</code> 和 <code>container start</code> 命令还有 <code>--rm</code> 选项，它们表示希望容器在停止后立即被移除。 执行以下命令，使用 <code>--rm</code> 选项启动另一个 <code>hello-dock</code> 容器：</p>
<pre><code>docker container run --rm --detach --publish 8888:80 --name hello-dock-volatile fhsinchy/hello-dock

# 0d74e14091dc6262732bee226d95702c21894678efb4043663f7911c53fb79f3
</code></pre>
<p>可以使用 <code>container ls</code> 命令来验证容器是否正在运行：</p>
<pre><code>docker container ls

# CONTAINER ID   IMAGE                 COMMAND                  CREATED              STATUS              PORTS                  NAMES
# 0d74e14091dc   fhsinchy/hello-dock   "/docker-entrypoint.…"   About a minute ago   Up About a minute   0.0.0.0:8888-&gt;80/tcp   hello-dock-volatile
</code></pre>
<p>现在，如果停止了容器，使用 <code>container ls --all</code> 命令再次检查：</p>
<pre><code>docker container stop hello-dock-volatile

# hello-dock-volatile

docker container ls --all

# CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
</code></pre>
<p>该容器已被自动删除。从现在开始，我将对大多数容器使用 <code>--rm</code> 选项。不需要的地方我会明确提到。</p>
<h3 id="">怎样以交互式模式运行容器</h3>
<p>到目前为止，只运行了 <a href="https://hub.docker.com/_/hello-world">hello-world</a> 镜像或 <a href="https://chinese.freecodecamp.org/news/the-docker-handbook/https/hub.docker.com/r/fhsinchy/hello-dock">fhsinchy/hello-dock</a> 镜像。这些镜像用于执行非交互式的简单程序。</p>
<p>好吧，镜像并不是那么简单。镜像可以将整个 Linux 发行版封装在其中。</p>
<p>流行的发行版，例如 <a href="https://ubuntu.com/">Ubuntu</a>，<a href="https://fedora.org/">Fedora</a> 和 <a href="https://debian.org/">Debian</a> 都在 hub 有官方的 Docker 镜像。编程语言，例如 <a href="https://hub.docker.com/_/python">python</a>、<a href="https://hub.docker.com/_/php">php</a>、[go](https：// hub.docker.com/_/golang) 或类似 <a href="https://hub.docker.com/_/node">node</a> 和 <a href="https://hub.docker.com/r/hayd/deno">deno</a> 都有其官方镜像。</p>
<p>这些镜像不但仅运行某些预配置的程序。还将它们配置为默认情况下运行的 shell 程序。在镜像是操作系统的情况下，它可以是诸如 <code>sh</code> 或 <code>bash</code> 之类的东西，在镜像是编程语言或运行时的情况下，通常是它们的默认语言的 shell。</p>
<p>正如可能从以前的计算机中学到的一样，shell 是交互式程序。被配置为运行这样的程序的镜像是交互式镜像。这些镜像需要在  <code>container run</code>  命令中传递特殊的 <code>-it</code> 选项。</p>
<p>例如，如果通过执行 <code>docker container run ubuntu</code> 使用 <code>ubuntu</code> 镜像运行一个容器，将不会发生任何事情。但是，如果使用 <code>-it</code> 选项执行相同的命令，会直接进入到 Ubuntu 容器内的 bash 上。</p>
<pre><code>docker container run --rm -it ubuntu

# root@dbb1f56b9563:/# cat /etc/os-release
# NAME="Ubuntu"
# VERSION="20.04.1 LTS (Focal Fossa)"
# ID=ubuntu
# ID_LIKE=debian
# PRETTY_NAME="Ubuntu 20.04.1 LTS"
# VERSION_ID="20.04"
# HOME_URL="https://www.ubuntu.com/"
# SUPPORT_URL="https://help.ubuntu.com/"
# BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
# PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
# VERSION_CODENAME=focal
# UBUNTU_CODENAME=focal
</code></pre>
<p>从  <code>cat /etc/os-release</code>  命令的输出中可以看到，我确实正在与在 Ubuntu 容器中运行的 bash 进行交互。</p>
<p><code>-it</code> 选项提供了与容器内的程序进行交互的场景。此选项实际上是将两个单独的选项混在一起。</p>
<ul>
<li>选项  <code>-i</code> 或  <code>--interactive</code> 连接到容器的输入流，以便可以将输入发送到 bash。</li>
<li><code>-t</code> 或 <code>--tty</code>  选项可通过分配伪 tty 来格式化展示并提供类似本机终端的体验。</li>
</ul>
<p>当想以交互方式运行容器时，可以使用 <code>-it</code> 选项。以交互式方式运行 <code>node</code> 镜像，如下：</p>
<pre><code>docker container run -it node

# Welcome to Node.js v15.0.0.
# Type ".help" for more information.
# &gt; ['farhan', 'hasin', 'chowdhury'].map(name =&gt; name.toUpperCase())
# [ 'FARHAN', 'HASIN', 'CHOWDHURY' ]
</code></pre>
<p>任何有效的 JavaScript 代码都可以在 node shell 中执行。除了输入 <code>-it</code>，还可以输入 <code>--interactive --tty</code>，效果一样，只不过更冗长。</p>
<h3 id="">怎样在容器里执行命令</h3>
<p>在本书中<a href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS1b3opwENd_9qH1jTO/hello-world-in-docker">初识 Docker</a> 部分中，已经了解了在 Alpine Linux 容器内执行命令。 它是这样的：</p>
<pre><code>docker run alpine uname -a
# Linux f08dbbe9199b 5.8.0-22-generic #23-Ubuntu SMP Fri Oct 9 00:34:40 UTC 2020 x86_64 Linux
</code></pre>
<p>在此命令中，在 Alpine Linux 容器中执行了 <code>uname -a</code> 命令。像这样的场景（要做的就是在特定的容器内执行特定的命令）非常常见。</p>
<p>假设想使用 <code>base64</code> 程序对字符串进行编码。几乎所有基于 Linux 或 Unix 的操作系统都可以使用此功能（但 Windows 则不可用）。</p>
<p>在这种情况下，可以使用 <a href="https://hub.docker.com/_/busybox">busybox</a> 之类的镜像快速启动容器，然后执行命令。</p>
<p>使用 <code>base64</code> 编码字符串的通用语法如下：</p>
<pre><code>echo -n my-secret | base64

# bXktc2VjcmV0
</code></pre>
<p>将命令传递到未运行的容器的通用语法如下：</p>
<pre><code>docker container run &lt;image name&gt; &lt;command&gt;
</code></pre>
<p>要使用 busybox 镜像执行 base64  编码，可以执行以下命令：</p>
<pre><code>docker container run --rm busybox echo -n my-secret | base64

# bXktc2VjcmV0
</code></pre>
<p>这里发生的是，在 <code>container run</code> 命令中，镜像名称后传递的任何内容都将传递到镜像的默认入口里。</p>
<p>入口点就像是通往镜像的网关。除可执行镜像外的大多数镜像（在<a href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS1b3opwENd_9qH1jTO/container-manipulation-basics#working-with-executable-images">使用可执行镜像</a>小节中说明）使用 shell 或 <code>sh</code> 作为默认入口点。因此，任何有效的 shell 命令都可以作为参数传递给它们。</p>
<h3 id="">如何处理可执行镜像</h3>
<p>在上一节中，我简要提到了可执行镜像。这些镜像旨在表现得像可执行程序。</p>
<p>以的 <a href="https://github.com/fhsinchy/rmbyext">rmbyext</a> 项目为例。这是一个简单的 Python 脚本，能够递归删除给定扩展名的文件。 要了解有关该项目的更多信息，可以查看<a href="https://github.com/fhsinchy/rmbyext">仓库</a>。</p>
<p>如果同时安装了 Git 和 Python，则可以通过执行以下命令来安装此脚本：</p>
<pre><code>pip install git+https://github.com/fhsinchy/rmbyext.git#egg=rmbyext
</code></pre>
<p>假设的系统上已经正确设置了 Python，则该脚本应该可以在终端的任何位置使用。使用此脚本的通用语法如下：</p>
<pre><code>rmbyext &lt;file extension&gt;
</code></pre>
<p>要对其进行测试，请在一个空目录下打开终端，并在其中创建具有不同扩展名的一些文件。可以使用 <code>touch</code> 命令来做到这一点。现在，计算机上有一个包含以下文件的目录：</p>
<pre><code>touch a.pdf b.pdf c.txt d.pdf e.txt

ls

# a.pdf  b.pdf  c.txt  d.pdf  e.txt
</code></pre>
<p>要从该目录删除所有 <code>pdf</code> 文件，可以执行以下命令：</p>
<pre><code>rmbyext pdf

# Removing: PDF
# b.pdf
# a.pdf
# d.pdf
</code></pre>
<p>该程序的可执行镜像能够将文件扩展名用作参数，并像 <code>rmbyext</code> 程序一样删除它们。</p>
<p><a href="https://hub.docker.com/r/fhsinchy/rmbyext">fhsinchy/rmbyext</a> 镜像的行为类似。该镜像包含 <code>rmbyext</code> 脚本的副本，并配置为在容器内的目录 <code>/zone</code>上运行该脚本。</p>
<p>现在的问题是容器与本地系统隔离，因此在容器内运行的 <code>rmbyext</code> 程序无法访问本地文件系统。因此，如果可以通过某种方式将包含 pdf 文件的本地目录映射到容器内的 <code>/zone</code> 目录，则容器应该可以访问这些文件。</p>
<p>授予容器直接访问本地文件系统的一种方法是使用<a href="https://docs.docker.com/storage/bind-mounts/">绑定挂载</a>。</p>
<p>绑定挂载可以在本地文件系统目录（源）与容器内另一个目录（目标）之间形成双向数据绑定。这样，在目标目录中进行的任何更改都将在源目录上生效，反之亦然。</p>
<p>让我们看一下绑定挂载的实际应用。要使用此镜像而不是程序本身删除文件，可以执行以下命令：</p>
<pre><code>docker container run --rm -v $(pwd):/zone fhsinchy/rmbyext pdf

# Removing: PDF
# b.pdf
# a.pdf
# d.pdf
</code></pre>
<p>已经在命令中看到了 <code>-v $(pwd):/zone</code>  部分，你可能已经猜到了 <code>-v</code>  或 <code>--volume</code> 选项用于为容器创建绑定挂载。该选项可以使用三个以冒号（<code>:</code>）分隔的字段。该选项的通用语法如下：</p>
<pre><code>--volume &lt;local file system directory absolute path&gt;:&lt;container file system directory absolute path&gt;:&lt;read write access&gt;
</code></pre>
<p>第三个字段是可选的，但必须传递本地目录的绝对路径和容器内目录的绝对路径。</p>
<p>在这里，源目录是 <code>/home/fhsinchy/the-zone</code>。假设终端当前在目录中，则 <code>$(pwd)</code> 将替换为包含先前提到的 <code>.pdf</code> 和 <code>.txt</code> 文件的 <code>/home/fhsinchy/the-zone</code>。</p>
<p>可以在<a href="https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html">command substitution here</a> 上了解更多信息。</p>
<p><code>--volume</code> 或  <code>-v</code>  选项对 <code>container run</code> 以及 <code>container create</code> 命令均有效。我们将在接下来的部分中更详细地探讨卷，因此，如果在这里不太了解它们，请不要担心。</p>
<p>常规镜像和可执行镜像之间的区别在于，可执行镜像的入口点设置为自定义程序而不是 <code>sh</code>，在本例中为 <code>rmbyext</code> 程序。正如在上一小节中所学到的那样，在 <code>container run</code> 命令中在镜像名称之后编写的所有内容都将传递到镜像的入口点。</p>
<p>所以最后，<code>docker container run --rm -v $(pwd):/zone fhsinchy/rmbyext pdf</code> 命令转换为容器内的  <code>rmbyext pdf</code> 。可执行镜像并不常见，但在某些情况下可能非常有用。</p>
<h2 id="docker">Docker 镜像操作基础知识</h2>
<p>现在，已经对如何使用公开可用的镜像运行容器有了深入的了解，是时候学习如何创建自己的镜像了。</p>
<p>在本部分中，将学习创建镜像，使用镜像运行容器以及在线共享镜像的基础知识。</p>
<p>我建议在 <a href="https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker">Visual Studio Code</a> 中安装官方的 <a href="https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode">Docker Extension</a> 。 这将提升开发效率。</p>
<h3 id="docker">怎样创建  Docker 镜像</h3>
<p>正如我在<a href="https://www.freecodecamp.org/news/the-docker-handbook/#image">初识 Docker</a> 部分中已经解释的那样，镜像是分层的自包含文件，它们充当用于创建 Docker 容器的模板。它们就像是容器的冻结的只读副本。</p>
<p>为了使用程序创建镜像，必须对要从镜像中获得什么有清晰的认识。以官方 <a href="https://hub.docker.com/_/nginx">nginx</a> 镜像为例。只需执行以下命令即可使用该镜像启动容器：</p>
<pre><code>docker container run --rm --detach --name default-nginx --publish 8080:80 nginx

# b379ecd5b6b9ae27c144e4fa12bdc5d0635543666f75c14039eea8d5f38e3f56

docker container ls

# CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
# b379ecd5b6b9        nginx               "/docker-entrypoint.…"   8 seconds ago       Up 8 seconds        0.0.0.0:8080-&gt;80/tcp   default-nginx
</code></pre>
<p>现在，如果在浏览器中访问 <code>http://127.0.0.1:8080</code>，则会看到一个默认的响应页面。<br>
<img src="https://www.freecodecamp.org/news/content/images/2021/01/nginx-default.png" alt="nginx-default" width="600" height="400" loading="lazy"></p>
<p>看起来不错，但是，如果想制作一个自定义的 NGINX 镜像，该镜像的功能与正式镜像完全一样，但是由你自己创建，可行吗？老实说，完全可行。实际上，只需这样做。</p>
<p>为了制作自定义的 NGINX 镜像，必须清楚了解镜像的最终状态。我认为镜像应如下所示：</p>
<ul>
<li>该镜像应预安装 NGINX，可以使用程序包管理器完成该镜像，也可以从源代码构建该镜像。</li>
<li>该镜像在运行时应自动启动 NGINX。</li>
</ul>
<p>很简单，如果你已经克隆了本书中链接的项目仓库，请进入项目根目录并在其中查找名为 <code>custom-nginx</code> 的目录。</p>
<p>现在，在该目录中创建一个名为 <code>Dockerfile</code> 的新文件。<code>Dockerfile</code> 是指令的集合，该指令会被守护程序生成镜像。<code>Dockerfile</code> 的内容如下：</p>
<pre><code>FROM ubuntu:latest

EXPOSE 80

RUN apt-get update &amp;&amp; \
    apt-get install nginx -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

CMD ["nginx", "-g", "daemon off;"]
</code></pre>
<p>镜像是多层文件，在此文件中，编写的每一行（称为说明）都会为镜像创建一个层。</p>
<ul>
<li>每个有效的 <code>Dockerfile</code> 均以 <code>FROM</code>  指令开头。该指令为生成的镜像设置基本镜像。通过在此处将 <code>ubuntu：latest</code> 设置为基本镜像，可以在自定义镜像中使用 Ubuntu 的所有功能，因此可以使用 <code>apt-get</code> 命令之类的东西来轻松安装软件包。</li>
<li><code>EXPOSE</code> 指令表示需要发布的端口。使用此指令并不意味着不需要  <code>--publish</code>  端口。仍然需要显式使用 <code>--publish</code> 选项。该 <code>EXPOSE</code> 指令的工作原理类似于文档，适用于试图使用你的镜像运行容器的人员。它还有一些其他用途，我将不在这里讨论。</li>
<li><code>Dockerfile</code> 中的 <code>RUN</code> 指令在容器 shell 内部执行命令。<code>apt-get update &amp;&amp; apt-get install nginx -y</code> 命令检查更新的软件包版本并安装 NGINX。<code>apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*</code> 命令用于清除程序包缓存，因为不希望镜像中出现任何不必要的文件。这两个命令是简单的 Ubuntu 东西，没什么特别的。此处的 <code>RUN</code> 指令以 <code>shell</code> 形式编写。这些也可以以 <code>exec</code> 形式编写。 可以查阅<a href="https://docs.docker.com/engine/reference/builder/#run">官方参考</a>了解更多信息。</li>
<li>最后，<code>CMD</code>  指令为镜像设置了默认命令。该指令以 <code>exec</code> 形式编写，此处包含三个独立的部分。这里，<code>nginx</code>  是指 NGINX 可执行文件。 <code>-g</code> 和 <code>daemon off</code> 是 NGINX 的选项。 在容器内将 NGINX 作为单个进程运行是一种最佳实践，因此请使用此选项。<code>CMD</code> 指令也可以以 <code>shell</code> 形式编写。 可以查阅<a href="https://docs.docker.com/engine/reference/builder/#cmd">官方参考</a>了解更多信息。</li>
</ul>
<p>既然具有有效的 <code>Dockerfile</code>，可以从中构建镜像。就像与容器相关的命令一样，可以使用以下语法来执行与镜像相关的命令：</p>
<pre><code>docker image &lt;command&gt; &lt;options&gt;
</code></pre>
<p>要使用刚刚编写的  <code>Dockerfile</code> 构建镜像，请在 <code>custom-nginx</code> 目录中打开终端并执行以下命令：</p>
<pre><code>docker image build .

# Sending build context to Docker daemon  3.584kB
# Step 1/4 : FROM ubuntu:latest
#  ---&gt; d70eaf7277ea
# Step 2/4 : EXPOSE 80
#  ---&gt; Running in 9eae86582ec7
# Removing intermediate container 9eae86582ec7
#  ---&gt; 8235bd799a56
# Step 3/4 : RUN apt-get update &amp;&amp;     apt-get install nginx -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*
#  ---&gt; Running in a44725cbb3fa
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container a44725cbb3fa
#  ---&gt; 3066bd20292d
# Step 4/4 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 4792e4691660
# Removing intermediate container 4792e4691660
#  ---&gt; 3199372aa3fc
# Successfully built 3199372aa3fc
</code></pre>
<p>为了执行镜像构建，守护程序需要两条非常具体的信息。Dockerfile 的名称和构建上下文。在上面执行的命令中：</p>
<ul>
<li><code>docker image build</code> 是用于构建镜像的命令。守护程序在上下文中找到任何名为 Dockerfile 的文件。</li>
<li>最后的 <code>.</code>  设置了此构建的上下文。上下文是指在构建过程中守护程序可以访问的目录。</li>
</ul>
<p>现在要使用此镜像运行容器，可以将 <code>container run</code> 命令与在构建过程中收到的镜像 ID 结合使用。在我这里，通过上一个代码块中的 <code>Successfully built 3199372aa3fc</code> 行可以看到 id 为 <code>3199372aa3fc</code>。</p>
<pre><code>docker container run --rm --detach --name custom-nginx-packaged --publish 8080:80 3199372aa3fc

# ec09d4e1f70c903c3b954c8d7958421cdd1ae3d079b57f929e44131fbf8069a0

docker container ls

# CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                  NAMES
# ec09d4e1f70c        3199372aa3fc        "nginx -g 'daemon of…"   23 seconds ago      Up 22 seconds       0.0.0.0:8080-&gt;80/tcp   custom-nginx-packaged
</code></pre>
<p>要进行验证，请访问 <code>http://127.0.0.1:8080</code> ，应该会看到默认的响应页面。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/nginx-default.png" alt="nginx-default" width="600" height="400" loading="lazy"></p>
<h3 id="docker">如何标记 Docker 镜像</h3>
<p>就像容器一样，可以为镜像分配自定义标识符，而不必依赖于随机生成的 ID。如果是镜像，则称为标记而不是命名。在这种情况下，使用 <code>--tag</code> 或  <code>-t</code> 选项。</p>
<p>该选项的通用语法如下：</p>
<pre><code>--tag &lt;image repository&gt;:&lt;image tag&gt;
</code></pre>
<p>repository 通常指镜像名称，而 tag 指特定的构建或版本。</p>
<p>以官方 <a href="https://hub.docker.com/_/mysql">mysql</a> 镜像为例。如果想使用特定版本的MySQL（例如5.7）运行容器，则可以执行 <code>docker container run mysql:5.7</code>，其中 <code>mysql</code> 是镜像 repository，<code>5.7</code> 是 tag。</p>
<p>为了用  <code>custom-nginx:packaged</code> 标签标记自定义 NGINX 镜像，可以执行以下命令：</p>
<pre><code>docker image build --tag custom-nginx:packaged .

# Sending build context to Docker daemon  1.055MB
# Step 1/4 : FROM ubuntu:latest
#  ---&gt; f63181f19b2f
# Step 2/4 : EXPOSE 80
#  ---&gt; Running in 53ab370b9efc
# Removing intermediate container 53ab370b9efc
#  ---&gt; 6d6460a74447
# Step 3/4 : RUN apt-get update &amp;&amp;     apt-get install nginx -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*
#  ---&gt; Running in b4951b6b48bb
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container b4951b6b48bb
#  ---&gt; fdc6cdd8925a
# Step 4/4 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 3bdbd2af4f0e
# Removing intermediate container 3bdbd2af4f0e
#  ---&gt; f8837621b99d
# Successfully built f8837621b99d
# Successfully tagged custom-nginx:packaged
</code></pre>
<p>除了现在可以将镜像称为  <code>custom-nginx:packaged</code>（而不是一些较长的随机字符串）之外，什么都不会改变。</p>
<p>如果在构建期间忘记为镜像添加标记，或者你想更改标记，可以使用 <code>image tag</code> 命令执行此操作：</p>
<pre><code>docker image tag &lt;image id&gt; &lt;image repository&gt;:&lt;image tag&gt;

## 或者 ##

docker image tag &lt;image repository&gt;:&lt;image tag&gt; &lt;new image repository&gt;:&lt;new image tag&gt;
</code></pre>
<h3 id="">如何删除、列表展示镜像</h3>
<p>就像 <code>container ls</code> 命令一样，可以使用 <code>image ls</code> 命令列出本地系统中的所有镜像：</p>
<pre><code>docker image ls

# REPOSITORY     TAG        IMAGE ID       CREATED         SIZE
# &lt;none&gt;         &lt;none&gt;     3199372aa3fc   7 seconds ago   132MB
# custom-nginx   packaged   f8837621b99d   4 minutes ago   132MB
</code></pre>
<p>可以使用  <code>image rm</code>  命令删除此处列出的镜像。通用语法如下：</p>
<pre><code>docker image rm &lt;image identifier&gt;
</code></pre>
<p>标识符可以是镜像 ID 或镜像仓库。 如果使用仓库，则还必须指定标记。要删除 <code>custom-nginx:packaged</code> 镜像，可以执行以下命令：</p>
<pre><code>docker image rm custom-nginx:packaged

# Untagged: custom-nginx:packaged
# Deleted: sha256:f8837621b99d3388a9e78d9ce49fbb773017f770eea80470fb85e0052beae242
# Deleted: sha256:fdc6cdd8925ac25b9e0ed1c8539f96ad89ba1b21793d061e2349b62dd517dadf
# Deleted: sha256:c20e4aa46615fe512a4133089a5cd66f9b7da76366c96548790d5bf865bd49c4
# Deleted: sha256:6d6460a744475a357a2b631a4098aa1862d04510f3625feb316358536fcd8641
</code></pre>
<p>还可以使用 <code>image prune</code> 命令来清除所有未标记的挂起的镜像，如下所示：</p>
<pre><code>docker image prune --force

# Deleted Images:
# deleted: sha256:ba9558bdf2beda81b9acc652ce4931a85f0fc7f69dbc91b4efc4561ef7378aff
# deleted: sha256:ad9cc3ff27f0d192f8fa5fadebf813537e02e6ad472f6536847c4de183c02c81
# deleted: sha256:f1e9b82068d43c1bb04ff3e4f0085b9f8903a12b27196df7f1145aa9296c85e7
# deleted: sha256:ec16024aa036172544908ec4e5f842627d04ef99ee9b8d9aaa26b9c2a4b52baa

# Total reclaimed space: 59.19MB
</code></pre>
<p><code>--force</code>  或 <code>-f</code> 选项会跳过所有确认问题。也可以使用 <code>--all</code> 或  <code>-a</code>  选项删除本地仓库中的所有缓存镜像。</p>
<h3 id="docker">理解 Docker 镜像的分层</h3>
<p>从本书的开始，我就一直在说镜像是多层文件。在本小节中，我将演示镜像的各个层，以及它们如何在该镜像的构建过程中发挥重要作用。</p>
<p>在本演示中，我将使用上一小节的 <code>custom-nginx:packaged</code> 镜像。</p>
<p>要可视化镜像的多个层，可以使用 <code>image history</code> 命令。<code>custom-nginx:packaged</code> 图像的各个层可以如下所示：</p>
<pre><code>docker image history custom-nginx:packaged

# IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
# 7f16387f7307        5 minutes ago       /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon…   0B                             
# 587c805fe8df        5 minutes ago       /bin/sh -c apt-get update &amp;&amp;     apt-get ins…   60MB                
# 6fe4e51e35c1        6 minutes ago       /bin/sh -c #(nop)  EXPOSE 80                    0B                  
# d70eaf7277ea        17 hours ago        /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B                  
# &lt;missing&gt;           17 hours ago        /bin/sh -c mkdir -p /run/systemd &amp;&amp; echo 'do…   7B                  
# &lt;missing&gt;           17 hours ago        /bin/sh -c [ -z "$(apt-get indextargets)" ]     0B                  
# &lt;missing&gt;           17 hours ago        /bin/sh -c set -xe   &amp;&amp; echo '#!/bin/sh' &gt; /…   811B                
# &lt;missing&gt;           17 hours ago        /bin/sh -c #(nop) ADD file:435d9776fdd3a1834…   72.9MB
</code></pre>
<p>此镜像有八层。最上面的一层是最新的一层，当向下移动时，这些层会变老。最顶层是通常用于运行容器的那一层。</p>
<p>现在，让我们仔细看看从镜像 <code>d70eaf7277ea</code> 到镜像 <code>7f16387f7307</code> 的所有镜像。我将忽略 <code>IMAGE</code> 是 <code>&lt;missing&gt;</code> 的最下面的四层，因为它们与我们无关。</p>
<ul>
<li><code>d70eaf7277ea</code>  是由 <code>/bin/sh -c #(nop)  CMD ["/bin/bash"]</code> 创建的，它指示Ubuntu 内的默认 shell 已成功加载。</li>
<li><code>6fe4e51e35c1</code> 是由 <code>/bin/sh -c #(nop)  EXPOSE 80</code> 创建的，这是代码中的第二条指令。</li>
<li><code>587c805fe8df</code> 是由 <code>/bin/sh -c apt-get update &amp;&amp; apt-get install nginx -y &amp;&amp; apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*</code> 创建的，这是代码中的第三条指令。如果在执行此指令期间安装了所有必需的软件包，可以看到该镜像的大小为  <code>60MB</code>。</li>
<li>最后，最上层的 <code>7f16387f7307</code> 是由 <code>/bin/sh -c #(nop)  CMD ["nginx", "-g", "daemon off;"]</code> 创建的，它为该镜像设置了默认命令。</li>
</ul>
<p>如你所见，该镜像由许多只读层组成，每个层都记录了由某些指令触发的一组新的状态更改。当使用镜像启动容器时，会在其他层之上获得一个新的可写层。</p>
<p>每次使用 Docker 时都会发生这种分层现象，这是通过一个称为 union file system 的技术概念而得以实现的。 在这里，联合意味着集合论中的联合。根据 <a href="https://en.wikipedia.org/wiki/UnionFS">Wikipedia</a></p>
<blockquote>
<p>它允许透明地覆盖独立文件系统（称为分支）的文件和目录，从而形成单个一致的文件系统。合并分支内具有相同路径的目录的内容将在新的虚拟文件系统内的单个合并目录中一起看到。</p>
</blockquote>
<p>通过利用这一概念，Docker 可以避免数据重复，并且可以将先前创建的层用作以后构建的缓存。这样便产生了可在任何地方使用的紧凑，有效的镜像。</p>
<h3 id="nginx">怎样从源码构建 NGINX</h3>
<p>在上一小节中，了解了 <code>FROM</code>、<code>EXPOSE</code>、<code>RUN</code> 和 <code>CMD</code> 指令。在本小节中，将学到更多有关其他指令的信息。</p>
<p>在本小节中，将再次创建一个自定义的 NGINX 镜像。但是，不同之处在于，将从源代码构建 NGINX，而不是像上一个示例那样使用诸如 <code>apt-get</code> 之类的软件包管理器进行安装。</p>
<p>从源代码构建 NGINX，首先需要 NGINX 的源代码。 如果克隆了我的项目仓库，则会在 <code>custom-nginx</code> 目录中看到一个名为 <code>nginx-1.19.2.tar.gz</code> 的文件。将使用此归档文件作为构建 NGINX 的源。</p>
<p>在开始编写代码之前，先规划一下流程。 这次的镜像创建过程可以分七个步骤完成。如下：</p>
<ul>
<li>获得用于构建应用程序的基础镜像，例如 <a href="https://hub.docker.com/_/ubuntu">ubuntu</a>。</li>
<li>在基础镜像上安装必要的构建依赖项。</li>
<li>复制  <code>nginx-1.19.2.tar.gz</code>  文件到镜像里。</li>
<li>解压缩压缩包的内容并删除压缩包。</li>
<li>使用  <code>make</code> 工具配置构建，编译和安装程序。</li>
<li>删除解压缩的源代码。</li>
<li>运行<code>nginx</code>可执行文件。</li>
</ul>
<p>现在有了一个规划，让我们开始打开旧的 <code>Dockerfile</code> 并按如下所示更新其内容：</p>
<pre><code>FROM ubuntu:latest

RUN apt-get update &amp;&amp; \
    apt-get install build-essential\ 
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl-dev \
                    -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

COPY nginx-1.19.2.tar.gz .

RUN tar -xvf nginx-1.19.2.tar.gz &amp;&amp; rm nginx-1.19.2.tar.gz

RUN cd nginx-1.19.2 &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install

RUN rm -rf /nginx-1.19.2

CMD ["nginx", "-g", "daemon off;"]
</code></pre>
<p>如你所见，<code>Dockerfile</code> 中的代码反映了我上面提到的七个步骤。</p>
<ul>
<li><code>FROM</code> 指令将 Ubuntu 设置为基本映像，从而为构建任何应用程序提供了理想的环境。</li>
<li><code>RUN</code>  指令安装了从源代码构建 NGINX 所需的标准软件包。</li>
<li>这里的  <code>COPY</code>  指令是新的东西。该指令负责在映像内复制 <code>nginx-1.19.2.tar.gz</code> 文件。 <code>COPY</code> 指令的通用语法是  <code>COPY &lt;source&gt; &lt;destination&gt;</code>，其中 source 在本地文件系统中，而 destination 在镜像内部。作为目标的 <code>.</code> 表示镜像内的工作目录，除非另有设置，否则默认为 <code>/</code>。</li>
<li>这里的第二条 <code>RUN</code> 指令使用 <code>tar</code> 从压缩包中提取内容，然后将其删除。</li>
<li>存档文件包含一个名为 <code>nginx-1.19.2</code> 的目录，其中包含源代码。因此，下一步，将 <code>cd</code>  进入该目录并执行构建过程。 可以阅读 <a href="https://itsfoss.com/install-software-from-source-code/">How to Install Software from Source Code… and Remove it Afterwards</a> 文章，以了解有关该主题的更多信息。</li>
<li>构建和安装完成后，使用  <code>rm</code>  命令删除 <code>nginx-1.19.2</code> 目录。</li>
<li>在最后一步，像以前一样以单进程模式启动 NGINX。</li>
</ul>
<p>现在，要使用此代码构建镜像，请执行以下命令：</p>
<pre><code>docker image build --tag custom-nginx:built .

# Step 1/7 : FROM ubuntu:latest
#  ---&gt; d70eaf7277ea
# Step 2/7 : RUN apt-get update &amp;&amp;     apt-get install build-essential                    libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*
#  ---&gt; Running in 2d0aa912ea47
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 2d0aa912ea47
#  ---&gt; cbe1ced3da11
# Step 3/7 : COPY nginx-1.19.2.tar.gz .
#  ---&gt; 7202902edf3f
# Step 4/7 : RUN tar -xvf nginx-1.19.2.tar.gz &amp;&amp; rm nginx-1.19.2.tar.gz
 ---&gt; Running in 4a4a95643020
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 4a4a95643020
#  ---&gt; f9dec072d6d6
# Step 5/7 : RUN cd nginx-1.19.2 &amp;&amp;     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &amp;&amp;     make &amp;&amp; make install
#  ---&gt; Running in b07ba12f921e
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container b07ba12f921e
#  ---&gt; 5a877edafd8b
# Step 6/7 : RUN rm -rf /nginx-1.19.2
#  ---&gt; Running in 947e1d9ba828
# Removing intermediate container 947e1d9ba828
#  ---&gt; a7702dc7abb7
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 3110c7fdbd57
# Removing intermediate container 3110c7fdbd57
#  ---&gt; eae55f7369d3
# Successfully built eae55f7369d3
# Successfully tagged custom-nginx:built
</code></pre>
<p>这段代码还不错，但是我们可以在某些地方进行改进。</p>
<ul>
<li>可以使用  <code>ARG</code>  指令创建自变量，而不是像 <code>nginx-1.19.2.tar.gz</code> 这样的文件名进行硬编码。这样，只需更改参数即可更改版本或文件名。</li>
<li>可以让守护程序在构建过程中下载文件，而不是手动下载存档。还有另一种类似于<code>COPY</code>  的指令，称为 <code>ADD</code>  指令，该指令能够从互联网添加文件。</li>
</ul>
<p>打开 <code>Dockerfile</code> 文件，并按如下所示更新其内容：</p>
<pre><code>FROM ubuntu:latest

RUN apt-get update &amp;&amp; \
    apt-get install build-essential\ 
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl-dev \
                    -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"

ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .

RUN tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION}

RUN cd ${FILENAME} &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install

RUN rm -rf /${FILENAME}}

CMD ["nginx", "-g", "daemon off;"]
</code></pre>
<p>该代码几乎与先前的代码块相同，除了在第 13、14 行有一条名为 <code>ARG</code> 的新指令，以及在第 16 行用法了 <code>ADD</code> 指令。有关更新代码的说明如下：</p>
<ul>
<li><code>ARG</code> 指令可以像其他语言一样声明变量。以后可以使用 <code>${argument name}</code> 语法访问这些变量或参数。在这里，我将文件名 <code>nginx-1.19.2</code> 和文件扩展名 <code>tar.gz</code> 放在了两个单独的参数中。这样，我只需在一个地方进行更改就可以在 NGINX 的较新版本或存档格式之间进行切换。在上面的代码中，我向变量添加了默认值。变量值也可以作为 <code>image build</code> 命令的选项传递。你可以查阅<a href="https://docs.docker.com/engine/reference/builder/#arg">官方参考</a>了解更多详细信息。</li>
<li>在 <code>ADD</code> 指令中，我使用上面声明的参数动态形成了下载 URL。<code>https://nginx.org/download/${FILENAME}.${EXTENSION}</code> 行将在构建过程生成类似于 <code>https://nginx.org/download/nginx-1.19.2.tar.gz</code> 的内容。可以通过一次更改文件版本或扩展名的方式来更改文件版本或扩展名，这里要使用 <code>ARG</code> 指令。</li>
<li>默认情况下，<code>ADD</code> 指令不会提取从互联网获取的文件，因此在第18行使用了 <code>tar</code>。</li>
</ul>
<p>其余代码几乎不变。 现在应该可以自己理解参数的用法。最后，让我们尝试从此更新的代码构建镜像。</p>
<pre><code>docker image build --tag custom-nginx:built .

# Step 1/9 : FROM ubuntu:latest
#  ---&gt; d70eaf7277ea
# Step 2/9 : RUN apt-get update &amp;&amp;     apt-get install build-essential                    libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*
#  ---&gt; cbe1ced3da11
### LONG INSTALLATION STUFF GOES HERE ###
# Step 3/9 : ARG FILENAME="nginx-1.19.2"
#  ---&gt; Running in 33b62a0e9ffb
# Removing intermediate container 33b62a0e9ffb
#  ---&gt; fafc0aceb9c8
# Step 4/9 : ARG EXTENSION="tar.gz"
#  ---&gt; Running in 5c32eeb1bb11
# Removing intermediate container 5c32eeb1bb11
#  ---&gt; 36efdf6efacc
# Step 5/9 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================&gt;]  1.049MB/1.049MB
#  ---&gt; dba252f8d609
# Step 6/9 : RUN tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION}
#  ---&gt; Running in 2f5b091b2125
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 2f5b091b2125
#  ---&gt; 2c9a325d74f1
# Step 7/9 : RUN cd ${FILENAME} &amp;&amp;     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &amp;&amp;     make &amp;&amp; make install
#  ---&gt; Running in 11cc82dd5186
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 11cc82dd5186
#  ---&gt; 6c122e485ec8
# Step 8/9 : RUN rm -rf /${FILENAME}}
#  ---&gt; Running in 04102366960b
# Removing intermediate container 04102366960b
#  ---&gt; 6bfa35420a73
# Step 9/9 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 63ee44b571bb
# Removing intermediate container 63ee44b571bb
#  ---&gt; 4ce79556db1b
# Successfully built 4ce79556db1b
# Successfully tagged custom-nginx:built
</code></pre>
<p>现在可以使用 <code>custom-nginx:built</code> 镜像来运行容器了。</p>
<pre><code>docker container run --rm --detach --name custom-nginx-built --publish 8080:80 custom-nginx:built

# 90ccdbc0b598dddc4199451b2f30a942249d85a8ed21da3c8d14612f17eed0aa

docker container ls

# CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                  NAMES
# 90ccdbc0b598        custom-nginx:built   "nginx -g 'daemon of…"   2 minutes ago       Up 2 minutes        0.0.0.0:8080-&gt;80/tcp   custom-nginx-built
</code></pre>
<p>使用 <code>custom-nginx:built-v2</code> 映像的容器已成功运行。 现在可以从 <code>http://127.0.0.1:8080</code> 访问该容器。<br>
<img src="https://www.freecodecamp.org/news/content/images/2021/01/nginx-default.png" alt="nginx-default" width="600" height="400" loading="lazy"></p>
<p>这是 NGINX 的默认响应页面。可以访问<a href="https://docs.docker.com/engine/reference/builder/">官方参考</a>网站，以了解有关可用指令的更多信息。</p>
<h3 id="docker">怎样优化 Docker 镜像</h3>
<p>在上一个小节中构建的镜像具有功能，但是没有经过优化。为了证明我的观点，让我们使用  <code>image ls</code> 命令来查看镜像的大小：</p>
<pre><code>docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED          SIZE
# custom-nginx       built     1f3aaf40bb54   16 minutes ago   343MB
</code></pre>
<p>对于仅包含 NGINX 的镜像，这太大了。 如果拉取官方镜像并检查其大小，会看到它很小：</p>
<pre><code>docker image pull nginx:stable

# stable: Pulling from library/nginx
# a076a628af6f: Pull complete 
# 45d7b5d3927d: Pull complete 
# 5e326fece82e: Pull complete 
# 30c386181b68: Pull complete 
# b15158e9ebbe: Pull complete 
# Digest: sha256:ebd0fd56eb30543a9195280eb81af2a9a8e6143496accd6a217c14b06acd1419
# Status: Downloaded newer image for nginx:stable
# docker.io/library/nginx:stable

docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED          SIZE
# custom-nginx       built     1f3aaf40bb54   25 minutes ago   343MB
# nginx              stable    b9e1dc12387a   11 days ago      133MB
</code></pre>
<p>为了找出根本原因，让我们首先看一下 <code>Dockerfile</code>：</p>
<pre><code>FROM ubuntu:latest

RUN apt-get update &amp;&amp; \
    apt-get install build-essential\ 
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl-dev \
                    -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"

ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .

RUN tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION}

RUN cd ${FILENAME} &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install

RUN rm -rf /${FILENAME}}

CMD ["nginx", "-g", "daemon off;"]
</code></pre>
<p>正如在第 3 行看到的那样，<code>RUN</code> 指令安装了很多东西。尽管这些软件包对于从源代码构建 NGINX 是必需的，但对于运行它而言则不是必需的。</p>
<p>在安装的 6 个软件包中，只有两个是运行 NGINX 所必需的，即 <code>libpcre3</code> 和 <code>zlib1g</code>。 因此，一个更好的主意是在构建过程完成后，卸载其他软件包。</p>
<p>为此，请按如下所示更新的 <code>Dockerfile</code> ：</p>
<pre><code>FROM ubuntu:latest

EXPOSE 80

ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"

ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .

RUN apt-get update &amp;&amp; \
    apt-get install build-essential \ 
                    libpcre3 \
                    libpcre3-dev \
                    zlib1g \
                    zlib1g-dev \
                    libssl-dev \
                    -y &amp;&amp; \
    tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION} &amp;&amp; \
    cd ${FILENAME} &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install &amp;&amp; \
    cd / &amp;&amp; rm -rfv /${FILENAME} &amp;&amp; \
    apt-get remove build-essential \ 
                    libpcre3-dev \
                    zlib1g-dev \
                    libssl-dev \
                    -y &amp;&amp; \
    apt-get autoremove -y &amp;&amp; \
    apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

CMD ["nginx", "-g", "daemon off;"]
</code></pre>
<p>如你所见，在第 10 行上，一条 <code>RUN</code>  指令正在执行所有必要的核心操作。确切的事件链如下：</p>
<ul>
<li>从第 10 行到第 17 行，安装所有必需的软件包。</li>
<li>在第 18 行，将提取源代码，并删除下载的存档。</li>
<li>从第 19 行到第 28 行，NGINX 在系统上配置，构建和安装。</li>
<li>在第 29 行，从下载的档案中提取的文件将被删除。</li>
<li>从第 30 行到第 36 行，所有不必要的软件包都将被卸载并清除缓存。运行 NGINX 需要 <code>libpcre3</code> 和 <code>zlib1g</code> 包，因此我们保留了它们。</li>
</ul>
<p>你可能会问，为什么我要在一条  <code>RUN</code> 指令中做这么多工作，而不是像我们之前那样将它们很好地拆分成多个指令。 好吧，将它们拆分会是一个错误。</p>
<p>如果安装了软件包，然后按照单独的 <code>RUN</code> 说明将其删除，则它们将位于镜像的不同层中。尽管最终镜像不会包含已删除的包，但是由于它们存在于组成该图像的一层之一中，因此它们的大小仍将添加到最终镜像中。因此，请确保在单层上进行了此类更改。</p>
<p>让我们使用此 <code>Dockerfile</code> 来构建映像，并查看它们之间的区别。</p>
<pre><code>docker image build --tag custom-nginx:built .

# Sending build context to Docker daemon  1.057MB
# Step 1/7 : FROM ubuntu:latest
#  ---&gt; f63181f19b2f
# Step 2/7 : EXPOSE 80
#  ---&gt; Running in 006f39b75964
# Removing intermediate container 006f39b75964
#  ---&gt; 6943f7ef9376
# Step 3/7 : ARG FILENAME="nginx-1.19.2"
#  ---&gt; Running in ffaf89078594
# Removing intermediate container ffaf89078594
#  ---&gt; 91b5cdb6dabe
# Step 4/7 : ARG EXTENSION="tar.gz"
#  ---&gt; Running in d0f5188444b6
# Removing intermediate container d0f5188444b6
#  ---&gt; 9626f941ccb2
# Step 5/7 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================&gt;]  1.049MB/1.049MB
#  ---&gt; a8e8dcca1be8
# Step 6/7 : RUN apt-get update &amp;&amp;     apt-get install build-essential                     libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION} &amp;&amp;     cd ${FILENAME} &amp;&amp;     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &amp;&amp;     make &amp;&amp; make install &amp;&amp;     cd / &amp;&amp; rm -rfv /${FILENAME} &amp;&amp;     apt-get remove build-essential                     libpcre3-dev                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     apt-get autoremove -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*
#  ---&gt; Running in e5675cad1260
### LONG INSTALLATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container e5675cad1260
#  ---&gt; dc7e4161f975
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in b579e4600247
# Removing intermediate container b579e4600247
#  ---&gt; 512aa6a95a93
# Successfully built 512aa6a95a93
# Successfully tagged custom-nginx:built

docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED              SIZE
# custom-nginx       built     512aa6a95a93   About a minute ago   81.6MB
# nginx              stable    b9e1dc12387a   11 days ago          133MB
</code></pre>
<p>如你所见，镜像大小从 343MB 变为 81.6MB。官方镜像是 133MB。这是一个非常优化的构建，我们可以在下一部分中进一步介绍。</p>
<h3 id="alpinelinux">拥抱 Alpine Linux</h3>
<p>如果之前了解过 Docker，可能已经听说了 <a href="https://alpinelinux.org/">Alpine Linux</a>。 这是功能齐全的 <a href="https://en.wikipedia.org/wiki/Linux">Linux</a> 发行版，就像 <a href="https://ubuntu.com/">Ubuntu</a>、<a href="https://www.debian.org/">Debian</a> 或 <a href="https://getfedora.org/">Fedora</a>。</p>
<p>但是 Alpine 的好处是它是基于 <code>musl</code>，<code>libc</code> 和 <code>busybox</code> 构建的，并且是轻量级的。最新的 <a href="https://hub.docker.com/_/ubuntu">ubuntu</a> 镜像大约为 28MB，而 <a href="https://hub.docker.com/_/alpine">alpine</a> 仅为 2.8MB。</p>
<p>除了轻量之外，Alpine 还很安全，比其他发行版更适合创建容器。</p>
<p>尽管不像其他商业发行版那样用户友好，但是向 Alpine 的过渡仍然非常简单。在本小节中，将学习有关以 Alpine 镜像为基础重新创建 <code>custom-nginx</code> 镜像的信息。</p>
<p>打开的 <code>Dockerfile</code> 并更新其内容，如下所示：</p>
<pre><code>FROM alpine:latest

EXPOSE 80

ARG FILENAME="nginx-1.19.2"
ARG EXTENSION="tar.gz"

ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .

RUN apk add --no-cache pcre zlib &amp;&amp; \
    apk add --no-cache \
            --virtual .build-deps \
            build-base \ 
            pcre-dev \
            zlib-dev \
            openssl-dev &amp;&amp; \
    tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION} &amp;&amp; \
    cd ${FILENAME} &amp;&amp; \
    ./configure \
        --sbin-path=/usr/bin/nginx \
        --conf-path=/etc/nginx/nginx.conf \
        --error-log-path=/var/log/nginx/error.log \
        --http-log-path=/var/log/nginx/access.log \
        --with-pcre \
        --pid-path=/var/run/nginx.pid \
        --with-http_ssl_module &amp;&amp; \
    make &amp;&amp; make install &amp;&amp; \
    cd / &amp;&amp; rm -rfv /${FILENAME} &amp;&amp; \
    apk del .build-deps

CMD ["nginx", "-g", "daemon off;"]
</code></pre>
<p>除了几处更改外，代码几乎完全相同。我将列出更改并在进行过程中进行解释：</p>
<ul>
<li>我们不使用 <code>apt-get install</code> 来安装软件包，而是使用 <code>apk add</code>。<code>--no-cache</code> 选项意味着下载的软件包将不会被缓存。同样，我们将使用 <code>apk del</code> 代替 <code>apt-get remove</code> 来卸载软件包。</li>
<li><code>apk add</code> 命令的 <code>--virtual</code> 选项用于将一堆软件包捆绑到单个虚拟软件包中，以便于管理。仅用于构建程序所需的软件包被标记为  <code>.build-deps</code>，然后通过执行 <code>apk del .build-deps</code> 命令在第 29 行将其删除。可以在官方文档中了解有关 <a href="https://docs.alpinelinux.org/user-handbook/0.1a/Working/apk.html#_virtuals">virtuals</a> 的更多信息。</li>
<li>软件包名称在这里有些不同。通常，每个 Linux 发行版都有其软件包仓库，可供在其中搜索软件包的每个人使用。如果你知道某项任务所需的软件包，则可以直接转到指定发行版的仓库的并进行搜索。可以 <a href="https://pkgs.alpinelinux.org/packages">在此处了解 Alpine Linux软件包</a>。</li>
</ul>
<p>现在使用此 <code>Dockerfile</code> 构建一个新镜像，并查看文件大小的差异：</p>
<pre><code>docker image build --tag custom-nginx:built .

# Sending build context to Docker daemon  1.055MB
# Step 1/7 : FROM alpine:latest
#  ---&gt; 7731472c3f2a
# Step 2/7 : EXPOSE 80
#  ---&gt; Running in 8336cfaaa48d
# Removing intermediate container 8336cfaaa48d
#  ---&gt; d448a9049d01
# Step 3/7 : ARG FILENAME="nginx-1.19.2"
#  ---&gt; Running in bb8b2eae9d74
# Removing intermediate container bb8b2eae9d74
#  ---&gt; 87ca74f32fbe
# Step 4/7 : ARG EXTENSION="tar.gz"
#  ---&gt; Running in aa09627fe48c
# Removing intermediate container aa09627fe48c
#  ---&gt; 70cb557adb10
# Step 5/7 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================&gt;]  1.049MB/1.049MB
#  ---&gt; b9790ce0c4d6
# Step 6/7 : RUN apk add --no-cache pcre zlib &amp;&amp;     apk add --no-cache             --virtual .build-deps             build-base             pcre-dev             zlib-dev             openssl-dev &amp;&amp;     tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION} &amp;&amp;     cd ${FILENAME} &amp;&amp;     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &amp;&amp;     make &amp;&amp; make install &amp;&amp;     cd / &amp;&amp; rm -rfv /${FILENAME} &amp;&amp;     apk del .build-deps
#  ---&gt; Running in 0b301f64ffc1
### LONG INSTALLATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 0b301f64ffc1
#  ---&gt; dc7e4161f975
# Step 7/7 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in b579e4600247
# Removing intermediate container b579e4600247
#  ---&gt; 3e186a3c6830
# Successfully built 3e186a3c6830
# Successfully tagged custom-nginx:built

docker image ls

# REPOSITORY         TAG       IMAGE ID       CREATED         SIZE
# custom-nginx       built     3e186a3c6830   8 seconds ago   12.8MB
</code></pre>
<p>ubuntu 版本为 81.6MB，而 alpine 版本已降至 12.8MB，这是一个巨大的进步。除了 <code>apk</code> 软件包管理器外，Alpine 和 Ubuntu 还有一些其他的区别，但是没什么大不了的。遇到困难，可以搜索互联网。</p>
<h3 id="docker">怎样创建可执行 Docker 镜像</h3>
<p>在上一节中，使用了 <a href="https://hub.docker.com/r/fhsinchy/rmbyext">fhsinchy/rmbyext</a> 镜像。在本节中，将学习如何制作这样的可执行镜像。</p>
<p>首先，打开本书随附仓库的目录。<code>rmbyext</code> 应用程序的代码位于同名子目录中。</p>
<p>在开始使用 <code>Dockerfile</code> 之前，请花一点时间来规划最终的输出。我认为应该是这样的：</p>
<ul>
<li>该镜像应预安装 Python。</li>
<li>它应该包含 <code>rmbyext</code> 脚本的副本。</li>
<li>应该在将要执行脚本的地方设置一个工作目录。</li>
<li>应该将 <code>rmbyext</code> 脚本设置为入口点，以便镜像可以将扩展名用作参数。</li>
</ul>
<p>要构建上面提到的镜像，请执行以下步骤：</p>
<ul>
<li>获得可以运行 Python 脚本基础镜像，例如 <a href="https://hub.docker.com/_/python">python</a>。</li>
<li>将工作目录设置为易于访问的目录。</li>
<li>安装 Git，以便可以从我的 GitHub 仓库中安装脚本。</li>
<li>使用 Git 和 pip 安装脚本。</li>
<li>删除不必要的构建软件包。</li>
<li>将 <code>rmbyext</code> 设置为该图像的入口点。</li>
</ul>
<p>现在在 <code>rmbyext</code> 目录中创建一个新的 <code>Dockerfile</code>，并将以下代码放入其中：</p>
<pre><code>FROM python:3-alpine

WORKDIR /zone

RUN apk add --no-cache git &amp;&amp; \
    pip install git+https://github.com/fhsinchy/rmbyext.git
    apk del git

ENTRYPOINT [ "rmbyext" ]
</code></pre>
<p>该文件中的指令说明如下：</p>
<ul>
<li><code>FROM</code> 指令将 <a href="https://hub.docker.com/_/python">python</a> 设置为基本镜像，从而为运行 Python 脚本提供了理想的环境。<code>3-alpine</code> 标记表示需要 Python 3 的 Alpine 版本。</li>
<li>这里的 <code>WORKDIR</code> 指令将默认工作目录设置为 <code>/zone</code>。这里的工作目录名称完全是随机的。我发现 zone 是一个合适的名称，你也可以换成任何你想要的名称。</li>
<li>假设从 GitHub 安装了 <code>rmbyext</code> 脚本，则 <code>git</code> 是安装时的依赖项。第 5 行的 <code>RUN</code> 指令先安装 <code>git</code>，然后使用 Git 和 pip 安装  <code>rmbyext</code> 脚本。之后也删除了<code>git</code>。</li>
<li>最后，在第 9 行，<code>ENTRYPOINT</code> 指令将 <code>rmbyext</code> 脚本设置为该镜像的入口点。</li>
</ul>
<p>在整个文件中，第 9 行是将这个看似正常的镜像转换为可执行镜像的关键。现在要构建镜像，可以执行以下命令：</p>
<pre><code>docker image build --tag rmbyext .

# Sending build context to Docker daemon  2.048kB
# Step 1/4 : FROM python:3-alpine
# 3-alpine: Pulling from library/python
# 801bfaa63ef2: Already exists 
# 8723b2b92bec: Already exists 
# 4e07029ccd64: Already exists 
# 594990504179: Already exists 
# 140d7fec7322: Already exists 
# Digest: sha256:7492c1f615e3651629bd6c61777e9660caa3819cf3561a47d1d526dfeee02cf6
# Status: Downloaded newer image for python:3-alpine
#  ---&gt; d4d4f50f871a
# Step 2/4 : WORKDIR /zone
#  ---&gt; Running in 454374612a91
# Removing intermediate container 454374612a91
#  ---&gt; 7f7e49bc98d2
# Step 3/4 : RUN apk add --no-cache git &amp;&amp;     pip install git+https://github.com/fhsinchy/rmbyext.git#egg=rmbyext &amp;&amp;     apk del git
#  ---&gt; Running in 27e2e96dc95a
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 27e2e96dc95a
#  ---&gt; 3c7389432e36
# Step 4/4 : ENTRYPOINT [ "rmbyext" ]
#  ---&gt; Running in f239bbea1ca6
# Removing intermediate container f239bbea1ca6
#  ---&gt; 1746b0cedbc7
# Successfully built 1746b0cedbc7
# Successfully tagged rmbyext:latest

docker image ls

# REPOSITORY         TAG        IMAGE ID       CREATED         SIZE
# rmbyext            latest     1746b0cedbc7   4 minutes ago   50.9MB
</code></pre>
<p>这里在镜像名称之后没有提供任何标签，因此默认情况下该镜像已被标记为 <code>latest</code>。 应该能够像在上一节中看到的那样运行该镜像。请记住，参考你设置的实际镜像名称，而不是这里的 <code>fhsinchy/rmbyext</code>。</p>
<p>现在知道如何制作镜像了，是时候与全世界分享它们了。在线共享镜像很容易。所需要做的就是在任何在线仓库中注册一个帐户。在此处我将使用 <a href="https://hub.docker.com/">Docker Hub</a>。</p>
<p>导航到 <a href="https://hub.docker.com/signup">Sign Up</a> 页面并创建一个免费帐户。一个免费帐户可托管无限的公共仓库和一个私有仓库。</p>
<p>创建帐户后，需要使用 Docker CLI 登录。打开终端并执行以下命令：</p>
<pre><code>docker login

# Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
# Username: fhsinchy
# Password: 
# WARNING! Your password will be stored unencrypted in /home/fhsinchy/.docker/config.json.
# Configure a credential helper to remove this warning. See
# https://docs.docker.com/engine/reference/commandline/login/#credentials-store
#
# Login Succeeded
</code></pre>
<p>系统将提示输入用户名和密码。如果输入正确，则应该成功登录到你的帐户。</p>
<p>为了在线共享镜像，必须对镜像进行标记。已经在上一小节中学习了有关标记的信息。只是为了加深记忆，<code>--tag</code> 或 <code>-t</code> 选项的通用语法如下：</p>
<pre><code>--tag &lt;image repository&gt;:&lt;image tag&gt;
</code></pre>
<p>例如，让我们在线共享 <code>custom-nginx</code> 图像。 为此，请在 <code>custom-nginx</code> 项目目录中打开一个新的终端窗口。</p>
<p>要在线共享镜像，必须使用 <code>&lt;docker hub username&gt;/&lt;image name&gt;:&lt;image tag&gt;</code> 语法对其进行标记。我的用户名是 <code>fhsinchy</code>，因此命令如下所示：</p>
<pre><code>docker image build --tag fhsinchy/custom-nginx:latest --file Dockerfile.built .

# Step 1/9 : FROM ubuntu:latest
#  ---&gt; d70eaf7277ea
# Step 2/9 : RUN apt-get update &amp;&amp;     apt-get install build-essential                    libpcre3                     libpcre3-dev                     zlib1g                     zlib1g-dev                     libssl-dev                     -y &amp;&amp;     apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*
#  ---&gt; cbe1ced3da11
### LONG INSTALLATION STUFF GOES HERE ###
# Step 3/9 : ARG FILENAME="nginx-1.19.2"
#  ---&gt; Running in 33b62a0e9ffb
# Removing intermediate container 33b62a0e9ffb
#  ---&gt; fafc0aceb9c8
# Step 4/9 : ARG EXTENSION="tar.gz"
#  ---&gt; Running in 5c32eeb1bb11
# Removing intermediate container 5c32eeb1bb11
#  ---&gt; 36efdf6efacc
# Step 5/9 : ADD https://nginx.org/download/${FILENAME}.${EXTENSION} .
# Downloading [==================================================&gt;]  1.049MB/1.049MB
#  ---&gt; dba252f8d609
# Step 6/9 : RUN tar -xvf ${FILENAME}.${EXTENSION} &amp;&amp; rm ${FILENAME}.${EXTENSION}
#  ---&gt; Running in 2f5b091b2125
### LONG EXTRACTION STUFF GOES HERE ###
# Removing intermediate container 2f5b091b2125
#  ---&gt; 2c9a325d74f1
# Step 7/9 : RUN cd ${FILENAME} &amp;&amp;     ./configure         --sbin-path=/usr/bin/nginx         --conf-path=/etc/nginx/nginx.conf         --error-log-path=/var/log/nginx/error.log         --http-log-path=/var/log/nginx/access.log         --with-pcre         --pid-path=/var/run/nginx.pid         --with-http_ssl_module &amp;&amp;     make &amp;&amp; make install
#  ---&gt; Running in 11cc82dd5186
### LONG CONFIGURATION AND BUILD STUFF GOES HERE ###
# Removing intermediate container 11cc82dd5186
#  ---&gt; 6c122e485ec8
# Step 8/9 : RUN rm -rf /${FILENAME}}
#  ---&gt; Running in 04102366960b
# Removing intermediate container 04102366960b
#  ---&gt; 6bfa35420a73
# Step 9/9 : CMD ["nginx", "-g", "daemon off;"]
#  ---&gt; Running in 63ee44b571bb
# Removing intermediate container 63ee44b571bb
#  ---&gt; 4ce79556db1b
# Successfully built 4ce79556db1b
# Successfully tagged fhsinchy/custom-nginx:latest
</code></pre>
<p>在此命令中，<code>fhsinchy/custom-nginx</code> 是镜像仓库，而 <code>latest</code> 是标签。镜像名称可以是任何名称，上传镜像后即无法更改。可以随时更改标签，该标签通常反映软件的版本或其他类型的内部版本。</p>
<p>以 <code>node</code> 镜像为例。<code>node:lts</code> 镜像是指 Node.js 的长期支持版本，而 <code>node:lts-alpine</code> 版本是指为 Alpine Linux 构建的 Node.js 版本，它比常规版本小得多。</p>
<p>如果你给镜像添加任何标签，则会将其自动标记为 <code>latest</code>。但这并不意味着 <code>latest</code> 标签将始终引用最新版本。如果出于某种原因，将镜像的较旧版本明确标记为 <code>latest</code>，则 Docker 将不会做出任何额外的工作来进行交叉检查。</p>
<p>生成镜像后，可以通过执行以下命令来上传镜像：</p>
<pre><code>docker image push &lt;image repository&gt;:&lt;image tag&gt;
</code></pre>
<p>因此，在我这里，命令如下所示：</p>
<pre><code>docker image push fhsinchy/custom-nginx:latest

# The push refers to repository [docker.io/fhsinchy/custom-nginx]
# 4352b1b1d9f5: Pushed 
# a4518dd720bd: Pushed 
# 1d756dc4e694: Pushed 
# d7a7e2b6321a: Pushed 
# f6253634dc78: Mounted from library/ubuntu 
# 9069f84dbbe9: Mounted from library/ubuntu 
# bacd3af13903: Mounted from library/ubuntu 
# latest: digest: sha256:ffe93440256c9edb2ed67bf3bba3c204fec3a46a36ac53358899ce1a9eee497a size: 1788
</code></pre>
<p>根据镜像大小，上传可能需要一些时间。完成后，应该可以在中心配置文件页面中找到该镜像。</p>
<h2 id="javascript">如何容器化 JavaScript 应用程序</h2>
<p>现在，已经了解了创建镜像的知识，是时候做一些更相关的工作了。</p>
<p>在本小节中，将使用在之前小节上使用的 <a href="https://hub.docker.com/r/fhsinchy/hello-dock">fhsinchy/hello-dock</a> 镜像的源代码。在容器化这个非常简单的应用的过程中，介绍了 volumes  和多阶段构建，这是 Docker 中两个很重要的概念。</p>
<h3 id="dockerfile">如何编写开发 Dockerfile</h3>
<p>首先，打开用来克隆本书随附仓库的目录。<code>hello-dock</code> 应用程序的代码位于具有相同名称的子目录中。</p>
<p>这是一个非常简单的 JavaScript 项目，由 <a href="https://github.com/vitejs/vite">vitejs/vite</a> 项目构建。不过，请不要担心，无需了解 JavaScript 或 vite 即可学习本小节。了解 <a href="https://nodejs.org/">Node.js</a> 和 <a href="https://www.npmjs.com/">npm</a> 就足够了。</p>
<p>与上一部分中完成的其他项目一样，将从制定如何运行该应用程序的规划开始。如下：</p>
<ul>
<li>获得可以运行 JavaScript 应用程序的基础镜像，例如 <a href="https://hub.docker.com/_/node">node</a>。</li>
<li>在镜像内设置默认的工作目录。</li>
<li>将  <code>package.json</code> 文件复制到镜像中。</li>
<li>安装必要的依赖项。</li>
<li>复制其余的项目文件。</li>
<li>通过执行 <code>npm run dev</code> 命令来启动 <code>vite</code> 开发服务。</li>
</ul>
<p>该规划应始终来由应用程序的开发人员制定。如果你是开发人员，那么应该已经对如何运行此应用程序有正确的了解。</p>
<p>现在，如果将上述计划放入 <code>Dockerfile.dev</code> 中，则该文件应如下所示：</p>
<pre><code>FROM node:lts-alpine

EXPOSE 3000

USER node

RUN mkdir -p /home/node/app

WORKDIR /home/node/app

COPY ./package.json .
RUN npm install

COPY . .

CMD [ "npm", "run", "dev" ]
</code></pre>
<p>此代码的说明如下：</p>
<ul>
<li>这里的 <code>FROM</code> 指令将官方的 Node.js 镜像设置为基础镜像，从而可以运行 JavaScript 应用。<code>lts-alpine</code> 标签代表镜像要使用针对 Alpine 的长期支持版本。 可以在 <a href="https://hub.docker.com/_/node">node</a> 页面上找到该镜像的所有标签和其它必要的文档。</li>
<li><code>USER</code> 指令将镜像的默认用户设置为 <code>node</code>。 默认情况下，Docker 以 root 用户身份运行容器。 但是根据 <a href="https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md">Docker and Node.js Best Practices</a>，这有安全隐患。因此，最好是尽可能以非 root 用户身份运行。node 镜像附带一个名为 <code>node</code> 的非 root 用户，可以使用 <code>USER</code> 指令将其设置为默认用户。</li>
<li><code>RUN mkdir -p /home/node/app</code> 指令在 <code>node</code>  用户的主目录内创建一个名为 <code>app</code> 的目录。默认情况下，Linux 中任何非 root 用户的主目录通常是 <code>/home/&lt;user name&gt;</code>。</li>
<li>然后，<code>WORKDIR</code> 指令将默认工作目录设置为新创建的 <code>/home/node/app</code> 目录。 默认情况下，任何镜像的工作目录都是根目录。如果不希望在根目录中放置不必要的文件，可以将默认工作目录更改为更合理的目录，例如 <code>/home/node/app</code> 或你喜欢的任何目录。该工作目录将适用于任何连续的 <code>COPY</code>、<code>ADD</code>、<code>RUN</code> 和 <code>CMD</code> 指令。</li>
<li>此处的 <code>COPY</code> 指令复制了 <code>package.json</code>文件，该文件包含有关此应用程序所有必需依赖项的信息。<code>RUN</code> 指令执行 <code>npm install</code> 命令，这是在 Node.js 项目中使用 <code>package.json</code> 文件安装依赖项的默认命令。 最后的  <code>.</code>  代表工作目录。</li>
<li>第二条 <code>COPY</code> 指令将其余内容从主机文件系统的当前目录（<code>.</code>）复制到镜像内的工作目录（<code>.</code>）。</li>
<li>最后，这里的 <code>CMD</code> 指令为该镜像设置了默认命令，即以 <code>exec</code> 形式编写的 <code>npm run dev</code>。</li>
<li>默认情况下，<code>vite</code> 开发服务器在端口 <code>3000</code> 上运行，最好添加一个 <code>EXPOSE</code> 命令。</li>
</ul>
<p>现在，要由此  <code>Dockerfile.dev</code> 构建像镜，可以执行以下命令：</p>
<pre><code>docker image build --file Dockerfile.dev --tag hello-dock:dev .

# Step 1/7 : FROM node:lts
#  ---&gt; b90fa0d7cbd1
# Step 2/7 : EXPOSE 3000
#  ---&gt; Running in 722d639badc7
# Removing intermediate container 722d639badc7
#  ---&gt; e2a8aa88790e
# Step 3/7 : WORKDIR /app
#  ---&gt; Running in 998e254b4d22
# Removing intermediate container 998e254b4d22
#  ---&gt; 6bd4c42892a4
# Step 4/7 : COPY ./package.json .
#  ---&gt; 24fc5164a1dc
# Step 5/7 : RUN npm install
#  ---&gt; Running in 23b4de3f930b
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 23b4de3f930b
#  ---&gt; c17ecb19a210
# Step 6/7 : COPY . .
#  ---&gt; afb6d9a1bc76
# Step 7/7 : CMD [ "npm", "run", "dev" ]
#  ---&gt; Running in a7ff529c28fe
# Removing intermediate container a7ff529c28fe
#  ---&gt; 1792250adb79
# Successfully built 1792250adb79
# Successfully tagged hello-dock:dev
</code></pre>
<p>如果文件名不是  <code>Dockerfile</code>，则必须使用 <code>--file</code> 选项显式传递文件名。 通过执行以下命令，可以使用此镜像运行容器：</p>
<pre><code>docker container run \
    --rm \
    --detach \
    --publish 3000:3000 \
    --name hello-dock-dev \
    hello-dock:dev

# 21b9b1499d195d85e81f0e8bce08f43a64b63d589c5f15cbbd0b9c0cb07ae268
</code></pre>
<p>现在访问 <code>http://127.0.0.1:3000</code>，可以看到 <code>hello-dock</code> 应用程序。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/hello-dock-dev.png" alt="hello-dock-dev" width="600" height="400" loading="lazy"></p>
<p>恭喜你在容器内运行了你的第一个实际应用程序。刚刚编写的代码还可以，但是它存在一个大问题，可以在某些地方进行改进。让我们先从问题开始。</p>
<h3 id="dockerbindmounts">如何在 Docker 中使用 Bind Mounts</h3>
<p>如果你以前使用过任何前端 JavaScript 框架，则应该知道这些框架中的开发服务器通常带有热重载功能。也就是说，如果对代码进行更改，服务器将重新加载，并自动反映立即进行的所有更改。</p>
<p>但是，如果现在对代码进行任何更改，将不会在浏览器中运行任何应用程序。这是因为正在更改本地文件系统中的代码，但是在浏览器中看到的应用程序位于容器文件系统中。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/local-vs-container-file-system.svg" alt="local-vs-container-file-system" width="600" height="400" loading="lazy"></p>
<p>要解决此问题，可以再次使用 <a href="https://docs.docker.com/storage/bind-mounts/">绑定挂载</a>。 使用绑定挂载，可以轻松地在容器内安装本地文件系统目录。绑定挂载可以直接从容器内部引用本地文件系统，而无需复制本地文件。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/bind-mounts.svg" alt="bind-mounts" width="600" height="400" loading="lazy"></p>
<p>这样，对本地源代码所做的任何更改都会及时反映在容器内部，从而触发 <code>vite</code> 开发服务器的热重载功能。对容器内部文件系统所做的更改也将反映在本地文件系统上。</p>
<p>已经在<a href="https://www.freecodecamp.org/news/the-docker-handbook/#working-with-executable-images">使用可执行镜像</a>小节中学习到，可以对  <code>container run</code> 或 <code>container start</code> 命令使用 <code>--volume</code> 或 <code>-v</code> 选项创建绑定挂载。 回顾一下，通用语法如下：</p>
<pre><code>--volume &lt;local file system directory absolute path&gt;:&lt;container file system directory absolute path&gt;:&lt;read write access&gt;
</code></pre>
<p>停止先前启动的 <code>hello-dock-dev</code> 容器，并通过执行以下命令来启动新的容器：</p>
<pre><code>docker container run \
    --rm \
    --publish 3000:3000 \
    --name hello-dock-dev \
    --volume $(pwd):/home/node/app \
    hello-dock:dev

# sh: 1: vite: not found
# npm ERR! code ELIFECYCLE
# npm ERR! syscall spawn
# npm ERR! file sh
# npm ERR! errno ENOENT
# npm ERR! hello-dock@0.0.0 dev: `vite`
# npm ERR! spawn ENOENT
# npm ERR!
# npm ERR! Failed at the hello-dock@0.0.0 dev script.
# npm ERR! This is probably not a problem with npm. There is likely additional logging output above.
# npm WARN Local package.json exists, but node_modules missing, did you mean to install?
</code></pre>
<p>请记住，我省略了 <code>--detach</code> 选项，这只是说明一个非常重要的观点。如你所见，该应用程序现在根本没有运行。</p>
<p>这是因为尽管 volume 解决了热重载的问题，但它引入了另一个问题。如果你以前有过使用 Node.js 的经验，你可能会知道 Node.js 项目的依赖项位于项目根目录的 <code>node_modules</code> 目录中。</p>
<p>现在，将项目根目录作为容器中的 volume  安装在本地文件系统上，容器中的内容将被包含所有依赖项的 <code>node_modules</code> 目录替换。 这意味着 <code>vite</code> 软件包不见了。</p>
<h3 id="docker">如何在 Docker 中使用匿名卷</h3>
<p>可以使用匿名卷解决此问题。匿名卷除了无需在此处指定源目录之外，与绑定挂载相同。 创建匿名卷的通用语法如下：</p>
<pre><code>--volume &lt;container file system directory absolute path&gt;:&lt;read write access&gt;
</code></pre>
<p>因此，用两个卷启动 <code>hello-dock</code> 容器的最终命令应如下：</p>
<pre><code>docker container run \
    --rm \
    --detach \
    --publish 3000:3000 \
    --name hello-dock-dev \
    --volume $(pwd):/home/node/app \
    --volume /home/node/app/node_modules \
    hello-dock:dev

# 53d1cfdb3ef148eb6370e338749836160f75f076d0fbec3c2a9b059a8992de8b
</code></pre>
<p>在这里，Docker 将从容器内部获取整个 <code>node_modules</code> 目录，并将其存放在主机文件系统上由 Docker 守护程序管理的其他目录中，并将该目录作为 <code>node_modules</code> 挂载在容器中。</p>
<h3 id="docker">如何在 Docker 中执行多阶段构建</h3>
<p>到目前为止，在本节中，已经构建了用于在开发模式下运行 JavaScript 应用程序的镜像。现在，如果要在生产模式下构建镜像，会有一些新的挑战。</p>
<p>在开发模式下，<code>npm run serve</code> 命令启动一个开发服务器，该服务器将应用程序提供给用户。该服务器不仅提供文件，还提供热重载功能。</p>
<p>在生产模式下，<code>npm run build</code> 命令将所有 JavaScript 代码编译为一些静态 HTML、CSS 和 JavaScript 文件。要运行这些文件，不需要 node 或任何其他运行时依赖项。只需要一个像 <code>nginx</code>  这样的服务器。</p>
<p>要在应用程序以生产模式运行时创建镜像，可以执行以下步骤：</p>
<ul>
<li>使用 <code>node</code> 作为基础镜像并构建应用程序。</li>
<li>在 node 镜像中安装 <code>nginx</code> 并使用它来提供静态文件。</li>
</ul>
<p>这种方法是完全有效的。但是问题在于，<code>node</code> 镜像很大，并且它所承载的大多数内容对于静态文件服务而言都是不必要的。解决此问题的更好方法如下：</p>
<ul>
<li>使用<code>node</code>图像作为基础并构建应用程序。</li>
<li>将使用 <code>node</code> 镜像创建的文件复制到 <code>nginx</code> 映像。</li>
<li>根据 <code>nginx</code> 创建最终镜像，并丢弃所有与 <code>node</code> 相关的东西。</li>
</ul>
<p>这样，镜像仅包含所需的文件，变得非常方便。</p>
<p>这种方法是一个多阶段构建。要执行这样的构建，在 <code>hello-dock</code> 项目目录中创建一个新的 <code>Dockerfile</code>，并将以下内容放入其中：</p>
<pre><code>FROM node:lts-alpine as builder

WORKDIR /app

COPY ./package.json ./
RUN npm install

COPY . .
RUN npm run build

FROM nginx:stable-alpine

EXPOSE 80

COPY --from=builder /app/dist /usr/share/nginx/html
</code></pre>
<p>如你所见，<code>Dockerfile</code> 看起来很像以前的 Dockerfile，但有一些不同之处。该文件的解释如下：</p>
<ul>
<li>第 1 行使用 <code>node:lts-alpine</code> 作为基础镜像开始构建的第一阶段。<code>as builder</code> 语法为此阶段分配一个名称，以便以后可以引用。</li>
<li>从第 3 行到第 13 行，以前已经看过很多次了。实际上，<code>RUN npm run build</code> 命令会编译整个应用程序，并将其存放在 <code>/app/dist</code> 目录中，其中 <code>/app</code> 是工作目录，<code>/dist</code>  是 <code>vite</code> 应用程序的默认输出目录。</li>
<li>第 15 行使用 <code>nginx:stable-alpine</code> 作为基础镜像开始构建的第二阶段。</li>
<li>NGINX 服务器默认在端口 80 上运行，因此添加了 <code>EXPOSE 80</code> 行。</li>
<li>最后一行是 <code>COPY</code> 指令。<code>--from=builder</code> 部分表示要从 <code>builder</code> 阶段复制一些文件。之后，这是一条标准的复制指令，其中 <code>/app/dist</code> 是 source，而 <code>/usr/share/nginx/html</code>  是 destination。 这里使用的 destination 是 NGINX 的默认站点路径，因此放置在其中的任何静态文件都将自动提供。</li>
</ul>
<p>如你所见，生成的镜像是基于 <code>nginx</code> 的镜像，仅包含运行应用程序所需的文件。要构建此镜像，请执行以下命令：</p>
<pre><code>docker image build --tag hello-dock:prod .

# Step 1/9 : FROM node:lts-alpine as builder
#  ---&gt; 72aaced1868f
# Step 2/9 : WORKDIR /app
#  ---&gt; Running in e361c5c866dd
# Removing intermediate container e361c5c866dd
#  ---&gt; 241b4b97b34c
# Step 3/9 : COPY ./package.json ./
#  ---&gt; 6c594c5d2300
# Step 4/9 : RUN npm install
#  ---&gt; Running in 6dfabf0ee9f8
# npm WARN deprecated fsevents@2.1.3: Please update to v 2.2.x
#
# &gt; esbuild@0.8.29 postinstall /app/node_modules/esbuild
# &gt; node install.js
#
# npm notice created a lockfile as package-lock.json. You should commit this file.
# npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@~2.1.2 (node_modules/chokidar/node_modules/fsevents):
# npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.1.3: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
# npm WARN hello-dock@0.0.0 No description
# npm WARN hello-dock@0.0.0 No repository field.
# npm WARN hello-dock@0.0.0 No license field.
#
# added 327 packages from 301 contributors and audited 329 packages in 35.971s
#
# 26 packages are looking for funding
#   run `npm fund` for details
#
# found 0 vulnerabilities
#
# Removing intermediate container 6dfabf0ee9f8
#  ---&gt; 21fd1b065314
# Step 5/9 : COPY . .
#  ---&gt; 43243f95bff7
# Step 6/9 : RUN npm run build
#  ---&gt; Running in 4d918cf18584
#
# &gt; hello-dock@0.0.0 build /app
# &gt; vite build
#
# - Building production bundle...
#
# [write] dist/index.html 0.39kb, brotli: 0.15kb
# [write] dist/_assets/docker-handbook-github.3adb4865.webp 12.32kb
# [write] dist/_assets/index.eabcae90.js 42.56kb, brotli: 15.40kb
# [write] dist/_assets/style.0637ccc5.css 0.16kb, brotli: 0.10kb
# - Building production bundle...
#
# Build completed in 1.71s.
#
# Removing intermediate container 4d918cf18584
#  ---&gt; 187fb3e82d0d
# Step 7/9 : EXPOSE 80
#  ---&gt; Running in b3aab5cf5975
# Removing intermediate container b3aab5cf5975
#  ---&gt; d6fcc058cfda
# Step 8/9 : FROM nginx:stable-alpine
# stable: Pulling from library/nginx
# 6ec7b7d162b2: Already exists 
# 43876acb2da3: Pull complete 
# 7a79edd1e27b: Pull complete 
# eea03077c87e: Pull complete 
# eba7631b45c5: Pull complete 
# Digest: sha256:2eea9f5d6fff078ad6cc6c961ab11b8314efd91fb8480b5d054c7057a619e0c3
# Status: Downloaded newer image for nginx:stable
#  ---&gt; 05f64a802c26
# Step 9/9 : COPY --from=builder /app/dist /usr/share/nginx/html
#  ---&gt; 8c6dfc34a10d
# Successfully built 8c6dfc34a10d
# Successfully tagged hello-dock:prod
</code></pre>
<p>生成镜像后，可以通过执行以下命令来运行新容器：</p>
<pre><code>docker container run \
    --rm \
    --detach \
    --name hello-dock-prod \
    --publish 8080:80 \
    hello-dock:prod

# 224aaba432bb09aca518fdd0365875895c2f5121eb668b2e7b2d5a99c019b953
</code></pre>
<p>正在运行的应用程序应位于 <code>http://127.0.0.1:8080</code> 上：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/hello-dock.png" alt="hello-dock" width="600" height="400" loading="lazy"></p>
<p>在这里，可以看到我所有的 <code>hello-dock</code> 应用程序。 如果要构建具有大量依赖关系的大型应用程序，那么多阶段构建可能会非常有用。如果配置正确，则可以很好地优化和压缩分多个阶段构建的镜像。</p>
<h3 id="">如何忽略不必要的文件</h3>
<p>如果了解 <code>git</code>，你可能会知道项目中的 <code>.gitignore</code> 文件。 这些文件包含要从仓库中排除的文件和目录的列表。</p>
<p>嗯，Docker 也有类似的概念。<code>.dockerignore</code> 文件包含要从镜像构建中排除的文件和目录的列表。可以在 <code>hello-dock</code> 目录中有一个预先创建的 <code>.dockerignore</code> 文件。</p>
<pre><code>.git
*Dockerfile*
*docker-compose*
node_modules
</code></pre>
<p>该 <code>.dockerignore</code> 文件必须位于构建上下文中。这里提到的文件和目录将被 <code>COPY</code> 指令忽略。但是，如果执行绑定挂载，则 <code>.dockerignore</code> 文件将对此无效。我已经在项目仓库中的必要位置添加了 <code>.dockerignore</code> 文件。</p>
<h2 id="docker">Docker 中的网络操作基础知识</h2>
<p>到目前为止，在本书中，仅处理了单个容器项目。但是在实际应用中，多数项目都具有多个容器。老实说，如果不了解容器隔离的细微差别，使用一堆容器可能会有些困难。</p>
<p>因此，在本书的这一部分中，将介绍 Docker 的基本网络，并涉及一个小型的多容器项目。</p>
<p>好了，已经在上一节中了解到容器是隔离的环境。现在考虑一个场景，其中有一个基于 <a href="https://expressjs.com/">Express.js</a>  <code>notes-api</code> 应用程序和一个 <a href="https://www.postgresql.org/">PostgreSQL</a> 数据库服务，他们在两个单独的容器中运行。</p>
<p>这两个容器彼此完全隔离，并且彼此无关。<strong>那么如何连接两者？ 将是一个挑战。</strong></p>
<p>你可能会想到针对此问题的两种可能的解决方案。 它们如下：</p>
<ul>
<li>使用暴露的端口访问数据库服务。</li>
<li>使用其 IP 地址和默认端口访问数据库服务。</li>
</ul>
<p>第一个涉及从 <code>postgres</code> 容器暴露一个端口，<code>notes-api</code> 将通过该端口进行连接。假设来自 <code>postgres</code> 容器的暴露端口为 5432。现在，如果尝试从 <code>notes-api</code> 容器内部连接到 <code>127.0.0.1:5432</code>，会发现 <code>notes-api</code> 根本找不到数据库服务。</p>
<p>原因是，在 <code>notes-api</code> 容器内的 <code>127.0.0.1</code> 时，只是代表当前容器的 <code>localhost</code> 。<code>postgres</code> 服务根本不存在。结果是，<code>notes-api</code> 应用程序无法连接。</p>
<p>你可能想到的第二个解决方案找到  <code>postgres</code> 容器的确切 IP 地址，使用 <code>container inspect</code> 命令并将其与端口一起使用。 假设 <code>postgres</code>  容器的名称为 <code>notes-api-db-server</code> ，则可以通过执行以下命令轻松获得 IP 地址：</p>
<pre><code>docker container inspect --format='{{range .NetworkSettings.Networks}} {{.IPAddress}} {{end}}' notes-api-db-server

#  172.17.0.2
</code></pre>
<p>现在假设 <code>postgres</code> 的默认端口是 <code>5432</code>，可以通过从 <code>notes-api</code> 容器连接到 <code>172.17.0.2:5432</code> 来非常容易地访问数据库服务。</p>
<p>这种方法也存在问题。 不建议使用 IP 地址来引用容器。另外，如果容器被破坏并重新创建，则 IP 地址可能会更改。跟踪这些不断变化的 IP 地址可能非常麻烦。</p>
<p>现在，我已经排除了对原始问题的可能错误答案，正确的答案是，<strong>将它们放置在用户定义的桥接网络下即可将它们连接起来。</strong></p>
<h3 id="docker">Docker 网络基础</h3>
<p>Docker 中的网络是另一个逻辑对象，和容器和镜像一样。就像其他两个一样，在 <code>docker network</code> 组下有很多用于操纵网络的命令。</p>
<p>要列出系统中的网络，请执行以下命令：</p>
<pre><code>docker network ls

# NETWORK ID     NAME      DRIVER    SCOPE
# c2e59f2b96bd   bridge    bridge    local
# 124dccee067f   host      host      local
# 506e3822bf1f   none      null      local
</code></pre>
<p>你应该在系统中看到三个网络。现在在这里查看表的 <code>DRIVER</code> 列。这些 drivers 可以视为网络类型。</p>
<p>默认情况下，Docker 具有五类网络驱动。它们如下：</p>
<ul>
<li><code>bridge</code> - Docker 中的默认网络驱动程序。当多个容器以标准模式运行并且需要相互通信时，可以使用此方法。</li>
<li><code>host</code> -  完全消除网络隔离。在 <code>host</code>  网络下运行的任何容器基本上都连接到主机系统的网络。</li>
<li><code>none</code> - 此驱动程序完全禁用容器的联网。 我还没有找到其应用场景。</li>
<li><code>overlay</code> - 这用于跨计算机连接多个 Docker 守护程序，这超出了本书的范围。</li>
<li><code>macvlan</code> - 允许将 MAC 地址分配给容器，使它们的功能类似于网络中的物理设备。</li>
</ul>
<p>也有第三方插件，可让你将 Docker 与专用网络堆栈集成。在上述五种方法中，本书仅使用 <code>bridge</code> 网络驱动程序。</p>
<h3 id="docker">如何在 Docker 中创建用户定义的桥接网络</h3>
<p>在开始创建自己的桥接网络之前，我想花一些时间来讨论 Docker 随附的默认桥接网络。让我们首先列出系统上的所有网络：</p>
<pre><code>docker network ls

# NETWORK ID     NAME      DRIVER    SCOPE
# c2e59f2b96bd   bridge    bridge    local
# 124dccee067f   host      host      local
# 506e3822bf1f   none      null      local
</code></pre>
<p>如你所见，Docker 随附了一个名为 <code>bridge</code> 的默认桥接网络。 运行的任何容器都将自动连接到此网桥网络：</p>
<pre><code>docker container run --rm --detach --name hello-dock --publish 8080:80 fhsinchy/hello-dock
# a37f723dad3ae793ce40f97eb6bb236761baa92d72a2c27c24fc7fda0756657d

docker network inspect --format='{{range .Containers}}{{.Name}}{{end}}' bridge
# hello-dock
</code></pre>
<p>连接到默认桥接网络的容器可以使用我在上一小节中不鼓励使用的 IP 地址相互通信。</p>
<p>但是，用户定义的桥接网络比默认桥接网络多一些额外的功能。根据有关此主题的官方 <a href="https://docs.docker.com/network/bridge/#differences-between-user-defined-bridges-and-the-default-bridge">docs</a>，一些值得注意的额外功能如下：</p>
<ul>
<li><strong>用户定义的网桥可在容器之间提供自动 DNS 解析：</strong> 这意味着连接到同一网络的容器可以使用容器名称相互通信。 因此，如果你有两个名为 <code>notes-api</code> 和 <code>notes-db</code> 的容器，则 API 容器将能够使用 <code>notes-db</code> 名称连接到数据库容器。</li>
<li><strong>用户定义的网桥提供更好的隔离性：</strong> 默认情况下，所有容器都连接到默认桥接网络，这可能会导致它们之间的冲突。将容器连接到用户定义的桥可以确保更好的隔离。</li>
<li><strong>容器可以即时与用户定义的网络连接和分离：</strong> 在容器的生命周期内，可以即时将其与用户定义的网络连接或断开连接。要从默认网桥网络中删除容器，需要停止容器并使用其他网络选项重新创建它。</li>
</ul>
<p>既然已经了解了很多有关用户定义的网络的知识，那么现在该为自己创建一个了。可以使用 <code>network create</code> 命令创建网络。该命令的通用语法如下：</p>
<pre><code>docker network create &lt;network name&gt;
</code></pre>
<p>要创建名称为 <code>skynet</code> 的网络，请执行以下命令：</p>
<pre><code>docker network create skynet

# 7bd5f351aa892ac6ec15fed8619fc3bbb95a7dcdd58980c28304627c8f7eb070

docker network ls

# NETWORK ID     NAME     DRIVER    SCOPE
# be0cab667c4b   bridge   bridge    local
# 124dccee067f   host     host      local
# 506e3822bf1f   none     null      local
# 7bd5f351aa89   skynet   bridge    local
</code></pre>
<p>如你所见，已经使用给定名称创建了一个新网络。当前没有容器连接到该网络。在下一小节中，将学习有关将容器连接到网络的信息。</p>
<h3 id="docker">如何在 Docker 中将容器连接到网络</h3>
<p>将容器连接到网络的方式主要有两种。首先，可以使用 network connect 命令将容器连接到网络。该命令的通用语法如下：</p>
<pre><code>docker network connect &lt;network identifier&gt; &lt;container identifier&gt;
</code></pre>
<p>要将 <code>hello-dock</code> 容器连接到 <code>skynet</code> 网络，可以执行以下命令：</p>
<pre><code>docker network connect skynet hello-dock

docker network inspect --format='{{range .Containers}} {{.Name}} {{end}}' skynet

#  hello-dock

docker network inspect --format='{{range .Containers}} {{.Name}} {{end}}' bridge

#  hello-dock
</code></pre>
<p>从两个  <code>network inspect</code>  命令的输出中可以看到，<code>hello-dock</code> 容器现在已连接到 <code>skynet</code> 和默认的 <code>bridge</code> 网络。</p>
<p>将容器连接到网络的第二种方法是对 <code>container run</code>  或 <code>container create</code> 命令使用 <code>--network</code> 选项。 该选项的通用语法如下：</p>
<pre><code>--network &lt;network identifier&gt;
</code></pre>
<p>要运行连接到同一网络的另一个 <code>hello-dock</code> 容器，可以执行以下命令：</p>
<pre><code>docker container run --network skynet --rm --name alpine-box -it alpine sh

# lands you into alpine linux shell

/ # ping hello-dock

# PING hello-dock (172.18.0.2): 56 data bytes
# 64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.191 ms
# 64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.103 ms
# 64 bytes from 172.18.0.2: seq=2 ttl=64 time=0.139 ms
# 64 bytes from 172.18.0.2: seq=3 ttl=64 time=0.142 ms
# 64 bytes from 172.18.0.2: seq=4 ttl=64 time=0.146 ms
# 64 bytes from 172.18.0.2: seq=5 ttl=64 time=0.095 ms
# 64 bytes from 172.18.0.2: seq=6 ttl=64 time=0.181 ms
# 64 bytes from 172.18.0.2: seq=7 ttl=64 time=0.138 ms
# 64 bytes from 172.18.0.2: seq=8 ttl=64 time=0.158 ms
# 64 bytes from 172.18.0.2: seq=9 ttl=64 time=0.137 ms
# 64 bytes from 172.18.0.2: seq=10 ttl=64 time=0.145 ms
# 64 bytes from 172.18.0.2: seq=11 ttl=64 time=0.138 ms
# 64 bytes from 172.18.0.2: seq=12 ttl=64 time=0.085 ms

--- hello-dock ping statistics ---
13 packets transmitted, 13 packets received, 0% packet loss
round-trip min/avg/max = 0.085/0.138/0.191 ms
</code></pre>
<p>如你所见，从 <code>alpine-box</code> 容器内部运行 <code>ping hello-dock</code> 是可行的，因为这两个容器都在同一用户定义的网桥网络下，并且自动 DNS 解析有效。</p>
<p>但是请记住，为了使自动 DNS 解析正常工作，必须为容器分配自定义名称。使用随机生成的名称将不起作用。</p>
<h3 id="docker">如何在 Docker 中从网络分离容器</h3>
<p>在上一小节中，了解了有关将容器连接到网络的信息。在本小节中，将学习如何分离它们。</p>
<p>可以使用 <code>network disconnect</code> 命令来执行此任务。该命令的通用语法如下：</p>
<pre><code>docker network disconnect &lt;network identifier&gt; &lt;container identifier&gt;
</code></pre>
<p>要从 <code>skynet</code> 网络分离 <code>hello-dock</code> 容器，可以执行以下命令：</p>
<pre><code>docker network disconnect skynet hello-dock
</code></pre>
<p>就像 <code>network connect</code> 命令一样，<code>network disconnect</code>  命令也不给出任何输出。</p>
<h3 id="docker">如何删除 Docker 中的网络</h3>
<p>就像 Docker 中的其他逻辑对象一样，可以使用 <code>network rm</code> 命令删除网络。该命令的通用语法如下：</p>
<pre><code>docker network rm &lt;network identifier&gt;
</code></pre>
<p>要从系统中删除 <code>skynet</code> 网络，可以执行以下命令：</p>
<pre><code>docker network rm skynet
</code></pre>
<p>也可以使用 <code>network prune</code> 命令从系统中删除所有未使用的网络。该命令还具有 <code>-f</code> 或 <code>--force</code> 和 <code>-a</code> 或 <code>--all</code> 选项。</p>
<h2 id="javascript">如何容器化多容器 JavaScript 应用程序</h2>
<p>既然已经对 Docker 中的网络有了足够的了解，那么在本节中，将学习如何将成熟的多容器项目容器化。涉及的项目是一个基于 Express.js 和 PostgreSQL 的简单 <code>notes-api</code>。</p>
<p>在此项目中，需要使用网络连接两个容器。除此之外，还将学习诸如环境变量和命名卷之类的概念。因此，事不宜迟，让我们直接开始。</p>
<h3 id="">如何运行数据库服务</h3>
<p>该项目中的数据库服务器是一个简单的 PostgreSQL 服务，使用官方的 <a href="https://hub.docker.com/_/postgres">postgres</a> 镜像。</p>
<p>根据官方文档，为了使用此镜像运行容器，必须提供 <code>POSTGRES_PASSWORD</code> 环境变量。除此之外，还将使用 <code>POSTGRES_DB</code> 环境变量为默认数据库提供一个名称。默认情况下，PostgreSQL 监听 <code>5432</code> 端口，因此也需要公开它。</p>
<p>要运行数据库服务，可以执行以下命令：</p>
<pre><code>docker container run \
    --detach \
    --name=notes-db \
    --env POSTGRES_DB=notesdb \
    --env POSTGRES_PASSWORD=secret \
    --network=notes-api-network \
    postgres:12

# a7b287d34d96c8e81a63949c57b83d7c1d71b5660c87f5172f074bd1606196dc

docker container ls

# CONTAINER ID   IMAGE         COMMAND                  CREATED              STATUS              PORTS      NAMES
# a7b287d34d96   postgres:12   "docker-entrypoint.s…"   About a minute ago   Up About a minute   5432/tcp   notes-db
</code></pre>
<p><code>container run</code>  和 <code>container create</code> 命令的 <code>--env</code> 选项可用于向容器提供环境变量。如你所见，数据库容器已成功创建并且正在运行。</p>
<p>尽管容器正在运行，但是存在一个小问题。 PostgreSQL、MongoDB 和 MySQL 等数据库将其数据保留在目录中。 PostgreSQL使用容器内的 <code>/var/lib/postgresql/data</code> 目录来持久化数据。</p>
<p>现在，如果容器由于某种原因被破坏怎么办？ 将丢失所有数据。为了解决此问题，可以使用命名卷。</p>
<h3 id="docker">如何在 Docker 中使用命名卷</h3>
<p>之前，已经使用了绑定挂载和匿名卷。命名卷与匿名卷非常相似，不同之处在于可以使用其名称引用命名卷。</p>
<p>卷也是 Docker 中的逻辑对象，可以使用命令行进行操作。<code>volume create</code> 命令可用于创建命名卷。</p>
<p>该命令的通用语法如下：</p>
<pre><code>docker volume create &lt;volume name&gt;
</code></pre>
<p>要创建一个名为 <code>notes-db-data</code> 的卷，可以执行以下命令：</p>
<pre><code>docker volume create notes-db-data

# notes-db-data

docker volume ls

# DRIVER    VOLUME NAME
# local     notes-db-data
</code></pre>
<p>这个卷现在可以被安装到 <code>notes-db</code> 容器中的 <code>/var/lib/postgresql/data</code> 中。为此，请停止并删除 <code>notes-db</code> 容器：</p>
<pre><code>docker container stop notes-db

# notes-db

docker container rm notes-db

# notes-db
</code></pre>
<p>现在运行一个新容器，并使用 <code>--volume</code> 或 <code>-v</code> 选项分配卷。</p>
<pre><code>docker container run \
    --detach \
    --volume notes-db-data:/var/lib/postgresql/data \
    --name=notes-db \
    --env POSTGRES_DB=notesdb \
    --env POSTGRES_PASSWORD=secret \
    --network=notes-api-network \
    postgres:12

# 37755e86d62794ed3e67c19d0cd1eba431e26ab56099b92a3456908c1d346791
</code></pre>
<p>现在检查 <code>notes-db</code> 容器以确保安装成功：</p>
<pre><code>docker container inspect --format='{{range .Mounts}} {{ .Name }} {{end}}' notes-db

#  notes-db-data
</code></pre>
<p>现在，这些数据将安全地存储在 <code>notes-db-data</code> 卷中，并且将来可以重复使用。在这里也可以使用绑定挂载代替命名卷，但是在这种情况下，我更喜欢使用命名卷。</p>
<h3 id="docker">如何从 Docker 中的容器访问日志</h3>
<p>为了查看来自容器的日志，可以使用 <code>container logs</code>  命令。 该命令的通用语法如下：</p>
<pre><code>docker container logs &lt;container identifier&gt;
</code></pre>
<p>要从 <code>notes-db</code> 容器访问日志，可以执行以下命令：</p>
<pre><code>docker container logs notes-db

# The files belonging to this database system will be owned by user "postgres".
# This user must also own the server process.

# The database cluster will be initialized with locale "en_US.utf8".
# The default database encoding has accordingly been set to "UTF8".
# The default text search configuration will be set to "english".
#
# Data page checksums are disabled.
#
# fixing permissions on existing directory /var/lib/postgresql/data ... ok
# creating subdirectories ... ok
# selecting dynamic shared memory implementation ... posix
# selecting default max_connections ... 100
# selecting default shared_buffers ... 128MB
# selecting default time zone ... Etc/UTC
# creating configuration files ... ok
# running bootstrap script ... ok
# performing post-bootstrap initialization ... ok
# syncing data to disk ... ok
#
#
# Success. You can now start the database server using:
#
#     pg_ctl -D /var/lib/postgresql/data -l logfile start
#
# initdb: warning: enabling "trust" authentication for local connections
# You can change this by editing pg_hba.conf or using the option -A, or
# --auth-local and --auth-host, the next time you run initdb.
# waiting for server to start....2021-01-25 13:39:21.613 UTC [47] LOG:  starting PostgreSQL 12.5 (Debian 12.5-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
# 2021-01-25 13:39:21.621 UTC [47] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
# 2021-01-25 13:39:21.675 UTC [48] LOG:  database system was shut down at 2021-01-25 13:39:21 UTC
# 2021-01-25 13:39:21.685 UTC [47] LOG:  database system is ready to accept connections
#  done
# server started
# CREATE DATABASE
#
#
# /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/*
#
# 2021-01-25 13:39:22.008 UTC [47] LOG:  received fast shutdown request
# waiting for server to shut down....2021-01-25 13:39:22.015 UTC [47] LOG:  aborting any active transactions
# 2021-01-25 13:39:22.017 UTC [47] LOG:  background worker "logical replication launcher" (PID 54) exited with exit code 1
# 2021-01-25 13:39:22.017 UTC [49] LOG:  shutting down
# 2021-01-25 13:39:22.056 UTC [47] LOG:  database system is shut down
#  done
# server stopped
#
# PostgreSQL init process complete; ready for start up.
#
# 2021-01-25 13:39:22.135 UTC [1] LOG:  starting PostgreSQL 12.5 (Debian 12.5-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit
# 2021-01-25 13:39:22.136 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
# 2021-01-25 13:39:22.136 UTC [1] LOG:  listening on IPv6 address "::", port 5432
# 2021-01-25 13:39:22.147 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
# 2021-01-25 13:39:22.177 UTC [75] LOG:  database system was shut down at 2021-01-25 13:39:22 UTC
# 2021-01-25 13:39:22.190 UTC [1] LOG:  database system is ready to accept connections
</code></pre>
<p>从第 57 行的文本可以看出，数据库已启动，并准备接受来自外部的连接。该命令还有 <code>--follow</code> 或 <code>-f</code> 选项，使可以将控制台连接到日志输出并获得连续的文本流。</p>
<h3 id="docker">如何在 Docker 中创建网络并连接数据库服务</h3>
<p>如在上一节中所学，容器必须连接到用户定义的桥接网络，才能使用容器名称相互通信。为此，请在系统中创建一个名为 <code>notes-api-network</code> 的网络：</p>
<pre><code>docker network create notes-api-network
</code></pre>
<p>现在，通过执行以下命令，将 <code>notes-db</code> 容器连接到该网络：</p>
<pre><code>docker network connect notes-api-network notes-db
</code></pre>
<h3 id="dockerfile">如何编写 Dockerfile</h3>
<p>转到克隆项目代码的目录。在其中，进入 <code>notes-api/api</code> 目录，并创建一个新的 <code>Dockerfile</code>。 将以下代码放入文件中：</p>
<pre><code># stage one
FROM node:lts-alpine as builder

# install dependencies for node-gyp
RUN apk add --no-cache python make g++

WORKDIR /app

COPY ./package.json .
RUN npm install --only=prod

# stage two
FROM node:lts-alpine

EXPOSE 3000
ENV NODE_ENV=production

USER node
RUN mkdir -p /home/node/app
WORKDIR /home/node/app

COPY . .
COPY --from=builder /app/node_modules  /home/node/app/node_modules

CMD [ "node", "bin/www" ]
</code></pre>
<p>这是一个多阶段构建。第一阶段用于使用 <code>node-gyp</code> 构建和安装依赖项，第二阶段用于运行应用程序。我将简要介绍以下步骤：</p>
<ul>
<li>阶段1使用 <code>node：lts-alpine</code> 作为基础，并使用 <code>builder</code> 作为阶段名称。</li>
<li>在第 5 行，安装了  <code>python</code>、<code>make</code> 和 <code>g++</code>。<code>node-gyp</code> 工具需要这三个软件包才能运行。</li>
<li>在第 7 行，我们将 <code>/app</code>  目录设置为 <code>WORKDIR</code>。</li>
<li>在第 9 和 10 行，将 <code>package.json</code> 文件复制到 <code>WORKDIR</code> 并安装所有依赖项。</li>
<li>第 2 阶段还使用 <code>node-lts：alpine</code> 作为基础镜像。</li>
<li>在第 16 行，将环境变量 <code>NODE_ENV</code> 设置为 <code>production</code>。 这对于 API 正常运行很重要。</li>
<li>从第 18 行到第 20 行，将默认用户设置为 <code>node</code>，创建 <code>/home/node/app</code> 目录，并将其设置为 <code>WORKDIR</code>。</li>
<li>在第 22 行，复制了所有项目文件，在第 23 行，从 <code>builder</code>  阶段复制了 <code>node_modules</code> 目录。此目录包含运行应用程序所需的所有已构建依赖关系。</li>
<li>在第 25 行，设置了默认命令。</li>
</ul>
<p>要从此 <code>Dockerfile</code> 构建镜像，可以执行以下命令：</p>
<pre><code>docker image build --tag notes-api .

# Sending build context to Docker daemon  37.38kB
# Step 1/14 : FROM node:lts-alpine as builder
#  ---&gt; 471e8b4eb0b2
# Step 2/14 : RUN apk add --no-cache python make g++
#  ---&gt; Running in 5f20a0ecc04b
# fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/main/x86_64/APKINDEX.tar.gz
# fetch http://dl-cdn.alpinelinux.org/alpine/v3.11/community/x86_64/APKINDEX.tar.gz
# (1/21) Installing binutils (2.33.1-r0)
# (2/21) Installing gmp (6.1.2-r1)
# (3/21) Installing isl (0.18-r0)
# (4/21) Installing libgomp (9.3.0-r0)
# (5/21) Installing libatomic (9.3.0-r0)
# (6/21) Installing mpfr4 (4.0.2-r1)
# (7/21) Installing mpc1 (1.1.0-r1)
# (8/21) Installing gcc (9.3.0-r0)
# (9/21) Installing musl-dev (1.1.24-r3)
# (10/21) Installing libc-dev (0.7.2-r0)
# (11/21) Installing g++ (9.3.0-r0)
# (12/21) Installing make (4.2.1-r2)
# (13/21) Installing libbz2 (1.0.8-r1)
# (14/21) Installing expat (2.2.9-r1)
# (15/21) Installing libffi (3.2.1-r6)
# (16/21) Installing gdbm (1.13-r1)
# (17/21) Installing ncurses-terminfo-base (6.1_p20200118-r4)
# (18/21) Installing ncurses-libs (6.1_p20200118-r4)
# (19/21) Installing readline (8.0.1-r0)
# (20/21) Installing sqlite-libs (3.30.1-r2)
# (21/21) Installing python2 (2.7.18-r0)
# Executing busybox-1.31.1-r9.trigger
# OK: 212 MiB in 37 packages
# Removing intermediate container 5f20a0ecc04b
#  ---&gt; 637ca797d709
# Step 3/14 : WORKDIR /app
#  ---&gt; Running in 846361b57599
# Removing intermediate container 846361b57599
#  ---&gt; 3d58a482896e
# Step 4/14 : COPY ./package.json .
#  ---&gt; 11b387794039
# Step 5/14 : RUN npm install --only=prod
#  ---&gt; Running in 2e27e33f935d
#  added 269 packages from 220 contributors and audited 1137 packages in 140.322s
#
# 4 packages are looking for funding
#   run `npm fund` for details
#
# found 0 vulnerabilities
#
# Removing intermediate container 2e27e33f935d
#  ---&gt; eb7cb2cb0b20
# Step 6/14 : FROM node:lts-alpine
#  ---&gt; 471e8b4eb0b2
# Step 7/14 : EXPOSE 3000
#  ---&gt; Running in 4ea24f871747
# Removing intermediate container 4ea24f871747
#  ---&gt; 1f0206f2f050
# Step 8/14 : ENV NODE_ENV=production
#  ---&gt; Running in 5d40d6ac3b7e
# Removing intermediate container 5d40d6ac3b7e
#  ---&gt; 31f62da17929
# Step 9/14 : USER node
#  ---&gt; Running in 0963e1fb19a0
# Removing intermediate container 0963e1fb19a0
#  ---&gt; 0f4045152b1c
# Step 10/14 : RUN mkdir -p /home/node/app
#  ---&gt; Running in 0ac591b3adbd
# Removing intermediate container 0ac591b3adbd
#  ---&gt; 5908373dfc75
# Step 11/14 : WORKDIR /home/node/app
#  ---&gt; Running in 55253b62ff57
# Removing intermediate container 55253b62ff57
#  ---&gt; 2883cdb7c77a
# Step 12/14 : COPY . .
#  ---&gt; 8e60893a7142
# Step 13/14 : COPY --from=builder /app/node_modules  /home/node/app/node_modules
#  ---&gt; 27a85faa4342
# Step 14/14 : CMD [ "node", "bin/www" ]
#  ---&gt; Running in 349c8ca6dd3e
# Removing intermediate container 349c8ca6dd3e
#  ---&gt; 9ea100571585
# Successfully built 9ea100571585
# Successfully tagged notes-api:latest
</code></pre>
<p>在使用该镜像运行容器之前，请确保数据库容器正在运行，并且已附加到 <code>notes-api-network</code> 上。</p>
<pre><code>docker container inspect notes-db

# [
#     {
#         ...
#         "State": {
#             "Status": "running",
#             "Running": true,
#             "Paused": false,
#             "Restarting": false,
#             "OOMKilled": false,
#             "Dead": false,
#             "Pid": 11521,
#             "ExitCode": 0,
#             "Error": "",
#             "StartedAt": "2021-01-26T06:55:44.928510218Z",
#             "FinishedAt": "2021-01-25T14:19:31.316854657Z"
#         },
#         ...
#         "Mounts": [
#             {
#                 "Type": "volume",
#                 "Name": "notes-db-data",
#                 "Source": "/var/lib/docker/volumes/notes-db-data/_data",
#                 "Destination": "/var/lib/postgresql/data",
#                 "Driver": "local",
#                 "Mode": "z",
#                 "RW": true,
#                 "Propagation": ""
#             }
#         ],
#         ...
#         "NetworkSettings": {
#             ...
#             "Networks": {
#                 "bridge": {
#                     "IPAMConfig": null,
#                     "Links": null,
#                     "Aliases": null,
#                     "NetworkID": "e4c7ce50a5a2a49672155ff498597db336ecc2e3bbb6ee8baeebcf9fcfa0e1ab",
#                     "EndpointID": "2a2587f8285fa020878dd38bdc630cdfca0d769f76fc143d1b554237ce907371",
#                     "Gateway": "172.17.0.1",
#                     "IPAddress": "172.17.0.2",
#                     "IPPrefixLen": 16,
#                     "IPv6Gateway": "",
#                     "GlobalIPv6Address": "",
#                     "GlobalIPv6PrefixLen": 0,
#                     "MacAddress": "02:42:ac:11:00:02",
#                     "DriverOpts": null
#                 },
#                 "notes-api-network": {
#                     "IPAMConfig": {},
#                     "Links": null,
#                     "Aliases": [
#                         "37755e86d627"
#                     ],
#                     "NetworkID": "06579ad9f93d59fc3866ac628ed258dfac2ed7bc1a9cd6fe6e67220b15d203ea",
#                     "EndpointID": "5b8f8718ec9a5ec53e7a13cce3cb540fdf3556fb34242362a8da4cc08d37223c",
#                     "Gateway": "172.18.0.1",
#                     "IPAddress": "172.18.0.2",
#                     "IPPrefixLen": 16,
#                     "IPv6Gateway": "",
#                     "GlobalIPv6Address": "",
#                     "GlobalIPv6PrefixLen": 0,
#                     "MacAddress": "02:42:ac:12:00:02",
#                     "DriverOpts": {}
#                 }
#             }
#         }
#     }
# ]
</code></pre>
<p>已经缩短了输出，以便于在此处查看。在我的系统上，<code>notes-db</code> 容器正在运行，使用 <code>notes-db-data</code> 卷，并连接到 <code>notes-api-network</code> 桥接网络。</p>
<p>一旦确定一切就绪，就可以通过执行以下命令来运行新容器：</p>
<pre><code>docker container run \
    --detach \
    --name=notes-api \
    --env DB_HOST=notes-db \
    --env DB_DATABASE=notesdb \
    --env DB_PASSWORD=secret \
    --publish=3000:3000 \
    --network=notes-api-network \
    notes-api
    
# f9ece420872de99a060b954e3c236cbb1e23d468feffa7fed1e06985d99fb919
</code></pre>
<p>应该理解这个长命令，因此只简要介绍下环境变量。</p>
<p><code>notes-api</code> 应用程序需要设置三个环境变量。 它们如下：</p>
<ul>
<li><code>DB_HOST</code> - 这是数据库服务的主机。假定数据库服务和 API 都连接到同一用户定义的桥接网络，则可以使用其容器名称（在这种情况下为 <code>notes-db</code>）引用数据库服务。</li>
<li><code>DB_DATABASE</code> - 此API将使用的数据库。 在<a href="https://www.freecodecamp.org/news/@fhsinchy/s/the-docker-handbook/~/drafts/-MS2MtB5zjVVjK3Ujaz4/containerizing-a-multi-container-javascript-application#running-the-database-server">运行数据库服务</a>小节，使用环境变量 <code>POSTGRES_DB</code> 将默认数据库名称设置为 <code>notesdb</code>。 将在这里使用它。</li>
<li><code>DB_PASSWORD</code> - 连接数据库的密码。 这也在运行数据库服务小节涉及，使用环境变量<code>POSTGRES_PASSWORD</code>。</li>
</ul>
<p>要检查容器是否正常运行，可以使用 <code>container ls</code> 命令：</p>
<pre><code>docker container ls

# CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS          PORTS                    NAMES
# f9ece420872d   notes-api     "docker-entrypoint.s…"   12 minutes ago   Up 12 minutes   0.0.0.0:3000-&gt;3000/tcp   notes-api
# 37755e86d627   postgres:12   "docker-entrypoint.s…"   17 hours ago     Up 14 minutes   5432/tcp                 notes-db
</code></pre>
<p>容器正在运行。 可以访问 <code>http://127.0.0.1:3000/</code> 来查看正在使用的API。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/bonjour-mon-ami.png" alt="bonjour-mon-ami" width="600" height="400" loading="lazy"></p>
<p>该 API 总共有五个路由，可以在 <code>/notes/api/api/api/routes/notes.js</code>  文件中看到。 它是用我的一个<a href="https://github.com/fhsinchy/create-node-rocket-api">开源项目</a>引导的。</p>
<p>尽管容器正在运行，但是在开始使用它之前，还有最后一件事要做。必须运行设置数据库表所必需的数据库迁移，并且可以通过在容器内执行 <code>npm run db:migrate</code> 命令来执行此操作。</p>
<h3 id="">如何在正在运行的容器中执行命令</h3>
<p>已经了解了在停止的容器中执行命令的知识。另一种情况是在正在运行的容器内执行命令。</p>
<p>为此，必须使用 <code>exec</code> 命令在正在运行的容器内执行自定义命令。</p>
<p><code>exec</code> 命令的通用语法如下：</p>
<pre><code>docker container exec &lt;container identifier&gt; &lt;command&gt;
</code></pre>
<p>要执行 <code>notes-api</code> 容器内的 <code>npm run db:migrate</code>，可以执行以下命令：</p>
<pre><code>docker container exec notes-api npm run db:migrate

# &gt; notes-api@ db:migrate /home/node/app
# &gt; knex migrate:latest
#
# Using environment: production
# Batch 1 run: 1 migrations
</code></pre>
<p>如果要在正在运行的容器中运行交互式命令，则必须使用 <code>-it</code>  标志。例如，如果要访问在 <code>notes-api</code> 容器中运行的 shell，可以执行以下命令：</p>
<pre><code>docker container exec -it notes-api sh

# / # uname -a
# Linux b5b1367d6b31 5.10.9-201.fc33.x86_64 #1 SMP Wed Jan 20 16:56:23 UTC 2021 x86_64 Linux
</code></pre>
<h3 id="docker">如何在 Docker 中编写管理脚本</h3>
<p>管理多容器项目以及网络，卷和内容意味着编写大量命令。为了简化过程，我通常会从简单的 <a href="https://opensource.com/article/17/1/getting-started-shell-scripting">shell脚本</a>和 <a href="https://opensource.com/article/18/8/what-how-makefile">Makefile</a> 来提高效率。</p>
<p>可以在 <code>notes-api</code> 目录中找到四个 shell 脚本。 它们如下：</p>
<ul>
<li><code>boot.sh</code> - 用于启动容器（如果已存在）。</li>
<li><code>build.sh</code> - 创建并运行容器。如果需要，它还会创建镜像，卷和网络。</li>
<li><code>destroy.sh</code> - 删除与此项目关联的所有容器，卷和网络。</li>
<li><code>stop.sh</code> - 停止所有正在运行的容器。</li>
</ul>
<p>还有一个 <code>Makefile</code>，其中包含名为<code>start</code>、<code>stop</code>、<code>build</code>和 <code>destroy</code> 的四个目标，每个目标都调用前面提到的 shell 脚本。</p>
<p>如果容器在系统中处于运行状态，执行 <code>make stop</code>  将停止所有容器。 执行 <code>make destroy</code> 应该停止容器并删除所有东西。 确保正在 <code>notes-api</code> 目录中运行脚本：</p>
<pre><code>make destroy

# ./shutdown.sh
# stopping api container ---&gt;
# notes-api
# api container stopped ---&gt;

# stopping db container ---&gt;
# notes-db
# db container stopped ---&gt;

# shutdown script finished

# ./destroy.sh
# removing api container ---&gt;
# notes-api
# api container removed ---&gt;

# removing db container ---&gt;
# notes-db
# db container removed ---&gt;

# removing db data volume ---&gt;
# notes-db-data
# db data volume removed ---&gt;

# removing network ---&gt;
# notes-api-network
# network removed ---&gt;

# destroy script finished
</code></pre>
<p>如果遇到权限拒绝错误，请在脚本上执行 <code>chmod + x</code>：</p>
<pre><code>chmod +x boot.sh build.sh destroy.sh shutdown.sh
</code></pre>
<p>这里不解释这些脚本，因为它们是简单的 <code>if-else</code>  语句以及一些已经看过很多次的  Docker 命令。如果对 Linux Shell 有所了解，那么也应该能够理解这些脚本。</p>
<h2 id="dockercompose">如何使用 Docker-Compose 组合项目</h2>
<p>在上一节中，了解了有关管理多容器项目的困难。除了编写许多命令之外，还有一种更简单的方法来管理多容器项目，该工具叫作<a href="https://docs.docker.com/compose/">Docker Compose</a>。</p>
<p>根据 Docker 的<a href="https://docs.docker.com/compose/">文档</a> -</p>
<blockquote>
<p>Compose 是用于定义和运行多容器 Docker 应用程序的工具。通过 Compose，可以使用 YAML 文件来配置应用程序的服务。然后，使用一个命令，就可以从配置中创建并启动所有服务。</p>
</blockquote>
<p>尽管 Compose 可在所有环境中使用，但它更专注于开发和测试。完全不建议在生产环境上使用 Compose。</p>
<h3 id="dockercompose">Docker Compose 基础</h3>
<p>转至用来克隆本书随附仓库的目录。进入 <code>notes-api/api</code> 目录并创建 <code>Dockerfile.dev</code> 文件。将以下代码放入其中：</p>
<pre><code>
FROM node:lts-alpine as builder


RUN apk add --no-cache python make g++

WORKDIR /app

COPY ./package.json .
RUN npm install


FROM node:lts-alpine

ENV NODE_ENV=development

USER node
RUN mkdir -p /home/node/app
WORKDIR /home/node/app

COPY . .
COPY --from=builder /app/node_modules /home/node/app/node_modules

CMD [ "./node_modules/.bin/nodemon", "--config", "nodemon.json", "bin/www" ]
</code></pre>
<p>该代码与上一小节中使用的 <code>Dockerfile</code> 几乎相同。 此文件中的三个区别如下：</p>
<ul>
<li>在第 10 行中，执行 <code>npm install</code> 而不是 <code>npm run install --only = prod</code>，因为还需要开发依赖项。</li>
<li>在第 15 行，将环境变量 <code>NODE_ENV</code> 设置为  <code>development</code> 而不是 <code>production</code>。</li>
<li>在第 24 行，使用名为 <a href="https://nodemon.io/">nodemon</a> 的工具来获取 API 的热重载功能。</li>
</ul>
<p>已经知道该项目有两个容器：</p>
<ul>
<li><code>notes-db</code> - 一个基于 PostgreSQL 的数据库服务</li>
<li><code>notes-api</code> - 一个基于 Express.js 的 REST API</li>
</ul>
<p>在 Compose 的世界中，组成应用程序的每个容器都叫作服务。组合多容器项目的第一步就是定义这些服务。</p>
<p>就像 Docker 守护进程使用 <code>Dockerfile</code> 构建映像一样，Docker Compose 使用 <code>docker-compose.yaml</code> 文件从中读取服务定义。</p>
<p>转到 <code>notes-api</code> 目录并创建一个新的 <code>docker-compose.yaml</code> 文件。 将以下代码放入新创建的文件中：</p>
<pre><code>version: "3.8"

services: 
    db:
        image: postgres:12
        container_name: notes-db-dev
        volumes: 
            - notes-db-dev-data:/var/lib/postgresql/data
        environment:
            POSTGRES_DB: notesdb
            POSTGRES_PASSWORD: secret
    api:
        build:
            context: ./api
            dockerfile: Dockerfile.dev
        image: notes-api:dev
        container_name: notes-api-dev
        environment: 
            DB_HOST: db 
            DB_DATABASE: notesdb
            DB_PASSWORD: secret
        volumes: 
            - /home/node/app/node_modules
            - ./api:/home/node/app
        ports: 
            - 3000:3000

volumes:
    notes-db-dev-data:
        name: notes-db-dev-data
</code></pre>
<p>每个有效的 <code>docker-compose.yaml</code> 文件均通过定义文件版本开始。在撰写本文时，<code>3.8</code> 是最新版本。可以在<a href="https://docs.docker.com/compose/compose-file/">此处</a>查找最新版本。</p>
<p>YAML 文件中的块由缩进定义。将仔细介绍每个块，并解释它们的作用。</p>
<ul>
<li><code>services</code> 块包含应用程序中每个服务或容器的定义。<code>db</code> 和 <code>api</code> 是构成该项目的两个服务。</li>
<li><code>db</code>  块在应用程序中定义了一个新服务，并保存了启动容器所需的信息。每个服务都需要一个预先构建的镜像或一个 <code>Dockerfile</code> 来运行容器。 对于 <code>db</code> 服务，我们使用的是官方 PostgreSQL 镜像。</li>
<li>与 <code>db</code> 服务不同的是，不存在 <code>api</code> 服务的预构建镜像。因此，将使用 <code>Dockerfile.dev</code> 文件。</li>
<li><code>volumes</code>  块定义了任何服务所需的任何名称卷。当时，它仅登记 <code>db</code> 服务使用的是 <code>notes-db-dev-data</code> 卷。</li>
</ul>
<p>既然已经对 <code>docker-compose.yaml</code> 文件有了一个高层次的概述，那么让我们仔细看一下各个服务。</p>
<p><code>db</code> 服务的定义代码如下：</p>
<pre><code>db:
    image: postgres:12
    container_name: notes-db-dev
    volumes: 
        - db-data:/var/lib/postgresql/data
    environment:
        POSTGRES_DB: notesdb
        POSTGRES_PASSWORD: secret
</code></pre>
<ul>
<li><code>image</code> 键保存用于此容器的镜像仓库和标签。使用 <code>postgres:12</code> 镜像来运行数据库容器。</li>
<li><code>container_name</code> 指示容器的名称。默认情况下，容器使用 <code>&lt;project directory name&gt;_&lt;service name&gt;</code> 语法命名。可以使用 <code>container_name</code> 覆盖它。</li>
<li><code>volumes</code> 数组保存该服务的卷映射，并支持命名卷，匿名卷和绑定挂载。 语法 <code>&lt;source&gt;:&lt;destination&gt;</code> 与之前相同。</li>
<li><code>environment</code> map 包含服务所需的各种环境变量的值。</li>
</ul>
<p><code>api</code> 服务的定义代码如下：</p>
<pre><code>api:
    build:
        context: ./api
        dockerfile: Dockerfile.dev
    image: notes-api:dev
    container_name: notes-api-dev
    environment: 
        DB_HOST: db 
        DB_DATABASE: notesdb
        DB_PASSWORD: secret
    volumes: 
        - /home/node/app/node_modules
        - ./api:/home/node/app
    ports: 
        - 3000:3000
</code></pre>
<ul>
<li><code>api</code> 服务没有预构建的镜像。相反，它具有构建配置。在 <code>build</code> 块下，定义了用于构建镜像的上下文和 Dockerfile 的名称。到目前为止，应该已经了解了上下文和 Dockerfile，因此我不会花时间解释它们。</li>
<li><code>image</code> 键保存要构建的镜像的名称。如果未分配，则将使用 <code>&lt;project directory name&gt;_&lt;service name&gt;</code> 语法来命名镜像。</li>
<li>在 <code>environment</code> map 内部，<code>DB_HOST</code> 变量演示了 Compose 的功能。即，可以使用其名称引用同一应用程序中的另一服务。因此，此处的  <code>db</code>  将被 <code>api</code> 服务容器的 IP 地址代替。<code>DB_DATABASE</code> 和 <code>DB_PASSWORD</code>  变量必须分别与 <code>db</code> 服务定义中的 <code>POSTGRES_DB</code> 和 <code>POSTGRES_PASSWORD</code> 匹配。</li>
<li>在 <code>volumes</code> map 中，可以看到一个匿名卷和一个绑定挂载。语法与上一节中看到的相同。</li>
<li><code>ports</code> 映射定义了端口映射。 语法 <code>&lt;host port&gt;:&lt;container port&gt;</code> 与以前使用的 <code>--publish</code> 选项相同。</li>
</ul>
<p>最后，<code>volumes</code> 的代码如下：</p>
<pre><code>volumes:
    db-data:
        name: notes-db-dev-data
</code></pre>
<p>在此处定义服务中使用的命名卷。如果未定义名称，则将使用 <code>&lt;project directory name&gt;_&lt;volume key&gt;</code> 命名该卷，此处的密钥为 <code>db-data</code>。</p>
<p>可以在官方<a href="https://docs.docker.com/compose/compose-file/compose-file-v3/#volumes">文档</a>中了解有关卷配置的更多选项。</p>
<h3 id="dockercompose">如何在 Docker Compose 中启动服务</h3>
<p>有几种启动 YAML 文件中定义的服务的方法。将了解的第一个命令是 <code>up</code> 命令。<code>up</code> 命令可以构建所有丢失的镜像，创建容器，然后一次性启动它们。</p>
<p>在执行命令之前，请确保已在 <code>docker-compose.yaml</code> 文件所在的目录中打开了终端。 这对于执行的每个 <code>docker-compose</code> 命令都非常重要。</p>
<pre><code>docker-compose --file docker-compose.yaml up --detach

# Creating network "notes-api_default" with the default driver
# Creating volume "notes-db-dev-data" with default driver
# Building api
# Sending build context to Docker daemon  37.38kB
#
# Step 1/13 : FROM node:lts-alpine as builder
#  ---&gt; 471e8b4eb0b2
# Step 2/13 : RUN apk add --no-cache python make g++
#  ---&gt; Running in 197056ec1964
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 197056ec1964
#  ---&gt; 6609935fe50b
# Step 3/13 : WORKDIR /app
#  ---&gt; Running in 17010f65c5e7
# Removing intermediate container 17010f65c5e7
#  ---&gt; b10d12e676ad
# Step 4/13 : COPY ./package.json .
#  ---&gt; 600d31d9362e
# Step 5/13 : RUN npm install
#  ---&gt; Running in a14afc8c0743
### LONG INSTALLATION STUFF GOES HERE ###
#  Removing intermediate container a14afc8c0743
#  ---&gt; 952d5d86e361
# Step 6/13 : FROM node:lts-alpine
#  ---&gt; 471e8b4eb0b2
# Step 7/13 : ENV NODE_ENV=development
#  ---&gt; Running in 0d5376a9e78a
# Removing intermediate container 0d5376a9e78a
#  ---&gt; 910c081ce5f5
# Step 8/13 : USER node
#  ---&gt; Running in cfaefceb1eff
# Removing intermediate container cfaefceb1eff
#  ---&gt; 1480176a1058
# Step 9/13 : RUN mkdir -p /home/node/app
#  ---&gt; Running in 3ae30e6fb8b8
# Removing intermediate container 3ae30e6fb8b8
#  ---&gt; c391cee4b92c
# Step 10/13 : WORKDIR /home/node/app
#  ---&gt; Running in 6aa27f6b50c1
# Removing intermediate container 6aa27f6b50c1
#  ---&gt; 761a7435dbca
# Step 11/13 : COPY . .
#  ---&gt; b5d5c5bdf3a6
# Step 12/13 : COPY --from=builder /app/node_modules /home/node/app/node_modules
#  ---&gt; 9e1a19960420
# Step 13/13 : CMD [ "./node_modules/.bin/nodemon", "--config", "nodemon.json", "bin/www" ]
#  ---&gt; Running in 5bdd62236994
# Removing intermediate container 5bdd62236994
#  ---&gt; 548e178f1386
# Successfully built 548e178f1386
# Successfully tagged notes-api:dev
# Creating notes-api-dev ... done
# Creating notes-db-dev  ... done
</code></pre>
<p>这里的  <code>--detach</code> 或 <code>-d</code> 选项的功能与之前相同。仅当 YAML 文件未命名为 <code>docker-compose.yaml</code> 时才需要使用  <code>--file</code> 或  <code>-f</code> 选项（但我已在此处用于演示目的）。</p>
<p>除了 <code>up</code> 命令外，还有 <code>start</code> 命令。两者之间的主要区别在于，<code>start</code> 命令不会创建丢失的容器，而只会启动现有的容器。基本上与 <code>container start</code> 命令相同。</p>
<p><code>up</code> 命令的 <code>--build</code> 选项强制重建镜像。可以在官方<a href="https://docs.docker.com/compose/reference/up/">文档</a>中查阅 <code>up</code> 命令的更多选项。</p>
<h3 id="dockercompose">如何在 Docker Compose 中列表展示服务</h3>
<p>尽管可以使用 <code>container ls</code> 命令列出由 Compose 启动的服务容器，但是还可以用  <code>ps</code>  命令列出仅在 YAML 中定义的容器。</p>
<pre><code>docker-compose ps

#     Name                   Command               State           Ports         
# -------------------------------------------------------------------------------
# notes-api-dev   docker-entrypoint.sh ./nod ...   Up      0.0.0.0:3000-&gt;3000/tcp
# notes-db-dev    docker-entrypoint.sh postgres    Up      5432/tcp
</code></pre>
<p>它不如 <code>container ls</code> 输出的信息丰富，但是当同时运行大量容器时，它很有用。</p>
<h3 id="dockercompose">如何在 Docker Compose 正在运行的服务中执行命令</h3>
<p>我希望你记得上一部分，必须运行一些迁移脚本来为此 API 创建数据库表。</p>
<p>就像 <code>container exec</code> 命令一样，<code>docker-compose</code> 也有 <code>exec</code> 命令。该命令的通用语法如下：</p>
<pre><code>docker-compose exec &lt;service name&gt; &lt;command&gt;
</code></pre>
<p>要在 <code>api</code> 服务中执行 <code>npm run db:migrate</code> 命令，可以执行以下命令：</p>
<pre><code>docker-compose exec api npm run db:migrate

# &gt; notes-api@ db:migrate /home/node/app
# &gt; knex migrate:latest
# 
# Using environment: development
# Batch 1 run: 1 migrations
</code></pre>
<p>与 <code>container exec</code> 命令不同，不需要为交互式会话传递 <code>-it</code> 标志。<code>docker-compose</code> 是自动完成的。</p>
<h3 id="dockercompose">如何访问 Docker Compose 中正在运行的服务日志</h3>
<p>也可以使用 <code>logs</code> 命令从正在运行的服务中检索日志。该命令的通用语法如下：</p>
<pre><code>docker-compose logs &lt;service name&gt;
</code></pre>
<p>要从 <code>api</code> 服务访问日志，请执行以下命令：</p>
<pre><code>docker-compose logs api

# Attaching to notes-api-dev
# notes-api-dev | [nodemon] 2.0.7
# notes-api-dev | [nodemon] reading config ./nodemon.json
# notes-api-dev | [nodemon] to restart at any time, enter `rs`
# notes-api-dev | [nodemon] or send SIGHUP to 1 to restart
# notes-api-dev | [nodemon] ignoring: *.test.js
# notes-api-dev | [nodemon] watching path(s): *.*
# notes-api-dev | [nodemon] watching extensions: js,mjs,json
# notes-api-dev | [nodemon] starting `node bin/www`
# notes-api-dev | [nodemon] forking
# notes-api-dev | [nodemon] child pid: 19
# notes-api-dev | [nodemon] watching 18 files
# notes-api-dev | app running -&gt; http://127.0.0.1:3000
</code></pre>
<p>这只是日志输出的一部分。可以使用 <code>-f</code> 或 <code>--follow</code> 选项来钩住服务的输出流并实时获取日志。只要不按 <code>ctrl + c</code> 或关闭窗口退出，任何以后的日志都会立即显示在终端中。即使退出日志窗口，该容器也将继续运行。</p>
<h3 id="dockercompose">如何在 Docker Compose 中停止服务</h3>
<p>要停止服务，可以采用两种方法。第一个是 <code>down</code> 命令。<code>down</code> 命令将停止所有正在运行的容器并将其从系统中删除。它还会删除所有网络：</p>
<pre><code>docker-compose down --volumes

# Stopping notes-api-dev ... done
# Stopping notes-db-dev  ... done
# Removing notes-api-dev ... done
# Removing notes-db-dev  ... done
# Removing network notes-api_default
# Removing volume notes-db-dev-data
</code></pre>
<p><code>--volumes</code> 选项表示要删除 <code>volumes</code> 块中定义的所有命名卷。可以在官方<a href="https://docs.docker.com/compose/reference/down/">文档</a> 中查阅有关 <code>down</code> 命令的更多选用法。</p>
<p>另一个停止服务的命令是 <code>stop</code> 命令，其功能与  <code>container stop</code>  命令相同。它停止应用程序的所有容器并保留它们。这些容器可以稍后使用 <code>start</code> 或 <code>up</code> 命令启动。</p>
<h3 id="dockercompose">如何在 Docker Compose 中编写全栈应用程序</h3>
<p>在本小节中，我们将在 Notes API 中添加一个前端，并将其转变为一个完整的全栈应用程序。在本小节中，我将不解释 <code>Dockerfile.dev</code> 文件内容（除了关于 <code>nginx</code> 服务的部分），因为它们与上一小节中已经看到的其他文件相同。</p>
<p>如果已经克隆了项目代码仓库，进入 <code>fullstack-notes-application</code> 目录。项目根目录下的每个目录都包含每个服务的代码和相应的 <code>Dockerfile</code>。</p>
<p>在开始使用 <code>docker-compose.yaml</code> 文件之前，让我们看一下该应用程序的流程图：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/fullstack-application-design.svg" alt="fullstack-application-design" width="600" height="400" loading="lazy"></p>
<p>与其像以前那样直接接受请求，在此应用程序中，所有请求都将首先由 NGINX（我们称其为路由器）服务接收。</p>
<p>然后，路由器将查看所请求的路径中是否包含 <code>/api</code>。如果是，则路由器会将请求路由到后端，否则，路由器会将请求路由到前端。</p>
<p>这样做是因为在运行前端应用程序时，它不会在容器中运行。 它在浏览器上运行，并通过容器提供服务。结果，Compose 网络无法按预期工作，并且前端应用程序无法找到 <code>api</code> 服务。</p>
<p>另一方面，NGINX 在容器内运行，并且可以与整个应用程序中的不同服务进行通信。</p>
<p>在这里介绍不 NGINX 的配置。该主题有点超出了本书的范围。如果你想了解，请继续阅读 <code>/notes-api/nginx/development.conf</code> 和 <code>/notes-api/nginx/production.conf</code> 文件。<code>/notes-api/nginx/Deockerfile.dev</code> 的代码如下：</p>
<pre><code>FROM nginx:stable-alpine

COPY ./development.conf /etc/nginx/conf.d/default.conf
</code></pre>
<p>它所做的只是将配置文件复制到容器内的 <code>/etc/nginx/conf.d/default.conf</code> 中。</p>
<p>让我们开始编写  <code>docker-compose.yaml</code> 文件。除了 <code>api</code> 和 <code>db</code> 服务之外，还有<code>client</code> 和 <code>nginx</code> 服务。还将很快介绍一些网络定义。</p>
<pre><code>version: "3.8"

services: 
    db:
        image: postgres:12
        container_name: notes-db-dev
        volumes: 
            - db-data:/var/lib/postgresql/data
        environment:
            POSTGRES_DB: notesdb
            POSTGRES_PASSWORD: secret
        networks:
            - backend
    api:
        build: 
            context: ./api
            dockerfile: Dockerfile.dev
        image: notes-api:dev
        container_name: notes-api-dev
        volumes: 
            - /home/node/app/node_modules
            - ./api:/home/node/app
        environment: 
            DB_HOST: db 
            DB_PORT: 5432
            DB_USER: postgres
            DB_DATABASE: notesdb
            DB_PASSWORD: secret
        networks:
            - backend
    client:
        build:
            context: ./client
            dockerfile: Dockerfile.dev
        image: notes-client:dev
        container_name: notes-client-dev
        volumes: 
            - /home/node/app/node_modules
            - ./client:/home/node/app
        networks:
            - frontend
    nginx:
        build:
            context: ./nginx
            dockerfile: Dockerfile.dev
        image: notes-router:dev
        container_name: notes-router-dev
        restart: unless-stopped
        ports: 
            - 8080:80
        networks:
            - backend
            - frontend

volumes:
    db-data:
        name: notes-db-dev-data

networks: 
    frontend:
        name: fullstack-notes-application-network-frontend
        driver: bridge
    backend:
        name: fullstack-notes-application-network-backend
        driver: bridge
</code></pre>
<p>该文件与之前用到的文件几乎相同。唯一需要说明的是网络配置。<code>networks</code> 块的代码如下：</p>
<pre><code>networks: 
    frontend:
        name: fullstack-notes-application-network-frontend
        driver: bridge
    backend:
        name: fullstack-notes-application-network-backend
        driver: bridge
</code></pre>
<p>我定义了两个桥接网络。默认情况下，Compose 创建一个桥接网络并将所有容器连接到该网络。但是，在这个项目中，我想要适当的网络隔离。 因此，我定义了两个网络，一个用于前端服务，一个用于后端服务。</p>
<p>我还在每个服务定义中添加了  <code>networks</code>  块。 这样，<code>api</code> 和 <code>db</code> 服务将被附加到同一个网络，而 <code>client</code> 服务将被附加到一个单独的网络。 但是 <code>nginx</code> 服务将同时连接到两个网络，因此它可以充当前端和后端服务之间的路由器。</p>
<p>通过执行以下命令来启动所有服务：</p>
<pre><code>docker-compose --file docker-compose.yaml up --detach

# Creating network "fullstack-notes-application-network-backend" with driver "bridge"
# Creating network "fullstack-notes-application-network-frontend" with driver "bridge"
# Creating volume "notes-db-dev-data" with default driver
# Building api
# Sending build context to Docker daemon  37.38kB
# 
# Step 1/13 : FROM node:lts-alpine as builder
#  ---&gt; 471e8b4eb0b2
# Step 2/13 : RUN apk add --no-cache python make g++
#  ---&gt; Running in 8a4485388fd3
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container 8a4485388fd3
#  ---&gt; 47fb1ab07cc0
# Step 3/13 : WORKDIR /app
#  ---&gt; Running in bc76cc41f1da
# Removing intermediate container bc76cc41f1da
#  ---&gt; 8c03fdb920f9
# Step 4/13 : COPY ./package.json .
#  ---&gt; a1d5715db999
# Step 5/13 : RUN npm install
#  ---&gt; Running in fabd33cc0986
### LONG INSTALLATION STUFF GOES HERE ###
# Removing intermediate container fabd33cc0986
#  ---&gt; e09913debbd1
# Step 6/13 : FROM node:lts-alpine
#  ---&gt; 471e8b4eb0b2
# Step 7/13 : ENV NODE_ENV=development
#  ---&gt; Using cache
#  ---&gt; b7c12361b3e5
# Step 8/13 : USER node
#  ---&gt; Using cache
#  ---&gt; f5ac66ca07a4
# Step 9/13 : RUN mkdir -p /home/node/app
#  ---&gt; Using cache
#  ---&gt; 60094b9a6183
# Step 10/13 : WORKDIR /home/node/app
#  ---&gt; Using cache
#  ---&gt; 316a252e6e3e
# Step 11/13 : COPY . .
#  ---&gt; Using cache
#  ---&gt; 3a083622b753
# Step 12/13 : COPY --from=builder /app/node_modules /home/node/app/node_modules
#  ---&gt; Using cache
#  ---&gt; 707979b3371c
# Step 13/13 : CMD [ "./node_modules/.bin/nodemon", "--config", "nodemon.json", "bin/www" ]
#  ---&gt; Using cache
#  ---&gt; f2da08a5f59b
# Successfully built f2da08a5f59b
# Successfully tagged notes-api:dev
# Building client
# Sending build context to Docker daemon  43.01kB
# 
# Step 1/7 : FROM node:lts-alpine
#  ---&gt; 471e8b4eb0b2
# Step 2/7 : USER node
#  ---&gt; Using cache
#  ---&gt; 4be5fb31f862
# Step 3/7 : RUN mkdir -p /home/node/app
#  ---&gt; Using cache
#  ---&gt; 1fefc7412723
# Step 4/7 : WORKDIR /home/node/app
#  ---&gt; Using cache
#  ---&gt; d1470d878aa7
# Step 5/7 : COPY ./package.json .
#  ---&gt; Using cache
#  ---&gt; bbcc49475077
# Step 6/7 : RUN npm install
#  ---&gt; Using cache
#  ---&gt; 860a4a2af447
# Step 7/7 : CMD [ "npm", "run", "serve" ]
#  ---&gt; Using cache
#  ---&gt; 11db51d5bee7
# Successfully built 11db51d5bee7
# Successfully tagged notes-client:dev
# Building nginx
# Sending build context to Docker daemon   5.12kB
# 
# Step 1/2 : FROM nginx:stable-alpine
#  ---&gt; f2343e2e2507
# Step 2/2 : COPY ./development.conf /etc/nginx/conf.d/default.conf
#  ---&gt; Using cache
#  ---&gt; 02a55d005a98
# Successfully built 02a55d005a98
# Successfully tagged notes-router:dev
# Creating notes-client-dev ... done
# Creating notes-api-dev    ... done
# Creating notes-router-dev ... done
# Creating notes-db-dev     ... done
</code></pre>
<p>现在访问 <code>http://localhost:8080</code>，瞧瞧！</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/notes-application.png" alt="notes-application" width="600" height="400" loading="lazy"></p>
<p>尝试添加和删除注释，以查看应用程序是否正常运行。该项目还带有 shell 脚本和<code>Makefile</code>。研究一下他们，以了解如何像上一节中那样在没有 <code>docker-compose</code> 的帮助下运行该项目。</p>
<h2 id="">结语</h2>
<p>衷心感谢你花了宝贵的时间阅读本书。我希望你喜欢它并学到 Docker 的相关知识。</p>
<p>如果你喜欢我的文笔，则可以在<a href="https://books.farhan.info/">这里找到更多的的书</a>，我偶尔也写一些<a href="https://www.farhan.info/">博客</a>。</p>
<p>可以在 Twitter <a href="https://twitter.com/frhnhsin">@frhnhsin</a> 上关注我，也可以在 LinkedIn <a href="https://www.linkedin.com/in/farhanhasin/">/in/farhanhasin</a> 联系我。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/the-docker-handbook/#how-to-install-docker">The Docker Handbook – 2021 Edition</a>，作者：<a href="https://www.freecodecamp.org/news/author/farhanhasin/">Farhan Hasin Chowdhury</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Python Flask-RESTPlus 工程化实践 ]]>
                </title>
                <description>
                    <![CDATA[ 本指南将逐步介绍构建用于测试、开发和生产环境的 Flask RESTPlus Web 应用程序的方法。 将使用基于 Linux 的操作系统（Ubuntu），但是大多数步骤都可以在 Windows 和 Mac 上执行。 在继续阅读本指南之前，你应该对 Python 编程语言和 Flask 框架有基本的了解。如果你不熟悉这些内容，建议阅读介绍性文章 - 如何使用 Python 和 Flask 构建 Web 应用程序 [https://www.freecodecamp.org/news/how-to-use-python-and-flask-to-build-a-web-app-an-in-depth-tutorial-437dbfe9f1c6/] 。 本指南的结构 本指南分为以下几部分：  * 功能  * Flask-RESTPlus 是什么？  * 安装和配置  * 项目配置和结构  * 配置设定  * Flask Script  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/structuring-a-flask-restplus-web-service-for-production-builds/</link>
                <guid isPermaLink="false">6064373e4c5a5f0564330537</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Wed, 31 Mar 2021 08:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/03/1_TSbCf17bFXOYAb-l0HJ7rQ.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>本指南将逐步介绍构建用于测试、开发和生产环境的 Flask RESTPlus Web 应用程序的方法。 将使用基于 Linux 的操作系统（Ubuntu），但是大多数步骤都可以在 Windows 和 Mac 上执行。</p>
<p>在继续阅读本指南之前，你应该对 Python 编程语言和 Flask 框架有基本的了解。如果你不熟悉这些内容，建议阅读介绍性文章 - <a href="https://www.freecodecamp.org/news/how-to-use-python-and-flask-to-build-a-web-app-an-in-depth-tutorial-437dbfe9f1c6/">如何使用 Python 和 Flask 构建 Web 应用程序</a>。</p>
<h4 id="">本指南的结构</h4>
<p>本指南分为以下几部分：</p>
<ul>
<li><a href="#features">功能</a></li>
<li><a href="#what-is-flask-restplus">Flask-RESTPlus 是什么？</a></li>
<li><a href="#setup-and-installation">安装和配置</a></li>
<li><a href="#project-setup-and-organization">项目配置和结构</a></li>
<li><a href="#configuration-settings">配置设定</a></li>
<li><a href="#flask-script">Flask Script</a></li>
<li><a href="#database-models-and-migration">数据库 Model 和迁移</a></li>
<li><a href="#testing">测试</a></li>
<li><a href="#configuration">配置</a></li>
<li><a href="#user-operations">User 操作</a></li>
<li><a href="#security-and-authentication">安全与认证</a></li>
<li><a href="#extending-the-app-conclusion">拓展 &amp; 结论</a></li>
</ul>
<h4 id="">功能</h4>
<p>项目将涉及以下功能和扩展。</p>
<ul>
<li><a href="https://flask-bcrypt.readthedocs.io/">Flask-Bcrypt</a>:  <em>一个 Flask 扩展，提供了 bcrypt 散列功能。</em></li>
<li><a href="https://flask-migrate.readthedocs.io/">Flask-Migrate</a>: <em>一个使用 Alembic 为 Flask 应用处理 SQLAlchemy 数据库迁移的扩展，可以通过 Flask 的命令行接口或者 Flask-Scripts 对数据库进行操作。</em></li>
<li><a href="http://flask-sqlalchemy.pocoo.org/">Flask-SQLAlchemy</a>: <em>一个 <a href="http://flask.pocoo.org">Flask</a> 扩展，给应用添加了 <a href="http://www.sqlalchemy.org">SQLAlchemy</a> 支持。</em></li>
<li><a href="https://pyjwt.readthedocs.io/">PyJWT</a>: <em>可以编码解码 JSON Web Tokens (JWT) 的 Python 库。JWT 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准（(RFC 7519)。</em></li>
<li><a href="https://flask-script.readthedocs.io/">Flask-Script</a>: <em>一个提供了向 Flask 插入外部脚本的功能的扩展，它可以运行除 web 应用之外的命令行任务。</em></li>
<li><a href="http://flask-restplus.readthedocs.io/en/stable/scaling.html">Namespaces</a> (<a href="http://exploreflask.com/en/latest/blueprints.html">Blueprints</a>)</li>
<li><a href="https://flask-restplus.readthedocs.io/">Flask-restplus</a></li>
<li>UnitTest</li>
</ul>
<h4 id="flaskrestplus">Flask-RESTPlus 是什么？</h4>
<p>Flask-RESTPlus 是 Flask 的扩展，可以通过它快速构建 REST API。Flask-RESTPlus 最佳实践鼓励配置尽可能少。它提供了大量的装饰器和工具来描述 API，并以文档化的形式将这些接口展现出来（通过 Swagger 来实现）。</p>
<h4 id="">安装和配置</h4>
<p>在 Terminal 中输入命令 <code>pip --version</code> 来检查是否已安装 pip，然后回车。</p>
<pre><code class="language-shell">pip --version
</code></pre>
<p>如果终端输出版本号，表示已安装 pip，可以继续执行下一步，否则请<a href="https://pip.pypa.io/en/latest/installing/">先安装 pip</a>，如果使用 Linux 包管理器，可以在终端上运行以下命令，回车。选择 Python 2.x 或 3.x 版本。</p>
<ul>
<li>Python 2.x</li>
</ul>
<pre><code class="language-shell">sudo apt-get install python-pip
</code></pre>
<ul>
<li>Python 3.x</li>
</ul>
<pre><code class="language-shell">sudo apt-get install python3-pip
</code></pre>
<p>设置 virtual  环境或 virtual 环境 wrapper（只需要其中之一，取决于上面安装的版本）：</p>
<pre><code class="language-shell">sudo pip install virtualenv

sudo pip3 install virtualenvwrapper
</code></pre>
<p>请按照<a href="https://medium.com/@gitudaniel/installing-virtualenvwrapper-for-python3-ad3dfea7c717">此链接</a>进行 virtual 环境 wrapper 的完整设置。</p>
<p>通过在终端上执行以下命令来创建新环境并激活它：</p>
<pre><code class="language-shell">mkproject name_of_your_project
</code></pre>
<h4 id="">项目配置和结构</h4>
<p>这里使用<a href="http://exploreflask.com/zh-CN/latest/blueprints.html#functional-structure">功能性结构</a>通过文件的功能来组织项目文件。在功能结构里，模板、静态文件、视图在三个不同的目录中。</p>
<p>在项目目录中，创建一个名为 <code>app</code> 的新包。在 <code>app</code> 内部，创建两个包 <code> main</code> 和 <code>test</code>。 目录结构如下。</p>
<pre><code>.
├── app
│   ├── __init__.py
│   ├── main
│   │   └── __init__.py
│   └── test
│       └── __init__.py
└── requirements.txt
</code></pre>
<p>接下来使用功能结构来模块化应用程序。</p>
<p>在 <code>main</code> 包中，再创建三个包，即：<code>controller</code>，<code>service</code> 和 <code>model</code>。 <code>model</code> 包将包含所有的数据库模型，而 <code>service</code> 包将包含应用程序的所有业务逻辑，最后 <code>controller</code> 包将包含所有的应用程序接口。 现在，树结构应如下所示：</p>
<pre><code>.
├── app
│   ├── __init__.py
│   ├── main
│   │   ├── controller
│   │   │   └── __init__.py
│   │   ├── __init__.py
│   │   ├── model
│   │   │   └── __init__.py
│   │   └── service
│   │       └── __init__.py
│   └── test
│       └── __init__.py
└── requirements.txt
</code></pre>
<p>现在，来安装所需的软件包。 确保已激活创建的 virtual 环境，并在终端上运行以下命令：</p>
<pre><code class="language-shell">pip install flask-bcrypt

pip install flask-restplus

pip install Flask-Migrate

pip install pyjwt

pip install Flask-Script

pip install flask_testing
</code></pre>
<p>通过运行以下命令来创建/更新 <code>requirements.txt</code> 文件：</p>
<pre><code class="language-shell">pip freeze &gt; requirements.txt
</code></pre>
<p>生成的 <code>requirements.txt</code> 文件应该如下：</p>
<pre><code>alembic==0.9.8
aniso8601==3.0.0
bcrypt==3.1.4
cffi==1.11.5
click==6.7
Flask==0.12.2
Flask-Bcrypt==0.7.1
Flask-Migrate==2.1.1
flask-restplus==0.10.1
Flask-Script==2.0.6
Flask-SQLAlchemy==2.3.2
Flask-Testing==0.7.1
itsdangerous==0.24
Jinja2==2.10
jsonschema==2.6.0
Mako==1.0.7
MarkupSafe==1.0
pycparser==2.18
PyJWT==1.6.0
python-dateutil==2.7.0
python-editor==1.0.3
pytz==2018.3
six==1.11.0
SQLAlchemy==1.2.5
Werkzeug==0.14.1
</code></pre>
<h4 id="">配置设定</h4>
<p>在 <code>main</code> 包中创建一个名为 <code>config.py</code> 的文件，内容如下：</p>
<pre><code class="language-python">import os




basedir = os.path.abspath(os.path.dirname(__file__))

class Config:
    SECRET_KEY = os.getenv('SECRET_KEY', 'my_precious_secret_key')
    DEBUG = False


class DevelopmentConfig(Config):
    
    
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'flask_boilerplate_main.db')
    SQLALCHEMY_TRACK_MODIFICATIONS = False


class TestingConfig(Config):
    DEBUG = True
    TESTING = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'flask_boilerplate_test.db')
    PRESERVE_CONTEXT_ON_EXCEPTION = False
    SQLALCHEMY_TRACK_MODIFICATIONS = False


class ProductionConfig(Config):
    DEBUG = False
    
    


config_by_name = dict(
    dev=DevelopmentConfig,
    test=TestingConfig,
    prod=ProductionConfig
)

key = Config.SECRET_KEY
</code></pre>
<p>配置文件包含三个环境设置 class，其中包括 <code>testing</code>、<code>development</code> 和  <code>production</code>。</p>
<p>这里将使用<a href="http://flask.pocoo.org/docs/0.12/patterns/appfactories/">应用程序工厂模式</a>创建 Flask 对象。在对不同的配置创建多个实例时这个模式很方便。通过传入必填参数调用 <code>create_app</code> 函数，可以方便地在测试、开发和生产环境之间进行切换。</p>
<p>在 <code>main</code> 包内的 <code>__init__.py</code> 文件中，输入以下代码：</p>
<pre><code class="language-python">from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt

from .config import config_by_name

db = SQLAlchemy()
flask_bcrypt = Bcrypt()


def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config_by_name[config_name])
    db.init_app(app)
    flask_bcrypt.init_app(app)

    return app
</code></pre>
<h4 id="flaskscript">Flask Script</h4>
<p>现在，创建应用程序入口点。在项目的根目录中，创建一个名为 <code>manage.py</code> 的文件，其内容如下：</p>
<pre><code class="language-python">import os
import unittest

from flask_migrate import Migrate, MigrateCommand
from flask_script import Manager

from app.main import create_app, db

app = create_app(os.getenv('BOILERPLATE_ENV') or 'dev')

app.app_context().push()

manager = Manager(app)

migrate = Migrate(app, db)

manager.add_command('db', MigrateCommand)

@manager.command
def run():
    app.run()

@manager.command
def test():
    """Runs the unit tests."""
    tests = unittest.TestLoader().discover('app/test', pattern='test*.py')
    result = unittest.TextTestRunner(verbosity=2).run(tests)
    if result.wasSuccessful():
        return 0
    return 1

if __name__ == '__main__':
    manager.run()
</code></pre>
<p>上面的 <code>manage.py</code> 中的代码做了以下操作：</p>
<ul>
<li><code>line 4</code>和 <code>5</code> 分别导入 migration 和 manager 模块（很快会用到 migration 命令）。</li>
<li><code>line 9</code> 调用开始创建的 <code>create_app</code> 函数，使用环境变量中的必添参数创建应用程序实例，该参数可以是 - <code>dev</code>、<code>prod</code> 或 <code>test</code>。如果环境变量中未设置任何值，则默认使用 <code>dev</code>。</li>
<li><code>line 13</code>  和 <code>15</code> 将 app 实例传递给它们各自的构造函数来实例化 manager 和 migrate class。</li>
<li>在 <code>line 17</code> 中，将 <code>db</code> 和 <code>MigrateCommand</code> 实例传递给 <code>manager</code> 的 <code>add_command</code> 接口，以通过 Flask-Script 暴露所有数据库迁移命令。</li>
<li><code>line 20</code> 和 <code>25</code> 将这两个函数标记为可从命令行执行函数。</li>
</ul>
<blockquote>
<p><em>Flask-Migrate 暴露了两个 class，<code>Migrate</code> 和 <code>MigrateCommand</code>。 <code>Migrate</code> class 包含扩展的所有功能。<code>MigrateCommand</code> class 仅在需要通过 Flask-Script 扩展公开数据库迁移命令时使用。</em></p>
</blockquote>
<p>此时，可以通过在项目根目录中运行以下命令来测试应用程序。</p>
<pre><code>python manage.py run
</code></pre>
<p>如果一切正常，应该会看到类似以下内容：</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*5_9GQCi5Z7J13iUbp82bHw.png" alt="1*5_9GQCi5Z7J13iUbp82bHw" width="600" height="400" loading="lazy"></p>
<h4 id="model">数据库 Model 和迁移</h4>
<p>现在，开始创建模型。这里使用 sqlalchemy 的 <code>db</code> 实例来创建模型。</p>
<p><code>db</code> 实例包含 **sqlalchemy ** 和 <strong><a href="http://docs.sqlalchemy.org/en/latest/orm/scalar_mapping.html#module-sqlalchemy.orm">sqlalchemy.orm</a></strong>，它提供了一个名为 Model 的 class，该 class 是用于声明 model 的基础性声明。</p>
<p>在 <code>model</code> 包中，创建一个名为 <code>user.py</code> 的文件，其内容如下：</p>
<pre><code class="language-python">from .. import db, flask_bcrypt

class User(db.Model):
    """ User Model for storing user related details """
    __tablename__ = "user"

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    email = db.Column(db.String(255), unique=True, nullable=False)
    registered_on = db.Column(db.DateTime, nullable=False)
    admin = db.Column(db.Boolean, nullable=False, default=False)
    public_id = db.Column(db.String(100), unique=True)
    username = db.Column(db.String(50), unique=True)
    password_hash = db.Column(db.String(100))

    @property
    def password(self):
        raise AttributeError('password: write-only field')

    @password.setter
    def password(self, password):
        self.password_hash = flask_bcrypt.generate_password_hash(password).decode('utf-8')

    def check_password(self, password):
        return flask_bcrypt.check_password_hash(self.password_hash, password)

    def __repr__(self):
        return "&lt;User '{}'&gt;".format(self.username)
</code></pre>
<p>上面的 <code>user.py</code> 代码执行以下操作：</p>
<ul>
<li><code>line 3:</code> <code>user</code>  class 继承自 <code>db.Model</code> class，声明为 sqlalchemy 的模型。</li>
<li><code>line7</code> 行到 <code>13</code> 会为 user 表创建所需的列。</li>
<li><code>line 21</code> 是 <code>password_hash</code> 字段的 setter，它使用 <code>flask-bcrypt</code> 来使用提供的密码来生成哈希。</li>
<li><code>line 24</code> 将给定的密码和已经保存的 <code>password_hash</code> 进行比较。</li>
</ul>
<p>现在要从刚刚创建的 <code>user</code> model 生成数据库表，将通过 <code>manager</code> 接口的 <code>migrateCommand</code> 来生成。为了使 <code>manager</code> 能够检测到我们的 model，我们必须通过在 <code>manage.py</code> 文件中添加以下代码来导入 <code>user</code> 模型：</p>
<pre><code class="language-python">...
from app.main.model import user
...
</code></pre>
<p>现在，可以通过在项目根目录上运行以下命令来继续执行 <strong>migration</strong>：</p>
<p>1. 使用 <code>init</code> 命令启动一个迁移文件夹以使 Alembic 执行迁移。</p>
<pre><code class="language-shell">python manage.py db init
</code></pre>
<p>2. 使用 <code>migrate</code> 命令检测 model 的更改并创建迁移脚本。这不会影响数据库。</p>
<pre><code class="language-python">python manage.py db migrate --message 'initial database migration'
</code></pre>
<p>2. 使用 <code>upgrade</code> 命令将迁移脚本应用于数据库</p>
<pre><code class="language-python">python manage.py db upgrade
</code></pre>
<p>如果一切顺利运行，则应该创建了一个新的 sqlite 数据库，并在主包内生成一个<code>flask_boilerplate_main.db</code> 文件。</p>
<blockquote>
<p>每次数据库模型更改时，都执行一次 <code>migrate</code> 和 <code>upgrade</code> 命令</p>
</blockquote>
<h3 id="">测试</h3>
<h4 id="">配置</h4>
<p>为确保的环境配置的设置没问题，来编写一些测试。</p>
<p>在测试包中创建一个名为 <code>test_config.py</code> 的文件，其内容如下：</p>
<pre><code class="language-python">import os
import unittest

from flask import current_app
from flask_testing import TestCase

from manage import app
from app.main.config import basedir


class TestDevelopmentConfig(TestCase):
    def create_app(self):
        app.config.from_object('app.main.config.DevelopmentConfig')
        return app

    def test_app_is_development(self):
        self.assertFalse(app.config['SECRET_KEY'] is 'my_precious')
        self.assertTrue(app.config['DEBUG'] is True)
        self.assertFalse(current_app is None)
        self.assertTrue(
            app.config['SQLALCHEMY_DATABASE_URI'] == 'sqlite:///' + os.path.join(basedir, 'flask_boilerplate_main.db')
        )


class TestTestingConfig(TestCase):
    def create_app(self):
        app.config.from_object('app.main.config.TestingConfig')
        return app

    def test_app_is_testing(self):
        self.assertFalse(app.config['SECRET_KEY'] is 'my_precious')
        self.assertTrue(app.config['DEBUG'])
        self.assertTrue(
            app.config['SQLALCHEMY_DATABASE_URI'] == 'sqlite:///' + os.path.join(basedir, 'flask_boilerplate_test.db')
        )


class TestProductionConfig(TestCase):
    def create_app(self):
        app.config.from_object('app.main.config.ProductionConfig')
        return app

    def test_app_is_production(self):
        self.assertTrue(app.config['DEBUG'] is False)


if __name__ == '__main__':
    unittest.main()
</code></pre>
<p>使用以下命令运行测试：</p>
<pre><code class="language-shell">python manage.py test
</code></pre>
<p>应该会看到以下输出：</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*6_E40FN6IFz5EtwL1JqQTw.png" alt="1*6_E40FN6IFz5EtwL1JqQTw" width="600" height="400" loading="lazy"></p>
<h4 id="user">User 操作</h4>
<p>现在，来做如下与 user 相关的操作：</p>
<ul>
<li>创建一个新 user</li>
<li>通过 user 的 <code>public_id</code> 获取一个已注册的 user</li>
<li>获取所有的注册 user</li>
</ul>
<p>**User Service class：**此 class 处理与 user model 有关的所有逻辑。<br>
在 <code>service</code> 包中，创建一个具有以下内容的新文件 <code>user_service.py</code>：</p>
<pre><code class="language-python">import uuid
import datetime

from app.main import db
from app.main.model.user import User


def save_new_user(data):
    user = User.query.filter_by(email=data['email']).first()
    if not user:
        new_user = User(
            public_id=str(uuid.uuid4()),
            email=data['email'],
            username=data['username'],
            password=data['password'],
            registered_on=datetime.datetime.utcnow()
        )
        save_changes(new_user)
        response_object = {
            'status': 'success',
            'message': 'Successfully registered.'
        }
        return response_object, 201
    else:
        response_object = {
            'status': 'fail',
            'message': 'User already exists. Please Log in.',
        }
        return response_object, 409


def get_all_users():
    return User.query.all()


def get_a_user(public_id):
    return User.query.filter_by(public_id=public_id).first()


def save_changes(data):
    db.session.add(data)
    db.session.commit()
</code></pre>
<p>上面的 <code>user_service.py</code> 中的代码执行以下操作：</p>
<ul>
<li><code>line 8</code> 到 <code>29</code> 首先检查该 user 是否已存在然后在创建新 user；如果 user 不存在，则返回成功的 <code>response_object</code>，否则返回错误代码 <code>409</code>  和失败的 <code>response_object</code>。</li>
<li><code>line 33</code> 和 <code>37</code> 分别通过提供 <code>public_id</code> 返回所有注册用户的列表和一个用户的对象。</li>
<li><code>line 40</code> 到 <code>42</code> 将更改提交到数据库。</li>
</ul>
<blockquote>
<p>无需在使用 <a href="http://flask.pocoo.org/docs/0.12/api/#module-flask.json">jsonify</a> 将对象格式化为 JSON，Flask-restplus 会自动将其格式化</p>
</blockquote>
<p>在 <code>main</code> 包中，创建一个名为 <code>util</code> 的新包。该软件包将包含我们在应用程序中可能需要的所有必要工具。</p>
<p>在 <code>util</code> 包中，创建一个新文件 <code>dto.py</code>。顾名思义，就是数据传输对象（<a href="https://en.wikipedia.org/wiki/Data_transfer_object">DTO</a>）将负责在进程之间传递数据。在这里，它将用于封装 API 调用的数据。在使用中会更容易理解。</p>
<pre><code class="language-python">from flask_restplus import Namespace, fields


class UserDto:
    api = Namespace('user', description='user related operations')
    user = api.model('user', {
        'email': fields.String(required=True, description='user email address'),
        'username': fields.String(required=True, description='user username'),
        'password': fields.String(required=True, description='user password'),
        'public_id': fields.String(description='user Identifier')
    })
</code></pre>
<p>上面的 <code>dto.py</code> 中代码执行以下操作：</p>
<ul>
<li><code>line 5</code> 为与 user 相关的操作创建了一个新的命名空间。Flask-RESTPlus 提供了一种使用几乎与<a href="http://exploreflask.com/en/latest/blueprints.html#what-is-a-blueprint">蓝图</a>模式相同的的方法。主要思想是将应用拆分为可重用的命名空间。命名空间模块将包含 models 和资源声明。</li>
<li><code>line 6</code> 通过 <code>line 5</code> 中的 <code>api</code> 命名空间提供的 <code>model</code> 接口创建了新用户的 dto。</li>
</ul>
<p><strong>User Controller：</strong> User Controller class 处理所有传入 HTTP 的与 user 有关的请求。</p>
<p>在  <code>controller</code> 包下，创建一个名为 <code>user_controller.py</code> 的新文件，其内容如下：</p>
<pre><code class="language-python">from flask import request
from flask_restplus import Resource

from ..util.dto import UserDto
from ..service.user_service import save_new_user, get_all_users, get_a_user

api = UserDto.api
_user = UserDto.user


@api.route('/')
class UserList(Resource):
    @api.doc('list_of_registered_users')
    @api.marshal_list_with(_user, envelope='data')
    def get(self):
        """List all registered users"""
        return get_all_users()

    @api.response(201, 'User successfully created.')
    @api.doc('create a new user')
    @api.expect(_user, validate=True)
    def post(self):
        """Creates a new User """
        data = request.json
        return save_new_user(data=data)


@api.route('/&lt;public_id&gt;')
@api.param('public_id', 'The User identifier')
@api.response(404, 'User not found.')
class User(Resource):
    @api.doc('get a user')
    @api.marshal_with(_user)
    def get(self, public_id):
        """get a user given its identifier"""
        user = get_a_user(public_id)
        if not user:
            api.abort(404)
        else:
            return user
</code></pre>
<p><code>line 1</code> 到 <code>8</code> 行会导入 user controller 所需的所有资源。在 user controller 中定义了两个具体的 class，分别是 <code>userList</code> 和 <code>user</code>。这两个 class 扩展了抽象的 flask-restplus 资源。</p>
<blockquote>
<p><em>具体资源应从此 class 扩展并暴露每个支持的 HTTP 方法。如果使用不支持的 HTTP 方法调用资源，则 API 将返回状态为 405 Method Not Allowe 的响应。否则，将调用适当的方法并在将资源添加到 API 实例时传递所有的 URL 参数</em>。</p>
</blockquote>
<p>上面 <code> line 7</code>中的 <code>api</code> 命名空间为 controller 提供了多个装饰器，包括但不限于以下几种：</p>
<ul>
<li>api.<strong>route</strong>: <strong>route 资源的装饰器</strong></li>
<li>api.<strong>marshal_with</strong>: <strong>一个用来指定需要序列化字段的装饰器 (就是用到的之前创建的 <code>__userDto__</code> )</strong></li>
<li>api.<strong>marshal_list_with</strong>: <strong><code>as_list = True__</code> 上面的 <code>__marshal_with__</code> 的快捷装饰器</strong></li>
<li>api.<strong>doc</strong>: <strong>用于向装饰对象添加 api 文档的装饰器</strong></li>
<li>api.<strong>response:</strong> <strong>用于指定预期的一个响应的装饰器</strong></li>
<li>api.<strong>expect:</strong><strong>一个装饰器，用于指定预期的输入 model（仍然使用</strong><code>__userDto__</code> <strong>作为预期的输入）的装饰器</strong></li>
<li>api.<strong>param:</strong> <strong>指定一个预期参数的装饰器</strong></li>
</ul>
<p>现在，已经使用 user controller 定义了命名空间。现在是时候将其添加到应用程序入口了。</p>
<p>在 <code>app</code> 包的 <code>__init__.py</code> 文件中，输入以下内容：</p>
<pre><code class="language-python">
from flask_restplus import Api
from flask import Blueprint

from .main.controller.user_controller import api as user_ns

blueprint = Blueprint('api', __name__)

api = Api(blueprint,
          title='FLASK RESTPLUS API BOILER-PLATE WITH JWT',
          version='1.0',
          description='a boilerplate for flask restplus web service'
          )

api.add_namespace(user_ns, path='/user')
</code></pre>
<p>上面的  <code>blueprint.py</code>  代码执行以下操作：</p>
<ul>
<li>在 <code>line 8</code> 中，通过传入 <code>name</code> 和 <code>import_name</code> 来创建一个蓝图实例。<code>API</code> 是应用程序资源的主要入口，因此需要在 <code>line 10</code> 中使用 <code>blueprint</code> 进行初始化。</li>
<li>在 <code>line 16</code> 中，将 user 命名空间 <code>user_ns</code> 添加到 API 实例中的命名空间列表中。</li>
</ul>
<p>现在，已经定义了蓝图。 现在是时候在 Flask 应用中注册它了。<br>
更新 <code>manage.py</code>，导入 <code>blueprint</code> 并将其注册到 Flask 应用程序实例中。</p>
<pre><code class="language-python">from app import blueprint
...

app = create_app(os.getenv('BOILERPLATE_ENV') or 'dev')
app.register_blueprint(blueprint)

app.app_context().push()

...
</code></pre>
<p>现在测试一下的应用程序，看看是否一切正常。</p>
<pre><code class="language-shell">python manage.py run
</code></pre>
<p>现在，在浏览器中打开 URL <a href="http://127.0.0.1:5000/">http://127.0.0.1:5000</a>。 应该可以看到 swagger 的文档。</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*Us_S2WLR3AQAyfOvkzZ38Q.png" alt="1*Us_S2WLR3AQAyfOvkzZ38Q" width="600" height="400" loading="lazy"></p>
<p>让我们使用 swagger 的测试功能来测试 <strong>create new user</strong> 接口。</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*x3oZjCsUXVHjP4_YgndmFA.png" alt="1*x3oZjCsUXVHjP4_YgndmFA" width="600" height="400" loading="lazy"></p>
<p>应该会得到如下响应</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*ITTWVn8rJbIG-muhQCXsWg.png" alt="1*ITTWVn8rJbIG-muhQCXsWg" width="600" height="400" loading="lazy"></p>
<h4 id="">安全与认证</h4>
<p>创建一个 <code> blacklistToken</code> model 来存储列入黑名单的 tokens。在 <code>models</code> 包中，创建具有以下内容的 <code>blacklist.py</code>文件：</p>
<pre><code class="language-python">from .. import db
import datetime


class BlacklistToken(db.Model):
    """
    Token Model for storing JWT tokens
    """
    __tablename__ = 'blacklist_tokens'

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    token = db.Column(db.String(500), unique=True, nullable=False)
    blacklisted_on = db.Column(db.DateTime, nullable=False)

    def __init__(self, token):
        self.token = token
        self.blacklisted_on = datetime.datetime.now()

    def __repr__(self):
        return '&lt;id: token: {}'.format(self.token)

    @staticmethod
    def check_blacklist(auth_token):
        
        res = BlacklistToken.query.filter_by(token=str(auth_token)).first()
        if res:
            return True
        else:
            return False
</code></pre>
<p>别忘了 migrate  所做的更改以对数据库生效。<br>
在 <code>manage.py</code> 中导入 <code>blacklist</code> 类。</p>
<pre><code>from app.main.model import blacklist
</code></pre>
<p>运行 <code>migrate</code> 和 <code>upgrade</code> 命令</p>
<pre><code class="language-shell">python manage.py db migrate --message 'add blacklist table'

python manage.py db upgrade
</code></pre>
<p>接下来，在服务包中创建内容如下的 <code>blacklist_service.py</code>，以将令牌列入黑名单：</p>
<pre><code class="language-python">from app.main import db
from app.main.model.blacklist import BlacklistToken


def save_token(token):
    blacklist_token = BlacklistToken(token=token)
    try:
        
        db.session.add(blacklist_token)
        db.session.commit()
        response_object = {
            'status': 'success',
            'message': 'Successfully logged out.'
        }
        return response_object, 200
    except Exception as e:
        response_object = {
            'status': 'fail',
            'message': e
        }
        return response_object, 200
</code></pre>
<p>使用编码和解码令牌的静态方法来更新 <code>user</code> 模型。添加以下导入：</p>
<pre><code class="language-python">import datetime
import jwt
from app.main.model.blacklist import BlacklistToken
from ..config import key
</code></pre>
<ul>
<li>编码</li>
</ul>
<pre><code>def encode_auth_token(self, user_id):
        """
        Generates the Auth Token
        :return: string
        """
        try:
            payload = {
                'exp': datetime.datetime.utcnow() + datetime.timedelta(days=1, seconds=5),
                'iat': datetime.datetime.utcnow(),
                'sub': user_id
            }
            return jwt.encode(
                payload,
                key,
                algorithm='HS256'
            )
        except Exception as e:
            return e
</code></pre>
<ul>
<li>解码：在对身份验证令牌进行解码时，会考虑列入黑名单的令牌、过期的令牌和无效的令牌。</li>
</ul>
<pre><code class="language-python">  @staticmethod  
  def decode_auth_token(auth_token):
        """
        Decodes the auth token
        :param auth_token:
        :return: integer|string
        """
        try:
            payload = jwt.decode(auth_token, key)
            is_blacklisted_token = BlacklistToken.check_blacklist(auth_token)
            if is_blacklisted_token:
                return 'Token blacklisted. Please log in again.'
            else:
                return payload['sub']
        except jwt.ExpiredSignatureError:
            return 'Signature expired. Please log in again.'
        except jwt.InvalidTokenError:
            return 'Invalid token. Please log in again.'
</code></pre>
<p>现在，为 <code>user</code> model 编写一个测试，以确保 <code>encode</code> 和 <code>decode</code> 功能运行正常。</p>
<p>在 <code>test</code> 包中，创建内容如下的 <code>base.py</code> 文件：</p>
<pre><code class="language-python">from flask_testing import TestCase
from app.main import db
from manage import app


class BaseTestCase(TestCase):
    """ Base Tests """

    def create_app(self):
        app.config.from_object('app.main.config.TestingConfig')
        return app

    def setUp(self):
        db.create_all()
        db.session.commit()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
</code></pre>
<p><code>BaseTestCase</code> 在扩展它的每个测试用例之前和之后设置了测试环境。</p>
<p>使用以下测试用例创建 <code>test_user_medol.py</code>：</p>
<pre><code class="language-python">import unittest
import datetime

from app.main import db
from app.main.model.user import User
from app.test.base import BaseTestCase


class TestUserModel(BaseTestCase):

    def test_encode_auth_token(self):
        user = User(
            email='test@test.com',
            password='test',
            registered_on=datetime.datetime.utcnow()
        )
        db.session.add(user)
        db.session.commit()
        auth_token = user.encode_auth_token(user.id)
        self.assertTrue(isinstance(auth_token, bytes))

    def test_decode_auth_token(self):
        user = User(
            email='test@test.com',
            password='test',
            registered_on=datetime.datetime.utcnow()
        )
        db.session.add(user)
        db.session.commit()
        auth_token = user.encode_auth_token(user.id)
        self.assertTrue(isinstance(auth_token, bytes))
        self.assertTrue(User.decode_auth_token(auth_token.decode("utf-8") ) == 1)


if __name__ == '__main__':
    unittest.main()
</code></pre>
<p>使用 <code>python manage.py test</code> 运行测试。所有测试都应该通过。</p>
<p>来为 <code>login</code> 和 <code>logout</code>  创建一个 <strong>authentication endpoints</strong>。</p>
<ul>
<li>首先，我们需要一个 <code>dto</code> 作为登录 payload。将在登录端点的 <code>@expect</code> 注解中使用 auth dto。 将下面的代码添加到 <code>dto.py</code> 中</li>
</ul>
<pre><code class="language-python">class AuthDto:
    api = Namespace('auth', description='authentication related operations')
    user_auth = api.model('auth_details', {
        'email': fields.String(required=True, description='The email address'),
        'password': fields.String(required=True, description='The user password '),
    })
</code></pre>
<ul>
<li>接下来，创建一个身份验证 helper 类，以处理所有与身份验证相关的操作。该 <code>auth_helper.py</code> 将包含在服务包中，并将包含两个静态方法，分别是 <code>login_user</code> 和 <code>logout_user</code>。</li>
</ul>
<blockquote>
<p><strong>用户退出登录后，该用户的令牌将被列入黑名单，即该用户无法使用该令牌再次登录。</strong></p>
</blockquote>
<pre><code class="language-python">from app.main.model.user import User
from ..service.blacklist_service import save_token


class Auth:

    @staticmethod
    def login_user(data):
        try:
            
            user = User.query.filter_by(email=data.get('email')).first()
            if user and user.check_password(data.get('password')):
                auth_token = user.encode_auth_token(user.id)
                if auth_token:
                    response_object = {
                        'status': 'success',
                        'message': 'Successfully logged in.',
                        'Authorization': auth_token.decode()
                    }
                    return response_object, 200
            else:
                response_object = {
                    'status': 'fail',
                    'message': 'email or password does not match.'
                }
                return response_object, 401

        except Exception as e:
            print(e)
            response_object = {
                'status': 'fail',
                'message': 'Try again'
            }
            return response_object, 500

    @staticmethod
    def logout_user(data):
        if data:
            auth_token = data.split(" ")[1]
        else:
            auth_token = ''
        if auth_token:
            resp = User.decode_auth_token(auth_token)
            if not isinstance(resp, str):
                
                return save_token(token=auth_token)
            else:
                response_object = {
                    'status': 'fail',
                    'message': resp
                }
                return response_object, 401
        else:
            response_object = {
                'status': 'fail',
                'message': 'Provide a valid auth token.'
            }
            return response_object, 403
</code></pre>
<ul>
<li>现在让为 <code>login</code> 和 <code>logout</code> 操作创建 API。在 controller 包中，创建具有以下内容的 <code>auth_controller.py</code>：</li>
</ul>
<pre><code class="language-python">from flask import request
from flask_restplus import Resource

from app.main.service.auth_helper import Auth
from ..util.dto import AuthDto

api = AuthDto.api
user_auth = AuthDto.user_auth


@api.route('/login')
class UserLogin(Resource):
    """
        User Login Resource
    """
    @api.doc('user login')
    @api.expect(user_auth, validate=True)
    def post(self):
        
        post_data = request.json
        return Auth.login_user(data=post_data)


@api.route('/logout')
class LogoutAPI(Resource):
    """
    Logout Resource
    """
    @api.doc('logout a user')
    def post(self):
        
        auth_header = request.headers.get('Authorization')
        return Auth.logout_user(data=auth_header)
</code></pre>
<ul>
<li>此时，剩下的事情就是向应用程序 <code>Blueprint</code> 注册 auth <code>api</code>  命名空间。</li>
</ul>
<p>如下更新 <code>app</code> 软件包的 <code>__init __.py</code> 文件</p>
<pre><code class="language-python">
from flask_restplus import Api
from flask import Blueprint

from .main.controller.user_controller import api as user_ns
from .main.controller.auth_controller import api as auth_ns

blueprint = Blueprint('api', __name__)

api = Api(blueprint,
          title='FLASK RESTPLUS API BOILER-PLATE WITH JWT',
          version='1.0',
          description='a boilerplate for flask restplus web service'
          )

api.add_namespace(user_ns, path='/user')
api.add_namespace(auth_ns)
</code></pre>
<p>使用 <code>python manage.py run</code> 运行应用程序，然后在浏览器中打开网址 <a href="http://127.0.0.1:5000/">http://127.0.0.1:5000</a>。</p>
<p>swagger 文档现在应该展示出新创建的带有 <code>login</code> 和 <code>logout</code> 接口的 <code>auth </code>命名空间。</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*K4ZVMOwsOIIzBOV8bfqJew.png" alt="1*K4ZVMOwsOIIzBOV8bfqJew" width="600" height="400" loading="lazy"></p>
<p>在编写测试以确保身份验证能够正常工作之前，先修改注册接口，以在 user 注册成功后自动登录。</p>
<p>将下面的方法 <code>generate_token</code> 添加到 <code>user_service.py</code> 中：</p>
<pre><code class="language-python">def generate_token(user):
    try:
        
        auth_token = user.encode_auth_token(user.id)
        response_object = {
            'status': 'success',
            'message': 'Successfully registered.',
            'Authorization': auth_token.decode()
        }
        return response_object, 201
    except Exception as e:
        response_object = {
            'status': 'fail',
            'message': 'Some error occurred. Please try again.'
        }
        return response_object, 401
</code></pre>
<p><code>generate_token</code> 方法通过对用户 <code>id</code> 进行编码来生成身份验证<strong>令牌</strong>。此<strong>令牌</strong>作为响应返回。</p>
<p>接下来，在下面的 <code>save_new_user</code> 方法中替换 <strong>return</strong> 代码块</p>
<pre><code class="language-python">response_object = {
    'status': 'success',
    'message': 'Successfully registered.'
}
return response_object, 201
</code></pre>
<p>为</p>
<pre><code class="language-python">return generate_token(new_user)
</code></pre>
<p>现在该测试 <code>login</code> 和 <code>logout</code> 功能了。在测试包中创建一个具有以下内容的新测试文件 <code>test_auth.py</code>：</p>
<pre><code class="language-python">import unittest
import json
from app.test.base import BaseTestCase


def register_user(self):
    return self.client.post(
        '/user/',
        data=json.dumps(dict(
            email='example@gmail.com',
            username='username',
            password='123456'
        )),
        content_type='application/json'
    )


def login_user(self):
    return self.client.post(
        '/auth/login',
        data=json.dumps(dict(
            email='example@gmail.com',
            password='123456'
        )),
        content_type='application/json'
    )


class TestAuthBlueprint(BaseTestCase):

    def test_registered_user_login(self):
            """ Test for login of registered-user login """
            with self.client:
                
                user_response = register_user(self)
                response_data = json.loads(user_response.data.decode())
                self.assertTrue(response_data['Authorization'])
                self.assertEqual(user_response.status_code, 201)

                
                login_response = login_user(self)
                data = json.loads(login_response.data.decode())
                self.assertTrue(data['Authorization'])
                self.assertEqual(login_response.status_code, 200)

    def test_valid_logout(self):
        """ Test for logout before token expires """
        with self.client:
            
            user_response = register_user(self)
            response_data = json.loads(user_response.data.decode())
            self.assertTrue(response_data['Authorization'])
            self.assertEqual(user_response.status_code, 201)

            
            login_response = login_user(self)
            data = json.loads(login_response.data.decode())
            self.assertTrue(data['Authorization'])
            self.assertEqual(login_response.status_code, 200)

            
            response = self.client.post(
                '/auth/logout',
                headers=dict(
                    Authorization='Bearer ' + json.loads(
                        login_response.data.decode()
                    )['Authorization']
                )
            )
            data = json.loads(response.data.decode())
            self.assertTrue(data['status'] == 'success')
            self.assertEqual(response.status_code, 200)

if __name__ == '__main__':
    unittest.main()
</code></pre>
<p>请访问 <a href="https://github.com/cosmic-byte/flask-restplus-boilerplate">GitHub repo</a> 以获得更详尽的测试用例。</p>
<p>到目前为止，我们已经成功创建了接口，实现了登录和注销功能，但是接口仍然不受保护。</p>
<p>我们需要一种定义规则的方法，该规则确定接口是开放的的还是需要身份验证甚至是管理员特权才能访问。</p>
<p>可以通过为接口创建自定义装饰器来实现。</p>
<p>在可以保护或授权任何接口之前，需要知道当前登录的用户。为此，可以使用 flask 库的 <code>request</code> 从当前请求的 header 中提取 <code>Authorization token</code> 。然后，我们将从  <code>Authorization token</code> 中解码用户详细信息。</p>
<p>在 <code>auth_helper.py</code> 文件的 <code>Auth</code> 类中，添加以下静态方法：</p>
<pre><code class="language-python">@staticmethod
def get_logged_in_user(new_request):
        
        auth_token = new_request.headers.get('Authorization')
        if auth_token:
            resp = User.decode_auth_token(auth_token)
            if not isinstance(resp, str):
                user = User.query.filter_by(id=resp).first()
                response_object = {
                    'status': 'success',
                    'data': {
                        'user_id': user.id,
                        'email': user.email,
                        'admin': user.admin,
                        'registered_on': str(user.registered_on)
                    }
                }
                return response_object, 200
            response_object = {
                'status': 'fail',
                'message': resp
            }
            return response_object, 401
        else:
            response_object = {
                'status': 'fail',
                'message': 'Provide a valid auth token.'
            }
            return response_object, 401
</code></pre>
<p>现在可以从请求中检索已登录的用户，来继续创建 <code>decorators</code>。</p>
<p>在 <code>util</code> 包中创建一个文件 <code>decorator.py</code>，内容如下：</p>
<pre><code class="language-python">from functools import wraps
from flask import request

from app.main.service.auth_helper import Auth


def token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):

        data, status = Auth.get_logged_in_user(request)
        token = data.get('data')

        if not token:
            return data, status

        return f(*args, **kwargs)

    return decorated


def admin_token_required(f):
    @wraps(f)
    def decorated(*args, **kwargs):

        data, status = Auth.get_logged_in_user(request)
        token = data.get('data')

        if not token:
            return data, status

        admin = token.get('admin')
        if not admin:
            response_object = {
                'status': 'fail',
                'message': 'admin token required'
            }
            return response_object, 401

        return f(*args, **kwargs)

    return decorated
</code></pre>
<p>有关 <strong>decorators</strong> 及其创建方法的更多信息，请查看<a href="https://realpython.com/primer-on-python-decorators/">这个链接</a>。</p>
<p>现在已经分别为有效令牌和管理员令牌创建了装饰器 <code>token_required</code> 和 <code>admin_token_required</code>，剩下的就是用 freecodecamp orgappropriate  <strong>decorator</strong> 为希望保护的接口添加注解。</p>
<p>当前，要在应用程序中执行某些任务，需要运行不同的命令来启动应用程序、运行测试、安装依赖项等。可以使用 <code>Makefile</code> 将所有命令放在一个文件中来批量执行。</p>
<p>在应用程序的根目录上，创建一个没有文件扩展名的 <code>Makefile</code>。该文件应包含以下内容：</p>
<pre><code class="language-makefile">.PHONY: clean system-packages python-packages install tests run all

clean:
   find . -type f -name '*.pyc' -delete
   find . -type f -name '*.log' -delete

system-packages:
   sudo apt install python-pip -y

python-packages:
   pip install -r requirements.txt

install: system-packages python-packages

tests:
   python manage.py test

run:
   python manage.py run

all: clean install tests run
</code></pre>
<p>这是 make file 的选项。</p>
<ol>
<li><code>make install</code> : 安装 system-packages 以及 python-packages</li>
<li><code>make clean</code> : 清理 app</li>
<li><code>make tests</code> : 运行所有 tests</li>
<li><code>make run</code> : 启动所有 application</li>
<li><code>make all</code> : 执行 <code>clean-up</code>、<code>installation</code> 、 运行 <code>tests</code> ，并 <code>starts</code>  app.</li>
</ol>
<h3 id="">拓展 &amp; 结论</h3>
<p>复制当前应用程序架构并对其进行扩展，为该应用程序添加更多功能/接口非常容易。只需查看之前已实施的路由即可。</p>
<p>如有任何问题，意见或建议，请随时发表评论。另外，如果该帖子对你有所帮助，请分享出去，这样其他人也会看到并受益。</p>
<p>请访问 <a href="https://github.com/cosmic-byte/flask-restplus-boilerplate">GitHub 仓库</a>，以获取完整的项目。</p>
<p>感谢阅读，祝进步！</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/structuring-a-flask-restplus-web-service-for-production-builds-c2ec676de563/">How to structure a Flask-RESTPlus web service for production builds</a>，作者：Greg Obinna</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 什么是霍夫曼编码？ ]]>
                </title>
                <description>
                    <![CDATA[ 许多压缩算法（例如 DEFLATE）的核心都是霍夫曼编码算法，DEPLATE 是 PNG 图像格式和 GZIP 的压缩算法。 您是否也曾想过：  * 如何压缩某些东西而不丢失任何数据？  * 为什么有些算法压缩得比其他算法好？  * GZIP 是如何工作？ 假设我们要压缩一个字符串（霍夫曼编码可以用于任何数据，但是字符串是很好的例子）。 不可避免地，在要压缩的文本中，某些字符会比其他字符出现得更频繁。 霍夫曼编码（Huffman Coding）基于这一事实，对文本进行了编码，以使 最常用的字符比不常用的字符占用更少的空间。 编码字符串 让我们使用霍夫曼编码来压缩星球大战中 Yoda 的口头禅： “do or do not”。 “do or do not” 的长度为 12 个字符。 它有几个重复的字符，因此应该可以压缩一下。 为了便于讨论，假设存储每个字符需要 8 bits（字符编码完全是另一个主题）。这句话将占用 96 bits，但是使用霍夫曼编码可以做得更好！ 先从建立树形结构开始。 数据中出现频率更高的字符更靠近树的根，而离根远的节点字符出现频率更低。 这是字符串 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/what-is-huffman-coding/</link>
                <guid isPermaLink="false">605714fab3d80b05cac6568a</guid>
                
                    <category>
                        <![CDATA[ 算法 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Sun, 21 Mar 2021 09:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/03/azharul-islam-9LMGWHqUwnc-unsplash--1-.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>许多压缩算法（例如 DEFLATE）的核心都是霍夫曼编码算法，DEPLATE 是 PNG 图像格式和 GZIP 的压缩算法。</p><p>您是否也曾想过：</p><ul><li>如何压缩某些东西而不丢失任何数据？</li><li>为什么有些算法压缩得比其他算法好？</li><li>GZIP 是如何工作？</li></ul><p>假设我们要压缩一个字符串（霍夫曼编码可以用于任何数据，但是字符串是很好的例子）。</p><p>不可避免地，在要压缩的文本中，某些字符会比其他字符出现得更频繁。 霍夫曼编码（Huffman Coding）基于这一事实，对文本进行了编码，以使<strong>最常用的字符比不常用的字符占用更少的空间</strong>。</p><h2 id="-">编码字符串</h2><p>让我们使用霍夫曼编码来压缩星球大战中 Yoda 的口头禅： “do or do not”。</p><p>“do or do not” 的长度为 12 个字符。 它有几个重复的字符，因此应该可以压缩一下。</p><p>为了便于讨论，假设存储每个字符需要 8 bits（字符编码完全是另一个主题）。这句话将占用 96 bits，但是使用霍夫曼编码可以做得更好！</p><p>先从建立树形结构开始。 数据中出现频率更高的字符更靠近树的根，而离根远的节点字符出现频率更低。</p><p>这是字符串 “do or do not” 的霍夫曼树：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/05/image-9.png" class="kg-image" alt="image-9" width="749" height="443" loading="lazy"></figure><p>字符串中最常见的字符是 “ o”（出现 4 次）和空格（出现 3 次）。请注意，这些字符的路径离根只有两步，而最不常见的字符（"t"）则有三步。</p><p>所以，可以不存储字符，而存储<strong>指向字符的路径</strong>。</p><p>从根节点开始，然后沿着树一直向着要编码的字符前进。 如果向左走，则将存储一个 <code>0</code>，如果向右走，则将存储一个 <code>1</code>。</p><p>这是使用这棵树编码第一个字符 d 的方法：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/05/image-10.png" class="kg-image" alt="image-10" width="749" height="443" loading="lazy"></figure><p>最终结果是 <code>1</code> <code>0</code> <code>0</code> - 3 位而不是 8 位。这是一个很大的改进！</p><p>编码后的全部字符串如下所示：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/05/image-11.png" class="kg-image" alt="image-11" width="749" height="125" loading="lazy"></figure><p>占用 29 位而不是 96 位，并且没有数据丢失。 优秀！</p><h2 id="--1">解码字符串</h2><p>要解码文本，只需根据 <code>0</code>（左分支）或 <code>1</code>（右分支）前进，直到获取到一个字符。 写下该字符，然后从顶部重新开始：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/05/image-12.png" class="kg-image" alt="image-12" width="749" height="443" loading="lazy"></figure><h2 id="--2">发送编码后的文本</h2><p>但是等等，当我们将编码后的文本发送给其他人时，他们不是也需要这个编码树？ 是的！ <strong>另一端需要相同的霍夫曼树才能正确解码文本</strong>。</p><p>最简单也是最低效的方法是将树与压缩文本一起发送。</p><p>当然也可以先约定同一棵树，然后使用该树对字符串进行编解码。 如果可以提前预测字符的分布，构建一个相对高效而无需每次都分析内容的树（例如，使用英文文本）也是可以的。</p><p>另一种选择是发送足够多的信息，以保证另一端与我们建立的是同一棵树（这也是 GZIP 的工作方式）。 例如，我们可以发送每个字符出现的总次数。 但是必须要小心；<strong>对于相同的文本块，可能有不止一个霍夫曼树</strong>，因此必须确保双方都以完全相同的方式构造树。</p><h2 id="--3">想要了解更多？</h2><p>查看以下链接：</p><ul><li><a href="https://www.programiz.com/dsa/huffman-coding" rel="nofollow">How to build the Huffman tree (it’s easier than you think)</a></li><li><a href="https://jvns.ca/blog/2015/02/22/how-gzip-uses-huffman-coding/" rel="nofollow">How this is used in GZIP</a></li></ul><p>原文：<a href="https://www.baseclass.io/huffman-coding/">What is Huffman Coding?</a><strong> 作者：</strong>Dave</p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
