Koa2从零搭建完整工程②

2017-09-29  本文已影响725人  二娃__

写在前面

看完了廖大神的 JavaScript 教程,特记录一下从0搭建一个完整的 koa2 工程,主要包含:

目录

  1. 数据库 ORM 处理模块
  2. REST API 处理中间件
  3. 动态解析 controllers 中间件
  4. 最终的 app.js 文件

数据库 ORM 处理模块

数据库配置分离

新建config-default.jsconfig-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 模块

分别安装sequelizenode-uuidmysql2

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.jscontrollers文件夹

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...');

写在最后

至此,整个工程也就搭建完了,当然还是要对整个基础工程的功能进行测试一下,才能保证可用。等测试完毕后,还可以进一步制作成脚手架

上一篇下一篇

猜你喜欢

热点阅读