Edited at

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は何が違うんだろう?あるいは比べるようなものではないのか。

それすらもわからない。試せばきっとわかる。