0%

Taro 小程序开发大型实战(五):尝鲜微信小程序云

@tuture-dev

查看代码

微信小程序云初尝鲜

欢迎继续阅读《Taro 小程序开发大型实战》系列,前情回顾:

如果你敲到这里,会发现我们之后的内容都是纯前端(小程序端)的逻辑,一个完整的可上线小程序应用应该还要有后端,在这篇文章中,我们将使用微信小程序云作为我们的后台,接着我们会引进 redux-saga 来帮助 Redux 优雅的处理异步流程,本文最终的实现效果如下:

在这一篇中,我们将开始引入和实践 Redux 异步工作流,如果你不熟悉 Redux,推荐阅读我们的《Redux 包教包会》系列教程:

如果你希望直接从这一步开始,请运行以下命令:

git clone -b miniprogram-start https://github.com/tuture-dev/ultra-club.git
cd ultra-club

在前面的代码中,我们通过将数据保存在 Storage 里面来完成数据的持久化,这样可以解决小规模数据的存储和查询问题,一旦数据量变大了,那么查询和存储就需要依靠专门的数据库来解决了,一般我们可以通过自建后端和数据库的方式来解决,但当小程序正越来越火的同时,一种被称为 Serverless 的模式被提出并也逐渐火爆起来,通俗意义上来概括就是 “无后端”,即把后端交给云服务厂商(阿里云、腾讯云、京东云等),开发者只需要专注于前端逻辑,快速交付功能。

一般的小程序 Serverless 服务都包含三大功能:

  • 数据库:一般是以 JSON 数据格式进行存储,可以将数据存储在云端数据库中。
  • 存储:支持文本、图片等用户生成内容的存储,可以获取资源的链接进行使用。
  • 云函数:可以用 Node.js 进行开发,自己编写对应的后端逻辑,并把写好的代码传到云端,然后在小程序前端使用 API 进行调用。

提示

关于小程序 Serverless 的详细描述,这里推荐一篇文章,有兴趣的同学可以详细了解一下:什么是小程序Serverless?

在这一节中,我们使用微信小程序云作为我们的 “后端”,微信小程序云和小程序账号绑定在一起,一个小程序账号可以开通一个小程序云空间,接下来我们来详细讲解如何开通小程序云。

开通小程序云

  1. 首先确保你注册了小程序的微信公众平台账号:注册地址
  2. 登录之后,在菜单栏开发 > 开发设置里面找到 AppID,他应该是一个18位字符串。
  3. 使用微信开发者工具打开我们的 ultra-club 项目文件夹,然后在微信开发者工具菜单栏中选择设置 > 项目设置,打开设置栏:

4.找到设置栏的基本信息,AppID 栏将其修改为上面的 AppID 如下:

5.当设置了 AppID 之后,我们的开发者工具里面的 “云开发” 按钮应该就会变成可点击状态,找到左上角的 “云开发” 的按钮并点击,类似下面这张图:

4.点击 ”云开发“ 按钮之后会弹出确认框,点击同意就会进到小程序云开发控制台:

进来之后我们首先看到的是云开发控制台的 ”运营分析“ 界面,这是用来可视化云开发各类资源的使用情况的界面,在这篇教程中我们不会讲解这方面内容。我们主要来讲一下图中标红的部分:

  • 其中序号为 1 的就是我们的云数据库,它是一个 JSON 数据库,里面存储着我们在开发时需要的数据。
  • 序号为2的是存储,即我们可以上传一些文本、图片、音/视频,然后返回给我们访问这些资源的链接。
  • 序号3是云函数,即我们可以在这里面管理一些我们编写的的后端 Node.js 逻辑,它运行在云中,我们可以在小程序端通过 API 来调用它们。
  • 序号4是代表我们此次的云环境的标识符,可以用于在小程序端以 API 调用云开发资源时标志此时的调用的云环境。

在本篇教程中,我们会用到上面提到的数据库和云函数两项功能。

创建数据库表

介绍完小程序云的界面,我们马上来动手实践,来创建我们需要的数据库表,因为我们前端逻辑主要分为 userpost 两类逻辑,所以我们在数据库中创建两张表:

这里我们具体来解释一下这个数据库操作界面的含义:

  • 可以看到,点击云开发控制台左上角的第二个按钮,然后点击图中标红序号为1的 “+” 按钮,创建两个集合 userpost,这样我们就创建好了我们的数据库表。
  • 序号为2表示我们可以选中某个集合,点击右键进行删除操作。
  • 序号为3表示我们可以给某个集合添加记录,因为是 JSON 数据库,集合中每条记录都可以不一样。
  • 序号4表示我们可以选中某条记录,点击右键进行删除操作
  • 序号5表示我们可以给单个记录添加字段
  • 序号6表示我们可以选中单个记录进行删/改操作
  • 序号7表示我们可以查询这个集合中某条记录

创建 post 记录

这里我们添加了一条默认的 post 记录,表示之前我们之前小程序端的那条默认数据,这条数据记录了 post 的相关信息:

  • _id: 此条数据的唯一标识符
  • title: 文章标题
  • content: 文章内容
  • user: 发表此文章的用户,这里我们为了方便起见,直接保存了用户的完整信息,一般的最佳实践建议是保存此用户的 _id 属性,然后在查询 post 时,取出此用户的 _id 属性,然后去查 user 得到用户的完整信息。
  • updatedAt:此条记录的上次更新时间
  • createdAt:此条记录的创建时间

创建 user 记录

上面我们提到了我们在这条文章记录里面保存了发帖作者信息,那么当然我们的 user 集合中就要新建一条此作者的信息如下:

可以看到,我们添加了一条用户记录,它的字段如下:

  • _id:此用户在 user 集合中的唯一标识符
  • avatar:此用户的头像地址
  • nickName:此用户的昵称,我们将用它来进行登录
  • createdAt:创建此记录的时间
  • updatedAt:上次更新此记录的时间

在小程序端初始化小程序云环境

在开通了小程序云之后,我们还需要在小程序前端代码中进行小程序云环境的初始化设置,这样才能在小程序前端调用小程序的 API。

打开 src/index/index.jsx 文件,在其中添加如下的代码:

import Taro, { useEffect } from '@tarojs/taro'

// ... 其余代码一致

export default function Index() {
// ... 其余代码一致
useEffect(() => {
const WeappEnv = Taro.getEnv() === Taro.ENV_TYPE.WEAPP

if (WeappEnv) {
Taro.cloud.init()
}

// ...其余代码一致
return (
<View className="index">
...
</View>
)
}

可以看到,我们增加了微信小程序环境的获取和判断,当当前环境是微信小程序环境时,我们需要调用 Taro.cloud.init() 来进行小程序云环境的初始化。

小结

到现在为止,我们讲解了如何开通小程序云,然后讲解了小程序云控制台界面,同时,我们讲解了将会用到的数据库功能界面,在其中创建了我们应用需要的两张表(集合):postuser,并且各自初始化了一条记录。

好了,准备好了小程序云,我们开始准备在应用中接入它了,但在此之前,因为我们要接入小程序云,那么势必要发起异步的请求,这就需要了解一下 Redux 的异步处理流程,在下一节中,我们将使用 redux-saga 中间件来简化 Redux 处理异步的流程。

Redux 异步工作流解析

我们来看一下 Redux 的数据流动图:

上图中灰色的那条路径是我们之前一直在使用的 Redux 的数据流动图,它是 Redux 同步数据流动图:

  • viewdispatch(syncAction) 一个同步 action 来更新 store 中的数据
  • reducer 响应 action,更新 store 状态
  • connect 将更新后的状态传给 view
  • view 接收新的数据重新渲染

提示

对 Redux 还不了解的同学可以学习一下图雀社区的 Redux 包教包会系列教程哦。

现在我们要去向小程序云发起请求,这个请求是一个异步的请求,它不会立刻得到响应,所以我们需要一个中间状态(这里我们使用 Saga)来回处理这个异步请求并得到数据,然后再执行和之前同步请求类似的路径,即为我们上图中绿色的部分+剩下灰色的部分,所以异步工作流程就变成了这样:

  • viewdispatch(asyncAction) 一个异步 action 来获取后端(这里是小程序云)的数据
  • saga 处理这个异步 action,并等待数据响应
  • saga 得到响应的数据,dispatch(syncAction) 一个同步的 action 来更新 store 的状态
  • reducer 响应 action,更新 store 状态
  • connect 将更新后的状态传给 view
  • view 接收新的数据重新渲染

提示

图雀社区日后会出一篇教程专门讲解 Redux 异步工作流,这里不会细究整个异步流程的原理,只会讲解如何整合这个异步工作流。敬请期待哦✌️~

实战 Redux 异步工作流

安装

我们使用 redux-saga 这个中间件来接管 Redux 异步工作流的处理异步请求部分,首先在项目根目录下安装 redux-saga 包:

$ npm install redux-saga
package.json查看完整代码
// ...
"prop-types": "^15.7.2",
"redux": "^4.0.5",
"redux-logger": "^3.0.6",
[tuture-add] "redux-saga": "^1.1.3",
"taro-ui": "^2.2.4"
},
"devDependencies": {
// ...

主要

redux-sagaredux 的一个处理异步流程的中间件,那么 Saga 是什么?Saga的定义是“长时间活动的事务”(Long Lived Transaction,后文简称为LLT)。他是普林斯顿大学HECTOR GARCIA-MOLINA教授在1987年的一篇关于分布式数据库的论文中提出来的概念。

官方把一个 saga 比喻为应用程序中的一个单独的线程,它负责独立的处理副作用,在 JavaScript 中,副作用就是指异步网络请求、本地读取 localStorage/Cookie 等外界操作。

配置 redux-saga 中间件

安装完之后,我们接着要先配置 redux-saga 才能使用它,打开 src/store/index.js 文件,对其中的内容作出对应的修改如下:

src/store/index.js查看完整代码
import { createStore, applyMiddleware } from 'redux'
import { createLogger } from 'redux-logger'
[tuture-add]import createSagaMiddleware from 'redux-saga'
[tuture-add]
import rootReducer from '../reducers'
[tuture-add]import rootSaga from '../sagas'

[tuture-del]const middlewares = [createLogger()]
[tuture-add]const sagaMiddleware = createSagaMiddleware()
[tuture-add]const middlewares = [sagaMiddleware, createLogger()]

export default function configStore() {
const store = createStore(rootReducer, applyMiddleware(...middlewares))
[tuture-add]
[tuture-add] sagaMiddleware.run(rootSaga)
[tuture-add]
return store
}

可以看到,我们上面的文件作出以下四处改动:

  • 首先我们导出了 createSagaMiddleware
  • 接着我们从 src/store/sagas 文件夹下导出了一个 rootSaga,它组合了所有的 saga 文件,这类似组合 reducercombineReducers,我们将在后续的步骤中编写这些 sagas
  • 接着我们调用 createSagaMiddleware 生成 sagaMiddleware 中间件,并将其放置在 middleware 数组中,这样 Redux 就会注册这个中间件,在响应异步 action 时,sagaMiddleware 会介入,并将其转交给我们定义的 saga 函数来处理。
  • 最后在 createStore 函数里面,当创建 store 之后,我们调用 sagaMiddleware.run(rootSaga) 来将所有的 sagas 跑起来开始监听并响应异步 action。

View 中发起异步请求

配置使用 redux-saga 中间件,并将 sagas 跑起来之后,我们可以开始在 React 中 dispatch 异步的 action 了。

让我们遵照之前的重构顺序,先来搞定登录的异步数据流处理,打开 src/components/LoginForm/index.jsx 文件,对其中的内容作出对应的修改如下:

src/components/LoginForm/index.jsx查看完整代码
// ...
import { AtButton, AtImagePicker } from 'taro-ui'
import { useDispatch } from '@tarojs/redux'

[tuture-del]import { SET_LOGIN_INFO, SET_IS_OPENED } from '../../constants'
[tuture-add]import { LOGIN } from '../../constants'
import './index.scss'

export default function LoginForm(props) {
// ...
setFiles([])
setFormNickName('')

[tuture-del] // 缓存在 storage 里面
[tuture-del] await Taro.setStorage({ key: 'userInfo', data: userInfo })
[tuture-del]
[tuture-del] dispatch({ type: SET_LOGIN_INFO, payload: userInfo })
[tuture-del]
[tuture-del] // 关闭弹出层
[tuture-del] dispatch({ type: SET_IS_OPENED, payload: { isOpened: false } })
[tuture-add] // 向后端发起登录请求
[tuture-add] dispatch({ type: LOGIN, payload: { userInfo: userInfo } })
}

return (
// ...

可以看到,我们对上面的代码做出了以下三处改动:

  • 我们将之前设置用户登录信息的 SET_LOGIN_INFO 和设置登录框弹出层的 SET_IS_OPENED 换成了 LOGIN 常量,代表我们要先向小程序云发起登录请求,然后获取到登录的数据再设置登录信息和关闭登录框弹出层(其实这里也可以直接关闭弹出层,有点失策(⊙o⊙)…)。
  • 接着我们将之前的设置登录信息和关闭登录框弹出层的操作删除掉。
  • 最后我们将 dispatch 一个 action.typeLOGIN 的 action,带上我们的需要进行登录的信息 userInfo

增加 Action 常量

我们在上一步中使用到了 LOGIN 常量,打开 src/constants/user.js,在其中增加 LOGIN 常量:

export const SET_IS_OPENED = 'MODIFY_IS_OPENED'
export const SET_LOGIN_INFO = 'SET_LOGIN_INFO'
export const LOGIN = 'LOGIN'

Saga 处理异步请求

Saga 在处理异步请求时有很多种方式,因项目不同,可以采用不同的方式,这里我们选用了官方推荐的最佳实践:

  • watcherSaga 监听异步的 action
  • handlerSaga 处理异步的 action
  • dispatch 同步的 action,更新异步 action 成功/失败的状态

运用最近实践之后,之前的 Redux 数据流动图就变成了下面这样子:

好了,讲解了 redux-saga 处理异步 Action 的最佳实践之后,我们马上来运用最佳实践来编写处理异步 Action 的 Saga 文件。

在我们的应用中可能涉及到多个异步请求,所以 redux-saga 推荐的最佳实践是单独创建一个 sagas 文件夹,来存放所有处理异步请求的 sagas 文件,以及可能用到的辅助文件。

在上一步中,我们从 view 中发出了 LOGIN 异步登录请求,接下来我们要编写对应处理这个 LOGIN 请求的 saga 文件,在 src 文件夹下创建 sagas 文件夹,并在其中创建 user.js,在其中编写如下内容:

src/sagas/user.js查看完整代码
import Taro from '@tarojs/taro'
import { call, put, take, fork } from 'redux-saga/effects'

import { userApi } from '../api'
import {
SET_LOGIN_INFO,
LOGIN_SUCCESS,
LOGIN,
LOGIN_ERROR,
SET_IS_OPENED,
} from '../constants'

/***************************** 登录逻辑开始 ************************************/

function* login(userInfo) {
try {
const user = yield call(userApi.login, userInfo)

// 将用户信息缓存在本地
yield Taro.setStorage({ key: 'userInfo', data: user })

// 其实以下三步可以合成一步,但是这里为了讲解清晰,将它们拆分成独立的单元

// 发起登录成功的 action
yield put({ type: LOGIN_SUCCESS })

// 关闭登录框弹出层
yield put({ type: SET_IS_OPENED, payload: { isOpened: false } })

// 更新 Redux store 数据
const { nickName, avatar, _id } = user
yield put({
type: SET_LOGIN_INFO,
payload: { nickName, avatar, userId: _id },
})

// 提示登录成功
Taro.atMessage({ type: 'success', message: '恭喜您!登录成功!' })
} catch (err) {
console.log('login ERR: ', err)

// 登录失败,发起失败的 action
yield put({ type: LOGIN_ERROR })

// 提示登录失败
Taro.atMessage({ type: 'error', message: '很遗憾!登录失败!' })
}
}

function* watchLogin() {
while (true) {
const { payload } = yield take(LOGIN)

console.log('payload', payload)

yield fork(login, payload.userInfo)
}
}

/***************************** 登录逻辑结束 ************************************/

export { watchLogin }

可以看到,上面的改动主要是创建 watcherSagahandlerSaga

创建 **watcherSaga**

  • 我们创建了登录的 watcherSagawatchLogin,它用来监听 action.typeLOGIN 的 action,并且当监听到 LOGIN action 之后,从这个 action 中获取必要的 userInfo 数组,然后激活 handlerSagalogin 去处理对应的登录逻辑。
  • 这里的 watcherSagawatchLogin 是一个生成器函数,它内部是一个 while 无限循环,表示在内部持续监听 LOGIN action。
  • 在循环内部,我们使用了 redux-saga 提供的 effects helper 函数:take,它用于监听 LOGIN action,获取 action 中携带的数据。
  • 接着我们使用了另外一个 effects helper 函数:fork,它表示非阻塞的执行 handlerSagalogin,并将 payload.userInfo 作为参数传给 login

创建 **handlerSaga**

  • 我们创建了登录的 handlerSagalogin,它用来处理登录逻辑。
  • login 也是一个生成器函数,在它内部是一个 try/catch 语句,用于处理登录请求可能存在的错误情况。
  • try 语句中,首先是使用了 redux-saga 提供给我们的 effects helper 函数:call 来调用登录的 API:userApi.login,并把 userInfo 作为参数传给这个 API。
  • 接着,我们使用 redux-saga 提供的 effects helpers 函数:putput 类似之前在 view 中的 dispatch 操作,,来 dispatch 了三个 action:LOGIN_SUCCESSSET_IS_OPENEDSET_LOGIN_INFO,代表更新登录成功的状态,关闭登录框,设置登录信息到 Redux Store 中。
  • 最后我们使用了 Taro UI 提供给我们的消息框,来显示一个 success 消息。

如果登录失败,我们则使用 put 发起一个 LOGIN_ERROR 的 action 来更新登录失败的信息到 Redux Store,接着使用了 Taro UI 提供给我们的消息框,来显示一个 error 消息。

提示

对生成器函数不了解的同学可以看一下这篇文档:迭代器和生成器

一些额外的工作

为了创建 watcherSagahandlerSaga,我们还导入了 userApi,我们将在后面来创建这个 API。

除此之外我们还导入了需要使用的 action 常量:

  • SET_LOGIN_INFO:设置登录信息
  • LOGIN_SUCCESS:更新登录成功信息
  • LOGIN:监听登录动作
  • LOGIN_ERROR:更新登录失败信息
  • SET_IS_OPENED:设置登录框开启/关闭的信息

我们还从 redux-saga/effects 包中导入了必要的函数:

  • call:在 saga 函数中调用其他异步/同步函数,获取结果
  • put:类似 dispatch,用于在 saga 函数中发起 action
  • take:在 saga 函数中监听 action,并获取对应 action 所携带的数据
  • fork:在 saga 函数中无阻塞的调用 handlerSaga,即调用之后,不会阻塞后续的执行逻辑。

最后,我们导出了 watchLogin

创建 saga 中心调度文件

我们在上一步中导出了 watchLogin,它类似 reducers 里面的单个 reducer 函数,我们还需要有类似 combineReducers 组合 reducer 一样来组合所以的 watcherSaga

src/sagas 文件夹下创建 index.js 文件,并在其中编写如下的内容:

import { fork, all } from 'redux-saga/effects'

import { watchLogin } from './user'

export default function* rootSaga() {
yield all([
fork(watchLogin)
])
}

可以看到,上面的文件主要有三处改动:

  • 我们从 redux-saga/effects 导出了 effects helper 函数 forkall
  • 接着我们从 user.js saga 中导入了 watchLogin
  • 最后我们导出了一个 rootSaga,它是调度所有 sagas 函数的中心,通过在 all 函数中传入一个数组,并且 fork 非阻塞的执行 watchLogin,进而开始监听和分发异步的 Action,一旦监听到 LOGIN action,则激活 watchLogin 里面的处理逻辑。

提示

目前 all 函数接收的数组还只有 fork(watchLogin),等到后续加入 post 的异步逻辑时,还会给数组增加多个 fork(watcherSaga)

添加 action 常量

因为在上一步的 user saga 文件中,我们使用到了一些还未定义的常量,所以接下来我们马上来定义它们,打开 src/constants/user.js,在其中添加对应的常量如下:

src/constants/user.js查看完整代码
export const SET_IS_OPENED = 'MODIFY_IS_OPENED'
export const SET_LOGIN_INFO = 'SET_LOGIN_INFO'
[tuture-add]
[tuture-add]export const LOGIN = 'LOGIN'
[tuture-add]export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
[tuture-add]export const LOGIN_ERROR = 'LOGIN_ERROR'
[tuture-add]export const LOGIN_NORMAL = 'LOGIN_NORMAL'

可以看到,上面除了我们在 “saga 处理异步请求” 中使用到的常量之外,还多了一个 LOGIN_NORMAL 常量,它主要是用于设置登录状态的默认状态的常量。

实现请求 login API

在之前的 user saga 文件里面,我们使用到了 userApi,它里面封装了用于向后端(这里我们是小程序云)发起请求的逻辑,让我们马上来实现它吧。

我们统一将所有的 API 文件放到 api 文件夹里面,这便于我们日后的代码维护工作,在 src 文件夹下创建 api 文件夹,在其中添加 user.js 文件,并在文件中编写内容如下:

src/api/user.js查看完整代码
import Taro from '@tarojs/taro'

async function login(userInfo) {
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: 'login',
data: {
userInfo,
},
})

return result.user
}
} catch (err) {
console.error('login ERR: ', err)
}
}

const userApi = {
login,
}

export default userApi

在上面的代码中,我们定义了 login 函数,它是一个 async 函数,用来处理异步逻辑,在 login 函数中,我们对当前的环境进行了判断,且只在微信小程序,即 isWeapp 的条件下执行登录的操作,对于支付宝小程序和 H5,我们则放在下一节使用 LeanCloud 的 Serverless 来解决。

登录逻辑是一个 try/catch 语句,用于捕捉可能存在的请求错误,在 try 代码块中,我们使用了 Taro 为我们提供的微信小程序云的云函数 API Taro.cloud.callFunction 来便捷的向小程序云发起云函数调用请求,它的调用体是一个类似下面结构的对象:

{
name: '', // 需要调用的云函数名
data: {} // 需要传递给云函数的数据
}

这里我们调用了一个 login 云函数,并将 userInfo 作为参数传给云函数,用于在云函数中使用用户信息来注册用户并保存到数据库,我们将在下一节中实现这个云函数。

提示

想了解更多关于微信小程序云函数的内容,可以查阅微信小程序云函数文档:文档地址

如果调用成功,我们可以接收返回值,用于从后端返回数据,这里我们使用解构的方法,从返回体里面拿到了 result 对象,然后取出其中的 user 对象并作为 login API 函数的返回值。

如果调用失败,则打印错误。

最后我们定义了一个 userApi 对象,用于存放所有和用户逻辑有个的函数,并添加 login API 属性然后将其导出,这样在 user saga 函数里面就可以导入 userApi 然后通过 userApi.login 的方式来调用 login API 处理登录逻辑了。

创建 API 默认导出文件

我们创建了 src/api/user.js 文件,我们需要建立一个统一的导出所有 API 文件的默认文件,方便统一分发所有的 API,在 src/api 文件夹下建立 index.js 文件,并在其中编写如下内容:

import userApi from './user'
export { userApi }

可以看到,我们从 user.js 里面默认导出了 userApi,并将其加为 export 导出的对象的属性。

配置云函数开发环境

我们在上一小节中使用 Taro 为我们提供的云函数 API 调用了一个 login 云函数,现在我们马上来实现这个云函数。

微信小程序文档中要求我们在项目根目录下面建立一个一个存储云函数的文件夹,然后在 project.config.jsoncloudfunctionRoot 字段的值指定为这个目录,这样小程序开发者工具就可以识别此目录为存放云函数的目录,并做特殊的标志处理。

我们在项目根目录下创建了一个 functions 文件夹,它与 src 文件夹是同级的:

.
├── LICENSE
├── README.md
├── config
├── dist
├── functions
├── node_modules
├── package.json
├── project.config.json
├── src
├── tuture-assets
├── tuture-build
├── tuture.yml
└── yarn.lock

接着我们在根目录的 project.config.json 文件中添加 cloudfunctionRoot 字段,并将其设置为 'functions/' 如下:

project.config.json查看完整代码
{
[tuture-del] "miniprogramRoot": "dist/",
[tuture-del] "projectname": "ultra-club",
[tuture-del] "description": "",
[tuture-del] "appid": "touristappid",
[tuture-del] "setting": {
[tuture-del] "urlCheck": true,
[tuture-del] "es6": false,
[tuture-del] "postcss": false,
[tuture-del] "minified": false
[tuture-del] },
[tuture-del] "compileType": "miniprogram",
[tuture-del] "simulatorType": "wechat",
[tuture-del] "simulatorPluginLibVersion": {},
[tuture-del] "condition": {}
[tuture-del]}
[tuture-add] "miniprogramRoot": "dist/",
[tuture-add] "projectname": "ultra-club",
[tuture-add] "description": "",
[tuture-add] "appid": "",
[tuture-add] "cloudfunctionRoot": "functions/",
[tuture-add] "setting": {
[tuture-add] "urlCheck": true,
[tuture-add] "es6": false,
[tuture-add] "postcss": false,
[tuture-add] "minified": false
[tuture-add] },
[tuture-add] "compileType": "miniprogram",
[tuture-add] "simulatorType": "wechat",
[tuture-add] "simulatorPluginLibVersion": {},
[tuture-add] "cloudfunctionTemplateRoot": "cloudfunctionTemplate",
[tuture-add] "condition": {}
[tuture-add]}

可以看到,当我们创建了上面的文件夹并设置了 project.config.json 之后,我们的小程序开发者工具会变成下面这个样子:

我们创建的那个 functions 文件夹多了一个额外的云图标,并且文件夹的命名从 functions 变成了 functions | ultra-club,竖杠右边的是我们当前的小程序环境。

并且当我们在小程序开发者工具里面右键点击这个 functions 文件夹时,会出现菜单弹框,允许我们进行云函数相关的操作:

我们可以看到有很多操作,这里我们主要会用到如下几个操作:

  • 新建 Node.js 云函数
  • 开启云函数本地调试

提示

其它的操作等你走完整个小程序云开发的流程之后,当需要编写更加复杂的业务逻辑时都会遇到,具体可以参考小程序云的文档:文档地址

提示

必须先开通小程序云开发环境才能使用云函数。具体步骤可以参考我们在 “开通小程序云” 这一节中的讲解。

创建 login 云函数

讲解了微信小程序云函数的配置,终于到了创建云函数的阶段了,我们在小程序开发者工具中右键点击 functions 文件夹,然后选择新建 Node.js 云函数,输入 login,然后回车创建,会看到小程序开发者工具自动帮我们创建了如下的代码文件:

可以看到,一个云函数是一个独立的 Node.js 模块,它处理一类逻辑。

我们先来看一下 package.json 文件如下:

functions/login/package.json查看完整代码
{
"name": "login",
"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"
}
}

可以看到,在添加云函数时,小程序开发者工具默认为我们添加了一项 wx-server-sdk 依赖,我们在云函数中需要用到它内置的相关 API 来操作小程序云。

为了使这个 Node.js 云函数/项目跑起来,我们需要安装依赖,进入 functions/login 目录,在目录下运行 npm install 命令来安装依赖。

了解默认生成的云函数

当创建了云函数,并安装了依赖之后,我们马上来揭开云函数的神秘面纱,打开 functions/login/index.js,可以看到如下代码:

// 云函数入口文件
const cloud = require('wx-server-sdk')

cloud.init()

// 云函数入口函数
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext()

return {
event,
openid: wxContext.OPENID,
appid: wxContext.APPID,
unionid: wxContext.UNIONID,
}
}

可以看到,默认生成的代码主要做了下面几项工作:

  • 导入 wx-server-sdk 包,并命名为 cloud,所有我们需要操作小程序云的方法都绑定在 cloud 对象上。
  • 接着调用 cloud.init() 来初始化云函数的云开发环境,我们将在后续实现 login 逻辑时设置环境。
  • 最后是云函数的入口函数,它默认以 main 函数作为导出函数,是一个 async 函数,我们可以在函数内部以同步的方式处理异步逻辑,可以看到,这个函数接收两个参数:eventcontextevent 指的是触发云函数的事件,当小程序端调用云函数时,event 就是小程序端调用云函数时传入的参数,外加后端自动注入的小程序用户的 openid 和小程序的 appidcontext 对象包含了此处调用的调用信息和运行状态,可以用它来了解服务运行的情况。默认生成的函数内部代码主要是获取了此时微信上下文信息,然后与 event 对象一同返回,这样当我们在小程序端以 Taro.cloud.callFunction 调用这个函数获得的返回值就是包含微信上下文信息和 event 的对象。

编写 login 云函数

了解了云函数的具体逻辑,我们马上在云函数中来实现我们具体的登录逻辑,打开 functions/login/index.js,对其中的代码做出对应的修改如下:

functions/login/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 { userInfo } = event

console.log('event', event)

try {
const { data } = await db
.collection('user')
.where({
nickName: userInfo.nickName,
})
.get()

if (data.length > 0) {
return {
user: data[0],
}
} else {
const { _id } = await db.collection('user').add({
data: {
...userInfo,
createdAt: db.serverDate(),
updatedAt: db.serverDate(),
},
})

const user = await db.collection('user').doc(_id)

return {
user,
}
}
} catch (err) {
console.error(`login ERR: ${err}`)
}
}

可以看到上面的代码改动主要有以下六处:

  • 首先我们给 cloud.init() 传入了环境参数,我们使用了内置的 cloud.DYNAMIC_CURRENT_ENV,表示自动设置为当前的云环境,即在右键点击小程序开发者工具里 functions 文件夹时选择的环境。
  • 接着,我们通过 cloud.database() 生成了数据实例 db,用于之后在函数体中便捷的操作云数据库。
  • 接着就是 main 函数体,我们首先从 event 对象中取到了在小程序的调用 Taro.cloud.callFunction 传过来的 userInfo 数据。
  • 然后,跟着取数据的是一个 try/catch 语句块,用于捕获错误,在 try 语句块中,我们使用 db 的查询操作:db.collection('user').where().get(),表示查询 where 条件的 user 表数据,它查出来应该是个数组,如果不存在满足 where 条件的,那么是一个空数组,如果存在满足 where 条件的,那么返回一个 user 数组。
  • 接着,我们判断是否查询出来的用户数组为空,如果为空表示用户还未注册过,则创建一个新用户,如果不为空,那么返回查询到的第一个元素。
  • 这里我们使用的 db.collection('user').add(),用于添加一个 user 数据,然后在 add 方法中传入 data 字段,表示设置此用户的初始值,这里我们额外使用了 db.serverDate() 用于记录创建此用户的时间和更新此用户的时间,方便之后做条件查询;因为向数据库添加一个记录之后只会返回此记录的 _id,所以我们需要一个额外的操作 db.collection('user').doc() 来获取此条记录,这个 doc 用于获取指定的记录引用,返回的是这条数据,而不是一个数组。

提示

这里关于云数据库的相关操作,可以查阅微信小程序云文档,在文档里提供了详尽的实例:数据库文档

适配异步 action 的 reducer

我们在前面处理登录时,在组件内部 dispatchLOGIN action,在处理异步 action 的 saga 函数中,使用 put 发起了一系列更新 store 中登录状态的 action,现在我们马上来实现响应这些 action 的 reducers,打开 src/reducers/user.js,对其中的代码做出对应的修改如下:

src/reducers/user.js查看完整代码
[tuture-del]import { SET_LOGIN_INFO, SET_IS_OPENED } from '../constants/'
[tuture-add]import {
[tuture-add] SET_LOGIN_INFO,
[tuture-add] SET_IS_OPENED,
[tuture-add] LOGIN_SUCCESS,
[tuture-add] LOGIN,
[tuture-add] LOGIN_ERROR,
[tuture-add] LOGIN_NORMAL,
[tuture-add]} from '../constants/'

const INITIAL_STATE = {
[tuture-add] userId: '',
avatar: '',
nickName: '',
isOpened: false,
[tuture-add] isLogin: false,
[tuture-add] loginStatus: LOGIN_NORMAL,
}

export default function user(state = INITIAL_STATE, action) {
// ...
}

case SET_LOGIN_INFO: {
[tuture-del] const { avatar, nickName } = action.payload
[tuture-add] const { avatar, nickName, userId } = action.payload

[tuture-del] return { ...state, nickName, avatar }
[tuture-add] return { ...state, nickName, avatar, userId }
[tuture-add] }
[tuture-add]
[tuture-add] case LOGIN: {
[tuture-add] return { ...state, loginStatus: LOGIN, isLogin: true }
[tuture-add] }
[tuture-add]
[tuture-add] case LOGIN_SUCCESS: {
[tuture-add] return { ...state, loginStatus: LOGIN_SUCCESS, isLogin: false }
[tuture-add] }
[tuture-add]
[tuture-add] case LOGIN_ERROR: {
[tuture-add] return { ...state, loginStatus: LOGIN_ERROR, isLogin: false }
}

default:
// ...

看一看到上面的代码主要有三处改动:

  • 首先我们导入了必要的 action 常量
  • 接着我们给 INITIAL_STATE 增加了几个字段:
  • isLogin:用于标志登录过程中是否在执行登录逻辑,true 表示正在执行登录中,false 表示登录逻辑执行完毕
  • loginStatus:用于标志登录过程中的状态:开始登录(LOGIN)、登录成功(LOGIN_SUCCESS)、登录失败(LOGIN_ERROR

最后就是 switch 语句中响应 action,更新相应的状态。

收尾 User 剩下的异步逻辑

微信登录

我们在上一节 “实现 Redux 异步逻辑” 中,着重实现了普通登录按钮的异步逻辑,现在我们来收尾一下使用微信登录的逻辑。打开 src/components/WeappLoginButton/index.js 文件,对其中的内容作出对应的修改如下:

src/components/WeappLoginButton/index.js查看完整代码
// ...
import { useDispatch } from '@tarojs/redux'

import './index.scss'
[tuture-del]import { SET_LOGIN_INFO } from '../../constants'
[tuture-add]import { LOGIN } from '../../constants'

export default function WeappLoginButton(props) {
const [isLogin, setIsLogin] = useState(false)
// ...
setIsLogin(true)

const { avatarUrl, nickName } = e.detail.userInfo
[tuture-del]
[tuture-del] await Taro.setStorage({
[tuture-del] key: 'userInfo',
[tuture-del] data: { avatar: avatarUrl, nickName },
[tuture-del] })
[tuture-add] const userInfo = { avatar: avatarUrl, nickName }

dispatch({
[tuture-del] type: SET_LOGIN_INFO,
[tuture-add] type: LOGIN,
payload: {
[tuture-del] avatar: avatarUrl,
[tuture-del] nickName,
[tuture-add] userInfo: userInfo,
},
})

// ...

可以看到,上面的代码主要有一下三处改动:

  • 我们删掉了之前直接设置登录信息的 SET_LOGIN_INFO 常量,取而代之的是 LOGIN 常量。
  • 接着我们删掉了直接设置 storage 缓存的代码逻辑
  • 最后,我们将之前发起 SET_LOGIN_INFO action 的逻辑改为了发起 LOGIN 异步 action,来处理登录,并且组装了 userInfo 对象作为 payload 对象的属性。

因为我们在上一节 “实现 Redux 异步逻辑” 中已经处理了 LOGIN 的整个异步数据流逻辑,所以这里只需要 dispatch 对应的 LOGIN action 就可以处理微信登录的异步逻辑了。

优化 user 逻辑顶层组件

最后,我们来收尾一下 user 逻辑的顶层组件,mine 页面,打开 src/pages/mine/mine.jsx,对其中的内容作出对应的修改如下:

src/pages/mine/mine.jsx查看完整代码
import Taro, { useEffect } from '@tarojs/taro'
import { View } from '@tarojs/components'
[tuture-del]import { useDispatch } from '@tarojs/redux'
[tuture-add]import { useDispatch, useSelector } from '@tarojs/redux'

import { Header, Footer } from '../../components'
import './mine.scss'
// ...

export default function Mine() {
const dispatch = useDispatch()
[tuture-add] const nickName = useSelector(state => state.user.nickName)
[tuture-add]
[tuture-add] const isLogged = !!nickName

useEffect(() => {
async function getStorage() {
try {
const { data } = await Taro.getStorage({ key: 'userInfo' })

[tuture-del] const { nickName, avatar } = data
[tuture-add] const { nickName, avatar, _id } = data

// 更新 Redux Store 数据
[tuture-del] dispatch({ type: SET_LOGIN_INFO, payload: { nickName, avatar } })
[tuture-add] dispatch({
[tuture-add] type: SET_LOGIN_INFO,
[tuture-add] payload: { nickName, avatar, userId: _id },
[tuture-add] })
} catch (err) {
console.log('getStorage ERR: ', err)
}
}

[tuture-del] getStorage()
[tuture-add] if (!isLogged) {
[tuture-add] getStorage()
[tuture-add] }
})

return (
// ...

可以看到,我们对上面的代码做出了三处修改如下:

  • 首先我们导出了 useSelector Hooks,从 Redux Store 里获取到了 nickName
  • 接着,因为我们在 “实现 Redux 异步逻辑” 一节中,保存了 userId 到 Redux Store 的 user 逻辑部分,所以这里我们从 storage 获取到了 _id,然后给之前的 SET_LOGIN_INFOpayload 带上了 userId 属性。
  • 最后,我们判断一下 getStorage 的逻辑,只有当此时 Redux Store 里面没有数据时,我们才去获取 storage 里面的数据来更新 Redux Store。

扩充 Logout 的清空数据范围

因为在 Redux Store 里面的 user 属性中多出了一个 userId 属性,所以我们在 Logout 组件里 dispatch action 时,要清空 userId 如下:

src/components/Logout/index.js查看完整代码
// ...
payload: {
avatar: '',
nickName: '',
[tuture-add] userId: '',
},
})
} catch (err) {
// ...

小结

大功告成!到这里我们就把 user 逻辑接入了小程序云,并能成功实现微信小程序端的小程序云登录,让我们马上来尝试一下预览本地调试时的效果预览图:

可以看到,我们在本地调试云函数,以及小程序端接入云函数的步骤如下:

  • 我们首先右键点击 functions 文件夹,开启了 “云函数本地调试”。
  • 接着选中我们的 login 云函数,然后点击开启本地调试,这样我们就可以在本地调试云函数了。
  • 接着我们在小程序端点击微信登录,然后我们会看到小程序开发者工具控制台和云函数调试控制台都会答应此时云函数的运行情况。
  • 最后,我们登陆成功,成功在小程序端显示了登录的昵称和头像,并且检查云开发 > 数据库 > user 表,它确实增加了一个对应的 user 记录,说明我们成功接通了小程序端和小程序云。

一般在本地调试完后,我们就可以将云函数上传到云端,这样,我们就可以不用开启本地调试才能使用云函数了,这对于发布上线的小程序是必须的,具体上传云函数可以在小程序开发者工具中右键点击 functions 文件夹下对应的云函数,然后选择 “上传并部署:云端安装所以依赖”:

在这篇教程中,我们实现了 User 逻辑的异步流程,在下一篇教程中,我们将实现 Post 逻辑的异步流程,敬请期待!

“六脉神剑” 搞定 createPost 异步逻辑

在上一篇文章中,我们将我们两大逻辑之一 User 部分接入了 Redux 异步处理流程,接着接入了微信小程序云,使得 User 逻辑可以在云端永久保存,好不自在:),两兄弟一个得了好处,另外一个不能干瞪眼对吧?在这一篇教程中,我们想办法把 User 另外一个兄弟 Post 捞上来,也把 Redux 异步流程和微信小程序给它整上,这样就齐活了😆

我们首先来看一看最终的完成效果:

不知道看到这里的读者有没有发现上篇文章其实打造了一套讲解模式,即按照如下的 “六步流程” 来讲解,我们也称为 “六脉神剑” 讲解法:

  • 将组件中的同步逻辑重构到异步逻辑
  • 声明和补充对应需要的异步 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 ,对其中的内容作出对应的修改如下:

src/components/PostForm/index.jsx查看完整代码
// ...
import { useDispatch, useSelector } from '@tarojs/redux'

import './index.scss'
[tuture-del]import { SET_POSTS, SET_POST_FORM_IS_OPENED } from '../../constants'
[tuture-add]import { CREATE_POST } from '../../constants'

[tuture-del]export default function PostForm(props) {
[tuture-add]export default function PostForm() {
const [formTitle, setFormTitle] = useState('')
const [formContent, setFormContent] = useState('')

[tuture-del] const nickName = useSelector(state => state.user.nickName)
[tuture-del] const avatar = useSelector(state => state.user.avatar)
[tuture-add] const userId = useSelector(state => state.user.userId)

const dispatch = useDispatch()

// ...
}

dispatch({
[tuture-del] type: SET_POSTS,
[tuture-add] type: CREATE_POST,
payload: {
[tuture-del] post: {
[tuture-add] postData: {
title: formTitle,
content: formContent,
[tuture-del] user: { nickName, avatar },
},
[tuture-add] userId,
},
})

setFormTitle('')
setFormContent('')
[tuture-del]
[tuture-del] dispatch({
[tuture-del] type: SET_POST_FORM_IS_OPENED,
[tuture-del] payload: { isOpened: false },
[tuture-del] })
[tuture-del]
[tuture-del] Taro.atMessage({
[tuture-del] message: '发表文章成功',
[tuture-del] type: 'success',
[tuture-del] })
}

return (
// ...

可以看到,上面的内容做了如下四处修改:

  • 首先我们现在是接收用户的文章输入数据然后向小程序云发起创建文章的请求,所以我们将之前的 dispatch SET_POSTS Action 改为 CREATE_POST Action,并且将之前的 action payload 简化为 postDatauserId,因为我们可以通过小程序云数据库查询 userId 得到创建文章的用户信息,所以不需要再携带用户的数据。
  • 接着,因为我们不再需要用户的 avatarnickName 数据,所以我们删掉了对应的 useSelector 语句。
  • 接着,因为请求是异步的,所以需要等待请求完成之后再设置对应的发表文章的状态以及发表文章弹出层状态,所以我们删掉了对应的 dispatch SET_POST_FORM_IS_OPENED Action 逻辑以及 Taro.atMessage 逻辑。
  • 最后我们删掉不需要的常量 SET_POSTSSET_POST_FORM_IS_OPENED,然后导入异步创建文章的常量 CREATE_POST

增加 Action 常量

我们在上一步中使用到了 CREATE_POST 常量,打开 src/constants/post.js,在其中增加 CREATE_POST 常量:

export const CREATE_POST = 'CREATE_POST'

到这里,我们的 “六步流程” 讲解法就走完了第一步,即从组件中发起对应的异步请求,这里我们是发出的 action.typeCREATE_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 }

可以看到,上面的改动主要是创建 watcherSagahandlerSaga

创建 **watcherSaga**

  • 我们创建了登录的 watcherSagawatchCreatePost,它用来监听 action.typeCREATE_POST 的 action,并且当监听到 CREATE_POST action 之后,从这个 action 中获取必要的 postDatauserId 数据,然后激活 handlerSagacreatePost 去处理对应的创建帖子的逻辑。
  • 这里的 watcherSagawatchCreatePost 是一个生成器函数,它内部是一个 while 无限循环,表示在内部持续监听 CREATE_POST action。
  • 在循环内部,我们使用了 redux-saga 提供的 effects helper 函数:take,它用于监听 CREATE_POST action,获取 action 中携带的数据。
  • 接着我们使用了另外一个 effects helper 函数:fork,它表示非阻塞的执行 handlerSagacreatePost,并将 payload.postDatapayload.userId 作为参数传给 createPost

创建 **handlerSaga**

  • 我们创建了创建帖子的 handlerSagacreatePost,它用来处理创建逻辑。
  • createPost 也是一个生成器函数,在它内部是一个 try/catch 语句,用于处理创建帖子请求可能存在的错误情况。
  • try 语句中,首先是使用了 redux-saga 提供给我们的 effects helper 函数:call 来调用登录的 API:postApi.createPost,并把 postDatauserId 作为参数传给这个 API。
  • 最后我们使用了 Taro UI 提供给我们的消息框,来显示一个 success 消息。

如果发帖失败,我们则使用 put 发起一个 POST_ERROR 的 action 来更新创建帖子失败的信息到 Redux Store,接着使用了 Taro UI 提供给我们的消息框,来显示一个 error 消息。

一些额外的工作

为了创建 watcherSagahandlerSaga,我们还导入了 postApi,我们将在后面来创建这个 API。

除此之外我们还导入了需要使用的 action 常量:

  • POST_SUCCESS:设置处理帖子逻辑成功信息
  • POST_ERROR:设置处理帖子逻辑失败信息
  • SET_POSTS:将新帖子添加到 Redux Store
  • CREATE_POST: 相应创建帖子的常量
  • SET_POST_FORM_IS_OPENED:更新发帖框的开闭逻辑

这里的 POST_SUCCESSPOST_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 云函数,并将 postDatauserId 作为参数传给云函数,用于在云函数中使用用户 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 文件,它和之前的类似。

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 文件,对其中的内容作出对应的修改如下:

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 传过来的 postDatauserId 数据。
  • 然后,跟着取数据的是一个 try/catch 语句块,用于捕获错误,在 try 语句块中,我们使用 db 的查询操作:db.collection('user').doc(userId).get(),表示查询 iduserIduser 表数据,它查出来应该是个唯一值,如果不存在满足 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 文件

我们在前面处理创建帖子时,在组件内部 dispatchCREATE_POST action,在处理异步 action 的 saga 函数中,使用 put 发起了一系列更新 store 中帖子状态的 action,现在我们马上来实现响应这些 action 的 reducers,打开 src/reducers/post.js,对其中的代码做出对应的修改如下:

src/reducers/post.js查看完整代码
[tuture-del]import { SET_POSTS, SET_POST_FORM_IS_OPENED } from '../constants/'
[tuture-add]import {
[tuture-add] SET_POST,
[tuture-add] SET_POSTS,
[tuture-add] SET_POST_FORM_IS_OPENED,
[tuture-add] POST_ERROR,
[tuture-add] CREATE_POST,
[tuture-add] POST_NORMAL,
[tuture-add] POST_SUCCESS,
[tuture-add]} from '../constants/'

import avatar from '../images/avatar.png'

const INITIAL_STATE = {
[tuture-del] posts: [
[tuture-del] {
[tuture-del] title: '泰罗奥特曼',
[tuture-del] content: '泰罗是奥特之父和奥特之母唯一的亲生儿子',
[tuture-del] user: {
[tuture-del] nickName: '图雀酱',
[tuture-del] avatar,
[tuture-del] },
[tuture-del] },
[tuture-del] ],
[tuture-add] posts: [],
[tuture-add] post: {},
isOpened: false,
[tuture-add] isPost: false,
[tuture-add] postStatus: POST_NORMAL,
}

export default function post(state = INITIAL_STATE, action) {
switch (action.type) {
[tuture-del] case SET_POSTS: {
[tuture-add] case SET_POST: {
const { post } = action.payload
[tuture-del] return { ...state, posts: state.posts.concat(post) }
[tuture-add] return { ...state, post }
[tuture-add] }
[tuture-add]
[tuture-add] case SET_POSTS: {
[tuture-add] const { posts } = action.payload
[tuture-add] return { ...state, posts: state.posts.concat(...posts) }
}

case SET_POST_FORM_IS_OPENED: {
// ...
return { ...state, isOpened }
}

[tuture-add] case CREATE_POST: {
[tuture-add] return { ...state, postStatus: CREATE_POST, isPost: true }
[tuture-add] }
[tuture-add]
[tuture-add] case POST_SUCCESS: {
[tuture-add] return { ...state, postStatus: POST_SUCCESS, isPost: false }
[tuture-add] }
[tuture-add]
[tuture-add] case POST_ERROR: {
[tuture-add] return { ...state, postStatus: POST_ERROR, isPost: false }
[tuture-add] }
[tuture-add]
default:
return state
}
// ...

看一看到上面的代码主要有三处改动:

  • 首先我们导入了必要的 action 常量
  • 接着我们给 INITIAL_STATE 增加了几个字段:
  • post:保存单个帖子,我们将在之后讲解用于在获取帖子详情时保存数据用。

isPost:用于标志帖子逻辑过程中是否在执行创帖逻辑,true 表示正在执行创帖中,false 表示登录逻辑执行完毕

最后就是 switch 语句中响应 action,更新相应的状态。

“六脉神剑” 搞定 getPosts 异步逻辑

在上一 “大” 节中,我们使用了图雀社区不传之术:“六脉神剑” 搞定了 createPost 的异步逻辑,现在我们马上趁热打铁来巩固我们的武功,搞定 getPosts 异步逻辑,它对应着我们小程序底部两个 tab 栏的第一个,也就是我们打开小程序的首屏渲染逻辑,也就是一个帖子列表。

第一剑:index 组件中发起异步请求

打开 src/pages/index/index.jsx 文件,对其中的内容作出对应的修改如下:

src/pages/index/index.jsx查看完整代码
// ...

import { PostCard, PostForm } from '../../components'
import './index.scss'
[tuture-del]import { SET_POST_FORM_IS_OPENED, SET_LOGIN_INFO } from '../../constants'
[tuture-add]import {
[tuture-add] SET_POST_FORM_IS_OPENED,
[tuture-add] SET_LOGIN_INFO,
[tuture-add] GET_POSTS,
[tuture-add]} from '../../constants'

export default function Index() {
const posts = useSelector(state => state.post.posts) || []
// ...
const dispatch = useDispatch()

useEffect(() => {
[tuture-add] const WeappEnv = Taro.getEnv() === Taro.ENV_TYPE.WEAPP
[tuture-add]
[tuture-add] if (WeappEnv) {
[tuture-add] Taro.cloud.init()
[tuture-add] }
[tuture-add]
async function getStorage() {
try {
const { data } = await Taro.getStorage({ key: 'userInfo' })

[tuture-del] const { nickName, avatar } = data
[tuture-add] const { nickName, avatar, _id } = data

// 更新 Redux Store 数据
[tuture-del] dispatch({ type: SET_LOGIN_INFO, payload: { nickName, avatar } })
[tuture-add] dispatch({
[tuture-add] type: SET_LOGIN_INFO,
[tuture-add] payload: { nickName, avatar, userId: _id },
[tuture-add] })
} catch (err) {
console.log('getStorage ERR: ', err)
}
}

[tuture-del] getStorage()
[tuture-del] })
[tuture-add] if (!isLogged) {
[tuture-add] getStorage()
[tuture-add] }
[tuture-add]
[tuture-add] async function getPosts() {
[tuture-add] try {
[tuture-add] // 更新 Redux Store 数据
[tuture-add] dispatch({
[tuture-add] type: GET_POSTS,
[tuture-add] })
[tuture-add] } catch (err) {
[tuture-add] console.log('getPosts ERR: ', err)
[tuture-add] }
[tuture-add] }
[tuture-add]
[tuture-add] if (!posts.length) {
[tuture-add] getPosts()
[tuture-add] }
[tuture-add] }, [])

function setIsOpened(isOpened) {
dispatch({ type: SET_POST_FORM_IS_OPENED, payload: { isOpened } })
// ...
return (
<View className="index">
<AtMessage />
[tuture-del] {posts.map((post, index) => (
[tuture-del] <PostCard key={index} postId={index} post={post} isList />
[tuture-add] {posts.map(post => (
[tuture-add] <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 接收的 keypostId 属性变成了真实的帖子 _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 }

可以看到,上面的改动主要是创建 watcherSagahandlerSaga

创建 **watcherSaga**

  • 我们创建了登录的 watcherSagawatchGetPosts,它用来监听 action.typeGET_POSTS 的 action,并且当监听到 GET_POSTS action 之后,然后激活 handlerSagagetPosts 去处理对应的获取帖子列表的逻辑。
  • 这里的 watcherSagawatchGetPosts 是一个生成器函数,它内部是一个 while 无限循环,表示在内部持续监听 GET_POSTS action。
  • 在循环内部,我们使用了 redux-saga 提供的 effects helper 函数:take,它用于监听 GET_POSTS action,获取 action 中携带的数据。
  • 接着我们使用了另外一个 effects helper 函数:fork,它表示非阻塞的执行 handlerSagagetPosts,因为这里获取帖子列表不需要传数据,所以这里没有额外的数据传递逻辑。

创建 **handlerSaga**

  • 我们创建了创建帖子的 handlerSagagetPosts,它用来处理创建逻辑。
  • getPosts 也是一个生成器函数,在它内部是一个 try/catch 语句,用于处理获取帖子列表请求可能存在的错误情况。
  • try 语句中,首先是使用了 redux-saga 提供给我们的 effects helper 函数:call 来调用登录的 API:postApi. getPosts

如果获取帖子列表失败,我们则使用 put 发起一个 POST_ERROR 的 action 来更新获取帖子列表失败的信息到 Redux Store

一些额外的工作

为了创建 watcherSagahandlerSaga,我们还导入了 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 文件,它和之前的类似。

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 文件,对其中的内容作出对应的修改如下:

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 文件,对其中的内容作出对应的修改如下:

src/pages/post/post.jsx查看完整代码
[tuture-del]import Taro, { useRouter } from '@tarojs/taro'
[tuture-add]import Taro, { useRouter, useEffect } from '@tarojs/taro'
import { View } from '@tarojs/components'
[tuture-del]import { useSelector } from '@tarojs/redux'
[tuture-add]import { useDispatch, useSelector } from '@tarojs/redux'

import { PostCard } from '../../components'
import './post.scss'
[tuture-add]import { GET_POST, SET_POST } from '../../constants'

export default function Post() {
const router = useRouter()
const { postId } = router.params

[tuture-del] const posts = useSelector(state => state.post.posts)
[tuture-del] const post = posts[postId]
[tuture-add] const dispatch = useDispatch()
[tuture-add] const post = useSelector(state => state.post.post)

[tuture-del] console.log('posts', posts, postId)
[tuture-add] useEffect(() => {
[tuture-add] dispatch({
[tuture-add] type: GET_POST,
[tuture-add] payload: {
[tuture-add] postId,
[tuture-add] },
[tuture-add] })
[tuture-add]
[tuture-add] return () => {
[tuture-add] dispatch({ type: SET_POST, payload: { post: {} } })
[tuture-add] }
[tuture-add] }, [])

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 }

可以看到,上面的改动主要是创建 watcherSagahandlerSaga

创建 **watcherSaga**

  • 我们创建了登录的 watcherSagawatchGetPost,它用来监听 action.typeGET_POST 的 action,并且当监听到 GET_POST action 之后,然后激活 handlerSagagetPost 去处理对应的获取单个帖子的逻辑。
  • 这里的 watcherSagawatchGetPost 是一个生成器函数,它内部是一个 while 无限循环,表示在内部持续监听 GET_POST action。
  • 在循环内部,我们使用了 redux-saga 提供的 effects helper 函数:take,它用于监听 GET_POST action,获取 action 中携带的数据,这里我们拿到了传过来的 payload 数据。
  • 接着我们使用了另外一个 effects helper 函数:fork,它表示非阻塞的执行 handlerSagagetPost,并传入了获取到 payload.postId 参数。

创建 **handlerSaga**

  • 我们创建了获取单个帖子的 handlerSagagetPost,它用来处理获取帖子逻辑。
  • getPost 也是一个生成器函数,在它内部是一个 try/catch 语句,用于处理获取单个帖子请求可能存在的错误情况。
  • try 语句中,首先是使用了 redux-saga 提供给我们的 effects helper 函数:call 来调用登录的 API:postApi. getPost

如果获取单个帖子失败,我们则使用 put 发起一个 POST_ERROR 的 action 来更新获取单个帖子失败的信息到 Redux Store

一些额外的工作

为了创建 watcherSagahandlerSaga,我们还导入了 postApi.getPost,我们将在后面来创建这个 API。

除此之外我们还导入了需要使用的 action 常量:

  • SET_POST:响应获取帖子列表的 ACTION 常量,我们将在 “第三剑” 中创建它

加入 saga 中心调度文件

我们像之前将 watchGetPosts 等加入到 sagas 中心调度文件一样,将我们创建好的 watchGetPost 也加入进去:

打开 src/sagas/index.js 文件,对其中的内容作出如下的修改:

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 为我们提供的微信小程序云的云函数 API Taro.cloud.callFunction 来便捷的向小程序云发起云函数调用请求。
  • 这里我们调用了一个 getPost 云函数,并给它传递了对应要获取的帖子的 postId 我们将在下一节中实现这个云函数。
  • 如果调用成功,我们可以接收返回值,用于从后端返回数据,这里我们返回了 result.post 数据,即从小程序云返回的单个帖子。
  • 如果调用失败,则打印错误。
  • 最后我们在已经定义好的 postApi 对象里,添加 getPost API 属性然后将其导出,这样在 post saga 函数里面就可以导入 postApi 然后通过 postApi. getPost 的方式来调用 getPost API 处理获取单个帖子的逻辑了。

第五剑:创建对应的微信小程序云函数

创建 getPost 云函数

按照和之前创建 getPosts 云函数类似,我们创建 getPost 云函数。

创建成功之后,我们可以得到两个文件,一个是 functions/getPost/package.json 文件,它和之前的类似。

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 文件,对其中的内容作出对应的修改如下:

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(),表示查询所有的对应 _idpostId 的单个帖子数据
  • 最后我们返回查询到的 post 数据。

第六剑: 定义对应的 reducers 文件

因为这里 SET_POST 的 Action 我们在上上 “大” 节中创建帖子时已经定义了,所有在 “这一剑” 中我们无需添加额外的代码,复用之前的逻辑就好。

小结

在这篇教程中,我们连续使用了三次 “六脉神剑” 讲完了我们的 Post 逻辑的异步流程,让我们再来复习一下我们开头提到的 “六脉神剑”:

  • 将组件中的同步逻辑重构到异步逻辑
  • 声明和补充对应需要的异步 sagas 文件
  • 定义 sagas 需要的常量文件
  • 定义 sagas 涉及到的前端 API 文件
  • 创建对于的微信小程序云函数,并编写对应的 Node.js 处理逻辑
  • 定义对应的 reducers 文件

这是一套讲解模式,也是一套写代码的最佳实践方式之一,希望你能受用。

一点遗憾

这两篇讲解微信小程序云的文章有一个小小的遗憾,我们也在之前的文章中提到过了,就是微信小程序云仅限于微信小程序内的使用,如果我们想做多端应用,比如支付宝小程序云,H5 网站,那么单单使用微信小程序就显得无能为力了,我们将在下一篇文章中引入 LeanCloud Serverless 服务,并使用它来补齐我们跨端小程序开发的短板,敬请期待!✌️

图雀社区 微信公众号

扫一扫关注上方公众号,拉学习群和答疑解惑

  • 本文作者: 图雀社区
  • 本文链接: https://tuture.co/2020/01/17/b32362b/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!