【Koa.js Blueprints】搭建一個基本 Koa 應用

胡才俊(前道)發表於2017-04-18

Koa 是一個 Node.js 的 Web 開發框架。Node.js 是一個開源的、跨平臺的、可用於服務端和網路應用的執行環境。Node.js 採用 Google 的 V8 引擎執行程式碼,以單執行緒執行,基於事件驅動,使用非阻塞 I/O 呼叫,開發者可以在不使用執行緒的情況下開發出一個能夠承受高併發的伺服器。Koa 站在 Node.js 的肩膀之上,提供了開發健壯的 Web 應用所需要的工具。

Koa 短小精悍,核心中沒有繫結任何中介軟體,良好的擴充套件性、開放性使得開發工程師掌握更多的控制權,幾乎可以完成任何工作。通過學習以下內容,你將明白如何以正確的方式使用 Koa 框架:

  • 設定 Koa 靜態站點
  • 身份認證
  • 個人資料頁面
  • 測試

設定 Koa 靜態站點

瞭解如何響應基本的 HTTP 請求是正式邁開 Koa 的第一步。下面的例子中,我們會處理幾個 GET 請求,首先以純文字響應,然後以靜態 HTML 響應。為了保證本書中所有的例子能夠正常執行,請確保你已經安裝好 Node 和 NPM ,並且 Node 的版本不能低於 v7.6.0。

小提示:考慮到 NPM 預設從國外獲取和下載依賴包,國內的訪問速度很不理想,推薦大家使用淘寶 NPM 映象 cnpm,你可以參考 快速搭建 Node.js 開發環境以及加速 npm 一文。

Hello, World

如果你對 Koa 還不是很熟悉的話,我們將從一個簡單的例子 - Hello, World! 開始。

首先建立一個空資料夾,使用下面程式碼初始化一個 Node.js 專案:

$ npm init

根據命令列中提示的資訊,可以按照預設設定一步步走下去,當提示輸入 "entry point: (index.js)" 時輸入 "server.js",最終會在資料夾下生成一個 package.json 檔案。

{
  "name": "hello-world",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

該檔案定義了這個專案所依賴的各種模組,以及專案的配置資訊(比如名稱、版本、描述、作者、許可證等後設資料)。

社群裡面有很多腳手架或生成器幫助你快速生成一個 Koa 應用,不過現在我們將手動建立專案骨架。我們需要使用 npm 來下載 koa 包,為了將這些依賴資訊同步到 package.json 檔案中,需要使用 --flage 標識,完整的命令列如下:

$ npm install --save koa

執行完畢後,開啟 package.json 檔案:

{
  "name": "hello-world",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "koa": "^2.2.0"
  }
}

檔案中新新增了 dependencies 欄位,並新增了 "koa": "^2.2.0" 資料。必須注意本書所有示例均是基於 Koa 2.x,大家在安裝 koa 時注意一下。

小提示:Koa 1.x 和 Koa 2.x 的區別,以及如何做遷移,大家可以參考:https://github.com/koajs/koa/blob/master/docs/migration.md

你可以按照下面的示例內容,建立 server.js 檔案:

const Koa = require('koa');
const app = new Koa();

app.use(ctx => {
  ctx.body = 'Hello, World!';
});

app.listen(3000);
console.log('Koa started on port 3000');

程式碼源自:chapter1/hello-world

這個檔案是我們應用的切入點,我們建立了一個 app 應用例項,給所有的請求設定響應體,最後在 3000 埠上監聽請求。 有過 Express.js 應用開發經驗的同學也會納悶,難道 Koa 連路由都不支援?對的,之前提過 Koa 短小精悍,核心中沒有繫結任何中介軟體。不過 Koa 生態圈為我們提供了很多優質的路由中介軟體,比如 koa-routerkoa-route。你可以根據業務特點選擇一個合適的路由,甚至可以自己定製一個,本文我們將選擇 koa-router 作為應用的路由。

$ npm install --save koa-router

下載好 koa-router 後,修改 server.js 檔案:

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

router.get('/', (ctx, next) => {
  ctx.body = 'Hello World!';
});

app
  .use(router.routes())
  .use(router.allowedMethods());

app.listen(3000);
console.log('Koa started on port 3000');

上面的例子中,我們在 router 上註冊了一個監聽服務端根目錄的 GET 請求,當請求過來時,都會向客戶端返回字串 Hello World! 。koa-router 除了 get 方法對應 HTTP GET 方法外,還有 router.postrouter.putrouter.deleterouter.patchrouter.all 等對應 HTTP 的 POST、PUT、DELETE 方法。

最後讓我們通過下面命令來啟動服務:

$ node server.js

我們可以通過瀏覽器訪問 http://localhost:3000/,或者在命令列中輸入 curl -v localhost:3000 進行驗證。

模板引擎

我們已經能夠向客戶端傳送純文字資料,下面我們開始學習如何將 HTML 封裝到一個獨立的模板中,併傳送到客戶端。Koa 框架沒有內建模板引擎,我們將引入 koa-views 作為模板解決方案,並支援多模板渲染。Node.js 相關的模板引擎有很多,常見的有 jadehandlebarshamlmustacheswigatplejsjazzmarkopug (formerly jade)underscore 等等,更多引擎可以訪問 https://github.com/tj/consolidate.js#supported-template-engines。本書我們將支援 jade 和 mustache 兩種模板渲染,首先使用下面命令安裝相關依賴。

$ npm install --save koa-views jade mustache

在 server.js 檔案中,新增以下程式碼:

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

app.use(views(__dirname + '/views', {
  map: { jade: 'jade', html: 'mustache' }
}));

router.get('/', async (ctx, next) => {
  await ctx.render('index.jade', {
    pageTitle: '首頁'
  });
});

router.get('/app', async (ctx, next) => {
  await ctx.render('app.html', {
    pageTitle: '應用控制檯'
  });
});

在專案根目錄下建立 views 資料夾,並建立 index.jade 和 app.html 兩個檔案:

views/index.jade

doctype html
html(lang="en")
  head
    title= pageTitle
  body
    h1 首頁
    p
      a(href="/app") 前往應用控制檯

views/app.html

<!DOCTYPE html>
<html>
  <head>
    <title>{{ pageTitle }}</title>
  </head>
  <body>
    <h1>應用控制檯</h1>
    <p>
      <a href="/">返回首頁</a>
    </p>
  </body>
</html>

程式碼源自:chapter1/template-engine

koa-views 通過 views(__dirname + '/views', 指定了模板檔案所在的目錄 - 根目錄下的 views 資料夾;通過 { map: { jade: 'jade', html: 'mustache' } } 指定使用 jade 模板引擎解析 .jade 檔案,使用 mustache 模板引擎解析 .html 檔案。

啟動 Node 服務,通過瀏覽器訪問 http://localhost:3000/ 時,會顯示 views/index.jade 檔案內容;通過瀏覽器訪問 http://localhost:3000/app 時,會顯示 views/app.html 檔案內容。

隨著業務的發展、技術的進步,我們可能會發現某個模板引擎效能更好、語法更加優雅,那是不是可以在不影響當前業務的情況下實現技術平穩升級?基於 koa-views 顯然可以,比如我們現在又要接入 ejs 模板引擎,只需要三步:

第一步:下載 ejs 依賴包。

$ npm install --save ejs

第二步:在 koa-views 的 map 屬性中配置以 ejs 模板引擎解析 .ejs 檔案。

app.use(views(__dirname + '/views', {
  map: { jade: 'jade', html: 'mustache', ejs: 'ejs' }
}));

第三步:在 views 目錄下建立 ejs.ejs 例子。

<!DOCTYPE html>
<html>
  <head>
    <title><%= pageTitle %></title>
  </head>
  <body>
    <h1>ejs</h1>
  </body>
</html>

現在我們可以正式使用了,我們將在 server.js 中註冊一個 /ejs 路由,用於渲染 ejs.ejs 檔案內容。

router.get('/ejs', async (ctx, next) => {
  await ctx.render('ejs.ejs', {
    pageTitle: 'ejs 模板引擎'
  });
});

通過瀏覽器訪問 http://localhost:3000/ejs 即可展示 views/ejs.ejs 檔案內容。

身份認證

身份認證是指通過一定的手段完成對使用者身份的確認,身份認證的方法有很多,基本上可分為:基於共享金鑰的身份驗證、基於生物學特徵的身份驗證和基於公開金鑰加密演算法的身份驗證。對於 Web 應用開發而言,身份認證是最基礎功能,它主要包括使用者登入、註冊、退出等操作,目前主要包括 3 種形式的認證:

  • HTTP Basic 和 HTTP Digest 認證
  • 本地身份認證,一般都是基於 session、cookie 認證
  • 第三方整合認證:Google、Github、Facebook、QQ、微博等

HTTP Basic 和 HTTP Digest 認證是 HTTP 協議最常見的認證。這種認證模型非常簡單,就是所謂的質詢/響應(challenge/response)框架:當使用者向伺服器傳送一條 HTTP 請求報文時,伺服器首先回復一個“認證質詢”響應,要求使用者提供身份資訊,然後使用者再一次傳送 HTTP 請求報文,這次的請求頭中附帶上身份資訊(使用者名稱密碼),如果身份匹配,伺服器則正常響應,否則伺服器會繼續對使用者進行質詢或者直接拒絕請求。

接下來,我們主要講解本地身份認證和第三方整合認證。

本地身份認證

本節中,我們將探討 Koa 應用實現本地身份認證的最佳實踐。我們會使用 MongoDB 來儲存使用者資料,使用 Mongoose 作為物件文件對映(Object Document Mapper),接著我們會利用 Passport 實現身份認證。Passport 程式碼乾淨、易於維護,可以根據應用的特點,配置不同的認證機制,非常方便地整合到 Koa 應用中。

開始本節前,請確保你已經下載好 MongoDB 並且使用 npm 安裝好 mongoose。MongoDB 是一個開源的文件型別資料庫,它具有高效能、高可用、可自動收縮的特性。ODM 的概念對應關係型資料庫的 ORM,Mongoose 作為 ODM,能夠極大的簡化程式對 MongoDB 操作。通過 Mongoose 可以定義資料庫中的資料格式,它可以把資料庫中的 document 對映成記憶體中的一個物件,該物件包含 .save().update().title.author 等一系列方法和屬性。在呼叫這些方法和屬性時,Mongoose 會根據呼叫時所提供的條件,自動轉換成相應的 MongoDB shell 語句,並運算元據庫。

使用者物件建模

開始本節前,請執行以下命令,安裝好 mongoose、bcrypt、validator 三個依賴包。

$ npm install --save mongoose bcrypt validator

為了實現身份認證,我們需要使用資料庫儲存使用者資訊。本節我們將使用 Mongoose 定義使用者模型 - user,目前 user 只有 email、password 以及 created_at 三個欄位。

const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
  email: { type: String, trim: true, required: true, unique: true },
  password: { type: String, required: true },
  created_at: { type: Date, default: Date.now }
});

UserSchema.pre('save', function(next) {
  if (!this.isModified('password')) {
    return next();
  }
  this.password = User.encryptPassword(this.password);
  next();
});

const User = mongoose.model('User', UserSchema);

module.exports = User;

程式碼源自:chapter1/custom-user-auth/models/user.js

這裡我們建立了 UserSchema 模式來描述使用者,Mongoose 提供了很多簡便的方式幫助我們更好地定義欄位,比如,只需要在宣告欄位時新增 required: true 就可以指定該欄位在資料庫中是不能為空的。上面程式碼中,我們在宣告 email 欄位時,使用 type 屬性設定欄位的型別是字串;使用 trim 設定欄位儲存到資料庫前需要去除收尾空白字元;使用 required 設定欄位不能為空,是必須提供的;使用 unique 設定欄位值是唯一的,不能重複。

Mongoose 基於 Hooks JS 為 Schema 內建了很多鉤子,比如上面例子中的前置鉤子 - pre,當 Mongoose 儲存 user 模型前,會觸發 pre('save') 這個鉤子,我們在回撥函式中檢測模型中 password 的值是否發生變化,如果發生了改變,需要重新加密修改後的密碼,在執行完相關程式碼後,執行 next() 方法來觸發序列中的下一個鉤子。

Mongoose 提供了兩種型別的前置鉤子:序列(Serial)和並行(Parallel)。 序列中介軟體是一個接一個的執行,你可以通過 next 呼叫下一個中介軟體,示例程式碼如下:

var schema = new Schema(..);
schema.pre('save', function(next) {
  // do stuff
  next();
});

並行中介軟體提供了更小細粒度的控制,你可以執行非同步函式,示例程式碼如下:

var schema = new Schema(..);

// `true` means this is a parallel middleware. You **must** specify `true`
// as the second parameter if you want to use parallel middleware.
schema.pre('save', true, function(next, done) {
  // calling next kicks off the next middleware in parallel
  next();
  setTimeout(done, 100);
});

本書的後續章節還會詳細介紹。

為了保證 user 模型資料的正確性,我們需要使用 validator 新增驗證邏輯,程式碼如下:

const validator = require('validator');

User.schema.path('email').validate(function(email) {
  return validator.isEmail(email);
});

User.schema.path('password').validate(function(password) {
  return validator.isLength(password, 6);
});

我們使用了 validator 的 isEmail 方法驗證 email 值是不是一個合法的郵箱地址,使用 isLength 方法驗證 password 長度是不是合法。當然 validator 作為一個強大的字串驗證器和轉換型別的庫,還提供很多強大的驗證方法,大家可以通過 https://github.com/chriso/validator.js#validators 獲取更多資訊。

models/user.js 檔案中,我們也使用 Mongoose 提供的 statics 方法定義了三個靜態方法,模型物件可以直接訪問。

UserSchema.statics = {
  makeSalt: function() {
    return bcrypt.genSaltSync(10);
  },
  encryptPassword: function(password) {
    if (!password) {
      return '';
    }
    return bcrypt.hashSync(password, User.makeSalt());
  },
  register: function(email, password, cb) {
    var user = new User({
      email: email,
      password: password
    });
    user.save(function(err) {
      cb(err, user);
    });
  }
};

提到 MVC,相信大家都不會陌生,它作為一種軟體設計模式,將應用的輸入、處理和輸出分開。MVC 應用軟體被分成了三個基本部分:模型(Model)、檢視(View)和控制器(Controller),它們之間相互作用。模型有對資料庫直接訪問和操作的權力;控制器負責轉發和處理請求;檢視則負責頁面的渲染。本章節我們將使用 MVC 設計模式建立 Koa 應用,上面定義 User 模型是我們邁出的第一步。

介紹 Koa 中介軟體

Passport 是專門為身份認證而設計的 Node.js 中介軟體。為了應對多種多樣的認證方式,Passport 採用了一種叫做策略(Strategies)的方案,也就是為每一種認證提供一個獨立的策略。比如我們需要實現 Github 第三方登入,我們只需要引入 passport-github 策略,如果需要實現本地登入,我們只需要使用 passport-local 策略。為了在 Koa 應用中實現本地身份認證,我們需要使用 npm 下載 koa-passport 和 passport-local 中介軟體,在正式接觸這兩個中介軟體前,我們先來了解一下 Koa 中介軟體相關知識。

在 Koa 的世界裡,萬物皆中介軟體,實際上一個 Koa 應用就是一個物件,這個物件包含一箇中介軟體陣列。中介軟體(也稱為前置和後置鉤子)是非同步函式執行過程中傳遞的控制的函式。這些中介軟體由外而內相互巢狀,執行完畢之後再由內到外依次執行回撥。下面通過一個簡單的例子解釋:

const Koa = require('koa');
const app = new Koa();

// x-response-time
app.use(async (ctx, next) => {
  const start = new Date();
  await next();
  const ms = new Date() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// logger
app.use(async (ctx, next) => {
  const start = new Date();
  await next();
  const ms = new Date() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});

// response
app.use(ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

程式碼源自:chapter1/middleware-philosophy

例子中,我們定義了三個中介軟體,第一個中介軟體新增 X-Response-Time 響應頭,第二個中介軟體記錄日誌資訊,第三個中介軟體為每一個請求設定響應體為 Hello World。這三個中介軟體的執行順序如下:

  • 請求從 3000 埠進來
  • 第一個中介軟體接收到執行訊號
  • 建立了一個 Date 物件,並賦值給 start
  • 遇到非同步流程控制 - await,暫停執行當前中介軟體程式碼,開始進入第二個中介軟體
  • 第二個中介軟體接收到執行訊號
  • 建立了一個 Date 物件,並賦值給 start
  • 遇到非同步流程控制 - await,暫停執行當前中介軟體程式碼,開始進入第三個中介軟體
  • 第三個中介軟體接收到執行訊號
  • 設定 Context 物件 body 屬性的值為 Hello World
  • 第三個中介軟體執行完畢
  • 第二個中介軟體**再次**接收到執行訊號
  • 建立一個 Date 物件,並根據 start 進行計算,並賦值給 ms
  • 列印出請求日誌時間 - ${ctx.method} ${ctx.url} - ${ms}
  • 第二個中介軟體執行完畢
  • 第一個中介軟體**再次**接收到執行訊號
  • 計算出響應時間,並設定響應頭 - X-Response-Time
  • 第一個中介軟體執行完畢,請求到達頂端,返回響應到客戶端

Koa 中介軟體使用的模型,也被稱為洋蔥圈模型,洋蔥圖如下:

洋蔥圈模型

每一箇中介軟體就類似每一層洋蔥圈,上面例子中的第一個中介軟體 "x-response-time" 就好比洋蔥的最外層,第二個中介軟體 "logger" 就好比第二層,第三個中介軟體 "response" 就好比最裡面那一層,所有的請求經過中介軟體的時候都會執行兩次。

Koa 支援 3 種不同型別的中介軟體寫法:

  • Common 函式
  • Generator 函式
  • Async 函式
Common 函式
const Koa = require('koa');
const app = new Koa();

// logger
app.use((ctx, next) => {
  const start = new Date();
  return next().then(() => { //  ******** Common 函式用法  ********
    const ms = new Date() - start;
    console.log(`${ctx.method} ${ctx.url} - ${ms}`);
  });
});

// response
app.use(ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

本質上每一個 Koa 中介軟體,在框架內部經過 compose 封裝後都是一個 Promise 物件,因此可以將需要做非同步處理的邏輯作為 then() 方法的回撥函式。

Generator 函式

查閱 Koa 框架原始碼,大家會發現在 use 方法中,提示 V3 將拋棄單純以 Generator 函式作為中介軟體的寫法。

if (isGeneratorFunction(fn)) {
  deprecate('Support for generators will be removed in v3. ' +
    'See the documentation for examples of how to convert old middleware ' +
    'https://github.com/koajs/koa/blob/master/docs/migration.md');
  fn = convert(fn);
}

也就是說,下面的寫法即將過時:

app.use(function *(next) {
  const start = new Date();
  yield next;
  const ms = new Date() - start;
  console.log(`${this.method} ${this.url} - ${ms}ms`);
});

不過,大家可以使用 koa-convert 或者 co 進行轉化。

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

app.use(convert(function *(next) {
  const start = new Date();
  yield next;
  const ms = new Date() - start;
  console.log(`${this.method} ${this.url} - ${ms}ms`);
}));

使用 co 轉化程式碼:

const co = require('co');

app.use(co.wrap(function *(ctx, next) {
  const start = new Date();
  yield next();
  const ms = new Date() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
}));

目前社群中的很多優秀的模組只支援 Koa 1.x,比如 koa-generic-session ,如果需要繼續在 Koa 2.x 中使用,可以使用 **koa-convert** 轉化一下。

const session = require('koa-generic-session');
const convert = require('koa-convert');

app.keys = ['session-secret'];
app.use(convert(session({
  store: new MongoStore({
    url: config.session_db
  })
})));
Async 函式
const Koa = require('koa');
const app = new Koa();

// logger
app.use(async (ctx, next) => {
  const start = new Date();
  await next(); // ******** Async 函式用法 ********
  const ms = new Date() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});

// response
app.use(ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

Node.js v7.6.0 已經開始原生支援 Async/Await,好不誇張地說,Async/Await 是目前非同步流程控制最好的解決方案,本書中的例子均採用這種中介軟體寫法。

設定 Passport 中介軟體

執行以下命令,安裝好 Passport 相關的中介軟體:

$ npm install --save koa-passport passport-local

Koa Passport 中介軟體 koa-passport 利用 koa-generic-session 進行 session 儲存和管理。使用 Passport 中介軟體有三個重要的事情需要考慮:

  • 如何在使用者成功登入後將使用者資訊儲存到會話 session 中(序列化使用者)?
  • 如何從會話 session 中讀取使用者資訊(反序列化使用者)?
  • 如何校驗提供的 email/password 是不是一個合法的使用者?

    const passport = require('koa-passport'); const LocalStrategy = require('passport-local').Strategy; const User = require('mongoose').model('User');

    passport.serializeUser((user, done) => { done(null, user.id); });

    passport.deserializeUser((id, done) => { User.findById(id, done); });

程式碼源自:custom-user-auth/core/passport.js

我們在 passport.serializeUser 方法的回撥函式中,告訴 Passport 使用使用者的 id 資訊序列化使用者,即使用者成功登陸後將使用者的 id 資訊儲存到會話 session 中。接著我們在 passport.deserializeUser 中宣告,通過使用者的 id 從會話 session 中反序列化使用者資訊。當請求過來時,我們從會話 session 中獲取使用者 id,再根據使用者 id 從資料庫中獲取使用者記錄,最後將使用者記錄以 user 屬性的形式掛載到 Context 物件 - ctx 上。

下面將對 Passport 本地認證策略進行配置:

function authFail(done) {
  done(null, false, { message: 'Incorrent email/password combination' });
}

passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => {
  User.findOne({
    email: email
  }, function(err, user) {
    if (err) return done(err);
    if (!user) {
      return authFail(done);
    }
    if (!user.validPassword(password)) {
      return authFail(done);
    }
    return done(null, user);
  });
}));

上面的程式碼中,我們建立了一個 LocalStrategy 物件,並傳遞一個回撥函式,該回撥函式會接受 emailpassword,和 done 三個引數。在該函式中,首先會根據 email 值從資料庫中獲取使用者資料,如果沒有找到使用者資料,我們將返回異常資訊,如果該使用者存在,但是 passport 無效,也會返回異常資訊。如果 email 和 password 正確,我們會呼叫 done(null, user) 回撥函式。

設定好 Passport 之後,我們就需要在 Koa 應用中整合 Passport:

const mongoose = require('mongoose');
const convert = require('koa-convert');
const session = require('koa-generic-session');
const MongoStore = require('koa-generic-session-mongo');
const bodyParser = require('koa-bodyparser');
const User = require('./models/user');
const passport = require('./core/passport');

// https://github.com/Automattic/mongoose/issues/4291
mongoose.Promise = global.Promise;
mongoose.connect(config.db_url, err => {
  if (err) throw err;
});

// sessions
app.keys = ['your-session-secret'];
app.use(convert(session({
  store: new MongoStore({
    url: config.session_db
  })
})));

// body parser
app.use(bodyParser());

// authentication
app.use(passport.initialize());
app.use(passport.session());

為了能夠在 Koa 應用中成功使用 Passport,我們需要在初始化 Koa 物件時,對其進行相關設定。首先我們需要開啟對於 cookie 和 session 的支援,上面的例子中我們使用 koa-generic-session 中介軟體,該中介軟體解析 cookie 物件並新增到 ctx.session.cookie。接著,我們新增 koa-bodyparser 中介軟體,該中介軟體解析 HTTP 請求,並將這些請求體封裝成 JavaScript 物件 ctx.request.body,本章節例子中,我們需要通過該中介軟體,實現從 POST 請求的 body 體中獲取 email 和 password 值。最後我們需要初始化 Passport 中介軟體,並啟用 Passport 的 session 功能。

koa-generic-session 預設使用 MemoryStore 進行內容 session 儲存,檢視原始碼,我們會發現該中介軟體不推薦在生產環境下使用這種預設方式,會有記憶體洩漏問題。

const warning = 'Warning: koa-generic-session\'s MemoryStore is not\n' +
  'designed for a production environment, as it will leak\n' +
  'memory, and will not scale past a single process.';

程式碼源自:generic-session/lib/session.js

本例子中,我們使用 koa-generic-session-mongo 提供的 MongoStore 來替代 MemoryStore 儲存會話 session 內容。

實際專案中,我們經常使用大名鼎鼎的 redis 儲存會話。下面我們將使用 redis - koa-redis 替代 MongoStore,作為一項快取技術, redis 的效能和持久化方案都不錯。

const convert = require('koa-convert');
const session = require('koa-generic-session');
const RedisStore = require('koa-redis');

app.keys = ['your-session-secret'];
app.use(convert(session({
  store: new RedisStore()
})));
使用者註冊

正如上面提到的,我們將使用 MVC 模式建立 Koa 應用,上面章節我們建立好了 user 模型,為了完整實現使用者註冊功能,我們還需要建立控制器和檢視。首先我們需要建立使用者控制器,該控制器中擁有操作使用者模型的所有路由。

var User = require('mongoose').model('User');

module.exports.getRegister = async (ctx, next) => {
  await ctx.render('register.jade');
};

module.exports.postRegister = async (ctx, next) => {

  const registerPromise = function() {
    return new Promise((resolve, reject) => {
      User.register(ctx.request.body.email, ctx.request.body.password, (err, user) => {
        if (err) {
          reject({code: 404, err: err});
        } else {
          ctx.login(user, function(err) {
            if (err) {
              reject({code: 500, err: err});
            } else {
              resolve();
            }
          });
        }
      });
    });
  };

  await registerPromise().then(() => {
    return ctx.redirect('/login');
  }, (info) => {
    return ctx.throw(info.code, info.err.message);
  });
};

程式碼源自:chapter1/custom-user-auth/controllers/user.js

上面暴露了兩個方法:getRegisterpostRegister。前者匹配 /register 的 GET 請求,用於渲染 views/register.jade 頁面內容;後者匹配 /register 的 POST 請求,在處理函式中,我們直接呼叫 User 模型中封裝的 register 方法,給該方法傳入引數 ctx.request.body.emailctx.request.body.password,並且註冊一個回撥函式,如果註冊成功告訴客戶端重定向到登入頁面,如果失敗向客戶端丟擲異常資訊。

然後我們在 routes.js 檔案中,將路由與控制器中的操作一一對應起來:

const Router = require('koa-router');
const userController = require('./controllers/user');

const router = new Router();

module.exports.initialize = function(app) {

  router.get('/register', userController.getRegister);
  router.post('/register', userController.postRegister);

  app
    .use(router.routes())
    .use(router.allowedMethods());
};

程式碼源自:chapter1/custom-user-auth/routes.js

最後,我們在 server.js 檔案中,初始化應用路由:

const routes = require('./routes');

// routes
routes.initialize(app);
使用者登入

我們已經完成了使用者註冊功能,接下來我們將要完成使用本地郵箱和密碼實現使用者登入的功能。首先我們需要在使用者控制器 controllers/user.js 檔案中新增登入相關的操作:

const passport = require('../core/passport');

module.exports.getLogin = async (ctx) => {
  await ctx.render('login.jade');
};

module.exports.postLogin = passport.authenticate('local', {
  successRedirect: '/app',
  failureRedirect: '/login'
});

讓我們來分析一下當提交 login POST 請求時會發生什麼。我們呼叫了 passport.authenticate() 方法,並傳遞了兩個引數:local{ successRedirect: '/app', failureRedirect: '/login' }。前者告訴 Passport,我們將使用本地認證策略處理,也就是說 Passport 將會將請求代理到 LocalStrategy。如果提供的 email/password 正確,登入成功的話,會告訴客戶端重定向到 /app 路由,否則將重新跳到 /login 路由。

接著,我們需要在 routes.js 檔案中將這些回撥函式繫結到對應的路由上:

router.get('/login', userController.getLogin);
router.post('/login', userController.postLogin);

到目前為止,我們就已經實現了本地使用者註冊和本地使用者登入功能。

相關文章