React服務端渲染之路04——redux-01

全部源代碼、文檔和圖片都在 github 的倉庫裏,點擊進入倉庫javascript

相關閱讀

1. redux

  • 路由完成了以後,咱們就須要考慮數據了,咱們採用使用的最爲普遍的 redux 來管理 react 的數據狀態
  • 更新 store 有三種css

    • 同步,這個同步是包括客戶端和服務端的統一更新
    • 客戶端異步,這個就是平時咱們經常使用的客戶端發送請求,異步獲取數據,而後修改 store 的值
    • 服務端異步,這個比較複雜, 放在下一節介紹
  • 因此這一節主要介紹同步更新 store 和客戶端異步更新 store

1.1 介紹

  • store 的建立分爲兩種,一種是客戶端,另一種是服務端,並且每個端的 store 都要分開,做爲一個方法調用,這樣作的目的是客戶端的話,每個用戶都有一個客戶端,使用的是本身的 store 裏的數據,可是服務端不同,不管有多少個客戶端,服務端只有一個,因此,爲了不每一個用戶的 store 數據混亂,因此咱們把服務端的 store 做爲一個方法調用,這樣,每一個用戶調用服務端 store 的時候,就有一個本身的方法,調用的是本身的數據,這樣,數據就不會混亂
  • 客戶端使用 store 的方法和平時的客戶端渲染是同樣的,沒有區別
  • 服務端使用 store 的方法也僅僅是在 StaticRouter 外邊包裹一層 Provider,而後傳入服務端的 store 便可

1.2 redux 須要使用到的庫

  • npm i redux react-redux redux-thunk redux-logger -S
  • npm i redux-devtools-extension -D
  • redux,這個就是 redux 的核心庫
  • react-redux,因爲 react 和 redux 是徹底沒有關係的,是能夠互相獨立使用的,能夠直接在 react 裏引入 redux,可是用起來比較麻煩,不太方便。因此咱們爲了方便在 react 和 redux 之間創建聯繫,因此咱們使用 react-redux
  • redux-thunk,讓 redux 在 dispatch 的時候,使用一個方法,這裏咱們用的方法主要是爲了異步獲取數據
  • redux-logger,在控制檯上顯示 state 變化的記錄
  • redux-devtools-extension,這個是一個谷歌瀏覽器上的 redux 的插件,這個插件須要咱們使用中間件開啓,纔可以查看到 state 的變化狀態

2. 使用最簡單的 redux

  • 因爲代碼比較簡單,與平時咱們客戶端使用 redux 差別很小,因此直接看代碼也能夠看明白的
  • 這裏的功能有三個html

    • 第一個是直接獲取 redux 裏的數據,獲取 user 下的 username 的值
    • 第二個是獲取完數據後,還能夠點擊按鈕,修改 user 下的 age 的值
    • 第三個是客戶端調用第三方接口,獲取數據,修改 user 下的 schoolList 的值

2.1 建立 store

  • store/index.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { composeWithDevTools } from 'redux-devtools-extension';

import reducers from './reducers';

export const getServerStore = () => createStore(
  reducers,
  composeWithDevTools(applyMiddleware(thunk, logger))
);

export const getClientStore = () => createStore(
  reducers,
  composeWithDevTools(applyMiddleware(thunk, logger))
);
  • store/reducers.js
import { combineReducers } from 'redux';

import userReducer from './user/reducer';

export default combineReducers({
  user: userReducer
});
  • store/user/actionTypes.js
export const SET_INCREMENT_AGE = 'SET_INCREMENT_AGE';

export const GET_SCHOOL_LIST = 'GET_SCHOOL_LIST';
  • store/user/createActions.jsjava

    • 關於 action 的值,有人喜歡用 payload,有人喜歡直接用須要的值的變量名,這個用什麼都行,只要先後統一便可,沒有強制性的規範。
    • 因爲在 redux-logger 裏採用的是 payload,因此建議仍是使用 payload
    • 這實際上就是一個屬性值,只要保證 actions 裏定義的和 reducer 裏獲取的是同一個就行
    • 注意:這裏的接口,是一個模擬的接口,就是下邊的 2.4 節接口服務,本身能夠簡單定義一個接口,目的是爲了作 ajax 請求響應數據
import * as Types from './actionTypes';
import axios from 'axios';

export const incrementAge = () => {
  return {
    type: Types.SET_INCREMENT_AGE
  }
};

export const getSchoolList = () => {
  return (dispatch) => {
    return axios.get('http://localhost:8758/api/getSchoolList').then(res => {
      if (res.status === 200) {
        let schoolList = res.data.schoolList;
        console.log(res.data);
        dispatch({
          type: Types.GET_SCHOOL_LIST,
          payload: schoolList
        });
      }
    });
  }
}
  • store/user/reducer.js
import * as Types from './actionTypes';

const initState = {
  name: 'mark',
  age: 18,
  schoolList: []
};

export default (state = initState, action) => {
  switch (action.type) {
    case Types.SET_INCREMENT_AGE:
      return { ...state, age: state.age + 1 };
    case Types.GET_SCHOOL_LIST:
      console.log(action);
      return { ...state, schoolList: action.payload };
    default:
      return { ...state };
  }
}

2.2 路由文件的修改

  • 之因此要修改路由文件,實際上是否修改在這裏沒什麼影響,可是在下一小節裏也是須要修改的,並且這一小節也比較簡單,因此直接放在這裏修改,避免與下一節的內容搞混亂
  • 以前咱們的路由是這麼寫的
export default (
  <>
    <Route path='/' exact component={Home}/>
    <Route path='/news' component={News}/>
  </>
);
  • 如今咱們改爲數組對象的形式,由於這樣能夠方便咱們在組件上進行異步數據加載
export default [
  {
    path: '/',
    component: Home,
    exact: true,
    key: '/'
  },
  {
    path: '/news',
    component: News,
    exact: true,
    key: '/news'
  }
];
  • 而後咱們在客戶端和服務端循環遍歷,再組裝改爲 Route 的形式,仔細看看這兩種寫法也沒啥區別,就是換了一種形式而已,爲了方便後邊咱們使用
{
  routes.map(route => <Route {...route} />)
}

2.2 客戶端下的 redux

  • client/index.js
import { Provider } from 'react-redux';
import { Route } from 'react-router-dom';
import { getClientStore } from "../store";

hydrate(
  <Provider store={getClientStore()}>
    <BrowserRouter>
      <>
        <Header/>
        <div className="container" style={{ marginTop: 70 }}>
            {
              routes.map(route => <Route {...route} />)
            }
          </div>
      </>
    </BrowserRouter>
  </Provider>, window.root);
  • containers/Home/index.jsreact

    • 關於 react-redux 的 connect 的用法,能夠把 connect 做爲組件的裝飾器使用,也能夠做爲函數直接調用使用,由於裝飾器實際上就是函數屢次調用的語法糖,因此我統一把 connect 的寫成函數調用的形式
    • connect 的參數,能夠直接把方法寫在參數裏,也能夠像這裏同樣,把 mapStateToProps 和 mapDispatchToProps 先定義成方法,而後直接把方法做爲參數
    • 關於 actions 裏方法的調用,我這裏採用的方法,實際上是有些複雜的,最簡單其實就是直接在組件內部調用 actions 裏的方法。我在這裏又在組件內部定義了一個方法 A ,在這個組件的 props 裏又定義了一個方法 B ,假如 actions 裏的方法是 C 。那麼最簡單的方法就是直接調用 this.props.C(),可是我這裏的順序是這樣的,先調用 A(),而後 A() 調用 B(),最後在 B() 裏調用 C()。具體如何調用呢,根據我的喜愛選擇。
    • 這裏呢,倒不是我雞賊,老是說怎麼用都行,實際上這個也沒有什麼標準寫法。我仍是蠻喜歡如今這種寫法的,比較清晰明瞭,傳遞參數和調用什麼的,都很方便,缺點就是代碼量多,修改的時候,改動多
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as UserActions from '../../store/user/createActions';

class Home extends Component {

  state = {
    number: 0
  };

  handleClick = () => {
    this.setState({
      number: this.state.number + 1
    });
  };

  incrementAge = () => {
    this.props.propIncrementAge();
  };

  getSchoolList = () => {
    this.props.propGetSchoolList();
  }

  render() {
    return (
      <div>
        <h2>HELLO, HOME PAGE</h2>
        <h2>
          <button className="btn btn-primary" onClick={this.handleClick}>click</button>
          &nbsp;&nbsp;&nbsp;&nbsp;
          <span>{this.state.number}</span>
        </h2>
        <ul className="list-group">
          <li className="list-group-item">name: {this.props.user.name}</li>
          <li className="list-group-item">
            <button className="btn btn-primary" onClick={this.incrementAge}>increment age</button> &nbsp;&nbsp;&nbsp;&nbsp;
            <span>{this.props.user.age}</span></li>
        </ul>
        <h2>
          <button className="btn btn-primary" onClick={this.getSchoolList}>schoolList</button>
        </h2>
        <ul className="list-group">
          {
            this.props.user.schoolList.map(school => (
              <li key={school.id} className="list-group-item">
                {school.id}. {school.name}
              </li>
            ))
          }
        </ul>
      </div>
    );
  }
}

const mapStateToProps = state => ({
  user: state.user
});

const mapDispatchToProps = dispatch => ({
  propIncrementAge() {
    dispatch(UserActions.incrementAge());
  },
  propGetSchoolList() {
    dispatch(UserActions.getSchoolList());
  }
})

export default connect(mapStateToProps, mapDispatchToProps)(Home);

2.3 服務端下的 redux

  • 服務端的 redux 在同步的狀態下,寫起來比較簡單,沒有什麼複雜的,其實就是直接把 store 傳遞給 Provider 就能夠
  • server/index.js
import { Provider } from 'react-redux';
import { Route } from 'react-router-dom';

let domContent = renderToString(
  <Provider store={getServerStore()}>
    <StaticRouter context={context} location={req.path}>
      <>
        <Header/>
        <div className="container" style={{ marginTop: 70 }}>
            {
              routes.map(route => <Route {...route} />)
            }
          </div>
      </>
    </StaticRouter>
  </Provider>
);

2.4 接口服務

  • /server/app.js,這裏已經徹底放開了跨域,暫不處理,後期要作修改調整
const express = require('express');

let app = express();
const PORT = 8758;

app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "content-type");
  res.header("Access-Control-Allow-Methods", "DELETE,PUT,POST,GET,OPTIONS");
  next();
});

app.get('/api/getSchoolList', (req, res) => {
  let schoolList = [
    { id: 1, name: '動物大學' },
    { id: 2, name: '植物大學' },
    { id: 3, name: '建築大學' },
    { id: 4, name: '服裝大學' }
  ]
  return res.json({ schoolList });
});

app.listen(PORT, err => {
  if (err) {
    console.log(err);
  } else {
    console.log(`the server is running at http://localhost:${PORT}`);
  }
});

2.4 總結

  • 總的來看,同步的 redux 和客戶端異步獲取數據,用起來實際上跟普通的客戶端渲染的時候,沒什麼大的區別,因此仍是比較簡單的
  • 複雜的是服務端異步獲取,這裏牽涉到組件的方法,promise 的包裝,脫水和注水等,咱們統一放到下一節介紹

3. 拆分 server/index.js 裏的代碼

  • 由於後邊咱們要屢次修改 server/index.js 的代碼,因此先把代碼進行拆分,拆分出一個 render.js 的文件,專門用來作渲染,而 index.js 文件只作單獨的服務
  • /server/index.js
import express from 'express';
import render from './render';

const app = express();
const PORT = 3000;

app.use(express.static('public'));

app.get('*', (req, res) => {
  render(req, res);
});

app.listen(PORT, err => {
  if (err) {
    console.log(err);
  } else {
    console.log(`Server is running at http://localhost:${PORT}`);
  }
});
  • /server/render.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Route, matchPath } from 'react-router-dom';
import { Provider } from 'react-redux';
import { getServerStore } from '../store';

import Header from './../components/Header/index';
import routes from '../routes';

export default (req, res) => {

  let context = {};

  let store = getServerStore();

  let domContent = renderToString(
      <Provider store={store}>
        <StaticRouter context={context} location={req.path}>
          <>
            <Header />
            <div className="container" style={{ marginTop: 70 }}>
              {
                routes.map(route => <Route {...route} />)
              }
            </div>
          </>
        </StaticRouter>
      </Provider>
    );
    let 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" />
  <link href="https://cdn.bootcss.com/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet">
  <title>react-ssr</title>
</head>
<body>
<div id="root">${domContent}</div>
<script>
  window.context = {
    state: ${JSON.stringify(store.getState())}
  }
</script>
<script src="/client.js"></script>
</body>
</html>
`;

    res.send(html);
};

相關閱讀

相關文章
相關標籤/搜索