美文网首页
Koa2从零搭建完整工程②

Koa2从零搭建完整工程②

作者: 二娃__ | 来源:发表于2017-09-29 12:15 被阅读725次

    写在前面

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

    • 处理静态资源
    • 模版渲染处理
    • 数据库 ORM 处理
    • REST API 处理
    • 动态解析 controllers

    目录

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

    写在最后

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

    相关文章

      网友评论

          本文标题:Koa2从零搭建完整工程②

          本文链接:https://www.haomeiwen.com/subject/nulpextx.html