うしろのこの本ください

なんでもかきます

MAN vs. ペヤング

まるか ペヤング超超超超超超大盛ペタマックスを食べました。 GIGAMAXでした、そんなに苦しくないと思ったらペタの半分だった。

Amazon | ペヤング ソースやきそば 超超超大盛 GIGAMAX 439g 1ケース(8食入) | ペヤング | 焼きそば 通販

お腹が減っていたので出来心で買ってきたこれを食べました。あんまり写真とってなくてフードファイト系の食レポとしてはアレだけど。使うお湯が1300mlということで、小鍋一杯分くらいお湯を沸かさないと作れません。注ぐのも怖かったです。

完成品がこちら

f:id:apple19940820:20201214150250j:plain

f:id:apple19940820:20201214150313j:plain

映っているキーボードはHHKB Professional HYBRID Type-S(長い)なのでいわゆる60%キーボードです。横もでかいけど高さもあり、FILCOパームレスト3.5つ分ほどまで麺があります。4000kcal 2000kcalオーバーなので一食で一般的な成人男性の摂取量相当ですが、僕は体が大きいので摂取しなければならないカロリーも多めでした。

f:id:apple19940820:20201214150657p:plain

普段全然噛まずに食べてしまうので、だいたい長くて5分で食事が終わるんですが、こいつはちょうど20分くらいかかりました。1/4くらいまで食べたところでペヤングカップ焼きそば特有の飽きが来て、家にあったクレイジー岩塩をかけて食べました。クレイジー岩塩ってクレイジーソルトではないんですね、初めて知った。

というわけで完食、別に大きいからといって美味しいわけでもなくただのペヤングって感じでした。 

f:id:apple19940820:20201214150945j:plain

最近よくディスカバリーチャンネルでMan vs. WILDをみてるんですが、あれだけの運動をする場合は日に4000kcal摂取しないといけないらしく、お湯だけで作れるペタマックスはサバイバル向きだなと思いました。お腹いっぱいです。別の味でまともなのが出たら食べてみたいと思います。

おわり

追記

記事も少し修正しましたが、GIGAMAXみたいでペタの半分程度の大きさでした。 あれの倍となると流石に一食分じゃない気がするので昼/夜に分けることになりそうです。一回で完食するのはそれこそフードファイターっぽい。

Vue3と書き物の悩み

今自分はいろいろあってVue3に対応している技術書の執筆を行っている。(やってることは言っても良いとなってる)(はず) が、色々なジレンマやこの先どうなるか想像がつかない領域での作業になるのでかなり方針に迷いが出ていてなかなか進まない。なので、ここにVue3関連の悩みを全部書き出して整理したい。めちゃくちゃに書き殴っているので読みづらい&文字だらけなのは許してください。

読んだら何ができるようになる本なのか

そもそもComposition APIはVueのメインを張る機能というわけでなく、Vueのリアクティブなしくみを提供する1つの手段なだけであってここに重きを置くのは初学者の理解を妨げる可能性がある。というのも、Vue3の本質はProxyによるリアクティブ化とかであってOptions APIとどっちが良い?みたいな話じゃなく、用途によって使い分けなきゃいけない。(と自分は思っているけど実際のところどうするのが良いのかわからない)

で、Vue3対応の技術書がどういう存在であれば役割を果たせるのかまだイメージがついていない。個人的には以下に該当するものは本来あるべき本の姿ではない気がしている。

  • Vue2からのマイグレートを中心に据えた内容
  • Composition APIの使い方を羅列する本
  • Options APIとComposition APIの違いを示した本

本当に読みたいのは実践でVue 3をどう使っていくと良いのかという知見ベースの話であり、これらは公式ドキュメントを読んだ方がどう考えても良い。ただ技術書となると使い方を書かないわけにもいかず、Options APIとComposition APIを混ぜて話を進めると読者の混乱を招くのは目に見えているのでComposition APIとはなにか、なぜ必要か、どう使うかは書くことになるし、なんならOptions APIについても記述しておいた方が親切…みたいなことを考えているとこの本の趣旨とかやりたいことってなんだっけ?ターゲットは?ってなる。

つまりは読者にある程度の知識を持っていることを期待するか、0ベースからの知識構築を支援するかでその本の方針が決まってくるという。どちらもカバーするのは半年程度で書ける内容ではない気もしている。(そもそもv3リリースから3ヶ月もたっていない)

などと考えつつ今はComposition APIについての項を粛々と書き進めていて、なるべくドキュメントからはわからない自分がコードを追ったりして得た知見を混ぜてやっている。ドキュメントを読め、コードを読めで済む話と頭で分かっていても書いておいた方がお得なのには変わりない。とりあえず読めばComposition APIの使い方がわかるし、Options APIを使ってた人はそれとの違いとか必要性がわかる、という方針でやっているが微妙にしっくりこない。

Composition APIって何が良いんだっけ問題

そもそもComposition APIは現状のOptions APIでカバーしきれない用途(大きなプロダクトにおけるコードベースの抽象化やロジック構成の関心の凝縮)を補うための存在でComposition APIを使うために既存の資産に手を入れるほどのものじゃないかもしれない。課題が見えていてComposition APIでそれが解決できるときに利用する手段の1つというポジション。ただ、すべてをComposition APIで記述するメリットというのもあって、例えばTSフレンドリーなコードにできるとか、バンドルサイズが小さくなるとか。と考えると0ベースならOptions APIは書かないのか?と言われると実際は怪しくて、Options APIにも良い面があるので実際どう書いてくのが良いのかまだコミュニティも分かってないという感じ。

Vueの最大の特徴はSFCで、通常そのコンポーネントが最悪でも(mixinsを利用していなければ)外部にそのロジックをシェアできない = コンポーネントに閉じ込める事ができるというある種のメリットがあった。コンポーネントは自身のcomputedであらゆるロジックを管理できて外への影響を考えなくても良かったが、Composition APIはそうではなく、シェアラブルなロジックを常に管理するというコストが発生する。というかそのために使うはずなので必然的に管理コストが高くなる。

VueのSFCは処理 + テンプレートをパターンとして切り出すことが苦手で、やることが増えるほどコンポーネントが肥大化していくし、コンポネを分割するにはファイルを増やさないといけないしReactのように右クリックでFC化みたいなことができないので分割するモチベが低いというのもある。Options APIは上記のように処理を閉じて自身の関心事をcomputedプロパティで管理していればよかったが、コンポーネントの分割ラビリティが低いままsetupに全てを自由に記述できます!みたいな状況になるとデカくていろんな外界と接続している複雑難解なコンポーネント爆誕するのは割と容易に想像できる。熟練のVue技術者であればslotを活用してうまいことやれるとは思うけど、やはりファイルをわけなければいけないという制約がモチベーションを下げていてどの現場もそれほど活用できていない印象がある。(自分がそんなに多くの現場をみたわけじゃないのでここは確証がない)

ともすれば、VueにSFC内で複数のコンポーネント(テンプレートとロジックを持つ何か)が定義できない状況でsetupのような高尚な機能が取り回し切れるのか?という疑問がでてくる。Options APIの時にはReactから輸入したPresentational/Container Componentパターンが定着し、うまいことやっていたがVue3時代にも何かコンポーネントを管理するための指針が必要な気がしてならない。ということでComposition APIはReact Hooksとは結構立場も違うよなと思ってたりもします。

jsxサポートへの期待など

VueでjsxするならReactやるぜという話は無限に聞こえてくるが、そもそもReactとVueのリアクティブシステムはしくみも開発者の責任範囲も違うためorで考えるものじゃない気がする。VueがjsxをやるメリットとしてはSFCを管理しなくて済むという1点だが、なかなかに強力で上記の肥大するsetupをうまくダイエットしつつ、テンプレート + ロジックのセットを細かく分解して外界接続による影響範囲を小さくすることができる。つまりはHooksと同じようなメリットを享受しちゃおうぜという話。ただこの話もまだ調査しきれていない部分が多くて、例えばvue templateはVueインスタンスに保持している特定のフラグを見る事でレンダリングを自動で最適化してくれるがjsxを使った場合それがちゃんと動作するのかわからない。VueとReactの最大の差は開発者がレンダリングに関心を持つのか、リアクティブステートに関心を持つのかなので、jsxを使う事でレンダリングの最適化に関しても面倒を見なきゃいけないみたいになるとVue自体がそれをサポートするAPIをあまり提供していないので話が難しくなってくる。ReactにはmemoやuseEffectがあるがVueにはないので。

Vueにおいてjsxを使うことは一生オプションで、技術書でメインになれないというジレンマがあって悩ましい。Vueを学ぼうと思ったらvue templateはコンポネの管理に難があるからjsxやります!って書いてあっても意味がわからないと思うのでしょうがないけど、そうしたらどこでこの話を解消すればいいんだろう…。

本当にこの辺りはロクに調べていなくて妄想で語っている節がある。@potato4d がVue + jsx(tsx)に関して詳しいので興味がある人はフォローとかしてみるとよさそうです。

終わり

そのうち誰かに相談するかも。有識者求む

computedはどうやって読み取り専用オブジェクトを返すか

最近他でブログを書く事が増えてこちらが寂しいので最近vue-nextのコード読んで得た知見をメモする。正直読んだらわかることをブログにするのも微妙なんだけど、なんとなく書いてる。そのうち refreactive がどう動いているのかも書くつもり。

今回は computed プロパティが読み取り専用オブジェクトを返す挙動についてのメモです。

computed のしくみ

読めばわかる

vue-next/computed.ts at ffdb05e1f1b69e737e249b081e7049c74aaf25e8 · vuejs/vue-next · GitHub

で終わっても面白くないので簡単に解説します。Composition API版の computed はOptions APIとは違い、基本的にReadonlyなリアクティブオブジェクトを返却する。v3的に言うと Readonly<Ref<T>> となって返ってくる。 そのため、直接上書こうとするとランタイムで怒られる。TSを使っている場合はVeturが検知するのでエディタ上でも静的に検出できる。

import { computed } from 'vue';

export default {
  setup() {
    const count = ref(1)

    const immutable = computed(() => {
      return count * 2
    })

    console.log(immutable.value = 0) // => Write operation failed: computed value is readonly
  }
}

Options APIではこのような挙動をしていなかったので、単純に置き換えた場合壊れる可能性がある。例えば methods でcomputedの返却値を加工してからAPIリクエストとして送るものすごいお行儀の悪いコードがあるかもしれない。そうした場合読み取り専用だと対応できないので、Composition API版では引数にgetter/setterを持つオブジェクトをとることもできる。

import { computed } from 'vue';
import { api } from '~/api'

export default {
  setup() {
    const text = ref('foo')

    const mutable = computed({
      get: () => {
        return text.value;
      },
      set: (newValue) => {
        text.value = newValue;
      },
    });

    function postText() {
      mutable.value + Date.now()
      api.post('/path/to/api', mutable.value)
    }

    return {
      mutable,
      postText
    }
  }
}

set の引数型は get の返却値から自動的に推論されるため型注釈は不要。Options APIでの挙動を再現したいなら一旦こちらに書き換えると後方互換性を保つ事ができる。

これをどう実現しているかだが、早い話引数に関数が渡ってくるかオブジェクトが渡ってくるかで処理を変えるだけで、以下が実装部分。

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any
}

isFunction で引数が関数かどうかをチェックし、関数であればcomutedを生成するクラスは空のsetterで初期化される。関数でない場合オブジェクトのはずなので、その set プロパティが利用される。computed を生成するクラスは以下のようなもの。

class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true

  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    this.effect = effect(getter, {
      lazy: true,
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })

    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    if (this._dirty) {
      this._value = this.effect()
      this._dirty = false
    }
    track(toRaw(this), TrackOpTypes.GET, 'value')
    return this._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

リアクティブな値を追跡するための処理や最適化のための処理があるので少しごちゃついているが基本的には渡されたgetter/setterで初期化して ref 相当のオブジェクトを返却している。computed の関数型は引数によって返却する型を変えていて、それによって読み取り専用か上書き可能かが型で判別できるようになっている。

export declare function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>;

export declare function computed<T>(options: WritableComputedOptions<T>): WritableComputedRef<T>;

TSを使っていないとランタイムでしかわからないのでComposition APIの恩恵をちゃんと受けたいのであれば導入を検討すると良い。

余談

computedref っぽいオブジェクトを返すが、これはProxyではない。実は ref 自体もProxyを利用しておらずVueが持つイベント管理の仕組みを利用したクラスになっている。こちらの解説は別の記事でやります。

ストアーズ・ドット・ジェーピー株式会社に入社しました

5月末で株式会社ROXXを退職して、7月15日付けでストアーズ・ドット・ジェーピー株式会社に入社しました。 ストアーズ・ドット・ジェーピー株式会社はSTORESを運営する、hey株式会社を株主に持つ会社です。実質弊社がhey社。

about.stores.jp

本当は試用期間が終わってから書こうかと思ったんですが、早く書きたくなったのとチームメンバーから書いちゃえよと背中を押してもらったので書いています。 退職から入社1ヶ月までの体験を簡単に残せたら良いかなと思います。

退職

よく退職の理由として、ネガティブな表現が少なからず出てしまいそこばかり目立ってしまうことがあると思いますが(むしろ退職エントリーはそれをみにくる人が多そう)、自分の場合退職のフックになる出来事はあれど、それをカバーしようと周りの人たちがかなり動いてくれたり、仕組み化して再発しないよう取り組んでくれたお陰で不満は概ね解消されていました。

転職しようと思った一番大きな理由は、フロントエンドにおいてよりレベルの高い現場で働いてみたいという欲です。要するに自分のわがままなんですが、理解してくれたメンバー(特にCTOであり自分を拾ってくれた松本さん)には感謝しています、時期も時期ですし。

とは言いつつ自分の実力も別に大したものではなく、もっと密度濃いめのエンジニア生活をしていればなと色々悔やむところもあったので、次に入るところはフロントエンドのメンバーが今より多く、HRTech以外で(ドメイン知識で殴れない環境に身を置きたかった)、より多くのチャレンジができるような会社にしようと思いました。また社内LTやテックブログがあるか、OSSコントリビュータがいるかなど業務以外での活動が活発かどうかも重視しました。

転職活動

転職活動は概ね以下のサービスを使って行いました。

Wantedlyは使うか迷ったんですが、選んでられる身分じゃないと思ってしっかり使い倒しました。数件お声がけいただき、カジュアル面談から選考までいった会社はhey社含めて4社くらいでした。 特に面接対策などはせず、素の自分をみてもらってその会社に合うかどうかを重視していました。落ちるのはミスマッチを防げたと捉えてなるべく前向きにやっていたつもりです。(とはいえお祈り連発は心にくる)

前職が人材系の会社だったのもあり、転職活動自体身近に感じていましたが、いざ自分の番となると緊張するしお祈りで気持ちが沈むし、面接はかなり体力を使うしで改めて転職活動のしんどさが身に染みました。

STORESに決めた理由

せっかくなら共感できるミッションを持っている会社がいいなと思いました。STORESのスローガンは「Go Original」です。しっかり説明されている良い記事があるので抜粋します。

「Go Original」というのは、「独創的であれ」という意味ではなく、「自分にとっての普遍」つまり「こだわり」をさします。このこだわりは、ときに、多くの人に理解してもらえず、単なるわがままで、何をやっているんだろうと思われることもあるでしょう。でも、発揮できる場所さえあれば、その人にしか作れない唯一無二の価値に変わります。「STORES」はそのような場所でありたいと思います。

[hey] Coiney と STORES. jpをひとつの「STORES」ブランドに|naoko|note

シンプルで良いなと思いました。また自分自身が普段、自分たちが楽しいと思うことをやりつつスモールビジネスで食べている人たちに近いコミュニティにいるため、少しでもそういう人の力になれたらそれはすごい事だなと思ったのもあります。コロナの影響もあり、悩んでいる人は自分の観測している範囲ですら数え切れないほど増えていたのも、STORESなら力になれるかも?と考えた理由の一つです。

技術面は面接を通して一通りキャッチアップしました。自身の求める環境にほぼほぼマッチしたので、安心して選考を進めていました。

その他細かいところをあげると、デザインが綺麗でかわいい、メンバーがみんないい人そう、藤村さんがいる、hey社が弊社って言ってみたいなどなどです。ちなみにまだ藤村さんとは会話したことがないです。

入社後

入社に際して物凄い手厚いサポートを受けました。面接から入社手続き、入社後の諸々セットアップまでリモートで行うので、必要なものはesaにすべてまとまっています。 またSTORES自体を理解するための仕組みも充実しており、2,3日程度でおおよそ把握できるようになっています。

入社して数日間は経営陣やチームメンバー、チーム外のエンジニアと昼食しつつ雑談する時間があり、経営陣と割と喋りたい派の自分としてはかなり助かりました。おすすめショップと商品を聞いたのにその場の人間がこぞって買っていって結局自分の分はsold outで買えなかったのは謎でした。

その他

個人的にとても気に入っている制度があって、STORESを利用しているショップでの購入費用を5000円まで負担してくれるSTORE Visitというものがあります。

[hey] STORE Visitをはじめました|naoko|note

社内Slackには社員が利用した店舗のログが流れてくる他、esaにおすすめショップまとめみたいなものもあるのでそれを追っているだけで時間が溶けていきます。ドッグフーディングで経済を回せる良い仕組みだと思います。

終わりに

この状況での急な転職活動で家族含め他方に心配をかけてしまいました。東京にきた時も3日後から上京だからよろしく!みたいな感じだったし、もうアラサーだし、そろそろ落ち着きたいところ。

それから、hey社は今日も一緒に働く仲間をさがしています。 ちなみに自分はheyの第一印象は、エモい事をいうウェイ系の会社です。全然そんなことなくて、お客さんにとって最高のものを届けようという人たちが集まっています。

speakerdeck.com

このカップ麺がうまい2020夏

カップ麺はうまいのでランキングをつくった

カップ麺にはカップ型と丼型があって、内容量の問題でカップ型を好む傾向があります

TL;DR

キリマル帰ってきてくれ

1位 小笠原製粉 キリマル

f:id:apple19940820:20200819133355p:plain

しっかりと味がしつつもあっさりで汁までうまい、そしてなにより麺がうまい。庶民的なパッケージに恥じない庶民的な味わいだが、フレンチ感覚でも食える至極の逸品。 しかしローソン限定で、いまや野菜があーだこーだいうファッキンラーメンに枠を潰された。 あなたのことが好きです。戻ってきてください…ちなみにキリン味ではない。

2位 明星 ぶぶか 油そば

f:id:apple19940820:20200819134215p:plain

油そばカップ麺は数あれどこれの右に出る油そばはいない。麺がうまい。アフターコロナのお供として名声を勝ち取っている。 しかしニンニクが人を選ぶのでこの順位。うまさで言えば1位になってもおかしくない。

3位 日清 カネキッチンヌードル

f:id:apple19940820:20200819134130p:plain

キリマルと並ぶかそれ以上の出来。圧倒的あっさり醤油ラーメンで、麺がめちゃくちゃうまい。 七夕に再販されたらしいが、それ以降どうなったかはしらない。みかけたら必ず買うべき至宝。

4位 農心ジャパン 辛ラーメンカップ

f:id:apple19940820:20200819140604p:plain

辛いもの好きならおすすめの一品。麺は細麺でかなりあっさりとしていて、蒙古タンメンとはまた違った味わいがある。 辛すぎていつも完食できないが、うまいことには代わりない。袋麺も人気でいまや蒙古タンメンと並ぶ定番激辛ラーメンというポジション。

5位 日清 蒙古タンメン中本 辛旨味噌タンメン

f:id:apple19940820:20200819140232p:plain

正直辛いのはそんなに得意じゃないが、これは辛さを超えてうまさを体感できる。自分は味噌の方が好き。 うまいカップ麺食いたいな、という時に候補にあがるほどの出来。辛いもの好きは皆ここへ帰結するらしい。辛いカップ麺界のローマとも言える。

6位 スーパーカップ1.5倍 豚キムチラーメン 超やみつきブタキムオイル仕上げ

f:id:apple19940820:20200819135531p:plain

皆一度は手にしたことがあるのではないだろうか?辛いものが苦手な人が食べられる程度の辛さに弾力のある麺が光る。 このカップ麺の味を表すなら豚キム味だと思う。カップ麺好きに何味のラーメンが好き?と問うと2割くらいの確率で豚キム!と返ってくる。

7位 エースコック スーパーカップMAX しょうゆラーメン

f:id:apple19940820:20200819141043p:plain

豚キムと並ぶエースコックのエース。若干のくどさはあるが麺のうまさがカバーしているので完食までは梃摺らない。 スープを飲み干すのは後に罪悪感で死にたくなるためおすすめしない。

おわり

最近くってうまかった奴とずっと好きな奴が混ざっているので2020夏ではないなと思った

Vueのmixinsはなぜ辛かったのか

TL;DR

ES6 modules(import/export)が優秀なのでこれで良いと思う。むりに難しくする必要はない。

Vue3 からはcomposition apiを使えるけどこれもES6 modulesベースで使っていくことになる。ReactのContext的なことがしたいならinject/provideを使おう。


自分は今までいろいろな理由でvueのmixinsを避けてたけど、最近転職して強制的に目の当たりにしたため改めてmixinsの何が自分にとって辛いものなのかを書いてみようと思った。ちなみにすでに社のslackでお気持ち表明済みで、いい反応をもらったことだしなんとかしていこうとなっている(なっただけ)。

thisがmixinの中にいる

mixinsはプロパティオプションをコンポーネント間で使い回す(マージする)ためにオプション丸ごと外部定義する機能だが、普通に運用していればいずれthisが生える。コンポーネント内ではどのmixinに依存しているのかは見た目でわかるが、どのプロパティに依存しているかはわからない。特にテンプレート内ではthisすら書かないので、この値はどこからきてるんだろう?と言う時にファイル内で検索してもでてこないし、ファイルジャンプもできない(エディタによっては魔力で飛べるかも)。ts化することで解決もできるが、後述の理由でそれもしんどい。

そもそもコンポーネント間で共有したいのはオプションではなく処理そのものだし、SFCにmethodsとかcomputedとかdataとか書きたくない!って需要ではない(いるかもしれないけど)からmixinは大仰なことが多い。thisから生えるものの名前は衝突しやすく同じ名前だけど別の型&初期値というのはよくある話(formDataとかhandleSubmitとか)で、いちいちユニークになるようにプレフィックスとしてmixinごとの名前をつけることもできるが、自分は名前をつけることは境界を定めることで、一意なプレフィックスをつける時点で共有すべきものじゃないと思っているのでいい使い方とは思わない。マージされて欲しい時とそうでない時でコントロールできれば多少マシになるかもしれないけど、事故が怖い。

一番辛いのが複数のmixinを継承している時で、mixinA、mixinBがある時新規に作ったコンポーネントに諸事情でmixinCを追加して名前が衝突するパターン。極端な例で言うとCとBが衝突しないように修正すると、AとBがその影響によって衝突してしまいAも修正するが、今度はAに依存している別のコンポーネントが…みたいな状況になりうる。これはライブラリがmixinを用いている時にでも起こりうるので、単にプロダクト内で気をつけていればいいという問題でもない。影響範囲の読みにくさが大きな割れ窓になることがある。

型の付け方が冗長

こんな感じになる。せっかくVue3からtsフレンドリーになるのにこれを続けるのは辛い。

(this as IncetanceType<typeof hogeMixin>).somethingMethod()

ES6 modulesならアサーションなんてしなくてもいいし、Vue.extendしていればcomputedでもreturnの型を書けばいいだけだし低コスト&高拡張性でVSCodeにも優しい。

import { somethingMethod, somethingComputed as someCom, SomethingComputed } from './something'

computed: {
  someCom(): SomethingComputed {
    return someCom(this.val)
  }
},
methods: {
  somethingMethod(val) {
    return somethingMethod(val)
  }
}

mixinの問題である以下の点をすべて解消できる

  • どこからきている値かわかりにくい -> importで明確
  • 型付けが冗長 -> アサーションを用いずに解決できる
  • 名前の競合 -> importの時点で別名をつけられる(あんまりよくない)

また、Vue3から使えるcomposition apiは関数ベースでES modules前提で使うことになるし、そもそもsetupからmixinでマージしたプロパティにアクセスすることはできない。もとからES modulesで運用していればcompositionへの移行もスムーズにできてお得。mixinsは引き続き使えるが、役目を終えた印象。

おわり

mixinsは一見処理を抽象化して共有できる便利な機能に見えるが、DXの悪化具合が激しくて、言語機能やエディタによる支援もまともに受けられないことが足枷になっていてプロダクションで扱うには厳しいかなと思う。import/exportがポータビリティと拡張性の高さで圧勝なので、無理にVueライクになる必要はなく、Vueもjsなんだと割り切っていくことも大切な気がする。

vue3のprovide,injectで型安全なストアを作る

vue3が本格的に使われるようになってくるとvue2系では影が薄かったprovideinjectも流行ってくると思います。(願望) こいつらがいい感じに型で補強してあげるとより使いやすくなるので紹介します。

provide/injectって何

そもそもこいつらが何かという話ですが、簡単に言えば親のインスタンスにkey:valueを保持しておいてどのコンポーネントからでも取り出せるようにする関数です。 以下のように、provideで登録したkeyとvalueinjectで簡単に利用することができます。valueは登録時にリアクティブにしておけば、取り出した側での変更を追跡します。

main.ts

import { createApp, ref } from 'vue'
import App from './App.vue'

const key = 'count' // 一意なキーを生成する
const value = ref(0)

const app = createApp(App)
app.provide(key, value) // app.provideかvueからimportしたprovide()で登録
app.mount('#app')

components/ReactiveCounter.vue

<script lang="ts">
import { defineComponent, inject } from 'vue'

export default defineComponent({
  name: 'ReactiveCounter',
  setup() {
    const count = inject('count')
    const plusOne = () => count.value++
    return {
      count,
      plusOne
    }
  }
})
</script>

<template>
  <div>{{ count.value }}</div> // カウントアップされる
  <button  @click="plusOne">+</button>
</template>

この登録したvalueであるcountは別のコンポーネントから参照した際にも変更を保持しています。つまり、provide/injectは親のインスタンスに値を保持して管理するリアクティブなDI機構と言えます。

実装はここら辺ですが非常にシンプルで、currentInstanceにキー付きで値を保持しているだけです。

vue-next/apiInject.ts at 94562daea70fde33a340bb7b57746523c3660a8e · vuejs/vue-next · GitHub

ちなみにcurrentInstanceはgetCurrentInstance関数で取得することができるので、開発者がcurrentInstance.providesに対してProxyを通して任意のsetter/getterトラップを仕込むこともできそうです。(強制的にreadonlyにする、setter関数をreturnするようにする等)

provide/injectに型をつける

さて肝心の使い心地ですが、ちょっとプリミティブすぎて大人数で開発すると普通に事故りそうですね。せめてキーに基づいた登録できる値の型の制限とか、直参照禁止とかそういうレベルになってほしいところです。ここら辺のニーズはtsの恩恵を受けることで満たすことができます。

vue3からinjectとは別にInjectionKeyという型がexportされています。使い方は以下のような感じで、injectによって取り出す値の型をInjectionKeyジェネリクスとして渡し、それをkeyの型として扱うことでprovide時に登録するvalueの型をチェックするというものです。

composables/store.ts

import { InjectionKey, Ref } from 'vue'

type Count = Ref<number>

export const CountSymbol: InjectionKey<Count> = Symbol()

main.ts

import { createApp, ref } from 'vue'
import { CountSymbol } from './composables/store'
import App from './App.vue'

const value = ref('0')

const app = createApp(App)
app.provide(CountSymbol, value) // error: 型 '"0'" の引数を型 'Count' のパラメーターに割り当てることはできません。
app.mount('#app')

これで初期値を間違えて入れてしまってあらゆるDI先を破壊してしまう、みたいな事故を防げますし、vue-test-utilsでのprovideのモックも型情報からダミーデータを生成しやすくなって楽です。(この辺はまた別で記事にします) 今回はInjectionKeyを利用していますが、provideinjectジェネリクスに型を渡しても同じことは実現できるので好みの問題かもしれません。キーの発行やデータの取り出しをstoreなどに隠蔽したい場合はこの方法がおすすめです。

使う側はそのままinjectを使うよりもcomposableなカスタム関数を用意した方が都合が良いです。なお、injectはキーが存在しない場合undefinedを返すようになっているのでチェックが必要です。

余談ですが、compositionを利用した関数は以下のようにcomposablesディレクトリ配下からuseというプレフィックスをつけて提供するのがVue周辺ライブラリでは定着しつつあります。

composables/store.ts

import { inject, InjectionKey, Ref } from 'vue'

type Count = Ref<number>

export const CountSymbol: InjectionKey<Count> = Symbol()

export const useCount = () => {
  const count = inject(CountSymbol)
  if (!count) {
    throw new Error('useCount() is called without provider.')
  }
  return count
}

これで利用側でinject用のキーを扱わなくてもよくなります。

もう少し堅牢にしてみる

これだけでは若干使いやすくなった程度なので、もう少し工夫して信頼できる程度にしてみます。vue3からreadonlyという関数が提供されているのでそれを使います。readonlyはその名の通り、tsの組み込み型であるReadonlyで引数をラップしつつリアクティブ化して返却する関数です。これにより提供する値を直接上書きすることを禁止し、別途提供される更新用関数を用いて更新させるようにします。readonlyにはオブジェクトしか渡せませんが、refを通した値は{ value: count }という形でラップされるため扱うことができます。

composables/store.ts

import { inject, InjectionKey, Ref } from 'vue'

type Count = Ref<number>

export const CountSymbol: InjectionKey<Count> = Symbol()

export const useCount = () => {
  const c = inject(CountSymbol)
  if (!c) {
    throw new Error('useCount() is called without provider.')
  }

  const count = readonly(c)
  const setCountPlusOne = () => c.value++ // readonlyにする前の値を更新する
  return { count, setCountPlusOne  }
}

これでsetter経由でしかストアの値を更新できないようにできました。この辺の実装はVuexのmutation/stateの関係と似ていますね。(あちらは型で守るには工夫が必要ですが)

components/ReactiveCounter.vue

<script lang="ts">
import { defineComponent } from 'vue'
import { useCount } from './composables/store'

export default defineComponent({
  name: 'ReactiveCounter',
  setup() {
    return {
      ...useCount(),
    }
  }
})
</script>

<template>
  <div>{{ count.value }}</div> // setCountPlusOneを介してのみカウントアップされる
  <button  @click="setCountPlusOne">+</button>
  {{ count + 1 }} // Cannot assign to 'count' because it is a read-only property.
</template>

ちなみにreadonlyを通した値をテンプレート上で上書きしようとするとVeturがエラーにしてくれますが、ブラウザではコンソールでSet operation on key "xxx" failed: target is readonly.という警告を出します。エラーではないので要注意です。

テンプレートの型チェックの体験をなんとかする記事はこれ ushirock.hateblo.jp

propsにする場合の型は以下のようにすればOKです。

import { defineComponent, PropType, Ref } from 'vue'

type Prop = {
  count: Ref<number>
  setCountPlusOne: () => void
}

export default defineComponent({
  props: {
    // プリミティブな値(ref())の場合
    refValue: {
      type: Object as PropType<Readonly<Ref<number>>>
    }
    // オブジェクト(reactive())の場合
    objValue: {
      type: Object as PropType<Readonly<Prop>>
    }
  }
})

おわり

実際productionで運用するにはまだまだ足りていない部分も多いですが、かなりシンプルな実装で型に守られた小さなストアを構築できるのがprovideinjectの強みです。また、vue-test-utilsのglobal.provide関数によって簡単にデータをモックすることができる点も魅力的です。型を活かすことで様々な恩恵が受けられるため、ぜひともlang="ts"defineComponent()と合わせて使ってみてください。

それでは

【アロガラトリル】S8マスターランクスタートダッシュレポート

スタートダッシュで2桁まで見たので雑感です。(ニートでよかった)

自分の構築

f:id:apple19940820:20200702232558j:plain

ダイマックスエースとしてのアローラガラガラと物理受けかつハイドロポンプを習得して火力まで出せるようになった原種ヤドランがつよそうだったのでトリパにしました。ノーマルタイプの受けポケモンが増えることはカジュアルに潜っていてなんとなく察しており、対策として一撃ウーラオスを入れました。挑発からの強打で受けを許さず、ダイジェットエースと対面しても襷で耐えて強打+不意打ちでかなりのHPを持っていくことができます。

またトリル展開が難しそうな相手にはS7から強かったエースバーンゴリランダーの並びで制圧したかった(使いたかっただけ)ので雑に入れました。ただこの並びでさえミミッキュは出てきて仕事していくのでキョダイマックスといえど過信はできませんでした。

欠点や反省点として環境に多い悪技が一貫することとトゲキッスアシレーヌラプラスに弱いこと、トリル切れまでにガラガラが抜き切れず後続のエースバーンに全抜きを許す展開が多かったことがあります。スクショ段階ではポリゴン2はスピードスワップになっていますが、強かったもののスワップしてもガラガラの方が遅いという欠陥に気付き後にトリルに替えています。

ヤドランはマスターへ上がるまではオボンでしたが、ステロや天候ダメージで削れていると陽気珠エースバーンのダイアークを耐えなくなってしまうため食べ残しに変更しました。特性はどんかんといまだに迷っています。今のところあんまり環境に挑発がいないのと2度目のトリルを張る展開がそこそこあったりしたし再生力のままでいいか、となっています。

強かった動き

とにかくアロガラの火力が凄まじく、技範囲の広さも相まってほぼ全てのポケモンを8割以上持っていきます(HB特化ドヒドイデをA1↑ダイホロウで高乱数1)。ダイナックルを積めば相手がDMしていても1撃で葬ることができる火力を持ちます。ドサイドンと違い耐久に難があり、運用当初はかわらわりではなくホネブーメランにしてダイアースで耐久を上げていました。しかし、Dを積んだところで現環境の特殊アタッカーの技範囲は広く、DM後に生存しても何もできず突破されることが多かったのでいっそ殴る方に振り切った方が良いと判断しました。また壁展開にも弱いのでそこも補完できていい感じでした。避雷針を警戒せずに電気技を撃ってくる相手が多かったのも印象的でした(いしあたまも強いと思います)。

基本選出はウーラオス+アロガラ+ポリゴン2 or ヤドランで、悪の一貫が厳しそうならポリゴン2にしていました(後にスピードスワップじゃダメなことに気が付く)。ウーラオスで耐久ポケモンを起点にし、いけそうなタイミングでヤドランやポリ2を投げたり初手ヤドラン展開が多かったです。

ヤドランに対して初手でダイアークしてくるエースバーンが非常に多く、最大乱数でも98%なので安定してトリル+ボディプレスを決めることができて良かった反面ラプラストゲキッスアシレーヌみたいな水/格闘/炎/悪あたりを簡単に受けてくるようなポケモンは辛かったです。

強そうなポケモン/多かったポケモン

めちゃ強い

  • エースバーン(キョダイ)
  • ゴリランダー(キョダイ)
  • アシレーヌ

強い

強いかも

変わらず対戦は御三家モンスターズです。圧倒的にエースバーン、ゴリランダー、ポリゴン2が多いです。次点でマリルリヒートロトム、ウーラオスドヒドイデあたり(ヒトムはマッチが偏ってた可能性高い)。ウーラオスは水が多いですが悪も一定数いた印象でした。ジバコイルハピナスポリゴンZ等新しく使えるようになった特殊アタッカーもちょくちょく見る感じ。ドラパルトミミッキュは前環境よりは減ってましたが、みんな単純に新しいポケモンが使いたいだけな気がします。時間が経てば戻ってくるでしょう。

S8から見るようになったポケモンで強いなと感じたのはエースバーンなんですが、これはダイアークが辛い自分のパーティの都合な気がします(ほぼ100%でてくるし)。ウーラオスはやっぱり水は強くて、ダイサンダー持ちは受けるのきついです。ただゴリランダーが幅を利かせている現環境では十二分に力を発揮できてないですね。ヒートロトムとの相性が良いです。

先にも書いたアシレーヌラプラスはまだまだ強く、環境にも刺さっていると感じました。環境 = エースバーンゴリランダーポリゴン2です。ラプラスはほぼ100弱点保険で容易に草/電気技を撃てないのがずるいです。アシレーヌはS7終盤に型がめちゃくちゃ増えて読みづらいというのが強さの要因です。今後流行るかわかりませんがトノグドラを簡単にメタれるところもすごい。裏にウーラオスがいるとポリゴン2も受けが安定しません。

あんまり期待してなかったんですが、ハッサムは8世代は厳しそうです。単純にダイジェットの火力がでないのと、エースバーンが多すぎて剣舞をしている暇がないのが原因な気がします。使うのであれば鉢巻蜻蛉みたいなサイクルパーツとしての運用くらい。弱点保険はDMしても結構な確率で死ぬし相性が悪いです。

カジュアルでたくさんいたペンドラーは1回も見ませんでした…。Hぶっぱでダイマしても珠キョダイカキュウでワンパンされてしまうのが原因な気がします。

総評

ぶっちゃけずっとエースバーンにダイアーク/不意打ち撃たれてるだけだった気がするしエースバーン以外そんなにきつくなかった説はあります。ラプラスアシレーヌマリルリなどの水アタッカーの対策はヤドラン1匹だけでは辛く、ゴリランダー同時選出でなんとか凌いでいた感じなのでもう少しeasy winできるように構築自体でメタっていきたいです。ぶっちゃけこれらの裏にゴリランダーがいるだけでめちゃくちゃきつくなるのが問題なんですが…。

逆に言えば、これらを除けばトリパはかなり刺さる環境と言えます。エースバーンDM後からトリル展開+先制持ちで封殺の流れで楽に勝てる試合もあり、こちらが先制技持ちが多い都合上トリル始動+トリルアタッカー+なんかの選出が安定していたのは良かったです。トリル始動要員としてのヤドランはそこそこ優秀だけどアタッカーとしては中途半端、ドロポン外さなきゃ合格点といったところ。もう少し模索してみます。

スピードスワップポリゴン2はトリルを抜きにすればめちゃくちゃ強かったです。8世代から取得し読まれにくいのもあって、90 ~ 120族あたりのSを奪ってダウンロードで上げた火力で1.5体ほど持っていってしまう性能があります。ちなみにこのポリゴン2はHC控えめですが、ウーラオスインファイトを耐えないのでもう少し耐久に振ってもいいと思います。スピードスワップがもっと生きる調整がありそう、といった感じです。

今後新気鋭で流行りそうなのは

  • ペリトノグドラ
    • エースバーンゴリランダーに対してまあまあ強く出られる
  • ファイアロー
    • ウーラオスを問答無用でしばける
  • ドラミドロ
    • 水/フェアリーアタッカーに対して強く出られる高火力ドラゴンアタッカー
  • ジバコイル
    • 頑丈になって水/フェアリーを縛りにきそう

あたり。ジバコイルはキョダイカキュウのせいか頑丈が少なくアナライズばかりなんですが、アロガラがエースだったのもあり頑丈だったら嫌だなぁって展開の方が多かったです。増えたらそれはそれで厄介な気がします。

今回はここまで

Vueのテンプレートの型補完をキーボードショートカットでトグルできるようにする

Vueを触っていてVSCodeを使っているユーザーはもれなくVeturという拡張機能を入れている。

marketplace.visualstudio.com

VeturはVueのオプション(methods, computedなど)の補完やスニペット、thisの型検査など開発に必要な機能の詰め合わせお得パックみたいなやつだが、長らくテンプレート内で型が検知できないという状態だった。

去年の夏頃にexperimentalという形でテンプレート内の式を型検査してくれる機能が入り、vscodeの設定からチェックを入れることで利用できるが、若干問題があって今まであんまりつかってなかった。

問題1 tsじゃないSFCを開くと真っ赤になる

問題1といったがこれしかない。

lang="ts"になっていないSFCファイルを開くと全体が警告に染まってしまいうざい。これのせいでいちいちチェックを付けたり外したりする羽目になっていたので、ちゃんとは使えていなかった。

f:id:apple19940820:20200612011114p:plain

キーボードショートカットでトグルできたらマシになるかな

方法としては

  1. Toggle(拡張)を入れる
  2. keybindings.jsonに設定を記述する

でできる。ただし、トグルするたびにファイルを開き直さなきゃいけないし、ファイルを開いた時に有効になっているかどうかがパッと見でわからない(テンプレでホバーすればわかる)のでまだ開発体験が良いとは言えない。

Toggle marketplace.visualstudio.com

ショートカットの設定サンプル gist.github.com

f:id:apple19940820:20200612012713g:plain

理想的にはこれだと思う

タイミング的にlang="ts"検知後に有効になっても開き直す必要ありそうなのがアレだけどうまいことできたらいいなと思ってそのうちissue投げる。

おわり

Vue3のinject/provideでミニマルなストアを作る

ストアの型はサボった。 github.com

Vue3からinjectprovideという関数が提供されており、(機能的にはVue2のものと同じだが)コンポネ間の状態共有が簡単にできるようになった。

ドキュメント https://composition-api.vuejs.org/api.html#dependency-injection

簡単に言うと、provideにキー付きで渡したリアクティブな状態をinjectで取り出すことでどこからでも状態を参照できるようになるもの。

provideinjectsetupでのみ動作する。

例えば状態を共有したいコンポーネント2つがあった時、それ用のスコープでディレクトリを切って専用のストアを作ることが可能。 Symbolを使って一意のキーを生成し、リアクティブにしたい状態や関数をセットでprovideに渡す。

components/miniStore/index.ts

import { ref, inject, provide } from 'vue'

export const key = Symbol()

export const createStore = () => {
  const count = ref(0)
  const increment = () => count.value++

  return {
    count,
    increment,
  }
}

export const provideStore = () => {
  provide(key, createStore())
}

export const useStore = (): any => {
  return inject(key)
}

コンポネではexportされたuseStoresetupで呼び出して、使いたい値を取り出すだけ。

カウントアップする側 index.vue

<script lang="ts">
import { defineComponent } from 'vue'
import { useStore } from './miniStore'

export default defineComponent({
  setup() {
    const { increment } = useStore()
    return {
      increment
    }
  }
})
</script>

<template>
  <button @click="increment">increment</button>
</template>

カウント出す側 sub.vue

<script lang="ts">
import { defineComponent } from 'vue'
import { useStore } from './miniStore'

export default defineComponent({
  setup() {
    const { count } = useStore()
    return {
      count
    }
  }
})
</script>

<template>
  <div>{{ count }}</div>
</template>

アプリケーションのエントリーポイント等でminiStoreからproviderを呼び出し登録をする。

App.vue

<script lang="ts">
import { defineComponent } from 'vue'
import { provideStore } from './components/miniStore'
import Index from './components/index.vue'
import Sub from './components/sub.vue'

export default defineComponent({
  components: {
    Index,
    Sub,
  },
  setup() {
    provideStore()
  }
})
</script>

<template>
  <div>
    <Sub />
    <Index />
  </div>
</template>

動きます。

f:id:apple19940820:20200529214133g:plain

おわり

Vuexを使わなくてもコンポーネント間で状態の共有が簡単にできるため、ユースケースによってはこれで事足りるかもしれない。Vue 3からこういう便利関数みたいなのが沢山exportされているので結構遊べそうな雰囲気。特に@vue/reactivity周り。

vite + preactで理解するviteの依存管理

やった

github.com

本当はviteで作る超速モノレポ環境!!をやりたかったが、やっていくうちに一番大事なところが微妙になったので調査の知見を残すことにした(なった)

viteとは

github.com

そもそもviteがなにかというと、今までバンドル作ってそれをwatchしてたけどなんらかの手段でHMRが実現できたら開発時はバンドルせんでもよくね?という発想から生まれた、バンドルを作らない開発サーバである。(alt VuePressであるVitePressがやりたかっただけ説もあるけど)

仕組みとしては、ブラウザが解釈できる形になんらかの方法でファイルを加工(tsxならtscsfcなら@vue/compiler-sfc)してESM形式で読み込ませることでバンドルを作らずにブラウザに即時反映している(と理解している)。

バンドルを作らないためwebpackのhmrの比ではないほどの反映速度で、体験的にはSnowpackに近い。今回はモノレポで親側をvite、子をpackages配下からルートのnode_modules経由でシムリンクにしてコンポーネントライブラリにしたらめちゃくちゃ体験いいんじゃねって思ってやったが、後述する依存最適化周りでうーんって感じになった。

モノレポの説明とかツールとかの説明は省きます。yarn workspace: trueにしてlernaコマンド書いてるだけ。

環境構築

viteのエントリーポイントがある方のpackage.jsonにはコンポーネントライブラリとなる@vite-playgorund/baseとpreactをdepsに入れる。 preactを使う場合は--jsx-factory=hフラグをつける。

バージョン指定で"*"をつけると、パブリッシュしていなくてもモノレポ内部から同名のpackage.jsonを読み出してシムリンクを張ってくれる。これで@vite-playgorund/base側のコンポーネントをいちいちビルドせずとも直接読み出すことができ、親側で巻き込んでビルドすることができる。

app/vite-app/package.json

{
  "name": "vite-playground",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite --jsx-factory=h",
  },
  "dependencies": {
    "@vite-playground/base": "*",
    "preact": "^10.4.1"
  }
}

vite側ではindex.htmlでエントリーポイントを読み込む。

app/vite-app/index.html

<div id="app"></div>
<script type="module" src="./main.tsx"></script>

通常のReactアプリケーションと同様にmain.tsxでルートコンポーネントをマウントすれば良い。

app/vite-app/main.tsx

import { h, render } from 'preact'
import { useState } from 'preact/hooks'

// @ts-ignore
import { BaseButton } from '@vite-playground/base'

const Counter = () => {
  const [count, setCount] = useState(0)
  const increment = () => setCount(count + 1)
  const decrement = () => setCount((currentCount) => currentCount - 1)

  return (
    <div>
      <p>Count: {count}</p>
      <BaseButton text="Increment" handleClick={increment} />
      <BaseButton text="Decrement" handleClick={decrement} />
    </div>
  )
}

// @ts-ignore
render(<Counter />, document.getElementById('app'))

@vite-playground/baseからコンポーネントを読み込んでいる。re export用のindex.jsとcomponents/BaseButton.tsxがあるだけのシンプルな内部パッケージで、自力でビルドする環境はない。

packages/base/BaseButton.tsx

import { h } from 'preact'

interface Props {
  handleClick: () => void;
  text: string;
}

const BaseButton = (props: Props) => {
return <button onClick={() => props.handleClick()}>{props.text}</button>
}

export default BaseButton

この状態でyarn devすると、viteが起動してBaseButtonの解決も行われてエラーもなく動作する。

f:id:apple19940820:20200526013102g:plain

これでBaseButton.tsxを変更すれば瞬時に画面反映されるんじゃ!やっぱモノレポなんだよなぁと息巻いていたが普通に変更無視されて虚無になった。くやしいので原因を書く。

depOptimizer

depOptimizerはそもそもページロードの高速化のための関数だが、事前にバンドルされていない依存が通るとそれをビルドする。生成物はキャッシュとしてnode_modules/.vite_opt_cacheの配下に置かれる。

vite.config.jsで無効にできるが、無効にするとビルドされていないnode_modules配下のコンポーネントなどは当然読み込むことができないためエラーになる。(やたらとtext/planeを読み込んどるぞというコンソール出力を目にすることになる)

https://github.com/vitejs/vite/blob/master/src/node/depOptimizer.ts

依存がバンドルを生成しているとキャッシュは作られないので普通にvite側に反映される。ただしページは自前でリロードする必要があるし、結局子はバンドルしとるのでお得感があんまりない。

@vite-component/base側で以下のようなrollupの設定を書いてwatchしたらBaseButton.tsxの編集がちゃんと反映されるようになった。

packages/base/rollup.config.js

import preact from 'rollup-plugin-preact'
import typescript from 'rollup-plugin-typescript2'

export default {
  input: {
    ui: 'index.ts',
  },
  output: {
    dir: 'dist',
    entryFileNames: 'bundle.js',
    format: 'esm',
    sourcemap: false,
  },
  plugins: [
    typescript(),
    preact(),
  ],
  external: ['preact', 'preact-compat', 'preact-compat2'],
}

f:id:apple19940820:20200526024840g:plain

ちなみにwebpackなどでesm形式以外でビルドしていた場合、以下のような丁寧な標準出力を吐いてくれる。

vite-playground: [vite] The following dependencies seem to be CommonJS modules that
vite-playground: do not provide ESM-friendly file formats:
vite-playground:   @vite-playground/base
vite-playground: - If you are not using them in browser code, you can move them
vite-playground: to devDependencies or exclude them from this check by adding
vite-playground: them to optimizeDeps.exclude in vue.config.js.
vite-playground: - If you do intend to use them in the browser, you can try adding
vite-playground: them to optimizeDeps.commonJSWhitelist in vue.config.js but they
vite-playground: may fail to bundle or work properly. Consider choosing more modern
vite-playground: alternatives that provide ES module build formts.

最適化から外すなりホワイトリストに入れるなりなんかした方がいいぜって感じ。気を使ってexcludeに入れてくるのでキャッシュも作られない。優しすぎない? 依存側もrollup使っておけば問題はなさそう。webpackの場合はめんどくさい。

おわり

理想はviteだけで全てのモノレポ内依存が解決され超速hmrが動き出荷するときにvite build一発で終わる世界だったが、そんなに美味しい話はなかった。

今回preactを使ったのはVueでやるとcompiler-sfcの依存とかが必要でめんどくさいのと前にdepOptimizerを見たときは拡張子決め打ちで.vueを対応してなくてpreactうごくyoって言ってたから。いつのまにかvitejs/viteにリポジトリも移動してるしVueだけのものとして扱わない方針っぽい。

冒頭でも触れたけどSnowpackとは今後連携して動作するようにしていくらしいので、引き続き理想のモノレポ環境を試していきたい。

余談

vite、ヴァイトだと思ってたけどヴィッテ、ヴィーテが発音的には正しいらしい。

はやそう

f:id:apple19940820:20200526103313p:plain

Vue 3 + vue-router-nextを動かす

Vue 3のbeta版がリリースされて、あわせて周辺ツールがalphaからbetaへ作業中とのことだったのでvue-router動くかなと思ってやってみた。

github.com

github.com

以下素振りりぽじとり

github.com

プロジェクトのセットアップ

必要なものをyarn addする。

yarn add vue@next vue-router@next

あと開発用にいつもの。lint周りはお好みなので省略

yarn add -D webpack webpack-cli webpack-dev-server ts-loader vue-loader clean-webpack-plugin html-webpack-plugin typescript

webpackの設定書く

webpack.config.js

/* eslint-disable @typescript-eslint/no-var-requires */
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const webpack = require('webpack')

const outputPath = resolve(__dirname, 'dist')

/** @type {import('webpack').ConfigurationFactory} */
const config = (env = {}) => ({
  mode: env.prod ? 'production' : 'development',
  devtool: env.prod ? 'source-map' : 'inline-source-map',
  devServer: {
    contentBase: outputPath,
    historyApiFallback: true,
    hot: true,
    stats: 'minimal',
  },
  output: {
    path: outputPath,
    publicPath: '/',
    filename: 'bundle.js',
  },
  entry: [resolve(__dirname, 'src/main.ts')],
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules|vue\/src/,
        use: [
          {
            loader: 'ts-loader',
            options: {
              appendTsSuffixTo: [/\.vue$/],
              transpileOnly: true,
            },
          },
        ],
      },
      {
        test: /\.vue$/,
        use: 'vue-loader',
      },
    ],
  },
  resolve: {
    alias: {
      vue: '@vue/runtime-dom',
      '~': resolve('src'),
    },
    extensions: ['.ts', 'd.ts', '.tsx', '.js', '.vue'],
  },
  plugins: [
    new VueLoaderPlugin(),
    new HtmlWebpackPlugin({
      template: resolve(__dirname, 'src/index.html'),
    }),
    new CleanWebpackPlugin(),
  ],
})

module.exports = config

適当にエイリアスの設定とかもしておく。

package.jsonに開発鯖起動用のスクリプト書く。

"scripts": {
    "dev": "webpack-dev-server --mode=development",
}

これでsrc/main.tsをエントリポイントとしてサーバーが立ち上がるようになるはず。

composition api + vue-router

viewsにindex.htmlを適当に用意。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Poketto</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

エントリポイントを定義する。従来とは若干apiが変わっているため注意。

createAppに<router-view />が定義されているメインのコンポーネントを渡し、rootにマウントする。やっていることは今までのVueと同じ。

main.ts

import { createApp } from 'vue'
import App from '~/App.vue'
import { route } from '~/router'

const app = createApp(App)
app.use(route)
app.mount('#root')

現時点でApp.vueもrouterもないので定義していく。

App.vue

<script>
export default {
  name: 'App',
}
</script>

<template>
  <div>
    <router-view />
  </div>
</template>

ページコンポーネントを定義する。

今回はカレントパスとなるindex.vueとサブページsub.vueを作る。なんか表示したかったので適当にcomputedを利用したreadonlyなデータを吐く関数も用意した。(useAppConfig)

vue-routerは既存のthis.$routeからのアクセスではなくなり、useRouterというnamed exportされている関数を用いることでjavascript側からhistoryの操作をすることができる。 router-linkは今まで通りに使えるが、特に型が効いたりはしない。

views/useAppConfig

import { computed } from 'vue'

export const useAppConfig =
  computed(() => {
    return {
      name: 'poketto',
      version: '0.0.1',
      mode: process.env.NODE_ENV,
    }
  })

views/index.vue

<script lang="ts">
import { defineComponent } from 'vue'
import { useAppConfig } from '~/views/useAppConfig'
import { useRouter } from 'vue-router'

export default defineComponent({
  name: 'Index',
  setup() {
    const router = useRouter()
    const toSub = () => router.push({ name: 'sub' })
    return {
      useAppConfig,
      toSub
    }
  },
})
</script>

<template>
  <div>
    <p>{{ useAppConfig.name }}</p>
    <p>{{ useAppConfig.version }}</p>
    <p>{{ useAppConfig.mode }}</p>
    <router-link :to="{ name: 'sub' }">
      to sub link
    </router-link>
    <div>
      <button @click="toSub">
        to sub button
      </button>
    </div>
  </div>
</template>

views/sub.vue

<script lang="ts">
import { defineComponent } from 'vue'
import { useRouter } from 'vue-router'

export default defineComponent({
  name: 'Sub',
  setup() {
    const router = useRouter()
    const toHome = () => router.push({ path: '/' })
    return {
      toHome
    }
  },
})
</script>

<template>
  <div>
    <p>Sub Page</p>
    <router-link :to="{ path: '/' }">
      home
    </router-link>
    <div>
      <button @click="toHome">
        to sub button
      </button>
    </div>
  </div>
</template>

routerの定義。

useRouterと同様に、新しくrouter作成用の関数などがnamed exportされるようになっているため、これらを使う。

router.ts

import { createRouter, createWebHistory } from 'vue-router'
import Index from '~/views/index.vue'
import Sub from '~/views/sub.vue'

export const routerHistory = createWebHistory()

export const route = createRouter({
  history: routerHistory,
  routes: [
    {
      path: '/home',
      redirect: '/',
    },
    {
      path: '/',
      name: 'index',
      component: Index,
    },
    {
      path: '/sub',
      name: 'sub',
      component: Sub,
    },
  ],
})

ここまでできたらyarn devで動作確認。

f:id:apple19940820:20200428191849g:plain

うごく

おわり

vue-routerはまだalphaなので大きくapiが変わる可能性もあるが、現時点ではちゃんと動作する。

別でフルtsxで書いてみたけどvue-routerはなんか動かなかった。あまり追えてない。

github.com

font-awesomeをtree shakingする

メモ

Nuxtのpluginsでfont-awesome(pro)を読み込んでいたが、脳死で全部importしていてくそでかall.jsが鎮座し虚無だった。 不要なフォントをtree shakingするようにした。

befor

import '@fortawesome/fontawesome-pro/js/all'

after

import { dom, library } from '@fortawesome/fontawesome-svg-core'
import { faAngleRight } from '@fortawesome/pro-regular-svg-icons/faAngleRight'
import { faAngleLeft } from '@fortawesome/pro-regular-svg-icons/faAngleLeft'

library.add(faAngleRight, faAngleLeft)
dom.watch()

これでプロジェクト内ではdeep importされているアイコンが使え、残りはwebpackのビルドで落ちる。

before f:id:apple19940820:20200328003027p:plain

after f:id:apple19940820:20200328003218p:plain

2MBくらい減って笑顔になった。font周りは下手なjsライブラリよりもビルドサイズ肥大化に貢献してくるため注意したい。

参考 https://fontawesome.com/how-to-use/with-the-api/other/tree-shaking

Vueコンポーネントのmethodsだけテストする

VueはReactと違いコンポーネントからロジックを剥がそうとするモチベーションがあまり起きない作りだなーと感じている。SFCファイルに全部閉じ込めてしまった方が気持ち良い。しかしテストをmethodsやcomputedの入出力に対して行いたいものの、外出ししようとすると以下のようなthisを受け取る関数を作ることになったりする。テストはやっぱり関数に対して書きたいしなるべくvue-test-utilsに頼りたくない。

const validatePath = (path: string) => /^\/.+$/.test(path)


export default {
  data() {
    return {
      path: '/'
    }
  },
  methods: {
    validatePath() {
      return validatePath(this.path)
    },
  }
}

これはこれで良いが、Vueコンポーネントはマウントせずにmethodsやcomputedを分割代入で取り出すこともできる。テストファイルで以下のようにすれば、ロジックを関数に抽出していなくてもとりあえずテストを書くことができる。

// Login.vue
<template>
  <div>
    <input v-model="path" />
    <button @click="validatePath(path)" />
  </div>
</template>

export default {
  data() {
    return {
      path: '/'
    }
  },
  methods: {
    validatePath(path) {
      return validatePath(path)
    },
  }
}

// Login.spec.ts
import Login from '~/pages/Login'

describe('ログインページのテスト', () => {
  const { methods: { validatePath } } = Login as any

  test('validatePathメソッドが先頭スラッシュの文字列でTrueを返す', () => {
    const valid = validatePath('/Hello')
    expect(valid).toBeTruthy()
  })

  test('validatePathメソッドが先頭スラッシュじゃない文字列でFalseを返す', () => {
    const valid = validatePath('Hello/vue')
    expect(valid).toBeFalsy()
  })
})

ただし、前述のように常に関数として切り出しておいた方がテスト自体書きやすいしそもそもテストファイルにコンポーネントをimportしなくて済む。

コンポーネント設計時にテストを書くことが考慮されていなかった場合、methodsが副作用を持っていたりしてテスタブルじゃなくなっていることも多いためなるべく関数に分解することが大切。特にVueはthisというコンテキストに依存するライブラリなのでこれがじわじわ効いてくる。

殆どの問題はVue3で解消するが、今のうちにテスタブルな関数の勘所を抑えておくと違和感なく移行できると思う。

lernaコマンドの標準出力が微妙な時はstreamオプションをつける

TL;DR タイトル

lernaはモノレポ管理下にあるpackage.jsonのコマンドを同時に実行することができる。

lerna run --scope s-* lint

とするとpackage.jsonのnameがs-で始まるすべてのワークスペースnpm run lintが実行される。(s-はオレオレパッケージのプレフィックスです)

f:id:apple19940820:20200120131838p:plain

モノレポではワークスペースでlintの設定を統一したり、ビルドのコマンドを統一したりするのでlernaのコマンド実行は便利。

しかし問題もなくはない。

標準出力がしょぼい

一部コマンドではルートのpackage.jsonにlernaコマンドを書いて実行すると、Warningになっていても標準出力に出ずsuccess!とだけ表示される。鬱なのでなんとかしたい。

lernaからの実行では一見パーフェクトに見えるが f:id:apple19940820:20200120125453p:plain

ワークスペースに潜ってyarn generateするとバンドルサイズ周りでwebpackが警告出していたという罠 f:id:apple19940820:20200120125604p:plain

--stream オプションをつけて子の標準出力をlernaに送る

runコマンドはオプションで--streamをつけると子プロセスの出力をストリーミングすることができる。各ワークスペースのコマンド実行ログがそのまま流れるため、lernaからの実行時でも重要な標準出力を見逃すことがない。

出力がごちゃ混ぜになってやばいみたいなこともない。

f:id:apple19940820:20200120130206p:plain

他にも並列実行のオプションや実行時のパフォーマンスを測定するオプションもある。需要に応じて使ってみると良いかもしれない。