LoginSignup
56
48

More than 3 years have passed since last update.

SSR対応SPAアプリの作り方(React/TypeScript/Express)

Last updated at Posted at 2019-11-03

Next.jsを使わないReactのSSR解説で分かりやすいやつが無かったので、書きます!

環境構築からとっても丁寧にやっていきます:cactus:

##最初に

読んで欲しい人

  • SSRしたいけど難しそう....:fearful:

    って人はもちろん

  • 『何も分からないけどSPAが作ってみたい!:smiley:

    って人も実際に作って楽しめるように書いてます。

SPA (Single Page Application) : ネイティブアプリ(AppStoreやGooglePlayからインストールできるやつ)っぽいWebサイトのこと。ReactやVueなどのJavaScriptフレームワークを使って作るのが一般的です。

作るもの

SSRを使用した簡単なカウントアップアプリを作ってみます。

countup.gif

最終的な制作物のソースコードはこちらです。

学べること

  • 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と呼ぶので難しく感じるだけです。

not-ssr.jpg

この記事では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で開発するなら必須です。

  1. AppStoreからXcodeをインストールします。

  2. 以下のコマンドを実行して、コマンドライン・デベロッパツールをインストールします。

    $ xcode-select --install
    
  3. 以下のコマンドを実行して、Homebrewをインストールします。

    $ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    
  4. 以下のコマンドを実行して、Example usage: ...が表示されればOKです。

    $ brew
    

参考 : macOSにHomebrewをインストール

VSCode

たぶん現時点では最も優れたコードエディタであるVSCodeをインストールします。

ダウンロードボタン
  • ダウンロードしたzipファイルを解凍してインストーラを実行し、インストールを完了してください。

###Node & Yarn

Nodeは、サーバーサイドで使えるJavaScript環境です。クライアントサイドの開発環境(Webpack等)にも必須なのでよく誤解がありますが、一応サーバーで動くものです。

Yarnは、Nodeのパッケージマネージャです。同じNodeのパッケージマネージャに有名なnpmがありますが、Yarnはnpmよりインストールの速度が速く、npmの上位互換と考えていいと思います。

NodeはNodebrewでバージョン管理すると便利なので、ここではNodebrewを使用してNodeをインストールします。

homebrew.jpg

  • 以下のコードを実行してください。

$(コマンドプロンプト)が書かれているコードは、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」をダウンロードさせないようにしています。

参考 : install nodebrew, node and yarn

作業ディレクトリの作成

  • 適当なディレクトリを作成して移動します。
$ 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など)を一括でインストールしています。

参考: Github: Airbnb/JavaScript

ESLintの設定ファイルを作成

.eslintrcを作成して、

$ touch .eslintrc

以下を書き込んでください。

{
  "extends": [
      "airbnb",
      "airbnb/hooks"
  ],
  "env": {
    "browser": true
  },
}

extendsは指定した設定を継承します。airbnbとReact Hooksを使用するためのairbnb/hooksを継承しています。

envbrowserを指定することで、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を指定しないと修正されません。

参考 : microsoft/vscode-eslint

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.jsonscript:{}に追加したコマンドは、yarn startのように実行することができます。

###5. Expressの起動

  • Expressを起動しましょう!
$ yarn start

ページをリロードすると時間表示が更新されます。ページのリクエストごとにJavaScriptからHTMLを生成していることが確認できます。

まだハリボテのHTMLを生成しているだけなので、+ ボタンを押しても何もおきません。

STEP1の画像

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()などを使うために必要です。
  • 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.jsondevDependenciesに追加されます。dependenciesに追加してもdevDependenciesに追加してもパッケージとして公開しなければ違いはありませんが、ExpressやReactは、dependenciesの追加しないとESLintに怒られます。

参考 : 【いまさらですが】package.jsonのdependenciesとdevDependencies

2. .babelrcの作成

  • Babelの設定ファイル.babelrcを作成します。
$ touch .babelrc
  • 以下の内容を書き込みます。
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ],
    [
      "@babel/preset-react",
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

参考 : Babel7.4で非推奨になったbabel/polyfillの代替手段と設定方法

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.jsxCountUp.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の図は正確ではなく、修正すると以下のようになります。

SSRの図2

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オプションでproductiondevelopmentを指定しないと、ビルド時に警告が出ます。

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.jsapp.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

表示は同じですが、+を押すことでカウントが増え、時刻表示も更新されるのが分かります。

countup.gif

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.tstsxの拡張子を処理するように書き換えます。

  • また、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.tsxssr.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

お疲れ様でした!:tada::tada::tada:

以上がモダンなTypeScript & ReactでのSSR/SPA開発です。

しっかりと開発環境を作ったので、複雑なプロジェクトにも応用できると思います。

参考

React Server-Side Rendering Example : めちゃくちゃ参考にしました。SSRありとなしをエンドポイントで分けているので、違いを体感できて面白いです。

最後に

分からないところ、ご指摘等あれば、お気軽にコメントやTwitterまでご連絡ください。

本当はContextを使うところまでやりたかったですが、分かりづらくなりそうだったのでやめました。

56
48
6

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
56
48