5000字Egg.js快速入门到基础到进阶!!!

一、Egg.js的行业背景

1.1 Egg.js是什么

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

2.2 与社区框架有哪些差异

Express 是 Node.js 社区广泛使用的框架,简单且扩展性强,非常适合做个人项目。但框架本身缺少约定,标准的 MVC 模型会有各种千奇百怪的写法。Egg 按照约定进行开发,奉行『约定优于配置』,团队协作成本低。

Sails 是和 Egg 一样奉行『约定优于配置』的框架,扩展性也非常好。但是相比 Egg,Sails 支持 Blueprint REST API、WaterLine 这样可扩展的 ORM、前端集成、WebSocket 等,但这些功能都是由 Sails 提供的。而 Egg 不直接提供功能,只是集成各种功能插件,比如实现 egg-blueprint,egg-waterline 等这样的插件,再使用 sails-egg 框架整合这些插件就可以替代 Sails 了。

2.3 Egg和Koa之间的关系

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。采用的中间件是洋葱模型:

image.png

2.3.1 Egg 继承于 Koa

Koa 是一个非常优秀的框架,然而对于企业级应用来说,它还比较基础。 Egg 选择了 Koa 作为其基础框架,在它的模型基础上,进一步对它进行了一些增强。扩展和插件更为完善和便捷。

下面就一起快速学习Egg.js吧!

项目git地址: github.com/liujun8892/…

二、初识Egg.js

2.1 Egg.js的安装、配置、项目基本结构

2.1.1 安装

$ mkdir egg-example && cd egg-example
$ npm init egg --type=simple
$ npm i
$ npm run dev
$ open http://localhost:7001
  • 一个Egg项目就已经初始化好啦,可以跑起来了:

image.png

2.1.2 项目目录结构

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

2.1.3 内置的ctx对象

  • ctx这个对象非常重要,请求来的参数,返回去的消息都需要通过ctx这个对象获取或者设置
  • ctx是继承koa的context对象通过设置ctx.body, 可以改变返回信息

image.png

image.png

2.1.4. 尝试新写一个路由

在controller中

 async list() {
    const { ctx } = this;
    ctx.body = {
      code: 200,
      data: [
        {
          id: '1',
          name: '张三',
        },
        {
          id: '2',
          name: '李四',
        },
        {
          id: '3',
          name: '王五',
        },
      ],
      msg: '操作成功',
    };
  }

在router.js中匹配新写的路由

router.get('/list', controller.home.list);

效果:

image.png

  • 一个新的路由就已经创建好啦

2.2 Egg.js的控制层、params和query传参、返回状态码设置

工欲善其事,必先利其器。为了使用egg的快捷键和代码提示,推荐使用vscode的egg插件。

image.png

2.2.1 控制层controller

image.png

配置新controller的路由

// 用户controller
  router.get('/user/info', controller.user.info);

image.png

2.2.2 路由params传参方式

在router中通过 :id,接受参数

router.get('/user/findById/:id', controller.user.info);

在controller中,通过params拿参数

  async findById() {
    const { ctx, params } = this;
    const userId = params;
    const userlist = [
      {
        id: '1',
        name: '张三',
      },
      {
        id: '2',
        name: '李四',
      },
      {
        id: '3',
        name: '王五',
      },
    ];
    const result = userlist.find(v => v.id === userId);
    ctx.body = {
      code: 200,
      data: result,
      msg: '操作成功',
    };
  }

image.png

2.2.3 query的形式传参

  async findById2() {
    const { ctx } = this;
    const userId = ctx.query.id;
    const userlist = [
      {
        id: '1',
        name: '张三',
      },
      {
        id: '2',
        name: '李四',
      },
      {
        id: '3',
        name: '王五',
      },
    ];
    const result = userlist.find(v => v.id === userId);
    ctx.body = {
      code: 200,
      data: result,
      msg: '操作成功',
    };
  }

image.png

2.2.4 修改返回的状态码

image.png

image.png

2.3、post请求

  • 实际业务中,为了接口请求的安全性,和多数据请求参数,多类型请求方式,附件,图片上传等多采用post请求。
  • 下面就介绍如何在Egg中使用post请求

2.3.1 配置csrf跨域相关配置

安装跨域插件

npm i egg-cors --save

配置config下的plugin.js

'use strict';

/** @type Egg.EggPlugin */
module.exports = {
  // had enabled by egg
  // static: {
  //   enable: true,
  // }
  cors: {
    enable: true,
    package: 'egg-cors',
  },

};

配置config下的config.default.js

// 关闭crsf,开启跨域
  config.security = {
    csrf: {
      enable: false,
    },
    domainWhiteList: [ ],
  };
  // 允许跨域方法
  config.cors = {
    origin: '*',
    allowMethods: 'GET, PUT,  POST, DELETE, PATCH',
  };

测试,创建一个post请求

async createUser() {
    const { ctx } = this;
    console.log(ctx.request.body, 'bodyield...1');
    const result = {
      username: ctx.request.body.username,
      age: ctx.request.body.age,
    };
    ctx.body = {
      code: 200,
      data: result,
      msg: '操作成功',
    };
  }

image.png

  • 至此,post请求的相关环境就已经准备好了

2.4 egg.js 的路由分组

  • 当们写很多controller,需要配置的路由很多,这时在一个文件中写所有的路由文件,这会使路由文件变得十分冗余,不易阅读。这时候需要我们把路由分模块划分,在总路由中再去引入。

2.4.1 路由分组

在新建一个router目录,按模块抽离路由

image.png 让后在router.js中通过require去引入

image.png

三、再识Egg.js

3.1 配置MySQL数据库

  • 之前的文章都是静态的数据,要完成持久化的真实数据,就需要用到数据库,这里我们使用的是工作中使用最频繁的数据库MySQL,通过 egg-sequelize插件,就可以像操作一些对象属性一样,完成业务的增删改查,语义化的操作,而不用手动的去写SQL。

3.2.数据库迁移

3.2.1 前期准备

安装并配置egg-sequelize插件(它会辅助我们将定义好的 Model 对象加载到 app 和 ctx 上)和mysql2模块:

npm install --save egg-sequelize mysql2

3.2.2. 在config/plugin.js中引入 egg-sequelize 插件

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

3.2.3. 在config/config.default.js

config.sequelize = {
    dialect:  'mysql',
    host:  '127.0.0.1',
    username: 'root',
    password:  'root',
    port:  3306,
    database:  'eggapi',
    // 中国时区
    timezone:  '+08:00',
    define: {
        // 取消数据表名复数
        freezeTableName: true,
        // 自动写入时间戳 created_at updated_at
        timestamps: true,
        // 字段生成软删除时间戳 deleted_at
        paranoid: true,
        createdAt: 'created_at',
        updatedAt: 'updated_at',
        deletedAt: 'deleted_at',
        // 所有驼峰命名格式化
        underscored: true
    }
};

3.2.4. sequelize 提供了sequelize-cli工具来实现Migrations,我们也可以在 egg 项目中引入 sequelize-cli。

npm install --save-dev sequelize-cli

3.2.5. egg 项目中,我们希望将所有数据库 Migrations 相关的内容都放在database目录下,所以我们在项目根目录下新建一个.sequelizerc配置文件:

'use strict';
​
const path = require('path');
​
module.exports = {
  config: path.join(__dirname, 'database/config.json'),
  'migrations-path': path.join(__dirname, 'database/migrations'),
  'seeders-path': path.join(__dirname, 'database/seeders'),
  'models-path': path.join(__dirname, 'app/model'),
};

3.2.6. 初始化 Migrations 配置文件和目录

ps: 注意准备php集成环境,windows推荐wamp,mac推荐mamp

image.png

image.png windows: 集成环境下载:upupw.net

npx sequelize init:config
npx sequelize init:migrations
// npx sequelize init:models

3.2.7. 行完后会生成database/config.json文件和database/migrations目录,我们修改一下database/config.json中的内容,将其改成我们项目中使用的数据库配置:

{
  "development": {
    "username": "root",
    "password": "root",
    "port": "8889"
    "database": "eggapi",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "timezone": "+08:00"
  }
}

3.2.8. 创建数据库

npx sequelize db:create

数据库创建成功

截屏2021-11-20 下午1.26.08.png

3.3 创建数据表、回滚数据

3.3.1 创建数据迁移表

npx sequelize migration:generate --name=init-user

1.执行完命令后,会在database / migrations / 目录下生成数据表迁移文件,然后定义

'use strict';
​
module.exports = {
    up: async (queryInterface, Sequelize) => {
        const { INTEGER, STRING, DATE, ENUM } = Sequelize;
        // 创建表
        await queryInterface.createTable('user', {
            id: { type: INTEGER(20).UNSIGNED, primaryKey: true, autoIncrement: true },
            username: { type: STRING(30), allowNull: false, defaultValue: '', comment: '用户名称', unique: true},
            password: { type: STRING(200), allowNull: false, defaultValue: '' },
            avatar_url: { type: STRING(200), allowNull: true, defaultValue: '' },
            sex: { type: ENUM, values: ['男','女','保密'], allowNull: true, defaultValue: '男', comment: '用户性别'},
            created_at: DATE,
            updated_at: DATE
        });
    },
​
    down: async queryInterface => {
        await queryInterface.dropTable('user')
    }
};

3.3.2 执行 migrate 进行数据库变更

# 升级数据库
npx sequelize db:migrate
# 如果有问题需要回滚,可以通过 `db:migrate:undo` 回退一个变更
# npx sequelize db:migrate:undo
# 可以通过 `db:migrate:undo:all` 回退到初始状态
# npx sequelize db:migrate:undo:all

image.png

四、Egg.js实操

  • 上面的三大步,已经完成了从controller到数据,再返回的基本架子了,平常在工作中,用到Egg.js的就是增、删、改、查。下面我们对每一部分,详细的实操:

4.1 新增相关接口和栗子🌰

4.1.1 新增一个用户

// app/controller/user.js
 async createUser() {
    const { ctx } = this;
    const { username, password } = ctx.request.body;
    const result = await ctx.model.User.create({ username, password });
    ctx.body = {
      code: 200,
      data: result,
      msg: '操作成功',
    };
  }

image.png 数据库中插入一条新用户数据

image.png

4.1.2 注册多个用户

// 创建多个用户
  async createUserList() {
    const { ctx } = this;
    const { data } = ctx.request.body;
    const result = await ctx.model.User.bulkCreate(data);
    console.log(data, '1111');
    ctx.body = {
      code: 200,
      data: result,
      msg: '操作成功',
    };
  }

image.png

image.png

4.1.3 巧用set方法实现加密储存密码

  • 在模型中利用set方法将密码改成加密密码 安装md5
npm install md5

修改model/user.js模型, 使用set方法

'use strict';
const md5 = require('md5');

module.exports = app => {
  const { STRING, INTEGER, DATE, ENUM } = app.Sequelize;
  // 配置(重要:一定要配置详细,一定要!!!)
  const User = app.model.define('user', {
    id: { type: INTEGER(20).UNSIGNED, primaryKey: true, autoIncrement: true },
    username: { type: STRING(30), allowNull: false, defaultValue: '', comment: '用户名称', unique: true },
    password: { type: STRING(200), allowNull: false, defaultValue: '',
      set(value) {
        const enCrpty = md5(value + 'secret');
        this.setDataValue('password', enCrpty);
      },
    },
    avatar_url: { type: STRING(200), allowNull: true, defaultValue: '' },
    sex: { type: ENUM, values: [ '男', '女', '保密' ], allowNull: true, defaultValue: '男', comment: '用户性别' },
    created_at: DATE,
    updated_at: DATE,
  }, {
    timestamps: true, // 是否自动写入时间戳
    tableName: 'user', // 自定义数据表名称
  });

  return User;
};

image.png 后面的密码就是加密过了的了

image.png

// 查

4.2 查询相关的接口和栗子🌰

查询基本是一些详情和列表的业务,业务不同,查的方式也多种多样。

4.2.1 查一个findByPk方法

 // 查单个
  async findUserByUserId() {
    const id = parseInt(this.ctx.params.id);
    console.log(id);
    const user = await this.ctx.model.User.findByPk(id);
    if (!user) {
      this.ctx.body = {
        code: 200,
        data: null,
        msg: '操作成功',
      };
      return;
    }
    this.ctx.body = {
      code: 200,
      data: user,
      msg: '操作成功',
    };
  }

image.png

4.2.2 where条件查询

  • 条件查单个findOne方法,条件写在where里
  // findOne条件查
  async findUserByCondition() {
    const user = await this.ctx.model.User.findOne({
      where: {
        id: 2,
        sex: '女',
      },
    });
    if (!user) {
      this.ctx.body = {
        code: 200,
        data: null,
        msg: '操作成功',
      };
      return;
    }
    this.ctx.body = {
      code: 200,
      data: user,
      msg: '操作成功',
    };
  }

image.png

4.2.3 列表查询

  • 查多个,使用findAll方法
// 查询多个
  async findAll() {
    const result = await this.ctx.model.User.findAll();
    if (!result) {
      this.ctx.body = {
        code: 200,
        data: null,
        msg: '操作成功',
      };
      return;
    }
    this.ctx.body = {
      code: 200,
      data: result,
      msg: '操作成功',
    };
  }

image.png

4.2.4 列表并计数查询

  • 查多个,并计数,使用findAndCountAll
 // 查询多个并计数
  async findAndCountAll() {
    const result = await this.ctx.model.User.findAndCountAll();
    if (!result) {
      this.ctx.body = {
        code: 200,
        data: null,
        msg: '操作成功',
      };
      return;
    }
    this.ctx.body = {
      code: 200,
      data: result,
      msg: '操作成功',
    };
  }

image.png

4.2.5 巧用get方法二次加工查询出来的值

  • get方法修改查询出来的值
// model/user.js
 created_at: {
      type: DATE,
      get() {
        const val = this.getDataValue('created_at');
        return 'zh' + val;
      },
    },

截屏2021-11-20 下午4.24.35.png

4.2.6 findAll 中where的使用

  // 条件查多个
  async findAllCondition() {
    const Op = this.app.Sequelize.Op;
    const result = await this.ctx.model.User.findAll({
      where: {
        // id大于3
        id: {
          [Op.gt]: 3,
        },
        // 只查男
        sex: '男',
        // 模糊搜用户名
        username: {
          [Op.like]: '%3%',
        },
      },
    });
    if (!result) {
      this.ctx.body = {
        code: 200,
        data: null,
        msg: '操作成功',
      };
      return;
    }
    this.ctx.body = {
      code: 200,
      data: result,
      msg: '操作成功',
    };
  }

image.png where还有以下常用的条件

where: {

    id: {

      [Op.and]: {a: 5},           // 且 (a = 5)

      [Op.or]: [{a: 5}, {a: 6}],  // (a = 5 或 a = 6)

      [Op.gt]: 6,                // id > 6

      [Op.gte]: 6,               // id >= 6

      [Op.lt]: 10,               // id < 10

      [Op.lte]: 10,              // id <= 10

      [Op.ne]: 20,               // id != 20

      [Op.between]: [6, 10],     // 在 6 和 10 之间

      [Op.notBetween]: [11, 15], // 不在 11 和 15 之间

      [Op.in]: [1, 2],           // 在 [1, 2] 之中

      [Op.notIn]: [1, 2],        // 不在 [1, 2] 之中

      [Op.like]: '%hat',         // 包含 '%hat'

      [Op.notLike]: '%hat',       // 不包含 '%hat'

      [Op.iLike]: '%hat',         // 包含 '%hat' (不区分大小写)  (仅限 PG)

      [Op.notILike]: '%hat',      // 不包含 '%hat'  (仅限 PG)

      [Op.overlap]: [1, 2],       // && [1, 2] (PG数组重叠运算符)

      [Op.contains]: [1, 2],      // @> [1, 2] (PG数组包含运算符)

      [Op.contained]: [1, 2],     // <@ [1, 2] (PG数组包含于运算符)

      [Op.any]: [2,3],            // 任何数组[2, 3]::INTEGER (仅限 PG)

    },

    status: {

      [Op.not]: false,           // status 不为 FALSE

    }

4.2.7 只查询某些需要的字段

  • 限制字段 attributes
// 限制字段
  async findAllLimtColumn() {
    const result = await this.ctx.model.User.findAll({
      attributes: [ 'id', 'username', 'password' ],
    });
    if (!result) {
      this.ctx.body = {
        code: 200,
        data: null,
        msg: '操作成功',
      };
      return;
    }
    this.ctx.body = {
      code: 200,
      data: result,
      msg: '操作成功',
    };
  }

image.png

  • 排除某些字段其他全部查 attributes/exclude
 const result = await this.ctx.model.User.findAll({
      attributes: {
        exclude: [ 'password' ],
      },
    });

image.png

4.2.8 查询结构进行排序

  • 排序 order,和attributes是同级的位置
 order: [
        [ 'updated_at', 'DESC' ],
        [ 'id', 'DESC' ],
      ],

image.png

4.2.9 分页查询

  • 分页查询。offset从那个开始,limit限制几条
// 分页查询
  async findAllByPage() {
    const { query } = this.ctx;
    const limit = 5;
    const offset = (query.page - 1) * limit;
    const result = await this.ctx.model.User.findAll({
      attributes: {
        exclude: [ 'password' ],
      },
      order: [
        [ 'updated_at', 'DESC' ],
        [ 'id', 'DESC' ],
      ],
      offset,
      limit,
    });
    if (!result) {
      this.ctx.body = {
        code: 200,
        data: null,
        msg: '操作成功',
      };
      return;
    }
    this.ctx.body = {
      code: 200,
      data: result,
      msg: '操作成功',
    };
  }

image.png

image.png

4.3 更新相关接口和栗子🌰

例如:用户修改了个人信息,这时我们需要同步更新数据库中数据

  • 更新
 // 更新用户
  async updateUserInfo() {
    const id = this.ctx.params.id ? parseInt(this.ctx.params.id) : 0;
    const result = await this.ctx.model.User.findByPk(id);
    if (!result) {
      this.ctx.body = {
        code: 200,
        data: null,
        msg: '未找到该用户',
      };
      return;
    }
    result.username = this.ctx.request.body.username;
    const updateResult = result.save();

    this.ctx.body = {
      code: 200,
      data: updateResult,
      msg: '操作成功',
    };

  }

image.png

update一次可以更新多个字段,通过fileds可以限制只允许更新哪些字段,增加安全性

const updateParams = this.ctx.request.body;
    const updateResult = await result.update(updateParams, {
      fields: [ 'username' ],
    });

image.png

4.4 删除相关接口和栗子🌰

4.4.1 删除单个

删除单个

  // 删除
  async destroy() {
    const id = this.ctx.params.id ? parseInt(this.ctx.params.id) : 0;
    const result = await this.ctx.model.User.findByPk(id);
    if (!result) {
      this.ctx.body = {
        code: 200,
        data: null,
        msg: '未找到该用户',
      };
      return;
    }

    const destroyResult = await result.destroy();
    this.ctx.body = {
      code: 200,
      data: destroyResult,
      msg: '操作成功',
    };
  }

image.png

4.1.2 批量删除

  • 批量删除 使用Op中的条件匹配,小于7的都会被删除
  const Op = this.app.model.Sequelize.Op;
    const destroyResult = await this.app.model.User.destroy({
      where: {
        id: {
          [Op.lte]: 7,
        },
      },
    });

五、Egg.js 进阶

5.1 异常处理

Egg中不对异常进行同一处理,一旦报错,就是Egg框架中返回的错误html页面,这既不友好,也不知道是为了什么报错。 异常处理

  • 抛出异常
this.ctx.throw(500, '自定义出错信息');

image.png

5.2 中间件, 同一异常处理

5.2.1 先写错误处理的中间件

image.png

5.2.2 再配置中间件

image.png

image.png

image.png

5.2.3 先同一异常处理逻辑,这块可根据实际需求自由调整


module.exports = () => {
  return async function errrorHanlder(ctx, next) {
    try {
      await next();
    } catch (error) {
      // 记录日志用
      ctx.app.emit('error', error, ctx);
      // 同一异常返回
      ctx.status = error.status;
      ctx.body = {
        msg: 'fail',
        data: error.message,
      };
    }
  };
};

image.png

5.2.4 配置异常开关,过滤一些异常路由

  // flag to enable your hanlder
  config.errorHanlder = {
    enable: true,
    // match: '/user/findUserByUserId',
    ignore: '/user/findUserByUserId',
  };

5.3 参数验证

对于一些客户端的参数,有必要进行一些校验,保证了系统的安全性和数据的可靠性

// 1. 安装egg-valparams
npm i egg-valparams --save
// 2.配置
// config/plugin.js
valparams : {
  enable : true,
  package: 'egg-valparams'
},
// config/config.default.js
config.valparams = {
    locale    : 'zh-cn',
    throwError: true
};
// 3.
  // 创建单个用户
  async createUser() {
    const { ctx } = this;
    ctx.validate({
      username: { type: 'string', required: true, desc: '用户名' },
      password: { type: 'string', required: true, desc: '密码' },
      sex: { type: 'string', required: false, defValue: '男', desc: '性别' },
    });
    const { username, password } = ctx.request.body;

    const result = await ctx.model.User.create({ username, password });
    ctx.body = {
      code: 200,
      data: result,
      msg: '操作成功',
    };
  }

image.png

image.png

可以看见上面的错误是英文的,我们需要中文的报错信息,去错误中间件中写一下,422状态码,特殊处理异常

 if (error.status === 422) {
        ctx.body = {
          msg: 'fail',
          data: error.message,
        };
      }

image.png

后记: 好啦,看到这里,首先要感谢耐心阅读的自己;不管你是把本文放进收藏夹,还是跟着撸一遍项目,相信你都有些收货吧!喜欢的朋友点赞+关注哦!下面是项目源码地址哦: git地址: github.com/liujun8892/…