JScriptでaxiosを使う

JavaScriptのXHRライブラリに[axios](https://github.com/mzabriskie/axios)があります。
axiosはPromiseが使えるので個人的に好きです。

JSCriptでもXHRが使えるのですが、そのままではaxiosを使うことができません。
なので今回はaxiosを少しカスタマイズして、JScriptからでも使えるようにしてみました。
[babelを使ってES2015でJScriptを書こう](http://qiita.com/ABCanG1015/items/80b5733b5a96eeff9945)などを参考にJScriptでPromiseが使えるようにしておいてください。
[Promiseのpolyfill](https://github.com/lahmatiy/es6-promise-polyfill)を使ってもできるかもしれません。(未検証)

## カスタムアダプタの設定

axios.createを呼ぶことで、カスタマイズした設定を反映したインスタンスを生成することができます。
axiosはadapterと呼ばれるモジュールで実際の通信を行います。
設定の`adapter`にカスタマイズしたadapterを指定すると、通信時に優先的に呼ばれるようになります。

以下のコードはaxios v0.13の`lib/adapters/xhr.js`をベースに改変しています。
カスタムアダプタの仕様が変わると使えなくなるかもしれません。

```js:hoge.js
var axios = axios.create({
  adapter: function(config) {
    return new Promise(function dispatchXhrRequest(resolve, reject) {
      var requestData = config.data;
      var requestHeaders = config.headers;

      if (utils.isFormData(requestData)) {
        delete requestHeaders['Content-Type']; // Let the browser set it
      }

      var request = new ActiveXObject('MSXML2.ServerXMLHTTP');

      // HTTP basic authentication
      if (config.auth) {
        var username = config.auth.username || '';
        var password = config.auth.password || '';
        requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
      }

      try {
        request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true);

        // Listen for ready state
        request.onreadystatechange = function handleLoad() {
          if (!request || (request.readyState !== 4)) {
            return;
          }

          // The request errored out and we didn't get a response, this will be
          // handled by onerror instead
          if (request.status === 0) {
            return;
          }

          // Prepare the response
          var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
          var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.responseBody;
          var response = {
            data: responseData,
            // IE sends 1223 instead of 204 (https://github.com/mzabriskie/axios/issues/201)
            status: request.status === 1223 ? 204 : request.status,
            statusText: request.status === 1223 ? 'No Content' : request.statusText,
            headers: responseHeaders,
            config: config,
            request: request
          };

          settle(resolve, reject, response);

          // Clean up request
          request = null;
        };

        // Add xsrf header
        // This is only done if running in a standard browser environment.
        // Specifically not if we're in a web worker, or react-native.
        if (utils.isStandardBrowserEnv()) {
          var cookies = require('axios/lib/helpers/cookies');

          // Add xsrf header
          var xsrfValue = config.withCredentials || isURLSameOrigin(config.url) ?
          cookies.read(config.xsrfCookieName) :
          undefined;

          if (xsrfValue) {
            requestHeaders[config.xsrfHeaderName] = xsrfValue;
          }
        }

        // Add headers to the request
        if ('setRequestHeader' in request) {
          utils.forEach(requestHeaders, function setRequestHeader(val, key) {
            if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
              // Remove Content-Type if data is undefined
              delete requestHeaders[key];
            } else {
              // Otherwise add header to the request
              request.setRequestHeader(key, val);
            }
          });
        }

        if (requestData === undefined) {
          requestData = null;
        }

        // Send the request
        request.send(requestData);

        // Set the request timeout in MS
        request.waitForResponse(config.timeout);
        if (request.readyState !== 4) {
          request.abort();
          reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED'));
          request = null;
        }
      } catch (err) {
        reject(createError(err.description, config, (err.number >>> 0).toString(16)));
        // Clean up request
        request = null;
      }
    });
  },
});

```

主に変更した点は以下になります。

* `new XMLHttpRequest();`を`new ActiveXObject('MSXML2.ServerXMLHTTP');`に変更
* timeoutの設定をsetTimeoutsメソッドを使った方法に変更
* XDomainRequestに関する処理を省いた
* xhrのonerrorとontimeoutにメソッドを設定しないようにした
* withCredentials、responseType、progressに関する設定をしないようにした
* xhrのopenからsendまでをtry-catchでくくるようにした
* catchした時にエラーメッセージとエラーコードを設定するようにした

JScriptで利用できるXHRは設定可能なプロパティが少ないため、その部分を削っています。
全体をtry-catchでくくっているのは、
本来、通信が失敗した場合はonerrorやontimeoutが呼ばれますが、JScriptではこれらを設定することができません。
JScriptではonerrorやontimeoutを呼ぶ代わりに例外を投げるので、try-catchでくくってあげています。

## axiosを使った通信

先ほど生成したaxiosのインスタンスで通信を行う処理を書いてみます。

```js:hoge.js
// 上のコードの続き
axios.defaults.baseURL = 'http://jsonplaceholder.typicode.com';

axios.get('/posts/1').then((res) => {
  WScript.Echo(res.data.title);
  WScript.quit();
})['catch']((err) => {
  if (err.code) {
    WScript.Echo(err.code);
  }
  WScript.Echo(err.message);
  WScript.quit();
});

for (;;) {
  WScript.Sleep(100);
}
```

```
C:\wsh>cscript hoge.js
Microsoft (R) Windows Script Host Version 5.7
Copyright (C) Microsoft Corporation 1996-2001. All rights reserved.

sunt aut facere repellat provident occaecati excepturi optio reprehenderit
```

注意点としては、リクエストを送ったあとには何か他の処理をしていないと、レスポンスが返ってくる前に終了してしまいます。
そのため、ここでは無限にsleepをして、リクエストが返ってきたらquitしてあげています。
また、Promiseのcatchは上のように書いてあげる必要があります。
[IE9未満ではcatchが構文エラーになるようです。](https://github.com/lahmatiy/es6-promise-polyfill#usage-in-ie9)
[transform-es3-member-expression-literals](http://babeljs.io/docs/plugins/transform-es3-member-expression-literals/)を適用することで、今までながらの書き方をすることもできます。

## おわりに

JScriptも頑張れば最近のライブラリを使いまくれるのでいいですね。
需要はアレですが、何かの参考になるとうれしいです。

## 参考
* [IServerXMLHTTPRequest/ServerXMLHTTP メンバー](https://msdn.microsoft.com/ja-jp/library/ms754586(v=vs.85).aspx)