folder_open

30 天打造 MERN Stack Boilerplate

arrow_right
article

Day 12 - Infrastructure - Isomorphic API

Day 12 - Infrastructure - Isomorphic API

這裡所謂的 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>
);
}
}

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

ApiEngine

#

其實要對 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 } = {}) {
// ...
}
};

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

這樣做的好處是,習慣使用 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));

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

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.jsopen_in_new

此外,每一個 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.jsopen_in_new

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

用 Action Creator 包裝 API

#

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