欢迎继续阅读《Taro 小程序开发大型实战》系列,前情回顾:
- 熟悉的 React,熟悉的 Hooks[1]:我们用 React 和 Hooks 实现了一个非常简单的添加帖子的原型
- 多页面跳转和 Taro UI 组件库[2]:我们用 Taro 自带的路由功能实现了多页面跳转,并用 Taro UI 组件库升级了应用界面
- 实现微信和支付宝多端登录[3]:实现了微信、支付宝以及普通登录和退出登录
- 使用 Hooks 版的 Redux 实现大型应用状态管理(上篇)[4]:使用 Hooks 版的 Redux 实现了
user
逻辑的状态管理重构 - 使用 Hooks 版的 Redux 实现大型应用状态管理(下篇)[5]:使用 Hooks 版的 Redux 实现了
post
逻辑的状态管理重构 - Taro 小程序开发大型实战(六):尝鲜微信小程序云(上篇)[6]:
user
逻辑接入微信小程序云
在上一篇文章中,我们将我们两大逻辑之一 User 部分接入了 Redux 异步处理流程,接着接入了微信小程序云,使得 User 逻辑可以在云端永久保存,好不自在:),两兄弟一个得了好处,另外一个不能干瞪眼对吧?在这一篇教程中,我们想办法把 User 另外一个兄弟 Post 捞上来,也把 Redux 异步流程和微信小程序给它整上,这样就齐活了?。
我们首先来看一看最终的完成效果:
如果你不熟悉 Redux,推荐阅读我们的《Redux 包教包会》系列教程:
- Redux 包教包会(一):解救 React 状态危机[7]
- Redux 包教包会(二):趁热打铁,完全重构[8]
- Redux 包教包会(三):各司其职,重拾初心[9]
如果你希望直接从这一步开始,请运行以下命令:
代码语言:javascript复制git clone -b miniprogram-start https://github.com/tuture-dev/ultra-club.git
cd ultra-club
本文所涉及的源代码都放在了 Github[10] 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点个在看 Github仓库加星❤️哦~
此教程属于 React 前端工程师学习路线[11]的一部分,欢迎来 Star 一波,鼓励我们继续创作出更好的教程,持续更新中~
“六脉神剑” 搞定 createPost 异步逻辑
不知道看到这里的读者有没有发现上篇文章其实打造了一套讲解模式,即按照如下的 “六步流程” 来讲解,我们也称为 “六脉神剑” 讲解法:
- 将组件中的同步逻辑重构到异步逻辑
- 声明和补充对应需要的异步
sagas
文件 - 定义
sagas
需要的常量文件 - 定义
sagas
涉及到的前端 API 文件 - 创建对于的微信小程序云函数,并编写对应的 Node.js 处理逻辑
- 定义对应的
reducers
文件 - 如此往复
可以看到我们上面的讲解顺序实际上是按照前端数据流的流动来进行的,我们对标上面的讲解逻辑来看一下前端数据流是如何流动的:
- 从组件中通过对应的常量发起异步请求
sagas
监听到对应的异步请求,开始处理流程- 在
sagas
调用对应的前端 API 文件向微信小程序云发起请求 - 微信小程序云函数处理对应的 API 请求,返回数据
sagas
中获取到对应的数据,dispatch
action 到对应的reducers
处理逻辑reducers
接收数据,开始更新本地 Redux Store 中的state
- 组件中重新渲染
好的,了解了讲解逻辑和对应前端数据流动逻辑之后,我们马上来实践这套逻辑,把 User 逻辑的好兄弟 Post 逻辑搞定。
第一剑:PostForm
组件中发起异步请求
首先从创建帖子逻辑动刀子,我们将创建帖子接入异步逻辑并接通小程序云,让文章上云。打开 src/components/PostForm/index.jsx
,对其中的内容作出对应的修改如下:
import { useDispatch, useSelector } from '@tarojs/redux'
import './index.scss'
import { CREATE_POST } from '../../constants'
export default function PostForm() {
const [formTitle, setFormTitle] = useState('')
const [formContent, setFormContent] = useState('')
const userId = useSelector(state => state.user.userId)
const dispatch = useDispatch()
... }
dispatch({
type: CREATE_POST,
payload: {
postData: {
title: formTitle,
content: formContent,
},
userId,
},
})
setFormTitle('')
setFormContent('')
}
return (
可以看到,上面的内容做了如下四处修改:
- 首先我们现在是接收用户的文章输入数据然后向小程序云发起创建文章的请求,所以我们将之前的
dispatch
SET_POSTS Action 改为 CREATE_POST Action,并且将之前的 action payload 简化为postData
和userId
,因为我们可以通过小程序云数据库查询userId
得到创建文章的用户信息,所以不需要再携带用户的数据。 - 接着,因为我们不再需要用户的
avatar
和nickName
数据,所以我们删掉了对应的useSelector
语句。 - 接着,因为请求是异步的,所以需要等待请求完成之后再设置对应的发表文章的状态以及发表文章弹出层状态,所以我们删掉了对应的
dispatch
SET_POST_FORM_IS_OPENED Action 逻辑以及Taro.atMessage
逻辑。 - 最后我们删掉不需要的常量
SET_POSTS
和SET_POST_FORM_IS_OPENED
,然后导入异步创建文章的常量CREATE_POST
。
增加 Action 常量
我们在上一步中使用到了 CREATE_POST
常量,打开 src/constants/post.js
,在其中增加 CREATE_POST
常量:
export const CREATE_POST = 'CREATE_POST'
到这里,我们的 “六步流程” 讲解法就走完了第一步,即从组件中发起对应的异步请求,这里我们是发出的 action.type
为 CREATE_POST
的异步请求。
第二剑:声明和补充对应需要的异步 sagas
文件
在 “第一剑” 中,我们从组件中 dispatch 了 action.type 为 CREATE_POST
的异步 Action,接下来我们要做的就是在对应的 sagas
文件中补齐响应这个异步 action 的 sagas。
在 src/sagas/
文件夹下面创建 post.js
文件,并在其中编写如下创建文章的逻辑:
import Taro from '@tarojs/taro'
import { call, put, take, fork } from 'redux-saga/effects'
import { postApi } from '../api'
import {
CREATE_POST,
POST_SUCCESS,
POST_ERROR,
SET_POSTS,
SET_POST_FORM_IS_OPENED,
} from '../constants'
function* createPost(postData, userId) {
try {
const post = yield call(postApi.createPost, postData, userId)
// 其实以下三步可以合成一步,但是这里为了讲解清晰,将它们拆分成独立的单元
// 发起发帖成功的 action
yield put({ type: POST_SUCCESS })
// 关闭发帖框弹出层
yield put({ type: SET_POST_FORM_IS_OPENED, payload: { isOpened: false } })
// 更新 Redux store 数据
yield put({
type: SET_POSTS,
payload: {
posts: [post],
},
})
// 提示发帖成功
Taro.atMessage({
message: '发表文章成功',
type: 'success',
})
} catch (err) {
console.log('createPost ERR: ', err)
// 发帖失败,发起失败的 action
yield put({ type: POST_ERROR })
// 提示发帖失败
Taro.atMessage({
message: '发表文章失败',
type: 'error',
})
}
}
function* watchCreatePost() {
while (true) {
const { payload } = yield take(CREATE_POST)
console.log('payload', payload)
yield fork(createPost, payload.postData, payload.userId)
}
}
export { watchCreatePost }
可以看到,上面的改动主要是创建 watcherSaga
和 handlerSaga
。
创建 watcherSaga
- 我们创建了登录的
watcherSaga
:watchCreatePost
,它用来监听action.type
为CREATE_POST
的 action,并且当监听到CREATE_POST
action 之后,从这个 action 中获取必要的postData
和userId
数据,然后激活handlerSaga
:createPost
去处理对应的创建帖子的逻辑。 - 这里的
watcherSaga
:watchCreatePost
是一个生成器函数,它内部是一个while
无限循环,表示在内部持续监听CREATE_POST
action。 - 在循环内部,我们使用了
redux-saga
提供的effects helper
函数:take
,它用于监听CREATE_POST
action,获取 action 中携带的数据。 - 接着我们使用了另外一个
effects helper
函数:fork
,它表示非阻塞的执行handlerSaga
:createPost
,并将payload.postData
和payload.userId
作为参数传给createPost
。
创建 handlerSaga
- 我们创建了创建帖子的
handlerSaga
:createPost
,它用来处理创建逻辑。 createPost
也是一个生成器函数,在它内部是一个try/catch
语句,用于处理创建帖子请求可能存在的错误情况。- 在
try
语句中,首先是使用了redux-saga
提供给我们的effects helper
函数:call
来调用登录的 API:postApi.createPost
,并把postData
和userId
作为参数传给这个 API。- 如果创建帖子成功,我们使用
redux-saga
提供的effects helpers
函数:put
,put
类似之前在view
中的dispatch
操作,,来dispatch
了三个 action:POST_SUCCESS
,SET_POST_FORM_IS_OPENED
,SET_POSTS
,代表更新创建帖子成功的状态,关闭发帖框,设置最新创建的帖子信息到 Redux Store 中。 - 最后我们使用了 Taro UI 提供给我们的消息框,来显示一个
success
消息。
- 如果创建帖子成功,我们使用
- 如果发帖失败,我们则使用
put
发起一个POST_ERROR
的 action 来更新创建帖子失败的信息到 Redux Store,接着使用了 Taro UI 提供给我们的消息框,来显示一个error
消息。
一些额外的工作
为了创建 watcherSaga
和 handlerSaga
,我们还导入了 postApi
,我们将在后面来创建这个 API。
除此之外我们还导入了需要使用的 action 常量:
POST_SUCCESS
:设置处理帖子逻辑成功信息POST_ERROR
:设置处理帖子逻辑失败信息SET_POSTS
:将新帖子添加到 Redux StoreCREATE_POST
:相应创建帖子的常量SET_POST_FORM_IS_OPENED
:更新发帖框的开闭逻辑
这里的 POST_SUCCESS
和 POST_ERROR
我们还没有创建,我们将马上在 “下一剑” 中创建它。
以及一些 redux-saga/effects
相关的 helper 函数,我们已经在之前的内容中详细讲过了,这里就不再赘述了。
加入 saga 中心调度文件
我们像之前将 watchLogin
等加入到 sagas
中心调度文件一样,将我们创建好的 watchCreatePost
也加入进去:
// ...之前的逻辑
import { watchCreatePost } from './post'
export default function* rootSaga() {
yield all([
// ... 之前的逻辑
fork(watchCreatePost)
])
}
第三剑:定义 sagas
需要的常量文件
打开 src/constants/post.js
文件,定义我们之前创建的常量文件如下:
export const POST_SUCCESS = 'POST_SUCCESS'
export const POST_ERROR = 'POST_ERROR'
第四剑:定义 sagas
涉及到的前端 API 文件
在之前的 post
saga 文件里面,我们使用到了 postApi
,它里面封装了用于向后端(这里我们是小程序云)发起和帖子有关请求的逻辑,让我们马上来实现它吧。
在 src/api/
文件夹下添加 post.js
文件,并在文件中编写内容如下:
import Taro from '@tarojs/taro'
async function createPost(postData, userId) {
const isWeapp = Taro.getEnv() === Taro.ENV_TYPE.WEAPP
const isAlipay = Taro.getEnv() === Taro.ENV_TYPE.ALIPAY
console.log('postData', postData, userId)
// 针对微信小程序使用小程序云函数,其他使用小程序 RESTful API
try {
if (isWeapp) {
const { result } = await Taro.cloud.callFunction({
name: 'createPost',
data: {
postData,
userId,
},
})
return result.post
}
} catch (err) {
console.error('createPost ERR: ', err)
}
}
const postApi = {
createPost,
}
export default postApi;
在上面的代码中,我们定义了 createPost
函数,它是一个 async
函数,用来处理异步逻辑,在 createPost
函数中,我们对当前的环境进行了判断,且只在微信小程序,即 isWeapp
的条件下执行创建帖子的操作,对于支付宝小程序和 H5,我们则放在下一节使用 LeanCloud 的 Serverless 来解决。
创建帖子逻辑是一个 try/catch
语句,用于捕捉可能存在的请求错误,在 try
代码块中,我们使用了 Taro
为我们提供的微信小程序云的云函数 API Taro.cloud.callFunction
来便捷的向小程序云发起云函数调用请求。
这里我们调用了一个 createPost
云函数,并将 postData
和 userId
作为参数传给云函数,用于在云函数中使用用户 Id 和帖子数据来创建一个属于此用户的帖子并保存到数据库,我们将在下一节中实现这个云函数。
如果调用成功,我们可以接收返回值,用于从后端返回数据,这里我们返回了 result.post
数据。
如果调用失败,则打印错误。
最后我们定义了一个 postApi
对象,用于存放所有和用户逻辑有个的函数,并添加 createPost
API 属性然后将其导出,这样在 post
saga 函数里面就可以导入 postApi
然后通过 postApi.createPost
的方式来调用 createPost
API 处理创建帖子的逻辑了。
在 API 默认文件统一导出
在 src/api/index.js
文件中导入上面创建的 postApi
并进行统一导出如下:
import postApi from './post'
export { postApi }
第五剑:创建对应的微信小程序云函数
创建 createPost 云函数
按照和之前创建 login
云函数类似,我们创建 createPost
云函数。
创建成功之后,我们可以得到两个文件,一个是 functions/createPost/package.json
文件,它和之前的类似。
{
"name": "createPost",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"wx-server-sdk": "latest"
}
}
第二个文件就是我们需要编写创建帖子逻辑的 functions/createPost/index.js
文件,微信小程序开发者工具会默认为我们生成一段样板代码。
我们在 function/createPost
文件夹下同样运行 npm install
安装对应的云函数依赖,这样我们才能运行它。
编写 createPost 云函数
打开 functions/createPost/index.js
文件,对其中的内容作出对应的修改如下:
// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
})
const db = cloud.database()
// 云函数入口函数
exports.main = async (event, context) => {
const { postData, userId } = event
console.log('event', event)
try {
const user = await db
.collection('user')
.doc(userId)
.get()
const { _id } = await db.collection('post').add({
data: {
...postData,
user: user.data,
createdAt: db.serverDate(),
updatedAt: db.serverDate(),
},
})
const newPost = await db
.collection('post')
.doc(_id)
.get()
return {
post: { ...newPost.data },
}
} catch (err) {
console.error(`createUser ERR: ${err}`)
}
}
可以看到上面的代码改动主要有以下七处:
- 首先我们给
cloud.init()
传入了环境参数,我们使用了内置的cloud.DYNAMIC_CURRENT_ENV
,表示自动设置为当前的云环境,即在右键点击小程序开发者工具里functions
文件夹时选择的环境。 - 接着,我们通过
cloud.database()
生成了数据实例db
,用于之后在函数体中便捷的操作云数据库。 - 接着就是
main
函数体,我们首先从event
对象中取到了在小程序的调用Taro.cloud.callFunction
传过来的postData
和userId
数据。 - 然后,跟着取数据的是一个
try/catch
语句块,用于捕获错误,在try
语句块中,我们使用db
的查询操作:db.collection('user').doc(userId).get()
,表示查询id
为userId
的user
表数据,它查出来应该是个唯一值,如果不存在满足where
条件的,那么是一个null
值,如果存在满足 条件的,那么返回一个user
对象。 - 接着,我们使用的
db.collection('post').add()
添加一个post
数据,然后在add
方法中传入data
字段,这里我们不仅传入了postData
,还将user
也一同传入了,原因我们将在之后来讲解。除此之外,这里我们额外使用了db.serverDate()
用于记录创建此帖子的时间和更新此帖子的时间,方便之后做条件查询。 - 接着,因为向数据库添加一个记录之后只会返回此记录的
_id
,所以我们需要一个额外的操作db.collection('post').doc()
来获取此条记录,这个doc
用于获取指定的记录引用,返回的是这条数据,而不是一个数组。 - 最后我们返回新创建的
post
。
提示 我们在上面创建
post
的时候,将user
对象也添加到了post
数据中,这里是因为小程序云数据库是 JSON 数据库,所以没有关系数据库的外键概念,导致建关系困难,所以为了之后查询post
的时候方便展示user
数据,我们才这样保存的. 当然更加科学的做法是在post
里面保存userId
,这样能减少数据冗余,但是因为做教学用,所以这些我们偷了一点懒。 所以我们这里强烈建议,在正规的环境下,关系型数据库应该建外键,JSON 数据库也至少应该保存userId
。:
第六剑:定义对应的 reducers
文件
我们在前面处理创建帖子时,在组件内部 dispatch
了 CREATE_POST
action,在处理异步 action 的 saga 函数中,使用 put
发起了一系列更新 store 中帖子状态的 action,现在我们马上来实现响应这些 action 的 reducers
,打开 src/reducers/post.js
,对其中的代码做出对应的修改如下:
import {
SET_POST,
SET_POSTS,
SET_POST_FORM_IS_OPENED,
POST_ERROR,
CREATE_POST,
POST_NORMAL,
POST_SUCCESS,
} from '../constants/'
import avatar from '../images/avatar.png'
const INITIAL_STATE = {
posts: [],
post: {},
isOpened: false,
isPost: false,
postStatus: POST_NORMAL,
}
export default function post(state = INITIAL_STATE, action) {
switch (action.type) {
case SET_POST: {
const { post } = action.payload
return { ...state, post }
}
case SET_POSTS: {
const { posts } = action.payload
return { ...state, posts: state.posts.concat(...posts) }
}
case SET_POST_FORM_IS_OPENED: {... return { ...state, isOpened }
}
case CREATE_POST: {
return { ...state, postStatus: CREATE_POST, isPost: true }
}
case POST_SUCCESS: {
return { ...state, postStatus: POST_SUCCESS, isPost: false }
}
case POST_ERROR: {
return { ...state, postStatus: POST_ERROR, isPost: false }
}
default:
return state
}
看一看到上面的代码主要有三处改动:
- 首先我们导入了必要的 action 常量
- 接着我们给
INITIAL_STATE
增加了几个字段:posts
:保存帖子列表的数据,创建新的帖子也会保存在这里面。post
:保存单个帖子,我们将在之后讲解用于在获取帖子详情时保存数据用。
isPost
:用于标志帖子逻辑过程中是否在执行创帖逻辑,true
表示正在执行创帖中,false
表示登录逻辑执行完毕postStatus
:用于标志创帖过程中的状态:开始创帖(CREATE_POST
)、创帖成功(POST_SUCCESS
)、登录失败(POST_ERROR
)
- 最后就是
switch
语句中响应 action,更新相应的状态。
“六脉神剑” 搞定 getPosts 异步逻辑
在上一 “大” 节中,我们使用了图雀社区不传之术:“六脉神剑” 搞定了 createPost
的异步逻辑,现在我们马上趁热打铁来巩固我们的武功,搞定 getPosts 异步逻辑,它对应着我们小程序底部两个 tab 栏的第一个,也就是我们打开小程序的首屏渲染逻辑,也就是一个帖子列表。
第一剑:index
组件中发起异步请求
打开 src/pages/index/index.jsx
文件,对其中的内容作出对应的修改如下:
import { PostCard, PostForm } from '../../components'
import './index.scss'
import {
SET_POST_FORM_IS_OPENED,
SET_LOGIN_INFO,
GET_POSTS,
} from '../../constants'
export default function Index() {
const posts = useSelector(state => state.post.posts) || []... const dispatch = useDispatch()
useEffect(() => {
const WeappEnv = Taro.getEnv() === Taro.ENV_TYPE.WEAPP
if (WeappEnv) {
Taro.cloud.init()
}
async function getStorage() {
try {
const { data } = await Taro.getStorage({ key: 'userInfo' })
const { nickName, avatar, _id } = data
// 更新 Redux Store 数据
dispatch({
type: SET_LOGIN_INFO,
payload: { nickName, avatar, userId: _id },
})
} catch (err) {
console.log('getStorage ERR: ', err)
}
}
if (!isLogged) {
getStorage()
}
async function getPosts() {
try {
// 更新 Redux Store 数据
dispatch({
type: GET_POSTS,
})
} catch (err) {
console.log('getPosts ERR: ', err)
}
}
if (!posts.length) {
getPosts()
}
}, [])
function setIsOpened(isOpened) {
dispatch({ type: SET_POST_FORM_IS_OPENED, payload: { isOpened } })... return (
<View className="index">
<AtMessage />
{posts.map(post => (
<PostCard key={post._id} postId={post._id} post={post} isList />
))}
<AtFloatLayout
isOpened={isOpened}
可以看到,上面的内容做了如下四处修改:
- 首先我们对当前的开发环境做了判断,如果是微信小程序环境,我们就使用
Taro.cloud.init()
进行小程序环境的初始化。 - 接着,我们在
useEffects
Hooks 里面定义了getPosts
函数,它是一个异步函数,用于dispatch
GET_POSTS 的异步请求,并且我们进行了判断,当此时 Redux Store 内部没有文章时,才进行数据的获取。 - 接着,我们改进了
getStorage
获取缓存的函数,将其移动到useEffects
Hooks 里面,并额外增加了_id
属性,它被赋值给userId
一起设置 Redux Store 中关于用户的属性,这样做的目的主要是为了之后发帖标志用户,或者获取用户的个人信息用。并且,加了一层if
判断,只有当没有登录时,即isLogged
为 false 的时候,才进行获取缓存操作。 - 最后我们导入了必要的
GET_POSTS
常量,并且将return
语句里的PostCard
接收的key
和postId
属性变成了真实的帖子_id
。这样我们在帖子详情时可以直接拿postId
向小程序云发起异步请求。
注意 在上一篇教程中,有同学提到没有使用
Taro.cloud.init()
初始化的问题,是因为分成了两篇文章,在这篇文章才初始化。要使用小程序云,初始化环境是必要的。
第二剑:声明和补充对应需要的异步 sagas
文件
在 “第一剑” 中,我们从组件中 dispatch 了 action.type 为 GET_POSTS
的异步 Action,接下来我们要做的就是在对应的 sagas
文件中补齐响应这个异步 action 的 sagas。
打开 src/sagas/post.js
文件,在其中定义 getPosts
sagas 逻辑如下:
import {
GET_POSTS,
} from '../constants'
function* getPosts() {
try {
const posts = yield call(postApi.getPosts)
// 其实以下三步可以合成一步,但是这里为了讲解清晰,将它们拆分成独立的单元
// 发起获取帖子成功的 action
yield put({ type: POST_SUCCESS })
// 更新 Redux store 数据
yield put({
type: SET_POSTS,
payload: {
posts,
},
})
} catch (err) {
console.log('getPosts ERR: ', err)
// 获取帖子失败,发起失败的 action
yield put({ type: POST_ERROR })
}
}
function* watchGetPosts() {
while (true) {
yield take(GET_POSTS)
yield fork(getPosts)
}
}
export { watchGetPosts }
可以看到,上面的改动主要是创建 watcherSaga
和 handlerSaga
。
创建 watcherSaga
- 我们创建了登录的
watcherSaga
:watchGetPosts
,它用来监听action.type
为GET_POSTS
的 action,并且当监听到GET_POSTS
action 之后,然后激活handlerSaga
:getPosts
去处理对应的获取帖子列表的逻辑。 - 这里的
watcherSaga
:watchGetPosts
是一个生成器函数,它内部是一个while
无限循环,表示在内部持续监听GET_POSTS
action。 - 在循环内部,我们使用了
redux-saga
提供的effects helper
函数:take
,它用于监听GET_POSTS
action,获取 action 中携带的数据。 - 接着我们使用了另外一个
effects helper
函数:fork
,它表示非阻塞的执行handlerSaga
:getPosts
,因为这里获取帖子列表不需要传数据,所以这里没有额外的数据传递逻辑。
创建 handlerSaga
- 我们创建了创建帖子的
handlerSaga
:getPosts
,它用来处理创建逻辑。 getPosts
也是一个生成器函数,在它内部是一个try/catch
语句,用于处理获取帖子列表请求可能存在的错误情况。- 在
try
语句中,首先是使用了redux-saga
提供给我们的effects helper
函数:call
来调用登录的 API:postApi. getPosts
。- 如果获取帖子列表成功,我们使用
redux-saga
提供的effects helpers
函数:put
,put
类似之前在view
中的dispatch
操作,,来dispatch
了两个 action:POST_SUCCESS
,SET_POSTS
,代表更新获取帖子列表成功的状态,设置最新获取的帖子列表到 Redux Store 中。
- 如果获取帖子列表成功,我们使用
- 如果获取帖子列表失败,我们则使用
put
发起一个POST_ERROR
的 action 来更新获取帖子列表失败的信息到 Redux Store
一些额外的工作
为了创建 watcherSaga
和 handlerSaga
,我们还导入了 postApi. getPosts
,我们将在后面来创建这个 API。
除此之外我们还导入了需要使用的 action 常量:
GET_POSTS
:响应获取帖子列表的 ACTION 常量,我们将在 “第三剑” 中创建它。
加入 saga 中心调度文件
我们像之前将 watchCreatePost
等加入到 sagas
中心调度文件一样,将我们创建好的 watchGetPosts
也加入进去:
// ...之前的逻辑
import { watchGetPosts } from './post'
export default function* rootSaga() {
yield all([
// ... 之前的逻辑
fork(watchGetPosts)
])
}
第三剑:定义 sagas
需要的常量文件
打开 src/constants/post.js
文件,定义我们之前创建的常量文件如下:
export const GET_POSTS = 'GET_POSTS'
第四剑:定义 sagas
涉及到的前端 API 文件
在之前的 post
saga 文件里面,我们使用到了 postApi.getPosts
,它里面封装了用于向后端(这里我们是小程序云)发起和获取帖子列表有关请求的逻辑,让我们马上来实现它吧。
打开 src/api/post.js
文件,并在其中编写内容如下:
// ... 其余逻辑一样
async function getPosts() {
const isWeapp = Taro.getEnv() === Taro.ENV_TYPE.WEAPP
const isAlipay = Taro.getEnv() === Taro.ENV_TYPE.ALIPAY
// 针对微信小程序使用小程序云函数,其他使用小程序 RESTful API
try {
if (isWeapp) {
const { result } = await Taro.cloud.callFunction({
name: 'getPosts',
})
return result.posts
}
} catch (err) {
console.error('getPosts ERR: ', err)
}
}
const postApi = {
// ... 之前的 API
getPosts,
}
// ... 其余逻辑一样
在上面的代码中,我们定义了 getPosts
函数,它是一个 async
函数,用来处理异步逻辑,在 getPosts
函数中,我们对当前的环境进行了判断,且只在微信小程序,即 isWeapp
的条件下执行获取帖子列表的操作,对于支付宝小程序和 H5,我们则放在下一节使用 LeanCloud 的 Serverless 来解决。
创建帖子逻辑是一个 try/catch
语句,用于捕捉可能存在的请求错误,在 try
代码块中,我们使用了 Taro
为我们提供的微信小程序云的云函数 API Taro.cloud.callFunction
来便捷的向小程序云发起云函数调用请求。
这里我们调用了一个 getPosts
云函数,我们将在下一节中实现这个云函数。
如果调用成功,我们可以接收返回值,用于从后端返回数据,这里我们返回了 result.posts
数据,即从小程序云返回的帖子列表。
如果调用失败,则打印错误。
最后我们在已经定义好的 postApi
对象里,添加 getPosts
API 属性然后将其导出,这样在 post
saga 函数里面就可以导入 postApi
然后通过 postApi. getPosts
的方式来调用 getPosts
API 处理获取帖子列表的逻辑了。
第五剑:创建对应的微信小程序云函数
创建 getPosts 云函数
按照和之前创建 createPost
云函数类似,我们创建 getPosts
云函数。
创建成功之后,我们可以得到两个文件,一个是 functions/getPosts/package.json
文件,它和之前的类似。
{
"name": "getPosts",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"wx-server-sdk": "latest"
}
}
第二个文件就是我们需要编写创建帖子逻辑的 functions/getPosts/index.js
文件,微信小程序开发者工具会默认为我们生成一段样板代码。
我们在 function/getPosts
文件夹下同样运行 npm install
安装对应的云函数依赖,这样我们才能运行它。
编写 getPosts 云函数
打开 functions/getPosts/index.js
文件,对其中的内容作出对应的修改如下:
// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
})
const db = cloud.database()
const _ = db.command
// 云函数入口函数
exports.main = async (event, context) => {
try {
const { data } = await db.collection('post').get()
return {
posts: data,
}
} catch (e) {
console.error(`getPosts ERR: ${e}`)
}
}
可以看到上面的代码改动主要有以下处:
- 首先我们给
cloud.init()
传入了环境参数,我们使用了内置的cloud.DYNAMIC_CURRENT_ENV
,表示自动设置为当前的云环境,即在右键点击小程序开发者工具里functions
文件夹时选择的环境。 - 接着,我们通过
cloud.database()
生成了数据实例db
,用于之后在函数体中便捷的操作云数据库。 - 接着就是
main
函数体,里面是一个try/catch
语句块,用于捕获错误,在try
语句块中,我们使用db
的查询操作:db.collection('post').get()
,表示查询所有的post
数据。 - 最后我们返回查询到的
posts
数据。
第六剑:定义对应的 reducers
文件
因为这里 SET_POSTS
的 Action 我们在上一 “大” 节中创建帖子时已经定义了,所有在 “这一剑” 中我们无需添加额外的代码,复用之前的逻辑就好。
“六脉神剑” 搞定 getPost 异步逻辑
在上面两 “大” 节中,我们连续用了两次 “六脉神剑”,相信跟到这里的同学应该对我们接下来要做的事情已经轻车熟路了吧?。
接下来,我们将收尾 Post 逻辑的最后一公里,即帖子详情的异步逻辑 “getPost” 接入,话不多说就是干!
第一剑:post
组件中发起异步请求
打开 src/pages/post/post.jsx
文件,对其中的内容作出对应的修改如下:
import Taro, { useRouter, useEffect } from '@tarojs/taro'
import { View } from '@tarojs/components'
import { useDispatch, useSelector } from '@tarojs/redux'
import { PostCard } from '../../components'
import './post.scss'
import { GET_POST, SET_POST } from '../../constants'
export default function Post() {
const router = useRouter()
const { postId } = router.params
const dispatch = useDispatch()
const post = useSelector(state => state.post.post)
useEffect(() => {
dispatch({
type: GET_POST,
payload: {
postId,
},
})
return () => {
dispatch({ type: SET_POST, payload: { post: {} } })
}
}, [])
return (
<View className="post">
可以看到,上面的内容做了如下四处修改:
- 首先我们使用
useDispatch
Hooks 获取到了dispatch
函数。 - 接着,在
useEffects
Hooks 里面定义了 dispatch 了 action.type 为 GET_POST 的 action,它是一个异步 Action,并且我们在 Hooks 最后返回了一个函数,其中的内容为将post
设置为空对象,这里用到的SET_POST
常量我们将在后面定义它。这个返回函数主要用于post
组件卸载之后,Redux Store 数据的重置,避免下次打开帖子详情还会渲染之前获取到的帖子数据。 - 接着,我们使用
useSelector
Hooks 来获取异步请求到的post
数据,并用于return
语句中的数据渲染。 - 最后我们删除了不必要的获取
posts
数据的useSelector
Hooks,以及删掉了不必要的调试console.log
语句。
第二剑:声明和补充对应需要的异步 sagas
文件
在 “第一剑” 中,我们从组件中 dispatch 了 action.type 为 GET_POST
的异步 Action,接下来我们要做的就是在对应的 sagas
文件中补齐响应这个异步 action 的 sagas。
打开 src/sagas/post.js
文件,在其中定义 getPosts
sagas 逻辑如下:
// ... 和之前的逻辑一样
import {
// ... 和之前的逻辑一样
SET_POST,
} from '../constants';
// ... 和之前的逻辑一样
function* getPost(postId) {
try {
const post = yield call(postApi.getPost, postId)
// 其实以下三步可以合成一步,但是这里为了讲解清晰,将它们拆分成独立的单元
// 发起获取帖子成功的 action
yield put({ type: POST_SUCCESS })
// 更新 Redux store 数据
yield put({
type: SET_POST,
payload: {
post,
},
})
} catch (err) {
console.log('getPost ERR: ', err)
// 获取帖子失败,发起失败的 action
yield put({ type: POST_ERROR })
}
}
function* watchGetPost() {
while (true) {
const { payload } = yield take(GET_POST)
yield fork(getPost, payload.postId)
}
}
export { watchGetPost }
可以看到,上面的改动主要是创建 watcherSaga
和 handlerSaga
。
创建 watcherSaga
- 我们创建了登录的
watcherSaga
:watchGetPost
,它用来监听action.type
为GET_POST
的 action,并且当监听到GET_POST
action 之后,然后激活handlerSaga
:getPost
去处理对应的获取单个帖子的逻辑。 - 这里的
watcherSaga
:watchGetPost
是一个生成器函数,它内部是一个while
无限循环,表示在内部持续监听GET_POST
action。 - 在循环内部,我们使用了
redux-saga
提供的effects helper
函数:take
,它用于监听GET_POST
action,获取 action 中携带的数据,这里我们拿到了传过来的payload
数据。 - 接着我们使用了另外一个
effects helper
函数:fork
,它表示非阻塞的执行handlerSaga
:getPost
,并传入了获取到payload.postId
参数。
创建 handlerSaga
- 我们创建了获取单个帖子的
handlerSaga
:getPost
,它用来处理获取帖子逻辑。 getPost
也是一个生成器函数,在它内部是一个try/catch
语句,用于处理获取单个帖子请求可能存在的错误情况。- 在
try
语句中,首先是使用了redux-saga
提供给我们的effects helper
函数:call
来调用登录的 API:postApi. getPost
。- 如果获取单个帖子成功,我们使用
redux-saga
提供的effects helpers
函数:put
,put
类似之前在view
中的dispatch
操作,,来dispatch
了两个 action:POST_SUCCESS
,SET_POSTS
,代表更新获取单个帖子成功的状态,设置最新获取的帖子到 Redux Store 中。
- 如果获取单个帖子成功,我们使用
- 如果获取单个帖子失败,我们则使用
put
发起一个POST_ERROR
的 action 来更新获取单个帖子失败的信息到 Redux Store
一些额外的工作
为了创建 watcherSaga
和 handlerSaga
,我们还导入了 postApi.getPost
,我们将在后面来创建这个 API。
除此之外我们还导入了需要使用的 action 常量:
SET_POST
:响应获取帖子列表的 ACTION 常量,我们将在 “第三剑” 中创建它
加入 saga 中心调度文件
我们像之前将 watchGetPosts
等加入到 sagas
中心调度文件一样,将我们创建好的 watchGetPost
也加入进去:
打开 src/sagas/index.js
文件,对其中的内容作出如下的修改:
import { fork, all } from 'redux-saga/effects'
import { watchLogin } from './user'
import { watchCreatePost, watchGetPosts, watchGetPost } from './post'
export default function* rootSaga() {
yield all([
fork(watchLogin),
fork(watchCreatePost),
fork(watchGetPosts),
fork(watchGetPost),
])
}
第三剑:定义 sagas
需要的常量文件
打开 src/constants/post.js
文件,定义我们之前创建的常量文件 GET_POST
:
export const SET_POST = 'SET_POST'
第四剑:定义 sagas
涉及到的前端 API 文件
在之前的 post
saga 文件里面,我们使用到了 postApi.getPost
,它里面封装了用于向后端(这里我们是小程序云)发起和获取单个帖子有关请求的逻辑,让我们马上来实现它吧。
打开 src/api/post.js
文件,并在其中编写内容如下:
// ... 其他内容和之前一致
async function getPost(postId) {
const isWeapp = Taro.getEnv() === Taro.ENV_TYPE.WEAPP
const isAlipay = Taro.getEnv() === Taro.ENV_TYPE.ALIPAY
// 针对微信小程序使用小程序云函数,其他使用小程序 RESTful API
try {
if (isWeapp) {
const { result } = await Taro.cloud.callFunction({
name: 'getPost',
data: {
postId,
},
})
return result.post
}
} catch (err) {
console.error('getPost ERR: ', err)
}
}
const postApi = {
getPost,
}
export default postApi
可以看到上面的代码有如下六处改动:
- 在上面的代码中,我们定义了
getPost
函数,它是一个async
函数,用来处理异步逻辑,在getPost
函数中,我们对当前的环境进行了判断,且只在微信小程序,即isWeapp
的条件下执行获取单个帖子的操作,对于支付宝小程序和 H5,我们则放在下一节使用 LeanCloud 的 Serverless 来解决。 - 创建帖子逻辑是一个
try/catch
语句,用于捕捉可能存在的请求错误,在try
代码块中,我们使用了Taro
为我们提供的微信小程序云的云函数 APITaro.cloud.callFunction
来便捷的向小程序云发起云函数调用请求。 - 这里我们调用了一个
getPost
云函数,并给它传递了对应要获取的帖子的postId
我们将在下一节中实现这个云函数。 - 如果调用成功,我们可以接收返回值,用于从后端返回数据,这里我们返回了
result.post
数据,即从小程序云返回的单个帖子。 - 如果调用失败,则打印错误。
- 最后我们在已经定义好的
postApi
对象里,添加getPost
API 属性然后将其导出,这样在post
saga 函数里面就可以导入postApi
然后通过postApi. getPost
的方式来调用getPost
API 处理获取单个帖子的逻辑了。
第五剑:创建对应的微信小程序云函数
创建 getPost 云函数
按照和之前创建 getPosts
云函数类似,我们创建 getPost
云函数。
创建成功之后,我们可以得到两个文件,一个是 functions/getPost/package.json
文件,它和之前的类似。
{
"name": "getPost",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"wx-server-sdk": "latest"
}
}
第二个文件就是我们需要编写创建帖子逻辑的 functions/getPost/index.js
文件,微信小程序开发者工具会默认为我们生成一段样板代码。
我们在 function/getPost
文件夹下同样运行 npm install
安装对应的云函数依赖,这样我们才能运行它。
编写 getPost 云函数
打开 functions/getPost/index.js
文件,对其中的内容作出对应的修改如下:
// 云函数入口文件
const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV,
})
const db = cloud.database()
// 云函数入口函数
exports.main = async (event, context) => {
const { postId } = event
try {
const { data } = await db
.collection('post')
.doc(postId)
.get()
return {
post: data,
}
} catch (e) {
console.error(`getPost ERR: ${e}`)
}
}
可以看到上面的代码改动主要有以下处:
- 首先我们给
cloud.init()
传入了环境参数,我们使用了内置的cloud.DYNAMIC_CURRENT_ENV
,表示自动设置为当前的云环境,即在右键点击小程序开发者工具里functions
文件夹时选择的环境。 - 接着,我们通过
cloud.database()
生成了数据实例db
,用于之后在函数体中便捷的操作云数据库。 - 接着就是
main
函数体,里面是一个try/catch
语句块,用于捕获错误,在try
语句块中,我们首先从event
对象里面获取到了postId
,接着我们使用db
的查询操作:db.collection('post').doc(postId).get()
,表示查询所有的对应_id
为postId
的单个帖子数据 - 最后我们返回查询到的
post
数据。
第六剑:定义对应的 reducers
文件
因为这里 SET_POST
的 Action 我们在上上 “大” 节中创建帖子时已经定义了,所有在 “这一剑” 中我们无需添加额外的代码,复用之前的逻辑就好。
小结
在这篇教程中,我们连续使用了三次 “六脉神剑” 讲完了我们的 Post 逻辑的异步流程,让我们再来复习一下我们开头提到的 “六脉神剑”:
- 将组件中的同步逻辑重构到异步逻辑
- 声明和补充对应需要的异步
sagas
文件 - 定义
sagas
需要的常量文件 - 定义
sagas
涉及到的前端 API 文件 - 创建对于的微信小程序云函数,并编写对应的 Node.js 处理逻辑
- 定义对应的
reducers
文件
这是一套讲解模式,也是一套写代码的最佳实践方式之一,希望你能受用。
一点遗憾
这两篇讲解微信小程序云的文章有一个小小的遗憾,我们也在之前的文章中提到过了,就是微信小程序云仅限于微信小程序内的使用,如果我们想做多端应用,比如支付宝小程序云,H5 网站,那么单单使用微信小程序就显得无能为力了,我们将在下一篇文章中引入 LeanCloud Serverless 服务,并使用它来补齐我们跨端小程序开发的短板,敬请期待!✌️
想要学习更多精彩的实战技术教程?来图雀社区[12]逛逛吧。
本文所涉及的源代码都放在了 Github[13] 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点个在看 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]
使用 Hooks 版的 Redux 实现大型应用状态管理(下篇): https://juejin.im/post/5e1e63e8f265da3e4412b1f1
[6]
Taro 小程序开发大型实战(六):尝鲜微信小程序云(上篇): https://juejin.im/post/5e1dd614f265da3e12181ff3
[7]
Redux 包教包会(一):解救 React 状态危机: https://juejin.im/post/5df62cd8e51d4558270ef5ca
[8]
Redux 包教包会(二):趁热打铁,完全重构: https://juejin.im/post/5df7b11c51882512664b1068
[9]
Redux 包教包会(三):各司其职,重拾初心: https://juejin.im/post/5e0fe9705188253ab044c869
[10]
Github: https://github.com/tuture-dev/ultra-club
[11]
React 前端工程师学习路线: https://github.com/tuture-dev/react-roadmap
[12]
图雀社区: https://tuture.co?utm_source=juejin_zhuanlan
[13]
Github: https://github.com/tuture-dev/ultra-club
代码语言:javascript复制● 一杯茶的时间,上手React框架开发● Taro小程序开发大型实战(一):熟悉的React,熟悉的Hooks● Taro小程序开发大型实战(二):多页面跳转和TaroUI组件库
·END·