歡迎繼續閱讀《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倉庫加星❤️哦