ゼロから始める TypeScript対応 Reactボイラープレート入門(前編)

はじめまして。7月から業務委託として、FiNCのサーバーサイドエンジニアを担当している@yhirano55です。最近、来年3月に開催予定のRails Developers Meetup 2019の準備を始めました(次回は3トラック同時で進行する予定です)。

本記事では、Railsエンジニア向けnpm initから始める、TypeScript対応の Reactボイラープレートの作り方を紹介します。チームや組織で共通するボイラープレートがあると、SPA(Single Page Application)を量産する際に生産性が爆速になるのですが、ちょうど最近、Reactボイラープレートを作る機会を得まして、その過程で得られた試行錯誤を記事としてまとめてみました。

モチベーション

Ruby on Railsを通してフロントエンドの実装をしていると、各種設定は、webpackerを使って簡単(暗黙的)に実現できてしまうため、その分、いざイチから設定しようとすると分からないことが多かったりします。

本記事ではそれぞれの設定方法の流れを理解することで、ゼロから設定できるようになることを目指します(そのため、Railsもwebpackerも利用しません)。もちろん、create-react-app やスターが多くついている野生のボイラープレートを使うのも簡単で便利ではありますが、それぞれが何をしているかの最低限の知識すら無いと、依存ライブラリをアップグレードする際などに困ることになります。なので、そんな場合は、ひとまず本記事に書いてある通りにコマンドを実行してみて、一度、自分で手を動かして構築してみることをオススメします。作業時間は、概ね30分です。

対象読者

  • 普段はWebpackerで設定を済ませているRailsエンジニア
  • NodeJSのアプリをゼロから設定したことがない
  • 最近のJavaScriptの雰囲気をざっくり把握したい
  • 普段、JavaScriptを避けがちなサーバーサイドエンジニア

動作環境

本記事は、2018年7月9日時点での下記の環境での動作を保証しています。ライブラリの更新などが頻繁にありますので、将来的に動作することはお約束できませんので、あらかじめご了承ください。

macOS 10.13.4
node v10.5.0
npm 6.1.0
Docker version 18.03.1-ce-mac65
docker-compose version 1.21.1

各種ライブラリの導入理由について

本記事のボイラープレートはFiNCで開発するSPAで実際に利用される想定で実装されています。定番ツールがほとんどですが、『それぞれのライブラリが、何を考慮してチームで採用されたのか』をカンタンに触れておきます。

ビルドツール(npm, webpack, webpack-serve)

まずはビルドツールですが、NodeJSのプロジェクトなので、Yarnではなく、npmを採用しました。Dockerで利用する場合のベースイメージをそのまま利用できるのがメリットでしょうか。

モジュールバンドラは、定番のwebpackを採用しています(ゼロコンフィグのpercelはパフォーマンスがよさそうなので気になっていますが、あまり試したことがないので、現時点ではwebpackが妥当と判断)。利用したいアドオン等の都合で、v3が利用されることもありますが、将来を見越して、v4を採用しました。開発用のサーバーには、webpack-serveを採用しました。webpack-dev-serverは現在メンテナンスモードなのと、特別凝った使い方はしない予定なので、webpack-serveで問題ないという判断でした。

言語(TypeScript)

長期的に保守するアプリケーションであれば、やはり型は大事です。メンバーの入れ替わりがあっても、壊れにくい仕組みで担保するのは、導入時期の重要な判断となります。本ボイラープレートでは、TypeScriptを採用しています。Flowでも問題ないのですが、TypeScriptの場合、トランスパイラであるBabelを使わずに、JSXの変換が標準機能として提供されているので、TypeScriptを採用しています(ちなみにBabelとの併用も可能です)。

静的ファイル(url-loader, file-loader)

こちらは定番のurl-loaderとfile-loaderを使います。

CSS(css-modules)

SPAでCSSを管理する方法としては、次のアプローチがあります。

  1. グローバルにCSS/SASSを書く
  2. CSS in JS(styled-components, styled-jsx)
  3. css-modules

1は、BEMやSMACSSなどの命名規則で保守するテクニックがありますが、実装者が入れ替わった途端に壊れやすいため採用していません。スコープがない時点で、辛い未来しか待っていないのではないでしょうか。そうなると、2か3となるのですが、CSSはCSSファイルとして存在していた方が、デザイナーも編集することができるという点で、DX(開発エクスペリエンス)が良く、3が採用されました。css-modulesであれば、extract-text-webpack-pluginを使って、CSSファイルをJSと切り離して生成・配信することもできるので利点があります。コンポーネントとスタイルの関係を考慮したとき、最終的に1:1になることがほとんどなので、styled-jsxも検討しましたが、過去のプロジェクトでもcss-modulesが採用された経緯もあり、そこもDXの観点で優先されました。

テスト環境(Jest, Enzyme, Puppeteer)

テストフレームワークでは、Reactと同じFacebook謹製のJestを採用しました。単体テストでは、コンポーネントやreducerが主な対象とし、react-test-rendererを使って、スナップショットを書き出すテストを書き、より詳細なテストを行う場合は、Enzymeを使います。E2Eテストでは、Google製のPuppeteerを採用しました。こちらはRailsアプリケーションで言うところのsystem testと同様、ログインなど、シナリオに沿ったテストを行います。

コンポーネント開発効率化(Storybook)

効率的にコンポーネントを開発するためのデファクトスタンダードなツールとなっているStorybookを採用しました。複数人で分散作業をする場合は、特に必須なツールで、ストーリーを書くこととテストを書くことはもはや同義で、デザインの崩れなども発見できるため、必ず使った方がよいツールとなります。

静的解析ツール(Tslint, Prettier)

複数人でコードを管理していると、メンバー間での書き方の差異が生じるので、静的解析ツールは必須です。違反コードの指摘だけでなく、自動補正もできるので、生産性の向上も期待できます。

開発効率化(Docker)

開発環境のポータビリティを担保するため、Docker環境を初期段階から用意しています。新規メンバーがすぐに環境を構築して、作業が始められるようにしておくのは、Ruby on Railsの bin/setup スクリプトのように、あらかじめ整えておきたい要素となります。なお、CIと組み合わせた継続的デリバリーを実現する場合も、あらかじめDockerfileを用意しておけば、すぐに対応することが可能なので便利です。


前置きが長くなりましたが、それではボイラープレートを作っていきましょう。まずはディレクトリを作成し、 package.jsonを初期化します。

$ mkdir ts-react-boilerplate && cd $_
$ npm init -y
$ git init
$ git add .
$ git commit -m "npm init -y"

.gitignore も追加しておきます。

.DS_Store
node_modules/
dist/
coverage/
storybook-static/
typings/**/*.css.d.ts
typings/**/*.scss.d.ts

Step 1. ビルドツール(webpack, webpack-serve)

まずは開発用のオプションを追加し、ライブラリをインストールします。

$ npm i -D webpack webpack-cli webpack-serve

Step 2.React

続いて、Reactをインストールします。

$ npm i react react-dom

Step 3.言語(TypeScript)

続いて、TypeScript環境を作ります。今回はSPAなので、エントリーポイントとなるHTMLを生成できるよう、html-webpack-pluginもインストールします。 typesは型定義となります。

$ npm i -D typescript awesome-typescript-loader source-map-loader
$ npm i -D @types/react @types/react-dom
$ npm i -D html-webpack-plugin

tsconfig.json を追加します。

{
"compilerOptions": {
"baseUrl": "src",
"rootDirs": ["src"],
"outDir": "dist",
"sourceMap": true,
"module": "commonjs",
"target": "es5",
"lib": ["es5", "es6", "es7", "es2018", "dom"],
"jsx": "react",
"allowJs": true,
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": true,
"experimentalDecorators": true
},
"include": ["./src/**/*"],
"exclude": [
"dist",
"node_modules"
]
}

webpack.config.js を追加します。

const path = require("path");
const webpack = require("webpack");
const htmlWebpackPlugin = require("html-webpack-plugin");
module.exports = (env, argv) => {
const mode = process.env.NODE_ENV || "development";
const isProduction = mode === "production";
return {
mode: mode,
entry: {
main: [path.resolve(__dirname, "src/index.tsx")]
},
output: {
filename: isProduction ? "bundle.[chunkhash].js" : "[name].js",
path: path.resolve(__dirname, "dist")
},
devtool: isProduction ? false : "source-map",
resolve: {
extensions: [".ts", ".tsx", ".js", ".json"]
},
optimization: {
splitChunks: {
name: "vendor",
chunks: "initial",
}
},
module: {
rules: [
{ test: /\.tsx?$/, loader: "awesome-typescript-loader" },
{ enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
]
},
plugins: [
new htmlWebpackPlugin({
template: path.resolve(__dirname, "src/index.html"),
})
]
}
}

生成前のソースを src に配置し、ビルド後の成果物を dist に出力します。

$ mkdir -p src/components

サンプルとなるコンポーネント src/components/Hello.tsx を追加します。

import * as React from "react"
export type HelloProps = {
compiler: string
framework: string
}
export const Hello = (props: HelloProps) => (
<h1>
Hello from {props.compiler} and {props.framework}!
</h1>
)

エントリーポイントとなる、 src/index.tsx を追加します。

import * as React from "react"
import * as ReactDOM from "react-dom"
import { Hello } from "./components/Hello"
ReactDOM.render(
<Hello compiler="TypeScript" framework="React" />,
document.getElementById("root")
)

出力されるHTMLのテンプレート src/index.html を追加します。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>Hello React with TypeScript</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

npx webpack-serve を実行( npx はbundlerでいうところの bundle exec )し、 http://localhost:8080 を確認しましょう。問題なければ、 package.json に実行スクリプトを追加します。

...
"scripts": {
"start:dev": "webpack-serve --host 0.0.0.0",
"build": "NODE_ENV=production webpack",
"build:dev": "webpack --watch --progress"
},

Step 4. 静的解析ツール(Tslint, Prettier)

静的解析ツールをインストールしていきます。

$ npm i -D tslint prettier tslint-config-prettier tslint-config-airbnb

.prettierignore を追加します。

package.json
.DS_Store
*.html
*.css
*.scss
*.snap

.prettierrcを追加します。

{
"semi": false
}

tslint.json を追加します。

{
"defaultSeverity": "error",
"extends": [
"tslint:latest",
"tslint-config-airbnb",
"tslint-config-prettier"
],
"jsRules": {},
"rules": {
"interface-over-type-literal": false,
"object-literal-sort-keys": false,
"no-implicit-dependencies": [true, "dev"],
"no-boolean-literal-compare": false,
"variable-name": false
},
"rulesDirectory": []
}

あとは package.json に実行コマンドを追加すれば、リンターやコードフォーマッターが利用できるようになります。

  "scripts": {
...
"lint": "tslint src/**/*.ts src/**/*.tsx --format stylish",
"fmt": "prettier --config .prettierrc --write src/** && tslint src/**/*.ts src/**/*.tsx --fix",

Step 5. テスト環境(Jest, Enzyme, Puppeteer)

次はテスト環境の構築です。

$ npm i -D jest ts-jest @types/jest
$ npm i -D react-test-renderer @types/react-test-renderer
$ npm i -D enzyme enzyme-adapter-react-16 enzyme-to-json
$ npm i -D puppeteer jest-puppeteer @types/puppeteer

設定ファイルを追加していきます。まずは、 jest.config.js から。

module.exports = {
setupTestFrameworkScriptFile: "<rootDir>/src/setupTests.ts",
moduleFileExtensions: ["js", "jsx", "json", "ts", "tsx"],
transform: {
"\\.(ts|tsx)$": "ts-jest",
},
modulePaths: ["./src"],
testMatch: ["<rootDir>/src/**/*.(test|spec).+(ts|tsx)"],
collectCoverage: true,
snapshotSerializers: ["enzyme-to-json/serializer"],
};

続いて、 src/setupTests.ts 。Enzymeを有効化しています。

import { configure } from "enzyme"
import * as EnzymeAdapter from "enzyme-adapter-react-16"
configure({ adapter: new EnzymeAdapter() })

サンプルのテストコードを src/components/__tests__/Hello.test.tsxを配置しましょう。 npx jestで単体テストの実行確認ができます。

import * as React from "react"
import * as renderer from "react-test-renderer"
import { shallow } from "enzyme"
import { Hello } from "../Hello"
describe("<Hello />", () => {
describe("with react-test-renderer", () => {
const component = renderer.create(
<Hello compiler="TypeScript" framework="React" />
)
    it("should display Hello", () => {
const tree = component.toJSON()
expect(tree).toMatchSnapshot()
})
})
  describe("with enzyme", () => {
it("should display expected elements", () => {
const wrapper = shallow(<Hello compiler="TypeScript" framework="React" />)
expect(wrapper.text()).toBe("Hello from TypeScript and React!")
})
})
})

npx jestで単体テストが実行できたら、スクリプトを追加しましょう。

...
"scripts": {
...
"test": "jest",
"test:watch": "jest --watchAll",

続いてE2Eテストの環境を構築します。PuppeteerはHeadless Chromeを通して、テストを行うのでサーバーの起動が必要です。

$ npm i express

サーバー起動スクリプト server.js を追加します。

const path = require("path");
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
const host = process.env.HOST || "0.0.0.0";
const env = process.env.NODE_ENV || "development";
app.use(express.static(path.join(__dirname, "dist")));
app.listen(port, host, () => {
console.log(`[${env}] express server listening on ${host}:${port}`);
});

E2Eテスト用の設定ファイル jest.config.feature.jsを追加します。

const host = process.env.HOST || "0.0.0.0";
const port = process.env.PORT || 3000;
module.exports = {
preset: "jest-puppeteer",
moduleFileExtensions: ["js", "jsx", "json", "ts", "tsx"],
testMatch: ["<rootDir>/feature/**/*.(test|spec).+(ts|tsx)"],
transform: {
"\\.(ts|tsx)$": "ts-jest",
},
globals: {
"host": `${host}:${port}`
}
};

jest-puppeteer の設定ファイル jest-puppeteer.config.jsを追加します( jest-puppeteer は起動・終了を簡略化するヘルパーです)。

module.exports = {
server: {
command: "node server.js",
port: process.env.PORT || 3000,
debug: true
},
launch: {
args: ["--no-sandbox", "--headless", "--disable-gpu"]
}
};

テスト feature/App.test.tsを追加しましょう。サンプルなのでテスト内容はあくまで動作検証用です。

import { Page } from "puppeteer"
declare const page: Page
declare const host: string
describe("App", () => {
beforeAll(async () => {
await page.goto(`http://${host}/`)
})
  it("should display 'Hello from TypeScript and React!' text on page", async () => {
const text = await page.evaluate(() => document.body.textContent)
expect(text).toContain("Hello from TypeScript and React!")
})
})

tsconfig.json の除外リストに設定ファイルやテスト用のファイルを追加します。

...
"exclude": [
"coverage",
"dist",
"node_modules",
"jest.config.js",
"jest.config.feature.js",
"jest-puppeteer.config.js",
"./src/setupTests.ts",
"./src/**/*.test.ts",
"./src/**/*.test.tsx",
"./feature/**/*.test.ts",
"./feature/**/*.test.tsx"
]

実行スクリプトを package.jsonに追加して、テスト環境構築は完了です(実行スクリプトの接頭辞に pre を与えると、事前実行します)。

...
"scripts": {
...
"pretest:feature": "npm run build",
"test:feature": "jest --config=jest.config.feature.js",

Step 6. 静的ファイルとCSS(url-loader, file-loader, css-modules)

続いて静的ファイルとCSSです。

$ npm i -D css-loader style-loader sass-loader node-sass postcss-loader style-loader file-loader url-loader autoprefixer
$ npm i -D extract-text-webpack-plugin@next
$ npm i -D jest-css-modules-transform
$ npm i -D typed-css-modules typed-css-modules-loader
$ npm i react-css-modules normalize.css

TypeScriptとcss-modulesを組み合わせると、importするstyleにもインターフェイスの定義が求められます。これらを自動生成するため、typed-css-modulesとtyped-css-modules-loaderを追加しています。この件については以下を読むと理解できるでしょう。

まずは webpack.config.js を修正します。静的ファイルについては、Webフォント等も読み込めるように各種拡張子を追加しますが、 url-loaderのファイルサイズの上限指定をしないと、すべてのファイルをData URI スキームで扱ってしまうため、limitオプションを追加します。

const path = require("path");
const webpack = require("webpack");
const autoprefixer = require("autoprefixer");
const htmlWebpackPlugin = require("html-webpack-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = (env, argv) => {
const mode = process.env.NODE_ENV || "development";
const isProduction = mode === "production";
return {
mode: mode,
entry: {
main: [
path.resolve(__dirname, "src/index.tsx")
]
},
output: {
filename: isProduction ? "bundle.[chunkhash].js" : "[name].js",
path: path.resolve(__dirname, "dist")
},
devtool: isProduction ? false : "source-map",
resolve: {
extensions: [".ts", ".tsx", ".js", ".json", ".css", ".scss", ".jpg", ".jpeg", ".gif", ".png", ".bmp", ".tiff", "woff", "eot", "ttf", ".svg", ".ico"]
},
optimization: {
splitChunks: {
name: "vendor",
chunks: "initial",
}
},
module: {
rules: [
{ test: /\.tsx?$/, loader: "awesome-typescript-loader" },
{ enforce: "pre", test: /\.js$/, loader: "source-map-loader" },
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: [
{
loader: "css-loader",
options: {
importLoaders: 2,
modules: true,
localIdentName: "[name]-[local]-[hash:base64:5]",
sourceMap: !isProduction,
minimize: isProduction
}
},
{
loader: "typed-css-modules-loader",
options: {
camelCase: true,
searchDir: "./src",
outDir: "./typings"
}
},
{
loader: "postcss-loader",
options: {
sourceMap: !isProduction,
plugins: [
autoprefixer()
]
}
}, {
loader: "sass-loader"
}
]
})
}, {
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
}, {
test: /\.(png|jpe?g|gif|bmp|tiff|woff|eot|ttf|svg|ico)$/,
use: [{
loader: "url-loader",
options: {
limit: 8192,
name: isProduction ? "[name]-[hash].[ext]" : "[name].[ext]"
}
}]
}
]
},
plugins: [
new htmlWebpackPlugin({
template: path.resolve(__dirname, "src/index.html"),
}),
new ExtractTextPlugin({
filename: isProduction ? "bundle.[chunkhash].css" : "[name].css",
allChunks: true
}),
]
}
}

tsconfig.json も修正します。

  {
"compilerOptions": {
"baseUrl": "src",
+ "rootDirs": ["src", "typings"],
"outDir": "dist",

jest.config.js も修正します。

  module.exports = {
...
transform: {
"\\.(ts|tsx)$": "ts-jest",
+ "\\.(css|styl|less|sass|scss)$": "jest-css-modules-transform",
},

autoprefixer用の設定 browserslistを追加します。

last 1 version
> 1%
IE 11

src/index.tsxnormalize.css を適用しましょう。これは単純にimportするだけです。

  import * as React from "react"
import * as ReactDOM from "react-dom"
+ import "normalize.css"

コンポーネント用のCSSも書いてみましょう。 src/components/Hello.scss を追加します。

$color: #888;
.title {
color: $color;
font-weight: 700;
}

続いてコンポーネント src/components/Hello.tsx を次のように修正します。

  import * as React from "react"
+ import * as style from "./Hello.scss"
  ...
export const Hello = (props: HelloProps) => (
+ <h1 className={style.title}>
...
)

ただし、このままでは先述した型の問題でエラーになるので、CSSのインターフェイスの定義を書き出すスクリプトを package.json に追加する必要があります。

   ...
"scripts": {
+ "tcm": "tcm ./src -c -o ./typings",
+ "prestart:dev": "npm run tcm",
"start:dev": "webpack-serve --host 0.0.0.0",
+ "prebuild": "npm run tcm",
"build": "NODE_ENV=production webpack",
+ "prebuild:dev": "npm run tcm",

各種コマンド実行前に型定義ファイルを自動生成するため、特に意識することなく、開発が捗ります。なお、自動生成した型定義ファイルは、.gitignoreに条件を追加( typings/**/*.scss.d.ts)して、リポジトリには含めないようにします。

なお、画像についても型を定義するのがよいのですが、CommonJS形式 const image = require("./image.jpg"); で読み込めば型定義は不要なので、そちらを使います(もっと良い方法がありそうですが…)。

Step 7. コンポーネント開発効率化(Storybook)

Storybookの設定です。セットアップ用の getstorybook コマンドがあるので、それを npx から利用したいところですが、Webpack v4を利用しているため、事前にグローバルにCLIをインストールする必要があります。

$ npm i -g @storybook/cli@alpha
$ getstorybook

設定ファイルが一式生成され、 package.json にコマンドも追加されます。便利ですが、そのままでは利用できません。まずは不要なパッケージも追加されるので、アンインストールします。

$ npm uninstall -D babel-core babel-runtime

型定義ファイルと、ワークアラウンドな対応となりますが、動作させるための core-js をインストールします。

$ npm i -D @types/storybook__react @types/storybook__addon-actions
$ npm i -D core-js

続いて、設定していきます。まずは .storybook/config.js の拡張子を修正します。

import { configure } from '@storybook/react';
// automatically import all files ending in *.stories.(ts|tsx)
const req = require.context('../stories', true, /.stories.tsx?$/);
...

.storybook/webpack.config.js を修正します。

const autoprefixer = require("autoprefixer");
module.exports = {
resolve: {
extensions: [".ts", ".tsx", ".js", ".json", ".css", ".scss", ".jpg", ".jpeg", ".gif", ".png", ".bmp", ".tiff", "woff", "eot", "ttf", ".svg", ".ico"]
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "awesome-typescript-loader"
}, {
test: /\.scss$/,
use: [
{
loader: "style-loader",
},
{
loader: "css-loader",
options: {
importLoaders: 2,
modules: true,
localIdentName: "[name]-[local]-[hash:base64:5]",
sourceMap: true,
minimize: false
}
},
{
loader: "typed-css-modules-loader",
options: {
camelCase: true,
searchDir: "./src",
outDir: "./typings"
}
},
{
loader: "postcss-loader",
options: {
sourceMap: true,
plugins: [
autoprefixer()
]
}
}, {
loader: "sass-loader"
}
]
}, {
test: /\.css$/,
use: [
{
loader: "style-loader"
},
{
loader: "css-loader"
}
]
}, {
test: /\.(png|jpe?g|gif|bmp|tiff|woff|eot|ttf|svg|ico)$/,
use: [{
loader: "url-loader",
options: {
limit: 8192
}
}]
}
]
},
plugins: []
};

.storybook/addons.js を追加します。

const action = require('@storybook/addon-actions/register');
const linkTo = require('@storybook/addon-links/register');

tsconfig.jsonrootDirs を修正し、 stories を追加します。

  {
"compilerOptions": {
"baseUrl": "src",
+ "rootDirs": ["src", "stories", "typings"],

stories/index.stories.tsx を修正しましょう。

import * as React from 'react';
import { storiesOf } from '@storybook/react';
import { Hello } from "../src/components/Hello"
import 'normalize.css';
const stories = storiesOf('Hello', module);
stories.add('to Storybook', () => <Hello compiler="TypeScript" framework="React" />);

npm run storybooklocalhost:6006 を開くと、コンポーネントの動きが確認できます。

Docker環境でも動作できるように、 package.json の起動スクリプトを一部修正します。

...
"scripts": {
...
"storybook": "start-storybook -h 0.0.0.0 -p 6006",

Step 8. 開発効率化(Docker)

Dockerfile を追加します。

FROM node:10-slim
ENV APP_ROOT /src/app
RUN apt-get update -y && \
apt-get install ca-certificates \
gconf-service \
libasound2 \
libatk1.0-0 \
libatk1.0-0 \
libdbus-1-3 \
libgconf-2-4 \
libgtk-3-0 \
libnspr4 \
libnss3 \
libx11-xcb1 \
libxss1 \
libxtst6 \
fonts-liberation \
libappindicator3-1 \
xdg-utils \
lsb-release \
wget \
curl \
xz-utils -y --no-install-recommends && \
wget https://dl.google.com/linux/direct/google-chrome-unstable_current_amd64.deb && \
dpkg -i google-chrome*.deb && \
apt-get install -f && \
apt-get clean autoclean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* google-chrome-unstable_current_amd64.deb
WORKDIR $APP_ROOT
COPY package*.json ./
RUN npm install
COPY . $APP_ROOT

続いて、 docker-compose.yml

version: '3'
services:
app:
image: ts-react-boilerplate
build: .
command: /bin/sh -c 'while true; do sleep 60; done'
volumes:
- .:/src/app
- /src/app/node_modules
ports:
- '3000:3000'
- '6006:6006'
- '8080:8080'
- '8081:8081'
tty: true
stdin_open: true

.dockerignore も追加しましょう。

node_modules
dist
.git
coverage
storybook-static
.dockerignore
Dockerfile

これでDocker環境で開発できるようになりました。それぞれ、以下のコマンドで動作します。

$ docker-compose up --build -d
$ docker-compose exec app npm run start:dev

ただ、毎回 docker-compose exec appと入力するのは現実的ではないので、 docker/exec に次のようなスクリプトを追加します(以下、それぞれ chmod +x docker/* で実行権限を与えてください)。

#!/bin/bash
exec docker-compose exec app "$@"

続いて、 docker/npmdocker/npm run ... でコマンドが実行できます。

#!/bin/bash
exec docker/exec npm "$@"

続いて、 docker/bash 。コンテナにすぐに接続したいときに便利です。

#!/bin/bash
exec docker/exec bash "$@"

なお、 docker/* のスクリプトは、rails-contributorsというOSSの作者であるXavier Noria氏のアイディアを利用しています。

同じ要領で、 docker/updocker/down を追加すれば、Dockerを使った開発に馴染みのない、たとえばプランナーやデザイナーでも、Dockerさえインストールされていれば手元で動かせるようになるので、何かと便利かもしれません。


完成品のリポジトリは以下です。

いかがでしたでしょうか? 実際に手を動かして、ボイラープレートを作ってみると、各種ライブラリがどういう目的で導入されているのか、webpackercreate-react-app では隠蔽されている知識が身につき、フロントエンドの開発も捗るのではないかと思います。

加えて、今回のボイラープレートに、react-reduxreact-router を組み合わせればより実用的に仕上がりますが、それらは後編にて紹介できればと思いますので、どうぞご期待ください。

最後にお知らせです。

ヘルスケアサービスを展開する株式会社FiNCでは、ReactやNodeJSに関心があるRailsエンジニアを広く募集しています。Rubyも書きたいけど、JavaScriptも書いていきたい方は、ぜひ一緒に働きましょう😎😎😎

あと、FiNCがサポートしているMicroServices Meetup #7が7月31日(火)に開催されます。テーマは、Micro Frontendsなので、フロントエンドの未来に関心があるエンジニアはぜひ参加してみてはいかがでしょうか?

Like what you read? Give Yoshiyuki Hirano a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.