typescript + tslint 规范化代码,选择typeorm作为数据库的orm,选择log4js输出日志
├── bin
│ └── www.ts // 启动应用
├── logs // 存放日志
├── src
│ ├── app
│ │ ├── components // 控制器组件
│ │ │ ├── account
│ │ │ │ ├── account.controller.ts
│ │ │ │ └── account.service.ts
│ │ │ └── user
│ │ │ ├── user.controller.ts
│ │ │ └── user.service.ts
│ │ ├── constants // 常量
│ │ │ └── index.ts
│ │ ├── core // 一些核心类或全局引用的方法
│ │ │ ├── error.ts
│ │ │ └── logger.ts
│ │ ├── database // 数据库连接
│ │ │ └── index.ts
│ │ ├── entities // 实体
│ │ │ └── user
│ │ │ ├── user.entity.ts
│ │ │ └── user.model.ts
│ │ ├── middleware // 中间件
│ │ │ ├── error.middleware.ts
│ │ │ ├── jwt.middleware.ts
│ │ │ ├── logger.middleware.ts
│ │ │ └── response.middleware.ts
│ │ └── utils // 工具函数
│ │ │ └── crypto.ts
│ │ └── app.ts // 应用启动类
│ └── environments // 多环境配置
│ ├── env.dev.ts
│ ├── env.prop.ts
│ └── index.ts
├── nodemon.json // nodemon 配置, watch ts文件
├── package-lock.json
├── package.json
├── tsconfig.json
└── tslint.json
{
"name": "huzz-koa-template",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"@koa/cors": "^3.0.0",
"koa": "^2.11.0",
"koa-body": "^4.1.1",
"koa-jwt": "^3.6.0",
"koa-route-decors": "^1.0.3",
"koa-router": "^7.4.0",
"koa-static": "^5.0.0",
"log4js": "^5.3.0",
"mysql2": "^2.0.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.20"
},
"devDependencies": {
"@types/jsonwebtoken": "^8.3.5",
"@types/koa": "^2.0.52",
"@types/koa-router": "^7.0.42",
"@types/koa-static": "^4.0.1",
"@types/koa__cors": "^2.2.3",
"cross-env": "^6.0.3",
"nodemon": "^1.19.4",
"ts-node": "^8.5.0",
"tslint": "^5.20.1",
"tslint-config-standard": "^9.0.0",
"typescript": "^3.7.2"
},
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon --config nodemon.json",
"compile": "tsc",
"start": "npm run compile && pm2 start ./bin/www --name app",
"restart": "npm run compile && pm2 start ./dist/app/app.js",
"stop": "pm2 stop app"
},
"repository": {
"type": "git",
"url": "git+https://github.com/xhuz/huzz-koa-template.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/xhuz/huzz-koa-template/issues"
},
"homepage": "https://github.com/xhuz/huzz-koa-template#readme"
}
// error.ts
// 继承自原生Error类, 方便抛出各类异常
export class CustomError extends Error {
code: number;
constructor(code: number, message: string) {
super(message);
this.code = code;
}
}
// logger.ts 具体配置自行查看log4js 文档
import {configure, getLogger} from 'log4js';
import {resolve} from 'path';
import {Context} from 'koa';
const logPath = resolve(__dirname, '../../../logs'); // log存放路径,确保该路径存在
configure({
appenders: {
console: {type: 'console'},
dateFile: {type: 'dateFile', filename: `${logPath}/log.log`, pattern: 'yyyy-MM-dd', alwaysIncludePattern: true, keepFileExt: true}
},
categories: {
default: {
appenders: ['console', 'dateFile'],
level: 'info'
},
mysql: {
appenders: ['console', 'dateFile'],
level: 'info'
}
}
});
export const logger = getLogger('default');
export const mysqlLogger = getLogger('mysql');
export function logText(ctx: Context, ms: number) {
const remoteAddress = ctx.headers['x-forwarded-for'] || ctx.ip || ctx.ips || (ctx.socket && ctx.socket.remoteAddress);
return `${ctx.method} ${ctx.status} ${ctx.url} - ${remoteAddress} - ${ms}ms`;
}
配置数据库
import {createConnection, ConnectionOptions, Logger, QueryRunner} from 'typeorm';
import {environment} from '../../environments'; // 支持多环境自动导入不同配置,详见environments
import {mysqlLogger} from '../core/logger';
// 导出连接数据库函数
export function connection() {
const config: ConnectionOptions = environment.db as any;
Object.assign(config, {logger: new DbLogger()});
createConnection(config).then(() => {
console.log('mysql connect success');
}).catch(err => {
mysqlLogger.error(err);
});
}
// 用我们自己的logger来接管typeorm logger
class DbLogger implements Logger {
logQuery(query: string, parameters?: any[], queryRunner?: QueryRunner) {
mysqlLogger.info(query);
}
logQueryError(error: string, query: string, parameters?: any[], queryRunner?: QueryRunner) {
mysqlLogger.error(query, error);
}
logQuerySlow(time: number, query: string, parameters?: any[], queryRunner?: QueryRunner) {
mysqlLogger.info(query, time);
}
logSchemaBuild(message: string, queryRunner?: QueryRunner) {
mysqlLogger.info(message);
}
logMigration(message: string, queryRunner?: QueryRunner) {
mysqlLogger.info(message);
}
log(level: 'log' | 'info' | 'warn', message: any, queryRunner?: QueryRunner) {
switch (level) {
case 'info': {
mysqlLogger.info(message);
break;
}
case 'warn': {
mysqlLogger.warn(message);
}
}
}
}
多环境配置
// env.dev.ts
import {ConnectionOptions} from 'typeorm';
export const db: ConnectionOptions = {
type: 'mysql',
host: 'localhost',
port: 3307,
username: 'root',
password: '123456',
database: 'test',
logging: true,
// synchronize: true,
timezone: '+08:00',
dateStrings: true,
entities: ['../**/*.entity.ts']
};
// env.prod.ts
import {ConnectionOptions} from 'typeorm';
export const db: ConnectionOptions = {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '123456',
database: 'test',
logging: true,
timezone: '+08:00',
dateStrings: true,
entities: ['../**/*.entity.ts']
};
// index.ts 根据不同环境导出配置,需要其他环境,可自行添加环境文件,修改下面代码
import * as dev from './env.dev';
import * as prop from './env.prop';
const env = process.env.NODE_ENV;
let environment = dev;
if (env !== 'development') {
environment = prop;
}
export {environment};
// crypto.ts 密码加密
import * as Crypto from 'crypto';
export function cryptoPassword(pwd: string, key: string) {
return Crypto.createHmac('sha256', key).update(pwd).digest('hex');
}
// index.ts
export const JWT_SECRET = 'huzz-koa-server'; // jwt 秘钥
export const NO_AUTH_PATH = {
path: [/\//, /\/register/, /\/login/] // 公共接口,jwt排除的路由
};
// error.middleware.ts
import {Context} from 'koa';
import {logger} from '../core/logger';
export async function errorHandle(ctx: Context, next: () => Promise<any>) {
try {
await next();
} catch (err) {
if (!err.code) {
logger.error(err.stack);
}
ctx.body = {
code: err.code || -1,
message: err.message.trim()
};
ctx.status = 200; // http 状态码设为200,让前端不报错
}
}
// resoponse.middleware.ts
import {Context} from 'koa';
export async function responseHandle(ctx: Context, next: () => Promise<any>) {
if (ctx.result !== undefined) {
ctx.type = 'json';
ctx.body = {
code: 0,
data: ctx.result,
message: 'ok'
};
}
await next();
}
// logger.middleware.ts
import {Context} from 'koa';
import {logText, logger} from '../core/logger';
export async function loggerHandle(ctx: Context, next: () => Promise<any>) {
const start = Date.now();
await next();
const end = Date.now();
const ms = end - start;
const log = logText(ctx, ms);
logger.info(log);
}
import * as koaJwt from 'koa-jwt';
import {JWT_SECRET, NO_AUTH_PATH} from '../constants';
export const jwt = koaJwt({
secret: JWT_SECRET
}).unless(NO_AUTH_PATH); // 排除公共路由
数据库对应实体
// user/user.entity.ts 创建用户实体
import {Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn} from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id!: number; // ts严格模式下添加非空断言 "!"
@Column()
username!: string;
@Column()
password!: string;
@Column()
nickname!: string;
@CreateDateColumn()
createTime!: Date;
@UpdateDateColumn()
updateTime!: Date;
}
// user/user.model.ts 创建实体模型
import {User} from './user.entity';
import {getRepository, Repository} from 'typeorm';
import {cryptoPassword} from '../../utils/crypto';
import {Injectable} from 'koa-route-decors'; // 导入Injectable装饰器,申明该类可被注入
@Injectable()
export class UserModel {
private repository: Repository<User>;
private select: (keyof User)[] = ['id', 'username', 'nickname'];
constructor() {
this.repository = getRepository(User);
}
async create(user: User) {
const result = await this.repository.save(user);
return result;
}
async findById(id: number) {
const user = await this.repository.findOne(id, {select: this.select});
return user;
}
async findByUsername(username: string) {
const user = await this.repository.findOne({username}, {select: this.select});
return user;
}
async findAndCheckPassword(username: string, password: string) {
const user = await this.repository.findOne({username, password: cryptoPassword(password, username)}, {select: this.select});
return user;
}
async findAll() {
const users = await this.repository.find({select: ['id', 'username', 'nickname']});
return users;
}
}
// account.controller.ts 控制器,处理http的逻辑
import {Context} from 'koa';
import {Post, Controller} from 'koa-route-decors';
import * as jwt from 'jsonwebtoken';
import {JWT_SECRET} from '../../constants';
import {AccountService} from './account.service';
@Controller()
export class AccountController {
constructor(private accountService: AccountService) {}
@Post()
async register(ctx: Context, next: () => Promise<any>) {
const {username, password, nickname} = ctx.request.body;
const result = await this.accountService.insert(username, password, nickname);
ctx.result = {
id: result.id,
username: result.username,
nickname: result.nickname
};
await next();
}
@Post()
async login(ctx: Context, next: () => Promise<any>) {
const {username, password} = ctx.request.body;
// 验证密码并生成token
const user = await this.accountService.verifyPassword(username, password);
const token = jwt.sign({username: user.username, id: user.id}, JWT_SECRET, {expiresIn: '30d'});
ctx.result = {
id: user.id,
username: user.username,
nickname: user.nickname,
token
};
await next();
}
}
// account.service.ts 处理实体模型相关的逻辑
import {UserModel} from '../../entities/user/user.model';
import {CustomError} from '../../core/error';
import {User} from '../../entities/user/user.entity';
import {cryptoPassword} from '../../utils/crypto';
import {Injectable} from 'koa-route-decors';
@Injectable()
export class AccountService {
constructor(private userModel: UserModel) {}
async insert(username: string, password: string, nickname: string = '') {
const exist = await this.userModel.findByUsername(username);
if (exist) {
throw new CustomError(-1, '用户已存在');
}
const user = new User();
user.username = username;
user.password = cryptoPassword(password, username);
user.nickname = nickname;
const result = await this.userModel.create(user);
return result;
}
async verifyPassword(username: string, password: string) {
const user = await this.userModel.findAndCheckPassword(username, password);
if (user) {
return user;
} else {
throw new CustomError(-1, '用户名或密码错误');
}
}
}
其他和http相关的逻辑,扩展components
// app.ts
import 'reflect-metadata';
import * as Koa from 'koa';
import * as cors from '@koa/cors';
import * as body from 'koa-body';
import * as staticService from 'koa-static';
import * as Router from 'koa-router';
import {autoRouter} from 'koa-route-decors';
import {loggerHandle} from './middleware/logger.middleware';
import {errorHandle} from './middleware/error.middleware';
import {responseHandle} from './middleware/response.middleware';
import {jwt} from './middleware/jwt.middleware';
import {resolve} from 'path';
import {connection} from './database';
export class App {
private app: Koa;
constructor() {
this.app = new Koa();
this.init().catch(err => console.log(err));
}
// 装配各种中间件
private async init() {
const router = new Router();
const subRouter = await autoRouter(resolve(__dirname, './'));
router.use(subRouter.routes(), jwt); // 路由添加jwt验证
this.app
.use(cors())
.use(loggerHandle)
.use(errorHandle)
.use(body({
multipart: true
}))
.use(router.routes())
.use(router.allowedMethods())
.use(staticService(resolve(__dirname, '../../static')))
.use(responseHandle);
}
start(port: number) {
this.app.listen(port, () => {
connection();
console.log('service is started');
});
}
}
单独拆分出www主要是为了,分离服务层和应用层的逻辑
// www
const env = process.env.NODE_ENV;
let App = null;
if (env === 'development') {
App = require('../src/app/app').App;
} else {
App = require('../dist/app/app').App;
}
const app = new App();
app.start(8080); // 应用成功启动,监听8080端口
到这里整个koa项目架构就搭建完毕了,可能有些同学会发现没用配置路由,这是由于使用koa-route-decors库实现的用装饰器模式自动配置路由,想了解具体实现可以查看我的另外一篇文章用装饰器的语法写koa路由,这个库同时实现了控制器的依赖注入。
本项目已经上传到github,可点击链接查看源代码huzz-koa-tempalte