学习egg.js,看这一篇就够了!

egg 介绍

egg 是什么?

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

为什么叫 egg ?

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

哪些产品是用 egg 开发的?

语雀 就是用 egg 开发的,架构图如下:

学习egg.js,看这一篇就够了!_第1张图片

哪些公司在用 egg?

盒马,转转二手车、PingWest、小米、58同城等(技术栈选型参考链接)

egg 支持 Typescript 吗?

虽然 egg 本身是用 JavaScript 写的,但是 egg 应用可以采用 Typescript 来写,使用下面的命令创建项目即可(参考链接):

$ npx egg-init --type=ts showcase

学习egg.js,看这一篇就够了!_第2张图片

用 JavaScript 写 egg 会有智能提示吗?

会的,只要在 package.json 中添加下面的声明之后,会在项目根目录下动态生成 typings 目录,里面包含各种模型的类型声明(参考链接):

"egg": {
   
  "declarations": true
}

egg 和 koa 是什么关系?

koa 是 egg 的基础框架,egg 是对 koa 的增强。

学习 egg 需要会 koa 吗?

不会 koa 也可以直接上手 egg,但是会 koa 的话有助于更深层次的理解 egg。

创建项目

我们采用基础模板、选择国内镜像创建一个 egg 项目:

$ npm init egg --type=simple --registry=china
# 或者 yarn create 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

创建完毕之后,目录结构如下(忽略 README文件 和 test 目录):

├── app
│   ├── controller
│   │   └── home.js
│   └── router.js
├── config
│   ├── config.default.js
│   └── plugin.js
├── package.json

这就是最小化的 egg 项目,用 npm iyarn 安装依赖之后,执行启动命令:

$ npm run dev

[master] node version v14.15.1
[master] egg version 2.29.1
[master] agent_worker#1:23135 started (842ms)
[master] egg started on http://127.0.0.1:7001 (1690ms)

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

目录约定

上面创建的项目只是最小化结构,一个典型的 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)

这是由 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)

其中 verb 一般是 HTTP 动词的小写,例如:

  • HEAD - router.head
  • OPTIONS - router.options
  • GET - router.get
  • PUT - router.put
  • POST - router.post
  • PATCH - router.patch
  • DELETE - router.deleterouter.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)
}

除了使用动词的方式创建路由之外,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 中实现对应的方法即可。

当项目越来越大之后,路由映射会越来越多,我们可能希望能够将路由映射按照文件进行拆分,这个时候有两种办法:

  1. 手动引入,即把路由文件写到 app/router 目录下,然后再 app/router.js 中引入这些文件。示例代码:

    // 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
    }
    

除了 HTTP verb 之外,Router 还提供了一个 redirect 方法,用于内部重定向,例如:

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

中间件(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 

你可能感兴趣的:(Node.js,node.js)