Egg.js小记

写在前面

有关Egg.js(以下简称:egg)的基本介绍这里就不再赘述了,官网上写的很清楚,下面直接对egg的使用及原理进行叙述。

一、约定

首先,egg实际上是继承于koa的,egg在koa的基础上制定了很多约定和规范,扩展性也很高。但是有的地方必须遵循这些约定和规范,比如目录结构:
目录结构

二、内置对象

既然是基于koa那肯定支持koa的内置对象,包括:Application, Context, Request, Response;同时,扩展的对象有:Controller, Service, Helper, Config, Logger。

有关如何使用和获取这些对象在下面会写到。

三、实战

上面大概说了一下目录结构以及egg框架的一些对象,下面通过实战来进一步了解egg。

既然是Node框架,这里就实现一支服务来举栗子。

1. app.js

在根目录映入眼帘的是一个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获取到。

2. router.js

上面我们说要写一个服务,那请求的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的路由,这里放一张官网表格:

MethodPathRoute NameController.Action
GET/postspostsapp.controllers.posts.index
GET/posts/newnew_postapp.controllers.posts.new
GET/posts/:idpostapp.controllers.posts.show
GET/posts/:id/editedit_postapp.controllers.posts.edit
POST/postspostsapp.controllers.posts.create
PUT/posts/:idpostapp.controllers.posts.update
DELETE/posts/:idpostapp.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方法,其他以此类推。

3. 控制器(Controller)

首先发送请求会调用控制器中的方法,控制器中主要工作是接受用户的参数并进行处理,然后将处理好的参数发送给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上下文,上面挂载着requestapp等对象。
4. 服务(Service)

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提供的东西没有使用,记下来对其他功能就行展示。

5. 配置(Config)

egg提供5种配置文件:
- config.default.js:默认配置文件;
- config.local.js:开发环境下的配置,与默认配置合并,同名则覆盖默认配置;
- config.prod.js:生产环境下的配置,与默认配置合并,同名则覆盖默认配置;
- config.test.js:测试环境下的配置,与默认配置合并,同名则覆盖默认配置;
- config.unittest.js:单元测试环境下的配置,与默认配置合并,同名则覆盖默认配置;

配置文件的写法为(下面会有一些具体的配置):

module.exports = {
    key: value
}
// 或
exports.key = value;
6. 中间件(Middleware)

egg和koa的中间价一样,都是基于洋葱圈模型,下面再说,先来一张官网图:
洋葱圈模型图

接着我们编写两个测试中间件middlewareOnemiddlewareTwo

// 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中配置;
  • appctx上面也说过了,有这两个对象在中间件中可以搞好多事,比如可以拿到ctx.request对参数进行修改等;
    next为一个函数,下面会说作用。

写完中间件之后是需要在刚才说的config.default.js中进行配置的:

// config/config.default.js
exports.middleware = ['middlewareOne', 'middlewareTwo']; // 数组的顺序为中间件执行的顺序
exports.middlewareOne = { // 中间件参数(即options)
    msg: "extra message"
}

洋葱圈模型就是说请求和响应都会走一遍中间件,通过next()方法分割,请求时执行到next()方法就跳到下一个中间价,看了这个执行顺序图应该对洋葱圈模型有了清晰的理解,如图:
中间件顺序

7. 扩展(Extend)

框架的很多对象都是支持扩展的,我们可以在很多对象上扩展自定义的属性和方法,可扩展的对象有:ApplicationContexthelperRequestResponse
编写扩展的方法就是创建对应的文件,之后会与原对象进行合并对象的prototype上,以此实现了扩展的功能,在extend文件夹下创建扩展文件:
extend

为什么application会有三个扩展文件呢?看到这个命名应该就能猜到了,实际上egg也支持按照环境扩展,所以我们可以在特定的环境下扩展需要的功能。

8. 插件(Plugin)

刚开始的时候也有疑惑,这个插件是干嘛的?egg提供了中间件的功能,还可以写扩展,对这个插件机制感觉作用不大。
但是我换了一个角度,因为想到了一句话:“存在即合理“,egg这样做一定有他的道理,所以进一步去了解这个插件的作用,官网这里自己说明了一下中间件可能存在的问题。
插件可以抽离出来,虽然说不能独立运行,但是可以作为一种包的形式存在,也可以放在npm上,比如一下错误处理插件、模版引擎插件、数据库相关插件和安全相关插件等等。这些东西都是一个一个小的包,每个egg工程都可以复用,他们之间互相没有耦合,也使得开发效率提升,并且提供了定制上层框架的能力。

举一个栗子,一个简单的目录结构:
plugin

插件名叫做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上直接看代码就行了。

  • 5
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值