<?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[ hylerrix han - 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[ hylerrix han - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 23 May 2026 19:22:15 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/hylerrix/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 如何在 Deno 下使用 AlpehJS 库构建 React 应用 ]]>
                </title>
                <description>
                    <![CDATA[ 如果你是刚刚开始使用 Deno 的前端开发人员，你可能会想知道，你是否可以使用 Deno 构建像 NextJS 或 create-react-app（CRA）应用程序一样复杂的东西？ 我最近在想同样的事情。我想尝试使用能提供高共享性的 Deno，其能够直接从 URL 运行 JavaScript 和 TypeScript 应用程序。同时 Deno 编译器支持从 URL 中导入模块，从而实现了更大的可移植性。 我查阅了线上是否有任何相应的解决方案，但我只先找到了这篇文章 [https://dev.to/adriantwarog/react-deno-server-side-rendering-with-deno-ssr-4438] ，它使用了一些复杂的技术构建了一个 SSR 的 React 应用程序。复杂得并不简单，不像 NextJS 或 CRA 那样入门友好。 所以，通过更多的查阅，我最终找到了 AlephJS 框架，有着相比之前最酷的主页。 与 NextJS 一样，Aleph 是一个零配置，由 TypeScript 驱动的 React 框架。唯一的缺点是 Aleph 仍然处 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/build-react-app-using-deno-and-alephjs/</link>
                <guid isPermaLink="false">608b6d660998fd05ae8c8137</guid>
                
                    <category>
                        <![CDATA[ Deno ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ hylerrix han ]]>
                </dc:creator>
                <pubDate>Fri, 30 Apr 2021 02:40:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/Kapture-2021-03-11-at-11.33.15-4.gif" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>如果你是刚刚开始使用 Deno 的前端开发人员，你可能会想知道，你是否可以使用 Deno 构建像 NextJS 或 create-react-app（CRA）应用程序一样复杂的东西？</p>
<p>我最近在想同样的事情。我想尝试使用能提供高共享性的 Deno，其能够直接从 URL 运行 JavaScript 和 TypeScript 应用程序。同时 Deno 编译器支持从 URL 中导入模块，从而实现了更大的可移植性。</p>
<p>我查阅了线上是否有任何相应的解决方案，但我只先找到了<a href="https://dev.to/adriantwarog/react-deno-server-side-rendering-with-deno-ssr-4438">这篇文章</a>，它使用了一些复杂的技术构建了一个 SSR 的 React 应用程序。复杂得并不简单，不像 NextJS 或 CRA 那样入门友好。</p>
<p>所以，通过更多的查阅，我最终找到了 AlephJS 框架，有着相比之前最酷的主页。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Kapture-2021-03-11-at-11.33.15-3.gif" alt="Kapture-2021-03-11-at-11.33.15-3" width="600" height="400" loading="lazy"></p>
<p>与 NextJS 一样，Aleph 是一个零配置，由 TypeScript 驱动的 React 框架。唯一的缺点是 Aleph 仍然处于 Alpha 状态。</p>
<p>因此，让我们开始使用 AlephJS，来在 Deno 中获得真正的类似于 Next 的 React 体验。它具有 Next 相同的许多约定，例如：</p>
<ul>
<li>一个 <code>/pages</code> 目录用来创建 URL 路由</li>
<li>pages 中直接支持 <code>.js .jsx .ts .tsx</code> 格式。</li>
<li>一个 <code>/public</code> 目录用来存放静态资源比如视频、音频或图片文件。</li>
<li>一个 /pages/api 目录用来存放 JavaScript 或 TypeScript 的 serverless APIs。</li>
</ul>
<h2 id="alephjs">如何从 AlephJS 开始</h2>
<p>为了能够使用到 AlephJS，你需要确保你已安装了 Deno。你可以在我<a href="https://chinese.freecodecamp.org/news/build-a-url-shortener-in-deno/">之前的这篇文章中</a>查看如何安装 Deno。</p>
<p>准备好后，你需要先通过如下命令安装 Aleph CLI：</p>
<pre><code class="language-bash">deno install -A -f -n aleph https://deno.land/x/aleph@v0.3.0-alpha.1/cli.ts
</code></pre>
<p>安装完后，你可以运行 <code>aleph -h</code> 来检查是否安装成功。</p>
<p>由于 Deno 的 URL 特性，你可以使用 <code>deno run -A [https://deno.land/x/aleph@v0.3.0-alpha.1/cli.ts](https://deno.land/x/aleph@v0.3.0-alpha.1/cli.ts) start $app_URI</code> 替换 aleph 来执行任何命令，并且无需本地安装 CLI 即可运行 Aleph 程序。</p>
<p>运行如下命令来初始化一个应用：</p>
<pre><code class="language-bash">aleph init hello
cd hello
</code></pre>
<p>然后使用 <code>aleph dev</code> 在开发模式下启动该应用，以在端口 <code>8080</code> 上启动服务器。</p>
<p>要以生产模式启动该应用程序，你必须首先 <code>build</code> 构建该应用程序。你可以通过以下命令执行此操作：</p>
<pre><code class="language-bash">aleph build # build your app
aleph start # runs built app
</code></pre>
<p>在你初始化完毕 Aleph 之后，你会发现根组件已经在 <code>pages/index.tsx</code> 中定义了。这是正常的 React 组件。你可以对其进行修改，以了解 Aleph 的工作原理。</p>
<p>你也可以通过在 <code>pages</code> 文件夹中创建更多 <code>.jsx</code> 或 <code>.tsx</code> 文件来向应用添加更多的路由。你可以<a href="https://alephjs.org/docs/basic-features/routing">在此处</a>阅读有关 Aleph 中路由的更多知识。</p>
<h2 id="deno">如何在 Deno 中导入库</h2>
<p>我之前在 freeCodeCamp 中写过有关 Deno 的文章，其中演示了一些 Deno 的基础知识，比如 URL 的导入。由于 Aleph 是 Deno 框架，因此所有导入均以“Deno 的方式”进行。</p>
<p>你可以在 Deno 应用程序中导入这两种库：</p>
<ol>
<li>导入 Deno 原生库：这些库要么是为 Deno 构建的，要么是从 npm 移植过来的，均支持 Deno 的使用。</li>
<li>从 NPM 中导入：如果你最近使用过 JS，则很可能了解 npm。不了解的话：npm（node 包管理器背后的公司）是所有 JavaScript 库的一种标准托管库。幸运的是，Deno 对 npm 库有一定限度的支持。可以使用诸如 <a href="http://esm.sh/">esm.sh</a> 或 skypack.dev 这样的工具，用户可以将 npm 库导入到 Deno 中。</li>
</ol>
<h3 id="1deno">1. 如何导入 Deno 原生库</h3>
<p>你可以直接通过 URL 导入 Deno 原生库。你也可以在这里这倒 Deno 库的列表：<a href="http://deno.land/x">deno.land/x</a>。</p>
<p>想要测试这个功能，让我们导入一个标准 Deno 日期格式库，然后在 React 页面中调用日期格式函数。在应用程序的 <code>pages</code> 文件夹中创建一个文件 <code>date-import.tsx</code>。在文件内部编写如下代码：</p>
<pre><code class="language-jsx">// react 是 Aleph 中已经安装好的基础库
import React from "react";

// 从 URL 中导入其 format 函数
import { format } from "https://deno.land/std@0.88.0/datetime/mod.ts";

// 将函数名称大写，以便将其识别为一个 React 组件
export default function DateImport() {
  // 在这里，直接调用 format 函数可以正常使用
  return &lt;section&gt;Hello all! Today is: {format(new Date(), "dd-MM-yyyy")}&lt;/section&gt;;
}
</code></pre>
<p>要查看此文件的输出，请跳转到 <a href="http://localhost:8080/date-import">localhost:8080/date-import</a>，你应该可以看到显示今天日期的页面。</p>
<h3 id="2npm">2. 如何从 NPM 中导入库</h3>
<p>要导入 npm 库，你也可以直接从 URL 导入——但在这种情况下，需要进行一些更改。自从我们讨论了 <a href="http://esm.sh/">esm.sh</a> 和 skypack.dev 以来，让我们尝试在实际中使用它们。此时，我们尝试在项目中导入 <a href="https://www.npmjs.com/package/dayjs">dayjs</a> 库。</p>
<blockquote>
<p>注意：并非所有 npm 库都可以在 Deno 中正常工作，因为它们可能依赖于特定于 Node 的功能。</p>
</blockquote>
<p>要在 <a href="http://esm.sh/">esm.sh</a> 中导入库，请将库的软件包名追加到 URL 后面。比如导入 dayjs 的话，我们可以导入 <code>[https://esm.sh/dayjs](https://esm.sh/dayjs)</code> 地址。这同时适用于你可能从库中导入的任何 CSS 文件。</p>
<p>现在，让我们在名为 <code>pages</code> 中创建一个文件 <code>dayjs-import.tsx</code>。因此，我们页面中的代码将如下所示：</p>
<pre><code class="language-jsx">// react 是 Aleph 中已经安装好的基础库
import React from "react";

// 使用 esm.sh 导入 dayjs npm 库
import dayjs from "https://esm.sh/dayjs";

// 将函数名称大写，以便将其识别为一个 React 组件
export default function DateImport() {
  // 直接调用 dayjs 函数来显示今天的日期
  return &lt;section&gt;Hello all! Today is: {dayjs().format("DD-MM-YYYY")}&lt;/section&gt;;
}
</code></pre>
<p>要查看此文件的输出，打开 <a href="http://localhost:8080/dayjs-import">localhost:8080/dayjs-import</a>，你应该会看到显示当天日期的页面。</p>
<p>但在我们进行下一步之前，有一件重要的事——你如何导入 React 下的 <code>useState</code>、<code>useEffect</code> 等？幸运的是，Aleph 开发人员已经为我们编写了一个示例。</p>
<p>进入 <code>./lib/useCounter.ts</code>，你将找到在页面中用于计数器自定义钩子的代码。</p>
<p>由于我在本文中将重点介绍 Aleph 和 React 本身，因此，要查看在 Aleph 中导入 CSS文件的所有不同方法，请访问<a href="https://alephjs.org/docs/basic-features/built-in-css-support">官方文档中的相关页面</a>。</p>
<h2 id="denoalephjs">如何使用 Deno 和 AlephJS 构建示例应用</h2>
<p>现在，让我们进入实战环境，来尝试自己在 Aleph 中构建一个 React 应用。我们将构建“Is It Down”，这是我使用现有的网站检查 API 制作的一个示例应用。这个应用将使我们能够检查一个网站当前是打开还是关闭的。</p>
<p>这是 CodeSandbox 链接：<a href="https://codesandbox.io/s/awesome-firefly-5dofg">https://codesandbox.io/s/awesome-firefly-5dofg</a></p>
<p>构建此应用将向你展示如何使用 State hook、Effect Hook 以及如何在 Aleph 中进行 API 调用。</p>
<p>在 <code>pages</code> 文件夹中创建一个名为 <code>web-checker.tsx</code> 的新文件。让我们从仅添加 UI 元素开始。我们将显示一个带标题的 <code>h1</code> 元素，一个链接到 API 的 <code>h2</code> 元素以及一个接受用户输入的 form 元素。这是一个非交互式的页面，仅显示元素。</p>
<pre><code class="language-jsx">import React from "react";

export default function App() {
	return (
    &lt;div style={{ fontFamily: "sans-serif", textAlign: "center" }}&gt;
      &lt;h1&gt;Is it Down?&lt;/h1&gt;
      &lt;h2&gt;
        Go{" "}
        &lt;a
          href="https://rapidapi.com/jakash1997/api/website-data-gathering-and-update-tracking"
          target="_blank"
        &gt;
          here
        &lt;/a&gt;{" "}
        to get an API key
      &lt;/h2&gt;

      &lt;form
        onSubmit={(e) =&gt; {
          e.preventDefault();
        }}
      &gt;
        &lt;input
          type="text"
        /&gt;
        &lt;button type="submit"&gt;Submit&lt;/button&gt;
      &lt;/form&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>接下来，为了捕获输入字段的状态，以及捕获我们必须进行的 API 调用的响应，让我们引入一下 state。</p>
<pre><code class="language-jsx">// import useState from react
import React, { useState } from "react";

export default function App() {
  // define both state variables
  const [siteURL, setUrl] = useState("");
  const [response, setResponse] = useState(undefined);
...
</code></pre>
<p>现在，我们可以再输入元素中使用此状态，以便它可以对此作出响应：</p>
<pre><code class="language-jsx">...
&lt;input
  value={siteURL}
  onChange={(e) =&gt; setUrl(e.target.value)}
  type="text"
/&gt;
...
</code></pre>
<p>当 API 返回响应时，我们还需要添加一些代码来显示响应：</p>
<pre><code class="language-jsx">...
	&lt;/form&gt;
	
	

	
	&lt;code&gt;{JSON.stringify(response, null, 2)}&lt;/code&gt;
&lt;/div&gt;
...
</code></pre>
<p>现在，准备开始集成 API。让我们尝试正确的组合请求体。此时，API 是一个简单的 <code>GET</code> 调用，因此我们只需传递一个参数和一个 API 秘钥。</p>
<p>首先转到此处来生成 API 秘钥：<a href="https://rapidapi.com/jakash1997/api/website-data-gathering-and-update-tracking">https://rapidapi.com/jakash1997/api/website-data-gathering-and-update-tracking</a>。如下截图所示，找到密码并将其存储到安全的地方：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Screenshot_2021-03-08_at_3.47.01_PM.png" alt="Screenshot_2021-03-08_at_3.47.01_PM" width="600" height="400" loading="lazy"></p>
<p>接下来，让我们创建一个单独的函数 <code>sumitData</code>，用来生成所需的请求体。我们将使用 <code>axios</code> 库进行 GET 调用，同时用到 <code>options</code> 对象。</p>
<pre><code class="language-jsx">...
const [response, setResponse] = useState(undefined);

const submitData = (siteURL) =&gt; {
  setResponse("Loading...");
  const options = {
		// passing siteURL here through object shorthand
    params: { siteURL },

		// passing the required headers here
    headers: {
      "x-rapidapi-key": "YOUR_API_KEY",
      "x-rapidapi-host":
        "website-data-gathering-and-update-tracking.p.rapidapi.com",
    },
  };

	// print options here
	console.log("options", options);
};

return (
...
</code></pre>
<p>然后，将其添加到表单的 <code>onSubmit</code> 函数中。</p>
<pre><code class="language-jsx">onSubmit={(e) =&gt; {
  e.preventDefault();
  submitData(siteURL);
}}
</code></pre>
<p>现在，每当你点击 Submit 按钮时，你都能看到我们在控制台中生成的 <strong>options</strong>。如果你能看到，代表目前为止一切都正常！</p>
<p>接下来，我们只需执行简单的步骤，即使用 <a href="http://esm.sh/">http://esm.sh</a> 来导入 axios 库并使用它进行 API 调用。</p>
<p>在 <code>React</code> 导入之后，像这样导入 <code>axios</code>：</p>
<pre><code class="language-jsx">import React, { useState } from "react";
import axios from "https://esm.sh/axios";

...
</code></pre>
<p>并将其在 submitData 函数中使用：</p>
<pre><code class="language-jsx">...
	axios
    .get(
      "https://website-data-gathering-and-update-tracking.p.rapidapi.com/sitecheck",
      options
    )
    .then(function (response) {
      setResponse(response.data);
      console.log(response.data);
    })
    .catch(function (error) {
      console.error(error);
    });
};
...
</code></pre>
<p>就这么简单！尝试再次提交表单，这次你将在屏幕和控制台上都能看到正确的结果。</p>
<h2 id="">结论</h2>
<p>现在你了解了 Aleph 的基础知识。这是一个非常有趣的工具，它使你可以将现有的 React 知识与 <a href="http://deno.land/">deno.land</a> 的前沿性和安全性结合。</p>
<p>如果你喜欢本教程，可以在 Twitter 上关注我 <a href="http://twitter.com/thewritingdev">@thewritingdev</a>。</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/build-react-app-using-deno-and-alephjs/">How to Build React Applications with Deno Using the AlephJS Library</a>，作者：<a href="https://www.freecodecamp.org/news/author/akash/">Akash Joshi</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何在 Deno 中构建一个 URL 短链生成器 ]]>
                </title>
                <description>
                    <![CDATA[ 在本文中，我们将要学习 Deno 的基础，比如如何运行一个程序并且拥抱 Deno 的安全特性。 Deno 是用 Rust 编写的一个全新 JavaScript 和 TypeScript 运行时。它提供了严格的安全性、开箱即用的 TypeScript、一个单个可运行的执行文件，以及一组经过代码审查的标准模块。 像 Node.js 下的 npm 一样，Deno 的生态库被管理在 X [https://deno.land/x/]  中心库下。我们将使用其中的一个库——Oak，在 Deno 中构建基于 REST API 的服务器。 通过使用类似 Express 的路由管理库 Oak [https://deno.land/x/oak@v6.3.0] 的基础知识后，我们将深入探讨 Deno 并构建一个完整的应用程序。 这是我们将构建此应用程序的步骤：  1. 使用基于 JSON 的配置文件来将 URL 短链映射到端口上  2. 在每个 URL 上附加有效期，以便这些重定向仅在有效的时间内生效。 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/build-a-url-shortener-in-deno/</link>
                <guid isPermaLink="false">608b6ad50998fd05ae8c810d</guid>
                
                    <category>
                        <![CDATA[ Deno ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ hylerrix han ]]>
                </dc:creator>
                <pubDate>Fri, 30 Apr 2021 02:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/5f9c9848740569d1a4ca192c.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>在本文中，我们将要学习 Deno 的基础，比如如何运行一个程序并且拥抱 Deno 的安全特性。</p>
<p>Deno 是用 Rust 编写的一个全新 JavaScript 和 TypeScript 运行时。它提供了严格的安全性、开箱即用的 TypeScript、一个单个可运行的执行文件，以及一组经过代码审查的标准模块。</p>
<p>像 Node.js 下的 npm 一样，Deno 的生态库被管理在 <a href="https://deno.land/x/">X</a> 中心库下。我们将使用其中的一个库——Oak，在 Deno 中构建基于 REST API 的服务器。</p>
<p>通过使用类似 Express 的路由管理库 <a href="https://deno.land/x/oak@v6.3.0">Oak</a> 的基础知识后，我们将深入探讨 Deno 并构建一个完整的应用程序。</p>
<p>这是我们将构建此应用程序的步骤：</p>
<ol>
<li>使用基于 JSON 的配置文件来将 URL 短链映射到端口上</li>
<li>在每个 URL 上附加有效期，以便这些重定向仅在有效的时间内生效。</li>
</ol>
<h2 id="0">0. 前置准备</h2>
<ol>
<li>从<a href="https://deno.land/#installation">这个链接</a>中安装 Deno。</li>
<li>确保你知道一些 JavaScript 基础。</li>
</ol>
<p>尽管并不是本文所需，但你还是可以通过以下视频的形式看看 Deno 的基础介绍。</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-embed-card" data-test-label="fitted">
        <div class="fluid-width-video-container">
          <div style="padding-top: 56.49999999999999%;" class="fluid-width-video-wrapper">
            <iframe width="200" height="113" src="https://www.youtube.com/embed/VQ8Jb7GLHgk?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" name="fitvid0"></iframe>
          </div>
        </div>
      </figure><!--kg-card-begin: markdown--><p>那么，让我们来正式开始？</p>
<h2 id="1">1. 如何构建路由</h2>
<p>要为我们的应用编写服务端代码，我们将使用 Oak 模块。它具有类似于 Express 定义 API 路由的语法。</p>
<p>如果我们在<a href="https://deno.land/x/oak">这个文档</a>中，“<a href="https://deno.land/x/oak">基础用法</a>”部分几乎涵盖了我们会在本文中用到的一切路由。因此，我们直接拓展这段代码来构建我们的应用。</p>
<p>要直接用到这段代码，可以在文件夹中创建一个名为 index.ts 的文件，然后将“基本用法”里的代码复制到其中。</p>
<p>要了解如何在 Deno 中运行 TypeScript 或 JavaScript 文件，你首先需要理解 Deno 是如何运行文件的。</p>
<p>你可以通过运行 <code>deno run file_name.ts</code> 或 <code>file_name.js</code> 命令来运行文件，后面可以跟一组参数标志，这些标志将为你的应用程序提供某些系统权限。</p>
<p>为了测试刚刚粘贴的“基础用法”代码能否跑通，使用如下命令：<code>deno run index.ts</code>。</p>
<p>你会看到 Deno 警示你没有授予该代码访问网络的权限。所以你需要添加 <code>-allow-net</code> 到刚才的 run 命令中。该命令最终会像这样：<code>deno run index.ts -allow-net</code>。</p>
<p>“基础用法”中的路由代码会如下所示：</p>
<pre><code class="language-jsx">router
  .get("/", (context) =&gt; {
    context.response.body = "Hello world!";
  })
  .get("/book", (context) =&gt; {
    context.response.body = Array.from(books.values());
  })
  .get("/book/:id", (context) =&gt; {
    if (context.params &amp;&amp; context.params.id &amp;&amp; books.has(context.params.id)) {
      context.response.body = books.get(context.params.id);
    }
  });
</code></pre>
<p>拆解上面的代码：首先定义了一个 <code>router</code> 对象，然后在路由器上调用 get 函数，以定义应用程序的各种端口。端口相应的逻辑在回调函数中定义。</p>
<p>例如，对于 "/" 端口，已定义了在响应体重返回 “Hello World” 的回调函数。我们可以先保持此端口不变，以通过接收响应来测试我们的应用程序服务器是否正在运行。</p>
<p>我们不需要已定义的 “/book” URL，因此可以安全地删除其定义。此时，你的路由应具有如下结构：</p>
<pre><code>router
  .get("/", (context) =&gt; {
    context.response.body = "Hello world!";
  })
  .get("/book/:id", (context) =&gt; {
    if (context.params &amp;&amp; context.params.id &amp;&amp; books.has(context.params.id)) {
      context.response.body = books.get(context.params.id);
    }
  });
</code></pre>
<p>在下一节中，我们将着手于开始实战构建应用程序。</p>
<h2 id="2howtobuildtheurlshortener">2. How to Build the URL Shortener</h2>
<p>现在让我们开始实战构建 URL 短链生成器。</p>
<p>它应该根据 <code>shortcode</code> 来重定向到目的地（<code>dest</code>）。重定向还应仅在有效期到期之前有效，可以以年-月-日格式提供。</p>
<p>基于这些假设，让我们创建一个名为 <code>urls.json</code> 的配置文件。该文件的格式为：</p>
<pre><code class="language-jsx">{
  "shortcode": {
    "dest": "destination_url_string",
    "expiryDate": "YYYY-MM-DD"
  }
}
</code></pre>
<p>你可以<a href="https://github.com/akash-joshi/deno-url-shortener/blob/master/urls.json">参考这个 JSON 文件</a>。</p>
<p>要在你的代码中读取这个 JSON 文件，请在 <code>index.ts</code> 顶部添加以下内容：</p>
<pre><code class="language-jsx">import { Application, Router } from "&lt;https://deno.land/x/oak/mod.ts&gt;";

const urls = JSON.parse(Deno.readTextFileSync("./urls.json"));

console.log(urls);
</code></pre>
<p>现在，要运行 <code>index.ts</code>，你需要另一个标志 <code>-allow-read</code>，否则 Deno 将抛出“未提供读取权限”错误。你的最终命令应该是 <code>deno run —allow-net —allow-read index.ts</code>。</p>
<p>运行此命令后，你将在终端窗口中看到打印的 JSON 文件。这意味着你的程序能够正确读取 JSON 文件。</p>
<p>如果我们回到上面看到的“基本用法”示例，则路由 “/book/:id” 风格正是我们接下来所需要的。</p>
<p>将 "/book/:id" 替换为 "/shrt/:urlid"，此时我们将基于 URL ID（<code>:urlid</code>）获得各个 URL。</p>
<p>用以下代码替换 "/book/:id" 路由中存在的现有代码：</p>
<pre><code class="language-jsx">.get("/shrt/:urlid", (context) =&gt; {
    if (context.params &amp;&amp; context.params.urlid &amp;&amp; urls[context.params.urlid]) {
      context.response.redirect(urls[context.params.urlid].dest);
    } else {
      context.response.body = "404";
    }
  });
</code></pre>
<p>路由中的 <code>if</code> 条件执行以下操作：</p>
<ol>
<li>检查参数是否存在于路由中</li>
<li>检查参数 <code>urlid</code> 是否在参数列表中</li>
<li>检查 <code>urlid</code> 是否与我们 JSON 中的任何 URL 匹配。</li>
</ol>
<p>如果有所匹配，用户将重定向到正确的 URL。如果无所匹配，则返回 404 响应。</p>
<p>想要测试这段代码，请将如下代码复制到 <code>index.ts</code> 中。路由现在长这个样子：</p>
<pre><code class="language-jsx">router
  .get("/", (context) =&gt; {
    context.response.body = "Hello world!";
  })
	.get("/shrt/:urlid", (context) =&gt; {
	    if (context.params &amp;&amp; context.params.urlid &amp;&amp; urls[context.params.urlid]) {
	      context.response.redirect(urls[context.params.urlid].dest);
	    } else {
	      context.response.body = "404";
	    }
	  });
</code></pre>
<p>接下来使用 <code>deno run —allow-net —allow-read index.ts</code> 运行文件。</p>
<p>如果你从示例中复制了 JSON 文件，此时打开 <code>http://localhost:8000/shrt/g</code>，你会正常重定向到 Google 主页上。</p>
<p>另一方面，如果你使用的随机 shortcode 在我们网址配置中不起作用，则会进入到 404 页面上。</p>
<p>但是，你会看到我们的短链器不会实时响应 JSON 文件中的变更。想要增加更多的配置，请以如下相同格式向 <code>urls.json</code> 中添加新的重定向。</p>
<pre><code>"shortcode": {
    "dest": "destination_url_string",
    "expiryDate": "YYYY-MM-DD"
  }
</code></pre>
<p>这是因为 <code>urls.json</code> 仅在刚开始时被读取一次。现在，我们需要将实时更新功能添加到服务端上。</p>
<h2 id="3howtoaddlivereloading">3. How to Add Live-Reloading</h2>
<p>为了使 <strong>urls</strong> 对象能够实时响应 JSON 文件中的更改，我们只需将 read 语句移动到路由中即可。会长这样：</p>
<pre><code class="language-jsx">.get("/shrt/:urlid", (context) =&gt; {
  const urls = JSON.parse(Deno.readTextFileSync("./urls.json"));

  if (context.params &amp;&amp; context.params.urlid &amp;&amp; urls[context.params.urlid]) {
    context.response.redirect(urls[context.params.urlid].dest);
  } else {
    context.response.body = "404";
  }
});
</code></pre>
<p>请注意我们如何是路由内部移动 URL 对象的。此时，每次调用该路由时都会读取配置文件，因此它可以实时响应 <code>urls.json</code> 文件中所做的任何更改。即使我们现在添加或删除其他重定向，我们的代码也会做出新的响应。</p>
<h2 id="4howtoaddanexpirationtotheurls">4. How to Add an Expiration to the URLs</h2>
<p>为了使我们的 URL 在某个时间点上过期，我们将使用流行的 Moment.js 库，该库使处理日期变得更容器。</p>
<p>幸运的是，它已经被<a href="https://deno.land/x/moment">良好移植到了 Deno 上</a>。要了解其工作原理，请在上一句的链接中查看其文档。</p>
<p>要在代码中使用到，请直接通过 URL 导入：</p>
<pre><code class="language-jsx">import { Application, Router } from "&lt;https://deno.land/x/oak/mod.ts&gt;";
import { moment } from "&lt;https://deno.land/x/moment/moment.ts&gt;";

const router = new Router();
</code></pre>
<p>为了检查 URL 什么时候过期，我们检查 urls 对象上的 expiryDate 键值。如下所示：</p>
<pre><code class="language-jsx">if (context.params &amp;&amp; context.params.urlid &amp;&amp; urls[context.params.urlid]) {
  if (
    urls[context.params.urlid].expiryDate &gt; moment().format("YYYY-MM-DD")
  ) {
    context.response.redirect(urls[context.params.urlid].dest);
  } else {
    context.response.body = "Link Expired";
  }
} else {
  context.response.body = "404";
}
</code></pre>
<p>在 <code>moment().format("YYYY-MM-DD")</code> 中，我们使用 moment() 来获取当前的时间。然后使用 <code>.format("YYYY-MM-DD")</code> 将其格式化为 "YYYY-MM-DD"（年-月-日）格式。</p>
<p>通过将其与我们的 <strong>expiryDate</strong> 键进行比较，我们可以检查当前的 URL 是否已过期。</p>
<p>就是这样！你已经在 Deno 中构建了功能齐全的 URL 短链器。你可以<a href="https://github.com/akash-joshi/deno-url-shortener">在这个 Github 库</a>中找打最终的代码。</p>
<p>通过将 <code>expiryDate</code> 设置为当前日期并对 <code>urls.json</code> 和我们的代码进行其它更改可以测试更多功能。</p>
<h3 id="deno">我对 Deno 的看法</h3>
<p>为了总结这篇文章，我将谈谈我对 deno.land 的思考。</p>
<p>当看到一种考虑安全性并支持 TypeScript 的服务端运行时令人耳目一新，但 Deno 在应用到生产环境之前还有很长的路要走。</p>
<p>例如，即使对于像我们刚开发的那样简单的程序，使用 TypeScript 编译也得需要约为 20 秒的时间，这非常的慢。</p>
<p>在错误报告方面，描述错误的地方还很糟糕。比如，当在函数本身中嵌入代码以读取 <code>urls.json</code> 时，Deno 无法报告未设置 <code>-allow-read</code> 标志。相反，它只会引发内部错误，而不会在终端上打印正确的错误。</p>
<blockquote>
<p>译者注：现 Deno 1.9 版本已经很好地支持权限提示了，其它的也在逐步支持中。</p>
</blockquote>
<h3 id="">接下来是什么？</h3>
<p>你可以通过构建更复杂的应用程序（比如<a href="https://css-tricks.com/build-a-chat-app-using-react-hooks-in-100-lines-of-code/">聊天应用程序</a>或 <a href="https://auth0.com/blog/building-a-wikipedia-app-using-react-hooks-and-auth0/">Wikipedia 克隆版</a>）来提高你的 Deno 或 TypeScript 的技能。</p>
<p>你也可以浏览 deno.land 上的 Deno 文档，来更熟悉基础知识。</p>
<p>感谢你阅读本文，祝你编程愉快！</p>
<h3 id="importantlinks">Important Links</h3>
<p>Deno - <a href="https://deno.land">https://deno.land</a><br>
Deno X (package repository) - <a href="https://deno.land/x/">https://deno.land/x/</a><br>
Oak (REST framework) - <a href="https://deno.land/x/oak">https://deno.land/x/oak</a><br>
Oak Basic Usage - <a href="https://deno.land/x/oak@v6.3.1#basic-usage">https://deno.land/x/oak@v6.3.1#basic-usage</a><br>
Final GitHub Repo - <a href="https://github.com/akash-joshi/deno-url-shortener">https://github.com/akash-joshi/deno-url-shortener</a></p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/build-a-url-shortener-in-deno/">How to Build a URL Shortener in Deno</a>，作者：<a href="https://www.freecodecamp.org/news/author/akash/">Akash Joshi</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Rust 语言入门教程：从实战 To-Do App 开始 ]]>
                </title>
                <description>
                    <![CDATA[ Rust 语言从 2015 年发布的首个开源版本开始，便获得了社区大量的关注。从 StackOverflow [https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-languages]  上的开发者调查来看，Rust 也是 2016 年每年都最受开发者喜欢的编程语言。 Rust 由 Mozilla 设计，被定义为一个系统级编程语言（就像 C 和 C++）。Rust 没有垃圾处理器，因此性能极为优良。且其中的一些设计也常让 Rust 看起来很高级。 Rust 的学习曲线被普遍认为是较为艰难的。我并不是 Rust 语言的深入了解者，但在这篇教程中，我将尝试提供一些概念的实用方法，来帮助你更深入的理解。 我们将在这篇实战教程中构建什么？ 我决定通过遵循 JavaScript 应用的悠久传统，来将一个 to-do app 当做我们的第一个 Rust 项目。我们将重点使用命令行，所以有关命令行的知识必须有所了解。同时，你还需要了解一些有关编程概念的基础知识。 这个程 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-build-a-to-do-app-with-rust/</link>
                <guid isPermaLink="false">6018d5fb6183a7054015637c</guid>
                
                    <category>
                        <![CDATA[ Rust ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 项目 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ hylerrix han ]]>
                </dc:creator>
                <pubDate>Mon, 01 Feb 2021 07:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/02/rust-mascot.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Rust 语言从 2015 年发布的首个开源版本开始，便获得了社区大量的关注。从 <a href="https://insights.stackoverflow.com/survey/2020#technology-most-loved-dreaded-and-wanted-languages">StackOverflow</a> 上的开发者调查来看，Rust 也是 2016 年每年都最受开发者喜欢的编程语言。</p>
<p>Rust 由 Mozilla 设计，被定义为一个系统级编程语言（就像 C 和 C++）。Rust 没有垃圾处理器，因此性能极为优良。且其中的一些设计也常让 Rust 看起来很高级。</p>
<p>Rust 的学习曲线被普遍认为是较为艰难的。我并不是 Rust 语言的深入了解者，但在这篇教程中，我将尝试提供一些概念的实用方法，来帮助你更深入的理解。</p>
<h2 id="">我们将在这篇实战教程中构建什么？</h2>
<p>我决定通过遵循 JavaScript 应用的悠久传统，来将一个 to-do app 当做我们的第一个 Rust 项目。我们将重点使用命令行，所以有关命令行的知识必须有所了解。同时，你还需要了解一些有关编程概念的基础知识。</p>
<p>这个程序将基于终端运行。我们将存储一些元素的集合，并在其中分别存储一个表示其活动状态的布尔值。</p>
<h2 id="">我们将会围绕哪些概念来讨论？</h2>
<ul>
<li>Rust 中的错误处理。</li>
<li>Options 和 Null 类型。</li>
<li>Structs 和 impl。</li>
<li>终端 I/O。</li>
<li>文件系统处理。</li>
<li>Rust 中的所有权（Ownership）和借用（borrow）。</li>
<li>匹配模式。</li>
<li>迭代器和闭包。</li>
<li>使用外部的包（crates）。</li>
</ul>
<h2 id="">在我们开始之前</h2>
<p>对于来自 JavaScript 背景的开发者来说，这里有几个我们开始深入前的建议：</p>
<ul>
<li>Rust 是一个强类型的语言。这意味着当编译器无法为我们推断类型时，我们需要时刻关注变量类型。</li>
<li>同样和 JavaScript 不同的是，Rust 中没有 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Automatic_semicolon_insertion">AFI</a>。这意味着我们必须主动在语句后键入分号 (";")——除非它是函数的最后一条语句（此时可以省略分号 <code>;</code> 来将其当做一条 return）。</li>
</ul>
<blockquote>
<p>译者注：AFI，Automatic semicolon insertion，自动分号插入。JavaScript 可以不用写分号，但某些语句也必须使用分号来保证正确地被执行。</p>
</blockquote>
<p>事不宜迟，让我们开始吧！</p>
<h2 id="rust">Rust 如何从零开始</h2>
<p>开始的第一步：下载 Rust 到你的电脑上。想要下载，可以在 Rust 官方文档中的<a href="https://www.rust-lang.org/learn/get-started">入门篇</a>中根据指导来安装。</p>
<blockquote>
<p>译者注：通过 <code>curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh</code> 安装。</p>
</blockquote>
<p>在上面的文档中，你还会找到有关如何将 Rust 与你熟悉的编辑器集成以获得更好开发体验的相关说明。</p>
<p>除了 Rust 编译器本身外，Rust 还附带了一个工具——<a href="https://doc.rust-lang.org/cargo/index.html">Cargo</a>。Cargo 是 Rust 的包管理工具，就像 JavaScript 开发者会用到的 npm 和 yarn 一样。</p>
<p>要开始一个新项目，请先在终端下进入到你想要创造项目的位置，然后只需运行 <code>cargo new &lt;project-name&gt;</code> 即可开始。就我而言，我决定将我的项目命名为“todo-cli”，所以有了如下命令：</p>
<pre><code class="language-bash">$ cargo new todo-cli
</code></pre>
<p>现在切入到新创建的项目目录并打印出其文件列表。你应该会在其中看到这两个文件：</p>
<pre><code class="language-bash">$ tree .
.
├── Cargo.toml
└── src
 └── main.rs
</code></pre>
<p>在本教程的剩余篇章中，我们将会主要关注在 <code>src/main.rs</code> 文件上，所以直接打开这个文件吧。</p>
<p>就像其它众多的编程语言一样，Rust 有一个 main 函数来当作一切的入口。<code>fn</code> 来声明一个函数，同时 <code>println!</code> 中的 <code>!</code> 符号是一个<a href="https://doc.rust-lang.org/book/ch19-06-macros.html">宏</a>（macro）。你很可能会立马看出来，这是 Rust 语言下的一个“<code>Hello World</code>”程序。</p>
<p>想要编译并运行这个程序，可以直接直接 <code>cargo run</code>。</p>
<pre><code class="language-bash">$ cargo run
Hello world!
</code></pre>
<h2 id="">如何读取命令行参数</h2>
<p>我们的目标是让我们的 CLI 工具接收两个参数：第一个参数代表要执行的操作类型，第二个参数代表要操作的对象。</p>
<p>我们将从读取并打印用户输入的参数开始入手。</p>
<p>使用如下内容<strong>替换</strong>掉 main 函数里的内容：</p>
<pre><code class="language-rust">let action = std::env::args().nth(1).expect("Please specify an action");
let item = std::env::args().nth(2).expect("Please specify an item");

println!("{:?}, {:?}", action, item);
</code></pre>
<p>来一起消化下代码里的重要信息：</p>
<ul>
<li><code>let</code>&nbsp;<a href="https://doc.rust-lang.org/std/keyword.let.html">[文档]</a> 给变量绑定一个值</li>
<li><code>std::env::args()</code> <a href="https://doc.rust-lang.org/std/env/fn.args.html">[文档]</a> 是从标准库的 env 模块中引入的函数，该函数返回启动程序时传递给其的参数。由于它是一个迭代器，我们可以使用 <code>nth()</code> 函数来访问存储在每个位置的值。位置 0 引向程序本身，这也是为什么我们从第一个元素而非第零个元素开始读取的原因。</li>
<li><code>expect()</code> <a href="https://doc.rust-lang.org/std/option/enum.Option.html#method.expect">[文档]</a> 是一个 <code>Option</code> 枚举定义的方法，该方法将返回一个需要给定的值，如果给定的值不存在，则程序立即会被停止，并打印出指定的错误信息。</li>
</ul>
<p>由于程序可以不带参数直接运行，因此 Rust 通过给我们提供 Option 类型来要求我们检查是否确实提供了该值。</p>
<p>作为开发者，我们有责任确保在每种条件下都采取适当的措施。</p>
<p>目前我们的程序中，如果未提供参数，程序会被立即退出。</p>
<p>让我们通过如下命令运行程序的同时传递两个参数，记得参数要附加在 <code>--</code> 之后。</p>
<pre><code class="language-bash">$ cargo run -- hello world!
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running target/debug/todo_cli hello 'world'\!''
"hello", "world!"
</code></pre>
<h2 id="">如何使用一个自定义类型插入和保存数据</h2>
<p>让我们考虑一下我们想在这个程序中实现的目标：能够读取用户在命令行输入的参数，更新我们的 todo 清单，然后存储到某个地方来提供记录。</p>
<p>为了达到这个目标，我们将实现自定义类型，来在其中满足我们的业务。</p>
<p>我们将使用 Rust 中的 <a href="https://doc.rust-lang.org/std/keyword.struct.html">struct</a>（结构体），它使开发者能设计有着更优良结构的代码，从而避免了必须在主函数中编写所有的代码。</p>
<h3 id="">如何定义我们的结构体</h3>
<p>由于我们将在项目中会用到很多 HashMap，因此我们可以考虑将其纳入自定义结构体中。</p>
<p>在文件顶部添加如下行：</p>
<pre><code class="language-rust">use std::collections::HashMap
</code></pre>
<p>这将让我们能直接地使用 <code>HashMap</code>，而无需每次使用时都键入完整的包路径。</p>
<p>在 main 函数的下方，让我们添加以下代码：</p>
<pre><code class="language-rust">struct Todo {
  // 使用 Rust 内置的 HashMap 来保存 key - val 键值对。
  map: HashMap&lt;String, bool&gt;,
}
</code></pre>
<p>这将定义出我们需要的 Todo 类型：一个有且仅有 map 字段的结构体。</p>
<p>这个字段是 <a href="https://doc.rust-lang.org/std/collections/struct.HashMap.html">HashMap</a> 类型。你可以将其考虑为一种 JavaScript 对象，在 Rust 中要求我们声明键和值的类型。</p>
<ul>
<li><code>HashMap&lt;String, bool&gt;</code> 表示我们具有一个字符串组成的键，其值是一个布尔值：在应用中来代表当前元素的活动状态。</li>
</ul>
<h3 id="">如何给我们的结构体中新增方法</h3>
<p>方法就像常规的函数一样——都是由 <code>fn</code> 关键字来声明，都接受参数且都可以有返回值。</p>
<p>但是，它们与常规函数不同之处在于它们是在 struct 上下文中定义的，并且它们的第一个参数始终是 <code>self</code>。</p>
<p>我们将定义一个 <em>impl</em>（实现）代码块在上文新增的结构体下方。</p>
<pre><code class="language-rust">impl Todo {
  fn insert(&amp;mut self, key: String) {
    // 在我们的 map 中新增一个新的元素。
    // 我们默认将其状态值设置为 true
    self.map.insert(key, true);
  }
}
</code></pre>
<p>该函数内容十分简单明了：它通过使用 HashMap 内置的 <a href="https://doc.rust-lang.org/std/collections/struct.HashMap.html#method.insert">insert</a> 方法将传入的 key 插入到 map 中。</p>
<p>其中两个很重要的知识是：</p>
<ul>
<li><strong>mut</strong> &nbsp;<a href="https://doc.rust-lang.org/std/keyword.mut.html">[doc]</a> 设置一个可变变量
<ul>
<li>在 Rust 中，每个变量默认是不可变的。如果你想改变一个值，你需要使用 <code>mut</code> 关键字来给相关变量加入可变性。由于我们的函数需要通过修改 map 来添加新的值，因此我们需要将其设置为可变值。</li>
</ul>
</li>
<li><strong>&amp;</strong> &nbsp;<a href="https://doc.rust-lang.org/std/primitive.reference.html">[doc]</a> 标识一个引用。
<ul>
<li>你可以认为这个变量是一个指针，指向内存中保存这个值的具体地方，并不是直接存储这个值。</li>
<li>在 Rust 属于中，这被认为是一个<strong>借用</strong>（borrow），意味着函数并不拥有该变量，而是指向其存储位置。</li>
</ul>
</li>
</ul>
<h2 id="rust">Rust 所有权系统的简要概览</h2>
<p>有了前面关于借用（borrow）和引用（reference）的知识铺垫，现在是个很好的时机来简要地讨论 Rust 里的所有权（ownership）。</p>
<p>所有权是 Rust 中最独特的功能，它使 Rust 程序员无需手动分配内存（例如在 C/C++ 中）就可以编写程序，同时仍可以在无需垃圾收集器（如 JavaScript 或 Python）的情况下运行，Rust 会不断查看程序的内存以释放未使用的资源。</p>
<p>所有权系统有如下三个规则：</p>
<ul>
<li>Rust 中每个值都有一个变量：即为其所有者。</li>
<li>每个值一次只能有一个所有者。</li>
<li>当所有者超出范围时，该值将被删除。</li>
</ul>
<p>Rust 会在编译时检查这些规则，这意味着是否以及何时要在内存中释放值需要被开发者明确指出。</p>
<p>思考一下如下示例：</p>
<pre><code class="language-rust">fn main() {
  // String 的所有者是 x
  let x = String::from("Hello");

  // 我们将值移动到此函数中
  // 现在 doSomething 是 x 的所有者
  // 一旦超出 doSomething 的范围
  // Rust 将释放与 x 关联的内存。
  doSomething(x);

  // 由于我们尝试使用值 x，因此编译器将引发错误
  // 因为我们已将其移至 doSomething 内
  // 我们此时无法使用它，因为此时已经没有所有权
  // 并且该值可能已经被删除了
  println!("{}", x);
}
</code></pre>
<p>在学习 Rust 时，这个概念被广泛地认为是最难掌握的，因为它对许多程序员来说都是新概念。</p>
<p>你可以从 Rust 的官方文档中阅读有关<a href="https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html">所有权</a>的更深入的说明。</p>
<p>我们不会深入研究所有权制度的来龙去脉。现在，请记住我上面提到的规则。尝试在每个步骤中考虑是否需要“拥有”这些值后删除它们，或者是否需要继续引用它以便可以保留它。</p>
<p>例如，在上面的 insert 方法中，我们不想拥有 <code>map</code>，因为我们仍然需要它来将其数据存储在某个地方。只有这样，我们才能最终释放被分配的内存。</p>
<h3 id="map">如何将 map 保存到硬盘上</h3>
<p>由于这是一个演示程序，因此我们将采用最简单的长期存储解决方案：将 map 写入文件到磁盘。</p>
<p>让我们在 <code>impl</code> 块中创建一个新的方法。</p>
<pre><code class="language-rust">impl Todo {
  // [其余代码]
  fn save(self) -&gt; Result&lt;(), std::io::Error&gt; {
    let mut content = String::new();
    for (k, v) in self.map {
      let record = format!("{}\t{}\n", k, v);
      content.push_str(&amp;record)
    }
    std::fs::write("db.txt", content)
  }
}
</code></pre>
<ul>
<li><code>-&gt;</code> 表示函数返回的类型。我们在这里返回的是一个 <code>Result</code> 类型。</li>
<li>我们遍历 map，并分别格式化出一个字符串，其中同时包括 key 和 value，并用 tab 制表符分隔，同最后用新的一个换行符来结尾。</li>
<li>我们将格式化的字符串放入到 content 变量中。</li>
<li>我们将 <code>content</code> 容写入名为 <code>db.txt</code> 的文件中。</li>
</ul>
<p>值得注意的是，<code>save</code> 拥有自 self 的_所有权_。此时，如果我们在调用 save 之后意外尝试更新 map，编译器将会阻止我们（因为 self 的内存将被释放）。</p>
<p>这是一个完美的例子，展示了如何使用 Rust 的内存管理来创建更为严格的代码，这些代码将无法编译（以防止开发过程中的人为错误）。</p>
<h3 id="main">如何在 main 中使用结构体</h3>
<p>现在我们有了这两种方法，就可以开始使用了。现在我们将继续在之前编写的 main 函数内编写功能：如果提供的操作是 add，我们将该元素插入并存储到文件中以供未来使用。</p>
<p>将如下代码添加到之前编写的两个参数绑定的下方：</p>
<pre><code class="language-rust">fn main() {
  // ...[参数绑定代码]

  let mut todo = Todo {
    map: HashMap::new(),
  };
  if action == "add" {
    todo.insert(item);
    match todo.save() {
      Ok(_) =&gt; println!("todo saved"),
      Err(why) =&gt; println!("An error occurred: {}", why),
    }
  }
}
</code></pre>
<p>让我们看看我们都做了什么：</p>
<ul>
<li><code>let mut todo = Todo</code> 让我们实例化一个结构体，绑定它到一个可变变量上。</li>
<li>我们通过 <code>.</code> 符号来调用&nbsp;<code>TODO insert</code> 方法。</li>
<li>我们将<a href="https://doc.rust-lang.org/std/keyword.match.html">匹配</a> save 功能返回的结果，并在不同情况下载屏幕上显示一条消息。</li>
</ul>
<p>让我们测试运行吧。打开终端并输入：</p>
<pre><code class="language-rust">$ cargo run -- add "code rust"
todo saved
</code></pre>
<p>让我们来检查元素是否真的保存了：</p>
<pre><code class="language-console">$ cat db.txt
code rust true
</code></pre>
<p>你可以在这个 <a href="https://gist.github.com/Marmiz/b67e98c2fc7be3561d124294cf3cb6ac">gist</a> 中找到完整的代码片段。</p>
<h2 id="">如何读取文件</h2>
<p>现在我们的程序有个根本性的缺陷：每次“add”添加时，我们都会重写整个 map 而不是对其进行更新。这是因为我们在程序运行的每一次都创造一个全新的空 map 对象，现在一起来修复它。</p>
<h3 id="todonew">在 TODO 中新增 new 方法</h3>
<p>我们将为 Todo 结构实现一个新的功能。调用后，它将读取文件的内容，并将已存储的值返回给我们的 Todo。请注意，这不是一个方法，因为它没有将 self 作为第一个参数。</p>
<p>我们将其称为 <code>new</code>，这只是一个 Rust 约定（请参阅之前使用的 <code>HashMap::new()</code>）。</p>
<p>让我们在 impl 块中添加以下代码：</p>
<pre><code class="language-rust">impl Todo {
  fn new() -&gt; Result&lt;Todo, std::io::Error&gt; {
    let mut f = std::fs::OpenOptions::new()
      .write(true)
      .create(true)
      .read(true)
      .open("db.txt")?;
    let mut content = String::new();
    f.read_to_string(&amp;mut content)?;
    let map: HashMap&lt;String, bool&gt; = content
      .lines()
      .map(|line| line.splitn(2, '\t').collect::&lt;Vec&lt;&amp;str&gt;&gt;())
      .map(|v| (v[0], v[1]))
      .map(|(k, v)| (String::from(k), bool::from_str(v).unwrap()))
      .collect();
    Ok(Todo { map })
  }

// ...剩余的方法
}
</code></pre>
<p>如果看到上面的代码感到头疼的话，请不用担心。我们这里使用了一种更具函数式的编程风格，主要是用来展示 Rust 支持许多其他语言的范例，例如迭代器，闭包和 lambda 函数。</p>
<p>让我们看看上面代码都具体发生了什么：</p>
<ul>
<li>我们定义了一个 <code>new</code> 函数，其会返回一个 Result 类型，要么是 <code>Todo</code> 结构体要么是 <code>io:Error</code>。</li>
<li>我们通过定义各种 <a href="https://doc.rust-lang.org/std/fs/struct.OpenOptions.html">OpenOptions</a> 来配置如何打开“db.txt”。最显著的是 <code>create(true)</code> 标志，这代表如果该文件不存在则创建这个文件。</li>
<li><code>f.read_to_string(&amp;mut content)?</code> 读取文件中的所有字节，并将它们附加到 <code>content</code> 字符串中。
<ul>
<li><em>注意</em>：记得添加使用 <code>std:io::Read</code> 在文件的顶部以及其他 use 语句来使用 <code>read_to_string</code> 方法。</li>
</ul>
</li>
<li>我们需要将文件中的 String 类型转换为 HashMap。为此我们将 map 变量与此行绑定：<code>let map: HashMap&lt;String, bool&gt;</code>。
<ul>
<li>这是编译器在为我们推断类型时遇到麻烦的情况之一，因此我们需要自行声明。</li>
</ul>
</li>
<li>lines <a href="https://doc.rust-lang.org/std/primitive.str.html#method.lines">[文档]</a> 在字符串的每一行上创建一个 Iterator 迭代器，来在文件的每个条目中进行迭代。因为我们已在每个条目的末尾使用了 <code>/n</code> 格式化。</li>
<li>map <a href="https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.map">[文档]</a> 接受一个闭包，并在迭代器的每个元素上调用它。</li>
<li><code>line.splitn(2, '\t')</code>&nbsp;<a href="https://doc.rust-lang.org/std/primitive.str.html#method.splitn">[文档]</a> 将我们的每一行通过 tab 制表符切割。</li>
<li><code>collect::&lt;Vec&lt;&amp;str&gt;&gt;()</code> <a href="https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.collect">[文档]</a> 是标准库中最强大的方法之一：它将迭代器转换为相关的集合。
<ul>
<li>在这里，我们告诉 map 函数通过将 <code>::Vec&lt;&amp;str&gt;</code> 附加到方法中来将我们的 Split 字符串转换为借来的字符串切片的 Venctor，这回告诉编译器在操作结束时需要哪个集合。</li>
</ul>
</li>
<li>然后为了方便起见，我们使用 <code>.map(|v| (v[0], v[1]))</code> 将其转换为元祖类型。</li>
<li>然后使用 <code>.map(|(k, v)| (String::from(k), bool::from_str(v).unwrap()))</code> 将元祖的两个元素转换为 String 和 boolean。
<ul>
<li>注意：记得添加 <code>use std::str::FromStr;</code> 在文件顶部以及其它 use 语句，以便能够使用 from_str 方法。</li>
</ul>
</li>
<li>我们最终将它们收集到我们的 HashMap 中。这次我们不需要声明类型，因为 Rust 从绑定声明中推断出了它。</li>
<li>最后，如果我们从未遇到任何错误，则使用 <code>Ok(Todo { map })</code> 将结果返回给调用方。
<ul>
<li>注意，就像在 JavaScript 中一样，如果键和变量在结构内具有相同的名称，则可以使用较短的表示法。</li>
</ul>
</li>
</ul>
<p><em>phew!</em></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/01/dancing-ferris.gif" alt="dancing-ferris" width="600" height="400" loading="lazy"></p>
<p>你做的很棒！图片来源于 <a href="https://rustacean.net/">https://rustacean.net/</a>。</p>
<h3 id="">另一种等价方式</h3>
<p>尽管通常认为 map 更为好用，但以上内容也可以通过基本的 <code>for</code> 循环来使用。你可以选择自己喜欢的方式。</p>
<pre><code class="language-rust">fn new() -&gt; Result&lt;Todo, std::io::Error&gt; {
  // 打开 db 文件
  let mut f = std::fs::OpenOptions::new()
    .write(true)
    .create(true)
    .read(true)
    .open("db.txt")?;
  // 读取其内容到一个新的字符串中
  let mut content = String::new();
  f.read_to_string(&amp;mut content)?;
  
  // 分配一个新的空的 HashMap
  let mut map = HashMap::new();
  
  // 遍历文件中的每一行
  for entries in content.lines() {
    // 分割和绑定值
    let mut values = entries.split('\t');
    let key = values.next().expect("No Key");
    let val = values.next().expect("No Value");
    // 将其插入到 HashMap 中
    map.insert(String::from(key), bool::from_str(val).unwrap());
  }
  // 返回 Ok
  Ok(Todo { map })
}
</code></pre>
<p>上述代码和之前的函数式代码是功能性等价的关系。</p>
<h3 id="">如何使用这个新方法</h3>
<p>在 main 中，只需要用以下代码块来初始化 todo 变量：</p>
<pre><code class="language-rust">let mut todo = Todo::new().expect("Initialisation of db failed");
</code></pre>
<p>现在如果我们回到终端并执行若干个如下“add”命令，我们应该可以看到我们的数据库被正确的更新了。</p>
<pre><code class="language-console">$ cargo run -- add "make coffee"
todo saved
$ cargo run -- add "make pancakes"
todo saved
$ cat db.txt
make coffee     true
make pancakes   true
</code></pre>
<p>你可以在这个 <a href="https://gist.github.com/Marmiz/b659c7835054d25513106e3804c4539f">gist</a> 中找到目前阶段下所有的完整代码。</p>
<h2 id="">如何在集合中更新一个值</h2>
<p>正如所有的 todo app 一样，我们希望不仅能够添加项目，而且能够对齐进行状态切换并将其标记为已完成。</p>
<h3 id="complete">如何新增 complete 方法</h3>
<p>我们需要在 Todo 结构体中新增一个 complete 方法。在其中，我们获取到 key 的引用值，并更新其值。在 key 不存在的情况下，返回 <code>None</code>。</p>
<pre><code class="language-rust">impl Todo {
  // [其余的 TODO 方法]

  fn complete(&amp;mut self, key: &amp;String) -&gt; Option&lt;()&gt; {
    match self.map.get_mut(key) {
      Some(v) =&gt; Some(*v = false),
      None =&gt; None,
    }
  }
}
</code></pre>
<p>让我们看看上面代码发生了什么：</p>
<ul>
<li>我们声明了方法的返回类型：一个空的 <code>Option</code>。</li>
<li>整个方法返回 <code>Match</code> 表达式的结果，该结果将为空 <code>Some() </code>或 <code>None</code>。</li>
<li>我们使用 <code>*</code>&nbsp;<a href="https://doc.rust-lang.org/book/appendix-02-operators.html">[文档]</a> 运算符来取消引用该值，并将其设置为 false。</li>
</ul>
<h3 id="complete">如何使用 complete 方法</h3>
<p>我们可以像之前使用 insert 一样使用 “complete” 方法。</p>
<p>在 <code>main</code> 函数中，我们使用 <code>else if</code> 语句来检查命令行传递的动作是否是“complete”。</p>
<pre><code class="language-rust">// 在 main 函数中

if action == "add" {
  // add 操作的代码
} else if action == "complete" {
  match todo.complete(&amp;item) {
    None =&gt; println!("'{}' is not present in the list", item),
    Some(_) =&gt; match todo.save() {
      Ok(_) =&gt; println!("todo saved"),
      Err(why) =&gt; println!("An error occurred: {}", why),
    },
  }
}
</code></pre>
<p>是时候来分析我们在上述代码中做的事了：</p>
<ul>
<li>如果我们检测到返回了 Some 值，则调用 todo.save 将更改永久存储到我们的文件中。</li>
<li>我们匹配由 <code>todo.complete(&amp;item)</code> 方法返回的 Option。</li>
<li>如果返回结果为 <code>None</code>，我们将向用户打印警告，来提供良好的交互性体验。
<ul>
<li>我们通过 <code>&amp;item</code> 将 item 作为引用传递给“todo.complete”方法，以便 main 函数仍然拥有该值。这意味着我们可以再接下来的 <code>println!</code> 宏中继续使用到这个变量。</li>
<li>如果我们不这样做，那么该值将由“complete”用于，最终被意外丢弃。</li>
</ul>
</li>
<li>如果我们检测到返回了 <code>Some</code> 值，则调用 <code>todo.save</code> 将此次更改永久存储到我们的文件中。</li>
</ul>
<p>和之前一样，你可以在这个 <a href="https://gist.github.com/Marmiz/1480b31e8e0890e8745e7b6b44a803b8">gist</a> 中找到目前阶段下的所有相关代码。</p>
<h2 id="">运行这个程序吧</h2>
<p>现在是时候在终端来完整运行我们开发的这个程序了。让我们通过先删除掉之前的 db.txt 来从零开始这个程序：</p>
<pre><code class="language-bash">$ rm db.txt
</code></pre>
<p>然后在 todos中进行新增和修改操作：</p>
<pre><code class="language-bash">$ cargo run -- add "make coffee"
$ cargo run -- add "code rust"
$ cargo run -- complete "make coffee"
$ cat db.txt
make coffee     false
code rust       true
</code></pre>
<p>这意味着在这些命令执行完成后，我们将会得到一个完成的元素（“make coffee”），和一个尚未完成的元素（“code rust”）。</p>
<p>假设我们此时再重新新增一个喝咖啡的元素“make coffee”：</p>
<pre><code class="language-bash">$ cargo run -- add "make coffee"
$ cat db.txt
make coffee     true
code rust       true
</code></pre>
<h2 id="serdejson">番外：如何使用 Serde 将其存储为 JSON</h2>
<p>该程序即使很小，但也能正常运行了。此外，我们可以稍微改变一些逻辑。对于来自 JavaScript 世界的我，决定将值存储为 JSON 文件而不是纯文本文件。</p>
<p>我们将借此机会了解如何安装和使用来自 Rust 开源社区的名为 <a href="https://crates.io/">creates.io</a> 的软件包。</p>
<h3 id="serde">如何安装 serde</h3>
<p>要将新的软件包安装到我们的项目中，请打开 <code>cargo.toml</code> 文件。在底部，你应该会看到一个 <code>[dependencies]</code> 字段：只需要将以下内容添加到文件中：</p>
<pre><code class="language-rust">[dependencies]
serde_json = "1.0.60"
</code></pre>
<p>这就够了。下次我们运行程序的时候，cargo 将会编译我们的程序并下载和导入这个新的包到我们的项目之中。</p>
<h3 id="todonew">如何改动 Todo::New</h3>
<p>我们要使用 Serde 的第一个地方是在读取 db 文件时。现在，我们要读取一个 JSON 文件而非“.txt”文件。</p>
<p>在 <code>impl</code> 代码块中，我们更像一下 <code>new</code> 方法：</p>
<pre><code class="language-rust">// 在 Todo impl 代码块中

fn new() -&gt; Result&lt;Todo, std::io::Error&gt; {
  // 打开 db.json
  let f = std::fs::OpenOptions::new()
    .write(true)
    .create(true)
    .read(true)
    .open("db.json")?;
  // 序列化 json 为 HashMap
  match serde_json::from_reader(f) {
    Ok(map) =&gt; Ok(Todo { map }),
    Err(e) if e.is_eof() =&gt; Ok(Todo {
      map: HashMap::new(),
    }),
    Err(e) =&gt; panic!("An error occurred: {}", e),
  }
}
</code></pre>
<p>值得注意的改动是：</p>
<ul>
<li>文件选项不再需要 <code>mut f</code> 来绑定，因为我们不需要像以前一样手动将内容分配到 String 中。Serde 会来处理相关逻辑。</li>
<li>我们将文件拓展名更新为了 <code>db.json</code>。</li>
<li><code>serde_json::from_reader</code> <a href="https://docs.serde.rs/serde_json/fn.from_reader.html">[文档]</a> 将为我们反序列化文件。它会干扰 map 的返回类型，并会尝试将 JSON 转换为兼容的 HashMap。如果一切顺利，我们将像以前一样返回 Todo 结构。</li>
<li><code>Err(e) if e.is_eof()</code> 是一个<a href="https://doc.rust-lang.org/reference/expressions/match-expr.html#match-guards">匹配守卫</a>，可让我们优化 Match 语句的行为。
<ul>
<li>如果 Serde 作为错误返回一个过早的 EOF（文件结尾），则意味着该文件完全为空（例如，在第一次运行时，或如果我们删除了该文件）。在那种情况下，我们从错误中恢复并返回一个空的 HashMap。</li>
</ul>
</li>
<li>对于其它所有错误，程序会立即被中断退出。</li>
</ul>
<h3 id="todosave">如何改动 Todo.save</h3>
<p>我们要使用 Serde 的另一个地方是将 map 另存为 JSON。为此，将 impl 块中的 <code>save</code> 方法更新为：</p>
<pre><code class="language-rust">// 在 Todo impl 代码块中
fn save(self) -&gt; Result&lt;(), Box&lt;dyn std::error::Error&gt;&gt; {
  // 打开 db.json
  let f = std::fs::OpenOptions::new()
    .write(true)
    .create(true)
    .open("db.json")?;
  // 通过 Serde 写入文件
  serde_json::to_writer_pretty(f, &amp;self.map)?;
  Ok(())
}
</code></pre>
<p>和以前一样，让我们看看这里所做的更改：</p>
<ul>
<li><code>Box&lt;dyn std::error::Error&gt;</code>。这次我们返回一个包含 Rust 通用错误实现的 <a href="https://doc.rust-lang.org/std/boxed/index.html">Box</a>。
<ul>
<li>简而言之，Box 是指向内存中分配的指针。</li>
<li>由于打开文件时可能会返回 Serde 错误，所以我们实际上并不知道函数会返回这两个错误里的哪一个。</li>
<li>因此我们需要返回一个指向可能错误的指针，而不是错误本身，以便调用者处理它们。</li>
</ul>
</li>
<li>我们当然已经将文件名更新为 <code>db.json</code> 以匹配文件名。</li>
<li>最后，我们让 Serde 承担繁重的工作：将 HashMap 编写为 JSON 文件。</li>
<li>请记得从文件顶部删除 <code>use std::io::Read;</code> 和&nbsp;<code>use std::str::FromStr;</code>，因为我们不再需要它们了。</li>
</ul>
<p>这就搞定了。</p>
<p>现在你可以运行你的程序并检查输出是否保存到文件中。如果一切都很顺利，你会看到你的 todos 都保持为 JSON 了。</p>
<p>你可以在这个 <a href="https://gist.github.com/Marmiz/541c3ccea832a27bfb60d4882450a4a8">gist</a> 中阅读当前阶段下完整的代码。</p>
<h2 id="">结语、技巧和更多资源</h2>
<p>这是一段漫长的旅程，很荣幸你能阅读到这里。</p>
<p>我希望你能在这个教程中学到一些东西，并产生了更多的好奇心。别忘了我们在这里介绍的是一门非常“底层”的语言。</p>
<p>这是 Rust 吸引我的重要原因——Rust 使我能够编既快速又具有内存效率的代码，而不必畏惧承担过多的编码责任：我知道编译器会帮我优化更多，在运行前可能会出现错误的情况下提前中断运行。</p>
<p>在结束前，我想向你分享一些其他技巧和资源，以帮助你在 Rust 的旅途中继续前行：</p>
<ul>
<li><a href="https://github.com/rust-lang/rustfmt">Rust fmt </a>是一个非常方便的工具，你可以按照一致的模式运行以格式化代码。不必再浪费时间配置你喜欢的 linter 插件。</li>
<li><code>cargo check</code>&nbsp;<a href="https://doc.rust-lang.org/cargo/commands/cargo-check.html">[文档]</a> 将尝试在不运行的情况下编译代码：这在你只想在不实际运行时检查代码正确性的情况下，会变得很有用。</li>
<li>Rust 带有集成的测试套件和生成文档的工具：<a href="https://doc.rust-lang.org/cargo/commands/cargo-test.html">cargo test</a> 和 <a href="https://doc.rust-lang.org/cargo/commands/cargo-rustdoc.html">cargo doc</a>。这次我们没有涉及它们，因为本教程内容量已经足够多了，或许未来会有所涉及。</li>
</ul>
<p>想要了解有关 Rust 的更多内容，我认为这些资源真的很棒：</p>
<ul>
<li>官方 <a href="https://www.rust-lang.org/">Rust 网站</a>，所有重要信息的聚集地。</li>
<li>如果你喜欢通过聊天来互动交流，Rust 的 Discord <a href="https://discord.gg/rust-lang">服务器</a>是个很活跃和有用的社区。</li>
<li>如果你想要通过读书来学习，“Rust 程序设计语言”一书是个很好的选择。</li>
<li>如果你更喜欢视频类型的资料，Ryan Levick 的“<a href="https://youtu.be/WnWGO-tLtLA">Rust 介绍</a>”视频系列是个很棒的资源。</li>
</ul>
<p>你可以在 <a href="https://github.com/Marmiz/todo-cli">GitHub</a> 中找到本文的相关源码。</p>
<p>文中的插图来自于&nbsp;<a href="https://rustacean.net/">https://rustacean.net/</a>。</p>
<p>感谢阅读，祝你编码愉快！</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/how-to-build-a-to-do-app-with-rust/">Rust Programming Language Tutorial – How to Build a To-Do List App</a>，作者：<a href="https://www.freecodecamp.org/news/author/claudio/">Claudio Restifo</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 为什么我认为 Deno 是一个迈向错误方向的 JavaScript 运行时？ ]]>
                </title>
                <description>
                    <![CDATA[ 译者序 在为《Deno 钻研之术 [https://github.com/hylerrix/deno-tutorial] 》引入第一篇翻译文章的时候，就看到了这篇文章，那时还觉得驾驭不了，就重点先写了若干篇入门级别的 Deno 文章。 转眼到 2021 年，从《Deno 双周刊 [https://github.com/hylerrix/deno-feedly]》第一期继续开启新的一年的 Deno 之旅，于是就回想起了本文，觉得可以通过好好阅读本文及相关后，从另一个角度了解 Deno，翻译就开始了。 相比前面对文章的直接翻译外，本文有很多想要记笔记的地方（甚至“大开眼界”的地方），因此独特地开辟本文的“译者序”篇章，来梳理一下原文及其相关视频到底针对 Deno 的另一面，都讲了什么。 也正如原文所说，这注定是一篇有争议的文章，从文章中引入的两个视频中的点踩数都微微大于点赞数、其评论区都充满着“火热”的讨论就能看出多个观点的冲撞。原文也存在些过度批评的地方，但是不妨碍下方译文开始后一比一地还原原作者想要表达的观点！ 所以，原作者 Mehul 在原文以及两个视频中到底想要说 Deno ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/why-deno-is-a-wrong-step-in-the-future/</link>
                <guid isPermaLink="false">6001045a5f61e30501b5bd70</guid>
                
                    <category>
                        <![CDATA[ Deno ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ hylerrix han ]]>
                </dc:creator>
                <pubDate>Fri, 15 Jan 2021 03:27:07 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/image-13.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="">译者序</h2>
<p>在为《<a href="https://github.com/hylerrix/deno-tutorial">Deno 钻研之术</a>》引入第一篇翻译文章的时候，就看到了这篇文章，那时还觉得驾驭不了，就重点先写了若干篇入门级别的 Deno 文章。</p>
<p>转眼到 2021 年，从《<a href="https://github.com/hylerrix/deno-feedly">Deno 双周刊</a>》第一期继续开启新的一年的 Deno 之旅，于是就回想起了本文，觉得可以通过好好阅读本文及相关后，从另一个角度了解 Deno，翻译就开始了。</p>
<p>相比前面对文章的直接翻译外，本文有很多想要记笔记的地方（甚至“大开眼界”的地方），因此独特地开辟本文的“译者序”篇章，来梳理一下原文及其相关视频到底针对 Deno 的另一面，都讲了什么。</p>
<p>也正如原文所说，这注定是一篇有争议的文章，从文章中引入的两个视频中的点踩数都微微大于点赞数、其评论区都充满着“火热”的讨论就能看出多个观点的冲撞。原文也存在些过度批评的地方，但是不妨碍下方译文开始后一比一地还原原作者想要表达的观点！</p>
<p>所以，原作者 Mehul 在原文以及两个视频中到底想要说 Deno 为什么没那么好？其观点大致梳理如下：</p>
<blockquote>
<p>注意：如果想先看原文的话可以跳过阅读并在之后来看这份简单的总结。</p>
</blockquote>
<ul>
<li>Deno 和 Node 确实有<strong>竞争关系</strong>，因为你必须在你的下个项目中作出选择。</li>
<li>Deno 现在所做的<strong>成果并不是很多</strong>，大多特性都可以在 Node 生态中较好地解决掉。</li>
<li><strong>URL import</strong> 还是一场灾难。NPM 中已经有很多明星项目“竟然只有一行代码”、“暗中偷窃用户数据”、“注入挖矿代码”、“兼容性出现问题导致很多上游库受影响”等问题，URL import 本身并不能解决这些问题，更没有一个像 Node 一样强壮的社区来保证受人信任的依赖库，也就不会有更多的开发者愿意加入到 Deno 生态中。</li>
<li>由于 <strong>TypeScript</strong> 是 JavaScript 的超集，完全可以选择跳过类型验证，此时推荐新手在 Deno 上直接使用 TypeScript 编程坑会很大，很可能会出现一堆 any 类型。在经常性的调试报错下，TypeScript 的学习成本也较高，很容易写出低质量代码。</li>
<li><strong>TypeScript</strong> 并不是直接在 Deno 上跑的，其实还是变成了 JavaScript 来跑，何必一定要集成到 Deno 中呢？</li>
<li><strong>安全</strong>是一个很难的事情，Deno 宣传自己的“安全沙箱”注定要承担很大的责任。Deno 安全沙箱也没有必要，完全可以用 Docker 等容器或虚拟化技术来支持。同时，真正想搞破坏的脚本也会找到自己的方式来规避安全问题。</li>
<li>以当时版本下的 <code>deno --allow-run</code> 运行主进程从而开启的子进程能轻松突破安全沙箱的验证来获得更多权限为例，发现 Deno 的“<strong>安全沙箱</strong>”并没有所说的那么安全。</li>
<li>Deno 没有必要<strong>集成太多工具链</strong>（代码格式化、测试工具、TypeScript 等等）于一体，让各种第三方工具链来一起共建生态的同时，保证 Deno 本身的专注性并提供更友好的插件支持会很好。</li>
<li>Node 的异步模型<strong>并没有被淘汰</strong>，promise 和事件侦听器模型也没有套题，因此不确定 Deno 将如何解决这些没有被淘汰的问题。</li>
<li>未来并不确定会有多少开发者愿意将 npm 中的成熟库逐渐<strong>迁移到 Deno 中</strong>。</li>
</ul>
<p>可以看出，无论你是 Node 开发者还是 Deno 爱好者，这些观点都有很多值得思考的地方。但也有有失偏颇的地方，比如文中将 Deno 说明为编程语言，也将 Deno 只发展了两年多的生态直接和建设了十年的 Node 生态作横向对比——Deno 注定会有自己独特的发展轨迹。</p>
<p>最后，原文作者 Mehul 总结了他眼中的 Deno v1.0 的真实面貌：</p>
<blockquote>
<p>Deno = (大多数情况下)[TypeScript + Node + 正确配置的 Docker 容器 + 单一可执行程序 + &lt;各种各样的小工具&gt; - NPM]</p>
</blockquote>
<h2 id="">译文开始</h2>
<p>目前，我还没有在 Youtube 上找到任何一个像 <a href="https://www.youtube.com/codedamn">codedamn</a> 一样的频道（超过 10 万名开发者订阅），其对 Deno v1 版本的发布并不感到兴奋。</p>
<p>上周，我在我的频道上发布了一个视频，介绍了一些“为什么我认为我们不需要 Deno——另一个基于 V8 和 Node 的 JavaScript 运行时”的原因，这些原因对我来说都很简明扼要。</p>
<p>同时通过本文，我可以在视频外拓展阐述更多的原因。但如果你想先看视频，可以看看这个：</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 width="560" height="315" src="https://www.youtube.com/embed/Uf1md0k6ATs" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" name="fitvid0"></iframe>
          </div>
        </div>
      </figure>
<p>为了证明我总体上并不是反对 Deno 甚至 JavaScript 本身，我想声明一下：我喜欢 JavaScript 胜过于很多其它的编程技术。我的主要技术栈也只围绕着 JavaScript 展开——Node/React/MongoDB/React Native/NativeScript/Ionic/甚至你能想到的更多相关库。</p>
<p>我也是主要用 JavaScript 这一门语言，就让我的 <a href="https://www.youtube.com/codedamn">Youtube 频道</a>及一个<a href="https://codedamn.com/">开发者平台</a>的订阅人数达到了 10 万多人。</p>
<p>但重要的事情在于，保持公平客观的角度来看到一个事务的两面性很重要。Deno 当然有好的一面，但也有大多数人尚未看到/写文探讨的另一面。一起来看看吧！<br>
_<br>
<strong><em>注意：</em></strong><em>本文注定会是一个有争议性的文章。请先让我们保持礼貌的态度并控制好自己的情绪。如果你能仔细阅读全文直到结尾，然后再说说你的更多想法，那我会备受感激。</em><br>
_<br>
<em>本文底部我会列出我的社交账号，也希望我们能在哪里针对此主题进行更多的良好探讨。</em></p>
<h2 id="denovsnode">Deno vs Node：名副其实的竞争关系</h2>
<p>有很多业内人士都在说：“Deno 和 Node 之间没有任何竞争关系，彼此之间可以学到很多东西”。</p>
<p>在某种程度上，我同意这个看法，Node 和 Deno 确实可以互相学习。但是两者间真的没有竞争关系了吗？我完全不同意这关观点。</p>
<p>让我们重新看一下 Deno 和 Node 的共同特征：</p>
<ol>
<li>它们都是 JavaScript 的运行时环境；</li>
<li>它们都可以在可以运行 V8 的任何计算机上运行；</li>
<li>它们都有 ECMAScript 标准支持；</li>
<li>它们都在被积极的维护中。</li>
</ol>
<p>如果你这两年都是 Deno 的粉丝，这两年里不选择 Node 而直接用 Deno 作为新项目的技术选型是不可能的。</p>
<p>同理，如果你以前从未使用过 TypeScript，并且认为自己想要尝试 Deno，此时你会很难同时用上 NPM 生态中的各种模块。</p>
<p>所以：Deno 和 Node 的开发人员目前确实存在分歧——你必须做选择，我想说这是两者为竞争关系的一个很重要的例子。</p>
<h2 id="deno">Deno 到底好在哪里？</h2>
<p>首先，我需要列举一下 Deno 对自己的宣传中的那些优势，Deno 为什么说自己是更好的运行时：</p>
<ol>
<li>它克服了 Node 的一些缺点；</li>
<li>它在默认情况下是一个安全的运行时环境；</li>
<li>它内置 TypeScript 支持；</li>
<li>它将 Promise 的支持下放到底层；</li>
<li>它基于 Rust 语言构建（对比与 C++ 之于 Node）。</li>
</ol>
<p>在接下来的章节中，我将一个一个针对上面的每一个属于 Deno 的优点，来看看 Node 可以从中学到什么。我也将会在必要之时探讨，为什么 Deno 还没有那么有意义。</p>
<h2 id="deno">Deno 画蛇添足在了哪些地方？</h2>
<p>让我们开始拿起 Deno 的独特宣传点（USP，Unique Selling Proposition） 并将它们一一解析：</p>
<h3 id="">默认安全的运行时环境</h3>
<p>这是 Deno 里很受欢迎的特性，我很惊喜。Deno 直接默认支持一个安全的沙箱环境，除非你明确的选择开启访问文件系统或访问网络等功能权限。</p>
<p>Deno 这样做是因为它想更加地贴近浏览器。Deno 遵守 ECMAScript 标准这点很不错，但为什么如此热衷于率先贴近浏览器？</p>
<p>或许答案是，Deno 想要保持在客户端和服务端上编写的代码之间有良好的兼容性。但 Deno 如此强烈的想要支持浏览器以至于我觉得方向有些偏失、甚至有些过头了。</p>
<p>Node 虽不支持“安全的运行时”——但经过深思熟虑后，我觉得也有理由支持 Node：</p>
<ol>
<li>众所周知，你不应该在系统上运行不受信任的代码和可执行文件。这就是我们总是选择像 Express 库之于 Node 的原因（而不是随便找个声称自己速度比 Express 快 100 倍的库）。信任来自于社区的大量使用。</li>
<li>我不知道有任何编程语言像 Deno 这样提供如此的沙箱环境。尽管这个功能可能不错，但似乎应该交由诸如 Docker 这类的容器环境来完成。我相信一个被良好配置的 Docker 环境，相比在沙箱化的 Deno 环境中运行不受信任的 Node 代码来说，能更好的处理不受信任文件的安全性问题。</li>
<li>沙箱化并没那么容易——我虽然不是网络安全专家，但我觉得某些功能越多，攻击面就可能越大。Deno 承诺提供安全的运行时环境，但我想说安全很难实现。Deno 的承诺带来了巨大的安全责任。世界上最大的企业们为支持它们的安全白帽计划，需要每年为独立开发者和安全公司投入将近数亿美金。因此，Deno 到底会将它们的“安全环境”带向何方？时间会证明一切。</li>
</ol>
<p>所以，Node 可以从 Deno 中学到什么？我想说不会学到太多。Node 或许可以从竞争对手可以引入一些安全环境的标识，但是没有太多意义。如果你想在你的系统上随意运行一些未知代码，则最好克隆一个 C/C++ 仓库并运行 make 命令损害系统。</p>
<blockquote>
<p>译者注：上段最后一句话是“If you randomly run arbitrary code on your systems, you might as well clone a C/C++ repo and run a make command over it and get your whole system compromised.”，有些难以翻译，也不容易看出为什么从 Node/Deno 突然跑到了 C/C++，欢迎交流。</p>
</blockquote>
<p>据我所知，你不能在像 C/C++ 这样的底层语言上来“沙盒化”文件系统或网络访问——这样效率并不高。</p>
<p>注意：最近我发现启用 <code>--allow-run</code> 标志的 Deno 几乎可以完成任何操作。该视频详细介绍了相关内容：</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 width="560" height="315" src="https://www.youtube.com/embed/_lvas914dXI" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="" name="fitvid1"></iframe>
          </div>
        </div>
      </figure>
<h3 id="typescript">内置支持 TypeScript</h3>
<p>为 TypeScript 现阶段的进展欢呼，我很高兴 Deno 开箱即用地支持 TypeScript。</p>
<p><em><strong>注:</strong> &nbsp;感谢 <a href="https://chinese.freecodecamp.org/news/lilasquared">@lilasquared </a>指出 Deno 也能开箱即用地运行&nbsp;<code>.js</code> 文件。本文重点强调使用&nbsp;<code>.ts</code> 文件编写代码。Deno 当然可以直接运行 .js 文件。</em></p>
<p>但是，让我们退一步来说：你知道为什么 JavaScript 和 Node 在全球拥有数以万计的开发人员吗？因为进入这个领域的壁垒几乎为零。JavaScript 是灵活的，可以容许你的诸多错误。而 TypeScript 总会给你一些奇怪的错误。</p>
<p>对于生产级的应用程序来说就糟糕了：生产环境上可不需要这些时髦的东西。同时对于学习者来说，JavaScript 是宽容的，纵使你可能会遇到一些 Bug，但也可以很轻松的改正，引用一句话，JavaScript 可以被快速编码并将事情搞定。</p>
<p>对于初学者来说，我担心他们如果选择使用 Deno（并被要求使用 TypeScript），因为他们还不了解 TypeScript，想着快速在服务端上跑通代码，我们可能会看到很多这种的代码：</p>
<pre><code class="language-typescript">const httpResponse: any = await getAPIResponse&lt;any&gt;({ myParams })
// ...
const someOtherVariable = something() as any
// ...
any, any, any
</code></pre>
<p>TypeScript 是 JavaScript 的超集。你完全可以无意识间写一段质量很差的 TypeScript 代码，仅仅使用 TypeScript 并不会让你的项目无懈可击。</p>
<p>直到你想起来本来就能在 Node 中写 TypeScript 之前，这种体验确实很有趣。我相信现在每个在生产环境中使用 Node 的大型公司都引入了 TypeScript 来编写项目——没有例外。当你处理诸多文件及其依赖关系、处理大量代码时，JavaScript 真的很难拓展。</p>
<p>TypeScript 是 JavaScript 生态系统中革命性的工具包，更好地同时支持了静态和动态语言。</p>
<p>因此 Deno 声明自己会内置 TypeScript 支持。但我想问为什么一定要这样？确实，如果不内置的话，可能需要额外引入 Babel 和 Webpack 来完成这项工作，但这不是一堆第三方工具链围绕身边来共建生态的重要意义吗？我们难道不想增强 DX 吗？</p>
<blockquote>
<p>译者注：DX，开发人员体验，是 Developer Experience 的简称。当软件或系统的用户是开发人员时，开发人员体验（DX）就相当于用户体验（UX, User experience design）。</p>
</blockquote>
<p>JavaScript 依然还会是 JavaScript，保持自身的风格。况且，如果 Deno 真的能直接运行 TypeScript（或类似于 TypeScript 的语言），我觉得没什么问题。但事实上，Deno 其实也只是将 TypeScript 代码转换为 JavaScript 代码，并运行它罢了。</p>
<p>从这些角度能看出，Deno 似乎是一个 Node 下各种工具的的集成版本，Deno 将一个测试工具、一个代码格式化程序和 TypeScript 等一次性的包括进来。我更喜欢一个精简的编程语言并在合适的时候由我自己添加各种插件——当然，我不能代表所有开发人员，这也只是我的观点。</p>
<p>为什么我会在已经有专注于代码格式化的 prettier 库的情况下，依然需要领一个内置的代码格式化工具？为什么要解决这种本身就做的不错的东西？</p>
<p>单体架构确实集中起来提供了很多工具，但它也真的很庞大，一个更精简和专注的核心库才能更好的维护和拓展。</p>
<h3 id="promise">Promise 的支持下放到底层</h3>
<p>和 Node 作为对比，Deno v1 的发布对我来说看不出太多的意义。我非常尊重 Node 和 Deno 的创建者，但是 Node 拥有一些 Deno 所没有的东西——世界各地众多经验丰富的开发人员的支持。</p>
<p>Node 由近 3000 位开发者贡献力量，并且是异步 I/O 事件处理的引领者。Deno 确实建立在 Rust 之上，并公开了类似 Promise 的的抽象。但是 Node 有 C++，有 3000 名开发人员以及 10 年以上的开发和维护。</p>
<p>Node 的异步模型并没有被淘汰，promise 和事件侦听器模型也没有被淘汰，因此我不确定 Deno 将如何解决这些并没被淘汰的问题。</p>
<h3 id="npm">再见了，npm</h3>
<p>很重要的事情是：Deno 并不支持 NPM。Ryan（Node 和 Deno 的创建者）在为此推广 Go 语言的相关特性。让我想到一些包管理器：</p>
<ol>
<li>npm for JS (obviously)</li>
<li>npm 之于 JS（真很明细）</li>
<li>apt-get</li>
<li>composer 之于 PHP</li>
<li>brew 之于 macOS</li>
<li>cargo 之于 Rust（Deno 正是基于 Rust 构建)</li>
</ol>
<p>我认为不使用包管理器来管理是很不好的一步。包管理器能做的太多了：标明版本、编写脚本、管理依赖关系等等。为什么 Deno 不使用 npm 呢？我并不清楚，但这些是我想到的：</p>
<ol>
<li>首先，因为 Deno 需要 TypeScript 生态，但是后者生态更多的是 JavaScript 的。更正：Deno 也能良好的运行 <code>.js</code> 文件。</li>
<li>其次，大量 npm 模块需要要求使用到文件/网络甚至更多的条件，而这些 Deno 都很严格的默认不提供这些权限了。所以你需要在 package.json 里注入大量颇许冗余的“permissions”字段来提供更多权限。然而...Deno 无法和 npm 互相配合，因此也没有 package.json。接下来我们会来看看 Deno 到底如何处理模块系统的。</li>
<li>NPM 模块数量及其庞大甚至臃肿，但这也是 Node 生态的强大生命力所在。想要找一个库来将 tar 文件内容提取到 stream 流中？你可以选择 tar-steram。想要一个数据格式验证库？你可以选择 joi。想要配合 JWT 协同使用？你可以选择 jsonwebtoken。我怀疑得有多久才能让开发者们将他们的各种库变得兼容 Deno 系统？</li>
</ol>
<p>Deno 对模块系统采用了一种完全不同的方法。但无论如何，Deno 在尝试以某种方式“修补”现有的 NPM 模块。那么除了尝试在 Docker 容器中“入侵”（hacking around）一个 TS + Node 项目外，我看不到太多使用 Deno 的意义。</p>
<p>根据我目前所了解的有关 Deno 的一切，这是 Deno 现在的真实面貌：</p>
<blockquote>
<p><strong>Deno</strong> = (大多数情况下)[TypeScript + Node + 正确配置的 Docker 容器 + 单一可执行程序 + &lt;各种各样的小工具&gt; - NPM]</p>
</blockquote>
<p>搞定！让我们冷静一下，然后听我一下的总结。</p>
<h2 id="">总结</h2>
<p>我和其它人对 Deno 的出现一样感到兴奋。但当 Deno 准备完全重写 JavaScript 运行时时，我的期望便有所变动。</p>
<p>Deno 的自动化 TypeScript 文档等诸多不错的特性我没有提到，是因为我希望这篇文章旨在展示 Deno 的另一面。因为 Deno 的优点几乎可以再任何其他 Deno 的文章中找到，所以我需要再次强调硬币的两面性。</p>
<p>坦白来说，Deno 看起来为了一些很小的益处承担了巨大的责任和代价，包括转移现有的 NPM 模块和代码库的诸多债务。你同意还是不同意我的这些观点呢？我很期待你的想法。推特联系我 <a href="https://twitter.com/mehulmpt">@mehulmpt</a> 或 <a href="https://instagram.com/mehulmpt">Instagram</a> 也可以！</p>
<p>祝好！</p>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/why-deno-is-a-wrong-step-in-the-future/">Why I Believe Deno is a Step in the Wrong Direction for JavaScript Runtime Environments</a>，作者：<a href="https://www.freecodecamp.org/news/author/mehulmpt/">Mehul Mohan</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Deno + Oak 连接 MySQL 实战教程 ]]>
                </title>
                <description>
                    <![CDATA[ 我最近写了一篇关于 Deno + Oak 构建酷炫 Todo API [https://www.freecodecamp.org/news/create-a-todo-api-in-deno-written-by-a-guy-coming-from-node/]   的文章 ，其中并没有使用数据库相关的知识点。您可以在我的 Github 仓库adeelibr/deno-playground [https://github.com/adeelibr/deno-playground/tree/master/chapter_1:oak] 的 chapter_1:oak 中查看当时的整套代码。 > 译者注：翻译版的《Deno + Oak 构建酷炫的 Todo API》 [https://deno-tutorial.js.org/translations/004-deno-oak-todo-api.html]在这里，相关 Demo 也可以在电子书仓库中找到。 本文将进一步讲起，一起来学习如何将 MySQL 集成到我们的 Deno + Oak 项目中。 如果你想随时了解本文的完整代码，可以 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-use-mysql-in-deno-oak/</link>
                <guid isPermaLink="false">5f042d11db4be8080eb711af</guid>
                
                    <category>
                        <![CDATA[ Deno ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ hylerrix han ]]>
                </dc:creator>
                <pubDate>Tue, 07 Jul 2020 08:26:10 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/07/photo-1591509867461-d9f58becc082.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>我最近写了一篇关于 <a href="https://www.freecodecamp.org/news/create-a-todo-api-in-deno-written-by-a-guy-coming-from-node/">Deno + Oak 构建酷炫 Todo API</a> 的文章 ，其中并没有使用数据库相关的知识点。您可以在我的 Github 仓库&nbsp;<a href="https://github.com/adeelibr/deno-playground/tree/master/chapter_1:oak">adeelibr/deno-playground</a>&nbsp;的 chapter_1:oak 中查看当时的整套代码。</p>
<blockquote>
<p>译者注：翻译版的<a href="https://deno-tutorial.js.org/translations/004-deno-oak-todo-api.html">《Deno + Oak 构建酷炫的 Todo API》</a>在这里，相关 Demo 也可以在电子书仓库中找到。</p>
</blockquote>
<p>本文将进一步讲起，一起来学习如何将 MySQL 集成到我们的 Deno + Oak 项目中。</p>
<p>如果你想随时了解本文的完整代码，可以在&nbsp;<a href="https://github.com/adeelibr/deno-playground/tree/master/chapter_2:mysql">chapter_2:mysql</a> 中找到，欢迎给仓库点个 Star。</p>
<p>我将假设你已经阅读了上一篇文章，如果没有，可以先在<a href="https://www.freecodecamp.org/news/create-a-todo-api-in-deno-written-by-a-guy-coming-from-node/">此处</a>阅读完后再回到本文来。</p>
<p>在我们开始前，请确保你已经安装了一个 MySQL 客户端并你能成功运行：</p>
<ul>
<li>MySQL community server [<a href="https://dev.mysql.com/downloads/mysql/">在这里下载</a>]</li>
<li>MySQL Workbench [<a href="https://dev.mysql.com/downloads/workbench/">在这里下载</a>]</li>
</ul>
<p>同时我也为 MacOS 用户写了一个简短的，关于<a href="https://github.com/adeelibr/deno-playground/blob/master/guidelines/setting-up-mysql-mac-os-catalina.md">如何安装 MySQL 的教程</a>。</p>
<p>如果你是在 Windows 环境下，你可以使用和上面相同的工具，或者直接使用 <a href="https://www.apachefriends.org/index.html">XAMPP</a>&nbsp;来快速运行 MySQL 实例到你的机器上。</p>
<p>当你将 MySQL 成功跑起来后，我们就可以开始本文的探索了。</p>
<h2 id="">让我们开始吧</h2>
<p>假设你已经阅读了<a href="https://www.freecodecamp.org/news/create-a-todo-api-in-deno-written-by-a-guy-coming-from-node/">上一篇文章</a>，我们将编写如下功能：</p>
<ul>
<li>创建一个 MySQL 数据库的连接；</li>
<li>编写一个小脚本，每当我们重启 Deno 服务器时数据库会自动重置；</li>
<li>在一个数据表上执行 CRUD 操作；</li>
<li>将 CURD 操作连接到我们的 API 控制器中。</li>
</ul>
<p>开始前的最后一件事：我将上一篇的代码添加 MySQL 版本后的具体 Git 变动可以<a href="https://github.com/adeelibr/deno-playground/pull/1/commits/5b63b51ebcadededcfec452fe6877a0bd0f1f83f">在这里</a>查阅。</p>
<p>在你的项目根目录中（我的叫做 <em><code>chapter_2:mysql</code></em>，你的可以随便起），创建一个 <strong>db</strong> 文件夹，并在其中创建一个 <strong>config.ts</strong> 并添加如下内容：</p>
<pre><code class="language-typescript">export const DATABASE: string = "deno";
export const TABLE = {
  TODO: "todo",
};
</code></pre>
<p>这里没什么新知识点，仅仅导出了我们定义的数据库的名称以及一个 TABLE 对象。通过这个导出，我们的项目中将会有一个名为 “deno” 的数据库，其中又会有一个名为 “todo” 的数据表。</p>
<p>接下来，在 <strong>db</strong> 文件夹中再创建一个名为 <strong>client.ts</strong> 的文件并填充如下内容：</p>
<pre><code class="language-typescript">import { Client } from "https://deno.land/x/mysql/mod.ts";
// config
import { DATABASE, TABLE } from "./config.ts";

const client = await new Client();
client.connect({
  hostname: "127.0.0.1",
  username: "root",
  password: "",
  db: "",
});
</code></pre>
<p>这段代码包含了若干条功能。</p>
<p>我们从 Deno 的一个第三方&nbsp;<code>mysql</code>&nbsp;模块中解构出了 <code>Client</code>&nbsp;变量，这个变量可以用来连接数据库并执行指定的增删改查工作。</p>
<pre><code class="language-typescript">client.connect({
  hostname: "127.0.0.1",
  username: "root",
  password: "",
  db: "",
});
</code></pre>
<p><code>Client</code>&nbsp;内置一个 <code>connect</code> 方法，用来供我们设置&nbsp;<code>hostname</code>、<code>username</code>、<code>password</code>&nbsp;和&nbsp;<code>db</code>&nbsp;等字段的值，以设置与 MySQL 的连接配置。</p>
<p>请确保你的 <code>username</code>&nbsp;用户没有设置&nbsp;<code>password</code>，因为目前的 Deno MySQL 模块无法连接有密码的用户。如果你不知道如何清空用户密码，可以阅读<a href="https://github.com/adeelibr/deno-playground/blob/master/guidelines/setting-up-mysql-mac-os-catalina.md#set-your-mysql-password-to-empty">这里</a>。</p>
<p>我在此处将  <code>database</code>&nbsp;字段留空，因为我想稍后在脚本中手动选择它。</p>
<p>让我们添加一个用来初始化名为“deno”的数据库并为其创建一个名为“todo”表的脚本。</p>
<p>在&nbsp;<code>db/client.ts</code>&nbsp;文件中我们添加以下内容：</p>
<pre><code class="language-typescript">import { Client } from "https://deno.land/x/mysql/mod.ts";
// 加载配置文件里的配置
import { DATABASE, TABLE } from "./config.ts";
const client = await new Client();
client.connect({
  hostname: "127.0.0.1",
  username: "root",
  password: "",
  db: "",
});
const run = async () =&gt; {
  // 创建一个数据库 (前提是之前没有创建过)
  await client.execute(CREATE DATABASE IF NOT EXISTS ${DATABASE});
  // 选择我们的数据库
  await client.execute(USE ${DATABASE});
  // 如果已经创建过名为 Todo 的数据表，将其删除
  await client.execute(DROP TABLE IF EXISTS ${TABLE.TODO});
  // 创建 Todo 数据表
  await client.execute(CREATE TABLE ${TABLE.TODO} (
        id int(11) NOT NULL AUTO_INCREMENT,
        todo varchar(100) NOT NULL,
        isCompleted boolean NOT NULL default false,
        PRIMARY KEY (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;);
};

run();

export default client;
</code></pre>
<p>这里我们从我们的最早的配置文件中导入了 <code>DATABASE</code>&nbsp;和&nbsp;<code>TABLE</code>，并通过  <code>run()</code> 方法创建相关的数据库和表。</p>
<p>让我们截取&nbsp;<code>run()</code> 方法相关的代码片段。我在代码中已经编写了帮助你理解的注释。</p>
<pre><code class="language-typescript">const run = async () =&gt; {
  // 创建一个数据库 (前提是之前没有创建过)
  await client.execute(CREATE DATABASE IF NOT EXISTS ${DATABASE});
  // 选择我们的数据库
  await client.execute(USE ${DATABASE});
  // 如果已经创建过名为 Todo 的数据表，将其删除
  await client.execute(DROP TABLE IF EXISTS ${TABLE.TODO});
  // 创建 Todo 数据表
  await client.execute(CREATE TABLE ${TABLE.TODO} (
        id int(11) NOT NULL AUTO_INCREMENT,
        todo varchar(100) NOT NULL,
        isCompleted boolean NOT NULL default false,
        PRIMARY KEY (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;);
};

run();
</code></pre>
<ul>
<li>创建一个名为 <code>deno</code>&nbsp;的数据库，如果之前已经有这个数据库则跳过此步骤。</li>
<li>选择我们当前名为 <code>deno</code>&nbsp;的这个数据库。</li>
<li>在 <code>deno</code>&nbsp;数据库中，如果名为&nbsp;<code>todo</code>&nbsp;的表存在，则将其删除。</li>
<li>接下来，在 <code>deno</code> 数据库中创建一个新的&nbsp;<code>todo</code>&nbsp;表，并且定义其表结构：表结构将包含一个唯一的、自增长的、数值型的 <code>id</code> 字段；也将包含一个名为&nbsp;<code>todo</code>&nbsp;的字符串型字段；同时还包含一个名为  <code>isCompleted</code>&nbsp;的布尔型字段；最后将 <code>id</code> 字段定义为主键。</li>
</ul>
<p>我写这段代码的原因是因为我不想在 MySQL 实例中有代码上无法直观看出来的信息。有了这段代码后，每次重启服务器时，它都会重新初始化所有内容。</p>
<p>你可以不编写这段代码。但如果不编写的话，则必须手动创建数据库和表。</p>
<p>同时，你也可以查阅 Deno MySQL 模块的 <a href="https://deno.land/x/mysql/#create-database">db creation</a>&nbsp;和&nbsp;<a href="https://deno.land/x/mysql/#create-table">table creation</a>&nbsp;文档。</p>
<p>回到文章的主旨，我们已经完成了上面提到的四个目标的两个目标：</p>
<ul>
<li>创建一个 MySQL 数据库的连接；</li>
<li>编写一个小脚本，每当我们重启 Deno 服务器时数据库会自动重置。</li>
</ul>
<p>这意味着本文 50% 的知识点已经介绍完毕。但不幸运的是，现在还测试不了任何数据操作功能。一起来快速添加几个 CRUD 功能来看看具体是怎样执行的。</p>
<h2 id="crudapi">在数据表上执行 CRUD 操作并将功能添加到 API 控制器中</h2>
<p>我们需要先编写 Todo 接口。创建&nbsp;<code>interfaces/Todo.ts</code> 文件并添加如下内容：</p>
<pre><code class="language-typescript">export default interface Todo {
  id?: number,
  todo?: string,
  isCompleted?: boolean,
}
</code></pre>
<p>代码中的&nbsp;<code>?</code>&nbsp;符号代表这个键是可选的。之所以这样做是因为接下来我们有的地方仅需要其中的若干个键。</p>
<p>如果你想了解更多 TypeScript 中可选的属性，可以查阅<a href="https://www.typescriptlang.org/docs/handbook/interfaces.html#optional-properties">这里</a>。</p>
<p>接下来，在根目录创建一个名为 <strong>models</strong> 的文件夹并在其中创建一个名为 <strong>todo.ts</strong> 的文件，添加如下内容：</p>
<pre><code class="language-typescript">import client from "../db/client.ts";
// 加载配置文件
import { TABLE } from "../db/config.ts";
// 加载接口文件
import Todo from "../interfaces/Todo.ts";

export default {
  /**
   * 通过解构的 id 参数值，来检查相应的 todo 元素是否存在于数据表中
   * @param id
   * @returns 返回布尔值来代表是否存在
   */
  doesExistById: async ({ id }: Todo) =&gt; {},
  /**
   * 将会返回 todo 表中的所有内容
   * @returns 返回全部都是 todo 元素的数组
   */
  getAll: async () =&gt; {},
  /**
   * 过解构的 id 参数值，来返回相应的 todo 元素
   * @param id
   * @returns 返回一个 todo 元素
   */
  getById: async ({ id }: Todo) =&gt; {},
  /**
   * 在 todo 表中增加一个新的 todo 元素
   * @param todo
   * @param isCompleted
   */
  add: async (
    { todo, isCompleted }: Todo,
  ) =&gt; {},
  /**
   * 修改某个 todo 元素的内容
   * @param id
   * @param todo
   * @param isCompleted
   * @returns 返回一个数字 (代表影响的行数)
   */
  updateById: async ({ id, todo, isCompleted }: Todo) =&gt; {},
  /**
   * 通过 ID 来删除指定的元素
   * @param id
   * @returns integer (count of effect rows)
   */
  deleteById: async ({ id }: Todo) =&gt; {},
};
</code></pre>
<p>此时每个函数都是空的，不用担心，我们接下来会一一填充。</p>
<p>接下来创建 <code>controllers/todo.ts</code> 文件并保证填充如下内容：</p>
<pre><code class="language-typescript">// 加载接口文件
import Todo from "../interfaces/Todo.ts";
// 加载模型操作文件
import TodoModel from "../models/todo.ts";

export default {
  /**
   * @description 获取所有 todo 元素
   * @route GET /todos
   */
  getAllTodos: async ({ response }: { response: any }) =&gt; {},
  /**
   * @description 新增一个 todo 元素
   * @route POST /todos
   */
  createTodo: async (
    { request, response }: { request: any; response: any },
  ) =&gt; {},
  /**
   * @description 通过 id 获取指定的 todo 元素
   * @route GET todos/:id
   */
  getTodoById: async (
    { params, response }: { params: { id: string }; response: any },
  ) =&gt; {},
  /**
   * @description 通过 id 更新指定的 todo 元素
   * @route PUT todos/:id
   */
  updateTodoById: async (
    { params, request, response }: {
      params: { id: string };
      request: any;
      response: any;
    },
  ) =&gt; {},
  /**
   * @description 通过 id 删除指定的 todo 元素
   * @route DELETE todos/:id
   */
  deleteTodoById: async (
    { params, response }: { params: { id: string }; response: any },
  ) =&gt; {},
};
</code></pre>
<p>这个文件目前同样是空的内容，现在开始一一将其填充吧。</p>
<h3 id="gettodosapi">[Get] 获取全部 Todos 的 API</h3>
<p>在&nbsp;<code>models/todo.ts</code>&nbsp;文件中为&nbsp;<code>getAll</code>&nbsp;方法添加具体逻辑：</p>
<pre><code class="language-typescript">import client from "../db/client.ts";
// config
import { TABLE } from "../db/config.ts";
// Interface
import Todo from "../interfaces/Todo.ts";

export default {
   /**
   * 将会返回所有 todo 表中的数据
   * @returns array of todos
   */
  getAll: async () =&gt; {
    return await client.query(`SELECT * FROM ${TABLE.TODO}`);
  },
}
</code></pre>
<p>我们这里直接用 SQL 原生语法来获取表中的所有内容。</p>
<p>除了 <code>connect</code>（使用于 db/client.ts 文件中）方法外， <code>Client</code> 还公开了另一种方法，即 <code>query</code>。通过 <code>client.query</code> 方法，我们可以直接从 Deno 代码上运行 MySQL 查询。</p>
<p>接下来打开&nbsp;<code>controllers/todo.ts</code> 文件并为&nbsp;<code>getAllTodos</code> 填充内容：</p>
<pre><code class="language-typescript">// interfaces
import Todo from "../interfaces/Todo.ts";
// models
import TodoModel from "../models/todo.ts";

export default {
  /**
   * @description 获取所有 todo
   * @route GET /todos
   */
  getAllTodos: async ({ response }: { response: any }) =&gt; {
    try {
      const data = await TodoModel.getAll();
      response.status = 200;
      response.body = {
        success: true,
        data,
      };
    } catch (error) {
      response.status = 400;
      response.body = {
        success: false,
        message: `Error: ${error}`,
      };
    }
  },
}
</code></pre>
<p>我们这里所做的就是导入 <code>TodoModel</code> 对象并使用其中我们刚定义不久的 <code>getAll</code> 方法。因为需要该函数需要处理 Promise 类型的异步过程，所以我们将整个函数定义为 async/await 类型。</p>
<p><code>TodoModel.getAll()</code> 方法返回一个数组后，我们将这个数组包装起来并将 <code>response.body</code> 的响应状态 <code>status</code> 设置为 <code>200</code>。</p>
<p>如果执行过程中有任何异常比如 Promise 报错，程序将通过进入 catch 块，向用户返回状态码为 400 的响应体（此时 <code>success</code> 为 false，<code>message</code> 为错误原因。</p>
<p>就这么简单地搞定了，现在来在终端上运行。</p>
<p>请保证你的 MySQL 实例运行中，然后在终端输入：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts 
</code></pre>
<p>不出意外的话，此时你的终端会有这样类似的结果：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screenshot-2020-06-04-at-23.29.19.png" alt="Screenshot-2020-06-04-at-23.29.19" width="600" height="400" loading="lazy"></p>
<p>这也是当我通过命令行运行服务器时终端的样子。</p>
<p>终端告诉了我们两件事：</p>
<ol>
<li>Deno API 服务器成功运行在了 8080 端口上；</li>
<li>Deno API 服务器程序成功连接到了 MySQL 客户端 <code>127.0.0.1:3306</code> （<code>http://localhost:3306</code>）上。</li>
</ol>
<p>让我们测试下我们的 API，我使用的是 <a href="https://www.postman.com/">Postman</a>，但是你可以用任何你喜欢的 API 测试工具。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screenshot-2020-06-04-at-23.31.07.png" alt="Screenshot-2020-06-04-at-23.31.07" width="600" height="400" loading="lazy"></p>
<p>执行 [GET] localhost:8080/todos 后，我们得到了所有 todos 列表。</p>
<p>虽然现在的 todos 列表返回的是空数组，但当我们能成功给 <code>todo</code> 数据表增加数据后就会获得更多数据。</p>
<p>棒极了，一个 API 搞定只剩下四个要搞。</p>
<h3 id="posttodoapi">[Post] 新增一个 Todo 的 API</h3>
<p>在 <code>models/todo.ts</code> 文件中，为 <code>add()</code> 函数添加如下内容：</p>
<pre><code class="language-typescript">export default {
   /**
   * 为 todo 数据表新增一行数据
   * @param todo
   * @param isCompleted
   */
  add: async (
    { todo, isCompleted }: Todo,
  ) =&gt; {
    return await client.query(
      `INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)`,
      [
        todo,
        isCompleted,
      ],
    );
  },
}
</code></pre>
<p>add 函数在参数列表中将解构 <code>todo</code> 和&nbsp;<code>isCompleted</code> 两个变量。</p>
<p>同时，上面代码的 <code>add: async ({ todo, isCompleted }: Todo) =&gt; {}</code> 片段和 <code>({todo, isCompleted}: {todo:string, isCompleted:boolean})</code> 语句是等价的。但我们已经在  <code>interfaces/Todo.ts</code> 中定义过 Todo 接口：</p>
<pre><code class="language-typescript">export default interface Todo {
  id?: number,
  todo?: string,
  isCompleted?: boolean,
}
</code></pre>
<p>此时我们将可以简单地写成  <code>add: async ({ todo, isCompleted }: Todo) =&gt; {}</code>。这条语句告诉 TypeScript 当前函数有两个参数：字符串类型的 <code>todo</code>，以及布尔类型的 <code>isCompleted</code>。</p>
<p>如果你想了解更多关于接口的知识，TypeScript 官方文档上有一个绝佳的介绍，可以查看<a href="https://www.typescriptlang.org/docs/handbook/interfaces.html">这里</a>。</p>
<p>在 add 函数中还有如下代码：</p>
<pre><code class="language-typescript">return await client.query(
  `INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)`,
  [
    todo,
    isCompleted,
  ],
);
</code></pre>
<p>这是一条 MySQL 查询语句，可以被拆分为如下两个部分：</p>
<ul>
<li><code>INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)</code>。其中的两个问号意味着这里需要使用到变量的值。</li>
<li>另一部分 <code>[todo, isCompleted]</code> 是上一部分需要使用的变量，其值将会替代 <code>(?, ?)</code>。</li>
<li><code>Table.Todo</code> 是一个从  <code>db/config.ts</code> 读取来的字符串，其值为"<code>todo</code>"。</li>
</ul>
<p>接下来在我们的控制器 <code>controllers/todo.ts</code> 文件中，编写 <code>createTodo()</code> 函数：</p>
<pre><code class="language-typescript">export default {
   /**
   * @description 新增一个 todo
   * @route POST /todos
   */
  createTodo: async (
    { request, response }: { request: any; response: any },
  ) =&gt; {
    const body = await request.body();
    if (!request.hasBody) {
      response.status = 400;
      response.body = {
        success: false,
        message: "No data provided",
      };
      return;
    }

    try {
      await TodoModel.add(
        { todo: body.value.todo, isCompleted: false },
      );
      response.body = {
        success: true,
        message: "The record was added successfully",
      };
    } catch (error) {
      response.status = 400;
      response.body = {
        success: false,
        message: `Error: ${error}`,
      };
    }
  },
}
</code></pre>
<p>我们继续将其拆分为两个部分来介绍：</p>
<p><strong>第一部分</strong></p>
<pre><code class="language-typescript">const body = await request.body();
if (!request.hasBody) {
  response.status = 400;
  response.body = {
    success: false,
    message: "No data provided",
  };
  return;
}
</code></pre>
<p>我们在这里所做的是检查用户请求当前接口时是否在 body 中传递了请求数据。如果没有的话将返回一个有 <code>400</code> 状态码且包括  <code>success: false</code> 和&nbsp;<code>message: &lt;erromessage-string&gt;</code> 的响应体。</p>
<p><strong>第二部分</strong></p>
<pre><code class="language-typescript">try {
  await TodoModel.add(
    { todo: body.value.todo, isCompleted: false },
  );
  response.body = {
    success: true,
    message: "The record was added successfully",
  };
} catch (error) {
  response.status = 400;
  response.body = {
    success: false,
    message: `Error: ${error}`,
  };
}
</code></pre>
<p>接下来如果没有任何意外错误，则调用 <code>TodoModel.add()</code> 函数并返回一个状态为 <code>200</code> 且给用户提示执行函数成功的响应体。</p>
<p>否则将进入 catch 代码段，来返回函数执行错误及其原因，正如前文介绍过的 API 一样。</p>
<p>现在我们搞定了。打开你的终端并且确保你的 MySQL 正在运行。在终端输入：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts
</code></pre>
<p>打开 <a href="https://www.postman.com/">Postman</a> 并且测试当前 API 能否正常运行：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screenshot-2020-06-04-at-23.55.02.png" alt="Screenshot-2020-06-04-at-23.55.02" width="600" height="400" loading="lazy"></p>
<p>执行 [POST] localhost:8080/todos =&gt; 将会为 todo 列表新增一个新的数据。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screenshot-2020-06-04-at-23.57.06.png" alt="Screenshot-2020-06-04-at-23.57.06" width="600" height="400" loading="lazy"></p>
<p>再次执行 [GET] localhost:8080/todos =&gt; 将会返回所有 todos，可以看到刚刚新增 todo 已经被加入到数据库中。</p>
<p>很棒，已经搞定了两个 API，只剩三个要做。</p>
<h3 id="getidtodoapi">[GET] 通过 ID 查询某 Todo 的 API</h3>
<p>在你的 <code>models/todo.ts</code> 文件中，为 <code>doesExistById()</code> 和&nbsp;<code>getById()</code> 两个函数填充其内容：</p>
<pre><code class="language-typescript">export default {
   /**
   * Takes in the id params &amp; checks if the todo item exists
   * in the database
   * @param id
   * @returns boolean to tell if an entry of todo exits in table
   */
  doesExistById: async ({ id }: Todo) =&gt; {
    const [result] = await client.query(
      `SELECT COUNT(*) count FROM ${TABLE.TODO} WHERE id = ? LIMIT 1`,
      [id],
    );
    return result.count &gt; 0;
  },
  /**
   * 解构 id 变量 &amp; 返回找到的相关 todo
   * against it.
   * @param id
   * @returns object of todo item
   */
  getById: async ({ id }: Todo) =&gt; {
    return await client.query(
      `SELECT * FROM ${TABLE.TODO} WHERE id = ?`,
      [id],
    );
  },
}
</code></pre>
<p>让我们逐个介绍这两个函数：</p>
<ul>
<li><code>doesExistById</code> 函数从参数列表中解构出&nbsp;<code>id</code> 变量，并返回一个&nbsp;<code>boolean</code> 布尔值，来表明想要检测的这个独特 todo 是否存在于数据库中。</li>
</ul>
<pre><code class="language-typescript">const [result] = await client.query(
  `SELECT COUNT(*) count FROM ${TABLE.TODO} WHERE id = ? LIMIT 1`,
  [id],
);
return result.count &gt; 0;
</code></pre>
<p>我们通过 count 值来检查指定 todo 是否存在。如果其值大于 0 返回 <code>true</code>，否则返回 <code>false</code>。</p>
<ul>
<li><code>getById</code> 函数通过指定的 id 返回相应的数据：</li>
</ul>
<pre><code class="language-typescript">return await client.query(
  `SELECT * FROM ${TABLE.TODO} WHERE id = ?`,
  [id],
);
</code></pre>
<p>上面一行代码直接执行了 MySQL 语句，来通过 id 查询数据并返回结果。</p>
<p>接下来，打开 <code>controllers/todo.ts</code> 文件并为 <code>getTodoById</code> 控制器填充内容：</p>
<pre><code class="language-typescript">export default {
   /**
   * @description 通过 id 获取相关 tod
   * @route GET todos/:id
   */
  getTodoById: async (
    { params, response }: { params: { id: string }; response: any },
  ) =&gt; {
    try {
      const isAvailable = await TodoModel.doesExistById(
        { id: Number(params.id) },
      );

      if (!isAvailable) {
        response.status = 404;
        response.body = {
          success: false,
          message: "No todo found",
        };
        return;
      }

      const todo = await TodoModel.getById({ id: Number(params.id) });
      response.status = 200;
      response.body = {
        success: true,
        data: todo,
      };
    } catch (error) {
      response.status = 400;
      response.body = {
        success: false,
        message: `Error: ${error}`,
      };
    }
  },
}
</code></pre>
<p>这段代码也可以拆分为两段更小的部分来看：</p>
<pre><code class="language-typescript">const isAvailable = await TodoModel.doesExistById(
  { id: Number(params.id) },
);

if (!isAvailable) {
  response.status = 404;
  response.body = {
    success: false,
    message: "No todo found",
  };
  return;
}
</code></pre>
<p>首先我们通过以下代码来检查想要查找的 todo 是否存在于数据库中：</p>
<pre><code class="language-typescript">const isAvailable = await TodoModel.doesExistById(
  { id: Number(params.id) },
);
</code></pre>
<p>我们在其中需要转换 <code>params.id</code> 为 <code>Number</code> 数值类，因为接口中声明了我们的 <code>id</code> 键值必须是一个数字。接下来我们将转换为数值后的 <code>params.id</code> 传递给 <code>doesExistById</code> 方法，这个方法会返回布尔值。</p>
<p>接着检查这个布尔值，如果是 false 则返回像前文一样的包含 <code>404</code> 状态码的响应体：</p>
<pre><code class="language-typescript">if (!isAvailable) {
  response.status = 404;
  response.body = {
    success: false,
    message: "No todo found",
  };
  return;
}
</code></pre>
<p>第二部分是：</p>
<pre><code class="language-typescript">try {
  const todo: Todo = await TodoModel.getById({ id: Number(params.id) });
  response.status = 200;
  response.body = {
    success: true,
    data: todo,
  };
} catch (error) {
  response.status = 400;
  response.body = {
    success: false,
    message: `Error: ${error}`,
  };
}
</code></pre>
<p>这段代码和前文的很像。我们从数据库中获取到指定数据给 <code>todo</code> 变量，然后返回响应体。如果执行过程中有任何错误则响应体将包含错误信息返回给用户。</p>
<p>现在打开你的终端并且确保你的 MySQL 正在运行。在终端输入：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts
</code></pre>
<p>打开 <a href="https://www.postman.com/">Postman</a> 来测试当前接口能否正常运行。</p>
<p>请记住我们每次重启服务器时都会重置数据库。如果你不想要这个功能，你可以注释掉 <code>db/client.ts</code> 文件中的整个 <code>run</code> 方法。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screenshot-2020-06-07-at-17.09.32.png" alt="Screenshot-2020-06-07-at-17.09.32" width="600" height="400" loading="lazy"></p>
<p>执行 [POST] localhost:8080/todos =&gt; 将会新增一个新的 todo。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screenshot-2020-06-07-at-17.10.13.png" alt="Screenshot-2020-06-07-at-17.10.13" width="600" height="400" loading="lazy"></p>
<p>执行 [POST] localhost:8080/todos =&gt; 将会返回所有的 todo。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screenshot-2020-06-07-at-17.11.03.png" alt="Screenshot-2020-06-07-at-17.11.03" width="600" height="400" loading="lazy"></p>
<p>执行 [GET] localhost:8080/todos/:id =&gt; 将会当查找指定 todo，并返回其内容。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screenshot-2020-06-07-at-17.16.06.png" alt="Screenshot-2020-06-07-at-17.16.06" width="600" height="400" loading="lazy"></p>
<p>执行 [GET] localhost:8080/todos/ =&gt; 将会返回包含 404 状态码及其错误信息的响应体。</p>
<p>目前我们搞定了如下 API：</p>
<ul>
<li>获取所有 todos</li>
<li>创建一个新的 todo</li>
<li>通过 ID 获取指定的 Todo</li>
</ul>
<p>仅剩下的 API：</p>
<ul>
<li>通过 ID 更新指定的 Todo</li>
<li>通过 ID 删除指定的 Todo</li>
</ul>
<h3 id="putidtodoapi">[PUT] 通过 ID 更新某 Todo 的 API</h3>
<p>让我们先为这个 API 创建模型（models）代码。进入 <code>models/todo.ts</code> 文件并为 <code>updateById</code> 方法填充其内容：</p>
<pre><code class="language-typescript">**
 * 更新某个指定 todo
 * @param id
 * @param todo
 * @param isCompleted
 * @returns integer (count of effect rows)
 */
updateById: async ({ id, todo, isCompleted }: Todo) =&gt; {
  const result = await client.query(
    `UPDATE ${TABLE.TODO} SET todo=?, isCompleted=? WHERE id=?`,
    [
      todo,
      isCompleted,
      id,
    ],
  );
  // return count of rows updated
  return result.affectedRows;
},
</code></pre>
<p><code>updateById</code> 方法将会从参数列表解构三个变量：<code>id</code>、<code>todo</code> 以及&nbsp;<code>isCompleted</code>。</p>
<p>我们直接编写了 MySQL 语句来执行查询：</p>
<pre><code class="language-typescript">const result = await client.query(
  `UPDATE ${TABLE.TODO} SET todo=?, isCompleted=? WHERE id=?`,
  [
    todo,
    isCompleted,
    id,
  ],
);
</code></pre>
<p>这段代码将通过 <code>id</code> 来更新指定的 <code>todo</code> 其&nbsp;<code>isCompleted</code> 的值。</p>
<p>接下来我们返回此条 MySQL 语句执行后影响到的数据表行数：</p>
<pre><code class="language-typescript">// return count of rows updated
return result.affectedRows;
</code></pre>
<p>这个行数值只会是 0 或者 1，且绝不会超过 1。因为我们数据表中的 ID 是唯一的——不同 todo 共用同一 ID的情况是不存在的。</p>
<p>接下来打开 <code>controllers/todo.ts</code> 文件并为 <code>updateTodoById</code> 方法填充其内容：</p>
<pre><code class="language-typescript">updateTodoById: async (
  { params, request, response }: {
    params: { id: string };
    request: any;
    response: any;
  },
) =&gt; {
  try {
    const isAvailable = await TodoModel.doesExistById(
      { id: Number(params.id) },
    );
    if (!isAvailable) {
      response.status = 404;
      response.body = {
        success: false,
        message: "No todo found",
      };
      return;
    }

    // 如果 todo 被找到了则更新它
    const body = await request.body();
    const updatedRows = await TodoModel.updateById({
      id: Number(params.id),
      ...body.value,
    });
    response.status = 200;
    response.body = {
      success: true,
      message: `Successfully updated ${updatedRows} row(s)`,
    };
  } catch (error) {
    response.status = 400;
    response.body = {
      success: false,
      message: `Error: ${error}`,
    };
  }
},
</code></pre>
<p>这段代码和前文的几个 API 中的代码几乎一样。与众不同的地方在这里：</p>
<pre><code class="language-typescript">// if todo found then update todo
const body = await request.body();
const updatedRows = await TodoModel.updateById({
  id: Number(params.id),
  ...body.value,
});
</code></pre>
<p>我们将用户传递来的 JSON 格式的 body 数据直接传给 <code>TodoModel.updateById</code> 函数。</p>
<p>记得需要转换 <code>id</code> 的变量类型为数值型以遵循接口的类型约束。</p>
<p>这行代码执行后将返回受到影响的行数。我们直接将其包装在响应体里返回。执行过程中如果有任何错误，将会被 catch 到并返回通用的报错信息。</p>
<p>让我们来重启服务器来检查是否能成功运行。请确保你的 MySQL 正在运行并在终端输入：</p>
<pre><code class="language-typescript">$ deno run --allow-net server.ts
</code></pre>
<p>打开 <a href="https://www.postman.com/">Postman</a> 来测试当前接口能否正常运行：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screenshot-2020-06-07-at-17.42.02.png" alt="Screenshot-2020-06-07-at-17.42.02" width="600" height="400" loading="lazy"></p>
<p>执行 [PUT] localhost:8080/todos/:id =&gt; 将会通过指定的 id 来更新相应的 todo 内容</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screenshot-2020-06-07-at-17.43.13.png" alt="Screenshot-2020-06-07-at-17.43.13" width="600" height="400" loading="lazy"></p>
<p>执行 [GET] localhost:8080/todos/ =&gt; 将会返回所有 todo 列表，来验证是否更新成功。</p>
<h3 id="deleteidtodoapi">[DELETE] 通过 ID 删除某 Todo 的 API</h3>
<p>在你的&nbsp;<code>models/todo.ts</code> 文件中创建一个&nbsp;<code>deleteById</code> 函数并填充如下内容：</p>
<pre><code class="language-typescript">/**
 * 通过指定 ID 来删除相应 todo
 * @param id
 * @returns integer (count of effect rows)
 */
deleteById: async ({ id }: Todo) =&gt; {
  const result = await client.query(
    `DELETE FROM ${TABLE.TODO} WHERE id = ?`,
    [id],
  );
  // return count of rows updated
  return result.affectedRows;
},
</code></pre>
<p>这里我们根据解构出的 <code>id</code> 值来通过 MySQL 删除指定的元素，并返回受到影响的行数。影响的行数的值依然只能是 0 或者 1，因为这个 ID 最多只会对应一个元素。</p>
<p>接下来，打开 <code>controllers/todo.ts</code> 文件并填充 <code>deleteByTodoId</code> 方法：</p>
<pre><code class="language-typescript">/**
 * @description 通过指定 ID 来删除相应 todo 
 * @route DELETE todos/:id
 */
deleteTodoById: async (
  { params, response }: { params: { id: string }; response: any },
) =&gt; {
  try {
    const updatedRows = await TodoModel.deleteById({
      id: Number(params.id),
    });
    response.status = 200;
    response.body = {
      success: true,
      message: `Successfully updated ${updatedRows} row(s)`,
    };
  } catch (error) {
    response.status = 400;
    response.body = {
      success: false,
      message: `Error: ${error}`,
    };
  }
},
</code></pre>
<p>这里很“爽快”地将解构出的 <code>params.id</code> 交给 <code>TodoModel.deleteById</code> 发方法，并返回此次执行过程中在数据库中的影响行数。</p>
<p>如果执行过程中有任何错误都会返回标准错误响应体。</p>
<p>让我们来检查这个 API 能否成功运行。</p>
<p>请确保你的 MySQL 正在运行，并在终端输入：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts
</code></pre>
<p>打开&nbsp;<a href="https://www.postman.com/">Postman</a>&nbsp;来测试：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screenshot-2020-06-07-at-18.23.04.png" alt="Screenshot-2020-06-07-at-18.23.04" width="600" height="400" loading="lazy"></p>
<p>执行 [GET] localhost:8080/todos/ =&gt; 将会得到所有 todo。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screenshot-2020-06-07-at-18.23.11.png" alt="Screenshot-2020-06-07-at-18.23.11" width="600" height="400" loading="lazy"></p>
<p>执行 [DELETE] localhost:8080/todos/:id =&gt; 将会通过指定的 id 删除相应元素。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/06/Screenshot-2020-06-07-at-18.23.44.png" alt="Screenshot-2020-06-07-at-18.23.44" width="600" height="400" loading="lazy"></p>
<p>执行 [GET] localhost:8080/todos/ =&gt; 将会返回所有 todo 列表，来看看之前想要删除的 todo 是否还在。</p>
<p>到了这里我们就结束了 Deno + Oak + MySQL 的实战教程。</p>
<p>整篇文章的代码可以在这里看到：<a href="https://github.com/adeelibr/deno-playground">https://github.com/adeelibr/deno-playground</a>。如果你有任何问题都可以在上面交流。或者提交你的 PR 到仓库中。</p>
<p>如果你感觉本系列很有帮助，可以分享它到社交网络中。同时我的 <a href="https://twitter.com/adeelibr">Twitter 账号是 @adeelibr</a>。我会很期待听到你的任何想法。</p>
<p>原文：<a href="https://www.freecodecamp.org/news/how-to-use-mysql-in-deno-oak/">How to Use MySQL With Deno and Oak</a>，作者：Adeel Imran</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Deno + Oak 构建酷炫的 Todo API ]]>
                </title>
                <description>
                    <![CDATA[ 序言 我是一位 JavaScript/Node 开发者，默默地喜欢甚至爱慕着 Deno。Deno 诞生之初就深深地吸引了我，此后我成为了 Deno 的忠实粉丝，期待着有朝一日能正式玩上 Deno。 本文专注于创造一个基于 REST API 设计的待做清单（Todo）应用。请记住本文中还不会涉及有关数据库操作的知识，其内容会在我之后的另一篇文章 [https://www.freecodecamp.org/news/how-to-use-mysql-in-deno-oak/]中进行详细介绍。 如果你想能够随时回顾或参考本文的代码，可以访问我的这个仓库：@adeelibr/deno-playground [https://github.com/adeelibr/deno-playground]，收录了该系列的所有代码。 > 译者注：另一篇文章《How to Use MySQL With Deno and Oak》即将会被翻译，其相关 Demo 也会被收录在《Deno 钻研之术》中。 照片来自于Bernard de Clerk [https://unsplash.com/@ber ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/create-a-todo-api-in-deno-written-by-a-guy-coming-from-node/</link>
                <guid isPermaLink="false">5ee70006db4be8080eb70e1d</guid>
                
                    <category>
                        <![CDATA[ Deno ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ hylerrix han ]]>
                </dc:creator>
                <pubDate>Mon, 15 Jun 2020 05:32:31 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/06/deno-oak-todo-api.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="">序言</h2>
<p>我是一位 JavaScript/Node 开发者，默默地喜欢甚至爱慕着 Deno。Deno 诞生之初就深深地吸引了我，此后我成为了 Deno 的忠实粉丝，期待着有朝一日能正式玩上 Deno。</p>
<p>本文专注于创造一个基于 REST API 设计的待做清单（Todo）应用。请记住本文中还不会涉及有关数据库操作的知识，其内容会在我之后的<a href="https://www.freecodecamp.org/news/how-to-use-mysql-in-deno-oak/">另一篇文章</a>中进行详细介绍。</p>
<p>如果你想能够随时回顾或参考本文的代码，可以访问我的这个仓库：<a href="https://github.com/adeelibr/deno-playground">@adeelibr/deno-playground</a>，收录了该系列的所有代码。</p>
<blockquote>
<p>译者注：另一篇文章《How to Use MySQL With Deno and Oak》即将会被翻译，其相关 Demo 也会被收录在《Deno 钻研之术》中。</p>
</blockquote>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1590672617573-08866973bf72.jpeg" alt="photo-1590672617573-08866973bf72" width="2000" height="1333" loading="lazy"></p>
<p>照片来自于&nbsp;<a href="https://unsplash.com/@bernardtheclerk?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit">Bernard de Clerk</a> / <a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit">Unsplash</a></p>
<h3 id="">本文会涉及的内容</h3>
<ul>
<li>创建一个最基础的服务器</li>
<li>创建 5 个 APIs（路由 routes/控制器 controller）</li>
<li>创建一个中间件来给 API 请求添加终端输出的日志功能</li>
<li>创建一个 404 中间件来处理用户访问未知 API 时的情况</li>
</ul>
<h3 id="">本文需要的知识准备</h3>
<ul>
<li>一个已经安装好的 Deno 环境（别怕，我会告诉你怎么做）</li>
<li>对 TypeScript 有浅要的了解</li>
<li>如果你之前对 Node/Express 一定的了解就更好了（不了解也没关系，本文还是很通俗易懂的）</li>
</ul>
<h2 id="">让我们开始吧</h2>
<p>首先我们要先安装 Deno。由于我使用的是 Mac 操作系统，所以在这里我将使用 brew。只需要打开终端并输入这条命令即可：</p>
<pre><code class="language-bash">$ brew install deno
</code></pre>
<p>但如果你用的是其它操作系统的话，这里有一个安装手册可以看看：<a href="https://deno.land/#installation">deno.land installation</a>。上面有多样化的安装方式可供你根据不同的操作系统来选择。</p>
<p>一旦你安装成功，关闭终端并打开另一个后，输入这条命令：</p>
<pre><code class="language-bash">$ deno --version
</code></pre>
<p>一切正常的话终端会产生如下输出：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-28-at-22.34.24.png" alt="Screenshot-2020-05-28-at-22.34.24" width="600" height="400" loading="lazy"></p>
<p><code>deno --version</code> 命令用来查看当前安装的 Deno 是哪个版本。</p>
<p>棒极了！通过这个介绍我们已经成功完成了本文 10% 的挑战。</p>
<p>让我们继续探索，并为我们的待做清单应用创建一个后端 API 吧。</p>
<h2 id="">项目的准备工作</h2>
<p>阅读下文，可以来提前来仓库里看看本文收录的所有代码：<a href="https://github.com/adeelibr/deno-playground">@adeelibr/deno-playground</a>。</p>
<p>这里我们从零做起：</p>
<ul>
<li>创建一个名为 <code>chapter_1:oak</code> 的新文件夹（你也可以随意起名）。</li>
<li>当你创建完毕后使用 <code>cd</code> 命令进入这个文件夹中。创建一个名为 <strong>server.ts</strong> 的文件并填充如下代码：</li>
</ul>
<pre><code class="language-javascript">import { Application } from "https://deno.land/x/oak/mod.ts";

const app = new Application();
const port: number = 8080;

console.log('running on port ', port);
await app.listen({ port });
</code></pre>
<p>让我们先运行这个文件。打开你的终端并进入当前项目的根目录后，输入如下命令：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts
</code></pre>
<p>别急别急，我会在之后来介绍 <code>--allow-net</code> 参数到底做了什么的 😄。</p>
<p>不出意外的话，你会得到如下结果：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-28-at-22.33.28.png" alt="Screenshot-2020-05-28-at-22.33.28" width="600" height="400" loading="lazy"></p>
<p>到现在为止，我们创建了一个监听着 8080 端口的服务端应用。只有 8080 端口不被占用，这个应用才能正常执行。</p>
<p>如果你有过使用 JavaScript 开发的经验，你可能会注意到我们导入模块的方式有些不一样。我们在这里是这样导入模块的：</p>
<pre><code>import { Application } from "https://deno.land/x/oak/mod.ts";
</code></pre>
<p>当你在终端中执行 <code>deno run ---allow-net &lt;file_name&gt;</code> 命令时，Deno 会读取你的导入信息，并在本地的全局环境中没有安装该模块的情况下安装这些模块。</p>
<p>第一次执行时 Deno 会尝试访问 <code>https://deno.land/x/oak/mod.ts</code> 模块并安装 <code>oak</code> 库。 Oak 是一个专注于编写 API 的 Deno Web 框架。</p>
<p>接下来的一行我们是这样写的：</p>
<pre><code class="language-javascript">const app = new Application();
</code></pre>
<p>这条语句为我们的应用创建了一个实例，这个实例是本文深入探索 Deno 的基石。你可以为这个实例增加路由，配置中间件（如日志中间件），编写 404 未知路由处理程序等等。</p>
<p>接下来我们是这样写的：</p>
<pre><code class="language-javascript">const port: number = 8080;
// const port = 8080; // =&gt; 也可以写成这样
</code></pre>
<p>上面两行在功能上是等价的，唯一的区别是 <code>const port: number = 8080</code> 告诉 TypeScript： <code>port</code> 变量的类型是数值类的。</p>
<p>如果你这样写的话：<code>const port: number = "8080"</code>，终端会产生类似这样的报错：port 变量应该是&nbsp;<code>number</code> 类型的，但是这类尝试用&nbsp;<code>string</code> 类型的 "8080" 来为其赋值。</p>
<p>如果你想学习关于 Type 的更多类型现在就可以看看这个简单的文档：<a href="https://www.typescriptlang.org/docs/handbook/basic-types.html">TypeScript 官方 - 基础 Types 类型</a>。仅仅 2~3 分钟就可以重新回到本文。</p>
<p>在文件的最后我们是这样写的：</p>
<pre><code class="language-javascript">console.log('running on port ', port);
await app.listen({ port });
</code></pre>
<p>如上我们让 Deno 监听了 8080 端口，端口号是写死的。</p>
<p>在你的 <strong>server.ts</strong> 文件中添加如下更多的代码：</p>
<pre><code class="language-javascript">import { Application, Router } from "https://deno.land/x/oak/mod.ts";

const app = new Application();
const port: number = 8080;

const router = new Router();
router.get("/", ({ response }: { response: any }) =&gt; {
  response.body = {
    message: "hello world",
  };
});
app.use(router.routes());
app.use(router.allowedMethods());

console.log('running on port ', port);
await app.listen({ port });
</code></pre>
<p>相比之前新增的内容是从 <code>oak</code> 中同时导入了 <code>Application</code> 和 <code>Router</code>&nbsp;变量。</p>
<p>其中关于 <code>Router</code> 的相关代码是：</p>
<pre><code>const router = new Router();
router.get("/", ({ response }: { response: any }) =&gt; {
  response.body = {
    message: "hello world",
  };
});
app.use(router.routes());
app.use(router.allowedMethods());
</code></pre>
<p>我们通过 <code>const router = new Router()</code> 语句创建了新的 Router 示例，然后我们为其根目录 <code>/</code> 创建了处理 <code>get</code> 请求的执行方式。</p>
<p>让我们重点看看如下内容：</p>
<pre><code class="language-javascript">router.get("/", ({ response }: { response: any }) =&gt; {
  response.body = {
    message: "hello world",
  };
});
</code></pre>
<p><code>router.get</code> 函数接收两个参数。第一个参数是路由挂载的路径 <code>/</code>，第二个参数是一个函数。函数本身也接受一个对象参数，这里使用 ES6 语法将其解构，只取了其中 response 变量的值。</p>
<p>接下来就像之前编写&nbsp;<code>const port: number = 8080;</code> 语句一样为 <code>response</code> 变量声明类型。<code>{ response }: { response: any }</code> 语句告诉 TypeScript 我们这里解构的 <code>response</code> 变量是 <code>any</code> 类型的。</p>
<p><code>any</code> 类型可以帮准你避免 TypeScript 进行严格的类型检查，你可以通过<a href="https://www.typescriptlang.org/docs/handbook/basic-types.html#any">这个文档</a>来了解更多。</p>
<p>接下来我所编写的就是使用 <code>response</code> 变量，并设置 <code>response.body.message = "hello world";</code>。</p>
<pre><code>response.body = {
  message: "hello world",
};
</code></pre>
<p>最后同样重要的是，我们编写了如下两行代码：</p>
<pre><code>app.use(router.routes());
app.use(router.allowedMethods());
</code></pre>
<p>第一行告诉 Deno 要包含我们的 router 变量里设置的所有路径（目前我们只设置了根路径），第二行让 Deno 允许任意访问方法来请求我们设置的路径，比如 <code>GET, POST, PUT, DELETE</code>。</p>
<p>到这里就可以测试运行了 ✅ ，让我们执行这行语句来看看最终会发生什么：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts
</code></pre>
<p><code>---allow-net</code> 参数告诉 Deno：用户授予了这个应用在打开的端口上访问网络的权限。</p>
<p>现在通过你常用的浏览器打开 <code>http://localhost:8080</code> 地址，就可以得到如下结果：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-28-at-23.11.08.png" alt="Screenshot-2020-05-28-at-23.11.08" width="600" height="400" loading="lazy"><br>
浏览器打开 localhost:8080 的执行结果</p>
<p>最难的部分差不多搞定了，但在对概念的更多了解中我们只进行了 60% 的介绍。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/images.jpeg#align=left&amp;display=inline&amp;height=195&amp;margin=%5Bobject%20Object%5D&amp;originHeight=195&amp;originWidth=258&amp;status=done&amp;style=none&amp;width=258" alt="images" width="600" height="400" loading="lazy"><br>
来自 Yoda 大师的批准</p>
<p>棒极了。</p>
<p>在我们正式开始编写待做清单的 API 前，我们最后要做的事是将如下代码：</p>
<pre><code class="language-javascript">console.log('running on port ', port);
await app.listen({ port });
</code></pre>
<p>替换成这样：</p>
<pre><code class="language-javascript">app.addEventListener("listen", ({ secure, hostname, port }) =&gt; {
  const protocol = secure ? "https://" : "http://";
  const url = ${protocol}${hostname ?? "localhost"}:${port};
  console.log(Listening on: ${port});
});

await app.listen({ port });
</code></pre>
<p>我们之前的代码是先在控制台上简单的打印一条成功日志，然后再让应用开始在端口上监听，不是很优雅（译者注：有可能会在监听失败的情况下依然打印监听成功的日志）。</p>
<p>在替换后的版本中，我们通过 <code>app.addEventListener("listen", ({ secure, hostname, port }) =&gt; {}</code> 语句来向应用实例添加事件侦听器后，再让应用监听在端口上。</p>
<p>侦听器的第一个参数是我们想侦听的事件。一语双关，这里侦听（listen）的就是 <code>listen</code> 事件 😅。第二个参数是一个可以被解构的对象，这里解构出 <code>{ secure, hostname, port }</code> 三个变量。Secure 变量是布尔类型，hostname 变量是字符串类型，port 变量是数值类型。</p>
<p>此时运行这个应用的话，只有在成功监听指定端口后才会输出监听成功的日志，</p>
<p>我们可以再向远方迈出一步，使其更加丰富多彩。让我们在 <code>server.ts</code> 文件的顶部添加这样一个新模块：</p>
<pre><code class="language-javascript">import { green, yellow } from "https://deno.land/std@0.53.0/fmt/colors.ts";
</code></pre>
<p>接下来我们可以在之前的事件侦听器函数里将如下代码：</p>
<pre><code class="language-javascript">console.log(Listening on: ${port});
</code></pre>
<p>替换为：</p>
<pre><code class="language-javascript">console.log(${yellow("Listening on:")} ${green(url)});
</code></pre>
<p>接下来当我们执行：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts
</code></pre>
<p>将会打印输出如下日志：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-28-at-23.34.29.png" alt="Screenshot-2020-05-28-at-23.34.29" width="600" height="400" loading="lazy"></p>
<p>太酷了，我们现在有了一个色彩缤纷的控制台。</p>
<p>如果你在某处卡住了，你可以直接访问本教程的源码仓库：<a href="https://github.com/adeelibr/deno-playground/tree/master/chapter_1:oak">@adeelibr/deno-playground</a>。</p>
<p>让我们接下来创建待做清单的 API 吧。</p>
<ul>
<li>在项目的根目录创建一个 <code>routes</code> 文件夹，然后再文件夹里面创建一个 <code>todo.ts</code> 文件。</li>
<li>与此同时在项目根目录创建一个 <code>controllers</code> 文件夹，再在文件夹里也创建一个 <code>todo.ts</code> 文件。</li>
</ul>
<p>我们先来填充 <code>controllers/todo.ts</code> 文件里的内容：</p>
<pre><code class="language-javascript">export default {
  getAllTodos: () =&gt; {},
  createTodo: async () =&gt; {},
  getTodoById: () =&gt; {},
  updateTodoById: async () =&gt; {},
  deleteTodoById: () =&gt; {},
};
</code></pre>
<p>我们在这里先简单地导出了一个包含很多有名字的函数的对象，这些函数目前都是空的。</p>
<p>接下来在 <code>routes/todo.ts</code> 文件中填充这些：</p>
<pre><code class="language-javascript">import { Router } from "https://deno.land/x/oak/mod.ts";
const router = new Router();
// controller 控制器
import todoController from "../controllers/todo.ts";
router
  .get("/todos", todoController.getAllTodos)
  .post("/todos", todoController.createTodo)
  .get("/todos/:id", todoController.getTodoById)
  .put("/todos/:id", todoController.updateTodoById)
  .delete("/todos/:id", todoController.deleteTodoById);

export default router;
</code></pre>
<p>对于编写过 Node 和 Express 的人来说，对如上的代码风格一定很熟悉。</p>
<p>其中包括从 <code>oak</code> 中导入了 <code>Route</code> 变量并通过 <code>const router = new Router();</code> 语句将其实例化。</p>
<p>接下来我们导入我们的控制器：</p>
<pre><code>import todoController from "../controllers/todo.ts";
</code></pre>
<p>这里需要注意的是：在 Deno 中我们每次导入一个本地文件到项目中的时候，我们都必须填写完整这个文件的后缀。否则 Deno 是不知道用户想要导入的文件后缀到底以 <code>.js</code>&nbsp;还是 <code>.ts</code> 结尾。</p>
<p>接下来我们通过如下代码为应用配置了我们需要的所有 RESTful 风格的路径。</p>
<pre><code class="language-javascript">router
  .get("/todos", todoController.getAllTodos)
  .post("/todos", todoController.createTodo)
  .get("/todos/:id", todoController.getTodoById)
  .put("/todos/:id", todoController.updateTodoById)
  .delete("/todos/:id", todoController.deleteTodoById);
</code></pre>
<p>上面的代码会将路径解析为这样：</p>
<table>
<thead>
<tr>
<th>请求方式</th>
<th>API 路由</th>
</tr>
</thead>
<tbody>
<tr>
<td>GET</td>
<td>/todos</td>
</tr>
<tr>
<td>GET</td>
<td>/todos/:id</td>
</tr>
<tr>
<td>POST</td>
<td>/todos</td>
</tr>
<tr>
<td>PUT</td>
<td>/todos/:id</td>
</tr>
<tr>
<td>DELETE</td>
<td>/todos/:id</td>
</tr>
</tbody>
</table>
<p>最后我们通过&nbsp;<code>export default router;</code>&nbsp;语句来将配置好的路由导出。</p>
<p>此时我们已经完成了创建路由的工作（但是由于我们的控制器还是空的函数，所以每个路由并都不会做任何反应，我们将向其中添加功能）。</p>
<p>在我们开始向每个控制器添加功能之前的最后一个难题是，我们需要将此 <code>router</code> 挂载到我们的&nbsp;<code>app</code> 实例上。</p>
<p>因此回到 <code>server.ts</code>&nbsp;文件中我们这样做：</p>
<ul>
<li>将这行代码添加至文件顶部：</li>
</ul>
<pre><code class="language-javascript">// routes 路由
import todoRouter from "./routes/todo.ts";
</code></pre>
<ul>
<li>删除这一段代码：</li>
</ul>
<pre><code class="language-javascript">const router = new Router();
router.get("/", ({ response }: { response: any }) =&gt; {
  response.body = {
    message: "hello world",
  };
});
app.use(router.routes());
app.use(router.allowedMethods());
</code></pre>
<ul>
<li>将其替换为：</li>
</ul>
<pre><code>app.use(todoRouter.routes());
app.use(todoRouter.allowedMethods());
</code></pre>
<p>终于搞定了，你的 <code>server.ts</code> 现在应该是这个样子：</p>
<pre><code class="language-javascript">import { Application } from "https://deno.land/x/oak/mod.ts";
import { green, yellow } from "https://deno.land/std@0.53.0/fmt/colors.ts";

// routes
import todoRouter from "./routes/todo.ts";

const app = new Application();
const port: number = 8080;

app.use(todoRouter.routes());
app.use(todoRouter.allowedMethods());

app.addEventListener("listen", ({ secure, hostname, port }) =&gt; {
  const protocol = secure ? "https://" : "http://";
  const url = `${protocol}${hostname ?? "localhost"}:${port}`;
  console.log(
    `${yellow("Listening on:")} ${green(url)}`,
  );
});

await app.listen({ port });
</code></pre>
<p>如果你在某处卡住了，你可以直接访问本教程的源码仓库：<a href="https://github.com/adeelibr/deno-playground/tree/master/chapter_1:oak">@adeelibr/deno-playground</a>。</p>
<p>由于路由的控制器上暂时没有任何功能，现在一起来手动为我们的控制器添加功能。</p>
<p>在此之前我们得先创建两个（小）文件：</p>
<ul>
<li>在项目的根目录上创建一个 <code>interfaces</code>&nbsp;文件夹并在其中创建一个 <code>Todo.ts</code>（确保 Todo 首字母大写，因为如果不这样做，它将不会在此处给出任何语法错误——这只是一种约定）。</li>
<li>同时在项目根目录创建一个 <code>stubs</code>&nbsp;文件夹并在其中创建一个&nbsp;<code>todos.ts</code>&nbsp;文件。</li>
</ul>
<p>在 <code>interfaces/Todo.ts</code>&nbsp;文件中编写如下接口说明：</p>
<pre><code class="language-interfaces">export default interface Todo {
  id: string,
  todo: string,
  isCompleted: boolean,
}
</code></pre>
<p>什么是 interface（接口）？</p>
<p>要知道 TypeScript 的核心功能之一是检查一个变量的类型。就像前文的 <code>const port: number = 8080</code>&nbsp;和&nbsp;<code>{ response }: { response : any }</code>&nbsp;一样，我们也可以检测一个变量是否为对象类型。</p>
<p>在 TypeScript 中，interface 负责命名类型，并且是<strong>定义代码内外类型约束</strong>的有效方法。</p>
<p>这里有一个有关 interface 的示例：</p>
<pre><code class="language-javascript">// 写了个接口
interface LabeledValue {
  label: string;
}

// 此函数的labeledObj 参数是符合 LabeledValue 接口类型的
function printLabel(labeledObj: LabeledValue) {
  console.log(labeledObj.label);
}

let myObj = {label: "Size 10 Object"};
printLabel(myObj);
</code></pre>
<p>希望如上示例可以让你对 interface 有更多的了解。如果你想了解更多的信息可以查看：<a href="https://www.typescriptlang.org/docs/handbook/interfaces.html">Interfaces 官方文档</a>。</p>
<p>现在关于 interface 的知识已经介绍够了，我们一起来模拟一些假数据（因为本文不涉及有关数据库的操作）。</p>
<p>我们在 <code>stubs/todos.ts</code> 文件中来为 todos 变量填充一些模拟数据。这样即可：</p>
<pre><code class="language-javascript">import { v4 } from "https://deno.land/std/uuid/mod.ts";
// interface
import Todo from '../interfaces/Todo.ts';

let todos: Todo[] = [
  {
    id: v4.generate(),
    todo: 'walk dog',
    isCompleted: true,
  },
  {
    id: v4.generate(),
    todo: 'eat food',
    isCompleted: false,
  },
];

export default todos;
</code></pre>
<ul>
<li>有两件需要注意的事项：我们这里引用了一个新的模块并且通过 <code>import { v4 } from "https://deno.land/std/uuid/mod.ts";</code>&nbsp;语句解构了其中的 <code>v4</code>&nbsp;变量。接下来我们每次使用 <code>v4.generate()</code>&nbsp;语句都能生成一个随机的 ID 字符串。这个 <code>id</code>&nbsp;不能是&nbsp;<code>number</code> 类型的，而需是&nbsp;<code>string</code>&nbsp;类型的，因为我们之前的 <code>Todo</code> 接口已经声明了&nbsp;<code>id</code>&nbsp;的类型必须是字符串。</li>
<li>另一个需要注意的是 <code>let todos: Todo[] = []</code>&nbsp;语句。此语句告诉 Deno 我们的 todos 变量是一个&nbsp;<code>Todo</code> 数组（此时编译器将会知道数组的每一个元素都是&nbsp;<code>{id: _string_, todo: _string_ &amp; isCompleted: _boolean_}</code> 类型的，并不允许其他任何类型）。</li>
</ul>
<p>如果你想了解更多的信息可以查看：<a href="https://www.typescriptlang.org/docs/handbook/interfaces.html">Interfaces 官方文档</a>。</p>
<p>太棒了，你已经进行到如此之远，再接再厉。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/download-1.jpeg#align=left&amp;display=inline&amp;height=168&amp;margin=%5Bobject%20Object%5D&amp;originHeight=168&amp;originWidth=300&amp;status=done&amp;style=none&amp;width=300" alt="download-1" width="600" height="400" loading="lazy"></p>
<p>巨石强森感激你所做的一切努力。</p>
<h2 id="">让我们关注在控制器上</h2>
<p>在你的&nbsp;<code>controllers/todo.ts</code>&nbsp;文件中：</p>
<pre><code class="language-javascript">export default {
  getAllTodos: () =&gt; {},
  createTodo: async () =&gt; {},
  getTodoById: () =&gt; {},
  updateTodoById: async () =&gt; {},
  deleteTodoById: () =&gt; {},
};
</code></pre>
<p>让我们先编写&nbsp;<code>getAllTodos</code>&nbsp;控制器：</p>
<pre><code class="language-javascript">// stubs
import todos from "../stubs/todos.ts";

export default {
  /**
   * @description 获取所有 todos
   * @route GET /todos
   */
  getAllTodos: ({ response }: { response: any }) =&gt; {
    response.status = 200;
    response.body = {
      success: true,
      data: todos,
    };
  },
  createTodo: async () =&gt; {},
  getTodoById: () =&gt; {},
  updateTodoById: async () =&gt; {},
  deleteTodoById: () =&gt; {},
};
</code></pre>
<p>在开始介绍这段代码前，让我解释下每个控制器都有的参数——<code>context</code>（上下文）参数。</p>
<p>因此我们才能解构 <code>getAllTodos: (context) =&gt; {}</code>&nbsp;为：</p>
<pre><code class="language-javascript">getAllTodos: ({ request, response, params }) =&gt; {}
</code></pre>
<p>并且自从哪个我们使用&nbsp;<code>typescript</code>&nbsp;后，我们需要为每个这样的变量添加类型声明：</p>
<pre><code class="language-javascript">getAllTodos: (
  { request, response, params }: { 
    request: any, 
    response: any, 
    params: { id: string },
  },
) =&gt; {}
</code></pre>
<p>此时我们为解构的三个变量&nbsp;<code>{ request, response, params }</code>&nbsp;添加了类型说明。</p>
<ul>
<li><code>request</code>&nbsp;变量有关用户发来的请求（比如请求头和 JSON 类的请求体）。</li>
<li><code>response</code>&nbsp;变量有关服务器端通过 API 返回的信息。</li>
<li><code>params</code>&nbsp;变量是我们在路由配置中定义的参数，如下：</li>
</ul>
<pre><code class="language-javascript">.get("/todos/:id", ({ params}: { params: { id: string } }) =&gt; {})
</code></pre>
<p><code>/todos/:id</code>&nbsp;中的&nbsp;<code>:id</code>&nbsp;是一个变量，用来从 URL 中获得动态的数据。因此当用户访问这个 API （比如 <code>/todos/756</code>）的时候，<strong>756</strong> 则是 <strong>:id</strong> 参数的值。并且我们知道 URL 里的这个值的类型是&nbsp;<code>string</code>&nbsp;类的。</p>
<p>现在我们有了基本的声明后，让我们回到我们的 todos 控制器：</p>
<pre><code class="language-javascript">// stubs
import todos from "../stubs/todos.ts";

export default {
  /**
   * @description 获取所有 todos
   * @route GET /todos
   */
  getAllTodos: ({ response }: { response: any }) =&gt; {
    response.status = 200;
    response.body = {
      success: true,
      data: todos,
    };
  },
  createTodo: async () =&gt; {},
  getTodoById: () =&gt; {},
  updateTodoById: async () =&gt; {},
  deleteTodoById: () =&gt; {},
};
</code></pre>
<p>对于 <code>getAllTodos</code>&nbsp;方法来说我们只需要简单的返回结果。如果你记得之前说的，会想起来&nbsp;<code>response</code>&nbsp;是用来处理服务器想要给用户返回的数据。</p>
<p>对于编写过 Node 和 Express 的人来说，这里的一大不同是我们不需要 <code>return</code>&nbsp;响应对象。 Deno 会自动为我们执行此操作。</p>
<p>我们需要做的第一件事是通过 <code>response.status</code> 来设置此次请求的响应码是 <code>200</code>。</p>
<p>更多 HTTP 响应码可以看 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status">MDN 上的&nbsp;HTTP 响应状态码文档</a>。</p>
<p>另一件事是设置 <code>response.body</code>&nbsp;的值为：</p>
<pre><code class="language-javascript">{
  success: true,
  data: todos
}
</code></pre>
<p>重新运行我们的服务器：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts
</code></pre>
<blockquote>
<p>修订：--allow-net 属性告诉 Deno，此应用程序授予用户通过打开的端口访问网络的权限。</p>
</blockquote>
<p>一旦你的服务端示例跑通，挺可以通过 <code>GET /todos</code>&nbsp;方式来请求这个 API。这里我使用的是 Google Chrome 浏览器下的一个插件 <code>postman</code>，<a href="https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop//%40">在这里下载</a>。</p>
<p>你可以使用任意的 REST 风格的客户端，我喜欢使用 <code>postman</code> 是因为它真的很简单好用。</p>
<p>在 Postman 中，打开一个新的标签页。设置请求方式为&nbsp;<code>GET</code>&nbsp;请求并且在&nbsp;<code>URL</code>&nbsp;输入框中输入 <code>http://localhost:8080/todos</code>&nbsp;。点击 <code>Send</code> 按钮便会得到想要的结果：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-02.01.11.png" alt="Screenshot-2020-05-29-at-02.01.11" width="600" height="400" loading="lazy"></p>
<p>GET /todos API 返回结果。</p>
<p>酷！一个 API 搞定了，还剩 4 个等着我们&nbsp;👍👍。</p>
<p>如果你在某处卡住了，可以在<a href="https://github.com/adeelibr/deno-playground/tree/master/chapter_1:oak">配套的源码仓库</a>中寻找答案。</p>
<p>让我们关注下一个控制器吧：</p>
<pre><code class="language-javascript">import { v4 } from "https://deno.land/std/uuid/mod.ts";
// interfaces
import Todo from "../interfaces/Todo.ts";
// stubs
import todos from "../stubs/todos.ts";

export default {
  getAllTodos: () =&gt; {},
  /**
   * @description Add a new todo
   * @route POST /todos
   */
  createTodo: async (
    { request, response }: { request: any; response: any },
  ) =&gt; {
    const body = await request.body();
    if (!request.hasBody) {
      response.status = 400;
      response.body = {
        success: false,
        message: "No data provided",
      };
      return;
    }

    // 如果请求体验证通过，则返回新增后的所有 todos
    let newTodo: Todo = {
      id: v4.generate(),
      todo: body.value.todo,
      isCompleted: false,
    };
    let data = [...todos, newTodo];
    response.body = {
      success: true,
      data,
    };
  },
  getTodoById: () =&gt; {},
  updateTodoById: async () =&gt; {},
  deleteTodoById: () =&gt; {},
};
</code></pre>
<p>由于我们将要添加一个新的 Todo 到列表中，因此我在 controller 文件中导入了 2 个通用模块：</p>
<ul>
<li><code>import { v4 } from "https://deno.land/std/uuid/mod.ts"</code>&nbsp;语句用来为每一个 todo 元素创建一个独一无二的标识。</li>
<li><code>import Todo from "../interfaces/Todo.ts";</code> 语句用来保证新建的 todo 遵循 todo 元素的接口格式标准。</li>
</ul>
<p>我们的 <code>createTodo</code>&nbsp;控制器是 <code>async</code> 异步的代表函数中会使用到一些 Promise 技术。</p>
<p>让我们来截断说明其中的小片段：</p>
<pre><code class="language-javascript">const body = await request.body();
if (!request.hasBody) {
  response.status = 400;
  response.body = {
    success: false,
    message: "No data provided",
  };
  return;
}
</code></pre>
<p>首先我们读取请求体中用户传来的的 JSON 内容。接下来我们使用 <code>oak</code>&nbsp;的内置 <code>request.hasBody</code>&nbsp;方法来检查用户传来的内容是否为空。如果为空，我们将进入&nbsp;<code>if (!request.hasBody) {}</code>&nbsp;代码块中执行相关操作。</p>
<p>里面我们将响应体的状态码设置成 <code>400</code>（400 代表着用户端出现了一些本不该出现的错误），并且服务端返回的响应体为 <code>{success: false, message: "no data provided }</code>。之后程序直接执行&nbsp;<code>return;</code>&nbsp;语句来保证接下来的代码不会被执行。</p>
<p>接下来我们这样编写：</p>
<pre><code class="language-javascript">// 如果请求体验证通过，则返回新增后的所有 todos
let newTodo: Todo = {
  id: v4.generate(),
  todo: body.value.todo,
  isCompleted: false,
};
let data = [...todos, newTodo];
response.body = {
  success: true,
  data,
};
</code></pre>
<p>其中我们通过如下代码创建了一个全新的 todo 元素：</p>
<pre><code class="language-javascript">let newTodo: Todo = {
  id: v4.generate(),
  todo: body.value.todo,
  isCompleted: false,
};
</code></pre>
<p><code>let newTodo: Todo = {}</code> 保证&nbsp;<code>newTodo</code>&nbsp;变量的值和其它 todo 元素一样都遵循相同的接口格式。然后，我们使用 <code>v4.generate()</code>&nbsp;分配一个随机 ID，将 todo 的键值设置为 <code>body.value.todo</code>&nbsp;并将 <code>isCompleted</code>&nbsp;变量值设置为&nbsp;<code>false</code>。</p>
<p>这里需要知道的是，用户给我们发的内容我们可以通过 <code>oak</code>&nbsp;中的&nbsp;<code>body.value</code>&nbsp;来获取。</p>
<p>接下来我们这样做：</p>
<pre><code class="language-javascript">let data = [...todos, newTodo];
response.body = {
  success: true,
  data,
};
</code></pre>
<p>这里将 <code>newTodo</code>&nbsp;添加到整个 todo 列表中中，并在响应体中返回&nbsp;<code>{success: true &amp; data: data</code>。</p>
<p>此时这个控制器也运行成功了&nbsp;✅。</p>
<p>让我们重新运行我们的服务器：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts
</code></pre>
<p>在 postman 中，我再打开一个新的标签页。设置请求的方式为 <code>POST</code>&nbsp;类型，并在&nbsp;<code>URL</code>&nbsp;输入框中输入 <code>http://localhost:8080/todos</code>&nbsp;后，点击 <code>Send</code>&nbsp;便会得到如下结果：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-02.24.00.png" alt="Screenshot-2020-05-29-at-02.24.00" width="600" height="400" loading="lazy"></p>
<p>因为上面的请求体中发送了空的内容，所以得到了 400 错误响应码及其错误原因。</p>
<p>但如果我们给请求体中加入如下 JSON 内容，并重新发送：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-02.24.15.png" alt="Screenshot-2020-05-29-at-02.24.15" width="600" height="400" loading="lazy"></p>
<p>通过 { todo: "eat a lamma" } 来&nbsp;POST /todos&nbsp;后的成功结果，我们可以看到新的元素已经加入到列表中。</p>
<p>酷，我买可以看到我们的 API 已经一个个以预期的方式执行成功了。</p>
<p>两个 API 搞定，还剩三个要做。</p>
<p>我们快要搞定了，因为大部分难的内容已经介绍完毕。☺️ 🙂🤗🤩</p>
<p>让我们看看第三个 API：</p>
<pre><code class="language-javascript">import { v4 } from "https://deno.land/std/uuid/mod.ts";
// interfaces
import Todo from "../interfaces/Todo.ts";
// stubs
import todos from "../stubs/todos.ts";

export default {
  getAllTodos: () =&gt; {},
  createTodo: async () =&gt; {},
  /**
   * @description 通过 ID 获取 todo
   * @route GET todos/:id
   */
  getTodoById: (
    { params, response }: { params: { id: string }; response: any },
  ) =&gt; {
    const todo: Todo | undefined = todos.find((t) =&gt; {
      return t.id === params.id;
    });
    if (!todo) {
      response.status = 404;
      response.body = {
        success: false,
        message: "No todo found",
      };
      return;
    }

    // 如果 todo 找到了
    response.status = 200;
    response.body = {
      success: true,
      data: todo,
    };
  },
  updateTodoById: async () =&gt; {},
  deleteTodoById: () =&gt; {},
};
</code></pre>
<p>我们先来聊聊&nbsp;<code>GET todos/:id</code>&nbsp;下的控制器，此控制器会通过 ID 来查找相应的 todo 元素。</p>
<p>让我们继续通过截取小片段来深入分析：</p>
<pre><code class="language-javascript">const todo: Todo | undefined = todos.find((t) =&gt; t.id === params.id);
if (!todo) {
  response.status = 404;
  response.body = {
    success: false,
    message: "No todo found",
  };
  return;
}
</code></pre>
<p>在第一行我们声明了一个 <code>const todo</code> 变量并将其类型设置为&nbsp;<code>Todo</code>&nbsp;或&nbsp;<code>undefined</code>&nbsp;类。因此 <code>todo</code> 元素只能是符合 <code>Todo</code>&nbsp;接口规范的变量或者是一个 <code>undefined</code>&nbsp;值，而不能是其它任何类型。</p>
<p>我们接下来使用 <code>todos.find((t) =&gt; t.id === params.id);</code>&nbsp;语句来通过&nbsp;<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find">Array.find()</a>&nbsp;方法和&nbsp;<code>params.id</code>&nbsp;的值来查找指定的 <code>todo</code>&nbsp;元素。如果找到了我们会得到&nbsp;<code>Todo</code>&nbsp;类型的 <code>todo</code>&nbsp;元素，发否则得到一个&nbsp;<code>undefined</code>&nbsp;值。</p>
<p>如果得到的 <code>todo</code>&nbsp;的值是 undefined 的，意味着如下 if 条件中的代码会执行：</p>
<pre><code class="language-javascript">if (!todo) {
  response.status = 404;
  response.body = {
    success: false,
    message: "No todo found",
  };
  return;
}
</code></pre>
<p>这里我们设置响应的状态码为 <code>404</code>，代表着 <code>not found</code>&nbsp;没有找到相关元素，并且返回体的格式也是标准的&nbsp;<code>{ status, message }</code>。</p>
<p>很酷不是嘛？ 😄</p>
<p>接下来我们简单地编写：</p>
<pre><code class="language-javascript">// 如果 todo 找到了
response.status = 200;
response.body = {
  success: true,
  data: todo,
};
</code></pre>
<p>设置一个响应状态码为 <code>200</code>&nbsp;的响应体并返回&nbsp;<code>success: true &amp; data: todo</code>&nbsp;内容。</p>
<p>我们来在 postman 中测试：</p>
<p>先一起重新启动服务端：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts
</code></pre>
<p>在 postman 中，继续打开一个新的标签页，设置请求方式为&nbsp;<code>GET</code>&nbsp;请求并在&nbsp;<code>URL</code>&nbsp;输入框中输入 <code>http://localhost:8080/todos/:id</code>&nbsp;后，点击 <code>Send</code>&nbsp;来执行请求。</p>
<p>自从我们使用了随机 ID 生成器，首先我们需要调取获取所有元素的 API。并在元素列表里选取一个 ID 来测试这个新的 API。每次你重启 Deno 程序时，新的 ID 都会被重新生成。</p>
<p>我们这样输入：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-02.40.52.png" alt="Screenshot-2020-05-29-at-02.40.52" width="600" height="400" loading="lazy"></p>
<p>服务端返回 404，且告诉我们没有相关数据被找到。,</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-02.41.36.png" alt="Screenshot-2020-05-29-at-02.41.36" width="600" height="400" loading="lazy"></p>
<p>但如果输入一个正确的 ID，服务端会返回其 ID 和这个 ID 的一样的数据并且响应状态为 200。</p>
<p>如果你需要参考本文的源码可以访问这里：<a href="https://github.com/adeelibr/deno-playground">@adeelibr/deno-playground</a>。</p>
<p>不错，3 个 API 搞定，只剩 2 个了。</p>
<pre><code class="language-javascript">import { v4 } from "https://deno.land/std/uuid/mod.ts";
// interfaces
import Todo from "../interfaces/Todo.ts";
// stubs
import todos from "../stubs/todos.ts";

export default {
  getAllTodos: () =&gt; {},
  createTodo: async () =&gt; {},
  getTodoById: () =&gt; {},
  /**
   * @description Update todo by id
   * @route PUT todos/:id
   */
  updateTodoById: async (
    { params, request, response }: {
      params: { id: string },
      request: any,
      response: any,
    },
  ) =&gt; {
    const todo: Todo | undefined = todos.find((t) =&gt; t.id === params.id);
    if (!todo) {
      response.status = 404;
      response.body = {
        success: false,
        message: "No todo found",
      };
      return;
    }

    // 如果找到相应 todo 则更新它
    const body = await request.body();
    const updatedData: { todo?: string; isCompleted?: boolean } = body.value;
    let newTodos = todos.map((t) =&gt; {
      return t.id === params.id ? { ...t, ...updatedData } : t;
    });
    response.status = 200;
    response.body = {
      success: true,
      data: newTodos,
    };
  },
  deleteTodoById: () =&gt; {},
};
</code></pre>
<p>让我们来探讨下一个控制器&nbsp;<code>PUT todos/:id</code>。这个控制器会更新一个元素的内容。</p>
<p>我们继续截断代码来细看：</p>
<pre><code class="language-javascript">const todo: Todo | undefined = todos.find((t) =&gt; t.id === params.id);
if (!todo) {
  response.status = 404;
  response.body = {
    success: false,
    message: "No todo found",
  };
  return;
}
</code></pre>
<p>这里做的和之前控制器做的一样，所以我就不深入介绍了。</p>
<p>高级提示：如果你想将这段代码设为通用代码块，然后在两个控制器中都使用它，完全可以。</p>
<p>接下来我们这样做：</p>
<pre><code class="language-javascript">// 如果找到相应 todo 则更新它
const body = await request.body();
const updatedData: { todo?: string; isCompleted?: boolean } = body.value;
let newTodos = todos.map((t) =&gt; {
  return t.id === params.id ? { ...t, ...updatedData } : t;
});
response.status = 200;
response.body = {
  success: true,
  data: newTodos,
};
</code></pre>
<p>其中我想在这里重点讨论的代码如下：</p>
<pre><code class="language-javascript">const updatedData: { todo?: string; isCompleted?: boolean } = body.value;
let newTodos = todos.map((t) =&gt; {
  return t.id === params.id ? { ...t, ...updatedData } : t;
});
</code></pre>
<p>首先，我们执行 <code>const updatedData = body.value</code>，然后将类型检查添加到 <code>updatedData</code> 上，如下所示：</p>
<pre><code class="language-javascript">updatedData: { todo?: string; isCompleted?: boolean }
</code></pre>
<p>这一小段代码告诉 TS：<code>updatedData</code>&nbsp;变量是一个有可能包含也有可能不包含 todo、isComplete 熟悉的对象。</p>
<p>接下来我们遍历每一个 todo 元素，就像这样：</p>
<pre><code class="language-javascript">let newTodos = todos.map((t) =&gt; {
  return t.id === params.id ? { ...t, ...updatedData } : t;
});
</code></pre>
<p>其中当&nbsp;<code>params.id</code>&nbsp;和&nbsp;<code>t.id</code>&nbsp;的值一致时，我们将此时的对象的内容重新覆盖为用户传来的想要更改为的内容。</p>
<p>我们也编写成功了这个 API。</p>
<p>让我们重新启动服务器：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts
</code></pre>
<p>在 Postman 中打开一个标签页。将请求方式设置为&nbsp;<code>PUT</code>，并在 <code>URL</code>&nbsp;输入框中输入&nbsp;<code>http://localhost:8080/todos/:id</code> 后，点击 <code>Send</code>：</p>
<p>自从我们使用了随机 ID 生成器，首先我们需要调取获取所有元素的 API。并在元素列表里选取一个 ID 来测试这个新的 API。</p>
<p>每次重启 Deno 程序时，新的 ID 都会被重新生成。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-02.59.39.png" alt="Screenshot-2020-05-29-at-02.59.39" width="600" height="400" loading="lazy"></p>
<p>如上返回了 404 状态码并提示我们没有找到相关的 todo 元素。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-03.00.21.png" alt="Screenshot-2020-05-29-at-03.00.21" width="600" height="400" loading="lazy"></p>
<p>提供一个已知的 ID，并且请求体中填写需要改变的内容。服务端会返回一个更改后的元素及其它所有元素。</p>
<p>酷，四个 API 搞定我们只剩最后一个需要做。</p>
<pre><code class="language-javascript">import { v4 } from "https://deno.land/std/uuid/mod.ts";
// interfaces
import Todo from "../interfaces/Todo.ts";
// stubs
import todos from "../stubs/todos.ts";

export default {
  getAllTodos: () =&gt; {},
  createTodo: async () =&gt; {},
  getTodoById: () =&gt; {},
  updateTodoById: async () =&gt; {},
  /**
   * @description 通过 ID 删除指定 todo
   * @route DELETE todos/:id
   */
  deleteTodoById: (
    { params, response }: { params: { id: string }; response: any },
  ) =&gt; {
    const allTodos = todos.filter((t) =&gt; t.id !== params.id);

    // remove the todo w.r.t id and return
    // remaining todos
    response.status = 200;
    response.body = {
      success: true,
      data: allTodos,
    };
  },
};
</code></pre>
<p>让我们最后来讨论下 <code>Delete todos/:id</code> 控制器的执行过程，此控制器会通过给定的 ID 来删除相应 todo 元素。</p>
<p>我们这里只需简单地加一条过滤方法：</p>
<pre><code>const allTodos = todos.filter((t) =&gt; t.id !== params.id);
</code></pre>
<p>遍历所有元素并删除&nbsp;<code>todo.id</code> 和&nbsp;<code>params.id</code>&nbsp;值一样的元素，并返回其余所有元素。</p>
<p>接下来我们这样编写：</p>
<pre><code class="language-javascript">// 删除这个 todo 并返回其它所有内容
response.status = 200;
response.body = {
  success: true,
  data: allTodos,
};
</code></pre>
<p>只需返回所有没有相同 todo.id 的待办事项清单即可。</p>
<p>让我们重启服务器：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts
</code></pre>
<p>在 Postman 中打开一个标签页。将请求方式设置为 <code>PUT</code>，并在 <code>URL</code> 输入框中输入 <code>http://localhost:8080/todos/:id</code> 后，点击 <code>Send</code>：</p>
<p>自从我们使用了随机 ID 生成器，首先我们需要调取获取所有元素的 API。并在元素列表里选取一个 ID 来测试这个新的 API。每次你重启 Deno 程序时，新的 ID 都会被重新生成。</p>
<p>每次重启 Deno 程序时，新的 ID 都会被重新生成。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-03.07.54.png" alt="Screenshot-2020-05-29-at-03.07.54" width="600" height="400" loading="lazy"></p>
<p>我们终于搞定了所有 5 个 API。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/75bdf06df3fd6ddd9d3311d8cb2be029.jpg#align=left&amp;display=inline&amp;height=400&amp;margin=%5Bobject%20Object%5D&amp;originHeight=400&amp;originWidth=400&amp;status=done&amp;style=none&amp;width=400" alt="75bdf06df3fd6ddd9d3311d8cb2be029" width="600" height="400" loading="lazy"></p>
<p>现在我们只剩下两件事了：</p>
<ul>
<li>增加一个 404 中间件，来让用户访问不存在的路由时得到该有的提示；</li>
<li>增加一个日志 API 来打印所有请求的执行时间。</li>
</ul>
<h2 id="404">创建一个 404 路由中间件</h2>
<p>在项目的根目录中创建一个名为&nbsp;<code>middlewares</code>&nbsp;的文件夹，并在其中创建一个名为 <code>notFound.ts</code> 的文件后，添加如下代码：</p>
<pre><code class="language-javascript">export default ({ response }: { response: any }) =&gt; {
  response.status = 404;
  response.body = {
    success: false,
    message: "404 - Not found.",
  };
};
</code></pre>
<p>如上代码并没有引入什么新的知识点——它对于我们的控制器结构来使用了说很熟悉的风格。这里仅仅返回了&nbsp;<code>404</code>&nbsp;状态码（代表着相关路由没有找到）并且返回了一段 JSON 内容：&nbsp;<code>{ success, message }</code>。</p>
<p>接下来在你的&nbsp;<code>server.ts</code>&nbsp;文件中增加如下内容：</p>
<ul>
<li>在文件顶部添加相关导入语句：</li>
</ul>
<pre><code class="language-javascript">// 没有找到
import notFound from './middlewares/notFound.ts';
</code></pre>
<ul>
<li>接下来在 <code>app.use(todoRouter.allowedMethods())</code>&nbsp;下面增加如下内容：</li>
</ul>
<pre><code class="language-javascript">app.use(todoRouter.routes());
app.use(todoRouter.allowedMethods());

// 404 page
app.use(notFound);
</code></pre>
<p>执行顺序在这里很重要：每当我们尝试访问 API 路由时，它都会首先匹配/检查来自 <code>todoRouter</code>&nbsp;的路由。 如果没有找到，它将执行 <code>app.use(notFound);</code>&nbsp;语句。</p>
<p>让我们看看是否能成功运行。</p>
<p>重启服务器：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts
</code></pre>
<p>在 Postman 中打开一个标签页。将请求方式设置为 <code>PUT</code>，并在 <code>URL</code> 输入框中输入 <code>http://localhost:8080/todos/:id</code> 后，点击 <code>Send</code>：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-12.28.10.png" alt="Screenshot-2020-05-29-at-12.28.10" width="600" height="400" loading="lazy"></p>
<p>因此，我们现在有了一个路由中间件，将 <code>app.use(notFound);</code>&nbsp;放在&nbsp;<code>server.ts</code>&nbsp;文件中其它路由的后面。如果请求路由不存在，它将执行并返回 <code>404</code>&nbsp;状态代码（表示未找到），并像往常一样简单地返回一个响应消息，即 <code>{success, message}</code>。</p>
<p><strong>高级贴士</strong>：我们已经约束 <code>{success, message}</code>&nbsp;是在请求失败时返回的格式，<code>{success, data}</code> 是在请求成功时候返回给用户的格式。因此，我们甚至可以将其作为对象接口，并将其添加到项目中，以确保接口的一致性和进行安全的类型检查。</p>
<p>酷，现在我们已经搞定了其中一个中间件——让我们添加另一个中间件来在终端打印日志吧。</p>
<p><strong>切记</strong>：如果你在某些地方卡住了，可以看看文章的配套源码：<a href="https://github.com/adeelibr/deno-playground">@adeelibr/deno-playground</a>。</p>
<h2 id="">终端中打印日志的中间件</h2>
<p>在你的 <code>middlewares</code> 文件夹中创建一个新的&nbsp;<code>logger.ts</code> 文件并填充如下内容：</p>
<pre><code class="language-javascript">import {
  green,
  cyan,
  white,
  bgRed,
} from "https://deno.land/std@0.53.0/fmt/colors.ts";

const X_RESPONSE_TIME: string = "X-Response-Time";

export default {
  logger: async (
    { response, request }: { response: any, request: any },
    next: Function,
  ) =&gt; {
    await next();
    const responseTime = response.headers.get(X_RESPONSE_TIME);
    console.log(`${green(request.method)} ${cyan(request.url.pathname)}`);
    console.log(`${bgRed(white(String(responseTime)))}`);
  },
  responseTime: async (
    { response }: { response: any },
    next: Function,
  ) =&gt; {
    const start = Date.now();
    await next();
    const ms: number = Date.now() - start;
    response.headers.set(X_RESPONSE_TIME, `${ms}ms`)
  },
};
</code></pre>
<p>在&nbsp;<code>server.ts</code>&nbsp;文件中添加如下代码：</p>
<ul>
<li>文件顶部添加 import 语句来导入模块：</li>
</ul>
<pre><code class="language-javascript">// logger
import logger from './middlewares/logger.ts';
</code></pre>
<ul>
<li>在之前提到的&nbsp;<code>todoRouter</code>&nbsp;代码前这样增加中间件代码：</li>
</ul>
<pre><code class="language-javascript">// 以下代码的编写顺序很重要
app.use(logger.logger);
app.use(logger.responseTime);

app.use(todoRouter.routes());
app.use(todoRouter.allowedMethods());
</code></pre>
<p>现在我们来讨论到底发生了什么。</p>
<p>我们先来讨论 <code>logger.ts</code>&nbsp;文件，先截断看这里：</p>
<pre><code class="language-javascript">import {
  green,
  cyan,
  white,
  bgRed,
} from "https://deno.land/std@0.53.0/fmt/colors.ts";
</code></pre>
<p>我在这里导入了有关终端颜色的模块，想要用在我们的日志中间件上。</p>
<p>这里和我们在之前的&nbsp;<code>server.ts</code> 文件中使用 <code>eventListener</code>&nbsp;的方式很像。我们将使用有颜色的日志信息来记录我们的 API 请求。</p>
<p>接下来我们设置了&nbsp;<code>const X_RESPONSE_TIME: string = "X-Response-Time";</code>。这条语句用来在与用户请求到来时给响应头的 Header 中注入 <code>X_RESPONSE_TIME</code>&nbsp;变量的值：<code>X-Response-Time</code>。我会在后面进行说明。</p>
<p>然后我们像这样一样导出一个对象：</p>
<pre><code class="language-javascript">export default {
  logger: async ({ response, request }, next) {}
  responseTime: async ({ response }, next) {}
};
</code></pre>
<p>此时我们在&nbsp;<code>server.ts</code>&nbsp;中这样使用：</p>
<pre><code class="language-javascript">// 以下两行的编写顺序很重要
app.use(logger.logger);
app.use(logger.responseTime);
</code></pre>
<p>现在我们来讨论下日志中间件到底做了什么，并且通过&nbsp;<code>next()</code> 来说明其执行过程。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-12.51.36.png" alt="Screenshot-2020-05-29-at-12.51.36" width="600" height="400" loading="lazy"></p>
<p>上图为调用 GET / todos API 时日志记录中间件的执行顺序。</p>
<p>这里和以前的控制器唯一的区别是使用了 <code>next()</code>&nbsp;函数，此函数有助于我们从一个控制器跳到另一个控制器，如上图所示。</p>
<p>因此有了这段：</p>
<pre><code class="language-javascript">export default {
  logger: async (
    { response, request }: { response: any, request: any },
    next: Function,
  ) =&gt; {
    await next();
    const responseTime = response.headers.get(X_RESPONSE_TIME);
    console.log(${green(request.method)} ${cyan(request.url.pathname)});
    console.log(${bgRed(white(String(responseTime)))});
  },
  responseTime: async (
    { response }: { response: any },
    next: Function,
  ) =&gt; {
    const start = Date.now();
    await next();
    const ms: number = Date.now() - start;
    response.headers.set(X_RESPONSE_TIME, ${ms}ms)
  },
};
</code></pre>
<p>请留意我们在 <code>server.ts</code> 中的编写方式：</p>
<pre><code class="language-javascript">// 以下代码的编写顺序很重要
app.use(logger.logger);
app.use(logger.responseTime);

app.use(todoRouter.routes());
app.use(todoRouter.allowedMethods());
</code></pre>
<p>这里的执行顺序如下：</p>
<ul>
<li>logger.logger 中间件</li>
<li>logger.responseTime 中间件</li>
<li>todoRouter 控制器（无论用户想要访问什么路由，出于解释的目的，这里假设用户都调用 <code>GET /todos</code>&nbsp;来获取所有待办事项）。</li>
</ul>
<p>因此会先执行&nbsp;logger.logger 的内容：</p>
<pre><code class="language-javascript">logger: async (
    { response, request }: { response: any, request: any },
    next: Function,
  ) =&gt; {
    await next();
    const responseTime = response.headers.get(X_RESPONSE_TIME);
    console.log(${green(request.method)} ${cyan(request.url.pathname)});
    console.log(${bgRed(white(String(responseTime)))});
  },
</code></pre>
<p>当遇到&nbsp;<code>await next()</code> 时会立即跳到下一个中间件——<code>responseTime</code>&nbsp;上。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-12.51.36-1.png" alt="Screenshot-2020-05-29-at-12.51.36-1" width="600" height="400" loading="lazy"></p>
<p>再次分享此图来回顾这个过程。</p>
<p>在&nbsp;<code>responseTime</code>&nbsp;中，只会先执行如下两行（参考上图的执行过程 2）：</p>
<pre><code class="language-javascript">const start = Date.now();
await next();
</code></pre>
<p>然后跳转到&nbsp;<code>getAllTodos</code>&nbsp;控制器中并执行 <code>getAllTodos</code>&nbsp;里的所有代码。</p>
<p>在这个控制器中我们不需要使用 <code>next()</code>，它会自动返回到 <code>responseTime</code>&nbsp;中间件中，并执行接下来的内容：</p>
<pre><code class="language-javascript">const ms: number = Date.now() - start;
response.headers.set(X_RESPONSE_TIME, ${ms}ms)
</code></pre>
<p>现在，我们便了解了 2、3、4 的执行顺序过程（参见上图）。</p>
<p>这里是发生的具体过程：</p>
<ul>
<li>我们通过执行 <code>const start = Date.now();</code>&nbsp;来捕获以 <code>ms</code>&nbsp;为单位的数据。然后，我们立即调用 <code>next()</code> 来跳转到 <code>getAllTodos</code>&nbsp;控制器并运行其中的代码。然后再次返回到 <code>responseTime</code>&nbsp;控制器中。</li>
<li>然后，通过执行 <code>const ms: number = Date.now() - start;</code>&nbsp;来减去请求刚来的时间。在这里，它将返回一个毫秒差的数字，将告诉 Deno 执行 getAllTodos 控制器所花费的所有时间。</li>
</ul>
<p>再次分享这个文件来回顾这个过程：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-12.51.36-2.png" alt="Screenshot-2020-05-29-at-12.51.36-2" width="600" height="400" loading="lazy"></p>
<ul>
<li>接下来我们在 <code>response</code>&nbsp;响应头的 Headers 中设置：</li>
</ul>
<pre><code class="language-javascript">response.headers.set(X_RESPONSE_TIME, ${ms}ms)
</code></pre>
<p>将 X-Response-Time 的值设置为 Deno getAllTodos API 所花费的毫秒数。</p>
<ul>
<li>然后从执行顺序 4 返回到执行顺序 5（参考上图）。</li>
</ul>
<p>在这里简单地编写：</p>
<pre><code class="language-javascript">const responseTime = response.headers.get(X_RESPONSE_TIME);
console.log(${green(request.method)} ${cyan(request.url.pathname)});
console.log(${bgRed(white(String(responseTime)))});
</code></pre>
<ul>
<li>打印日志时我们从&nbsp;<code>X-Response-Time</code>&nbsp;中获取到了执行 API 耗费的时间。</li>
<li>接下来我们用带有颜色的字体将其打印在终端。</li>
</ul>
<p><code>request.method</code>&nbsp;返回用户请求的方式，比如&nbsp;<code>GET, PUT 等</code>，同时&nbsp;<code>request.url.pathname</code>&nbsp;返回用户请求的路径，比如&nbsp;<code>/todos</code>。</p>
<p>让我们看看是否能成功运行。</p>
<p>重启服务器：</p>
<pre><code class="language-bash">$ deno run --allow-net server.ts
</code></pre>
<p>在 Postman 中打开一个标签页。将请求方式设置为 <code>GET</code>，并在 <code>URL</code> 输入框中输入 <code>http://localhost:8080/todos</code>&nbsp;后，点击 <code>Send</code>：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-13.17.13.png" alt="Screenshot-2020-05-29-at-13.17.13" width="600" height="400" loading="lazy"></p>
<p>在 Postman 中多请求几次 API，然后返回到控制台查看日志时，应该看到类似如下的内容：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-29-at-13.21.03.png" alt="Screenshot-2020-05-29-at-13.21.03" width="600" height="400" loading="lazy"></p>
<p>每个 API 请求都会被日志中间件记录在终端。</p>
<p>就是这样 —— 我们搞定了这一切。</p>
<p>如果你在哪里卡住了，可以看看本文的全部源码：<a href="https://github.com/adeelibr/deno-playground/tree/master/chapter_1:oak">github.com/adeelibr/deno-playground/tree/master/chapter_1:oak</a></p>
<p>我希望你觉得本文会很有帮助，并且真的能帮助你学到一些新的知识。</p>
<p>如果你喜欢，欢迎分享到社交平台上。如果你想要深入交流，可以在&nbsp;<a href="https://twitter.com/adeelibr">Twitter</a>&nbsp;上与我联系。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Deno 入门手册：附大量 TypeScript 代码实例 ]]>
                </title>
                <description>
                    <![CDATA[ 我每周都在探索新的项目，很少会有一个像 Deno [https://deno.land/] 这样的项目让我如此着迷。 在本手册中我想要让你快速入手 Deno。我会将其与 Node.js 进行对比，然后助力你在 Deno 上搭建第一个 REST API Demo。 目录  * 什么是 Deno？  * 为什么是 Deno？为什么是现在？  * 你应该学习 Deno 吗？  * Deno 将取代 Node.js 吗？  * 一流的 TypeScript 支持  * 与 Node.js 的异同  * 不再有包管理器  * 安装 Deno ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-deno-handbook-with-examples/</link>
                <guid isPermaLink="false">5ec33d6bdb4be8080eb70b6c</guid>
                
                    <category>
                        <![CDATA[ Deno ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ hylerrix han ]]>
                </dc:creator>
                <pubDate>Tue, 19 May 2020 02:24:01 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-11-at-18.55.24.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>我每周都在探索新的项目，很少会有一个像 <a href="https://deno.land/">Deno</a> 这样的项目让我如此着迷。</p><p>在本手册中我想要让你快速入手 Deno。我会将其与 Node.js 进行对比，然后助力你在 Deno 上搭建第一个 REST API Demo。</p><h2 id="-">目录</h2><ul><li>什么是 Deno？</li><li>为什么是 Deno？为什么是现在？</li><li>你应该学习 Deno 吗？</li><li>Deno 将取代 Node.js 吗？</li><li>一流的 TypeScript 支持</li><li>与 Node.js 的异同</li><li>不再有包管理器</li><li>安装 Deno</li><li>Deno 命令</li><li>你的第一个 Deno 应用</li><li>Deno 代码实例</li><li>你的第一个 Deno 应用（深入版）</li><li>Deno 安全沙箱（Sandbox）</li><li>格式化代码</li><li>标准库</li><li>另一个 Deno 示例</li><li>Deno 是否有 Express/Hapi/Koa/*？</li><li>示例：使用 Oak 构建 REST API</li><li>更多内容</li><li>结语</li></ul><p>此外，<a href="https://flaviocopes.com/page/deno-handbook/">你可以在此处获取此 Deno 手册的 PDF / ePub / Mobi 版本。</a></p><h2 id="-deno-">什么是 Deno？</h2><p>如果你熟悉流行的服务器端 JavaScript 运行时 Node.js，那么 Deno 就像 Node.js 一样，但却在很多方面都得到了深刻改善的全新 JavaScript / TypeScript 运行时。</p><p>让我们从 Deno 的功能列表快速了解：</p><ul><li>Deno 基于最新的 JavaScript 语言；</li><li>Deno 具有覆盖面广泛的标准库；</li><li>Deno 以 TypeScript 为核心，配以更多独特的方式从而带来了巨大的优势，其中包括一流的 TypeScript 支持（Deno 自动编译 TypeScript 而无需你单独编译）；</li><li>Deno 大力拥抱 ES 模块标准；</li><li>Deno 没有包管理器；</li><li>Deno 具有一流的 <code>await</code> 语法支持；</li><li>Deno 内置测试工具；</li><li>Deno 旨在尽可能地与浏览器兼容，例如通过提供内置对象 <code>fetch</code> 和全局 <code>window</code> 对象。</li></ul><p>我们将在本手册中展开探索所有上述功能。</p><p>在你实战完 Deno 并了解它独特的功能魅力后，Node.js 或许会看起来有些过时。</p><p>特别是因为 Node.js 的 API 是基于回调机制的，因为 Node.js 是在 Promise 和 Async / Await 定义在标准之前编写的。Node.js 中无法对此机制进行全新的更改，因为此类更改将产生“毁灭性”的影响。因此，在 Node.js 中我们陷入了回调大量 API 的困境。</p><p>Node.js 的确很棒，并在可见的未来将继续成为 JavaScript 世界中事实上的标准。但我认为我们将逐渐看到 Deno 会因其一流的 TypeScript 支持和其内置的、覆盖面广泛的现代标准库而越来越被重视和采用。</p><p>由于没有向后兼容性的历史原因，Deno 将可以承担起所有使用现代 Web 技术编写的工程建设。但目前的现实是，我们也无法保证十年之内 Deno 不会发生像 Node.js 同样的事情，并且不会出现一项新技术代替 Deno。</p><h2 id="-deno--1">为什么是 Deno？为什么是现在？</h2><p>大约 2 年前，Node.js 的创建者 Ryan Dahl 在 JSConf EU 上首次介绍了 Deno。观看<a href="https://www.youtube.com/watch?v=M3BM9TB-8yA">当时的演讲视频</a>会非常有趣。如果你平时在大量接触 Node.js 和 JavaScript，这个视频请不要错过。</p><p>每个项目经理都必须下发决定。Ryan 回看 Node.js 中的一些早期设计依然感觉十分遗憾。此外，在 ES6/2016/2017 等持续发展中的标准加持下，如今的 JavaScript 与 2009 年 Node.js 创立时的 JavaScript 已经大不相同。</p><p>因此，他开启了一个全新项目，从而创建出服务器端的第二代 JavaScript 运行时。</p><p>新生的技术需要大量时间才能成熟，这正是我现在撰写本手册而不是两年前就开始撰写的原因。如今，第一个正式稳定的 Deno v1.0 版本终于指日可待（不出意外的话，v1.0 会在 2020 年 5 月 13 日发布）。</p><p>（译者注：翻译本手册时 Deno 1.0 已经发布。）</p><p>1.0 看起来仅仅是个数字，但在社区约定下，意味着直到 Deno 2.0 前 Deno 都不会有太多重大的破坏性改变——这很重要，因为你终于可以安心学习 Deno 当前的稳定版本了。</p><h2 id="-deno--2">你应该学习 Deno 吗？</h2><p>这并不那么容易回答。</p><p>学习像 Deno 这样全新的知识需要不少的前期技术沉淀。我的建议是：如果你现在才开始在服务器端使用 JavaScript 编程，并且你还不了解 Node.js，更没有任何 TypeScript 应用开发经验——那么请从 Node.js 学起。</p><p>毕竟用通俗观点来说，没有人会在如今因为选择学习 Node.js 而被解雇。</p><p>但如果你喜欢 TypeScript、也不想让项目中依赖无比庞大的 NPM 软件包、还想要随时随地使用 <code>await</code> 等语法，那么你可能真的需要 Deno。</p><h2 id="deno-node-js-">Deno 将取代 Node.js 吗？</h2><p>不能。Node.js 的生态已经十分庞大和完善，获得了数以万计的优秀技术支持，将能再战数十年。</p><h2 id="-typescript-">一流的 TypeScript 支持</h2><p>Deno 基于 Rust 和 TypeScript 这两种今天正在迅速发展的语言编写。</p><p>这意味着，即使我们可能选择编写纯 JavaScript 代码来运行在基于 TypeScript 语言编写的 Deno 上，我们也可以获得 TypeScript 的很多好处。</p><p>使用 Deno 运行 TypeScript 代码无需任何手动编译——Deno 会自动为你执行此步骤。</p><p>你不必非得在 Deno 上编写 TypeScript 代码，但是 Deno 因其核心由 TypeScript 语言编写的相关背景是不容忽视的：</p><p>首先，越来越多的 JavaScript 程序员开始喜欢上了 TypeScript 语言。</p><p>其次，你使用的工具可以方便地推断出许多有关用 TypeScript 语言编写的软件的信息，例如 Deno。</p><p>因此，当我们在 VS Code（紧密集成 TypeScript 的编辑器）上的编码环节就能及时地体会到类型检查和高级<a href="https://code.visualstudio.com/docs/editor/intellisense">智能感知（IntelliSense）</a>功能带来的好处。换句话说，编辑器可以以非常有用的方式来帮助我们了解 TypeScript 项目。</p><h2 id="-node-js-">与 Node.js 的异同</h2><p>由于 Deno 从某种角度来讲是 Node.js 的替代品，因此直接比较两者的异同对我们的理解会很有帮助。</p><p>相似之处：</p><ul><li>两者都是基于 <a href="https://flaviocopes.com/v8/">V8 引擎</a>开发的；</li><li>两者都非常适合在服务器端上编写 JavaScript 应用。</li></ul><p>差异之处：</p><ul><li>Node.js 用 C++ 和 JavaScript 语言编写。Deno 用 Rust 和 TypeScript 语言编写。</li><li>Node.js 有一个官方的软件包管理器，称为 NPM。Deno 不会有，而会允许你从 URL 导入任何 ES 模块。</li><li>Node.js 使用 CommonJS 模块语法导入软件包。Deno 使用 ES 标准模块导入。</li><li>Deno 在其所有 API 和标准库中都使用现代 ECMAScript 功能，而 Node.js 使用基于回调的标准库，并且没有计划对其进行升级。</li><li>Deno 通过权限控制提供了一个安全的沙箱环境，程序只能访问由用户设置为可执行标志的文件。Node.js 程序可以直接访问用户足以访问的任何内容。</li><li>Deno 长期以来一直在探索将程序编译成单个可执行文件的可能性，从而使得该可执行文件可以在没有外部依赖项（例如 Go）的情况下运行，但这并<a href="https://github.com/denoland/deno/issues/986">不是一件容易的事</a>，如果做得到，将会成为更有话语权的游戏规则改变者。</li></ul><h2 id="--1">没有包依赖管理器</h2><p>没有像 NPM 一样的程序包管理器并且大量依靠 URL 来承载和导入程序包是有利有弊的。但我真的很喜欢这个特性：它将会非常灵活，我们可以直接创建软件包而无需在 NPM 这样的存储库中发布它们。</p><p>虽然还没有官方的消息，但我认为 Deno 下的某种软件包管理器将会出现。</p><p>与此同时，Deno 网站为第三方软件包提供代码托管服务（并帮助其通过 URL 分发）：详见 <a href="https://deno.land/x/">https://deno.land/x/</a>。</p><h2 id="install-deno">Install Deno</h2><p>就闲聊到这里吧！让我们开始着手安装 Deno。</p><p>最简单的方法是使用 <a href="https://flaviocopes.com/homebrew/">Homebrew</a>：</p><pre><code class="language-bash">brew install deno
</code></pre><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-12.04.45.jpg" class="kg-image" alt="brew install" width="600" height="400" loading="lazy"></figure><p>输出如上命令后，你将可以访问 <code>deno</code> 命令。帮助是<code>deno --help</code>：</p><p>（译者注：如果 HomeBrew 安装太慢可以尝试输入如下命令手动关闭 HomeBrew 的自动更新检测： <code>export HOMEBREW_NO_AUTO_UPDATE=true</code>。）</p><pre><code class="language-bash">flavio@mbp~&gt; deno --help
deno 0.42.0
A secure JavaScript and TypeScript runtime

Docs: https://deno.land/std/manual.md
Modules: https://deno.land/std/ https://deno.land/x/
Bugs: https://github.com/denoland/deno/issues
To start the REPL, supply no arguments:
  deno
To execute a script:
  deno run https://deno.land/std/examples/welcome.ts
  deno https://deno.land/std/examples/welcome.ts
To evaluate code in the shell:
  deno eval "console.log(30933 + 404)"
Run 'deno help run' for 'run'-specific flags.
USAGE:
    deno [OPTIONS] [SUBCOMMAND]
OPTIONS:
    -h, --help
            Prints help information
-L, --log-level &amp;lt;log-level&amp;gt;
        Set log level [possible values: debug, info]

-q, --quiet
        Suppress diagnostic output
        By default, subcommands print human-readable diagnostic messages to stderr.
        If the flag is set, restrict these messages to errors.
-V, --version
        Prints version informationSUBCOMMANDS:
    bundle         Bundle module and dependencies into single file
    cache          Cache the dependencies
    completions    Generate shell completions
    doc            Show documentation for a module
    eval           Eval script
    fmt            Format source files
    help           Prints this message or the help of the given subcommand(s)
    info           Show info about cache or info related to source file
    install        Install script as an executable
    repl           Read Eval Print Loop
    run            Run a program given a filename or url to the module
    test           Run tests
    types          Print runtime TypeScript declarations
    upgrade        Upgrade deno executable to newest version

</code></pre><h2 id="deno-">Deno 命令</h2><p>请注意上节中 <code>deno --help</code> 后 <code>SUBCOMMANDS</code> 中的部分，其中列出了我们在当前版本（0.42.0）中可以运行的所有命令，如下：</p><ul><li><code>bundle</code> ：将项目的模块和依赖项捆绑到单个文件中；</li><li><code>cache</code> ：缓存依赖项；</li><li><code>completions</code> ：generate shell completions；</li><li><code>doc</code> ：显示某模块的文档；</li><li><code>eval</code> ：运行一段代码，例如 <code>deno eval "console.log(1 + 2)</code>；</li><li><code>fmt</code> ：内置的代码格式化程序（类似于 Go 语言中的 <code>gofmt</code>）；</li><li><code>help</code> ：打印某消息或某给定子命令的帮助信息；</li><li><code>info</code> ：显示有关缓存的信息或与源文件有关的信息；</li><li><code>install</code> ：将脚本安装为可执行文件；</li><li><code>repl</code> ：开启 REPL 环境（默认子命令）；</li><li><code>run</code> ：运行给定文件名或 URL 的程序；</li><li><code>test</code> ：运行测试；</li><li><code>types</code> ：打印运行时的 TypeScript 声明；</li><li><code>upgrade</code> ：升级 Deno 到最新版本。</li></ul><p>你可以运行 <code>deno &lt;subcommand&gt; help</code> 以获取该子命令的特定文档，例如 <code>deno run --help</code>。</p><p>如下所示，我们可以直接输入 <code>deno</code> 命令命令来默认启动 REPL（Read-Execute-Print-Loop）环境直接调试功能，这与运行 <code>deno repl</code> 效果是相同的。</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-12.07.50.png" class="kg-image" alt="Read-Execute-Print-Loop" width="600" height="400" loading="lazy"></figure><p>一个更常见的直接使用 <code>deno</code> 命令的场景是执行在 TypeScript 文件中写的 Deno 应用程序。</p><p>（译者注：现在需要使用 <code>deno run</code> 命令而非 <code>deno</code> 命令来执行 TypeScript 文件。）</p><p>你可以同时运行 TypeScript（.ts）文件或 JavaScript（.js）文件。</p><p>如果你不熟悉 TypeScript，请不要担心——Deno 是用 TypeScript 编写的，并且你可以使用纯 JavaScript 编写“客户端”应用程序。</p><p>如果你想快速上手的 TypeScript 话，可以阅读我的 <a href="https://flaviocopes.com/typescript/">TypeScript 教程</a>。</p><h2 id="-deno--3">你的第一个 Deno 应用</h2><p>让我们来运行第一个 Deno 应用程序。</p><p>Deno 让我感到非常惊奇的特性是：你甚至不必写一行代码，便可以直接运行任何 URL 上的 Deno 应用程序。</p><p>此时 Deno 会将 URL 上的程序下载到本地并进行编译，然后运行：</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-12.22.30.jpg" class="kg-image" alt="Deno" width="600" height="400" loading="lazy"></figure><p><strong>当然，我一般不建议从 Internet 运行无法保障安全性的代码。在这种情况下，我们先运行 Deno 官方网站上提供的 Demo；另外 Deno 还有一个沙箱，可以阻止程序执行你不希望做的事情。稍后再详细介绍。</strong></p><p>这个程序很简单，只需要一个<code>console.log()</code>调用：</p><pre><code class="language-ts">console.log("Welcome to Deno 🦕");
</code></pre><p>如果使用浏览器打开直接打开 <a href="https://deno.land/std/examples/welcome.ts">https://deno.land/std/examples/welcome.ts</a> 这个 URL，则会看到以下页面：</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-13.50.00.png" class="kg-image" alt="welcome.ts" width="600" height="400" loading="lazy"></figure><p>奇怪吧？你可能期待着打开 URL 后出现一个纯 TypeScript 文件以供下载，但是我们却看到了一个网页。原因是 Deno 网站的 Web 服务器知道你正在使用浏览器，并为你提供了对用户更加友好的页面。</p><p>为了验证这个功能，我们可以使用 <code>wget</code> 命令来测试这个 URL，<code>wget</code> 使用 <code>text/plain</code> 下载文本而不是 <code>text/html</code>：</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-13.52.25.png" class="kg-image" alt="wget" width="600" height="400" loading="lazy"></figure><p>如果你想再运行这个程序，现在已经被 Deno 缓存了，不需要再下载和编译了。</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-12.22.47.jpg" class="kg-image" alt="Deno" width="600" height="400" loading="lazy"></figure><p>你可以用 <code>--reload</code> 参数强制重新下载和编译原始源码。</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-12.28.57.jpg" class="kg-image" alt="Screen-Shot-2020-05-09-at-12.28.57" width="600" height="400" loading="lazy"></figure><p>在当前版本（0.42.0）中，<code>deno run</code> 有许多未在 <code>deno --help</code> 清单中列出的功能。你需要运行 <code>deno run --help</code> 以显示更多。</p><pre><code class="language-bash">flavio@mbp~&gt; deno run --help
deno-run
Run a program given a filename or url to the module.
By default all programs are run in sandbox without access to disk, network or
ability to spawn subprocesses.
  deno run https://deno.land/std/examples/welcome.ts
Grant all permissions:
  deno run -A https://deno.land/std/http/file_server.ts
Grant permission to read from disk and listen to network:
  deno run --allow-read --allow-net https://deno.land/std/http/file_server.ts
Grant permission to read whitelisted files from disk:
  deno run --allow-read=/etc https://deno.land/std/http/file_server.ts
USAGE:
    deno run [OPTIONS] &lt;SCRIPT_ARG&gt;...
OPTIONS:
    -A, --allow-all
            Allow all permissions
    --allow-env
        Allow environment access

    --allow-hrtime
        Allow high resolution time measurement

    --allow-net=&amp;lt;allow-net&amp;gt;
        Allow network access

    --allow-plugin
        Allow loading plugins

    --allow-read=&amp;lt;allow-read&amp;gt;
        Allow file system read access

    --allow-run
        Allow running subprocesses

    --allow-write=&amp;lt;allow-write&amp;gt;
        Allow file system write access

    --cached-only
        Require that remote dependencies are already cached

    --cert &amp;lt;FILE&amp;gt;
        Load certificate authority from PEM encoded file

-c, --config &amp;lt;FILE&amp;gt;
        Load tsconfig.json configuration file

-h, --help
        Prints help information

    --importmap &amp;lt;FILE&amp;gt;
        UNSTABLE:
        Load import map file
        Docs: https://deno.land/std/manual.md#import-maps
        Specification: https://wicg.github.io/import-maps/
        Examples: https://github.com/WICG/import-maps#the-import-map
    --inspect=&amp;lt;HOST:PORT&amp;gt;
        activate inspector on host:port (default: 127.0.0.1:9229)

    --inspect-brk=&amp;lt;HOST:PORT&amp;gt;
        activate inspector on host:port and break at start of user script

    --lock &amp;lt;FILE&amp;gt;
        Check the specified lock file

    --lock-write
        Write lock file. Use with --lock.

-L, --log-level &amp;lt;log-level&amp;gt;
        Set log level [possible values: debug, info]

    --no-remote
        Do not resolve remote modules

-q, --quiet
        Suppress diagnostic output
        By default, subcommands print human-readable diagnostic messages to stderr.
        If the flag is set, restrict these messages to errors.
-r, --reload=&amp;lt;CACHE_BLACKLIST&amp;gt;
        Reload source code cache (recompile TypeScript)
        --reload
          Reload everything
        --reload=https://deno.land/std
          Reload only standard modules
        --reload=https://deno.land/std/fs/utils.ts,https://deno.land/std/fmt/colors.ts
          Reloads specific modules
    --seed &amp;lt;NUMBER&amp;gt;
        Seed Math.random()

    --unstable
        Enable unstable APIs

    --v8-flags=&amp;lt;v8-flags&amp;gt;
        Set V8 command line options. For help: --v8-flags=--help
</code></pre><h2 id="deno--1">Deno 代码实例</h2><p>除了前文我们运行的 Demo 外，Deno 官网还提供了一些其他的例子，可以在这里查看：<a href="https://deno.land/std/examples/">https://deno.land/std/examples/</a>。</p><p>（译者注：你可能需要配置代理来更好地访问 DenoLand。）</p><p>在撰写本手册时，我们可以找到：</p><ul><li><code>cat.ts</code> ：打印的内容是作为参数提供的文件列表；</li><li><code>catj.ts</code> ：打印的内容是作为参数提供的文件列表；</li><li><code>chat/</code> ：聊天的一种实现；</li><li><code>colors.ts</code> ：打印一个彩色版本的 Hello world!；</li><li><code>curl.ts</code> ：一个简单的实现，curl 它打印指定为参数的 URL 的内容；</li><li><code>echo_server.ts</code> ：TCP 回显服务器；</li><li><code>gist.ts</code> ：一个将文件发布到 gist.github.com 的程序；</li><li><code>test.ts</code> ：样本测试套件；</li><li><code>welcome.ts</code> ：一个简单的 console.log 语句（我们在上面运行的第一个程序）；</li><li><code>xeval.ts</code> ：允许你为收到的任何标准输入行运行任何 TypeScript 代码。曾经被设计为 <code>deno xeval</code> 子命令但现在从官方命令中删除。</li></ul><h2 id="-deno--4">你的第一个 Deno 应用（深入版）</h2><p>我们来写一些代码吧。</p><p>前文执行的 <code>deno run https://deno.land/std/examples/welcome.ts</code> 命令执行的是官网提供的一个 Deno 应用，所以我们没有看到任何关于 Deno 代码具体的样子。</p><p>接下来让我们从 Deno 官方网站上列出的默认示例应用开始。</p><pre><code class="language-ts">import { serve } from "https://deno.land/std/http/server.ts";
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}
</code></pre><p>这段代码从 <code>http/server</code> 模块中导入服务函数。可见我们不需要先安装这些模块，而且也不会像 Node.js 那样将这些模块大量存储在本地机器上。这也是 Deno 安装速度快的原因之一。</p><p>从 <code>https://deno.land/std/http/server.ts</code> 中导入会导入最新版本的模块。你可以使用<code>@VERSION</code>导入特定的版本，如下所示。</p><pre><code class="language-ts">import { serve } from "https://deno.land/std@v0.42.0/http/server.ts";
</code></pre><p>该 serve 函数在此文件中的定义如下：</p><pre><code class="language-ts">/**
 * Create a HTTP server
 *
 *     import { serve } from "https://deno.land/std/http/server.ts";
 *     const body = "Hello World\n";
 *     const s = serve({ port: 8000 });
 *     for await (const req of s) {
 *       req.respond({ body });
 *     }
 */
export function serve(addr: string | HTTPOptions): Server {
  if (typeof addr === "string") {
    const [hostname, port] = addr.split(":");
    addr = { hostname, port: Number(port) };
  }

  const listener = listen(addr);
  return new Server(listener);
}
</code></pre><p>我们接下来实例化一个服务器，调用 <code>server()</code> 函数传递一个带有端口属性的对象。</p><p>然后我们运行如下循环来响应来自服务器的每一个请求。</p><pre><code class="language-ts">for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}
</code></pre><p>请注意，我们在这里使用 <code>await</code> 关键字而不需要将其封装到异步函数中，因为 Deno 在其内部实现了顶层的 <code>await</code> 支持。</p><p>让我们在本地运行这个程序。假设你使用的是 VS Code（你可以使用任何你喜欢的编辑器），我建议从 <code>justjavac</code> 开发的 Deno VS Code 扩展入手（当我尝试的时候还有一个同名的扩展，但是已经被淘汰了，可能将来会消失）。</p><p>（译者注：justjavac 的 Deno VS Code 拓展将被官方收录，以后可以直接使用官方的拓展。）</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-15.28.06.png" class="kg-image" alt="justjavac" width="600" height="400" loading="lazy"></figure><p>该扩展将为 <code>VS Code</code> 提供几个实用工具和不错的东西来帮助你编写应用程序。</p><p>现在在一个文件夹中创建一个<code>app.ts</code> 文件，然后粘贴上面的代码。</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-15.40.18.png" class="kg-image" alt="app.ts" width="600" height="400" loading="lazy"></figure><p>现在用 <code>deno run app.ts</code> 运行它。</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-15.39.28.jpg" class="kg-image" alt="app.ts" width="600" height="400" loading="lazy"></figure><p>Deno 会先下载、编译我们导入的那个依赖及其所有需要的依赖项。</p><p>这是由于我们导入的 <code>https://deno.land/std/http/server.ts</code> 文件本身就有数个其它依赖：</p><pre><code class="language-ts">import { encode } from '../encoding/utf8.ts'
import { BufReader, BufWriter } from '../io/bufio.ts'
import { assert } from '../testing/asserts.ts'
import { deferred, Deferred, MuxAsyncIterator } from '../async/mod.ts'
import {
bodyReader,
chunkedBodyReader,
emptyReader,
writeResponse,
readRequest,
} from './_io.ts'
import Listener = Deno.Listener
import Conn = Deno.Conn
import Reader = Deno.Reader

</code></pre><p>但 Deno 都会帮我们自动导入。</p><p>在最后，我们还有一个问题。</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-15.42.05.png" class="kg-image" alt="app.ts" width="600" height="400" loading="lazy"></figure><p>这是怎么回事？我们为什么会收到执行权限被拒绝的提示？</p><p>这就涉及到了 Deno 的 Sandbox 问题，我们一起来看看。</p><h2 id="deno-sandbox-">Deno 安全沙箱（Sandbox）</h2><p>我之前提到过，Deno 有一个安全沙箱，可以防止程序做一些你不允许的事情。</p><p>这意味着什么呢？</p><p>Ryan 曾在 Deno 的介绍讲座中提到的一件事是：有时候你想在 Web 浏览器之外运行一个 JavaScript 程序，却不想让它肆意在你的系统中访问任何它想要的东西，比如使用网络与外部世界对话。</p><p>为什么我们通常只安装来自可信来源的 Node.js 包？这是因为没有什么可以阻止 Node.js 程序获取你系统上的 SSH 密钥或其他任何东西，并将其发送到服务器上。但是，我们该怎么知道自己或其他人使用的一个项目是否被黑客入侵了？</p><p>Deno 的解决方案是试图大量借鉴浏览器实现相同的权限模型——除非你明确允许，否则在浏览器中运行的任何 JavaScript 都不能在你的系统上做不正当的事情。</p><p>回到 Deno，如果一个程序想要像前面的例子一样访问网络，那么我们需要给它权限。</p><p>我们可以通过在运行命令时传递一个标志来实现，本例中是 <code>--allow-net</code>。</p><pre><code class="language-bash">deno run --allow-net app.ts
</code></pre><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-15.48.41.png" class="kg-image" alt="allow-net" width="600" height="400" loading="lazy"></figure><p>该应用程序现在监听在 8000 端口上运行着 HTTP 服务器：</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-15.49.02.png" class="kg-image" alt="allow-net" width="600" height="400" loading="lazy"></figure><p>其他标志允许 Deno 解锁其他功能，如下所示：</p><ul><li><code>--allow-env</code> ：允许访问环境变量；</li><li><code>--allow-hrtime</code> ：允许高分辨率时间测量；</li><li><code>--allow-net=&lt;allow-net&gt;</code> ：允许网络访问；</li><li><code>--allow-plugin</code> ：允许加载插件；</li><li><code>--allow-read=&lt;allow-read&gt;</code> ：允许文件系统读取权限；</li><li><code>--allow-run</code> ：允许运行子进程；</li><li><code>--allow-write=&lt;allow-write&gt;</code> ：允许文件系统写入访问；</li><li><code>--allow-all</code> ：允许所有权限(与<code>-A</code>相同)。</li></ul><p>其中，<code>net</code>、<code>read</code> 和 <code>write</code> 的权限可以是细化的。例如，你可以使用 <code>--allow-read=/dev</code>，允许从特定文件夹中读取。</p><h2 id="--2">格式化代码</h2><p>Go 语言编译器自带的 <code>gofmt</code> 命令是我非常喜欢 Go 语言特性之一。所有的 Go 代码的格式看起来都是一样的。每位 Go 程序员都在使用 <code>gofmt</code>。</p><p>JavaScript 程序员都习惯于运行 <a href="https://flaviocopes.com/prettier/">Prettier</a> 工具，而 <code>deno fmt</code> 实际上直接内置相关库到底层上运行。</p><p>假设你有一个格式化问题严重的文件如下图所示。</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-16.06.58.png" class="kg-image" alt="deno fmt" width="600" height="400" loading="lazy"></figure><p>你运行 <code>deno fmt app.ts</code>，它就会执行正确的代码格式化，包括自动加上缺失的分号。</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-16.07.25.png" class="kg-image" alt="deno fmt" width="600" height="400" loading="lazy"></figure><h2 id="--3">标准库</h2><p>尽管 Deno 还很年轻，但它的标准库仍然很庞大。这包括：</p><ul><li><code>archive</code> ：tar 文件归档的实用程序</li><li><code>async</code> ：异步工具</li><li><code>bytes</code> ：帮助器来操作字节切片</li><li><code>datetime</code> ：日期 / 时间解析</li><li><code>encoding</code> ：各种格式的编码/解码</li><li><code>flags</code> ：解析命令行标志</li><li><code>fmt</code> ：格式化和打印</li><li><code>fs</code> ：文件系统 API</li><li><code>hash</code> ：加密库</li><li><code>http</code> ：HTTP 服务器</li><li><code>io</code> ：I/O 库</li><li><code>log</code> ：日志实用程序</li><li><code>mime</code> ：支持多类型数据</li><li><code>node</code> ：Node.js 兼容层</li><li><code>path</code> ：路径操纵</li><li><code>ws</code> ：WebSockets</li></ul><h2 id="-deno--5">另一个 Deno 示例</h2><p>我们再来看看另一个 Deno APP 的例子，以如下 <code>cat.ts</code> 为例。</p><pre><code class="language-ts">const filenames = Deno.args;
for (const filename of filenames) {
  const file = await Deno.open(filename);
  await Deno.copy(file, Deno.stdout);
  file.close();
}
</code></pre><p>这里把 <code>Deno.args</code> 的值分配给了 filenames 变量，<code>Deno.args</code> 是一个包含所有发送到命令中的参数的变量。</p><p>我们对这些参数进行迭代：对每一个参数，我们使用 <code>Deno.open()</code> 打开文件，并使用 <code>Deno.copy()</code> 将文件的内容打印到 <code>Deno.stdout</code> 中，最后我们关闭该文件。</p><p>如果你使用如下命令：</p><pre><code class="language-bash">deno run https://deno.land/std/examples/cat.ts
</code></pre><p>程序被下载编译后，由于我们没有指定任何参数，所以没有发生任何事情。</p><p>现在试试这个：</p><pre><code class="language-bash">deno run https://deno.land/std/examples/cat.ts app.ts
</code></pre><p>假设你在同一个文件夹里有之前项目中的 <code>app.ts</code>。</p><p>你会得到如下权限错误。</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-17.06.31-1.png" class="kg-image" alt="app.ts" width="600" height="400" loading="lazy"></figure><p>这是因为 Deno 默认情况下不允许访问文件系统。需要使用 <code>--allow-read=./</code> 命令授予对当前文件夹的访问权限：</p><pre><code class="language-bash">deno run --allow-read=./ https://deno.land/std/examples/cat.ts app.ts
</code></pre><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-09-at-17.07.54-6.png" class="kg-image" alt="allow-read" width="600" height="400" loading="lazy"></figure><h2 id="deno-express-hapi-koa-">Deno 是否有 Express/Hapi/Koa/*</h2><p>当然有。可以看看下方这些库。</p><ul><li><a href="https://github.com/drashland/deno-drash">deno-drash</a></li><li><a href="https://github.com/NMathar/deno-express">deno-express</a></li><li><a href="https://github.com/oakserver/oak">oak</a></li><li><a href="https://github.com/sholladay/pogo">pogo</a></li><li><a href="https://github.com/keroxp/servest">servest</a></li></ul><h2 id="-oak-rest-api">示例：使用 Oak 构建 REST-API</h2><p>我想在这里做一个简单的 Demo 实战，介绍一下如何使用 Oak 框架构建<code>REST API</code>。Oak 很有意思，因为它的灵感来自于 Koa，一个流行的 Node.js 中间件。正因为如此，如果你以前用过 Koa 的话，会很快熟悉 Oak。</p><p>我们要构建的 API 示例也非常简单。</p><p>我们的服务器将在内存中存储一个带有名字和年龄的旺柴的列表。</p><p>我们的需求是：</p><ul><li>添加旺柴；</li><li>列出旺柴；</li><li>获取有关特定旺柴的详细信息；</li><li>从名单上删除一只旺柴；</li><li>更新旺柴的年龄；</li></ul><p>我们将使用 TypeScript 进行此操作，但是没有什么可以阻止你使用 JavaScript 编写 API——你只需要删除下方 TypeScript 文件中所有有关类型描述的代码并将文件名后缀改为 <code>.js</code>。</p><p>创建一个 <code>app.ts</code> 文件。</p><p>让我们开始从 Oak 导入 <code>Application</code> 和 <code>Router</code> 对象：</p><pre><code class="language-ts">import { Application, Router } from "https://deno.land/x/oak/mod.ts";
</code></pre><p>然后我们得到环境变量 <code>PORT</code> 和 <code>HOST</code>:</p><pre><code class="language-ts">const env = Deno.env.toObject();
const PORT = env.PORT || 4000;
const HOST = env.HOST || "127.0.0.1";
</code></pre><p>默认情况下，我们的应用程序将在 <code>localhost：4000</code> 上运行。</p><p>现在，我们创建 <code>Oak</code> 应用程序并启动它：</p><pre><code class="language-ts">const router = new Router();

const app = new Application();

app.use(router.routes());
app.use(router.allowedMethods());

console.log(`Listening on port ${PORT}...`);

await app.listen(`${HOST}:${PORT}`);
</code></pre><p>现在，应用程序应该可以正常编译了。</p><pre><code class="language-bash">deno run --allow-env --allow-net app.ts
</code></pre><p>然后 Deno 将下载依赖项：</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-10-at-16.31.11.jpg" class="kg-image" alt="Deno" width="600" height="400" loading="lazy"></figure><p>这时程序监听在 <code>4000</code> 端口上。</p><p>下次运行该命令时，Deno 会跳过安装部分，因为这些包已经被缓存了。</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-10-at-16.32.40.png" class="kg-image" alt="Deno" width="600" height="400" loading="lazy"></figure><p>在文件的顶部，让我们定义一个旺柴的接口，然后我们声明一个初始的 <code>Dogs</code> 数组 <code>Dog</code> 对象。</p><pre><code class="language-ts">interface Dog {
  name: string;
  age: number;
}

let dogs: Array&lt;Dog&gt; = [
  {
    name: "Roger",
    age: 8,
  },
  {
    name: "Syd",
    age: 7,
  },
];
</code></pre><p>现在，让我们来实现具体 API。</p><p>我们已经准备好了一切。在你创建了路由器之后，让我们添加一些函数，这些函数将在任何时候触发这些路由中的一个端点时被调用。</p><pre><code class="language-ts">const router = new Router();

router
  .get("/dogs", getDogs)
  .get("/dogs/:name", getDog)
  .post("/dogs", addDog)
  .put("/dogs/:name", updateDog)
  .delete("/dogs/:name", removeDog);
</code></pre><p>看到了吗？我们的 API 定义是：</p><ul><li><code>GET /dogs</code></li><li><code>GET /dogs/:name</code></li><li><code>POST /dogs</code></li><li><code>PUT /dogs/:name</code></li><li><code>DELETE /dogs/:name</code></li></ul><p>让我们开始一一实现。</p><p>从开始 <code>GET /dogs</code>，它将返回所有旺柴的列表：</p><pre><code class="language-ts">export const getDogs = ({ response }: { response: any }) =&gt; {
  response.body = dogs;
};
</code></pre><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-10-at-16.47.41.png" class="kg-image" alt="getDogs" width="600" height="400" loading="lazy"></figure><p>接下来，我们就来看看如何通过名字来检索旺柴。</p><pre><code class="language-ts">export const getDog = ({
  params,
  response,
}: {
  params: {
    name: string;
  };
  response: any;
}) =&gt; {
  const dog = dogs.filter((dog) =&gt; dog.name === params.name);
  if (dog.length) {
    response.status = 200;
    response.body = dog[0];
    return;
  }

  response.status = 400;
  response.body = { msg: `Cannot find dog ${params.name}` };
};
</code></pre><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-10-at-16.47.53.png" class="kg-image" alt="getDog" width="600" height="400" loading="lazy"></figure><p>这是我们添加一个新的旺柴的方法：</p><pre><code class="language-ts">export const addDog = async ({
  request,
  response,
}: {
  request: any;
  response: any;
}) =&gt; {
  const body = await request.body();
  const dog: Dog = body.value;
  dogs.push(dog);

  response.body = { msg: "OK" };
  response.status = 200;
};
</code></pre><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-10-at-16.48.02.png" class="kg-image" alt="addDog" width="600" height="400" loading="lazy"></figure><p>注意，我现在使用 <code>const body = await request.body()</code> 来获取正文的内容，因为 <code>name</code> 和 <code>age</code> 值是以 JSON 的形式传递的。</p><p>这是我们更新旺柴的年龄的方法：</p><pre><code class="language-ts">export const updateDog = async ({
  params,
  request,
  response,
}: {
  params: {
    name: string;
  };
  request: any;
  response: any;
}) =&gt; {
  const temp = dogs.filter((existingDog) =&gt; existingDog.name === params.name);
  const body = await request.body();
  const { age }: { age: number } = body.value;

  if (temp.length) {
    temp[0].age = age;
    response.status = 200;
    response.body = { msg: "OK" };
    return;
  }

  response.status = 400;
  response.body = { msg: `Cannot find dog ${params.name}` };
};
</code></pre><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-10-at-16.48.11.png" class="kg-image" alt="updateDog" width="600" height="400" loading="lazy"></figure><p>这是我们如何从列表中删除旺柴的方法：</p><pre><code class="language-ts">export const removeDog = ({
  params,
  response,
}: {
  params: {
    name: string;
  };
  response: any;
}) =&gt; {
  const lengthBefore = dogs.length;
  dogs = dogs.filter((dog) =&gt; dog.name !== params.name);

  if (dogs.length === lengthBefore) {
    response.status = 400;
    response.body = { msg: `Cannot find dog ${params.name}` };
    return;
  }

  response.body = { msg: "OK" };
  response.status = 200;
};
</code></pre><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screen-Shot-2020-05-10-at-16.48.32.png" class="kg-image" alt="removeDog" width="600" height="400" loading="lazy"></figure><p>这是完整的示例代码：</p><pre><code class="language-ts">import { Application, Router } from "https://deno.land/x/oak/mod.ts";

const env = Deno.env.toObject();
const PORT = env.PORT || 4000;
const HOST = env.HOST || "127.0.0.1";

interface Dog {
  name: string;
  age: number;
}

let dogs: Array&lt;Dog&gt; = [
  {
    name: "Roger",
    age: 8,
  },
  {
    name: "Syd",
    age: 7,
  },
];

export const getDogs = ({ response }: { response: any }) =&gt; {
  response.body = dogs;
};

export const getDog = ({
  params,
  response,
}: {
  params: {
    name: string;
  };
  response: any;
}) =&gt; {
  const dog = dogs.filter((dog) =&gt; dog.name === params.name);
  if (dog.length) {
    response.status = 200;
    response.body = dog[0];
    return;
  }

  response.status = 400;
  response.body = { msg: `Cannot find dog ${params.name}` };
};

export const addDog = async ({
  request,
  response,
}: {
  request: any;
  response: any;
}) =&gt; {
  const body = await request.body();
  const { name, age }: { name: string; age: number } = body.value;
  dogs.push({
    name: name,
    age: age,
  });

  response.body = { msg: "OK" };
  response.status = 200;
};

export const updateDog = async ({
  params,
  request,
  response,
}: {
  params: {
    name: string;
  };
  request: any;
  response: any;
}) =&gt; {
  const temp = dogs.filter((existingDog) =&gt; existingDog.name === params.name);
  const body = await request.body();
  const { age }: { age: number } = body.value;

  if (temp.length) {
    temp[0].age = age;
    response.status = 200;
    response.body = { msg: "OK" };
    return;
  }

  response.status = 400;
  response.body = { msg: `Cannot find dog ${params.name}` };
};

export const removeDog = ({
  params,
  response,
}: {
  params: {
    name: string;
  };
  response: any;
}) =&gt; {
  const lengthBefore = dogs.length;
  dogs = dogs.filter((dog) =&gt; dog.name !== params.name);

  if (dogs.length === lengthBefore) {
    response.status = 400;
    response.body = { msg: `Cannot find dog ${params.name}` };
    return;
  }

  response.body = { msg: "OK" };
  response.status = 200;
};

const router = new Router();
router
  .get("/dogs", getDogs)
  .get("/dogs/:name", getDog)
  .post("/dogs", addDog)
  .put("/dogs/:name", updateDog)
  .delete("/dogs/:name", removeDog);

const app = new Application();

app.use(router.routes());
app.use(router.allowedMethods());

console.log(`Listening on port ${PORT}...`);

await app.listen(`${HOST}:${PORT}`);
</code></pre><h2 id="--4">更多内容</h2><p>Deno 官方网站为 <a href="https://deno.land">https://deno.land</a>。</p><p>API 文档位于 <a href="https://doc.deno.land">https://doc.deno.land</a> 和 <a href="https://deno.land/typedoc/index.html">https://deno.land/typedoc/index.html</a> 中。</p><p>一份 Awesome Deno 资源清单 <a href="https://github.com/denolib/awesome-deno">https://github.com/denolib/awesome-deno</a>。</p><p>（译者注：中文的 Awesome Deno 清单由译者持续维护中，可以访问这里：<a href="https://github.com/hylerrix/awesome-deno-cn">Awesome Deno 资源全图谱</a>。）</p><h2 id="--5">花絮</h2><ul><li>Deno 提供了一个内置的 <code>fetch</code> 实现，该实现与浏览器中可用的匹配</li><li>Deno 正在进行与 Node.js stdlib 的兼容层</li></ul><h2 id="--6">结语</h2><p>我希望你喜欢这个 Deno 入门手册！</p><p>别忘了，<a href="https://flaviocopes.com/page/deno-handbook/">你可以在此处获取此 Deno 手册的 PDF / ePub / Mobi 版本。</a></p><p>原文：<a href="https://www.freecodecamp.org/news/the-deno-handbook/">The Deno Handbook: A TypeScript Runtime Tutorial with Code Examples</a>，作者：Flavio Copes，译者：hylerrix, Yunkou</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 凝果开源 | 一副“程序员扑克牌”的故事 ]]>
                </title>
                <description>
                    <![CDATA[ 2019 年 4 月 25 日，Github Star 第一的开源项目 freeCodeCamp 创始人 Qunicy 发表了一篇文章， 《Introducing Programmer Playing Cards》 [https://www.freecodecamp.org/news/programmer-playing-cards/] ，介绍了一副可以边玩边了解程序员历史的扑克牌。几个月后，因着自己对 FCC 中文社区的持续投入，收到了其中的两份卡牌，也塑造了这篇文章、以及这个开源项目的灵感起源。 整副卡牌共 54 张扑克，包含大小王和四种花色的各 13 张牌。卡牌全部由 100% PVC 材质打造，适度弯曲、浸水以及打火机烘烤都不会影响卡片的耐久度；卡牌包含 54 位程序员先锋的故事，每一张卡片内含一个程序员的生活照片、主要成就清单，以及摘自 TA 的名言——正是这些先锋们开创了现代程序员所依赖的技术。 游历其中，能感受到短短的年代中计算机技术飞跃发展的历史，以及欣赏到每个人对自己所热爱的这份事业所作出的答卷。同时也能看出由于 54 张卡片数量的局限性，无法对更多辛勤贡献的程序 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/introducing-ningowood-open-source-project/</link>
                <guid isPermaLink="false">5e23cd4cca1efa04e196aead</guid>
                
                <dc:creator>
                    <![CDATA[ hylerrix han ]]>
                </dc:creator>
                <pubDate>Sun, 19 Jan 2020 03:32:26 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/01/WechatIMG2197.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>2019 年 4 月 25 日，Github Star 第一的开源项目 freeCodeCamp 创始人 Qunicy 发表了一篇文章，<a href="https://www.freecodecamp.org/news/programmer-playing-cards/">《Introducing Programmer Playing Cards》</a>，介绍了一副可以边玩边了解程序员历史的扑克牌。几个月后，因着自己对 FCC 中文社区的持续投入，收到了其中的两份卡牌，也塑造了这篇文章、以及这个开源项目的灵感起源。</p><p>整副卡牌共 54 张扑克，包含大小王和四种花色的各 13 张牌。卡牌全部由 100% PVC 材质打造，适度弯曲、浸水以及打火机烘烤都不会影响卡片的耐久度；卡牌包含 54 位程序员先锋的故事，每一张卡片内含一个程序员的生活照片、主要成就清单，以及摘自 TA 的名言——正是这些先锋们开创了现代程序员所依赖的技术。</p><p>游历其中，能感受到短短的年代中计算机技术飞跃发展的历史，以及欣赏到每个人对自己所热爱的这份事业所作出的答卷。同时也能看出由于 54 张卡片数量的局限性，无法对更多辛勤贡献的程序员们提供展示的舞台。因此，为了让这份有关“程序员扑克牌”的喜悦能够通过互联网传播、通过开源项目传播，并逐步收录更多的故事，塑造更多的可能性——那就从为其写一份 UI 开始吧！</p><p>值 2020.01.01 新的十年伊始之际，以全新品牌“<a href="https://links.jianshu.com/go?to=%255Bhttps%3A%2F%2Fgithub.com%2Fningowood%255D%28https%3A%2F%2Fgithub.com%2Fningowood%29">凝果屋(@ningowood)</a>”为出发点，以全新开源项目“<a href="https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fningowood%2Fpoker-coder">程序员扑克牌(poker-coder)</a>”为着手点，用更多实打实的开源项目驱动学习，开启新十年的开源征程！</p><p>开源项目仓库地址：<a href="https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fningowood%2Fpoker-coder">https://github.com/ningowood/poker-coder</a><br>开源项目部署地址：<a href="https://links.jianshu.com/go?to=https%3A%2F%2Fningowood.github.io%2Fpoker-coder%2F">https://ningowood.github.io/poker-coder/</a></p><figure class="kg-card kg-image-card"><img src="https://upload-images.jianshu.io/upload_images/2558748-a74cc38e8bd903f8.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp" class="kg-image" alt="webp" width="1500" height="1181" loading="lazy"></figure><p>设计灵感来源于 @freeCodeCamp</p><h1 id="-54-54-">一、54 张扑克牌，54 个技术故事</h1><p>收录的 54 个程序员里，包含有业界耳熟能详的 Ruby on Rails 之父 David、比特币之父中本聪、Linux 之父 Linus、以及有“第一位程序员”以及“第一位女性程序员”之称的 Ada Lovelace......下面我们先大致介绍一下收录中的这 54 位程序员吧。</p><p>目录结构分为“大小王 Jokers”、“黑桃 Spades”、“红桃 Hearts”、“梅花 Clubs”、“方块 Diamonds”以及“亚裔程序员”和“女性程序员”板块。其中“亚裔程序员”和“女性程序员”摘自前面花色中，且有重复性，但不妨我们从另一个角度看待全球计算机发展史上亚洲程序员以及最重要却最容易被忽视的——女性程序员的贡献。</p><h2 id="-jokers">大小王 Jokers</h2><p>借助基于密码证明的电子货币，无需信任第三方中间商，资金就可以安全，交易也不费吹灰之力。——Satoshi Nakamoto(中本聪)</p><figure class="kg-card kg-image-card"><img src="https://upload-images.jianshu.io/upload_images/2558748-cb6f21b2772e1c4b.png?imageMogr2/auto-orient/strip|imageView2/2/w/608/format/webp" class="kg-image" alt="webp" width="608" height="850" loading="lazy"></figure><p>Satoshi Nakamoto花色程序员成就概要大王David Heinemeier Hansson创造了 Ruby on Rails小王Satoshi Nakamoto发明了区块链；设计了比特币</p><h2 id="-spades">黑桃 Spades</h2><p>你可以说我在计算上很懒惰，但因此我创造了计算机。——Konrad Zuse</p><figure class="kg-card kg-image-card"><img src="https://upload-images.jianshu.io/upload_images/2558748-3d4b08b720211aaa.png?imageMogr2/auto-orient/strip|imageView2/2/w/608/format/webp" class="kg-image" alt="webp" width="608" height="852" loading="lazy"></figure><p>Konrad Zuse花色程序员成就概要♠ AAda Lovelace发明了计算机算法；编写了第一个计算机程序♠ 2Linus Torvalds创造了 Linux 操作系统；创造了 Git 版本控制系统♠ 3Bjarne Stroustrup创造了 C++ 程序语言♠ 4Patricia Sellinger帮助创造 System R 项目(SQL 的第一次实现)♠ 5Tim Berners-Lee创造了万维网♠ 6Richard Stallman发明了 GNU 操作系统；建立了自由软件基金会；创造了 GNU Emacs 编辑器♠ 7Raymond Kurzweil发明了 OCR(光学字符识别)；开发了首款商用文本语音合成器♠ 8Ken Thompson发明了 B 语言；合作发明了 Unix 操作系统；合作发明了 Go 语言♠ 9Ray Tomlinson发明了 Email♠ 10Mary Allen Wilkes为 LINC 设计了交互式操作系统 LAP6；第一个在家中建造和使用个人计算机♠ JEdsger Dijkstra发明了 Dijkstra 最短路径算法；提出了哲学家就餐问题♠ QDouglas Engelbart发明了鼠标；帮助发明了图形用户界面♠ KKonrad Zuse建造了第一个二进制电脑；建造了第一个可编程数字计算机；设计了第一个高级程序设计语言</p><h2 id="-hearts">红桃 Hearts</h2><p>Python是程序员需要多少自由度的实验。自由度太大，没有人可以阅读别人的代码；太少了，表现力受到了威胁。——Guido Van Rossum</p><figure class="kg-card kg-image-card"><img src="https://upload-images.jianshu.io/upload_images/2558748-c17e6e0a6549bd1b.png?imageMogr2/auto-orient/strip|imageView2/2/w/608/format/webp" class="kg-image" alt="webp" width="608" height="852" loading="lazy"></figure><p>Guido Van Rossum花色程序员成就概要♥ AGrace Hopper首创术语“bug”；发明了编译器；合作开发了 COBOL♥ 2Andrew Ng领导斯坦福人工智能机器人项目；领导斯坦福自治直升机项目；上线线上机器学习课程♥ 3Corrinne Yu为美国航天飞机计划编写代码；为自己创建了 3D 图形引擎；领导 Halo 游戏系列的开发♥ 4Brian Fox创造了 GNU 的 Bash Shell；合作开发了 OVC 开源投票系统♥ 5Guido Van Rossum创造了 Python 语言♥ 6Larry Wall创建了 Git 的前身 Patch，创建了 Per 语言♥ 7Bob Frankston &amp; Dan Bricklin创造了第一个电子表格程序 VisiCalc♥ 8Adele Goldberg提出用于图形用户界面的概念；合作开发了 Smalltalk-80 语言♥ 9Brian Kernighan合作开发了 Unix 操作系统；编写了第一个 “Hello World” 程序；合作发明了 Awk 语言♥ 10Donald Knuth出版了《计算机编程艺术》；设计了 TeX 类型设置系统♥ JJudea Pearl创建了人工智能的概率方法；发明了贝叶斯网络♥ QJohn McCarthy提出了术语“人工智能”；创建了 Lisp 语言♥ KMaurice Wilkes打造了第一台具有内部存储程序的计算机 EDSAC</p><h2 id="-clubs">梅花 Clubs</h2><p>知识管理首先是要利用信息来提出正确的问题，这本身就是一个巨大且通常是无法识别的挑战。——Dana Ulery</p><figure class="kg-card kg-image-card"><img src="https://upload-images.jianshu.io/upload_images/2558748-ef9e3f80edf29e76.png?imageMogr2/auto-orient/strip|imageView2/2/w/610/format/webp" class="kg-image" alt="webp" width="610" height="856" loading="lazy"></figure><p>Dana Ulery花色程序员成就概要♣ AIda Rhodes从事于“数学表项目”；和 Betty Holberton 一起为 UNIVAC-I 设计了 C-10 语言♣ 2Bram Cohen创造了 BitTorrent 对等协议♣ 3Brendan Eich创造了 JavaScript 语言♣ 4Sophie Wilson设计了橡子微型计算机；开发了 BBC Basic 语言♣ 5James Gosling创造了 Java 语言♣ 6Bill Joy创造了 VI 编辑器；创造了 Unix 下的 C Shell♣ 7Steve Wozniak设计了第一台大众市场的微型计算机 Apple II；发明了通用遥控器♣ 8Ward Christensen开发了一种简单的文件传输协议 XMODEM，第一个公告板 CBBS 共同创始人♣ 9Dennis Ritchie合作创建了 Unix 操作系统；设计了 C 语言♣ 10Dana Ulery早期的科学计算应用先锋；建立了电子数据交换标准♣ JFrances Allen为最早的超级计算机之一 IBM Stretch 设计了编译器♣ QMarvin Minsky发明了神经网络和第一台自学机器；发明了第一台头戴式图形显示器♣ KAlan Turing发明了图灵机；开发出可以破解纳粹加密的计算机；发明了图灵测试</p><h2 id="-diamonds">方块 Diamonds</h2><p>始终如一地编写安全代码要比指出不安全代码要困难得多。——Parisa Tabriz</p><figure class="kg-card kg-image-card"><img src="https://upload-images.jianshu.io/upload_images/2558748-c5d9052fe891586e.png?imageMogr2/auto-orient/strip|imageView2/2/w/608/format/webp" class="kg-image" alt="webp" width="608" height="854" loading="lazy"></figure><p>Parisa Tabriz花色程序员成就概要♦ ADorothy Vaughan计算出了美国太空计划的飞行轨迹；教员工行 FORTRAN 编程♦ 2Parisa Tabriz监督 Google Chrome 的安全性；成立了安全倡导者会议♦ 3Yukihiro Matsumoto设计了 Ruby 语言♦ 4Stephen Wolfram开发了计算机代数系统 Mathematica；开发了应答引擎 Wolfram Alpha♦ 5Alexey Pajitnov设计并开发了俄罗斯方块♦ 6Phil Zimmermann创建了公共密钥加密程序 Pretty Good Privacy♦ 7Radia Perlman设计了以太网的生成树协议♦ 8Andrew Yao用极小定理提出姚的极小极大原理；引入通信复杂性理论；提出了姚的百万富翁问题♦ 9Vint Cerf &amp; Bob Kahn发明了传输控制协议；发明了互联网协议♦ 10Alan Kay率先使用了面向对象程序设计♦ JMargaret Hamilton负责阿波罗登月任务的软件开发♦ QJean E. Sammet合作开发了 COBOL 语言；开发了 FORMAC 语言♦ KVera Molnar开始迭代组合图像；开始基于几何形状和主题创建算法绘画</p><h2 id="-">亚裔程序员</h2><p>节选名言：今天，我实际上很难考虑一个在未来几年内不会被人工智能改变的行业。——Andrew Ng</p><figure class="kg-card kg-image-card"><img src="https://upload-images.jianshu.io/upload_images/2558748-93c14202107af224.png?imageMogr2/auto-orient/strip|imageView2/2/w/608/format/webp" class="kg-image" alt="webp" width="608" height="854" loading="lazy"></figure><p>Andrew Ng花色程序员成就概要小王Satoshi Nakamoto发明了区块链；设计了比特币♥ 2Andrew Ng领导斯坦福人工智能机器人项目；领导斯坦福自治直升机项目；上线线上机器学习课程♦ 3Yukihiro Matsumoto设计了 Ruby 语言♦ 8Andrew Yao用极小定理提出姚的极小极大原理；引入通信复杂性理论；提出了姚的百万富翁问题</p><h2 id="--1">女性程序员</h2><p>有时我会拥抱我的代码。我会抓取一些代码打印输出，将自己围在几本书中，握住法律垫子，curl 缩在带毯子的沙发上，然后开始阅读并乱涂乱画。[机翻]——Corrinne Yu</p><figure class="kg-card kg-image-card"><img src="https://upload-images.jianshu.io/upload_images/2558748-2a0739474960006f.png?imageMogr2/auto-orient/strip|imageView2/2/w/608/format/webp" class="kg-image" alt="webp" width="608" height="852" loading="lazy"></figure><p>Corrinne Yu花色程序员成就概要♠ AAda Lovelace发明了计算机算法；编写了第一个计算机程序♠ 4Patricia Sellinger帮助创造 System R 项目(SQL 的第一次实现)♠ 10Mary Allen Wilkes为 LINC 设计了交互式操作系统 LAP6；第一个在家中建造和使用个人计算机♥ 3Corrinne Yu为美国航天飞机计划编写代码；为自己创建了 3D 图形引擎；领导 Halo 游戏系列的开发♥ 8Adele Goldberg提出用于图形用户界面的概念；合作开发了 Smalltalk-80 语言♣ AIda Rhodes从事于“数学表项目”；和 Betty Holberton 一起为 UNIVAC-I 设计了 C-10 语言♣ 4Sophie Wilson设计了橡子微型计算机；开发了 BBC Basic 语言♣ 10Dana Ulery早期的科学计算应用先锋；建立了电子数据交换标准♣ JFrances Allen为最早的超级计算机之一 IBM Stretch 设计了编译器♦ ADorothy Vaughan计算出了美国太空计划的飞行轨迹；教员工行 FORTRAN 编程♦ 2Parisa Tabriz监督 Google Chrome 的安全性；成立了安全倡导者会议♦ 7Radia Perlman设计了以太网的生成树协议♦ JMargaret Hamilton负责阿波罗登月任务的软件开发♦ QJean E. Sammet合作开发了 COBOL 语言；开发了 FORMAC 语言</p><h2 id="-55-">隐藏的第 55+ 张扑克：你</h2><p>整副扑克牌 UI 已经变成 CSS 布局，因此在本地可以自定义自己的卡牌。</p><figure class="kg-card kg-image-card"><img src="https://upload-images.jianshu.io/upload_images/2558748-e175e2e100c052fc.png?imageMogr2/auto-orient/strip|imageView2/2/w/608/format/webp" class="kg-image" alt="webp" width="608" height="852" loading="lazy"></figure><p>隐藏的扑克牌</p><h1 id="--2">二、纯前端开源项目开发历程小记</h1><p>本项目目前专注于纯前端技术的建设，主要的前端技术栈如下：</p><ul><li>React：前端视图层核心库</li><li>TypeScript：提供静态类型检查</li><li>Material UI：Material Design 风格的 UI 设计库</li><li>...</li></ul><p><strong><em>React + Material UI</em></strong></p><p>这里不做过多的技术探讨，NingoWood 的主要前端技术选型也将围绕 React + Material Design 搭建前端风格。未来会在学习过程中逐步分享相关技术知识点。</p><p>从零到 Create React App，再到正式开发业务代码的小记(构建项目，安装依赖)如下：</p><pre><code class="language-ruby">$ npx create-react-app poker-coder --typescript
$ git remote add origin git@github.ningowood/poker-coder.git
$ git flow init
$ git flow feature start poker-coder
$ mkdir src/views src/store src/routes src/config src/components src/commons
$ sudo commitizen init cz-conventional-changelog --yarn --dev --exact --force
$ sudo yarn add react-router-dom @types/react-router-dom
$ sudo yarn add redux react-redux @types/react-redux
$ sudo yarn add redux-devtools-extension redux-logger redux-thunk @types/redux-logger
$ sudo yarn add @material-ui/core @material-ui/icons axios moment notistack
$ sudo yarn add react-i18next i18next
$ sudo yarn add --dev less less-loader node-sass react-hot-loader
</code></pre><p><strong><em>布局 + 填充</em></strong></p><p>从看到扑克牌 UI 到落实的过程中，需要进行 CSS 思维的转换。每一张卡片分为左侧和右侧的花色姓名栏以及中间的内容部分；内容部分又包括程序员图片、里程碑事件以及名言引用块。</p><p>关键的 CSS 突破点在于栅格系统 + 响应式高度来实现扑克牌的宽高比例恒定问题。其实只要会搜索，就会发现可以用 <code>padding-bottom: 140%</code> 来保证高度是宽度的 1.4 倍，然后宽度随着栅格系统改变即可。</p><figure class="kg-card kg-image-card"><img src="https://upload-images.jianshu.io/upload_images/2558748-78ac4433b748e744.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp" class="kg-image" alt="webp" width="1500" height="750" loading="lazy"></figure><p>开发 UI 过程中的某次截图</p><p>容器的 CSS 代码如下，具体可以参考开源项目根目录下 src/components 下的写法。</p><pre><code class="language-css">root: {
  height: 0,
  width: '100%',
  paddingBottom: '140%',
  position: 'relative',
},
cardWrap: {
  height: '100%',
  width: '100%',
  backgroundColor: '#fff',
  position: 'absolute',
  textAlign: 'center',
},
</code></pre><p><strong><em>开源 + 部署</em></strong></p><p>为了贯穿良好的开发习惯，整个开发过程从 Git Flow 的选用以及 Git Commit Message 等角度都做了较好的规范，并最终通过<code>npm run build</code>和<code>npm run deploy</code>部署到 Github Pages 页面上。</p><p>截止 v0.1.0 发布，通过 <code>git log --pretty=format:'%ar,%s' &gt; log.csv</code> 命令生成并通过 Emoji 适度修改的 Commit Log 记录如下，统一记录在了 Release 里。</p><figure class="kg-card kg-image-card"><img src="https://upload-images.jianshu.io/upload_images/2558748-71e0987b227181ba.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp" class="kg-image" alt="webp" width="1500" height="1235" loading="lazy"></figure><h1 id="-v0-1-0-">三、v0.1.0 正式发布，以及未来版本蓝图</h1><p>回顾整个开发过程，通过简单的 CSS 布局 + 卡片内容填充以及 Github Pages 部署上线，v0.1.0 正式发布。在这个过程中初期灵感得到实现，也随之而来了各种各样的新灵感。记录在这里，也逐步开放在 Github 仓库中的 issue 里，欢迎大家的加入。</p><p><strong><em>[1] 不仅仅是程序员扑克牌</em></strong></p><p>从之前介绍的隐藏的第 55+ 张扑克牌可以看出：我们不仅仅可以记录程序员的故事，也能记录各行各业先驱的故事；我们不仅仅可以记录人的故事，也可以记录不同事务的故事——如不同的编程语言甚至不同超级英雄的故事——一切都是开放性的。</p><p><strong><em>[2] 不仅仅只有一个卡组</em></strong></p><p>也因此得出这个待做事项——支持更多的卡组，记录更多的人与事。</p><p><strong><em>[3] 自定义卡片与卡组</em></strong></p><p>目前的重点是纯前端实现“程序员扑克牌”项目，这就可以发展处让用户不用克隆项目至本地修改源码来自定义卡片，直接在线编辑卡片并导出，甚至保存在云端。</p><p><strong><em>[3] i18n: 国际化语言</em></strong></p><p>由于这幅扑克牌的介绍都是纯英文的，也因此从纯英文支持出发。接下来逐步开放多语言支持，并对社区提供翻译入口的开放，对中文更加友好。</p><p><strong><em>以及更多...</em></strong></p><h1 id="--3">尾、寒冬寄语，欢迎加入凝果开源社</h1><p>纵观 2019，寒冬越来越“寒”，同时，中文语境下的“程序员”三个字也逐渐变得和“被动”、“猝死”、“淘汰”、“失业”有关。希望我们能够通过这篇文章对“程序员扑克牌”的介绍，通过这个项目对这些“Programmer”的真实记录，重新找回属于自身职业的荣耀，让自己对事业能多添一份激情所在。</p><p>那么，欢迎加入凝果开源社，2020 伊始，对未来开源世界的持续支持与关注，就从订阅我们的微信公众号(@ningowood)开始吧！</p><p>我的 Github 地址：<a href="https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fhylerrix">https://github.com/hylerrix</a><br>凝果屋的 Github 地址：<a href="https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fningowood">https://github.com/ningowood</a><br>程序员扑克牌的 Github Pages：<a href="https://links.jianshu.com/go?to=https%3A%2F%2Fningowood.github.io%2Fpoker-coder%2F">https://ningowood.github.io/poker-coder/</a></p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
