前言
学习 React 不久, 觉得实战才是检验自己学习程度的最好方法, 也顺便加深一下自己对 React 的理解, 于是做了这么一个小项目分享一下.
技术栈
- react
- react-router
- react-redux
- Less
基本项目搭建
node 开发环境
安装依赖: yarn
项目启动: yarn start
涉及到第三方 API 接口, 小伙伴们可以自己去接口地址 https://www.jisuapi.com/api/recipe/ 申请一个 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>
结语
项目传送门 https://github.com/MrsignzZ/react-kitchen
写项目的时候也遇到了许多小问题, 都是慢慢查文档一个一个解决的, 不断的思考然后解决问题也是成长的一部分.
当然, 项目还有许多需要完善的地方, 如果发现有错误或者不足的地方, 也希望大家能够指点一二
最后的最后, 厚颜无耻地求一个 STAR
来源: https://juejin.im/post/5c7f8c426fb9a049bd43108d