从零搭建个人博客(5)-SSR渲染

为什么要使用服务器端渲染(SSR)

  • 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面
  • 解决首屏白屏问题
  • 学习新技能

使用Node进行服务端渲染

同构

在服务端渲染 调用 React 的 服务端渲染方法 renderToString 但是无法绑定事件,我们需要在 里面再插入前端打包后的JS,我们需要将React代码在服务端执行一遍,在客户端再执行一遍,这种服务器端和客户端共用一套代码的方式就称之为同构

首先服务端调用 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, 在服务端我们使用 StaticRouter

解决页面刷新后重定向问题


app.get('*', (req, res) => {
    ...
    <StaticRouter location={req.url} context={context}>
    ...
    </StaticRouter>
})

解决CSS

在服务端解析 CSS 解析使用 isomorphic-style-loader ,会有一个 _getCss 方法。

isomorphic-style-loader 提供了一个withStyles 高阶函数

import withStyles from 'isomorphic-style-loader/withStyles'
export default withStyles(styles)(App)

拼接CSS

在服务器端

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>

在服务端使用Redux

Redux 的时候和正常在客户端使用一样,但是要防止服务端 所有调用者引用同一个对象

// 每一次调用返回一个新的store,避免服务器端所有人都引用的同一个对象
export const getServerStore = (req) => {
    const middleWares = thunk.withExtraArgument(serverAxios(req));
    return createStore(
        reducers,
        applyMiddleware(middleWares)
    )
}

使用 Provider 进行连接

<Provider store={getServerStore(req)}>
    <StaticRouter location={req.url} context={context}>
        <Fragment>{renderRoutes(routers)}</Fragment>
    </StaticRouter>
</Provider>

在客户端使用

export const getClienStore = () => {

    // 如果服务器端已经产生了数据,就作为默认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 进行异步请求

这里使用到了 Node 作为中间件 转发数据

分为 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 转发

app.use('/api', createProxyMiddleware({ target: 'http://localhost:8085', changeOrigin: true }));

如果在服务端相当于直接访问本地 Node 8085 服务, 在客户端我们发送请求 利用nginx 转发 到本地,再 利用 http-proxy-middleware 进行转发到别的服务器上,这里我们服务器就是本地

在服务端就行数据加载渲染

首先要匹配要那些页面, react-router-config 提供了 matchRoutes 方法

 const matchedRoutes = matchRoutes(routes, req.path);

在需要数据预渲染路由添加 loadData 方法

{
    path: '/home',
    key: 'home',
    exact: true,
    component: Home,
    loadData: Home.loadData
},

在服务端执行 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);
    }
})

// 数据全部渲染完 返回html
Promise.all(promises).then(() => {
    const html = reder(store, req, res)
    res.send(html)
})

具体页面的操作

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))
})

数据注水和数据脱水

上面在使用 Redux 的时候我们提到了脱水,为什么要使用这个概念呢.

因为我们是 SSR 渲染,有些数据在服务端已经预先加载好,为了到客户端二次重新请求,就有了 注水脱水的概念

获取服务端的 store

const store = getServerStore(req);

进行注水

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 = () => {

    // 如果服务器端已经产生了数据,就作为默认store使用 也就是脱水操作
    const defaultStore = window.REDUX_STORE || {};

    return createStore(
        reducers,
        defaultStore,
        applyMiddleware(thunk.withExtraArgument(clientAxios))
    )
}

使用 html-minifier 进行压缩

对得到渲染后的 html 节点 进行压缩

import { minify } from 'html-minifier';

const minifyHtml = minify(html, {
    minifyCSS: true,
    minifyJS: true,
    minifyURLs: true,
});

使用 react-helmet 管理 head信息

SEO 主要是针对搜索引擎进行优化,为了提高网站在搜索引擎中的自然排名,但搜索引擎只能爬取落地页内容(查看源代码时能够看到的内容),而不能爬取 js 内容,我们可以在服务器端做优化。

常规的 SEO 主要是优化:文字,链接,多媒体。

  • 内部链接尽量保持相关性
  • 外部链接尽可能多
  • 多媒体尽量丰富
    我们需要做的就是优化页面的 title,description 等,让爬虫爬到页面后能够展示的更加友好。

这里借助于 react-helmet 库,在服务期端进行 title,meta 等信息注入。

Node 启用 Gzip

安装一个compression依赖

npm install compression

使用

var compression = require('compression')
var app = express();

//尽量在其他中间件前使用compression
app.use(compression());

总结

使用了 SSR 不得不说,页面渲染真的快了很多,白屏时间大大减少,但是这中间的 真的不少,每一步都需要自己去折腾,一路下来,收获不少.

最后附上地址

博客预览: - 博客地址

项目地址: -github

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345