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

5,459 阅读17分钟

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

如果你敲到这里,会发现我们之后的内容都是纯前端(小程序端)的逻辑,一个完整的可上线小程序应用应该还要有后端,在这篇文章中,我们将使用微信小程序云作为我们的后台,接着我们会引进 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?

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

开通小程序云

  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 就变成了如下这样:

{
  "dependencies": {
    ...
    "redux-saga": "^1.1.3",
    "taro-ui": "^2.2.4"
  },
}

redux-sagaredux 的一个处理异步流程的中间件,那么 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 文件,这类似组合 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 文件,对其中的内容作出对应的修改如下:

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.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 开始的状态
    • 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 }

可以看到,上面的改动主要是创建 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。
    • 接着如果登录成功,我们将登录成功的 user 缓存到 storage 里面。
    • 接着,我们使用 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,在其中添加对应的常量如下:

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.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/' 如下:

{
  "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 函数,我们可以在函数内部以同步的方式处理异步逻辑,可以看到,这个函数接收两个参数:eventcontextevent 指的是触发云函数的事件,当小程序端调用云函数时,event 就是小程序端调用云函数时传入的参数,外加后端自动注入的小程序用户的 openid 和小程序的 appidcontext 对象包含了此处调用的调用信息和运行状态,可以用它来了解服务运行的情况。默认生成的函数内部代码主要是获取了此时微信上下文信息,然后与 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

我们在前面处理登录时,在组件内部 dispatchLOGIN 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_INFOpayload 带上了 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仓库加星❤️哦