一文看懂 Eggjs-基礎全面講解(上)

Egg 繼承於 Koa,Egg 選擇了 Koa 做爲其基礎框架,在它的模型基礎上,進一步對它進行了一些加強。javascript

執行流程分析

Koa 的中間件選擇了洋蔥圈模型。css

中間件洋蔥圖

全部的請求通過一箇中間件的時候都會執行兩次,對比 Express 形式的中間件,Koa 的模型能夠很是方便的實現後置處理邏輯,能夠看到執行是從前到後再從後到前。html

中間件執行順序圖 java

中間件執行順序圖

目錄結構分析

egg-project
├── package.json
├── app.js (可選)
├── agent.js (可選)
├── app
|   ├── router.js
│   ├── controller
│   |   └── home.js
│   ├── service (可選)
│   |   └── user.js
│   ├── middleware (可選)
│   |   └── response_time.js
│   ├── schedule (可選)
│   |   └── my_task.js
│   ├── public (可選)
│   |   └── reset.css
│   ├── view (可選)
│   |   └── home.tpl
│   └── extend (可選)
│       ├── helper.js (可選)
│       ├── request.js (可選)
│       ├── response.js (可選)
│       ├── context.js (可選)
│       ├── application.js (可選)
│       └── agent.js (可選)
├── config
|   ├── plugin.js
|   ├── config.default.js
│   ├── config.prod.js
|   ├── config.test.js (可選)
|   ├── config.local.js (可選)
|   └── config.unittest.js (可選)
└── test
    ├── middleware
    |   └── response_time.test.js
    └── controller
        └── home.test.js
複製代碼

由框架約定的目錄:web

  • app/router.js 用於配置 URL 路由規則
  • app/controller/** 用於解析用戶的輸入,處理後返回相應的結果
  • app/service/** 用於編寫業務邏輯層,可選
  • app/middleware/** 用於編寫中間件,可選
  • app/public/** 用於放置靜態資源,可選
  • app/extend/** 用於框架的擴展,可選
  • config/config.{env}.js 用於編寫配置文件
  • config/plugin.js 用於配置須要加載的插件
  • test/** 用於單元測試
  • app.jsagent.js 用於自定義啓動時的初始化工做,可選

由內置插件約定的目錄:json

  • app/public/** 用於放置靜態資源,可選
  • app/schedule/** 用於定時任務

文件加載順序

Egg 將應用、框架和插件都稱爲加載單元(loadUnit)bash

文件 應用 框架 插件
package.json ✔︎ ︎ ✔
config/plugin.{env}.js ✔︎ ✔︎
config/config.{env}.js ✔︎ ✔︎ ✔︎
app/extend/application.js ✔︎ ︎ ✔︎
app/extend/request.js ✔︎ ✔︎ ✔︎
app/extend/response.js ✔︎ ✔︎ ✔︎
app/extend/context.js ✔︎ ✔︎ ✔︎
app/extend/helper.js ✔︎ ✔︎ ✔︎
agent.js ✔︎ ✔︎ ✔︎
app.js ✔︎ ✔︎ ✔︎
app/service ✔︎ ✔︎ ✔︎
app/middleware ✔︎ ✔︎ ✔︎
app/controller ✔︎
app/router.js ✔︎

文件按表格內的順序自上而下加載,Egg 會遍歷全部的 loadUnit 加載上述的文件(應用、框架、插件各有不一樣),加載時有必定的優先級。websocket

  • 按插件 => 框架 => 應用依次加載
  • 插件之間的順序由依賴關係決定,被依賴方先加載,無依賴按 object key 配置順序加載
  • 框架按繼承順序加載,越底層越先加載

生命週期

  • 配置文件即將加載,這是最後動態修改配置的時機(configWillLoad
  • 配置文件加載完成(configDidLoad
  • 文件加載完成(didLoad
  • 插件啓動完畢(willReady
  • worker 準備就緒(didRead
  • 應用啓動完成(serverDidReady
  • 應用即將關閉(beforeClose

定義以下:app

// app.js or agent.js
class AppBootHook {
  constructor(app) {
    this.app = app;
  }
  configWillLoad() {
    // Ready to call configDidLoad,
    // Config, plugin files are referred,
    // this is the last chance to modify the config.
    console.log('configWillLoad');
  }
  configDidLoad() {
    // Config, plugin files have been loaded.
    console.log('configDidLoad');
  }
  async didLoad() {
    // All files have loaded, start plugin here.
    console.log('didLoad');
  }
  async willReady() {
    // All plugins have started, can do some thing before app ready
    console.log('willReady');
  }
  async didReady() {
    // Worker is ready, can do some things
    // don't need to block the app boot.
    console.log('didReady');
  }
  async serverDidReady() {
    // Server is listening.
    console.log('serverDidReady');
  }
  async beforeClose() {
    // Do some thing before app close.
    console.log('configWillLoad');
  }
}
module.exports = AppBootHook;
複製代碼

啓動時候執行的順序 框架

框架內置基礎對象*

doc

框架中內置的一些基礎對象,包括從 Koa 繼承而來的 4 個對象

  • Application
  • Context
  • Request
  • Response

以及框架擴展的一些對象

  • Controller
  • Service
  • Helper
  • Config
  • Logger

Application

Application 是全局應用對象,在一個應用中,只會實例化一個,它繼承自 Koa.Application,在它上面咱們能夠掛載一些全局的方法和對象。

事件

  • server 該事件一個 worker 進程只會觸發一次,在 HTTP 服務完成啓動後,會將 HTTP server 經過這個事件暴露出來給開發者
  • error 運行時有任何的異常被 onerror 插件捕獲後,都會觸發 error 事件
  • requestresponse 應用收到請求和響應請求時,分別會觸發 request 和 response 事件
// app.js
module.exports = app => {
  app.once('server', server => {
    // websocket
    console.log('server', server);
    
  });
  app.on('error', (err, ctx) => {
    // report error
    console.log('error', err);
  });
  app.on('request', ctx => {
    // log receive request
    console.log('request');
  });
  app.on('response', ctx => {
    // ctx.starttime is set by framework
    const used = Date.now() - ctx.starttime;
    // log total cost
    console.log('used', used);
  });
};

// 初始化的時候會打印 server Server 
// request
// used 6
// request
// used 1
複製代碼

獲取方式

幾乎全部被框架 Loader 加載的文件(Controller,Service,Schedule 等),均可以 export 一個函數,這個函數會被 Loader 調用,並使用 app 做爲參數

  • 啓動自定義腳本
// app.js
module.exports = app => {
  app.cache = new Cache();
};
複製代碼
  • Controller 文件
// app/controller/user.js
class UserController extends Controller {
  async fetch() {
    this.ctx.body = this.app.cache.get(this.ctx.query.id);
  }
}
複製代碼

和 Koa 同樣,在 Context 對象上,能夠經過 ctx.app 訪問到 Application 對象。也就是說

結果爲 true

// app/controller/user.js
class UserController extends Controller {
  async fetch() {
    this.ctx.body = this.ctx.app.cache.get(this.ctx.query.id);
  }
}
複製代碼

Context

Context 是一個請求級別的對象,繼承自 Koa.Context

在每一次收到用戶請求時,框架會實例化一個 Context 對象,這個對象封裝了此次用戶請求的信息,並提供了許多便捷的方法來獲取請求參數或者設置響應信息

獲取方式

// Koa v1
function* middleware(next) {
  // this is instance of Context
  console.log(this.query);
  yield next;
}

// Koa v2
async function middleware(ctx, next) {
  // ctx is instance of Context
  console.log(ctx.query);
}
複製代碼

除了在請求時能夠獲取 Context 實例以外, 在有些非用戶請求的場景下咱們須要訪問 service / model 等 Context 實例上的對象,能夠用Application.createAnonymousContext() 方法建立一個匿名 Context 實例。

// app.js
module.exports = app => {
  app.beforeStart(async () => {
    const ctx = app.createAnonymousContext();
    // preload before app start
    await ctx.service.posts.load();
  });
}
複製代碼

在定時任務中的每個 task 都接受一個 Context 實例做爲參數

// app/schedule/refresh.js
exports.task = async ctx => {
  await ctx.service.posts.refresh();
};
複製代碼

Request & Response

  • Request 是一個請求級別的對象,繼承自 Koa.Request。封裝了 Node.js 原生的 HTTP Request 對象,提供了一系列輔助方法獲取 HTTP 請求經常使用參數
  • Response 是一個請求級別的對象,繼承自 Koa.Response。封裝了 Node.js 原生的 HTTP Response 對象,提供了一系列輔助方法設置 HTTP 響應

獲取方式

能夠在 Context 的實例上獲取到當前請求的 Request(ctx.request) 和 Response(ctx.response) 實例。

// app/controller/user.js
class UserController extends Controller {
  async fetch() {
    const { app, ctx } = this;
    const id = ctx.request.query.id;
    ctx.response.body = app.cache.get(id);
  }
}
複製代碼
  • Koa 會在 Context 上代理一部分 Request 和 Response 上的方法和屬性,參見 Koa.Context
  • 如上面例子中的 ctx.request.query.idctx.query.id 是等價的,ctx.response.body=ctx.body= 是等價的
  • 須要注意的是,獲取 POST 的 body 應該使用 ctx.request.body,而不是 ctx.body

Controller

框架提供了一個 Controller 基類,並推薦全部的 Controller 都繼承於該基類實現。

這個 Controller 基類有下列屬性:

  • ctx - 當前請求的 Context 實例
  • app - 應用的 Application 實例
  • config - 應用的配置
  • service - 應用全部的 service
  • logger - 爲當前 controller 封裝的 logger 對象

在 Controller 文件中,能夠經過兩種方式來引用 Controller 基類:

// app/controller/user.js

// 從 egg 上獲取(推薦)
const Controller = require('egg').Controller;
class UserController extends Controller {
  // implement
}
module.exports = UserController;

// 從 app 實例上獲取
module.exports = app => {
  return class UserController extends app.Controller {
    // implement
  };
};
複製代碼

Service

框架提供了一個 Service 基類,並推薦全部的 Service 都繼承於該基類實現。

Service 基類的屬性和 Controller 基類屬性一致,訪問方式也相似:

// app/service/user.js

// 從 egg 上獲取(推薦)
const Service = require('egg').Service;
class UserService extends Service {
  // implement
}
module.exports = UserService;

// 從 app 實例上獲取
module.exports = app => {
  return class UserService extends app.Service {
    // implement
  };
};
複製代碼

Helper

Helper 用來提供一些實用的 utility 函數。它的做用在於咱們能夠將一些經常使用的動做抽離在 helper.js 裏面成爲一個獨立的函數,這樣能夠用 JavaScript 來寫複雜的邏輯,避免邏輯分散各處,同時能夠更好的編寫測試用例。

Helper 自身是一個類,有和 Controller 基類同樣的屬性,它也會在每次請求時進行實例化,所以 Helper 上的全部函數也能獲取到當前請求相關的上下文信息。

獲取方式

能夠在 Context 的實例上獲取到當前請求的 Helper(ctx.helper) 實例。

// app/controller/user.js
class UserController extends Controller {
  async fetch() {
    const { app, ctx } = this;
    const id = ctx.query.id;
    const user = app.cache.get(id);
    ctx.body = ctx.helper.formatUser(user);
  }
}
複製代碼

自定義 helper 方法

// app/extend/helper.js
module.exports = {
  formatUser(user) {
    return only(user, [ 'name', 'phone' ]);
  }
};
複製代碼

Config

推薦應用開發遵循配置和代碼分離的原則,將一些須要硬編碼的業務配置都放到配置文件中,同時配置文件支持各個不一樣的運行環境使用不一樣的配置,使用起來也很是方便,全部框架、插件和應用級別的配置均可以經過 Config 對象獲取到。

獲取方式

經過 app.config 從 Application 實例上獲取到 config 對象,也能夠在 Controller, Service, Helper 的實例上經過 this.config 獲取到 config 對象。

Logger

每個 logger 對象都提供了 4 個級別的方法:

  • logger.debug()
  • logger.info()
  • logger.warn()
  • logger.error()

獲取方式

  • app.logger 若是咱們想作一些應用級別的日誌記錄,如記錄啓動階段的一些數據信息,記錄一些業務上與請求無關的信息,均可以經過 App Logger 來完成
  • app.coreLogger 在開發應用時都不該該經過 CoreLogger 打印日誌,而框架和插件則須要經過它來打印應用級別的日誌,這樣能夠更清晰的區分應用和框架打印的日誌,經過 CoreLogger 打印的日誌會放到和 Logger 不一樣的文件中
  • ctx.logger 從 Context 實例上獲取到它,從訪問方式上咱們能夠看出來,Context Logger 必定是與請求相關的,它打印的日誌都會在前面帶上一些當前請求相關的信息(如[$userId/$ip/$traceId/${cost}ms $method $url])
  • this.logger 能夠在 Controller 和 Service 實例上經過 this.logger 獲取到它們,它們本質上就是一個 Context Logger,不過在打印日誌的時候還會額外的加上文件路徑
相關文章
相關標籤/搜索