Storybookから出力したHTMLファイルをサブディレクトリで公開するにはbaseタグを使う

Storybookは build-storybook コマンドを利用すると静的なHTML、JSファイルとして出力できるので、どこか静的ファイルを公開できるところにホスティングするとすぐに公開することができます。
(参考: https://storybook.js.org/docs/basics/exporting-storybook/)

ただ、 https://example.com/storybook/ のようなサブディレクトリにおいた場合にHTMLに入っているJSのリンクがファイル名のみになっていて、サブディレクトリを参照してくれないため404になってしまいます。

こんな感じ。

f:id:kimromi:20200128220843p:plain

f:id:kimromi:20200128221010p:plain

その場合はHTMLのbaseタグのhrefを使うと解決できます。 developer.mozilla.org

baseタグはaタグのhrefやimgタグのsrcなどのURLを指定する部分を相対パスで書いたときにベースになるURLを指定することができるタグです。(<img src="/hoge.png">のように絶対パスで書くと適用されません)

Storybookの設定ファイルを置くディレクトリ(通常は.storybook/)にmanager-head.htmlというファイルを置くと、Storybookを起動した際のheadタグにカスタムタグを追加することができます(参照)。これを利用しmanager-head.htmlに以下を記載します。

<base href="/storybook/">

こんなコマンドでどうぞ。

$ echo '<base href="/storybook/">' >> .storybook/manager-head.html

再度 build-storybook して見てみるとタグとしては相対パスでファイル名が出力されていますが

f:id:kimromi:20200128222936p:plain

HTTPリクエストとしては /storybook/ をベースにしてリクエストされているためファイルが取得できページが見れるようになります。

f:id:kimromi:20200128223341p:plain

JSでいう document.baseURL が今見ているURLではなくbaseタグで指定したURLになっています。

f:id:kimromi:20200128224435p:plain

実はここにヒントがかいてありました。 Absolute versus relative paths https://storybook.js.org/docs/configurations/serving-static-files/#absolute-versus-relative-paths

baseタグ知らなかったのでStorybook以外でも何かに使えそうだなと思いました。

マネーフォワードにジョインして3ヶ月で取り組んだフロントエンドの改善と所感

こちらはMoney Forward Advent Calendar 2019の13日目の記事です。マネーフォワード クラウド経費というBtoB向けサービスのフロントエンドエンジニアとして2019年9月に入社し3ヶ月が立ちました。開発はすべて福岡支社で行われており、私も福岡で働いています。試用期間を終えちょうどアドベントカレンダーの時期であったので3ヶ月で取り組んだことと所感を残しておこうと思います。

まずは取り組んだことについてまとめていきます。

フロントエンド開発環境の改善

👉 既存の問題発見と解決策・ロードマップの共有

まずは既存のフロントエンドにおける開発の問題点についてヒアリングし、解決する方法を考えチームに共有しました。

現在のフロントエンド開発における問題点として大きく2つありました。

これらの問題の解決のため、まずは開発基盤の整備によるメンテナンスしやすさの向上を進めていくことにしました。フロントエンド改善における意義と、全体として何をいつまでに取り組んでいくかのロードマップやをドキュメントに残しておくことで「なぜ改善が必要なのか」に迷ったときに立ち戻れるようにしています。

f:id:kimromi:20191212185341p:plain
長期的な目標を書いたドキュメント

今時点の最終ゴールとしてはRailsはWebAPIのみ、フロントエンドはSPAとして独立したものにすることを目標に進めていますが、既存機能もたくさんあるのでとても長いプロジェクトになると思います。

また、前述の通りReact.jsでの開発が一部導入されていたため、より開発が進めやすくするためにコンポーネントの粒度やディレクトリ構成などのルールを考え、共有し議論しながらまとめていきました。

当然進めていくなかで思った通りに進まないところもあるので、臨機応変に対応しつつチームに共有し合意を得ながら進めています。

👉 開発基盤の整備

ディレクトリ構成の変更

SPAではなくRailsのView上で部分的にReact.jsのコンポーネントを利用していますが、一部汎用的なコンポーネントはあるものの、画面ごとに独自に作られたコンポーネントが多くメンテナンスしづらい状態でした。できるだけ汎用的にコンポーネントを実装しやすいようにRailsのそれに近かったディレクトリ構成をフレームワークに近い形に変更しました。現在のディレクトリ構成は以下です。

client/
 ├─ .storybook/ ... Storybookの設定ファイル
 ├─ assets/ ... 画像などの静的ファイル
 ├─ components/ ... React.jsのコンポーネントを置く
 ├─ consts/ ... 各種定数をまとめる
 ├─ docs/ ... 各種ドキュメントをまとめる
 ├─ lib/ ... 共通処理などを実装する
 ├─ locales/ ... I18n用の設定ファイル
 ├─ pages/ ... pageコンポーネント(Atomic Designのpagesレベル)
 ├─ stores/ ... Reduxのソースコード
 ├─ tests/ ... テストコード
 └─ types/ ... 型定義ファイルを置く

components/以下はAtomic Designの考えにもとづいて整理し抽象化することでより汎用的に使えることを目指しています。Atomic Designを採用したのはチームに経験者がいたというのと、これからチームで開発する際に意思を統一しやすい概念であるというところから採用しています。

・react_on_railsの導入

しばらくはRailsのView上でのコンポーネント利用になるため、できるだけRailsからReactコンポーネントを使いやすくするためのライブラリとしてreact_on_railsというgemを導入しました。react-railsというライブラリもあったのですが、Webpackerを利用する前提のものだったので見送りました(Webpackerはもともと使われていませんでした)。

簡単な使用方法としてはReactコンポーネントが実装されているビルド後のJavaScriptを読み込んだ状態で、Railsのview(controllerも可)内でreact_componentメソッドで使用したいコンポーネント名を文字列で渡すと使用することができ、Propsとしてサーバーサイドのセットすることができます。

= react_component('FooComponent', props: {bar: 'baz'})

やっていることとしてはHTMLのdata属性にJSONに変換したサーバーサイドのデータをセットしておき、コンポーネントのPropsとして渡す際にObjectに変換しているようです。

また、Rails側のI18n設定(config/locales)をJavaScriptに変換しクライアント側で使えるようにする機能も持っているのでこちらも使用しています。

react_on_railsの導入により部分的にReactコンポーネントに置き換えることが容易になりました。サーバーサイドレンダリングにも対応しているので必要な場合は利用することも想定しています。

クロスブラウザE2Eテストの導入

フロントエンド環境をドラスティックに変更していくにあたり、意図せずIE11で動作しないということが発生したため、クロスブラウザでのE2EテストをGitHub Actionsを利用し自動化しました。詳細は社のエンジニアブログに書いています。

moneyforward.com

・TypeScriptの部分導入、既存実装のFlowからの全移行

既存ではFlowでの型チェックが導入されていました。私の入社以前からTypeScirptへの移行は検討されていましたが、進んでいなかったこともあり移行しました。TypeScriptのアドベントカレンダーで書いたのですが、まずは新しく実装するコンポーネントはTypeScriptで書けるよう並行運用ができるようにしました。

kimromi.hatenablog.jp

その後、既存のソースコードをすべてTypeScriptに移行し先日リリースすることができたので、Railsと切り離しているJavaScriptに関してはすべてTypeScriptへの移行が完了しています。(any使ったところもたくさんあるけど)

・その他

あとは以下のような修正も行ないました。

  • テストランナーをmochaからJestに変更し、容易にスナップショットテストができるようにした
  • CommonJS形式のモジュールのexport/import方法が混在していたためES Modules形式に統一
  • Storybookの導入
・その他

コンポーネント開発をチームとしてやっていくぞということでAtomic Designの読書会を始めました。書籍は「Atomic Design ~堅牢で使いやすいUIを効率良く設計する」です。デザイナーも参加しており、スキルアップのみならずコンポーネントで開発することのメリットを共有することでモチベーションアップにもつながっていると感じています。読書会のメンバー内でコンポーネントの粒度設計のレビューをするなど実際のプロダクトに活かされていきています。

また週1回のフロントエンドの座談会も始め、日々の開発で困っていることの相談や新しい知識のインプットなどを行なっています。

👉 今後について

開発基盤を整備し既存のコードを少しリファクタリングしましたが、これからが本番で、Rails上のCoffeeScriptやSassをReact.js側に移行していきながらUI自体の改善も行なっていく必要があります。既存のデザインもリニューアルしていくプロジェクトも動いているので、便乗しつつReactコンポーネントへのリファクタリングを今後進めていきます。

さらに昨今はデザインやアクセシビリティ向上の重要性も高まっているため、デザイナーと連携しながらデザインシステムの構築などの技術的支援もやっていきたいと考えています。

マネーフォワード福岡オフィスに入って3ヶ月の所感

入社して3ヶ月、仕事をしてきて感じていることを書きます。

裁量を持って仕事ができる環境

これまでの体制としてRailsのエンジニアが中心で、フロントエンドに課題感を持っていたもののなかなか進まない状態でした。その課題感と私のやりたいことが合致したこともあり、提案したことに対してすごく前向きに捉えていただきすごく仕事が進めやすい環境です。同時に責任感もすごく感じるので、やりがいを持って仕事に取り組めています。

裁量労働制ありがたい

裁量労働制なので、2人の子供を育てている身としてとてもありがたい環境です。私はだいたい朝9時半ごろ出社して18〜19時前後に退社していますが、家族の体調不良などあれば臨機応変に対応できます。試用期間が終わればリモートワークもできるのでこれから利用していきます。

エンジニアチーム全体で技術に貪欲

興味がある技術はどんどん触れてみようという意思を感じます。私自身もすごくいい影響を受けています。Tech Talkという福岡オフィスのエンジニアが不定期に集まり、開発環境の効率化やスクラムの話などなんでもよいので技術共有する会があります。私もドメイン/DNS入門 (社内勉強会用) - Qiitaでお話しました。バックエンドのエンジニアもReact.jsやTypeScriptのキャッチアップも各自やっていてありがたいです。

Slackなどの非同期コミュニケーションが少ない

社内ドキュメントツールとして esa.io を使っていて、全職種がドキュメントを残す文化があります。必要な情報は結構まとまっているので、まずesaで検索してみると大概の情報はあります。そのためか、Slackなどでの非同期のコミュニケーションが少ない(前職が多めだったのかもしれない)と感じました。最初は少し寂しさもあったのですが、最近は慣れてきて逆に仕事に集中できているなと感じます。

フロントエンドエンジニア、デザイナーが少ない(福岡)

現在福岡オフィスにはフロントエンドエンジニアはインターンシップの学生さん含め3人、デザイナーは1人しかいません。ちなみに私は2019年12月からフロントエンドとデザイナーをまとめるチームのリーダーになりました。バックエンドエンジニアを巻き込みつつ仕事していますが、フロントエンド、デザイナーは足りていません!興味を持った方は是非こちらからよろしくお願いします。

www.wantedly.com

www.wantedly.com

corp.moneyforward.com

ということで引き続き頑張っていこうと思います。

FlowからTypeScriptに段階的に移行する

この記事は TypeScript Advent Calendar 2019 の 3日目の記事です。

私の所属するマネーフォワード クラウド経費ではフロントエンドの基盤整備が進んでいます。もともとFlowでの型チェックが入っていたJavaScriptソースコードをTypeScriptへ移行しましたので知見を共有します。この記事ではなぜTypeScriptへ移行するのかの理由などは紹介せず、段階的に移行する方法について紹介します。

なぜ段階的に移行する必要があったか

既存のJavaScriptソースコードの数がそれなりに多く、一気に移行し不具合が発生するリスクを抑えるため、少しずつ段階的に移行する戦法を取ることにしました。

% find client/ -name '*.js' | wc -l
384

% wc -l `find client/ -name '*.js'`
...
57075 total

JavaScriptのファイル数は384、行数は57075でした。コンポーネントのテスト自体はある程度書かれていて、テストが通り軽く打鍵テストをすれば問題なく移行できそうなのは幸いでした。

どうやって移行したか

TypeScriptに段階的に移行するということは、.js, jsxファイル(以下.jsx?)と.ts, .tsxファイル(以下.tsx?)を混在させるということです。.jsx?は既存の方法でビルドをしつつ、.tsx?はTypeScriptとしてビルドなどをするという拡張子別に処理を分ける戦法です。

以下について、拡張子別に処理を分ける方法を紹介します。

  • Webpackでのビルド
  • Jestによるテスト
  • ESLintでのlint

Webpackでのビルド

もともとBabelを使用していたので.jsx?はBabel(babel-loader)を使用し、.tsx?はts-loaderを利用してビルドする戦略をとりました。Babelの7から対応したTypeScriptのプリセットを使うと既存の.jsx?にまで影響が及んでしまうため、loaderを分けています。

webpack.config.jsのmodule設定は以下の感じです。

module.exports = {
  ...

  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        loader: 'ts-loader',
      },
    ],
  },

  ...
}

Jestによるテスト

テストランナーとしてJestを利用しており、.tsx?のテストはts-jestを利用することでテスト時にビルドと型チェックをしてくれます。ts-jestを使うにはJestのtransform設定に.tsx?はts-jestを使う設定を追加します。(下記はpackage.jsonにJestの設定を書いた例)

  "jest": {
    "transform": {
      "^.+\\.tsx?$": "ts-jest"
    },
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "jsx"
    ],
  },

しかし、上記の設定を追加すると.jsx?ファイルのテストがうまく動かなくなります。Jestはデフォルトではbabel-jestというプラグインを利用して.jsx?をビルドしています。transform設定に.jsx?はbabel-jestを使用する設定を追加すると動くようになります。babel-jestはJestに依存してインストールされているため、あらたにnpm installをする必要はありません。

  "jest": {
    "transform": {
      "^.+\\.tsx?$": "ts-jest",
      "^.+\\.jsx?$": "babel-jest"
    },
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "jsx"
    ],
  },

また、ts-jestにはJestのpreset設定用に3つのPresetが用意されていますts-jest/presets/js-with-babel のPresetを利用すると.tsx?はts-jest、.jsx?ばbabel-jestを使ってビルドしてくれるので、transform設定ではなくpreset設定でも同じことができます。

  "jest": {
    "preset": "ts-jest/presets/js-with-babel",
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "jsx"
    ],
  },

ESlintによるlint

ESlintを利用していますが、ESLintの実行時に設定ファイルの指定と拡張子の指定ができます。.tsx?用の.eslintrcと.jsx?の.eslintrcを別々に用意し、eslintコマンドを別々に実行することで別々にlintすることができます。

  "scripts": {
    "lint": "npm run lint:ts && npm run lint:js",
    "lint:ts": "eslint -c .eslintrc.ts.yml --ext .ts,.tsx src/",
    "lint:js": "eslint -c .eslintrc.js.yml --ext .js,.jsx src/",
  },

ただlintは最悪あとからでも修正できるため、移行した.tsx?だけlintをかけるとか、後で全ファイルまとめて対応するでもよいと思います。私はlintはあとでまとめて全ファイルにかけるようにし、まずは移行を優先で進めるようにしています。

あとは気合で少しずつ移行していく

FlowからTypeScriptへ移行する際のビルド、テスト、lintを拡張子別に分けて段階的に移行していく方法を紹介しましたが、ここからFlowで書かれているコードを正規表現などで置換していき、TypeScript導入により厳密になった型チェックで怒られる部分を修正していく作業が待ち受けます。移行の際にはniieani/typescript-vs-flowtypeなどを参考にしつつ移行を進めました。

2019年11月頭頃から移行を始め、実はこの記事を書くまでにはすべてTypeScriptへ移行してリリースまでしている予定でしたが、思ったより手こずりまだリリースまでこぎつけていません。引き続きがんばります。