欢迎继续阅读《Taro 小程序开发大型实战》系列,前情回顾:
user 逻辑的状态管理重构这是使用 Hooks 版的 Redux 重构状态管理的下篇,在上篇中我们实现了 user 部分 的状态管理的重构,但受限于篇幅,我们还剩下 Footer 组件部分没有重构,在这一篇中,我们将首先实现 Footer 组件的状态管理的重构,接着我们马上来实现 post 逻辑的状态管理的重构。
如果你不熟悉 Redux,推荐阅读我们的《Redux 包教包会》系列教程:
本文所涉及的源代码都放在了 Github[8] 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点个在看+Github仓库加星❤️哦~
本来这个小标题我是不想起的,但是因为,是吧,大家上面在没有小标题的情况下看了这么久,可能已经废(累)了,所以我就贴心的加上一个小标题,帮助你定位接下来讲解的重心。
是的接下来,我们要重构 “我的" tab 页面中的下半部分组件 src/components/Footer/index.js 我们遵循自顶向下的方式来重构,首先是 src/components/Logout/index.js 文件,我们打开这个文件,对其中内容作出如下修改:
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 状态管理逻辑压平,使得底层组件可以在自身中就解决响应的状态和逻辑问题。
可以看到,我们上面的文件中主要有五处改动:
@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 链接[9],有了 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 帖子详情页面组件。
能跟着这么长的文章坚持到这里,我想给你鼓个掌,也希望你能给自己鼓个掌,我想,我可以非常肯定且自豪的颁布给你第一名的奖章了?。
终于,这漫长的第五篇结束了。在接下来的文章中,我们将接触小程序云后台开发,并在前端接入后台数据。
想要学习更多精彩的实战技术教程?来图雀社区[10]逛逛吧。
本文所涉及的源代码都放在了 Github[11] 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点个在看+Github仓库加星❤️哦
[1]
熟悉的 React,熟悉的 Hooks: https://juejin.im/post/5e046c4fe51d45584221e508
[2]
多页面跳转和 Taro UI 组件库: https://juejin.im/post/5e0891b66fb9a0165936fb0b
[3]
实现微信和支付宝多端登录: https://juejin.im/post/5e10118be51d454165777203
[4]
使用 Hooks 版的 Redux 实现大型应用状态管理(上篇): https://juejin.im/post/5e100f78e51d4541493621cd
[5]
Redux 包教包会(一):解救 React 状态危机: https://juejin.im/post/5df62cd8e51d4558270ef5ca
[6]
Redux 包教包会(二):趁热打铁,完全重构: https://juejin.im/post/5df7b11c51882512664b1068
[7]
Redux 包教包会(三):各司其职,重拾初心: https://juejin.im/post/5e0fe9705188253ab044c869
[8]
Github: https://github.com/tuture-dev/ultra-club
[9]
taro-ui 链接: https://taro-ui.jd.com/#/docs/article
[10]
图雀社区: https://tuture.co?utm_source=juejin_zhuanlan
[11]
Github: https://github.com/tuture-dev/ultra-club
● 一杯茶的时间,上手React框架开发● Taro小程序开发大型实战(一):熟悉的React,熟悉的Hooks● Taro小程序开发大型实战(二):多页面跳转和TaroUI组件库
·END·