2020年師走における Next.js をベースとしたフロントエンドの環境構築
さて、今年ももう終盤。12 月ですね。
今年のはじめに考えていたことが出来たのかといえば、出来ていないというのが正直なところです。とはいえ、別に後悔はなく、やりたいことが増えて優先順位を大きく変えただけの話ですかね。まあ、残された時間は有限ですので、その辺のコントロールもしなきゃなと思いつつダラダラと過ごしている日々です。
そして 12 月といえば、そうですアドベントカレンダーですね。
実は私、今年の 3 月頃に以下の記事を書いたんですよ。
それをアップデートしたのがこの記事であり、Next.js Advent Calendar 2020 3 日目の記事となります。
一部修正というよりは、ほとんど書き直しています。たった 9 ヶ月ほど前の記事なのですが、既に古くなっていると感じたがゆえの書き直しです。構成自体はそこまでは大きくは変わらないのですが、Next.js そのものや個々のモジュールの使い方などなど細々と変わってきています。
全体的な印象ですが、以前よりも簡単に構築できるようなりました。
以前の記事の公開後に気になっていた箇所もあったのですが、実際に記事を書き直しながら環境構築を進めてみて、前よりも使いやすくなったと感じる部分もあります。書き直してよかったと思える内容にはなりました。
Next.js とえいば、3 月頃は SSG(静的サイト生成)が実装されたことで盛り上がっていましたね。たまたま記事を書いていた私として丁度よいタイミングだったと記憶しています。それからも次々とアップデートを重ねて、Next.js Conf も開催されてより一層の盛り上がりをみせているようにも感じます。
特定のフレームワークなどに固執しすぎることは良いことだとは思いませんが、Next.js 自体は好きであり良いものであるとは思っていますので現状は喜ばしいです。また、だからこそ熱量をもって記事を書くことが出来たのかなと。まあ、最新の機能はサラッと見た程度なので試せていないのが多いですが。
それでは、相変わらず長い記事ではありますが、楽しみながら読んで試してもらえればと思います。
技術選定
基本的には最新かつ人気のある構成を目指して選定していますが、まあ、好みの部分も大きいです。
できるだけ必要なものだけを追加したり、別のものを使用できるように記載はしているつもりですが、TypeScript などの基礎となるものを省くとけっこう分かりづらくなるかもです。
- フレームワーク: Next.js v10.0.2 (React v17.0.1)
- 静的型付け: TypeScript v4.0.5
- PWA: next-pwa v3.1.5
- スタイリング: CSS Modules + SASS v1.29.0
- 状態管理: Recoil v0.1.2
- 静的解析&整形: EditorConfig + ESLint v7.14.0 + Prettier v2.2.0
- テスト: Jest v26.6.3 + React Testing Library v11.2.2 + Cypress v6.0.1
- コンポーネントカタログ: Stroybook v6.1.9 (StoryShots v6.1.9 を含む)
- フックスクリプト: lint-staged v10.5.2 + husky v4.3.0
構築手順
セクションごとに細かく分けて記述していますので、ひとつひとつは難しいものではないかと思います。私の記載ミスとか、環境依存またはモジュールのアップデートなどによるエラーなどが発生する場合はコメントいただけると助かります。
構築済みのプロジェクトのリポジトリを以下に用意していますので、参考にするなり、そのまま使うなりしてもらえればと思います。
1. プロジェクトを作成
それではプロジェクトを作成していきますが、表題にあるとおり Next.js を使います。Next.js とは JavaScript のライブラリである React のフレームワークです。数年前までは薄いフレームワークといった印象でしたが、現在では Nuxt.js に負けないくらい様々な機能が初期状態から追加されています。
Next.js のプロジェクトをセットアップします。
$ yarn create next-app .
2. ベースディレクトリを変更
Next.js で v9.1 から src
に pages
などを入れることができます。のちのち複雑になる可能性を考えて src ディレクトリへ移動しておくことにします。
2-1. ディレクトリを移動
pages
と styles
を src
のディレクトリ内に移動します。
$ mkdir src && \
mv pages/ src/pages & \
mv styles/ src/styles
3. TypeScript に対応
TypeScript は簡単にいうと JavaScript に静的型付けを加えたスーパーセットです。高い保守性と堅牢性を得ることでプロジェクトを健全に保ちやすくなります。
3-1. インストール
TypeScript 関連のモジュールをインストールします。
$ yarn add -D typescript @types/react @types/react-dom @types/node
3-2. 設定ファイルなどを追加
開発サーバを起動することで tsconfig.json
, next-env.d.ts
を自動生成します。
$ yarn dev
生成されたら開発サーバを終了します。
3-3. TS ファイルに変換
src
ディレクトリ配下の JS ファイルを TS ファイルに変換します。
$ find src/pages -name "_app.js" -or -name "index.js" | sed 'p;s/.js$/.tsx/' | xargs -n2 mv & \
find src/pages/api -name "*.js" | sed 'p;s/.js$/.ts/' | xargs -n2 mv
3-4. App コンポーネントを変更
App コンポーネントを TypeScript に対応します。
// React と AppProps を読み込む
import React from 'react'
import { AppProps } from 'next/app'
// 引数に型を追加する
function MyApp({ Component, pageProps }: AppProps): JSX.Element {
// 関数の内容はそのまま
}
3-5. ページコンポーネントを変更
ページコンポーネントを TypeScript に対応します。
// React と NextPage を読み込む
import React from 'react'
import { NextPage } from 'next'
// 型を追加
const Home: NextPage = () => {
// 関数の内容はそのまま
}
// export を分離
export default Home
3-6. API を変更
API を TypeScript に対応します。
// レスポンスの型を追加
type Response = {
statusCode: number
json({ name: string }): void
}
// 型を指定&使用していない引数にアンダースコア接頭詞を追加
export default (_req: void, res: Response): void => {
// 関数の内容はそのまま
}
4. Document コンポーネントを追加
Document コンポーネント を使うと、初期状態だと自動で追加される <html>
や <body>
に変更を加えることができます。
まずは、Document コンポーネントを作成します。
$ touch src/pages/_document.jsx
作成した Document コンポーネントに以下を記述します。
import React from 'react'
import Document, { Html, Head, Main, NextScript } from 'next/document'
interface MyDocumentInterface {
url: string
title: string
description: string
}
class MyDocument extends Document implements MyDocumentInterface {
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="twitter:card" content="summary_large_image" />
<meta name="format-detection" content="telephone=no" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
5. ベース URL を設定
src
に pages
ディレクトリなどを移動しましたので、src
をベース URL に設定します。
5-1. TypeScript の設定を変更
TypeScript の設定にモジュールインポートのベース URL を追記します。
{
"compilerOptions": {
// ベース URL を追加
"baseUrl": "src"
}
}
5-2. 各コンポーネントを変更
各コンポーネントのモジュールインポートの指定を、ベース URL 指定に変更します。
$ sed -i '' -e 's/..\/styles/styles/' src/pages/_app.tsx & \
sed -i '' -e 's/..\/styles/styles/' src/pages/index.tsx
6. PWA に対応
PWA は Progressive Web Apps の略称で、クロスプラットフォームのウェブアプリケーションのことです。PWA の利点を引用すると以下のように記載されています。
PWA は発見でき、インストールでき、リンクでき、ネットワークに依存せず、プログレッシブで、再エンゲージでき、レスポンシブで、安全です。
前の記事では next-offline を使いましたが、執筆時点での公式の FAQ から参照されている 参考の実装では、next-pwa が使われていました。
なかなか設定するのが面倒と感じていましたが、だいぶ簡単になった気がします。
6-1. インストール
PWA のモジュールをインストールします。
$ yarn add next-pwa
6-1. 設定ファイルの追加
まずは、Next.js の設定ファイルがまだ無いかと思いますので作成します。
$ touch next.config.js
作成した設定ファイルに以下を記述します。
const withPWA = require('next-pwa')
module.exports = withPWA({
pwa: {
dest: 'public'
}
})
6-2. ウェブアプリマニフェストを追加
- Web App Manifest Generator でウェブアプリマニフェスト関連のファイルを作成します
- 作成した
manifest.json
とimages
フォルダをpublic
直下に設置します
6-3. Document コンポーネントを変更
Document コンポーネントにマニュフェストへのリンクを追記します。
<Head>
<link rel="manifest" href="/manifest.json" />
</Head>
6-4. 無視ファイルを追加
gitignore にリポジトリで管理しないファイルを追記します。
# PWA 関連ファイルの自動生成ファイルを追加
**/public/precache.*.*.js
**/public/sw.js
**/public/workbox-*.js
**/public/worker-*.js
**/public/precache.*.*.js.map
**/public/sw.js.map
**/public/workbox-*.js.map
**/public/worker-*.js.map
7. 状態管理ライブラリを追加
前回の記事では状態管理に Redux Toolkit を追加しましたが、これについては様々な新しい選択肢がでてきている状態かと思います。
今回はこの項目を省こうかとも思ったのですが、ある程度の規模になると何かしら状態管理ライブラリを入れることにはなるかと思います。ですので、一応なにかしらを追加してみます。
Redux Toolkit 以外の選択肢ですと、例えば以下などですかね。
状態管理ライブラリを追加する必要があるのであれば、それぞれの状況に合わせ、ライブラリの特徴を確認したうえで慎重に選択してください。
今回は個人的に使いたいと思っている Recoil を追加してみます。
7-1. Recoil のインストール
Recoil をインストールします。
$ yarn add recoil
使い方はドキュメントを確認してください。
8. スタイルの設定
Next.js はデフォルトで CSS Modules に対応しています。
CSS-in-JS を使うこともでき、選択肢としては以下などがあります。
今回はなにを選ぶか悩みましたが決めることができなかったので、デフォルトの CSS Modules のままにしておきます。
CSS Modules と CSS-in-JS どちらを選ぶかについて
CSS-in-JS は同じ JS ファイル内に記述できることもあり楽には感じますが、それによるいくつかの問題があります。解決することができるものもありますが、それを解決していくことの辛みが伴いますし、そのような問題は新しく機能を拡張をする際にもついて回ってくることが多い気がします。
それであれば、CSS Modules を使うほうが良いのではないか?という考え方もあります。しかし、CSS Modules にも全く問題は無いわけではないですので、それぞれのメリットやデメリットを比較してどちらを選ぶのかが重要かと思います。
詳細は省きますが、個人的には以下の記事が分かりやすかったです。
つぎに、CSS プリプロセッサについてです。
有名どころですと SASS、LESS、Stylus があります。
もしくは、PostCSS などで標準仕様にそった新しい機能のみを追加する方法もあります。
今回は使用している割合が比較的に高そうな SASS を追加してみます。
CSS プリプロセッサを使用するかどうかについて
今どきのブラウザを対象とするのであれば、特に追加しないでも良いんじゃないかなとも思えてきています。ベンダープレフィックスが必要なプロパティもほぼ無いですし、カスタムプロパティ(カスケード変数)も使えますので。
ただ、mixins などの便利な機能もありますので、CSS プリプロセッサを使うのも視野に入れたうえで検討するとよいです。
8-1. SASS のインストール
SASS をインストールします。
$ yarn add -D sass
8-2. SASS ファイルに変換
src/styles
ディレクトリ内の CSS ファイルを SASS ファイルに変換します。
$ find src/styles -name "*.css" | sed 'p;s/.css$/.scss/' | xargs -n2 mv
8-3. SASS ファイルを読み込むように変更
CSS ファイルを SASS ファイルに変換しましたので、正しく読み込めるようにコンポーネントを変更します。
$ sed -i '' -e 's/\.css/\.scss/' src/pages/_app.tsx & \
sed -i '' -e 's/\.css/\.scss/' src/pages/index.tsx
9. デフォルト CSS の追加
ブラウザ間の誤差を吸収する為や、スタイルを追加しやすくする為に、デフォルト CSS はあります。
デフォルトスタイルについては、必要な箇所でだけ自前で記載してもよいかもですが、デフォルト CSS があったほうが楽かなとは思います。
ここ数年おおきな変化は無い気がしますが、選択肢としては以下などですかね。
- sanitize.css
- normalize.css
- reset.css
比較的新しい(とはいえ十分に枯れている)し思想も好きですので、今回は sanitize.css を使用します。
どのデフォルト CSS を使用するのかについて
単純なスタイルの当てやすさだけであれば reset.css が楽なのかもしれません。ただ、そうなると全てのブラウザのデフォルトスタイルを上書きすることになるので、無駄が発生します。
normalize.css であれば必要な箇所にだけスタイルを当てることができます。
sanitize.css は normalize.css 上位互換という感じで、使いやすくなるように一部のスタイルを調整してくれています。ただし、その分の記述量が増えています。
それぞれ一長一短がありますので、プロジェクトに合わせて選択するとよいです。
9-1. インストール
sanitize.css をインストールします。
$ yarn add -D sanitize.css
9-2. App コンポーネントを変更
デフォルト CSS を全体に適応する為に、App コンポーネントで sanitize.css を読み込みます。
// sanitize.css を読み込む
import 'sanitize.css'
10. 静的解析と整形のツールを追加
コードの静的解析と整形の為に、以下のツールを追加します。
10-1. EditorConfig の追加
EditorConfig は言語を問わず、テキストファイルのレウアウトルールを設定することができるツールです。
設定ファイルを作成します。
$ touch .editorconfig
つぎに、作成した設定ファイルに以下を記述します。
# 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
10-2. Prettier の追加
Prettier は自動整形ツールです。
10-2-1. インストール
Prettier のインストールをします。
$ yarn add -D prettier
10-2-2. 設定ファイルの追加
Prettier の設定ファイルの作成をします。
$ touch .prettierrc.js
つぎに、作成した設定ファイルに以下を記述します。
module.exports = {
semi: false,
arrowParens: 'always',
singleQuote: true,
}
10-3. ESLint の追加
ESLint は JavaScript または TypeScript の静的コード分析ツールです。
10-3-1. インストール
ESLint の関連モジュールをインストールします。
$ yarn add -D eslint eslint-plugin-react \
eslint-config-prettier eslint-plugin-prettier \
@typescript-eslint/parser @typescript-eslint/eslint-plugin
10-3-2. 設定ファイルの追加
ESLint の設定ファイルを作成します。
$ touch .eslintrc.js
つぎに、作成した設定ファイルに以下を記述します。
module.exports = {
ignorePatterns: ['!.eslintrc.js', '!.prettierrc.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'
}
}
10-3-3. Next.js の設定ファイルを修正
Next.js の設定ファイルの先頭に eslint-disable を設定する。
/* eslint-disable
@typescript-eslint/no-var-requires
*/
10-3-4. 無視ファイルを追加
gitignore にリポジトリで管理しないファイルを追記します。
# ESLint のキャッシュファイルを追加
.eslintcache
10-3-5. NPM スクリプトの追加
ESLint を実行する NPM スクリプトを追記します。
{
"scripts": {
"lint": "eslint --ext .js,.jsx,.ts,.tsx --ignore-path .gitignore ."
}
}
問題なく Lint が通るか確認します。
$ yarn lint
10-4. VSCode の設定
VSCode の設定ファイルを追加します。
$ mkdir .vscode && \
touch .vscode/settings.json
以下を設定することファイルの保存時に自動整形することができます。
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
]
}
11. テストの追加
テストと言っても色々とありますが、今回は以下のものを追加します。
- Jest (Unit テスト)
- React Testing Library (Integration テスト)
- Cypress (E2E テスト)
テストの種類について
テストの種類は以前の記事の一部で簡単にまとめていますので、確認してみてください。
Static テストについては既に追加済みですので、全ての層におけるテストはひととおり準備できていることになります。
11-1. Jest を追加
11-1-1. インストール
# Jest 関連モジュールをインストール
$ yarn add -D jest identity-obj-proxy
# Jest の TypeScript に関するモジュールをインストール
$ yarn add -D ts-jest @types/jest
11-1-2. 設定ファイルの追加
Jest の設定ファイルを作成します。
$ touch jest.config.js
作成した設定ファイルに以下を記述します。
module.exports = {
preset: 'ts-jest',
roots: ['<rootDir>/src'],
moduleNameMapper: {
// CSS モックをモックする設定
'\\.(css|scss)$': 'identity-obj-proxy',
// pages と components ディレクトリのエイリアスを設定(必要であれば他のディレクトリも追加)
'^(pages|components)/(.+)': '<rootDir>/src/$1/$2',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
globals: {
'ts-jest': {
tsconfig: {
jsx: 'react',
},
},
},
}
11-1-3. NPM スクリプトの追加
テストを実行する為の NPM スクリプトを追記します。
{
"scripts": {
"test": "jest src/__tests__/.*/*.test.tsx?$",
}
}
11-1-4. テストの追加
必要であればテストを追加します。
現時点でテストを書く必要がなくとも、テストが問題なく動作している状態にしておくのは良いことだと思います。
まずはテストファイルを置くフォルダと、サンプルのテストファイルを作成します。
$ mkdir src/__tests__ && \
touch src/__tests__/Sample.test.tsx
今回はとりあえず現状で用意できる適当なテストを書いてみます。
/// <reference types="jest" />
import React from 'react'
import Home from 'pages/index'
it('Home ページコンポーネントが存在している', () => {
expect(Home).toBeTruthy()
})
問題なくテストが動作しているか確認します。
$ yarn test
エラーにならずに書いたテストをパスしていれば問題ないです。
11-2. React Testing Library を追加
React Testing Library をインストールします。
$ yarn add -D @testing-library/react
あとは、Jest で使うテストファイル内で import して使用できます。
適当なテストを追記してみます。
// React と React Testing Library を読み込みます
import React from 'react'
import { cleanup, render, screen } from '@testing-library/react'
// 各テスト実行後にレンダーしたコンポーネントをアンマウントする
afterEach(cleanup)
it('「Next.js!」のリンクが Next.js の公式サイトのトップページである', () => {
render(<Home />)
expect(screen.getByText('Next.js!').getAttribute('href')).toBe(
'https://nextjs.org'
)
})
11-3. Cypress を追加
11-3-1. インストール
$ yarn add -D cypress
11-3-2. TypeScript に対応
$ find cypress -name "*.js" | sed 'p;s/.js$/.ts/' | xargs -n2 mv
プラグインファイルでワーニングがでるので修正します。
// 使っていない引数を削除して ES Modules の記述に変更
export default (): void => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}
コマンドファイルでエラーがでるので修正します。
// `isolatedModules: true` なので解決する為に追加
export {}
11-3-3. NPM スクリプトの追加
Cypress を起動する為の NPM スクリプトを追記します。
{
"scripts": {
"cypress:open": "cypress open",
"cypress:run": "cypress run"
}
}
11-3-4. 初期ファイルを追加
Cypress を起動して初期ファイルをプロジェクト内に展開します。
$ yarn cypress:open
以下のようにウィンドウが開かれます。
このウィンドウは、そのまま閉じてよいです。
あと、サンプルファイルは不要であれば削除します。
$ rm -rf cypress/integration/examples
11-3-5. 無視ファイルを追加
gitignore にリポジトリで管理しないファイルを追記します。
# cypress
cypress/videos
cypress/screenshots
11-3-6. 設定ファイルを追加
Cypress の設定ファイルを作成します。
$ touch cypress.json
作成した設定ファイルに以下を記述します。
{
"baseUrl": "http://localhost:3000"
}
11-3-6. テストファイルの追加
必要であればテストファイルを作成します。
$ touch cypress/integration/sample_spec.ts
適当なテストを記述してみます。
/// <reference types="cypress" />
describe('タイトルのテスト', () => {
it('タイトルが「Create Next App」である', () => {
cy.visit('/')
cy.title().should('include', 'Create Next App')
})
})
// TypeScript でエラーがでるので記述
export {}
まず、開発サーバを起動します。
$ yarn dev
次にテストを実行して、問題なくテストが動作しているか確認します。
$ yarn cypress:run
12. コンポーネントカタログの追加
コンポーネントを元にそれを組みわせてアプリケーションを作り上げていく形では、コンポーネントの一覧や状態を確認できるコンポーネントカタログはとても便利です。
コンポーネントカタログといえば、Storybook ですかね。
他の選択もあるかとは思いますが、今のところはこれ以外のまともな選択肢がわからないですので、今回は Storybook を追加していきます。
12-1. Storybook のインストール
Storybook をセットアップします。
$ npx sb init
セットアップが終わると以下が対応されています。
- 関連ファイルのインスール
- 設定ファイルの追加
- サンプルファイルの追加
- NPM スクリプトの追記
12-2. ESLint の設定を変更
ESLint の設定ファイルに Storybook の設定ファイルを無視しないように追記します。
module.exports = {
ignorePatterns: [
// Storybook の設定フォルダを追加する
'!.storybook'
],
}
一度、自動整形を実行します。
$ yarn lint --fix
いくつかのサンプルファイルでエラーがでているので修正します。
export interface HeaderProps {
// `{}` の型を変更する
user?: Record<string, unknown>
}
export interface PageProps {
// `{}` の型を変更する
user?: Record<string, unknown>
}
export const Page: React.FC<PageProps> = () => (
<li>
Use a higher-level connected component. Storybook helps you compose
{/* `"` を実体参照に変更する */}
such data from the "args" of child component stories
</li>
)
12-3. sass-loader をインストール
SASS を使っていれば sass-loader をインスールします。
$ yarn add -D sass-loader
12-4. Storybook の設定を変更
設定ファイルにエイリアスと SASS の設定を追記します。
// ESLint のエラーを回避する
/* eslint-disable
@typescript-eslint/no-var-requires
*/
const { resolve } = require('path')
module.exports = {
webpackFinal: async (config) => {
// SASS ファイルを読み込みるように設定する
config.module.rules.push({
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
include: resolve(__dirname, '../'),
})
// エイリアスを設定する
config.resolve.alias = {
...config.resolve.alias,
components: resolve(__dirname, '../src/components'),
styles: resolve(__dirname, '../src/styles'),
}
return config
},
}
12-5. 無視ファイルを追加
gitignore にリポジトリで管理しないファイルを追記します。
# Storybook のビルドディレクトリを追加
storybook-static
12-6. デフォルト CSS を追加
Storybook で表示されるコンポーネント自体に、デフォルト CSS を効かせる為に以下を追記します。
// デフォルト CSS を読み込む
import 'sanitize.css'
13. コンポーネントカタログのスナップショットの追加
13-1. StoryShots の追加
スナップショットのテストをすることで、機能追加などの際に意図しない変更が起きていないかを確認することができます。
共通コンポーネントの変更は影響範囲が大きいですので、明確に差分を確認させることでリクスを減らすことができるかと思います。
13-1-1. インストール
$ yarn add -D @storybook/addon-storyshots
13-1-2. 設定の追加
Storybook の設定ファイルを作成します。
$ mkdir storyshots && \
touch storyshots/storybook.test.ts
設定ファイルに以下を記述します。
import initStoryshots from '@storybook/addon-storyshots'
initStoryshots()
13-1-3. TS ファイルに変換
JS ファイルのままだと tsconfig.json の設定を使えずに、ファイルの読み込みエラーなどがおきるので preview.js を TS ファイルに に変換します。
$ mv .storybook/preview.js .storybook/preview.ts
13-1-4. ファイルの読み込みに対応
サンプルのストーリーファイルで DMX と SVG のファイルを読み込んでいる為、スナップショット時にエラーがでてしまいます。
SVG の変換をするモジュールを追加します。
$ yarn add -D jest-svg-transformer
テストの設定ファイルに MDX の変換処理 と SVG の変換処理を追加します。
module.exports = {
// DMX と SVG の変換処理を追加
transform: {
'^.+\\.svg$': 'jest-svg-transformer',
'^.+\\.jsx?$': 'ts-jest',
'^.+\\.mdx$': '@storybook/addon-docs/jest-transform-mdx',
},
}
13-1-5. NPM スクリプトの追加
スナップショットテストを実行する NPM スクリプトを追記します。
{
"scripts": {
"storyshots": "jest src/storybook.test.ts",
}
}
13-2. Puppeteer storyshots の追加
Puppeteer を使いスクレイピングすることで、ストーリーごとの画像キャプチャを撮り見た目の差分を検知することができます。
13-2-1. インストール
$ yarn add -D @storybook/addon-storyshots-puppeteer puppeteer
13-2-2. 設定の追加
Storybook の設定ファイルを作成します。
# 基本(PC用)の設定ファイルを作成
$ touch storyshots/puppeteer-storyshots.test.ts
# タブレット用の設定ファイルを作成
$ touch storyshots/puppeteer-storyshots-ipad.test.ts
# スマホ用の設定ファイルを作成
$ touch storyshots/puppeteer-storyshots-iphone8.test.ts
基本(PC用)の設定ファイルに以下を記述します。
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'
initStoryshots({
test: imageSnapshot(),
})
タブレット用の設定ファイルに以下を記述します。
import initStoryshots from '@storybook/addon-storyshots'
import { imageSnapshot } from '@storybook/addon-storyshots-puppeteer'
import { devices } from 'puppeteer'
const customizePage = (page) => 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'
const customizePage = (page) => page.emulate(devices['iPhone 8'])
initStoryshots({
suite: 'Image storyshots: iPhone 8',
test: imageSnapshot({ customizePage }),
})
13-2-3. 無視ファイルを追加
gitignore にリポジトリで管理しないファイルを追記します。
# Storyshots の差分ディレクトリを追加
src/storyshots/__snapshots__/__diff_output__
13-2-4. NPM スクリプトの追加
画像キャプチャのスナップショットテストを実行する NPM スクリプトを追記します。
{
"scripts": {
"puppeteer-storyshots": "jest storyshots/puppeteer-storyshots*.test.ts",
}
}
14. フックスクリプトの追加
リポジトリへのコミットやプッシュの際に、事前に Lint やテストを自動実行できるようにします。
これによりプロジェクトを健全に保つことができます。
14-1. lint-staged の追加
lint-staged は Git のステージに上っているファイルだけを Lint の対象にすることができるツールです。
14-1-1. インストール
lint-staged をインストールします。
$ npx mrm lint-staged
14-1-2. NPM スクリプトに追加
lint-staged を実行する NPM スクリプトを追記します。
{
"scripts": {
"lint-staged": "lint-staged"
}
}
14-2. husky の追加
husky はコミットやプッシュの前にテストなどを実行して、失敗したら止めることができる Git hooks を簡単に設定することがツールです。
14-2-1. インストール
husky のインストールをします。
$ yarn add -D husky@next
14-2-2. Git hooks の有効化
以下のコマンドで Git hooks を有効化します。
$ yarn husky install
14-2-3. フックスクリプトを追加
Git コマンド実行時に以下の処理を実行するようにします。
- コミット前にステージにあるファイルを対象に ESLint の実行
- プッシュ前にすべてのテストの実行
$ yarn husky add pre-commit "yarn lint-staged" & \
yarn husky add pre-push "yarn test && yarn storyshots && yarn puppeteer-storyshots"
15. 環境変数の追加
開発環境や本番環境ごとなどに、違った変数を用意することができます。それを環境変数といいます。Next.js では環境変数をデフォルトで設定できるようになっています。
今回はとりあえず適当に環境変数を追加します。
15-1. 環境変数ファイルを追加
開発環境と本番環境の環境変数ファイルを作成する。
$ touch .env.development & \
touch .env.production
作成した開発環境の環境変数ファイルに、開発サーバの URL を環境変数として用意する。
NEXT_PUBLIC_SITE_URL=http://localhost:3000
作成した本番環境の環境変数ファイルに、本番サーバの URL を環境変数として用意する。
NEXT_PUBLIC_SITE_URL=https://example.com
15-2. 環境変数を使用する
Document コンポーネントのサイト URL に環境変数を設定します。
class MyDocument extends Document implements MyDocumentInterface {
// 環境変数を追加
url = process.env.NEXT_PUBLIC_SITE_URL
}
これで今回お伝えする環境構築は終わりになります。
長かったですね。とはいえ、まだ追加したほうがよいものもある気もします。が、あまり詰め込み過ぎてもというところはありますのでコレくらいにしておきます。
所感
どうでしょうか?前回の記事をみている方からすると前より簡単になった気がしませんか?単純に初期設定で対応してきたのもありますが、ここ最近の流行りですかね?設定をできるだけ書かなくてよいようにしようみたいな流れは感じますね。Parcel あたりからの流れでしょうか?
個人的には明示的であることが好きではあるのですが、Webpack あたりの設定の複雑さとか破壊的アップデートとかをみていると、その辛みも感じるところではあります。そういった複雑な設定による辛みやパフォーマンスの低下を回避する為に Rome などのモノリシックなフロントエンドが産まれるんだなとかも思ったりしたり。まあ、良くも悪くも時代は繰り返しているんだなと思いつつ、数年ほどの短いエンジニア人生を振り返っていました。
ちなみに今回の記事は前回よりも見やすいように、あまり複雑にならないように余計な設定は抑えようとか、公式のドキュメントに合わせて記述しようとか、セクションはちゃんと種類別に分けようとか、個々の説明を少しでも記載しようとか、そのあたりは意識して書いてみました。そのぶん長くなっちゃいましたが。。。
年内にこの記事を書き直すなんて思ってもいなかったですが、まあ、変わっている部分も多いのでしかたないですかね。とはいえ常に最新状態に合わせて更新するとか、また大幅に書き直すなんてことはしないかとは思います。本当にただただ記事の想定外の速さでの老朽化と、私の気が向いたタイミングがあっただけという感じですかね。どうせまた、半年もすればこの記事も古くなることでしょうし。
個人的にも新たな収穫もあったのでそれはそれで良かったです。明日以降はしばらく Next.js というかフロントエンド以外のことを試して過ごす日々になりそうなモチベーションです。飽き性なんでね。とはいえ仕事はあるけれども。
それではまたいつか、次の記事にて。
Discussion