<?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[ Jing Wu - 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[ Jing Wu - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 23 May 2026 08:28:39 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/jing/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Next.js 图像处理教程——如何上传、裁剪和调整浏览器中的图像大小 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：Next.js Image Tutorial – How to Upload, Crop, and Resize Images in the Browser in Next [https://www.freecodecamp.org/news/how-to-upload-crop-resize-images-in-the-browser-in-nextjs/] ，作者：Idris Olubisi [https://www.freecodecamp.org/news/author/idris/] 两个最基本的图像编辑功能是调整大小和裁剪。但是你应该谨慎执行这些操作，因为它们可能会降低图像质量。 裁剪总是会删除原始图像的一部分，从而导致一些像素的丢失。 这篇文章将教你如何在浏览器中上传、裁剪和调整图像大小。 我在 Codesandbox [https://codesandbox.io/s/serverless-leaf-vc9rls?file=/pages/index.js]  中构建了这个项目。要快速开始的话，请下载 Codesandbox [https://codesan ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-upload-crop-resize-images-in-the-browser-in-nextjs/</link>
                <guid isPermaLink="false">629d76c7dbb8cc083a127e4c</guid>
                
                    <category>
                        <![CDATA[ NextJS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jing Wu ]]>
                </dc:creator>
                <pubDate>Sat, 04 Jun 2022 03:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/06/pexels-cottonbro-5083407.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/how-to-upload-crop-resize-images-in-the-browser-in-nextjs/">Next.js Image Tutorial – How to Upload, Crop, and Resize Images in the Browser in Next</a>，作者：<a href="https://www.freecodecamp.org/news/author/idris/">Idris Olubisi</a></p><!--kg-card-begin: markdown--><p>两个最基本的图像编辑功能是调整大小和裁剪。但是你应该谨慎执行这些操作，因为它们可能会降低图像质量。</p>
<p>裁剪总是会删除原始图像的一部分，从而导致一些像素的丢失。</p>
<p>这篇文章将教你如何在浏览器中上传、裁剪和调整图像大小。</p>
<p>我在 <a href="https://codesandbox.io/s/serverless-leaf-vc9rls?file=/pages/index.js">Codesandbox</a> 中构建了这个项目。要快速开始的话，请下载 <a href="https://codesandbox.io/s/serverless-leaf-vc9rls?file=/pages/index.js">Codesandbox</a> 并运行项目。</p>
<h2 id="">先决条件</h2>
<p>要跟上本教程，你应该有一些 JavaScript 和 React.js 经验。对 Next.js 的经验不是必须的，但有这个经验也不错。</p>
<p>你还需要一个 <a href="https://cloudinary.com/users/register/free">Cloudinary account</a> 帐户来存储媒体文件。</p>
<p><a href="https://cloudinary.com/documentation/image_video_and_file_upload#upload_options_overview">Cloudinary</a> 提供了一个安全且完整的 API，用于快速和有效地从服务器、浏览器或移动应用程序上传媒体文件。</p>
<p>最后你需要 <a href="https://nextjs.org/">Next.js</a> 。它是一个基于 React 的开源前端开发 Web 框架，允许服务端渲染和生成静态网站和应用。</p>
<h2 id="">项目设置和安装</h2>
<p>使用 <code>npx create-next-app</code> 命令在你选择的目录中创建一个新项目。</p>
<p>你可以使用以下命令执行此操作：</p>
<pre><code>npx create-next-app &lt;project name&gt;
</code></pre>
<p>要安装这些依赖项，请使用这些命令：</p>
<pre><code>cd &lt;project name&gt; 
npm install cloudinary-react
</code></pre>
<p>在该应用程序创建并安装完依赖项后，你会看到一条信息，其中有关于导航到你的网站并在本地运行的说明。</p>
<p>你可以使用以下命令执行此操作：</p>
<pre><code>npm run dev
</code></pre>
<p>Next.js 将启动一个默认可访问的热重载开发环境 <a href="http://localhost:3000">http://localhost:3000</a> 。</p>
<h2 id="">如何构建用户界面</h2>
<p>在我们的项目中，我们希望能够在主页的用户界面上上传、裁剪和调整图像大小。我们将通过更新 <code>pages/index.js</code> 文件为组件来做到这一点：</p>
<pre><code>import React, { useState } from "react";
import Head from "next/head";

const IndexPage = () =&gt; {

  return (
    &lt;&gt;
      &lt;div className="main"&gt;
        &lt;div className="splitdiv" id="leftdiv"&gt;
          &lt;h1 className="main-h1"&gt;
            How to Crop, Resize &amp; Upload Image in the Browser using Cloudinary
            Transformation
          &lt;/h1&gt;
          &lt;div id="leftdivcard"&gt;
            &lt;h2 className="main-h2"&gt;Resize Options&lt;/h2&gt;
          &lt;/div&gt;

          &lt;button type="button" id="leftbutton"&gt;
            Upload Image
          &lt;/button&gt;
        &lt;/div&gt;

        &lt;div className="splitdiv" id="rightdiv"&gt;
        &lt;h1&gt; Image will appear here&lt;/h1&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};
export default IndexPage;

</code></pre>
<p>不过，当前的用户界面看起来并不那么好。我们将在 style.css 文件中添加一些 CSS 样式，如下所示：</p>
<pre><code>@import url("https://fonts.googleapis.com/css?family=Acme|Lobster");

/* 这使我能够拥有整个页面的宽度，而没有最初的 padding/margin。 */
body,
html {
  margin: 0;
  padding: 0;
  height: 100%;
  width: 100%;
  font-family: Acme;
  min-width: 700px;
}

.splitdiv {
  height: 100%;
  width: 50%;
}

/* 这一部分包含了屏幕左侧的所有内容 */
/* ----------------------------------------- */
#leftdiv {
  float: left;
  background-color: #fafafa;
  height: 932px;
}

#leftdivcard {
  margin: 0 auto;
  width: 50%;
  background-color: white;
  margin-top: 25vh;
  transform: translateY(-50%);
  box-shadow: 10px 10px 1px 0px rgba(78, 205, 196, 0.2);
  border-radius: 10px;
}

#leftbutton {
  background-color: #512cf3;
  border-radius: 5px;
  color: #fafafa;
  margin-left: 350px;
}

/* ----------------------------------------- */

/* 这一部分包含了屏幕右侧的所有内容 */
/* ----------------------------------------- */
#rightdiv {
  float: right;
  background-color: #cbcfcf;
  height: 932px;
}

#rightdivcard {
  margin: 0 auto;
  width: 50%;
  margin-top: 50vh;
  transform: translateY(-50%);
  background-position: bottom;
  background-size: 20px 2px;
  background-repeat: repeat-x;
}

/* ----------------------------------------- */

/* 基础样式 */
/* ----------------------------------------- */

button {
  outline: none !important;
  font-family: Lobster;
  margin-bottom: 15px;
  border: none;
  font-size: 20px;
  padding: 8px;
  padding-left: 20px;
  padding-right: 20px;
  margin-top: -15px;
  cursor: pointer;
}

h1 {
  font-family: Lobster;
  color: #512cf3;
  text-align: center;
  font-size: 40px;
}

input {
  font-family: Acme;
  font-size: 16px;
  font-family: 15px;
}

input {
  width: 30%;
  height: 20px;
  padding: 16px;
  margin-left: 1%;
  margin-right: 2%;
  margin-top: 15px;
  margin-bottom: 10px;
  display: inline-block;
  border: none;
}

input:focus {
  outline: none !important;
  border: 1px solid #512cf3;
  box-shadow: 0 0 1px round #719ece;
}

/* ----------------------------------------- */

.main {
  height: 100%;
  width: 100%;
  display: inline-block;
}

.main-h2 {
  padding-top: 20px;
  text-align: center;
}

.body-h1 {
  padding-top: 20px;
  text-align: center;
  color: white;
}

.inner-p {
  color: white;
  text-align: center;
}

.main-align {
  text-align: center;
}

.form-control {
  margin-left: 15px;
}
</code></pre>
<p>我们的应用程序现在应该看起来如 <a href="http://localhost:3000/">http://localhost:3000/</a> 上所呈现出来那样：</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1650105687298/eeGTDWFHA.png" alt="How to Upload, Crop, &amp; Resize Image in the Browser in Next.js" width="3360" height="1876" loading="lazy"></p>
<h2 id="">如何创建图像上传小部件</h2>
<p>Cloudinary 的上传小部件让我们可以从多个来源上传媒体资源，包括 Dropbox、Facebook、Instagram 和直接从我们设备相机拍摄的图像。我们将在这个项目中使用这个上传小部件。</p>
<p>创建一个免费的 cloudinary 帐户, 获取你的云账户名称（cloud name）和上传预设（upload_presets）。</p>
<p><code>upload_presets</code> 允许我们集中定义一组资源上传选项，而不是在每次上传调用中去提供它们。Cloudinary 中的 <code>cloud name</code>是与你的 Cloudinary 帐户关联的唯一标识符。</p>
<p>首先， 我们通过内容分发网络（CDN）将 Cloudinary 小部件的 JavaScript 文件添加到位于 <code>pages/index.js</code> 中的 <code>index.js</code>中。并且使用 <code>next/head</code> 中的文件去包裹所有 meta 标签，这使我们可以将数据添加到 React 中 HTML 文档的 Head 部分。</p>
<p>接下来，在 <code>pages/index.js</code> 文件中，我们从 <code>next/head</code> 导入 <code>Head</code> 组件并添加脚本文件。</p>
<pre><code>import React, { useState } from "react";
import Head from "next/head";

const IndexPage = () =&gt; {

  return (
    &lt;&gt;
      &lt;Head&gt;
        &lt;title&gt;How to Crop and Resize Image in the Browser&lt;/title&gt;
        &lt;link rel="icon" href="/favicon.ico" /&gt;
        &lt;meta charSet="utf-8" /&gt;
        &lt;script
          src="https://widget.Cloudinary.com/v2.0/global/all.js"
          type="text/javascript"
        &gt;&lt;/script&gt;
      &lt;/Head&gt;
      &lt;div className="main"&gt;
          [...]
      &lt;/div&gt;
    &lt;/&gt;
  );
};
export default IndexPage;
</code></pre>
<p>在 <code>pages/index.js</code> 文件中，我们将在点击按钮时触发的方法中创建一个小部件的实例，以及一个状态变量 <code>imagePublicId</code> 。</p>
<pre><code>import React, { useState } from "react";
import Head from "next/head";

const IndexPage = () =&gt; {
  const [imagePublicId, setImagePublicId] = useState("");

  const openWidget = () =&gt; {
    // create the widget
    const widget = window.cloudinary.createUploadWidget(
      {
        cloudName: "olanetsoft",
        uploadPreset: "w42epls7"
      },
      (error, result) =&gt; {
        if (
          result.event === "success" &amp;&amp;
          result.info.resource_type === "image"
        ) {
          console.log(result.info);
          setImagePublicId(result.info.public_id);
        }
      }
    );
    widget.open(); // open up the widget after creation
  };

  return (
    &lt;&gt;
      //...
    &lt;/&gt;
  );
};
export default IndexPage;
</code></pre>
<p>该小部件需要我们的 Cloudinary 的 <code>cloud_name</code> 和 <code>uploadPreset</code>。该 <code>createWidget()</code> 函数会创建一个新的上传小部件。成功上传图像后，我们将资产的 <code>public_id</code> 分配给相关的状态变量。</p>
<p>要获得我们的 <code>cloudname</code> 和 <code>uploadPreset</code>, 我们需要按照以下步骤操作：</p>
<p>你可以从 Cloudinary 的仪表板获取 <code>cloudName</code>，如下所示。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1650106671153/wjBrA3_m0.png" alt="How to Upload, Crop, &amp; Resize Image in the Browser in Next.js" width="3360" height="1368" loading="lazy"></p>
<p>你可以在 Cloudinary 设置页面的 <code>Upload</code> 选项卡中找到 <code>upload_preset</code>。你可以通过点击仪表板页面右上角的齿轮图标来访问它。</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1650106901391/73lFzuxLQ.png" alt="How to Upload, Crop, &amp; Resize Image in the Browser in Next.js" width="2969" height="232" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1650106814185/GqnIFsNYS.png" alt="How to Upload, Crop, &amp; Resize Image in the Browser in Next.js" width="2653" height="738" loading="lazy"></p>
<p>向下滚动到页面底部的上传预设部分，你将在其中看到你的 <code>upload_presets</code> ，或者如果你没有任何预设，可以选择创建一个。</p>
<p>我们将继续在我们的图片上传按钮的 <code>onClick</code> 处理程序中调用 <code>openWidget</code> 函数，如下所示：</p>
<pre><code>//...

const IndexPage = () =&gt; {
//...
  return (
    &lt;&gt;
     //....
      &lt;div className="main"&gt;
        &lt;div className="splitdiv" id="leftdiv"&gt;
          //...
          &lt;div id="leftdivcard"&gt;
            &lt;h2 className="main-h2"&gt;Resize Options&lt;/h2&gt;
             //...
            &lt;/div&gt;

          &lt;button type="button" id="leftbutton" onClick={openWidget}&gt;
            Upload Image
          &lt;/button&gt;
        &lt;/div&gt;

        &lt;div className="splitdiv" id="rightdiv"&gt;
        &lt;h1&gt; Image will appear here&lt;/h1&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};
export default IndexPage;

</code></pre>
<p>当我们在浏览器中打开我们的应用程序并单击 <code>Upload Image</code> 按钮时，我们应该会看到如下内容：</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1650111448538/pglrS-Exs.png" alt="How to Upload, Crop, &amp; Resize Image in the Browser in Next.js" width="3360" height="1881" loading="lazy"></p>
<h2 id="">如何实现自定义转换功能</h2>
<p>我们需要创建一个组件, 根据传递给它的 props 属性来处理转换。我们将在根文件夹中创建一个 <code>components/</code> 目录。在该目录下，我们将创建一个名为 <code>image.js</code> 的文件，内容如下：</p>
<pre><code>import { CloudinaryContext, Transformation, Image } from "cloudinary-react";

const TransformImage = ({ crop, image, width, height }) =&gt; {
  return (
    &lt;CloudinaryContext cloudName="olanetsoft"&gt;
      &lt;Image publicId={image}&gt;
        &lt;Transformation width={width} height={height} crop={crop} /&gt;
      &lt;/Image&gt;
    &lt;/CloudinaryContext&gt;
  );
};

export default TransformImage;
</code></pre>
<p>在上面的代码片段中，我们导入了 <code>CloudinaryContext</code>，这是一个包装 Cloudinary 组件，用于管理其所有子 Cloudinary 组件之间的共享信息。渲染的 <code>TransformImage</code> 组件将图像转换的数据作为 props 属性。</p>
<p>当我们将其导入到 pages/index.js 时，上面的代码块会渲染上传的图片：</p>
<pre><code>//...
import TransformImage from "../components/image";

const IndexPage = () =&gt; {
  const [imagePublicId, setImagePublicId] = useState("");
  const [alt, setAlt] = useState("");
  const [crop, setCrop] = useState("scale");
  const [height, setHeight] = useState(200);
  const [width, setWidth] = useState(200);

  return (
    &lt;&gt;
     //...
      &lt;div className="main"&gt;
        &lt;div className="splitdiv" id="leftdiv"&gt;
          //...
       &lt;/div&gt;
        &lt;div className="splitdiv" id="rightdiv"&gt;
          &lt;h1&gt; Image will appear here&lt;/h1&gt;
          &lt;div id="rightdivcard"&gt;
            {imagePublicId ? (
              &lt;TransformImage
                crop={crop}
                image={imagePublicId}
                width={width}
                height={height}
              /&gt;
            ) : (
              &lt;h1&gt; Image will appear here&lt;/h1&gt;
            )}
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};
export default IndexPage;
</code></pre>
<p>接下来，我们将添加 <code>Resize Options</code> 单选按钮，这样我们就可以通过以下代码片段选择不同的调整大小和裁剪选项：</p>
<pre><code>//...

const IndexPage = () =&gt; {
//...

  return (
    &lt;&gt;
    //...
      &lt;div className="main"&gt;
        &lt;div className="splitdiv" id="leftdiv"&gt;
          //...
          &lt;div id="leftdivcard"&gt;
            &lt;h2 className="main-h2"&gt;Resize Options&lt;/h2&gt;

          &lt;label className="form-control"&gt;Select Crop Type&lt;/label&gt;
            &lt;div&gt;
              &lt;label className="form-control"&gt;Scale&lt;/label&gt;
              &lt;input
                type="radio"
                value="scale"
                name="crop"
                onChange={(event) =&gt; setCrop(event.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;div&gt;
              &lt;label className="form-control"&gt;Crop&lt;/label&gt;
              &lt;input
                type="radio"
                value="crop"
                name="crop"
                onChange={(event) =&gt; setCrop(event.target.value)}
              /&gt;
            &lt;/div&gt;
            &lt;input
              type="number"
              placeholder="Height"
              onChange={(event) =&gt; setHeight(event.target.value)}
            /&gt;
            &lt;input
              type="number"
              placeholder="Width"
              onChange={(event) =&gt; setWidth(event.target.value)}
            /&gt;
          &lt;/div&gt;

          &lt;button type="button" id="leftbutton" onClick={openWidget}&gt;
            Upload Image
          &lt;/button&gt;
        &lt;/div&gt;

        &lt;div className="splitdiv" id="rightdiv"&gt;
          //...
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};
export default IndexPage;
</code></pre>
<p>在上面的代码片段中：</p>
<ul>
<li>添加了裁剪类型以及宽度和高度选项</li>
<li>添加了一个 onChange 属性来分别跟踪高度和宽度输入框的变化</li>
</ul>
<p>我们的应用程序的最终输出应该类似于下面的内容：</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1650112568692/2htjubfOv.png" alt="How to Upload, Crop, &amp; Resize Image in the Browser in Next.js" width="3360" height="1882" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1650112581661/JnEP--CHC.png" alt="How to Upload, Crop, &amp; Resize Image in the Browser in Next.js" width="3360" height="1874" loading="lazy"></p>
<p>如果你想查看完整代码，这里是项目的 GitHub 仓库：<a href="https://github.com/Olanetsoft/how-to-upload-crop-and-resize-images-in-the-browser-in-next.js">https://github.com/Olanetsoft/how-to-upload-crop-and-resize-images-in-the-browser-in-next.js</a></p>
<h2 id="">结论</h2>
<p>这篇文章展示了如何在 Next.js 的浏览器中上传、裁剪和调整图像大小。</p>
<h2 id="">资源</h2>
<p>你可能会发现这些资源很有帮助。</p>
<ul>
<li><a href="https://cloudinary.com/documentation/transformation_reference">Cloudinary transformation URL reference</a></li>
<li><a href="https://cloudinary.com/documentation/image_transformations">Cloudinary Image Transformation</a></li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何在 React 应用中使用 Hooks、Redux 等管理状态 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：How to Manage State in a React App – With Hooks, Redux, and More [https://www.freecodecamp.org/news/how-to-manage-state-in-a-react-app/]，作者： Germán Cocca [https://www.freecodecamp.org/news/author/gercocca/] 你好！在本文中，我们将了解在 React 应用程序中管理状态的多种方式。 我们将从讨论什么是状态开始，然后介绍许多用于管理状态的工具。 我们将了解简单的 useState hook，并学习更复杂的库，如 Redux。然后我们将查看最新可用的库，例如 Recoil 和 Zusand。 目录  * React 中的状态是什么  * 如何使用 useState hook * 如何使用 useEffect 读取状态更新     * 如何传递一个回调给状态更新函数  ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-manage-state-in-a-react-app/</link>
                <guid isPermaLink="false">629762c4dbb8cc083a127c74</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jing Wu ]]>
                </dc:creator>
                <pubDate>Tue, 31 May 2022 05:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/06/lautaro-andreani-UYsBCu9RP3Y-unsplash-1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/how-to-manage-state-in-a-react-app/">How to Manage State in a React App – With Hooks, Redux, and More</a>，作者：<a href="https://www.freecodecamp.org/news/author/gercocca/">Germán Cocca</a></p><!--kg-card-begin: markdown--><p>你好！在本文中，我们将了解在 React 应用程序中管理状态的多种方式。</p>
<p>我们将从讨论什么是状态开始，然后介绍许多用于管理状态的工具。</p>
<p>我们将了解简单的 useState hook，并学习更复杂的库，如 Redux。然后我们将查看最新可用的库，例如 Recoil 和 Zusand。</p>
<h2 id="">目录</h2>
<ul>
<li><a href="#whatisstateinreact">React 中的状态是什么</a></li>
<li><a href="#howtousetheusestatehook">如何使用 useState hook</a>
<ul>
<li><a href="#howtouseuseeffecttoreadstateupdates">如何使用 useEffect 读取状态更新</a></li>
<li><a href="#howtopassacallbacktostateupdatefunction">如何传递一个回调给状态更新函数</a></li>
</ul>
</li>
<li><a href="#managingscaleandcomplexity">管理规模和复杂性</a>
<ul>
<li><a href="#reactcontext">React context</a></li>
<li><a href="#howtousetheusereducerhook">如何使用 useReducer hook</a></li>
<li><a href="#whataboutredux">那么 Redux 呢</a></li>
</ul>
</li>
<li><a href="#alternativestoredux">Redux 的替代品</a>
<ul>
<li><a href="#reduxtoolkit">Redux toolkit</a>
<ul>
<li><a href="#amentionforreduxthunkandreduxsaga">提到 Redux Thunk 和 Redux Saga</a></li>
</ul>
</li>
<li><a href="#recoil">Recoil</a></li>
<li><a href="#jotai">Jotai</a></li>
<li><a href="#zustand">Zustand</a></li>
</ul>
</li>
<li><a href="#conclusion">总结</a></li>
</ul>
<h2 id="whatisstateinreact">React 中的状态是什么</h2>
<p>在现代 React 中，我们使用<strong>函数组件</strong>构建我们的应用程序。组件本身就是 JavaScript 函数，是独立且<strong>可复用</strong>的代码。</p>
<p>使用组件构建应用程序的目的是使其具有模块化架构，具有明确的关注点分离。这使代码更易于理解、更易于维护并且在可能的情况下更易于复用。</p>
<p><strong>而状态（state）是一个保存有组件信息的对象</strong>。普通 JavaScript 函数没有存储信息的能力。一旦执行完成，它们中的代码就会执行并“消失”。</p>
<p>但是有了状态之后，React 函数组件即使在执行后也可以存储信息。当我们需要一个组件来存储或“记住”某些东西，或者根据环境以不同的方式执行时，状态就是我们所需要的可以让这些生效的东西。</p>
<p>值得一提的是，在 React 应用程序中的并非所有组件都必须具有状态，也有无状态组件，它们只呈现其内容而无需存储任何信息，这也很好。</p>
<p>另一件重要的事情是状态变化是使 React 组件重新渲染的两个原因之一（另一个是 props 的变化）。因此，状态存储了组件的信息同时也控制了它的行为。</p>
<h2 id="howtousetheusestatehook">如何使用 useState hook</h2>
<p>为了在我们的组件中实现状态，React 为我们提供了一个名为 <strong>useState</strong> 的钩子（hook）。让我们看看它是如何与以下示例一起工作的。</p>
<p>我们将使用经典的计数器示例，其中我们将显示一个数字，并且我们有几个按钮用于增加、减少或重置该数字。</p>
<p>这是一个很好的应用程序示例，我们需要存储一条信息并在每次信息更改时呈现不同的内容。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/03/5bueYQblr--2-.gif" alt="5bueYQblr--2-" width="600" height="400" loading="lazy"></p>
<p>此应用程序的代码如下所示：</p>
<pre><code class="language-js">// App.js
import { useState } from 'react'

function App() {

  const [count, setCount] = useState(0)

  return (
    &lt;div className="App"&gt;
      &lt;p&gt;Count is: {count}&lt;/p&gt;

      &lt;div&gt;
        &lt;button onClick={() =&gt; setCount(count+1)}&gt;Add 1&lt;/button&gt;
        &lt;button onClick={() =&gt; setCount(count-1)}&gt;Decrease 1&lt;/button&gt;

        &lt;button onClick={() =&gt; setCount(count+10)}&gt;Add 10&lt;/button&gt;
        &lt;button onClick={() =&gt; setCount(count-10)}&gt;Decrease 10&lt;/button&gt;

        &lt;button onClick={() =&gt; setCount(0)}&gt;Reset count&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default App
</code></pre>
<ul>
<li>
<p>首先我们从 React 导入钩子（hook）：<code>import { useState } from 'react'</code></p>
</li>
<li>
<p>然后我们初始化状态：<code>const [count, setCount] = useState(0)</code></p>
</li>
</ul>
<p>在这里，我们为状态提供了一个变量名（<code>count</code>）和一个我们将在每次需要更新该状态时使用的函数名（<code>setCount</code>）。最后，我们设置状态的初始值（<code>0</code>），这将是应用程序每次启动时默认加载的值。</p>
<ul>
<li>最后，如上所述，每次我们想要更新状态时，都必须使用我们声明的函数：<code>setCount</code>，只需要调用它并将我们想要的新状态作为参数传递给它。也就是说，如果我们想在前一个状态加 1，我们需要调用 <code>setCount(count+1)</code>。</li>
</ul>
<p>如前所述，这将导致状态更新，从而导致组件的重新渲染。在我们的应用程序中我们将在屏幕上看到计数器增加。</p>
<h3 id="howtouseuseeffecttoreadstateupdates">如何使用 useEffect 读取状态更新 </h3>
<p>一个需要提到的重要信息是 setState 函数是<strong>异步的</strong>。因此，如果我们尝试在更新状态后立即读取它，例如：</p>
<pre><code class="language-js">&lt;button onClick={() =&gt; {
          setCount(count+1)
          console.log(count)
}}&gt;Add 1&lt;/button&gt;
</code></pre>
<p>我们会得到之前的状态值，并没有得到更新。</p>
<p>在更新状态后读取状态的正确方法是使用 <strong>useEffect</strong> hook。它允许我们在每个组件重新渲染后（默认情况下）或在我们声明更改的任何特定变量之后执行一个函数。</p>
<p>就像这样：</p>
<pre><code class="language-js">useEffect(() =&gt; console.log(value), [value])
</code></pre>
<h3 id="howtopassacallbacktostateupdatefunction">如何传递一个回调给状态更新函数</h3>
<p>在非常频繁和快速的状态变更时，异步的 useState 也会产生一些影响。</p>
<p>举个例子，用户连续多次按下 ADD 按钮，或者一个循环中发出一定次数的点击事件。</p>
<p>通过<code>setCount(count+1)</code>更新状态，在下一个事件被触发时 <code>count</code> 会有不被更新的风险。</p>
<p>例如，假设在开始时 <code>count = 0</code>，然后调用 <code>setCount(count+1)</code> 异步更新状态。</p>
<p>但是在状态更新完成之前再次调用了 <code>setCount(count+1)</code>。这意味着仍然是<code>count = 0</code>，这意味着第二个<code>setCount</code>不会正确更新状态。</p>
<p>一种比较防守型的方法是向 <code>setCount</code> 传递一个回调，如下所示：<code>setCount(prevCount =&gt; prevCount+1)</code>。</p>
<p>这样可以确保要更新的值是最新的，并使我们远离上述问题。每次我们对先前的状态执行更新时，我们都应该使用这种方法。</p>
<h2 id="managingscaleandcomplexity">管理规模和复杂性</h2>
<p>到目前为止，状态管理似乎是小菜一碟。我们只需要一个 hook、一个值和一个函数来更新它，我们就可以开始了。</p>
<p>但是，一旦应用程序开始变得更大更复杂时，仅使用这一种方式可能会开始导致一些问题。</p>
<h3 id="reactcontext">React context</h3>
<p>第一个可能出现的问题是当我们有很多嵌套组件时，我们需要许多“兄弟”组件来共享相同的状态。</p>
<p>显而易见的答案是“提升”状态，这意味着父组件将成为持有状态的组件，并将状态作为 prop 传递给子组件。</p>
<p>这很好用，但是当我们有很多嵌套组件时，可能需要通过许多层级组件传递 props。这被称为 <strong>prop drilling</strong>，不仅很难看，而且会创建难以维护的代码。</p>
<p>Prop drilling 还可能导致<strong>不必要的重新渲染</strong>，这可能会影响我们应用程序的性能。如果在我们的父组件（存储状态）和子组件（使用状态）之间还有其他组件（“中间组件”），我们也需要通过这些中间组件传递 prop，即使它们并不需要 prop。</p>
<p>这意味着这些“中间组件”将在 prop 变更时重新渲染，即使它们没有不同的内容需要渲染。</p>
<p>解决这个问题的一种方法是使用 <strong>React context</strong>，简单来说，这是一种创建包装组件的方法，该组件包装我们那些想要并且可以直接传递 props 的组件组，而且无需 “drill” 通过那些不是必须使用该状态的组件。</p>
<p>使用 context 时要注意的是，当 context 状态发生变化时，所有接收该状态的被包装组件都将重新渲染。这种情况下，这可能不是必然的，也可能导致性能问题。</p>
<p>因此，我们是否真的需要让一个状态对许多组件可用，或者我们是否可以将其保持在单个组件中, 在这两者之前取一个平衡是非常重要的。如果我们需要让许多组件都可以使用它，把它放在 context 中真的是一个好主意吗？或者我们是否可以把它提升一个层次？</p>
<p>Kent C Dodds 有一篇关于这个主题的<a href="https://kentcdodds.com/blog/application-state-management-with-react">很酷的文章</a>。</p>
<h3 id="howtousetheusereducerhook">如何使用 useReducer hook</h3>
<p>当你使用 useState 时，要设置的新状态取决于先前的状态（如我们的计数示例），或者当我们的应用程序中状态更改非常频繁，这种情况下可能会出现另一个问题。</p>
<p>在这些情况下，useState 可能会引发一些意想不到的和不可预知的行为。接下来的 <strong>reducers</strong> 将解决这个问题。</p>
<p><strong>reducer</strong> 是一个纯函数，它将前一个状态和一个动作作为参数，并返回下一个状态。它被称为 reducer，是因为它与你传递给数组的函数类型相同：<code>Array.prototype.reduce(reducer, initialValue)</code>。</p>
<p><strong>useReducer</strong> 是 React 提供的 hook，它让我们实现 reducer 来管理状态。使用这个 hook，我们之前的示例应用程序看起来像这样：</p>
<pre><code class="language-js">// App.js
import { useReducer } from 'react'
import './App.scss'

function App() {

  function reducer(state, action) {
    switch (action.type) {
      case 'ADD': return { count: state.count + 1 }
      case 'SUB': return { count: state.count - 1 }
      case 'ADD10': return { count: state.count + 10 }
      case 'SUB10': return { count: state.count - 10 }
      case 'RESET': return { count: 0 }
      default: return state
    }
  }

  const [state, dispatch] = useReducer(reducer, { count: 0 })  

  return (
    &lt;div className="App"&gt;
      &lt;p&gt;Count is: {state.count}&lt;/p&gt;

      &lt;div&gt;
        &lt;button onClick={() =&gt; dispatch({type: 'ADD'})}&gt;Add 1&lt;/button&gt;
        
        &lt;button onClick={() =&gt; dispatch({type: 'SUB'})}&gt;Decrease 1&lt;/button&gt;

        &lt;button onClick={() =&gt; dispatch({type: 'ADD10'})}&gt;Add 10&lt;/button&gt;
        &lt;button onClick={() =&gt; dispatch({type: 'SUB10'})}&gt;Decrease 10&lt;/button&gt;

        &lt;button onClick={() =&gt; dispatch({type: 'RESET'})}&gt;Reset count&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export default App
</code></pre>
<ul>
<li>
<p>我们再次从 React 导入 hook 开始：<code>import { useReducer } from 'react'</code></p>
</li>
<li>
<p>然后我们将声明一个 reducer 函数，将接收当前的状态和对其执行的动作（action）作为参数。并且在函数里有一个 switch 语句，该语句将读取动作类型，对状态执行相应的动作，并返回更新后的状态。</p>
</li>
</ul>
<blockquote>
<p>通常做法是在 reducer 上使用 switch 语句, 并且使用大写字母来声明动作。</p>
</blockquote>
<pre><code class="language-js">function reducer(state, action) {
    switch (action.type) {
      case 'ADD': return { count: state.count + 1 }
      case 'SUB': return { count: state.count - 1 }
      case 'ADD10': return { count: state.count + 10 }
      case 'SUB10': return { count: state.count - 10 }
      case 'RESET': return { count: 0 }
      default: return state
    }
  }
</code></pre>
<ul>
<li>之后，是时候声明我们的 <strong>useReducer</strong> hook 了，它看起来与 useState hook 非常相似。我们为我们的状态声明一个<strong>变量</strong>（在我们的例子中是'state'），和一个<strong>我们将用来修改这个变量的函数</strong>（'dispatch'），然后 useReducer 将接收上面的 <strong>reducer 函数</strong> 作为第一个参数，和一个<strong>默认状态</strong>作为第二个参数。</li>
</ul>
<pre><code class="language-js">const [state, dispatch] = useReducer(reducer, { count: 0 })  
</code></pre>
<ul>
<li>最后，我们不会直接调用 reducer 去更新状态，而是调用我们刚刚创建的函数（'dispatch'），将我们想要执行的对应的动作类型传递给它。其中，dispatch 函数将与 reducer 连接并实际修改 state。</li>
</ul>
<pre><code class="language-js">&lt;button onClick={() =&gt; dispatch({type: 'ADD'})}&gt;Add 1&lt;/button&gt;
</code></pre>
<p>这比使用 useState 多了不少模板，但useReducer毕竟没有那么复杂。</p>
<p>总结一下，我们只需要：</p>
<ul>
<li>一个 reducer，合并所有可能的状态变化的函数</li>
<li>一个 dispatch 函数，将修改动作传递给 reducer</li>
</ul>
<p>这里的问题是, UI 元素将不能像以前那样通过用一个值调用 setState 去直接更新状态。现在它们需要调用一个动作类型（action type）并通过 reducer，这使得状态管理更加模块化和可预测。</p>
<h3 id="whataboutredux">那么 Redux 呢</h3>
<p><a href="https://redux.js.org/">Redux</a> 是一个已经存在很长时间并且在 React 中被广泛使用的库。</p>
<p>Redux 是一个工具，它可以解决前面提到的两个问题（prop drilling 和频繁和复杂状态变更时不可预测的状态行为）。</p>
<p>值得一提的是，Redux 是一个不可知的库，这意味着它可以在任何前端应用程序上实现，不仅仅是 React。</p>
<p>Redux 工具集与我们刚刚看到的 useReducer 非常相似，但多了一些东西。Redux 中有三个主要的构建块：</p>
<ul>
<li><strong>store</strong> — 一个保存应用状态数据的对象</li>
<li><strong>reducer</strong> — 一个由动作类型（action type）触发，并返回一些状态数据的函数</li>
<li><strong>action</strong> — 一个告诉 reducer 如何改变状态的对象，它必须包含一个 type 属性，并且它还可以包含一个可选的 payload 属性</li>
</ul>
<p>实现 Redux，我们的示例应用程序如下所示：</p>
<pre><code class="language-js">// App.js
import './App.scss'

import { Provider, useSelector, useDispatch } from 'react-redux'
import { addOne, subOne, addSome, subSome, reset } from './store/actions/count.actions'

import store from './store'

function App() {

  const dispatch = useDispatch()
  const count = useSelector(state =&gt; state.count)

  return (
    &lt;Provider store={store}&gt;
      &lt;div className="App"&gt;
        &lt;p&gt;Count is: {count}&lt;/p&gt;

        &lt;div&gt;
          &lt;button onClick={() =&gt; dispatch(addOne())}&gt;Add 1&lt;/button&gt;
          
          &lt;button onClick={() =&gt; dispatch(subOne())}&gt;Decrease 1&lt;/button&gt;

          &lt;button onClick={() =&gt; dispatch(addSome(10))}&gt;Add 10&lt;/button&gt;
          &lt;button onClick={() =&gt; dispatch(subSome(10))}&gt;Decrease 10&lt;/button&gt;

          &lt;button onClick={() =&gt; dispatch(reset())}&gt;Reset count&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/Provider&gt;
  )
}

export default App
</code></pre>
<p>此外，我们需要一个新的 <strong>store</strong> 文件夹，包含相应的 store、reducer 和 actions 文件。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/03/Cbdp2DJY9.png" alt="Cbdp2DJY9" width="600" height="400" loading="lazy"></p>
<pre><code class="language-js">// index.js (STORE)
import { createStore } from 'redux'
import CountReducer from './reducers/count.reducer'

export default createStore(CountReducer)
</code></pre>
<pre><code class="language-js">// count.reducer.js
import { ADD, SUB, ADDSOME, SUBSOME, RESET } from '../actions/count.actions'

const CountReducer = (state = { count: 0 }, action) =&gt; {
    switch (action.type) {
      case ADD: return { count: state.count + 1 }
      case SUB: return { count: state.count - 1 }
      case ADDSOME: return { count: state.count + action.payload }
      case SUBSOME: return { count: state.count - action.payload }
      case RESET: return { count: 0 }
      default: return state
    }
}

export default CountReducer
</code></pre>
<pre><code class="language-js">// count.actions.js
export const ADD = 'ADD'
export const addOne = () =&gt; ({ type: ADD })

export const SUB = 'SUB'
export const subOne = () =&gt; ({ type: SUB })

export const ADDSOME = 'ADDSOME'
export const addSome = (value) =&gt; ({
    type: ADDSOME,
    payload: value
})

export const SUBSOME = 'SUBSOME'
export const subSome = (value) =&gt; ({
    type: SUBSOME,
    payload: value
})

export const RESET = 'RESET'
export const reset = () =&gt; ({ type: RESET })
</code></pre>
<p>这比我们之前看到的还要更多的模板（这也是 Redux 被批评的主要原因），所以让我们把它分解成几块：</p>
<ul>
<li>
<p>正如我提到的，Redux 是一个外部库，所以在进行任何操作之前，我们需要通过运行 <code>npm i redux react-redux</code> 来安装它。<code>redux</code> 将带来管理状态所需的核心函数，而<code>react-redux</code> 将安装一些很酷的 hook，可以轻松地从我们的组件中读取和修改状态。</p>
</li>
<li>
<p>现在，首先是 <strong>store</strong>。在 Redux 中，store 是拥有所有应用程序状态信息的实体。多亏 Redux，我们能够从任何想要的组件中访问 store（就像使用 context 一样）。</p>
</li>
</ul>
<p>为了创建一个 store，我们导入 <code>createStore</code> 函数，并将一个 reducer 函数作为输入传递给它。</p>
<p>要知道，你也可以将不同的 reducers 合并然后传递给同一个 store，这样你就可以将关注点分离到不同的 reducers 中。</p>
<pre><code class="language-js">import { createStore } from 'redux'
import CountReducer from './reducers/count.reducer'

export default createStore(CountReducer)
</code></pre>
<ul>
<li>然后是 <strong>reducer</strong>，它的工作方式与之前我们看到的 useReducer 完全相同。它接收默认状态和一个动作（action）作为参数，然后在它里面有一个 switch 语句来读取 action type，执行相应的状态修改，并返回更新后的状态。</li>
</ul>
<pre><code class="language-js">import { ADD, SUB, ADDSOME, SUBSOME, RESET } from '../actions/count.actions'

const CountReducer = (state = { count: 0 }, action) =&gt; {
    switch (action.type) {
      case ADD: return { count: state.count + 1 }
      case SUB: return { count: state.count - 1 }
      case ADDSOME: return { count: state.count + action.payload }
      case SUBSOME: return { count: state.count - action.payload }
      case RESET: return { count: 0 }
      default: return state
    }
}

export default CountReducer
</code></pre>
<ul>
<li>接着是 <strong>actions</strong>。actions 用于告诉 reducer 如何更新状态。在代码中，你可以看到，对于每个 action，我们都声明了常量来代替普通的字符串（这是一个可以提高可维护性的好做法），以及一些仅返回一个 type 或者 一个 type 和一个 payload 的函数。这些函数就是我们要从组件中 dispatch 去更改状态的函数。</li>
</ul>
<p>请注意，我对这个例子做了一些改变，以显示在谈论 actions 时 payload 的含义。如果我们想在 dispatch action 时<strong>从组件传递一个参数</strong>，<strong>payload 就是存放该信息的地方</strong>。</p>
<p>在示例中，你可以看到我们在调用 ADDSOME/SUBSOME 时可以直接从组件中传递我们想要加/减的数字。</p>
<pre><code class="language-js">export const ADD = 'ADD'
export const addOne = () =&gt; ({ type: ADD })

export const SUB = 'SUB'
export const subOne = () =&gt; ({ type: SUB })

export const ADDSOME = 'ADDSOME'
export const addSome = value =&gt; ({
    type: ADDSOME,
    payload: value
})

export const SUBSOME = 'SUBSOME'
export const subSome = value =&gt; ({
    type: SUBSOME,
    payload: value
})

export const RESET = 'RESET'
export const reset = () =&gt; ({ type: RESET })
</code></pre>
<ul>
<li>最后是我们的组件。这里有 3 件事需要注意：</li>
</ul>
<ol>
<li>
<p>首先，我们有一个 <strong>provider</strong> 组件，它接收 <strong>store</strong> 作为 props。这是对所有被包装在其中的组件访问 store 的授权。</p>
</li>
<li>
<p>然后我们有一个名为 <strong>useDispatch()</strong>（我们将用于 dispatch actions）和另一个名为 <strong>useSelector()</strong> 的 hook（我们将用于从 store 中读取状态）。</p>
</li>
<li>
<p>最后，请注意我们将要 <strong>dispatch 我们在 action 文件中声明的函数</strong>，并传递一个匹配的值作为输入。这个值是 actions 接收作为 payload 的值，以及 reducer 将用来修改状态的值。</p>
</li>
</ol>
<pre><code class="language-js">import './App.scss'

import { useSelector, useDispatch } from 'react-redux'
import { addOne, subOne, addSome, subSome, reset } from './store/actions/count.actions'

function App() {

  const dispatch = useDispatch()
  const count = useSelector(state =&gt; state.count)

  return (
      &lt;div className="App"&gt;
        &lt;p&gt;Count is: {count}&lt;/p&gt;

        &lt;div&gt;
          &lt;button onClick={() =&gt; dispatch(addOne())}&gt;Add 1&lt;/button&gt;
          
          &lt;button onClick={() =&gt; dispatch(subOne())}&gt;Decrease 1&lt;/button&gt;

          &lt;button onClick={() =&gt; dispatch(addSome(10))}&gt;Add 10&lt;/button&gt;
          &lt;button onClick={() =&gt; dispatch(subSome(10))}&gt;Decrease 10&lt;/button&gt;

          &lt;button onClick={() =&gt; dispatch(reset())}&gt;Reset count&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  )
}

export default App
</code></pre>
<p>Redux 是一个很好的工具，它同时解决了两个问题（prop drilling 和复杂的状态变化）。不过，它确实产生了很多模板，使状态管理成为一个更难理解的话题，特别是在处理不同的文件和实体，如 actions、reducers、store......</p>
<p>这里要提到的重要一点是，这些管理状态的工具或方法并不是相互排斥的，它们可以而且可能应该同时使用，并各自解决它们所擅长的具体问题。</p>
<p>对于 Redux，要解决的问题是处理<strong>全局状态</strong>（指影响整个应用程序或其中很大一部分的状态）。用 Redux 来处理像我们的例子中的计数器或模态的打开和关闭是没有意义的。</p>
<p>一个好的黄金法则是 <strong>useState 用于组件状态，Redux 用于应用程序状态</strong>。</p>
<h2 id="alternativestoredux">Redux 的替代品</h2>
<p>如果这个主题对你来说还不够复杂，在过去的几年里，出现了许多作为 Redux 替代品的新的库，每个库都有自己的状态管理方法。</p>
<p>为了获得很好的概述，让我们快速了解它们。</p>
<h3 id="reduxtoolkit">Redux toolkit</h3>
<p><a href="https://redux-toolkit.js.org/">Redux toolkit</a> 是一个建立在 Redux 之上的库，其目的是去除 Redux 产生的一些复杂性和模板。</p>
<p>Redux toolkit 基于两件事：</p>
<ul>
<li><strong>store</strong>，它的工作方式与普通 Redux store 完全相同</li>
<li><strong>slices</strong> 将普通的 Redux actions 和 reducer 压缩成一个单一的东西</li>
</ul>
<p>实现 Redux Toolkit，我们的示例应用程序如下所示：</p>
<pre><code class="language-js">// App.js
import './App.scss'

import { useSelector, useDispatch } from 'react-redux'
import { addOne, subOne, addSome, subSome, reset } from './store/slices/count.slice'

function App() {

  const dispatch = useDispatch()
  const count = useSelector(state =&gt; state.counter.count)

  return (
      &lt;div className="App"&gt;
        &lt;p&gt;Count is: {count}&lt;/p&gt;

        &lt;div&gt;
          &lt;button onClick={() =&gt; dispatch(addOne())}&gt;Add 1&lt;/button&gt;
          
          &lt;button onClick={() =&gt; dispatch(subOne())}&gt;Decrease 1&lt;/button&gt;

          &lt;button onClick={() =&gt; dispatch(addSome(10))}&gt;Add 10&lt;/button&gt;
          &lt;button onClick={() =&gt; dispatch(subSome(10))}&gt;Decrease 10&lt;/button&gt;

          &lt;button onClick={() =&gt; dispatch(reset())}&gt;Reset count&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  )
}

export default App
</code></pre>
<pre><code class="language-js">// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { Provider } from 'react-redux'
import store from './store/index'

ReactDOM.render(
  &lt;React.StrictMode&gt;
    &lt;Provider store={store}&gt;
      &lt;App /&gt;
    &lt;/Provider&gt;
  &lt;/React.StrictMode&gt;,
  document.getElementById('root')
)
</code></pre>
<pre><code class="language-js">// Index.jsx (STORE)
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './slices/count.slice'

export const store = configureStore({
  reducer: {
      counter: counterReducer
  },
})

export default store
</code></pre>
<pre><code class="language-js">// count.slice.jsx
import { createSlice } from '@reduxjs/toolkit'

const initialState = { count: 0 }

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    addOne: state =&gt; {state.count += 1},
    subOne: state =&gt; {state.count -= 1},
    addSome: (state, action) =&gt; {state.count += action.payload},
    subSome: (state, action) =&gt; {state.count -= action.payload},
    reset: state =&gt; {state.count = 0}
  },
})

export const { addOne, subOne, addSome, subSome, reset } = counterSlice.actions

export default counterSlice.reducer
</code></pre>
<ul>
<li>
<p>首先我们需要通过运行 <code>npm install @reduxjs/toolkit react-redux</code> <strong>来安装它</strong></p>
</li>
<li>
<p>在我们的 <strong>store</strong> 中，我们从 Redux toolkit 中导入 <code>configureStore</code> 函数，通过调用此函数来创建 store，并将一个带有 reducer 的对象传递给它，该对象本身就是一个包含 slice 的对象。</p>
</li>
</ul>
<pre><code class="language-js">export const store = configureStore({
  reducer: {
      counter: counterReducer
  },
})
</code></pre>
<ul>
<li>正如我所提到的，<strong>slice</strong> 是一种将 action 和 reducer 压缩为同一个的方法。我们从 Redux toolkit 中导入<code>createSlice</code> 函数，然后声明初始状态并初始化 slice。</li>
</ul>
<p>这个函数将接收 slice 的名称、初始状态以及我们将从组件派发以修改状态的函数作为参数。</p>
<p>注意这里没有任何 actions。 UI 将直接调用 reducer 函数。这就是 Redux toolkit “带走”的复杂性。</p>
<pre><code class="language-js">export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    addOne: state =&gt; {state.count += 1},
    subOne: state =&gt; {state.count -= 1},
    addSome: (state, action) =&gt; {state.count += action.payload},
    subSome: (state, action) =&gt; {state.count -= action.payload},
    reset: state =&gt; {state.count = 0}
  },
})
</code></pre>
<ul>
<li>在 index.js 中，我们用 provider 组件包装应用程序，这样的话我们可以从任何地方访问状态。</li>
</ul>
<pre><code class="language-js">    &lt;Provider store={store}&gt;
      &lt;App /&gt;
    &lt;/Provider&gt;
</code></pre>
<ul>
<li>最后，我们使用 hooks 从组件中读取状态和 dispatch 修改函数，就像使用普通的 Redux 一样。</li>
</ul>
<pre><code class="language-js">function App() {

  const dispatch = useDispatch()
  const count = useSelector(state =&gt; state.counter.count)

  return (
      &lt;div className="App"&gt;
        &lt;p&gt;Count is: {count}&lt;/p&gt;

        &lt;div&gt;
          &lt;button onClick={() =&gt; dispatch(addOne())}&gt;Add 1&lt;/button&gt;
          
          &lt;button onClick={() =&gt; dispatch(subOne())}&gt;Decrease 1&lt;/button&gt;

          &lt;button onClick={() =&gt; dispatch(addSome(10))}&gt;Add 10&lt;/button&gt;
          &lt;button onClick={() =&gt; dispatch(subSome(10))}&gt;Decrease 10&lt;/button&gt;

          &lt;button onClick={() =&gt; dispatch(reset())}&gt;Reset count&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  )
}
</code></pre>
<p>Redux toolkit 旨在成为处理 Redux 的一种更简单的方法，但在我看来，它仍然是几乎相同的模板，与普通的 Redux 没有太大区别。</p>
<h4 id="amentionforreduxthunkandreduxsaga">提到 Redux Thunk 和 Redux Saga</h4>
<p><a href="https://github.com/reduxjs/redux-thunk">Redux thunk</a> 和 <a href="https://redux-saga.js.org/">Redux Saga</a> 是两个与 Redux <strong>一起</strong>使用的很流行的中间件库；</p>
<p>具体来说，Thunk 和 Saga 都是为了处理副作用或异步任务所使用的。</p>
<h3 id="recoil">Recoil</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/03/2CYCmD92D.png" alt="2CYCmD92D" width="600" height="400" loading="lazy"></p>
<p><a href="https://recoiljs.org/">Recoil</a> 是一个开源状态管理库，专门用于由 Facebook（或 Meta，等等）构建的 React。根据他们的网站，Recoil是为“最小化和响应式”而建立的，在这个意义上，它看起来和感觉都像普通的 React 代码。</p>
<p>Recoil 基于<strong>原子（atom）</strong> 的概念。来自他们的文档，</p>
<blockquote>
<p>“一个原子代表一个状态。原子可以从任何组件读取和写入。读取原子值的组件隐式订阅了该原子，因此任何原子更新都会导致所有订阅该原子的组件重新渲染”。</p>
</blockquote>
<p>使用 Recoil，我们的示例应用程序将如下所示：</p>
<pre><code class="language-js">// App.js
import countState from './recoil/counter.atom'
import './App.scss'

import { useRecoilState } from 'recoil'

function App() {

  const [count, setCount] = useRecoilState(countState)

  return (
      &lt;div className="App"&gt;
        &lt;p&gt;Count is: {count}&lt;/p&gt;

        &lt;div&gt;
          &lt;button onClick={() =&gt; setCount(count+1)}&gt;Add 1&lt;/button&gt;
          
          &lt;button onClick={() =&gt; setCount(count-1)}&gt;Decrease 1&lt;/button&gt;

          &lt;button onClick={() =&gt; setCount(count+10)}&gt;Add 10&lt;/button&gt;
          &lt;button onClick={() =&gt; setCount(count-10)}&gt;Decrease 10&lt;/button&gt;

          &lt;button onClick={() =&gt; setCount(0)}&gt;Reset count&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  )
}

export default App
</code></pre>
<pre><code class="language-js">// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { RecoilRoot } from 'recoil'

ReactDOM.render(
  &lt;React.StrictMode&gt;
    &lt;RecoilRoot&gt;
      &lt;App /&gt;
    &lt;/RecoilRoot&gt;
  &lt;/React.StrictMode&gt;,
  document.getElementById('root')
)
</code></pre>
<pre><code class="language-js">// counter.atom.jsx
import { atom } from 'recoil'

const countState = atom({
  key: 'countState', // 唯一 ID (区别于其他 atoms/selectors)
  default: 0 // 默认值 (又称初始值)
})

export default countState
</code></pre>
<p>你可能马上就能看到，这比 Redux 的模板要少很多。</p>
<ul>
<li>
<p>首先我们通过运行 <code>npm install recoil</code> 来安装它</p>
</li>
<li>
<p>那些使用 recoil 状态的组件需要在其父组件的某处使用 <code>RecoilRoot</code>，所以我们用它来包装我们的应用程序</p>
</li>
</ul>
<pre><code class="language-js">  &lt;React.StrictMode&gt;
    &lt;RecoilRoot&gt;
      &lt;App /&gt;
    &lt;/RecoilRoot&gt;
  &lt;/React.StrictMode&gt;
</code></pre>
<ul>
<li>然后我们声明我们的 <strong>atom</strong>，它只是一个包含键和默认值的对象：</li>
</ul>
<pre><code>const countState = atom({
  key: 'countState', // 唯一 ID (区别于其他 atoms/selectors)
  default: 0 // 默认值 (又称初始值)
})
</code></pre>
<ul>
<li>最后，在我们的组件中，我们导入 <code>useRecoilState</code> hook，用它声明我们的状态，并将我们刚刚在 atom 中声明的唯一键传递给它</li>
</ul>
<pre><code>const [count, setCount] = useRecoilState(countState)
</code></pre>
<p>正如你所看到的，这看起来与常规的 useState hook 非常相似。</p>
<p>在我们的 UI 中，我们只是调用 <code>setCount</code> 函数来更新我们的状态。</p>
<pre><code class="language-js">&lt;button onClick={() =&gt; setCount(count+1)}&gt;Add 1&lt;/button&gt;
</code></pre>
<p>最小，并且非常易于使用。Recoil 仍然是一种实验性的，并没有被广泛使用，但你可以看到世界各地的开发人员将如何转向这个工具。</p>
<h3 id="jotai">Jotai</h3>
<p><a href="https://jotai.org/">Jotai</a> 是一个为 React 构建的开源状态管理库，其灵感来自 Recoil。它与 Recoil 的不同之处在于寻找一个更加简约的 API -- 它不使用字符串键，而且是面向 TypeScript 的。</p>
<p>与 Recoil 一样，Jotai 使用 atoms。<strong>atom</strong> 代表一片状态。你只需要指定一个初始值，它可以是原始值，如字符串和数字、对象和数组。然后在你的组件中使用该 atom，在每次 atom 更改时该组件将重新渲染。</p>
<p>使用 Jotai，我们的示例应用程序如下所示：</p>
<pre><code class="language-js">// App.js
import './App.scss'

import { useAtom } from 'jotai'

function App() {

  const [count, setCount] = useAtom(countAtom)

  return (
      &lt;div className="App"&gt;
        &lt;p&gt;Count is: {count}&lt;/p&gt;

        &lt;div&gt;
          &lt;button onClick={() =&gt; setCount(count+1)}&gt;Add 1&lt;/button&gt;
          
          &lt;button onClick={() =&gt; setCount(count-1)}&gt;Decrease 1&lt;/button&gt;

          &lt;button onClick={() =&gt; setCount(count+10)}&gt;Add 10&lt;/button&gt;
          &lt;button onClick={() =&gt; setCount(count-10)}&gt;Decrease 10&lt;/button&gt;

          &lt;button onClick={() =&gt; setCount(0)}&gt;Reset count&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  )
}

export default App
</code></pre>
<pre><code class="language-js">// counter.atom.jsx
import { atom } from 'jotai'

const countAtom = atom(0)

export default countAtom
</code></pre>
<p>如你所见，它比 Recoil 还要小。</p>
<ul>
<li>
<p>我们通过运行 <code>npm install jotai</code> 来安装它</p>
</li>
<li>
<p>然后我们声明一个具有默认值的 atom：</p>
</li>
</ul>
<pre><code class="language-js">const countAtom = atom(0)
</code></pre>
<ul>
<li>在我们的组件中，通过使用 <code>useAtom</code> 去使用它：</li>
</ul>
<pre><code class="language-js">const [count, setCount] = useAtom(countAtom)
</code></pre>
<p>真的很好，很简单！</p>
<h3 id="zustand">Zustand</h3>
<p><a href="https://github.com/pmndrs/zustand">Zusand</a> 是另一个为 React 构建的开源状态管理库。它的灵感来自于在 Redux 出现之前广泛使用的库 Flux，它的目标是</p>
<blockquote>
<p>“一个小型的、快速的、非观点性的、可扩展的准系统状态管理解决方案，具有基于 hooks 的舒适 API，并且几乎没有模板”</p>
</blockquote>
<p>Zusand 使用 store 的方式与 Redux 类似，但不同之处在于，在 Zusand 中，store 是一个 hook，它需要的模板要少得多。</p>
<p>使用 Zusand，我们的示例应用程序将如下所示：</p>
<pre><code class="language-js">// App.js
import './App.scss'
import useStore from './store'

function App() {

  const count = useStore(state =&gt; state.count)
  const { addOne, subOne, add10, sub10, reset } = useStore(state =&gt; state)

  return (
      &lt;div className="App"&gt;
        &lt;p&gt;Count is: {count}&lt;/p&gt;

        &lt;div&gt;
            &lt;button onClick={() =&gt; addOne()}&gt;Add 1&lt;/button&gt;
          
          &lt;button onClick={() =&gt; subOne()}&gt;Decrease 1&lt;/button&gt;

          &lt;button onClick={() =&gt; add10()}&gt;Add 10&lt;/button&gt;
          &lt;button onClick={() =&gt; sub10()}&gt;Decrease 10&lt;/button&gt;

          &lt;button onClick={() =&gt; reset()}&gt;Reset count&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  )
}

export default App
</code></pre>
<pre><code class="language-js">// Index.jsx (STORE)
import create from 'zustand'

const useStore = create(set =&gt; ({
  count: 0,
  addOne: () =&gt; set(state =&gt; ({count: state.count += 1})),
  subOne: () =&gt; set(state =&gt; ({count: state.count -= 1})),
  add10: () =&gt; set(state =&gt; ({count: state.count += 10})),
  sub10: () =&gt; set(state =&gt; ({count: state.count -= 10})),
  reset: () =&gt; set({count: 0})
}))

export default useStore
</code></pre>
<ul>
<li>
<p>我们通过运行 <code>npm install zustand</code> 安装它</p>
</li>
<li>
<p>我们使用从 Zusand 导入的 <code>create</code> 函数创建了一个 store，在函数里我们设置了默认状态和我们将用来修改状态的函数</p>
</li>
</ul>
<pre><code class="language-js">const useStore = create(set =&gt; ({
  count: 0,
  addOne: () =&gt; set(state =&gt; ({count: state.count += 1})),
  subOne: () =&gt; set(state =&gt; ({count: state.count -= 1})),
  add10: () =&gt; set(state =&gt; ({count: state.count += 10})),
  sub10: () =&gt; set(state =&gt; ({count: state.count -= 10})),
  reset: () =&gt; set({count: 0})
}))
</code></pre>
<ul>
<li>然后在我们的组件中导入刚刚创建的 store，并通过以下方式从中读取状态和修改函数：</li>
</ul>
<pre><code class="language-js">  const count = useStore(state =&gt; state.count)
  const { addOne, subOne, add10, sub10, reset } = useStore(state =&gt; state)
</code></pre>
<p>在我们的 UI 中可以像这样调用修改函数：</p>
<pre><code class="language-js">&lt;button onClick={() =&gt; addOne()}&gt;Add 1&lt;/button&gt;
</code></pre>
<p>你可以看到来自 Redux 相同概念的 Zusand，却有一个更干净、更简单的方法。</p>
<h2 id="conclusion">总结</h2>
<p>状态管理是前端开发中最复杂的主题之一。你可以看到有多少人试图让它以可预测和可扩展的方式，而且是以干净和易于使用的方式工作。</p>
<p>特别是在过去的几年里，出现了很多很好的工具，提供了处理状态管理的好方法。</p>
<p>不过，作为开发者，我们必须牢记，Redux 和其他库的创建是为了解决特定的状态管理问题，特别是在真正的大型、复杂和大量使用的应用程序中。</p>
<p>我认为，如果你没有遇到这些问题，真的没有必要增加额外的模板，使你的代码复杂化。即使使用那些几乎不添加样板的现代库。</p>
<p>React 本身是一个非常强大和可靠的库，useState、useReducer 和 useContext 等工具足以解决大多数问题。因此，我会坚持基本的东西，除非因为某些原因，基本的东西已经不够用了。</p>
<p>当需要更具体、更强大的状态管理库时，我认为应该在可靠性和简单性之间做出选择。</p>
<p>Redux 是最成熟和使用最广泛的库，它附带大量文档、在线社区以及在每个新版本中发现和解决的以前错误。</p>
<p>它的坏处是，作为开发者，它给我们带来了一些我们必须学习和思考的新概念。我们还需要添加相当多的代码来使其工作，而且它所增加的复杂性可能超过它所帮助解决的问题。</p>
<p>相反，我们之前所看到的现代库要简单得多，而且直截了当，但是它们没有得到广泛使用和测试，仍然是一种实验性的。</p>
<p>但就我们目前所看到的而言，其中一个或一些带头成为更广泛使用的工具似乎只是时间问题。</p>
<p>我希望你喜欢这篇文章并学到了一些新东西。</p>
<p>如果你愿意，你也可以在 <a href="https://www.linkedin.com/in/germancocca/">linkedin</a> 或 <a href="https://twitter.com/CoccaGerman">twitter</a> 上关注我。</p>
<p>干杯，下次见！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何用 Travis CI 在 GitHub 页面上托管一个 Angular 应用程序 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：How to Host an Angular Application on GitHub Pages with Travis CI [https://www.freecodecamp.org/news/host-an-angular-application-on-github-pages-with-travis-ci/] ，作者：Rodrigo Kamada [https://www.freecodecamp.org/news/author/rodrigokamada/] 在本文中，我们将使用最新版本的 Angular 创建一个应用程序。然后我们将它托管在 GitHub 页面的静态网站服务上，使用持续集成工具 Travis CI 来部署该应用程序。 前提 在开始之前，你需要安装和配置以下工具来创建 Angular 应用程序。  * Git [https://git-scm.com/]：Git 是一个分布式版本控制系统，我们将使用它来同步仓库。  * Node.js and npm [https://nodejs.org/]：Node.js 是一个基于谷歌 V8 引擎的 Ja ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/host-an-angular-application-on-github-pages-with-travis-ci/</link>
                <guid isPermaLink="false">627a2894c9c067061df8b817</guid>
                
                    <category>
                        <![CDATA[ Angular ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jing Wu ]]>
                </dc:creator>
                <pubDate>Tue, 10 May 2022 07:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/05/angular-travisci-cover-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/host-an-angular-application-on-github-pages-with-travis-ci/">How to Host an Angular Application on GitHub Pages with Travis CI</a>，作者：<a href="https://www.freecodecamp.org/news/author/rodrigokamada/">Rodrigo Kamada</a></p><!--kg-card-begin: markdown--><p>在本文中，我们将使用最新版本的 Angular 创建一个应用程序。然后我们将它托管在 GitHub 页面的静态网站服务上，使用持续集成工具 Travis CI 来部署该应用程序。</p>
<h2 id="">前提</h2>
<p>在开始之前，你需要安装和配置以下工具来创建 Angular 应用程序。</p>
<ul>
<li><a href="https://git-scm.com/">Git</a>：Git 是一个分布式版本控制系统，我们将使用它来同步仓库。</li>
<li><a href="https://nodejs.org/">Node.js and npm</a>：Node.js 是一个基于谷歌 V8 引擎的 JavaScript 代码运行时软件。 npm 是 Node.js 的包管理器（Node Package Manager）。我们将使用这些来构建和运行 Angular 应用程序并安装库（依赖库）。</li>
<li><a href="https://angular.io/cli">Angular CLI</a>：Angular CLI 是 Angular 的一个命令行实用工具，我们将使用它来创建 Angular 应用程序的基本结构。</li>
<li>IDE（例如 <a href="https://code.visualstudio.com/">Visual Studio Code</a> 或 <a href="https://www.jetbrains.com/webstorm/">WebStorm</a>）：IDE（集成开发环境）是具有图形界面的工具，可帮助我们开发应用程序。在这里，我们将使用其中一个来开发 Angular 应用程序。</li>
</ul>
<h2 id="">开始</h2>
<h3 id="github">在 GitHub 上创建和配置你的帐户</h3>
<p><a href="https://github.com/">GitHub</a> 是一个使用 Git 工具进行版本控制的源代码和文件存储服务。<a href="https://pages.github.com/">GitHub Pages</a> 是使用公共仓库的静态文件托管服务。</p>
<p>首先，如果你还没有帐户，则需要在 GitHub 上创建一个帐户。访问 <a href="https://github.com/">https://github.com/</a> 并点击按钮 <em>Sign up</em>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/github-step1.png" alt="github-step1" width="600" height="400" loading="lazy"></p>
<p>填写用户名、电子邮件地址和密码字段，点击 “Verify” 按钮通过测试，然后点击 “Create account” 按钮。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/github-step2.png" alt="github-step2" width="600" height="400" loading="lazy"></p>
<p>接下来，我们会生成在 Travis CI 中使用到的令牌。单击你的头像打开菜单，然后单击菜单 Settings。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/github-step3.png" alt="github-step3" width="600" height="400" loading="lazy"></p>
<p>单击菜单 Developer settings。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/github-step4.png" alt="github-step4" width="600" height="400" loading="lazy"></p>
<p>单击菜单 Personal access tokens。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/github-step5.png" alt="github-step5" width="600" height="400" loading="lazy"></p>
<p>点击按钮 Generate new token。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/github-step6.png" alt="github-step6" width="600" height="400" loading="lazy"></p>
<p>填写字段 Note，选择选项 repo 并点击按钮 Create token。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/github-step7.png" alt="github-step7" width="600" height="400" loading="lazy"></p>
<p>复制生成的令牌。因为接下来该令牌将在 Travis CI 中配置。在我的例子中，“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” 就是我生成的令牌。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/github-step8.png" alt="github-step8" width="600" height="400" loading="lazy"></p>
<p>让我们创建仓库。单击你的头像打开菜单，然后单击菜单 Your repositories。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/github-step9.png" alt="github-step9" width="600" height="400" loading="lazy"></p>
<p>点击按钮 New。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/github-step10.png" alt="github-step10" width="600" height="400" loading="lazy"></p>
<p>填写仓库名称字段并单击按钮 Create repository。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/github-step11.png" alt="github-step11" width="600" height="400" loading="lazy"></p>
<p>准备好了！帐户已创建好、令牌已生成，并且仓库 <a href="https://github.com/rodrigokamada/angular-travisci"><code>https://github.com/rodrigokamada/angular-travisci</code></a> 也创建好了。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/github-step12.png" alt="github-step12" width="600" height="400" loading="lazy"></p>
<h3 id="travisci">在 Travis CI 上创建和配置你的帐户</h3>
<p><a href="https://www.travis-ci.com/">Travis CI</a> 是与 GitHub 集成的部署服务。</p>
<p>首先，如果你还没有 Travis CI 帐户，则需要创建一个。访问 <a href="https://travis-ci.com/">https://travis-ci.com/</a> 并点击按钮 Sign up。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/travisci-step1.png" alt="travisci-step1" width="600" height="400" loading="lazy"></p>
<p>单击 SIGN IN WITH GITHUB 按钮以使用你的 GitHub 帐户登录。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/travisci-step2.png" alt="travisci-step2" width="600" height="400" loading="lazy"></p>
<p>如果 Travis CI 请求列出 GitHub 仓库的权限，则接受该请求。单击仓库链接 angular-travisci。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/travisci-step3.png" alt="travisci-step3" width="600" height="400" loading="lazy"></p>
<p>让我们设置 GitHub 访问令牌。单击菜单 More options，然后单击菜单 Settings。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/travisci-step4.png" alt="travisci-step4" width="600" height="400" loading="lazy"></p>
<p>字段 NAME的值填写为 GITHUB_TOKEN ，VALUE 的值填写为你在 GitHub 上生成的令牌的值，然后单击按钮 Add。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/travisci-step5.png" alt="travisci-step5" width="600" height="400" loading="lazy"></p>
<p>准备好了！账户已创建，仓库也已配置好了。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/04/travisci-step6.png" alt="travisci-step6" width="600" height="400" loading="lazy"></p>
<h3 id="angular">创建你的 Angular 应用程序</h3>
<p><a href="https://angular.io/">Angular</a> 是一个使用 HTML、CSS 和 TypeScript（JavaScript）构建 Web、移动和桌面应用程序的开发平台。目前，Angular 的版本为 13，Google 是该项目的主要维护者。</p>
<p>让我们使用带有路由文件和 SCSS 样式格式的 <code>@angular/cli</code> 创建具有 Angular 基础结构的应用程序。</p>
<pre><code class="language-powershell">? Would you like to add Angular routing? Yes
? Which stylesheet format would you like to use? SCSS   [ https://sass-lang.com/documentation/syntax#scss                ]
CREATE angular-travisci/README.md (1061 bytes)
CREATE angular-travisci/.editorconfig (274 bytes)
CREATE angular-travisci/.gitignore (604 bytes)
CREATE angular-travisci/angular.json (3267 bytes)
CREATE angular-travisci/package.json (1078 bytes)
CREATE angular-travisci/tsconfig.json (783 bytes)
CREATE angular-travisci/.browserslistrc (703 bytes)
CREATE angular-travisci/karma.conf.js (1433 bytes)
CREATE angular-travisci/tsconfig.app.json (287 bytes)
CREATE angular-travisci/tsconfig.spec.json (333 bytes)
CREATE angular-travisci/src/favicon.ico (948 bytes)
CREATE angular-travisci/src/index.html (301 bytes)
CREATE angular-travisci/src/main.ts (372 bytes)
CREATE angular-travisci/src/polyfills.ts (2820 bytes)
CREATE angular-travisci/src/styles.scss (80 bytes)
CREATE angular-travisci/src/test.ts (743 bytes)
CREATE angular-travisci/src/assets/.gitkeep (0 bytes)
CREATE angular-travisci/src/environments/environment.prod.ts (51 bytes)
CREATE angular-travisci/src/environments/environment.ts (658 bytes)
CREATE angular-travisci/src/app/app-routing.module.ts (245 bytes)
CREATE angular-travisci/src/app/app.module.ts (393 bytes)
CREATE angular-travisci/src/app/app.component.scss (0 bytes)
CREATE angular-travisci/src/app/app.component.html (23809 bytes)
CREATE angular-travisci/src/app/app.component.spec.ts (1087 bytes)
CREATE angular-travisci/src/app/app.component.ts (221 bytes)
✔ Packages installed successfully.
    Successfully initialized git.
</code></pre>
<p>创建 <code>.travis.yml</code> 文件。</p>
<pre><code class="language-powershell">touch .travis.yml
</code></pre>
<p>使用以下内容配置  <code>.travis.yml</code> 文件。</p>
<pre><code class="language-yaml">notifications:
  email:
    recipients:
      - rodrigo@kamada.com.br

language: node_js

node_js:
  - 16

before_script:
  - npm install

script:
  - npm run test:headless

before_deploy:
  - npm run build:prod

deploy:
  provider: pages
  skip_cleanup: true
  github_token: $GITHUB_TOKEN
  local_dir: dist/angular-travisci
  on:
    branch: main
</code></pre>
<p>更改 <code>package.json</code> 文件，并添加以下脚本。 将 <code>rodrigokamada</code> 替换为你的 GitHub 用户名。</p>
<pre><code class="language-json">  "build:prod": "ng build --prod --base-href https://rodrigokamada.github.io/angular-travisci/",
  "test:headless": "ng test --watch=false --browsers=ChromeHeadless"
</code></pre>
<p>更改 <code>src/app/app.component.spec.ts</code> 文件并删除测试 <code>should have as title 'angular-travisci'</code> 和 <code>should render title</code>。</p>
<p>使用以下命令运行测试：</p>
<pre><code class="language-powershell">npm run test:headless

&gt; angular-travisci@1.0.0 test:headless
&gt; ng test --watch=false --browsers=ChromeHeadless

⠋ Generating browser application bundles (phase: setup)...Compiling @angular/core/testing : es2015 as esm2015
Compiling @angular/compiler/testing : es2015 as esm2015
Compiling @angular/platform-browser/testing : es2015 as esm2015
Compiling @angular/common/testing : es2015 as esm2015
Compiling @angular/platform-browser-dynamic/testing : es2015 as esm2015
Compiling @angular/router/testing : es2015 as esm2015
⠸ Generating browser application bundles (phase: building)...05 09 2021 19:40:04.329:INFO [karma-server]: Karma v6.3.4 server started at http://localhost:9876/
05 09 2021 19:40:04.331:INFO [launcher]: Launching browsers ChromeHeadless with concurrency unlimited
05 09 2021 19:40:04.369:INFO [launcher]: Starting browser ChromeHeadless
✔ Browser application bundle generation complete.
05 09 2021 19:40:09.704:INFO [Chrome Headless 92.0.4515.159 (Linux x86_64)]: Connected on socket NUtJhjavb1JvssqOAAAB with id 25989029
Chrome Headless 92.0.4515.159 (Linux x86_64): Executed 1 of 1 SUCCESS (0.068 secs / 0.042 secs)
TOTAL: 1 SUCCESS
</code></pre>
<p>使用以下命令运行应用程序，访问地址  <code>http://localhost:4200/</code>，并检查该应用程序是否正常工作。</p>
<pre><code class="language-powershell">npm start

&gt; angular-travisci@1.0.0 start
&gt; ng serve

✔ Browser application bundle generation complete.

Initial Chunk Files | Names         |      Size
vendor.js           | vendor        |   2.39 MB
polyfills.js        | polyfills     | 128.51 kB
main.js             | main          |   8.89 kB
runtime.js          | runtime       |   6.63 kB
styles.css          | styles        |   1.18 kB

                    | Initial Total |   2.53 MB

Build at: 2021-09-05T22:35:38.010Z - Hash: a4cfc9149589386eca5b - Time: 39997ms

** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **


✔ Compiled successfully.
</code></pre>
<p>使用以下命令构建应用程序：</p>
<pre><code class="language-powershell">npm run build:prod

&gt; angular-travisci@1.0.0 build:prod
&gt; ng build --configuration production --base-href https://rodrigokamada.github.io/angular-travisci/

✔ Browser application bundle generation complete.
✔ Copying assets complete.
✔ Index html generation complete.

Initial Chunk Files           | Names         |      Size
main.c678fa8750e7c769.js      | main          | 177.63 kB
polyfills.6d7801353e02e327.js | polyfills     |  36.21 kB
runtime.b136bda8a38c4f2e.js   | runtime       |   1.06 kB
styles.ef46db3751d8e999.css   | styles        |   0 bytes

                              | Initial Total | 214.90 kB

Build at: 2021-09-05T22:42:19.525Z - Hash: 83bfffc079b083727ca4 - Time: 26030ms
</code></pre>
<p>在你创建的 GitHub 仓库上同步应用程序。</p>
<p>准备好了！在 GitHub 仓库上同步应用程序后，Travis CI 会构建应用程序并在分支 <code>gh-pages</code> 上同步。</p>
<p>访问地址 <a href="https://rodrigokamada.github.io/angular-travisci/"><code>https://rodrigokamada.github.io/angular-travisci/</code></a> 并检查应用程序是否正常工作。将 <code>rodrigokamada</code> 值替换为你的 GitHub 用户名。</p>
<p>就是这样！<a href="https://github.com/rodrigokamada/angular-travisci">https://github.com/rodrigokamada/angular-travisci</a> 上的应用程序仓库现在可以用了。</p>
<h2 id="">结语</h2>
<p>总结本文所涵盖的内容：</p>
<ul>
<li>在 GitHub 上创建了一个帐户</li>
<li>在 GitHub 上创建了一个访问令牌</li>
<li>在 GitHub 上创建了一个仓库</li>
<li>在 Travis CI 上创建了一个帐户</li>
<li>在 Travis CI 上配置了 GitHub 访问令牌</li>
<li>创建一个 Angular 应用程序</li>
</ul>
<p>你可以使用本文创建你的个人网站并拥有在线作品集。</p>
<p>感谢你阅读本文，希望你喜欢这篇文章！</p>
<p>如果你需要在我发布新文章时获得最新内容，请在 <a href="https://twitter.com/rodrigokamada">Twitter</a> 上关注我。</p>
<p>本教程以葡萄牙语发布在我的<a href="https://rodrigo.kamada.com.br/share/blog/hospedando-uma-aplicacao-angular-no-github-pages-usando-o-travis-ci">博客</a>。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
