Koa2从零搭建完整工程②
2017-09-29 本文已影响725人
二娃__
写在前面
看完了廖大神的 JavaScript 教程,特记录一下从0搭建一个完整的 koa2 工程,主要包含:
- 处理静态资源
- 模版渲染处理
- 数据库 ORM 处理
- REST API 处理
- 动态解析 controllers
目录
- 数据库 ORM 处理模块
- REST API 处理中间件
- 动态解析 controllers 中间件
- 最终的 app.js 文件
数据库 ORM 处理模块
数据库配置分离
新建config-default.js
、config-test.js
文件
module.exports = {
database: 'test',
username: 'root',
password: 'root',
host: 'localhost',
dialect: 'mysql',
port: 3306
};
新建config.js
文件
const defaultConfig = './config-default.js';//默认配置
const overrideConfig = './config-override.js';//线上配置,自动覆盖其他配置
const testConfig = './config-test.js';
const fs = require('mz/fs');
var config = null;
if (process.env.NODE_ENV === 'test') {
console.log(`Load ${testConfig}...`);
config = require(testConfig);
} else {
console.log(`Load ${defaultConfig}...`);
config = require(defaultConfig);
try {
//如果线上配置存在,就是覆盖默认配置
if (fs.statSync(overrideConfig).isFile) {
console.log(`Load ${overrideConfig}...`);
config = require(overrideConfig);
}
} catch (e) {
console.log(`Cannot load ${overrideConfig}...`);
}
}
module.exports = config;
封装 db 模块
分别安装sequelize
、node-uuid
、mysql2
npm i -S sequelize
npm i -S node-uuid
npm i -S mysql2
新建db.js
文件
const Sequelize = require('sequelize');
const config = require('./config');
const uuid = require('node-uuid');
console.log('init sequelize...');
//生成uuid的方法
function generateId() {
return uuid.v4;
}
//根据配置创建 sequelize 实例
const sequelize = new Sequelize(config.database, config.username, config.password, {
host: config.host,
dialect: config.dialect,
pool: {
max: 5,
min: 0,
idle: 10000
}
});
//监听数据库连接状态
sequelize
.authenticate()
.then(() => {
console.log('Connection has been established successfully.');
})
.catch(e => {
console.error('Unable to connect to the database:', e);
});
//定义统一的 id 类型
const ID_TYPE = Sequelize.STRING(50);
//定义字段的所有类型
const TYPES = ['STRING', 'INTEGER', 'BIGINT', 'TEXT', 'DOUBLE', 'DATEONLY', 'BOOLEAN'];
//要对外暴露的定义 model 的方法
function defineModel(name, attributes) {
var attrs = {};
//解析外部传入的属性
Object.keys(attributes).forEach(key => {
var value = attributes[key];
if (typeof value === 'object' && value['type']) {
//默认字段不能为 null
value.allowNull = value.allowNull || false;
attrs[key] = value;
} else {
attrs[key] = {
type: value,
allowNull: false
};
}
});
//定义通用的属性
attrs.id = {
type: ID_TYPE,
primaryKey: true
};
attrs.createAt = {
type: Sequelize.BIGINT,
allowNull: false
};
attrs.updateAt = {
type: Sequelize.BIGINT,
allowNull: false
};
attrs.version = {
type: Sequelize.BIGINT,
allowNull: false
};
//真正去定义 model
return sequelize.define(name, attrs, {
tableName: name,
timestamps: false,
hooks: {
beforeValidate: obj => {
var now = Date.now();
if (obj.isNewRecord) {
console.log('will create entity...' + obj);
if (!obj.id) {
obj.id = generateId();
}
obj.createAt = now;
obj.updateAt = now;
obj.version = 0;
} else {
console.log('will update entity...' + obj);
obj.updateAt = now;
obj.version++;
}
}
}
});
}
//模块对外暴露的属性
var exp = {
//定义 model 的方法
defineModel: defineModel,
//自动创建数据表的方法,注意:这是个异步函数
sync: async () => {
// only allow create ddl in non-production environment:
if (process.env.NODE_ENV !== 'production') {
await sequelize
.sync({ force: true })//注意:这是个异步函数
.then(() => {
console.log('Create the database tables automatically succeed.');
})
.catch(e => {
console.error('Automatically create the database table failed:', e);
});
} else {
throw new Error('Cannot sync() when NODE_ENV is set to \'production\'.');
}
}
};
//模块输出所有字段的类型
TYPES.forEach(type => {
exp[type] = Sequelize[type];
});
exp.ID = ID_TYPE;
exp.generateId = generateId;
module.exports = exp;
封装 model 模块
新建model.js
文件和models
文件夹
const fs = require('mz/fs');
const db = require('./db');
//读取 models 下的所有文件
var files = fs.readdirSync(__dirname + '/models');
//过滤出 .js 结尾的文件
var js_files = files.filter(f => {
return f.endsWith('.js');
});
module.exports = {};
//模块输出所有定义 model 的模块
js_files.forEach(f => {
console.log(`import model from file ${f}...`);
//得到模块的名字
var name = f.substring(0, f.length - 3);
module.exports[name] = require(__dirname + '/models/' + name);
});
//模块输出数据库自动建表的方法,注意:这是个异步函数
module.exports.sync = async () => {
await db.sync();
};
最后创建init-db.js
const model = require('./model.js');
//异步可执行函数
(async () => {
//调用 sync 方法初始化数据库
await model.sync();
console.log('init db ok!');
//初始化成功后退出。这里有个坑,因为 sync 是异步函数,所以要等该函数返回再执行退出程序!
process.exit(0);
})();
REST API 处理中间件
新建rest.js
文件
//模块输出为一个 json 对象v
module.exports = {
//定义 APIError 对象
APIError: function (code, message) {
//错误代码命名规范为 大类:子类
this.code = code || 'internal:unknown_error';
this.message = message || '';
},
//初始化 restify 中间件的方法
restify: pathPrefix => {
//处理请求路径的前缀
pathPrefix = pathPrefix || '/api/';
//返回 app.use() 要用的异步函数
return async (ctx, next) => {
var rpath = ctx.request.path;
//如果前缀请求的是 api
if (rpath.startsWith(pathPrefix)) {
ctx.rest = data => {
ctx.response.type = 'application/json';
ctx.response.body = data;
};
try {
//尝试捕获后续中间件抛出的错误
await next();
} catch (e) {
//捕获错误后的处理
ctx.response.status = 400;
ctx.response.type = 'application/json';
ctx.response.body = {
code: e.code || 'internal:unknown_error',
message: e.message || '',
};
}
} else {
await next();
}
};
}
};
在app.js
中使用该中间件
...
//直接引入初始化的方法
const restify = require('./rest').restify;
...
//中间件5:REST API 中间件
app.use(restify());
...
动态解析 controllers 中间件
新建controller.js
和controllers
文件夹
const path = require('path');
const fs = require('mz/fs');
function addControllers(router, dir) {
//读取控制器所在目录所有文件
var files = fs.readdirSync(path.join(__dirname, dir));
//过滤出 .js 文件
var js_files = files.filter(f => {
return f.endsWith('.js');
});
//遍历引入控制器模块并处理 路径-方法 的映射
js_files.forEach(f => {
console.log(`Process controller ${f}...`);
//引入控制器模块
var mapping = require(path.join(__dirname, dir, f));
//处理映射关系
addMapping(router, mapping);
});
}
function addMapping(router, mapping) {
//定义跟 router 方法的映射
//以后想要扩展方法,直接在这里加就可以了
const methods = {
'GET': router.get,
'POST': router.post,
'PUT': router.put,
'DELETE': router.delete
};
//遍历 mapping,处理映射
//mapping key 的格式:'GET /'
Object.keys(mapping).forEach(url => {
//用 every 方法遍历 methods
Object.keys(methods).every((key, index, array) => {
//如果前缀匹配就注册到 router
var prefix = key + ' ';
if (url.startsWith(prefix)) {
//获取 path
var path = url.substring(prefix.length);
//注册到 router
array[key].call(router, path, mapping[url]);
console.log(`Register URL mapping: ${url}...`);
//终止 every 循环
return false;
}
//遍历到最后未能注册上时,打印出信息
if (index == array.length - 1) {
console.log(`invaild URL ${url}`);
}
//继续 every 循环
return true;
});
});
}
//模块输出一个函数,dir 为控制器目录
module.exports = dir => {
var
dir = dir || 'controllers',
router = require('koa-router')();
//动态注册控制器
addControllers(router, dir);
return router.routes();
};
在app.js
中使用该中间件
...
const controller = require('./controller');
...
//中间件6:动态注册控制器
app.use(controller());
...
最终的 app.js 文件
// 导入koa,和koa 1.x不同,在koa2中,我们导入的是一个class,因此用大写的Koa表示:
const Koa = require('koa');
const bodyparser = require('koa-bodyparser');
const templating = require('./templating');
//直接引入初始化的方法
const restify = require('./rest').restify;
const controller = require('./controller');
// 创建一个Koa对象表示web app本身:
const app = new Koa();
// 生产环境上必须配置环境变量 NODE_ENV = 'production'
const isProduction = process.env.NODE_ENV === 'production';
//中间件1:计算响应耗时
app.use(async (ctx, next) => {
console.log(`Precess ${ctx.request.method} ${ctx.request.url}...`);
var
start = Date.now(),
ms;
await next();// 调用下一个中间件(等待下一个异步函数返回)
ms = Date.now() - start;
ctx.response.set('X-Response-Time', `${ms}ms`);
console.log(`Response Time: ${ms}ms`);
});
//中间件2:处理静态资源,非生产环境下使用
if (!isProduction) {
//引入 static-files 中间件,直接调用该模块输出的方法
app.use(require('./static-files')());
}
//中间件3:解析原始 request 对象 body,绑定到 ctx.request.body
app.use(bodyparser());
//中间件4:模版文件渲染
app.use(templating({
noCache: !isProduction,
watch: !isProduction
}));
//中间件5:REST API 中间件
app.use(restify());
//中间件6:动态注册控制器
app.use(controller());
// 在端口3000监听:
app.listen(3000);
console.log('app started at port 3000...');
写在最后
至此,整个工程也就搭建完了,当然还是要对整个基础工程的功能进行测试一下,才能保证可用。等测试完毕后,还可以进一步制作成脚手架
。