React+MobX+TypeScriptが個人的にちょうど良い

Vue使いやすい

以前はフロントエンドといえばReact+Reduxでしょ!というのが主流だったが、最近ではVue+Vuex、あるいはNuxt.jsが注目を集めてる。

私自身も、趣味・業務両方でVueを使っていて、とても使いやすいな〜と思ってる。

TypeScript使うとちょっとつらい

TypeScript自体はかなり強力で、例えばkeyofのような機能はとても魅力的だったりする。

ただ、Vueと組み合わせてTypeScriptを使った型付けをしたい場合に、2018年7月現在ではわりと厳しい感じがしている。私が調べた限りでは、Vue&VSCodeを使うことでthisからの補完などはある程度はできるが、Vuexを絡めた場合には頑張って型を書く必要がある。(Nuxtの場合はもっと大変)

このあたりは、ClassComponentを駆使する有志作成のライブラリを使うことである程度はカバーできる。

ただ、人によって好みもあると思うけど、いわゆる一般的に見られるVueの書き方とは大きく離れることになってしまうことが多く、軽い気持ちで導入するにはハードルが高いな〜、と感じてしまう。

Reactの場合

Reactの場合はTypeScriptとの相性が良く、VSCodeを組み合わせた場合もかなり使いやすい。例えば、JSXに記述するpropsも厳密にチェックすることができる。PropTypesを使わずとも事前に検証できるのも心強い。

状態管理は?

王道だとReduxを採用することになると思うが、私はつらい。(これには個人的なトラウマによる偏見もかなり含まれている。)

これはもちろん開発対象のプロダクトやチームとの相性に依る部分もあるけど、Reduxはかなり記述が増えて、重厚なイメージがある。

もっと軽い感じで使いたい。

このあたりはVuexはとても良くできていて、理解もしやすい。

MobX

mobx.js.org

というわけで、ここ最近MobXを使ってみているが、これはいいな〜という手応えを感じている。

クラスベースでの定義

たとえば、Reduxで一通りのフローを作る場合

  • ActionCreator
  • Reducer
  • Store

あたりを書くことになるが、ファイルがめっちゃ増えたり、Reducerがなんかとんでもない感じに成長してしまうみたいなことがあった。Reducerがとにかくやばい。

MobXの場合は、状態管理をしたいクラスからインスタンスを作成して利用する。こんな感じ。

import axios from 'axios';
import { action, observable, toJS } from 'mobx';

export class Item {
  @observable public name: string = '';
  @observable public image: string = '';

  @action.bound
  public async save(): Promise<void> {
    const res = await axios.post('/items', toJS(this));
    return res.data.id;
  }
}

これが非常にわかりやすく、

  • 状態は @observable 定義のインスタンス変数
  • 処理や状態変更は @action.bound のメソッドを実行

だけで、非常にシンプル。実質ただのインスタンス

そしてTypeScriptと組み合わせた場合に嬉しい効果として、クラス定義されているので、別途型定義を書く必要がない。

Reactと組み合わせる場合にはこれが非常に嬉しく、Propsにクラス定義を指定しておくだけで、そこに紐づくプロパティが全部型チェックできる。

これが出来るかどうかということより、「意識せずに簡単にできる」というのが重要で、書いてて幸せな気持ちになれた。

Reactと組み合わせる

Reactと組み合わせる場合は mobx-react を使う。
このあたりの初期化周りは react-redux を使った場合と似てる。

ただ、ストアにするインスタンスを作って突っ込んでるんだな〜っていう感じで、わりとわかりやすい。

import { Provider } from 'mobx-react';
import { Item } from './Item';

const store = {
  item: new Item()
};

const App = () => (
  <Provider {...store}>
    ...
  </Provider>
);

export default App;

参照する場合には@inject を使う。 react-reduxのときの connectに近い。

import { inject, observer } from 'mobx-react';
import * as React from 'react';
import { Item } from './Item';

interface Props {
  item: Item;
}

@inject('item') @observer
export default class ItemContainer extends React.Component<Props> {
  public render() {
    return (
      <React.Fragment>
        <div>{this.props.item.name}</div>
        <ImageComponent item={this.props.item} />
        <button onClick={() => this.props.item.save()}>save</button>
      </React.Fragment>
    );
  }
}

Actionの呼び出しは、直接インスタンス変数のメソッドを呼び出してる。

この部分。

<button onClick={() => this.props.item.save()}>save</button>

これについては賛否両論あるかもしれない。

Reduxを使っていた場合に、子コンポーネントのReduxへの直接依存を避けるためにメソッドもPropsリレーをしてコードを書いたことがあるが、往々にしてバケツリレー地獄になって、最後には辛いなという気持ちになってた。

結局、「ActionをDispatchするのだけは子コンポーネントからでもOK」みたいになることも多くて、それと同じことをやってる感じ。

ただ、TypeScriptを組み合わせることで、どのメソッドがどのコンポーネントで呼ばれているのかを静的チェックすることができるので、これが非常に強力で、Actionがどこで実行されてるのか把握でき、リファクタリングをする際もある程度ビビらずに直していける。

TypeScriptと使う

というわけで、多々TypeScriptと組み合わせた場合のことを書いた通り、TypeScriptとの相性がかなり良いっぽい。

React+MobXだけで書いた場合には、「簡単に状態管理が出来てうれしい!」くらいで終わりそうだが、TypeScriptを組み合わせることで、クラスベースであることが一気に強力な機能に変わるような印象を受けた。

まとめ

というわけで、React+MobX+TypeScriptがかなり良さそうですよっていうのをどこかに書きたくて書いてみた。

やりたいことを「意識せずに」「簡単に」できるのが大事で、そのあたりを今の所ちょうど良い具合で実現してくれるのがこの組み合わせだな〜と感じてる。

良いことばかり書いてるが、趣味で書いている程度なので、大規模プロダクトの場合や、パフォーマンスが要求される場合などはどうなの?とか色々考えれてない部分はある。

けど、書いてて楽しいので、細かいことはいいやってなってる。

同じことを感じている人がいることに期待している。

とはいえ、Vueはやっぱり使いやすい。pugとかに慣れてると、JSXでclassNameって書くたびにアアアアアア!!!!ってなるし、気をつけててもネストが深くなりがちなのはつらい。

そもそもこれを試したのは「Vue+Vuex (or Nuxt) でTypeScriptを使いたいと思ったけど辛かったから」というのが大きいので、逆に言えばそちらが快適に書けるようになれば、そちらを使うことが多くなりそうではある。

ともあれ、何が一番良いかとかではなく、都度状況を見ながら最適なものを選べるようになっていきたいと思った。

終わり。

Toyama.rbで「オフラインリアルタイムどう書く」をやってみたらとても良かった

Toyama.rb #30のイベントとして、「オフラインリアルタイムどう書く」というのをやった。

toyamarb.connpass.com

とても楽しかったし、参加者の方からも好評だった。

オフラインリアルタイムどう書く

横浜へなちょこプログラミング勉強会さんで実施されているものがオリジナルのイベントです。

趣旨としては、主催者の出した問題を皆で解きましょう!といったもの。

先日のRubyKaigi AfterPartyで@yanchaさんと同じテーブルになる機会があり、そのときに「勉強会のネタに困ってるんですよね〜」という話をしたところ、オリジナルのイベントについて教えていただいた。

Toyama.rbではもくもく会を中心に活動しているが、たまにはテーマを決めてコードを書きたいな〜とも思っていたので、内容的にもピッタリで、とても楽しそうなのでやってみることにした。

当日の流れ

基本的な流れは

  1. 問題を参加者に共有
  2. 同時に、1時間のタイマーを全員が見える形でスタート
  3. 1時間後、トランプを配る
  4. 数の小さい人から発表。解けてなくても、どう挑んだか?みたいなとこを発表する
  5. 「解けた!」という人は最後に発表してもらう

といった感じにした。

2問用意し、だいたい1問につき2時間半程度でやった。

オリジナルである横浜へなちょこプログラミング勉強会さんで開催されている際の流れでは、最後に主催者側が事前に用意していた解答例を最後に解説する形とのこと。

ただ、私自身も楽しみたい!!!という思いから、問題自体は過去問題を利用させていただき、そのかわりに、解けた人に最後に発表してもらう形としてみた。

1時間で誰も解けなかったら少し延長しようかな?と考えていたものの、2問用意していたが、2問ともに1時間できっちり解ける参加者が現れたので、とてもちょうどよかった。

1問目 / フォークじゃない

フォークじゃない 〜 横へな 2014.2.1 問題

1問目は

  • (ほぼ)全員が初挑戦
  • 勝手がよくわからないで戸惑いそう
  • 解ける、あるいは解けそうな感じが見える

のほうが良いかな〜と思っていた。

過去問題には、主にテキストのみで出題されているものと、イメージ・図形が絡む問題のざっくり2種類があった。テキストのみのほうが簡単なのかな?と思い、その中でもスーパーのレジを題材にしている問題がキャッチーな感じだったので利用させていただいた。

結果

1名のみ時間内に完成した。

私はあるバグにハマってしまい、ギリギリ間に合わなかった。その後の発表の最中で気づき、1行だけ追加したら全部通ったのでめっちゃ惜しかった、くやしい。

ちなみに、私が書いたのはこんな感じになった。

2018/7/14 Toyama.rb #30 オフラインリアルタイムどう書く · GitHub

1問目の所感

そもそも「文章の理解にめっちゃ時間が..!!」みたいになってて面白かった。

問題文自体を理解するまでがまず最初の課題だった。

また、コードを書く順番も人によって全然違っていて、一気に作ろうと思うとハマる要素が多かった。

この問題については、

  • 1-9 が来たらレジに並ぶ
  • . が来たらレジが処理をする
  • x が来たらそいつは後ろの客をせき止める

といった形で、段階的に機能追加が可能だったので、そこに気付いてTDD的に作っている参加者がわりと惜しいところ、あるいは正解まで辿り着いていたように見えた。

ただ、どんな言語でもレジに並んでいる人数を算出・計算する部分が少しややこしくなるようで、私も作ってて混乱した。

唯一解けた人は、レジの人数を数値で保持するのではなく、例えば5人なら nnnnn といった形でその分の文字を確保することで、単純に文字列長を見るだけでレジ人数が取れるようになっていた。

1時間という制約の中ではまったく思いつかなかったので、聞いてて素直に「ほ〜〜なるほど〜〜」となっていて、同じ問題でこうも違う解き方になるんだなぁと、聞いてて楽しかった。

あと、Rubyが強い。

Array周りの標準ライブラリの充実度が半端ではないので、配列操作が絡んだときのコードの簡潔さは圧倒的だった。

2問目 / 積み木の水槽

積み木の水槽 〜 横へな 2013.9.6

2問目は、画像が使われてる問題を利用させていただいた。

なんの根拠もないが、画像が絡んでるとそれだけで難しそうに見えたので、「最悪全員解けないのも面白いかな」ということで、この問題にした。

結果

当日は7名の参加者がいて、そのうち1名が中学生プログラマだったのだが、その方のみが鮮やかに解答し、その他普段お金をもらってコードを書いてる大人の皆さん全員が解けないという楽しい結果になった。私自身も1問目とは異なり、もう全然だめだった。

一応、私の書いた無残なコードを残しておく。

2018/7/14 Toyama.rb #30 オフラインリアルタイムどう書く · GitHub

2問目の所感

めっちゃ難しかった!

やってて難しいと思ったポイントは、

  • そもそもどうアプローチしていいかわからない
  • 配列のインデックスがわけわからなくなる

だった。

まず、何をどう考えたらいいのかがわからず、コードを書き始めるまでに15分くらい要してしまった。もうこの時点で私の勝敗は決していた感がある。

また、Webサービスのためにコードを書いている中では、実際のところ二次元配列をX/Y軸を意識しながらゴリゴリに回しながら操作するというのは、そこまで登場頻度は高くはないと思う。

コードを書きながら「あぁ、昔こんな感じにJavaテトリス作ったな..」と懐かしい気持ちになりつつ、完全に混乱していた。

なお、こちらの問題は、1問目よりもさらに他の人の発表を聞くのが楽しかった。

1問目よりも人によってアプローチの差異が大きく、

  • 水が入る場所を1列単位で考えて割り出す人(正解者)
  • 水が入らない死んでいる空間を割り出して、水が残る箇所を逆算しようとする人(私)
  • 全空間に水を入れてから、ライフゲーム的に水を流動させようとする人

と、多種多少だった。最後のライフゲーム的に動かすというのは微塵も思いついてなかったので、すげーなーと思いつつ、実際に完成したものがグラフィカルに描画されるのを見てみたかった。

感想

とても楽しいイベントで、かなり盛り上がった。

2問目で中学生の方が見事に正解コードを書いたあとに、コードの内容に興味津々な大人達が質問しまくって説明してもらって「おー、なるほど!!」となっていて、年齢とか仕事とか関係なくワイワイやれてる感じがあってとても良かった。

そして大人の自分としては「精進せねば...!!!」という気持ちになった。

参加者の方からも「たのしかった!」という声が聞けたので、ぜひともまた開催したいな〜と思う。

その時は、今度はオリジナルの問題を用意したいところだ。


最後に、イベントについてご紹介していただいた@yanchaさん、そして、当日の進め方等について教えていただいた鍋谷さん、ありがとうございました!

リモートワークで昼に冷凍食品食べ続けたのでオススメを紹介する

リモートワークで家で仕事をするようになってから1年以上が経過した。

リモートワーカー昼飯問題

リモートワークをしていると困るのがお昼ご飯。

選択肢としてはいろいろあって、聞いたことがあるものとしては

  • 夕飯の残りを食べる
  • 普通に自分で作る
  • 外で食べる
  • コンビニとかで買う

などがある。

冷凍食品

私は基本的に冷凍食品を食べることが多かった。

栄養が〜!という考えもあると思うけど、個人的には昼食にそもそも栄養バランスなんて期待してないので、他の部分を重要視している。

いろいろメリットがあって

  • 外に出なくていいので楽
  • ほぼ温めるだけでよく、温めてる間に洗濯物を畳んだりできる
  • 安い
  • 最近のものは美味しい

といった具合。

一年も続けると、それなりに種類も食べてきたので、個人的なオススメを紹介したい。


日清具多 辣椒担々麺

https://images-na.ssl-images-amazon.com/images/I/617l3OKqreL.jpg

出典:www.amazon.co.jp

www.nissin.com

これをオススメしたくてこの記事を書いてると言っても過言ではない。

具材のひき肉も結構たくさん入ってて、かなり美味しい。
山椒みたいのが入ってるが、もともとの状態で結構辛いので入れなくてもいいかも。

鍋を用意する必要もなく、電気ケトルとかで300ccのお湯を沸かすだけでいい。

辛いのがだめじゃない人は是非一度買って食べてみてほしい。

セブンイレブンのつけ麺

f:id:mugi1:20180623211549j:plain

チャーシュー盛つけ麺 - セブン-イレブン~近くて便利~

コンビニの冷凍食品もかなりレベルが高くて、最近のはとても美味しい。

特にこのつけ麺が美味しかった。

麺が太麺で結構食べごたえもあるし、つけだれも魚介ベースな感じが美味しい。

これも、つけだれ用のお湯を少し沸かすだけでいいので作るのが楽でいい。

炒飯の極み[えび五目XO醤]

f:id:mugi1:20180623212814p:plain

出典:www.amazon.co.jp

炒飯の極み[えび五目XO醤]|冷凍食品|商品情報|マルハニチロ株式会社

冷凍チャーハンは人によって好みが分かれそうだな〜とは思うけど、個人的にはこれをオススメしたい。

他の冷凍チャーハンはチャーシューだったり、ガツンとした味付けみたいなところで推してるのが多いが、これは珍しくエビがゴロゴロ入ってるタイプ。

(私がエビ好きだっていうのもある)

海鮮感が嫌いじゃない人は食べてもらうといいかも。

ただ、以前Toyama.rbの年末LTでソニックガーデンの木原さんも同じことを言っていた記憶があるが、基本的に冷凍のチャーハンはだいたいうまい。

とりあえずチャーハンがあれば安心感がある。

他のだとこのへんが安定して美味しい。

具材の大きさとか本格っぽさであればコレがかなりすごかった。

が、これは八角の香りが結構強く、人によっては好き嫌いがあるかも。

私は大丈夫だったのでたまに食べてる。

オーマイプレミアム 海の幸のペスカトーレ

f:id:mugi1:20180623220249p:plain

出典:www.amazon.co.jp

www.nippn.co.jp

パスタであればこれがオススメだった。

パッケージを見てもらうとわかるが、具材がすごい。ゴロゴロ入ってる。

そして、これはトレーに入った状態で冷凍されているので、お皿がいらないのもありがたい。洗い物が減る。

味も美味しい。

まとめ

というわけで、

  • 麺(汁あり)
  • つけ麺
  • チャーハン
  • パスタ

の4つを書いてみた。これらは本当によく食べているので、食べたことがなければ是非1回食べてみてほしい。

が、どれだけ美味しかろうとも同じものを食べていると飽きる。

なので、なんかオススメあったら教えてください。(これを言いたかった)

リモートワークしてて気になってきたのでデスク周りのケーブルをスッキリさせた

気がつけばリモートワークを始めてから1年経過していた。

最初にデスク周りは一旦整理したけど、1年もあると色々モノも増えたりしてきて、結構ゴチャゴチャとしてきていた。

特にケーブル周りがひどく、たとえば電源ケーブル等についてはケーブルボックスに収納して見えなくしていたけど、

  • ケーブルボックスから距離があるもの
  • ケーブル自体の長さが中途半端なもの
  • 定期的に抜き差しするためボックスにしまいたくないもの

については適当な状態で、長い間見てみぬフリをしてた。

(以前の状態) f:id:mugi1:20180408151130j:plain

この写真の通り、そのまま垂れ下がってるような感じ。

見えないからいいんじゃね?という考えもできそうだけど、

  • 掃除機をかけるときに邪魔
  • 仕事中に足にケーブルがあたって鬱陶しい
  • なんかイヤ

という問題があるので、気合を入れて整理することにしてみた。

最終的にどうなったか

こうなった

f:id:mugi1:20180408164808j:plain

めっちゃスッキリした。

ポリシー

  • がんばりたくない(コードを1本ずつ固定するとかはやりたくない)
  • ケーブルが増えたり減ったりしても楽に対応したい
  • 雑でいいので、いい感じになってほしい

使ったもの

ネオジム磁石

  • 100均で売ってる

f:id:mugi1:20180408135140j:plain

ステンレスシート

  • ホームセンターに売ってる
  • 1枚700円くらい
  • 買った磁石を持っていって、ちゃんとくっつくものを買う

f:id:mugi1:20180408135058j:plain

ワイヤーネット

  • 100均
  • こちらも磁石がくっつくものを買う
  • ⇣こんな感じのやつ

アイリスオーヤマ メッシュ パネル MPP-6090 ブラック

マスキングテープ

  • 100均
  • なんでもいいけど、磁石より幅があるといい

f:id:mugi1:20180408223200j:plain

普段外さないケーブル類の整理

ワイヤーネットを使って、ケーブルをすべて机の裏に抑えつける。
固定自体は磁石のパワーで雑にやれる。

ステンレスシートを机の裏に貼る

それっぽい場所に適当に貼る

f:id:mugi1:20180408222821j:plain

ワイヤーネットにネオジム磁石を貼り付ける

適当でいい。磁石が強いので勝手にくっつく

f:id:mugi1:20180408154502j:plain

ケーブルをガッと掴んでガッとワイヤーネットに乗せる

ガッとつかんで

f:id:mugi1:20180408222854j:plain

ガッとのせる

f:id:mugi1:20180408222913j:plain

ワイヤーネットごとステンレスシートにガッとくっつける

ネオジム磁石の力によって、適当にやってもいい感じにケーブルごと止まる

f:id:mugi1:20180408222933j:plain

ケーブルが増えたら、ワイヤーネットを引っ張れば簡単に外れるので、 そこにガッとケーブルをねじこんでガッと止めれば終わり。簡単。

取り外すことのあるケーブルの固定

これはお好みでやればいいと思う。

私は磁石が余ったのでやった。

マスキングテープで磁石を挟む

こんなのいくつか作る

f:id:mugi1:20180408223023j:plain

  • マスキングテープ2切れの粘着面で磁石を挟む
    (粘着面同士を貼る。ステンレスシートに磁力でくっつける。)
  • 両端に1個ずつ磁石をいれる
  • どうせ見えないし適当でいい

ステンレスシートにくっつけてケーブルを挟む

こんな感じ。

f:id:mugi1:20180408223047j:plain

簡単に取れるので、ケーブルの取り外しも簡単。

f:id:mugi1:20180408223032j:plain

壁にケーブル止めるやつを作る

これでもまだ磁石が余った。(買いすぎ)

なので、もう何も考えずに、椅子から手がとどく位置の壁にマスキングテープで貼った。

イヤホンとかマイクロUSBケーブルとかがくっついて便利。

f:id:mugi1:20180408231521j:plain

以上です

だいぶスッキリした。

簡単なのでオススメです。

SlackBotをGUIでポチポチしていい感じにするツールを作ってる

年末年始の休みぐらいから、SlackBotをGUIでポチポチするだけでいい感じに作れるようなツールを作ってる。

画面でポチポチ作ると

f:id:mugi1:20180128144304p:plain

こんな感じで動く

f:id:mugi1:20180128144705p:plain

とりあえずある程度動くようになったのでリポジトリ公開しといた

github.com

動機

SlackBotについて考えてみたときに、以下のようなことだけ簡単に実現できて自由に組み合わせることが出来れば、全部じゃなくとも80%くらいのやりたいことは実現できるんじゃね?とある日思った。

  • 実行のトリガー
    • 何らかのキーワードで実行(@bot 次の予定 みたいな)
    • スケジュールして一定時間ごとに実行
    • 外部からのWebRequestで処理を実行
  • トリガーで処理すること
    • どっかにWebRequestを投げて結果をもらう
  • 最後にやること
    • どっかにWebRequestを投げる(投げ捨てる)
    • 何か結果をチャンネルに返す

すでにBotを構築するツールはいっぱいあると思うけど、GUIでポチポチやって簡単に作れたら楽だな〜というのと、なんでもいいのでNuxt.jsを使ってなんか作りたかった、というのがあって、作ってみることにした。

使ったもの

  • Nuxt.js
  • express : APIサーバ兼、Nuxt.jsの動作用
  • mongo : データ保存用。雑にmongooseでアクセスしてる
  • element-ui : 見た目はこいつに任せてる
  • axios : 外部へのWebRequest発行用
  • node-schedule : スケジュール実行用

でだいたい動いてる。

深い意味や理由はまったくないが、全部jsで書いてみようと思ったのでexpressとかを使ってる。

概念的な話

以下のような名前をつけて取り扱ってる

  • Flow : 一連の処理の流れ
  • Trigger : Flowを起動するためのきっかけとなるもの。Flowにつき1つ。
  • Action : Triggerによって実行される一連の処理。Flowにつき任意数。
  • Finisher : Flowの最後にやりたい処理。Flowにつき1つ。

ActionやFinisherは1つまえに実行されたものから何らかのパラメータを受け取って自由に扱う。

たとえば上に貼った画像の例ではToyama.rbのイベント情報をチャンネルに返してるが、これはActionで行ったaxiosのレスポンスが次のFinisherに渡されてるので可能になってる。

(ちなみにFinisherではlodash#template形式で雑にテンプレートを書けるようにしてみた)

Slackへの依存をいかに切り出すか

Slack以外のツールにも対応させたいな〜みたいな思いがあったので、Slackに依存する部分とのやりとりはすべて events 経由で間接的に行ってる。

具体的には、Botの動作全体を管理するクラスでは

module.exports = class Bot {
  constructor (client) {
    this.client = client;
  }

  start (configuration) {
    this.client.start(configuration);
  }

  stop () {
    this.client.stop();
  }

  bindMessageTriggerFlow (flow) {
    this.client.on('messageReceived', (data) => {
      if (!data.actionName || data.actionName !== flow.trigger.data.word) return;

      this.executeAllActions(flow, data);
    });
  }


  ...

みたいにして、client がSlackかどうかは関係なく、特定のinterfaceを持ってるかどうかだけ気にするようにしてみた。

client自体は

const EventEmitter = require('events').EventEmitter;

module.exports = class BaseClient extends EventEmitter {
  start (state) {
    throw new Error('not implements error');
  }

  stop () {
    throw new Error('not implements error');
  }

  emitMessageReceived (message = {}) {
    this.emit('messageReceived', message);
  }

  sendMessage () {
    throw new Error('not implements error');
  }
};

みたいなBaseクラスを継承させていて、Chatサービスに依存する実装はサブクラスに丸投げしてる。

定期実行の方法

cron形式で定期実行できるようにしたかったんだけど、その実現に node-schedule を利用させてもらった。

github.com

さくっと動いて便利だった。

気をつけないといけなかったのが、当たり前だけど、Botを止めてもScheduleは止まらない。そのあたりの実装忘れてて、テスト用チャンネルが地獄みたいなことになってた。

f:id:mugi1:20180128152458p:plain

これから作ろうと思ってるもの

  • Herokuのデプロイボタン置く
  • Finisherの概念って実はいらないのでActionに統合する
    • 冷静に考えると最後のActionをFinisherにすればそれでいいので、Finisherなんていらんかったんや、ってこのエントリ書いてて気づいた。
  • ランダムなURLを発行して、それをTriggerにする
  • 認証
    • URLさえ知ってればokみたいになってるので、超簡易的な認証をつけときたい
  • 他のChatツールに対応したい
    • なんとかなると信じてる

まだ途中だけど、作ってる感想としてはNuxt.jsがとても楽でいい感じ。 router書かなくて良いのは最高です。

Nuxt.jsのbuild&startが何をやってるのかをコードから追ってみる

以前、Herokuで簡易的なNuxt.jsのアプリケーションのSSRを確認するエントリーを書いた。

mugi1.hateblo.jp

書いたはいいけど、で...

なんでこれ動いてんの?

デプロイできたやった〜と言いたいところだけど、あまりにも動作がブラックボックスすぎるので、一体何がどうなっているのかを調べてみることにする。

Herokuへのデプロイ

node.jsアプリケーションをHerokuにデプロイする際は、デフォルトではpacakge.jsonに記載のstart scriptが実行される。

(参考: Deploying Node.js Apps on Heroku | Heroku Dev Center)

そしてさらに、デプロイ時には以下の設定を追加する必要がある。

 "heroku-postbuild": "npm run build"

つまりデプロイ時には以下の流れでビルドが実行されていることがわかる。

  1. nuxt build
  2. nuxt start

オフィシャルのガイドによると

  • nuxt build - アプリケーションを Webpack でビルドし、JS と CSS をプロダクション向けにミニファイします
  • nuxt start - プロダクションモードでサーバーを起動します(nuxt build 後に実行してください)

とのこと。

調べること

というわけで、以下を調べてみることにする。

  • next build では何をビルドして何を作っているのか
  • nuxt start では何を起動していて、どこでSSRをやっているのか。

nuxt スクリプト

そもそも nuxt というスクリプトのコードを確認してみる。

nuxt.js/nuxt at dev · nuxt/nuxt.js · GitHub

色々やってるけど、とりあえず

const bin = join(__dirname, 'nuxt-' + cmd)

という箇所があり、cmd の部分に指定したオプションが渡る。

つまり、nuxt-buildnuxt-start といったスクリプトが実行される。

nuxt-build

nuxt.js/nuxt-build at dev · nuxt/nuxt.js · GitHub

オプションのロードなどの後に、以下のようなコードがある。

...

if (options.mode !== 'spa') {
  // Build for SSR app
  builder.build()
    .then(() => debug('Building done'))
    .catch((err) => {
      console.error(err)
      process.exit(1)
    })
} else {
  ...

options.mode !== 'spa' という記述があるが、Nuxt.jsではSSRを使わないアプリケーションの構築もサポートしているので、その場合にビルド方法が変わるものと思われる。

https://ja.nuxtjs.org/guide#シングルページアプリケーション-spa-

デフォルトではSSRは有効なので、どうやら builder.build() がビルドの本体のようだ。

実態は Builderというクラス。コード的にはこのあたり。

https://github.com/nuxt/nuxt.js/blob/dev/lib/builder/builder.js#L107

処理の中で特に重要そうなものだけピックアップすると

  • this.nuxt.ready()
  • this.generateRoutesAndFiles()
  • this.webpackBuild()

かな〜と思われる。

this.nuxt.ready()

実体は Nuxt#ready が該当する。

https://github.com/nuxt/nuxt.js/blob/dev/lib/core/nuxt.js#L51

内部ではさらにModuleContainer#readyRenderer#readyを呼び出しており、デフォルトのものに加えて、任意に追加されたミドルウェアやモジュールをロードしている。

this.generateRoutesAndFiles()

webpackでビルドする前のファイルをテンプレートをもとに生成している。

例えば、pages/ の配下にコンポーネントを突っ込むだけで自動的にルーティングが生成されるのは、このあたりで動的にファイルを取得した上でテンプレートファイルからvue-router向けのjsファイルが出力されている。

pages/ に限らず、Nuxt.jsがいい感じにに解決してくれているものの多くがlib/app 配下にテンプレートとして用意されているっぽいので、生成されるファイルの元となるものが見たい場合はここを参照するとよさそう。

https://github.com/nuxt/nuxt.js/tree/dev/lib/app

this.webpackBuild()

みんな大好きwebpackビルド。

lib/builder/webpackの配下にwebpackビルドで必要なコンフィグファイルがまとまっている。

クライアント側とサーバ側でエントリーファイルが異なるので、client.config.jsserver.config.js が存在しており、それぞれについてビルドが実行される。

最終的な成果物はデフォルトだとプロジェクトルートから見て .nuxt/dist 配下に出力される。

nuxt-start

nuxt.js/nuxt-start at dev · nuxt/nuxt.js · GitHub

Nuxt#listen を実行している。

実行前に .nuxt/dist や、SSRを行う場合には .nuxt/dist/server-bundle.json の存在をチェックしているので、あらかじめ nuxt build が実行されていないと落ちる。

Nuxt#listen

https://github.com/nuxt/nuxt.js/blob/dev/lib/core/nuxt.js#L124

this.renderer.app.listen を実行しており、この app の実体は connectインスタンス。これで実際にNuxt.jsアプリケーションが待ち受け状態になる。

github.com

サーバサイドでのルーティングについては、初期化の過程でRenderer#readyがコールされており、そこからさらに辿ると以下のコードが見つけられる。

// Finally use nuxtMiddleware
this.useMiddleware(this.nuxtMiddleware.bind(this))

useMiddlewareは、connectインスタンスに対してMiddlewareを登録している。

// Use middleware
this.app.use(path, handler)

nuxtMiddlewareについては、内部で renderRoute をコールしており、この中で実際にレンダリングしたHTMLを返却している。

// Call renderToString from the bundleRenderer and generate the HTML (will update the context as well)
let APP = await this.bundleRenderer.renderToString(context)

bundleRendererにはvue-server-renderer#createBundleRenderer で生成されたバンドルレンダラがセットされており、.nuxt/dist/server-bundle.json が元になっている。細かいところまで追えてないが、URLとのマッピングはこのへんで解決しているものと思われる。

というわけで

コードを追ってみることで、事前のビルドで何をしていて、どのあたりでサーバサイドでHTMLを生成しているのかはなんとなくわかった。(ような気がする。)

深く読み込むというよりかは、大体の流れを追うようにコードを見ていったので、もしかすると一部で間違いとかもあるかもしれない。そうだったらスイマセン。

とりあえず、自分の中で完全に闇だった部分が少しわかったのは良かった。

感想としては、コードを見ていると「よくこんなの作ったな...」という気持ちになってきた。OSSコントリビュータに感謝しよう。

parcelでビルド結果から別ファイルを出力するプラグインをつくるまでの道のり

Misoca Advent Calendar 2017 - Qiita : 18日目のエントリーです。


以前、RailsでWebpackerをparcelに置き換えるにはどうしたら?という記事をQiitaに書いた。

qiita.com

この中で、しれっと以下のようなことやっている。

  • ビルド結果からfingerprint付きのファイルを出力したい
  • マッピングが定義されたjsonファイルを吐き出したい
  • というプラグインを作った

実際に作ったプラグインはこれ。

github.com

コード自体は大したことないが、作成する際にドキュメントに書かれている方法だけでは厳しく、parcel自体のコードを読むことでそれっぽいものが作れたという経緯があった。なかなか面白かったので完成までの道のりを記事にしてみる。

欲しかったもの

元の記事にも書いてある内容と同じになりますが、簡単に言うと entry.jsというファイルをビルドした場合に、

  • entry-aUjc10lx83jjXwlechKSaxa3Le9kSjfe.js
  • manifest.json

のようなファイルを吐き出し、manifest.jsonには

{
  "entry.js": "entry-aUjc10lx83jjXwlechKSaxa3Le9kSjfe.js"
}

が記録されている、みたいなことをしたい。

プロダクションで利用する際に、ビルド結果に差分があった場合にキャッシュではなく新しいファイルを参照するように、fingerprintが付与されたファイルを配信したいが、都度scriptタグのsrcを書き換えるのはメンテナンスのコストが高く、別途jsonファイルにマッピングを記録し読み替えるようにしたい、といった具合。

つまり、

  • View上 : 何らかのヘルパー経由でentry.jsを指定
  • 何らかのヘルパー : manifest.json経由で実際に読み込むjsファイルを解決
  • ブラウザ : entry-aUjc10lx83jjXwlechKSaxa3Le9kSjfe.js をロード

といった流れになる。

parcel自体の機能で実現できないか?

そのような機能はまだサポートされていないように見えた。

オフィシャルドキュメントのProductionビルドのあたりを見てみると、htmlからjsをロードした状態とし、htmlをビルド対象とすることでjsのファイル名をユニークなものにしてくれる、いった機能はすでに提供されているっぽい。

SPAを構築する場合などではこれで充分そうだけど、今回の目的とは少し違うようだった。残念。

参考: https://parceljs.org/production.html#set-the-public-url-to-serve-on

自力でなんとかする

ポイントとしては、 「ビルド結果が出力された後の時点で実行したい」といった一点かと思う。

parcelのオフィシャルドキュメントを見ていると、3つの拡張方法が存在するようで、以下のように理解した。

  • Asset
    • ファイルに応じてパース、依存解決、transformなどを行い、変換後コードを生成する
  • Packager
    • 出力ファイルタイプに応じて、Assetで変換したものを取り纏めて実際のファイル出力を行う
  • Plugin
    • parcelビルドを非標準的な方法で自由に拡張する場合に利用する

単純に扱えるファイルの種類を増やしたい場合などはAsset/Packagerを利用するのが正しいアプローチのようだが、今回実現したいことにはファイルタイプは関係なく、純粋に出力結果となるファイルのみを考慮したいので、Pluginを利用するのが正しそう。

プラグインを作る

まずはドキュメントに記載されているプラグインの説明を見てみる。 (https://parceljs.org/plugins.html#plugin-api)

Parcel plugins are very simple. They are simply modules that export a single function, which is called by Parcel automatically during initialization. The function receives as input the Bundler object, and can do configuration such as registering asset types and packagers.

Publish this package on npm using the parcel-plugin- prefix, and it will be automatically detected and loaded as described below.

記載されているサンプルコードも抜粋。

module.exports = function (bundler) {
  bundler.addAssetType('ext', require.resolve('./MyAsset'));
  bundler.addPackager('foo', require.resolve('./MyPackager'));
};

上記を見るからに、単一のプラグインをpackage.jsonに追加するだけで、一気に対応するサポートファイルを追加したりすることができるようだ。

これはこれでとても便利そうですが、Asset/Packageは今回は使わないので、違うアプローチを取る必要がありそう。

では、引数で与えられている bundler が一体何者なのかを追ってみる。

Bundler

実際のコードは以下。

https://github.com/parcel-bundler/parcel/blob/master/src/Bundler.js

Bundlerクラス自体は、CLIから実行した際にインスタンス化されており、外部からはbundleメソッドのみがコールされる。

parcelによるビルド処理本体の起点であり、本体といっても良さそう。

ただ、addAssetType/addPackagerといったメソッドは存在するが、他にそれらしいメソッドは存在せず。どうやってビルド終了時に処理を実行させれば良いのだろう?

そこで、ポイントとなるのはクラス定義となる部分。

class Bundler extends EventEmitter {

nodejsを利用している方ならお馴染みの、EventEmitterを継承している。 (https://nodejs.org/api/events.html)

つまり、プラグインが受け取るbundlerは何らかのタイミングでイベントをemitしてくる(かもしれない)ことがわかる。

emitしている箇所

以下の2つが該当する。

  • this.emit('buildEnd'); - code
  • this.emit('bundled', bundle); - code

ビルド終了時に処理をしたいので、buildEndかな?と思ったけど、buildEndは成功・エラーにかかわらずemitされてしまう模様。

引数のbundleを参照することで引数に指定した出力ディレクトリなども動的に解決することが出来るので、bundledを参照するのが正しそう。

最終的に完成したプラグインのコード

というわけで、上記を元に作ったプラグインが以下。それほど大きくないので全部貼ります。

const path = require('path');
const hasha = require('hasha');
const fs = require('fs');

module.exports = function (bundler) {
  const logger = bundler.logger;

  const readManifestJson = (path) => {
    if (!fs.existsSync(path)) {
      logger.status('✨', 'create manifest file');
      return {};
    };

    logger.status('🖊', 'update manifest file');

    try {
      return JSON.parse(fs.readFileSync(path, 'utf8'));
    } catch(e) {
      logger.error('manifest file is invalid');
      throw e; 
    }
  };

  bundler.on('bundled', (bundle) => {
    const dir = path.dirname(bundle.name);

    logger.status('📦', 'PackageManifestPlugin');
    logger.status('📁', `     dir : ${dir}`);

    const f = bundle.name;
    const hash = hasha.fromFileSync(f, { algorithm: 'sha256' });
    const ext = path.extname(f);
    const basename = path.basename(f, ext);
    const hashFile = path.join(dir, `${basename}-${hash}${ext}`);

    logger.status('✓', `  bundle : ${bundle.name}`);
    logger.status('✓', `        => ${hashFile}`);    

    // create hash included bundle file
    fs.createReadStream(f).pipe(fs.createWriteStream(path.resolve(dir, hashFile)));

    const manifestPath = path.resolve(dir, 'parcel-manifest.json');

    logger.status('📄', `manifest : ${manifestPath}`);

    const manifestValue = readManifestJson(manifestPath);
    manifestValue[path.relative(dir, f)] = path.relative(dir, hashFile);

    fs.writeFileSync(manifestPath, JSON.stringify(manifestValue));
  });
};

parcel-plugin-bundle-manifest/BundleManifestPlugin.js at master · mugi-uno/parcel-plugin-bundle-manifest · GitHub

だいたい以下のような流れで処理してる

  • bundler.on('bundled', ... が処理の起点
  • bundle.name で出力されたファイルへのパスを取得する
  • ファイル内容からhashを作成し、fingerprint付きのファイルをコピーする
  • manifest.jsonファイルを作成する。すでに存在する場合はマージする
  • manifest.jsonファイルを書き込む

終わってみれば大した内容ではない。

あとは、名称を parcel-plugin-xxx といった形でnpmに公開すると、parcelビルド時にpackage.jsonのdependenciesを元に自動的にプラグインが実行されるようになる。

最初は parcel-plugin-manifest とかにしようかと思ったけど、私が一等地っぽい名前を取ってしまうのは如何なものかと思ったので、parcel-plugin-bundle-manifestにした。

www.npmjs.com

作ってみて

EventEmitter周りとかは後々追記される可能性もありそうだけど、ドキュメントだけではなく、コードも見るのが大事だよな〜と改めて感じた。

プラグインを作る過程の中で、基本的な動作とかもなんとなく理解できるようになってくるので、とても勉強になってよかった。

manifestファイルの吐き出しについては、もしかすると将来的にparcel自体の機能としてサポートされるようになるかもしれない。自分としてもそちらのほうが安心なので、ぜひ期待したい。

parcelについて

少しズレるけど、parcel自体についての個人的な所感。(個人的なものです)

まるで設定なしで魔法の如く動いているように理解してしまいそうだけど、実際にはwebpackのloaderなりpluginなりがやってくれていたことを裏側で隠蔽して意識しないで良いようにしてくれているだけなので、フロントエンドのビルド周りで理解しないといけないことが格別減るというわけではないと思う。

(知らないまま使っても良さそうだけど、ハマったときに即死しそう。)

でも、ゼロコンフィグで動かそうというのはとても快適に感じたし、cssファイルをエントリーに取ってビルドできる点などは、webpackのExtractTextPluginにつらみを感じていたので「そうそう、これがやりたかったんだよ!!」という気持ちになった。

これからも出来ることは増えていくだろうし、webpackに取って代わる日が来るのも有り得るかもしれない。

とりあえずどうなっても良いように、webpackへのガッツリ依存している部分を少しずつ外しておくことで、いつか幸せになれる日が来ることを信じている。


さて、明日は弊社代表の@toyoshiがラズパイで頑張った話を書いてくれるようです。

前回のエントリが400近いブックマーク数になるほど読まれていたので、今回も注目ですね!!