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

回顧一下上篇講到的內容,上篇講了:css

運行環境

一個 Web 應用自己應該是無狀態的,並擁有根據運行環境設置自身的能力。html

指定運行環境

  • 經過 config/env 文件指定,該文件的內容就是運行環境,如 prod
  • 經過 EGG_SERVER_ENV 環境變量指定

方式 2 比較經常使用,由於經過 EGG_SERVER_ENV 環境變量指定運行環境更加方便,好比在生產環境啓動應用:node

EGG_SERVER_ENV=prod npm start
複製代碼

應用內獲取運行環境

使用 app.config.env 獲取ios

與 NODE_ENV 的區別

框架默認支持的運行環境及映射關係(若是未指定 EGG_SERVER_ENV 會根據 NODE_ENV 來匹配)git

NODE_ENV EGG_SERVER_ENV 說明
local 本地開發環境
test unittest 單元測試
production prod 生產環境

NODE_ENVproductionEGG_SERVER_ENV 未指定時,框架會將 EGG_SERVER_ENV 設置成 prodgithub

自定義環境

常規開發流程可能不只僅只有以上幾種環境,Egg 支持自定義環境來適應本身的開發流程。web

好比,要爲開發流程增長集成測試環境 SIT。將 EGG_SERVER_ENV 設置成 sit(並建議設置 NODE_ENV = production),啓動時會加載 config/config.sit.js,運行環境變量 app.config.env 會被設置成 sitnpm

與 Koa 的區別

在 Koa 中咱們經過 app.env 來進行環境判斷,app.env 默認的值是 process.env.NODE_ENVjson

在 Egg(和基於 Egg 的框架)中,配置統一都放置在 app.config 上,因此咱們須要經過 app.config.env 來區分環境,app.env 再也不使用。後端

Config 配置

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

Egg 選擇了配置即代碼,配置的變動也應該通過 review 後才能發佈。應用包自己是能夠部署在多個環境的,只須要指定運行環境便可。

多環境配置

框架支持根據環境來加載配置,定義多個環境的配置文件

config
|- config.default.js
|- config.prod.js
|- config.unittest.js
|- config.local.js
複製代碼

config.default.js 爲默認的配置文件,全部環境都會加載這個配置文件,通常也會做爲開發環境的默認配置文件。

當指定 env 時會同時加載對應的配置文件,並覆蓋默認配置文件的同名配置。如 prod 環境會加載 config.prod.jsconfig.default.js 文件,config.prod.js 會覆蓋 config.default.js 的同名配置。

配置寫法

配置文件返回的是一個 object 對象,能夠覆蓋框架的一些配置,應用也能夠將本身業務的配置放到這裏方便管理,獲取時直接經過 app.config

導出對象式寫法

// 配置 logger 文件的目錄,logger 默認配置由框架提供
module.exports = {
  logger: {
    dir: '/home/admin/logs/demoapp',
  },
};
複製代碼

配置文件也能夠返回一個 function,能夠接受 appInfo 參數

// 將 logger 目錄放到代碼目錄下
const path = require('path');
module.exports = appInfo => {
  return {
    logger: {
      dir: path.join(appInfo.baseDir, 'logs'),
    },
  };
};
複製代碼

內置的 appInfo 有:

appInfo 說明
pkg package.json
name 應用名,同 pkg.name
baseDir 應用代碼的目錄
HOME 用戶目錄,如 admin 帳戶爲 /home/admin
root 應用根目錄,只有在 local 和 unittest 環境下爲 baseDir,其餘都爲 HOME。

appInfo.root 是一個優雅的適配,好比在服務器環境咱們會使用 /home/admin/logs 做爲日誌目錄,而本地開發時又不想污染用戶目錄,這樣的適配就很好解決這個問題。

合併規則

配置的合併使用 extend2 模塊進行深度拷貝

const a = {
  arr: [ 1, 2 ],
};
const b = {
  arr: [ 3 ],
};
extend(true, a, b);
// => { arr: [ 3 ] }
// 框架直接覆蓋數組而不是進行合併。
複製代碼

配置結果

框架在啓動時會把合併後的最終配置 dump 到 run/application_config.json(worker 進程)和 run/agent_config.json(agent 進程)中,能夠用來分析問題。

中間件(Middleware)

Egg 是基於 Koa 實現的,因此 Egg 的中間件形式和 Koa 的中間件形式是同樣的,都是基於洋蔥圈模型。每次咱們編寫一箇中間件,就至關於在洋蔥外面包了一層。

編寫中間件

寫法

中間件內容的寫法

// app/middleware/gzip.js
const isJSON = require('koa-is-json');
const zlib = require('zlib');

async function gzip(ctx, next) {
  await next();

  // 後續中間件執行完成後將響應體轉換成 gzip
  let body = ctx.body;
  if (!body) return;
  if (isJSON(body)) body = JSON.stringify(body);

  // 設置 gzip body,修正響應頭
  const stream = zlib.createGzip();
  stream.end(body);
  ctx.body = stream;
  ctx.set('Content-Encoding', 'gzip');
}
複製代碼

框架的中間件和 Koa 的中間件寫法是如出一轍的,因此任何 Koa 的中間件均可以直接被框架使用。

配置

通常來講中間件也會有本身的配置。

咱們約定一箇中間件是一個放置在 app/middleware 目錄下的單獨文件,它須要 exports 一個普通的 function,接受兩個參數:

  • options: 中間件的配置項,框架會將 app.config[${middlewareName}] 傳遞進來,因此能夠直接獲取到配置文件中的中間件配置
  • app: 當前應用 Application 的實例

將上面的 gzip 中間件作一個簡單的優化,讓它支持指定只有當 body 大於配置的 threshold 時才進行 gzip 壓縮,咱們要在 app/middleware 目錄下新建一個文件 gzip.js

// app/middleware/gzip.js
const isJSON = require('koa-is-json');
const zlib = require('zlib');

module.exports = options => {
  return async function gzip(ctx, next) {
    await next();

    // 後續中間件執行完成後將響應體轉換成 gzip
    let body = ctx.body;
    if (!body) return;

    // 支持 options.threshold
    if (options.threshold && ctx.length < options.threshold) return;

    if (isJSON(body)) body = JSON.stringify(body);

    // 設置 gzip body,修正響應頭
    const stream = zlib.createGzip();
    stream.end(body);
    ctx.body = stream;
    ctx.set('Content-Encoding', 'gzip');
  };
};
複製代碼

使用中間件

中間件編寫完成後,咱們還須要手動掛載

在應用中使用中間件

config.default.js 中加入下面的配置就完成了中間件的開啓和配置

module.exports = {
  // 配置須要的中間件,數組順序即爲中間件的加載順序
  middleware: [ 'gzip' ],

  // 配置 gzip 中間件的配置
  gzip: {
    threshold: 1024, // 小於 1k 的響應體不壓縮
  },
  // options.gzip.threshold
};
複製代碼

該配置最終將在啓動時合併到 app.config.appMiddleware

在框架和插件中使用中間件

框架和插件不支持在 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 上。

router 中使用中間件

以上兩種方式配置的中間件是全局的,會處理每一次請求

若是你只想針對單個路由生效,能夠直接在 app/router.js 中實例化和掛載,以下:

module.exports = app => {
 const gzip = app.middleware.gzip({ threshold: 1024 });
 app.router.get('/needgzip', gzip, app.controller.handler);
};
複製代碼

框架默認中間件

除了應用層加載中間件以外,框架自身和其餘的插件也會加載許多中間件。

全部的這些自帶中間件的配置項都經過在配置中修改中間件同名配置項進行修改。

框架自帶的中間件中有一個 bodyParser 中間件(框架的加載器會將文件名中的各類分隔符都修改爲駝峯形式的變量名),咱們想要修改 bodyParser 的配置,只須要在 config/config.default.js 中編寫

module.exports = {
  bodyParser: {
    jsonLimit: '10mb',
  },
};
複製代碼

使用 Koa 的中間件

在框架裏面能夠很是容易的引入 Koa 中間件生態。

以 koa-compress 爲例,在 Koa 中使用時:

const koa = require('koa');
const compress = require('koa-compress');

const app = koa();

const options = { threshold: 2048 };
app.use(compress(options));
複製代碼

Egg

// app/middleware/compress.js
// koa-compress 暴露的接口(`(options) => middleware`)和框架對中間件要求一致
module.exports = require('koa-compress');
複製代碼

配置中間件

// config/config.default.js
module.exports = {
  middleware: [ 'compress' ],
  compress: {
    threshold: 2048,
  },
}
複製代碼

通用配置

  • enable:控制中間件是否開啓
  • match:設置只有符合某些規則的請求才會通過這個中間件
  • ignore:設置符合某些規則的請求不通過這個中間件

match 和 ignore

match 和 ignore 支持的參數都同樣,只是做用徹底相反,match 和 ignore 不容許同時配置。

module.exports = {
  gzip: {
    match: '/static',
  },
};

module.exports = {
  gzip: {
    match(ctx) {
      // 只有 ios 設備纔開啓
      const reg = /iphone|ipad|ipod/i;
      return reg.test(ctx.get('user-agent'));
    },
  },
};
複製代碼

匹配規則,摘自 egg-path-matching

const pathMatching = require('egg-path-matching');
const options = {
  ignore: '/api', // string will use parsed by path-to-regexp
  // support regexp
  ignore: /^\/api/,
  // support function
  ignore: ctx => ctx.path.startsWith('/api'),
  // support Array
  ignore: [ ctx => ctx.path.startsWith('/api'), /^\/foo$/, '/bar'],
  // support match or ignore
  match: '/api',
};
複製代碼

路由

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.user.info);
};
複製代碼
  • app/controller 目錄下面實現 Controller
// app/controller/user.js
class UserController extends Controller {
  async info() {
    const { ctx } = this;
    ctx.body = {
      name: `hello ${ctx.params.id}`,
    };
  }
}
複製代碼

Router 詳細定義說明

下面是路由的完整定義,參數能夠根據場景的不一樣,自由選擇:

router.verb('path-match', app.controller.action);
router.verb('router-name', 'path-match', app.controller.action);
router.verb('path-match', middleware1, ..., middlewareN, app.controller.action);
router.verb('router-name', 'path-match', middleware1, ..., middlewareN, app.controller.action);
複製代碼

路由完整定義主要包括5個主要部分:

  • verb - 用戶觸發動做,支持 get,post 等全部 HTTP 方法,後面會經過示例詳細說明。
    • router.head - HEAD
    • router.options - OPTIONS
    • router.get - GET
    • router.put - PUT
    • router.post - POST
    • router.patch - PATCH
    • router.delete - DELETE
    • router.del - 因爲 delete 是一個保留字,因此提供了一個 delete 方法的別名。
    • router.redirect - 能夠對 URL 進行重定向處理,好比咱們最常用的能夠把用戶訪問的根目錄路由到某個主頁。
  • router-name 給路由設定一個別名,能夠經過 Helper 提供的輔助函數 pathFor 和 urlFor 來生成 URL。(可選)
  • path-match - 路由 URL 路徑
  • middleware1...middlewareN - 在 Router 裏面能夠配置多個 Middleware。(可選),指定該 URL 路徑只通過這些 middleware 處理
  • controller - 指定路由映射到的具體的 controller 上,controller 能夠有兩種寫法:
    • app.controller.user.fetch - 直接指定一個具體的 controller
    • 'user.fetch' - 能夠簡寫爲字符串形式

注意事項

  • 在 Router 定義中, 能夠支持多個 Middleware 串聯執行
  • Controller 必須定義在 app/controller 目錄中
  • 一個文件裏面也能夠包含多個 Controller 定義,在定義路由的時候,能夠經過 ${fileName}.${functionName} 的方式指定對應的 Controller。
  • Controller 支持子目錄,在定義路由的時候,能夠經過 ${directoryName}.${fileName}.${functionName} 的方式制定對應的 Controller。

demo

// 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
};
複製代碼

RESTful 風格的 URL 定義

提供了 app.resources('routerName', 'pathMatch', controller) 快速在一個路徑上生成 CRUD 路由結構。

// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.resources('posts', '/api/posts', controller.posts);
  router.resources('users', '/api/v1/users', controller.v1.users); // app/controller/v1/users.js
};
複製代碼

上面代碼就在 /posts 路徑上部署了一組 CRUD 路徑結構,對應的 Controller 爲 app/controller/posts.js 接下來, 你只須要在 posts.js 裏面實現對應的函數就能夠了。

Method Path Route Name Controller.Action
GET /posts posts app.controllers.posts.index
GET /posts/new new_post app.controllers.posts.new
GET /posts/:id post app.controllers.posts.show
GET /posts/:id/edit edit_post app.controllers.posts.edit
POST /posts posts app.controllers.posts.create
PUT /posts/:id post app.controllers.posts.update
DELETE /posts/:id post app.controllers.posts.destroy
// app/controller/posts.js
exports.index = async () => {};

exports.new = async () => {};

exports.create = async () => {};

exports.show = async () => {};

exports.edit = async () => {};

exports.update = async () => {};

exports.destroy = async () => {};
複製代碼

若是咱們不須要其中的某幾個方法,能夠不用在 posts.js 裏面實現,這樣對應 URL 路徑也不會註冊到 Router。

router 實戰

參數獲取

Query String 方式

ctx.query.xxx

// app/router.js
module.exports = app => {
  app.router.get('/search', app.controller.search.index);
};

// app/controller/search.js
exports.index = async ctx => {
  ctx.body = `search: ${ctx.query.name}`;
};

// curl http://127.0.0.1:7001/search?name=egg
複製代碼

參數命名方式

/user/:id/:name => ctx.params.xxx

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

// app/controller/user.js
exports.info = async ctx => {
  ctx.body = `user: ${ctx.params.id}, ${ctx.params.name}`;
};

// curl http://127.0.0.1:7001/user/123/xiaoming
複製代碼

複雜參數的獲取

路由裏面也支持定義正則,能夠更加靈活的獲取參數:

// app/router.js
module.exports = app => {
  app.router.get(/^\/package\/([\w-.]+\/[\w-.]+)$/, app.controller.package.detail);
};

// app/controller/package.js
exports.detail = async ctx => {
  // 若是請求 URL 被正則匹配, 能夠按照捕獲分組的順序,從 ctx.params 中獲取。
  // 按照下面的用戶請求,`ctx.params[0]` 的 內容就是 `egg/1.0.0`
  ctx.body = `package:${ctx.params[0]}`;
};

// curl http://127.0.0.1:7001/package/egg/1.0.0
複製代碼

表單內容的獲取

// app/router.js
module.exports = app => {
  app.router.post('/form', app.controller.form.post);
};

// app/controller/form.js
exports.post = async ctx => {
  ctx.body = `body: ${JSON.stringify(ctx.request.body)}`;
};
複製代碼

這裏直接發起 POST 請求會報錯。

上面的校驗是由於框架中內置了安全插件 egg-security,提供了一些默認的安全實踐,而且框架的安全插件是默認開啓的,若是須要關閉其中一些安全防範,直接設置該項的 enable 屬性爲 false 便可。

暫時關閉 csrf

exports.security = {
  csrf: false
};
複製代碼

表單校驗

npm i -S egg-validate
複製代碼
// config/plugin.js
module.exports = {
  // had enabled by egg
  // static: {
  // enable: true,
  // }
  validate: {
    enable: true,
    package: 'egg-validate',
  },
};
複製代碼
// app/router.js
module.exports = app => {
  app.router.resources('/user', app.controller.user);
};

// app/controller/user.js
const createRule = {
  username: {
    type: 'email',
  },
  password: {
    type: 'password',
    compare: 're-password',
  },
};

exports.create = async ctx => {
  // 若是校驗報錯,會拋出異常
  ctx.validate(createRule);
  ctx.body = ctx.request.body;
};
複製代碼

重定向

內部重定向

// app/router.js
module.exports = app => {
  app.router.get('index', '/home/index', app.controller.home.index);
  app.router.redirect('/', '/home/index', 303); // 訪問 / 自動重定向到 /home/index
};

// app/controller/home.js
exports.index = async ctx => {
  ctx.body = 'hello controller';
};
複製代碼

外部重定向

exports.index = async ctx => {
  const type = ctx.query.type;
  const q = ctx.query.q || 'nodejs';

  if (type === 'bing') {
    ctx.redirect(`http://cn.bing.com/search?q=${q}`);
  } else {
    ctx.redirect(`https://www.google.co.kr/search?q=${q}`);
  }
};
複製代碼

控制器(Controller)

Controller 負責解析用戶的輸入,處理後返回相應的結果。

框架推薦 Controller 層主要對用戶的請求參數進行處理(校驗、轉換),而後調用對應的 service 方法處理業務,獲得業務結果後封裝並返回

  • 獲取用戶經過 HTTP 傳遞過來的請求參數
  • 校驗、組裝參數
  • 調用 Service 進行業務處理,必要時處理轉換 Service 的返回結果,讓它適應用戶的需求
  • 經過 HTTP 將結果響應給用戶

如何編寫 Controller

全部的 Controller 文件都必須放在 app/controller 目錄下,能夠支持多級目錄,訪問的時候能夠經過目錄名級聯訪問

Controller 類(推薦)

// app/controller/post.js
const Controller = require('egg').Controller;
class PostController extends Controller {
  async create() {
    const { ctx } = this;
    
    ctx.body = 'PostController';
    ctx.status = 201;
  }
}
module.exports = PostController;
複製代碼

上面定義的方法,能夠在路由中經過 app.controller 根據文件名和方法名定位到它

// app/router.js
module.exports = app => {
  const { router, controller } = app;
  router.post('createPost', '/api/posts', controller.post.create);
}
複製代碼

Controller 支持多級目錄,例如若是咱們將上面的 Controller 代碼放到 app/controller/sub/post.js 中,則

app.router.post('createPost', '/api/posts', app.controller.sub.post.create);
複製代碼

屬性介紹 定義的 Controller 類,會在每個請求訪問到 server 時實例化一個全新的對象,而項目中的 Controller 類繼承於 egg.Controller,會有下面幾個屬性掛在 this 上。

  • this.ctx: 當前請求的上下文 Context 對象的實例,經過它咱們能夠拿到框架封裝好的處理當前請求的各類便捷屬性和方法。
  • this.app: 當前應用 Application 對象的實例,經過它咱們能夠拿到框架提供的全局對象和方法。
  • this.service:應用定義的 Service,經過它咱們能夠訪問到抽象出的業務層,等價於 this.ctx.service 。
  • this.config:應用運行時的配置項。
  • this.logger:logger 對象,上面有四個方法(debuginfowarnerror),分別表明打印四個不一樣級別的日誌,使用方法和效果與 context logger 中介紹的同樣,可是經過這個 logger 對象記錄的日誌,在日誌前面會加上打印該日誌的文件路徑,以便快速定位日誌打印位置。

自定義 Controller 基類

// app/core/base_controller.js
const { Controller } = require('egg');
class BaseController extends Controller {
  get user() {
    return this.ctx.session.user;
  }

  success(data) {
    this.ctx.body = {
      success: true,
      data,
    };
  }

  notFound(msg) {
    msg = msg || 'not found';
    this.ctx.throw(404, msg);
  }
}
module.exports = BaseController;
複製代碼

此時在編寫應用的 Controller 時,能夠繼承 BaseController,直接使用基類上的方法:

//app/controller/post.js
const Controller = require('../core/base_controller');
class PostController extends Controller {
  async list() {
    const posts = await this.service.listByUser(this.user); // 使用基類的方法
    this.success(posts); // 使用基類的方法
  }
}
複製代碼

Controller 方法

每個 Controller 都是一個 async function,它的入參爲請求的上下文 Context 對象的實例,經過它咱們能夠拿到框架封裝好的各類便捷屬性和方法。

// app/controller/post.js
exports.create = async ctx => {
  const createRule = {
    title: { type: 'string' },
    content: { type: 'string' },
  };
  // 校驗參數
  ctx.validate(createRule);
  // 組裝參數
  const author = ctx.session.userId;
  const req = Object.assign(ctx.request.body, { author });
  // 調用 service 進行業務處理
  const res = await ctx.service.post.create(req);
  // 設置響應內容和響應狀態碼
  ctx.body = { id: res.id };
  ctx.status = 201;
};
複製代碼

獲取 HTTP 請求參數

query

class PostController extends Controller {
  async listPosts() {
    const query = this.ctx.query;
    // {
    // category: 'egg',
    // language: 'node',
    // }
  }
}
複製代碼

當 Query String 中的 key 重複時,ctx.query 只取 key 第一次出現時的值,後面再出現的都會被忽略。

queries

有時候咱們的系統會設計成讓用戶傳遞相同的 key,例如 GET /posts?category=egg&id=1&id=2&id=3

架提供了 ctx.queries 對象,這個對象也解析了 Query String,可是它不會丟棄任何一個重複的數據,而是將他們都放到一個數組中。

// GET /posts?category=egg&id=1&id=2&id=3
class PostController extends Controller {
  async listPosts() {
    console.log(this.ctx.queries);
    // {
    // category: [ 'egg' ],
    // id: [ '1', '2', '3' ],
    // }
  }
}
複製代碼

Router params

Router 上也能夠申明參數,這些參數均可以經過 ctx.params 獲取到。

// app.get('/projects/:projectId/app/:appId', 'app.listApp');
// GET /projects/1/app/2
class AppController extends Controller {
  async listApp() {
    assert.equal(this.ctx.params.projectId, '1');
    assert.equal(this.ctx.params.appId, '2');
  }
}
複製代碼

body

框架內置了 bodyParser 中間件來對這兩類格式的請求 body 解析成 object 掛載到 ctx.request.body 上。

通常來講咱們最常常調整的配置項就是變動解析時容許的最大長度,能夠在 config/config.default.js 中覆蓋框架的默認值。

module.exports = {
  bodyParser: {
    jsonLimit: '1mb',
    formLimit: '1mb',
  },
};
複製代碼

若是用戶的請求 body 超過了咱們配置的解析最大長度,會拋出一個狀態碼爲 413(Request Entity Too Large) 的異常,若是用戶請求的 body 解析失敗(錯誤的 JSON),會拋出一個狀態碼爲 400(Bad Request) 的異常。

一個常見的錯誤是把 ctx.request.bodyctx.body 混淆,後者實際上是 ctx.response.body 的簡寫。

獲取上傳的文件

通常來講,瀏覽器上都是經過 Multipart/form-data 格式發送文件的,框架經過內置 Multipart 插件來支持獲取用戶上傳的文件。

File 模式:

在 config 文件中啓用 file 模式:

// config/config.default.js
exports.multipart = {
  mode: 'file',
};
複製代碼
上傳 / 接收文件:

上傳 / 接收單個文件:

<form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
  title: <input name="title" />
  file: <input name="file" type="file" />
  <button type="submit">Upload</button>
</form>
複製代碼

對應的後端代碼以下:

// app/controller/upload.js
const Controller = require('egg').Controller;
const fs = require('mz/fs');

module.exports = class extends Controller {
  async upload() {
    const { ctx } = this;
    const file = ctx.request.files[0];
    const name = 'egg-multipart-test/' + path.basename(file.filename);
    let result;
    try {
      // 處理文件,好比上傳到雲端
      result = await ctx.oss.put(name, file.filepath);
    } finally {
      // 須要刪除臨時文件
      await fs.unlink(file.filepath);
    }

    ctx.body = {
      url: result.url,
      // 獲取全部的字段值
      requestBody: ctx.request.body,
    };
  }
};
複製代碼

上傳 / 接收多個文件:

對於多個文件,咱們藉助 ctx.request.files 屬性進行遍歷,而後分別進行處理:

<form method="POST" action="/upload?_csrf={{ ctx.csrf | safe }}" enctype="multipart/form-data">
  title: <input name="title" />
  file1: <input name="file1" type="file" />
  file2: <input name="file2" type="file" />
  <button type="submit">Upload</button>
</form>
複製代碼

對應的後端代碼:

// app/controller/upload.js
const Controller = require('egg').Controller;
const fs = require('mz/fs');

module.exports = class extends Controller {
  async upload() {
    const { ctx } = this;
    console.log(ctx.request.body);
    console.log('got %d files', ctx.request.files.length);
    for (const file of ctx.request.files) {
      console.log('field: ' + file.fieldname);
      console.log('filename: ' + file.filename);
      console.log('encoding: ' + file.encoding);
      console.log('mime: ' + file.mime);
      console.log('tmp filepath: ' + file.filepath);
      let result;
      try {
        // 處理文件,好比上傳到雲端
        result = await ctx.oss.put('egg-multipart-test/' + file.filename, file.filepath);
      } finally {
        // 須要刪除臨時文件
        await fs.unlink(file.filepath);
      }
      console.log(result);
    }
  }
};
複製代碼

爲了保證文件上傳的安全,框架限制了支持的的文件格式,框架默認支持白名單以下:

// images
'.jpg', '.jpeg', // image/jpeg
'.png', // image/png, image/x-png
'.gif', // image/gif
'.bmp', // image/bmp
'.wbmp', // image/vnd.wap.wbmp
'.webp',
'.tif',
'.psd',
// text
'.svg',
'.js', '.jsx',
'.json',
'.css', '.less',
'.html', '.htm',
'.xml',
// tar
'.zip',
'.gz', '.tgz', '.gzip',
// video
'.mp3',
'.mp4',
'.avi',
複製代碼

用戶能夠經過在 config/config.default.js 中配置來新增支持的文件擴展名,或者重寫整個白名單。

  • 新增支持的文件擴展名
module.exports = {
  multipart: {
    fileExtensions: [ '.apk' ] // 增長對 apk 擴展名的文件支持
  },
};
複製代碼
  • 覆蓋整個白名單
module.exports = {
  multipart: {
    whitelist: [ '.png' ], // 覆蓋整個白名單,只容許上傳 '.png' 格式
  },
};
複製代碼

詳見 Egg-Multipart

header

除了從 URL 和請求 body 上獲取參數以外,還有許多參數是經過請求 header 傳遞的。

  • ctx.headersctx.headerctx.request.headersctx.request.header:這幾個方法是等價的,都是獲取整個 header 對象
  • ctx.get(name)ctx.request.get(name):獲取請求 header 中的一個字段的值,若是這個字段不存在,會返回空字符串。
  • 咱們建議用 ctx.get(name) 而不是 ctx.headers['name'],由於前者會自動處理大小寫。

Cookie

經過 ctx.cookies,咱們能夠在 Controller 中便捷、安全的設置和讀取 Cookie。

class CookieController extends Controller {
  async add() {
    const ctx = this.ctx;
    const count = ctx.cookies.get('count');
    count = count ? Number(count) : 0;
    ctx.cookies.set('count', ++count);
    ctx.body = count;
  }

  async remove() {
    const ctx = this.ctx;
    const count = ctx.cookies.set('count', null);
    ctx.status = 204;
  }
}
複製代碼

Session

經過 Cookie,咱們能夠給每個用戶設置一個 Session,用來存儲用戶身份相關的信息,這份信息會加密後存儲在 Cookie 中,實現跨請求的用戶身份保持。

框架內置了 Session 插件,給咱們提供了 ctx.session 來訪問或者修改當前用戶 Session 。

class PostController extends Controller {
  async fetchPosts() {
    const ctx = this.ctx;
    // 獲取 Session 上的內容
    const userId = ctx.session.userId;
    const posts = await ctx.service.post.fetch(userId);
    // 修改 Session 的值
    ctx.session.visited = ctx.session.visited ? ++ctx.session.visited : 1;
    ctx.body = {
      success: true,
      posts,
    };
  }
}
複製代碼

Session 的使用方法很是直觀,直接讀取它或者修改它就能夠了,若是要刪除它,直接將它賦值爲 null

class SessionController extends Controller {
  async deleteSession() {
    this.ctx.session = null;
  }
};
複製代碼

配置

module.exports = {
  key: 'EGG_SESS', // 承載 Session 的 Cookie 鍵值對名字
  maxAge: 86400000, // Session 的最大有效時間
};
複製代碼

參數校驗

藉助 Validate 插件提供便捷的參數校驗機制,幫助咱們完成各類複雜的參數校驗。

由於是插件,因此要完成插件的註冊:

// config/plugin.js
exports.validate = {
  enable: true,
  package: 'egg-validate',
};
複製代碼

經過 ctx.validate(rule, [body]) 直接對參數進行校驗:

this.ctx.validate({
  title: { type: 'string' },
  content: { type: 'string' },
});
複製代碼

當校驗異常時,會直接拋出一個異常,異常的狀態碼爲422(Unprocessable Entity),errors 字段包含了詳細的驗證不經過信息。若是想要本身處理檢查的異常,能夠經過 try catch 來自行捕獲。

class PostController extends Controller {
  async create() {
    const ctx = this.ctx;
    try {
      ctx.validate(createRule);
    } catch (err) {
      ctx.logger.warn(err.errors);
      ctx.body = { success: false };
      return;
    }
  }
};
複製代碼

校驗規則

參數校驗經過 Parameter 完成,支持的校驗規則能夠在該模塊的文檔中查閱到。

能夠經過 app.validator.addRule(type, check) 的方式新增自定義規則。

// app.js
app.validator.addRule('json', (rule, value) => {
  try {
    JSON.parse(value);
  } catch (err) {
    return 'must be json string';
  }
});
複製代碼

調用 Service

在 Controller 中能夠調用任何一個 Service 上的任何方法,同時 Service 是懶加載的,只有當訪問到它的時候框架纔會去實例化它。

發送 HTTP 響應

設置 status

ctx.status = 201;
複製代碼

設置 body

絕大多數的數據都是經過 body 發送給請求方的,和請求中的 body 同樣,在響應中發送的 body,也須要有配套的 Content-Type 告知客戶端如何對數據進行解析。

ctx.bodyctx.response.body 的簡寫,不要和 ctx.request.body 混淆了。

class ViewController extends Controller {
  async show() {
    this.ctx.body = {
      name: 'egg',
      category: 'framework',
      language: 'Node.js',
    };
  }

  async page() {
    this.ctx.body = '<html><h1>Hello</h1></html>';
  }
}
複製代碼

因爲 Node.js 的流式特性,咱們還有不少場景須要經過 Stream 返回響應,例如返回一個大文件,代理服務器直接返回上游的內容,框架也支持直接將 body 設置成一個 Stream,並會同時處理好這個 Stream 上的錯誤事件。

class ProxyController extends Controller {
  async proxy() {
    const ctx = this.ctx;
    const result = await ctx.curl(url, {
      streaming: true,
    });
    ctx.set(result.header);
    // result.res 是一個 stream
    ctx.body = result.res;
  }
};
複製代碼

設置 Header

經過 ctx.set(key, value) 方法能夠設置一個響應頭,ctx.set(headers) 設置多個 Header。

重定向

框架經過 security 插件覆蓋了 koa 原生的 ctx.redirect 實現,以提供更加安全的重定向。

  • ctx.redirect(url) 若是不在配置的白名單域名內,則禁止跳轉。
  • ctx.unsafeRedirect(url) 不判斷域名,直接跳轉,通常不建議使用,明確瞭解可能帶來的風險後使用。

用戶若是使用 ctx.redirect 方法,須要在應用的配置文件中作以下配置:

// config/config.default.js
exports.security = {
  domainWhiteList:['.domain.com'],  // 安全白名單,以 . 開頭
};
複製代碼

若用戶沒有配置 domainWhiteList 或者 domainWhiteList 數組內爲空,則默認會對全部跳轉請求放行,即等同於 ctx.unsafeRedirect(url)

相關文章
相關標籤/搜索