<?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[ luojiyin - 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[ luojiyin - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Sat, 23 May 2026 19:22:11 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/author/luojiyin/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 如何使用懒加载优化 Next.js 应用程序性能 ]]>
                </title>
                <description>
                    <![CDATA[ 人们不喜欢使用速度慢的应用程序。初始加载时间对网络应用程序和网站来说至关重要。 如果应用程序的加载时间超过 3 秒，就会被视为速度缓慢，可能导致用户离开应用程序或网站。 Next.js 是一个基于 React 的框架，可以用来构建可伸缩、性能良好且速度快的网络应用程序和网站。随着 React 服务器组件 [https://www.freecodecamp.org/news/how-to-use-react-server-components/] 在 Next.js 应用程序路由版本中的引入，开发人员有了一个新的心智模型来“建立服务器组件的开发思维模式”。它解决了 SEO 问题，帮助创建 零打包体积（zero bundle size） 的 React 组件，最终实现 UI 组件的更快加载。 但你的应用程序可能并不总是关于服务端组件。你可能还需要使用客户端组件。此外，你可能希望将它们作为应用程序初始加载的一部分或按需加载（比如点击按钮时）。 在浏览器中加载客户端组件包括将组件代码下载到浏览器、下载所有导入到该客户端组件中的库和其他组件，以及 React 为确保你的组件正常工作而处理 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/next-js-performance-optimization/</link>
                <guid isPermaLink="false">67ebdbb1930d5ec015f6bd16</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Tue, 01 Apr 2025 12:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2025/04/lazyloading-next.js.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/next-js-performance-optimization/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Optimize Next.js App Performance With Lazy Loading</a>
      </p><!--kg-card-begin: markdown--><p>人们不喜欢使用速度慢的应用程序。初始加载时间对网络应用程序和网站来说至关重要。</p>
<p>如果应用程序的加载时间超过 3 秒，就会被视为速度缓慢，可能导致用户离开应用程序或网站。</p>
<p><code>Next.js</code> 是一个基于 React 的框架，可以用来构建可伸缩、性能良好且速度快的网络应用程序和网站。随着 <a href="https://www.freecodecamp.org/news/how-to-use-react-server-components/">React 服务器组件</a> 在 Next.js 应用程序路由版本中的引入，开发人员有了一个新的心智模型来“建立服务器组件的开发思维模式”。它解决了 SEO 问题，帮助创建 <code>零打包体积（zero bundle size）</code> 的 React 组件，最终实现 UI 组件的更快加载。</p>
<p>但你的应用程序可能并不总是关于服务端组件。你可能还需要使用客户端组件。此外，你可能希望将它们作为应用程序初始加载的一部分或按需加载（比如点击按钮时）。</p>
<p>在浏览器中加载客户端组件包括将组件代码下载到浏览器、下载所有导入到该客户端组件中的库和其他组件，以及 React 为确保你的组件正常工作而处理的一些额外事项。</p>
<p>根据用户的互联网连接和其他网络因素，整个客户端组件的加载可能需要一段时间，这可能会阻止用户更快地使用应用程序。</p>
<p>这就是 <code>懒加载</code> 技术可以派上用场的地方。它们可以帮助你避免在浏览器上单体加载客户端组件。</p>
<p>在本文中，我们将讨论在 Next.js 中用于客户端组件加载优化的几种懒加载技术。我们还会讨论一些你需要知道的边缘情况。</p>
<p>如果你也喜欢通过视频内容学习，本文还提供了视频教程：🙂</p>
<figure class="kg-card kg-embed-card" data-test-label="fitted">
        <div class="fluid-width-video-container">
          <div style="padding-top: 56.25%;" class="fluid-width-video-wrapper">
            <iframe width="560" height="315" src="https://www.youtube.com/embed/gq9bBZru78Y" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube 视频播放器" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy" name="fitvid0"></iframe>
          </div>
        </div>
      </figure>
<p>在我们开始之前，有几件事需要告诉你：</p>
<ul>
<li>我们将编写大量代码来构建一个演示懒加载技术的应用程序。你可以从这个 GitHub 存储库找到所有源代码：<a href="https://github.com/tapascript/nextjs-lazy-load">https://github.com/tapascript/nextjs-lazy-load</a>。但我强烈建议你在我们进行的过程中自己编写代码，并仅将存储库作为参考。</li>
<li>你也可以通过 <a href="https://nextjs-lazy-load.netlify.app/">Netlify 上的公开部署</a> 访问应用程序。</li>
</ul>
<p>让我们开始吧 🚀。哦对了，如果你是汤姆和杰瑞的卡通迷，你会更喜欢这篇文章！</p>
<h2 id=""><strong>目录</strong></h2>
<ul>
<li><a href="#heading-what-is-lazy-loading">什么是懒加载</a></li>
<li><a href="#heading-lazy-loading-techniques-in-nextjs">Next.js 中的懒加载技术</a></li>
<li><a href="#heading-lazy-loading-with-dynamic-import-and-nextdynamic">使用 dynamic import 和 next/dynamic 进行懒加载</a></li>
<li><a href="#heading-lazy-loading-with-reactlazy-and-suspense">使用 React.lazy() 和 Suspense 进行懒加载</a></li>
<li><a href="#heading-how-to-lazy-load-the-named-exported-components">如何懒加载命名导出的组件</a></li>
<li><a href="#heading-lazy-loading-your-server-components-1">懒加载你的服务端组件</a></li>
<li><a href="#heading-lazy-loading-your-server-components-1">我们应该懒加载 Next.js 中的所有客户端组件吗</a></li>
<li><a href="#heading-whats-next">接下来是什么</a></li>
</ul>
<h2 id="">什么是懒加载</h2>
<p>在现代 Web 应用程序开发中，我们不会将所有逻辑编写到一个 JavaScript/TypeScript 文件中，或将所有样式编写到一个巨大的 CSS 文件中。而是将它们在源代码级别拆分，创建逻辑模块、业务逻辑、展示组件和样式相关文件。这帮助我们更好地组织代码。</p>
<p>然后我们使用一种称为打包器的工具，它在应用程序开发过程的构建阶段启动。它为我们的脚本和样式创建包。一些著名的打包器包括 Webpack、Rollup 和 Parcel 等。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/07/image-43.png" alt="图片" width="600" height="400" loading="lazy"> <em>打包器从源代码创建包</em></p>
<p>现在，由于我们有了包，如果尝试将它们一起加载到浏览器中，我们会遇到一些缓慢。这是因为需要将完整的包加载到浏览器中，用户界面才能正常工作。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/07/image-44.png" alt="图片" width="600" height="400" loading="lazy"> <em>加载大量包会导致加载体验差</em></p>
<p>因此，现代 Web 开发库和工具系统允许我们将包分块加载，而不是等待大量包加载到浏览器中。</p>
<p>我们可能希望立即加载一些块，因为用户可能会在应用程序加载时更早需要它们。与此同时，我们可能希望等待加载网页的某些部分，直到它们真正需要时。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/07/image-45.png" alt="图片" width="600" height="400" loading="lazy"> <em>拆分成块并加载所需的部分</em></p>
<p>这种等待加载网页或应用程序部分的机制，并仅在绝对必要时加载它们，被称为 <code>懒加载</code>。懒加载的概念并不是 React 或 Next.js 特有的。这是一种性能优化技术，你可以使用各种库和框架来实现。</p>
<p>Next.js 中的懒加载技术用于减少一个路径所需的 JavaScript 量。这有助于提高应用程序的初始加载性能。我们可以推迟加载客户端组件和导入的库，直到它们真正需要时。</p>
<p>我们可以通过两种方式在 Next.js 中实现懒加载技术：</p>
<ul>
<li>使用 <code>next/dynamic</code> 包的动态导入。</li>
<li>使用 <code>React.lazy()</code> 和 <code>Suspense</code> 的组合。</li>
</ul>
<p>让我们通过代码示例来理解这些技术。</p>
<h2 id="dynamicimportnextdynamic">使用 <code>dynamic import</code> 和 <code>next/dynamic</code> 实现懒加载</h2>
<p><code>next/dynamic</code> 是 ReactJS 中的 React.lazy() 和 Suspense 的组合。使用 <code>next/dynamic</code> 包进行动态导入是实现 Next.js 中懒加载的首选方法。</p>
<p>为了演示这一点，我们首先用以下命令创建一个 Next.js 应用：</p>
<pre><code class="language-shell">npx create-next-app@latest
</code></pre>
<p>您可以使用以下命令在本地启动应用：</p>
<pre><code class="language-shell">## 使用 npm
npm run dev

## 使用 yarn
yarn dev

## 或者使用 pnpm, bun，您喜欢的都可以！
</code></pre>
<p>现在在 <code>app/</code> 目录下创建一个名为 <code>components</code> 的文件夹。我们将在这个组件文件夹下创建所有的组件。现在，在 <code>app/components/</code> 下创建一个名为 <code>tom</code> 的文件夹。最后，在 <code>app/components/tom/</code> 目录下创建一个名为 <code>tom.jsx</code> 的 React 组件，代码如下：</p>
<pre><code class="language-jsx">// tom.jsx

const LazyTom = () =&gt; {
  return (
    &lt;div className="flex flex-col"&gt;
      &lt;h1 className="text-3xl my-2"&gt;The Lazy Tom&lt;/h1&gt;
      &lt;p className="text-xl my-1"&gt;
        Tom, named &amp;quot;Jasper&amp;quot; in his debut appearance, is a gray and white
        domestic shorthair cat 🐈. &amp;quot;Tom&amp;quot; is a generic name for a male cat. He is
        usually but not always, portrayed as living a comfortable, or even
        pampered life. Tom is no match for Jerry&amp;apos;s wits.
      &lt;/p&gt;
      &lt;p className="text-xl my-1"&gt;
        Although cats typically chase mice to eat them, it is quite rare for Tom
        to actually try to eat Jerry. He tries to hurt or compete with him just
        to taunt Jerry, even as revenge, or to obtain a reward from a human,
        including his owner(s)/master(s), for catching Jerry, or for generally
        doing his job well as a house cat. By the final &amp;quot;fade-out&amp;quot; of each
        cartoon, Jerry usually gets the best of Tom.
      &lt;/p&gt;
    &lt;/div&gt;
  );
};

export default LazyTom;
</code></pre>
<p>解释上述代码：</p>
<ul>
<li>我们创建了一个名为 <code>LazyTom</code> 的 ReactJS 组件。</li>
<li>这是一个简单的展示组件，包含一个标题和几段关于著名的 <code>Tom &amp; Jerry</code> 动画片中的猫 Tom 的描述。</li>
<li>最后，我们默认导出了该组件以便在其他地方导入。</li>
</ul>
<p>现在，在 <code>app/components/tom/</code> 目录下创建另一个名为 <code>tom-story.jsx</code> 的文件，代码如下：</p>
<pre><code class="language-jsx">// tom-story.jsx

"use client";

import { useState } from "react";
import dynamic from "next/dynamic";

const LazyTom = dynamic(() =&gt; import("./tom"), {
    loading: () =&gt; &lt;h1&gt;Loading Tom&amp;apos;s Story...&lt;/h1&gt;,
});

function TomStory() {
    const [shown, setShown] = useState(false);

    return (
        &lt;div className="flex flex-col m-8 w-[300px]"&gt;
            &lt;h2 className="text-xl my-1"&gt;Demonstrating &lt;strong&gt;dynamic&lt;/strong&gt;&lt;/h2&gt;
            &lt;button
                className="bg-blue-600 text-white rounded p-1"
                onClick={() =&gt; setShown(!shown)}
            &gt;
                Load 🐈 Tom&amp;apos;s Story
            &lt;/button&gt;

            {shown &amp;&amp; &lt;LazyTom /&gt;}
        &lt;/div&gt;
    );
}

export default TomStory;
</code></pre>
<p>以上代码实现了懒加载的主要部分：</p>
<ul>
<li>我们创建了一个名为 <code>TomStory</code> 的客户端组件，并使用 <code>"use client"</code> 指令。</li>
<li>首先，我们导入了用于管理切换状态的 <code>useState</code> 钩子，以及用于懒加载我们之前创建的组件的 <code>dynamic</code> 函数。</li>
<li><code>dynamic</code> 函数接受一个返回导入组件的函数作为参数。你还可以通过提供一个可选的配置对象来配置自定义加载消息。</li>
<li><code>dynamic()</code> 函数返回懒加载的组件实例，即 <code>LazyTom</code>（可以是任何名称）。但这个组件尚未加载。</li>
<li>在 JSX 中，我们有一个切换按钮用于显示和隐藏 <code>LazyTom</code> 组件。请注意，该组件将在首次点击按钮时懒加载到用户浏览器中。之后，如果您再次隐藏和显示它，<code>LazyTom</code> 组件将不会重新加载，除非我们硬刷新浏览器或清除浏览器缓存。</li>
<li>最后，我们默认导出了 <code>TomStory</code> 组件。</li>
</ul>
<p>现在我们需要测试它。为此，打开 <code>app/</code> 目录下的 <code>page.js</code> 文件，并用以下代码替换内容：</p>
<pre><code class="language-typescript">import TomStory from "./components/tom/tom-story";

export default function Home() {
  return (
    &lt;div className="flex flex-wrap justify-center "&gt;
      &lt;TomStory /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>这是一个简单的 ReactJS 组件，导入了 <code>TomStory</code> 组件并在其 JSX 中使用它。现在打开您的浏览器窗口。打开浏览器的开发者工具并打开 <code>网络</code> 选项卡。确保选中 <code>全部</code> 过滤器。</p>
<p><code>LazyTom</code>组件从 <code>tom.jsx</code> 文件还没有被下载，因为我们还没有点击 <code>加载 Tom 的故事</code> 按钮。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/07/Screenshot-2024-07-17-at-9.21.10-AM.png" alt="Image" width="600" height="400" loading="lazy"> <em>按钮来懒加载 Tom 的故事</em></p>
<p>现在，点击按钮。你应该会看到一个加载信息，然后组件会加载 Tom 的故事。你可以在 <code>Network</code> 选项卡中看到 <code>tom.jsx</code> 组件被列出，并且页面上也会渲染出 Tom 的故事。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/07/Screenshot-2024-07-17-at-9.27.55-AM.png" alt="Image" width="600" height="400" loading="lazy"> <em>现在 Tom 的故事已被懒加载</em></p>
<p>现在你已经体验了 <code>next/dynamic</code> 的 <code>dynamic</code> 函数如何帮助我们懒加载组件，让我们开始使用 <code>React.lazy()</code> 和 <code>Suspense</code> 的另一种技术。</p>
<h2 id="reactlazysuspense">使用 <code>React.lazy()</code> 和 <code>Suspense</code> 懒加载</h2>
<p>为展示此技术，让我们从 Jerry 的故事开始，这是我最喜欢的《猫和老鼠》卡通角色。</p>
<p>首先，我们将在 <code>app/components/</code> 目录下创建一个名为 <code>jerry</code> 的文件夹。现在，在 <code>app/components/jerry/</code> 目录下创建一个名为 <code>jerry.jsx</code> 的文件，并添加以下代码：</p>
<pre><code class="language-tsx">// jerry.jsx

const LazyJerry = () =&gt; {
  return (
    &lt;div className="flex flex-col justify-center"&gt;
      &lt;h1 className="text-3xl my-2"&gt;The Lazy Jerry&lt;/h1&gt;
      &lt;p className="text-xl my-1"&gt;
        Jerry 🐀, whose name is not explicitly mentioned in his debut appearance,
        is a small, brown house mouse who always lives in close proximity to
        Tom. Despite being very energetic, determined and much larger, Tom is no
        match for Jerry&amp;apos;s wits. Jerry possesses surprising strength for his
        size, approximately the equivalent of Tom&amp;apos;s, lifting items such as
        anvils with relative ease and withstanding considerable impacts.
      &lt;/p&gt;
      &lt;p className="text-xl my-1"&gt;
        Although cats typically chase mice to eat them, it is quite rare for Tom
        to actually try to eat Jerry. He tries to hurt or compete with him just
        to taunt Jerry, even as revenge, or to obtain a reward from a human,
        including his owner(s)/master(s), for catching Jerry, or for generally
        doing his job well as a house cat. By the final &amp;quot;fade-out&amp;quot; of each
        cartoon, Jerry usually gets the best of Tom.
      &lt;/p&gt;
    &lt;/div&gt;
  );
};

export default LazyJerry;
</code></pre>
<p><code>jerry.jsx</code> 的内容结构上与 <code>tom.jsx</code> 类似。这里我们发布了 Jerry 的故事，而不是 Tom 的，并默认导出了这个组件。</p>
<p>像上次一样，我们来创建一个 <code>jerry-story.jsx</code> 文件来展示 Jerry 的故事懒加载。将该文件创建在 <code>app/components/jerry/</code> 目录下，添加以下代码：</p>
<pre><code class="language-tsx">// jerry-story.jsx

"use client";

import React, { useState, Suspense } from "react";

const LazyJerry = React.lazy(() =&gt; import('./jerry'));

function JerryStory() {
    const [shown, setShown] = useState(false);

    return (
        &lt;div className="flex flex-col m-8 w-[300px]"&gt;
            &lt;h2 className="text-xl my-1"&gt; 演示 &lt;strong&gt;React.lazy()&lt;/strong&gt;&lt;/h2&gt;
            &lt;button
                className="bg-pink-600 text-white rounded p-1"
                onClick={() =&gt; setShown(!shown)}
            &gt;
                加载 🐀 Jerry 的故事
            &lt;/button&gt;

            {shown &amp;&amp; &lt;Suspense fallback={&lt;h1&gt;加载 Jerry 的故事&lt;/h1&gt;}&gt;
                &lt;LazyJerry /&gt;
            &lt;/Suspense&gt;}
        &lt;/div&gt;
    );
}

export default JerryStory;
</code></pre>
<p>这里我们也有一个客户端组件，我们将使用 React 的 <code>lazy()</code> 方法和 <code>Suspense</code>，所以我们导入了它们。像上次的 <code>dynamic()</code> 函数一样，<code>lazy()</code> 函数也需要一个返回懒加载组件的函数作为参数。我们还提供了要加载的组件的相对路径。</p>
<p>注意，用 <code>dynamic()</code> 我们可以将加载信息作为函数的一部分进行定制。而用 <code>lazy()</code>，我们将在 <code>Suspense</code> 的 fallback 中进行处理。</p>
<p>Suspense 在等待数据加载时会使用 fallback。如果你想深入了解 ReactJS 的 Suspense 和 Error Boundary，可以<a href="https://www.youtube.com/watch?v=OpHbSHp8PcI">查看这个视频教程</a>。</p>
<p>在这里，由于我们的 <code>LazyJerry</code> 组件是懒加载的，我们提供了一个 fallback 信息，在组件代码成功下载并渲染到浏览器之前显示一个加载信息。</p>
<pre><code class="language-tsx">{shown &amp;&amp; 
    &lt;Suspense fallback={&lt;h1&gt;加载 Jerry 的故事&lt;/h1&gt;}&gt;
                &lt;LazyJerry /&gt;
    &lt;/Suspense&gt;
}
</code></pre>
<p>此外，正如你所看到的，我们在第一次点击按钮时加载组件。这里组件不会在每次点击按钮时重新加载，除非我们刷新浏览器或清除浏览器缓存。</p>
<p>现在通过将其导入 <code>page.js</code> 文件并将组件添加到 JSX 中来进行测试。</p>
<pre><code class="language-tsx">// page.js

import TomStory from "./components/tom/tom-story";
import JerryStory from "./components/jerry/jerry-story"; 

export default function Home() {
  return (
    &lt;div className="flex flex-wrap justify-center "&gt;
      &lt;TomStory /&gt;
      &lt;JerryStory /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/07/Screenshot-2024-07-17-at-9.33.36-AM.png" alt="Image" width="600" height="400" loading="lazy"> <em>懒加载 Jerry 故事的按钮</em></p>
<p>现在，点击按钮。你会看到组件被加载，并且你可以在网络选项卡的列表中看到它。你应该能够看到作为懒加载组件一部分渲染的 Jerry 的故事。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/07/Screenshot-2024-07-17-at-9.37.30-AM.png" alt="Image" width="600" height="400" loading="lazy"> <em>Jerry 的故事已经被懒加载</em></p>
<h2 id="">如何懒加载命名导出的组件</h2>
<p>到目前为止，我们已经用两种技术导入了用 <code>default export</code> 导出的组件，并且懒加载了它。在 JavaScript（以及在 React 中），你可以通过两种不同的方式导入和导出模块：</p>
<ul>
<li>使用 <code>default</code> 关键字。在这种情况下，导出的模块可以用任何名称导入。如果你想从一个模块中只导出一个功能，你会使用这种方式。</li>
<li>不使用 <code>default</code> 关键字，这被称为 <code>命名导出</code>。在这种情况下，你必须在导出和导入时保持相同的模块名称。在导入时，还需要将模块名包含在大括号（{...}）中。如果你想从一个模块中导出多个功能，你会使用这种方式。</li>
</ul>
<p>如果你想深入了解 JavaScript 模块以及它们是如何工作的，我建议你浏览一下 freeCodeCamp YouTube 频道上发布的<a href="https://www.youtube.com/watch?v=KeBxopnhizw">这门速成课</a>。</p>
<p>为了演示 <code>命名导出</code> 组件的懒加载，让我们创建另一个简单的展示用 React 组件。这次我们将使用 Tom &amp; Jerry 动画中的愤怒但可爱的狗 <code>Spike</code>。</p>
<p>在 <code>app/components/</code> 目录下创建一个名为 <code>spike</code> 的文件夹。现在，在 <code>app/components/spike/</code> 目录下创建一个名为 <code>spike.jsx</code> 的文件，并添加以下代码：</p>
<pre><code class="language-jsx">// spike.jsx

export const LazySpike = () =&gt; {
  return (
    &lt;div className="flex flex-col"&gt;
      &lt;h1 className="text-3xl my-2"&gt;The Lazy Spike&lt;/h1&gt;
      &lt;p className="text-xl my-1"&gt;
        In his attempts to catch Jerry, Tom often has to deal with Spike 🦮, known
        as &amp;quot;Killer&amp;quot; and &amp;quot;Butch&amp;quot; in some shorts, an angry, vicious but easily
        duped bulldog who tries to attack Tom for bothering him or his son Tyke
        while trying to get Jerry. Originally, Spike was unnamed and mute, aside
        from howls and biting noises as well as attacking indiscriminately, not
        caring whether it was Tom or Jerry though usually attacking Tom.
      &lt;/p&gt;
      &lt;p className="text-xl my-1"&gt;
      In
        later cartoons, Spike spoke often, using a voice and expressions,
        performed by Billy Bletcher and later Daws Butler, modeled after
        comedian Jimmy Durante. Spike&amp;apos;s coat has altered throughout the years
        between gray and creamy tan. The addition of Spike&amp;apos;s son Tyke in the
        late 1940s led to both a slight softening of Spike&amp;apos;s character and a
        short-lived spin-off theatrical series called Spike and Tyke.
      &lt;/p&gt;
    &lt;/div&gt;
  );
};
</code></pre>
<p>同样，这个组件在结构上与之前看到的 <code>tom.jsx</code> 和 <code>jerry.jsx</code> 组件完全相同，但有两个主要区别：</p>
<ol>
<li>这里，我们没有使用默认关键词导出组件，因此它是一个 <code>命名导出</code>。</li>
<li>我们在讲述的是狗，Spike。</li>
</ol>
<p>现在，我们需要处理一个命名导出组件的懒加载，并且这与默认导出组件略有不同。</p>
<p>在 <code>app/components/spike/</code> 目录下创建一个名为 <code>spike-story.jsx</code> 的文件，并添加以下代码：</p>
<pre><code class="language-jsx">// spike-story.jsx

"use client";

import { useState } from "react";
import dynamic from "next/dynamic";

const LazySpike = dynamic(() =&gt; import("./spike").then((mod) =&gt; mod.LazySpike), {
    loading: () =&gt; &lt;h1&gt;Loading Spike&amp;apos;s Story...&lt;/h1&gt;,
});

function SpikeStory() {
    const [shown, setShown] = useState(false);

    return (
        &lt;div className="flex flex-col m-8 w-[300px]"&gt;
            &lt;h2 className="text-xl my-1"&gt;Demonstrating &lt;strong&gt;Named Export&lt;/strong&gt;&lt;/h2&gt;
            &lt;button
                className="bg-slate-600 text-white rounded p-1"
                onClick={() =&gt; setShown(!shown)}
            &gt;
                Load 🦮 Spike&amp;apos;s Story
            &lt;/button&gt;

            {shown &amp;&amp; &lt;LazySpike /&gt;}
        &lt;/div&gt;
    );
}

export default SpikeStory;
</code></pre>
<p>像 <code>tom-story</code> 一样，我们使用了 next/dynamic 的动态导入。但是让我们来深入分析一下上面的代码块：</p>
<pre><code>const LazySpike = dynamic(() =&gt; import("./spike").then((mod) =&gt; mod.LazySpike), {
    loading: () =&gt; &lt;h1&gt;加载 Spike 的故事...&lt;/h1&gt;,
});
</code></pre>
<p>你会注意到的变化是我们使用 <code>.then()</code> 处理函数明确地从 <code>import("./spike")</code> 函数中解析了 promise。我们首先获取模块，然后通过其实际名称选择导出的组件——在这种情况下是 <code>LazySpike</code>。其余的事情与 <code>tom-story</code> 中的情况保持不变。</p>
<p>现在，为了测试它，再次将组件导入到 <code>page.js</code> 文件中，并像前两次那样在 JSX 中使用它。</p>
<pre><code class="language-javascript">// page.js

import TomStory from "./components/tom/tom-story";
import JerryStory from "./components/jerry/jerry-story";
import SpikeStory from "./components/spike/spike-story"; 
</code></pre>
<p>那里 – 你应该会在浏览器上看到新组件，并带有一个按钮，从尚未加载的 <code>spike.jsx</code> 文件中加载 Spike 的故事。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/07/Screenshot-2024-07-17-at-9.59.55-AM.png" alt="图片" width="600" height="400" loading="lazy"> <em>Lazy 加载 Spike 的故事的按钮</em></p>
<p>点击按钮将加载文件到浏览器，并渲染组件来展示 Spike 的故事。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/07/Screenshot-2024-07-17-at-10.02.01-AM.png" alt="图片" width="600" height="400" loading="lazy"> <em>Spike 的故事已懒加载</em></p>
<p>下面你可以看到所有三个组件侧并肩展示三种不同的技术和使用案例。你可以一起测试它们。下面的图片展示了两个组件并行地懒加载，而另一个组件已经被懒加载了。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/07/Screenshot-2024-07-17-at-10.14.21-AM.png" alt="图片" width="600" height="400" loading="lazy"> <em>并行懒加载多个组件</em></p>
<p>这是另一个案例，通过点击各自的按钮，所有三个组件都按需懒加载了。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/07/Screenshot-2024-07-17-at-10.05.35-AM-1.png" alt="图片" width="600" height="400" loading="lazy"> <em>所有的故事都被懒加载</em></p>
<h2 id="">懒加载你的服务器组件</h2>
<p>我们谈到了客户端组件的懒加载技术。我们是否也可以对服务器组件使用相同的技术呢？其实你可以，但不需要，因为服务器组件已经是 <code>代码分割</code> 的，加载方面已经由 Next.js 处理好了。如果你尝试这样做也不会出现任何类型的错误，但这是不必要的。</p>
<p>如果你动态导入一个拥有一个或多个客户端组件作为子组件的服务器组件，那些客户端组件将被懒加载。但对于（父）服务器组件本身不会有任何影响。</p>
<p>下面是一个包含两个客户端组件作为子组件的服务器组件的示例：</p>
<pre><code class="language-jsx">// server-comp.jsx

import ComponentA from "./a-client-comp";
import ComponentB from "./b-client-comp";

import React from 'react'

const AServerComp = () =&gt; {
  return (
    &lt;div className="flex flex-col m-8 w-[300px]"&gt;
      &lt;ComponentA /&gt;
      &lt;ComponentB /&gt;
    &lt;/div&gt;
  )
}

export default AServerComp
</code></pre>
<p>现在，我们将动态导入服务器组件到 <code>page.js</code> 文件中并在 JSX 中使用它。动态导入服务器组件的子客户端组件将被懒加载，但服务器组件本身不会。</p>
<pre><code class="language-jsx">// page.js

import dynamic from "next/dynamic";

import TomStory from "./components/tom/tom-story";
import JerryStory from "./components/jerry/jerry-story";
import SpikeStory from "./components/spike/spike-story";

const AServerComp = dynamic(() =&gt; import('./components/server-comps/server-comp'), {
  loading: () =&gt; &lt;h1&gt;Loading Through Server Component...&lt;/h1&gt;,
})


export default function Home() {
  return (
    &lt;div className="flex flex-wrap justify-center "&gt;
      &lt;TomStory /&gt;
      &lt;JerryStory /&gt;
      &lt;SpikeStory /&gt;

      &lt;AServerComp /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<h2 id="nextjs">我们应该在 Next.js 中懒加载所有客户端组件吗</h2>
<p>当我第一次开始学习懒加载时，我也有这个问题。现在我对这项技术有了更多的了解，这是我的看法：</p>
<p>你并不需要懒加载所有的客户端组件。优化是很好的，但过度优化可能会有反作用。你需要识别哪些地方需要这些优化。</p>
<ul>
<li>你的客户端组件是否真的很笨重？</li>
<li>你是否不必要地把很多东西放进一个组件，你应该拆分并重构它吗？</li>
<li>你是否在你的客户端组件中导入了沉重的库？</li>
<li>你选择了 tree-shaking 吗？</li>
<li>你是否可以按路由标记笨重的客户端组件，并且没有问题在该路由的页面初始加载时不加载它们中的一些或全部？</li>
</ul>
<p>如你所见，这些是在你开始优化前需要问的几个有意义的问题。一旦你有了答案，并决定需要懒加载，那么你可以实现你从这篇文章中学到的技术。</p>
<h2 id="">接下来是什么</h2>
<p>暂时就这些了。你喜欢阅读这篇文章并学到了一些新东西吗？如果是这样，我很想知道内容是否对你有帮助。以下是我的社交媒体账号。</p>
<p>接下来，如果你愿意学习 <code>Next.js</code> 及其生态系统如 <code>Next-Auth(V5)</code> 的基本概念和项目，我有个好消息给你：你可以在我的 <a href="https://www.youtube.com/watch?v=VSB2h7mVhPg&amp;list=PLIJrr73KDmRwz_7QUvQ9Az82aDM9I8L_8">YouTube</a> 频道上查看这个播放列表，目前有 20+ 个视频教程和超过 11 小时的精彩内容，免费提供。我希望你也喜欢这些内容。</p>
<p>欢迎和我联系。</p>
<ul>
<li>订阅我的 <a href="https://www.youtube.com/tapasadhikary?sub_confirmation=1">YouTube 频道</a>。</li>
<li><a href="https://twitter.com/tapasadhikary">在 X (Twitter)</a> 或者 <a href="https://www.linkedin.com/in/tapasadhikary/">LinkedIn</a> 上关注我，不要错过每日技能提升的推文。</li>
<li>查看并关注我在 <a href="https://github.com/atapas">GitHub</a> 上的开源作品。</li>
<li>我定期在我的 <a href="https://blog.greenroots.info/">GreenRoots 博客</a> 上发布有意义的文章，你可能也会发现它们对你有帮助。</li>
</ul>
<p>下篇文章见。不管怎样，请照顾好自己，并保持学习。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 从 0 到 1 学习 Go ]]>
                </title>
                <description>
                    <![CDATA[ 让我们先对 Go（或称 Golang ）做一个小小的介绍。Go 是由谷歌工程师 Robert Griesemer、Rob Pike 和 Ken Thompson 设计的。它是一种静态类型的、编译的语言。第一个版本于 2012 年 3 月作为开源版本发布。 > "Go 是一种开源的编程语言，它使人们能够轻松地构建简单、可靠和高效的软件"。- GoLang 在许多编程语言中，有许多方法来解决一个特定的问题。程序员要花很多时间去思考解决它的最佳方法。 Go 却相信用较少的功能--只有一种正确的方式来解决问题。 这为开发人员节省了时间，并使大型代码库易于维护。 Go 中没有像 maps 和 filters 这样的 "表达性 "功能。 > "当你有增加表现力的功能时，通常会增加系统开销。"--Rob Pike 最近发表的新的 golang 标志: https://blog.golang.org/go-brand 入门 Go 是由 packages（包）组成的。package main 告诉 Go 编译器，该程序被编译为可执行文件，而不是共享库。它是一个应用程序的入口点。pa ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learning-go-from-zero-to-hero-d2a3223b3d86/</link>
                <guid isPermaLink="false">668227379100b5049d88a6ae</guid>
                
                    <category>
                        <![CDATA[ Golang ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Mon, 01 Jul 2024 04:15:36 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/07/1_30aoNxlSnaYrLhBT0O1lzw.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/learning-go-from-zero-to-hero-d2a3223b3d86/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Learning Go — from zero to hero</a>
      </p><!--kg-card-begin: markdown--><p>让我们先对 Go（或称 Golang ）做一个小小的介绍。Go 是由谷歌工程师 Robert Griesemer、Rob Pike 和 Ken Thompson 设计的。它是一种静态类型的、编译的语言。第一个版本于 2012 年 3 月作为开源版本发布。</p>
<blockquote>
<p>"Go 是一种开源的编程语言，它使人们能够轻松地构建简单、可靠和高效的软件"。- GoLang</p>
</blockquote>
<p>在许多编程语言中，有许多方法来解决一个特定的问题。程序员要花很多时间去思考解决它的最佳方法。</p>
<p>Go 却相信用较少的功能--只有一种正确的方式来解决问题。</p>
<p>这为开发人员节省了时间，并使大型代码库易于维护。 Go 中没有像 <code>maps</code> 和 <code>filters</code> 这样的 "表达性 "功能。</p>
<blockquote>
<p>"当你有增加表现力的功能时，通常会增加系统开销。"--Rob Pike</p>
</blockquote>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*AUiSG5Gqz8MzaGCvGpckGA.png" alt="1*AUiSG5Gqz8MzaGCvGpckGA" width="800" height="400" loading="lazy"></p>
<p>最近发表的新的 golang 标志: <a href="https://blog.golang.org/go-brand">https://blog.golang.org/go-brand</a></p>
<h3 id="">入门</h3>
<p>Go 是由 packages（包）组成的。package main 告诉 Go 编译器，该程序被编译为可执行文件，而不是共享库。它是一个应用程序的入口点。package main 的定义如下:</p>
<pre><code class="language-go">package main
</code></pre>
<p>让我们继续前进，在 Go workspace 创建一个 <code>main.go</code> 文件，编写一个简单的 hello world 例子。</p>
<h4 id="workspace"><strong>Workspace</strong></h4>
<p>Go 中的 workspace 是由环境变量 <code>GOPATH</code> 定义的。</p>
<p>你写的任何代码都要写在 workspace 里面。Go 将搜索 <code>GOPATH</code> 目录内的任何软件包，或者 <code>GOROOT</code> 目录，该目录在安装 Go 时默认设置。<code>GOROOT</code> 是安装 Go 的路径。</p>
<p>设置 <code>GOPATH</code> 到你想要的目录。现在，让我们把它添加到 <code>~/workspace</code> 文件夹内。</p>
<pre><code class="language-shell"># export env
export GOPATH=~/workspace
# go inside the workspace directory
cd ~/workspace
</code></pre>
<p>在我们刚刚创建的 workspace 文件夹中创建 <code>main.go</code> 文件，其中包含以下代码。</p>
<h4 id="helloworld">Hello World</h4>
<pre><code class="language-go">package main

import (
 "fmt"
)

func main(){
  fmt.Println("Hello World!")
}
</code></pre>
<p>在上面的例子中，<code>fmt</code>是 Go 中的一个内置包，它实现了用于格式化 I/O 输出的函数。</p>
<p>我们通过使用 <code>import</code> 关键字在 Go 中导入一个包。<code>func main</code> 是代码被执行的主入口点。<code>Println</code> 是包 <code>fmt</code> 中的一个函数，它为我们打印出 "hello world"。</p>
<p>让我们通过运行这个文件来看看。我们有两种方法可以运行 Go 命令。正如我们所知，Go 是一种编译语言，所以我们首先需要在执行之前编译它。</p>
<pre><code class="language-shell">&gt; go build main.go
</code></pre>
<p>这将创建一个二进制可执行文件<code>main</code>，现在我们可以运行:</p>
<pre><code class="language-shell">&gt; ./main 
# Hello World!
</code></pre>
<p>还有一种更简单的方法来运行程序。<code>go run</code> 命令有助于抽象编译步骤（译者注：直接执行源码中的 main() 函数，不会在当前目录留下可执行文件）。你可以简单地运行以下命令来执行该程序。</p>
<pre><code class="language-shell">go run main.go
# Hello World!
</code></pre>
<p><strong><em>注意</em></strong> :  <em>要尝试本博客中提到的代码，你可以使用 <a href="https://play.golang.org/">https://play.golang.org</a></em></p>
<h3 id="variables">Variables（变量）</h3>
<p>Go 中的变量是明确声明的。Go 是一种静态类型的语言。这意味着在声明变量的时候会检查变量的类型。一个变量:</p>
<pre><code class="language-go">var a int
</code></pre>
<p>在这种情况下，值将被设置为 0。 使用下面的语法来声明和初始化一个具有不同值的变量:</p>
<pre><code class="language-go">var a = 1
</code></pre>
<p>这里的变量被自动分配为<code>int</code>。 我们可以对变量的声明使用一个简短定义，即:</p>
<pre><code class="language-go">message := "hello world"
</code></pre>
<p>我们也可以在同一行中声明多个变量:</p>
<pre><code class="language-go">var b, c int = 2, 3
</code></pre>
<h3 id="datatypes">Data types（数据类型）</h3>
<p>像其他编程语言一样，Go 支持各种不同的数据结构。让我们来探索其中:</p>
<h4 id="numberstringandboolean"><strong>Number, String, and Boolean (整型 字符串和布尔值)</strong></h4>
<p>支持的整型包括 int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、uintptr（译者注：无符号整型，长度跟平台相关，它的长度可以用来保存一个指针地址）等。</p>
<p>字符串类型存储一个字节序列。它用关键字 <code>string</code> 来表示和声明。</p>
<p>布尔值使用关键字 <code>bool</code> 来存储。</p>
<p>Go 也支持复数类型，可以用 <code>complex64</code> 和 <code>complex128</code> 来声明。</p>
<pre><code class="language-go">var a bool = true
var b int = 1
var c string = 'hello world'
var d float32 = 1.222
var x complex128 = cmplx.Sqrt(-5 + 12i)
</code></pre>
<h4 id="arraysslicesandmaps">Arrays, Slices, and Maps（数组、切片和映射）</h4>
<p>数组是由相同数据类型的元素组成的一个序列。数组在声明时有一个固定的长度，所以它不能被扩大到超过这个长度。声明一个数组：</p>
<pre><code class="language-go">var a [5]int
</code></pre>
<p>数组也可以是多维的。我们可以简单地用以下方式创建它们:</p>
<pre><code class="language-go">var multiD [2][3]int
</code></pre>
<p>在运行时更改数组是受限的。数组也没有提供获取子数组的能力。 为此，Go 有一种数据类型，叫做切片（slices）。</p>
<p>切片存储了一连串的元素，并且可以在任何时候扩展。切片声明与数组声明类似--但没有定义容量:</p>
<pre><code class="language-go">var b []int
</code></pre>
<p>这将创建一个容量为 0、长度为 0 的切片。也可以用容量和长度来定义切片。我们可以用下面的语法来定义它：</p>
<pre><code class="language-go">numbers := make([]int,5,10)
</code></pre>
<p>这里，切片的初始长度为 5，容量为 10。</p>
<p>切片是对数组的一种抽象。切片使用一个数组作为底层结构。一个切片包含三个部分：容量、长度和一个指向底层数组的指针，如下图所示:</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*P0lNCO0sQwIYHLEX_mfSOQ.png" alt="1*P0lNCO0sQwIYHLEX_mfSOQ" width="517" height="193" loading="lazy"></p>
<p>图片源自: <a href="https://blog.golang.org/go-slices-usage-and-internals">https://blog.golang.org/go-slices-usage-and-internals</a></p>
<p>一个切片的容量可以通过使用 append 或 copy 函数来增加。append 函数将值添加到数组的末端，如果需要的话也可以增加容量。</p>
<pre><code class="language-go">numbers = append(numbers, 1, 2, 3, 4)
</code></pre>
<p>另一种增加切片容量的方法是使用 copy 函数。简单地创建另一个容量更大的切片，并将原来的切片复制到新创建的切片上:</p>
<pre><code class="language-go">// create a new slice
number2 := make([]int, 15)
// copy the original slice to new slice
copy(number2, number)
</code></pre>
<p>我们可以创建一个切片的子切片。这可以通过以下命令简单地完成：</p>
<pre><code class="language-go">// initialize a slice with 4 len and values
number2 = []int{1,2,3,4}
fmt.Println(numbers) // -&gt; [1 2 3 4]
// create sub slices
slice1 := number2[2:]
fmt.Println(slice1) // -&gt; [3 4]
slice2 := number2[:3]
fmt.Println(slice2) // -&gt; [1 2 3]
slice3 := number2[1:4]
fmt.Println(slice3) // -&gt; [2 3 4]
</code></pre>
<p>映射是 Go 中的一种数据类型，它将键映射到值。我们可以使用以下命令来定义一个 map：</p>
<pre><code class="language-go">var m map[string]int
</code></pre>
<p><code>m</code> 是新的 map 变量, 它的键是 <code>string</code> 类型， 值是 <code>integers</code> 类型。我们很容易在 map 上添加键值对:</p>
<pre><code class="language-go">// adding key/value
m['clearity'] = 2
m['simplicity'] = 3
// printing the values
fmt.Println(m['clearity']) // -&gt; 2
fmt.Println(m['simplicity']) // -&gt; 3
</code></pre>
<h3 id="typecasting"><strong>Typecasting（类型转换）</strong></h3>
<p>一种类型的数据类型可以通过类型转换转换为另一种类型。让我们看看一个简单的类型转换：</p>
<pre><code class="language-go">a := 1.1
b := int(a)
fmt.Println(b)
//-&gt; 1
</code></pre>
<p>不是所有类型的数据类型都可以转换为另一种类型。请确保数据类型与转换的内容相匹配。</p>
<h3 id="conditionalstatements">Conditional Statements（条件语句）</h3>
<h4 id="ifelse">if else</h4>
<p>对于条件性语句，我们可以使用 if-else 语句，如下例所示。请确保大括号与条件语句在同一行。</p>
<pre><code class="language-go">if num := 9; num &lt; 0 {
 fmt.Println(num, "is negative")
} else if num &lt; 10 {
 fmt.Println(num, "has 1 digit")
} else {
 fmt.Println(num, "has multiple digits")
}
</code></pre>
<h4 id="switchcase">switch case</h4>
<p>Switch cases 有助于组织多个条件语句。下面的例子显示了一个简单的 switch 语句：</p>
<pre><code class="language-go">i := 2
switch i {
case 1:
 fmt.Println("one")
case 2:
 fmt.Println("two")
default:
 fmt.Println("none")
}
</code></pre>
<h3 id="looping">Looping（循环）</h3>
<p>Go 有一个循环的关键词 <code>for</code>。for 循环命令用于实现不同种类的循环：</p>
<pre><code class="language-go">i := 0
sum := 0
for i &lt; 10 {
 sum += 1
  i++
}
fmt.Println(sum)
</code></pre>
<p>上面的例子类似于 C 语言中的 while 循环。</p>
<p>Go 中的 for 语句也可以用于普通的 for 循环：</p>
<pre><code class="language-go">sum := 0
for i := 0; i &lt; 10; i++ {
  sum += i
}
fmt.Println(sum)
</code></pre>
<p>Go 中的死循环:</p>
<pre><code class="language-go">for {
}
</code></pre>
<h3 id="pointers">Pointers（指针）</h3>
<p>Go 提供了指针。指针是用来保存一个变量的地址的地方。指针是由 * 定义的。指针是根据数据的类型来定义的，例如：</p>
<pre><code class="language-go">var ap *int
</code></pre>
<p><code>ap</code> 是指向一个整数类型的指针。 <code>&amp;</code> 操作符可以用来获取一个变量的地址。</p>
<pre><code class="language-go">a := 12
ap = &amp;a
</code></pre>
<p>指针所指向的值可以使用 <code>*</code> 操作符来访问：</p>
<pre><code class="language-go">fmt.Println(*ap)
// =&gt; 12
</code></pre>
<p>在传递结构体作为参数时，或者在为定义的类型声明方法时，通常倾向于使用指针。</p>
<ol>
<li>传递值时，值实际上被复制，这意味着需要更多的内存。</li>
<li>传递指针时，函数改变的值会反映回方法/函数的调用者。</li>
</ol>
<p>例如:</p>
<pre><code class="language-go">func increment(i *int) {
  *i++
}
func main() {
  i := 10
  increment(&amp;i)
  fmt.Println(i)
}
//=&gt; 11
</code></pre>
<p>注意：当你在尝试博客中的示例代码时，不要忘记包含 <code>package main</code>，并在需要时导入 <code>fmt</code> 或其他包，如上面第一个 main.go 例子中所示。</p>
<h3 id="functions">Functions（函数）</h3>
<p>在 main package 中定义的 main 函数是 go 程序执行的入口。更多的函数可以被定义和使用。让我们来看看一个简单的例子。</p>
<pre><code class="language-go">func add(a int, b int) int {
  c := a + b
  return c
}
func main() {
  fmt.Println(add(2, 1))
}
//=&gt; 3
</code></pre>
<p>在上面的例子中我们可以看到，Go 函数是用 <strong>func</strong> 关键字来定义的，后面是函数名称。一个函数的 <strong>参数</strong> 需要根据其数据类型来定义，最后是返回的数据类型。</p>
<p>一个函数的返回值也可以在函数中预先定义：</p>
<pre><code class="language-go">func add(a int, b int) (c int) {
  c = a + b
  return
}
func main() {
  fmt.Println(add(2, 1))
}
//=&gt; 3
</code></pre>
<p>这里 c 被定义为返回变量。所以定义的变量 c 会自动返回，而不需要在最后的返回语句中定义。</p>
<p>你也可以从一个函数中返回多个返回值，用逗号来分隔返回值。</p>
<pre><code class="language-go">func add(a int, b int) (int, string) {
  c := a + b
  return c, "successfully added"
}
func main() {
  sum, message := add(2, 1)
  fmt.Println(message)
  fmt.Println(sum)
}
</code></pre>
<h3 id="methodstructsandinterfaces">Method, Structs, and Interfaces（方法，结构体，接口）</h3>
<p>Go 并不是一种完全面向对象的语言，但通过结构体（Struct）、接口（Interface）和方法（Method），它有很多面向对象的支持和感觉。</p>
<h4 id="struct">Struct（结构体）</h4>
<p>结构体是一种类型化的、不同字段的集合。结构体用于将数据分组。例如，如果我们想对 Person 类型的数据进行分组，我们可以定义一个人的属性，其中可能包括姓名、年龄、性别。可以使用以下语法来定义一个结构体:</p>
<pre><code class="language-go">type person struct {
  name string
  age int
  gender string
}
</code></pre>
<p>在定义了一个人的类型结构后，现在让我们来创建一个 person：</p>
<pre><code class="language-go">//way 1: specifying attribute and value
p = person{name: "Bob", age: 42, gender: "Male"}
//way 2: specifying only value
person{"Bob", 42, "Male"}
</code></pre>
<p>我们可以很容易地用一个点（.）来访问这些数据。</p>
<pre><code class="language-go">p.name
//=&gt; Bob
p.age
//=&gt; 42
p.gender
//=&gt; Male
</code></pre>
<p>你也可以用结构的指针直接访问其属性：</p>
<pre><code class="language-go">pp = &amp;person{name: "Bob", age: 42, gender: "Male"}
pp.name
//=&gt; Bob
</code></pre>
<h4 id="methods">Methods（方法）</h4>
<p>方法(Method)是一种特殊的函数类型，它有一个 <em>receiver</em> 。 <em>receiver</em> 可以是一个值或一个指针。让我们创建一个名为 describe 的方法(Method)，它有一个我们在上面的例子中创建的接收器类型的 person：</p>
<pre><code class="language-go">package main
import "fmt"

// struct definition
type person struct {
  name   string
  age    int
  gender string
}

// method definition
func (p *person) describe() {
  fmt.Printf("%v is %v years old.", p.name, p.age)
}
func (p *person) setAge(age int) {
  p.age = age
}

func (p person) setName(name string) {
  p.name = name
}

func main() {
  pp := &amp;person{name: "Bob", age: 42, gender: "Male"}
  pp.describe()
  // =&gt; Bob is 42 years old
  pp.setAge(45)
  fmt.Println(pp.age)
  //=&gt; 45
  pp.setName("Hari")
  fmt.Println(pp.name)
  //=&gt; Bob
}
</code></pre>
<p>正如我们在上面的例子中看到的，现在可以使用点运算符来调用该方法，如 <code>pp.describe</code>。请注意，<em>receiver</em> 是一个指针。使用指针，我们传递的是一个值的引用，所以如果我们在方法中做任何改变，都会反映在  <em>receiver</em> pp 中。它也不会创建一个新的对象的副本，这就节省了内存。</p>
<p>请注意，在上面的例子中，年龄的值被改变了，而名字的值没有改变，因为 setName 方法是 <em>receiver</em> 类型是值类型，而 setAge 是指针类型的。</p>
<h4 id="interfaces">Interfaces（接口）</h4>
<p>Go 接口（interfaces）是一个方法（methods）的集合。接口有助于将一个类型的属性组合在一起。让我们以一个接口 animal 为例：</p>
<pre><code class="language-go">type animal interface {
  description() string
}
</code></pre>
<p>animal 是一个接口（interface）类型。现在让我们创建两个不同类型的 animal，它们都实现了 animal 接口类型：</p>
<pre><code class="language-go">package main

import (
  "fmt"
)

type animal interface {
  description() string
}

type cat struct {
  Type  string
  Sound string
}

type snake struct {
  Type      string
  Poisonous bool
}

func (s snake) description() string {
  return fmt.Sprintf("Poisonous: %v", s.Poisonous)
}

func (c cat) description() string {
  return fmt.Sprintf("Sound: %v", c.Sound)
}

func main() {
  var a animal
  a = snake{Poisonous: true}
  fmt.Println(a.description())
  a = cat{Sound: "Meow!!!"}
  fmt.Println(a.description())
}

//=&gt; Poisonous: true
//=&gt; Sound: Meow!!!
</code></pre>
<p>type cat struct {<br>
在主函数中，我们创建一个动物类型的变量 <code>a</code>。我们给动物分配一个 snake 和一个 cat 的类型，并使用 Println 来打印 a.description。由于我们在两种类型（cat 和 snake）中都以不同的方式实现了 describe 方法，我们得到了打印的动物描述。</p>
<h3 id="packages">Packages（包）</h3>
<p>我们把 Go 的所有代码都写在一个包里。<strong>main</strong> package 是程序执行的入口点。Go 中有很多内置包。我们一直在使用的最著名的是<strong>fmt</strong>包。</p>
<blockquote>
<p>"Go 软件包是 Go 提供的大型编程的主要机制，它们使得将一个大型项目分割成小块成为可能。"<br>
— Robert Griesemer</p>
</blockquote>
<h4 id="installingapackage">Installing a package (安装一个包)</h4>
<pre><code class="language-shell">go get &lt;package-url-github&gt;
// example
go get github.com/satori/go.uuid
</code></pre>
<p>我们安装的软件包被保存在 GOPATH 环境变量设置的工作目录。你可以通过进入我们工作目录下的 pkg 文件夹 <code>cd $GOPATH/pkg</code> 来查看这些软件包。</p>
<h4 id="somebuiltinpackagesingogo">Some built-in packages in Go（Go 内置包）</h4>
<p><strong>fmt</strong></p>
<p>该包实现了格式化的 I/O 函数。我们已经用这个包实现了向 stdout 打印的功能。</p>
<p><strong>json</strong></p>
<p>Go 中另一个有用的包是 json 包。这有助于对 JSON 进行编码/解码。让我们举个例子，对一些 JSON 进行编码/解码：</p>
<p>编码</p>
<pre><code class="language-go">package main

import (
  "fmt"
  "encoding/json"
)

func main(){
  mapA := map[string]int{"apple": 5, "lettuce": 7}
  mapB, _ := json.Marshal(mapA)
  fmt.Println(string(mapB))
}
</code></pre>
<p>解码</p>
<pre><code class="language-go">package main

import (
  "fmt"
  "encoding/json"
)

type response struct {
  PageNumber int `json:"page"`
  Fruits []string `json:"fruits"`
}

func main(){
  str := `{"page": 1, "fruits": ["apple", "peach"]}`
  res := response{}
  json.Unmarshal([]byte(str), &amp;res)
  fmt.Println(res.PageNumber)
}
//=&gt; 1
</code></pre>
<p>当使用 unmarshal 解码 json 字节时，第一个参数是 json 字节，第二个参数是我们希望 json 被映射到的响应类型结构的地址。注意，<code>json: "page"</code>将页面键映射到结构中的 PageNumber 键。</p>
<h3 id="errorhandling">Error Handling（错误处理）</h3>
<p>错误是指程序中不想要的和意外的结果。比方说，我们正在对一个外部服务进行 API 调用。这个 API 调用可能是成功的，也可能是失败的。当错误类型出现时，Go 程序中的错误可以被识别。让我们看看这个例子:</p>
<pre><code class="language-go">resp, err := http.Get("http://example.com/")
</code></pre>
<p>在这里，API 调用可能通过也可能失败。我们可以检查错误是否为 <code>nil</code>或存在，并相应地处理响应:</p>
<pre><code class="language-go">package main

import (
  "fmt"
  "net/http"
)

func main(){
  resp, err := http.Get("http://example.com/")
  if err != nil {
    fmt.Println(err)
    return
  }
  fmt.Println(resp)
}
</code></pre>
<h4 id="returningcustomerrorfromafunction">Returning custom error from a function (从函数返回自定义错误)</h4>
<p>当我们在编写自己的函数时，有些情况下会出现错误。这些错误可以在错误对象的帮助下返回:</p>
<pre><code class="language-go">func Increment(n int) (int, error) {
  if n &lt; 0 {
    // return error object
    return nil, errors.New("math: cannot process negative number")
  }
  return (n + 1), nil
}
func main() {
  num := 5
 
  if inc, err := Increment(num); err != nil {
    fmt.Printf("Failed Number: %v, error message: %v", num, err)
  }else {
    fmt.Printf("Incremented Number: %v", inc)
  }
}
</code></pre>
<p>大多数 Go 中内置的包，或者我们使用的外部包，都有一个错误处理的机制。所以我们调用的任何函数都有可能出现错误。这些错误绝不应该被忽视，总是在我们调用这些函数的地方优雅地处理，正如我们在上面的例子中所做的那样。</p>
<h4 id="panic">Panic</h4>
<p>Panic 是指在程序执行过程中突然遇到的未被处理的东西。在 Go 中，Panic 不是处理程序中异常的理想方式。建议使用一个错误对象来代替。当 Panic 发生时，程序的执行会被停止。Panic 发生后被执行的东西是 defer。</p>
<pre><code class="language-go">//Go
package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func f() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i &gt; 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}
</code></pre>
<h4 id="defer">Defer</h4>
<p>Defer 是指总是在函数的末尾被执行的东西。</p>
<p>在上面的例子中，我们用 panic() 使程序的执行陷入 panic。正如你所注意到的，这里有一个 defer 语句，它将使程序在最后执行这一行。当我们需要在函数结束时执行一些东西时也可以使用 defer，例如关闭一个文件。</p>
<h3 id="concurrency">Concurrency (并发)</h3>
<p>Go 是在考虑到并发性的情况下建立的。Go 中的并发性可以通过 Go 协程实现，它是轻量级的线程。</p>
<p><strong>Go routine (Go 协程)</strong></p>
<p>Go 协程是可以与另一个函数并行或同时运行的函数。创建一个 Go 协程非常简单。只需在一个函数前面加上关键字 Go，我们就可以让它并行执行。Go 协程是非常轻量级的，所以我们可以创建成千上万的协程。让我们来看看一个简单的例子:</p>
<pre><code class="language-go">package main
import (
  "fmt"
  "time"
)
func main() {
  go c()
  fmt.Println("I am main")
  time.Sleep(time.Second * 2)
}
func c() {
  time.Sleep(time.Second * 2)
  fmt.Println("I am concurrent")
}
//=&gt; I am main
//=&gt; I am concurrent
</code></pre>
<p>正如你在上面的例子中所看到的，函数 c 是一个 Go 例程，与 Go 主线程并行执行。有些时候，我们希望在多个线程之间共享资源。Go 倾向于不将一个线程的变量与另一个线程共享，因为这样会增加死锁和资源等待的可能性。还有一种方法可以在 Go 协程之间共享资源：通过 Go channels。</p>
<p><strong>Channels (通道)</strong></p>
<p>我们可以使用通道在两个 Go 协程之间传递数据。在创建 channel 时，有必要指定该 channel 接收什么样的数据。让我们创建一个简单的字符串类型的 channel，如下所示:</p>
<pre><code class="language-go">c := make(chan string)
</code></pre>
<p>通过这个 channel，我们可以发送字符串类型的数据。我们可以在这个 channel 中发送和接收数据:</p>
<pre><code class="language-go">package main

import "fmt"

func main(){
  c := make(chan string)
  go func(){ c &lt;- "hello" }()
  msg := &lt;-c
  fmt.Println(msg)
}
//=&gt;"hello"
</code></pre>
<p>接收方 channel 等待，直到发送方发送数据到 channel。</p>
<p><strong>One way channel (单向通道)</strong></p>
<p>有些情况下，我们希望 Go 程序通过 channel 接收数据，但不发送数据，反之亦然。为此，我们也可以创建一个<strong>单向 channel</strong>。让我们来看看一个简单的例子:</p>
<pre><code class="language-go">package main

import (
 "fmt"
)

func main() {
 ch := make(chan string)
 
 go sc(ch)
 fmt.Println(&lt;-ch)
}

func sc(ch chan&lt;- string) {
 ch &lt;- "hello"
}
</code></pre>
<p>在上面的例子中，<code>sc</code> 是一个只用于发送消息到通道但不能接受消息的 go 协程。</p>
<h3 id="organizingmultiplechannelsforagoroutineusingselectselectgo">Organizing multiple channels for a Go routine using select (使用 select 为 Go 协程组织多个通道)</h3>
<p>一个函数可能有多个 channel 在等待。为此，我们可以使用一个选择（select）语句。让我们看一个例子，以了解更清楚的情况:</p>
<pre><code class="language-go">package main

import (
 "fmt"
 "time"
)

func main() {
 c1 := make(chan string)
 c2 := make(chan string)
 go speed1(c1)
 go speed2(c2)
 fmt.Println("The first to arrive is:")
 select {
 case s1 := &lt;-c1:
  fmt.Println(s1)
 case s2 := &lt;-c2:
  fmt.Println(s2)
 }
}

func speed1(ch chan string) {
 time.Sleep(2 * time.Second)
 ch &lt;- "speed 1"
}

func speed2(ch chan string) {
 time.Sleep(1 * time.Second)
 ch &lt;- "speed 2"
}
</code></pre>
<p>在上面的例子中，main 正在等待两个 channel，c1 和 c2。通过 select case 语句，main 函数打印出，信息从它先收到的 channel 中发送出来。</p>
<p><strong>Buffered channel(带缓冲的通道)</strong></p>
<p>你可以在 go 中创建一个缓冲 channel。有了缓冲 channel，如果缓冲区满了，发送到该 channel 的消息就会被阻断。让我们看一下这个例子:</p>
<pre><code class="language-go">package main

import "fmt"

func main(){
  ch := make(chan string, 2)
  ch &lt;- "hello"
  ch &lt;- "world"
  ch &lt;- "!" // extra message in buffer
  fmt.Println(&lt;-ch)
}

// =&gt; fatal error: all goroutines are asleep - deadlock!
</code></pre>
<p>正如我们在上面看到的，一个 channel 接受的信息不超过 2 条。</p>
<h4 id="golang">为什么 Golang 会成功？</h4>
<blockquote>
<p>简洁性…… — Rob-pike</p>
</blockquote>
<h3 id="great">Great</h3>
<p>我们学习了 Go 的一些主要组成部分和特点。</p>
<ol>
<li>变量、数据类型</li>
<li>数组 切片 和 映射</li>
<li>函数</li>
<li>循环和条件语句</li>
<li>指针</li>
<li>软件包</li>
<li>方法、结构体和接口</li>
<li>错误处理</li>
<li>并发 - Go 协程和通道</li>
</ol>
<p>恭喜你，你现在对 Go 有了相当的了解。</p>
<blockquote>
<p>我最有成效的一天是减少了 1000 行代码。<br>
— Ken Thompson</p>
</blockquote>
<p>不要停在这里。继续向前推进。思考一个小的应用并开始创建。</p>
<p><a href="https://www.linkedin.com/in/milap-neupane-99a4b565/">LinkedIn</a><br>
<a href="http://github.com/milap-neupane">Github</a><br>
<a href="https://twitter.com/_milap">Twitter</a></p>
<p>本文也发布在 Milap Neupane 博客：<a href="https://milapneupane.com.np/2019/07/06/learning-golang-from-zero-to-hero/">从 0 到 1 学习 Go</a></p>
<!--kg-card-end: markdown--><p>感谢 <a href="https://www.freecodecamp.org/chinese/news/author/mingxun/">Mingxun Liu</a> 校对这篇译文。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Git Rebase 手册 ]]>
                </title>
                <description>
                    <![CDATA[ 开发人员的工具箱中最强大的工具之一是git rebase。但它因复杂和被误解而臭名昭著。 事实上，如果你了解它的实际作用，git rebase是一个非常优雅和直接的工具，可以实现 Git 中许多不同的事情。 在之前的文章中，你了解了 什么是 Git diff [https://www.freecodecamp.org/news/git-diff-and-patch/]、什么是 merge [https://www.freecodecamp.org/news/the-definitive-guide-to-git-merge/]以及 Git 如何解决合并冲突(merge conflicts) [https://www.freecodecamp.org/news/the-definitive-guide-to-git-merge/] 。在这篇文章中，你将了解什么是 Git rebase，为什么它与 merge 不同，以及如何放心地进行 rebase 💪🏻 开始前的说明  1. 我还制作了一个涵盖本文内容的视频。如果你想在阅读的同时观看视频，可以在这里 [https://youtu ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/git-rebase-handbook/</link>
                <guid isPermaLink="false">65df05784985d903ee575ea1</guid>
                
                    <category>
                        <![CDATA[ Git ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Wed, 28 Feb 2024 10:19:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/02/The-Git-Rebase-Handbook-Book-Cover--1-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/git-rebase-handbook/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">The Git Rebase Handbook – A Definitive Guide to Rebasing</a>
      </p><!--kg-card-begin: markdown--><p>开发人员的工具箱中最强大的工具之一是<code>git rebase</code>。但它因复杂和被误解而臭名昭著。</p>
<p>事实上，如果你了解它的实际作用，<code>git rebase</code>是一个非常优雅和直接的工具，可以实现 Git 中许多不同的事情。</p>
<p>在之前的文章中，你了解了 <a href="https://www.freecodecamp.org/news/git-diff-and-patch/">什么是 Git diff</a>、<a href="https://www.freecodecamp.org/news/the-definitive-guide-to-git-merge/">什么是 merge</a>以及 <a href="https://www.freecodecamp.org/news/the-definitive-guide-to-git-merge/">Git 如何解决合并冲突(merge conflicts)</a>。在这篇文章中，你将了解什么是 Git rebase，为什么它与 merge 不同，以及如何放心地进行 rebase 💪🏻</p>
<h2 id="">开始前的说明</h2>
<ol>
<li>我还制作了一个涵盖本文内容的视频。如果你想在阅读的同时观看视频，可以在<a href="https://youtu.be/3VFsitGUB3s">这里</a>查看。</li>
<li>如果你想玩玩我用的软件库，自己试试这些命令，<a href="https://github.com/Omerr/rebase_playground">这里</a>是软件库。</li>
<li>我正在写一本关于 Git 的书！你有兴趣阅读初始版本并提供反馈吗？请给我发<a href="https://chinese.freecodecamp.org/news/git-rebase-handbook/gitting.things@gmail.com">邮件</a></li>
</ol>
<p>好了，你准备好了吗？</p>
<h1 id="gitmerge">简要回顾 Git Merge🤔</h1>
<p>从底层来讲，<code>git rebase</code>和<code>git merge</code>是非常、非常不同的事情。那为什么人们一直在比较它们呢？</p>
<p>原因是它们的用法。使用 Git 时，我们通常在不同的分支工作，并对这些分支进行修改。</p>
<p>在<a href="https://www.freecodecamp.org/news/the-definitive-guide-to-git-merge/#howgits3waymergealgorithmworks">以前的教程</a>中，我举了一个例子，John 和 paul（披头士乐队）正在共同创作一首新歌。他们从<code>main</code>分支开始，然后各自发散，修改歌词并提交他们的修改。</p>
<p>然后，两人想整合他们的改动，这是使用 Git 工作时经常发生的事情。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-197.png" alt="image-197" width="600" height="400" loading="lazy"></p>
<p>分歧（diverged）的历史 - <code>paul_branch</code> 和 <code>john_branch</code> 与 <code>main</code>分歧（来源：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>在 Git 中，有两种主要的方式来整合不同分支的变化，或者说，不同的提交和提交历史。它们是 merge 和 rebase。</p>
<p><a href="https://www.freecodecamp.org/news/the-definitive-guide-to-git-merge/">在之前的教程中</a>，我们对 <code>git merge</code> 有了相当的了解。我们看到，在执行合并时，我们会创建一个 <strong>合并提交（merge Commit）</strong>,这个提交的内容是两个分支的组合，它也有两个父分支，每个分支一个。</p>
<p>所以，假设你在分支<code>john_branch</code>上（假设是上图中描述的历史），你运行<code>git merge paul_branch</code>。你会得到这样的状态--在<code>john_branch</code>上，有一个新的提交，有两个父分支。第一个是合并前<code>HEAD</code>指向的<code>john_branch</code>分支上的提交，本例中是 <code>Commit 6</code>。第二个是 <code>paul_branch</code> 所指向的提交 <code>Commit 9</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-196.png" alt="image-196" width="600" height="400" loading="lazy"></p>
<p>运行<code>git merge paul_branch</code>的结果：一个新的合并提交（Merge Commit），有两个父分支（Source：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>再看一下历史图：你创建了一个 <strong>分歧的（diverged）</strong> 历史。你实际上可以看到它在哪里分叉（branched），在哪里又合并了（merged）。</p>
<p>所以当使用<code>git merge</code>时，你并没有重写历史--而是在现有的历史中增加一个提交。具体来说，是在现有的历史中增加一个提交，创造一个分歧（diverged）的历史。</p>
<h1 id="gitrebasegitmerge"><code>git rebase</code> 和 <code>git merge</code> 有什么区别🤔</h1>
<p>当使用<code>git rebase</code>时，会发生不同的情况。🥁</p>
<p>让我们从大的方面开始：如果你在<code>paul_branch</code>上，并使用<code>git rebase john_branch</code>，Git 会去找 John 的分支和 Paul 的分支的共同祖先。然后把 Paul 分支的提交中引入的补丁，应用到 John 分支。</p>
<p>所以在这里，你用<code>rebase</code>把在一个分支，Paul 的分支上提交的修改，在另一个分支<code>john_branch</code>上重演（replay）。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-198.png" alt="image-198" width="600" height="400" loading="lazy"></p>
<p>运行<code>git rebase john_branch</code>的结果：<code>paul_branch</code>上的提交被 <code>重演（replay）</code> 到 <code>john_branch</code>之上（来源：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>等等，那是什么意思？🤔</p>
<p>我们现在将一点一点地进行分析，以确保你完全理解在底层发生的事情 😎</p>
<h1 id="cherrypickrebase">以 <code>cherry-pick</code> 作为 Rebase 的基础</h1>
<p>使用 git rebase 可以理解为执行 <code>git cherry-pick</code>，<code>git cherry-pick</code> 是一个命令，它接受一个提交，计算出该提交引入的补丁（patch），即计算出父提交和该提交之间的差异，并且通过 <code>cherry-pick</code> 将这个差异<code>重演（replay）</code>出来。</p>
<p>让我们手动来做这个。</p>
<p>如果我们通过执行<code>git diff main &lt;SHA_OF_Commit_5&gt;</code>来看看 <code>Commit 5</code> 引入的差异:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-199.png" alt="image-199" width="600" height="400" loading="lazy"></p>
<p>运行 <code>git diff</code> 来观察 <code>Commit 5</code> 引入的补丁（Source：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>如果你想玩玩我用的版本库，自己试试这些命令，<a href="https://github.com/Omerr/rebase_playground">这里</a>是版本库。</p>
<p>你可以看到，在这个提交中，John 开始创作一首名为 <code>Lucy in the Sky with Diamonds</code> 的歌曲：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-200.png" alt="image-200" width="600" height="400" loading="lazy"></p>
<p><code>git diff</code>的输出结果,<code>Commit 5</code> 引入的补丁(patch) (来源: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>提醒一下，你也可以使用<code>git show</code>命令来获得同样的输出：</p>
<pre><code class="language-shell">git show &lt;SHA_OF_Commit_5&gt;
</code></pre>
<p>现在，如果你<code>cherry-pick</code>这个提交，你将在活动分支(active branch)上专门引入这个改动。先切换到 "main":</p>
<p><code>git checkout main</code> (or <code>git switch main</code>)</p>
<p>并创建另一个分支，只是为了明确:</p>
<p><code>git checkout -b my_branch</code> (or <code>git switch -c my_branch</code>)</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-201.png" alt="image-201" width="600" height="400" loading="lazy"></p>
<p>从 <code>main</code> 分支创建出 <code>my_branch</code> 分支 (源自: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p><code>cherry-pick</code>这个提交(Commit):</p>
<pre><code class="language-shell">git cherry-pick &lt;SHA_OF_Commit_5&gt;
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-202.png" alt="image-202" width="600" height="400" loading="lazy"></p>
<p>使用 <code>cherry-pick</code> 将 <code>Commit 5</code> 中引入的修改应用到 <code>main</code>上（来源：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>）<br>
考虑一下日志（<code>git lol</code>的输出）：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-205.png" alt="image-205" width="600" height="400" loading="lazy"></p>
<p><code>git lol</code>的输出（源自：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>(<code>git lol</code>是我加在 Git 上的一个别名，用来以图形的方式直观地查看历史。你可以找到它<a href="https://gist.github.com/Omerr/8134a61b56ca82dd90e546e7ef04eb77">这里</a>)。</p>
<p>你似乎是复制了 <code>Commit 5</code>。请记住，尽管它有相同的提交信息，并引入了相同的修改，甚至在这种情况下指向与原始 <code>Commit 5</code>相同的树对象,它仍然是一个不同的提交对象，因为它是以不同的时间戳创建的。</p>
<p>看一下这些变化，使用<code>git show HEAD</code>:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-204.png" alt="image-204" width="600" height="400" loading="lazy"></p>
<p><code>git show HEAD</code>的输出结果（Source：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>它们与 <code>Commit 5</code> 的相同。</p>
<p>当然，如果你看一下这个文件（比如，用<code>nano lucy_in_the_sky_with_diamonds.md</code>），它的状态和最初的 <code>Commit 5</code> 之后的状态是一样的。</p>
<p>酷! 😎</p>
<p>好了，现在你可以删除新的分支，这样它就不会每次都出现在你的历史记录上:</p>
<pre><code class="language-shell">git checkout main
git branch -D my_branch
</code></pre>
<h2 id="cherrypickgitrebase">在 <code>cherry-pick</code> 之外– 如何使用 <code>git rebase</code></h2>
<p>你可以把 <code>git rebase</code> 看成是一个接一个地执行多个 <code>cherry-pick</code> 的方法，也就是 <code>重放(replay)</code>多个提交。这不是<code>rebase</code>唯一能做的事情，但它是我们解释的一个很好的起点。<br>
是时候玩玩<code>git rebase</code>了！ 👏🏻👏🏻</p>
<p>之前，你把 <code>paul_branch</code> 合并到了 <code>john_branch</code>。如果把<code>paul_branch</code> <em>rebased</em> <code>john_branch</code>，会发生什么呢？你会得到一个非常不同的历史(history)。</p>
<p>从本质上说，就好像我们把在<code>paul_branch</code>上的提交中引入的变更，在<code>john_branch</code>上重放(replay)一样。结果就是一个 <strong>线性</strong> 历史。[译者注：<code>git log --graph</code> 输出历史的是直线的，没有分叉]</p>
<p>为了理解这个过程，我将提供一个高层视图，然后深入到每一步。将一个分支重定向(rebasing)到另一个分支之上的过程如下:</p>
<ol>
<li>找到共同的祖先(ancestor)。</li>
<li>确定要 <code>重放(replayed)</code>的提交。</li>
<li>对于每个提交<code>X</code>，计算<code>diff(parent(X), X)</code>，并存储为<code>patch(X)</code>。</li>
<li>移动 <code>HEAD</code> 到新的基(base).</li>
<li>在目标分支上按顺序应用生成的补丁。每次都用新的状态创建一个新的提交对象。</li>
</ol>
<p>在新提交中使用与现有提交相同的变更集的过程也被称为 <strong>重放(replaying)</strong>，我们已经使用过这个术语。</p>
<h1 id="rebase">是时候亲手操作 Rebase 了🙌🏻</h1>
<p>从 Paul 的分支开始：</p>
<pre><code class="language-shell">git checkout paul_branch
</code></pre>
<p>这是提交历史：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-206.png" alt="image-206" width="600" height="400" loading="lazy"></p>
<p>执行<code>git rebase</code>前的提交历史（Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>现在，进入激动人心的部分：</p>
<pre><code class="language-shell">git rebase john_branch
</code></pre>
<p>查看历史:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-207.png" alt="image-207" width="600" height="400" loading="lazy"></p>
<p>rebase 后的历史（Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>( <code>gg</code> 是我的 <a href="https://youtu.be/3VFsitGUB3s">视频</a> 中介绍的一个外部工具的别名). [译者注: <a href="https://github.com/mlange-42/git-graph">git-graph</a>]</p>
<p>因此，使用 <code>git merge</code> 你增加了历史(history)，而使用 <code>git rebase</code>, 你改写了历史。你创建了新的提交对象。此外，结果是一个线性的历史图,而不是一个发散图。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-209.png" alt="image-209" width="600" height="400" loading="lazy"></p>
<p>rebase 后的历史（来源：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>本质上，我们 <code>复制</code> 了 <code>paul_branch</code> 上 <code>Commit 4</code> 之后的提交，并将它们 <code>粘贴</code>到了 <code>john_branch</code> 上。</p>
<p>这个命令被称为 <code>rebase</code>，因为它改变了运行它的分支的基点提交(base Commit)。也就是说，在运行<code>git rebase</code>之前，<code>paul_branch</code>的基点提交是 <code>Commit 4</code>,因为这是分支 <code>诞生(born)</code> 的地方（从<code>main</code>开始）。使用 <code>rebase</code> 时，你要求 Git 给它另一个基点，也就是假装它是从 <code>Commit 6</code> 诞生的。</p>
<p>为此，Git 将原来的 <code>Commit 7</code> 的改动 <code>重放(replayed)</code>到 <code>Commit 6</code> 上，然后创建了一个新的提交对象。这个对象与原来的 <code>Commit 7</code> 有三点不同:</p>
<ol>
<li>时间戳不同。</li>
<li>它有不同的父提交, <code>Commit 6</code> 而不是 <code>Commit 4</code>。</li>
<li>它指向的<a href="https://www.freecodecamp.org/news/git-internals-objects-branches-create-repo/">tree object</a> 是不同的,因为修改被引入到了 <code>Commit 6</code> 指向的树，而不是 <code>Commit 4</code> 指向的树。</li>
</ol>
<p>注意这里的最后一个提交，<code>Commit 9</code>。它所代表的快照 (也就是它所指向的 <a href="https://www.freecodecamp.org/news/git-internals-objects-branches-create-repo/">tree</a>) 与合并两个分支后得到的树完全相同。Git 仓库中文件的状态与使用 <code>git merge</code> 时一样。不同的只是历史，当然还有提交对象。</p>
<p>现在，您可以简单地使用:</p>
<pre><code class="language-shell">git checkout main
git merge paul_branch
</code></pre>
<p>Hm…… 如果运行最后这条命令，会发生什么？🤔 在查看了 <code>main</code> 之后，再次查看提交历史:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-210.png" alt="image-210" width="600" height="400" loading="lazy"></p>
<p>rebase 后，再切换到 <code>main</code> 分支的历史 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>合并 <code>paul_branch</code> 到 <code>main</code> 会发生什么?</p>
<p>事实上，Git 可以简单地执行快进合并(fast-forward merge)，因为历史是完全线性的（如果你需要关于快进合并的提醒，请查看 <a href="https://www.freecodecamp.org/news/the-definitive-guide-to-git-merge/#timetogethandson">this post</a> ）。因此，<code>main</code> 和 <code>paul_branch</code> 现在指向同一个提交:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-211.png" alt="image-211" width="600" height="400" loading="lazy"></p>
<p>快进合并(fast-forward merge)的结果 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<h1 id="advancedrebasingingit">Advanced Rebasing in Git💪🏻</h1>
<p>既然你已经了解了 rebase 的基础知识，现在就该考虑更高级的情况了，在这些情况下，<code>rebase</code> 命令的附加选项和参数就会派上用场。</p>
<p>在前面的例子中，当你只说了 <code>rebase</code>（没有附加选项），Git 就会重放(replayed) 从共同祖先到当前分支顶端的所有提交。</p>
<p>但是，rebase 是一个超级强大的命令，它能够...，改写历史。如果你想修改历史，把它变成你自己的，它就会派上用场。</p>
<p>让 <code>main</code> 再次指向 <code>Commit 4</code>，撤销上次的合并:</p>
<pre><code class="language-shell">git reset -–hard &lt;ORIGINAL_Commit 4&gt;
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-238.png" alt="image-238" width="600" height="400" loading="lazy"></p>
<p><code>撤销(undoing)</code> 上次合并操作 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>通过 rebase 进行撤销:</p>
<pre><code class="language-shell">git checkout paul_branch
git reset -–hard &lt;ORIGINAL_Commit 9&gt;
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-239.png" alt="image-239" width="600" height="400" loading="lazy"></p>
<p><code>撤销</code> rebase 操作 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>请注意，您的历史记录与以前完全相同:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-240.png" alt="image-240" width="600" height="400" loading="lazy"></p>
<p>在 <code>撤销</code> rebase 操作后可视化历史记录 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>需要再次说明的是，<code>Commit 9</code> 并不是在当前 <code>HEAD</code> 无法访问时就消失了。相反，它仍然保存在对象数据库中。当你使用 <code>git reset</code> 将 <code>HEAD</code> 改为指向该提交(Commit 9)时，你就能检索到它以及它的父提交，因为它们也存储在数据库中。很酷吧？😎</p>
<p>好了，快速查看 Paul 介绍的更改:</p>
<pre><code class="language-shell">git show HEAD
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-241.png" alt="image-241" width="600" height="400" loading="lazy"></p>
<p><code>git show HEAD</code> 显示了 <code>Commit 9</code> 引入的补丁 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>在提交图(Commit graph) 中继续向后退:</p>
<pre><code class="language-shell">git show HEAD~
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-242.png" alt="image-242" width="600" height="400" loading="lazy"></p>
<p><code>git show HEAD~</code>（与 <code>git show HEAD~1</code>相同）显示 <code>Commit 8</code> 引入的补丁  (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>更进一步:</p>
<pre><code class="language-shell">git show HEAD~2
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-243.png" alt="image-243" width="600" height="400" loading="lazy"></p>
<p><code>git show HEAD~2</code> 显示 <code>Commit 7</code> 引入的补丁 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>所以，这些改动很好，但也许 Paul 并不想要这样的历史记录。相反，他想让 <code>Commit 7</code> 和 <code>Commit 8</code> 中的改动看起来像是一次提交。</p>
<p>为此，你可以使用 <strong>interactive(交互式)</strong> rebase。为此，我们在 <code>rebase</code> 命令中添加 <code>-i</code>（或 <code>--interactive</code>）选项:</p>
<pre><code class="language-shell">git rebase -i &lt;SHA_OF_Commit_4&gt;
</code></pre>
<p>或者，由于 <code>main</code> 指向 <code>Commit 4</code>，我们只需运行:</p>
<pre><code class="language-shell">git rebase -i main
</code></pre>
<p>通过运行这条命令，你会告诉 Git 使用一个新的基(base) <code>Commit 4</code>。这样，Git 就会回溯到所有在 <code>Commit 4</code>之后提交的、从当前的 <code>HEAD</code> 可以到达的提交，并重放(replay) 这些提交。</p>
<p>对于每一个被重放的提交，Git 都会询问我们想对它做什么:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-250.png" alt="image-250" width="600" height="400" loading="lazy"></p>
<p><code>git rebase -i main</code> 会提示您选择对每次提交的处理方式 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>在这种情况下，将提交视为补丁是很有用的。也就是说，<code>Commit 7</code> 就是 <code>Commit 7</code> 在其父版本之上引入的补丁"。</p>
<p>一种选项是使用 <code>pick</code>。这是默认行为，它告诉 Git 重放该提交中引入的改动。在这种情况下，如果保持原样, <code>pick</code> 所有提交,就会得到相同的历史记录，Git 甚至不会创建新的提交对象。</p>
<p>另一个选项是 <code>squash</code>。一个 <em>squashed</em> 提交的内容会被 <code>折叠(folded)</code> 到它之前的提交内容中。因此，在我们的例子中，Paul 想把 <code>Commit 8</code> 压缩成 <code>Commit 7</code>:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-251.png" alt="image-251" width="600" height="400" loading="lazy"></p>
<p><code>Commit 8</code> 压缩成 <code>Commit 7</code> (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>如你所见，<code>git rebase -i</code> 提供了更多选项，但我们不会在这篇文章中一一介绍。如果允许 <code>rebase</code> 运行，系统会提示你为新创建的提交（即引入了 <code>Commit 7</code>和 <code>Commit 8</code>改动的提交）选择提交信息：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-252.png" alt="image-252" width="600" height="400" loading="lazy"></p>
<p>提供提交信息: <code>Commits 7+8</code> (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>再看历史:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-253.png" alt="image-253" width="600" height="400" loading="lazy"></p>
<p>运行 interactive rebase 后的历史 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>正如我们想要的那样!我们在<code>paul_branch</code>分支上有 <code>Commit 9</code> (当然,它是一个不同的对象,与原来的 <code>Commit 9</code> 不同)。它指向 <code>Commit 7+8</code>,这是一个单独的提交,引入了原来 <code>Commit 7</code>和 <code>Commit 8</code> 的所有变更。这个提交的父提交是 <code>Commit 4</code>,也就是<code>main</code>分支当前所指向的提交。你现在在<code>john_branch</code>分支上。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-254.png" alt="image-254" width="600" height="400" loading="lazy"></p>
<p>interactive rebase  后可视化的历史 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>哇哦，是不是很酷？ 😎</p>
<p><code>git rebase</code> 允许你无限制地控制任何分支的形态。你可以用它来重新排序提交，或删除错误的改动，或回溯修改改动。或者，你也可以把分支的基础移到另一个提交上，任何你想要的提交。</p>
<h2 id="howtousetheontoswitchofgitrebase">How to Use the <code>--onto</code> Switch of <code>git rebase</code></h2>
<p>让我们再看一个例子。再次进入 <code>main</code>:</p>
<pre><code class="language-shell">git checkout main
</code></pre>
<p>然后删除 <code>paul_branch</code> 和 <code>john_branch</code> 分支 ，这样在提交图中就看不到它们了:</p>
<pre><code class="language-shell">git branch -D paul_branch
git branch -D john_branch
</code></pre>
<p>现在从 <code>main</code> 分支基础上开一个新的分支:</p>
<pre><code class="language-shell">git checkout -b new_branch
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-255.png" alt="image-255" width="600" height="400" loading="lazy"></p>
<p>从 <code>main</code> 分支上创建一个新分支(new_branch) (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-256.png" alt="image-256" width="600" height="400" loading="lazy"></p>
<p>一个干净的历史记录, 从 <code>main</code> 分支上创建的 <code>new_branch</code> 分支。 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>现在，在此处添加一些更改并提交:</p>
<pre><code class="language-shell">nano code.py
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-257.png" alt="image-257" width="600" height="400" loading="lazy"></p>
<p><code>new_branch</code> 分支上添加 <code>code.py</code> 文件（Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<pre><code class="language-shell">git add code.py
git Commit -m "Commit 10"
</code></pre>
<p>切回 <code>main</code> 分支:</p>
<pre><code class="language-shell">git checkout main
</code></pre>
<p>并引入另一个变化:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-258.png" alt="image-258" width="600" height="400" loading="lazy"></p>
<p>在文件开头添加了文档字符串 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>是时候提交这些更改:</p>
<pre><code class="language-shell">git add code.py
git Commit -m "Commit 11"
</code></pre>
<p>另一个变化:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-259.png" alt="image-259" width="600" height="400" loading="lazy"></p>
<p>添加 <code>@Author</code> 的描述 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>提交变化:</p>
<pre><code class="language-shell">git add code.py
git Commit -m "Commit 12"
</code></pre>
<p>哦，等等，现在我意识到，我是想让你把 <code>Commit 11</code> 中引入的更改作为 <code>new_branch</code> 的一部分。唉。你能怎么办呢？ 🤔</p>
<p>回顾 git 提交历史:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-260.png" alt="image-260" width="600" height="400" loading="lazy"></p>
<p><code>Commit 12</code> 后的历史 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>我希望 <code>Commit 10</code> 不只出现在 <code>main</code> 分支上，而是同时出现在 <code>main</code> 分支和 <code>new_branch</code> 上。从视觉上看，我希望把它移到图的下面:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-261.png" alt="image-261" width="600" height="400" loading="lazy"></p>
<p>如图所示, 我想让你 <code>push</code> "Commit 10" (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>你能看清楚我意图? 😇</p>
<p>我们清楚, <code>rebase</code> 允许我们重新使用 <code>new_branch</code> 分支引入变更,也就是 <code>Commit 10</code> 引入的变更,就像这些变更最初是在 <code>Commit 11</code>上进行的,而不是在 <code>Commit 4</code> 上进行的。</p>
<p>要实现这个目标,你可以使用 <code>git rebase</code> 的其他参数。你可以告诉 Git,你想要取 <code>main</code> 分支和 <code>new_branch</code> 分支的共同祖先 <code>Commit 4</code> 之后引入的所有历史变更,并将这部分历史变更的新的基础设置为 <code>Commit 11</code>。要实现这个目的,可以使用:</p>
<pre><code class="language-shell">git rebase -–onto &lt;SHA_OF_Commit_11&gt; main new_branch
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-262.png" alt="image-262" width="600" height="400" loading="lazy"></p>
<p>重置前后的历史记录, <code>Commit 10</code> 已经被推送(pushed) (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>看看我们美丽的历史! 😍</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-263.png" alt="image-263" width="600" height="400" loading="lazy"></p>
<p>重置前后的历史记录, `Commit 10 已经被推送(pushed) (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>让我们再看一个例子。</p>
<p>假设我开始在一个分支上工作，却犯了错误，从 <code>feature_branch_1</code> 而不是从 <code>main</code> 开始。<br>
因此，要模拟这种情况，请创建 <code>feature_branch_1</code>:</p>
<pre><code class="language-shell">git checkout main
git checkout -b feature_branch_1
</code></pre>
<p>删除 <code>new_branch</code> 后，图表中就看不到它了:</p>
<pre><code class="language-shell">git branch -D new_branch
</code></pre>
<p>创建一个简单的 Python 文件 <code>1.py</code>:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-264.png" alt="image-264" width="600" height="400" loading="lazy"></p>
<p>新文件 <code>1.py</code>,里面有 <code>print('Hello world!')</code> (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>提交此文件:</p>
<pre><code class="language-shell">git add 1.py
git Commit -m  "Commit 13"
</code></pre>
<p>现在(错误地)从 <code>feature_branch_1</code>开出新分支:</p>
<pre><code class="language-shell">git checkout -b feature_branch_2
</code></pre>
<p>创建新文件 <code>2.py</code>:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-265.png" alt="image-265" width="600" height="400" loading="lazy"></p>
<p>创建的 <code>2.py</code> (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>提交该文件:</p>
<pre><code class="language-shell">git add 2.py
git Commit -m  "Commit 14"
</code></pre>
<p>再添加一些代码到文件 <code>2.py</code>:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-266.png" alt="image-266" width="600" height="400" loading="lazy"></p>
<p>修改 <code>2.py</code> (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>提交该变化:</p>
<pre><code class="language-shell">git add 2.py
git Commit -m  "Commit 15"
</code></pre>
<p>到目前为止，您应该有这样的历史记录:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-267.png" alt="image-267" width="600" height="400" loading="lazy"></p>
<p>引入 <code>Commit 15</code> 的历史 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>切回 <code>feature_branch_1</code> 分支，编辑文件 <code>1.py</code>:</p>
<pre><code class="language-shell">git checkout feature_branch_1
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-268.png" alt="image-268" width="600" height="400" loading="lazy"></p>
<p>修改 <code>1.py</code> (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>提交修改:</p>
<pre><code class="language-shell">git add 1.py
git Commit -m  "Commit 16"
</code></pre>
<p>你的历史记录应该是这样的:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-270.png" alt="image-270" width="600" height="400" loading="lazy"></p>
<p>引入 <code>Commit 16</code> 后的历史 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>说现在你意识到了，你犯了一个错误。你实际上想让 <code>feature_branch_2</code> 从 <code>main</code> 分支中分出，而不是从 <code>feature_branch_1</code> 中分出。</p>
<p>怎样才能做到这一点呢？🤔</p>
<p>试着根据历史图和你所学到的关于 <code>rebase</code> 命令的 <code>--onto</code> 参数来思考一下。</p>
<p>你想把 <code>feature_branch_2</code> 上第一个提交的父分支，也就是 <code>commit 14</code>，替换到 <code>main</code> 分支的顶部，这里是 <code>commit 12</code>，而不是 <code>feature_branch_1</code> 的起点，这里是 <code>commit 13</code>。因此，你将再次创建一个 <em>新的基点</em>，这次是在<code>feature_branch_2</code>上的第一个 Commit。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-271.png" alt="image-271" width="600" height="400" loading="lazy"></p>
<p>你想要在 <code>Commit 14</code> 和 <code>Commit 15</code> 之间进行移动（来源：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>你打算怎么做呢？</p>
<p>第一，切到 <code>feature_branch_2</code> 分支：</p>
<pre><code class="language-shell">git checkout feature_branch_2
</code></pre>
<p>然后你可以执行:</p>
<pre><code class="language-shell">git rebase -–onto main &lt;SHA_OF_Commit_13&gt;
</code></pre>
<p>因此，您的 <code>feature_branch_2</code> 是基于 <code>main</code> 分支而不是 <code>feature_branch_1</code> 分支：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-272.png" alt="image-272" width="600" height="400" loading="lazy"></p>
<p>执行变基(rebase)后的提交历史（来源：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>该命令的语法是:</p>
<pre><code class="language-shell">git rebase --onto &lt;new_parent&gt; &lt;old_parent&gt;
</code></pre>
<h2 id="">如何在一个单独的分支上执行变基</h2>
<p>在查看单个分支的历史时，您也可以使用 git rebase。</p>
<p>让我们看看你是否能在这里帮助我。</p>
<p>假设我是从 <code>feature_branch_2</code> 开始工作的，具体来说是编辑了文件 <code>code.py</code>。我首先将所有字符串的引号从单引号改为双引号：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-273.png" alt="image-273" width="600" height="400" loading="lazy"></p>
<p>在 <code>code.py</code> 中将 <code>'</code> 改为 <code>"</code>（来源：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>然后，我将其进行了暂存（staged）并提交（Committed）：</p>
<pre><code class="language-shell">git add code.py
git Commit -m "Commit 17"
</code></pre>
<p>然后我决定在文件开头添加一个新函数:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-274.png" alt="image-274" width="600" height="400" loading="lazy"></p>
<p>添加函数 <code>another_feature</code> (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>然后，我将其进行了暂存（staged）并提交（Committed）:</p>
<pre><code class="language-shell">git add code.py
git Commit -m "Commit 18"
</code></pre>
<p>现在我意识到我实际上忘记了将 <code>main</code> 用双引号包裹起来（你可能已经注意到了），所以我也做了这个改动:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-275.png" alt="image-275" width="600" height="400" loading="lazy"></p>
<p>将 <code>'__main__'</code> 改成 <code>"__main__"</code>  (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>当然, 我将其进行了暂存（staged）并提交（Committed）:</p>
<pre><code class="language-shell">git add code.py
git Commit -m "Commit 19"
</code></pre>
<p>现在，让我们来看看历史:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-276.png" alt="image-276" width="600" height="400" loading="lazy"></p>
<p>引入 <code>Commit 19</code> 之后的提交记录 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>这样看起来不太好，对吧？我的意思是，<code>Commit 17</code> 和 <code>Commit 19</code>（将<code>'</code>改为<code>"</code>）是相关的，但它们被无关的 <code>Commit 18</code>（我在那里添加了一个新函数）分隔开了。我们能做些什么？🤔 你能帮我吗？</p>
<p>直觉上，我想在这里修改历史:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-277.png" alt="image-277" width="600" height="400" loading="lazy"></p>
<p>这些是我想要修改的提交 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>那么，你会怎么做呢？</p>
<p>你说得对！👏🏻</p>
<p>我可以在 <code>commit 15</code>的基础上，将历史记录从 <code>commit 17</code> 变基(rebase)为 <code>commit 19</code>。要做到这一点:</p>
<pre><code class="language-shell">git rebase --interactive --onto &lt;SHA_OF_Commit_15&gt; &lt;SHA_OF_Commit_15&gt;
</code></pre>
<p>请注意，我指定了 <code>Commit 15</code> 作为提交范围的起点，不包括本次提交。而且我不需要明确指定 <code>HEAD</code> 作为最后一个参数。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-279.png" alt="image-279" width="600" height="400" loading="lazy"></p>
<p>在单个分支上使用 <code>rebase --onto</code> (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>按照您的建议运行 <code>rebase</code> 命令后（谢谢！😇），我看到了下面的显示：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-280.png" alt="image-280" width="600" height="400" loading="lazy"></p>
<p>交互式变基 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>那我该怎么办呢？我想把 <code>Commit 19</code> 放在 <code>Commit 18</code> 之前，这样它就紧跟在 <code>Commit 17</code>之后。我还可以进一步将它们合并在一起，就像这样：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-281.png" alt="image-281" width="600" height="400" loading="lazy"></p>
<p>交互式 rebase - 调整提交顺序并合并 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>现在当我被提示输入提交信息时，我可以提供信息 <code>Commit 17+19</code>:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-282.png" alt="image-282" width="600" height="400" loading="lazy"></p>
<p>输入 Commit 信息 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>现在，让我们来看看我们美丽的提交历史吧:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-283.png" alt="image-283" width="600" height="400" loading="lazy"></p>
<p>由此产生的提交历史 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>再次感谢！ 🙌🏻</p>
<h1 id="">更多变基用户案例 + 更多实践</h1>
<p>现在，我希望你已经对 变基(rebase) 的语法感到得心应手了。要真正理解它，最好的办法是理解各种案例，并自己想办法解决它们。</p>
<p>对于接下来的用例，我强烈建议你在我介绍完每个用例后就停止阅读，然后尝试自己解决。</p>
<h2 id="commits">如何排除 Commits</h2>
<p>假设您在另一个软件仓库中有这样的历史记录:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-284.png" alt="image-284" width="600" height="400" loading="lazy"></p>
<p>别的 Commit 提交历史 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>在使用之前，先将 <code>tag</code> 存储为 <code>original_Commit_f</code>，以便稍后再查看:</p>
<pre><code class="language-shell">git tag original_Commit_f
</code></pre>
<p>现在，你实际上并不希望包含 <code>Commit C</code>和 <code>Commit D</code> 中的更改。你可以像之前一样使用交互式 rebase，删除它们的改动。或者，也可以再次使用 <code>git rebase--onto</code>。如何使用 <code>--onto</code>来 <code>移除(remove)</code> 这两个提交(commit)呢？</p>
<p>你可以在 <code>commit B</code>的基础上重建 <code>HEAD</code>，原来的父提交是 <code>commit D</code>，现在应该是 <code>commit B</code>,看提交历史记录：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-284.png" alt="image-284" width="600" height="400" loading="lazy"></p>
<p>再次回顾提交历史 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>rebase 使 <code>Commit B</code> 成为 <code>Commit E</code> 的 <em>基(base)</em>，意味着 <code>移动(moving)</code> <code>Commit E</code>和 <code>Commit F</code>，并赋予它们另一个 <em>基(base)</em> -- <code>Commit B</code>。你能自己想出这个命令吗？</p>
<pre><code class="language-shell">git rebase --onto &lt;SHA_OF_Commit_B&gt; &lt;SHA_OF_Commit_D&gt; HEAD
</code></pre>
<p>请注意，使用上述语法不会移动 <code>main</code> 指向新的 Commit，因此结果是一个 <code>分离的(detached)</code> <code>HEAD</code>。如果你使用 <code>gg</code> 或其他显示分支历史的工具，这可能会让你感到困惑：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-285.png" alt="image-285" width="600" height="400" loading="lazy"></p>
<p>用 <code>--onto</code> 变基(rebase) 会导致一个分离(detached)的 <code>HEAD</code> （来源：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>但如果使用 <code>git log</code>（或我的别名 <code>git lol</code>），就能看到想要的历史记录：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-286.png" alt="image-286" width="600" height="400" loading="lazy"></p>
<p>由此形成的提交历史 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>我不知道你怎么想，但这些事情让我非常开心。 😊😇</p>
<p>顺便说一下，你可以省略前面命令中的 <code>HEAD</code>，因为这是第三个参数的默认值。因此，只需使用:</p>
<pre><code class="language-shell">git rebase --onto &lt;SHA_OF_Commit_B&gt; &lt;SHA_OF_Commit_D&gt;
</code></pre>
<p>会有同样的效果。最后一个参数实际上是告诉 Git 当前提交序列的终点在哪里。所以有三个参数的 <code>git rebase --onto</code> 的语法是</p>
<pre><code class="language-shell">git rebase --onto &lt;new_parent&gt; &lt;old_parent&gt; &lt;until&gt;
</code></pre>
<h2 id="">如何在不同分支间移动提交</h2>
<p>因此，让我们回到之前的提交历史:</p>
<pre><code class="language-shell">git checkout original_Commit_f
</code></pre>
<p>现在我只想让 <code>commit E</code>位于基于 <code>commit B</code> 的分支上。也就是说，我想建立一个新的分支，从 <code>commit B</code>分支出去，其中只有 <code>commit E</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-287.png" alt="image-287" width="600" height="400" loading="lazy"></p>
<p>考虑到 <code>commit E</code> 的当前历史 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>那么，这意味着什么呢？请看上图。我应该变基(rebase)哪个提交（或哪些提交），哪个提交是新的基础(base)？</p>
<p>我知道在这里我可以让你来 😉</p>
<p>我想要的是取出 <code>commit E</code>，只有这个提交，并将其基础更改为 <code>commit B</code>。换句话说，将<code>commit E</code>引入的更改 <code>重放(replay)</code> 到 <code>commit B</code>上。</p>
<p>你能将这个逻辑应用到 <code>git rebase</code> 的语法中吗？</p>
<p>这里是语法（这次我用 <code>&lt;Commit_B&gt;</code> 代替 <code>&lt;SHA_OF_Commit_B&gt;</code>，为了简洁起见）：</p>
<pre><code class="language-shell">git rebase –-onto &lt;Commit_B&gt; &lt;Commit_D&gt; &lt;Commit_E&gt;
</code></pre>
<p>现在的提交历史是这样的:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-288.png" alt="image-288" width="600" height="400" loading="lazy"></p>
<p>变基后的提交历史 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>棒极了！</p>
<h1 id="">关于冲突的说明</h1>
<p>请注意，进行变基时可能会遇到与合并时一样的冲突。可能会出现冲突，因为在变基时，您试图将补丁(patches)应用在不同的基础上，也许这些补丁(patches)并不适用。</p>
<p>例如，再次考虑之前的代码库，特别是考虑由 <code>main</code> 指向的 <code>commit 12</code> 引入的更改：</p>
<pre><code class="language-shell">git show main
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-289.png" alt="image-289" width="600" height="400" loading="lazy"></p>
<p>在 <code>Commit 12</code> 中引入的补丁 (Source: <a href="https://youtu.be/3VFsitGUB3s">Brief</a>)</p>
<p>我在<a href="https://www.freecodecamp.org/news/git-diff-and-patch/">上一篇文章</a>中已经详细介绍了 <code>git diff</code> 的格式，但为了快速提醒大家，这个 Commit 会指示 Git 在两行上下文之后添加一行：</p>
<pre><code>```shell
This is a sample file
</code></pre>
<p>而在这三行上下文之前:</p>
<pre><code>```
def new_feature():
  print('new feature')
</code></pre>
<p>假设您正试图将 <code>commit 12</code> 重定向到另一个提交上。如果由于某种原因，这些上下文行并不存在于您要重置的 Commit 上的补丁中，那么就会产生冲突。要进一步了解冲突以及如何解决冲突，请参阅 <a href="https://www.freecodecamp.org/news/the-definitive-guide-to-git-merge/">本指南</a>。</p>
<h1 id="">放眼全局</h1>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-290.png" alt="image-290" width="600" height="400" loading="lazy"></p>
<p>比较变基(rebase)和合并(merge)（来源：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>在本指南的开始，我提到了 <code>git merge</code> 和 <code>git rebase</code> 之间的相似性：它们都用于整合不同历史中引入的更改。</p>
<p>但是，正如你现在所知，它们在操作方式上有很大的不同。合并导致了一个分叉的历史，而变基导致了一个线性的历史。在两种情况下都可能出现冲突。表格中还有一列需要特别关注。</p>
<p>现在你知道了什么是 <code>Git 变基（rebase）</code>，以及如何使用交互式变基或 <code>rebase --onto</code>，希望你理解，<code>git rebase</code> 是一个超级强大的工具。然而，与合并相比，它有一个巨大的缺点。</p>
<p>Git 变基改变了提交历史。</p>
<p>这意味着你<strong>不应该</strong>对存在于你本地代码库之外的并且其他人可能以此为基础进行提交的提交进行变基。</p>
<p>换句话说，如果问题涉及的只有你在本地创建的提交，那就继续使用变基，尽情操作。</p>
<p>但是，如果这些提交已经被推送，这可能会导致一个巨大的问题，因为其他人可能依赖这些提交，而后来你覆盖了它们，然后你和他们将拥有存储库的不同版本。</p>
<p>这与我们所见的不修改历史的 <code>merge</code> 不同。</p>
<p>例如，考虑最后一个情况，我们进行了变基，导致了这样的提交历史:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/image-288.png" alt="image-288" width="600" height="400" loading="lazy"></p>
<p>变基后的提交历史（来源：<a href="https://youtu.be/3VFsitGUB3s">Brief</a>）</p>
<p>现在，假设我已经将这个分支推送到远程仓库。在我推送了这个分支之后，另一个开发人员拉取了它，并从 <code>commit C</code>创建了一个新分支。另一个开发人员不知道与此同时，我正在本地对我的分支进行变基，并且稍后会再次推送它。</p>
<p>这导致了一个不一致：另一个开发人员从一个在我的代码库副本上不再可用的提交中进行工作。</p>
<p>我不会在本指南中详细阐述这到底会导致什么，因为我的主要观点是你绝对应该避免这种情况。如果你对实际会发生什么感兴趣，我会在下面留下一个有用资源的链接。现在，让我们总结一下我们所讨论的内容。</p>
<h1 id="">回顾</h1>
<p>在本教程中，你将学习到 <code>git rebase</code> 这个在 Git 中重写历史的超级强大工具。你考虑了一些<code>git rebase</code>可能有用的用例，以及如何使用一个、两个或三个参数，使用或不使用<code>--onto</code>开关。</p>
<p>我希望我能让你相信，<code>git rebase</code> 不仅功能强大，而且一旦掌握了要领，使用起来也很简单。它是一个 <code>复制粘贴（copy-paste）</code> 提交（或者更准确地说，是补丁）的工具。它是一个非常有用的工具。</p>
<h1 id="">其他参考资料</h1>
<ul>
<li><a href="https://www.youtube.com/playlist?list=PL9lx0DXCC4BNUby5H58y6s2TQVLadV8v7">Git Internals YouTube 播放列表 - Brief</a>（我的 YouTube 频道）</li>
<li><a href="https://www.freecodecamp.org/news/git-internals-objects-branches-create-repo/">Omer 上一篇关于 Git 内部结构的文章</a></li>
<li><a href="https://medium.com/@Omer_Rosenbaum/git-undo-how-to-rewrite-git-history-with-confidence-d4452e2969c2">Omer 的 Git UNDO 教程--用 Git 重写历史</a></li>
<li><a href="https://git-scm.com/book/en/v2/Git-Branching-Rebasing">关于变基的 Git 文档</a></li>
<li><a href="https://jwiegley.github.io/git-from-the-bottom-up/1-Repository/7-branching-and-the-power-of-rebase.html">分支和 rebase 的强大功能</a></li>
<li><a href="https://jwiegley.github.io/git-from-the-bottom-up/1-Repository/8-interactive-rebasing.html">交互式变基</a></li>
<li><a href="https://womanonrails.com/git-rebase-onto">Git rebase--onto</a></li>
</ul>
<h1 id=""><strong>关于作者</strong></h1>
<p><a href="https://www.linkedin.com/in/omer-rosenbaum-034a08b9/">Omer Rosenbaum</a> 是 <a href="https://swimm.io/">Swimm</a> 的首席技术官。他是<a href="https://youtube.com/@BriefVid">Brief YouTube 频道</a> 的作者。他也是一位网络安全培训专家，创立了 Checkpoint Security Academy。他是 <a href="https://data.cyber.org.il/networks/networks.pdf">《计算机网络》（希伯来语版）</a> 的作者。你可以在 <a href="https://twitter.com/Omer_Ros">Twitter</a> 上找到他。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何开始自由开发业务 ]]>
                </title>
                <description>
                    <![CDATA[ 许多想尝试自由职业的人认为，他们需要有多年的经验才能成功。但这是一个常见的误解，许多自由职业者已经推翻了这个误解。 无论你从事哪个行业，一个有用的蓝图可以成为你快速获得自由职业成功的全部条件。这篇文章强调了开始自由职业者网络开发业务所需要知道的东西。 成为自由开发者及其优势 开始成为一名自由职业者的Web设计师可能是一项具有挑战性的壮举。但如果你成功了，你可以在你的领域中处于高价值的地位。 作为一个自由职业者，你需要有能力经营自己的业务。这项任务需要自律，这可能是一项艰苦的工作。你需要为自己实施严格的硬性规定，才能取得成功。 为了鼓励你，告诉你以下是成为自由网页开发者的几个主要优势。 1. 对网页设计服务的存在大量需求 最明显的好处是对你的服务需求很大。来自Indeed的数据显示，截至本文写作时，美国有超过60,000个Web开发工作 [https://www.indeed.com/q-web-developer-l-usa-jobs.html?vjk=36fc4e40b6a03689]。 对Web设计服务的高需求量确保你总是有客户。有很多人愿意为你的技能付费。 2. 开 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-start-a-freelance-dev-business/</link>
                <guid isPermaLink="false">658c1565576bb403fc7912a8</guid>
                
                    <category>
                        <![CDATA[ 自由职业 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Thu, 28 Dec 2023 01:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/12/freelance-dev-business-article-image.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-start-a-freelance-dev-business/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Start a Freelance Dev Business</a>
      </p><!--kg-card-begin: markdown--><p>许多想尝试自由职业的人认为，他们需要有多年的经验才能成功。但这是一个常见的误解，许多自由职业者已经推翻了这个误解。</p>
<p>无论你从事哪个行业，一个有用的蓝图可以成为你快速获得自由职业成功的全部条件。这篇文章强调了开始自由职业者网络开发业务所需要知道的东西。</p>
<h2 id="">成为自由开发者及其优势</h2>
<p>开始成为一名自由职业者的Web设计师可能是一项具有挑战性的壮举。但如果你成功了，你可以在你的领域中处于高价值的地位。</p>
<p>作为一个自由职业者，你需要有能力经营自己的业务。这项任务需要自律，这可能是一项艰苦的工作。你需要为自己实施严格的硬性规定，才能取得成功。</p>
<p>为了鼓励你，告诉你以下是成为自由网页开发者的几个主要优势。</p>
<h3 id="1">1. 对网页设计服务的存在大量需求</h3>
<p>最明显的好处是对你的服务需求很大。来自Indeed的数据显示，截至本文写作时，美国有超过<a href="https://www.indeed.com/q-web-developer-l-usa-jobs.html?vjk=36fc4e40b6a03689">60,000个Web开发工作</a>。</p>
<p>对Web设计服务的高需求量确保你总是有客户。有很多人愿意为你的技能付费。</p>
<h3 id="2">2. 开发人员短缺</h3>
<p>另一个优势是，在美国和全世界范围内都存在着大量的<a href="https://www.daxx.com/blog/development-trends/software-developer-shortage-us">开发人员短缺</a>。Web设计师的缺乏意味着你不必为了获得自由职业者的报酬而与同行业中太多的人竞争。</p>
<h3 id="3">3. 你可以做你自己的老板</h3>
<p>自由职业也意味着你是你自己的老板。与为别人工作相比，你有更多的自由和时间。</p>
<p>当你经营自己的生意时，你不需要向任何人报告。你可以选择接受哪些新客户，工作多少小时，以及你的费率是多少。</p>
<h3 id="4">4. 诱人的赚钱潜力</h3>
<p>网页设计师的服务得到了体面的报酬。根据美国劳工统计局的数据，2020年，Web开发人员的平均年薪为<a href="https://www.bls.gov/ooh/computer-and-information-technology/web-developers.htm">77,200美元</a>。</p>
<h2 id="">成为自由开发者需要采取的重要步骤</h2>
<p>本节强调了三个关键步骤，以帮助你的自由职业者业务启动和运行。阅读后，你可以轻松地开始从家里提供你的Web发服务。</p>
<h3 id="1">1. 选择一个细分市场</h3>
<p>不可能在Web设计的所有领域都同样出色。选择一个利基市场可以让你把时间和精力集中在更狭窄的任务上。</p>
<p>拥有一个利基市场还可以让你成为一个专家，可以解决一组客户的需求。这样做要比对不同的业务或网站风格都学一点要容易得多。</p>
<p>在选择方向时，仅仅对某一利基感兴趣是不够的。你必须知道这个利基市场对网站是否有需求。问问自己，人们是否愿意为其付费。</p>
<p>这一步对任何创业者来说都是至关重要的，无论你是什么行业。你首先需要知道你的商业理念的市场可行性。</p>
<p>能帮助你完成这一步的是研究市场和寻找机会。</p>
<p>例如，如果你喜欢收集玩具，你可以为销售稀有动作人物的网店工作。你的卖点可以是了解玩具收藏者的想法以及他们在玩具人物网店中的需求。</p>
<p>如果你是一名小学教师，你可以帮助为教育工作者建立网站。一个网站可以帮助他们节省时间，更好地推销他们的教学服务，并获得更多的收入。</p>
<p>如果你是一个活动家，可以接触到一些组织。看看是否有人需要一个网站来宣传他们的主张。</p>
<p>你可以在网上研究，访问Facebook、Reddit或Quora上的社区。这些平台是来自不同利基市场的人经常在网上活动的地方。</p>
<p>阅读他们的问题，寻找他们的痛点，并确定他们需要什么样的产品。在阅读之后，你可以定制你的网站开发服务来解决他们的问题。</p>
<h3 id="2">2. 建立一个作品集</h3>
<p>对于许多Web设计师来说，获得第一个项目可能是最具挑战性的事情之一。如果你没有作品集，要说服潜在客户是很困难的。幸运的是，有一些方法可以在没有任何经验的情况下确保你的第一个工作。</p>
<p>你<a href="https://myaws.co.nz/website-design-could-be-the-difference-between-business-success-and-failure/">需要有一个网页设计作品集，作为你的网站</a>。该网站应该告诉客户你做什么以及你如何满足他们的需求。</p>
<p>你必须包括前客户的反馈和你过去的几个优秀项目。</p>
<p>为了得到你的第一个项目，你可能需要免费建立几个网站。</p>
<p>在你选择的利基市场，寻找一个急需网站的组织，并免费为其服务。作为交换，你要求他们提交一份关于你的服务如何帮助他们的推荐信。</p>
<p>寻找客户可以很容易，因为你提供的是免费工作。如果他们选择接受你，他们将不会有任何损失。</p>
<p>你可以使用像WordPress这样的网络开发平台来更快地完成你的工作。你需要在工作中牢记用户体验、可访问性和转换率优化。</p>
<p>除了这三个因素之外，你还应该记得把客户的需求放在首位。你应该以解决他们具体需求的方式来设计网站。</p>
<p>有些客户可能更担心转换问题，而不是用户体验。有些人可能有其他的网站关注点，比如<a href="https://www.similarweb.com/corp/blog/marketing/seo/">搜索引擎优化</a>（SEO）。不管他们需要什么，你必须确保你的产出能够实现。</p>
<p>如果你不想寻找客户，你可以购买域名并自己建立网站。这一步可能是让你的业务启动和运行的更快但更昂贵的方式。</p>
<p>如果客户想看到的只是你的技术，那么这可能是一个很好的解决方案。不幸的是，如果你选择购买域名，你将不会收到任何推荐信。</p>
<p>一旦你完成两到三个项目，你可以开始寻找你的第一个付费客户。</p>
<h3 id="3">3. 寻找客户</h3>
<p>现在你有一个展示你技能的作品集，你可以开始寻找客户了。</p>
<p>在寻找愿意为你的服务付费的客户时，社交媒体可以起到帮助作用。</p>
<p>例如，你可以通过使用Facebook Groups来推销你的新业务。你可以建立一个新的Group或加入一个现有的Group。</p>
<p>Facebook groups是由具有共同兴趣的人组成的。无论你的业务是什么利基，你都可能在Facebook上找到一个相关的小组。</p>
<p>使用社交媒体来推销你的网页设计服务，不仅可以帮助你找到客户。它还可以让你更好地了解你的目标客户。它告诉你他们的问题是什么，他们想要什么解决方案。</p>
<p>另一种认识客户的方式是在活动中发言。参加有关网页设计的活动可以是建立信誉的一个好方法。它也可以是一个建立联系网络的有效途径。</p>
<p>这种方法的缺点是，要获得活动邀请可能很有挑战性，而且需要做大量的准备。</p>
<p>如果你不热衷于当面应酬，你可以通过在线招聘网站寻找客户。定期访问这些网站可以帮助你获得更多工作。尝试访问Smashing Jobs、Mediabistro、Coroflot或RemoteOK。</p>
<p>你也可以尝试自由职业者市场。确保创建一个详细的个人资料，告知你的客户你的专长以及你能如何帮助他们。</p>
<p>在为自由职业者网站创建简介时，不要忘记包括你的技能、经验和作品集。</p>
<h2 id="">结语</h2>
<p>成为一名自由职业者的Web设计师需要投入大量的时间和精力。但其结果是为你提供了你想要的自由、灵活和收入的职业。</p>
<p>因此，推动自己学习并提高自己的技能，为潜在客户提供最大的价值。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 生产环境 Node.js  检查清单 ]]>
                </title>
                <description>
                    <![CDATA[ 你在生产中是否正确使用 Node？让我们看看人们在生产环境中运行Node的一些常见错误（直接来自我自己的项目——如codedamn [https://codedamn.com]），以及如何减少这些错误。 当你部署Node应用程序时，你可以把它作为你在生产环境中的检查清单。由于这是一篇关于生产环境准备的文章，当你在本地系统上开发应用程序时，其中很多内容并不适用。 以cluster（集群）模式运行/separate node进程 记住，Node是单线程（single threaded）的。它可以将很多事情（如HTTP请求和文件系统的读写）委托给操作系统，由它在多线程（multithreaded）环境下处理。但是，你写的代码，即业务逻辑，仍然是在单线程中运行。 通过在单线程中运行，你的Node进程总是被限制在你的CPU上一个核心。因此，如果你的服务器有多个CPU核，那么你在服务器上只运行一个Node进程就是在浪费计算能力。 running Node just once 是什么意思？你看，操作系统有一个内置的调度器，它负责程序进程如何在机器的CPU上分配。当你在一台双核机器上只运行2个 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/node-js-production-checklist/</link>
                <guid isPermaLink="false">658bf0a0576bb403fc79122c</guid>
                
                    <category>
                        <![CDATA[ Node ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Wed, 27 Dec 2023 09:48:42 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/12/0_4citSsBqjYDX7hUO.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/node-js-production-checklist/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">The Ultimate Node.js Production Checklist</a>
      </p><!--kg-card-begin: markdown--><p>你在生产中是否正确使用 Node？让我们看看人们在生产环境中运行Node的一些常见错误（直接来自我自己的项目——如<a href="https://codedamn.com">codedamn</a>），以及如何减少这些错误。</p>
<p>当你部署Node应用程序时，你可以把它作为你在生产环境中的检查清单。由于这是一篇关于生产环境准备的文章，当你在本地系统上开发应用程序时，其中很多内容并不适用。</p>
<h2 id="clusterseparatenode">以cluster（集群）模式运行/separate node进程</h2>
<p>记住，Node是单线程（single threaded）的。它可以将很多事情（如HTTP请求和文件系统的读写）委托给操作系统，由它在多线程（multithreaded）环境下处理。但是，你写的代码，即业务逻辑，仍然是在单线程中运行。</p>
<p>通过在单线程中运行，你的Node进程总是被限制在你的CPU上一个核心。因此，如果你的服务器有多个CPU核，那么你在服务器上只运行一个Node进程就是在浪费计算能力。</p>
<p><code>running Node just once</code>是什么意思？你看，操作系统有一个内置的调度器，它负责程序进程如何在机器的CPU上分配。当你在一台双核机器上只运行2个进程时，操作系统会决定最好在不同的CPU核上分别运行这两个进程，以挤出最大的性能。</p>
<p>对Node也需要做类似的事情。在这一点上，你有两个选择：</p>
<ol>
<li><strong>Run Node in cluster mode</strong>——集群（cluster）模式是一种架构，Node本身已经支持。简单地说，Node会分叉（forks）出更多自己的进程，并通过一个主进程（master process）分配负载。</li>
<li><strong>Run Node processes independently</strong>——这个选项与上面的略有不同，因为你现在没有一个主进程来控制子Node进程。这意味着，当你产生不同的Node进程时，它们将完全独立运行。没有共享内存，没有IPC，没有通信，什么都没有。</li>
</ol>
<p>根据<a href="https://stackoverflow.com/a/47122606/2513722">stackoverflow answer</a>，后者（选择 2）的性能好得多，但设置起来有点麻烦。</p>
<p>为什么呢？因为在Node应用中，不仅有应用逻辑，而且当你在Node代码中设置服务时，几乎总是需要绑定端口。而一个应用程序的代码库不能在同一个操作系统上绑定两次相同的端口。</p>
<p>然而，这个问题是很容易解决的。环境变量、Docker容器、NGiNX前端代理（frontend proxy）等等都是这方面的一些解决方案。</p>
<h2 id="endpoints">限制你的接入点（endpoints）速率</h2>
<p>让我们面对现实吧。世界上不是每个人都对你的服务器抱有良好的意图。当然，像DDoS这样的攻击是非常复杂的，甚至像GitHub这样的巨头在发生这样的事情时也会瘫痪。</p>
<p>但是，你至少可以防止一个脚本小子对你的服务器上暴露的一个昂贵的、没有设置任何速率限制的 API 端点进行攻击，从而导致你的服务器瘫痪。</p>
<p>如果你使用Express和Node，有2个好用的包（packages），它们可以无缝地一起工作，在第7层限制流量。</p>
<ol>
<li>Express Rate Limit - <a href="https://www.npmjs.com/package/express-rate-limit">https://www.npmjs.com/package/express-rate-limit</a></li>
<li>Express Slow Down - <a href="https://www.npmjs.com/package/express-slow-down">https://www.npmjs.com/package/express-slow-down</a></li>
</ol>
<p>Express Slow Down实际上是给你的请求慢慢增加的响应延迟，而不是丢弃它们。这样一来，如果合法用户不小心DDoS了（在这里和那里点击按钮的超级活动），他们只是被减慢了速度，而不会被限制速率。</p>
<p>另一方面，如果有一个脚本小子在运行脚本来破坏服务器，Express速率限制器会根据用户的IP、用户账户或其他你想要的东西来监控和限制这个特定的用户。</p>
<p>速率限制也可以（应该！）通过IP地址应用在第4层（第4层意味着在发现内容之前阻止流量-HTTP）。如果你愿意，你可以设置一个NGiNX规则，在第4层拦截流量，拒绝来自一个IP的流量，从而使你的服务器进程不至于不堪重负。</p>
<h2 id="frontendserverssl">使用前端服务器（frontend server）终止SSL</h2>
<p>Node提供了开箱即用的支持，使用<code>https</code>服务器模块和所需的SSL证书与浏览器进行SSL握手。</p>
<p>但是，让我们在这里说实话，无论如何，你的应用程序不应该首先关注SSL。这不是应用逻辑应该做的事情。你的节点代码应该只负责处理请求，而不是对进出服务器的数据进行预处理和后处理。</p>
<p>SSL终止（SSL termination）是指将流量从HTTPS转换为HTTP。在这方面，有比Node更好的工具可用。我推荐NGiNX或HAProxy。两者都有免费版本，可以完成工作，Node不用处理SSL问题。</p>
<h2 id="frontendserver">使用前端服务器（frontend server）来提供静态文件服务</h2>
<p>同样，与其使用内置的方法如<code>express.static</code>来提供静态文件，不如使用前端的反向代理服务器如NGiNX来来处理磁盘上的静态文件。</p>
<p>首先，NGiNX比Node做得更快（因为它是从头开始建立的，只做这个）。但它也从单线程的Node进程中接过文件服务，而Node可以把别的事做得更好。</p>
<p>不仅如此--像NGiNX这样的前端代理服务器还可以利用GZIP压缩技术帮助你更快地传送内容。你还可以设置过期标题、缓存数据等等，这不是我们应该期望Node做的事情（不过，Node还是可以做到的）。</p>
<h2 id="">配置错误处理</h2>
<p>正确的错误处理可以使你免于花费数小时的时间来调试和试图重现困难的错误。在服务器上，设置错误处理的架构特别容易，因为你是运行它的人。我推荐像<a href="https://sentry.io">Sentry</a>与Node这样的工具，它可以记录、报告并在服务器因源代码中的错误而崩溃时向你发送电子邮件。</p>
<p>一旦到位，现在是时候在服务器崩溃时重新启动它了，这样整个网站就不会瘫痪几个小时，直到你手动将它重新启动。</p>
<p>为此，你可以使用像<a href="https://www.npmjs.com/package/pm2">PM2</a>这样的进程管理器。或者更好的是，使用一个docker化的容器环境，使用诸如<code>restart: always</code>的策略，并设置适当的内存和磁盘限制。</p>
<p>Docker设置确保即使你的容器在OME中运行，进程也会再次启动起来（这在PM2环境中可能不会发生，因为如果运行中的进程在某处有内存泄漏，操作系统可能会杀死PM2）。</p>
<h2 id="">正确配置日志</h2>
<p>所有的答案都在日志中。服务器黑客攻击、服务器崩溃、可疑的用户行为等。为此，你必须确保：</p>
<ol>
<li>每一个请求尝试都会被记录下来，包括IP地址/请求方式/访问路径，基本上可以记录尽可能多的信息（当然，密码和信用卡信息等私人信息除外）</li>
<li>这可以通过<a href="https://www.npmjs.com/package/morgan">morgan</a>软件包实现。</li>
<li>在生产中设置<strong>文件流日志（file stream logs）</strong>，而不是控制台输出。这更快，更容易看到，并允许你将日志导出到在线日志查看服务。</li>
<li>不是所有的日志信息都具有同等重要性。有些日志只是用来调试的，而如果有些日志出现了，则可能预示着火烧眉毛的情况（如服务器被黑或未经授权的访问）。使用 winston-logger 可记录不同级别的日志。</li>
<li>设置<strong>日志轮换（log rotation）</strong>，这样你就不会在一个月左右后看到服务器的日志大小为GB。</li>
<li>在轮换（rotation）之后，<strong>GZIP</strong>你的日志文件。文本系统开销少（Text is cheap），而且可压缩性高，容易存储。只要文本日志被压缩了，而且你运行的服务器有相当大的磁盘空间（25GB以上），你就不应该面临文本日志的问题。</li>
</ol>
<h2 id="">结语</h2>
<p>在生产过程中注意到一些做法很容易，这可以让你在日后的调试过程中少走弯路。请确保你遵循这些最佳做法，并让我知道你的想法，请在我的<a href="https://twitter.com/mehulmpt">推特</a>反馈。</p>
<p>如果你喜欢这篇文章，让我们在社交媒体上交流。这是我的<a href="https://instagram.com/mehulmpt">Instagram</a>和 <a href="https://twitter.com/mehulmpt">Twitter</a>。我是超级活跃的人，很愿意聊天，欢迎你和我联系。</p>
<p>Peace!<br>
Mehul</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ LangChain 教程——如何构建自定义知识聊天机器人 ]]>
                </title>
                <description>
                    <![CDATA[ 你可能已经了解到过去几个月中发布的大量人工智能应用程序。你甚至可能已经开始使用其中的一些。 ChatPDF [https://www.chatpdf.com/] 和 CustomGPT AI [https://customgpt.ai/use-cases/] 等人工智能工具对人们非常有用，这是有道理的。你需要翻阅长达 50 页的文档才能找到一个简单答案的时代已经一去不复返了。取而代之的是，你可以依靠人工智能来完成繁重的工作。 但是，这些开发人员究竟是如何创建和使用这些工具的呢？他们中的许多人都在使用一个名为 LangChain 的开源框架。 在本文中，我将向你介绍 LangChain，并向你展示如何将其与 OpenAI 的 API 结合使用，以创建这些改变游戏规则的工具。希望我的介绍能激发你们的灵感，创造出属于自己的工具。那么，让我们开始吧！ 什么是 LangChain LangChain [https://github.com/hwchase17/langchain/]是一个开源框架，允许人工智能开发人员将 GPT-4 等大型语言模型（LLM）与外部数据相结合。它提供 P ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/langchain-how-to-create-custom-knowledge-chatbots/</link>
                <guid isPermaLink="false">652ce3d97452bd03fdf58d3c</guid>
                
                    <category>
                        <![CDATA[ 人工智能 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Mon, 16 Oct 2023 07:25:04 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/10/ThumbnailArticle--1-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/langchain-how-to-create-custom-knowledge-chatbots/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">LangChain Tutorial – How to Build a Custom-Knowledge Chatbot</a>
      </p><!--kg-card-begin: markdown--><p>你可能已经了解到过去几个月中发布的大量人工智能应用程序。你甚至可能已经开始使用其中的一些。</p>
<p><a href="https://www.chatpdf.com/">ChatPDF</a> 和 <a href="https://customgpt.ai/use-cases/">CustomGPT AI</a> 等人工智能工具对人们非常有用，这是有道理的。你需要翻阅长达 50 页的文档才能找到一个简单答案的时代已经一去不复返了。取而代之的是，你可以依靠人工智能来完成繁重的工作。</p>
<p>但是，这些开发人员究竟是如何创建和使用这些工具的呢？他们中的许多人都在使用一个名为 LangChain 的开源框架。</p>
<p>在本文中，我将向你介绍 LangChain，并向你展示如何将其与 OpenAI 的 API 结合使用，以创建这些改变游戏规则的工具。希望我的介绍能激发你们的灵感，创造出属于自己的工具。那么，让我们开始吧！</p>
<h2 id="langchain">什么是 LangChain</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/05/Screenshot-2023-05-29-at-5.40.38-PM.png" alt="Screenshot-2023-05-29-at-5.40.38-PM" width="600" height="400" loading="lazy"></p>
<p><a href="https://github.com/hwchase17/langchain/">LangChain</a>是一个开源框架，允许人工智能开发人员将 GPT-4 等大型语言模型（LLM）与外部数据相结合。它提供 Python 或 JavaScript（TypeScript）包。</p>
<p>大家可能知道，GPT 模型是在 2021 年之前的数据上训练出来的，这可能是一个很大的局限。虽然这些模型的常识很不错，但如果能将它们与自定义数据和计算连接起来，就会打开很多大门。这正是 LangChain 所要做的。</p>
<p>从本质上讲，它可以让你的 LLM 在得出答案时参考整个数据库。因此，你现在可以让你的 GPT 模型访问报告、文档和网站信息等形式的最新数据。</p>
<p>最近，LangChain 的受欢迎程度大幅上升，尤其是在三月份推出 GPT-4 之后。这要归功于它的多功能性，以及与功能强大的 LLM 配合后所带来的多种可能性。</p>
<h2 id="langchain">LangChain 是怎样工作的</h2>
<p>可能会觉得 LangChain 听起来很复杂，但实际上它非常容易上手。</p>
<p>简而言之，LangChain 就是将大量数据组成一个 LLM 可以轻松引用的数据链，而且计算能力越低越好。它的工作原理是将大量数据（例如 50 页的 PDF 文件）分解成 <code>块(chunks)</code>，然后将这些 <code>块(chunks)</code> 嵌入到 向量存储(Vector Store) 中。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/05/------LangChain.png" alt="------LangChain" width="600" height="400" loading="lazy"></p>
<p>以上是创建向量存储(Vector Store) 的简单示意图。</p>
<p>现在，我们已经有了大型文档的向量化表示，可以将其与 LLM 结合使用，在创建 提示-完成对<br>
(prompt-completion pair) 时，只检索我们需要引用的信息。</p>
<p>当我们在新聊天机器人中插入 提示(prompt) 时，LangChain 会查询 向量存储库(Vector Store) 以获取相关信息。把它想象成你文档的迷你谷歌。一旦检索到相关信息，我们就会将其与 提示(prompt ) 信息一起输入 LLM，生成我们的答案。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/05/ByteSizedThumbnail--1200---800-px---10-.png" alt="ByteSizedThumbnail--1200---800-px---10-" width="600" height="400" loading="lazy"></p>
<p>以上展示了 LangChain 如何与 OpenAI 的 LLM 协同工作。</p>
<p>LangChain 还允许你创建可以执行操作的应用程序，例如上网、发送电子邮件和完成其他与 API 相关的任务。请查看 <a href="https://Agentsgpt.reworkd.ai/">AgentsGPT</a>，这就是一个很好的例子。</p>
<p>这有很多可能的用例，以下是我想到的几个：</p>
<ul>
<li>个人 AI 电子邮件助理</li>
<li>人工智能学习伙伴</li>
<li>人工智能数据分析</li>
<li>定制公司客户服务聊天机器人</li>
<li>社交媒体内容创建助手</li>
</ul>
<p>还有更多。我将在今后的文章中介绍正确的构建教程，敬请期待。</p>
<h2 id="langchain">怎样开始使用 LangChain</h2>
<p>LangChain 应用程序由 5 个主要部分组成：</p>
<ol>
<li>Models (LLM Wrappers)</li>
<li>Prompts</li>
<li>Chains</li>
<li>Embeddings and Vector Stores</li>
<li>Agents</li>
</ol>
<p>我将为你逐一介绍，以便你对 LangChain 的工作原理有一个高层次的了解。接下来，你应该能够应用这些概念，开始制作自己的用例并创建自己的应用程序。</p>
<p>我将用 Rabbitmetrics 的简短代码片段（<a href="https://github.com/rabbitmetrics/langchain-13-min/blob/main/notebooks/langchain-13-min.ipynb">Github</a>）来解释一切。他就这一主题提供了很好的教程。通过这些代码片段，你可以完成所有设置并准备使用 LangChain。</p>
<p>首先，我们来设置环境。你可以用 pip 安装 3 个需要的库：</p>
<pre><code class="language-shell">pip install -r requirements.txt
</code></pre>
<pre><code class="language-requirements.txt">python-dotenv==1.0.0
langchain==0.0.137
pinecone-client==2.2.1
</code></pre>
<p><a href="https://www.pinecone.io/">Pinecone</a>是我们将与 LangChain 结合使用的向量存储。使用这些工具时，请确保将 OpenAI、Pinecone Environment 和 Pinecone API 的 API 密钥存储到环境文件中。你可以在它们各自的网站上找到这些信息。然后，我们只需在环境文件中加载以下内容即可：</p>
<pre><code class="language-Python"># Load environment variables

from dotenv import load_dotenv,find_dotenv
load_dotenv(find_dotenv())
</code></pre>
<p>现在，我们可以开始了！</p>
<h3 id="modelsllmwrappers">Models（LLM Wrappers）</h3>
<p>为了与我们的 LLM 进行交互，我们将为 OpenAI 的 GPT 模型实例化一个封装器。在本例中，我们将使用 OpenAI 的 GPT-3.5-turbo，因为它最具性价比。但如果你有授权，也可以使用功能更强大的 GPT4。</p>
<pre><code class="language-Python"># import schema for chat messages and ChatOpenAI in order to query chatmodels GPT-3.5-turbo or GPT-4

from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage
)
from langchain.chat_models import ChatOpenAI
     

chat = ChatOpenAI(model_name="gpt-3.5-turbo",temperature=0.3)
messages = [
    SystemMessage(content="You are an expert data scientist"),
    HumanMessage(content="Write a Python script that trains a neural network on simulated data ")
]
response=chat(messages)

print(response.content,end='\n')
     
</code></pre>
<p>本质上，<code>SystemMessage</code> 为 GPT-3.5-turbo 模块提供了上下文，它将为每个提示-完成对引用该模块。 HumanMessage 指的是你在 ChatGPT 界面中输入的内容(你的提示)。</p>
<p>但对于定制知识聊天机器人，我们经常抽象出 提示（prompt）中的重复组件。例如，如果我正在创建一个推文生成器应用程序，我不想继续输入<code>给我写一条关于...的推文</code>。其实，<a href="https://ilampadman.com/best-ai-writer-best-ai-copywriter">AI 写作工具</a> 就是这么简单开发出来的！</p>
<p>那么让我们看看如何使用 提示模板(prompt templates) 将其抽象出来。</p>
<h3 id="prompts">Prompts（提示）</h3>
<p>LangChain 提供 PromptTemplates，允许你根据用户输入动态更改提示，类似于使用正则表达式。</p>
<pre><code class="language-Python"># Import prompt and define PromptTemplate

from langchain import PromptTemplate

template = """
You are an expert data scientist with an expertise in building deep learning models. 
Explain the concept of {concept} in a couple of lines
"""

prompt = PromptTemplate(
    input_variables=["concept"],
    template=template,
)
     

# Run LLM with PromptTemplate

llm(prompt.format(concept="autoencoder"))
llm(prompt.format(concept="regularization"))
</code></pre>
<p>你可以通过不同的方式改变它们以适合你的用例。 如果你熟悉使用 ChatGPT，这对你来说应该很舒服。</p>
<h3 id="chains">Chains（链）</h3>
<p>链（Chains）允许你采用简单的 PromptTemplates 并在它们之上构建功能。本质上，链就像 <a href="https://www.freecodecamp.org/news/function-composition-in-javascript/">复合函数(composite functions)</a>，允许你将 PromptTemplates 和 LLM 集成在一起。</p>
<p>使用之前的装饰器(wrappers) 和 PromptTemplates，我们可以使用单个链运行相同的提示，该链采用 PromptTemplate 并将其与 LLM 组合：</p>
<pre><code class="language-Python"># Import LLMChain and define chain with language model and prompt as arguments.

from langchain.chains import LLMChain
chain = LLMChain(llm=llm, prompt=prompt)

# Run the chain only specifying the input variable.
print(chain.run("autoencoder"))
     
</code></pre>
<p>最重要的是，顾名思义，我们可以将它们链接在一起以创建更大的作品。</p>
<p>例如，我可以从一个链中获取结果并将其传递到另一个链中。 在此片段中，Rabbitmetrics 从第一个链中获取补全内容，并将其传递到第二个链中，以便向 5 岁的孩子解释。</p>
<p>然后，你可以将这些链组合成一个更大的链并运行它。</p>
<pre><code class="language-Python"># Define a second prompt 

second_prompt = PromptTemplate(
    input_variables=["ml_concept"],
    template="Turn the concept description of {ml_concept} and explain it to me like I'm five in 500 words",
)
chain_two = LLMChain(llm=llm, prompt=second_prompt)

# Define a sequential chain using the two chains above: the second chain takes the output of the first chain as input

from langchain.chains import SimpleSequentialChain
overall_chain = SimpleSequentialChain(chains=[chain, chain_two], verbose=True)

# Run the chain specifying only the input variable for the first chain.
explanation = overall_chain.run("autoencoder")
print(explanation)
</code></pre>
<p>通过链，你可以创建大量的功能，这就是 LangChain 如此多功能的原因。 但它真正的亮点在于将其与前面讨论的向量存储结合使用。 我们来介绍一下这个组件。</p>
<h3 id="embeddingsvectorstores">Embeddings（嵌入）和 Vector Stores（向量存储）</h3>
<p>这就是我们整合 LangChain 的自定义数据方面的地方。 如前所述，嵌入(embeddings) 和向量存储背后的想法是将大数据分成块并在相关时存储要查询的数据。</p>
<p>LangChain 有一个文本分割器函数(text splitter function) 可以做到这一点：</p>
<pre><code class="language-Python"># Import utility for splitting up texts and split up the explanation given above into document chunks

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 100,
    chunk_overlap  = 0,
)

texts = text_splitter.create_documents([explanation])
</code></pre>
<p>分割文本需要两个参数：块有多大 (chunk_size) 以及每个块重叠的程度 (chunk_overlap)。 每个块之间的重叠对于帮助识别相关的相邻块非常重要。</p>
<p>每个块都可以这样检索：</p>
<pre><code class="language-Python">texts[0].page_content
</code></pre>
<p>获得这些块后，我们需要将它们转化为嵌入(embeddings)。这允许向量存储在查询时查找并返回每个块。我们将使用 OpenAI 的 嵌入模型(embedding model) 来完成此操作。</p>
<pre><code class="language-Python"># Import and instantiate OpenAI embeddings

from langchain.embeddings import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model_name="ada")
     

# Turn the first text chunk into a vector with the embedding

query_result = embeddings.embed_query(texts[0].page_content)
print(query_result)
</code></pre>
<p>最后，我们需要有一个地方来存储这些 矢量化嵌入(vectorized embeddings)。如前所述，我们将为此使用 Pinecone。使用之前环境文件中的 API key，我们可以初始化 Pinecone 来存储我们的嵌入。</p>
<pre><code class="language-Python"># Import and initialize Pinecone client

import os
import pinecone
from langchain.vectorstores import Pinecone


pinecone.init(
    api_key=os.getenv('PINECONE_API_KEY'),  
    environment=os.getenv('PINECONE_ENV')  
)
     

# Upload vectors to Pinecone

index_name = "langchain-quickstart"
search = Pinecone.from_documents(texts, embeddings, index_name=index_name)
     

# Do a simple vector similarity search

query = "What is magical about an autoencoder?"
result = search.similarity_search(query)

print(result)
</code></pre>
<p>现在，我们能够从 Pinecone 向量存储中查询相关信息了！剩下要做的就是把我们学到的知识结合起来，创建我们的特定用例。这就是我们的专业人工智能 <code>Agents</code>。</p>
<h3 id="agents">Agents</h3>
<p>Agents 本质上是一种自主的人工智能，它接收输入并按顺序完成这些任务，直至达到最终目标。这就需要我们的人工智能使用其他应用程序接口，从而完成发送电子邮件或做数学题等任务。结合我们的 LLM + 提示链(prompt chains)，我们可以串联起一个合适的人工智能应用程序。</p>
<p>现在，要解释这部分内容会很费劲，因此这里有一个简单的例子，说明如何在 LangChain 中使用 Python Agents 来解决一个简单的数学问题。本例中的 Agents 通过连接我们的 LLM 来运行 Python 代码，并使用 NumPy 求根来解决问题：</p>
<pre><code class="language-Python"># Import Python REPL tool and instantiate Python Agents

from langchain.Agents.Agents_toolkits import create_python_Agents
from langchain.tools.python.tool import PythonREPLTool
from langchain.python import PythonREPL
from langchain.llms.openai import OpenAI

Agents_executor = create_python_Agents(
    llm=OpenAI(temperature=0, max_tokens=1000),
    tool=PythonREPLTool(),
    verbose=True
)
     

# Execute the Python Agents

Agents_executor.run("Find the roots (zeros) if the quadratic function 3 * x**2 + 2*x -1")
</code></pre>
<p>定制知识聊天机器人本质上是一个 Agents，它可以将查询向量化存储的 提示(prompts) 和操作串联起来，获取结果，并将其与原始问题串联起来！</p>
<p>如果你想了解更多关于人工智能 Agents 的信息，<a href="https://lablab.ai/t/ai-Agents-tutorial-how-to-use-and-create-them">这个</a> 是一个很好的资源。</p>
<h2 id="">别的因素</h2>
<p>即使你刚刚对 LangChain 的功能有了基本的了解，我相信你此时也会有很多想法。</p>
<p>但到目前为止，我们只研究了一个 OpenAI 模型，那就是基于文本的 GPT-3.5-turbo。OpenAI 有一系列模型可供 LangChain 使用，包括使用 Dall-E 生成图像。应用我们讨论过的相同概念，我们可以创建 <a href="https://julianlankstead.com/ai-nft-art-generator">人工智能艺术生成器</a> 代理、网站生成器代理等。</p>
<p>花点时间探索一下人工智能领域，我相信你会有越来越多的想法。</p>
<h2 id="">总结</h2>
<p>我希望你已经对所有这些新人工智能工具的幕后工作有了更多了解。作为一名程序员，了解 LangChain 的工作原理是一项宝贵的技能，它可以为你的人工智能开发带来无限可能。</p>
<p>如果你喜欢这篇文章，并想了解更多有关人工智能创造者们正在构建的新工具的信息，你可以通过我的 <a href="https://bytesizedai.beehiiv.com/subscribe">Byte-Sized AI Newsletter</a> 随时了解最新信息。我希望你能加入我们的社区。</p>
<p>你也可以在<a href="https://twitter.com/Shuggggan">Twitter</a> 上关注我，我们也可以在那里保持联系。</p>
<p>除此之外，请开始尝试使用 LangChain 并创建一些有趣的人工智能项目。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何使用 LangChain 创建人工智能推文生成器 ]]>
                </title>
                <description>
                    <![CDATA[ 今天我给大家带来了一个有趣的教程。如果你读过我上一篇关于使用 LangChain 构建自定义知识聊天机器人 [/news/langchain-how-to-create-custom-knowledge-chatbots/] 的文章，那么你一定对自己可以创建的伟大项目充满了想法。 好的，我想鼓励你的创造力，并举例说明使用 LangChain 和大型语言模型（LLM）可以创建什么。虽然听起来有点吓人，但实际上实现起来非常简单。 今天，我们将使用 LangChain 和 OpenAI 的 LLM 创建一个 AI Tweet 生成器。这是一个简单的项目，它接收一个 Tweet 主题并输出相关的 Tweet。 这有什么特别的？有趣的是，我们将使用 LangChain 通过维基百科上的最新信息。这让我们克服了 ChatGPT 的训练数据限制，因为它只能在 2021 年之前的数据上进行训练。 这就是我们要做的： 请看下面我们的推特所引用的信息。它使用了维基百科关于微软在 2023 年对 OpenAI 进行投资的信息。因此，现在你不必担心你的人工智能所引用的数据会过时了。 如果你 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/create-an-ai-tweet-generator-openai-langchain/</link>
                <guid isPermaLink="false">6525fbf3c8bb7703fa0287e1</guid>
                
                    <category>
                        <![CDATA[ 人工智能 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Tue, 10 Oct 2023 01:43:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/10/cover.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/create-an-ai-tweet-generator-openai-langchain/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Create an AI Tweet Generator Using LangChain</a>
      </p><!--kg-card-begin: markdown--><p>今天我给大家带来了一个有趣的教程。如果你读过我上一篇关于使用 LangChain 构建<a href="https://chinese.freecodecamp.org/news/langchain-how-to-create-custom-knowledge-chatbots/">自定义知识聊天机器人</a>的文章，那么你一定对自己可以创建的伟大项目充满了想法。</p>
<p>好的，我想鼓励你的创造力，并举例说明使用 LangChain 和大型语言模型（LLM）可以创建什么。虽然听起来有点吓人，但实际上实现起来非常简单。</p>
<p>今天，我们将使用 LangChain 和 OpenAI 的 LLM 创建一个 AI Tweet 生成器。这是一个简单的项目，它接收一个 Tweet 主题并输出相关的 Tweet。</p>
<p>这有什么特别的？有趣的是，我们将使用 LangChain 通过维基百科上的最新信息。这让我们克服了 ChatGPT 的训练数据限制，因为它只能在 2021 年之前的数据上进行训练。</p>
<p>这就是我们要做的：</p>
<p><img src="https://lh4.googleusercontent.com/B-AqnuHPFtkT010tllL0VZlbZRK-wasEjUwl8a5yzDRCuG3VYRt8hz1QPC3tz1F_vnDSXwHM8gJNIbM9jFcGbnz1uu4OSQB-hTVSuDYULlfVRWlQfewvFpS4-XF8pkMn37Gu5Au4liSxujehfV7uCWg" alt="B-AqnuHPFtkT010tllL0VZlbZRK-wasEjUwl8a5yzDRCuG3VYRt8hz1QPC3tz1F_vnDSXwHM8gJNIbM9jFcGbnz1uu4OSQB-hTVSuDYULlfVRWlQfewvFpS4-XF8pkMn37Gu5Au4liSxujehfV7uCWg" width="600" height="400" loading="lazy"></p>
<p>请看下面我们的推特所引用的信息。它使用了维基百科关于微软在 <strong>2023 年</strong>对 OpenAI 进行投资的信息。因此，现在你不必担心你的人工智能所引用的数据会过时了。</p>
<p><img src="https://lh6.googleusercontent.com/r5CxHduLOViifkCaI2R84nl-n26rGVHnJCOa3Rgpt_WqXlyL9O7Hnar52p0yGLhKNhe3F5F3X6CNM98-0oJBeBXQ8IvQvNgTZirblgs5lSU4j8G9X_X1ROgoPd06vIGhLd_mdmWyEZzAtrC5ESSXvZA" alt="r5CxHduLOViifkCaI2R84nl-n26rGVHnJCOa3Rgpt_WqXlyL9O7Hnar52p0yGLhKNhe3F5F3X6CNM98-0oJBeBXQ8IvQvNgTZirblgs5lSU4j8G9X_X1ROgoPd06vIGhLd_mdmWyEZzAtrC5ESSXvZA" width="600" height="400" loading="lazy"></p>
<p>如果你觉得听起来不错，那我们就开始吧。</p>
<h2 id="">如何设置项目</h2>
<p>虽然这个项目需要多个组件，但实际上所有内容都可以很好地整合到一个 app.py 文件中，该文件只需将多个 API 整合在一起即可。</p>
<p>在结构上，我们只需创建 app.py 文件和 apikey.py 文件来存储 API key（主要用于 <a href="https://openai.com/blog/openai-api">OpenAI</a>）。</p>
<p>此外，我们还要安装的库。以下是我们将在本项目中使用的库列表：</p>
<ul>
<li><strong>Streamlit</strong> – Used to build the app（用于构建应用程序）</li>
<li><strong>LangChain</strong> – Used to build LLM workflow（用于建立 LLM 工作流程）</li>
<li><strong>OpenAI</strong> – For using OpenAI GPT（调用 OpenAI GPT 接口）</li>
<li><strong>Wikipedia</strong> – Used to connect GPT to WIKIPEDIA（用于连接 GPT 和 WIKIPEDIA）</li>
<li><strong>ChromaDB</strong> – Vector storage（向量存储)）</li>
<li><strong>Tiktoken</strong> – Backend tokenizer for OpenAI（OpenAI 的后台令牌生成器）</li>
</ul>
<p>要安装这些软件，请在终端运行以下命令：</p>
<pre><code class="language-shell">pip install streamlit langchain openai wikipedia chromadb tiktoken
</code></pre>
<p>如果你的系统已经包含其中一些服务，则可以逐一安装。此外，我们还将为该环境配置 API Key 变量。将这些内容导入 app.py 文件后，我们就可以开始了。</p>
<pre><code class="language-Python">import os 
from apikey import apikey 

import streamlit as st 
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain, SequentialChain 
from langchain.memory import ConversationBufferMemory
from langchain.utilities import WikipediaAPIWrapper

os.environ['OPENAI_API_KEY'] = apikey
</code></pre>
<h2 id="">如何实现用户界面</h2>
<p>现在，我们要创建的应用程序相当简单。因此，我决定尽可能简化用户界面，只使用一个标题和文本输入框。对于这个 Tweet 生成器来说，这已经达到了目的。</p>
<pre><code class="language-Python"># Creating the title and input field
st.title('🦜🔗 Tweet Generator')
prompt = st.text_input('Tweet topic: ')
</code></pre>
<p>稍后，我们将添加功能来显示我们的话题历史、推特历史，以及最重要的，我们引用的维基百科数据。现在，这就是我们要使用的用户界面：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/Screenshot-2023-06-21-at-4.38.00-PM.png" alt="Screenshot-2023-06-21-at-4.38.00-PM" width="600" height="400" loading="lazy"></p>
<h2 id="">如何包含提示模板</h2>
<p>现在，我们将进入 LangChain 领域。在这一点上，如果你对 LangChain 不熟悉，也没有阅读过我关于 LangChain 的 <a href="https://www.freecodecamp.org/news/langchain-how-to-create-custom-knowledge-chatbots/">上一篇文章</a>，我强烈建议你阅读一下，以便更好地了解接下来的步骤。</p>
<p>我们要做的第一件事就是介绍 PromptTemplates。概括地说，PromptTemplates 是提示语的封装器，可以通过多种操作将它们串联起来，这也是 LangChain 的基础。</p>
<p>此外，我们还将为维基百科应用程序接口（Wikipedia API）提供一个封装，以便在链式执行中包含数据。</p>
<pre><code class="language-Python"># template for the title
title_template = PromptTemplate(
    input_variables = ['topic'], 
    template='write me a tweet about {topic}'
)

# template for the tweet
tweet_template = PromptTemplate(
    input_variables = ['title', 'wikipedia_research'], 
    template='write me a tweet on this title TITLE: {title} while leveraging this wikipedia reserch:{wikipedia_research} '
)

# wrapper for Wikipedia data
wiki = WikipediaAPIWrapper()
</code></pre>
<p>在这个例子中，我还创建了一个 标题提示（title prompt），为我们的推文主题提供一个总标题。至于实际的提示，如果你一直在使用 ChatGPT，它的概念基本相同，只是现在我们引入了变量（推特话题）。</p>
<p>这样我们就可以避免每次输入时都输入 <code>给我写一条关于...的推文</code>。相反，我们只需插入话题即可。说完这些，让我们开始介绍实际的 OpenAI LLM。</p>
<h2 id="introducingopenaisllms">Introducing OpenAI's LLMs</h2>
<p>有几种方法可以做到这一点，你可以选择自己认为合适的模式。在上一篇文章中，我使用了 GPT-3.5-turbo <a href="https://python.langchain.com/docs/modules/model_io/models/chat/integrations/openai">chat model</a>，代码如下：</p>
<pre><code class="language-Python">chat = ChatOpenAI(model_name="gpt-3.5-turbo",temperature=0.3)
messages = [
    SystemMessage(content="You are an expert data scientist"),
    HumanMessage(content="Write a Python script that trains a neural network on simulated data ")
]
</code></pre>
<p>不过，你可以根据自己的 API 密钥决定使用哪个模块，然后按照 <a href="https://python.langchain.com/docs/modules/model_io/models/llms/integrations/openai">LangChain 文档</a> 进行设置即可。</p>
<p>今天，我们将使用 "text-davinci-003 "模型，它与 ChatGPT 早期的 GPT-3 模型基本相同。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/06/Screenshot-2023-06-22-at-12.17.17-PM.png" alt="Screenshot-2023-06-22-at-12.17.17-PM" width="600" height="400" loading="lazy"></p>
<p>上图是 OpenAI 的各种模型（见其<a href="https://platform.openai.com/docs/models/overview">网站</a>）。</p>
<p>你可以随意尝试使用这些模型，看看哪种推文能产生最好的结果。你甚至可以尝试功能更强大（也更昂贵）的 GPT-4，但对于像推文生成器这样简单的提示完成案例，可能就没有必要了。</p>
<pre><code class="language-Python">llm = OpenAI(model_name="text-davinci-003", temperature=0.9) 
title_chain = LLMChain(llm=llm, prompt=title_template, verbose=True, output_key='title', memory=title_memory)
script_chain = LLMChain(llm=llm, prompt=script_template, verbose=True, output_key='script', memory=script_memory)
</code></pre>
<p>我还决定将 <code>temperature</code> 设定为 0.9，以便让模型发表更有创意的推文。<code>temperature</code> 可以衡量模型回复的随机性和创造性，0 代表简单明了，1 代表随机性极强。如果你希望你的回应更符合事实和确定性，只需调低 <code>temperature</code> 即可。</p>
<p><code>temperature</code> 将是我们进行这项工作所需的唯一变量。如果你想了解其他字段，请花些时间阅读文档，了解它们的含义。</p>
<p>例如，我们可以指定令牌限制(token limits)，以确保不会得到冗长的响应，但使用我们当前的 tweet 提示模板应该不成问题。</p>
<h2 id="tweet">如何跟踪你的 Tweet 生成历史</h2>
<p>这是一个可选部分，但可为应用程序提供额外功能。如果你想跟踪应用程序活动的历史记录，包括以前的标题或推文等信息，则只需包含以下步骤即可：</p>
<pre><code class="language-Python"># Memory 
title_memory = ConversationBufferMemory(input_key='topic', memory_key='chat_history')
script_memory = ConversationBufferMemory(input_key='title', memory_key='chat_history')
</code></pre>
<p>在上面的代码中，我们创建了两个不同的内存变量：<code>title_memory</code> 和 <code>script_memory</code>。<code>title_memory</code> 保存推文主题的历史记录，<code>script_memory</code> 保存推文的历史记录。</p>
<p><a href="https://python.langchain.com/docs/modules/memory/how_to/buffer">ConversationBufferMemory</a> 是 LangChain 的一项功能，它允许你跟踪迄今为止的输入和输出（在本例中，这只是我们之前生成的话题和推文）。</p>
<h2 id="">如何将组件串联起来</h2>
<p>现在，我们已经对应用程序的所有组件（用户界面、提示模板和维基百科封装器）进行了分类，可以将它们组合在一起执行了。这正是 LangChain 的价值所在。</p>
<p>标准程序中的 复合函数（composite functions）就是一个很好的类比。只不过，这里的函数是 PromptTemplates、LLMs 和 Wikipedia 数据。使用之前的封装器，我们只需决定执行顺序（就像一个链条），就能获得我们想要的输出。</p>
<p>在本例中，首先从我们的主题中获取标题，然后使用维基百科对该主题的相关研究创建 Tweet，最后使用 Streamlit 显示这些内容。</p>
<pre><code class="language-Python">if prompt: 
    title = title_chain.run(prompt)  # Running the title prompt
    wiki_research = wiki.run(prompt)  # Performing wikipedia research
    script = script_chain.run(title=title, wikipedia_research=wiki_research) # Creating the tweet

    st.write(title)  # Show the topic/title of the tweet
    st.write(script)  # Show the generated tweet

    with st.expander('Title History'): 
        st.info(title_memory.buffer)   # Storing the topic of the tweet to the history

    with st.expander('Tweet History'): 
        st.info(script_memory.buffer)  # Storing the tweet to the history

    with st.expander('Wikipedia Research'): 
        st.info(wiki_research)  # Storing the Wikipedia Research on the topic to the history
</code></pre>
<p>当我们运行 这些链（those chain）时，基本上是从用户界面获取话题，然后将其插入到与 LLM 相连的 PromptTemplate 中。推文 PromptTemplate 也会从维基百科中获取数据，并输入到 LLM 中。</p>
<p>最后，是时候检查我们的应用程序了。使用以下命令运行它：</p>
<pre><code class="language-shell">streamlit run app.py
</code></pre>
<h2 id="">最终代码和下一步行动</h2>
<p>其结果是能够克服 ChatGPT 过时的信息限制，并创建相关推文。如果你觉得难以理解，下面是我们目前掌握的情况：</p>
<pre><code class="language-Python"># Importing necessary packages, files and services
import os 
from apikey import apikey 

import streamlit as st 
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain, SequentialChain 
from langchain.memory import ConversationBufferMemory
from langchain.utilities import WikipediaAPIWrapper

os.environ['OPENAI_API_KEY'] = apikey

# App UI framework
st.title('🦜🔗 Tweet Generator')
prompt = st.text_input('Tweet topic: ') 

# Prompt templates
title_template = PromptTemplate(
    input_variables = ['topic'], 
    template='write me a tweet about {topic}'
)

tweet_template = PromptTemplate(
    input_variables = ['title', 'wikipedia_research'], 
    template='write me a tweet on this title TITLE: {title} while leveraging this wikipedia reserch:{wikipedia_research} '
)

# Wikipedia data
wiki = WikipediaAPIWrapper()

# Memory 
title_memory = ConversationBufferMemory(input_key='topic', memory_key='chat_history')
tweet_memory = ConversationBufferMemory(input_key='title', memory_key='chat_history')


# Llms
llm = OpenAI(model_name="text-davinci-003", temperature=0.9) 
title_chain = LLMChain(llm=llm, prompt=title_template, verbose=True, output_key='title', memory=title_memory)
tweet_chain = LLMChain(llm=llm, prompt=tweet_template, verbose=True, output_key='script', memory=tweet_memory)

# Chaining the components and displaying outputs
if prompt: 
    title = title_chain.run(prompt)
    wiki_research = wiki.run(prompt) 
    tweet = tweet_chain.run(title=title, wikipedia_research=wiki_research)

    st.write(title) 
    st.write(tweet) 

    with st.expander('Title History'): 
        st.info(title_memory.buffer)

    with st.expander('Tweet History'): 
        st.info(tweet_memory.buffer)

    with st.expander('Wikipedia Research'): 
        st.info(wiki_research)
</code></pre>
<p>当然，Tweet 生成器只是 LangChain 和 LLMs 的一个简单示例。例如，你还可以应用相同的流程创建 YouTube 脚本生成器或社交媒体内容日历助手，无限可能。</p>
<h2 id="">总结</h2>
<p>希望你喜欢这个有趣的教程！LangChain 最近非常流行，这是有原因的--它的用途非常广泛。我强烈推荐你去看看。</p>
<p>如果你喜欢这篇文章，并想了解更多关于人工智能创造者正在开发的新工具的信息，你可以通过我的<a href="https://bytesizedai.beehiiv.com/subscribe"><strong>字节大小的人工智能通讯</strong></a>了解最新信息。这里有大量关于人工智能领域的精彩故事，我希望你能加入我们的社区。</p>
<p>我也会定期在 <a href="https://www.linkedin.com/in/shanepduggan/">Linkedin</a> 上发帖，我很乐意与你联系！除此之外，祝大家享受创建项目的过程，我很期待看到你们分享的项目。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 使用 Docker 搭建私有软件仓库 ]]>
                </title>
                <description>
                    <![CDATA[ 我写这篇文章的起因是 npm 安装软件包网速慢，如果使用淘宝的镜像源 [https://registry.npmmirror.com/] ，一些非主流的包，更新不及时，还出现下载失败。如果使用官方源 [https://registry.npmjs.org]，网速就太慢了。 我将在这里分享自己的实践经验。 选型 verdaccio 因为考虑单纯为 npm 做本地镜像源，考虑使用 verdaccio [https://verdaccio.org/] ，想做成一个后台运行的服务，可以开机自启动，就考虑到 PM2 [https://github.com/Unitech/pm2]，但是有个问题，如果想 pm2 startup 生效，就必须以 root 用户运行这个命令。对系统侵入性太大了。 就考虑以 docker 的形式运行 verdaccio。 第一版 docker-compose.yml 文件： version: '3.1' services:   verdaccio:     image: verdaccio/verdaccio     restart: always    ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/use-docker-to-build-private-software-repositories/</link>
                <guid isPermaLink="false">641c4da65e1a4c068f38d69e</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Wed, 30 Aug 2023 02:12:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/03/1_AUiK5PwnsPG_xaT9jcVoSA-2.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>我写这篇文章的起因是 npm 安装软件包网速慢，如果使用淘宝的<a href="https://registry.npmmirror.com/">镜像源</a>，一些非主流的包，更新不及时，还出现下载失败。如果使用<a href="https://registry.npmjs.org">官方源</a>，网速就太慢了。</p>
<p>我将在这里分享自己的实践经验。</p>
<h2 id="">选型</h2>
<h3 id="verdaccio">verdaccio</h3>
<p>因为考虑单纯为 npm 做本地镜像源，考虑使用 <a href="https://verdaccio.org/">verdaccio</a>，想做成一个后台运行的服务，可以开机自启动，就考虑到 <a href="https://github.com/Unitech/pm2">PM2</a>，但是有个问题，如果想 <code>pm2 startup</code> 生效，就必须以 root 用户运行这个命令。对系统侵入性太大了。</p>
<p>就考虑以 docker 的形式运行 verdaccio。</p>
<p>第一版 docker-compose.yml 文件：</p>
<pre><code class="language-yaml">version: '3.1'

services:
  verdaccio:
    image: verdaccio/verdaccio
    restart: always
    container_name: 'verdaccio'
    ports:
      - '4873:4873'
    volumes:
      - './storage:/verdaccio/storage'
      - './config:/verdaccio/conf'
      - './plugins:/verdaccio/plugins'
</code></pre>
<p>创建 config 文件夹，在里面创建 config.yaml 文件：</p>
<pre><code class="language-shell">mkdir config
cd config
touch config.yaml
</code></pre>
<p>config.yaml 文件内容：</p>
<pre><code class="language-yaml">#
# This is the default config file. It allows all users to do anything,
# so don't use it on production systems.
#
# Look here for more config file examples:
# https://github.com/verdaccio/verdaccio/tree/master/conf
#

# path to a directory with all packages
storage: /verdaccio/storage
# path to a directory with plugins to include
plugins: /verdaccio/plugins
# print logs
# logs: ./logs

web:
  title: Verdaccio
  # comment out to disable gravatar support
  # gravatar: false
  # by default packages are ordercer ascendant (asc|desc)
  # sort_packages: asc
  # convert your UI to the dark side
  # darkMode: true
  #  HTML tags injected after manifest &lt;scripts/&gt;
  # scriptsBodyAfter:
  #    - '&lt;script type="text/javascript" src="https://my.company.com/customJS.min.js"&gt;&lt;/script&gt;'
  #  HTML tags injected before ends &lt;/head&gt;
  #  metaScripts:
  #    - '&lt;script type="text/javascript" src="https://code.jquery.com/jquery-3.5.1.slim.min.js"&gt;&lt;/script&gt;'
  #    - '&lt;script type="text/javascript" src="https://browser.sentry-cdn.com/5.15.5/bundle.min.js"&gt;&lt;/script&gt;'
  #    - '&lt;meta name="robots" content="noindex" /&gt;'
  #  HTML tags injected first child at &lt;body/&gt;
  #  bodyBefore:
  #    - '&lt;div id="myId"&gt;html before webpack scripts&lt;/div&gt;'
  #  Public path for template manifest scripts (only manifest)
  #  publicPath: http://somedomain.org/
# translate your registry, api i18n not available yet
# i18n:
# list of the available translations https://github.com/verdaccio/ui/tree/master/i18n/translations
#   web: en-US

auth:
  htpasswd:
    file: ./htpasswd
    # Maximum amount of users allowed to register, defaults to "+inf".
    # You can set this to -1 to disable registration.
    # max_users: 1000

# a list of other known repositories we can talk to
uplinks:
  npmjs:
    url: https://registry.npmjs.org/
  taobao:
    url: https://registry.npmmirror.com/
  tencent:
    url: https://mirrors.cloud.tencent.com/npm/
packages:
  '@*/*':
    # scoped packages
    access: $all
    publish: $authenticated
    unpublish: $authenticated
    proxy: npmjs

  '**':
    # allow all users (including non-authenticated users) to read and
    # publish all packages
    #
    # you can specify usernames/groupnames (depending on your auth plugin)
    # and three keywords: "$all", "$anonymous", "$authenticated"
    access: $all

    # allow all known users to publish/publish packages
    # (anyone can register by default, remember?)
    publish: $authenticated
    unpublish: $authenticated

    # if package is not available locally, proxy requests to 'npmjs' registry
    proxy: npmjs

server:
  # deprecated
  keepAliveTimeout: 60
#  rateLimit:
#    windowMs: 1000
#    max: 10000

middlewares:
  audit:
    enabled: true
listen: 0.0.0.0:4873

# log settings
logs:
  # Logger as STDOUT
  { type: stdout, format: pretty, level: http }
  # Logger as STDOUT as JSON
  # { type: stdout, format: json, level: http }
  # Logger as STDOUT as JSON
  # { type: stdout, format: pretty-timestamped, level: http }
  # Logger as STDOUT as custom prettifier
  # { type: stdout, plugin: { dest: '@verdaccio/logger-prettify' : options: { foo: 1, bar: 2}}, level: http }
  # Logger as file
  # { type: file, path: verdaccio.log, level: http}
  # FIXME: this should be documented
  # More info about log rotation https://github.com/pinojs/pino/blob/master/docs/help.md#log-rotation

# This affect the web and api (not developed yet)
i18n:
  web: en-US
</code></pre>
<h4 id="">意外</h4>
<p>启动容器</p>
<pre><code class="language-shell">docker-compose up 
</code></pre>
<p>报错 <code> permission denied, mkdir</code></p>
<p><a href="https://github.com/verdaccio/verdaccio/issues/3665">具体见这里</a>。</p>
<p>第二版</p>
<pre><code class="language-yaml">version: '3.1'

services:
  verdaccio:
    image: verdaccio/verdaccio
    restart: always
    user: 1000:1000
    container_name: 'verdaccio'
    ports:
      - '4873:4873'
    volumes:
      - './storage:/verdaccio/storage'
      - './config:/verdaccio/conf'
      - './plugins:/verdaccio/plugins'
</code></pre>
<p>通过修改文件夹的所有者</p>
<pre><code class="language-shell">id 
uid=1000(luo) gid=1000(luo) 组=1000(luo),4(adm),24(cdrom),27(sudo)

sudo  chown -R  1000:100  storage/  config/  plugins/
</code></pre>
<p>就解决问题了，user 的设置要你自己 Linux 服务器上一致，通过 <code>id</code> 命令获得，格式为  <code>user: uid: gid</code>。</p>
<p>启动</p>
<pre><code class="language-shell">docker-compose up -d 
</code></pre>
<p>在网页浏览器里输入 <code>http://ip:4873</code>，然后再终端输入，进行设置，使用本地的软件源</p>
<pre><code class="language-shell">npm set registry http://ip:4873/
</code></pre>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/03/----_20230323213523.png" alt="----_20230323213523" width="1304" height="760" loading="lazy"></p>
<h3 id="nexus3">Nexus 3</h3>
<p>选一个通用型软件镜像服务（Nexus 3），大部分的开发语言都支持，一次投入，多次收益，如图：</p>
<p><img src="https://chinese.freecodecamp.org/news/content/images/2023/03/nexus3.png" alt="nexus3" width="2561" height="1946" loading="lazy"></p>
<p>docker-compose.yml 文件</p>
<pre><code class="language-yaml">version: "3.6"

services:
  nexus:
    image: sonatype/nexus3:3.49.0
    restart: always
    environment:
      - INSTALL4J_ADD_VM_PARAMS=-Xms2g -Xmx2g -XX:MaxDirectMemorySize=2g -Djava.util.prefs.userRoot=/nexus-data/javaprefs -Duser.time    volumes:
      - ./nexus-data:/nexus-data
    ports:
      - "8081:8081"
</code></pre>
<p>Java 应用要限制一下内存，避免把宿主机的内存全吃掉，可根据实际情况调整。</p>
<p>启动</p>
<pre><code class="language-shell">docker-compose up -d 
</code></pre>
<p>还是遇到权限问题</p>
<pre><code class="language-shell">mkdir: cannot create directory '../sonatype-work/nexus3/log': Permission denied

mkdir: cannot create directory '../sonatype-work/nexus3/tmp': Permission denied
</code></pre>
<p>解决办法：<a href="https://soulteary.com/2018/10/08/how-to-migrate-nexus.html">参考资料</a></p>
<pre><code class="language-shell">sudo chown -R 200 ~/dockerVolume/nexus
</code></pre>
<p>重新启动</p>
<pre><code class="language-shell">docker-compose up -d
</code></pre>
<p>获得 admin 的登录密码</p>
<pre><code class="language-shell">
# 在启动应用的目录中执行

cat nexus-data/admin.password

# 或者直接使用 Docker CLI 执行容器命令
docker exec -it nexus.lab.io cat /nexus-data/admin.password
</code></pre>
<p>通过网页浏览器输入 <code>http://ip:8081</code> 访问管理后台。在输入了正确的初始账号和密码后，新版软件会人性化的引导我们设置新密码，以及设置是否允许匿名用户使用。</p>
<p>如果是个人使用，或者团队在内网使用，可以勾选“允许匿名访问”。</p>
<p>然后建个 npm(hosted) 和 npm(proxy)，组成一个 npm(grounp)，<br>
具体教程可以看 <a href="https://juejin.cn/post/6911642325559017480">这个</a>，但不要像它里面开放多个端口，没必要。</p>
<p>设置 npm 使用本地软件源（根据你的服务器的实际 IP 或者域名进行修改）</p>
<pre><code class="language-shell">npm set registry  http://192.168.2.114:8081/repository/npm-group/
</code></pre>
<h2 id="">总结</h2>
<p>docker 存储方面的权限设置是个问题，解决办法：</p>
<ol>
<li>自己构建 docker 镜像，从根源上解决问题，这难度比较大，要自己不断学习尝试。</li>
<li>多查找网络资料，通过设置解决。</li>
</ol>
<p>docker 部署方便，避免对系统的入侵，具有良好的隔离性，并且多平台支持，但是在权限安全方面还有很多要注意的。</p>
<p>如果你对这篇文章有任何疑问，或者如果你想和我探讨更多技术内容，欢迎和我联系。我的邮箱是：luojiyin@hotmail.com。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Next.js SEO——如何使用 Next 构建高性能应用程序 ]]>
                </title>
                <description>
                    <![CDATA[ Next.js 是一个流行的基于 React 的 Web 框架，近年来获得了流行和不断增长的社区。它是一个强大的工具，用于构建快速的和对 SEO 友好的 Web 应用，其动态页面在移动设备上运行良好。 由于同构系统设计的复杂性质，Next.js 的 SEO 可能是一个棘手的话题，让你头疼。特别是如果你在传统的 React 应用程序应用它，而且你只依赖文档。 凭借其对服务器端渲染、静态网站生成以及现在的 React 服务器组件的内置支持，Next.js 提供了一个强大的平台，可以在你的 Web 应用中实现高质量的 SEO 指标。它还可以帮助你在 Node 和 React 应用程序中的多个页面上提供卓越的用户体验，同时使它们对 SEO 友好。 为什么要学习 NextJS 用于前端开发？ 简而言之，最新版本的 NextJS 是一个开源平台，解决了 React 目前存在的很多渲染问题。我写这篇文章是因为很多前端开发者对我很生气:-D。 他们花了 6-9 个月开发一个 React App，然后我不得不要求他们重构代码。 Next.js 避免了很多渲染问题。它让搜索引擎非常容易理解你的网 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/nextjs-seo/</link>
                <guid isPermaLink="false">64af71ed486c7406702c8e98</guid>
                
                    <category>
                        <![CDATA[ NextJS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Thu, 13 Jul 2023 03:50:24 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/07/pexels-andrei-photo-2127783.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/nextjs-seo/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Next.js SEO for Developers – How to Build Highly Performant Apps with Next</a>
      </p><!--kg-card-begin: markdown--><p>Next.js 是一个流行的基于 React 的 Web 框架，近年来获得了流行和不断增长的社区。它是一个强大的工具，用于构建快速的和对 SEO 友好的 Web 应用，其动态页面在移动设备上运行良好。</p>
<p>由于同构系统设计的复杂性质，Next.js 的 SEO 可能是一个棘手的话题，让你头疼。特别是如果你在传统的 React 应用程序应用它，而且你只依赖文档。</p>
<p>凭借其对服务器端渲染、静态网站生成以及现在的 React 服务器组件的内置支持，Next.js 提供了一个强大的平台，可以在你的 Web 应用中实现高质量的 SEO 指标。它还可以帮助你在 Node 和 React 应用程序中的多个页面上提供卓越的用户体验，同时使它们对 SEO 友好。</p>
<h2 id="nextjs">为什么要学习 NextJS 用于前端开发？</h2>
<p>简而言之，最新版本的 NextJS 是一个开源平台，解决了 React 目前存在的很多渲染问题。我写这篇文章是因为很多前端开发者对我很生气:-D。</p>
<p>他们花了 6-9 个月开发一个 React App，然后我不得不要求他们重构代码。</p>
<p>Next.js 避免了很多渲染问题。它让搜索引擎非常容易理解你的网站是怎么回事。</p>
<h3 id="">谁会从这篇文章中得到最大的收获？</h3>
<p>如果你是一个营销人员或遇到 SEO 问题的资深开发人员，这将对你很有帮助。</p>
<p>然而，也欢迎新手开发者查看这些信息，因为它将对你有长期的帮助。</p>
<h2 id="js">你应该如何渲染你的下一个 JS 网页应用程序？</h2>
<p>我个人从我的咨询公司<a href="https://www.ohmycrawl.com/">OhMyCrawl</a>查看了大量的这些网站，并制作了一个<a href="https://www.youtube.com/watch?v=U8V0rk5AwBU">视频</a>概述，以帮助了解使用 Next.js 等框架对 SEO 的好处。</p>
<h2 id="nextseo">Next SEO 与其他框架有什么不同？</h2>
<p>Next SEO 通过将如此多的功能和免费工具精简到一个组织良好的软件包中，使你可以轻松地处理和应用于你的单页应用，这是它的优势。当涉及到搜索引擎优化、图像优化和最小化累积布局转移等任务时，Next 做得很好。</p>
<p>Next.js SEO 的好处还不止于此。我们将介绍 Next.js 带来的许多与搜索引擎有关的好东西。</p>
<h2 id="ssrssg">搜索引擎、SSR 和 SSG 的概念在不断发展</h2>
<p>大多数开发者和 SEO 专家已经对现有的页面创建策略和整个 SSR 与 SSG 范式感到相当满意。他们也对 Next.js 的第 12 版产生了高度的信任，该版本提供了一个清晰的方式来处理这两种形式的页面生成。</p>
<p>不过，像往常一样，另一个 Web 应用模式的转变正在进行中，这次是以 React 服务器组件（RSCs）的形式出现的，Next.js 第 13 版中默认包含了这些组件。</p>
<h3 id="seo">SEO 的概念没有改变--只是方法改变了</h3>
<p>Nextjs SEO 在概念上不会有太大变化。如果你想获得良好的搜索引擎结果和流量增长，关键仍然是快速载入页面、快速渲染、低累积布局变动等等。静态页面仍然扮演着重要角色。</p>
<p>但是，Next.js 提供了一些非常棒而独特的功能，可以帮助我们实现出色的搜索引擎指标，它不仅仅是 React Server Components。</p>
<p>我们将探讨一些最佳实践，以及使用 Next.js 实现出色的 SEO 优化指标的不同技术和策略。我们还将看到如何利用它独特的功能来提高网站的搜索引擎可见性（网页里优先显示）和用户参与度。</p>
<h2 id="nextjs13seo">Next.js 13 有哪些与 SEO 相关的新内容？</h2>
<p>我们不会给你一个关于第 13 版技术变化的全面指南，而是主要关注 Next JS 的 SEO 相关优势。我们还将探讨如何利用最佳的 SEO 实践，在搜索引擎中取得尽可能好的结果，而且能帮你节省不少精力。</p>
<p>我们将在这里讨论的 Nextjs 13 变化如下：</p>
<ul>
<li>React 服务器组件</li>
<li>流式 UI（Streaming UI chunks）</li>
<li>升级的 Next 图片组件</li>
<li>Next 字体组件</li>
</ul>
<p>除了 Next.js 默认的 SEO 属性外，这些特定的升级是 Next.js 版本 13 中 SEO 改进的基石。每个升级都有其自身的优点，我们将很快逐个介绍。</p>
<h3 id="react">React 服务器组件</h3>
<p>RSCs 允许在客户端和服务器上采用更精细的渲染方式。</p>
<p>React 允许开发者选择组件是在服务器上还是在客户端渲染，而不是在用户请求时被迫决定在客户端还是服务器上渲染整个页面。这可以让你在搜索引擎结果页面中获得巨大优势。</p>
<p>如今，绝大多数的页面优化都是围绕着减少向客户端发送 JavaScript。毕竟，这是使用预渲染和服务器端渲染来创建网页和 HTML 页面的主要好处。</p>
<p>RSC 是帮助实现这一目的的另一个工具，并从你的网页或单页应用程序中获得尽可能多的 SEO 价值。这有助于通过刷新 React 组件中的动态数据，同时保持页面内容的静态部分不变，从而实现更好的 SEO。</p>
<h3 id="ui">流式 UI</h3>
<p>Next.js SEO 在添加 RSC（React Server Components）的同时，迈出了一大步，而流式 UI（Streaming UI）代码块则是锦上添花。流式 UI 是一个类似的新兴设计模式，称为<code>岛屿架构（the island architecture）</code>，旨在在首次加载时尽量向客户端发送最少的代码。</p>
<p>允许细粒度的控制非常好，但为什么不向客户端发送一个无需 JavaScript 的完全渲染的页面，然后再发送剩余的内容呢？这正是流式 UI 代码块所实现的目标。</p>
<p>当 Next.js 在服务器端渲染页面时，通常会将页面的所有 JavaScript 捆绑并与之一起发送。而流式 UI 代码块的引入消除了这种需要，允许向客户端发送一个非常小的静态页面，显著改善了诸如首次内容呈现时间和整体页面速度等指标。</p>
<h3 id="nextjs13app">Next.js 13 App 目录</h3>
<p>当你启动一个新的 Next.js 13 项目时，你会注意到一个名为*<em>app</em>的新目录。在 app 目录下的所有东西都是预先配置好的，以允许 RSCs 和流式 UI 的出现。你只需要创建一个<a href="https://beta.nextjs.org/docs/routing/loading-ui">loading.js</a>组件，它将完全包住页面组件和 suspense 边界内的任何子节点。</p>
<p>你可以通过自己手动创建 suspense 边界来实现更精细的加载模式，基本上可以无限地控制初始请求时加载的内容。</p>
<p>流式 UI 的步骤大致如下：</p>
<ul>
<li>用户发起初始请求</li>
<li>渲染并发送基本的 HTML 页面给客户端</li>
<li>服务器准备 JavaScript 捆绑文件</li>
<li>在客户端浏览器中显示需要 JavaScript 的页面部分</li>
<li>仅将该组件所需的 JavaScript 捆绑文件发送给客户端</li>
</ul>
<p>这种新的工具对于技术性 SEO 具有重要影响，它使得更具交互性的页面能够与静态页面竞争，提高页面载入速度和其他在搜索引擎中用作排名因素的指标。</p>
<h3 id="nextimage">升级的 Next Image 组件</h3>
<p>Next.js SEO 领域的另一个重要升级是图片组件（Image component）。虽然它被低估了，但在我看来，最大的改进是利用了原生的懒加载。</p>
<p>浏览器对本地懒加载的支持已经有一段时间了，为这个功能添加额外的 JavaScript 只是浪费带宽而已。</p>
<p>其他一些对 SEO 有很大帮助的改进是：</p>
<ul>
<li>默认需要 alt 标签。</li>
<li>更好的验证，以确定涉及无效属性的错误。</li>
<li>由于有了一个更像 HTML 的界面，更容易进行样式设计。</li>
</ul>
<p>总的来说，新的图片组件被简化和精简了，而在编程领域，简单的东西几乎总是更好。</p>
<h3 id="nextfont">Next Font 组件</h3>
<p>字体组件（font component）对 Next.js 的 SEO 来说是一个巨大的胜利，它肯定会帮助减轻未来的许多头痛问题。任何有经验的开发者都知道，正确配置字体是多么繁琐的事情（在这种情况下，正确不是相对的！）。</p>
<p>由于加载缓慢而导致的累积布局转移是一个常见的困扰，像谷歌这样的搜索引擎已经<a href="https://developers.google.com/publisher-tag/guides/minimize-layout-shift">公开表示</a>，CLS 是一个重要的指标。（CLS 是 Cumulative Layout Shift 的缩写，衡量在网页的整个生命周期内发生的所有意外布局偏移的得分总和。）</p>
<p>根据你所使用的框架（我想到的是 Gatsby），让你的字体有效地预加载可能是很棘手的。一段时间以来，向谷歌等字体库发出外部请求是一个避免不了的丑陋行为，在许多 SPA 应用程序中造成了一个难以管理的瓶颈。</p>
<p>Next Font Component 旨在解决这个问题，它在构建时获取所有的外部字体，并从你自己的域中自我托管它们。字体也被自动优化，并且通过自动利用 CSS <strong>size-adjust（大小调整）</strong> 属性实现了零累积的布局转移。</p>
<h2 id="nextjsseo">用 Next.js 完成与 SEO 相关的常见任务</h2>
<p>为 Next.js 13 配置常见 SEO 任务时，有几个重要的议题需要考虑。</p>
<h3 id="nextjs13seo">Next.js 13 的 SEO</h3>
<p>Nextjs 的 React Head 组件通常被用来给文档头部的（Head）元标签赋值，也可以用来注入结构化数据。</p>
<p>然而，Nextjs13 中，Head 组件就不复存在了。起初，Next 选择利用一个名为 <strong>head.js</strong> 的特殊文件，其工作方式与 Head 组件类似。在 13.2 版本之后，Next 实现了<strong>Metadata</strong>组件，这是一个更专有的实现，通过轻松填充元标签来解决元数据问题。</p>
<p>让我们仔细看看这两个常见的 SEO 任务，并检查它们过去是如何处理的，而不是新的 13 版的方式。</p>
<h2 id="headtag">如何为搜索引擎优化配置头部标签（head tag）</h2>
<p>在 13 版之前，我们会导入 <strong>Next/Head</strong> 组件，并在网页的 HTML 文件中设置任何必要的元数据值，如标题和描述或其他元标签。</p>
<p>12 版中 Head 组件的一个简单例子是这样的：</p>
<pre><code class="language-js">import Head from 'next/head';
const structData = {
  '@context': 'https://schema.org',
  '@type': 'BlogPosting',
  headline: 'Learning Next.js SEO',
  description: 'All about Next.js features and more',
  author: [
    {
      '@type': 'Person',
      name: 'Jane Doe'
    }
  ],
  datePublished: '2023-02-16T09:00:00.000Z'
};
function IndexPage() {
  return (
    &lt;div&gt;
      &lt;Head&gt;
        &lt;meta name="viewport" content="initial-scale=1.0, width=device-width" /&gt;
        &lt;title&gt;My page title&lt;/title&gt;
        &lt;script
          key="structured-1"
          type="application/ld+json"
          dangerouslySetInnerHTML={{ __html: JSON.stringify(structData) }}
        /&gt;
      &lt;/Head&gt;
      &lt;p&gt;Hello world!&lt;/p&gt;
    &lt;/div&gt;
  );
}
export default IndexPage;
</code></pre>
<p>在页面的元数据中添加结构化数据，如标题和描述或任何额外的元标签，这只是一个简单的问题，包括一个带有 <strong>dangerouslySetInnerHTML</strong> 属性的脚本标签，如例子中所示。</p>
<p>大多数开发人员编写一个利用 Head 组件的 SEO 组件，以实现更多的 DRY（不要重复自己）方法。在这里，你防止相同的数据或 HTML 文件被多次发送给用户。但在幕后都是一样的，Head 是优化网页元标签方面的首选方法。</p>
<h3 id="nextheadjs">Next 特殊的 head.js 文件</h3>
<p>在第 13 版中，你可以完全忘记通常的 Head 组件。从 13 版的第一次迭代开始，Next 实现了<strong>head.js（或.tsx）</strong> 文件。这个文件可以包含在应用程序目录内的任何文件夹中，以动态管理 SEO 元数据，并声明哪些标签，以及它们的值，将被用于特定的路线和特定的页面。</p>
<p>应用程序目录中的每个文件夹都是一个新的路径，这就是为什么你需要在每个文件夹中创建一个 <strong>head.js</strong> 文件来配置你的元数据值。下面是一个 <strong>head.js</strong> 文件的例子：</p>
<pre><code class="language-js">export default function Head(params) {
  return (
    &lt;&gt;
      &lt;title&gt;head.js Example&lt;/title&gt;
    &lt;/&gt;
  );
}
</code></pre>
<p>注意，我们返回的是一个 React 片段，而不是一个实际的 head 标签，或任何其他元素。这是 <strong>head.js</strong> 组件的一个必要方面。</p>
<p>你只能从片段中返回以下元数据标签：<code>&lt;title&gt;</code>、<code>&lt;meta&gt;</code>、<code>&lt;link&gt;</code>（优先级属性）或 <code>&lt;script&gt;</code>（带有 async 属性）。</p>
<p>Next 从来没有完全充实过这个组件，这就是为什么大多数开发者为添加结构化数据而开发了自定义实现。不过，后来 Next 确实开始建议将结构化数据添加到布局或页面组件中，我们稍后将对此进行讨论。</p>
<p>这个组件在 13.2 版本中被废弃，Vercel 团队转而使用 <strong>Metadata</strong> 组件。</p>
<h3 id="nextmetadata">Next Metadata 组件</h3>
<p>随着 Next 13.2 版本的发布，Next 决定完全不使用 head 组件，而是创建了 <strong>Metadata</strong> 组件。</p>
<p>在写这篇文章的时候，还没有大量的使用例子等材料。事实上，13.2 甚至还没有发布，而我们目前只到了 13.1.7-canaray。</p>
<p>尽管如此，Next 公司在他们的文档中还是有一个很好的写法，所以我们会去看一下用法，并给出一个基本的分析。如果你打算在 Next.js 的搜索引擎优化上有所作为，那么了解这方面的情况是很重要的，因为它即将到来。</p>
<p>Metadata 组件旨在成为一站式商店，以高效和易于使用的方式为 <strong>head</strong> 标签填充标题和描述以及其他动态元数据。使用方法其实很简单，包括从页面组件本身导出一个名为 <strong>metadata</strong> 的对象或一个名为 <strong>generateMetadata</strong> 的函数。</p>
<p>让我们来看看一个基本的使用例子：</p>
<h3 id="nextjsexportmetadata">Next.js export metadata 例子</h3>
<p><strong>examplePage.tsx</strong></p>
<pre><code class="language-js">import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Example component',
description: 'Learning Next.js SEO',
};
export default function Page() {
return (
&lt;&gt;
&lt;div&gt;Example page component…&lt;/div&gt;
)  	&lt;/&gt;
}
</code></pre>
<p>这将自动为 <strong>examplePage.tsx</strong> 组件填充适当的 HTML 元标签和给定值。</p>
<p><strong>metadata</strong> 组件也提供了一组默认标签，它自动设置了以下 <strong>charset</strong> 和 <strong>viewport</strong> 元标签：</p>
<pre><code class="language-js">
&lt;meta charset="utf-8" /&gt;
&lt;meta name="viewport" content="width=device-width, initial-scale=1" /&gt;
</code></pre>
<h3 id="nextjsexportgeneratemetadata">Next.js export generateMetadata 例子</h3>
<p>在许多应用程序中，特别是电子商务应用，你可能会遇到动态填充元标签的需求，以确保这些数据始终反映数据库或其他数据存储的更改。</p>
<p>对于这些情况，Next.js 提供了 generateMetadata 函数，可以从任何页面组件中导出，并与获取和注入所需数据的任何必要的 API 调用一起使用。</p>
<p>以下是一个利用该方法的示例页面组件：</p>
<p><strong>pageExample.tsx</strong></p>
<pre><code class="language-js">import type { Metadata } from 'next';
async function getInfo(id) {
  const res = await fetch(`https://someapi/product/${id}`);
  return res.json();
}
export async function generateMetadata({ params }): Promise&lt;Metadata&gt; {
  const product = await getInfo(params.id);
  return { title: product.title };
}
export default async function Page() {
  return &lt;div&gt;Example page…&lt;/div&gt;;
}
</code></pre>
<p>正如你所看到的，我们创建了一个方法，从 API 返回一些关于产品的信息，并在我们的 <strong>generateMetadata</strong> 函数中使用该方法，然后填充 <strong>title</strong> 属性。这又将在我们渲染的 HTML 页面中设置标题标签（title tag）。</p>
<p>值得注意的是，<strong>generateMetadata</strong> 函数只能在服务器组件中使用，这一点我们在前面讨论过。记住，在<strong>app</strong>目录下的所有组件都被自动配置为默认的服务器组件。</p>
<p>关于 <strong>Metadata</strong> 组件可用的属性和属性扩展的详尽列表，请看<a href="https://beta.nextjs.org/docs/api-reference/metadata">文档</a>。几乎所有你能想到的东西都可以相对容易地完成。</p>
<h2 id="next13">如何用 Next 13 实现结构化数据</h2>
<p>接下来建议将结构化的 JSON-LD 数据添加到布局或页面组件中。说实话，这一直是一个更简单的解决方案，因为谷歌从来没有说过要把结构化数据从页面本身排除出去。</p>
<p>事实上，这早已是一种常见的做法，至于为什么许多开发者都固定在将其放在头部标签（head tag）的想法上，这就有点神秘了。</p>
<h3 id="layoutpagecomponent">如何向版面（Layout）或页面组件（Page Component）添加结构化数据</h3>
<p>将结构化数据添加到一个组件中，无论是布局还是页面，看起来都是这样的（例子直接来自文档）：</p>
<pre><code class="language-js">export default async function Page({ params }) {
  const product = await getProduct(params.id);
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    image: product.image,
    description: product.description
  };
  return (
    &lt;section&gt;
      {/* Add JSON-LD to your page */}
      &lt;script type="application/ld+json"&gt;{JSON.stringify(jsonLd)}&lt;/script&gt;
      {/* ... */}
    &lt;/section&gt;
  );
}
</code></pre>
<p>正如你所看到的，这是超级简单的，真的没有必要通过尝试将结构化数据注入头部标签（head tag）来使事情复杂化。</p>
<h2 id="nextjsseo">Next.js SEO – 结束语</h2>
<p>我们在这里对 <a href="https://www.ohmycrawl.com/next-js-seo/">Nextjs SEO</a> 进行了相当多的讨论。从 Next 13 中包含的旨在解决许多 SEO 相关问题的新功能，到为获得更好的开发者体验而对旧功能进行改造和精简，Next 的情况看起来很好。</p>
<p>如果你打算使用 Next.js 作为你的首选框架，我强烈建议你尝试使用 Next 13。尽管诸如字体组件等功能仍处于测试阶段，而且整个系统仍处于金丝雀（测试阶段），但第 13 版的大部分内容已被认为是稳定的，并被许多开发者和世界领先的公司所使用。</p>
<p>主要的版本更新可能是令人生畏的，但请阅读完整的文档，并尝试使用它，以确保你跟上 Next.js SEO 的最新进展。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 用 Ghost API 和 Next.js 创建一个博客网站 ]]>
                </title>
                <description>
                    <![CDATA[ Ghost CMS 是一个流行的内容管理系统，许多开发者和公司用它来托管他们的博客。 它有许多功能和一个高度优化的编辑器，适合写作。你甚至可以使用 handlebars.js [https://handlebarsjs.com/]  构建不同的主题。 但如果你不了解 Handlebars，学习它可能是一个漫长而困难的过程。如果你已经是一个 Next.js 的开发者，而你不知道 Handlebars，为你基于 Ghost 的网站创建一个新的主题可能会很艰难。 在这篇文章中，我将教你如何使用 Ghost CMS 作为后端和 Next.js 作为前端。我将指导你完成与 Nextjs 13 应用目录 [https://beta.nextjs.org/docs/getting-started] 和 Ghost CMS API 有关的一切。 Next.js 13 团队目前正在开发实验性的 app 文件夹。Next 使用基于文件的路由与page目录。新的 app  目录基于文件系统路由，并提供额外的功能，如布局、错误处理、组件加载、服务器端和客户端渲染等。 所有的代码都可以在 GitHub ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/build-a-blog-website-with-ghost-api-and-nextjs/</link>
                <guid isPermaLink="false">64a572bf0f6905067957c329</guid>
                
                    <category>
                        <![CDATA[ NextJS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Wed, 05 Jul 2023 05:55:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/07/Ghost-API-and-Nextjs--2-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/build-a-blog-website-with-ghost-api-and-nextjs/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Build a Blog with the Ghost API and Next.js</a>
      </p><!--kg-card-begin: markdown--><p>Ghost CMS 是一个流行的内容管理系统，许多开发者和公司用它来托管他们的博客。</p>
<p>它有许多功能和一个高度优化的编辑器，适合写作。你甚至可以使用 <a href="https://handlebarsjs.com/">handlebars.js</a> 构建不同的主题。</p>
<p>但如果你不了解 Handlebars，学习它可能是一个漫长而困难的过程。如果你已经是一个 Next.js 的开发者，而你不知道 Handlebars，为你基于 Ghost 的网站创建一个新的主题可能会很艰难。</p>
<p>在这篇文章中，我将教你如何使用 Ghost CMS 作为后端和 Next.js 作为前端。我将指导你完成与 <a href="https://beta.nextjs.org/docs/getting-started">Nextjs 13 应用目录</a> 和 Ghost CMS API 有关的一切。</p>
<p>Next.js 13 团队目前正在开发实验性的 app 文件夹。Next 使用基于文件的路由与<code>page</code>目录。新的 <code>app</code> 目录基于文件系统路由，并提供额外的功能，如布局、错误处理、组件加载、服务器端和客户端渲染等。</p>
<p>所有的代码都可以在 <a href="https://github.com/officialrajdeepsingh/nextjsghostcms">GitHub</a> 上找到。你也可以查看网上的 <a href="https://nextjsghostcms.vercel.app/">演示网站</a>。</p>
<h2 id="">目录</h2>
<ol>
<li><a href="#why-use-next-js-for-the-front-end-and-not-a-ghost-cms-theme">为什么在前端使用 Next.js 而不是 Ghost CMS 主题</a></li>
<li><a href="#project-requirements">做项目前需要做好的准备</a></li>
<li><a href="#how-to-set-up-ghost-cms">如何设置 Ghost CMS</a></li>
<li><a href="#how-to-set-up-ghost-cms-with-the-cloud">如何利用云计算建立 Ghost CMS</a></li>
<li><a href="#how-to-get-the-blog-template">如何获得博客模板</a></li>
<li><a href="#how-to-set-up-next-js">如何设置 Next.js</a></li>
<li><a href="#what-to-know-before-following-this-tutorial">在学习本教程之前，需要知道什么</a></li>
<li><a href="#folder-structure">文件夹结构</a></li>
<li><a href="#how-to-configure-ghost-cms-and-next-js">如何配置 Ghost CMS 和 Next.js</a></li>
<li><a href="#understanding-the-next-js-13-app-folder">了解 Next.js 13 app 文件夹</a></li>
<li><a href="#demo-data-for-the-project">项目的演示数据</a></li>
<li><a href="#how-to-build-the-blog">如何建立博客</a></li>
<li><a href="#how-to-build-the-header">如何建立页眉（header）</a></li>
<li><a href="#how-to-build-the-footer">如何建立页脚（Footer）</a></li>
<li><a href="#how-to-build-the-layout">如何建立 layout</a></li>
<li><a href="#how-to-built-the-homepage">如何建立主页（homepage）</a></li>
<li><a href="#how-to-build-the-reading-page">如何建立阅读页（reading page）</a></li>
<li><a href="#how-to-build-the-tag-page">如何建立标签页（tag page）</a></li>
<li><a href="#how-to-build-the-author-page">如何建立作者页（author page）</a></li>
<li><a href="#how-to-build-single-pages">如何建立单页（single pages）</a></li>
<li><a href="#how-to-handle-pagination">如何处理分页（pagination）</a></li>
<li><a href="#next-js-seo">Next.js SEO</a></li>
<li><a href="#how-to-enable-search">如何开启搜索</a></li>
<li><a href="#error-handling">错误处理</a></li>
<li><a href="#how-to-rebuild-your-static-site-with-webhooks">如何用 webhooks 重新构建你的静态网站</a></li>
<li><a href="#conclusion">总结</a></li>
</ol>
<p>在这篇文章中，我们将介绍 Next 的带有实验性的 app 文件目录的基本情况。然后，我将教你如何在本地加强 Next 和 Ghost CMS，以及如何将 Ghost 与 Next 整合。最后，我会告诉你如何从后端（通过 Ghost CMS 的 API）获取数据，并用 React.js 在网站上显示。</p>
<h2 id="why-use-next-js-for-the-front-end-and-not-a-ghost-cms-theme">为什么在前端使用 Next.js 而不是 Ghost CMS 主题</h2>
<p>有几个原因可以让你考虑使用 Next 作为你的博客的前端框架：</p>
<ol>
<li>Ghost CMS 不生成静态构建，但 Next.js 可以。</li>
<li>使用 Next.js，你可以获得更高的网站速度和性能，而且它现在提供了内置的 SEO 支持和其他优化功能。Ghost 不具备其中的一些功能。</li>
<li>对于 React 开发者来说，用 Next 构建一个新的博客很容易（因为 Next 是基于 React 的），你不需要学习额外的工具。</li>
<li>你会发现有一些服务提供商可以为 Ghost 提供服务，一键部署 Ghost 博客。他们中的大多数都有一个付费计划，而有一两个提供免费计划（但这些往往有时间和功能限制）。对于 Next.js，市场上有许多供应商。</li>
</ol>
<p>通常来说，当涉及到静态构建和网站性能时，Ghost 在这两种情况下的表现都不尽如人意。另一个选择是使用一个前端平台，如 Next、React、Angular 或 Vue。</p>
<p>我选择 Next 是因为它是一个需求量很大、很受欢迎的 React 框架，而且大量的工具和库都是围绕它建立的。</p>
<p>请注意，目前的项目还没有为 TypeScript 做好准备，但我正在努力。因为这个原因 <a href="https://medium.com/frontendweb/basic-explanation-about-the-next-config-js-file-eaa539e1fea3">我在构建时禁用了 TypeScript</a>，像这样：</p>
<pre><code class="language-typescript">/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true
  },

  typescript: {
    ignoreBuildErrors: false
  }
};

module.exports = nextConfig;
</code></pre>
<p>在开发过程中忽略构建错误</p>
<h2 id="project-requirements">做项目前需要做好的准备</h2>
<p>要跟上这个教程，你需要具备以下软件包的基本知识：</p>
<ol>
<li><a href="https://pnpm.io/">PNPM</a>是一个类似于 npm 或 yarn 的 Node.js 包管理器（你也可以使用你喜欢的任何一个）。</li>
<li><a href="https://www.typescriptlang.org/">TypeScript</a>帮助你在 JavaScript 中编写类型安全的代码，也可以帮助提高生产力。不过，这不是必须的。你可以在你的项目中使用 JavaScript。</li>
<li><a href="https://react.dev/">React.js</a>是一个免费和开源的前端 JavaScript 库，用于用类和函数组件构建用户界面。</li>
<li><a href="https://beta.nextjs.org/docs/getting-started">Next.js 13 (app)</a>是基于 React 的，它提供了额外的功能，如路由、错误处理和布局。</li>
<li><a href="https://ghost.org/docs/content-api/">Ghost CMS API</a>是一个开源的内容管理系统（CMS），类似于 WordPress。Ghost 是专门为博客设计和建造的。在这个项目中，我们将 Ghost 作为后端，Next 作为前端。对于后端和前端开发之间的通信，我们将使用 Ghost CMS API。</li>
<li><a href="https://tailwindcss.com/">Tailwind CSS</a>是一个开源的 CSS 的框架，类似于 <a href="https://getbootstrap.com/">Bootstrap</a>。我们将使用 Tailwind CSS 来设计我们的博客网站。</li>
</ol>
<h2 id="how-to-set-up-ghost-cms">如何设置 Ghost CMS</h2>
<p>下一步是在本地安装 Ghost，你可以用一条命令完成。首先，你需要用 pnpm、yarn 或 npm 全局安装<code>ghost-cli</code>。</p>
<pre><code class="language-bash">pnpm add -g ghost-cli@latest

# or

yarn global add ghost-cli@latest

# or

npm install ghost-cli@latest -g
</code></pre>
<p>global</p>
<p>安装 Ghost CLI 后，你可以用以下命令在本地创建一个新的 Ghost 博客项目：</p>
<pre><code class="language-bash">ghost install local
</code></pre>
<p>博客安装完成后，你可以用 <code>ghost start</code> 命令启动你的本地开发服务器，你的本地开发服务可以通过<code>http://localhost:2368/ghost</code> 访问。</p>
<h3 id="ghostcli">其他 Ghost CLI 命令</h3>
<p>在使用 Ghost CLI 时，有几个命令是有帮助的：</p>
<ul>
<li><code>ghost start</code>：启动你的服务</li>
<li><code>ghost stop</code>：停止运行你的 Ghost 服务</li>
<li><code>ghost help</code>：查看可用的命令列表</li>
</ul>
<p><strong>注意：</strong></p>
<p>在安装之前，请确保你当前的安装目录是空的。目前，你是在开发模式下安装 Ghost。对于生产来说，你不会遵循同样的步骤。</p>
<h2 id="how-to-set-up-ghost-cms-with-the-cloud">如何利用云计算建立Ghost CMS</h2>
<p>如果你在本地安装 Ghost 时遇到任何问题，或者可能太复杂，或者你的驱动器上没有足够的空间，你可以使用像 <a href="https://www.digitalpress.blog/">digital press</a> 这样的工具或任何其他云服务，如 GCP 或 AWS，Digital Ocean，等等。</p>
<p>我喜欢 digital press，因为它有一个免费计划。其他云服务不提供这一点，这就是为什么我建议它。</p>
<h2 id="how-to-get-the-blog-template">如何获得博客模板</h2>
<p>从头开始创建一个新的博客可能很困难。在本教程中，我们将使用一个来自 <a href="https://github.com/orgs/frontendweb3">the frontend web</a> 的预构建好的模板。所有的模板都有一个开源的 MIT 许可，所以你可以使用它们，而且你不需要设置一切。</p>
<p>我从里面挑选了 <a href="https://github.com/frontendweb3/open-blog">Open-blog</a> 的模板。</p>
<h2 id="how-to-set-up-next-js">如何设置 Next.js</h2>
<p>设置 Next 是本教程的主要部分之一，你将花时间和精力在编码、调试和部署网站上。</p>
<p>以下是要运行的命令，取决于你使用的是 npx、yarn，还是 pnpm：</p>
<pre><code class="language-bash">npx create-next-app@latest --experimental-app

# or

yarn create next-app --experimental-app

# or

pnpm create next-app --experimental-app
</code></pre>
<p>安装 nextjs 的时候使用新的实验性功能。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/03/ghostandnextjs--1-.png" alt="create a new nextjs app." width="600" height="400" loading="lazy"></p>
<p>创建一个新的 Nextjs 应用程序。</p>
<p>完成安装过程后，我们必须为博客安装一些额外的 Node 包。</p>
<p>这些 Node 包可以帮助你加快开发进程。请确保安装以下所有的包，以便跟上本指南：</p>
<h3 id="node">要安装的 Node 包</h3>
<ol>
<li><code>pnpm add @tryghost/content-api</code>(required)</li>
<li><code>pnpm add @types/tryghost__content-api</code> (required by TypeScript)</li>
<li><code>pnpm add tailwindcss postcss autoprefixer</code></li>
<li><code>pnpm add @tailwindcss/typography</code></li>
<li><code>pnpm add react-icons</code></li>
<li><code>pnpm add date-fns</code></li>
<li><code>pnpm add next-themes</code></li>
<li><code>pnpm add @radix-ui/react-popover</code></li>
</ol>
<p>以下是这些包的作用：</p>
<ul>
<li><a href="https://www.npmjs.com/package/@tryghost/content-api">@tryghost/content-api</a> 是一个 Ghost JavaScript 客户端库，用于获取<a href="https://ghost.org/docs/content-api/">content API</a>数据。</li>
<li><a href="https://www.npmjs.com/package/@types/tryghost__content-api">@types/tryghost__content-api</a> 包含@tryghost/content-api 的类型定义。</li>
<li>TailwindCSS、autoprefixer 和 PostCSS 都是在使用时需要的包。 <a href="https://beta.nextjs.org/docs/styling/tailwind-css">Tailwind CSS</a>.</li>
<li><a href="https://tailwindcss.com/docs/typography-plugin">@tailwindcss/typography</a> 用于用 Tailwind CSS 处理动态排版的包。</li>
<li><a href="https://www.npmjs.com/package/next-themes">next-themes</a> 主题包，如在你的网站上从黑暗模式切换到日间模式。</li>
<li><a href="https://www.npmjs.com/package/react-icons">react-icons</a>为项目提供了大量的 SVG 图标。这样一来，你就不需要手动下载它们了。</li>
<li><a href="https://www.radix-ui.com/docs/primitives/components/popover#installation">@radix-ui/react-popover</a>是 Radix UI 生态系统的一部分。我选择 Radix 的弹出式组件来设计网站上的搜索组件。</li>
<li><a href="https://www.npmjs.com/package/date-fns">date-fns</a> 有助于将你的<code>published_at</code>日期转换成不同的日期格式的包。</li>
</ul>
<h2 id="what-to-know-before-following-this-tutorial">在学习本教程之前，需要知道什么</h2>
<p>在构建这个项目之前，我强烈建议在 YouTube 上观看一些教程（尤其是如果你是 Next.js 的初学者）。这些将帮助你了解有关 Next.js app 文件夹的实验性功能一些基本知识。</p>
<p>每个视频将解释同一类主题。如果你看了这四个视频中的每一个，你就对 Next.js 应用文件夹的工作原理有了基本的了解。这将使这个高级教程更容易理解。</p>
<h3 id="vercel"><a href="https://www.youtube.com/@VercelHQ">Vercel</a></h3>
<p>在本教程中，Lee Robinson 介绍了路由(route)、动态路由段(dynamic route segments)、数据获取(data fetching)、缓存(caching)和元数据(metadata)的基础知识。</p>
<h3 id="sakuradev"><a href="https://www.youtube.com/@SakuraDev">Sakura Dev</a></h3>
<p>Sakura Dev 用实例教你 Next.js 页面和 App 文件夹以及路由之间的区别。</p>
<h3 id="tuomokankaanpaa">Tuomo Kankaanpaa</h3>
<p>Tuomo Kankaanpaa 教你了解 Next 应用程序的文件夹路由(folder routing)、布局(layouts)和服务器组件(server components)。</p>
<h3 id="piyushgarg"><a href="https://www.youtube.com/watch?v=CBfBZvDQLis">Piyush Garg</a></h3>
<p>Piyush Garg 编译了所有 Next 的新功能，并将其转换为一个小的速成课程，并建立了一个演示项目。</p>
<p>现在你已经准备好了，让我们开始建立我们的博客。</p>
<h2 id="folder-structure">文件夹结构</h2>
<p>对于我们的演示应用程序，我们的文件夹结构看起来像这样：</p>
<pre><code class="language-bash">.
├── next.config.js
├── next-env.d.ts
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
├── README.md
├── search.json
├── src
│   └── app
│       ├── authors
│       │   └── [slug]
│       │       └── page.tsx
│       ├── BlogLayout.tsx
│       ├── cards.min.css
│       ├── Card.tsx
│       ├── error.tsx
│       ├── favicon.ico
│       ├── Footer.tsx
│       ├── ghost-client.ts
│       ├── globals.css
│       ├── Header.tsx
│       ├── layout.tsx
│       ├── not-found.tsx
│       ├── pages
│       │   └── [slug]
│       │       └── page.tsx
│       ├── page.tsx
│       ├── pagination
│       │   └── [item]
│       │       └── page.tsx
│       ├── Pagination.tsx
│       ├── read
│       │   └── [slug]
│       │       ├── Newsletter.tsx
│       │       └── page.tsx
│       ├── Search.tsx
│       ├── SocialIcons.tsx
│       └── tags
│           └── [slug]
│               └── page.tsx
├── tailwind.config.js
└── tsconfig.json

13 directories, 30 files
</code></pre>
<p>使用 Nextjs 和 Ghost cms 的文件夹结构</p>
<h2 id="how-to-configure-ghost-cms-and-next-js">如何配置Ghost CMS和Next.js</h2>
<p>下一步是为 Ghost Content API 设置数据获取。这就是为什么我们安装了上面的<a href="https://www.npmjs.com/package/@tryghost/content-api">@tryghost/content-api</a>包。</p>
<p>Ghost CMS 带有两种类型的 API：第一种是<a href="https://ghost.org/docs/content-api/">内容 API</a>，第二种是<a href="https://ghost.org/docs/admin-api/">管理 API</a>。对于博客，我们将使用<a href="https://ghost.org/docs/content-api/">内容 API</a>。</p>
<p>内容 API 是一个 RESTful API，为 Ghost 数据库获取已发布的内容。它是一个只读的 API。你不能用它来调用 POST 请求。</p>
<p>为了配置它，我们在<code>src/app</code>文件夹下创建了一个新的文件<code>ghost-client.ts</code>。在该文件中，我们有一个新的 Ghost API 实例。</p>
<pre><code class="language-typescript">// ghost-client.ts

import GhostContentAPI from '@tryghost/content-api';

// Create API instance with site credentials
const api = new GhostContentAPI({
  url: process.env.GHOST_URL as string,
  key: process.env.GHOST_KEY as string,
  version: 'v5.0'
});
</code></pre>
<p>创建一个新的 Ghost CMS 实例。</p>
<p>我们需要博客的 URL、Key 和版本来在 Next 中配置 Ghost 的内容 API。你可以在 Ghost 仪表盘中找到 URLs 和 Key 属性，以及版本值，它是你当前 Ghost CMS 的版本。</p>
<p>进入 Ghost 仪表板：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/03/ghost-next.gif" alt="获取你的 KEY 和 URL" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>获取你的 KEY 和 URL</figcaption>
</figure>
<p>去到 <code>dashboard</code> &gt; <code>settings</code> &gt; <code>integrations</code> &gt; <code>Your-intergration-id</code>， 获得你的 <code>GHOST_URL</code> 和 <code>GHOST_KEY</code>。 现在你可以复制这两份信息，并将其粘贴在你的 <code>.env.local</code> 文件.</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/03/next-and-ghost.png" alt="获取你的 KEY 和 URL" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>获得你的 GHOST_KEY 和 GHOST_URL</figcaption>
</figure>
<h2 id="understanding-the-next-js-13-app-folder">了解Next.js 13 app文件夹</h2>
<p>随着 Next.js 13 的发布，Next.js 的<code>pages</code>文件夹和<code>app</code>文件夹发生了很多变化。我们现在就来讨论一些重要的东西，在构建应用时再讨论更多：</p>
<ol>
<li>没有<code>_app</code>、<code>_document</code>、<code>getServerSideProps</code>、<code>getStaticProps</code>、<code>getStaticPaths</code>、<code>404</code>和<code>useRouter</code></li>
<li>现在它将<code>_app</code>和<code>_document</code>文件与<code>layout</code>文件相结合。</li>
<li><code>useRouter</code>是从<code>next/navigation</code>中导入的。</li>
<li><code>404</code>文件被<code>notFound()</code>函数取代。</li>
<li><code>error.tsx</code>文件提供了对错误边界的反应等功能。</li>
<li>现在<code>index.js</code>文件被<code>page.js</code>取代。</li>
<li>传递动态路由段<code>pages/blog/[slug].js</code>被改变，下一个应用程序目录看起来像这样： <code>app/blog/[slug]/page.js</code>。</li>
</ol>
<h3 id="">例子</h3>
<p>为了理解 Next 带有实验性的 app 文件夹，让我们看看一个真实的例子：</p>
<ol>
<li><strong>tag page</strong> =&gt; <code>app/tag/[slug]/page.ts</code>。</li>
<li><strong>category</strong> =&gt; <code>app/tag/[slug]/page.ts</code>。</li>
</ol>
<p>现在你可以在每个路由里面创建五个文件。例如，如果你在你的 app 文件夹中创建一个<code>tag</code> 或 <strong><code>category</code></strong> 路由，那么你可以在你的 app 路由文件夹中创建四个文件。</p>
<ul>
<li><code>page.ts</code>（必填）：它是你的主文件。</li>
<li><code>layout.ts</code>（可选）：它有助于设计你的布局。</li>
<li><code>loading.ts</code>（可选）：它用 React suspense 创建一个加载指标。</li>
<li><code>error.ts</code>（可选）：它帮助处理你的 React 应用程序中的错误。</li>
<li><code>components</code>（可选）：你也可以在你的路由中创建另一个组件。</li>
</ul>
<p>让我们通过一个真实的例子来了解新的 Next.js 13 路由是如何工作的：你的标签路由文件夹看起来像这样。</p>
<pre><code class="language-typescript">app / tag / [slug] / page.ts;
app / tag / [slug] / loading.ts;
app / tag / [slug] / layout.ts;
app / tag / [slug] / error.ts;
app / tag / [slug] / my - card - component.ts;
</code></pre>
<p>Tag 文件夹结构</p>
<h2 id="demo-data-for-the-project">项目的演示数据</h2>
<p>你不必担心创建一个演示或假的博客文章数据。对于你的测试，你可以从这个<a href="https://github.com/officialrajdeepsingh/nextjsghostcms/blob/main/.github/demo-post-for-ghost.json">GitHub 仓库</a>下载它。</p>
<h2 id="how-to-build-the-blog">如何建立博客</h2>
<p>我们将在下面的章节中对博客的每个部分进行梳理和构建，这样你就可以在家里一个人跟着做。</p>
<ol>
<li><a href="#how-to-build-the-header">如何建立页眉(header)</a></li>
<li><a href="#how-to-build-the-footer">如何建立页脚(footer)</a></li>
<li><a href="#how-to-build-the-layout">如何建立 layout</a></li>
<li><a href="#how-to-built-the-homepage">如何建立主页(homepage)</a></li>
<li><a href="#how-to-build-the-reading-page">如何建立阅读页(reading page)</a></li>
<li><a href="#how-to-build-the-tag-page">如何建立标签页(tag page)</a></li>
<li><a href="#how-to-build-the-author-page">如何建立作者页(author page)</a></li>
<li><a href="#how-to-build-single-pages">如何建立单页(single pages)</a></li>
<li><a href="#how-to-handle-pagination">如何处理分页(pagination)</a></li>
<li><a href="#next-js-seo">Next.js SEO</a></li>
<li><a href="#how-to-enable-search">如何开启搜索</a></li>
<li><a href="#error-handling">错误处理</a></li>
<li><a href="#how-to-rebuild-your-static-site-with-webhooks">如何用 webhooks 重建你的静态网站</a></li>
</ol>
<h3 id="how-to-build-the-header">如何建立页眉(header)</h3>
<p>网站的第一个也是最主要的部分是页眉(header)。首先，我们将为我们的演示博客创建一个简单的页眉(header)。我们的页眉最终将看起来像这样：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/header.png" alt="页眉的设计" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>页眉的设计</figcaption>
</figure>
<p>首先是 logo，接下来是带有各种元素的导航栏（nav），最后是图标部分（icon）。所有的数据都来自 Ghost CMS 的 API。你可以在 Ghost CMS 里面改变东西，而且会反映在网站上。</p>
<p>下面是建立标题组件的代码：</p>
<pre><code class="language-typescript">// Header.tsx

import Link from 'next/link';
import SocialIcons from './SocialIcons';
import Image from 'next/image';
import type { Settings } from '@tryghost/content-api';

function Header({ setting }: { setting: Settings }) {
  return (
    &lt;header className="px-2 sm:px-4 py-2.5 dark:bg-gray-900 w-full"&gt;
      &lt;div className="container flex flex-wrap items-center justify-between mx-auto"&gt;
        {/* Logo for blog */}
        &lt;Link href="/" className="flex items-center"&gt;
          {setting.logo !== null ? (
            &lt;Image
              alt={setting.title}
              width={200}
              height={100}
              src={setting.logo}
              className="self-center text-xl font-semibold whitespace-nowrap dark:text-white"
            /&gt;
          ) : (
            setting.title
          )}
        &lt;/Link&gt;
        &lt;div className="flex md:order-2"&gt;
          &lt;ul className="flex flex-wrap p-4 md:space-x-8 md:mt-0 md:text-sm md:font-medium"&gt;
            {
              /* Blog Navigation Edit in GHOST CMS  */
              setting.navigation !== undefined
                ? setting?.navigation.map((item) =&gt; (
                    &lt;li
                      key={item.label}
                      className="block py-2 pl-3 pr-4 text-gray-700 rounded hover:text-blue-700 dark:hover:text-blue-700 md:p-0 dark:text-white"
                      aria-current="page"
                    &gt;
                      &lt;Link href={item.url}&gt;{item.label}&lt;/Link&gt;
                    &lt;/li&gt;
                  ))
                : ' '
            }
          &lt;/ul&gt;
        &lt;/div&gt;
        &lt;SocialIcons setting={setting} /&gt;
      &lt;/div&gt;
    &lt;/header&gt;
  );
}
export default Header;
</code></pre>
<h3 id="how-to-build-the-footer">如何建立页脚（Footer）</h3>
<p>页脚(footer)也是博客网站的一个重要部分。它显示你的重要信息和各种有用的链接。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/footer.png" alt="页脚的设计" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>页脚的设计</figcaption>
</figure>
<p>我设计了一个带有版权文本的基本页脚（footer），并为网站添加了社交图标。这些社交图标来自 Ghost CMS 的 API。</p>
<pre><code class="language-typescript">// Footer.tsx

import { FaTwitter, FaFacebook } from 'react-icons/fa';
import Link from 'next/link';
import type { Settings } from '@tryghost/content-api';

function Footer({ setting }: { setting: Settings }) {
  return (
    &lt;footer className="px-2 sm:px-4 py-2.5 dark:bg-gray-900 w-full"&gt;
      &lt;div className="container flex flex-wrap items-center justify-between mx-auto"&gt;
        &lt;Link
          href="https://github.com/frontendweb3"
          className="flex items-center"
        &gt;
          &lt;span className="self-center text-gray-800 text-sm font-semibold whitespace-nowrap dark:text-white"&gt;
            2023 copyright frontend web
          &lt;/span&gt;
        &lt;/Link&gt;

        &lt;div className="flex md:order-2"&gt;
          &lt;ul className="flex p-4 flex-row md:space-x-8 md:mt-0 md:text-sm font-medium"&gt;
            {setting.twitter !== null ? (
              &lt;li&gt;
                &lt;Link
                  target="_blank"
                  href={`https://twitter.com/${setting.twitter}`}
                  className="block py-2 pl-3 pr-4 text-gray-700 rounded hover:text-blue-700 dark:hover:text-blue-700 md:p-0 dark:text-white"
                  aria-current="page"
                &gt;
                  &lt;FaTwitter /&gt;
                &lt;/Link&gt;
              &lt;/li&gt;
            ) : (
              ' '
            )}

            {setting.facebook !== null ? (
              &lt;li&gt;
                &lt;Link
                  target="_blank"
                  href={`https://www.facebook.com/${setting.facebook}`}
                  className="block py-2 pl-3 pr-4 text-gray-700 rounded hover:text-blue-700 dark:hover:text-blue-700 md:p-0 dark:text-white "
                &gt;
                  &lt;FaFacebook /&gt;
                &lt;/Link&gt;
              &lt;/li&gt;
            ) : (
              ' '
            )}
          &lt;/ul&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/footer&gt;
  );
}

export default Footer;
</code></pre>
<h3 id="how-to-build-the-layout">如何建立 layout</h3>
<p>我为博客设计了一个基本的布局（layout）。为了在 Next.js 中构建布局，有一个特殊的<code>layout.tsx</code>文件。</p>
<p>在创建布局(layout)设计之前，我们需要定义一个<code>getNavigation</code> 函数来从 Ghost 中获取导航和基本的网站相关数据。</p>
<pre><code class="language-typescript">// ghost-client.ts

export async function getNavigation() {
  return await api.settings.browse();
}
</code></pre>
<p>Fetch</p>
<h4 id="">这些数据看起来像这样</h4>
<pre><code class="language-object">{
  title: 'Rajdeep Singh',
  description: 'Thoughts, stories and ideas.',
  logo: 'http://localhost:2368/content/images/2023/04/nextjsandghostlogo-2.png',
  icon: 'http://localhost:2368/content/images/size/w256h256/2023/04/nextjs-60pxx60px.png',
  accent_color: '#d27fa0',
  cover_image: 'https://static.ghost.org/v4.0.0/images/publication-cover.jpg',
  facebook: 'ghost',
  twitter: '@ghost',
  lang: 'en',
  locale: 'en',
  timezone: 'Etc/UTC',
  codeinjection_head: null,
  codeinjection_foot: null,
  navigation: Array(5) [
    { label: 'Home', url: '/' }, { label: 'JavaScript', url: '/tags/javascript/' }, { label: 'Nextjs', url: '/tags/nextjs/' },
    { label: 'Reactjs', url: '/tags/reactjs/' }, { label: 'Ghost CMS', url: '/tags/ghost-cms/' }
  ],
  secondary_navigation: Array(1) [ { label: 'Login', url: '#/portal/' } ],
  meta_title: 'My demo post',
  meta_description:
    'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.',
  og_image: null,
  og_title: null,
  og_description:
    'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.',
  twitter_image: null,
  twitter_title: null,
  twitter_description:
    'Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.',
  members_support_address: 'noreply',
  members_enabled: true,
  members_invite_only: false,
  paid_members_enabled: false,
  firstpromoter_account: null,
  portal_button_style: 'icon-and-text',
  portal_button_signup_text: 'Subscribe',
  portal_button_icon: null,
  portal_plans: Array(1) [ 'free' ],
  portal_name: true,
  portal_button: true,
  comments_enabled: 'all',
  url: 'http://localhost:2368/',
  version: '5.39'
}
</code></pre>
<p>api.settings.browse()接收的数据</p>
<p><code>getNavigation</code>函数返回设置数据，然后我们把数据作为 props 传给页眉(header)和页脚(footer)组件。</p>
<p>我们的主文件<code>layout.tsx</code>在服务器端工作。它通过 React <code>use</code> hook 帮助在服务器端获取数据。</p>
<pre><code class="language-typescript">// Layout.tsx

import './globals.css';
import BlogLayout from './BlogLayout';
import { getNavigation } from './ghost-client';
import { use } from 'react';
import type { Settings } from '@tryghost/content-api';

interface UpdateSettings extends Settings {
  accent_color?: string;
}

export default function RootLayout({
  children
}: {
  children: React.ReactNode;
}) {
  const settings: UpdateSettings = use(getNavigation());

  return (
    &lt;html className="light" lang="en"&gt;
      &lt;body
        style={{
          '--bg-color': settings?.accent_color ? settings.accent_color : ''
        }}
        className={` bg-[--bg-color] dark:bg-gray-900`}
      &gt;
        &lt;BlogLayout setting={settings}&gt;{children}&lt;/BlogLayout&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}
</code></pre>
<h4 id="bloglayout">BlogLayout 组件</h4>
<p><code>BlogLayout</code>组件在客户端工作。在 Next.js 应用程序文件夹中，你可以通过以下 <code>use client</code> 的语法轻松地将服务器端的组件转换到客户端。</p>
<p>BlogLayout 组件的目的是包含<a href="https://www.npmjs.com/package/next-themes">ThemeProvider</a>、页眉(header)和页脚(footer)。ThemeProvider 是一个高阶组件，它提供额外的功能，比如将主题从深色改为浅色。我们用 ThemeProvider 的高阶组件来包含网站内的内容。在旧页面目录中，我们用 nextjs 中的 <code>_app.ts</code>自定义应用程序实现类似的功能。</p>
<p>ThemeProvider 组件有助于将主题从浅色变为深色模式。</p>
<pre><code class="language-typescript">'use client';

// BlogLayout.tsx

import Footer from './Footer';
import Header from './Header';
import { ThemeProvider } from 'next-themes';
import type { Settings } from '@tryghost/content-api';
function Layout({
  setting,
  children
}: {
  setting: Settings;
  children: React.ReactNode;
}) {
  return (
    &lt;ThemeProvider attribute="class"&gt;
      &lt;Header setting={setting} /&gt;
      {children}
      &lt;Footer setting={setting} /&gt;
    &lt;/ThemeProvider&gt;
  );
}
export default Layout;
</code></pre>
<p><code>BlogLayout.tsx</code> component</p>
<h3 id="how-to-built-the-homepage">如何建立主页(homepage)</h3>
<p>Next.js 有一个特殊的<code>app/page.tsx</code>文件，用于设计和建立主页(home page)。我们的博客网站的主页看起来就像你下面看到的那样。我们在主页(home page)上导入页眉(header)、卡片(card)、分页(pagination)和页脚(footer)。页眉(header)和页脚(footer)是<code>layout.tsx</code>的一部分。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/Home-page-1.png" alt="Home page（主页）" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>Home page（主页）</figcaption>
</figure>
<p>首先，我们在<code>ghost-client.ts</code>文件中定义的<code>getPosts</code>函数的帮助下，从 Ghost CMS 获取所有帖子数据。</p>
<pre><code class="language-typescript">// ghost-client.ts

export async function getPosts() {
  return await api.posts
    .browse({
      include: ['tags', 'authors'],
      limit: 10
    })
    .catch((err) =&gt; {
      throw new Error(err);
    });
}
</code></pre>
<p>api.post.browse()接收的数据</p>
<p>默认情况下，<code>api.post.browse()</code>只返回文章数据，但你可以轻松地扩展它。在每篇文章或帖子数据中，我们还用<code>include</code>的帮助包括标签和作者。然后，我们将文章限制设置为 10 条。</p>
<h4 id="">数据看起来像这样</h4>
<pre><code class="language-JSON"> [
  {
    id: '6422a742136f5d40f37294f5',
    uuid: '8c2fcfda-a6e4-4383-893b-ba18511c0f67',
    title: 'Demo Posts with Nextjs and Ghost Editor',
    slug: 'demo-posts-with-nextjs-and-reactjs',
    html: `&lt;p&gt;&lt;strong&gt;Lorem Ipsum&lt;/strong&gt; is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text si
nce the 1500s when an unknown printer scrambled a galley of type and scrambled it to make a type specimen book. &lt;/p&gt;&lt;p&gt;It has survived five centuries and the leap i
nto electronic typesetting, remaining essentially unchanged. &lt;/p&gt;&lt;p&gt;It was popularised in the 1960s with Letraset sheets containing Lorem Ipsum passages and, more r
ecently, desktop publishing software like Aldus PageMaker, including versions of Lorem Ipsum.&lt;/p&gt;&lt;figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascap
tion"&gt;&lt;div class="kg-gallery-container"&gt;&lt;div class="kg-gallery-row"&gt;&lt;div class="kg-gallery-image"&gt;&lt;img src="http://localhost:2368/content/images/2023/03/Build-and-d
eploy.png" width="1500" height="400" loading="lazy" alt srcset="http://localhost:2368/content/images/size/w600/2023/03/Build-and-deploy.png 600w, http://localhost:2
368/content/images/size/w1000/2023/03/Build-and-deploy.png 1000w, http://localhost:2368/content/images/2023/03/Build-and-deploy.png 1500w" sizes="(min-width: 720px)
 720px"&gt;&lt;/div&gt;&lt;div class="kg-gallery-image"&gt;&lt;img src="http://localhost:2368/content/images/2023/03/Build-and-deploy-profile-1.png" width="1500" height="400" loading
="lazy" alt srcset="http://localhost:2368/content/images/size/w600/2023/03/Build-and-deploy-profile-1.png 600w, http://localhost:2368/content/images/size/w1000/2023
/03/Build-and-deploy-profile-1.png 1000w, http://localhost:2368/content/images/2023/03/Build-and-deploy-profile-1.png 1500w" sizes="(min-width: 720px) 720px"&gt;&lt;/div&gt;
&lt;/div&gt;&lt;div class="kg-gallery-row"&gt;&lt;div class="kg-gallery-image"&gt;&lt;img src="http://localhost:2368/content/images/2023/03/Build-and-deploy-profile--1--1.png" width="15
00" height="400" loading="lazy" alt srcset="http://localhost:2368/content/images/size/w600/2023/03/Build-and-deploy-profile--1--1.png 600w, http://localhost:2368/co
ntent/images/size/w1000/2023/03/Build-and-deploy-profile--1--1.png 1000w, http://localhost:2368/content/images/2023/03/Build-and-deploy-profile--1--1.png 1500w" siz
es="(min-width: 720px) 720px"&gt;&lt;/div&gt;&lt;div class="kg-gallery-image"&gt;&lt;img src="http://localhost:2368/content/images/2023/03/Build--Test-and-Deploy-profile-1.png" width
="1500" height="400" loading="lazy" alt srcset="http://localhost:2368/content/images/size/w600/2023/03/Build--Test-and-Deploy-profile-1.png 600w, http://localhost:2
368/content/images/size/w1000/2023/03/Build--Test-and-Deploy-profile-1.png 1000w, http://localhost:2368/content/images/2023/03/Build--Test-and-Deploy-profile-1.png
1500w" sizes="(min-width: 720px) 720px"&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;figcaption&gt;Build and deploy&lt;/figcaption&gt;&lt;/figure&gt;&lt;h2 id="why-do-we-use-it"&gt;Why do we use it?&lt;/h2&gt;&lt;p&gt;It is
 a long-established fact that a reader will be distracted by the readable content of a page when looking at its layout. &lt;/p&gt;&lt;p&gt;The point of using Lorem Ipsum is tha
t it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. &lt;/p&gt;&lt;p&gt;Many desktop
publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their
infancy. &lt;/p&gt;&lt;p&gt;Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).&lt;/p&gt;&lt;hr&gt;&lt;h2 id="where-can-i
-get-some"&gt;Where can I get some?&lt;/h2&gt;&lt;p&gt;There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by i
njected humour, or randomised words which don't look even slightly believable. &lt;/p&gt;&lt;p&gt;If you are going to use a passage of Lorem Ipsum, you need to be sure there is
n't anything embarrassing hidden in the middle of text. &lt;/p&gt;&lt;p&gt;All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making
this the first true generator on the Internet. &lt;/p&gt;&lt;p&gt;It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generat
e Lorem Ipsum which looks reasonable. &lt;/p&gt;&lt;p&gt;The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.&lt;/
p&gt;&lt;div class="kg-card kg-callout-card kg-callout-card-red"&gt;&lt;div class="kg-callout-emoji"&gt;💡&lt;/div&gt;&lt;div class="kg-callout-text"&gt;My note is here&amp;nbsp;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;&lt;/
p&gt;&lt;div class="kg-card kg-header-card kg-width-full kg-size-small kg-style-dark" style data-kg-background-image&gt;&lt;h2 class="kg-header-card-header" id="product"&gt;Produc
t&lt;/h2&gt;&lt;h3 class="kg-header-card-subheader" id="my-blog-list"&gt;My blog list&lt;/h3&gt;&lt;/div&gt;&lt;p&gt;&lt;/p&gt;&lt;figure class="kg-card kg-embed-card kg-card-hascaption"&gt;&lt;iframe width="2
00" height="113" src="https://www.youtube.com/embed/_q1K7cybyRk?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gy
roscope; picture-in-picture; web-share" allowfullscreen title="Next.js 13.1 Explained!"&gt;&lt;/iframe&gt;&lt;figcaption&gt;youtube&lt;/figcaption&gt;&lt;/figure&gt;&lt;hr&gt;&lt;figure class="kg-card
 kg-embed-card"&gt;&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;In 2022, we enabled developers to create at the moment of inspiration, now with over 2 mill
ion deployments per week.&lt;br&gt;&lt;br&gt;Here&amp;#39;s what we shipped ↓ &lt;a href="https://t.co/6k7Xmbpna3?ref=localhost"&gt;pic.twitter.com/6k7Xmbpna3&lt;/a&gt;&lt;/p&gt;&amp;mdash; Vercel (@ver
cel) &lt;a href="https://twitter.com/vercel/status/1611094825587167254?ref_src=twsrc%5Etfw&amp;ref=localhost"&gt;January 5, 2023&lt;/a&gt;&lt;/blockquote&gt;\n` +
      '&lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;\n' +
      '&lt;/figure&gt;&lt;hr&gt;&lt;figure class="kg-card kg-bookmark-card kg-card-hascaption"&gt;&lt;a class="kg-bookmark-container" href="https://medium.com/frontendweb/what-is-progre
ssive-web-app-and-how-to-enable-it-in-nextjs-application-17f2e3240390?ref=localhost"&gt;&lt;div class="kg-bookmark-content"&gt;&lt;div class="kg-bookmark-title"&gt;What is Progres
sive Web App and How to enable it in nextjs Application?&lt;/div&gt;&lt;div class="kg-bookmark-description"&gt;A detailed guide to Progressive Web Apps: How to use it with next
js and publish on Google play store, Microsoft store, Meta Quest, and…&lt;/div&gt;&lt;div class="kg-bookmark-metadata"&gt;&lt;img class="kg-bookmark-icon" src="https://cdn-static-
1.medium.com/_/fp/icons/Medium-Avatar-500x500.svg" alt&gt;&lt;span class="kg-bookmark-author"&gt;FrontEnd web&lt;/span&gt;&lt;span class="kg-bookmark-publisher"&gt;Rajdeep singh&lt;/span&gt;&lt;
/div&gt;&lt;/div&gt;&lt;div class="kg-bookmark-thumbnail"&gt;&lt;img src="https://miro.medium.com/v2/resize:fit:1200/1*yAoHfq4Wm2Bp8DU1Dav29Q.png" alt&gt;&lt;/div&gt;&lt;/a&gt;&lt;figcaption&gt;Bookmark&lt;
/figcaption&gt;&lt;/figure&gt;&lt;div class="kg-card kg-header-card kg-width-full kg-size-small kg-style-dark" style data-kg-background-image&gt;&lt;h2 class="kg-header-card-header"
id="thank-you"&gt;Thank you&lt;/h2&gt;&lt;/div&gt;',
    comment_id: '6422a742136f5d40f37294f5',
    feature_image: 'https://images.unsplash.com/photo-1543966888-7c1dc482a810?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDE2fHxqYXZhc2Nya
XB0fGVufDB8fHx8MTY3OTk5MjY1NA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000',
    featured: false,
    visibility: 'public',
    created_at: '2023-03-28T08:37:22.000+00:00',
    updated_at: '2023-03-28T08:51:38.000+00:00',
    published_at: '2023-03-28T08:50:44.000+00:00',
    custom_excerpt: 'It has survived five centuries and the leap into electronic typesetting, remaining essentially unchanged. ',
    codeinjection_head: null,
    codeinjection_foot: null,
    custom_template: null,
    canonical_url: null,
    tags: [ [Object] ],
    authors: [ [Object] ],
    primary_author: {
      id: '1',
      name: 'Rajdeep Singh',
      slug: 'rajdeep',
      profile_image: 'https://www.gravatar.com/avatar/dafca7497609ae294378279ad1d6136c?s=250&amp;r=x&amp;d=mp',
      cover_image: null,
      bio: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry. ',
      website: 'https://officialrajdeepsingh.dev',
      location: 'India',
      facebook: 'officialrajdeepsingh',
      twitter: '@Official_R_deep',
      meta_title: null,
      meta_description: null,
      url: 'http://localhost:2368/author/rajdeep/'
    },
    primary_tag: {
      id: '6422aa9a136f5d40f3729552',
      name: 'demo',
      slug: 'demo',
      description: null,
      feature_image: null,
      visibility: 'public',
      og_image: null,
      og_title: null,
      og_description: null,
      twitter_image: null,
      twitter_title: null,
      twitter_description: null,
      meta_title: null,
      meta_description: null,
      codeinjection_head: null,
      codeinjection_foot: null,
      canonical_url: null,
      accent_color: null,
      url: 'http://localhost:2368/tag/demo/'
    },
    url: 'http://localhost:2368/demo-posts-with-nextjs-and-reactjs/',
    excerpt: 'It has survived five centuries and the leap into electronic typesetting, remaining essentially unchanged. ',
    reading_time: 3,
    access: true,
    comments: true,
    og_image: null,
    og_title: null,
    og_description: null,
    twitter_image: null,
    twitter_title: null,
    twitter_description: null,
    meta_title: null,
    meta_description: null,
    email_subject: null,
    frontmatter: null,
    feature_image_alt: 'Demo Posts with Nextjs and Ghost Editor',
    feature_image_caption: 'Photo by &lt;a href="https://unsplash.com/@pinjasaur?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit"&gt;Paul Esch-Laurent&lt;/a&gt; /
&lt;a href="https://unsplash.com/?utm_source=ghost&amp;utm_medium=referral&amp;utm_campaign=api-credit"&gt;Unsplash&lt;/a&gt;'
  },
meta:{
    pagination: { page: 1, limit: 10, pages: 2, total: 12, next: 2, prev: null }
  }
]
</code></pre>
<p>由<code>api.post.browse()</code>接收的数据</p>
<p>现在我们在服务器端调用<code>getPosts</code>函数。它返回所有的帖子数据以及相关的标签和作者。现在你可以用<code>map()</code>函数循环浏览这些数据。</p>
<p>我们将数据传入<code>app/page.tsx</code>到<code>card.tsx</code>组件。我们把文章数据作为 prop 传给卡片组件。</p>
<pre><code class="language-typescript">// src/app/page.tsx

import { getPosts } from './ghost-client';
import Card from './Card';

export default async function Home() {
  const getPost = await getPosts();

  return (
    &lt;&gt;
      &lt;main className="container my-12 mx-auto grid grid-cols-1 gap-2 md:gap-3 lg:gap-4 lg:grid-cols-3 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-4"&gt;
        {getPost?.map((item) =&gt; {
          return &lt;Card key={item.uuid} item={item} /&gt;;
        })}
      &lt;/main&gt;
    &lt;/&gt;
  );
}
</code></pre>
<p>Design home <code>/app/page.tsx</code></p>
<h4 id="card">Card 组件</h4>
<p>我为博客设计了一张基本的卡片。卡片组件看起来像这样：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/card.png" alt="卡片组件" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>卡片组件</figcaption>
</figure>
<p>我把来自主页的每项数据都渲染成 prop，并用<code>Card.tsx</code>在网站上显示。</p>
<pre><code class="language-typescript">// Card.tsx

import Image from 'next/image';
import Link from 'next/link';
import type { PostOrPage } from '@tryghost/content-api';
import { format } from 'date-fns';

function Card({ item }: { item: PostOrPage }) {
  return (
    &lt;div className="max-w-full bg-white dark:bg-gray-800"&gt;
      {item.featured !== null &amp;&amp; item.feature_image !== undefined ? (
        &lt;Link href={`/read/${item.slug}`}&gt;
          &lt;Image
            className="rounded-lg p-3"
            width={1000}
            height={324}
            src={item.feature_image}
            alt={item.feature_image_alt || item.title}
          /&gt;
        &lt;/Link&gt;
      ) : (
        ' '
      )}

      &lt;div className="p-3"&gt;
        &lt;div className="flex mb-3"&gt;
          {item.published_at !== null &amp;&amp; item.published_at !== undefined ? (
            &lt;p className="text-sm text-gray-500 dark:text-gray-400"&gt;
              {format(new Date(item.published_at), 'dd MMMM, yyyy')}
            &lt;/p&gt;
          ) : (
            ''
          )}
          &lt;p className="text-sm text-gray-500 dark:text-gray-400 mx-1"&gt; , &lt;/p&gt;
          &lt;p className="text-sm text-gray-500 dark:text-gray-400"&gt;
            {item.reading_time} min read
          &lt;/p&gt;
        &lt;/div&gt;

        &lt;Link href={`/read/${item.slug}`}&gt;
          &lt;h5 className="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white"&gt;
            {item.title}
          &lt;/h5&gt;
        &lt;/Link&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}

export default Card;
</code></pre>
<p>-</p>
<h3 id="how-to-build-the-reading-page">如何建立阅读页(reading page)</h3>
<p>阅读页面(reading page)是博客网站的第二大重要页面。如果人们不能弄清楚如何阅读作者写的东西，这对前端开发者来说是个大问题。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/ghostandnext-reading.png" alt="阅读页" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>阅读页</figcaption>
</figure>
<p>首先，我们从 Ghost CMS 的 API 中获得一篇基于其 slug(一种模板) 的文章。我们用 <code>链接(Link)</code> 组件把它传递给 <code>卡片(Card)</code>组件。</p>
<pre><code class="language-typescript">// ghost-client.ts

export async function getSinglePost(postSlug: string) {
  return await api.posts
    .read(
      {
        slug: postSlug
      },
      { include: ['tags', 'authors'] }
    )
    .catch((err) =&gt; {
      console.error(err);
    });
}
</code></pre>
<p>检索基于 slug 的单个帖子。</p>
<p><code>getSinglePost(&lt;you-slug&gt;)</code>函数返回单篇文章的数据，你可以在页面上渲染这些数据。</p>
<pre><code class="language-typescript">// src/app/read/[slug]/page.tsx

import Newsletter from './Newsletter';
import Link from 'next/link';
import { getSinglePost, getPosts } from '../../ghost-client';
import Image from 'next/image';
// import icon
import { FaAngleLeft } from 'react-icons/fa';

// types for typescript
import type { Metadata } from 'next';
import type { PostOrPage } from '@tryghost/content-api';

// format the date
import { format } from 'date-fns';

// css for card
import '../../cards.min.css';

export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) =&gt; ({
    slug: post.slug
  }));
}

async function Read({ params }: { params: { slug: string } }) {
  const getPost = await getSinglePost(params.slug);

  return (
    &lt;&gt;
      &lt;main className="pt-8 pb-16 lg:pt-16 lg:pb-24 dark:bg-gray-900"&gt;
        &lt;div className="flex justify-between px-4 mx-auto max-w-screen-xl "&gt;
          &lt;article className="mx-auto w-full max-w-3xl prose prose-xl prose-p:text-gray-800  dark:prose-p:text-gray-100 sm:prose-base prose-a:no-underline prose-blue dark:prose-invert"&gt;
            &lt;div className="flex mb-4 w-full justify-between"&gt;
              &lt;Link className="inline-flex items-center" href={`/`}&gt;
                &lt;FaAngleLeft /&gt; Back
              &lt;/Link&gt;

              {getPost.primary_tag ? (
                &lt;Link href={`/tags/${getPost?.primary_tag.slug}`}&gt;
                  # {getPost?.primary_tag.name}
                &lt;/Link&gt;
              ) : (
                ''
              )}
            &lt;/div&gt;

            &lt;h1 className="mb-4 text-3xl font-extrabold leading-tight text-gray-900 lg:mb-6 lg:text-4xl dark:text-white"&gt;
              {getPost.title}
            &lt;/h1&gt;

            &lt;p className="lead"&gt;{getPost.excerpt}&lt;/p&gt;

            &lt;header className="mb-4 lg:mb-6 not-format"&gt;
              &lt;address className="flex items-center mb-6 not-italic"&gt;
                &lt;div className="inline-flex items-center mr-3 text-sm text-gray-900 dark:text-white"&gt;
                  &lt;Image
                    width={32}
                    height={32}
                    className="mr-4 w-10 h-10 rounded-full"
                    src={getPost?.primary_author.profile_image}
                    alt={getPost?.primary_author.name}
                  /&gt;
                  {getPost.primary_author ? (
                    &lt;Link
                      href={`/authors/${getPost?.primary_author.slug}`}
                      rel="author"
                      className="text-xl font-bold text-gray-800 dark:text-white"
                    &gt;
                      {getPost?.primary_author.name}
                    &lt;/Link&gt;
                  ) : (
                    ' '
                  )}

                  {getPost.published_at ? (
                    &lt;time
                      className="text-base font-light text-gray-800 dark:text-white mx-1"
                      dateTime={getPost?.published_at}
                      title={format(
                        new Date(getPost?.published_at),
                        'yyyy-MM-dd'
                      )}
                    &gt;
                      {format(new Date(getPost?.published_at), 'dd MMMM, yyyy')}
                    &lt;/time&gt;
                  ) : (
                    ''
                  )}

                  &lt;div className="text-base w-1 h-1 rounded-full bg-black dark:bg-white mx-1"&gt;&lt;/div&gt;

                  &lt;p className="text-base font-light text-gray-500 dark:text-gray-400"&gt;
                    {' '}
                    {getPost.reading_time} Min Read
                  &lt;/p&gt;
                &lt;/div&gt;
              &lt;/address&gt;
            &lt;/header&gt;

            &lt;figure&gt;
              &lt;Image
                className="mx-auto"
                width={1000}
                height={250}
                src={getPost.feature_image}
                alt={getPost.feature_image_alt}
              /&gt;
              &lt;figcaption
                className="text-center"
                dangerouslySetInnerHTML={{
                  __html: getPost?.feature_image_caption
                }}
              &gt;&lt;/figcaption&gt;
            &lt;/figure&gt;

            &lt;div dangerouslySetInnerHTML={{ __html: getPost?.html }}&gt;&lt;/div&gt;
          &lt;/article&gt;
        &lt;/div&gt;
      &lt;/main&gt;
      &lt;Newsletter /&gt;
    &lt;/&gt;
  );
}
export default Read;
</code></pre>
<p>你用<code>dangerouslySetInnerHTML</code>渲染帖子的 HTML 数据。但是你需要写很多 CSS 来处理来自 Ghost CMS API 的动态内容。</p>
<p>为了解决这个问题，我使用了<code>@tailwindcss/typography</code>包。我还从 Ghost 下载了<code>cards.min.css</code>。现在你不需要在你的 Next 应用程序中写一行 CSS 了。</p>
<p>用<code>generateStaticParams</code>函数生成静态网站。之前，我们使用<code>getStaticProps</code>函数。</p>
<pre><code class="language-typescript">// ghost-client.ts

export async function generateStaticParams() {
  // fetch All posts

  const posts = await getPosts();

  // return the slug

  return posts.map((post) =&gt; ({
    slug: post.slug
  }));
}
</code></pre>
<p>为文章阅读页面(reading page)生成静态网站 slug</p>
<h3 id="how-to-build-the-reading-page">如何建立阅读页(reading page)</h3>
<p>我为博客设计了一个简单的标签页(Tag Page)。标签页显示与所使用的标签(tags)有关的文章。</p>
<p>你也可以创建一个分类页(category)。标签页(Tag pages)和分类页(category pages)使用相同的逻辑和功能。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/ghostandnextjs-tag.png" alt="标签页" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>标签页</figcaption>
</figure>
<p>与阅读页(reading page)类似，我们将根据 Ghost CMS API 的标签来获取文章。</p>
<pre><code class="language-typescript">// ghost-client.ts

// return all posts realted to tag slug
export async function getTagPosts(tagSlug: string) {
  return await api.posts
    .browse({ filter: `tag:${tagSlug}`, include: 'count.posts' })
    .catch((err) =&gt; {
      throw new Error(err);
    });
}

// return all the slugs to build static with generateStaticParams
export async function getAllTags() {
  return await api.tags
    .browse({
      limit: 'all'
    })
    .catch((err) =&gt; {
      console.log(err);
    });
}
</code></pre>
<p><code>getTagPosts(&lt;tag-slug&gt;)</code>函数返回所有与特定标签相关的可用帖子。</p>
<p>在用<code>getTagPosts()</code>接收所有帖子后，我们用<code>map()</code>方法渲染所有帖子。</p>
<pre><code class="language-typescript">// src/app/tag/[slug]/page.tsx

import React from 'react';
import Card from '../../Card';

import { getTagPosts, getAllTags } from '../../ghost-client';

import { notFound } from 'next/navigation';

import type { PostsOrPages } from '@tryghost/content-api';

export async function generateStaticParams() {
  const allTags: Tags = await getAllTags();

  let allTagsItem: { slug: string }[] = [];

  // genrate the slug for static site

  allTags?.map((item) =&gt; {
    allTagsItem.push({
      slug: item.slug
    });
  });

  return allTagsItem;
}

async function Tag({ params }: { params: { slug: string } }) {
  let tagPosts: PostsOrPages = await getTagPosts(params.slug);

  // Handling 404 error

  if (tagPosts.length === 0) {
    notFound();
  }

  return (
    &lt;aside
      aria-label="Related articles"
      className="py-8 lg:py-24 dark:bg-gray-800"
    &gt;
      &lt;div className="px-4 mx-auto max-w-screen-xl"&gt;
        &lt;h2 className="mb-8 text-2xl font-bold text-gray-900 dark:text-white"&gt;
          More articles from {params.slug.split('-').join(' ')}
        &lt;/h2&gt;

        &lt;div className="container my-12 mx-auto grid grid-cols-1 gap-12 md:gap-12 lg:gap-12  lg:grid-cols-3  md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-4 "&gt;
          {tagPosts.map((item) =&gt; (
            &lt;Card key={item.uuid} item={item} /&gt;
          ))}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/aside&gt;
  );
}

export default Tag;
</code></pre>
<p>用<code>generateStaticParams</code>函数生成静态网站。它有助于生成静态构建的 slug。</p>
<pre><code class="language-typescript">// ghost-client.ts

export async function getAllTags() {
  return await api.tags
    .browse({
      limit: 'all'
    })
    .catch((err) =&gt; {
      console.log(err);
    });
}
</code></pre>
<p>为标签页生成静态网站 slug</p>
<h3 id="how-to-build-the-author-page">如何建立作者页(author page)</h3>
<p>博客网站的最后一个也是最重要的一个页面是作者页。在这里，读者可以了解更多关于作者的信息。</p>
<p>对于这个演示博客，我为作者设计了一个基本页面。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/nextandghostauthor.png" alt="作者页" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>作者页</figcaption>
</figure>
<p>我们将以类似于建立标签页的方式来建立这个页面。首先，我们从 Ghost CMS 的 API 中获取作者的元数据和作者的帖子。</p>
<pre><code class="language-typescript">// ghost-client.ts

// get author meta Data

export async function getSingleAuthor(authorSlug: string) {
  return await api.authors
    .read(
      {
        slug: authorSlug
      },
      { include: ['count.posts'] }
    )
    .catch((err) =&gt; {
      console.log(err);
    });
}

// get author related posts

export async function getSingleAuthorPosts(authorSlug: string) {
  return await api.posts
    .browse({ filter: `authors:${authorSlug}` })
    .catch((err) =&gt; {
      console.log(err);
    });
}

// get All author from Ghost CMS for generateStaticParams

export async function getAllAuthors() {
  return await api.authors
    .browse({
      limit: 'all'
    })
    .catch((err) =&gt; {
      throw new Error(err);
    });
}
</code></pre>
<p><code>getSingleAuthor(&lt;author-slug&gt;)</code>根据作者的名字返回单个作者的数据，<code>getSingleAuthorPosts(&lt;author-slug&gt;)</code>函数返回与作者有关的所有帖子。</p>
<p>我们在<code>map()</code>方法的帮助下渲染帖子数据。</p>
<pre><code class="language-typescript">// src/app/author/[slug]/page.tsx

import React from 'react';
import Link from 'next/link';
import { FaFacebook, FaTwitter, FaGlobe } from 'react-icons/fa';
import Card from '../../Card';

import {
  getSingleAuthor,
  getSingleAuthorPost,
  getAllAuthors
} from '../../ghost-client';

import Image from 'next/image';
import { notFound } from 'next/navigation';

import type { Author, PostsOrPages } from '@tryghost/content-api';

export async function generateStaticParams() {
  const allAuthor: Author[] = await getAllAuthors();

  let allAuthorItem: { slug: string }[] = [];

  allAuthor.map((item) =&gt; {
    allAuthorItem.push({
      slug: item.slug
    });
  });
  return allAuthorItem;
}

async function AuthorPage({ params }: { params: { slug: string } }) {
  const getAuthor: Author = await getSingleAuthor(params.slug);

  const allAuthor: PostsOrPages = await getSingleAuthorPost(params.slug);

  // Handling 404 errors
  if (allAuthor?.length === 0) {
    notFound();
  }

  return (
    &lt;&gt;
      &lt;section className="dark:bg-gray-900"&gt;
        &lt;div className="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6"&gt;
          &lt;div className=" p-10 text-gray-500 sm:text-lg dark:text-gray-400"&gt;
            {getAuthor?.profile_image !== undefined ? (
              &lt;Image
                height={30}
                width={30}
                className="w-36 h-36 p-2 rounded-full mx-auto ring-2 ring-gray-300 dark:ring-gray-500"
                src={getAuthor?.profile_image}
                alt={getAuthor?.name}
              /&gt;
            ) : (
              ''
            )}

            {getAuthor?.name ? (
              &lt;h2 className="mb-4 mt-4 text-4xl tracking-tight font-bold text-center text-gray-900 dark:text-white"&gt;
                {getAuthor?.name.split(' ')[0]}
                &lt;span className="font-extrabold"&gt;
                  {getAuthor?.name?.split(' ')[1]}
                &lt;/span&gt;
              &lt;/h2&gt;
            ) : (
              ''
            )}

            &lt;p className="mb-4 font-light text-center"&gt;{getAuthor?.bio} &lt;/p&gt;

            &lt;ul className="flex flex-wrap p-4 justify-center md:space-x-8 md:mt-0 md:text-sm md:font-medium"&gt;
              {getAuthor?.website !== null ? (
                &lt;li&gt;
                  &lt;Link
                    href={getAuthor?.website}
                    className="block py-2 pl-3 pr-4 text-gray-700 hover:text-blue-700 dark:hover:text-blue-700 rounded md:p-0 dark:text-white"
                    aria-current="page"
                  &gt;
                    &lt;FaGlobe /&gt;
                  &lt;/Link&gt;{' '}
                &lt;/li&gt;
              ) : (
                ' '
              )}

              {getAuthor?.twitter !== null ? (
                &lt;li&gt;
                  &lt;Link
                    href={getAuthor?.twitter}
                    className="block py-2 pl-3 pr-4 text-gray-700 rounded hover:text-blue-700 dark:hover:text-blue-700 md:p-0 dark:text-white"
                    aria-current="page"
                  &gt;
                    &lt;FaTwitter /&gt;
                  &lt;/Link&gt;
                &lt;/li&gt;
              ) : (
                ' '
              )}

              {getAuthor?.facebook !== null &amp;&amp;
              getAuthor.facebook !== undefined ? (
                &lt;li&gt;
                  &lt;Link
                    href={getAuthor?.facebook}
                    className="block py-2 pl-3 pr-4 text-gray-700 rounded  hover:text-blue-700 dark:hover:text-blue-700 md:p-0 dark:text-white"
                  &gt;
                    {' '}
                    &lt;FaFacebook /&gt;
                  &lt;/Link&gt;
                &lt;/li&gt;
              ) : (
                ' '
              )}
            &lt;/ul&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/section&gt;

      &lt;aside
        aria-label="Related articles"
        className="py-8 lg:py-24 dark:bg-gray-800"
      &gt;
        &lt;div className="px-4 mx-auto max-w-screen-xl"&gt;
          &lt;h2 className="mb-8 text-2xl font-bold text-gray-900 dark:text-white"&gt;
            More articles from {getAuthor?.name}
          &lt;/h2&gt;

          &lt;div className="container my-12 mx-auto grid grid-cols-1 gap-12 md:gap-12 lg:gap-12  lg:grid-cols-3  md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-4 "&gt;
            {allAuthor?.map((item) =&gt; (
              &lt;Card key={item?.uuid} item={item} /&gt;
            ))}
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/aside&gt;
    &lt;/&gt;
  );
}
export default AuthorPage;
</code></pre>
<p>为了生成静态网站的作者 slug，我们需要使用<code>generateStaticParams</code>函数。我们不需要其他东西来建立静态网站。</p>
<pre><code class="language-typescript">// ghost-client.ts

// Build Static Site

export async function generateStaticParams() {
  const allAuthor: Author[] = await getAllAuthors();

  let allAuthorItem: { slug: string }[] = [];

  allAuthor.map((item) =&gt; {
    allAuthorItem.push({
      slug: item.slug
    });
  });
  return allAuthorItem;
}
</code></pre>
<h3 id="how-to-build-the-author-page">如何建立作者页(author page)</h3>
<p>对于像 <code>关于(About)</code>、<code>联系(Contact)</code>、<code>隐私政策(Privacy Policy)</code> 等单页(single page)，你也可以用 Ghost Content API 创建它们。</p>
<p>我们的单页设计看起来像这样：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/single-blog.png" alt="博客页" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>博客页</figcaption>
</figure>
<p>首先，你需要从 Ghost Content API 中获取所有页面和单页的数据。</p>
<pre><code class="language-typescript">// ghost-client.tsx

// fetch all pages

export async function getSinglePage(pageSlug: string) {
  return await api.pages
    .read({
      slug: pageSlug
    })
    .catch((err) =&gt; {
      console.error(err);
    });
}

// single page data

export async function getSinglePage(pageSlug: string) {
  return await api.pages
    .read(
      {
        slug: pageSlug
      },
      { include: ['tags'] }
    )
    .catch((err) =&gt; {
      console.error(err);
    });
}
</code></pre>
<p><code>getSinglePage(page-slug)</code>函数返回基于 slug 页面的单一页面数据，<code>getAllPages()</code>函数返回所有可用的已发布页面数据，以便用<code>generateStaticParams()</code>函数生成动态参数。</p>
<pre><code class="language-typescript">// src/app/pages/[slug]/page.tsx

import { getSinglePage, getAllPages } from '../../ghost-client';
import { notFound } from 'next/navigation';
import type { PostOrPage } from '@tryghost/content-api';
import '../../cards.min.css';

// genrate Static slug or params for blog

export async function generateStaticParams() {
  const pages = await getAllPages();

  return pages.map((post) =&gt; ({
    slug: post.slug
  }));
}

async function Pages({ params }: { params: { slug: string } }) {
  // fetch single page
  const getPage = await getSinglePage(params.slug);

  // handle 404 error
  if (!getPage) {
    notFound();
  }

  return (
    &lt;&gt;
      &lt;main className="pt-8 pb-16 lg:pt-16 lg:pb-24 dark:bg-gray-900"&gt;
        &lt;div className="flex justify-between px-4 mx-auto max-w-screen-xl "&gt;
          &lt;article className="mx-auto w-full max-w-3xl prose prose-xl prose-p:text-gray-800  dark:prose-p:text-gray-100 sm:prose-base prose-a:no-underline prose-blue dark:prose-invert"&gt;
            &lt;h1 className="mb-14 text-3xl font-extrabold leading-tight text-gray-900 lg:mb-6 lg:text-4xl dark:text-white"&gt;
              {getPage.title}
            &lt;/h1&gt;

            &lt;div dangerouslySetInnerHTML={{ __html: getPage?.html }}&gt;&lt;/div&gt;
          &lt;/article&gt;
        &lt;/div&gt;
      &lt;/main&gt;
    &lt;/&gt;
  );
}
export default Pages;
</code></pre>
<h3 id="how-to-handle-pagination">如何处理分页（pagination）</h3>
<p>分页(Pagination )有助于加快你的网站访问速度，并将你的网站分成更小的部分，更容易消化的页面。你可以用 <code>prev</code>和 <code>next</code> 将你的文章相互连接起来。</p>
<pre><code class="language-json">meta:{
    pagination: { page: 1, limit: 10, pages: 2, total: 12, next: 2, prev: null }
 }
</code></pre>
<p><code>next</code> 跳转到下一个页面，<code>prev</code> 跳转到上一个页面</p>
<p>首先，我们将创建一个<code>Pagination.tsx</code>文件作为 React 组件。</p>
<pre><code class="language-typescript">// Pagination.tsx

import Link from 'next/link';
import { Pagination } from '@tryghost/content-api';

function PaginationItem({ item }: { item: Pagination }) {
  let paginationItems = [];

  for (let index = 1; index &lt;= item?.pages; index++) {
    paginationItems.push(
      &lt;li key={index * 2}&gt;
        &lt;Link
          href={index === 1 ? '/' : `/pagination/${index}`}
          className="px-3 py-2 leading-tight bg-blue-100 hover:bg-blue-200 border-transparent border rounded-lg text-black dark:bg-gray-800 dark:text-gray-400 mx-2 dark:hover:bg-gray-700 dark:hover:text-white"
        &gt;
          {index}
        &lt;/Link&gt;
      &lt;/li&gt;
    );
  }

  return (
    &lt;nav aria-label="pagination" className="mx-auto my-20 container"&gt;
      &lt;ul className="mx-auto flex justify-center -space-x-px"&gt;
        &lt;li&gt;
          {item.prev ? (
            &lt;Link
              href={item.prev === 1 ? '/' : `/pagination/${item.prev}`}
              className="px-3 py-2 mr-2 border border-transparent rounded-md  leading-tight bg-white hover:text-blue-700 dark:bg-gray-800 dark:text-gray-400
              dark:hover:bg-gray-700 dark:hover:text-white"
            &gt;
              Prev
            &lt;/Link&gt;
          ) : (
            ' '
          )}
        &lt;/li&gt;

        {paginationItems}

        &lt;li&gt;
          {item.next ? (
            &lt;Link
              href={`/pagination/${item.next}`}
              className="px-3 py-2 ml-2 border border-transparent rounded-md leading-tight bg-white hover:text-blue-700 dark:bg-gray-800 dark:text-gray-400
            dark:hover:bg-gray-700 dark:hover:text-white"
            &gt;
              Next
            &lt;/Link&gt;
          ) : (
            ' '
          )}
        &lt;/li&gt;
      &lt;/ul&gt;
    &lt;/nav&gt;
  );
}

export default PaginationItem;
</code></pre>
<p>当你调用<code>api.post.browse({ limit: 10 })</code>请求时，API 端点会返回十个帖子和一个带有<code>pagination</code>的<code>meta</code>对象。</p>
<h4 id="apipostbrowselimit10"><code>api.post.browse({ limit: 10 })</code>返回的数据看起来像这样</h4>
<pre><code class="language-json"> [
  {title: 'Demo Posts with Nextjs and Ghost Editor',... },
  {title: Trigger the hook and rebuild the nextjs site',... }

meta:{
    pagination: { page: 1, limit: 10, pages: 2, total: 12, next: 2, prev: null }
  }
]
</code></pre>
<p><code>api.posts.browse({ limit: 10 })</code></p>
<p>现在基于<code>meta</code>，我们可以创建分页，并将<code>meta.pagination</code>作为 prop 传递给<code>Pagination</code>组件。</p>
<pre><code class="language-typescript">// src/app/page.tsx

import { getPosts } from './ghost-client';
import Pagination from './Pagination';

export default async function Home() {
  const getPost = await getPosts();

  const AllPostForSerach = await getSearchPosts();

  return (
    &lt;&gt;
      {/* rest of code  */}
      &lt;Pagination item={getPost.meta.pagination} /&gt;
    &lt;/&gt;
  );
}
</code></pre>
<p>为了启用动态分页(dynamic pagination)，我们将在博客中创建一个<code>src/app/pagination/[item]/page.tsx</code>路由。你可以为分页路由(pagination route)使用任何你想要的名字。</p>
<pre><code class="language-typescript">// ghost-client.tsx

// return all posts for generateStaticParams

export async function getPosts() {
  return await api.posts
    .browse({
      include: ['tags', 'authors'],
      limit: 10
    })
    .catch((err) =&gt; {
      throw new Error(err);
    });
}

//
export async function getPaginationPosts(page: number) {
  return await api.posts
    .browse({
      include: ['tags', 'authors'],
      limit: 10,
      page: page
    })
    .catch((err) =&gt; {
      throw new Error(err);
    });
}
</code></pre>
<p><code>getPosts</code>是用来渲染分页上的<code>Pagination</code>组件。重要的部分是<code>getPaginationPosts(&lt;pagination-page-number&gt;)</code>函数，它根据分页的页码返回帖子。</p>
<pre><code class="language-typescript">// src/app/pagination/[item]/page.tsx

import { getPaginationPosts, getPosts } from '../../ghost-client';
import Card from '../../Card';
import PaginationItem from '../../Pagination';
import type { Metadata } from 'next';
import type { PostsOrPages } from '@tryghost/content-api';

export async function generateStaticParams() {
  const posts: PostsOrPages = await getPosts();

  let paginationItem: { item: number }[] = [];

  for (let index = 1; index &lt;= posts?.meta.pagination.pages; index++) {
    paginationItem.push({
      item: index
    });
  }

  return paginationItem;
}

export default async function Pagination({
  params
}: {
  params: { item: string };
}) {
  let getParams: number = Number.parseInt(params.item);

  const getPost: PostsOrPages = await getPaginationPosts(getParams);

  return (
    &lt;&gt;
      &lt;main className="container my-12 mx-auto grid grid-cols-1 gap-2 md:gap-3 lg:gap-4 lg:grid-cols-3 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-4"&gt;
        {getPost?.map((item) =&gt; {
          return &lt;Card key={item.uuid} item={item} /&gt;;
        })}
      &lt;/main&gt;

      &lt;PaginationItem item={getPost.meta.pagination} /&gt;
    &lt;/&gt;
  );
}
</code></pre>
<p>use</p>
<h3 id="next-js-seo">Next.js SEO</h3>
<p>如果你是一个博主，你知道 SEO 在帮助人们找到你的博客和你的文章方面是多么重要。对于 SEO，Next.js 提供了一个<code>generateMetadata</code>功能，为你的网站生成动态 SEO 元数据。这意味着你不需要任何额外的包来进行 SEO。</p>
<p>在这个例子中，我将解释如何为博客只在主页和阅读页上启用 SEO。你可以使用同样的逻辑在你的任何其他页面上启用它。</p>
<p>首先，让我们看看如何在主页上启用 SEO：</p>
<pre><code class="language-typescript">// ghost-client.ts

// Get you settings meta data from Ghost CMS
export async function getNavigation() {
  return await api.settings.browse();
}
</code></pre>
<pre><code class="language-typescript">// src/app/page.tsx

import { getNavigation } from './ghost-client';

export async function generateMetadata(): Promise&lt;Metadata&gt; {
  const Metadata = await getNavigation();
  return {
    title: Metadata.title,
    description: Metadata.description,
    keywords: ['Next.js', 'React', 'JavaScript']
  };
}
</code></pre>
<p>现在我们来看看如何在阅读页(reading page)上启用 SEO:</p>
<pre><code class="language-typescript">// ghost-client.ts

export async function getSinglePost(postSlug: string) {
  return await api.posts
    .read(
      {
        slug: postSlug
      },
      { include: ['tags', 'authors'] }
    )
    .catch((err) =&gt; {
      console.error(err);
    });
}
</code></pre>
<p><code>generateMetadata</code>有 params prop，可以帮助访问 slug。然后，基于 slug，我们获得数据并返回。</p>
<pre><code class="language-typescript">export async function generateMetadata({
  params
}: {
  params: { slug: string };
}): Promise&lt;Metadata&gt; {
  const metaData: PostOrPage = await getSinglePost(params.slug);

  let tags = metaData?.tags.map((item) =&gt; item.name);

  return {
    title: metaData.title,
    description: metaData.description,
    keywords: tags,
    openGraph: {
      title: metaData.title,
      description: metaData.excpet,
      url: metaData.url,
      keywords: tags,
      images: [
        {
          url: metaData.feature_image
        }
      ],
      locale: metaData.locale,
      type: 'website'
    }
  };
}
</code></pre>
<h3 id="how-to-enable-search">如何开启搜索</h3>
<p>在静态博客上启用搜索是很难从头做起的。相反，你可以使用第三方的 Node 页面，如 <a href="https://github.com/oramasearch/orama">Orama</a> 或 <a href="https://github.com/nextapps-de/flexsearch">Flex search</a>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/04/searchbarinnextjs.gif" alt="searchbarinnextjs" width="600" height="400" loading="lazy"></p>
<p>对于我们的演示，我们创建了一个非常简单的搜索栏功能，无需安装任何额外的软件包。</p>
<p>首先，我们从 Ghost CMS 的 API 中获取所有帖子。</p>
<pre><code class="language-typescript">// ghost-client.ts

export async function getSearchPosts() {
  return await api.posts.browse({ limit: "all"}).catch(err =&gt; {
    console.log(err)
  });
</code></pre>
<p>在我们用<code>JSON.stringify()</code>的帮助下将其转换为字符串后，我们再创建一个新的<code>search.json</code>文件。在每次请求时，它都会更新或重写我们的<code>search.json</code>文件。</p>
<pre><code class="language-typescript">// src/app/page.tsx

import { getSearchPosts } from './ghost-client';
import * as fs from 'node:fs';

export default async function Home() {
  // get All posts for search
  const AllPostForSerach = await getSearchPosts();

  // Enable getSearch

  try {
    const jsonString = JSON.stringify(AllPostForSerach);

    fs.writeFile('search.json', jsonString, 'utf8', (err) =&gt; {
      if (err) {
        console.log('Error writing file', err);
      } else {
        console.log('Successfully wrote file');
      }
    });
  } catch (error) {
    console.log('error : ', error);
  }

  return (
    &lt;&gt;
      &lt;main className="container my-12 mx-auto grid grid-cols-1 gap-2 md:gap-3 lg:gap-4 lg:grid-cols-3 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-4"&gt;
        {/* rest code... */}
      &lt;/main&gt;
    &lt;/&gt;
  );
}
</code></pre>
<p>当你在搜索输入中输入文本时，根据文本查询，我们比较查询或文本在<code>search.json</code>文件的数据。如果它与查询的文章标题相匹配，那么我们就存储<code>searchPost</code>变量，最后我们在<code>searchPost</code>变量页面呈现存储的数据。</p>
<pre><code class="language-typescript">'use client';

import React, { useEffect, useState } from 'react';
import * as Popover from '@radix-ui/react-popover';
import { FaSearch } from 'react-icons/fa';
import Link from 'next/link';
import searchData from '../../search.json';
import type { PostOrPage } from '@tryghost/content-api';

let searchPost: PostOrPage[] = [];

function Search() {
  const [query, setQuery] = useState(null);

  useEffect(() =&gt; {
    searchPost.length = 0;

    searchData.map((item: PostOrPage) =&gt; {
      if (
        item?.title.trim().toLowerCase().includes(query?.trim().toLowerCase())
      ) {
        searchPost.push(item);
      }
    });
  }, [query]);

  return (
    &lt;Popover.Root&gt;
      &lt;Popover.Trigger asChild&gt;
        &lt;button className="cursor-pointer outline-none" aria-label="Search"&gt;
          &lt;FaSearch /&gt;
        &lt;/button&gt;
      &lt;/Popover.Trigger&gt;

      &lt;Popover.Portal&gt;
        &lt;Popover.Content
          className="rounded p-2 bg-white dark:bg-gray-800 w-[480px] will-change-[transform,opacity] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade"
          sideOffset={5}
        &gt;
          &lt;div className="my-2"&gt;
            &lt;label
              htmlFor="default-search"
              className="mb-2 mt-5 text-sm font-medium text-gray-900 sr-only dark:text-white"
            &gt;
              Search bar{' '}
            &lt;/label&gt;
            &lt;div className="relative"&gt;
              &lt;div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"&gt;
                &lt;svg
                  className="w-5 h-5 text-gray-500 dark:text-gray-400"
                  fill="none"
                  stroke="currentColor"
                  viewBox="0 0 24 24"
                  xmlns="http://www.w3.org/2000/svg"
                &gt;
                  &lt;path
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    strokeWidth="2"
                    d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
                  &gt;&lt;/path&gt;
                &lt;/svg&gt;
              &lt;/div&gt;
              &lt;input
                type="search"
                id="default-search"
                onChange={(event) =&gt; setQuery(event?.target.value)}
                className="block w-full p-4 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                placeholder="Start searching here ..."
                required
              /&gt;
            &lt;/div&gt;
          &lt;/div&gt;

          {serachPost.length &gt; 0
            ? serachPost.map((item) =&gt; {
                return (
                  &lt;div key={item.uuid} className="my-3"&gt;
                    &lt;div className="text-white my-2 py-2 bg-blue-400 dark:bg-gray-900 dark:hover:bg-blue-400 border-none rounded-md dark:text-white"&gt;
                      &lt;Link
                        href={`read/${item.slug}`}
                        className="relative inline-flex items-center rounded-lg w-full px-4 py-2 text-sm font-medium"
                      &gt;
                        {item.title}
                      &lt;/Link&gt;
                    &lt;/div&gt;
                  &lt;/div&gt;
                );
              })
            : ' '}
        &lt;/Popover.Content&gt;
      &lt;/Popover.Portal&gt;
    &lt;/Popover.Root&gt;
  );
}

export default Search;
</code></pre>
<h3 id="how-to-enable-search">如何开启搜索</h3>
<p>Next.js 有两种类型的 <a href="https://beta.nextjs.org/docs/routing/error-handling#how-errorjs-works">错误处理</a>。第一种是基于布局，第二种是 <a href="https://beta.nextjs.org/docs/routing/error-handling#handling-errors-in-root-layouts">全局错误</a> 处理。对于这里的演示，我们将使用基于布局的错误处理。</p>
<p>Next 提供一个特殊类型的<code>error.tsx</code>文件来处理你网站上的错误。它不处理 404，500 等，它只处理运行时错误。</p>
<pre><code class="language-typescript">'use client'; // Error components must be Client components
import React from 'react';
import { useEffect } from 'react';
import Link from 'next/link';
export default function Error({
  error,
  reset
}: {
  error: Error;
  reset: () =&gt; void;
}) {
  useEffect(() =&gt; {
    console.error(error);
  }, [error]);

  return (
    &lt;section className="dark:bg-gray-900 my-16"&gt;
      &lt;div className="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6"&gt;
        &lt;div className="mx-auto max-w-screen-sm text-center"&gt;
          &lt;h1 className="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-primary-600 dark:text-primary-500"&gt;
            Something wrong
          &lt;/h1&gt;
          &lt;p className="mb-4 text-lg p-2 font-light bg-red-500 text-white dark:bg-red-400 dark:text-white"&gt;
            {error.message}
          &lt;/p&gt;

          &lt;div className="flex justify-around mt-2"&gt;
            &lt;Link
              href="#"
              className="inline-flex bg-gray-600 text-white hover:bg-gray-700 focus:ring-4 font-medium rounded-lg text-sm p-2
                text-center"
            &gt;
              Back to Homepage
            &lt;/Link&gt;

            &lt;button
              className="bg-gray-600 text-white rounded-lg p-2"
              onClick={() =&gt; reset()}
            &gt;
              Try again
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  );
}
</code></pre>
<h4 id="404">如何处理 404 错误</h4>
<p>为了处理 Next.js 应用程序文件夹中的 404 错误，你需要在你的文件夹最顶层创建一个<code>not-found.tsx</code>文件。</p>
<p>我们的 404 文件看起来像这样：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/nextjsandghosterror.png" alt="404 error" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>404 error</figcaption>
</figure>
<p>以下是相关代码：</p>
<pre><code class="language-typescript">import Link from 'next/link';

function NotFound() {
  return (
    &lt;section className="bg-white dark:bg-gray-900 my-16"&gt;
      &lt;div className="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6"&gt;
        &lt;div className="mx-auto max-w-screen-sm text-center"&gt;
          &lt;h1 className="mb-4 text-7xl tracking-tight lg:text-9xl text-primary-600 dark:text-primary-500"&gt;
            404
          &lt;/h1&gt;
          &lt;p className="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl dark:text-white"&gt;
            {' '}
            Something wrong
          &lt;/p&gt;
          &lt;p className="mb-4 text-lg font-light text-gray-500 dark:text-gray-400"&gt;
            Sorry, we cant find that article. You will find lots to explore on
            the home page.
          &lt;/p&gt;
          &lt;Link
            href="/"
            className="inline-flex text-white bg-black dark:bg-white dark:text-black p-3 hover:bg-gray-800 my-4"
          &gt;
            Back to Homepage
          &lt;/Link&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/section&gt;
  );
}

export default NotFound;
</code></pre>
<p><code>not-found.tsx</code>错误文件的问题是它在 Next（v13.3.0）中不能自动显示。要显示 404 错误，你需要手动显示该错误。这里是你如何做的：</p>
<pre><code class="language-typescript">import { notFound } from 'next/navigation';

async function Read({ params }: { params: { slug: string } }) {
  const getPost = await getSinglePost(params.slug);

  // if not found getPost, then show 404 error

  if (!getPost) {
    notFound();
  }

  return (
    &lt;main className="pt-8 pb-16 lg:pt-16 lg:pb-24 dark:bg-gray-900"&gt;
      rest of code ....
    &lt;/main&gt;
  );
}
</code></pre>
<h3 id="how-to-rebuild-your-static-site-with-webhooks">如何用 webhooks 重新构建你的静态网站</h3>
<p>当你创建一个静态网站时，最大的问题发生在有人在 Ghost 中写了一个新的帖子或改变了一个现有的帖子。对于一个个人项目，你可以手动重新部署你的网站。但对于一个较大的网站来说，你不可能在每次发生这种情况时都这样做。</p>
<p>最好的解决办法是使用 webhooks。Ghost 提供 webhook 支持。如果你更新一个现有的帖子或写一个新的帖子，它就会在 Ghost 中更新。</p>
<p>在演示项目中，我们使用 Vercel webhooks 来部署我们的博客。当我们创建一个新的博客或更新网站上的东西时，Ghost 会触发 Vercel webhook。然后 Vercel 根据需要重建网站。</p>
<p>你不需要为这个写代码,只要跟着你的思路，边走边复制粘贴。</p>
<h4 id="vercelwebhook">如何从 Vercel 获得 webhook</h4>
<p>首先，进入 Vercel 仪表板。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/select1.png" alt="Vercel 仪表板" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>Vercel 仪表板</figcaption>
</figure>
<p>选择你的项目，你将在那里部署你的 Ghost 前台。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/select2.png" alt="在你的 Vercel 仪表板上选择项目" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>在你的 Vercel 仪表板上选择项目</figcaption>
</figure>
<p>点击你的 Vercel 项目中的设置标签（settings）。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/select3.png" alt="点击 Git 标签" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>点击 Git 标签</figcaption>
</figure>
<p>然后点击 Git 标签。向下滚动后，你可以看到 deploy hook 的选择。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/select4.png" alt="转到 deploy hook 部分" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>转到 deploy hook 部分</figcaption>
</figure>
<p>输入你的 webhook 名称和分支名称，然后点击 <code>create hook</code> 按钮</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/select5.png" alt="复制你的 webhook 网址" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>复制你的 webhook 网址</figcaption>
</figure>
<p>点击 <code>copy</code> 按钮，复制你的 vercel webhook。</p>
<h4 id="ghostvercelwebhook">如何在 Ghost 仪表板中集成 Vercel 的 web hook</h4>
<p>当 Ghost 中发生变化时，它就会触发 Vercel 的 webhook URL。然后，Vercel 会重新部署博客网站。</p>
<p>要将 Vercel webhook 与 Ghost 集成，只需遵循以下步骤：</p>
<p>打开 Ghost CMS 仪表板。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/ghost1.png" alt="Ghost 仪表板" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>Ghost 仪表板</figcaption>
</figure>
<p>点击设置（齿轮）图标。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/ghost3.png" alt="Ghost 设置" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>Ghost 设置</figcaption>
</figure>
<p>点击 <code>New custom integration</code> 按键。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/ghost4.png" alt="添加新的集成" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>添加新的集成</figcaption>
</figure>
<p>输入 <code>integration</code> 名字</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/ghost5.png" alt="添加 integration 的命名" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>添加 integration 的命名</figcaption>
</figure>
<p>点击 <code>add webhook</code> 按键。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/04/ghost7.png" alt="怎么添加 webhook" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>怎么添加 webhook</figcaption>
</figure>
<p>首先，输入名称，然后选择事件，并粘贴你从 Vercel 仪表板上复制的 URL。</p>
<p>基于该事件，Ghost 将调用 webhook，你的网站将重新构建。重新部署需要时间，这取决于你的网站有多大，以此类推。</p>
<h2 id="conclusion">总结</h2>
<p>使用 Next.js 和 Ghost CMS，一切都应该正常工作，正如我们在本教程中所做的那样。</p>
<p>但是 Ghost 的一些编辑器组件，比如切换器，在需要 JavaScript 交互的地方，却不能工作。你可以通过编写你自己的 JavaScript 或者获得 Ghost 的 JavaScript 文件，并将其添加到<code>read/[slug]/page.tsx</code>文件中来解决这个问题。</p>
<p>通过结合 Next.js 和 Ghost CMS API，你可以节省大量的主机费用，但你会失去一些功能，如内置的注册(signup)、登录(login)、账户(accounts)、订阅(subscriptions)、搜索栏(search bar)和会员访问级别(member access levels)。</p>
<p>你可以在 <a href="https://twitter.com/Official_R_deep">Twitter</a> 和 <a href="https://www.linkedin.com/in/officalrajdeepsingh/">Linkedin</a> 上关注我或联系我。如果你喜欢我的工作，你可以在我的博客、<a href="https://officialrajdeepsingh.dev/">officialrajdeepsingh.dev</a>、<a href="https://medium.com/frontendweb">frontend web</a>上阅读更多内容，并注册我的<a href="https://officialrajdeepsingh.medium.com/subscribe">免费通讯(free newsletter)</a> 。</p>
<p>你还可以查看 <a href="https://github.com/officialrajdeepsingh/awesome-nextjs">awesome-next</a>，这是一个精心策划的基于 Nextjs 的很棒的库列表，有助于用 Next.js 构建小型和大型应用程序。</p>
<p>这里有一些补充内容：</p>
<ul>
<li><a href="https://ghost.org/docs/jamstack/next/">用 Headless Ghost+Next.js 构建自定义 JavaScript 应用程序</a></li>
<li><a href="https://www.digitalocean.com/community/tutorials/how-to-build-your-blog-on-digitalocean-with-ghost-and-next-js">用 Ghost 为你的服务器端应用提供动力，用 Next.js 的 React 框架构建一个完全自定义的前端</a></li>
<li><a href="https://ghost.org/docs/content-api/">Ghost 内容 API 文档</a></li>
<li><a href="https://nextjs.org/docs">入门｜Next.js</a></li>
</ul>
<p>我在 Next 上写了大量的文章。如果你对 Next 和相关的东西感兴趣，你可以在 <a href="https://officialrajdeepsingh.medium.com/">Medium</a> 上关注我，并加入 <a href="https://medium.com/frontendweb">frontend web publication</a>。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Next.js 和 React 有哪些区别？ ]]>
                </title>
                <description>
                    <![CDATA[ 大家好！最近，Next.js 13 正在流行，这是一个学习它的好时机。但 Next.js 到底是什么？Next.js 中的预渲染是什么，我们为什么要使用它？ 嗯，让我解释一下。 Next.js 是什么？ Next.js 是一个基于 React 的后端框架。 我们在 React 中能做的一切，在 Next.js 中也能做,还有一些额外的功能，如路由、API 调用、认证等等。我们在 React 中没有这些功能。相反，我们必须安装一些外部库和依赖项。例如，React Router 用于单页 React 应用程序的路由。 但在 Next.js 中，情况就不同了。我们不需要依赖外部库来完成这些事情。当我们创建一个 Next.js 应用程序时，它们就被内置在软件包中。 这就是 Next.js 应用与传统 React 应用不同的主要原因。 客户端渲染 VS 服务器端渲染 Next.js 也使用了一种叫做服务器端渲染的东西。而为了理解它的工作原理，我们也需要谈谈客户端渲染。 基本上，客户端就是我们在屏幕上看到的东西（用户界面）。这就是客户端，我们能看到的东西。换句话说，它是代码的前端部分 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/next-vs-react/</link>
                <guid isPermaLink="false">6485a6673c820a06f4b65acb</guid>
                
                    <category>
                        <![CDATA[ NextJS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Sun, 11 Jun 2023 10:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/06/What--7-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/next-vs-react/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Next.js vs React – What are the Differences?</a>
      </p><!--kg-card-begin: markdown--><p>大家好！最近，Next.js 13 正在流行，这是一个学习它的好时机。但 Next.js 到底是什么？Next.js 中的预渲染是什么，我们为什么要使用它？</p>
<p>嗯，让我解释一下。</p>
<h2 id="nextjs">Next.js 是什么？</h2>
<p>Next.js 是一个基于 React 的后端框架。</p>
<p>我们在 React 中能做的一切，在 Next.js 中也能做,还有一些额外的功能，如路由、API 调用、认证等等。我们在 React 中没有这些功能。相反，我们必须安装一些外部库和依赖项。例如，React Router 用于单页 React 应用程序的路由。</p>
<p>但在 Next.js 中，情况就不同了。我们不需要依赖外部库来完成这些事情。当我们创建一个 Next.js 应用程序时，它们就被内置在软件包中。</p>
<p>这就是 Next.js 应用与传统 React 应用不同的主要原因。</p>
<h2 id="vs">客户端渲染 VS 服务器端渲染</h2>
<p>Next.js 也使用了一种叫做服务器端渲染的东西。而为了理解它的工作原理，我们也需要谈谈客户端渲染。</p>
<p>基本上，客户端就是我们在屏幕上看到的东西（用户界面）。这就是客户端，我们能看到的东西。换句话说，它是代码的前端部分。</p>
<p>另一方面，服务器是我们看不到的东西。它是代码的后端，或服务器代码。</p>
<p>现在，在客户端渲染中，应用程序加载并在浏览器上动态地生成输出。换句话说，浏览器使用 JavaScript 渲染页面。</p>
<p>但在服务器端渲染中，我们在屏幕上看到的用户界面不是由浏览器生成的，而是在服务器上生成的。当一个应用程序加载时，它不需要解析浏览器上的用户界面。相反，它来自于服务器端，是在服务器上预先生成的。</p>
<h2 id="reactcsr">React 和 CSR 如何工作</h2>
<p>因此，当我们加载一个 React 应用程序时，或者当它被安装后，我们在浏览器中检查源代码，我们会得到这样的东西：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/03/Screenshot-2023-03-18-at-7.41.25-PM-1.png" alt="React 源代码" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>React 源代码</figcaption>
</figure>
<p>如果你简化它，我们得到以下结果：</p>
<pre><code class="language-html">&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="utf-8" /&gt;
    &lt;link rel="icon" href="/favicon.ico" /&gt;
    &lt;meta name="viewport" content="width=device-width,initial-scale=1" /&gt;
    &lt;meta name="theme-color" content="#000000" /&gt;
    &lt;meta name="description" content="Blogs by Cybernatico" /&gt;
    &lt;link rel="apple-touch-icon" href="/logo192.png" /&gt;
    &lt;link rel="manifest" href="/manifest.json" /&gt;
    &lt;title&gt;Blogs by Cybernatico&lt;/title&gt;
    &lt;link href="/static/css/2.877ae64e.chunk.css" rel="stylesheet" /&gt;
    &lt;link href="/static/css/main.4d9c354c.chunk.css" rel="stylesheet" /&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;noscript&gt;You need to enable JavaScript to run this app.&lt;/noscript&gt;
    &lt;div id="root"&gt;&lt;/div&gt;

    &lt;script src="/static/js/2.48c493c5.chunk.js"&gt;&lt;/script&gt;
    &lt;script src="/static/js/main.f9b5cf72.chunk.js"&gt;&lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>如果你看一下用户界面中的输出，它将是这样的：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2023/03/Screenshot-2023-03-18-at-7.46.23-PM.png" alt="React 源代码" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>React 应用</figcaption>
</figure>
<p>在这个页面的源代码中，我们只得到几行代码，其中包括标题、 meta 标签和链接参考（link references）。</p>
<p>但在 body 中，我们只有以下内容：</p>
<pre><code class="language-html">&lt;div id="root"&gt;&lt;/div&gt;
</code></pre>
<p>那么，其余的代码在哪里呢？当页面加载时，我们在浏览器中看不到它。这是因为 React 使用客户端渲染（CSR）。React 应用程序在客户端处理 DOM，也就是在浏览器中。</p>
<p>每当我们加载一个 React 应用程序，所有的 UI 组件都会在浏览器上动态生成。</p>
<h2 id="nextjsssr">Next.js 和 SSR 如何工作？</h2>
<p>如果你做了我们之前做的同样的事情，但用 Next.js 应用程序，你会得到不同的东西：</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
  &lt;head&gt;
    &lt;meta charset="utf-8" /&gt;
    &lt;title&gt;Next.js Tutorial&lt;/title&gt;
    &lt;meta name="description" content="This is a Next.js Tutorial" /&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1" /&gt;
    &lt;link rel="icon" href="/favicon.ico" /&gt;
    &lt;meta name="next-head-count" content="5" /&gt;
    &lt;noscript data-n-css=""&gt;&lt;/noscript&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;div id="__next"&gt;
      &lt;div&gt;
        &lt;h2&gt;This is the Home Page!&lt;/h2&gt;
        &lt;a href="/profile/1"&gt;&lt;p&gt;Go to the Profile Page of 1!&lt;/p&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>现在，这是一个简单的 Next.js 应用程序的源代码。我们看到整个内容，如 HTML、CSS 和 JavaScript。</p>
<p>这意味着，当 Next.js 应用程序加载时，我们在用户界面上看到的网络上的内容已经生成。而这是在服务器上发生的。这是因为 Next.js 利用了服务器端渲染（或 SSR），也被称为预渲染。</p>
<h2 id="prerendering">什么是 Pre-Rendering（预渲染）？</h2>
<p>预渲染是服务器端渲染的一个例子，在浏览器上加载应用程序或网站之前，内容已经生成。</p>
<h3 id="prerendering">为什么使用 Pre-Rendering（预渲染）？</h3>
<p>服务器端渲染（或预渲染）使应用程序的加载速度加快。这是因为我们将要看到的输出已经在服务器端生成。它不需要在浏览器上生成。这使得应用更快。</p>
<h2 id="">感谢阅读！</h2>
<p>现在你应该知道 Next.js 和 React 的主要区别了。React 使用 CSR 或客户端渲染，其中 UI 元素是在浏览器上生成的。在 Next.js 中，用户界面来自服务器，提前生成。</p>
<p>如果你想开发电子商务、营销网站或简单的登陆页面等应用，你可以使用 Next.js。如果你想开发社交媒体应用程序或流媒体工具，如 Netflix 或 Youtube，你可以使用 React。</p>
<p>如果你想观看本博客的视频版本，你可以在这里找到它：<a href="https://youtu.be/3oV9SgC8ufI">Next.js 框架课程--Next.js 的预渲染(Pre-Rendering)</a>。</p>
<p>如果你想进一步了解 Next.js，我正在建立一个关于它的课程。这是一个播放列表，你将在其中学习所有这些 Next.js 的东西。它仍在进行中。请看这里: <a href="https://youtube.com/playlist?list=PLWgH1O_994O_8Hg0-Q1xaD8ewXMx-fsBb">https://youtube.com/playlist?list=PLWgH1O_994O_8Hg0-Q1xaD8ewXMx-fsBb</a></p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 什么是 Git 远程分支？如何从 GitHub 查看远程分支？ ]]>
                </title>
                <description>
                    <![CDATA[ Git 是一个免费的开源工具。具体来说，它是当今软件开发中最流行的版本控制系统。 Git 允许你跟踪和维护一个编程项目的不同版本。 有了 Git，开发者和技术团队可以在一个项目上进行协作和工作。 多个开发者可以同时在项目的相同或不同部分工作，而不互相干扰，提高生产力和效率。 借助 Git 提供的内置功能和工具，开发者可以同时进行协作，并在他们的环境中工作。其中一个功能就是分支（branch）。 什么是 Git 中的分支 Git 中的分支是一个独立的、安全的、与主项目相分离的开发区域。 分支允许开发人员开发和测试新的功能，修复错误，实验新的想法，并减少破坏代码库中稳定代码的风险。 本地分支与远程分支--有什么区别 在 Git 中，有两种类型的分支：本地分支（local branch）和远程分支（remote branch）。 一个本地分支只存在于你的本地机器上。 所有你引入并提交到本地仓库的修改都只存储在你的本地系统上。它们提供了一种实验、修复错误和开发新功能而不影响主代码库的方式。 要创建一个本地分支，可以使用 git branch <branch-name>  命 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/remote-branches-in-git/</link>
                <guid isPermaLink="false">63fdffd50687e3060be26dc1</guid>
                
                    <category>
                        <![CDATA[ Git ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Thu, 08 Jun 2023 01:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/02/pexels-antonio-borriello-1297611.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/remote-branches-in-git/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">What is a Remote Branch in Git? How to Check out Remote Branches from GitHub</a>
      </p><!--kg-card-begin: markdown--><p>Git 是一个免费的开源工具。具体来说，它是当今软件开发中最流行的版本控制系统。</p>
<p>Git 允许你跟踪和维护一个编程项目的不同版本。</p>
<p>有了 Git，开发者和技术团队可以在一个项目上进行协作和工作。</p>
<p>多个开发者可以同时在项目的相同或不同部分工作，而不互相干扰，提高生产力和效率。</p>
<p>借助 Git 提供的内置功能和工具，开发者可以同时进行协作，并在他们的环境中工作。其中一个功能就是分支（branch）。</p>
<h2 id="git">什么是 Git 中的分支</h2>
<p>Git 中的分支是一个独立的、安全的、与主项目相分离的开发区域。</p>
<p>分支允许开发人员开发和测试新的功能，修复错误，实验新的想法，并减少破坏代码库中稳定代码的风险。</p>
<h3 id="">本地分支与远程分支--有什么区别</h3>
<p>在 Git 中，有两种类型的分支：本地分支（local branch）和远程分支（remote branch）。</p>
<p>一个本地分支只存在于你的本地机器上。</p>
<p>所有你引入并提交到本地仓库的修改都只存储在你的本地系统上。它们提供了一种实验、修复错误和开发新功能而不影响主代码库的方式。</p>
<p>要创建一个本地分支，可以使用 <code>git branch &lt;branch-name&gt;</code> 命令，其中 <code>branch-name</code> 是新分支的名称。</p>
<p>例如，如果你想创建一个名为 "test "的新分支，你可以使用以下命令：</p>
<pre><code class="language-bash">git branch test
</code></pre>
<p>你可以使用<code>git checkout</code>命令切换到新的分支，并创建你想要的修改：</p>
<pre><code class="language-bash">git checkout test
</code></pre>
<p>要查看本地分支的列表，请使用<code>git branch</code>命令。</p>
<p>说了这么多，你不能在本地分支上与其他开发者协作。这时，远程分支就派上用场了。</p>
<p>远程分支是开发者在同一个项目上同时协作的方式。</p>
<p>远程分支存在于一个远程仓库中（通常称为 <code>origin</code>），托管在 <a href="https://github.com/">GitHub</a> 等平台上。</p>
<p>当你向本地分支提交了修改，你可以使用<code>git push &lt;remote&gt; &lt;local&gt;</code>语法将本地分支推送到远程仓库（<code>origin</code>）。然后，其他人就可以看到你的代码了。</p>
<pre><code class="language-bash">git push -u origin test
</code></pre>
<p>Git 会在远程仓库中创建一个本地分支的副本。这个副本被称为远程分支。</p>
<p>要查看项目中所有远程分支的列表，请使用<code>git branch -r</code>命令。</p>
<h2 id="git">如何在 Git 中签出一个远程分支</h2>
<p>你可能需要访问另一个开发者创建的分支，以进行审查或协作。</p>
<p>这个分支不在你的本地系统上,它是存储在远程仓库上的一个远程分支。</p>
<p>问题是，Git 并不自动允许你在别人的分支上工作。</p>
<p>相反，你需要创建一个本地副本，映射你要处理的远程分支，然后在本地进行修改。</p>
<p>让我们看看下面几节中你需要采取的步骤。</p>
<h3 id="">如何获取所有远程分支</h3>
<p>首先，你需要使用<code>git fetch</code>命令获取必要的分支数据，以及远程存储库的名称：</p>
<pre><code class="language-bash">git fetch origin
</code></pre>
<p>这个命令将从远程仓库下载最新的修改（包括远程分支）到你的本地机器。</p>
<p>如果你有一个不同的远程名称，用它替换<code>origin</code>。</p>
<h3 id="">如何查看可检出的分支</h3>
<p>接下来，要查看可供签出的分支列表，请使用以下命令：</p>
<pre><code class="language-bash">git branch -r
</code></pre>
<p><code>-r</code>（是 remote 的缩写）选项告诉 Git 列出远程分支。</p>
<p>该命令的输出将是一个所有可供签出的远程分支的列表。你会看到分支名称前有<code>remotes/origin</code>的前缀。</p>
<h3 id="">如何检出远程分支的情况</h3>
<p>你不能直接对你感兴趣的远程分支进行修改，你需要一个本地副本。</p>
<p>你需要检出(checkout)你感兴趣的分支，这样你就可以开始在本地进行你想做的修改。</p>
<p>要做到这一点，使用<code>git checkout</code>命令，加上<code>-b</code>（代表分支）选项。语法是这样的：</p>
<pre><code class="language-bash">git checkout -b &lt;new-local-branch-name&gt; origin/&lt;remote-branch-name&gt;
</code></pre>
<p>因此，如果你想获得远程分支<code>new-feature</code>的副本，你应该做以下事情：</p>
<pre><code class="language-bash">git checkout -b new-feature origin/new-feature
</code></pre>
<p>上面的命令在你的机器上创建了一个新的本地副本，它基于并连接到远程分支。</p>
<p>它创建了一个名为 "new-feature "的新分支，检出（checkout）该分支，并将 <code>origin/new-feature</code> 中的修改拉到新分支中。</p>
<p>现在你可以向<code>origin/new-feature</code>推送新的提交。</p>
<h3 id="">谢谢你阅读本文</h3>
<p>现在你知道如何在 Git 中检出和使用远程分支了。Happy coding!</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 学习 Web 开发——面向初学者的免费全栈开发课程 ]]>
                </title>
                <description>
                    <![CDATA[ 术语“全栈 [https://www.freecodecamp.org/news/what-is-a-full-stack-developer-back-end-front-end-full-stack-engineer/] 开发者”指的是同时处理Web应用的前端和后端组件。 前台是用户与之交互的东西，而后台是Web应用的逻辑。 在这篇文章中，我将介绍一些可以帮助你成为全栈开发者的资源：  * freeCodeCamp [https://www.freecodecamp.org/learn/]  * CS50's Web Programming with Python and JavaScript    [https://www.edx.org/course/cs50s-web-programming-with-python-and-javascript]  * The Odin Project [https://www.theodinproject.com/] 我还将提供更多的YouTube课程链接，你可以通过创建更多的项目继续练习你的技能。  * freeCodeCamp ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/learn-web-development-free-full-stack-developer-courses-for-beginners/</link>
                <guid isPermaLink="false">646729a04de70a072b7e4371</guid>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Fri, 19 May 2023 07:54:39 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/05/ferenc-almasi-L8KQIPCODV8-unsplash.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/learn-web-development-free-full-stack-developer-courses-for-beginners/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Learn Web Development – Free Full Stack Developer Courses for Beginners</a>
      </p><!--kg-card-begin: markdown--><p>术语“<a href="https://www.freecodecamp.org/news/what-is-a-full-stack-developer-back-end-front-end-full-stack-engineer/">全栈</a>开发者”指的是同时处理Web应用的前端和后端组件。</p>
<p>前台是用户与之交互的东西，而后台是Web应用的逻辑。</p>
<p>在这篇文章中，我将介绍一些可以帮助你成为全栈开发者的资源：</p>
<ul>
<li><a href="https://www.freecodecamp.org/learn/">freeCodeCamp</a></li>
<li><a href="https://www.edx.org/course/cs50s-web-programming-with-python-and-javascript">CS50's Web Programming with Python and JavaScript</a></li>
<li><a href="https://www.theodinproject.com/">The Odin Project</a></li>
</ul>
<p>我还将提供更多的YouTube课程链接，你可以通过创建更多的项目继续练习你的技能。</p>
<ul>
<li><a href="#freecodecamp">freeCodeCamp</a></li>
<li><a href="#the-odin-project">The Odin Project</a></li>
<li><a href="#cs50-s-web-programming-with-python-and-javascript">CS50's Web Programming with Python and JavaScript</a></li>
<li><a href="#suggested-youtube-full-stack-project-tutorials">Suggested YouTube full stack project tutorials</a></li>
</ul>
<h2 id="freecodecamp">freeCodeCamp</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-31-at-1.02.41-AM.png" alt="Screen-Shot-2022-01-31-at-1.02.41-AM" width="600" height="400" loading="lazy"></p>
<p><a href="https://www.freecodecamp.org/learn/">freeCodeCamp</a>是一个免费的在线交互式学习平台，你可以在这里学习网络开发，并顺便获得认证。每个课程都有一系列的挑战，你将学习这些课程，然后完成5个认证项目。</p>
<p>前四门课程涵盖前端技术，包括HTML、CSS、Vanilla JavaScript、React和D3。后端开发、关系数据库课程和质量保证认证涵盖Node、Express、SQL、用Chai测试、MongoDB等。</p>
<p>以下是你将建立的一些项目的列表。</p>
<ul>
<li>产品登陆页</li>
<li>随机报价机</li>
<li>25+5时钟</li>
<li>世界杯数据库</li>
<li>数独解题器</li>
</ul>
<p>完成这些认证后，你将知道如何构建全栈Web应用程序。从那里你可以为你的作品集创建自己的项目，并开始申请初级工作。</p>
<p>其余的认证包括Python和机器学习。这些都是比较中高级的认证，并假定你已经完成了之前的JavaScript认证。</p>
<p>如果你在课程上需要帮助，请联系<a href="https://forum.freecodecamp.org/">freeCodeCamp论坛</a>，世界各地的开发者可以在代码上帮助你。</p>
<h2 id="theodinproject">The Odin Project</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/Screen-Shot-2022-01-31-at-1.22.36-AM.png" alt="Screen-Shot-2022-01-31-at-1.22.36-AM" width="600" height="400" loading="lazy"></p>
<p>这是一个免费的基于项目的在线平台，在这里你可以学习<a href="https://www.theodinproject.com/paths/full-stack-javascript?">全栈式JavaScript</a>或<a href="https://www.theodinproject.com/paths/full-stack-ruby-on-rails?">全栈式Ruby on Rails</a>。</p>
<p>你将首先通过<a href="https://www.theodinproject.com/paths/foundations/courses/foundations">基础课程</a>，学习HTML、CSS、JavaScript基础知识、Git、命令行以及如何使用文本编辑器。这些课程有建议的读物、作业和项目，以便在学习过程中完成。</p>
<p>以下是你将在<a href="https://www.theodinproject.com/paths/foundations/courses/foundations">基础课程</a>中建立的一些项目的清单。</p>
<ul>
<li>Rock Paper Scissors</li>
<li>Etch-a-Sketch</li>
<li>Landing page</li>
</ul>
<p>从那里你可以选择JavaScript或Ruby on Rails课程。如果你需要帮助决定选择哪一个，请阅读<a href="https://www.theodinproject.com/paths/foundations/courses/foundations/lessons/choose-your-path-forward">The Odin Project的这一指南</a>。</p>
<p><a href="https://www.theodinproject.com/paths/full-stack-ruby-on-rails?">Ruby on Rails课程</a>包括Ruby编程、中高级HTML和CSS、Ruby on Rails框架等。</p>
<p>以下是你将在<a href="https://www.theodinproject.com/paths/full-stack-ruby-on-rails?">Ruby on Rails课程</a>中建立的一些项目。</p>
<ul>
<li>Tic Tac Toe</li>
<li>SQL Zoo</li>
<li>Personal Portfolio</li>
</ul>
<p><a href="https://www.theodinproject.com/paths/full-stack-javascript?">JavaScript课程</a>涵盖了中高级HTML和CSS、JavaScript、MongoDB、Node、Express等内容。他们还包括一份关于<a href="https://www.theodinproject.com/paths/full-stack-ruby-on-rails/courses/getting-hired">如何被雇用</a>的有用指南，以便找到你的第一份工作。</p>
<p>以下是你将在<a href="https://www.theodinproject.com/paths/full-stack-javascript?">JavaScript课程</a>中建立的一些项目：</p>
<ul>
<li>restaurant page</li>
<li>weather app</li>
<li>blog API</li>
</ul>
<p>如果你在课程上需要帮助，请访问<a href="https://discord.com/invite/fbFCkYabZB">The Odin Project discord channel</a>。</p>
<h2 id="cs50pythonjavascriptweb">CS50 Python和JavaScript Web编程</h2>
<p>这个<a href="https://www.edx.org/course/cs50s-web-programming-with-python-and-javascript">CS50 Web编程课程</a>将教你HTML、CSS、JavaScript、Git、Python、Django、SQL等。你首先需要学习<a href="https://www.edx.org/course/introduction-computer-science-harvardx-cs50x">CS50的计算机科学入门</a>。</p>
<p>计算机科学导论将通过一系列问题集和一个最终项目教给你编程的基础知识。然后你可以进入Web编程课程，进一步发展你的技能。</p>
<p>该课程由David Malan和Brian Yu教授，他们是哈佛大学的顶级讲师。在你完成这些课程后，你将在全栈Web开发方面有一个坚实的起步基础。</p>
<p>这两门课程都可以在<a href="https://www.edx.org/">edX</a>上找到，可以免费审核。</p>
<p>如果你在CS50课程中需要帮助，请通过他们的任何一个<a href="https://cs50.harvard.edu/x/2022/communities/">社交媒体平台</a>联系。</p>
<h2 id="youtube">YouTube上的全栈项目教程</h2>
<p>在你建立了全栈Web开发的基本基础后，你可以研究这些额外的资源，以创建更多的项目来加强你的技能。</p>
<ul>
<li><a href="https://www.youtube.com/watch?v=mEPm9w5QlJM">Flutter &amp; Firebase课程--建立一个全栈式的仿Instagram</a></li>
<li><a href="https://www.youtube.com/watch?v=OUzaUJ3gEug">云计算中的全栈网络开发课程 - Svelte、Postgres、Vercel、Gitpod</a></li>
<li><a href="https://www.youtube.com/watch?v=ngc9gnGgUdA">全栈MERN项目 - 构建和部署一个应用程序 | React + Redux、Node、Express、MongoDB[Part 1/2]</a></li>
<li><a href="https://www.youtube.com/watch?v=aibtHnbeuio">全栈MERN项目 - 构建和部署一个应用程序 | React + Redux、Node、Express、MongoDB[Part 2/2]</a></li>
<li><a href="https://www.youtube.com/watch?v=Yg5zkd9nm6w">使用Django和Vue的电子商务网站教程（Django Rest 框架）</a></li>
<li><a href="https://www.youtube.com/watch?v=0iB5IPoTDts">Python微服务网络应用（使用React、Django、Flask）--完整课程</a></li>
<li><a href="https://www.youtube.com/watch?v=J01rYl9T3BU">PERN Stack 课程 - 仿 Yelp(Postgres、Express、React、Node.js)</a></li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何使用 React、React Hooks 和 Axios 进行 CRUD 操作 ]]>
                </title>
                <description>
                    <![CDATA[ 如果你正在使用React，理解和实现API请求可能是相当困难的。 所以在这篇文章中，我们将通过使用React、React Hooks、React Router和Axios实现CRUD操作来学习这一切。 让我们深入了解一下。 如何安装Node和npm 首先，让我们在系统中安装Node。我们将主要用它来执行我们的JavaScript代码。 去官方网站下载Node，https://nodejs.org/en/。 你还需要node包管理器 ，如npm，它是内建在Node上的。你可以用它来为你的JavaScript应用程序安装包。幸运的是，Node自带npm，所以你不需要单独下载它。 一旦它们都完成了，打开你的终端或命令提示符，输入node -v。这将检查你有哪个版本的Node。 如何创建你的React应用 为了创建你的React应用，在终端输入 npx create-react-app <你的应用程序名称>， 或者本例中的**npx create-react-app**react-crud**。 你会看到软件包正在被安装。 一旦软件包安装完毕，进入项目文件夹，输入npm st ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-perform-crud-operations-using-react/</link>
                <guid isPermaLink="false">645859f60f634b0716652bb9</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React Hooks ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Axios ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Fri, 05 May 2023 11:10:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/05/React-CRUD-Operations-using-React-and-React-Hooks-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/how-to-perform-crud-operations-using-react/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Perform CRUD Operations using React, React Hooks, and Axios</a>
      </p><!--kg-card-begin: markdown--><p>如果你正在使用React，理解和实现API请求可能是相当困难的。</p>
<p>所以在这篇文章中，我们将通过使用React、React Hooks、React Router和Axios实现CRUD操作来学习这一切。</p>
<p>让我们深入了解一下。</p>
<h2 id="nodenpm">如何安装Node和npm</h2>
<p>首先，让我们在系统中安装Node。我们将主要用它来执行我们的JavaScript代码。</p>
<p>去官方网站下载Node，<a href="https://nodejs.org/en/">https://nodejs.org/en/</a>。</p>
<p>你还需要<strong>node包管理器</strong>，如npm，它是内建在Node上的。你可以用它来为你的JavaScript应用程序安装包。幸运的是，Node自带npm，所以你不需要单独下载它。</p>
<p>一旦它们都完成了，打开你的终端或命令提示符，输入<code>node -v</code>。这将检查你有哪个版本的Node。</p>
<h2 id="react">如何创建你的React应用</h2>
<p>为了创建你的React应用，在终端输入 <strong><strong><code>npx create-react-app &lt;你的应用程序名称&gt;</code></strong></strong>， 或者本例中的**<code>npx create-react-app**react-crud</code>**。</p>
<p>你会看到软件包正在被安装。</p>
<p>一旦软件包安装完毕，进入项目文件夹，输入<code>npm start</code>。</p>
<p>你会看到默认的React模板，像这样：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-124754.png" alt="默认的 React Boilerplate 模板" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>默认的 React Boilerplate 模板</figcaption>
</figure>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-124858.png" alt="我们的 App.js文件" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>我们的 App.js文件</figcaption>
</figure>
<h2 id="reactsemanticui">如何为React安装Semantic UI包（库）</h2>
<p>让我们在我们的项目中安装Semantic UI React软件包。Semantic UI是一个为React制作的UI库，它有预建的UI组件，比如表格、按钮和许多功能。</p>
<p>你可以使用下面的一个命令来安装它，这取决于你的包管理器。</p>
<pre><code class="language-bash">yarn add semantic-ui-react semantic-ui-css
</code></pre>
<p>对于使用 Yarn 包管理器</p>
<pre><code class="language-bash">npm install semantic-ui-react semantic-ui-css
</code></pre>
<p>对于使用 NPM 包管理器</p>
<p>同时，在你的应用程序的主入口文件中导入该库，也就是index.js。</p>
<pre><code class="language-js">import 'semantic-ui-css/semantic.min.css'
</code></pre>
<p>在你的index.js文件中粘贴上面一行内容。</p>
<h2 id="crud"><strong>如何构建你的CRUD应用</strong></h2>
<p>现在，让我们开始使用React构建我们的CRUD应用。</p>
<p>首先，我们要给我们的应用程序添加一个标题。</p>
<p>在我们的app.js文件中，添加一个标题，像这样：</p>
<pre><code>import './App.css';

function App() {
  return (
    &lt;div&gt;
      React Crud Operations
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>在我们的应用程序中添加一个标题</p>
<p>现在，让我们确保它居中。</p>
<p>给父级div一个classname，即main。在App.css文件中，我们将使用Flexbox来使标题居中。</p>
<pre><code>import './App.css';

function App() {
  return (
    &lt;div className="main"&gt;
      React Crud Operations
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>app.js文件，在父 div 中的 className 为main的css定义。</p>
<pre><code>.main{
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}
</code></pre>
<p>我们的 app.css 文件</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-130340.png" alt="Screenshot-2021-07-24-130340" width="600" height="400" loading="lazy"></p>
<p>现在我们的标题已经完全居中了。</p>
<p>为了让它看起来更好一些，我们需要使它加粗，并添加一些很酷的字体。要做到这一点，我们将在我们的标题周围使用标题标签，像这样：</p>
<pre><code>import './App.css';

function App() {
  return (
    &lt;div className="main"&gt;
      &lt;h2 className="main-header"&gt;React Crud Operations&lt;/h2&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>让我们从 Google Font导入一种字体. 从 <a href="https://fonts.google.com/">https://fonts.google.com/</a>选择一种。</p>
<p>选择任何你喜欢的字体，但我将使用Montserrat字体家族。</p>
<p>在App.css文件中导入你选择的字体，像这样：</p>
<pre><code>@import url('https://fonts.googleapis.com/css2?family=Montserrat&amp;display=swap');
</code></pre>
<p>现在，让我们改变标题的字体。</p>
<pre><code>&lt;div className="main"&gt;
      &lt;h2 className="main-header"&gt;React Crud Operations&lt;/h2&gt;
    &lt;/div&gt;
</code></pre>
<p>给 <code>h2</code>一个 <code>lassName</code> 为 <code>main-header</code>，就像上面。</p>
<p>然后，在你的 App.css文件，添加font family：</p>
<pre><code>.main-header{
  font-family: 'Montserrat', sans-serif;
}
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-132749.png" alt="Screenshot-2021-07-24-132749" width="600" height="400" loading="lazy"></p>
<p>现在你会看到改变后的标题。</p>
<h2 id="crud">如何创建你的CRUD组件</h2>
<p>让我们创建四个CRUD组件，它们将是创建、读取、更新和删除。</p>
<p>在我们的src文件夹中，创建一个名为组件的文件夹。在这个文件夹中，创建三个文件--创建、读取和更新。对于删除，我们不需要任何额外的组件。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-133242.png" alt="Screenshot-2021-07-24-133242" width="600" height="400" loading="lazy"></p>
<p>现在，让我们来实现这些。</p>
<p>但为此，我们需要使用Mock API。这些API将向我们将要创建的假服务器发送数据，这只是为了学习的目的。</p>
<p>所以，请前往 <a href="https://mockapi.io/">https://mockapi.io/</a> ，创建账号。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-133456.png" alt="MockAPI" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>MockAPI</figcaption>
</figure>
<p>通过点击<code>(plus)加号</code>按钮创建一个项目。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-133553.png" alt="Screenshot-2021-07-24-133553" width="600" height="400" loading="lazy"></p>
<p>点击<code>(plus)加号</code>按钮，创建一个新的项目。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-133702.png" alt="Screenshot-2021-07-24-133702" width="600" height="400" loading="lazy"></p>
<p>添加你的项目名称，然后点击<code>(create)创建</code>按钮。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-133748.png" alt="Screenshot-2021-07-24-133748" width="600" height="400" loading="lazy"></p>
<p>现在，通过点击 <code>(NEW RESOURCE)新资源</code> 按钮创建一个新资源。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-133847.png" alt="Screenshot-2021-07-24-133847" width="600" height="400" loading="lazy"></p>
<p>它将要求你提供<code>(RESOURCE name)资源名称</code>，所以输入你想使用的名称。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-134009.png" alt="Screenshot-2021-07-24-134009" width="600" height="400" loading="lazy"></p>
<p>删除额外的字段，<code>如姓名(name)、头像(avatar)或创建时间(createdAt）</code>，因为我们将不需要这些。然后，点击<code>创建(create)</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-134140.png" alt="Screenshot-2021-07-24-134140" width="600" height="400" loading="lazy"></p>
<p>现在，我们已经创建了我们的 <code>fake API(假API)</code>，我把它命名为fakeData。</p>
<p>点击fakeData，你会看到API在一个新的标签中打开。现在的数据库是空的。</p>
<h2 id="createcomponent">如何为(create Component)创建组件创建一个表格</h2>
<p>让我们使用Semantic UI库中的一个表单。</p>
<p>前往Semantic React，在左边的搜索栏中搜索Form。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-134532.png" alt="Screenshot-2021-07-24-134532" width="600" height="400" loading="lazy"></p>
<p>你会看到一个像这样的表格，点击左上方的 <code>试试(Try it)</code>，就可以得到代码。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-134654.png" alt="Screenshot-2021-07-24-134654" width="600" height="400" loading="lazy"></p>
<p>复制这段代码并将其粘贴到你的Create.js文件中，像这样：</p>
<pre><code>import React from 'react'
import { Button, Checkbox, Form } from 'semantic-ui-react'

const Create = () =&gt; (
    &lt;Form&gt;
        &lt;Form.Field&gt;
            &lt;label&gt;First Name&lt;/label&gt;
            &lt;input placeholder='First Name' /&gt;
        &lt;/Form.Field&gt;
        &lt;Form.Field&gt;
            &lt;label&gt;Last Name&lt;/label&gt;
            &lt;input placeholder='Last Name' /&gt;
        &lt;/Form.Field&gt;
        &lt;Form.Field&gt;
            &lt;Checkbox label='I agree to the Terms and Conditions' /&gt;
        &lt;/Form.Field&gt;
        &lt;Button type='submit'&gt;Submit&lt;/Button&gt;
    &lt;/Form&gt;
)

export default Create;
</code></pre>
<p>在你的app.js文件中导入创建组件(Create Component)。</p>
<pre><code>import './App.css';
import Create from './components/create';

function App() {
  return (
    &lt;div className="main"&gt;
      &lt;h2 className="main-header"&gt;React Crud Operations&lt;/h2&gt;
      &lt;div&gt;
        &lt;Create/&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>
<p>就像这样：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-135249.png" alt="Screenshot-2021-07-24-135249" width="600" height="400" loading="lazy"></p>
<p>但这里有一个问题--项目没有正确对齐，文本输入标签颜色是黑色的。所以，让我们来改变它。</p>
<p>在create.js文件中，给<strong>Form</strong>一个<code>create-form</code>的className。</p>
<pre><code>import React from 'react'
import { Button, Checkbox, Form } from 'semantic-ui-react'

const Create = () =&gt; (
    &lt;Form className="create-form"&gt;
        &lt;Form.Field&gt;
            &lt;label&gt;First Name&lt;/label&gt;
            &lt;input placeholder='First Name' /&gt;
        &lt;/Form.Field&gt;
        &lt;Form.Field&gt;
            &lt;label&gt;Last Name&lt;/label&gt;
            &lt;input placeholder='Last Name' /&gt;
        &lt;/Form.Field&gt;
        &lt;Form.Field&gt;
            &lt;Checkbox label='I agree to the Terms and Conditions' /&gt;
        &lt;/Form.Field&gt;
        &lt;Button type='submit'&gt;Submit&lt;/Button&gt;
    &lt;/Form&gt;
)

export default Create;
</code></pre>
<p>app.js</p>
<p>并在你的App.css文件中添加以下类。</p>
<pre><code>.create-form label{
  color: whitesmoke !important;
  font-family: 'Montserrat', sans-serif;
  font-size: 12px !important;
}
</code></pre>
<p>App.css</p>
<p>这个类将针对所有的表格字段标签并应用白烟的颜色。它还将改变字体并增加字体大小。</p>
<p>现在，在我们的主<code>className</code>中，添加一个flex-direction属性。这个属性将设置方向为列，所以主<code>className</code>中的每个元素都将水平对齐。</p>
<pre><code>.main{
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #212121;
  color: whitesmoke;
  flex-direction: column;
}
</code></pre>
<p>App.css</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-140141.png" alt="Screenshot-2021-07-24-140141" width="600" height="400" loading="lazy"></p>
<p>你可以看到，我们的表单现在看起来好多了。</p>
<p>接下来，让我们从表单字段中获取数据到我们的控制台(console)。为此，我们将使用React的<code>useState</code>钩子。</p>
<p>在我们的create.js文件中，从React中导入<code>useState</code>。</p>
<pre><code>import React, { useState } from 'react';
</code></pre>
<p>然后，为名字(first name)、姓氏(last name)和复选框(checkbox)创建状态。我们将这些状态初始化为空或假。</p>
<pre><code>import React, { useState } from 'react';
import { Button, Checkbox, Form } from 'semantic-ui-react'

export default function Create() {
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [checkbox, setCheckbox] = useState(false);
    return (
        &lt;div&gt;
            &lt;Form className="create-form"&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;First Name&lt;/label&gt;
                    &lt;input placeholder='First Name' /&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;Last Name&lt;/label&gt;
                    &lt;input placeholder='Last Name' /&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;Checkbox label='I agree to the Terms and Conditions' /&gt;
                &lt;/Form.Field&gt;
                &lt;Button type='submit'&gt;Submit&lt;/Button&gt;
            &lt;/Form&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>你可以看到，这现在是作为一个函数组件。所以，我们需要把这个组件改成一个函数组件。这是因为我们只能在函数组件中使用钩子。</p>
<p>现在，让我们分别使用<code>setFirstName</code>、<code>setLastName</code>和<code>setCheckbox</code>属性来设置名字、姓氏和复选框。</p>
<pre><code>&lt;input placeholder='First Name' onChange={(e) =&gt; setFirstName(e.target.value)}/&gt;

&lt;input placeholder='Last Name' onChange={(e) =&gt; setLastName(e.target.value)}/&gt;

&lt;Checkbox label='I agree to the Terms and Conditions' onChange={(e) =&gt; setCheckbox(!checkbox)}/&gt;
</code></pre>
<p>我们正在获得名字(first name)、姓氏(last name)和复选框(checkout)的状态。</p>
<p>创建一个名为<code>postData</code>的函数，我们将用它来向API发送数据。在该函数中，写下这段代码。</p>
<pre><code>const postData = () =&gt; {
        console.log(firstName);
        console.log(lastName);
        console.log(checkbox);
}
</code></pre>
<p>我们在控制台中打印出名字(firstName)、姓氏(lastName)和复选框(checkbox)的值。</p>
<p>在(Submit button)提交按钮上，使用onClick事件调用这个函数，这样，每当我们按下提交按钮，这个函数就会被调用。</p>
<pre><code>&lt;Button onClick={postData} type='submit'&gt;Submit&lt;/Button&gt;
</code></pre>
<p>这里是 <em>create</em> 文件的全部代码。</p>
<pre><code>import React, { useState } from 'react';
import { Button, Checkbox, Form } from 'semantic-ui-react'

export default function Create() {
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [checkbox, setCheckbox] = useState(false);
    const postData = () =&gt; {
        console.log(firstName);
        console.log(lastName);
        console.log(checkbox);
    }
    return (
        &lt;div&gt;
            &lt;Form className="create-form"&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;First Name&lt;/label&gt;
                    &lt;input placeholder='First Name' onChange={(e) =&gt; setFirstName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;Last Name&lt;/label&gt;
                    &lt;input placeholder='Last Name' onChange={(e) =&gt; setLastName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;Checkbox label='I agree to the Terms and Conditions' onChange={(e) =&gt; setCheckbox(!checkbox)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Button onClick={postData} type='submit'&gt;Submit&lt;/Button&gt;
            &lt;/Form&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>在名字和姓氏中输入一些数值，并勾选复选框。然后，点击提交按钮。你会看到控制台中打印出的数据是这样的。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-142717.png" alt="Screenshot-2021-07-24-142717" width="600" height="400" loading="lazy"></p>
<h2 id="axiosmockapis">如何使用Axios向Mock APIs发送请求</h2>
<p>让我们使用Axios来发送我们的表单数据到模拟服务器。</p>
<p>但首先，我们需要安装它。</p>
<p>只要输入<code>npm i axios</code>来安装这个包。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-174213.png" alt="Screenshot-2021-07-24-174213" width="600" height="400" loading="lazy"></p>
<p>软件包安装完毕后，让我们开始(create)创建操作。</p>
<p>在文件的顶部导入Axios。</p>
<pre><code>import axios from 'axios';
</code></pre>
<p>导入Axios</p>
<p>在<code>postData</code>函数中，我们将使用Axios来发送POST请求。</p>
<pre><code>const postData = () =&gt; {
        axios.post(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData`, {
            firstName,
            lastName,
            checkbox
        })
    }
</code></pre>
<p>发送 Post 请求</p>
<p>如你所见，我们正在使用axios.post。在axios.post中, 我们有 API endpoint(接入点 请求地址), 这是我们之前创建的。然后，我们有被大括号包裹的表单字段。</p>
<p>当我们点击提交时，这个函数将被调用，它将向API服务器发布数据。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-174834.png" alt="Screenshot-2021-07-24-174834" width="600" height="400" loading="lazy"></p>
<p>输入你的名字(first name)，姓氏(last name)，并勾选复选框。点击提交。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-174930.png" alt="Screenshot-2021-07-24-174930" width="600" height="400" loading="lazy"></p>
<p>然后，你检查这个API的返回值，你会得到你的名字、姓氏，复选框为真的值，被包裹在一个对象中。</p>
<h2 id="">如何实现读取和更新操作</h2>
<p>为了开始(read)读取操作，我们需要创建一个读取页面。我们还需要React Router包来导航到不同的页面。</p>
<p>前往<a href="https://reactrouter.com/web/guides/quick-start">https://reactrouter.com/web/guides/quick-start</a>查看文档，同时运行 <code>npm i react-router-dom</code>进行安装。</p>
<p>安装完毕后，从React Router导入一些东西：</p>
<pre><code>import { BrowserRouter as Router, Route } from 'react-router-dom'
</code></pre>
<p>从<code>React Router</code>中导入<code>Router</code>和<code>Route</code>。</p>
<p>在我们的App.js中，把整个返回包成一个Router。这基本上意味着，无论这个Router里面有什么，都能在React中使用。</p>
<pre><code>import './App.css';
import Create from './components/create';
import { BrowserRouter as Router, Route } from 'react-router-dom'

function App() {
  return (
    &lt;Router&gt;
      &lt;div className="main"&gt;
        &lt;h2 className="main-header"&gt;React Crud Operations&lt;/h2&gt;
        &lt;div&gt;
          &lt;Create /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/Router&gt;
  );
}

export default App;
</code></pre>
<p>我们的App.js现在看起来会像上面的样子。</p>
<p>替换掉返回里面的Create，并添加以下代码。</p>
<pre><code>import './App.css';
import Create from './components/create';
import { BrowserRouter as Router, Route } from 'react-router-dom'

function App() {
  return (
    &lt;Router&gt;
      &lt;div className="main"&gt;
        &lt;h2 className="main-header"&gt;React Crud Operations&lt;/h2&gt;
        &lt;div&gt;
          &lt;Route exact path='/create' component={Create} /&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/Router&gt;
  );
}

export default App;
</code></pre>
<p>在这里，我们使用Route组件作为Create。我们已经将Create的路径设置为'/create'。因此，如果我们进入<a href="http://localhost:3000/create">http://localhost:3000/create</a> ，我们将看到创建页面。</p>
<p>同样地，我们需要(read)读取和(update)更新的路由。</p>
<pre><code>import './App.css';
import Create from './components/create';
import Read from './components/read';
import Update from './components/update';
import { BrowserRouter as Router, Route } from 'react-router-dom'

function App() {
  return (
    &lt;Router&gt;
      &lt;div className="main"&gt;
        &lt;h2 className="main-header"&gt;React Crud Operations&lt;/h2&gt;
        &lt;div&gt;
          &lt;Route exact path='/create' component={Create} /&gt;
        &lt;/div&gt;
        &lt;div style={{ marginTop: 20 }}&gt;
          &lt;Route exact path='/read' component={Read} /&gt;
        &lt;/div&gt;

        &lt;Route path='/update' component={Update} /&gt;
      &lt;/div&gt;
    &lt;/Router&gt;
  );
}

export default App;
</code></pre>
<p>因此，(read)读取和(update)更新路由，类似你上面看到的。</p>
<p>如果你前往 <a href="http://localhost:3000/read">http://localhost:3000/read</a>，会看到下面的：</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-180318.png" alt="Read Route" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>Read Route</figcaption>
</figure>
<p>在<a href="http://localhost:3000/update">http://localhost:3000/update</a> 网址，我们可以看到更新组件（Update Component），像这样：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-180440.png" alt="Screenshot-2021-07-24-180440" width="600" height="400" loading="lazy"></p>
<h3 id="">读取操作</h3>
<p>对于读取操作，我们将需要一个表组件。因此，前往React Semantic UI，并使用库中的一个表。</p>
<pre><code>import React from 'react';
import { Table } from 'semantic-ui-react'
export default function Read() {
    return (
        &lt;div&gt;
            &lt;Table singleLine&gt;
                &lt;Table.Header&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.HeaderCell&gt;Name&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;Registration Date&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;E-mail address&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;Premium Plan&lt;/Table.HeaderCell&gt;
                    &lt;/Table.Row&gt;
                &lt;/Table.Header&gt;

                &lt;Table.Body&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.Cell&gt;John Lilki&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;September 14, 2013&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;jhlilk22@yahoo.com&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;No&lt;/Table.Cell&gt;
                    &lt;/Table.Row&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.Cell&gt;Jamie Harington&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;January 11, 2014&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;jamieharingonton@yahoo.com&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;Yes&lt;/Table.Cell&gt;
                    &lt;/Table.Row&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.Cell&gt;Jill Lewis&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;May 11, 2014&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;jilsewris22@yahoo.com&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;Yes&lt;/Table.Cell&gt;
                    &lt;/Table.Row&gt;
                &lt;/Table.Body&gt;
            &lt;/Table&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>Read.js</p>
<p>在这里，你可以看到我们有一个带有一些假数据(dummy data)的表。但我们只需要一个表行。所以，让我们删除其他的。</p>
<pre><code>import React from 'react';
import { Table } from 'semantic-ui-react'
export default function Read() {
    return (
        &lt;div&gt;
            &lt;Table singleLine&gt;
                &lt;Table.Header&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.HeaderCell&gt;Name&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;Registration Date&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;E-mail address&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;Premium Plan&lt;/Table.HeaderCell&gt;
                    &lt;/Table.Row&gt;
                &lt;/Table.Header&gt;

                &lt;Table.Body&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.Cell&gt;John Lilki&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;September 14, 2013&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;jhlilk22@yahoo.com&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;No&lt;/Table.Cell&gt;
                    &lt;/Table.Row&gt;
                &lt;/Table.Body&gt;
            &lt;/Table&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>Read.js</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-182905.png" alt="Screenshot-2021-07-24-182905" width="600" height="400" loading="lazy"></p>
<p>这是“阅读”页面的输出。我们有一个有四列的表，但我们只需要三列。</p>
<p>删除多余的字段列，并像这样重新命名字段。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-183105.png" alt="Screenshot-2021-07-24-183105" width="600" height="400" loading="lazy"></p>
<p>这就是我们的阅读页面现在的样子：</p>
<pre><code>import React from 'react';
import { Table } from 'semantic-ui-react'
export default function Read() {
    return (
        &lt;div&gt;
            &lt;Table singleLine&gt;
                &lt;Table.Header&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.HeaderCell&gt;First Name&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;Last Name&lt;/Table.HeaderCell&gt;
                        &lt;Table.HeaderCell&gt;Checked&lt;/Table.HeaderCell&gt;
                    &lt;/Table.Row&gt;
                &lt;/Table.Header&gt;

                &lt;Table.Body&gt;
                    &lt;Table.Row&gt;
                        &lt;Table.Cell&gt;Nishant&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;Kumar&lt;/Table.Cell&gt;
                        &lt;Table.Cell&gt;Yes&lt;/Table.Cell&gt;
                    &lt;/Table.Row&gt;
                &lt;/Table.Body&gt;
            &lt;/Table&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>Read.js</p>
<p>现在，让我们发送GET请求，从API获得数据。</p>
<p>当我们的应用程序加载时，我们需要这些数据。所以，我们要使用<code>useEffect</code>钩子(hook)。</p>
<pre><code>import React, { useEffect } from 'react';

 useEffect(() =&gt; {
       
 }, [])
</code></pre>
<p>useEffect钩子(hook)</p>
<p>创建一个包含传入数据的状态。这将是一个数组。</p>
<pre><code>import React, { useEffect, useState } from 'react';

const [APIData, setAPIData] = useState([]);
useEffect(() =&gt; {
       
}, [])
</code></pre>
<p>APIData state 来存储API传入的数据</p>
<p>在<code>useEffect</code>钩子(hook)中，让我们发送GET请求。</p>
<pre><code> useEffect(() =&gt; {
        axios.get(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData`)
            .then((response) =&gt; {
                setAPIData(response.data);
            })
    }, [])
</code></pre>
<p>因此，我们使用axios.get来向API发送GET请求。然后，如果请求被满足，我们就在我们的_APIData_状态中设置响应数据。</p>
<p>现在，让我们根据API数据来映射我们的表行。</p>
<p>我们将使用Map函数来做这件事。它将对数组进行迭代，并在输出中显示数据。</p>
<pre><code>&lt;Table.Body&gt;
  {APIData.map((data) =&gt; {
     return (
       &lt;Table.Row&gt;
          &lt;Table.Cell&gt;{data.firstName}&lt;/Table.Cell&gt;
           &lt;Table.Cell&gt;{data.lastName}&lt;/Table.Cell&gt;
           &lt;Table.Cell&gt;{data.checkbox ? 'Checked' : 'Unchecked'}&lt;/Table.Cell&gt;
        &lt;/Table.Row&gt;
   )})}
&lt;/Table.Body&gt;
</code></pre>
<p>我们根据API中的数据来映射firstName、lastName和checkbox。但我们的复选框有一点不同。我在这里使用了一个三元操作符（'?'）。如果data.checkbox为真，输出将是Checked，否则将是Unchecked。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-184955.png" alt="Read.js 输出" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>Read.js 输出</figcaption>
</figure>
<h3 id="update">更新(Update)操作</h3>
<p>再为更新创建一个标题，并在表行中为更新按钮创建一列。使用Semantic UI React的按钮。</p>
<pre><code>&lt;Table.HeaderCell&gt;Update&lt;/Table.HeaderCell&gt;

&lt;Table.Cell&gt; 
  &lt;Button&gt;Update&lt;/Button&gt;
&lt;/Table.Cell&gt;
</code></pre>
<p>创建(Update)更新按钮</p>
<p>现在，当我们点击这个按钮，我们应该被重定向到更新页面。为此，我们需要React Router的链接。</p>
<p>从React Router导入Link。并将更新按钮的表格单元格包装成Link标签。</p>
<pre><code>import { Link } from 'react-router-dom';

&lt;Link to='/update'&gt;
  &lt;Table.Cell&gt; 
     &lt;Button&gt;Update&lt;/Button&gt;
   &lt;/Table.Cell&gt;
&lt;/Link&gt;
</code></pre>
<p>为更新按钮添加链接(Link)</p>
<p>因此，如果我们点击更新按钮，我们将被重定向到更新页面。</p>
<p>为了更新列的数据，我们需要它们各自的ID，这从APIs获得。</p>
<p>创建一个名为 "setData "的函数。将其绑定到更新按钮上。</p>
<pre><code> &lt;Button onClick={() =&gt; setData()}&gt;Update&lt;/Button&gt;
</code></pre>
<p>现在，我们需要将数据作为参数传递给上面的函数。</p>
<pre><code> &lt;Button onClick={() =&gt; setData(data)}&gt;Update&lt;/Button&gt;
</code></pre>
<p>并在上面的的函数中，在控制台中打印这些数据。</p>
<pre><code>const setData = (data) =&gt; {
   console.log(data);
}
</code></pre>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-190515.png" alt="控制台的数据" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>控制台的数据</figcaption>
</figure>
<p>点击表中的更新按钮，并查看控制台。你会得到相应表字段的数据。</p>
<p>让我们把这些数据设置到localStorage中。</p>
<pre><code>const setData = (data) =&gt; {
        let { id, firstName, lastName, checkbox } = data;
        localStorage.setItem('ID', id);
        localStorage.setItem('First Name', firstName);
        localStorage.setItem('Last Name', lastName);
        localStorage.setItem('Checkbox Value', checkbox)
}
</code></pre>
<p>在本地存储中(Local Storage)设置数据</p>
<p>我们正在将我们的数据解构为id、firstName、lastName和checkbox，然后我们将这些数据设置到本地存储(Local Storage)。你可以使用本地存储(Local Storage)来在浏览器中的存储数据。</p>
<p>现在，在更新组件中，我们需要一个表单来进行更新操作。让我们复制(reate component)创建组件中的表单。只要把函数的名称从Create改为Update。</p>
<pre><code>import React, { useState } from 'react';
import { Button, Checkbox, Form } from 'semantic-ui-react'
import axios from 'axios';

export default function Update() {
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [checkbox, setCheckbox] = useState(false);

    return (
        &lt;div&gt;
            &lt;Form className="create-form"&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;First Name&lt;/label&gt;
                    &lt;input placeholder='First Name' onChange={(e) =&gt; setFirstName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;Last Name&lt;/label&gt;
                    &lt;input placeholder='Last Name' onChange={(e) =&gt; setLastName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;Checkbox label='I agree to the Terms and Conditions' onChange={(e) =&gt; setCheckbox(!checkbox)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Button type='submit'&gt;Update&lt;/Button&gt;
            &lt;/Form&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>我们的更新页面</p>
<p>在Update组件中创建一个`useEffect'钩子(hook)。我们将用它来获取我们之前存储在本地存储的数据。同时，为ID字段再创建一个状态(state)。</p>
<pre><code>const [id, setID] = useState(null);

useEffect(() =&gt; {
        setID(localStorage.getItem('ID'))
        setFirstName(localStorage.getItem('First Name'));
        setLastName(localStorage.getItem('Last Name'));
        setCheckbox(localStorage.getItem('Checkbox Value'))
}, []);
</code></pre>
<p>根据你的keys(字典，map 数据结构)从本地存储设置相应的数据。我们需要在表格字段中设置这些值。当更新页面加载时，它将自动填入这些字段。</p>
<pre><code>&lt;Form className="create-form"&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;First Name&lt;/label&gt;
                    &lt;input placeholder='First Name' value={firstName} onChange={(e) =&gt; setFirstName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;Last Name&lt;/label&gt;
                    &lt;input placeholder='Last Name' value={lastName} onChange={(e) =&gt; setLastName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;Checkbox label='I agree to the Terms and Conditions' checked={checkbox} onChange={(e) =&gt; setCheckbox(!checkbox)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Button type='submit'&gt;Update&lt;/Button&gt;
            &lt;/Form&gt;
</code></pre>
<p>设置表格字段的值</p>
<p>现在，如果我们点击阅读页面中的更新按钮，我们将被重定向到更新页面，在那里我们将看到所有根据数据自动填充的表单。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-193521.png" alt="更新页面" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>更新页面</figcaption>
</figure>
<p>现在，让我们创建更新请求来更新数据。</p>
<p>创建一个名为<code>updateAPIData</code>的函数。在这个函数中，我们将使用axios.put来发送一个PUT请求，以更新我们的数据。</p>
<pre><code>const updateAPIData = () =&gt; {
    axios.put(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData/${id}`, {
        firstName,
         lastName,
         checkbox
	})
}
</code></pre>
<p>在这里，你可以看到我们在API端点上附加了一个id字段。</p>
<p>当我们点击表中的字段时，它的ID会被存储到本地存储器(Local Storage)中。而在更新页面，我们正在检索它。然后，我们将该ID存储在_<code>id</code>_状态中。</p>
<p>之后，我们将ID传递给端点。这使我们能够更新我们传递ID的字段。</p>
<p>将<code>updateAPIData</code>函数绑定到更新按钮上。</p>
<pre><code>&lt;Button type='submit' onClick={updateAPIData}&gt;Update&lt;/Button&gt;
</code></pre>
<p>将updateAPIData绑定到更新按钮上</p>
<p>点击读取页面中表格的更新按钮，改变你的姓氏（last name），然后点击更新页面中的更新按钮。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-194627.png" alt="更新字段" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>更新字段</figcaption>
</figure>
<p>回到“阅读”页面，或查看API。你会看到你的姓氏(last name)已被改变。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-194756.png" alt="The Mock API" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>The Mock API</figcaption>
</figure>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-194822.png" alt="我们的读取表格" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>我们的读取表格</figcaption>
</figure>
<h3 id="">删除操作</h3>
<p>在读取表(read table)中再添加一个Button，我们将用它来进行删除操作。</p>
<pre><code>&lt;Table.Cell&gt;
   &lt;Button onClick={() =&gt; onDelete(data.id)}&gt;Delete&lt;/Button&gt;
&lt;/Table.Cell&gt;
</code></pre>
<p>读取表中的删除按钮</p>
<p>创建一个名为 "onDelete "的函数，并将此函数绑定到删除按钮上。这个函数将在点击删除按钮时接收一个ID参数。</p>
<pre><code>const onDelete = (id) =&gt; {

}
</code></pre>
<p>The Delete Function</p>
<p>We are going to use axios.delete to delete the respective columns.</p>
<pre><code>const onDelete = (id) =&gt; {
  axios.delete(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData/${id}`)
}
</code></pre>
<p>从API中删除字段</p>
<p>点击删除按钮并检查API。你会看到数据已经被删除。</p>
<p>我们需要在表被删除后获得该表的数据。</p>
<p>因此，创建一个函数来获得API数据。</p>
<pre><code>const getData = () =&gt; {
    axios.get(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData`)
        .then((getData) =&gt; {
             setAPIData(getData.data);
         })
}
</code></pre>
<p>获取API数据</p>
<p>现在，在<code>onDelete</code>函数中，我们需要在删除一个字段后获得更新的数据。</p>
<pre><code>const onDelete = (id) =&gt; {
        axios.delete(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData/${id}`)
     .then(() =&gt; {
        getData();
    })
}
</code></pre>
<p>删除一个字段后获得更新数据</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-201047.png" alt="读取表格" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>读取表格</figcaption>
</figure>
<p>因此，现在如果我们在任何字段上点击Delete，它将删除该字段并自动刷新表格。</p>
<figure class="kg-card kg-card-image kg-card-hascaption">
    <img src="https://www.freecodecamp.org/news/content/images/2021/07/Screenshot-2021-07-24-201423.png" alt="删除一个字段后读表" class="kg-image" width="600" height="400" loading="lazy">
    <figcaption>删除一个字段后读表</figcaption>
</figure>
<h2 id="crud">让我们对我们的CRUD应用程序做一些改进吧</h2>
<p>因此，当我们在Create页面发布我们的数据时，我们只是在模拟(mock)数据库中获得数据。当我们的数据在创建页面中被创建时，我们需要重定向到读取页面。</p>
<p>从React Router导入`useHistory'。</p>
<pre><code>import { useHistory } from 'react-router';
</code></pre>
<p>从React Router导入useHistory</p>
<p>创建变量<code>history</code>使用 <code>let</code>。</p>
<pre><code>let history = useHistory();
</code></pre>
<p>然后，使用history.push函数，在post API被调用后推送到阅读页面。</p>
<pre><code>const postData = () =&gt; {
        axios.post(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData`, {
            firstName,
            lastName,
            checkbox
        }).then(() =&gt; {
            history.push('/read')
        })
    }
</code></pre>
<p>在发布API成功后推送到阅读页面</p>
<p>它将使用<code>useHistory</code>钩子(hook)推送到阅读页面。</p>
<p>对更新页面做同样的处理。</p>
<pre><code>import React, { useState, useEffect } from 'react';
import { Button, Checkbox, Form } from 'semantic-ui-react'
import axios from 'axios';
import { useHistory } from 'react-router';

export default function Update() {
    let history = useHistory();
    const [id, setID] = useState(null);
    const [firstName, setFirstName] = useState('');
    const [lastName, setLastName] = useState('');
    const [checkbox, setCheckbox] = useState(false);

    useEffect(() =&gt; {
        setID(localStorage.getItem('ID'))
        setFirstName(localStorage.getItem('First Name'));
        setLastName(localStorage.getItem('Last Name'));
        setCheckbox(localStorage.getItem('Checkbox Value'));
    }, []);

    const updateAPIData = () =&gt; {
        axios.put(`https://60fbca4591156a0017b4c8a7.mockapi.io/fakeData/${id}`, {
            firstName,
            lastName,
            checkbox
        }).then(() =&gt; {
            history.push('/read')
        })
    }
    return (
        &lt;div&gt;
            &lt;Form className="create-form"&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;First Name&lt;/label&gt;
                    &lt;input placeholder='First Name' value={firstName} onChange={(e) =&gt; setFirstName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;label&gt;Last Name&lt;/label&gt;
                    &lt;input placeholder='Last Name' value={lastName} onChange={(e) =&gt; setLastName(e.target.value)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Form.Field&gt;
                    &lt;Checkbox label='I agree to the Terms and Conditions' checked={checkbox} onChange={() =&gt; setCheckbox(!checkbox)}/&gt;
                &lt;/Form.Field&gt;
                &lt;Button type='submit' onClick={updateAPIData}&gt;Update&lt;/Button&gt;
            &lt;/Form&gt;
        &lt;/div&gt;
    )
}
</code></pre>
<p>Update.js</p>
<p>现在你知道如何使用React和React Hooks进行CRUD操作了吧！</p>
<p>另外，如果你想补充学习，你可以观看我在Youtube上的[React CRUD操作视频]（<a href="https://youtu.be/-ZMP8ZladIQ%EF%BC%89%E3%80%82">https://youtu.be/-ZMP8ZladIQ）。</a></p>
<blockquote>
<p><strong>学习愉快</strong></p>
</blockquote>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 边缘云微服务——如何利用 WasmEdge 和 Rust 构建高性能和安全的应用程序 ]]>
                </title>
                <description>
                    <![CDATA[ 边缘云允许开发者在靠近用户的地方部署微服务（即细粒度的网络服务）。这为他们提供了更好的用户体验（和非常快的响应时间）、安全和高可用性。 它还利用本地甚至私人数据中心、CDN 网络和电信数据中心（例如 5G MECs）来提供计算服务。 边缘云的成功例子包括 Cloudflare、Fastly、Akamai、fly.io、Vercel、Netlify 等等。 但与大型公有云相比，边缘云也是一个资源受限的环境。如果边缘微服务本身很慢或很臃肿或不安全，它们将违背在边缘云上部署的整个目的。 在这篇文章中，我将向你展示如何在 WebAssembly 沙盒 [https://github.com/WasmEdge]中创建轻量级和高性能的 Web 服务，然后将它们免费部署在边缘云提供商 fly.io [http://fly.io]。 Fly.io [http://Fly.io] 是一家在边缘云上提供虚拟机服务的领先供应商。它在世界各地都有边缘数据中心。fly.io [http://Fly.io] 的虚拟机支持应用服务器、数据库，以及在我们的案例中支持微服务的轻量级运行时。 我将使用 Was ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/edge-cloud-microservices-with-wasmedge-and-rust/</link>
                <guid isPermaLink="false">63f2fb60cf34b8063af88de8</guid>
                
                    <category>
                        <![CDATA[ Rust ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Thu, 16 Feb 2023 04:40:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/02/richard-r-schunemann-DD3VNthK_Kw-unsplash.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/edge-cloud-microservices-with-wasmedge-and-rust/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">Edge Cloud Microservices – How to Build High Performance &amp; Secure Apps with WasmEdge and Rust</a>
      </p><!--kg-card-begin: markdown--><p>边缘云允许开发者在靠近用户的地方部署微服务（即细粒度的网络服务）。这为他们提供了更好的用户体验（和非常快的响应时间）、安全和高可用性。</p>
<p>它还利用本地甚至私人数据中心、CDN 网络和电信数据中心（例如 5G MECs）来提供计算服务。</p>
<p>边缘云的成功例子包括 Cloudflare、Fastly、Akamai、fly.io、Vercel、Netlify 等等。</p>
<p>但与大型公有云相比，边缘云也是一个资源受限的环境。如果边缘微服务本身很慢或很臃肿或不安全，它们将违背在边缘云上部署的整个目的。</p>
<p>在这篇文章中，我将向你展示如何在 <a href="https://github.com/WasmEdge">WebAssembly 沙盒</a>中创建轻量级和高性能的 Web 服务，然后将它们免费部署在边缘云提供商 <a href="http://fly.io">fly.io</a>。</p>
<p><a href="http://Fly.io">Fly.io</a> 是一家在边缘云上提供虚拟机服务的领先供应商。它在世界各地都有边缘数据中心。<a href="http://Fly.io">fly.io</a> 的虚拟机支持应用服务器、数据库，以及在我们的案例中支持微服务的轻量级运行时。</p>
<p>我将使用 <a href="https://github.com/WasmEdge/WasmEdge">WasmEdge Runtime</a> 作为这些微服务的安全沙盒。WasmEdge 是一个专门为云原生服务优化的 WebAssembly 运行时。</p>
<p>我们将把用 Rust 或 JavaScript 编写的微服务应用打包在<a href="https://hub.docker.com/u/wasmedge">基于 WasmEdge 的 Docker 镜像</a>中。</p>
<p>这种方法有几个引人注目的优势：</p>
<ul>
<li>WasmEdge 以接近原生的速度运行沙盒应用程序。根据一项同行评议的研究，WasmEdge 运行 Rust 程序的速度几乎与 Linux 运行本地机器代码的速度相同。</li>
<li>WasmEdge 是一个高度安全的运行时。它可以保护你的应用程序免受外部和内部威胁。</li>
<li>WasmEdge 运行时的攻击面比普通的 Linux 操作系统运行时大大减少。</li>
<li>由于 WebAssembly 沙盒只能访问明确声明的资源，软件供应链攻击的风险大大降低。</li>
<li>WasmEdge 提供了一个完整的和可移植的应用程序运行环境，其内存占用只有标准 Linux 操作系统运行时镜像的 1/10。</li>
<li>WasmEdge 运行时是跨平台的。这意味着开发和部署的机器不必是相同的。而一旦你创建了一个 WasmEdge 应用程序，你可以将它部署到任何支持 WasmEdge 的地方，包括 <a href="http://fly.io">fly.io</a> 基础设施。</li>
</ul>
<p>如果应用程序很复杂，则性能优势会被放大。例如，WasmEdge 上的 AI 人工智能应用程序不需要安装 Python。 WasmEdge 上的 node.js 应用程序不需要安装 Node.js 和 v8。</p>
<p>在本文的其余部分，我将演示如何运行：</p>
<ul>
<li>一个异步的 HTTP 服务器（用 Rust 实现）</li>
<li>一个非常快速的图像分类网络服务（用 Rust 实现），以及</li>
<li>一个 node.JS 网络服务器</li>
<li>具有数据库连接的有状态微服务</li>
</ul>
<p>所有这些都在 WasmEdge 中快速、安全地运行，而消耗的资源是普通 Linux 容器的 1/10。</p>
<h3 id="">环境准备</h3>
<p>首先，如果你的系统中已经安装了 Docker 工具，那就太好了。如果没有，请按照<a href="https://www.freecodecamp.org/chinese/news/the-docker-handbook/">本手册的第一节</a> 现在就安装 Docker。然后我们将使用在线安装程序来安装 WasmEdge、Rust 和 [fly.io] 的 <code>flyctl</code> 工具（<a href="http://fly.io">http://fly.io</a>）。</p>
<p>安装 WasmEdge。<a href="https://wasmedge.org/book/en/start/install.html">详见这里</a>。</p>
<pre><code class="language-bash">curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash -s -- -e all
</code></pre>
<p>安装 Rust。<a href="https://www.rust-lang.org/tools/install">详见这里</a>。</p>
<pre><code class="language-bash">curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
</code></pre>
<p>为 fly.io 安装 <code>flyctl</code> 工具（<a href="http://fly.io">http://fly.io</a> ）。<a href="https://fly.io/docs/hands-on/install-flyctl/">详见这里</a>。</p>
<pre><code class="language-bash">curl -L https://fly.io/install.sh | sh
</code></pre>
<p>一旦你安装了<code>flyctl</code>，按照指示在 <a href="http://fly.io">fly.io</a> 上<a href="https://fly.io/docs/hands-on/sign-up/">注册一个免费账户</a>。现在你已经准备好在边缘云上部署网络服务了。</p>
<h2 id="rust">用 Rust 实现的一个简单的微服务</h2>
<p>我们的第一个例子是一个用 Rust 编写的简单 HTTP 服务。它展示了一个现代网络应用，可以扩展到支持任意复杂的业务逻辑。</p>
<p>基于流行的 tokio 和 hyper crates，这个微服务是快速的、异步的（非阻塞的），并且对开发者来说非常容易创建。</p>
<p>完全静态链接的 WasmEdge 镜像只有 4MB，而基本的 Linux 镜像是 40MB。这足以运行一个用 Rust 的 tokio 和 hyper 框架编写的异步 HTTP 服务。</p>
<p>运行以下两个 CLI 命令，从我们为 WasmEdge 设计的超薄（slim） Docker 镜像中创建并部署一个 <a href="http://fly.io">fly.io</a> 应用程序。</p>
<pre><code class="language-bash">$ flyctl launch --image juntaoyuan/flyio-echo
$ flyctl deploy
</code></pre>
<p>为什么这样做？你可以使用 curl 命令来测试所部署的网络服务是否真的工作。无论你向它发布什么数据，它都会回显（echoes back）出来。</p>
<pre><code class="language-bash">$ curl https://proud-sunset-3795.fly.dev/echo -d "Hello WasmEdge on fly.io!"
Hello WasmEdge on fly.io!
</code></pre>
<p><code>juntaoyuan/flyio-echo</code> Docker 镜像的 Docker 文件包含 WasmEdge 运行时的完整包和自定义网络应用<code>wasmedge_hyper_server.wasm</code>。</p>
<pre><code class="language-Dockerfile">FROM wasmedge/slim-runtime:0.11.0
ADD wasmedge_hyper_server.wasm /
CMD ["wasmedge", "--dir", ".:/", "/wasmedge_hyper_server.wasm"]
</code></pre>
<p>构建<code>wasmedge_hyper_server.wasm</code>应用程序的 Rust 源代码项目，<a href="https://github.com/WasmEdge/wasmedge_hyper_demo/tree/main/server">可在 GitHub 上找到</a>。它使用 tokio API 来启动一个 HTTP 服务器。</p>
<p>当服务器收到一个请求时，它委托给 <code>echo()</code> 函数来异步处理该请求。这使得微服务可以接受并处理多个并发的 HTTP 请求。</p>
<pre><code class="language-Rust">#[tokio::main(flavor = "current_thread")]
async fn main() -&gt; Result&lt;(), Box&lt;dyn std::error::Error + Send + Sync&gt;&gt; {
    let addr = SocketAddr::from(([0, 0, 0, 0], 8080));

    let listener = TcpListener::bind(addr).await?;
    println!("Listening on http://{}", addr);
    loop {
        let (stream, _) = listener.accept().await?;

        tokio::task::spawn(async move {
            if let Err(err) = Http::new().serve_connection(stream, service_fn(echo)).await {
                println!("Error serving connection: {:?}", err);
            }
        });
    }
}
</code></pre>
<p>异步的 <code>echo()</code> 函数如下。它利用 hyper 提供的 HTTP API 来解析请求并生成响应。这里，响应只是请求数据体。</p>
<pre><code class="language-Rust">async fn echo(req: Request&lt;Body&gt;) -&gt; Result&lt;Response&lt;Body&gt;, hyper::Error&gt; {
    match (req.method(), req.uri().path()) {
        ... ...
        (&amp;Method::POST, "/echo") =&gt; Ok(Response::new(req.into_body())),
        ... ...

        // Return the 404 Not Found for other routes.
        _ =&gt; {
            let mut not_found = Response::default();
            *not_found.status_mut() = StatusCode::NOT_FOUND;
            Ok(not_found)
        }
    }
}
</code></pre>
<p>现在，让我们在基本的微服务中添加一些令人印象深刻的东西吧！</p>
<h2 id="rustai">用 Rust 实现的 AI 人工智能的微服务</h2>
<p>在这个例子中，我们将创建一个用于图像分类的网络服务。它通过一个 Tensorflow Lite 模型处理上传的图像。</p>
<p>我们将使用 WasmEdge 的 Rust API 来访问 Tensorflow，而不是创建一个复杂（和臃肿）的 Python 程序，它以完整的本地机器码速度运行推理任务（例如，如果有的话，利用 GPU 硬件）。</p>
<p>通过 WASI-NN 标准，WasmEdge 的 Rust API 可以使用 Tensorflow、PyTorch、OpenVINO 和其他 AI 框架中的 AI 模型。</p>
<p>对于包含完整 Tensorflow Lite 依赖项的 AI 推理应用程序，WasmEdge 占用空间小于 115MB。 相比之下，标准 Tensorflow Linux 映像超过 400MB。</p>
<p>运行以下两个 CLI 命令，从我们用于 WasmEdge + Tensorflow 的超薄 Docker 镜像创建并部署一个 <a href="http://fly.io">fly.io</a> 应用程序。</p>
<pre><code class="language-bash">$ flyctl launch --image juntaoyuan/flyio-classify
$ flyctl deploy
</code></pre>
<p>就是这样！你可以使用 curl 命令来测试部署的 Web 服务是否真正起作用。它返回具有置信度（confidence level）的图像分类结果。</p>
<pre><code class="language-bash">$ curl https://silent-glade-6853.fly.dev/classify -X POST --data-binary "@grace_hopper.jpg"
military uniform is detected with 206/255 confidence
</code></pre>
<p><code>juntaoyuan/flyio-classify</code> Docker 镜像的 Dockerfile 包含 WasmEdge 运行时的完整包、整个 Tensorflow 库及其依赖项，以及自定义 Web 应用程序 <code>wasmedge_hyper_server_tflite.wasm</code>。</p>
<pre><code class="language-Dockerfile">FROM wasmedge/slim-tf:0.11.0
ADD wasmedge_hyper_server_tflite.wasm /
CMD ["wasmedge-tensorflow-lite", "--dir", ".:/", "/wasmedge_hyper_server_tflite.wasm"]
</code></pre>
<p>构建<code>wasmedge_hyper_server_tflite.wasm</code>应用程序的 Rust 源代码项目，<a href="https://github.com/WasmEdge/wasmedge_hyper_demo/tree/main/server-tflite">可在 GitHub 上找到</a>。基于 tokio 的异步 HTTP 服务器在异步<code>main()</code>函数中，与前面的例子一样。</p>
<p><code>classify()</code>函数处理请求中的图像数据，将图像变成张量，运行 Tensorflow 模型，然后将返回值（在张量中）变成文本标签和可能分类的概率。</p>
<pre><code class="language-Rust">async fn classify(req: Request&lt;Body&gt;) -&gt; Result&lt;Response&lt;Body&gt;, hyper::Error&gt; {
    let model_data: &amp;[u8] = include_bytes!("models/mobilenet_v1_1.0_224/mobilenet_v1_1.0_224_quant.tflite");
    let labels = include_str!("models/mobilenet_v1_1.0_224/labels_mobilenet_quant_v1_224.txt");
    match (req.method(), req.uri().path()) {

        (&amp;Method::POST, "/classify") =&gt; {
            let buf = hyper::body::to_bytes(req.into_body()).await?;
            let flat_img = wasmedge_tensorflow_interface::load_jpg_image_to_rgb8(&amp;buf, 224, 224);

            let mut session = wasmedge_tensorflow_interface::Session::new(&amp;model_data, wasmedge_tensorflow_interface::ModelType::TensorFlowLite);
            session.add_input("input", &amp;flat_img, &amp;[1, 224, 224, 3])
                .run();
            let res_vec: Vec&lt;u8&gt; = session.get_output("MobilenetV1/Predictions/Reshape_1");
            ... ...

            let mut label_lines = labels.lines();
            for _i in 0..max_index {
              label_lines.next();
            }
            let class_name = label_lines.next().unwrap().to_string();

            Ok(Response::new(Body::from(format!("{} is detected with {}/255 confidence", class_name, max_value))))
        }

        // Return the 404 Not Found for other routes.
        _ =&gt; {
            let mut not_found = Response::default();
            *not_found.status_mut() = StatusCode::NOT_FOUND;
            Ok(not_found)
        }
    }
}
</code></pre>
<p>在本文的最后一节，我们将讨论如何为 Rust 微服务添加更多的功能，如数据库客户端和 Web 服务客户端。</p>
<h2 id="nodejs">用 Node.js 实现的一个简单的微服务</h2>
<p>虽然基于 Rust 的微服务是轻量和快速的，但不是每个人都是 Rust 开发者。</p>
<p>如果你更熟悉 JavaScript，你仍然可以在边缘云中利用 WasmEdge 的安全性、性能、占用空间小和可移植性。具体来说，你可以使用 Node.js API 为 WasmEdge 创建微服务！</p>
<p>对于 Node.js 应用程序，WasmEdge 占用空间小于 15MB。相比之下，标准 Node.js Linux 映像超过 150MB。</p>
<p>运行以下两个 CLI 命令，从我们用于 WasmEdge + Node.js 的超薄 Docker 映像创建并部署一个 <a href="http://fly.io">fly.io</a> 应用程序。</p>
<pre><code class="language-Rust">$ flyctl launch --image juntaoyuan/flyio-nodejs-echo
$ flyctl deploy
</code></pre>
<p>这就是了！你可以使用 curl 命令来测试所部署的网络服务是否真的工作。无论你向它发布什么数据，它都会回显出来。</p>
<pre><code class="language-bash">$ curl https://solitary-snowflake-1159.fly.dev -d "Hello WasmEdge for Node.js on fly.io!"
Hello WasmEdge for Node.js on fly.io!
</code></pre>
<p><code>juntaoyuan/flyio-nodejs-echo</code> Docker 镜像的 Docker 文件包含 WasmEdge 运行时、QuickJS 运行时<code>wasmedge_quickjs.wasm</code>、Node.js <a href="https://wasmedge.org/book/en/dev/js/nodejs.html#the-javascript-modules">模块</a>和网络服务应用程序<code>node_echo.js</code>的完整包。</p>
<pre><code class="language-Dockerfile">FROM wasmedge/slim-runtime:0.11.0
ADD wasmedge_quickjs.wasm /
ADD node_echo.js /
ADD modules /modules
CMD ["wasmedge", "--dir", ".:/", "/wasmedge_quickjs.wasm", "node_echo.js"]
</code></pre>
<p><code>node_echo.js</code>应用程序的完整 JavaScript 源代码如下。你可以清楚地看到，它只是使用标准的 Node.js APIs 来创建一个异步的 HTTP 服务器，回显 HTTP 请求体。</p>
<pre><code class="language-Javascript">import { createServer, request, fetch } from 'http';

createServer((req, resp) =&gt; {
  req.on('data', (body) =&gt; {
    resp.end(body)
  })
}).listen(8080, () =&gt; {
  print('listen 8080 ...\n');
})
</code></pre>
<p>WasmEdge 的 QuickJS 引擎不仅提供 Node.js 支持，还提供 Tensorflow 推理支持。我们将 Rust Tensorflow 和 WASI-NN SDKs 包装成 JavaScript APIs，以便 JavaScript 开发人员可以<a href="https://wasmedge.org/book/en/dev/js/tensorflow.html">轻松创建 AI 人工智能应用程序</a>。</p>
<h2 id="">在边缘部署的有状态的微服务</h2>
<p>使用 WasmEdge，也可以创建由数据库支持的有状态的微服务。<a href="https://github.com/WasmEdge/wasmedge-db-examples">这个 GitHub repo</a> 包含了 WasmEdge 应用程序中基于 tokio 的非阻塞数据库客户端的例子。</p>
<ul>
<li><a href="https://github.com/WasmEdge/wasmedge-db-examples/tree/main/mysql">MySQL 客户端</a>允许 WasmEdge 应用程序访问大多数云数据库。</li>
<li><a href="https://github.com/essa-project/anna-rs">anna-rs 项目</a>是一个边缘原生 KV 存储，在边缘节点上具有可调整的同步和一致性级别。WasmEdge 应用程序<a href="https://github.com/WasmEdge/wasmedge-db-examples/tree/main/anna">可以使用 anna-rs</a> 作为边缘缓存或数据库。</li>
</ul>
<p>你现在可以使用 WasmEdge SDK 和运行时在边缘云上构建各种 Web 服务。期待很快可以看到你的作品！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
