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

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

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

  • 数据库:通常是以 JSON 数据格式进行存储,能够将数据存储在云端数据库中。
  • 存储:支持文本、图片等用户生成内容的存储,能够获取资源的连接进行使用。
  • 云函数:能够用 Node.js 进行开发,本身编写对应的后端逻辑,并把写好的代码传到云端,而后在小程序前端使用 API 进行调用。
关于小程序 Serverless 的详细描述,这里推荐一篇文章,有兴趣的同窗能够详细了解一下: 什么是小程序Serverless?

在这一节中,咱们使用微信小程序云做为咱们的 “后端”,微信小程序云和小程序帐号绑定在一块儿,一个小程序帐号能够开通一个小程序云空间,接下来咱们来详细讲解如何开通小程序云。数据库

开通小程序云

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

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

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

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仓库加星❤️哦

相关文章
相关标签/搜索