以前書いた記事は間違いでした
公式ドキュメントに沿って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
が生成されます。
{
"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.json
をtest-dir/
直下に作成します。
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react"
},
"include": [
"./src/**/*"
]
}
Reactコンポーネントの作成
Sample
コンポーネントを書いてみます。
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
も作ります。
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
コンポーネントも作成してみます。
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
を作成します。
import * as React from "react";
import * as ReactDOM from "react-dom";
import Counter from "./Counter";
ReactDOM.hydrate(
<Counter />,
document.getElementById("counter")
);
レンダリングされたReactコンポーネントを流し込むHTMLを作成
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-dom
のserver.renderToString()
を利用してReactファイルをレンダリングするサンプルです。
レンダリングしたReactファイルをHTMLファイルに渡し、expressがそれを返却します。
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のルーティングファイルを作成します。
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
です。
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に追加しています。)
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
に追加しましょう。
{
...
"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を呼ぶだけにしたいなぁ