csr(客户端渲染) & ssr(服务端渲染) & nsr & esr(边缘渲染)
-
csr:
页面内容的渲染来自于服务端返回的js脚本和ajax请求的数据的执行。csr的出现实现了前后端架构分离。csr的本质就是拿到html后请求打包工具打包生成的js脚本,然后执行该脚本在渲染。缺点:不利于seo和首次渲染时间长
-
Ssr:
在服务端完成页面模板、数据预取、填充,并且在服务端就可以将完整的 HTML 内容返回给浏览器。ssr本质上就是把原来csr的初始化数据这个操作让服务端执行了,然后调用renderToString函数生成html文本,再执行服务生成一个初始的静态页面。缺点: 会增加项目整体复杂度,对服务器压力大
-
Nsr:
这是一种在 hybrid 中特有的渲染技术。简单说就是通过 Native 渲染生成 HTML 数据,并且缓存在客户端。这样一来,对于一个 hybrid WebView 的用户访问,会优先从离线包中加载离线页面模板,再通过前端 Ajax/或客户端能力请求数据,最终完成页面完整的展示
这样做的好处显而易见:我们将服务器的渲染工作放在了一个个独立的移动设备中,并借助离线存储技术,实现了页面的预加载,同时又不会增加额外的服务器压力。
-
Esr
边缘渲染则更加激进。ESR 其实借助了最近几年较火的“边缘计算”能力。
边缘计算: 是指在靠近物或数据源头的一侧,采用网络、计算、存储、应用核心能力为一体的开放平台,就近提供最近端服务。其应用程序在边缘侧发起,产生更快的网络服务响应,满足行业在实时业务、应用智能、安全与隐私保护等方面的基本需求。边缘计算处于物理实体和工业连接之间,或处于物理实体的顶端。而云端计算,仍然可以访问边缘计算的历史数据。
ESR 渲染利用了 CDN 能力。ESR会在 CDN 上缓存页面的静态部分,这样在用户访问页面时,可以快速返回给用户静态内容,同时在 CDN 节点上也发起动态部分内容请求,在动态内容获取之后,利用流的方式,继续返回给用户
手动搭建一个ssr框架
同构
同构:一套react代码,在服务端执行一次,在客户端也执行一次,reactDom.renderToString
将jsx转为html文本的时候,不会处理jsx上面的attrs的事件属性。所以需要在客户端执行ReactDom.hydrate
,把事件和属性生效
那么怎么同构呢? 首先服务端运行react生成一遍html, 然后在html中嵌入打包后的index.js, 把index.js放入静态文件目录,这样就可以通过请求获取到index.js. 这里的index.js内容是reactDom.hydrate重新执行渲染操作。
-
服务端渲染jsx->html,使用
ReactDom.renderToString
生成 -
客户端在运行jsx->html,使用
ReactDom.hydrate
进行客户端的再次渲染
这里客户端的代码写在client,用webpack.client.js打包,打包到src/public目录。 服务端的代码写在server,用webpack.server.js打包,打包到build目录。 流程:服务端运行react渲染出html->发送html到浏览器->浏览器加载js文件->js中的react代码重新执行->js中的react代码接管页面操作。
路由的处理
客户端使用BrowserRouter, 服务端使用StaticRouter。
客户端执行react-router的时候,是可以知道浏览器当前的url的。
然而服务端只是返回的StaticRouter生成的html文本。StaticRouter并不知道当前浏览器访问的路径
所以需要node app应用将自己的context.request.path
传入到StaticRouter
,再渲染出对应的html文本。
hydrate(注水) & 脱水 & 全局context store
同构项目中如何引入context store
主要代码如下:
import {
Store as GlobalStore,
useReduxHook as useGlobalReduxHook,
initStore,
} from "../store/global";
function GlobalProvider({ children }) {
const [state, dispatch] = useGlobalReduxHook();
return (
<GlobalStore.Provider value={{ state, dispatch }}>
{children}
</GlobalStore.Provider>
);
}
服务端renderToString这个函数不会执行组件的生命周期函数。所以客户端的生命周期运行生成的数据,服务端拿不到。那么怎么解决异步数据服务器渲染呢?
目标:服务端根据用户访问的路径不同,加载不同的异步数据。
方法: 改写路由给组件添加静态方法loadData, 调用loadData方法, 将请求的数据更新到 Context store. 这样就可以拿到数据在renderString渲染了
主要代码如下:
// 组件异步请求数据 const Home = (props) => { const { dispatch: globalDispatch, state: globalState } = useContext(GlobalStore); const { weatherMessage } = globalState; return <div /> } Home.loadData = (Store) => { return serverAxios .get("/api/homedata") .then((response) => { Store.weatherMessage = response.data.weatherMessage; }) .catch((err) => { console.log("error", err); }); }; // 路由执行组件的异步处理函数loadData import { matchRoutes } from "react-router-config"; router.get("/(.*)", async (context, next) => { // 根据路由的路径,来往store里面加数据 const promises = []; const matchedRoutes = matchRoutes(Routes, context.request.path); // 让matchRoutes里面的所有组件对应的loadData方法执行一次 // 使得数据提前加载好在渲染。 matchedRoutes.forEach((item) => { if (item.route.loadData) { const promise = new Promise((resolve, reject) => { return item.route .loadData(initStore) .then(resolve()) .catch(resolve()); }); promises.push(promise); } }); Promise.all(promises).then(() => { const ctx = { css: [], }; const html = render(context.request, Routes, ctx); if (ctx.notFound) { context.response.status = 404; } // staticRouter的Redirect组件被调用 // ctx增加了action,url属性 if (ctx.action === "REPLACE") { context.response.redirect(301, ctx.url); } context.response.body = html; }); await next(); });
虽然服务端渲染执行loadData拿到了数据,但是客户端渲染的时候,初始store并没有数据,那么怎么保证组件在客户端和服务端的初始返回渲染文本一致呢?这就需要注水与出水操作。
注水:将服务端渲染的数据放到script中的window.context 出水:客户端的store初始数据从window.context里面拿
另外客户端在执行渲染的时候,由于history.go, link等方法都是不会通过本地的BrowserRouter根据路由的变化返回响应的组件,这个时候组件的渲染不是由服务器执行loadData返回的,客户端出水操作拿到的初始store的是空值。所以需要客户端在生命周期函数中也执行数据的请求在渲染。
404页面的编写和301重定向
多级路由下,没有配置的路径显示404组件 服务端的staticRouter的context对象添加一个NotFound属性作为标识
react-router的Redirect组件调用的时候,服务端的staticRouter的context对象会增加一个action属性。
if (ctx.action === "REPLACE") {
context.response.redirect(301, ctx.url);
}
样式的处理
服务端不能用style-loader, css-loader. 因为在nodeJs中window是不存在的。
那么在组件使用css需要怎么解决呢,这里使用isomorphic-style-loader
插件
注意:
- webpack配置isomorphic-style-loader的时候,必须使用CommonJs模块语法,必须写上esModule:false
- isomorphic-style-loader这个插件不会将css文案写入到html中。它只会生成className. 所以需要自己手动引入
use: ['isomorphic-style-loader', {loader: 'css-loader', options: {esModule: false}}]
样式的处理主要代码如下:
import styles from './style.css'
// props.staticContext 是有staticRouter的context属性注入的
if(props.staticContext) {
props.staticContext.css.push(styles._getCss())
}
seo
使用react-helmet插件, 会在head头部注入: 和
主要代码如下:
// 组件中写入:
<Helment>
<title />
<meta name="description" content="" />
</Helment>
// 渲染时
const helmet = Helmet.renderStatic()
${helmet.title.toString()}
${helmet.meta.toString()}
react ssr 搭建代码
- scss的使用
- 关于context中notFound,action属性,打印一下context