27
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Polymer + Browserify + Gulpでユニットテストまでやってみた。

Posted at

気づいたら八月になってました。私は夏が嫌い(主に暑いのと暑いのと暑いので)なので、疾く早くこの季節が過ぎ去ってくれることを期待して耐えます。最近は九月になっても暑いのでもう十月まで夏でいいんじゃないかな、と思っています。
どうでもいい話でした。

Polymerを利用して、Polymer Elementを作成していると、ふとテストってどうやるねん?という疑念がわき上がりました。ちょっと調べると、Core Elementに対するテストが作成されていました。
https://github.com/Polymer/core-tests

内容をみてみると、Mocha + Chaiで作成されており、各Elementに対するテストがhtml内に書かれており、それをiframe上に読み込む、という手法をとっているようでした。

実際、私もRequire.js + Polymerという環境でテストを作ってみたことがありますが、Polymerは複数回読み出されることは考慮されていないので、polymerの準備が完了したタイミングで発行される polymer-ready イベントが一回しか発行されない、という問題があったので、これについてはiframeを使うのが最適化と思います。

Core Elementのテストは、それぞれがほぼ完全に分割されたPolymer Elementで、また他のJavaScriptライブラリに一切依存していないため、単純なHTMLだけでできる、という側面があると思います。

あまり想像できていないのですが、例えばあるプロジェクトで画面要素を作ったとしたとき、その要素に対するモデルやロジックが、その要素だけで閉じる、ということはまずおこらないと思います。とすれば、何らかの形で外部ライブラリを利用できるような仕組みを用意する必要があります。

PolymerとRequire.js

調べるとそれなりに出てきます(ほとんど英語圏の話題ですが)が、正直実際に使ってみた感じは、 相性が悪い という感じです。
どちらも非同期でのソース読み込みを行う機構なので、順序の制御が大変なのと、いざテストをしようとしたときに、様々な小細工が必要になります。それだけであればまだいいのですが、結局Require.jsで読み込むテストとPolymer Elementの間でイベントの同期をとるのが非常に面倒でした。

なので、個人的にはこの組み合わせはもう使う気はしません。

となると、残るのはBrowserifyになります。

PolymerとBrowserify、そしてVulcanize

Browserifyは、かなり前にみたときはブラウザAPIの模倣?みたいな感じだった気がしましたが、今はブラウザで実行するJavaScriptについてもnode.jsの仕組みに取り込むことができる、みたいな感じになっているようです。

この辺りについては、以下のサイトを参考にしました。

これで、Polymer Elementで読み込むJavaScriptをBrowserifyしたものにすることで、外部ライブラリの問題については基本的に解決できるはずです。
ですが、Polymer(というかWeb Component)について、一つ仕組み上の問題が提起されていました。

複数のWeb Componentは、当然ながらそれぞれがimportで読み込まれます。読み込まれたWeb Componentがまた他のWeb Componentをimportして・・・という形で、一つのWeb Componentをimportしたつもりが、依存を含めてimportすることで、大量のコネクションが必要になる、という点です。また、速度面でも不利になります。

これを解決するため、Polymerプロジェクトは vulcanize というツールを提供しています。
vulcanizeは、簡単にいえばPolymer Elementの結合用ツールです。複数のPolymer Elementを、そのPolymer Elementの依存も含めてすべて結合した、単一のPolymer Elementを作ることができます。

これを利用した場合、共通するPolymer Elementに対するキャッシュが効かない、サイズが基本的にかなり大きくなる、などの問題はありますが、サイズについてはgzip転送することでほとんど解決でき(るはず)、キャッシュについては・・・とりあえずあきらめるということで(ぉ

ただ、内部だけで利用するようなPolymer Elementについては、これでまとめて読み込んでしまう、というのがおそらく一番手っ取り早いと思いますし、HTMLに埋め込まれたJavaScriptを自動的に外部ファイルにする、といったことまで行えるため、Browserifyすることもわりと簡単です。

というわけで、実際にやってみんとわからんべ、ということで非常に簡単なサンプルを作成してみました。

gulpを選んだ理由

gulpを選んだ理由は、単純に使ってみたかったことと、Browserify自体がgulpと親和性が高い、ということからでした。

さて、実際のサンプルはこちらになります。正直リポジトリの名前間違ったとしか思えませんが・・・。

このリポジトリは、

  • Bowerで画面側の依存(主にPolymer)を管理
  • npmでいろいろインストール
  • gulp buildでビルド。ファイルはすべてdest以下に。
  • gulp watchで、Componentも含めてすべてBrowserify化。

という感じになっています。実際には、これにさらにLESSやSass、画像についてのなんやらまで含まれるのでしょうが、今回はあくまでPolymerとBrowserifyを組み合わせるとしたらどうする?という視点でやっているのでその辺は省いています。

なお、gulpとbrowserifyの組み合わせについては、

https://github.com/substack/stream-handbook
http://viget.com/extend/gulp-browserify-starter-faq

このへんを盛大にパクリ参考にさせていただきました。

PolymerをBrowserifyする際の方針

基本的には上述したvulcanizeすることを前提にしています。このサンプルリポジトリでは、プロジェクト内である要素だったりページをComponentで作る(それが現実的かどうかはさておいて)、ということにしてあるので、基本的にはVulcanizeします。

Vulcanizeすると、例えば以下のようなディレクトリ構成の場合、


- assets
  └ components
    └ x-test
      └ x-test.html

このx-testに対してvulcanizeすると、次のようになります。


- assets
  └ components
    ├ x-test
      └ x-test.html
    ├ x-test.html   // これがvulcanizeされた要素
    └ x-test.js     // 分離した場合このファイルにJavaScriptがまとめられる

このため、vulcanizeする先は別ディレクトリにしておいた方が得策です。サンプルリポジトリでは、次のようにしてBrowserifyしたものを作成しています。

  1. assets/components以下に各コンポーネントを作成
  2. src/components以下にvulcanizeされた各コンポーネントを追加
  3. dest/componentsにbrowserifiyされたJavaScriptと、それに対応するHTMLを生成

browserifyされたものの出力先がdestってどうなの、とかあるとは思いますがとりあえずおいておいて。。。

ただし、この方針で作ったサンプルリポジトリは次のように問題もあります。

  • vulcanizeする要素を、すべての要素にしてしまっているため、Browserify時に相応の時間がかかるようになる
  • vulcanizeしたファイルに対してBrowserifyすると、おそらく異常に大きくなるケースがいっぱい出てくる。

Polymer Elementでは、そもそもjQueryの必要性自体が乏しいので、jQueryについては入れる必要自体そんなない、と思っていますが、npmでインストールしたライブラリを利用する、などがあると結局でかくなるので、これはあまり解決できなさそう・・・とは思ってます。

BrowserifyとPolymerとテスト

ここでのテストはユニットテストです。当然、Polymer Elementは単独で完結できるケースも多いと思うので、当然ながらテストも書きやすいんじゃないか、と思うのですが、実際にやってみるとなかなか難しいです。

サンプルリポジトリでは、core-testsで利用されているツールをほとんどそのまま利用しています。ランナーとしてtestemを利用しているくらいです。

ただし、Polymerに対してBrowserifyを利用し、require('モジュール')という文がPolymer ElementのJavaScript内のあると、当然ながらここで落ちてしまうのと、テストケース内で他のライブラリを同様に利用するケースで落ちてしまうため、テストケースについても必然的にBrowserifyする必要があります。

まぁ書いてみたのですが・・・各テストケースごとにHTMLを作成するのはコストが結構かかるので、実際には一つのHTMLの中で複数のテストを作成する、という感じになるのですが、今の仕組みだとその内部でエラーになると気づくことができないので、このへんが修正するポイントかなぁ、と思います

複数ファイルをBrowserifyする場合

今回、いろいろやってみた中で一番?となったのは、複数あるファイルにたいして個別にBrowserifyする、というのをどう書こうか?という点です。

Browserifyのサンプルとかをみてると、たいていがエントリーポイントのapp.jsとかを単独で指定している、というものばかりでした。今回はそれだとまったくもって動作しないので、ちょっとうなりながら以下のような関数を用意して、それを内部で利用することにしました。

var gutil = require('gulp-util');
var browserify = require('browserify');
var gulp = require('gulp');
var source = require('vinyl-source-stream');
var glob = require('glob');
var path = require('path');
var fs = require('fs');
var remapify = require('remapify');

// fileに対してdestを対象にして出力する。
module.exports = function(file, options) {
  var dest = (options && options.dest) || './dest';
  var suffix = (options && options.suffix) || '';
  var b = browserify(file);

  var realpath = fs.realpathSync(file);
  var dirname = path.dirname(realpath).replace(process.cwd(), '').split(path.sep);
  dirname = dirname.slice(2, dirname.length);
  var fname = path.basename(file, '.js');
  var re = new RegExp('.*' + suffix + '.*');
  if (suffix && !re.test(fname)) {
    fname += suffix;
  }
  fname += path.extname(file);
  dirname.push(fname);

  b.plugin(remapify, [
    {
      src: './**/*.js',
      expose: 'app/',
      cwd : __dirname + '/../src'
    }
  ]);

  b.bundle()
    .on('error', function (e) {
      gutil.log('Browserify Error', e);
    })
    .pipe(source(path.join.apply(path, dirname)))
    .pipe(gulp.dest(dest));
};

browserifyとremapifyに対して基本的な設定をおこなっています。あと、渡されたファイル名に対して、適切になるように無理矢理調整しています。

これを利用して、browserifyのタスクはこんな感じになりました。

var gulp = require('gulp');
var glob = require('glob');
var util = require('../util');

gulp.task('browserify', function() {

  glob('./src/**/*.js', function(err, matches) {
    matches.forEach(function(file) {
      util(file);
    });
  });

  glob('./test/specs/**/*spec.js', function(err, matches) {
    matches.forEach(function(file) {
      util(file, {dest : './test_browserified'});
    });
  });
});

今回の工夫といえるのはこれくらいかと思います。

Polymer + Browserify > Polymer + Require.js

今回試してみた結果、PolymerとRequire.jsという組み合わせよりは、Polymer + Browserifyという組み合わせの方が現実味があるなぁ、と感じました。

ただ、現状Polymer(というかWeb Component)を利用できる環境というのが非常に制限されているため、こうやって仕組みを作ってもほとんど使う場所がない、というのも問題になります。
Platform.jsがかなりの部分にたいしてPolyfillを作ってくれているとはいえ、やはりシステム側で用意されているかどうかはかなり大きいです。
今時点では、Polymerを使えるのは、社内システムとかアクセスする人とかが制限される環境下で、ブラウザまでを制限している場合、というわりかしレアな場所じゃないとできないかなぁ、と思ってます。

現在Polymerを利用していて、実際にこれを利用するときに何が障害になるのか、と考えると、やっぱりライブラリや関連ファイルをどう利用するか、というノウハウやツール、仕組みがあるかどうかが障害なんじゃないかなぁ、と思います。

ただ、Web Componentの中で一番重要な技術はShadow DOMだと思っているので、ライブラリについては、もうJavaScriptの仕組み上わりとどうしようもない部分もあるので、その辺は今回みたいにツールで解決することもできると思います。

まず普及も何もChorme以外ではPolymer使わないとどうしようもない、という点がなんとかならないと、ですかね。

とりあえず今回はこんな感じです。あ、gulpは非常に軽かったので大変よろしゅうございました。プロジェクトでも導入しようかどうかちょっと迷いますね。

27
27
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?