React服务端渲染入门

821 阅读6分钟

前言

在最近两年的React生态圈中,Next.js脱颖而出,备受好评和关注的情况下,学习服务端渲染相关技术也逐步成为前端工程师必备技能。

本文将由浅入深,介绍React SSR的基本概念和原理,带你入门服务端渲染~

*注:本文完整 Demo可在 github查看 ->shuangmianxiaoQ | react-ssr

基础概念

客户端渲染

在传统的React单页应用中,服务器返回一个HTML模板,页面内容和交互都是通过js渲染出来的,我们称之为Client Side Render

image.png

服务端渲染

在服务器上完成HTML页面内容的生成,并直接展示到浏览器,这个过程称之为Server-Side Rendering

image.png

服务端渲染的优势

  1. 更快的首屏渲染,减少白屏时间:不需要等待所有的JS下载并执行才显示页面,而是由服务器端生成HTML,浏览器解析后即可展示页面

image.png

  1. 利于SEO:搜索引擎的爬虫一般会优先爬取HTML内容,对于客户端渲染的网站很不友好,而服务器端则可以发挥此优势获得更好的搜索排名

React SSR

renderToString

ReactDOMServer.renderToString(element)

虚拟DOM本质上是JS对象,所以可以通过renderToString方法可以将React元素转换为HTML字符串,并提供给服务端使用。

const app = express();
const content = renderToString(<App />);

app.get('/', (req, res) => {
  res.send(`
    <html>
    <head>
      <title>SSR</title>
    </head>
    <body>${content}</body>
    </html>
  `);
});

const server = app.listen(3000);

同构

通过renderToString方法完成了服务端渲染的第一步,但是该方法不会去渲染事件点击等交互行为,这时候就需要通过客户端来完成了。

所谓同构,就是一套React代码,在服务器端执行一次渲染页面内容,客户端再执行一次处理交互。

ReactDOM.hydrate(<App />, document.getElementById('root'));

注意客户端渲染时,不再使用render方法,而是hydrate方法,叫做“注水”,它的作用是让客户端尽量复用服务端渲染的内容,只对事件绑定等客户端特有内容进行补充。

路由

React Router在客户端一般会使用BrowserRouterHashRouter,但在服务端渲染使用中使用的叫做StaticRouter

// location用于服务器路由匹配,context用于数据传递
renderToString(
  <StaticRouter location={req.path} context={{}}>
    <App />
  </StaticRouter>
);

数据获取

客户端

客户端获取数据,一般是在组件挂载(componentDidMountuseEffect)时发送请求,获取到数据后更新组件状态。

componentDidMountuseEffect方法都不会在服务端去执行,所以渲染页面的HTML中也不会有数据生成,那么服务端怎么获取数据呢?

服务端

React-Router提供了一种loadData的方法来实现数据获取:server-rendering | data-loading,具体实现如下:

  1. 为组件添加loadData静态方法
Home.loadData = () => {
  // 服务端渲染之前,把当前路由需要的数据提前加载好
};
  1. 将路由改为配置的方式
const routes = [
  {
    path: "/",
    component: Home,
    loadData: Home.loadData
  }
  // etc.
];
  1. 服务端使用matchPath方法来匹配路由,并执行相应的loadData方法
import { matchPath } from "react-router-dom";

// inside a request
const promises = [];
// use `some` to imitate `<Switch>` behavior of selecting only
// the first to match
routes.some(route => {
  // use `matchPath` here
  const match = matchPath(req.path, route);
  if (match) promises.push(route.loadData(match));
  return match;
});

Promise.all(promises).then(data => {
  // do something w/ the data so the client
  // can access it then render the app
});

如果需要多级路由处理,推荐使用 react-router-config 来处理路由匹配

数据脱水与注水

当服务器端获取到数据并通过loadData方法加载之后,发现页面会有闪动,这是怎么回事呢?

chrome-capture-2022-4-21.gif

服务端渲染没有问题,问题出在客户端也获取了一次数据并进行渲染,这里需要对数据服务端注水,客户端脱水来解决。

  1. 服务端数据注水
`
<body>
  <div id="root">${content}</div>
  <script>
    // 将 Redux 状态注入 context
    window.context = {
      state: ${JSON.stringify(store.getState())}
    }
  </script>
  <script src="/index.js"></script>
</body>
`
  1. 客户端数据脱水
export const getClientStore = () => {
  // 客户端使用 Redux状态时,将 context中的数据设置为初始状态
  const preloadedState = window.context.state;
  return createStore(reducer, preloadedState, applyMiddleware(thunk));
};
const Home = ({ list, getHomeList }) => {
  useEffect(() => {
    // list内容为空(即非首次服务端渲染)时,再发送请求
    if (!list.length) {
      getHomeList();
    }
  }, []);
  ...
};

错误处理

404 Not Found

当访问到不存在的路由时,一般会返回404页,但服务端返回的HTML状态码仍然是200,那么该如何处理404状态呢?

前面使用StaticRouter时,有个context属性用于数据传递,错误处理正是通过它来解决:server-rendering | adding-app-specific-context-information

const context = {};

const content = renderToString(
  <StaticRouter location={req.path} context={context}>
    <App />
  </StaticRouter>
);

if (context.status === 404) {
  res.status(404);
}

res.send(html);
const NotFound = ({ staticContext }) => {
  // 只有服务端渲染时才有 staticContext
  if (staticContext) {
    staticContext.status = 404;
  }
  
  return (
    <h3>Sorry, can’t find that.</h3>
  );
};

301 Redirect

客户端使用Redirect组件进行路由重定向,可以参考404处理的方案封装一个RedirectWithStatus组件来处理。

使用react-router-config的话,会在重定向的时候在context中添加相关属性,可直接用于判断并重定向:

// react-router-config添加的属性
{ action: 'REPLACE', location: xxx, url: xx }
if (context.action === 'REPLACE') {
  res.redirect(301, context.url);
}

SEO

React Helmetreact-helmet是一个HTML文档head管理工具,可以很轻松的设置titledescriptionmeta等标签,以便搜索引擎和爬虫读取。

// 客户端使用 Helmet 组件
const Home = () => {
  return (
    <div>
      <Helmet>
        <title>SSR Home</title>
        <meta name="description" content="SSR Home Page" />
      </Helmet>
      ...
    </div>
  );
};
// 服务端使用 renderStatic 生成标记
const helmet = Helmet.renderStatic();

`
<head>
  ${helmet.title.toString()}
  ${helmet.meta.toString()}
</head>
`

Next.js

简介

Next.js是一个用于生产环境的轻量级React框架,提供了生产环境所需的所有功能以及最佳的开发体验:包括SSG & SSR、支持TypeScript、代码拆分打包、 基于文件系统的路由等功能 无需任何配置。

去年Next.js 12发布,采用了Rust编译器(SWC,使用Rust编写的高性能转移器,类似于babel),极大的提升了构建速度,同时还专门把SWC的作者和Parcel的核心贡献者挖了过去,因此关注度也持续居高不下。

使用

快速入门Next.js的方式就是学习官方文档的入门教程:Introduction | Learn Next.js,不得不说这个教程是技术文档里体验最友好的,一步步带你完成一个博客项目,还有答题得分环节,下面我总结下一些主要的功能及特性。

基于文件系统的路由

Next.js中,页面路由是与文件名关联的,举个例子:

  • pages/index.js映射的是/根路由;
  • pages/posts/first-post.js映射的是posts/first-post
  • pages/posts/[id].js映射的是/posts/<id>动态路由,即带参数的路由。

路由间的跳转,使用Next.js提供的Link组件:

import Link from 'next/link';

// render jsx
<Link href="/">
  <a>Back to home</a>
</Link>

资源处理

  1. 图片:Next.js提供了开箱即用的Image组件,支持对图片的优化,如占位符,懒加载,webp等优化
import Image from 'next/image';

const YourComponent = () => (
  <Image src="/images/profile.jpg" width={144} height={144} alt="Name" />
);
  1. 第三方JSNext.jsscript标签做了优化,加载第三方js使用Script组件
import Script from 'next/script';

<Script
  src="https://connect.facebook.net/en_US/sdk.js"
  strategy="lazyOnload"
  onLoad={() =>
    console.log(`script loaded correctly, window.FB has been populated`)
  }
/>
  1. 样式支持:Next.js内置支持css modulesasscss in js等方案
  • css module:使用xxx.module.css启用
  • sass:安装sass即可使用
  • css in js:内置支持styled-jsx,也可以使用styled-componentsemotion等其他css in js的库
<style jsx>{`
  …
`}</style>
  1. 全局样式:pages文件夹下创建_app.js文件,该文件中有一个App组件代表顶级组件,类似React应用中的入口文件,会在所有页面生效
import '../styles/global.css';

export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />;
}
  1. 元数据:用于SEO的一些标签,Next.js中用Head包裹即可为每个页面动态配置
import Head from 'next/head';

<Head>
  <title>Create Next App</title>
  <link rel="icon" href="/favicon.ico" />
  <meta name="description" content="Learn how to build a personal website using Next.js" />
</Head>

预渲染

普通React应用要想实现SEO和首屏渲染,还有种简单的方式,就是预渲染Pre-rendering。预渲染不需要后端服务器动态编译HTML,而是直接在构建时生成静态HTML,数据不一定是最新的,适用于静态站点生成。一般会使用prerender-spa-pluginwebpack中配置来实现预渲染。

Next.js中预渲染呈现为Static GenerationServer-side Rendering两种形式:

  1. Static Generation:通常叫做SSG,静态站点生成器,常用于数据不会频繁更新的静态站点,如静态营销网站,博客系统,文档系统等。搭过博客的话,肯定听过Jekyll, Hexo, Hugo, Gatsby, Nuxt, VuePress这些框架,都是SSG领域的

image.png

  1. Server-side Rendering:服务端渲染,如果页面数据需要请求更新,那么应选择SSR,比如营销官网等

image.png

Next.js中允许不同页面选择不同的渲染,比如 A页面使用SSR,B页面使用SSG,C页面又使用SSR,主要是通过两个不同的函数(getStaticPropsgetServerSideProps)来实现。

数据获取

  1. getStaticProps -> SSG
export async function getStaticProps() {
  // Get external data from the file system, API, DB, etc.
  const data = ...

  // The value of the `props` key will be
  //  passed to the `Home` component
  return {
    props: ...
  }
}
  1. getServerSideProps -> SSR
export async function getServerSideProps(context) {
  return {
    props: {
      // props for your component
    },
  };
}

数据请求推荐下Next.js自家的SWR,屏幕聚焦请求、缓存、轮询等功能时真香,可以说是Hooks出现以来最具潜力的库,ahooksuseRequest也是基于它做的

动态路由

前面提到动态路由通过文件名如[id].js来生成,那么路由中的参数如何获取呢?答案是getStaticPaths

export async function getAllPostIds() {
  // fetch post data from an external API endpoint
  const res = await fetch('..');
  const posts = await res.json();
  return posts.map((post) => ({
    params: {
      id: post.id,
    }
  }));
}

export async function getStaticPaths() {
  const paths = getAllPostIds();

  return {
    paths,
    // If fallback is false, then any paths not returned by getStaticPaths will result in a 404 page.
    fallback: false,
  };
}

fallbackfalse代表如果路由参数不在params中则会到404页面

API路由

Next.js中创建API端点也很简单,直接在pages/api目录下创建如下格式的函数即可:

// req = HTTP incoming message, res = HTTP server response
export default function handler(req, res) {
  // ...
}

部署

最后,讲一下部署Next.js应用到Vercel,也是自家产品,提供免费的网站托管服务,githubcodesandbox都有集成,使用起来也很简单。

  1. 使用github登录后Vercel选择要部署的仓库导入

image.png

  1. 确认配置信息后,点击部署即可

image.png

  1. 部署完成之后,便可以将你的应用分享出来:Next.js Bolg

如果要了解更多Vercel的功能,比如构建配置,代码检查等,可自行研究。

总结

从客户端渲染到服务端渲染,方案也逐渐成熟,不管是React还是Vue,本质上的概念和原理都是相同的,理解了这些背后的逻辑之后,再去使用框架都会容易很多。

如果在日常工作或是学习中有用到SSR技术,都可以去实践下,学以致用~