grunt-ts が .baseDir.ts ファイルを作るのを抑制する
Grunt を使っていて TypeScript のビルドを行うタスクを定義する際には grunt-ts を使うことが多いでしょう。 (grunt-typescript もあるけど。) grunt-ts で困ったことがあったので書いておきます。
outDir
オプションを使うと .baseDir.ts ファイルが作られる問題
grunt-ts を使って tsc
コマンドの --outDir
オプション指定を行うには outDir
プロパティが使えるのですが、この値を指定すると、何故か 「.baseDir.ts」 という名前のファイルがソースディレクトリの方に生成されてしまいます。
このファイルは何かというと、TypeScript 1.5 で --rootDir
オプションが導入される前に grunt-ts 独自に --rootDir
相当のことをするために導入されたものです。 grunt-ts のドキュメントを読む限り、baseDir
オプションを使っておらず、fast コンパイルが無効になっている場合には 「.baseDir.ts」 ファイルは作られないはずです。 しかしながら、grunt-ts 5.5.1 で試したところ、baseDir
オプションを使わず fast
オプションに "never"
を指定しても outDir
オプションを指定すると 「.baseDir.ts」 ファイルが作成されてしまいました。 個人的にはバグっぽいなーと思っています *1。 一応 pull request を投げておきました。
ちなみに以下のような Gruntfile.js (一部) で動作確認しました。
grunt.initConfig({ ts: { default: { src: ["src/ts/**/*.ts"], outDir: "build/ts" options: { fast: "never" } } } });
回避方法
現在 (バージョン 5.5.1) の grunt-ts に .baseDir.ts ファイルを作らせないようにする方法はいくつかあります。
outDir
オプションではなく additionalFlags
で --outDir
オプションを指定する
grunt-ts は outDir
オプションを見て 「.baseDir.ts」 ファイルを生成するかどうか決めているので、additionalFlags
の方で --outDir
を指定すると 「.baseDir.ts」 ファイルは生成されません。
grunt.initConfig({ ts: { default: { src: ["src/ts/**/*.ts"], options: { additionalFlags: "--outDir build/ts" } } } });
tsconfig.json ファイルを指定する
tsconfig.json の中で outDir
オプションを指定し、grunt-ts には tsconfig
プロパティで tsconfig.json のパスを指定教えるだけにするという方法もあります。 ただし、普通に指定するだけでは tsconfig.json を解析して grunt-ts が余計なことをするっぽい (tsconfig.json 側で outDir
オプションを指定していると、grunt-ts が .baseDir.ts ファイルを作ってしまう) ので、passThrough
オプションを有効にして、grunt-ts が余計なことをしないようにする必要があります。
grunt.initConfig({ ts: { default: { tsconfig: { tsconfig: "src/ts/tsconfig.json", // このファイルの中でコンパイル対象のファイル群や `outDir` 指定を行う passThrough: true } } } });
おわり
こんなしょうもないことをやってるのは無駄だと思うし grunt-ts はオススメしません。
関連ページ
- Stop creating .baseDir.ts file · Issue #300 · TypeStrong/grunt-ts · GitHub :
rootDir
オプションが有効なら .baseDir.ts ファイルを生成しないとかしてもいいのでは、みたいな話がされてる。 - TypeScript needs a `baseDir` option · Issue #77 · TypeStrong/grunt-ts · GitHub :
baseDir
オプションが導入されることになった経緯。
*1:ドキュメントの記述から察するに仕様ではなさそうだし、コードの実装も微妙に変なので
レスポンシブデザインのために resize イベントを使うのはやめて matchMedia メソッドを使おう
レスポンシブデザインのために CSS メディアクエリを使うことが多いと思います。 CSS 側だけで完結したらいいのですが、JavaScript 側でも画面サイズの変更を検知したかったり、画面サイズ以外のメディアクエリ相当のことをしたくなったりすることはありますよね。
画面サイズの変更自体は window
に発生する resize
イベント (window.onresize
イベントハンドラ) で検知できますが、CSS メディアクエリとこれを組み合わせてレスポンシブ対応しようとすると以下の問題がでてきます。
- ウィンドウサイズ変更時に
resize
イベントが高頻度で発生するので、resize
イベントのリスナでコストのかかる処理を行うのはよくないとされる。 (Throttling することが推奨される。)- 参考 : resize | MDN
- CSS メディアクエリと完全に対応するものではないので、CSS 側との対応を取りづらい。
上の方はまあ throttling すればいいのですが、下の問題はどうしようもないですね。
window.matchMedia
メソッドと MediaQueryList
そこで別の方法としておすすめしたいのが window.matchMedia
メソッドとその返り値の MediaQueryList
オブジェクトです。 標準化についてはまだ完了しておらず、CSSOM View Module で作業されているようです。 とはいえ最近のブラウザだとどのブラウザでも使えるみたいなので、実用的に使っていける状況になっているかと思います。 (IE 9 とか少し古めの Android のブラウザとかでは使えないので、そこら辺のサポートが必要ならまだ使えませんが><)
window.matchMedia
メソッドの引数としてメディアクエリのリスト (media query list: メディアクエリをカンマ区切りで繋げたもの) を渡すと、それらのメディアクエリのリストを表す MediaQueryList
オブジェクトが返されます。 MediaQueryList#matches
プロパティを使うことで、リスト中のメディアクエリのうち少なくとも 1 つ以上が真になっているか、もしくはすべてが偽であるかを判別できます。
また、MediaQueryList#addListenr
メソッドでイベントリスナを設定することで、matches
の値の変化を検知することができます。
// TypeScript です。 // 縦 600px 以下、または横 600px 以下の場合に matches state が真になるメディアクエリリスト。 var mql = window.matchMedia("(max-width: 600px), (max-height: 600px)"); // メディアクエリリストの matches state に応じた処理を行う関数。 function handleMediaQueryListMatchesState(matches: boolean) { if (matches) { // メディアクエリリストの matches state が真の場合の処理。 } else { // 偽の場合の処理。 } } // イベントリスナを設定して matches state の変化を検知。 mql.addListener((evt) => handleMediaQueryListMatchesState(evt.matches)); // 初期化。 handleMediaQueryListMatchesState(mql.matches);
便利ですね。 非対応ブラウザを切っていいようでしたらどんどん使っていきましょう。
歴史的経緯?
ところで MediaQueryList
は addEventListener
メソッドを持っているはずなのに、それとは別に addListener
メソッドも持っていてどうなってるんだろう、と思いますよね。 私も思いました。 どうやら昔は MediaQueryList
は独自のコールバックの仕組みを使っていて、addEventListener
を持っていなかったようです。 また、コールバックメソッドに引数として渡される値も MediaQueryListEvent
ではなく、MediaQueryList
オブジェクトがそのまま渡されていたようです。
Note: This specification initially had a custom callback mechanism with
CSSOM View Module, 4.2. The MediaQueryList InterfaceaddListener()
andremoveListener()
, and the callback was invoked with the associated media query list as argument. Now the normal event mechanism is used instead. For backwards compatibility, theaddListener()
andremoveListener()
methods are basically aliases foraddEventListener()
andremoveEventListener()
, respectively, and the change event masquerades as aMediaQueryList
.
実際に試したところ、Firefox 49.0.1 や Edge 38.14393.0.0 では古い挙動になっていました。 Chrome 53.0.2785.116m は最新の CSSOM View Module にあった実装になっていました。 Firefox や Edge の実装はまだ最新の CSSOM View Module にあった実装になっていないので、しばらくは addListener
メソッドを使っていくようにするのが良さそうです。
他の方法
CSS 側でメディアクエリを使って特定要素のプロパティを変更するなどして、JS 側からはそのプロパティを見ることでどのメディアクエリが有効になってるのか検査するのが今のところは安定、という話も。 IE 9 や古めの Android 端末をサポートするならそういう方法が良さそうです。 (ということですよね? 他の理由があるなら教えてください!)
レスポンシブデザインのために resize イベントを使うのはやめて matchMedia メソッドを使おう - ひだまりソケットは壊れない今はまだ画面サイズ検知用の要素作って、font-familyとかをメディアクエリで変更するのが一番楽かな。font-family: "sp";とか。resizeイベント側はfont名を見るだけ。ただイベントで変化を検知できるのはいいなぁ
2016/10/02 14:57
matchMedia使うよりも対象メディアクエリ内で特定要素のdisplsyプロパティを検査した方が安全で簡単な場合が多いかな。https://t.co/WZ3VhfpJfQ
— h. kitago (@hkitago) October 2, 2016
参考
- CSSOM View Module : 標準化作業中の文書。
- 本記事記述のために参考にしたバージョンは こちら。
- スクリプトからのメディアクエリの使用 - ウェブデベロッパーガイド | MDN : MDN 上の
matchMedia
メソッドとMediaQueryList
の説明。 - Responsive Web Design Basics | Web | Google Developers : レスポンシブデザインの全体的な話。 (JS 上でのメディアクエリの話はないけど参考に。)
- Media Queries : メディアクエリについての W3C 勧告。
- GitHub - paulirish/matchMedia.js: matchMedia polyfill for testing media queries in JS :
matchMedia
の polyfil。 - ブログ記事もいくつかあります。
- JavaScript でメディアクエリを行う window.matchMedia の使い方 | Mozilla Developer Street (modest)
- やるやんwindow.matchmedia - FICC Workbook
- $(window).resize()は使わず、window.matchMediaを使って、jsでのメディアクエリー的な事をしてみよう的なお話 ~ 適当な感じでプログラミングとか! : サンプルコードで、毎回
matchMedia
メソッドを呼んでるのが微妙。 - レスポンシブWeb制作時に便利なmatchMediaメソッド | while(isプログラマ) : サンプルコード中に 「イベントリスナで実行された場合は、必ずtrue」 って書かれてるけど実際はそんなことはない。
- matchMediaを使ってみる | cly7796.net :
resize
イベントのリスナの中でmatchMedia
メソッドを使っていて微妙。
gif-writer (GifWriter.js) version 0.9.3 をリリースしました
2013 年にリリースした GIF Encoder のライブラリ “GifWriter.js” を npm モジュールにして “gif-writer” としてリリースしました。 バージョンは 0.9.3 です。 *1
変更内容としては以下のとおりです。
- 各ファイルをモジュールに変更。
- パッケージに TypeScript の型定義ファイル (
.d.ts
ファイル) を含めるように。
npm モジュールに型定義を含める話
TypeScript 1.6 からモジュール名解決を npm モジュールから行う機能が入りました。
- Typings for npm packages · Microsoft/TypeScript Wiki · GitHub
- TypeScript 1.6時代の.d.ts管理について意見を述べておく - Qiita
今回のリリースでは、npm モジュールに TypeScript の型定義ファイルを含めています。 よって、TypeScript で書いているプロジェクトで “gif-writer” モジュールを使う場合、単に以下のように import
するだけで型定義が有効になります。
import * as gw from "gif-writer";
これは便利!!
*1:バージョン 0.9.0 と 0.9.1 はパッケージングに失敗したもので、0.9.2 と 0.9.3 の差分は README のみです。
ECMA-262 6th (ES 2015) のモジュールについて (入門編)
はじめに
ECMA-262 6th がリリースされて ECMAScript (JavaScript) にモジュールの機能が導入されたわけですが、実際どんなものなのかはっきりわかってなかったので ECMA-262 6th を読んでみました。 このブログ記事はそのまとめです。 実践的な話 (TypeScript でモジュールの機能を使うことについてや、既存プロジェクトへのモジュールの導入など) はまた今度書くつもりです。
この文書では歴史的背景などには触れずに坦々と ECMA-262 を読んでいくだけですので、歴史的背景などを含めた解説を読みたい方は最後に挙げている関連ページなどを読むと良いと思います。
ECMA-262 6th を読む
ECMA-262 6th (ECMAScript 2015 Language Specification) の中で、モジュールに関して述べられているところを引用しながらコメントしていきます。
Introduction
Goals for ECMAScript 2015 include providing better support for large applications, library creation, and for use of ECMAScript as a compilation target for other languages. Some of its major enhancements include modules, class declarations, lexical block scoping, iterators and generators, promises for asynchronous programming, destructuring patterns, and proper tail calls.
ECMA-262 6th (Introduction)
ES 2015 の主たる拡張の一番最初にモジュールの導入があげられています。 ES 2015 の目標に 「better sopport for large applications and library creation」 が含まれていますが、モジュールの導入はまさに大きなアプリケーションやライブラリ記述のよりよいサポートのためのものといえるでしょう。
モジュールの概要
Large ECMAScript programs are supported by modules which allow a program to be divided into multiple sequences of statements and declarations. Each module explicitly identifies declarations it uses that need to be provided by other modules and which of its declarations are available for use by other modules.
ECMA-262 6th (4.2 ECMAScript Overview)
モジュールを使うことでプログラムを複数の 「文や定義の連なり」 に分割できるので、巨大なプログラムを開発しやすくなります。 各モジュールは、別のモジュールでの宣言を明示的に import
して使用したり、別モジュールに提供するためにモジュール内での宣言を export
したりできます。 依存先モジュールが明示的に示されるのがいいところですね。
モジュールのスコープ
A module environment is a Lexical Environment that contains the bindings for the top level declarations of a Module. It also contains the bindings that are explicitly imported by the Module. The outer environment of a module environment is a global environment.
ECMA-262 6th (8.1 Lexical Environment)
モジュールの環境 (module environment) は、モジュールのトップレベルの宣言のバインディングを保持したレキシカル環境 (Lexical Environment) です。 そこには明示的にインポートされた宣言も含まれています。 んでもって、モジュール環境の外側の環境 (outer environment) はグローバル環境となってます。
つまり、モジュールでの宣言はモジュール環境に閉じていて、グローバル環境からは (直接は) モジュールでの宣言を参照することはできないわけです。 一方で、モジュール環境の外側の環境はグローバル環境なので、グローバル環境での宣言をモジュールから参照することはできます。
よって、これまでモジュールを使っていないプログラムにモジュールを導入する場合は、どこからも参照されない箇所 (すなわちプログラムのエントリポイント) から少しずつモジュールにしていくという作戦をとれます。
ES 2015 のモジュールの仕様が Node.js のモジュールの仕組みや AMD を元に作成されたであろう *1 ことを考えると、納得の仕様ですね。
モジュールのソースコードはモジュールコードとして読み込まれる
There are four types of ECMAScript code:
ECMA-262 6th (10.2 Types of Source Code)
- (略)
- Module code is source text that is code that is provided as a ModuleBody. It is the code that is directly evaluated when a module is initialized. The module code of a particular module does not include any source text that is parsed as part of a nested FunctionDeclaration, FunctionExpression, GeneratorDeclaration, GeneratorExpression, MethodDefinition, ArrowFunction, ClassDeclaration, or ClassExpression.
ソースコードの種類として、モジュールコードと呼ばれるものがあります。 ES 2015 で追加されました。 モジュールのソースコードはモジュールコードとして解釈されるわけです。
ECMA-262 6th (10.2.1 Strict Mode Code)
- Module code is always strict mode code.
モジュールコードは常に strict モードになるみたいです。 Babel などのトランスパイラを使って ES 2015 のモジュールを現在の ES 実行環境で疑似的に実現させた場合は strict モードで動かないかもしれませんが、モジュールは strict モードで動くものとしてコードを書きましょう。 (モジュールには "use strict";
を付ける、とか。)
ちなみに TypeScript の 1.8 からは、モジュールのコンパイル後の JS には "use strict";
が付けられるようになるようです。
Modules were always parsed in strict mode as per ES6, but for non-ES6 targets this was not respected in the generated code. Starting with TypeScript 1.8, emitted modules are always in strict mode.
What's new in TypeScript · Microsoft/TypeScript Wiki · GitHub
Import と Export
上でも述べた通り、モジュールはモジュール内の宣言を別のモジュールから参照できるように export できます。 逆にいえば、モジュールは、別のモジュールの宣言を import できます。 ここでは import と export の宣言に触れます。
Export の構文
クラスや変数の宣言と同時にそれを export することができます。 (下記の例以外にもありそうです。)
// VariableStatement
export var identifier1, identifier2 = ..., ...;
// Declaration
// ClassDeclaration
export class Identifier { ... }
// LexicalDeclaration
export let identifier3, identifier4 = ..., ...;
export const identifier5, identifier6 = ..., ...;
モジュール内で宣言した変数などの名前を指定して export することもできます。
export { name, localName as exportName, ... };
上の例では、モジュール内で宣言された name という名前の宣言を name という名前のまま export し、モジュール内で宣言された localName という宣言を exportName という名前で export します。
さらに、「default」 という特別な名前で export する構文も存在します。 (下記の例のすべての Identifier は省略可能です。)
// HoistableDeclaration[Default]
// FunctionDeclaration[Default]
export default function Identifier (...) { ... }
// GeneratorDeclaration[Default]
export default function * Identifier (...) { ... }
// ClassDeclaration[Default]
export default class Identifier { ... }
// AssignmentExpression[In] (下記以外もいろいろある)
export default (...) => { ... } // ArrowFunction
export default 100;
また、別のモジュールから import した宣言を export することもできます。
export * from "module-specifier";
export { name, localName as exportName, ... } from "module-specifier";
export する際の名前が重複しているとエラーになるようです。 (なので default export はモジュールにつき 1 つまで。)
Import の構文
まず、宣言を参照しない単純な import を示します。 これにより import 先のモジュールコードの処理が走ります。 (そのモジュールの宣言を参照することはできません。)
import "module-specifier";
Import 先モジュールが export している宣言の名前を指定してモジュール内で参照できるようにする import 方法は次の通りです。
import { importedBinding1, identifier as importedBinding2, ... } from "module-specifier";
上の例では、importedBinding1 という名前で export されている宣言を importedBinding1 という名前のまま import し、identifier という名前で export されている宣言を importedBinding2 という名前で import します。
Import 先モジュールが export しているすべての宣言を含むオブジェクトを得るような import 方法もあります。
import * as importedBinding from "module-specifier";
上の例では、importedBinding に Module Namespace オブジェクトが結び付けられます。 Module Namespace オブジェクトは、import 先モジュールが export している宣言をプロパティとして持つオブジェクトです。 (詳しくは後述。)
また、特別な名前 「default」 で export されている宣言を受け取る import 構文もあります。 この構文は上の 2 つの構文と組み合わせて使うことができます。
// モジュール "module-specifier" で default export された宣言を importedDefaultBinding という名前に結びつける
import importedDefaultBinding from "module-specifier";
// 下のように組み合わせて使うこともできる
import importedDefaultBinding, { importedBinding1, identifier as importedBinding2, ... } from "module-specifier";
import importedDefaultBinding, * as importedBinding from "module-specifier";
Module Namespace オブジェクト
Import 時に *
を使うと作られる Module Namespace オブジェクトについては次のように書かれています。 このオブジェクトのプロパティを通してモジュールがエクスポートしてる宣言にアクセスできるようになる、って理解でいいと思います。
A Module Namespace Object is a module namespace exotic object that provides runtime property-based access to a module’s exported bindings. There is no constructor function for Module Namespace Objects. Instead, such an object is created for each module that is imported by an ImportDeclaration that includes a NameSpaceImport (See 15.2.2).
In addition to the properties specified in 9.4.6 each Module Namespace Object has the own following properties:
ECMA-262 6th (26.3 Module Namespace Objects)
A module namespace object is an exotic object that exposes the bindings exported from an ECMAScript Module (See 15.2.3). There is a one-to-one correspondence between the String-keyed own properties of a module namespace exotic object and the binding names exported by the Module. The exported bindings include any bindings that are indirectly exported using export * export items.
ECMA-262 6th (9.4.6 Module Namespace Exotic Objects)
モジュール読み込みについて
肝心のモジュールをどう実行するのか、あるいはモジュール内でモジュールを読み込むのはどういう処理なのか、については実装依存ぽいですね。
- モジュールが実行される際の処理については 15.2.1.16.5 節 「ModuleEvaluation() Concrete Method」 に書かれています。
- 読み込むモジュールを解決する処理は 15.2.1.17 節 「Runtime Semantics: HostResolveImportedModule (referencingModule, specifier)」 ぽいです。
- 『implementation defined abstract operation』 なので実装依存ですね。
- また、そもそものプログラムのエントリポイントについては 8.5 節 「ECMAScript Initialization()」 です。
- 『In an implementation dependent manner, obtain the ECMAScript source texts』 とのことなのでこっちも実装依存。
WHATWG に Loader ってのがあるけど、これがモジュール読み込みの仕様を決めるんですかね。 『This specification describes the behavior of loading JavaScript modules from a JavaScript host environment』 ってあるのでモジュールの読み込み関係っぽいけどちゃんと読んでないのでわかんないです。
Web ブラウザでのモジュール読み込みについて (追記)
最近 HTML Standard の 8.1.3.7.1 節 「HostResolveImportedModule(referencingModule, specifier)」 でブラウザからのモジュールの参照の方法が定義されましたよ
Loader との関係はまだふわっとしていそう
というコメントを id:wakabatan から貰いました!
読んでみたところ、モジュールスクリプト自体は HTML Standard の 8.1.3.2 節 「Fetching scripts」 に書かれているようにフェッチされて、environment settings object の module map に格納されて、スクリプト実行時にはフェッチ済みのものがあればそれが使われ、なければエラー、って感じのようです。 モジュールスクリプト自体は URL で一意に識別されるみたいですね。 (module map のキーが、モジュールスクリプトの URL です。)
TypeScript のモジュールの機能を使って開発した JS を web 用にデプロイすることを考えると、モジュール探索のベースとなる URL を指定できるような仕組みだと嬉しいと思うんですが、今のところそういう感じではなさそうですね。 (まあ TypeScript → JavaScript の変換のところでなんかやればいいという話ではあります。)
小ネタ
await
はモジュールでのみ FutureReservedWord
みたいです。
await is only treated as a FutureReservedWord when Module is the goal symbol of the syntactic grammar.
ECMA-262 6th (11.6.2.2 Future Reserved Words)
おわり
- 軽くググっても ES 2015 のモジュールの import や export についてよくわかんなかったので ECMA-262 を読んでまとめました。
- 雑ですが。
- ミス等あったらご指摘ください。
- 現時点では直接 ES 2015 のモジュールを解釈できる ES 実行環境はないと思いますが、TypeScript のモジュールを使うにしろ *2、Babel などのトランスコンパイラでモジュールを使うにしろ、ES 2015 のモジュールについて理解しておくとモジュールを扱いやすいと思います。
- 作者: Aaron Frost
- 出版社/メーカー: Oreilly & Associates Inc
- 発売日: 2017/04/25
- メディア: ペーパーバック
- この商品を含むブログを見る
関連ページ
- ES6 in Practice : スライド。 ES 6 の話で、モジュールについても書かれている。
- (Babel 5における)ES6のモジュールを解説してみた - uehaj's blog : Babel で ES 2015 モジュールを使う実践的な話。
- Exploring ES6 - 16. Modules
- You Don't Know ES Modules : スライド。 ES 6 モジュールについてまとまっています。
UWP アプリ開発に TypeScript + React を導入することの検討 (Node.MSBuild.Npm の紹介)
こんにちは! 株式会社はてなにて、主に 「はてなブックマーク」 Android アプリの開発を行っている id:nobuoka です。 この記事は、「はてなデベロッパーアドベントカレンダー 2015」 の 14 日目の記事です。 昨日は id:hatz48 による 「TypeScript だけで Web アプリケーションを作る」 でした。
今日は、昨日に引き続き TypeScript の話題となります。 主にクライアントサイド (特に UWP アプリ) での TypeScript と React の組み合わせについて検討したいと思います。
(この記事の内容は、UWP アプリへの導入を目的としたときの TypeScript + React の環境の一例であり、ベストプラクティスではありません。 より良い方法などがありましたら教えてくださいませ。)
UWP アプリと TypeScript + React
UWP アプリとは 「Universal Windows Platform アプリ」 のことで、Windows 10 で導入された Windows プラットフォーム向けのアプリの一種です。 Windows 8 で導入された Windows ストアアプリや Windows Phone 8.1 向けの Windows Phone アプリの進化版という感じですね。
現在のところ株式会社はてなでは UWP アプリの開発は行っていませんが、Windows ストアアプリ 「はてなブックマーク」 を 2012 年にリリースしています。 *1
JS を快適に書く : TypeScript の導入
Windows ストアアプリや Windows Phone アプリと同様に、UWP アプリも JS + HTML + CSS で開発することができます。 Windows ストアアプリ 「はてなブックマーク」 も JS + HTML + CSS の組み合わせで書かれています。
JS は Web 開発者にとって馴染みのある言語ですので、アプリ開発に取り掛かりやすいというのは利点ですね。 一方で、素の JS には変数に型がないためリファクタリングなどがしづらいという難があります *2。 それを解決するため、いわゆる Alt JS を使用することが検討されると思いますが、UWP アプリ開発で使用するのであればまず TypeScript が候補にあがるでしょう。 UWP アプリ開発に使用される統合開発環境 (IDE) Visual Studio 2015 では標準で TypeScript がサポートされているため、UWP アプリのプロジェクトに容易に TypeScript を導入できます。 Microsoft が TypeScript のリリースを行っているあたりも TypeScript 導入にあたっての安心感につながっています。 よほど TypeScript と比べて有利な言語があればそれを使うとよいかもしれませんが、私自身は現状では TypeScript を使うのが一番だと考えています。
実際に 「はてなブックマーク」 Windows ストアアプリ *3 でも TypeScript を導入して開発しており、素の JS を使うのに比べて楽に開発を進めることができています。
View の処理を快適にするために : React の導入を検討
TypeScript を導入することで JS の変数に型付けが行われて IDE のサポートを受けやすくなり、JS の世界については快適になります。 しかしながら TypeScript を導入するだけでは view 操作 (「何らかのデータを画面に表示する」 という処理) の部分はあまり改善されません。 UWP アプリ開発のための JS のライブラリとして WinJS というものがあり、画面にデータを表示するためのテンプレート・バインディングの機能が提供されているのですが、これがなかなか素朴な仕様で、型の恩恵を受けづらいためです。
「はてなブックマーク」 Windows ストアアプリの開発でも、やはり View を扱う部分がきれいに書けず、よりよい方法を取り入れたいと考えています。
そこで、何らかのライブラリを導入することを検討しました。 候補としては、以下のものを考えました。 いずれも JS 界隈で話題になりやすいライブラリですね。
- React
- JSX 構文を使って JS 側に HTML 構造を記述する。
- TypeScript との相性が良い。
- アプリ開発をするうえで扱いやすそう。
- Polymer
- 標準の Web Components を意識して開発されており、将来性はあるように見える。
- 独自の HTML タグを定義して HTML 側で使用できるので、(React と比べて) サーバーサイドで HTML を生成するような場面で扱いやすそう。
- TypeScript との相性は不明。
- AngularJS
- 現在 Angular 2 が開発されており、バージョン 1 と 2 の互換性がなくなりそうなことから、現在 Angular を導入するのはリスキーだと考えられる。
- View 部分を手軽に扱うためだけに導入するには規模が大きいように感じる。
全部試してみてどれが良いか評価できればよかったのですが、まずは TypeScript と相性がよく、アプリ開発で扱いやすそうな React だけを試してみました。
TypeScript と React (JSX) の相性
React では JSX 構文 (JSX syntax) を用いて JS の中に DOM 構造を記述します。 そして、TypeScript では JSX 構文がサポートされています。
JSX 構文での型チェックなどもサポートされているので、React を使うことで View の処理の部分でも IDE のサポートが受けられやすくなります。 プラグインなどではなく TypeScript の処理系に JSX 構文の処理が組み込まれているので、TypeScript と JSX の相性は非常に良いといえます。
TypeScript + React の環境
さて、ここからは実際に TypeScript + React を UWP アプリ開発で用いるための環境について考えます。 UWP アプリは、ローカルファイル上の HTML + JS + CSS をブラウザ上に表示するのと似たような仕組みで動きますので、まずはローカルファイルシステム上でビルドしてブラウザ上に表示できるようにする環境を考えます。
TypeScript + React の環境を整えるためには、npm を使用するのが簡単でしょう。
- TypeScript のビルド : npm の typescript モジュール
- React のソースコード : npm の react モジュール
- React の型定義 : npm の dtsm モジュールを使用して DefinitelyTyped から react.d.ts を取得
React を使うにあたっては CommonJS モジュールシステムを使うことが推奨されていますので、TypeScript を記述する際はモジュール形式で記述し、CommonJS 形式でコンパイルするようにしました。
We recommend using React with a CommonJS module system like browserify or webpack. Use the react and react-dom npm packages.
Getting Started | React
コンパイルした JS をブラウザ上で実行するために、また、UWP アプリの JS ファイルとして使用するためには、webpack や Browerify などを使用してパックしてやる必要があります。 今回は私は webpack を使用してみました。 (webpack を選択した理由は特にありません。 同僚からは、「webpack は大艦巨砲という感じなので、Browserify で事足りるなら Browserify で良さそう」 という意見をもらいました。)
これらのツールを使用して TypeScript + React のビルドを行うようにしたサンプルプロジェクトを GitHub で公開しています。 (ここではビルドシステムとして Gradle を使用しています。)
UWP アプリのプロジェクトへの TypeScript + React 開発環境の導入 : Node.MSBuild.Npm の紹介
さて、最後に UWP アプリのプロジェクトに上述の環境を構築することを考えましょう。
上で紹介した Gradle を使ったプロジェクトでは、Gradle から Node.js と npm を使用するために Gradle Plugin for Node を使用しています。 このプラグインは、ビルド時に Node.js と npm をダウンロードしてきて、セットアップしてくれるというものです。
一般的な UWP アプリのプロジェクトのビルドシステムである MSBuild でも同様の機能を使えれば、上で紹介したプロジェクトの Gradle 部分を MSBuild に置き換えることで、UWP アプリのプロジェクトに TypeScript + React の開発環境を導入できるでしょう。
MSBuild に似たような機能を提供する NuGet パッケージがないかどうか探したのですが、NuGet パッケージの中に node.exe や npm の各種ファイルを含むようなものは見つけられたものの、ビルド時にセットアップするタイプのものはなさそうでした *4。 そこで、そのような機能を持つ Node.MSBuild.Npm という NuGet パッケージを作成し、公開しました。
使用方法は簡単です。 まず、Visual Studio で NuGet パッケージマネージャを起動して 「Node.MSBuild.Npm」 を検索してインストールしてください。 あとは package.json を記述して、ビルド時に実行したい処理を build
スクリプトとして package.json に定義するだけです。
例えば、TypeScript + React 環境を構築してビルド時に TypeScript のビルドや webpack による変換を実行するには、以下のように記述します。
{ "name": "my-app", "private": true, "devDependencies": { "react": "^0.13.3", "webpack": "^1.12.9", "typescript": "^1.7.3", "dtsm": "^0.13.0" }, "scripts": { "build": "dtsm install & tsc -p ts --outDir built\\typescript & webpack" }, // 一部略 }
ビルド時に実行したい処理の記述方法に関しては、もう少し扱いやすいようにできると良いなぁと思っています。
ぜひご利用ください。
TypeScript + React 環境に対する評価
まだ軽く試している段階ですが、現時点での評価は以下のような感じで、個人的には他のライブラリ (Polymer や AngularJS) を試すまでもなく UWP アプリ開発に導入して良さそうという気がしています。
- (良い) TypeScript が JSX をサポートしているので、JSX 構文内でも IDE のサポートを受けやすくて記述しやすい。
- これは本当に便利です。
- (良い) React 自体は大きなフレームワークではなく、導入がしやすい。
- 画面内の一部の DOM 構造の構築のみに React を使用するなど。
- (良い) React により View のコンポーネント化ができ、開発しやすくなる。
- (良い) WinJS 側で React 用のアダプタが用意されており、WinJS と一緒に使いやすい。
- (微妙) TypeScript + React の環境を作るのが少し面倒。 慣れれば問題はなさそう。
- TypeScript をモジュールとして記述するかどうかや、モジュールとして記述する場合には browserify を使うか webpack を使うか、といったことで悩みそうです。 (React は CommonJS のモジュールシステムを使用することを推奨している。)
- (不明、問題なさそう) 将来にわたってメンテナンスしやすいかどうか。
- Facebook によりリリースされているので、ライブラリのメンテナンスの心配はそれほどなさそう。
- 使い方次第ではあるが、React を使いながら徐々に別のライブラリに移行するというのも難しくはなさそう。
「はてなブックマーク」 の Windows ストアアプリを更新する際には、こういった技術を導入することでより開発しやすく、メンテナンスしやすいコードを書けるようになりそうです。 *5
おわりに
はてなでは、より開発しやすく、よりメンテナンスしやすいコードを記述していこうとするエンジニアを募集しています!
明日の 「はてなデベロッパーアドベントカレンダー 2015」 の担当は id:motemen です。 お楽しみに!