前言
學習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?