LoginSignup
5
6

More than 5 years have passed since last update.

.mjs + HTTP/2 = Universal JS

Last updated at Posted at 2017-10-09

この記事は2017/10/06のGotanda.js#9でLTした内容と同じです。
但しこちらでは、Node.js等のリリースと共に内容をアップデートしていく予定です。
(発表資料はsnapshotとしてアップデートしません)

(new) Node.js >= v10.0.0 HTTP/2 & ES Modules

core modulesにおいて、http2をfeature flagなしで実行できるようになりました。
ES modulesについては未だフラグ付きなものの、dynamic importやimport.metaがサポートされました。

koaまたはhapiはhttp2サポートされているのでproductionで使い始めるところも出てきてるかもですが、10系のLTSは2018/10からなのでまだオススメしません。
expressにおいては未だhttp2サポートされてませんが、spdyであれば可能なようです。
RisingStack: Node v10 is Here - Feature Breakdown!

(new) Performance by HTTP/2 + module

2017/10/07にChromeで大量のmoduleをロードした際のPerformanceについてのサマリーが公開された。

Loading Performance with (Many) Modules

これによると現状、HTTP/2でも下記が推奨らしい。

  • 数百に細分化され、依存も深い(depth>=5)場合はバンドルすべき
  • 依存性の低いツリー(depth<3)で100未満のモジュール数であれば、パフォーマンス的に問題ない(ただしバンドルした方が高速)

すぐにパフォーマンス改善していくよ、と締められているが、もし今ES Modulesを実装するなら、dynamic importなど非同期読み込みを使った方が良さそうです。

What’s .mjs?

  • Node.jsでES Modulesを扱うための新拡張子
    • Node.jsはrequire()などのModules APIを捨てる
    • import/export構文に統一
    • flaggedで実行可能(>= v8.5)
node --experimental-modules hoge.mjs

ブラウザ実装状況

es6-module.png

before

<!-- browser -->
<script type=text/javascript src='bundle.js'></script>

// server
const pkg = require('pkg');
module.exports = {pkg};

after

<!-- browser -->
<script type=module src='not_bundle.js'></script>

// server
import hoge from './hoge';
export default hoge;

Diff (Node.js)

  • Modules APIが消えるのでそこから生えてたGlobal変数等も消える
    • module
    • require.cache
    • NODE_PATH
    • __dirname/__filename
    • etc...

Diff (ECMAScript)

  • not Function but Syntax
    • 静的解析がしやすくなる
    • 動的ロードはできなくなる
  • not PATH but URL (Node.jsではfile://)
  • strict mode
  • スコープが区切られる
  • thisundefined

動的ロードェ...

const myPkg = env === 'development' ? require('../mypkg') : require('mypkg');
  • 自分はNode.jsで環境によってpkg or directoryと読み込み方変えてた
  • せめて$NODE_PATHが使えれば(´・ω・`)
  • 非同期でよければimport()(Dynamic Import)で代用可能

移行大変そう。でもこの一歩は、

go_universal.jpeg

Load to Universal JS

  • ブラウザでも.mjsで読み込もう
    • <script type=module src="./foo.mjs">
  • 但しX-Content-Type-Options: nosniffなのでContent-Typeをjsとして明示
    • WebサーバーによってはContent-Type: application/javascriptで送ってくれない
      • 自分の環境ではapacheはダメだったけどexpressでは明示せずとも渡せた
    • application/octet-streamnosniffだからダメ

Try Universal JS

$cat validation.mjs
export default (input) => typeof input === 'string';

$cat browserside.js
<script type=module>
import validation from './validation.mjs';
if (validation('user-input')) console.log('ok'); // ok
</script>

$cat serverside.mjs && node --experimental-modules serverside.mjs
import validation from './validation';
if (validation('user-input from browser')) console.log('ok');
(node:72314) ExperimentalWarning: The ESM module loader is experimental.
ok
  • validationをブラウザ・サーバー共通で使う例
  • Node.jsではimportする側もES Modulesである必要がある
  • ブラウザ側は拡張子まで含む必要があることに注意

Try Universal JS (using npm package)

$cat validation.mjs
import validation from './node_modules/myValidation/index.mjs';
export default (input) => validation(input);
  • validationの中でnpm packageを使う場合の例
  • nodeではfrom 'myValidation'の指定で十分
  • だがブラウザ側は拡張子まで必要
  • なので現状だと上記のような工夫が必要......

HTTP/2

  • not Text but Binary
    • 1本のTCP接続の内部に仮想のTCPソケットを作成
    • 並列接続でもハンドシェイク不要
    • cf. HTTP/1.1では1 reqがsocketを占有した状態で並列接続(ex, Chrome:6本)
  • Hpack
  • Server Push
    • CSS/JS/画像を事前に送りブラウザのキャッシュに入れておける
    • 表示ページのassetだけでなく今後遷移するページのもPUSH可能(広告歓喜!)

Performance

HTTP/2 対応状況

Try HTTP/2 with express

  1. 鍵作成
    • 主要ブラウザにおいてHTTP/2はTLS前提
    • 但しlocalhostはブラウザが特別扱いしてくれてTLS不要
    • のはずがダメだった(ServiceWorkerでは不要だったのに)
  2. http2 module
    • streamイベントを利用するのが王道だがhttp/https Compatibility APIを利用
    • 従来通りcreateServer()express()を渡す
  3. 実行
    • with --expose-http2 flag

Try HTTP/2 with express: オレオレ証明書作成

$brew upgrade openssl  # macデフォのだとversionが古くてできない
$openssl version
OpenSSL 1.0.2l  25 May 2017

$openssl genrsa -out my.key 2048
$openssl req -new -key my.key -out my.csr  # 対話では全てdefaultを回答
$openssl x509 -req -days 365 -in my.csr -signkey my.key -out my.cert

Try HTTP/2 with express: http2 module

$cat server.js
const express = require('express');
const http2 = require('http2');
const fs = require('fs');
const options = {
  key: fs.readFileSync(__dirname + '/my.key'),
  cert: fs.readFileSync(__dirname + '/my.cert'),
};
const app = express();
app.use(express.static(__dirname + '/views'));
app.use(express.static(__dirname + '/mjs', {
  setHeaders: (res, path, stat) => {
    res.header('Content-Type', 'application/javascript');
  }
}));

http2.createSecureServer(options, app).listen(8080);

Try HTTP/2 with express: 実行

$npm install express
$cat views/index.html
<h1>mjs</h1>
<script type=module src=./validation.mjs> <!-- mjs試すなら用意 -->

$node --expose-http2 server.js
(node:73026) ExperimentalWarning: The http2 module is an experimental API.

listenできた! https://localhost:8080/を開くと......

_http_incoming.js:104
  if (this.socket.readable)
                 ^

TypeError: Cannot read property 'readable' of undefined
    at IncomingMessage._read (_http_incoming.js:104:18)
    at IncomingMessage.Readable.read (_stream_readable.js:445:10)
    at IncomingMessage.read (_http_incoming.js:96:15)
    at resume_ (_stream_readable.js:825:12)
    at _combinedTickCallback (internal/process/next_tick.js:138:11)
    at process._tickCallback (internal/process/next_tick.js:180:9)

対処法

Try HTTP/2 with express: 実行(revenge)

$git clone https://github.com/phouri/express.git -b initial-support-http2
$cat server.js
const express = require('./express'); // <- line.1を左記に変更
$node --expose-http2 server.js

https://localhost:8080/を開くと......h2通信できた!

h2.png

まとめ

  • ES Modulesが来る!
    • browserはほぼ実装済、Node.jsではこれから
    • 移行大変そうだけど、Universal JSへの大事な一歩
  • HTTP/2が来た!!
    • 分割してもロードは早くなる(見込)
  • Universal JSが、来る!!!

References

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6