ハローワールド。
引き続きivoryを作成していきます。
本日はまだツール群の構築。storybookとjest、そしてvscodeのスニペットの設定です。
Storybook
StorybookはVueやReactなどで作ったコンポーネントをカタログ化してくれるツール、というのが直感的な説明となります。
今まではお世話になることはなかったのですが、今回はUIコンポーネントを作らないので、コンポーネントの管理と見た目のテストを簡略化するためにStoryBookを導入しました。まぁ、本音では使ってみたかったツールだから使う、ぐらいの感覚ですが。
導入
インストールページを参照してインストールしていきます。といっても、Automatic stupで何ら問題ないのでそれでやっていきます。
ただしページではnpm(npx)を利用していましたが、今回はyarnを利用しているので多少コマンドが異なります。
$ yarn add @storybook/cli -D
$ yarn sb init --type react
TypeScript対応
次にTypeScript対応をしていきましょう。
TypeScript対応ページを参考にやっていきます。
$ yarn add -D @storybook/addon-info react-docgen-typescript-loader
次にstorybook用のwebpackを導入していきます。
.storybook/main.js
にwebpackの設定を書いていきます。
module.exports = {
webpackFinal: async config => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
use: [
{
loader: require.resolve('ts-loader'),
},
// Optional
{
loader: require.resolve('react-docgen-typescript-loader'),
},
],
});
config.resolve.extensions.push('.ts', '.tsx');
return config;
},
};
ただ、残念ながらこれでは一切動きません。もちろんcss-loader
とか入っていませんからね。
幸い、この中身は殆ど既存のwebpackと同じで問題ないですのでコピペして行きましょう。少し書き方は違いますが。
const path = require('path');
module.exports = {
stories: ['../src/**/*.stories.tsx'],
addons: [
'@storybook/addon-actions',
'@storybook/addon-links',
],
webpackFinal: async (config) => {
console.log(config.resolve);
config.module.rules.push({
test: /\.(ts|tsx)$/,
use: [
{
loader: require.resolve('ts-loader'),
},
{
loader: require.resolve('react-docgen-typescript-loader'),
},
],
});
config.module.rules.push({
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { importLoaders: 1 } },
{
loader: 'postcss-loader',
options: {
config: {
path: './.storybook/postcss.config',
},
},
},
],
include: path.resolve(__dirname),
});
config.module.rules.push({
test: /\.s[ca]ss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
});
config.module.rules.push({
test: /\.(jpeg|png|gif|svg)$/,
loader: 'file-loader?name=[name].[ext]',
});
config.resolve.alias['@'] = path.resolve(__dirname, '../src');
config.resolve.alias['public'] = path.resolve(__dirname, '../public');
config.resolve.extensions.push('.ts', '.tsx');
return config;
},
};
僕の場合はこんな感じ。例えばresolve
のalias
が設定してあったり、tailwind.css
用にpostcss-loader
が入っていたりしますが、このあたりは現行のwebpackを参考に記載してください。
試してみる
まずはStorybook用のコンポーネントを作りましょう。とりあえず使うかどうかわかりませんが、タイトルロゴ付きのヘッダーを作って行きます。
import React from 'react';
import './index.scss';
import { TitleLogo } from '@/components/atoms/TitleLogo';
export function LayoutHeader() {
return (
<header className="LayoutHeader h-16">
<div className="h-full .z-0 flex relative items-center">
<TitleLogo />
</div>
</header>
);
}
こんな感じかな。LayoutHeader
クラスのcssの内容はこんな感じ。
.LayoutHeader {
contain: layout;
box-shadow: 0 2px 4px -1px rgba(0, 0, 0, 0.2), 0 4px 5px 0 rgba(0, 0, 0, 0.14),
0 1px 10px 0 rgba(0, 0, 0, 0.12);
}
実際にビルドして表示してみるとこんな感じ。
ではこれをStorybookへ登録していきましょう。
といってもすごく簡単。今回の場合は*.stories.tsx
というファイル名であれば登録されるので、適当なところに*.stories.tsx
ファイルを作って下記のような設定にしましょう。
import React from 'react';
import { LayoutHeader as Component } from './index';
export default {
title: 'organisms',
};
export const LayoutHeader = () => <Component />;
後は下記コマンドでstorybookを起動します。
$ yarn storybook
実行、しかし…
しかし、実行してみると下記の用になります。
なんか色々突っ込みどころはありますが、これはおそらくtailwind.cssのcssが効いてない気がします。
これはtailwind.cssをimportできてないので発生します。
なので.storybook/config.ts
にtailwind.css
を読み込むような設定を入れます。ちなみにファイルの場所は前回の記事で設定しました。
import '@/style/tailwind.css';
OKですね!
Test
お次にテストです。今回はjest
とenzyme
の鉄板セット(?)を使ってテスト環境を構築していきます。
導入
jestのwebpack用の設定ページを参考に作っていきます。
まず、jest
とenzyme
に必要なデータをゴリゴリ集めていきます。
$ yarn add -D babel-jest @babel/preset-env @babel/preset-react react-test-renderer identity-obj-proxy @types/jest
$ yarn add -D enzyme enzyme-adapter-react-16 enzyme-to-json jest-enzyme @types/enzyme-adapter-react-16
次にルートフォルダにjest.config.json
を作成し、下記のようにします。ただし、cssを使ってないとかファイルを使ってないとか、エイリアスを使ってないとかあると思うので下記通りにはならないかもしれません。
{
"snapshotSerializers": ["enzyme-to-json/serializer"],
"setupFilesAfterEnv": ["<rootDir>/src/plugins/setupJestTest.ts"],
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
},
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|scss)$": "identity-obj-proxy",
"^@/(.+?)$": "<rootDir>/src/$1",
"^public/(.+?)$": "<rootDir>/public/$1"
}
}
jestではファイルの取り扱いが面倒くさいです。なのでfile用のMockを作ります。mockの場所は上記の
で指定されていますので、ここに下記のような設定にしましょう。
module.exports = 'test-file-stub';
また、上記のjestの設定ファイルにsetupFilesAfterEnv
というパラメータが指定されてしますが、これは環境を作成した後(テストを実行する前)に実行されるファイルです。ここで大体全体で利用する設定を行います。ちなみにこれはドキュメントに書いてないので、もしかしたら推奨されないやり方かも知れませんので悪しからず。
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });
テスト実行
後はテスト用のファイルを作るだけです。さっき作ったLayoutHeader
に対して正常に表示されるのかどうかのテスト、後スナップショットテストをかましていきましょう。
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import LayoutHeader from './layoutHeader';
describe('LayoutHeader', () => {
test('is rendered', () => {
const wrapper = shallow(<LayoutHeader />);
expect(wrapper).toBeTruthy();
expect(toJson(wrapper)).toMatchSnapshot();
});
});
$ yarn test
yarn run v1.22.4
$ jest
PASS src/components/organisms/layoutHeader.test.tsx
LayoutHeader
√ is rendered (7 ms)
› 1 snapshot written.
Snapshot Summary
› 1 snapshot written from 1 test suite.
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 written, 1 total
Time: 1.866 s, estimated 2 s
Ran all test suites.
Done in 2.52s.
yarn jest
でも同様の結果になります。今回はpackage.json
のscripts
に設定を入れ込んでいるのでyarn test
としていますが、ここの設定に関しては省略しています。
VScodeのスニペット設定
今回は構築から少し外れて、お困りごとを解決するためのVSCodeのスニペット設定について記載します。
Reactのファイル構造ルール
まず困りごとの前にReactのファイル構造のルールについて。
Reactではファイル構成が割と十人十色というか、あまり決まりきったルールがあるわけではないのですが、私の場合はComponent.tsx
を作ったら同じ階層にComponent.scss
、Component.test.tsx
、Component.stoires.tsx
を作成するようなルールにしています。
上記ルールの問題は、一つの階層にコンポーネント数 * 4のファイルが出来上がるのですごく見通しが悪くなります。
なので今回はフォルダをそれぞれ作って、その中にindex.tsx
、index.scss
…を作るルールにしました。またその中でしか利用しない子コンポーネントは同じ階層に入れる用にもしています。
例えば、上記のLayoutHeaderは中にTitleLogoコンポーネントを利用しています。TitleLogoコンポーネントはおそらく色々なところで使われますが、今回はLayoutHeaderコンポーネント内でしか使われないと仮定します。
そうした場合、下記のようなフォルダ構成になります。
LayoutHeader/
|-index.scss
|-index.stories.tsx
|-index.test.tsx
|-index.tsx
|-TitleLogo.scss
|-TitleLogo.stories.tsx
|-TitleLogo.test.tsx
|-TitleLogo.tsx
困りごと
お話変わりまして、react、stories、test、それぞれVSCodeのスニペットを設定したいのです。というのも、それぞれ初期状態がだいたい一緒なので、いちいちコピペするよりはVSCodeのスニペット機能を利用したいです。
例えば、上記のLayoutHeader/index.tsx
であればファイルが作られた状態で下記のようになっていてほしいものです。
import React from 'react';
import './index.scss';
interface Props {}
export const LayoutHeader: React.FC<Props> = (props) => {
return <div></div>;
}
上記のデフォルト状態が良いか悪いかはおいておいて、ここで重要なのはexport const LayoutHeader
のLayoutHeader
の部分です。これはフォルダ名です。
しかしながら、TitleLogo.tsx
も上記に沿った内容にしたい場合、LayoutHeader
の部分をTitleLogo
にする必要があります。が、これはファイル名です。
つまり、ファイル名がindex.tsx
である場合はフォルダ名を、それ以外の場合はファイル名から拡張子を抜いた名前を利用したいのです。
VSCodeのスニペット機能
解決方法の前に、VSCodeのスニペットについて少し説明します。
VSCodeのスニペット機能は、特定の入力を行うことで事前に設定した文を出力してくれる機能です。
百聞は一見に如かずというので、実際に動いているのを見ていただいたほうが早いと思います。
これはES7 React/Redux/GraphQL/React-Native snippetsのスニペットの一つであるrfc
を入力した際の動きです。この場合はReactのFunctional Componentを入力してくれるスニペットですね。
見ていただいたとおり関数名がindex
になっています。まぁdefault exportなので問題はないですが、VSCodeでの自動インポート機能が全く効かなくなるので、できるだけデフォルトエクスポートは避けています。
ちなみにこのindex
はファイル名から拡張子を抜いた名前です。
VSCodeのスニペットでは様々な変数が利用できます。例えば$TM_FILENAME_BASE
はファイル名から拡張子を抜いた名前を提供してくれます。
また、${変数名/from/to/g}
で正規表現を利用した置換が可能です。今回はこの正規表現を利用して置換します。
解決方法
上記をまとめると、今回はindex.tsxの場合はフォルダ名を、それ以外の場合はフォルダ名を取得する正規表現を作成することで解決できそうです。
なので、今回は下記のような正規表現を作成しました。
.*[\/\\]([^\/\\]+)[\/\\]index\.tsx$|.*[\/\\](.*?)(?:\.[^.]*)$
Brainf*ckかな?
正規表現の可視化サイトを利用してわかりやすくしてみましょう。
今回利用したサイトはRegexperです。
まぁ簡単に行ってしまえば、末尾がindex.tsxであれば上に、そうでなければ下にマッチする正規表現です。
ここで、groupを見てみると、上のgroup#1はindex.tsxの手前の\か/以外の文字列の連続にマッチします。すなわちindex.tsxの親フォルダが取得できます。
下のgroup#2は拡張子より前のファイルの名前にマッチします。
VSCodeではグループを$1
、$2
という感じで利用できます。そして、group1がマッチしたとき$2はマッチしていないので空文字、逆も同様なので$1$2
とやると欲しい物が得られます。
では今回はこんなスニペットを用意します。
"React Functional Component": {
"prefix": "reafunc",
"body": [
"import React from 'react';",
"import './${2:$TM_FILENAME_BASE}.scss';",
"",
"interface Props {}",
"",
"export const ${1:${TM_FILEPATH/.*[\\/\\\\]([^\\/\\\\]+)[\\/\\\\]index\\.tsx$|.*[\\/\\\\](.*?)(?:\\.[^.]*)$/$1$2/}}: React.FC<Props> = (props) => {",
" return <div></div>;",
"}",
"",
]
},
ちなみにバックスラッシュはエスケープのために2つ重ねないと行けないので大変なことになっています。
やってみた
まずはindex.tsxで上記スニペットを実行した様子です。
名前がLayoutHeaderになっていますね。
次にLayoutHeader/TitleLogo.tsx
でreafuncを実行した結果です。
今度は名前がTItleLogoになっています。良いね。
まとめ
本日はStorybook、JestとEnzyme、そしてVSCodeのスニペット設定を行いました。
VSCodeのスニペットは調べても似たような事象がほとんどがなかったので、詳細に解説して別記事にしようと考えています。もしかして、需要がないんでしょうか。
本日は以上です。