欢迎继续阅读《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仓库加星❤️哦