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);

到目前为止,我们就已经实现了本地用户注册和本地用户登录功能。