Edited at

express-generatorで生成したテンプレートプロジェクトをTypeScript実装に置換

More than 1 year has passed since last update.

Node.jsの実装をTypeScriptでやった事が無かったので試しにやってみました。

express todo

で生成したテンプレートプロジェクトをそのままTypeScriptに実装し直しただけです。


環境構築

一連の下記の作業をやるとこんな感じのディレクトリ構成になる

Screen Shot 2018-04-17 at 18.09.54.png


設定ファイルの準備


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


ページにアクセス

http://localhost:3000/

Screen Shot 2018-04-17 at 17.43.33.png


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


ページにアクセス

http://localhost:3000

うむ。ちゃんと表示された。ミッション完了。


感想

しかし、es5にしないとimport文が動かないのか。

ESMとかいう、拡張子.mjsの形式だと、import文そのまま使えるらしい。

少しやってみたけど、それほど簡単には行かなさそうだ。

参考:Node v9 で ES Module import を使ってみる


次回予告

今回は単純にTypeScriptに置換しただけですが、もう少しリファクタリングしていこうかと思います。

ちゃんとクラス構文を使うことにします。

以前、これからWeb開発のバックエンドを学ぶ in 2018(PHP7編 - 1.0日目)|DDDへ向かって行く

で、Laravel5.6でのルーティング処理をコントローラークラスに移譲するようにしましたが、

routes内のコントローラー処理をアレと同じ感じにできるはずだ。


参考(修正履歴)

https://github.com/masaaki-uegaki/todo/commit/b75ac3529898de12eaa8d420737c48894011c57c