<?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[ NextJS - 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[ NextJS - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 04 Jun 2026 15:37:38 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/nextjs/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 如何在 Next.js 应用程序中使用服务器端渲染来提升 SEO ]]>
                </title>
                <description>
                    <![CDATA[ 服务端渲染（SSR）是一种可以帮助提升网站 SEO 的网页开发技术。它通过在服务器端生成 HTML 内容来响应用户的请求。 这种方法与客户端渲染（CSR）形成对比，在客户端渲染中，内容作为基本的 HTML 框架传递，JavaScript 在浏览器中获取和显示数据。 SSR 提供了显著的 SEO 优势，使其非常适合使用 Next.js 这样流行的 React 框架。让我们来讨论一下在 Next.js 中使用 SSR 如何提升网站的搜索引擎可见性。 内容目录  1. 什么是服务端渲染  2. 如何开始使用 Next.js 和 SSR  3. Next.js 如何实现服务端渲染  4. 使用 getStaticProps 和 getServerSideProps 获取数据  5. SSR 为 Next.js 带来的 SEO 优势及优化方法 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/server-side-rendering-in-next-js-for-improved-seo/</link>
                <guid isPermaLink="false">67ca7a6b3d55950475baa137</guid>
                
                    <category>
                        <![CDATA[ NextJS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SEO ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ wendy chen ]]>
                </dc:creator>
                <pubDate>Fri, 07 Mar 2025 05:29:31 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2025/03/1741325343969.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p data-test-label="translation-intro">
        <strong>原文：</strong> <a href="https://www.freecodecamp.org/news/server-side-rendering-in-next-js-for-improved-seo/" target="_blank" rel="noopener noreferrer" data-test-label="original-article-link">How to Use Server-Side Rendering in Next.js Apps for Better SEO</a>
      </p><!--kg-card-begin: markdown--><p>服务端渲染（SSR）是一种可以帮助提升网站 SEO 的网页开发技术。它通过在服务器端生成 HTML 内容来响应用户的请求。</p>
<p>这种方法与客户端渲染（CSR）形成对比，在客户端渲染中，内容作为基本的 HTML 框架传递，JavaScript 在浏览器中获取和显示数据。</p>
<p>SSR 提供了显著的 SEO 优势，使其非常适合使用 Next.js 这样流行的 React 框架。让我们来讨论一下在 Next.js 中使用 SSR 如何提升网站的搜索引擎可见性。</p>
<h3 id="">内容目录</h3>
<ol>
<li><a href="#heading-what-is-server-side-rendering">什么是服务端渲染</a></li>
<li><a href="#heading-how-to-get-started-with-nextjs-and-ssr">如何开始使用 Next.js 和 SSR</a></li>
<li><a href="#heading-how-nextjs-enables-server-side-rendering">Next.js 如何实现服务端渲染</a></li>
<li><a href="#heading-data-fetching-with-getstaticprops-and-getserversideprops">使用 getStaticProps 和 getServerSideProps 获取数据</a></li>
<li><a href="#heading-benefits-of-ssr-for-seo-with-nextjs-and-how-to-optimize">SSR 为 Next.js 带来的 SEO 优势及优化方法</a></li>
<li><a href="#heading-conclusion">结论</a></li>
</ol>
<h2 id="">什么是服务端渲染</h2>
<p>服务端渲染（SSR）是一种网页开发技术，在网页服务器发送请求到用户的浏览器之前，就生成完整的 HTML 网页内容。</p>
<p>这与客户端渲染（CSR）不同，客户端渲染是在浏览器下载基本 HTML 结构后再使用 JavaScript 获取和显示内容。</p>
<h2 id="nextjsssr">如何开始使用 Next.js 和 SSR</h2>
<p>开始使用 Next.js 和服务端渲染（SSR）需要几个步骤。以下是一个帮助你设置 Next.js 项目并实现 SSR 的分步骤指南。</p>
<h3 id="nextjs">第一步：安装 Next.js</h3>
<p>首先，你需要安装 Next.js。你可以使用 <code>create-next-app</code> 来设置一个具有默认配置的新的 Next.js 项目。在终端运行如下命令：</p>
<pre><code>npx create-next-app my-next-app
cd my-next-app
npm run dev
</code></pre>
<p>此命令会在名为 <code>my-next-app</code> 的文件夹中创建一个新的 Next.js 应用程序，并启动开发服务器。</p>
<h3 id="">第二步：了解项目结构</h3>
<p>Next.js 通过一些默认的文件夹和文件组织项目：</p>
<ul>
<li><strong><code>pages/</code></strong>：该文件夹包含应用程序的所有页面。每个文件代表应用中的一个路由。</li>
<li><strong><code>public/</code></strong>：可以放置图像等静态资源。</li>
<li><strong><code>styles/</code></strong>：包含用于应用程序样式的 CSS 文件。</li>
</ul>
<h3 id="ssr">第三步：创建一个使用 SSR 的简单页面</h3>
<p>现在，让我们创建一个使用 SSR 的简单页面。</p>
<p>新建文件 <code>pages/index.js</code>：</p>
<pre><code class="language-javascript">// pages/index.js
import React from 'react';

const Home = ({ data }) =&gt; {
  return (
    &lt;div&gt;
      &lt;h1&gt;欢迎使用 Next.js 和 SSR&lt;/h1&gt;
      &lt;p&gt;从服务器获取的数据：{data.message}&lt;/p&gt;
    &lt;/div&gt;
  );
};

export async function getServerSideProps() {
  // 从 API 或其他来源获取数据
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();

  // 将数据作为 props 返回给 Home 组件
  return {
    props: {
      data,
    },
  };
}

export default Home;
</code></pre>
<p>让我们详细讨论一下这段代码。对于 <code>Home</code> 组件：</p>
<ul>
<li><code>Home</code> 组件是一个接收 <code>props</code> 的函数式组件。</li>
<li><code>data</code> prop 包含从服务器获取的数据。</li>
<li>在组件内部，我们渲染欢迎消息和获取的数据。</li>
</ul>
<p><code>getServerSideProps</code> 函数：</p>
<ul>
<li>这个函数从 <code>pages/index.js</code> 文件中导出。</li>
<li>对于每个请求，这个函数在服务器上执行。</li>
<li>在此函数中可以执行异步操作，如从外部 API 获取数据。</li>
<li>获取的数据作为一个带有 <code>props</code> 属性的对象返回，该对象会作为 props 传给 <code>Home</code> 组件。</li>
</ul>
<p>你可以为 <code>getServerSideProps</code> 函数添加错误处理，以应对数据获取过程中可能发生的问题。以下是一个例子：</p>
<pre><code class="language-javascript">export async function getServerSideProps() {
  try {
    const res = await fetch('https://api.example.com/data');
    if (!res.ok) {
      throw new Error('数据获取失败');
    }
    const data = await res.json();
    return {
      props: {
        data,
      },
    };
  } catch (error) {
    console.error(error);
    return {
      props: {
        data: { message: '数据获取时出错' },
      },
    };
  }
}
</code></pre>
<h4 id="">第四步：运行应用程序</h4>
<p>如果开发服务器还未启动，请启动它：</p>
<pre><code>npm run dev
</code></pre>
<p>打开浏览器并访问 <code>http://localhost:3000</code>。你应该看到页面上显示了从 API 获取的信息。</p>
<h3 id="nextjs">Next.js 如何实现服务端渲染</h3>
<p>Next.js 提供了一种无缝的方式来启用 SSR 和静态站点生成（SSG）。默认情况下，它会预渲染每个页面。根据用例的不同，你可以在 SSR 和 SSG 之间进行选择：</p>
<ul>
<li><strong>服务端渲染（SSR）</strong>：页面在每次请求时渲染。</li>
<li><strong>静态站点生成（SSG）</strong>：页面在构建时生成。</li>
</ul>
<p>Next.js 根据你在页面组件中实现的函数（<code>getStaticProps</code> 和 <code>getServerSideProps</code>）来确定使用哪种渲染方式。</p>
<h3 id="nextjs">Next.js 页面组件</h3>
<p>Next.js 使用 <code>pages/</code> 目录来定义路由。在这个目录中的每一个文件都对应你应用中的一个路由。</p>
<ul>
<li><code>pages/index.js</code> → <code>/</code></li>
<li><code>pages/about.js</code> → <code>/about</code></li>
<li><code>pages/posts/[id].js</code> → <code>/posts/:id</code></li>
</ul>
<p>以下是一个页面组件的基础示例：</p>
<pre><code class="language-javascript">// pages/index.js
import React from 'react';

const Home = () =&gt; {
  return (
    &lt;div&gt;
      &lt;h1&gt;欢迎来到 Next.js&lt;/h1&gt;
      &lt;p&gt;这是首页。&lt;/p&gt;
    &lt;/div&gt;
  );
};

export default Home;
</code></pre>
<h3 id="getstaticpropsgetserversideprops">使用 <code>getStaticProps</code> 和 <code>getServerSideProps</code> 进行数据获取</h3>
<p><code>getStaticProps</code> 用于静态生成。它在构建时运行，并允许你获取数据并将其作为 props 传递给页面。对于不经常更改的数据，使用它。</p>
<p>示例：</p>
<pre><code class="language-javascript">// pages/index.js
import React from 'react';

const Home = ({ posts }) =&gt; {
  return (
    &lt;div&gt;
      &lt;h1&gt;博客文章&lt;/h1&gt;
      &lt;ul&gt;
        {posts.map(post =&gt; (
          &lt;li key={post.id}&gt;{post.title}&lt;/li&gt;
        ))}
      &lt;/ul&gt;
    &lt;/div&gt;
  );
};

// 该函数在构建时运行
export async function getStaticProps() {
  // 从 API 获取数据
  const res = await fetch('https://jsonplaceholder.typicode.com/posts');
  const posts = await res.json();

  return {
    props: {
      posts,
    },
  };
}

export default Home;
</code></pre>
<p><code>getServerSideProps</code> 用于服务器端渲染。它在每个请求时运行，并允许你在请求时获取数据。</p>
<p>示例：</p>
<pre><code class="language-javascript">// pages/index.js
import React from 'react';

const Home = ({ data }) =&gt; {
  return (
    &lt;div&gt;
      &lt;h1&gt;使用 Next.js 进行服务器端渲染&lt;/h1&gt;
      &lt;p&gt;从服务器获取的数据: {data.message}&lt;/p&gt;
    &lt;/div&gt;
  );
};

// 该函数在每个请求时运行
export async function getServerSideProps() {
  // 从外部 API 获取数据
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();

  return {
    props: {
      data,
    },
  };
}

export default Home;
</code></pre>
<h2 id="ssrseo">使用 SSR 进行 SEO 的优势及优化方式</h2>
<p>在本节中，我们将了解使用 SSR 进行 SEO 的主要优势，并提供易于遵循的提示，告诉你如何在 Next.js 应用中充分利用这些优势。</p>
<h3 id="1">1. 提高搜索引擎索引</h3>
<p>客户端渲染 (CSR) 可能导致搜索引擎难以正确索引内容，因为内容是在用户的浏览器中通过 JavaScript 渲染的。</p>
<p>然而，SSR 会在将内容发送到用户的浏览器之前在服务器上渲染，确保 HTML 是完整的，搜索引擎可以轻松抓取和索引。</p>
<p><strong>对重要页面使用 SSR：</strong> 确保关键页面如登陆页、博客文章和产品页面在服务器上渲染，以便更好地进行索引。</p>
<p>示例 – 使用 SSR 进行博客文章页面：</p>
<pre><code class="language-javascript">// pages/blog/[id].js
import React from 'react';
import { useRouter } from 'next/router';
import Head from 'next/head';

const BlogPost = ({ post }) =&gt; {
  const router = useRouter();
  if (router.isFallback) {
    return &lt;div&gt;加载中...&lt;/div&gt;;
  }

  return (
    &lt;div&gt;
      &lt;Head&gt;
        &lt;title&gt;{post.title}&lt;/title&gt;
        &lt;meta name="description" content={post.excerpt} /&gt;
      &lt;/Head&gt;
      &lt;h1&gt;{post.title}&lt;/h1&gt;
      &lt;p&gt;{post.content}&lt;/p&gt;
    &lt;/div&gt;
  );
};

export async function getServerSideProps({ params }) {
  const res = await fetch(`https://api.example.com/posts/${params.id}`);
  const post = await res.json();

  return {
    props: {
      post,
    },
  };
}

export default BlogPost
</code></pre>
<ul>
<li><strong>BlogPost 组件：</strong> 此组件显示一篇博文。它使用 <code>next/head</code> 来管理元标签，这对 SEO 很重要。</li>
<li><strong>getServerSideProps 函数：</strong> 此函数从 API 获取博文数据。它在每次请求此页面时在服务器上运行，确保内容在搜索引擎抓取页面时已准备好进行索引。</li>
</ul>
<h3 id="2">2. 加快加载时间</h3>
<p>搜索引擎如 Google 使用页面加载速度作为排名因素。SSR 可以改善首次加载时间，因为服务器会向浏览器发送一个完全渲染的页面，增强感知性能和用户体验。</p>
<p><strong>优化服务器响应时间：</strong> 确保你的服务器优化以实现快速响应。使用缓存策略以减轻服务器负载。</p>
<p>示例 – SSR 的 cache-control 头部：</p>
<pre><code class="language-javascript">export async function getServerSideProps({ res }) {
  res.setHeader('Cache-Control', 'public, s-maxage=10, stale-while-revalidate=59');

  const resData = await fetch('https://api.example.com/data');
  const data = await resData.json();

  return {
    props: {
      data,
    },
  };
}
</code></pre>
<ul>
<li><strong><code>getServerSideProps</code> 函数：</strong> 此函数设置 cache-control 头以将响应缓存 10 秒，并在重新验证的 59 秒内提供陈旧内容。这样可以提高服务器响应时间和页面加载速度，从而改善 SEO。</li>
</ul>
<h3 id="3">3. 改善社交媒体分享</h3>
<p>在社交媒体上分享链接时，像 Facebook 和 Twitter 这样的平台会抓取 URL 内容以生成预览。SSR 确保必要的元数据在初始 HTML 中可用，从而生成更好的预览并提高点击率。</p>
<p><strong>使用 <code>next/head</code> 管理元标签:</strong> 使用 <code>next/head</code> 组件为社交媒体和SEO添加元标签。</p>
<p>示例 – 向页面添加元标签：</p>
<pre><code class="language-javascript">import Head from 'next/head';

const Page = ({ data }) =&gt; (
  &lt;div&gt;
    &lt;Head&gt;
      &lt;title&gt;{data.title}&lt;/title&gt;
      &lt;meta name="description" content={data.description} /&gt;
      &lt;meta property="og:title" content={data.title} /&gt;
      &lt;meta property="og:description" content={data.description} /&gt;
      &lt;meta property="og:image" content={data.image} /&gt;
      &lt;meta name="twitter:card" content="summary_large_image" /&gt;
    &lt;/Head&gt;
    &lt;h1&gt;{data.title}&lt;/h1&gt;
    &lt;p&gt;{data.content}&lt;/p&gt;
  &lt;/div&gt;
);
</code></pre>
<ul>
<li><strong><code>Page</code>组件：</strong> 该组件使用<code>next/head</code>添加SEO元标签，包括用于社交媒体预览的Open Graph标签。这确保了当页面被分享时，社交媒体平台可以使用所提供的元数据生成丰富的预览。</li>
</ul>
<h3 id="4">4. 增强用户体验</h3>
<p>一个更快、响应更迅速的网站能够提升整体用户体验，从而延长访问时长并降低跳出率。这两个因素都会对 SEO 排名产生积极影响。</p>
<p><strong>使用静态生成（SSG）预渲染较少动态的内容页面：</strong> 对不经常变化的页面使用SSG，以减少服务器负载并提高性能。</p>
<p>示例 – SSG 用于静态页面：</p>
<pre><code class="language-javascript">export async function getStaticProps() {
  const res = await fetch('https://api.example.com/static-data');
  const data = await res.json();

  return {
    props: {
      data,
    },
    revalidate: 10, // 至多每10秒重新验证一次
  };
}

const StaticPage = ({ data }) =&gt; (
  &lt;div&gt;
    &lt;h1&gt;{data.title}&lt;/h1&gt;
    &lt;p&gt;{data.content}&lt;/p&gt;
  &lt;/div&gt;
);

export default StaticPage;
</code></pre>
<ul>
<li><strong><code>StaticPage</code>组件：</strong> 该组件显示从API获取的静态内容。</li>
<li><strong><code>getStaticProps</code>函数：</strong> 此函数在构建时获取数据，并每十秒重新验证一次，确保内容始终新鲜，同时减少服务器负担。</li>
</ul>
<h2 id="">结论</h2>
<p>结合使用服务端渲染和Next.js就像为您的网站在搜索引擎中提供了额外的推动力。利用预构建的内容以及流畅的用户体验，您的网站可以更自然地被更多人看到。</p>
<p>这对任何类型的网站都非常有效，从在线商店到博客。使用Next.js与SSR能够轻松构建一个搜索引擎友好且用户喜欢的网站。</p>
<p>这就是本篇文章的全部内容！如果您想继续交流或者有问题、建议或反馈，欢迎随时联系我的<a href="https://ng.linkedin.com/in/joan-ayebola">LinkedIn</a>。如果您喜欢这篇内容，可以考虑<a href="https://www.buymeacoffee.com/joanayebola">请我喝杯咖啡</a>，以支持更多面向开发人员的内容创作。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何为 Nestjs 编写单元测试和 E2E 测试 ]]>
                </title>
                <description>
                    <![CDATA[ 前言 最近在给一个 nestjs 项目写单元测试（Unit Testing）和 e2e 测试（End-to-End Testing，端到端测试，简称 e2e 测试），这是我第一次给后端项目写测试，发现和之前给前端项目写测试还不太一样，导致在一开始写测试时感觉无从下手。后来在看了一些示例之后才想明白怎么写测试，所以打算写篇文章记录并分享一下，以帮助和我有相同困惑的人。 同时我也写了一个 demo 项目，相关的单元测试、e2e 测试都写好了，有兴趣可以看一下。代码已上传到 GitHub: nestjs-demo [https://link.segmentfault.com/?enc=HLCKHEcBvrrecUEHdZldwg%3D%3D.rqCa%2BxjnMJTtq32ACk7AhaKfYgiVglZx55Z7ivxfu3eX7EDOP%2Fzeot%2FIeXAwGxeG] 。 单元测试和 E2E 测试的区别 单元测试和 e2e 测试都是软件测试的方法，但它们的目标和范围有所不同。 单元测试是对软件中的最小可测试单元进行检查和验证。比如一个函数、一个方法都可以是一个单元。在单元 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-write-unit-tests-and-e2e-tests-for-nestjs/</link>
                <guid isPermaLink="false">6690f01ddd1680043183e1d0</guid>
                
                    <category>
                        <![CDATA[ NextJS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 测试 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Fri, 12 Jul 2024 02:50:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/07/pexels-luis-gomes-546819.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="-">前言</h2><p>最近在给一个 nestjs 项目写单元测试（Unit Testing）和 e2e 测试（End-to-End Testing，端到端测试，简称 e2e 测试），这是我第一次给后端项目写测试，发现和之前给前端项目写测试还不太一样，导致在一开始写测试时感觉无从下手。后来在看了一些示例之后才想明白怎么写测试，所以打算写篇文章记录并分享一下，以帮助和我有相同困惑的人。</p><p>同时我也写了一个 demo 项目，相关的单元测试、e2e 测试都写好了，有兴趣可以看一下。代码已上传到 GitHub: <a href="https://link.segmentfault.com/?enc=HLCKHEcBvrrecUEHdZldwg%3D%3D.rqCa%2BxjnMJTtq32ACk7AhaKfYgiVglZx55Z7ivxfu3eX7EDOP%2Fzeot%2FIeXAwGxeG" rel="nofollow">nestjs-demo</a>。</p><h2 id="-e2e-">单元测试和 E2E 测试的区别</h2><p>单元测试和 e2e 测试都是软件测试的方法，但它们的目标和范围有所不同。</p><p>单元测试是对软件中的最小可测试单元进行检查和验证。比如一个函数、一个方法都可以是一个单元。在单元测试中，你会对这个函数的各种输入给出预期的输出，并验证功能的正确性。单元测试的目标是快速发现函数内部的 bug，并且它们容易编写、快速执行。</p><p>而 e2e 测试通常通过模拟真实用户场景的方法来测试整个应用，例如前端通常使用浏览器或无头浏览器来进行测试，后端则是通过模拟对 API 的调用来进行测试。</p><p>在 nestjs 项目中，单元测试可能会测试某个服务（service）、某个控制器（controller）的一个方法，例如测试 Users 模块中的 <code>update</code> 方法是否能正确的更新一个用户。而一个 e2e 测试可能会测试一个完整的用户流程，如创建一个新用户，然后更新他们的密码，然后删除该用户。这涉及了多个服务和控制器。</p><h2 id="--1">编写单元测试</h2><p>为一个工具函数或者不涉及接口的方法编写单元测试，是非常简单的，你只需要考虑各种输入并编写相应的测试代码就可以了。但是一旦涉及到接口，那情况就复杂了。用代码来举例：</p><pre><code class="language-ts">async validateUser(
  username: string,
  password: string,
): Promise&lt;UserAccountDto&gt; {
  const entity = await this.usersService.findOne({ username });
  if (!entity) {
    throw new UnauthorizedException('User not found');
  }

  if (entity.lockUntil &amp;&amp; entity.lockUntil &gt; Date.now()) {
    const diffInSeconds = Math.round((entity.lockUntil - Date.now()) / 1000);
    let message = `The account is locked. Please try again in ${diffInSeconds} seconds.`;
    if (diffInSeconds &gt; 60) {
      const diffInMinutes = Math.round(diffInSeconds / 60);
      message = `The account is locked. Please try again in ${diffInMinutes} minutes.`;
    }

    throw new UnauthorizedException(message);
  }

  const passwordMatch = bcrypt.compareSync(password, entity.password);
  if (!passwordMatch) {
    // $inc update to increase failedLoginAttempts
    const update = {
      $inc: { failedLoginAttempts: 1 },
    };

    // lock account when the third try is failed
    if (entity.failedLoginAttempts + 1 &gt;= 3) {
      // $set update to lock the account for 5 minutes
      update['$set'] = { lockUntil: Date.now() + 5 * 60 * 1000 };
    }

    await this.usersService.update(entity._id, update);
    throw new UnauthorizedException('Invalid password');
  }

  // if validation is sucessful, then reset failedLoginAttempts and lockUntil
  if (
    entity.failedLoginAttempts &gt; 0 ||
    (entity.lockUntil &amp;&amp; entity.lockUntil &gt; Date.now())
  ) {
    await this.usersService.update(entity._id, {
      $set: { failedLoginAttempts: 0, lockUntil: null },
    });
  }

  return { userId: entity._id, username } as UserAccountDto;
}
</code></pre><p>上面的代码是 <code>auth.service.ts</code> 文件里的一个方法 <code>validateUser</code>，主要用于验证登录时用户输入的账号密码是否正确。它包含的逻辑如下：</p><ol><li>根据 <code>username</code> 查看用户是否存在，如果不存在则抛出 401 异常（也可以是 404 异常）</li><li>查看用户是否被锁定，如果被锁定则抛出 401 异常和相关的提示文字</li><li>将 <code>password</code> 加密后和数据库中的密码进行对比，如果错误则抛出 401 异常（连续三次登录失败会被锁定账户 5 分钟）</li><li>如果登录成功，则将之前登录失败的计数记录进行清空（如果有）并返回用户 <code>id</code> 和 <code>username</code> 到下一阶段</li></ol><p>可以看到 <code>validateUser</code> 方法包含了 4 个处理逻辑，我们需要对这 4 点都编写对应的单元测试代码，以确定整个 <code>validateUser</code> 方法功能是正常的。</p><h3 id="--2">第一个测试用例</h3><p>在开始编写单元测试时，我们会遇到一个问题，<code>findOne</code> 方法需要和数据库进行交互，它要通过 <code>username</code> 查找数据库中是否存在对应的用户。但如果每一个单元测试都得和数据库进行交互，那测试起来会非常麻烦。所以可以通过 mock 假数据来实现这一点。</p><p>举例，假如我们已经注册了一个 <code>woai3c</code> 的用户，那么当用户登录时，在 <code>validateUser</code> 方法中能够通过 <code>const entity = await this.usersService.findOne({ username });</code> 拿到用户数据。所以只要确保这行代码能够返回想要的数据，即使不和数据库交互也是没有问题的。而这一点，我们能通过 mock 数据来实现。现在来看一下 <code>validateUser</code> 方法的相关测试代码：</p><pre><code class="language-ts">import { Test } from '@nestjs/testing';
import { AuthService } from '@/modules/auth/auth.service';
import { UsersService } from '@/modules/users/users.service';
import { UnauthorizedException } from '@nestjs/common';
import { TEST_USER_NAME, TEST_USER_PASSWORD } from '@tests/constants';

describe('AuthService', () =&gt; {
  let authService: AuthService; // Use the actual AuthService type
  let usersService: Partial&lt;Record&lt;keyof UsersService, jest.Mock&gt;&gt;;

  beforeEach(async () =&gt; {
    usersService = {
      findOne: jest.fn(),
    };

    const module = await Test.createTestingModule({
      providers: [
        AuthService,
        {
          provide: UsersService,
          useValue: usersService,
        },
      ],
    }).compile();

    authService = module.get&lt;AuthService&gt;(AuthService);
  });

  describe('validateUser', () =&gt; {
    it('should throw an UnauthorizedException if user is not found', async () =&gt; {
      await expect(
        authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
      ).rejects.toThrow(UnauthorizedException);
    });

    // other tests...
  });
});
</code></pre><p>我们通过调用 <code>usersService</code> 的 <code>fineOne</code> 方法来拿到用户数据，所以需要在测试代码中 mock <code>usersService</code> 的 <code>fineOne</code> 方法：</p><pre><code class="language-ts"> beforeEach(async () =&gt; {
    usersService = {
      findOne: jest.fn(), // 在这里 mock findOne 方法
    };

    const module = await Test.createTestingModule({
      providers: [
        AuthService, // 真实的 AuthService，因为我们要对它的方法进行测试
        {
          provide: UsersService, // 用 mock 的 usersService 代替真实的 usersService 
          useValue: usersService,
        },
      ],
    }).compile();

    authService = module.get&lt;AuthService&gt;(AuthService);
  });
</code></pre><p>通过使用 <code>jest.fn()</code> 返回一个函数来代替真实的 <code>usersService.findOne()</code>。如果这时调用 <code>usersService.findOne()</code> 将不会有任何返回值，所以第一个单元测试用例就能通过了：</p><pre><code class="language-ts">it('should throw an UnauthorizedException if user is not found', async () =&gt; {
  await expect(
    authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
  ).rejects.toThrow(UnauthorizedException);
});
</code></pre><p>因为在 <code>validateUser</code> 方法中调用 <code>const entity = await this.usersService.findOne({ username });</code> 的 <code>findOne</code> 是 mock 的假函数，没有返回值，所以 <code>validateUser</code> 方法中的第 2-4 行代码就能执行到了：</p><pre><code class="language-ts">if (!entity) {
  throw new UnauthorizedException('User not found');
}
</code></pre><p>抛出 401 错误，符合预期。</p><h3 id="--3">第二个测试用例</h3><p><code>validateUser</code> 方法中的第二个处理逻辑是判断用户是否锁定，对应的代码如下：</p><pre><code class="language-ts">if (entity.lockUntil &amp;&amp; entity.lockUntil &gt; Date.now()) {
  const diffInSeconds = Math.round((entity.lockUntil - Date.now()) / 1000);
  let message = `The account is locked. Please try again in ${diffInSeconds} seconds.`;
  if (diffInSeconds &gt; 60) {
    const diffInMinutes = Math.round(diffInSeconds / 60);
    message = `The account is locked. Please try again in ${diffInMinutes} minutes.`;
  }

  throw new UnauthorizedException(message);
}
</code></pre><p>可以看到如果用户数据里有锁定时间 <code>lockUntil</code> 并且锁定结束时间大于当前时间就可以判断当前账户处于锁定状态。所以需要 mock 一个具有 <code>lockUntil</code> 字段的用户数据：</p><pre><code class="language-ts">it('should throw an UnauthorizedException if the account is locked', async () =&gt; {
  const lockedUser = {
    _id: TEST_USER_ID,
    username: TEST_USER_NAME,
    password: TEST_USER_PASSWORD,
    lockUntil: Date.now() + 1000 * 60 * 5, // The account is locked for 5 minutes
  };

  usersService.findOne.mockResolvedValueOnce(lockedUser);

  await expect(
    authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
  ).rejects.toThrow(UnauthorizedException);
});
</code></pre><p>在上面的测试代码里，先定义了一个对象 <code>lockedUser</code>，这个对象里有我们想要的 <code>lockUntil</code> 字段，然后将它作为 <code>findOne</code> 的返回值，这通过 <code>usersService.findOne.mockResolvedValueOnce(lockedUser);</code> 实现。然后 <code>validateUser</code> 方法执行时，里面的用户数据就是 mock 出来的数据了，从而成功让第二个测试用例通过。</p><h3 id="--4">单元测试覆盖率</h3><p>剩下的两个测试用例就不写了，原理都是一样的。如果剩下的两个测试不写，那么这个 <code>validateUser</code> 方法的单元测试覆盖率会是 50%，如果 4 个测试用例都写完了，那么 <code>validateUser</code> 方法的单元测试覆盖率将达到 100%。</p><p>单元测试覆盖率（Code Coverage）是一个度量，用于描述应用程序代码有多少被单元测试覆盖或测试过。它通常表示为百分比，表示在所有可能的代码路径中，有多少被测试用例覆盖。</p><p>单元测试覆盖率通常包括以下几种类型：</p><ul><li>行覆盖率（Lines）：测试覆盖了多少代码行。</li><li>函数覆盖率（Funcs）：测试覆盖了多少函数或方法。</li><li>分支覆盖率（Branch）：测试覆盖了多少代码分支（例如，<code>if/else</code> 语句）。</li><li>语句覆盖率（Stmts）：测试覆盖了多少代码语句。</li></ul><p>单元测试覆盖率是衡量单元测试质量的一个重要指标，但并不是唯一的指标。高的覆盖率可以帮助检测代码中的错误，但并不能保证代码的质量。覆盖率低可能意味着有未被测试的代码，可能存在未被发现的错误。</p><p>下图是 demo 项目的单元测试覆盖率结果：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2024/07/image-3.png" class="kg-image" alt="image-3" srcset="https://chinese.freecodecamp.org/news/content/images/size/w600/2024/07/image-3.png 600w, https://chinese.freecodecamp.org/news/content/images/2024/07/image-3.png 655w" width="655" height="566" loading="lazy"></figure><p>像 service 和 controller 之类的文件，单元测试覆盖率一般尽量高点比较好，而像 module 这种文件就没有必要写单元测试了，也没法写，没有意义。上面的图片表示的是整个单元测试覆盖率的总体指标，如果你想查看某个函数的测试覆盖率，可以打开项目根目录下的 <code>coverage/lcov-report/index.html</code> 文件进行查看。例如我想查看 <code>validateUser</code> 方法具体的测试情况：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2024/07/image-4.png" class="kg-image" alt="image-4" srcset="https://chinese.freecodecamp.org/news/content/images/size/w600/2024/07/image-4.png 600w, https://chinese.freecodecamp.org/news/content/images/2024/07/image-4.png 711w" width="711" height="780" loading="lazy"></figure><p>可以看到原来 <code>validateUser</code> 方法的单元测试覆盖率并不是 100%，还是有两行代码没有执行到，不过也无所谓了，不影响 4 个关键的处理节点，不要片面的追求高测试覆盖率。</p><h2 id="-e2e--1">编写E2E 测试</h2><p>在单元测试中我们展示了如何为 <code>validateUser()</code> 的每一个功能点编写单元测试，并且使用了 mock 数据的方法来确保每个功能点都能够被测试到。而在 e2e 测试中，我们需要模拟真实的用户场景，所以要连接数据库来进行测试。因此，这次测试的 <code>auth.service.ts</code> 模块里的方法都会和数据库进行交互。</p><p><code>auth</code> 模块主要有以下几个功能：</p><ul><li>注册</li><li>登录</li><li>刷新 token</li><li>读取用户信息</li><li>修改密码</li><li>删除用户</li></ul><p>e2e 测试需要将这六个功能都测试一遍，从<code>注册</code>开始，到<code>删除用户</code>结束。在测试时，我们可以建一个专门的测试用户来进行测试，测试完成后再删除这个测试用户，这样就不会在测试数据库中留下无用的信息了。</p><pre><code class="language-ts">beforeAll(async () =&gt; {
  const moduleFixture: TestingModule = await Test.createTestingModule({
    imports: [AppModule],
  }).compile()

  app = moduleFixture.createNestApplication()
  await app.init()

  // 执行登录以获取令牌
  const response = await request(app.getHttpServer())
    .post('/auth/register')
    .send({ username: TEST_USER_NAME, password: TEST_USER_PASSWORD })
    .expect(201)

  accessToken = response.body.access_token
  refreshToken = response.body.refresh_token
})

afterAll(async () =&gt; {
  await request(app.getHttpServer())
    .delete('/auth/delete-user')
    .set('Authorization', `Bearer ${accessToken}`)
    .expect(200)

  await app.close()
})
</code></pre><p><code>beforeAll</code> 钩子函数将在所有测试开始之前执行，所以我们可以在这里注册一个测试账号 <code>TEST_USER_NAME</code>。<code>afterAll</code> 钩子函数将在所有测试结束之后执行，所以在这删除测试账号 <code>TEST_USER_NAME</code> 是比较合适的，还能顺便对注册和删除两个功能进行测试。</p><p>在上一节的单元测试中，我们编写了关于 <code>validateUser</code> 方法的相关单元测试。其实这个方法是在登录时执行的，用于验证用户账号密码是否正确。所以这一次的 e2e 测试也将使用登录流程来展示如何编写 e2e 测试用例。</p><p>整个登录测试流程总共包含了五个小测试：</p><pre><code class="language-ts">describe('login', () =&gt; {
    it('/auth/login (POST)', () =&gt; {
      // ...
    })

    it('/auth/login (POST) with user not found', () =&gt; {
      // ...
    })

    it('/auth/login (POST) without username or password', async () =&gt; {
      // ...
    })

    it('/auth/login (POST) with invalid password', () =&gt; {
      // ...
    })

    it('/auth/login (POST) account lock after multiple failed attempts', async () =&gt; {
      // ...
    })
  })
</code></pre><p>这五个测试分别是：</p><ol><li>登录成功，返回 200</li><li>如果用户不存在，抛出 401 异常</li><li>如果不提供密码或用户名，抛出 400 异常</li><li>使用错误密码登录，抛出 401 异常</li><li>如果账户被锁定，抛出 401 异常</li></ol><p>现在我们开始编写 e2e 测试：</p><pre><code class="language-ts">// 登录成功
it('/auth/login (POST)', () =&gt; {
  return request(app.getHttpServer())
    .post('/auth/login')
    .send({ username: TEST_USER_NAME, password: TEST_USER_PASSWORD })
    .expect(200)
})

// 如果用户不存在，应该抛出 401 异常
it('/auth/login (POST) with user not found', () =&gt; {
  return request(app.getHttpServer())
    .post('/auth/login')
    .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
    .expect(401) // Expect an unauthorized error
})
</code></pre><p>e2e 的测试代码写起来比较简单，直接调用接口，然后验证结果就可以了。比如登录成功测试，我们只要验证返回结果是否是 200 即可。</p><p>前面四个测试都比较简单，现在我们看一个稍微复杂点的 e2e 测试，即验证账户是否被锁定。</p><pre><code class="language-ts">it('/auth/login (POST) account lock after multiple failed attempts', async () =&gt; {
  const moduleFixture: TestingModule = await Test.createTestingModule({
    imports: [AppModule],
  }).compile()

  const app = moduleFixture.createNestApplication()
  await app.init()

  const registerResponse = await request(app.getHttpServer())
    .post('/auth/register')
    .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })

  const accessToken = registerResponse.body.access_token
  const maxLoginAttempts = 3 // lock user when the third try is failed

  for (let i = 0; i &lt; maxLoginAttempts; i++) {
    await request(app.getHttpServer())
      .post('/auth/login')
      .send({ username: TEST_USER_NAME2, password: 'InvalidPassword' })
  }

  // The account is locked after the third failed login attempt
  await request(app.getHttpServer())
    .post('/auth/login')
    .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
    .then((res) =&gt; {
      expect(res.body.message).toContain(
        'The account is locked. Please try again in 5 minutes.',
      )
    })

  await request(app.getHttpServer())
    .delete('/auth/delete-user')
    .set('Authorization', `Bearer ${accessToken}`)

  await app.close()
})
</code></pre><p><strong>当用户连续三次登录失败的时候，账户就会被锁定</strong>。所以在这个测试里，我们不能使用测试账号 <code>TEST_USER_NAME</code>，因为测试成功的话这个账户就会被锁定，无法继续进行下面的测试了。我们需要再注册一个新用户 <code>TEST_USER_NAME2</code>，专门用来测试账户锁定，测试成功后再删除这个用户。所以你可以看到这个 e2e 测试的代码非常多，需要做大量的前置、后置工作，其实真正的测试代码就这几行：</p><pre><code class="language-ts">// 连续三次登录
for (let i = 0; i &lt; maxLoginAttempts; i++) {
  await request(app.getHttpServer())
    .post('/auth/login')
    .send({ username: TEST_USER_NAME2, password: 'InvalidPassword' })
}

// 测试账号是否被锁定
await request(app.getHttpServer())
  .post('/auth/login')
  .send({ username: TEST_USER_NAME2, password: TEST_USER_PASSWORD })
  .then((res) =&gt; {
    expect(res.body.message).toContain(
      'The account is locked. Please try again in 5 minutes.',
    )
  })
</code></pre><p>可以看到编写 e2e 测试代码还是相对比较简单的，不需要考虑 mock 数据，不需要考虑测试覆盖率，只要整个系统流程的运转情况符合预期就可以了。</p><h2 id="--5">应不应该写测试</h2><p>如果有条件的话，我是比较建议大家写测试的。因为写测试可以提高系统的健壮性、可维护性和开发效率。</p><h3 id="--6">提高系统健壮性</h3><p>我们一般编写代码时，会关注于正常输入下的程序流程，确保核心功能正常运作。但是一些边缘情况，比如异常的输入，这些我们可能会经常忽略掉。但当我们开始编写测试时，情况就不一样了，这会逼迫你去考虑如何处理并提供相应的反馈，从而避免程序崩溃。可以说写测试实际上是在<strong>间接</strong>地提高系统健壮性。</p><h3 id="--7">提高可维护性</h3><p>当你接手一个新项目时，如果项目包含完善的测试，那将会是一件很幸福的事情。它们就像是项目的指南，帮你快速把握各个功能点。只看测试代码就能够轻松地了解每个功能的预期行为和边界条件，而不用你逐行的去查看每个功能的代码。</p><h3 id="--8">提高开发效率</h3><p>想象一下，一个长时间未更新的项目突然接到了新需求。改了代码后，你可能会担心引入 bug，如果没有测试，那就需要重新手动测试整个项目——浪费时间，效率低下。而有了完整的测试，一条命令就能得知代码更改有没有影响现有功能。即使出错了，也能够快速定位，找到问题点。</p><h3 id="--9">什么时候不建议写测试？</h3><p><strong>短期项目</strong>、<strong>需求迭代非常快的项目</strong>不建议写测试。比如某些活动项目，活动结束就没用了，这种项目就不需要写测试。另外，需求迭代非常快的项目也不要写测试，我刚才说写测试能提高开发效率是有前提条件的，就是<strong>功能迭代比较慢的情况下，写测试才能提高开发效率</strong>。如果你的功能今天刚写完，隔一两天就需求变更了要改功能，那相关的测试代码都得重写。所以干脆就别写了，靠团队里的测试人员测试就行了，因为写测试是非常耗时间的，没必要自讨苦吃。</p><p>根据我的经验来看，国内的绝大多数项目（尤其是政企类项目）都是没有必要写测试的，因为需求迭代太快，还老是推翻之前的需求，代码都得加班写，那有闲情逸致写测试。</p><h2 id="--10">总结</h2><p>在细致地讲解了如何为 Nestjs 项目编写单元测试及 e2e 测试之后，我还是想重申一下测试的重要性，它能够提高系统的健壮性、可维护性和开发效率。如果没有机会写测试，我建议大家可以自己搞个练习项目来写，或者说参加一些开源项目，给这些项目贡献代码，因为开源项目对于代码要求一般都比较严格。贡献代码可能需要编写新的测试用例或修改现有的测试用例。</p><p>最后，再推荐一下我的其他文章，如果你有兴趣，不妨一读：</p><ul><li><a href="https://www.freecodecamp.org/chinese/news/search?query=%E5%85%A5%E9%97%A8%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B">带你入门前端工程</a></li><li><a href="https://www.freecodecamp.org/chinese/news/browser-rendering-engine/">从零开始实现一个玩具版浏览器渲染引擎</a></li><li><a href="https://www.freecodecamp.org/chinese/news/build-a-micro-frontend-framework/">手把手教你写一个简易的微前端框架</a></li><li><a href="https://www.freecodecamp.org/chinese/news/tech-analysis-of-front-end-monitoring-sdk/">前端监控 SDK 的一些技术要点原理分析</a></li><li><a href="https://www.freecodecamp.org/chinese/news/visual-drag-and-drop-component-library/">可视化拖拽组件库一些技术要点原理分析</a></li><li><a href="https://www.freecodecamp.org/chinese/news/visual-drag-and-drop-component-library-part-2/">可视化拖拽组件库一些技术要点原理分析（二）</a></li><li><a href="https://www.freecodecamp.org/chinese/news/visual-drag-and-drop-component-library-part-3/">可视化拖拽组件库一些技术要点原理分析（三）</a></li><li><a href="https://www.freecodecamp.org/chinese/news/visual-drag-and-drop-component-library-part-4/">可视化拖拽组件库一些技术要点原理分析（四）</a></li><li><a href="https://www.freecodecamp.org/chinese/news/exploration-and-practices-for-low-code-and-llms/">低代码与大语言模型的探索实践</a></li><li><a href="https://www.freecodecamp.org/chinese/news/improve-front-end-performance/">前端性能优化 24 条建议（2020）</a></li><li><a href="https://www.freecodecamp.org/chinese/news/create-a-scaffold/">手把手教你写一个脚手架</a></li><li><a href="https://www.freecodecamp.org/chinese/news/create-a-scaffold-2/">手把手教你写一个脚手架（二）</a></li></ul><h3 id="--11">参考资料</h3><ul><li><a href="https://nestjs.com/">NestJS</a>: A framework for building efficient, scalable Node.js server-side applications.</li><li><a href="https://www.mongodb.com/">MongoDB</a>: A NoSQL database used for data storage.</li><li><a href="https://jestjs.io/">Jest</a>: A testing framework for JavaScript and TypeScript.</li><li><a href="https://github.com/ladjs/supertest">Supertest</a>: A library for testing HTTP servers.</li></ul> ]]>
                </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[ Next.js 完全手册 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：The Next.js Handbook [https://www.freecodecamp.org/news/the-next-js-handbook/]，作者：Flavio Copes [https://www.freecodecamp.org/news/author/flavio/] 我写这个教程是为了帮助你快速学习 Next.js 并熟悉它的工作原理。 如果你对 Next.js 没有什么了解，过去使用过 React，并且希望更深入地了解 React 生态系统，特别是服务器端的渲染，那么这篇教程就非常适合你。 我发现 Next.js 是创建 Web 应用的一个很棒的工具，在这篇文章的最后，我希望你会像我一样对它感到兴奋。我也希望它能帮助你学习 Next.js！ 注意：你可以下载本教程的 PDF/ePub/Mobi 版本，以便你可以离线阅读。 [https://flaviocopes.com/page/nextjs-handbook/] 目录  1.  介绍 [./#introduction]  2.  Next.js 提供的主要功能 [./#the-main-fe ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-next-js-handbook/</link>
                <guid isPermaLink="false">62a09c25dbb8cc083a12801a</guid>
                
                    <category>
                        <![CDATA[ NextJS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Wed, 08 Jun 2022 05:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/06/Group-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/the-next-js-handbook/">The Next.js Handbook</a>，作者：<a href="https://www.freecodecamp.org/news/author/flavio/">Flavio Copes</a></p><!--kg-card-begin: markdown--><p>我写这个教程是为了帮助你快速学习 Next.js 并熟悉它的工作原理。</p>
<p>如果你对 Next.js 没有什么了解，过去使用过 React，并且希望更深入地了解 React 生态系统，特别是服务器端的渲染，那么这篇教程就非常适合你。</p>
<p>我发现 Next.js 是创建 Web 应用的一个很棒的工具，在这篇文章的最后，我希望你会像我一样对它感到兴奋。我也希望它能帮助你学习 Next.js！</p>
<p><a href="https://flaviocopes.com/page/nextjs-handbook/">注意：你可以下载本教程的 PDF/ePub/Mobi 版本，以便你可以离线阅读。</a></p>
<h2 id="">目录</h2>
<ol>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#introduction">介绍</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#the-main-features-provided-by-next-js">Next.js 提供的主要功能</a></li>
<li><a href="#next-js-vs-gatsby-vs-create-react-app">Next.js vs Gatsby vs <code>create-react-app</code></a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#how-to-install-nextjs">如何安装 Next.js</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#view-source-to-confirm-ssr-is-working">查看来源以确认 SSR 的工作</a></li>
<li><a href="#the-app-bundles">The app bundles</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#what-s-that-icon-on-the-bottom-right">右下角的那个图标是什么？</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#install-the-react-developer-tools">安装 React DevTools</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#other-debugging-techniques-you-can-use">你可以使用的其他调试技术</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#adding-a-second-page-to-the-site">在网站上添加第二页</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#linking-the-two-pages">链接这两个页面</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#dynamic-content-with-the-router">路由与动态内容</a></li>
<li><a href="#prefetching-1">Prefetching(预取)</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#using-the-router-to-detect-the-active-link">使用路由器来检测活动链接</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#using-next-router">使用 next/router</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#feed-data-to-the-components-using-getinitialprops">使用 getInitialProps() 向组件提供数据</a></li>
<li><a href="#css">CSS</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#populating-the-head-tag-with-custom-tags">用自定义标签填充 head 标签</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#adding-a-wrapper-component">添加一个封装组件</a></li>
<li><a href="#api-routes">API routes</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#run-code-only-on-the-server-side-or-client-side">在服务器端，或在客户端运行代码</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#deploying-the-production-version">部署生产版本</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#deploying-on-now">部署在 Now</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#analyzing-the-app-bundles">分析应用程序 bundles 的情况</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#lazy-loading-modules">模块懒加载</a></li>
<li><a href="https://chinese.freecodecamp.org/news/the-next-js-handbook/#where-to-go-from-here">今后的发展方向</a></li>
</ol>
<h2 id="introduction">介绍</h2>
<p>开发由 React 支持的现代 JavaScript 应用程序是一件非常棒的事情，直到你意识到有几个与在客户端渲染所有内容有关的问题。</p>
<p>首先，页面加载需要更长的时间才能对用户可见，因为在内容加载之前，所有的 JavaScript 必须加载，你的应用程序需要运行以确定在页面上显示什么。</p>
<p>第二，如果你正在创建一个公开的网站，你有一个内容搜索引擎优化（SEO）的问题。搜索引擎在运行和索引 JavaScript 应用程序方面越来越好，但如果我们能把内容发给它们，而不是让它们自己想办法解决，那就好得多。</p>
<p>解决这两个问题的方法是<strong>服务器渲染（server rendering）</strong>，也叫<strong>静态预渲染（static pre-rendering）</strong>。</p>
<p><a href="https://nextjs.org">Next.js</a> 是一个 React 框架，以一种非常简单的方式完成所有这些工作，但它并不限于此。它的创造者把它宣传为一个<strong>零配置（zero-configuration）、单指令（single-command）的 React 应用工具链</strong>。</p>
<p>它提供了一个通用的结构，使你能够轻松地构建一个前端的 React 应用程序，并透明地为你处理服务器端的渲染。</p>
<h2 id="the-main-features-provided-by-next-js">Next.js提供的主要功能</h2>
<p>下面是一份 Next.js 不完全的主要功能的清单：</p>
<h3 id="hotcodereloading">Hot Code Reloading（代码热加载）</h3>
<p>Next.js 在检测到保存到磁盘的任何变化时，会重新加载页面。</p>
<h3 id="automaticrouting">Automatic Routing（自动路由）</h3>
<p>任何 URL 都被映射到文件系统中，映射到放在 <code>pages</code> 文件夹中的文件，你不需要任何配置（当然你有自定义选项）。</p>
<h3 id="singlefilecomponents">Single File Components（单文件组件）</h3>
<p>使用<code>styled-jsx</code>，完全集成在同一个团队中，为组件添加样式的范围是很简单的。</p>
<h3 id="serverrendering">Server Rendering（服务器端渲染）</h3>
<p>你可以在服务器端渲染 React 组件，然后再将 HTML 发送到客户端。</p>
<h3 id="ecosystemcompatibility">Ecosystem Compatibility（生态系统的兼容性）</h3>
<p>Next.js 与 JavaScript、Node 和 React 生态系统的其他部分配合良好。</p>
<h3 id="automaticcodesplitting">Automatic Code Splitting（自动代码拆分）</h3>
<p>渲染页面时，只需使用它们需要的库和 JavaScript，而无需其他。Next.js 不会生成一个包含所有应用程序代码的单一 JavaScript 文件，而是将应用程序自动分解为几个不同的资源。</p>
<p>加载一个页面只加载该特定页面所需的 JavaScript。</p>
<p>Next.js 通过分析导入的资源来做到这一点。</p>
<p>例如，如果你只有一个页面导入了 Axios 库，那么这个特定的页面将在打包（bundle）的时候包含该库。</p>
<p>这可以确保你的第一个页面加载速度尽可能快，而且只有未来的页面加载（如果它们将被触发）才会向客户端发送所需的 JavaScript。</p>
<p>有一个值得注意的例外。如果经常使用的导入程序在网站页面中至少有一半被使用，它们就会被打包到主 JavaScript 中。</p>
<h3 id="prefetching">Prefetching（预取）</h3>
<p>用于连接不同页面的 <code>Link</code> 组件支持 <code>prefetch</code> prop ，在后台自动预取页面资源（包括因代码分割而丢失的代码）。</p>
<h3 id="dynamiccomponents">Dynamic Components（动态组件）</h3>
<p>你可以动态地导入 JavaScript 模块和 React 组件。</p>
<h3 id="staticexports">Static Exports（静态导出）</h3>
<p>使用<code>next export</code>命令，Next.js 允许你从你的应用程序导出一个完全静态的网站。</p>
<h3 id="typescriptsupporttypescript">TypeScript Support（支持 TypeScript）</h3>
<p>Next.js 是用 TypeScript 编写的，因此，它具有出色的 TypeScript 支持。</p>
<h2 id="nextjsvsgatsbyvscreatereactapp">Next.js vs Gatsby vs <code>create-react-app</code></h2>
<p>Next.js、<a href="https://flaviocopes.com/gatsby/">Gatsby</a> 和 <a href="https://flaviocopes.com/react-create-react-app/"><code>create-react-app</code></a> 是了不起的工具，我们可以用它们来驱动我们的应用程序。</p>
<p>让我们先说说它们的共同点。它们都有 React，为整个开发体验提供支持。它们还抽象了 <a href="https://flaviocopes.com/webpack/">webpack</a> 和所有那些我们在过去的好日子里需要手动配置底层东西。</p>
<p><code>create-react-app</code> 并不能帮助你轻松生成一个服务器端渲染的应用程序。任何与之相关的东西（SEO、速度...）都只能由 Next.js 和 Gatsby 等工具提供。</p>
<p><strong>Next.js 什么时候比 Gatsby 好？</strong></p>
<p>它们都可以帮助 <strong>服务器端渲染(server-side rendering)</strong>，但有两种不同的方式。</p>
<p>使用 Gatsby 的最终结果是一个静态网站生成器，没有服务器。你创建网站，然后将创建过程的结果静态地部署在 Netlify 或其他静态托管网站上。</p>
<p>Next.js 提供了一个可以在服务器端渲染响应请求的后端，允许你创建一个动态网站，这意味着你将把它部署在一个可以运行 Node.js 的平台上。</p>
<p>Next.js 也可以生成静态网站，但我不会说这是它的主要使用场景。</p>
<p>如果我的目标是创建一个静态网站，我将很难选择，Gatsby 可能有一个更好的插件生态系统，包括许多特别用于博客的插件。</p>
<p>Gatsby 在很大程度上也是基于 <a href="https://flaviocopes.com/graphql/">GraphQL</a>，根据你的想法和需要，你可能真的喜欢或不喜欢。</p>
<h2 id="how-to-install-nextjs">如何安装Next.js</h2>
<p>要安装 Next.js，你需要安装 Node.js。</p>
<p>确保你有最新版本的 Node。在终端运行 <code>node -v</code> 进行检查，并与 <a href="https://nodejs.org/">https://nodejs.org/</a> 上列出的最新 LTS 版本进行比较。</p>
<p>在你安装 Node.js 之后，你的命令行中就会有<code>npm</code>命令。</p>
<p>如果你在这个阶段有任何困难，我推荐你看我为你写的以下教程:</p>
<ul>
<li><a href="https://flaviocopes.com/node-installation/">怎样安装 Node.js</a></li>
<li><a href="https://flaviocopes.com/how-to-update-node/">怎样升级 Node.js</a></li>
<li><a href="https://flaviocopes.com/npm/">npm 软件包管理器的介绍</a></li>
<li><a href="https://flaviocopes.com/shells/">Unix Shells 入门</a></li>
<li><a href="https://flaviocopes.com/macos-terminal/">如何使用 macOS 终端</a></li>
<li><a href="https://flaviocopes.com/bash/">The Bash Shell</a></li>
</ul>
<p>现在你有了 Node，更新到了最新版本，还有<code>npm</code>，我们已经准备好了!</p>
<p>我们现在可以选择两条路线：使用 <code>create-next-app</code> 或传统的方法，即手动安装和设置 Next 应用程序。</p>
<h3 id="createnextapp">使用 create-next-app</h3>
<p>如果你熟悉 <a href="https://flaviocopes.com/react-create-react-app/"><code>create-react-app</code></a>，<code>create-next-app</code> 也是一样的，只不过它创建的是一个 Next 应用，而不是 React 应用，正如其名字所暗示的。</p>
<p>我假设你已经安装了 Node.js，从 5.2 版本开始（写这篇文章的时候已经是 2 年多以前了），Node.js 捆绑了 <a href="https://flaviocopes.com/npx/"><code>npx</code>命令</a>。这个方便的工具可以让我们下载并执行一个 JavaScript 命令，我们将这样使用它:</p>
<pre><code class="language-bash">npx create-next-app
</code></pre>
<p>该命令询问应用程序的名称（并以该名称为你创建一个新的文件夹），然后下载所有它需要的包（<code>react</code>，<code>react-dom</code>，<code>next</code>），将 <code>package.json</code> 设置为:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-14-at-16.46.47.png" alt="Screen-Shot-2019-11-14-at-16.46.47" width="600" height="400" loading="lazy"></p>
<p>你可以通过运行 <code>npm run dev</code> 立即运行示例应用程序:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-14-at-16.46.32.png" alt="Screen-Shot-2019-11-14-at-16.46.32" width="600" height="400" loading="lazy"></p>
<p>在下面看到 <a href="http://localhost:3000">http://localhost:3000</a>:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-14-at-16.47.17.png" alt="Screen-Shot-2019-11-14-at-16.47.17" width="600" height="400" loading="lazy"></p>
<p>这是启动 Next.js 应用程序的推荐方法，因为它为你提供了结构和示例代码。 不仅仅是默认的示例应用程序。 您可以使用存储在 <a href="https://github.com/zeit/next.js/tree/canary/examples">https://github.com/zeit/next.js/tree/canary/examples</a> 中的任何示例 ) 使用 <code>--example</code> 选项。 例如尝试:</p>
<pre><code class="language-bash">npx create-next-app --example blog-starter
</code></pre>
<p>这给你提供了一个开箱即用的博客实例，而且还带有语法高亮:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-14-at-17.13.29.png" alt="Screen-Shot-2019-11-14-at-17.13.29" width="600" height="400" loading="lazy"></p>
<h3 id="nextjs">手动创建一个 Next.js 应用程序</h3>
<p>如果你想从头开始创建一个 Next 应用程序，你可以避免 <code>create-next-app</code>。方法是：在你喜欢的任何地方创建一个空文件夹，例如在你的主文件夹中，然后进入该文件夹:</p>
<pre><code class="language-sh">mkdir nextjs
cd nextjs
</code></pre>
<p>并创建你的第一个 Next 项目目录:</p>
<pre><code class="language-sh">mkdir firstproject
cd firstproject
</code></pre>
<p>现在使用<code>npm</code>命令将其初始化为一个 Node 项目:</p>
<pre><code class="language-sh">npm init -y
</code></pre>
<p><code>-y</code>选项告诉<code>npm</code>使用项目的默认设置，生成一个模板文件——<code>package.json</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-16.59.21.png" alt="Screen-Shot-2019-11-04-at-16.59.21" width="600" height="400" loading="lazy"></p>
<p>现在安装 Next 和 React:</p>
<pre><code class="language-sh">npm install next react react-dom
</code></pre>
<p>你的项目文件夹现在应该有两个文件:</p>
<ul>
<li><code>package.json</code> (<a href="https://flaviocopes.com/package-json/">see my tutorial on it</a>)</li>
<li><code>package-lock.json</code> (<a href="https://flaviocopes.com/package-lock-json/">see my tutorial on package-lock</a>)</li>
</ul>
<p>和 <code>node_modules</code> 文件夹。</p>
<p>用你喜欢的编辑器打开项目文件夹。我最喜欢的编辑器是 <a href="https://flaviocopes.com/vscode/">VS Code</a>。如果你安装了该软件，你可以在终端运行 <code>code</code>，在编辑器中打开当前文件夹（如果该命令对你不起作用，请参见 <a href="https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line">this</a>)</p>
<p>打开<code>package.json</code> 文件，它现在有这样的内容:</p>
<pre><code class="language-json">{
  "name": "firstproject",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" &amp;&amp; exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies":  {
    "next": "^9.1.2",
    "react": "^16.11.0",
    "react-dom": "^16.11.0"
  }
}
</code></pre>
<p>并将 <code>scripts</code> 部分改为:</p>
<pre><code class="language-json">"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}
</code></pre>
<p>来添加 Next.js 的构建命令，我们很快就会用到它。</p>
<p>提示：使用 <code>"dev": "next -p 3001",</code> 来改变端口，在本例中，运行在 3001 端口。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-17.01.03.png" alt="Screen-Shot-2019-11-04-at-17.01.03" width="600" height="400" loading="lazy"></p>
<p>现在创建一个<code>pages</code>文件夹，并添加一个<code>index.js</code>文件。</p>
<p>在这个文件中，让我们创建我们的第一个 React 组件。</p>
<p>我们将使用它作为默认输出:</p>
<pre><code class="language-js">const Index = () =&gt; (
  &lt;div&gt;
    &lt;h1&gt;Home page&lt;/h1&gt;
  &lt;/div&gt;
)

export default Index
</code></pre>
<p>现在使用终端，运行 <code>npm run dev</code> 来启动 Next 开发服务器。</p>
<p>这将使应用程序在本地主机上的 3000 端口可用。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-11.24.02.png" alt="Screen-Shot-2019-11-04-at-11.24.02" width="600" height="400" loading="lazy"></p>
<p>Open <a href="http://localhost:3000">http://localhost:3000</a> in your browser to see it.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-11.24.23.png" alt="Screen-Shot-2019-11-04-at-11.24.23" width="600" height="400" loading="lazy"></p>
<h2 id="view-source-to-confirm-ssr-is-working">查看来源以确认SSR的工作</h2>
<p>现在让我们检查一下这个应用程序是否按照我们期望的那样工作。这是一个 Next.js 应用程序，所以它应该是 <strong>服务器端渲染的(server side rendered)</strong>。</p>
<p>这是 Next.js 的主要卖点之一：如果我们使用 Next.js 创建一个网站，网站页面会在服务器上渲染，而服务器会将 HTML 传递给浏览器。</p>
<p>这有 3 个主要好处:</p>
<ul>
<li>客户端不需要实例化 React 来渲染，这使得网站对你的用户来说更快。</li>
<li>搜索引擎会对页面进行索引，而不需要运行客户端的 JavaScript。谷歌开始解决这个问题（客户端渲染），但公开承认是一个较慢的过程（如果你想获得好的排名，你应该尽可能地帮助谷歌）。</li>
<li>你可以有社交媒体元标签，对添加预览图片，为你在 Facebook、Twitter 上分享的任何页面定制标题和描述都很有用。</li>
</ul>
<p>让我们查看一下应用程序的源代码。<br>
使用 Chrome 浏览器，你可以在页面的任何地方点击右键，然后按 <strong>查看网页源代源（View Page Source）</strong>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-11.33.10.png" alt="Screen-Shot-2019-11-04-at-11.33.10" width="600" height="400" loading="lazy"></p>
<p>如果你查看页面的源代码，你会看到 HTML<code>body</code>中的<code>&lt;div&gt;&lt;h1&gt;Home page&lt;/h1&gt;&lt;/div&gt;</code>片段，以及一堆 JavaScript 文件——程序打包出的。</p>
<p>我们不需要设置什么，SSR（服务器端渲染）已经在为我们工作了。</p>
<p>React 应用程序将在客户端启动，并将是一个使用客户端渲染来支持点击链接等交互的应用程序。但重新加载一个页面将从服务器上重新加载。而使用 Next.js，在浏览器中的结果应该是没有区别的——服务器渲染的页面看起来应该和客户端渲染的页面一模一样。</p>
<h2 id="theappbundles">The app bundles</h2>
<p>当我们查看页面源代码时，我们看到一堆 JavaScript 文件正在被加载:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-11.34.41.png" alt="Screen-Shot-2019-11-04-at-11.34.41" width="600" height="400" loading="lazy"></p>
<p>让我们先把代码放在 <a href="https://htmlformatter.com/">HTML formatter</a> 中，使代码格式化得更好，这样我们更好地理解它:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;

&lt;head&gt;
    &lt;meta charSet="utf-8" /&gt;
    &lt;meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1" /&gt;
    &lt;meta name="next-head-count" content="2" /&gt;
    &lt;link rel="preload" href="/_next/static/development/pages/index.js?ts=1572863116051" as="script" /&gt;
    &lt;link rel="preload" href="/_next/static/development/pages/_app.js?ts=1572863116051" as="script" /&gt;
    &lt;link rel="preload" href="/_next/static/runtime/webpack.js?ts=1572863116051" as="script" /&gt;
    &lt;link rel="preload" href="/_next/static/runtime/main.js?ts=1572863116051" as="script" /&gt;
&lt;/head&gt;

&lt;body&gt;
    &lt;div id="__next"&gt;
        &lt;div&gt;
            &lt;h1&gt;Home page&lt;/h1&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;script src="/_next/static/development/dll/dll_01ec57fc9b90d43b98a8.js?ts=1572863116051"&gt;&lt;/script&gt;
    &lt;script id="__NEXT_DATA__" type="application/json"&gt;{"dataManager":"[]","props":{"pageProps":{}},"page":"/","query":{},"buildId":"development","nextExport":true,"autoExport":true}&lt;/script&gt;
    &lt;script async="" data-next-page="/" src="/_next/static/development/pages/index.js?ts=1572863116051"&gt;&lt;/script&gt;
    &lt;script async="" data-next-page="/_app" src="/_next/static/development/pages/_app.js?ts=1572863116051"&gt;&lt;/script&gt;
    &lt;script src="/_next/static/runtime/webpack.js?ts=1572863116051" async=""&gt;&lt;/script&gt;
    &lt;script src="/_next/static/runtime/main.js?ts=1572863116051" async=""&gt;&lt;/script&gt;
&lt;/body&gt;

&lt;/html&gt;
</code></pre>
<p>我们有 4 个 JavaScript 文件被声明要在 <code>head</code> 中预加载，使用<br>
<code>rel="preload" as="script"</code>:</p>
<ul>
<li><code>/_next/static/development/pages/index.js</code> (96 LOC)</li>
<li><code>/_next/static/development/pages/_app.js</code> (5900 LOC)</li>
<li><code>/_next/static/runtime/webpack.js</code> (939 LOC)</li>
<li><code>/_next/static/runtime/main.js</code> (12k LOC)</li>
</ul>
<p>这告诉浏览器在正常的渲染流程开始之前，尽快开始加载这些文件。如果没有这些，脚本的加载会有额外的延迟，这就提高了页面的加载性能。</p>
<p>然后这 4 个文件被加载到 <code>body</code> 的末尾，还有<code>/_next/static/development/dll/dll_01ec57fc9b90d43b98a8.js</code>（31k LOC），以及一个为页面数据设置一些默认值的 JSON 片段:</p>
<pre><code class="language-html">&lt;script id="__NEXT_DATA__" type="application/json"&gt;
{
  "dataManager": "[]",
  "props": {
    "pageProps":  {}
  },
  "page": "/",
  "query": {},
  "buildId": "development",
  "nextExport": true,
  "autoExport": true
}
&lt;/script&gt;
</code></pre>
<p>所加载的 4 个 bundle 文件已经实现了一个叫做代码分割(code splitting)的功能。<code>index.js</code> 文件提供了 <code>index</code> 组件所需的代码，它为<code>/</code>路由提供服务，如果我们有更多的页面，我们将为每个页面提供更多的 bundle，然后只有在需要时才会被加载——为页面提供一个更高性能的加载时间。</p>
<h2 id="what-s-that-icon-on-the-bottom-right">右下角的那个图标是什么？</h2>
<p>你看到页面右下方那个像闪电的小图标了吗？</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-13.21.42.png" alt="Screen-Shot-2019-11-04-at-13.21.42" width="600" height="400" loading="lazy"></p>
<p>如果你把它悬停，它就会显示 "Prerendered Page"（预渲染的页面）:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-13.21.46.png" alt="Screen-Shot-2019-11-04-at-13.21.46" width="600" height="400" loading="lazy"></p>
<p>这个图标当然只在 <em>开发模式下可见</em>，它告诉你这个页面符合自动静态优化的条件，这基本上意味着它不依赖于需要在调用时获取的数据，它可以在构建时（当我们运行<code>npm run build</code>时）预先渲染并构建为静态 HTML 文件。</p>
<p>下一步可以通过页面组件上没有 <code>getInitialProps()</code>方法来确定。</p>
<p>在这种情况下，我们的页面可以更快，因为它将被静态化的一个 HTML 文件提供，而不是通过 Node.js 服务器生成 HTML 输出。</p>
<p>另一个有用的图标可能会出现在它旁边，或者在非预渲染页面上代替它，是一个小的动画三角形:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-14-at-14.56.21.png" alt="Screen-Shot-2019-11-14-at-14.56.21" width="600" height="400" loading="lazy"></p>
<p>这是一个编译指示器，当你保存一个页面，Next.js 正在编译应用程序，然后热代码重载启动，自动重新加载应用程序中的代码时，它就会出现。</p>
<p>这是一个非常好的方法，可以立即确定应用程序是否已经被编译，你可以测试你正在做的一部分。</p>
<h2 id="install-the-react-developer-tools">安装React DevTools</h2>
<p>Next.js 是基于 React 的，所以我们绝对需要安装一个非常有用的工具（如果你还没有），那就是 React 开发者工具（React Developer Tools）。</p>
<p>React 开发者工具同时适用于 <a href="https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en">Chrome</a> 和 <a href="https://addons.mozilla.org/en-US/firefox/addon/react-devtools/">Firefox</a>，它是你用来检查 React 应用程序的一个重要工具。</p>
<p>现在，React 开发者工具并不是专门针对 Next.js 的，但我想介绍一下，因为你可能对 React 提供的所有工具还不是 100%熟悉。与其假设你已经知道它们，不如去了解一下调试工具的情况。</p>
<p>他们提供了一个检查器，揭示了构建你的页面的 React 组件树，对于每个组件，你可以去检查 props、state、hooks，还有很多。</p>
<p>一旦你安装了 React 开发者工具，你可以打开常规的浏览器 devtools（在 Chrome 中，在页面上点击右键，然后点击<code>Inspect</code>），你会发现两个新的面板。<strong>Components</strong> 和 <strong>Profiler</strong>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-14.26.12.png" alt="Screen-Shot-2019-11-04-at-14.26.12" width="600" height="400" loading="lazy"></p>
<p>如果你把鼠标移到组件(components)上，你会看到，在页面中，浏览器会选择由该组件渲染的部分。</p>
<p>如果你选择树中的任何一个组件(components)，右边的面板就会显示对的**父组件(parent component)**的引用，以及传递给它的 props:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-14.27.05.png" alt="Screen-Shot-2019-11-04-at-14.27.05" width="600" height="400" loading="lazy"></p>
<p>你可以通过点击组件(components)名称来轻松查找。</p>
<p>你可以点击开发工具工具栏中的 <strong>眼睛图标</strong> 来检查 DOM 元素，另外，如果你使用第一个图标，即带有鼠标图标的图标（它方便地位于类似的常规 DevTools 图标下），你可以在浏览器 UI 中悬停一个元素，直接选择渲染它的 React 组件。</p>
<p>你可以使用 <code>bug</code> 图标来记录一个组件的数据到控制台。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-14.31.25.png" alt="Screen-Shot-2019-11-04-at-14.31.25" width="600" height="400" loading="lazy"></p>
<p>这非常棒，因为一旦你把数据打印出来，你可以右击任何元素，然后按 "Store as a global variable"(存储为全局变量)。例如，在这里我对<code>url</code> prop 做了这个操作，我能够使用分配给它的临时变量 <code>temp1</code> 在控制台中检查它。:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-14.40.22.png" alt="Screen-Shot-2019-11-04-at-14.40.22" width="600" height="400" loading="lazy"></p>
<p>使用 Next.js 在开发模式下自动加载的 <strong>Source Maps</strong>，，我们可以在组件面板上点击<code>&lt;&gt;</code>代码，DevTools 将切换到 <code>Source panel</code>，向我们展示组件的源代码:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-14.41.33.png" alt="Screen-Shot-2019-11-04-at-14.41.33" width="600" height="400" loading="lazy"></p>
<p>如果可能的话，<strong>Profiler</strong> 标签甚至更棒。它允许我们在应用程序中<strong>record an interaction(记录一个交互)</strong>，并看看会发生什么。我还不能展示一个例子，因为它需要至少 2 个组件来创建一个交互，而我们现在只有一个。我以后再谈这个问题。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-14.42.24.png" alt="Screen-Shot-2019-11-04-at-14.42.24" width="600" height="400" loading="lazy"></p>
<p>我使用 Chrome 浏览器展示了所有的截图，但 React 开发工具在 Firefox 中的工作方式是一样的:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-14.45.20.png" alt="Screen-Shot-2019-11-04-at-14.45.20" width="600" height="400" loading="lazy"></p>
<h2 id="other-debugging-techniques-you-can-use">你可以使用的其他调试技术</h2>
<p>除了 React 开发者工具（这是构建 Next.js 应用程序所必需的）之外，我想强调调试 Next.js 应用程序的 2 种方法。</p>
<p>第一个显然是<code>console.log()</code>和所有<a href="https://flaviocopes.com/console-api/">其他 Console API</a>工具。Next 应用程序的工作方式会使日志语句在浏览器控制台或在你使用<code>npm run dev</code>启动 Next 的终端中发挥作用。</p>
<p>特别是，如果页面从服务器上加载，当你把 URL 指向它，或者你点击刷新按钮/cmd/ctrl-R，任何控制台日志都会在终端发打印。</p>
<p>随后通过点击鼠标发生的页面转换将使所有的控制台记录发生在浏览器内。</p>
<p>如果你对缺失的日志记录感到惊讶，请记住。</p>
<p>另一个必不可少的工具是 <code>debugger</code> 语句。将此语句添加到一个组件中，将暂停浏览器渲染页面。:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-15.10.32.png" alt="Screen-Shot-2019-11-04-at-15.10.32" width="600" height="400" loading="lazy"></p>
<p>真的很厉害，因为现在你可以使用浏览器调试器来检查数值，并逐行运行你的应用程序。</p>
<p>你也可以使用 VS 代码调试器来调试服务器端的代码。我提到这个技术和<a href="https://github.com/Microsoft/vscode-recipes/tree/master/Next-js">这个教程</a>来设置这个。</p>
<h2 id="adding-a-second-page-to-the-site">在网站上添加第二页</h2>
<p>现在我们已经很好地掌握了可以用来帮助我们开发 Next.js 应用程序的工具，让我们从我们的第一个应用程序的基础上继续前进吧:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-13.21.42-1.png" alt="Screen-Shot-2019-11-04-at-13.21.42-1" width="600" height="400" loading="lazy"></p>
<p>我想给这个网站添加第二个页面，一个博客。它将被送入<code>/blog</code>，目前它只包含一个简单的静态页面，就像我们的第一个<code>index.js</code>组件（组件）一样:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-15.39.40.png" alt="Screen-Shot-2019-11-04-at-15.39.40" width="600" height="400" loading="lazy"></p>
<p>保存新文件后，已经运行的<code>npm run dev</code>进程已经能够渲染页面，不需要再重新启动。</p>
<p>当我们点击 URL <a href="http://localhost:3000/blog">http://localhost:3000/blog</a>，我们就有了新的页面:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-15.41.39.png" alt="Screen-Shot-2019-11-04-at-15.41.39" width="600" height="400" loading="lazy"></p>
<p>以下是终端告诉我们的情况:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-15.41.03.png" alt="Screen-Shot-2019-11-04-at-15.41.03" width="600" height="400" loading="lazy"></p>
<p>现在，URL 是<code>/blog</code>这一事实只取决于文件名，以及它在<code>pages</code>文件夹下的位置。</p>
<p>你可以创建一个<code>pages/hey/ho</code>页面，该页面将显示在 URL <a href="http://localhost:3000/hey/ho">http://localhost:3000/hey/ho</a>上。</p>
<p>对于 URL 的目的来说，文件中的组件名称并不重要。</p>
<p>试着去查看页面的源代码，当从服务器加载时，它会列出<code>/_next/static/development/pages/blog.js</code>作为加载的包（bundle）之一，而不是像主页那样列出<code>/_next/static/development/pages/index.js</code>。这是因为由于自动代码拆分，我们不需要为主页服务的 bundle。只需要服务于博客页面的 bundle。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-16.24.53.png" alt="Screen-Shot-2019-11-04-at-16.24.53" width="600" height="400" loading="lazy"></p>
<p>我们也可以直接从<code>blog.js</code>导出一个匿名函数:</p>
<pre><code class="language-js">export default () =&gt; (
  &lt;div&gt;
    &lt;h1&gt;Blog&lt;/h1&gt;
  &lt;/div&gt;
)
</code></pre>
<p>或者如果你喜欢非箭头的函数语法:</p>
<pre><code class="language-js">export default function() {
  return (
    &lt;div&gt;
      &lt;h1&gt;Blog&lt;/h1&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<h2 id="linking-the-two-pages">链接这两个页面</h2>
<p>现在我们有两个页面，分别由<code>index.js</code>和<code>blog.js</code>定义，我们可以引入链接。</p>
<p>页面内的普通 HTML 链接使用 <code>a</code> 标签完成:</p>
<pre><code class="language-html">&lt;a href="/blog"&gt;Blog&lt;/a&gt;
</code></pre>
<p>我们不能在 Next.js 中这样做。</p>
<p>为什么？当然，我们在技术上是 <em>可以的</em>，因为这是网络，而且在网络上，事情永远不会中断（这就是为什么我们仍然可以使用<code>&lt;marquee&gt;</code>标签。但是，使用 Next 的主要好处之一是，一旦一个页面被加载，由于客户端的渲染，过渡到其他页面的速度非常快。</p>
<p>如果你使用一个普通的<code>a</code>链接:</p>
<pre><code class="language-js">const Index = () =&gt; (
  &lt;div&gt;
    &lt;h1&gt;Home page&lt;/h1&gt;
    &lt;a href='/blog'&gt;Blog&lt;/a&gt;
  &lt;/div&gt;
)

export default Index
</code></pre>
<p>现在打开 <strong>DevTools</strong>，特别是 <strong>Network panel</strong>。第一次加载<code>http://localhost:3000/</code>时，我们得到了所有加载的页面 bundles:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-16.26.00.png" alt="Screen-Shot-2019-11-04-at-16.26.00" width="600" height="400" loading="lazy"></p>
<p>现在，如果你点击 "Preserve log" 按钮（以避免清除网络面板），并点击 "Blog" 链接，就会发生这种情况:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-16.27.16.png" alt="Screen-Shot-2019-11-04-at-16.27.16" width="600" height="400" loading="lazy"></p>
<p>我们又从服务器上得到了所有的 JavaScript! 但是...如果我们已经得到了所有的 JavaScript，我们就不需要这些了。我们只需要<code>blog.js</code>页面 bundle，这是唯一一个新的页面。</p>
<p>为了解决这个问题，我们使用 Next 提供的一个组件，叫做 <code>Link</code>。</p>
<p>我们导入它:</p>
<pre><code class="language-js">import Link from 'next/link'
</code></pre>
<p>然后我们用它来包住（warp）我们的链接，像这样:</p>
<pre><code class="language-js">import Link from 'next/link'

const Index = () =&gt; (
  &lt;div&gt;
    &lt;h1&gt;Home page&lt;/h1&gt;
    &lt;Link href='/blog'&gt;
      &lt;a&gt;Blog&lt;/a&gt;
    &lt;/Link&gt;
  &lt;/div&gt;
)

export default Index
</code></pre>
<p>现在，如果你重试我们之前做的事情，你将能够看到，当我们移动到博客页面时，只有<code>blog.js</code> bundle 被加载。:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-04-at-16.35.18.png" alt="Screen-Shot-2019-11-04-at-16.35.18" width="600" height="400" loading="lazy"></p>
<p>而且页面的加载速度比以前快了很多，浏览器通常在标签上的旋转器甚至都没有出现。然而网址却变了，你可以看到。这是与浏览器<a href="https://flaviocopes.com/history-api/">历史 API</a>的无缝工作。</p>
<p>这就是客户端渲染的作用。</p>
<p>如果你现在按下后退按钮呢？没有什么被加载，因为浏览器仍然有旧的<code>index.js</code> bundle，准备加载<code>/index</code>路由。这都是自动的!</p>
<h2 id="dynamic-content-with-the-router">路由与动态内容</h2>
<p>在上一章中，我们看到了如何将主页(index)链接到博客(blog)页面。</p>
<p>博客是 Next.js 的一个很好的用例，在本章中我们将通过添加 <strong>博客文章</strong> 来继续探索。</p>
<p>博客文章有一个动态的 URL。例如，一篇题为 "Hello World "的文章的 URL 是<code>/blog/hello-world</code>。一篇题为 "我的第二篇文章 "的文章的 URL 是<code>/blog/my-second-post</code>。</p>
<p>这些内容是动态的，可能取自数据库、markdown 文件或更多。</p>
<p>Next.js 可以根据一个**dynamic URL(动态 URL)**来提供动态内容。</p>
<p>我们通过使用<code>[]</code>语法创建一个动态页面来创建一个动态 URL。</p>
<p>如何创建？我们添加一个<code>pages/blog/[id].js</code>文件。这个文件将处理<code>/blog/</code>路径下的所有动态 URL，比如我们上面提到的那些。<code>/blog/hello-world</code>, <code>/blog/my-second-post</code>等等。</p>
<p>在文件名中，方括号内的<code>[id]</code>意味着任何动态的东西都将被放在 **路由的查询属性（query property）**的<code>id</code>参数中。</p>
<p>好吧，这一次的事情有点多了。</p>
<p>什么是<strong>路由（router）</strong>？</p>
<p>路由(router)是 Next.js 提供的一个库。</p>
<p>我们从 <code>next/router</code> 导入它:</p>
<pre><code class="language-js">import { useRouter } from 'next/router'
</code></pre>
<p>而一旦我们有了<code>useRouter</code>，我们就用<code>useRouter</code>来实例化路由对象：</p>
<pre><code class="language-js">const router = useRouter()
</code></pre>
<p>一旦我们有了这个路由对象，我们就可以从中提取信息。</p>
<p>特别是我们可以通过访问<code>router.query.id</code>来获得<code>[id].js</code>文件中 URL 的动态部分。</p>
<p>动态部分也可以只是 URL 的一部分，如<code>post-[id].js</code>。</p>
<p>所以让我们继续在实践中应用所有这些东西。</p>
<p>创建文件<code>pages/blog/[id].js</code>:</p>
<pre><code class="language-js">import { useRouter } from 'next/router'

export default () =&gt; {
  const router = useRouter()

  return (
    &lt;&gt;
      &lt;h1&gt;Blog post&lt;/h1&gt;
      &lt;p&gt;Post id: {router.query.id}&lt;/p&gt;
    &lt;/&gt;
  )
}
</code></pre>
<p>现在，如果你去<code>http://localhost:3000/blog/test</code>路由，你应该看到这个：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-05-at-16.41.32.png" alt="Screen-Shot-2019-11-05-at-16.41.32" width="600" height="400" loading="lazy"></p>
<p>我们可以使用这个 <code>id</code> 参数，从一个帖子列表中收集帖子。比如说，从一个数据库中。为了简单起见，我们将在项目根目录下添加一个<code>posts.json</code>文件：</p>
<pre><code class="language-js">{
  "test": {
    "title": "test post",
    "content": "Hey some post content"
  },
  "second": {
    "title": "second post",
    "content": "Hey this is the second post content"
  }
}
</code></pre>
<p>现在我们可以导入它，并从<code>id</code>键中查找帖子：</p>
<pre><code class="language-js">import { useRouter } from 'next/router'
import posts from '../../posts.json'

export default () =&gt; {
  const router = useRouter()

  const post = posts[router.query.id]

  return (
    &lt;&gt;
      &lt;h1&gt;{post.title}&lt;/h1&gt;
      &lt;p&gt;{post.content}&lt;/p&gt;
    &lt;/&gt;
  )
}
</code></pre>
<p>重新加载页面应该会显示这个结果：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-05-at-16.44.07.png" alt="Screen-Shot-2019-11-05-at-16.44.07" width="600" height="400" loading="lazy"></p>
<p>但事实并非如此! 相反，我们在控制台得到一个错误，在浏览器中也得到一个错误：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-05-at-18.18.17.png" alt="Screen-Shot-2019-11-05-at-18.18.17" width="600" height="400" loading="lazy"></p>
<p>为什么？因为在渲染过程中，当组件被初始化时，数据还不在那里。我们将在下一课看到如何用 getInitialProps 向组件提供数据。</p>
<p>现在，在返回 JSX 之前添加一个小的<code>if (!post) return &lt;p&gt;&lt;/p&gt;</code>进行检查：</p>
<pre><code class="language-js">import { useRouter } from 'next/router'
import posts from '../../posts.json'

export default () =&gt; {
  const router = useRouter()

  const post = posts[router.query.id]
  if (!post) return &lt;p&gt;&lt;/p&gt;

  return (
    &lt;&gt;
      &lt;h1&gt;{post.title}&lt;/h1&gt;
      &lt;p&gt;{post.content}&lt;/p&gt;
    &lt;/&gt;
  )
}
</code></pre>
<p>现在，事情应该工作了。最初，该组件的渲染没有动态的<code>router.query.id</code>信息。渲染后，Next.js 触发了查询值的更新，页面显示了正确的信息。</p>
<p>如果你查看源代码，在 HTML 中会有一个空的<code>&lt;p&gt;</code>标签：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-05-at-18.20.58.png" alt="Screen-Shot-2019-11-05-at-18.20.58" width="600" height="400" loading="lazy"></p>
<p>我们很快就会解决这个问题，它不能实现 SSR，这既损害了用户的加载时间，也损害了 SEO 和社交分享，我们已经讨论过了。</p>
<p>我们可以通过在<code>pages/blog.js</code>中列出这些帖子来完成博客的例子：</p>
<pre><code class="language-js">import posts from '../posts.json'

const Blog = () =&gt; (
  &lt;div&gt;
    &lt;h1&gt;Blog&lt;/h1&gt;

    &lt;ul&gt;
      {Object.entries(posts).map((value, index) =&gt; {
        return &lt;li key={index}&gt;{value[1].title}&lt;/li&gt;
      })}
    &lt;/ul&gt;
  &lt;/div&gt;
)

export default Blog
</code></pre>
<p>我们可以通过从<code>next/link</code>中导入<code>Link</code>并在帖子循环中使用它，将它们链接到各个帖子页面：</p>
<pre><code class="language-js">import Link from 'next/link'
import posts from '../posts.json'

const Blog = () =&gt; (
  &lt;div&gt;
    &lt;h1&gt;Blog&lt;/h1&gt;

    &lt;ul&gt;
      {Object.entries(posts).map((value, index) =&gt; {
        return (
          &lt;li key={index}&gt;
            &lt;Link href='/blog/[id]' as={'/blog/' + value[0]}&gt;
              &lt;a&gt;{value[1].title}&lt;/a&gt;
            &lt;/Link&gt;
          &lt;/li&gt;
        )
      })}
    &lt;/ul&gt;
  &lt;/div&gt;
)

export default Blog
</code></pre>
<h2 id="prefetching">Prefetching</h2>
<p>我之前提到过 Next.js <code>Link</code> 组件可以用来创建 2 个页面之间的链接，当你使用它时，Next.js 会<strong>透明地为我们处理前端路由</strong>，所以当用户点击一个链接时，前端会负责显示新的页面，而不会像通常网页那样触发新的客户/服务器请求和响应周期。</p>
<p>当你使用 <code>Link</code> 时，Next.js 还为你做了一件事。</p>
<p>只要被<code>&lt;Link&gt;</code>包裹的元素出现在视口（viewport）中（这意味着网站用户可以看到它），Next.js 就会预取(prefetch)它所指向的 URL，只要它是一个本地链接（在你的网站上），就会使应用程序对浏览者来说超级快速。</p>
<p>这种行为只在<strong>生产模式（production mode）</strong> 下被触发（我们稍后会深入讨论这个问题），这意味着如果你用<code>npm run dev</code>运行应用程序，你必须停止它，用<code>npm run build</code>编译你的生产包，用<code>npm run start</code>运行它。</p>
<p>使用 DevTools 中的 Network inspector，你会注意到在页面加载时，任何在折叠上方的链接都会在你的页面上触发<code>load</code>事件（当页面完全加载时触发，发生在<code>DOMContentLoaded</code>事件之后）时开始预取（prefetch）。</p>
<p>任何不在视口（viewport）中的其他<code>链接</code>标签将被预取（prefetch），当用户滚动时，它将被预取（prefetch）。</p>
<p>预取在高速连接（Wifi 和 3g 以上连接）上是自动的，除非浏览器发送 <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Save-Data"><code>Save-Data</code> HTTP Header</a>。</p>
<p>你可以通过设置 <code>prefetch</code> prop 为 <code>false</code>来选择不预取单个 <code>Link</code> 实例：</p>
<pre><code class="language-jsx">&lt;Link href="/a-link" prefetch={false}&gt;
  &lt;a&gt;A link&lt;/a&gt;
&lt;/Link&gt;
</code></pre>
<h2 id="using-the-router-to-detect-the-active-link">使用路由器来检测活动链接</h2>
<p>在处理链接时，一个非常重要的功能是确定什么是当前的 URL，特别是给活动链接分配一个类别，这样我们就可以使它的样式与其他的不同。</p>
<p>例如，这在你的网站标题中特别有用。</p>
<p>Next.js 在<code>next/link</code>中提供的<code>Link</code>组件默认并不自动为我们做这些。</p>
<p>我们可以自己创建一个 Link 组件，并将其存储在 Components 文件夹下的<code>Link.js</code>文件中，然后导入该组件，而不是默认的<code>next/link</code>。</p>
<p>在这个组件中，我们首先从<code>react</code>导入 React，从<code>next/link</code>导入 Link，从<code>next/router</code>导入<code>useRouter</code> hook。</p>
<p>在组件内部，我们确定当前的路径名称是否与组件的<code>href</code> prop 相匹配，如果是，我们将 <code>selected</code> 类追加到子节点(children)上。</p>
<p>最后，我们使用<code>React.cloneElement()</code> 返回带有更新后的类的子节点（children）：</p>
<pre><code class="language-js">import React from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'

export default ({ href, children }) =&gt; {
  const router = useRouter()

  let className = children.props.className || ''
  if (router.pathname === href) {
    className = `${className} selected`
  }

  return &lt;Link href={href}&gt;{React.cloneElement(children, { className })}&lt;/Link&gt;
}
</code></pre>
<h2 id="using-next-router">使用 next/router</h2>
我们已经看到了如何使用 Link 组件来声明式地处理 Next.js 应用程序中的路由。
<p>在 JSX 中管理路由真的很方便，但有时你需要以编程方式触发路由变化。</p>
<p>在这种情况下，你可以直接访问 Next.js Router，在<code>next/router</code>包中提供，并调用其<code>push()</code>方法。</p>
<p>下面是一个访问路由的例子：</p>
<pre><code class="language-js">import { useRouter } from 'next/router'

export default () =&gt; {
  const router = useRouter()
  //...
}
</code></pre>
<p>一旦我们通过调用<code>useRouter()</code>得到路由对象，我们就可以使用它的方法。</p>
<p>这是客户端的路由，所以方法应该只在面向前端的代码中使用。确保这一点的最简单方法是在<code>useEffect()</code>React hook 中调用，或在<code>componentDidMount()</code>中调用 React 有状态组件。</p>
<p>你可能最常使用的是<code>push()</code>和<code>prefetch()</code>。</p>
<p><code>push()</code>允许我们在前端以编程方式触发 URL 变化：</p>
<pre><code class="language-js">router.push('/login')
</code></pre>
<p><code>prefetch()</code>允许我们以编程方式预取（prefetch）一个 URL，当我们没有自动处理预取的<code>Link</code>标签时很有用：</p>
<pre><code class="language-js">router.prefetch('/login')
</code></pre>
<p>Full example:</p>
<pre><code class="language-js">import { useRouter } from 'next/router'

export default () =&gt; {
  const router = useRouter()

  useEffect(() =&gt; {
    router.prefetch('/login')
  })
}
</code></pre>
<p>你也可以使用路由来监听 <a href="https://nextjs.org/docs#router-events">路由变更事件</a>。</p>
<h2 id="feed-data-to-the-components-using-getinitialprops">使用 getInitialProps() 向组件提供数据</h2>
<p>在上一章中，我们在动态生成帖子页面时遇到了一个问题，因为该组件需要一些前期的数据，我们试图从 JSON 文件中获取数据时：</p>
<pre><code class="language-js">import { useRouter } from 'next/router'
import posts from '../../posts.json'

export default () =&gt; {
  const router = useRouter()

  const post = posts[router.query.id]

  return (
    &lt;&gt;
      &lt;h1&gt;{post.title}&lt;/h1&gt;
      &lt;p&gt;{post.content}&lt;/p&gt;
    &lt;/&gt;
  )
}
</code></pre>
<p>我们得到了这个错误：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-05-at-18.18.17-1.png" alt="Screen-Shot-2019-11-05-at-18.18.17-1" width="600" height="400" loading="lazy"></p>
<p>我们如何解决这个问题？我们又如何使 SSR 工作于动态路由？</p>
<p>我们必须为组件提供 props，使用一个名为 <code>getInitialProps()</code> 的特殊函数，它被附加到组件上</p>
<p>要做到这一点，首先我们要为该组件命名：</p>
<pre><code class="language-js">const Post = () =&gt; {
  //...
}

export default Post
</code></pre>
<p>then we add the function to it:</p>
<pre><code class="language-js">const Post = () =&gt; {
  //...
}

Post.getInitialProps = () =&gt; {
  //...
}

export default Post
</code></pre>
<p>这个函数得到一个对象作为其参数，其中包含几个属性。特别是，我们现在感兴趣的是，我们得到<code>query</code>对象，也就是我们之前用来得到帖子 id 的对象。</p>
<p>所以我们可以用 <em>对象析构(object destructuring)</em> 的语法来获得它：</p>
<pre><code class="language-js">Post.getInitialProps = ({ query }) =&gt; {
  //...
}
</code></pre>
<p>现在我们可以从这个函数中返回帖子(post)：</p>
<pre><code class="language-js">Post.getInitialProps = ({ query }) =&gt; {
  return {
    post: posts[query.id]
  }
}
</code></pre>
<p>我们也可以删除 <code>useRouter</code> 的导入，我们从传递给<code>Post</code> 组件的<code>props</code>属性中获得帖子(post)：</p>
<pre><code class="language-js">import posts from '../../posts.json'

const Post = props =&gt; {
  return (
    &lt;div&gt;
      &lt;h1&gt;{props.post.title}&lt;/h1&gt;
      &lt;p&gt;{props.post.content}&lt;/p&gt;
    &lt;/div&gt;
  )
}

Post.getInitialProps = ({ query }) =&gt; {
  return {
    post: posts[query.id]
  }
}

export default Post
</code></pre>
<p>现在不会有错误了，而且 SSR 会像预期的那样工作，你可以看到查看源代码：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-05-at-18.53.02.png" alt="Screen-Shot-2019-11-05-at-18.53.02" width="600" height="400" loading="lazy"></p>
<p><code>getInitialProps</code>函数将在服务器端执行，但也会在客户端执行，当我们使用<code>Link</code>组件导航到一个新页面时，就像我们所做的那样。</p>
<p>值得注意的是，<code>getInitialProps</code> 在它收到的上下文对象(context object )中，除了<code>query</code>对象外，还得到了其他的属性：</p>
<ul>
<li><code>pathname</code>: URL 的 <code>path</code> 部分</li>
<li><code>asPath</code> - 浏览器中显示的实际路径（包括查询 query）的字符串</li>
</ul>
<p>在调用<code>http://localhost:3000/blog/test</code>的情况下，将分别导致：</p>
<ul>
<li><code>/blog/[id]</code></li>
<li><code>/blog/test</code></li>
</ul>
<p>而在服务器端渲染的情况下，它也将收到：</p>
<ul>
<li><code>req</code>: HTTP request 对象</li>
<li><code>res</code>: HTTP response 对象</li>
<li><code>err</code>: error 对象</li>
</ul>
<p>如果你做过任何 Node.js 编码，<code>req</code>和<code>res</code>将是你熟悉的。</p>
<h2 id="css">CSS</h2>
<p>我们如何在 Next.js 中设计 React 组件的样式？</p>
<p>我们有很大的自由度，因为我们可以使用任何我们喜欢的库。</p>
<p>但 Next.js 内置了<a href="https://github.com/zeit/styled-jsx"><code>styled-jsx</code></a>，因为那是由维护 Next.js 的人创建的一个库。</p>
<p>这是一个很酷的库，它为我们提供了范围广泛的 CSS，这对可维护性非常好，因为 CSS 只影响它所应用的组件。</p>
<p>我认为这是一个写 CSS 的好方法，不需要应用额外的库或预处理器来增加复杂性。</p>
<p>要在 Next.js 中为 React 组件添加 CSS，我们要在 JSX 的一个片段中插入它，该片段以</p>
<pre><code class="language-js">&lt;style jsx&gt;{`
</code></pre>
<p>并以以下内容结束</p>
<pre><code class="language-js">`}&lt;/style&gt;
</code></pre>
<p>在这个奇怪的块中，我们写了普通的 CSS，就像我们在<code>.css</code>文件中做的那样：</p>
<pre><code class="language-js">&lt;style jsx&gt;{`
  h1 {
    font-size: 3rem;
  }
`}&lt;/style&gt;
</code></pre>
<p>你把它写在 JSX 里面，像这样:</p>
<pre><code class="language-js">const Index = () =&gt; (
  &lt;div&gt;
  &lt;h1&gt;Home page&lt;/h1&gt;

  &lt;style jsx&gt;{`
    h1 {
      font-size: 3rem;
    }
  `}&lt;/style&gt;
  &lt;/div&gt;
)

export default Index
</code></pre>
<p>在块内，我们可以使用插值来动态地改变数值。例如，这里我们假设一个 <code>size</code> prop 由父组件传递，我们在 <code>styled-jsx</code> 块中使用它：</p>
<pre><code class="language-js">const Index = props =&gt; (
  &lt;div&gt;
  &lt;h1&gt;Home page&lt;/h1&gt;

  &lt;style jsx&gt;{`
    h1 {
      font-size: ${props.size}rem;
    }
  `}&lt;/style&gt;
  &lt;/div&gt;
)
</code></pre>
<p>如果你想在全局范围内应用一些 CSS，而不是局限于某个组件，你可以在<code>style</code>标签上添加<code>global</code>关键字：</p>
<pre><code class="language-jsx">&lt;style jsx global&gt;{`
body {
  margin: 0;
}
`}&lt;/style&gt;
</code></pre>
<p>如果你想在 Next.js 组件中导入一个外部 CSS 文件，你必须先安装<code>@zeit/next-css</code>：</p>
<pre><code class="language-bash">npm install @zeit/next-css
</code></pre>
<p>然后在项目的根部创建一个配置文件，名为<code>next.config.js</code>，内容如下:</p>
<pre><code class="language-js">const withCSS = require('@zeit/next-css')
module.exports = withCSS()
</code></pre>
<p>重新启动 Next 应用程序后，你现在可以像通常使用 JavaScript 库或组件那样导入 CSS:</p>
<pre><code class="language-js">import '../style.css'
</code></pre>
<p>你也可以直接导入一个 SASS 文件，用<a href="https://github.com/zeit/next-plugins/tree/master/packages/next-sass"><code>@zeit/next-sass</code></a>库。</p>
<h2 id="populating-the-head-tag-with-custom-tags">用自定义标签填充head标签</h2>
<p>从任何 Next.js 页面组件中，你都可以向页面标题添加信息。</p>
<p>这在以下情况下是很方便的:</p>
<ul>
<li>你想定制页面的标题</li>
<li>你想改变一个元(meta)标签</li>
</ul>
<p>你怎么能这样做呢？</p>
<p>在每个组件内，你可以从 <code>next/head</code> 导入 <code>Head</code> 组件，并将其包含在你的组件 JSX 输出中：</p>
<pre><code class="language-js">import Head from 'next/head'

const House = props =&gt; (
  &lt;div&gt;
    &lt;Head&gt;
      &lt;title&gt;The page title&lt;/title&gt;
    &lt;/Head&gt;
    {/* the rest of the JSX */}
  &lt;/div&gt;
)

export default House
</code></pre>
<p>你可以在页面的<code>&lt;head&gt;</code>部分添加任何你想出现的 HTML 标签。</p>
<p>当安装该组件时，Next.js 将确保<code>Head</code>内的标签被添加到页面的标题中。当卸载组件时，Next.js 将负责删除这些标签。</p>
<h2 id="adding-a-wrapper-component">添加一个封装组件</h2>
<p>你网站上的所有页面看起来都差不多。有一个 chrome 窗口，一个共同的基础层，你只是想改变里面的内容。</p>
<p>有一个导航栏，一个侧边栏，然后是实际内容。</p>
<p>你如何在 Next.js 中创建这样的系统？</p>
<p>有 2 种方法。一种是使用 <a href="https://flaviocopes.com/react-higher-order-components/">高阶组件</a>，通过创建一个<code>components/Layout.js</code>组件：</p>
<pre><code class="language-js">export default Page =&gt; {
  return () =&gt; (
    &lt;div&gt;
      &lt;nav&gt;
        &lt;ul&gt;....&lt;/ul&gt;
      &lt;/hav&gt;
      &lt;main&gt;
        &lt;Page /&gt;
      &lt;/main&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<p>在那里，我们可以为标题和/或侧边栏导入单独的组件，我们还可以添加我们需要的所有 CSS。</p>
<p>你可以在每一个页面中像这样使用它:</p>
<pre><code class="language-js">import withLayout from '../components/Layout.js'

const Page = () =&gt; &lt;p&gt;Here's a page!&lt;/p&gt;

export default withLayout(Page)
</code></pre>
<p>但我发现这只适用于简单的情况，你不需要在一个页面上调用<code>getInitialProps()</code>。</p>
<p>为什么？</p>
<p>因为<code>getInitialProps()</code>只在页面组件上被调用。但是如果我们从一个页面导出高阶组件 withLayout()，<code>Page.getInitialProps()</code>就不会被调用。但 <code>withLayout.getInitialProps()</code>会。</p>
<p>为了避免不必要地使我们的代码库复杂化，另一种方法是使用 props：</p>
<pre><code class="language-js">export default props =&gt; (
  &lt;div&gt;
    &lt;nav&gt;
      &lt;ul&gt;....&lt;/ul&gt;
    &lt;/hav&gt;
    &lt;main&gt;
      {props.content}
    &lt;/main&gt;
  &lt;/div&gt;
)
</code></pre>
<p>而在我们的页面中，我们现在这样使用它:</p>
<pre><code class="language-js">import Layout from '../components/Layout.js'

const Page = () =&gt; (
  &lt;Layout content={(
    &lt;p&gt;Here's a page!&lt;/p&gt;
  )} /&gt;
)
</code></pre>
<p>这种方法让我们在页面组件中使用<code>getInitialProps()</code>，唯一的缺点是必须在<code>content</code>prop 中写入组件的 JSX:</p>
<pre><code class="language-js">import Layout from '../components/Layout.js'

const Page = () =&gt; (
  &lt;Layout content={(
    &lt;p&gt;Here's a page!&lt;/p&gt;
  )} /&gt;
)

Page.getInitialProps = ({ query }) =&gt; {
  //...
}
</code></pre>
<h2 id="apiroutes">API Routes</h2>
<p>除了创建 <strong>页面路由（page routes）</strong>，也就是将页面作为网页提供给浏览器之外，Next.js 还可以创建 <strong>API 路由（API routes）</strong>。</p>
<p>这是一个非常有趣的功能，因为它意味着 Next.js 可以用来创建一个由 Next.js 本身存储和检索的数据的前端，通过 fetch 请求传输 JSON。</p>
<p>API 路由位于<code>/pages/api/</code>文件夹下，并被映射到<code>/api</code>端点（endpoint）。</p>
<p>这个功能在创建应用程序时非常有用。</p>
<p>在这些路由中，我们编写 Node.js 代码（而不是 React 代码）。这是一个范式的转变，你从前端移到后端，但非常无缝。</p>
<p>假设你有一个<code>/pages/api/comments.js</code>文件，其目标是以 JSON 格式返回一篇博客文章的评论。</p>
<p>假设你有一个存储在<code>comments.json</code>文件中的评论列表：</p>
<pre><code class="language-json">[
  {
    "comment": "First"
  },
  {
    "comment": "Nice post"
  }
]
</code></pre>
<p>下面是一个示例代码，它向客户返回评论的列表:</p>
<pre><code class="language-js">import comments from './comments.json'

export default (req, res) =&gt; {
  res.status(200).json(comments)
}
</code></pre>
<p>它将监听<code>/api/comments</code> URL 的 GET 请求，你可以尝试用浏览器调用它：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-07-at-11.14.42.png" alt="Screen-Shot-2019-11-07-at-11.14.42" width="600" height="400" loading="lazy"></p>
<p>API 路由也可以像页面一样使用 <strong>动态路由</strong>，使用<code>[]</code>语法来创建动态 API 路由，如<code>/pages/api/comments/[id].js</code>，它将检索特定于帖子 id 的评论。</p>
<p>在<code>[id].js</code>中，你可以通过在<code>req.query</code>对象中查找来检索<code>id</code>值：</p>
<pre><code class="language-js">import comments from '../comments.json'

export default (req, res) =&gt; {
  res.status(200).json({ post: req.query.id, comments })
}
</code></pre>
<p>在这里你可以看到上述代码的运行情况：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-07-at-11.59.53.png" alt="Screen-Shot-2019-11-07-at-11.59.53" width="600" height="400" loading="lazy"></p>
<p>在动态页面中，你需要从<code>next/router</code>中导入<code>useRouter</code>，然后使用<code>const router = useRouter()</code>获得路由对象，然后我们就可以使用<code>router.query.id</code>获得<code>id</code>值。</p>
<p>在服务器端，这一切都很容易，因为查询是附在请求对象上的。</p>
<p>如果你做一个 POST 请求，所有的工作方式都是一样的——都是通过那个默认的出口。</p>
<p>要把 POST 从 GET 和其他 HTTP 方法（PUT、DELETE）中分离出来，可以查询<code>req.method</code>值：</p>
<pre><code class="language-js">export default (req, res) =&gt; {
  switch (req.method) {
    case 'GET':
      //...
      break
    case 'POST':
      //...
      break
    default:
      res.status(405).end() //Method Not Allowed
      break
  }
}
</code></pre>
<p>除了我们已经看到的<code>req.query</code>和<code>req.method</code>之外，我们还可以通过引用<code>req.cookies</code>，即<code>req.body</code>中的请求体来访问 cookies。</p>
<p>从更底层角度来讲，这一切都由<a href="https://github.com/zeit/micro">Micro</a>提供动力，这是一个支持异步 HTTP 微服务的库，由 Next.js 团队开发维护。</p>
<p>你可以在我们的 API 路由中使用任何 Micro 中间件来增加更多的功能。</p>
<h2 id="run-code-only-on-the-server-side-or-client-side">在服务器端，或在客户端运行代码</h2>
<p>在你的页面组件中，你可以通过检查<code>window</code>属性，判断在服务器端或在客户端执行代码。</p>
<p>这个属性只存在于浏览器内部，所以你可以检查</p>
<pre><code class="language-js">if (typeof window === 'undefined') {

}
</code></pre>
<p>并在该块中添加服务器端的代码。</p>
<p>同样地，你可以通过检查来是否可以执行客户端代码</p>
<pre><code class="language-js">if (typeof window !== 'undefined') {

}
</code></pre>
<p>JS 提示。我们在这里使用<code>typeof</code>操作符，因为我们无法通过其他方式检测一个未定义的值。我们不能做<code>if (window == undefined)</code>，因为我们会得到一个 "window is not defined" 的运行时错误。</p>
<p>Next.js，作为构建时的优化，也从 bundles 中删除了使用这些检查的代码。客户端 bundle  将不包括包裹在<code>if (typeof window === 'undefined') {}</code>块中的内容。</p>
<h2 id="deploying-the-production-version">部署生产版本</h2>
<p>在教程中，部署应用程序总是被放在最后。</p>
<p>在这里，我想提前介绍一下，因为部署 Next.js 应用非常容易，我们现在就可以深入研究，然后再继续研究其他更复杂的话题。</p>
<p>记得在 "如何安装 Next.js "一章中，我告诉你要在<code>package.json</code>的<code>script</code>部分添加这 3 行：</p>
<pre><code class="language-json">"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}
</code></pre>
<p>到目前为止，我们使用<code>npm run dev</code>来调用安装在本地的<code>node_modules/next/dist/bin/next</code>中的<code>next</code>命令。这启动了开发服务器，它为我们提供了<strong>source maps</strong>和<strong>hot code reloading</strong>这两个在调试时非常有用的功能。</p>
<p>同样的命令可以通过运行 <code>npm run build</code>来创建网站，并加上 <code>build</code> 标志。然后，同样的命令可以用来启动生产应用程序，通过运行 <code>npm run start</code> 来传递 <code>start</code> 标志。</p>
<p>这两个命令是我们必须调用的，以成功地在本地部署我们网站的生产版本。生产版本是高度优化的，没有 <code>source maps</code>和其他诸如 <code>hot code reloading</code> 的东西，这对我们的终端用户没有好处。</p>
<p>所以，让我们创建一个生产版的应用程序。用以下方法创建它：</p>
<pre><code class="language-bash">npm run build
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-06-at-13.46.31.png" alt="Screen-Shot-2019-11-06-at-13.46.31" width="600" height="400" loading="lazy"></p>
<p>命令的输出告诉我们，一些路由（<code>/</code>和<code>/blog</code>现在被预设为静态 HTML，而<code>/blog/[id]</code>将由 Node.js 后端提供。</p>
<p>然后你可以运行<code>npm run start</code>来启动本地的生产服务器。</p>
<pre><code class="language-bash">npm run start
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-06-at-13.47.01.png" alt="Screen-Shot-2019-11-06-at-13.47.01" width="600" height="400" loading="lazy"></p>
<p>访问<a href="http://localhost:3000">http://localhost:3000</a>将向我们展示应用程序的生产版本，在本地。</p>
<h2 id="deploying-on-now">部署在 Now</h2>
<p>在上一章中，我们在本地部署了 Next.js 应用程序。</p>
<p>我们如何将其部署到真正的网络服务器上，以便其他人能够访问它呢？</p>
<p>部署 Next 应用程序最简单的方法之一是通过<a href="https://zeit.co">Zeit</a>创建的<strong>Now</strong>平台，该公司正是创建开源项目 Next.js 的公司。你可以使用 Now 来部署 Node.js 应用程序、静态网站等。</p>
<p>现在使应用程序的部署和分发步骤变得非常、非常简单和快速，除了 Node.js 应用程序外，他们还支持部署 Go、PHP、Python 和其他语言。</p>
<p>你可以把它看作是 "云"，因为你并不真正知道你的应用程序将被部署在哪里，但你知道你将有一个可以到达的 URL。</p>
<p>现在开始使用是免费的，有慷慨的免费计划，目前包括 100GB 的主机，每天 1000 次<a href="https://www.freecodecamp.org/news/serverless/">无服务器</a>函数调用，每月 1000 次构建，每月 100GB 的带宽，以及一个<a href="https://www.freecodecamp.org/news/cdn/">CDN</a>位置。如果你需要更多，<a href="https://zeit.co/pricing">定价页</a>有助于了解成本。</p>
<p>开始使用 Now 的最好方法是使用官方 Now CLI：</p>
<pre><code class="language-bash">npm install -g now
</code></pre>
<p>一旦有了这个命令，运行</p>
<pre><code class="language-bash">now login
</code></pre>
<p>而应用程序将要求你提供你的电子邮件。</p>
<p>如果你还没有注册，请在继续之前在<a href="https://zeit.co/signup">https://zeit.co/signup</a>上创建一个账户，然后将你的电子邮件添加到 CLI 客户端。</p>
<p>一旦完成这些，从 Next.js 项目根目录下运行</p>
<pre><code class="language-bash">now
</code></pre>
<p>而该应用程序将被立即部署到 Now 云中，你将得到独特的应用程序 URL：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-06-at-14.21.09.png" alt="Screen-Shot-2019-11-06-at-14.21.09" width="600" height="400" loading="lazy"></p>
<p>一旦你运行<code>now</code>程序，应用程序就会被部署到<code>now.sh</code>域下的一个随机 URL。</p>
<p>我们可以在图片中给出的输出中看到 3 个不同的 URL</p>
<ul>
<li><a href="https://firstproject-2pv7khwwr.now.sh">https://firstproject-2pv7khwwr.now.sh</a></li>
<li><a href="https://firstproject-sepia-ten.now.sh">https://firstproject-sepia-ten.now.sh</a></li>
<li><a href="https://firstproject.flaviocopes.now.sh">https://firstproject.flaviocopes.now.sh</a></li>
</ul>
<p>为什么有这么多？</p>
<p>首先是识别部署的 URL。每次我们部署应用程序时，这个 URL 都会改变。</p>
<p>你可以通过改变项目代码中的某些内容，并再次运行 <code>now</code> 来立即测试：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-06-at-15.08.11.png" alt="Screen-Shot-2019-11-06-at-15.08.11" width="600" height="400" loading="lazy"></p>
<p>其他 2 个 URL 将不会改变。第一个是随机的，第二个是你的项目名称（默认为当前项目文件夹，你的账户名，然后是<code>now.sh</code>。</p>
<p>如果你访问这个 URL，你会看到应用被部署到生产中。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-06-at-14.21.43.png" alt="Screen-Shot-2019-11-06-at-14.21.43" width="600" height="400" loading="lazy"></p>
<p>你可以将 Now 配置为将网站提供给你自己的自定义域或子域，但我现在不会深入研究这个。</p>
<p><code>now.sh</code>子域对于我们的测试目的已经足够了。</p>
<h2 id="analyzing-the-app-bundles">分析应用程序bundles的情况</h2>
<p>下一步为我们提供了一种分析生成的 bundles 的方法。</p>
<p>打开应用程序的 package.json 文件，在 scripts 部分添加这 3 个新命令：</p>
<pre><code class="language-json">"analyze": "cross-env ANALYZE=true next build",
"analyze:server": "cross-env BUNDLE_ANALYZE=server next build",
"analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build"
</code></pre>
<p>Like this:</p>
<pre><code class="language-json">{
  "name": "firstproject",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start",
    "analyze": "cross-env ANALYZE=true next build",
    "analyze:server": "cross-env BUNDLE_ANALYZE=server next build",
    "analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "next": "^9.1.2",
    "react": "^16.11.0",
    "react-dom": "^16.11.0"
  }
}
</code></pre>
<p>然后安装这两个软件包:</p>
<pre><code class="language-bash">npm install --dev cross-env @next/bundle-analyzer
</code></pre>
<p>在项目根部创建一个<code>next.config.js</code>文件，其内容如下:</p>
<pre><code class="language-js">const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true'
})

module.exports = withBundleAnalyzer({})
</code></pre>
<p>现在运行命令</p>
<pre><code class="language-bash">npm run analyze
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-06-at-16.12.40.png" alt="Screen-Shot-2019-11-06-at-16.12.40" width="600" height="400" loading="lazy"></p>
<p>这应该在浏览器中打开两个页面。一个用于客户端捆绑包，另一个用于服务器端捆绑包：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-06-at-16.11.14.png" alt="Screen-Shot-2019-11-06-at-16.11.14" width="600" height="400" loading="lazy"></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-06-at-16.11.23.png" alt="Screen-Shot-2019-11-06-at-16.11.23" width="600" height="400" loading="lazy"></p>
<p>这非常有用。您可以检查捆绑包中占用最多空间的内容，还可以使用侧边栏排除捆绑包，以更轻松地可视化较小的捆绑包：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-06-at-16.14.12.png" alt="Screen-Shot-2019-11-06-at-16.14.12" width="600" height="400" loading="lazy"></p>
<h2 id="lazy-loading-modules">模块懒加载</h2>
<p>能够直观地分析一个 bundle 是非常好的，因为我们可以非常容易地优化我们的应用程序。</p>
<p>假设我们需要在我们的博客文章中加载 Moment 库，运行：</p>
<pre><code class="language-bash">npm install moment
</code></pre>
<p>将其导入项目中。</p>
<p>现在让我们模拟一下，我们在两个不同的路由上需要它。<code>/blog</code>和<code>/blog/[id]</code>。</p>
<p>我们在<code>pages/blog/[id].js</code>中导入它：</p>
<pre><code class="language-jsx">import moment from 'moment'

...

const Post = props =&gt; {
  return (
    &lt;div&gt;
      &lt;h1&gt;{props.post.title}&lt;/h1&gt;
      &lt;p&gt;Published on {moment().format('dddd D MMMM YYYY')}&lt;/p&gt;
      &lt;p&gt;{props.post.content}&lt;/p&gt;
    &lt;/div&gt;
  )
}
</code></pre>
<p>我只是加入今天的日期，作为一个例子。</p>
<p>这将包括 Moment.js 在博文页面的 bundle，你可以通过运行<code>npm run analyze</code>看到：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-06-at-17.56.14.png" alt="Screen-Shot-2019-11-06-at-17.56.14" width="600" height="400" loading="lazy"></p>
<p>看，我们现在在<code>/blog/[id]</code>里有一个红色的条目，就是我们添加 Moment.js 的那个路由！</p>
<p>它从 1kB 变成了 350kB，相当大的变化。而这是因为 Moment.js 库本身就是 349kB。</p>
<p>客户端 bundles 的可视化显示，更大的是页面 bundles，而之前是很少的。而它 99%的代码都是 Moment.js。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-06-at-17.55.50.png" alt="Screen-Shot-2019-11-06-at-17.55.50" width="600" height="400" loading="lazy"></p>
<p>每当我们加载一篇博客文章时，我们就会把所有这些代码转移到客户端。这并不理想。</p>
<p>一个解决方法是寻找一个体积更小的库，因为 Moment.js 并不以轻量级著称（尤其是开箱后包含了所有的地域性），但为了这个例子，我们假设我们必须使用它。</p>
<p>我们可以做的是将所有的 Moment 代码分离在一个<strong>独立的 bundle 里</strong>。</p>
<p>怎么做？我们在<code>getInitialProps</code>里面进行异步导入，而不是在组件层面上导入 Moment，我们计算出要发送给组件的值。</p>
<p>记住，我们不能在<code>getInitialProps()</code>返回的对象中返回复杂的对象，所以我们在里面计算出日期：</p>
<pre><code class="language-js">import posts from '../../posts.json'

const Post = props =&gt; {
  return (
    &lt;div&gt;
      &lt;h1&gt;{props.post.title}&lt;/h1&gt;
      &lt;p&gt;Published on {props.date}&lt;/p&gt;
      &lt;p&gt;{props.post.content}&lt;/p&gt;
    &lt;/div&gt;
  )
}

Post.getInitialProps = async ({ query }) =&gt; {
  const moment = (await import('moment')).default()
  return {
    date: moment.format('dddd D MMMM YYYY'),
    post: posts[query.id]
  }
}

export default Post
</code></pre>
<p>看到在 “await import” 之后对 “.default()” 的特殊调用吗？它需要在动态导入中引用默认导出（见<a href="https://v8.dev/features/dynamic-import">https://v8.dev/features/dynamic-import</a>）。</p>
<p>现在，如果我们再次运行<code>npm run analyze</code>，我们可以看到这个：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2019/11/Screen-Shot-2019-11-06-at-18.00.22.png" alt="Screen-Shot-2019-11-06-at-18.00.22" width="600" height="400" loading="lazy"></p>
<p>我们的<code>/blog/[id]</code>捆绑文件又非常小了，因为 Moment 已经被移到它自己的捆绑文件中，由浏览器单独加载。</p>
<h2 id="where-to-go-from-here">今后的发展方向</h2>
<p>关于 Next.js，还有很多东西需要了解。我没有谈及用登录管理用户会话、无服务器、管理数据库等等。</p>
<p>本手册的目的不是要教你所有的东西，而是要逐步向你介绍 Next.js 的所有功能。</p>
<p>我建议的下一步是仔细阅读<a href="https://nextjs.org/docs">Next.js 官方文档</a>，以了解我没有谈到的所有特性和功能，并看看<a href="https://github.com/zeit/next-plugins">Next.js 插件</a>引入的所有额外功能，其中有些功能相当惊人。</p>
<p>你可以在 Twitter <a href="https://twitter.com/flaviocopes">@flaviocopes</a>上找到我。</p>
<p>还可以查看我的网站，<a href="https://flaviocopes.com/">flaviocopes.com</a>。</p>
<p><a href="https://flaviocopes.com/page/nextjs-handbook/">注意：你可以下载本教程的 PDF/ePub/Mobi 版本，以便你可以离线阅读。</a></p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Next.js 图像处理教程——如何上传、裁剪和调整浏览器中的图像大小 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：Next.js Image Tutorial – How to Upload, Crop, and Resize Images in the Browser in Next [https://www.freecodecamp.org/news/how-to-upload-crop-resize-images-in-the-browser-in-nextjs/] ，作者：Idris Olubisi [https://www.freecodecamp.org/news/author/idris/] 两个最基本的图像编辑功能是调整大小和裁剪。但是你应该谨慎执行这些操作，因为它们可能会降低图像质量。 裁剪总是会删除原始图像的一部分，从而导致一些像素的丢失。 这篇文章将教你如何在浏览器中上传、裁剪和调整图像大小。 我在 Codesandbox [https://codesandbox.io/s/serverless-leaf-vc9rls?file=/pages/index.js]  中构建了这个项目。要快速开始的话，请下载 Codesandbox [https://codesan ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-upload-crop-resize-images-in-the-browser-in-nextjs/</link>
                <guid isPermaLink="false">629d76c7dbb8cc083a127e4c</guid>
                
                    <category>
                        <![CDATA[ NextJS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jing Wu ]]>
                </dc:creator>
                <pubDate>Sat, 04 Jun 2022 03:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/06/pexels-cottonbro-5083407.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/how-to-upload-crop-resize-images-in-the-browser-in-nextjs/">Next.js Image Tutorial – How to Upload, Crop, and Resize Images in the Browser in Next</a>，作者：<a href="https://www.freecodecamp.org/news/author/idris/">Idris Olubisi</a></p><!--kg-card-begin: markdown--><p>两个最基本的图像编辑功能是调整大小和裁剪。但是你应该谨慎执行这些操作，因为它们可能会降低图像质量。</p>
<p>裁剪总是会删除原始图像的一部分，从而导致一些像素的丢失。</p>
<p>这篇文章将教你如何在浏览器中上传、裁剪和调整图像大小。</p>
<p>我在 <a href="https://codesandbox.io/s/serverless-leaf-vc9rls?file=/pages/index.js">Codesandbox</a> 中构建了这个项目。要快速开始的话，请下载 <a href="https://codesandbox.io/s/serverless-leaf-vc9rls?file=/pages/index.js">Codesandbox</a> 并运行项目。</p>
<h2 id="">先决条件</h2>
<p>要跟上本教程，你应该有一些 JavaScript 和 React.js 经验。对 Next.js 的经验不是必须的，但有这个经验也不错。</p>
<p>你还需要一个 <a href="https://cloudinary.com/users/register/free">Cloudinary account</a> 帐户来存储媒体文件。</p>
<p><a href="https://cloudinary.com/documentation/image_video_and_file_upload#upload_options_overview">Cloudinary</a> 提供了一个安全且完整的 API，用于快速和有效地从服务器、浏览器或移动应用程序上传媒体文件。</p>
<p>最后你需要 <a href="https://nextjs.org/">Next.js</a> 。它是一个基于 React 的开源前端开发 Web 框架，允许服务端渲染和生成静态网站和应用。</p>
<h2 id="">项目设置和安装</h2>
<p>使用 <code>npx create-next-app</code> 命令在你选择的目录中创建一个新项目。</p>
<p>你可以使用以下命令执行此操作：</p>
<pre><code>npx create-next-app &lt;project name&gt;
</code></pre>
<p>要安装这些依赖项，请使用这些命令：</p>
<pre><code>cd &lt;project name&gt; 
npm install cloudinary-react
</code></pre>
<p>在该应用程序创建并安装完依赖项后，你会看到一条信息，其中有关于导航到你的网站并在本地运行的说明。</p>
<p>你可以使用以下命令执行此操作：</p>
<pre><code>npm run dev
</code></pre>
<p>Next.js 将启动一个默认可访问的热重载开发环境 <a href="http://localhost:3000">http://localhost:3000</a> 。</p>
<h2 id="">如何构建用户界面</h2>
<p>在我们的项目中，我们希望能够在主页的用户界面上上传、裁剪和调整图像大小。我们将通过更新 <code>pages/index.js</code> 文件为组件来做到这一点：</p>
<pre><code>import React, { useState } from "react";
import Head from "next/head";

const IndexPage = () =&gt; {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

const IndexPage = () =&gt; {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        &lt;div className="splitdiv" id="rightdiv"&gt;
          //...
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/&gt;
  );
};
export default IndexPage;
</code></pre>
<p>在上面的代码片段中：</p>
<ul>
<li>添加了裁剪类型以及宽度和高度选项</li>
<li>添加了一个 onChange 属性来分别跟踪高度和宽度输入框的变化</li>
</ul>
<p>我们的应用程序的最终输出应该类似于下面的内容：</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1650112568692/2htjubfOv.png" alt="How to Upload, Crop, &amp; Resize Image in the Browser in Next.js" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1650112581661/JnEP--CHC.png" alt="How to Upload, Crop, &amp; Resize Image in the Browser in Next.js" width="600" height="400" loading="lazy"></p>
<p>如果你想查看完整代码，这里是项目的 GitHub 仓库：<a href="https://github.com/Olanetsoft/how-to-upload-crop-and-resize-images-in-the-browser-in-next.js">https://github.com/Olanetsoft/how-to-upload-crop-and-resize-images-in-the-browser-in-next.js</a></p>
<h2 id="">结论</h2>
<p>这篇文章展示了如何在 Next.js 的浏览器中上传、裁剪和调整图像大小。</p>
<h2 id="">资源</h2>
<p>你可能会发现这些资源很有帮助。</p>
<ul>
<li><a href="https://cloudinary.com/documentation/transformation_reference">Cloudinary transformation URL reference</a></li>
<li><a href="https://cloudinary.com/documentation/image_transformations">Cloudinary Image Transformation</a></li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 使用 Next.js 和 Supabase 进行全栈开发 ]]>
                </title>
                <description>
                    <![CDATA[ 原文：Full Stack Development with Next.js and Supabase – The Complete Guide [https://www.freecodecamp.org/news/the-complete-guide-to-full-stack-development-with-supabas/] ，作者：Nader Dabit [https://www.freecodecamp.org/news/author/nader/] Supabase [https://twitter.com/supabase_io]  是一个Firebase的开源替代品，让你在不到两分钟的时间内创建一个实时后台。 在过去的几个月里，Supabase 持续被我周围的开发人员宣传和采用。和我交流过的很多人都喜欢它利用 SQL 风格的数据库这一事实，他们也喜欢它是开源的。 当你创建一个项目时，Supabase会自动给你一个Postgres SQL数据库、用户认证和API。从那里你可以很容易地实现额外的功能，如实时订阅和文件存储。 在本指南中，你将学习如何构建一个全栈应用 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/the-complete-guide-to-full-stack-development-with-supabas/</link>
                <guid isPermaLink="false">623541df14f43b06999d26c8</guid>
                
                    <category>
                        <![CDATA[ NextJS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Fri, 18 Mar 2022 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/03/supanext.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/the-complete-guide-to-full-stack-development-with-supabas/">Full Stack Development with Next.js and Supabase – The Complete Guide</a>，作者：<a href="https://www.freecodecamp.org/news/author/nader/">Nader Dabit</a></p><!--kg-card-begin: markdown--><p><a href="https://twitter.com/supabase_io">Supabase</a> 是一个Firebase的开源替代品，让你在不到两分钟的时间内创建一个实时后台。</p>
<p>在过去的几个月里，Supabase 持续被我周围的开发人员宣传和采用。和我交流过的很多人都喜欢它利用 SQL 风格的数据库这一事实，他们也喜欢它是开源的。</p>
<p>当你创建一个项目时，Supabase会自动给你一个Postgres SQL数据库、用户认证和API。从那里你可以很容易地实现额外的功能，如实时订阅和文件存储。</p>
<p>在本指南中，你将学习如何构建一个全栈应用程序，实现大多数应用程序需要的核心功能--如路由、数据库、API、认证、授权、实时数据和细粒度访问控制。我们将使用一个现代堆栈，包括<a href="https://reactjs.org/docs/getting-started.html">React</a>, <a href="https://nextjs.org/">Next.js</a>, 和 <a href="https://tailwindcss.com/">TailwindCSS</a>.</p>
<p>我试图把我自己在使用Supabase的过程中所学到的东西提炼成一个尽可能简短的指南，这样你也可以开始用这个框架构建全栈应用。</p>
<p>我们将建立的应用程序是一个多用户博客应用程序，其中包含了你在许多现代应用程序中看到的所有类型的功能。这将使我们超越基本的CRUD，实现文件存储以及授权和细粒度的访问控制等功能。</p>
<blockquote>
<p>你可以<a href="https://github.com/dabit3/supabase-next.js">在这里</a>找到我们将要建立的应用程序的代码。</p>
</blockquote>
<p>通过学习如何将所有这些功能结合在一起，你应该能够利用你在这里学到的东西，建立你自己的想法。了解基本结构本身，你就可以在将来带着这些知识，以你认为合适的方式使用它。</p>
<h2 id="supabase">Supabase概述</h2>
<h3 id="">如何构建全栈式应用程序</h3>
<p>我对全栈式无服务器框架非常着迷，因为它们为希望构建完整应用的开发者提供了大量的支持和敏捷性。</p>
<p>Supabase将强大的后端服务与易于使用的客户端库和SDK结合起来，提供了一个端到端的解决方案。</p>
<p>这种组合使你不仅可以在后端建立起必要的个别功能和服务，而且可以通过利用同一团队维护的客户端库，在前端轻松地将它们整合在一起。</p>
<p>由于Supabase是开源的，你可以选择自我托管或将你的后端部署为托管服务。而且，正如你所看到的，我们很容易就能使用它的免费服务，不需要信用卡。</p>
<h2 id="supabase">为什么使用Supabase</h2>
<p>我曾在AWS领导前端Web和移动开发者倡导团队，并写了一本关于构建这些类型的应用程序的书。所以我在这个领域积累了相当多的经验。</p>
<p>我认为Supabase带来了一些非常强大的功能，当我开始使用它进行构建时，这些功能立即让我感到震惊。</p>
<h3 id="">数据访问方式</h3>
<p>我过去使用的一些工具和框架的最大限制之一是缺乏查询功能。我非常喜欢Supabase，因为它是建立在Postgres之上的，它能够实现一套极其丰富的、开箱即用的高性能查询功能，而不需要编写任何额外的后端代码。</p>
<p>客户端SDK提供了易于使用的<a href="https://supabase.io/docs/reference/javascript/using-filters">filters</a> 和<a href="https://supabase.io/docs/reference/javascript/using-modifiers">modifiers</a>，以实现几乎无限的数据访问模式的组合。</p>
<p>因为数据库是SQL，关系型数据很容易配置和查询，而且客户端库把它作为第一等公民来考虑。</p>
<h3 id="">权限</h3>
<p>当你写好“hello world”时，许多类型的框架和服务很快就会被淘汰。这是因为大多数真实世界的需要远远超出了你经常看到的这些工具所提供的基本CRUD功能。</p>
<p>一些框架和管理服务的问题是，它们创建的抽象没有足够的扩展性，无法轻松修改配置或定制业务逻辑。这些限制往往使其难以考虑到在现实世界中构建应用程序时出现的许多一次性的用例。</p>
<p>除了实现广泛的数据访问模式外，Supabase还使配置授权和精细的访问控制变得容易。这是因为它是简单的Postgres，使你能够直接从内置的SQL编辑器中实现任何你想要的<a href="https://www.postgresql.org/docs/10/ddl-rowsecurity.html">row-level security policies(行级安全策略)</a> 你可以直接从内置的SQL编辑器中获取你想要的信息（我们将在这里讨论这个问题）。</p>
<h3 id="ui">UI 组件</h3>
<p>除了由构建其他Supabase工具的同一个团队维护的客户端库之外，他们还维护了一个<a href="https://ui.supabase.io/">UI 组件库</a> (beta)使你能够使用各种UI元素来启动和运行。</p>
<p>最强大的是<a href="https://ui.supabase.io/components/auth">Auth</a>，它与你的Supabase项目集成，以快速启动一个用户认证流程（我将在本教程中使用它）。</p>
<h3 id="">多种认证方式</h3>
<p>Supabase启用了以下所有类型的认证机制：</p>
<ol>
<li>用户名和密码</li>
<li>电子邮件链接</li>
<li>谷歌</li>
<li>Facebook</li>
<li>Apple</li>
<li>GitHub</li>
<li>Twitter</li>
<li>Azure</li>
<li>GitLab</li>
<li>Bitbucket</li>
</ol>
<h3 id="">开源</h3>
<p>它最大的优点之一是<a href="https://github.com/supabase">完全开源</a>（是的，后端也是）。这意味着，你可以选择无服务器托管的方式，也可以自己托管。</p>
<p>这意味着，如果你愿意，你可以<a href="https://supabase.io/docs/guides/self-hosting">用Docker运行Supabase，托管自己的应用程序</a> 在 AWS、GCP 或 Azure上。这将让你在使用Supabase时避免云服务商锁定问题。</p>
<h2 id="supabase">如何开始使用Supabase</h2>
<h3 id="">项目设置</h3>
<p>为了开始，让我们首先创建Next.js应用程序。</p>
<pre><code class="language-sh">npx create-next-app next-supabase
</code></pre>
<p>接下来，进入目录并使用NPM或Yarn安装我们的应用程序所需的依赖。</p>
<pre><code class="language-sh">npm install @supabase/supabase-js @supabase/ui react-simplemde-editor easymde react-markdown uuid
npm install tailwindcss@latest @tailwindcss/typography postcss@latest autoprefixer@latest
</code></pre>
<p>接下来，创建必要的Tailwind配置文件：</p>
<pre><code class="language-sh">npx tailwindcss init -p
</code></pre>
<p>现在更新<strong>tailwind.config.js</strong>，将Tailwind typography插件添加到插件数组中。我们将使用这个插件来为我们的博客设计标记。</p>
<pre><code>plugins: [
  require('@tailwindcss/typography')
]
</code></pre>
<p>最后，将<strong>styles/globals.css</strong>中的样式替换为以下内容。</p>
<pre><code>@tailwind base;
@tailwind components;
@tailwind utilities;
</code></pre>
<h3 id="supabase">Supabase项目初始化</h3>
<p>现在项目已经在本地创建，让我们来创建Supabase项目。</p>
<p>为此，请前往<a href="https://supabase.io/">Supabase.io</a>，点击<strong>Start Your Project</strong>。通过GitHub账号完成认证，然后在账户中提供给你的组织下创建一个新项目。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/myaccountsorg.jpg" alt="myaccountsorg" width="600" height="400" loading="lazy"></p>
<p>给项目一个<strong>Name</strong>和<strong>Password</strong>，然后点击<strong>Create new project</strong>。</p>
<p>你的项目将需要大约2分钟的时间来创建。</p>
<h3 id="supabase">如何在Supabase中创建一个数据库表</h3>
<p>当你创建了你的项目，让我们继续为我们的应用程序创建表，以及我们需要的所有权限。要做到这一点，请点击左边菜单中的<strong>SQL</strong>链接。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/Screen-Shot-2021-06-06-at-6.07.00-PM.png" alt="Screen-Shot-2021-06-06-at-6.07.00-PM" width="600" height="400" loading="lazy"></p>
<p>在这个视图中，点击<strong>Query-1</strong>下的 <strong>Open queries</strong>，粘贴以下SQL查询，并点击 <strong>RUN</strong>。</p>
<pre><code>CREATE TABLE posts (
  id bigint generated by default as identity primary key,
  user_id uuid references auth.users not null,
  user_email text,
  title text,
  content text,
  inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
);

alter table posts enable row level security;

create policy "Individuals can create posts." on posts for
    insert with check (auth.uid() = user_id);

create policy "Individuals can update their own posts." on posts for
    update using (auth.uid() = user_id);

create policy "Individuals can delete their own posts." on posts for
    delete using (auth.uid() = user_id);

create policy "Posts are public." on posts for
    select using (true);
</code></pre>
<p>这将创建我们将在应用程序中使用的<code>posts</code>表。它还启用了一些行级(row)权限:</p>
<ul>
<li>所有用户都可以查询帖子</li>
<li>只有已登录的用户可以创建帖子，他们的用户ID必须与传入参数的用户ID一致</li>
<li>只有帖子的主人可以更新或删除它</li>
</ul>
<p>现在，如果我们点击<strong>Table editor</strong>链接，我们应该看到我们的新表以适当的模式创建。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/Screen-Shot-2021-06-06-at-6.11.49-PM.png" alt="Screen-Shot-2021-06-06-at-6.11.49-PM" width="600" height="400" loading="lazy"></p>
<p>这这样！我们的后端已经准备好了，我们可以开始建立用户界面了。用户名+密码认证已经默认启用，所以我们现在需要做的就是在前端把一切都连接起来。</p>
<h3 id="nextjssupabase">Next.js Supabase 配置</h3>
<p>现在项目已经创建，我们需要一种方法让我们的Next.js应用程序知道我们刚刚为它创建的后端服务。</p>
<p>对我们来说，配置的最好方法是使用环境变量。Next.js允许通过在项目根部创建一个名为 <strong>.env.local</strong> 的文件来设置环境变量，并将它们存储在那里。</p>
<p>为了向浏览器暴露一个变量，你必须在变量前加上 <code>NEXT_PUBLIC_</code>。</p>
<p>在项目的根部创建一个名为**.env.local** 的文件，并添加以下配置。</p>
<pre><code>NEXT_PUBLIC_SUPABASE_URL=https://app-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-public-api-key
</code></pre>
<p>你可以在Supabase仪表板的设置中找到你的API URL和API密钥的值：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/appurls.jpg" alt="appurls" width="600" height="400" loading="lazy"></p>
<p>接下来，在项目的根部创建一个名为<strong>api.js</strong>的文件，并添加以下代码：</p>
<pre><code class="language-javascript">// api.js
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)
</code></pre>
<p>现在我们将能够导入<code>supabase</code>实例并在我们的应用程序中的任何地方使用它。</p>
<p>下面是使用Supabase JavaScript客户端与API互动的概况。</p>
<p><strong>查询数据:</strong></p>
<pre><code class="language-javascript">import { supabase } from '../path/to/api'

const { data, error } = await supabase
  .from('posts')
  .select()
</code></pre>
<p><strong>在数据库中创建新条目：</strong></p>
<pre><code class="language-javascript">const { data, error } = await supabase
  .from('posts')
  .insert([
    {
      title: "Hello World",
      content: "My first post",
      user_id: "some-user-id",
      user_email: "myemail@gmail.com"
    }
  ])
</code></pre>
<p>正如我前面提到的，<a href="https://supabase.io/docs/reference/javascript/using-filters">filters</a>和<a href="https://supabase.io/docs/reference/javascript/using-modifiers">modifiers</a>使得实现各种数据访问模式和你的数据的选择集变得非常容易。</p>
<p><strong>认证 – 注册:</strong></p>
<pre><code class="language-javascript">const { user, session, error } = await supabase.auth.signUp({
  email: 'example@email.com',
  password: 'example-password',
})
</code></pre>
<p><strong>认证 – 登录:</strong></p>
<pre><code class="language-javascript">const { user, session, error } = await supabase.auth.signIn({
  email: 'example@email.com',
  password: 'example-password',
})
</code></pre>
<p>在我们的案例中，我们不会手工编写主要的认证逻辑，我们将使<a href="https://ui.supabase.io/components/auth">Supabase UI</a>中的Auth组件。</p>
<h2 id="">如何构建应用程序</h2>
<p>现在，让我们开始构建用户界面吧！</p>
<p>为了开始，让我们首先更新应用程序，实现一些基本的导航和布局风格。</p>
<p>我们还将配置一些逻辑，以检查用户是否已经登录，并在他们登录后显示一个创建新帖子的链接。</p>
<p>最后，我们将为任何`auth'事件实现一个监听器。当一个新的“Auth”事件发生时，我们将检查以确保当前有一个登录的用户，以便显示或隐藏 <strong>Create Post</strong> 的链接。</p>
<p>打开文件 <strong>_app.js</strong>，添加以下代码：</p>
<pre><code class="language-javascript">// pages/_app.js
import Link from 'next/link'
import { useState, useEffect } from 'react'
import { supabase } from '../api'
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  const [user, setUser] = useState(null);
  useEffect(() =&gt; {
    const { data: authListener } = supabase.auth.onAuthStateChange(
      async () =&gt; checkUser()
    )
    checkUser()
    return () =&gt; {
      authListener?.unsubscribe()
    };
  }, [])
  async function checkUser() {
    const user = supabase.auth.user()
    setUser(user)
  }
  return (
  &lt;div&gt;
    &lt;nav className="p-6 border-b border-gray-300"&gt;
      &lt;Link href="/"&gt;
        &lt;span className="mr-6 cursor-pointer"&gt;Home&lt;/span&gt;
      &lt;/Link&gt;
      {
        user &amp;&amp; (
          &lt;Link href="/create-post"&gt;
            &lt;span className="mr-6 cursor-pointer"&gt;Create Post&lt;/span&gt;
          &lt;/Link&gt;
        )
      }
      &lt;Link href="/profile"&gt;
        &lt;span className="mr-6 cursor-pointer"&gt;Profile&lt;/span&gt;
      &lt;/Link&gt;
    &lt;/nav&gt;
    &lt;div className="py-8 px-16"&gt;
      &lt;Component {...pageProps} /&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  )
}

export default MyApp
</code></pre>
<h3 id="">如何制作一个用户资料页</h3>
<p>接下来，让我们创建<strong>profile</strong>页面。在页面目录中，创建一个名为<strong>profile.js</strong>的新文件，并添加以下代码:</p>
<pre><code class="language-javascript">// pages/profile.js
import { Auth, Typography, Button } from "@supabase/ui";
const { Text } = Typography
import { supabase } from '../api'

function Profile(props) {
    const { user } = Auth.useUser();
    if (user)
      return (
        &lt;&gt;
          &lt;Text&gt;Signed in: {user.email}&lt;/Text&gt;
          &lt;Button block onClick={() =&gt; props.supabaseClient.auth.signOut()}&gt;
            Sign out
          &lt;/Button&gt;
        &lt;/&gt;
      );
    return props.children 
}

export default function AuthProfile() {
    return (
        &lt;Auth.UserContextProvider supabaseClient={supabase}&gt;
          &lt;Profile supabaseClient={supabase}&gt;
            &lt;Auth supabaseClient={supabase} /&gt;
          &lt;/Profile&gt;
        &lt;/Auth.UserContextProvider&gt;
    )
}
</code></pre>
<p>简介页使用了<a href="https://ui.supabase.io/components/auth"><code>Auth</code></a>组件，源自<a href="https://ui.supabase.io/components/auth">Supabase UI library</a>。 这组件将渲染出一个有“sign up” 和 “sign in” 表单(form)为 <strong>未认证</strong> 用户, 一个带有“sign out”按钮的基本用户资料为<strong>已认证</strong>用户。 它还将启用一个有随机数(magic)的登录链接。</p>
<h3 id="">如何创建新的帖子</h3>
<p>接下来，让我们创建一个create-post页面。在pages目录下，创建一个名为create-post.js的页面，代码如下：</p>
<pre><code class="language-javascript">// pages/create-post.js
import { useState } from 'react'
import { v4 as uuid } from 'uuid'
import { useRouter } from 'next/router'
import dynamic from 'next/dynamic'
import "easymde/dist/easymde.min.css"
import { supabase } from '../api'

const SimpleMDE = dynamic(() =&gt; import('react-simplemde-editor'), { ssr: false })
const initialState = { title: '', content: '' }

function CreatePost() {
  const [post, setPost] = useState(initialState)
  const { title, content } = post
  const router = useRouter()
  function onChange(e) {
    setPost(() =&gt; ({ ...post, [e.target.name]: e.target.value }))
  }
  async function createNewPost() {
    if (!title || !content) return
    const user = supabase.auth.user()
    const id = uuid()
    post.id = id
    const { data } = await supabase
      .from('posts')
      .insert([
          { title, content, user_id: user.id, user_email: user.email }
      ])
      .single()
    router.push(`/posts/${data.id}`)
  }
  return (
    &lt;div&gt;
      &lt;h1 className="text-3xl font-semibold tracking-wide mt-6"&gt;Create new post&lt;/h1&gt;
      &lt;input
        onChange={onChange}
        name="title"
        placeholder="Title"
        value={post.title}
        className="border-b pb-2 text-lg my-4 focus:outline-none w-full font-light text-gray-500 placeholder-gray-500 y-2"
      /&gt; 
      &lt;SimpleMDE
        value={post.content}
        onChange={value =&gt; setPost({ ...post, content: value })}
      /&gt;
      &lt;button
        type="button"
        className="mb-4 bg-green-600 text-white font-semibold px-8 py-2 rounded-lg"
        onClick={createNewPost}
      &gt;Create Post&lt;/button&gt;
    &lt;/div&gt;
  )
}

export default CreatePost
</code></pre>
<p>这个组件渲染了一个Markdown编辑器，允许用户创建新的帖子。</p>
<p><code>createNewPost</code>函数将使用<code>supabase</code>实例，使用本地表单状态创建新的帖子。</p>
<p>你可能注意到，我们没有传入任何消息头(headers)。这是因为如果用户已经登录，Supabase客户端库会自动将访问令牌包含在已登录用户的消息头(headers)中。</p>
<h3 id="">如何查看单个帖子</h3>
<p>我们需要配置一个页面来查看单个帖子。</p>
<p>这个页面使用<code>getStaticPaths</code>来在构建时根据从API回来的帖子动态地创建页面。</p>
<p>我们还使用<code>fallback</code>标志来启用动态SSG页面生成的返回路径。</p>
<p>我们使用<code>getStaticProps</code>使帖子数据被获取，然后在构建时作为props传入页面。</p>
<p>在<strong>pages</strong>目录下创建一个名为<strong>posts</strong>的新文件夹， 并在该文件夹中创建一个名为<code>[id\].js</code> 的文件。在<code>pages/posts/[id\].js</code>，添加以下代码。</p>
<pre><code class="language-javascript">// pages/posts/[id].js
import { useRouter } from 'next/router'
import ReactMarkdown from 'react-markdown'
import { supabase } from '../../api'

export default function Post({ post }) {
  const router = useRouter()
  if (router.isFallback) {
    return &lt;div&gt;Loading...&lt;/div&gt;
  }
  return (
    &lt;div&gt;
      &lt;h1 className="text-5xl mt-4 font-semibold tracking-wide"&gt;{post.title}&lt;/h1&gt;
      &lt;p className="text-sm font-light my-4"&gt;by {post.user_email}&lt;/p&gt;
      &lt;div className="mt-8"&gt;
        &lt;ReactMarkdown className='prose' children={post.content} /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  )
}

export async function getStaticPaths() {
  const { data, error } = await supabase
    .from('posts')
    .select('id')
  const paths = data.map(post =&gt; ({ params: { id: JSON.stringify(post.id) }}))
  return {
    paths,
    fallback: true
  }
}

export async function getStaticProps ({ params }) {
  const { id } = params
  const { data } = await supabase
    .from('posts')
    .select()
    .filter('id', 'eq', id)
    .single()
  return {
    props: {
      post: data
    }
  }
}
</code></pre>
<h3 id="">如何查询和渲染帖子列表</h3>
<p>接下来，让我们更新<strong>index.js</strong>，以获取并渲染一个帖子的列表：</p>
<pre><code class="language-javascript">// pages/index.js
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { supabase } from '../api'

export default function Home() {
  const [posts, setPosts] = useState([])
  const [loading, setLoading] = useState(true)
  useEffect(() =&gt; {
    fetchPosts()
  }, [])
  async function fetchPosts() {
    const { data, error } = await supabase
      .from('posts')
      .select()
    setPosts(data)
    setLoading(false)
  }
  if (loading) return &lt;p className="text-2xl"&gt;Loading ...&lt;/p&gt;
  if (!posts.length) return &lt;p className="text-2xl"&gt;No posts.&lt;/p&gt;
  return (
    &lt;div&gt;
      &lt;h1 className="text-3xl font-semibold tracking-wide mt-6 mb-2"&gt;Posts&lt;/h1&gt;
      {
        posts.map(post =&gt; (
          &lt;Link key={post.id} href={`/posts/${post.id}`}&gt;
            &lt;div className="cursor-pointer border-b border-gray-300	mt-8 pb-4"&gt;
              &lt;h2 className="text-xl font-semibold"&gt;{post.title}&lt;/h2&gt;
              &lt;p className="text-gray-500 mt-2"&gt;Author: {post.user_email}&lt;/p&gt;
            &lt;/div&gt;
          &lt;/Link&gt;)
        )
      }
    &lt;/div&gt;
  )
}
</code></pre>
<h3 id="">让我们来测试一下</h3>
<p>现在我们的应用程序的所有部分都准备好了，所以让我们来试试。</p>
<p>要运行本地服务器，从你的终端运行<code>dev</code>命令。</p>
<pre><code class="language-sh">npm run dev
</code></pre>
<p>当应用程序加载时，你应该看到以下画面：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/SS1-1.png" alt="SS1-1" width="600" height="400" loading="lazy"></p>
<p>要注册，请点击<strong>Profile</strong>并创建一个新账户。你应该在注册后收到一个电子邮件链接，以确认你的账户。</p>
<p>你也可以通过使用随机数(magic)的链接创建一个新账户。</p>
<p>当你登录后，你应该能够创建新的帖子：</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/SS2.png" alt="SS2" width="600" height="400" loading="lazy"></p>
<p>回到主页，你应该能够看到你已经创建的帖子的列表，并能够点击帖子的链接来查看它。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/06/SS3.png" alt="SS3" width="600" height="400" loading="lazy"></p>
<h2 id="">如何编辑帖子</h2>
<p>现在我们已经启动并运行了应用程序，让我们来学习如何编辑帖子。为了开始学习，让我们创建一个新的视图(view)，它将只获取已登录用户创建的帖子。</p>
<p>为此，在项目的根目录创建一个名为<strong>my-posts.js</strong>的新文件，代码如下：</p>
<pre><code class="language-javascript">// pages/my-posts.js
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { supabase } from '../api'

export default function MyPosts() {
  const [posts, setPosts] = useState([])
  useEffect(() =&gt; {
    fetchPosts()
  }, [])

  async function fetchPosts() {
    const user = supabase.auth.user()
    const { data } = await supabase
      .from('posts')
      .select('*')
      .filter('user_id', 'eq', user.id)
    setPosts(data)
  }
  async function deletePost(id) {
    await supabase
      .from('posts')
      .delete()
      .match({ id })
    fetchPosts()
  }
  return (
    &lt;div&gt;
      &lt;h1 className="text-3xl font-semibold tracking-wide mt-6 mb-2"&gt;My Posts&lt;/h1&gt;
      {
        posts.map((post, index) =&gt; (
          &lt;div key={index} className="border-b border-gray-300	mt-8 pb-4"&gt;
            &lt;h2 className="text-xl font-semibold"&gt;{post.title}&lt;/h2&gt;
            &lt;p className="text-gray-500 mt-2 mb-2"&gt;Author: {post.user_email}&lt;/p&gt;
            &lt;Link href={`/edit-post/${post.id}`}&gt;&lt;a className="text-sm mr-4 text-blue-500"&gt;Edit Post&lt;/a&gt;&lt;/Link&gt;
            &lt;Link href={`/posts/${post.id}`}&gt;&lt;a className="text-sm mr-4 text-blue-500"&gt;View Post&lt;/a&gt;&lt;/Link&gt;
            &lt;button
              className="text-sm mr-4 text-red-500"
              onClick={() =&gt; deletePost(post.id)}
            &gt;Delete Post&lt;/button&gt;
          &lt;/div&gt;
        ))
      }
    &lt;/div&gt;
  )
}
</code></pre>
<p>在对 <code>post</code> 的查询中，我们使用用户 <code>id</code>，选择由登录用户创建的帖子。</p>
<p>接下来，在<strong>pages</strong>目录下创建一个名为<strong>edit-post</strong>的新文件夹。然后，在这个文件夹中创建一个名为 <code>[id\].js</code> 的文件。</p>
<p>在这个文件中，我们将从一个路由参数中获取访问帖子的`id'。当组件加载时，我们将使用来自路由的帖子ID来获取帖子数据，并使其可用于编辑。</p>
<p>在这个文件中，添加以下代码：</p>
<pre><code class="language-javascript">// pages/edit-post/[id].js
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import dynamic from 'next/dynamic'
import "easymde/dist/easymde.min.css"
import { supabase } from '../../api'

const SimpleMDE = dynamic(() =&gt; import('react-simplemde-editor'), { ssr: false })

function EditPost() {
  const [post, setPost] = useState(null)
  const router = useRouter()
  const { id } = router.query

  useEffect(() =&gt; {
    fetchPost()
    async function fetchPost() {
      if (!id) return
      const { data } = await supabase
        .from('posts')
        .select()
        .filter('id', 'eq', id)
        .single()
      setPost(data)
    }
  }, [id])
  if (!post) return null
  function onChange(e) {
    setPost(() =&gt; ({ ...post, [e.target.name]: e.target.value }))
  }
  const { title, content } = post
  async function updateCurrentPost() {
    if (!title || !content) return
    await supabase
      .from('posts')
      .update([
          { title, content }
      ])
      .match({ id })
    router.push('/my-posts')
  }
  return (
    &lt;div&gt;
      &lt;h1 className="text-3xl font-semibold tracking-wide mt-6 mb-2"&gt;Edit post&lt;/h1&gt;
      &lt;input
        onChange={onChange}
        name="title"
        placeholder="Title"
        value={post.title}
        className="border-b pb-2 text-lg my-4 focus:outline-none w-full font-light text-gray-500 placeholder-gray-500 y-2"
      /&gt; 
      &lt;SimpleMDE value={post.content} onChange={value =&gt; setPost({ ...post, content: value })} /&gt;
      &lt;button
        className="mb-4 bg-blue-600 text-white font-semibold px-8 py-2 rounded-lg"
        onClick={updateCurrentPost}&gt;Update Post&lt;/button&gt;
    &lt;/div&gt;
  )
}

export default EditPost
</code></pre>
<p>现在，在位于<strong>pages/_app.js</strong>的导航中添加一个新链接：</p>
<pre><code class="language-javascript">// pages/_app.js
{
  user &amp;&amp; (
    &lt;Link href="/my-posts"&gt;
      &lt;span className="mr-6 cursor-pointer"&gt;My Posts&lt;/span&gt;
    &lt;/Link&gt;
  )
}
</code></pre>
<p>当运行应用程序时，你应该能够查看你自己的帖子，编辑它们，并从更新用户界面删除它们。</p>
<h3 id="">如何启用实时更新</h3>
<p>现在，我们已经运行了应用程序，添加实时更新是很容易的。</p>
<p>默认情况下，数据库中的Realtime是禁用的。让我们为<strong>posts</strong>表打开Realtime。</p>
<p>要做到这一点，打开应用程序仪表板并点击 <strong>Databases</strong> -&gt; <strong>Replication</strong> -&gt; <strong>0 Tables</strong> (Source下面)。 打开 <strong>posts</strong> 表Realtime功能。<a href="https://supabase.io/docs/guides/api#managing-realtime">这里</a> 是一个关于如何做的视频演练，以使其清晰明了。</p>
<p>接下来，打开 <strong>src/index.js</strong>，用以下代码更新 <code>useEffect</code> hook：</p>
<pre><code>  useEffect(() =&gt; {
    fetchPosts()
    const mySubscription = supabase
      .from('posts')
      .on('*', () =&gt; fetchPosts())
      .subscribe()
    return () =&gt; supabase.removeSubscription(mySubscription)
  }, [])
</code></pre>
<p>现在，我们将订阅<strong>posts</strong>表中的实时变化。</p>
<blockquote>
<p>该应用程序的代码位于<a href="https://github.com/dabit3/supabase-next.js">这里</a>。</p>
</blockquote>
<h2 id="">下一步</h2>
<p>现在你应该对如何用Supabase和Next.js构建全栈应用有了一定的了解。</p>
<p>如果你想了解更多关于用Supabase构建全栈应用的信息，我建议查看以下资源。</p>
<ul>
<li><a href="https://supabase.io/docs">Supabase 文档</a></li>
<li><a href="https://github.com/supabase/supabase/tree/master/examples">Supabase 示例项目</a></li>
<li><a href="https://www.youtube.com/watch?v=WiwfiVdfRIc">Is Supabase Legit? Firebase Alternative Breakdown</a></li>
<li><a href="https://www.youtube.com/watch?v=v3Exg5YpJvE">Supabase Auth 深入探讨第 1 部分：JWT</a></li>
<li><a href="https://www.youtube.com/watch?v=p561ogKZ63o">Build in Public 001 - 构建 Next.js + Supabase 教程</a></li>
<li><a href="https://supabase.io/docs/learn/auth-deep-dive/auth-deep-dive-jwts">身份验证深入了解</a></li>
<li><a href="https://www.youtube.com/watch?v=j4AV2Liojk0">Supabase 和 Sveltekit</a></li>
<li><a href="https://www.youtube.com/watch?v=lQ5iIxaYduI"> 在Replit上使用Supabase和nodejs</a></li>
</ul>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 如何使用 GitHub Actions 将 Next.js 网站部署到 AWS S3 ]]>
                </title>
                <description>
                    <![CDATA[ Next.js和静态 web应用的好处是，你将它们储存在对象储存里，几乎可以在任何地方运行, 比如AWS S3。但是每次手动更新这些文件可能是一件痛苦的事. 我们如何用Github Actions来自动持续部署我们的应用程序到S3?  * 什么是GitHub Actions  * 什么是持续部署  * 我们怎么去构建  * 第0步: 在GitHub上建立一个新的Next.js项目  * 第1步: 手工创建一个用于部署next.js项目的新S3桶  * 第2步: 创建一个新的GitHub Action工作流来自动化构建一个Next.js项目  * 第3步: 配置一个GitHub Action，部署静态网站到S3上 什么是GitHub Actions GitHub Actions是GitHub的一项免费服务，它允许我们用代码实现任务自动化。 我在这篇文章 [https://www.freecodecamp.org/news/what-are-github-actions-and-how-can-you-automate-tests-and-slack-notifications/] ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-to-use-github-actions-to-deploy-a-next-js-website-to-aws-s3/</link>
                <guid isPermaLink="false">61dfafe26161280665ed8021</guid>
                
                    <category>
                        <![CDATA[ NextJS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Thu, 13 Jan 2022 04:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/01/actions-s3.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Next.js和静态 web应用的好处是，你将它们储存在对象储存里，几乎可以在任何地方运行, 比如AWS S3。但是每次手动更新这些文件可能是一件痛苦的事.</p>
<p>我们如何用Github Actions来自动持续部署我们的应用程序到S3?</p>
<ul>
<li>什么是GitHub Actions</li>
<li>什么是持续部署</li>
<li>我们怎么去构建</li>
<li>第0步: 在GitHub上建立一个新的Next.js项目</li>
<li>第1步: 手工创建一个用于部署next.js项目的新S3桶</li>
<li>第2步: 创建一个新的GitHub Action工作流来自动化构建一个Next.js项目</li>
<li>第3步: 配置一个GitHub Action，部署静态网站到S3上</li>
</ul>
<h2 id="githubactions">什么是GitHub Actions</h2>
<p>GitHub Actions是GitHub的一项免费服务，它允许我们用代码实现任务自动化。</p>
<p>我在<a href="https://www.freecodecamp.org/news/what-are-github-actions-and-how-can-you-automate-tests-and-slack-notifications/">这篇文章</a> 里介绍了如何用它来自动化任务，比如在运行代码中的测试，并向Slack发送通知。</p>
<p>它们提供一种灵活的方式，在我们现有的工作流基础上为自动化运行代码。这提供了很多的可能性，比如部署我们的网站。</p>
<h2 id="awss3">什么是 AWS S3？</h2>
<p><a href="https://aws.amazon.com/s3/">S3</a>（简单存储服务）是AWS的一个对象存储服务。它允许你在云上轻松存储文件，使它们在世界各地都可以使用。</p>
<p>它还允许你将这些文件作为一个网站使用。因为我们可以把HTML文件作为一个对象上传，我们也可以配置S3，让一个HTTP请求访问该文件。 这意味着，我们可以<a href="https://www.freecodecamp.org/news/how-to-host-and-deploy-a-static-website-or-jamstack-app-to-s3-and-cloudfront/">在S3中直接托管整个网站</a>.</p>
<h2 id="">什么使持续部署？</h2>
<p>持续部署（Continuous Deployment），通常是指将代码处在可发布的状态，自动化部署代码，缩短部署部署时间。</p>
<p>在这个示例中，我们将配置项目，持续将更新推送或者合并到git主分支，部署到网站。</p>
<h2 id="">我们怎样去构建?</h2>
<p>我们首先要使用默认的Next.js起始模板初始化一个简单的<a href="https://nextjs.org/">Next.js</a>应用，并配置将其编译成静态文件。</p>
<p>如果你不向创建一个Next.js项目，你甚至用一个简单的HTML文件跟着做，并不运行任何构建命令。但Next.js是构建动态网站应用的一种现代化方式，所以我将从这里开始。</p>
<p>随着我们的网站文件准备就绪，我们将在AWS中创建和配置一个S3桶，在S3桶上托管我们的网站。</p>
<p>最后，我们将创建一个新的GitHub Action工作流，当我们的主分支（<code>main</code>）发生新的变化时，它将自动更新S3中网站文件。</p>
<h2 id="0githubnextjs">第0步：在GitHub上创建一个新的Next.js项目</h2>
<p>我们将从Next.js的默认模板开始。</p>
<p>在创建你像创建项目的目录后，运行:</p>
<pre><code>yarn create next-app my-static-website
# 或者
npx create-next-app my-static-website
</code></pre>
<p>注意： 请注意将<code>my-static-website</code>替换为你想选择的名称。我们将在本教程的其余部分使用这个名字。</p>
<p>如果进入到该目录并运行开发命令，你应该能够成功启动你的开发服务器。</p>
<pre><code>cd my-static-website
yarn dev
# or
npm run dev
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/new-nextjs-app.jpg" alt="new-nextjs-app" width="600" height="400" loading="lazy"></p>
<p>New Next.js App</p>
<p>接下来，让我们把我们的项目配置为静态编译。</p>
<p>在 <code>package.json</code>文件, 把<code>build</code> 脚本改为 :</p>
<pre><code class="language-json">"build": "next build &amp;&amp; next export",
</code></pre>
<p>这样做的目的时告诉Next将网站导出为静态文件，我们将它用于托管网站。</p>
<pre><code>yarn build
# 或者
npm run build
</code></pre>
<p>一旦完成, 我们将查看 <code>out</code>目录，看到我们新网站的所有文件。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/nextjs-build-export-output.jpg" alt="nextjs-build-export-output" width="600" height="400" loading="lazy"></p>
<p>Next.js 的静态输出</p>
<p>最后，我们要把它推送到GitHub上。</p>
<p>在你的GitHub账号中 <a href="https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/create-a-repo">创建一个新的仓库</a>。然后会提供如何 <a href="https://docs.github.com/en/free-pro-team@latest/github/importing-your-projects-to-github/adding-an-existing-project-to-github-using-the-command-line">添加现有项目</a>到该仓库的说明。</p>
<p>一旦把你的项目推送到GitHub上，我们就做好了建立我的新网站项目的准备。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/project-on-github.jpg" alt="project-on-github" width="600" height="400" loading="lazy"></p>
<p>GitHub中的新repo</p>
<p>有下面的提交内容</p>
<ul>
<li><a href="https://github.com/colbyfayock/my-static-website/commit/ca9e4bca3c37fbd8553b0b183890c32836c35296">添加初始Next.js项目</a> 通过 <a href="https://nextjs.org/docs/api-reference/create-next-app">创建Next引用</a></li>
<li><a href="https://github.com/colbyfayock/my-static-website/commit/7907f4a0fac5f0aed2922202c5f0070dfc055f83">配置Next.js导出</a></li>
</ul>
<h2 id="1s3nextjs">第1步: 手动创建新的S3桶，并将Next.js项目部署到上面。</h2>
<p>要开始使用我们的新S3桶，首先登录你的AWS账号，并进入到S3服务。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/aws-s3-console.jpg" alt="aws-s3-console" width="600" height="400" loading="lazy"></p>
<p>发现没有桶</p>
<p>我们要创建一个新桶，使用我们选择的名字命名，用于我们网址托管的S3，我们还要配置我们的S3桶，使其能够托管一个网站。</p>
<p>_注意: 本教程步会指导你如何在S3上托管网站，但是你可以查看我的另一个教程，该教程将一步步地 <a href="https://www.freecodecamp.org/news/how-to-host-and-deploy-a-static-website-or-jamstack-app-to-s3-and-cloudfront/">指导你在S3上托管网站</a>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/s3-bucket-website-hosting.jpg" alt="s3-bucket-website-hosting" width="600" height="400" loading="lazy"></p>
<p>静态网站在AWS S3上托管</p>
<p>当我们把S3桶配置成一个网站，我们就可以回到Next.js项目文件夹，运行我们的构建命令，然后把<code>out</code>文件夹中的所有文件上传到我们的新建的S3桶。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/website-files-in-s3.jpg" alt="website-files-in-s3" width="600" height="400" loading="lazy"></p>
<p>S3桶上的静态应用</p>
<p>当这些文件被上传，并且我们已经为网站托管配置了S3桶，我们现在应该能看到我们的项目在网络上线运行！</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/nextjs-s3-website.jpg" alt="nextjs-s3-website" width="600" height="400" loading="lazy"></p>
<p>AWS S3托管Next.js应用程序</p>
<h2 id="2githubactionnextjs">第2步: 创建一个新的GitHub Action工作流来自动构建一个Next.js项目</h2>
<p>首先，我们需要创建一个新的工作流程(workflow)。</p>
<p>如果你熟悉GitHub Actions，你可以手动创建一个，单我们将通过用户界面快速创建一个。</p>
<p>进入GitHub的仓库中的<code>Action</code>标签，点击<code>set up a workflow yourself</code>,来自行设置工作流。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/github-actions-new-workflow.jpg" alt="github-actions-new-workflow" width="600" height="400" loading="lazy"></p>
<p>新的GitHub Action工作流</p>
<p>GitHub提供了一个模板，我们可以在工作流程中使用，不过我们要做一些修改。</p>
<p>让我们做以下工作。</p>
<ul>
<li>可选: 将文件重名为deploy.yml</li>
<li>可选: 将workflow重名为CD (因为它与CI不同)</li>
<li>可选: 删除所有的注释，使其更容易阅读</li>
<li>删除<code>on</code> 属性中的<code>pull_request</code></li>
<li>删除所有的 <code>steps</code> 除了<code>uses: actions/checkout@v2</code></li>
</ul>
<p>因此，在这一点上，我们应该剩下的是:</p>
<pre><code class="language-yaml">name: CD

on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
</code></pre>
<p>仅仅这段代码会触发一个流程，会启动一个新的Ubuntu实例，并在GitHub上有新的改动推送到主分支后，拉取代码到Ubuntu上。</p>
<p>接下来， 当我们获取我们的代码后，我们要构建它。然后将输出文件同步到S3。</p>
<p>这一步将基于你的项目使用yarn还是npm有所不同。</p>
<p>如果你使用yarn，在 <code>steps</code>定义下，添加以下内容。</p>
<pre><code class="language-yaml">- uses: actions/setup-node@v1
  with:
    node-version: 12
- run: npm install -g yarn
- run: yarn install --frozen-lockfile
- run: yarn build
</code></pre>
<p>如果是使用npm，添加以下内容:</p>
<pre><code class="language-yaml">- uses: actions/setup-node@v1
  with:
    node-version: 12
- run: npm ci
- run: npm run build
</code></pre>
<p>在这两个步骤之间，我们要做的是:</p>
<ul>
<li>设置 node: 这是为了我们能够使用npm 和node 来安装和运行的脚本</li>
<li>安装Yarn (仅对使用Yarn): 如果我们使用Yarn，我们将为其安装全局依赖，以便我们使用它</li>
<li>安装依赖: 我们安装我们的依赖，我们使用一个特定命令，确保我们使用<code>lock</code>文件，以避免任何意外的软件包升级</li>
<li>构建: 最后, 我们运行我们的构建命令，将我们的Next.js项目编译到<code>out</code>目录中。</li>
</ul>
<p>现在我们可以将该该文件直接提交到我们的<code>main</code>分支，这触发我们的workflow的运行，我们可以子啊<code>Actions</code>标签里看到。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/github-action-run-workflow.jpg" alt="github-action-run-workflow" width="600" height="400" loading="lazy"></p>
<p>在GitHub Actions中新的workflow</p>
<p>为了看到它的运行状态，我们进入运行的<code>workflow</code>，选择我们的<code>workflow</code>，看到所有我们的项目包含的步骤。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/github-action-successful-build.jpg" alt="github-action-successful-build" width="600" height="400" loading="lazy"></p>
<p>GitHub Action成功构建日志</p>
<p><a href="https://github.com/colbyfayock/my-static-website/commit/59e0a5158d6afbf54793d826d05455f5205c98fb">随着提交!</a></p>
<h2 id="3githubactions3">第3步: 配置一个GitHub Action，将静态网站部署到S3上</h2>
<p>现在我们正在自动构建我们的项目，我们想在S3中自动更新我们的网站。</p>
<p>为了做到这一点，我们将使用GitHub Action <a href="https://github.com/aws-actions/configure-aws-credentials">aws-actions/configure-aws-credentials(配置aws凭证)</a> 和 the AWS CLI(AWS提供的命令行)。</p>
<p>我们使用GitHub Action 将接收我们的AWS凭证和配置，并在workflow的生命周期内使用。</p>
<p>目前，GitHub Action中的Ubuntu实例允许使用AWS CLI，因为它包含在其中。因此，我们将能够在workflow中使用CLI命令。</p>
<p>另外，我们也可以使用<a href="https://github.com/jakejarvis/s3-sync-action">S3 Sync action</a>。但是通过使用AWS CLI，我们可以获得更多的灵活性来定制我们的设置，我们可以使用它来获得额外的CLI命令，一般来说，熟悉AWS CLI也是不错的。</p>
<p>为了开始，让我们在workflow添加以下片段作为附加步骤。</p>
<pre><code class="language-yaml">- uses: aws-actions/configure-aws-credentials@v1
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: us-east-1
</code></pre>
<p>上面要做的是使用AWS凭证配置action，根据我们的设置来设置我们的AWS的Access Key和Secret Key还有region(区域)。</p>
<p>AWS Region可以自定义为你通常使用的AWS账号的任何区域，我在美国东北部，所以我设置为<code>us-east-1</code>。</p>
<p>Access Key和Secret Key是你需要你的AWS账号生成的凭证。我们的代码设置方式是，我们将这些值存储在GitHub Secrets里，要防止这些密钥被泄。当action运行时，GitHub会将这些值改为星星(<code>***</code>)，这样人们就无法访问这些密钥。</p>
<p>为了设置这些secrets,我们首先要在AWS生成 <code>Access Keys</code>。</p>
<p>进入了AWS控制台。在用户菜单下，选择 <strong>My Security Credentials</strong>，然后选择 <strong>Create access key</strong>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/aws-console-create-access-key.jpg" alt="aws-console-create-access-key" width="600" height="400" loading="lazy"></p>
<p>在AWS创建一个 <code>Access Key</code></p>
<p>这会生成两个值  <strong>Access key ID</strong> 和<strong>Secret access key</strong>。必须保存好这些值，因为你将无法再次访问<code>Secret key ID </code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/aws-secret-access-keys.jpg" alt="aws-secret-access-keys" width="600" height="400" loading="lazy"></p>
<p>在AWS中寻找 <code>Secret Key</code> 和 <code>Access Key</code></p>
<p><em>注意: 记住不要再你的代码中包含<code>Access Key</code>和<code>Secret Key</code>。这可能导致有人破坏你的AWS凭证。</em></p>
<p>下一步，在GitHub repo中，进入到 Settings -&gt; Secrets，然后选择 <code>New secret</code>。</p>
<p>在这里，我们要使用AWS keys添加到下面的secrets:</p>
<ul>
<li>AWS_ACCESS_KEY_ID: your AWS Access key ID</li>
<li>AWS_SECRET_ACCESS_KEY: your AWS Secret key</li>
</ul>
<p>当保存下来，你就应该记住这两个新的<code>secrets</code>。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/github-secrets-access-keys.jpg" alt="github-secrets-access-keys" width="600" height="400" loading="lazy"></p>
<p>在GitHub中创建<code>Secrets</code></p>
<p>现在我们已经配置好了我们的凭证，我们应该为运行命令，将我们的项目同步到S3，做好准备。</p>
<p>在Github Action，添加以下步骤:</p>
<pre><code class="language-yaml">- run: aws s3 sync ./out s3://[bucket-name]
</code></pre>
<p><em>注意: 请确保<code>[bucket-name]</code> 替换为你的S3桶的名称。</em></p>
<p>这个命令会触发与我们的S3桶的同步(sync)，使用<code>out</code>目录的文件，也就是我们项目构建的地方。</p>
<p>现在，如果我们提交我们的修改，我们可以看到，一旦提交到<code>main</code>分支，我们的actions会自动触发，我们构建我们的项目并同步到S3！</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/github-action-sync-s3-bucket.jpg" alt="github-action-sync-s3-bucket" width="600" height="400" loading="lazy"></p>
<p>成功通过GitHub Action workflow 同步到AWS S3</p>
<p><em>注意: 请确保在设置这个action之前，你已经将S3桶配置为网站托管(包括解除S3桶权限) --否则这个action可能失败。</em></p>
<p>在这一点上，我们的项目可能看起来是一样的，因为我们对代码进行任何修改。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/nextjs-s3-website.jpg" alt="nextjs-s3-website" width="600" height="400" loading="lazy"></p>
<p>AWS S3的Next.js应用程序</p>
<p>但如果你做了一个代码修改，比如在<code>pages/index.js</code>中改变主页的标题，并提交该修改:</p>
<pre><code class="language-jsx">&lt;h1 className={styles.title}&gt;
  Colby's &lt;a href="https://nextjs.org"&gt;Next.js!&lt;/a&gt; Site
&lt;/h1&gt;
</code></pre>
<p>我们可以看到，我们的修改触发了workflow的启动:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/github-action-commit-workflow.jpg" alt="github-action-commit-workflow" width="600" height="400" loading="lazy"></p>
<p>新的GitHub Action workflow的触发来自代码改变</p>
<p>一旦我们的workflow完成，我们可以看到我们的内容已经在我们的网站上自动更新。</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/10/updated-nextjs-site-title.jpg" alt="updated-nextjs-site-title" width="600" height="400" loading="lazy"></p>
<p>AWS S3托管的应用程序，代码已经更新</p>
<p>随着内容的提交</p>
<ul>
<li><a href="https://github.com/colbyfayock/my-static-website/commit/f891412b827aca4b06e9bf3de8e4e5b4c5704fc8">添加ASW的配置和S3 sync命令</a></li>
<li><a href="https://github.com/colbyfayock/my-static-website/commit/bb9b981416645e35c6d3442e02d6b61f2ba032d2">测试workflow的标题的更新</a></li>
</ul>
<h2 id="">我们还能做什么?</h2>
<h3 id="cloudfront">设置CloudFront</h3>
<p>这个篇文章的目的不是要经历AWS配置网站的整个过程，但是你在S3上运行网站服务，你可能在之前考虑过CloudFront。</p>
<p>你可以查看以下<a href="https://www.freecodecamp.org/news/how-to-host-and-deploy-a-static-website-or-jamstack-app-to-s3-and-cloudfront/">我的另一个指南</a>，它指导你如何设置CloudFront，以及如何在S3中创建网站的手把手指南。</p>
<h3 id="cloudfront">CloudFront的缓存失效</h3>
<p>如果你的S3网站在CloudFront后面，有可能你会确保CloudFront没有缓存新的变化。</p>
<p>通过AWS CLI，我们也可以触发CloudFront的缓存失效，以确保它正在抓取最新的变化。</p>
<p><a href="https://docs.aws.amazon.com/cli/latest/reference/cloudfront/create-invalidation.html">请看这里的文档</a>学习更多的知识.</p>
<h3 id="pullrequest">pull request部署</h3>
<p>如果你不断地在pull request中的网站修改，有时候更容易看到网站的修改。</p>
<p>你可以设置一个新的workflow，只在pull request上运行，workflow可以根据分支或者环境动态创建一个新的桶，并在pull request上添加一个带有该URL的comment。</p>
<p>你也许能找到一个GitHub Action 作为你管理你pull request上带的comments,你可以查询<a href="https://docs.github.com/en/free-pro-team@latest/rest/reference/actions">GitHub Actions文档</a>.</p>
<p><a href="https://twitter.com/colbyfayock"><img src="https://res.cloudinary.com/fay/image/upload/w_2000,h_400,c_fill,q_auto,f_auto/w_1020,c_fit,co_rgb:007079,g_north_west,x_635,y_70,l_text:Source%20Sans%20Pro_64_line_spacing_-10_bold:Colby%20Fayock/w_1020,c_fit,co_rgb:383f43,g_west,x_635,y_6,l_text:Source%20Sans%20Pro_44_line_spacing_0_normal:Follow%20me%20for%20more%20JavaScript%252c%20UX%252c%20and%20other%20interesting%20things!/w_1020,c_fit,co_rgb:007079,g_south_west,x_635,y_70,l_text:Source%20Sans%20Pro_40_line_spacing_-10_semibold:colbyfayock.com/w_300,c_fit,co_rgb:7c848a,g_north_west,x_1725,y_68,l_text:Source%20Sans%20Pro_40_line_spacing_-10_normal:colbyfayock/w_300,c_fit,co_rgb:7c848a,g_north_west,x_1725,y_145,l_text:Source%20Sans%20Pro_40_line_spacing_-10_normal:colbyfayock/w_300,c_fit,co_rgb:7c848a,g_north_west,x_1725,y_222,l_text:Source%20Sans%20Pro_40_line_spacing_-10_normal:colbyfayock/w_300,c_fit,co_rgb:7c848a,g_north_west,x_1725,y_295,l_text:Source%20Sans%20Pro_40_line_spacing_-10_normal:colbyfayock/v1/social-footer-card" alt="关注我，了解更多的Javascript、UX和其他有趣的事情!" width="600" height="400" loading="lazy"></a></p>
<ul>
<li><a href="https://twitter.com/colbyfayock">🐦 在推特上关注我</a></li>
<li><a href="https://youtube.com/colbyfayock">🎥 在油管上订阅我</a></li>
<li><a href="https://www.colbyfayock.com/newsletter/">✉️ 订阅我的Newsletter</a></li>
<li><a href="https://github.com/sponsors/colbyfayock">💝 赞助我</a></li>
</ul>
<!--kg-card-end: markdown--><p>原文：<a href="https://www.freecodecamp.org/news/how-to-use-github-actions-to-deploy-a-next-js-website-to-aws-s3/">How to Use Github Actions to Deploy a Next.js Website to AWS S3</a>，作者：<a href="https://www.freecodecamp.org/news/author/colbyfayock/">Colby Fayock</a></p> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
