Koa框架

216 阅读2分钟

Koa框架

koa是一个新的web框架,致力于成为web应用和API开发领域中的一个更小、更富有表现力、更健壮的基石。

利用async函数丢弃回掉函数,并增强错误处理。koa没有任何预置的中间件,可快速而愉快地编写服务端应用程序。

脚手架工具

//全局安装
npm install -g koa-generator

//创建项目
koa projectName && cd project

//安装依赖
npm install

//启动项目
npm start

安装koa

Koa 依赖 node v7.6.0 或 ES2015及更高版本和 async 方法支持.

cnpm i koa -S

koa基本使用

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

// logger

app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time

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

// response

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

app.listen(3000);

koa中间件

核心概念

中间件的核心其实就是,koa在use里注入的next方法

  • koa Application(应用程序)
  • Context(上下文)
  • Request(请求)、Response(响应)

koa.png

工作原理

执行的顺序:顺序执行

回掉的顺序:反向执行

先进后出

koaycmx.png

同步中间件

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

const one = (cxt,next) => {
  console.log(`>>one`)
  next()
  console.log(`<<one`)
}
const two = (cxt,next) => {
  console.log(`>>two`)
  next()
  console.log(`<<two`)
}
const three = (cxt,next) => {
  console.log(`>>three`)
  next()
  console.log(`<<three`)
}

app.use(one)
app.use(two)
app.use(three)

app.listen(3000)


//输出顺序

// >>one
// >>two
// >>three
// <<three
// <<two
// <<one

#总结:每执行到一个中间件会进栈,当执行到中间件里的next方法的时候会去执行下一个中间件,直到next方法返回空,最后进栈的中间件会出栈执行next()后面的代码,直到第一个中间件出栈,就是洋葱模型了先进后出。

异步中间件

异步操作(比如读取数据库),中间件就必须写成 async await函数。

const fs = require('fs.promised');
const Koa = require('koa');
const app = new Koa();

const main = async function (ctx, next) {
  ctx.response.type = 'html';
  ctx.response.body = await fs.readFile('./demos/template.html', 'utf8');
};

app.use(main);
app.listen(3000);

#总结:如果存在异步中间件需要把全部中间件改为async await 把异步变为同步执行

中间件的合成

koa-compose模块可以将多个中间件合成为一个。

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

const logger = (ctx, next) => {
  console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
  next();
}

const main = ctx => {
  ctx.response.body = 'Hello World';
};

const middlewares = compose([logger, main]);
app.use(middlewares);
compose

compose函数的作用就是组合函数的,将函数串联起来执行,将多个函数组合起来,一个函数的输出结果是另一个函数的输入参数,一旦第一个函数开始执行,就会像多米诺骨牌一样推导执行了。

var greeting = (firstName, lastName) => 'hello, ' + firstName + ' ' + lastName
var toUpper = str => str.toUpperCase()
var fn = compose(toUpper, greeting)
console.log(fn('jack', 'smith'))
// ‘HELLO,JACK SMITH’
  • compose的参数是函数,返回的也是一个函数
  • 因为除了第一个函数的接受参数,其他函数的接受参数都是上一个函数的返回值,所以初始函数的参数是多元的,而其他函数的接受值是一元
  • compsoe函数可以接受任意的参数,所有的参数都是函数,且执行方向是自右向左的,初始函数一定放到参数的最右面

常用API

app.use(fun)

将给定的中间件方法添加到此应用程序

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

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

app.listen(3000);

app.listen(num)

#app.listen(...)是以下方式的语法糖
const http = require('http');
const https = require('https');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
https.createServer(app.callback()).listen(3001);

app.keys=

设置签名的 Cookie 密钥

app.keys = ['im a newer secret', 'i like turtle'];
app.keys = new KeyGrip(['im a newer secret', 'i like turtle'], 'sha256');

这些密钥可以倒换,并在使用 { signed: true } 参数签名 Cookie 时使用

ctx.cookies.set('name', 'tobi', { signed: true });

app.context

app.context 是从其创建 ctx 的原型。您可以通过编辑 app.contextctx 添加其他属性。这对于将 ctx 添加到整个应用程序中使用的属性或方法非常有用,这可能会更加有效(不需要中间件)和/或 更简单(更少的 require()),而更多地依赖于ctx,这可以被认为是一种反模式。

app.context.db = db();

app.use(async ctx => {
  console.log(ctx.db);
});
ctx
request
	method
    url
    header
    	host
        connection
		cache-control
		upgrade-insecure-requests
		user-agent
		sec-fetch-user
		accept
		sec-fetch-site
		sec-fetch-mode
		accept-encoding
		accept-language
response
	status
    message
app
	subdomainOffset
    proxy
    env
originalUrl
req
res
socket


{
  request: {
    method: 'GET',
    url: '/',
    header: {
      host: 'localhost:3000',
      connection: 'keep-alive',
      'cache-control': 'max-age=0',
      'upgrade-insecure-requests': '1',
      'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36',
      'sec-fetch-user': '?1',
      accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
      'sec-fetch-site': 'none',
      'sec-fetch-mode': 'navigate',
      'accept-encoding': 'gzip, deflate, br',
      'accept-language': 'zh-CN,zh;q=0.9'
    }
  },
  response: {
    status: 404,
    message: 'Not Found'
  },
  app: {
    subdomainOffset: 2,
    proxy: false,
    env: 'development'
  },
  originalUrl: '/',
  req: '<original node req>',
  res: '<original node res>',
  socket: '<original node socket>'
}


response类型

Koa 默认的返回类型是text/plain,如果想返回其他类型的内容,可以先用ctx.request.accepts判断一下,客户端希望接受什么数据(根据 HTTP Request 的Accept字段),然后使用ctx.response.type指定返回类型。

const main = ctx => {
  if (ctx.request.accepts('xml')) {
    ctx.response.type = 'xml';
    ctx.response.body = '<data>Hello World</data>';
  } else if (ctx.request.accepts('json')) {
    ctx.response.type = 'json';
    ctx.response.body = { data: 'Hello World' };
  } else if (ctx.request.accepts('html')) {
    ctx.response.type = 'html';
    ctx.response.body = '<p>Hello World</p>';
  } else {
    ctx.response.type = 'text';
    ctx.response.body = 'Hello World';
  }
};
网页模板

实际开发中,返回给用户的网页往往都写成模板文件。我们可以让 Koa 先读取模板文件,然后将这个模板返回给用户。

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

const main = (ctx) => {
    ctx.response.type = 'html';
    ctx.response.body = fs.createReadStream(path.resolve(path.join(__dirname, './demo.html')));
}

app.use(main);
app.listen(3000);

错误处理

500错误

如果代码运行过程中发生错误,我们需要把错误信息返回给用户。HTTP 协定约定这时要返回500状态码。Koa 提供了ctx.throw()方法,用来抛出错误,ctx.throw(500)就是抛出500错误。

const main = ctx => {
  ctx.throw(500);
};

400错误

const main = ctx => {
  ctx.response.status = 404;
  ctx.response.body = 'Page Not Found';
};

处理错误的中间件

为了方便处理错误,最好使用try...catch将其捕获。但是,为每个中间件都写try...catch太麻烦,我们可以让最外层的中间件,负责所有中间件的错误处理。

const handler = async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.response.status = err.statusCode || err.status || 500;
    ctx.response.body = {
      message: err.message
    };
  }
};

const main = ctx => {
  ctx.throw(500);
};

app.use(handler);
app.use(main);

error事件监听

运行过程中一旦出错,Koa 会触发一个error事件。监听这个事件,也可以处理错误。

const main = ctx => {
  ctx.throw(500);
};

app.on('error', (err, ctx) =>
  console.error('server error', err);
);

释放error事件

需要注意的是,如果错误被try...catch捕获,就不会触发error事件。这时,必须调用ctx.app.emit(),手动释放error事件,才能让监听函数生效。

const handler = async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.response.status = err.statusCode || err.status || 500;
    ctx.response.type = 'html';
    ctx.response.body = '<p>Something wrong, please contact administrator.</p>';
    ctx.app.emit('error', err, ctx);
  }
};

const main = ctx => {
  ctx.throw(500);
};

app.on('error', function(err) {
  console.log('logging error ', err.message);
  console.log(err);
});

//说明app是继承自nodejs的EventEmitter对象。

常用插件

//压缩
koa-compress #压缩

//安全
jsonwebtoken #签发令牌

koa-jwt #令牌校验

koa-session #鉴权方式

//日志
koa-logger #日志

koa-compose #整合中间件


koa-router

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

方法:
#restful api风格接口请求
router
  .get('/', (ctx, next) => {
    //获取请求参数
    ctx.requst.query
    
    ctx.body = 'Hello World!';
  })
  .post('/users', (ctx, next) => {
    // ...
  })
  .put('/users/:id', (ctx, next) => {
    // ...
  })
  .del('/users/:id', (ctx, next) => {
    // ...
  })
  .all('/users/:id', (ctx, next) => {
    // ...
  });

#把router中的方法添加到use中作为中间件处理
router.routes()
app.use(router.routes())

#拦截请求如果没有返回4xx,5xx状态码
router.allowedMethods()
app.use(router.allowedMethods())

#为已初始化的路由器实例设置路径前缀
router.prefix('/api')

@koa/cors

#处理跨域请求
const cors = require('@koa/cors')

app.use(cors())

koa-body

#协议处理,可以实现文件上传,同时也可以让koa能获取post请求的参数
const koaBody = require('koa-body')

app.use(koaBody())

原生post参数处理
var koa = require('koa');
var app = new koa();
var route = require('koa-route');

const main = (ctx) => {
    var dataArr = [];
    ctx.req.addListener('data', (data) => {
        dataArr.push(data);
    });
    ctx.req.addListener('end', () => {
        // console.log(jsonBodyparser(str));
        let data = Buffer.concat(dataArr).toString();
        console.log(data)
    });
    ctx.response.body = 'hello world';
}

app.use(route.post('/', main));  //  1. 路径 2. ctx函数
app.listen(3000);  // 起服务 , 监听3000端口
文件上传
const os = require('os');
const path = require('path');
const koaBody = require('koa-body');
var route = require('koa-route');
var koa = require('koa');
var app = new koa();
var fs = require('fs');

const main = async function(ctx) {
  const tmpdir = os.tmpdir(); 
  const filePaths = [];  
  const files = ctx.request.files || {};

  for (let key in files) {
    const file = files[key];
    const filePath = path.join(tmpdir, file.name);
    const reader = fs.createReadStream(file.path);
    const writer = fs.createWriteStream(filePath);
    reader.pipe(writer);
    filePaths.push(filePath);
  }
//   console.log('xxxxxxxx', filePaths)
  ctx.body = filePaths;
};

app.use(koaBody({ multipart: true }));  // 代表我们上传的是文件
app.use(route.post('/upload', main));
app.listen(3000);  // 起服务 , 监听3000端口

注意:koa-body和koa-bodyparser不要同时使用,会报错

koa-json

#json格式化
const json = require('koa-json')

app.use(json({ pretty: false, param: 'pretty' }))

koa-combine-routers

#路由压缩

//app.js
const Koa = require('koa')
const router = require('./routes')
 
const app = new Koa()
app.use(router())

//routes.js
const Router = require('koa-router')
const combineRouters = require('koa-combine-routers')
 
const dogRouter = new Router()
const catRouter = new Router()
 
dogRouter.get('/dogs', async ctx => {
  ctx.body = 'ok'
})
 
catRouter.get('/cats', async ctx => {
  ctx.body = 'ok'
})
 
const router = combineRouters(
  dogRouter,
  catRouter
)
 
module.exports = router

koa-helmet

#安全头处理
const helmet = require('koa-helmet')
app.use(helmet())

koa-static

#静态资源处理
const statics = require('koa-static')
app.use(statics(path.join(__dirname,'./public')))

koa-compose

#中间件合成

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

const logger = (ctx, next) => {
  console.log(`${Date.now()} ${ctx.request.method} ${ctx.request.url}`);
  next();
}

const main = ctx => {
  ctx.response.body = 'Hello World';
};


const middlewares = compose([logger, main]);
app.use(middlewares);

nodemon

#热更新,热加载
cnpm i -D nodemon  //安装开发依赖

npx nodemon --version  //查看版本

npx nodemon ./index.js //启动

"start": "nodemon ./index.js"  //添加成脚本启动 npm run start
"start": "nodemon --exec babel-node src/app.js"  //添加成脚本启动 npm run start

webpack

安装
cnpm i -D webpack webpack-cli


#通过webpack改写后改变了启动方式
npx babel-node .\src\app.js

#结合热更新nodemon
npx nodemon --exec babel-node .\src\app.js
webpack插件
cnpm i -D clean-webpack-plugin #清除
cnpm i -D webpack-node-externals #排除模块
cnpm i -D @babel/core #babel核心es6语法编译
cnpm i -D @babel/node 
cnpm i -D @babel/preset-env
cnpm i -D babel-loader
cnpm i -D cross-env

cnpm i -D clean-webpack-plugin webpack-node-externals @babel/core @babel/node @babel/preset-env babel-loader cross-env
webpack.config.js
.babelrc

模板引擎

art-template

官网

基本使用

index.html

<body>
  <h1>hello {{message}}</h1>
  <ul>
    {{each todos}}
    <li>{{$value.title}} <input type="checkbox" {{ $value.completed?'checked':''}}/> </li>
    {{/each}}
  </ul>
</body>

server.js

const http = require('http')
const fs = require('fs')
const mime = require('mime') //需要安装依赖
const path = require('path')
const template = require('art-template')


const hostname = '127.0.0.1'
const port = 3000

const server = http.createServer((req,res)=>{
  const {url} = req
  if (url === '/') {
    fs.readFile('./index.html', (err, data) => {
      if (err) throw err;

      const html = template.render(data.toString(),{
        message:'world',
        todos:[{title:'吃饭',completed:false},{title:'睡觉',completed:true},{title:'打豆豆',completed:false}]
      })
      res.statusCode = 200
      res.setHeader('Content-Type', 'text/html;charset=utf-8')
      res.end(html)
    });
  }else if(url.startsWith('/static/')){
    fs.readFile(`.${url}`,(err,data)=>{
      if (err) {
        res.statusCode = 404
        res.setHeader('Content-Type', 'text/plain;charset=utf-8')
        res.end('404 Not Found !')
      }
      const contentType = mime.getType(path.extname(url))
      res.statusCode = 200
      res.setHeader('Content-Type', contentType)
      res.end(data)
    })
  }else{
    res.statusCode = 404
    res.setHeader('Content-Type', 'text/plain;charset=utf-8')
    res.end('404 Not Found !')
  }
})

server.listen(port,hostname,()=>{
  console.log(`服务器运行在 http://${hostname}:${port}上`)
})