Next.jsを使わないReactのSSR解説で分かりやすいやつが無かったので、書きます!
環境構築からとっても丁寧にやっていきます
##最初に
読んで欲しい人
-
『SSRしたいけど難しそう....』
って人はもちろん
-
『何も分からないけどSPAが作ってみたい!』
って人も実際に作って楽しめるように書いてます。
SPA (Single Page Application) : ネイティブアプリ(AppStoreやGooglePlayからインストールできるやつ)っぽいWebサイトのこと。ReactやVueなどのJavaScriptフレームワークを使って作るのが一般的です。
作るもの
SSRを使用した簡単なカウントアップアプリを作ってみます。
最終的な制作物のソースコードはこちらです。
学べること
- Node, Yarn, Git, ESList, Babel, Webpackのフロントエンド開発環境の構築。
- React Hooks(useState)を使って、モダンな書き方のReactを書いてみる。
- TypeScriptを使ってみる。
- Express.jsを使ってSSRができるようになる。
ReactやTypeScriptの詳しい記法については踏み込みません。良質な記事がQiita等にたくさんあるので、気になるところは調べながら進めてください。
進め方について
なるべくコピペはせず、何をしているか考えて進めていただきたいです。
設定ファイルを暗記するのは無駄なので、設定ファイルはコピペしてください。
SSR(Server Side Rendering)とは
Reactなどで作成されたSPAアプリは、クライアント(ユーザーが使っているブラウザ)で実行され、UI (DOM)を形成します。その性質上、以下のような問題があります。
- Googleのクローラが正しくインデックスしてくれない。
- OGPを含む
<head/>
が全てのページで同じになるので、Twitterカードなどが意図通りに表示できない。 - 初回表示速度(クリティカルレンダリングパス)に時間がかかる。
そこで、サーバー側でJavaScriptをHTMLをして返すことをSSR(Server Side Rendering)と呼びます。やっていることはほとんどPHPなどのサーバーサイド言語と同じですが、SSRと呼ぶので難しく感じるだけです。
この記事ではSSRを解説しますが、小規模な(ページ数が限定されている)プロダクトでは、SSRの代わりにPrerenderingという手法を検討した方がいいです。これは、あらかじめSSG(Static Site Generator)を使用して、JavaScriptをHTMLファイルに変換して配置しておく方法です。SSRより簡潔で、リクエストごとにサーバーでHTMLを生成する処理が必要がないので、メンテナンスがしやすいことと、サーバーにかかる負荷がSSRより小さいことがメリットです。
クローラ : Googleが世界中にあるサイトを把握するために、いろんなサイトを巡回させているロボットのこと。クローラがサイトの情報を保存することをインデックスと呼びます。最近GoogleのクローラはJavaScriptを実行してくれるようになったらしいですが、まだ完全ではなくSEOが劣る可能性があります。
SEO (Search Engine Optimization) : Googleなどの検索結果の上に方に表示されるように調整すること。
UI (User Interface) : サイトの見た目のこと。
DOM (Document Object Model) : ブラウザがHTMLを元に形成するツリー状の構造のこと。よく混同されますがHTMLとは別のものです。JavaScriptから操作できます。
OGP (Open Graph Protocol) : Twitterカードなど、SNS上でリンク先の情報を表示するときに使われるheadタグです。OGPをクロールに来るbotはGoogleのbotと違ってJavaScriptを解釈しません。
環境構築
フロントエンド(クライアントサイド)の環境構築はとても面倒で楽しくないです。環境構築が終われば楽しいので、投げ出さずに頑張ってください!
推奨環境
環境構築はmacOSを使っている前提で進めます。
Windows等ご利用の方は別途調べながら進めてみてください。
Homebrew
Macのパッケージマネージャです。パッケージとはソフトウェアのこと。Qiitaなど多くの記事でインストールしてあることが前提になっているので、Macで開発するなら必須です。
-
AppStoreからXcodeをインストールします。
-
以下のコマンドを実行して、コマンドライン・デベロッパツールをインストールします。
$ xcode-select --install
-
以下のコマンドを実行して、Homebrewをインストールします。
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
-
以下のコマンドを実行して、
Example usage: ...
が表示されればOKです。$ brew
VSCode
たぶん現時点では最も優れたコードエディタであるVSCodeをインストールします。
- VSCode公式ホームページのダウンロードボタンからVSCodeをダウンロードします。
- ダウンロードしたzipファイルを解凍してインストーラを実行し、インストールを完了してください。
###Node & Yarn
Nodeは、サーバーサイドで使えるJavaScript環境です。クライアントサイドの開発環境(Webpack等)にも必須なのでよく誤解がありますが、一応サーバーで動くものです。
Yarnは、Nodeのパッケージマネージャです。同じNodeのパッケージマネージャに有名なnpmがありますが、Yarnはnpmよりインストールの速度が速く、npmの上位互換と考えていいと思います。
NodeはNodebrewでバージョン管理すると便利なので、ここではNodebrewを使用してNodeをインストールします。
- 以下のコードを実行してください。
※
$
(コマンドプロンプト)が書かれているコードは、Macのターミナルで実行してください。(iTerm等でも可)
# nodeのバージョン管理のためのnodebrewのインストール
$ brew install nodebrew
# nodebrewのディレクトリのセットアップ
$ nodebrew setup
# パスを通しておく
$ echo "export PATH=$HOME/.nodebrew/current/bin:$PATH" >> ~/.bash_profile
$ source ~/.bash_profile
# nodeのダウンロードとセット
$ nodebrew install stable
$ nodebrew use stable
# yarnのインストール
$ brew install yarn --ignore-dependencies
Yarnは「HomebrewのNode」に依存するので、Yarnをインストール時に「HomebrewのNode」も一緒にインストールしようとしますが、今回は「NodebrewのNode」使用しているので、--ignore-dependencies
オプションを使って「HomebrewのNode」をダウンロードさせないようにしています。
作業ディレクトリの作成
- 適当なディレクトリを作成して移動します。
$ mkdir ssr-sample && cd $_
$_
は直前に実行したコマンドの最後の引数(上のコマンドだとssr-sample
)を表します。
- Yarnを初期化(
package.json
の作成)しておきます。
$ yarn init -y
- 作成したディレクトリをVSCodeで開きましょう。『ファイル』 >『 開く』から開けます。
Git
プログラムのバージョンを管理するために、Gitも入れておきましょう。Gitはプログラムの更新履歴を管理したり、複数人で一つのプロダクトを更新するのに役立ちます。
-
GitをHomebrewからインストールします。
-
$ brew install git
-
Gitを初期化します。
$ git init
-
.gitignore
を作成します。$ touch .gitignore
-
.gitignore
に以下を書き込みます。/node_modules/
.gitignore
に書いたファイルはGitで管理されません。node_modules
ディレクトリ内のファイルはYarnでインストールしたパッケージなので容量が多い上に、package.json
があれば$ yarn install
コマンドで復元できるのでGit管理はしないように設定しましょう。この記事ではローカルリポジトリへのコミットしかしないので、詳しく知りたい人はGitをまとめた別記事を参照してください。
ESLint
ESListは、コードがちゃんとしたフォーマットになっているかチェックして、修正もしてくれるツールです。JavaScriptはセミコロン『 ; 』をつけるかどうかなど、コーディングの自由度が高いので、チームで開発する場合など特に活躍します。
設定は好みがあるのでいろいろな種類がありますが、Airbnbという企業の作った一番人気のコーディング規則を使ってみましょう。(細かくカスタマイズもできます)
① ESLintと関連パッケージのインストール
# airbnbのeslint設定に必要なパッケージを一括でインストールします。
# 『It seems as if you are using Yarn. Would you like to use Yarn for the installation? (y/n)』と聞かれたら、『y』と入力してEnterを押してください。
$ npx install-peerdeps --dev eslint-config-airbnb
npm(Yarnと同じパッケージマネージャで、Nodeと一緒にインストールされる)の
npx
コマンドを使ってinstall-peerdeps
コマンドを実行し、eslint-config-airbnb
が必要としているパッケージ(eslint
など)を一括でインストールしています。
② ESLintの設定ファイルを作成
.eslintrc
を作成して、
$ touch .eslintrc
以下を書き込んでください。
{
"extends": [
"airbnb",
"airbnb/hooks"
],
"env": {
"browser": true
},
}
extends
は指定した設定を継承します。airbnb
とReact Hooksを使用するためのairbnb/hooks
を継承しています。
env
でbrowser
を指定することで、window.document
などブラウザでのDOM操作をするためのコードでエラーが出ないようにしています。
③ VSCodeのESLint拡張のインストール
ESLintをYarnで入れただけでは、VSCode上で構文チェックできません。
左側にある のマークをクリックして、『eslint』を検索し、ESLintをインストールしてください。
④ VSCode-ESLintの設定
VSCode上でのESLintを設定します。
- 以下のコマンドを実行して
.vscode/settings.json
を作成します。
$ mkdir .vscode && touch $_/settings.json
- 以下の内容を書き込みます。(コピペ推奨)
{
"eslint.run": "onSave",
"eslint.autoFixOnSave": true,
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "typescript",
"autoFix": true
},
{
"language": "typescriptreact",
"autoFix": true
}
]
}
設定の詳細
- eslint.run :
onSave
を指定すると、保存時に構文チェックを行います。- eslint.autoFixOnSave :
true
にすると、保存時に構文の修正を行います。- eslint.validate : ESLintを有効にするファイルの種類を指定します。それぞれ、
.js
(JavaScript)、.jsx
(JavaScript + React)、.ts
(TypeScript)、.tsx
(TypeScript + React)を指定しています。.ts
と.tsx
については、autoFix
を指定しないと修正されません。
ESLintの設定が反映されずエラーが出たままのときは、VSCodeを開きなおすと直ります。
ファイル構成
.
├── .git ← Gitが作った。Git管理のための隠しファイル。
├── node_modules ← Yarnが作った。Yarnでインストールしたパッケージが入ってる
├── .eslintrc ← 新しく作った。eslintの設定ファイル。
├── .gitignore ← 新しく作った。Gitで管理しないファイルを指定する。
├── .vscode ← 新しく作った。vscodeの設定ファイルを入れる。
│ └── settings.json ← 新しく作った。vscode-eslintの設定をした。
├── package-lock.json ← Yarnが作った。パッケージの依存関係が書いてある。編集とかはしない。
├── package.json ← Yarnが作った。インストールしたパッケージとかが書いてある。
└── yarn.lock ← Yarnが作った。Yarnの依存関係を管理する。
STEP1 - ExpressでWebサーバを立てる
Webアプリを配信するためにはWebサーバが必要です。Webサーバとは、HTMLやJavaScript、画像などをクライアント(ブラウザ)に渡すものです。SSRでは、NodeでJavaScriptをHTMLに変換する必要があるので、NodeのフレームワークであるExpressを使いましょう。
1. 必要なパッケージのインストール
$ yarn add express esm
インストールしたパッケージの概要は以下の通りです。
- Express.js : JavaScriptのNode.jsのWebアプリケーションフレームワークです。
-
esm :
node -r esm index.js
の様に指定すると、Nodeでimport
/export
の構文を利用できるようになります。
###2. index.jsの作成
-
index.js
を作成します。
$ touch index.js
-
index.js
に以下を書き込みます。
import express from 'express';
import ssr from './src/ssr';
const app = express();
// 3000番ポートでWebサーバを立てる
app.listen(3000);
// https://localhost:3000 にアクセスがあったら ssr() を返す
app.get('/', (_, res) => {
res.send(ssr());
});
index.js
はExpressのルーティング(どのURLが来たら、どのファイルを返すか)などを設定するファイルです。今のコードでは、http://localhost:3000/でアクセスしたら、`./src/ssr`の`ssr()`を返す設定をしました。
###3. ssr.jsの作成
-
src/ssr.js
を作成します。
$ mkdir src && touch $_/ssr.js
-
src/ssr.js
に以下を書き込みます。
const ssr = () => (`
<html>
<head>
<title>CountUp</title>
<meta charset="utf-8"/>
</head>
<body>
<div id="app">
<h1>0</h1>
<button type="button">+</button>
<p>${new Date().toTimeString()}</p>
</div>
</body>
</html>
`);
export default ssr;
src/ssr.js
は、3.で設定した通り、実際に返される文字列になります。
new Date().toTimeString()
のところは、現在の時刻です。
4. package.jsonの編集
Expressを起動してWebサーバを立てるために、Nodeをindex.js
に対して実行するコマンドを設定します。
-
package.json
に以下を追加します。
"scripts": {
"start": "node -r esm index.js"
},
追加した後のpackage.json
はこのような感じになります。
{
"name": "ssr-sample",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "node -r esm index.js"
},
"devDependencies": {
"eslint": "^6.1.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.16.0",
"eslint-plugin-react-hooks": "^1.7.0"
},
"dependencies": {
"esm": "^3.2.25",
"express": "^4.17.1"
}
}
package.json
のscript:{}
に追加したコマンドは、yarn start
のように実行することができます。
###5. Expressの起動
- Expressを起動しましょう!
$ yarn start
ページをリロードすると時間表示が更新されます。ページのリクエストごとにJavaScriptからHTMLを生成していることが確認できます。
まだハリボテのHTMLを生成しているだけなので、+
ボタンを押しても何もおきません。
6. Gitにコミットする
- 忘れないようにgitにコミットしておきます。
$ git add .
$ git commit -m "STEP1 - ExpressでWebサーバを立てる"
###ファイル構成
.
├── .git
├── node_modules
├── .eslintrc
├── .gitignore
├── .vscode
│ └── settings.json
├── index.js ← 新しく作った。Expressの設定。
├── package-lock.json
├── package.json ← 編集した。startコマンドを追加。
├── src ← 新しく作った。ソースファイルを入れる。
│ └── ssr.js ← 新しく作った。ブラウザに送る文字列を返す。
└── yarn.lock
STEP2 - Reactを使う
STEP1では実装しなかったカウントアップの仕組みをReactを使って実装してみましょう。
ReactはそのままではNodeやブラウザで実行できないので、Nodeやブラウザで実行できるJavaScriptに変換するために、Babelを使用する必要があります。
1. 必要なパッケージのインストール
$ yarn add react react-dom
$ yarn add -D @babel/cli @babel/core @babel/preset-env core-js@3 @babel/preset-react
インストールしたパッケージの概要は以下の通りです。
-
React : JavaScriptのUIフレームワーク
-
react-dom :
render()
などを使うために必要です。
-
react-dom :
-
Babel : ES6の構文などをブラウザで表示できるように変換します。
- @babel/core : Babelのコア。
- @babel/cli : Babelをコマンドラインから使用できるようにします。
-
@babel/preset-env : 最新のJavaScriptを変換するための設定をまとめたプリセット。
- core-js : babel/preset-envがPolyfillをするために必要です。
- @babel/preset-react : Reactを変換するためのプリセット。
Polyfill : 最近の機能をサポートしていない古いブラウザーで、その機能を使えるようにするためにコードを変換すること。
-D
オプションをつけるかどうかの違い
yarn add
に-D
オプションをつけると、package.json
のdevDependencies
に追加されます。dependencies
に追加してもdevDependencies
に追加してもパッケージとして公開しなければ違いはありませんが、ExpressやReactは、dependencies
の追加しないとESLintに怒られます。
2. .babelrcの作成
- Babelの設定ファイル
.babelrc
を作成します。
$ touch .babelrc
- 以下の内容を書き込みます。
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
],
[
"@babel/preset-react",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
3. index.jsの編集
-
index.js
の2行目の./src/ssr
を./views/ssr
に変更して、以下のように書き換えます。
import express from 'express';
import ssr from './views/ssr';
const app = express();
app.listen(3000);
app.get('/', (_, res) => {
const response = ssr();
res.send(response);
});
import先のパスを変更したのは、./ssr
ディレクトリ内のReactで書かれたファイルをBabelでコンパイルして、Nodeで実行できるファイルとして./views
に出力するからです。
###4. ssr.jsxの編集
-
src/ssr.js
の拡張子を.jsx
に変更しします。
$ mv src/ssr.js src/ssr.jsx
.jsx : ReactでJSX(HTMLライクな記法)を用いる場合は、拡張子を
.jsx
にします。
-
renderToString
を使用して、以下のように書き換えます。
import React from 'react';
import { renderToString } from 'react-dom/server';
import CountUp from './CountUp';
// React ElementをHTMLに変換
const ssr = () => (`
<html>
<head>
<title>CountUp</title>
<meta charset="utf-8"/>
</head>
<body>
<div id="app">
${renderToString(<CountUp />)}
</div>
</body>
</html>
`);
export default ssr;
renderToString()
を使用すると、Reactのコンポーネントを文字列に変換することができます。上のコードでは、<CountUp />
コンポーネントを文字列に変換して、ssr()
で返す文字列の中に埋め込んでいます。
###5. CountUp.tsxの作成
-
src/CountUp.jsx
を作成します。
$ touch src/CountUp.jsx
- 以下の内容を書き込みます。
import React, { useState } from 'react';
const CountUp = () => {
const [count, setCount] = useState(0);
return (
<>
<h1>{count}</h1>
<button type="button" onClick={() => setCount(count + 1)}>+</button>
<p>{new Date().toTimeString()}</p>
</>
);
};
export default CountUp;
useStateは、React Hooksの機能の一つです。上の例だと、count
をState(状態)、setCount()
をStateを更新する関数として宣言しています。stateの初期値はuseState()
の引数で決まるので0です。Stateは絶対に直接変更せず、count
を1増やす場合はsetCount(count + 1)
のようにします。
<></>
で囲んでいるのは、Reactコンポーネントの返り値は何かのタグで囲っていないとエラーが出るためです。
6. package.jsonの編集
- Babelを実行するためのコマンド(
babel
)をpackage.json
に書き込みます。
"scripts": {
"start": "node -r esm index.js",
"babel": "babel src -x '.js,.jsx' -d views"
},
-x
オプションで対象とするファイルの拡張子を指定し、-d
オプションで書き出し先のディレクトリを指定しています。
上のコマンドだと、『src
ディレクトリに含まれる.js
か.jsx
拡張子のファイルをコンパイルしてviews
ディレクトリに書き出す』という意味になります。
7. Babelの実行
- Babelを実行します。
$ yarn babel
すると、Babelによってsrcディレクトリのssr.jsx
とCountUp.jsx
がviewsディレクトリに.js
拡張子で書き出されるはずです。
babelが正しく実行されない場合
コメントにてyarn babel
が正常に実行されないとのご指摘がありました。
実行環境による違いと思われますが、その場合はpackage.json
を以下のようにすると正常に実行されるようです。
1.「-x .js,.jsx」(シングルクォートで囲わない)もしくは
2.「-x ',.js,.jsx,'」(シングルクォートで囲い、カンマでも囲う)
8. Expressの起動
- Expressを起動します。
$ yarn start
表示されるUIはSTEP1と同じです。
9. Gitにコミットする
忘れないようにgitにコミットしておきます。
$ git add .
$ git commit -m "STEP2 - Reactを使う"
###ファイル構成
.
├── .git
├── node_modules
├── .babelrc ← 新しく作った。Babelの設定ファイル。
├── .eslintrc
├── .gitignore
├── .vscode
│ └── settings.json
├── index.js ← 編集した。ssr.jsのパスを変更。
├── package-lock.json
├── package.json ← 編集した。babelコマンドを追加。
├── src
│ ├── CountUp.jsx ← 編集した。ReactでState管理してカウントする。
│ └── ssr.jsx ← 編集した。CountUpを文字列に変換する。
├── views ← Babelによって生成される。
│ ├── CountUp.js ← Babelによって生成される。
│ └── ssr.js ← Babelによって生成される。
└── yarn.lock
ポイントはssr.jsx
内で、renderToString()
関数を使ってJSXをHTMLとして解釈できる文字列に変換しているところです。
しかし、**+
を押してもまだ何も起こりません。**それもそのはず、サーバーでJavaScriptをHTMLに変換してクライアントに返しているだけで、クライアントであるブラウザではJavaScriptが動いていないからです。クライアントに返されるHTMLは、STEP1と同じです。
つまり、この記事の冒頭に載せたSSRの図は正確ではなく、修正すると以下のようになります。
STEP3 - Webpackを使う
Expressで配信する静的なHTMLとは別に、クライアントで動作させるためのJavaScriptファイルも生成し、クライアントに読み込ませる必要があります。Webpackを使えば、Babelでコンパイルするのと同時に、複数のJavaScriptファイルを1つにバンドルすることができます。
1. 必要なパッケージのインストール
$ yarn add -D webpack webpack-cli babel-loader
インストールしたパッケージの概要は以下の通りです。
-
Webpack : 複数のJavaScriptなどのファイルを1つのファイルにバンドルします。
- webpack-cli : Webpackをコマンドラインから使用できるようにします。
- babel-loader : WebpackでBabelを使用してJavaScriptをコンパイルできるようにします。
2. エントリーポイントの作成
-
src/client.jsx
を作成します。$ touch src/client.jsx
-
以下の内容を書き込みます。
import React from 'react'; import { hydrate } from 'react-dom'; import CountUp from './CountUp'; // idがappの部分をhydrateで描画する hydrate(<CountUp />, document.querySelector('#app'));
SSRでないReactアプリでは
render()
を使用してUIを描画しますが、SSRを使用してサーバーで描画されている場合はhydrate()
を使用します。サーバーで描画した部分を、ブラウザで再描画しないようにするためです。
3. webpack.config.jsの作成
Webpackの設定ファイルwebpack.config.js
を作成します。
$ touch webpack.config.js
以下の内容を書き込みます。
const path = require('path');
module.exports = {
resolve: {
// 対象にする拡張子の指定
extensions: ['.js', '.jsx'],
},
entry: {
// エントリーポイントの指定
client: './src/client.jsx',
},
output: {
// アウトプット先のディレクトリを指定(assets)
path: path.resolve(__dirname, 'assets'),
// アウトプットするファイルの名前を指定(名前は変更しない)
filename: '[name].js',
},
module: {
rules: [
{
// 拡張子が.jsか.jsxだった場合に適用するルール
test: /\.js(x?)$/,
// node_modulesディレクトリ(Yarnでインストールしたパッケージが入ってる)は除外
exclude: /node_modules/,
use: [
{
// babelの設定
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: 3,
},
],
[
'@babel/preset-react',
{
useBuiltIns: 'usage',
corejs: 3,
},
],
],
},
},
],
},
],
},
};
4. Webpackを実行するコマンドの追加
- Webpackを実行するためのコマンド(
build
)をpackage.json
に書き込みます。
"scripts": {
"start": "node -r esm index.js",
"babel": "babel src -x '.js,.jsx' -d views",
"build": "webpack --mode development",
},
--mode
オプションでproduction
かdevelopment
を指定しないと、ビルド時に警告が出ます。
5. Webpackの実行
-
Webpackを実行します。
$ yarn build
すると、Babelによってsrcディレクトリのファイルが、assetsディレクトリに
client.js
という1ファイルにまとめて書き出されます。client.js
と関係があるファイルだけがまとめられるので、ssr.js
はバンドルに含まれません。
6. client.jsのルーティング
Webpackでバンドルしたassets/client.js
をクライアントで読み込む必要があるので、Expressでassets
ディレクトリ内のファイルを返すように設定します。
-
index.js
にapp.use(express.static('assets'));
を追加して、以下のように編集します。
import express from 'express';
import ssr from './views/ssr';
const app = express();
app.listen(3000);
app.use(express.static('assets'));
app.get('/', (_, res) => {
const response = ssr();
res.send(response);
});
これで、http://localhost/client.jsにアクセスすることで、assets/client.js
を取得することができるようになりました。
7. ssr.jsxの編集
-
src/ssr.jsx
に<script src="./client.js"></script>
を追加します。
import React from 'react';
import { renderToString } from 'react-dom/server';
import CountUp from './CountUp';
const ssr = (): string => (`
<html>
<head>
<title>CountUp</title>
<meta charset="utf-8"/>
</head>
<body>
<div id="app">
${renderToString(<CountUp />)}
</div>
<script src="./client.js"></script>
</body>
</html>
`);
export default ssr;
これで、ブラウザでWebpackでバンドルしたclient.js
が読み込まれることになります。
8. Babelの実行
"6." でssr.js
のを編集したので、Babelを実行して、サーバーで生成されるHTMLに<script src="./client.js"></script>
が追加されるようにします。
- Babelを実行します。
$ yarn babel
9. Expressの起動
- Expressを起動します。
$ yarn start
- http://localhost:3000/を開きます。
表示は同じですが、+
を押すことでカウントが増え、時刻表示も更新されるのが分かります。
10. Gitにコミットする
忘れないようにgitにコミットしておきます。
$ git add .
$ git commit -m "STEP3 - Webpackを使う"
ファイル構成
.
├── .git
├── node_modules
├── .babelrc
├── .eslintrc
├── .gitignore
├── .vscode
│ └── settings.json
├── assets ← Webpackによって生成される。
│ └── client.js ← Webpackによって生成される。srcをバンドルしたもの。
├── index.js ← 編集した。client.jsを配信するように設定。
├── package-lock.json
├── package.json ← 編集した。buildコマンドを追加。
├── src
│ ├── CountUp.jsx
│ ├── client.jsx ← 新しく作った。ブラウザで読み込む用のJavaScript。
│ └── ssr.jsx ← 編集した。client.jsをブラウザで読み込む。
├── views
│ ├── CountUp.js
│ ├── client.js ← Babelによって生成される。が、サーバーでは必要ないので使われない。
│ └── ssr.js
├── webpack.config.js ← 新しく作った。Webpackの設定ファイル。
└── yarn.lock
STEP4 - TypeScriptを使う
JavaScriptは動的型付けを行います。例えば、数値と文字を足し算するとこうなります。
const a = 1;
const b = '2';
const sum = a + b; // 12
これくらい簡単なプログラムだと素晴らしいことなんですが、プロジェクトが大きくなるとバグが起きやすくなります。一方、TypeScriptを使用して静的型付けを行うと、実行前にエラーを出してくれます。
const a = 1;
const b = '2';
const sum: number = a + b; // Error
sumの型をnumber
(数値)として指定しているので、数値と文字列の足し算に対してエラーを出してくれています。
1. 必要なパッケージのインストール
$ yarn add -D typescript @babel/preset-typescript
インストールしたパッケージの概要は以下の通りです。
-
Typescript : JavaScriptで静的型付けをできるようにした言語です。
- @babel/preset-typescript : TypeScriptをBabelでコンパイルできるようにするプリセットです。
2. ESLintの設定
- TypeScriptを扱うためのパーサをインストールします。
$ yarn add -D @typescript-eslint/parser
-
.eslintrc
を以下のように編集します。
{
"parser": "@typescript-eslint/parser",
"extends": [
"airbnb",
"airbnb/hooks"
],
"plugins": [
"@typescript-eslint"
],
"settings": {
"import/extensions": [
".js",
".jsx",
".ts",
".tsx"
],
"import/resolver": {
"node": {
"extensions": [
".js",
".jsx",
".ts",
".tsx"
]
}
}
},
"rules": {
"react/jsx-filename-extension": [
"error",
{
"extensions": [
".js",
".jsx",
".ts",
".tsx"
]
}
],
"import/extensions": [
"error", "always",
{
"js": "never",
"jsx": "never",
"ts": "never",
"tsx": "never"
}
]
}
}
TypeScriptをパース(構文解析)するために、parser
を指定しています。継承しているairbnb
には@typescript-eslint
が含まれていないので、TypeScriptのプラグインも追加しました。
.tsx
ファイルでもJSXを扱えるようにするためにreact/jsx-filename-extension
ルールを指定しています。
参考 : @typescript-eslintでtypescriptのlintをeslintで行いつつ、airbnbの設定でいきましょう的なお話
ESLint関係の設定は難しいので、動いてるからよし!ぐらいの心持ちで行きましょう。
3. 定義ファイルのインストール
Reactの型定義ファイルもインストールしておきます。
$ yarn add -D @types/react @types/react-dom
型定義ファイル : TypeScriptで型を扱うために、Reactなどの外部モジュールの型の定義が必要です。型定義ファイルは自作することもできます。
4. .babelrcの編集
-
.babelrc
に@babel/preset-typescript
を追加して、以下のように編集します。
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
],
[
"@babel/preset-react",
{
"useBuiltIns": "usage",
"corejs": 3
}
],
"@babel/preset-typescript"
]
}
5. webpack.config.jsの編集
-
webpack.config.js
を.ts
、tsx
の拡張子を処理するように書き換えます。 -
また、Babelでコンパイルできるように、4.で行なった
.babelrc
と同じく@babel/preset-typescript
を追加します。
const path = require('path');
module.exports = {
resolve: {
// 対象にする拡張子の指定(パッケージも含まれるので、.jsは必須)
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
entry: {
// エントリーポイントの指定
client: './src/client.tsx',
},
output: {
// 書き出し先のディレクトリを指定(assets)
path: path.resolve(__dirname, 'assets'),
// 書き出すファイルの名前を指定(名前は変更しない)
filename: '[name].js',
},
module: {
rules: [
{
// 拡張子が.jsか.jsxだった場合に適用するルール
test: /\.ts(x?)$/,
// node_modulesディレクトリは除外
exclude: /node_modules/,
use: [
{
// babelの設定
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: 3,
},
],
[
'@babel/preset-react',
{
useBuiltIns: 'usage',
corejs: 3,
},
],
'@babel/preset-typescript',
],
},
},
],
},
],
},
};
###6. ssr.jsxの編集
-
src/ssr.jsx
の拡張子を.tsx
に変更します。
$ mv src/ssr.jsx src/ssr.tsx
- src/ssr.tsx`を以下の様に書き換えます。
: string
という返り値の型を追加しました。
import React from 'react';
import { renderToString } from 'react-dom/server';
import CountUp from './CountUp';
// 返り値の型を指定しました。
const ssr = (): string => (`
<html>
<head>
<title>CountUp</title>
<meta charset="utf-8"/>
</head>
<body>
<div id="app">
${renderToString(<CountUp />)}
</div>
<script src="./client.js"></script>
</body>
</html>
`);
export default ssr;
###7. CountUp.jsxの編集
-
src/CountUp.jsx
の拡張子を.tsx
に変更します。
$ mv src/CountUp.jsx src/CountUp.tsx
-
src/CountUp.tsx
に以下の内容を書き込みます。: JSX.Element
という返り値の型を追加しました。
import React, { useState } from 'react';
const CountUp = (): JSX.Element => { // 返り値の型を指定しました。
const [count, setCount] = useState(0);
return (
<>
<h1>{count}</h1>
<button type="button" onClick={() => setCount(count + 1)}>+</button>
<p>{new Date().toTimeString()}</p>
</>
);
};
export default CountUp;
8. client.jsxの編集
-
src/client.jsx
の拡張子を.tsx
に変更します。
mv src/client.jsx src/client.tsx
9. package.jsonの編集
- Babelの対象とする拡張子を、
.js,.jsx
から.ts,.tsx
に編集します。
"scripts": {
"start": "node -r esm index.js",
"build": "babel src -x '.ts,.tsx' -d views"
},
10. Babelの実行
- Babelを実行します。
$ yarn babel
Babelによって、srcディレクトリのgreeting.tsx
とssr.tsx
がviewsディレクトリに.js
拡張子で書き出されます。
11. Webpackの実行
- Webpackを実行します。
$ yarn build
Webpackによって、srcディレクトリのファイルがclient.tsx
をエントリーポイントとしてバンドルされ、viewsディレクトリにclient.js
という1ファイルにまとめて書き出されます。
12. Expressの起動
yarn start
を実行してhttp://localhost:3000/を開きます。
$ yarn start
見た目は変わっていませんが、TypeScriptを使用することで強固なプログラムになりました。
13. Gitにコミットする
忘れないようにgitにコミットしておきます。
$ git add .
$ git commit -m "STEP4 - TypeScriptを使う"
###ファイル構成
.
├── .git
├── node_modules
├── .babelrc ← 編集した。TypeScriptをコンパイルする。
├── .eslintrc ← 編集した。TypeScriptを構文チェックする。
├── .gitignore
├── .vscode
│ └── settings.json
├── assets
│ └── client.js
├── index.js
├── package-lock.json
├── package.json ← 編集した。buildコマンドをTypeScriptに対応。
├── src
│ ├── CountUp.tsx ← 編集した。拡張子を変更と型の追加。
│ ├── client.tsx ← 編集した。拡張子を変更。
│ └── ssr.tsx ← 編集した。拡張子を変更と型の追加。
├── views
│ ├── CountUp.js
│ ├── client.js
│ └── ssr.js
├── webpack.config.js ← 編集した。TypeScriptをバンドルする。
└── yarn.lock
お疲れ様でした!
以上がモダンなTypeScript & ReactでのSSR/SPA開発です。
しっかりと開発環境を作ったので、複雑なプロジェクトにも応用できると思います。
参考
React Server-Side Rendering Example : めちゃくちゃ参考にしました。SSRありとなしをエンドポイントで分けているので、違いを体感できて面白いです。
最後に
分からないところ、ご指摘等あれば、お気軽にコメントやTwitterまでご連絡ください。
本当はContextを使うところまでやりたかったですが、分かりづらくなりそうだったのでやめました。