使用 React + Koa 從零開始一步一步的帶你開發一個 36kr SSR 案例

zwmmm發表於2019-03-25

前言

本專案原始碼地址 github.com/zwmmm/react… 喜歡的給個star鼓勵下作者,有問題可以提issue

也許你看過其他的ssr教程都會先說一說spa和ssr的區別以及優缺點,但是我相信能點進來看的小夥伴們肯定是對這兩個概念有過了解的,也無需我在這裡多費口舌。不懂的可以直接看這裡

那麼我們就直接進入正題了!!!

搭建目錄結構

首先我們建立一個react-ssr資料夾, 執行git init初始化git倉庫,新增如下目錄和檔案。

.
|-- app
|-- build
|-- server
|-- template
|-- package.json
|-- README.md
|-- .gitignore
複製程式碼

.gitignore忽略檔案

node_modules
.cache
.idea
複製程式碼

webpack的配置

安裝webpack

npm install --save-dev webpack webpack-cli
複製程式碼

推薦使用 --save-dev 安裝,因為現在webpack版本很多,全域性安裝不利於各個專案管理。

配置react環境

首先我們明確下目標,要想執行react的程式碼,首先將react中的jsx編譯成js程式碼。

先在app下建立入口檔案main.js

|-- app
|   |-- main.js
複製程式碼

template下建立模板檔案app.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>demo</title>
</head>
<body>
    <div id="app"></div>
</body>
</html>
複製程式碼

build資料夾中建立utils.js檔案。先寫一些公共的方法。

const path = require('path');

exports.resolve = (...arg) => path.join(__dirname, '..', ...arg);
複製程式碼

build資料夾中建立webpack.base.config.js檔案

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { resolve } = require('./utils');

module.exports = {
    entry: resolve('app/main.js'),
    output: {
        path: resolve('dist'),
        filename: 'index.js'
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                // 只編譯app資料夾下的檔案
                include: resolve('app'),
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            '@babel/preset-env',
                            '@babel/preset-react',
                        ],
                    }
                }
            },
        ]
    },
    resolve: {
        // 設定路徑別名
        alias: {
            '@': resolve('app'),
        },
        // 檔案字尾自動補全, 就是你import檔案的時候如果沒寫字尾名就會優先找下面這幾個
        extensions: [ '.js', '.jsx' ],
    },
    // 第三方依賴,可以寫在這裡,不打包
    externals: {},
    plugins: [
        new HtmlWebpackPlugin({
            filename: 'index.html',
            template: resolve('template/app.html')
        })
    ]
}
複製程式碼

安裝下上面用到的包

npm i -D @babel/cli @babel/core @babel/preset-env @babel/preset-react babel-loader html-webpack-plugin
複製程式碼

簡單說下這幾個配置的作用

  • entry 指定入口
  • output 設定出口並確定輸出的檔名稱
  • rules 配置loader
  • babel 編譯程式碼,將程式碼轉成瀏覽器可以執行的程式碼
  • HtmlWebpackPlugin 自動生成html的外掛

如果不熟悉babel的同學可以看這篇文章,不過我使用了babel7 所以在包名上會有不同,新版的babel統一有@babel字首

配置好了就需要我們寫點react程式碼測試下啦

首先下載react相關的資源包

npm i --save react react-dom
複製程式碼

app/main.js編寫如下程式碼

import React from 'react';
import { render } from 'react-dom';

function App() {
    return <div>Hello React</div>
}

render(<App/>, document.getElementById('app'));
複製程式碼

package.json中增加一條script命令

{
  "scripts": {
    "start": "webpack --config build/webpack.base.config.js"
  },
}
複製程式碼

執行npm start 開啟dist/index.html就可以檢視效果,正確情況下會顯示Hello React

到此我們就已經完成我們的第一階段,可以編寫react程式碼

配置開發環境

上面我們說了如何編譯react程式碼,但是在我們實際開發中不可能每次修改程式碼都要npm start,所以在上面的基礎上配置一個dev環境

在配置dev環境之前先介紹下webpack-dev-server,這個外掛可以在本地啟動一個本地服務,並且提供了非常豐富的功能,例如熱更新,介面代理。首先我們安裝下

npm i -D webpack-dev-server
複製程式碼

build下新建webpack.dev.config.js

const merge = require('webpack-merge');
const webpack = require('webpack');
const baseConfig = require('./webpack.base.config');

module.exports = merge(baseConfig, {
    // 用於除錯, inline-source-map模式效率比較高, 所以在dev模式下推薦使用這個
    devtool: 'inline-source-map',
    mode: 'development',
    // 設定dev伺服器
    devServer: {
        // 設定埠號,預設8080
        port: 8000,
    },
    plugins: [
        // 在js中注入全域性變數process.env用來區分環境
        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: JSON.stringify('development'),
            }
        }),
    ],
})
複製程式碼

安裝下webpack-merge

npm i -D webpack-merge
複製程式碼

簡單說下上面的配置

  • 使用webpack-merge複用之前的配置
  • 配置devServer
  • 注入process.env全域性變數區分環境

最後我們在修改下啟動命令

{
  "scripts": {
    "start": "webpack-dev-server --hot --config build/webpack.dev.config.js"
  },
}
複製程式碼

現在我們執行下npm start 瀏覽器開啟localhost:8000訪問,並嘗試修改main.js中的react程式碼,不重新整理瀏覽器是否會自動更新

現在我們的webpack已經可以支援簡單的開發了,但是這還遠遠不夠,在編寫前端程式碼時,我們還會接觸到cssimage、等其他檔案的使用,所以需要加強下webpack的配置

    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                // 只編譯app資料夾下的檔案
                include: resolve('app'),
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            '@babel/preset-env',
                            '@babel/preset-react',
                        ],
                    }
                }
            },
+           {
+               test: /\.html$/,
+               include: resolve('app'),
+               loader: 'html-loader'
+           },
+           {
+               test: /\.less/,
+               include: resolve('app'),
+               use: [
+                   'style-loader',
+                   'css-loader',
+                   'less-loader'
+               ]
+           },
+           {
+               test: /\.(png|jpg|gif|svg)$/,
+               loader: `url-loader?limit=1000`
+           },
+           {
+               test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
+               loader: `file-loader`
+           },
+       ]
    },
複製程式碼

下載需要的loader以及less

npm i -D html-loader style-loader css-loader less-loader url-loader file-loader less
複製程式碼

經過下面的配置我們就可以在程式碼中做如下的操作

import img from './xxx.png'
import 'xxx.less'
import html from 'xxx.html'
複製程式碼

那麼接下來我們就給我們的react豐富一下程式碼

首先在app資料夾下新建style static資料夾分別存放css檔案和靜態資源,

新增index.lesstimg.png

#app {
    text-align: center;
    color: deepskyblue;
}
.logo {
    width: 500px;
}
複製程式碼

然後修改main.js

import React from 'react';
import { render } from 'react-dom';
import './style/index.less';
import logo from './static/timg.jpg'

function App() {
    return <div>
        <h1>Hello React !!!</h1>
        <img src={ logo } className="logo"/>
    </div>
}

render(<App/>, document.getElementById('app'));
複製程式碼

最終的效果

使用 React + Koa 從零開始一步一步的帶你開發一個 36kr SSR 案例

這裡可能會有同學會有一個疑問, 圖片為什麼直接使用<img src="./static/time.png" className="logo"/>這樣引入?其實很好解釋,我們的網站是訪問的webpack-dev-server啟動的服務,如果沒有使用import引入圖片,則在伺服器中就不會存在這個圖片。而import圖片的時候 首先會找到對應的圖片資源存到伺服器上, 並且生成一個檔案路徑供我們訪問。

使用Koa搭建Node服務

react的部分我們先告一段落,後面還會繼續說到react-router redux,接下來我們說下服務端,也算是正式講點ssr的東西

首先在這裡提一嘴,ssr和普通的spa頁面最大的區別在於,我們是直接將完整的html返回給瀏覽器的。

話不多說,直接開工!!!

先下載koa

npm i -S koa
複製程式碼

建立server/app.js檔案

const Koa = require('koa');

const app = new Koa();

app.use(ctx => {
    ctx.body = '<div>Hello Koa<div/>'
})

app.listen(9000, () => {
    console.log(`node服務已經啟動, 請訪問localhost:9000`)
})
複製程式碼

新增一條script命令

"server": "node server/app.js"
複製程式碼

執行npm run server並訪問localhost:9000

這時候就可以看到Hello Koa,其實這就是一個最基本的直出服務,現在讓我們想一想,如果程式碼可以寫成這樣

    app.use(ctx => {
-       ctx.body = '<div>Hello Koa<div/>'
+       ctx.body = <App/>
    })
複製程式碼

直接返回一個react元件,那不就是我們要的react ssr?

當然上面的程式碼直接這麼執行肯定是會報錯,不過react給我們提供了renderToString方法,將元件轉成字串。這樣我們就可以實現渲染元件了!!!

來,我們改良下上面的程式碼,讓node支援jsx語法

先建立server/index.js,使用@babel/register在node執行時候編譯我們的jsx程式碼以及es6語法

安裝@babel/register

npm i -S @babel/register
複製程式碼
require('@babel/register')({
    presets: [
        '@babel/preset-react',
        '@babel/preset-env'
    ],
});
require('./app.js');
複製程式碼

修改script命令

- "server": "node server/app.js"
+ "server": "node server/index.js"
複製程式碼

重構app.js

因為前面使用了babel編譯了程式碼,所以可以使用es6的模組化

// jsx編譯之後會用到React物件, 所以需要引入
import React from 'react';
import Koa from 'koa';
import { renderToString } from "react-dom/server";

const app = new Koa();

const App = () => <div>Hello Koa SSR</div>

app.use(ctx => {
    ctx.body = renderToString(<App/>);
})

app.listen(9000, () => {
    console.log(`node服務已經啟動, 請訪問localhost:9000`)
})
複製程式碼

現在我們已經完成了最簡單的react ssr,下一步我們將加上路由,實現對應的路由顯示對應的元件

SSR下的路由

看完上面的章節,大夥是不是想說,ssr是實現了,但是好像和我得前端部分並沒有關聯起來啊,我在前端寫的元件應該怎麼在Node中去使用呢?下面我在路由這個篇章就會將前端和Node關聯起來講,讓大家知道頁面到底是怎麼渲染出來的。

在開始講之前我還是得先和大家說說傳統的spa頁面路由是怎麼配置的,下面就以history模式為例

首先我們從瀏覽器輸入url,不管你的url是匹配的哪個路由,後端統統都給你index.html,然後載入js匹配對應的路由元件,渲染對應的路由。

那我們的ssr路由是怎麼樣的模式呢?

首先我們從瀏覽器輸入url,後端匹配對應的路由獲取到對應的路由元件,獲取對應的資料填充路由元件,將元件轉成html返回給瀏覽器,瀏覽器直接渲染。當這個時候如果你在頁面中點選跳轉,我們依舊還是不會傳送請求,由js匹配對應的路由渲染

文字看懵的我們直接看圖

使用 React + Koa 從零開始一步一步的帶你開發一個 36kr SSR 案例

所以我們需要同時配置前端路由以及後端路由

那一步步來,我們先配置前端路由,前端路由使用react-router,如果不會使用react-router的同學可以看下我寫的這篇入門文章

下載react-router

npm i -S react-router-dom
複製程式碼

新建app/router.js

import { Link, Switch, Route } from 'react-router-dom';
import React from 'react';

const Home = () => (
    <div>
        <h1>首頁</h1>
        <Link to="/list">跳轉列表頁</Link>
    </div>
)

const list = [
    'react真好玩',
    'koa有點意思',
    'ssr更有意思'
]

const List = () => (
    <ul>
        { list.map((item, i) => <li key={ i }>{ item }</li>) }
    </ul>
)

export default () => (
    <Switch>
        <Route exact path="/" component={ Home }/>
        <Route exact path="/list" component={ List }/>
    </Switch>
)
複製程式碼

修改main.js

import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import Router from './router'

render(
    <BrowserRouter>
        <Router/>
    </BrowserRouter>,
    document.getElementById('app')
);
複製程式碼

執行npm start 訪問localhost:8000

ok,前端路由就這麼簡單的配置好了,現在如果你跳轉到列表頁,然後重新整理頁面就會提示404這是因為我們的dev-server沒有匹配上對應的路由,那麼接下來我們就來配置服務端路由來解決這個問題,並且實現ssr

服務端路由我們使用koa-router

先下載 npm i -S koa-router

新建server/router/index.js

import Router from 'koa-router';
import RouterConfig from '../../app/router';
import { StaticRouter } from 'react-router-dom';
import { renderToString } from "react-dom/server";
import React from 'react';

const routes = new Router();

routes.get('/', (ctx, next) => {
    ctx.body = renderToString(
        <StaticRouter location={ctx.url}>
            <RouterConfig/>
        </StaticRouter>
    )
    next();
})

routes.get('/list', (ctx, next) => {
    ctx.body = renderToString(
        <StaticRouter location={ctx.url}>
            <RouterConfig/>
        </StaticRouter>
    )
    next();
})

export default routes;
複製程式碼

一下看不懂沒關係,聽我來解釋

首先我們用koa-router註冊了/ /list 兩個路由,並且使用renderToString將元件轉成html

那這個StaticRouter是幹嘛的呢?和BrowserRouter有什麼區別?其實很簡單,在瀏覽器上我們可以使用js獲取到location,但是在node環境卻獲取不到,所以react-router提供了StaticRouter來讓我們自己設定location

現在你也許會有另外一個疑問,這兩個路由設定寫的程式碼不是都一樣的麼,為什麼還要去區分路由?這是應為在生成html之前我們還需要獲取對應的資料,所以必須要分開。後面我會繼續講ssr如何處理資料

接下來我們改造下app.js

import Koa from 'koa';
import routes from './router';

const app = new Koa();

app.use(routes.routes(), routes.allowedMethods());

app.listen(9000, () => {
    console.log(`node服務已經啟動, 請訪問localhost:9000`)
})
複製程式碼

啟動npm run server 訪問localhost:9000

現在我們的localhost:9000 localhost:8000 都可以瀏覽了,正好你們可以對比下兩種渲染方式。

ok,心細的朋友可能發現了localhost:9000下的頁面點選跳轉是重新整理頁面的,並不是單頁面跳轉。這是因為我們返回的html裡面根本就沒有攜帶js,所以跳轉路由當然是直接發生跳轉了啊,並且返回的html也是不完整的,現在我們就給我們的內容新增一個html模板

新建模板template/server.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>36氪_讓一部分人先看到未來</title>
    <link href="//36kr.com/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon">
</head>
<body>
    <div id="app">{{ html }}</div>
    <script src="http://localhost:8000/index.js"></script>
</body>
</html>
複製程式碼

這裡我們載入localhost:8000服務下的inedx.js,其實你可以吧webpack-dev-server想象成靜態資源伺服器了,這樣我們的靜態資源在你的開發階段就可以實時更新。

然後我們給ctx物件擴充套件一個render方法,用來渲染html

import fs from 'fs';
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom'
import RouterConfig from '../app/router'
import React from 'react';
import path from 'path';

// 匹配模板中的{{}}
function templating(props) {
    const template = fs.readFileSync(path.join(__dirname, '../template/server.html'), 'utf-8');
    return template.replace(/{{([\s\S]*?)}}/g, (_, key) => props[ key.trim() ]);
}

export default function(ctx, next) {
    try {
        ctx.render = () => {
            const html = renderToString(
                <StaticRouter location={ ctx.url }>
                    <RouterConfig/>
                </StaticRouter>
            );
            const body = templating({
                html
            });
            ctx.body = body;
        }
    }
    catch (err) {
        ctx.body = templating({ html: err.message });
    }
    ctx.type = 'text/html';
    // 這裡必須是return next() 不然非同步路由是404
    return next();
}
複製程式碼

然後在app.js中載入上面寫的中介軟體

    import Koa from 'koa';
    import routes from './router';
+   import templating from './templating'
    
    const app = new Koa();
    
+   app.use(templating);
    app.use(routes.routes(), routes.allowedMethods());
    
    app.listen(9000, () => {
        console.log(`node服務已經啟動, 請訪問localhost:9000`)
    })
複製程式碼

最後我們來改造下路由

import Router from 'koa-router';
import React from 'react';

const routes = new Router();

routes.get('/', (ctx, next) => {
    ctx.render();
    next();
})

routes.get('/list', (ctx, next) => {
    ctx.render();
    next();
})

export default routes;
複製程式碼

重啟你的localhost:9000看看現在跳轉list是不是就不會再重新整理頁面了。

到這裡我們的路由就算配置完成了。相信大家對ssr也有一定的瞭解了,但是還不夠,目前我們渲染的都是靜態頁面,也就是寫死的,而實際業務肯定是根據資料渲染出來的,之前的spa頁面我們會在元件中去傳送請求獲取資料渲染,但我們的ssr肯定不能這樣做,所以得在生成html這一步獲取資料,那資料又該怎麼傳進元件內呢?以及前後端資料怎麼做到同步呢?下一個章節我們就講講ssr的資料請求

SSR中的資料請求

react中運算元據無非兩種方式stateprops,我們在node中肯定是沒辦法給元件設定state的,所以只能通過props傳進去,並且我們的資料還要做到前後端同步,不然你就光渲染出了html,資料沒給前端這樣也不行啊。而redux剛好滿足這兩點需求。

既然要用redux那就得先從前端開始了啊,不熟悉redux的朋友建議先了解下基本概念

下載npm i redux react-redux -S

新建目錄

|-- app
|   |-- redux
|   |   |-- reducers
|   |   |-- store
複製程式碼

先建立reducers

// reducers/home.js
const defaultState = {
    title: 'Hello Redux'
}

export default function(state  = defaultState , action) {
    switch (action.type) {
        default:
            return state
    }
}
複製程式碼
// reducers/list.js
const defaultState = {
    list: [
        'react真好玩',
        'koa有點意思',
        'ssr更有意思'
    ]
}

export default function(state  = defaultState , action) {
    switch (action.type) {
        default:
            return state
    }
}
複製程式碼

合併reducers

// reducers/index.js
import home from './home';
import list from './list';
import { combineReducers  } from 'redux';

// 其實就是把分散的reducers給合併了
export default combineReducers({
    home,
    list,
})
複製程式碼

接下來建立store

import { createStore } from 'redux';
import reducers from '../reducers';

/**
 * 為什麼寫成函式?
 * 因為我們在前端和後端都需要去進行初始化store所以這裡封裝一個工廠函式
 * @param data
 * @returns {*}
 */
export default data => createStore(reducers, data);
複製程式碼

然後將store注入到元件中

// main.js
+ import { Provider } from 'react-redux';
+ import createStore from './redux/store/create';

+ const store = createStore();

render(
+   <Provider store={store}>
        <BrowserRouter>
            <Router/>
        </BrowserRouter>
+   </Provider>,
    document.getElementById('app')
);
複製程式碼

page從路由中抽離出來

// pages/home.js
import { Link } from 'react-router-dom';
import React from 'react';
import { connect } from 'react-redux';

const Home = props => (
    <div>
        <h1>{ props.title }</h1>
        <Link to="/list">跳轉列表頁</Link>
    </div>
)

/**
 * 通過connect將redux中的資料傳遞進入元件
 */
function mapStateTpProps(state) {
    return { ...state.home };
}

export default connect(mapStateTpProps)(Home)
複製程式碼
// pages/list.js
import React from 'react';
import { connect } from 'react-redux';

const List = props => (
    <ul>
        { props.list.map((item, i) => <li key={ i }>{ item }</li>) }
    </ul>
)

/**
 * 通過connect將redux中的資料傳遞進入元件
 */
function mapStateTpProps(state) {
    return { ...state.list };
}

export default connect(mapStateTpProps)(List)
複製程式碼

最後修改下路由

import { Switch, Route } from 'react-router-dom';
import React from 'react';
import Home from './pages/home';
import List from './pages/list';

export default () => (
    <Switch>
        <Route exact path="/" component={ Home }/>
        <Route exact path="/list" component={ List }/>
    </Switch>
)
複製程式碼

好了,最基本的redux已經完成,現在我們已經將資料從元件內部提取到了redux來管理,接下來我們實現在node中填充資料。

其實這一步非常簡單,只要修改下templating就可以,直接看程式碼

import fs from 'fs';
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom'
import RouterConfig from '../app/router'
import React from 'react';
import path from 'path';
+ import { Provider } from 'react-redux';
+ import createStore from '../app/redux/store/create';

// 匹配模板中的{{}}
function templating(props) {
    const template = fs.readFileSync(path.join(__dirname, '../template/server.html'), 'utf-8');
    return template.replace(/{{([\s\S]*?)}}/g, (_, key) => props[ key.trim() ]);
}

export default function(ctx, next) {
    try {
+       ctx.render = (data = {}) => {
+           const store = createStore(data);
            const html = renderToString(
+               <Provider store={ store }>
                    <StaticRouter location={ ctx.url }>
                        <RouterConfig/>
                    </StaticRouter>
+               </Provider>
            );
            const body = templating({
                html
            });
            ctx.body = body;
        }
    }
    catch (err) {
        ctx.body = templating({ html: err.message });
    }
    ctx.type = 'text/html';
    // 這裡必須是return next() 不然非同步路由是404
    return next();
}
複製程式碼

然後我們在呼叫ctx.render的時候將資料當做引數傳入就可以了

import Router from 'koa-router';
import React from 'react';

const routes = new Router();

routes.get('/', (ctx, next) => {
    ctx.render({
        home: {
            title: '我是從node中獲取的資料'
        }
    });
    next();
})

routes.get('/list', (ctx, next) => {
    ctx.render({
        list: {
            list: [
                '我是從node中獲取的資料',
                '感覺還不錯',
                '測試成功',
            ]
        }
    });
    next();
})

export default routes;
複製程式碼

重啟npm run server 重新整理下localhost:9000看看效果

誒,不對啊,是不是看到了,頁面一開始是正確的,然後又被重新覆蓋了?這是因為我們載入了index.js他又重新初始化store,所以會產生這樣的問題。

使用 React + Koa 從零開始一步一步的帶你開發一個 36kr SSR 案例

那怎麼解決?還記得剛開始說的前後端資料同步麼?只要我把node用到的資料傳給前端,前端基於這個資料去初始化store這樣不就可以了?

怎麼把資料傳給前端?很簡單,直接把store注入到window上就行。

先修改下我們的模板server.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>36氪_讓一部分人先看到未來</title>
    <link href="//36kr.com/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon">
</head>
<body>
<div id="app">{{ html }}</div>
+ <script>
+ window.__STORE__ = {{ store }}
+ </script>
<script src="http://localhost:8000/index.js"></script>
</body>
</html>
複製程式碼

改下templating

ctx.render = (data = {}) => {
    const store = createStore(data);
    const html = renderToString(
        <Provider store={ store }>
            <StaticRouter location={ ctx.url }>
                <RouterConfig/>
            </StaticRouter>
        </Provider>
    );
    const body = templating({
        html,
+       store: JSON.stringify(data, null, 4),
    });
    ctx.body = body;
}
複製程式碼

最後前端獲取store

+ const defaultStore = window.__STORE__ || {}
- const store = createStore();
+ const store = createStore(defaultStore);

render(
    <Provider store={store}>
        <BrowserRouter>
            <Router/>
        </BrowserRouter>
    </Provider>,
    document.getElementById('app')
);
複製程式碼

重啟npm run server 重新整理下localhost:9000是不是完美了

最後補充一點關於api請求的點

因為一個頁面可能是由node直出的,也有可能是js載入的,所以我們還需要在每個元件的componentDidMount中去分析有沒有事先注入過store,來判斷是否需要請求,如下面的虛擬碼。

componentDidMount() {
    const { news, fetchHome } = this.props;
    news.length || fetchHome();
}
複製程式碼

其實到這裡我們的ssr實現原理已經講完了,接下來的章節我會帶大家完成一個36kr的案例,想自己動手直接開擼的同學也可以直接看我的react-ssr-36kr原始碼,那如果你對redux以及koa不是很熟悉的同學則可以繼續看我的下篇文章,下篇文章會帶大家進行實戰開發以及build釋出線上環境的配置。

相關文章