React Demo Three - 簡書&掘金

jsliang發表於2019-04-23

Create by jsliang on 2019-4-7 19:37:41
Recently revised in 2019-04-23 09:40:44

Hello 小夥伴們,如果覺得本文還不錯,記得給個 star , 小夥伴們的 star 是我持續更新的動力!GitHub 地址

本文章最終成果:

React Demo Three - 簡書&掘金

本來這只是篇純粹的仿簡書首頁和文章詳情頁的文章,但是中間出了點情況(第十九章有提到),所以最終出來的是簡書和掘金的混合體~

一 目錄

不折騰的前端,和鹹魚有什麼區別

目錄
一 目錄
二 前言
三 初始化專案目錄
四 建立 React 頭部元件
五 編寫簡書頭部導航
六 設定輸入框動畫
七 優化程式碼
八 使用 redux-devtools-extension 外掛
九 優化:抽取 reducer.js
十 優化:抽取 action
十一 優化:immutable.js
十二 優化:redux-immutable
十三 功能實現:熱門搜尋
十四 程式碼優化
十五 解決歷史遺留問題
十六 功能實現:換一換
十七 功能優化
17.1 換一換圖示旋轉
17.2 避免聚焦重複請求
十八 React 路由
18.1 路由(一)
18.2 路由(二)
十九 頁面實現:二級導航欄
二十 頁面實現:首頁
20.1 多層級元件引用 store
20.2 完善整個首頁
二十一 總結

二 前言

返回目錄

歲月如梭,光陰荏苒。

既然決定了做某事,那就堅持下去。

相信,堅持必定有收穫,不管它體現在哪個方面。

React 的學習,邁開 TodoList,進一步前行。

三 初始化專案目錄

返回目錄

首先,引入 Simplify 目錄的內容到 JianShu 資料夾。或者前往文章 《React Demo One - TodoList》 手動進行專案簡化。

我們的最終目錄如下所示:

React Demo Three - 簡書&掘金

小夥伴們可以自行新建空檔案,在後續不會因為不知道該檔案放到哪,從而導致思路錯亂。

然後,我們通過:

  • 安裝依賴:npm i
  • 執行專案:npm run start

跑起專案來,執行結果如下所示:

React Demo Three - 簡書&掘金

接著,我們在 src 目錄下引入 reset.css,去除各種瀏覽器的差異性影響。

src/reset.css

程式碼詳情
/* 
  * reset 的目的不是讓預設樣式在所有瀏覽器下一致,而是減少預設樣式有可能帶來的問題。
  * The purpose of reset is not to allow default styles to be consistent across all browsers, but to reduce the potential problems of default styles.
  * create by jsliang
*/

/** 清除內外邊距 - clearance of inner and outer margins **/
body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* 結構元素 - structural elements */
dl, dt, dd, ul, ol, li, /* 列表元素 - list elements */
pre, /* 文字格式元素 - text formatting elements */
form, fieldset, legend, button, input, textarea, /* 表單元素 - from elements */
th, td /* 表格元素 - table elements */ {
    margin: 0;
    padding: 0;
}

/** 設定預設字型 - setting the default font **/
body, button, input, select, textarea {
    font: 18px/1.5 '黑體', Helvetica, sans-serif;
}
h1, h2, h3, h4, h5, h6, button, input, select, textarea { font-size: 100%; }

/** 重置列表元素 - reset the list element **/
ul, ol { list-style: none; }

/** 重置文字格式元素 - reset the text format element **/
a, a:hover { text-decoration: none; }

/** 重置表單元素 - reset the form element **/
button { cursor: pointer; }
input { font-size: 18px; outline: none; }

/** 重置表格元素 - reset the table element **/
table { border-collapse: collapse; border-spacing: 0; }

/*
  * 圖片自適應 - image responsize 
  * 1. 清空瀏覽器對圖片的設定
  * 2. <div>圖片</div> 的情況下,圖片會撐高 div,這麼設定可以清除該影響
*/
img { border: 0; display: inline-block; width: 100%; max-width: 100%; height: auto; vertical-align: middle; }

/* 
  * 預設box-sizing是content-box,該屬性導致padding會撐大div,使用border-box可以解決該問題
  * set border-box for box-sizing when you use div, it solve the problem when you add padding and don't want to make the div width bigger
*/
div, input { box-sizing: border-box; }

/** 清除浮動 - clear float **/
.jsliang-clear:after, .clear:after {
    content: '\20';
    display: block;
    height: 0;
    clear: both;
}
.jsliang-clear, .clear {
    *zoom: 1;
}

/** 設定input的placeholder - set input placeholder **/
input::-webkit-input-placeholder { color: #919191; font-size: 1em } /* Webkit browsers */
input::-moz-placeholder { color: #919191; font-size: 1em } /* Mozilla Firefox */
input::-ms-input-placeholder { color: #919191; font-size: 1em } /* Internet Explorer */
複製程式碼

順帶建立一個空的全域性樣式 index.css 檔案。

並在 index.js 中引入 reset.css 和 index.css。

src/index.js

程式碼詳情
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './reset.css';
import './index.css';

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

四 建立 React 頭部元件

返回目錄

首先,在 src 目錄下,新建 common 目錄,並在 common 目錄下,新建 header 目錄,其中的 index.js 內容如下:

src/common/header/index.js

程式碼詳情
import React, { Component } from 'react';

class Header extends Component {
  render() {
    return (
      <div>
        <h1>Header</h1>
      </div>
    )
  }
}

export default Header;
複製程式碼

然後,我們在 App.js 中引入 header.js:

src/App.js

程式碼詳情
import React, { Component } from 'react';
import Header from './common/header';

class App extends Component {
  render() {
    return (
      <div className="App">
        <Header />
      </div>
    );
  }
}

export default App;
複製程式碼

最後,頁面顯示為:

React Demo Three - 簡書&掘金

由此,我們完成了 Header 元件的建立。

五 編寫簡書頭部導航

返回目錄

首先,我們編寫 src/common/header 下的 index.js:

src/common/heder/index.js

程式碼詳情
import React, { Component } from 'react';
import './index.css';

import homeImage from '../../resources/img/header-home.png';
class Header extends Component {

  constructor(props) {
    super(props);
    this.state = {
      inputFocus: true
    }
    this.searchFocusOrBlur = this.searchFocusOrBlur.bind(this);
  }
  
  render() {
    return (
      <header>
        <div className="header_left">
          <a href="/">
            <img alt="首頁" src={homeImage} className="headef_left-img" />
          </a>
        </div>
        <div className="header_center">
          <div className="header_center-left">
            <div className="nav-item header_center-left-home">
              <i className="icon icon-home"></i>
              <span>首頁</span>
            </div>
            <div className="nav-item header_center-left-download">
              <i className="icon icon-download"></i>
              <span>下載App</span>
            </div>
            <div className="nav-item header_center-left-search">
              <input 
                className={this.state.inputFocus ? 'input-nor-active' : 'input-active'}
                placeholder="搜尋"
                onFocus={this.searchFocusOrBlur}
                onBlur={this.searchFocusOrBlur}
              />
              <i className={this.state.inputFocus ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
            </div>
          </div>
          <div className="header_center-right">
            <div className="nav-item header_right-center-setting">
              <span>Aa</span>
            </div>
            <div className="nav-item header_right-center-login">
              <span>登入</span>
            </div>
          </div>
        </div>
        <div className="header_right nav-item">
          <span className="header_right-register">註冊</span>
          <span className="header_right-write nav-item">
            <i className="icon icon-write"></i>
            <span>寫文章</span>
          </span>
        </div>
      </header>
    )
  }

  searchFocusOrBlur(e) {
    const inputFocus = this.state.inputFocus;
    this.setState( () => ({
      inputFocus: !inputFocus
    }))
  }

}

export default Header;
複製程式碼

然後,我們新增 CSS 樣式:

src/common/heder/index.css

程式碼詳情
header {
  width: 100%;
  height: 58px;
  display: flex;
  align-items: center;
  border-bottom: 1px solid #ccc;
  font-size: 17px;
}
.headef_left-img {
  width: 100px;
  height: 56px;
}
.header_center {
  width: 1000px;
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
}
.nav-item {
  margin-right: 30px;
  display: flex;
  align-items: center;
}
.header_center-left {
  display: flex;
}
.header_center-left-home {
  color: #ea6f5a;
}
.header_center-left-search {
  position: relative;
}
.header_center-left-search input {
  width: 240px;
  padding: 0 40px 0 20px;
  height: 38px;
  font-size: 14px;
  border: 1px solid #eee;
  border-radius: 40px;
  background: #eee;
}
.header_center-left-search .input-active {
  width: 280px;
}
.header_center-left-search i {
  position: absolute;
  top: 8px;
  right: 10px;
}
.header_center-left-search .icon-active {
  padding: 3px;
  top: 4px;
  border-radius: 15px;
  border: 1px solid #ea6f5a;
}
.header_center-left-search .icon-active:hover {
  cursor: pointer;
}
.header_center-right {
  display: flex;
  color: #969696;
}
.header_right-register, .header_right-write {
  width: 80px;
  text-align: center;
  height: 38px;
  line-height: 38px;
  border: 1px solid rgba(236,97,73,.7);
  border-radius: 20px;
  font-size: 15px;
  color: #ea6f5a;
  background-color: transparent;
}
.header_right-write {
  margin-left: 10px;
  padding-left: 10px;
  margin-right: 0px;
  color: #fff;
  background-color: #ea6f5a;
}
複製程式碼

接著,由於圖示這些,我們可以抽取到公用樣式表中,所以我們在 src 目錄下新增 common.css:

src/common.css

程式碼詳情
.icon {
  display: inline-block;
  width: 20px;
  height: 21px;
  margin-right: 5px;
}
.icon-home {
  background: url('./resources/img/icon-home.png') no-repeat center;
  background-size: 100%;
}
.icon-write {
  background: url('./resources/img/icon-write.png') no-repeat center;
  background-size: 100%;
}
.icon-download {
  background: url('./resources/img/icon-download.png') no-repeat center;
  background-size: 100%;
}
.icon-search {
  background: url('./resources/img/icon-search.png') no-repeat center;
  background-size: 100%;
}
複製程式碼

當然,我們需要位置存放圖片,所以需要在 src 目錄下,新建 recourses 目錄,recourses 目錄下存放 img 資料夾,該資料夾存放這些圖示檔案。

最後,我們在 src 下的 index.js 中引用 common.css

src/index.js

程式碼詳情
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './reset.css';
import './index.css';
import './common.css';

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

至此,我們頁面展示為:

React Demo Three - 簡書&掘金

六 設定輸入框動畫

返回目錄

參考地址:react-transition-group

  • 安裝動畫庫:npm i react-transition-group -S

修改程式碼:

src/common/header/index.js

程式碼詳情
import React, { Component } from 'react';
// 1. 引入動畫庫
import { CSSTransition } from 'react-transition-group';
import './index.css';

import homeImage from '../../resources/img/header-home.png';

class Header extends Component {

  constructor(props) {
    super(props);
    this.state = {
      inputBlur: true
    }
    this.searchFocusOrBlur = this.searchFocusOrBlur.bind(this);
  }

  render() {
    return (
      <header>
        <div className="header_left">
          <a href="/">
            <img alt="首頁" src={homeImage} className="headef_left-img" />
          </a>
        </div>
        <div className="header_center">
          <div className="header_center-left">
            <div className="nav-item header_center-left-home">
              <i className="icon icon-home"></i>
              <span>首頁</span>
            </div>
            <div className="nav-item header_center-left-download">
              <i className="icon icon-download"></i>
              <span>下載App</span>
            </div>
            <div className="nav-item header_center-left-search">
              {/* 2. 通過 CSSTransition 包裹 input */}
              <CSSTransition
                in={this.state.inputBlur}
                timeout={200}
                classNames="slide"
              >
                <input 
                  className={this.state.inputBlur ? 'input-nor-active' : 'input-active'}
                  placeholder="搜尋"
                  onFocus={this.searchFocusOrBlur}
                  onBlur={this.searchFocusOrBlur}
                />
              </CSSTransition>
              <i className={this.state.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
            </div>
          </div>
          <div className="header_center-right">
            <div className="nav-item header_right-center-setting">
              <span>Aa</span>
            </div>
            <div className="nav-item header_right-center-login">
              <span>登入</span>
            </div>
          </div>
        </div>
        <div className="header_right nav-item">
          <span className="header_right-register">註冊</span>
          <span className="header_right-write nav-item">
            <i className="icon icon-write"></i>
            <span>寫文章</span>
          </span>
        </div>
      </header>
    )
  }

  searchFocusOrBlur(e) {
    const inputBlur = this.state.inputBlur;
    this.setState( () => ({
      inputBlur: !inputBlur
    }))
  }

}

export default Header;
複製程式碼

src/common/header/index.css

程式碼詳情
header {
  width: 100%;
  height: 58px;
  display: flex;
  align-items: center;
  border-bottom: 1px solid #ccc;
  font-size: 17px;
}
.headef_left-img {
  width: 100px;
  height: 56px;
}
.header_center {
  width: 1000px;
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
}
.nav-item {
  margin-right: 30px;
  display: flex;
  align-items: center;
}
.header_center-left {
  display: flex;
}
.header_center-left-home {
  color: #ea6f5a;
}
.header_center-left-search {
  position: relative;
}
/* 3. 編寫對應的 CSS 樣式 */
.slide-enter {
  transition: all .2s ease-out;
}
.slide-enter-active {
  width: 280px;
}
.slide-exit {
  transition: all .2s ease-out;
}
.silde-exit-active {
  width: 240px;
}
/* 3. 結束 */
.header_center-left-search input {
  width: 240px;
  padding: 0 40px 0 20px;
  height: 38px;
  font-size: 14px;
  border: 1px solid #eee;
  border-radius: 40px;
  background: #eee;
}
.header_center-left-search .input-active {
  width: 280px;
}
.header_center-left-search i {
  position: absolute;
  top: 8px;
  right: 10px;
}
.header_center-left-search .icon-active {
  padding: 3px;
  top: 4px;
  border-radius: 15px;
  border: 1px solid #ea6f5a;
}
.header_center-left-search .icon-active:hover {
  cursor: pointer;
}
.header_center-right {
  display: flex;
  color: #969696;
}
.header_right-register, .header_right-write {
  width: 80px;
  text-align: center;
  height: 38px;
  line-height: 38px;
  border: 1px solid rgba(236,97,73,.7);
  border-radius: 20px;
  font-size: 15px;
  color: #ea6f5a;
  background-color: transparent;
}
.header_right-write {
  margin-left: 10px;
  padding-left: 10px;
  margin-right: 0px;
  color: #fff;
  background-color: #ea6f5a;
}
複製程式碼

這樣,經過四個操作步驟:

  1. 安裝動畫庫:npm i react-transition-group -S
  2. 引入動畫庫
  3. 通過 CSSTransition 包裹 input
  4. 編寫對應的 CSS 樣式

我們就成功實現了 CSS 動畫外掛的引入及使用,此時頁面顯示為:

React Demo Three - 簡書&掘金

七 優化程式碼

返回目錄

  • 安裝 Redux:npm i redux -S
  • 安裝 React-Redux:npm i react-redux -S
  • 開始在程式碼中加入 Redux 和 React-Redux
  1. 首先,建立 store 資料夾,並在裡面建立 index.js 和 reducer.js:

src/store/index.js

程式碼詳情
import { createStore } from 'redux';
import reducer from './reducer';

const store = createStore(reducer);

export default store;
複製程式碼

src/store/reducer.js

程式碼詳情
const defaultState = {
  inputBlur: true
};

export default (state = defaultState, action) => {
  return state;
}
複製程式碼
  1. 接著,在 App.js 中引用 react-redux 以及 store/index.js:

src/App.js

程式碼詳情
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import Header from './common/header';
import store from './store';

class App extends Component {
  render() {
    return (
      <Provider store={store} className="App">
        <Header />
      </Provider>
    );
  }
}

export default App;
複製程式碼
  1. 然後,修改 src 下 common 中 header 裡面 index.js 中的內容:

src/common/header/index.js

程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';

import homeImage from '../../resources/img/header-home.png';

class Header extends Component {
  render() {
    return (
      <header>
        <div className="header_left">
          <a href="/">
            <img alt="首頁" src={homeImage} className="headef_left-img" />
          </a>
        </div>
        <div className="header_center">
          <div className="header_center-left">
            <div className="nav-item header_center-left-home">
              <i className="icon icon-home"></i>
              <span>首頁</span>
            </div>
            <div className="nav-item header_center-left-download">
              <i className="icon icon-download"></i>
              <span>下載App</span>
            </div>
            <div className="nav-item header_center-left-search">
              <CSSTransition
                in={this.props.inputBlur}
                timeout={200}
                classNames="slide"
              >
                <input 
                  className={this.props.inputBlur ? 'input-nor-active' : 'input-active'}
                  placeholder="搜尋"
                  onFocus={this.props.searchFocusOrBlur}
                  onBlur={this.props.searchFocusOrBlur}
                />
              </CSSTransition>
              <i className={this.props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
            </div>
          </div>
          <div className="header_center-right">
            <div className="nav-item header_right-center-setting">
              <span>Aa</span>
            </div>
            <div className="nav-item header_right-center-login">
              <span>登入</span>
            </div>
          </div>
        </div>
        <div className="header_right nav-item">
          <span className="header_right-register">註冊</span>
          <span className="header_right-write nav-item">
            <i className="icon icon-write"></i>
            <span>寫文章</span>
          </span>
        </div>
      </header>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    inputBlur: state.inputBlur
  }
}

const mapDispathToProps = (dispatch) => {
  return {
    searchFocusOrBlur() {
      const action = {
        type: 'search_focus_or_blur'
      }
      dispatch(action);
    }
  }
}

export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
  1. 再來,我們再修改下 reducer.js,獲取並處理 src/index.js 中 dispatch 過來的值:

src/store/reducer.js

程式碼詳情
const defaultState = {
  inputBlur: true
};

export default (state = defaultState, action) => {
  if(action.type === 'search_focus_or_blur') {
    const newState = JSON.parse(JSON.stringify(state));
    newState.inputBlur = !newState.inputBlur
    return newState;
  }
  return state;
}
複製程式碼
  1. 此時,我們完成了修改的步驟。同時,這時候因為 src 下 common 中 header 裡面的 index.js 中只有 render 方法體,它構成了無狀態元件,所以我們將其轉換成無狀態元件:

src/common/header/index.js

程式碼詳情
import React from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';

import homeImage from '../../resources/img/header-home.png';

const Header = (props) => {
  return (
    <header>
      <div className="header_left">
        <a href="/">
          <img alt="首頁" src={homeImage} className="headef_left-img" />
        </a>
      </div>
      <div className="header_center">
        <div className="header_center-left">
          <div className="nav-item header_center-left-home">
            <i className="icon icon-home"></i>
            <span>首頁</span>
          </div>
          <div className="nav-item header_center-left-download">
            <i className="icon icon-download"></i>
            <span>下載App</span>
          </div>
          <div className="nav-item header_center-left-search">
            <CSSTransition
              in={props.inputBlur}
              timeout={200}
              classNames="slide"
            >
              <input 
                className={props.inputBlur ? 'input-nor-active' : 'input-active'}
                placeholder="搜尋"
                onFocus={props.searchFocusOrBlur}
                onBlur={props.searchFocusOrBlur}
              />
            </CSSTransition>
            <i className={props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
          </div>
        </div>
        <div className="header_center-right">
          <div className="nav-item header_right-center-setting">
            <span>Aa</span>
          </div>
          <div className="nav-item header_right-center-login">
            <span>登入</span>
          </div>
        </div>
      </div>
      <div className="header_right nav-item">
        <span className="header_right-register">註冊</span>
        <span className="header_right-write nav-item">
          <i className="icon icon-write"></i>
          <span>寫文章</span>
        </span>
      </div>
    </header>
  )
}

const mapStateToProps = (state) => {
  return {
    inputBlur: state.inputBlur
  }
}

const mapDispathToProps = (dispatch) => {
  return {
    searchFocusOrBlur() {
      const action = {
        type: 'search_focus_or_blur'
      }
      dispatch(action);
    }
  }
}

export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
  1. 最後,我們完成了 Redux、React-Redux 的引用及使用,以及對 header/index.js 的無狀態元件的升級。

由於我們只是將必要的資料儲存到 state 中,所以樣式和功能無變化,故不貼出效果圖。

八 使用 redux-devtools-extension 外掛

返回目錄

修改 src/store/index.js 如下:

src/store/index.js

程式碼詳情
import { createStore, compose } from 'redux';
import reducer from './reducer';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(reducer, composeEnhancers())

export default store;
複製程式碼

這時候,我們就成功開啟之前安裝過的 redux-devtools-extension 外掛。

使用一下:

React Demo Three - 簡書&掘金

九 優化:抽取 reducer.js

返回目錄

在專案開發中,我們會發現 reducer.js 隨著專案的開發越來越龐大,最後到不可維護的地步。

該視訊的慕課講師也提到:當你的一個 js 檔案程式碼量超過 300 行,說明它的設計從一開始來說就是不合理的。

所以,我們要想著進一步優化它。

首先,我們在 header 目錄下,新建 store,並新建 reducer.js,將 src/store 的 reducer.js 中的內容剪下到 header/store/reducer.js 中:

src/common/header/store/reducer.js

程式碼詳情
// 1. 將 reducer.js 轉移到 header/store/reducer.js 中
const defaultState = {
  inputBlur: true
};

export default (state = defaultState, action) => {
  if(action.type === 'search_focus_or_blur') {
    const newState = JSON.parse(JSON.stringify(state));
    newState.inputBlur = !newState.inputBlur
    return newState;
  }
  return state;
}
複製程式碼

然後,我們修改 src/store/reducer.js 的內容為:

src/store/reducer.js

程式碼詳情
// 2. 通過 combineReducers 整合多個 reducer.js 檔案
import { combineReducers } from 'redux';
import headerReducer from '../common/header/store/reducer';

const reducer =  combineReducers({
  header: headerReducer
})

export default reducer;
複製程式碼

最後,我們修改 src/common/header/index.js 內容:

src/common/header/index.js

程式碼詳情
// 程式碼省略 。。。
const mapStateToProps = (state) => {
  return {
    // 3. 因為引用的層級變了,所以需要修改 state.inputBlur 為 state.header.inputBlue
    inputBlur: state.header.inputBlur
  }
}
// 程式碼省略 。。。
複製程式碼

在這裡,我們需要知道的是:之前我們只有一層目錄,所以修改的是 state.inputBlur

但是,因為通過 combineReducers 將 reducer.js 進行了整合,所以需要修改為 state.header.inputBlur

至此,我們就完成了 reducer.js 的優化。

十 優化:抽取 action

返回目錄

  1. 首先,在 header 的 store 中新建 actionCreators.js 檔案:

src/common/header/store/actionCreators.js

程式碼詳情
// 1. 定義 actionCreators
export const searchFocusOrBlur = () => ({
  type: 'search_focus_or_blur'
})
複製程式碼
  1. 然後,我們在 header 中的 index.js 檔案引入 actionCreators.js,並在 mapDispathToProps 方法體中將其 dispatch 出去:

src/common/header/index.js

程式碼詳情
import React from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
// 2. 以 actionCreators 的形式將所有 action 引入進來
import * as actionCreators from './store/actionCreators';

import homeImage from '../../resources/img/header-home.png';

const Header = (props) => {
  return (
    <header>
      <div className="header_left">
        <a href="/">
          <img alt="首頁" src={homeImage} className="headef_left-img" />
        </a>
      </div>
      <div className="header_center">
        <div className="header_center-left">
          <div className="nav-item header_center-left-home">
            <i className="icon icon-home"></i>
            <span>首頁</span>
          </div>
          <div className="nav-item header_center-left-download">
            <i className="icon icon-download"></i>
            <span>下載App</span>
          </div>
          <div className="nav-item header_center-left-search">
            <CSSTransition
              in={props.inputBlur}
              timeout={200}
              classNames="slide"
            >
              <input 
                className={props.inputBlur ? 'input-nor-active' : 'input-active'}
                placeholder="搜尋"
                onFocus={props.searchFocusOrBlur}
                onBlur={props.searchFocusOrBlur}
              />
            </CSSTransition>
            <i className={props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
          </div>
        </div>
        <div className="header_center-right">
          <div className="nav-item header_right-center-setting">
            <span>Aa</span>
          </div>
          <div className="nav-item header_right-center-login">
            <span>登入</span>
          </div>
        </div>
      </div>
      <div className="header_right nav-item">
        <span className="header_right-register">註冊</span>
        <span className="header_right-write nav-item">
          <i className="icon icon-write"></i>
          <span>寫文章</span>
        </span>
      </div>
    </header>
  )
}

const mapStateToProps = (state) => {
  return {
    inputBlur: state.header.inputBlur
  }
}

const mapDispathToProps = (dispatch) => {
  return {
    searchFocusOrBlur() {
      // 3. 使用 actionCreators
      dispatch(actionCreators.searchFocusOrBlur());
    }
  }
}

export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
  1. 接著,因為我們在 actionCreators.js 中使用的 type 是字串,所以我們同樣在 store 中建立 actionTypes.js,將其變成常量:

src/common/header/store/actionTypes.js

程式碼詳情
export const SEARCH_FOCUS_OR_BLUR = 'search_focus_or_blur';
複製程式碼
  1. 再然後,我們在 actionCreators.js 中引入 actionTypes.js:

src/common/header/store/actionCreators.js

程式碼詳情
// 4. 引入常量
import { SEARCH_FOCUS_OR_BLUR } from './actionTypes';

// 1. 定義 actionCreators
// 5. 將 action 中的字串修改為常量
export const searchFocusOrBlur = () => ({
  type: SEARCH_FOCUS_OR_BLUR
})
複製程式碼
  1. 再接著,我們修改下 header 目錄中 store 下的 reducer.js,因為我們的字串變成了常量,所以這裡也需要做相應變更:

src/common/header/store/reducer.js

程式碼詳情
// 6. 引入常量
import * as actionTypes from './actionTypes'

const defaultState = {
  inputBlur: true
};

export default (state = defaultState, action) => {
  // 7. 使用常量
  if(action.type === actionTypes.SEARCH_FOCUS_OR_BLUR) {
    const newState = JSON.parse(JSON.stringify(state));
    newState.inputBlur = !newState.inputBlur
    return newState;
  }
  return state;
}
複製程式碼
  1. 然後,我們現在 header/store 目錄下有:actionCreators.js、actionTypes.js、reducer.js 三個檔案,如果我們每次引入都要一個一個找,那是相當麻煩的,所以我們在 header/store 目錄下再新建一個 index.js,通過 index.js 來管理這三個檔案,這樣我們其他頁面需要引入它們的時候,我們只需要引入 store 下的 index.js 即可。

src/common/header/store/index.js

程式碼詳情
// 8. 統一管理 store 目錄中的檔案
import * as actionCreators from './actionCreators';
import * as actionTypes from './actionTypes';
import reducer from './reducer';

export { actionCreators, actionTypes, reducer };
複製程式碼
  1. 此時,值得注意的是,這時候我們需要處理下 header/index.js 檔案:
程式碼詳情
import React from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
// 2. 以 actionCreators 的形式將所有 action 引入進來
// import * as actionCreators from './store/actionCreators';
// 9. 引入 store/index 檔案即可
import { actionCreators } from './store';

import homeImage from '../../resources/img/header-home.png';

// 程式碼省略
複製程式碼
  1. 最後,再處理下 src/store/reducer.js,因為它引用了 common/header/store 中的 reducer.js:
程式碼詳情
import { combineReducers } from 'redux';
// 10. 修改下引用方式
import { reducer as headerReducer } from '../common/header/store';

const reducer =  combineReducers({
  header: headerReducer
})

export default reducer;
複製程式碼

至此,我們就完成了本次的優化抽取。

十一 優化;immutable.js

返回目錄

在我們工作的過程中,如果一不小心,就會修改了 reducer.js 中的資料(平時開發的時候,我們會通過 JSON.parse(JSON.stringify()) 來進行深拷貝,獲取一份額外的來進行修改)。

所以,這時候,我們就需要使用 immutable.js,它是由 Facebook 團隊開發的,用來幫助我們生產 immutable 物件,從而限制 state 不可被改變。

  • 安裝 immutable.js:npm i immutable -S
  • 案例 immutable.js:
const { Map } = require('immutable');
const map1 = Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 50);
map1.get('b') + " vs. " + map2.get('b'); // 2 vs. 50
複製程式碼

看起來很簡單,我們直接在簡書 Demo 中使用:

src/common/header/store/reducer.js

程式碼詳情
import * as actionTypes from './actionTypes'
// 1. 通過 immutable 引入 fromJS
import { fromJS } from 'immutable';

// 2. 對 defaultState 使用 fromJS
const defaultState = fromJS({
  inputBlur: true
});

export default (state = defaultState, action) => {
  if(action.type === actionTypes.SEARCH_FOCUS_OR_BLUR) {
    // const newState = JSON.parse(JSON.stringify(state));
    // newState.inputBlur = !newState.inputBlur
    // return newState;

    // 4. 通過 immutable 的方法來 set state 的值
    // immutable 物件的 set 方法,會結合之前 immutable 物件的值和設定的值,返回一個全新的物件
    return state.set('inputBlur', !state.get('inputBlur'));
  }
  return state;
}
複製程式碼

src/common/header/index.js

程式碼詳情
import React from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';

import homeImage from '../../resources/img/header-home.png';

const Header = (props) => {
  return (
    <header>
      <div className="header_left">
        <a href="/">
          <img alt="首頁" src={homeImage} className="headef_left-img" />
        </a>
      </div>
      <div className="header_center">
        <div className="header_center-left">
          <div className="nav-item header_center-left-home">
            <i className="icon icon-home"></i>
            <span>首頁</span>
          </div>
          <div className="nav-item header_center-left-download">
            <i className="icon icon-download"></i>
            <span>下載App</span>
          </div>
          <div className="nav-item header_center-left-search">
            <CSSTransition
              in={props.inputBlur}
              timeout={200}
              classNames="slide"
            >
              <input 
                className={props.inputBlur ? 'input-nor-active' : 'input-active'}
                placeholder="搜尋"
                onFocus={props.searchFocusOrBlur}
                onBlur={props.searchFocusOrBlur}
              />
            </CSSTransition>
            <i className={props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
          </div>
        </div>
        <div className="header_center-right">
          <div className="nav-item header_right-center-setting">
            <span>Aa</span>
          </div>
          <div className="nav-item header_right-center-login">
            <span>登入</span>
          </div>
        </div>
      </div>
      <div className="header_right nav-item">
        <span className="header_right-register">註冊</span>
        <span className="header_right-write nav-item">
          <i className="icon icon-write"></i>
          <span>寫文章</span>
        </span>
      </div>
    </header>
  )
}

const mapStateToProps = (state) => {
  return {
    // 3. 通過 immutable 提供的 get() 方法來獲取 inputBlur 屬性
    inputBlur: state.header.get('inputBlur')
  }
}

const mapDispathToProps = (dispatch) => {
  return {
    searchFocusOrBlur() {
      dispatch(actionCreators.searchFocusOrBlur());
    }
  }
}

export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼

我們大致做了四個步驟,從而完成了 immutable.js 的引用及使用:

  1. 通過 import immutable 引入 fromJS
  2. defaultState 使用 fromJS
  3. 這時候我們就不能直接修改 matStateToProps 中的值了,而是 通過 immutable 提供的 get() 方法來獲取 inputBlur 屬性
  4. 通過 immutable 的方法來 set state 的值。immutable 物件的 set 方法,會結合之前 immutable 物件的值和設定的值,返回一個全新的物件

這樣,我們就成功保護了 state 的值。

十二 優化:redux-immutable

返回目錄

當然,在上面,我們保護了 header 中的 state,我們在程式碼中:

inputBlur: state.header.get('inputBlur')
複製程式碼

這個 header 也是 state 的值,所以我們也需要對它進行保護,所以我們就需要 redux-immutable

  • 安裝 redux-immutable:npm i redux-immutable -S
  • 使用 redux-immutable:

src/store/reducer.js

程式碼詳情
// import { combineReducers } from 'redux';
// 1. 通過 redux-immutable 引入 combineReducers 而非原先的 redux
import { combineReducers } from 'redux-immutable';
import { reducer as headerReducer } from '../common/header/store';

const reducer =  combineReducers({
  header: headerReducer
})

export default reducer;
複製程式碼

src/common/header/index.js

程式碼詳情
// 程式碼省略。。。
const mapStateToProps = (state) => {
  return {
    // 2. 通過同樣的 get 方法來獲取 header
    inputBlur: state.get('header').get('inputBlur')
  }
}
// 程式碼省略。。。
複製程式碼

這樣,通過簡單的三個步驟,我們就保護了主 state 的值:

  1. 安裝 redux-immutable:npm i redux-immutable -S
  2. 通過 redux-immutable 引入 combineReducers 而非原先的 redux
  3. 通過同樣的 get 方法來獲取 header

十三 功能實現:熱門搜尋

返回目錄

本章節完成三個功能:

  1. 寫熱門搜尋顯示隱藏
  2. 安裝 redux-thunk
  3. 使用 React 中 Node 提供的作假資料的功能,在 public/api 下寫個檔案 headerList.json,並做假資料,使用方式為 axios.get('/api/headerList.json').then()

首先,我們完成熱門搜尋的顯示隱藏:

src/common.css

程式碼詳情
.icon {
  display: inline-block;
  width: 20px;
  height: 21px;
  margin-right: 5px;
}
.icon-home {
  background: url('./resources/img/icon-home.png') no-repeat center;
  background-size: 100%;
}
.icon-write {
  background: url('./resources/img/icon-write.png') no-repeat center;
  background-size: 100%;
}
.icon-download {
  background: url('./resources/img/icon-download.png') no-repeat center;
  background-size: 100%;
}
.icon-search {
  background: url('./resources/img/icon-search.png') no-repeat center;
  background-size: 100%;
}
.display-hide {
  display: none;
}
.display-show {
  display: block;
}
複製程式碼

src/common/header/index.css

程式碼詳情
header {
  width: 100%;
  height: 58px;
  display: flex;
  align-items: center;
  border-bottom: 1px solid #ccc;
  font-size: 17px;
}

/* 頭部左邊 */
.header_left-img {
  width: 100px;
  height: 56px;
}

/* 頭部中間 */
.header_center {
  width: 1000px;
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
}
.nav-item {
  margin-right: 30px;
  display: flex;
  align-items: center;
}

/* 頭部中間左部 */
.header_center-left {
  display: flex;
}

/* 頭部中間左部 - 首頁 */
.header_center-left-home {
  color: #ea6f5a;
}

/* 頭部中間左部 - 搜尋框 */
.header_center-left-search {
  position: relative;
}
.slide-enter {
  transition: all .2s ease-out;
}
.slide-enter-active {
  width: 280px;
}
.slide-exit {
  transition: all .2s ease-out;
}
.silde-exit-active {
  width: 240px;
}
.header_center-left-search input {
  width: 240px;
  padding: 0 45px 0 20px;
  height: 38px;
  font-size: 14px;
  border: 1px solid #eee;
  border-radius: 40px;
  background: #eee;
}
.header_center-left-search .input-active {
  width: 280px;
}
.header_center-left-search .icon-search {
  position: absolute;
  top: 8px;
  right: 10px;
}
.header_center-left-search .icon-active {
  padding: 3px;
  top: 4px;
  border-radius: 15px;
  border: 1px solid #ea6f5a;
}

/* 頭部中間左部 - 熱搜 */
.header_center-left-search .icon-active:hover {
  cursor: pointer;
}
.header_center-left-hot-search:before {
  content: "";
  left: 27px;
  width: 10px;
  height: 10px;
  transform: rotate(45deg);
  top: -5px;
  z-index: -1;
  position: absolute;
  background-color: #fff;
  box-shadow: 0 0 8px rgba(0,0,0,.2);
}
.header_center-left-hot-search {
  position: absolute;
  width: 250px;
  left: 0;
  top: 125%;
  padding: 15px;
  font-size: 14px;
  background: #fff;
  border-radius: 4px;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
}
.header_center-left-hot-search-title {
  display: flex;
  justify-content: space-between;
  color: #969696;
}
.header_center-left-hot-search-change {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.icon-change {
  display: inline-block;
  width: 20px;
  height: 14px;
  background: url('../../resources/img/icon-change.png') no-repeat center;
  background-size: 100%;
}
.icon-change:hover {
  cursor: pointer;
}
.header_center-left-hot-search-content span {
  display: inline-block;
  margin-top: 10px;
  margin-right: 10px;
  padding: 2px 6px;
  font-size: 12px;
  color: #787878;
  border: 1px solid #ddd;
  border-radius: 3px;
}
.header_center-left-hot-search-content span:hover {
  cursor: pointer;
}

/* 頭部中間右部 */
.header_center-right {
  display: flex;
  color: #969696;
}

/* 頭部右邊 */
.header_right-register, .header_right-write {
  width: 80px;
  text-align: center;
  height: 38px;
  line-height: 38px;
  border: 1px solid rgba(236,97,73,.7);
  border-radius: 20px;
  font-size: 15px;
  color: #ea6f5a;
  background-color: transparent;
}
.header_right-write {
  margin-left: 10px;
  padding-left: 10px;
  margin-right: 0px;
  color: #fff;
  background-color: #ea6f5a;
}
複製程式碼

src/common/header/index.js

程式碼詳情
import React from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';

import homeImage from '../../resources/img/header-home.png';

const Header = (props) => {
  return (
    <header>
      <div className="header_left">
        <a href="/">
          <img alt="首頁" src={homeImage} className="header_left-img" />
        </a>
      </div>
      <div className="header_center">
        <div className="header_center-left">
          <div className="nav-item header_center-left-home">
            <i className="icon icon-home"></i>
            <span>首頁</span>
          </div>
          <div className="nav-item header_center-left-download">
            <i className="icon icon-download"></i>
            <span>下載App</span>
          </div>
          <div className="nav-item header_center-left-search">
            <CSSTransition
              in={props.inputBlur}
              timeout={200}
              classNames="slide"
            >
              <input 
                className={props.inputBlur ? 'input-nor-active' : 'input-active'}
                placeholder="搜尋"
                onFocus={props.searchFocusOrBlur}
                onBlur={props.searchFocusOrBlur}
              />
            </CSSTransition>
            <i className={props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
            {/* 新增熱搜模組 */}
            <div className={props.inputBlur ? 'display-hide header_center-left-hot-search' : 'display-show header_center-left-hot-search'}>
              <div className="header_center-left-hot-search-title">
                <span>熱門搜尋</span>
                <span>
                  <i className="icon-change"></i>
                  <span>換一批</span>
                </span>
              </div>
              <div className="header_center-left-hot-search-content">
                <span>考研</span>
                <span>慢死人</span>
                <span>悅心</span>
                <span>一致</span>
                <span>是的</span>
                <span>jsliang</span>
              </div>
            </div>
          </div>
        </div>
        <div className="header_center-right">
          <div className="nav-item header_right-center-setting">
            <span>Aa</span>
          </div>
          <div className="nav-item header_right-center-login">
            <span>登入</span>
          </div>
        </div>
      </div>
      <div className="header_right nav-item">
        <span className="header_right-register">註冊</span>
        <span className="header_right-write nav-item">
          <i className="icon icon-write"></i>
          <span>寫文章</span>
        </span>
      </div>
    </header>
  )
}

const mapStateToProps = (state) => {
  return {
    inputBlur: state.get('header').get('inputBlur')
  }
}

const mapDispathToProps = (dispatch) => {
  return {
    searchFocusOrBlur() {
      dispatch(actionCreators.searchFocusOrBlur());
    }
  }
}

export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼

由此,我們完成了熱門搜尋的顯示隱藏:

React Demo Three - 簡書&掘金

PS:由於頁面逐漸增大,所以我們 header 中使用無狀態元件已經滿足不了我們要求了,我們需要將無狀態元件改成正常的元件:

src/common/header/index.js

程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';

import homeImage from '../../resources/img/header-home.png';

class Header extends Component {
  render() {
    return (
      <header>
        <div className="header_left">
          <a href="/">
            <img alt="首頁" src={homeImage} className="header_left-img" />
          </a>
        </div>
        <div className="header_center">
          <div className="header_center-left">
            <div className="nav-item header_center-left-home">
              <i className="icon icon-home"></i>
              <span>首頁</span>
            </div>
            <div className="nav-item header_center-left-download">
              <i className="icon icon-download"></i>
              <span>下載App</span>
            </div>
            <div className="nav-item header_center-left-search">
              <CSSTransition
                in={this.props.inputBlur}
                timeout={200}
                classNames="slide"
              >
                <input 
                  className={this.props.inputBlur ? 'input-nor-active' : 'input-active'}
                  placeholder="搜尋"
                  onFocus={this.props.searchFocusOrBlur}
                  onBlur={this.props.searchFocusOrBlur}
                />
              </CSSTransition>
              <i className={this.props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
              <div className={this.props.inputBlur ? 'display-hide header_center-left-hot-search' : 'display-show header_center-left-hot-search'}>
                <div className="header_center-left-hot-search-title">
                  <span>熱門搜尋</span>
                  <span>
                    <i className="icon-change"></i>
                    <span>換一批</span>
                  </span>
                </div>
                <div className="header_center-left-hot-search-content">
                  <span>考研</span>
                  <span>慢死人</span>
                  <span>悅心</span>
                  <span>一致</span>
                  <span>是的</span>
                  <span>jsliang</span>
                </div>
              </div>
            </div>
          </div>
          <div className="header_center-right">
            <div className="nav-item header_right-center-setting">
              <span>Aa</span>
            </div>
            <div className="nav-item header_right-center-login">
              <span>登入</span>
            </div>
          </div>
        </div>
        <div className="header_right nav-item">
          <span className="header_right-register">註冊</span>
          <span className="header_right-write nav-item">
            <i className="icon icon-write"></i>
            <span>寫文章</span>
          </span>
        </div>
      </header>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    inputBlur: state.get('header').get('inputBlur')
  }
}

const mapDispathToProps = (dispatch) => {
  return {
    searchFocusOrBlur() {
      dispatch(actionCreators.searchFocusOrBlur());
    }
  }
}

export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼

然後,由於我們的資料是從介面模擬過來的,而在上一篇文章說過,如果要對介面程式碼進行管理,最好使用 Redux-Thunk 和 Redux-Saga,這裡我們使用 Redux-Thunk:

  1. 安裝 redux-thunk:cnpm i redux-thunk -S
  2. 安裝 axios:cnpm i axios -S

在這裡,我們要知道 create-react-app 的配置是包含 Node.js 的,所以我們可以依靠 Node.js 進行開發時候的 Mock 資料。

下面開始開發:

src/store/index.js

程式碼詳情
// 2. 引入 redux 的 applyMiddleware,進行多中介軟體的使用
import { createStore, compose, applyMiddleware } from 'redux';
// 1. 引入 redux-thunk
import thunk from 'redux-thunk';
import reducer from './reducer';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

// 3. 通過 applyMiddleware 同時使用 redux-thunk 和 redux-dev-tools
const store = createStore(reducer, composeEnhancers(
  applyMiddleware(thunk)
));

export default store;
複製程式碼
  1. 引入 redux-thunk
  2. 引入 redux 的 applyMiddleware,進行多中介軟體的使用
  3. 通過 applyMiddleware 同時使用 redux-thunk 和 redux-dev-tools

這樣,我們就可以正常使用 redux-thunk 了。

  1. src/common/header/index.js
程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';

import homeImage from '../../resources/img/header-home.png';

class Header extends Component {
  render() {
    return (
      <header>
        <div className="header_left">
          <a href="/">
            <img alt="首頁" src={homeImage} className="header_left-img" />
          </a>
        </div>
        <div className="header_center">
          <div className="header_center-left">
            <div className="nav-item header_center-left-home">
              <i className="icon icon-home"></i>
              <span>首頁</span>
            </div>
            <div className="nav-item header_center-left-download">
              <i className="icon icon-download"></i>
              <span>下載App</span>
            </div>
            <div className="nav-item header_center-left-search">
              <CSSTransition
                in={this.props.inputBlur}
                timeout={200}
                classNames="slide"
              >
                <input 
                  className={this.props.inputBlur ? 'input-nor-active' : 'input-active'}
                  placeholder="搜尋"
                  onFocus={this.props.searchFocusOrBlur}
                  onBlur={this.props.searchFocusOrBlur}
                />
              </CSSTransition>
              <i className={this.props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
              <div className={this.props.inputBlur ? 'display-hide header_center-left-hot-search' : 'display-show header_center-left-hot-search'}>
                <div className="header_center-left-hot-search-title">
                  <span>熱門搜尋</span>
                  <span>
                    <i className="icon-change"></i>
                    <span>換一批</span>
                  </span>
                </div>
                <div className="header_center-left-hot-search-content">
                  {/* 15. 遍歷輸出該資料 */}
                  {
                    this.props.list.map((item) => {
                      return <span key={item}>{item}</span>
                    })
                  }
                </div>
              </div>
            </div>
          </div>
          <div className="header_center-right">
            <div className="nav-item header_right-center-setting">
              <span>Aa</span>
            </div>
            <div className="nav-item header_right-center-login">
              <span>登入</span>
            </div>
          </div>
        </div>
        <div className="header_right nav-item">
          <span className="header_right-register">註冊</span>
          <span className="header_right-write nav-item">
            <i className="icon icon-write"></i>
            <span>寫文章</span>
          </span>
        </div>
      </header>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    inputBlur: state.get('header').get('inputBlur'),
    // 14. 獲取 reducer.js 中的 list 資料
    list: state.get('header').get('list')
  }
}

const mapDispathToProps = (dispatch) => {
  return {
    searchFocusOrBlur() {
      // 4. 派發 action 到 actionCreators.js 中的 getList() 方法
      dispatch(actionCreators.getList());
      dispatch(actionCreators.searchFocusOrBlur());
    }
  }
}

export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
  1. src/common/header/store/actionCreators.js
程式碼詳情
import * as actionTypes from './actionTypes'
// 7. 引入 axios
import axios from 'axios';
// 11. 引入 immutable 的型別轉換
import { fromJS } from 'immutable';

export const searchFocusOrBlur = () => ({
  type: actionTypes.SEARCH_FOCUS_OR_BLUR
})

// 10. 定義 action,接受引數 data,同時因為我們使用了 Immutable,所以需要將獲取的資料轉換為 immutable 型別
const changeList = (data) => ({
  type: actionTypes.GET_LIST,
  data: fromJS(data)
})

// 5. 編寫 getList 的 action,由於需要 actionTypes 中定義,所以前往 actionTypes.js 中新增
export const getList = () => {
  return (dispatch) => {
    // 8. 呼叫 create-react-app 中提供的 Node 伺服器,從而 mock 資料
    axios.get('/api/headerList.json').then( (res) => {
      if(res.data.code === 0) {
        const data = res.data.list;
        // 由於資料太多,我們限制資料量為 15 先
        data.length = 15;
        // 12. 派發 changeList 型別
        dispatch(changeList(data));
      }
    }).catch( (error) => {
      console.log(error);
    });
  }
}
複製程式碼
  1. src/common/header/store/actionTypes.js
程式碼詳情
export const SEARCH_FOCUS_OR_BLUR = 'header/search_focus_or_blur';
// 6. 新增 actionType
export const GET_LIST = 'header/get_list';
複製程式碼
  1. src/common/header/store/reducer.js
程式碼詳情
import * as actionTypes from './actionTypes'
import { fromJS } from 'immutable';

const defaultState = fromJS({
  inputBlur: true,
  // 9. 給 header 下的 reducer.js 提供儲存資料的地方
  list: []
});

export default (state = defaultState, action) => {
  if(action.type === actionTypes.SEARCH_FOCUS_OR_BLUR) {
    return state.set('inputBlur', !state.get('inputBlur'));
  }
  // 13. 判斷 actionTypes 是否為 GET_LIST,如果是則執行該 action
  if(action.type === actionTypes.GET_LIST) {
    return state.set('list', action.data);
  }
  return state;
}
複製程式碼
  1. public/api/headerList.json
程式碼詳情
{
  "code": 0,
  "list": ["區塊鏈","小程式","vue","畢業","PHP","故事","flutter","理財","美食","投稿","手帳","書法","PPT","穿搭","打碗碗花","簡書","姥姥的澎湖灣","設計","創業","交友","籽鹽","教育","思維導圖","瘋哥哥","梅西","時間管理","golang","連載","自律","職場","考研","慢世人","悅欣","一紙vr","spring","eos","足球","程式設計師","林露含","彩鉛","金融","木風雜談","日更","成長","外婆是方言","docker"]
}
複製程式碼

通過下面步驟:

  1. 派發 action 到 actionCreators.js 中的 getList() 方法
  2. 編寫 getListaction,由於需要 actionTypes 中定義,所以前往 actionTypes.js 中新增
  3. 新增 actionType
  4. 引入 axios
  5. 呼叫 create-react-app 中提供的 Node 伺服器,從而 mock 資料
  6. 給 header 下的 reducer.js 提供儲存資料的地方
  7. 定義 action,接受引數 data,同時因為我們使用了 Immutable,所以需要將獲取的資料轉換為 immutable 型別
  8. 引入 Immutable 的型別轉換
  9. 派發 changeList 型別
  10. 判斷 actionTypes 是否為 GET_LIST,如果是則執行該 action
  11. 獲取 reducer.js 中的 list 資料
  12. 遍歷輸出該資料

這樣,我們就成功地獲取了 mock 提供的資料:

React Demo Three - 簡書&掘金

十四 程式碼優化

返回目錄

  • reducer.js 中使用 switch...case... 替換掉 if... 語句。

src/common/header/store/reducer.js

程式碼詳情
import * as actionTypes from './actionTypes'
import { fromJS } from 'immutable';

const defaultState = fromJS({
  inputBlur: true,
  list: []
});

export default (state = defaultState, action) => {
  switch(action.type) {
    case actionTypes.SEARCH_FOCUS_OR_BLUR:
      return state.set('inputBlur', !state.get('inputBlur'));
    case actionTypes.GET_LIST:
      return state.set('list', action.data);
    default:
      return state;
  }
}
複製程式碼

十五 解決歷史遺留問題

返回目錄

在這裡,我們解決下歷史遺留問題:在我們失焦於輸入框的時候,我們的【熱門搜尋】模組就會消失,從而看不到我們點選【換一換】按鈕的效果,所以我們需要修改下程式碼,在我們滑鼠在【熱門模組】中時,這個模組不會消失,當我們滑鼠失焦且滑鼠不在熱門模組中時,熱門模組才消失。

  1. src/common/header/store/reducer.js
程式碼詳情
import * as actionTypes from './actionTypes'
import { fromJS } from 'immutable';

const defaultState = fromJS({
  inputFocus: false,
  // 1. 設定滑鼠移動到熱門模組為 false
  mouseInHot: false,
  list: [],
});

export default (state = defaultState, action) => {
  switch(action.type) {
    case actionTypes.SEARCH_FOCUS:
      return state.set('inputFocus', true);
    case actionTypes.SEARCH_BLUR:
      return state.set('inputFocus', false);
    case actionTypes.GET_LIST:
      return state.set('list', action.data);
    // 6. 在 reducer.js 中判斷這兩個 action 執行設定 mouseInHot
    case actionTypes.ON_MOUSE_ENTER_HOT:
      return state.set('mouseInHot', true);
    case actionTypes.ON_MOUSE_LEAVE_HOT:
      return state.set('mouseInHot', false);
    default:
      return state;
  }
}
複製程式碼
  1. src/common/header/index.js
程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';

import homeImage from '../../resources/img/header-home.png';

class Header extends Component {
  render() {
    return (
      <header>
        <div className="header_left">
          <a href="/">
            <img alt="首頁" src={homeImage} className="header_left-img" />
          </a>
        </div>
        <div className="header_center">
          <div className="header_center-left">
            <div className="nav-item header_center-left-home">
              <i className="icon icon-home"></i>
              <span>首頁</span>
            </div>
            <div className="nav-item header_center-left-download">
              <i className="icon icon-download"></i>
              <span>下載App</span>
            </div>
            <div className="nav-item header_center-left-search">
              <CSSTransition
                in={this.props.inputFocus}
                timeout={200}
                classNames="slide"
              >
                <input 
                  className={this.props.inputFocus ? 'input-active' : 'input-nor-active'}
                  placeholder="搜尋"
                  onFocus={this.props.searchFocus}
                  onBlur={this.props.searchBlur}
                />
              </CSSTransition>
              <i className={this.props.inputFocus ? 'icon icon-search icon-active' : 'icon icon-search'}></i>
              {/* 8. 在判斷中加多一個 this.props.mouseInHot,這樣只要有一個為 true,它就不會消失 */}
              <div 
                className={this.props.inputFocus || this.props.mouseInHot ? 'display-show header_center-left-hot-search' : 'display-hide header_center-left-hot-search'}
                // 2. 設定移入為 onMouseEnterHot,移出為 onMouseLeaveHot
                onMouseEnter={this.props.onMouseEnterHot}
                onMouseLeave={this.props.onMouseLeaveHot}
              >
                <div className="header_center-left-hot-search-title">
                  <span>熱門搜尋</span>
                  <span>
                    <i className="icon-change"></i>
                    <span>換一批</span>
                  </span>
                </div>
                <div className="header_center-left-hot-search-content">
                  {
                    this.props.list.map((item) => {
                      return <span key={item}>{item}</span>
                    })
                  }
                </div>
              </div>
            </div>
          </div>
          <div className="header_center-right">
            <div className="nav-item header_right-center-setting">
              <span>Aa</span>
            </div>
            <div className="nav-item header_right-center-login">
              <span>登入</span>
            </div>
          </div>
        </div>
        <div className="header_right nav-item">
          <span className="header_right-register">註冊</span>
          <span className="header_right-write nav-item">
            <i className="icon icon-write"></i>
            <span>寫文章</span>
          </span>
        </div>
      </header>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    inputFocus: state.get('header').get('inputFocus'),
    list: state.get('header').get('list'),
    // 7. 在 index.js 中獲取
    mouseInHot: state.get('header').get('mouseInHot'),
  }
}

const mapDispathToProps = (dispatch) => {
  return {
    searchFocus() {
      dispatch(actionCreators.getList());
      dispatch(actionCreators.searchFocus());
    },
    searchBlur() {
      dispatch(actionCreators.searchBlur());
    },
    // 3. 定義 onMouseEnterHot 和 onMouseLeaveHot 方法
    onMouseEnterHot() {
      dispatch(actionCreators.onMouseEnterHot());
    },
    onMouseLeaveHot() {
      dispatch(actionCreators.onMouseLeaveHot());
    },
  }
}

export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
  1. src/common/header/store/actionCreators.js
程式碼詳情
import * as actionTypes from './actionTypes'
import axios from 'axios';
import { fromJS } from 'immutable';

export const searchFocus = () => ({
  type: actionTypes.SEARCH_FOCUS
})

export const searchBlur = () => ({
  type: actionTypes.SEARCH_BLUR
})

// 4. 在 actionCreators.js 中定義這兩個方法:onMouseEnterHot 和 onMouseLeaveHot
export const onMouseEnterHot = () => ({
  type: actionTypes.ON_MOUSE_ENTER_HOT,
})

export const onMouseLeaveHot = () => ({
  type: actionTypes.ON_MOUSE_LEAVE_HOT,
})

export const getList = () => {
  return (dispatch) => {
    axios.get('/api/headerList.json').then( (res) => {
      if(res.data.code === 0) {
        const data = res.data.list;
        // 由於資料太多,我們限制資料量為 15 先
        data.length = 15;
        dispatch(changeList(data));
      }
    }).catch( (error) => {
      console.log(error);
    });
  }
}

const changeList = (data) => ({
  type: actionTypes.GET_LIST,
  data: fromJS(data)
})
複製程式碼
  1. src/common/header/store/actionTypes.js
程式碼詳情
export const SEARCH_FOCUS = 'header/search_focus';
export const SEARCH_BLUR = 'header/search_blur';
export const GET_LIST = 'header/get_list';
// 5. 在 actionTypes.js 中新增 action 型別
export const ON_MOUSE_ENTER_HOT = 'header/on_mouse_enter_hot';
export const ON_MOUSE_LEAVE_HOT = 'header/on_mouse_leave_hot';
複製程式碼

我們先看實現:

React Demo Three - 簡書&掘金

然後我們看看實現邏輯:

  1. 在 reducer.js 中設定滑鼠移動到熱門模組為 false
  2. 在 index.js 中設定移入為 onMouseEnterHot,移出為 onMouseLeaveHot
  3. 在 index.js 中 mapDispathToProps 定義 onMouseEnterHotonMouseLeaveHot 方法
  4. 在 actionCreators.js 中定義這兩個方法:onMouseEnterHotonMouseLeaveHot
  5. 在 actionTypes.js 中新增 action 型別
  6. 在 reducer.js 中判斷這兩個 action 執行設定 mouseInHot
  7. 在 index.js 中 mapStateToProps 獲取 mouseInHot
  8. 在 index.js 中的判斷中加多一個 this.props.mouseInHot,這樣只要有一個為 true,它就不會消失

注意:由於之前設定的 this.props.inputFoucsOrBlur 會造成聚焦和失焦都會呼叫一次介面,而且邏輯比較複雜,容易出錯,所以這裡我們進行了修改,將其分為聚焦和失焦兩部分。

十六 功能實現:換一換

返回目錄

下面我們開始做換一換功能:

  1. src/common/header/store/reducer.js
程式碼詳情
import * as actionTypes from './actionTypes'
import { fromJS } from 'immutable';

const defaultState = fromJS({
  inputFocus: false,
  mouseInHot: false,
  list: [],
  // 1. 在 reducer.js 中設定頁數和總頁數
  page: 1,
  totalPage: 1,
});

export default (state = defaultState, action) => {
  switch(action.type) {
    case actionTypes.SEARCH_FOCUS:
      return state.set('inputFocus', true);
    case actionTypes.SEARCH_BLUR:
      return state.set('inputFocus', false);
    case actionTypes.GET_LIST:
      // 4. 我們通過 merge 方法同時設定多個 state 值
      return state.merge({
        list: action.data,
        totalPage: action.totalPage
      });
    case actionTypes.ON_MOUSE_ENTER_HOT:
      return state.set('mouseInHot', true);
    case actionTypes.ON_MOUSE_LEAVE_HOT:
      return state.set('mouseInHot', false);
    // 11. 判斷 action 型別,並進行設定
    case actionTypes.CHANGE_PAGE:
      return state.set('page', action.page + 1);
    default:
      return state;
  }
}
複製程式碼
  1. src/common/header/store/actionCreators.js
程式碼詳情
import * as actionTypes from './actionTypes'
import axios from 'axios';
import { fromJS } from 'immutable';

export const searchFocus = () => ({
  type: actionTypes.SEARCH_FOCUS
})

export const searchBlur = () => ({
  type: actionTypes.SEARCH_BLUR
})

export const onMouseEnterHot = () => ({
  type: actionTypes.ON_MOUSE_ENTER_HOT,
})

export const onMouseLeaveHot = () => ({
  type: actionTypes.ON_MOUSE_LEAVE_HOT,
})

export const getList = () => {
  return (dispatch) => {
    axios.get('/api/headerList.json').then( (res) => {
      if(res.data.code === 0) {
        const data = res.data.list;
        // 2. 由於資料太多,我們之前限制資料量為 15,這裡我們去掉該行程式碼
        // data.length = 15;
        dispatch(changeList(data));
      }
    }).catch( (error) => {
      console.log(error);
    });
  }
}

const changeList = (data) => ({
  type: actionTypes.GET_LIST,
  data: fromJS(data),
  // 3. 我們在這裡計算總頁數
  totalPage: Math.ceil(data.length / 10)
})

// 9. 定義 changePage 方法
export const changePage = (page) => ({
  type: actionTypes.CHANGE_PAGE,
  page: page,
})
複製程式碼
  1. src/common/header/index.js
程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';

import homeImage from '../../resources/img/header-home.png';

class Header extends Component {
  render() {
    return (
      <header>
        <div className="header_left">
          <a href="/">
            <img alt="首頁" src={homeImage} className="header_left-img" />
          </a>
        </div>
        <div className="header_center">
          <div className="header_center-left">
            <div className="nav-item header_center-left-home">
              <i className="icon icon-home"></i>
              <span>首頁</span>
            </div>
            <div className="nav-item header_center-left-download">
              <i className="icon icon-download"></i>
              <span>下載App</span>
            </div>
            <div className="nav-item header_center-left-search">
              <CSSTransition
                in={this.props.inputFocus}
                timeout={200}
                classNames="slide"
              >
                <input 
                  className={this.props.inputFocus ? 'input-active' : 'input-nor-active'}
                  placeholder="搜尋"
                  onFocus={this.props.searchFocus}
                  onBlur={this.props.searchBlur}
                />
              </CSSTransition>
              <i className={this.props.inputFocus ? 'icon icon-search icon-active' : 'icon icon-search'}></i>
              <div 
                className={this.props.inputFocus || this.props.mouseInHot ? 'display-show header_center-left-hot-search' : 'display-hide header_center-left-hot-search'}
                onMouseEnter={this.props.onMouseEnterHot}
                onMouseLeave={this.props.onMouseLeaveHot}
              >
                <div className="header_center-left-hot-search-title">
                  <span>熱門搜尋</span>
                  {/* 7. 進行換頁功能實現,傳遞引數 page 和 totalPage */}
                  <span onClick={() => this.props.changePage(this.props.page, this.props.totalPage)}>
                    <i className="icon-change"></i>
                    <span className="span-change">換一批</span>
                  </span>
                </div>
                <div className="header_center-left-hot-search-content">
                  {
                    // 6. 在 index.js 中進行計算:
                    // 一開始顯示 0-9 共 10 條,換頁的時候顯示 10-19 ……以此類推
                    this.props.list.map((item, index) => {
                      if(index >= (this.props.page - 1) * 10 && index < this.props.page * 10) {
                        return <span key={item}>{item}</span>
                      } else {
                        return '';
                      }
                    })
                  }
                </div>
              </div>
            </div>
          </div>
          <div className="header_center-right">
            <div className="nav-item header_right-center-setting">
              <span>Aa</span>
            </div>
            <div className="nav-item header_right-center-login">
              <span>登入</span>
            </div>
          </div>
        </div>
        <div className="header_right nav-item">
          <span className="header_right-register">註冊</span>
          <span className="header_right-write nav-item">
            <i className="icon icon-write"></i>
            <span>寫文章</span>
          </span>
        </div>
      </header>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    inputFocus: state.get('header').get('inputFocus'),
    list: state.get('header').get('list'),
    mouseInHot: state.get('header').get('mouseInHot'),
    // 5. 在 index.js 中 mapStateToProps 獲取資料
    page: state.get('header').get('page'),
    totalPage: state.get('header').get('totalPage'),
  }
}

const mapDispathToProps = (dispatch) => {
  return {
    searchFocus() {
      dispatch(actionCreators.getList());
      dispatch(actionCreators.searchFocus());
    },
    searchBlur() {
      dispatch(actionCreators.searchBlur());
    },
    onMouseEnterHot() {
      dispatch(actionCreators.onMouseEnterHot());
    },
    onMouseLeaveHot() {
      dispatch(actionCreators.onMouseLeaveHot());
    },
    // 8. 呼叫 changePage 方法
    changePage(page, totalPage) {
      if(page === totalPage) {
        page = 1;
        dispatch(actionCreators.changePage(page));
      } else {
        dispatch(actionCreators.changePage(page));
      }
    }
  }
}

export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
  1. src/common/header/store/actionTypes.js
程式碼詳情
export const SEARCH_FOCUS = 'header/search_focus';
export const SEARCH_BLUR = 'header/search_blur';
export const GET_LIST = 'header/get_list';
export const ON_MOUSE_ENTER_HOT = 'header/on_mouse_enter_hot';
export const ON_MOUSE_LEAVE_HOT = 'header/on_mouse_leave_hot';
// 10. 定義 action 
export const CHANGE_PAGE = 'header/change_page';
複製程式碼

此時我們程式碼思路是:

  1. 在 reducer.js 中設定頁數 page 和總頁數 totalPage
  2. 在 actionCreators.js 中,之前由於資料太多,我們之前限制資料量為 15,這裡我們去掉該行程式碼
  3. 在 actionCreators.js 這裡計算總頁數
  4. 在 reducer.js 中通過 merge 方法同時設定多個 state
  5. 在 index.js 中 mapStateToProps 獲取資料
  6. 在 index.js 中進行計算:一開始顯示 0-9 共 10 條,換頁的時候顯示 10-19 ……以此類推
  7. 在 index.js 中進行換頁功能實現,傳遞引數 pagetotalPage
  8. 在 index.js 呼叫 changePage 方法,進行是否重置為第一頁判斷,並 dispatch 方法
  9. 在 actionCreators.js 中定義 changePage 方法
  10. 在 actionTypes.js 中定義 action
  11. 在 reducer.js 中判斷 action 型別,並進行設定

如此,我們就實現了換一換功能:

React Demo Three - 簡書&掘金

十七 功能優化

返回目錄

17.1 換一換圖示旋轉

返回目錄

src/common/header/index.css

程式碼詳情
header {
  width: 100%;
  height: 58px;
  display: flex;
  align-items: center;
  border-bottom: 1px solid #ccc;
  font-size: 17px;
}

/* 頭部左邊 */
.header_left-img {
  width: 100px;
  height: 56px;
}

/* 頭部中間 */
.header_center {
  width: 1000px;
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
}
.nav-item {
  margin-right: 30px;
  display: flex;
  align-items: center;
}

/* 頭部中間左部 */
.header_center-left {
  display: flex;
}

/* 頭部中間左部 - 首頁 */
.header_center-left-home {
  color: #ea6f5a;
}

/* 頭部中間左部 - 搜尋框 */
.header_center-left-search {
  position: relative;
}
.slide-enter {
  transition: all .2s ease-out;
}
.slide-enter-active {
  width: 280px;
}
.slide-exit {
  transition: all .2s ease-out;
}
.silde-exit-active {
  width: 240px;
}
.header_center-left-search input {
  width: 240px;
  padding: 0 45px 0 20px;
  height: 38px;
  font-size: 14px;
  border: 1px solid #eee;
  border-radius: 40px;
  background: #eee;
}
.header_center-left-search .input-active {
  width: 280px;
}
.header_center-left-search .icon-search {
  position: absolute;
  top: 8px;
  right: 10px;
}
.header_center-left-search .icon-active {
  padding: 3px;
  top: 4px;
  border-radius: 15px;
  border: 1px solid #ea6f5a;
}

/* 頭部中間左部 - 熱搜 */
.header_center-left-search .icon-active:hover {
  cursor: pointer;
}
.header_center-left-hot-search:before {
  content: "";
  left: 27px;
  width: 10px;
  height: 10px;
  transform: rotate(45deg);
  top: -5px;
  z-index: -1;
  position: absolute;
  background-color: #fff;
  box-shadow: 0 0 8px rgba(0,0,0,.2);
}
.header_center-left-hot-search {
  position: absolute;
  width: 250px;
  left: 0;
  top: 125%;
  padding: 15px;
  font-size: 14px;
  background: #fff;
  border-radius: 4px;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
}
.header_center-left-hot-search-title {
  display: flex;
  justify-content: space-between;
  color: #969696;
}
.header_center-left-hot-search-change {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.icon-change {
  display: inline-block;
  width: 20px;
  height: 14px;
  background: url('../../resources/img/icon-change.png') no-repeat center;
  background-size: 100%;
  /* 1. 在 index.css 中新增動畫 */
  transition: all .2s ease-in;
  transform-origin: center center;
}
.icon-change:hover {
  cursor: pointer;
}
.span-change:hover {
  cursor: pointer;
}
.header_center-left-hot-search-content span {
  display: inline-block;
  margin-top: 10px;
  margin-right: 10px;
  padding: 2px 6px;
  font-size: 12px;
  color: #787878;
  border: 1px solid #ddd;
  border-radius: 3px;
}
.header_center-left-hot-search-content span:hover {
  cursor: pointer;
}

/* 頭部中間右部 */
.header_center-right {
  display: flex;
  color: #969696;
}

/* 頭部右邊 */
.header_right-register, .header_right-write {
  width: 80px;
  text-align: center;
  height: 38px;
  line-height: 38px;
  border: 1px solid rgba(236,97,73,.7);
  border-radius: 20px;
  font-size: 15px;
  color: #ea6f5a;
  background-color: transparent;
}
.header_right-write {
  margin-left: 10px;
  padding-left: 10px;
  margin-right: 0px;
  color: #fff;
  background-color: #ea6f5a;
}
複製程式碼

src/common/header/index.js

程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';

import homeImage from '../../resources/img/header-home.png';

class Header extends Component {
  render() {
    return (
      <header>
        <div className="header_left">
          <a href="/">
            <img alt="首頁" src={homeImage} className="header_left-img" />
          </a>
        </div>
        <div className="header_center">
          <div className="header_center-left">
            <div className="nav-item header_center-left-home">
              <i className="icon icon-home"></i>
              <span>首頁</span>
            </div>
            <div className="nav-item header_center-left-download">
              <i className="icon icon-download"></i>
              <span>下載App</span>
            </div>
            <div className="nav-item header_center-left-search">
              <CSSTransition
                in={this.props.inputFocus}
                timeout={200}
                classNames="slide"
              >
                <input 
                  className={this.props.inputFocus ? 'input-active' : 'input-nor-active'}
                  placeholder="搜尋"
                  onFocus={this.props.searchFocus}
                  onBlur={this.props.searchBlur}
                />
              </CSSTransition>
              <i className={this.props.inputFocus ? 'icon icon-search icon-active' : 'icon icon-search'}></i>
              <div 
                className={this.props.inputFocus || this.props.mouseInHot ? 'display-show header_center-left-hot-search' : 'display-hide header_center-left-hot-search'}
                onMouseEnter={this.props.onMouseEnterHot}
                onMouseLeave={this.props.onMouseLeaveHot}
              >
                <div className="header_center-left-hot-search-title">
                  <span>熱門搜尋</span>
                  {/* 2. 在 index.js 中給 i 標籤新增 ref,並通過 changePage 方法傳遞過去 */}
                  <span onClick={() => this.props.changePage(this.props.page, this.props.totalPage, this.spinIcon)}>
                    <i className="icon-change" ref={(icon) => {this.spinIcon = icon}}></i>
                    <span className="span-change">換一批</span>
                  </span>
                </div>
                <div className="header_center-left-hot-search-content">
                  {
                    this.props.list.map((item, index) => {
                      if(index >= (this.props.page - 1) * 10 && index < this.props.page * 10) {
                        return <span key={item}>{item}</span>
                      } else {
                        return '';
                      }
                    })
                  }
                </div>
              </div>
            </div>
          </div>
          <div className="header_center-right">
            <div className="nav-item header_right-center-setting">
              <span>Aa</span>
            </div>
            <div className="nav-item header_right-center-login">
              <span>登入</span>
            </div>
          </div>
        </div>
        <div className="header_right nav-item">
          <span className="header_right-register">註冊</span>
          <span className="header_right-write nav-item">
            <i className="icon icon-write"></i>
            <span>寫文章</span>
          </span>
        </div>
      </header>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    inputFocus: state.get('header').get('inputFocus'),
    list: state.get('header').get('list'),
    mouseInHot: state.get('header').get('mouseInHot'),
    page: state.get('header').get('page'),
    totalPage: state.get('header').get('totalPage'),
  }
}

const mapDispathToProps = (dispatch) => {
  return {
    searchFocus() {
      dispatch(actionCreators.getList());
      dispatch(actionCreators.searchFocus());
    },
    searchBlur() {
      dispatch(actionCreators.searchBlur());
    },
    onMouseEnterHot() {
      dispatch(actionCreators.onMouseEnterHot());
    },
    onMouseLeaveHot() {
      dispatch(actionCreators.onMouseLeaveHot());
    },
    changePage(page, totalPage, spinIcon) {
      // 3. 在 index.js 中設定它原生 DOM 的 CSS 屬性
      if(spinIcon.style.transform === 'rotate(360deg)') {
        spinIcon.style.transform = 'rotate(0deg)';
      } else {
        spinIcon.style.transform = 'rotate(360deg)';
      }
      if(page === totalPage) {
        page = 1;
        dispatch(actionCreators.changePage(page));
      } else {
        dispatch(actionCreators.changePage(page));
      }
    }
  }
}

export default connect(mapStateToProps, mapDispathToProps)(Header);

複製程式碼

這裡我們通過三個步驟實現了圖示旋轉:

  1. 在 index.css 中新增動畫
  2. 在 index.js 中給 i 標籤新增 ref,並通過 changePage 方法傳遞過去
  3. 在 index.js 中設定它原生 DOM 的 CSS 屬性

實現效果如下:

React Demo Three - 簡書&掘金

17.2 避免聚焦重複請求

返回目錄

在程式碼中,我們每次聚焦,都會請求資料,所以我們需要根據 list 的值來判斷是否請求資料:

src/common/header/index.js

程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';

import homeImage from '../../resources/img/header-home.png';

class Header extends Component {
  render() {
    return (
      <header>
        <div className="header_left">
          <a href="/">
            <img alt="首頁" src={homeImage} className="header_left-img" />
          </a>
        </div>
        <div className="header_center">
          <div className="header_center-left">
            <div className="nav-item header_center-left-home">
              <i className="icon icon-home"></i>
              <span>首頁</span>
            </div>
            <div className="nav-item header_center-left-download">
              <i className="icon icon-download"></i>
              <span>下載App</span>
            </div>
            <div className="nav-item header_center-left-search">
              <CSSTransition
                in={this.props.inputFocus}
                timeout={200}
                classNames="slide"
              >
                <input 
                  className={this.props.inputFocus ? 'input-active' : 'input-nor-active'}
                  placeholder="搜尋"
                  // 1. 給 searchFocus 傳遞 list
                  onFocus={() => this.props.searchFocus(this.props.list)}
                  onBlur={this.props.searchBlur}
                />
              </CSSTransition>
              <i className={this.props.inputFocus ? 'icon icon-search icon-active' : 'icon icon-search'}></i>
              <div 
                className={this.props.inputFocus || this.props.mouseInHot ? 'display-show header_center-left-hot-search' : 'display-hide header_center-left-hot-search'}
                onMouseEnter={this.props.onMouseEnterHot}
                onMouseLeave={this.props.onMouseLeaveHot}
              >
                <div className="header_center-left-hot-search-title">
                  <span>熱門搜尋</span>
                  <span onClick={() => this.props.changePage(this.props.page, this.props.totalPage, this.spinIcon)}>
                    <i className="icon-change" ref={(icon) => {this.spinIcon = icon}}></i>
                    <span className="span-change">換一批</span>
                  </span>
                </div>
                <div className="header_center-left-hot-search-content">
                  {
                    this.props.list.map((item, index) => {
                      if(index >= (this.props.page - 1) * 10 && index < this.props.page * 10) {
                        return <span key={item}>{item}</span>
                      } else {
                        return '';
                      }
                    })
                  }
                </div>
              </div>
            </div>
          </div>
          <div className="header_center-right">
            <div className="nav-item header_right-center-setting">
              <span>Aa</span>
            </div>
            <div className="nav-item header_right-center-login">
              <span>登入</span>
            </div>
          </div>
        </div>
        <div className="header_right nav-item">
          <span className="header_right-register">註冊</span>
          <span className="header_right-write nav-item">
            <i className="icon icon-write"></i>
            <span>寫文章</span>
          </span>
        </div>
      </header>
    )
  }
}

const mapStateToProps = (state) => {
  return {
    inputFocus: state.get('header').get('inputFocus'),
    list: state.get('header').get('list'),
    mouseInHot: state.get('header').get('mouseInHot'),
    page: state.get('header').get('page'),
    totalPage: state.get('header').get('totalPage'),
  }
}

const mapDispathToProps = (dispatch) => {
  return {
    searchFocus(list) {
      // 2. 判斷 list 的 size 是不是等於 0,是的話才請求資料(第一次),不是的話則不請求
      if(list.size === 0) {
        dispatch(actionCreators.getList());
      }
      dispatch(actionCreators.searchFocus());
    },
    searchBlur() {
      dispatch(actionCreators.searchBlur());
    },
    onMouseEnterHot() {
      dispatch(actionCreators.onMouseEnterHot());
    },
    onMouseLeaveHot() {
      dispatch(actionCreators.onMouseLeaveHot());
    },
    changePage(page, totalPage, spinIcon) {
      if(spinIcon.style.transform === 'rotate(360deg)') {
        spinIcon.style.transform = 'rotate(0deg)';
      } else {
        spinIcon.style.transform = 'rotate(360deg)';
      }
      if(page === totalPage) {
        page = 1;
        dispatch(actionCreators.changePage(page));
      } else {
        dispatch(actionCreators.changePage(page));
      }
    }
  }
}

export default connect(mapStateToProps, mapDispathToProps)(Header);

複製程式碼

在這裡,我們做了兩個步驟:

  1. searchFocus 傳遞 list
  2. searchFocus 中判斷 listsize 是不是等於 0,是的話才請求資料(第一次),不是的話則不請求

這樣,我們就成功避免聚焦重複請求。

十八 React 路由

返回目錄

18.1 路由(一)

返回目錄

  • 什麼是路由?

前端路由就是根據 URL 的不同,顯示不同的內容。

  • 安裝 React 的路由:npm i react-router-dom -S

安裝完畢之後,我們只需要修改下 src/App.js,就可以體驗到路由:

src/App.js

程式碼詳情
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import Header from './common/header';
import store from './store';
// 1. 引入 React 路由的 BrowserRouter 和 Route
import { BrowserRouter, Route } from 'react-router-dom';

class App extends Component {
  render() {
    return (
      <Provider store={store} className="App">
        <Header />
        {/* 2. 在頁面中使用 React 路由 */}
        <BrowserRouter>
          <Route path="/" exact render={() => <div>HOME</div>}></Route>
          <Route path="/detail" exact render={() => <div>DETAIL</div>}></Route>
        </BrowserRouter>
      </Provider>
    );
  }
}

export default App;
複製程式碼

在這裡我們僅需要做兩個步驟:

  1. 引入 React 路由的 BrowserRouterRoute
  2. 在頁面中使用 React 路由

這樣,我們就實現了路由:

React Demo Three - 簡書&掘金

18.2 路由(二)

返回目錄

  1. 在 src 下新建 pages 資料夾,然後在該資料夾下新建資料夾和檔案:
    1. src/pages/detail/index.js
    2. src/pages/home/index.js
  2. 它們的內容如下:

React Demo Three - 簡書&掘金

src/pages/detail/index.js

程式碼詳情
import React, { Component } from 'react'

class Detail extends Component {
  render() {
    return (
      <div>Detail</div>
    )
  }
}

export default Detail;
複製程式碼

src/pages/home/index.js

程式碼詳情
import React, { Component } from 'react'

class Home extends Component {
  render() {
    return (
      <div>Home</div>
    )
  }
}

export default Home;
複製程式碼

在有 header 的經驗下,我們應該知道,我們希望在 URL 輸入路徑 localhost:3000 的時候,訪問 home 元件;在輸入 localhost:3000/detail 的時候,訪問 detail 元件。

  1. 到這步,我們僅需要修改下 src/App.js,就可以實現目標:

src/App.js

程式碼詳情
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import Header from './common/header';
import store from './store';
import { BrowserRouter, Route } from 'react-router-dom';
// 1. 引入 Home、Detail 元件
import Home from './pages/home';
import Detail from './pages/detail';

class App extends Component {
  render() {
    return (
      <Provider store={store} className="App">
        <Header />
        <BrowserRouter>
          {/* 2. 在頁面中引用元件 */}
          <Route path="/" exact component={Home}></Route>
          <Route path="/detail" exact component={Detail}></Route>
        </BrowserRouter>
      </Provider>
    );
  }
}

export default App;
複製程式碼

現在,我們切換下路由,就可以看到不用的頁面,這些頁面我們也可以通過編輯對應的 index.js 來修改了。

十九 頁面實現:二級導航欄

返回目錄

由於前面有過程式設計經驗了,所以在這裡我們就不多說廢話,直接進行實現。

「簡書」因違反《網路安全法》《網際網路資訊服務管理辦法》《網際網路新聞資訊服務管理規定》等相關法律法規,嚴重危害網際網路資訊傳播秩序,根據網信主管部門要求,從 2019 年 4 月 13 日 0 時至 4 月 19 日 0 時,暫停更新 PC 端上的內容,並對所有平臺上的內容進行全面徹底的整改。

沒法,本來想根據簡書的首頁繼續編寫的,但是恰巧碰到簡書出問題了,只好拿掘金的首頁和詳情頁來實現了。

React Demo Three - 簡書&掘金

我們將掘金首頁劃分為 3 個模組:頂部 TopNav、左側 LeftList、右側 RightRecommend。所以我們在 home 下面新建個 components 目錄,用來存放這三個元件。同時,在開發 common/header 的時候,我們也知道,還需要一個 store 資料夾,用來存放 reducer.js 等:

- pages
  - detail
    - index.js
  - home
    - components
      - LeftList.js
      - RightRecommend.js
      - TopNav.js
    - store
      - actionCreators.js
      - actionTypes.js
      - index.js
      - reducer.js
    - index.css
    - index.js
複製程式碼
  1. src/index.css
程式碼詳情
body {
  background: #f4f5f5;
}
複製程式碼
  1. src/App.js
程式碼詳情
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import Header from './common/header';
import store from './store';
import { BrowserRouter, Route } from 'react-router-dom';
import Home from './pages/home';
import Detail from './pages/detail';

class App extends Component {
  render() {
    return (
      <Provider store={store} className="App">
        <Header />
        <BrowserRouter>
          <Route path="/" exact component={Home}></Route>
          <Route path="/detail" exact component={Detail}></Route>
        </BrowserRouter>
      </Provider>
    );
  }
}

export default App;
複製程式碼
  1. src/common/header/index.css
程式碼詳情
header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 58px;
  display: flex;
  align-items: center;
  border-bottom: 1px solid #f1f1f1;
  font-size: 17px;
  background: #fff;
}

/* 頭部左邊 */
.header_left-img {
  width: 100px;
  height: 56px;
}

/* 頭部中間 */
.header_center {
  width: 1000px;
  margin: 0 auto;
  display: flex;
  justify-content: space-between;
}
.nav-item {
  margin-right: 30px;
  display: flex;
  align-items: center;
}

/* 頭部中間左部 */
.header_center-left {
  display: flex;
}

/* 頭部中間左部 - 首頁 */
.header_center-left-home {
  color: #ea6f5a;
}

/* 頭部中間左部 - 搜尋框 */
.header_center-left-search {
  position: relative;
}
.slide-enter {
  transition: all .2s ease-out;
}
.slide-enter-active {
  width: 280px;
}
.slide-exit {
  transition: all .2s ease-out;
}
.silde-exit-active {
  width: 240px;
}
.header_center-left-search {
  z-index: 999;
}
.header_center-left-search input {
  width: 240px;
  padding: 0 45px 0 20px;
  height: 38px;
  font-size: 14px;
  border: 1px solid #eee;
  border-radius: 40px;
  background: #eee;
}
.header_center-left-search .input-active {
  width: 280px;
}
.header_center-left-search .icon-search {
  position: absolute;
  top: 8px;
  right: 10px;
}
.header_center-left-search .icon-active {
  padding: 3px;
  top: 4px;
  border-radius: 15px;
  border: 1px solid #ea6f5a;
}

/* 頭部中間左部 - 熱搜 */
.header_center-left-search .icon-active:hover {
  cursor: pointer;
}
.header_center-left-hot-search:before {
  content: "";
  left: 27px;
  width: 10px;
  height: 10px;
  transform: rotate(45deg);
  top: -5px;
  z-index: -1;
  position: absolute;
  background-color: #fff;
  box-shadow: 0 0 8px rgba(0,0,0,.2);
}
.header_center-left-hot-search {
  position: absolute;
  width: 250px;
  left: 0;
  top: 125%;
  padding: 15px;
  font-size: 14px;
  background: #fff;
  border-radius: 4px;
  box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
}
.header_center-left-hot-search-title {
  display: flex;
  justify-content: space-between;
  color: #969696;
}
.header_center-left-hot-search-change {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.icon-change {
  display: inline-block;
  width: 20px;
  height: 14px;
  background: url('../../resources/img/icon-change.png') no-repeat center;
  background-size: 100%;
  transition: all .2s ease-in;
  transform-origin: center center;
}
.icon-change:hover {
  cursor: pointer;
}
.span-change:hover {
  cursor: pointer;
}
.header_center-left-hot-search-content span {
  display: inline-block;
  margin-top: 10px;
  margin-right: 10px;
  padding: 2px 6px;
  font-size: 12px;
  color: #787878;
  border: 1px solid #ddd;
  border-radius: 3px;
}
.header_center-left-hot-search-content span:hover {
  cursor: pointer;
}

/* 頭部中間右部 */
.header_center-right {
  display: flex;
  color: #969696;
}

/* 頭部右邊 */
.header_right-register, .header_right-write {
  width: 80px;
  text-align: center;
  height: 38px;
  line-height: 38px;
  border: 1px solid rgba(236,97,73,.7);
  border-radius: 20px;
  font-size: 15px;
  color: #ea6f5a;
  background-color: transparent;
}
.header_right-write {
  margin-left: 10px;
  padding-left: 10px;
  margin-right: 0px;
  color: #fff;
  background-color: #ea6f5a;
}

複製程式碼
  1. src/pages/home/index.js
程式碼詳情
import React, { Component } from 'react';
import LeftList from './components/LeftList';
import RightRecommend from './components/RightRecommend';
import TopNav from './components/TopNav';
import './index.css';

class Home extends Component {
  render() {
    return (
      <div className="container">
        <TopNav />
        <div className="main-container">
          <LeftList />
          <RightRecommend />
        </div>
      </div>
    )
  }
}

export default Home;
複製程式碼
  1. src/pages/home/index.css
程式碼詳情
/* 主體 */
.container {
  width: 960px;
  margin: 0 auto;
}
.main-container {
  display: flex;
}

/* 頂部 */
.top-nav {
  position: fixed;
  left: 0;
  top: 59px;
  width: 100%;
  height: 46px;
  line-height: 46px;
  z-index: 100;
  box-shadow: 0 1px 2px 0 rgba(0,0,0,.05);
  font-size: 14px;
  background: #fff;
}
.top-nav-list {
  display: flex;
  width: 960px;
  margin: auto;
  position: relative;
}
.top-nav-list-item a {
  height: 100%;
  align-items: center;
  display: flex;
  flex-shrink: 0;
  color: #71777c;
  padding-right: 12px;
}
.active a {
  color: #007fff;
}
.top-nav-list-right {
  position: absolute;
  top: 0;
  right: 0;
}

/* 主內容 */
.main-container {
  margin-top: 120px;
}

/* 左側 */
.left-list {
  width: 650px;
  height: 1000px;
  background: #fff;
}

/* 右側 */
.right-recommend {
  width: 295px;
  height: 1000px;
  margin-left: 15px;
  background: #fff;
}
複製程式碼
  1. src/pages/home/components/TopNav.js
程式碼詳情
import React, { Component } from 'react';
import { Link } from 'react-router-dom';

class TopNav extends Component {
  render() {
    return (
      <div className="top-nav">
        <ul className="top-nav-list">
          <li className="top-nav-list-item active">
            <Link to="tuijian">推薦</Link>
          </li>
          <li className="top-nav-list-item">
            <Link to="guanzhu">關注</Link>
          </li>
          <li className="top-nav-list-item">
            <Link to="houduan">後端</Link>
          </li>
          <li className="top-nav-list-item">
            <Link to="qianduan">前端</Link>
          </li>
          <li className="top-nav-list-item">
            <Link to="anzhuo">Android</Link>
          </li>
          <li className="top-nav-list-item">
            <Link to="ios">IOS</Link>
          </li>
          <li className="top-nav-list-item">
            <Link to="rengongzhineng">人工智慧</Link>
          </li>
          <li className="top-nav-list-item">
            <Link to="kaifagongju">開發工具</Link>
          </li>
          <li className="top-nav-list-item">
            <Link to="daimarensheng">程式碼人生</Link>
          </li>
          <li className="top-nav-list-item">
            <Link to="yuedu">閱讀</Link>
          </li>
          <li className="top-nav-list-item top-nav-list-right">
            <Link to="biaoqianguanli">標籤管理</Link>
          </li>
        </ul>
      </div>
    )
  }
}

export default TopNav;
複製程式碼
  1. src/pages/home/components/LeftList.js
程式碼詳情
import React, { Component } from 'react'

class LeftList extends Component {
  render() {
    return (
      <div className="left-list">
        左側
      </div>
    )
  }
}

export default LeftList;
複製程式碼
  1. src/pages/home/components/RightRecommend.js
程式碼詳情
import React, { Component } from 'react'

class RightRecommend extends Component {
  render() {
    return (
      <div className="right-recommend">
        右側
      </div>
    )
  }
}

export default RightRecommend;
複製程式碼

此時,頁面顯示為:

React Demo Three - 簡書&掘金

二十 頁面實現:首頁

返回目錄

20.1 多層級元件引用 store

返回目錄

在我們規劃中,App 是主元件,下面有 header | home | detail,然後 home 下面有 LeftList | RightRecommend,那麼 App/home/leftList 如何引用 store 呢?

src/pages/home/components/LeftList.js

程式碼詳情
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
// 1. 在 LeftList 中引入 react-redux 的 connect
import { connect } from 'react-redux';
import { actionCreators } from '../store';

class LeftList extends Component {
  render() {
    return (
      <div className="left-list">
        <div className="left-list-top">
          <ul className="left-list-top-left">
            <li className="active">
              <Link to='remen'>熱門</Link>
            </li>
            <span>|</span>
            <li>
              <Link to='zuixin'>最新</Link>
            </li>
            <span>|</span>
            <li>
              <Link to='pinglun'>評論</Link>
            </li>
          </ul>
          <ul className="left-list-top-right">
            <li>
              <Link to='benzhouzuire'>本週最熱</Link>
            </li>
            ·
            <li>
              <Link to='benyuezuire'>本月最熱</Link>
            </li>
            ·
            <li>
              <Link to='lishizuire'>歷史最熱</Link>
            </li>
          </ul>
        </div>
        <div className="left-list-container">
          {/* 5. 迴圈輸出 props 裡面的資料 */}
          {
            this.props.list.map((item) => {
              return (
                <div className="left-list-item" key={item.get('id')}>
                  <div className="left-list-item-tag">
                    <span className="hot"></span>·
                    <span className="special">專欄</span>·
                    <span>
                      {
                        item.get('user').get('username')
                      }
                    </span>·
                    <span>一天前</span>·
                    <span>
                      {
                        item.get('tags').map((tagsItem, index) => {
                          if (index === 0) {
                            return tagsItem.get('title');
                          } else {
                            return null;
                          }
                        })
                      }
                    </span>
                  </div>
                  <h3 className="left-list-item-title">
                    <Link to="detail">{item.get('title')}</Link>
                  </h3>
                  <div className="left-list-item-interactive">
                    <span>{item.get('likeCount')}</span>
                    <span>{item.get('commentsCount')}</span>
                  </div>
                </div>
              )
            })
          }
        </div>
      </div>
    )
  }

  componentDidMount() {
    this.props.getLeftList();
  }
}

// 3. 在 LeftList 中定義 mapStateToProps
const mapStateToProps = (state) => {
  return {
    list: state.get('home').get('leftNav')
  }
};

// 4. 在 LeftList 中定義 mapDispathToProps
const mapDispathToProps = (dispatch) => {
  return {
    getLeftList() {
      dispatch(actionCreators.getLeftList());
    }
  }
};

// 2. 在 LeftList 中使用 connect
export default connect(mapStateToProps, mapDispathToProps)(LeftList);
複製程式碼

20.2 完善整個首頁

返回目錄

當然,如果僅僅是執行上面的程式碼,你會發現它是報錯的。

是的,因為它只是全部程式碼的一部分,所以需要你去完善它。當然,你也可以直接獲取全部程式碼:

不管如何,你實現的最終成果如下所示:

React Demo Three - 簡書&掘金

二十一 總結

返回目錄

寫到這裡,我們已經完成了一個首頁的開發。

在這個開發中,我們學習到了非常多。

當然,後面 jsliang 自己也是偷懶了,慕課原視訊中還有:

  1. 載入更多功能實現
  2. 跳轉到頂部功能實現
  3. 詳情頁開發
  4. 登入頁開發
  5. 登入鑑權功能實現
  6. 單頁面非同步載入元件(react-loadable)
  7. ……

這裡不一一列舉了,因為 jsliang 感覺它們重複性很大,我們只需要在下一個專案中去實踐,相信能獲得更清晰的印象。(當然,前提是你跟 jsliang 一樣有動力深入學習)

那麼,到這裡我們就宣佈結束啦,我們下篇文章見!


jsliang 廣告推送:
也許小夥伴想了解下雲伺服器
或者小夥伴想買一臺雲伺服器
或者小夥伴需要續費雲伺服器
歡迎點選 雲伺服器推廣 檢視!

React Demo Three - 簡書&掘金
React Demo Three - 簡書&掘金

知識共享許可協議
jsliang 的文件庫樑峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議進行許可。
基於github.com/LiangJunron…上的作品創作。
本許可協議授權之外的使用許可權可以從 creativecommons.org/licenses/by… 處獲得。

相關文章