從零搭建個(gè)人博客(5)-SSR渲染

為什么要使用服務(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蛾坯,一起剝皮案震驚了整個(gè)濱河市光酣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌脉课,老刑警劉巖救军,帶你破解...
    沈念sama閱讀 218,607評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件财异,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡唱遭,警方通過(guò)查閱死者的電腦和手機(jī)戳寸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)拷泽,“玉大人疫鹊,你說(shuō)我怎么就攤上這事∷局拢” “怎么了拆吆?”我有些...
    開(kāi)封第一講書人閱讀 164,960評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)蚌吸。 經(jīng)常有香客問(wèn)我锈拨,道長(zhǎng),這世上最難降的妖魔是什么羹唠? 我笑而不...
    開(kāi)封第一講書人閱讀 58,750評(píng)論 1 294
  • 正文 為了忘掉前任奕枢,我火速辦了婚禮,結(jié)果婚禮上佩微,老公的妹妹穿的比我還像新娘缝彬。我一直安慰自己,他們只是感情好哺眯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布谷浅。 她就那樣靜靜地躺著,像睡著了一般奶卓。 火紅的嫁衣襯著肌膚如雪一疯。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,604評(píng)論 1 305
  • 那天夺姑,我揣著相機(jī)與錄音墩邀,去河邊找鬼。 笑死盏浙,一個(gè)胖子當(dāng)著我的面吹牛眉睹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播废膘,決...
    沈念sama閱讀 40,347評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼竹海,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了丐黄?” 一聲冷哼從身側(cè)響起斋配,我...
    開(kāi)封第一講書人閱讀 39,253評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后许起,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體十偶,經(jīng)...
    沈念sama閱讀 45,702評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評(píng)論 3 336
  • 正文 我和宋清朗相戀三年园细,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了惦积。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,015評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡猛频,死狀恐怖狮崩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情鹿寻,我是刑警寧澤睦柴,帶...
    沈念sama閱讀 35,734評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站毡熏,受9級(jí)特大地震影響坦敌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜痢法,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評(píng)論 3 330
  • 文/蒙蒙 一狱窘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧财搁,春花似錦蘸炸、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,934評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至提茁,卻和暖如春淹禾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背茴扁。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,052評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工铃岔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人丹弱。 一個(gè)月前我還...
    沈念sama閱讀 48,216評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像铲咨,于是被迫代替她去往敵國(guó)和親躲胳。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評(píng)論 2 355