タグ別アーカイブ: JavaScript

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

指定した行数でテキストを省略できるライブラリ 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 を探索空間に含めたままにする必要があります。

最後に

バグ報告大歓迎です!

State パターンでアニメーションの挙動を制御する

あるオブジェクトが絶えずアニメーションをするようなコードを書く時、様々な状態に応じて挙動を変えたいというのはよくあることだと思います。単純に実装すると、状態に対応するフラグを記録しておき、それに応じて条件分岐するという書き方になると思います。しかし、状態の数が増えると、フラグ管理が大変になり、コードがどんどん汚くなっていきます。そこで、State パターンのように、状態ごとに別々の挙動を行う関数を定義し、オブジェクトの状態が遷移した時に実行する関数を切り替えるようにすると、コードが状態ごとに整理され、見やすくなります。

アニメーションにおける条件分岐のつらさ

例えば、下記のように、矢印キーで動かすことができ、壁にぶつかると跳ね返るような Ball オブジェクトについて考えます。Ball は update() メソッドを持ち、requestAnimationFrame() で毎回このメソッドが呼ばれます。cx, cy が Ball の中心の座標で、vx, vyupdate() ごとに cx, cy に加算される値 (速度) です。矢印キーを押すと ax, ay (加速度) が更新され、押した方向へとボールが動きます。簡単のために、DOM への要素の追加などのコードは省いています。

const viewportHeight = 300;
const viewportWidth = 400;

class Ball {
  constructor() {
    // 中心点の座標
    this.cx = 200;
    this.cy = 150;

    // 速度
    this.vx = this.vy = 0;

    // 加速度
    this.ax = this.ay = 0;

    // ボールの半径
    this.r = 10;
  }

  update() {
    // 壁にぶつかったら跳ね返る
    if (this.cx - this.r < 0 || this.cy + this.r > viewportWidth) {
      this.vx *= -1;
    }
    if (this.cy - this.r < 0 || this.cy + this.r > viewportHeight) {
      this.vy *= -1;
    }

    this.vx += this.ax;
    this.vy += this.ay;
    this.cx += this.vx;
    this.cy += this.vy;
  }

  accelerate(code) {
    this.ax = this.ay = 0;
    
    switch (code) {
      case 37: // left
        this.ax = -0.1;
        break;
      case 38: // up
        this.ay = -0.1;
        break;
      case 39: // right
        this.ax = 0.1;
        break;
      case 40: // down
        this.ay = 0.1;
        break;
      default:
        // do nothing
    }
  }
}

let b = new Ball();
$(window)
  .on('keydown', (e) => b.accelerate(e.which))
  .on('keyup', () => b.accelerate());

animate();

function animate() {
  b.update();
  requestAnimationFrame(animate);
}

この Ball オブジェクトを拡張して、一定の速度以上の時は壁にぶつからなくするようにしたり、マウスポインターを乗せたときに動きを止め何らかのエフェクトをかけたりすると、条件分岐が増えてつらくなってきます。書いた直後は良いのですが、時間が立つにつれ、各条件分岐について完全に把握することは難しくなり、コードの変更の際、バグを混入させる確率が上がります。

class Ball {
  ...
  update() {
    if (/* マウスホバーしていたら */) {
      /* 何かのエフェクト */
      return;
    }

    if (/* 速度が一定以下なら */) {
      // 壁にぶつかったら跳ね返る
      if (this.cx - this.r < 0 || this.cy + this.r > viewportWidth) {
        this.vx *= -1;
      }
      if (this.cy - this.r < 0 || this.cy + this.r > viewportHeight) {
        this.vy *= -1;
      }
    }

    this.vx += this.ax;
    this.vy += this.ay;
    this.cx += this.vx;
    this.cy += this.vy;
  }
  ...
}

条件分岐の代わりに State パターンを使う

State パターンを使うと、上記の Ball オブジェクトの挙動をよりすっきりとしたコードで書くことができます。State パターンでは、各状態ごとにクラスを定義し、ある状態の時のオブジェクトの挙動は、対応する状態のクラスのみに記述されます。つまり、オブジェクトの挙動をそのオブジェクトのクラスに書くのではなく、状態のクラスに書くという点が特徴です。

上記の Ball オブジェクトの挙動を State パターンで書くと以下のようになります。まず、各状態ごとの挙動を定義した関数を作ります。次に Ball オブジェクトに新たに behavior というプロパティを追加します。behavior は関数が入るプロパティであり、update() メソッドの中で毎回呼ばれます。そして、状態が切り替わるごとに、behavior に状態に対応した関数を代入するようにします。今回の例では、update の内部以外に状態によって挙動が変わる部分がないため、関数自体を切り替えています。完全なコードは CodePen に置いておきました。Simple Ball Animation with State Pattern

class Ball {
  constructor() {
    this.behavior = moveBehavior;
  }

  update() {
    this.behavior(this);
  }
}

function moveBehavior(ball) {
  /* 矢印キーで動かす処理 */

  if (/* 速度が一定以上 */) {
    ball.behavior = leaveBehavior; // 状態を変える
  }
}

function effectBehavior(ball) {
  /* マウスホバーでエフェクトをかける処理 */
}

function leaveBehavior(ball) {
  /* 壁にぶつからずに動く処理 */
}

まとめ

オブジェクトの update 時に実行される関数を状態ごとに変えることによって、アニメーションの挙動の記述の条件分岐を減らし、コードをすっきりとさせました。単純なアニメーションなら同一のオブジェクトに直接挙動を書けば良いですが、ユーザーの入力が増えたりして、取りうる状態が増えそうであれば、State パターンで書き直したほうが良いように思います。

Vue.js を使った中規模 Web アプリ向けのディレクトリ構造を考えた

最近 Vue.js を使って Web アプリを書いていて、どんなディレクトリ構造だと良いんだろうなーということを考えた結果を書きとめようと思います。Angular Best Practice for App Structureっぽい感じです。

ディレクトリ構造

├── app
│   ├── components
│   │   ├── component1
│   │   │   ├── component1.html
│   │   │   ├── component1.js
│   │   │   └── component1.css
│   │   ├── component2
│   │   │   ├── component2.html
│   │   │   ├── component2.js
│   │   │   └── component2.css
│   │   ├── component3
│   │   │   ...
│   │   ...
│   ├── filters
│   │   └── filter1.js
│   ├── directives
│   │   └── directive1.js
│   ├── plugins
│   │   └── plugin1.js
│   ├── images
│   │   ...
│   ├── index.html
│   ├── main.js
│   └── main.css
├── gulpfile.js
├── package.json
├── webpack.conf.js
...

コンポーネントごとにディレクトリを分け、コンポーネントを構成する html, css, js ファイルを同一のディレクトリ内に入れるのが良いと思います。html や css ファイルも一緒にすることで、関連するファイルを探すのが楽になります。コンポーネントを取り除くときや、リファクタリングする時も、一つにまとまってるので楽です。

vueifyvue-loader によってコンポーネントを一つの .vue ファイルで構成するというやり方もありますが、個人的には分割するほうが良いのではないかなーと思います。まず、.vue ファイル一つだとコードが長くなると見通すのがつらいです。vueify や vue-loader の例を見るとすっきりとして見やすいなーと思いますが、実際のコンポーネントはもっと量が多く、スクロールで行ったり来たりするのが結構つらいです。また、エディタが対応してないのがつらいというのもあります。html として扱えばシンタックスハイライトはしてくれますが、各種 Lint 系ツールが動かなかったり、動いても不具合があったりしてつらかったです。

app/ 直下の main.js には各種プラグインの読み込みや、設定、ルーティングなどを書いてます。main.js をエントリポイントにして、ここから各種コンポーネントなどを読み込んでいく形になります。また、main.css にはコンポーネントには入らない、リセット系のスタイルなどを定義するようにします。

また、filter や directive、plugin などは components と同じ階層にディレクトリを作って、それぞれ分けています。この辺りはまだ全然実装してないので、コンポーネントと合わせれば良いのではないかなーという程度の考えです。

具体的にどう実装するのか

ディレクトリ構造を決めた後は、実際にその構造で動くように実装をしました。具体的には、webpack を用いてコンポーネント間の依存関係を解決し、各コンポーネントの css, js も読みこむようにしました。ここでは、webpack の具体的な設定を説明するのではなく、最終的にどのように書けるようにしたかを書きます。

// component1.js の例
require('./component1.css');

module.exports = {
  template: require('./component1.html'),

  ...

  components: {
    component2: require('../component2/component2')
  },
  filters: {
    filter1: require('../../filters/filter1')
  },
  ...
};

それぞれの js ファイルはコンポーネントのオプションオブジェクトを export するように書きます。Vue.component() メソッドは使用しません。filter や directive なども同様です。export させることで、コンポーネント間の依存関係を webpack に解決させてます。また、そういった書き方をしているので、各コンポーネントのオプションオブジェクトの中で、依存するコンポーネントなどを指定する必要があります。コンポーネントごとに依存関係を指定するのは面倒ですが、ソースコード内に依存関係が明示されるのは良いのではないかと考えています。

html, css ファイルは、そのコンポーネントの js ファイルの中で読み込ませます。webpack の html-loader を使えば html を文字列として取得し、template に渡すことができます。また、style-loader を使えば、読み込んだ css を style 要素としてページに追加することができます。html や css は普通に書けば良いです。

もうちょっと何とかしたい点

以下の点をもうちょっと何とかしたいです。

  • あるコンポーネントから別のコンポーネントや、フィルター、ディレクティブを参照する時の表現が冗長
  • CSS Module に対応したい
  • いろんな部分で使い回すようなモジュールはどういう扱いにするのか?

例えば上の例だと、component2 を参照する時は component2 を二回書かないとダメですし、filter1 に関しては ../../ がつらい感じです。前者についてはファイル名をすべて index.(ext) にすると、一回の記述ですみます。しかし、すべて index.(ext) にしてしまうと、エディタで開いた時がつらいです。後者については、webpack の resolve.root の設定に app/ ディレクトリを入れると省略はできますが、ライブラリ系以外で resolve.root 使うのはどうなんだろうなーという感じです。

また、css に関して、コンポーネントごとに分けてはいますが、普通に衝突します。css-loader は CSS Module にも対応しているようなので、試してみたいです。

いろんな部分で使い回すようなモジュールに関しては、とりあえず Vue.js のプラグインとして書いてみていますが、普通に Vue.js とは独立した書き方で書いて、それを import させても良さそうな気がします。

最後に

Web ページのディレクトリ構造といえばファイルの種類ごとに分けるのが一般的だったと思うのですが、各種フレームワークとか webpack とかを使ってコンポーネント化を意識すると、ファイルの種類ごとに分けると依存関係がわからなくなってつらくなることが多いように感じます。初めは少し抵抗を覚えますが、コンポーネントごとに分けるのも良いのではないかと考えます。

今回のディレクトリ構造を中規模向けとしたのは、おそらく大規模になってくるとコンポーネントの数が多すぎて、うまく目的のものを見つけられないということが起こると考えたからです。大規模な Web アプリの場合は、ページごとにさらにディレクトリを分けるなどする必要があるんじゃないかなと思います。

box-sizing: border-box; を指定している時の jQuery UI Resizable の調整方法

CSS の幅や高さ指定を直感的なものにしてくれる border-box というものがありますが、これを使うと jQuery UI の Resizable の挙動がおかしくなります。この問題は短くて単純なコードで解決することができます。

通常、CSS の width や height は HTML 要素のコンテンツのサイズを指定します。つまり、border や padding の値は width や height には含まれません。例えば、width が 100px で、左右の padding が 20 px の HTML 要素を表示してみると、見た目上の幅は 140px となるわけです。これは CSS を書いてると結構気持ち悪く感じることがあると思います(他の要素との組み合わせを考える時にめんどくさかったり)。

上記の問題(?)はCSS で、box-sizing: border-box; を指定することにより解決することができます。つまり、width や height の値を padding や border も含んだものとして指定できるようになります。先の例の width が 100px で、左右の padding が 20px の HTML 要素を考えると、見た目の幅が 100px となり、要素内のコンテンツの幅が 60px になります。

しかし、box-sizing を border-box にすると、jQuery UI の Resizable の挙動が気持ち悪くなります。具体的には、HTML 要素のサイズを変更するためにドラッグを開始すると、padding や border の値だけ大きさがずれます。このせいで、ドラッグ中のマウスポインタの座標と、HTML 要素の端が一致しないのですごく気持ち悪いです。

Resizable の border-box 問題に関しては、jQuery 側ではまだ対応されていないみたいなので、自分で対応してみました。以下がそのコードです。jQuery UI のコードを読み解くのは嫌だったので、普通な感じの解法です。$el が border-box と resizable の両方を適用している HTML 要素の jQuery オブジェクトです。

$el.on("resize", function(ev, ui) {
    // box-sizing: border-box; に対応
    ui.element.height(ui.size.height);
    ui.element.width(ui.size.width);
});

HTML 要素がリサイズされた時に発生するイベント “resize” に対してイベントハンドラを割り当て、この中で width や height の値を修正しています。”resize” イベントのイベントハンドラは第2引数に、ui オブジェクトが与えられます。ui.element はリサイズ対象の HTML 要素の jQuery オブジェクトで、ui.size はリサイズ後の HTML 要素のサイズを示しています。ui.size のそれぞれの値を ui.element の width、height 関数に与えるだけで border-box に対応することができます。これは、ui.size の値が padding や border の値を考慮していない値になっており、さらに、jQuery の width、height 関数は同様に padding や border の値を含まないのサイズを指定するようになっているためです(border-box っぽく指定したい時は css 関数で指定できます)。

border-box と Resizable を併用することで発生するサイズの問題は “resize” イベントのイベントハンドラで HTML 要素の正しいサイズを設定してやると解決できました。偶然だと思いますが、結構短いコードで解決できたのが面白いです。初めに真面目に padding と border を取得していたのは何だったのかと。