Skip to content

Instantly share code, notes, and snippets.

@niaeashes
Last active December 9, 2016 06:45
Show Gist options
  • Save niaeashes/4aa50753b604de21e069b38158b117b2 to your computer and use it in GitHub Desktop.
Save niaeashes/4aa50753b604de21e069b38158b117b2 to your computer and use it in GitHub Desktop.
'use strict'
import pluralize from 'pluralize'
import ApiRequest from 'lib/ApiRequest'
import { SubmissionError } from 'redux-form'
const ADD_API_RESOURCE = '@api/ADD_RESOURCE'
const REMOVE_API_RESOURCE = '@api/REMOVE_RESOURCE'
class API {
constructor() {
this.dispatch = null
this.token = null
this.host = null
this.requests = []
this.resources = {}
this.tokenStrage = 'accessToken'
}
/**
* apiConfig is Immutable.Map
*/
setup(apiConfig) {
this.host = apiConfig.get('scheme', 'https')+'://'+apiConfig.get('host', 'localhost')+apiConfig.get('endpoint', '')
if ( this.host == null ) console.error('API host is empty. Use absolute path.')
this.tokenStrage = apiConfig.get('tokenStrage', this.tokenStrage)
}
init(dispatch, callback) {
this.dispatch = dispatch
this.token = localStorage.getItem(this.tokenStrage)
if ( this.token ) {
this.fetch('/token/verify')
.then(data => callback(data.success ? this.token : null))
} else {
callback(null)
}
}
syncToken(token) {
this.token = token
if ( this.token ) {
localStorage.setItem(this.tokenStrage, this.token)
} else {
localStorage.removeItem(this.tokenStrage)
}
}
generateReducer() {
const initialState = {
resources: {}
}
return (state = initialState, action) => {
if ( action.type != ADD_API_RESOURCE && action.type != REMOVE_API_RESOURCE ) return state
if ( action.type == ADD_API_RESOURCE ) {
return {
...state,
resources: {
...state.resources,
[action.identifier]: { api: { fetching: false }, data: action.resource, fetching: false }
}
}
}
if ( action.type == REMOVE_API_RESOURCE ) {
return {
...state,
resources: { ...state.resources, [action.identifier]: null }
}
}
return state
}
}
getCollection(resourceName, store, options = {}) {
const identifier = this._getIdentifier(resourceName)
const newOptions = { defaultData: this._defaultData(identifier, []), ...options }
return this.getRequest(resourceName, store, null, newOptions)
}
getSingle() {
return this.getCollection(...arguments)
}
get(resourceName, store, id, options = {}) {
return this.getRequest(resourceName, store, id, options)
}
getRequest(resourceName, store, id, options) {
const identifier = this._getIdentifier(resourceName, id)
const name = options['name'] || resourceName
if ( ! this._requestDoing(identifier) && ! this.hasResource(store, identifier) ) {
this._addRequest(identifier, () => {
this
.fetch(this._getResourceUrl(resourceName, id), { method: 'get' })
.then(data => this._processResource(identifier, resourceName, id, options, data))
})
}
return store.api.resources[identifier] || options.defaultData || this._defaultData(identifier)
}
post(resourceName, data) {
const identifier = this._getIdentifier(resourceName, null, 'post')
}
put(resourceName, id, data) {
const identifier = this._getIdentifier(resourceName, id, 'put')
}
// del means "delete".
// delete is reserved keyword as of ECMAScript 6.
del(resourceName, id) {
const identifier = this._getIdentifier(resourceName, id, 'del')
}
hasResource(store, identifier) {
return store.api.resources[identifier] != null
}
releaseResource(resourceName, id = null) {
let identifier = this._getIdentifier(resourceName, id)
this.dispatch({ type: REMOVE_API_RESOURCE, identifier })
}
fetch(path, options = {}) {
return (new ApiRequest({ ...options, token: this.token }, this.dispatch))
.promise(this._getURL(path))
}
form(path, options = {}) {
return this.fetch(path, options).then(this.processForm)
}
processForm(data) {
if ( data.success ) {
return data
} else {
throw new SubmissionError(data.errors)
}
}
_getIdentifier(resourceName, id = null, type = 'get') {
if ( id === null ) {
return `@${type}:${resourceName}`
} else {
return `@${type}:${resourceName}:${id}`
}
}
_defaultData(identifier, defaultObject = {}) {
return {
api: { fetching: this._requestDoing(identifier) },
data: defaultObject,
fetching: this._requestDoing(identifier)
}
}
_processResource(identifier, resourceName, id, options, data) {
const name = options['name'] || resourceName
const { dispatch } = this
dispatch({ type: ADD_API_RESOURCE, resource: data[name], identifier })
if ( this._requestDoing(identifier) ) this._removeRequest(identifier)
}
_getResourceUrl(resourceName, id = null) {
if ( id === null ) {
return `/${resourceName}`
} else {
return `/${pluralize(resourceName)}/${id}`
}
}
_getURL(path) {
return this.host + `/${path}`.replace('//', '/')
}
/**
* Request list controller
*/
_addRequest(identifier, callback) {
const { dispatch } = this
this.requests = [ ...this.requests, identifier ]
if ( typeof callback === 'function' ) callback()
}
_removeRequest(identifier, callback) {
const { dispatch } = this
let index = this.requests.indexOf(identifier)
if ( index > -1 ) {
this.requests = this.requests.slice(0, index).concat(this.requests.slice(index+1))
}
if ( typeof callback === 'function' ) callback()
}
_requestDoing(identifier) {
let index = this.requests.indexOf(identifier)
return index > -1
}
}
export default (new API())
import fetch from 'isomorphic-fetch'
class ApiRequest {
constructor(options, dispatch) {
this.options = options
this.dispatch = dispatch
}
promise(url) {
let options = { ...this.options }
options.headers = {
...options.headers,
'Accept': 'application/json',
'Content-Type': 'application/json; charset=UTF-8'
}
if ( options.token ) {
options.headers['X-Access-Token'] = options.token
delete options.token
}
if ( options.params ) {
path += this._generateParamsString(options.params)
delete options.params
}
if ( options.data ) {
options.body = JSON.stringify(options.data)
delete options.data
}
if ( options.file ) {
delete options.headers['Content-Type']
let formData = new FormData()
formData.append("file", options.file)
options.body = formData
delete options.file
}
return this._generatePromise({ url, options })
}
_generateParamsString(baseParams) {
if ( typeof baseParams.length == undefined || baseParams.length == 0 ) return ''
let params = []
for ( var name in baseParams ) {
let value = baseParams[name]
if ( Array.isArray(value) ) {
for ( var d in value ) {
params.push(`${encodeURIComponent(name)}[]=${encodeURIComponent(value[d])}`)
}
} else {
params.push(`${encodeURIComponent(name)}=${encodeURIComponent(value)}`)
}
}
return params.length > 0 ? "?"+params.join('&') : ''
}
_generatePromise(request) {
let promise = fetch( request.url, request.options )
return promise.then(response => this._process(request, response, this.dispatch))
}
_process(request, response, dispatch) {
return response.json().then(data => {
if ( process.env.NODE_ENV == 'development' ) {
console.log(request.url, request, response, data)
}
if ( data.action && data.action == 'reset_token' ) dispatch(resetToken())
return data
})
}
}
export default ApiRequest
@niaeashes
Copy link
Author

以下メモだけどアレ、あのアレ、適当だからアレ。

  • APIへのリクエストをインスタンスにする?
  • リクエストはRender後に処理開始されるようにしたい。なぜかというと、本質的に同じリクエストは1度に処理したいから。
  • リクエスト開始時、レスポンス受け取り時にイベントを挟む。
  • APIレスポンスに含まれるAssociationをポスト用に変換する処理をAPIに実装する。例えば、ranks を ranks_attributes に変更する。現状ではレスポンスに ranks および ranks_attributes が両方含まれてしまっていて無駄だし、クライアント側で ranks と rnaks_attributes を区別しなければならない。
  • レスポンスを受け取ると汎用アクションが発行される。
  • APIへのリクエストは典型的なものと、そうでないものがある。典型的なリクエストは、クラスリストの取得、ログインしているユーザーの情報取得など。典型的でないリクエストは、検索リクエストなど、パラメータがいろいろ考えられるもの。典型的なものはフラグを立てて、短い期間に連続したリクエストが行われないようにしたい。
  • APIへのリクエストを行っている時、その情報をStoreに持たせる案はある。ただこれすると「APIのリクエスト」というインスタンスが状態だけをStoreに投げてることになるので、やりすぎな気がする。そもそもAPIの処理はFluxサイクルの外部システムだと考えたほうがいいのではないだろうか。
  • APIへのリクエストを発行する仕組み、APIからのレスポンスを受け取る仕組み、APIからのリクエストをStoreに保存する仕組み、保存したStoreからデータを受け取る仕組み、リクエスト発行者が適切にデータを受け取る仕組み、の構築が必要になる。
  • 「任意のデータをリクエストする」時点で、それがAPIに依存する場合、APIに対してリクエストを発行する。また、データには待ち時間が発生する。
  • ReduxとconnectされたComponentが特定のStoreをリクエストした場合に限り、APIに対してリクエストが発行される方がいい。connect内部でStoreから単にデータを受け取るのではなく、データ受取用のオブジェクトを経由すると検出できるのではないだろうか。Decorator的な。
  • DataRequesterみたいなの。
  • APIから受け取ったデータは変更され、変更結果がAPIに対してSyncされることがある。したがって、APIから受け取ったデータそのものは独立していたほうがいい。

connect(
(state) => ({
articles: API.getCollection('articles', state)
})
)(SampleComponent)

connect(
(state, ownProps) => ({
article: API.get('article', ownProps.params.id, state)
}),
(dispatch, ownProps) => ({
onSubmit: (data) => {
API.put('article', ownProps.params.id, data)
},
onSubmitSuccess: (response) => {
},
onSubmitFailure: (errors) => {
}
})
)(SampleComponent)

  • API.get および API.getCollection の返り値は、{ api: [Object], data: [Object or Array], fetching: Boolean } の形式を取る。fetching は api.fetching と同値。api には API から返される固有の情報が格納されている。
  • API.get や API.getCollection 時、すでに同一 API にリクエストが発行されている場合、リクエストの発行はキャンセルされる。
  • APIモジュールはReducerを提供する。コレを使うことでComponentはAPIの状態を知ることもできる。
  • 典型的なAPIへのリクエストは、コレクションの場合、"@collection:klasses" のような識別子で、単一リソースの場合、"@single:klass:1" のような識別子で区別される。典型的でない複雑なパラメータを持つリクエストは、パラメータの結合ハッシュを付与して "@collection:klasses:{Hash}" "@single:klass:1:{Hash}" のように識別される。
  • この記法だと、APIを使ったデータとそうでないデータを一意に区別できる。
  • APIへのリクエストをアクションとして処理する必要がなくなる。単にComponentに渡されるデータの変更と考えればオッケー。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment