トップ 最新 追記

2018-05-01 [長年日記]

_ ViewModel系フレームワークでデータの変化を監視させない

実は最近 Vue.js ばかりいじっていて、その時に

data の変更を component が全部検知してしまうとモノによってはパフォーマンスペナルティがでかいのでは?

と気になったので、ついでにメジャーなフレームワークについて「監視させない方法」を調べてみた。今回調べたのは以下の4つ。

  • Vue.js
  • Mithril
  • React
  • Angular (2+)

ちなみに React 以外をちらっと触ったことある程度なので、間違えてる可能性はおおいにあります!!1

Vue.js

Vue の場合は data あるいは data() で「ビルド時に明示されていないもの」は自動監視できない。

つまり created() などで this に対して値をセットするとそれは auto redraw の対象から外れるので変化の激しいオブジェクトなどはこのような方法で与えるとよい。

data() {
  return {}
},
created() {
  this.nowatch = NotWatchedObject
}

Mithril

Mithril にはデータの変更を自動検知して redraw する機能は存在しない。あくまで VM component はイベントハンドラ実行後に redraw を行う。つまり特定の値の変更に寄与する関数を Mithril に明示しない限り component は値の変更、変化の影響を受けない。

したがって自由に外の世界を統べる World オブジェクトなどを配置することができる。

はず。

cf. 自動再描画システム - Mithril.js

React

class ベースの React.component は state と props が監視対象であり、それ以外の property は自由に使える。

While this.props is set up by React itself and this.state has a special meaning, you are free to add additional fields to the class manually if you need to store something that doesn’t participate in the data flow (like a timer ID).

State and Lifecycle - React

Angular 2+

Angular 2+ は Zone というライブラリを使って

  • ユーザーの fire したイベント
  • XHR
  • タイマー

を監視しており、この結果の component の変化は自動で検知(Change Detection)される。

NgZone#runOutsideAngular の中で実行したものは Change Detection の対象外になるらしいので、特定の property の変更に寄与するイベントをこれで囲んであげるとよさげ。

まとめ

これまであまり気にしていなかったが、実はメジャーなViewModel系のフレームワークの考え方は

  1. 特定のデータ監視系(React, Vue)
  2. イベント監視系(Mithril)
  3. 非同期処理監視系(AngularJS)

に分類できることが分かった。(あえて言うと 2 は 3 の簡略版みたいなものと言えるかもしれない。)

Tags: JavaScript
本日のツッコミ(全1件) [ツッコミを入れる]

_ ふぁらお加藤 [> 間違えてる可能性はおおいにあります!!1 「1」がまざってます。あ、わざと?]


2018-05-02 [長年日記]

_ MobXをVue.js(Nuxt.js)で試してみた

なぜVuexじゃないのか

先日来、ViewModel フレームワークが片っぱしから値の変化やイベントを検出して View を書き換えようとする姿勢に疑問が生じていて、本来は MVVM とかじゃなかったの? M 無視しすぎじゃね? あるいは M は全部 HTTP とか割り切りすぎじゃね? という感じになってきていた。*1

それで検出されないようにするにはどうしたらよいかというのが昨日の「ViewModel系フレームワークでデータの変化を監視させない - あーありがち(2018-05-01)」になるわけだけど、これはいわゆる VM component 内の話。でも実際のところ Model はだいたいグローバルでシングルトンになるんじゃねーの?という気がしていて、そうなると component でだけ実現できてもなーと思うわけですよ。

で、じゃあグローバルでシングルトンてことになると Flux 系 Store 具体的に例えば Vue.js だとまぁ Vuex になるんだと思うけど、その Vuex ではこういうことはできない。Vuex は雑に言うと監視対象となる state とその state に変更を加えることを許された mutation から成る*2 が、これらはそれぞれ独立した function であり、mutation には基本的には state しか渡ってこない。mutation 以外に action や getter も定義できるが、事情は同じである。すべて state を中心に置いている。そして改めてくり返すが、その state はすべて監視対象というわけだ。

class ベースの React.Component みたいに Store という名の Model 相当の class があって自由に property を増やせるわけではない。その強い制約こそがグローバルな状態管理によいのかもしれないが、それにしてもなぁという感じ。

MobXのサンプルコードを見てみる

そこで MobX.

Introduction | MobX

こいつは Store というよりは基本的には Observer と言ってよいと思う。

公式のサンプルコードを引用するとこんな感じ。

import { observable } from "mobx"

class Todo {
  id = Math.random();
  @observable title = "";
  @observable finished = false;
}

ここで「これ JS か?」と思うかもしれないけど、class property と decorator を Babel ってやれば使えるれっきとした ES.next valid な JS なので、JS 脳が化石化してる人は慌てた方がよいかもしれない。

ポイントは @observable で、この decorator がついている値だけが監視対象になる。ね、これこれ、こういうことですよ、やりたいのは。

ついでにこの observable な値に変更を加えるには @action を付けた function を経由するとデバッグしやすくてよいよ、ということなので、それらを適当に書き足すと

class Todo {
  @observable title = "";

  @action setTitle(title) {
    this.title = title;
  }
}

こんな感じ。

これは「各自が気ままに値を書き換えると追えなくなるよ」という教えに従っているだけで、絶対守らなきゃいけないってわけじゃないけど、まぁ守っておこう。別に混乱を生みたいわけじゃない。

実際にVue.jsで試してみた

Vue.js と MobX をブリッジしてくれるライブラリは有名どころで二つある。これ以外にもあるのかもしれないけど、見つけられなかった。

実際に利用したパッケージは

  • mobx 4.2.0
  • vue-mobx 2.0.0
  • movue 0.3.0

ここで雑に比較すると、とにかく component 内での記述を少なくしたいなら vue-mobx がオススメ。Vue.use の記述は済んでいるものとして component 内のコードはこれだけで済む。

import Model from '..'

export default {
  fromMobx: {
    Model
  }
}

なんと、fromMobx に Model を与えるだけ。こうしておいて、この object を食わせて Vue component を生成するだけでいきなり Model 内の observable な値と action が component 内に生える。

いきなり生えるのが Ruby の module っぽくて「おぉ、なるほど」となるのだが、さすがにどんな component から呼ばれるか分からない Model の observable と action がそのまま生えると、やたらと VM component が増えがちな昨今のフレームワークでカジュアルに使うにはちょっとまずそうな気がしないでもない。

そんなあなたには movue.

movue も fromMobx という property で VM component 内に Model の情報を呼んでくるのは一緒だけど、この fromMobx では細かく設定をコントロールできるので、こっちの component ではこの名前で、あっちの component ではこの名前で、といった使い方ができそう。

Decorator対応

MobX の decorator は optional なので、実はさっきの例は以下のようにも書ける。

import { observable, decorate } from "mobx"

class Todo {
  id = Math.random();
  title = "";
  finished = false;
}

decorate(Todo, {
  title: observable,
  finished: observable
})

こっちの方がより広範囲で動くのだけど、確かに decorator を使えた方が記述が分離せずに分かりやすい。

ということで使えるようにしていこう。

Nuxt x Babel対応

正直ややこしい。

Babel は 6 と 7 の間で decorator の扱いが変わっている。今回の実験は Nuxt.js 上で行っており、2018-05 現時点で Nuxt 1.0.0 は Babel 6 依存なのでそれで動くようにしていく。

babel-preset-mobx があるのでそれを使ってみたのだが、これは動かなかった。

zwhitchcox/babel-preset-mobx: All dependencies mobx depends on

中身を見ると preset と言いつつ実際には以下の4つの plugin を利用する

  • babel-plugin-transform-class-properties
  • babel-plugin-transform-decorators-legacy
  • babel-plugin-transform-es2015-classes
  • babel-plugin-transform-regenerator

Nuxt.js の Babel の設定は vue-app ( vue + babel-preset-env ) なので本当に必要なのは以下の2つ

  • transform-decorators-legacy
  • transform-class-properties

これを plugins に追加してやれば ok

順番が大事。 decorators-legacy の次に class-properties ここら辺は Babel のイヤンなところ。エラーを見ても何が起きてるのか分からん。

おまけ VS Code 対応

decorator でずっと警告が出るので ESLint の設定がちゃんと反映されてないのかなーと思って疑ってたけど、これは VS Code で独自に出してるやつでした。

javascript.implicitProjectConfig.experimentalDecorators": false,

を true にすると解決。

まとめ

MobX は Redux の面倒くささを解消してくれるとか jQuery とも合わせられるとかいろいろ言われている最近注目の状態管理ライブラリらしいけど、「observable で明示するだけ」というシンプルさは自分の考えている、もっと広く一般的な Model としても扱いやすそうに見えた。

昨今の ViewModel フレームワークが薄いフレームワークになることで View 側の問題の本質に注力しているように、Model 側は Model 側で「Backbone.Model のようないい具合にイベント起こしてくれる程度のシンプルなやつねーかな」と思っていたところに MobX を見つけることができたので、自分の中では割と期待が大きくなってきている。

(これなら既存の jQuery を叩き直すのにも、何かの拍子に付き合わなきゃいけなくなる可能性の高い React ともなんとかやっていけるかも?)

まだテスト周りがよく分かっていないので、次はテスト周りをチェックして、よさそうなら今後は MobX に振っていくかもしれないなーという気がしてます。

*1 MVW って言ってた Angular でさえ M のことは知らんてなってる感じがすごい(小並感

*2 action はいったん置いておく


2018-05-03 [長年日記]

_ Vue.js x SSRメモ

これ読め。

Introduction · GitBook

Nuxt をちょっと使った程度で気になったところだけを抜粋。

まず prerender を検討せよ

SSR は考えることが増えるので prerender をお勧めされる。

vue-server-renderer

vue-server-renderer が String を生成してくれる。

express などサーバサイドのフレームワーク上で Vue component を利用してページ生成することができる。

VM Life Cycle が異なる

Server Side では data reactivity は不要であり、VM life cycle は以下の 2つしかない。

  • beforeCreate
  • created

だから created() で store の registerModule を route に応じて行う Nuxtでrouteに応じてVuex Storeをmodule分割する方法 - あーありがち(2018-04-19) コードを試した時に訳の分からない動きをしたのか。

custom directive は要注意

  • 多くは生DOMに依存するので動かない
  • <no-ssr> で回避できる

Nuxtegoist/vue-no-ssr: Vue component to wrap non SSR friendly components (428 bytes) で利用できる。

自分で実装する際には custom directive ではなく component にして VirtualDOM にする方法を選択すべし。

stateful な singleton を排除

サーバサイドの Node.js プロセスは長寿命であり、Vue のレベルで singleton を作ってしまうと複数のリクエストで状態が share されてしまう。

routing を vue-router に

サーバサイドフレームワークの routing を素通しにして vue-router で routing を行うことで client-side と同じ動きにできる。

サーバサイドでの dynamic component の lazy loading は危険

const Page = () => import('Page.vue')

みたいなやつ。

以前試したのだが、2つ問題があった。

一つはサーバサイドで実現するには描画開始前に非同期コンポーネントを先に解決する必要があり、これが Nuxtでrouteに応じてVuex Storeをmodule分割する方法 - あーありがち(2018-04-19) の registerModule と矛盾していた。

もう一つは route レベルでない場合は単純に難しいということ。

cf. ルーティングとコード分割 &#183; GitBook

Nuxt で route が解決されたあとに Page component レベルで dynamic component を使って route を偽装しようとするのは可能だが、問題が複雑すぎた。

Nuxt に頼らず自前で router を直接記述しつつ Vue + SSR を実現できるならイケたかもしれないが、Page component を利用しつつ routing を差し替えるには、実は routes を直接書き換えてしまえばよいことが分かっているので、今さらわざわざこの方法を採用する必要もないだろう。

JavaScriptでRubyのArray#replaceのようなことをしたい - あーありがち(2018-04-26)

nuxt.config.js の

router: {
  extendRoutes(routes, resolve) { // <- この routes ね

store と asyncData

サーバサイドでも生かすには root component の asyncData を使う。 こうすることで route から特定の処理を挟んで store に落としてこれをクライアントサイドで利用することができる。

root component の asyncData 内で store.registerModule する方法もあるが、その場合は destroyed で解放しておかないとクライアントサイドで二重に register しようとして壊れる。

ただそもそも data reactivity を component をまたいで実現しつつ persisted にしたい(要は store を localStorage などに保存したい)とかいう場合はサーバサイドで動く意味がなく、asyncData の利用は検討外と言ってよいだろう。

個人的まとめ

今回最後の store 辺りで散々苦しんだが、理屈は分かっていなかったが、動作から逆に導いて結果として正しい選択をすることはできたみたいだ。

Vue.js の SSR について理解しないままなんかできるっぽいよという理由で Nuxt で書き始めてハマって分かったことは、SSR 固有のエラーかどうかはログを見ても知識がないとさっぱり分からないということ。

ということで自分でゼロから書かなくても公式の SSR のドキュメントは読んでおこう、という当たり前の話に戻ってきました。おしまい。


2018-05-06 [長年日記]

_ ViewModel ComponentにModelを通じて世界の覗き窓を作る

先日来 JavaScript の ViewModel 系フレームワークで監視対象外のデータをどう扱うかというところに注目しているのだけど、これには理由があって、

  • VM Component の単位で影響を閉じ込めるという発想はGood
  • 単方向のデータバインディング、データフローもGood
  • VM Component が UI を司るのでどうしても VM Component が Model を mount するという形になり、かつモダン JS の世界では Rails 的にすべての Model が global に展開されるという考え方は NG
  • VM Component のどこかに Model を mount しつつ「すべてのデータを監視対象とするわけではない」という形が扱いやすそう
    • Angular で Change Detection を打ち消しまくろうとするアプローチは「気にしたくないものを網羅する」という形になるので現実的な感じがしない(まず漏れると思う)

ということで MobX にたどり着いたわけだ(Vuex Store が State 前提すぎるので)。ただし、MobX ですべて解決とは思っていなくて、自分の感覚としては

  1. VM Component が世界のすべてを知っている必要はない(むしろユーザーとのインタラクションと直接関係しないロジックで世界は満ちている)
  2. Component の外の世界はいきなり HTTP とは限定できない
  3. Component を司る Parent Component で Model を通じた外の世界の覗き窓を提供する

くらいの感じがいいのかなと思っている。

Modelを管理するModelからParent ComponentがChild Componentに世界の覗き窓を教えてあげる関係図

いったんのイメージとしては図のような感じ。

ここでロジックとして「掌握する世界がより広い」のは Model > VM Component であり、Parent Component > Child Component である。

Parent Component の初期化の際に(Vue + MobX なら fromMobx のような方法を通じて) Model を持ち、さらにこの Parent Component が Child Component を生成する際にも(例えば v-for などの記述の際に)Parent Model の「機能」や「State 相当の data」を props で渡してあげるようにすることで、より広い「世界」に対する「覗き窓」を手に入れるという具合である。

「覗き窓」というのが肝心なところで、props で渡されたものを監視しないのは無理だし、Child Component の責務は基本的には Model への直接の介入ではなく Parent Component への Event Bubbling に留めた方がよいという判断。

本当は Child Component からうまく Model の一部の機能にタッチすることができた方が便利だなと思っていたんだけど、それをやるにはやはり独自の名前空間を切って plugin として Vue Component に inject してやらないと無理っぽいので、いったんは「覗き窓」の部分だけを明文化しておこうと思った次第。

今回の話は特に目新しい話もなければ包括的な設計指針でもない。そういう話は メンテナンスしやすいVueComponentを設計するために気をつけていること などを読むとよい。

ちなみに上で言っている Parent と Child というのは React を参考にしつつ用語を整理していくとどうも

Container か否か

という切り方をするっぽい。

ここで言い方を変えると Container が Coponent 生成時に Model から覗き窓を props で渡す という感じか。

なるほどなるほど。

順番が前後したが、なぜわざわざこれを書き起こしているかということを整理しておしまいにしようと思う。

React 以降の VirtualDOM 前提の ViewModel フレームワークはあくまで VirtualDOM というごく限定的な世界のことをうまく扱うように特化しており、ViewModel フレームワークの強い制約はその中で破綻しないようにするためにはよい工夫ではあるが、ユーザーとのインタラクションは生DOM や Window, Canvas など VirtualDOM ではどうしようもない部分も含んでいるし、Model 自体は ViewModel Framework の制約とは独立して考えてよいというか独立して考えるべき、そうでないとどうしても無理が出ると言ってよいと思う。

極端な話、ViewModel Framework は Model の全体像など知らなくてよい「あくまで VirtualDOM へマップしやすい情報だけを扱っているのだ」くらいの認識にしておかないと Model 設計の際に VM Component の制約で頭が固まりそうで怖いな、と感じたので、選択肢を作り*1、それを文字に起こしておくことにした。

あとはねぇ、Rails の ActiveDecorator みたいなことをやれれば Model はさらにすっきりするんだけどねぇ…。Model に View 向けの変換が入っていると邪魔だし、かと言って Component で持つと似た記述があちこちに散らばるし、mixin を使えばできなくはないんだけど、Model をどう Component 内に持つかは Component 次第なので mixin で解決しきるのはとても厳しいわけですよ。ね。

参考

Getterの辺りに異論はあるのですが、よくまとまっていて非常に参考になります。

*1 監視しないデータとロジックの置き場所を確保すること


2018-05-07 [長年日記]

_ ESLintの--ignore-pathに注意

eslint-cli のマニュアルには --ignore-path という設定が説明されていて、

Command Line Interface - ESLint - Pluggable JavaScript linter

そこに

eslint --ignore-path .gitignore file.js

のように .gitignore と組み合わせる方法が紹介されているのだが、これはあぶない。具体的に言うと

git では無視されないけど eslint では無視されるファイルが生まれる

可能性がある。

今回自分が喰らったのは

*~

Emacsen 使いなら分かると思うんだけど、こんな「普通の glob」で踏み抜くとは思わず、なぜか意図通りに lint で警告されないとウンウン2時間も時間を消費してしまいましたとさ。

ちなみに本当に悪さしていたのは ESLint ではなく

kaelzhang/node-ignore: &#128269; node-ignore is the manager and filter for .gitignore rules, the one used by eslint, gitbook and many others.

なのだそうです。うーん、なるほど。

Tags: JavaScript

2018-05-13 [長年日記]

_ 改めてVue.jsのデータの監視方法と回避方法とコンポーネント設計

まとめ

  • Vue で監視していたのは data ではなく setter を通ったかどうかだった
    • そこは Backbone.js 時代から変わってなかった
  • Array や Object の一部を書き換える処理は setter を通らないので Vue は検知できない
  • 逆に丸ごと壊せば setter を通すことはできる
  • しかし恐らくその場合は書き換える DOM の範囲が広がり、VirtualDOM の速さのメリットがスポイルされる
  • コンポーネントを小さくしろというのはそういう意味でもあった
参考

Arrayの変更、Objectの変更はsetterを通らないので検知できない

まぁ、分かってしまえば当たり前の話なんですけど、こういう感じのコードがあると思いねぇ。

data() {
  return {
    model: {}
  }
},
methods: {
  action() {
    this.model['foo'] = 'bar'
  }
}

これ、data だからとか computed だからとか watch じゃないからとかじゃなくて、Vue では検知できないです。

これはごく簡単な理由で

this.model = {foo: 'bar'}

じゃないと setter を通らないから。つまり、

Vueは値は監視していない。setter が通知してるだけ。

そうと分かれば話は簡単。つまり Bachbone.Model の時代から本質は変わっていないわけです。あの頃は Model 側も event を通知する処理を自前で書いていましたが、その部分の記述が setter を通すことで省略できているだけ。

じゃあ検知させたければsetterを通せばよい

その通り。setter を通せばよいのです。

上のコードを本当に動くようにすると以下のようになります。

methods: {
  action() {
    let model    = JSON.parse(JSON.stringify(this.model))
    model['foo'] = 'bar'
    this.model   = model
  }
}

こうすれば setter を通るので複雑なデータ構造でも reactive system を動作させることができます。

でも関係するVirtualDOMは全書き換えになっちゃうし、影響も把握しにくい

当たり前ですね。そうなるとせっかく変更のある部分だけ書き換えることを容易に実現できる VirtualDOM の特長が犠牲になるわけです。例えば Array の変更を setter で通知してしまうと、v-for で回している部分は全書き換えになります。

さらに複雑で深い構造のデータを data として保持していて、もしそれがそのまま表示に関係している場合、その component と子の component など、多くの場所でこの data の書き換えの影響を受けます。言い換えると component が大きすぎるわけです。これは実行速度でもマイナスですが、変更もテストもやりにくくなります。*1

素直にsetterで渡せる範囲がVue componentの適切な大きさの一つの指針なのかも

今回 setter の存在に改めて行き着いて、複雑な構造のデータの変更を無理矢理検知させることをやりましたが、本当はこれは Vue 的にはオススメできない方法のような気がします。

今回気づいた setter の動作が理想的なものなのか技術的な制約に基づく妥協なのかは分かりませんが、少なくともフレームワークにとって自然な動作を満たすようにしておくというのは、他の人とコードや知見を共有する際に有効です。逆にイレギュラーな使い方はこの共有を難しくします。

「component は小さく」とはよく言われますが、もしかしたらこの setter を意識せずに使える範囲こそが、component の適切な大きさなのかもしれないという、改めて考えるとごくごく当たり前のことに気づいたという話でした。

落穂拾い

以下、うまくハマらなかったものを殴り書きして終わります。

「component は小さく」とは言いますが、不必要に作りすぎても管理が大変です。小さく保つのは大事ですが、すべてを component に分ける必要はないと思います。

props バケツリレーは確かに面倒ですが、2, 3 階層程度なら割と慣れます。

props で渡すデータの数が増えてもあんまり気にしない方がいいかも。function の引数とは意味が異なると捉え、何も考えずに で展開できる単純さを優先してもよいと思います。「責務が一つであればデータは一つでなくてもよい」という考え方。

*1 例えば自動テストをするにしても初期化、stub out が面倒です。


2018-05-17 [長年日記]

_ NuxtデフォルトサーバはCache-Controlできないので注意

まとめ

  • Nuxtは各種のサーバサイドフレームワークと組み合わせられるがNuxtだけでもサーバとして機能する
  • デフォルトで ETag は吐くが Cache-Control は吐かないのでそのまま CDN に食わせるといつまでもリフレッシュされない
    • この影響を受けるのはあくまで vue-router を経由する Page だけ
    • assets に関しては nuxt start で serve している場合は問答無用で max-age は 1 year になる*1
  • optional な http response header を追加する middleware はどうもなさげ
    • 他のフレームワークと組み合わせてその middleware を使えば追加できる(Koa で確認済み)

そもそもWebのcacheって

  • private cache と public cache がある
  • CDN は public cache
  • ブラウザは private cache
  • Cache-Control を private にすると CDN は cache できない(したがって、リクエスト数も転送量も減らない)
  • とにかく Nuxt.js は何の情報も追加しない

※ public と private の話は恥ずかしながら CDN切り替え作業における、Web版メルカリの個人情報流出の原因につきまして - Mercari Engineering Blog にあるようにメルカリのやらかしで知りました。(ちなみに Rails はデフォルトで max-age=0, private, must-revalidate を付加するので特に何も考える必要なく CDN はスルーになる。)

CDN側では基本的にmax-ageを推奨してる

基本的には Cache-Control: max-age= を設定するのがよさげ。(Surrogate-Control や s-maxage だったりもしますが、細かいことは置いておく。)

しかし Nuxt.js にはサーバ側の設定の余地がない。(SSR用の MartinLG/nuxt-custom-headers: Nuxt module to add custom headers to SSR rendered pages. というものはある。)

NuxtではETagと304しか考えてない

具体的なコードは以下の部分。

lib/core/middleware/nuxt.js

  // Add ETag header
   if (!error && this.options.render.etag) {
     const etag = generateETag(html, this.options.render.etag)
     if (fresh(req.headers, { etag })) {
       res.statusCode = 304
       res.end()
       return
     }
     res.setHeader('ETag', etag)
   }

nuxt.config.js の render.etag option を見るこの辺以外に cache を考慮しているコードは存在しない。ETag を付けるか付けないかだけ。直接ブラウザとやりとりするならこれだけでも十分かもしれないが CDN を通す場合はそうはいかない。

そこでNode.jsのサーバサイドフレームワークと組み合わせる

nuxt-community/create-nuxt-app: Create Nuxt.js App in seconds.

を使えば Express, Koa, Hapi, ... などなどからフレームワークを選べる。これらの middleware で custom header を吐いてあげればおk

具体的に効果的にcacheするには

例えば Koa であれば

を組み合わせて、./server/index.js で

const Koa = require('koa')

const app = new Koa()

require('koa-ctx-cache-control')(app)

app.use(async (ctx, next) => {
  ctx.cacheControl('max-age=0')

こんな感じ。

こうすると pages 以下の component を serve する時には Cache-Control: max-age=0 が付加されたうえで ETag でのキャッシュ内容のチェックが行われるので、origin サーバから直接転送されるものはコンテンツの鮮度チェックはしつつも転送はほぼエッヂサーバから行われるので速い。(はず)

*1 cache buster の効くファイルが生成されている


2018-05-23 [長年日記]

_ eslint-plugin-nodeでNode8を使いつつNode6で問題の起きないコードを書く

いや、実際にはちゃんとプロダクションコードを書いていないので、他にも気をつけるべきポイントはあると思うのですが、とりあえずついったーで「 Google Functions はなぜ未だに Node 6 なのか」みたいなを見かけて、

そういや、Node 8 LTS を基準に環境作ってて Node 6 要求されたら面倒くさいな

と気になったので調べてみました。

調べたのは二つで、

  • babel-preset-node6
  • eslint-plugin-node

です。

最初「node 8 で書きつつデプロイ時に node 6 で動くコードに変換できればいんじゃね?」と思って、それっぽい名前の babel-preset-node6 を試してみましたが、そんなことは無理でした。こいつの役割は V8 native で動くコードはそのままに、node6 で ES2015 compat じゃない部分を補うためのもので、目的は、transpile の高速化ですね。

で、変換でどうにかするのは早々に諦めて、じゃー lint だなと。

mysticatea/eslint-plugin-node: Additional ESLint's rules for Node.js

できました。

package.json で示すと以下のような感じで指定すれば ok.

{
  "engines": {
    "node": ">= 6" // <- ここで対象バージョンを指定
  },
  "devDependencies": {
    "eslint": "*",
    "eslint-plugin-node": "*"
  }
  "eslintConfig": {
    "plugins": ["node"],
    "extends": ["plugin:node/recommended"]
  }
}

これで例えば以下のように怒られます。

error  Async functions are not supported yet on Node >=6.0.0 \
node/no-unsupported-features

うんうん。

package.json の engines 指定の意味が増えるのはちょっとどうなのかなという気はするのですが*1、まぁとりあえず方法は分かったのでよしとします。

*1 例えば徐々にバージョンを上げていこうとなった場合などに


2018-05-26 [長年日記]

_ ES2017のasync/awaitのキソ練習

最近(2018-05現在)はライブラリで Promise 前提のものが増えてきてるしサンプルにもしれっと普通の顔で async/await が登場するので慣れておかないといけない。特に Node.js ではもう LTS である 8 (正確には 7 の途中)で標準的に使えるので、今後はもっと当たり前に登場してくる機会が増えてくることが予想される。*1

ということで今回はasync/awaitの練習。ただし await の入れ子や Promise.all で束ねるといった複雑なことは扱っていない。

※ Promise.all() の話は async/awaitやPromiseで気をつけること - あーありがち(2018-06-15) で!

またここで扱っているのはあくまで ES2017 の async/await であって他の独自に実装された async ライブラリのことでもないです。確認は素の Node 8 で行ってます。

まずはPromise

雰囲気だけならこちらをどうぞ。 jQuery.deferredを経由しつつES2015に入ったPromiseの雰囲気を味わう - あーありがち(2016-06-19)

async/await の理解には Promise の理解が欠かせない。なぜなら async/await はあくまで Promise を同期的に、手続き的な処理のように書けるだけだから。

さて。

まず Promise がやってくれるのは非同期処理を抽象化し、統一インターフェイスを提供すること。

プロミスはいつ生成されるかわからない値のプロキシのようなものであり、成功したり失敗したりする動作を扱う時の理想的なパターンです

JavaScriptにおける非同期パターン #翻訳 &#171; Tatyusa's Note

例えば Node.js 標準の callback 関数の受け取り方と jQuery のそれはルールが異なる。*2これが ES6 Promise では then(), catch() に制約されるし、Promise は 定義された状態しか持たない。そのうちの一部が fulfilled や rejected である。

つまり、「Promise を返す」とさえ言えばあとは使い方は「Promise を勉強してくれ」で済む。実装者間の共通言語、インターフェイスとして非常に優れている。(ただし学習コストは一時的には上がる。)

Promiseの例

Promise は上にあるように非同期処理に特化したものではない。

単なる即値であっても Array であっても Promise オブジェクト足り得る。例えば以下のような超短いコードも Promise オブジェクトではある。

const p1 = Promise.resolve('a')

これを console.log すると

Promise { 'a' }

と表示される。よく分からないけど 'a' を返してくれる Promise なんだなという雰囲気は伝わってくる。

実際に欲しいのは Promise じゃなくて値だ? ごもっとも。そのためには arrow function を使えば以下のように非常に短い記述で取り出せる。*3

p1.then(data => console.log(data))

async functionとawait演算子

まずは基本的なことから
  • async function は Promise を返す
    • Promise を return していない場合は自動的に補完される
    • ただし callback を Promise に変換するのは自分で書くべき

例えば上の Promise は async function を使うと以下のように書ける。

async function asyncValue() {
  return 'a'
}

正確には Promise を返す function を定義しただけなので、同じ挙動をするように手直しをすると、関数の即時実行を使って以下のようになる。

const p2 = (async function() { return 'a' })()

だるいですね。arrow function でこうしちゃう。

const p2 = (async () => 'a')()
callbackを受け取るものは明示的にPromiseに組み直す

async function は確かに自動で Promise を返すんだけど、setTimeout などの callback を受け付けるものは自分で wrap しないと resolve 時に何が渡ってくるのか分からないので、以下のように変換する。

async function lazyA() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('a')
    }, 1000)
  })
}

この場合、async はあってもなくても Promise を返す。Promise が二重化することはなくて、Promise を返すものはそのままスルーしてくれる。ということで、

Promise を返すものは慣習として async を頭に付けちゃう

という運用でよいような気がする。(だったらなくてもいんじゃね?と思うかもしれないが、以下の理由により、逆の運用の方がよいのだ。)

awaitは「解決済み」の値を取り出す演算子

解決済みまで待つので例えば上の lazyA() を例にとると

await lazyA()

とすると then() とか面倒なことしなくても 'a' を取り出すことができる。

ただしawaitは自由には使えない
  • async の中でしか使えない

逆に言うと async function の中では他の async function を await で呼べるということである。

  • 解決済みの Promise の値をオブジェクトとしてメソッドチェインはできない
(async () => {
  const a = await lazyA()
})()

は正しく 'a' になる。しかし以下のようには書けない。

(async () => {
  (await lazyA()).length
})()

つまり以下のようにも書けない。

(async () => {
  (await lazyArray()).map(e => ..)
})()

こういう書き方にしようとすると結局 Promise が顔を出してくる。

await - JavaScript | MDN

ここで

[rv] = await expression;

と書かれているのはどうも「値を受け取る人」が必要っぽい。

awaitの後ろは切る

ということで同期処理を始める前に await の結果は代入して処理は切れるようにしておく。以下のような感じ。

(async () => {
  const arr = await lazyArray()
  arr.map(e => e * 2)
})

ただし後ろに繋げるのはダメだが、前ならよい。こんな感じ。(はっきり代入ではないが、扱いとしては同じっぽい。)

let newVal = []

for ( let e on lazyArray ) {
  newVal.push(e * 2)
}

参考

Tags: JavaScript

*1 Node 8 が出たのはちょうど1年前の2017年5月です!

*2 jQuery では使いたい API によっても異なる。

*3 ただし取り出しが then() の中の callback という形になってしまう。Promise は callback 地獄を解決してくれるのでは?という気持ちになる部分でもある。


2018-05-27 [長年日記]

_ ES2015のarrow functionを雑にまとめておく

JavaScript復習2017 - importとuse strictとメソッド定義 - - あーありがち(2017-02-19)

ではあえてほとんど省略していた arrow function の自分向けメモ。

参考 13. Arrow functions

自分のバックグラウンド

CoffeeScriptは割と好き。

  • 全部「式」になるのは圧倒的に正しい
  • 特に spec を書く際に function() {} じゃなくて -> だけでサクッと書けたのは本当によかった
  • インデント? 普通守るでしょ

arrow functionのメリット

なんといっても function() {} というクソ長い文字列を見なくてよくなる

これでようやくテストのためだけに CoffeeScript が恋しくなることはなくなる。

this を気にしなくてよくなる

「気にしなくてよい」は非常に曖昧な表現だが、心理的には事実。より正確な言い方をすると Function と arrow function はスコープの扱いが異なる。

JavaScript の this は非常にやっかいな存在で、JavaScript の function はこの this を惑わす元凶なのに JavaScript では普通の function も class も function でかつ callback を使いまくるという地獄のような仕様になっていた。

var self = this

みたいなコードがそこら中に書かれてしまうのはそういう理由だ。これが arrow function では this は他の変数同様レキシカルに決まる。arrow function の中と外で this は同じものを指すことになる。

Ruby のブロックのようにかなりカジュアルに function が使えるようになったと言ってよいと思う。

arrow functionの注意点

() の省略がビミョい

() は「引数が1つの時だけ」省略できる。これが CoffeeScript のように「引数があれば省略できる」と思うとハマる。

() の省略なんてなかったんやと思った方が間違いにくいと思う。

{} の省略がビミョい

{} は省略できるが省略すると意味が変わる。

() => { statement }
() => expression

{} を省略すると return を省略できる。逆に言うと return を省略するためには {} が省略できなければいけないが、そうすると長い処理は書きにくいというか事実上は書けなくなる。

{} の省略なんてなかったんやと思った方が間違いにくいと思う。

※ どっちも短ければ短い方がよい例文では使ってしまうんだなぁ。

参考

13. Arrow functions

Tags: JavaScript