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 モジュールについてまとまっています。
『Web API: The Good Parts』 を読んだ
- 作者: 水野貴明
- 出版社/メーカー: オライリージャパン
- 発売日: 2014/11/21
- メディア: 大型本
- この商品を含むブログ (6件) を見る
同僚から借りて読みました。 全体としては 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
メソッドでもプリフライトリクエストするとは限らない、みたいな話を聞いたことはあったけど、詳しくは知らなかった)、とか Cookie
やAuthentication
などのヘッダで認証情報をやり取りする場合は、サーバー側は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 を無効にできます。 さらに、Firefox や Chrome、IE 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 を発生させそうなパターンがレスポンスに含まれている場合に、ブラウザがレスポンスをブロックする機能を有効にしたり無効にしたりできるヘッダフィールドぽい。 この機能は Chrome と Safari、IE (8 以降) がサポートしてるぽい?- ちゃんとしたドキュメントはあんまり見当たらない。
- XSS ブロックの機能はデフォルトで有効っぽい *4 から、サーバーサイドでは基本的には付けなくてもよさそう?
- 参考 : IE8 Security Part IV: The XSS Filter | IEBlog
Strict-Transport-Security
レスポンスヘッダ : ブラウザからのアクセスを HTTPS に限定させる。- http でリクエストされたときに https にリダイレクトする、という挙動だと、最初の http でのアクセスが中間者攻撃で書き換えられる恐れがある → 以前にこのヘッダが送られてきていたら、ブラウザは最初から https でアクセスする。
- RFC : RFC 6797 - HTTP Strict Transport Security (HSTS)
- 参考 : HTTP Strict Transport Security - Security | MDN
Public-Key-Pins
レスポンスヘッダ : ブラウザに暗号公開鍵とドメイン (?) を結び付けさせる → ブラウザは将来に証明書が偽造された場合に検知できるようになる。- 攻撃者により認証局が危殆化した場合に中間者攻撃されるのを防げる。
- RFC : RFC 7469 - Public Key Pinning Extension for HTTP
- 参考 : Public Key Pinning - Web security | MDN
本書の 6.5 節 「セキュリティ関係の HTTP ヘッダ」 に書かれています。
ブラウザがレスポンス内容を UTF-7 と誤認することによる XSS の成立
XSS 対策として 「<」 という文字列をエスケープするなどの方法がありますが、ブラウザにレスポンス内容の文字エンコーディングを UTF-7 だと誤認させることで、「<」 のエスケープをしていても 「<script>」 という文字列をそのままブラウザに食わせることができる、という攻撃があるとのことです。
UTF-7 では、「<」 という文字は ASCII の 「+ADw-」 として表現されるためです。 「+Adw-script+AD4-」 という文字列を UTF-7 としてブラウザに解釈させれば 「<script>」 とみなされるので、「<」 という文字のエスケープをしていてもそれをすり抜けて XSS ができることがあるようです。 (古いブラウザの脆弱性。)
最近のブラウザだと問題ないようですが、念のため対策するのであれば 「+」 をエスケープすると良いようです。
- 参考 : 第1回 UTF-7によるクロスサイトスクリプティング攻撃[前編]:本当は怖い文字コードの話|gihyo.jp … 技術評論社
- 参考 : UTF-7でXSSを発生させる10の方法 - 葉っぱ日記
本書の 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)
VirtualBox 5 上の Debian 8 に Guest Additions をインストールする
まあドキュメントを読めって話ではあります。 次のページにまとまっています。
背景
わりと長く VirtualBox を使ってきて、何度も Guest Additions をインストールしてきましたが、これまで Guest Additions のインストール手順をちゃんと見たことがありませんでした。
これまでは雑にインストールメディアからインストールできてたのですが、今回 VirtualBox 上の仮想マシンに Debian 8 をセットアップして Guest Additions をインストールしたらちゃんとインストールできてなかった *1 ので、改めて調べてみました。
インストール方法
最初に言った通りドキュメントに全部書かれてますので、詳細はドキュメントを読んでください。
事前準備
まず、事前準備として、dkms
パッケージをインストールします。 (Debian/Ubuntu 系の場合。)
apt-get update apt-get install dkms
(root
ユーザー以外で上記コマンドを実行する場合は sudo
してください。)
んでもってシステムを再起動します。
インストール
再起動後、ゲスト OS が起動したら、まずは Guest Additions のインストールメディア (VBoxGuestAdditions.iso
) を挿入します。 「デバイス」 メニューの 「Guest Additions CD イメージの挿入」 を選択すると、ゲスト OS にインストールメディアが (仮想的に) 挿入されます。
挿入された CD-ROM は /media/cdrom
にマウントされます。 端末を開いて /media/cdrom
に移動して、次のコマンドを実行することでインストールが開始されます。
sh ./VBoxLinuxAdditions.run
(root
ユーザー以外で実行する場合は sudo
しましょう。)
関連エントリ
VirtualBox で共有フォルダを使う
- VirtualBox 4 における共有フォルダの自動マウントの設定 - ひだまりソケットは壊れない : Guest Additions をインストールしたら共有フォルダ機能が使えます。
VirtualBox 上の Debian に Guest Additions をインストールする
dkms
パッケージをインストールする以外の方法が紹介されてますが、まあ gcc
と make
と linux-headers
の正しいバージョンが入ればなんでも良さそうです。
*1:事前準備ができてなかった
UWP アプリ 「みお×ぽん」 をリリースしました
2016 年 1 月 19 日に、Universal Windows Platform (UWP) アプリ 「みお×ぽん」 をリリースしました。 下記ページからダウンロードできます。
どんなアプリ?
株式会社インターネットイニシアティブが提供するインターネット通信サービス 「IIJmio 高速モバイル / D サービス」 の利用状況の確認と、高速通信のためのクーポンの有効・無効の切り替えを行うことができます。 Windows 10 を搭載した PC や、Windows 10 mobile 端末などで利用できます。
IIJ 公式アプリ 「みおぽん」 (Android 用 / iOS 用) の機能を Windows プラットフォームで提供するために開発しました。
既知の問題点
Chrome からの共有で onNewIntent が呼ばれない問題 (Android アプリの documentLaunchMode の話)
Android の API level 21 で導入された documentLaunchMode
に関する Activity の挙動にバグっぽいところがあって、結構扱いに困るのでまとめておきます。 「documentLaunchMode
? 関係ないや」 って思ってる人でも、外部アプリからの Intent を扱うアプリを書くときに影響されるかもしれません。
まとめ
- Activity の
documentLaunchMode
としてintoExisting
が指定されており、既存の Activity が再利用される場合は、launchMode
がstandard
であっても Activity のonNewIntent
メソッドが呼ばれるべき *1 だが、実際には呼ばれない。- バグっぽい。
- AndroidManifext.xml で
documentLaunchMode
の値を指定していなくても、外部アプリが投げる Intent にintoExisting
相当のフラグ (FLAG_ACTIVITY_NEW_DOCUMENT
フラグ) が設定されていることがあるので、外部からの Intent を受け取る Activity を書いている場合には否が応でもこの問題に悩まされる。 - とりあえずの対処法は、AndroidManifest.xml で、Activity に
launchMode="singleTop"
を指定すること。- アプリ内部で使用することを考えて
singleTop
にしづらい場合は、内部で使用する Activity と外部からの Intent を受け取るための Activity を別に定義して (単純にサブクラスを作れば良いだろう)、外部からの Intent を受け取るための Activity のlaunchMode
をsingleTop
にする、などの対応が必要。
- アプリ内部で使用することを考えて
関連する発表資料
この記事の内容に関連した話を 2015 年 9 月 30 日の 「関西モバイル研究会 #6」 で発表しました。
背景
この問題に行きついた背景です。
- 外部アプリからテキスト (
Intent.EXTRA_TEXT
) を含む Intent を受け取って処理する Activity を含むアプリを開発していた。- Chrome アプリの 「共有」 で投げられる Intent も受け取れる。
- 次のようなユーザー操作を行うと、Activity が新しい Intent を扱えなくて困った。
- Chrome でとあるページ (例として 「http://example.com/1」) で 「共有」 し、Activity を起動。 → Activity では Intent から 「http://example.com/1」 というテキストを取りだせる。
- Activity を終了せずに、アプリを切り替えて Chrome に戻る。
- 別のページ (例として 「http://example.com/2」) で再度 「共有」 し、同じ Activity を起動。 → Activity が破棄されずに残っていた場合、
onStart
メソッドやonResume
メソッドが呼ばれるが、onNewIntent
メソッドが呼ばれず、getIntent
メソッドで取得できる Intent も前に開いたときの Intent になっている。 → 新しい Intent の情報が得られない!
documentLaunchMode
の話
documentLaunchMode
とはなんぞや、という話。
- 公式ドキュメントとしては以下のページを読むとわかりやすいです。
- API level 21 で導入。
- Activity 起動時の挙動を制御するためのもの。
- これを指定することで、「最近のタスク一覧」 に同じアプリケーションの複数ドキュメントを表示できる。
- AndroidManifest.xml で指定することもできるし、
startActivity
にフラグとして渡すこともできる。
intoExisting
documentLaunchMode
の値としてintoExisting
を指定すると、既存のすべてのタスクの中から Intent のコンポーネント情報とdata
の URI が同じものが探される。- あれば、そのタスクの中身がリセットされて、Activity が再利用されて Activity の
onNewIntent
メソッドが呼ばれる。 - なければ、新しいタスクが生成される。
- あれば、そのタスクの中身がリセットされて、Activity が再利用されて Activity の
- と、ドキュメントには書かれているが、
onNewIntent
メソッドが呼ばれるのはタスクの Activity のlaunchMode
としてsingleTop
などの値が指定されている場合で、standard では呼ばれなかった。 (まじかよ……) - DocumentCentricApps というサンプルコードがあるが、サンプルコードのコメントなどはドキュメント通りの挙動を期待しているが、実際の挙動はドキュメントどおりにはならなかった。 (Nexus 5; Android 5.1.1 で確認)
バグっぽい
- ドキュメントを読む限り、
documentedLaunchMode
がintoExisting
ならば、たとえlaunchMode
がstandard
でもonNewIntent
メソッドが呼ばれてもよさそうだけど、実際には呼ばれない。 - API level 23 (Android 6.0 Marshmallow) での挙動も見てみたけどやっぱり同じだった。
- もともと
launchMode
がstandard
だとonNewIntent
メソッドは呼ばれないものなので、仕様なのかもしれないけど仕様だとしたら糞仕様だしバグならつらい。 - ていうか 『Activities launched with the
FLAG_ACTIVITY_NEW_DOCUMENT
flag must have theandroid:launchMode="standard"
attribute value』 って書かれてるし……。 - StackOverflow でこの問題について書いてる人が居た : Android API guide > Overview Screen: onNewIntent() not called with FLAG_ACTIVITY_NEW_DOCUMENT - Stack Overflow
- バグ報告した : Issue 188033 - android - onNewIntent method not called with FLAG_ACTIVITY_NEW_DOCUMENT flag - Android Open Source Project - Issue Tracker - Google Project Hosting
Chrome アプリの 「共有」 で onNewIntent
が呼ばれない問題
原因
Chrome アプリの 「共有」 (メニューの 「共有」 の右側に表示されている、最後に起動したアプリアイコンをタップ) で投げられる Intentを見てみたところ、以下のフラグが設定されていました。
FLAG_ACTIVITY_PREVIOUS_IS_TOP
FLAG_ACTIVITY_FORWARD_RESULT
FLAG_GRANT_READ_URI_PERMISSION
FLAG_ACTIVITY_NEW_DOCUMENT
(API level 21;documentLaunchMode="intoExisting"
相当のフラグ)
上述のとおり、documentLaunchMode
が intoExisting
で、launchMode
が standard
だと、古い Activity が再利用されるもの onNewIntent
メソッドが呼ばれないのです。 つらいですね。
回避策
一つの解決策としては、Activity に launchMode="singleTop"
を設定する方法があります。 これだと onNewIntent
メソッドが呼ばれるようになります。
しかし、外部からの Intent を受け取るだけじゃなくてアプリ内部からも起動される Activity の場合は launchMode="singleTop"
を設定することができないことも多いでしょう。 そういう場合は、アプリ内部からしか起動されない Activity と外部からの Intent を受け取る Activity を別のクラスにしてしまって (片方をもう一方のサブクラスとするなど)、外部からの Intent を受け取る Activity にだけ launchMode="singleTop"
を設定するなどの方法を採ることになるでしょうか。 もうちょっといい方法があれば嬉しいですが、それぐらいしか思いつきませんでした。
終わり
この問題、とにかく辛いのですがあんまり困ってる人を見かけないので、もしいい感じの回避策があるのでしたら教えてください!!!!
*1:ドキュメントを読む感じだとそうだと思われる