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

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

More than 1 year has passed since last update.

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

リファクタリング

前回、TypeScriptで置換したソースをカスタマイズしやすいようにしたくて、リファクタリングしました。
以前はexpress-generatorで生成したスクリプトに対して型付けした程度でしたが、ちゃんとクラス作りました。
標準テンプレートより少し処理を載せてるけど。コントローラにCRUDで使う想定のメソッド足しといた。

環境

  • Node.js:9.11.1
  • TypeScript:2.8.1

修正内容

起動スクリプト

  • bin/www.ts としてTypeScript化したものの、わかりにくいので、極力そのまま使うイメージで、bin/www(JavaScript)に戻した。
  • use strictモード付与
  • app.tsの構成を変えたので、appの取得方法を変更
bin/www
#!/usr/bin/env node
"use strict";

//module dependencies
const application = require("../dist/app");
const debug = require("debug")("todo:server");
const http = require("http");

//create http server
const httpPort = normalizePort(process.env.PORT || 3000);
const app = application.App.bootstrap().app;
app.set("port", httpPort);
const httpServer = http.createServer(app);

//listen on provided ports
httpServer.listen(httpPort);

//add error handler
httpServer.on("error", onError);

//start listening on port
httpServer.on("listening", onListening);

/**
 * Normalize a port into a number, string, or false.
 */
function normalizePort(val) {
  const port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */
function onError(error) {
  if (error.syscall !== "listen") {
    throw error;
  }

  const bind = typeof httpPort === "string"
    ? "Pipe " + httpPort
    : "Port " + httpPort;

  // 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() {
  const addr = httpServer.address();
  const bind = typeof addr === "string"
    ? "pipe " + addr
    : "port " + addr.port;
  debug("Listening on " + bind);
}

メイン処理(サーバ定義)

  • やってる事は全く同じだが、クラス構造に変えた。
src/app.ts
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 { IndexController } from './controllers/index';
import { UserController } from './controllers/user';

/**
 * Application.
 *
 * @class App
 */
export class App {
  public app: express.Application;

  /**
   * Bootstrap the application.
   *
   * @static
   * @return {ng.auto.IInjectorService} Returns the newly created injector for this app.
   */
  public static bootstrap(): App {
    return new App();
  }

  /**
   * Constructor.
   *
   * @constructor
   */
  constructor() {
    this.app = express();
    this.setConfig();
    this.setRoutes();
    this.setApiRoutes();
    this.setErrorHandler();
  }

  /**
   * Configure application
   *
   */
  private setConfig(): void {
    this.app.set('views', path.join(__dirname, 'views'));
    this.app.set('view engine', 'jade');

    this.app.use(logger('dev'));

    this.app.use(express.json());
    this.app.use(express.urlencoded({ extended: false }));
    this.app.use(cookieParser());
    this.app.use(express.static(path.join(__dirname, 'public')));
  }

  /**
   * Create and return Router.
   *
   */
  private setRoutes(): void {
    // Custom routes
    this.app.use('/', new IndexController().create());
    this.app.use('/users', new UserController().create());
  }

  /**
   * Create REST API routes
   *
   */
  private setApiRoutes(): void {
  }

  /**
   * Create Error handler
   *
   */
  private setErrorHandler(): void {
    // Catch 404 and forward to error handler
    this.app.use((req: Request, res: Response, next: NextFunction) => {
      next(createError(404));
    });

    // Error handler
    this.app.use((err: app.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');
    });
  }
}

モデルのデータ定義追加

  • app.model.Userを追加(項目はfirebaseでの利用を想定)
src/typings.d.ts
import { Moment } from 'moment';

declare global {
  namespace app {
    type Error = {
      message: string;
      status: number;
    }

    namespace model {
      type User = {
        uid: string;
        email: string;
        displayName?: string;
        photoURL?: string;
        profile?: string;
      };
    }
  }

  namespace todo {
    namespace model {
      type Task = {
        id: string;
        name: string;
        desc?: string;
        priority?: string;
        startAt?: string | Moment;
        dueAt?: string | Moment;
        expectedManHour?: number;
        actualManHour?: number;
        expectedCost?: number;
        actualCost?: number;
        progress?: number;
        createdAt: string | Moment;
        createdUid: string;
        modifiedAt?: string | Moment;
        modifiedUid?: string;
      }
    }
  }
}

ルーティング処理

  • 基底コントローラクラス
  • ルーティング生成結果を返却するcreate()だけ抽象メソッドにした
src/controllers/base.ts
import { Router, NextFunction, Request, Response } from 'express';

/**
 * BaseController
 *
 * @class BaseController
 */
export abstract class BaseController {
  protected title: string;
  private scripts: string[];

  /**
   * Constructor
   *
   * @constructor
   */
  constructor() {
    this.title = 'TODO';
    this.scripts = [];
  }

  /**
   * Create routes.
   *
   */
  public abstract create(): Router;

  /**
   * Add a JS external file to the request.
   *
   * @param src {string} The src to the external JS file.
   */
  protected addScript(src: string): BaseController {
    this.scripts.push(src);
    return this;
  }

  /**
   * Render page.
   *
   * @param req {Request} Request object.
   * @param res {Response} Response object.
   * @param view {String} View to render.
   * @param options {Object} Additional options to append to the view's local scope.
   */
  protected render(req: Request, res: Response, view: string, options?: Object): void {
    res.locals.BASE_URL = '/';
    res.locals.scripts = this.scripts;
    res.locals.title = this.title;
    res.render(view, options);
  }
}
  • 初期表示コントローラクラス
  • create()をオーバーライド実装して、ルーティング設定を返す
src/controllers/index.ts
import { Router, Request, Response, NextFunction } from 'express';
import { BaseController } from './base';

/**
 * IndexController
 *
 * @class IndexController
 */
export class IndexController extends BaseController {

  /**
   * Create routes.
   *
   * @override
   */
  public create(): Router {
    const router = Router();
    this.index(router);
    return router;
  }

  /**
   * Constructor
   *
   * @constructor
   */
  constructor() {
    super();
  }

  /**
   * Show home.
   *
   * @param router {Router} Express Router object.
   */
  private index(router: Router): void {
    router.get('/', (req: Request, res: Response, next: NextFunction) => {
      try {
        this.title = `Home | TODO`;
        this.render(req, res, 'index');
        next();
      } catch (err) {
        next(err);
      }
    });
  }
}
  • ユーザーコントローラクラス
  • create()をオーバーライド実装して、ルーティング設定を返す
src/controllers/user.ts
import { Router, Request, Response, NextFunction } from 'express';
import { BaseController } from './base';

/**
 * / User controller
 *
 * @class UserController
 */
export class UserController extends BaseController {
  /**
   * Create routes.
   *
   * @override
   */
  public create(): Router {
    const router = Router();
    this.index(router);
    this.show(router);
    this.update(router);
    this.destroy(router);
    return router;
  }

  /**
   * Constructor
   *
   * @constructor
   */
  constructor() {
    super();
  }

  /**
   * Show all users.
   *
   * @param router {Router} Express Router object.
   */
  private index(router: Router): void {
    router.get('/', (req: Request, res: Response, next: NextFunction) => {
      try {
        res.send(`User | TODO`);
        next();
      } catch (err) {
        next(err);
      }
    });
  }

  /**
   * Show specified user.
   *
   * @param router {Router} Express Router object.
   */
  private show(router: Router): void {
    router.get('/:uid', (req: Request, res: Response, next: NextFunction) => {
      try {
        const user: app.model.User = req.params;
        res.send(`User:${user.uid} | TODO`);
        next();
      } catch (err) {
        next(err);
      }
    });
  }

  /**
   * Update specified user.
   *
   * @param router {Router} Express Router object.
   */
  private update(router: Router): void {
    router.put('/:uid', (req: Request, res: Response, next: NextFunction) => {
      try {
        const user: app.model.User = req.params;
        res.send(`[UPDATED] User:${user.uid} | TODO`);
        next();
      } catch (err) {
        next(err);
      }
    });
  }

  /**
   * Delete specified user.
   *
   * @param router {Router} Express Router object.
   */
  private destroy(router: Router): void {
    router.delete('/:uid', (req: Request, res: Response, next: NextFunction) => {
      try {
        const user: app.model.User = req.params;
        res.send(`[DELETED] User:${user.uid} | TODO`);
        next();
      } catch (err) {
        next(err);
      }
    });
  }
}

package.json

  • ES Module対応の --experimental-modulesでの呼びだしが必要なくなったので辞めた。
package.json
・・・・・・
  "scripts": {
    "start": "nodemon ./bin/www",
    "debug": "nodemon --inspect ./bin/www",
・・・・・・

tsconfig.json

  • "module": "commonjs"を指定する事で、トランスパイル時にimportがrequireに変わるようにした。
  • "target": "es5" → "es2017"にアップグレード
tsconfig.json
・・・・・・
"compilerOptions": {
    "module": "commonjs",
    "target": "es2017",
・・・・・・

npm:buildのタスクを実行してし、ページにアクセス。

  • npm:buildタスク:npm run build-ts && npm run copy-static-assets
  • 修正後の再起動はnodemonが自動検知して勝手にやってくれるので、ビルドのみでOK

http://localhost:3000

Screen Shot 2018-04-18 at 11.00.23.png

http://localhost:3000/users

Screen Shot 2018-04-18 at 11.03.45.png

http://localhost:3000/users/user0001

Screen Shot 2018-04-18 at 14.56.41.png

参考

トランスパイル後のソース

  • import文がrequireになってる。
  • 後はES2017に従って大体そのまま
dist/app.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const createError = require("http-errors");
const express = require("express");
const path = require("path");
const cookieParser = require("cookie-parser");
const logger = require("morgan");
const index_1 = require("./controllers/index");
const user_1 = require("./controllers/user");
/**
 * Application.
 *
 * @class App
 */
class App {
    /**
     * Bootstrap the application.
     *
     * @static
     * @return {ng.auto.IInjectorService} Returns the newly created injector for this app.
     */
    static bootstrap() {
        return new App();
    }
    /**
     * Constructor.
     *
     * @constructor
     */
    constructor() {
        this.app = express();
        this.setConfig();
        this.setRoutes();
        this.setApiRoutes();
        this.setErrorHandler();
    }
    /**
     * Configure application
     *
     */
    setConfig() {
        this.app.set('views', path.join(__dirname, 'views'));
        this.app.set('view engine', 'jade');
        this.app.use(logger('dev'));
        this.app.use(express.json());
        this.app.use(express.urlencoded({ extended: false }));
        this.app.use(cookieParser());
        this.app.use(express.static(path.join(__dirname, 'public')));
    }
    /**
     * Create and return Router.
     *
     */
    setRoutes() {
        // Custom routes
        this.app.use('/', new index_1.IndexController().create());
        this.app.use('/users', new user_1.UserController().create());
    }
    /**
     * Create REST API routes
     *
     */
    setApiRoutes() {
    }
    /**
     * Create Error handler
     *
     */
    setErrorHandler() {
        // Catch 404 and forward to error handler
        this.app.use((req, res, next) => {
            next(createError(404));
        });
        // Error handler
        this.app.use((err, req, res, next) => {
            // 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');
        });
    }
}
exports.App = App;

修正履歴

https://github.com/masaaki-uegaki/todo/commit/475f65fd5e2e96c9012959426f2d8455b2a5c0ce
https://github.com/masaaki-uegaki/todo/commit/6d639d2c934d52b53b3a294472f601fb8c7dedb5

プロジェクトのソース

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

感想

Visual Studio Codeで初めてTypeScriptのデバッグ実行した。むしろ今まで知らずに開発してたのがすごい。

次やりたい事

そろそろビューを付けたくなって来たが、Angular6で行くか、巷で話題(?)のVue.jsで行くか悩んでる。
なんで急激にVue.js推しが始まってるのか、わかってない。

やっぱりAngularの初期学習コストは他と比べてエゲツないなんだろうか。
かく言う自分もAngularの全てを自由自在に使いこなせるタマじゃあない。

どうやらNodeのv8.4以降、HTTP/2が使えるようになってるようだ。よくわからないが雰囲気で使ってみたい。
あるいは、WebSocketならば昔、Socket.ioを使った事があるのでそちらでやるか。

HTTP/2とWebSocketは何が違うんだろう?あるいは比べるようなものではないのか。
それすらもわからない。試せばきっとわかる。

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