技術探し

技術探し

JavaScriptを中心に記事を書いていきます :;(∩´﹏`∩);:

NodeにPerformance Timing APIが追加される

NodeへPerformance Timing APIの初期実装が入ります。

資料

developer.mozilla.org

W3C

Performance Timeline

High Resolution Time Level 2

github.com

NodeへのPR

github.com

まだ、masterやリリースラインに入っていないのでドキュメントはPRから見てください。
今回は、PerformanceFramePerformanceNodeTiming の説明は書かないので詳しくはドキュメントへ。

Nodeのイベントループの時間測定を測定するのも実装されている。

Performance Timing API

マイクロ秒で経過時間を取得するAPI である。

Dateを使い計測を行うことがあると思うが、以下のような場合がある。

const markStart = Date.now();

(() => {})();

const duration = Date.now() - markStart;

console.log(duration); // 0

実際は、正の値, 負の値, 0の値になり得てしまう。

実装

Nodeで使ってみる

processperformanceというメソッドが追加された。

> process.performance
{ timeOrigin: 14194760.508743,
  nodeTiming:
   PerformanceNodeTiming {
     duration: 2735.97479,
     startTime: 14194760.508743,
     type: 'node',
     name: 'node',
     arguments: 14194763.6652,
     initialize: 14194764.104207,
     inspectorStart: 14194769.714933,
     loopStart: 14194854.360176,
     loopExit: 0,
     loopFrame: 14196891.511942,
     bootstrapComplete: 14194854.356646,
     third_party_main_start: 0,
     third_party_main_end: 0,
     cluster_setup_start: 0,
     cluster_setup_end: 0,
     module_load_start: 14194813.331148,
     module_load_end: 14194813.33181,
     preload_modules_load_start: 14194813.332309,
     preload_modules_load_end: 14194813.356287 },
  nodeFrame:
   PerformanceFrame {
     prior: 0.012497,
     type: 'frame',
     name: 'frame',
     duration: 605.560913,
     startTime: 14196891.511942 } }
>

手順

基本的には、

  1. 開始区間のマーキング(mark)
  2. 終了区間のマーキング(mark)
  3. その区間の名前を定義して保存されます。(measure)
  4. すべて保存されたリストから3で定義した区間の名前を指定し取得(getEntriesByName)

また、同じ区間の名前をつけたときはスタックしていきます。
なのでgetEntriesByNameで取得した時に配列になっています。
これは新しいのから順にpushされます。(つまりタイムライン順)

区間を逆(未来 to 過去)にした場合、数値がすごいことになるのは仕様なのかな・・・?

少し長いですが、以下のコードを読めばわかると思います。

コード

const fs = require('fs');

const perf = process.performance;

perf.mark('A');

(() => {})();

perf.mark('B');

perf.measure('A to B', 'A', 'B'); // tagname, startMark, endMark

const measure1 = perf.getEntriesByName('A to B')[0]; // measure1's length is 1
console.log(measure1.duration); // 0.009461

/* ----------------------------------------------------- */

perf.mark('C');

fs.readFileSync('./node');

perf.mark('D');

perf.measure('C to D', 'C', 'D');

const measure2 = perf.getEntriesByName('C to D')[0]; // measure2's length is 1
console.log(measure2.duration); // 28.502531

/* ----------------------------------------------------- */

perf.measure('A to B', 'C', 'D'); // push to `A to B`

const measure3 = perf.getEntriesByName('A to B'); // measure3's length is 2
console.log(measure3); // display the performance timeline of `A to B`
/*
  [ PerformanceMeasure {
      duration: 0.009461,
      startTime: 15995656.619165,
      type: 'measure',
      name: 'A to B' },
    PerformanceMeasure {
      duration: 28.502531,
      startTime: 15995675.467252,
      type: 'measure',
      name: 'A to B' } ]
*/

console.log(perf.getEntries()); // display the performance timeline(combined `mark` and `measure` into the timeline)
/*
  [ PerformanceNodeTiming {
      duration: 107.495859,
      startTime: 15995598.683741,
      type: 'node',
      name: 'node',
      arguments: 15995602.156617,
      initialize: 15995602.750924,
      inspectorStart: 15995609.924404,
      loopStart: 0,
      loopExit: 0,
      loopFrame: 0,
      bootstrapComplete: 0,
      third_party_main_start: 0,
      third_party_main_end: 0,
      cluster_setup_start: 0,
      cluster_setup_end: 0,
      module_load_start: 15995653.258513,
      module_load_end: 15995653.313254,
      preload_modules_load_start: 15995653.313785,
      preload_modules_load_end: 15995653.341459 },
    PerformanceFrame {
      prior: 0,
      type: 'frame',
      name: 'frame',
      duration: 15995706.323224,
      startTime: 0 },
    PerformanceMark {
      duration: 0,
      startTime: 15995656.619165,
      type: 'mark',
      name: 'A' },
    PerformanceMark {
      duration: 0,
      startTime: 15995656.628626,
      type: 'mark',
      name: 'B' },
    PerformanceMeasure {
      duration: 0.009461,
      startTime: 15995656.619165,
      type: 'measure',
      name: 'A to B' },
    PerformanceMark {
      duration: 0,
      startTime: 15995675.467252,
      type: 'mark',
      name: 'C' },
    PerformanceMark {
      duration: 0,
      startTime: 15995703.969783,
      type: 'mark',
      name: 'D' },
    PerformanceMeasure {
      duration: 28.502531,
      startTime: 15995675.467252,
      type: 'measure',
      name: 'C to D' },
    PerformanceMeasure {
      duration: 28.502531,
      startTime: 15995675.467252,
      type: 'measure',
      name: 'A to B' } ]
 */

API

common

now()

現在の高分解能時刻タイムスタンプを返します。

mark(name: string)

パフォーマンスタイムラインへ新しいPerformanceMarkを追加します。
performanceEntry.typeは常にmarkです。

measure(name: string, startMark: string, endMark: string)

パフォーマンスタイムラインへ新しいPerformanceMeasureを追加します。
performanceEntry.typeは常にmeasureです。

name区間の名前です。
startMarkは開始地点の名前を設定します。もし名前が存在しない場合、timeOriginが基準となります。
endMarkは終了地点の名前を設定します。もし指定しない場合は、エラーが投げられます。

get

getEntries()

startTimeを基準にすべてのPerformanceEntry Objectのリストを時間順に返します。
mark 及び measure のタイムラインとなります。

getEntriesByType(type: string)

performanceEntry.typeと同じtypeを持つすべてのPerformanceEntry ObjectのリストをstartTimeを基準に時間順に返します。
古い物から先頭となります。

getEntriesByName(name: string, type?: string)

performanceEntry.typeと同じnameを持つすべてのPerformanceEntry ObjectのリストをstartTimeを基準に時間順に返します。
古い物から先頭となります。

clear

clearMarks(name?: string)

名前を指定すれば、そのマークを消し、
指定しなければ、すべてのマークをパフォーマンスタイムラインから消します。

clearMeasures(name?: string)

名前を指定すれば、そのメジャーを消し、
指定しなければ、すべてのメジャーをパフォーマンスタイムラインから消します。

仕様分類

High Resolution Time Level 2

High Resolution Time Level 2

toJson()

High Resolution Time

High Resolution Time

now()

Navigation Timing

timing, navigation

Performance Timeline Time Level 2

Performance Timeline Level 2

getEntries()

Performance Timeline

Performance Timeline

getEntries(), getEntriesByType(), getEntriesByName()

Resource Timing Level 1

Resource Timing Level 1

clearResourceTimings(), setResourceTimingBufferSize(), onresourcetimingbufferfull

User Timing

User Timing Level 2

User Timing

mark(), clearMark(), measure(), clearMeasure()

まとめ

Performance Timing APIに関する初期実装がもうすぐ入りそう。
まだまだ未実装な部分が多いので、今後に期待。

Node8.3.0でデフォルトになるTF/Iに関わるベンチマークについて

buildersconでのスライド

speakerdeck.com

8.3.0 プロポーザル

github.com

大きな変更はやはりV8の6.0とWHATWG Encoding Standardが入ることです🎉
今回はV8の6.0の話をします。
これはLTS対象であるNode8へのバックポートもされNodeのバージョンを9へ上げる準備ができました。

github.com

github.com

今現在のcurrent(master branch)はv9.0.0-preです。

TurboFan + Ignition

V8の5.9からデフォルトになるTurboFanとIgnitionというものがあります。 今までは、Crankshaftと呼ばれるコンパイルインフラストラクチャがありましたがそれが置き換わります。

v8project.blogspot.jp

TurboFanとIgnitionを簡単に説明します。

TurboFan

JITコンパイラ
Crankshaftから置き換わるもの。
Crankshaftではtryスコープ、ES2015の最適化(e.g. scope, class literal, for of, etc…)ができなかったですが、TurboFanではそれを可能とします。
chrome 41から段階的に使用されてました。(その頃はbaselineでTurboFanとCrankshaftでルーティングしていました)

Ignition

レジスタマシン。
モバイル端末のメモリ消費削減を目標として作られたインタプリタで、JSのコードをbytecodeにします。
V8のJITの問題点として、コードが一回実行されただけでも大量のメモリ消費をする可能性があり、メモリーオーバーヘッドを避ける必要がありました。

Flow

f:id:about_hiroppy:20170803092506j:plain

docs.google.com

V8の5.9は・・・?

Nodeでは8.3.0でV8の5.9を入れる予定でしたが、6.0を待つことになりました。

github.com

github.com

6.0ではTF関連のパフォーマンスが5.9よりも改善されるからです。
どうしてもエッジケースでパフォーマンスが以前から落ちる可能性があったからです。(もちろん6.0ですべて改善されたわけではないです)

パフォーマンス

medium.com

github.com

$ node benchmark/compare.js --old ./current --new ./8.3.0 arrays assert buffers child_process cluster crypto dgram dns domain es events fs http misc module net os path process querystring stream string_decoder timers tls url util v8 vm > compare-8.3.0.csv

Nodeのコアのベンチマークは上記のようにとります。(といっても普通に数日かかる量なので今回はissueのデータを使います。)

NodeのInternal ModulesのV8の5.8と5.9のパフォーマンスは以下のとおりです。
この頃は6.0がまだNodeのPRとして存在しなかったため5.9

github.com

量が多いので結論だけ言うと、劇的に改善される部分(100%以上)もあれば、劇的に遅くなる部分もあります。
例えば、buffers, events, http (!!), querystring, streams, string_decoder, url, utilは改悪になってしまってます。
今後、コアコードのパフォーマンスチューニングが必要なのではないかと思います。

Node.js Benchmarking

webpack

github.com

8.2.1(V8 5.8)

node benchmark.js  390.80s cpu 8:16.27 total

8.3.0(V8 6.0)

node benchmark.js  385.03s cpu 7:47.66 total

結果

webpackのベンチマークはあまり詳細がでないのでこれぐらいしか言えないですが、数秒程度速くなる可能性が見込まれると思います。

ESLint

$ node Makefile.js perf

8.2.1(V8 5.8)

Loading:
  Load performance Run #1:  370.81079ms
  Load performance Run #2:  138.677693ms
  Load performance Run #3:  162.385871ms
  Load performance Run #4:  147.627503ms
  Load performance Run #5:  138.628092ms

  Load Performance median:  147.627503ms


Single File:
  CPU Speed is 3100 with multiplier 13000000
  Performance Run #1:  5423.646407ms
  Performance Run #2:  5004.476339ms
  Performance Run #3:  5003.407218ms
  Performance Run #4:  5906.832898ms
  Performance Run #5:  5831.056636ms

  Performance budget exceeded: 5423.646407ms (limit: 4193.548387096775ms)


Multi Files (0 files):
  CPU Speed is 3100 with multiplier 39000000
  Performance Run #1:  15058.862498ms
  Performance Run #2:  13073.789987ms
  Performance Run #3:  12624.443172ms
  Performance Run #4:  12712.246881ms
  Performance Run #5:  16854.266736ms

  Performance budget exceeded: 13073.789987ms (limit: 12580.645161290322ms)

8.3.0(V8 6.0)

Loading:
  Load performance Run #1:  353.895106ms
  Load performance Run #2:  146.332188ms
  Load performance Run #3:  137.545153ms
  Load performance Run #4:  137.215065ms
  Load performance Run #5:  141.143209ms

  Load Performance median:  141.143209ms


Single File:
  CPU Speed is 3100 with multiplier 13000000
  Performance Run #1:  5469.148856ms
  Performance Run #2:  5300.968599ms
  Performance Run #3:  5379.501312ms
  Performance Run #4:  4955.262333ms
  Performance Run #5:  5242.219478ms

  Performance budget exceeded: 5300.968599ms (limit: 4193.548387096775ms)


Multi Files (0 files):
  CPU Speed is 3100 with multiplier 39000000
  Performance Run #1:  13120.178363ms
  Performance Run #2:  12653.286066ms
  Performance Run #3:  12639.912582ms
  Performance Run #4:  16274.700631ms
  Performance Run #5:  20859.144258ms

  Performance budget exceeded: 13120.178363ms (limit: 12580.645161290322ms)

結果

LoadingとSingle Fileは速くなっているが、Multi Filesが遅くなっている。
また、部分部分のパフォーマンスを見ると、必ずしもNode8.3.0が速いわけではない。
しかし、基本的には改善されていると思える。

まとめ

  • 8月中に8.3.0がリリースされる
    • 8.3.0でTurboFanとIgnitionがデフォルトになる v5.8 -> v6.0へ
    • WHATWG Encoding Standard(TextDecoderTextEncoder)が実装された
  • 今後、Node、Chromeはどんどん速くなる

PWAの実装をしてみた

そういえばPWAの実装したことがなかったなと思ったので少し触ってみた。

PWAとは?

PWA(Progressive Web Apps)

インストールが不要で、不安定なネットワークでも素早く起動し、プッシュ通知を可能にします。
また、ホーム画面にアイコンも表示でき、アプリと同様の扱いをすることが可能となります。

つまり、アプリに近づけたwebですね。

以下の記事が詳しいのでそちらを見てください;)

developers.google.com

目的

  • https, localhostでしかService Workerは動かないので常に安全
  • Service Workerの更新プロセスにより常に最新
  • App Shellモデルによる構成でUIをネイティブにさらに近づける
  • プッシュ通知による再エンゲージメント
  • キャッシュすることにより、ネットワークの依存度を下げる

技術スタック

Service Worker

対応状況は以下の通り
f:id:about_hiroppy:20170727174456p:plain

Can I use... Support tables for HTML5, CSS3, etc

代表的な提供機能

  • オフライン機能
  • push通知
  • バックグラウンドコンテンツの更新
  • コンテンツキャッシュ

レシピ: ServiceWorker Cookbook

PWAのview

PWAにはApp ShellとContentというものがあります。
以下の図を見るとわかりやすいと思います。

f:id:about_hiroppy:20170727175055p:plain

developers.google.com

つまり、アプリケーションシェルというのはダイナミックコンテンツじゃない部分を指します。

App Shell

一番、PWAの実現で難しい部分であり、一番パフォーマンスの向上を図ることが期待される部分(らしい)
シンプルなデザインコンセプトで設計されます。
Service Workerのキャッシング機能により、パフォーマンスの向上が可能です。
最上位のアプリのロジック、ルーターなどがあります。

Content

動的なビューです。例えばTwitterのタイムラインとか。
ここもそれぞれのコンテンツで必要に応じてJSのチャンクは細かく切られます。

キャッシュ戦略

基本的に、App ShellとContentのJSは別チャンクにするべきです。
Service Workerにそれぞれのチャンクを保持させることにより、ユーザが前のページに戻った時に早く読み込むことが可能です。
理論上、App Shellの読み込みとContentの読み込みを別にすることにより、パフォーマンスとユーザビリティの向上が図れるらしいです。
App Shellはどのページでも常に読み込まれ、Contentは必要に応じて読み込むという感じです。

読み込みフロー

PWAは先にその時に必要なものだけを取り、それをキャッシュする仕組みです。
なのでwebpackでチャンクを細かく切ることにより、必要なリソースだけを読み込めるように設計します。
その後、Service Worker側で追加のリソースを事前に取得して、将来的な読み込みを行います。 また、ローディングの順序はApp Shellで基本的なUIを構築し、その後にコンテンツです。

Web App Manifest

manifest.jsonに名前、カラー、ホーム画面に置くアイコンの設定等を書きます。
PWAには必須です。

以下のようなスキーマになります。

{
  "name": "My PWA Sample",
  "orientation": "portrait",
  "display": "standalone",
  "start_url": ".",
  "short_name": "MyPWA",
  "description": "This is a sample App!",
  "background_color": "#f5f5f5"
}

また、HTMLでは以下のように指定します。

<link rel="manifest" href="/manifest.json">

developers.google.com

今回は、webpack-pwa-manifestというプラグインがあったのでそれを使いました。

github.com

デザイン

PRPL patternというのがあります。
より高速にモバイルでwebのエクスペリエンスを提供します。
Push、Render、Pre-cache、Lazy-loadで構成されます。

以下のAddy Osmaniの記事がとてもわかりやすいです!
The PRPL Pattern  |  Web  |  Google Developers

また今度、ブログにでも書こうかと思います。

FirefoxChromeしかService Workerないけどどうするの?

普通のHTML、CSS、JSなので問題はありません。
Service Workerはあくまでもネイティブ機能に近づける実現方法なので今までどおりにフォールバックします。

ネットワーク

Twitter Liteでhttp/2, GraphQLが使われています。

実装

github.com

上記のリポジトリで開発してみました。
SPA + Service Workerで実現しています。
今回はCSRのみです。

ライブラリ

react, react-routerで構築しました。
詳しくは、リポジトリwebpack.config.jsとかを見てください。

webpack-offline

Service Workerのファイル吐き出しと結合を行ってくれるプラグイン

注意点として、キャッシュファイルの保存容量が超えた時のエラーがわかりづらい。
Uncaught (in promise) DOMException: Quota exceeded. service worker は多分そのエラー。

なので、基本的にはdevではofflineを使わないでプロダクションのときにだけ使うようにしたほうがいい。(HMRもおそらくできないので)
ただ、もちろんswに本当に接続できているか確認したいときはあるのでそのときはライブラリ群のチャンクをキャッシュから外してデバッグしている。

様々なオプションがあるのでチューニングによるパフォーマンスとかの変化はありそう。

構成

ファイル

                                    Asset       Size  Chunks                    Chunk Names
                       vendor.2a193704.js     806 kB       5  [emitted]  [big]  vendor
node-8f3bc311d3d7fbab90a659d57e126fbf.png    3.73 kB          [emitted]
         Boron.content.bundle.2a193704.js     2.3 kB       1  [emitted]         Boron.content
         Argon.content.bundle.2a193704.js     2.3 kB       2  [emitted]         Argon.content
          List.content.bundle.2a193704.js    1.89 kB       3  [emitted]         List.content
                       bundle.2a193704.js    9.28 kB       4  [emitted]         bundle
        Carbon.content.bundle.2a193704.js     2.3 kB       0  [emitted]         Carbon.content
                            manifest.json  367 bytes          [emitted]
                               index.html  462 bytes          [emitted]
                                    sw.js    23.2 kB          [emitted]
               appcache/manifest.appcache  265 bytes          [emitted]
                   appcache/manifest.html     3.3 kB          [emitted]

ファイルは上記のように分けました。
vendor.jsではライブラリのコードのみが入っています。
なので一番サイズが大きいです。
ライブラリのバージョンが頻繁に変わらないためコアコードから隔離します。

bundle.jsがApp Shellです。
ルーティングとToolbarを持っています。
ベストプラクティスがわからないですが、これは一緒にしないほうがいいかもです。

*.content.bundle.jsがcontentです。
今回は、Argon, Boron, Carbonの三種類とルートページのリスト、合計4チャンクあります。
ちなみにNodeのLTSの名前がこの三種類です(v4, v6, v8)

ルーティング

慣れているreact-routerを使う。
必要な時にcontentのチャンクを取得するためにlazy loadを使います。

react-routerには慣れているつもりだったが、v4からgetComponentがなくなっていることに気付いてなかった。。

昔は、

<Route
    path="/"
    getComponent={(location, callback) => {
      require.ensure([], require => {
        callback(null, require('./Root'))
      }, 'Root')
    }}
  />

みたいに書けたはずなのに今は書けなくなっている。

調べた限り多分、FBの方が書いてたこれが一番キレイな書き方。

Quick and dirty code splitting with React Router v4 · GitHub

うーん。。。 ラッパーを書かないといけないの。。(あとreact-routerのリファレンス読みづらいんだよね。。)

ということで、これをラップしてたライブラリがあったので今回はそれを使った。

github.com

ただ、残念なことに<Switch>が対応してなくて、つらい。。
今PRが出ているのでそれ待ちという状態です。
Can not use with Switch · Issue #4 · mhaagens/lazy-route · GitHub

しょうがないので、/に対してexactを付けることにした。

// 長いので省略部あり

const Routes = () => (
  <App>
    <Route
      exact
      path="/"
      render={() =>
        <LazyRoute
          component={
            import(/* webpackChunkName: 'List.content' */ './components/contents/List')
          }
        />
      }
    />
    <Route
      path="/argon"
      render={() =>
        <LazyRoute
          component={
            import(/* webpackChunkName: 'Argon.content' */ './components/contents/Argon')
          }
        />
      }
    />
  </App>
);

AppはView全体を構築します。
この中にToolbarが入っており、this.props.childrenが上記に当てはまったルートになりそれをレンダリングします。
よくreact-routerでやる部分的更新の手法ですね。

しかし、今回はレンダリング時にファイルを取得し読み込み流したいので動的に取得する実装が必要です。
なのでstage-3のdynamic importを使う必要があります。(webpack2以降はデフォルトで入っています)

アセットのインストー

f:id:about_hiroppy:20170727190449p:plain

このような感じで保存される。 キャッシュの種類は main, additional, optional の三種類ある。

mainはinstallイベント時にservice workerにキャッシュされ、もし失敗したら全部のキャッシュはされません。
additionalmainが正常にロードされた後、ロードされます。
optionalはサーバからfetchされたときのみキャッシュされるので、事前にダウンロードしません。

github.com

今回はindex.htmlと各種JS,画像を保存しているので、これでたとえserveしていなかったりネットを切っている状態でも恐竜が現れるのではなく通常のページが表示されます。

ネットワークタイムラインはこのようになります。
f:id:about_hiroppy:20170727191352p:plain

sizeのところを見るとわかりますが、Service Workerからコードを取得しています。
ネットワークが切れているので、sw.jsの取得は失敗しますがコアコードはすでに取得済みなのであたかも生きているようにレンダリングされる。
また高速です。(vendor.jsとか重いのに。。)

API周り

今回は、通信周りの実装を行っていないのでコードはありませんが、
Twitter LiteではAPIを叩いた後Normalizrを通してReduxに効率よくデータを渡しています。 また、IndexedDBにもその結果を保存します。

SSR

今回のサンプルではやっていませんが、基本的にはやったほうがいいと思っています。
Twitter Liteでは、認証をし、初期状態の構成をし、App Shellのレンダリングをする設計になっています。
Twitter Liteをインスペクタで色々見ると楽しいかも。

資料

developers.google.com

The PRPL Pattern  |  Web  |  Google Developers

blog.twitter.com

medium.com

さいごに

手探りでやっていてこれがすべてベストプラクティスではなかったり、誤読があるかもしれないので、何かあればPRやIssue、コメントなど出してもらえると助かります:p

英語

今勤めているメルカリにはDMM英会話を契約させてもらって会社でレッスンをしていい素晴らしい制度がある。

自分は、DMM英会話を解約して会社の方の契約でやろうと思って一ヶ月ちょいぐらいたったがその結果を記録しておきたかったので書いた。

結果として、自分でお金を払ってないのは自分の性格上、長続きしないという結論に至った。(本当に会社には申し訳ないです)
どうも日々お金を払っているという感覚がなく、仕事や作業を優先してしまうことがわかった。


さて言い訳はさておき、自分はフロントエンドチームに所属しているが最近、また外国人の方が増えた。
日本語は喋れない方だったので共通語が完全に英語となった。(元々、他の外国人の方もいたが日本語が理解できる方だったのでたまに日本語でもよかった)

結論としてご飯やMTG、雑談等で英語で会話する機会が増えたので日々英語を喋るということはクリアーしており、一旦これでいいのではないかなと思っている。 DMM英会話、予約取るまでがめんどいというか腰が重い。。

メルカリでは、GitHub上では基本英語で会話しており英語に触れる機会が昔よりかなり多くなった。

特に同じフィールドのエンジニアと英語で会話できるのは一番の幸せであり、いい練習にもなっていると思う。
とりあえず英語喋るときに緊張するのをいい加減にやめたいのと単語力を高めたい。。。

Node.js FoundationとNode.jsについて

Node.js Foundation

Node.js

JoyentのNode.js開発停滞を見直すためにLinux Foundation以下に作られた財団。後にio.jsも合流しました。
オープンガバナンスモデルを採用しています。

www.slideshare.net

CIのサーバのホスティングやCollaboratorsのsummitやcode & learnなどの旅費などの管理(出費)をしています。
Node.js foundationは後援している企業によって支えられています。 Node.js Foundation Members | Node.js

相関図

f:id:about_hiroppy:20170703093327p:plain

委員会

TSC(Technical Steering Comittee)

github.com

Node.jsのCore, それに関連のあるワーキンググループのすべての統括をします。
https://github.com/nodejs/TSC#tsc-scope

CTC(Core Technical Committee)

github.com

Node.jsのCoreの技術委員会です。
Coreというのはnodejs/nodeを指します。

CTCでは毎週MTGを行っており、そこでPRやIssueで相談があるものは話されます。(ctc-reviewが必要になった場合)
また、CTCのメンバーは投票に偏りが出る可能性があるため、同じ雇用先のメンバーがCTC全体の1/3いてはいけません。

Core Working Groups

Technical Working Groupsの中でもCoreなのが以下になります。 これらはTop-Level Working Groupsに所属します。
CTCによって作成されるワーキンググループです。
CoreのNodeとは独立して扱われます。(Coreのコミットを権限持っていても以下のWGに対して必ずしも権限を持っているわけではない)

  • Website
    • Node.js のサイトのメンテナンスを行います
  • Streams
    • Node.jsのStreams APIのサポート及び改善を行います
  • Build
  • Diagnostics
    • ツールベンダーに対して信頼性の高い診断ツールを提供します
    • inspectorプロトコルやAsync Hooks
  • i18n
  • Evangelism
    • Node.jsの開発状況等を世界、コミュニティに知らせることを行います
    • 後ほど、CommCommへ移動されます(まだ正式にはこちらで書かれているのでここにも書きます)
    • github.com
  • Docker
    • 公式のDockerイメージを管理します
  • Addon API
    • NANのようなネイティブアドオンを作る作者のための抽象レイヤーのプロジェクトのメンテナンスを行います
  • Benchmarking
  • Post-mortem
  • Intl
    • Node.jsの国際化(i18n)、及び地域化(l10n)のサポートと改善を行います
    • コンテンツの翻訳に関してはi18n working groupが責任を負います

Working Groups | Node.js

また、Core Working Groupではないが他のworking group(e.g. promise, API, etc…)、及びteam(HTTP, HTTP/2, Smoke Testing, etc..)などがあります。
https://github.com/nodejs/TSC#dependent-projects https://github.com/nodejs/TSC#adjacent-projects

CommComm(Community Committee)

github.com

Node.jsの全体のコミュニティに関して統括する委員会です。
最近できたばかりなので今後発展していくと思います。

もともと、Top-Level Working GroupsのInclusivityがCommCommになった感じです。
なので、Top-Level Working Groupsは現在、Technical Working Groupsしか存在しません。

github.com

Working Groups

Teams

  • nodejs-collection

    • Node.jsに関する記事を管理します
    • 記事を提出し、それがレビューされNode.js Foundation Mediumに置くことが可能となります
    • medium.com
  • community-events

    • コミュニティのイベント主催者同士が協力しあう場所
  • education

    • 教育も今はTSCの下ですが、CommCommへ移動する流れです
    • github.com

今後のTSCからCommCommへの移行リストは以下のとおりです。 github.com


会議

それぞれのワーキンググループでは会議が開かれます。
この会議を見ることにより、最新のNode.jsの情報を得ることができます。

カレンダー

Node.js (obsolete)
久しぶりに見たけど、5月ぐらいから記録されてないですね。。

CoreのMTGノート

https://github.com/nodejs/CTC/tree/master/meetings

MTGの動画リスト

www.youtube.com

Summit

github.com

Node.js Collaborator Summitと呼ばれる、Node.jsのコラボレータが集まる定期的なsummitがあります。
前回の開催は5/4, 5で行われました。
ここではNode.jsの問題点、改善点などのコアな話を現地で話し合います。


Node

github.com

NodeはCore Collaborators(CTCを含む)により管理されます。

LTS(Long Term Support)

github.com

LTS Working Groupにより管理されます。
Node.jsの長期サポートを行い、偶数バージョンがそれに当たります。

f:id:about_hiroppy:20170703093852p:plain

毎年4月(今年の8はv8のバージョン周りで5月)にリリースされる偶数バージョンが対象で毎年10月から開始されます。
この時の10月に奇数バージョンが切られそれがmaster(current)になります。
LTSは18ヶ月サポートされ、その後にメンテナンスが12ヶ月あります。
currentに入ったバグ修正や脆弱性修正がこのサポートされているバージョンへバックポートされる仕組みです。

LTSは今のところ、v4, v6, v8(これは10月から)です。
Latestはcurrentになります。(masterブランチのバージョン)

担当リポジトリ

すべてNodeのリポジトリにIssueやRFCを投げるのではなく、用途にあったリポジトリに投げる必要があります。

Node.jsに具体的な機能追加がある場合

github.com

将来的な機能追加の提案

github.com

ヘルプ

github.com

本体のバグ、修正はCoreへ提出してください。

CI

Nodeではjenkinsを使って様々なプラットフォームのテストをしています。
arm-fanned, aix, arm, freebsd, linux, linux-fips, linux-one, osx, ppc-linux, smartos, windows-fanned上でテストが行われます。
また、本体コードはもちろんですが、ドキュメントのlintもあります。

GitHub - nodejs/build: Better build and test infra for Node.

PR

PRのマージには最低48時間、休日は72時間の時間を必要とします。
これはレビュワーが世界中にいるため時間がレビューされる前にマージされるのを防ぐためです。
またマージには2人以上の承認が必要です。

貢献する

読むべきもの

CoC(Code of Conduct)

行動規範です。 これは必ず守るべきものです。
TSC/CODE_OF_CONDUCT.md at master · nodejs/TSC · GitHub

Contributing to Node

PRを提出するまでの手順が書かれています。
https://github.com/nodejs/node/blob/master/CONTRIBUTING.md

Good First Contribution

簡単に修正できるIssueに関しては、この good first contribution というラベルが付けられているのでそれを拾って作業を始めるのは最初の一歩としていいと思います。
Issues · nodejs/node · GitHub

他には、実装してないけど先にコメントアウトしてテストだけ書いてくれてたりするのでそれを見て実装するのもありだと思います。

Documents

Nodeリポジトリにはapiのドキュメントがあります。
ドキュメントは大切ですが、なかなか整備が追いつかない部分(外部リンク切れ等)があります。

サポート

もし、「やってみたいけどよくわからない」や「これ大丈夫なの?」などがあればNode.js日本ユーザーグループのslackにある#node-contributionで気軽に聞いてください。
Node.js 日本ユーザーグループ

さいごに

Node.jsが今どのように管理されていて、どういう活動をしているかというのを多くの人に知ってもらえれば嬉しいです。
また、少しでも興味を持っていただけたら嬉しいと思います。

Node学園26時限目まとめ

25時限目に引き続きオーガナイザーをしました about_hiroppy です。

今回の26時限目は、150人来ていただき本当に楽しく勉強になる時間でした。
改めてありがとうございました。
また、メルカリさんには会場整理、食事様々なことを手伝っていただき感謝します。ありがとうございました。

さて、今回の勉強会ですが、各フレームワークの話や型の話、低レイヤーのモジュールの話、 Chunked Encodingの話と結構コアな話が多い印象を受けました。
また懇親会では、古川さんからNode Collaboration Summit(Nodeのコアな人が集まるサミット(自分はこの回は行ってないです))やJSConf EU 2017で上がった議題の話がありました。(主にv8の最適化の話)
自分のwbepack3の発表を期待していた方はスミマセン(資料作ってなくて発表できなかったので。。。)

特に、React、vue、angualrの話が集まったことにより各フレームワークのことが知れて大変面白かったです。
次は自分もNodeの話をしようと思います😙

nodejs.connpass.com

進行しながら各LTの話をメモしようと思っていたのですが予想以上にできなかったので資料だけまとめておきます

資料一覧

capability of react fiber

speakerdeck.com

vue on storybook

speakerdeck.com

dependent type and typescript

speakerdeck.com

production E2E test with ProxyServer

speakerdeck.com

Angular PWA

slides.com

Nested Native Modules

ごめんなさい、資料見つけれなかったのでもし見つけたら更新します><

Chunked encoding を使った高速化の考察

www.slideshare.net

[懇親会] Node Collaborators Meetup & JSConf.EU

speakerdeck.com

まとめ

今回はNodeという単語を(おそらく)一回しか聞いてない!
次はまた二ヶ月後の8月です! お楽しみに:)

個人的には、Node, React, Vue, Angularでもっとこまめに情報交換とか色々できたらいいなと思いました。

宣伝

11月25, 26にNode学園祭があります! 登壇者、参加者募集していますので是非よろしくお願いします!

nodefest.jp

react-router, redux-sagaのテストの書き方

今回は、自分のelectronのテンプレートを参考にして話したいと思います。

github.com

記事が長くなってしまうので、プロダクションのコードは折りたたみしておきます。

使用ライブラリ

  • Jest
  • Enzyme
  • jest-serializer-enzyme(enzymeでsnapshotとるため)
  • redux-mock-store
  • redux-saga-test-plan

enzymeですが、snapshotに対応してないため react-test-rendererが最近の流れかもしれません。(むしろ推奨?)

facebook.github.io

github.com


react-router

github.com

react-router@4とreact-router-redux@5を使っています。

Routes.js

各パスをルーティングする部分です。
親がこのファイルをimportします。

import Routes from '../Routes';

const Root = () => (
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <Routes />
    </ConnectedRouter>
  </Provider>
);

このようにファイルを分離することによりテストをしやすくします。

テストコード

import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router';
import configureStore from 'redux-mock-store';
import { render } from 'enzyme';
import Routes from '../../src/renderer/routes';
import rootReducer from '../../src/renderer/reducers';

describe('routes', () => {
  const createDOM = (path = '/') => {
    const state = createStore(rootReducer).getState();
    const store = configureStore()(state);

    return render(
      <Provider store={store}>
        <MemoryRouter initialEntries={[path]}>
          <Routes />
        </MemoryRouter>
      </Provider>
    );
  };

  it('should render the root page', () => {
    expect(createDOM()).toMatchSnapshot();
  });


  it('should render the login page', () => {
    expect(createDOM('/login')).toMatchSnapshot();
  });
});

4系から入ったMemoryRouterを使い、テストをします。
基本的に、スナップショットで比較するだけで自分はいいと思っています。(もちろん確認でwrapper内に対してfind, contains等してもいいとは思います)
react-router-reduxを使っている場合、storeに入れるrouteのlocationのkeyがテスト毎ごとに異なるので、mountではなくrenderを使います。


Redux-saga

github.com

非同期処理をmiddlewareで行うために使います。

Root

各ファイル(e.g. ページ毎)のforkを一括で行います。

テストコード

import rootSaga from '../../../src/renderer/sagas';

describe('root saga', () => {
  it('should register sagas', () => {
    const tasks = 2;

    expect(rootSaga().next().value.length).toEqual(tasks);
  });
});

昔は、rootSaga()._invoke().value.length で取れていまいたが、今は取れません。
ここでは登録されているタスク数(各ファイル数)の個数を確認します。

Partial

それぞれのファイル内のタスクのビジネスロジックが書かれます。
このファイルのrootでそれぞれのactionとタスクを紐付けし、その処理を追えた後再度アクションを発行します。

テストコード

import { expectSaga } from 'redux-saga-test-plan';
import auth from '../../../src/renderer/sagas/auth';

describe('auth saga', () => {
  const storeState = {
    auth: {
      mail: 'a@b.com'
    }
  };

  it('should take on the LOGIN action', () => {
    return expectSaga(auth)
      .withState(storeState)
      .put({
        type   : 'LOGIN_SUCCESS',
        payload: {
          mail: '--a@b.com'
        }
      })
      .dispatch({
        type: 'LOGIN',
        mail: '--a@b.com'
      })
      .run();
  });

  it('should fail on the LOGIN action', () => {
    return expectSaga(auth)
      .withState(storeState)
      .put({
        type : 'ERROR',
        error: {
          code: 'ERROR_LOGIN'
        }
      })
      .dispatch({
        type: 'LOGIN',
        mail: 'a@b.com'
      })
      .run();
  });

  it('should take on the LOGOUT action', () => {
    return expectSaga(auth)
      .withState(storeState)
      .put({ type: 'LOGOUT_SUCCESS' })
      .dispatch({ type: 'LOGOUT' })
      .run();
  });

  it('should fail on the LOGOUT action', () => {
    return expectSaga(auth)
      .withState({
        auth: {
          mail: ''
        }
      })
      .put({
        type : 'ERROR',
        error: {
          code: 'ERROR_LOGOUT'
        }
      })
      .dispatch({ type: 'LOGOUT' })
      .run();
  });
});

redux-saga-test-planを使ってテストしていきます。
基本的にdispatchして、putでyieldされた結果(reducerへ渡す部分)を期待します。
自分はエラー系を全部sagaとして一枚挟んでいます。
template-electron/error.js at master · my-dish/template-electron · GitHub

Selectors

saga内で状態がほしいときに取得する関数を定義します。

テストコード

import { createStore } from 'redux';
import rootReducer from '../../../src/renderer/reducers';
import * as selectors from '../../../src/renderer/sagas/selectors';

describe('selectors', () => {
  const storeState = createStore(rootReducer).getState();

  it('should get auth.email', () => {
    expect(selectors.getMail(storeState)).toEqual('');
  });
});

自分はselectorsに関してはredux-saga-test-planを使いません。
selectorsは単純な関数であるので、stateだけしっかりしていればいいかなと思っているからです。


さいごに

storeでredux-devtools-extensionとかを使っているとテストできない箇所が出てしまうのどうにかならないかな。。。

今月の28日、Node学園 26時限目やりますので是非きてくださいね!(募集は26から)
react, vue, angular, jsconf and Node Collaboratos Summit, webpack3(多分…), etc…の話が聞ける予感する 😁 nodejs.connpass.com