SSR介绍 + NodeJS服务端渲染调研报告

来源: FE研发(上海) 团队 - 葛婷html

1、 服务端渲染(Server-Side Render)介绍

什么是SSR?为何要用SSR?

客户端渲染(Client-Side Render):客户端渲染,页面初始加载的 HTML 页面中无网页展现内容,须要加载执行 JavaScript 文件中的 React 代码,经过 JavaScript 渲染生成页面,同时,JavaScript 代码会完成页面交互事件的绑定。 服务端渲染:全部数据请求和 html 内容已在服务端处理完成,浏览器收到的是完整的 html 内容,能够更快的看到渲染内容,在服务端完成数据请求确定是要比在浏览器端效率要高的多。前端

SSR + SPA

CSR 的痛点:node

  1. CSR 项目的 TTFP(Time To First Page)时间比较长,在 CSR 的页面渲染流程中,首先要加载 HTML 文件,以后要下载页面所需的 JavaScript 文件,而后 JavaScript 文件渲染生成页面。在这个渲染过程当中至少涉及到两个 HTTP 请求周期,因此会有必定的耗时,这也是为何你们在低网速下访问普通的 React 或者 Vue 应用时,初始页面会有出现白屏的缘由。
  2. CSR 项目的 SEO 能力极弱,在搜索引擎中基本上不可能有好的排名。由于目前大多数搜索引擎主要识别的内容仍是 HTML,对 JavaScript 文件内容的识别都还比较弱。若是一个项目的流量入口来自于搜索引擎,这个时候你使用 CSR 进行开发,就很是不合适了。

SSR 的产生,主要就是为了解决上面所说的两个问题。在 React 中使用 SSR 技术,咱们让 React 代码在服务器端先执行一次,使得用户下载的 HTML 已经包含了全部的页面展现内容,这样,页面展现的过程只须要经历一个 HTTP 请求周期,TTFP 时间获得一倍以上的缩减。 同时,因为 HTML 中已经包含了网页的全部内容,因此网页的 SEO 效果也会变的很是好。以后,咱们让 React 代码在客户端再次执行,为 HTML 网页中的内容添加数据及事件的绑定,页面就具有了 React 的各类交互能力。react

所以,最好的方案就是这两种体验和技术的结合,第一次访问页面是服务端渲染,基于第一次访问后续的交互就是 SPA 的效果和体验,还不影响SEO 效果。而要实现两种技术的结合,其核心原理是 同构webpack

什么是同构?

同构这个概念存在于 Vue,React 这些新型的前端框架中,同构其实是客户端渲染和服务器端渲染的一个整合。咱们把页面的展现内容和交互写在一块儿,让代码执行两次。在服务器端执行一次,用于实现服务器端渲染,在客户端再执行一次,用于接管页面交互。web

2、实现原理

首先咱们须要两套 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>
);
复制代码

注入CSS

使用了 style-components 库的 ServerStyleSheet 提取样式后,渲染模板时注入 html 文件中bash

服务端获取数据

以详情页为例,咱们须要在服务端请求接口后,将获得的数据注入组件内,而后在组件的 构造函数 中将数据保存进组件 state 或 store 中。前端框架

组件的生命周期在服务端渲染的过程当中不会执行

最终返回完整的 html

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"));
}
复制代码

3、总结

好处:

  • 优化用户体验,解决首屏白屏的问题,用户可以更快的看到实际页面内容
  • 利于SEO

SSR 改造面临的问题:

  • 现前端项目中,存在大量直接使用浏览器端全局变量的代码(例如: window.location / window.document 等),没法直接用于服务端渲染,必须改造
  • 须要作 SSR 的页面,都须要分别对相应组件的数据获取相关逻辑作改造,以上都须要不小的成本
  • 引入 SSR 后,前端开发时须要考虑兼容双端逻辑,开发成本提升

NodeJS 做为服务端的问题:

  • nodejs 作 React 的 SSR 很好作,但公司没有相应技术栈,使用 nodejs 作为服务端须要一整套解决方案,从开发调试到部署监控,现阶段出问题没有保障

参考连接:

相关文章
相关标签/搜索