我们研发开源了一款基于 Git 进行技术实战教程写作的工具,我们图雀社区的所有教程都是用这款工具写作而成,欢迎 Star 哦
如果你想快速了解如何使用,欢迎阅读我们的 教程文档哦
欢迎继续阅读《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 (
// 返回的组件...
)
}
可以看到,我们对上面的代码做出了以下三处改动:
- 我们将之前设置用户登录信息的
SET_LOGIN_INFO
和设置登录框弹出层的SET_IS_OPENED
换成了LOGIN
常量,代表我们要先向小程序云发起登录请求,然后获取到登录的数据再设置登录信息和关闭登录框弹出层(其实这里也可以直接关闭弹出层,有点失策(⊙o⊙)…)。 - 接着我们将之前的设置登录信息和关闭登录框弹出层的操作删除掉。
- 最后我们将
dispatch
一个action.type
为LOGIN
的 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 开始的状态dispatch
同步的 action,更新异步 action 成功/失败的状态
运用最近实践之后,之前的 Redux 数据流动图就变成了下面这样子:
好了,讲解了 redux-saga
处理异步 Action 的最佳实践之后,我们马上来运用最佳实践来编写处理异步 Action 的 Saga 文件。
在我们的应用中可能涉及到多个异步请求,所以 redux-saga
推荐的最佳实践是单独创建一个 sagas
文件夹,来存放所有处理异步请求的 sagas
文件,以及可能用到的辅助文件。
在上一步中,我们从 view 中发出了 LOGIN
异步登录请求,接下来我们要编写对应处理这个 LOGIN
请求的 saga
文件,在 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 }
可以看到,上面的改动主要是创建 watcherSaga
和 handlerSaga
。
创建 watcherSaga
- 我们创建了登录的
watcherSaga
:watchLogin
,它用来监听action.type
为LOGIN
的 action,并且当监听到LOGIN
action 之后,从这个 action 中获取必要的userInfo
数组,然后激活handlerSaga
:login
去处理对应的登录逻辑。 - 这里的
watcherSaga
:watchLogin
是一个生成器函数,它内部是一个while
无限循环,表示在内部持续监听LOGIN
action。 - 在循环内部,我们使用了
redux-saga
提供的effects helper
函数:take
,它用于监听LOGIN
action,获取 action 中携带的数据。 - 接着我们使用了另外一个
effects helper
函数:fork
,它表示非阻塞的执行handlerSaga
:login
,并将payload.userInfo
作为参数传给login
。
创建 handlerSaga
- 我们创建了登录的
handlerSaga
:login
,它用来处理登录逻辑。 login
也是一个生成器函数,在它内部是一个try/catch
语句,用于处理登录请求可能存在的错误情况。- 在
try
语句中,首先是使用了redux-saga
提供给我们的effects helper
函数:call
来调用登录的 API:userApi.login
,并把userInfo
作为参数传给这个 API。- 接着如果登录成功,我们将登录成功的
user
缓存到storage
里面。 - 接着,我们使用
redux-saga
提供的effects helpers
函数:put
,put
类似之前在view
中的dispatch
操作,,来dispatch
了三个 action:LOGIN_SUCCESS
,SET_IS_OPENED
,SET_LOGIN_INFO
,代表更新登录成功的状态,关闭登录框,设置登录信息到 Redux Store 中。 - 最后我们使用了 Taro UI 提供给我们的消息框,来显示一个
success
消息。
- 接着如果登录成功,我们将登录成功的
- 如果登录失败,我们则使用
put
发起一个LOGIN_ERROR
的 action 来更新登录失败的信息到 Redux Store,接着使用了 Taro UI 提供给我们的消息框,来显示一个error
消息。
注意
对生成器函数不了解的同学可以看一下这篇文档:迭代器和生成器。
一些额外的工作
为了创建 watcherSaga
和 handlerSaga
,我们还导入了 userApi
,我们将在后面来创建这个 API。
除此之外我们还导入了需要使用的 action 常量:
SET_LOGIN_INFO
:设置登录信息LOGIN_SUCCESS
:更新登录成功信息LOGIN
:监听登录动作LOGIN_ERROR
:更新登录失败信息SET_IS_OPENED
:设置登录框开启/关闭的信息
我们还从 redux-saga/effects
包中导入了必要的函数:
call
:在saga
函数中调用其他异步/同步函数,获取结果put
:类似dispatch
,用于在saga
函数中发起 actiontake
:在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
函数fork
和all
。 - 接着我们从
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
,在其中添加对应的常量如下:
export const SET_IS_OPENED = 'MODIFY_IS_OPENED'
export const SET_LOGIN_INFO = 'SET_LOGIN_INFO'
export const LOGIN = 'LOGIN'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
export const LOGIN_ERROR = 'LOGIN_ERROR'
export const LOGIN_NORMAL = 'LOGIN_NORMAL'
可以看到,上面除了我们在 “saga 处理异步请求” 中使用到的常量之外,还多了一个 LOGIN_NORMAL
常量,它主要是用于设置登录状态的默认状态的常量。
实现请求 login API
在之前的 user
saga 文件里面,我们使用到了 userApi
,它里面封装了用于向后端(这里我们是小程序云)发起请求的逻辑,让我们马上来实现它吧。
我们统一将所有的 API 文件放到 api
文件夹里面,这便于我们日后的代码维护工作,在 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.json
的 cloudfunctionRoot
字段的值指定为这个目录,这样小程序开发者工具就可以识别此目录为存放云函数的目录,并做特殊的标志处理。
我们在项目根目录下创建了一个 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/'
如下:
{
"miniprogramRoot": "dist/",
"projectname": "ultra-club",
"description": "",
"appid": "",
"cloudfunctionRoot": "functions/",
"setting": {
"urlCheck": true,
"es6": false,
"postcss": false,
"minified": false
},
"compileType": "miniprogram",
"simulatorType": "wechat",
"simulatorPluginLibVersion": {},
"cloudfunctionTemplateRoot": "cloudfunctionTemplate",
"condition": {}
}
可以看到,当我们创建了上面的文件夹并设置了 project.config.json
之后,我们的小程序开发者工具会变成下面这个样子:
我们创建的那个 functions
文件夹多了一个额外的云图标,并且文件夹的命名从 functions
变成了 functions | ultra-club
,竖杠右边的是我们当前的小程序环境。
并且当我们在小程序开发者工具里面右键点击这个 functions
文件夹时,会出现菜单弹框,允许我们进行云函数相关的操作:
我们可以看到有很多操作,这里我们主要会用到如下几个操作:
- 新建 Node.js 云函数
- 开启云函数本地调试
注意
其它的操作等你走完整个小程序云开发的流程之后,当需要编写更加复杂的业务逻辑时都会遇到,具体可以参考小程序云的文档:文档地址。
注意
必须先开通小程序云开发环境才能使用云函数。具体步骤可以参考我们在 “开通小程序云” 这一节中的讲解。
创建 login 云函数
讲解了微信小程序云函数的配置,终于到了创建云函数的阶段了,我们在小程序开发者工具中右键点击 functions
文件夹,然后选择新建 Node.js 云函数,输入 login
,然后回车创建,会看到小程序开发者工具自动帮我们创建了如下的代码文件:
可以看到,一个云函数是一个独立的 Node.js 模块,它处理一类逻辑。
我们先来看一下 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
函数,我们可以在函数内部以同步的方式处理异步逻辑,可以看到,这个函数接收两个参数:event
和context
,event
指的是触发云函数的事件,当小程序端调用云函数时,event 就是小程序端调用云函数时传入的参数,外加后端自动注入的小程序用户的openid
和小程序的appid
。context
对象包含了此处调用的调用信息和运行状态,可以用它来了解服务运行的情况。默认生成的函数内部代码主要是获取了此时微信上下文信息,然后与event
对象一同返回,这样当我们在小程序端以Taro.cloud.callFunction
调用这个函数获得的返回值就是包含微信上下文信息和event
的对象。
编写 login 云函数
了解了云函数的具体逻辑,我们马上在云函数中来实现我们具体的登录逻辑,打开 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
我们在前面处理登录时,在组件内部 dispatch
了 LOGIN
action,在处理异步 action 的 saga 函数中,使用 put
发起了一系列更新 store 中登录状态的 action,现在我们马上来实现响应这些 action 的 reducers
,打开 src/reducers/user.js
,对其中的代码做出对应的修改如下:
import {
SET_LOGIN_INFO,
SET_IS_OPENED,
LOGIN_SUCCESS,
LOGIN,
LOGIN_ERROR,
LOGIN_NORMAL,
} from '../constants/'
const INITIAL_STATE = {
userId: '',
avatar: '',
nickName: '',
isOpened: false,
isLogin: false,
loginStatus: LOGIN_NORMAL,
}
export default function user(state = INITIAL_STATE, action) {
switch (action.type) {
case SET_IS_OPENED: {
const { isOpened } = action.payload
return { ...state, isOpened }
}
case SET_LOGIN_INFO: {
const { avatar, nickName, userId } = action.payload
return { ...state, nickName, avatar, userId }
}
case LOGIN: {
return { ...state, loginStatus: LOGIN, isLogin: true }
}
case LOGIN_SUCCESS: {
return { ...state, loginStatus: LOGIN_SUCCESS, isLogin: false }
}
case LOGIN_ERROR: {
return { ...state, loginStatus: LOGIN_ERROR, isLogin: false }
}
default:
return state
}
}
看一看到上面的代码主要有三处改动:
- 首先我们导入了必要的 action 常量
- 接着我们给
INITIAL_STATE
增加了几个字段:userId
:用于之后获取用户数据,以及标志用户的登录状态isLogin
:用于标志登录过程中是否在执行登录逻辑,true
表示正在执行登录中,false
表示登录逻辑执行完毕loginStatus
:用于标志登录过程中的状态:开始登录(LOGIN
)、登录成功(LOGIN_SUCCESS
)、登录失败(LOGIN_ERROR
)
- 最后就是
switch
语句中响应 action,更新相应的状态。
收尾 User 剩下的异步逻辑
微信登录
我们在上一节 “实现 Redux 异步逻辑” 中,着重实现了普通登录按钮的异步逻辑,现在我们来收尾一下使用微信登录的逻辑。打开 src/components/WeappLoginButton/index.js
文件,对其中的内容作出对应的修改如下:
import Taro, { useState } from '@tarojs/taro'
import { Button } from '@tarojs/components'
import { useDispatch } from '@tarojs/redux'
import './index.scss'
import { LOGIN } from '../../constants'
export default function WeappLoginButton(props) {
const [isLogin, setIsLogin] = useState(false)
const dispatch = useDispatch()
async function onGetUserInfo(e) {
setIsLogin(true)
const { avatarUrl, nickName } = e.detail.userInfo
const userInfo = { avatar: avatarUrl, nickName }
dispatch({
type: LOGIN,
payload: {
userInfo: userInfo,
},
})
setIsLogin(false)
}
return (
<Button
openType="getUserInfo"
onGetUserInfo={onGetUserInfo}
type="primary"
className="login-button"
loading={isLogin}
>
微信登录
</Button>
)
}
可以看到,上面的代码主要有一下三处改动:
- 我们删掉了之前直接设置登录信息的
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
,对其中的内容作出对应的修改如下:
import Taro, { useEffect } from '@tarojs/taro'
import { View } from '@tarojs/components'
import { useDispatch, useSelector } from '@tarojs/redux'
import { Header, Footer } from '../../components'
import './mine.scss'
import { SET_LOGIN_INFO } from '../../constants'
export default function Mine() {
const dispatch = useDispatch()
const nickName = useSelector(state => state.user.nickName)
const isLogged = !!nickName
useEffect(() => {
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()
}
})
return (
<View className="mine">
<Header />
<Footer />
</View>
)
}
Mine.config = {
navigationBarTitleText: '我的',
}
可以看到,我们对上面的代码做出了三处修改如下:
- 首先我们导出了
useSelector
Hooks,从 Redux Store 里获取到了nickName
。 - 接着,因为我们在 “实现 Redux 异步逻辑” 一节中,保存了
userId
到 Redux Store 的user
逻辑部分,所以这里我们从storage
获取到了_id
,然后给之前的SET_LOGIN_INFO
的payload
带上了userId
属性。 - 最后,我们判断一下
getStorage
的逻辑,只有当此时 Redux Store 里面没有数据时,我们才去获取 storage 里面的数据来更新 Redux Store。
扩充 Logout 的清空数据范围
因为在 Redux Store 里面的 user
属性中多出了一个 userId
属性,所以我们在 Logout
组件里 dispatch
action 时,要清空 userId
如下:
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: '',
userId: '',
},
})
} catch (err) {
console.log('removeStorage ERR: ', err)
}
setIsLogout(false)
}
return (
<AtButton type="secondary" full loading={isLogout} onClick={handleLogout}>
退出登录
</AtButton>
)
}
小结
大功告成!到这里我们就把 user
逻辑接入了小程序云,并能成功实现微信小程序端的小程序云登录,让我们马上来尝试一下预览本地调试时的效果预览图:
可以看到,我们在本地调试云函数,以及小程序端接入云函数的步骤如下:
- 我们首先右键点击
functions
文件夹,开启了 “云函数本地调试”。 - 接着选中我们的
login
云函数,然后点击开启本地调试,这样我们就可以在本地调试云函数了。 - 接着我们在小程序端点击微信登录,然后我们会看到小程序开发者工具控制台和云函数调试控制台都会答应此时云函数的运行情况。
- 最后,我们登陆成功,成功在小程序端显示了登录的昵称和头像,并且检查云开发 > 数据库 > user 表,它确实增加了一个对应的
user
记录,说明我们成功接通了小程序端和小程序云。
一般在本地调试完后,我们就可以将云函数上传到云端,这样,我们就可以不用开启本地调试才能使用云函数了,这对于发布上线的小程序是必须的,具体上传云函数可以在小程序开发者工具中右键点击 functions
文件夹下对应的云函数,然后选择 “上传并部署:云端安装所以依赖”:
在这篇教程中,我们实现了 User 逻辑的异步流程,在下一篇教程中,我们将实现 Post 逻辑的异步流程,敬请期待!
想要学习更多精彩的实战技术教程?来图雀社区逛逛吧。
本文所涉及的源代码都放在了 Github 上,如果您觉得我们写得还不错,希望您能给❤️这篇文章点赞+Github仓库加星❤️哦