《前端之路》— 重溫 Egg.js

《前端之路》— 重溫 Egg.js

在 nodejs 發展日益健壯和穩定的情況下,我們在日常的開發中使用 node 已經是一件非常常規的事情了,那麼對於我們必要的掌握一個服務端框架還是非常有必要的。下面我們就開始吧。

一、基礎功能

1.1、目錄結構

在了解目錄結構之前,我們需要對 mvc 的編程模式,進行一個理解。

  • M =》 Model(模型) 是處理應用程式數據邏輯的部分,主要是和資料庫打交道。
  • V =》 View(視圖) 是作為視圖展示使用,如果沒有這部分的需求的化, view 這一個層面的內容是可以被省略的。
  • C =》 Controller(控制器) 是應用程式中處理與用戶交互的部分,通常是為了 視圖需要的數據做準備,並向 Model 發送數據(get、post) 等
  • S =》 Service(服務) 在實際應用中,Controller 一般不會自己產出數據,也不會包含複雜的邏輯,複雜的過程應抽象為業務邏輯層 Service

但是在一些 mvc 框架的論壇中,也有一部分的聲音是說希望,業務層面的架構是一個 Thin Controller Fat Model and no need Service 那麼在這樣的一個出發點上來將的話,我們可以在後面實際的業務中去不斷嘗試。

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

目錄結構

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

1.2、內置對象

我們在看 egg 的內置對象的時候,我們先看下 koa 的內置對象,application、context、request、response。

  • 再對比下 egg 的內置對象: 包括從 Koa 繼承而來的 4 個對象(Application, Context, Request, Response) 以及框架擴展的一些對象(Controller, Service, Helper, Config, Logger)
1.2.1、Application

Application 是一個全局對象,上面掛載了這個框架常用到的 全局方法和對象。

1.2.2、Context

Context 是一個請求級別的對象,最常見的 Context 實例獲取方式是在 Middleware, Controller 以及 Service 中,例如:

// controller  class HomeController extends Controller {    async index() {      const { ctx } = this;      ctx.body = 'hi, egg, ss';    }  }
1.2.3、Request

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

// 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);    }  }    // 但是如果 需要注意的是,獲取 POST 的 body 應該使用 ctx.request.body,而不是 ctx.body. 所以我們在獲取 請求對應參數的時候均採用 ctx.request.query 或者 ctx.request.body 的方式.  
1.2.4、Response

Response 是一個請求級別的對象,繼承自 Koa.Response。封裝了 Node.js 原生的 HTTP Response 對象,提供了一系列輔助方法設置 HTTP 響應。

1.2.5、Controller

框架提供了一個 Controller 基類,這個基類包括了一些常用的屬性: ctx、app、config、service、logger 等

const Controller = require('egg').Controller;    class TestController extends Controller {      async index() {          const { ctx, app, config, service, logger } = this;          ctx.body = 'test';      }  }    module.exports = TestController;
1.2.6、Service

框架提供了一個 Service 基類, Service 基類的屬性和 Controller 基類屬性一致,訪問方式也類似

const Service = require('egg').Service;    class TestService extends Service {      index() {          const { ctx } = this;          return ctx.request.query;      }  }    module.exports = TestService;
1.2.7、Helper

Helper 用來提供一些實用的 utility 函數 ( 工具函數 ) ,可以在 Context 的實例上獲取到當前請求的 Helper(ctx.helper) 實例。 其中我們還可以自定義 helper 方法,如下:

// Demo  // app/extend/helper.js  module.exports = {      upperCase(str) {          return str.toUpperCase();      }  };
1.2.8、Config

我們可以通過 app.config 從 Application 實例上獲取到 config 對象,也可以在 Controller, Service, Helper 的實例上通過 this.config 獲取到 config 對象。 被創建出來的原則就是希望,配置和程式碼分離的原則。

1.2.9、Logger

日誌系統包含了 四大層面的 日誌對象, 分別是 App Logger、App CoreLogger、Context Logger、Context CoreLogger、Controller Logger & Service Logger

這些從 app、context 、logger、service 等各個層面都提供對應的 debug 方法,這些方法中包括

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

後需在開發中使用到,再具體介紹每一部分的功能。

1.3、運行環境

通過 config/env 文件指定對應的環境,但是一般推薦使用 構建工具來生成這個文件,那麼我們看下這個文件裡面的內容是什麼樣子的

//  config/env  prod  // 對,就是這麼簡單。

在 Koa 中我們通過 app.env 來進行環境判斷,app.env 默認的值是 process.env.NODE_ENV。但是在 Egg(和基於 Egg 的框架)中,配置統一都放置在 app.config 上,所以我們需要通過 app.config.env 來區分環境,app.env 不再使用。

1.4、配置

框架提供了強大且可擴展的配置功能,可以自動合併應用、插件、框架的配置,按順序覆蓋,且可以根據環境維護不同的配置。合併後的配置可直接從 app.config 獲取。

配置文件:

  • config.default.js —默認配置文件,一般也作為開發環境使用
  • config.prod.js — 生產環境配置文件,會覆蓋默認配置文件的同名配置
  • config.unittest.js — 單元測試, 測試環境
  • config.local.js — 本地開發環境,額外於 默認配置。

1.5、中間件

這裡簡單介紹下,如何編寫 中間件 和如何使用 中間件。

1.5.1、如何編寫中間件

從文件夾規則來說,我們編寫的自己的中間件一般都會放在 app/middleware/ xxx.js 具體的 Demo 如下:

// egg 的中間件  // 基於 洋蔥圈模型  // 和 koa 的中間件的構成差不多,只不過 egg 在外面包裹了一層  module.exports = options => {      return async function toLowerCase(ctx, next) {          await next();          const result = ctx.request.query;          ctx.body = result;      };  };
// koa 的中間件  function log(ctx) {      console.log(ctx.method, ctx.header.host);  }    module.exports = function() {      return async function(ctx, next) {          next();          await log(ctx);      };  };
1.5.2、如何使用中間件

在開始問如何使用中間件的時候,我們要清除一個問題就是我們往往在什麼時候需要用到中間件、怎麼用的問題,下面就簡單介紹下。

  • 在應用中使用中間件
// config.default.js  module.exports = {    // 配置需要的中間件,數組順序即為中間件的載入順序    middleware: [ 'gzip' ],      // 配置 gzip 中間件的配置    gzip: {      threshold: 1024, // 小於 1k 的響應體不壓縮    },  };  
  • 在框架和插件中使用中間件
// 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.coreMiddleware.unshift('report') 
  • router 中使用中間件
module.exports = app => {    const gzip = app.middleware.gzip({ threshold: 1024 });    app.router.get('/needgzip', gzip, app.controller.handler);  };
  • 框架本身自帶中間件
// 修改一些初始化的配置  // config/config.default.js  module.exports = {    bodyParser: {      jsonLimit: '10mb',    },  };
1.5.3、中間件的通用配置
  • enable 是否開啟使用中間件
  • match 設置只有符合某些規則的請求才會經過這個中間件
  • ignore 設置符合某些規則的請求不經過這個中間件
module.exports = {    bodyParser: {      enable: fasle   // 不開啟使用 bodyParser 中間件    },    gzip: {      match: '/static'   // 只壓縮 /static 目錄下的文件    },    apiAgent: {      ignore: /^/api/   // 只用 /api 的請求路徑才不會使用  apiAgent 中間件    }  };  

1.6、路由

Router 主要用來描述請求 URL 和具體承擔執行動作的 Controller 的對應關係, 框架約定了 app/router.js 文件用於統一所有路由規則

// demo  module.exports = app => {      const { router, controller } = app;      router.get('/', controller.home.index);      router.get('/news', controller.news.list);      router.get('/user/:id', controller.test.user);  };

router 提供的訪問方法

  • router.head – HEAD
  • router.options – OPTIONS
  • router.get – GET
  • router.put – PUT
  • router.post – POST
  • router.patch – PATCH
  • router.delete – DELETE
  • router.redirect — 這裡重點提示下 redirect 重定向,可以對 URL 進行重定向處理,比如我們最經常使用的可以把用戶訪問的根目錄路由到某個主頁。

router 有哪些使用方式?
主要列舉2中方式:

  • 最簡單的一種,直接 router.method(‘/url’, controller)
router.verb('path-match', app.controller.action);
  • 最複雜的一種方式
router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action);    // router-name 是指路徑別名  // middleware 是指 中間件  // path-match 是指 路由 url

如何使用 RESTful 風格的 URL 定義

// 定義  // router.js  router.resources('posts', '/api/posts', controller.posts);
// controller/posts.js  exports.index = async ctx => {      ctx.body = {          success: true,          data: [1, 2, 34]      };  };

訪問 http://127.0.0.1:7001/api/posts 即可

這裡需要仔細一點,查看關於 restful 的 使用方法,需要對 restful 有一個基本對理解。

二、總結

其實整篇文章,寫到這裡已經初步對於 egg 已經有了一個大概的了解了,那麼後續我猜應該還會有對應的實際的寫項目的文章吧(希望有時間和經歷來寫)

GitHub 地址:(歡迎 star 、歡迎推薦 : )
《前端之路》 – 重溫 EggJS