Express+TypeScript で Server Side Rendering (SSR) して、React をテンプレートエンジンとして使用してみます。Babel は使用しません。
TypeScript は JSX に対応しているので、JSX を変換するだけなら Babel は不要です。
express-react-views をかなり参考にしています。
ディレクトリ構成
.
├── app.ts
├── createEngine.ts
├── package.json
├── pages/
│ └── Hello.tsx
└── tsconfig.json
設定ファイル
{
"name": "react-ssr-ts",
"version": "1.0.0",
"license": "CC0-1.0",
"private": true,
"scripts": {
"dev": "ts-node-dev --no-notify app.ts",
"build": "tsc",
"start": "node dist/app.js"
},
"dependencies": {
"express": "^4.17.1",
"react": "^16.11.0",
"react-dom": "^16.11.0"
},
"devDependencies": {
"@types/express": "^4.17.1",
"@types/react": "^16.9.9",
"@types/react-dom": "^16.9.2",
"ts-node-dev": "^1.0.0-pre.43",
"typescript": "^3.6.4"
}
}
ts-node-dev はファイルに変更があると自動で再起動してくれるツールです。ts-node と node-dev を掛け合わせたようなやつです。
{
"compilerOptions": {
"target": "es2019",
"module": "commonjs",
"jsx": "react",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
}
}
"jsx": "react"
がミソです。tsc が JSX を React.createElement
に置き換えてくれるので、普通に JS として実行可能になります。
ページ
import React from 'react';
type HelloProps = {
name: string;
};
const Hello: React.FC<HelloProps> = ({ name }) => (
<h1>Hello, {name}</h1>
);
export default Hello;
ページは pages/
に置いていますが、app.ts の設定でディレクトリ名を自由に指定できます。
ページのコンポーネントは export default
してください。
テンプレートエンジン
import React from 'react';
import ReactDOMServer from 'react-dom/server';
export type EngineOptions = {
doctype?: string;
};
export const createEngine = ({ doctype = '<!doctype html>' }: EngineOptions = {}) => {
return (path: string, options: object, callback: (e: any, rendered: string) => void): void => {
try {
const component = require(path).default as React.ComponentType<any>;
const markup = ReactDOMServer.renderToStaticMarkup(
React.createElement(component, options)
);
return callback(null, doctype + markup);
} catch (e) {
return callback(e, '');
}
};
};
tsx ファイルをテンプレートとして使うためのテンプレートエンジンです。渡された js または tsx ファイルを ReactDOMServer.renderToStaticMarkup
で HTML の文字列に変換しています。
エントリポイント
import express from 'express';
import { createEngine } from './createEngine';
const app = express();
const isTsNodeDev = Object.keys(require.cache).some(path => path.includes('/ts-node-dev/'));
const ext = isTsNodeDev ? 'tsx' : 'js';
app.set('views', __dirname + '/pages');
app.set('view engine', ext);
app.engine(ext, createEngine());
app.get('/:name', (req, res) => {
res.render('Hello', { name: req.params.name });
});
app.listen(8080, () => {
console.log('> Ready on http://localhost:8080/');
});
先ほどのテンプレートエンジンを app.engine
に登録します。ページのディレクトリ名を変えたい場合は、app.set('views', __dirname + '/pages')
のところを変えてください。
view engine
の値を js と tsx で分けている理由ですが、ts-node-dev で実行した場合 tsx が渡されてくる一方、tsc して node dist/app.js
した場合 js が渡されてくるので、それぞれの場合でテンプレートの拡張子を変える必要があります。このあたりもう少しスマートにできないものか・・・
追記: (2019-10-24)
@suin さんの『ts-node-devで起動されたかを調べる方法』を参考に、NODE_ENV で判定していたところを、ts-node-dev で起動されたかどうかで判定するよう書き換えました。ありがとうございます!
おわり
以上です。意外と簡単に React をテンプレートエンジンとして使うことができました。
普通は Next.js を使うのがいいと思います(Dynamic Routing で難がありますが)。