這裡所謂的 API 指的是 Client 向 Server 的某一個 Path 發出 Request,比如說:
初學者很可能用慣了 jQuery, 用的很開心,於是就會寫成這副德性:
@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> ); }}
這樣的寫法我個人覺得太冗長,不夠簡潔,不夠優雅,不易管理也不易維護。
其實要對 Server 發出 Request 有很多種作法,每個人習慣使用的 Library 可能也不盡相同,例如前面例子中的 jQuery,或者是目前最新潮的 Fetch API,我把這些用來協助我們發送 Request 的 Agent 統稱為 。
為了因應每個人不同的習慣,我在 Boilerplate 中把 ApiEngine 獨立抽象成一個 Class,目前使用的核心 Package 是 superagentopen_in_new,這個 Class 有四個對應 HTTP Verb 的 Instance Methods:、、、。
export default class ApiEngine { get(path, { params, data, files } = {}) { // ... }
post(path, { params, data, files } = {}) { // ... }
put(path, { params, data, files } = {}) { // ... }
del(path, { params, data, files } = {}) { // ... }};
這樣做的好處是,習慣使用 jQuery 的人可以寫自己的一套 jQueryApiEngine Class,習慣使用 Fetch 的使用者也可以寫一個 FetchApiEngine Class,只要 Expose 共同的四個 Instance Methods,無論你是 jQuery、Fetch 還是 Superagent,日後呼叫 API 的寫法都將會是一致的,如此便兼顧了彈性與可維護性。
我在實作上把 Instance 塞入 Flux 的 Store 中,搭配 這個 Action Creator 來切換目前要使用的 ApiEngine:
let apiEngine = new ApiEngine();store.dispatch(setApiEngine(apiEngine));
經過了 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}`),});
此外,每一個 API Function 都應該以 Promise 的形式來處理,這讓我們可以明確掌控非同步的流程、可以有一致的錯誤管理機制,而且最重要的是 Promise 在前後端都能跑,這就意味著它是 Isomorphic 的寫法,所以 ApiEngine 中的四個 Instance Methods 回傳值必須是 Promise。
我們就直接重構文首的範例吧,順便讓讀者可以比較其中差異。
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> ); }}
前面說到這種 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(); }); },};
這個 Server 端的使用範例是用來搭配 SSR 的頁面,當你想要在 Server 上就先載入 Store 中的某些 State 時,就可能需要在 Server 先呼叫 API。這麼說也許有點抽象,實際應用例如:部落格的文章,如果搭配在 Server 端預載入 State,搜尋引擎就能夠爬到文章內容,這對 SEO 是非常有幫助的!
一些網路範例中甚至會把 API 的呼叫包入 Action Creator 中,這就必須搭配 redux-thunkopen_in_new 一起服用了,在 Boilerplate 載入語系時有使用到這樣的寫法,但再講解下去就有點偏離本文了,有興趣的讀者請見原始碼:src/common/actions/intlActions.jsopen_in_new。
Day 12 - Infrastructure - Isomorphic API
Day 12 - Infrastructure - Isomorphic API