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… 处得到。

相关文章
相关标签/搜索