内容简介:Create byRecently revised inHello 小伙伴们,如果觉得本文还不错,记得给个star, 小伙伴们的star是我持续更新的动力!
Create by jsliang on 2019-4-7 19:37:41
Recently revised in 2019-04-23 09:40:44
Hello 小伙伴们,如果觉得本文还不错,记得给个star, 小伙伴们的star是我持续更新的动力! GitHub 地址
本文章最终成果:
本来这只是篇纯粹的仿简书首页和文章详情页的文章,但是中间出了点情况(第十九章有提到),所以最终出来的是简书和掘金的混合体~
一 目录
不折腾的前端,和咸鱼有什么区别
目录 |
---|
四 创建 React 头部组件 |
八 使用 redux-devtools-extension 插件 |
九 优化:抽取 reducer.js |
十一 优化:immutable.js |
十二 优化:redux-immutable |
十三 功能实现:热门搜索 |
十五 解决历史遗留问题 |
十六 功能实现:换一换 |
17.2 避免聚焦重复请求 |
十九 页面实现:二级导航栏 |
20.1 多层级组件引用 store |
二 前言
岁月如梭,光阴荏苒。
既然决定了做某事,那就坚持下去。
相信,坚持必定有收获,不管它体现在哪个方面。
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; } 复制代码
这样,经过四个操作步骤:
- 安装动画库:
npm i react-transition-group -S
- 引入动画库
- 通过
CSSTransition
包裹input
- 编写对应的 CSS 样式
我们就成功实现了 CSS 动画插件的引入及使用,此时页面显示为:
七 优化代码
npm i redux -S npm i react-redux -S
- 首先 ,创建 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; } 复制代码
- 接着 ,在 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; 复制代码
- 然后 ,修改 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); 复制代码
- 再来 ,我们再修改下 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; } 复制代码
- 此时 ,我们完成了修改的步骤。同时,这时候因为 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); 复制代码
- 最后 ,我们完成了 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
- 首先 ,在 header 的 store 中新建 actionCreators.js 文件:
src/common/header/store/actionCreators.js
// 1. 定义 actionCreators export const searchFocusOrBlur = () => ({ type: 'search_focus_or_blur' }) 复制代码
- 然后 ,我们在 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); 复制代码
- 接着 ,因为我们在 actionCreators.js 中使用的
type
是字符串,所以我们同样在 store 中创建 actionTypes.js,将其变成常量:
src/common/header/store/actionTypes.js
export const SEARCH_FOCUS_OR_BLUR = 'search_focus_or_blur'; 复制代码
- 再然后 ,我们在 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 }) 复制代码
- 再接着 ,我们修改下 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; } 复制代码
- 然后 ,我们现在 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 }; 复制代码
- 此时 ,值得注意的是,这时候我们需要处理下 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'; // 代码省略 复制代码
- 最后 ,再处理下 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
不可被改变。
npm i immutable -S
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 的引用及使用:
- 通过
import
immutable
引入fromJS
- 对
defaultState
使用fromJS
- 这时候我们就不能直接修改
matStateToProps
中的值了,而是 通过immutable
提供的get()
方法来获取inputBlur
属性 - 通过
immutable
的方法来set
state
的值。immutable
对象的set
方法,会结合之前immutable
对象的值和设置的值,返回一个全新的对象
这样,我们就成功保护了 state
的值。
十二 优化:redux-immutable
当然,在上面,我们保护了 header 中的 state
,我们在代码中:
inputBlur: state.header.get('inputBlur') 复制代码
这个 header
也是 state
的值,所以我们也需要对它进行保护,所以我们就需要 redux-immutable
npm i redux-immutable -S
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
的值:
- 安装 redux-immutable:
npm i redux-immutable -S
- 通过 redux-immutable 引入
combineReducers
而非原先的 redux - 通过同样的
get
方法来获取header
十三 功能实现:热门搜索
本章节完成三个功能:
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:
cnpm i redux-thunk -S 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; 复制代码
applyMiddleware applyMiddleware
这样,我们就可以正常使用 redux-thunk 了。
- 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); 复制代码
- 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); }); } } 复制代码
- 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'; 复制代码
- 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; } 复制代码
- public/api/headerList.json
{ "code": 0, "list": ["区块链","小程序","vue","毕业","PHP","故事","flutter","理财","美食","投稿","手帐","书法","PPT","穿搭","打碗碗花","简书","姥姥的澎湖湾","设计","创业","交友","籽盐","教育","思维导图","疯哥哥","梅西","时间管理","golang","连载","自律","职场","考研","慢世人","悦欣","一纸vr","spring","eos","足球","程序员","林露含","彩铅","金融","木风杂谈","日更","成长","外婆是方言","docker"] } 复制代码
通过下面步骤:
- 派发
action
到 actionCreators.js 中的getList()
方法 - 编写
getList
的action
,由于需要actionTypes
中定义,所以前往 actionTypes.js 中新增 - 新增 actionType
- 引入 axios
- 调用 create-react-app 中提供的 Node 服务器,从而 mock 数据
- 给 header 下的 reducer.js 提供存储数据的地方
- 定义
action
,接受参数data
,同时因为我们使用了 Immutable,所以需要将获取的数据转换为immutable
类型 - 引入 Immutable 的类型转换
- 派发
changeList
类型 - 判断
actionTypes
是否为GET_LIST
,如果是则执行该action
- 获取 reducer.js 中的
list
数据 - 遍历输出该数据
这样,我们就成功地获取了 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; } } 复制代码
十五 解决历史遗留问题
在这里,我们解决下历史遗留问题:在我们失焦于输入框的时候,我们的【热门搜索】模块就会消失,从而看不到我们点击【换一换】按钮的效果,所以我们需要修改下代码,在我们鼠标在【热门模块】中时,这个模块不会消失,当我们鼠标失焦且鼠标不在热门模块中时,热门模块才消失。
- 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; } } 复制代码
- 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); 复制代码
- 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) }) 复制代码
- 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'; 复制代码
我们先看实现:
然后我们看看实现逻辑:
- 在 reducer.js 中设置鼠标移动到热门模块为
false
- 在 index.js 中设置移入为
onMouseEnterHot
,移出为onMouseLeaveHot
- 在 index.js 中
mapDispathToProps
定义onMouseEnterHot
和onMouseLeaveHot
方法 - 在 actionCreators.js 中定义这两个方法:
onMouseEnterHot
和onMouseLeaveHot
- 在 actionTypes.js 中新增
action
类型 - 在 reducer.js 中判断这两个
action
执行设置mouseInHot
- 在 index.js 中
mapStateToProps
获取mouseInHot
- 在 index.js 中的判断中加多一个
this.props.mouseInHot
,这样只要有一个为true
,它就不会消失
注意:由于之前设置的 this.props.inputFoucsOrBlur
会造成聚焦和失焦都会调用一次接口,而且逻辑比较复杂,容易出错,所以这里我们进行了修改,将其分为聚焦和失焦两部分。
十六 功能实现:换一换
下面我们开始做换一换功能:
- 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; } } 复制代码
- 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, }) 复制代码
- 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); 复制代码
- 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'; 复制代码
此时我们代码思路是:
- 在 reducer.js 中设置页数
page
和总页数totalPage
- 在 actionCreators.js 中,之前由于数据太多,我们之前限制数据量为 15,这里我们去掉该行代码
- 在 actionCreators.js 这里计算总页数
- 在 reducer.js 中通过
merge
方法同时设置多个state
值 - 在 index.js 中
mapStateToProps
获取数据 - 在 index.js 中进行计算:一开始显示 0-9 共 10 条,换页的时候显示 10-19 ……以此类推
- 在 index.js 中进行换页功能实现,传递参数
page
和totalPage
- 在 index.js 调用
changePage
方法,进行是否重置为第一页判断,并dispatch
方法 - 在 actionCreators.js 中定义
changePage
方法 - 在 actionTypes.js 中定义
action
- 在 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); 复制代码
这里我们通过三个步骤实现了图标旋转:
- 在 index.css 中添加动画
- 在 index.js 中给
i
标签添加ref
,并通过changePage
方法传递过去 - 在 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); 复制代码
在这里,我们做了两个步骤:
- 给
searchFocus
传递list
- 在
searchFocus
中判断list
的size
是不是等于 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; 复制代码
在这里我们仅需要做两个步骤:
- 引入 React 路由的
BrowserRouter
和Route
- 在页面中使用 React 路由
这样,我们就实现了路由:
18.2 路由(二)
- 在 src 下新建 pages 文件夹,然后在该文件夹下新建文件夹和文件:
- src/pages/detail/index.js
- src/pages/home/index.js
- 它们的内容如下:
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 组件。
- 到这步,我们仅需要修改下
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 复制代码
- src/index.css
body { background: #f4f5f5; } 复制代码
- 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; 复制代码
- 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; } 复制代码
- 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; 复制代码
- 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; } 复制代码
- 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; 复制代码
- 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; 复制代码
- 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 完善整个首页
当然,如果仅仅是运行上面的代码,你会发现它是报错的。
是的,因为它只是全部代码的一部分,所以需要你去完善它。当然,你也可以直接获取全部代码:
不管如何,你实现的最终成果如下所示:
以上所述就是小编给大家介绍的《React Demo Three - 简书&掘金》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 掘金小册写作参考
- 使用xposed更改掘金的侧滑退出的触发范围(左撇子,掘金的这个侧滑退出的体验一言难尽)
- 掘金小册优惠折扣一览
- React Demo Four - 掘金
- 为掘金小册添加目录
- 掘金翻译计划月报 — 2018 年 11 月
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Design for Hackers
David Kadavy / Wiley / 2011-10-18 / USD 39.99
Discover the techniques behind beautiful design?by deconstructing designs to understand them The term ?hacker? has been redefined to consist of anyone who has an insatiable curiosity as to how thin......一起来看看 《Design for Hackers》 这本书的介绍吧!