前言
接触React也有一个月了。为了能让自己更好的学习React,于是便寻思着写下自己实战的过程。
这是一篇很简单的实战,不涉及Redux,非常适合像我一样的初学者。
本文用的是class式的写法,当然接下来也会用函数式重写一遍。
源码
技术栈
react + react-router + axios+ antd
实现效果
项目结构
|-cnode 项目名
|-node_modules 依赖包
|-public
|-src
|-components 组件目录
|-articlelist 首页文章列表组件
|-articlelistmenu 首页文章导航组件
|-detailmain 文章详情主体组件
|-detailreply 文章详情回复组件
|-usertopic 用户详情组件
|-pages 页面
|-home 首页
|-detail 文章详情页
|-user 用户详情页
|-routes 路由配置
|-utils 工具类
|-App.css 页面样式
|-App.jsx 主页面
|-index.js 入口文件
packjon.json 全局配置
README.md readme文件
功能实现
路由配置
首先是路由配置,虽然只有三个页面,但是为了结构清晰,且培养好的编码习惯,还是将路由配置抽离出来。
这里我将项目的路由配置文件放在routes文件下,然后在App.js中引入,并map循环遍历出来。
路由配置文件
import Home from '../pages/home';
import User from '../pages/user';
import Detail from '../pages/detail';
const routes = [
{
path:'/:tab?',
name:'首页',
exact:true,
component:Home
},
{
path:'/detail/:id?',
name:'文章详情',
exact:true,
component:Detail
},
{
path:'/user/:loginname?',
name:'用户详情',
exact:true,
component:User
}
];
export default routes;
App.jsx
import React from 'react';
import Axios from 'axios';
import './App.css';
import { Switch , Route , Link} from 'react-router-dom';
import { Layout } from 'antd';
import routes from './routes/index';
const { Header , Content } = Layout;
Axios.defaults.baseURL = 'https://cnodejs.org/api/v1'; //设置请求默认地址
class App extends React.Component{
render(){
return (
<Layout>
<Header className='header'> {/* 设置项目通用头部,主要包括logo */}
<Link to='/'>
<div className='logo'>
<img src="//static2.cnodejs.org/public/images/cnodejs_light.svg" alt=""/>
</div>
</Link>
</Header>
<Content className='main'> {/* 循环遍历路由配置。并根据不同的路由渲染不同的内容 */}
<Switch>
{
routes.map( item =>{
return (
<Route key={item.name} exact={item.exact} path={item.path} component={item.component} />
)
})
}
</Switch>
</Content>
</Layout>
)
}
}
export default App;
首页
首页主要包括导航和文章列表两个组件。
menuList , colorList这两个配置文件是为了循环渲染出5个导航,并根据不同的导航渲染不同的图标。
import React , { Component } from 'react';
import { Row , Col } from 'antd';
import ArticleList from '../components/articlelist';
import ArticleMenu from '../components/articlelistmenu';
import { menuList , colorList } from '../utils/utils';
export default class Home extends Component {
render(){
const tab = this.props.match.params.tab ?? 'all';
//这里使用的是ES的新语法,意思是tab为null或undefined时默认为'all'
return (
<Row>
<Col span={2}>
<ArticleMenu menuList={menuList} tab={tab} /> {/* 引入导航组件 */}
</Col>
<Col span={22}>
<ArticleList menuList={menuList} colorList={colorList} tab={tab} /> {/* 引入文章列表组件 */}
</Col>
</Row>
)
}
}
导航组件
将初始导航状态存在state里的tab里,并在componentDidMount之后将父组件传进来的tab设为state里的tab。
将父组件传进来的menuList循环遍历成5个导航,因为menuList是一个对象,所以这里使用Object.entries方法。这个方法可以将对象设为二维数组并返回。
import React , { Component } from 'react';
import { Menu } from 'antd';
import { Link } from 'react-router-dom';
export default class ArticleMenu extends Component {
state={
tab:'all'
}
componentDidMount(){
this.setState({
tab:this.props.tab
})
}
render(){
const { menuList } = this.props;
const { tab } = this.state;
return (
<Menu
theme='light'
mode='vertical'
selectedKeys={[tab]}
onSelect={({key})=>{
this.setState({tab:key})
}}
>
{
Object.entries(menuList).map(item => {
return <Menu.Item key={item[0]}>
<Link to={`/${item[0]}`}>{item[1]}</Link>
</Menu.Item>
})
}
</Menu>
)
}
}
tips:这里有个两个小坑。
1.导航选中一项后,刷新页面,会发现路由没变,但是导航的选中状态变为了[全部]
2.导航的选中状态不会随着点击而实时改变
解决方法:
1.原因是使用了antd的Menu组件,并设置了defaultSelectedKeys,将defaultSelectedKeys修改为selectedKeys就好了
2.同样是Menu组件的原因,设置onSelcct,通过onSelcct触发的回调函数更改state里的tab值即可
文章列表组件
主要作用是通过点击导航列表里的不同项,加载不同类型的文章列表,并渲染在页面上。
在这个组件里将数据请求封装在了getData方法里,并在componentDidMount组件挂载完成后触发。
import React,{ Component } from 'react';
import Axios from 'axios';
import { fromNow , locate } from 'silly-datetime'
import { List , Avatar , Tag } from 'antd';
import {Link} from 'react-router-dom';
locate('zh-cn');
export default class ArticleList extends Component{
state = {
dataList:[],
page:1
}
componentDidMount(){
const { tab } = this.props;
this.getData(tab);
}
shouldComponentUpdate(nextProps){
if(nextProps.tab !== this.props.tab){
this.getData(nextProps.tab)
return false;
}
return true;
}
getData = (tab='all',page=1) => {
Axios.get(`/topics?page=${page}&tab=${tab}&limit=${15}`
)
.then(data => data.data.data)
.then( data => this.setState({dataList:data}))
}
render(){
const { dataList } = this.state;
const { menuList , colorList , tab } = this.props;
return (
<List
className='list'
dataSource={dataList}
pagination={{
pageSize: 15 ,
total: 600 ,
showSizeChanger:false ,
onChange:(page)=>{
this.setState({page});
this.getData( tab ,page)
}
}}
renderItem={(item)=>{
return (<List.Item
key={item.id}
className='item'
actions={[`回复 ${item.reply_count}`,`访问 ${item.visit_count}`]}
>
<List.Item.Meta
avatar={
<Link to={`/user/${item.author.loginname}`} >
<Avatar src={item.author.avatar_url}/>
</Link>
}
title={
<div>
<Tag
color={colorList[item.tab]}
>
{item.top ? '置顶' : (item.good ? '精华' : menuList[item.tab])}
</Tag>
<Link to={`/detail/${item.id}`}>
{item.title}
</Link>
</div>
}
description={
<div>
<Link to={`/user/${item.author.loginname}`} className='user_name'>
{item.author.loginname}
</Link>
<span className='reply_time'>
最后回复时间:{fromNow(item.last_reply_at)}
</span>
</div>
}
/>
</List.Item>)
}}
/>
)
}
}
tips:这里也有个小坑,就是点击不同的导航项,路由改变,但列表数据并不会重新渲染。
摸索后发现原因是数据请求只在componentDidMount里触发了一次,要在shouldComponentUpdata里通过对比props传入的tab,不同则再请求一次数据来触发页面更新。
文章详情页
文章详情页主要包括文章主体和文章回复两个组件。
在componentDidMount组件挂载完毕后请求数据,并将数据传递给两个子组件。
import React, { Component } from 'react';
import Axios from 'axios';
import DetailMain from '../components/detailmain';
import DetailReply from '../components/detailreply';
export default class Detail extends Component{
state = {
data:[]
}
componentDidMount(){
const { id } = this.props.match.params;
Axios.get(`/topic/${id}`).then(data => {
this.setState({data:data.data.data})
})
}
render(){
return (
<div className='wrap'>
<DetailMain data={this.state.data} />
{this.state.data.replies?.length ? <DetailReply data={this.state.data.replies} /> : '' }
</div>
)
}
}
文章主体内容组件
这个组件UI采用的是antd的Card,接受到父组件传递过来的数据后进行展示。
由于数据中的内容部分接受到的是Dom结构的数据,所以采用dangerouslySetInnerHTML来加载内容。
import React,{ Component } from 'react';
import { fromNow } from 'silly-datetime';
import { Card , Tag } from 'antd';
import { menuList , colorList } from '../utils/utils';
export default class DetailMain extends Component{
render(){
const { data } = this.props;
return (
<Card
type='inner'
title={
<div>
<div className='title'>
<Tag
color={colorList[data.tab]}
>
{data.top ? '置顶' : (data.good ? '精华' : menuList[data.tab])}
</Tag>
<span className='title_name'>
{data.title}
</span>
</div>
<div className='info'>
<span> 发布于 {fromNow(data.create_at)} </span>
<span> 作者 {data.author?.loginname} </span>
<span> {data.visit_count} 次浏览 </span>
<span> 来自 {menuList[data.tab]} </span>
</div>
</div>
}
>
{
<div dangerouslySetInnerHTML={{
__html:data.content
}}
className='content'
/>
}
</Card>
)
}
}
文章回复内容组件
这个组件在文章回复量为0的时候不展示。
由于数据中的内容部分接受到的是Dom结构的数据,所以采用dangerouslySetInnerHTML来加载内容。
import React,{ Component } from 'react';
import { Card , List , Avatar } from 'antd';
import { LikeOutlined } from '@ant-design/icons';
import { fromNow } from 'silly-datetime';
import { Link } from 'react-router-dom';
const { Item } = List;
export default class DetailReply extends Component{
render(){
const { data } = this.props;
return (
<Card
title={
<div>{data?.length}条回复</div>
}
>
<List
itemLayout='vertical'
className='reply_list'
pagination={{
pageSize:5,
total: data?.length,
hideOnSinglePage:true
}}
dataSource={data}
renderItem={(item)=>{
return (
<Item
extra={item.ups?.length ? <div><LikeOutlined /> {item.ups?.length}</div> : '' }
className='item'
>
<Item.Meta
avatar={
<Link to={`/user/${item.author.loginname}`}>
<Avatar src={item.author?.avatar_url}/>
</Link>
}
description={
<div>
<Link to={`/user/${item.author.loginname}`} className='user_name'>
{item.author?.loginname}
</Link>
<span>发表于:{fromNow(item.create_at)}</span>
</div>
}
>
</Item.Meta>
{
<div dangerouslySetInnerHTML={{
__html:item.content
}} />
}
</Item>
)
}}
/>
</Card>
)
}
}
用户详情页
用户详情页主要包括用户信息、用户最近创建的话题、用户最近回复的话题三个部分。
用户信息内容直接用antd的Card组件的title配合Descriptions组件来完成。
由于最近创建的话题和最近回复的话题两部分非常相似,所以采用同一个组件配合父组件传入的不同数据来完成。
**tips:**这里有个坑,我摸索了很久才解决。在最近回复的话题点击其他用户的头像,路由改变但是页面不会重新渲染。
经过摸索和google发现也是由于数据只在component里只请求了一次,所以页面不会更新。
在componentDidUpdate对比前后props再请求一次数据即可。
import React, { Component } from 'react';
import { Card , Descriptions , Avatar} from 'antd';
import Axios from 'axios';
import { fromNow } from 'silly-datetime';
import UserTopic from '../components/usertopic';
const { Item } = Descriptions;
export default class User extends Component{
state = {
data : []
}
componentDidMount(){
const { loginname } = this.props.match.params;
Axios.get(`/user/${loginname}`).then( data => data.data.data ).then( data => this.setState({data}))
}
componentDidUpdate(prevProps){
if(prevProps.match.params.loginname !== this.props.match.params.loginname){
Axios.get(`/user/${this.props.match.params.loginname}`)
.then(data => data.data.data)
.then(data => this.setState({data}))
}
}
render(){
const { data } = this.state;
return (
<div className='wrap'>
<Card
title={
<Descriptions
title={<Avatar src={data.avatar_url} style={{marginBottom:'20px'}} />}
>
<Item label='用户名'>
{data.loginname}
</Item>
<Item label='积分'>
{data.score}
</Item>
<Item label='创建时间'>
{fromNow(data.create_at)}
</Item>
</Descriptions>
}
>
<UserTopic title={'最近创建的话题'} data={data.recent_topics} />
<UserTopic title={'最近回复的话题'} data={data.recent_replies} />
</Card>
</div>
)
}
}
用户最近创建的话题和用户最近回复的话题
这两个部分只是数据上有差别。主体都是相似的。
import React,{ Component } from 'react';
import { Card , List , Avatar } from 'antd';
import { Link } from 'react-router-dom';
import { fromNow } from 'silly-datetime';
const { Item } = List;
export default class UserTopic extends Component{
render(){
const { title , data } = this.props;
return(
<Card
title={title}
>
<List
dataSource={data}
pagination={{
pageSize: 5 ,
total: data?.length ,
hideOnSinglePage:true
}
}
renderItem={(item)=>{
return (<Item
key={item.id}
className='item'
extra={`最后回复时间:${fromNow(item.last_reply_at)}`}
>
<Item.Meta
avatar={
<Link to={`/user/${item.author.loginname}`}>
<Avatar src={item.author.avatar_url}/>
</Link>
}
title={
<div>
<Link to={`/detail/${item.id}`}>
{item.title}
</Link>
</div>
}
/>
</Item>)
}}
/>
</Card>
)
}
}
结语
项目传送门
虽然项目看起来简单,但是自己写起来还是碰到不少问题。不断的通过google和看文档来解决一个又一个坑,并牢记不再踩也是成长的一部分。
尤其是相同页面但是参数不同页面不重新渲染的坑,以后要牢记。
不能只在componentDidMount里只请求一次数据,要在shouldComponentUpdate或componentDidUpdate通过对比前后props来请求一次数据。
最后,项目还有很多需要完善和改进,如果有错误或者可以优化的地方,欢迎各位大佬指点一二。
**最后的最后,臭不要脸的求一个STAR!**😋