Create by jsliang on 2019-4-7 19:37:41
Recently revised in 2019-04-23 09:40:44
Hello 小伙伴们,如果觉得本文还不错,记得给个 star , 小伙伴们的 star 是我持续更新的动力!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'));
复制代码
至此,我们页面展示为:
六 设置输入框动画
- 安装动画库:
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 动画插件的引入及使用,此时页面显示为:
七 优化代码
- 安装 Redux:
npm i redux -S
- 安装 React-Redux:
npm i react-redux -S
- 开始在代码中加入 Redux 和 React-Redux
- 首先,创建 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
不可被改变。
- 安装 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 的引用及使用:
- 通过
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
- 安装 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
的值:
- 安装 redux-immutable:
npm i redux-immutable -S
- 通过 redux-immutable 引入
combineReducers
而非原先的 redux - 通过同样的
get
方法来获取header
十三 功能实现:热门搜索
本章节完成三个功能:
- 写热门搜索显示隐藏
- 安装 redux-thunk
- 使用 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:
- 安装 redux-thunk:
cnpm i redux-thunk -S
- 安装 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;
复制代码
- 引入 redux-thunk
- 引入 redux 的
applyMiddleware
,进行多中间件的使用 - 通过
applyMiddleware
同时使用 redux-thunk 和 redux-dev-tools
这样,我们就可以正常使用 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 完善整个首页
当然,如果仅仅是运行上面的代码,你会发现它是报错的。
是的,因为它只是全部代码的一部分,所以需要你去完善它。当然,你也可以直接获取全部代码:
不管如何,你实现的最终成果如下所示:
二十一 总结
写到这里,我们已经完成了一个首页的开发。
在这个开发中,我们学习到了非常多。
当然,后面 jsliang 自己也是偷懒了,慕课原视频中还有:
- 加载更多功能实现
- 跳转到顶部功能实现
- 详情页开发
- 登录页开发
- 登录鉴权功能实现
- 单页面异步加载组件(react-loadable)
- ……
这里不一一列举了,因为 jsliang 感觉它们重复性很大,我们只需要在下一个项目中去实践,相信能获得更清晰的印象。(当然,前提是你跟 jsliang 一样有动力深入学习)
那么,到这里我们就宣布结束啦,我们下篇文章见!
jsliang 广告推送:
也许小伙伴想了解下云服务器
或者小伙伴想买一台云服务器
或者小伙伴需要续费云服务器
欢迎点击 云服务器推广 查看!
jsliang 的文档库 由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于github.com/LiangJunron…上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。