読者です 読者をやめる 読者になる 読者になる

本で勉強するか Web で勉強するか

最近技術書を買うかどうかの判断が確立してきた。 のでブログにまとめてみる。

自分の場合、以下のような大きい問題領域を扱う技術を本気で勉強したい、となったら本を買うと決めている。

けどライブラリツールの場合、なるべく本は買わない。 それは今までハズレが多かったから。 この「ハズレ」というのは以下の様なケースを指している。

  • 自分が知識を吸収できなかった
  • それほど得るものがなかった
  • 買ったものの情報の鮮度が古くて使えないと判断した

なぜツールとライブラリはこのようなケースが多いのか。 それはまず前提として、「ツールやライブラリは何らかの特定領域の問題を解決するためのものである」という事。

これにより界隈の状況や HW リソースの向上により問題自体が無くなったり、性質が変わったりしてしまう(Web 界隈は特にその傾向が激しい)。

また、情報の鮮度も比較的落ちやすい。 よって公式ドキュメントにあたった方が確かな情報を得られる。

また公式ドキュメントを見る他の理由として、ツールやライブラリの場合、情報が公式のコミュニティに集積しやすいというのがあると思っている。

これに関してはプログラミング言語や OS となると問題領域が大きすぎて(自分にとって)実用的な情報が公式コミュニティからは得られにくい。*1

よって、その根本的な所の思想を含めた技術を理解しないと「私○○できます」とは言えない。*2

なのでそういった類の技術だと判断したら技術書を買う事にしている。


ぶっちゃけリファレンスなら Web の方がずっと役に立つし、公式サイトを見れば分かるような本は買いたくない。

でも小ネタばかりを集めると 〜 Hacks とかみたいに結構根本につながる知識に出会えたりもする。 でもそれは Web でもできることなので、今はそういった本を積極的に買う気にはなれない…(主に経済的理由で) 個人的には Advent Calendar でいいやと思い始めている。*3

Linuxサーバ Hacks 2 ―コネクティング、モニタリング、トラブルシューティング

Linuxサーバ Hacks 2 ―コネクティング、モニタリング、トラブルシューティング

Binary Hacks ―ハッカー秘伝のテクニック100選

Binary Hacks ―ハッカー秘伝のテクニック100選

*1:余談になるが、PHP は特定領域に特化してるからこそ良質で実用的な情報が集まったのではないかと思う

*2:まぁぶっちゃけどの技術に対してもそんな恐れ多い事は言えないんだけども…

*3:最近は Advent Calendar も tips 系だけじゃなくなって来てるけど… というかそもそも数多すぎ

eslint-config-airbnb を React 以外のプロジェクトにも使いたい

Can I use eslint-config-airbnb without eslint-plugin-react? · Issue #451 · airbnb/javascript · GitHub

React 以外のプロジェクトに eslint-config-airbnb を使うには、eslint-config-airbnb-base を使う。 上記 issue では extendsairbnb/base を指定するとあるが、今は airbnb-base を指定するようになっていた。

.eslintrc

{
    "extends": "airbnb-base"
}

小ネタ

ちなみに .eslintrc ではなく package.json に ESLint の設定を書く事もできる。 これは .babelrc も同様で、package.json に Babel の設定を書く事ができる。

{
  // ...

  "babel": {
    "presets": ["es2016", "stage-3"]
  },
  "eslintConfig": {
    "extends": "airbnb-base",
    "globals": {
      "angular": true
    }
  }
}

new Date('hoge').toString() が 'Invalid Date' になる

現場からは以上です。

Angular の $http, $resource が返す promise で受け取れる値

Angular

Angular 使ってて今更だけど $http$resource が返す promise で受け取れる引数が違って混乱したのでまとめてみた。

$http

まず $http の場合は resolve(), reject() された値を受け取る方法として2通りある。

  1. $http(...).then(successCallback, errorCallback) で受け取る
  2. $http(...).success(successCallback)$http(...).error(errorCallback) で受け取る

1の場合の callback の引数は以下の通り (ソース:公式ドキュメント)。

  1. response – Object
    • data – {string|Object} – The response body transformed with the transform functions.
    • status – {number} – HTTP status code of the response.
    • headers – {function([headerName])} – Header getter function.
    • config – {Object} – The configuration object that was used to generate the request.
    • statusText – {string} – HTTP status text of the response.
$http.get().then(function onSuccess(res) {
  // res.data, res.status, res.headers, res.config, res.statusText
  // ...
}, function onError(res) {
  // res.data, res.status, res.headers, res.config, res.statusText
  // ...
});

// error の場合はこう受け取っても構わない。
// こっちのが本来(?)の Promise と同じインターフェースなのと例外処理な感じがして好き。
$http.get().then(function onSuccess(res) {
  // ...
}.catch(function onError(res) {
  // ...
});

2の場合の callback の引数は以下の通り。 1の場合のオブジェクトがフラットに渡される感じ (statusText 以外)。

  1. data – {string|Object} – The response body transformed with the transform functions.
  2. status – {number} – HTTP status code of the response.
  3. headers – {function([headerName])} – Header getter function.
  4. config – {Object} – The configuration object that was used to generate the request.
$http.get().success(function onSuccess(data, status, headers, config) {
  // ...
}).error(function onError(data, status, headers, config) {
  // ...
});

ただ説明した後でアレだけど、2の success()error() は deprecated とされているので使わない方がいい。 既存コードで使われているのを見たら渡される引数の違いを意識した方がいいということ。

ちなみに以下のようにして success()error() を使ったらエラーにできる (useLegacyPromiseExtensions はデフォルト true)。

angular.module('sample')
  .config(['$httpProvider', function($httpProvider) {
    $httpProvider.useLegacyPromiseExtensions(false);
  }]);

$resource

$resource は内部で $http を使っているが、インターフェースが若干違う。 まず Resource.post(parameters, successCallback, errorCallback)Resource.get(successCallback, errorCallback) などのようにメソッドに直接コールバックを指定する場合。 以下公式ドキュメントからの引用。 (get() などの場合は parameters を省略できます。正確な引数の順番や受け取り方はソース見たほうが早い)

  1. HTTP GET "class" actions: Resource.action([parameters], [success], [error])
  2. non-GET "class" actions: Resource.action([parameters], postData, [success], [error])
  3. non-GET instance actions: instance.$action([parameters], [success], [error])

ここで "class" や "instance" と言っているのは以下の違い。 まずリクエストを送る時は以下のようにリソースを定義する必要がある。

// Define CreditCard class
var CreditCard = $resource('/user/:userId/card/:cardId',
 {userId:123, cardId:'@id'}, {
  charge: {method:'POST', params:{charge:true}}
 });

class

CreditCard.query(parameters, function onSuccess(data, headers) {
  // ...
}, function onError(res) {
  // ...
});

instance

var instance = new CreditCard(parameters)
instance.query(function(data, headers) {
  // ...
}, function onError(res) {
  // ...
});

また、"class" と "instance" 経由で取得する以外に $promise 経由で取得する方法がある。

CreditCard.query(parameters).$promise.then(function onSuccess(data, headers) {
  // ...
}).catch(function onError(res) {
  // ...
});

ちなみに (追記)

Angular で async/await を使う際は自動で $scope.$apply() が呼ばれないのでこういうの使うといいみたいです。

以上

ここまで書いといてなんだけど、公式ドキュメント見た方がいい。

Angular UI Router の resolve プロパティが失敗(reject)した場合に値を受け取りたい

Angular

angularjs - Reloading current state - refresh data - Stack Overflow

Update for newer versions:

$state.reload();

Which is an alias for:

$state.transitionTo($state.current, $stateParams, { 
  reload: true, inherit: false, notify: true
});

まず上記エントリで $state.reload()$state.transitionTo() を呼び出すらしいことが分かった。 $state.go() も同様に $state.transitionTo() を呼び出す。

Home · angular-ui/ui-router Wiki · GitHub

$rootScope.$on('$stateChangeStart', 
function(event, toState, toParams, fromState, fromParams, options){ 
    event.preventDefault(); 
    // transitionTo() promise will be rejected with 
    // a 'transition prevented' error
})

そしてどうやら $state.transitionTo() は promise を返すらしい。 つまり resolve プロパティで reject したら $state.reload()$state.go() が返す promise で受け取れる。

  angular.module('sample')
    .config(['$stateProvider', function($stateProvider) {
      $stateProvider
        .state('top', {
          url: '/',
          templateUrl: '/page/top.html',
          controller: 'topCtrl as $ctrl',
          resolve: {
            fetchResult: ['Book', '$q', function (Book, $q) {
              var deferred = $q.defer()
              Book.get().$promise.then(function onSuccess(res) {
                deferred.resolve({
                  books: res.books,
                  alerts: []
                })
              }, function onError(res) {
                if (angular.isObject(res) &&
                    angular.isString(res.statusText) &&
                    angular.isObject(res.data) &&
                    angular.isObject(res.data.error) &&
                    angular.isString(res.data.error.msg)) {
                  deferred.resolve({
                    books: [],
                    alerts: [{
                      type: 'danger',
                      msg: res.statusText + ' (' + res.data.error.msg.trim() + ')'
                    }]
                  })
                } else {
                  // Offline?
                  deferred.reject({
                    books: [],
                    alerts: [{
                      type: 'danger',
                      msg: 'Couldn\'t connect to server.'
                    }]
                  })
                }
              })
              return deferred.promise
            }]
          }
        })
    }])

    .controller('topCtrl', ['fetchResult', '$state', function topCtrl(fetchResult, $state) {
      var $ctrl = this
      $ctrl.books = fetchResult.books
      $ctrl.alerts = fetchResult.alerts

      $ctrl.closeAlert = function closeAlert(index) {
        $ctrl.alerts.splice(index, 1)
      }

      $ctrl.refresh = function refresh() {
        $state.reload().catch(function onError(fetchResult) {
          $ctrl.alerts = $ctrl.alerts.concat(fetchResult.alerts)
        })
      }
    }])

ほぼ実際のコードからの引用なので長くなってしまった (日記ブログからの転載まんまなので雑)。 画面上の更新ボタンを押した時($ctrl.refresh())、fetchResult を取得するために投げている GET リクエストが失敗し、オフラインっぽかったらその旨を画面上に表示する。

UI Bootstrap のウインドウを Draggable にする

Angular UI Bootstrap

これでできた。

angular.js を読み込む前に jQuery, jQuery UI の .js を読み込む必要があります。

  angular.module('app')
    .directive('uibModalWindow', function () {
      return {
        restrict: 'A',
        link: function (scope, element) {
          $(element).draggable()
        }
      }
    })

情報源は以下の Stack Overflow のエントリ。 ただ注意点として、UI Bootstrap の全ディレクティブに uib- が付くようになったので、コメントの modalWindowuibModalWindow と読み替える必要がある。

angularjs - AngularUI modal to be draggable and resizable - Stack Overflow

こういう記事もあったけど、directive 以外で element をいじるのは Angular 的にダメなんじゃないだろうか。

angular.js/ui-bootstrapでui-bootstrapのモーダルウィンドウをドラッガブルにするには - わからん

あとこういう記事もあった。

Twitter Bootstrap のモーダルをドラッグで動かしたいときは - AngularJS Ninja Blog

このままだとセレクタ.modal.fadeと.modal.fade.inに定義されているtopとtransitionのプロパティ(モーダルが上から滑り降りてくるトランジション)による影響で、縦方向のドラッグがの〜んびりした動きになって違和感が残る。 なので、上から滑り降りてくるトランジションはあきらめて、class 属性からfadeを消してしまうのがいいと思う。

この現象は確認できなかった。 自分の確認不足の可能性もあるので困った時のためにメモ。

ところで:$uibModal の resolve プロパティ

$uibModal のドキュメント見てたらふと resolve ってプロパティを見つけた。 これにキーと任意のオブジェクトを渡せば、そのキー名でモーダルウィンドウ側のコントローラーで inject できるようになるらしい。 こんな感じ (コードはここからの引用)。

$uibModal.open({
    controller : 'DialogController',
    resolve: {
        params: function() {
            return {
              title:'DialogSample',
              message:'Take it easy!'
            };
        }
    }});

angular.module('DialogSample').controller('DialogController', ['$uibModalInstance', 'params',
      function($uibModalInstance, params){
        console.log(params.title); // => 'DialogSample'
        console.log(params.message); // => 'Take it easy!'
      }]);

わざわざ受け渡しをするだけのサービスを作る必要はなかった…すごく無駄な再発明をしててすごくショックを受けたので羞恥周知してみる。

Chrome DevTools で使える通信制限(オフライン環境をエミュレートする方法)

先日、単体テストを行っていて JS からのリクエストを失敗させるテストケースがあった。 バックエンドのサーバを停止させればいいが、毎回起動したり停止したりするのは面倒。 ということで効率の良いテスト実施方法がないか調べていた。*1

通信制

結論から言うと、タイトルの通り Chrome の DevTools の機能で擬似的にオフラインの環境や、回線が細い環境をエミュレートできる。

以下スクリーンショット

オフライン環境をエミュレート

まずは Ajax で GET リクエストを飛ばす。 コンソールで jQuery を読み込ませて GET / してみる。

// jQuery をロード
var $script = document.createElement('script');
$script.src = "https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js";
document.getElementsByTagName('head')[0].appendChild($script);
// GET リクエスト送信
jQuery.get('/', res => { window.alert(res) })

f:id:tyru:20160808004231p:plain

そして制限をかける方法と、かけた結果がこちら。

f:id:tyru:20160808004235g:plain f:id:tyru:20160808004239p:plain

回線の細い環境をエミュレート

2枚目の GIF にもチラッと映ってる通り、DevTools の Network タブをクリックし、 「No throttling」のプルダウンをクリックすると「Presets」以下に回線が細い環境などをエミュレートするためのプリセットが色々表示されている。

Chrome DevTools は偉大

JS を実行制限させたかったら大体 DevTools でなんとかできる(適当)と思ってるので、もしそういうケースがあったら一度 DevTools 開いて色々探してみると良い。

あと GyazoGyazo GIF は便利。

*1:そもそも自動化しろという話だけど…