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倉庫加星❤️哦

相關文章
相關標籤/搜索