TypeScript での Chrome 拡張機能開発 Tips

はじめまして、こんにちは、水音ぴねといいます。高知で大学生してます。
今回は、私が TypeScript を使って Google Chrome 拡張機能を作る上で見つけたことを書いていきたいと思います。

少々長くなりすぎた感があります。必要なところを、つまみ食いしてください。

# 前置き
## Google Chrome 拡張機能とは ?
Google Chrome の機能を拡張するソフトウェアで、Web の技術 (JavaScript / HTML / CSS) 使って作ります。そして、一般的に Chrome 拡張機能と呼ばれるものには、大きく分けて以下の二種類があります。

1. 拡張機能
2. アプリ (chrome://apps/ に表示されるもの)

「拡張機能」は、ブラウザの UI を拡張したり、ウェブページの内容を書き換えたりするものを指します。「アプリ」は、Chrome の API を使ったアプリケーションです。アプリには、Gmail や Google Calendar のようにウェブページにリダイレクトするだけのものも多いですが、ローカルで動作する高性能なアプリも作成することができます。

「拡張機能」と「アプリ」は別の仕様ですが、共通で使える API もあり、パッケージング、公開方法は同じであるため、知識の流用は容易です。

## なぜ TypeScript で Chrome 拡張機能を作るのか
なぜならば、Chrome 拡張機能と TypeScript は非常に相性が良いからです。

Chrome 拡張機能で用いる API は、Google により厳密にドキュメント化されています (例: [extensions/webRequest](https://developer.chrome.com/extensions/webRequest))。このドキュメントは、よくある曖昧な JavaScript のリファレンスとは違い、型がきちんと明記され、どの引数が省略可能であるかまできちんと書かれています。これは、静的に型付けされた TypeScript と非常に相性が良いです。膨大な数の API と複雑なデータ構造を扱う Chrome 拡張機能開発では、この静的型付けの力が強く発揮されます。

また、Chrome 拡張機能では、ドメインの異なる複数のページ間 (例: Content scripts と Event pages) で、JSON 形式のメッセージやり取りが必要になることが多くあります。複数の拡張機能間でメッセージのやり取りを行うこともあります。このような際に、メッセージの形式を TypeScript の interface として定義することにより、統一化が図れます。

このように、Chrome 拡張機能開発に TypeScript を使うことにより、事前の型チェック、型定義による補完、データ構造の共通化などのメリットを享受できます。それにより、開発効率の向上が見込めるのではないでしょうか。

# 開発全般
## 開発の進め方
Chrome 拡張機能の開発は、普通のウェブアプリケーションの開発とだいたい同じです。違うところは、動作環境として Chrome のみを考えたらよいことと、通常のウェブとは違う権限で動作していることぐらいです。

## 開発ツール
Chrome 拡張機能は動作する OS を選ばないので、好きな環境で好きなエディタ、IDE を使うことができます。私は、Windows 環境で以下のツールを用いて開発しています。

### 例 (Windows)
私は、今のところ Visual Studio 2013 が TypeScript 開発には最強だと思ってます (WebStorm も良いですが)。

- Git & GitHub
- Grunt & CoffeeScript
- Visual Studio 2013 (with Web Essentials 2013) & TypeScript

# 型定義ファイル
## 型定義ファイルを入手するには ?
Chrome 拡張機能を TypeScript で開発するには、型定義ファイルが必要です。幸い、準公式リポジトリである [DefinitelyTyped](https://github.com/borisyankov/DefinitelyTyped) には、chrome 拡張機能用の型定義ファイル [chrome.d.ts](https://github.com/borisyankov/DefinitelyTyped/tree/master/chrome) が既に公開されていますので、こちらを利用しましょう。chrome-app.d.ts は、アプリの開発の際に利用します (その際は、chrome.d.ts も合わせて使います)。

## TSD
型定義ファイルの管理には、[TSD](https://github.com/DefinitelyTyped/tsd) を使うと便利です。TSD は、以下のようにして使用します。

```sh
$ npm install -g tsd # インストール
$ tsd version # バージョンを表示

>> tsd 0.5.7

tsd 0.5.7

$ tsd init # tsd.json を作成 (npm の package.json に相当)
$ tsd query chrome chrome-app # 検索
$ tsd query chrome chrome-app -a install -so # インストールして、tsd.json に保存 
```

# モジュールシステム & ツール
## CDN を使わない (使えない) 理由
JavaScript / CSS のライブラリのロードには、CDN (Contents Delivery Network) が広く利用されています。しかし、Chrome 拡張機能では、CSP や権限の関係上、基本的に CDN の JavaScript を組み込むことができません。使うライブラリはアーカイブに組み込んで配布するのが一般的です。

## Bower
Bower に関しては、別の方がたくさん記事にしているのでそちらを御覧ください。

- [JSライブラリの管理ツールbowerを使ってみた](http://qiita.com/grapswiz/items/57a626aece11f9714a9b)
- [Bower入門(基礎編)](http://yosuke-furukawa.hatenablog.com/entry/2013/06/01/173308)

### grunt-bower-concat
Grunt を使っている人で、Bower で入れたライブラリを拡張機能として配布する際は、[grunt-bower-concat](https://github.com/sapegin/grunt-bower-concat) を使うのがおすすめです。Bower で入れたライブラリを、JavaScript と CSS を別々に 1 つのファイルに結合することができます。

#### 設定例 (CoffeeScript)
Content scripts に Bootstrap を適応すると、ウェブページのスタイルが崩れるため、別々のファイルに統合します。私は、Grunt / gulp の設定ファイルは CoffeeScript で記述していますが、JavaScript もしくは他の altJS で記述している場合は、適宜置き換えてください。

```coffeescript
bower_concat:
    main:
        exclude: ['bootstrap']
        dest: 'bower_concat.js'
        cssDest: 'bower_concat.css'
    
    bootstrap:
        include: ['bootstrap']
        dest: 'bootstrap.js'
        cssDest: 'bootstrap.css'
```

## Browserify
こちらが本命です。Browserify については、他の記事を参照してください。

- [Browserify: それはrequire()を使うための魔法の杖](http://qiita.com/cognitom/items/4c63969b5085c90639d4)

Chrome 拡張機能の作成では、Content scripts, Event pages, Background pages など、ターゲットごとに複数の JavaScript ファイルを生成する必要があります。その際、Browserify を使ってターゲットごとに JavaScript ファイルと依存ライブラリを結合したファイルを生成する使い方がお勧めです。

![Browserify.png](https://qiita-image-store.s3.amazonaws.com/0/51394/84233b11-be4d-0177-9bb0-bf3c0f977cb6.png)

Browserify で TypeScript を使うには、TypeScript 用の transformer である [typescriptifier](https://github.com/guzart/browserify-typescriptifier) を使って変換させる必要があります。

### grunt-browserify
Grunt から Browserify を使う場合は、[grunt-browserify](https://github.com/jmreidy/grunt-browserify.git) を使いましょう。

#### 設定例 (CoffeeScript)
transformer として、TypeScript をサポートする typescriptifier の他、'use strict' を自動付与する [strictify](https://github.com/jsdf/strictify)、Bower のモジュールを読み込む [debowerify](https://github.com/eugeneware/debowerify) を使っています。

```coffeescript
pkg: grunt.file.readJSON 'package.json'
browserify:
    options:
        transform: ['typescriptifier', 'strictify', 'debowerify']
        browserifyOptions:
            extensions: ['.ts']
    main_background:
        src: 'background/js/*.ts'
        dest: 'bin/<%= pkg.name %>/background/js/background.js'
    main_content_scripts:
        src: 'content_scripts/js/*.ts'
        dest: 'bin/<%= pkg.name %>/content_scripts/js/content_scripts.js'
```

## webpack
Browserify の他にも、[webpack](http://webpack.github.io) なども同じような目的に利用できます。こちらもお勧めです。

- [全部のせRequireJSっぽいWebpackを使ってみた。](http://qiita.com/shn/items/7bd544a39ee7ac858669)

# ビルドシステム
## Grunt
私が今までに作ってきた拡張機能では、Grunt を使ってきました。最近は gulp も使い始めていますが、今はまだ Grunt の方がプラグインの数は優勢です。そのうち逆転するかもですね。

### [grunt-contrib-compress](https://github.com/gruntjs/grunt-contrib-compress)
Chrome 拡張機能を Chrome Store で公開する際は、一度 ZIP ファイルに圧縮する必要があります。そのため、ビルド時に ZIP ファイルの作成まで行うと、公開作業が非常に楽になります (後述の grunt-webstore-upload と組み合わせると、公開まで自動になります)。

### [grunt-webstore-upload](https://github.com/c301/grunt-webstore-upload)
超便利なプラグインで、Grunt から Chrome Store のファイルを更新することができます。このプラグインを使うと、コンパイル→公開までほぼ自動で行え、大変便利です。

![Grunt.png](https://qiita-image-store.s3.amazonaws.com/0/51394/a0495628-322c-33af-60e5-fda46cf33963.png)

#### 設定例 (CoffeeScript)

##### Gruntfile.coffee
```coffeescript
_ = require 'lodash'

module.exports = (grunt) ->
    # 設定を読み込む
    webstore_upload = try
        grunt.file.readJSON('.webstore_upload.json')
    catch
        {} # CI でエラーにならないようにする
    
    grunt.initConfig
        pkg: grunt.file.readJSON('package.json')
        webstore_upload: _.extend(webstore_upload,
            extensions:
                main:
                  appID: 'chrome_extension_id',
                  zip: 'bin/<%= pkg.name %>.zip' # アップロードするファイルを指定
            )
```

##### .webstore_upload.json
プライベートな情報は別ファイルに記載して、.gitignore に追加。

```json
{
  "browser_path": "Browser_Path.exe",
  "accounts": {
    "default": {
      "publish": true,
      "client_id": "client_id",
      "client_secret": "client_secret"
    }
  }
}
```

### [grunt-crx](https://github.com/oncletom/grunt-crx)
自分でパッケージングして公開する場合、このプラグインを使って .crx ファイルを生成します。多くの場合、Chrome Store に公開すると思いますが、企業内のみで使う拡張機能や、公開しない場合には使えるのでは。

## gulp
Grunt でできることは、大体 gulp でもできると思います。説明は他の詳しい人に託します。世間の流れに乗って、私も新規プロジェクトでは gulp を使うようにしています。

- [gulp.js ハンズオン](http://qiita.com/entotsu@github/items/5bfb96788ae579084a51)
- [gulp-tscとgulp-typescriptの利用方法の違いについて](http://qiita.com/yuichiroharai/items/42e559e2c15e1e5ffa39)

# ライブラリ
## DOM 系
### jQuery
定番です。Chrome 拡張機能のバックグラウンドで動作する Background pages や Event pages も ''ひとつ'' のウェブページとしての扱いなので、jQuery が正常に動作します。パーミッションで許可すれば、Background pages でのクロスドメイン Ajax が可能です。Chrome のみがターゲットとなるので、1.9.x 系ではなく、2.x 系を使いましょう。

### [Zepto.js](http://zeptojs.com)
Zepto.js は jQuery と API 互換性があり、jQuery よりもファイルサイズ、動作速度の面で有利です。しかし、jQuery のプラグインを動作させるには、少し手を加える必要があり、少々面倒です。

## MV* (MVC / MVVM) 系
### Knockout.js
Knockout.js は MVVM アーキテクチャを採用したフレームワークで、レガシーブラウザのサポートと、Microsoft 社員によって開発が行われていることが特徴です。私は、Chrome 拡張機能で採用する MV* フレームワークとして、Knockout.js をオススメします。オススメする理由は、以下の2つです。

1. TypeScript のクラスシステムとの親和性が高い<br />
(TypeScript のクラスを ViewModel の定義として利用可能)
2. CSP が有効な状態でもデータバインディングが可能<br />
(eval, new Function(...) を使わないデータバインディングが可能)

それぞれ、以下のプラグインを利用することで実現できます。

- [Knockout Secure Binding](https://github.com/brianmhunt/knockout-secure-binding)
- [Knockout ES5](https://github.com/SteveSanderson/knockout-es5)

2 について。Knockout.js はデータバインディング時の名前解決に eval 系関数を使用しています。これは、Knockout Secure Binding を使用することにより、回避できます。

#### 参考資料
- [Knockout ES5 でより自然な書き方へ](http://kojs.sukobuto.com/tips/knockout-es5)
- [KnockoutJS Advent Calendar 2014](http://qiita.com/advent-calendar/2014/knockoutjs)

### AngularJS
AngularJS も簡単に CSP に対応できるため、Chrome 拡張機能で利用するのは容易です。プロジェクトの大きさに応じて、AngularJS を選択するのもありだと思います。

- [AngularJSでChromeExtension開発をするならCSPに気をつけよう](http://blog.takuti.me/2013/11/crx-dev-using-angularjs/)

# テスト
## 良いテスト方法が見つからない
Chrome 拡張機能を作る上で一番難しいのがテストです。動作環境が特殊なため、一般的なテストランナーからではそのままテストが行えません。また、Background pages などは、テスト結果をそのまま取得するのは困難です。今までに、いろいろ試行錯誤してみたので、その結果を書きたいと思います。

良いテスト方法を知っている方が居ましたら、コメントで教えていただけたら嬉しいです。

### mocha + chai + カスタム reporter
テストランナーと組み合わせて使います。mocha でテストを実行し、Background page のテスト結果をカスタム reporter を使ってメッセージ化し Content scripts に流します。ここまでは、実際に動作したのですが、肝心のテストランナーが CI で動作させることができませんでした…。

### [Selenium](http://www.seleniumhq.org) + ChromeDriver + ChromeDriver オプション
Windows のローカル環境では正常に Chrome が起動し、拡張機能が読み込まれました。しかし、CI 環境では何故か正常に起動しませんでした (Travis-CI と AppVeyor)。

### [Karma](http://karma-runner.github.io/) + Chrome コマンドラインオプション
Windows のローカル環境では正常に Chrome が起動し、拡張機能が読み込まれました。しかし、こちらも CI 環境では何故か正常に起動しませんでした (Travis-CI と AppVeyor)。

### Node.js でテスト
実際に Chrome で動作させることを諦めて、Node.js + Mock でテストするアプローチです。こちらは、私の作ってる拡張機能で実際に使ってる所もあります。

Node.js で Chrome 拡張機能をテストする際に使えそうな物を以下に貼っておきます。確かにテストはできるのですが、やはり実環境でテストしたいという気持ちになります。

- [Sinon.js](https://github.com/cjohansen/Sinon.JS)
- [sinon-chrome](https://github.com/vitalets/sinon-chrome)

# まとめ
- TypeScript の型付け & 補完は最強
- モジュールシステムを利用して効率的に開発
- Grunt / gulp でコンパイル→公開まで自動化すると楽
- CSP (Content Security Policy) に注意

# あとがき
JavaScript 業界はいま大変熱く、めまぐるしい勢いで進化しています。私は、来年度から東京の方で働くのですが、東京では毎日にように何処かで勉強会が開催されていると聞きます。進化に置いていかれないように、みなさん先輩方と一緒に勉強していきたいと思っていますので、勉強会などありましたらぜひ誘ってください! よろしくお願いします。

# ライセンス
この記事に含まれる文章・画像・コードスニペットは、すべてパブリックドメインです。

- [Unlicense Yourself: Set Your Code Free](http://unlicense.org)