Egg


Egg 概述

仅使用基础库的工程:

使用 Egg 的工程:

可见 Egg 的特点:

  • 整合了后端技术,提供一站式开发体验
  • 制定了一套规范,统一了开发模式
  • 提供了各种插件,具备灵活的扩展能力

除此之外,Egg 还具有以下特点:

  • Convention Over Configuration,约定优于配置
  • 内置多进程管理
  • 使用 MVC 模式

Egg 的应用场景:任何前端服务器都可以使用 Egg,如常见的:

  • 中间服务器
  • 纯后端服务器

Hello World

官网:https://eggjs.org/zh-cn/index.html

使用 Egg 搭建工程

方式 1: 使用 egg 脚手架

方式 2: 手动搭建

npm i egg # 安装egg核心库
npm i -D egg-bin # 安装egg命令行工具

编写代码

目录结构:

root
├── app  # 应用程序目录,几乎所有核心代码都在此目录
│   ├── controller  # 控制器目录,每个控制器用于处理请求
│   │   └── home.js  # 某个控制器
│   └── router.js # 路由设置,将请求路径、方法映射到控制器
├── config # 配置目录
│   └── config.default.js # 默认配置
└── package.json
// 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);
};
// config/config.default.js
exports.keys = <此处改为你自己的 Cookie 安全字符串>;

添加 npm scriptspackage.json

{
  "scripts": {
    "dev": "egg-bin dev"
  }
}

运行

npm run dev

访问:http://localhost:7001

核心概念

约定

Egg 中使用了很多约定,包括对文件夹的约定、对文件名的约定等等,这些约定的存在,减少了大量的配置

运行流程

  • 生成配置:egg 会在启动时读取config文件夹中的配置,以及app/router.js中的路由信息,然后将最终的配置生成到run文件夹中
  • 启动 egg:egg 会在内部创建Koa实例,并作出适当的初始化工作,然后监听7001端口(默认)
  • 匹配路由:egg 在内部使用了@koa/router,会根据路由表中请求的路径和方法,把请求交给指定的action进行处理
  • 请求处理:egg 遵循MVC模式,请求始终是交给**Controller**中的**Action**进行处理
    • Controller表现为一个类,继承自egg中的Controller
    • Action表现为一个Controller中的实例方法,用于处理请求

路由和控制器

路由匹配

路由是一个桥梁,它连接了请求请求处理函数

普通路由设置

// app/router.js
// app对象是全局对象,跨越所有请求,它在egg启动后就会创建
module.exports = (app) => {
  // app.router:通过 new Router 创建的路由对象
  // app.controller: 根据 app/controller 文件夹中的文件创建的对象
  const { router, controller } = app; //解构
  router.get("/user/:id", controller.user.info);
  // 或
  // router.get('/user/:id', "user.info");
};

配置路由时,可以给路由取一个合适的名称,该名称在将来可能会有用

router.get("userdetail", "/user/:id", "user.info");//get,也可以其他请求方式。
参数: 名字,匹配的路径,运行的action

重定向

router.get("/login", "user.login");
router.redirect("/sign-in", "/login"); //访问旧的地址的时候跳转到新地址

> 解除了请求与功能的耦合,功能没变,就不需要改,路由变了,只改路由就行

也可以这样

router.get("/login",'home.login);
router.get("/signed-in","home.login")
实现访问不同路径看到同一个内容

路由映射过多?

app/router.js是集中用于映射路由的模块,不建议把它的代码分散的其他地方

可以参考下面的做法:

const mapper = {};
mapper.mapUser = function (app) {
  const { router } = app;
  const prefix = "/api/user";
  router.post(`${prefix}/login`, "user.login");
  router.post(`${prefix}/reg`, "user.reg");
  router.get(`${prefix}/:id`, "user.info");
  // ...
};

mapper.mapNews = function (app) {
  const { router } = app;
  const prefix = "/api/news";
  router.get(`${prefix}/`, "news.all");
  router.get(`${prefix}/:id`, "news.one");
  // ...
};

module.exports = (app) => {
  Object.values(mapper).forEach((m) => m(app));
};

控制器在子目录?

假设控制器在app/controller/user/auth

router.get("/user/login", "user.auth.login");
router.get("/user/login", controller.user.auth.login);

RESTful 风格的 URL 定义

router提供了resources函数,用于定义RESTful风格的api接口

// blogs: RESTful风格对应的资源名称
// /api/blog: 基础路径
// controller
router.resources("blogs", "/b", controller.blog);

上面一句代码的结果类似于:

router.get("blogs", "/b", controller.blog.index); // 获取所有博客 或 分页获取博客
router.get("new_blog", "/b/new", controller.blog.new); // 获取添加博客的表单页面
router.get("blog", "/b/:id", controller.blog.show); // 获取某一篇博客
router.get("edit_blog", "/b/:id/edit", controller.blog.edit); // 获取某一篇博客的编辑界面
router.post("blogs", "/b", controller.blog.create); // 添加一篇博客
router.put("blog", "/b/:id", controller.blog.update); // 修改一篇博客
router.delete("blog", "/b/:id", controller.blog.destroy); // 删除一篇博客

如果我们不需要其中的某几个方法,可以不用在 blog.js 里面实现,这样对应 URL 路径也不会注册到 Router。

Controller 和 action

网站中有很多的资源,比如用户文章评论

大部分情况下,对某个资源的处理,就对应一个Controller

egg中,对Controller的要求如下:

  • 必须写到app/controller文件夹中
  • 基类继承自Controller的类(非必须,但建议这么做)
  • 文件名就是Controller的名称

当匹配到某个Controller,同时匹配到某个action时,egg会:

  1. 创建Controller实例
  2. 调用对应的action方法

ctx 的获取

Koacontext对象可以通过以下两种途径获取:

  • action的参数
  • this.ctxcontroller实例中包含context对象

下一个中间件?

MVC的结构中,controller不应该关心其他中间件的执行

因此,在整个洋葱模型中,controller应该处于洋葱的最里层

所以,egg没有把next函数给予controller

静态资源

默认情况下,app/public目录为静态资源目录,请求路径/public/**位置对应的请求将被映射到app/public目录

访问静态资源:127.0.0.1:700/public/index.html

egg之所以能够映射静态资源,并非它本身具有这样的能力,而是它在内部使用了插件egg-static

https://github.com/eggjs/egg-static

插件

egg本身其实只是搭建了一个框架,拥有一套规范,更多的额外功能都是靠各种插件完成的

插件的命名

egg 插件的命名规范为egg-*

比如,静态资源映射的插件名称为egg-static

插件的启用

安装好插件后,默认是没有启动该插件的,需要在config/plugin.js中启用插件

module.exports = {
  插件名称: {
    enable: 是否启用,
    package: 插件在node_modules中的包名,
    path: 插件的绝对路径,与package配置互斥  自己写的插件
  }
}

比如,对于egg-static插件,可以通过下面的配置启用它

module.exports = {
  static: {
    enable: true,
    package: "egg-static",
  },
};;

exports.static = {
  enable: true,
  package: "egg-static",
};

由于egg-static是一个内置插件,大部分内置插件都是自动启用的。同时,内置插件可以通过更加简单的方式进行启用和关闭

exports.static = false;

插件的配置

config/plugin.js只是控制插件的启用和关闭,对于插件的配置需要在config/config.default.js中完成

这样做的逻辑理念是:集中配置,集中管理

不同的插件有不同的配置,需要阅读插件的官方文档

exports.static = {
  // egg-static 的配置
};

中间服务器的常见职责

场景 1:代替传统后端服务器,托管静态资源、动态渲染页面、提供少量 api 访问

场景 2:托管单页应用程序的静态资源、提供各种数据 api

egg 中的模板引擎

如果要使用传统的方式进行服务端渲染,就需要用到模板引擎

egg 内置了插件egg-view,它本身不是模板引擎,但它可以对不同的模板引擎统一配置、统一处理

你需要安装具体的模板引擎插件,完成模板引擎的启用

https://github.com/eggjs/egg-view

安装模板引擎插件

egg-view支持多种模板引擎,用的较多是egg-view-nunjucksegg-view-ejs

npm i egg-view-ejs

https://github.com/eggjs/egg-view-ejs

启用模板引擎插件

// config/plugin.js
exports.ejs = {
  enable: true,
  package: "egg-view-ejs",
};

统一配置

egg启用后,会自动加载插件egg-viewegg-view会读取配置中的view配置,来使用你指定的模板引擎

//config/config.default.js
exports.view = {
  // 该配置会被 egg-view 读取
  root: "模板所在的根目录", // 告诉egg-view到哪里去寻找模板,多个绝对路径使用逗号分割,默认 /app/view
  cache: true, // 是否在启动时缓存模板路径,以提高效率,默认开启
  mapping: {
    // 映射配置,将不同的模板后缀映射到对应的模板引擎处理
    ".ejs": "ejs",
    ".html": "ejs",
  },
  defaultViewEngine: "ejs", //如果映射找不到对应的模板引擎,将使用该值作为默认使用的模板引擎
  defaultExtension: ".ejs", //后续在controller中渲染模板时,默认渲染的模板后缀名
};

渲染页面

配置好模板引擎后,即可在app/view中书写各种模板

当某个请求到达后,如果需要经过模板渲染页面,只需要在action中使用对应代码即可

render(name, model); // 渲染模板文件, 并赋值给 ctx.body
renderView(name, model); // 渲染模板文件, 仅返回不赋值
renderString(tpl, model); // 渲染模板字符串, 仅返回不赋值

在渲染时,它将按照view的配置进行处理

此时,已形成完整的MVC模式(比较适合客户端使用)


请求到来了,controller 处理,处理过程中组装出来了一个数据,拿到一个模型(可以是数据库模型,可以远程获取,可以字面量,模型可以是任何东西,本质是 UI 渲染模型),模型交给视图,渲染完交给客户端了。客户端可能会重新请求服务器,又请求了 contrller。

编写中间件

在 egg 中编写 koa 的中间件,需要满足一定的规范

在实际开发中,绝大部分中间件都是通过一个高阶函数得到的

var myMiddleware = (options) => {
  return async function(ctx, next){
    ...
  }
}

// 使用
myMiddleware(配置对象); // 得到一个中间件

egg 秉承了这种思想,并把它当做规范使用

你需要在app/middleware文件夹中编写中间件

// app/middleware/mymid.js
module.exports = (options, app) => {
  // options是针对该中间件的配置,app是egg全局应用对象
  return async function (ctx, next) {
    console.log("中间件开始", options);
    await next();
    console.log("中间件结束", options);
  };
};

应用中间件

在 egg 中,中间件的使用也是遵循规范的,它的使用分为两种,分别是全局局部

全局中间件

全局中间件会在所有控制器处理之前运行

按照 egg 统一配置的原则,需要在config/config.default.js中配置中间件

// config/config.default.js
// 配置全局中间件,数组中的字符串对应 app/middleware 中的文件名
// 数组中的顺序对应中间件的运行顺序
exports.middleware = ["mymid"];

// mymid 对应中间件的配置,该配置会传递到中间件的options参数中
exports.mymid = {
  a: 1,
  b: 2,
};

在中间件的配置中,有一部分是通用配置,通用配置会影响 egg 是否运行中间件,通用配置包括:

  • enable:boolean,是否启用中间件
  • match 和 ignore,分别表示匹配和忽略,它们均支持多种类型的配置方式
    • 字符串:当参数为字符串类型时,配置的是一个 url 的路径前缀,所有以配置的字符串作为前缀的 url 都会匹配上。 当然,你也可以直接使用字符串数组。
    • 正则:当参数为正则时,直接匹配满足正则验证的 url 的路径。
    • 函数:当参数为一个函数时,会将请求上下文传递给这个函数,最终取函数返回的结果(true/false)来判断是否匹配。

路由中间件

有些中间件并不需要全局使用,而是仅仅针对某个或某几个路由使用

此时,就不需要在config/config.default.js进行配置了,而是:

module.exports = (app) => {
  const { router } = app;
  const mymid = app.middleware.mymid(配置); // 得到中间件
  router.get("/", mymid, "home.index"); // 仅在该路由中应用中间件
  router.get("/login", "user.login");
  router.post("/login", "user.handleLogin");
};

内置中间件

egg 提供了一些内置的中间件,可通过app.config.coreMiddlewares查看

这些内置中间件将会和自定义的中间件配置合并,最终形成一个真正的中间件函数数组:app.middleware,真正起作用的是该数组中的函数

横切关注点(AOP)


文章作者: Sunny
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Sunny !
  目录