Hey Guys, 以前寫過一篇React + Koa 服務端渲染SSR的文章,都是大半年前的事了🤣,最近回顧了一下,發現有些以前主流的懶加載組件的庫已通過時了,而後關於SSR彷佛以前的文章沒有涉及到React-v16的功能,特別是v16新加的stream API,只是在上一篇文章的末尾提了一下,因此在這篇Part 2的版本中會添加這些新功能🍺javascript
Why use [Part II]?: Go to play
The Last of Us
and wait forThe Last of Us Part II
🚸html
react-loadable
,使用loadable-componentsloadable-components
來實現瀏覽器端和服務端的異步組件功能react-loadable已經很久沒維護了,並且跟最新的webpack4+,還有babel7+都不兼容,還會有Deprecation Warning,若是你使用koa-web-kitv2.8及以前的版本的話,webpack build的時候會出現warning,並且可能還有一些潛在未知的坑在裏面,因此咱們第一件要作的事就是把它替換成別的庫,並且要跟最新的React.lazy|React Suspense
這類API完美兼容,loadable-components
是個官方推薦的庫, 若是咱們既想在客戶端懶加載組件,又想實現SSR的話(React.lazy
暫不支持SSR).java
首先咱們安裝須要的庫:node
# For `dependencies`:
npm i @loadable/component @loadable/server
# For `devDependencies`:
npm i -D @loadable/babel-plugin @loadable/webpack-plugin
複製代碼
而後你能夠在對應的webpack配置文件及babel配置文件裏把react-loadable/webpack
和 react-loadable/babel
移除掉,替換成@loadable/webpack-plugin
和 @loadable/babel-plugin
。 而後下一步咱們須要對咱們的懶加載的組件作一些修改。react
在一個須要懶加載 React 組件的地方:webpack
// import Loadable from 'react-loadable';
import loadable from '@loadable/component';
const Loading = <h3>Loading...</h3>;
const HelloAsyncLoadable = loadable(
() => import('components/Hello'),
{ fallback: Loading, }
);
//簡單使用
export default MyComponent() {
return (
<div> <HelloAsyncLoadable /> </div>
)
}
//配合 react-router 使用
export default MyComponent() {
return (
<Router>
<Route path="/hello" render={props => <HelloAsyncLoadable {...props}/>}/>
</Router>
)
}
複製代碼
其實跟以前react-loadable的使用方式差很少,傳一個callback進去,返回動態import,也能夠選擇性的傳入loading時須要顯示的組件。git
而後咱們須要在入口文件中hydrate
服務端渲染出來的內容,在src/index.js
:github
import React from 'react';
import ReactDOM from 'react-dom';
import { loadableReady } from '@loadable/component';
import App from './App';
loadableReady(() => {
ReactDOM.hydrate(
<App />, document.getElementById('app') ); }); 複製代碼
OK, 上面這個基本就是客戶端須要作的修改,下一步咱們須要對服務端的代碼作修改,來使得loadable-components能完美的運行在SSR的環境中。web
在以前使用react-loadable的時候,咱們須要在服務端調用Loadable.preloadAll()
來預先加載全部異步的組件,由於在服務端不必實時異步加載組件,初始化的時候就能夠所有加載進來,可是在使用loadable-components的時候已經不須要了,因此直接刪掉這個方法的調用。而後在咱們的服務端的webpack入口文件中:數據庫
import path from 'path';
import { StaticRouter } from 'react-router-dom';
import ReactDOMServer from 'react-dom/server';
import { ChunkExtractor } from '@loadable/server';
import AppRoutes from 'src/AppRoutes';
//...可能還一下其餘的庫
function render(url, initialData = {}) {
const extractor = new ChunkExtractor({ statsFile: path.resolve('../dist/loadable-stats.json') });
const jsx = extractor.collectChunks(
<StaticRouter location={url}>
<AppRoutes initialData={data} />
</StaticRouter>
);
const html = ReactDOMServer.renderToString(jsx);
const renderedScriptTags = extractor.getScriptTags();
const renderedLinkTags = extractor.getLinkTags();
const renderedStyleTags = extractor.getStyleTags();
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>React App</title>
${renderedLinkTags}
${renderedStyleTags}
</head>
<body>
<div id="app">${html}</div>
<script type="text/javascript">window.__INITIAL_DATA__ = ${JSON.stringify(
initialData
)}</script>
${renderedScriptTags}
</body>
</html>
`;
}
複製代碼
其實就是renderToString
附近那塊作一些修改,根據新的庫換了一些寫法,對於同步渲染基本上就OK了😀。
React v16+中,React團隊添加了一個Stream APIrenderToNodeStream
來提高渲染大型React App的性能,因爲JS的單線程特色,頻繁同步的調用renderToString
會柱塞event loop,使得其餘的http請求/任務會等待很長時間,很影響性能,因此接下來咱們使用流API來提高渲染的性能。
以一個koa route做爲例子:
router.get('/index', async ctx => {
//防止koa自動處理response, 咱們要直接把react stream pipe到ctx.res
ctx.respond = false;
//見下面render方法
const {htmlStream, extractor} = render(ctx.url);
const before = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> ${extractor.getStyleTags()} </head> <body><div id="app">`;
//先往res裏html 頭部信息,包括div容器的一半
ctx.res.write(before);
//把react放回的stream pipe進res, 而且傳入`end:false`關閉流的自動關閉,由於咱們還有下面一半的html沒有寫進去
htmlStream.pipe(
ctx.res,
{ end: false }
);
//監聽react stream的結束,而後把後面剩下的html寫進html document
htmlStream.on('end', () => {
const after = `</div> <script type="text/javascript">window.__INITIAL_DATA__ = ${JSON.stringify( extra.initialData || {} )}</script> ${extractor.getScriptTags()} </body> </html>`;
ctx.res.write(after);
//所有寫完後,結束掉http response
ctx.res.end();
});
});
function render(url){
//...
//替換renderToString 爲 renderToNodeStream,返回一個ReadableStream,其餘都差很少
const htmlStream = ReactDOMServer.renderToNodeStream(jsx);
return {
htmlStream,
extractor,
}
//...
}
複製代碼
上面的代碼加了註釋說明每一行的功能,主要分爲3個部分,咱們先向response寫入head相關的html, 而後把react返回的readableStream pipe到response, 監聽react stream的結束,而後寫入剩下通常的html, 而後手動調用res.end()
結束repsonse stream,由於咱們上面關閉了response stream 的自動關閉,因此這裏要手動end掉,否則瀏覽器會一直處於pending狀態。
使用Stream API OK後,咱們還有一個在生產環境中常見的問題:對於每個進來的請求,特別是一些靜態頁面,咱們其實不必都從新渲染一次App, 這樣的話對於同步渲染和stream渲染都會或多或少產生影響,特別是當App很大的時候,因此爲了解決這樣的問題,咱們須要在這中間加一層緩存,咱們能夠存到內存,文件,或者數據庫,取決於你項目的實際狀況。
若是咱們使用renderToString
的話其實很簡單,只須要拿到html後根據key(url或者其餘的)存到某個地方就好了,可是對於Stream 渲染的話可能會有些tricky。由於咱們把react的stream直接pipe到response了,這裏咱們使用了2種stream類型,ReadableStream
(ReactDom.renderToNodeStream)和WritableStream
(ctx.res),但其實node裏還有其餘的stream類型,其中的TransformStream類型就能夠幫咱們解決上面stream的問題,咱們能夠在把react的readableStream pipe到TransformStream,而後這個TransformStream再pipe到res, 在transform的過程當中(其實這裏咱們沒有修改任何數據,只是爲了拿到全部的html),咱們就能夠拿到全部react渲染出來的內容了,而後在transform結束時把全部拿到的chunk組合起來就是完整的html, 再像同步渲染的方式同樣緩存起來就搞定了🦄
OK,不扯淡了, 直接上代碼:
const { Transform } = require('stream');
//這裏簡單用Map做爲緩存的地方
const cache = new Map();
//臨時的數組用來把react stream每次拿到的數據塊存起來
const bufferedChunks = [];
//建立一個transform Stream來獲取全部的chunk
const cacheStream = new Transform({
//每次從react stream拿到數據後,會調用此方法,存到bufferedChunks裏面,而後原封不動的扔給res
transform(data, enc, cb) {
bufferedChunks.push(data);
cb(null, data);
},
//等所有結束後會調用flush
flush(cb) {
//把bufferedChunks組合起來,轉成html字符串,set到cache中
cache.set(key, Buffer.concat(bufferedChunks).toString() );
cb();
},
});
複製代碼
能夠把上面的代碼封裝成一個方法,以便每次請求進來方便調用,而後咱們在使用的時候:
//假設上面的代碼已經封裝到createCacheStream方法裏了,key能夠爲當前的url,或者其餘的
const cacheStream = createCacheStream(key);
//cacheStream如今會pipe到res
cacheStream.pipe(
res,
{ end: false }
);
//這裏只顯示部分html
const before = ` <!DOCTYPE html> <html lang="en"> <head>...`;
//如今是往cacheStream裏直接寫html
cacheStream.write(before);
// res.write(before);
//react stream pipe到cacheStream
htmlStream.pipe(
cacheStream,
{ end: false }
);
//同上監聽react渲染結束
htmlStream.on('end', () => {
const after = `</div> <script type="text/javascript">window.__INITIAL_DATA__ = ${JSON.stringify( {} )}</script> ${extractor.getScriptTags()} </body> </html>`;
cacheStream.write(after);
console.log('streaming rest html content done!');
//結束http response
res.end();
//結束cacheStream
cacheStream.end();
});
複製代碼
上面咱們把htmlStream 經過管道扔給cacheStream,來讓cacheStream能夠獲取react渲染出來的html,而且緩存起來,而後下次同一個url請求過來時,咱們能夠經過key檢查一下(如: cache.has(key)
)當前url是否已經有渲染過的html了,有的話直接扔給瀏覽器而不須要再從新渲染一遍。
好了,上面就是此次SSR更新的主要內容了。
💖想嘗試完整demo的話能夠關顧一下 koa-web-kit, 而後體驗SSR給你帶來的效果吧😀
Part II的主要內容就是上面這些,咱們主要替換了再也不維護的react-loadable,而後使用stream API來提高大型React App的渲染性能,再經過加上cache層進一步提高響應速度🎉。上面可能有些stream相關的API須要不熟悉的同窗先去了解一下node stream的相關內容,想要查看一下SSR的基礎配置的話也能夠回顧第一部分的內容。
🙌Stay tuned for Part III🙌
English Version: React Server Side Rendering with Koa Part II