Node v8.0 がリリースされた

Node v8.0 is released!!!!!!

f:id:yosuke_furukawa:20170605234126p:plain

Node v7 から半年経過して次のLTS対象になる可能性が高い Node v8.0 がリリースされました。 いくつか Notable Changes を話そうかなと。ちなみに Node v8 と言うと内部で使っている JS エンジンの V8 と混同されるので、みんな Node8 とか呼んでるときが多いです。このブログの中ではまだ出たばかりとあって、 v8.0 と minor バージョン付きで紹介します。

LTS 候補

v6.x 依頼の一年ぶりの LTS 候補になります。 LTS になるのは 10月以降と予定されています。 Current から LTS になるためにはコアの成熟を待つ必要があり、リリースから半年経過させる予定です。

https://talks.continuation.io/nodeweek-4-17/images/lts.png

Notable Changes

いくつか変更点をピックアップして紹介します。

  • npm5
  • util.promisify
  • V8 5.8
  • async_hooks
  • N-API
  • buffer improves more secure API
  • WHATWG URL is not experimental

npm5

npm5がNode v8.0にバンドルされることになりました。

yosuke-furukawa.hatenablog.com

先日紹介したとおりですね。Node v8.0 ではnpm5がデフォルトになるのでNode8でnpm installとかやるとpackage-lock.jsonがデフォルトで作られます。 package-lock.json 自身はファイルを作った時にリポジトリへコミットすることを推奨されています。 package.jsonpackage-lock.json が同一階層に並ぶのが Node v8.0 の標準プロジェクト構成ということになるでしょう。

util.promisify

これも先日紹介した util.promisify 関数ですね、 Promise をもう少し Node.js フレンドリーに扱えるようになりました。

yosuke-furukawa.hatenablog.com

Promise自身は unhandledRejection の取扱い等で Node で使うには注意点が必要ですが、 PromiseがNodeで使いやすくなったという変更は朗報と言えるでしょう。

V8 5.8

V8 のバージョンが新しくなりました。これに伴いEcmaScript の新機能やエンジンの最適化で下記の機能が追加されました。

function trailing commas

function trailing commas という機能が追加されました。これは関数の定義と呼び出しでケツカンマを許容するというものです。

function clownPuppiesEverywhere(
  param1,
  param2, // 今まではここのカンマが許容されてなかった
) { /* ... */ }

clownPuppiesEverywhere(
  'foo',
  'bar', // 呼び出し時にもケツカンマ OK
);

String padStart/padEnd

文字列の行揃えをするための関数である、 padStart / padEnd が追加されました。

console.log('hello'.padStart(10));  // '     hello'
console.log('hello'.padEnd(10));    // 'hello     '

個人的には leftpad 問題で一気に有名になり、脚光を浴びた機能なので感慨深いですね。

yosuke-furukawa.hatenablog.com

--harmony でフラグ付きで有効になる機能

--harmony フラグありだと下記の機能が有効になります。

template literal revision (–harmony)

ES2015 の template literal ではバックスラッシュ付きの文字列が与えられた際に「特別な役割を持った文字列」として動いてしまうことが多いです。例えば、

  • \uunicode 文字列のエスケープ用の接頭語 例: \u{1F4A4} や \u004B
  • \x は16進数文字列のエスケープ用の接頭語 例: \x4B
  • \ に数字がつくと、8進数文字列のエスケープ用の接頭語 例: \121

として扱われていたりします。 こうなると、以下の文字は tagged template literal では扱えません。

latex`\unicode`
windowsPath`C:\uuu\xxx\111`

これを解決するのが今回の template literal revision です、これにより、 DSL 用途で使いやすくなります。

// --harmony
function tagFunc(tmplObj, substs) {
    return {
        Cooked: tmplObj,
        Raw: tmplObj.raw,
    };
}

tagFunc`\uu ${1} \xx`
// { Cooked: [ undefined, undefined ], Raw: [ '\\uu ', ' \\xx' ] }

Object rest/spread properties (–harmony)

ES2015 の時点では 配列にしか無かった Rest / Spread 演算子が Object でも使えるようになりました。

  • Rest properties
// --harmony
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x; // 1
y; // 2
z; // { a: 3, b: 4 }
  • Spread properties
// --harmony
let n = { x, y, ...z };
n; // { x: 1, y: 2, a: 3, b: 4 }

これによって、 Object同士を merge するのには assign を使わなくても、以下のように書けるようになりました。

// --harmony
const merged = {...obj1, ...obj2};
// same: const merged = Object.assign({}, obj1, obj2);

async-await

また、v7.x の時点で既に追加されてましたが、 async-await も有効です。

util.promisify と組み合わせてコアの機能を async-await で書くことができます。

const fs = require('fs');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);

const read = async (path) => {
  const result = await readFile(path);
  console.log(result.toString());
};

read('test.txt');

V8 Turbofan and Ignition

V8 に新しく Ignition と呼ばれるインタプリターと Turbofan と呼ばれる新しいJIT Optimizerが追加されました。

ただし、これらもまだデフォルトでは on になっていません。試したい場合は --ignition--turbo を付ける必要があります。

これまでは Crankshaft と呼ばれるJIT エンジンだけでしたが、 Ignition と呼ばれるインタプリターが追加されたことでメモリフットプリントの軽量化とJITの高速化が図られる予定です。

docs.google.com

v8project.blogspot.jp

こちらちなみに jsconf.eu に行った時にV8チームの人達から話を聞いた所、「モバイルでのページ参照の時に低スペックでも高速に動くように検討された結果生まれた」という話を聞きました。 Node では IoT などの環境で動く際には役立つ可能性が高いです。

この話はもう少し詳しく別なエントリで語ります。

async_hooks

Node.js 内部のtraceやdiagnosticに使うための新しいAPIである async_hooks が追加されました。 async_hooks はNode.js 内のイベントループで起きているイベントを監視できるようにするための API です。 まだ、 API 一覧に内容は書かれていませんが、以下の Pull Request には上がっているのでそこから抜粋します。

node/async_hooks.md at 5d7a544bd19d46f24f58c20a4530334ea9ada64d · thlorenz/node · GitHub

以下のコードは async_hooks を使って Node.js 内部のオブジェクトとコールバックの発火有無を監視するためのものです。

const async_hooks = require('async_hooks');
const net = require('net');

let ws = 0;
async_hooks.createHook({
  // Node.js の内部オブジェクトが初期化される際に実行される関数
  // この時点ではまだ実体は存在せず、リソースは確保されていない
  init (id, type, triggerId) {
    const cId = async_hooks.currentId();
    process._rawDebug(' '.repeat(ws) +
                      `${type}(${id}): trigger: ${triggerId} scope: ${cId}`);
  },
  // コールバックが実行される前に呼び出される関数
  before (id) {
    process._rawDebug(' '.repeat(ws) + 'before: ', id);
    ws += 2;
  },
  // コールバックが実行された後に呼び出される関数
  after (id) {
    ws -= 2;
    process._rawDebug(' '.repeat(ws) + 'after:  ', id);
  },
  // 実体が削除された時に呼び出される関数
  destroy (id) {
    process._rawDebug(' '.repeat(ws) + 'destroy:', id);
  },
}).enable();

net.createServer(() => {}).listen(8080, () => {
  // 10ms 待ってから async_hooks を使って今のEventのIDを表示する。
  setTimeout(() => {
    console.log('>>>', async_hooks.currentId());
  }, 10);
});

これを実行すると下記のようなログが出力されます。

TCPWRAP(2): trigger: 1 scope: 1
TickObject(3): trigger: 2 scope: 1
before:  3
  Timeout(4): trigger: 3 scope: 3
  TIMERWRAP(5): trigger: 3 scope: 3
after:   3
destroy: 3
before:  5
  before:  4
    TTYWRAP(6): trigger: 4 scope: 4
    SIGNALWRAP(7): trigger: 4 scope: 4
    TTYWRAP(8): trigger: 4 scope: 4
>>> 4
    TickObject(9): trigger: 4 scope: 4
  after:   4
after:   5
before:  9
after:   9
destroy: 4
destroy: 9
destroy: 5

TCPWRAP や Timer などのNode.js の内部オブジェクトが実行されてる様子が見て取れるかと思います。 これをうまく使えばエラーのデバッグに役立てる情報を出力できます。ログを毎回出したくない場合は enable/disable メソッドを使ってhookをオンオフさせることも可能です。

const async_hooks = require('async_hooks');
const net = require('net');

let ws = 0;
const hook = async_hooks.createHook({
  init (id, type, triggerId) {
    const cId = async_hooks.currentId();
    process._rawDebug(' '.repeat(ws) +
                      `${type}(${id}): trigger: ${triggerId} scope: ${cId}`);
  },
  before (id) {
    process._rawDebug(' '.repeat(ws) + 'before: ', id);
  },
  after (id) {
    process._rawDebug(' '.repeat(ws) + 'after:  ', id);
  },
  destroy (id) {
    process._rawDebug(' '.repeat(ws) + 'destroy:', id);
  },
});

net.createServer(() => {}).listen(8080, () => {
  setTimeout(() => {
   // ここで改めてログを出す。
    hook.enable();
    console.log('>>>', async_hooks.currentId());
  }, 10);
});

N-API

N-API は新しく Native Addon 用の機能を提供するラッパーAPIです。これまでの Node.js ではコアでサポートしているラッパーAPIは存在せず、NANと呼ばれる ライブラリがサポートされていました。

これを利用して、 node-sass や leveldown や imagemagick-native といった Native Addon のライブラリが提供されています。今回からは NAN だけではなく、 N-API というコアが新しくサポートした機能を使って Native Addon を書くことが可能です。

詳しくはここの資料を見ると良いでしょう。

medium.com

また既に N-API を使った leveldown を使って Native Addon を活用するためのデモも存在します。

github.com

buffer improves more secure API

Buffer の API を実行する際に必ず初期ヒープ領域が 0 埋めされるようになりました。 この変更の重要さを語るためにはまず、 Buffer(number) を実行したときの問題点から語る必要があります。

Buffer をコンストラクトするには Buffer(number) のように数字を渡す方法と Buffer(string) などの文字列を渡す方法、配列を渡したり、 TypedArray を渡す方法などの複数の種類がありました。僕がよく使うのは new Buffer(string, encoding) で文字列をエンコードする時でしょうか。 new Buffer(number) を使うとその number に渡したサイズ分のバッファを事前に確保して使うことが可能です。

しかしながら、 以前の new Buffer(number) は初期化段階ではヒープメモリ中の中身を 0 埋めしていません。つまりヒープメモリ中の値の中身を見ようと思えば見れてしまいます。

var token = 'paSsWord!ASD, totally secret!';
for (var step = 0; step < 100000; step++) {
    // ここで token の中身を取れる
    var buf = (new Buffer(200)).toString('ascii');
    if (buf.indexOf(token) !== -1) {
        console.log('Found at step ' + step + ': ' + buf);
    }
}

github.com

昔の ws という WebSocket のためのモジュールがこの問題をもろに引き起こし、 攻撃を引き起こす可能性があるという脆弱性を持っていました。

https://nodesecurity.io/advisories/67

この問題に対処するため、 new Buffer(number) を呼び出す際、初期値を Buffer.fill(0) として0埋めされたバッファとして確保してしまう対応がなされました。

これにはパフォーマンスの問題が伴う可能性があります。つまり、必ずサイズ分の全ての値を 0 埋めする結果、パフォーマンス的に遅延してしまう可能性も考えられます。

セキュリティよりもパフォーマンスを優先させたい場合は専用の Buffer.allocUnsafe メソッドを使って実行する必要があります。

// 必ず最初に 0 で初期化され、安全にメモリアロケートされる
const safeBuffer1 = Buffer.alloc(10);
const safeBuffer2 = new Buffer(10);

// 初期化されずにいきなりアロケートされる
const unsafeBuffer = Buffer.allocUnsafe(10);

それに伴い、 Buffer(number) での呼び出しは pending deprecated 扱いになってます。今後はエラーになる可能性もあるので、 Buffer.allocBuffer.allocUnsafe に移行できるようにしておいてください。

WHATWG URL is not experimental

WHATWG URL は v7 で追加された新しい URL Parser ですが、それの扱いが格上げされ、 stable な API として扱われることになりました。

const URL = require('url').URL;

const myUrl = new URL('/a/path', 'https://example.org/');

v7 の初期までは URLSearchParams などにも対応していませんでしたが、それらのすべてのspecに対応されました。

そのため、以下のコードも正常に動作するようになりました。

const URL = require('url').URL;

const u = new URL('https://jxck.io?log=warn&lang=ja');
const searchParams = u.searchParams;
searchParams.get('log') // "warn"
searchParams.getAll('log') // ["warn"]
searchParams.delete('log') // undefined
searchParams.has('log') // false
searchParams.append('debug', true) // undefined
searchParams.toString() // "lang=ja&debug=true"

for ([k, v] of searchParams) {
  console.log(k, v);
  // lang ja
  // debug true
}

以下より抜粋

blog.jxck.io

他にも

と盛りだくさんです。すべてをちゃんと紹介しきれる気がしないので、いくつか抜粋しましたが、ちゃんと変更点を追いたい方は、以下の記事をご一読ください。

Node v8.0.0 (Current) | Node.js

まとめ

Node.js v8.0 が日本時間の 6/1 朝に公開されました。今回はv8.0の主だった機能を紹介しました。一週間経過して、今のところ問題も散見されておりますが、概ね動作しているように見えます。

ちなみに今週中に v8.1 が出るようなのでその時また少し変わるかもしれません。

ひとまず、自分の目の届くプロダクトでは Node v8.0 を試すことにして、実績を積み、問題があったらフィードバックする形にしていこうと思っています。

この他にも V8 の変更であったり、 N-API であったりは実は別枠で語らないといけないような大きな変更なので、随時紹介して行こうと思います。