Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React Server Component - 在服务端写 React 组件是什么体验? #51

Open
qufei1993 opened this issue May 17, 2023 · 0 comments
Open
Assignees

Comments

@qufei1993
Copy link
Owner

qufei1993 commented May 17, 2023

What?在服务端写 React 组件?这看起来是一件不可思议的事情,首先在服务端,我们有很多的权限,例如访问文件系统、数据库等。如果在服务端写前端组件,是不是意味着 “前端可以直链数据库了?” 还有安全性可言吗?

本文代码示例基于 Next.js 框架,让我们先看一段代码示例,你可以先思考一个问题,这是服务端代码还是前端代码?

// src/app/home/page.tsx
import fs from 'fs/promises';
const DisplayText = async () => {
  try {
    const text = await fs.readFile('/xxx/my-next-app/text.txt', { encoding: 'utf8' });
    console.log(text); 
    return <p>{text}</p>
  } catch (err) {
    return <p>{err.message}</p>
  }
}


const Home = () => {
  return <div>
    <p>Hello Server Component</p>
    {/* @ts-expect-error */}
    <DisplayText />
  </div>
}


export default Home;

看到这段代码你的想法是什么?如果是前端组件代码,怎么会有 fs 模块呢?在前端是没有文件系统的,更不能访问本地的文件。如果是后端,看起来怎么还有前端的组件代码?

没什么神秘的,这是 React 中的 Server Component,简称 RSC。它是 React 提出的一种新型组件类型,以前我们编写组件的方式可以称为 Client Component。Server Component 它的渲染是在服务端完成之后通过网络请求交给客户端 React 做整合,如果运行时是 Node.js,在 Server Component 中就可以使用 Node.js 中的所有模块资源,访问数据库这些自然就可以了。

它安全吗?有些代码你可能只想在服务器上运行,例如在 Server Component 会有一些链接数据库读取数据的代码逻辑,但 JavaScript 模块可以在 Server Component 和 Client Component 之间实现共享,有时你会无意识地在 Client Component 中导入服务器端的代码,为了避免敏感的信息被暴露,在 Next.js 框架中,我们可以在文件头部添加一个 import 'server-only'; 语句,表示该模块只在服务器端使用。

为什么要用 RSC?

React Server Component 带来了一种新的思维模式,我们要考虑为什么使用它?以下是它带来的一些好处:

  • 不必在客户端渲染整个应用程序,这带来了一些好处:减少了包的体积、更快的首页加载。
  • 拥有 Node.js 执行环境,能够轻松的获取到后端的完整资源,例如访问文件系统、数据库等。
  • 优化了 Client -> Server 端的瀑布流(Waterfall),原先如果需要多个请求获取数据,需要在 useEffect(() => {}, []) 中请求 API 获取数据,做渲染,使用 Server Component 后可以减少这些请求。
  • 结合 Streaming、Suspense 可以让页面的部分内容先返回渲染,相比 SSR 会带来更好的体验。

这里有一个重点是要与 SSR 区别开来。SSR 也是在服务端运行,使用它可以解决首屏加载、SEO 问题,它的工作方式是把整个应用程序打包为 html 然后返回到浏览器渲染。React Server Component 返回的是一个客户端可以解析的 React 结构,之后会在客户端同 Client Component 混合在做渲染。在 React Server Component 中还可以结合 Streaming、Suspense 允许服务器先返回页面的部分内容,待一些异步组件稍后就绪时再返回余下的内容,这也是其强大的一方面。

RSC 流式传输

下例代码中,如果去掉 <Suspense> </Suspense>,React 会等待 <DisplayText /> 组件渲染完成,才会发送内容到浏览器。假设读取数据很慢,需要 5 秒中,此时页面就有 5 秒钟的等待。如果是 SSR 来处理,它的问题也是如此,会在这里等待,等所有的内容拼接完成后才会返回。

现在我们可以先让页面的其它部分先返回到浏览器渲染,Suspense 的 fallback 先渲染第一个版本,待 <DisplayText /> 组件就绪时渲染第二个版本。

import fs from 'fs/promises';
import { Suspense } from 'react';


+ const sleep = (ms: number) => new Promise((resolve, reject) => setTimeout(() => resolve(1), ms))
const DisplayText = async () => {
  try {
+   await sleep(5000);
    const text = await fs.readFile('/xxx/my-next-app/text.txt', { encoding: 'utf8' });
    console.log(text); 
    return <p>{text}</p>
  } catch (err) {
    return <p>{err.message}</p>
  }
}


const Home = () => {
  return <div>
    <p>Hello Server Component</p>
+   <Suspense fallback={<> Loading... </>}>
+     {/* @ts-expect-error */}
+     <DisplayText />
+   </Suspense>
  </div>
}


export default Home;

以下是运行后的预览效果,刚开始会显示 Loading...

image.png

大约 5 秒钟后,展示 <DisplayText /> 组件获取到的内容

什么情况下使用 RSC?

Next.js 框架 App Router 路由默认开启 Server Component 模式,它认为在大多数情况下应该编写 Server Component 而不是 Client Component。对于开发者而言要有一个边界,来区分什么情况下应该使用 Server Component、什么情况下应该使用 Client Component。

上图是 Next.js 文档的一个描述,以下做了一些总结:

应该用 Server Component 的情况:

  • 获取数据渲染组件
  • 访问后端资源(文件系统或数据库)
  • 组件不会被客户端组件引用

应该用 Client Component 的情况:

  • 存在客户端交互行为(具有 onClick、onChange 等事件行为)
  • 使用 React 生命周期函数(useEffect()、useState())
  • 不支持 React Server Component 的一些三方库(如果想使用也可以通过自己包装的方式)
  • 使用仅限于浏览器的 API
  • 使用 React Class Component

一个 Server、Client Component 示例

现在我们知道了 Server Component 有一些限制,当我们需要写一些具有交互性的组件时必须在 Client Component 内完成。

以下是一个 Server Component 引入 Client Component 的示例,它们在服务器上预渲染并在客户端上混合。

// src/app/home/page.tsx
import Count from './Count';
const Home = () => {
  return <div>
    ...
    <Count />
  </div>
}


export default Home;

Server Component 是默认的组件类型,如果要写 Client Component 需要先在文件顶部引入 'use client' 指令,定义 Client Component 和 Server Component 的边界。

// src/app/home/Count.tsx
'use client';
import { useState } from "react";
const btnStyle = { padding: '2px 5px', marginLeft: '10px' };
const Count = () => {
  console.log('Client Component');
  const [count, setCount] = useState(0);
  return <div>
    current count: {count}
    <button style={{...btnStyle}} onClick={() => setCount(preCount  => preCount + 1)}> + </button>
    <button style={{...btnStyle}} onClick={() => setCount(preCount  => preCount - 1)}> - </button>
  </div>
};


export default Count;

总结

React Server Component 不是一个新的概念了,第一次提出是在 2020 年 12 月,它提出的这种 Server Component 编程范式,还是挺有趣的,值得学习下。

之前也是大致了解,最近看了一些 Next.js 相关的内容,在 2022 年底 Next.js 13 发布时整合了一些 React Server Component 相关的内容,并在 App Router 将 Server Component 做为默认的组件类型,这也是值得关注的一个地方。

在当前前后端分离的背景下,这种前后端写在一起的方式是不是和以前写 jadeejs 这种类似?看似回到了以前,但 Server Component 也解决了一些问题,例如减少了 Bundle Size、请求的瀑布流等,并且在服务端也可以使用一些更成熟的 UI 库来写 Server Component。

Server Component 的到来,也看到了前端的边界在不断的扩大。学习一些 Node.js 和后端相关的知识也是必要的。

Reference

https://scastiel.dev/view-counter-react-server-components
https://nextjs.org/docs/getting-started/react-essentials
https://oldmo860617.medium.com/從-next-js-13-認識-react-server-components-37c2bad96d90

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant