歡迎繼續閱讀《Taro 小程序開發大型實戰》系列,前情回顧:css
user
邏輯的狀態管理重構這是使用 Hooks 版的 Redux 重構狀態管理的下篇,在上篇中咱們實現了 user
部分 的狀態管理的重構,但受限於篇幅,咱們還剩下 Footer
組件部分沒有重構,在這一篇中,咱們將首先實現 Footer
組件的狀態管理的重構,接着咱們立刻來實現 post
邏輯的狀態管理的重構。前端
若是你不熟悉 Redux,推薦閱讀咱們的《Redux 包教包會》系列教程:git
本文所涉及的源代碼都放在了 Github 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+Github倉庫加星❤️哦~
原本這個小標題我是不想起的,可是由於,是吧,你們上面在沒有小標題的狀況下看了這麼久,可能已經廢(累)了,因此我就貼心的加上一個小標題,幫助你定位接下來說解的重心。github
是的接下來,咱們要重構 「個人" tab 頁面中的下半部分組件 src/components/Footer/index.js
咱們遵循自頂向下的方式來重構,首先是 src/components/Logout/index.js
文件,咱們打開這個文件,對其中內容做出以下修改:redux
import Taro, { useState } from '@tarojs/taro' import { AtButton } from 'taro-ui' import { useDispatch } from '@tarojs/redux' import { SET_LOGIN_INFO } from '../../constants' export default function LoginButton(props) { const [isLogout, setIsLogout] = useState(false) const dispatch = useDispatch() async function handleLogout() { setIsLogout(true) try { await Taro.removeStorage({ key: 'userInfo' }) dispatch({ type: SET_LOGIN_INFO, payload: { avatar: '', nickName: '', }, }) } catch (err) { console.log('removeStorage ERR: ', err) } setIsLogout(false) } return ( <AtButton type="secondary" full loading={isLogout} onClick={handleLogout}> 退出登陸 </AtButton> ) }
這一步多是最能體現引入 Redux 進行狀態管理帶來好處的一步了 -- 咱們將以前至上而下的 React 狀態管理邏輯壓平,使得底層組件能夠在自身中就解決響應的狀態和邏輯問題。小程序
能夠看到,咱們上面的文件中主要有五處改動:segmentfault
@tarojs/taro
裏面導出 useState
Hooks。src/pages/mine/mine.js
中定義的 isLogout
狀態移動到組件 Logout
組件內部來,由於它只和此組件有關係。isLogout
替換在 AtButton
裏面用到的 props.loading
屬性。props.handleLogout
Redux 化,咱們將這個點擊以後的回調函數 handleLogout
在組件內部定義。@tarojs/redux
中導入 useDispatch
Hooks,並在組件中調用成咱們須要的 dispatch
函數,接着咱們在 handleLogout
函數中去 dispatch 一個 SET_LOGIN_INFO
action 來重置 Store 中的 nickName
和 avatar
屬性。提示這裏咱們在組件內定義的
handleLogout
函數和咱們以前在src/pages/mine/mine.js
中定義的相似,只是使用 dispatch action 的方式替換了重置nickName
和avatar
的部分。數組
搞定完 Logout
組件,接着就是 LoginForm
組件的重構了,讓咱們馬不停蹄,讓它也接受 Redux 光環的洗禮吧!瀏覽器
打開 src/components/LoginForm/index.jsx
,對其中的內容做出相應的修改以下:緩存
import Taro, { useState } from '@tarojs/taro' import { View, Form } from '@tarojs/components' import { AtButton, AtImagePicker } from 'taro-ui' import { useDispatch } from '@tarojs/redux' import { SET_LOGIN_INFO, SET_IS_OPENED } from '../../constants' import './index.scss' export default function LoginForm(props) { // Login Form 登陸數據 const [formNickName, setFormNickName] = useState('') const [files, setFiles] = useState([]) const [showAddBtn, setShowAddBtn] = useState(true) const dispatch = useDispatch() function onChange(files) { if (files.length > 0) { setShowAddBtn(false) } else { setShowAddBtn(true) } setFiles(files) } function onImageClick() { Taro.previewImage({ urls: [props.files[0].url], }) } async function handleSubmit(e) { e.preventDefault() // 鑑權數據 if (!formNickName || !files.length) { Taro.atMessage({ type: 'error', message: '您還有內容沒有填寫!', }) return } setShowAddBtn(true) // 提示登陸成功 Taro.atMessage({ type: 'success', message: '恭喜您,登陸成功!', }) // 緩存在 storage 裏面 const userInfo = { avatar: files[0].url, nickName: formNickName } // 清空表單狀態 setFiles([]) setFormNickName('') // 緩存在 storage 裏面 await Taro.setStorage({ key: 'userInfo', data: userInfo }) dispatch({ type: SET_LOGIN_INFO, payload: userInfo }) // 關閉彈出層 dispatch({ type: SET_IS_OPENED, payload: { isOpened: false } }) } return ( <View className="post-form"> <Form onSubmit={handleSubmit}> <View className="login-box"> <View className="avatar-selector"> <AtImagePicker length={1} mode="scaleToFill" count={1} files={files} showAddBtn={showAddBtn} onImageClick={onImageClick} onChange={onChange} /> </View> <Input className="input-nickName" type="text" placeholder="點擊輸入暱稱" value={formNickName} onInput={e => setFormNickName(e.target.value)} /> <AtButton formType="submit" type="primary"> 登陸 </AtButton> </View> </Form> </View> ) }
這一步和上一步相似,可能也是最能體現引入 Redux 進行狀態管理帶來好處的一步了,咱們一樣將以前在頂層組件中提供的狀態壓平到了底層組件內部。
能夠看到,咱們上面的文件中主要有四處改動:
formNickName
和 files
等狀態放置到 LoginForm
組件內部,並使用 useState
Hooks 管理起來,由於它們只和此組件有關係。AtImagePicker
裏面的 props.files
替換成 files
,將它的 onChange
回調函數內部的設置改變狀態的 props.handleFilesSelect(files)
替換成 setFiles(files)
。能夠看到這裏咱們還對 files.length = 0
的形式作了一個判斷,當沒有選擇圖片時,要把咱們選擇圖片的按鈕顯示出來。Input
組件的 props.formNickName
替換成 formNickName
,將以前 onInput
接收的回調函數換成了 setFormNickName
的形式來設置 formNickName
的變化。接着,咱們將以前提交表單須要調用的父組件方法 props.handleSubmit
移動到組件內部來定義,能夠看到,這個 hanldeSubmit
組合了以前在 src/components/Footer/index.jsx
和 src/pages/mine/mine.js
組件裏的 handleSubmit
邏輯:
e.preventDefault
禁止瀏覽器默認行爲。warning
,當時寫代碼時石樂志😅)。LoginForm
表單數據要被清除,因此咱們將選中圖片的按鈕又設置爲可顯示狀態。將登陸數據緩存在 storage
裏面,在 Taro 裏面使用 Taro.setStorage({ key, data })
的形式來緩存,其中 key
是字符串,data
是字符串或者對象。
useDispatch
Hooks,使用 useDispatch
Hooks 生成的 dispatch
函數的引用來發起更新 Redux store 的 action 來更新本地數據,type
爲 SET_LOGIN_INFO
的 action 用來更新用戶登陸信息,type
爲 SET_IS_OPENED
的 action 用來更新 isOpened
屬性,它將關閉展現登陸框的彈出層 FloatLayout
組件。講到這裏,咱們的 Footer
部分的重構大業還剩下臨門一腳了。讓咱們打開 src/components/Footer/index.js
文件,立馬來重構它:
import Taro from '@tarojs/taro' import { View } from '@tarojs/components' import { AtFloatLayout } from 'taro-ui' import { useSelector, useDispatch } from '@tarojs/redux' import Logout from '../Logout' import LoginForm from '../LoginForm' import './index.scss' import { SET_IS_OPENED } from '../../constants' export default function Footer(props) { const nickName = useSelector(state => state.user.nickName) const dispatch = useDispatch() // 雙取反來構造字符串對應的布爾值,用於標誌此時是否用戶已經登陸 const isLogged = !!nickName // 使用 useSelector Hooks 獲取 Redux Store 數據 const isOpened = useSelector(state => state.user.isOpened) return ( <View className="mine-footer"> {isLogged && <Logout />} <View className="tuture-motto"> {isLogged ? 'From 圖雀社區 with Love ❤' : '您還未登陸'} </View> <AtFloatLayout isOpened={isOpened} title="登陸" onClose={() => dispatch({ type: SET_IS_OPENED, payload: { isOpened: false } }) } > <LoginForm /> </AtFloatLayout> </View> ) }
能夠看到上面的代碼主要有五處改動:
nickName
抽取到 Redux store 保存的狀態中,因此以前從父組件獲取的 props.isLogged
判斷是否登陸的信息,咱們移動到組件內部來,使用 useSelector
Hooks 從 Redux store 從獲取 nickName
屬性,進行雙取反操做成布爾值來表示是否已經登陸的 isLogged
屬性,並使用它來替換以前的 props.isLogged
屬性。props.isOpened
屬性,咱們使用 useSelector
Hooks 從 Redux store 中獲取對應的 isOpened
屬性,而後替換以前的 props.isOpened
,用戶控制登陸框窗口的彈出層 AtFloatLayout
的打開和關閉。AtFloatLayout
關閉時(onClose
)的回調函數替換成 dispatch 一個 type
爲 SET_IS_OPENED
的 action 來設置 isOpened
屬性將 AtFloatLayout
關閉。Logout
和 LoginForm
組件上再也不須要傳遞的屬性,由於在對應的組件中咱們已經聲明瞭對應的屬性了。Footer
組件內的 formNickName
和 files
等狀態,以及再也不須要的 handleSubmit
函數,由於它已經在 LoginForm
裏面定義了。熟悉套路的同窗可能都知道起這個標題的含義了吧 😏。
咱們一路打怪重構到這裏,相比眼尖的人已經摸清楚 Redux 的套路了,結合 Redux 來寫 React 代碼,就比如 「千里之堤,始於壘土」 通常,咱們先把全部細小的分支組件搞定,進而一步一步向頂層組件進發,以完成全部組件的編寫。
而這個 src/pages/mine/mine.jsx
組件就是 「個人」 這一 tab 頁面的頂層組件了,也是咱們在 「個人」 頁面須要重構的最後一個頁面了,是的,咱們立刻就要達到第一階段性勝利了✌️。如今就打開這個文件,對其中的內容做出以下的修改:
import Taro, { useEffect } from '@tarojs/taro' import { View } from '@tarojs/components' import { useDispatch } from '@tarojs/redux' import { Header, Footer } from '../../components' import './mine.scss' import { SET_LOGIN_INFO } from '../../constants' export default function Mine() { const dispatch = useDispatch() useEffect(() => { async function getStorage() { try { const { data } = await Taro.getStorage({ key: 'userInfo' }) const { nickName, avatar } = data // 更新 Redux Store 數據 dispatch({ type: SET_LOGIN_INFO, payload: { nickName, avatar } }) } catch (err) { console.log('getStorage ERR: ', err) } } getStorage() }) return ( <View className="mine"> <Header /> <Footer /> </View> ) } Mine.config = { navigationBarTitleText: '個人', }
能夠看到,上面的代碼作了一下五處改動:
useDispatch
Hooks 和 SET_LOGIN_INFO
常量,並把以前在 getStorage
方法裏面設置 nickName
和 avatar
的操做替換成了 dispatch 一個 type
爲 SET_LOGIN_INFO
的 action。formNickName
、files
、isLogout
、isOpened
狀態,以及 setLoginInfo
、handleLogout
、handleSetIsOpened
、handleClick
、handleSubmit
方法。Header
和 Footer
組件上再也不不須要的屬性。大功告成🥈!這裏給你頒發一個銀牌,以獎勵你能一直堅持閱讀並跟到這裏,咱們這一篇教程很長很長,能跟下來的都不容易,但願你能在內心或用實際行動給本身鼓鼓掌👏。
小憩一下,恢復精力,整裝待發!不少同窗可能很好奇了,爲何還只能拿一個銀牌呢?那是由於咱們的重構進程才走了一半呀✌️,可是不要擔憂,咱們全部新的東西都已經講完了,接下來就只是一些收尾工做了,當你能堅持到終點的時候,會有驚喜等着你哦!加油吧騷年💪。
咱們依然按照以前的套路,從最底層的組件開始重構,首先是咱們的登陸框彈出層 LoginForm
組件,讓咱們打開 src/components/PostForm/index.jsx
文件,對其中的內容做出相應的修改以下:
import Taro, { useState } from '@tarojs/taro' import { View, Form, Input, Textarea } from '@tarojs/components' import { AtButton } from 'taro-ui' import { useDispatch, useSelector } from '@tarojs/redux' import './index.scss' import { SET_POSTS, SET_POST_FORM_IS_OPENED } from '../../constants' export default function PostForm(props) { const [formTitle, setFormTitle] = useState('') const [formContent, setFormContent] = useState('') const nickName = useSelector(state => state.user.nickName) const avatar = useSelector(state => state.user.avatar) const dispatch = useDispatch() async function handleSubmit(e) { e.preventDefault() if (!formTitle || !formContent) { Taro.atMessage({ message: '您還有內容沒有填寫完哦', type: 'warning', }) return } dispatch({ type: SET_POSTS, payload: { post: { title: formTitle, content: formContent, user: { nickName, avatar }, }, }, }) setFormTitle('') setFormContent('') dispatch({ type: SET_POST_FORM_IS_OPENED, payload: { isOpened: false }, }) Taro.atMessage({ message: '發表文章成功', type: 'success', }) } return ( <View className="post-form"> <Form onSubmit={handleSubmit}> <View> <View className="form-hint">標題</View> <Input className="input-title" type="text" placeholder="點擊輸入標題" value={formTitle} onInput={e => setFormTitle(e.target.value)} /> <View className="form-hint">正文</View> <Textarea placeholder="點擊輸入正文" className="input-content" value={formContent} onInput={e => setFormContent(e.target.value)} /> <AtButton formType="submit" type="primary"> 提交 </AtButton> </View> </Form> </View> ) }
這個文件的形式和咱們以前的 src/components/LoginForm/index.jsx
文件相似,能夠看到,咱們上面的文件中主要有四處改動:
formTitle
和 formContent
等狀態放置到 PostForm
組件內部,並使用 useState
Hooks 管理起來,由於它們只和此組件有關係。Input
裏面的 props.formTitle
替換成 formTitle
,將它的 onInput
回調函數內部的設置改變狀態的 props. handleTitleInput
替換成 setFormTitle(e.target.value)
的回調函數。Textarea
組件的 props. formContent
替換成 formContent
,將以前 onInput
接收的回調函數換成了 setFormContent
的形式來設置 formContent
的變化。最後,咱們將以前提交表單須要調用的父組件方法 props.handleSubmit
移動到組件內部來定義,能夠看到,這個 hanldeSubmit
和咱們以前定義在 src/pages/index/index.js
組件裏的 handleSubmit
邏輯相似:
e.preventDefault
禁止瀏覽器默認行爲。type
爲 SET_POSTS
的 action,將新發表的 post 添加到 Redux store 對應的 posts
數組中。咱們注意到這裏咱們使用 useSelector
Hooks 從 Redux store 裏面獲取了 nickName
和 avatar
屬性,並把它們組合到 post.user
屬性裏,隨着 action 的 payload 一塊兒被 dispatch,咱們用這個 user
屬性標誌發帖的用戶屬性。type
爲 SET_POST_FORM_IS_OPENED
的 action 用來更新 isOpened
屬性,它將關閉展現發表帖子的表單彈出層 FloatLayout
組件。接着是咱們 「首頁」 頁面組件另一個底層子組件 PostCard
,它主要用於展現一個帖子,讓咱們 src/components/PostCard/index.jsx
文件,對其中的內容做出對應的修改以下:
import Taro from '@tarojs/taro' import { View } from '@tarojs/components' import classNames from 'classnames' import { AtAvatar } from 'taro-ui' import './index.scss' export default function PostCard(props) { // 注意: const { title = '', content = '', user } = props.post const { avatar, nickName } = user || {} const handleClick = () => { // 若是是列表,那麼就響應點擊事件,跳轉到帖子詳情 if (props.isList) { Taro.navigateTo({ url: `/pages/post/post?postId=${props.postId}`, }) } } const slicedContent = props.isList && content.length > 66 ? `${content.slice(0, 66)} ...` : content return ( <View className={classNames('at-article', { postcard__isList: props.isList })} onClick={handleClick} > <View className="post-header"> <View className="at-article__h1">{title}</View> <View className="profile-box"> <AtAvatar circle size="small" image={avatar} /> <View className="at-article__info post-nickName">{nickName}</View> </View> </View> <View className="at-article__content"> <View className="at-article__section"> <View className="at-article__p">{slicedContent}</View> </View> </View> </View> ) } PostCard.defaultProps = { isList: '', post: [], }
能夠看到這個組件基本不保有本身的狀態,它接收來自父組件的狀態,咱們對它的修改主要有下面五個部分:
props.title
和 props.content
放到了 props.post
屬性中,咱們從 props.post
屬性中導出咱們須要展現的 title
和 content
,還要一個額外的 user
屬性,它應該是一個對象,保存着發帖人的用戶屬性,咱們使用解構的方法獲取 user.avatar
和 user.nickName
的值。return
的組件結構發生了很大的變化,這裏咱們爲了方便,使用了 taro-ui
提供給咱們的 Article
文章樣式組件,用於展現相似微信公衆號文章頁的一些樣式,可供用戶快速呈現文章內容,能夠詳情能夠查看 taro-ui 連接,有了 taro-ui
加持,咱們就額外的展現了發表此文章的用戶頭像(avatar
)和暱稱(nickName
)。content
作了一點修改,當 PostCard
組件在文章列表中被引用的時候,咱們對內容長度進行截斷,當超過 66 字符時,咱們就截斷它,並加上省略號 ...
。handleClick
方法,以前是在跳轉路由的頁面路徑裏直接帶上查詢參數 title
和 content
,當咱們要傳遞的內容多了,這個路徑就會顯得很臃腫,因此這裏咱們傳遞此文章對應的 id
,這樣能夠經過此 id
取到完整的 post
數據,使路徑保持簡潔,這也是最佳實踐的推薦作法。接着咱們補充一下在 PostCard
組件裏面會用到的樣式,打開 src/components/PostCard/index.scss
文件,補充和改進對應的樣式以下:
@import '~taro-ui/dist/style/components/article.scss'; .postcard { margin: 30px; padding: 20px; } .postcard__isList { border-bottom: 1px solid #ddd; padding-bottom: 20px; } .post-header { display: flex; flex-direction: column; align-items: center; } .profile-box { display: flex; flex-direction: row; align-items: center; } .post-nickName { color: #777; }
能夠看到咱們更新了一些樣式,而後引入了 taro-ui
提供給咱們的 article
文章樣式。
重構完 「首頁」 頁面組件的全部底層組件,咱們開始完成最終的頂層組件,打開 src/pages/index/index.jsx
文件,對相應的內容修改以下:
import Taro, { useEffect } from '@tarojs/taro' import { View, Text } from '@tarojs/components' import { AtFab, AtFloatLayout, AtMessage } from 'taro-ui' import { useSelector, useDispatch } from '@tarojs/redux' import { PostCard, PostForm } from '../../components' import './index.scss' import { SET_POST_FORM_IS_OPENED, SET_LOGIN_INFO } from '../../constants' export default function Index() { const posts = useSelector(state => state.post.posts) || [] const isOpened = useSelector(state => state.post.isOpened) const nickName = useSelector(state => state.user.nickName) const isLogged = !!nickName const dispatch = useDispatch() useEffect(() => { async function getStorage() { try { const { data } = await Taro.getStorage({ key: 'userInfo' }) const { nickName, avatar } = data // 更新 Redux Store 數據 dispatch({ type: SET_LOGIN_INFO, payload: { nickName, avatar } }) } catch (err) { console.log('getStorage ERR: ', err) } } getStorage() }) function setIsOpened(isOpened) { dispatch({ type: SET_POST_FORM_IS_OPENED, payload: { isOpened } }) } function handleClickEdit() { if (!isLogged) { Taro.atMessage({ type: 'warning', message: '您還未登陸哦!', }) } else { setIsOpened(true) } } console.log('posts', posts) return ( <View className="index"> <AtMessage /> {posts.map((post, index) => ( <PostCard key={index} postId={index} post={post} isList /> ))} <AtFloatLayout isOpened={isOpened} title="發表新文章" onClose={() => setIsOpened(false)} > <PostForm /> </AtFloatLayout> <View className="post-button"> <AtFab onClick={handleClickEdit}> <Text className="at-fab__icon at-icon at-icon-edit"></Text> </AtFab> </View> </View> ) } Index.config = { navigationBarTitleText: '首頁', }
能夠看到咱們上面的內容有如下五處改動:
useSelector
鉤子,而後從 Redux store 中獲取了 posts
、isOpened
和 nickName
等屬性。PostCard
組件上的屬性進行了一次換血,以前是直接傳遞 title
和 content
屬性,如今咱們傳遞整個 post
屬性,而且額外傳遞了一個 postId
屬性,用於在 PostCard
裏面點擊跳轉路由時進行標註。PostForm
組件上面的全部屬性,由於咱們已經在組件內部定義了它們。useEffect
Hooks,在裏面定義並調用了 getStorage
方法,獲取了咱們保存在 storage
裏面的用戶登陸信息,若是用戶登陸了,咱們 dispatch 一個 type
爲 SET_LOGIN_INFO
的 action,將這份登陸信息保存在 Redux store 裏面以供後續使用。AtFab
的 onClick
回調函數替換成 handleClickEdit
,在其中對用戶點擊進行判斷,若是用戶未登陸,那麼彈出警告,告知用戶,若是用戶已經登陸,那麼就 dispatch 一個 type
爲 SET_POST_FORM_IS_OPENED
的 action 去設置 isOpened
屬性,打開發帖的彈出層,容許用戶進行發帖操做。最後,讓咱們堅持一下,跑贏重構工做的最後一千米💪!完成 「文章詳情」 頁的重構。
讓咱們打開 src/pages/post/post.jsx
文件,對其中的內容做出相應的修改以下:
import Taro, { useRouter } from '@tarojs/taro' import { View } from '@tarojs/components' import { useSelector } from '@tarojs/redux' import { PostCard } from '../../components' import './post.scss' export default function Post() { const router = useRouter() const { postId } = router.params const posts = useSelector(state => state.post.posts) const post = posts[postId] console.log('posts', posts, postId) return ( <View className="post"> <PostCard post={post} /> </View> ) } Post.config = { navigationBarTitleText: '帖子詳情', }
能夠看到,上面的文件作了如下四處修改:
router.params
中導出了 postId
,由於以前咱們在 PostCard
裏面點擊跳轉的路徑參數使用了 postId
。useSelector
Hooks 獲取了保存在 Redux store 中的 posts
屬性,而後使用上一步獲取到的 postId
,來獲取咱們最終要渲染的 post
屬性。PostCard
的屬性改爲上一步獲取到的 post
。注意這裏的
console.log
是調試時使用的,生產環境中建議刪掉。
能夠看到,在未登陸狀態下,會提示請登陸:
在已登陸的狀況下,發帖子會顯示當前登陸用戶的頭像和暱稱:
有幸!到這裏,咱們 Redux 重構之旅的萬里長征就跑完了!讓咱們來回顧一下咱們在這一小節中學到了那些東西。
post
和 user
;接着咱們將將 Redux 和 React 整合起來;由於 Action 是從組件中 dispatch 出來了,因此咱們接下來就開始了組件的重構之旅。LoggedMine
組件,再往上就是 Header
組件;重構完 Header
組件以後,咱們接着從 Footer
組件的底層組件 Logout
組件開始重構,而後重構了 LoginForm
組件,最後是 Footer
組件,重構完 Header
和 Footer
組件,咱們開始重構其上層組件 mine
頁面組件,自此咱們就完成了 「個人」 頁面的重構。PostForm
組件開始,接着是 PostCard
組件,最後再回到頂層組件 index
首頁頁面組件。在重構 「帖子詳情」 頁面組件時,由於其底層組件 PostCard
已經重構過了,因此咱們就直接重構了 post
帖子詳情頁面組件。
能跟着這麼長的文章堅持到這裏,我想給你鼓個掌,也但願你能給本身鼓個掌,我想,我能夠很是確定且自豪的頒佈給你第一名的獎章了🥇。
終於,這漫長的第五篇結束了。在接下來的文章中,咱們將接觸小程序雲後臺開發,並在前端接入後臺數據。
想要學習更多精彩的實戰技術教程?來 圖雀社區逛逛吧。本文所涉及的源代碼都放在了 Github 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+Github倉庫加星❤️哦