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(响应)
工作原理
执行的顺序:顺序执行
回掉的顺序:反向执行
先进后出
同步中间件
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.context 为 ctx 添加其他属性。这对于将 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}上`)
})