為什么要使用服務(wù)器端渲染(SSR)
- 更好的 SEO巡雨,由于搜索引擎爬蟲(chóng)抓取工具可以直接查看完全渲染的頁(yè)面
- 解決首屏白屏問(wèn)題
- 學(xué)習(xí)新技能
使用Node進(jìn)行服務(wù)端渲染
同構(gòu)
在服務(wù)端渲染 調(diào)用 React 的 服務(wù)端渲染方法 renderToString 但是無(wú)法綁定事件,我們需要在 里面再插入前端打包后的JS,我們需要將React代碼在服務(wù)端執(zhí)行一遍次酌,在客戶端再執(zhí)行一遍晶丘,這種服務(wù)器端和客戶端共用一套代碼的方式就稱之為同構(gòu)
首先服務(wù)端調(diào)用 renderToString 渲染組件
import { renderToString } from 'react-dom/server'
const ele = renderToString(
<StaticRouter location={req.url} context={context}>
<Fragment>{renderRoutes(routers)}</Fragment>
</StaticRouter>
)
const html = `<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<div id="root">${ele}</div>
<script src="/index.js"></script>
</body>
</html>
`
再在 body 里面插入 打包后的 JS
路由的使用
在客戶端我們可以使用 BrowserRouter, 在服務(wù)端我們使用 StaticRouter
解決頁(yè)面刷新后重定向問(wèn)題
app.get('*', (req, res) => {
...
<StaticRouter location={req.url} context={context}>
...
</StaticRouter>
})
解決CSS
在服務(wù)端解析 CSS 解析使用 isomorphic-style-loader ,會(huì)有一個(gè) _getCss 方法齐婴。
isomorphic-style-loader 提供了一個(gè)withStyles 高階函數(shù)
import withStyles from 'isomorphic-style-loader/withStyles'
export default withStyles(styles)(App)
拼接CSS
在服務(wù)器端
const css = new Set() // CSS for all rendered React components
const insertCss = (...styles) => styles.forEach(style => css.add(style._getCss()))
<StyleContext.Provider value={{ insertCss }}>
...
</StyleContext.Provider>
把CSS 插入到 head
<html lang="en">
<head>
<style>${[...css].join('')}</style>
</head>
<body>
<div id="root">${ele}</div>
<script src="/index.js"></script>
</body>
</html>
在客戶端
const insertCss = (...styles) => {
const removeCss = styles.map(style => style._insertCss())
return () => removeCss.forEach(dispose => dispose())
}
<StyleContext.Provider value={{ insertCss }}>
...
</StyleContext.Provider>
在服務(wù)端使用Redux
Redux 的時(shí)候和正常在客戶端使用一樣,但是要防止服務(wù)端 所有調(diào)用者引用同一個(gè)對(duì)象
// 每一次調(diào)用返回一個(gè)新的store诸老,避免服務(wù)器端所有人都引用的同一個(gè)對(duì)象
export const getServerStore = (req) => {
const middleWares = thunk.withExtraArgument(serverAxios(req));
return createStore(
reducers,
applyMiddleware(middleWares)
)
}
使用 Provider 進(jìn)行連接
<Provider store={getServerStore(req)}>
<StaticRouter location={req.url} context={context}>
<Fragment>{renderRoutes(routers)}</Fragment>
</StaticRouter>
</Provider>
在客戶端使用
export const getClienStore = () => {
// 如果服務(wù)器端已經(jīng)產(chǎn)生了數(shù)據(jù)团南,就作為默認(rèn)store使用 也就是脫水操作
const defaultStore = window.REDUX_STORE || {};
return createStore(
reducers,
defaultStore,
applyMiddleware(thunk.withExtraArgument(clientAxios))
)
}
這里使用到了脫水操作,后面再講
<Provider store={getClienStore()}>
<BrowserRouter>
<Fragment>{renderRoutes(routers)}</Fragment>
</BrowserRouter>
</Provider>
使用Axios 進(jìn)行異步請(qǐng)求
這里使用到了 Node 作為中間件 轉(zhuǎn)發(fā)數(shù)據(jù)
分為 client 和 server axios
Client
import axios from 'axios';
const instance = axios.create({
baseURL: '/api',
});
export default instance;
import axios from 'axios';
const instance = req => axios.create({
baseURL: 'http://localhost:8085/api',
});
export default instance;
配置 http-proxy-middleware 轉(zhuǎn)發(fā)
app.use('/api', createProxyMiddleware({ target: 'http://localhost:8085', changeOrigin: true }));
如果在服務(wù)端相當(dāng)于直接訪問(wèn)本地 Node 8085 服務(wù), 在客戶端我們發(fā)送請(qǐng)求 利用nginx 轉(zhuǎn)發(fā) 到本地,再 利用 http-proxy-middleware 進(jìn)行轉(zhuǎn)發(fā)到別的服務(wù)器上,這里我們服務(wù)器就是本地
在服務(wù)端就行數(shù)據(jù)加載渲染
首先要匹配要那些頁(yè)面, react-router-config 提供了 matchRoutes 方法
const matchedRoutes = matchRoutes(routes, req.path);
在需要數(shù)據(jù)預(yù)渲染路由添加 loadData 方法
{
path: '/home',
key: 'home',
exact: true,
component: Home,
loadData: Home.loadData
},
在服務(wù)端執(zhí)行 loadData 方法
matchedRoutes.forEach(item => {
if (item.route.loadData) {
const promise = new Promise((resolve) => {
item.route
.loadData(store, item.match.params, req.query)
.then(resolve)
.catch(resolve);
});
promises.push(promise);
}
})
// 數(shù)據(jù)全部渲染完 返回html
Promise.all(promises).then(() => {
const html = reder(store, req, res)
res.send(html)
})
具體頁(yè)面的操作
ExportHome.loadData = async store => {
await store.dispatch(actions.getBlogList())
await store.dispatch(actions.getHotBlog())
await store.dispatch(actions.getTagList())
}
actions
export const getBlogList = (params = {}) => (dispatch, getState, axios) => axios.get('/blog/findAndCountAll', { params }).then(res => {
dispatch(chanegState(constants.HOME_GETBLOGLIST, res.data.data))
})
數(shù)據(jù)注水和數(shù)據(jù)脫水
上面在使用 Redux 的時(shí)候我們提到了脫水,為什么要使用這個(gè)概念呢.
因?yàn)槲覀兪?SSR 渲染,有些數(shù)據(jù)在服務(wù)端已經(jīng)預(yù)先加載好,為了到客戶端二次重新請(qǐng)求,就有了 注水 和 脫水的概念
獲取服務(wù)端的 store
const store = getServerStore(req);
進(jìn)行注水
const html = `<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<script>
window.REDUX_STORE = ${JSON.stringify(store.getState())};
</script>
<script src="/index.js"></script>
</body>
</html>
`
脫水操作
export const getClienStore = () => {
// 如果服務(wù)器端已經(jīng)產(chǎn)生了數(shù)據(jù),就作為默認(rèn)store使用 也就是脫水操作
const defaultStore = window.REDUX_STORE || {};
return createStore(
reducers,
defaultStore,
applyMiddleware(thunk.withExtraArgument(clientAxios))
)
}
使用 html-minifier 進(jìn)行壓縮
對(duì)得到渲染后的 html 節(jié)點(diǎn) 進(jìn)行壓縮
import { minify } from 'html-minifier';
const minifyHtml = minify(html, {
minifyCSS: true,
minifyJS: true,
minifyURLs: true,
});
使用 react-helmet 管理 head信息
SEO 主要是針對(duì)搜索引擎進(jìn)行優(yōu)化插爹,為了提高網(wǎng)站在搜索引擎中的自然排名介褥,但搜索引擎只能爬取落地頁(yè)內(nèi)容(查看源代碼時(shí)能夠看到的內(nèi)容),而不能爬取 js 內(nèi)容递惋,我們可以在服務(wù)器端做優(yōu)化。
常規(guī)的 SEO 主要是優(yōu)化:文字溢陪,鏈接萍虽,多媒體。
- 內(nèi)部鏈接盡量保持相關(guān)性
- 外部鏈接盡可能多
- 多媒體盡量豐富
我們需要做的就是優(yōu)化頁(yè)面的 title形真,description 等杉编,讓爬蟲(chóng)爬到頁(yè)面后能夠展示的更加友好。
這里借助于 react-helmet 庫(kù)咆霜,在服務(wù)期端進(jìn)行 title邓馒,meta 等信息注入。
Node 啟用 Gzip
安裝一個(gè)compression依賴
npm install compression
使用
var compression = require('compression')
var app = express();
//盡量在其他中間件前使用compression
app.use(compression());
總結(jié)
使用了 SSR 不得不說(shuō),頁(yè)面渲染真的快了很多,白屏?xí)r間大大減少,但是這中間的 坑 真的不少,每一步都需要自己去折騰,一路下來(lái),收獲不少.
最后附上地址
博客預(yù)覽: - 博客地址
項(xiàng)目地址: -github