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の取得方法を変更
#!/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);
}
メイン処理(サーバ定義)
- やってる事は全く同じだが、クラス構造に変えた。
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での利用を想定)
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()だけ抽象メソッドにした
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()をオーバーライド実装して、ルーティング設定を返す
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()をオーバーライド実装して、ルーティング設定を返す
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での呼びだしが必要なくなったので辞めた。
・・・・・・
"scripts": {
"start": "nodemon ./bin/www",
"debug": "nodemon --inspect ./bin/www",
・・・・・・
tsconfig.json
- "module": "commonjs"を指定する事で、トランスパイル時にimportがrequireに変わるようにした。
- "target": "es5" → "es2017"にアップグレード
・・・・・・
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
・・・・・・
npm:buildのタスクを実行してし、ページにアクセス。
- npm:buildタスク:npm run build-ts && npm run copy-static-assets
- 修正後の再起動はnodemonが自動検知して勝手にやってくれるので、ビルドのみでOK
参考
トランスパイル後のソース
- import文がrequireになってる。
- 後はES2017に従って大体そのまま
"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
プロジェクトのソース
感想
Visual Studio Codeで初めてTypeScriptのデバッグ実行した。むしろ今まで知らずに開発してたのがすごい。
次やりたい事
そろそろビューを付けたくなって来たが、Angular6で行くか、巷で話題(?)のVue.jsで行くか悩んでる。
なんで急激にVue.js推しが始まってるのか、わかってない。
やっぱりAngularの初期学習コストは他と比べてエゲツないなんだろうか。
かく言う自分もAngularの全てを自由自在に使いこなせるタマじゃあない。
どうやらNodeのv8.4以降、HTTP/2が使えるようになってるようだ。よくわからないが雰囲気で使ってみたい。
あるいは、WebSocketならば昔、Socket.ioを使った事があるのでそちらでやるか。
HTTP/2とWebSocketは何が違うんだろう?あるいは比べるようなものではないのか。
それすらもわからない。試せばきっとわかる。