Help us understand the problem. What is going on with this article?

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

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