【egg】egg学习笔记(一)

前言

  • 最近比较忙,四个项目并行整,学习计划又拖了点,抓紧摸鱼学习下egg。

官网

  • https://eggjs.org/zh-cn/intro/

安装

  • 使用:
npx egg-init --type=ts packageName
  • 也可以新建个目录使用国内镜像模板来安装:
npm init egg --type=simple --registry=china

路由

  • 路由定义映射关系,语法如下:
router.verb('path-match', controllerAction)
  • 其中 verb 一般是 HTTP 动词的小写,例如:
HEAD - router.head
OPTIONS - router.options
GET - router.get
PUT - router.put
POST - router.post
PATCH - router.patch
DELETE - router.delete 或 router.del
除此之外,还有一个特殊的动词 router.redirect 表示重定向。
  • 而 controllerAction 则是通过点指定 controller 目录下某个文件内的某个具体函数,例如:
controller.home.index // 映射到 controller/home.js 文件的 index 方法
controller.v1.user.create // controller/v1/user.js 文件的 create 方法
  • 下面是一些示例及其解释:
module.exports = app => {
  const { router, controller } = app
  // 当用户访问 news 会交由 controller/news.js 的 index 方法进行处理
  router.get('/news', controller.news.index)
  // 通过冒号 `:x` 来捕获 URL 中的命名参数 x,放入 ctx.params.x
  router.get('/user/:id/:name', controller.user.info)
  // 通过自定义正则来捕获 URL 中的分组参数,放入 ctx.params 中
  router.get(/^\/package\/([\w-.]+\/[\w-.]+)$/, controller.package.detail)
 
  app.router.get('index', '/home/index', app.controller.home.index)
  app.router.redirect('/', '/home/index', 302)
 
}

CRUD路由

  • 除了使用动词的方式创建路由之外,egg 还提供了下面的语法快速生成 CRUD 路由:
 // 对 posts 按照 RESTful 风格映射到控制器 controller/posts.js 中
router.resources('posts', '/posts', controller.posts)
  • 会自动生成下面的路由:
HTTP方法	请求路径	路由名称	控制器函数
GET	/posts	posts	app.controller.posts.index
GET	/posts/new	new_post	app.controller.posts.new
GET	/posts/:id	post	app.controller.posts.show
GET	/posts/:id/edit	edit_post	app.controller.posts.edit
POST	/posts	posts	app.controller.posts.create
PATCH	/posts/:id	post	app.controller.posts.update
DELETE	/posts/:id	post	app.controller.posts.destroy
  • 只需要到 controller 中实现对应的方法即可。

路由拆分

  • 如果路由过多,希望以文件拆分,有以下几种方式:
  • 手动拆分:
// app/router.js
module.exports = app => {
  require('./router/news')(app)
  require('./router/admin')(app)
};

// app/router/news.js
module.exports = app => {
  app.router.get('/news/list', app.controller.news.list)
  app.router.get('/news/detail', app.controller.news.detail)
};

// app/router/admin.js
module.exports = app => {
  app.router.get('/admin/user', app.controller.admin.user)
  app.router.get('/admin/log', app.controller.admin.log)
};
  • 2 使用 egg-router-plus 插件自动引入 app/router/**/*.js,并且提供了 namespace 功能:
// app/router.js
module.exports = app => {
  const subRouter = app.router.namespace('/sub')
  subRouter.get('/test', app.controller.sub.test) // 最终路径为 /sub/test
}

中间件

  • koa封装的都长这样:
async function gzip(ctx, next) {
  // 前置代码
  await next()
  // 后置代码
}
  • egg 约定一个中间件是一个放置在 app/middleware 目录下的单独文件,它需要导出一个普通的函数,该函数接受两个参数:

  • options: 中间件的配置项,框架会将 app.config[${middlewareName}] 传递进来。

  • app: 当前应用 Application 的实例。

  • 建个中间件试一下,先在middleware/slow.js里写入如下内容:

module.exports = (options, app) => {
  return async function (ctx, next) {
    const startTime = Date.now() // 记录开始时间
    await next()
    const consume = Date.now() - startTime // 记录总耗时
    const { threshold = 0 } = options || {}
    if (consume > threshold) { // 如果耗时超过指定阈值就打印日志
      console.log(`${ctx.url}请求耗时${consume}毫秒`)
    }
  }
}
  • 在config.default.js中加入该中间件:
	config.middleware = ["slow"];
  • 这的里配置的中间件是全局启用的,如果只是想在指定路由中使用中间件的话,例如只针对 /api 前缀开头的 url 请求使用某个中间件的话,有两种方式:

  • 在 config.default.js 配置中设置 match 或 ignore 属性:

module.exports = {
  middleware: [ 'slow' ],
  slow: {
    threshold: 1,
    match: '/api'
  },
};
  • 如果config写法是这样:
	config.slow = {
		threshold: 1,
		match: "/api",
	};
  • 或者在路由中加入:(注意需要在config.default中把middleware那些去了,所以 app.config.appMiddleware应该没有该中间件)
module.exports = app => {
  const { router, controller } = app
  // 在 controller 处理之前添加任意中间件
  router.get('/api/home', app.middleware.slow({ threshold: 1 }), controller.home.index)
}
  • 可以通过app看见框架的中间件
  console.log(app.config.appMiddleware)
  console.log(app.config.coreMiddleware)
  • coreMiddleware是默认的,如果不需要,可以关闭:
module.exports = {
  i18n: {
    enable: false
  }
}

控制器

  • 简单的控制器如下:
const { Controller } = require('egg')
class HomeController extends Controller {
  async index() {
    const { ctx } = this
    ctx.body = 'hi, egg'
  }
}
module.exports = HomeController
  • 在 Controller 中通过 this.ctx 可以获取上下文对象,方便获取和设置相关参数,例如:
ctx.query:URL 中的请求参数(忽略重复 key)
ctx.quries:URL 中的请求参数(重复的 key 被放入数组中)
ctx.params:Router 上的命名参数
ctx.request.body:HTTP 请求体中的内容
ctx.request.files:前端上传的文件对象
ctx.getFileStream():获取上传的文件流
ctx.multipart():获取 multipart/form-data 数据
ctx.cookies:读取和设置 cookie
ctx.session:读取和设置 session
ctx.service.xxx:获取指定 service 对象的实例(懒加载)
ctx.status:设置状态码
ctx.body:设置响应体
ctx.set:设置响应头
ctx.redirect(url):重定向
ctx.render(template):渲染模板

Service

  • Controller 可以调用任何一个 Service 上的任何方法,值得注意的是:Service 是懒加载的,即只有当访问到它的时候框架才会去实例化它。
  • service案例:
// app/service/user.js
const { Service } = require('egg').Service;

class UserService extends Service {
  async find(uid) {
    const user = await this.ctx.db.query('select * from user where uid = ?', uid);
    return user;
  }
}

module.exports = UserService;
  • 在controller中调用:
class UserController extends Controller {
  async info() {
    const { ctx } = this;
    const userId = ctx.params.id;
    const userInfo = await ctx.service.user.find(userId);
    ctx.body = userInfo;
  }
}
  • 注意,Service 文件必须放在 app/service 目录,支持多级目录,访问的时候可以通过目录名级联访问:
app/service/biz/user.js => ctx.service.biz.user
app/service/sync_user.js => ctx.service.syncUser
app/service/HackerNews.js => ctx.service.hackerNews
  • Service 里面的函数,可以理解为某个具体业务逻辑的最小单元,Service 里面也可以调用其他 Service,值得注意的是:Service 不是单例,是 请求级别 的对象,框架在每次请求中首次访问 ctx.service.xx 时延迟实例化,都继承于 egg.Service,每个 Service 实例都会拥有下列属性:
this.ctx: 当前请求的上下文 Context 对象的实例
this.app: 当前应用 Application 对象的实例
this.service:等价于 this.ctx.service
this.config:应用运行时的配置项
this.logger:logger 对象,上面有四个方法(debug,info,warn,error),分别代表打印四个不同级别的日志

模板渲染

  • egg 框架内置了 egg-view 作为模板解决方案,并支持多种模板渲染,例如 ejs、handlebars、nunjunks 等模板引擎,每个模板引擎都以插件的方式引入,默认情况下,所有插件都会去找 app/view 目录下的文件,然后根据 config\config.default.js 中定义的后缀映射来选择不同的模板引擎:
config.view = {
  defaultExtension: '.nj',
  defaultViewEngine: 'nunjucks',
  mapping: {
    '.nj': 'nunjucks',
    '.hbs': 'handlebars',
    '.ejs': 'ejs',
  },
}

上面的配置表示,当文件:

后缀是 .nj 时使用 nunjunks 模板引擎
后缀是 .hbs 时使用 handlebars 模板引擎
后缀是 .ejs 时使用 ejs 模板引擎
当未指定后缀时默认为 .html
当未指定模板引擎时默认为 nunjunks

接下来安装模板引擎插件:

$ npm i egg-view-nunjucks egg-view-ejs egg-view-handlebars --save
  • 然后在 config/plugin.js 中启用该插件:
exports.nunjucks = {
  enable: true,
  package: 'egg-view-nunjucks',
}
exports.handlebars = {
  enable: true,
  package: 'egg-view-handlebars',
}
exports.ejs = {
  enable: true,
  package: 'egg-view-ejs',
}
  • 在app/view下新增下列文件:
app/view
├── ejs.ejs
├── handlebars.hbs
└── nunjunks.nj
<!-- ejs.ejs 文件代码 -->
<h1>ejs</h1>
<ul>
  <% items.forEach(function(item){ %>
    <li><%= item.title %></li>
  <% }); %>
</ul>
      
<!-- handlebars.hbs 文件代码 -->
<h1>handlebars</h1>
{{#each items}}
  <li>{{title}}</li>
{{~/each}}
    
<!-- nunjunks.nj 文件代码 -->
<h1>nunjunks</h1>
<ul>
{% for item in items %}
  <li>{{ item.title }}</li>
{% endfor %}
</ul>
  • 在router中配置:
module.exports = app => {
  const { router, controller } = app
  router.get('/ejs', controller.home.ejs)
  router.get('/handlebars', controller.home.handlebars)
  router.get('/nunjunks', controller.home.nunjunks)
}
  • controller中返回:
const Controller = require('egg').Controller

class HomeController extends Controller {
  async ejs() {
    const { ctx } = this
    const items = await ctx.service.view.getItems()
    await ctx.render('ejs.ejs', {items})
  }

  async handlebars() {
    const { ctx } = this
    const items = await ctx.service.view.getItems()
    await ctx.render('handlebars.hbs', {items})
  }

  async nunjunks() {
    const { ctx } = this
    const items = await ctx.service.view.getItems()
    await ctx.render('nunjunks.nj', {items})
  }
}

module.exports = HomeController
  • 数据放到service
const { Service } = require('egg')

class ViewService extends Service {
  getItems() {
    return [
      { title: 'foo', id: 1 },
      { title: 'bar', id: 2 },
    ]
  }
}

module.exports = ViewService
  • egg-view 对 context 进行扩展,为 ctx 上下文对象增加了 render、renderView 和 renderString 三个方法。

集成mysql

  • 这里简单写一下,有需要再查文档。
  • 安装插件
$ npm i egg-mysql
  • 在 config/plugin.js 中开启插件:
exports.mysql = {
  enable: true,
  package: 'egg-mysql',
}
  • 在 config/config.default.js 中定义连接参数:
config.mysql = {
  client: {
    host: 'localhost',
    port: '3306',
    user: 'root',
    password: 'root',
    database: 'cms',
  }
}
  • 即可拿到mysql对象:
class UserService extends Service {
  async find(uid) {
    const user = await this.app.mysql.get('users', { id: 11 });
    return { user }
  }
}
  • 如果mysql启动报错,可能是密码问题,这么解决:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'
flush privileges
  • 集成sequelize
npm install egg-sequelize --save 
  • 然后在 config/plugin.js 中开启 egg-sequelize 插件:
exports.sequelize = {
  enable: true,
  package: 'egg-sequelize',
}
  • 同样要在 config/config.default.js 中编写 sequelize 配置
config.sequelize = {
  dialect: 'mysql',
  host: '127.0.0.1',
  port: 3306,
  database: 'example',
}
  • 然后在 egg_example 库中创建 books 表:
CREATE TABLE `books` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'primary key',
  `name` varchar(30) DEFAULT NULL COMMENT 'book name',
  `created_at` datetime DEFAULT NULL COMMENT 'created time',
  `updated_at` datetime DEFAULT NULL COMMENT 'updated time',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='book';
  • 创建 model/book.js 文件,代码是:
module.exports = app => {
  const { STRING, INTEGER } = app.Sequelize
  const Book = app.model.define('book', {
    id: { type: INTEGER, primaryKey: true, autoIncrement: true },
    name: STRING(30),
  })
  return Book
}

添加 controller/book.js 控制器:

const Controller = require('egg').Controller

class BookController extends Controller {
  async index() {
    const ctx = this.ctx
    ctx.body = await ctx.model.Book.findAll({})
  }

  async show() {
    const ctx = this.ctx
    ctx.body = await ctx.model.Book.findByPk(+ctx.params.id)
  }

  async create() {
    const ctx = this.ctx
    ctx.body = await ctx.model.Book.create(ctx.request.body)
  }

  async update() {
    const ctx = this.ctx
    const book = await ctx.model.Book.findByPk(+ctx.params.id)
    if (!book) return (ctx.status = 404)
    await book.update(ctx.request.body)
    ctx.body = book
  }

  async destroy() {
    const ctx = this.ctx
    const book = await ctx.model.Book.findByPk(+ctx.params.id)
    if (!book) return (ctx.status = 404)
    await book.destroy()
    ctx.body = book
  }
}

module.exports = BookController
  • 最后配置 RESTful 路由映射:
module.exports = app => {
  const {router, controller} = app
  router.resources('books', '/books', controller.book)
}
  • 同时用mysql和mongoose貌似有坑,需要改配置:https://github.com/eggjs/egg/issues/805

定时任务

  • egg 框架提供了定时任务功能,在 app/schedule 目录下,每一个文件都是一个独立的定时任务,可以配置定时任务的属性和要执行的方法,例如创建一个 update_cache.js 的更新缓存任务,每分钟执行一次:
const Subscription = require('egg').Subscription

class UpdateCache extends Subscription {
  // 通过 schedule 属性来设置定时任务的执行间隔等配置
  static get schedule() {
    return {
      interval: '1m', // 1 分钟间隔
      type: 'all', // 指定所有的 worker 都需要执行
    }
  }

  // subscribe 是真正定时任务执行时被运行的函数
  async subscribe() {
    const res = await this.ctx.curl('http://www.api.com/cache', {
      dataType: 'json',
    })
    this.ctx.app.cache = res.data
  }
}

module.exports = UpdateCache
*    *    *    *    *    *
┬    ┬    ┬    ┬    ┬    ┬
│    │    │    │    │    |
│    │    │    │    │    └ day of week (0 - 7) (0 or 7 is Sun)
│    │    │    │    └───── month (1 - 12)
│    │    │    └────────── day of month (1 - 31)
│    │    └─────────────── hour (0 - 23)
│    └──────────────────── minute (0 - 59)
└───────────────────────── second (0 - 59, optional)
  • 执行任务的类型有两种:

  • worker 类型:只有一个 worker 会执行这个定时任务(随机选择)

  • all 类型:每个 worker 都会执行这个定时任务

错误处理

  • 如果我们的项目是前后端分离的,所有返回都是 JSON 格式的话,可以在 config/plugin.js 中进行如下配置:
module.exports = {
  one rror: {
    accepts: () => 'json',
  },
};
  • 那么就会把错误调用栈直接以 JSON 的格式返回:
{
    "message": "Cannot read property 'find' of undefined",
    "stack": "TypeError: Cannot read property 'find' of undefined\n    at UserController.index (/Users/keliq/code/egg-project/app/controller/user.js:7:37)",
    "name": "TypeError",
    "status": 500
}
  • 也可返回html
module.exports = {
  one rror: {
      accepts: (ctx) => {
        if (ctx.get('content-type') === 'application/json') return 'json';
        return 'html';
      }
  },
};
  • 自定义错误页:
  • 在 config/config.default.js 中自定义错误:
module.exports = {
  one rror: {
    errorPageUrl: '/public/error.html',
  },
};
  • 404不会当作异常处理。
  • 很多厂都是自己写 404 页面的,如果你也有这个需求,也可以自己写一个 HTML,然后在 config/config.default.js 中指定:
module.exports = {
  notfound: {
    pageUrl: '/404.html',
  }
}

生命周期

class AppBootHook {
  constructor(app) {
    this.app = app
  }

  configWillLoad() {
    // config 文件已经被读取并合并,但是还并未生效,这是应用层修改配置的最后时机
    // 注意:此函数只支持同步调用
  }

  configDidLoad() {
    // 所有的配置已经加载完毕,可以用来加载应用自定义的文件,启动自定义的服务
  }

  async didLoad() {
    // 所有的配置已经加载完毕,可以用来加载应用自定义的文件,启动自定义的服务
  }

  async willReady() {
    // 所有的插件都已启动完毕,但是应用整体还未 ready
    // 可以做一些数据初始化等操作,这些操作成功才会启动应用
  }

  async didReady() {
    // 应用已经启动完毕
  }

  async serverDidReady() {
    // http / https server 已启动,开始接受外部请求
    // 此时可以从 app.server 拿到 server 的实例
  }

  async beforeClose() {
    // 应用即将关闭
  }
}

module.exports = AppBootHook
上一篇:nodejs之egg框架整合ejs模板


下一篇:OpenStack iaas-install-swift-controller.sh