トップ 追記

2018-06-15 [長年日記]

_ async/awaitやPromiseで気をつけること

ES2017のasync/awaitのキソ練習 - あーありがち(2018-05-26) のあとにがっつりハマっていたのでそこで得た知見をメモ。

asyncの効果は各functionブロックで切れるので逐一指定が必要

例えば

async foo() {
  return new Promise((resolve, reject) => {
    ..
  })
}

ってコードがあるじゃろ? この Promise の中では await できんのじゃ。正解はこうじゃ。

async foo() {
  return new Promise(async (resolve, reject) => {
    ..
  })
}

Promiseではちゃんとrejectする、rejectしてすぐreturnする

async foo() {
  return new Promise(async (resolve, reject) => {
    const bar = await this.bar()

    if ( !bar ) {
      return reject(new Error('message'))
    }

    resolve(bar)
  })
}

async function は Promise を返すので、通常の function のように値を返すことはできない。成功も返せないし失敗も返せない。

失敗は必ず reject() で教える必要がある。

そしてもう一つ、reject() は単なる function call であり、それが終わったら次に処理が進んでしまう。ちゃんと return して foo() を抜けなければいけない。

resolve ばかりを考えていると UnhandledPromiseRejectionWarning の警告が出まくることになる。ちゃんと reject してあげよう、そしてすぐ抜けよう。

async/awaitで待つコードを閉じ込める

async foo() {
  return new Promise(async (resolve, reject) => {
    let c = {}

    this.bar().forEach((e) => {
      c[e] = await this.baz(e)
    })
  })
}

みたいな、同期処理の中に非同期処理が紛れ込んでいてさらに全体のブロックの中で副作用ベースで変数を組み立てる、みたいなやつは大変あぶない。

async foo() {
  return new Promise(async (resolve, reject) => {
    // 非同期処理のカタマリ

    // 同期処理のカタマリ
  })
}

に分離しよう。

Promise.allで全部待て

上のコードはもっと言うとループの1回目は待つかもしれないけど全部は待ってくれないコードになり得る。そこで実際にやるとしたら Promise.all() を使って以下のような感じになる。

async foo() {
  return new Promise(async (resolve, reject) => {
    // 非同期処理のカタマリ
    const result = await Promise.all(this.bar().map(async (e) => {
      return ..
    })

    // 同期処理のカタマリ
    result.forEach((e) => {
      ..
    })

    ..
  })
}

ここに上で述べた resolve, reject を加えると以下のような感じがいったん完成形と言えそう。

async foo() {
  return new Promise(async (resolve, reject) => {
    // 非同期処理のカタマリ
    const result = await Promise.all(this.bar().map(async (e) => {
      return .. await this.baz() ..
    })

    // 同期処理のカタマリ
    let c = {}

    result.forEach((e) => {
      const [key, value] = e

      .. // 同期処理だけなら副作用ベースでも従来通り大丈夫

      if ( ) {
        return reject(new Error())
      }
    })

    resolve(c)
  })
}
Tags: JavaScript

2018-06-11 [長年日記]

_ Vue.jsチョット書いたのでふりかえり

ちまちま各論のメモは起こしてあるけど、ざっくり全体のメモがなかったので、自分が現時点で意識していることをまとめてみた。

Vue.jsのメリット

Single File Componentが標準で扱える

Single File Component がすべての問題を解決するとは思っていないのだけれど、まず自分は

  • jQuery 的な DOM 要素とそのイベント中心の書き方
  • Unobstrusive JavaScript

の組み合わせは「操作」が「要素」と密結合しているにも関わらず、コードとしては非常に距離が遠く凝集度が低すぎると考えている。HTML と JavaScript の紐付けは class や ID で行うが、ページをまたいで影響させるのは難易度が高すぎる。またコードがイベント中心かつ操作中心になりすぎてしまい、抽象度が低すぎる。本当に考えるべき状態やロジックまで意識を到達させるのはとても難しい。要するにものすごくちぐはぐなのだ。

この問題を Single File Component は比較的うまく解決していると思う。機能として意味のある HTML 断片をコンポーネントとして切り出し、その隣に実際の機能を記述することができる。また Scoped CSS を書けるのもよい。

これが Vue.js では追加設定なしに書ける。

ある程度オールインワンなので準備が楽

AngularJS (v1) のように世界が閉じすぎておらず、React や Mithril のように単機能すぎない。一応 Babel などのビルドプロセスがなくても動かすことができる。Angular (2+) のように TypeScript を新たに勉強する必要もない。

この準備が複雑すぎないことはとても重要と考えている。少なくとも現在の環境では

  • デザイナーなど必ずしも JavaScript に明るいわけではない人にも扱える
  • Node.js ヘビーな JavaScript の環境構築に詳しくない人でも扱える

ことを重視している。

環境構築は目的ではなく手段。手段の習得がヘビーすぎるのは完全に本末転倒と言ってよい。準備が楽であればあるほど新しく参加するメンバーの初期コストが下がる。それは特に小さく固定的でないチームにとって重要である。

必要な用語が少ない

正確な理解とより良いコードを書くためには、当然のことながら各機能の意味や役割を表現するために用語を使いこなす必要はあるのだが、さしあたって動くコードを書くためにあまり多くの用語を必要としないのはよいことである。

(※ ただし、準備が楽で必要な用語が少ないからと言って必ずしも学習コストが小さいとは言い切れないと思っているので、このような表現になっている。)

ある程度流行っている

個人的には、JavaScript を利用したシンプルな ViewModel フレームワークとしては Mithril が好きなのだけれど、いかんせん CSS フレームワーク、UI フレームワークと組み合わせるとか、単純にノウハウなどの流通において React, Vue.js, Angular に大きく水を開けられてしまっているのは事実である。

こうなると他のツールと同じことを再現しようとした時にはややコストの大きな環境になってしまいやすい。Mithril 自体の準備はそんなに大変じゃないけど、デザイナーが手を加えていくプロセスにおいてゼロベースのコーディングの量が増えてしまいやすかったり、何か意図通りに動かず困った事態に陥っても、とにかく自分で試行錯誤する以外の解決方法が見つからない可能性もある。

これがライブラリのアップデートに煩わされず、細かいデザイン要件のない業務向けなら CSS の優先度を下げて妥協するといった判断もできるが、Consumer 向けの Web を扱おうと思うとさすがにそういうわけにはいかないだろう。

Vue.js なら十分に流行っているのでそのような心配はほとんどないと言ってよい。安心してググれるし、情報交換が可能である。

PWAやネイティブアプリへの道もある

これらはさすがに標準対応じゃないけど、道があるという事実だけメモしておく。

現在のVueとの付き合い方

  • ノーフレームワーク(一部Babelだけアリ)
  • Rails + Webpacker
  • Nuxt(SSR 付き)

をやってみた*1 が、この中でバランス的には Rails + Webpacker で小さなアプリから始めるのがよさそうだと感じている。要するにサーバ側との統合作業も Webpack などの JavaScript 周りのごちゃごちゃとした設定作業も必要にならないもの、である。

逆にサーバ側と絶対に密結合にならないようにするために Nuxt で全部縛っちゃうというのもギプスとしてはアリかなという気がしている。少なくともこれが使いこなせればサーバ側の選択肢は広がる*2。ただし Nuxt は SSR がすごい悩む。最初は SPA モードのみにするのもアリだとは思う。*3

とりあえずサラから Webpack を書くのはやらないと決めている。このこだわりは正解だったと思っている。*4

Vueの扱いで気をつけるべきこと

Vue Componentに過度の期待をしない

Vue Component で扱えること、Vue Component に書けることは限られている。基本的には

  • 変更を監視するデータ
  • (ユーザの入力に対する)イベントハンドラ
  • VM Life Cycle Hook

くらいしかない。あとは変更を監視するのがデータそのものではなくメモ化機能付きの function か、extends 元があるか、くらいだ。

データバインディングはとても便利だ。変更の操作を記述する必要がない。しかしそれ以上は面倒を見てくれない。結局のところ人間の「考える」余地は大きく、設計がダメならコードが必要以上の複雑さを抱えてしまうのは普通のプログラミングと同じである。現実世界の課題は Vue Component というか React が集中しようとした ViewModel なコンポーネントで扱う範囲より広く、より複雑な形で存在している。

Vue Componentの外のコードを活用する

これはアセットバンドラがないとダメなので準備が楽という話と矛盾してしまうが、Vue Component の外にコードが置けることは重要である。

上にも挙げたが、Vue Component の中で function の書ける場所は基本的には computed か methods しかない。ごく単純に言い換えるとデータバインディング用かイベントハンドラ用である。

また Vue Component 同士で function を融通する方法は extends か mixin、要するに mixin である。雑に言うとフラットに function をぶちまけるだけで名前空間的にはぶつかりまくるし、Component から独立した function を融通する方法はないということである。

これはもう function をいい具合に管理することは Component のスコープから外したと考えるしかない。JavaScript が持っている機能は JavaScript の方で解決しましょう、普通に export / import しましょうということだ。Vue Component 以外を export / import すれば普通に class も利用できるし、Component から独立した共通の function を名前の重複を気にせず import することもできる。

VirtualDOMで扱えない世界や一般的なモデリングなどの手法に精通する

結局のところ現実世界の課題を分析し、適切なコードを書くには

  • HTML 5 時代の新しい要素である video や audio, canvas
  • Vue Component の外の Pure JS な世界
  • OOP や DDD などのパラダイム

に精通する必要があると言ってよい。

ViewModel コンポーネントが解決するのはあくまでごく一部なのだ。

Lifecycle Hookは用法、用量を守って

Lifecycle Hook は便利なのでついついいろんなことをさせてしまうが、Lifecycle Hook の中のコードが長くなると

  • function に分割されていないのでコードの意図を読み取りにくい
  • タイミングを制御しきれない場合がある
  • テストコードを書く際の stub out の単位がでかすぎる

といった問題が出てくる。これは歴史の中に学ことができる。昔なつかしい「コンストラクタ頑張りすぎ問題」だ。

Lifecycle Hook の中のコードを整理するためにも Vue Component の外にコードを分割して置くのが恐らくかなり重要なテクニックになると言ってよいと思う。

*1 素振りとしては、素から、vue-cli から、Parcel からも試したが、実プロダクトとしては上の 3 つになっている。

*2 単に API の役割を果たしてくれればコードベースを共有する必要もまったくない。

*3 自分はなんか逃げたくなかったのでやらなかったけど

*4 結局テストコードのために書くことになるんだけど


2018-06-08 [長年日記]

_ Vuex PersistedStateの値の復帰のタイミングが謎だったのでNuxtのplugin書いて解決した

背景

先日から Vue (Nuxt) + Vuex + Vuex PersistedSate でモノを動かしてるんだけど、

なんかふいにデータが矛盾して動作がおかしくなる

ことが起きていた。

まぁアタリはついていて、Nuxtでrouteに応じてVuex Storeをmodule分割する方法 - あーありがち(2018-04-19) にある動的に module 構造の Vuex Store を生成している処理と、 Vuex PersistedState を通じて localStorage からデータが返ってくる処理がそれぞれバラバラに動いていることで、「Vuex Store の値を見て判定する処理」がタイミング次第の微妙なシロモノになっているんだろうなぁと思ってたんだけど、決定的な解決策が見つからないでいた。

通常の使い方だと問題にならないけどテストしてるとよく踏み抜くので、ごまかしごまかしやっていたのだが、今回解決できたので記録しておく。

※ ちなみに今回の話は Vuex PersistedState が localStorage の値でメモリ上のデータを書き換え終わったら Vuex がそれを通知してくれて Vue コンポーネントの computed に反映できれば、実はあまり悩まなくて済んだのだけど、対策としてやったこと自体は Vuex PersistedState を使っていなくても参考になるし、今後自分で参考にしたいので書いてます。

使っているもの

おさらい - Nuxtを利用したVueアプリにきっとありがちなこと -

  1. Vuex PersistedState 経由で localStorage から値が復帰するのは mounted() 以降
  2. this.$route や this.$store に依存するコードが安定して動くのも mounted() 以降
  3. Nuxt では基本的にアプリの全体像を気にしなくても済むように Page component を突っ込むと routing も自動解決する、逆に言うとアプリ全体を組み立てるプロセスに介入するのは難しい

これらが合わさると何が起きるかというと、

Page component の mounted() が複雑で長大な一つのメソッドになりやすく、しかもタイミング問題まで抱え込んでしまう。

これ割と地獄感あるのですよ。

解決方法

Vuex Storeの初期化はpluginで行う

plugin function からは

  • 初期化済みの store オブジェクト
  • page component の有無から自動で解決される routes オブジェクト

の両方にアクセスすることができる。したがって、Nuxtでrouteに応じてVuex Storeをmodule分割する方法 - あーありがち(2018-04-19) でやったように

this.$store.registerModule(NAME, OBJECT)

ではなく、plugin の中で

export default ({app, store} => {
  store.registerModule(NAME, OBJECT)
})

みたいにすると、Vue の root component が初期化される前にすべてを揃えることができる。

mounted()の中でsetTimeout

Vuex Store に localStorage から値が返ってくるのは mounted() 以降なのだが、値が返ってきてからでないとメモリ上の値で reset することはできない。

ということで以下のような感じになる。

mounted() {
  setTimeout(() => {
    resetの処理({値1, 値2, ..})
  }, 0)
}

あれ、結局 mounted() の中が複雑なのは一緒じゃね?と思うかもしれないが、mounted() の中で registerModule すると、localStorage から値が返ってくるタイミングも遅くなり、かなり制御が難しくなるので、

mounted より前に registerModule しておいてからの setTimeout

という扱いにしておくのがよい。また、setTimeout() の中も直接長いコードを書かずに、遅延させてるのが分かる reset 処理だけ独立させた PureJS な function として切り出して、そいつに外からバンバン値を与えてやるのがよさそう。なんなら this.$route とか this.$store とかバンバン与える。

※ なんで reset したいのかはアプリによると思うが、保存しておいた何かと比較して違ってたら何か処理を行いたい、という要求は普通によくあると思う。

まとめ

Nuxt + Vue アプリを書く際には、

  • Vue component ではない形で解決できるものはないか考え、どんどん分離する
  • plugin は root component 初期化前に async/await で動作するのでよいぞ

を覚えておくのがよさそう。

特に、意外と View に関わらないコードは多くなるので、積極的に Vue component 以外の形にして、Nuxt のディレクトリレイアウトで言う components/ つまりレールから外れた場所に置くコードを増やしてやるのが吉っぽい。

です!

*1

参考

*1 今回、対処方法を見つけるまでの試行錯誤はそれなりにありましたが、見つかってからサクッと Vuex Store の組み立て、リセット処理の位置(実行タイミング)を分離、変更できたのは、もちろんできあがった Store の動作と Vue component からのアクセスについてもテストコードを書いてあったからです。


2018-06-05 [長年日記]

_ フロントエンドのrequireはテストを考えると注意が必要

先日導入した Nuxt の環境で mocha-webpack でテストコードを書いている。

なんだけど、require / import 周りでどうしても伝統的な環境のクセが出てハマることが何回か起きていたのでそのメモ。

非webpack環境下でファイルを扱うサンプル

例えばファイルを扱うコードを書く際に、従来(webpack じゃない環境)はよく以下のようなコードを書いていた。

class Klass {
  /**
   * @return {String}
   */
  path() {
  }

  /**
   * @return {String}
   */
  load() {
    return <require とか read とか>(this.path())
  }
}

ファイルの扱いを気にしたくないので、ファイルの情報とその内容、データ構造などをセットで class の責務として持たせ、Klass を使う側は欲しい情報だけを適度に扱うメソッド経由で触る。

で、テスト時に path() を stub out してテスト用のファイルを食わせる。HTTP request だと request 先の URI を環境に応じて切り替えるような感じ。stub out じゃなくて外からパス情報を与えろとかあるかもしれないけど、まぁいずれにせよ「パス情報だけを独立して扱える」という前提なわけだ。

ところがこのようなコードは webpack ではいくつかの意味でダメなコードなのだ。

  1. そもそも webpack で動く require には変数も与えられない
  2. webpack の require は parse 時に即実行される

そもそもwebpackで動くrequireには変数は与えられない

これはメソッドとして分離しているかどうかではなく、以下のように動的にパスを組み立てるコードでも一緒。

const path = dir + base + param + '.js'
require(path)

これは「require が現れたらその中のパスを解釈していい具合に読み込んで JS の中に JS やデータを置く」という webpack の挙動に対して、「パスの解釈を許さない」というコードになってしまうのだ。

require の中に動的な組み立てが入っている場合は解釈できるので、どうしてもやりたい場合はそのように書くとよいが、その場合もテストには向いていない。

webpackのrequireはparse時に即実行される

プロダクトコードに require を書き、テストコードで stub out した require を書いた場合、実は require そのものが2回起きる。

まぁそらそうかという感じではあるんだけど、要は webpack を通す段階で require の登場する部分は遅延評価できない。人間の意識としてはコードが実行される前、 webpack がコードの変換を行う段階で require は実行、解決されてしまう。

まとめ

だから stub out するかどうかではなく、テスト対象のコードの require は都合よく変更できないので、以下のようにしてあげるのが正解。

  1. パス情報の解釈、require の処理は外で行い、
  2. 外からデータを与え、
  3. データの解釈だけを責務としてあげる

require ではなく HTTP になっていれば問題ないが、今回はそこまでやる必要はないので webpack の require で食わせるデータをいい具合にビルドプロセス*1で生成してやろうと思っていたら、require 周りのテストで思ったように動かずに悩んでしまった、という話でした。

Tags: JavaScript

*1 こっちは webpack ではなく Node.js