Create by jsliang on 2019-4-7 19:37:41
Recently revised in 2019-04-23 09:40:44
Hello 小夥伴們,如果覺得本文還不錯,記得給個 star , 小夥伴們的 star 是我持續更新的動力!GitHub 地址
本文章最終成果:
本來這只是篇純粹的仿簡書首頁和文章詳情頁的文章,但是中間出了點情況(第十九章有提到),所以最終出來的是簡書和掘金的混合體~
一 目錄
不折騰的前端,和鹹魚有什麼區別
二 前言
歲月如梭,光陰荏苒。
既然決定了做某事,那就堅持下去。
相信,堅持必定有收穫,不管它體現在哪個方面。
React 的學習,邁開 TodoList,進一步前行。
三 初始化專案目錄
首先,引入 Simplify 目錄的內容到 JianShu 資料夾。或者前往文章 《React Demo One - TodoList》 手動進行專案簡化。
我們的最終目錄如下所示:
小夥伴們可以自行新建空檔案,在後續不會因為不知道該檔案放到哪,從而導致思路錯亂。
然後,我們通過:
- 安裝依賴:
npm i
- 執行專案:
npm run start
跑起專案來,執行結果如下所示:
接著,我們在 src 目錄下引入 reset.css,去除各種瀏覽器的差異性影響。
src/reset.css
程式碼詳情
/*
* reset 的目的不是讓預設樣式在所有瀏覽器下一致,而是減少預設樣式有可能帶來的問題。
* The purpose of reset is not to allow default styles to be consistent across all browsers, but to reduce the potential problems of default styles.
* create by jsliang
*/
/** 清除內外邊距 - clearance of inner and outer margins **/
body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, /* 結構元素 - structural elements */
dl, dt, dd, ul, ol, li, /* 列表元素 - list elements */
pre, /* 文字格式元素 - text formatting elements */
form, fieldset, legend, button, input, textarea, /* 表單元素 - from elements */
th, td /* 表格元素 - table elements */ {
margin: 0;
padding: 0;
}
/** 設定預設字型 - setting the default font **/
body, button, input, select, textarea {
font: 18px/1.5 '黑體', Helvetica, sans-serif;
}
h1, h2, h3, h4, h5, h6, button, input, select, textarea { font-size: 100%; }
/** 重置列表元素 - reset the list element **/
ul, ol { list-style: none; }
/** 重置文字格式元素 - reset the text format element **/
a, a:hover { text-decoration: none; }
/** 重置表單元素 - reset the form element **/
button { cursor: pointer; }
input { font-size: 18px; outline: none; }
/** 重置表格元素 - reset the table element **/
table { border-collapse: collapse; border-spacing: 0; }
/*
* 圖片自適應 - image responsize
* 1. 清空瀏覽器對圖片的設定
* 2. <div>圖片</div> 的情況下,圖片會撐高 div,這麼設定可以清除該影響
*/
img { border: 0; display: inline-block; width: 100%; max-width: 100%; height: auto; vertical-align: middle; }
/*
* 預設box-sizing是content-box,該屬性導致padding會撐大div,使用border-box可以解決該問題
* set border-box for box-sizing when you use div, it solve the problem when you add padding and don't want to make the div width bigger
*/
div, input { box-sizing: border-box; }
/** 清除浮動 - clear float **/
.jsliang-clear:after, .clear:after {
content: '\20';
display: block;
height: 0;
clear: both;
}
.jsliang-clear, .clear {
*zoom: 1;
}
/** 設定input的placeholder - set input placeholder **/
input::-webkit-input-placeholder { color: #919191; font-size: 1em } /* Webkit browsers */
input::-moz-placeholder { color: #919191; font-size: 1em } /* Mozilla Firefox */
input::-ms-input-placeholder { color: #919191; font-size: 1em } /* Internet Explorer */
複製程式碼
順帶建立一個空的全域性樣式 index.css 檔案。
並在 index.js 中引入 reset.css 和 index.css。
src/index.js
程式碼詳情
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './reset.css';
import './index.css';
ReactDOM.render(<App />, document.getElementById('root'));
複製程式碼
四 建立 React 頭部元件
首先,在 src 目錄下,新建 common 目錄,並在 common 目錄下,新建 header 目錄,其中的 index.js 內容如下:
src/common/header/index.js
程式碼詳情
import React, { Component } from 'react';
class Header extends Component {
render() {
return (
<div>
<h1>Header</h1>
</div>
)
}
}
export default Header;
複製程式碼
然後,我們在 App.js 中引入 header.js:
src/App.js
程式碼詳情
import React, { Component } from 'react';
import Header from './common/header';
class App extends Component {
render() {
return (
<div className="App">
<Header />
</div>
);
}
}
export default App;
複製程式碼
最後,頁面顯示為:
由此,我們完成了 Header 元件的建立。
五 編寫簡書頭部導航
首先,我們編寫 src/common/header 下的 index.js:
src/common/heder/index.js
程式碼詳情
import React, { Component } from 'react';
import './index.css';
import homeImage from '../../resources/img/header-home.png';
class Header extends Component {
constructor(props) {
super(props);
this.state = {
inputFocus: true
}
this.searchFocusOrBlur = this.searchFocusOrBlur.bind(this);
}
render() {
return (
<header>
<div className="header_left">
<a href="/">
<img alt="首頁" src={homeImage} className="headef_left-img" />
</a>
</div>
<div className="header_center">
<div className="header_center-left">
<div className="nav-item header_center-left-home">
<i className="icon icon-home"></i>
<span>首頁</span>
</div>
<div className="nav-item header_center-left-download">
<i className="icon icon-download"></i>
<span>下載App</span>
</div>
<div className="nav-item header_center-left-search">
<input
className={this.state.inputFocus ? 'input-nor-active' : 'input-active'}
placeholder="搜尋"
onFocus={this.searchFocusOrBlur}
onBlur={this.searchFocusOrBlur}
/>
<i className={this.state.inputFocus ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
</div>
</div>
<div className="header_center-right">
<div className="nav-item header_right-center-setting">
<span>Aa</span>
</div>
<div className="nav-item header_right-center-login">
<span>登入</span>
</div>
</div>
</div>
<div className="header_right nav-item">
<span className="header_right-register">註冊</span>
<span className="header_right-write nav-item">
<i className="icon icon-write"></i>
<span>寫文章</span>
</span>
</div>
</header>
)
}
searchFocusOrBlur(e) {
const inputFocus = this.state.inputFocus;
this.setState( () => ({
inputFocus: !inputFocus
}))
}
}
export default Header;
複製程式碼
然後,我們新增 CSS 樣式:
src/common/heder/index.css
程式碼詳情
header {
width: 100%;
height: 58px;
display: flex;
align-items: center;
border-bottom: 1px solid #ccc;
font-size: 17px;
}
.headef_left-img {
width: 100px;
height: 56px;
}
.header_center {
width: 1000px;
margin: 0 auto;
display: flex;
justify-content: space-between;
}
.nav-item {
margin-right: 30px;
display: flex;
align-items: center;
}
.header_center-left {
display: flex;
}
.header_center-left-home {
color: #ea6f5a;
}
.header_center-left-search {
position: relative;
}
.header_center-left-search input {
width: 240px;
padding: 0 40px 0 20px;
height: 38px;
font-size: 14px;
border: 1px solid #eee;
border-radius: 40px;
background: #eee;
}
.header_center-left-search .input-active {
width: 280px;
}
.header_center-left-search i {
position: absolute;
top: 8px;
right: 10px;
}
.header_center-left-search .icon-active {
padding: 3px;
top: 4px;
border-radius: 15px;
border: 1px solid #ea6f5a;
}
.header_center-left-search .icon-active:hover {
cursor: pointer;
}
.header_center-right {
display: flex;
color: #969696;
}
.header_right-register, .header_right-write {
width: 80px;
text-align: center;
height: 38px;
line-height: 38px;
border: 1px solid rgba(236,97,73,.7);
border-radius: 20px;
font-size: 15px;
color: #ea6f5a;
background-color: transparent;
}
.header_right-write {
margin-left: 10px;
padding-left: 10px;
margin-right: 0px;
color: #fff;
background-color: #ea6f5a;
}
複製程式碼
接著,由於圖示這些,我們可以抽取到公用樣式表中,所以我們在 src 目錄下新增 common.css:
src/common.css
程式碼詳情
.icon {
display: inline-block;
width: 20px;
height: 21px;
margin-right: 5px;
}
.icon-home {
background: url('./resources/img/icon-home.png') no-repeat center;
background-size: 100%;
}
.icon-write {
background: url('./resources/img/icon-write.png') no-repeat center;
background-size: 100%;
}
.icon-download {
background: url('./resources/img/icon-download.png') no-repeat center;
background-size: 100%;
}
.icon-search {
background: url('./resources/img/icon-search.png') no-repeat center;
background-size: 100%;
}
複製程式碼
當然,我們需要位置存放圖片,所以需要在 src 目錄下,新建 recourses 目錄,recourses 目錄下存放 img 資料夾,該資料夾存放這些圖示檔案。
最後,我們在 src 下的 index.js 中引用 common.css
src/index.js
程式碼詳情
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './reset.css';
import './index.css';
import './common.css';
ReactDOM.render(<App />, document.getElementById('root'));
複製程式碼
至此,我們頁面展示為:
六 設定輸入框動畫
- 安裝動畫庫:
npm i react-transition-group -S
修改程式碼:
src/common/header/index.js
程式碼詳情
import React, { Component } from 'react';
// 1. 引入動畫庫
import { CSSTransition } from 'react-transition-group';
import './index.css';
import homeImage from '../../resources/img/header-home.png';
class Header extends Component {
constructor(props) {
super(props);
this.state = {
inputBlur: true
}
this.searchFocusOrBlur = this.searchFocusOrBlur.bind(this);
}
render() {
return (
<header>
<div className="header_left">
<a href="/">
<img alt="首頁" src={homeImage} className="headef_left-img" />
</a>
</div>
<div className="header_center">
<div className="header_center-left">
<div className="nav-item header_center-left-home">
<i className="icon icon-home"></i>
<span>首頁</span>
</div>
<div className="nav-item header_center-left-download">
<i className="icon icon-download"></i>
<span>下載App</span>
</div>
<div className="nav-item header_center-left-search">
{/* 2. 通過 CSSTransition 包裹 input */}
<CSSTransition
in={this.state.inputBlur}
timeout={200}
classNames="slide"
>
<input
className={this.state.inputBlur ? 'input-nor-active' : 'input-active'}
placeholder="搜尋"
onFocus={this.searchFocusOrBlur}
onBlur={this.searchFocusOrBlur}
/>
</CSSTransition>
<i className={this.state.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
</div>
</div>
<div className="header_center-right">
<div className="nav-item header_right-center-setting">
<span>Aa</span>
</div>
<div className="nav-item header_right-center-login">
<span>登入</span>
</div>
</div>
</div>
<div className="header_right nav-item">
<span className="header_right-register">註冊</span>
<span className="header_right-write nav-item">
<i className="icon icon-write"></i>
<span>寫文章</span>
</span>
</div>
</header>
)
}
searchFocusOrBlur(e) {
const inputBlur = this.state.inputBlur;
this.setState( () => ({
inputBlur: !inputBlur
}))
}
}
export default Header;
複製程式碼
src/common/header/index.css
程式碼詳情
header {
width: 100%;
height: 58px;
display: flex;
align-items: center;
border-bottom: 1px solid #ccc;
font-size: 17px;
}
.headef_left-img {
width: 100px;
height: 56px;
}
.header_center {
width: 1000px;
margin: 0 auto;
display: flex;
justify-content: space-between;
}
.nav-item {
margin-right: 30px;
display: flex;
align-items: center;
}
.header_center-left {
display: flex;
}
.header_center-left-home {
color: #ea6f5a;
}
.header_center-left-search {
position: relative;
}
/* 3. 編寫對應的 CSS 樣式 */
.slide-enter {
transition: all .2s ease-out;
}
.slide-enter-active {
width: 280px;
}
.slide-exit {
transition: all .2s ease-out;
}
.silde-exit-active {
width: 240px;
}
/* 3. 結束 */
.header_center-left-search input {
width: 240px;
padding: 0 40px 0 20px;
height: 38px;
font-size: 14px;
border: 1px solid #eee;
border-radius: 40px;
background: #eee;
}
.header_center-left-search .input-active {
width: 280px;
}
.header_center-left-search i {
position: absolute;
top: 8px;
right: 10px;
}
.header_center-left-search .icon-active {
padding: 3px;
top: 4px;
border-radius: 15px;
border: 1px solid #ea6f5a;
}
.header_center-left-search .icon-active:hover {
cursor: pointer;
}
.header_center-right {
display: flex;
color: #969696;
}
.header_right-register, .header_right-write {
width: 80px;
text-align: center;
height: 38px;
line-height: 38px;
border: 1px solid rgba(236,97,73,.7);
border-radius: 20px;
font-size: 15px;
color: #ea6f5a;
background-color: transparent;
}
.header_right-write {
margin-left: 10px;
padding-left: 10px;
margin-right: 0px;
color: #fff;
background-color: #ea6f5a;
}
複製程式碼
這樣,經過四個操作步驟:
- 安裝動畫庫:
npm i react-transition-group -S
- 引入動畫庫
- 通過
CSSTransition
包裹input
- 編寫對應的 CSS 樣式
我們就成功實現了 CSS 動畫外掛的引入及使用,此時頁面顯示為:
七 優化程式碼
- 安裝 Redux:
npm i redux -S
- 安裝 React-Redux:
npm i react-redux -S
- 開始在程式碼中加入 Redux 和 React-Redux
- 首先,建立 store 資料夾,並在裡面建立 index.js 和 reducer.js:
src/store/index.js
程式碼詳情
import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(reducer);
export default store;
複製程式碼
src/store/reducer.js
程式碼詳情
const defaultState = {
inputBlur: true
};
export default (state = defaultState, action) => {
return state;
}
複製程式碼
- 接著,在 App.js 中引用 react-redux 以及 store/index.js:
src/App.js
程式碼詳情
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import Header from './common/header';
import store from './store';
class App extends Component {
render() {
return (
<Provider store={store} className="App">
<Header />
</Provider>
);
}
}
export default App;
複製程式碼
- 然後,修改 src 下 common 中 header 裡面 index.js 中的內容:
src/common/header/index.js
程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import homeImage from '../../resources/img/header-home.png';
class Header extends Component {
render() {
return (
<header>
<div className="header_left">
<a href="/">
<img alt="首頁" src={homeImage} className="headef_left-img" />
</a>
</div>
<div className="header_center">
<div className="header_center-left">
<div className="nav-item header_center-left-home">
<i className="icon icon-home"></i>
<span>首頁</span>
</div>
<div className="nav-item header_center-left-download">
<i className="icon icon-download"></i>
<span>下載App</span>
</div>
<div className="nav-item header_center-left-search">
<CSSTransition
in={this.props.inputBlur}
timeout={200}
classNames="slide"
>
<input
className={this.props.inputBlur ? 'input-nor-active' : 'input-active'}
placeholder="搜尋"
onFocus={this.props.searchFocusOrBlur}
onBlur={this.props.searchFocusOrBlur}
/>
</CSSTransition>
<i className={this.props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
</div>
</div>
<div className="header_center-right">
<div className="nav-item header_right-center-setting">
<span>Aa</span>
</div>
<div className="nav-item header_right-center-login">
<span>登入</span>
</div>
</div>
</div>
<div className="header_right nav-item">
<span className="header_right-register">註冊</span>
<span className="header_right-write nav-item">
<i className="icon icon-write"></i>
<span>寫文章</span>
</span>
</div>
</header>
)
}
}
const mapStateToProps = (state) => {
return {
inputBlur: state.inputBlur
}
}
const mapDispathToProps = (dispatch) => {
return {
searchFocusOrBlur() {
const action = {
type: 'search_focus_or_blur'
}
dispatch(action);
}
}
}
export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
- 再來,我們再修改下 reducer.js,獲取並處理 src/index.js 中
dispatch
過來的值:
src/store/reducer.js
程式碼詳情
const defaultState = {
inputBlur: true
};
export default (state = defaultState, action) => {
if(action.type === 'search_focus_or_blur') {
const newState = JSON.parse(JSON.stringify(state));
newState.inputBlur = !newState.inputBlur
return newState;
}
return state;
}
複製程式碼
- 此時,我們完成了修改的步驟。同時,這時候因為 src 下 common 中 header 裡面的 index.js 中只有
render
方法體,它構成了無狀態元件,所以我們將其轉換成無狀態元件:
src/common/header/index.js
程式碼詳情
import React from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import homeImage from '../../resources/img/header-home.png';
const Header = (props) => {
return (
<header>
<div className="header_left">
<a href="/">
<img alt="首頁" src={homeImage} className="headef_left-img" />
</a>
</div>
<div className="header_center">
<div className="header_center-left">
<div className="nav-item header_center-left-home">
<i className="icon icon-home"></i>
<span>首頁</span>
</div>
<div className="nav-item header_center-left-download">
<i className="icon icon-download"></i>
<span>下載App</span>
</div>
<div className="nav-item header_center-left-search">
<CSSTransition
in={props.inputBlur}
timeout={200}
classNames="slide"
>
<input
className={props.inputBlur ? 'input-nor-active' : 'input-active'}
placeholder="搜尋"
onFocus={props.searchFocusOrBlur}
onBlur={props.searchFocusOrBlur}
/>
</CSSTransition>
<i className={props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
</div>
</div>
<div className="header_center-right">
<div className="nav-item header_right-center-setting">
<span>Aa</span>
</div>
<div className="nav-item header_right-center-login">
<span>登入</span>
</div>
</div>
</div>
<div className="header_right nav-item">
<span className="header_right-register">註冊</span>
<span className="header_right-write nav-item">
<i className="icon icon-write"></i>
<span>寫文章</span>
</span>
</div>
</header>
)
}
const mapStateToProps = (state) => {
return {
inputBlur: state.inputBlur
}
}
const mapDispathToProps = (dispatch) => {
return {
searchFocusOrBlur() {
const action = {
type: 'search_focus_or_blur'
}
dispatch(action);
}
}
}
export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
- 最後,我們完成了 Redux、React-Redux 的引用及使用,以及對 header/index.js 的無狀態元件的升級。
由於我們只是將必要的資料儲存到 state 中,所以樣式和功能無變化,故不貼出效果圖。
八 使用 redux-devtools-extension 外掛
修改 src/store/index.js 如下:
src/store/index.js
程式碼詳情
import { createStore, compose } from 'redux';
import reducer from './reducer';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers())
export default store;
複製程式碼
這時候,我們就成功開啟之前安裝過的 redux-devtools-extension 外掛。
使用一下:
九 優化:抽取 reducer.js
在專案開發中,我們會發現 reducer.js 隨著專案的開發越來越龐大,最後到不可維護的地步。
該視訊的慕課講師也提到:當你的一個 js 檔案程式碼量超過 300 行,說明它的設計從一開始來說就是不合理的。
所以,我們要想著進一步優化它。
首先,我們在 header 目錄下,新建 store,並新建 reducer.js,將 src/store 的 reducer.js 中的內容剪下到 header/store/reducer.js 中:
src/common/header/store/reducer.js
程式碼詳情
// 1. 將 reducer.js 轉移到 header/store/reducer.js 中
const defaultState = {
inputBlur: true
};
export default (state = defaultState, action) => {
if(action.type === 'search_focus_or_blur') {
const newState = JSON.parse(JSON.stringify(state));
newState.inputBlur = !newState.inputBlur
return newState;
}
return state;
}
複製程式碼
然後,我們修改 src/store/reducer.js 的內容為:
src/store/reducer.js
程式碼詳情
// 2. 通過 combineReducers 整合多個 reducer.js 檔案
import { combineReducers } from 'redux';
import headerReducer from '../common/header/store/reducer';
const reducer = combineReducers({
header: headerReducer
})
export default reducer;
複製程式碼
最後,我們修改 src/common/header/index.js 內容:
src/common/header/index.js
程式碼詳情
// 程式碼省略 。。。
const mapStateToProps = (state) => {
return {
// 3. 因為引用的層級變了,所以需要修改 state.inputBlur 為 state.header.inputBlue
inputBlur: state.header.inputBlur
}
}
// 程式碼省略 。。。
複製程式碼
在這裡,我們需要知道的是:之前我們只有一層目錄,所以修改的是 state.inputBlur
。
但是,因為通過 combineReducers
將 reducer.js 進行了整合,所以需要修改為 state.header.inputBlur
至此,我們就完成了 reducer.js 的優化。
十 優化:抽取 action
- 首先,在 header 的 store 中新建 actionCreators.js 檔案:
src/common/header/store/actionCreators.js
程式碼詳情
// 1. 定義 actionCreators
export const searchFocusOrBlur = () => ({
type: 'search_focus_or_blur'
})
複製程式碼
- 然後,我們在 header 中的 index.js 檔案引入 actionCreators.js,並在
mapDispathToProps
方法體中將其dispatch
出去:
src/common/header/index.js
程式碼詳情
import React from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
// 2. 以 actionCreators 的形式將所有 action 引入進來
import * as actionCreators from './store/actionCreators';
import homeImage from '../../resources/img/header-home.png';
const Header = (props) => {
return (
<header>
<div className="header_left">
<a href="/">
<img alt="首頁" src={homeImage} className="headef_left-img" />
</a>
</div>
<div className="header_center">
<div className="header_center-left">
<div className="nav-item header_center-left-home">
<i className="icon icon-home"></i>
<span>首頁</span>
</div>
<div className="nav-item header_center-left-download">
<i className="icon icon-download"></i>
<span>下載App</span>
</div>
<div className="nav-item header_center-left-search">
<CSSTransition
in={props.inputBlur}
timeout={200}
classNames="slide"
>
<input
className={props.inputBlur ? 'input-nor-active' : 'input-active'}
placeholder="搜尋"
onFocus={props.searchFocusOrBlur}
onBlur={props.searchFocusOrBlur}
/>
</CSSTransition>
<i className={props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
</div>
</div>
<div className="header_center-right">
<div className="nav-item header_right-center-setting">
<span>Aa</span>
</div>
<div className="nav-item header_right-center-login">
<span>登入</span>
</div>
</div>
</div>
<div className="header_right nav-item">
<span className="header_right-register">註冊</span>
<span className="header_right-write nav-item">
<i className="icon icon-write"></i>
<span>寫文章</span>
</span>
</div>
</header>
)
}
const mapStateToProps = (state) => {
return {
inputBlur: state.header.inputBlur
}
}
const mapDispathToProps = (dispatch) => {
return {
searchFocusOrBlur() {
// 3. 使用 actionCreators
dispatch(actionCreators.searchFocusOrBlur());
}
}
}
export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
- 接著,因為我們在 actionCreators.js 中使用的
type
是字串,所以我們同樣在 store 中建立 actionTypes.js,將其變成常量:
src/common/header/store/actionTypes.js
程式碼詳情
export const SEARCH_FOCUS_OR_BLUR = 'search_focus_or_blur';
複製程式碼
- 再然後,我們在 actionCreators.js 中引入 actionTypes.js:
src/common/header/store/actionCreators.js
程式碼詳情
// 4. 引入常量
import { SEARCH_FOCUS_OR_BLUR } from './actionTypes';
// 1. 定義 actionCreators
// 5. 將 action 中的字串修改為常量
export const searchFocusOrBlur = () => ({
type: SEARCH_FOCUS_OR_BLUR
})
複製程式碼
- 再接著,我們修改下 header 目錄中 store 下的 reducer.js,因為我們的字串變成了常量,所以這裡也需要做相應變更:
src/common/header/store/reducer.js
程式碼詳情
// 6. 引入常量
import * as actionTypes from './actionTypes'
const defaultState = {
inputBlur: true
};
export default (state = defaultState, action) => {
// 7. 使用常量
if(action.type === actionTypes.SEARCH_FOCUS_OR_BLUR) {
const newState = JSON.parse(JSON.stringify(state));
newState.inputBlur = !newState.inputBlur
return newState;
}
return state;
}
複製程式碼
- 然後,我們現在 header/store 目錄下有:actionCreators.js、actionTypes.js、reducer.js 三個檔案,如果我們每次引入都要一個一個找,那是相當麻煩的,所以我們在 header/store 目錄下再新建一個 index.js,通過 index.js 來管理這三個檔案,這樣我們其他頁面需要引入它們的時候,我們只需要引入 store 下的 index.js 即可。
src/common/header/store/index.js
程式碼詳情
// 8. 統一管理 store 目錄中的檔案
import * as actionCreators from './actionCreators';
import * as actionTypes from './actionTypes';
import reducer from './reducer';
export { actionCreators, actionTypes, reducer };
複製程式碼
- 此時,值得注意的是,這時候我們需要處理下 header/index.js 檔案:
程式碼詳情
import React from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
// 2. 以 actionCreators 的形式將所有 action 引入進來
// import * as actionCreators from './store/actionCreators';
// 9. 引入 store/index 檔案即可
import { actionCreators } from './store';
import homeImage from '../../resources/img/header-home.png';
// 程式碼省略
複製程式碼
- 最後,再處理下 src/store/reducer.js,因為它引用了 common/header/store 中的 reducer.js:
程式碼詳情
import { combineReducers } from 'redux';
// 10. 修改下引用方式
import { reducer as headerReducer } from '../common/header/store';
const reducer = combineReducers({
header: headerReducer
})
export default reducer;
複製程式碼
至此,我們就完成了本次的優化抽取。
十一 優化;immutable.js
在我們工作的過程中,如果一不小心,就會修改了 reducer.js 中的資料(平時開發的時候,我們會通過 JSON.parse(JSON.stringify())
來進行深拷貝,獲取一份額外的來進行修改)。
所以,這時候,我們就需要使用 immutable.js,它是由 Facebook 團隊開發的,用來幫助我們生產 immutable
物件,從而限制 state
不可被改變。
- 安裝 immutable.js:
npm i immutable -S
。 - 案例 immutable.js:
const { Map } = require('immutable');
const map1 = Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 50);
map1.get('b') + " vs. " + map2.get('b'); // 2 vs. 50
複製程式碼
看起來很簡單,我們直接在簡書 Demo 中使用:
src/common/header/store/reducer.js
程式碼詳情
import * as actionTypes from './actionTypes'
// 1. 通過 immutable 引入 fromJS
import { fromJS } from 'immutable';
// 2. 對 defaultState 使用 fromJS
const defaultState = fromJS({
inputBlur: true
});
export default (state = defaultState, action) => {
if(action.type === actionTypes.SEARCH_FOCUS_OR_BLUR) {
// const newState = JSON.parse(JSON.stringify(state));
// newState.inputBlur = !newState.inputBlur
// return newState;
// 4. 通過 immutable 的方法來 set state 的值
// immutable 物件的 set 方法,會結合之前 immutable 物件的值和設定的值,返回一個全新的物件
return state.set('inputBlur', !state.get('inputBlur'));
}
return state;
}
複製程式碼
src/common/header/index.js
程式碼詳情
import React from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';
import homeImage from '../../resources/img/header-home.png';
const Header = (props) => {
return (
<header>
<div className="header_left">
<a href="/">
<img alt="首頁" src={homeImage} className="headef_left-img" />
</a>
</div>
<div className="header_center">
<div className="header_center-left">
<div className="nav-item header_center-left-home">
<i className="icon icon-home"></i>
<span>首頁</span>
</div>
<div className="nav-item header_center-left-download">
<i className="icon icon-download"></i>
<span>下載App</span>
</div>
<div className="nav-item header_center-left-search">
<CSSTransition
in={props.inputBlur}
timeout={200}
classNames="slide"
>
<input
className={props.inputBlur ? 'input-nor-active' : 'input-active'}
placeholder="搜尋"
onFocus={props.searchFocusOrBlur}
onBlur={props.searchFocusOrBlur}
/>
</CSSTransition>
<i className={props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
</div>
</div>
<div className="header_center-right">
<div className="nav-item header_right-center-setting">
<span>Aa</span>
</div>
<div className="nav-item header_right-center-login">
<span>登入</span>
</div>
</div>
</div>
<div className="header_right nav-item">
<span className="header_right-register">註冊</span>
<span className="header_right-write nav-item">
<i className="icon icon-write"></i>
<span>寫文章</span>
</span>
</div>
</header>
)
}
const mapStateToProps = (state) => {
return {
// 3. 通過 immutable 提供的 get() 方法來獲取 inputBlur 屬性
inputBlur: state.header.get('inputBlur')
}
}
const mapDispathToProps = (dispatch) => {
return {
searchFocusOrBlur() {
dispatch(actionCreators.searchFocusOrBlur());
}
}
}
export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
我們大致做了四個步驟,從而完成了 immutable.js 的引用及使用:
- 通過
import
immutable
引入fromJS
- 對
defaultState
使用fromJS
- 這時候我們就不能直接修改
matStateToProps
中的值了,而是 通過immutable
提供的get()
方法來獲取inputBlur
屬性 - 通過
immutable
的方法來set
state
的值。immutable
物件的set
方法,會結合之前immutable
物件的值和設定的值,返回一個全新的物件
這樣,我們就成功保護了 state
的值。
十二 優化:redux-immutable
當然,在上面,我們保護了 header 中的 state
,我們在程式碼中:
inputBlur: state.header.get('inputBlur')
複製程式碼
這個 header
也是 state
的值,所以我們也需要對它進行保護,所以我們就需要 redux-immutable
- 安裝 redux-immutable:
npm i redux-immutable -S
- 使用 redux-immutable:
src/store/reducer.js
程式碼詳情
// import { combineReducers } from 'redux';
// 1. 通過 redux-immutable 引入 combineReducers 而非原先的 redux
import { combineReducers } from 'redux-immutable';
import { reducer as headerReducer } from '../common/header/store';
const reducer = combineReducers({
header: headerReducer
})
export default reducer;
複製程式碼
src/common/header/index.js
程式碼詳情
// 程式碼省略。。。
const mapStateToProps = (state) => {
return {
// 2. 通過同樣的 get 方法來獲取 header
inputBlur: state.get('header').get('inputBlur')
}
}
// 程式碼省略。。。
複製程式碼
這樣,通過簡單的三個步驟,我們就保護了主 state
的值:
- 安裝 redux-immutable:
npm i redux-immutable -S
- 通過 redux-immutable 引入
combineReducers
而非原先的 redux - 通過同樣的
get
方法來獲取header
十三 功能實現:熱門搜尋
本章節完成三個功能:
- 寫熱門搜尋顯示隱藏
- 安裝 redux-thunk
- 使用 React 中 Node 提供的作假資料的功能,在 public/api 下寫個檔案 headerList.json,並做假資料,使用方式為
axios.get('/api/headerList.json').then()
首先,我們完成熱門搜尋的顯示隱藏:
src/common.css
程式碼詳情
.icon {
display: inline-block;
width: 20px;
height: 21px;
margin-right: 5px;
}
.icon-home {
background: url('./resources/img/icon-home.png') no-repeat center;
background-size: 100%;
}
.icon-write {
background: url('./resources/img/icon-write.png') no-repeat center;
background-size: 100%;
}
.icon-download {
background: url('./resources/img/icon-download.png') no-repeat center;
background-size: 100%;
}
.icon-search {
background: url('./resources/img/icon-search.png') no-repeat center;
background-size: 100%;
}
.display-hide {
display: none;
}
.display-show {
display: block;
}
複製程式碼
src/common/header/index.css
程式碼詳情
header {
width: 100%;
height: 58px;
display: flex;
align-items: center;
border-bottom: 1px solid #ccc;
font-size: 17px;
}
/* 頭部左邊 */
.header_left-img {
width: 100px;
height: 56px;
}
/* 頭部中間 */
.header_center {
width: 1000px;
margin: 0 auto;
display: flex;
justify-content: space-between;
}
.nav-item {
margin-right: 30px;
display: flex;
align-items: center;
}
/* 頭部中間左部 */
.header_center-left {
display: flex;
}
/* 頭部中間左部 - 首頁 */
.header_center-left-home {
color: #ea6f5a;
}
/* 頭部中間左部 - 搜尋框 */
.header_center-left-search {
position: relative;
}
.slide-enter {
transition: all .2s ease-out;
}
.slide-enter-active {
width: 280px;
}
.slide-exit {
transition: all .2s ease-out;
}
.silde-exit-active {
width: 240px;
}
.header_center-left-search input {
width: 240px;
padding: 0 45px 0 20px;
height: 38px;
font-size: 14px;
border: 1px solid #eee;
border-radius: 40px;
background: #eee;
}
.header_center-left-search .input-active {
width: 280px;
}
.header_center-left-search .icon-search {
position: absolute;
top: 8px;
right: 10px;
}
.header_center-left-search .icon-active {
padding: 3px;
top: 4px;
border-radius: 15px;
border: 1px solid #ea6f5a;
}
/* 頭部中間左部 - 熱搜 */
.header_center-left-search .icon-active:hover {
cursor: pointer;
}
.header_center-left-hot-search:before {
content: "";
left: 27px;
width: 10px;
height: 10px;
transform: rotate(45deg);
top: -5px;
z-index: -1;
position: absolute;
background-color: #fff;
box-shadow: 0 0 8px rgba(0,0,0,.2);
}
.header_center-left-hot-search {
position: absolute;
width: 250px;
left: 0;
top: 125%;
padding: 15px;
font-size: 14px;
background: #fff;
border-radius: 4px;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
}
.header_center-left-hot-search-title {
display: flex;
justify-content: space-between;
color: #969696;
}
.header_center-left-hot-search-change {
display: flex;
justify-content: space-between;
align-items: center;
}
.icon-change {
display: inline-block;
width: 20px;
height: 14px;
background: url('../../resources/img/icon-change.png') no-repeat center;
background-size: 100%;
}
.icon-change:hover {
cursor: pointer;
}
.header_center-left-hot-search-content span {
display: inline-block;
margin-top: 10px;
margin-right: 10px;
padding: 2px 6px;
font-size: 12px;
color: #787878;
border: 1px solid #ddd;
border-radius: 3px;
}
.header_center-left-hot-search-content span:hover {
cursor: pointer;
}
/* 頭部中間右部 */
.header_center-right {
display: flex;
color: #969696;
}
/* 頭部右邊 */
.header_right-register, .header_right-write {
width: 80px;
text-align: center;
height: 38px;
line-height: 38px;
border: 1px solid rgba(236,97,73,.7);
border-radius: 20px;
font-size: 15px;
color: #ea6f5a;
background-color: transparent;
}
.header_right-write {
margin-left: 10px;
padding-left: 10px;
margin-right: 0px;
color: #fff;
background-color: #ea6f5a;
}
複製程式碼
src/common/header/index.js
程式碼詳情
import React from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';
import homeImage from '../../resources/img/header-home.png';
const Header = (props) => {
return (
<header>
<div className="header_left">
<a href="/">
<img alt="首頁" src={homeImage} className="header_left-img" />
</a>
</div>
<div className="header_center">
<div className="header_center-left">
<div className="nav-item header_center-left-home">
<i className="icon icon-home"></i>
<span>首頁</span>
</div>
<div className="nav-item header_center-left-download">
<i className="icon icon-download"></i>
<span>下載App</span>
</div>
<div className="nav-item header_center-left-search">
<CSSTransition
in={props.inputBlur}
timeout={200}
classNames="slide"
>
<input
className={props.inputBlur ? 'input-nor-active' : 'input-active'}
placeholder="搜尋"
onFocus={props.searchFocusOrBlur}
onBlur={props.searchFocusOrBlur}
/>
</CSSTransition>
<i className={props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
{/* 新增熱搜模組 */}
<div className={props.inputBlur ? 'display-hide header_center-left-hot-search' : 'display-show header_center-left-hot-search'}>
<div className="header_center-left-hot-search-title">
<span>熱門搜尋</span>
<span>
<i className="icon-change"></i>
<span>換一批</span>
</span>
</div>
<div className="header_center-left-hot-search-content">
<span>考研</span>
<span>慢死人</span>
<span>悅心</span>
<span>一致</span>
<span>是的</span>
<span>jsliang</span>
</div>
</div>
</div>
</div>
<div className="header_center-right">
<div className="nav-item header_right-center-setting">
<span>Aa</span>
</div>
<div className="nav-item header_right-center-login">
<span>登入</span>
</div>
</div>
</div>
<div className="header_right nav-item">
<span className="header_right-register">註冊</span>
<span className="header_right-write nav-item">
<i className="icon icon-write"></i>
<span>寫文章</span>
</span>
</div>
</header>
)
}
const mapStateToProps = (state) => {
return {
inputBlur: state.get('header').get('inputBlur')
}
}
const mapDispathToProps = (dispatch) => {
return {
searchFocusOrBlur() {
dispatch(actionCreators.searchFocusOrBlur());
}
}
}
export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
由此,我們完成了熱門搜尋的顯示隱藏:
PS:由於頁面逐漸增大,所以我們 header 中使用無狀態元件已經滿足不了我們要求了,我們需要將無狀態元件改成正常的元件:
src/common/header/index.js
程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';
import homeImage from '../../resources/img/header-home.png';
class Header extends Component {
render() {
return (
<header>
<div className="header_left">
<a href="/">
<img alt="首頁" src={homeImage} className="header_left-img" />
</a>
</div>
<div className="header_center">
<div className="header_center-left">
<div className="nav-item header_center-left-home">
<i className="icon icon-home"></i>
<span>首頁</span>
</div>
<div className="nav-item header_center-left-download">
<i className="icon icon-download"></i>
<span>下載App</span>
</div>
<div className="nav-item header_center-left-search">
<CSSTransition
in={this.props.inputBlur}
timeout={200}
classNames="slide"
>
<input
className={this.props.inputBlur ? 'input-nor-active' : 'input-active'}
placeholder="搜尋"
onFocus={this.props.searchFocusOrBlur}
onBlur={this.props.searchFocusOrBlur}
/>
</CSSTransition>
<i className={this.props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
<div className={this.props.inputBlur ? 'display-hide header_center-left-hot-search' : 'display-show header_center-left-hot-search'}>
<div className="header_center-left-hot-search-title">
<span>熱門搜尋</span>
<span>
<i className="icon-change"></i>
<span>換一批</span>
</span>
</div>
<div className="header_center-left-hot-search-content">
<span>考研</span>
<span>慢死人</span>
<span>悅心</span>
<span>一致</span>
<span>是的</span>
<span>jsliang</span>
</div>
</div>
</div>
</div>
<div className="header_center-right">
<div className="nav-item header_right-center-setting">
<span>Aa</span>
</div>
<div className="nav-item header_right-center-login">
<span>登入</span>
</div>
</div>
</div>
<div className="header_right nav-item">
<span className="header_right-register">註冊</span>
<span className="header_right-write nav-item">
<i className="icon icon-write"></i>
<span>寫文章</span>
</span>
</div>
</header>
)
}
}
const mapStateToProps = (state) => {
return {
inputBlur: state.get('header').get('inputBlur')
}
}
const mapDispathToProps = (dispatch) => {
return {
searchFocusOrBlur() {
dispatch(actionCreators.searchFocusOrBlur());
}
}
}
export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
然後,由於我們的資料是從介面模擬過來的,而在上一篇文章說過,如果要對介面程式碼進行管理,最好使用 Redux-Thunk 和 Redux-Saga,這裡我們使用 Redux-Thunk:
- 安裝 redux-thunk:
cnpm i redux-thunk -S
- 安裝 axios:
cnpm i axios -S
在這裡,我們要知道 create-react-app 的配置是包含 Node.js 的,所以我們可以依靠 Node.js 進行開發時候的 Mock 資料。
下面開始開發:
src/store/index.js
程式碼詳情
// 2. 引入 redux 的 applyMiddleware,進行多中介軟體的使用
import { createStore, compose, applyMiddleware } from 'redux';
// 1. 引入 redux-thunk
import thunk from 'redux-thunk';
import reducer from './reducer';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 3. 通過 applyMiddleware 同時使用 redux-thunk 和 redux-dev-tools
const store = createStore(reducer, composeEnhancers(
applyMiddleware(thunk)
));
export default store;
複製程式碼
- 引入 redux-thunk
- 引入 redux 的
applyMiddleware
,進行多中介軟體的使用 - 通過
applyMiddleware
同時使用 redux-thunk 和 redux-dev-tools
這樣,我們就可以正常使用 redux-thunk 了。
- src/common/header/index.js
程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';
import homeImage from '../../resources/img/header-home.png';
class Header extends Component {
render() {
return (
<header>
<div className="header_left">
<a href="/">
<img alt="首頁" src={homeImage} className="header_left-img" />
</a>
</div>
<div className="header_center">
<div className="header_center-left">
<div className="nav-item header_center-left-home">
<i className="icon icon-home"></i>
<span>首頁</span>
</div>
<div className="nav-item header_center-left-download">
<i className="icon icon-download"></i>
<span>下載App</span>
</div>
<div className="nav-item header_center-left-search">
<CSSTransition
in={this.props.inputBlur}
timeout={200}
classNames="slide"
>
<input
className={this.props.inputBlur ? 'input-nor-active' : 'input-active'}
placeholder="搜尋"
onFocus={this.props.searchFocusOrBlur}
onBlur={this.props.searchFocusOrBlur}
/>
</CSSTransition>
<i className={this.props.inputBlur ? 'icon icon-search' : 'icon icon-search icon-active'}></i>
<div className={this.props.inputBlur ? 'display-hide header_center-left-hot-search' : 'display-show header_center-left-hot-search'}>
<div className="header_center-left-hot-search-title">
<span>熱門搜尋</span>
<span>
<i className="icon-change"></i>
<span>換一批</span>
</span>
</div>
<div className="header_center-left-hot-search-content">
{/* 15. 遍歷輸出該資料 */}
{
this.props.list.map((item) => {
return <span key={item}>{item}</span>
})
}
</div>
</div>
</div>
</div>
<div className="header_center-right">
<div className="nav-item header_right-center-setting">
<span>Aa</span>
</div>
<div className="nav-item header_right-center-login">
<span>登入</span>
</div>
</div>
</div>
<div className="header_right nav-item">
<span className="header_right-register">註冊</span>
<span className="header_right-write nav-item">
<i className="icon icon-write"></i>
<span>寫文章</span>
</span>
</div>
</header>
)
}
}
const mapStateToProps = (state) => {
return {
inputBlur: state.get('header').get('inputBlur'),
// 14. 獲取 reducer.js 中的 list 資料
list: state.get('header').get('list')
}
}
const mapDispathToProps = (dispatch) => {
return {
searchFocusOrBlur() {
// 4. 派發 action 到 actionCreators.js 中的 getList() 方法
dispatch(actionCreators.getList());
dispatch(actionCreators.searchFocusOrBlur());
}
}
}
export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
- src/common/header/store/actionCreators.js
程式碼詳情
import * as actionTypes from './actionTypes'
// 7. 引入 axios
import axios from 'axios';
// 11. 引入 immutable 的型別轉換
import { fromJS } from 'immutable';
export const searchFocusOrBlur = () => ({
type: actionTypes.SEARCH_FOCUS_OR_BLUR
})
// 10. 定義 action,接受引數 data,同時因為我們使用了 Immutable,所以需要將獲取的資料轉換為 immutable 型別
const changeList = (data) => ({
type: actionTypes.GET_LIST,
data: fromJS(data)
})
// 5. 編寫 getList 的 action,由於需要 actionTypes 中定義,所以前往 actionTypes.js 中新增
export const getList = () => {
return (dispatch) => {
// 8. 呼叫 create-react-app 中提供的 Node 伺服器,從而 mock 資料
axios.get('/api/headerList.json').then( (res) => {
if(res.data.code === 0) {
const data = res.data.list;
// 由於資料太多,我們限制資料量為 15 先
data.length = 15;
// 12. 派發 changeList 型別
dispatch(changeList(data));
}
}).catch( (error) => {
console.log(error);
});
}
}
複製程式碼
- src/common/header/store/actionTypes.js
程式碼詳情
export const SEARCH_FOCUS_OR_BLUR = 'header/search_focus_or_blur';
// 6. 新增 actionType
export const GET_LIST = 'header/get_list';
複製程式碼
- src/common/header/store/reducer.js
程式碼詳情
import * as actionTypes from './actionTypes'
import { fromJS } from 'immutable';
const defaultState = fromJS({
inputBlur: true,
// 9. 給 header 下的 reducer.js 提供儲存資料的地方
list: []
});
export default (state = defaultState, action) => {
if(action.type === actionTypes.SEARCH_FOCUS_OR_BLUR) {
return state.set('inputBlur', !state.get('inputBlur'));
}
// 13. 判斷 actionTypes 是否為 GET_LIST,如果是則執行該 action
if(action.type === actionTypes.GET_LIST) {
return state.set('list', action.data);
}
return state;
}
複製程式碼
- public/api/headerList.json
程式碼詳情
{
"code": 0,
"list": ["區塊鏈","小程式","vue","畢業","PHP","故事","flutter","理財","美食","投稿","手帳","書法","PPT","穿搭","打碗碗花","簡書","姥姥的澎湖灣","設計","創業","交友","籽鹽","教育","思維導圖","瘋哥哥","梅西","時間管理","golang","連載","自律","職場","考研","慢世人","悅欣","一紙vr","spring","eos","足球","程式設計師","林露含","彩鉛","金融","木風雜談","日更","成長","外婆是方言","docker"]
}
複製程式碼
通過下面步驟:
- 派發
action
到 actionCreators.js 中的getList()
方法 - 編寫
getList
的action
,由於需要actionTypes
中定義,所以前往 actionTypes.js 中新增 - 新增 actionType
- 引入 axios
- 呼叫 create-react-app 中提供的 Node 伺服器,從而 mock 資料
- 給 header 下的 reducer.js 提供儲存資料的地方
- 定義
action
,接受引數data
,同時因為我們使用了 Immutable,所以需要將獲取的資料轉換為immutable
型別 - 引入 Immutable 的型別轉換
- 派發
changeList
型別 - 判斷
actionTypes
是否為GET_LIST
,如果是則執行該action
- 獲取 reducer.js 中的
list
資料 - 遍歷輸出該資料
這樣,我們就成功地獲取了 mock 提供的資料:
十四 程式碼優化
- reducer.js 中使用
switch...case...
替換掉if...
語句。
src/common/header/store/reducer.js
程式碼詳情
import * as actionTypes from './actionTypes'
import { fromJS } from 'immutable';
const defaultState = fromJS({
inputBlur: true,
list: []
});
export default (state = defaultState, action) => {
switch(action.type) {
case actionTypes.SEARCH_FOCUS_OR_BLUR:
return state.set('inputBlur', !state.get('inputBlur'));
case actionTypes.GET_LIST:
return state.set('list', action.data);
default:
return state;
}
}
複製程式碼
十五 解決歷史遺留問題
在這裡,我們解決下歷史遺留問題:在我們失焦於輸入框的時候,我們的【熱門搜尋】模組就會消失,從而看不到我們點選【換一換】按鈕的效果,所以我們需要修改下程式碼,在我們滑鼠在【熱門模組】中時,這個模組不會消失,當我們滑鼠失焦且滑鼠不在熱門模組中時,熱門模組才消失。
- src/common/header/store/reducer.js
程式碼詳情
import * as actionTypes from './actionTypes'
import { fromJS } from 'immutable';
const defaultState = fromJS({
inputFocus: false,
// 1. 設定滑鼠移動到熱門模組為 false
mouseInHot: false,
list: [],
});
export default (state = defaultState, action) => {
switch(action.type) {
case actionTypes.SEARCH_FOCUS:
return state.set('inputFocus', true);
case actionTypes.SEARCH_BLUR:
return state.set('inputFocus', false);
case actionTypes.GET_LIST:
return state.set('list', action.data);
// 6. 在 reducer.js 中判斷這兩個 action 執行設定 mouseInHot
case actionTypes.ON_MOUSE_ENTER_HOT:
return state.set('mouseInHot', true);
case actionTypes.ON_MOUSE_LEAVE_HOT:
return state.set('mouseInHot', false);
default:
return state;
}
}
複製程式碼
- src/common/header/index.js
程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';
import homeImage from '../../resources/img/header-home.png';
class Header extends Component {
render() {
return (
<header>
<div className="header_left">
<a href="/">
<img alt="首頁" src={homeImage} className="header_left-img" />
</a>
</div>
<div className="header_center">
<div className="header_center-left">
<div className="nav-item header_center-left-home">
<i className="icon icon-home"></i>
<span>首頁</span>
</div>
<div className="nav-item header_center-left-download">
<i className="icon icon-download"></i>
<span>下載App</span>
</div>
<div className="nav-item header_center-left-search">
<CSSTransition
in={this.props.inputFocus}
timeout={200}
classNames="slide"
>
<input
className={this.props.inputFocus ? 'input-active' : 'input-nor-active'}
placeholder="搜尋"
onFocus={this.props.searchFocus}
onBlur={this.props.searchBlur}
/>
</CSSTransition>
<i className={this.props.inputFocus ? 'icon icon-search icon-active' : 'icon icon-search'}></i>
{/* 8. 在判斷中加多一個 this.props.mouseInHot,這樣只要有一個為 true,它就不會消失 */}
<div
className={this.props.inputFocus || this.props.mouseInHot ? 'display-show header_center-left-hot-search' : 'display-hide header_center-left-hot-search'}
// 2. 設定移入為 onMouseEnterHot,移出為 onMouseLeaveHot
onMouseEnter={this.props.onMouseEnterHot}
onMouseLeave={this.props.onMouseLeaveHot}
>
<div className="header_center-left-hot-search-title">
<span>熱門搜尋</span>
<span>
<i className="icon-change"></i>
<span>換一批</span>
</span>
</div>
<div className="header_center-left-hot-search-content">
{
this.props.list.map((item) => {
return <span key={item}>{item}</span>
})
}
</div>
</div>
</div>
</div>
<div className="header_center-right">
<div className="nav-item header_right-center-setting">
<span>Aa</span>
</div>
<div className="nav-item header_right-center-login">
<span>登入</span>
</div>
</div>
</div>
<div className="header_right nav-item">
<span className="header_right-register">註冊</span>
<span className="header_right-write nav-item">
<i className="icon icon-write"></i>
<span>寫文章</span>
</span>
</div>
</header>
)
}
}
const mapStateToProps = (state) => {
return {
inputFocus: state.get('header').get('inputFocus'),
list: state.get('header').get('list'),
// 7. 在 index.js 中獲取
mouseInHot: state.get('header').get('mouseInHot'),
}
}
const mapDispathToProps = (dispatch) => {
return {
searchFocus() {
dispatch(actionCreators.getList());
dispatch(actionCreators.searchFocus());
},
searchBlur() {
dispatch(actionCreators.searchBlur());
},
// 3. 定義 onMouseEnterHot 和 onMouseLeaveHot 方法
onMouseEnterHot() {
dispatch(actionCreators.onMouseEnterHot());
},
onMouseLeaveHot() {
dispatch(actionCreators.onMouseLeaveHot());
},
}
}
export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
- src/common/header/store/actionCreators.js
程式碼詳情
import * as actionTypes from './actionTypes'
import axios from 'axios';
import { fromJS } from 'immutable';
export const searchFocus = () => ({
type: actionTypes.SEARCH_FOCUS
})
export const searchBlur = () => ({
type: actionTypes.SEARCH_BLUR
})
// 4. 在 actionCreators.js 中定義這兩個方法:onMouseEnterHot 和 onMouseLeaveHot
export const onMouseEnterHot = () => ({
type: actionTypes.ON_MOUSE_ENTER_HOT,
})
export const onMouseLeaveHot = () => ({
type: actionTypes.ON_MOUSE_LEAVE_HOT,
})
export const getList = () => {
return (dispatch) => {
axios.get('/api/headerList.json').then( (res) => {
if(res.data.code === 0) {
const data = res.data.list;
// 由於資料太多,我們限制資料量為 15 先
data.length = 15;
dispatch(changeList(data));
}
}).catch( (error) => {
console.log(error);
});
}
}
const changeList = (data) => ({
type: actionTypes.GET_LIST,
data: fromJS(data)
})
複製程式碼
- src/common/header/store/actionTypes.js
程式碼詳情
export const SEARCH_FOCUS = 'header/search_focus';
export const SEARCH_BLUR = 'header/search_blur';
export const GET_LIST = 'header/get_list';
// 5. 在 actionTypes.js 中新增 action 型別
export const ON_MOUSE_ENTER_HOT = 'header/on_mouse_enter_hot';
export const ON_MOUSE_LEAVE_HOT = 'header/on_mouse_leave_hot';
複製程式碼
我們先看實現:
然後我們看看實現邏輯:
- 在 reducer.js 中設定滑鼠移動到熱門模組為
false
- 在 index.js 中設定移入為
onMouseEnterHot
,移出為onMouseLeaveHot
- 在 index.js 中
mapDispathToProps
定義onMouseEnterHot
和onMouseLeaveHot
方法 - 在 actionCreators.js 中定義這兩個方法:
onMouseEnterHot
和onMouseLeaveHot
- 在 actionTypes.js 中新增
action
型別 - 在 reducer.js 中判斷這兩個
action
執行設定mouseInHot
- 在 index.js 中
mapStateToProps
獲取mouseInHot
- 在 index.js 中的判斷中加多一個
this.props.mouseInHot
,這樣只要有一個為true
,它就不會消失
注意:由於之前設定的
this.props.inputFoucsOrBlur
會造成聚焦和失焦都會呼叫一次介面,而且邏輯比較複雜,容易出錯,所以這裡我們進行了修改,將其分為聚焦和失焦兩部分。
十六 功能實現:換一換
下面我們開始做換一換功能:
- src/common/header/store/reducer.js
程式碼詳情
import * as actionTypes from './actionTypes'
import { fromJS } from 'immutable';
const defaultState = fromJS({
inputFocus: false,
mouseInHot: false,
list: [],
// 1. 在 reducer.js 中設定頁數和總頁數
page: 1,
totalPage: 1,
});
export default (state = defaultState, action) => {
switch(action.type) {
case actionTypes.SEARCH_FOCUS:
return state.set('inputFocus', true);
case actionTypes.SEARCH_BLUR:
return state.set('inputFocus', false);
case actionTypes.GET_LIST:
// 4. 我們通過 merge 方法同時設定多個 state 值
return state.merge({
list: action.data,
totalPage: action.totalPage
});
case actionTypes.ON_MOUSE_ENTER_HOT:
return state.set('mouseInHot', true);
case actionTypes.ON_MOUSE_LEAVE_HOT:
return state.set('mouseInHot', false);
// 11. 判斷 action 型別,並進行設定
case actionTypes.CHANGE_PAGE:
return state.set('page', action.page + 1);
default:
return state;
}
}
複製程式碼
- src/common/header/store/actionCreators.js
程式碼詳情
import * as actionTypes from './actionTypes'
import axios from 'axios';
import { fromJS } from 'immutable';
export const searchFocus = () => ({
type: actionTypes.SEARCH_FOCUS
})
export const searchBlur = () => ({
type: actionTypes.SEARCH_BLUR
})
export const onMouseEnterHot = () => ({
type: actionTypes.ON_MOUSE_ENTER_HOT,
})
export const onMouseLeaveHot = () => ({
type: actionTypes.ON_MOUSE_LEAVE_HOT,
})
export const getList = () => {
return (dispatch) => {
axios.get('/api/headerList.json').then( (res) => {
if(res.data.code === 0) {
const data = res.data.list;
// 2. 由於資料太多,我們之前限制資料量為 15,這裡我們去掉該行程式碼
// data.length = 15;
dispatch(changeList(data));
}
}).catch( (error) => {
console.log(error);
});
}
}
const changeList = (data) => ({
type: actionTypes.GET_LIST,
data: fromJS(data),
// 3. 我們在這裡計算總頁數
totalPage: Math.ceil(data.length / 10)
})
// 9. 定義 changePage 方法
export const changePage = (page) => ({
type: actionTypes.CHANGE_PAGE,
page: page,
})
複製程式碼
- src/common/header/index.js
程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';
import homeImage from '../../resources/img/header-home.png';
class Header extends Component {
render() {
return (
<header>
<div className="header_left">
<a href="/">
<img alt="首頁" src={homeImage} className="header_left-img" />
</a>
</div>
<div className="header_center">
<div className="header_center-left">
<div className="nav-item header_center-left-home">
<i className="icon icon-home"></i>
<span>首頁</span>
</div>
<div className="nav-item header_center-left-download">
<i className="icon icon-download"></i>
<span>下載App</span>
</div>
<div className="nav-item header_center-left-search">
<CSSTransition
in={this.props.inputFocus}
timeout={200}
classNames="slide"
>
<input
className={this.props.inputFocus ? 'input-active' : 'input-nor-active'}
placeholder="搜尋"
onFocus={this.props.searchFocus}
onBlur={this.props.searchBlur}
/>
</CSSTransition>
<i className={this.props.inputFocus ? 'icon icon-search icon-active' : 'icon icon-search'}></i>
<div
className={this.props.inputFocus || this.props.mouseInHot ? 'display-show header_center-left-hot-search' : 'display-hide header_center-left-hot-search'}
onMouseEnter={this.props.onMouseEnterHot}
onMouseLeave={this.props.onMouseLeaveHot}
>
<div className="header_center-left-hot-search-title">
<span>熱門搜尋</span>
{/* 7. 進行換頁功能實現,傳遞引數 page 和 totalPage */}
<span onClick={() => this.props.changePage(this.props.page, this.props.totalPage)}>
<i className="icon-change"></i>
<span className="span-change">換一批</span>
</span>
</div>
<div className="header_center-left-hot-search-content">
{
// 6. 在 index.js 中進行計算:
// 一開始顯示 0-9 共 10 條,換頁的時候顯示 10-19 ……以此類推
this.props.list.map((item, index) => {
if(index >= (this.props.page - 1) * 10 && index < this.props.page * 10) {
return <span key={item}>{item}</span>
} else {
return '';
}
})
}
</div>
</div>
</div>
</div>
<div className="header_center-right">
<div className="nav-item header_right-center-setting">
<span>Aa</span>
</div>
<div className="nav-item header_right-center-login">
<span>登入</span>
</div>
</div>
</div>
<div className="header_right nav-item">
<span className="header_right-register">註冊</span>
<span className="header_right-write nav-item">
<i className="icon icon-write"></i>
<span>寫文章</span>
</span>
</div>
</header>
)
}
}
const mapStateToProps = (state) => {
return {
inputFocus: state.get('header').get('inputFocus'),
list: state.get('header').get('list'),
mouseInHot: state.get('header').get('mouseInHot'),
// 5. 在 index.js 中 mapStateToProps 獲取資料
page: state.get('header').get('page'),
totalPage: state.get('header').get('totalPage'),
}
}
const mapDispathToProps = (dispatch) => {
return {
searchFocus() {
dispatch(actionCreators.getList());
dispatch(actionCreators.searchFocus());
},
searchBlur() {
dispatch(actionCreators.searchBlur());
},
onMouseEnterHot() {
dispatch(actionCreators.onMouseEnterHot());
},
onMouseLeaveHot() {
dispatch(actionCreators.onMouseLeaveHot());
},
// 8. 呼叫 changePage 方法
changePage(page, totalPage) {
if(page === totalPage) {
page = 1;
dispatch(actionCreators.changePage(page));
} else {
dispatch(actionCreators.changePage(page));
}
}
}
}
export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
- src/common/header/store/actionTypes.js
程式碼詳情
export const SEARCH_FOCUS = 'header/search_focus';
export const SEARCH_BLUR = 'header/search_blur';
export const GET_LIST = 'header/get_list';
export const ON_MOUSE_ENTER_HOT = 'header/on_mouse_enter_hot';
export const ON_MOUSE_LEAVE_HOT = 'header/on_mouse_leave_hot';
// 10. 定義 action
export const CHANGE_PAGE = 'header/change_page';
複製程式碼
此時我們程式碼思路是:
- 在 reducer.js 中設定頁數
page
和總頁數totalPage
- 在 actionCreators.js 中,之前由於資料太多,我們之前限制資料量為 15,這裡我們去掉該行程式碼
- 在 actionCreators.js 這裡計算總頁數
- 在 reducer.js 中通過
merge
方法同時設定多個state
值 - 在 index.js 中
mapStateToProps
獲取資料 - 在 index.js 中進行計算:一開始顯示 0-9 共 10 條,換頁的時候顯示 10-19 ……以此類推
- 在 index.js 中進行換頁功能實現,傳遞引數
page
和totalPage
- 在 index.js 呼叫
changePage
方法,進行是否重置為第一頁判斷,並dispatch
方法 - 在 actionCreators.js 中定義
changePage
方法 - 在 actionTypes.js 中定義
action
- 在 reducer.js 中判斷
action
型別,並進行設定
如此,我們就實現了換一換功能:
十七 功能優化
17.1 換一換圖示旋轉
src/common/header/index.css
程式碼詳情
header {
width: 100%;
height: 58px;
display: flex;
align-items: center;
border-bottom: 1px solid #ccc;
font-size: 17px;
}
/* 頭部左邊 */
.header_left-img {
width: 100px;
height: 56px;
}
/* 頭部中間 */
.header_center {
width: 1000px;
margin: 0 auto;
display: flex;
justify-content: space-between;
}
.nav-item {
margin-right: 30px;
display: flex;
align-items: center;
}
/* 頭部中間左部 */
.header_center-left {
display: flex;
}
/* 頭部中間左部 - 首頁 */
.header_center-left-home {
color: #ea6f5a;
}
/* 頭部中間左部 - 搜尋框 */
.header_center-left-search {
position: relative;
}
.slide-enter {
transition: all .2s ease-out;
}
.slide-enter-active {
width: 280px;
}
.slide-exit {
transition: all .2s ease-out;
}
.silde-exit-active {
width: 240px;
}
.header_center-left-search input {
width: 240px;
padding: 0 45px 0 20px;
height: 38px;
font-size: 14px;
border: 1px solid #eee;
border-radius: 40px;
background: #eee;
}
.header_center-left-search .input-active {
width: 280px;
}
.header_center-left-search .icon-search {
position: absolute;
top: 8px;
right: 10px;
}
.header_center-left-search .icon-active {
padding: 3px;
top: 4px;
border-radius: 15px;
border: 1px solid #ea6f5a;
}
/* 頭部中間左部 - 熱搜 */
.header_center-left-search .icon-active:hover {
cursor: pointer;
}
.header_center-left-hot-search:before {
content: "";
left: 27px;
width: 10px;
height: 10px;
transform: rotate(45deg);
top: -5px;
z-index: -1;
position: absolute;
background-color: #fff;
box-shadow: 0 0 8px rgba(0,0,0,.2);
}
.header_center-left-hot-search {
position: absolute;
width: 250px;
left: 0;
top: 125%;
padding: 15px;
font-size: 14px;
background: #fff;
border-radius: 4px;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
}
.header_center-left-hot-search-title {
display: flex;
justify-content: space-between;
color: #969696;
}
.header_center-left-hot-search-change {
display: flex;
justify-content: space-between;
align-items: center;
}
.icon-change {
display: inline-block;
width: 20px;
height: 14px;
background: url('../../resources/img/icon-change.png') no-repeat center;
background-size: 100%;
/* 1. 在 index.css 中新增動畫 */
transition: all .2s ease-in;
transform-origin: center center;
}
.icon-change:hover {
cursor: pointer;
}
.span-change:hover {
cursor: pointer;
}
.header_center-left-hot-search-content span {
display: inline-block;
margin-top: 10px;
margin-right: 10px;
padding: 2px 6px;
font-size: 12px;
color: #787878;
border: 1px solid #ddd;
border-radius: 3px;
}
.header_center-left-hot-search-content span:hover {
cursor: pointer;
}
/* 頭部中間右部 */
.header_center-right {
display: flex;
color: #969696;
}
/* 頭部右邊 */
.header_right-register, .header_right-write {
width: 80px;
text-align: center;
height: 38px;
line-height: 38px;
border: 1px solid rgba(236,97,73,.7);
border-radius: 20px;
font-size: 15px;
color: #ea6f5a;
background-color: transparent;
}
.header_right-write {
margin-left: 10px;
padding-left: 10px;
margin-right: 0px;
color: #fff;
background-color: #ea6f5a;
}
複製程式碼
src/common/header/index.js
程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';
import homeImage from '../../resources/img/header-home.png';
class Header extends Component {
render() {
return (
<header>
<div className="header_left">
<a href="/">
<img alt="首頁" src={homeImage} className="header_left-img" />
</a>
</div>
<div className="header_center">
<div className="header_center-left">
<div className="nav-item header_center-left-home">
<i className="icon icon-home"></i>
<span>首頁</span>
</div>
<div className="nav-item header_center-left-download">
<i className="icon icon-download"></i>
<span>下載App</span>
</div>
<div className="nav-item header_center-left-search">
<CSSTransition
in={this.props.inputFocus}
timeout={200}
classNames="slide"
>
<input
className={this.props.inputFocus ? 'input-active' : 'input-nor-active'}
placeholder="搜尋"
onFocus={this.props.searchFocus}
onBlur={this.props.searchBlur}
/>
</CSSTransition>
<i className={this.props.inputFocus ? 'icon icon-search icon-active' : 'icon icon-search'}></i>
<div
className={this.props.inputFocus || this.props.mouseInHot ? 'display-show header_center-left-hot-search' : 'display-hide header_center-left-hot-search'}
onMouseEnter={this.props.onMouseEnterHot}
onMouseLeave={this.props.onMouseLeaveHot}
>
<div className="header_center-left-hot-search-title">
<span>熱門搜尋</span>
{/* 2. 在 index.js 中給 i 標籤新增 ref,並通過 changePage 方法傳遞過去 */}
<span onClick={() => this.props.changePage(this.props.page, this.props.totalPage, this.spinIcon)}>
<i className="icon-change" ref={(icon) => {this.spinIcon = icon}}></i>
<span className="span-change">換一批</span>
</span>
</div>
<div className="header_center-left-hot-search-content">
{
this.props.list.map((item, index) => {
if(index >= (this.props.page - 1) * 10 && index < this.props.page * 10) {
return <span key={item}>{item}</span>
} else {
return '';
}
})
}
</div>
</div>
</div>
</div>
<div className="header_center-right">
<div className="nav-item header_right-center-setting">
<span>Aa</span>
</div>
<div className="nav-item header_right-center-login">
<span>登入</span>
</div>
</div>
</div>
<div className="header_right nav-item">
<span className="header_right-register">註冊</span>
<span className="header_right-write nav-item">
<i className="icon icon-write"></i>
<span>寫文章</span>
</span>
</div>
</header>
)
}
}
const mapStateToProps = (state) => {
return {
inputFocus: state.get('header').get('inputFocus'),
list: state.get('header').get('list'),
mouseInHot: state.get('header').get('mouseInHot'),
page: state.get('header').get('page'),
totalPage: state.get('header').get('totalPage'),
}
}
const mapDispathToProps = (dispatch) => {
return {
searchFocus() {
dispatch(actionCreators.getList());
dispatch(actionCreators.searchFocus());
},
searchBlur() {
dispatch(actionCreators.searchBlur());
},
onMouseEnterHot() {
dispatch(actionCreators.onMouseEnterHot());
},
onMouseLeaveHot() {
dispatch(actionCreators.onMouseLeaveHot());
},
changePage(page, totalPage, spinIcon) {
// 3. 在 index.js 中設定它原生 DOM 的 CSS 屬性
if(spinIcon.style.transform === 'rotate(360deg)') {
spinIcon.style.transform = 'rotate(0deg)';
} else {
spinIcon.style.transform = 'rotate(360deg)';
}
if(page === totalPage) {
page = 1;
dispatch(actionCreators.changePage(page));
} else {
dispatch(actionCreators.changePage(page));
}
}
}
}
export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
這裡我們通過三個步驟實現了圖示旋轉:
- 在 index.css 中新增動畫
- 在 index.js 中給
i
標籤新增ref
,並通過changePage
方法傳遞過去 - 在 index.js 中設定它原生 DOM 的 CSS 屬性
實現效果如下:
17.2 避免聚焦重複請求
在程式碼中,我們每次聚焦,都會請求資料,所以我們需要根據 list
的值來判斷是否請求資料:
src/common/header/index.js
程式碼詳情
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { CSSTransition } from 'react-transition-group';
import './index.css';
import { actionCreators } from './store';
import homeImage from '../../resources/img/header-home.png';
class Header extends Component {
render() {
return (
<header>
<div className="header_left">
<a href="/">
<img alt="首頁" src={homeImage} className="header_left-img" />
</a>
</div>
<div className="header_center">
<div className="header_center-left">
<div className="nav-item header_center-left-home">
<i className="icon icon-home"></i>
<span>首頁</span>
</div>
<div className="nav-item header_center-left-download">
<i className="icon icon-download"></i>
<span>下載App</span>
</div>
<div className="nav-item header_center-left-search">
<CSSTransition
in={this.props.inputFocus}
timeout={200}
classNames="slide"
>
<input
className={this.props.inputFocus ? 'input-active' : 'input-nor-active'}
placeholder="搜尋"
// 1. 給 searchFocus 傳遞 list
onFocus={() => this.props.searchFocus(this.props.list)}
onBlur={this.props.searchBlur}
/>
</CSSTransition>
<i className={this.props.inputFocus ? 'icon icon-search icon-active' : 'icon icon-search'}></i>
<div
className={this.props.inputFocus || this.props.mouseInHot ? 'display-show header_center-left-hot-search' : 'display-hide header_center-left-hot-search'}
onMouseEnter={this.props.onMouseEnterHot}
onMouseLeave={this.props.onMouseLeaveHot}
>
<div className="header_center-left-hot-search-title">
<span>熱門搜尋</span>
<span onClick={() => this.props.changePage(this.props.page, this.props.totalPage, this.spinIcon)}>
<i className="icon-change" ref={(icon) => {this.spinIcon = icon}}></i>
<span className="span-change">換一批</span>
</span>
</div>
<div className="header_center-left-hot-search-content">
{
this.props.list.map((item, index) => {
if(index >= (this.props.page - 1) * 10 && index < this.props.page * 10) {
return <span key={item}>{item}</span>
} else {
return '';
}
})
}
</div>
</div>
</div>
</div>
<div className="header_center-right">
<div className="nav-item header_right-center-setting">
<span>Aa</span>
</div>
<div className="nav-item header_right-center-login">
<span>登入</span>
</div>
</div>
</div>
<div className="header_right nav-item">
<span className="header_right-register">註冊</span>
<span className="header_right-write nav-item">
<i className="icon icon-write"></i>
<span>寫文章</span>
</span>
</div>
</header>
)
}
}
const mapStateToProps = (state) => {
return {
inputFocus: state.get('header').get('inputFocus'),
list: state.get('header').get('list'),
mouseInHot: state.get('header').get('mouseInHot'),
page: state.get('header').get('page'),
totalPage: state.get('header').get('totalPage'),
}
}
const mapDispathToProps = (dispatch) => {
return {
searchFocus(list) {
// 2. 判斷 list 的 size 是不是等於 0,是的話才請求資料(第一次),不是的話則不請求
if(list.size === 0) {
dispatch(actionCreators.getList());
}
dispatch(actionCreators.searchFocus());
},
searchBlur() {
dispatch(actionCreators.searchBlur());
},
onMouseEnterHot() {
dispatch(actionCreators.onMouseEnterHot());
},
onMouseLeaveHot() {
dispatch(actionCreators.onMouseLeaveHot());
},
changePage(page, totalPage, spinIcon) {
if(spinIcon.style.transform === 'rotate(360deg)') {
spinIcon.style.transform = 'rotate(0deg)';
} else {
spinIcon.style.transform = 'rotate(360deg)';
}
if(page === totalPage) {
page = 1;
dispatch(actionCreators.changePage(page));
} else {
dispatch(actionCreators.changePage(page));
}
}
}
}
export default connect(mapStateToProps, mapDispathToProps)(Header);
複製程式碼
在這裡,我們做了兩個步驟:
- 給
searchFocus
傳遞list
- 在
searchFocus
中判斷list
的size
是不是等於 0,是的話才請求資料(第一次),不是的話則不請求
這樣,我們就成功避免聚焦重複請求。
十八 React 路由
18.1 路由(一)
- 什麼是路由?
前端路由就是根據 URL 的不同,顯示不同的內容。
- 安裝 React 的路由:
npm i react-router-dom -S
安裝完畢之後,我們只需要修改下 src/App.js
,就可以體驗到路由:
src/App.js
程式碼詳情
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import Header from './common/header';
import store from './store';
// 1. 引入 React 路由的 BrowserRouter 和 Route
import { BrowserRouter, Route } from 'react-router-dom';
class App extends Component {
render() {
return (
<Provider store={store} className="App">
<Header />
{/* 2. 在頁面中使用 React 路由 */}
<BrowserRouter>
<Route path="/" exact render={() => <div>HOME</div>}></Route>
<Route path="/detail" exact render={() => <div>DETAIL</div>}></Route>
</BrowserRouter>
</Provider>
);
}
}
export default App;
複製程式碼
在這裡我們僅需要做兩個步驟:
- 引入 React 路由的
BrowserRouter
和Route
- 在頁面中使用 React 路由
這樣,我們就實現了路由:
18.2 路由(二)
- 在 src 下新建 pages 資料夾,然後在該資料夾下新建資料夾和檔案:
- src/pages/detail/index.js
- src/pages/home/index.js
- 它們的內容如下:
src/pages/detail/index.js
程式碼詳情
import React, { Component } from 'react'
class Detail extends Component {
render() {
return (
<div>Detail</div>
)
}
}
export default Detail;
複製程式碼
src/pages/home/index.js
程式碼詳情
import React, { Component } from 'react'
class Home extends Component {
render() {
return (
<div>Home</div>
)
}
}
export default Home;
複製程式碼
在有 header 的經驗下,我們應該知道,我們希望在 URL 輸入路徑 localhost:3000
的時候,訪問 home 元件;在輸入 localhost:3000/detail
的時候,訪問 detail 元件。
- 到這步,我們僅需要修改下
src/App.js
,就可以實現目標:
src/App.js
程式碼詳情
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import Header from './common/header';
import store from './store';
import { BrowserRouter, Route } from 'react-router-dom';
// 1. 引入 Home、Detail 元件
import Home from './pages/home';
import Detail from './pages/detail';
class App extends Component {
render() {
return (
<Provider store={store} className="App">
<Header />
<BrowserRouter>
{/* 2. 在頁面中引用元件 */}
<Route path="/" exact component={Home}></Route>
<Route path="/detail" exact component={Detail}></Route>
</BrowserRouter>
</Provider>
);
}
}
export default App;
複製程式碼
現在,我們切換下路由,就可以看到不用的頁面,這些頁面我們也可以通過編輯對應的 index.js 來修改了。
十九 頁面實現:二級導航欄
由於前面有過程式設計經驗了,所以在這裡我們就不多說廢話,直接進行實現。
「簡書」因違反《網路安全法》《網際網路資訊服務管理辦法》《網際網路新聞資訊服務管理規定》等相關法律法規,嚴重危害網際網路資訊傳播秩序,根據網信主管部門要求,從 2019 年 4 月 13 日 0 時至 4 月 19 日 0 時,暫停更新 PC 端上的內容,並對所有平臺上的內容進行全面徹底的整改。
沒法,本來想根據簡書的首頁繼續編寫的,但是恰巧碰到簡書出問題了,只好拿掘金的首頁和詳情頁來實現了。
我們將掘金首頁劃分為 3 個模組:頂部 TopNav、左側 LeftList、右側 RightRecommend。所以我們在 home 下面新建個 components 目錄,用來存放這三個元件。同時,在開發 common/header 的時候,我們也知道,還需要一個 store 資料夾,用來存放 reducer.js 等:
- pages
- detail
- index.js
- home
- components
- LeftList.js
- RightRecommend.js
- TopNav.js
- store
- actionCreators.js
- actionTypes.js
- index.js
- reducer.js
- index.css
- index.js
複製程式碼
- src/index.css
程式碼詳情
body {
background: #f4f5f5;
}
複製程式碼
- src/App.js
程式碼詳情
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import Header from './common/header';
import store from './store';
import { BrowserRouter, Route } from 'react-router-dom';
import Home from './pages/home';
import Detail from './pages/detail';
class App extends Component {
render() {
return (
<Provider store={store} className="App">
<Header />
<BrowserRouter>
<Route path="/" exact component={Home}></Route>
<Route path="/detail" exact component={Detail}></Route>
</BrowserRouter>
</Provider>
);
}
}
export default App;
複製程式碼
- src/common/header/index.css
程式碼詳情
header {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 58px;
display: flex;
align-items: center;
border-bottom: 1px solid #f1f1f1;
font-size: 17px;
background: #fff;
}
/* 頭部左邊 */
.header_left-img {
width: 100px;
height: 56px;
}
/* 頭部中間 */
.header_center {
width: 1000px;
margin: 0 auto;
display: flex;
justify-content: space-between;
}
.nav-item {
margin-right: 30px;
display: flex;
align-items: center;
}
/* 頭部中間左部 */
.header_center-left {
display: flex;
}
/* 頭部中間左部 - 首頁 */
.header_center-left-home {
color: #ea6f5a;
}
/* 頭部中間左部 - 搜尋框 */
.header_center-left-search {
position: relative;
}
.slide-enter {
transition: all .2s ease-out;
}
.slide-enter-active {
width: 280px;
}
.slide-exit {
transition: all .2s ease-out;
}
.silde-exit-active {
width: 240px;
}
.header_center-left-search {
z-index: 999;
}
.header_center-left-search input {
width: 240px;
padding: 0 45px 0 20px;
height: 38px;
font-size: 14px;
border: 1px solid #eee;
border-radius: 40px;
background: #eee;
}
.header_center-left-search .input-active {
width: 280px;
}
.header_center-left-search .icon-search {
position: absolute;
top: 8px;
right: 10px;
}
.header_center-left-search .icon-active {
padding: 3px;
top: 4px;
border-radius: 15px;
border: 1px solid #ea6f5a;
}
/* 頭部中間左部 - 熱搜 */
.header_center-left-search .icon-active:hover {
cursor: pointer;
}
.header_center-left-hot-search:before {
content: "";
left: 27px;
width: 10px;
height: 10px;
transform: rotate(45deg);
top: -5px;
z-index: -1;
position: absolute;
background-color: #fff;
box-shadow: 0 0 8px rgba(0,0,0,.2);
}
.header_center-left-hot-search {
position: absolute;
width: 250px;
left: 0;
top: 125%;
padding: 15px;
font-size: 14px;
background: #fff;
border-radius: 4px;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
}
.header_center-left-hot-search-title {
display: flex;
justify-content: space-between;
color: #969696;
}
.header_center-left-hot-search-change {
display: flex;
justify-content: space-between;
align-items: center;
}
.icon-change {
display: inline-block;
width: 20px;
height: 14px;
background: url('../../resources/img/icon-change.png') no-repeat center;
background-size: 100%;
transition: all .2s ease-in;
transform-origin: center center;
}
.icon-change:hover {
cursor: pointer;
}
.span-change:hover {
cursor: pointer;
}
.header_center-left-hot-search-content span {
display: inline-block;
margin-top: 10px;
margin-right: 10px;
padding: 2px 6px;
font-size: 12px;
color: #787878;
border: 1px solid #ddd;
border-radius: 3px;
}
.header_center-left-hot-search-content span:hover {
cursor: pointer;
}
/* 頭部中間右部 */
.header_center-right {
display: flex;
color: #969696;
}
/* 頭部右邊 */
.header_right-register, .header_right-write {
width: 80px;
text-align: center;
height: 38px;
line-height: 38px;
border: 1px solid rgba(236,97,73,.7);
border-radius: 20px;
font-size: 15px;
color: #ea6f5a;
background-color: transparent;
}
.header_right-write {
margin-left: 10px;
padding-left: 10px;
margin-right: 0px;
color: #fff;
background-color: #ea6f5a;
}
複製程式碼
- src/pages/home/index.js
程式碼詳情
import React, { Component } from 'react';
import LeftList from './components/LeftList';
import RightRecommend from './components/RightRecommend';
import TopNav from './components/TopNav';
import './index.css';
class Home extends Component {
render() {
return (
<div className="container">
<TopNav />
<div className="main-container">
<LeftList />
<RightRecommend />
</div>
</div>
)
}
}
export default Home;
複製程式碼
- src/pages/home/index.css
程式碼詳情
/* 主體 */
.container {
width: 960px;
margin: 0 auto;
}
.main-container {
display: flex;
}
/* 頂部 */
.top-nav {
position: fixed;
left: 0;
top: 59px;
width: 100%;
height: 46px;
line-height: 46px;
z-index: 100;
box-shadow: 0 1px 2px 0 rgba(0,0,0,.05);
font-size: 14px;
background: #fff;
}
.top-nav-list {
display: flex;
width: 960px;
margin: auto;
position: relative;
}
.top-nav-list-item a {
height: 100%;
align-items: center;
display: flex;
flex-shrink: 0;
color: #71777c;
padding-right: 12px;
}
.active a {
color: #007fff;
}
.top-nav-list-right {
position: absolute;
top: 0;
right: 0;
}
/* 主內容 */
.main-container {
margin-top: 120px;
}
/* 左側 */
.left-list {
width: 650px;
height: 1000px;
background: #fff;
}
/* 右側 */
.right-recommend {
width: 295px;
height: 1000px;
margin-left: 15px;
background: #fff;
}
複製程式碼
- src/pages/home/components/TopNav.js
程式碼詳情
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
class TopNav extends Component {
render() {
return (
<div className="top-nav">
<ul className="top-nav-list">
<li className="top-nav-list-item active">
<Link to="tuijian">推薦</Link>
</li>
<li className="top-nav-list-item">
<Link to="guanzhu">關注</Link>
</li>
<li className="top-nav-list-item">
<Link to="houduan">後端</Link>
</li>
<li className="top-nav-list-item">
<Link to="qianduan">前端</Link>
</li>
<li className="top-nav-list-item">
<Link to="anzhuo">Android</Link>
</li>
<li className="top-nav-list-item">
<Link to="ios">IOS</Link>
</li>
<li className="top-nav-list-item">
<Link to="rengongzhineng">人工智慧</Link>
</li>
<li className="top-nav-list-item">
<Link to="kaifagongju">開發工具</Link>
</li>
<li className="top-nav-list-item">
<Link to="daimarensheng">程式碼人生</Link>
</li>
<li className="top-nav-list-item">
<Link to="yuedu">閱讀</Link>
</li>
<li className="top-nav-list-item top-nav-list-right">
<Link to="biaoqianguanli">標籤管理</Link>
</li>
</ul>
</div>
)
}
}
export default TopNav;
複製程式碼
- src/pages/home/components/LeftList.js
程式碼詳情
import React, { Component } from 'react'
class LeftList extends Component {
render() {
return (
<div className="left-list">
左側
</div>
)
}
}
export default LeftList;
複製程式碼
- src/pages/home/components/RightRecommend.js
程式碼詳情
import React, { Component } from 'react'
class RightRecommend extends Component {
render() {
return (
<div className="right-recommend">
右側
</div>
)
}
}
export default RightRecommend;
複製程式碼
此時,頁面顯示為:
二十 頁面實現:首頁
20.1 多層級元件引用 store
在我們規劃中,App 是主元件,下面有 header | home | detail,然後 home 下面有 LeftList | RightRecommend,那麼 App/home/leftList 如何引用 store 呢?
src/pages/home/components/LeftList.js
程式碼詳情
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
// 1. 在 LeftList 中引入 react-redux 的 connect
import { connect } from 'react-redux';
import { actionCreators } from '../store';
class LeftList extends Component {
render() {
return (
<div className="left-list">
<div className="left-list-top">
<ul className="left-list-top-left">
<li className="active">
<Link to='remen'>熱門</Link>
</li>
<span>|</span>
<li>
<Link to='zuixin'>最新</Link>
</li>
<span>|</span>
<li>
<Link to='pinglun'>評論</Link>
</li>
</ul>
<ul className="left-list-top-right">
<li>
<Link to='benzhouzuire'>本週最熱</Link>
</li>
·
<li>
<Link to='benyuezuire'>本月最熱</Link>
</li>
·
<li>
<Link to='lishizuire'>歷史最熱</Link>
</li>
</ul>
</div>
<div className="left-list-container">
{/* 5. 迴圈輸出 props 裡面的資料 */}
{
this.props.list.map((item) => {
return (
<div className="left-list-item" key={item.get('id')}>
<div className="left-list-item-tag">
<span className="hot">熱</span>·
<span className="special">專欄</span>·
<span>
{
item.get('user').get('username')
}
</span>·
<span>一天前</span>·
<span>
{
item.get('tags').map((tagsItem, index) => {
if (index === 0) {
return tagsItem.get('title');
} else {
return null;
}
})
}
</span>
</div>
<h3 className="left-list-item-title">
<Link to="detail">{item.get('title')}</Link>
</h3>
<div className="left-list-item-interactive">
<span>{item.get('likeCount')}</span>
<span>{item.get('commentsCount')}</span>
</div>
</div>
)
})
}
</div>
</div>
)
}
componentDidMount() {
this.props.getLeftList();
}
}
// 3. 在 LeftList 中定義 mapStateToProps
const mapStateToProps = (state) => {
return {
list: state.get('home').get('leftNav')
}
};
// 4. 在 LeftList 中定義 mapDispathToProps
const mapDispathToProps = (dispatch) => {
return {
getLeftList() {
dispatch(actionCreators.getLeftList());
}
}
};
// 2. 在 LeftList 中使用 connect
export default connect(mapStateToProps, mapDispathToProps)(LeftList);
複製程式碼
20.2 完善整個首頁
當然,如果僅僅是執行上面的程式碼,你會發現它是報錯的。
是的,因為它只是全部程式碼的一部分,所以需要你去完善它。當然,你也可以直接獲取全部程式碼:
不管如何,你實現的最終成果如下所示:
二十一 總結
寫到這裡,我們已經完成了一個首頁的開發。
在這個開發中,我們學習到了非常多。
當然,後面 jsliang 自己也是偷懶了,慕課原視訊中還有:
- 載入更多功能實現
- 跳轉到頂部功能實現
- 詳情頁開發
- 登入頁開發
- 登入鑑權功能實現
- 單頁面非同步載入元件(react-loadable)
- ……
這裡不一一列舉了,因為 jsliang 感覺它們重複性很大,我們只需要在下一個專案中去實踐,相信能獲得更清晰的印象。(當然,前提是你跟 jsliang 一樣有動力深入學習)
那麼,到這裡我們就宣佈結束啦,我們下篇文章見!
jsliang 廣告推送:
也許小夥伴想了解下雲伺服器
或者小夥伴想買一臺雲伺服器
或者小夥伴需要續費雲伺服器
歡迎點選 雲伺服器推廣 檢視!
jsliang 的文件庫 由 樑峻榮 採用 知識共享 署名-非商業性使用-相同方式共享 4.0 國際 許可協議進行許可。
基於github.com/LiangJunron…上的作品創作。
本許可協議授權之外的使用許可權可以從 creativecommons.org/licenses/by… 處獲得。