7

前言

这段时间,用Eggjs作为后端服务框架开发了几个项目。项目都很小,但为了进一步了解Eggjs,特意选择了Eggjs作为框架基础开发后端服务。期间也遇到过一些问题和坑,还有几个值得注意的点,下面来讲一下我这段时间开发的总结。


Egg.js 为企业级框架和应用而生 ,我们希望由 Egg.js 孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本。

这个是Eggjs文档Eggjs的解释,关于Eggjs的详细介绍和使用请点解前面的地址;相对于Egg.js 1.x版本的文档,已经有很大的改进了,很多关键的地方都可以比较完整讲解和带有代表性的实例。

步骤

开始

用的Egg.js版本是2.2.1,对环境有一定的要求,本人用的配置如下:

  • 操作系统:macOS
  • 运行环境:v9.8.0

使用脚手架快速创建项目:

$ npm i egg-init -g
$ egg-init egg-example --type=simple
$ cd egg-example
$ npm i

项目安装完毕,启动项目:

$ npm run dev
$ open localhost:7001

至此,项目顺利建立及启动完毕。

项目结构:(摘自文档)

egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app
|   ├── router.js
│   ├── controller
│   |   └── home.js
│   ├── service (可选)
│   |   └── user.js
│   ├── middleware (可选)
│   |   └── response_time.js
│   ├── schedule (可选)
│   |   └── my_task.js
│   ├── public (可选)
│   |   └── reset.css
│   ├── view (可选)
│   |   └── home.tpl
│   └── extend (可选)
│       ├── helper.js (可选)
│       ├── request.js (可选)
│       ├── response.js (可选)
│       ├── context.js (可选)
│       ├── application.js (可选)
│       └── agent.js (可选)
├── config
|   ├── plugin.js
|   ├── config.default.js
│   ├── config.prod.js
|   ├── config.test.js (可选)
|   ├── config.local.js (可选)
|   └── config.unittest.js (可选)
└── test
    ├── middleware
    |   └── response_time.test.js
    └── controller
        └── home.test.js

上述目录也是一个给开发者一个目录创建的指南,但按照文档建立的项目目录结构没有那么全,基本上标注为“可选”的都是初始没有的,在/config目录里也只有plugin.jsconfig.default.js两个文件,其他文件要自己根据需求创建。

建立控制器Controller

初始项目里会有一个示例Controller,在创建一个新的Controller可以参考/app/controller/home.js的示例,一般而言,推荐使用module.exports暴露出一个类或者参数为app返回一个类的函数(文档示例中为箭头函数,其他方式没试过不清楚),类里面包含着这块业务的一些操作,下面在控制器文件目录/app/controller/里新建一个文件名为user.js的控制器文件:

// 继承egg的控制器
const Controller = require('egg').Controller;
class UserController extends Controller {
  async index() {
    const { ctx } = this;
    const { name } = ctx.request.body;
    ctx.body = `hi, ${name}`;
  }
  async getUserById() {
     const { userId } = this.ctx.request.body;
     // 使用业务函数查询用户信息
     const userInfo = await this.service.user.findById(userId);
     this.ctx.body = {
         msgCode: 0,
         message: '成功',
         data: userInfo
     };
  }
}
// 注意:一定要将控制器暴露出去,否则请求的时候会报找不到该controller的错误;
module.exports = UserController;

添加路由

路由代码在/app目录之下,文件名router.js,添加路由的代码如下:

// 参数app为全局应用的对象
module.exports = app => {
    const { router, controller, middleware } = app;
    // 在这里controller相当于app下的controller文件目录,user为user.js,index为控制器类的index方法
    router.get('/', controller.user.index); 
};

编写业务

通常,controller主要处理数据的结构和处理返回的结果,具体的涉及的业务由service业务类方法完成,编写service,在目录/app/service/下建立user.js文件,并编写代码:

// 同样要继承egg的Service类
const Service = require('egg').Service;
class UserService extends Service {
    // 根据用户id查找用户
    async findUserById(id) {
        const mysql = this.app.mysql;
        const result = await mysql.get('users', { id });
        return result;
    }
}
module.exports = UserService;

添加插件

eggjs simple 版本旨在根据业务需求添加eggjs的插件来搭建上层框架。在本人开发过程中,用到的一些插件做简要说明。

插件安装:

$ npm i --save egg-pluginName

在文件/config/plugin.js添加配置:

exports.pluginName = {
  enable: true,
  package: 'egg-pluginName',
};

需要插件初始化配置的情况下,修改/config/config.default.js

config.pluginName = {
    // 配置项
};

egg-mysql

因使用mysql数据库,需要一个nodejsmysql的操作库,基于eggjs选择了egg-mysql ,操作文档点击这里。基本的数据库增删查改都能操作,写起来还挺方便,但是有个需求,编写某个接口,返回当前用户某一段时间的数据,这就比较蛋疼了,找了很久,就连egg-mysql封装的库ali-rds查看了源码也找不到这类方法,无奈之下,只能通过组织原生的mysql查询语句去动态拼凑,虽然不推荐,不过如果找到更好的方法,还是愿意改写的。

查询指定日期数据的mysql相关参考资料:

egg-redis

redis相当于基于内存的一个微型数据库,其存取速度非常快,代码执行的时候几乎感觉不到阻塞,这里使用egg-redis作为项目对redis的操作库,文档点击这里。文档说明解析了基本的存取和设置操作,对于较为复杂的操作只能通过查看redis官方文档,对应的命令小写即为方法名:
redis命令DECR,对应的方法使用:
// /app/controller/user.js
const value = await this.app.redis.decr(keyName);

扩展内置对象

添加过几个插件之后,发现其中的源代码都是以扩展内置对象的方式去挂载相关的库或者插件的。

ctx

文档原文:“一般来说属性的计算在同一次请求中只需要进行一次,那么一定要实现缓存,否则在同一次请求中多次访问属性时会计算多次,这样会降低应用性能。

推荐的方式是使用 Symbol + Getter 的模式。”

const jwt = require('jsonwebtoken');
const JWT = Symbol('Context#jwt');
module.exports = {
  get jwt() {
    if (!this[JWT]) {
      this[JWT] = jwt;
    }
    return this[JWT];
  }
};

第一次扩展cxt对象的时候,不明白为何要使用Symbol + Getter的模式,后来基于这个问题,查找资料,发现这种方式更能避免和其他属性名发生冲突,上述代码中,ctxjwt定义为只读方式。在方便维护的同时,生成一个带有命名空间(Context#jwt)字符串描述的Symbol实例数据, 作为ctx的属性,通过只读属性jwt来获取内部的JWT属性。
PS:

ctx.JWT == ctx[JWT] // false

关于Symbol的介绍和使用请参考阮一峰ES6

app

在扩展app对象的时候,遇到个问题,就是如果需要获取ctx怎么办?
查找文档,找到了在扩展app对象时,只需要在函数体里添加一句代码:

const ctx = app.createAnonymousContext();

就可以获取ctx对象,这对于使用其他函数提供了一道桥梁。

编写中间件

eggjs的中间件处理流程遵循koa的洋葱式请求模型
中间件的写法:
module.exports = options => {
    return async function middleWareFunctionName (ctx, next) {
        // 控制器之前业务处理代码
        // ...
        await next();
        //控制器之后业务处理代码
        // ...
    }
}

中间件以返回一个处理业务的函数为主体,函数接收两个参数:ctxnextctx则是请求级别的对象,next()方法可以让请求进入下一个步骤。特别注意的是:在一个控制器中,有对请求到达下一步之前做一些操作的,可以控制next()在代码流程中的位置,其后也可以处理请求之后的操作。

制定定时任务

eggjs写定时任务也是非常简单的,关注于业务代码,加以简单的配置,即可使用定时任务。
下面是一个简单的定时统计业务数据的定时任务:

const Subscription = require('egg').Subscription;

class Statistics extends Subscription {
  // 通过 schedule 属性来设置定时任务的执行间隔等配置
  static get schedule() {
    return {
      cron: '00 59 23 * * *', // 秒 分 时 日 月 年
      // interval: '10s', // 设置时间间隔触发,单位s为秒,ms为毫秒
      type: 'worker', // all 指定所有的 worker 都需要执行, worker 为某一个 worker 执行
    };
  }

  // subscribe 是真正定时任务执行时被运行的函数
  async subscribe() {
   // 定时任务业务代码
   // ...
  }
}

module.exports = Statistics;

在程序启动的时候,就会在配置的指定时机执行相关的业务代码。

配置(待补充)

csrf的讨论

eggjsv2.x版本之后默认开启了csrf插件,已确保基于cookie存储验证信息的网站信息安全。

csrf能将请求限制在同源网站,即只有拥有“专有令牌”的网站发送请求才会正确响应。此处容易与jwt的作用混淆,可以看看这篇文章

跨域

使用egg-cors;

前后端分离用户验证

使用jwt验证

jwt则在认证方式上跟csrf上有所不同,jwt可以在不使用cookie的情况下,以token的方式在前后端交互数据的body里传输,也可以在header里设置相关信息,详细可以参考这篇文章

日志(待补充)

类的写法

远程机开发部署

文档中,有《应用部署》一文,里面介绍的很详细的。使用egg-script插件启动生产环境中的应用程序。项目生产静默部署,启动使用npm start,停止使用npm stop另,在开发环境里想要使用pm2管理进程后台启动,--watch会不断打印控制台日志,原因不清楚。

生产环境部署

启动命令:

$ npm install --production
$ npm start

停止命令:

$ npm stop

总结

优点

使用eggjs开发企业级应用还是相当方便的,虽然说要根据需求装,但安装和配置步骤非常简单,很多有用的业务配置都能够很方便快速配置好,还可以区分环境,项目结构和调用方式很合理。

不足

工具函数的访问需要自己手动添加扩展

没有写测试,希望下次补上。

Juven
127 声望4 粉丝