Egg框架知识点1.目录结构和具体内容

在进行项目之前,最应该了解的就是项目结构,了解每一个文件存放的地方,为后续的修改做准备。
在这篇文章中,结合项目中的经历和EGG框架的目录结构进行详细整理。

目录结构:

server(egg-project)
├── app
| ├── router.js
│ ├── controller
│ | └── home.js
│ ├── service (可选)
│ | └── user.js
│ ├── middleware (可选)
│ | └── accesslog.js
│ └── extend (可选)
│ ├── helper.js
│ ├── request.js
│ ├── response.js
│ ├── context.js
│ ├── application.js
│ └── agent.js
├── config
| ├── plugin.js
| ├── config.default.js(默认环境)
| ├── config.local.js(本地开发环境)
│ ├── config.prod.js(线上环境)
| ├── config.test.js (测试环境)
| └── config.stage.js (预发环境)
├── logs
├── node_modules
├── jcloud
├── run
├── test
| ├── app
| | └── response_time.test.js
| └── config
| └── config.default.js
| └── config.default.js
├── package.json
├── app.js
├── build.js
├── sever.js
目录结构说明:
1. app/router.js :用于配置 URL 路由规则
2. app/controller/** :用于解析用户的输入,处理后返回相应的结果
3. app/service/**:用于编写业务逻辑层,可选,建议使用
4. app/middleware/**: 用于编写中间件
5. app/public/** :用于放置静态资源,可选
6. app/extend/** :用于框架的扩展,可选
7. config/config.{ENV}.js:用于编写配置文件
8. config/plugin.js: 用于配置需要加载的插件
9. test/**:用于单元测试
10. app.js 和 agent.js:用于自定义启动时的初始化工作,可选
11. app/schedule/** 用于定时任务,可选

具体内容

1.app/router.js :

Router 主要用来描述请求 URL 和具体承担执行动作的 Controller 的对应关系, 框架约定了 app/router.js 文件用于统一所有路由规则。
定义router方法:
app/router.js 里面定义 URL 路由规则

// app/router.js
module.exports = app => {
  const { router, controller } = app
  router.get(`/user/:id`, controller.home.index)
}

app/controller 目录下面实现 Controller

// app/controller/home.js
const Controller = require(`egg`).Controller
class HomeController extends Controller {
  async index(ctx) {
    ctx.body = 'hello buddy'
  }
}

module.exports = HomeController

这样就完成了一个最简单的 Router 定义,当用户执行 GET /user/123,home.js 这个里面的 index 方法就会执行
备注:params.id中params在路由表示为/id,若改为qurey.id在路由表示为? id
下面是一些路由定义的方式:

// app/router.js
module.exports = app => {
 const { router, controller } = app;
router.get('/home', controller.home);
router.get('/user/:id', controller.user.page);
router.post('/admin', isAdmin, controller.admin);
router.post('/user', isLoginUser, hasAdminPermission, controller.user.create);
router.post('/api/v1/comments',controller.v1.comments.create); 
  // app/controller/v1/comments.js
};

如上诉所示,路由完整定义主要包括5个主要部分:

router.verb(‘router-name’, ‘path-match’, middleware1, …,middlewareN,
app.controller.action);

1.verb - 用户触发动作,支持 get,post 等所有 HTTP 方法
2.router-name 给路由设定一个别名,可以通过 Helper 提供的辅助函数 pathFor 和 urlFor 来生成 URL
3.path-match - 路由 URL 路径
4.middleware1 - 在 Router 里面可以配置多个 Middleware
5.controller - 指定路由映射到的具体的 controller 上,controller 可以有两种写法:
其中:app.controller.user.fetch - 直接指定一个具体的 controller
‘user.fetch’ - 可以简写为字符串形式
(但在我的项目中,主要的路由配置在Vue的框架里)

2.app/controller/ :**

Controller 负责解析用户的输入,处理后返回相应的结果,例如:
a.在 RESTful 接口中,Controller 接受用户的参数,从数据库中查找内容返回给用户或者将用户的请求更新到数据库中。
b.在 HTML 页面请求中,Controller 根据用户访问不同的 URL,渲染不同的模板得到 HTML 返回给用户。
c.在代理服务器中,Controller 将用户的请求转发到其他服务器上,并将其他服务器的处理结果返回给用户。

3.app/service/:**

在 Controller 中不想实现太多业务逻辑,便可以在 Service 层进行业务逻辑的封装,这不仅能提高代码的复用性,同时可以让我们的业务逻辑更好测试。
在 Controller 中可以调用任何一个 Service 上的任何方法,同时 Service 是懒加载的,只有当访问到它的时候框架才会去实例化它。
使用service有以下几个好处:
1.保持 Controller 中的逻辑更加简洁。
2.保持业务逻辑独立性,抽象出来的 Service 可以被多个 Controller 重复调用
现在通过一组代码,详细展现router、controller、service直接三者的关系:

// app/router.js
module.exports = app => {
  app.router.get('/user/:id', app.controller.user.info);
};

// app/controller/user.js
const Controller = require('egg').Controller;
class UserController extends Controller {
  async info() {
    const userId = ctx.params.id;
    const userInfo = await ctx.service.user.find(userId);
    ctx.body = userInfo;
  }
}
module.exports = UserController;

// app/service/user.js
const Service = require('egg').Service;
class UserService extends Service {
  // 默认不需要提供构造函数。
  // constructor(ctx) {
  //   super(ctx); 如果需要在构造函数做一些处理,一定要有这句话,才能保证后面 `this.ctx`的使用。
  //   // 就可以直接通过 this.ctx 获取 ctx 了
  //   // 还可以直接通过 this.app 获取 app 了
  // }
  async find(uid) {
    // 假如 我们拿到用户 id 从数据库获取用户详细信息
    const user = await this.ctx.db.query('select * from user where uid = ?', uid);

    // 假定这里还有一些复杂的计算,然后返回需要的信息。
    const picture = await this.getPicture(uid);

    return {
      name: user.user_name,
      age: user.age,
      picture,
    };
  }

  async getPicture(uid) {
    const result = await this.ctx.curl(`http://photoserver/uid=${uid}`, { dataType: 'json' });
    return result.data;
  }
}
module.exports = UserService;

// curl http://127.0.0.1:7001/user/1234

4.app/middleware/**

Egg 的中间件形式和 Koa 的中间件形式是一样的,都是基于洋葱圈模型。
配置:
一般来说中间件也有自己的配置。在框架中,一个完整的中间件是包含了配置处理的。约定一个中间件是一个放置在 app/middleware 目录下的单独文件,它需要 exports 一个普通的 function,接受两个参数:
options: 中间件的配置项,框架会将 app.config[${middlewareName}] 传递进来。
app: 当前应用 Application 的实例
使用中间件
中间件编写完成后,我们还需要手动挂载,支持以下方式:

在应用中使用中间件
在应用中,可以完全通过配置来加载自定义的中间件,并决定它们的顺序。如果我们加载中间件,在 config.default.js 中加入配置就可以完成中间件的开启和配置:

//app/middleware/accesslog.js

const util = require(`util`)

module.exports = (options, app) => {
  return async (ctx, next) => {
    const startTime = Date.now()
    const accessLogger = app.getLogger('accessLogger')
    // todo test
    const data = {
      xForwardedFor: ctx.header[app.config.ipHeaders] || ``,
      ip: ctx.ip || ``,
      method: ctx.method,
      url: ctx.url,
      host: ctx.host || `-`
    }
    if (ctx.acceptJSON) {
      data.body = JSON.stringify(ctx.request.body)
    }
  }
}
 // config.default.js 

module.exports = appInfo => {
  const config = {}
  // use for cookie sign key, should change to your own and keep security
  config.keys = `${appInfo.name}_1623503412105_8112`

  // 运营后台
  config.consoleBack = true

  // add your config here
  config.middleware = ['accesslog']
    config.cluster = {
    listen: {
      port: 3001,
      hostname: '0.0.0.0'
    }
  }
    return config
}

在框架和插件中使用中间件
框架和插件不支持在 config.default.js 中匹配 middleware,需要通过以下方式:

// app.js
module.exports = app => {
  // 在中间件最前面统计请求时间
  app.config.coreMiddleware.unshift('report');
};

// app/middleware/report.js
module.exports = () => {
  return async function (ctx, next) {
    const startTime = Date.now();
    await next();
    // 上报请求时间
    reportTime(Date.now() - startTime);
  }
};

应用层定义的中间件(app.config.appMiddleware)和框架默认中间件(app.config.coreMiddleware)都会被加载器加载,并挂载到 app.middleware 上

中间件过程中发现了下面一些问题

 1.中间件加载其实是有先后顺序的,但是中间件自身却无法管理这种顺序,只能交给使用者。这样其实非常不友好,一旦顺序不对,结果可能有天壤之别。
 2.中间件的定位是拦截用户请求,并在它前后做一些事情,例如:鉴权、安全检查、访问日志等等。但实际情况是,有些功能是和请求无关的,例如:定时任务、消息订阅、后台逻辑等等。
 3.有些功能包含非常复杂的初始化逻辑,需要在应用启动的时候完成。这显然也不适合放到中间件中去实现

正是由于以上问题,引出之后的插件

5.app/public/**

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

6.app/extend/**

框架扩展:
框架提供了多种扩展点扩展自身的功能:
Application
Context
Request
Response
Helper
在开发中,我们既可以使用已有的扩展 API 来方便开发,也可以对以上对象进行自定义扩展,进一步加强框架的功能
在项目中,是对Context进行扩展,便以此为例进行说明


Context
Context 指的是 Koa 的请求上下文,这是 请求级别 的对象,每次请求生成一个 Context 实例,通常也简写成 ctx。在所有的文档中,Context 和 ctx 都是指 Koa 的上下文对象。

访问方式
middleware 中 this 就是 ctx,例如 this.cookies.get(‘foo’)。
controller 有两种写法:
类的写法通过 this.ctx
方法的写法直接通过 ctx 入参。
helper,service 中的 this 指向 helper,service 对象本身,使用 this.ctx 访问 context 对象,例如 this.ctx.cookies.get(‘foo’)。
扩展方式
框架会把 app/extend/context.js 中定义的对象与 Koa Context 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 ctx 对象
方法扩展:
要给Context扩展一个ctx.bizCurl()方法:

// app/extend/context.js
const querystring = require(`querystring`)
module.exports = {
  async bizCurl({ url, param, opts = {} }) {
    opts.contentType = `json`
    opts.dataType = `json`
    opts.method = this.method
    if (param) {
      url += `?${querystring.stringfy(param)}`
    }

    try {
      this.app.logger.debug(`开始请求中间层`, url, opts)
      const result = await this.curl(url, opts)
      this.app.logger.debug(`中间层返回结果`, result.data)
      return result
    } catch (e) {
      this.app.logger.error(e)
      throw e
    }
  }
}

属性扩展
一般来说属性的计算在同一次请求中只需要进行一次,那么一定要实现缓存,否则在同一次请求中多次访问属性时会计算多次,这样会降低应用性能。

推荐的方式是使用 Symbol + Getter 的模式。
例如,增加一个 ctx.bar 属性 Getter:

// app/extend/context.js
const BAR = Symbol('Context#bar');

module.exports = {
  get bar() {
    // this 就是 ctx 对象,在其中可以调用 ctx 上的其他方法,或访问属性
    if (!this[BAR]) {
      // 例如,从 header 中获取,实际情况肯定更复杂
      this[BAR] = this.get('x-bar');
    }
    return this[BAR];
  }
}

同样的思路也适合application,Request,Response,Helper


application
访问方式
ctx.app
Controller,Middleware,Helper,Service 中都可以通过 this.app 访问到 Application 对象,例如 this.app.config 访问配置对象。
在 app.js 中 app 对象会作为第一个参数注入到入口函数中

扩展方式
框架会把 app/extend/application.js 中定义的对象与 Koa Application 的 prototype 对象进行合并,在应用启动时会基于扩展后的 prototype 生成 app 对象


request
Request 对象和 Koa 的 Request 对象相同,是 请求级别 的对象,它提供了大量请求相关的属性和方法供使用。

访问方式
ctx.request
ctx 上的很多属性和方法都被代理到 request 对象上,对于这些属性和方法使用 ctx 和使用 request 去访问它们是等价的,例如 ctx.url === ctx.request.url。
扩展方式
框架会把 app/extend/request.js 中定义的对象与内置 request 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 request 对象


Response
Response 对象和 Koa 的 Response 对象相同,是 请求级别 的对象,它提供了大量响应相关的属性和方法供使用。

访问方式
ctx.response
ctx 上的很多属性和方法都被代理到 response 对象上,对于这些属性和方法使用 ctx 和使用 response 去访问它们是等价的,例如 ctx.status = 404 和 ctx.response.status = 404 是等价的。

扩展方式
框架会把 app/extend/response.js 中定义的对象与内置 response 的 prototype 对象进行合并,在处理请求时会基于扩展后的 prototype 生成 response 对象。

7.config/config.{ENV}.js:

 config配置:egg框架提供了强大且可扩展的配置功能,可以自动合并应用、插件、框架的配置,**按顺序覆盖**,且可以**根据环境维护不同的配置**。(正是这个按顺序覆盖和根据环境维护不同配置,才使得我们在egg框架下可以配置多种环境,如默认环境,本地开发环境,测试环境,预发环境和线上环境)
 egg框架的配置管理方法是使用代码管理配置,也就是**配置即代码**,在代码中添加多个环境的配置,在启动时传入当前环境的参数即可。但无法全局配置,必须修改代码。
  **多环境配置**

框架支持根据环境来加载配置,定义多个环境的配置文件,例如我现在项目中的结构:
├── config
├── plugin.js
├── config.default.js(默认环境)
├── config.local.js(本地开发环境)
├── config.prod.js(线上环境)
├── config.test.js (测试环境)
└── config.stage.js (可选)

config.default.js 为默认的配置文件,所有环境都会加载这个配置文件,一般也会作为开发环境的默认配置文件。
当指定 env 时会同时加载对应的配置文件,并覆盖默认配置文件的同名配置。如 prod 环境会加载 config.prod.js 和 config.default.js 文件,config.prod.js 会覆盖 config.default.js 的同名配置。
(备注:在配置环境时候,在webstrom中可以在dev编辑栏进行编辑。)
在我这个项目中,在config.default.js里放入项目运行的路由端口,在config.local.js里放入项目的基本配置,包括网管接口等,在运行时候,直接运行local文件,在后期按照需要进行环境切换。
配置写法:
在官网中提及的配置写法有三种,我们采用第三种写法,
即配置文件也可以返回一个 function,可以接受 appInfo 参数:

// 将 logger 目录放到代码目录下
const path = require('path');
module.exports = appInfo => {
  return {
    logger: {
      dir: path.join(appInfo.baseDir, 'logs'),
    },
  };
};

配置结果
框架在启动时会把合并后的最终配置 dump 到 run/application_config.json(worker 进程)和 run/agent_config.json(agent 进程)中,可以用来分析问题。
配置文件中会隐藏一些字段,主要包括两类:
如密码、密钥等安全字段,这里可以通过 config.dump.ignore 配置,必须是 Set 类型。
如函数、Buffer 等类型,JSON.stringify 后的内容特别大,还会生成
run/application_config_meta.json(worker进程)和 run/agent_config_meta.json(agent 进程)文件,用来排查属性的来源,如

{
  "logger": {
    "dir": "/path/to/config/config.default.js"
  }
}

8.config/plugin.js:用于配置需要加载的插件

中间件、插件、应用的关系
一个插件其实就是一个『迷你的应用』,和应用(app)几乎一样:
它包含了 Service、中间件、配置、框架扩展等等。
它没有独立的 Router 和 Controller。
它没有 plugin.js,只能声明跟其他插件的依赖,而不能决定其他插件的开启与否。

他们的关系是:
应用可以直接引入 Koa 的中间件。
当遇到上一节提到的场景时,则应用需引入插件。
插件本身可以包含中间件。
多个插件可以包装为一个上层框架。

简而言之,插件就是一个小型应用,在主界面里通过添加插件可以实现一个完整的且逻辑复杂的功能,这样可以确保中间件简单易读。

9.test/**:用于单元测试

测试框架:Mocha
Egg选择和推荐大家使用 Mocha,功能非常丰富,支持运行在 Node.js 和浏览器中, 对异步测试支持非常友好。
断言库:power-assert
因为『No API is the best API』, 最终我们重新回归原始的 assert 作为默认的断言库
测试目录结构
约定 test 目录为存放所有测试脚本的目录,测试所使用到的 fixtures 和相关辅助脚本都应该放在此目录下。
测试脚本文件统一按 ${filename}.test.js 命名,必须以 .test.js 作为文件后缀。
运行工具
只需要在 package.json 上配置好 scripts.test 即可。

{
“scripts”: {
“test”: “egg-bin test”
}
}
然后就可以按标准的 npm test 来运行测试了

10.app.js 和 agent.js:用于自定义启动时的初始化工作,可选

我们常常需要在应用启动期间进行一些初始化工作,等初始化完成后应用才可以启动成功,并开始对外提供服务。

框架提供了统一的入口文件(app.js)进行启动过程自定义,这个文件只返回一个函数。

const webpackMiddleware = require('koa-webpack')

module.exports = app => {
  app.beforeStart(async () => {
    if (app.config.webpack) {
      const md = await webpackMiddleware(app.config.webpack)
      app.use(md)
    }
  })
}

11.app/schedule/** 用于定时任务,可选

虽然我们通过框架开发的 HTTP Server 是请求响应模型的,但是仍然还会有许多场景需要执行一些定时任务,例如:

定时上报应用状态。
定时从远程接口更新本地缓存。
定时进行文件切割、临时文件删除。
框架提供了一套机制来让定时任务的编写和维护更加优雅。

编写定时任务
所有的定时任务都统一存放在 app/schedule 目录下,每一个文件都是一个独立的定时任务,可以配置定时任务的属性和要执行的方法。
(在项目中没有使用过这个功能,之后用到再补充)

12.build.js

console.log(`node got EGG_SERVER_ENV is ${process.env.EGG_SERVER_ENV}`)
console.log(`node got NODE_ENV is ${process.env.NODE_ENV}`)
const assert = require('assert')
const mm = require('egg-mock')
const fs = require('fs')

mm.env(process.env.EGG_SERVER_ENV || 'prod')
mm.consoleLevel('DEBUG')

let app = mm.app() // 创建当前应用的 app 实例
app.ready().then(  // 等待 app 启动成功,才能执行测试用例
  function() {
    const config = JSON.parse(
      fs.readFileSync(app.config.rundir + '/webpack_config_alias.json', 'utf8')
    )
    app.close().then(function() {
      console.log('app closed')
      process.nextTick(function() {
        assert.ok(!!config.bizUrl)
        process.exit(0)
      })
    })
  },
  function(error) {
    console.log(error.message)
    process.exit(-1)
  }
)

13.sever.js

const startCluster = require('egg').startCluster
startCluster(
  {
    baseDir: __dirname
  },
  () => {
    console.log('app started')
  }
)

你可能感兴趣的:(Egg)