來源: 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"));
}
複製代碼
參考連接: