SSR:即服務端渲染(Server Side Render) 傳統的服務端渲染可使用Java,php 等開發語言來實現,隨着 Node.js 和相關前端領域技術的不斷進步,前端同窗也能夠基於此完成獨立的服務端渲染。php
過程:瀏覽器發送請求 -> 服務器運行 react代碼生成頁面 -> 服務器返回頁面 -> 瀏覽器下載HTML文檔 -> 頁面準備就緒 即:當前頁面的內容是服務器生成好給到瀏覽器的。html
如何區分頁面是否服務端渲染: 右鍵點擊 -> 顯示網頁源代碼,若是頁面上的內容在HTML文檔裏,是服務端渲染,不然就是客戶端渲染。前端
對比node
首屏加載時間優化,因爲SSR是直接返回生成好內容的HTML,而普通的CSR是先返回空白的HTML,再由瀏覽器動態加載JavaScript腳本並渲染好後頁面纔有內容;因此SSR首屏加載更快、減小白屏的時間、用戶體驗更好。react
SEO (搜索引擎優化),搜索關鍵詞的時候排名,對大多數搜索引擎,不識別JavaScript 內容,只識別 HTML 內容。 (注:原則上能夠不用服務端渲染時最好不用,因此若是隻有 SEO 要求,能夠用預渲染等技術去替代)webpack
(1) 使用 Node.js 做爲服務端和客戶端的中間層,承擔 proxy代理,處理cookie等操做。ios
(2) hydrate 的使用:在有服務端渲染狀況下,使用hydrate代替render,它的做用主要是將相關的事件注水進HTML頁面中(即:讓React組件的數據隨着HTML文檔一塊兒傳遞給瀏覽器網頁),這樣能夠保持服務端數據和瀏覽器端一致,避免閃屏,使第一次加載體驗更高效流暢。web
ReactDom.hydrate(<App />, document.getElementById('root')); 複製代碼
(3) 服務端代碼webpack編譯:一般會建一個webpack.server.js文件,除了常規的參數配置外,還須要設置target參數爲'node'。redux
const serverConfig = {
target: 'node',
entry: './src/server/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, '../dist')
},
externals: [nodeExternals()],
module: {
rules: [{
test: /\.js?$/,
loader: 'babel-loader',
exclude: [
path.join(__dirname, './node_modules')
]
}
...
]
}
(此處省略樣式打包,代碼壓縮,運行壞境配置等等...)
...
};
複製代碼
(4) 使用react-dom/server下的 renderToString方法在服務器上把各類複雜的組件和代碼轉化成 HTML 字符串返回到瀏覽器,並在初始請求時發送標記以加快頁面加載速度,並容許搜索引擎抓取頁面以實現SEO目的。axios
const render = (store, routes, req, context) => {
const content = renderToString((
<Provider store={store}> <StaticRouter location={req.path} context={context}> <div> {renderRoutes(routes)} </div> </StaticRouter> </Provider>
));
return ` <html> <head> <title>ssr</title> </head> <body> <div id='root'>${content}</div> <script src='/index.js'></script> </body> </html> `;
}
app.get('*', function (req, res) {
...
const html = render(store, routes, req, context);
res.send(html);
});
複製代碼
與renderToString相似功能的還有: i. renderToStaticMarkup:區別在於renderToStaticMarkup 渲染出的是不帶data-reactid的純HTML,在JavaScript加載完成後由於不認識以前服務端渲染的內容致使從新渲染(可能頁面會閃一下)。
ii. renderToNodeStream:將React元素渲染爲其初始HTML,返回一個輸出HTML字符串的可讀流。
iii. renderToStaticNodeStream:與renderToNodeStream此相似,除了這不會建立React在內部使用的額外DOM屬性,例如data-reactroot。
(5) 使用redux 承擔數據準備,狀態維護的職責,一般搭配react-redux, redux-thunk(中間件:發異步請求用到action)使用。(本猿目前使用比較可能是就是Redux和Mobx,這裏以Redux爲例)。 A. 建立store(服務器每次請求都要建立一次,客戶端只建立一次):
const reducer = combineReducers({
home: homeReducer,
page1: page1Reducer,
page2: page2Reducer
});
export const getStore = (req) => {
return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios(req))));
}
export const getClientStore = () => {
return createStore(reducer, window.STATE_FROM_SERVER, applyMiddleware(thunk.withExtraArgument(clientAxios)));
}
複製代碼
B. action: 負責把數據從應用傳到store,是store數據的惟一來源
export const getData = () => {
return (dispatch, getState, axiosInstance) => {
return axiosInstance.get('interfaceUrl/xxx')
.then((res) => {
dispatch({
type: 'HOME_LIST',
list: res.list
})
});
}
}
複製代碼
C. reducer:接收舊的state和action,返回新的state,響應actions併發送到store。
export default (state = { list: [] }, action) => {
switch(action.type) {
case 'HOME_LIST':
return {
...state,
list: action.list
}
default:
return state;
}
}
export default (state = { list: [] }, action) => {
switch(action.type) {
case 'HOME_LIST':
return {
...state,
list: action.list
}
default:
return state;
}
}
複製代碼
D. 使用react-redux的connect,Provider把組件和store鏈接起來
const content = renderToString((
<Provider store={store}> <StaticRouter location={req.path} context={context}> <div> {renderRoutes(routes)} </div> </StaticRouter> </Provider>
));
複製代碼
connect(mapStateToProps(),mapDispatchToProps())(MyComponent)
複製代碼
(6) 使用react-router承擔路由職責 服務端路由不一樣於客戶端,它是無狀態的。React 提供了一個無狀態的組件StaticRouter,向StaticRouter傳遞當前URL,調用ReactDOMServer.renderToString() 就能匹配到路由視圖。
服務端
import { StaticRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config'
import routes from './router.js'
<StaticRouter location={req.path} context={{context}}>
{renderRoutes(routes)}
</StaticRouter>
複製代碼
瀏覽器端
import { BrowserRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config'
import routes from './router.js'
<BrowserRouter>
{renderRoutes(routes)}
</BrowserRouter>
複製代碼
當瀏覽器的地址欄發生變化的時候,前端會去匹配路由視圖,同時因爲req.path發生變化,服務端匹配到路由視圖,這樣保持了先後端路由視圖的一致,在頁面刷新時,仍然能夠正常顯示當前視圖。若是隻有瀏覽器端路由,並且是採用BrowserRouter,當頁面地址發生變化後去刷新頁面時,因爲沒有對應的HTML,會致使頁面找不到,可是加了服務端路由後,刷新發生時服務端會返回一個完整的html給客戶端,頁面仍然正常顯示。 推薦使用 react-router-config插件,而後如上代碼在StaticRouter和BrowserRouter標籤的子元素里加renderRoutes(routes):建一個router.js文件
const routes = [{ component: Root,
routes: [
{ path: '/',
exact: true,
component: Home,
loadData: Home.loadData
},
{ path: '/child/:id',
component: Child,
loadData: Child.loadData
routes: [
path: '/child/:id/grand-child',
component: GrandChild,
loadData: GrandChild.loadData
]
}
]
}];
複製代碼
在瀏覽器端請求一個地址的時候,server.js 裏在實際渲染前能夠經過matchRouters 這種方式肯定要渲染的內容,調用loaderData函數進行action派發,返回promise->promiseAll->renderToString,最終生成HTML文檔返回。
import { matchRoutes } from 'react-router-config'
const loadBranchData = (location) => {
const branch = matchRoutes(routes, location.pathname)
const promises = branch.map(({ route, match }) => {
return route.loadData
? route.loadData(match)
: Promise.resolve(null)
})
return Promise.all(promises)
}
複製代碼
(7) 寫組件注意代碼同構(即:一套React代碼在服務端執行一次,在客戶端再執行一次) 因爲服務器端綁定事件是無效的,因此服務器返回的只有頁面樣式(&注水的數據),同時返回JavaScript文件,在瀏覽器上下載並執行JavaScript時才能把事件綁上,而咱們但願這個過程只需編寫一次代碼,這個時候就會用到同構,服務端渲染出樣式,在客戶端執行時綁上事件。
優勢: 共用前端代碼,節省開發時間 弊端: 因爲服務器端和瀏覽器環境差別,會帶來一些問題,如document等對象找不到,DOM計算報錯,前端渲染和服務端渲染內容不一致等;前端能夠作很是複雜的請求合併和延遲處理,但爲了同構,全部這些請求都在預先拿到結果纔會渲染。
做者簡介
朵拉,銅板街前端開發工程師,2015年8月加入團隊,目前主要負責運營側APP端項目開發。