Egg.js

@Egg介绍

概要

egg 是阿里出品的一款 node.js 后端 web 框架,基于 koa 封装,并做了一些约定。

为什么叫 egg ?

egg 有孕育的含义,因为 egg 的定位是企业级 web 基础框架,旨在帮助开发者孕育适合自己团队的框架。

技术名词解释

技术细节

创建项目

  1. 安装 Node.js
    确保你的系统中安装了 Node.js。你可以从 Node.js 官网 下载并安装。

  2. 创建 Egg.js 后端项目
    使用 egg-init 创建一个新的 egg.js 项目:

npm install -g egg-init
egg-init egg-example --type=simple
cd egg-example
npm install

另外一种方式:
$ npm init egg --type=simple --registry=china
解释一下 npm init egg 这种语法:
npm@6 版本引入了 npm-init 语法,等价于 npx create- 命令,而 npx 命令会去 $PATH 路径和 node_modules/.bin 路径下寻找名叫 create- 的可执行文件,如果找到了就执行,找不到就去安装。
也就是说,npm init egg 会去寻找或下载 create-egg 可执行文件,而 create-egg 包就是 egg-init 包的别名,相当于调用了 egg-init 。

  1. 开发 Egg.js 后端
    在 egg-example 目录中,你可以开始开发你的后端服务。例如,创建一个简单的 API:
// app/controller/home.js
const Controller = require('egg').Controller;

class HomeController extends Controller {
  async index() {
    this.ctx.body = 'Hello world';
  }
}
module.exports = HomeController;

配置路由:

// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
};

创建完毕之后,目录结构如下(忽略 README文件 和 test 目录):
├── app
│ ├── controller
│ │ └── home.js
│ └── router.js
├── config
│ ├── config.default.js
│ └── plugin.js
├── package.json

启动 Egg.js 服务:

npm run dev

2024-02-18 16:07:04,402 INFO 8076 [master] node version v16.15.0
2024-02-18 16:07:04,404 INFO 8076 [master] egg version 3.19.0
2024-02-18 16:07:06,303 INFO 8076 [master] agent_worker#1:22064 started (1894ms)
2024-02-18 16:07:08,379 INFO 8076 [master] egg started on http://127.0.0.1:7001 (3975ms)

打开 http://127.0.0.1:7001/ 会看到网页上显示 hi, egg

工程结构概述

egg-project
├── package.json
├── app.js (可选)
├── agent.js (可选)
├── app/
| ├── router.js # 用于配置 URL 路由规则
│ ├── controller/ # 用于存放控制器(解析用户的输入、加工处理、返回结果)
│ ├── model/ (可选) # 用于存放数据库模型
│ ├── service/ (可选) # 用于编写业务逻辑层
│ ├── middleware/ (可选) # 用于编写中间件
│ ├── schedule/ (可选) # 用于设置定时任务
│ ├── public/ (可选) # 用于放置静态资源
│ ├── view/ (可选) # 用于放置模板文件
│ └── extend/ (可选) # 用于框架的扩展
│ ├── helper.js (可选)
│ ├── request.js (可选)
│ ├── response.js (可选)
│ ├── context.js (可选)
│ ├── application.js (可选)
│ └── agent.js (可选)
├── config/
| ├── plugin.js # 用于配置需要加载的插件
| ├── config.{env}.js # 用于编写配置文件(env 可以是 default,prod,test,local,unittest)
config.default.js
config.local.js
config.prod.js
config.test.js
这是由 egg 框架或内置插件约定好的,是阿里总结出来的最佳实践,虽然框架也提供了让用户自定义目录结构的能力,但是依然建议大家采用阿里的这套方案。在接下来的篇章当中,会逐一讲解上述约定目录和文件的作用。

路由(Router)

路由定义了 请求路径(URL) 和 控制器(Controller) 之间的映射关系,即用户访问的网址应交由哪个控制器进行处理。我们打开 app/router.js 看一下:

module.exports = app => {
  const { router, controller } = app
  router.get('/', controller.home.index)
};

可以看到,路由文件导出了一个函数,接收 app 对象作为参数,通过下面的语法定义映射关系:

router.verb('path-match', controllerAction)

中间件(Middleware)

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 中使用:

module.exports = {
  // 配置需要的中间件,数组顺序即为中间件的加载顺序
  middleware: [ 'slow' ],
  // slow 中间件的 options 参数
  slow: {
    enable: true
  },
}

这里配置的中间件是全局启用的,如果只是想在指定路由中使用中间件的话,例如只针对 /api 前缀开头的 url 请求使用某个中间件的话,有两种方式:
1.在 config.default.js 配置中设置 match 或 ignore 属性:

module.exports = {
  middleware: [ 'slow' ],
  slow: {
    threshold: 1,
    match: '/api'
  },
};

2.在路由文件 router.js 中引入

module.exports = app => {
  const { router, controller } = app
  // 在 controller 处理之前添加任意中间件
  router.get('/api/home', app.middleware.slow({ threshold: 1 }), controller.home.index)
}

egg 把中间件分成应用层定义的中间件(app.config.appMiddleware)和框架默认中间件(app.config.coreMiddleware),我们打印看一下:

module.exports = app => {
  const { router, controller } = app
  console.log(app.config.appMiddleware)
  console.log(app.config.coreMiddleware)
  router.get('/api/home', app.middleware.slow({ threshold: 1 }), controller.home.index)
}

结果是:

// appMiddleware
[ 'slow' ] 
// coreMiddleware
[
  'meta',
  'siteFile',
  'notfound',
  'static',
  'bodyParser',
  'overrideMethod',
  'session',
  'securities',
  'i18n',
  'eggLoaderTrace'
]

其中那些 coreMiddleware 是 egg 帮我们内置的中间件,默认是开启的,如果不想用,可以通过配置的方式进行关闭:

module.exports = {
  i18n: {
    enable: false
  }
}

控制器(Controller)

Controller 负责解析用户的输入,处理后返回相应的结果,一个最简单的 helloworld 示例:

const { Controller } = require('egg');
class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    ctx.body = 'hi, egg';
  }
}
module.exports = HomeController;

当然,我们实际项目中的代码不会这么简单,通常情况下,在 Controller 中会做如下几件事情:

接收、校验、处理 HTTP 请求参数
向下调用服务(Service)处理业务
通过 HTTP 将结果响应给用户
一个真实的案例如下:

const { Controller } = require('egg');
class PostController extends Controller {
  async create() {
    const { ctx, service } = this;
    const createRule = {
      title: { type: 'string' },
      content: { type: 'string' },
    };
    // 校验和组装参数
    ctx.validate(createRule);
    const data = Object.assign(ctx.request.body, { author: ctx.session.userId });
    // 调用 Service 进行业务处理
    const res = await service.post.create(data);
    // 响应客户端数据
    ctx.body = { id: res.id };
    ctx.status = 201;
  }
}
module.exports = PostController;

由于 Controller 是类,因此可以通过自定义基类的方式封装常用方法,例如:

// app/core/base_controller.js
const { Controller } = require('egg');
class BaseController extends Controller {
  get user() {
    return this.ctx.session.user;
  }
  success(data) {
    this.ctx.body = { success: true, data };
  }
  notFound(msg) {
    this.ctx.throw(404, msg || 'not found');
  }
}
module.exports = BaseController;

然后让所有 Controller 继承这个自定义的 BaseController:

// app/controller/post.js
const Controller = require('../core/base_controller');
class PostController extends Controller {
  async list() {
    const posts = await this.service.listByUser(this.user);
    this.success(posts);
  }
}

在 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):渲染模板

this.ctx 上下文对象是 egg 框架和 koa 框架中最重要的一个对象,我们要弄清楚该对象的作用,不过需要注意的是,有些属性并非直接挂在 app.ctx 对象上,而是代理了 request 或 response 对象的属性,我们可以用 Object.keys(ctx) 看一下:

[
  'request', 'response', 'app', 'req', 'res', 'onerror', 'originalUrl', 'starttime', 'matched',
  '_matchedRoute', '_matchedRouteName', 'captures', 'params', 'routerName', 'routerPath'
]

关于ctx对象说明

在 Egg.js 中,ctx 是 context 的缩写,它代表了一次请求的上下文。ctx 是一个封装了 Node.js 原生 HTTP 请求和响应对象的对象,提供了一系列实用的方法和属性,使得处理 HTTP 请求变得更加方便。

ctx 对象包含了许多重要的属性和方法,例如:

ctx.req 和 ctx.res:分别代表 Node.js 的原生 request 和 response 对象。
ctx.request 和 ctx.response:是对原生 req 和 res 的封装,提供了更多的功能和更友好的 API。
ctx.state:用于通过中间件传递信息和你的前端视图。
ctx.params:包含路由参数,例如在路由 /users/:id 中,:id 就是一个路由参数。
ctx.query:包含 URL 查询字符串的参数。
ctx.body:用于获取或设置响应体。
当你在 Egg.js 中定义路由时,你可以使用路径参数来捕获 URL 中的动态值。在路由定义中,以冒号 : 开头的部分是路径参数的占位符。例如,在路由 /users/:id 中,:id 是一个路径参数,它会匹配任何在 /users/ 后面的一段 URL 路径。

在控制器方法中,你可以通过 ctx.params 对象访问这些路径参数。例如:

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

class UserController extends Controller {
  async show() {
    const userId = this.ctx.params.id; // 获取路径参数 id 的值
    // ... 根据 userId 进行业务逻辑处理
    this.ctx.body = `User ID: ${userId}`;
  }
}

module.exports = UserController;

在这个例子中,如果有一个请求是 GET /users/123,那么 ctx.params.id 的值将会是 ‘123’,控制器方法可以使用这个值来进行进一步的处理,比如查询数据库中对应的用户信息。

总结来说,ctx 是 Egg.js 提供的一个核心对象,它封装了一次 HTTP 请求的所有相关信息和操作方法,使得开发者可以方便地处理请求和响应。

服务(Service)

Service 是具体业务逻辑的实现,一个封装好的 Service 可供多个 Controller 调用,而一个 Controller 里面也可以调用多个 Service,虽然在 Controller 中也可以写业务逻辑,但是并不建议这么做,代码中应该保持 Controller 逻辑简洁,仅仅发挥「桥梁」作用。
Controller 可以调用任何一个 Service 上的任何方法,值得注意的是:Service 是懒加载的,即只有当访问到它的时候框架才会去实例化它。
通常情况下,在 Service 中会做如下几件事情:

处理复杂业务逻辑
调用数据库或第三方服务(例如 GitHub 信息获取等)

一个简单的 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 时延迟实例化,所以 Service 中可以通过 this.ctx 获取到当前请求的上下文。

模板渲染

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
# 或者 yarn add egg-view-nunjucks egg-view-ejs egg-view-handlebars

然后在 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

访问下面的地址可以查看不同模板引擎渲染出的结果:

GET http://localhost:7001/nunjunks
GET http://localhost:7001/handlebars
GET http://localhost:7001/ejs

你可能会问,ctx.render 方法是哪来的呢?没错,是由 egg-view 对

context 进行扩展而提供的,为 ctx 上下文对象增加了 render、renderView 和 renderString 三个方法,代码如下:
const ContextView = require('../../lib/context_view')
const VIEW = Symbol('Context#view')

module.exports = {
  render(...args) {
    return this.renderView(...args).then(body => {
      this.body = body;
    })
  },

  renderView(...args) {
    return this.view.render(...args);
  },

  renderString(...args) {
    return this.view.renderString(...args);
  },

  get view() {
    if (this[VIEW]) return this[VIEW]
    return this[VIEW] = new ContextView(this)
  }
}

它内部最终会把调用转发给 ContextView 实例上的 render 方法,ContextView 是一个能够根据配置里面定义的 mapping,帮助我们找到对应渲染引擎的类。

参考链接:https://juejin.cn/post/6995063516470198279

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值