たにしきんぐダム

プログラミングやったりゲームしてます

JS知識ほぼ0からTypeScript入門してる

この記事は CAMPHOR- Advent Calendar 2016 23日目の記事です。
JS知識ほぼ0は言い過ぎかもしれないが、いわゆるモダンJSというものには縁遠く、つい最近まで jQuery をブラウザからぽちぽちダウンロードして適当に ajax とか使う人生を送ってまいりました。(当然フレームワークとか使ったことない)

まさしくこの記事みたいな状況

kikuchi1201.hateblo.jp

最近 TypeScript を書く機会があって、開発環境は用意されてるのでなんとなく書けるけど、エコシステムとかいろいろ全くわかってなくてこのまま旧石器時代然としたJavaScriptを書いていてはまずい気がすると思って勉強することにしました。

目標は TypeScript を使ってこんなんを作る、テストも書こうね(こんなのに何をテストするんだ)
この記事では主に環境構築~DOM操作のテストまでをやっていてTypeScriptの文法みたいな話はしません

https://gyazo.com/ac3e0137e71e2ef475c685185fdcd06f

最終的なプロジェクトのディレクトリ構成はこんな感じ

.
├── gulpfile.js
├── karma.conf.js
├── package.json
├── src
│   └── ts
│       ├── index.ts
│       ├── piyo.ts
│       └── test
│           ├── index.ts
│           ├── piyo.html
│           └── piyo.ts
├── static
│   ├── html
│   │   └── index.html
│   └── js
│       └── app.js
├── tsconfig.json
└── typings.json

npm とか node のバージョンを管理

バージョン切り替えの必要はあるのかわからんけど一応...
nodebrew を使って、node や npm のバージョンを切り替える。他にもいろいろ同様のツールはあるみたいですね

nodebrew ls-remote  # バージョンを眺めたり
nodebrew ls # インストールされたバージョンを見たり
nodebrew install-binary <version> # 指定バージョンのnodeをバイナリインストールしたり
nodebrew use <version> # バージョン切り替え

npm でパッケージ管理をする

bower はもう古い、フロントだろうかバックエンドだろうか npm でパッケージ管理をするらしい
最近は Yarn とか良いらしいけどよくわからないので npm で...

npm install -g <package> # グローバルな環境にパッケージをインストール
npm install <package> # だとローカル(カレントディレクトリの `node_modules` 以下?)

モジュールがダウンロードされる node_modules はgit管理から外しておく

プロジェクトごとにパッケージ管理

プロジェクトルートに package.json を配置

  • npm init で対話的に package.json がつくられる
  • npm install --save <package> だと package.jsondependencies に書き込まれ
  • npm install --save-dev <package> だと devDependencies に書き込まれる。

開発ツールとかは --save-dev でインストールしていく感じで便利
package.json に書かれたモジュールたちは npm install でインストール

TypeScript

TypeScriptはAltJSのひとつで、ES5(多分僕が昔なんとなしに書いていたもの)のsupersetとして設計されたもので、ES5+αな言語。
class とかの ES2015 な要素もあるっぽい。

TypeScriptは全くの新言語、というわけではありません。ECMAScript 5のsupersetとして設計されているため、皆さんが慣れ親しんだJavaScriptの構文に少しの+αを加えた言語がTypeScriptです。ECMAScript 5のsupersetなのですが、設計当初よりクラスなどのECMAScript 2015の要素も含んでいました。ゆくゆくはECMAScript 2015のsupersetにもなっていくことでしょう。

TypeScriptを使ってECMAScript 2015時代のJavaScriptを先取りしよう! | HTML5Experts.jp

(babel はES2015やES7をES5にコンパイルするツールなのね)

開発環境

npm install --save-dev typescript
$(npm bin)/tsc -v
Version 2.0.10

これからTypeScriptを始める人のための開発環境入門

tsconfig.json

TypeScriptをコンパイルするときにコンパイル対象のtsコードの指定とかコンパイルオブションを書く。

tsconfig.jsontsc --init で簡易的なものが得られる。上記のバージョンでは以下の tsconfig.json ができた

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es5",
        "noImplicitAny": false,
        "sourceMap": false
    }
}
  • module: commonjs: モジュールシステムとしてCommonJSを使う
  • target: es5: ES5をターゲット言語としてトランスパイル
  • noImplicitAny: 暗黙的なAny型宣言をエラーにしてくれる
  • sourceMap: 生成したjsと元のtsを結びつけてくれる君、デバッグ時に便利

TypeScript の開発環境構築と周辺ツールの紹介 | HTML5Experts.jp

外部モジュールをTypeScriptから使う

npm install した外部モジュールを使おうとするとトランスパイル時に怒られる!
型定義ファイルというものをインストールします。
nodeなどのために作られた外部モジュールの、メソッドなどの引数や返り値の型はTypeScriptからは分からないのでその型を実装とは別に記述してあげてやるもの

npm install --save-dev typings
$(npm bin)/typings init # typings.json ができる

typings はTypeScriptの型定義ファイルを管理するためのツール、昔はtsdとかいうのが使われてたらしい。
(この記事だと後に出てくる)assertionのためのchaiというモジュールを例にとってみる

npm install --save-dev chai
$(npm bin)/typings search chai
$(npm bin)/typings install --global --save-dev dt~chai
  • dt~* というので DefinitelyTyped からダウンロードするということ
  • --global (かつての --ambient) は declare hogehoge というアンビエント宣言をインストールするときはオプションにつけないといけないみたい(このへんちょっと自信がない)

型定義ファイルは typings 以下に保存される、node_modules と同じようにgit管理からは外すのが吉
今はなんとなくDefinitelyTypedから適当に型定義ファイルをインストールしてなんとなく使ってるけど、型定義ファイルについては こちらが詳しそう

実際にTypeScriptプログラムを書く

ちょっと分量多いのでgistに(別に読まなくてもOKです) index.html · GitHub

それじゃあこれらをjsにトランスパイルしてstatic/js 以下に生成物を出力しましょう

$(npm bin)/tsc --outDir static/js/ src/ts/*.ts

static/js/index.jsstatic/js/piyo.js ができましたが

// piyo.js
var Piyo = (function () {
// ...
}());
exports.Piyo = Piyo;
// index.js
var piyo_1 = require("./piyo");
// ...

このままじゃブラウザで動かせなさそうだな〜

CommonJS

export module とかでモジュールを作り、 require でモジュールをインポートする、という約束事のモジュールシステムのことらしい
こういうモジュールシステムはブラウザ上では動きません、どうすんねん

browserify

CommonJSモジュールで記述されたJSをブラウザでも動くJSに変換してくれる君のようです

$(npm bin)/browserify src/static/js/index.js -o src/static/js/app.js

何だか激しいjsファイルが生成されました、これをHTMLからinclude したらOK、ワイワイ

gulp

これまでやったこと

  • TypeScriptをCommonJSモジュールで記述されたJSに変換し
  • CommonJSモジュールで記述されたJSをbrowserifyでブラウザからも読み込めるJSに変換

こういった一連の処理は自動化したいですね... 最近は webpackとかいうのが有名らしいけど今回はgulpとかいうので、 gulpはvinylというファイルを抽象化したもののストリームを扱うタスクランナーらしい。
gulp とついでに vinyl-source-streamtsify をインストールします。

  • tsify: typescriptをコンパイルするためのbrowserifyのプラグインtsconfig.json を読み込んでくれる
  • vinyl-source-stream: ファイルストリームをvynylに変換してくれる
npm install --save-dev gulp
npm install --save-dev vinyl-source-stream
npm install --save-dev tsify

プロジェクトルートに gulpfile.js

// gulpfile.js
// gulp build-ts で、functionの処理が行われるように
var gulp = require('gulp');
var browserify = require('browserify');
var source = require('vinyl-source-stream');

gulp.task('build-ts', function () {
    return browserify({
        entries: './src/ts/index.ts'
    })
    .plugin('tsify') // tsify プラグインを有効化
    .bundle()         // コンパイルしてファイルストリームを返す
    .pipe(source('app.js'))  // ファイルストリームをvynylに変換して名前をつける
    .pipe(gulp.dest('./static/js'));  // ./static/js 以下に書き込む
});
$(npm bin)/gulp build-ts

これでブラウザでも動く static/js/app.js が作られたぞ!
これをhtmlから読み込んだらワイワイ完成

テスト

JavaScriptだってテストしたい!

環境構築

  • chai: should, expect, assertとか提供してくれる
    • 同様のツールに assert power-assert など
  • mocha: テストフレームワーク、describe, it とか実行コマンドとか
    • 同様のツールに jasmine などがある
    • 基本的にNode.jsで動くテストが書ける
  • karma: DOM API を持つブラウザを介してテストできる君
  • sinon: Mockとかさせてくれる君

今回はブラウザ上の挙動をテストしたい(ボタン押したらpiyoが追加されて欲しい)のでkarmaもインストール

npm install --save-dev chai
npm install --save-dev mocha
npm install --save-dev karma karma-browserify watchify # 前処理でtsをトランスパイル
npm install --save-dev karma-mocha
npm install --save-dev jsdom karma-jsdom-launcher # とりあえず実行環境としてjsdomを利用
npm install --save-dev karma-mocha-reporter  # optional テスト出力結果をmochaっぽい感じに
$(npm bin)/typings install --global --save-dev dt~chai
$(npm bin)/typings install --global --save-dev dt~mocha

karmaを走らせるにはkarma.conf.jsが必要、karma init で対話的に生成する

$(npm bin)/karma init

いろいろ質問に答えると karma.conf.js のスケルトンが手に入る

などを設定する

// karma.conf.js
frameworks: ['mocha', 'browserify'],

files: [
   'src/ts/test/**/*.ts'
],

preprocessors: {
  'src/ts/test/**/*.ts': ['browserify'],
},

// karma-mocha-reporter
reporters: ['mocha'],

browsers: ['jsdom'],

browserify: {
  extensions: ['.ts'],
  plugin: ['tsify']
}

テストはとりあえずこんな感じで

// src/ts/test/index.ts
import { assert } from 'chai';
import { getPiyo } from "../index";

describe('index', () => {
    describe('getPiyo', () => {
        it('should always return piyo', () => {
            const expect = 'piyo';
            assert.equal(getPiyo(), expect);
        });
    });
});

それでは実行

$(npm bin)/karma init

https://gyazo.com/ab53dae568913415f9f5d44bde262baa

うんうん、karma-reporter-mocha のおかげで小洒落たテストレポートを返してくれた

HTML断片を読んでDOM操作のテスト

piyo.ts Piyo のようなDOM操作に対するテストを書いていきます。
class Piyo に渡すべきテストのためのHTML断片を用意、それをkarmaの前処理でテストに読み込ませるために karma-html2js-preprocessor を使う(document.body.innerHTML = '<div>...</div>'でも良いけど大変)

npm install --save-dev karma-html2js-preprocessor
// karma.conf.js
files: [
  'src/ts/test/**/*.ts',
  'src/ts/test/**/*.html',
],

preprocessors: {
  'src/ts/test/**/*.ts': ['browserify'],
  'src/ts/test/**/*.html': ['html2js']
},

これで __html__['path/to/test.html']でhtmlを読み込めるように

<!--src/ts/test/piyo.html-->
<div class="container">
  <ul class="piyo-list">
    <li><a href="" class="piyo-button">Add piyo</a></li>
  </ul>
</div>
// src/ts/test/piyo.ts
import { assert } from 'chai';
import { Piyo } from '../piyo';

describe('Piyo', () => {
    beforeEach(() => {
        document.body.innerHTML =
            __html__['src/ts/test/piyo.html'];
    });

    it('can add piyo on click', () => {
        const view = new Piyo(
            <HTMLElement>document.querySelector('.container')
        );
        const list = document.querySelector('.piyo-list');
        const button = document.querySelector('.piyo-button');

        assert.equal(list.children.length, 1, '最初は1件');

        // ボタンをクリック
        const evt = document.createEvent("HTMLEvents");
        evt.initEvent('click', true, true);
        button.dispatchEvent(evt);

        assert.equal(list.children.length, 2, 'ボタンクリックでappend');
        assert.equal(list.children[1].textContent, 'piyo', 'リストの中身はpiyo');
    });
});
$(npm bin)/karma start

https://gyazo.com/bf640959d6c2d8c3949f8bbe1cd35d08

npm script からテストなどを呼び出す

これまでは $(npm bin)/karma start でテストを実行したり、$(npm bin)/gulp build-ts でTypeScriptをビルドしたりしてたけど大変
npm scripts に登録してやると npm run なんちゃら で実行できるようになるしパスが通るので $(npm bin) とか書かなくてよい
package.json に追記

"scripts": {
  "build": "gulp build-ts",
  "test": "karma start"
},

感想

JavaScript界隈、特に勉強もせず側から見てるといろんなツールの名前が死ぬほどでてきて意味不明という印象だったけれど、ちゃんとした記事とかを見て勉強してみると各ツールの責任範囲が細かく分かれていて、その範囲の中でいくつかのツールが台頭しているっていうだけで思っていたほど混沌とはしていないなと思いました(もっと異常な状況だと思ってた)
とはいえ勉強することは多いな(設計、tsの型定義ファイルなど)と思ったのでじゃんじゃん勉強していきたい

参考