本項目github地址 react-koa2-ssrhtml
所用到技術棧 react16.x + react-router4.x + koa2.x前端
前段時間業餘作了一個簡單的古文網 ,可是項目是使用React SPA 渲染的,不利於SEO,便有了服務端渲染這個需求。後面就想寫個demo把整個過程總結一下,同時也加深本身對其的理解,期間因爲工做,過程是斷斷續續 。總以後來就有了這個項目吧。關於服務端渲染的優缺點,vue服務端渲染官方文檔講的最清楚。講的最清楚。 對於大部分場景最主要仍是兩點 提升首屏加載速度 和方便SEO.爲了快速構建開發環境,這裏直接使用create-react-app 和koa2.x生成一個基礎項目 。整個項目即是以此做爲基點進行開發的,目前也只是完成了最基本的需求, 還有不少Bug 和能夠優化的地方, 歡迎交流。vue
首先先後端分別使用create-react-app 和koa2的腳手架快速生成, 而後再將兩個項目合併到一塊兒。這樣咱們省去了webpack的一些繁瑣配置 ,同時服務端使用了babel編譯。看這個以前 默認已經掌握webpack 和 koa2.x,babel的相關知識。 咱們直切重要的步驟吧。我以爲搭建一個react-ssr環境主要只有三點 第一是react服務端提供的渲染API,二是先後端路由的同構,三則是初始化異步數據的同構。所以這個簡單的demo主要從這三方面入手。react
其實能夠看 《深刻React技術棧》的第七章, 介紹的很是詳細。 歸納來講 React 之因此能夠作到服務端渲染 是由於ReactDOM提供了服務端渲染的APIwebpack
咱們能夠調用這兩個API 實現傳入ReactComponent 返回對應的html字符串到客戶端。瀏覽器端接收到這段html之後不會從新去渲染DOM樹,只是去作事件綁定等操做。這樣就提升了首屏加載的性能。ios
react-router4.x 相對於以前的版本,作了較大的改動。 整個路由變得組件化了。 能夠着重看這裏 官方給出了詳細的例子和文檔能夠做爲基本思想的和標準參考。git
服務端渲染與客戶端渲染的不一樣之處在於其路由是沒有狀態的,因此咱們須要經過一個無狀態的router組件 來包裹APP,經過服務端請求的url來匹配到具體的路由數組和其相關屬性。 因此咱們在客戶端使用 BrowserRouter,服務端則使用無狀態的 StaticRouter。github
// 服務端路由配置
import { createServer } from 'http'
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import { StaticRouter } from 'react-router'
import App from './App'
createServer((req, res) => {
const context = {}
const html = ReactDOMServer.renderToString(
<StaticRouter
location={req.url}
context={context}
>
<App/>
</StaticRouter>
)
if (context.url) {
res.writeHead(301, {
Location: context.url
})
res.end()
} else {
res.write(`
<!doctype html>
<div id="app">${html}</div>
`)
res.end()
}
}).listen(3000)
And then the client:import ReactDOM from 'react-dom'
// 客戶端路由配置
import { BrowserRouter } from 'react-router-dom'
import App from './App'
ReactDOM.render((
<BrowserRouter>
<App/>
</BrowserRouter>
), document.getElementById('app'))
複製代碼
咱們把koa的路由url傳入 ,後者會根據url 自動匹配對應的React組件,這樣咱們就能實現,刷新頁面,服務端返回的對應路由組件與客戶端一致。 到這一步咱們已經能夠實現頁面刷新 服務端和客戶端保持一致了。web
首先下官方文檔作了簡單的介紹介紹cn.redux.js.org/docs/recipe…redux
其處理步驟以下:
const store = createStore(counterApp, preloadedState)
,const finalState = store.getState()
方法獲取到store的初始化state.服務端
<html>
<head>
<title>Redux Universal Example</title>
</head>
<body>
<div id="root">${html}</div>
<script>
window.__INITIAL_STATE__ = ${JSON.stringify(finalState)}
</script>
<script src="/static/bundle.js"></script>
</body>
</html>
複製代碼
客戶端
...
// 經過服務端注入的全局變量獲得初始 state
const preloadedState = window.__INITIAL_STATE__
// 使用初始 state 建立 Redux store
const store = createStore(counterApp, preloadedState)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
複製代碼
這個基本上就是一個標準的redux同構流程, 其實更多的官方是在給咱們提供一種標準化的思路,咱們能夠順着這個作更多的優化。 首先咱們並不須要直接經過API做爲映射 服務端和客戶端各搞一套異步加載的方法,這樣顯得很是冗餘。 react-router 包裏面提供了react-router-config主要用於靜態路由配置。 提供的 matchRoutes API能夠根據傳入的url 返回對應的路由數組。咱們能夠經過這個方法在服務端直接訪問到對應的React組件。 若是要從路由中直接獲取異步方法,我看了不少相似的同構方案,
本項目採用了第二種方案,先看一下代碼:
/**
* 渲染服務端路由
*/
module.exports.render = async(ctx,next) =>{
const { store ,history} = getCreateStore(ctx);
const branch = matchRoutes(router, ctx.req.url);
const promises = branch.map(({route}) => {
const fetch = route.component.fetch;
return fetch instanceof Function ? fetch(store) : Promise.resolve(null)
});
await Promise.all(promises).catch((err)=>{
console.log(err);
});
const html = ReactDOMServer.renderToString(
<Provider store={store}>
<StaticRouter
location={ctx.url}
context={{}}>
<App/>
</StaticRouter>
</Provider>
)
let initState=store.getState();
const body = layout(html,initState);
ctx.body =body;
}
複製代碼
對應容器組件提供了一個靜態的fetch方法
class Home extends Component {
...
static fetch(store){
return store.dispatch(fetchBookList({page:1,size:20}))
}
複製代碼
這是咱們的 actions
/**
* 獲取書籍目錄
* @param {*} param
*/
export const fetchBookList = (params) => {
return async (dispatch, getState) => {
await axios.get(api.url.booklist, {
params: params
}).then((res) => {
dispatch(booklist(res.data.result));
}).catch((err) => {
})
}
}
複製代碼
首先咱們經過 matchRoutes 拿到當前路由下全部的路由,再對其遍歷獲得有關一個異步方法的Promise數組,這裏咱們所謂的異步方法就是actions中的異步方法。因爲咱們在服務端也初始化的store因此咱們能夠直接在服務端調用actions,這裏咱們須要給容器組件的static方法傳入store ,這樣咱們就能夠經過store.dispatch(fetchBookList({page:1,size:20}))
調用actions了。上面的方法咱們獲得了一個Promise 數組。咱們使用 Promise.all將異步所有執行。這個時候實際上 store的運行跟客戶端是同樣的。 咱們在異步的過程當中 將初始數據所有寫入了 store中。因此咱們經過store.getState()
就能夠拿到初始化數據了。客戶端的初始化跟Redux官方例子是同樣的。直接判斷是否傳入初始化state,若是傳入就作爲初始化數據。咱們服務端的初始化異步和客戶端的初始化異步 如何避免重複。 這裏咱們直接先獲取store中的對應初始數據 ,看是否存在,若是不存在咱們再進行加載。
到這一步咱們已經能夠實現刷新頁面異步數據服務端處理,不刷新頁面前端處理,一個基本的同構方案主體就出來了,剩下的就是一些優化項和一些項目定製性的東西了。
對於服務器而言不只會收到前端路由的請求還會收到各類其餘靜態資源的請求 import {matchPath} from 'react-router-dom';
咱們這裏使用react-router-dom包裏面的 matchPath API 來匹配當前請求路由是否與咱們客戶端的路由配置相同若是不一樣咱們默認爲請求的是靜態資源或其餘。若是不匹配當前路由咱們直接執行 next() 進入到下一個中間件 。由於咱們這個項目實際上仍是是一個先後端分離的項目 只不過增長了服務端渲染的方式而已。 若是服務端還要處理其餘請求,那麼其實咱們也能夠在經過服務端 增長其餘路由 ,經過映射來匹配對應的渲染頁面和API。
寫這個demo看了不少的github項目以及相關文章,這些資料對本項目有很大的啓發
咱們知道服務端渲染的 優點在於能夠極快的首屏優化 ,支持SEO,與傳統的SPA相比多了一種數據的處理方式。 缺點也很是明顯,服務端渲染至關因而把客戶端的處理流程部分移植到了服務端,這樣就增長了服務端的負載。所以要作一個好的SSR方案,緩存是必不可少的。與此同時工程化方面也是有不少值得優化的地方。這裏只是淺嘗輒止,並無作相關的處理,估計後面有時間會作一些優化歡迎你們關注。
本項目github地址 github.com/yangfan0095…
以上です