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