$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

TypeScriptでのフロントエンド開発環境作成総まとめ

これまで自分のブログで、TypeScriptを使ったフロントエンド開発環境についてブログをいくつか書いてきた。ひとまずこの辺りで、TypeScriptでフロントエンドを開発するための最低限の環境を構築できるようになったので、総まとめとしてブログエントリを書いておく。

今回のサンプルコードは https://github.com/shibayu36/typescript-project-sample/tree/4653cd002eef3ee1946a2ca1da344e0076b2844f に置いたので参考に。

TypeScriptを使ったフロントエンド開発に最低限何が必要か

TypeScriptを使ってフロントエンドをスムーズに開発をするためには、以下の様な環境を最初に作っておくと良いと感じている。

  • 依存ライブラリの管理 & 型定義の管理
  • TypeScriptをビルドし、ブラウザ向けのJSを作る環境
  • ユニットテスト環境

既に昔書いたエントリで触れられていることではあるが、それぞれ軽くまとめていく。

今回の前提とディレクトリ構成

今回はTypeScript等で実装したものをビルドし、1ファイルに結合したものをJSとして配信するという前提の環境を作る。

理解しやすくするために、先に今回の構成でどのようなディレクトリ構成になるか書いておく。

.
├── gulpfile.js   # ビルドツール用設定
├── karma.conf.js # テストランナー用設定
├── node_modules  # 依存モジュールが入っているディレクトリ
├── package.json  # 依存モジュール用設定
├── src           # src以下に自分で実装とテストを書く
│   └── ts
│       ├── app.ts
│       ├── module1.ts
│       ├── module2.ts
│       └── test # ユニットテスト置き場
│           ├── module1.ts
│           └── module2.ts
├── static # static以下を配信(本当はnginxとかで配信するが、今回はそこまではしない)
│   ├── html
│   │   └── index.html
│   └── js
│       └── app.js   # ビルド後のJS
├── tsconfig.json     # TypeScriptのコンパイルオプションなど
├── typings           # 型定義ファイルの置き場
│   ├── browser
│   │   └── ...
│   └── browser.d.ts # TypeScriptファイルでreferenceするための型定義ファイル
└── typings.json      # 型定義ツール用設定

依存ライブラリの管理 & 型定義の管理

結論としては、

  • 依存ライブラリの管理に npmを利用
  • 型定義ファイルの管理に typings

を使う。


全てのものを自分で実装したくはないので、依存ライブラリの追加・管理は必須である。昔はブラウザで利用したいライブラリ(例: jQuery等)は、scriptタグを使って読み込み、その上で自分で実装することが多かった。しかし最近はnpmモジュールとしていろいろなライブラリが公開されていて、またnpmを使えば依存管理もできる。以前はbowerというツールもあったが、npmで最近は事足りている。これらのことから、依存ライブラリの管理はnpmで行う。

しかしnpmに公開されているのはJSで書かれたファイルなので、TypeScriptでそのまま使うことは出来ず、そのライブラリ用の型定義ファイルを用意しなければならない。そこで依存ライブラリの管理ツールに加えて型定義ファイルの管理ツールも使う必要がある。これも管理ツールはいろいろあるが、最近はtsdの後継であるtypingsを利用するのが良い。


npmとtypingsを使えば依存の追加は簡単。例えばjQuery をインストールしたければ以下のようにすれば良い。

$ npm install jquery --save-dev
$ $(npm bin)/typings install jquery --ambient --save

npmのコマンドにより、package.jsonに依存が追加され、node_modules/以下にjQueryがインストールされる。typingsのコマンドによりtypings/以下にjQueryの型定義ファイルがインストールされる。あとはTypeScriptで実装をするファイルでtypingsで管理された型定義ファイルにreferenceして、jQueryをimportするだけである。

src/ts/app.ts

/// <reference path="../../typings/browser.d.ts" />
import * as $ from "jquery";
$().ready(() => { ... }


依存ライブラリの管理には bower というものもあるし、型定義ファイルの管理にはdtsm というのもある。この辺を使ってもやりたいことはできるのでお好みで使ったら良いとは思う。


詳しくは以下の記事にまとめてあるので参考にしてほしい。

blog.shibayu36.org

TypeScriptをビルドし、ブラウザ向けのJSを作る環境

TypeScriptはそのままではブラウザで解釈できないので、何らかの形でブラウザに解釈できる形式にコンパイルしておく必要がある。この時ブラウザ向けに配信するために自分がビルド時に最低限やっておきたいことは以下のことである。

  • TypeScriptをJSにコンパイルする
  • 依存を解決して配信用に一つのファイルにまとめておく
  • TypeScriptファイルに書かれたコメントくらいは消しておきたい
    • ただしnpmに公開されているモジュールのライセンスコメントは残しておきたい

これらを満たすために、自分はgulp + browserify + tsify + licensify + gulp-uglifyを利用した。

  • ビルドツールとして gulp を利用
  • TypeScriptのコンパイル + ブラウザ用に連結するためにbrowserify + tsify を利用
  • モジュールのライセンスコメントを残すために licensify を利用
  • 自分の書いたコメントを消すためにgulp-uglify を利用

これらのツールを使って上記のやっておきたいことをするためには以下のようなgulpfile.jsを書いておくだけである。

gulpfile.js

var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var uglify = require('gulp-uglify');

gulp.task('build-ts', function () {
    return browserify({
        entries: './src/ts/app.ts'
    }).plugin('tsify')
        .plugin('licensify')
        .bundle()
        .pipe(source('app.js'))
        .pipe(buffer())
        .pipe(uglify({
            preserveComments: 'license'
        }))
        .pipe(gulp.dest('./static/js'));
});

gulp.task('watch', function () {
    gulp.watch('src/ts/**/*.ts', ['build-ts']);
});

この設定だけで、$(npm bin)/gulp build-ts をすれば、src/ts/app.tsをエントリポイントとして、上記やりたいことを全部行ったファイルをstatic/js/app.jsに出力してくれる*1。HTMLファイルではこれをscriptタグで読み込むだけで良い。

<script src="../js/app.js"></script>

また、TypeScriptのファイル変更時に自動でコンパイルしたいなら、$(npm bin)/gulp watchを起動しておけば良い。


使っているツールについては正直なんでも良い。ビルドツールはtscとnpmがあったら十分とか、ブラウザ向けコンパイルはwebpackを使いたいとか、そういう意見も無限にある。まあ他に同じようなことができる組み合わせはいくらでもあると思うし、自分の好きなようにやったら良いと思う。ただし、シンプルさはできる限り追求したほうが良さそう。今回の環境は非常にシンプルとはいえないが、自分の思う利便性とシンプルさの妥協点を探ってこんな感じにしている。もちろん今後のデファクトに従って変わるかもしれない。


今回の説明ではTypeScriptのコンパイルオプションや各ツールの説明に触れていないし、詳しいところは以下の記事に書いてあるので参考にどうぞ。

blog.shibayu36.org
blog.shibayu36.org

ユニットテスト環境

昨今プログラムを書くにあたって、ユニットテストを行う環境を作っておくのは必須だと感じている。もちろんテスト自体もTypeScriptで書いておきたい。

これを行うためには、今のところ、テストランナー・テストフレームワークアサーションライブラリを組み合わせて行うようにする。

自分は以下の様なツールを利用して、このような環境を作った。

これらも別のツールに置き換え可能である。karmaは使わずにmochaのテストランナーの機能を使うだけでも良いし、mochaを使わずにjasmineを使う、chaiを使わずにassertやpower-assertを使うなど。なので、これは単なる参考例であり、やりたいことに準じて自分が好きなツールを使えば良い。


これらを使ってどうやってユニットテスト環境を作るかは、以下の記事にまとめてあるので参考にして欲しい。

blog.shibayu36.org

あとは自分で実装する

ここまで環境を整えられれば、あとはテストを書きながら実装を書いていくだけである。僕だったら

  • src/ts/以下にうまくクラス分割などをしながらファイルを増やしていく
  • それぞれの実装に対応するファイルをsrc/ts/test/以下に作っていく
  • それをsrc/ts/app.tsにimportしながら大本の実装を作る

というような手順でフロントエンドを開発していきそう。

まとめ

これまで自分が書いた記事の総まとめとして、TypeScriptでフロントエンド開発を開始するために最初にやっておきたいことについて書いてみた。フロントエンド開発プロジェクトを始めるにあたって、一つの例として参考になれば嬉しい。


どうやってこれらのツールを選んだかと言えば、いろんな視点はあるけど以下のようなところを見ながら選んだ。

  • 今のデファクトは何か
    • npmのダウンロード数とか、コミュニティの活発さ具合とか
  • ググった時に出てくる情報は多いか
  • 可能な限りシンプルに、ただし利便性を損なわず


さて少し話は変わるけど、よくフロントエンドは学習コストが高い、速度が速すぎる、とかいう話がある。確かにいろいろな過去のしがらみとか、GUIアプリという本質的な難しさとか、なんかいろいろあってフロントエンドを開発するための課題は多い。課題が多くなるとそれを解決するためのツールが増えていくのは仕方ないかなと思う。このことから確かに最初の学習コストは高いかもしれない。確かにTypeScriptの環境をここまでまとめていくのはかなり大変だった。

ただし、その時にツールの覚え方を単にググッて分かった気になって終わりにすると今後の動きにはついていけないとも思う。最初に学習する時にこのツールがどういう課題を解決するのか、またこのツール以外の同じ課題を解決するための選択肢はないのか、このツールが持つ概念はなんなのか、他のツールとはどのような関係なのかなどと自分なりに調べておけば、次に別のツールが出てきたとしてもそれがどのレイヤーに位置して何のためのものなのかすんなり理解できると思う。

なので結局は「ツールの使い方」を勉強するのではなくて、「それが出てきた背景や概念」を勉強するということを繰り返すことが大事なのだなと改めて感じた。

*1:ついでにminifyもしてくれている。

Emacsで今編集しているJSのテストのみ実行する(Karma + Mocha環境の場合)

blog.shibayu36.org

前回の記事でKarma, Mocha, Chaiを使ったJSのユニットテスト環境を作ることができた。しかしテストを書き続けていると、「手元で全体のテストを再実行するのに時間がかかる」という問題が起こった。そこで今回は「今編集中のテストのみをEmacsから実行する」という作戦で問題を解決しようと考えた。

今回のサンプルコードは https://github.com/shibayu36/typescript-project-sample/tree/9e6baf1ebc9cd60083515918b23b6cb1dc24cea8 にあるので参考に。

課題

  • JSのテストをずっと書き続けていると全体のテストを実行するのに10〜数十秒程度かかるようになってくる
  • 手元でkarma startを使ってテストをしていると、ファイル変更のたびにテストを実行してくれるがkarma.conf.jsで指定されたファイル全部のテストを実行してしまう
  • 手元で開発中はまさに今編集しているテストのみ実行してくれるだけで良いのに、全体のテストを実行するのは無駄である
    • CIで全てのテストを実行しているというのは前提として

解決案

  • mochaコマンドのMochaのusageによると、grepというオプションでテストの絞り込みが出来る
  • Karmaを使っているので、直接mochaコマンドを使うことはできないが、 Karma - Configuration File を読むと、karma runの指定によってmochaに引数を渡すことが出来る
  • ならばEmacs上で今編集中のファイルから、カーソルの直近のdescribeの名前を抜き出して、それをgrepに渡すことで編集中のテストのみを実行できるのではないか

というので解決ができそうなのでやってみた。

テストファイル例

テストファイルは非常にシンプルなものを二つ作っておく。

module1という関数はmodule1という文字列を返し、 module2という関数はmodule2という文字列を返すとすると、これをテストするファイルは以下のようになる。

src/ts/test/module1.ts

/// <reference path="../../../typings/browser.d.ts" />

"use strict";

import { assert } from "chai";
import module1 from "../module1";

describe("module1 default function", () => {
    it("returns module1", () => {
        let expect: string;
        expect = 'module1';
        assert.equal(module1(), expect);
    });
});

src/ts/test/module2.ts

/// <reference path="../../../typings/browser.d.ts" />

"use strict";

import { assert } from "chai";
import module2 from "../module2";

describe("module2 default function", () => {
    it("returns module2", () => {
        let expect: string;
        expect = 'module2';
        assert.equal(module2(), expect);
    });
});

karma runを使って一部テストのみを実行する

上記のテスト例で、一部のテストのみをKarma + Mochaの環境で実行してみる。

mochaコマンドのMochaのusageによると、grepというオプションでpatternを指定すれば、patternにマッチするテストのみを実行することができる。この絞り込みはdescribeやitに適用されるみたい(ちゃんと調べてはいない)。

つまり、以下のようなコマンドを発行すれば、test/ディレクトリ以下でマッチするテストのみを実行することができる。

mocha --grep 'module1 default function' test/**/*


これができることは分かったけど、現状はkarmaをテストランナーとして利用しているため、このコマンドを直に打つことが出来ない。次にこれを解決する。

Karma - Configuration File を読むと、client.argsというものを利用すれば、利用しているテストフレームワークに引数を渡すことができるようだ。今回はテストフレームワークとしてmochaを利用しているので、これを利用してgrepオプションを渡せば良い。

このclient.argsはkarma runの引数として渡しながら実行もできる。見てみると -- 以降のオプションは全てclient.argsとしてテストフレームワークに渡されるようだ。

client.argsはkarma startには渡せないが、karma runだったら渡すことができる。karma runとは何かはkarma run --helpすれば分かるのだけど、既にstartで起動しているサーバーを利用して、テストを実行してくれるもののようだ。


いろいろ分かったところで、実際に使ってみる。まずはkarma startでサーバを起動する。

$(npm bin)/karma start
24 04 2016 13:50:53.442:INFO [framework.browserify]: registering rebuild (autoWatch=true)
24 04 2016 13:50:55.885:INFO [framework.browserify]: 208523 bytes written (0.41 seconds)
24 04 2016 13:50:55.886:INFO [framework.browserify]: bundle built
24 04 2016 13:50:55.888:WARN [karma]: No captured browser, open http://localhost:9876/
24 04 2016 13:50:55.892:INFO [karma]: Karma v0.13.22 server started at http://localhost:9876/
24 04 2016 13:50:55.902:INFO [launcher]: Starting browser Chrome
24 04 2016 13:50:57.066:INFO [Chrome 50.0.2661 (Mac OS X 10.10.5)]: Connected on socket /#9k4SvBn1G2VitcBsAAAA with id 61120412
Chrome 50.0.2661 (Mac OS X 10.10.5): Executed 2 of 2 SUCCESS (0.013 secs / 0.001 secs)

Chromeで起動して、2つのテストを実行したようだ。module1とmodule2のテストがあるので、2つのテストが実行されるのは意図通り。

続いてこのサーバを起動したままにしておいて、karma runを利用してmodule1のテストだけを実行してみる。grepを使って、"module1 default function"というのを絞り込めば良い。

$(npm bin)/karma run -- --grep 'module1 default function'
[2016-04-24 13:53:21.084] [DEBUG] config - Loading config /Users/shibayu36/development/src/github.com/shibayu36/typescript-project-sample/karma.conf.js
Chrome 50.0.2661 (Mac OS X 10.10.5): Executed 1 of 1 SUCCESS (0.005 secs / 0.001 secs)

これで先ほど起動していたサーバを使ってテストを行い、実際に1つのテストが実行されている。repoterがテスト名を出してくれないので分からないが、console.logとかを使うと実際にdescribeでmodule1 default functionと命名しているテストのみを実行してくれている。


これでkarma + mochaの環境で一部のテストのみを実行することが出来た。まとめると以下のコマンドを使うだけ。

$(npm bin)/karma start # こちらは起動しっぱなしにしておく
$(npm bin)/karma run -- --grep 'テスト名'

Emacs上でいま編集中のテストのみを実行する

ここまでで、describeのテスト名を指定すれば一部のテストを実行できるということが分かった。ならEmacs上で今編集中のファイルから、カーソルの直近のdescribeの名前を抜き出して、それをgrepに渡すことで編集中のテストのみを実行できる。

これを実行するためには以下の様な関数を定義しておけば良い。ただし、「プロジェクトがgitで管理されている」、「quickrunがemacsにインストールされている」ことが必要。

;; gitで管理されているrepositoryのトップディレクトリを探すUtility
(defun git-root-directory ()
  (cond ((git-project-p)
         (chomp
          (shell-command-to-string "git rev-parse --show-toplevel")))
        (t
         "")))

(defun run-js-mocha-describe-test ()
  (interactive)
  (let* ((topdir (git-root-directory))
         (test-grep-args nil))
    (save-excursion
      (when (or
             ;; 直近もしくは直後でdescribe('テスト名')となっている場所を探し、テスト名を抜き出す
             (re-search-backward "\\bdescribe(\s*[\"']\\(.*?\\)[\"']" nil t)
             (re-search-forward "\\bdescribe(\s*[\"']\\(.*?\\)[\"']" nil t))
        (setq test-grep-args (match-string 1))))
    (if test-grep-args
        ;; テスト名があったらquickrunを用いて
        ;; $(npm bin)/karma run -- --grep 'テスト名'
        ;; のようなコマンドを実行する
        (quickrun
         :source
         `((:command . "$(npm bin)/karma")
           (:default-directory . ,topdir)
           (:exec . (,(concat "%c run -- --grep " test-grep-args))))))))

あとはkarma startで常時テストサーバを起動させておいた上で、テストファイルを編集中にこのrun-js-mocha-describe-testを実行すれば良いだけ。これで以下のようにEmacs上で一部のテストだけ実行することができるようになった。


まとめ

今回は「手元でJSの全体のテストを実行するのに時間がかかる」という課題を、「今編集中のテストのみをEmacsから実行する」という方法で解決してみた。編集しているテストをサクッと実行できると開発速度が上がるのでおすすめ。

JSのテスト環境をいろいろ試してみてるけど、便利にするためにKarmaとMochaを組み合わせるということをしているために、逆にカスタマイズはやりづらいという面がある。この辺は今後なんかいい感じになってデファクトが便利になってくれるといいなあという気持ちになった。

最近は憂鬱だったけど去年も憂鬱だった

blog.shibayu36.org
blog.shibayu36.org

最近全体的に憂鬱で仕事もプライベートも何もやる気が起こらないと思っていたのだけど、はてなブログから1年前のブログが送られてきて去年も同じ時期に憂鬱であったことが分かった。単なる季節要因ではないかという気持になってきたけど、憂鬱なのはだるいのでなんとかしたい。

【追記】よく見たら3年前の記事だった。しかし去年も憂鬱だったことを覚えているので、少なくとも2013, 2015, 2016は4月〜5月にかけて憂鬱だったようだ。あと2014は5月が1年でブログ記事が一番少ない。