欢迎继续阅读《Taro 小程序开发大型实战》系列,前情回顾:
- 熟悉的 React,熟悉的 Hooks:我们用 React 和 Hooks 实现了一个非常简单的添加帖子的原型
- 多页面跳转和 Taro UI 组件库:我们用 Taro 自带的路由功能实现了多页面跳转,并用 Taro UI 组件库升级了应用界面
- 实现微信和支付宝多端登录:实现了微信、支付宝以及普通登录和退出登录
- Hooks + Redux 双剑合璧:使用了 Hooks 版的 Redux 来重构应用的状态管理
- 使用 Hooks 版的 Redux 实现大型应用状态管理(上篇):使用 Hooks 版的 Redux 实现了
user
逻辑的状态管理重构 - 使用 Hooks 版的 Redux 实现大型应用状态管理(下篇):使用 Hooks 版的 Redux 实现了
post
逻辑的状态管理重构
如果你敲到这里,会发现我们之后的内容都是纯前端(小程序端)的逻辑,一个完整的可上线小程序应用应该还要有后端,在这篇文章中,我们将使用微信小程序云作为我们的后台,接着我们会引进 redux-saga
来帮助 Redux 优雅的处理异步流程,本文最终的实现效果如下:
如果你不熟悉 Redux,推荐阅读我们的《Redux 包教包会》系列教程:
如果你希望直接从这一步开始,请运行以下命令:
git clone -b miniprogram-start https://github.com/tuture-dev/ultra-club.git
cd ultra-club
本文所涉及的源代码都放在了 Github 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点赞+Github仓库加星❤️哦~
为了将数据持久化存储和高效的查询,我们需要把数据存储到数据库中,为了实现⼩程序端便捷的开发体验,⼀大批小程序 Serverless 服务兴起,⽽微信⼩程序云正是为了微信⼩程序的快速开发⽽生的。在这篇⽂章中,我们将使⽤微信小程序云作为我们的后端,并讲解如何引入和实现 Redux 异步工作流来实现小程序端访问⼩程序云的状态管理。
微信小程序云初尝鲜
在前面的代码中,我们通过将数据保存在 Storage 里面来完成数据的持久化,这样可以解决小规模数据的存储和查询问题,一旦数据量变大了,那么查询和存储就需要依靠专门的数据库来解决了,一般我们可以通过自建后端和数据库的方式来解决,但当小程序正越来越火的同时,一种被称为 Serverless 的模式被提出并也逐渐火爆起来,通俗意义上来概括就是 “无后端”,即把后端交给云服务厂商(阿里云、腾讯云、京东云等),开发者只需要专注于前端逻辑,快速交付功能。
一般的小程序 Serverless 服务都包含三大功能:
- 数据库:一般是以 JSON 数据格式进行存储,可以将数据存储在云端数据库中。
- 存储:支持文本、图片等用户生成内容的存储,可以获取资源的链接进行使用。
- 云函数:可以用 Node.js 进行开发,自己编写对应的后端逻辑,并把写好的代码传到云端,然后在小程序前端使用 API 进行调用。
关于小程序 Serverless 的详细描述,这里推荐一篇文章,有兴趣的同学可以详细了解一下:什么是小程序Serverless?
在这一节中,我们使用微信小程序云作为我们的 “后端”,微信小程序云和小程序账号绑定在一起,一个小程序账号可以开通一个小程序云空间,接下来我们来详细讲解如何开通小程序云。
开通小程序云
- 首先确保你注册了小程序的微信公众平台账号:注册地址。
- 登录之后,在菜单栏开发 > 开发设置里面找到
AppID
,他应该是一个18位字符串。 - 使用微信开发者工具打开我们的
ultra-club
项目文件夹,然后在微信开发者工具菜单栏中选择设置 > 项目设置,打开设置栏:
4.找到设置栏的基本信息,AppID 栏将其修改为上面的 AppID 如下:
5.当设置了 AppID 之后,我们的开发者工具里面的 “云开发” 按钮应该就会变成可点击状态,找到左上角的 “云开发” 的按钮并点击,类似下面这张图:
4.点击 ”云开发“ 按钮之后会弹出确认框,点击同意就会进到小程序云开发控制台:
进来之后我们首先看到的是云开发控制台的 ”运营分析“ 界面,这是用来可视化云开发各类资源的使用情况的界面,在这篇教程中我们不会讲解这方面内容。我们主要来讲一下图中标红的部分:
- 其中序号为 1 的就是我们的云数据库,它是一个 JSON 数据库,里面存储着我们在开发时需要的数据。
- 序号为2的是存储,即我们可以上传一些文本、图片、音/视频,然后返回给我们访问这些资源的链接。
- 序号3是云函数,即我们可以在这里面管理一些我们编写的的后端 Node.js 逻辑,它运行在云中,我们可以在小程序端通过 API 来调用它们。
- 序号4是代表我们此次的云环境的标识符,可以用于在小程序端以 API 调用云开发资源时标志此时的调用的云环境。
在本篇教程中,我们会用到上面提到的数据库和云函数两项功能。
创建数据库表
介绍完小程序云的界面,我们马上来动手实践,来创建我们需要的数据库表,因为我们前端逻辑主要分为 user
和 post
两类逻辑,所以我们在数据库中创建两张表:
这里我们具体来解释一下这个数据库操作界面的含义:
- 可以看到,点击云开发控制台左上角的第二个按钮,然后点击图中标红序号为1的 “+” 按钮,创建两个集合
user
和post
,这样我们就创建好了我们的数据库表。 - 序号为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()
来进行小程序云环境的初始化
小结
到现在为止,我们讲解了如何开通小程序云,然后讲解了小程序云控制台界面,同时,我们讲解了将会用到的数据库功能界面,在其中创建了我们应用需要的两张表(集合):post
和 user
,并且各自初始化了一条记录。
好了,准备好了小程序云,我们开始准备在应用中接入它了,但在此之前,因为我们要接入小程序云,那么势必要发起异步的请求,这就需要了解一下 Redux 的异步处理流程,在下一节中,我们将使用 redux-saga
中间件来简化 Redux 处理异步的流程。
Redux 异步工作流解析
我们来看一下 Redux 的数据流动图:
上图中灰色的那条路径是我们之前一直在使用的 Redux 的数据流动图,它是 Redux 同步数据流动图:
view
中dispatch(syncAction)
一个同步 action 来更新store
中的数据reducer
响应 action,更新store
状态connect
将更新后的状态传给view
view
接收新的数据重新渲染
注意
对 Redux 还不了解的同学可以学习一下图雀社区的 Redux 包教包会系列教程哦。
现在我们要去向小程序云发起请求,这个请求是一个异步的请求,它不会立刻得到响应,所以我们需要一个中间状态(这里我们使用 Saga
)来回处理这个异步请求并得到数据,然后再执行和之前同步请求类似的路径,即为我们上图中绿色的部分+剩下灰色的部分,所以异步工作流程就变成了这样:
view
中dispatch(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
就变成了如下这样:
{
"dependencies": {
...
"redux-saga": "^1.1.3",
"taro-ui": "^2.2.4"
},
}
redux-saga
是redux
的一个处理异步流程的中间件,那么 Saga 是什么?Saga的定义是“长时间活动的事务”(Long Lived Transaction,后文简称为LLT)。他是普林斯顿大学HECTOR GARCIA-MOLINA教授在1987年的一篇关于分布式数据库的论文中提出来的概念。官方把一个 saga 比喻为应用程序中的一个单独的线程,它负责独立的处理副作用,在 JavaScript 中,副作用就是指异步网络请求、本地读取 localStorage/Cookie 等外界操作。
配置 redux-saga
中间件
安装完之后,我们接着要先配置 redux-saga
才能使用它,打开 src/store/index.js
文件,对其中的内容作出对应的修改如下:
import {
createStore, applyMiddleware } from 'redux'
import {
createLogger } from 'redux-logger'
import createSagaMiddleware from 'redux-saga'
import rootReducer from '../reducers'
import rootSaga from '../sagas'
const sagaMiddleware = createSagaMiddleware()
const middlewares = [sagaMiddleware, createLogger()]
export default function configStore() {
const store = createStore(rootReducer, applyMiddleware(...middlewares))
sagaMiddleware.run(rootSaga)
return store
}
可以看到,我们上面的文件作出以下四处改动:
- 首先我们导出了
createSagaMiddleware
- 接着我们从
src/store/sagas
文件夹下导出了一个rootSaga
,它组合了所有的saga
文件,这类似组合reducer
的combineReducers
,我们将在后续的步骤中编写这些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
文件,对其中的内容作出对应的修改如下:
import Taro, { useState } from '@tarojs/taro'
import { View, Form } from '@tarojs/components'
import { AtButton, AtImagePicker } from 'taro-ui'
import { useDispatch } from '@tarojs/redux'
import { LOGIN } from '../../constants'
import './index.scss'
export default function LoginForm(props) {
// 其他逻辑不变 ...
async function handleSubmit(e) {
// 其他逻辑不变 ...
// 缓存在 storage 里面
const userInfo = { avatar: files[0].url, nickName: formNickName }
// 清空表单状态
setFiles([])
setFormNickName('')
// 向后端发起登录请求
dispatch({ type: LOGIN, payload: { userInfo: userInfo } })
}
return (
// 返回的组件...
)
}
可以看到