Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
15
Help us understand the problem. What is going on with this article?
@adibozu

Typescript + Express + React + Webpack でSSR(サーバサイドレンダリング)を検証

More than 1 year has passed since last update.

以前書いた記事は間違いでした

公式ドキュメントに沿ってTypescript + Express + React + Webpackを検証 - Qiita
公式ドキュメントはSSRしていませんでした…orz
嘘書いててすみませんでした、修正しました。ページのソースを表示でHTML吐き出されているかちゃんと確認しないといけませんでした。

そもそもReactでSSRって?

サーバサイドレンダリングとは、JSによるHTMLレンダリング処理を、サーバサイドで行うことです。
テンプレートエンジン(JSではないもの)で、サーバサイドで動的にHTMLを生成することも、サーバサイドレンダリングと呼ぶようです。

react-domというライブラリを使うことで、ReactでレンダリングしたHTMLを文字列として出力することができるため、

サーバサイドでReactを使ってHTMLレンダリング → クライアントに返す

というReactのサーバサイドでの利用が可能になります。

今度こそSSRする

下記のドキュメントを参考にしました。
Server-Side Rendering with React and TypeScript – Atticus Engineering – Medium

ちなみにローカルで作業を行いましたので、以前書いた内容を取り込みつつ、その前提で進めさせていただきます。

はじめのディレクトリ構成

最初に作業ディレクトリとしてtest-dirを用意し、その下に下記のようにディレクトリを作成しました。

test-dir/
├── dist/
└── src/
    ├── controllers/
    └── views/
        ├── components/
        │   └── sample/
        └── pages/
            └── sample/

イニシャライズ

test-dirディレクトリ直下で、npmパッケージとするためのコマンドを実行します。

npm init

すると、いろいろ質問されますが、ひとまずエンターをただただ押していって、、

Is this OK? (yes)

と聞かれたらyと打ってエンターキーを押すとpackage.jsonが生成されます。

package.json
{
  "name": "test-dir",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Node.jsの他に必要なパッケージのインストール

今回の検証に必要なパッケージをインストールします。

Webpack

npm install --save-dev webpack webpack-cli

TypeScript

npm install --save-dev typescript awesome-typescript-loader source-map-loader

Express

npm install --save express @types/express

React

npm install --save react react-dom @types/react @types/react-dom

tsconfig.jsonを作成

TypeScriptの設定ファイルであるtsconfig.jsontest-dir/直下に作成します。

tsconfig.json
{
    "compilerOptions": {
        "outDir": "./dist/",
        "sourceMap": true,
        "noImplicitAny": true,
        "module": "commonjs",
        "target": "es5",
        "jsx": "react"
    },
    "include": [
        "./src/**/*"
    ]
}

Reactコンポーネントの作成

Sampleコンポーネントを書いてみます。

src/views/components/sample/Sample.tsx
import * as React from "react";

export interface SampleProps { compiler: string; framework: string; library: string; }

export class Sample extends React.Component<SampleProps, {}> {
    render() {
        return <h1>Hello from {this.props.compiler} and {this.props.framework} and {this.props.library}!</h1>;
    }
}

export default Sample;

Sampleコンポーネントをインポートするindex.tsxも作ります。

src/views/components/sample/index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";

import { Sample } from "./Sample";

ReactDOM.hydrate(
    <Sample compiler="TypeScript" framework="Express" library="React" />,
    document.getElementById("sample")
);

また、参考にした資料にもあるCounterコンポーネントも作成してみます。

src/views/components/counter/Counter.tsx
import * as React from 'react';

export interface CounterProps {
}

export interface CounterState {
    counter: number;
  }

  class Counter extends React.Component<CounterProps, CounterState> {

    constructor(props: any) {
      super(props);
      this.state = { counter: 0 };
    }

    incrementCounter() {
      this.setState({ counter: this.state.counter + 1 });
    }

    render() {
      return (
        <div>
          <h1>counter at: {this.state.counter}</h1>
          <button
            onClick={() => this.incrementCounter()}
          />
        </div>
      );
    }
  }

  export default Counter;

これも同様にindex.tsxを作成します。

src/views/components/counter/index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";

import Counter from "./Counter";

ReactDOM.hydrate(
    <Counter />,
    document.getElementById("counter")
);

レンダリングされたReactコンポーネントを流し込むHTMLを作成

src/views/pages/sample/html.ts
const html = ({ counter, sample }: { counter: string, sample: string }) => `
  <!DOCTYPE html>
  <html>
    <head>
    </head>
    <body style="margin:0">
      <div id="counter">${counter}</div>
      <div id="sample">${sample}</div>
    </body>
    <script src="js/counter.js" defer></script>
  </html>
`;

export default html;

サーバサイドの処理を追加

react-domserver.renderToString()を利用してReactファイルをレンダリングするサンプルです。
レンダリングしたReactファイルをHTMLファイルに渡し、expressがそれを返却します。

src/controllers/sampleController.ts
import * as express from 'express';
import * as React from 'react';
import { renderToString } from 'react-dom/server';

import html from '../views/pages/sample/html';
import Counter from '../views/components/Counter/Counter';
import Sample from '../views/components/Sample/Sample';

export default (req: express.Request, res: express.Response)  => {
    const counter = renderToString(React.createElement(Counter));
    const sample = renderToString(React.createElement(Sample));

    res.send(
      html({
        counter, sample
      })
    );
}

/にリクエストが来たらsampleControllerの処理が走るように、Expressのルーティングファイルを作成します。

src/server.ts
import * as Express from 'express';
import sampleController from './controllers/sampleController';

const app = Express();

app.get('/', sampleController);

app.listen(process.env.PORT || 3000, function () {
  console.log('express app is started.');
});

Webpackの設定ファイルを作成

今回は
- サーバサイドで読み込むJSファイル
- クライアントサイドで読み込むJSファイル
をそれぞれ作りたいので、Webpackの設定ファイルをそれぞれ用意しました。
サーバサイドで読み込むファイルは、直上で作成したserver.tsです。

webpack.server.config.js
const path = require("path");

module.exports = {
  target: "node",
  entry: "./src/server.ts",
  output: {
    filename: "js/server.js",
    path: path.resolve(process.cwd(), "dist"),
    publicPath: "/"
  },
  devtool: "cheap-module-eval-source-map",
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".json"]
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [
          {
            loader: "ts-loader"
          }
        ]
      }
    ]
  }
};

クライアントサイドで読み込むJSファイルは、それぞれのコンポーネントをインポートしているindex.tsxファイルです。html.tsにある、

    <script src="js/counter.js" defer></script>

がwebpackでビルドされたJSファイルを読み込む部分です。
(sampleコンポーネントのjsファイルはクライアントサイドで読み込んでいませんが、足並みを揃える目的でentryに追加しています。)

webpack.client.config.js
const path = require("path");

module.exports = {
  entry: {
    counter: "./src/views/components/counter/index.tsx",
    sample: "./src/views/components/sample/index.tsx"
  },
  output: {
    filename: "js/[name].js",
    path: path.resolve(process.cwd(), "dist"),
    publicPath: "/"
  },
  devtool: "cheap-module-eval-source-map",
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".json"]
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [
          {
            loader: "ts-loader"
          }
        ]
      }
    ]
  }
};

package.jsonにコマンドを追加

webpackのコマンドと、アプリケーションを起動するコマンドをpackage.jsonに追加しましょう。

package.json
{
...
  "scripts": {
    "build": "webpack --config webpack.client.config.js && webpack --config webpack.server.config.js",
    "start": "node dist/js/server.js",
...
}

動作確認

package.jsonにコマンドを追加したので、下記コマンドでビルドと起動を行います。

$ npm run build
$ npm run start

localhost:3000にアクセスするとサーバサイドレンダリングされたReactコンポーネントを含むHTMLが表示されます。

最終的なディレクトリ構成

下記のようになりました。

test-dir/
├── dist
│   └── js
│       ├── counter.js
│       ├── sample.js
│       └── service.js
├── node_modules
│   ├── ...
├── package.json
├── src
│   ├── controllers
│   │   └── sampleController.ts
│   ├── server.ts
│   └── views
│       ├── components
│       │   ├── counter
│       │   │   ├── Counter.tsx
│       │   │   └── index.tsx
│       │   └── sample
│       │       ├── Sample.tsx
│       │       └── index.tsx
│       └── pages
│           └── sample
│               └── html.ts
├── tsconfig.json
├── webpack.client.config.js
└── webpack.server.config.js

`src/services/`を作成して、そこにもっとビジネスロジックを寄せて、controllerはrouterからimportされてserviceを呼ぶだけにしたいなぁ

参考文献

15
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
adibozu

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
15
Help us understand the problem. What is going on with this article?