ブラウザに実装されている ECMAScript modules について

ECMAScriptのモジュールシステムのブラウザへの実装が始まってるということで、これらを試用してみる。

従来のECMAScript modulesのセットアップ

まずは従来のモジュールのセットアップ方法。
下記のようにシンプルな依存関係のあるファイルが存在する場合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// app/someone.js
export default class Someone {
constructor(name) {
this.name = name || 'bob';
}

callme() {
return this.name;
}
}

// app/entry.js
import Someon from './someon';
const someone = new Someon('Joe');

document.addEventListener("DOMContentLoaded", event => {
document.body.innerHTML(someone.callme());
});

依存性の解決のために browserify または webpack などを使って、依存ファイルをbundle(一つに結合して)する。
ここでは、上記の依存関係のあるファイルを dist/bundle.js という形でbundleする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// webpack.config.js
'use strict';

const path = require('path');
module.exports = {
entry: './app/entry.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['env']
}
}
]
}
}

これらをhtmlから参照する必要があった。

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Someone Class module depends</title>
<script src="/dist/bundle.js"></script>
</head>
<body>
...
</body>
</html>

ブラウザネイティブのECMAScript modulesのセットアップ

現在実装中のECMAScript modulesを試すには以下の条件が必要

engine version config setting
Firefox 54+ about:config
Chrome 60+ chrome:flags
EDGE 15+ about:flags
Webkit Safari10.1 (iOS10.3) default
Node wip -

次のようなhtmlファイルを作成

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Someone Class module depends</title>
<!-- type="module" でmoduleをinportする -->
<script type="module" src="app/entry.js"></script>
</head>
<body>
...
</body>
</html>

type=”module” で import する際は ファイルの拡張子まで含む必要がある。

1
2
- import Someon from './someon';
+ import Someon from './someon.js';

ローカルのサーバーから配信すると、moduleがimportされている(依存性の解決ができてること)がわかる。

1
2
3
4
5
[Sun Fri 02 2017 18:56:58 GMT+0900 (JST)] "GET /" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3147.0 Safari/537.36"
[Sun Jul 02 2017 18:56:58 GMT+0900 (JST)] "GET /app/entry.js" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3147.0 Safari/537.36"
[Sun Fri 02 2017 18:56:58 GMT+0900 (JST)] "GET /app/someone.js" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3147.0 Safari/537.36"
[Sun Fri 02 2017 18:56:58 GMT+0900 (JST)] "GET /favicon.ico" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3147.0 Safari/537.36"
[Sun Fri 02 2017 18:56:58 GMT+0900 (JST)] "GET /favicon.ico" Error (404): "Not found"

type属性のmoduleを指定することでブラウザが内部的にES6 moduleとして依存解決してくれる。
もちろんインラインスクリプトでも実現出来る。

1
2
3
4
5
6
7
8
<script type="module">
import Someone from './app/someone.js';
const someone = new Someone('Joe');

document.addEventListener("DOMContentLoaded", event => {
document.body.innerHTML = someone.callme();
});
</script>

未サポートブラウザへの対応

ブラウザが ECMAScript module をサポートしてない場合、下位互換としてtype属性のnomoduleが提供されている。
これを使ってfallbackスクリプトを追加して動作を保証できます。

1
2
3
4
<!-- Support browser es6 modules -->
<script type="module" src="app/entry.js"></script>
<!-- Unsupport browser es6 modules -->
<script type="nomodule" src="dis/bundle.js"></script>

今回の場合は、nomoduleでbundleしたファイルを読込むことで期待した動きが担保できます。

モジュールのロードと実行タイミング

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- scriptタグ -->
<script>
console.log('inline script')
</script>
<!-- deferでhtml解析後に遅延実行 -->
<script deferd src="app/deferd.js"></script>
<!-- asyncで非同期実行 -->
<script async src="app/async.js"></script>
<!-- moduleでimport -->
<script type="module" src="app/module.js"></script>
<!-- moduleでインラインスクリプト -->
<script type="module">
console.log('js module')
</script>

この上記の場合の実行順序は

  1. scriptタグ
  2. deferd (html解析後まで実行遅延)
  3. async (非同期実行)
  4. type=”module”の外部ファイル
  5. type=”module”のインラインスクリプト

の順番となる。

type属性にmoduleを指定した場合、scriptの実行はdefer属性が与えられたものと同じように振る舞う。
つまりhtmlの解析後まで実行がキューイングされ、解析後からDOMContentLoadedイベントの間に実行される。

モジュールの複数回の読込

1
2
<script type="module" src="app/module.js"></script>
<script type="module" src="app/module.js"></script>

複数回のmoduleの読込んでも、ECMAScriptのモジュールの仕様の通りで一度しか実行されない。

トップレベルのthis

1
2
3
4
<script type="module" src="app/this.js"></script>
<script type="module">
console.log(this)
</script>

type属性にmoduleを指定した場合、トップレベルのthisはundefinedになる。
これは通常のブラウザでのトップレベルのthisがwindowとなる点と異なる。

トップレベルの変数

変数に関してもECMAScriptのモジュールの仕様の通りで、モジュールでのトップレベル変数はローカル変数として扱われる。
ブラウザ側のトップレベルの変数はモジュール側で参照できる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
global_variables = 'script';
</script>
<!--
global_variables は app/variables.js から参照できるが、
モジュール内の変数はブラウザ側から参照でいない
-->
<script type="module" src="app/variables.js"></script>
<script type="module">
console.log(global_variables) // script
console.log(module_variables) // ReferenceError
</script>
<script>
console.log(global_variables) // script
console.log(module_variables) // ReferenceError
</script>

まとめ

ECMAScript moduleのブラウザ実装が進めば、複雑なビルドシステムなどを利用することがなくなり、
より開発者にとって受け入れやすいものになるとおもわれる。
ただし、すべてを分割するという運用はやはりパフォーマンス的な問題もあるので、この辺りはノウハウの蓄積とか必要なのかな。

参考にしたページ

Native ECMAScript modules - the first overview

Comments