第8章 项目实战:首页开发
8-1 什么是路由,如何在React中使用路由功能
npm i react-router-dom
src
——App.js
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import { GlobalStyle } from './style';
import Header from './common/header';
import store from './store';
function App() {
return (
<Provider store={store}>
<GlobalStyle />
<Header />
<BrowserRouter>
<Route path='/' exact render={()=><div>home</div>}></Route>
<Route path='/detail' exact render={()=><div>detail</div>}></Route>
</BrowserRouter>
</Provider>
);
}
export default App;
8-2 首页组件的拆分
1.定义 Home 组件
src
——pages
————home
——————index.js
import React from 'react';
function Home() {
return (
<div>-Home-</div>
);
}
export default Home;
2.定义 Detail 组件
src
——pages
————detail
——————index.js
import React from 'react';
function Detail() {
return (
<div>-Detail-</div>
);
}
export default Detail;
3.引入 Home 和 Detail 组件
src
——App.js
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import Header from './common/header';
import Home from './pages/home';
import Detail from './pages/detail';
import { GlobalStyle } from './style';
import store from './store';
function App() {
return (
<Provider store={store}>
<GlobalStyle />
<Header />
<BrowserRouter>
<Route path='/' exact component={Home}></Route>
<Route path='/detail' exact component={Detail}></Route>
</BrowserRouter>
</Provider>
);
}
export default App;
4.定义 Home 组件的样式
src
——pages
————home
——————style.js
import styled from 'styled-components';
export const HomeWrapper = styled.div`
overflow: hidden;
width: 960px;
margin: 0 auto;
`;
export const HomeLeft = styled.div`
float: left;
margin-left: 15px;
padding-top: 30px;
width: 625px;
.banner-img {
width: 625px;
height: 270px;
}
`;
export const HomeRight = styled.div`
width: 240px;
float: right;
`;
5.在 Home 组件中引入样式
src
——pages
————home
——————index.js
import React from 'react';
import {
HomeWrapper,
HomeLeft,
HomeRight
} from './style';
function Home() {
return (
<HomeWrapper>
<HomeLeft>
<img className="banner-img" src="//upload.jianshu.io/admin_banners/web_images/4318/60781ff21df1d1b03f5f8459e4a1983c009175a5.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/1250/h/540" alt=""/>
</HomeLeft>
<HomeRight></HomeRight>
</HomeWrapper>
);
}
export default Home;
6.拆分 Home 页面的组件
src
——pages
————home
——————components
————————Topic.js
import React from 'react';
function Topic() {
return (
<div>-Topic-</div>
);
}
export default Topic;
————————List.js
import React from 'react';
function List() {
return (
<div>-List-</div>
);
}
export default List;
————————Recommend.js
import React from 'react';
function Recommend() {
return (
<div>-Recommend-</div>
);
}
export default Recommend;
————————Writer.js
import React from 'react';
function Writer() {
return (
<div>-Writer-</div>
);
}
export default Writer;
7.在 Home 组件中引入四个子组件
src
——pages
————home
——————index.js
import React from 'react';
import Topic from './components/Topic';
import List from './components/List';
import Recommend from './components/Recommend';
import Writer from './components/Writer';
import {
HomeWrapper,
HomeLeft,
HomeRight
} from './style';
function Home() {
return (
<HomeWrapper>
<HomeLeft>
<img className="banner-img" src="//upload.jianshu.io/admin_banners/web_images/4318/60781ff21df1d1b03f5f8459e4a1983c009175a5.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/1250/h/540" alt=""/>
<Topic />
<List />
</HomeLeft>
<HomeRight>
<Recommend></Recommend>
<Writer></Writer>
</HomeRight>
</HomeWrapper>
);
}
export default Home;
8-3 首页专题区域布局及reducer的设计
immutable 对象的获取方式
列表循环的获取方式要改为:item.get('imgUrl'),不能为点语法,切记!!!不要忘记!!!
mapStateToProps里的获取方式为:list: state.home.get('blogList')
src
——pages
————home
——————components
————————Topic.js
import React from 'react';
import { TopicWrapper, TopicItem } from '../style';
function Topic() {
return (
<TopicWrapper>
<TopicItem>
<img className='topic-pic' src='//upload.jianshu.io/collections/images/261938/man-hands-reading-boy-large.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64' alt=''/>
社会热点
</TopicItem>
</TopicWrapper>
);
}
export default Topic;
1.将子组件的样式放到 Home 组件的样式里一起管理
src
——pages
————home
——————style.js
// ...
export const TopicWrapper = styled.div`
overflow: hidden;
padding: 20px 0 10px 0;
margin-left: -18px;
border-bottom: 1px solid #dcdcdc;
`;
export const TopicItem= styled.div`
float: left;
height: 32px;
line-height: 32px;
margin-left: 18px;
margin-bottom: 18px;
padding-right: 10px;
background: #f7f7f7;
font-size: 14px;
color: #000;
border: 1px solid #dcdcdc;
border-radius: 4px;
.topic-pic {
display: block;
float: left;
width: 32px;
height: 32px;
margin-right: 10px;
}
`;
2.TopicItem 应该是列表数据循环出来的
src
——pages
————home
——————store
————————reducer.js
import { fromJS } from 'immutable';
const defaultState = fromJS({
topicList: [{
id: 1,
title: '社会热点',
imgUrl: '//upload.jianshu.io/collections/images/261938/man-hands-reading-boy-large.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64'
},{
id: 2,
title: '手绘',
imgUrl: '//upload.jianshu.io/collections/images/21/20120316041115481.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64'
}]
});
export default (state = defaultState, action) => {
switch(action.type) {
default:
return state;
}
};
src
——pages
————home
——————store
————————index.js
import reducer from './reducer';
export { reducer };
3.引用 Home 的 Reducer
src
———store
————reducer.js
import { combineReducers } from 'redux-immutable';
import { reducer as headerReducer } from '../common/header/store';
import { reducer as homeReducer} from '../pages/home/store';
const reducer = combineReducers({
header: headerReducer,
home: homeReducer
})
export default reducer;
4.引入 Store
src
——pages
————home
——————components
————————Topic.js
import React from 'react';
import { connect } from 'react-redux';
import { TopicWrapper, TopicItem } from '../style';
function Topic(props) {
return (
<TopicWrapper>
{
props.list.map((item) => {
return (
<TopicItem key={item.get('id')}>
<img className='topic-pic' src={item.get('imgUrl')} alt=''/>
{item.get('title')}
</TopicItem>
)
})
}
</TopicWrapper>
)
}
const mapState = (state) => ({
list: state.getIn(['home', 'topicList'])
});
export default connect(mapState, null)(Topic);
8-4 首页文章列表制作
1.
src
——pages
————home
——————components
————————List.js
import React from 'react';
import { ListItem, ListInfo } from '../style';
import { connect } from 'react-redux';
function List(props) {
return (
<div>
{
props.list.map((item) => {
return (
<ListItem key={item.get('id')}>
<img className='pic' src={item.get('imgUrl')} alt=''/>
<ListInfo>
<h3 className='title'>{item.get('title')}</h3>
<p className='desc'>{item.get('desc')}</p>
</ListInfo>
</ListItem>
)
})
}
</div>
);
}
const mapState = (state) => ({
list: state.getIn(['home', 'articleList'])
})
export default connect(mapState, null)(List);
2.
src
——pages
————home
——————style.js
//...
export const ListItem = styled.div`
overflow: hidden;
padding: 20px 0;
border-bottom: 1px solid #dcdcdc;
.pic {
display: block;
width: 125px;
height: 100px;
float: right;
border-radius: 4px;
}
`;
export const ListInfo = styled.div`
width: 480px;
float: left;
padding-right: 20px;
.title {
line-height: 27px;
font-size: 18px;
font-weight: bold;
color: #333;
}
.desc {
line-height: 24px;
font-size: 13px;
color: #999;
}
`;
3.
src
——pages
————home
——————store
————————reducer.js
articleList: [{
id: 1,
title: '胡歌12年后首谈车祸:既然活下来了,就不能白白活着',
desc: '文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...',
imgUrl:'https://img.haomeiwen.com/i20131416/b99eb38c969c45b9.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/360/h/240'
}, {
id: 2,
title: '胡歌12年后首谈车祸:既然活下来了,就不能白白活着',
desc: '文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...',
imgUrl:'https://img.haomeiwen.com/i20131416/b99eb38c969c45b9.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/360/h/240'
}]
8-5 首页推荐部分代码编写
8-6 首页异步数据获取
1.定后端接口,获取真实数据
/api/home.json
{
"sucess": true,
"data": {
"topicList": [],
"articleList": [],
"recommendList": []
}
}
2.创建一个 JSON 文件做接口数据的模拟
public
——api
————home.json
{
"success": true,
"data": {
"topicList": [
{
"id": 1,
"title": "社会热点",
"imgUrl": "//upload.jianshu.io/collections/images/261938/man-hands-reading-boy-large.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64"
},
{
"id": 2,
"title": "手绘",
"imgUrl": "//upload.jianshu.io/collections/images/21/20120316041115481.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/64/h/64"
}
],
"articleList": [
{
"id": 1,
"title": "胡歌12年后首谈车祸",
"desc": "文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...",
"imgUrl": "https://img.haomeiwen.com/i2259045/2986b9be86b01f63?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240"
},
{
"id": 2,
"title": "胡歌12年后首谈车祸:既然活下来了,就不能白白活着",
"desc": "文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...",
"imgUrl": "https://img.haomeiwen.com/i2259045/2986b9be86b01f63?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240"
},
{
"id": 3,
"title": "胡歌12年后首谈车祸:既然活下来了,就不能白白活着",
"desc": "文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...",
"imgUrl": "https://img.haomeiwen.com/i2259045/2986b9be86b01f63?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240"
},
{
"id": 4,
"title": "胡歌12年后首谈车祸:既然活下来了,就不能白白活着",
"desc": "文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...",
"imgUrl": "https://img.haomeiwen.com/i2259045/2986b9be86b01f63?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240"
}
]
}
}
3.清空前端的数据
src
——pages
————home
——————store
————————reducer.js
import { fromJS } from 'immutable';
const defaultState = fromJS({
topicList: [],
articleList: []
});
export default (state = defaultState, action) => {
switch(action.type) {
default:
return state;
}
};
4.通过 AJAX 获取数据,改为《状态组件》
npm install axios
src
——pages
————home
——————index.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import axios from 'axios';
class Home extends Component {
render() {
return (
<HomeWrapper>
...
</HomeWrapper>
);
}
componentDidMount() {
axios.get('/api/home.json').then((res) => {
const result = res.data.data;
const action = {
type: 'change_home_data',
topicList: result.topicList,
articleList: result.articleList
}
this.props.changeHomeData(action)
})
}
}
const mapDispatch = (dispatch) => ({
changeHomeData(action) {
dispatch(action);
}
});
export default connect(null, mapDispatch)(Home);
5.派发给 Store,Store 转发给 Reducer
src
——pages
————home
——————store
————————reducer.js
import { fromJS } from 'immutable';
const defaultState = fromJS({
topicList: [],
articleList: []
});
export default (state = defaultState, action) => {
switch(action.type) {
case 'change_home_data':
return state.merge({
topicList: fromJS(action.topicList),
articleList: fromJS(action.articleList)
})
default:
return state;
}
};
8-7 异步操作代码拆分优化
1.UI 组件并不应该有太多业务逻辑
src
——pages
————home
——————index.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { actionCreators } from './store';
class Home extends Component {
render() {
return (
<HomeWrapper>
...
</HomeWrapper>
);
}
componentDidMount() {
this.props.changeHomeData();
}
}
const mapDispatch = (dispatch) => ({
changeHomeData() {
const action = actionCreators.getHomeInfo();
dispatch(action);
}
});
export default connect(null, mapDispatch)(Home);
2.在创建 Store 的地方引用 redux-thunk
npm install redux-thunk
src
——store
————index.js
import { createStore, compose, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducer';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(reducer, composeEnhancers(
applyMiddleware(thunk)
));
export default store;
3.通过 redux-thunk 将异步放在 action 里统一管理
src
——pages
————home
——————store
————————actionCreators.js
import axios from 'axios';
import * as constants from './constants';
const changeHomeData = (result) => ({
type: constants.CHANGE_HOME_DATA,
topicList: result.topicList,
articleList: result.articleList
})
export const getHomeInfo = () => {
return (dispatch) => {
axios.get('/api/home.json').then((res) => {
const result = res.data.data;
dispatch(changeHomeData(result));
})
}
}
src
——pages
————home
——————store
————————index.js
import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as constants from './constants';
export { reducer, actionCreators, constants };
4.
src
——pages
————home
——————store
————————constants.js
export const CHANGE_HOME_DATA = 'home/CHANGE_HOME_DATA';
5.
src
——pages
————home
——————store
————————reducer.js
import { fromJS } from 'immutable';
import * as constants from './constants';
const defaultState = fromJS({
topicList: [],
articleList: []
});
export default (state = defaultState, action) => {
switch(action.type) {
case constants.CHANGE_HOME_DATA:
return state.merge({
topicList: fromJS(action.topicList),
articleList: fromJS(action.articleList)
})
default:
return state;
}
};
8-8 实现加载更多功能
1.新增加载点击事件
src
——pages
————home
——————components
————————List.js
import { ListItem, ListInfo, LoadMore } from '../style';
// ...
import { actionCreators } from '../store';
function List(props) {
const { list, getMoreList, page } = props;
return (
<div>
{
list.map((item, index) => {
return (
<ListItem key={index}>
// ...
</ListItem>
)
})
}
<LoadMore onClick={() => getMoreList(page)}>更多文字</LoadMore>
</div>
);
}
const mapState = (state) => ({
list: state.getIn(['home', 'articleList']),
page: state.getIn(['home', 'articlePage'])
});
const mapDispatch = (dispatch) => ({
getMoreList(page) {
dispatch(actionCreators.getMoreList(page));
}
});
export default connect(mapState, mapDispatch)(List);
2.定义组件样式
src
——pages
————home
——————style.js
//...
export const LoadMore = styled.div`
width:100%;
height: 40px;
line-height: 40px;
margin: 30px 0;
background: #a5a5a5;
text-align: center;
border-radius: 20px;
color: #fff;
cursor: pointer;
`;
3.利用 redux-thunk 发送 AJAX 请求
src
——pages
————home
——————store
————————actionCreators.js
import axios from 'axios';
import * as constants from './constants';
import { fromJS } from 'immutable';
const changeHomeData = (result) => ({
// ...
});
const addHomeList = (list, nextPage) => ({
type: constants.ADD_ARTICLE_LIST,
list: fromJS(list),
nextPage
});
export const getHomeInfo = () => {
// ...
};
export const getMoreList = (page) => {
return (dispatch) => {
axios.get('/api/homeList.json?page=' + page).then((res) => {
const result = res.data.data;
dispatch(addHomeList(result, page + 1));
})
}
};
4.mock 数据
public
——api
————homeList.json
{
"success": true,
"data": [{
"id": 5,
"title": "胡歌12年后首谈车祸",
"desc": "文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...",
"imgUrl": "https://img.haomeiwen.com/i2259045/2986b9be86b01f63?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240"
}, {
"id": 6,
"title": "胡歌12年后首谈车祸:既然活下来了,就不能白白活着",
"desc": "文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...",
"imgUrl": "https://img.haomeiwen.com/i2259045/2986b9be86b01f63?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240"
}, {
"id": 7,
"title": "胡歌12年后首谈车祸:既然活下来了,就不能白白活着",
"desc": "文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...",
"imgUrl": "https://img.haomeiwen.com/i2259045/2986b9be86b01f63?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240"
}, {
"id": 8,
"title": "胡歌12年后首谈车祸:既然活下来了,就不能白白活着",
"desc": "文/麦大人 01 胡歌又刷屏了。 近日他上了《朗读者》,而这一期的主题是“生命”,他用磁性的嗓音,朗读了一段《哈姆雷特》中的经典独白,相当震撼:...",
"imgUrl": "https://img.haomeiwen.com/i2259045/2986b9be86b01f63?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/240"
}]
}
5.
src
——pages
————home
——————store
————————constants.js
export const ADD_ARTICLE_LIST = 'home/ADD_ARTICLE_LIST';
6.加载更多 + 分页
src
——pages
————home
——————store
————————reducer.js
import { fromJS } from 'immutable';
import * as constants from './constants';
const defaultState = fromJS({
topicList: [],
articleList: [],
articlePage: 1
});
export default (state = defaultState, action) => {
switch(action.type) {
case constants.CHANGE_HOME_DATA:
return state.merge({
topicList: fromJS(action.topicList),
articleList: fromJS(action.articleList)
});
case constants.ADD_ARTICLE_LIST:
return state.merge({
articleList: state.get('articleList').concat(action.list),
articlePage: action.nextPage
});
default:
return state;
}
};
加载更多:
- 定义点击事件,组件样式
- 调用 获取更多的方法
- 发送请求,保存数据到 store
- 在 store 更新数据
分页:
- 在 store 里初始化页码
- 在列表页码获取页码
- 在事件触发的方法里传入页码
- 在请求的 URL 里加入页码
- 每次分发 action 的页码+1
8-9 返回顶部功能实现
1.代码量很少,直接写在 index 里
src
——pages
————home
——————index.js
//...
import {
HomeWrapper,
HomeLeft,
HomeRight,
BackTop
} from './style';
class Home extends Component {
handleScrollTop() {
window.scrollTo(0, 0);
}
render() {
return (
<HomeWrapper>
...
{ this.props.showScroll ? <BackTop onClick={this.handleScrollTop}>Top</BackTop> : null}
</HomeWrapper>
);
}
componentDidMount() {
//...
this.bindEvents();
}
componentWillUnmount() {
window.removeEventListener('scroll', this.props.changeScrollTopShow);
}
bindEvents() {
window.addEventListener('scroll', this.props.changeScrollTopShow);
}
}
const mapState = (state) => ({
showScroll: state.getIn(['home', 'showScroll'])
})
const mapDispatch = (dispatch) => ({
changeHomeData() {
//...
},
changeScrollTopShow() {
if (document.documentElement.scrollTop > 400) {
dispatch(actionCreators.toggleTopShow(true));
} else {
dispatch(actionCreators.toggleTopShow(false));
}
}
});
export default connect(mapState, mapDispatch)(Home);
2.
src
——pages
————home
——————style.js
//...
export const BackTop = styled.div`
position: fixed;
right: 100px;
bottom: 100px;
width: 60px;
height: 60px;
line-height: 60px;
text-align: center;
border: 1px solid #ccc;
font-size: 14px;
cursor: pointer;
`;
3.
src
——pages
————home
——————store
————————reducer.js
import { fromJS } from 'immutable';
import * as constants from './constants';
const defaultState = fromJS({
//...
showScroll: false
});
const changeHomeData = (state, action) => {
return state.merge({
topicList: fromJS(action.topicList),
articleList: fromJS(action.articleList)
})
}
const addArticleList = (state, action) => {
return state.merge({
articleList: state.get('articleList').concat(action.list),
articlePage: action.nextPage
});
}
export default (state = defaultState, action) => {
switch(action.type) {
case constants.CHANGE_HOME_DATA:
return changeHomeData(state, action);
case constants.ADD_ARTICLE_LIST:
return addArticleList(state, action);
case constants.TOGGLE_SCROLL_TOP:
return state.set('showScroll', action.show);
default:
return state;
}
};
4.
src
——pages
————home
——————store
————————actionCreators.js
import axios from 'axios';
import * as constants from './constants';
import { fromJS } from 'immutable';
const changeHomeData = (result) => ({
// ...
});
const addHomeList = (list, nextPage) => ({
//...
});
export const getHomeInfo = () => {
// ...
};
export const getMoreList = (page) => {
//...
};
export const toggleTopShow = (show) => {
type: constants.TOGGLE_SCROLL_TOP,
show
};
src
——pages
————home
——————store
————————constants.js
export const TOGGLE_SCROLL_TOP = 'home/TOGGLE_SCROLL_TOP';
8-10 首页性能优化及路由跳转
代码调优:只重新渲染相关组件
src
——pages
————home
——————index.js
import React, { PureComponent } from 'immutable';
class Home extends PureComponent {
// shouldComponentUpdate() {}
}
路由跳转
src
——pages
————home
——————components
————————List.js
//...
import { Link } from 'react-router-dom';
function List(props) {
const { list, getMoreList, page } = props;
return (
<div>
{
list.map((item, index) => {
return (
<Link key={index} to='/detail'>
<ListItem>
// ...
</ListItem>
</Link>
)
})
}
<LoadMore onClick={() => getMoreList(page)}>更多文字</LoadMore>
</div>
);
}
const mapState = (state) => ({
//...
});
const mapDispatch = (dispatch) => ({
//...
});
export default connect(mapState, mapDispatch)(List);
src
——common
————header
——————index.js
// ...
<Link to='/'>
<Logo />
</Link>
// ...
src
——common
————header
——————style.js
export const Logo = styled.div``
You should not use <Link> outside a <Router>
src
——App.js
//...
function App() {
return (
<Provider store={store}>
<GlobalStyle />
<BrowserRouter>
<Header />
<Route path='/' exact component={Home}></Route>
<Route path='/detail' exact component={Detail}></Route>
</BrowserRouter>
</Provider>
);
}
//...
网友评论