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 作爲服務端須要一整套解決方案,從開發調試到部署監控,現階段出問題沒有保障

參考連接:

相關文章
相關標籤/搜索