iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 12
0
Modern Web

30 天打造 MERN Stack Boilerplate系列 第 12

Day 12 - Infrastructure - Isomorphic API

這裡所謂的 API 指的是 Client 向 Server 的某一個 Path 發出 Request,比如說:

  • POST /todos
  • GET /todos/9527

初學者很可能用慣了 jQuery,$.ajax 用的很開心,於是就會寫成這副德性:

@connect(({ todo }) => ({ todo }))
class ExampleComponent extends Component {
  componentDidMount() {
    let { dispatch } = this.props;

    $.ajax({
      url: '/todos/9527',
      type: 'GET',
      dataType: 'json',
      success: (json) => {
        dispatch(setTodo(json.todo));
      },
    });
  }

  render() {
    return (
      <div>{this.props.todo.text}</div>
    );
  }
}

這樣的寫法我個人覺得太冗長,不夠簡潔,不夠優雅,不易管理也不易維護。

ApiEngine

其實要對 Server 發出 Request 有很多種作法,每個人習慣使用的 Library 可能也不盡相同,例如前面例子中的 jQuery,或者是目前最新潮的 Fetch API,我把這些用來協助我們發送 Request 的 Agent 統稱為 ApiEngine

為了因應每個人不同的習慣,我在 Boilerplate 中把 ApiEngine 獨立抽象成一個 Class,目前使用的核心 Package 是 superagent,這個 Class 有四個對應 HTTP Verb 的 Instance Methods:getpostputdel

export default class ApiEngine {
  get(path, { params, data, files } = {}) {
    // ...
  }

  post(path, { params, data, files } = {}) {
    // ...
  }

  put(path, { params, data, files } = {}) {
    // ...
  }

  del(path, { params, data, files } = {}) {
    // ...
  }
};

完整程式碼:src/common/utils/ApiEngine.js

這樣做的好處是,習慣使用 jQuery 的人可以寫自己的一套 jQueryApiEngine Class,習慣使用 Fetch 的使用者也可以寫一個 FetchApiEngine Class,只要 Expose 共同的四個 Instance Methods,無論你是 jQuery、Fetch 還是 Superagent,日後呼叫 API 的寫法都將會是一致的,如此便兼顧了彈性與可維護性。

我在實作上把 apiEngine Instance 塞入 Flux 的 Store 中,搭配 setApiEngine 這個 Action Creator 來切換目前要使用的 ApiEngine:

let apiEngine = new ApiEngine();
store.dispatch(setApiEngine(apiEngine));

完整程式碼:src/client/index.js

Isomorphic API & Promise

經過了 ApiEngine 的包裝,實務上開 API 模組時還能夠再包裝成 Function,並且加上 RESTful 的語意:

export default (apiEngine) => ({
  list: ({ page }) => apiEngine.get('/api/todos', { params: { page } }),
  create: (todo) => apiEngine.post('/api/todos', { data: todo }),
  read: (id) => apiEngine.get(`/api/todos/${id}`),
  update: (id, todo) => apiEngine.put(`/api/todos/${id}`, { data: todo }),
  remove: (id) => apiEngine.del(`/api/todos/${id}`),
});

完整程式碼:src/common/api/todo.js

此外,每一個 API Function 都應該以 Promise 的形式來處理,這讓我們可以明確掌控非同步的流程、可以有一致的錯誤管理機制,而且最重要的是 Promise 在前後端都能跑,這就意味著它是 Isomorphic 的寫法,所以 ApiEngine 中的四個 Instance Methods 回傳值必須是 Promise

如何在 Client 端使用 API

我們就直接重構文首的範例吧,順便讓讀者可以比較其中差異。

import { connect } from 'react-redux';
import todoAPI from '/common/api/todo';

@connect(({ apiEngine, todo }) => ({ apiEngine, todo }))
class ExampleComponent extends Component {
  componentDidMount() {
    let { dispatch, apiEngine } = this.props;

    todoAPI(apiEngine)
      .read(9527)
      .then((json) => {
        dispatch(setTodo(json.todo));
      });
  }

  render() {
    return (
      <div>{this.props.todo.text}</div>
    );
  }
}

如何在 Server 端使用 API 預載入 State

前面說到這種 API 寫法是 Isomorphic,所以看過 Client 端的例子再來看看 Server 端的用法:

export default {
  // ... other controllers
  fetchTodo: (req, res, next) => {
    todoAPI(req.store.getState().apiEngine)
      .read(req.params.todoId)
      .then((json) => {
        req.store.dispatch(setTodo(json.todo));
        next();
      });
  },
};

完整程式碼:src/server/controllers/ssrFetchState.js

這個 Server 端的使用範例是用來搭配 SSR 的頁面,當你想要在 Server 上就先載入 Store 中的某些 State 時,就可能需要在 Server 先呼叫 API。這麼說也許有點抽象,實際應用例如:部落格的文章,如果搭配在 Server 端預載入 State,搜尋引擎就能夠爬到文章內容,這對 SEO 是非常有幫助的!

用 Action Creator 包裝 API

一些網路範例中甚至會把 API 的呼叫包入 Action Creator 中,這就必須搭配 redux-thunk 一起服用了,在 Boilerplate 載入語系時有使用到這樣的寫法,但再講解下去就有點偏離本文了,有興趣的讀者請見原始碼:src/common/actions/intlActions.js


上一篇
Day 11 - Infrastructure - Introduction
下一篇
Day 13 - Infrastructure - Error Handling
系列文
30 天打造 MERN Stack Boilerplate30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言