♪めぐり逢いを約束にいつかはきっと (c) 渡辺徹
非同期なファイルのリクエストをPromise化して関数を遅延実行するための簡易ラッパー
Promiseとかfetch()とか非同期でファイルを取得してから関数を遅延実行させるためのラッパー関数のようなものを作ってみた。 Promiseって.then()でいくらでも繋げるので確かに処理過程はわかりやすくなってるんだけど、でも一々書くのが面倒なのでそれらの処理をまとめて隠蔽。
だって、.then().then()とか書き連ねるのって全然イケてないじゃん…みたいなw
主な使い方など
ラッパーとしてrequestFile()とrequestFileHeader()の2つを用意してます。 下記ソースのコメント・アウトを外せば、グローバルな関数(API)として、どこでも使えるようになります。
- ファイルをリクエスト
-
requestFile(url, callback[, option]);
- レスポンス・ヘッダーでリクエスト
-
requestFileHeader(url, callback[, option]);
使い方は、例えば、こんな感じ。
/* JSONを取得して実行 */
function toggleJson(o) {
/* oにはJSON */
}
function onError(e) {
console.log(e.message);
}
requestFile('http://example.com/some.json', toggleJson, { method: 'GET', type: 'json', fail: onError });
/* 画像の存在確認後に実行 */
function imageExist(o) {
/* oには'a'か'b' */
}
requestFileHeader('http://example.com/a.png', imageExist, { value: 'a' });
requestFileHeader('http://example.com/b.png', imageExist, { value: 'b' });
引数について
- url
- ファイルのURL。必須。相対指定も可能。なお、httpまたはhttps以外の場合はエラーを返します
- callback
- レスポンス後に実行する関数。必須
- option
- オプション指定。{}(Object型)で指定。プロパティーはfetch()のinitに準じ、独自にtype、fail、valueも加えている。どのプロパティーも省略可能
{ method: requestMethod, type: responseType, headers: requestHeaders, body: requestBody, fail: errorHandling, value: returnValue, mode: mode, credentials: credentials, cache: cache }
- method:
- リクエスト・メソッド。'GET' / 'POST' / 'HEAD' / 'DELETE' / 'OPTIONS' / 'PUT'の何れか。省略された場合は'GET'
- type:
- レスポンス・タイプ。'arraybuffer' / 'blob' / 'document' / 'json' / 'text' / ''(空文字列)。省略された場合は''(空文字列)
- headers:
- リクエスト・ヘッダーの名前と値のペアのオブジェクト
headers: { 'Content-Type': 'application/json', 'Charset': 'UTF-8' }
または、Headersオブジェクト - body:
- リクエスト時に送信する本文。'GET' / 'HEAD'では無効
- fail:
- リクエストがリジェクトされたときなどに実行するエラー処理関数
- value:
- レスポンスの戻り値の代わりに遅延実行させる関数に渡す値。配列・真偽値・関数・日付・数値・任意のオブジェクト・正規表現・文字列・HTMLの要素など
- mode: / credentials: / cache:
- 実装しているがfetch()未対応のブラウザには完全に反映させることができないので使用には注意が必要。 また、クロス・ドメインのリクエストを行う場合は、modeに'cors'などを必ず指定すること
レスポンス・タイプを指定することによってJSON、ArrayBuffer、Blob、XML(HTML)やTextとして取得できます。
省略しても、JSONやXML/HTMLの場合には、ある程度自動判別されます。
/* JSONを取得して実行 */
function toggleJson(o) {
/* oにはJSON */
}
requestFile('http://example.com/some.json', toggleJson);
// typeを省略しても自動判別が効いてJSONで渡されるはず
ただし、古いブラウザの場合や、ファイルのcharsetがUTF-8以外の場合などは、レスポンス・タイプを指定していてもTextとして渡っていることがあり得ます。
遅延実行する関数で、再度、型のチェックをかけておいた方がいいかもしれません。
さらに、関数に渡す値を任意に指定できるようにもしてあります。 これはHEADで使用することを想定したものですが、GETなどでも一応使えます。
なお、GETやHEADでの動作は一通りの(つーか通り一遍w)テストを行いましたが、POST時の動作については未検証です。 また、fetch()のheaders(XHRのsetRequestHeader)なども、とりあえず実装してはみたものの、これまた実地未検証です(ぉ
ということで、ここは一つ他力本願でちゃんと動作したかどうかの結果報告を是非お願いします!w
だって、うちのサイトではPOSTとか使う機会がないんだもん…
自分でもいずれ機会を見つけてとは思っていますが、果たしていつになることやら。
♪いつかはきっと…つーてねw
IE9やAndroidブラウザでのバイナリ・データやクロス・ドメインの扱いについて
IEがXMLHttpRequestでバイナリ・データやクロス・ドメインのデータを扱えるようになったのはIE10からです。 AndroidブラウザもBlobの扱いに問題があります。 (今はなきBlobBuilder()でBlobは生成できるものの、windows.URLに未対応なためそのBlobのurlを作れない、など)
そこでIE9やAndroidブラウザ向けにいくつかフォールバックを適用させています。
レスポンス・タイプが'blob'で、かつ、リクエストしているファイルが画像やオーディオ、ビデオだった場合、IE9やAndroidブラウザではBlobオブジェクトではなくimg要素・audio要素・video要素が関数に渡されます。
したがって、関数の中でBlobなのかimgなどの要素なのかを判別する必要があります。
function loadVideo(o) {
/* oにはBlobまたはvideo要素 */
var x, y = document.createElement('video');
o && (x = _win.Blob && _win.URL && o.type ? URL.createObjectURL(o) : o.src && o.src);
x && (y.src = x);
}
requestFile('some.mp4', loadVideo, { type: 'blob' });
IE9ではレスポンス・タイプに'arraybuffer'を指定している場合は、ArrayBufferではなく、バイナリ・データを格納した配列を渡します。 Uint8Arrayと同じ配列です。
また、IE9では'blob'で取得しようとしているファイルが画像やオーディオ、ビデオ以外だった場合には('arraybuffer'を指定した時と同様に)バイナリ・データを格納した配列が渡されます。
さらに、IE9ではこれらのフォールバックが効くのは同一オリジンでのリクエストだけです。 クロス・ドメインのリクエストについては、IE9は未対応としています。
JavaScript (最終更新: 2015/5/9)
/**
* requestFile(), requestFileHeader()
* ファイルの取得をPromise化して関数を遅延実行するための簡易ラッパー
* Copyright (c) 2015 Kazz
* http://asamuzak.jp
* Dual licensed under MIT or GPL
* http://asamuzak.jp/license
*/
(function(_win, _doc) {
'use strict';
function onError(m) {
throw new Error(m);
}
function isType(o, t) {
var x = o && Object.prototype.toString.call(o).slice(8, -1);
return o && (t ? new RegExp('^' + t + '$', 'i').test(x) : x);
}
function isStringOrEmptyString(o) {
return /^(?:String)?$/i.test(isType(o));
}
function isValue(v) {
return isStringOrEmptyString(v) || isType(isType(v), 'String');
}
function getUriComponents(u) {
var x = { valid: null, uri: '', scheme: '', host: '', port: '', path: '', query: '', fragment: '' }, y, z, i, l;
if(isType(u, 'String')) {
y = u.match(/^([a-z][a-z0-9\+\-\.]*):(?:\/\/(?:(?:[a-z0-9\-\._~!\$&'\(\)\*\+,;=:]|%[0-9A-F]{2})*@)?(\[(?:(?:(?:(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|[0-9a-f]{1,4}?::(?:[0-9a-f]{1,4}:){4}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4}:){6})(?:(?:(?:(?:1[0-9]|[1-9])?[0-9]|2(?:[0-4][0-9]|5[0-5]))\.){3}(?:(?:1[0-9]|[1-9])?[0-9]|2(?:[0-4][0-9]|5[0-5]))|[0-9a-f]{1,4}:[0-9a-f]{1,4})|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)%25(?:[a-z0-9\-\._~]|%[0-9A-F]{2})+|(?:(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|[0-9a-f]{1,4}?::(?:[0-9a-f]{1,4}:){4}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4}:){6})(?:(?:(?:(?:1[0-9]|[1-9])?[0-9]|2(?:[0-4][0-9]|5[0-5]))\.){3}(?:(?:1[0-9]|[1-9])?[0-9]|2(?:[0-4][0-9]|5[0-5]))|[0-9a-f]{1,4}:[0-9a-f]{1,4})|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::|v[0-9a-f]+\.[a-z0-9\-\._~!\$&'\(\)\*\+,;=:]+)\]|(?:(?:(?:1[0-9]|[1-9])?[0-9]|2(?:[0-4][0-9]|5[0-5]))\.){3}(?:(?:1[0-9]|[1-9])?[0-9]|2(?:[0-4][0-9]|5[0-5]))|(?:[a-z0-9\-\._~!\$&'\(\)\*\+,;=]|%[0-9A-F]{2})*)(:[0-9]*)?((?:\/(?:[a-z0-9\-\._~!\$&'\(\)\*\+,;=:@]|%[0-9A-F]{2})*)*)|(\/(?:(?:[a-z0-9\-\._~!\$&'\(\)\*\+,;=:@]|%[0-9A-F]{2})+(?:\/(?:[a-z0-9\-\._~!\$&'\(\)\*\+,;=:@]|%[0-9A-F]{2})*)*)?|(?:[a-z0-9\-\._~!\$&'\(\)\*\+,;=:@]|%[0-9A-F]{2})+(?:\/(?:[a-z0-9\-\._~!\$&'\(\)\*\+,;=:@]|%[0-9A-F]{2})*)*|(?:[a-z0-9\-\._~!\$&'\(\)\*\+,;=:@]|%[0-9A-F]{2}){0}))(\?(?:[a-z0-9\-\._~!\$&'\(\)\*\+,;=:@\/\?]|%[0-9A-F]{2})*)?(#(?:[a-z0-9\-\._~!\$&'\(\)\*\+,;=:@\/\?]|%[0-9A-F]{2})*)?$/i);
if(y) {
u = /^[a-z][a-z0-9\+\-\.]*:[\/]{2}/.test(u);
x.valid = true;
for(i = 0, l = y.length; i < l; i++) {
z = y[i];
if(z) {
switch(i) {
case 0:
x.uri = z; break;
case 1:
x.scheme = z; break;
case 2:
u && (x.host = z); break;
case 3:
u && (x.port = z); break;
case 4:
u && (x.path = z); break;
case 5:
x.path = z; break;
case 6:
x.query = z; break;
case 7:
x.fragment = z; break;
}
}
}
}
else {
x.valid = false;
}
}
else {
x.valid = false;
}
return x;
}
function relativeReferenceToUri(r) {
var x = _doc.createElement('span'), y = _doc.createElement('a');
y.href = r;
x.appendChild(y);
r = x.firstChild.href;
x.removeChild(y);
return r;
}
function isUrl(u) {
if(isType(u, 'String')) {
!/^[a-z][a-z0-9\+\-\.]*:[\/]{2}/i.test(u) && (u = relativeReferenceToUri(u));
return getUriComponents(u).valid;
}
else {
return false;
}
}
function isJsonParsable(t) {
return t && /^application\/(?:[a-zA-Z0-9\.\-_]+\+)?json/.test(t);
}
function isDomParsable(t) {
return t && /^(?:(?:application\/(?:[a-zA-Z0-9\.\-_]+\+)?|image\/[a-zA-Z0-9\.\-_]+\+)x|text\/(?:ht|x))ml/.test(t);
}
function parseResponse(r, t) {
var x;
t && (x = t.match(/(;\s*charset=['\"]?utf-8[\"']?;?)$/i)) && (t = t.replace(x[1], ''));
return r && t && (isJsonParsable(t) ? JSON.parse(r) : isDomParsable(t) ? new DOMParser().parseFromString(r, t) : r);
}
function getXhrResponse(r, b) {
function getBlobFromBlobBuilder(s) {
var x = new WebKitBlobBuilder();
x.append(s);
return x.getBlob();
}
function getUint8ArrayFromVbArray(s) {
var x = new VBArray(s);
return x.toArray && x.toArray();
}
var t = r.getResponseHeader('content-type');
return r.response ? isType(r.response, 'String') ? b && /^blob$/.test(b) && _win.Blob && _win.WebKitBlobBuilder ? getBlobFromBlobBuilder(r.response) : parseResponse(r.response, t) : r.response : /Document$/.test(isType(r.responseXML)) ? r.responseXML : b && /^(?:arraybuffer|blob)$/.test(b) && r.responseBody !== undefined && _win.VBArray ? getUint8ArrayFromVbArray(r.responseBody) : parseResponse(r.responseText, t);
}
function getMethod(m) {
if(isType(m, 'String')) {
if(/^(?:TRAC[EK]|CONNECT)$/i.test(m)) {
return onError('Security Error: Forbidden Method');
}
else {
return /^(?:(?:P(?:OS|U)|GE)T|OPTIONS|DELETE|HEAD)$/i.test(m) ? m.toUpperCase() : onError('Syntax Error: Not a Method');
}
}
else {
return 'GET';
}
}
function getResponseType(t) {
return isStringOrEmptyString(t) && /^(?:(?:documen|tex)t|arraybuffer|blob|json)?$/i.test(t) && t.toLowerCase();
}
function getMode(s) {
return isType(s, 'String') && /^(?:cors(?:-with-forced-preflight)?|same-origin)$/i.test(s) ? s.toLowerCase() : 'no-cors';
}
function getCredentials(s) {
return isType(s, 'String') && /^(?:same-origin|include)$/i.test(s) ? s.toLowerCase() : 'omit';
}
function includeCredentials(o, s) {
return isType(o, 'String') && isType(s, 'Boolean') && (/^include$/.test(o) || /^same-origin$/.test(o) && s) ? true : false;
}
function isSameOrigin(u) {
var x = getUriComponents(_doc.URL), y = { same: null, origin: (x.scheme + '://' + x.host + x.port), scheme: null };
if(isType(u, 'String')) {
!/^[a-z][a-z0-9\+\-\.]*:[\/]{2}/i.test(u) && (u = relativeReferenceToUri(u));
u = getUriComponents(u);
y.same = u.scheme === x.scheme && u.host === x.host && u.port === x.port ? true : false;
y.scheme = u.scheme.toLowerCase();
}
return y;
}
function verifyHeaders(h) {
var x, y, z, i, l;
if(_win.Headers && isType(h, 'Headers')) {
return h;
}
else {
x = _win.Headers ? new Headers() : {};
y = /^(?:(?:Acce(?:ss-Control-Request-(?:Headers|Method)|pt-(?:Encoding|Charset))|Co(?:n(?:tent-Length|nection)|okie2?)|T(?:ra(?:nsfer-Encoding|iler)|E)|U(?:ser-Agent|pgrade)|(?:Expec|Hos)t|D(?:ate|NT)|Keep-Alive|Referer|Origin|Via)$|(?:Proxy|Sec)-)/i;
try {
Object.keys(h).forEach(function(n) {
if(!y.test(n)) {
if(_win.Headers) {
z = h[n].split(/\s*,\s*/);
for(i = 0, l = z.length; i < l; i++) {
x.append(n.toLowerCase(), z[i]);
}
}
else {
x[n.toLowerCase()] = h[n];
}
}
});
}
catch(e) {
}
return x;
}
}
function createInit(o) {
var x;
if(o && o.verify) {
x = { method: o.method, headers: o.headers, mode: o.mode, credentials: o.credentials, cache: o.cache };
o.body && (x.body = o.body);
}
else {
x = { method: 'GET', headers: _win.Headers ? new Headers() : {}, mode: 'no-cors', credentials: 'omit', cache: 'default' };
if(isType(o)) {
x.method = getMethod(o.method);
isType(o.headers) && (x.headers = verifyHeaders(o.headers));
isType(o.mode) && (x.mode = getMode(o.mode));
isType(o.credentials) && (x.credentials = getCredentials(o.credentials));
isType(o.cache, 'String') && /^(?:(?:no-(?:cach|stor)|force-cach)e|(?:only-if-cache|reloa)d)$/i.test(o.cache) && (x.cache = o.cache.toLowerCase());
!/GET|HEAD/.test(x.method) && isType(o.body) && (x.body = o.body);
}
}
return x;
}
function fetchFile(u, o) {
if(_win.Promise && (o && o.verify || isUrl(u))) {
if(_win.fetch) {
return _win.fetch(u, createInit(o)).then(function(r) {
return r.status === 200 ? Promise.resolve(r) : Promise.reject(new Error(r.statusText));
});
}
else {
return new Promise(function(r, s) {
var x, y = o && o.verify, m = 'GET', t = '', h = null, b = null, a = null, d = null;
y ? (y = o.origin, m = o.method, t = o.type, h = o.headers, o.body && (b = o.body), a = o.mode, d = o.credentials) : o && (y = o.origin, m = getMethod(o.method), t = getResponseType(o.type) || '', isType(o.headers) && (h = verifyHeaders(o.headers)), !/GET|HEAD/.test(m) && isType(o.body) && (b = o.body), isType(o.mode) && (a = getMode(o.mode)), isType(o.credentials) && (d = getCredentials(o.credentials)));
!y && (y = isSameOrigin(u));
if(!y.same && a === 'same-origin' || !/^https?$/.test(y.scheme)) {
s(new Error('Network Error'));
}
else {
x = new XMLHttpRequest();
x.addEventListener('load', function(evt) {
var z = evt.target;
z.status === 200 ? r(z) : s(new Error(z.statusText));
}, false);
x.addEventListener('error', function(evt) {
s(new Error(evt.target.statusText));
}, false);
x.open(m, u, true);
t !== '' && (x.responseType = t);
h && Object.keys(h).forEach(function(n) { x.setRequestHeader(n, h[n]); });
d && includeCredentials(d, y.same) && (x.withCredentials = true);
x.send(b);
}
});
}
}
}
function fetchFileByXhr(u, o) {
var x, y = o && o.verify, m = 'GET', c, t = '', h = null, b = null, f = null, a = null, d = null;
if(y || isUrl(u)) {
y ? (y = o.origin, m = o.method, c = o.callback, t = o.type, h = o.headers, o.body && (b = o.body), f = o.fail, a = o.mode, d = o.credentials) : isType(o) && (y = o.origin, m = getMethod(o.method), c = o.callback, t = getResponseType(o.type) || '', isType(o.headers) && (h = verifyHeaders(o.headers)), !/GET|HEAD/.test(m) && isType(o.body) && (b = o.body), isType(o.fail, 'Function') && (f = o.fail), isType(o.mode) && (a = getMode(o.mode)), isType(o.credentials) && (d = getCredentials(o.credentials)));
if(c) {
!y && (y = isSameOrigin(u));
if(!y.same && a === 'same-origin' || !/^https?$/.test(y.scheme)) {
return f ? f(new Error('Network Error')) : onError('Network Error');
}
else {
x = new XMLHttpRequest();
x.addEventListener('load', function(evt) {
var r = evt.target, t = o.type, c = o.callback, f = o.fail || null, v = o.value || null, z;
if(r.status === 200) {
z = r.getResponseHeader('content-type');
(z = z.match(/^((?:audi|vide)o|image)/)) && (z = z[1]);
if((!_win.Blob || _win.Blob && _win.WebKitBlobBuilder) && t === 'blob' && z) {
z === 'image' && (z = 'img');
r = _doc.createElement(z);
r.src = u;
c(v ? v : r);
}
else {
c(v ? v : getXhrResponse(r, t));
}
}
else {
f && f(new Error(r.statusText));
}
}, false);
x.addEventListener('error', function(evt) {
var f = o.fail || null;
f && f(new Error(evt.target.statusText));
}, false);
try {
x.open(m, u, true);
}
catch(e) {
x = null;
return f ? f(new Error(e.message)) : onError(e.message);
}
try {
t !== '' && (x.responseType = t);
h && Object.keys(h).forEach(function(n) { x.setRequestHeader(n, h[n]); });
d && includeCredentials(d, y.same) && (x.withCredentials = true);
}
catch(e) {
}
x.send(b);
}
}
}
}
function requestFile(u, c, o) {
var x = {}, t, f, v;
if(isUrl(u) && isType(c, 'Function')) {
o && (x = createInit(o), t = getResponseType(o.type) || '', f = isType(o.fail, 'Function') ? o.fail : null, v = isValue(o.value) ? o.value : null);
x.type = t;
x.origin = isSameOrigin(u);
x.verify = true;
if(_win.Promise) {
fetchFile(u, x).then(function(r) {
if(_win.fetch) {
switch(t) {
case 'arraybuffer':
return r.arrayBuffer();
case 'blob':
return r.blob();
case 'json':
return r.json();
default:
t = r.headers.get('content-type');
return isJsonParsable(t) ? r.json() : isDomParsable(t) ? r.text().then(function(s) { return parseResponse(s, t); }) : r.text();
}
}
else {
return getXhrResponse(r);
}
}).then(function(s) {
c(v ? v : s);
}).catch(function(e) {
f && f(e);
});
}
else {
x.callback = c;
f && (x.fail = f);
v && (x.value = v);
fetchFileByXhr(u, x);
}
}
}
function requestFileHeader(u, c, o) {
var x = {}, f, v;
if(isUrl(u) && isType(c, 'Function')) {
o && (x = createInit(o), f = isType(o.fail, 'Function') ? o.fail : null, v = isValue(o.value) ? o.value : null);
x.method = 'HEAD';
x.type = '';
x.origin = isSameOrigin(u);
x.verify = true;
if(_win.Promise) {
fetchFile(u, x).then(function(r) {
c(v ? v : r);
}).catch(function(e) {
f && f(e);
});
}
else {
x.callback = c;
f && (x.fail = f);
v && (x.value = v);
fetchFileByXhr(u, x);
}
}
}
/* APIとして外に出す場合 */
// _win.requestFile = requestFile;
// _win.requestFileHeader = requestFileHeader;
})(window, document);
"♪めぐり逢いを約束にいつかはきっと (c) 渡辺徹"へのTwitter上でのコメントやRT
5件のツイートがあります。
ツイート 1
asamuzaK.jp : ♪めぐり逢いを約束にいつかはきっと (c) 渡辺徹 ~ 非同期なファイルのリクエストをPromise化して関数を遅延実行するための簡易ラッパー http://t.co/F6IMY5cal2
ツイート 2
headersも関数を噛ませてとりあえず検証するようにしてみた ただし、依然として実地では未検証w asamuzaK.jp : ♪めぐり逢いを約束にいつかはきっと (c) 渡辺徹 http://t.co/F6IMY5cal2
ツイート 3
リクエスト・メソッドに、'GET' / 'POST' / 'HEAD' 以外にも 'DELETE' / 'OPTIONS' / 'PUT' も追加 asamuzaK.jp : ♪めぐり逢いを約束にいつかはきっと (c) 渡辺徹 http://t.co/F6IMY5cal2
ツイート 4
Fetch Standard https://t.co/88devPMGkP にあわせてあれやこれやと追加・修正してみたりなど asamuzaK.jp : ♪めぐり逢いを約束にいつかはきっと (c) 渡辺徹 http://t.co/F6IMY5cal2
ツイート 5
Fetch APIを便利に扱えるEasyAgent.jsを作りました http://t.co/1JE4SGyN4K 似たようなことを考えている人がいた! うちの野良スクリプトと同列視は迷惑かも新米けどやっぱFetchめんどいよね…w http://t.co/F6IMY5cal2