Vue テンプレート内の式の型チェックと解析ができるまで

Vue の TypeScript 対応は v2.0 から公式に型定義がサポートされるようになったりv2.5 で Vue.extend を使ったときに this の型が推論されるようになったりと、改善が何度も行われています。

しかし、課題はまだたくさんあり、その中でもよく聞くのが、テンプレート内の式の型チェックがされないという課題でした。TypeScript はテンプレートを解釈できないので当たり前ですが、もしそれが解決できたらより安全になるでしょうし、開発体験も向上すると思います。

そしてこの課題は Vetur の最新版で解決されました。

Vetur v0.19.0

VeturVisual Studio Code の拡張機能の一つで、Vue の単一ファイルコンポーネント (.vue ファイル) のための様々な機能を提供しています。例えば、<script> ブロック内の TypeScript コードの型チェック、補完などの機能は Vetur によって提供されています。

先日 Vetur v0.19.0 がリリースされました。大きな更新としては Template Interpolation Service があります。これにより、テンプレート内に書かれている式の型チェック、ホバーによる変数情報の表示、補完、定義ジャンプ、参照の表示ができるようになりました。以下の Pine (Vetur の作者) のツイートの動画を見るとわかりやすいと思います。

この機能はまだ実験的なものなので、設定から vetur.experimental.templateInterpolationService を有効にすることで使えるようになります。

実装には僕も関わっていて、いろいろと試行錯誤をしてやっと完成したという感じです。完成するまでの道のりを書いていこうと思います。

Pine もこの機能についての記事を書いているので、合わせて読むといいと思います。

Generic Vue Template Interpolation Language Features | Pine Wu’s Blog

型チェックの実装を模索

テンプレートの型チェックはずっと前からやりたいと思っていたことで、2017 年から模索を始めていました。いくつかの方法を試してはうまくいかずに失敗するというのを繰り返し、かなり時間がかかってしまいました。

コンパイルした JS コードの利用

最初は最も簡単にできそうだということで、vue-template-compiler によって JavaScript に変換したテンプレートをコンポーネントに差し込み、あとは普通に TypeScript のコンパイルをするというアプローチを試してみました。これに関しては Vue.js Tokyo v-meetup=”#3″ で話しているので、詳細は以下のスライドを見てください。

実装は GitHub にあります。

https://github.com/ktsn/typed-vue-template

この方法は、型チェックは正しく動くのですが、元のソースコードから変換を行っている関係で、エラーが発生した場所が正しく出力できないという問題がありました。vue-template-compiler は source map をサポートしていないので、元のテンプレートの位置を復元することができず、また、vue-template-compiler はブラウザ上で動くことも想定していることから、実装が最小限に抑えられており、source map の機能を入れることはあまり現実的ではなさそうでした。

Vetur に Issue を上げる

そんなときに Pine もテンプレートの型チェックをやろうとしているという話を Evan (Vue.js の作者) から聞き、Vetur のリポジトリに Issue を上げました。

https://github.com/vuejs/vetur/issues/209

僕はあまり TypeScript の内部実装や TypeScript Compiler API には詳しくなかったので、Issue 内で紹介されているドキュメントや、Vetur のコードを読んで勉強しつつ、アプローチを考えていました。

自前の型検査器

当時すでに Angular はテンプレートの型チェックを実装していて、その実装も読んでいました。読み進めてみると、どうやら Angular は自前で簡単な型検査器を書いているようで、Vue のテンプレートでも同じことができないかと試してみました。

型検査器を作るにあたって Angular の実装はもちろん、TAPL を読んだりもしました。また、TypeScript のコードの型情報を取るために TypeScript Compiler API を使うのですが、ドキュメントはほぼなく、API の型定義を見て手探りで進めていくしかないので、かなり苦戦しました。「TypeScript Compiler API の基本的な使い方、コード例と作ってみたもの」を読めばなんとなく雰囲気はわかると思います。学んだことをアウトプットしたりもしてました。

そして作ったものが以下です。

https://github.com/ktsn/vue-template-diagnostic

このアプローチはたしかに型検査もできて、エラー位置も正しく取れるのですが、どうしても簡単な検査しかできないというのがネックでした。例えば、ジェネリックスや関数オーバーロードの実装をしようとしたらとても大変になることが容易に想像できます。実際に、少なくとも当時は Angular もテンプレート内の関数呼び出しの型チェックは完全ではないようでした。

テンプレート → TypeScript AST 変換

最終的に、やはり型検査自体は TypeScript にやらせたほうが良いということで、テンプレートを TypeScript AST (Abstract Syntax Tree: 抽象構文木) に変換し、それを TypeScript のコンパイラーに検査させるというアプローチを取りました。

実装について

処理の流れは大雑把に以下のようになります。

  1. 単一ファイルコンポーネントから <template> 部分を抽出する (すでに Vetur が実装済み)。
  2. vue-eslint-parser を使ってテンプレートをパースし、AST にする。
  3. 2 のテンプレートの AST を対応する TypeScript の AST に変換する。
    • ノードに元のテンプレートの式の位置情報を記録する。
  4. TypeScript のコンパイラーに 3 を処理させ、エラー情報を取得する。

例えば、以下のようなファイルが Hello.vue として存在するとします。

<!-- Hello.vue -->
<template>
  <p>{{ message }}</p>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({
  data() {
    return {
      message: 'Hello'
    }
  }
})
</script>

Vetur はこのファイルの <template> ブロックを抽出し、以下のような TypeScript コードを仮想的に生成します (読みやすくするために整形してます)。

// Hello.vue.template
import __Component from "./Hello.vue";
import { __vlsRenderHelper, __vlsComponentHelper } from "vue-editor-bridge";

__vlsRenderHelper(__Component, function () {
    __vlsComponentHelper("p", { props: {}, on: {}, directives: [] }, [this.message]);
});

Vetur はすでに .vue ファイルの <script> ブロックを抽出しているため、./Hello.vue をインポートしたものは <script> ブロック内でエクスポートしているものになります。よって、__Component はコンポーネントの型情報を持っています。

__vlsRenderHelper__Component のインスタンスの型を this に付与するための仮想的なヘルパーで、その定義は以下のようになっています。

export declare const __vlsRenderHelper: {
  <T>(Component: (new (...args: any[]) => T), fn: (this: T) => any): any;
};

これによって、this.message の型をチェックすることができるようになります。

TypeScript の AST には任意の位置の情報を入れることができる (ts.setTextRange) ので、テンプレートの式に対応する TypeScript のノードに、元の位置を記録します。これによって、出力される型検査エラーの位置は元のテンプレートの式に沿ったものになります。上記の例なら、this.message に対応するノードに、元のテンプレートの message の位置 ([38, 45]) を記録します。しかし、後述するように、この実装には問題がありました。

テンプレートのパースには vue-eslint-parser を使っています。Vetur 自身にもテンプレートパーサーはあるのですが、 ほぼ HTML のパーサーそのままで、ディレクティブや (当時は) mustache 式のパースはしていませんでした。一方で vue-eslint-parser はディレクティブの argument や modifier、v-for のような特別な式など、細かく情報が取れるのが利点でした。Vetur のパーサーを拡張することも考えましたが、まずはとにかく本質的な機能に集中して実装してしまいたかったので、vue-eslint-parser を選びました。

型検査も完全だし、エラー時に元のテンプレートの位置も出せるので、元々やりたかったことはすべてカバーできるアプローチでした。

Vetur に PR を出す

そして、最低限動きそうだという段階まで来たときに以下の PR を出しました。一番最初の typed-vue-template のコミットが 2017 年の 3 月で、この PR は 2018 年の 2 月だったので、1 年くらい試行錯誤してたことになります (途中でなにもしてない期間もありましたが……)。そしていろいろあってこの PR は今年の 4 月にマージされたので、実現するまでに 2 年かかったことになります。

https://github.com/vuejs/vetur/pull/681

問題

しかし、この実装が原因で、TypeScript コンパイラーのクラッシュが発生するケースがありました。原因は当時すでにわかっていて、TypeScript コンパイラーに投げた AST が不正な位置情報を持っていることで、内部のアサーションが失敗しているためでした。

PR の時点では、TypeScript Compiler API から生成したノードは位置情報が [-1, -1] となっていて、そのノードが型チェックに引っかかると、アサーションで落ちるということがわかっていました。それを防ぐためにテンプレートの式に関係ないノードにはすべて [0, 0] をセットしたりしていましたが、すべてのクラッシュは解決できていませんでした。また、これをするとソースコードが地獄のように煩雑になるのも問題でした。

これを解決するために、Pine のアイデアで、テンプレートの式と変換後の TypeScript コードのソースマップを生成することになりました。

ソースマップの生成

Pine が以下の PR でテンプレートの式と変換後の TypeScript コードのソースマップの生成を実装しました (一般的な source map とは厳密には違います)。

https://github.com/vuejs/vetur/pull/1215

以下のような流れでソースマップを生成しています。

  1. テンプレートから正しくない位置情報の TypeScript AST (a) に変換する過程で、位置情報を ts.setSourceMapRange を使って書き込む。
  2. 生成された TypeScript AST をプリントして、コードの文字列を取得する。
  3. 2 をパースして、正しい位置情報を持った TypeScript AST (b) を取得する。
  4. (a) と (b) を比較してソースマップを生成する。
    1. (a) と (b) は同じ構造なので、順番に各ノードを比較する。
    2. ts.getSourceMapRange が以前にセットした位置情報を返すノードに関して、ソースマップに書き込む。

テンプレート内の message の位置が [38, 45] で、変換後の this.message が [311, 323] とすると、この処理を行うと以下のようなマッピングが作られます。

SourceMapNode {
  from: {
    start: 38,
    end: 45,
    filename: 'Hello.vue'
  },
  to: {
    start: 311,
    end: 323,
    filename: 'Hello.vue.template'
  },
  offsetMapping: {
    38: 316,
    39: 317,
    40: 318,
    41: 319,
    42: 320,
    43: 321,
    44: 322,
    45: 323
  },
  offsetBackMapping: {
    311: 38,
    316: 38,
    317: 39,
    318: 40,
    319: 41,
    320: 42,
    321: 43,
    322: 44,
    323: 45
  }
}

offsetMapping では変換後の this. の位置情報 ([311, 315]) をスキップしており、offsetBackMapping では、this. の最初 (311) と最後 (316) の位置を元の message の先頭 (38) にマッピングしています。このようにすることで、テンプレートの位置 |message を仮想的な TypeScript コード内では this.|message とマッピングでき、逆に TypeScript コード内の |this.message および this.|message|message にマッピングすることができます。

これにより、TypeScript コンパイラーのクラッシュはなくなり、また、この実装でチェック対象の TypeScript AST が正しくなったことにより、ホバーによる変数情報の表示、補完などの他の機能も提供することができるようになりました。テンプレートから TypeScript AST に変換するコードもかなり読みやすくなりました。

今後の開発

まだこの機能は十分にテストできていないので、解決すべき問題がいろいろ出てきています。また、v-if でチェックした変数から null を取り除く機能 (type narrowing) や、コンポーネントの props の検査など、実装したい機能もたくさんあります。

特に、Vue Language Service (VLS: Vetur の裏側で動いてるモジュール) を CLI から実行できるようにすることはかなりやりたいです。これができるようになるとテンプレートの型チェックを CI で動かすなど、利用の幅がかなり広がります。

この記事を読んでいる Vetur ユーザーの方は、ぜひ vetur.experimental.templateInterpolationService を有効にして、Template Interpolation Service を試してみてください。そして、何かあればフィードバックを Vetur の Issue にもらえると嬉しいです。

謝辞

この機能の実装は Pine の協力と Vetur という土台がなければ実現しなかったと思います。特に TypeScript コンパイラーのクラッシュの問題は僕一人では対処できませんでした。

Nagashima さん が vue-eslint-parser を作ってくれていたおかげで、テンプレートの型チェックの実装がとても容易になりました。

元の Issue で議論に参加してくれた Herrington Darkholme にも感謝したいです。彼のコメントがなかったら今のアプローチに至ることはなかったと思います。

任意の背景色に対して読みやすい文字色を選択する方法

GitHub の Issue ラベルなど、任意の色の中に文字を入れたい場合があります。このとき文字色が一色のみだと、背景色と似たような色のときに読みづらくなってしまいます。

以下のスクショでわかるように、GitHub は背景色によって文字色を黒か白のどちらにするかを計算しているようです。

個々のサービスの実装についてはわかりませんが、WCAG (Web Content Accessibility Guidelines) で定義されているコントラスト比を使うことで、背景色に対して読みやすい文字色を選択することができます。

WCAG で定義されているコントラスト比 (contrast ratio)

WCAG は Web のコンテンツを障害者がより利用しやすくするためにはどうするべきかが記された指標です。また一般的に、WCAG にそった Web ページを作ることで、全体的なユーザビリティの向上も期待できるとされています。

WCAG には様々な項目がありますが、ガイドライン 1.4.3 の Contrast (Minimum) に注目します。

The visual presentation of text and images of text has a contrast ratio of at least 4.5:1, except for the following:

— 翻訳 —

文字や画像文字の可視表現は少なくとも 4.5:1 のコントラスト比を持つこと。ただし、次のものは除く (省略)

4.5:1 というのは一つの目安なのですが、ここでは読みやすさの指標にコントラスト比というものを使用している点が重要です。背景色と文字色のコントラスト比を計算することができれば、それをもとに読みやすい文字色を選択できると考えられます。

コントラスト比の定義を見てみると、以下のようになっています。

(L1 + 0.05) / (L2 + 0.05)

L1 is the relative luminance of the lighter of the colors, and
L2 is the relative luminance of the darker of the colors.

— 翻訳 —

(L1 + 0.05) / (L2 + 0.05)

L1 は明るい方の色の相対輝度 (relative luminance)
L2 は暗い方の色の相対輝度

相対輝度というものが出てきたので更にたどってみます。以下は相対輝度の定義です。

the relative brightness of any point in a colorspace, normalized to 0 for darkest black and 1 for lightest white

Note 1: For the sRGB colorspace, the relative luminance of a color is defined as L = 0.2126 * R + 0.7152 * G + 0.0722 * B where R, G and B are defined as:

if RsRGB <= 0.03928 then R = RsRGB/12.92 else R = ((RsRGB+0.055)/1.055) ^ 2.4
if GsRGB <= 0.03928 then G = GsRGB/12.92 else G = ((GsRGB+0.055)/1.055) ^ 2.4
if BsRGB <= 0.03928 then B = BsRGB/12.92 else B = ((BsRGB+0.055)/1.055) ^ 2.4

and RsRGB, GsRGB, and BsRGB are defined as:

RsRGB = R8bit/255
GsRGB = G8bit/255
BsRGB = B8bit/255

— 翻訳 —

色空間の任意の地点における相対的な明るさ。最も暗い黒は 0、最も明るい白は 1 に正規化される。

注 1: sRGB 色空間では、ある色の相対輝度は L = 0.2126 * R + 0.7152 * G + 0.0722 * B である。ただし R, G, B は以下の通り。

RsRGB <= 0.03928 のとき R = RsRGB/12.92 それ以外のとき R = ((RsRGB+0.055)/1.055) ^ 2.4
GsRGB <= 0.03928 のとき G = GsRGB/12.92 それ以外のとき G = ((GsRGB+0.055)/1.055) ^ 2.4
BsRGB <= 0.03928 のとき B = BsRGB/12.92 それ以外のとき B = ((BsRGB+0.055)/1.055) ^ 2.4

そして RsRGB, GsRGB, BsRGB は以下のように定義される。

RsRGB = R8bit/255
GsRGB = G8bit/255
BsRGB = B8bit/255

CSS の色の値は sRGB なので、上記の式がそのまま使えます。これらの情報を使って文字色を選択します。

背景色とのコントラスト比が高い色を文字色にする

文字色にはたいてい黒か白を使用すると考えられるので、黒と背景色、白と背景色の 2 つのコントラスト比を計算し、大きい方の色を文字色として採用するという方法をとってみます。

実装は以下のようになります。

function chooseTextColor(red, green, blue) {
  // sRGB を RGB に変換し、背景色の相対輝度を求める
  const toRgbItem = item => {
    const i = item / 255
    return i <= 0.03928 ? i / 12.92 : Math.pow((i + 0.055) / 1.055, 2.4)
  }
  const R = toRgbItem(red)
  const G = toRgbItem(green)
  const B = toRgbItem(blue)
  const Lbg = 0.2126 * R + 0.7152 * G + 0.0722 * B

  // 白と黒の相対輝度。定義からそれぞれ 1 と 0 になる。
  const Lw = 1
  const Lb = 0

  // 白と背景色のコントラスト比、黒と背景色のコントラスト比を
  // それぞれ求める。
  const Cw = (Lw + 0.05) / (Lbg + 0.05)
  const Cb = (Lbg + 0.05) / (Lb + 0.05)

  // コントラスト比が大きい方を文字色として返す。
  return Cw < Cb ? 'black' : 'white'
}

// 適当な要素で試してみる
const body = document.body
body.innerHTML = 'Test'
body.style.backgroundColor = 'rgb(50, 50, 255)'
body.style.color = chooseTextColor(50, 50, 255) // 'white'

上記を動かすと以下のようになります。

様々な色で試すと以下のようになります。

背景色が暗い場合は白、明るい場合は黒が文字色として選択されていることがわかります。

まとめ

任意の背景色に対して読みやすい文字色を選択するために WCAG のコントラスト比を使いました。背景色と白、背景色と黒、それぞれのコントラスト比を求め、大きい方を文字色として採用します。これによって、背景色が暗い場合は白、明るい場合は黒が文字色として選択され、なにが背景色にきても文字を読みやすくすることができます。

GraphQL の情報を雑にまとめる

最近 GraphQL を使うことがあり、いろいろ調べたりしていることをメモしておきます。自分が必要なものしかまとめてないので情報には偏りがあります (具体的には Apollo、TypeScript や Ruby あたりに偏ってます)。

GraphQL / Apollo まわりのテストの話は別の記事で詳しく書くかもしれないです。

基本知識

GraphQL | A query language for your API

公式サイト。GraphQL のコンセプトやできることを知りたいときに見る。

GraphQL Concepts Visualized – Apollo GraphQL

GraphQL のコンセプトを図を交えて説明。アプリケーションのデータをグラフで表し、その一部を取得するために GraphQL を使う。効率的なキャッシュを行うこともできる。Apollo Client のキャッシュの実装についても解説。

JavaScript / TypeScript 関連

Apollo GraphQL

GraphQL サーバー、クライアントを作るための JavaScript ライブラリ。各種 UI ライブラリのバインディング、キャッシュなどがデフォルトで対応されてて便利。個人的には TypeScript の型定義がパッケージに含まれてるのが良い。

apollographql/apollo-cli

クエリから各種言語のコード (d.ts とか) を生成したり、クエリのバリデーションができる CLI ツール。

dotansimha/graphql-code-generator

スキーマやクエリからコード生成ができるツール。apollo-cli とは異なり、テンプレートを作ることで様々な形式のコードを生成することができる。

Getting started with TypeScript and Apollo – Apollo GraphQL

Apollo + TypeScript でどのように書くのか解説。apollo-cli でクエリの型定義を生成し、それを Apollo の型パラメータに付与する。

Mocking | GraphQL Tools

GraphQL クライアントのモックの仕方。graphql-tools を使って Resolver をモックにできる。

apollo-link/packages/apollo-link-schema

Apollo のリクエストとレスポンスをモックするために使う。

How to manage file uploads in GraphQL mutations using Apollo/Graphene

multipart/formdata を使って GraphQL でファイルアップロードを実現する仕様。Apollo の実装がすでにある。

facebook/dataloader

DB などへのリクエストをバッチ化するためのライブラリ。N+1 問題を避けるために使う。

Ruby 関連

GraphQL Ruby

GraphQL のスキーマを Ruby の DSL で書けるようにするやつ。GraphQL のスキーマファイルを生成する Rake Task も提供されている。

GraphQL – Lazy Execution

N+1 問題を避けるために DB アクセスのバッチ化をどのようにするかの解説。解説のための単純な実装なので、実際のアプリではちゃんとしたライブラリを使ったほうが良さそう。

sheerun/dataloader

facebook/dataloader の Ruby 実装。これ系のやつの中では一番単純で使いやすそう。

Nuxt.js のような自動ルーティングを可能にする Vue CLI プラグインを作った

Nuxt.js という Vue.js で SSR をするアプリケーションが簡単に書けるフレームワークがあります。Nuxt.js は SSR だけでなく、webpack の設定やディレクトリ構造なども最初から決められており、規約がすでに存在することによる開発の効率化の面においても注目されています。

個人的に Nuxt.js で便利だと感じている機能にルーティングの自動解決レイアウト機能があります。通常の Vue Router を使ったアプリではルーティングの設定は自分で書く必要がありますが、Nuxt.js では pages/ ディレクトリ以下の構造から自動的にルーティングの設定を生成してくれます。また、Rails のレイアウトのように、各ページごとにレイアウトファイルを指定することができます。

これらの機能に慣れてしまうと、Nuxt.js を使っていないプロジェクトにおいて自分でルーティングの設定を書くのがとても面倒になってきたので、Nuxt.js じゃなくてもいい感じにする Vue CLI プラグインを書きました。

vue-cli-plugin-auto-routing

また、vue-cli-plugin-auto-routing は独立したパッケージを組み合わせているので Vue CLI プラグインを使えない環境の場合は以下の2つを直接使うと良いです。

vue-auto-routing: ルーティングの設定をディレクトリ構造から生成する webpack プラグイン
vue-router-layout: レイアウト機能を提供するコンポーネント

vue-cli-plugin-auto-routing の使い方

Vue CLI v3 用のプラグインなので、それで構築したプロジェクトでのみ使用することができます。Vue CLI v3 でプロジェクトを生成するには以下のようにします。

# vue-cli v3 をインストール
$ npm install -g @vue/cli

# プロジェクトを作成
$ vue create new-project

vue-cli-plugin-auto-routing を使うにはプロジェクトのディレクトリ内で以下のコマンドを実行します。

$ vue add auto-routing

これを実行すると router.js が書き換えられ、layouts/, pages/ ディレクトリが追加されます。これらのディレクトリ内にファイルを追加すると、Nuxt.js と同様に動作することがわかると思います。

vue-auto-routing と vue-router-layout

vue-cli-plugin-auto-routing で生成された router.js を見ると以下のように記述されています。

import Vue from 'vue'
import Router from 'vue-router'
import routes from 'vue-auto-routing'
import { createRouterLayout } from 'vue-router-layout'

Vue.use(Router)

const RouterLayout = createRouterLayout(layout => {
  return import(`@/layouts/${layout}.vue`)
})

export default new Router({
  routes: [
    {
      path: '/',
      component: RouterLayout,
      children: routes
    }
  ]
})

vue-auto-routingpages/ 以下のディレクトリ構造からルーティングを生成しています。vue-router-layout は現在表示されているコンポーネントに指定されているレイアウトを layouts 以下から選択して描画するためのコンポーネントです。createRouterLayout のコールバックでレイアウトのコンポーネントを返しています。

Nuxt.js と異なり、Vue Router の設定は隠蔽されていないので、カスタマイズの幅は広いと思います。例えば、createRouterLayout のコールバックを書き換えてレイアウトの解決の仕方を変えたり、routes に追加のルーティングを増やしたりできます。

Vue CLI v3 が使えない場合はこれらのライブラリを直接使うことができます。詳細な使い方は vue-auto-routingvue-router-layout の README を読んでください。

まとめ

Nuxt.js を使っていないプロジェクトにおいても pages/layouts/ 機能を使えるようにする Vue CLI プラグイン vue-cli-plugin-auto-routing を作りました。Vue CLI プラグインが使えない環境では vue-auto-routingvue-router-layout を使うことで同じ機能を使えるようにできます。

Vue Router のルーティングはいろいろと細かく設定できますが、実際に開発していると Nuxt.js のルーティングで十分に感じます。これからはルーティングの設定を手で書くことは少なくなっていくのではないかなーと思います。

vue-thin-modal v1.0.0 をリリースしました

去年から作っていた Vue のモーダルコンポーネント vue-thin-modal の v1.0.0 をリリースしました。仕事でも結構使っていて、特に大きな問題もなく、API も安定しているのでメジャーバージョンを上げました。

vue-thin-modal は世の中の多くのつらいモーダル実装を見て、つらくならなくするために作ったライブラリです。主に以下のような特徴があります。

  • モーダルはどこに置いても DOM の実態は <body> 直下にマウントされる (いわゆる Portal)。
  • モーダルが開くと通常のコンテンツ部分はスクロールが止まる。モーダル内のコンテンツがウィンドウサイズを超えてもスクロールできる。
    • これで発生する、スクロールバーが消えることによるガタツキを防ぐ実装もしている。
  • モーダルを閉じたときに元のコンテンツにフォーカスを戻す。
  • デフォルトの CSS スタイルが提供されていて、何もしなくてもそれっぽく動く。
  • 背景とモーダルコンテンツのトランジションが独立していて、柔軟に設定できる。
  • モーダルの表示はスタックで管理していて、モーダルの上にモーダルとかもやろうと思えばできる (UI 的にどうなんだというのは置いといて)。

モーダルのつらさについては CodeGrid 5周年記念パーティーで話しているので、そのスライドも見てみてください。

vue-thin-modal を作る際には Bootstrap ModalModaal をかなり参考にしました。特に Bootstrap Modal の作り込みはすごくて、その完成度の高さに驚いた覚えがあります。

関連ライブラリとして vuex-modal というものもあります。こちらは vue-thin-modal を作る前に作ったものですが、今は内部で vue-thin-modal を使用しています。

自分の欲しい機能はすべて実装したので、これからの展望は特にないですが、バグレポートや機能要望などは歓迎です!

SSR + vue-meta で hydration 直後の変更が反映されない問題の対策

発生した問題

最近 Nuxt を使った Web サイトを実装することがあり、以下のように SSR の hydration 後に vue-meta を使用して viewport の値を書き換えるようなコードを書いていました。

// layouts/default.vue

export default {
  data () {
    return {
      screenWidth: 0
    }
  },

  mounted () {
    // クライアントサイドでデバイスの幅を取得する
    this.screenWidth = window.screen.availWidth
  },

  head () {
    const contentWidth = 1024
    const breakpoint = 767
    const likelyTablet = breakpoint < this.screenWidth && this.screenWidth < contentWidth

    // タブレットっぽかったら viewport をコンテンツ幅に指定
    const content = likelyTablet 
      ? `width=${contentWidth}` 
      : 'width=device-width,initial-scale=1'

    return {
      meta: [
        { hid: 'viewport', name: 'viewport', content }
      ]
    }
  }
}

SSR の段階では screenWidth が設定されないため、viewport が width=device-width,initial-scale=1 となりますが、その後、スクリーンの幅を見て、その値がある一定の範囲に収まる場合は viewport の設定を変えています。

vue-meta は Vue インスタンスのデータが更新されたらそれに対応する meta 要素を更新するようにしているので、これでうまくいくような気がしますが、動きません。実際には SSR 直後の値のまま変わらないです。

原因

原因は上記のような SSR 直後に発生させる更新はすべて vue-meta の hydration 処理として扱われてしまっているためでした。

vue-meta の実装を追いかけてみると、各コンポーネントで metaInfo (Nuxt では head) が指定されているとき、meta 系の要素の更新リクエストを発生させているようですが、それらはすべて requestAnimationFrame で遅延させて、短い間に複数の更新リクエストが走っても 1 つにまとめられているようです。

また、一方で、SSR で描画された meta 要素を再描画してしまわないために、1 回目の更新では実際の DOM の更新は行われないようです。

これによって、mounted フックの中で発生させた値の更新 (とそれに伴う meta の更新) は SSR 直後の 1 回目の更新に吸収され、期待した挙動をしていなかったということでした。

対策

とりあえず以下のように setTimeout などで mounted 内の更新も遅らせましょう。

export default {
  // ... 省略 ...

  mounted () {
    // vue-meta の hydration を待つ
    setTimeout(() => {
      // クライアントサイドでデバイスの幅を取得する
      this.screenWidth = window.screen.availWidth
    }, 0)
  },
}

vue-meta を直してもらうのが一番ですが、実装を見る感じだとこれに対処するのは難しそう & マウント直後に meta 要素を更新するケースは結構ニッチ (な気がする) なので、ドキュメントでこの挙動と対策について触れるのが落とし所になるのかなーという気がしています。

(Issue は一応作ってます。 https://github.com/declandewet/vue-meta/issues/224)

TypeScript Compiler API の基本的な使い方、コード例と作ってみたもの

2月20日 (火) に JavaScript メタプログラミング勉強会 Metapro.es という勉強会があり、そこで TypeScript (TS) Compiler API について LT しました。内容は TS Compiler API の基本的な使い方を話したものですが、短い時間で話しきれる内容でなかったのと、TS Compiler API の日本語資料は少ないので、ここに補足記事を書いておきます。

また、example コードを Github に置いているので、適宜参照・実行しながら読むと理解しやすいかもしれません。

TypeScript Compiler API とは

その名の通り、TypeScript のコンパイラーをアレコレすることのできる API です。あまり実践的な使い方を話題に上げている人は見かけないですが、コードの解析、変換、型の取得など、色々と強力なことができます。例えば、TSLintは TS Compiler API を使ってコードの解析を行っていると聞きます。また、Angular も利用しているらしく、Transformer という機能を使っていたり、自分が前にコード読んだ時はテンプレート内の式の型チェックをするために、コンポーネントの型情報を利用していた覚えがあります。Vue のための VSCode 拡張の Vetur でも使われており、コードの補完やホバー時の情報表示に TypeScript の型定義の情報を利用しています。

ドキュメントはほとんど書かれておらず、使う際にはエディターの補完と型情報を頼りに手探りで書くことになります。また、Wiki にも記載のある通り、この API は安定版ではないので、これからのバージョンアップで壊れる可能性が十分にあります。ただし、破壊的変更はこのページで一覧にしてくれるようです。

コード例

簡単なコード例を挙げながら使い方を説明します。メタプロの勉強会で話した内容なので、メタプロっぽい機能だけ紹介します。TS Compiler API はフラットな名前空間で展開されているので、とりあえず import で読み込みましょう。

import * as ts from 'typescript'

ts 以下に、色々な関数、型などがあるので、とりあえず ts. まで入力して補完を眺めてみるだけでもそれっぽい関数が見つかるかもしれません。

コードの AST を取得する

この例は ts-compiler-api-examples の 1-read-ast.ts に対応しています。

まず、最も簡単な例として、すでにある TypeScript のコードの AST を取得してみましょう。この例ではまず Program というものを作ります。Program はコンパイル対象とするソースコードやコンパイルの設定をすべて保持しているオブジェクトで、TS Compiler API のエントリポイントです。Program を生成することで、関連するソースコードが内部的に AST に変換され、それを取り出すことができます。Program を生成するには ts.createProgram を実行します。

const program = ts.createProgram(['test.ts'], {})

ts.createProgram の第1引数には対象とする TypeScript のコードへのパス、第2引数にはコンパイラーオプションを渡します。

次に、test.ts の AST を取得するために Program から SourceFile を取得します。SourceFile はその名の通り、TypeScript のソースコードのファイルを表すオブジェクトで、パースされた AST が格納されています。SourceFile を得るためには program.getSourceFile に、ほしいソースコードのパスを渡して呼び出します。

const source = program.getSourceFile('test.ts')

SourceFile の AST に直接アクセスするには statements プロパティを参照すれば良いです。また、ts.forEachChild というヘルパー関数も用意されているのでそれを使うのも良いでしょう。AST の内部構造は型定義を見るのも良いですが、AST Explorer に対象とするコードを入力して確認するのが一番楽だと思います。

if (source) {
  // 直接 AST を参照
  console.log(source.statements)

  // ts.forEachChild を使って走査
  ts.forEachChild(source, node => {
    console.log(node)
  })
}

AST を構築し、コードを生成する

この例は ts-compiler-api-examples の 2-print-code.ts に対応しています。

AST を読むだけでなく生成することもできます。AST 生成の関数はすべて ts.create から始まるため、そこまで入力して補完からそれっぽいものを選びましょう。生成したい AST のノードの型 (kind) を AST Explorer で確認し、そのノードの名前を ts.create の後ろにつけると良いです。例えば、VariableDeclarationList のノードを生成したい時は ts.createVariableDeclarationList 関数を使います。

const ast = ts.createVariableDeclarationList([
  ts.createVariableDeclaration(
    ts.createIdentifier('test'),
    ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
    ts.createLiteral('Hello!')
  )
], ts.NodeFlags.Const)

AST を文字列として出力するには Printer を作ります。Printer は ts.createPrinter から作成することができます。

const printer = ts.createPrinter()

Printer に対して出力したいノードと適当な SourceFile を与えると文字列が出力されます。

const source = ts.createSourceFile('test.ts', '', ts.ScriptTarget.Latest)
const code = printer.printNode(ts.EmitHint.Unspecified, ast, source)
console.log(code) // const test: string = 'Hello!'; と出力される

型情報を取得する

この例は ts-compiler-api-examples の 4-read-type-info.ts に対応しています。

TS Compiler API を使ってコードの型情報を取得するには TypeChecker を利用します。TypeChecker はコードの型チェックを行うモジュールですが、型の情報を取得して利用することもできます。TypeChecker を得るには program.getTypeChecker を実行します。

const checker = program.getTypeChecker()

TypeChecker で得られるものには、主に型 (Type) とシンボル (Symbol) があります。型は普段 TypeScript を書いていて使う型と同じで、ある型を表す型です (紛らわしい)。シンボルに関しては理解が怪しいのですが、束縛を表すもので、それへの参照を解決するために生成されるオブジェクトだという理解です。例えば、以下のようにあるクラス Foo が宣言されている時、その下で new Foo() が呼び出されている時、この2つのノードはあるシンボルを通じて結ばれています。

class Foo {
  name = 'Foo'
}

const foo = new Foo()

以下はソースコード中のすべてのクラスから型情報を取得し、マークダウンとして出力する例です。型を取得するには checker.getTypeAtLocation、シンボルを取得するには checker.getSymbolAtLocation を使用します。どちらも引数にノードを渡し、そのノードに対応する型、および、シンボルが返されます。なぜかクラス定義のノードから直接型を取得するとインスタンス (new して得られるオブジェクト、new foo = new Foo()foo) の型が返り、クラス定義の Identifier のシンボルから型を取得するとコンストラクタ (new されるオブジェクト、new foo = new Foo()Foo) の型が返るので、以下の例ではそのどちらも取得しています。

ts.forEachChild(source, function next(node) {
  if (
    ts.isClassDeclaration(node) &&
    node.name
  ) {
    // クラスインスタンスの型を取得
    const type = checker.getTypeAtLocation(node)

    // クラスコンストラクタのシンボルを取得
    const ctorSymbol = checker.getSymbolAtLocation(node.name)
    if (!ctorSymbol) return

    console.log(printClassDoc(type, ctorSymbol))
  }
})

上記の printClassDoc の中身は以下のようになります。

function printClassDoc(type: ts.Type, ctorSymbol: ts.Symbol): string {
  // クラス名を取得
  let buf = '## ' + ctorSymbol.name + '\n'

  // コンストラクタの型を取得
  const ctorType = checker.getTypeOfSymbolAtLocation(ctorSymbol, ctorSymbol.valueDeclaration!)
  ctorType.getConstructSignatures().forEach(sig => {
    // 引数の型
    const params = sig.parameters.map(serializeSymbol)

    // 戻り型
    const ret = checker.typeToString(sig.getReturnType())

    buf += '\nnew (' + params.join(', ') + ') => ' + ret + '\n'
  })

  buf += '\n### Properties\n'

  // プロパティを取得
  type.getProperties().forEach(p => {
    buf += '\n- ' + serializeSymbol(p)
  })

  return buf + '\n'
}

function serializeSymbol(symbol: ts.Symbol): string {
  const type = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!)
  return symbol.name + ': ' + checker.typeToString(type)
}

コンストラクタの型 (new する時に通る引数と戻り型) を取得するために checker.getTypeOfSymbolAtLocation を使って、シンボルから型を取得しています。また、得られた型の getConstructSignatures を使って、new する時に関する情報を取得し、引数と戻り値の型情報を取り出しています。プロパティについても同じような感じです。

作ってみたもの

vuetype

確か一番最初に TS Compiler API を使って作ったのがこれで、Vue の SFC (.vue ファイル) から TypeScript 部分を抽出して .d.ts ファイルを出力するというものです。この記事では紹介していないのですが、LanguageService という API に .d.ts の文字列を出力する機能があり、それを使用しています。

typed-vue-template

Vue のテンプレートの型チェックをする試み第一弾で、クラス構文で書かれた Vue のコンポーネントにコンパイルされた render 関数を挿入してうまいこと型チェックさせようという試みでした。手探りで書いているのでかなりやっつけコードです。文字列で出力してしまってるので、エラーの位置がずれて微妙な感じだったので他の手段を探すことになりました。

vue-template-diagnostic

Vue のテンプレートの型チェックをする試み第二弾で、Angular の Language Service と同じで、自分で型チェッカーを書いてみたものです。コンポーネントの型情報を取得し、それをもとにテンプレート内の式を検証していく感じです。これは単純なケースだとうまく動いたのですが、複雑な型が入るかもしれないことを考えると (ジェネリックス、関数オーバーロードなど) つらくなってきたので諦めました。

Vetur のテンプレート型チェック機能

Vue のテンプレートの型チェックをする試み第三弾で、typed-vue-template と発想は似ていて、render 関数を TypeScript の型チェッカーにチェックさせるというアプローチです。しかし、こちらは HTML から生成した AST (HTML) を AST (TS) に変換し、それをそのまま型チェッカーに渡しています。これによって、エラーの場所はもとの HTML の適切な位置に出力されるし、TypeScript でチェックできるものはすべて型チェックできるようになりました。Vetur は VSCode のチームの人がオーナーだったり、TypeScript のコントリビューターが PR 投げてたりするので、結構 TS Compiler API の勉強になるかと思います。僕も Vetur のコードを読んで TS Compiler API の理解がだいぶ深まったと思います。

といったように、僕の場合は Vue のテンプレートの型チェックをしたくて TS Compiler API にいつの間にか詳しくなっていたという感じでした。

まとめ

TypeScript Compiler API でできることは結構あり、TypeScript で書かれたコードの解析、生成、型情報の取得など、様々なことができます。ただし、API はまだ安定しておらず、ドキュメントもほとんど書かれていないので、使う時には苦労すると思います。実例として、Vue のテンプレートの型チェックをする機能を作っていて、結構おもしろいことができます。

ただ、Babel 7 で TypeScript のコードをパースできるようになっているので、単純なコード解析や変換であれば Babel を使ったほうが良いんじゃないかとは思います。おそらく TS Compiler API じゃないとできないのは型まわりの情報を扱うことなので、そういうことをしたい時に使うのが良さそうです。

Vue のテンプレートの型チェックについて

静的型が好きな人と話していると大体テンプレートの型をチェックしたいという話を聞くのですが、Vue には今のところそれをうまく行う方法はありません。

すこし前に Vue のテンプレートの型チェックについて LT したのですが、これは vue-class-component などの Vue 標準の API から離れた書き方を強制するのでちょっと微妙な感じでした。これは、以前は Vue のコンポーネントの this の型を得るためには、クラス構文を使う必要があったためです。

しかし、TypeScript v2.3 に導入された ThisType によって、オブジェクトリテラル内部のメソッドの this の型推論が行えるようになったのと、 Vue v2.5 から、TypeScript の型定義が大きく改善されたことで、Vue 標準の API を使っても this の型をうまく得ることができるようになりました。

良い機会なので、上記の LT をした時に作ったものを更新して、標準の API でもテンプレートの型チェックをできるようにしてみました。

  • typed-vue-template – テンプレートを TypeScript として script ブロックに挿入する実装部分
  • typed-vue-loader – typed-vue-template を webpack loader のインターフェースとして提供してるもの

typed-vue-loader をクローンして、npm i && npm run example:dev した後に、example ディレクトリのソースを編集すると動作がわかると思います。

ただし、この実装ではコンパイル時に型チェックができるだけであり、エラーが発生している場所はわからないですし、エディタ上で型情報を利用した補完機能を利用するということはできないです。そのあたりを頑張ろうとして作ったのが以下です。

vue-template-diagnostic

vue-template-diagnostic の発想は Angular と同じで、自分で型チェッカーを作ってしまおうというものです。しかし、やはり型チェッカーを再実装するのは結構つらいのと、Vue のテンプレート内部の式はほぼ JavaScript と互換性を持っているので、うまいこと TypeScript で処理させたほうが良さそうだなーと感じ、開発は止まってます。

エラーの場所がわからないということに目をつぶれば、typed-vue-template でやっていることを AST に対して行う Language Service を作れば、テンプレートの型チェック処理を TypeScript に丸投げしつつエディタからその情報を取れそうな気がするので、次はそれを試してみようかなと考えてます。

TypeScript の恩恵を受けつつ Vue を使いたい その2 (Value オブジェクトを扱う)

せっかく型のある TypeScript を使うなら、なるべくプリミティブを使わずに Value オブジェクトを使いたいです。この記事では TypeScript + Vue で Value オブジェクトを扱うためにいろいろと考えたり、試したことを述べます。この記事に書かれていることは Vue 公式にはあまり推奨されない (と思われる) 方法なので、採用する場合には注意してください。

Value オブジェクトとは

Value オブジェクトとは文字通り値を表すオブジェクトのことで、ドメイン駆動設計において用いられる概念です。例えば、料金を表す値を扱いたい時、プリミティブ型 (number) で表現するのではなく、Value オブジェクトとして Price クラスを定義します。Price クラスを作ることによって、単なる数値を表現するだけでなく、料金に関する処理を Price クラスに追加することができます。また、Price という型がつくことによって、料金が入ってほしい部分に料金以外の値が入ることをコンパイル時に防ぐことができます。

具体的には、以下のようなクラスを作ります。

class Price {
  // 数値はプライベートな値として持っておく
  constructor(private value: number) {}

  // 値の表示形式など、その値に関する責務を持たせる
  format() : string {
    return `¥${this.value}`;
  }

  valueOf() : number {
    return this.value;
  }
}

let price: Price = new Price(100);
price = 200;  // コンパイルエラーになる。型をつけることで誤った種類の値が入ることを防げる

Vue の VM に渡すデータはプレーンなオブジェクトでなければならない?

Vue の VM に定義した Value オブジェクトを監視させたいのですが、VM にはプレーンなネイティブオブジェクトを渡す必要があると公式のドキュメントに書いてあります。プレーンなオブジェクトとはおそらく、Object リテラルで作るようなオブジェクト ({ hoge: 'fuga' } みたいな) を指すのだと思いますが、そうなると、Vue で Value オブジェクトを扱えるのかどうかが疑問になります。

なぜプレーンなオブジェクトである必要があると言われているのか

Vue の issue を眺めていると、getter で取得できる値が監視されないと報告されているものを見かけます。Vue は監視対象のオブジェクトの getter, setter を書き換えて値の監視を実現しているので、元々 getter が設定されているものは監視することができません。また、ソースコードを読んでみると、オブジェクトのメンバが列挙可能でないと監視対象にならないのもわかります。これらは Vue の設計上どうしようもないことなので、これらのケースには対応しないという意思表示のために、プレーンなオブジェクトを渡すように書かれているのだろうと思います。

変更の監視が行われるケース

まず、重要なのが、オブジェクトの構造がなんであれ、そのオブジェクト全体を入れ替えてしまえば、値の変更が検知されるという点です。よって、どうしても変更の監視をさせたいオブジェクトがあるときは、とりあえず更新があるときに全体を入れ替えるようにするとうまくいきそうです。

具体的なコードは以下のようになります。

class Test {
  constructor(private value: string) {}

  // getter なので Vue に監視されない
  get text() : string {
    return `Test ${this.value}`;
  }
}

const vm = new Vue({
  template: '<div>{{ test.text }}</div>', // getter を指定している
  data: {
    test: new Test('Text1')
  }
})

vm.$mount('#app'); // Test Text1 と表示される

vm.test = new Test('Text2'); // test 全体を入れ替えたので変更が検知され、
                             // 表示が Test Text2 に更新される

また、getter で得られる値や列挙不可能な値でなければ監視できるのではないかと思います。先に書いた Price クラスのインスタンスを VM の data に渡すと、value プロパティが Vue によって置き換えられて、監視されているのがわかります。

Value オブジェクトとして不変かつ拡張不可なオブジェクトを作って Vue で扱う

上で述べたように、Value オブジェクト自体はプレーンなオブジェクトではないので、これを監視するのは公式には推奨されていません。しかし、オブジェクトの変更ではなく、そのオブジェクト全体を入れ替える時は、オブジェクトの中身がどのようになっていても変更を検知できます。

よって、Value オブジェクトを変更不可にし、値を変更する時はその都度新しい Value オブジェクトを代入し直すようにすれば、Vue でも Value オブジェクトを扱えそうです。Value オブジェクトは状態を持たず不変であるべきなので、この制約によって実装がつらくなることはないかと思います。

また、Value オブジェクト内に列挙可能なプロパティが存在する場合、それが Vue によって監視されますが、Value オブジェクト内のプロパティを監視するのはあまり意味が無いのと、Value オブジェクトが書き換わるのは気持ち悪いのでこれを防ぎたいと思います。Vue の Observer のコードを読むと、オブジェクトが拡張不可能である場合はそれ以降の監視をしないようになっている (Object.isExtensible(value) が false になる時は監視をしない) ので、Object.seal などで Value オブジェクトを不変にすると良さそうです。もしくは、Vue は toString でプレーンなオブジェクトかどうかを判断し、そうでない場合は監視を行わないようになっているので、文字列にした時の表現方法が決まっているのであれば toString をオーバーライドしてしまうのも良いかもしれません。ただ、こちらは isExtensible よりも Vue の実装の細かな部分に依存してしまうように思うので、あまりやらないほうが良いかもしれません。

上記を実装すると、以下の様なコードになります。

// Value オブジェクト
class Price {
  constructor(private value: number) {
    // Object.seal で Vue がこのオブジェクトを書きかえるのを防ぐ
    Object.seal(this);
  }

  format() : string {
    return `¥${this.value}`;
  }

  valueOf() : number {
    return this.value;
  }
}

// VM で使う例
const vm = new Vue({
  el: '#app',
  data: {
    // 100円を表す Value オブジェクトを監視させる
    price: new Price(100)
  }
});

// 200円に更新する
// Value オブジェクトそのものを更新すれば値の変更を検知できる
vm.price = new Price(200);

独自のデータ形式も Vue で扱える?

Value オブジェクトだけでなく、独自のデータ構造を扱う時でも上記を知っていると役に立ちそうです。例えば、ES2015 の Map の中の値の変更は Vue で監視することができませんが、値の更新時に新たな Map を返す不変な Map を独自に定義すると、Vue でも Map を扱うことができるようになります。ただし、対象とするデータが大きくなるほど、更新の量が多くなり、パフォーマンスが悪化するので注意して使う必要はありそうです。

まとめ

Vue にオブジェクトを渡すと、それが拡張可能であるときに、getter ではないプロパティ、かつ、列挙可能なプロパティが監視対象となります。Value オブジェクトはプレーンなオブジェクトではないので、公式にはこれを監視させることは推奨されませんが、Value オブジェクトを不変にし、値の更新の度に全体を入れ替えることで、Vue でも変更の監視が可能になります。同様に独自のデータ形式も Vue で扱うことができますが、パフォーマンスが悪化する可能性があるため、注意して使う必要がありそうです。

TypeScript の恩恵を受けつつ Vue を使いたい その1

最近 TypeScript の恩恵を受けつつ Vue を使うためにいろいろと試行錯誤しています。この記事ではコンポーネントの定義と、コンポーネント内のロジックを再利用可能にするための Mixin の定義を TypeScript でどのように書けばよいかを述べます。

vue-class-component を使う

TypeScript で Vue を使う時は vue-class-component はほぼ必須だと思います。なぜなら、Vue のコンポーネントの生成の仕方では、TypeScript コンパイラがコンポーネントの型を解釈することが難しいからです。

例えば、コンポーネントの生成を行うコードは以下のように書けますが、reverseMessage メソッドの中では、this がそのコンポーネントであることがわからないですし、this.message が存在していることもわかりません。

const App = Vue.extend({
  data() {
    return {
      message: 'Hello Vue.js!'
    };
  },
  methods: {
    reverseMessage() {
      this.message = this.message.split('').reverse().join('');
    }
  }
});

const vm = new App({ el: '#app' });

上記のコードを vue-class-component を用いて書き換えると以下のようになります。

import Component from 'vue-class-component';

@Component
class App extends Vue {
  message: string;

  data() {
    return {
      message: 'Hello Vue.js!'
    };
  }

  reverseMessage() {
    this.message = this.message.split('').reverse().join('');
  }
}

const vm = new App({ el: '#app' });

vue-class-component を使用すると、class 構文を使ってコンポーネントを生成できるため、先に述べた、this やそれに生えているプロパティの型がわからないという問題が解消されます。

Vue の Mixin を活用して、責務を分割する

Vue には mixins オプションがあり、あらかじめ定義しておいた機能を一つのコンポーネントに継承する事ができます。これを利用することで、コンポーネントから機能を細かく分割することができます。

例えば、バリデーションのロジックは特定のコンポーネントに持たせるよりは、Mixin にして、再利用可能にするのが良いでしょう。また、同じ機能でも、デザインの都合でコンポーネントを再利用できない場合に、ロジックの部分のみを Mixin にして再利用できるようにするのも良いと思います。

Mixin を作る

Mixin はコンポーネントと同様の書き方で書きます。コンポーネントを作る時と同じように、持たせたいメソッドや、プロパティの定義などを vue-class-component の書き方で書いていきます。

// ユーザー情報の入力値をバリデーションする機能を持たせる Mixin
@Component
class UserValidationMixin extends Vue {
  validateName(name: string) : boolean {
    return name.test(/^[0-9a-zA-Z]+$/);
  }
}

// ユーザー情報の入力を親に通知する機能を持たせる Mixin
@Component({
  props: {
    onChangeName: {
      type: Function,
      required: true
    }
  }
})
class UserUpdateMixin extends Vue {
  onChangeName:  (name: string) => void;
}

Mixin を使うコンポーネントに Mixin の型を持たせる

Mixin を利用するには、コンポーネントの mixins オプションに、Mixin のコンストラクタを渡せば良いです。また、TypeScript に Mixin で定義したプロパティやメソッドを認識させたい時は、Mixin をインタフェースとして渡せば良いです。

以下の例では、先に定義した UserValidationMixinUserUpdateMixin を利用したコンポーネントを定義しています。

@Component({
  // Mixin を適用する
  mixins: [
    UserValidationMixin,
    UserUpdateMixin
  ],

  props: {
    user: {
      type: Object,
      required: true
    }
  },

  template: `
  <form action="#" method="post">
    <input
      type="text"
      name="name"
      :data-error="nameError"
      :value="user.name"
      @input="handleInputName">
  </form>`
})
class UserForm 
    extends Vue
    implements UserValidationMixin, UserUpdateMixin {

  user: IUser;

  validateName: (name: string) => boolean;

  onChangeName: (name: string) => void;

  get nameError() : boolean {
    return this.validateName(this.user.name);
  }

  handleInputName(event: Event) {
    this.onChangeName(event.target.value);
  }
}

Vue v1.0.24 以下での注意点

現在の Vue のバージョン (v1.0.24) では、Vue のコンストラクタを mixins オプションに指定することはできませんが、それを可能にするパッチがマージされたので、次のバージョンでは上の書き方が可能になると思います。

現在のバージョンでこの記事の書き方を実践するには、以下の様な、vue-class-component のラッパーを作ると良いです。

import originalDecorator from 'vue-class-component';

// vue-class-component のデコレータをラップする
function Component(options) {
  if (typeof options === 'function') {
    return originalDecorator(options);
  }
  return function(target) {
    const mixins = options.mixins || [];

    // Vue のコンストラクタは自身が生成された時の options を保持している
    options.mixins = mixins.map(m => m.options);

    return originalDecorator(options)(target);
  };
}

// vue-class-component のように使える
@Component({
  mixins: [MixinA, MixinB]
})
class SomeComponent extends Vue implements MixinA, MixinB {}

つらい部分

この記事のように書くことで、TypeScript で Vue を使うのはかなり良い感じになったのですが、まだまだつらい部分があります。

Mixin の継承先に型を書くこと

Mixin で定義したプロパティやメソッドの型を、継承先のコンポーネントで改めて書かないといけないのがとてもつらいです。TypeScript 自体が Mixin 構文をサポートすればこのつらさもなくなるような気がしますが、TypeScript 公式でこのめんどくさい Mixin のやり方を紹介しているので望みは薄いような気も……。

ただ、ECMAScript の class 式を使えば Mixin っぽいことができるので、この辺の書き方がもっと楽に書けるようになれば良いような気もします。

View の方はまったく型に守られないこと

当たり前なのですが、コンポーネントの template の内部では型が合っているかまったくチェックされません。なので、template の中からメソッドを呼び出すときに、そのメソッドが本当に存在するのか、引数の型が合っているのか、などは人間が注意して書く必要があります。この辺りもちゃんとやりたいなら React とか別のライブラリを使えという話になると思います。

まとめ

TypeScript の利点を受けつつ Vue を使うためには、vue-class-component を使うと良いです。また、コンポーネント内の機能を再利用可能にするために、Vue の Mixin を活用するのも良いでしょう。Mixin を使う時は、mixins オプションに Mixin のコンストラクタを渡すとともに、コンポーネントの宣言時に implements で Mixin を渡すことで、Mixin の型も TypeScript に認識させることができるようになります。

vq にイベントハンドリングの機能を実装した

vq v1.1.1 をリリースしました。主な更新点は、イベントハンドリング機能の追加です。

vq は複雑なアニメーションを簡潔に書くことを目指しているライブラリで、関数型プログラミングの考え方から着想を得ています。今回の更新点以外の詳しい説明は以前書いた記事や README を参照してください。

Example

今回のアップデートで vq.click, vq.focus など、DOM 要素のイベントが発火してからアニメーションを実行するための関数を追加しました。関数の一覧は README に記載しています。例えば、クリックするたびに要素を移動させるようなコードは以下のように書けます。

const box = document.getElementById('box');

// アニメーションのプロパティ & オプション
const move1 = {/* ... */};
const move2 = {/* ... */};
const move3 = {/* ... */};

// box をクリックした時になんらかのアニメーションを実行したい
const clickBox = vq.click(box);

const moveBox = vq.sequence([
  clickBox(vq(box, move1)),
  clickBox(vq(box, move2)),
  clickBox(vq(box, move3)),
  (done) => moveBox(done) // 無限にアニメーションをさせる
]);

moveBox();

また、vq.element という、より低レベルな関数も提供しており、イベントの発火後にアニメーションを実行するかどうかをより細かくコントロールすることができます。第一引数に指定した DOM 要素が、第二引数に指定したイベントを監視します。第三引数には関数を渡すことができ、イベントが発火するたびにこの関数が実行されます。この関数が true を返した時、指定したアニメーションが実行されます。

const alert = document.getElementById('alert');

// アニメーションのプロパティ & オプション
const show = {/* ... */};
const hide = {/* ... */};

// Enter キーが押された時に何らかのアニメーションを実行したい
const pressEnter = vq.element(window, 'keypress', event => {
  return event.which === 13 // press enter
});

vq.sequence([
  vq(alert, show);            // アラートを表示させる
  pressEnter(vq(alert, hide)) // Enter が押されたらアラートを隠す
])();

イベントのハンドリングを vq で扱えるようにしたことで、ユーザーのインタラクションを伴うアニメーションもより簡単にかけるようになったのではないかと思います。それぞれのイベントハンドラはアニメーションとは独立した関数として生成できるため、小さなコードを組み上げるような実装ができ、コードの見通しが良くなることを期待しています。これからの更新では、イベントハンドラの結合や、DOM 以外のイベントを扱うための API を実装する予定です。

Vue のコンポーネントと Vuex Store を繋げるためのヘルパ vuex-connect を作った

某勉強会中にネタを思いついて、急いで作って LT してその日のうちに npm にアップしたら、翌日Vue 公式に紹介されていてだいぶビビったやつです。VueVuex のヘルパなので、この二つを理解していることが前提になります。

vuex-connect (Github)
vuex-connect (npm)

vuex-connect の機能

vuex-connect は connect 関数のみ提供しており、やっていることは react-reduxconnect と同じです。connect は第一引数に Vuex の getters、第二引数に actions を受け取ります。 また、戻り値として別の関数を返し、こちらの関数には、コンポーネント名と、コンポーネントのコンストラクタを渡します。 最終的に、渡したコンポーネントのプロパティに getter と action をつなげた上位コンポーネント (コンテナ) を返します。

// コンポーネントを定義
const HelloComponent = Vue.extend({
  props: {
    message: {
      type: String,
      required: true
    },
    updateInput: {
      type: Function,
      required: true
    }
  },
  template: `
  <div>
    <p>{{ message }}</p>
    <input type="text" :value="message" @input="updateInput">
  </div>
  `
});

// コンポーネントと Store をつなげる
const getters = {
  message: (state) => state.message
};

const actions = {
  updateInput: ({ dispatch }, event) => dispatch('UPDATE_INPUT', event.target.value)
};

const HelloContainer = connect(
  getters,
  actions
)('hello', HelloComponent);

なぜ有用なのか?

これも react-redux で言われていることと同じですが、開発の定石として、フレームワークやライブラリへの依存をなるべく少なくするというものがあります。UI コンポーネントと、状態管理を司るフレームワークを疎結合にするために、Redux では、UI の見た目の実装を行うためのコンポーネントと、Store 内の状態とやり取りするためのコンポーネントを分けるという方針をとっています。これらに関して、前者は Presentational Component、後者は Container Component と呼ばれています (Usage with React | Redux)。

Vue と Vuex で例えると、Presentational Component、Container Component のどちらも Vue のコンポーネントとして実装されます。Presentational Component は Vuex への依存関係を持っておらず、Vue のみで完結しています。Container Component は Vue と Vuex の両方に依存しており、両者をつなげる役割のみを担います。ここで、Vuex を使うのをやめて別の状態管理のフレームワークを使うケースを考えます。Vuex に依存しているコンポーネントは Container Component のみであり、また、その役割は Vue と Vuex をつなげるということのみであるため、Container Component を交換するのみで良くなります。すなわち、Vue と Vuex の依存を最小限にすることで、Presentational Component として実装した Vue コンポーネントが再利用できるようになっているということです。

Vuex の公式ドキュメントを見てみると、上記のようなことは考慮されておらず、Vue と Vuex が密結合になっているように感じたため、vuex-connect を作りました。Vue だと props でバケツリレーするのはちょっと違和感あるのですが、アプリケーションのあちこちで状態を変えられるよりは良いのかなと……。もしくは、action を props に渡すのではなく、events の方に結びつけても良いかもしれないですね。

実装

実装したての頃は react-redux とインターフェースを合わせようとして四苦八苦していたこともあり、Vue.prototype._init を上書きしたりしてました。しかし、よくよく考えてみると vuex オプションにそのまま受け取った引数を渡せばいいことに気が付き、かなり簡潔な実装になりました。

https://github.com/ktsn/vuex-connect/commit/f0a254ea5c7b330bcc2446167b970940d67a724b

コンポーネントの props に getter と action を渡したいので、template の部分をがんばって生成しています。

悩み

connect で生成したコンポーネントに props を渡せるようにするべきかを悩んでいます。react-redux だと渡せるようにしているのですが、正直あんまりそうするケースが思い浮かばないし、データの流れが二股になって複雑になることを懸念しています。また、React の場合は react-router などが props にデータを渡してくるので、それに対処するために props の値を読めるのは有用なのですが、Vue の場合はそういうケースで props を使うことがないので、やるとしても別のアプローチをするべきなのかなーとも思ってます。あと、Vue の props はちゃんとコンポーネント側で定義してあげる必要があるので実装がめんどくさそう……。

最後に

なんとなく Vuex 使ってる人はものすごく少ないような感じがするのですが、Vuex を使うことがあったら vuex-connect も一緒に使っていただけると嬉しいです。そして contribution もウェルカムです!

テキスト編集における Selective Undo を実装した

Selective Undo とは、その名の通り Undo したい処理を選択することのできる Undo のことです。テキスト編集で Selective Undo をするためのライブラリを書いたので、それを実装する上で学んだことを簡単に述べます。

ktsn/selectivie-undo-text: A Selective Undo library for text editing (Github)
selective-undo-text (npm)
Demo

基本的なコンセプト

Selective Undo を実現するための手法は複数あるのですが、この記事では操作の逆変換を用いる Selective Undo について述べます。

逆操作を生成、実行する

ある操作 A があるとき、その逆操作 A’ は、A を打ち消す操作のことを指します。例えば、文字列 ‘abc’ を 5 番目に挿入する操作 ins(5, ‘abc’) がある時、5 番目から 3 文字削除する操作 del(5, 3) は ins(5, ‘abc’) の逆操作と言えます。定義から、元の操作 A と逆操作 A’ の両方を実行した時、得られる文字列は入力した文字列と同一であるということが言えます。すなわち、操作 A を Undo したい時はその逆操作 A’ を生成し、実行すれば良いということになります。

しかし、Undo を選択可能にするためには、逆操作を生成するだけでは不十分であり、生成した逆操作を変換する必要があります。なぜなら、Undo したい操作が実行された時の文字列の内容が、現在の文字列の状態と異なることにより、得られる結果が期待していないものとなる場合があるためです。

例えば、操作 A = ins(0, ‘abc’) で、操作 B = ins(0, ‘123’) とします。A, B がこの順番で実行された時、文字列は ‘123abc’ となります。このとき、操作 A の逆操作を実行すると、A’ = del(0, 3) となり、文字列は ‘abc’ となります。操作 A は ‘abc’ を挿入する操作なので、逆操作 A’ は ‘abc’ を消すことが期待されますが、’123′ が消されてしまいました。

これは、操作 B が実行されたことで、対象の文字列の場所がずれてしまったためです。このようなことを防ぐために、次で述べる操作変換を行う必要があります。

現在のコンテキストに適用できるようになるまで操作変換する

ある操作 A, B があるとき、操作変換とは A, B から変換後の操作 A’, B’ を生成することであり、A(B(text)) == B'(A'(text)) となります (B, A の順で実行した時の結果と A’, B’ の順で実行した時の結果が一致する)。ただし、Selective Undo の実装上は、逆操作の変換結果のみを使用するのみで良いです。その説明は長くなりそうなので省略しますが、直感的には逆操作ではない方の操作は、履歴にすでに入っている操作であるため、変換する必要がないというイメージです。

ここで、コンテキストとは、ある操作を実行するときの、全体の文字列の状態を指します。操作を実行する時は、その操作が想定しているコンテキストと、現在のコンテキストが一致していなければ、正しい結果が得られません。これは、前の節で述べた、操作の順番によって期待される結果が得られないという話と同じです。

逆操作を操作変換する流れは以下のとおりになります。操作 A, B, C があり、操作 A を Undo したいとき、逆操作 A’ が生成されます。このとき、操作の履歴は以下のようになり、A’ は A が実行された直後のコンテキストを期待しています。操作は左から右に適用されるとし、カッコはカッコ内の操作がその位置で実行されることが期待されていることを示します。

A (A') B C [現在のコンテキスト]

A’ を現在のコンテキストで適用できるようにするため、操作変換を行います。まずは A’ と B で操作変換します。

A B (A'') C [現在のコンテキスト]

次に A” と C で操作変換します。

A B C (A''')[現在のコンテキスト]

A”’ は現在のコンテキストに適用できるようになったため、A”’ を実行することで A の Undo となります。

実装

各操作ごとのクラスを作り、それぞれが apply, inverse, transform メソッドを持つようにしています。

apply はその操作を実行させるためのメソッドで、引数で渡された文字列に対して操作を行います。

inverse はその操作の逆操作を生成するためのメソッドです。削除操作に関しては、何を削除したのかがわからなければ逆操作を生成することができないため、apply が実行された後でなければエラーを投げるようにしています。

transform は引数に渡された操作から、自分自身を操作変換するメソッドです。操作変換は、Selective Undo を適用するアプリケーションに依存する処理で、かつ、操作の種類の組み合わせごとに処理を書かなければならないので、最も実装が大変な部分だと思います。

また、undo 時に逆操作を生成、操作変換を行う処理は Buffer クラスに書いています。主要な処理は各操作のクラスに書いているため、こちらはすっきりしてます。

未実装部分など

Undo の使い勝手を良くするために、追加した文字列が連結可能な場合など、まとめることのできる操作はまとめるのが良いと思いますが、それはまだ実装していません。また、テストを十分にしているわけではないので、Undo の結果がおかしかったり、人間の直感に反する場合があると思います。

Selective Undo が最も活きるユースケースが複数人で一つの文書を編集しているときだと考えているため、そういったケースで使えるようにはしたいです。複数人で編集できるようにするには Operational Transformation の実装などしないといけないのでかなりめんどくさそうですが……。以前、Google Wave OT は実装したのですが、Selective Undo と組み合わせた時にどうなるかなど、考慮すべき点はまだまだありそうです。

参考文献

Selective Undo の基本的な考え方は以下の論文から。

操作変換の部分は Operational Transformation と似てるので、以下も参考になると思います。

webpack + Testem でフロントエンド JavaScript のテストを書く

webpack を使っているプロジェクトで、テストコードも webpack で依存関係の解決やトランスパイルをしたいということがあったので、その時に行ったことをまとめます。

webpack の設定

テストコードは個々のファイルがエントリポイントとなっているため、webpack の設定でもそのように指定する必要があります。 entry にエントリポイントにしたいファイルを配列で渡すことで、ロード時に指定されたファイルをすべて実行するようにビルドされます。webpack が標準で glob パターンなどをサポートしてないので、別途自分で書く必要があります。

// webpack.config.test.js
const path = require('path');
const glob = require('glob');

module.exports = {
  // ...
  entry: glob.sync('./test/**/*.js'),
  output: {
    path: path.resolve(__dirname, '.tmp'),
    filename: 'test.js'
  },
  // ...
};

Testem の設定

Testem には webpack で出力されたファイルを読み込ませます。 src_files パスを指定するだけで良いです。

# testem.yml
---
  framework: mocha
  src_files:
    - .tmp/test.js

watch させるスクリプトを書く

テストコードが変更される度にテストの実行を行いたい場合、別途スクリプトを書く必要があります。Node で書く場合、以下のように、 Testem.prototype.startDev を実行することで Testem を起動させることができます (たぶんドキュメントに書かれてない)。gulp を使っているプロジェクトなら、 webpack と Testem を watch させるタスクを書くのが一番簡単だと思います。

// gulpfile.js
const fs = require('fs');
const webpack = require('webpack');
const Testem = require('testem');
const yaml = require('js-yaml');

gulp.task('webpack:test', () => {
  const compiler = webpack(require('...config_file...'));

  compiler.watch(200, (err) => {
    if (err) throw new err;
  });
});

gulp.task('testem', () => {
  const testem = new Testem();
  testem.startDev(yaml.safeLoad(fs.readFileSync(__dirname + '/testem.yml')));
});

gulp.task('test', ['webpack:test', 'testem']);

シェルスクリプトで書く場合は以下のようになります。単純に & で複数実行するだけだと、 ctrl-C した時にバックグラウンドタスクが終了しないため、終了させるための処理を書く必要があります。 trap 'kill %1' SIGINTctrl-C された時にバックグラウンドタスクにまわした webpack を終了してくれるようになります。また、Testem を終了させた時、webpack を終了させるまで待つために、 wait コマンドの実行が必要です。

#!/bin/bash

trap 'kill %1' SIGINT
webpack --watch --config (config_file) & testem
wait

テストコードを書く

ここまでやれば後はテストコードを書くだけです。webpack がモジュール読み込みやトランスパイルなどをやってくれるため、ブラウザ上で動かす必要のあるテストも書きやすいと思います。

import assert from 'power-assert';
import module from '../src/some-module';

describe('Module', () => {
  it('should return true', () => {
    assert(module() === true);
  });

  ...
});

おわりに

個人的に、テストコードを書くことは二の次になりやすいので、テストを行うまでのプロセスを簡単にすることや、テストコードを書くこと自体を楽にすることは重要視しています。テストコードを webpack でビルドできるようにしたり、変更を watch させたりすることで、テストコードをとても書きやすくなったと感じてます。

Vue.js における methods の this は自動的に VM に束縛される

執筆当時の環境

  • Vue.js v1.0.17

JavaScript で以下の様なコードを書いた時、onResize 内の this はグローバルオブジェクト (window) となり、this.log('resized') はエラーとなります。

const obj = {
  log: function(str) {
    console.log(str);
  },

  onResize: function(event) {
    this.log('resized');
  }
};

window.addEventListener('resize', obj.onResize); // this.log('resized') でエラー

上記のコードの obj を、以下のように Vue.js の VM にするとエラーが起きなくなり、意図した通りに動作するようになります。また、this の値は obj に束縛されています。

const obj = new Vue({
  methods: {
    log: function(str) {
      console.log(str);
    },

    onResize: function(event) {
      this.log('resized');
    }
  }
});

window.addEventListener('resize', obj.onResize); // エラーが発生しない

このことから、Vue.js では methods に渡された関数を VM に束縛していると言えます。実際にコードを追って確かめてみます。

methods で Github のリポジトリを検索すると、 _initMethods というメソッドが存在しているのがわかります。このメソッドは src/instance/internal/state.js の中に定義されています。_initMethods の中を見てみると、bind(methods[key], this) という記述があります。JavaScript ネイティブの bind ではないですが、どうやらここで this を束縛しているように見えます。

vue/state.js at 521e8d2754c2e7f172c3c9702fdb74fe993027fb · vuejs/vue

なぜわざわざ独自の bind 関数を使っているのかを調べるために、この bind の定義も見てみました。bindsrc/util/lang.js に定義されています。

vue/lang.js at 521e8d2754c2e7f172c3c9702fdb74fe993027fb · vuejs/vue

関数の上のコメントには、ネイティブの bind よりも早いと書いてあります。少し調べてみたところネイティブの bind は、this の束縛の他に型チェックや、引数の束縛なども行うため遅くなっているとのことでした。ただ、遅いとはいってもほとんどのケースでは気にならない違いだと思うので、普通にアプリケーションを作る際には気にしなくても良さそうです。

javascript – Why is bind slower than a closure? – Stack Overflow

JavaScript ライブラリを npm で公開するためにやっていること

最近、何度か自作のライブラリを npm にアップしています。その時にやっていることを書き留めておきます。

前提条件

  • ライブラリはビルドが必要なもの (webpack や Browserify などを使う)
  • ビルド後のファイルを公開したい

公開時にビルドをするように設定

ビルド後のファイルはバージョン管理システムにはコミットするべきではありません。したがって、 npm publish をするタイミングでビルドを走らせて、生成されたファイルを公開するようにします。これを行うには、 prepublish を使うのが良いです。 prepublish に指定されたコマンドは、 npm publish の直前に実行されるようになります。

// package.json
{
  ...
  "scripts": {
    "prepublish": "gulp build"
  }
  ...
}

公開の必要がないファイルを無視

ライブラリを使う側にとってはビルド後のファイル以外 (テストコードなど) は必要ありません。npm publish 時に除外したいファイルは .npmignore で指定することができます。.npmignore.gitignore と同じような書き方で書けます。僕が作っているライブラリだと、babel や eslint の設定ファイル、gulpfile、ビルド前のファイルなどを除外しています。

# .npmignore
/.babelrc
/.eslintrc
/gulpfile.js
/src/

main にビルド後のファイルを指定

CommonJS や AMD による読み込み対応しているライブラリの場合、package.jsonmain を指定するべきです。main に指定したファイルが、require('<ライブラリ名>') と書かれた時に読み込まれるようになります。

main にはビルド後のファイルを指定します。ライブラリの使用者が、作成者と同じ設定で module loader を使っているとは限らないためです。

// package.json
{
  ...
  "main": "dist/library.js"
  ...
}

ビルド後のファイルにライセンスコメントを付与

以下の様な感じで、gulp-header を使い、ビルド後のファイルの頭にライセンスコメントを付けています。コメントの中身は別ファイルに分けて BANNER みたいなファイル名にしています。また、ライブラリ名やバージョンなどは package.json の中から読み込んでいます。

// gulpfile.js
const gulp = require('gulp');
const fs = require('fs');
const header = require('gulp-header');

gulp.task('header', () => {
  return gulp.src(['dist/**/*.js'])
    .pipe(header(fs.readFileSync('./BANNER', 'utf-8'), require('./package.json')))
    .pipe(gulp.dest('dist'));
});

まとめ

  • 公開の直前にビルドが走るように、prepublish にビルドを行うコマンドを指定
  • .npmignore で公開の必要がないファイルを除外
  • require('<ライブラリ名>') で読み込めるように、main にビルド後のファイルを指定
  • gulp-header を使ってライセンスコメントを付与

複雑なアニメーションとそれに伴う処理を簡潔に書くことのできるライブラリ vq を作った

最近 JavaScript のアニメーションの実装につらみを感じていたので、それを解消するためにライブラリを作りました。 vq というライブラリで、Velocity.js というライブラリのヘルパーという位置づけです。 内部のアニメーションは Velocity.js にまかせていて、vq は記述を簡潔に書けるようにしています。

vq – GitHub
vq – NPM

Velocity.js のつらみ

Velocity.js は、jQuery.animate と同じような文法で DOM 要素のアニメーションを記述することのできる JavaScript ライブラリです。 Velocity.js のインタフェースは jQuery と似たような感じですが、実装では requestAnimationFrame でアニメーションさせてたり、jQuery.animate にはない機能を備えていたりと、jQuery.animate よりも優れています。DOM のアニメーションを実装するときには無くてはならない存在です(と思ってます)。

しかし、アニメーションの規模や複雑さが増加してくると、Velocity.js 単体ではかなりつらくなってきます。以下に具体例を挙げます。

1. 複数の要素のアニメーション

Velocity.js は1つの要素を対象としたアニメーションは、メソッドチェーンで簡潔に書くことができます。

$el.velocity(...).velocity(...);

しかし、アニメーションさせたい要素が二つ以上あり、それぞれ、他の要素のアニメーションの進捗に依存している時、うまく書くことができなくなります。例えば、complete コールバックを使用して、あるアニメーションの終了したことを確認して、次のアニメーションを行うという処理を書くとき、ネストが発生してしまいます。

$el1
  .velocity({
    height: 200,
    width: 300
  }, {
    duration: 500,
    complete: function() {
      $el2.velocity({ // <--- つらい
        ...
      });
    }
  });

Velocity UI Pack には複数の要素のアニメーションを書くための RunSequence という関数も用意されていますが、こちらも少々力不足なように感じます。例えば、アニメーションの合間に、アニメーション以外の処理を挟む際に、ネストが発生してしまいます。これは 2. 3. で詳しく述べます。

2. アニメーションの間に特定の処理を入れる

Velocity.js はアニメーション以外の処理をハンドリングすることが苦手です。 例えば、以下のように、RunSequence で複数の要素をアニメーションさせる例を考えます。

$.Velocity.RunSequence([
  { e: el1, p: props1, o: opts1 },
  { e: el2, p: props2, o: opts2 },
  { e: el3, p: props3, o: opts3 }
]);

ここで、el2 をアニメーションさせた後に、特定の処理 (たとえば、テキストを変えるとか) を行う時を考えます。RunSequence はアニメーションのみをサポートしているため、アニメーション以外の処理は complete コールバック内で行う必要があります。また、上記のように、アニメーションに関する設定があらかじめ変数として別の部分で定義されている時、complete コールバックを生やすことが必要であり、そのコードを書くと一気に汚くなります。

$.Velocity.RunSequence([
  { e: el1, p: props1, o: opts1 },
  { e: el2, p: props2, o: $.extend({
    complete: function() { // <-- とてもつらい
      ...
    }
  }, opts2) },
  { e: el3, p: props3, o: opts3 }
]);

上記の例だけでもかなりつらいですが、これに加えて、complete 内の処理が非同期で、その非同期処理の後にアニメーションを続けるといった処理を書くとなると、絶望的な状況になります。

3. 共通処理を少し変更するというのがやりづらい

Velocity.js は jQuery.animate のように、第一引数にアニメーションさせるプロパティ、第二引数にアニメーションのオプションを指定することができますが、これらを一つのオブジェクトにまとめて渡すこともできます。これを利用して、共通のアニメーションを別の場所に定義して使い回すという使い方もできます。具体的には以下の様なコードになります。

// プロパティを p, オプションを o で指定
var fadeIn = {
  p: {
    opacity: [1, 0]
  },
  o: {
    duration: 500,
    easing: 'easeOutQuad'
  }
};

$el.velocity(fadeIn);

この書き方はコードの見通しが良くなって便利なのですが、ある状況特有の設定をしたい時につらくなります。 例えば、ある部分だけアニメーションにディレイを書けたい場合、odelay を追加する必要があるのですが、2. で挙げたように、変数に格納したオプションに新たな値を追加するのはコードをかなり汚くしてしまいます。

また、Velocity.js には RegisterEffect という、あるアニメーションを登録して、使い回すという機能があります。RegisterEffect を使えばある程度共通処理はきれいになりますが、これを使うと RunSequence が使えなかったりします。

vq の特徴

vq は上記のようなつらさを解消できる設計となっています。上記のようなケースでもネストを発生させることなく、オプションに値を追加することも簡潔な記法で行うことができます。 vq の基本的な書き方は以下のとおりです。vq(el, animation) は Velocity.js の書き方を真似ていて、el がアニメーションさせる要素、animation がアニメーションのプロパティとオプションです。引数が二つの場合は、二番目の引数にプロパティとオプションの両方が記載されているとみなします。プロパティとオプションを分けて、それぞれ第二引数、第三引数として渡すこともできます。

以下の例では、el1 に animation1、el2 に animation2、el3 に animation3 が順番に実行され、その後、ログに “animated” と出力されます。

vq.sequence([
  vq(el1, animation1),
  vq(el2, animation2),
  vq(el3, animation3),
  function() { console.log('animated') }
]);

HTML 要素とアニメーションの設定を分けている

前に述べたとおり、アニメーションのプロパティとオプションをあらかじめ変数に入れておくことはコードの見通しが良くなるため、複雑なアニメーションを書くときには有効だと思います。しかし、Velocity.js の RunSequence は、HTML 要素、プロパティ、オプションをまとめた一つのオブジェクトしか受け付けないため、あらかじめ変数に入れて使い回す書き方がしづらいと感じます。なぜなら、アニメーションを使い回すのに、プロパティとオプションを切り出すのは良いのですが、HTML 要素は切り出すべきものではないためです。むしろ、HTML 要素は場面によっていろいろと異なるものになると思います。

vq では、HTML 要素と、プロパティ、オプションを別の引数として受け取るため、これらを分離することが容易にできます。

アニメーションとそれ以外の処理を同じものとして扱う

vq.sequence は Velocity.js の RunSequence と同じように、アニメーションを順番に実行する関数です。この関数はアニメーションの実行だけでなく、任意のタイミングで関数を実行することができます。以下の例では、animation1 と animation2 の間に、”log” と出力する処理を書いています。

vq.sequence([
  vq(el1, animation1),
  function() { console.log('log') },
  vq(el2, animation2)
]);

固有の処理を後で付け加えることが容易

ある特定の状況だけ、共通処理とは少し異なる挙動のアニメーションをしたい場合、メソッドチェーンによってそれを行うことができます。以下の例では、animation1 に 1000ms のディレイをつけ、duration を 700ms に変更しています。

vq.sequence([
  vq(el1, animation1).delay(1000).duration(700),
  vq(el2, animation2)
]);

vq の仕組み

以下、vq の具体的な実装について説明します。

アニメーションを実行する関数を生成する

vq(el, animation) という関数は実はアニメーションを実行させているのではなく、アニメーションを実行する関数を返しています。具体的に、以下のようなコードを考えます。

vq.sequence([
  vq(el1, animation1),
  function() {
    console.log('test');
  }
]);

このコードは以下のコードと同じと考えて良いです。(厳密には違いますが)

vq.sequence([
  function(done) {
    animation1.o.complete = done;
    el1.velocity(animation1);
  },
  function() {
    console.log('test');
  }
]);

すなわち、vq.sequence は関数の配列を受け取り、それを端から順番に実行していくだけの関数です。 また、引数に与えられた関数が引数を受け取る形になっている場合、第一引数をコールバック関数とみなし、そのコールバックが呼ばれないかぎり、次の処理には移りません。上記の例では、vq によって生成された関数が、第一引数に done というコールバックを持っています。これをオプションの complete に代入しているため、vq.sequence はアニメーション終了まで処理を待機させます。

関数オブジェクトにさらに関数を生やす

vq で生成した関数には、オプションを変更するためのメソッドが生えています。これは単純に、関数を生成する際に、そのメンバとしてメソッドを代入しているだけです。 JavaScript は関数もオブジェクトとして扱われるため、通常のオブジェクトと同様に、メンバを追加することができます。

生成した関数に、アニメーションのオプションを記録しておき、メソッドが呼ばれた時、オプションの値を変更させています。 また、メソッドチェーンができるように戻り値で元の関数を返すようにしています。

まとめ

JavaScript のアニメーションライブラリには Velocity.js という便利なライブラリがありますが、規模が大きく、複雑なアニメーションを実装するときはかなりつらくなってしまうケースがありました。今回、そのつらみを取り除くために、vq というライブラリを作りました。vq を使うと、ネストをすることなく複雑なアニメーションを書くことができます。また、あるケースの時だけ、共通部品として定義したアニメーションとは異なる挙動をさせたいということも簡潔に書くことができます。

現在は並列で実行するようなアニメーションには対応していないため、次はそれに対応したいと考えています。ぜひ、試してみて、フィードバックをいただけたら嬉しいです。

vq – GitHub
vq – NPM

XCode で動作環境に応じて API の URL などの設定を変更する

OSX, iOS アプリでも Rails の RAILS_ENV のように、動作環境に応じて値を変えたいという時があります。 この記事では、アプリから利用する Web API の URL を動作環境に応じて切り替えるのを例に、そのやり方を説明します。執筆時の開発環境は下記のとおりです。

  • XCode: Version 7.2
  • Swift: version 2.1.1

1. Build Configuration を作成する

PROJECT -> Info -> Configurations に、動作環境の分だけビルド設定を追加します。例えば、ローカル環境を対象としたビルド設定を追加したいときは、Debug Local と Release Local を追加します。追加するときは、それぞれ、元からある Debug と Release をコピーすると良いでしょう。

Build Configurations

2. Scheme を作成、編集する

Manage Schemes から、動作環境の分だけ Scheme を作成します。また、作成した Scheme それぞれをダブルクリックで編集し、ビルド設定を 1. で作成したものに変更します。例えば、ローカル環境用の Scheme として、AppLocal を作成したら、そのビルド設定は Debug Local および Release Local に設定しておきます。

Scheme

Manage Schemes

Build Configuration の変更

3. コンパイラの設定を変更する

Scheme の作成後は、ソース中で環境ごとに条件分岐ができるように、コンパイラの設定を変更します。PROJECT -> Build Settings 内をいじります。

Objective-C を使う場合は Apple LLVM 7.0 – Preprocessing -> Preprocessor Macros の値を変えます。各動作環境が判別できるように、適当な変数を定義すると良いです。今回の例だと、Debug Local と Release Local に LOCAL=1 を設定します。

Objective-C のコンパイラ設定の変更

Swift を使う場合は Swift Compiler – Custom Flags -> Other Swift Flags の値を変えます。 -D <flag> の形式で <flag>true にできるようなので、そのように書きます。今回の例だと -D LOCAL を追加すると良いです。また、複数の値を設定する場合は -D DEBUG -D LOCAL のように書くことができます。

Swift の方にはなぜか DEBUG の値が設定されていないため、ついでに設定しておくと良いと思います。

Swift のコンパイラ設定の変更

4. Conditional compilation statement を書く

Conditional compilation statement で条件分岐させて、開発環境ごとに異なる値を変数に入れてやります。

Objective-C の場合は以下のように書きます。

#ifdef LOCAL
  NSString * const kAPIBase = @"http://localhost.example.com/api/";
#else
  NSString * const kAPIBase = @"http://production.example.com/api/";
#endif

Swift の場合は以下のように書きます。

#if LOCAL
  let kAPIBase = "http://localhost.example.com/api/"
#else
  let kAPIBase = "http://production.example.com/api/"
#endif

参考

指定した行数でテキストを省略できるライブラリ Truncator を作った

N 文字目以降を省略するというライブラリはたくさんあるのですが、行数指定できるものは見かけないので作りました。N 行以上になった時は省略したいというケースは結構ある気がするんですが、なぜそういうライブラリは無いのだろう……。

Truncator – NPM
Truncator – Github

使い方

truncate(el, text, { line: 3, ellipsis: '……' });

のような感じで使います。この例だと、要素 el に文字列 text を入れて、それが 3 行に収まるように省略します。また、省略記号は ...... を指定してます。

アルゴリズム

以下のような感じのアルゴリズムで動いてます。
1. el の一行の高さ L を取得する。
2. 行数 n * L で目標の高さ H を算出する。
3. eltext を入れてその高さ h を算出する。
4. h <= H を満たす、最大の省略位置を二分探索し、省略後の文字列を el に入れる。

一行の高さを取得する

一行の高さは window.getComputedStyle(el).lineHeight で簡単に取得できる……と思いきや、normal とかが返ってくるケースがあるので、工夫する必要があります。 Truncator では、対象の要素に適当な一文字を入れた時の高さを一行の高さとして扱っています。

省略すべき位置を二分探索する

単純に、center = (left + right) / 2 して、text.substring(0, center) し、h <= H だったら left を center に、そうでなければ right を center にする二分探索です。ただ、h <= H だったときでも center が解ではないとは限らないため、次の探索でも center を探索空間に含めたままにする必要があります。

最後に

バグ報告大歓迎です!

Bookshelf のアソシエーションで発生する循環読み込みによるエラーを回避する

Node.js の ORM である Bookshelf では公式サイトの例のようにアソシエーションを定義することができます。 しかし、モデルの定義を別ファイルに分けるとこの例が動かなくなります。 この記事では、モデルの定義を別ファイルに分けても意図通りに動くコードの書き方の説明と、それを支援する Bookshelf プラグインの紹介をします。

循環読み込みする例

以下のコードは page.js 内で book.js を読み込み、 book.js 内で page.js を読み込んでおり、循環読み込みが発生しています。 例えばこの状態で、別のファイルから page.js を読み込むと、 book.js 内の require('./page'){} を返し、 Book モデルの pages アソシエーションを使用するときにエラーが発生します。

// book.js
const Page = require('./page');

const Book = bookshelf.Model.extend({
  pages: function() {
    return this.hasMany(Page);
  }
});

module.exports = Book;
// page.js
const Book = require('./book');

const Page = bookshelf.Model.extend({
  book: function() {
    return this.belongsTo(Book);
  }
});

module.exports = Page;

解決策

この問題については、生成したモデルを保持するオブジェクトを別で定義するというのが一般的な解決策だと思います。 つまり、以下のようなコードになります。 以下のコードは、 require の循環読み込みが発生しているものの、実際にモデルが利用されるときには models 以下に定義したモデルが格納されているため、意図した通りの動作になります。

// book.js
const models = require('./models');

require('./page');
const Book = bookshelf.Model.extend({
  pages: function() {
    return this.hasMany(models.Page);
  }
});

module.exports = models.Book = Book;
// page.js
const models = require('./models');

require('./book');
const Page = bookshelf.Model.extend({
  book: function() {
    return this.belongsTo(models.Book);
  }
});

module.exports = models.Page = Page;
// models.js
exports = {};

あらかじめ全て読み込んでおく例

上記の解決策でも動くのですが、各モデルのファイルで依存するファイルを読み込む必要があります(require してる部分)。なぜなら、あるモデルを利用する時に、依存する他のモデルの生成が完了しているとは限らないためです。 モデルの数が増えてくると、依存関係を書くのがめんどくさくなってくると思います。また、漏れがあった時に気づくことが困難です。

よって、 models.js 内であらかじめモデルをすべて読み込んでおくのも良いのではないかと思います。 各モデルのファイルから依存するモデルを読み込む require を削除し、 models.js を以下のようにします。 MODEL_DIR には、各モデルのファイルが入っているディレクトリを指定します。各モデルのファイルと models.js の間で循環読み込みしていますが、 exports = {}; の後に require しているため、 require('./models') は意図したオブジェクトを返してくれます。

// models.js
exports = {};

const fs = require('fs');
const MODEL_DIR = './';
fs.readdirSync(MODEL_DIR)
  .filter((filename) => 'models.js' !== filename)
  .forEach((filename) => {
    require(MODEL_DIR + filename);
  });

ES6 Proxy を使って必要なときに読み込む例

すべて読み込むのではなく、必要となったタイミングで読み込むようなコードは以下のようになります。 Proxy を使わなくても models('ModelName') でモデルを返すような関数を定義すれば同じようなことは実現できます(文字列で書きたくなかった)。Node.js ではまだデフォルトで Proxy を使えない点と、最新の記法を使うために Polyfill が必要な点、また、モデル名をファイル名に変換する処理が必要な点に気をつける必要があります。(執筆時の Node.js の最新版は 5.4.0)

const _ = require('lodash');
const MODEL_DIR = './';

const proxy = new Proxy({}, {
  get: function(models, name) {
    return models[name]
      || (models[name] = require(MODEL_DIR + _.snakeCase(name));
  }
});

exports = proxy;

registry プラグイン

上記のようなことをやってくれる Bookshelf のプラグインが標準で提供されています。

Plugin: Model Registry · tgriesser/bookshelf Wiki

bookshelf.model('ModelName', Model) でモデルを登録し、 this.hasMany('ModelName') のように、アソシエーションの時に文字列でモデル名を指定することができます。 しかし、アソシエーション以外で、モデルを利用する時に、 bookshelf.model('ModelName') と書くのがめんどくさいです。個人的には自前で書いてしまう方が良いように思います。

まとめ

Bookshelf のモデルの定義を別ファイルに分けると、単純な書き方では循環読み込みでエラーが発生するようになります。 この問題は、生成したモデルを保持するオブジェクトを別で定義することで解決することができます。 モデルを定義する作業を楽にするために、モデルのファイルをあらかじめすべて読み込む処理を書いたり、モデルが必要となったタイミングで動的に読み込む処理を書くのも有効です。 また、Bookshelf 標準でこういった問題を解決してくれる registry というプラグインが提供されています。