用戶
 找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

掃一掃,登錄網站

小程序社區 首頁 教程 實戰教程 查看內容

Taro 小程序開發大型實戰:嘗鮮微信小程序云

Rolan 2020-2-19 00:37

歡迎繼續閱讀《Taro 小程序開發大型實戰》系列,前情回顧:熟悉的 React,熟悉的 Hooks:我們用 React 和 Hooks 實現了一個非常簡單的添加帖子的原型多頁面跳轉和 Taro UI 組件庫:我們用 Taro 自帶的路由功能實現了 ...

歡迎繼續閱讀《Taro 小程序開發大型實戰》系列,前情回顧:

在上一篇文章中,我們將我們兩大邏輯之一 User 部分接入了 Redux 異步處理流程,接著接入了微信小程序云,使得 User 邏輯可以在云端永久保存,好不自在:),兩兄弟一個得了好處,另外一個不能干瞪眼對吧?在這一篇教程中,我們想辦法把 User 另外一個兄弟 Post 撈上來,也把 Redux 異步流程和微信小程序給它整上,這樣就齊活了:laughing:。

我們首先來看一看最終的完成效果:

如果你不熟悉 Redux,推薦閱讀我們的《Redux 包教包會》系列教程:

如果你希望直接從這一步開始,請運行以下命令:

git clone -b miniprogram-start https://github.com/tuture-dev/ultra-club.git
cd ultra-club
復制代碼

本文所涉及的源代碼都放在了 Github 上,如果您覺得我們寫得還不錯,希望您能給 :heart:這篇文章點贊+Github倉庫加星? ?哦~

此教程屬于 React 前端工程師學習路線 的一部分,歡迎來 Star 一波,鼓勵我們繼續創作出更好的教程,持續更新中~

“六脈神劍” 搞定 createPost 異步邏輯

不知道看到這里的讀者有沒有發現上篇文章其實打造了一套講解模式,即按照如下的 “六步流程” 來講解,我們也稱為 “六脈神劍” 講解法:

sagas
sagas
sagas
reducers

可以看到我們上面的講解順序實際上是按照前端數據流的流動來進行的,我們對標上面的講解邏輯來看一下前端數據流是如何流動的:

  • 從組件中通過對應的常量發起異步請求
  • sagas 監聽到對應的異步請求,開始處理流程
  • 在 sagas 調用對應的前端 API 文件向微信小程序云發起請求
  • 微信小程序云函數處理對應的 API 請求,返回數據
  • sagas 中獲取到對應的數據, dispatch action 到對應的 reducers 處理邏輯
  • reducers 接收數據,開始更新本地 Redux Store 中的 state
  • 組件中重新渲染

好的,了解了講解邏輯和對應前端數據流動邏輯之后,我們馬上來實踐這套邏輯,把 User 邏輯的好兄弟 Post 邏輯搞定。

第一劍: PostForm 組件中發起異步請求

首先從創建帖子邏輯動刀子,我們將創建帖子接入異步邏輯并接通小程序云,讓文章上云。打開 src/components/PostForm/index.jsx ,對其中的內容作出對應的修改如下:

import { useDispatch, useSelector } from '@tarojs/redux'

import './index.scss'
import { CREATE_POST } from '../../constants'

export default function PostForm() {
  const [formTitle, setFormTitle] = useState('')
  const [formContent, setFormContent] = useState('')

  const userId = useSelector(state => state.user.userId)

  const dispatch = useDispatch()
...    }

    dispatch({
      type: CREATE_POST,
      payload: {
        postData: {
          title: formTitle,
          content: formContent,
        },
        userId,
      },
    })

    setFormTitle('')
    setFormContent('')
  }

  return (
復制代碼

可以看到,上面的內容做了如下四處修改:

  • 首先我們現在是接收用戶的文章輸入數據然后向小程序云發起創建文章的請求,所以我們將之前的 dispatch SET_POSTS Action 改為 CREATE_POST Action,并且將之前的 action payload 簡化為 postData 和 userId ,因為我們可以通過小程序云數據庫查詢 userId 得到創建文章的用戶信息,所以不需要再攜帶用戶的數據。
  • 接著,因為我們不再需要用戶的 avatar 和 nickName 數據,所以我們刪掉了對應的 useSelector 語句。
  • 接著,因為請求是異步的,所以需要等待請求完成之后再設置對應的發表文章的狀態以及發表文章彈出層狀態,所以我們刪掉了對應的 dispatch SET_POST_FORM_IS_OPENED Action 邏輯以及 Taro.atMessage 邏輯。
  • 最后我們刪掉不需要的常量 SET_POSTS 和 SET_POST_FORM_IS_OPENED ,然后導入異步創建文章的常量 CREATE_POST 。

增加 Action 常量

我們在上一步中使用到了 CREATE_POST 常量,打開 src/constants/post.js ,在其中增加 CREATE_POST 常量:

export const CREATE_POST = 'CREATE_POST'
復制代碼

到這里,我們的 “六步流程” 講解法就走完了第一步,即從組件中發起對應的異步請求,這里我們是發出的 action.type 為 CREATE_POST 的異步請求。

第二劍: 聲明和補充對應需要的異步 sagas 文件

在 “第一劍” 中,我們從組件中 dispatch 了 action.type 為 CREATE_POST 的異步 Action,接下來我們要做的就是在對應的 sagas 文件中補齊響應這個異步 action 的 sagas。

在 src/sagas/ 文件夾下面創建 post.js 文件,并在其中編寫如下創建文章的邏輯:

import Taro from '@tarojs/taro'
import { call, put, take, fork } from 'redux-saga/effects'

import { postApi } from '../api'
import {
  CREATE_POST,
  POST_SUCCESS,
  POST_ERROR,
  SET_POSTS,
  SET_POST_FORM_IS_OPENED,
} from '../constants'

function* createPost(postData, userId) {
  try {
    const post = yield call(postApi.createPost, postData, userId)

    // 其實以下三步可以合成一步,但是這里為了講解清晰,將它們拆分成獨立的單元

    // 發起發帖成功的 action
    yield put({ type: POST_SUCCESS })

    // 關閉發帖框彈出層
    yield put({ type: SET_POST_FORM_IS_OPENED, payload: { isOpened: false } })

    // 更新 Redux store 數據
    yield put({
      type: SET_POSTS,
      payload: {
        posts: [post],
      },
    })

    // 提示發帖成功
    Taro.atMessage({
      message: '發表文章成功',
      type: 'success',
    })
  } catch (err) {
    console.log('createPost ERR: ', err)

    // 發帖失敗,發起失敗的 action
    yield put({ type: POST_ERROR })

    // 提示發帖失敗
    Taro.atMessage({
      message: '發表文章失敗',
      type: 'error',
    })
  }
}

function* watchCreatePost() {
  while (true) {
    const { payload } = yield take(CREATE_POST)

    console.log('payload', payload)

    yield fork(createPost, payload.postData, payload.userId)
  }
}

export { watchCreatePost }
復制代碼

可以看到,上面的改動主要是創建 watcherSaga 和 handlerSaga 。

創建 watcherSaga

  • 我們創建了登錄的 watcherSaga : watchCreatePost ,它用來監聽 action.type 為 CREATE_POST 的 action,并且當監聽到 CREATE_POST action 之后,從這個 action 中獲取必要的 postData 和 userId 數據,然后激活 handlerSaga : createPost 去處理對應的創建帖子的邏輯。
  • 這里的 watcherSaga : watchCreatePost 是一個生成器函數,它內部是一個 while 無限循環,表示在內部持續監聽 CREATE_POST action。
  • 在循環內部,我們使用了 redux-saga 提供的 effects helper 函數: take ,它用于監聽 CREATE_POST action,獲取 action 中攜帶的數據。
  • 接著我們使用了另外一個 effects helper 函數: fork ,它表示非阻塞的執行 handlerSaga: createPost ,并將 payload.postData 和 payload.userId 作為參數傳給 createPost 。

創建 handlerSaga

  • 我們創建了創建帖子的 handlerSaga : createPost ,它用來處理創建邏輯。
  • createPost 也是一個生成器函數,在它內部是一個 try/catch 語句,用于處理創建帖子請求可能存在的錯誤情況。
  • 在 try 語句中,首先是使用了 redux-saga 提供給我們的 effects helper 函數: call 來調用登錄的 API: postApi.createPost ,并把 postData 和 userId 作為參數傳給這個 API。
    • 如果創建帖子成功,我們使用 redux-saga 提供的 effects helpers 函數: put , put類似之前在 view 中的 dispatch 操作,,來 dispatch 了三個 action: POST_SUCCESS , SET_POST_FORM_IS_OPENED , SET_POSTS ,代表更新創建帖子成功的狀態,關閉發帖框,設置最新創建的帖子信息到 Redux Store 中。
    • 最后我們使用了 Taro UI 提供給我們的消息框,來顯示一個 success 消息。
  • 如果發帖失敗,我們則使用 put 發起一個 POST_ERROR 的 action 來更新創建帖子失敗的信息到 Redux Store,接著使用了 Taro UI 提供給我們的消息框,來顯示一個 error 消息。

一些額外的工作

為了創建 watcherSaga 和 handlerSaga ,我們還導入了 postApi ,我們將在后面來創建這個 API。

除此之外我們還導入了需要使用的 action 常量:

POST_SUCCESS
POST_ERROR
SET_POSTS
CREATE_POST
SET_POST_FORM_IS_OPENED

這里的 POST_SUCCESS 和 POST_ERROR 我們還沒有創建,我們將馬上在 “下一劍” 中創建它。

以及一些 redux-saga/effects 相關的 helper 函數,我們已經在之前的內容中詳細講過了,這里就不再贅述了。

加入 saga 中心調度文件

我們像之前將 watchLogin 等加入到 sagas 中心調度文件一樣,將我們創建好的 watchCreatePost 也加入進去:

// ...之前的邏輯
import { watchCreatePost } from './post'
export default function* rootSaga() {
  yield all([
   // ... 之前的邏輯
    fork(watchCreatePost)
  ])
}
復制代碼

第三劍:定義 sagas 需要的常量文件

打開 src/constants/post.js 文件,定義我們之前創建的常量文件如下:

export const POST_SUCCESS = 'POST_SUCCESS'
export const POST_ERROR = 'POST_ERROR'
復制代碼

第四劍:定義 sagas 涉及到的前端 API 文件

在之前的 post saga 文件里面,我們使用到了 postApi ,它里面封裝了用于向后端(這里我們是小程序云)發起和帖子有關請求的邏輯,讓我們馬上來實現它吧。

在 src/api/ 文件夾下添加 post.js 文件,并在文件中編寫內容如下:

import Taro from '@tarojs/taro'

async function createPost(postData, userId) {
  const isWeapp = Taro.getEnv() === Taro.ENV_TYPE.WEAPP
  const isAlipay = Taro.getEnv() === Taro.ENV_TYPE.ALIPAY

  console.log('postData', postData, userId)

  // 針對微信小程序使用小程序云函數,其他使用小程序 RESTful API
  try {
    if (isWeapp) {
      const { result } = await Taro.cloud.callFunction({
        name: 'createPost',
        data: {
          postData,
          userId,
        },
      })

      return result.post
    }
  } catch (err) {
    console.error('createPost ERR: ', err)
  }
}

const postApi = {
  createPost,
}
export default postApi;
復制代碼

在上面的代碼中,我們定義了 createPost 函數,它是一個 async 函數,用來處理異步邏輯,在 createPost 函數中,我們對當前的環境進行了判斷,且只在微信小程序,即 isWeapp 的條件下執行創建帖子的操作,對于支付寶小程序和 H5,我們則放在下一節使用 LeanCloud 的 Serverless 來解決。

創建帖子邏輯是一個 try/catch 語句,用于捕捉可能存在的請求錯誤,在 try 代碼塊中,我們使用了 Taro 為我們提供的微信小程序云的云函數 API Taro.cloud.callFunction 來便捷的向小程序云發起云函數調用請求。

這里我們調用了一個 createPost 云函數,并將 postData 和 userId 作為參數傳給云函數,用于在云函數中使用用戶 Id 和帖子數據來創建一個屬于此用戶的帖子并保存到數據庫,我們將在下一節中實現這個云函數。

如果調用成功,我們可以接收返回值,用于從后端返回數據,這里我們返回了 result.post 數據。

如果調用失敗,則打印錯誤。

最后我們定義了一個 postApi 對象,用于存放所有和用戶邏輯有個的函數,并添加 createPost API 屬性然后將其導出,這樣在 post saga 函數里面就可以導入 postApi 然后通過 postApi.createPost 的方式來調用 createPost API 處理創建帖子的邏輯了。

在 API 默認文件統一導出

在 src/api/index.js 文件中導入上面創建的 postApi 并進行統一導出如下:

import postApi from './post'
export { postApi }
復制代碼

第五劍:創建對應的微信小程序云函數

創建 createPost 云函數

按照和之前創建 login 云函數類似,我們創建 createPost 云函數。

創建成功之后,我們可以得到兩個文件,一個是 functions/createPost/package.json 文件,它和之前的類似。

{
  "name": "createPost",
  "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"
  }
}
復制代碼

第二個文件就是我們需要編寫創建帖子邏輯的 functions/createPost/index.js 文件,微信小程序開發者工具會默認為我們生成一段樣板代碼。

我們在 function/createPost 文件夾下同樣運行 npm install 安裝對應的云函數依賴,這樣我們才能運行它。

編寫 createPost 云函數

打開 functions/createPost/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 { postData, userId } = event

  console.log('event', event)

  try {
    const user = await db
      .collection('user')
      .doc(userId)
      .get()
    const { _id } = await db.collection('post').add({
      data: {
        ...postData,
        user: user.data,
        createdAt: db.serverDate(),
        updatedAt: db.serverDate(),
      },
    })

    const newPost = await db
      .collection('post')
      .doc(_id)
      .get()

    return {
      post: { ...newPost.data },
    }
  } catch (err) {
    console.error(`createUser ERR: ${err}`)
  }
}
復制代碼

可以看到上面的代碼改動主要有以下七處:

  • 首先我們給 cloud.init() 傳入了環境參數,我們使用了內置的 cloud.DYNAMIC_CURRENT_ENV,表示自動設置為當前的云環境,即在右鍵點擊小程序開發者工具里 functions 文件夾時選擇的環境。
  • 接著,我們通過 cloud.database() 生成了數據實例 db ,用于之后在函數體中便捷的操作云數據庫。
  • 接著就是 main 函數體,我們首先從 event 對象中取到了在小程序的調用 Taro.cloud.callFunction 傳過來的 postData 和 userId 數據。
  • 然后,跟著取數據的是一個 try/catch 語句塊,用于捕獲錯誤,在 try 語句塊中,我們使用 db 的查詢操作: db.collection('user').doc(userId).get() ,表示查詢 id 為 userId 的 user 表數據,它查出來應該是個唯一值,如果不存在滿足 where 條件的,那么是一個 null值,如果存在滿足 條件的,那么返回一個 user 對象。
  • 接著,我們使用的 db.collection('post').add() 添加一個 post 數據,然后在 add 方法中傳入 data 字段,這里我們不僅傳入了 postData ,還將 user 也一同傳入了,原因我們將在之后來講解。除此之外,這里我們額外使用了 db.serverDate() 用于記錄創建此帖子的時間和更新此帖子的時間,方便之后做條件查詢。
  • 接著,因為向數據庫添加一個記錄之后只會返回此記錄的 _id ,所以我們需要一個額外的操作 db.collection('post').doc() 來獲取此條記錄,這個 doc 用于獲取指定的記錄引用,返回的是這條數據,而不是一個數組。
  • 最后我們返回新創建的 post 。

提示

我們在上面創建 post 的時候,將 user 對象也添加到了 post 數據中,這里是因為小程序云數據庫是 JSON 數據庫,所以沒有關系數據庫的外鍵概念,導致建關系困難,所以為了之后查詢 post 的時候方便展示 user 數據,我們才這樣保存的. 當然更加科學的做法是在 post 里面保存 userId ,這樣能減少數據冗余,但是因為做教學用,所以這些我們偷了一點懶。

所以我們這里強烈建議,在正規的環境下,關系型數據庫應該建外鍵,JSON 數據庫也至少應該保存 userId 。:

第六劍: 定義對應的 reducers 文件

我們在前面處理創建帖子時,在組件內部 dispatch 了 CREATE_POST action,在處理異步 action 的 saga 函數中,使用 put 發起了一系列更新 store 中帖子狀態的 action,現在我們馬上來實現響應這些 action 的 reducers ,打開 src/reducers/post.js ,對其中的代碼做出對應的修改如下:

import {
  SET_POST,
  SET_POSTS,
  SET_POST_FORM_IS_OPENED,
  POST_ERROR,
  CREATE_POST,
  POST_NORMAL,
  POST_SUCCESS,
} from '../constants/'

import avatar from '../images/avatar.png'

const INITIAL_STATE = {
  posts: [],
  post: {},
  isOpened: false,
  isPost: false,
  postStatus: POST_NORMAL,
}

export default function post(state = INITIAL_STATE, action) {
  switch (action.type) {
    case SET_POST: {
      const { post } = action.payload
      return { ...state, post }
    }

    case SET_POSTS: {
      const { posts } = action.payload
      return { ...state, posts: state.posts.concat(...posts) }
    }

    case SET_POST_FORM_IS_OPENED: {...      return { ...state, isOpened }
    }

    case CREATE_POST: {
      return { ...state, postStatus: CREATE_POST, isPost: true }
    }

    case POST_SUCCESS: {
      return { ...state, postStatus: POST_SUCCESS, isPost: false }
    }

    case POST_ERROR: {
      return { ...state, postStatus: POST_ERROR, isPost: false }
    }

    default:
      return state
  }
復制代碼

看一看到上面的代碼主要有三處改動:

  • 首先我們導入了必要的 action 常量
  • 接著我們給 INITIAL_STATE 增加了幾個字段:
    posts
    post
    
  • isPost :用于標志帖子邏輯過程中是否在執行創帖邏輯, true 表示正在執行創帖中, false 表示登錄邏輯執行完畢
    • postStatus :用于標志創帖過程中的狀態:開始創帖( CREATE_POST )、創帖成功( POST_SUCCESS )、登錄失?。?nbsp;POST_ERROR )
  • 最后就是 switch 語句中響應 action,更新相應的狀態。

“六脈神劍” 搞定 getPosts 異步邏輯

在上一 “大” 節中,我們使用了圖雀社區不傳之術:“六脈神劍” 搞定了 createPost 的異步邏輯,現在我們馬上趁熱打鐵來鞏固我們的武功,搞定 getPosts 異步邏輯,它對應著我們小程序底部兩個 tab 欄的第一個,也就是我們打開小程序的首屏渲染邏輯,也就是一個帖子列表。

第一劍: index 組件中發起異步請求

打開 src/pages/index/index.jsx 文件,對其中的內容作出對應的修改如下:

import { PostCard, PostForm } from '../../components'
import './index.scss'
import {
  SET_POST_FORM_IS_OPENED,
  SET_LOGIN_INFO,
  GET_POSTS,
} from '../../constants'

export default function Index() {
  const posts = useSelector(state => state.post.posts) || []...  const dispatch = useDispatch()

  useEffect(() => {
    const WeappEnv = Taro.getEnv() === Taro.ENV_TYPE.WEAPP

    if (WeappEnv) {
      Taro.cloud.init()
    }

    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()
    }

    async function getPosts() {
      try {
        // 更新 Redux Store 數據
        dispatch({
          type: GET_POSTS,
        })
      } catch (err) {
        console.log('getPosts ERR: ', err)
      }
    }

    if (!posts.length) {
      getPosts()
    }
  }, [])

  function setIsOpened(isOpened) {
    dispatch({ type: SET_POST_FORM_IS_OPENED, payload: { isOpened } })...  return (
    <View className="index">
      <AtMessage />
      {posts.map(post => (
        <PostCard key={post._id} postId={post._id} post={post} isList />
      ))}
      <AtFloatLayout
        isOpened={isOpened}
復制代碼

可以看到,上面的內容做了如下四處修改:

  • 首先我們對當前的開發環境做了判斷,如果是微信小程序環境,我們就使用 Taro.cloud.init() 進行小程序環境的初始化。
  • 接著,我們在 useEffects Hooks 里面定義了 getPosts 函數,它是一個異步函數,用于 dispatch GET_POSTS 的異步請求,并且我們進行了判斷,當此時 Redux Store 內部沒有文章時,才進行數據的獲取。
  • 接著,我們改進了 getStorage 獲取緩存的函數,將其移動到 useEffects Hooks 里面,并額外增加了 _id 屬性,它被賦值給 userId 一起設置 Redux Store 中關于用戶的屬性,這樣做的目的主要是為了之后發帖標志用戶,或者獲取用戶的個人信息用。并且,加了一層 if 判斷,只有當沒有登錄時,即 isLogged 為 false 的時候,才進行獲取緩存操作。
  • 最后我們導入了必要的 GET_POSTS 常量,并且將 return 語句里的 PostCard 接收的 key 和 postId 屬性變成了真實的帖子 _id 。這樣我們在帖子詳情時可以直接拿 postId 向小程序云發起異步請求。

注意

在上一篇教程中,有同學提到沒有使用 Taro.cloud.init() 初始化的問題,是因為分成了兩篇文章,在這篇文章才初始化。要使用小程序云,初始化環境是必要的。

第二劍:聲明和補充對應需要的異步 sagas 文件

在 “第一劍” 中,我們從組件中 dispatch 了 action.type 為 GET_POSTS 的異步 Action,接下來我們要做的就是在對應的 sagas 文件中補齊響應這個異步 action 的 sagas。

打開 src/sagas/post.js 文件,在其中定義 getPosts sagas 邏輯如下:

import {
  GET_POSTS,
} from '../constants'

function* getPosts() {
  try {
    const posts = yield call(postApi.getPosts)

    // 其實以下三步可以合成一步,但是這里為了講解清晰,將它們拆分成獨立的單元

    // 發起獲取帖子成功的 action
    yield put({ type: POST_SUCCESS })

    // 更新 Redux store 數據
    yield put({
      type: SET_POSTS,
      payload: {
        posts,
      },
    })
  } catch (err) {
    console.log('getPosts ERR: ', err)

    // 獲取帖子失敗,發起失敗的 action
    yield put({ type: POST_ERROR })
  }
}
function* watchGetPosts() {
  while (true) {
    yield take(GET_POSTS)

    yield fork(getPosts)
  }
}

export { watchGetPosts }
復制代碼

可以看到,上面的改動主要是創建 watcherSaga 和 handlerSaga 。

創建 watcherSaga

  • 我們創建了登錄的 watcherSaga : watchGetPosts ,它用來監聽 action.type 為 GET_POSTS的 action,并且當監聽到 GET_POSTS action 之后,然后激活 handlerSaga : getPosts 去處理對應的獲取帖子列表的邏輯。
  • 這里的 watcherSaga : watchGetPosts 是一個生成器函數,它內部是一個 while 無限循環,表示在內部持續監聽 GET_POSTS action。
  • 在循環內部,我們使用了 redux-saga 提供的 effects helper 函數: take ,它用于監聽 GET_POSTS action,獲取 action 中攜帶的數據。
  • 接著我們使用了另外一個 effects helper 函數: fork ,它表示非阻塞的執行 handlerSaga: getPosts ,因為這里獲取帖子列表不需要傳數據,所以這里沒有額外的數據傳遞邏輯。

創建 handlerSaga

  • 我們創建了創建帖子的 handlerSaga : getPosts ,它用來處理創建邏輯。
  • getPosts 也是一個生成器函數,在它內部是一個 try/catch 語句,用于處理獲取帖子列表請求可能存在的錯誤情況。
  • 在 try 語句中,首先是使用了 redux-saga 提供給我們的 effects helper 函數: call 來調用登錄的 API: postApi. getPosts 。
    • 如果獲取帖子列表成功,我們使用 redux-saga 提供的 effects helpers 函數: put , put 類似之前在 view 中的 dispatch 操作,,來 dispatch 了兩個 action: POST_SUCCESS , SET_POSTS ,代表更新獲取帖子列表成功的狀態,設置最新獲取的帖子列表到 Redux Store 中。
  • 如果獲取帖子列表失敗,我們則使用 put 發起一個 POST_ERROR 的 action 來更新獲取帖子列表失敗的信息到 Redux Store

一些額外的工作

為了創建 watcherSaga 和 handlerSaga ,我們還導入了 postApi. getPosts ,我們將在后面來創建這個 API。

除此之外我們還導入了需要使用的 action 常量:

  • GET_POSTS :響應獲取帖子列表的 ACTION 常量,我們將在 “第三劍” 中創建它。

加入 saga 中心調度文件

我們像之前將 watchCreatePost 等加入到 sagas 中心調度文件一樣,將我們創建好的 watchGetPosts 也加入進去:

// ...之前的邏輯
import { watchGetPosts } from './post'
export default function* rootSaga() {
  yield all([
   // ... 之前的邏輯
    fork(watchGetPosts)
  ])
}
復制代碼

第三劍:定義 sagas 需要的常量文件

打開 src/constants/post.js 文件,定義我們之前創建的常量文件如下:

export const GET_POSTS = 'GET_POSTS'
復制代碼

第四劍:定義 sagas 涉及到的前端 API 文件

在之前的 post saga 文件里面,我們使用到了 postApi.getPosts ,它里面封裝了用于向后端(這里我們是小程序云)發起和獲取帖子列表有關請求的邏輯,讓我們馬上來實現它吧。

打開 src/api/post.js 文件,并在其中編寫內容如下:

// ... 其余邏輯一樣
async function getPosts() {
  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: 'getPosts',
      })

      return result.posts
    }
  } catch (err) {
    console.error('getPosts ERR: ', err)
  }
}

const postApi = {
  // ... 之前的 API
  getPosts,
}

// ... 其余邏輯一樣
復制代碼

在上面的代碼中,我們定義了 getPosts 函數,它是一個 async 函數,用來處理異步邏輯,在 getPosts 函數中,我們對當前的環境進行了判斷,且只在微信小程序,即 isWeapp 的條件下執行獲取帖子列表的操作,對于支付寶小程序和 H5,我們則放在下一節使用 LeanCloud 的 Serverless 來解決。

創建帖子邏輯是一個 try/catch 語句,用于捕捉可能存在的請求錯誤,在 try 代碼塊中,我們使用了 Taro 為我們提供的微信小程序云的云函數 API Taro.cloud.callFunction 來便捷的向小程序云發起云函數調用請求。

這里我們調用了一個 getPosts 云函數,我們將在下一節中實現這個云函數。

如果調用成功,我們可以接收返回值,用于從后端返回數據,這里我們返回了 result.posts數據,即從小程序云返回的帖子列表。

如果調用失敗,則打印錯誤。

最后我們在已經定義好的 postApi 對象里,添加 getPosts API 屬性然后將其導出,這樣在 post saga 函數里面就可以導入 postApi 然后通過 postApi. getPosts 的方式來調用 getPosts API 處理獲取帖子列表的邏輯了。

第五劍:創建對應的微信小程序云函數

創建 getPosts 云函數

按照和之前創建 createPost 云函數類似,我們創建 getPosts 云函數。

創建成功之后,我們可以得到兩個文件,一個是 functions/getPosts/package.json 文件,它和之前的類似。

{
  "name": "getPosts",
  "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"
  }
}
復制代碼

第二個文件就是我們需要編寫創建帖子邏輯的 functions/getPosts/index.js 文件,微信小程序開發者工具會默認為我們生成一段樣板代碼。

我們在 function/getPosts 文件夾下同樣運行 npm install 安裝對應的云函數依賴,這樣我們才能運行它。

編寫 getPosts 云函數

打開 functions/getPosts/index.js 文件,對其中的內容作出對應的修改如下:

// 云函數入口文件
const cloud = require('wx-server-sdk')

cloud.init({
  env: cloud.DYNAMIC_CURRENT_ENV,
})

const db = cloud.database()
const _ = db.command

// 云函數入口函數
exports.main = async (event, context) => {
  try {
    const { data } = await db.collection('post').get()

    return {
      posts: data,
    }
  } catch (e) {
    console.error(`getPosts ERR: ${e}`)
  }
}
復制代碼

可以看到上面的代碼改動主要有以下處:

  • 首先我們給 cloud.init() 傳入了環境參數,我們使用了內置的 cloud.DYNAMIC_CURRENT_ENV,表示自動設置為當前的云環境,即在右鍵點擊小程序開發者工具里 functions 文件夾時選擇的環境。
  • 接著,我們通過 cloud.database() 生成了數據實例 db ,用于之后在函數體中便捷的操作云數據庫。
  • 接著就是 main 函數體,里面是一個 try/catch 語句塊,用于捕獲錯誤,在 try 語句塊中,我們使用 db 的查詢操作: db.collection('post').get() ,表示查詢所有的 post 數據。
  • 最后我們返回查詢到的 posts 數據。

第六劍: 定義對應的 reducers 文件

因為這里 SET_POSTS 的 Action 我們在上一 “大” 節中創建帖子時已經定義了,所有在 “這一劍” 中我們無需添加額外的代碼,復用之前的邏輯就好。

“六脈神劍” 搞定 getPost 異步邏輯

在上面兩 “大” 節中,我們連續用了兩次 “六脈神劍”,相信跟到這里的同學應該對我們接下來要做的事情已經輕車熟路了吧:grin:。

接下來,我們將收尾 Post 邏輯的最后一公里,即帖子詳情的異步邏輯 “getPost” 接入,話不多說就是干!

第一劍: post 組件中發起異步請求

打開 src/pages/post/post.jsx 文件,對其中的內容作出對應的修改如下:

import Taro, { useRouter, useEffect } from '@tarojs/taro'
import { View } from '@tarojs/components'
import { useDispatch, useSelector } from '@tarojs/redux'

import { PostCard } from '../../components'
import './post.scss'
import { GET_POST, SET_POST } from '../../constants'

export default function Post() {
  const router = useRouter()
  const { postId } = router.params

  const dispatch = useDispatch()
  const post = useSelector(state => state.post.post)

  useEffect(() => {
    dispatch({
      type: GET_POST,
      payload: {
        postId,
      },
    })

    return () => {
      dispatch({ type: SET_POST, payload: { post: {} } })
    }
  }, [])

  return (
    <View className="post">
復制代碼

可以看到,上面的內容做了如下四處修改:

  • 首先我們使用 useDispatch Hooks 獲取到了 dispatch 函數。
  • 接著,在 useEffects Hooks 里面定義了 dispatch 了 action.type 為 GET_POST 的 action,它是一個異步 Action,并且我們在 Hooks 最后返回了一個函數,其中的內容為將 post 設置為空對象,這里用到的 SET_POST 常量我們將在后面定義它。這個返回函數主要用于 post 組件卸載之后,Redux Store 數據的重置,避免下次打開帖子詳情還會渲染之前獲取到的帖子數據。
  • 接著,我們使用 useSelector Hooks 來獲取異步請求到的 post 數據,并用于 return 語句中的數據渲染。
  • 最后我們刪除了不必要的獲取 posts 數據的 useSelector Hooks,以及刪掉了不必要的調試 console.log 語句。

第二劍: 聲明和補充對應需要的異步 sagas 文件

在 “第一劍” 中,我們從組件中 dispatch 了 action.type 為 GET_POST 的異步 Action,接下來我們要做的就是在對應的 sagas 文件中補齊響應這個異步 action 的 sagas。

打開 src/sagas/post.js 文件,在其中定義 getPosts sagas 邏輯如下:

// ... 和之前的邏輯一樣
import {
  // ... 和之前的邏輯一樣
  SET_POST,
} from '../constants';

  // ... 和之前的邏輯一樣

function* getPost(postId) {
  try {
    const post = yield call(postApi.getPost, postId)

    // 其實以下三步可以合成一步,但是這里為了講解清晰,將它們拆分成獨立的單元

    // 發起獲取帖子成功的 action
    yield put({ type: POST_SUCCESS })

    // 更新 Redux store 數據
    yield put({
      type: SET_POST,
      payload: {
        post,
      },
    })
  } catch (err) {
    console.log('getPost ERR: ', err)

    // 獲取帖子失敗,發起失敗的 action
    yield put({ type: POST_ERROR })
  }
}
function* watchGetPost() {
  while (true) {
    const { payload } = yield take(GET_POST)

    yield fork(getPost, payload.postId)
  }
}

export { watchGetPost }
復制代碼

可以看到,上面的改動主要是創建 watcherSaga 和 handlerSaga 。

創建 watcherSaga

  • 我們創建了登錄的 watcherSaga : watchGetPost ,它用來監聽 action.type 為 GET_POST的 action,并且當監聽到 GET_POST action 之后,然后激活 handlerSaga : getPost 去處理對應的獲取單個帖子的邏輯。
  • 這里的 watcherSaga : watchGetPost 是一個生成器函數,它內部是一個 while 無限循環,表示在內部持續監聽 GET_POST action。
  • 在循環內部,我們使用了 redux-saga 提供的 effects helper 函數: take ,它用于監聽 GET_POST action,獲取 action 中攜帶的數據,這里我們拿到了傳過來的 payload 數據。
  • 接著我們使用了另外一個 effects helper 函數: fork ,它表示非阻塞的執行 handlerSaga: getPost ,并傳入了獲取到 payload.postId 參數。

創建 handlerSaga

  • 我們創建了獲取單個帖子的 handlerSaga : getPost ,它用來處理獲取帖子邏輯。
  • getPost 也是一個生成器函數,在它內部是一個 try/catch 語句,用于處理獲取單個帖子請求可能存在的錯誤情況。
  • 在 try 語句中,首先是使用了 redux-saga 提供給我們的 effects helper 函數: call 來調用登錄的 API: postApi. getPost 。
    • 如果獲取單個帖子成功,我們使用 redux-saga 提供的 effects helpers 函數: put , put 類似之前在 view 中的 dispatch 操作,,來 dispatch 了兩個 action: POST_SUCCESS , SET_POSTS ,代表更新獲取單個帖子成功的狀態,設置最新獲取的帖子到 Redux Store 中。
  • 如果獲取單個帖子失敗,我們則使用 put 發起一個 POST_ERROR 的 action 來更新獲取單個帖子失敗的信息到 Redux Store

一些額外的工作

為了創建 watcherSaga 和 handlerSaga ,我們還導入了 postApi.getPost ,我們將在后面來創建這個 API。

除此之外我們還導入了需要使用的 action 常量:

  • SET_POST :響應獲取帖子列表的 ACTION 常量,我們將在 “第三劍” 中創建它

加入 saga 中心調度文件

我們像之前將 watchGetPosts 等加入到 sagas 中心調度文件一樣,將我們創建好的 watchGetPost 也加入進去:

打開 src/sagas/index.js 文件,對其中的內容作出如下的修改:

import { fork, all } from 'redux-saga/effects'

import { watchLogin } from './user'
import { watchCreatePost, watchGetPosts, watchGetPost } from './post'

export default function* rootSaga() {
  yield all([
    fork(watchLogin),
    fork(watchCreatePost),
    fork(watchGetPosts),
    fork(watchGetPost),
  ])
}
復制代碼

第三劍:定義 sagas 需要的常量文件

打開 src/constants/post.js 文件,定義我們之前創建的常量文件 GET_POST :

export const SET_POST = 'SET_POST'
復制代碼

第四劍:定義 sagas 涉及到的前端 API 文件

在之前的 post saga 文件里面,我們使用到了 postApi.getPost ,它里面封裝了用于向后端(這里我們是小程序云)發起和獲取單個帖子有關請求的邏輯,讓我們馬上來實現它吧。

打開 src/api/post.js 文件,并在其中編寫內容如下:

// ... 其他內容和之前一致
async function getPost(postId) {
  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: 'getPost',
        data: {
          postId,
        },
      })

      return result.post
    }
  } catch (err) {
    console.error('getPost ERR: ', err)
  }
}

const postApi = {
  getPost,
}
export default postApi
復制代碼

可以看到上面的代碼有如下六處改動:

  • 在上面的代碼中,我們定義了 getPost 函數,它是一個 async 函數,用來處理異步邏輯,在 getPost 函數中,我們對當前的環境進行了判斷,且只在微信小程序,即 isWeapp 的條件下執行獲取單個帖子的操作,對于支付寶小程序和 H5,我們則放在下一節使用 LeanCloud 的 Serverless 來解決。

  • 創建帖子邏輯是一個 try/catch 語句,用于捕捉可能存在的請求錯誤,在 try 代碼塊中,我們使用了 Taro 為我們提供的微信小程序云的云函數 API Taro.cloud.callFunction 來便捷的向小程序云發起云函數調用請求。

  • 這里我們調用了一個 getPost 云函數,并給它傳遞了對應要獲取的帖子的 postId 我們將在下一節中實現這個云函數。

  • 如果調用成功,我們可以接收返回值,用于從后端返回數據,這里我們返回了 result.post數據,即從小程序云返回的單個帖子。

  • 如果調用失敗,則打印錯誤。

  • 最后我們在已經定義好的 postApi 對象里,添加 getPost API 屬性然后將其導出,這樣在 post saga 函數里面就可以導入 postApi 然后通過 postApi. getPost 的方式來調用 getPostAPI 處理獲取單個帖子的邏輯了。

第五劍:創建對應的微信小程序云函數

創建 getPost 云函數

按照和之前創建 getPosts 云函數類似,我們創建 getPost 云函數。

創建成功之后,我們可以得到兩個文件,一個是 functions/getPost/package.json 文件,它和之前的類似。

{
  "name": "getPost",
  "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"
  }
}
復制代碼

第二個文件就是我們需要編寫創建帖子邏輯的 functions/getPost/index.js 文件,微信小程序開發者工具會默認為我們生成一段樣板代碼。

我們在 function/getPost 文件夾下同樣運行 npm install 安裝對應的云函數依賴,這樣我們才能運行它。

編寫 getPost 云函數

打開 functions/getPost/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 { postId } = event

  try {
    const { data } = await db
      .collection('post')
      .doc(postId)
      .get()

    return {
      post: data,
    }
  } catch (e) {
    console.error(`getPost ERR: ${e}`)
  }
}
復制代碼

可以看到上面的代碼改動主要有以下處:

  • 首先我們給 cloud.init() 傳入了環境參數,我們使用了內置的 cloud.DYNAMIC_CURRENT_ENV,表示自動設置為當前的云環境,即在右鍵點擊小程序開發者工具里 functions 文件夾時選擇的環境。
  • 接著,我們通過 cloud.database() 生成了數據實例 db ,用于之后在函數體中便捷的操作云數據庫。
  • 接著就是 main 函數體,里面是一個 try/catch 語句塊,用于捕獲錯誤,在 try 語句塊中,我們首先從 event 對象里面獲取到了 postId ,接著我們使用 db 的查詢操作: db.collection('post').doc(postId).get() ,表示查詢所有的對應 _id 為 postId 的單個帖子數據
  • 最后我們返回查詢到的 post 數據。

第六劍: 定義對應的 reducers 文件

因為這里 SET_POST 的 Action 我們在上上 “大” 節中創建帖子時已經定義了,所有在 “這一劍” 中我們無需添加額外的代碼,復用之前的邏輯就好。

小結

在這篇教程中,我們連續使用了三次 “六脈神劍” 講完了我們的 Post 邏輯的異步流程,讓我們再來復習一下我們開頭提到的 “六脈神劍”:

sagas
sagas
sagas
reducers

這是一套講解模式,也是一套寫代碼的最佳實踐方式之一,希望你能受用。

鮮花
鮮花
雞蛋
雞蛋
分享至 : QQ空間
收藏
原作者: 圖雀社區 來自: 掘金
上证指数十年走势图