徹底理解React 之React SSR、React服務端渲染,教你從零搭建配置

技術棧: React16.x + React-router4.x + React-redux5.x + Redux-thunk2.x + express4.x

前言

前段時間研究了下React SSR,SEO。后面就想把整個過程總結一下,同時也加深自己對其的理解 。(好慌!第一次寫簡書) 關于服務端渲染的優缺點,vue服務端渲染官方文檔講的很清楚。網上關于React的SSR也很多,但都不夠詳細。不過這篇文章我將詳細的一步一步介紹、深入配置React SSR、讓每個看到的人都能看懂。 SSR對于大部分場景最主要還是兩點 提高首屏加載速度 和方便SEO.為了快速構建開發環境,這里直接從githup上下載了一個別人寫好的 使用create-react-app搭建的項目 。傳送門(https://github.com/wujiabk/zhaopinApp)進行從零搭建服務端渲染,歡迎交流。

為什么使用服務器端渲染(SSR)?

與傳統 SPA(Single-Page Application - 單頁應用程序)相比,服務器端渲染(SSR)的優勢主要在于:

1.更好的 SEO,由于搜索引擎爬蟲抓取工具可以直接查看完全渲染的頁面。

2.解決首屏白屏問題

3.順應時代潮流(開玩笑啦!)

開始!

準備工作:

 // 簡單修改index.js代碼,把路由抽離到app.js中,如下:
import React from 'react'; 
import ReactDOM from 'react-dom';
import {
    createStore,
    applyMiddleware,
    compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
import {
    BrowserRouter
} from "react-router-dom";
import reducers from "./reducer";
import Routers from './router'
// 引入antd css
import 'antd-mobile/dist/antd-mobile.css';

const store = createStore(reducers,compose(
    applyMiddleware(thunk),
));

ReactDOM.render(
    <Provider store={store}>
        <BrowserRouter>
            <Routers/>
        </BrowserRouter>
    </Provider>,
    document.getElementById('root')
);

項目上線一般都是前端build好包,后臺去部署,ok,咱們打包下。


image.png

此時項目中多出來一個build文件,里面有asset-manifest.json的文件,里面有咱們打包后js、css的路徑,仔細的同學可以看到每個路徑后面都有一個哈希值,哈希值每次打包的都不一樣,來區分版本。我們從找個入口就可以提取到我們打包后的代碼,為以后做服務端渲染做鋪墊!


image.png

正式開始

1.首先我們要看到build以后的文件,需要在本地搭建一個server,修改server文件的server.js,如下:

const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());

// 用戶接口模塊
app.use("/user",userRoute);

// 映射到build后的路徑
//設置build以后的文件路徑 項目上線用
app.use((req, res, next) => {
    if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
        return next()
    }
    return res.sendFile(path.resolve('build/index.html'))
})
app.use('/', express.static(path.resolve('build')))

app.listen("9000",function(){
    console.log("open Browser http://localhost:9000");
});

然后配置package.json,加一條命令,scripts如下:

 "scripts": {
    "start": "node scripts/start.js",
    "build": "node scripts/build.js",
    "test": "node scripts/test.js --env=jsdom",
    "server1": "nodemon server/server.js"
  },

然后運行cnpm run server1,打開http://localhost:9000/
可以看到項目已經運行成功,此時我們的端口是9000和開發端口3000已經沒關系了。并且代碼運行的都是build以后的代碼

image.png

想要做到首屏SSR,就需要后臺返回首屏login頁面所有的jsx代碼,因為nodejs不支持es6、es7語法,所以需要手動配置下,ok:
.... 運行cnpm install babel-cli --save
這個是babel命令行工具,我們要用到它里面的babel-node
修改package.json里面scripts,如下:

 "scripts": {
    "start": "node scripts/start.js",
    "build": "node scripts/build.js",
    "test": "node scripts/test.js --env=jsdom",
    "server": "cross-env NODE_ENV=test nodemon --exec babel-node server/server.js",
    "server1": "nodemon server/server.js"
  },

設置node環境為test,默認為node 修改為nodemon --exec babel-node

需要注意:

cross-env能跨平臺地設置及使用環境變量
大多數情況下,在windows平臺下使用類似于: NODE_ENV=production的命令行指令會卡住,windows平臺與POSIX在使用命令行時有許多區別(例如在POSIX,使用$ENV_VAR,在windows,使用%ENV_VAR%。。。)
cross-env讓這一切變得簡單,不同平臺使用唯一指令,無需擔心跨平臺問題
運行:cnpm i --save-dev cross-env ?,F在服務端已經支持es6、es7語法了。
但是現在還不支持jsx語法,既然客戶端可以用babel支持,服務端當然也可以了,我們新建.babelrc 里面寫入、

{
  "presets": [
    "react-app"
  ],
  "plugins": [
    "transform-decorators-legacy"
  ]
}

ReactDOMServer.renderToString

這里簡單解釋下,React.createElement把React類進行實例化,實例化后的組件就可以進行mount操作了,在瀏覽器環境我們是使用ReactDOM.render()來進行掛載操作的。ReactDOMServer.renderToString則是把React實例渲染成HTML標簽。接下里就需要吧index.js里面代碼搬到server.js里面,渲染成html返回給前端就OK!
進行代碼改造,完成后如下:

const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());

/**
 * 插入react代碼 進行服務端改造
 */
import React from 'react';
import ReactDOM from 'react-dom';
import {
    createStore,
    applyMiddleware,
    compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// 引入renderToString
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
// 服務端是沒有BrowserRouter 所以用StaticRouter
import { StaticRouter } from "react-router-dom";
// 引入reducer
import reducers from "../src/reducer";
// 引入前端路由
import Routers from '../src/router'

const store = createStore(reducers, compose(
    applyMiddleware(thunk),
));

// 用戶接口模塊
app.use("/user", userRoute);

// 映射到build后的路徑
//設置build以后的文件路徑 項目上線用
app.use((req, res, next) => {
    if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
        return next()
    }
    const context = {}
    const frontComponents = renderToString(
        (<Provider store={store}>
            <StaticRouter
                location={req.url}
                context={context}>
                <Routers />
            </StaticRouter>
        </Provider>)
    )
    res.send(frontComponents)
    // return res.sendFile(path.resolve('build/index.html'))
})
app.use('/', express.static(path.resolve('build')))

app.listen("9000", function () {
    console.log("open Browser http://localhost:9000");
});

可以看到我們將前端代碼用renderToString()處理后返回給前端,這樣前端就能接收到首屏加載所需要的代碼了,但是需要注意的是,此時控制臺報錯了。


image.png

這個報錯意思就是后端不支持css,所以我們要做處理。
運行 cnpm install css-modules-require-hook --save

安裝好依賴以后,我們需要引入配置,并新建crmh,.conf.js鉤子文件進行配置。如下圖:
image.png

配置代碼:
module.exports = {
    generateScopedName: '[name]__[local]___[hash:base64:5]',
}

然后在服務端server.js引入css配置進行代碼改造,最終如下:

需要注意的此處有一小坑,csshook必須放在第一行,代碼初始化的時候先引入css

// 處理css
import csshook from 'css-modules-require-hook/preset';

const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());

/**
 * 插入react代碼 進行服務端改造
 */
import React from 'react';
import ReactDOM from 'react-dom';
import {
    createStore,
    applyMiddleware,
    compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// 引入renderToString
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
// 服務端是沒有BrowserRouter 所以用StaticRouter
import { StaticRouter } from "react-router-dom";
// 引入reducer
import reducers from "../src/reducer";
// 引入前端路由
import Routers from '../src/router'

const store = createStore(reducers, compose(
    applyMiddleware(thunk),
));

// 用戶接口模塊
app.use("/user", userRoute);

// 映射到build后的路徑
//設置build以后的文件路徑 項目上線用
app.use((req, res, next) => {
    if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
        return next()
    }
    const context = {}
    const frontComponents = renderToString(
        (<Provider store={store}>
            <StaticRouter
                location={req.url}
                context={context}>
                <Routers />
            </StaticRouter>
        </Provider>)
    )
    res.send(frontComponents)
    // return res.sendFile(path.resolve('build/index.html'))
})
app.use('/', express.static(path.resolve('build')))

app.listen("9000", function () {
    console.log("open Browser http://localhost:9000");
});

處理好css后,運行cnpm run server,此時看到報錯:


image.png

這個錯誤的意思就是服務端還沒有處理圖片問題,ok,讓我們處理下
運行 cnpm install asset-require-hook --save 成功后引入配置,改造服務端server.js代碼,改造后如下:

// 處理css
import csshook from 'css-modules-require-hook/preset';
// 處理圖片
import assethook from 'asset-require-hook';
assethook({
    extensions: ['png', 'jpg']
});

const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());

/**
 * 插入react代碼 進行服務端改造
 */
import React from 'react';
import ReactDOM from 'react-dom';
import {
    createStore,
    applyMiddleware,
    compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// 引入renderToString
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
// 服務端是沒有BrowserRouter 所以用StaticRouter
import { StaticRouter } from "react-router-dom";
// 引入reducer
import reducers from "../src/reducer";
// 引入前端路由
import Routers from '../src/router'

const store = createStore(reducers, compose(
    applyMiddleware(thunk),
));

// 用戶接口模塊
app.use("/user", userRoute);

// 映射到build后的路徑
//設置build以后的文件路徑 項目上線用
app.use((req, res, next) => {
    if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
        return next()
    }
    const context = {}
    const frontComponents = renderToString(
        (<Provider store={store}>
            <StaticRouter
                location={req.url}
                context={context}>
                <Routers />
            </StaticRouter>
        </Provider>)
    )
    res.send(frontComponents)
    // return res.sendFile(path.resolve('build/index.html'))
})
app.use('/', express.static(path.resolve('build')))

app.listen("9000", function () {
    console.log("open Browser http://localhost:9000");
});

刷新頁面后,打開Network下面的response 可以看到服務端已經把前端html成功的返回

<div data-reactroot=""><div class="bigbox"><div class="logo-container"><img src="72ba71de2dfbe02c990266c62394b476.png" alt=""/></div><h1 style="color:red;text-align:center">React SSR </h1><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-wingblank am-wingblank-lg"><div class="am-list"><div class="am-list-header"></div><div class="am-list-body"><div class="am-list-item am-input-item am-list-item-middle"><div class="am-list-line"><div class="am-input-label am-input-label-5"><i class="iconfont icon-yonghu c-blue"></i></div><div class="am-input-control"><input type="text" placeholder="請輸入用戶名" value=""/></div></div></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-list-item am-input-item am-list-item-middle"><div class="am-list-line"><div class="am-input-label am-input-label-5"><i class="iconfont icon-mima c-blue" style="font-size:19px"></i></div><div class="am-input-control"><input type="password" placeholder="請輸入密碼" value=""/></div></div></div></div></div><div class="am-whitespace am-whitespace-md"></div><div class="ta-right"><a href="/">忘記密碼?</a></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><a role="button" class="am-button am-button-primary" aria-disabled="false"><span>登 錄</span></a><div class="am-whitespace am-whitespace-md"></div><a role="button" class="am-button am-button-primary" aria-disabled="false"><span>注 冊</span></a><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div></div></div></div>

但是看頁面


image.png

明顯不是我們想要的結果,這是為什么呢?仔細想想,一個完整的頁面,他是需要html body head footer 等標簽元素構成的,所以現在缺少了一個支持頁面的骨架,ok,那我們就在server端加上骨架返回給前端,對server.js進行改造如下:

// 處理css
import csshook from 'css-modules-require-hook/preset';
// 處理圖片
import assethook from 'asset-require-hook';
assethook({
    extensions: ['png', 'jpg']
});

const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());

/**
 * 插入react代碼 進行服務端改造
 */
import React from 'react';
import ReactDOM from 'react-dom';
import {
    createStore,
    applyMiddleware,
    compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// 引入antd css
import 'antd-mobile/dist/antd-mobile.css';
// 引入renderToString
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
// 服務端是沒有BrowserRouter 所以用StaticRouter
import { StaticRouter } from "react-router-dom";
// 引入reducer
import reducers from "../src/reducer";
// 引入前端路由
import Routers from '../src/router'

const store = createStore(reducers, compose(
    applyMiddleware(thunk),
));

// 用戶接口模塊
app.use("/user", userRoute);

// 映射到build后的路徑
//設置build以后的文件路徑 項目上線用
app.use((req, res, next) => {
    if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
        return next()
    }
    const context = {}
    const frontComponents = renderToString(
        (<Provider store={store}>
            <StaticRouter
                location={req.url}
                context={context}>
                <Routers />
            </StaticRouter>
        </Provider>)
    )
    // 新建骨架
    const _frontHtml = `<!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
            <meta name="theme-color" content="#000000">
            <title>人才市場</title>
        </head>
        <body>
            <noscript>
            You need to enable JavaScript to run this app.
            </noscript>
            <div id="root">${frontComponents}</div>
        </body>
    </html>`
    res.send(_frontHtml)
    // return res.sendFile(path.resolve('build/index.html'))
})
app.use('/', express.static(path.resolve('build')))

app.listen("9000", function () {
    console.log("open Browser http://localhost:9000");
});

此時刷新頁面后,打開Network下面的response 可以看到服務端已返回結果是

<!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
            <meta name="theme-color" content="#000000">
            <title>人才市場</title>
        </head>
        <body>
            <noscript>
            You need to enable JavaScript to run this app.
            </noscript>
            <div id="root"><div data-reactroot=""><div class="bigbox"><div class="logo-container"><img src="72ba71de2dfbe02c990266c62394b476.png" alt=""/></div><h1 style="color:red;text-align:center">React SSR </h1><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-wingblank am-wingblank-lg"><div class="am-list"><div class="am-list-header"></div><div class="am-list-body"><div class="am-list-item am-input-item am-list-item-middle"><div class="am-list-line"><div class="am-input-label am-input-label-5"><i class="iconfont icon-yonghu c-blue"></i></div><div class="am-input-control"><input type="text" placeholder="請輸入用戶名" value=""/></div></div></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><div class="am-list-item am-input-item am-list-item-middle"><div class="am-list-line"><div class="am-input-label am-input-label-5"><i class="iconfont icon-mima c-blue" style="font-size:19px"></i></div><div class="am-input-control"><input type="password" placeholder="請輸入密碼" value=""/></div></div></div></div></div><div class="am-whitespace am-whitespace-md"></div><div class="ta-right"><a href="/">忘記密碼?</a></div><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div><a role="button" class="am-button am-button-primary" aria-disabled="false"><span>登 錄</span></a><div class="am-whitespace am-whitespace-md"></div><a role="button" class="am-button am-button-primary" aria-disabled="false"><span>注 冊</span></a><div class="am-whitespace am-whitespace-md"></div><div class="am-whitespace am-whitespace-md"></div></div></div></div></div>
        </body>
    </html>

我們的代碼已經成功有了骨架,ok,離成功更進一步!
但是細心的同學一定發現,此時頁面還不是我們想要的結果,因為我們只是生成了html,并沒有引入css 和 js ,文章開頭我們說過build以后有一個asset-manifest.json的文件,里面有我們想要的css和js,我們引入它,就可以拿到每次build以后最新的代碼,對server.js進行代碼改造如下:

// 處理css
import csshook from 'css-modules-require-hook/preset';
// 處理圖片
import assethook from 'asset-require-hook';
assethook({
    extensions: ['png', 'jpg']
});

const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());

/**
 * 插入react代碼 進行服務端改造
 */
import React from 'react';
import ReactDOM from 'react-dom';
import {
    createStore,
    applyMiddleware,
    compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// 引入antd css
import 'antd-mobile/dist/antd-mobile.css';
// 引入renderToString
import { renderToString, renderToStaticMarkup } from 'react-dom/server';
// 服務端是沒有BrowserRouter 所以用StaticRouter
import { StaticRouter } from "react-router-dom";
// 引入reducer
import reducers from "../src/reducer";
// 引入前端路由
import Routers from '../src/router';
// 引入css 和 js
import buildPath from '../build/asset-manifest.json';

const store = createStore(reducers, compose(
    applyMiddleware(thunk),
));

// 用戶接口模塊
app.use("/user", userRoute);

// 映射到build后的路徑
//設置build以后的文件路徑 項目上線用
app.use((req, res, next) => {
    if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
        return next()
    }
    const context = {}
    const frontComponents = renderToString(
        (<Provider store={store}>
            <StaticRouter
                location={req.url}
                context={context}>
                <Routers />
            </StaticRouter>
        </Provider>)
    )
    // 新建骨架
    const _frontHtml = `<!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
            <meta name="theme-color" content="#000000">
            <title>人才市場</title>
            <link rel="stylesheet" type="text/css" href="/${buildPath['main.css']}">
        </head>
        <body>
            <noscript>
            You need to enable JavaScript to run this app.
            </noscript>
            <div id="root">${frontComponents}</div>
            <script src="/${buildPath['main.js']}"></script>
        </body>
    </html>`
    res.send(_frontHtml)
    // return res.sendFile(path.resolve('build/index.html'))
})
app.use('/', express.static(path.resolve('build')))

app.listen("9000", function () {
    console.log("open Browser http://localhost:9000");
});

刷新頁面后可以看到基本是我們想要的頁面了,可以看到后臺已經返回給一個完整的html

<!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
            <meta name="theme-color" content="#000000">
            <title>人才市場</title>
            <link rel="stylesheet" type="text/css" href="/static/css/main.f4dbdb58.css">
        </head>
        <body>
            <noscript>
            You need to enable JavaScript to run this app.
            </noscript>
            <div id="root"><div data-reactroot=""><div><div class="am-navbar am-navbar-dark"><div class="am-navbar-left" role="button"></div><div class="am-navbar-title">消息列表</div><div class="am-navbar-right"></div></div><div class="mt-45 mb-50"><div>msgpages</div></div><div class="am-tab-bar"><div class="am-tabs am-tabs-horizontal am-tabs-bottom"><div class="am-tabs-content-wrap" style="touch-action:pan-x pan-y;position:relative;left:-200%"><div class="am-tabs-pane-wrap am-tabs-pane-wrap-inactive"></div><div class="am-tabs-pane-wrap am-tabs-pane-wrap-inactive"><div class="am-tab-bar-item"></div></div><div class="am-tabs-pane-wrap am-tabs-pane-wrap-active"><div class="am-tab-bar-item"></div></div><div class="am-tabs-pane-wrap am-tabs-pane-wrap-inactive"><div class="am-tab-bar-item"></div></div></div><div class="am-tabs-tab-bar-wrap"><div class="am-tab-bar-bar" style="background-color:white"><div class="am-tab-bar-tab"><div class="am-tab-bar-tab-icon" style="color:#888"><img class="am-tab-bar-tab-image" src="a12c878ee5f7d318376da191b5b76ef7.png" alt="BOSS"/></div><p class="am-tab-bar-tab-title" style="color:#888">BOSS</p></div><div class="am-tab-bar-tab"><div class="am-tab-bar-tab-icon" style="color:#888"><img class="am-tab-bar-tab-image" src="c6c95d08bf1b9a888cce1053bfa2bf18.png" alt="牛人"/></div><p class="am-tab-bar-tab-title" style="color:#888">牛人</p></div><div class="am-tab-bar-tab"><div class="am-tab-bar-tab-icon" style="color:#108ee9"><img class="am-tab-bar-tab-image" src="f73fc85762cfbe7999c792ce031c7fce.png" alt="消息"/></div><p class="am-tab-bar-tab-title" style="color:#108ee9">消息</p></div><div class="am-tab-bar-tab"><div class="am-tab-bar-tab-icon" style="color:#888"><img class="am-tab-bar-tab-image" src="3bf7c1d72788277ba13047821af2d180.png" alt="我的"/></div><p class="am-tab-bar-tab-title" style="color:#888">我的</p></div></div></div></div></div></div></div></div>
            <script src="/static/js/main.7993f0a1.js"></script>
        </body>
    </html>

基本上React 服務端渲染就結束了!恭喜你,你已經掌握它了。
到現在我們不妨回頭想想,做SSR是為了什么,無非就是SEO,其實我們可以在_frontHtml拼接的時候做很多SEO優化,舉個小栗子:

   const urlObj = {
        '/login': '我是登陸xxxxx',
        '/register': '我是注冊xxxx',
        '/xxx': 'xxxxxx.....'
    }
    // 新建骨架
    const _frontHtml = `<!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
            <meta name="theme-color" content="#000000">
            <title>${urlObj[req.url]}</title>
            <link rel="stylesheet" type="text/css" href="/${buildPath['main.css']}">
            <meta name="keywords" content="seo、seo、seo、seo,搜到我吧!">
            <meta name="description" content="${urlObj[req.url]}">
            <meta name="author" content="你的大名">
        </head>
        <body>
            <noscript>
            You need to enable JavaScript to run this app.
            </noscript>
            <div id="root">${frontComponents}</div>
            <script src="/${buildPath['main.js']}"></script>
        </body>
    </html>`

結束....................


提升 ------ React16 服務端渲染帶來新的API

renderToNodeStream

renderToNodeStream支持直接渲染到節點流。渲染到流可以減少你的內容的第一個字節(TTFB)的時間,在文檔的下一部分生成之前,將文檔的開頭至結尾發送到瀏覽器。 當內容從服務器流式傳輸時,瀏覽器將開始解析HTML文檔。速度是renderToString的三倍,ok,讓我們吧server.js簡單改造下。

// 處理css
import csshook from 'css-modules-require-hook/preset';
// 處理圖片
import assethook from 'asset-require-hook';
assethook({
    extensions: ['png', 'jpg']
});

const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const userRoute = require("./userRoute");
const app = express();
const path = require('path');
app.use(cookieParser());
app.use(bodyParser.json());

/**
 * 插入react代碼 進行服務端改造
 */
import React from 'react';
import ReactDOM from 'react-dom';
import {
    createStore,
    applyMiddleware,
    compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
// 引入antd css
import 'antd-mobile/dist/antd-mobile.css';
// 引入renderToString
import { renderToString, renderToNodeStream } from 'react-dom/server';
// 服務端是沒有BrowserRouter 所以用StaticRouter
import { StaticRouter } from "react-router-dom";
// 引入reducer
import reducers from "../src/reducer";
// 引入前端路由
import Routers from '../src/router';
// 引入css 和 js
import buildPath from '../build/asset-manifest.json';

const store = createStore(reducers, compose(
    applyMiddleware(thunk),
));

// 用戶接口模塊
app.use("/user", userRoute);

// 映射到build后的路徑
//設置build以后的文件路徑 項目上線用
app.use((req, res, next) => {
    if (req.url.startsWith('/user/') || req.url.startsWith('/static/')) {
        return next()
    }
    const urlObj = {
        '/login': '我是登陸xxxxx',
        '/register': '我是注冊xxxx',
        '/xxx': 'xxxxxx.....'
    }
    res.write(`<!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
            <meta name="theme-color" content="#000000">
            <title>${urlObj[req.url]}</title>
            <link rel="stylesheet" type="text/css" href="/${buildPath['main.css']}">
            <meta name="keywords" content="seo、seo、seo、seo,搜到我吧!">
            <meta name="description" content="${urlObj[req.url]}">
            <meta name="author" content="你的大名">
        </head>
        <body>
            <noscript>
            You need to enable JavaScript to run this app.
            </noscript>
            <div id="root">`)
    const context = {}
    const frontComponents = renderToNodeStream(
        (<Provider store={store}>
            <StaticRouter
                location={req.url}
                context={context}>
                <Routers />
            </StaticRouter>
        </Provider>)
    )
    // 推送的前端
    // end表示節點流還沒有結束
    frontComponents.pipe(res, { end: false })
    // 監聽事件結束后 把剩下的流推過去
    frontComponents.on('end', _ => {
        res.write(`</div>
                        <script src="/${buildPath['main.js']}"></script>
                    </body>
                </html>`)
        res.end()
    })
})
app.use('/', express.static(path.resolve('build')))

app.listen("9000", function () {
    console.log("open Browser http://localhost:9000");
});

用時我們要注意 前端render方法要改為hydrate方法避免報錯

import React from 'react';
import ReactDOM from 'react-dom';
import {
    createStore,
    applyMiddleware,
    compose
} from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
import { BrowserRouter } from "react-router-dom";
import reducers from "./reducer";
import Routers from './router'
// 引入antd css
import 'antd-mobile/dist/antd-mobile.css';


const store = createStore(reducers, compose(
    applyMiddleware(thunk),
));

ReactDOM.hydrate(
    <Provider store={store}>
        <BrowserRouter>
            <Routers />
        </BrowserRouter>
    </Provider>,
    document.getElementById('root')
);

刷新頁面,可以看到跟剛才一模一樣,但是速度是之前的三倍左右(官方是這么說的)

總結

我們已經學會了React15 和 React16的服務端渲染,并可以做小小的seo優化。
大概可以總結為兩點:
1.搭建node環境,可以正式訪問到線上文件(build包)
2.使用renderToString 把骨架拼接好返回給前端
但是中間需要注意的點很多處理css jsx 圖片 babel 。

完結

覺得寫的不錯的小伙伴記得點贊+關注哦!-.-....
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容