最近Next.jsを書くようになって、自分なりのコーディングスタイルが固まってきたので今後パパっとセットアップができるようにメモっておきます。
この記事では以下のことができるようになります。
- Next.jsをTypeScript / TSX で書く
- ESLintとPrettierとVSCodeを組み合わせ、保存時自動フォーマット
- importを相対パスではなく絶対パスで書く
- ServerをExpressで動かす
- 開発時は.envで、本番は環境変数によるアプリケーション設定値を扱う
- 【発展編】Kubernetesを見据えてDockerで動かす
Next.jsをTypeScript / TSX で書く
※執筆時Next.js version: 9.1.1
create-next-app
コマンドで雛形を作ってもいいのですが、余計なものもくっついてくるのでスクラッチでインストールします。
$ mkdir nextjs-starter
$ npm init -y
$ npm install next react react-dom
これでインストールできました。
ベースとなるディレクトリ構造は(自分の場合ですが)以下のようにしています。
$ mkdir -p public src/pages src/components
次にTypeScriptと最低限必要な型定義をインストールします。
$ npm install -D typescript @types/node @types/react @types/react-dom
確認のためsrc/pages/index.tsx
を作成します。
import { NextPage } from "next";
const Page: NextPage = () => <h1>Helloworld!</h1>;
export default Page;
package.json
のscripts
を以下のようにします。
{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
}
npm run dev
で http://localhost:3000 に開発サーバが立ち上がるのを確認します。
このときNext.jsはTypeScriptを検知してtsconfig.json
を勝手に作ってくれます。
厳し目に書きたいので
"strict": true
としておきます。
それ以外のコンパイラオプションは
https://www.typescriptlang.org/docs/handbook/compiler-options.html
を参照してください。
ESLintとPrettierとVSCodeを組み合わせ、保存時自動フォーマット
PrettierとESLint、ReactのLintルールをインストールします。
$ npm install -D eslint prettier eslint-plugin-react
ESLintとPrettierを併用する場合、双方でコンフリクトするルールを無効化してくれる eslint-config-prettier が必要になります。
また、 eslint-plugin-prettier を利用することで、PrettierをESLintルールとして扱えるようになります。
詳しくはPrettierのドキュメントに記載があります。
$ npm install -D eslint-config-prettier eslint-plugin-prettier
TypeScriptのLintルールも利用したいので、@typescript-eslint/parser と @typescript-eslint/eslint-pluginもインストールします。
$ npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
詳しくはtypescript-eslint のドキュメントに記載があります。
.eslintrc.json
を作成し、以下のようにします。
{
"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
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"react/prop-types": "off",
"react/react-in-jsx-scope": "off",
"@typescript-eslint/no-explicit-any": "off"
}
}
rulesはお好みでどうぞ。
VSCodeで自動フォーマットするには、VS Code ESLint extensionが必要です。
インストールしたらVSCodeの設定を以下のようにします。
{
"eslint.autoFixOnSave": true,
"eslint.validate": [
"javascript",
"javascriptreact",
{"language": "typescript", "autoFix": true },
{"language": "typescriptreact", "autoFix": true }
]
}
設定したら一度VSCodeを再起動して、自動フォーマットされるのを確認します。
importを相対パスではなく絶対パスで書く
src
ディレクトリ内にはコンポーネントやアプリ内モジュールを書いていくのですが、importするときに
import Hello from "../../../components/Hello";
と書くのは面倒なので、
import Hello from "~/components/Hello";
と書けるようにします。
TypeScriptコンパイラにモジュール解決のための設定を追記します。
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"~/*": ["src/*"]
},
...(略)...
},
...(略)...
}
next.config.js
を作成して、webpackにも同様にモジュール解決のための記述をします。
const path = require("path");
module.exports = {
webpack: config => {
config.resolve.alias = {
...config.resolve.alias,
"~": path.resolve(__dirname, "./src")
};
return config;
},
};
試しにコンポーネントを作成し、importしてみます。
const Hello: React.FC = () => <h1>HelloWorld!</h1>;
export default Hello;
import { NextPage } from "next";
import Hello from "~/components/Hello";
const Page: NextPage = () => <Hello />;
export default Page;
問題なく表示できればOKです。
ServerをExpressで動かす
Next.jsのデフォルトサーバではなく、Expressで動作させようと思います。
理由としてはExpressのほうが使い慣れているからです。
$ npm install express
$ mkdir server
const express = require("express");
const next = require("next");
const http = require("http");
const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== "production";
const nextApp = next({ dev });
const handle = nextApp.getRequestHandler();
nextApp.prepare().then(() => {
const app = express();
app.get("*", (req, res) => {
return handle(req, res);
});
const server = http.createServer(app);
server.listen(port, err => {
if (err) throw err;
console.log(`> Ready on http://localhost:${port}`);
});
});
package.json
のscripts
を以下のよう書き換えます。
{
"scripts": {
"dev": "node server/index.js",
"build": "next build",
"start": "NODE_ENV=production node server/index.js"
}
}
getInitialProps
以外のサーバサイドのコードは分離させたいのでserver
というディレクトリを掘っていますが、そもそもサーバサイドのコードが肥大化している場合はマイクロサービスに切り出す、という気持ちを持っています。
開発時は.envで、本番は環境変数によるアプリケーション設定値を扱う
Twelve-Factor Appによると、設定を環境変数に格納する べきであるとあります。
とはいえ開発時は.env
を利用するのが複数人で開発する上でも楽なので、開発時は.envを、その他の環境では環境変数が優先して利用されるようにします。
Next.jsの場合はdotenv-webpackを利用するのが良いです。
$ npm install -D dotenv-webpack
next.config.js
で以下のようにすることで、システムの環境変数も読み込まれ、優先的に利用されるようになります。
const path = require("path");
const Dotenv = require("dotenv-webpack");
module.exports = {
webpack: config => {
config.resolve.alias = {
...config.resolve.alias,
"~": path.resolve(__dirname, "./src")
};
config.plugins = [
...config.plugins,
// 環境変数を優先して読み込む
new Dotenv({
systemvars: true
})
];
return config;
}
};
.env
ファイルは必ず.gitignore
に追加するようにしてください。
【発展編】 Kubernetesを見据えてDockerで動かす
nodeアプリケーションをDockerで動かすことに関しては、DockerキャプテンであるBret Fisher氏の記事や、DockerConでの発表資料が参考になります。
ポイントは
-
node:alpine
よりnode:slim
のほうがおすすめ -
root
ユーザではなくnode
ユーザで実行する -
npm
やnodemon
でアプリケーションを起動してはいけない -
SIGINT
やSIGTERM
といったシグナルを受け付け、Graceful Shutdownが実現できるアプリケーションコードを書く
です。特に後半2つはnodeのPID 1問題に起因するもので、Kubernetesの利用を考えている場合は要注意です。
これらのポイントを踏まえて、以下のようなDockerfileを作成しました。
# Base Image
FROM node:10.16.3-slim as base
RUN apt-get update && apt-get install -y \
git
ENV TINI_VERSION v0.18.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
EXPOSE 3000
RUN mkdir /app && chown -R node:node /app
WORKDIR /app
USER node
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci && npm cache clean --force
# Develop Image
FROM base as dev
ENV NODE_ENV=development
ENV PATH=/app/node_modules/.bin:$PATH
CMD ["npm", "run", "dev"]
# Source Image
FROM base as source
COPY --chown=node:node . .
# Production Image
FROM source as prod
ENV NODE_ENV=production
RUN npm run build
ENTRYPOINT ["/tini", "--"]
CMD ["node","server/index.js"]
余計なファイルが転送されないように.dockerignore
ファイルを追加します。
node_modules
npm-debug.log
.next
Graceful Shutdownを実現するために @godaddy/terminus を利用します。
$ npm install @godaddy/terminus
server/index.js
を以下のように書き換えます。
const express = require("express");
const next = require("next");
const http = require("http");
const { createTerminus } = require("@godaddy/terminus"); // <-- 追加
const port = parseInt(process.env.PORT, 10) || 3000;
const dev = process.env.NODE_ENV !== "production";
const nextApp = next({ dev });
const handle = nextApp.getRequestHandler();
nextApp.prepare().then(() => {
const app = express();
app.get("*", (req, res) => {
return handle(req, res);
});
const server = http.createServer(app);
// 追加
// Graceful shutdown
function beforeShutdown() {
// SIGTERMシグナルが送信された後でもリクエストが流れてくる可能性を考慮し5秒待つ
return new Promise(resolve => {
setTimeout(resolve, 5000);
});
}
createTerminus(server, { beforeShutdown });
// 追加ここまで
server.listen(port, err => {
if (err) throw err;
console.log(`> Ready on http://localhost:${port}`);
});
});
開発時はdocker-compose
を利用することで少し楽をします。
version: '3.7'
services:
node:
build:
dockerfile: Dockerfile
context: .
target: dev
volumes:
- .:/app:delegated
env_file: .env
ports:
- "3000:3000"
targetをdev
にすることで、Dockerfileに記したマルチステージのdev用イメージを指定しています。
開発時はホスト側でnpm install
は行わず、
$ docker-compose run node npm install
でDocker内でnpmコマンドを叩くようにします。
$ docker-compose up
で開発サーバを起動します。
まとめ
出来上がったものがこちらになります。
https://github.com/matkatsu/nextjs-starter
あくまで自分の場合はこうしてるというだけですが、参考になればと思います。
他の人がどんな工夫をしているかとかも知りたいので、なんでもコメント頂ければ幸いです。