前言
本專案原始碼地址 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
配置loaderbabel
編譯程式碼,將程式碼轉成瀏覽器可以執行的程式碼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
已經可以支援簡單的開發了,但是這還遠遠不夠,在編寫前端程式碼時,我們還會接觸到css
、image
、等其他檔案的使用,所以需要加強下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.less
和 timg.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'));
複製程式碼
最終的效果
這裡可能會有同學會有一個疑問, 圖片為什麼直接使用
<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-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
中運算元據無非兩種方式state
和props
,我們在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
,所以會產生這樣的問題。
那怎麼解決?還記得剛開始說的前後端資料同步麼?只要我把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
釋出線上環境的配置。