前言
在最近两年的
React生态圈中,Next.js脱颖而出,备受好评和关注的情况下,学习服务端渲染相关技术也逐步成为前端工程师必备技能。
本文将由浅入深,介绍React SSR的基本概念和原理,带你入门服务端渲染~
*注:本文完整 Demo可在 github查看 ->shuangmianxiaoQ | react-ssr
基础概念
客户端渲染
在传统的React单页应用中,服务器返回一个HTML模板,页面内容和交互都是通过js渲染出来的,我们称之为Client Side Render
服务端渲染
在服务器上完成HTML页面内容的生成,并直接展示到浏览器,这个过程称之为Server-Side Rendering
服务端渲染的优势
- 更快的首屏渲染,减少白屏时间:不需要等待所有的
JS下载并执行才显示页面,而是由服务器端生成HTML,浏览器解析后即可展示页面
- 利于
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在客户端一般会使用BrowserRouter和HashRouter,但在服务端渲染使用中使用的叫做StaticRouter
// location用于服务器路由匹配,context用于数据传递
renderToString(
<StaticRouter location={req.path} context={{}}>
<App />
</StaticRouter>
);
数据获取
客户端
客户端获取数据,一般是在组件挂载(componentDidMount或useEffect)时发送请求,获取到数据后更新组件状态。
componentDidMount或useEffect方法都不会在服务端去执行,所以渲染页面的HTML中也不会有数据生成,那么服务端怎么获取数据呢?
服务端
React-Router提供了一种loadData的方法来实现数据获取:server-rendering | data-loading,具体实现如下:
- 为组件添加
loadData静态方法
Home.loadData = () => {
// 服务端渲染之前,把当前路由需要的数据提前加载好
};
- 将路由改为配置的方式
const routes = [
{
path: "/",
component: Home,
loadData: Home.loadData
}
// etc.
];
- 服务端使用
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方法加载之后,发现页面会有闪动,这是怎么回事呢?
服务端渲染没有问题,问题出在客户端也获取了一次数据并进行渲染,这里需要对数据服务端注水,客户端脱水来解决。
- 服务端数据注水
`
<body>
<div id="root">${content}</div>
<script>
// 将 Redux 状态注入 context
window.context = {
state: ${JSON.stringify(store.getState())}
}
</script>
<script src="/index.js"></script>
</body>
`
- 客户端数据脱水
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 Helmet,react-helmet是一个HTML文档head管理工具,可以很轻松的设置title,description,meta等标签,以便搜索引擎和爬虫读取。
// 客户端使用 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>
资源处理
- 图片:
Next.js提供了开箱即用的Image组件,支持对图片的优化,如占位符,懒加载,webp等优化
import Image from 'next/image';
const YourComponent = () => (
<Image src="/images/profile.jpg" width={144} height={144} alt="Name" />
);
- 第三方
JS:Next.js对script标签做了优化,加载第三方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`)
}
/>
- 样式支持:
Next.js内置支持css module,sass,css in js等方案
css module:使用xxx.module.css启用sass:安装sass即可使用css in js:内置支持styled-jsx,也可以使用styled-components,emotion等其他css in js的库
<style jsx>{`
…
`}</style>
tailwind:可查看tailwind官网配置或Next.js提供的 Demo:next-with-tailwindcss
- 全局样式:
pages文件夹下创建_app.js文件,该文件中有一个App组件代表顶级组件,类似React应用中的入口文件,会在所有页面生效
import '../styles/global.css';
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />;
}
- 元数据:用于
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-plugin在webpack中配置来实现预渲染。
在Next.js中预渲染呈现为Static Generation和Server-side Rendering两种形式:
Static Generation:通常叫做SSG,静态站点生成器,常用于数据不会频繁更新的静态站点,如静态营销网站,博客系统,文档系统等。搭过博客的话,肯定听过Jekyll,Hexo,Hugo,Gatsby,Nuxt,VuePress这些框架,都是SSG领域的
Server-side Rendering:服务端渲染,如果页面数据需要请求更新,那么应选择SSR,比如营销官网等
在Next.js中允许不同页面选择不同的渲染,比如 A页面使用SSR,B页面使用SSG,C页面又使用SSR,主要是通过两个不同的函数(getStaticProps和getServerSideProps)来实现。
数据获取
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: ...
}
}
getServerSideProps->SSR
export async function getServerSideProps(context) {
return {
props: {
// props for your component
},
};
}
数据请求推荐下Next.js自家的SWR,屏幕聚焦请求、缓存、轮询等功能时真香,可以说是Hooks出现以来最具潜力的库,ahooks的useRequest也是基于它做的
动态路由
前面提到动态路由通过文件名如[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,
};
}
fallback为false代表如果路由参数不在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,也是自家产品,提供免费的网站托管服务,github,codesandbox都有集成,使用起来也很简单。
- 使用
github登录后Vercel选择要部署的仓库导入
- 确认配置信息后,点击部署即可
- 部署完成之后,便可以将你的应用分享出来:Next.js Bolg
如果要了解更多Vercel的功能,比如构建配置,代码检查等,可自行研究。
总结
从客户端渲染到服务端渲染,方案也逐渐成熟,不管是React还是Vue,本质上的概念和原理都是相同的,理解了这些背后的逻辑之后,再去使用框架都会容易很多。
如果在日常工作或是学习中有用到SSR技术,都可以去实践下,学以致用~