這裡所謂的 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>
);
}
}
這樣的寫法我個人覺得太冗長,不夠簡潔,不夠優雅,不易管理也不易維護。
其實要對 Server 發出 Request 有很多種作法,每個人習慣使用的 Library 可能也不盡相同,例如前面例子中的 jQuery,或者是目前最新潮的 Fetch API,我把這些用來協助我們發送 Request 的 Agent 統稱為 ApiEngine
。
為了因應每個人不同的習慣,我在 Boilerplate 中把 ApiEngine 獨立抽象成一個 Class,目前使用的核心 Package 是 superagent,這個 Class 有四個對應 HTTP Verb 的 Instance Methods:get
、post
、put
、del
。
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 的寫法都將會是一致的,如此便兼顧了彈性與可維護性。
我在實作上把 apiEngine
Instance 塞入 Flux 的 Store 中,搭配 setApiEngine
這個 Action Creator 來切換目前要使用的 ApiEngine:
let apiEngine = new ApiEngine();
store.dispatch(setApiEngine(apiEngine));
完整程式碼:src/client/index.js
經過了 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。
我們就直接重構文首的範例吧,順便讓讀者可以比較其中差異。
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-thunk 一起服用了,在 Boilerplate 載入語系時有使用到這樣的寫法,但再講解下去就有點偏離本文了,有興趣的讀者請見原始碼:src/common/actions/intlActions.js。