内容简介:学习React不久,觉得实战才是检验自己学习程度的最好方法,也顺便加深一下自己对React的理解,于是做了这么一个小项目分享一下。作为一个单页面项目,第一步当然是搭建页面路由了,因为是一个菜谱项目,所以路由还是比较多的,这里我把路由的结构都放在config文件下,在NavLeft导航组件下用map函数去将菜单渲染出来,这样既避免了自己一个一个去写重复的代码,也方便后面添加新的导航。实现代码:
学习React不久,觉得实战才是检验自己学习程度的最好方法,也顺便加深一下自己对React的理解,于是做了这么一个小项目分享一下。
- react
- react-router
- react-redux
- less
基本项目搭建
- node开发环境
- 安装依赖: yarn
- 项目启动: yarn start
- 涉及到第三方API接口,小伙伴们可以自己去接口地址申请一个appkey,毕竟请求次数也是有限的嘛
页面结构
|-react-kitchen 项目名 |-node_modules 依赖包 |-public |-src |-api 请求数据接口 |-components 组件目录 |-CardList 卡片列表组件 |-Footer 底部组件 |-Header 头部组件 |-NavLeft 左侧导航 |-NavRight 右侧标签 |-config 菜单配置 |-pages 页面 |-Collections 收藏页 |-Detail 详情页 |-Home 首页 |-Search 搜索页 |-NoMatch 无数据页 |-。。。 其他导航页 |-redux redux数据管理 action-types actions reducers store |-utils 工具类 admin.js 页面外层结构 App.js 页面路由 common.less 页面样式 index.js 入口文件 config-overrides.js antd主题设置 packjon.json 全局配置 README.md readme文件 复制代码
功能实现
路由配置
作为一个单页面项目,第一步当然是搭建页面路由了,因为是一个菜谱项目,所以路由还是比较多的,这里我把路由的结构都放在config文件下,在NavLeft导航组件下用map函数去将菜单渲染出来,这样既避免了自己一个一个去写重复的代码,也方便后面添加新的导航。
实现代码:
import React from 'react'; import { Menu} from 'antd'; import { NavLink } from 'react-router-dom' import MenuConfig from '../../config/menuConfig' const SubMenu = Menu.SubMenu; export default class NavLeft extends React.Component { componentWillMount() { const menuTreeNode = this.renderMenu(MenuConfig); this.setState({ menuTreeNode }) } // // 菜单渲染 renderMenu = (data) => { return data.map((item) => { if (item.children) { return ( <SubMenu title={item.title} key={item.key}> {this.renderMenu(item.children)} </SubMenu> ) } return <Menu.Item title={item.title} key={item.key}> <NavLink to={item.key}>{item.title}</NavLink> </Menu.Item> }) } render() { return ( <div> <Menu onClick={this.handleClick} > {this.state.menuTreeNode} </Menu> </div> ) } } 复制代码
CardList组件封装
菜谱的预览图用的是antd的Card组件,页面刚开始加载的时候向API请求很多组数据,而且几乎每个导航页用到的列表都是一样的,这里就应该把整个列表抽取出来成为一个组件进行复用。
先从接口中获取数据列表
getMenuAPIList = (keyword) => { const num = 12 Axios .jsonp({ url: `http://api.jisuapi.com/recipe/search?keyword=${keyword}&num=${num}&appkey=9d1f6ec2fd2463f7` }) .then(res => { if (res.status === '0') { let cardList = this.renderCardList(res.result.list) this.setState({ cardList: cardList }) } }) } 复制代码
再调用数据渲染列表页,这里需要注意的是,渲染完预览图之后,点击进入到详情页如何获取当前的的数据去渲染详情页呢?
我想到了三种思路:
- 将数据传到共同的父组件,父组件通过props的方式再将数据传给详情页组件
- 通过路由的方式,react-router v4 中 link可以通过state的方式将参数传递给下一个组件,下一个组件可以通过this.props.location.state来得到数据
- 使用redux来管理数据
这里我用的是第二种方式
// 渲染卡片列表 renderCardList = (data) => { return data.map((item) => { return ( <NavLink key={item.id} to={{ pathname: `/common/detail/${item.id}`, state: item }} > <Card hoverable className="card" cover={<img alt="example" src={item.pic} />} onClick={this.openMenuDetail} id={item.id} > <Meta style={{ whiteSpace: 'nowrap' }} title={item.name} description={item.tag} /> </Card> </NavLink> ) }) } 复制代码
搜索功能
上面我们说到,可以用link携带参数进行组件之间的通信,这里的搜索功能我是用redux进行组件之间的数据传输,也就是将输入框的value值传给搜索页组件,让它拿到value值后去向API请求数据。
- 先用createStore生成一个store容器,容器接受一个纯函数reducer作为参数返回新的store
const store = createStore(reducer)
- reducer接受 Action 和当前 State 作为参数,返回一个新的 State
export function reducer(state = 1, action) { switch (action.type) { case TRANSMIT: return action.data default: return state } } 复制代码
- 输入框中的value值有无数种,也就是用户发送的Action有无数种,可以用一个Action Creator函数来生成Actions
export const transmit = (data) => { return { type: TRANSMIT, data: data } } 复制代码
- 这里引入react-redux 用Provider将根组件包裹起来,所有的子组件默认都可以拿到state
ReactDOM.render(<Provider store={store} ><App /></Provider>, document.getElementById('root')); 复制代码
- 用connect()连接UI组件Header和Search,connect方法接受两个参数: mapStateToProps和mapDispatchToProps。 mapStateToProps会订阅store,state更新时会自动执行,Search组件可以通过this.props.keyword来拿到当前的state, mapDispatchToProps作为对象,里面的每个键值被当做Action Creator
export default connect( state => ({keyword: state}), {transmit} )(Header) export default connect( state => ({keyword : state}), {} )(Search) 复制代码
由于自己对redux了解并不是很深,所以这里过程讲的有点繁琐,简单地分享自己的一点理解,小伙伴可以去看看阮一峰老师的redux教程,讲的非常细致
收藏功能
收藏夹功能主要是用localStorage实现,主要的思路是:点击收藏时,判断数据在localstorage中是否存在,不存在,先将数据用JSON.stringify()转化为字符串存进localStorage,localstorage.setItem(),存在则localstorage.removeItem()取消收藏
handleCollect = () => { let starColor = this.state.starColor let isCollect = this.state.isCollect const menu = JSON.stringify(this.state.menu) const menuName = this.state.menu.name if (isCollect === false) { starColor = '#FDDA04' isCollect = !isCollect localStorage.setItem(menuName, menu) } else { starColor = '#52c41a' isCollect = !isCollect localStorage.removeItem(menuName) } this.setState({ starColor, isCollect }) message.success((isCollect ? '收藏成功' : '取消收藏'), 1) } 复制代码
项目踩坑
antd Input.Search
点击搜索实现路由跳转 因为antd把输入框和按钮封装了 如果用link包裹Search,没输入文字就会直接跳转
解决办法:不用Input.Search, 直接用input输入框+Button按钮,在Button的点击事件中获取input的value值,再用Link包裹按钮进行路由跳转。这是我想到的办法,如果还有更好的解决办法,也欢迎小伙伴提出~
搜索页面的重新渲染
启用react-redux管理数据,在页面第一次渲染的时候用componentWillMount请求api接口函数,将状态进行传参用的是this.props.keyword,之后的搜索渲染页面的时候用的钩子函数是componentWillReceiveProps,这个时候传递的参数是nextProps.keyword,而不是this.props.keyword
react渲染html代码例如 <br />
时无法正确显示
原因: react的JSX 防注入攻击XSS使得大括号里的html代码全部变成字符串进行渲染,而不是html代码
解决:使用标签属性dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{__html: code}}></div> 复制代码
结语
项目传送门
写项目的时候也遇到了许多小问题,都是慢慢查文档一个一个解决的,不断的思考然后解决问题也是成长的一部分。
当然,项目还有许多需要完善的地方,如果发现有错误或者不足的地方,也希望大家能够指点一二
最后的最后,厚颜无耻地求一个STAR:yum:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 「Flask实战」鱼书项目实战一
- 「Flask实战」鱼书项目实战三
- 「Flask实战」鱼书项目实战四
- 「Flask实战」鱼书项目实战六
- RocketMQ实战系列从理论到实战
- 「Flask实战」flask鱼书项目实战二
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。