アップデート版の記事を以下に用意しています。
2020年師走における Next.js をベースとしたフロントエンドの環境構築
さて、今年に入って既に2ヶ月が経ちました。ということは3月に突入しているってことで、それは僕が東京で働き初めて2年が過ぎ去り、SPA なフロントエンドの環境をプロジェクトとして初めて構築して1年あまりということです。そして、冬も過ぎ去り春が来ようかというようなこの時期に、小さくはあるけれど新たな挑戦として Next.js を使うことになりました。
こんな記事を読んでいる方なら分かるとは思いますが、Next.js とは JavaScript のライブラリである React のフレームワーク です。Next.js といえば、同くフレームワークである Gatsby になんとなく押され気味なイメージを感じていましたが、v9 以降のアップデートがよい感じで、さらにごく最近の v9.3 のアップデートでは少しばかり興奮してしまいました。今回はアップデートの内容には触れませんが、とてもおもしろい機能が追加されているので確認してみることをおすすめします。
その Next.js で環境構築した際の内容と手順をご紹介していきます。
技術選定
好みがないとは言い切れないですが、基本的には 最新かつ人気のある構成 を目指して選定しました。一応は盛りすぎないようにも気をつけていますが、不要な場合はある程度は省きやすいようにしていますので、うまく読み替えてもらえればと思います。ただし、TypeScrpt を前提 で書いているので TypeScript を省くのは難しいかもです。
- フレームワーク: Next.js v9.3.0 (React v16.13.0)
- 静的型付け: TypeScript v3.8.3
- PWA: next-offline v5.0.0
- スタイリング: styled-components v5.0.1 + styled-media-query v2.1.2
- 状態管理: Redux Toolkit v1.2.5
- ルール&整形: EditorConfig + ESLint v6.8.0 + Prettier v1.19.1
- テスト: Jest v25.1.0 + React Testing Library v9.4.1
- コンポーネントカタログ: Stroybook v5.3.14 (StoryShots を含む)
- Git フック: Husky v4.2.3
これらで構築されたプロジェクトのリポジトリは以下になります。
https://github.com/syuji-higa/template-nextjs-2020-beginning
※上記以外のモジュールなども含まれており、以降で説明する構築手順とは違った内容の箇所もありますが、選定したものは全て含まれています。あとから別途リポジトリを用意するかも。
(追記:2020/03/13 22:35)
この記事の構築手順だけにしたリポジトリに差し替えました。
また、その際に通しで確認してうまくいかない箇所を見つけたので細々と記事を修正しています。
構築手順
少しばかり長くなっていますが、セクションごとにみると大したことはしていないので、少しずつ構築していくとよいかと思います。
(その過程で問題があるようでしたら、教えてもらえると大変うれしいです)
**※Linux コマンドでファイルの作成などをしていますので、windows の方などは読み替えてもらえればと思います。**ちなみにこの記事の大半は windows で書いています。
1. Next.js を追加
ベースとなるフレームワークの選定ですが、TypeScript と SSR がしたかった。くらいで、深い理由はなくて比較的簡単にできそうな Next.js をなんとなく選んだくらいの感覚です。ちなみに僕は React をまともに書いたことはないです。
# Next.js をセットアップ
# (現在のディレクトリで作成しますが必要に応じて変更)
$ npm init next-app .
# pages を src ディレクトリへ移動
$ mkdir src ; mv pages/ src/pages
# Next.js の設定ファイルを作成
$ touch next.config.js
Next.js で v9.1 から src
に pages
を入れること可能になったので、src
に入れる形で進めます。
1-1. Next.js の設定
const { resolve } = require('path')
const nextConfig = {
webpack: (config) => {
// src ディレクトリをエイリアスのルートに設定
config.resolve.alias['~'] = resolve(__dirname, 'src')
return config
}
}
module.exports = nextConfig
2. TypeScript に対応
TypeScript は簡単にいうと JavaScript に 静的型付け を加えたスーパーセットです。JavaScript は動的型付けである為、初心者にとってはわかりやすくはあるかもしれません。しかし、プロジェクトの規模が大きくなるほど静的型付けの 安全性 というメリットが強みを増してきます。「プロジェクトよ健全であれ」と誰が言ったかは分かりませんが、少しでも大きなプロジェクトであれば導入しておくのが良いでしょう。
# TypeScript 関連のモジュールをインストール
$ npm i -D typescript @types/react @types/react-dom @types/node
# TypeScript の設定ファイルを作成
$ touch tsconfig.json
# Next.js を開発モードで起動することで必要なファイルを自動生成
$ npm run dev
# 生成されたら終了する
# src ディレクトリ配下の js と jsx のファイルを ts と tsx に変換
$ find src -name "*.js" | sed 'p;s/.js$/.tsx/' | xargs -n2 mv
2-1. TypeScript の設定に追加
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"~/*": [
"./src/*"
]
}
}
}
src
ディレクトリをエイリアスのルートに設定しています。
2-2. ページを TypeScript に対応
// React を読み込む
import * as React from 'react'
// NextPage を読み込む
import { NextPage } from 'next'
// 型を追加
const Home: NextPage = () => (
// JSX はそのまま
}
2-3. Next.js の設定に追加
// ファイルの先頭に eslint-disable を追加
/* eslint-disable
@typescript-eslint/no-var-requires,
@typescript-eslint/explicit-function-return-type
*/
これはあとから追加する ESLint の話なのですが、eslint-disable
は ESLint の設定を TypeScript にする為、js ファイルに適応すると問題がおきるので設定しています。ESLint の適応外にすることはできるのですが、自動整形を優先してこの形にしました。
本来であれば以下のどちらかにできればよいのですが、方法がわからなかったです。
-
next.config.ts
にする - js ファイルには TypeScript 関連の Lint を適応しない
わかる方いたら教えてくださいませ。
3. Next.js の App と Document のコンポーネントをオーバーライド
# _app.tsx と _document.tsx のファイルを作成
$ touch src/pages/_app.tsx && touch src/pages/_document.tsx
# sanitize.css をインストールする
$ npm i -D sanitize.css
APP コンポーネントは、全てのページでページを初期化しています。
Document コンポーネントは、<html>
と <body>
を補強しています。
_app.tsx
と _document.tsx
はそれらをオーバーライドしてカスタムすることができます。
3-1. APP コンポーネントをオーバーライド
import * as React from 'react'
import App, { AppProps } from 'next/app'
// 全体に適応する外部 CSS を読み込む
import 'sanitize.css'
class MyApp extends App {
render(): JSX.Element {
const { Component, pageProps }: AppProps = this.props
return (
<React.Fragment>
<Component {...pageProps} />
</React.Fragment>
)
}
}
export default MyApp
3-2. Document コンポーネントをオーバーライド
import Document, { Html, Head, Main, NextScript } from 'next/document'
interface CustomDocumentInterface {
url: string
title: string
description: string
}
class CustomDocument extends Document implements CustomDocumentInterface {
url = 'https://example.com'
title = 'Demo Next.js'
description = 'Demo of Next.js'
render(): JSX.Element {
return (
<Html lang="ja-JP">
<Head>
{/* `<Head>` の内容は必要に応じて変更 */}
<meta name="description" content={this.description} />
<meta name="theme-color" content="#333" />
<meta property="og:type" content="website" />
<meta property="og:title" content={this.title} />
<meta property="og:url" content={this.url} />
<meta property="og:description" content={this.description} />
<meta property="og:site_name" content={this.title} />
<meta property="og:image" content={`${this.url}/ogp.png`} />
<meta name="format-detection" content="telephone=no" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={this.title} />
<meta name="twitter:description" content={this.description} />
<meta name="twitter:image" content={`${this.url}/ogp.png`}></meta>
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default CustomDocument
4. PWA に対応
PWA は Progressive Web Apps の略称で、クロスプラットフォームのウェブアプリケーション のことです。様々なメリットがあり、かつ現在では比較的簡単に導入できるようになってきている為、既にウェブアプリのデファクトスタンダードになっているのではないでしょうか。
Next.js の PWA については next-offline を使います。公式ページをみると、めっちゃ簡単っぽく書いてるけど、ウェブアプリマニフェストは自前で用意する必要があったりして、 NuxtJS で PWA でしたことある人からすると地味に面倒に感じるかもです。それでも Service Worker の実装などは考えなくてよいので楽になっているのでしょう。
# PWA のモジュールをインストール
$ npm i -D next-offline
# now v2 でデプロイするとき以外は実行
$ touch server.js
Next.js のサーバを動かすときに、一般的な環境や now v1 ではとある問題があるため設定が必要です。詳細は後述します。
※ now は Next.js を提供している ZEIT が提供する PaaS です。
4-1. Next.js の設定に追加
// withOffline を読み込む
const withOffline = require('next-offline')
// nextConfig を withOffline に渡す
module.exports = withOffline(nextConfig)
4-2. ウェブアプリマニフェストを作成
- Web App Manifest Generator でウェブアプリマニフェスト関連のファイルを作成します。
- 作成した
manifest.json
とimages
フォルダをpublic
直下に設置します。
4-3. Document コンポーネントに追加
class CustomDocument extends Document implements CustomDocumentInterface {
render(): JSX.Element {
return (
<Html lang="ja-JP">
<Head>
{/* manifest.json を読み込む */}
<link rel="manifest" href="/manifest.json" />
</Head>
</Html>
)
}
}
4-4. カスタムサーバの設定
/* eslint-disable
@typescript-eslint/no-var-requires
*/
const { createServer } = require('http')
const { join } = require('path')
const { parse } = require('url')
const next = require('next')
const app = next({ dev: process.env.NODE_ENV !== 'production' })
const handle = app.getRequestHandler()
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true)
const { pathname } = parsedUrl
// handle GET request to /service-worker.js
if (pathname === '/service-worker.js') {
const filePath = join(__dirname, '.next', pathname)
app.serveStatic(req, res, filePath)
} else {
handle(req, res, parsedUrl)
}
}).listen(3000, () => {
console.log(`> Ready on http://localhost:${3000}`)
})
})
これは Next.js の Node によるサーバの設定です。通常は変更する必要はないのですが、ウェブアプリマニフェストが出力されるディレクトリの問題で読み込むできない為、設定を変更する必要があります。
ただ、カスタムサーバを使うとサーバレス機能や自動静的最適化が無効化されるとの記載があります。詳細を理解してはいないのですが、できるだけ使わない方がよいとのことなのでどうにかならないものかなとは思いますが、next-offline のドキュメントにあるので仕方ないかなという感じです。PWA にすることとどちらがメリットがあるのかは気になるところです。
この設定は now v2 では不要になります。
4-5. npm-scripts を変更
{
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
}
}
カスタムサーバを使用した場合は設定し直す必要があります。
5. styled components を追加
Next.js には初期設定で styled-jsx が含まれています。そちらもで良いかとは思うのですが、styled-components の方が人気があるように感じたので、今回は styled-components を選択しています。
# styled-components の関連モジュールをインストール
$ npm i -D styled-components styled-media-query
# .babelrc.js を作成(エラーを回避する為に追加。詳細は後述)
$ touch .babelrc.js
styled-media-query は styled-components で media queries を比較的簡単に記述できるようにするモジュールです。
5-1. APP コンポーネントのオーバーライドを変更
// ThemeProvider を読み込む
import { ThemeProvider } from 'styled-components'
// テーマを必要に応じて設定
const theme = {}
class MyApp extends App {
render(): JSX.Element {
return (
// `<React.Fragment>` を `<ThemeProvider>` に変更してテーマを渡す
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
)
}
}
テーマを使うと <ThemeProvider>
配下の全てのコンポーネントにスタイルを渡すことができます。深くネストされているコンポーネントでも問題ありません。
5-2. Document コンポーネントのオーバーライドに追加
// ServerStyleSheet を読み込む
import { ServerStyleSheet } from 'styled-components'
class CustomDocument extends Document implements CustomDocumentInterface {
// getInitialProps メソッドの追加
static async getInitialProps(ctx): Promise<any> {
const sheet = new ServerStyleSheet()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = (): any =>
originalRenderPage({
enhanceApp: (App) => (props): void =>
sheet.collectStyles(<App {...props} />)
})
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
)
}
} finally {
sheet.seal()
}
}
}
SSR 時に styled-components で追加したスタイルが適応されない問題があるので、こちらの対応が必要になります。
5-3. Babel を設定
module.exports = {
presets: ['next/babel'],
plugins: [
['styled-components', { ssr: true, displayName: true, preprocess: false }]
]
}
Next.js は v9 のバージョンアップにより、内部的に Babel が動作しているものの、Babel に関連するモジュールのインストールや設定ファイルが不要になりました。なので、.babelrc.js
も基本は不要ではあるのですが、問題があるために一部の設定をオーバーライドする必要があります。
問題というのは、ローカルの開発サーバー側の SSR 時と クライアント側のCSR 時に styled-components が付与するクラス名に差が生まれエラーが出てしまうというものです。
6. Redux Toolkit
Redux Toolkit は React における 状態管理 のライブラリである Redux を、効率的に扱うことのできるようにするライブラリです。プロジェクト全体に関わる状態をストアで管理することで、より効率的な設計ができると思います。
# Redux Toolkit 関連のモジュールをインストール
$ npm i -D @reduxjs/toolkit react-redux redux-logger
# Redux Toolkit に関するファイルを作成
$ touch src/store.ts && mkdir src/modules ; touch src/modules/rootState.ts
# ストアのサンプルを作成するのであれば実行
$ touch src/modules/sampleModule.ts
6-1. ストアを設定
import {
configureStore,
getDefaultMiddleware,
EnhancedStore
} from '@reduxjs/toolkit'
import { rootReducer } from './modules/rootState'
import logger from 'redux-logger'
export const setupStore = (): EnhancedStore => {
const middlewares = [...getDefaultMiddleware()]
// only development
if (process.env.NODE_ENV === 'development') {
middlewares.push(logger)
}
return configureStore({
reducer: rootReducer,
middleware: middlewares,
devTools: true
})
}
configureStore
の devTools
を true
にすることで Redux DevTools Extension を起動することができます。デバッグの役に立ちます。
6-2. APP コンポーネントに設定を追加
// Provider を読み込む
import { Provider } from 'react-redux'
// setupStore を読み込む
import { setupStore } from '~/store'
// ストアを作成
const store = setupStore()
class MyApp extends App {
render(): JSX.Element {
return (
// Provider で囲んで store を渡す
<Provider store={store}>
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
</Provider>
)
}
}
6-3. ストアのルートを追加
import { combineReducers } from '@reduxjs/toolkit'
// サンプルを追加するのであれば読み込む
import sampleModule, { SampleState } from './sampleModule'
export interface RootState {
// サンプルを追加するのであれば記述
sample: SampleState
}
export const rootReducer = combineReducers({
// サンプルを追加するのであれば記述
sample: sampleModule.reducer
})
6-4. ストアのモジュールを設定
サンプルのモジュールファイルを追加したのであれば対応してください。
import { createSlice } from '@reduxjs/toolkit'
export type SampleState = {
value: string
}
export const sampleInitialState: SampleState = {
value: ''
}
const sampleModule = createSlice({
name: 'sample',
initialState: sampleInitialState,
reducers: {
update: (state): SampleState => {
return { value: state.value }
}
}
})
export default sampleModule
7. EditorConfig と ESLint と Prettier を追加
これらは コードの記述ルールや整形 などをするものです。それぞれの担っている内容が重複しているところもあるのですが、ルールのコンフリクトを避けて全て導入しておくとよいです。もたらされたのは人類におけるタイプ数や無駄なコンフリクト、それに宗教戦争の大幅な削減です。これは、本当に素晴らしいことだと感じています。
# ESLint と prettier 関連モジュールをインストール
$ npm i -D eslint eslint-plugin-react prettier eslint-config-prettier eslint-plugin-prettier
# ESLint の TypeScript に関するモジュールをインストール
$ npm i -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
# ESLint と prettier の設定ファイルを作成
$ touch .editorconfig && touch .eslintrc.js && touch .prettierrc.js
# VSCode を使うのであれば設定ファイルを作成
$ mkdir .vscode ; touch .vscode/settings.json
TypeScript の Linter として TSLint というものがあるのですが、現在では非推奨になっており ESLintへの移行 を促しています。これは両者が同じ目的の為に存在しており、ESLint が TSLint の機能を統合する方向で動いているからです。
7-1. EditorConfig の設定
# 必要に応じて Prettier と競合が起きないように設定
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
7-2. ESLint の設定
module.exports = {
// 整形を効かせたいファイルなので除外を解除
ignorePatterns: ['!.eslintrc.js', '!.babelrc.js'],
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:prettier/recommended',
'prettier/@typescript-eslint'
],
plugins: ['@typescript-eslint', 'react'],
parser: '@typescript-eslint/parser',
env: {
browser: true,
node: true,
es6: true,
jest: true
},
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
settings: {
react: {
version: 'detect'
}
},
rules: {
// 必要に応じてルールを追加
'react/prop-types': 'off',
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/no-explicit-any': 'off'
}
}
7-3. Prettier を設定
module.exports = {
// 必要に応じて EditorConfig や ESLint と競合が起きないように設定
semi: false,
arrowParens: 'always',
singleQuote: true
}
7-4. npm-scripts を追加
{
"scripts": {
"lint": "eslint --ext .js,.jsx,.ts,.tsx --ignore-path .gitignore ."
}
}
eslint
コマンドは --fix
オプションによる 自動整形 もできます。npm-scripts を通してオプションを渡すため npm run lint -- --fix
とコマンド打つことで実行できます。
7-5. VSCode の設定
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
]
}
VSCode は使っていない人はさようなら。使っている人はこちらの設定で ファイルの保存時に自動整形 することができます。見た目なんてなにも考えずにひたすら書いて、そして保存。もはやなくてはならない空気のような存在です。
ちなみに、VSCode の設定ファイルを Git で管理するかどうかという話があるかと思います。個人的には VSCode の普及率と、あって困るものではないという点で含めちゃってよいと思いますが、含めたくないのであれば .gitignore に追加してくださいませ。
8. Jest と React Testing Library を追加
Jest は JavaScript のテストフレームワークです。また、React Testing Library はテストを ユーザが操作する手順と同じように書く ことができます。このライブラリは React でも推奨されています。テストを書くことにより得られる恩恵は様々ありますが、僕自身がほとんどテストを書いたことが無いので、あまり話せることはないです。すいません。今後は書いていきます。
# Jest 関連モジュールをインストール
$ npm i -D jest jest-dom jest-css-modules
# Jest の TypeScript に関するモジュールをインストール
$ npm i -D ts-jest @types/jest
# React Testing Library をインストール
$ npm i -D @testing-library/react
# Jest の設定ファイルとテストファイルのディレクトリを作成
$ touch jest.config.js && mkdir src/__tests__
# もしサンプルのテストファイルを追加するのであれば実行
$ touch src/__tests__/Sample.test.tsx
8-1. Jest を設定
module.exports = {
preset: 'ts-jest',
roots: ['<rootDir>/src'],
moduleNameMapper: {
// src ディレクトリをエイリアスのルートに設定
'^~/(.+)': '<rootDir>/src/$1',
// test 時に CSS ファイルを読み込まないようにする設定
'\\.css$': '<rootDir>/node_modules/jest-css-modules'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
globals: {
// test 時に TypeScript の設定を一部変更して実行する設定
'ts-jest': {
tsConfig: {
jsx: 'react'
}
}
}
}
jest-css-modules
はテスト時に import している CSS ファイルまで読み込んでしまいエラーになるのを防ぐために設定してます。
この時点ではまだ問題はないのですが、このあとに設定する StoryBook の SnapShots をテスト時に実行していて、その設定ファイル内で CSS ファイルを読み込んでいるのでエラーになります。
ts-jest
の tsConfig.jsx
については react
と preserve
があり、TypeScript だけでコンパイルするときは react
を使い、Babel などの変換を通す場合は preserve
を使います。
さきほど軽く説明したとおり、Next.js は Babel を内包しています。しかし、ts-jest
によるテストは TypeScript のみを使います。つまり、ビルド時とテスト時では tsConfig.jsx
の設定を変更する必要があるので、テスト時に設定を変更しているのです。
8-2. テストの設定
サンプルのテストファイルを追加したのであれば対応してください。
import * as React from 'react'
import { render, cleanup } from '@testing-library/react'
import Index from '~/pages/index'
afterEach(cleanup)
describe('Index page', (): void => {
// "Next.js!" というテキストのリンク先が Next.js のサイトであることをテスト
it('link to Next.js site', (): void => {
const { getByText } = render(<Index />)
expect(getByText('Next.js!').getAttribute('href')).toBe('https://nextjs.org')
})
// TODO を書ける
it.todo('Index TODO')
})
とりあえずページコンポーネントに対してテストを書いています。テスト内容も初期状態でできることがないので適当なものになっていますが、実際にはユーザの操作などを考えるとよいので、何かしらのイベントが入ることが多いかと思います。
また、TODO も書けるので TDD(テスト駆動開発)をするのに便利ですね。
8-3. npm-scripts を追加
{
"scripts": {
"test": "jest src/__tests__/.*/*.test.tsx?$",
}
}
test
コマンドは --watch
オプションによる 監視 もできます。npm-scripts を通してオプションを渡すため npm test -- --watch
とコマンドを打つことで実行できます。
あと、styled-jsx が記述されている為、Warning がでるのですが、テスト自体は通るのと、今回は styled-components を使う方向で進めているのでスルーしています。
9. Storybook を追加
# 1. Storybook の基本機能を追加
# storybook をインストール
$ npx -p @storybook/cli sb init --type react
# stories のファルダを src 配下に移動
$ mv stories/ src/stories
# Stroybook で使う webpack に関するモジュールをインストール
$ npm i -D babel-preset-react-app
# Storybook で使う webpack の設定ファイルを作成
$ touch .storybook/webpack.config.js
# src/stories ディレクトリ配下の js と jsx のファイルを ts と tsx に変換
find src/stories -name "*.js" | sed 'p;s/.js$/.tsx/' | $ xargs -n2 mv
# 2. Addon を追加
# メジャーな Addon をインストール(必要に応じて選択)
$ npm i -D @storybook/addon-knobs
$ npm i -D @storybook/addon-storysource
$ npm i -D @storybook/addon-viewport
$ npm i -D @storybook/addon-backgrounds
$ npm i -D @storybook/addon-console
# Storybook のレンダリングに関する設定ファイルを作成
$ touch .storybook/preview.ts
# 3. StoryShots に関する Addon を追加
# StoryShots と Puppeteer storyshots をインストール
$ npm i -D @storybook/addon-storyshots @storybook/addon-storyshots-puppeteer puppeteer react-test-renderer
# StoryShots の設定ファイルを作成
$ touch src/__tests__/storyshots.test.ts
# Puppeteer storyshots の設定ファイルを作成
# PC、タブレット、スマホの3サイズを用意(必要に応じて作成)
$ touch src/__tests__/puppeteer-storyshots.runner.ts && touch src/__tests__/puppeteer-storyshots-ipad.runner.ts && touch src/__tests__/puppeteer-storyshots-iphone8.runner.ts
9-1. Storybook の基本機能を追加
9-1-1. Storybook の設定を変更
module.exports = {
// ディレクトリと拡張子の変更
stories: ['../src/stories/**/*.stories.tsx']
}
9-1-2. ESLint の設定に追加
module.exports = {
ignorePatterns: [
// Storybook の設定に関するファイルを ESLint の対象に含める
'!.storybook/**/*.(js|ts)'
]
}
9-1-3. Stroybook で使う webpack を設定
/* eslint-disable
@typescript-eslint/no-var-requires,
@typescript-eslint/explicit-function-return-type
*/
const { resolve } = require('path')
module.exports = ({ config }) => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
loader: require.resolve('babel-loader'),
options: {
presets: [require.resolve('babel-preset-react-app')]
}
})
config.resolve.extensions.push('.ts', '.tsx')
config.resolve.alias['~'] = resolve(__dirname, '../src')
return config
}
Storybook のビルドやサーバは Next.js とは違う形で動かします。そのため、別途 Webpack の設定が必要になります。
9-1-4. gitignore に Storybook のビルドディレクトリを追加
# stroybook
storybook-static
9-1-5. ストーリーファイルを TypeScript に対応
// React の読み込みを import as に変更
import * as React from 'react'
// 返り値に型を追加
export const ToStorybook = (): JSX.Element => <Welcome showApp={linkTo('Button')} />
// React の読み込みを import as に変更
import * as React from 'react'
// 返り値に型を追加
export const Text = (): JSX.Element => (
// 返り値に型を追加
export const Emoji = (): JSX.Element => (
Storybook の型がよくわからないのですが、Warning がでてたので雰囲気で一応追加しました。
あと保存時に自動整形されますが、VSCode でない場合はコマンドで自動整形すると Lint 時の Error や Warning は消えます。
9-2. Addon の追加
9-2-1. Addon を設定に追加
module.exports = {
addons: [
// 必要に応じて追加
'@storybook/addon-knobs/register',
'@storybook/addon-storysource',
'@storybook/addon-viewport/register',
'@storybook/addon-backgrounds/register',
]
}
9-2-2. レンダリングに関する設定
import { addParameters } from '@storybook/react'
// addon-console を追加した場合は読み込む
import '@storybook/addon-console'
// addon-viewport を追加した場合は読み込む
import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'
import 'sanitize.css'
addParameters({
// addon-viewport を追加した場合は設定
viewport: {
viewports: INITIAL_VIEWPORTS
}
})
sanitize.css
を読み込んでいますが、Next.js の APP コンポーネントで読み込んだ sanitize.css
は Stroybook では反映されない為、こちらでも設定する必要があります。
9-3. StroyShots の追加
StroyShots とは Storybook のコードを流用して Jest の Snapshot Testing を実行できるものです。予定外の変更を検知 することができるので、安全に開発を進めやすくなります。
また、Puppeteer を使った StroyShotsもあり、Storybook をスクレイピングしてイメージショット をとることで見た目の差分を検知することができます。これは実行速度や正確性などの問題もあるので、どのようにテストしていくかは難しいところではありますが、コードでは追うことのできない 想定外の影響が及ぼすスタイルの変化 を検知することができます。
どちらも、うまく使うことで安全性の高い運用が期待できるかと思います。
9-3-1. StroyShots を設定
StroyShots は test
コマンド時に合わせて実行されます。-u
オプションによる スナップショットのアップデート や、-t
オプションによる ターゲットの指定 ができます。npm-scripts を通してオプションを渡すため npm test -- -u -t="TargetName"
のようにコマンドを打つことで実行できます。
import initStoryshots, {
Stories2SnapsConverter,
multiSnapshotWithOptions
} from '@storybook/addon-storyshots'
initStoryshots({
test: multiSnapshotWithOptions(),
stories2snapsConverter: new Stories2SnapsConverter({
snapshotsDirName: '../__tests__/__snapshots__/'
})
})
9-3-2. Puppeteer storyshots を設定
PC、タブレット、スマホの3サイズを用意しているので必要に応じて設定してください。
PC 用サイズを設定
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'
initStoryshots({
suite: 'Image storyshots: PC',
test: imageSnapshot()
})
タブレット用サイズを設定
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'
import devices from 'puppeteer/DeviceDescriptors'
const customizePage: any = (page) => {
return page.emulate(devices['iPad'])
}
initStoryshots({
suite: 'Image storyshots: iPad',
test: imageSnapshot({ customizePage })
})
スマホ用サイズを設定
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'
import devices from 'puppeteer/DeviceDescriptors'
const customizePage: any = (page) => {
return page.emulate(devices['iPhone 8'])
}
initStoryshots({
suite: 'Image storyshots: iPhone 8',
test: imageSnapshot({ customizePage })
})
9-3-3. Jest の設定に追加
module.exports = {
// ストーリーファイルを読み込めるようにする
transform: {
'^.+\\.stories\\.tsx$': '@storybook/addon-storyshots/injectFileName'
}
}
ストーリーファイルごとにスナップショットを取得 できる multiSnapshotWithOptions
オプションを設定して、かつ、Component Story Format という新しい形式を使っている場合は、ストーリーファイルを読み込めるようにする為にこの設定が必要です。
9-3-4. npm-scripts を追加
{
"scripts": {
"puppeteer-storyshots": "jest src/__tests__/puppeteer-storyshots*.runner.ts"
}
}
puppeteer-storyshots
コマンドは -u
オプションによる イメージショットのアップデート や、-t
オプションによる ターゲットの指定 ができます。npm-scripts を通してオプションを渡すため npm run puppeteer-storyshots -- -u -t="TargetName"
のようにコマンドを打つことで実行できます。こちらも同じく Jest を実行していますので、オプションは他のテストと同様のものになっています。
10. Husky を追加
Husky は Git フックを簡単に設定することのできるモジュールです。うまく Linter や test を走らせることで無駄なコミットなどを防ぐことができ、見通しのよいコミット履歴を保つことや不要な CI の実行などを減らすことができます。
$ npm i -D husky
10-1. Pre commit と Pre push を設定
{
"husky": {
"hooks": {
"pre-commit": "npm run lint",
"pre-push": "npm test && npm run puppeteer-storyshots"
}
}
}
git commit 時にLint を、git push 時に test を実行するようにしています。
テストに関しては以下の3種類があります。
- Jest + React Testing Library(ユニットテスト)
- StoryShots(スナップショットテスト)
- Puppeteer storyshots(イメージスナップショット)
Puppeteer storyshots は storybook をスクレイピングして画面をキャプチャします。なので storybook を起動しないといけない のと、コンポーネントが増えると処理がより重くなっていきそうな気もするので、git push 時に入れるのはどうなんだろうとは思っています。まあ、とりあえず入れておこうという感じです。
まとめ
実際のプロジェクトとして運用していく上では、他にも追加する設定やライブラリなどもあると思います。環境変数や Fetch にエラーページなどなど。
今回追加したものは、はじめの方で説明したとおり 最新かつ人気のある構成 を重視して、それでいて 実装のブレがあまり大きくならないもの にしていているつもりです。ですので、サンプルファイルを含めるかどうか悩んだのですが、無いと結局そこで不明点がでて手が止まってしまう可能性があるかと思い、 テストなどはせめてひとつのファイルくらいは動作確認ができるようにサンプルを追加しました。
最小の構成ではないが、モダンで必要なものは大体揃っていて、記述もドキュメントに可能な限り寄せて調べやすくし、そこから好きなようにライブラリなどを追加して、様々なプロジェクトに分岐していけるギリギリのラインを超えるか超えないか 、くらいの形にはなったかと思います。
まあ、つまりはそこそこ満足しています。
プロジェクトが動き出して問題点や直したいところなどもでてくるでしょうし、なによりもスピードの早いこの世界においては、数ヶ月もあれば既に古い構成になっている可能性 すらあります。辛くもあり、楽しくもある素敵な世界ですね。
それでは、この環境が廃れて、また新たな環境を構築するその時まで。