<?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[ 项目 - 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[ 项目 - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 04 Jun 2026 15:37:44 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/projects/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <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[ 用 Go 和 Vue 搭建一个网站缩略图生成器 ]]>
                </title>
                <description>
                    <![CDATA[ 当我刚开始使用 Go 编程时，我发现它很难理解，感觉它比我以前学的其他语言都更底层。学了几个月之后，我变成 Go 语言的忠粉，并用它开发许多项目。 在本文中，我会分享如何使用 Go 和 Vue 搭建全栈 Web 应用程序。 我们要搭建什么项目 我觉得搭建一个网站缩略图生成器会很酷，就是当你输入一个网站 URL，应用程序将生成该网站的缩略图。 配置 Go 模块 首先，创建一个新的目录。然后，通过运行以下命令来配置 Go 模块。 go mod init github.com/Dirk94/website-thumbnail-generator 这样就创建了一个 go.mod 文件，包含模块的所有依赖。这和 node 项目中的 package.json 文件类似。 接下来，创建一个新的目录 main，在里面增加一个 server.go 文件。这是应用程序的主入口。 现在，我们打印一行 “hello world”。 package main import "fmt" func main() { 	fmt.Println("Hello world") } 从项目目录运行以下 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/set-up-a-project-with-go-and-vue/</link>
                <guid isPermaLink="false">5fc85ed039641a0517d5171c</guid>
                
                    <category>
                        <![CDATA[ Vue ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Go ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 项目 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chengjun.L ]]>
                </dc:creator>
                <pubDate>Thu, 03 Dec 2020 08:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/12/1_9uBiGMLNlBPoNm8kmBCeYw--1-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>当我刚开始使用 Go 编程时，我发现它很难理解，感觉它比我以前学的其他语言都更底层。学了几个月之后，我变成 Go 语言的忠粉，并用它开发许多项目。</p><p>在本文中，我会分享如何使用 Go 和 Vue 搭建全栈 Web 应用程序。</p><h2 id="-"><strong>我们要搭建什么项目</strong></h2><p>我觉得搭建一个网站缩略图生成器会很酷，就是当你输入一个网站 URL，应用程序将生成该网站的缩略图。</p><h2 id="-go-"><strong>配置 Go 模块</strong></h2><p>首先，创建一个新的目录。然后，通过运行以下命令来配置 Go 模块。</p><pre><code>go mod init github.com/Dirk94/website-thumbnail-generator</code></pre><p>这样就创建了一个 <code>go.mod</code> 文件，包含模块的所有依赖。这和 node 项目中的 <code>package.json</code> 文件类似。</p><p>接下来，创建一个新的目录 <code>main</code>，在里面增加一个 <code>server.go</code> 文件。这是应用程序的主入口。</p><p>现在，我们打印一行 “hello world”。</p><pre><code class="language-go">package main

import "fmt"

func main() {
	fmt.Println("Hello world")
}</code></pre><p>从项目目录运行以下命令，运行该程序：</p><pre><code>go run main/server.go
Hello world</code></pre><p>太好了，到目前为止一切正常！</p><h2 id="-web-"><strong>配置 web 服务器</strong></h2><p>我们应该创建一个 web 服务器来监听传入的请求。</p><p>更新 main 函数：</p><pre><code class="language-go">func main() {
	http.HandleFunc("/", homePageHandler)

	fmt.Println("Server listening on port 3000")
	log.Panic(
		http.ListenAndServe(":3000", nil),
	)
}</code></pre><p>这将启动 web 服务器并监听端口 3000。</p><p><code>homePageHandler</code> 函数将处理所有传入的请求。</p><pre><code class="language-go">func homePageHandler(w http.ResponseWriter, r *http.Request) {
	_, err := fmt.Fprintf(w, "hello world")
	checkError(err)
}

func checkError(err error) {
	if err != nil {
		log.Panic(err)
	}
}</code></pre><p>这个函数做的事情就是把 “hello world” 写入 <code>http.ResponseWriter</code>。</p><p><code>checkError</code> 函数的作用是在出现 <code>error</code> 的时候停止程序并打印堆栈跟踪。</p><p>运行该程序时，web 服务器将正确打印 “hello world” 消息！</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-14-at-17.02.19.png" class="kg-image" alt="Screenshot-2020-05-14-at-17.02.19" width="600" height="400" loading="lazy"></figure><h2 id="-vue-"><strong>创建 Vue 项目</strong></h2><p>从项目目录运行以下命令，创建一个新的 Vue 项目。</p><pre><code>vue create frontend</code></pre><p>这会创建很多文件，但是没关系，我们先运行 Vue 服务器。</p><pre><code>yarn serve</code></pre><p>访问 localhost:8081，可以看到 Vue app 了！</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-15-at-19.11.06.png" class="kg-image" alt="Screenshot-2020-05-15-at-19.11.06" width="600" height="400" loading="lazy"></figure><p>接下来，我们清理一下前端目录。</p><p>删掉 <code>assets</code> 和 <code>components</code>，因为我们不需要它们。</p><p>然后更新 <code>App.vue</code> 文件。</p><pre><code class="language-html">&lt;template&gt;
  &lt;div id="app" class="container"&gt;
    &lt;div class="row"&gt;
      &lt;div class="col-md-6 offset-md-3 py-5"&gt;
        &lt;h1&gt;Generate a thumbnail of a website&lt;/h1&gt;

        &lt;form v-on:submit.prevent="makeWebsiteThumbnail"&gt;
          &lt;div class="form-group"&gt;
            &lt;input v-model="websiteUrl" type="text" id="website-input" placeholder="Enter a website" class="form-control"&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;button class="btn btn-primary"&gt;Generate!&lt;/button&gt;
          &lt;/div&gt;
        &lt;/form&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;</code></pre><p>使用 <code>v-model</code> 标签，在表单提交时调用 <code>makeWebsiteThumbnail</code> 函数。</p><pre><code class="language-javascript">&lt;script&gt;
export default {
  name: 'App',

  data() { return {
    websiteUrl: '',
  } },

  methods: {
    makeWebsiteThumbnail() {
      console.log(`I should create a website thumbnail of ${this.websiteUrl}`);
    }
  }
}
&lt;/script&gt;</code></pre><p>我也用了些 Bootstrap class，给 <code>public/index.html</code> 文件添加 CSS file。</p><pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"&gt;
      
      &lt;!--- The other stuff in the head tag here... --&gt;
  &lt;/head&gt;</code></pre><p>启动 web 服务器，检查是否看到日志消息。</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-15-at-18.36.31.png" class="kg-image" alt="Screenshot-2020-05-15-at-18.36.31" width="600" height="400" loading="lazy"></figure><p>没有问题！</p><h2 id="--1"><strong>创建网站缩略图</strong></h2><p>要创建网站缩略图，我将使用 <a href="https://screenshotapi.net/">screenshotapi.net</a>。这样，我只需要调用 API 即可完成复杂的工作。</p><p>安装 axios：</p><pre><code>yarn add axios</code></pre><p>将它引入到 <code>App.vue</code> 文件：</p><pre><code class="language-javascript">&lt;script&gt;
  import axios from 'axios';
  
  export default {
    name: 'App', 
    
    // The rest here...
    </code></pre><p>然后，更新 <code>makeWebsiteThumbnail</code> 函数，调用 screenshot API。</p><pre><code class="language-javascript">makeWebsiteThumbnail() {
  axios.post("https://screenshotapi.net/api/v1/screenshot", {
    token: "SCREENSHOTAPI_TOKEN",
    url: this.websiteUrl,
    width: 1920,
    height: 1080,
    output: 'json',
    thumbnail_width: 300
  })
  .then((response) =&gt; {
    this.thumbnailUrl = response.data.screenshot;
  })
  .catch((error) =&gt; {
    window.alert(`The API returned an error: ${error}`);
  })
}</code></pre><p>记得将 <code>SCREENSHOTAPI_TOKEN</code> 替换为你的 token。</p><p>将变量 <code>thumbnailUrl</code> 设置为通过 API 创建的 screenshot URL。要实现这个，需要做两件事。</p><p>首先，把 <code>thumbnailUrl</code> 变量添加到 Vue <code>data</code> 对象：</p><pre><code class="language-javascript">data: {
  websiteUrl: '',
  thumbnailUrl: '',
},</code></pre><p>其次，创建一个 <code>img</code> 标签，显示 <code>thumbnailUrl</code> 图像：</p><pre><code class="language-html">&lt;img :src="thumbnailUrl"/&gt;</code></pre><p>启动 &nbsp;web 服务器查看结果：</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-15-at-18.57.00.png" class="kg-image" alt="Screenshot-2020-05-15-at-18.57.00" width="600" height="400" loading="lazy"></figure><p>显示 freeCodeCamp 缩略图了，很棒！</p><h2 id="-go-vue-"><strong>合并 Go 和 Vue 的代码</strong></h2><p>现在，我们已经使用 Vue 开发服务器加速了前端，但是开发服务器只能在本地开发时使用。</p><p>当我们在生产环境中托管这个应用程序时，需要使用“真实的” web 服务器来处理传入的请求。</p><p>幸运的是，我们有 Go 服务器。</p><p>我们要做的第一件事是编译前端。</p><pre><code>yarn run build</code></pre><p>这样就创建了一个 <code>dist</code> 目录。</p><p>更新 Go 服务器，以从该目录提供文件。</p><p>为此，我更新了 <code>main.go</code> 文件中的 <code>main</code> 函数。</p><pre><code class="language-go">func main() {
	// Serve static files from the frontend/dist directory.
	fs := http.FileServer(http.Dir("./frontend/dist"))
	http.Handle("/", fs)

	// Start the server.
	fmt.Println("Server listening on port 3000")
	log.Panic(
		http.ListenAndServe(":3000", nil),
	)
}</code></pre><p>将 <code>frontend/dist</code> 目录传入 fileserver。</p><p>运行 Go 程序，访问 <code>localhost:3000</code>，就可以看到应用了！</p><h2 id="--2"><strong>提高应用程序的安全性</strong></h2><p>目前，这个应用存在一个重大的安全漏洞。screenshot API token 在前端代码中可见。这意味着检查该网页的任何人都可以窃取 token。</p><p>我们通过使用服务器调用 screenshot API 来解决这个问题。这样，只有服务器需要知道 token。</p><p>在 <code>server.go</code> 中，创建一个新函数，以监听对 <code>/api/thumbnail</code> 端点的任何请求。</p><pre><code class="language-go">type thumbnailRequest struct {
	Url string `json:"url"`
}

func thumbnailHandler(w http.ResponseWriter, r *http.Request) {
	var decoded thumbnailRequest

	// Try to decode the request into the thumbnailRequest struct.
	err := json.NewDecoder(r.Body).Decode(&amp;decoded)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	fmt.Printf("Got the following url: %s\n", decoded.Url)
}</code></pre><p>现在，我们仅从请求中提取并打印 URL 参数。通过更新 <code>main</code> 函数，使用 <code>thumbnailHandler</code> 函数来实现。</p><pre><code class="language-go">func main() {
	// Use the thumbnailHandler function 
	http.HandleFunc("/api/thumbnail", thumbnailHandler)

	fs := http.FileServer(http.Dir("./frontend/dist"))
	http.Handle("/", fs)

	fmt.Println("Server listening on port 3000")
	log.Panic(
		http.ListenAndServe(":3000", nil),
	)
}</code></pre><p>最后，更新 <code>App.vue</code> 文件，调用 Go 服务器，而不是 screenshot API。</p><pre><code class="language-javascript">makeWebsiteThumbnail() {
  // Call the Go API, in this case we only need the URL parameter.
  axios.post("http://localhost:3000/api/thumbnail", {
    url: this.websiteUrl,
  })
  .then((response) =&gt; {
    this.thumbnailUrl = response.data.screenshot;
  })
  .catch((error) =&gt; {
    window.alert(`The API returned an error: ${error}`);
  })
}</code></pre><p>测试新的设置，可以在 Go 服务器看到一条日志信息。</p><pre><code>go run main/server.go
Got the following url: freecodecamp.org</code></pre><h2 id="-go-screenshot-api"><strong>从 Go 调用 screenshot API</strong></h2><p>现在我们从 Go 服务器调用 screenshot API。</p><p>首先，创建一个 <code>struct</code>，包含调用 screenshot API 所需的所有参数。</p><pre><code class="language-go">type screenshotAPIRequest struct {
	Token          string `json:"token"`
	Url            string `json:"url"`
	Output         string `json:"output"`
	Width          int    `json:"width"`
	Height         int    `json:"height"`
	ThumbnailWidth int    `json:"thumbnail_width"`
}</code></pre><p>然后，更新 <code>thumbnailHandler</code> 函数，创建一个 http POST 请求，调用 API。</p><pre><code class="language-go">func thumbnailHandler(w http.ResponseWriter, r *http.Request) {
	var decoded thumbnailRequest

	// Try to decode the request into the thumbnailRequest struct.
	err := json.NewDecoder(r.Body).Decode(&amp;decoded)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Create a struct with the parameters needed to call the ScreenshotAPI.
	apiRequest := screenshotAPIRequest{
		Token:          "SCREENSHOTAPI_TOKEN",
		Url:            decoded.Url,
		Output:         "json",
		Width:          1920,
		Height:         1080,
		ThumbnailWidth: 300,
	}

	// Convert the struct to a JSON string.
	jsonString, err := json.Marshal(apiRequest)
	checkError(err)

	// Create a HTTP request.
	req, err := http.NewRequest("POST", "https://screenshotapi.net/api/v1/screenshot", bytes.NewBuffer(jsonString))
	req.Header.Set("Content-Type", "application/json")

	// Execute the HTTP request.
	client := &amp;http.Client{}
	response, err := client.Do(req)
	checkError(err)

	// Tell Go to close the response at the end of the function.
	defer response.Body.Close();

	// Read the raw response into a Go struct.
	type screenshotAPIResponse struct {
		Screenshot string `json"screenshot"`
	}
	var apiResponse screenshotAPIResponse
	err = json.NewDecoder(response.Body).Decode(&amp;apiResponse)
	checkError(err)

	// Pass back the screenshot URL to the frontend.
	_, err = fmt.Fprintf(w, `{ "screenshot": "%s" }`, apiResponse.Screenshot)
	checkError(err)
}</code></pre><p>重新启动 Go 服务器时，会看到缩略图生成器运行成功！而且，现在没有人可以窃取我们的 token。</p><figure class="kg-card kg-image-card"><img src="https://www.freecodecamp.org/news/content/images/2020/05/Screenshot-2020-05-16-at-10.00.18.png" class="kg-image" alt="Screenshot-2020-05-16-at-10.00.18" width="600" height="400" loading="lazy"></figure><h2 id="--3"><strong>总结</strong></h2><p>我们使用 Go 和 Vue 搭建了一个全栈网站缩略图生成器，前后端分离，并且我们添加了一个外部 API，通过 Go 服务器调用。</p><p><a href="https://coffeecoding.dev/website-thumbnail-generator">在线体验这个应用程序</a>。附上 <a href="https://github.com/Dirk94/website-thumbnail-generator">Github 源代码</a>。</p><p>Happy coding!</p><p>原文：<a href="https://www.freecodecamp.org/news/how-i-set-up-a-real-world-project-with-go-and-vue/">How to Set Up a Real-World Project with Go and Vue</a>，作者：Dirk Hoekstra</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何将 12 小时制（AM/PM）转换为 24 小时制 ]]>
                </title>
                <description>
                    <![CDATA[ 显示时间的主要方法有两种，一种是使用 AM  的 12 小时制时钟，另一种是使用 PM  的 24 小时制时钟。 大多数国家更喜欢 24 小时制，但是 12 小时制在拉丁美洲和英语国家广泛使用。在 12 小时制中，每天有两次 12 点，午夜（AM）和中午（PM）的。 下表显示了 12 小时制和 24 小时制之间的转换： 12 小时制24 小时制12:00 AM00:0001:00 AM01:0002:00 AM02:0003:00 AM03:0004:00 AM04:00 05:00 AM05:0006:00 AM06:0007:00 AM07:0008:00 AM08:0009:00 AM09:0010:00 AM10:00 11:00 AM11:0012:00 PM12:0001:00 PM13:0002:00 PM14:0003:00 PM15:0004:00 PM16:00 05:00 PM17:0006:00 PM18:0007:00 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/mathematics-converting-am-pm-to-24-hour-clock/</link>
                <guid isPermaLink="false">6010f42e5f61e30501b5c1a6</guid>
                
                    <category>
                        <![CDATA[ 项目 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp.org ]]>
                </dc:creator>
                <pubDate>Fri, 30 Oct 2020 07:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/01/photo-1524678714210-9917a6c619c2.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>显示时间的主要方法有两种，一种是使用 <strong>AM</strong> 的 <strong>12 小时制时钟</strong>，另一种是使用 <strong>PM</strong> 的 <strong>24 小时制时钟</strong>。</p><p>大多数国家更喜欢 24 小时制，但是 12 小时制在拉丁美洲和英语国家广泛使用。在 12 小时制中，每天有两次 12 点，午夜（AM）和中午（PM）的。</p><p>下表显示了 12 小时制和 24 小时制之间的转换：</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>12 小时制</th>
<th>24 小时制</th>
</tr>
</thead>
<tbody>
<tr>
<td>12:00 AM</td>
<td>00:00</td>
</tr>
<tr>
<td>01:00 AM</td>
<td>01:00</td>
</tr>
<tr>
<td>02:00 AM</td>
<td>02:00</td>
</tr>
<tr>
<td>03:00 AM</td>
<td>03:00</td>
</tr>
<tr>
<td>04:00 AM</td>
<td>04:00</td>
</tr>
<tr>
<td>05:00 AM</td>
<td>05:00</td>
</tr>
<tr>
<td>06:00 AM</td>
<td>06:00</td>
</tr>
<tr>
<td>07:00 AM</td>
<td>07:00</td>
</tr>
<tr>
<td>08:00 AM</td>
<td>08:00</td>
</tr>
<tr>
<td>09:00 AM</td>
<td>09:00</td>
</tr>
<tr>
<td>10:00 AM</td>
<td>10:00</td>
</tr>
<tr>
<td>11:00 AM</td>
<td>11:00</td>
</tr>
<tr>
<td>12:00 PM</td>
<td>12:00</td>
</tr>
<tr>
<td>01:00 PM</td>
<td>13:00</td>
</tr>
<tr>
<td>02:00 PM</td>
<td>14:00</td>
</tr>
<tr>
<td>03:00 PM</td>
<td>15:00</td>
</tr>
<tr>
<td>04:00 PM</td>
<td>16:00</td>
</tr>
<tr>
<td>05:00 PM</td>
<td>17:00</td>
</tr>
<tr>
<td>06:00 PM</td>
<td>18:00</td>
</tr>
<tr>
<td>07:00 PM</td>
<td>19:00</td>
</tr>
<tr>
<td>08:00 PM</td>
<td>20:00</td>
</tr>
<tr>
<td>09:00 PM</td>
<td>21:00</td>
</tr>
<tr>
<td>10:00 PM</td>
<td>22:00</td>
</tr>
<tr>
<td>11:00 PM</td>
<td>23:00</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><h3 id="12-"><strong>12 小时制</strong></h3><p>一天分为两个 12 小时时段，从午夜到中午（AM 时间），从中午到午夜（PM 时间）。<br>AM 和 PM 的缩写来自拉丁语：</p><ul><li>AM：正午之前</li><li>PM：正午之后</li></ul><h3 id="24-"><strong>24 小时制</strong></h3><p>一天是指从一个午夜到下一个午夜。从午夜 0 点到 23 点为 24 小时。时间以从午夜开始的小时和分钟表示。</p><h2 id="-12-24-"><strong>将 12 小时制转换为 24 小时制</strong></h2><p>从一天的第一个小时（12:00 AM 或午夜至 12:59 AM）开始，减去 12 小时：</p><ul><li>12:00 AM = 0:00</li><li>12:15 AM = 0:15</li></ul><p>从 1:00 AM 到 12:59 PM，小时和分钟保持一致：</p><ul><li>9:00 AM = 9:00</li><li>12:59 PM = 12:59</li></ul><p>1:00 PM 到 11:59 PM 之间的时间，加上 12 小时：</p><ul><li>3:17 PM = 15:17</li><li>11:59 PM = 23:59.</li></ul><h2 id="-24-12-"><strong>将 24 小时制转换为 12 小时制</strong></h2><p>从一天的第一个小时（0:00 或午夜至 00:59）开始，加上 12 小时，并在时间后面加上 AM：</p><ul><li>0:30 = 12:30 AM</li><li>0:55 = 12:55 AM</li></ul><p>从 1:00 到 11:59，只需要在时间后面加上 AM：</p><ul><li>2:25 = 2:25 AM</li><li>9:30 = 9:30 AM</li></ul><p>13:00 到 23:59 之间的时间，减去 12 小时，并在时间后面加上 PM：</p><ul><li>16:55 = 4:55 PM</li><li>21:45 = 8:45 PM</li></ul><p>原文：<a href="https://www.freecodecamp.org/news/mathematics-converting-am-pm-to-24-hour-clock/">24 Hour Clock Converter: How to Convert AM/PM to 24 Hour Time</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ React 项目实践——创建一个聊天机器人 ]]>
                </title>
                <description>
                    <![CDATA[ 我的理念很简单：如果你想要在某个方面精通，那么你需要持续实践，实践一次不会有多少效果，你必须反复实践。 我在编程这件事上就是这么做的。 在这个过程中，我特别感受到：创建一些有意思的好东西是非常有趣的。你可以向朋友展示自己引以为傲的作品，坐下来敲代码实现它的过程会让你感觉欢喜。 比如说我创建了一个聊天机器人（代码 [https://www.npmjs.com/package/react-chatbot-kit]）。 我们一起来创建吧！如果你想自己独立完成这个挑战，可以直接参考这份文档（其实是一个聊天机器人成品） [https://fredrikoseberg.github.io/react-chatbot-kit-docs/]。或者，你可以看这个 YouTube 视频 [https://youtu.be/vTpk-PKZwTs]来学习。 好啦，我们开始吧。我就假设你已经安装了 Node，可以运行 npx 命令。如果没有的话，访问这里 [https://nodejs.org/]。 初始设置 // 运行以下代码 npx create-react-app chatbot cd ch ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-build-a-chatbot-with-react/</link>
                <guid isPermaLink="false">5f69c0ec027c3105323f54ed</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 项目 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chengjun.L ]]>
                </dc:creator>
                <pubDate>Tue, 22 Sep 2020 09:20:05 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/09/wooden-robot-6069-1.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>我的理念很简单：如果你想要在某个方面精通，那么你需要持续实践，实践一次不会有多少效果，你必须反复实践。</p><p>我在编程这件事上就是这么做的。</p><p>在这个过程中，我特别感受到：创建一些有意思的好东西是非常有趣的。你可以向朋友展示自己引以为傲的作品，坐下来敲代码实现它的过程会让你感觉欢喜。</p><p>比如说我创建了一个聊天机器人（<a href="https://www.npmjs.com/package/react-chatbot-kit">代码</a>）。</p><p>我们一起来创建吧！如果你想自己独立完成这个挑战，可以直接参考<a href="https://fredrikoseberg.github.io/react-chatbot-kit-docs/">这份文档（其实是一个聊天机器人成品）</a>。或者，你可以看<a href="https://youtu.be/vTpk-PKZwTs">这个 YouTube 视频</a>来学习。</p><p>好啦，我们开始吧。我就假设你已经安装了 Node，可以运行 npx 命令。如果没有的话，访问<a href="https://nodejs.org/">这里</a>。</p><h2 id="-">初始设置</h2><pre><code>// 运行以下代码
npx create-react-app chatbot
cd chatbot
yarn add react-chatbot-kit
yarn start</code></pre><p>安装 npm 包，访问地址 localhost:3000。</p><p>然后打开 &nbsp;<code>App.js</code>，修改如下：</p><pre><code class="language-jsx">import Chatbot from 'react-chatbot-kit'

function App() {
  return (
    &lt;div className="App"&gt;
      &lt;header className="App-header"&gt;
        &lt;Chatbot /&gt;
      &lt;/header&gt;
    &lt;/div&gt;
  );
}</code></pre><p>现在你的界面是这样的：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/b408b8fde82c39cb658309d5637e1d297fcb245e/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f53637265656e73686f742d323032302d30362d31302d61742d31362e30332e35312e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f53637265656e73686f742d323032302d30362d31302d61742d31362e30332e35312e706e67" width="600" height="400" loading="lazy"></figure><p>聊天机器人要正常工作，需要接收三个 props。首先，需要具有 &nbsp;<code>initialMessages</code> &nbsp;属性，包含聊天信息对象。然后，需要有 &nbsp;<code>MessageParser</code> &nbsp;用于解析，还有 &nbsp;<code>ActionProvider</code> &nbsp;基于解析结果执行我们需要它执行的动作。</p><p>稍后我们进一步讲解这个。现在，<a href="https://gist.github.com/FredrikOseberg/c1e8ec83ade6e89ca84882e33caf599c">在这里获取代码</a>。</p><ul><li>把 &nbsp;<code>MessageParser</code> &nbsp;代码放到 &nbsp;<code>MessageParser.js</code> &nbsp;文件</li><li>把 &nbsp;<code>ActionProvider</code> &nbsp;代码放到 &nbsp;<code>ActionProvider.js</code> &nbsp;文件</li><li>把 config 代码放到 &nbsp;<code>config.js</code> &nbsp;文件</li></ul><p>现在返回到 &nbsp;<code>App.js</code> &nbsp;文件，添加以下代码：</p><pre><code class="language-jsx">import React from 'react';
import Chatbot from 'react-chatbot-kit'
import './App.css';

import ActionProvider from './ActionProvider';
import MessageParser from './MessageParser';
import config from './config';

function App() {
  return (
    &lt;div className="App"&gt;
      &lt;header className="App-header"&gt;
        &lt;Chatbot config={config} actionProvider={ActionProvider} 	    messageParser={MessageParser} /&gt;
      &lt;/header&gt;
    &lt;/div&gt;
  );
}</code></pre><p>localhost:3000 现在应该是这样显示：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/885259521c789a27e9116adcbe3fdcf12e69f7f5/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f53637265656e73686f742d323032302d30362d31302d61742d31362e31362e35372e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f53637265656e73686f742d323032302d30362d31302d61742d31362e31362e35372e706e67" width="600" height="400" loading="lazy"></figure><p>很棒！我们已经初始化了聊天机器人，可以输入和提交一些信息了。试试看。</p><h2 id="--1">理解聊天机器人是怎么工作的</h2><p>我们暂停一下，看看 &nbsp;<code>MessageParser</code> &nbsp;和 &nbsp;<code>ActionProvider</code> &nbsp;是怎么配合让聊天机器人执行动作的。</p><p>机器人初始化的时候，内部 state 的 &nbsp;<code>messages</code> &nbsp;属性获取 &nbsp;<code>initialMessages</code> &nbsp;属性值, 将信息渲染到屏幕。</p><p>接着，当我们在聊天框输入信息，点击 submit 提交时，<code>MessageParser</code>（作为 props 传递给机器人)调用 &nbsp;<code>parse</code> &nbsp;方法。</p><p>我们进一步看看 &nbsp;<code>MessageParser</code> &nbsp;的代码：</p><pre><code class="language-jsx">class MessageParser {
  constructor(actionProvider) {
    this.actionProvider = actionProvider;
  }

  parse(message) {
    ... parse logic
  }
}</code></pre><p>代码中包含 &nbsp;<code>actionProvider</code>，这跟我们传递给聊天机器人的 props &nbsp;<code>ActionProvider</code> &nbsp;是一样的。我们通过这个代码解析信息，并告诉机器人执行什么动作。</p><p>比如，我们创建一个简单的响应。首先，将 &nbsp;<code>MessageParser</code> &nbsp;改为：</p><pre><code>class MessageParser {
  constructor(actionProvider) {
    this.actionProvider = actionProvider;
  }

  parse(message) {
    const lowerCaseMessage = message.toLowerCase()
    
    if (lowerCaseMessage.includes("hello")) {
      this.actionProvider.greet()
    }
  }
}

export default MessageParser</code></pre><p><code>MessageParser</code> &nbsp;接收到用户的信息，检查是否包含 “hello”。如果包含，则调用 &nbsp;<code>actionProvider</code> &nbsp;的 &nbsp;<code>greet</code> &nbsp;方法。</p><p>不过现在还行不通，因为我们还没有执行 &nbsp;<code>greet</code> &nbsp;方法。稍后再处理这个。先处理 &nbsp;<code>ActionProvider.js</code> &nbsp;文件如下：</p><pre><code>class ActionProvider {
  constructor(createChatBotMessage, setStateFunc) {
    this.createChatBotMessage = createChatBotMessage;
    this.setState = setStateFunc;
  }
  
  greet() {
    const greetingMessage = this.createChatBotMessage("Hi, friend.")
    this.updateChatbotState(greetingMessage)
  }
  
  updateChatbotState(message) {
 
// NOTE: This function is set in the constructor, and is passed in      // from the top level Chatbot component. The setState function here     // actually manipulates the top level state of the Chatbot, so it's     // important that we make sure that we preserve the previous state.
 
    
   this.setState(prevState =&gt; ({
    	...prevState, messages: [...prevState.messages, message]
    }))
  }
}

export default ActionProvider</code></pre><p>现在我们在聊天框输入 “hello”，可以看到：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/accd248178f4bbfdd61212f4bcb9f42cc4729639/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f53637265656e73686f742d323032302d30362d31302d61742d31362e33392e34382e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f53637265656e73686f742d323032302d30362d31302d61742d31362e33392e34382e706e67" width="600" height="400" loading="lazy"></figure><p>很好！解析信息和响应都没有问题了。我们再做一些更复杂的东西，让机器人提供我们想要的编程语言学习资料。</p><h2 id="--2">创建一个学习机器人</h2><p>首先，回到 &nbsp;<code>config.js</code> &nbsp;文件，稍作修改：</p><pre><code>import { createChatBotMessage } from 'react-chatbot-kit';

const config = { 
  botName: "LearningBot",
  initialMessages: [createChatBotMessage("Hi, I'm here to help. What do you want to learn?")],
  customStyles: {
    botMessageBox: {
      backgroundColor: "#376B7E",
    },
    chatButton: {
      backgroundColor: "#376B7E",
    },
  },
}

export default config</code></pre><p>我们增加了一些属性，修改了初始信息，特别是给机器人取了个名字，更改了 &nbsp;<code>messagebox</code> &nbsp;和 &nbsp;<code>chatbutton</code> &nbsp;组件的颜色。</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/921a4b6f7ea36d0af067fb89218e3356e7ae916f/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f53637265656e73686f742d323032302d30362d32322d61742d30392e34342e33332e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f53637265656e73686f742d323032302d30362d32322d61742d30392e34342e33332e706e67" width="600" height="400" loading="lazy"></figure><p>好玩的部分来了。</p><p>我们不仅可以渲染信息和回复给用户，还可以根据想要的信息来自定义 React 组件。比如，我们创建一个选择组件，引导用户做不同选择。</p><p>首先，定义学习选项组件：</p><pre><code class="language-jsx">// in src/components/LearningOptions/LearningOptions.jsx

import React from "react";

import "./LearningOptions.css";

const LearningOptions = (props) =&gt; {
  const options = [
    { text: "Javascript", handler: () =&gt; {}, id: 1 },
    { text: "Data visualization", handler: () =&gt; {}, id: 2 },
    { text: "APIs", handler: () =&gt; {}, id: 3 },
    { text: "Security", handler: () =&gt; {}, id: 4 },
    { text: "Interview prep", handler: () =&gt; {}, id: 5 },
  ];

  const optionsMarkup = options.map((option) =&gt; (
    &lt;button
      className="learning-option-button"
      key={option.id}
      onClick={option.handler}
    &gt;
      {option.text}
    &lt;/button&gt;
  ));

  return &lt;div className="learning-options-container"&gt;{optionsMarkup}&lt;/div&gt;;
};

export default LearningOptions;

// in src/components/LearningOptions/LearningOptions.css

.learning-options-container {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
}

.learning-option-button {
  padding: 0.5rem;
  border-radius: 25px;
  background: transparent;
  border: 1px solid green;
  margin: 3px;
}</code></pre><p>然后在机器人代码中使用组件。对 &nbsp;<code>config.js</code> &nbsp;文件作如下操作：</p><pre><code class="language-jsx">import React from "react";
import { createChatBotMessage } from "react-chatbot-kit";

import LearningOptions from "./components/LearningOptions/LearningOptions";

const config = {
initialMessages: [
    createChatBotMessage("Hi, I'm here to help. What do you want to 		learn?", {
      widget: "learningOptions",
    }),
  ],
 ...,
 widgets: [
     {
     	widgetName: "learningOptions",
    	widgetFunc: (props) =&gt; &lt;LearningOptions {...props} /&gt;,
     },
 ],
}</code></pre><h3 id="-widgets">理解 widgets</h3><p>小结一下：</p><ul><li>我们创建了 &nbsp;<code>LearningOptions</code> &nbsp;组件</li><li>在 config 的 &nbsp;<code>widgets</code> &nbsp;下使用组件</li><li>给 &nbsp;<code>createChatbotMessage</code> &nbsp;函数一个选项对象，说明需要渲染哪个 widget 和信息</li></ul><p>结果：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/b620ee904c9f4b0120caba6b27f24fe4a4a2dff7/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f53637265656e73686f742d323032302d30362d32322d61742d31302e34312e34392e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f53637265656e73686f742d323032302d30362d32322d61742d31302e34312e34392e706e67" width="600" height="400" loading="lazy"></figure><p>很棒！但是，为什么要在 config 中以 widgets 的形式引入组件呢？</p><p>通过将其设置为函数，我们可以在调用时以聊天机器人的重要属性来装饰 widgets。</p><p>我们定义的 widgets 会接收到机器人的各种属性：</p><ul><li><code>actionProvider</code> &nbsp;- 将 &nbsp;<code>actionProvider</code> &nbsp;添加到 widgets，以执行动作</li><li><code>setState</code> &nbsp;- 将 &nbsp;<code>setState</code> &nbsp;添加到 widgets，以操作 state</li><li><code>scrollIntoView</code> &nbsp;- 滑动到聊天框底部，在需要调整视图时使用这个函数</li><li><code>props</code> &nbsp;- 给 widgets 定义的 props 将通过 &nbsp;<code>configProps</code> &nbsp;传递给 widgets</li><li><code>state</code> &nbsp;- 通过 &nbsp;<code>mapStateToProps</code> &nbsp;属性将自定义 state 传递给 widgets</li></ul><p>回头想想，我们给 &nbsp;<code>LearningOptions</code> &nbsp;组件设置了一些选项：</p><pre><code>  const options = [
    { text: "Javascript", handler: () =&gt; {}, id: 1 },
    { text: "Data visualization", handler: () =&gt; {}, id: 2 },
    { text: "APIs", handler: () =&gt; {}, id: 3 },
    { text: "Security", handler: () =&gt; {}, id: 4 },
    { text: "Interview prep", handler: () =&gt; {}, id: 5 },
  ];</code></pre><p>暂时这些选项有一个空的 handler，我们想调用 &nbsp;<code>actionProvider</code> &nbsp;替换 handler。</p><p>那么，我们想在执行这些函数的时候发生什么呢？理想状况下，机器人已经具有一些回复信息以及一个 widgets 显示每个主题对应的资源列表链接。我们看看怎么实现。</p><p>首先，创建一个链接列表组件：</p><pre><code class="language-jsx">// in src/components/LinkList/LinkList.jsx

import React from "react";

import "./LinkList.css";

const LinkList = (props) =&gt; {
  const linkMarkup = props.options.map((link) =&gt; (
    &lt;li key={link.id} className="link-list-item"&gt;
      &lt;a
        href={link.url}
        target="_blank"
        rel="noopener noreferrer"
        className="link-list-item-url"
      &gt;
        {link.text}
      &lt;/a&gt;
    &lt;/li&gt;
  ));

  return &lt;ul className="link-list"&gt;{linkMarkup}&lt;/ul&gt;;
};

export default LinkList;

// in src/components/LinkList/LinkList.css

.link-list {
  padding: 0;
}

.link-list-item {
  text-align: left;
  font-size: 0.9rem;
}

.link-list-item-url {
  text-decoration: none;
  margin: 6px;
  display: block;
  color: #1d1d1d;
  background-color: #f1f1f1;
  padding: 8px;
  border-radius: 3px;
  box-shadow: 2px 2px 4px rgba(150, 149, 149, 0.4);
}</code></pre><p>将这个组件添加到 widgets 中：</p><pre><code class="language-jsx">import React from "react";
import { createChatBotMessage } from "react-chatbot-kit";

import LearningOptions from "./components/LearningOptions/LearningOptions";
import LinkList from "./components/LinkList/LinkList";

const config = {
  ...
  widgets: [
    {
      widgetName: "learningOptions",
      widgetFunc: (props) =&gt; &lt;LearningOptions {...props} /&gt;,
    },
    {
      widgetName: "javascriptLinks",
      widgetFunc: (props) =&gt; &lt;LinkList {...props} /&gt;,
    },
  ],
};

export default config;
</code></pre><p>如果我们想动态给这个组件传递参数，以便对其他选项复用，那就需要给 widgets 添加另一个属性：</p><pre><code class="language-jsx">import React from "react";
import { createChatBotMessage } from "react-chatbot-kit";

import LearningOptions from "./components/LearningOptions/LearningOptions";
import LinkList from "./components/LinkList/LinkList";

const config = {
  ...,
  widgets: [
    ...,
    {
      widgetName: "javascriptLinks",
      widgetFunc: (props) =&gt; &lt;LinkList {...props} /&gt;,
      props: {
        options: [
          {
            text: "Introduction to JS",
            url:
              "https://www.freecodecamp.org/learn/javascript-algorithms-and-data-structures/basic-javascript/",
            id: 1,
          },
          {
            text: "Mozilla JS Guide",
            url:
              "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide",
            id: 2,
          },
          {
            text: "Frontend Masters",
            url: "https://frontendmasters.com",
            id: 3,
          },
        ],
      },
    },
  ],
};

export default config;
</code></pre><p>现在，这些 props 会作为参数传递给 &nbsp;<code>LinkList</code> &nbsp;组件。</p><p>我们再做两件事情。</p><ul><li>给 &nbsp;<code>actionProvider</code> &nbsp;添加一个方法</li></ul><pre><code class="language-jsx">class ActionProvider {
  constructor(createChatBotMessage, setStateFunc) {
    this.createChatBotMessage = createChatBotMessage;
    this.setState = setStateFunc;
  }

  handleJavascriptList = () =&gt; {
    const message = this.createChatBotMessage(
      "Fantastic, I've got the following resources for you on Javascript:",
      {
        widget: "javascriptLinks",
      }
    );

    this.updateChatbotState(message);
  };

  updateChatbotState(message) {
    // NOTICE: This function is set in the constructor, and is passed in from the top level Chatbot component. The setState function here actually manipulates the top level state of the Chatbot, so it's important that we make sure that we preserve the previous state.

    this.setState((prevState) =&gt; ({
      ...prevState,
      messages: [...prevState.messages, message],
    }));
  }
}

export default ActionProvider;
</code></pre><ul><li>把这个方法作为 &nbsp;<code>LearningOptions</code> &nbsp;组件 handler</li></ul><pre><code class="language-jsx">import React from "react";

import "./LearningOptions.css";

const LearningOptions = (props) =&gt; {
  const options = [
    {
      text: "Javascript",
      handler: props.actionProvider.handleJavascriptList,
      id: 1,
    },
    { text: "Data visualization", handler: () =&gt; {}, id: 2 },
    { text: "APIs", handler: () =&gt; {}, id: 3 },
    { text: "Security", handler: () =&gt; {}, id: 4 },
    { text: "Interview prep", handler: () =&gt; {}, id: 5 },
  ];

  const optionsMarkup = options.map((option) =&gt; (
    &lt;button
      className="learning-option-button"
      key={option.id}
      onClick={option.handler}
    &gt;
      {option.text}
    &lt;/button&gt;
  ));

  return &lt;div className="learning-options-container"&gt;{optionsMarkup}&lt;/div&gt;;
};

export default LearningOptions;
</code></pre><p>好啦，信息量比较大。现在如果我们点击聊天机器人的 JavaScript 按钮，会出现：</p><figure class="kg-card kg-image-card"><img src="https://camo.githubusercontent.com/7bbf25e3f370f5e75929ed4fcd065036341d98d2/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f53637265656e73686f742d323032302d30362d32322d61742d31372e33352e32372e706e67" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f53637265656e73686f742d323032302d30362d32322d61742d31372e33352e32372e706e67" width="600" height="400" loading="lazy"></figure><p>完美！再进一步，如果用户输入信息，机器人也应该响应。所以我们需要给 &nbsp;<code>MessageParser</code> &nbsp;创建新规则。</p><p>更新 &nbsp;<code>MessageParser.js</code> &nbsp;文件：</p><pre><code class="language-jsx">class MessageParser {
  constructor(actionProvider) {
    this.actionProvider = actionProvider;
  }

  parse(message) {
    const lowerCaseMessage = message.toLowerCase();

    if (lowerCaseMessage.includes("hello")) {
      this.actionProvider.greet();
    }

    if (lowerCaseMessage.includes("javascript")) {
      this.actionProvider.handleJavascriptList();
    }
  }
}

export default MessageParser;
</code></pre><p>在输入框键入 “javaScript”，机器人会回复同样的清单。完成啦！</p><p>欢迎在 GitHub 访问<a href="https://github.com/FredrikOseberg/react-chatbot-kit">代码和文档</a>。</p><h2 id="--3">结语</h2><p>创建项目很有趣，也是一个帮助你拓展技能的很棒的方式。你完全可以动动脑筋，在这个项目基础上再开发别的，比如一个机器人通过一些简单的问题找到网店里最适合的产品，或者是一个帮公司回复顾客常见问题的机器人。你可以尽量实践你的新想法。也欢迎你 pull request，帮我完善这个项目。</p><p>我觉得持续创建项目真的是开发者提升自己的唯一方式，建议你现在就动起来！</p><p>原文：<a href="https://www.freecodecamp.org/news/how-to-build-a-chatbot-with-react/">How to Build a Chatbot with React</a>，作者：Fredrik Strand Oseberg</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何提高编程能力？这里有项目开发创意 ]]>
                </title>
                <description>
                    <![CDATA[ 你是否曾经想开发一些东西但苦于无从下手？就像文学创作者会遭遇写作瓶颈，开发人员也不例外。 我跟我的朋友吉姆 Jim [https://twitter.com/jd_medlock]一起，创作了一个collection of application ideas [https://github.com/florinpop17/app-ideas]【应用创意想法集锦】，旨在一劳永逸地解决这个问题。 这些应用可以：  * 很好地提升你的编程技能  * 很好地接触新技术  * 成为你简历里打动下任老板或客户的经历  * 成为辅导材料（文章或者视频形式）里面的例子  * 很快完成，新性能扩展也十分容易 这不仅仅是一个项目的简单罗列。这份集锦详细地描述了每个项目，足够你从零开始。 每份项目规格包含：  1. 一个清晰描述对象  2. 需执行的用户需求清单（这些用户需求更像是一个行为准则而非必做事项。如果你有需要的话也可以根据自己的需要添加）  3. 追加选项清单。这个不仅可以改良基础项目，同时你的编程技巧也会有所提高  4. 所有能帮你发现完成项目所需物料的资源和链接 项目综述 根据完 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/here-are-some-app-ideas-you-can-build-to-level-up-your-coding-skills/</link>
                <guid isPermaLink="false">5f5f301ccd07b005bfb5b234</guid>
                
                    <category>
                        <![CDATA[ 项目 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 编程学习 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 自我提升 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miaomiao Ma ]]>
                </dc:creator>
                <pubDate>Mon, 14 Sep 2020 07:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/09/0_v3qXmKe1LTiiW_3H.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>你是否曾经想开发一些东西但苦于无从下手？就像文学创作者会遭遇写作瓶颈，开发人员也不例外。</p><p>我跟我的朋友吉姆 <a href="https://twitter.com/jd_medlock" rel="nofollow">Jim</a>一起，创作了一个<a href="https://github.com/florinpop17/app-ideas">collection of application ideas</a>【应用创意想法集锦】，旨在一劳永逸地解决这个问题。</p><p>这些应用可以：</p><ul><li>很好地提升你的编程技能</li><li>很好地接触新技术</li><li>成为你简历里打动下任老板或客户的经历</li><li>成为辅导材料（文章或者视频形式）里面的例子</li><li>很快完成，新性能扩展也十分容易</li></ul><p>这不仅仅是一个项目的简单罗列。这份集锦详细地描述了每个项目，足够你从零开始。</p><p>每份项目规格包含：</p><ol><li>一个清晰描述对象</li><li>需执行的用户需求清单（这些用户需求更像是一个行为准则而非必做事项。如果你有需要的话也可以根据自己的需要添加）</li><li>追加选项清单。这个不仅可以改良基础项目，同时你的编程技巧也会有所提高</li><li>所有能帮你发现完成项目所需物料的资源和链接</li></ol><h3 id="-">项目综述</h3><p>根据完成项目所需的知识储备和经验，所有的项目分为三个层级：</p><ol><li><strong>初级</strong> 针对的是刚刚起步的开发人员，特别是专注于开发面向用户应用的人员。</li><li><strong>中级</strong> 针对的是已经有学习和开发经验的老学员。他们在案例法过程中对用户界面和用户体验较为熟悉，会使用开发工具，会开发使用应用编程接口服务的应用。</li><li><strong>高级</strong> 针对的开发人员包含初级和中级提到的所有特点。他们还会额外学习高级技术，例如执行后端应用和数据库服务。</li></ol><p>接下来，每个层级中都会包含五个项目，总共十五个项目。但在我写文章时，在<a href="https://github.com/florinpop17/app-ideas">这个 Github 仓库</a>中共有三十多个项目。一定要确保你把这些项目都烂熟于心，因为未来我们计划增加更多的项目，非常欢迎你出一份力！（更多信息请关注下文的“贡献”部分）</p><h3 id="1-">1. 笔记应用</h3><p><strong>层级:</strong> 1-初级</p><p><strong>任务描述</strong>: 可以按照需求创建和存储笔记</p><h4 id="--1">用户需求</h4><ul><li>用户可以创建笔记</li><li>用户可以编辑笔记</li><li>用户可以删除笔记</li><li>浏览页面关闭时笔记可以自动存储；用户返回页面时，数据可以自行修复。</li></ul><h4 id="--2">追加选项</h4><ul><li>用户可以以Markdown格式创建和编辑笔记，存储以后内容会转换为HTML格式。</li><li>用户可以看见创建笔记的日期</li></ul><h4 id="--3">可以提供帮助的链接和资源</h4><ul><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage" rel="nofollow">localStorage</a></li><li><a href="https://www.markdownguide.org/basic-syntax/" rel="nofollow">Markdown Guide</a></li><li><a href="https://github.com/markedjs/marked">Marked — A markdown parser</a></li></ul><h3 id="--4">项目示例</h3><figure class="kg-card kg-embed-card"><iframe id="cp_embed_gbyygq" src="https://codepen.io/nickmoreton/embed/preview/gbyygq?height=300&amp;slug-hash=gbyygq&amp;default-tabs=css,result&amp;host=https://codepen.io" title="AngularJS Markdown Notes App" scrolling="no" frameborder="0" height="300" allowtransparency="true" class="cp_embed_iframe" style="width: 100%; overflow: hidden;" loading="lazy"></iframe></figure><h3 id="2-">2. 圣诞彩灯秀</h3><p><strong>层级:</strong> 1-初级</p><p><strong>任务描述</strong> ：圣诞彩灯秀应用需要利用你的编程开发能力来创造一个光彩炫目的灯光秀。你的任务是连续画出七个彩色光圈，然后根据时间变化每个光圈的亮度有所变化。一个光圈变亮时，前一个变亮的光圈恢复正常亮度。</p><p>在这个应用中，灯光就像涟漪一样一层一层荡漾开，好比圣诞节的灯光秀。</p><h4 id="--5">用户需求</h4><ul><li>用户可以通过按钮控制灯光秀</li><li>用户可以控制灯光秀亮度变化的时间间隔</li></ul><h4 id="--6">追加选项</h4><ul><li>用户可以选择每个光圈的颜色</li><li>用户可以控制光圈亮度</li><li>用户可以改变每个光圈大小</li><li>用户可以在1-7范围内设定灯光秀中的光圈个数</li></ul><h4 id="--7">可以提供帮助的链接资源</h4><ul><li><a href="https://previews.123rf.com/images/whiterabbit/whiterabbit1003/whiterabbit100300020/6582600-seven-color-balls-red-orange-yellow-green-cyan-blue-and-magenta-in-a-row-on-a-white-background.jpg" rel="nofollow">Sample Image</a></li><li><a href="https://cdn-shop.adafruit.com/970x728/1487-02.jpg" rel="nofollow">Adafruit LED Matrix</a></li></ul><h3 id="--8">项目实例</h3><figure class="kg-card kg-embed-card"><iframe id="cp_embed_QjvEex" src="https://codepen.io/tobyj/embed/preview/QjvEex?height=300&amp;slug-hash=QjvEex&amp;default-tabs=css,result&amp;host=https://codepen.io" title="Pure CSS Christmas Lights" scrolling="no" frameborder="0" height="300" allowtransparency="true" class="cp_embed_iframe" style="width: 100%; overflow: hidden;" loading="lazy"></iframe></figure><h3 id="3-">3. 图片翻转</h3><p><strong>层级:</strong> 1-初级</p><p><strong>任务描述</strong>: 因为大量应用依靠图片来呈现更丰富的用户界面、提供更棒的用户体验，所以对于开发人员来说，理解图片操作的基础非常重要。</p><p>图片翻转应用探索图片操作的一个方向-图片旋转。应用上显示一个方框，一张图片以2*2矩阵形式呈现。用户通过控制图片周围上下左右四个箭头来垂直或水平翻转图片。</p><p>必须使用原生HTML,CSS和Javascript语言来执行此应用。图像包和图像库均不允许使用。</p><h4 id="--9">用户需求</h4><ul><li>用户可以看见一个窗口，里面的单个图片一直以2*2矩阵分布</li><li>通过点击图片旁边的上下左右按钮，用户可以任意垂直或水平翻转任意图片</li></ul><h4 id="--10">追加选项</h4><ul><li>通过统一资源定位器（URL）查找别的图片，用户可以在输入栏更改默认图片</li><li>用户可以点击输入栏旁边的“秀”按钮，展示新图片</li><li>如果在统一资源定位器（URL）中没有找到新图片，用户可以看到错误信息提醒（error message）</li></ul><h4 id="--11">可以提供帮助的链接资源</h4><ul><li><a href="https://www.w3schools.com/howto/howto_css_flip_image.asp" rel="nofollow">How to Flip an Image</a></li><li><a href="https://davidwalsh.name/css-flip" rel="nofollow">Create a CSS Flipping Animation</a></li></ul><h3 id="--12">项目示例</h3><figure class="kg-card kg-embed-card"><iframe id="cp_embed_gvqYQv" src="https://codepen.io/seyedi/embed/preview/gvqYQv?height=300&amp;slug-hash=gvqYQv&amp;default-tabs=html,result&amp;host=https://codepen.io" title="Image Effects" scrolling="no" frameborder="0" height="300" allowtransparency="true" class="cp_embed_iframe" style="width: 100%; overflow: hidden;" loading="lazy"></iframe></figure><h3 id="4-">4. 问答应用</h3><p><strong>层级:</strong> 1-初级</p><p><strong>任务描述</strong>: 通过在问答应用上回答问题，来训练和测试你的知识存储。</p><p>作为一名开发人员，你可以开发一个能够测试其他开发人员编程知识的问答应用，诸如HTML, CSS, Javascript, Python, PHP知识等等。</p><h4 id="--13">用户需求</h4><ul><li>用点击按钮开始答题</li><li>用户面对的每个问题有四个答案选项</li><li>用户选中一道题的答案后，自动进行到下一题，直到整个问答结束</li><li>问答结束时，用户可以看到以下数据：</li></ul><ol><li>完成问答所耗时间</li><li>答对的问题个数</li><li>是否通过问答</li></ol><h4 id="--14">追加选项</h4><ul><li>用户可以在社交媒体分享问答结果</li><li>在应用上增加多份问答，用户可以决定做哪个问答</li><li>用户可以创建账户，存储所有得分结果。用户可以多次进行同一个问答</li></ul><h4 id="--15">可以提供帮助的链接资源</h4><ul><li><a href="https://opentdb.com/api_config.php" rel="nofollow">Open Trivia Database</a></li></ul><h3 id="--16">项目示例</h3><figure class="kg-card kg-embed-card"><iframe id="cp_embed_qqYNgW" src="https://codepen.io/FlorinPop17/embed/preview/qqYNgW?height=300&amp;slug-hash=qqYNgW&amp;default-tabs=css,result&amp;host=https://codepen.io" title="Quiz app interface" scrolling="no" frameborder="0" height="300" allowtransparency="true" class="cp_embed_iframe" style="width: 100%; overflow: hidden;" loading="lazy"></iframe></figure><p><a href="http://tranquil-beyond-43849.herokuapp.com/" rel="nofollow">Quiz app built with React</a> （应用已在 Heroku 上线，可下载）</p><h3 id="5-">5. 罗马-十进制数字转换器</h3><p><strong>层级:</strong> 1-初级</p><p><strong>任务描述</strong>: 罗马数字源于古罗马。直到中世纪后期，罗马数字系统一直是数字书写的主流，至今仍在使用。罗马数字包含七个符号，每个都有确定的整数值。</p><p>下表为罗马符号-整数值对应：</p><ul><li>I — 1</li><li>V — 5</li><li>X — 10</li><li>L — 50</li><li>C — 100</li><li>D — 500</li><li>M — 1000</li></ul><h4 id="--17">用户需求</h4><ul><li>用户可以在输入栏里输入一个罗马数字</li><li>点击按钮，用户可以在输出栏看见之前输入的罗马数字对应的十进制数字</li><li>如果输入罗马符号错误，用户可以看见错误提示</li></ul><h4 id="--18">追加选项</h4><ul><li>用户可以看见转换过程自动完成</li><li>用户可以完成十进制-罗马数字的逆过程转换</li></ul><h4 id="--19">可提供帮助的链接资源</h4><ul><li><a href="https://en.wikipedia.org/wiki/Roman_numerals" rel="nofollow">An explanation of Roman Numbers</a></li></ul><h4 id="--20">项目示例</h4><p><a href="https://www.calculatorsoup.com/calculators/conversions/roman-numeral-converter.php" rel="nofollow">Roman Number Converter</a></p><h3 id="6-">6. 寻书应用</h3><p><strong>层级:</strong> 2-中级</p><p><strong>任务描述</strong>: 创建一个用户可以搜寻书的应用。用户在输入相关书名、作者等信息后，网页上会排列出现所有相关书籍。</p><h4 id="--21">用户需求</h4><ul><li>用户可以在输入栏输入要查询的信息</li><li>用户可以提交查询信息。这就叫做应用编程接口（API），返回结果是与所有输入信息（例如名称，作者，出版日期，图像等）相关联的书籍</li><li>用户可以在页面上看到搜索出来的书籍清单</li></ul><h4 id="--22">追加选项</h4><ul><li>搜索清单上每个书籍条目要增加一个用户可以直达的外部站点链接，以便得到更多关于书的信息。</li><li>执行一个响应设计</li><li>增加预载动画</li></ul><h4 id="--23">可提供帮助的链接资源</h4><p><a href="https://developers.google.com/books/docs/overview" rel="nofollow">Google Books API</a></p><h4 id="--24">项目示例</h4><figure class="kg-card kg-embed-card"><iframe id="cp_embed_wpQBKV" src="https://codepen.io/chasebank/embed/preview/wpQBKV?height=300&amp;slug-hash=wpQBKV&amp;default-tabs=css,result&amp;host=https://codepen.io" title="Vue, Axios and Google Books" scrolling="no" frameborder="0" height="300" allowtransparency="true" class="cp_embed_iframe" style="width: 100%; overflow: hidden;" loading="lazy"></iframe></figure><p><a href="https://fethica.github.io/BookSearch-React/" rel="nofollow">BookSearch-React</a></p><h3 id="7-">7. 卡片记忆游戏</h3><p><strong>层级:</strong> 2-中级</p><p><strong>任务描述</strong>: 在卡片记忆游戏中，你需要点击一张卡片看是什么图形，然后努力在其余卡片中发现匹配的图形。</p><h4 id="--25">用户需求</h4><ul><li>用户可以看到一个包含有n*n张卡片的方格（n是整数）。所有的卡片一开始都是图形面朝下的状态（隐藏状态）</li><li>用户点击按钮开始游戏，同时计时器开始计时</li><li>用户可以点击任意一张卡片查看图形，这张卡片就处于可见状态，一直持续到用户点击第二张卡片</li></ul><p>用户点击第二张卡片后：</p><ul><li>如果两张卡片图形匹配，这两张卡片就会消失（或是隐藏或是移除，也可以让他俩处于可见状态）</li><li>如果跟第一张卡片图形不匹配，两张卡片就会恢复初始 隐藏状态</li><li>所有匹配结束以后，用户可以看见对话框显示“祝贺”信息，同时也会显示完成游戏所耗费的时间</li></ul><h4 id="--26">追加选项</h4><ul><li>用户可以自己选择游戏难度等级（易中难）。难度增加意味着：完成游戏的规定时间缩短或者图片个数增加</li><li>用户可以看见游戏数据（不是输赢次数，而是每个难度所对应的最好成绩）</li></ul><h4 id="--27">可提供帮助的链接资源</h4><ul><li><a href="https://en.wikipedia.org/wiki/Concentration_(game)" rel="nofollow">Wikipedia</a></li></ul><h4 id="--28">项目示例</h4><p><a href="https://codepen.io/zerospree/full/bNWbvW" rel="nofollow">Flip — card memory game</a></p><p><a href="https://codepen.io/hexagoncircle/full/OXBJxV" rel="nofollow">SMB3 Memory Card Game</a></p><h3 id="8-markdown-">8. Markdown表格生成器</h3><p><strong>层级:</strong> 2-中级</p><p><strong>任务描述</strong>: 创建一个可以将包含用户数据的常规表格（或者不包含，看用户需求）转换成Markdown形式表格的应用</p><h4 id="--29">用户需求</h4><ul><li>用户可以创建包含有特定行数或列数的HTML表格</li><li>用户可以在HTML表格中的每个单元格插入文本</li><li>用户可以生成包含有HTML表格数据的Markdown形式表格</li><li>用户可以预览Markdown形式表格</li></ul><h4 id="--30">追加选项</h4><ul><li>用户可以一键复制Markdown形式表格到剪切板</li><li>用户可以在特定区域插入行或者列</li><li>用户可以彻底删除某行某列</li><li>用户可以将某个单元格、某列、某行向左、向右或者居中对齐。</li></ul><h4 id="--31">可以提供帮助的链接资源</h4><ul><li><a href="https://www.markdownguide.org/" rel="nofollow">Markdown Guide</a></li><li><a href="https://github.com/markedjs/marked">Marked — A markdown parser</a></li><li><a href="https://www.w3schools.com/howto/howto_js_copy_clipboard.asp" rel="nofollow">How to Copy to Clipboard</a></li></ul><h4 id="--32">项目示例</h4><p><a href="https://www.tablesgenerator.com/markdown_tables" rel="nofollow">Tables Generator / Markdown Tables</a></p><h3 id="9-">9. 弦乐艺术</h3><p><strong>层级:</strong> 2-中级</p><p><strong>任务描述</strong>: 弦乐艺术应用的目的在于让开发人员练习，创建简便动画图表，在动画算法中运用几何知识，创造出富有视觉美感的画面。</p><p>弦乐艺术中，会有一条五彩斑斓的线条平稳地移动，直到它的一端碰到封闭窗口的一边。此时，线条就会因为反弹效应改变方向。</p><p>线条移动时，如果可以保留10-20张线条移动时的图形，便会出现涟漪效应。稍早出现的图形会慢慢消失不见。</p><p>不可以使用动画库。只可以使用Vanilla HTML/CSS/Javascript.</p><h4 id="--33">用户需求</h4><ul><li>在封闭窗口界限内的任意位置，用户以画一条五彩斑斓的线开始</li><li>每隔二十毫秒，在别的位置，按照前一条线条的轨迹复制一条-以端点作为标记，距离前一线条要越来越远</li><li>不论线条的哪个端点碰到封闭窗口的边界，都要改变线条的方向，角度随机改变</li><li>慢慢降低起初画的线条的亮度，确保最近画的10-20线条可见，这样才可以呈现动感或者涟漪效应</li></ul><h4 id="--34">追加选项</h4><ul><li>用户可以设定线条的长度和移动速度</li><li>用户可以设定窗口内的线条数量，所有线条可以按照不同轨迹和速度移动</li></ul><h4 id="--35">可以提供帮助的链接资源</h4><ul><li><a href="https://css-tricks.com/using-multi-step-animations-transitions/" rel="nofollow">Using Multistep Animations &amp; Transitions</a></li><li><a href="https://www.khanacademy.org/computing/computer-programming/programming/animation-basics/a/what-are-animations" rel="nofollow">Animation Basics</a></li></ul><h4 id="--36">项目示例</h4><p>这个项目非常封闭，包含一个小窗口，很单一。<a href="https://codepen.io/dgca/pen/dpxreO" rel="nofollow">Daniel Cortes</a></p><h3 id="10-">10. 计划清单应用</h3><p><strong>层级:</strong> 2-中级</p><p><strong>任务描述</strong>: 经典的计划清单应用：用户可以记下所有的待办事项。</p><h4 id="--37">用户需求</h4><ul><li>用户可以看见输入框，从而输入待办事项</li><li>点击“进入”按钮，用户可以提交待办事项，并且看见其已经进入待办事项清单</li><li>用户可以在待办事项上标注“已完成”</li><li>用户可以通过点击按钮取消待办事项，或者是在待办事项条目上直接操作</li></ul><h4 id="--38">追加选项</h4><ul><li>用户可以编辑待办事项</li><li>用户可以看见所有已完成待办事项的清单</li><li>用户可以看见所有自己创立的待办事项清单</li><li>用户可以看见创立待办事项的日期</li><li>关闭浏览窗口时，待办事项可以自动保存；用户返回页面时，数据可自行恢复</li></ul><h4 id="--39">可提供帮助的链接资源</h4><ul><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage" rel="nofollow">localStorage</a></li></ul><h4 id="--40">项目示例</h4><figure class="kg-card kg-embed-card"><iframe id="cp_embed_eJIuF" src="https://codepen.io/yesilfasulye/embed/preview/eJIuF?height=300&amp;slug-hash=eJIuF&amp;default-tabs=css,result&amp;host=https://codepen.io" title="To Do List" scrolling="no" frameborder="0" height="300" allowtransparency="true" class="cp_embed_iframe" style="width: 100%; overflow: hidden;" loading="lazy"></iframe></figure><p>计划清单<a href="http://todomvc.com/examples/react/#/" rel="nofollow">Todo App built with React</a></p><h3 id="11-">11. 战舰游戏引擎</h3><p><strong>层级:</strong> 3-高级</p><p><strong>任务描述</strong>: 战舰游戏引擎采用了回合制棋盘游戏的模式，与所有表示层（presentation layer）分离。它属于一种建筑模式类型，因为其允许无数的应用使用同一种服务，这种模式在很多应用上都十分有用。</p><p>战舰游戏引擎本身是一系列函数调用的结果，而非终端用户直接动作产生。从这个方面讲，使用战舰游戏引擎，与使用应用编程接口或者使用网络浏览器公开的一系列线路是相似的。</p><p>这个挑战不仅要求你开发出战舰游戏引擎，还要求你开发出非常精细的、基于文本的表示层，以检测表示层与引擎互不干涉。因此，用户需求包含两套：战舰游戏引擎一套，基于文本的表示层一套</p><p>战舰游戏引擎需要负责维持整个游戏的状态。</p><h4 id="--41">用户需求</h4><h4 id="--42">战舰游戏引擎</h4><ul><li>召集者调用<code>startGame()</code>函数开始单人游戏。此函数可以创立一个8*8的游戏阵地，阵地上有三艘船，他们的尺寸为：</li></ul><ol><li>驱逐舰：一方格宽，两方格长</li><li>巡洋舰：一方格宽，三方格长</li><li>战舰：一方格宽，四方格长</li></ol><p><code>startGame()</code>函数会随机地把这些战船放在阵地的任意方向，随后给出船的部署情况。</p><ul><li>召集者调用<code>shoot()</code>函数可以袭击阵地上的特定行和列构成的方格坐标。<code>shoot()</code>函数还会显示袭击是否成功，剩余战船数量，战船安置排列，以及更新袭击成功或者失败数据。</li></ul><p>某个方格被设为攻击目标后，会有所提示。 <code>O</code>代表方格被瞄准，但此地没有战船；<code>X</code>代表此地有战船。</p><h4 id="--43">基于文本的表示层</h4><ul><li>通过返回<code>startGame()</code>函数，用户可以看见命中或者未命中数值在阵地上以二维字符展示。</li><li>用户可以受鼓舞进入阵地上特定坐标。</li><li>在袭击之后，用户可以看见命中或未命中数值更新。</li><li>袭击结束后，不论命中与否，用户都可以看见信息提示。</li><li>在击中最后一艘战船时，用户可以看见“祝贺”信息</li><li>每局游戏结束后，用户都可以受鼓舞再来一局。拒绝再来一局则会结束游戏。</li></ul><h4 id="--44">追加选项</h4><h4 id="bge-">BGE战舰游戏引擎</h4><ul><li>召集者可以指定阵地上行与列的具体数值，这也是 <code>startGame()</code>函数的参数。</li><li>召集者可以调用<code>gameStats()</code> 函数，返回到Javascript对象，显示最近游戏数据，比如打了几局，命中数等。</li><li>在调用<code>startGame()</code>函数时，召集者可以指定玩家数量。此函数也可以为每个玩家创立阵地，随机安排战舰数量。</li></ul><p><code>shoot()</code>函数会计算做出特定坐标袭击的玩家数量。函数返回的数据针对某个特定玩家。</p><h4 id="--45">基于文本的表示层</h4><ul><li>用户在目标坐标进入<code>stats</code>相位，可以随时查看最近游戏数据。（注意此项操作需要战舰游戏引擎的<code>gameStats()</code>函数）</li><li>用户可以指定两人游戏模式，每个玩家在同一终端会话均可更改比赛回合。（注意此项操作需要战舰游戏引擎追加选项的相应配合）</li><li>用户在每个回合的输入提示中都可以看到玩家数量。</li><li>用户在每个回合结束时可以看见两个玩家的阵地。</li></ul><h4 id="--46">可以提供帮助的链接资源</h4><ul><li><a href="https://en.wikipedia.org/wiki/Battleship_(game)" rel="nofollow">Battleship Game (Wikipedia)</a></li><li><a href="https://www.hasbro.com/common/instruct/battleship.pdf" rel="nofollow">Battleship Game Rules (Hasbro)</a></li></ul><h4 id="--47">项目示例</h4><p>YouTobe上的这个视频展示了基于文本的战舰游戏是怎么玩的。<a href="https://www.youtube.com/watch?v=TKksu3JXTTM" rel="nofollow">Battleship Game</a></p><p>如果你对战舰游戏不熟悉，下面这个例子就是一个范例。记住你要实施一个基于文本的表示层用于测试。<a href="https://codepen.io/CodifyAcademy/pen/ByBEOz" rel="nofollow">Battleship Game by Chris Brody</a></p><h3 id="12-">12. 聊天应用</h3><p><strong>层级:</strong> 3-高级</p><p><strong>任务描述</strong>: 此项任务是创建一个可以支持多名用户互相发送信息的 实时聊天互动界面。 作为一个最小可行化产品，你可以关注聊天界面开发。实时性可作为追加选项稍后添加。</p><h4 id="--48">用户需求</h4><ul><li>用户在浏览聊天应用时根据提示输入用户名，用户名会在应用中保存</li><li>用户可在输入框中输入新信息</li><li>通过点击“进入”或者“发送”按钮，文本会出现在用户名旁边的聊天框中（比如： <code>John Doe: Hello World!</code>）</li></ul><h4 id="--49">追加选项</h4><ul><li>信息对于聊天应用（采用WebSockets）的所有用户可见</li><li>每当有一名新用户加入时，所有老用户都会受到提示信息</li><li>所有信息会存储在一个数据库中</li><li>用户可以发送图片，视频和链接。所有这些会以合适的形式展示</li><li>用户可以挑选，然后发送表情。</li><li>用户可以私聊</li><li>用户可以加入特定话题的频道</li></ul><h4 id="--50">可以提供帮助的链接资源</h4><ul><li><a href="https://socket.io/" rel="nofollow">Socket.io</a></li><li><a href="https://medium.freecodecamp.org/how-to-build-a-react-js-chat-app-in-10-minutes-c9233794642b" rel="nofollow">How to build a React.js chat app in 10 minutes — article</a></li></ul><h4 id="--51">项目示例</h4><figure class="kg-card kg-embed-card"><iframe id="cp_embed_ZWEdZj" src="https://codepen.io/iremlopsum/embed/preview/ZWEdZj?height=300&amp;slug-hash=ZWEdZj&amp;default-tabs=css,result&amp;host=https://codepen.io" title="Simple chat app using firebase" scrolling="no" frameborder="0" height="300" allowtransparency="true" class="cp_embed_iframe" style="width: 100%; overflow: hidden;" loading="lazy"></iframe></figure><p><a href="https://web-chatty.herokuapp.com/" rel="nofollow">Chatty2</a></p><h3 id="13-github-">13. GitHub时间线</h3><p><strong>层级:</strong> 3-高级</p><p><strong>任务描述</strong>: 应用程序接口和信息图示是当代应用的特点。 GitHub时间线整合这两个特点，力求创造一个用户GitHub活动的可视历史。</p><p>GitHub时间线的目标是，存储GitHub用户的姓名，制作一条时间线，包含每个存储库，像他们的名字，创立的时间，和相关描述。未来可以将时间线分享给老板。时间线需易于阅读，可以有效利用颜色和字体排版。</p><p>只有面向公众的存储库可以展示。</p><h4 id="--52">用户需求</h4><ul><li>用户可以提交GitHub用户名</li><li>用户可以通过点击“生成”按钮创建和展示已命名的用户存储库时间线</li><li>如果用户名在GitHub中无效的话，用户可以看到提示信息。</li></ul><h4 id="--53">追加选项</h4><ul><li>用户可以看见当年创建的存储库数量总结</li></ul><h4 id="--54">可以提供帮助的链接资源</h4><p>GitHub提供了两个你们可能用到的接触存储库数据的应用程序接口。你也可以采用Node包管理器（NPM）接触GitHub应用程序接口。</p><p>GitHub应用程序接口说明可以在以下两个链接中找到：</p><ul><li><a href="https://developer.github.com/v3/">GitHub REST API V3</a></li><li><a href="https://developer.github.com/v4/">GitHub GraphQL API V4</a></li></ul><p>使用GitHub应用程序接口的样本代码如下：</p><p>你可以用CURL命令看V3 REST应用程序接口返回的JSON，这个程序接口就跟你的存储库有关：</p><p><code>curl -u "user-id" https://api.github.com/users/user-id/repos</code></p><h4 id="--55">项目示例</h4><figure class="kg-card kg-embed-card"><iframe id="cp_embed_FemfK" src="https://codepen.io/NilsWe/embed/preview/FemfK?height=300&amp;slug-hash=FemfK&amp;default-tabs=css,result&amp;host=https://codepen.io" title="CSS Timeline" scrolling="no" frameborder="0" height="300" allowtransparency="true" class="cp_embed_iframe" style="width: 100%; overflow: hidden;" loading="lazy"></iframe></figure><figure class="kg-card kg-embed-card"><iframe id="cp_embed_QNeJgR" src="https://codepen.io/tutsplus/embed/preview/QNeJgR?height=300&amp;slug-hash=QNeJgR&amp;default-tabs=css,result&amp;host=https://codepen.io" title="Building a Vertical Timeline With CSS and a Touch of JavaScript" scrolling="no" frameborder="0" height="300" allowtransparency="true" class="cp_embed_iframe" style="width: 100%; overflow: hidden;" loading="lazy"></iframe></figure><h3 id="14-">14. 拼读拼写</h3><p><strong>层级:</strong> 3-高级</p><p><strong>任务描述</strong>: 懂得如何拼读拼写拼写是流利掌握每门语言的基础要求。不论你是牙牙学语的孩童或者正在接触一门新语言的个体，练习拼读拼写会强化你的语言技能。</p><p>拼读拼写应用播放单词录音，用户用电脑键盘将单词拼写出来，从而帮助用户锻炼拼读拼写能力</p><h4 id="--56">用户需求</h4><ul><li>用户点击“播放”按钮，听到要拼写的单词录音</li><li>用户在键盘上打完单词后，可以在输入框里看到他们打出的单词</li><li>用户点击“输入”健提交已在输入框里的单词</li><li>如果提交单词正确，用户可以看到确认信息</li><li>如果提交单词错误，应用提示用户再次输入单词</li><li>用户可以看到拼写正确的单词总数，拼写过的单词总数和成功提交的单词占比。</li></ul><h4 id="--57">追加选项</h4><ul><li>单词拼写正确时用户可以听到确认声音</li><li>单词拼写错误时用户可以听到警示声音</li><li>用户可以点击“提示”按钮查看输入框中哪些字母出错</li><li>用户可以敲键盘上的“输入”健或者点击窗口“提交”按钮提交单词</li></ul><h4 id="--58">可以提供帮助的链接资源</h4><ul><li><a href="https://en.wikipedia.org/wiki/Speak_%26_Spell_(toy)" rel="nofollow">Texas Instruments Speak and Spell</a></li><li><a href="https://codepen.io/2kool2/full/RgKeyp" rel="nofollow">Web Audio API</a></li><li><a href="https://codepen.io/shangle/full/Wvqqzq" rel="nofollow">Click and Speak</a></li></ul><h4 id="--59">项目示例</h4><p><a href="https://itunes.apple.com/app/id447312716" rel="nofollow">Word Wizard for iOS</a></p><p><a href="https://play.google.com/store/apps/details?id=au.id.weston.scott.SpeakAndSpell&amp;hl=en_US" rel="nofollow">Speak N Spell on Google Play</a></p><h3 id="15-">15. 调查应用</h3><p><strong>层级:</strong> 3-高级</p><p><strong>任务描述</strong>: 调查应用可谓开发人员工具箱必备。通过这些应用，开发人员可以获得用户的各种反馈，例如对应用满不满意、有没有新要求或者新需求，急需解决的问题是什么，有没有一些变得严重的问题。</p><p>全功能调查应用的开发给你机会让你去学习，看你会不会把自己开发的应用加入工具箱。它也帮助你提升多方面的能力，如如何定义调查；让用户在预先设定的时间内回应；将结果做成表格并且展示。</p><p>应用的用户可以鲜明地分为两组，每组都有不同的要求：</p><ul><li>调查协调组--制定并实施调查。他们有普通用户没有的管理员权限。</li><li>调查受访者--完成调查并查看结果。在应用内部他们没有管理员权限。</li></ul><p>商业调查工具包括分布功能，以此公众会发调查邮件给调查受访者。简单说来，应用网页可以直达对回应开放的调查。</p><h4 id="--60">用户需求</h4><h4 id="--61">综述</h4><ul><li>调查协调组和调查受访组可以在一般网站上制定、实施、查看调查和调查结果。</li><li>调查协调可以登录应用获得管理员权限，比如可以制定调查。</li></ul><h4 id="--62">制定调查</h4><ul><li>调查协调组负责制定调查，其中包含有1-10个多项选择问题。</li><li>调查协调组可以在每个问题下设定1-5个相互排斥的选项。</li><li>调查协调组可以给调查设定题目。</li><li>调查协调组可以点击“撤销”按钮，不保存调查，返回主页。</li><li>调查协调组可以点击“保存”按钮保存调查。</li></ul><h4 id="--63">实施调查</h4><ul><li>调查协调组可以从先前设定好的调查中选择一个调查并打开</li><li>调查协调组可以从已打开的调查中选择一个关闭</li><li>调查受访组可以从已打开的调查中选择一个完成</li><li>调查受访组可以通过点击复选框选择调查问题的回答</li><li>调查受访组可以看见，如果一道题在第二次选择时选择了跟第一次不同的答案，那先前的答案会自动消除</li><li>调查受访组可以点击“撤销”按钮不提交调查，返回主页</li><li>调查受访组可以点击“提交”按钮提交回答</li><li>如果调查未完成就点击“提交”按钮提交，调查受访组会看到提示“错误”信息</li></ul><h4 id="--64">查看调查结果</h4><ul><li>调查协调组和受访组可以从已关闭的调查中选择调查展示</li><li>调查协调组和受访组在表格形式下查看调查结果，表格可以显示每个问题的每个答案有多少人选择</li></ul><h4 id="--65">追加选项</h4><ul><li>调查受访组可以在应用中创立一个独有账户</li><li>调查受访组可以登录应用</li><li>调查受访组每个调查只能做一次</li><li>调查协调组和受访组可以查看以图表形式呈现的调查结果（类似饼状图，条形图等表格）</li></ul><h4 id="--66">可以提供帮助的链接资源</h4><p>制定调查的图书馆资源库： <a href="https://surveyjs.io/Overview/Library/" rel="nofollow">SurveyJS</a></p><p>商业调查服务包括： <a href="https://www.surveymonkey.com/" rel="nofollow">Survey Monkey</a> 和 <a href="https://www.typeform.com/" rel="nofollow">Typeform</a></p><h4 id="--67">项目示例</h4><figure class="kg-card kg-embed-card"><iframe id="cp_embed_oLChg" src="https://codepen.io/amyfu/embed/preview/oLChg?height=300&amp;slug-hash=oLChg&amp;default-tabs=js,result&amp;host=https://codepen.io" title="Javascript Questionnaire" scrolling="no" frameborder="0" height="300" allowtransparency="true" class="cp_embed_iframe" style="width: 100%; overflow: hidden;" loading="lazy"></iframe></figure><h3 id="--68">贡献</h3><p>非常欢迎大家在 <a href="https://github.com/florinpop17/app-ideas">GitHub 仓库</a>中为这个项目做出一些贡献！任何形式任何贡献我们都非常欢迎~</p><p>贡献有两种方式：</p><ol><li>你可以创立一个新话题并且告诉我们你的想法。一定要确保你采用了 <strong>新想法</strong>标签；</li><li>将项目拆分，并且提交一份性能要求。在做这个工作之前，要确保你已经读过贡献指南，并且你也是这么做的（在存储库中你可以找到贡献指南）；</li></ol><h4 id="--69">添加你的个人实例项目</h4><p>在你完成项目以后，一也可以把自己做出来的成果添加到项目上。非常鼓励你们这样做，这也向其他人展示了你创造出了多么精彩的应用！</p><h3 id="--70">帮忙扩散我们的文章</h3><p>如果这片文章或者存储库中的信息对你有所帮助，一定要记得给个星星哦，这样其他人才能找到它，从中受益。让我们一起成长，把我们的社区建立得更好。</p><p>你有没有什么建议能帮我们总体提升我们的项目吗？有的话不要谦虚哦，非常期待你的反馈。</p><h4 id="--71">主要作者</h4><p><strong>Florin Pop</strong>: <a href="https://twitter.com/florinpop1705" rel="nofollow">Twitter</a> &amp; <a href="https://florin-pop.com/" rel="nofollow">website</a>.</p><p><strong>Jim Medlock</strong>: <a href="https://twitter.com/jd_medlock" rel="nofollow">Twitter</a> &amp; <a href="https://medium.com/@jdmedlock" rel="nofollow">Medium</a></p><h3 id="--72"><strong>每周编程挑战</strong></h3><p>作为额外福利，<a href="https://www.florin-pop.com/blog/2019/03/weekly-coding-challenge/">这里</a>有一个每周编程挑战，你可以通过实战项目训练切实提升自己的编程技巧。</p><p>原文：<a href="https://www.freecodecamp.org/news/here-are-some-app-ideas-you-can-build-to-level-up-your-coding-skills-39618291f672/">Here are some app ideas you can build to level up your coding skills</a>，作者：Florin Pop</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ React 项目实践——搭建一个温度控制 App ]]>
                </title>
                <description>
                    <![CDATA[ 这个 React 项目面向初学者。我们将搭建一个温度控制 App，了解 state hook、handle 事件等等。 尝试自己搭建 如果你想先自己试着写这个项目，可以对照下列项目需求（你也可以参考下方的初始代码）：  * 当用户点击“+”按钮的时候，温度上升  * 温度不能高于 30℃  * 当用户点击“-”按钮的时候，温度降低  * 温度不能低于 0℃  * 当温度高于 15℃ 的时候，背景色变成红色（我创建了一个样式“hot”）  * 当温度高于 15℃ 的时候，背景色变成蓝色（我创建了一个样式“cold”） 初始代码 注：本文默认你已经安装好 React 开发环境。 首先在终端运行 create-react-app： npx create-react-app temperature-control 同时在 VS Code（或者别的编辑器）打开项目。删除 index.js 里的内容，然后将以下代码粘贴进这个文件： import React from ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/react-beginner-project-tutorial-temperature-control-app/</link>
                <guid isPermaLink="false">5f4fab08cd07b005bfb5adc3</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 项目 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chengjun.L ]]>
                </dc:creator>
                <pubDate>Thu, 03 Sep 2020 09:03:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/09/Build-a-Temperature-control-App.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>这个 React 项目面向初学者。我们将搭建一个温度控制 App，了解 state hook、handle 事件等等。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/09/project-1.gif" class="kg-image" alt="project-1" width="600" height="400" loading="lazy"></figure><h2 id="-"><strong><strong>尝试自己搭建</strong></strong></h2><p>如果你想先自己试着写这个项目，可以对照下列项目需求（你也可以参考下方的初始代码）：</p><ul><li>当用户点击“+”按钮的时候，温度上升</li><li>温度不能高于 30℃</li><li>当用户点击“-”按钮的时候，温度降低</li><li>温度不能低于 0℃</li><li>当温度高于 15℃ 的时候，背景色变成红色（我创建了一个样式“hot”）</li><li>当温度高于 15℃ 的时候，背景色变成蓝色（我创建了一个样式“cold”）</li></ul><h2 id="--1"><strong><strong>初始代码</strong></strong></h2><p>注：本文默认你已经安装好 React 开发环境。</p><p>首先在终端运行 <strong><strong>create-react-app</strong></strong>：</p><pre><code class="language-js">npx create-react-app temperature-control
</code></pre><p>同时在 VS Code（或者别的编辑器）打开项目。删除 <strong><strong>index.js</strong> </strong>里的内容，然后将以下代码粘贴进这个文件：</p><pre><code class="language-jsx">import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';

ReactDOM.render(
	&lt;React.StrictMode&gt;
		&lt;App /&gt;
	&lt;/React.StrictMode&gt;,
	document.getElementById('root')
);
</code></pre><p>同样，删除 <strong><strong>index.css</strong> </strong>的内容，粘贴以下内容：</p><pre><code class="language-css">body {
	font-family: sans-serif;
	text-align: center;
	display: flex;
	flex-direction: column;
	justify-content: center;
	align-items: center;
	text-align: center;
	min-height: 100vh;
}

.app-container {
	height: 400px;
	width: 300px;
	background: #2b5870;
	border-radius: 20px;
	box-shadow: 10px 10px 38px 0px rgba(0, 0, 0, 0.75);
}

.temperature-display-container {
	display: flex;
	justify-content: center;
	align-items: center;
	height: 70%;
}

.temperature-display {
	display: flex;
	border-radius: 50%;
	color: #ffffff;
	height: 220px;
	width: 220px;
	text-align: center;
	justify-content: center;
	align-items: center;
	font-size: 48px;
	border: 3px #ffffff solid;
	transition: background 0.5s;
}

button {
	border-radius: 100px;
	height: 80px;
	width: 80px;
	font-size: 32px;
	color: #ffffff;
	background: rgb(105, 104, 104);
	border: 2px #ffffff solid;
}

button:hover {
	background: rgb(184, 184, 184);
	cursor: pointer;
}

button:focus {
	outline: 0;
}

.button-container {
	display: flex;
	justify-content: space-evenly;
	align-items: center;
}

.neutral {
	background: rgb(184, 184, 184);
}

.cold {
	background: #035aa6;
}

.hot {
	background: #ff5200;
}
</code></pre><p>最后，删除 <strong><strong>App.js</strong> </strong>的内容，粘贴以下代码：</p><pre><code class="language-jsx">import React from 'react';

const App = () =&gt; {
	return (
		&lt;div className='app-container'&gt;
			&lt;div className='temperature-display-container'&gt;
				&lt;div className='temperature-display'&gt;10°C&lt;/div&gt;
			&lt;/div&gt;
			&lt;div className='button-container'&gt;
				&lt;button&gt;+&lt;/button&gt;
				&lt;button&gt;-&lt;/button&gt;
			&lt;/div&gt;
		&lt;/div&gt;
	);
};

export default App;
</code></pre><p>现在我们可以在 VS Code 打开终端，运行：</p><pre><code class="language-js">npm start
</code></pre><p>如果一切无误，将显示：</p><figure class="kg-card kg-image-card"><img src="https://d33wubrfki0l68.cloudfront.net/45824af046bb04e327540bada2a1d40195df999a/fba86/static/1001b5afc2ce9716db3d331a43dc2327/f8915/starter.png" class="kg-image" alt="starter" width="600" height="400" loading="lazy"></figure><p>好棒！接下来我们可以基于这个模版搭建了，不用担心 CSS 部分。</p><h2 id="-state"><strong><strong>动态显示温度值——使用</strong> State</strong></h2><p>首先我们让温度值动态显示。将温度值存储在 state 内，便于我们稍后读取数据，并用于逻辑呈现。</p><p><em><em>建议把引起</em> UI 改变的东西都放在 state 里。</em></p><p>在 <strong><strong>App.js</strong></strong> 文件开头导入 <strong><strong>useState</strong></strong> hook：</p><pre><code class="language-jsx">import React, { useState } from 'react';
</code></pre><p>在 <strong><strong>App function</strong> </strong>函数内添加：</p><pre><code class="language-jsx">const [temperatureValue, setTemperatureValue] = useState(10);
</code></pre><p>我们通过 <strong><strong>useState</strong> </strong>进行组件状态管理。<strong><strong>useState</strong></strong> hook 包含两个参数：</p><ul><li>一个表示状态初始值的变量</li><li>一个更新状态值的函数</li></ul><p>在这个例子中，我们调用了状态变量 <strong><strong>temperatureValue</strong> </strong>和函数 <strong><strong>setTemperatureValue</strong></strong>，将 10 这个值传递给 useState hook，作为 temperatureValue 的初始值。</p><p>现在我们把这个状态值用到代码里。记住了，我们从 <strong><strong>useState</strong></strong> 获取的值的用法和其他 JavaScript 变量和函数的用法一样。</p><p>将 JSX 里的固定的温度值改为状态变量。这是原来的值：</p><pre><code class="language-jsx">&lt;div className='temperature-display'&gt;10°C&lt;/div&gt;
</code></pre><p>改成这样：</p><pre><code class="language-jsx">&lt;div className='temperature-display'&gt;{temperatureValue}°C&lt;/div&gt;
</code></pre><p>注意我们使用 <strong><strong>{}</strong></strong> 来渲染 <strong><strong>temperatureValue</strong></strong> 变量。现在，如果温度值改变，组件将重新渲染，显示新的温度值。</p><p><strong><strong>App.js</strong></strong> 文件目前是这样的：</p><pre><code class="language-jsx">import React, { useState } from 'react';

const App = () =&gt; {
	const [temperatureValue, setTemperatureValue] = useState(10);

	return (
		&lt;div className='app-container'&gt;
			&lt;div className='temperature-display-container'&gt;
				&lt;div className='temperature-display'&gt;{temperatureValue}°C&lt;/div&gt;
			&lt;/div&gt;
			&lt;div className='button-container'&gt;
				&lt;button&gt;+&lt;/button&gt;
				&lt;button&gt;-&lt;/button&gt;
			&lt;/div&gt;
		&lt;/div&gt;
	);
};

export default App;
</code></pre><p>现在，如果你运行 app，浏览器中的一切好像跟之前一样。</p><p>但是，如果你将<strong>传递给 useState hook 的初始值</strong>从 10 改为其他（比如 15），你会看到 app 更新了，也就是说状态钩子起作用了！</p><h2 id="--2"><strong><strong>按键时更改状态</strong></strong></h2><p>接下来，我们要在按按钮时升高或降低温度。</p><p>useState hook 有一个 <strong><strong>setTemperatureValue</strong></strong> 函数，可以修改温度值，所以我们可以在按钮的 <strong><strong>onClick</strong></strong> 事件中用到它。</p><p>首先把“+”按钮的代码修改成：</p><pre><code class="language-jsx">&lt;button onClick={() =&gt; setTemperatureValue(temperatureValue + 1)}&gt;+&lt;/button&gt;
</code></pre><p>注意它是怎么调用 <strong><strong>setTemperatureValue</strong></strong> 函数的。获得当前温度值，加上 1，然后将其作为参数传递。</p><p>因为温度初始值是 10，加上 1 的话状态值就变成 11。再按一次按钮，状态值变成 12......</p><p>将“-”按钮的代码修改成：</p><pre><code class="language-jsx">&lt;button onClick={() =&gt; setTemperatureValue(temperatureValue - 1)}&gt;-&lt;/button&gt;
</code></pre><p>和对“+”按钮的操作类似，不过这次是降低温度值。</p><p>现在我们的代码是这样的：</p><pre><code class="language-jsx">import React, { useState } from 'react';

const App = () =&gt; {
	const [temperatureValue, setTemperatureValue] = useState(10);

	return (
		&lt;div className='app-container'&gt;
			&lt;div className='temperature-display-container'&gt;
				&lt;div className='temperature-display'&gt;{temperatureValue}°C&lt;/div&gt;
			&lt;/div&gt;
			&lt;div className='button-container'&gt;
				&lt;button onClick={() =&gt; setTemperatureValue(temperatureValue + 1)}&gt;+&lt;/button&gt;
				&lt;button onClick={() =&gt; setTemperatureValue(temperatureValue - 1)}&gt;-&lt;/button&gt;
			&lt;/div&gt;
		&lt;/div&gt;
	);
};

export default App;
</code></pre><p>试着在浏览器运行代码，点击按钮，温度值会升高或降低。</p><h2 id="--3"><strong><strong>基于状态修改颜色</strong></strong></h2><p>接下来我们做点有意思的东西——根据温度的高低显示不同的背景色。</p><p>如果温度是 15℃ 或以上，背景色是红色；反之，背景色是蓝色。</p><p>在 CSS 里，我写了这两个类：</p><ul><li><code>.cold</code> 将背景色设置为蓝色</li><li><code>.hot</code> 将背景色设置为红色</li></ul><p>将其中一个类添加至 <strong><strong>temperature display</strong></strong> div，会改变背景色，比如：</p><pre><code class="language-jsx">&lt;div className='temperature-display cold'&gt;{temperatureValue}°C&lt;/div&gt;
</code></pre><p>背景色是蓝色</p><pre><code class="language-jsx">&lt;div className='temperature-display hot'&gt;{temperatureValue}°C&lt;/div&gt;
</code></pre><p>背景色是红色</p><p>那么，怎么基于状态<strong>动态</strong>地应用这两个类呢？</p><p>创建另一个状态钩子，存放 <strong><strong>temperatureColor</strong></strong>：</p><pre><code class="language-jsx">const [temperatureColor, setTemperatureColor] = useState('cold');
</code></pre><p>注意我们给 <strong><strong>temperatureColor</strong></strong> 状态对象设置初始值为 “cold”（因为初始温度值为 10，我们希望背景色是蓝色）。</p><p>然后我们使用<strong>模板常量</strong>动态地添加需要的类：</p><pre><code class="language-jsx">&lt;div className={`temperature-display ${temperatureColor}`}&gt;{temperatureValue}°C&lt;/div&gt;
</code></pre><p>这样一来，创建一个字符串，动态应用 <strong><strong>temperatureColor</strong></strong> 变量。当 <strong><strong>temperatureColor</strong></strong> 变成 “hot” 的时候，组件会重新渲染，给 className &nbsp;字符串添加 “hot” 类。</p><p>我们的代码目前是这样的：</p><pre><code class="language-jsx">import React, { useState } from 'react';

const App = () =&gt; {
	const [temperatureValue, setTemperatureValue] = useState(10);
	const [temperatureColor, setTemperatureColor] = useState('cold');

	return (
		&lt;div className='app-container'&gt;
			&lt;div className='temperature-display-container'&gt;
				&lt;div className={`temperature-display ${temperatureColor}`}&gt;{temperatureValue}°C&lt;/div&gt;
			&lt;/div&gt;
			&lt;div className='button-container'&gt;
				&lt;button onClick={() =&gt; setTemperatureValue(temperatureValue + 1)}&gt;+&lt;/button&gt;
				&lt;button onClick={() =&gt; setTemperatureValue(temperatureValue - 1)}&gt;-&lt;/button&gt;
			&lt;/div&gt;
		&lt;/div&gt;
	);
};

export default App;
</code></pre><p>将初始 <strong><strong>temperatureColor</strong></strong> 状态变量改为 “hot” 或 “cold”，显示板的背景色随之改变。</p><p>我们已经有一个 <strong><strong>onClick </strong></strong>事件可更改 temperatureValue 的值，现在我们给这个事件增加新的逻辑。</p><p>目前 <strong><strong>onClick </strong></strong>事件有一个内联函数。对于单行函数来说用内联函数比较好。但是如果是有不同逻辑的多行函数，最好是将函数放到 JSX 外面，让代码更清晰。</p><p>将下列代码粘贴到状态下面：</p><pre><code class="language-jsx">const increaseTemperature = () =&gt; {
	setTemperatureValue(temperatureValue + 1);
};

const decreaseTemperature = () =&gt; {
	setTemperatureValue(temperatureValue - 1);
};
</code></pre><p>这里我们定义了两个函数，用于升高或降低温度。</p><p>接下来，修改按钮的 <strong><strong>onClick</strong></strong> 属性，调用这些函数：</p><pre><code class="language-jsx">    &lt;button onClick={increaseTemperature}&gt;+&lt;/button&gt;
    &lt;button onClick={decreaseTemperature}&gt;-&lt;/button&gt;
</code></pre><p>我们的代码目前是这样：</p><pre><code class="language-jsx">import React, { useState } from 'react';

const App = () =&gt; {
	const [temperatureValue, setTemperatureValue] = useState(10);
	const [temperatureColor, setTemperatureColor] = useState('cold');

	const increaseTemperature = () =&gt; {
		setTemperatureValue(temperatureValue + 1);
	};

	const decreaseTemperature = () =&gt; {
		setTemperatureValue(temperatureValue - 1);
	};

	return (
		&lt;div className='app-container'&gt;
			&lt;div className='temperature-display-container'&gt;
				&lt;div className={`temperature-display ${temperatureColor}`}&gt;{temperatureValue}°C&lt;/div&gt;
			&lt;/div&gt;
			&lt;div className='button-container'&gt;
				&lt;button onClick={increaseTemperature}&gt;+&lt;/button&gt;
				&lt;button onClick={decreaseTemperature}&gt;-&lt;/button&gt;
			&lt;/div&gt;
		&lt;/div&gt;
	);
};

export default App;
</code></pre><p>注意其实没啥改变，我们只是重新构造了代码，为接下来的工作做准备。</p><p>现在就更容易为点击按钮事件添加逻辑了。</p><p>给 <strong><strong>increaseTemperature</strong></strong> 函数添加逻辑：</p><pre><code class="language-jsx">const increaseTemperature = () =&gt; {
	const newTemperature = temperatureValue + 1;
	setTemperatureValue(newTemperature);

	if (newTemperature &gt;= 15) {
		setTemperatureColor('hot');
	}
};
</code></pre><p>当我们点击按钮若干次，<strong><strong>temperatureValue</strong></strong> 等于或大于 15℃ 时，<strong><strong>temperatureColor</strong></strong> 变量会更改，组件重新渲染，给显示板添加 “hot” 类。</p><p>降低温度时的逻辑是类似的：</p><pre><code class="language-jsx">const decreaseTemperature = () =&gt; {
	const newTemperature = temperatureValue - 1;
	setTemperatureValue(newTemperature);
	if (newTemperature &lt; 15) {
		setTemperatureColor('cold');
	}
};
</code></pre><p>app 最终的代码如下：</p><pre><code class="language-jsx">import React, { useState } from 'react';

const App = () =&gt; {
	const [temperatureValue, setTemperatureValue] = useState(10);
	const [temperatureColor, setTemperatureColor] = useState('cold');

	const increaseTemperature = () =&gt; {
		const newTemperature = temperatureValue + 1;
		setTemperatureValue(newTemperature);

		if (newTemperature &gt;= 15) {
			setTemperatureColor('hot');
		}
	};

	const decreaseTemperature = () =&gt; {
		const newTemperature = temperatureValue - 1;
		setTemperatureValue(newTemperature);
		if (newTemperature &lt; 15) {
			setTemperatureColor('cold');
		}
	};

	return (
		&lt;div className='app-container'&gt;
			&lt;div className='temperature-display-container'&gt;
				&lt;div className={`temperature-display ${temperatureColor}`}&gt;{temperatureValue}°C&lt;/div&gt;
			&lt;/div&gt;
			&lt;div className='button-container'&gt;
				&lt;button onClick={increaseTemperature}&gt;+&lt;/button&gt;
				&lt;button onClick={decreaseTemperature}&gt;-&lt;/button&gt;
			&lt;/div&gt;
		&lt;/div&gt;
	);
};

export default App;
</code></pre><p>运行 app，检查是不是一切 ok——太棒了！</p><h2 id="--4"><strong><strong>挑战</strong></strong></h2><p>你可能注意到这个温度控制 app 不是很安全——用户可能把温度升高到 100℃，或者降低到零下 100℃，简直太危险了！</p><p>如果你想挑战一下的话，可以给它设置温度限制，零下 30℃ 到 30℃。</p><p>提示：给 <strong><strong>increaseTemperature</strong></strong> 和 <strong><strong>decreaseTemperature</strong></strong> 函数添加逻辑。</p><p>原文：<a href="https://www.freecodecamp.org/news/react-beginner-project-tutorial-temperature-control-app/">How to Build a Temperature Control App in React – Tips and Starter Code Included</a>，作者：Chris Blakely<br></p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
