読者です 読者をやめる 読者になる 読者になる

ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「えっちなのはいけないと思います」 に書いています。

Android の Java で時刻を扱う (Date、Calendar、DateFormat クラス)

Android アプリ Java

Java エンジニアの皆様は Java SE 8 で導入された Date-Time パッケージ (Date and Time API; JSR 310) を便利に使っていることと思います。 残念ながら Android プラットフォームにはそれらの API がありませんので、Android アプリ開発時に時刻を扱う場合は、古くからある API を使用することになります。

この記事では、Android アプリ開発時にお世話になる Date クラス、Calendar クラス、DateFormat クラスについて、それぞれの役割や使い方、気を付けるべきことをまとめます。

ところで Time クラスは?

Android APIs には Time クラスってやつが含まれています。 API level 22 で非推奨 (deprecated) になったのでそっとしておきましょう。 (これまでありがとうございました。)

気を付けるべきことまとめ

記事が長いので、気を付けるべきことを最初に書いておきます。

  • Date オブジェクト自体はタイムゾーンを持ちません。
    • Calendar を使って指定の Date の年月日や時、分、秒などを取得するとき、あるいはそれらの値を設定したり計算したりするときは、Calendar に適切なタイムゾーンを設定しましょう。 ロケールも必要に応じて設定しましょう。
    • DateFormat を使って Web API などに投げるための時刻の文字列を生成するときは、DateFormat に適切なタイムゾーンを設定しましょう。 ロケールLocale.US が良さそうです。 *1
  • DateFormat を使って Web API などから取得した時刻の文字列を解析する際は、必要であれば適切なタイムゾーンを設定しましょう。 (解析対象の文字列にタイムゾーンが含まれているのであれば必要ない。) ロケールLocale.US が良さそうです。
  • Android *2SimpleDateFormat は、ISO 8601 や RFC 822 で定義されていて実際にしばしば使われる 「Z」 というタイムゾーン表記を解釈できない (Java SE 7 では X というフォーマット文字が導入されて解釈できるようになった) ので、気を付けましょう。
  • ユーザーに表示するための時刻の文字列を生成する際は、andorid.text.format.DateFormat オブジェクトを使うことで端末の設定を反映させると良いでしょう。

DateCalendarDateFormat、それぞれの役割

簡単にまとめると、Date クラスが 「時間軸上の特定の瞬間」 を表すもので、Calendar クラスが年月日や時、分、秒といった情報を扱うクラス、そして DateFormat が時刻を表す文字列と 「時間軸上の特定の瞬間」 の相互変換を行う、という感じです。 詳細は以下に述べます。

Date クラス

Date クラスは、時間軸上の特定の瞬間をミリ秒の精度で表現するクラスです。

純粋に 「時間軸上のある瞬間」 を表現するためのものなので、タイムゾーンを持ちませんし、自身が表現する時刻が何月何日の何時何分なのかを計算したりもしません。 時刻を表現する文字列をパースしたり、逆に時刻を表現する文字列を出力したりもしません。 (歴史的経緯によりそのような機能のメソッドDate クラスに定義されていますが、非推奨です。)

Calendar クラス

Calendar クラスは、次の 2 つの機能を提供するクラスです。

  • 「時間軸上の特定の瞬間」 と 「何月なのかや何日なのか、何時なのかといった数値情報・カレンダー情報」 の相互変換
  • 何日なのかや何時なのかといった数値情報・カレンダー情報に対する計算。 (例えば、1 週間後の日時を取得する、など。)

Calendar オブジェクトで時刻を表現させようと思えば実際上は可能ではありますが、そういう使い方には Date オブジェクトを用いるべきみたいですね。 年月日などを扱うので、このクラスを使う際は当然 タイムゾーンを気にする必要があります

GregorianCalendar クラス

Calendar クラスは抽象クラスで、変換や計算の処理は各種サブクラスが提供することになっています。 世界のほとんどの地域で使用される標準的な暦体系に対する実装として、GregorianCalendar が提供されています。

DateFormat クラス (java.text パッケージ)

「時間軸上の特定の瞬間」 を表す値から日付・時刻を表す文字列を生成したり、逆にそれらの文字列を解析する機能を持つクラスです。

年月日などを扱うので、このクラスを使う際は当然 タイムゾーンを気にする必要があります

SimpleDateFormat クラス

DateFormat のサブクラスです。 自分で日付・時刻の文字列のフォーマットを指定する場合はこのクラスを使用することになります。

DateFormat クラス (android.text.format パッケージ)

めちゃくちゃややこしいのですが、Android APIs には android.text.format.DateFormat というクラスも含まれています。 このクラス自身もフォーマット機能を持っていますし、java.text.DateFormat オブジェクトを返すファクトリメソッドも持っています。

API level 23 の実装だと、java.text.Date クラスの getDateInstance メソッドgetTimeInstance メソッドも端末の設定を見てくれるようですが、API level 10 では android.text.format.DateFormatgetDateFormat メソッドなどを使わないと端末の設定が反映されないようです。 (どこで変更されたかは調べてない。)

なので、端末の設定を反映させて日時や時刻を表示したい場合は、android.text.format.DateFormat クラスを使ってフォーマットを決める必要があるようです。

使い方

各種ユースケースにおいて、上で紹介したクラスをどのように使うかを簡単に紹介します。

Web API でやり取りする時刻の文字列と Date オブジェクトを相互変換する

Web API のレスポンスで "2016-01-01 00:00:00+0900" みたいな時刻を表す文字列を受け取ることがあると思いますが、そういった文字列を Date オブジェクトに変換するにはフォーマットを指定して SimpleDateFormat オブジェクトを作成しましょう。

java.text.DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ", Locale.US);
// タイムゾーンが含まれていないような日付文字列を解析する場合は、適したタイムゾーンを指定しましょう。
// df.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));

Date d = df.parse("2015-01-01 00:00:00+0900");

ロケールタイムゾーンの処理にも関わってるぽいです (まじか……) し、Locale.US にしておくのが良さそうです。 例えば、パターン Z を使用してタイムゾーンを解析する場合、Locale.US ならば 「EDT」 を処理できますが、Locale.ROOTLocale.JAPAN だとパースエラーになります。 (API level 23 で確認。 ロケールに関係なく RFC 822 で定義されているタイムゾーンは解析して欲しい……。 厳しい。) でも Locale.US でも 「Z」 を処理できないから、数値でなく名前で表現されるタイムゾーンの解析周りについてはそもそも信用しないのがいいかもしれないですね。

Web API などに送るための時刻文字列を生成する場合も、同様に SimpleDateFormat を使いましょう。 タイムゾーンは、受け取り側が期待するであろうものを設定しておくのが安全だと思います。

java.text.DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ssZ", Locale.US);
// タイムゾーンを出力するにしても Web API 側が期待するタイムゾーンを設定しておいた方が安全でしょう。
df.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));

String currentTime = df.format(new Date());

ユーザーに表示するために Date オブジェクトから時刻の文字列を生成する

ユーザーに表示するための時刻文字列は、ユーザーの端末のロケールタイムゾーンを使用すべきでしょう。 Java では DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault()) という感じでデフォルトロケールDateFormat を取得できますが、Android アプリの場合は、端末の設定を反映したフォーマットの DateFormat を取得するために android.text.format.DateFormat を使用するのが良さそうです。

詳細は以下のページを見てください。

(API level 23 では、DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault()) みたいな感じで取得した DateFormat オブジェクトも端末の設定を反映しているようでしたが、API level 10 だと反映してなかったので、古い端末もサポートするなら android.text.format.DateFormat を使うべきっぽいです。)

Date オブジェクトが表す日の 1 年後、を計算する

例えば今日の 1 年後を表す日を計算したいとします。 そういうときは Calendar を使います。 *3

// 操作対象の Date オブジェクト。
// 日本時間で 2016 年 2 月 29 日の 00:00:00。
Date d = new Date(145667160_0000L);

// Calendar オブジェクトを使って 1 年後を計算して Date オブジェクトを取得。
Calendar c = Calendar.getInstance(TimeZone.getTimeZone("Asia/Tokyo"), Locale.JAPAN);
c.setTime(d);
c.add(Calendar.YEAR, 1); // 年に 1 加算。
Date dateOneYearAfter = c.getTime(); // 2017 年 2 月 28 日の 00:00:00 (日本時間)。

上の例では、タイムゾーンを Asia/Tokyo に設定しています。 タイムゾーンを適切に設定しないと、渡された Date オブジェクトが表す日付が変わる可能性がありますので、適切にタイムゾーンを設定しましょう。 ロケールも週の開始曜日などに影響するので必要に応じて設定しましょう。 (デフォルトタイムゾーン・デフォルトロケールが良い場合はもちろんデフォルトの値を使えば良いです。)

おわり

時刻を扱うときはタイムゾーンに気を付けなさい」 ということはみんな耳にタコができるぐらい言われてきたと思うので大丈夫だと思いますが、タイムゾーンをちゃんと設定するように気を付けましょう。

*1:ドキュメントにも 『The main reason you'd create an instance this class directly is because you need to format/parse a specific machine-readable format, in which case you almost certainly want to explicitly ask for “US” to ensure that you get ASCII digits (rather than, say, Arabic digits).』 って書かれてる。

*2:少なくとも API level 23 までは。 それより後のバージョンは不明。

*3:単純に 365 日間に相当するミリ秒を加算する、みたいな方法だとうるう年などが考慮できないのでだめですよね。 「1 ヶ月後」 とかだと何日分加算すればいいのかわかんないし。

Java における byte 型について (あるいはバイナリデータを扱うためのキャスト)

Java

この記事は、もともと次のページに書かれていた内容に、加筆・修正を行ったものです。

はじめに

この記事では、ビット列を角括弧で囲み、[00000000] のように表現する事にします。

byte 型の表現範囲

Java のプリミティブ型 (基本データ型) の 1 つに byte 型というものがあります。 その名の通り 1 バイト (8 ビット) のサイズの整数値を表現できます。

つまり、[00000000] から [11111111] まで表現することができます。

数値でいうと 0 (0x00) から 255 (0xFF) まで表現できる、と思うところですが、実際には Javabyte 型は符号付きであり、表現できる範囲は -128 から 127 までとなっています。

The values of the integral types are integers in the following ranges:

  • For byte, from -128 to 127, inclusive
  • (略)
Chapter 4. Types, Values, and Variables

バイナリデータを扱うための byte 型のキャスト

符号なしで 0 から 255 の整数を表現したい

上述のとおり byte 型には符号が付いているのですが、バイナリデータの各バイトを 0 (0x00) から 255 (0xFF) の整数値で扱いたい場面がたまにあります。 当然 byte 型のままでは 128 以上の整数値を扱えませんので、short 型や int 型にキャストする必要があります。 逆に、shortint で表現される 0 から 255 までの整数値を byte にキャストしたい場面もあるでしょう。 (例えば InputStream#read() メソッドは、バイトの情報を 0 から 255 までの整数値として返してきます。)

byte 型からの変換

Java SE 8 からは Byte クラスに便利メソッドが用意されているので、それを使うといいでしょう。 (id:nowokay さんに教えていただきました! ありがとうございます!)

Java SE 7 以下では自分でキャストする必要があります。

キャストする場合 (Java SE 7 以下の場合)

byte から intshort へのキャストは、widening primitive conversion です。 このキャストでは、キャストによって数値の情報が失われることがありません。 (キャスト前とキャスト後で数値として同じ値が表現される。)

バイトを非負整数で表現するという目的においては、単にキャストするだけでは MSB (Most Significant Bit) が 1 の場合 (すなわち、値が負の場合) に、期待する結果にはなりません。

byte valueByte = -1; // ビット列で表すと [11111111]
short valueShort = (short) valueByte; // valueShort の値は -1
                                      // ビット列で表すと [1111111111111111]

short 型にしたときにビット列としては [0000000011111111] となって欲しいところですが、元が負値であるために上位ビットに全て 1 が入ってしまっているわけです。 byte 型をキャストして 0 から 255 の整数値の表現にするには、キャスト後に下位 8 ビット以外を 0 にすれば良いでしょう。

byte valueByte = -1; // ビット列で表すと [11111111]
short valueShort = (short) valueByte; // valueShort の値は -1
                                      // ビット列で表すと [1111111111111111]
// 下位の 8 ビットだけ残し、残りを全て 0 にする。
valueShort &= 0xFF; // 期待通りの 255 という数値になる
                    // ビット列で表すと [0000000011111111]

// キャストと下位 8 ビット以外を削る処理を 1 行で行っても良い。
valueShort = (short) (valueByte & 0xFF);

byte 型へのキャスト

一方で、intshort から byte へのキャストは、narrowing primitive conversion です。 このキャストで整数型を byte に変換する場合、単純に元の数値を表すビット列の下位 8 ビットがキャスト後の byte 値となります。

なので、0 から 255 の整数値で表現される値を byte 型にする場合は、単純にキャストしてやればよいのです。

short valueShort = 0xFF; // 数値としては 255。 ビット列で表すと [0000000011111111]
byte valueByte = (byte) short; // 数値としては -1。 ビット列で表すと [11111111]

byte 値を 16 進数表記の文字列にする

C 言語を使う人にはおなじみの printf ですが、似たものが Java にもあります。 (Java SE 5 より導入された模様。)

これを使用すれば、簡単に byte 値を 16 進数表記の文字列にできます。 標準出力などに出力して目で確認したい時にはなかなか便利です。

byte b = (byte) 0xE8; // 数値としては -24

// String#format メソッドを使用して文字列取得。
String bStr = String.format("%02X", b); // => "E8"

// PrintStream#printf メソッドを使用してそのまま出力する。
System.out.printf("%02X", b);

Java SE 1.4 以前は 「Javaのbyte型を16進数で表現」 みたいにまどろっこしいことをしないとダメだったと思います (もしかしたらもっと楽な方法はあるのかも知れませんが) けど、なかなか便利になったものです。

【v7 appcompat library を読む】 レイアウト XML のインフレート時に各種 view が compatible widget に変換される仕組み

Android アプリ ライブラリ

Android アプリ開発時にお世話になる v7 appcompat library と LayoutInflater の話です。

この記事の内容は、v7 appcompat library のバージョン 23.1.1 をもとに記述しました。

v7 appcompat library の compatible widget

v7 appcompat library には、AppCompatTextView などの compatible widget *1 がいくつか含まれています。 いくつか下に挙げてみます。

これらのクラスは、新しい API level で導入された機能の一部を古いプラットフォームにも提供してくれます。 クラスによって提供される機能は違うものの、主には widget tinting 周りの機能 (+α) が提供されると考えて良さそうです。

appcompat ライブラリによる widget tinting については下のブログエントリを参照してください。

インフレート時に自動的に変換される

各 compatible widget のドキュメントを読むと、以下のような説明が書かれています。 (以下は AppCompatTextView のもの。)

This will automatically be used when you use TextView in your layouts. You should only need to manually use this class when writing custom views.

AppCompatTextView | Android Developers

つまり、レイアウト XML 中に <TextView ...> って書いておけば、インフレート時に自動的に AppCompatTextView になる、ってことですね。 便利です。

インフレート時に自動で変換される仕組み

便利なのはいいけどどういう仕組みなのかわかっておかないと嵌ったりすることもあるので、どういう仕組みなのか調べてみました。

LayoutInflater に Factory をセットすることでインフレート時の挙動を変更できる

まずは LayoutInflater について調べてみます。 インフレート時に XML ファイル中の要素名を各 view のクラスに変換する処理は、Factory (LayoutInflater.Factory/LayoutInflater.Factory2 オブジェクト) が担っているようです。 下記メソッドを見てみると、ドキュメントにいろいろ書かれています。

AppCompatActivityLayoutInflater に Factory をセットしている

AppComaptActivity#onCreate メソッドの中を見ると、以下のように AppCompatDelegate#installViewFactory() メソッドを呼んでいます。

getDelegate().installViewFactory();

ドキュメントには 『Installs AppCompat's LayoutInflater Factory so that it can replace the framework widgets with compatible tinted versions』 と書かれていて、このメソッドLayoutInflater に独自の Factory をセットすることがわかります。 実装を追っていくと、このメソッドの中では LayoutInflaterCompat.setFactory メソッドが呼ばれていました。

まとめ

上で調べたように、AppCompatActivityLayoutInflater に独自の Factory をセットすることで、インフレート時の自動変換が実現されています。 よって、インフレーション時については以下のようにまとめられます。

  • AppCompatActivity で使う限り、LayoutInflater#from(Context) メソッドで取得できる LayoutInflater でのインフレート時に compatible widget への自動変換がはたらくと考えて良い。
  • AppCompatActivity を使わないのであれば、AppCompatDelegate#installViewFactory メソッドを使うことで使うことで同等の機能が実現される。

インフレーション時以外に何か良しなに変換してくれたりはしないので、以下のことにも気を付けましょう。 (appcompat ライブラリを使っている環境での話です。)

  • 独自 view のクラスを定義する場合は、compatible widget のクラスを拡張する。
    • 例えば TextView のサブクラス定義したい場合は、TextView ではなく AppCompatTextView のサブクラスにする。
    • そうしないと background tinting が有効にならないという問題が起こる。
  • 直接コンストラクタを呼んで view を生成したい場合は、compatible widgetコンストラクタを呼んで compatible widget を生成する。
    • 例えば、TextView オブジェクトを生成したい場合は、AppCompatTextViewコンストラクタを呼ぶ。
    • LayoutInflater でインフレートしたものは background tint を設定できるのに、直接コンストラクタを呼ぶと設定できない」 という問題を回避するため。

*1:『compatible widget』 という表現は、AppCompatDelegate#installViewFactory() メソッドのドキュメントから。

ECMA-262 6th (ES 2015) のモジュールについて (入門編)

ECMAScript JavaScript

はじめに

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) はグローバル環境となってます。

つまり、モジュールでの宣言はモジュール環境に閉じていて、グローバル環境からは (直接は) モジュールでの宣言を参照することはできないわけです。 一方で、モジュール環境の外側の環境はグローバル環境なので、グローバル環境での宣言をモジュールから参照することはできます。

よって、これまでモジュールを使っていないプログラムにモジュールを導入する場合は、どこからも参照されない箇所 (すなわちプログラムのエントリポイント) から少しずつモジュールにしていくという作戦をとれます。

https://docs.google.com/drawings/d/1oCy-Di73qDcmW6PsIlKk3OtYH1a5_2Vgvk6WhSiHBo8/pub?w=696&h=391

ES 2015 のモジュールの仕様が Node.js のモジュールの仕組みや AMD を元に作成されたであろう *1 ことを考えると、納得の仕様ですね。

モジュールのソースコードはモジュールコードとして読み込まれる

There are four types of ECMAScript 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.
ECMA-262 6th (10.2 Types of Source Code)

ソースコードの種類として、モジュールコードと呼ばれるものがあります。 ES 2015 で追加されました。 モジュールのソースコードはモジュールコードとして解釈されるわけです。

  • Module code is always strict mode code.
ECMA-262 6th (10.2.1 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)

モジュール読み込みについて

肝心のモジュールをどう実行するのか、あるいはモジュール内でモジュールを読み込むのはどういう処理なのか、については実装依存ぽいですね。

WHATWGLoader ってのがあるけど、これがモジュール読み込みの仕様を決めるんですかね。 『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 objectmodule 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 のモジュールについて理解しておくとモジュールを扱いやすいと思います。

Js.next: Ecmascript 6

Js.next: Ecmascript 6

関連ページ

*1:推測です

*2:TypeScript のモジュールは ES 2015 ベース

『Web API: The Good Parts』 を読んだ

HTTP RFC Web アプリケーション 書籍 書評

Web API: The Good Parts

Web API: The Good Parts

同僚から借りて読みました。 全体としては Web API の設計に少しでも携わる人間ならとりあえず読んでおいたらいいんじゃないかなーという感じです。 薄いし。

本書を読んだからと言って最高の Web API の設計ができるようになるとは思わないですが、Web API の設計をする際に知っておくべきことが一通りまとまっていて良い感じだと思いました。

学びメモ

知らなかったことや、なんとなく知ってたけど改めて調べたことなどまとめておきます。

RFC 5861 での Cache-Control ヘッダの拡張

RFC 5861 にて、Cache-Control ヘッダの 2 つの拡張が定義されています。

  • stale-while-revalidate ディレクティブ : プロキシサーバー上で max-age を超えてキャッシュが切れた後も、(裏側で非同期にキャッシュの検証を行いつつ) キャッシュ済みの古いレスポンスデータを返しても良い時間を指定できる。
  • stale-if-error ディレクティブ : オリジンサーバーへのリクエストに失敗する場合に、キャッシュ済みの古いレスポンスデータを返してもよい時間を指定できる。

RFC 5861 の現在の state は “Informational” らしいのでどれぐらいサポートされてるのかわからないですが、「プロキシサーバー上でキャッシュが無効になったあとも、裏でキャッシュ更新しつつユーザーからのリクエストに対してはレスポンスを返してほしい」 って状況はあるので、こういうのが既存のプロキシサーバーなどでサポートされてるとアプリケーション開発時に便利に使えそうです。

本書の 4.3.6 節 「Cache-Control ヘッダ」 に記述されています。

独自のメディアタイプを作る

メディアタイプの登録プロセスの効率と柔軟性を増やすために、サブタイプのいくつかの登録 『木』 (registration “trees”) が RFC 6838 にて定義されています。 それぞれの木には facet *1 が定義されているので、新たに作る独自のメディアタイプの用途に応じて登録木を選択し、それに応じた facet を付けることになります。 (登録木によっては登録作業なども必要。)

  • Standards Tree : IETF 標準に関連付けられるなどした、標準のメディアタイプのための木。 Facet はない。
  • Vendor Tree : 公に使用可能な製品に結びつけられたメディアタイプのための木。 Facet は “vnd.”。 (例 : “application/vnd.ms-excel”)
  • Personal or Vanity Tree : 実験的に作られたり、商業的に流通しない製品の一部だったりするメディアタイプのための木。 Facet は “prs.”。
  • Unregistered x. Tree : 私的に、ローカルな環境でのみ使われるメディアタイプのための木。 Facet は “x.”。

Web API で使用されるメディアタイプを新たに独自に定義する場合は、vendor tree か personal or vanity tree のどちらかになるでしょう

本書の 4.4.3 節 「自分でメディアタイプを定義する場合」 に書かれています。

CORS 周り

XMLHttpRequest *2 には同一生成元ポリシー (Same Origin Policy) が適用されるため、通常は生成元の異なる URL に XMLHttpRequest でリクエストを投げることができません。 異なる生成元へのリクエストが可能になるように考えられた仕様として CORS (Cross-Origin Resource Sharing) があります。

それ自体は知ってたのですが、

  • CORS でプリフライトリクエストが投げられる条件 (POST メソッドでもプリフライトリクエストするとは限らない、みたいな話を聞いたことはあったけど、詳しくは知らなかった)、とか
  • CookieAuthentication などのヘッダで認証情報をやり取りする場合は、サーバー側は Access-Control-Allow-Credentials ヘッダを返す必要があり、クライアント側は XMLHttpRequest#withCredentials というプロパティを true にしないといけない

など知らなかったので、いろいろあるなーと思いました。

ちなみに最近は CORS (や他の仕様) を置き換えるものとして Fetch Standard が作られてるみたいですね。

本書の 4.5 節 「同一生成元ポリシーとクロスオリジンリソース共有」 に書かれています。

セキュリティ周りの話

X-Content-Type-Options ヘッダ

IE (どのバージョンまで? 11 も?) には Content-Type ヘッダの値を無視してレスポンス内容のデータ形式を推定する Content Sniffing という機能があり、JSON として解釈されるべきコンテンツが HTML として解釈されて XSS 脆弱性になる、という問題があります。

IE 8 以降の場合は、サーバーがレスポンスとして次のレスポンスヘッダを返すことで Content Sniffing を無効にできます。 さらに、FirefoxChromeIE 9 以降では、このヘッダを付けることで JSON ハイジャック *3 (後述) の危険性を減らすことができるそうです。 (JSON を返すときは常に付けましょう。)

X-Content-Type-Options: nosniff

詳細は次のページに書かれています。

本書では、6.3.1 節 「XSS」 に書かれています。

JSON ハイジャック

JSON 形式のレスポンスを JavaScript としてブラウザに解釈させることで、別オリジンのサーバーから配信される JSON の内容を悪意ある第三者の web ページが読みだすことができてしまう、という脆弱性。 例えば、悪意ある第三者の web ページが Array オブジェクトのコンストラクタを書き換えたうえで、配列を返す JSON ファイルを script 要素で読み込むと、JSON の中身が書き換えられた Array オブジェクトのコンストラクタに渡されてデータが読まれる、という感じです。 (これは Firefox 2 系で発生していた脆弱性で、最近のブラウザは対策済みらしいです。)

サーバー側の対策として、以下のものがあります。

  • script 要素では読み込めないようにする : script 要素で読み込まれた場合には送られないヘッダフィールド (X-Requested-With ヘッダなど) を必須にする。
  • JSON のレスポンスをブラウザが JS として認識しないようにする : Content-Type: application/json にすることはもちろん、X-Content-Type-Options: nosniff を付ける。
  • JSON のレスポンスを JS として解釈不可能にする (あるいは JS として実行されてもデータが読まれないようにする) : レスポンスの先頭に while (true); を付けるとか。

本書では、6.3;3 節 「JSON ハイジャック」 に書かれています。

セキュリティ周りの各種ヘッダ
  • X-XSS-Protection レスポンスヘッダ : XSS を発生させそうなパターンがレスポンスに含まれている場合に、ブラウザがレスポンスをブロックする機能を有効にしたり無効にしたりできるヘッダフィールドぽい。 この機能は ChromeSafariIE (8 以降) がサポートしてるぽい?
    • ちゃんとしたドキュメントはあんまり見当たらない。
    • XSS ブロックの機能はデフォルトで有効っぽい *4 から、サーバーサイドでは基本的には付けなくてもよさそう?
    • 参考 : IE8 Security Part IV: The XSS Filter | IEBlog
  • Strict-Transport-Security レスポンスヘッダ : ブラウザからのアクセスを HTTPS に限定させる。
  • Public-Key-Pins レスポンスヘッダ : ブラウザに暗号公開鍵とドメイン (?) を結び付けさせる → ブラウザは将来に証明書が偽造された場合に検知できるようになる。

本書の 6.5 節 「セキュリティ関係の HTTP ヘッダ」 に書かれています。

ブラウザがレスポンス内容を UTF-7 と誤認することによる XSS の成立

XSS 対策として 「<」 という文字列をエスケープするなどの方法がありますが、ブラウザにレスポンス内容の文字エンコーディングUTF-7 だと誤認させることで、「<」 のエスケープをしていても 「<script>」 という文字列をそのままブラウザに食わせることができる、という攻撃があるとのことです。

UTF-7 では、「<」 という文字は ASCII の 「+ADw-」 として表現されるためです。 「+Adw-script+AD4-」 という文字列を UTF-7 としてブラウザに解釈させれば 「<script>」 とみなされるので、「<」 という文字のエスケープをしていてもそれをすり抜けて XSS ができることがあるようです。 (古いブラウザの脆弱性。)

最近のブラウザだと問題ないようですが、念のため対策するのであれば 「+」 をエスケープすると良いようです。

本書の 6.3.1 節 「XSS」 に書かれています。

ステータスコード 429 Too Many Request

レートリミットに達した場合に返すためのステータスコードとして 429 Too Many Request が定義されているとのこと。 初めて知りました。

ヘッダフィールドの値として使用できる時刻の形式は決められてる?

本書には以下のように書かれていて、X-Rate-Limit-Reset のようなヘッダフィールドの値として Unix タイムスタンプを使うことは問題がある、といってるんですけど実際どうなんでしょうね。 (本書 195 ページ。)

HTTP ヘッダに Unix タイムスタンプを入れるのは、実は問題があるのです。 というのは、RFC 7231 の HTTP 1.1 仕様によればヘッダに入れてよい時間の形式は以下の 3 種類に限定されているからです

多分根拠は RFC 7231 の以下の箇所だと思うんですけど、これって別にあらゆるヘッダフィールドの値として使われる時刻の形式について言っているのではなくて、HTTP-date について言ってるだけな気もします。 わかんないですけど。

A recipient that parses a timestamp value in an HTTP header field MUST accept all three HTTP-date formats. When a sender generates a header field that contains one or more timestamps defined as HTTP-date, the sender MUST generate those timestamps in the IMF-fixdate format.

RFC 7231 (section 7.1.1.1 Date/Time Formats)

*1:サブタイプの最初のドットの前の部分。 本書では 「接頭辞」 と呼ばれている。

*2:本書には 『XHTTPRequest』 って書かれてる……。

*3:本書では 『JSON インジェクション』 って書かれてたけど、「JSON ハイジャック」 が正しいはず。

*4:IE ではユーザーが設定で無効にできるぽいけど。