LoginSignup
95
82

More than 3 years have passed since last update.

自分流 Next.js + TypeScript + ESLint + Prettier + VSCode (+Dockerfile) 初期設定

Posted at

最近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

スクリーンショット 2019-10-17 23.53.46.png
Next.jsは9.1からpagesディレクトリをsrcディレクトリ以下に入れることが可能になりました。
またstaticディレクトリはdeprecatedになり、代わりにpublicディレクトリを使います。

次にTypeScriptと最低限必要な型定義をインストールします。

$ npm install -D typescript @types/node @types/react @types/react-dom

確認のためsrc/pages/index.tsxを作成します。

src/pages/index.tsx
import { NextPage } from "next";

const Page: NextPage = () => <h1>Helloworld!</h1>;
export default Page;

package.jsonscriptsを以下のようにします。

package.json
{
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  }
}

npm run devhttp://localhost:3000 に開発サーバが立ち上がるのを確認します。

このときNext.jsはTypeScriptを検知してtsconfig.jsonを勝手に作ってくれます。
厳し目に書きたいので

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を作成し、以下のようにします。

.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の設定を以下のようにします。

settings.json
{
  "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コンパイラにモジュール解決のための設定を追記します。

tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "~/*": ["src/*"]
    },
    ...(略)...
  },
...(略)...
}

next.config.jsを作成して、webpackにも同様にモジュール解決のための記述をします。

next.config.js
const path = require("path");

module.exports = {
  webpack: config => {
    config.resolve.alias = {
      ...config.resolve.alias,
      "~": path.resolve(__dirname, "./src")
    };
    return config;
  },
};

試しにコンポーネントを作成し、importしてみます。

src/components/Hello.tsx
const Hello: React.FC = () => <h1>HelloWorld!</h1>;

export default Hello;
src/pages/index.tsx
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
server/indexjs
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.jsonscriptsを以下のよう書き換えます。

package.json
{
  "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で以下のようにすることで、システムの環境変数も読み込まれ、優先的に利用されるようになります。

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ユーザで実行する
  • npmnodemonでアプリケーションを起動してはいけない
  • SIGINTSIGTERMといったシグナルを受け付け、Graceful Shutdownが実現できるアプリケーションコードを書く

です。特に後半2つはnodeのPID 1問題に起因するもので、Kubernetesの利用を考えている場合は要注意です。

これらのポイントを踏まえて、以下のようなDockerfileを作成しました。

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ファイルを追加します。

.dockerignore
node_modules
npm-debug.log
.next

Graceful Shutdownを実現するために @godaddy/terminus を利用します。

$ npm install @godaddy/terminus

server/index.js を以下のように書き換えます。

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を利用することで少し楽をします。

docker-compse.yml
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
あくまで自分の場合はこうしてるというだけですが、参考になればと思います。
他の人がどんな工夫をしているかとかも知りたいので、なんでもコメント頂ければ幸いです。

95
82
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
95
82