JS 流行框架(四):EggJS

JS 流行框架(四):EggJS

Egg 是阿里巴巴基于 Koa 的有约束和规范的企业级 Web 开发框架,基于 Egg 的项目目录结构和名称有严格的规定,和 ESLint 一样,如果不符合规定那么项目将无法运行,此外,Egg 基于 MVC 的架构模式,M —— Model 层负责应用程序的数据逻辑部分,类似于 Service、V —— View 层负责应用程序的数据显示部分(静态/动态网页),类似于 Router、C —— Controller 层负责应用程序的业务逻辑部分,将数据和页面关联

基本使用

在使用 EggJS 之前,必须先下载 egg 和 egg-bin 模块,前者是 EggJS 的核心模块,后者是用于本地开发调试的模块,示例如下:

npm install egg --save
npm install egg-bin --save-dev

在项目下载完成之后,可以在 package.json 文件中的 script 属性中添加 dev 命令以便于快速启动项目

在基于 EggJS 项目中,目录结构有严格的限制,以下是 Egg 项目中最简单的结构

  • /app
    • /controller
      • home.js
    • router.js
  • /config
    • config.default.js

在项目中,目录 app 专门用于保存项目的核心代码,子目录 controller 用于保存程序业务逻辑相关的代码,home.js 专门用于编写处理主页的业务逻辑代码,router.js 专门用于编写路由相关的代码,目录 config 专门用于保存项目的配置文件,config.default.js 用于保存项目的默认配置,此外,必须注意的是,除了 controller 目录下的文件之外,上述目录和文件的名称不能擅自修改,否则会报错,每个文件的内容如下所示:

  • config.default.js
module.exports = {
  keys: 'org.xin.*?'
}

此处的 keys 即项目用于生成 Cookie 时所用的密钥

  • home.js
const Controller = require('egg').Controller;

class HomeController extends Controller {
  async index() {
    this.ctx.body = 'Hello World!';
  }
}

module.exports = HomeController;

上述示例中,定义了一个继承自 Controller 的 HomeController 类,此类的所有实例方法即用于处理主页相关的业务逻辑,这些方法必须被标记为 async 异步方法,此外,凡是继承 Controller 类的类中的 this 拥有如下属性:

属性含义
this.ctx当前请求的 Context 实例,类似于 Koa 中的 ctx
this.app当前应用的 Application 实例,利用此实例可以获取框架提供的全局实例和方法
this.service应用定义的 Service 实例,利用此实例可以访问应用中 Service 层的方法
this.config应用运行时和配置相关的选项
this.logger应用提供的 logger 实例,用于记录日志,之后再详细说明
  • router.js
module.exports = app => {
  // 从 app 中解构出 router 和 controller
  const {router, controller} = app;
  /**
   * 利用 router 的 get 方法处理根路由(类似于 Koa2 中的 router),在此方法中
   * 第一个参数说明路由
   * 第二个参数说明处理此路由的方法,此方法来自 controller 目录下的 home.js 所暴露的类中的 index 实例方法
   * (即 controller 中保存了 controller 目录下的所有内容,访问 controller 就相当于访问 controller 目录)
   */
  router.get('/', controller.home.index);
}

上述示例中,向外界暴露了一个方法,此方法接收一个名称为 app 的参数,此参数即为当前应用的 Application 实例,此实例中拥有如下属性:

属性含义
env运行环境
name项目名称
baseDir项目目录
controller保存了项目中 controller 目录下的所有内容
loggers用于日志记录
middlewares保存了项目中的所有中间件
router用于路由

参数

EggJS 项目中获取请求参数的方式和 Koa 基本相同,示例如下:

  • /controller/user.js
const Controller = require('egg').Controller;

class UserController extends Controller {
  async getQuery() {
    this.ctx.body = this.ctx.query;
  }

  async getParams() {
    this.ctx.body = this.ctx.params;
  }

  async getBody() {
    this.ctx.body = this.ctx.request.body;
  }
}

module.exports = UserController;
  • router.js
module.exports = app => {
  // 从 app 中解构出 router 和 controller
  const {router, controller} = app;

  // 处理静态 get 请求
  router.get('/user/info', controller.user.getQuery);
  // 处理动态 get 请求
  router.get('/user/register/:username/:password', controller.user.getParams);
  // 处理 post 请求
  router.post('/user/login', controller.user.getBody);
}

默认情况下,上述示例中的 post 请求将失败,原因在于 EggJS 拥有一套安全机制,默认的 post 请求被认为是不安全的,所以必须在 config.default.js 中将此机制忽略,示例如下:

module.exports = {
  keys: 'org.xin.*?',
  security: {
    csrf: {
      ignoreJSON: true // 默认为 false,当设置为 true 时,将会放过所有 content-type 为 `application/json` 的请求
    }
  }
}

资源

静态

在基于 EggJS 的项目中,不用专门处理静态资源,因为 EggJS 已经处理好了,只需要将所有的静态资源存放在 app 目录下的 public 子目录下即可,此处不再详细说明,必须注意的是,目录名称必须为 public,且必须在 app 目录下,在通过浏览器访问静态资源时,必须加上 /public,否则将出现 404 错误,示例如下:

http://127.0.0.1:7001/public/login.html

动态

在基于 EggJS 的项目中,通过插件处理动态资源,在 EggJS 中,插件就是特殊的中间件,专门用于处理那些和请求无关的独立的业务逻辑,要想使用插件处理动态资源,必须先下载此插件,示例如下:

npm install egg-view-ejs --save

在下载完成之后,在 config 目录下新建一个 plugin.js 文件,用以描述插件相关的配置,示例如下:

module.exports = {
  ejs: {
    enable: true,
    package: 'egg-view-ejs'
  }
}

之后在 config.default.js 中新增如下配置:

module.exports = {
  view: {
    mapping: {
      '.html': 'ejs'
    }
  }
}

之后,必须在 app 目录下创建 view 子目录以存放动态资源,资源以 html 为扩展名,然后就可以在 controller 中利用 ctx 的 render 方法渲染动态资源,示例如下:

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

class HomeController extends Controller {
  async index() {
    await this.ctx.render('index', {
      username: 'Reyn Morales',
      password: 1024
    })
  }
}

module.exports = HomeController;

网络数据

在基于 EggJS 的项目中,不论是数据库中的数据还是临时请求网络中的数据,均在 Service 中处理,所以我们必须在 app 目录下新建一个 service 子目录(目录名称不能擅自修改),此目录专门用于保存数据逻辑相关的代码,此处说明如何临时请求网络上的数据,关于数据库相关的内容将在说明如何在 EggJS 中使用 MySQL 和 Sequelize 时说明,示例如下:

  • /service/home.js
const Service = require('egg').Service;

class HomeService extends Service {
  async getNews() {
    const response = await this.ctx.curl('http://127.0.0.1:3000/getNewsByGetWithoutParams');
    return JSON.parse(response.data);
  }
}

module.exports = HomeService;

和 Controller 类似,凡是继承 Service 类的类中的 this 也将拥有和 Controller 类中 this 所拥有的属性,此外还包含一些额外的属性,内容如下:

属性含义
this.ctx.curl发起网络调用
this.ctx.service.otherService调用其它 Service
this.ctx.db发起数据库调用,db 可能是其它插件提前挂载到 app 上的模块

实际上 curl 方法可以发起多种网络调用,示例如下:

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

class HomeService extends Service {
  async getNews() {
    /* 不带参数的 get 请求 */
    // const response = await this.ctx.curl('http://127.0.0.1:3000/getNewsByGetWithoutParams');
    
    /* 带参数的 get 请求 */
    // const response = await this.ctx.curl('http://127.0.0.1:3000/getNewsByGetWithParams?title=SXU');
    
    /* 不带参数的 post 请求 */
    // const response = await this.ctx.curl('http://127.0.0.1:3000/getNewsByPostWithoutParams', {
    //   method: 'post'
    // });
    
    /* 带参数的 post 请求 */
    const response = await this.ctx.curl('http://127.0.0.1:3000/getNewsByPostWithParams', {
      method: 'post',
      data: {
        name: 'Reyn Morales',
        age: 21
      }
    });
    
    return JSON.parse(response.data);
  }
}

module.exports = HomeService;
  • /controller/home.js
const Controller = require('egg').Controller;

class HomeController extends Controller {
  async findNews() {
    this.ctx.body = await this.service.home.getNews();
  }
}

module.exports = HomeController;

类似于 controller,如果通过 this 访问 service 属性,那么就相当于访问 app 目录下的 service 子目录,通过不同的模块名称可以调用不同模块提供的方法,此外,必须注意的是,service 中允许以多级目录的形式存储模块,如果模块是保存在多级目录下的,那么在调用时必须采用链式调用,示例如下:

/* 此处访问 /service/abc/def/temp 模块中的 xxx 方法 */
this.service.abc.def.temp.xxx();

不仅如此,如果 service 中的模块名称中的单词以 _ 连接或采用首字母大写的驼峰命名方式,那么在调用时必须采用驼峰命名,例如,在调用名称为 get_user 或 GetUser 模块的方法时,必须统一写为 getUser

  • router.js
module.exports = app => {
  // 从 app 中解构出 router 和 controller
  const {router, controller} = app;
  
  // 处理网络数据
  router.get('/news', controller.home.findNews);
}

Cookie

在基于 EggJS 的项目中,Cookie 通常在 controller 中添加、获取

添加

示例如下:

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

class HomeController extends Controller {
  async setCookie() {
    this.ctx.cookies.set('name', 'reyn', {
      path: '/',
      maxAge: 24 * 60 * 60 * 1000,
      httpOnly: true,
      signed: true, // 生成签名
      encrypt: true // 加密存储
    });
    this.ctx.body = 'Set Cookie!';
  } 
}

module.exports = HomeController;

在基于 EggJS 的项目中,为了安全着想,阿里的安全专家建议我们在添加 Cookie 时,为保存的数据生成一个签名,将来在获取数据时,再利用获取到的数据生成签名并和当初保存的签名进行对比,如果相同,那么表示保存在客户端的数据没有被篡改,否则说明保存在客户端的数据已经被篡改

获取

示例如下:

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

class HomeController extends Controller {
  async getCookie() {
    const cookie = this.ctx.cookies.get('name', {
      signed: true, // 检查签名
      encrypt: true // 解密读取
    });
    this.ctx.body = `Get Cookie -> ${cookie}`;
  }
}

module.exports = HomeController;

日志

在基于 EggJS 中的项目中,继承自 Controller 和 Service 的类的 this 中有 logger 属性实例专门用于日志记录,示例如下:

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

class HomeController extends Controller {
  async loggerTest() {
    this.logger.debug('Logger -> Debug');
    this.logger.info('Logger -> Info');
    this.logger.warn('Logger -> Warn');
    this.logger.error('Logger -> Error');
    this.ctx.body = 'Logger Success';
  }
}

module.exports = HomeController;

在项目运行时,系统将在项目目录中自动创建 logs 和 run 目录,logs 专门用于存储日志,类型如下:

  • common-error.log
  • egg-agent.log
  • egg-schedule.log
  • egg-web.log
  • project-web.log

基于 EggJS 的项目中有 5 个级别的日志,分别为 None、Debug、Info、Warn、Error,默认情况下仅输出 Info 及以上级别的日志到文件中,如果想要输出 Debug 日志,那么必须修改 config.default.js 配置文件,示例如下:

module.exports = {
  logger: {
    level: 'DEBUG'
  }
}

此外,在 EggJS 中不用我们手动切割日志,默认情况下 EggJS 将自动帮我们切割日志,默认情况下每一天就是一个新的日志文件,之前的日志文件将以其所记录日志的日期为扩展名,例如:common-error.log.2022-12-12

定时任务

即使我们通过框架开发的 Web 服务器是请求响应模型,但是在某些情况下,我们必须在某个特定的时刻或时间段执行任务,例如定时删除临时文件、上报应用状态等,EggJS 提供了一套机制来让定时任务的编写和维护更加优雅,首先我们要在 app 目录下新建一个名称为 schedule 的子目录,在子目录下可以以模块的形式创建定时任务,示例如下:

  • /schedule/updateMessage.js
const Subscription = require('egg').Subscription;

class UpdateCache extends Subscription {
  static get schedule() {
    return {
      interval: '3s', // 定时任务执行的时间间隔
      /**
       * 如果服务器的 CPU 是多核的,那么可以同时执行若干个程序
       * 此处的 all 即表示当前服务器上所有相同的 Node 进程都执行此任务
       * 由于用户每次访问服务器可能由不同的 Node 进程处理
       * 所以将 type 属性设置为 all 可以同步所有进程的信息,以避免每次访问数据时都不相同
       */
      type: 'all'
    }
  }

  async subscribe() {
    const response = await this.ctx.curl('http://127.0.0.1:3000/getMsg');
    console.log(response.data.toString());
  }
}

module.exports = UpdateCache;

上述示例中,通过 schedule 静态属性设置定时任务的执行间隔等配置选项,而 subscribe 实例方法是定时任务执行时被运行的函数

自定义启动项

定时任务总是在间隔一段时间后才会被运行,如果我们希望在程序一启动时就立刻执行定时任务,此时可以自定义启动项,我们必须在项目目录下(不是 app 目录下)创建一个 app.js 文件,在此文件中自定义启动项,示例如下:

  • /Project/app.js
class AppBootHook {
  constructor(app) {
    this.app = app
  }

  // 此方法将在 EggJS 程序启动完毕后执行
  async serverDidReady() {
    // 必须注意的是,此处传递的不是方法名称,而是需要被执行的定时任务所在的模块名称
    await this.app.runSchedule('updateMessage');
  }
}

module.exports = AppBootHook

上述示例中,函数 serverDidReady 是 EggJS 框架提供的生命周期函数,类似于 Vue 中的生命周期函数,在程序从生到死的过程中的某些时刻将自动调用这些生命周期函数,内容如下:

生命周期函数调用时机
configWillLoad配置文件即将加载,这是最后动态修改配置的时机
configDidLoad配置文件加载完成
didLoad文件加载完成
willReady插件启动完毕
didReadyworker 准备就绪
serverDidReady应用启动完成
beforeClose应用即将关闭

生命周期函数通常写在项目目录下的 app.js 文件中

框架扩展

虽然 EggJS 在 Application、Context、Request、Response 实例上提供了很多常用的方法供我们使用,但有时这些方法并不能满足我们的需求,所以 EggJS 框架提供了扩展的功能,通过此功能可以相当方便的为 ctx、request、response、application、helper 实例扩展方法,要想为这些实例扩展方法,必须先在 app 目录下新建一个 extend 目录,在目录中新建如下文件:

  • application.js
  • context.js
  • helper.js
  • request.js
  • response.js

每个文件分别为各自相应的实例扩展方法,示例如下:

  • /extend/application.js
module.exports = {
  extAppMethod(param) {
    // application 中的 this 就是 app 实例,通过此实例可以访问 app 上的其它属性或方法
    return `Call Application ${param}`;
  }
}
  • /extend/context.js
module.exports = {
  extCtxMethod(param) {
    // context 中的 this 就是 ctx 实例,通过此实例可以访问 ctx 上的其它属性或方法
    return `Call Context ${param}`;
  }
}
  • /extend/helper.js
const crypto = require('crypto');

module.exports = {
  md5: (msg) => {
    return crypto.createHash('md5').update(msg).digest('hex');
  }
}

实际上,helper 中专门用于定义一些工具方法

  • /extend/request.js
module.exports = {
  extReqMethod(param) {
    // request 中的 this 就是 request 实例,通过此实例可以访问 request 上的其它属性或方法
    return `Call Request ${param}`;
  }
}
  • /extend/response.js
module.exports = {
  extResMethod(param) {
    // response 中的 this 就是 response 实例,通过此实例可以访问 response 上的其它属性或方法
    return `Call Response ${param}`;
  }
}

如下示例演示了如何使用扩展的方法:

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

class HomeController extends Controller {
  async extendTest() {
    console.log(this.app.extAppMethod('app'));
    console.log(this.ctx.extCtxMethod('ctx'));
    console.log(this.ctx.request.extReqMethod('req'));
    console.log(this.ctx.response.extResMethod('res'));
    console.log(this.ctx.helper.md5('Reyn Morales'));
  }
}

module.exports = HomeController;

中间件

框架 EggJS 本质上是基于 Koa 的,所以 EggJS 的中间件形式和 Koa 的中间件形式相同,不过 EggJS 的中间件必须写在规定的目录中,且 EggJS 为中间件提供了多种使用方式,如果要自定义中间件,必须先在 app 目录下新建一个 middleware 子目录,此目录专门用于编写自定义的中间件,示例如下:

  • /middleware/clientCheck.js
/**
 * 客户端内核检测的中间件
 * @param options   选项说明检测哪个内核,必须以 RegExp 的形式说明,例如 { ua: /Chrome/ }
 * @param app       服务器实例
 * @returns {(function(*, *): Promise<void>)|*}
 */
module.exports = (options, app) => {
  return async (ctx, next) => {
    const userAgent = ctx.get('user-agent');
    if (options.ua.test(userAgent)) {
      ctx.status = 401;
      ctx.body = '不支持当前浏览器';
    }
    next();
  }
}

如下是一个使用自定义中间件的示例:

  • /app/router.js
module.exports = app => {
  // 从 app 中解构出 router 和 controller
  const {router, controller} = app;
  
  // 自定义中间件测试
  const clientCheck = app.middleware.clientCheck({ua: /Chrome/});
  router.get('/middlewareTest', clientCheck, controller.home.index);
}

上述示例中,通过 app 实例的 middleware 属性相当于访问 middleware 目录,通过 clientCheck 中间件实现了当用户访问 /middlewareTest 路由时检测客户端的功能,实际上,通过 config.default.js 可以让 clientCheck 中间件在全局起作用,示例如下:

module.exports = {
  // 必须注意的是,Key 必须是中间件所属模块的名称
  middleware: ['clientCheck'],
  // 以下的实例将成为中间件的 options 参数
  clientCheck: {
    ua: /Chrome/
  }
}

国际化

国际化实际上就是多语言,可以让网页在不同的国家显示不同的语言,也可以让网页支持语言切换,要想让项目实现国际化,必须在 config 目录下新建一个 locale 子目录,对于任意中语言都必须在此目录下新建一个配置文件,示例如下:

  • /locale/en-US.js
module.exports = {
  Email: 'email',
  userName: 'username',
  password: 'password'
}
  • /locale/zh-CN.js
module.exports = {
  Email: '邮箱',
  userName: '用户名',
  password: '密码'
}

以下是一个国际化的示例:

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

class HomeController extends Controller {
  async index() {
    await this.ctx.render('index', {
      username: this.ctx.__('userName'),
      password: this.ctx.__('password')
    })
  }
}

module.exports = HomeController;

MySQL

如果想在 EggJS 项目中使用 MySQL,必须先下载 mysql2 和 egg-mysql 插件,示例如下:

npm install mysql2 egg-mysql --save

之后在 plugin.js 中开启插件,示例如下:

module.exports = {
  mysql: {
    enable: true,
    package: 'egg-mysql'
  }
}

之后在 config.default.js 中配置数据库连接信息,示例如下:

module.exports = {
  mysql: {
    client: {
      host: '127.0.0.1',
      port: '3306',
      user: 'root',
      password: '1234',
      database: 'test'
    },
    app: true,
    agent: false
  }
}

之后就可以在程序中对数据库进行增删改查了,示例如下:

  • /service/user.js
const Service = require('egg').Service;

class UserService extends Service {
  async createUser(user) {
    const result = await this.app.mysql.insert('users', {
      name: user.name,
      age: user.age
    })

    return result.affectedRows === 1;
  }

  async getUser(userId) {
    const result = await this.app.mysql.get('users', {
      id: userId
    })

    return result;
  }
}

module.exports = UserService;

在上述示例中,说明了如何添加和查询记录,至于修改和删除记录此处不再说明,详情可以查询官方文档

  • /controller/user.js
const Controller = require('egg').Controller;

class UserController extends Controller {
  async createUser() {
    const isFailed = await this.service.user.createUser(this.ctx.query);
    if (isFailed) {
      this.ctx.body = 'Create Error'
    }

    this.ctx.body = 'Create Success'
  }

  async getUser() {
    const result = await this.service.user.getUser(this.ctx.query.id);
    this.ctx.body = result;
  }
}

module.exports = UserController;

Sequelize

如果想在 EggJS 项目中使用 Sequelize,必须先下载 mysql2 和 egg-sequelize 插件,示例如下:

npm install mysql2 egg-sequelize --save

之后在 plugin.js 中开启插件,示例如下:

module.exports = {
  sequelize: {
    enable: true,
    package: 'egg-sequelize'
  }
}

之后在 config.default.js 中配置 sequelize 相关信息,示例如下:

module.exports = {
  sequelize: {
    dialect: 'mysql',
    host: '127.0.0.1',
    port: 3306,
    user: 'root',
    password: '1234',
    database: 'test'
  }
}

创建一个数据库以及一张 user 表,之后在 app 目录下新建一个 model 子目录,此目录专门用于存放数据模型,示例如下:

  • /model/user.js
module.exports = app => {
  const {STRING, INTEGER, TINYINT} = app.Sequelize;

  const User = app.model.define('user', {
    id: {type: INTEGER, primaryKey: true, autoIncrement: true, allowNull: false},
    name: {type: STRING(255), allowNull: false},
    age: TINYINT
  }, {
    freezeTableName: true,
    timestamps: false
  });

  return User;
}

数据模型中的属性必须和数据表中的字段相应

之后就可以在程序中对数据库进行增删改查了,示例如下:

  • /controller/user.js
const Controller = require('egg').Controller;

class UserController extends Controller {
  async createUser() {
    const user = await this.ctx.model.User.create(this.ctx.query);
    this.ctx.body = user;
  }

  async getUser() {
    const user = await this.ctx.model.User.findByPk(this.ctx.query.id);
    this.ctx.body = user;
  }
}

module.exports = UserController;

如果通过 Sequelize 操纵数据库,那么就不需要再写 Service 相关的代码了,在上述示例中,说明了如何添加和查询记录,至于修改和删除记录此处不再说明,详情可以查询官方文档

配置文件

实际上,EggJS 框架为了提供了多种配置文件,以方便我们在不同的阶段中使用,内容如下:

配置文件加载时机
config.prod.js只在生产环境下加载
config.test.js只在测试环境下加载
config.local.js只在开发环境下加载
config.default.js所有环境都会加载

也就是说每次最多加载两个配置文件,如果在不同的配置文件中出现了同名的配置选项,那么 default 中的配置将被覆盖,此外,必须注意的是,所有的配置文件都必须写在 config 目录下,示例如下:

  • /config
    • config.default.js
    • config.local.js
    • config.prod.js

以下示例演示了在不同的配置文件中配置数据库连接相关的信息,在开发阶段和上线阶段使用不同的数据库:

  • config.local.js
module.exports = {
  sequelize: {
    dialect: 'mysql',
    host: '127.0.0.1',
    port: 3306,
    user: 'root',
    password: '1234',
    database: 'development_test_db'
  }
}
  • config.prod.js
module.exports = {
  sequelize: {
    dialect: 'mysql',
    host: '127.0.0.1',
    port: 3306,
    user: 'root',
    password: '1234',
    database: 'production_test_db'
  }
}

基于 EggJS 的项目通过 EGG_SERVER_ENV 设置环境模式,示例如下:

  • package.json
{
  "scripts": {
    "dev": "cross-env EGG_SERVER_ENV=dev egg-bin dev",
    "prod": "cross-env EGG_SERVER_ENV=prod egg-scripts start --daemon"
  }
}

egg-init

框架 EggJS 同样提供了脚手架工具以快速构建项目,在使用脚手架工具创建项目之前,必须先下载脚手架工具 egg-init,示例如下:

npm install egg-init -g

在下载完成之后,首先新建一个项目目录,通过 cd 命令切换到此目录之后,利用如下指令初始化 EggJS 项目:

npm init egg --type=simple

选项 type 用于指定初始化 EggJS 项目时所采用的骨架类型,内容如下:

骨架类型说明
simple简单 egg 应用程序骨架
empty空的 egg 应用程序骨架
pluginegg plugin 骨架
frameworkegg framework 骨架

在初始化完成之后,通过 cd 命令切换到项目目录下,执行 npm install 下载相关依赖,之后通过如下指令就可以运行项目了

npm run dev # 开发模式
npm run test # 测试模式
npm run start # 生产模式

CSRF 安全防范

在基于 EggJS 的项目中具有 CSRF 安全防范功能,默认情况下的 Post 请求是无法成功的,必须在请求头中将 Cookie 中的 csrfToken 一起发送至客户端才能通过 csrf 验证,如果是静态网页,那么可以通过 jQuery 在发送请求之前将 csrfToken 写入请求头,示例如下:

$(function() {
  /* 动态获取csrfToken*/
  function getCookie(key) {
    const res = document.cookie.split(';');
    for (let i = 0; i < res.length; i++) {
      const temp = res[i].split('=');
      if (temp[0].trim() === key) {
        return temp[1];
      }
    }
  }
  const csrftoken = getCookie('csrfToken');
  function csrfSafeMethod(method) {
    // these HTTP methods do not require CSRF protection
    return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
  }
  /* 动态设置csrfToken*/
  $.ajaxSetup({
    beforeSend(xhr, settings) {
      if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
        xhr.setRequestHeader('x-csrf-token', csrftoken);
      }
    },
  });
});

如果是动态网页,那么还有更简单的写法,此处不再详细说明,详情可以访问官方文档

数据校验

在基于 EggJS 的项目中,同样可以使用 ajv 利用 JSONSchema 进行数据校验,如果想要使用 ajv,必须先下载 ajv 插件 egg-ajv,示例如下:

npm install egg-ajv --save

之后我们必须在 plugin.js 中开启此插件,示例如下:

'use strict';

/** @type Egg.EggPlugin */
module.exports = {
  ajv: {
    enable: true,
    package: 'egg-ajv',
  },
};

之后我们必须在 app 目录下新建一个 schema 子目录专门用于保存 JSONSchema,示例如下:

  • /schema/user.js
const userValidator = {
  type: 'object',
  properties: {
    username: {
      type: 'string',
      pattern: '^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$',
      maxLength: 255,
      minLength: 3,
    },
    password: {
      type: 'string',
      pattern: '^[A-Za-z0-9]{6,20}$',
      maxLength: 20,
      minLength: 6,
    },
  },
  required: [ 'username', 'password' ],
};

module.exports = userValidator;

之后就可以在 controller 中通过 validate 进行数据校验,示例如下:

  • /controller/user.js
'use strict';

const { Controller } = require('egg');

class HomeController extends Controller {
  async register() {
    const isValidate = await this.ctx.validate('schema.user', this.ctx.request.body);
    if (!isValidate) {
      this.ctx.error(400, this.ctx.helper.errorCode[400]);
    } else {
      try {
        const user = await this.service.user.createUser(this.ctx.request.body);
        this.ctx.success(user);
      } catch (e) {
        this.ctx.error(-1, e.message);
      }
    }
  }
}

module.exports = HomeController;

Session

在基于 EggJS 的项目中,可以通过 egg-session-redis 和 egg-redis 插件实现将 Session 存储到 redis 中的功能,在使用之前必须先下载它们,示例如下:

npm install egg-redis egg-session-redis --save

之后在 plugin.js 中开启它们,示例如下:

'use strict';

/** @type Egg.EggPlugin */
module.exports = {
  redis: {
    enable: true,
    package: 'egg-redis',
  },
  sessionRedis: {
    enable: true,
    package: 'egg-session-redis',
  },
};

之后在 config.default.js 中配置 redis 的连接信息,示例如下:

/* eslint valid-jsdoc: "off" */

'use strict';

/**
 * @param {Egg.EggAppInfo} appInfo app info
 */
module.exports = appInfo => {
  /**
   * built-in config
   * @type {Egg.EggAppConfig}
   **/
  const config = exports = {};
  
  config.redis = {
    client: {
      port: 6379, // Redis port
      host: '127.0.0.1', // Redis host
      password: '',
      db: 0,
    },
  };

  return {
    ...config,
    ...userConfig,
  };
};

之后就可以在 controller 中添加 session 并将数据存储到 redis 中了,示例如下:

'use strict';

const { Controller } = require('egg');

class HomeController extends Controller {
  async login() {
    try {
      const user = await this.service.user.getUser(this.ctx.request.body);
      this.ctx.session.user = user;
      this.ctx.success(user);
    } catch (e) {
      this.ctx.error(-1, e.message);
    }
  }
}

module.exports = HomeController;
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值