ぬまのそこ

namazuのゆるいエンジニアブログ

LL2019で登壇してきました

今日は、日記。 

LL2019 (LeanLanguage2019) ( https://llevent.connpass.com/event/139342/ ) の"システムを作り直してみたよ!" というセッションで pixivをSPAにしている話という題でピクシブとして登壇してきました。

スライドは、

www.slideshare.net

になります。 10分枠だったので、なんとか喋りたいことを喋るために削りに削ってすごい速度で喋るっていうことになりました。。。

もともと登壇とか人前に出るの苦手ちゃんなので、わざわざ自分から登壇しにいったりなんてしないんですが、社の人に「なまず君よろ!」って投げられたのでやることに。

結果は自分としては、まぁよかったかなという感じです。 登壇のクオリティとしてはまだまだ全然だなって感じでしたが。

pixivは新卒で入って、ほぼずっとフロントをいじってきたんですが(サーバサイドも書くけど責任領域はフロント)、これまでのやったことを謙遜せず発表できたのがよかったかなと。

新卒で入ったときはフロントエンドに関して言うと、技術水準が低すぎて、そもそも外に出したらヤバいのではということで何も語れなかったんですが、ここまでいろいろとやってきてなんとか業界にやってってるんですよ。 というメッセージを発信できる状態になったかなという感じです。 

これをずっとやりたかったのでやりたいことの最初のフェーズは達成したかなと、それを対外発表という形で喋れてよかったなと。 ほぼ一人でなんとかしたしな。。。

もちろんpixivのフロントは業界のスタート地点に立っただけであって、先進性や独自性がアピールできる環境にはないので、これからそういったことをやっていく必要があるわけなんですが、スタート地点に立つまでの自分の1年を喋れたってのがよかったです。

基本外にプレゼンしにいったりなんてしないので、スライドとか最初に作ったやつは酷かったのですが、社のひとしっかり見てくれてなんとか喋れました。 ほんと感謝感激雨あられですね。 

今回の発表はpixivがこれまで外に言わなかったようなことを率直に言った、つまりなんとかする事ができるという状態に来ていることを示すので、 これからやっていかなければなという自分への戒めになりました。

他のセッションとか

とてもおもしろいセッションたくさんで楽しめるイベントでした。 ドキュメンテーションについて語るセッションを聴講していたのですが、参考になるなぁと。 私の喋ったシステムを作り直してみたよセッションもいろいろな作り直しの事例と当たった苦労が伝わってきてとても面白かったです。

そういえば

抽選でいただきました。 見てみたいなと思っていた本だったので手入って最高です! ありがとうございます:bow:

VueプロジェクトでのStorybook運用Tips

久しぶり更新。

半年ほどVue.jsのプロジェクトでStorybookを利用していたのですが、最近ガイドラインの整備や開発フロー等を固めることができ、現状割とよしなにできてきている(と個人的に思っている)ので、これまでの運用のTipsをまとめようと思います。

今回はVueのプロジェクトでStorybookを運用するにフォーカスを当てて、どうしてきたか、なぜそうしたのかをまとめます。

誰かに少しでもカオスなVueプロジェクトの整備に役立つ情報が提供できれば幸い。

TL;DR

先にまとめ。

  • StoryはvueファイルのSFCで定義する(記法を増やさない)
  • 規約を固める, 定義の仕方が一意に定まるようにする, 規約はテストする
  • Story定義と対象コンポーネントの乖離を防ぐためlintやtestをする
  • VuexはStoryごとにインスタンスを分ける(Story間で依存を出さない)
  • Storybookのメリットを最大化させるためにDOMSnapshot, ビジュアル回帰テスト等を組み合わせる
  • 常に最新のStorybookをエンジニア以外も確認できる状態にする

1. StoryはvueファイルのSFCで定義する(記法を増やさない)

Storybookの導入直後はjsxでStoryを定義していました。しかしこれはうまくいきませんでした。

Story対象のコンポーネントSFC(.vue)で定義しており、プロジェクトのVueファイルは基本SFCで書かれていました。 そのためStory定義だけJSXが現れる ということになりました。 

その結果、

  • webpackの設定が本家ビルドと乖離
  • JSXのためにlint等の設定も増える, vue-eslint-pluginの恩恵をStory定義は受けられない
  • whitespaceの扱い等vue-template-compilerを使わないことで感覚と違う挙動が起きる
  • 記法が違うのに慣れずVueは書けるがStoryはすぐ定義できないメンバ

等の問題が置きました。

特にStory定義をコードで見るとコンポーネントの使い方を示すものとしたかったのにもかかわらずv-modelv-on等が実際のコードと離れてしまいよくわからないという問題が致命的でした。

この問題に対処するため, SFCでStory定義を書くようにしました。

( v-model等がJSXでは厄介だったため, 一時期ComponentOptionのtemplate内にテンプレート文字列で定義なんてことも行われてカオスになりました )

だいぶ端折っていますが, 大体こんな感じのコードで実現します。

import { storiesOf } from '@storybook/vue';

// storybook/components/button/button-base.stories.vue みたいなSFCファイルを Components|Button/ButtonBase というStoryで登録する.
const vueContext = require.context('./stories', true, /\.stories\.vue$/);
vueContext.keys().forEach(path => {
  const pathStr = path.toString();
  const matches = pathStr.match(/^\.\/(.+)\/(.+).stories\.vue$/);
  const contextPaths = matches[1].split('/')
  const rootPath = contextPaths.shift();
  const kind = `${rootPath}|${contextPaths.join('/')}`;
  if (contextPaths.length === 0) {
    throw new Error('RootSeparatorの直下にStoryは配置できません');
  }
  const componentFileName = toCamelfromKebab(matches[2]);

  const stories = storiesOf(kind, module);

  const story = vueContext(path);
  const component = story.default;

 stories.add(`${componentFileName}`, () => ({
    ...component
  }));
});

require.contextstories.vue(この拡張子がstory定義ということにしています)を探してきて, そのファイルパスに応じてStory定義の階層を切ります。 そしてファイル名をStory名として追加します。

実際ではこのコードに加え, parameters(Storybookのデコレータで使うアレ)をSFC定義から流すとか , a/b/cパターンがあるコンポーネントのStory定義のための方法とか, knob対応とか諸々をし運用しています。 基本SFC側に任意のオプションを定義できるようにしてやってそれをこの登録スクリプトでよしなにしています。

2. 規約を固める, 定義の仕方が一意に定まるようにする

SFCで書くと割と書き方が一意になりますが、機械生成とかしたいのと考えることを減らしたいので、現在以下の規約を定めています。 定めた規約はテストしています。

コンポーネントに対し必ず1つの同名のStory定義ファイル(.stories.vue)が存在すること

ButtonBase.vue というファイルのStory定義は, ButtonBase.stories.vue というファイルであることを保証しています。

コンポーネントの使い方は?といったときに同名でファイル探索して探せるのと, Storybook上でも同名で定義されるのでStorybook上でみてコードベースで見てということが簡単にできます。

もし対象コンポーネントがpropsによってパターン分けがあって, それをStorybook上で定義したいときはpropsProviderというプロパティを生やしていて, そのキーでstoryの場合分けがされます.

// propsProvider  { [key: string]: () => { testOnly: boolean, [key:string]: any } }
  // Aパターン, Bパターン みたいなstoryをsfc内で複数定義可能にする.
  if (component.propsProvider !== undefined) {
    Object.entries(component.propsProvider).forEach(([patternName, data]) => {
      const storyModifier = patternName !== 'default' ? `[${patternName}]` : '';
      const providedProps = Object.fromEntries(Object.entries(data)
        .map(([name, value]) => [
          name,
          {
            default: value
          }
        ]));

      stories.add(`${componentFileName}${storyModifier}`, () => ({
        ...componentOptions,
        props: {
          ...providedProps,
          ...componentOptions.props
        }
      }));
    });

こんな感じです.

コードベースでコンポーネントの存在する階層とStory定義上の階層は一致すること

例えばcomponents/button/ButtonBase.vue みたいなコンポーネントがあります。 これをstorybook/stories/components/button/ButtonBase.stories.vue みたいなディレクトリに置くということです。 (components/modal/ModalBase.vue => storybook/stories/components/modal/ModalBase.vue) (実際はもっとコンポーネントの階層分けは厳密ですが, 対応関係を満たします。

1でsfcの置き場からStorybookでの階層を決めているとしたので, 全て対応するようにルールにしています。

これは, stories.vueファイルを対象としたテストで検証しています.

3. Story定義と対象コンポーネントの乖離を防ぐためlintやtestをする

コンポーネントにpropsが増やされたり消されたりして, propsが変化したときstory定義を古いままそのままにされると, すぐに乖離が起きてStory定義がコンポーネントの使い方や挙動を示すものとして使い物にならなくなります。

これを防ぐため, 必ず一致することを検証しています。 方法としてはstoryshotsが走るときvnodeを走査しもしズレてたらテストを落とします たぶんLintでやったほうが良いですが、Lint力がないので動的解析しています(fnComponentは諦めました)

こんな感じのをmixinでstoryshtosが走るときに混ぜています. 混ぜるとvnodeを見てアレならテストが落ちます.

export function testMounted() {
  const { story } = this.$options.$storyContext; // storyContextをvueインスタンスに混ぜて流しています
  const storyComponentName = story;
  const findStoryComponentNode = node => {
    // fnComponent
    if (node.fnOptions && node.fnOptions.name === storyComponentName) {
      return node;
    }
    if (node.componentOptions && node.componentOptions.tag === storyComponentName) {
      return node;
    }

    for (let i = 0; node.children && i < node.children.length; i++) {
      const test = findStoryComponentNode(node.children[i]);
      if (test !== null) {
        return test;
      }
    }
    return null;
  };
  const componentNode = findStoryComponentNode(this._vnode);

  // テスト対象コンポーネントがvNodeTreeに存在している
  expect(componentNode).not.toBeNull();

  // コンポーネント側で定義されたpropsをstory定義が満たしている
  if (!componentNode.componentOptions) {
    // fnのとき(やり方がわからない... )
  } else {
    const defineProps = componentNode.componentInstance.$options._propKeys;
    const propsData = Object.keys(componentNode.componentOptions.propsData || {});
    defineProps &&
      defineProps.length > 0 &&
      expect(propsData).toEqual(expect.arrayContaining(defineProps));
  }
}

上記以外にも、片方の名前が勝手に変わったとかディレクトリが移動したーとかで乖離することもありますが、双方が一致していないとテストが落ちるようにしています。 

レビューで指摘するのは辛い感じになる上普通に漏れるので、ルールにしたものは全てテストがある状態を心がけてます。

4. VuexはStoryごとにインスタンスを分ける

moduleのstateを関数形式で書いてなかったりしたとか, 全StoryでVuexのインスタンスが同じとかだと, たまに変な挙動をする上, storyshot等のテストが意図せず順序依存していたーなんてことになります。(2,3度引っかかりました)

Vuexのインスタンスはまぁ当然として, 読み込むとVue.prototypeとかを汚染するコンポーネント/ ディレクティブを利用したStory定義とか注意が必要です。

定義時にそれぞれ依存をなくすように心がけましょう。

5. DOMSnapshot, ビジュアル回帰テスト等をStorybookと合わせ活用する

Storybookの運用はやらないのと比較して当然コストがかかります。 なので得られるメリットを最大化しましょう。

うちではStoryshotsを利用したDOMSnapshotや, puppetterを利用してビジュアル回帰テスト等をしています。 ( カバレッジがちゃんとあると ) これらのCIが通るだけで割と安心できるので良いです。

6. 常に最新のStorybookをエンジニア以外も確認できる状態にする

しましょう。 やることはCIでビルドしてどっかにuploadするだけです。

特にデザイナーや、開発をやってない人に、ほらと見せられる状態にしておくのは価値があります。 webに上がっているStorybookを見せて, デザイナーとこのパターンがーみたいな話をすると話が早かったりしたことは何度も。

さいご

こんな感じで半年ほど運用しています。

うちは途中からStorybookを導入したのですが、今ではいわゆる共通コンポーネントレイヤーは100%Story定義がある状態をプロジェクトで実現できました。 Story定義もコードベースと乖離せずフレッシュな状態を保てています。 

Storybook入れる前はテスト等なく何もわからない変更がデプロイされていたのが、DOMSnapshotのDiffとカバレッジである程度安心できる状態。

引き続き頑張っていきたいです。

Vue.jsでcomputed以外のプロパティでcomputedっぽいことをする

標題のとおりです。

したいこと

export default {
    name: 'hoge-component',
    computed: {
        hoge ( ) {
          return 'xxx';
        }
    },
    meta: { // これがcomputedのように動いて欲しい
       reactiveComputed ( ) { // => 'xxx'
          return this.hoge;
       }
    }
}

普段はcomputed内だけに定義できる算出ゲッターをmeta内に定義できるようにします。

動的に変わりまくるtitleやpageContext的なものを扱うためにcomputedではないレイヤーにcomputedがほしかった。

やりかた

  1. globalMixinを使い,createdでmetaが入ってきたコンポーネントが作られたことを検出します。
  2. metaのそれぞれの関数にコンポーネントをbindして作った関数で別のVueインスタンスを作ります
  3. 新しく作るVueインスタンスのcomputedに登録してWatcherで追跡してもらいます
watcher = undefined;
Vue.mixin({
  created() {
    if (this.$options.meta) {
      const self = this;
      const meta = this.$options.meta;
      const computed = Object.entries(meta).reduce((acc, current) => {
        if (typeof current[1] === "function") {
          acc[current[0]] = current[1].bind(self);
        }
        return acc;
      }, {});
      watcher = new Vue({
        computed,
        watch: {
          hoge(to, from) {
            console.log(to);
          }
        }
      });
    }
  },
  beforeDestroy () {
    if (watcher) {
      watcher.$destroy();
    }
  }
});

これでok. リアクティブな値を好きなスコープに作るのはVue.util.defineReactiveで作れますが, computedを作るにはWatcherをnewするひつようがあるので別のVueインスタンスを作るしかなさげかなと。 Vue.prototype._initをいじればcomputedにmetaの中身をマージできますが, そうすると同名でcomputedに置けなくなってしまう。

こうすればインスタンスが増えてしまうけれどcomputedを作れます。

コード

Vue Template - CodeSandbox

さらに

computedと同時に同名のwatchを定義して, 別につくったVueインスタンスのdataを置き換えるようにします。 そうすると散らばったコンポーネントのmetaが一箇所にまとまります.

このVueインスタンスのdataを元に色々と処理をします。  そうすると各ページコンポーネントにmetaってプロパティで定義した算出ゲッターが返す値をdocument.titleに設定できるようになります。

べんり!

vue.js: functional componentとscoped css

最近はFF14をしたり、某社でフロントやphpを書いたりしています。 Vueをメインに書いていますが、その中でちょっとハマった点があったのでまとめがてら記載します。

vue (2.5.17) にはvueloaderのscoped cssをfunctional componentでうまく扱えない挙動があります。 その対策をメモ的に書いておきます。

現象

vueにはインスタンスを持たない軽量なコンポーネントとしてfunctionalというコンポーネントを作ることができるようなりました。 これは画期的で、大量に使うようなbaseコンポーネントを負荷を恐れず簡単に分離し大量に用いることができます。

そこで私はいろいろと共通化できる処理を細かい単位でfunctional-componentとして分離して共通化していっているのですが、その過程でちょっと困ったことになりました。 functionalでない普通のコンポーネントからfunctionalなコンポーネントを利用するとscoped cssやクラスがうまく付きませんでした。

まとめると、

です。

コードにすれば、

<template functional>
  <span v-bind="data.attrs">{{ props.text }}</span>
</template>

<script>
export default {
  name: 'functional-component',
  props: {
    text: String
  }
}
</script>

のようなfunctilnal componentに対して

<template>
  <div>
     <functilnal-component text='hoge' class='red'/>
  </div>
<template>

<style scoped>
   .hoge {
       color: red;
   }
</style>

をしたとき文字が赤色にならないということです。

原因

  1. 静的なクラスとして与えたhogeはfunctional-componentでは通常コンポーネントで行われるクラスのマージが行われないため,functional-componentのルート要素spanに付与されません。

  2. vueのscoped cssdata-v-xxxxのような属性をrender時に全ての要素に付与し, cssを属性セレクタ付きに書き換えることで実現していますが, 上記のように利用した場合 functional-component側でv属性はマージされず, functional-component側だけのv属性がDOMに付与されるため, cssセレクタに一致せずスタイルが適用されません。

これはclassや属性をマージする一般的なhowtoである、functionalコンポーネント側でv-bind=data.attrsのようなことをしても解決しません。

対策

  1. functionalコンポーネントでは(というよりvnodeでは)静的なクラスとして付与された文字列はdata.staticClassで参照可能ですそのためv-bind:class="data.staticClass"とすることで普通に渡したクラスをfunctionalコンポーネント側の要素に付与することができます。

  2. これは厄介ですがvue-template-loaderによって実現されるscopedCssに使うscopeId (data-v-xxx)はvueインスタンス$options._scope_idで取得することができます。 つまりこれをバインドしてやればfunctional-compnent側でdata-v-xxxな値をマージすることができます。

これらを合わせると上記の問題を解決するfunctional-componentのtemplateは以下になります。

<template functional>
  <span
    v-bind="(() => { const res = data.attrs; /* scopeIdをマージする */ res[`${parent.$options._scopeId}`] = ''; return res; })()"
    :class="data.staticClass"
  >{{ props.text }}</span>
</template>

このようにv-bindとclassbindingを書いておくと, functional-componentを自然に普通のコンポーネントから利用できるようになります。

v-bindの中が黒魔術

他にはcss-moduleを利用するなどのscopedcssに頼らない根本的な解決策がありますが、私はscoped-cssがシンプルでvueらしく好きなので一旦これで進むことにしました。

日記記事を別ブログで書くようにしました

日記記事を別のブログで書くようにしました

実は昨日、記事の投稿ボタンを押すのが数分遅れて、このブログ の連続投稿が途切れてしまいました

それにしても、ここ最近、毎日書くブログにアウトプットと日記が混在していることをもどかしく感じていました。 純粋な技術エントリと全くそうじゃないエントリが混ざっているのはなんだかなーと。  日記も書きにくくなるし、技術エントリも書きにくくなる。 良くないなぁって思っていました。

そこで連続投稿が途切れたのを切っ掛けに、日記ブログを新しく作り、そこで日記を書くことにしました。 Wordpressでやろうかと思ったのですが、Wordpressを上げられるサーバがない。 個人でAWSにいくつかサーバは持っているのですが自分のWordpress用にはあまり使いたくないなという状況でした。

結局はてなブログでささっと別のブログを開設しました。 今年中にあるタイミングでWordpressに移して自鯖運用にしたいなぁと考えています。 新しい日記ブログは

namazu-diary.hatenablog.com

こちらになります。 これまでこのブログにあった全ての記事を移行させました。

また、このブログから、日記記事に偏っているなという記事は全て削除しました。 

今後はなにか共有できそうな成果を出したらこのブログに書いていくというスタイルにしたいと思います。

最近のWineはエロゲ動くの?

wineでエロゲ動かす

私のノートPCは以前Windowsだったのだが、開発に使うには嫌気がさしてLinuxにしてしまった。 開発効率が上昇してとても良い感じだが、一点問題が。エロゲができない。

そもそも開発用のノートPCでやるなよという話なのだが、ノートPCしか手元にない状態で過ごしていると、なんとなくやりたくなったときにできなくてつらい。

家のメインPCにRDPしてやってもいいのだが、メインのデスクトップを常時付けていると電気代もアレなので大抵切ってしまっている。

この前実家に帰省したときにノートPCしかなくて辛くなって、なんとかしてLinuxノートでエロゲ動かそうと頑張ってみた。

Linuxwindowsアプリを動かすならwine。 昔に使ったときは、使い物にならなかったような記憶があるが、いまはどうなんだろうって。

環境

  • VAIO Z(VJZ13A1)
  • ArchLinux

wineインストール

Wine - ArchWiki

偉大なるArchWikiに沿ってやっていく。 適当に推奨されたものを突っ込んでいった。 実際に動かす上で足りない物もでてきたので、それらは随時いれた。

足りない奴は実行時にsoがないよーってログが吐かれるので、そこから推察してインストールすれば上手くいく。

私はサウンド関係を入れ忘れてて、音がでなかった。 必要パッケージをpacmanから放り込んだらすんなりいった。 (wikiにリスト載ってるんだからちゃんといれとけと言う話だが。。。)

動かしてみた

とりあえず試してみたのは『feng: 妹のセイイキ』 OPが好き。 三部作のなかでは一番好きですね。 イカもいいんだけど。 

ISOをノートPCにマウントして、wineコマンドでexeを起動。 インストーラはさらっと立ち上がって、普通にインストールできた。

起動したら、普通に画面出た。 普通に動くし問題なさそう!

f:id:kituneko-510:20180303230815p:plain

一つ動いたくらいだと偶然っぽいので、もうひとつやってみた。

ユニゾンシフト: 』 癒やしゲー。

こちらはインストールして起動したら、画面が出てこなかった。 おそらくDirectXとかあのあたりではないだろうかだろうと予想。 winetricsを入れて、DirectX周りのコンポーネント入れたらちゃんと立ち上がってくれた。 

f:id:kituneko-510:20180303231038p:plain

いいかんじ。

結論

二作品しか試していないけど、Linuxでもエロゲはなんだかんだ動くんじゃないでしょうか。 

どうしてもノートPCでエロゲがしたいときはなんとかできる。

どうでもいいはなし

きょうはみみのひで、みみけっとでした。 ぼーっと歩いてたらなんか色々買っていて、財布が空になってた。 こわい。

涼屋のうすい本、キャラが可愛くてなんとなーく欲しかったのですが、25分で全部完売しててゲットならずでした。  みみけの涼屋ってあんなんだったけ。。。 

昼は品川のラーメンを食べてきて家に戻ったのですが、書類がどっかいったことに気づき、家中探し回り、大学にまで探しに行った、大学で無事見つけられたのだが、その後また小川流でラーメン食べて返ってきた。 1日に二回もラーメンを食べてしまった。。。

エロゲと美少女ゲームの定義って結構差があるよね。

 

Nginxのauth_requestがPOST時にTimeoutする

Nginxのauth_request

Module ngx_http_auth_request_module

これをつかってSSO認証を制御していた。

上手いこと動いていた感じだったのだが、この認証下にある WordpressのログインがTimeoutするという問題が発生。 

調査していくと、wordpressへのPOST時のauth_requestでタイムアウトが発生していることがわかった。 認証サーバ側のNginxのログは見ると408を返していた。

解決策

auth_requestをProxyに流すときには、

  • Content-Length を空にして
  • proxy_request_body を off にすること。
 proxy_pass_request_body off;
 proxy_set_header Content-Length "";

公式にちゃんと書いてある()

行きつくまで

認証サーバ自体はRailsでできたちょっとしたものなので処理の中でタイムアウトが起きるはずはなかった。 またRailsには一切ログが吐き出されていなかった。

Railsのログに記載されないというのはどういうこと???ってなった。 低レイヤでどうなってるのか調べようと、unicornにstraceを掛けたり、tcpdumpしたり色々していた。 その過程でそこそこしょうもない知識を得た。

結局、nginxがGETを受けたまま、そのまま固まっているのでは?感がnginx-workerプロセスのstraceから見えてきた。 ヘッダを全部見返していったら、大本の設定でContent-Lengthが抜けてたことに気づいた。

他のauth_requestの設定をしてある場所はちゃんと公式の見本通りにヘッダがセットしてあったのに、ここだけ抜けていたのはなんでなんだろう。。。 Gitのログ曰く、最近テンプレートを書き直したときに、このヘッダだけ削っていたっぽい。 ふぇぇって感じ。

どうでもいいはなし

Nginxのことも含めて、

とか絶賛しょうもない日だった。

タスクが山積みなのでなんとかしないと。。。 明日大学に行って進捗を出すかなぁ。。。