React Demo Three - 簡書&掘金

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

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


【2019-08-16】Hello 小夥伴們,因爲 jsliang 對文檔庫進行了重構,這篇文章中的一些連接可能失效,而 jsliang 缺少精力維護掘金這邊的舊文章,對此深感抱歉。請須要獲取最新文章的小夥伴,點擊上面的 GitHub 地址,去文檔庫查看調整後的文章。html


本文章最終成果:前端

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

一 目錄

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

目錄
一 目錄
二 前言
三 初始化項目目錄
四 建立 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 完善整個首頁
二十一 總結

二 前言

返回目錄ios

歲月如梭,光陰荏苒。git

既然決定了作某事,那就堅持下去。程序員

相信,堅持一定有收穫,無論它體如今哪一個方面。github

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

三 初始化項目目錄

返回目錄

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

咱們的最終目錄以下所示:

小夥伴們能夠自行新建空文件,在後續不會由於不知道該文件放到哪,從而致使思路錯亂。

而後,咱們經過:

  • 安裝依賴:npm i
  • 運行項目:npm run start

跑起項目來,運行結果以下所示:

接着,咱們在 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;
複製代碼

最後,頁面顯示爲:

由此,咱們完成了 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-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 動畫插件的引入及使用,此時頁面顯示爲:

七 優化代碼

返回目錄

  • 安裝 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 插件。

使用一下:

九 優化:抽取 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);
複製代碼

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

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 提供的數據:

十四 代碼優化

返回目錄

  • 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';
複製代碼

咱們先看實現:

而後咱們看看實現邏輯:

  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 類型,並進行設置

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

十七 功能優化

返回目錄

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 屬性

實現效果以下:

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 路由

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

18.2 路由(二)

返回目錄

  1. 在 src 下新建 pages 文件夾,而後在該文件夾下新建文件夾和文件:
    1. src/pages/detail/index.js
    2. src/pages/home/index.js
  2. 它們的內容以下:

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 端上的內容,並對全部平臺上的內容進行全面完全的整改。

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

咱們將掘金首頁劃分爲 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;
複製代碼

此時,頁面顯示爲:

二十 頁面實現:首頁

返回目錄

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 完善整個首頁

返回目錄

固然,若是僅僅是運行上面的代碼,你會發現它是報錯的。

是的,由於它只是所有代碼的一部分,因此須要你去完善它。固然,你也能夠直接獲取所有代碼:

無論如何,你實現的最終成果以下所示:

二十一 總結

返回目錄

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

在這個開發中,咱們學習到了很是多。

固然,後面 jsliang 本身也是偷懶了,慕課原視頻中還有:

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

這裏不一一列舉了,由於 jsliang 感受它們重複性很大,咱們只須要在下一個項目中去實踐,相信能得到更清晰的印象。(固然,前提是你跟 jsliang 同樣有動力深刻學習)

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


jsliang 廣告推送:
也許小夥伴想了解下雲服務器
或者小夥伴想買一臺雲服務器
或者小夥伴須要續費雲服務器
歡迎點擊 雲服務器推廣 查看!

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

相關文章
相關標籤/搜索