来源: FE研发(上海) 团队 - 葛婷html
客户端渲染(Client-Side Render):客户端渲染,页面初始加载的 HTML 页面中无网页展现内容,须要加载执行 JavaScript 文件中的 React 代码,经过 JavaScript 渲染生成页面,同时,JavaScript 代码会完成页面交互事件的绑定。 服务端渲染:全部数据请求和 html 内容已在服务端处理完成,浏览器收到的是完整的 html 内容,能够更快的看到渲染内容,在服务端完成数据请求确定是要比在浏览器端效率要高的多。前端
CSR 的痛点:node
SSR 的产生,主要就是为了解决上面所说的两个问题。在 React 中使用 SSR 技术,咱们让 React 代码在服务器端先执行一次,使得用户下载的 HTML 已经包含了全部的页面展现内容,这样,页面展现的过程只须要经历一个 HTTP 请求周期,TTFP 时间获得一倍以上的缩减。 同时,因为 HTML 中已经包含了网页的全部内容,因此网页的 SEO 效果也会变的很是好。以后,咱们让 React 代码在客户端再次执行,为 HTML 网页中的内容添加数据及事件的绑定,页面就具有了 React 的各类交互能力。react
所以,最好的方案就是这两种体验和技术的结合,第一次访问页面是服务端渲染,基于第一次访问后续的交互就是 SPA 的效果和体验,还不影响SEO 效果。而要实现两种技术的结合,其核心原理是 同构
webpack
同构这个概念存在于 Vue,React 这些新型的前端框架中,同构其实是客户端渲染和服务器端渲染的一个整合。咱们把页面的展现内容和交互写在一块儿,让代码执行两次。在服务器端执行一次,用于实现服务器端渲染,在客户端再执行一次,用于接管页面交互。web
首先咱们须要两套 webpack 配置,一套用于编译生成
服务端
执行的脚本,一套用于编译生成客户端
的脚本。typescript
服务端路由须要使用 react-router
库的 StaticRouter
,而客户端路由须要使用 BrowserRouter
。浏览器
// 服务端
const sheet = new ServerStyleSheet();
const str = ReactDOMServer.renderToString(
sheet.collectStyles(
<StaticRouter location={req.url} context={{}}>
<MobileSiteAppWrapper>
<Switch>
{routes.map(route=>
<Route
key={route.path}
path={route.path}
render={props=> {
const Comp = route.component;
return <Comp {...props} data={data}/>; // 服务端注入数据
}}
exact={route.exact}
/>)}
</Switch>
</MobileSiteAppWrapper>
</StaticRouter>
)
);
const styles = sheet.getStyleTags();
复制代码
// 客户端
const App = () => (
<BrowserRouter>
<MobileSiteAppWrapper>
<Switch>
{routes.map((route, i) => {
return (
<Route
key={i}
path={route.path}
render={props=> {
const Comp = route.component;
return <Comp {...props} data={getInitData()} />; // 客户端注入数据
}}
exact={route.exact}
/>
);
})}
</Switch>
</MobileSiteAppWrapper>
</BrowserRouter>
);
复制代码
使用了 style-components
库的 ServerStyleSheet
提取样式后,渲染模板时注入 html 文件中bash
以详情页为例,咱们须要在服务端请求接口后,将获得的数据注入组件内,而后在组件的 构造函数
中将数据保存进组件 state 或 store 中。前端框架
组件的生命周期在服务端渲染的过程当中不会执行
ejs.renderFile("server/template.ejs", {
styles,
comp: str,
mainjs: manifestFile["main.js"],
}, (err, data) => {
if (err) {
console.error(err);
} else {
res.end(data);
}
});
复制代码
到此,咱们就已经在服务端渲染出带有数据的、看上去和原先彻底一致的 html 页面了。
可是服务运行起来以后,会发现一个新的问题,当浏览器端的 js 执行完成后,咱们又从新请求了一次数据,而且引起了组件的从新渲染。
这是由于在浏览器端,双端节点对比失败,致使组件从新渲染,也就是只有当服务端和浏览器端渲染的组件具备相同的
props
和 DOM 结构的时候,组件才能只渲染一次。 刚刚咱们在服务器端获取了数据,但也仅仅是服务端有,浏览器端是没有这个数据的,当客户端进行首次组件渲染的时候没有初始化的数据,渲染出的节点确定和服务端直出的节点不一样,致使组件从新渲染。 在服务端将预取的数据注入到浏览器,使浏览器端能够访问到,客户端进行渲染前将数据传入对应的组件便可,这样就保证了props
的一致。
// ejs 模板新增
<textarea style="display: none;" id="ssr-data"><%= data %></textarea>
复制代码
ejs.renderFile("server/template.ejs", {
styles,
comp: str,
mainjs: manifestFile["main.js"],
data: data ? JSON.stringify(data) : "", // 新增
}, (err, data) => {
if (err) {
console.error(err);
} else {
res.end(data);
}
});
复制代码
// 在客户端 js 首次执行时,尝试读取一次服务端注入在 html 中的数据(读取到后移除 dom),而后将数据一样传入组件中
export function getSSRData() {
let INIT_DATA = null;
try {
let stateNode = document.getElementById("ssr-data") as HTMLTextAreaElement;
if (stateNode && stateNode.value) {
INIT_DATA = JSON.parse(stateNode.value);
stateNode.parentNode.removeChild(stateNode);
}
} catch (e) {
console.error(e);
}
return INIT_DATA;
}
复制代码
// 组件内部
constructor (props) {
if (props.data) {
// 直接将数据填入 state 或 store 中
} else {
// 和原先同样请求数据
}
}
复制代码
这样,在浏览器端 js 执行后,页面应该就不会重复请求数据及渲染了。但实际项目应用中时,可能发现并不是如此,仍会出现页面闪烁,这是由于以前项目中使用了按需加载的动态导入进行过优化。
原项目中,采用了按需加载和代码分离进行优化,以下所示:
component: Loadable({
loader: () => import(/* webpackChunkName: "ProductDetail" */"./routes/ProductDetail"),
loading: LoadingComponent
}),
复制代码
浏览器端 js 执行时,会先显示LoadingComponet
,等对应的 chunk js 下载执行完毕后,再渲染出对应页面。所以,咱们须要对 SSR 作额外的处理,在匹配上路由后,先保证对应chunk js preload 完毕,再进行页面的渲染。
// ssr.ts
export async function mountComponent(path = location.pathname) {
let matchedRoute: RouteParams = null;
routes.some(route => {
if (matchPath(path, { path:route.path, exact:route.exact })) {
matchedRoute = route;
return true;
}
return false;
});
if (matchedRoute) {
return matchedRoute.component.preload();
} else {
return Promise.resolve();
}
}
// index.tsx
if (__SSR__) {
const { mountComponent } = require("./ssr");
mountComponent().then(() => {
ReactDOM.hydrate(<App />, document.getElementById("app"));
});
} else {
ReactDOM.render(<App />, document.getElementById("app"));
}
复制代码
参考连接: