Koa2從零搭建完整工程②

寫在前面

看完了廖大神的 JavaScript 教程,特記錄一下從0搭建一個(gè)完整的 koa2 工程,主要包含:

  • 處理靜態(tài)資源
  • 模版渲染處理
  • 數(shù)據(jù)庫(kù) ORM 處理
  • REST API 處理
  • 動(dòng)態(tài)解析 controllers

目錄

  1. 數(shù)據(jù)庫(kù) ORM 處理模塊
  2. REST API 處理中間件
  3. 動(dòng)態(tài)解析 controllers 中間件
  4. 最終的 app.js 文件

數(shù)據(jù)庫(kù) ORM 處理模塊

數(shù)據(jù)庫(kù)配置分離

新建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';//默認(rèn)配置
const overrideConfig = './config-override.js';//線上配置,自動(dòng)覆蓋其他配置
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 {
        //如果線上配置存在,就是覆蓋默認(rèn)配置
        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;
}

//根據(jù)配置創(chuàng)建 sequelize 實(shí)例
const sequelize = new Sequelize(config.database, config.username, config.password, {
    host: config.host,
    dialect: config.dialect,
    pool: {
        max: 5,
        min: 0,
        idle: 10000
    }
});

//監(jiān)聽(tīng)數(shù)據(jù)庫(kù)連接狀態(tài)
sequelize
    .authenticate()
    .then(() => {
        console.log('Connection has been established successfully.');
    })
    .catch(e => {
        console.error('Unable to connect to the database:', e);
    });

//定義統(tǒng)一的 id 類型
const ID_TYPE = Sequelize.STRING(50);
//定義字段的所有類型
const TYPES = ['STRING', 'INTEGER', 'BIGINT', 'TEXT', 'DOUBLE', 'DATEONLY', 'BOOLEAN'];

//要對(duì)外暴露的定義 model 的方法
function defineModel(name, attributes) {
    var attrs = {};
    //解析外部傳入的屬性
    Object.keys(attributes).forEach(key => {
        var value = attributes[key];
        if (typeof value === 'object' && value['type']) {
            //默認(rèn)字段不能為 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++;
                }
            }
        }
    });
}

//模塊對(duì)外暴露的屬性
var exp = {
    //定義 model 的方法
    defineModel: defineModel,
    //自動(dòng)創(chuàng)建數(shù)據(jù)表的方法,注意:這是個(gè)異步函數(shù)
    sync: async () => {
        // only allow create ddl in non-production environment:
        if (process.env.NODE_ENV !== 'production') {
            await sequelize
                .sync({ force: true })//注意:這是個(gè)異步函數(shù)
                .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');

//過(guò)濾出 .js 結(jié)尾的文件
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);
});

//模塊輸出數(shù)據(jù)庫(kù)自動(dòng)建表的方法,注意:這是個(gè)異步函數(shù)
module.exports.sync = async () => {
    await db.sync();
};
最后創(chuàng)建init-db.js
const model = require('./model.js');

//異步可執(zhí)行函數(shù)
(async () => {
    //調(diào)用 sync 方法初始化數(shù)據(jù)庫(kù)
    await model.sync();
    console.log('init db ok!');
    //初始化成功后退出。這里有個(gè)坑,因?yàn)?sync 是異步函數(shù),所以要等該函數(shù)返回再執(zhí)行退出程序!
    process.exit(0);
})();

REST API 處理中間件

新建rest.js文件

//模塊輸出為一個(gè) json 對(duì)象v
module.exports = {
    //定義 APIError 對(duì)象
    APIError: function (code, message) {
        //錯(cuò)誤代碼命名規(guī)范為 大類:子類
        this.code = code || 'internal:unknown_error';
        this.message = message || '';
    },
    //初始化 restify 中間件的方法
    restify: pathPrefix => {
        //處理請(qǐng)求路徑的前綴
        pathPrefix = pathPrefix || '/api/';
        //返回 app.use() 要用的異步函數(shù)
        return async (ctx, next) => {
            var rpath = ctx.request.path;
            //如果前綴請(qǐng)求的是 api
            if (rpath.startsWith(pathPrefix)) {
                ctx.rest = data => {
                    ctx.response.type = 'application/json';
                    ctx.response.body = data;
                };
                try {
                    //嘗試捕獲后續(xù)中間件拋出的錯(cuò)誤
                    await next();
                } catch (e) {
                    //捕獲錯(cuò)誤后的處理
                    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());
...

動(dòng)態(tài)解析 controllers 中間件

新建controller.jscontrollers文件夾

const path = require('path');
const fs = require('mz/fs');

function addControllers(router, dir) {
    //讀取控制器所在目錄所有文件
    var files = fs.readdirSync(path.join(__dirname, dir));
    //過(guò)濾出 .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));
        //處理映射關(guān)系
        addMapping(router, mapping);
    });
}

function addMapping(router, mapping) {
    //定義跟 router 方法的映射
    //以后想要擴(kuò)展方法,直接在這里加就可以了
    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) => {
            //如果前綴匹配就注冊(cè)到 router
            var prefix = key + ' ';
            if (url.startsWith(prefix)) {
                //獲取 path
                var path = url.substring(prefix.length);
                //注冊(cè)到 router
                array[key].call(router, path, mapping[url]);
                console.log(`Register URL mapping: ${url}...`);
                //終止 every 循環(huán)
                return false;
            }
            //遍歷到最后未能注冊(cè)上時(shí),打印出信息
            if (index == array.length - 1) {
                console.log(`invaild URL ${url}`);
            }
            //繼續(xù) every 循環(huán)
            return true;
        });
    });
}

//模塊輸出一個(gè)函數(shù),dir 為控制器目錄
module.exports = dir => {
    var
        dir = dir || 'controllers',
        router = require('koa-router')();
    //動(dòng)態(tài)注冊(cè)控制器
    addControllers(router, dir);
    return router.routes();
};

app.js中使用該中間件

...
const controller = require('./controller');
...
//中間件6:動(dòng)態(tài)注冊(cè)控制器
app.use(controller());
...

最終的 app.js 文件

// 導(dǎo)入koa,和koa 1.x不同,在koa2中,我們導(dǎo)入的是一個(gè)class,因此用大寫的Koa表示:
const Koa = require('koa');
const bodyparser = require('koa-bodyparser');
const templating = require('./templating');
//直接引入初始化的方法
const restify = require('./rest').restify;
const controller = require('./controller');

// 創(chuàng)建一個(gè)Koa對(duì)象表示web app本身:
const app = new Koa();
// 生產(chǎn)環(huán)境上必須配置環(huán)境變量 NODE_ENV = 'production'
const isProduction = process.env.NODE_ENV === 'production';

//中間件1:計(jì)算響應(yīng)耗時(shí)
app.use(async (ctx, next) => {
    console.log(`Precess ${ctx.request.method} ${ctx.request.url}...`);
    var
        start = Date.now(),
        ms;
    await next();// 調(diào)用下一個(gè)中間件(等待下一個(gè)異步函數(shù)返回)
    ms = Date.now() - start;
    ctx.response.set('X-Response-Time', `${ms}ms`);
    console.log(`Response Time: ${ms}ms`);
});

//中間件2:處理靜態(tài)資源,非生產(chǎn)環(huán)境下使用
if (!isProduction) {
    //引入 static-files 中間件,直接調(diào)用該模塊輸出的方法
    app.use(require('./static-files')());
}

//中間件3:解析原始 request 對(duì)象 body,綁定到 ctx.request.body
app.use(bodyparser());

//中間件4:模版文件渲染
app.use(templating({
    noCache: !isProduction,
    watch: !isProduction
}));

//中間件5:REST API 中間件
app.use(restify());

//中間件6:動(dòng)態(tài)注冊(cè)控制器
app.use(controller());

// 在端口3000監(jiān)聽(tīng):
app.listen(3000);
console.log('app started at port 3000...');

寫在最后

至此,整個(gè)工程也就搭建完了,當(dāng)然還是要對(duì)整個(gè)基礎(chǔ)工程的功能進(jìn)行測(cè)試一下,才能保證可用。等測(cè)試完畢后,還可以進(jìn)一步制作成腳手架

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容