为何使用
传统SSR
SSR 优缺点
服务端直出HTML
会让首屏较快展现,且利于SEO
。但所有页面的加载都需向服务端请求,如果访问量较大,会对服务器造成压力。此外,页面之间的跳转,页面局部内容的变动都会引起页面刷新,体验不够友好。
CSR
CSR(SPA) 优缺点
只有首次进入或刷新时需要请求服务器,页面之间的跳转由JS
脚本完成,响应较快。但由于服务端只返回一个空节点的HTML
,页面内容的呈现需等待JS
脚本加载执行完毕,首屏时间较长,对SEO
也不友好。
React SSR
相比于客户端渲染SPA
应用
由于首次进入或刷新页面时,服务端直接将有内容的页面返回给客户端,大大降低了白屏时间,同样也便于做SEO
相比于传统的SSR应用
不必跳转到不同页面都需要刷新一次浏览器,只在第一次访问的时候服务端直出HTML
,后续的页面跳转走CSR
(客户端渲染)
如何使用
组件同构
服务端
对于一个React
应用,想在首次进入或刷新页面服务端就直接返回完整的HTML
,需要在服务端将当前页面需渲染的组件转换成HTML
,React为此提供了相应方法。
import { renderToString } from 'react-dom/server'
const html = renderToString(
<App />
)
除renderToString
方法外,Reeact
也提供了ReactDom.renderToNodeStream
方法,返回一个可输出HTML
字符串的可读流。
客户端
虽然服务端已经直出了需要渲染的HTML
,但一些事件绑定的操作还是需要客户端JS
脚本来完成。如果客户端依旧执行ReactDOM.render
方法,会在首次调用时将容器节点下的所有DOM元素替换,这显然不是我们想要的结果,好在React提供了ReactDom.hydrate
方法。
import ReactDom from 'react-dom'
ReactDom.hydrate(
<App />,
document.getElementById('root')
)
在客户端渲染时,hydrate
方法会比较双端渲染结果是否一致,如一致则保留服务端渲染的结果,如不一致则使用客户端渲染的结果。
路由同构
SPA
应用的路由完全由客户端控制,跳转到不同路径时JS
脚本会替换组件使之呈现出不同内容。
而对React SSR
应用来说,只要用户首次进入或刷新页面时服务端能渲染正确的组件即可,后续的路由切换则完全由客户端JS
脚本控制。react-router
提供StaticRouter
组件可根据当前请求路径匹配渲染不同组件。
import { StaticRouter } from 'react-router'
// path为请求路径
<StaticRouter location={path}>
<App />
</StaticRouter>
数据同构
有时需要在服务端请求数据,直接渲染出带数据的HTML
页面。而由于客户端无此数据,渲染内容就会和服务端不一致。那如何将服务端请求的数据“注入”客户端呢?
数据注水
服务端可以控制直出的HTML
内容,既然如此就可以在直出的HTML内容中插入一段脚本。next.js
便是如此实现的。
数据脱水
服务端已将数据写入script
标签中,客户端渲染时便可直接用该数据进行渲染。
SEO TDK支持
如果只是要做SEO
支持,可以全部放在服务端,根据不同页面路径直出不同的TDK
数据。
但为保证体验和提高可维护性,最好是能将TDK写在页面组件里。这需要服务端渲染时能获取到当前页面的TDK数据,直出到HTML
,客户端渲染时能够比较TDK数据,做DOM
操作用新页面的TDK
数据替换掉老页面的。
react-helmet
对此提供了较好的支持
服务端示例(Koa)
import React from 'react'
import { renderToString } from 'react-dom/server'
import { Helmet } from 'react-helmet'
export default async (ctx, next) => {
const html = renderToString(
<App />
)
const helmet = Helmet.renderStatic()
ctx.body = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
${helmet.title.toString()}
${helmet.meta.toString()}
</head>
<body>
<div id="root">${html}</div>
</body>
</html>
`
return next();
}
客户端示例
import { Helmet } from 'react-helmet'
import tempData from './data'
const Index = () => {
return (
<>
<Helmet>
<title>index title</title>
<meta name="description" content="index description"></meta>
<meta name="keyword" content="index keyword"></meta>
</Helmet>
<div>Index</div>
</>
)
}
export default Index
CSS同构
目前next.js
和egg-react-ssr
都是将css
代码最终打包到一个文件内作为资源进行加载。
按这种方式,服务端直出的HTML结果应包含css
文件的link
标签,而客户端JS
脚本无需插入link
标签。
客户端
客户端使用mini-css-extract-plugin
插件提取css
文件,如果使用了按需加载还需将所有css
提取为单个文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
type: 'css/mini-extract',
// For webpack@4
// test: /\.css$/,
chunks: 'all',
enforce: true,
},
},
},
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
],
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
};
在打包时一般静态资源文件名会带上hash
值,这时为保证服务端能获取到正确的路径还需使用webpack-manifest-plugin
插件生成文件名和路径的映射文件,方便服务端获取正确的路径。
如果使用react-loadable
做按需加载,则可使用其提供的react-loadable-ssr-addon
插件生成映射文件。
服务端
如果没有启用css
模块化,客户端打包时已将用到的css
打包到了一个文件,服务端就无需处理css
文件。
webpack中可使用ignore-loader
忽略css
文件的处理
module.exports = {
// other configurations
module: {
loaders: [
{ test: /\.css$/, loader: 'ignore-loader' }
]
}
};
如果启用了css
模块化,css-loader
会生成标识符映射,服务端也需要生成标识符以保证双端渲染结果一致。css-loader
的module.exportOnlyLocals
选项提供了仅导出标识符映射,而不签入css
的功能
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
loader: "css-loader",
options: {
modules: {
exportOnlyLocals: true,
},
},
},
],
},
};
性能优化
按需加载(代码分割)
对一个较大项目来说,我们访问其中的一个页面时,只需加载当前页面的代码,其他页面的代码只要在访问的时候再加载即可。这可以有效减少访问一个页面时需加载的js
文件体积,提高页面响应速度。
在一个SPA
应用中实现按需加载,只需使用dynamic import
语法,webpack
支持该特性。使用动态import语法导入的模块会被单独打包到一个文件。
React SSR实现按需加载的一些坑
- 服务端渲染组件时,无法将按需加载的组件渲染成
HTML
- 即使服务端能够直出按需加载组件的
HTML
,客户端接管后由于异步JS
代码尚未加载,会先展示中间状态(一般中间状态会先渲染loading),这样双端的初次渲染结果就不一致了 - 既然采取了服务端渲染,在服务端渲染时直出的HTML最后能包含当前页面需异步加载的
JS
代码,即script
标签,无需客户端动态创建
针对第一点,其实服务端无需按需加载,应直接渲染出按需加载的组件;
第二点,为保证双端初次渲染结果一致,客户端应该等待当前页面按需加载的异步JS
代码下载后再进行渲染;
第三点,为使服务端渲染的HTML
能够包含按需加载的JS
代码的script
,需要获取到当前页面按需加载的组件名,和组件名对应JS
路径名的映射。在直出HTML时将当前页面按需加载的script
标签拼接进去。
react-loadable提供了上述问题的解决方案
react-loadable实现
组件
按需加载的组件使用Loadable
包裹,其中modules
和webpack
选项标识组件加载的是哪个模块,这样服务端就能根据渲染的组件获取到需加载的模块。
如果使用babel
插件react-loadable/babel
,便无需使用modules
和webpack
选项。
import Loadable from 'react-loadable';
Loadable({
loader: () => import('./Bar'),
modules: ['./Bar'],
webpack: () => [require.resolveWeak('./Bar')],
});
客户端
使用preloadReady
方法,等待按需加载的script
脚本加载完毕后再渲染。window.main
方法将在服务端直出的script
脚本加载后调用。
import Loadable from 'react-loadable'
window.main = () => {
Loadable.preloadReady().then(() => {
ReactDom.render(
<App />
document.getElementById('root')
)
})
}
生成模块映射(webpack)
生成加载的模块与webpack
打包后的bundles
的映射,服务端可据此判断应直出的scirpt
const ReactLoadableSSRAddon = require('react-loadable-ssr-addon');
module.exports = {
entry: {
// ...
},
output: {
// ...
},
module: {
// ...
},
plugins: [
new ReactLoadableSSRAddon({
filename: 'react-loadable.json',
}),
],
};
服务端(Koa)
将渲染的组件用Loadable.Capture
包裹,它提供一个回调report
方法,可以记录当前页面按需加载的模块名,根据生成的模块映射可获取到webpack
打包后的bundles
,由此直出当前页面需按需加载的scirpt
,无需客户端动态创建。
mport React from 'react';
import { renderToString } from 'react-dom/server';
import Loadable from 'react-loadable'
import { getBundles } from 'react-loadable-ssr-addon';
import manifest from '@dist/server/react-loadable.json';
export default async (ctx, next) => {
const modules = new Set();
const html = renderToString(
<Loadable.Capture report={moduleName => {
modules.add(moduleName)
}}>
<App />
</Loadable.Capture>
);
const modulesToBeLoaded = [...manifest.entrypoints, ...Array.from(modules)];
const bundles = getBundles(manifest, modulesToBeLoaded);
ctx.body = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
${bundle.css.join('\n')}
</head>
<body>
<div id="root">${html}</div>
</body>
</html>
${assets.js.join('\n')}
<script type="text/javascript">
window.main()
</script>
`;
return next();
}
参考文档
- next.js官方网站
- webpack官方网站
- React SSR 服务端渲染原理解析与同构实践
- 从头开始,彻底理解服务端渲染原理(8千字汇总长文)
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!