iThome 鐵人賽 30天大致介紹完Saga後,要來把最後一塊拼圖拼起來了,看看要怎麼把呼叫API放置去由Saga端管理吧!
昨天安裝完環境後今天就直接開始寫程式了,跟之前一樣,先在.\src內新增sagas的資料夾,裡面放saga的檔案。
.\src\sagas\index.js初始檔案:
import { takeEvery } from 'redux-saga/effects'
import * as action from "../action"
export default function* () {
}
再來為了把saga加進Redux的流程,所以在store更動一下程式碼,.\src\store\index.js:
import { createStore, applyMiddleware } from 'redux'
import rootReducer from '../reducer'
import createSagaMiddleware from 'redux-saga'
import sagas from '../sagas'
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
    rootReducer,
    applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(sagas)
export default store
就可以開始在Saga建立起一套API機制啦!
先來建立一套API呼叫機制與function,來方便之後不需要重複寫一大堆的程式碼,這裡利用了axios套件 (.\src\lib\api.js):
import axios from "axios"
const apiurl = 'http://localhost:5000' // 設定API網址
const cors = false
export default function ({ cmd, method = "GET", type = "json", data = {}, header = {}, fileList = [] }) {
  method = method.toUpperCase()
  type = type.toLowerCase()
  let url = `${apiurl}/${cmd}`
  let option = {
    method,
    headers: {
      "Content-Type": "application/json",
      ...header,
    },
  }
  if (fileList.length) {
    let formData = new FormData()
    option.headers["Content-Type"] = "multipart/form-data"
    let filelist = [...fileList]
    filelist.forEach((file) => {
      formData.append("files", file)
    })
    Object.keys(data).map((key) => {
      formData.append(key, data[key])
    })
    data = formData
  }
  switch (method) {
    case "POST":
    case "PUT":
    case "DELETE":
      option.data = data
      break
    case "GET":
      option.params = data
      break
  }
  return axios({
    method,
    url,
    ...option,
    withCredentials: !!cors,
  })
    .then((res) => {
      return {
        ok: res.status == "200",
        status: res.status,
        body: res.data,
      }
    })
    .catch((err) => {
      let res = err.response
      if (res) {
        return {
          ok: res.status == "200",
          status: res.status,
          body: res.data,
        }
      } else {
        return {
          ok: false,
          status: 404,
          body: err.message,
        }
      }
    })
}
接下來來實作如何從Component呼叫action到saga取得API資料後再把資料設定到reducer state去,看看Redux-Saga實際上是怎麼運作的。
從首頁取得資料開始,因為需要有取得資料的action,所以先建立action (.\src\action\home.js):
export const GET_DATA = "GET_DATA"
export const getData = () => action("GET_DATA", {})
container引入action (getData) 讓component使用 (src\containers\Main\HomePage.jsx):
import { connect } from 'react-redux'
import HomePage from '../../components/Main/HomePage'
import { setData, getData } from '../../action'
const mapStateToProps = (state) => ({
  home: state.home
})
const mapDispatchToProps = {
  setData,
  getData,
}
export default connect(mapStateToProps, mapDispatchToProps)(HomePage)
component修改取代component呼叫API (.\src\components\Main\HomePage.jsx):
import React, { useState, useEffect } from 'react'
import { Box } from '@mui/material'
import VideoCard from '../VideoCard'
export default function HomePage(props) {
  const { home, setData, getData } = props
  const { data } = home
  useEffect(() => {
    // 第一次進入HomePage.jsx時去取得資料
    getData()
  }, [])
  // const getData = async () => {
  //   // fetch取得API的資料
  //   const data = await fetch('http://localhost:5000/api/Notes').then(res => res.json())
  //   setData(data)
  // }
  return (
    <Box sx={{ p: 3, display: "flex", flexWrap: "wrap", justifyContent: "center" }}>
      {!!data && data.map(d =>
        <VideoCard
          key={d.v}
          v={d.v}
          title={d.title}
        />
      )}
    </Box>
  )
}
saga建立對應function並呼叫API (src\sagas\home.js):
import { take, call, put, select } from 'redux-saga/effects'
import * as action from '../action'
import api from '../lib/api'
const get_data = () => {
  return api({
    method: "GET",
    cmd: "api/Notes",
  })
}
export function* getData({ }) {
  // 呼叫相對應API
  let data = yield call(get_data,)
  if (data.ok) {
    // 取得資料後再次呼叫action來設定reducer資料
    yield put(action.setData(data.body))
  }
}
Saga入口連接對應function (.\src\sagas\index.js):
import { takeEvery } from 'redux-saga/effects'
import * as action from "../action"
import * as home from './home'
export default function* () {
  yield takeEvery(action.GET_DATA, home.getData)
}
完成後就可以看到畫面恢復正常了!
之後一樣是運用上述所講的流程去設計,把整個API呼叫搬移至Saga,就完成整個網站了 (歡迎大家動手實作)!最後的最後就把首頁新增影片後要重新更新取得資料的程式寫完今天就差不多啦!
.\src\action\home.js新增影片的action:
export const ADD_VIDEO = "ADD_VIDEO"
export const addVideo = (v, title) => action("ADD_VIDEO", { v, title })
.\src\sagas\home.js新增saga對應function,並在呼叫完新增影片後去呼叫取得首頁影片的action,達到更新:
const add_video = (req) => {
  return api({
    method: "GET",
    cmd: "api/Notes",
    data: req
  })
}
export function* addVideo({ v, title }) {
  let data = yield call(add_video, { v, title })
  if (data.ok) {
    yield put(action.getData())
  }
}
連接對應saga function (.\src\sagas\index.js):
import { takeEvery } from 'redux-saga/effects'
import * as action from "../action"
import * as home from './home'
export default function* () {
  yield takeEvery(action.GET_DATA, home.getData)
  yield takeEvery(action.ADD_VIDEO, home.addVideo)
}
Header新增影片後要去呼叫getData,所以新增Header的container (.\src\containers\Header.jsx):
import { connect } from 'react-redux'
import Header from '../components/Header'
import { addVideo } from '../action'
const mapStateToProps = (state) => ({})
const mapDispatchToProps = {
    addVideo,
}
export default connect(mapStateToProps, mapDispatchToProps)(Header)
更改App引用路徑.\src\App.js:
import Header from './containers/Header'
修改component呼叫新增影片 (.\src\components\Header.jsx):
const Header = (props) => {
  const { addVideo } = props
  const [open, setOpen] = React.useState(false);
  const handleClickOpen = () => {
    setOpen(true);
  };
  const handleClose = () => {
    setOpen(false);
  };
  const handleAddVideo = async (form) => {
    addVideo(form.v, form.title)
    handleClose()
  }
  return (
    <Box color="inherit">
      <AppBar position="static" sx={{ backgroundColor: "#000" }}>
        <Toolbar>
          <Typography
            variant="h6"
            noWrap
            component="div"
            sx={{ flexGrow: 1 }}
          >
            <Link className="reset white" to="/">
              {props.title}
            </Link>
          </Typography>
          <Search>
            <SearchIconWrapper>
              <SearchIcon />
            </SearchIconWrapper>
            <StyledInputBase
              placeholder="Search…"
              inputProps={{ 'aria-label': 'search' }}
            />
          </Search>
          <Box sx={{ flexGrow: 1 }} />
          <Button variant="contained" color="success" startIcon={<AddIcon />} onClick={handleClickOpen}>
            新增影片
          </Button>
        </Toolbar>
      </AppBar>
      <AddVideoForm open={open} handleClose={handleClose} handleSummit={handleAddVideo} />
    </Box >
  )
}
export default Header
終於完成連續30天的鐵人賽了!一開始由一群實驗室的夥伴慫恿參加到現在真的參加完全程真的是不簡單,雖然我的文章比較水一點比不上我們團隊其他人的水準,但我覺得算是滿意了 (畢竟也是做完了一個side project)!
不過這其實算是我研究生所做的系統IT108之中的一個小影片筆記系統,只是拉出來全部重新組裝重寫的小改版,有興趣的話也可以上來使用看看系統,但還有很多技術需要繼續琢磨啦!
另外這個小project其實還有很多地方可以改進的地方,但礙於篇幅與時間不足,所以沒辦法講得這麼詳細 (還有會想偷懶的因素?),不然其實可以把整個專案分流分配得更好,還可以在redux saga內加入logger方便debug~之後如果有什麼問題的話都歡迎來詢問,如果想要讓我更完善這個side project也可以說,不然的話你也可以動手改看看喔!看能不能超越我的這版來互相交流~
最後就下台一鞠躬啦!謝謝大家的收看~
對資安或Mapbox有興趣的話也可以觀看我們團隊的鐵人發文喔~