一休.com Developers Blog

一休のエンジニア、デザイナー、ディレクターが情報を発信していきます

Storybook を自作して「フロントエンドビルドが遅い問題」に立ち向かう

この記事は一休.comアドベントカレンダー2018の10日目です。

qiita.com


こんにちは。レストラン事業部の所澤です。 WEBアプリケーションエンジニアとしてフロント/サーバー問わず機能開発を行っています。

今回は一休.com レストランの旧アプリケーションのフロントエンド開発環境改善についてお話します。

※ この記事の執筆時点では以下の内容は master に取り込まれていません。同僚のフロントエンドエンジニア(ガチ勢)から何か指摘があったら追記します。

この記事の概要

  • 一休.com レストランの旧WEBアプリケーション(以下 restaurant1 )はなぜかフロントエンドビルドが超遅い。
  • Storybook のようなアプリと切り離された、高速でビルドできる環境があればもっと快適に開発できるのではないか?
  • Storybook だと vue-devtools が使えないので Storybook (の最低限の機能を持つ)小さいアプリケーションを作ってみた。

一休.com レストランの開発環境

新アーキテクチャへの移行状況

何度かこのブログでも取り上げていますが、現在(2018年12月)、一休.com レストランは新旧ふたつのWEBアプリケーションが並行する形で運用されています。 古くから稼働している VBScript で書かれたアプリケーションは restaurant1、リニューアル後の Python で書かれた新しいアプリケーションは restaurant2 と呼ばれています。 着々と restaurant2 への移行は順調に進んでいますが、依然として restaurant1 の上に乗っている部分も多く残っています。

f:id:shozawa:20181210145104p:plain:w300

たとえばスマートフォン版の店舗トップ画面は機能追加の機会が多いページですが、まだ restaurant2 への移行が完了していません。 新アーキテクチャだけを触ればOK、という状態まではまだ少しかかりそうだというのが現状です。

restaurant1 のフロントエンドについて

さて、"レガシー"などと言ってしまいましたが、実はフロントエンドに限って言えば旧アプリケーションもそこまで古くはありません。 jQueryでゴリゴリ書かれたページもありますが、主要ページに関しては ES2015+ と Vue.js で開発できる環境が整っています。 restaurant2 のピカピカのコードに比べると若干見劣りはしますが十分モダンだと言っていいでしょう。

問題は、フロントエンドビルドが とてつもなく遅い ことです。 フルビルドに時間がかかることに関して良いとしても watch しているときの差分ビルドも 1分以上 かかります。(Core i7 の開発機で)

遅い原因は特定できていないのですが、

  • アセットの肥大化
  • そもそも Windows だとビルドが遅い( restaurant1 は ASP で書かれているので Windows 必須です)
  • セキュリティのために入れているファイル監視ソフトの相性の問題

など、いろいろな可能性が考えられます。

さて本来であれば根本原因を特定して解決するのが筋ですが、どうにも問題の切り分けがうまくいかないので別の解決方法を考えてみます。

restaurant1 に Storybook を導入してみる

いままではユニットテストを書いてブラウザを使った動作確認の回数を減らし、なるべくこの問題を意識しなくて済むように気をつけていました。 しかしやはり新規のコンポーネントを0から作るときやデザインの微調整をする際はどうしてもビルドの遅さが気になります。

restaurant2 や宿泊のサイトでは 既に Storybook を導入済みだったこともあり、"開発用の Playground として" restaurant1 にも Storybook を入れてみようと試してみました。

f:id:shozawa:20181210150410p:plain:w300

※ restaurant2 では"デザイナーとの協働をスムーズにする" という目的で Storybook が活用されています。詳細はまたいつか。

Storybook で十分、か?

さて冒頭でも書きましたが結局 Storybook を導入することは見送りました。理由は vue-devtools が使えないからです。

今回はデザインシステムとしてではなく、開発用の Playground として Storybook を使いたいので開発ツールがうまく動かない点は致命的な問題です。

(最初に気付けよ、という感じですが私自身はそれまであまりちゃんと Storybook を使ったことなかったので...)

Github の issue を見るとワークアラウンドがありそうですが...。すでに導入でだいぶ消耗していて、これ以上の yak shaving をする気は起きなかったので別の方法を検討することにしました。

(追記: 無理やりですが iframe を別タブで開くと vue-devtools が使えます)

Storybook 相当のアプリケーションを作ってみる

よく考えれば今回の用途に限って言えば Storybook の全機能が使える必要はありません。やりたいことは至ってシンプルです。

要求・仕様

  • アプリケーションと独立した環境で動作確認しながらコンポーネントを開発できる
  • vue-devtools が使える
  • LiveReload
  • Mac でも開発できる
  • コードのコピペなどせずに restaurant1 のコードがそのまま確認できる
  • Storybook 風にストーリーが書ける

この仕様を満たす小さなアプリケーションを書いてみることにしました。 ※ あくまで Playground として使い、最終確認はアプリケーションに組み込んでやる前提。

まずは使い方をご紹介

  • stories.js にStorybook 風のAPIでストーリーを追加していく
storiesOf('sample')
  .add('hello', h => h('h3', ['hello. this is my story.'], {}));

storiesOf('DatePicker')
  .add('select', h => h(CustomDatePicker, { props: { value: '2018-12-01' } }));
  • restaurant1 内で yarn play を実行してサーバーを起動

f:id:shozawa:20181210144606g:plain

UIがダサいのはご容赦ください...。

これだけですが、「アプリケーションと切り離された環境でコンポーネントを開発する」ということは実現できています。

こだわりポイント

ここからは蛇足な気がしますが、せっかくなのでこだわりポイントをご紹介します。

  • とにかくシンプルに!
  • 新しいライブラリを追加しない
  • Storybook like な API

以上の3点を心がけて実装しました。

このツールを使う人やツールの機能を拡張しようとしてコードを読む人の負担が最小限になるように気をつけています。

まずは何よりコードが小さく、シンプルになるように心がけました。また不用意に新しいライブラリを追加すると、でコードを読んだ人に負担もかけてしまうので restaurant1 に追加済みのライブラリのみを使用することにしました。

webpack-dev-server でホスト

今回はアプリケーションの中に playground ディレクトリを作りそこに関連ファイルを格納し、 webpack-dev-server でホストしています。 今回のモチベーションが「ビルドの速度改善」なので 本アプリの webpack の設定を使い回すことはせずに新しく playground 以下に数十行のシンプルな設定ファイル( playground/webpack.config.js )を追加しました。

// package.json
{
  // 略
  "scripts": {
    // 略
    "play": "webpack-dev-server --config playground/webpack.config.js"
  }
}
// playground/webpack.config.js
{
  // 略
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    port: 9000,
  },
}

9000番ポートで dist ディレクトリの内容をホストします。

storiesOf 関数

storiesOf 関数の実装はこれだけです。 storiesOf と add でオブジェクトにコンポーネントを登録していきます。

// playground.js
const stories = {}; // Component を格納
const tableOfContents = {}; // ナビゲーション用

// TODO: HMR
function storiesOf(title) {
  tableOfContents[title] = tableOfContents[title] || {};
  return {
    add(scenario, value) {
      const key = `${title}:${scenario}`;
      tableOfContents[title][scenario] = key;
      playground[key] = { render: value };
      return this;
    },
  };
}

const getStories = () => stories;
const getTableOfContents = () => tableOfContents;

export {
  storiesOf,
  getStories,
  getTableOfContents,
};

※ vue-play の実装を参考にさせていただきました。

github.com

プレビュー機能

import { getStories } from './playground';
export default {
  name: 'PlaygroundPreview',
  data() {
    return {
      scenario: '',
      stories: {},
    };
  },
  methods: {
    setScenario() {
      const hash = decodeURI(window.location.hash);
      this.scenario = hash.replace('#', '');
    },
  },
  computed: {
    current() {
      return this.stories[this.scenario];
    },
  },
  created() {
    this.stories = getStories();
    this.setScenario();
    window.addEventListener('hashchange', this.setScenario);
  },
  render(h) {
    return h(this.current, [], {});
  },
};

ストーリーとして登録したコンポーネントのプレビュー部分です。 URL のハッシュ部分にコンポーネントのキーを入れるようにし、ハッシュの変更によってプレビューされるコンポーネントが切り替わるようにしています。

http://localhost:9000/#{ストーリーのタイトル}:{ストーリーの小見出し}

今後の展望

webpack の設定ファイルを含め、250行程度のコードでここまでの内容が実現できました。

シンプルな実装で最低限やりたいことはできた、と思っています。

  • WEBフォントの読み込みができていない
  • Vuex と連携するコンポーネントの動作確認ができない
  • DefinePlugin の対応
  • async/await を使っているコードでエラーが出るので webpack の設定を見直し
  • UIがイケてない

などすでにいくつか課題は見つかっているのですが、プレゼンテーションだけに責任を持つシンプルなコンポーネントの開発であれば十分に活用できるかと思います。

実はデモ用にいくつか実際に使われているコンポーネントを追加しようと思ったのですが、ほとんどの主要コンポーネントが Vuex に依存していてうまく追加できませでした。

よく言われていることですが、あらためて Presentation Component と Container Component の分離が重要ですね。

もう少しブラッシュアップして、良さそうであれば master に取り込もうと思います。