Create by jsliang on 2019-4-7 19:37:41
Recently revised in 2019-04-23 09:40:44php
Hello 小伙伴们,若是以为本文还不错,记得给个 star , 小伙伴们的 star 是我持续更新的动力!GitHub 地址css
【2019-08-16】Hello 小伙伴们,因为 jsliang 对文档库进行了重构,这篇文章中的一些连接可能失效,而 jsliang 缺少精力维护掘金这边的旧文章,对此深感抱歉。请须要获取最新文章的小伙伴,点击上面的 GitHub 地址,去文档库查看调整后的文章。html
本文章最终成果:前端
原本这只是篇纯粹的仿简书首页和文章详情页的文章,可是中间出了点状况(第十九章有提到),因此最终出来的是简书和掘金的混合体~vue
不折腾的前端,和咸鱼有什么区别react
返回目录ios
岁月如梭,光阴荏苒。git
既然决定了作某事,那就坚持下去。程序员
相信,坚持一定有收获,无论它体如今哪一个方面。github
React 的学习,迈开 TodoList,进一步前行。
首先,引入 Simplify 目录的内容到 JianShu 文件夹。或者前往文章 《React Demo One - TodoList》 手动进行项目简化。
咱们的最终目录以下所示:
小伙伴们能够自行新建空文件,在后续不会由于不知道该文件放到哪,从而致使思路错乱。
而后,咱们经过:
npm i
npm run start
跑起项目来,运行结果以下所示:
接着,咱们在 src 目录下引入 reset.css,去除各类浏览器的差别性影响。
src/reset.css
/*
* reset 的目的不是让默认样式在全部浏览器下一致,而是减小默认样式有可能带来的问题。
* The purpose of reset is not to allow default styles to be consistent across all browsers, but to reduce the potential problems of default styles.
* create by jsliang
*/
/** 清除内外边距 - clearance of inner and outer margins **/
body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* 结构元素 - structural elements */
dl, dt, dd, ul, ol, li, /* 列表元素 - list elements */
pre, /* 文本格式元素 - text formatting elements */
form, fieldset, legend, button, input, textarea, /* 表单元素 - from elements */
th, td /* 表格元素 - table elements */ {
margin: 0;
padding: 0;
}
/** 设置默认字体 - setting the default font **/
body, button, input, select, textarea {
font: 18px/1.5 '黑体', Helvetica, sans-serif;
}
h1, h2, h3, h4, h5, h6, button, input, select, textarea { font-size: 100%; }
/** 重置列表元素 - reset the list element **/
ul, ol { list-style: none; }
/** 重置文本格式元素 - reset the text format element **/
a, a:hover { text-decoration: none; }
/** 重置表单元素 - reset the form element **/
button { cursor: pointer; }
input { font-size: 18px; outline: none; }
/** 重置表格元素 - reset the table element **/
table { border-collapse: collapse; border-spacing: 0; }
/*
* 图片自适应 - image responsize
* 1. 清空浏览器对图片的设置
* 2. <div>图片</div> 的状况下,图片会撑高 div,这么设置能够清除该影响
*/
img { border: 0; display: inline-block; width: 100%; max-width: 100%; height: auto; vertical-align: middle; }
/*
* 默认box-sizing是content-box,该属性致使padding会撑大div,使用border-box能够解决该问题
* set border-box for box-sizing when you use div, it solve the problem when you add padding and don't want to make the div width bigger
*/
div, input { box-sizing: border-box; }
/** 清除浮动 - clear float **/
.jsliang-clear:after, .clear:after {
content: '\20';
display: block;
height: 0;
clear: both;
}
.jsliang-clear, .clear {
*zoom: 1;
}
/** 设置input的placeholder - set input placeholder **/
input::-webkit-input-placeholder { color: #919191; font-size: 1em } /* Webkit browsers */
input::-moz-placeholder { color: #919191; font-size: 1em } /* Mozilla Firefox */
input::-ms-input-placeholder { color: #919191; font-size: 1em } /* Internet Explorer */
复制代码
顺带建立一个空的全局样式 index.css 文件。
并在 index.js 中引入 reset.css 和 index.css。
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './reset.css';
import './index.css';
ReactDOM.render(<App />, document.getElementById('root')); 复制代码
首先,在 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 动画插件的引入及使用,此时页面显示为:
npm i redux -S
npm i react-redux -S
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;
}
复制代码
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
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);
复制代码
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;
}
复制代码
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);
复制代码
因为咱们只是将必要的数据存储到 state 中,因此样式和功能无变化,故不贴出效果图。
修改 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 随着项目的开发愈来愈庞大,最后到不可维护的地步。
该视频的慕课讲师也提到:当你的一个 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 的优化。
src/common/header/store/actionCreators.js
// 1. 定义 actionCreators
export const searchFocusOrBlur = () => ({
type: 'search_focus_or_blur'
})
复制代码
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);
复制代码
type
是字符串,因此咱们一样在 store 中建立 actionTypes.js,将其变成常量:src/common/header/store/actionTypes.js
export const SEARCH_FOCUS_OR_BLUR = 'search_focus_or_blur';
复制代码
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
})
复制代码
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;
}
复制代码
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 };
复制代码
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';
// 代码省略
复制代码
import { combineReducers } from 'redux';
// 10. 修改下引用方式
import { reducer as headerReducer } from '../common/header/store';
const reducer = combineReducers({
header: headerReducer
})
export default reducer;
复制代码
至此,咱们就完成了本次的优化抽取。
在咱们工做的过程当中,若是一不当心,就会修改了 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
的值。
固然,在上面,咱们保护了 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
的值:
npm i redux-immutable -S
combineReducers
而非原先的 reduxget
方法来获取 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 和 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 中新增action
,接受参数 data
,同时由于咱们使用了 Immutable,因此须要将获取的数据转换为 immutable
类型changeList
类型actionTypes
是否为 GET_LIST
,若是是则执行该 action
list
数据这样,咱们就成功地获取了 mock 提供的数据:
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';
复制代码
咱们先看实现:
而后咱们看看实现逻辑:
false
onMouseEnterHot
,移出为 onMouseLeaveHot
mapDispathToProps
定义 onMouseEnterHot
和 onMouseLeaveHot
方法onMouseEnterHot
和 onMouseLeaveHot
action
类型action
执行设置 mouseInHot
mapStateToProps
获取 mouseInHot
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';
复制代码
此时咱们代码思路是:
page
和总页数 totalPage
merge
方法同时设置多个 state
值mapStateToProps
获取数据page
和 totalPage
changePage
方法,进行是否重置为第一页判断,并 dispatch
方法changePage
方法action
action
类型,并进行设置如此,咱们就实现了换一换功能:
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);
复制代码
这里咱们经过三个步骤实现了图标旋转:
i
标签添加 ref
,并经过 changePage
方法传递过去实现效果以下:
在代码中,咱们每次聚焦,都会请求数据,因此咱们须要根据 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,是的话才请求数据(第一次),不是的话则不请求这样,咱们就成功避免聚焦重复请求。
前端路由就是根据 URL 的不一样,显示不一样的内容。
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;
复制代码
在这里咱们仅须要作两个步骤:
BrowserRouter
和 Route
这样,咱们就实现了路由:
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;
复制代码
此时,页面显示为:
在咱们规划中,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);
复制代码
固然,若是仅仅是运行上面的代码,你会发现它是报错的。
是的,由于它只是所有代码的一部分,因此须要你去完善它。固然,你也能够直接获取所有代码:
无论如何,你实现的最终成果以下所示:
写到这里,咱们已经完成了一个首页的开发。
在这个开发中,咱们学习到了很是多。
固然,后面 jsliang 本身也是偷懒了,慕课原视频中还有:
这里不一一列举了,由于 jsliang 感受它们重复性很大,咱们只须要在下一个项目中去实践,相信能得到更清晰的印象。(固然,前提是你跟 jsliang 同样有动力深刻学习)
那么,到这里咱们就宣布结束啦,咱们下篇文章见!
jsliang 广告推送:
也许小伙伴想了解下云服务器
或者小伙伴想买一台云服务器
或者小伙伴须要续费云服务器
欢迎点击 云服务器推广 查看!
jsliang 的文档库 由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议进行许可。
基于github.com/LiangJunron…上的做品创做。
本许可协议受权以外的使用权限能够从 creativecommons.org/licenses/by… 处得到。