React服务端组件实战指南:从零构建高性能博客

本文详细记录了作者从Angular转向React服务端组件的心路历程,包含完整的技术实现方案、常见问题解决方法和性能优化技巧,帮助开发者深入理解RSC的工作原理和实际应用场景。

React服务端组件实战:构建高性能博客的完整指南

我的学习之旅

几周前,我终于决定深入了解React服务端组件(RSC)。作为一名主要使用Angular的开发者,我一直在旁观React并学习它。RSC已经实验性存在多年,但随着React 19使其稳定,我认为是时候看看它到底有什么特别之处。原本计划"周末试试"的想法最终彻底重建了我对React工作原理的心智模型。

React服务端组件是什么?

两年前,我听到Dan Abramov在某个会议上谈论React服务端组件。我的第一反应是"太好了,又一个我可能永远不会使用的React功能"。当时它们还处于实验阶段,我以为会一直这样。我错了。

事情是这样的:你知道你的React应用会向浏览器发送一个庞大的JavaScript包,然后浏览器必须解析所有这些代码、运行它、进行API调用,最后向用户显示内容吗?是的,这…不太好。特别是在连接速度慢或手机较旧的情况下。

服务端组件颠覆了这一点。你的部分组件在服务器上运行,在那里完成所有繁重的工作,只将渲染好的HTML发送到浏览器。用户立即获得内容,你的JavaScript包缩小,每个人都很开心。

为什么我现在关心React服务端组件

我的包确实变小了

之前,我的测试项目主包大小为400KB。之后:118KB。这不是打字错误。事实证明,当你的一半组件在服务器上运行时,你不需要将它们的代码发送到浏览器。来自Angular背景,我们习惯了tree-shaking和懒加载,但这仍然令人印象深刻。

有意义的数据获取

我一直听说React开发者抱怨useEffect、useState、setLoading的舞蹈。来自Angular,我们有服务和observables,React的数据获取总是显得不必要的复杂。使用服务端组件,我只需…获取数据。在组件中。像一个普通函数。这几乎太简单了。

我的Lighthouse分数变得更好

核心Web指标"实际上相当不错",开箱即用。作为一个习惯使用Angular Universal进行SSR的人,这感觉出乎意料地轻松。

重大的心智转变

传统React:一切都在浏览器中发生。每个组件、每个状态更新、每个API调用 - 全部在客户端。工作正常,直到你的包大小开始变得庞大。

带RSC的React:一些组件在服务器上运行(无客户端代码),一些在客户端运行(用于交互性)。服务器组件可以直接与数据库通信。客户端组件处理点击和表单提交。这就像有两个不同的执行环境,不知何故可以一起工作,而你不必过多考虑。

来自Angular背景,这在某些方面感觉很熟悉,我们使用Angular Universal进行服务器端渲染已有多年。但RSC无缝混合服务器和客户端执行的方式确实不同。不再需要"在useEffect中获取数据,存储在状态中,显示加载微调器"的舞蹈。只需在组件中使用async/await,你就完成了。

设置我的Mac

在我们开始之前,让我们确保你的电脑不会与你对抗。我花了1小时调试一个最终是Node版本问题的事情,才艰难地学到了这一点。

基础

打开终端。你知道,那个让你感觉像黑客的应用。运行这些:

1
2
node --version
npm --version

你需要Node 18或更高版本。如果你使用的是旧版本,请访问nodejs.org并下载最新的LTS版本。

如果你没有VS Code,请获取它。

1
code --version

真正有帮助的扩展

我浪费了时间安装从不使用的扩展。以下是真正重要的:

打开VS Code,按⌘ + Shift + X并安装:

  • ES7+ React/Redux/React-Native snippets - 避免你无数次输入import React
  • Tailwind CSS Intelli-Sense - 因为没人记住CSS类名
  • Auto Rename Tag - 当你更改开始标签时更改结束标签。改变生活。
  • GitLens - 使Git在VS Code中真正有用

构建真实的东西(并立即破坏它)

是时候停止阅读并开始破坏东西了。我在Documents目录中创建了一个文件夹,因为我没有组织到拥有适当的项目文件夹:

1
2
cd ~/Documents
npx create-next-app@latest my-rsc-blog --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"

是的,这个命令很拗口。但每个标志都很重要:

  • –typescript 因为凌晨2点调试JavaScript错误不好玩
  • –tailwind 因为我CSS不好
  • –eslint 因为我犯愚蠢的错误
  • –app 因为那是RSC所在的地方
  • –src-dir 因为我喜欢有组织的项目
  • –import-alias 因为../../../components很丑
1
2
cd my-rsc-blog
npm run dev

访问http://localhost:3000,你应该看到Next.js欢迎页面。如果你遇到端口错误,只需使用不同的端口:

1
npm run dev -- -p 3001

你实际得到什么

在VS Code中打开项目:

1
code .

结构看起来像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
my-rsc-blog/
├── src/
│   └── app/
│       ├── globals.css
│       ├── layout.tsx
│       ├── page.tsx
│       └── favicon.ico
├── package.json
├── tailwind.config.ts
└── tsconfig.json

干净。简单。即将变得更加复杂。

服务器与客户端组件

这花了我尴尬的长时间才理解。让我为你节省一些痛苦。

默认情况下,app目录中的每个组件都是服务器组件。它在服务器上运行。它不能使用useState或onClick或任何浏览器API,因为没有浏览器。但它可以直接从数据库获取数据,真正保持秘密,并进行昂贵的计算而不会让手机烫手。

客户端组件在顶部有"use client"。它们在浏览器中运行。它们可以处理用户交互、管理状态并使用你熟悉和喜爱的所有React钩子。

我的第一个"等等,这真的有效"时刻

我创建了src/app/components/ServerTime.tsx:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 没有"use client" = 在服务器上运行
async function ServerTime() {
  // 这段代码永远不会到达浏览器
  const response = await fetch('http://worldtimeapi.org/api/timezone/Europe/London');
  const data = await response.json();

  return (
    <div className="p-4 bg-blue-50 rounded-lg">
      <h2 className="text-lg font-bold">服务器组件</h2>
      <p>当前时间: {data.datetime}</p>
      <p className="text-sm text-gray-600">
        这是在服务器上获取的。检查网络标签 - 没有API调用
      </p>
    </div>
  );
}

export default ServerTime;

疯狂的部分?没有useEffect。没有useState。没有加载状态。只是像普通函数一样的async/await。服务器获取数据,渲染HTML并将其发送到浏览器。浏览器从未看到API调用。

构建客户端组件进行比较

然后我创建了src/app/components/ClientCounter.tsx:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"use client"; // 这一行改变了一切

import { useState } from 'react';

function ClientCounter() {
  const [count, setCount] = useState(0);

  return (
    <div className="p-4 bg-green-50 rounded-lg">
      <h2 className="text-lg font-bold">客户端组件</h2>
      <p>计数: {count}</p>
      <button
        onClick={() => setCount(count + 1)}
        className="mt-2 px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
      >
        点击我!
      </button>
      <p className="text-sm text-gray-600">
        这在你的浏览器中运行并处理交互。
      </p>
    </div>
  );
}

export default ClientCounter;

我更新了src/app/page.tsx以使用两者:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import ServerTime from './components/ServerTime';
import ClientCounter from './components/ClientCounter';

export default function Home() {
  return (
    <main className="max-w-4xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">服务器 vs 客户端组件</h1>
      <div className="space-y-6">
        <ServerTime />
        <ClientCounter />
      </div>
    </main>
  );
}

刷新页面,观察服务器组件的时间在每次重新加载时更新,而客户端组件保持其状态。那时我才真正理解。

构建博客(因为TodoMVC已经过时了)

够了玩具例子。是时候为这个学习练习构建一些现实的东西了。博客似乎是完美的RSC用例,主要是静态内容和一些交互部分。

规划

我想要:

  • 博客文章列表(服务器组件)
  • 单独的文章页面(带动态路由的服务器组件)
  • 搜索功能(表单的客户端组件,结果的服务器组件)
  • 不让用户认为网站坏掉的加载状态
  • 不向用户显示堆栈跟踪的错误处理

假数据(因为我懒)

我创建了src/app/lib/blog-data.ts,包含一些模拟数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
export interface BlogPost {
  id: string;
  title: string;
  excerpt: string;
  content: string;
  author: string;
  publishedAt: string;
  readingTime: string;
  tags: string[];
}

const posts: BlogPost[] = [
  {
    id: '1',
    title: '为什么我终于学习了React服务端组件',
    excerpt: '忽略它们六个月后,我终于屈服了。这是我所学到的。',
    content: '我曾经认为React服务端组件只是炒作。',
    author: '我',
    publishedAt: '2025-07-15',
    readingTime: '5分钟',
    tags: ['React', 'Next.js', '学习']
  },
  {
    id: '2',
    title: 'CSS摧毁我灵魂的那一天',
    excerpt: '关于特异性、级联以及为什么我现在使用Tailwind的故事。',
    content: '想象一下:你花了三个小时让你的组件看起来完美。然后你添加一个CSS规则,一切都崩溃了...',
    author: '还是我',
    publishedAt: '2025-06-10',
    readingTime: '3分钟',
    tags: ['CSS', 'Tailwind']
  }
];

// 模拟慢速网络,因为真实API不是即时的
function delay(ms: number) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

export async function getBlogPosts(): Promise<BlogPost[]> {
  await delay(800); // 模拟一些加载时间
  return posts;
}

export async function getBlogPost(id: string): Promise<BlogPost | null> {
  await delay(600);
  return posts.find(post => post.id === id) || null;
}

export async function searchPosts(query: string): Promise<BlogPost[]> {
  await delay(500);
  if (!query.trim()) return [];

  return posts.filter(post =>
    post.title.toLowerCase().includes(query.toLowerCase()) ||
    post.content.toLowerCase().includes(query.toLowerCase()) ||
    post.tags.some(tag => tag.toLowerCase().includes(query.toLowerCase()))
  );
}

延迟函数很重要。真实API有延迟,你想测试你的加载状态。

构建文章卡片组件

我创建了src/app/components/PostCard.tsx:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import Link from 'next/link';
import { BlogPost } from '../lib/blog-data';

interface PostCardProps {
  post: BlogPost;
}

function PostCard({ post }: PostCardProps) {
  return (
    <article className="bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow p-6">
      <div className="flex items-center text-sm text-gray-500 mb-3">
        <span>{post.author}</span>
        <span className="mx-2"></span>
        <span>{new Date(post.publishedAt).toLocaleDateString()}</span>
        <span className="mx-2"></span>
        <span>{post.readingTime}</span>
      </div>

      <h2 className="text-xl font-bold text-gray-900 mb-3">
        <Link href={`/blog/${post.id}`} className="hover:text-blue-600">
          {post.title}
        </Link>
      </h2>

      <p className="text-gray-600 mb-4">{post.excerpt}</p>

      <div className="flex flex-wrap gap-2">
        {post.tags.map(tag => (
          <span key={tag} className="px-2 py-1 bg-gray-100 text-gray-700 text-sm rounded">
            {tag}
          </span>
        ))}
      </div>
    </article>
  );
}

export default PostCard;

没什么花哨的。只是一个看起来不错并链接到单独文章的卡片。

主页(事情变得有趣的地方)

我重写了src/app/page.tsx:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { Suspense } from 'react';
import { getBlogPosts } from './lib/blog-data';
import PostCard from './components/PostCard';
import SearchForm from './components/SearchForm';

async function PostList() {
  const posts = await getBlogPosts();

  return (
    <div className="grid gap-6 md:grid-cols-2">
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

function LoadingSkeleton() {
  return (
    <div className="grid gap-6 md:grid-cols-2">
      {[...Array(4)].map((_, i) => (
        <div key={i} className="bg-gray-200 rounded-lg h-48 animate-pulse" />
      ))}
    </div>
  );
}

export default function Home() {
  return (
    <main className="max-w-6xl mx-auto px-4 py-8">
      <header className="text-center mb-12">
        <h1 className="text-4xl font-bold text-gray-900 mb-4">我的博客</h1>
        <p className="text-xl text-gray-600">
          关于代码、生活以及为什么一切在凌晨3点崩溃的思考
        </p>
      </header>

      <SearchForm />

      <Suspense fallback={<LoadingSkeleton />}>
        <PostList />
      </Suspense>
    </main>
  );
}

这里的魔力是Suspense。当PostList在服务器上获取数据时,用户看到加载骨架。没有JavaScript瀑布,没有加载内容的闪烁 - 只有流畅的渐进式渲染。

单独文章页面(真正有效的动态路由)

我创建了src/app/blog/[id]/page.tsx:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { getBlogPost } from '../../lib/blog-data';
import type { Metadata } from 'next';

interface PostPageProps {
  params: Promise<{ id: string }>;
}

// 这为每篇文章生成元数据
export async function generateMetadata({ params }: PostPageProps): Promise<Metadata> {
  // Next.js 15需要await params(艰难地学到了这一点)
  const { id } = await params;
  const post = await getBlogPost(id);

  if (!post) {
    return { title: '文章未找到' };
  }

  return {
    title: post.title,
    description: post.excerpt,
    authors: [{ name: post.author }],
  };
}

export default async function PostPage({ params }: PostPageProps) {
  const { id } = await params;
  const post = await getBlogPost(id);

  if (!post) {
    notFound(); // 显示404页面
  }

  return (
    <main className="max-w-4xl mx-auto px-4 py-8">
      <Link href="/" className="text-blue-600 hover:text-blue-800 mb-8 inline-block">
         返回博客
      </Link>

      <article className="prose prose-lg max-w-none">
        <header className="mb-8">
          <h1 className="text-4xl font-bold text-gray-900 mb-4">{post.title}</h1>

          <div className="flex items-center text-gray-600 mb-6">
            <span>{post.author}</span>
            <span className="mx-2"></span>
            <span>{new Date(post.publishedAt).toLocaleDateString()}</span>
            <span className="mx-2"></span>
            <span>{post.readingTime}</span>
          </div>

          <div className="flex flex-wrap gap-2">
            {post.tags.map(tag => (
              <span key={tag} className="px-2 py-1 bg-gray-100 text-gray-700 text-sm rounded">
                {tag}
              </span>
            ))}
          </div>
        </header>

        <div className="text-gray-800 leading-relaxed">
          {post.content}
        </div>
      </article>
    </main>
  );
}

每篇文章都有自己的URL、适当的元数据和服务器端渲染。generateMetadata函数在服务器上运行,并用正确的信息填充标签。

添加搜索(客户端+服务器组件协同工作)

搜索表单需要交互,所以我使src/app/components/SearchForm.tsx成为客户端组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
"use client";

import { useState } from 'react';
import { useRouter } from 'next/navigation';

function SearchForm() {
  const [query, setQuery] = useState('');
  const router = useRouter();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (query.trim()) {
      router.push(`/search?q=${encodeURIComponent(query)}`);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="mb-8">
      <div className="flex gap-2 max-w-md mx-auto">
        <input
          type="text"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="搜索文章..."
          className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
        />
        <button
          type="submit"
          className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus:ring-2 focus:ring-blue-500"
        >
          搜索
        </button>
      </div>
    </form>
  );
}

export default SearchForm;

然后我在src/app/search/page.tsx创建了搜索结果页面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { Suspense } from 'react';
import Link from 'next/link';
import { searchPosts } from '../lib/blog-data';
import PostCard from '../components/PostCard';

interface SearchPageProps {
  searchParams: Promise<{ q?: string }>;
}

async function SearchResults({ query }: { query: string }) {
  const results = await searchPosts(query);

  if (results.length === 0) {
    return (
      <div className="text-center py-12">
        <p className="text-gray-600 text-lg">
          没有找到关于"{query}"的文章。尝试不同的关键词?
        </p>
      </div>
    );
  }

  return (
    <div className="grid gap-6 md:grid-cols-2">
      {results.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

export default async function SearchPage({ searchParams }: SearchPageProps) {
  // 另一个Next.js 15的事情 - searchParams必须被await
  const { q: query = '' } = await searchParams;

  return (
    <main className="max-w-6xl mx-auto px-4 py-8">
      <Link href="/" className="text-blue-600 hover:text-blue-800 mb-6 inline-block">
         返回博客
      </Link>

      <h1 className="text-3xl font-bold text-gray-900 mb-8">
        搜索结果
        {query && <span className="text-gray-600"> 关于 "{query}"</span>}
      </h1>

      {query ? (
        <Suspense fallback={<div>搜索中...</div>}>
          <SearchResults query={query} />
        </Suspense>
      ) : (
        <p className="text-gray-600">输入搜索词来查找文章。</p>
      )}
    </main>
  );
}

漂亮。客户端组件处理表单交互,服务器组件处理搜索逻辑和渲染。它们协同工作,而你无需考虑太多。

当一切崩溃时(有趣的部分)

没有灾难的教程是不完整的。以下是我遇到的真实问题以及如何修复它们。

伟大的Params Await灾难

这个错误消息困扰着我的梦想:“Route used params.id. params should be awaited before using its properties.”

发生了什么?我升级到Next.js 15,突然我所有的动态路由都崩溃了。事实证明Next.js 15改变了params的工作方式。你不能只是做params.id了。你必须先await整个params对象。

修复很烦人但简单:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 这在Next.js 15中崩溃
export default function BlogPost({ params }: { params: { id: string } }) {
  const post = getBlogPost(params.id); // 错误!
}

// 这有效
export default async function BlogPost({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params; // 必须先await
  const post = await getBlogPost(id);
}

searchParams也是一样。现在一切都必须被await。我花了1小时调试这个才找到迁移指南。

CSS消失事件

有一天我的样式就…消失了。一切工作,但看起来像1995年的网站。HTML有所有的Tailwind类,但没有CSS加载。

事实证明Next.js 15附带Tailwind CSS v4,它有一个完全不同的配置系统。我必须降级到v3:

1
2
npm uninstall tailwindcss @tailwindcss/postcss
npm install tailwindcss@^3.4.1 autoprefixer

然后更新postcss.config.mjs:

1
2
3
4
5
6
7
8
const config = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

export default config;

清除缓存并重启:

1
rm -rf .next && npm run dev

样式回来了。危机避免。

神秘的构建清单错误

有时Next.js只是失去理智,找不到自己的构建文件:

1
ENOENT: no such file or directory, open '.next/build-manifest.json'

修复总是一样的:

1
2
rm -rf .next
npm run dev

这发生的频率比应该的高,但通常是一个快速修复。每当奇怪的事情发生时,我已经开始自动这样做。

TypeScript变得额外

有时TypeScript决定变得"有帮助",并为绝对应该工作的事情抛出类型错误。核选项:

1
2
3
4
rm -rf node_modules/.cache
rm -rf .next
npm install
npm run dev

这会清除所有类型缓存,通常修复TypeScript抱怨的任何东西。

使其快速(好东西)

RSC应该是高性能的,但你仍然需要聪明地使用它。

博客文章的静态生成

由于博客文章不经常更改,我可以在构建时预生成它们:

1
2
3
4
5
// 将此添加到你的博客文章页面
export async function generateStaticParams() {
  const posts = await getBlogPosts();
  return posts.map(post => ({ id: post.id }));
}

现在当我构建站点时,Next.js为每个博客文章创建静态HTML文件。闪电般快速加载,零服务器工作。

使用Suspense进行流式传输

我添加的Suspense边界不仅用于加载状态 - 它们启用流式传输。服务器立即发送页面外壳,然后在内容可用时流式传输内容。用户立即看到某些东西,而不是盯着空白页面。

包分析

想看看你的JavaScript包中实际有什么吗?

1
npm install --save-dev @next/bundle-analyzer

将此添加到next.config.js:

1
2
3
4
5
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({});

运行ANALYZE=true npm run build,你将获得包大小的可视化细分。看到你的客户端包在RSC下有多小是相当令人满意的。

我实际学到了什么(真正的东西)

在构建这个学习项目之后,以下是真正坚持下来的东西:

RSC改变你对React的思考方式

最大的转变不是技术性的,而是心智上的。在传统React中,你从客户端状态和效果的角度思考。使用RSC,你从代码运行位置和每个组件需要什么数据的角度思考。在某些方面更简单,在其他方面更复杂。

服务器和客户端之间的边界很重要

你不能只是到处粘贴"use client"就完事了。每个边界都有性能影响。服务器组件非常适合数据获取和初始渲染。客户端组件对于交互性是必要的。明智选择。

Next.js 15大部分很棒

带有RSC的App Router现在感觉成熟了。开发者体验扎实,性能优势真实,生态系统正在迎头赶上。仍然有一些粗糙的边缘(比如params的事情),但比pages router是一个巨大的改进。

错误边界更重要

随着服务器和客户端组件混合在一起,错误边界变得关键。如果你不小心错误处理,失败的服务器组件可能会破坏整个页面。

它实际上相当简单

一旦你度过了最初的困惑,RSC是直接的。服务器组件获取数据并渲染HTML。客户端组件处理交互。框架处理使它们协同工作的复杂性。

结束(终于)

React服务端组件不仅仅是炒作。它们是对React应用构建方式的真正改进。性能优势是真实的,开发者体验比传统React更好(一旦你学会了怪癖),心智模型是有意义的,特别是来自服务器端框架。

它完美吗?不。有一个学习曲线,一些令人困惑的错误消息,你必须更仔细地思考代码运行的位置。但对于像博客、营销页面或电子商务这样内容繁重的站点,RSC可能足够引人注目,可以考虑在通常选择Angular的项目中使用React。

我构建的学习项目演示了核心概念:用于数据获取的服务器组件,用于交互性的客户端组件,适当的加载状态和错误处理。从这里,你可以添加身份验证、评论、CMS或你的实际项目需要的任何东西。

最重要的是:试验东西。破坏东西。修复东西。仔细阅读错误消息(它们通常很有帮助)。当事情变得奇怪时,不要害怕删除.next并重启。记住,如果你作为一个学习React的Angular开发者感到困惑,你并不孤单。概念不同,但随着练习开始变得有意义。

现在去构建一些酷的东西。当你不可避免地遇到我没有提到的奇怪错误时,你会弄清楚的。这就是我们所做的。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计