使用Koa做Web HTTP服务,根据MVC来组织项目结构,由于Koa自身已经在Express上精简了很多,在MVC结构上不免会向Koa的app上灌入很多对象,使用require去加载文件当项目规模越来越复杂时会变得异常复杂。现在Node中找到比较好的IoC,只找到Bearcat,已经在Pomelo中使用了Bearcat。Pomelo自身又不支持async/await,如是种种,蛋疼。现在先做HTTP服务器,借鉴Pomelo中一些比较不错的地方。
简单梳理一下,使用Node搭建的MVC框架,由于Web服务最终会做前后端分离,所以这里的MVC其实只是M和C,主要用来做HTTP接口使用。V已经被独立出来,所以这里也就先不管它了。Web框架使用的是Koa,为了避免不断地require。M和C以及router何如优雅的组合在一起,是一个头疼的问题。现在虽然已经完成,但不知道性能如何。关键部分,在两个位置,第一点是在router中动态加载controller,第二个位置是在controller中动态加载model。其实这两点最终归结到一点就是动态加载的问题。因此抽离出一个单独的加载器。参考多方面的资料,又因为在Pomelo中发现RPC的写法比较优雅,说白了,就是比较省事儿,省代码。
好了,简单说下原本的需求,本来是在是用Pomelo做实时应用,为了剥离出一些HTTP接口的服务,因为开始做一个HTTP MVC的服务。
废话说了一堆,首先看下拼积木的组件。
$ vim web-server/package.json
{"name": "web-server","version": "1.0.0","description": "web server","main": "app.js","scripts": {"test": "echo "Error: no test specified" && exit 1"},"keywords": ["web","server"],"author": "junchow","license": "ISC","dependencies": {"koa": "^2.11.0","koa-router": "^7.4.0","koa-static": "^5.0.0","mysql2": "^2.1.0","sequelize": "^5.21.3"}
}
这里只是用最基础的koa、koa-route、sequelize、mysql。后续需要的再添加。
HTTP服务的MVC结构
文件夹 | 描述 |
---|---|
app.js | 项目入口文件 |
config | 配置文件夹,配置文件以JSON格式,分为development和production。 |
config/app.json | 默认应用的配置文件 |
route | 路由文件夹,根据应用划分路由文件。 |
route/web.js | web应用的路由规则文件 |
util | 工具文件夹 |
util/loader.js | 文件加载器 |
app | 应用文件夹,内含MVC组织结构。 |
app/controller | 应用控制器文件将 |
app/model | 应用数据模型文件夹 |
app/service | 服务层文件夹 |
app/middleware | 应用中间件文件 |
app/view | 应用视图文件夹 |
应用入口文件
$ vim app.js
const path = require("path");
const Koa = require("koa");
const koaStatic = require("koa-static");
const KoaRouter = require("koa-router");
const Sequelize = require("sequelize");//环境类型
const env = v.NODE_ENV || "development";
//创建应用
const app = new Koa();
//创建路由
const router = new KoaRouter();//定义路径
const basePath = __dirname;
const configPath = path.join(basePath, "config");
const staticPath = path.join(basePath, "static");
const routePath = path.join(basePath, "route");
const utilPath = path.join(basePath, "util");const appPath = path.join(basePath, "app");
const controllerPath = path.join(appPath, "controller");
const modelPath = path.join(appPath, "model");
const viewPath = path.join(appPath, "view");//读取配置
const config = require(path.join(configPath, "app"))[env];//配置静态资源目录
app.use(koaStatic(staticPath));//链接数据库
const sequelize = new Sequelize(config.sequelize);
//测试链接
sequelize.authenticate().then(_=>console.log("connection has been established successfully")).catch(err=&("unable to connect to the database", err));//动态加载
const Loader = require("./util/loader");
ller = Loader.load(controllerPath);
del = Loader.load(modelPath, sequelize);//配置路由
require(path.join(routePath, "web"))(app, router);//监听端口
app.listen(config.server.port, config.server.host, _=>{console.log("server start", config.server.host, config.server.port);
});
说明一下
重点说明一下,app.js应用入口文件中最重要的是app这个对象,以及app对象上的context。其实不想将很多东西挂到app和context上,但为了代码书写更加方便,性能方面都没考虑。
应用配置
$ config/app.json
{"development":{"server":{"host":"127.0.0.1","port":3000},"sequelize":{"host":"127.0.0.1","port":3306,"username":"root","password":"root","database":"pomelo","dialect":"mysql","dialectOptions":{"charset":"utf8mb4"},"pool":{"min":0,"max":5,"idle":10000,"acquire":3000},"timezone":"+08:00"}}
}
关于动态加载应用,在app.js入口文件中首先定义了当前环境类型,默认使用development。
//环境类型
const env = v.NODE_ENV || "development";
然后是根据定义的环境类型进行require
const config = require(path.join(configPath, "app"))[env];
以后添加组件时,直接在app.json中加配置项,在app.js中使用config。
数据库这里有些蛋疼,使用的是sequelize这个ORM。为什么要使用ORM,虽然ORM会消耗性能,为了省事儿,其次是为了兼容不同的数据库。自己用mysql和mogoose做也可以,这里主要是为了学习学习Node的ORM。
在app.js中只是使用sequelize作为数据库配置和连接最后生成sequelize对象,这个对象在创建model的时候是必须的。不可能在每个model中都先config然后再connect,这又会产生一堆冗余代码。如果将初始化连接单独成一个文件,那每个model中有是一堆的require。真心不喜欢require,项目只会越来越复杂,一旦require,日后重构会跟见鬼似的。问题来了,如何将sequelize传递给每个model呢?这里先思考思考,留在后面再说。
//链接数据库
const sequelize = new Sequelize(config.sequelize);
//测试链接
sequelize.authenticate().then(_=>console.log("connection has been established successfully")).catch(err=&("unable to connect to the database", err));
先来说下路由,路由是MVC的第一关。
//配置路由
require(path.join(routePath, "web"))(app, router);
这里默认加载了route/web.js,web.js表示使用web应用的所有路由规则,传入了app和router。其中这种写法也不是很好,如果不传入app和router,web.js只是返回router,也会存在几个问题。先看下web.js的路由:
$ vim route/web.js
ports = (app, router) => {//路由规则("/", ller.index.index);("/test", st);//路由配置app.utes()).use(router.allowedMethods());
};
看到
//动态加载
const Loader = require(path.join(utilPath, "loader"));
ller = Loader.load(controllerPath);
这里还是loader起了关键的作用,也是寻思参照几个方案,再加上pomelo中使用方式比较多,所以最终觉得这些方式比较不错。至于后面再说。
既然路由已经达到了控制器,控制器接受路由时都会有两个参数ctx上下文和next回调函数。由于koa已经支持async/await,这一点儿是非常非常好的,以前的回调地狱的代码逻辑,看起来很痛苦。虽然也有async的waterfall,说实话看着也很别扭。在pomelo中,由于rpc不支持async/await,我也不得不使用async,两种方式对比,真的感觉很不一样。
$ vim app/controller/index.js
exports.index = async (ctx, next)=>{const where = {aid:900005};const users = del.user.findAll({where});console.log(users);ctx.body = "homepage";
};st = async (ctx, next)=>{console.log("index test");
};
路由到了具体的控制器方法后,不用说,控制器要去调用模型获取数据。这里也是一个寻思好久的位置。因为如果又是一堆require,会难受香菇。所以重点是这里 const users = del.user.findAll({where});
这里是直接调用app/model文件夹下的user模型,user模型是使用sequelizer创建的,sequelizer的model中提供了findAll的方法。最开始的想法是如何不适用require去加载模型,后来发现搞不了。接着又想如何在ctx上做些什么,才开始试了做了几个demo。发现代码真心丑。现在终于可以更加直观了。虽然这里是ctx,因为app到了controller,已经无法直接在使用了。所以这里就只能在ctx上搞事情了。回到app.js入口文件中,使用自定义的Loader加载器动态加载了model并穿入了sequelize对象,这一点儿很重要。
del = Loader.load(modelPath, sequelize);
写法上开始估计有点儿怪异,不会熟悉后估计会好很多,当没有了require,感觉真的好很多。
模型这里也说上太多,因为使用了sequelizer,所有的工作都是它完成的,自己也没有去做什么事情。之前没用orm自己用mysql做个model的基类,来拼sql,用起来也还可以,自己封装的,用起来方便。又来了orm最起码更换数据库后,这里将会强大不少。sequelizer也是才找到,慢慢开始学习使用。先学习学习Node的ORM再说。
const Sequelize = require("sequelize");
ports = sequelize=>{const model = sequelize.define("users",{aid:{type:Sequelize.BIGINT(11), allowNull:false},unick:{type:Sequelize.STRING(20), allowNull:false},uface:{type:Sequelize.STRING(255), allowNull:false},});veAttribute("id");veAttribute("createdAt");veAttribute("updatedAt");return model;
};
这里就是将接收到的sequelizer来定义指定数据库表名,简单的做了关系对象的映射。代码并不优雅,暂时对sequelizer使用不太多,先这样。先把流程打通。
其中关键的重点是在加载器上,之前使用require时并没有发现单独封装一个加载器有何作用。虽然其他的框架都会做这件事,在写了一些类似cocos的东西后发现加载器很重要。这里也是只是简单的使用了一下。有些项目会直接去fs读文件或文件夹,全部require。在网络应用中io的读写是比较吃力的一个操作,加载器会做这些事情,这里的加载器也是去读取文件,只是会动态的读取,而不会整个文件全部读取。也就是用到哪个读哪一个,另外这里的加载器会根据类似
$ vim util/loader.js
const path = require('path');
const fs = require('fs');class Loader {constructor () {}static load(filepath, options=null){const data = [filepath];const handler = {construct(target, args){return false;},get(target, key, receiver){if(key && String.call(key)==="[object String]"){if(data.indexOf(key) === -1){data.push(key);}}const file = solve(__dirname, lative(__dirname, data.join("/")));//console.log(data, key, data.indexOf(key), file);if(Loader.isDirectory(file)){return new Proxy({path: data}, handler);}else if(Loader.isFile(file)){if(options){return require(file)(options);}else{return require(file);}}}};return new Proxy({path:data}, handler);}static isDirectory (target) {if(!fs.existsSync(target)){return false;}let stat;try{stat = fs.statSync(target);}catch(e){(e);}if(stat && stat.isDirectory()) {return true}return false;}static isFile (target) {target = target + ".js";if(!fs.existsSync(target)){return false;}let stat;try{stat = fs.statSync(target);}catch(e){(e);}if(stat && stat.isFile()) {return true;}return false;}
}
ports = Loader;
Loader中重点是使用Node的Proxy组件,一直对proxy这个单词感觉很怪异。虽然读了proxy的文档,还是感觉半懂不懂的状态。慢慢来吧,反正是对proxy这个单词,总是有种云里雾里的感觉,说明白又不明白,说不明白又明白。还是不是很通透。换一章再单独深入下研究研究。
好的,到此为此,基本的架子有了,起码满足了个人的需要。至于性能和问题,在使用中在优化。边做边学再思考。其他的模块添加容易,这里也没有多说。优化升级再慢慢补充。
其实,一直在想有个IoC的容器去管理这个对象,但是一直没有发现比较好的。没有找到好的轮子,可以学习借鉴下。pomelo中虽然在使用bearcat,但说实话对bearcat的写法开始也不太习惯。还是要自己深入的学习源码。
本文发布于:2024-01-30 20:46:59,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170661884222729.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |