Koa2

485 阅读6分钟

koa是Express的下一代基于Node.js的web框架,由 Express 原班人马打造,利用 async 函数,Koa丢弃了回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件,给人一种干净利落的感觉,体积小、编程方式干净。 Koa官网链接:koa.bootcss.com/

历史

Express

Express是第一代最流行的web框架,它对Node.js的http进行了封装,用起来如下:

var express = require('express');
var app = express();

app.get('/', function (req, res) {
    res.send('Hello World!');
});

app.listen(3000, function () {
    console.log('server启动成功');
});

虽然Express的API很简单,但是它是基于ES5的语法,要实现异步代码,只有一个方法:回调。如果异步嵌套层次过多,代码写起来就非常难看:

app.get('/test', function (req, res) {
    fs.readFile('/file1', function (err, data) {
        if (err) {
            res.status(500).send('read file1 error');
        }
        fs.readFile('/file2', function (err, data) {
            if (err) {
                res.status(500).send('read file2 error');
            }
            res.type('text/plain');
            res.send(data);
        });
    });
});

虽然可以用async这样的库来组织异步代码,但是用回调写异步实在是太痛苦了!

koa1

随着新版Node.js开始支持ES6,Express的团队又基于ES6的generator重新编写了下一代web框架koa。和Express相比,koa 1.0使用generator实现异步,代码看起来像同步的:

var koa = require('koa');
var app = koa();

app.use('/test', function *() {
    yield doReadFile1();
    var data = yield doReadFile2();
    this.body = data;
});

app.listen(3000, function () {
    console.log('server启动成功');
});

koa2

koa团队并没有止步于koa 1.0,他们非常超前地基于ES7开发了koa2,和koa 1相比,koa2完全使用Promise并配合async来实现异步。

app.use(async (ctx, next) => {
    await next();
    var data = await doReadFile();
    ctx.response.type = 'text/plain';
    ctx.response.body = data;
});

Koa开发环境搭建

安装Node.js

因为node.js v7.6.0开始完全支持async/await,不需要加flag,所以node.js环境都要7.6.0以上。 node.js环境版本v7.6以上,npm 版本3.x以上。

安装Koa2

# 初始化package.json
npm init

# 安装koa2 
npm install koa

如果安装错误,一般都是网速问题,可以使用cnpm来进行安装。

hello Koa2

// 在koa2中,我们导入的是一个class,因此用大写的Koa表示:
const Koa = require('koa')
// 创建一个Koa对象表示web app本身:
const app = new Koa()

// 对于任何请求,app将调用该异步函数处理请求:
app.use( async ( ctx ) => {
  ctx.body = 'hello koa2'
})

app.listen(3000)
console.log('server启动成功')

在命令行中输入:

node index.js

然后在浏览器中输入:http://127.0.0.1:3000 就可以看到结果了。

async/await使用

快速理解

先复制以下这段代码,在粘贴在chrome的控制台console中,按回车键执行

function getSyncTime() {
  return new Promise((resolve, reject) => {
    try {
      let startTime = new Date().getTime()
      setTimeout(() => {
        let endTime = new Date().getTime()
        let data = endTime - startTime
        resolve( data )
      }, 500)
    } catch ( err ) {
      reject( err )
    }
  })
}

async function getSyncData() {
  let time = await getSyncTime()
  let data = `endTime - startTime = ${time}`
  return data
}

async function getData() {
  let data = await getSyncData()
  console.log( data )
}

getData()

结果:

image.png

从上述例子可以看出 async/await 的特点:

  • 可以让异步逻辑用同步写法实现
  • 最底层的await返回需要是Promise对象
  • 可以通过多层 async function 的同步写法代替传统的callback嵌套

请求数据获取

GET请求数据获取

koa2中GET请求通过request接收,接收的方式有两种:query和querystring。

  • query:返回的是格式化好的参数对象。
  • querystring:返回的是请求字符串。 由于ctx对request的API有直接引用的方式,所以也可以直接在ctx中得到GET请求。ctx中也分为query和querystring。
const Koa = require('koa')
const app = new Koa()

app.use( async ( ctx ) => {
  let url = ctx.url
  // 从上下文的request对象中获取
  let request = ctx.request
  let req_query = request.query
  let req_querystring = request.querystring

  // 从上下文中直接获取
  let ctx_query = ctx.query
  let ctx_querystring = ctx.querystring

  ctx.body = {
    url,
    req_query,
    req_querystring,
    ctx_query,
    ctx_querystring
  }
})

app.listen(3000, () => {
  console.log('server启动成功')
})

POST请求参数获取

对于POST请求的处理,koa2没有封装获取参数的方法,需要通过解析上下文context中的原生node.js请求对象req来获取。

获取Post请求的步骤:

  1. 解析上下文ctx中的原生nodex.js对象req。
  2. 将POST表单数据解析成query string-字符串.(例如:user=jspang&age=18)
  3. 将字符串转换成JSON格式。

注意:
         ctx.request是context经过封装的请求对象,
         ctx.req是context提供的node.js原生HTTP请求对象,可以得到更多的内容,适合我们深度编程;
         ctx.response是context经过封装的响应对象,
         ctx.res是context提供的node.js原生HTTP响应对象。 案例代码演示:

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

app.use( async ( ctx ) => {

  if ( ctx.url === '/' && ctx.method === 'GET' ) {
    // 当GET请求时候返回表单页面
    let html = `
      <h1>koa2 request post demo</h1>
      <form method="POST" action="/">
        <p>userName</p>
        <input name="userName" /><br/>
        <p>nickName</p>
        <input name="nickName" /><br/>
        <button type="submit">submit</button>
      </form>
    `
    ctx.body = html
  } else if ( ctx.url === '/' && ctx.method === 'POST' ) {
    // 当POST请求的时候,解析POST表单里的数据,并显示出来
    let postData = await parsePostData( ctx )
    ctx.body = postData
  } else {
    // 其他请求显示404
    ctx.body = '<h1>404!</h1>'
  }
})

// 解析上下文里node原生请求的POST参数
function parsePostData( ctx ) {
  return new Promise((resolve, reject) => {
    try {
      let postdata = "";
      ctx.req.addListener('data', (data) => {
        postdata += data
      })
      ctx.req.addListener("end",function(){
        let parseData = parseQueryStr( postdata )
        resolve( parseData )
      })
    } catch ( err ) {
      reject(err)
    }
  })
}

// 将POST请求参数字符串解析成JSON
function parseQueryStr( queryStr ) {
  let queryData = {}
  let queryStrList = queryStr.split('&')
  console.log( queryStrList )
  for (  let [ index, queryStr ] of queryStrList.entries()  ) {
    let itemList = queryStr.split('=')
    queryData[ itemList[0] ] = decodeURIComponent(itemList[1])
  }
  return queryData
}

app.listen(3000, () => {
  console.log('server启动成功')
})

koa-bodyparser中间件

对于POST请求的处理,koa-bodyparser中间件可以把koa2上下文的formData数据解析到ctx.request.body中

安装中间件

// 使用npm进行安装,需要注意的是我们这里要用–save,因为它在生产环境中需要使用。
npm install --save koa-bodyparser@3

引入使用

// 安装完成后,需要在代码中引入并使用。我们在代码顶部用require进行引入。
const bodyParser = require('koa-bodyparser');

// 由于middleware的顺序很重要,这个koa-bodyparser必须在router之前被注册到app对象上。
app.use(bodyParser());

在代码中使用后,直接可以用ctx.request.body进行获取POST请求参数,中间件自动给我们作了解析。

现在我们再优化刚刚案例代码:

const Koa = require('koa')
const app = new Koa()
const bodyParser = require('koa-bodyparser')

// 使用ctx.body解析中间件
app.use(bodyParser())

app.use( async ( ctx ) => {

  if ( ctx.url === '/' && ctx.method === 'GET' ) {
    // 当GET请求时候返回表单页面
    let html = `
      <h1>koa2 request post demo</h1>
      <form method="POST" action="/">
        <p>userName</p>
        <input name="userName" /><br/>
        <p>nickName</p>
        <input name="nickName" /><br/>
        <button type="submit">submit</button>
      </form>
    `
    ctx.body = html
  } else if ( ctx.url === '/' && ctx.method === 'POST' ) {
    // 当POST请求的时候,中间件koa-bodyparser解析POST表单里的数据,并显示出来
    let postData = ctx.request.body
    ctx.body = postData
  } else {
    // 其他请求显示404
    ctx.body = '<h1>404!</h1>'
  }
})

app.listen(3000, () => {
  console.log('server启动成功')
})

路由

Koa2原生路由实现

简单的例子

要想实现原生路由,需要得到地址栏输入的路径,然后根据路径的不同进行跳转。用ctx.request.url就可以实现。我们通过一个简单的例子了解一下如何获得访问路径。

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

app.use( async ( ctx ) => {
  let url = ctx.request.url
  ctx.body = url
})
app.listen(3000)

这时候访问http://127.0.0.1:3000/hello/12 页面会输出/hello/12。也就是说上下文的请求request对象中url之就是当前访问的路径名称,可以根据ctx.request.url 通过一定的判断或者正则匹配就可以定制出所需要的路由。

定制化的路由

源码文件目录:

.
├── app.js
├── package.json
└── view
    ├── 404.html
    ├── index.html
    └── todo.html

原生路由的实现需要引入fs模块来读取文件。然后再根据路由的路径去读取,最后返回给页面,进行渲染。我们看一个小例子进行学习。

/* app.js文件 */
const Koa = require('koa')
const fs = require('fs')
const app = new Koa()

/**
 * 用Promise封装异步读取文件方法
 * @param  {string} page html文件名称
 * @return {promise}      
 */
function render( page ) {
  return new Promise(( resolve, reject ) => {
    let viewUrl = `./view/${page}`
    fs.readFile(viewUrl, "binary", ( err, data ) => {
      if ( err ) {
        reject( err )
      } else {
        resolve( data )
      }
    })
  })
}

/**
 * 根据URL获取HTML内容
 * @param  {string} url koa2上下文的url,ctx.url
 * @return {string}     获取HTML文件内容
 */
async function route( url ) {
  let view = '404.html'
  switch ( url ) {
    case '/':
      view = 'index.html'
      break
    case '/index':
      view = 'index.html'
      break
    case '/todo':
      view = 'todo.html'
      break
    case '/404':
      view = '404.html'
      break
    default:
      break
  }
  let html = await render( view )
  return html
}

app.use( async ( ctx ) => {
  let url = ctx.request.url
  let html = await route( url )
  ctx.body = html
})

app.listen(3000, () => {
  console.log('server启动成功')
})

koa-router中间件

如果依靠ctx.request.url去手动处理路由,将会写很多处理代码,这时候就需要对应的路由的中间件对路由进行控制,这里介绍一个比较好用的路由中间件koa-router

安装koa-router中间件:

// koa2 对应的版本是 7.x
npm install --save koa-router@7

快速使用koa-router:

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

const Router = require('koa-router')

// 子路由1
let home = new Router()
home.get('/', async ( ctx )=>{
  let html = `
    <ul>
      <li><a href="/page/helloworld">/page/helloworld</a></li>
      <li><a href="/page/404">/page/404</a></li>
    </ul>
  `
  ctx.body = html
})

// 子路由2
let page = new Router()
page.get('/404', async ( ctx )=>{
  ctx.body = '404 page!'
})
.get('/helloworld', async ( ctx )=>{
  ctx.body = 'helloworld page!'
})

// 装载所有子路由
let router = new Router()
router.use('/', home.routes(), home.allowedMethods())
router.use('/page', page.routes(), page.allowedMethods())

// 加载路由中间件
app.use(router.routes()).use(router.allowedMethods())

app.listen(3000, () => {
  console.log('server启动成功')
})

cookie的使用

koa的上下文(ctx)直接提供了读取和写入的方法。

  • ctx.cookies.get(name,[optins]):读取上下文请求中的cookie。
  • ctx.cookies.set(name,value,[options]):在上下文中写入cookie。

写入Cookie操作:

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

app.use(async(ctx)=>{
    if(ctx.url=== '/index'){
        ctx.cookies.set(
            'MyName','FangC'
        );
        ctx.body = 'cookie is ok';
    }else{
        ctx.body = 'hello world'
    }
});

app.listen(3000, () => {
  console.log('server启动成功')
})

Cookie选项

比如我们要存储用户名,保留用户登录状态时,你可以选择7天内不用登录,也可以选择30天内不用登录。这就需要在写入是配置一些选项:

  • domain:写入cookie所在的域名
  • path:写入cookie所在的路径
  • maxAge:Cookie最大有效时长
  • expires:cookie失效时间
  • httpOnly:是否只用http请求中获得
  • overwirte:是否允许重写
const Koa  = require('koa');
const app = new Koa();

app.use(async(ctx)=>{
    if(ctx.url=== '/index'){
        ctx.cookies.set(
            'MyName','FangC',{
                domain:'127.0.0.1', // 写cookie所在的域名
                path:'/index',       // 写cookie所在的路径
                maxAge:1000*60*60*24,   // cookie有效时长
                expires:new Date('2021-12-31'), // cookie失效时间
                httpOnly:false,  // 是否只用于http请求中获取
                overwrite:false  // 是否允许重写
            }
        );
        ctx.body = 'cookie is ok';
    }else{
        ctx.body = 'hello world'
    }
});

app.listen(3000, () => {
  console.log('server启动成功')
})

读取Cookie:

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

app.use(async(ctx)=>{
    if(ctx.url=== '/index'){
        ctx.cookies.set(
            'MyName','FangC',{
                domain:'127.0.0.1', // 写cookie所在的域名
                path:'/index',       // 写cookie所在的路径
                maxAge:1000*60*60*24,   // cookie有效时长
                expires:new Date('2021-12-31'), // cookie失效时间
                httpOnly:false,  // 是否只用于http请求中获取
                overwrite:false  // 是否允许重写
            }
        );
        ctx.body = 'cookie is ok';
    }else{
        if( ctx.cookies.get('MyName')){
            ctx.body = ctx.cookies.get('MyName');
        }else{
            ctx.body = 'Cookie is none';
        }

    }
});

app.listen(3000, () => {
  console.log('server启动成功')
})

koa-static静态资源中间件

安装

npm install --save koa-static

新建static文件夹 然后在static文件中放入图片,css和js文件。

使用

const Koa = require('koa')
// 导入path包 用于拼接路径
const path = require('path')
 // 导入koa-static包
const static = require('koa-static')

const app = new Koa()

// 静态资源目录对于相对入口文件index.js的路径
const staticPath = './static'

// 开放的文件夹 __dirname为当前运行文件的目录  目前看来 只能开放文件夹 无法开放单独一个文件
app.use(static(
  path.join( __dirname,  staticPath)
))


app.use( async ( ctx ) => {
  ctx.body = 'hello world'
})

app.listen(3000, () => {
  console.log('server启动成功')
})

模板引擎

开发中不可能把所有的html代码全部写在JS里,这显然不现实,也没办法完成大型web开发。必须借用模板机制来帮助我们开发,koa2的模板机制要依靠中间件来完成开发。

安装中间件

npm install --save koa-views

安装ejs模板引擎

npm install --save ejs

编写模板
安装好ejs模板引擎后,就可以编写模板了,为了模板统一管理,我们新建一个view的文件夹,并在它下面新建index.ejs文件。

文章目录:

├── package.json
├── index.js
└── view
    └── index.ejs

view/index.ejs文件

<!DOCTYPE html>
<html>
<head>
    <title><%= title %></title>
</head>
<body>
    <h1><%= title %></h1>
    <p>EJS Welcome to <%= title %></p>
</body>
</html>

./index.js文件

const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()

// 加载模板引擎
app.use(views(path.join(__dirname, './view'), {
  extension: 'ejs'
}))

app.use( async ( ctx ) => {
  let title = 'hello koa2'
  await ctx.render('index', {
    title,
  })
})

app.listen(3000,()=>{
    console.log('server启动成功');
})

参考

千锋教育大前端小册 Koa2
技术胖-挑战全栈 Koa2教程
廖雪峰的官方网站 Koa
Node.js 系列 - 使用 EJS 模板引擎