欢迎继续阅读《Taro 小程序开发大型实战》系列,前情回顾:css
user
逻辑的状态管理重构post
逻辑的状态管理重构若是你敲到这里,会发现咱们以后的内容都是纯前端(小程序端)的逻辑,一个完整的可上线小程序应用应该还要有后端,在这篇文章中,咱们将使用微信小程序云做为咱们的后台,接着咱们会引进 redux-saga
来帮助 Redux 优雅的处理异步流程,本文最终的实现效果以下:html
若是你不熟悉 Redux,推荐阅读咱们的《Redux 包教包会》系列教程:前端
若是你但愿直接从这一步开始,请运行如下命令:node
git clone -b miniprogram-start https://github.com/tuture-dev/ultra-club.git cd ultra-club
本文所涉及的源代码都放在了 Github 上,若是您以为咱们写得还不错,但愿您能给❤️这篇文章点赞+Github仓库加星❤️哦~
为了将数据持久化存储和高效的查询,咱们须要把数据存储到数据库中,为了实现⼩程序端便捷的开发体验,⼀大批小程序 Serverless 服务兴起,⽽微信⼩程序云正是为了微信⼩程序的快速开发⽽生的。在这篇⽂章中,咱们将使⽤微信小程序云做为咱们的后端,并讲解如何引入和实现 Redux 异步工做流来实现小程序端访问⼩程序云的状态管理。git
在前面的代码中,咱们经过将数据保存在 Storage 里面来完成数据的持久化,这样能够解决小规模数据的存储和查询问题,一旦数据量变大了,那么查询和存储就须要依靠专门的数据库来解决了,通常咱们能够经过自建后端和数据库的方式来解决,但当小程序正愈来愈火的同时,一种被称为 Serverless 的模式被提出并也逐渐火爆起来,通俗意义上来归纳就是 “无后端”,即把后端交给云服务厂商(阿里云、腾讯云、京东云等),开发者只须要专一于前端逻辑,快速交付功能。es6
通常的小程序 Serverless 服务都包含三大功能:github
关于小程序 Serverless 的详细描述,这里推荐一篇文章,有兴趣的同窗能够详细了解一下: 什么是小程序Serverless?
在这一节中,咱们使用微信小程序云做为咱们的 “后端”,微信小程序云和小程序帐号绑定在一块儿,一个小程序帐号能够开通一个小程序云空间,接下来咱们来详细讲解如何开通小程序云。数据库
AppID
,他应该是一个18位字符串。ultra-club
项目文件夹,而后在微信开发者工具菜单栏中选择设置 > 项目设置,打开设置栏:4.找到设置栏的基本信息,AppID 栏将其修改成上面的 AppID 以下:npm
5.当设置了 AppID 以后,咱们的开发者工具里面的 “云开发” 按钮应该就会变成可点击状态,找到左上角的 “云开发” 的按钮并点击,相似下面这张图:json
4.点击 ”云开发“ 按钮以后会弹出确认框,点击赞成就会进到小程序云开发控制台:
进来以后咱们首先看到的是云开发控制台的 ”运营分析“ 界面,这是用来可视化云开发各种资源的使用状况的界面,在这篇教程中咱们不会讲解这方面内容。咱们主要来说一下图中标红的部分:
在本篇教程中,咱们会用到上面提到的数据库和云函数两项功能。
介绍完小程序云的界面,咱们立刻来动手实践,来建立咱们须要的数据库表,由于咱们前端逻辑主要分为 user
和 post
两类逻辑,因此咱们在数据库中建立两张表:
这里咱们具体来解释一下这个数据库操做界面的含义:
user
和 post
,这样咱们就建立好了咱们的数据库表。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 同步数据流动图:
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-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。配置使用 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
。咱们在上一步中使用到了 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 在处理异步请求时有不少种方式,因项目不一样,能够采用不一样的方式,这里咱们选用了官方推荐的最佳实践:
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 中。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)
。
由于在上一步的 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
常量,它主要是用于设置登陆状态的默认状态的常量。
在以前的 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 处理登陆逻辑了。
咱们建立了 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
文件夹时,会出现菜单弹框,容许咱们进行云函数相关的操做:
咱们能够看到有不少操做,这里咱们主要会用到以下几个操做:
注意其它的操做等你走完整个小程序云开发的流程以后,当须要编写更加复杂的业务逻辑时都会遇到,具体能够参考小程序云的文档:文档地址。
注意必须先开通小程序云开发环境才能使用云函数。具体步骤能够参考咱们在 “开通小程序云” 这一节中的讲解。
讲解了微信小程序云函数的配置,终于到了建立云函数的阶段了,咱们在小程序开发者工具中右键点击 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
的对象。了解了云函数的具体逻辑,咱们立刻在云函数中来实现咱们具体的登陆逻辑,打开 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
用于获取指定的记录引用,返回的是这条数据,而不是一个数组。注意这里关于云数据库的相关操做,能够查阅微信小程序云文档,在文档里提供了详尽的实例:数据库文档。
咱们在前面处理登陆时,在组件内部 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 } }
看一看到上面的代码主要有三处改动:
接着咱们给 INITIAL_STATE
增长了几个字段:
userId
:用于以后获取用户数据,以及标志用户的登陆状态isLogin
:用于标志登陆过程当中是否在执行登陆逻辑,true
表示正在执行登陆中,false
表示登陆逻辑执行完毕loginStatus
:用于标志登陆过程当中的状态:开始登陆(LOGIN
)、登陆成功(LOGIN_SUCCESS
)、登陆失败(LOGIN_ERROR
)switch
语句中响应 action,更新相应的状态。咱们在上一节 “实现 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
。userId
到 Redux Store 的 user
逻辑部分,因此这里咱们从 storage
获取到了 _id
,而后给以前的 SET_LOGIN_INFO
的 payload
带上了 userId
属性。getStorage
的逻辑,只有当此时 Redux Store 里面没有数据时,咱们才去获取 storage 里面的数据来更新 Redux Store。由于在 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
记录,说明咱们成功接通了小程序端和小程序云。通常在本地调试完后,咱们就能够将云函数上传到云端,这样,咱们就能够不用开启本地调试才能使用云函数了,这对于发布上线的小程序是必须的,具体上传云函数能够在小程序开发者工具中右键点击 functions
文件夹下对应的云函数,而后选择 “上传并部署:云端安装因此依赖”:
在这篇教程中,咱们实现了 User 逻辑的异步流程,在下一篇教程中,咱们将实现 Post 逻辑的异步流程,敬请期待!
想要学习更多精彩的实战技术教程?来 图雀社区逛逛吧。本文所涉及的源代码都放在了 Github 上,若是您以为咱们写得还不错,但愿您能给❤️这篇文章点赞+Github仓库加星❤️哦