有关Egg.js(以下简称:egg)的基本介绍这里就不再赘述了,官网上写的很清楚,下面直接对egg的使用及原理进行叙述。
首先,egg实际上是继承于koa的,egg在koa的基础上制定了很多约定和规范,扩展性也很高。但是有的地方必须遵循这些约定和规范,比如目录结构:
既然是基于koa那肯定支持koa的内置对象,包括:Application, Context, Request, Response;同时,扩展的对象有:Controller, Service, Helper, Config, Logger。
有关如何使用和获取这些对象在下面会写到。
上面大概说了一下目录结构以及egg框架的一些对象,下面通过实战来进一步了解egg。
既然是Node框架,这里就实现一支服务来举栗子。
在根目录映入眼帘的是一个app.js,首先它并不是像大多数前端框架中的一个入口文件,在egg中这个app.js是用来做一些初始化的工作,甚至可以没有这个app.js文件,所以它是可选的。
那这个app.js我们可以来做一些什么事呢?上面说的Application
是全局应用对象,在一个应用中,只会实例化一个,贯穿整个应用。我们可以在启动时做一些自定义的动作,比如监听以及在Application
上挂载一下属性或方法,就像这样:
// app.js
module.exports = app => {
// 自定义内容
app.projectName = 'eggManual'
app.beforeStart(async () => {
// 应用会等待这个函数执行完成才启动
console.log("==app beforeStart==");
});
app.ready(async () => {
console.log("==app ready==");
})
app.beforeClose(async () => {
console.log("==app beforeClose==");
})
};
这里的app就是Application对象,会在其他位置以参数或者this获取到。
上面我们说要写一个服务,那请求的URL如何定义?答案就是在router.js
中。它里面定义了我们的路由规则,所有的请求都会通过这个路由规则去找对应的Controller
,这样也可以做到统一管控。
接着我们定义一个服务请求的URL:
// router.js
module.exports = app => {
const { router, controller } = app;
// RESTful风格
router.resources("main", "/api/main", controller.main);
}
有关RESTful风格可以自行google或者百度。
这里说一下定义URL的规则,我们可以直接定义某种请求类型重新编写上面的router.js
,就像这样:
// router.js
module.exports = app => {
const { router, controller } = app;
// RESTful风格
router.get("/api/main/:id", controller.main.show);
}
这里说明一下router.resources('posts', '/api/posts', controller.posts)
好处相当于定义了一组RESTful的路由,这里放一张官网表格:
Method | Path | Route Name | Controller.Action |
---|---|---|---|
GET | /posts | posts | app.controllers.posts.index |
GET | /posts/new | new_post | app.controllers.posts.new |
GET | /posts/:id | post | app.controllers.posts.show |
GET | /posts/:id/edit | edit_post | app.controllers.posts.edit |
POST | /posts | posts | app.controllers.posts.create |
PUT | /posts/:id | post | app.controllers.posts.update |
DELETE | /posts/:id | post | app.controllers.posts.destroy |
所以上面的一句话相当于定义了下面七个:
router.get("/api/main", controller.main.index);
router.get("/api/main/new", controller.main.new);
router.get("/api/main/:id", controller.main.show);
router.get("/api/main/:id/edit", controller.main.edit);
router.post("/api/main", controller.main.create);
router.put("/api/main/:id", controller.main.update);
router.delete("/api/main/:id", controller.main.destroy);
上面对应的控制器就是controller目录下的main.js,后面为main.js中定义的方法名。
上面定义的GET请求的含义是,请求路径为:http://ip:port/api/main,调用的是controller目录下main.js中的show方法,其他以此类推。
首先发送请求会调用控制器中的方法,控制器中主要工作是接受用户的参数并进行处理,然后将处理好的参数发送给Service
层,然后把Service
的结果返回给用户。
其中对参数的处理包括但不仅限于参数校验和参数拼装,当然也可以直接返回不走Service
,都在Controller
层做处理,但是我们不建议这样做。
controller目录是可以支持多级的,比如controller目录下有个v1的文件夹,v1文件夹下有个main.js文件,那么路由配置里面就可以写”controller.v1.main.show”,以此类推。
接下来编写我们控制器中的方法:
// controller/main.js
const Controller = require('egg').Controller;
class MainController extends Controller {
async show() {
let { id } = this.ctx.params; // 获取路由参数
let { name } = this.ctx.query; // 获取用户入参
let options = {
id: id,
name: name
}
// 调用service方法
let info = await this.ctx.service.main.getInfo(options);
// 拼装响应体
this.ctx.body = {
code: 0,
data: info
};
this.ctx.status = 200;
}
}
module.exports = MainController;
GET
请求获取用户参数实际上省略了request
,完整的代码是:this.ctx.request.query
;其他请求是不能省略request
的,获取参数代码为:this.ctx.request.body
。ctx
即为Context
上下文,上面挂载着request
和app
等对象。
Service
层拿到Controller
层的数据之后,根据条件执行对数据库或者其他操作,最终将结果返回,一个请求的简单流程就算是完成了,下面编写service目录下的main.js文件:
const Service = require('egg').Service;
class MainService extends Service {
async getInfo(options) {
// 获取完整用户信息
options.height = 180;
options.age = 20;
return options;
}
}
module.exports = MainService;
这里就不去查数据库了,直接将用户信息返回,可能会在后续的文章中添加数据库操作。
这里使用postman来做测试,虽然egg自带HttpClient,但还是觉得这样更直观一点,看一下请求结果:
说到这里,一个基于egg的请求已经编写完成了,但是这只是最简单的,我们还有很多egg提供的东西没有使用,记下来对其他功能就行展示。
egg提供5种配置文件:
- config.default.js:默认配置文件;
- config.local.js:开发环境下的配置,与默认配置合并,同名则覆盖默认配置;
- config.prod.js:生产环境下的配置,与默认配置合并,同名则覆盖默认配置;
- config.test.js:测试环境下的配置,与默认配置合并,同名则覆盖默认配置;
- config.unittest.js:单元测试环境下的配置,与默认配置合并,同名则覆盖默认配置;
配置文件的写法为(下面会有一些具体的配置):
module.exports = {
key: value
}
// 或
exports.key = value;
egg和koa的中间价一样,都是基于洋葱圈模型,下面再说,先来一张官网图:
接着我们编写两个测试中间件middlewareOne
和middlewareTwo
:
// middleware/middlewareOne.js
module.exports = (options, app) => {
return async function middlewareOne(ctx, next) {
console.log("==request one==");
await next();
console.log("==response one==");
}
};
// middleware/middlewareTwo.js
module.exports = (options, app) => {
return async function middlewareTwo(ctx, next) {
console.log("==request two==");
await next();
console.log("==response two==");
}
};
- options:中间件的参数,可在
Config
中配置;app
和ctx
上面也说过了,有这两个对象在中间件中可以搞好多事,比如可以拿到ctx.request
对参数进行修改等;
next
为一个函数,下面会说作用。
写完中间件之后是需要在刚才说的config.default.js
中进行配置的:
// config/config.default.js
exports.middleware = ['middlewareOne', 'middlewareTwo']; // 数组的顺序为中间件执行的顺序
exports.middlewareOne = { // 中间件参数(即options)
msg: "extra message"
}
洋葱圈模型就是说请求和响应都会走一遍中间件,通过next()
方法分割,请求时执行到next()
方法就跳到下一个中间价,看了这个执行顺序图应该对洋葱圈模型有了清晰的理解,如图:
框架的很多对象都是支持扩展的,我们可以在很多对象上扩展自定义的属性和方法,可扩展的对象有:Application
、Context
、helper
、Request
和Response
。
编写扩展的方法就是创建对应的文件,之后会与原对象进行合并对象的prototype
上,以此实现了扩展的功能,在extend文件夹下创建扩展文件:
为什么application会有三个扩展文件呢?看到这个命名应该就能猜到了,实际上egg也支持按照环境扩展,所以我们可以在特定的环境下扩展需要的功能。
刚开始的时候也有疑惑,这个插件是干嘛的?egg提供了中间件的功能,还可以写扩展,对这个插件机制感觉作用不大。
但是我换了一个角度,因为想到了一句话:“存在即合理“,egg这样做一定有他的道理,所以进一步去了解这个插件的作用,官网这里自己说明了一下中间件可能存在的问题。
插件可以抽离出来,虽然说不能独立运行,但是可以作为一种包的形式存在,也可以放在npm上,比如一下错误处理插件、模版引擎插件、数据库相关插件和安全相关插件等等。这些东西都是一个一个小的包,每个egg工程都可以复用,他们之间互相没有耦合,也使得开发效率提升,并且提供了定制上层框架的能力。
插件名叫做transform-int
,必须要有一个package.json:
// lib/plugin/transform-int/package.json
{
"eggPlugin": {
"name": "tranInt"
}
}
application.js
中非常简单:
// lib/plugin/transform-int/app/extend/application.js
module.exports = {
tranInt(arg) {
return parseInt(arg);
}
}
其实插件相当于一个小型的应用,只不过缺少部分文件夹,所以才需要依赖于工程使用,使用的时候在plugin.js
文件中进行配置:
// config/plugin.js
const path = require('path');
exports.tranInt = {
enable: true,
path: path.join(__dirname, '../lib/plugin/transform-int')
}
因为是在Application
上的扩展,所以使用app.tranInt
即可调用插件中的方法。
算是一个egg的小笔记吧,本来还想多几个栗子,但是感觉篇幅有点长了,有时间更新到github上直接看代码就行了。