自定义web框架

不会造轮子的程序员不是一个好木匠

以下代码全部放在了github上:链接地址

制定web框架流程

我们要做的是一个轻量级,所以一些基础的功能还是需要的。这里制定一个简单的框架启动流程。
enter image description here

这里包含了几个简单的功能。

  1. 定制化的配置管理,根据环境变量自动加载
  2. 提供基础的工具库,可以自由加入需要的功能(加密,远程请求等)
  3. 一个简单使用的日志类,按照日期存储在项目根目录下的logs目录中
  4. 可以使用的路由基础方法,通过这个方法注入自定义的路由
  5. 一个简单实用的定时任务基础类,继承即可启动
  6. 可选择的app.js文件,可以对application对象注入一些自己的东西

定制目录规范

通过一个合适的规范可以减少很多开发上的东西。

我们通过在固定的目录中做固定的事情将框架内的不同功能区分开来。

1
2
3
4
5
6
7
8
9
10
11
- config //配置文件
- default.js //默认配置
- development.js //开发环境配置
- production.js //生成环境配置
- controller //控制器
- index.js //自定义路由地址
- schedule //定时任务文件
- test.js //自定义定时任务
- app.js //自定义启动文件
- index.js //启动文件,引用duck即可
- pm2.js //pm2的配置文件

这里只有根目录下的index.js是必须要的,主要是启动整个项目。config目录下主要是放置配置,这个目录一般情况下省不了。controller目录下放置路由的方法等,也是这个框架最常用的目录。其余的都是可选择的,不添加不影响功能。

基础框架

从上面的几点可以看出来,框架基于配置的方式做到动态加载。所以启动的时候会扫一次目录,将有的功能加进去,没有的功能不做处理。

lib目录下新建一个index.js文件,作为我们的整个框架的核心—-其实就是启动koa并将我们要实现的功能加上去:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Koa = require("koa");
const KoaBody = require("koa-body");

//附加一个根目录
const rootPath = process.cwd();
let app = new Koa();
app.root = rootPath;
//处理请求对象
app.use(KoaBody({
multipart: true,
strict: false,
jsonLimit: '10mb',
formLimit: '10mb',
textLimit: '10mb'
}));

使用过koa的同学可能会非常的熟悉,其实这里就是一个简单的koa初始化过程。不同的地方是后面我们要附加的一些操作。

执行app.js

我们给用户留一个可以操作顶层application对象的地方。入口就是这里:假如app.js文件存在就会将初始化中的app对象传进去处理一次再转出来。

1
2
3
4
5
6
7
//加载appcalition
try {
let application = require(app.root + "/app.js");
application(app);
} catch (error) {
Logger.info(error.message);
}

执行自定义路由

这里我们判断我们的controller目录下的文件。依次加载并将返回的路由对象加载到applicaiton上。这里利用的依然是一个动态加载的原理,将内容在启动阶段载入到内存中并执行它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//添加路由
if (fs.existsSync(app.root + "/controller")) {
let controller_list = fs.readdirSync(app.root + "/controller");
controller_list.forEach(item => {
try {
let controller_item = require(app.root + "/controller/" + item);
if (controller_item) {
app.use(controller_item.routes()).use(controller_item.allowedMethods());
}
} catch (error) {
Logger.info(error.message);
}
});
}

监听端口

正常情况下只需要监听一下端口即可。这里稍微处理了一次,假如用户使用命令退出进程或者进程异常退出,这里就会监听到这个操作。

这个地方可以自定义做一个触发事件,触发applicaiton的停止事件。比如数据库、redis等的连接断开操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const port = config("port") || 3000;
app.listen(port, function () {
Logger.info("app已启动:" + port)
});
process.on('SIGINT', function (a) {
process.exit();
});
process.on('exit', (code) => {
Logger.info("app已停止:" + code)
});
process.on('uncaughtException', (code) => {
Logger.info("app已停止:" + code)
});
module.exports = app;

启动定时任务

定时任务比较简单,只需要判断任务文件是否存在,然后执行即可。

1
2
3
4
5
6
7
8
9
10
11
12
//启动定时器
if (fs.existsSync(app.root + "/schedule")) {
let schedule_list = fs.readdirSync(app.root + "/schedule");
schedule_list.forEach(item => {
try {
let schedule_item = require(app.root + "/schedule/" + item);
if (schedule_item) new schedule_item()._run();
} catch (error) {
Logger.info(error.message);
}
});
}

tips:上面的几个方式可以作为一个工具类中的方法存在。

基础支持类、方法

上面的仅仅是一个基础的实现。假如你想现在就跑起来,不好意思,它们的依赖还没有实现呢。

基础配置类

npm库里已经有一个非常强大的配置加载方式了。这个轮子我们先不造了。npm install --save config安装一下就好了。

基础日志类

我们这里使用log4js库来完成日志的打印输出。这里稍微简单的配置一下内部的日志对象。

lib/logger.js中添加我们自己的日志配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const log4js = require('log4js');
log4js.configure({
appenders: {
stdout: {
type: "stdout"
},
error: {
type: 'dateFile',
filename: './logs/error.log',
},
info: {
type: 'dateFile',
filename: './logs/info.log'
}
},
categories: {
default: {
appenders: ["info", "stdout"],
level: 'info'
},
err: {
appenders: ['error', "stdout"],
level: 'error'
}
},
pm2: true,
pm2InstanceVar: "node-sso-wechat"
});

const logger = log4js.getLogger('error');
const logger2 = log4js.getLogger('info');

module.exports = {
error() {
logger.error(...arguments);
},
info() {
logger2.info(...arguments);
},
warm() {
logger2.info(...arguments);
}
};

这里我们简单的提供3中日志形式。

  1. 一直存在的输出到命令行
  2. 输出到info.log中info和warm方法
  3. 输出到error.js的error方法

这里将日志分开存主要是为了区分记录日志和错误信息这2中特殊情况。当然,也可以将这些配置放在config中,通过使用中去配置日志。

基础控制器方法

我们给使用者提供一个简单的路由方法,这样就不需要用户去主动实现路由了。用户只需要关注具体的路由和实现即可。

1
2
3
4
5
6
7
const Router = require("koa-router");

module.exports = function (prefix) {
let opts = {};
if (prefix) opts.prefix = prefix;
return new Router(opts);
}

可以看到我们使用的是koa-router这个库。用户调用的时候还可以传入prefix参数来配置路由的跟目录。

用户只需要使用返回的路由方法即可。

基础定时任务类

由于定时任务并不需要参数web的请求返回等过程,所以在设计的时候只需要继承我们的基础类就能实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const schedule = require('node-schedule');
/**
* 定时任务的基类
* 设置时间,可执行方法
* 默认执行run方法
*/
module.exports = class {
constructor() {
this.time = "";
}
start() {}
_run() {
schedule.scheduleJob(this.time, this.start);
}
}

我们仅仅做了3件事就完成了定时任务。

  1. 初始化time参数,预留给用户做时间间隔配置
  2. 初始化start方法,预留给用户做具体的执行内容
  3. _run方法就是我们自己的执行定时任务的方法,是不能给用户看到的。(当然还是可以重载了。。。)

使用我们的框架

一个简单的demo已经放在github上了:链接地址

启动入口

在根目录下添加index.js文件,引用我们的框架。

1
const Duck = require("node-duck");

自定义配置

在根目录下新建config目录,新建default.js默认配置,production.js等环境配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 默认的配置文件
* 不同环境下还会加载不同的配置
*/
module.exports = {
name: "duck.js",//给自己用的配置
port: "3001",//启动的接口
//初始化的body对象参数
body: {
multipart: true,
strict: false,
jsonLimit: '10mb',
formLimit: '10mb',
textLimit: '10mb'
}
}

自定义控制器

在根目录下新建controller目录,将我们的路由写在这个目录中。文件的名字对路由没有影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 测试路由
*/
const Duck = require("node-duck");
//默认跟路由
const Controller = new Duck.Controller("/");
//首页
Controller.get("", function (ctx) {
ctx.body = "hi,duck!";
})
//test目录
Controller.get("test", function (ctx) {
ctx.body = "hi,duck!";
})
//导出
module.exports = Controller;
`

从这里可以看到,我们的路由天生就配置比较简单。只需要配置一个prefix,剩下就简单多了。

自定义定时任务

在根目录下新建schedule目录,里面的任何一个文件都必须集成导出的schedule对象,不然会报错的^_^

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 测试定时任务
*/
const Schedule = require("node-duck").Schedule;

class testSchedule extends Schedule {
constructor() {
super();
this.time = "*/1 * * * * *";
}
start() {
console.log("执行一次", Date.now());
}
}
module.exports = testSchedule;

这里使用的时间配置是cron的格式:

1
2
3
4
5
6
7
8
9
*    *    *    *    *    *
┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ │
│ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun)
│ │ │ │ └───── month (1 - 12)
│ │ │ └────────── day of month (1 - 31)
│ │ └─────────────── hour (0 - 23)
│ └──────────────────── minute (0 - 59)
└───────────────────────── second (0 - 59, OPTIONAL)

ex:

  1. 每秒执行一次"*/1 * * * * *"
  2. 每分钟的第一秒执行一次"1 * * * * *"
  3. 第2到5秒执行一次"2-5 * * * * *"
  4. time参数也可以传Date对象

扩展

到这里的时候一个框架的基础架构以及demo展示都已经做完了。如果你还有自己更多的需求,也可以在这个的基础上扩展一下。

  1. 扩展utils,给自己添加更多的工具
  2. 扩展插件,添加数据库操作类等方法
  3. 扩展事件,将application的各种事件扩展出来

总结

写这个框架最初的目的可能就是用不惯其他框架吧。好用的又封装太厉害了。封装不那么厉害的实现又非常的复杂。既然如此,干脆自己写一个好了。反正这玩意也不是一个多难的东西,再说了我还可以自己往上面安装各种奇怪的功能。

我将我自己的想法分享出来,希望有需要的能够从中有一些收获。

请支持我一下吧.