Node.jsの実装をTypeScriptでやった事が無かったので試しにやってみました。
express todo
で生成したテンプレートプロジェクトをそのままTypeScriptに実装し直しただけです。
環境構築
- 参考:docker-compose(Docker for mac)で実践的なnode.js開発環境を作る)
- Node.js:9.11.1
- TypeScript:2.8.1
一連の下記の作業をやるとこんな感じのディレクトリ構成になる
設定ファイルの準備
docker-compose.yml
version: '3'
services:
nginx:
image: nginx:alpine
container_name: nginx
ports:
- "80:80"
volumes:
- "./conf.d:/etc/nginx/conf.d"
links:
- node_express
node_express:
image: node:9.11.1-alpine
container_name: node_express
hostname: node_express
volumes:
- ".:/src"
working_dir: /src
command: >
"npm i typings -g
&& npm i -D typescript tslint ts-node nodemon shelljs @types/cookie-parser @types/morgan @types/shelljs --save
&& typings i debug express http-errors
&& npm i jade debug http-errors express cookie-parser morgan path --save
&& npm run-script build
&& npm start"
ports:
- "3000:3000"
conf.d/nodejs.conf
server {
listen 80;
server_name _;
client_max_body_size 10M;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://node_express:3000/;
}
}
テンプレートプロジェクト生成
express todo
warning: the default view engine will not be jade in future releases
warning: use `--view=jade' or `--help' for additional options
create : todo/
create : todo/public/
create : todo/public/javascripts/
create : todo/public/images/
create : todo/public/stylesheets/
create : todo/public/stylesheets/style.css
create : todo/routes/
create : todo/routes/index.js
create : todo/routes/users.js
create : todo/views/
create : todo/views/error.jade
create : todo/views/index.jade
create : todo/views/layout.jade
create : todo/app.js
create : todo/package.json
create : todo/bin/
create : todo/bin/www
change directory:
$ cd todo
install dependencies:
$ npm install
run the app:
$ DEBUG=todo:* npm start
テンプレートプロジェクトのイメージをビルドして起動
docker-compose -p todo up -d --build
docker-compose ps
Name Command State Ports
------------------------------------------------------------------------------
nginx nginx -g daemon off; Up 0.0.0.0:80->80/tcp
node_express sh -c npm install && npm start Up 0.0.0.0:3000->3000/tcp
ページにアクセス
NodeのJavaScript実装をTypeScript実装に置換
tsconfig.jsonを設定
targetは"es5"にしておかないと、Node.jsでは、ES6のimport構文などがエラーで動かないので注意。
importでも動くようにするには、ESM(ECMAScript Modules)対応が必要
参考:Node v9 でフラグ付きで ES Modules の import/export 構文を使用する
tsconfig.json
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist",
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es5",
"typeRoots": [
"node_modules/@types",
"./src/typings"
],
"lib": [
"es2017",
"dom"
]
},
"include": [
"bin/**/*.ts",
"src/**/*.ts"
]
}
package.jsonにタスクランナーを定義
- "start"で起動するスクリプトをbin/www→./dist/bin/www.jsに変更
- 後はビルド用タスクなどを追加
- viewsをdistへコピーするためのcopy-static-assetsは下で別途作成する
package.json
{
"name": "todo",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "nodemon ./dist/bin/www.js",
"debug": "nodemon --inspect ./dist/bin/www.js",
"build": "npm run build-ts && npm run copy-static-assets",
"build-ts": "tsc",
"tslint": "tslint -c tslint.json -p tsconfig.json",
"copy-static-assets": "ts-node copyStaticAssets.ts"
},
"dependencies": {
"cookie-parser": "^1.4.3",
"debug": "~2.6.9",
"express": "^4.16.3",
"http-errors": "^1.6.3",
"jade": "~1.11.0",
"morgan": "^1.9.0",
"path": "^0.12.7"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.1",
"@types/express": "^4.11.1",
"@types/http-errors": "^1.6.1",
"@types/morgan": "^1.7.35",
"shelljs": "^0.8.1",
"ts-node": "^5.0.1",
"tslint": "^5.9.1",
"typescript": "^2.8.1",
"typings": "^2.1.1"
}
}
viewsをdistへコピーするcopy-static-assetsを作成
copyStaticAssets.ts
import * as shell from 'shelljs';
shell.cp('-R', 'src/views', 'dist/src/views/');
型定義追加
src/typings.d.ts
declare module global {
type Error = {
message: string;
status: number;
}
}
起動スクリプト
- requireをimport文に変更
- functionの引数と戻り値の型宣言を追加
bin/www.ts
#!/usr/bin/env node
/**
* Module dependencies.
*/
import app from '../src/app';
import * as debugModule from 'debug';
import * as http from 'http';
const debug = debugModule('todo:server');
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val: string): number | string | boolean {
const nport = parseInt(val, 10);
if (isNaN(nport)) {
// named pipe
return val;
}
if (nport >= 0) {
// port number
return nport;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error): void {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening(): void {
const addr = server.address();
const bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
メイン処理
- requireをimport文に変更
- functionの引数と戻り値の型宣言を追加
- module.exportsをexport defaultに変更
src/app.js
import { Router, NextFunction, Request, Response } from 'express';
import * as createError from 'http-errors';
import * as express from 'express';
import * as path from 'path';
import * as cookieParser from 'cookie-parser';
import * as logger from 'morgan';
import indexRouter from './routes/index';
import usersRouter from './routes/users';
const app: express.Express = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
// catch 404 and forward to error handler
app.use((req: Request, res: Response, next: NextFunction) => {
next(createError(404));
});
// error handler
app.use((err: global.Error, req: Request, res: Response, next: NextFunction) => {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
export default app;
ルーティング処理
- requireをimport文に変更
- functionの引数と戻り値の型宣言を追加
- module.exportsをexport defaultに変更
- あまり関係ないがtry-catch入れておく
src/routes/index.ts
import { Router, NextFunction, Request, Response } from 'express';
const router: Router = Router();
/* GET home page. */
router.get('/', (req: Request, res: Response, next: NextFunction) => {
try {
res.render('index', { title: 'Express' });
} catch (err) {
next(err);
}
});
export default router;
src/routes/users.ts
import { Router, NextFunction, Request, Response } from 'express';
const router: Router = Router();
/* GET users listing. */
router.get('/', (req: Request, res: Response, next: NextFunction) => {
try {
res.send('aaarespond with a resource');
} catch (err) {
next(err);
}
});
export default router;
TypeScriptをビルド
npm run build
ページにアクセス
うむ。ちゃんと表示された。ミッション完了。
感想
しかし、es5にしないとimport文が動かないのか。
ESMとかいう、拡張子.mjsの形式だと、import文そのまま使えるらしい。
少しやってみたけど、それほど簡単には行かなさそうだ。
参考:Node v9 で ES Module import を使ってみる
次回予告
今回は単純にTypeScriptに置換しただけですが、もう少しリファクタリングしていこうかと思います。
ちゃんとクラス構文を使うことにします。
以前、これからWeb開発のバックエンドを学ぶ in 2018(PHP7編 - 1.0日目)|DDDへ向かって行く
で、Laravel5.6でのルーティング処理をコントローラークラスに移譲するようにしましたが、
routes内のコントローラー処理をアレと同じ感じにできるはずだ。