前言
Koa是基于Node.js的下一代web框架,由Express團(tuán)隊(duì)打造,特點(diǎn):優(yōu)雅、簡潔、靈活、體積小。幾乎所有功能都需要通過中間件實(shí)現(xiàn)。
準(zhǔn)備
- 檢查Node版本,至少在7.6.0以上,因?yàn)镵oa采用很多Es7的語法,比如async/await,
- 創(chuàng)建項(xiàng)目、安裝依賴
mkdir study_koa
cd study_koa
npm init -y
npm install koa
一、基本用法
- 創(chuàng)建一個(gè)應(yīng)用程序 新建app.js
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
// ctx.body 即服務(wù)端響應(yīng)的數(shù)據(jù)
ctx.body = 'Hello Koa';
})
// 監(jiān)聽端口、啟動(dòng)程序
app.listen(3000, err => {
if (err) throw err;
console.log('runing...');
})
- 啟動(dòng)
node app.js
- 訪問 127.0.0.1:3000 會(huì)看到頁面顯示Hello Koa
即便沒有給ctx.body 設(shè)置響應(yīng)數(shù)據(jù),或訪問不存在的路由,頁面也會(huì)顯示Not Found,這是koa底層做了處理,不像原生Node或Express一樣頁面會(huì)一直處于響應(yīng)狀態(tài)。
二、Context
Koa將Node的request 和 response對象都封裝到了context中,每次請求都會(huì)創(chuàng)建一個(gè)ctx,并且在中間件中作為接收器使用。
以下是剛才訪問時(shí)的ctx對象
let ctx = {
// 請求
request: {
method: 'GET',
url: '/',
// request header
header: {
host: '127.0.0.1:3030',
connection: 'keep-alive',
'cache-control': 'max-age=0',
'upgrade-insecure-requests': '1',
'user-agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.67 Safari/537.36',
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8',
cookie: 'connect.sid=s%3AnQtQApNcQ55RmpjnkmQvWNTrdjYhZnlh.1FQUbVqpwpdRj8N6wjv8nOarf8hyzIpcxXN2LPYXGy0'
}
},
// 響應(yīng)
response: {
status: 200,
message: 'ok',
header: {
'content-type': 'text/plain; charset=utf-8',
'content-length': '9'
}
},
app: {
subdomainOffset: 2,
proxy: false,
env: 'development'
},
originalUrl: '/',
// 原生Node的request對象
req: '<original node req>',
// 原生Node的reponse對象
res: '<original node res>',
socket: '<original node socket>'
}
剖析ctx
- 區(qū)分request、response、req、res
- requset ctx的請求對象
- response ctx的響應(yīng)對象
- req Node的請求對象
- res Node的響應(yīng)對象
注意:繞過Koa的response是不被處理的,避免使用Node的屬性和方法,比如:
res.statusCode
res.writeHead()
res.write()
res.end()
即便使用ctx.res.write()也不會(huì)得到預(yù)期結(jié)果,比如:ctx.res.write('hello'),結(jié)果是hellook,會(huì)把message的值拼接上。
- ctx.state
推薦的命名空間,用于通過中間件傳遞信息到你的前端視圖。比如每個(gè)頁面都要用到用戶信息,那就可以掛載在ctx.state。類似于添加全局屬性。
ctx.state.userInfo = {
name: 'Jack',
age: 18
}
- ctx.app
應(yīng)用程序?qū)嵗?/li>
有關(guān)cookie和session單獨(dú)介紹用法。
三、路由
Koa中的路由和Express不同,Express是把路由集成在Express中,Koa則需要通過kao-router模塊使用。
- 安裝:
npm i koa-router
- 使用
const Koa = require('koa');
// 直接調(diào)用的方式
const router = require('koa-router')();
// 或 單獨(dú)創(chuàng)建router的實(shí)例
const Router = require('koa-router');
const router = new Router();
router.get('/', async ctx => {
ctx.body = 'Hello Router';
})
// 啟動(dòng)路由
app.use(router.routes()).use(router.allowedMethods())
// 以上為官方推薦方式,allowedMethods用在routes之后,作用是根據(jù)ctx.status設(shè)置response header.
app.listen(3000, err => {
if (err) throw err;
console.log('runing...');
});
四、中間件
Koa最大的特色和最優(yōu)的設(shè)計(jì)就是中間件,就是在匹配路由之前和匹配路由之后執(zhí)行函數(shù)。
使用app.use()加載中間件。每個(gè)中間件接收兩個(gè)參數(shù),ctx對象和next函數(shù),通過調(diào)用next將執(zhí)行權(quán)交給下一個(gè)中間件。
中間件分為:
- 應(yīng)用級(jí)中間件
- 路由級(jí)中間件
- 錯(cuò)誤處理中間件
- 第三方中間件
- 應(yīng)用級(jí)中間件
const Koa = require('koa');
const Router = require('koa-router');
const app = new Koa();
const router = new Router();
// 應(yīng)用級(jí)中間件
app.use(async (ctx, next) => {
await next();
})
router.get('/', async ctx => {
ctx.body = 'hello koa';
})
// 啟動(dòng)路由
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000, err => {
if (err) throw err;
console.log('runing...')
})
任何路由都會(huì)先經(jīng)過應(yīng)用級(jí)中間件,當(dāng)執(zhí)行完成next后再去匹配相應(yīng)的路由。
- 路由中間件
router.get('/user', async (ctx, next) => {
console.log(111)
await next();
})
router.get('/user', async (ctx, next) => {
console.log(222)
await next();
})
router.get('/user', async ctx => {
console.log(333)
ctx.body = 'Hello'
})
// 依次打印
111
222
333
路由匹配過程中,對于相同路由會(huì)從上往下依次執(zhí)行中間件,直到最后一個(gè)沒有next參數(shù)的中間件為止。
- 錯(cuò)誤處理中間件
app.use(async (ctx, next)=> {
await next();
if(ctx.status === 404){
ctx.body="404頁面"
}
});
路由在匹配成功并執(zhí)行完相應(yīng)的操作后還會(huì)再次進(jìn)入應(yīng)用級(jí)中間件執(zhí)行 next 之后的邏輯。所以對于404、500等錯(cuò)誤可以在最外層的(第一個(gè))應(yīng)用級(jí)中間件的next之后做相應(yīng)的處理。
如果只有一個(gè)應(yīng)用級(jí)中間件的話,順序就無所謂所有路由中間件之前和之后了。
第三方中間件
類似于koa-router、koa-bodyparser等就是第三方中間件。中間件的合成
koa-compose模塊可以將多個(gè)中間件合成為一個(gè)。
const compose = require('koa-compose')
const first = asycn (ctx, next) => {
await next();
}
const second = async ctx => {
ctx.body = 'Hello';
}
const middle = compose([first, second]);
app.use(middle);
- 中間件的執(zhí)行順序
多個(gè)中間件會(huì)形成堆棧結(jié)構(gòu),按先進(jìn)后出順序執(zhí)行
app.use(async (ctx, next) => {
console.log('1中間件第1次執(zhí)行')
await next();
console.log('7中間件第7次執(zhí)行')
})
app.use(async (ctx, next) => {
console.log('2中間件第2次執(zhí)行');
await next();
console.log('6中間件第6次執(zhí)行')
})
router.get('/user', async (ctx, next) => {
console.log('3中間件第3次執(zhí)行')
await next()
console.log('5中間件第5次執(zhí)行')
})
router.get('/user', (ctx, next) => {
console.log('4中間件第4次執(zhí)行')
ctx.body = 'Hello Koa';
})
// 1中間件第1次執(zhí)行
// 2中間件第2次執(zhí)行
// 3中間件第3次執(zhí)行
// 4中間件第4次執(zhí)行
// 5中間件第5次執(zhí)行
// 6中間件第6次執(zhí)行
// 7中間件第7次執(zhí)行
由此可以看出中間件的執(zhí)行順序是先進(jìn)后出的方式。類似于洋蔥圖。

五、獲取請求數(shù)據(jù)
- GET 傳值
-
Koa 中 GET傳值通過request接收,有兩種方式: query 和 querystring
query:返回的是參數(shù)對象。 {name: 'jack', age: 12}
querystring:返回的是請求字符串。 name=jack&age=12 query和querystring可以從request中獲取,也可以直接從ctx中獲取。
let request = ctx.request;
let query = request.query;
let querystring = request.querystring;
// 直接ctx獲取
ctx.query
ctx.querystring
- POST 傳值
通過post傳遞的值我們可以通過原生Node封裝,也可以通過第三方模塊接收。
- 自定義封裝
const querystring = require('querystring');
module.exports = ctx => {
return new Promise((resolve, reject) => {
try {
let data = '';
// ctx.req實(shí)際上就是原生node中的req
ctx.req.on('data', (chunk) => {
data += chunk;
})
ctx.req.on('end', () => {
data = querystring.parse(data);
resolve(data);
})
}
catch(err) {
reject(err);
}
})
}
- 使用koa-bodyparser模塊
const bodyParser = require('koa-bodyparser');
app.use(bodyParser());
// 獲取
ctx.request.body
六、處理靜態(tài)資源
對于諸如js、css、img等靜態(tài)資源采用koa-static中間件處理。
npm i koa-static
配置:
比如靜態(tài)目錄為static:
// 靜態(tài)資源配置
// app.use(require('koa-static')('static'))
// or
// app.use(require('koa-static')('./static'))
// or
// app.use(require('koa-static')(__dirname + '/static'))
// or 使用path.join() 的時(shí)候,static前面的/可加可不加,該方法會(huì)內(nèi)部會(huì)做處理
app.use(require('koa-static')(path.join(__dirname, 'static')));
在模板中即可訪問:
<link rel="stylesheet" href="/css/header.css">
<img src="/image/account.eb695dc.png"/>
七、模板引擎
koa生態(tài)的模板引擎挺多的,比如ejs、art-template等。
1. koa-ejs
npm i koa-ejs
const render = require('koa-ejs');
- 配置
render(app, {
// views 視圖根目錄
root: path.join(__dirname, 'views'),
layout: 'template', // 公用文件 若要禁用,設(shè)置為false即可
viewExt: 'html', // 擴(kuò)展名
cache: false, // 緩存 default true
debug: false // 如果開啟debug模式,則會(huì)在終端實(shí)時(shí)打印信息 default false
});
- 使用
/**
* 參數(shù)1: 模板名
* 參數(shù)2: 數(shù)據(jù)(可選)
*/
await ctx.render(templateName, data);
2. art-template
npm install --save art-template
npm install --save koa-art-template
- 配置
const render = require('koa-art-template');
// 配置模板引擎
render(app, {
root: path.join(__dirname, 'views'), // 視圖目錄
extname: '.html',
debug: process.env.NODE_ENV !== 'production'
});
使用方式和ejs一樣。
性能上相比,art-template比ejs快很多,開發(fā)中用的最多的還是art-template。
八、cookie 和 session
http是無狀態(tài)、無連接的。不會(huì)對之前發(fā)生過的請求和相應(yīng)狀態(tài)進(jìn)行管理。也就是說,無法根據(jù)之前的狀態(tài)進(jìn)行本次的請求處理。
比如訪問淘寶首頁并登錄賬號(hào)后,當(dāng)再打開淘寶其他頁面時(shí),因?yàn)槊恳淮蔚脑L問都是獨(dú)立的,服務(wù)器并不知道你已經(jīng)登錄,所以還是不能下單或者加購物車之類的操作。
cookie
- cookie簡單介紹
cookie是客戶端第一次訪問服務(wù)器的時(shí)候,服務(wù)器在下行HTTP報(bào)文時(shí)通過響應(yīng)頭的set-cookie字段給瀏覽器分配的一個(gè)具有特殊標(biāo)識(shí)的文本信息,此后當(dāng)客戶端再次訪問同一域名時(shí),便會(huì)將該字段通過請求頭攜帶到服務(wù)器。注意: 第一次訪問服務(wù)器是不可能攜帶cookie的。
缺陷: 1、cookie的數(shù)據(jù)存放在客戶端,不安全,容易被(CSRF)跨站請求偽造。攻擊者可以借助受害者的 Cookie 騙取服務(wù)器的信任,可以在受害者毫不知情的情況下以受害者名義偽造請求發(fā)送給受攻擊服務(wù)器,從而在并未授權(quán)的情況下執(zhí)行在權(quán)限保護(hù)之下的操作。2、單個(gè)cookie保存的數(shù)據(jù)不能超過4K,很多瀏覽器都限制一個(gè)站點(diǎn)最多保存20個(gè)cookie。
- Koa中使用cookie
- 設(shè)置cookie
ctx.cookies.get(name, [options])
通過 options 獲取 cookie name:
signed 所請求的cookie應(yīng)該被簽名
- 設(shè)置cookie
ctx.cookies.set(name, value, [options])
通過 options 設(shè)置 cookie name 的 value:
- maxAge: 有效期(一個(gè)數(shù)字表示從 Date.now() 得到的毫秒數(shù))
- signed: cookie 簽名值
- expires: cookie 過期的 Date
- path: cookie作用域(指定了主機(jī)下的哪些路徑可以接受Cookie), 默認(rèn)是
/ - domain: cookie作用域 (指定了哪些主機(jī)可以接受Cookie。如果不指定,默認(rèn)為當(dāng)前文檔的主機(jī)<不包含子域名>;如果指定了
Domain,則一般包含子域名。
) - secure: 安全 cookie。即標(biāo)記為 Secure 的Cookie只可以通過被HTTPS協(xié)議加密過的請求發(fā)送給服務(wù)端。
- httpOnly: 只可讓服務(wù)器通過http請求訪問cookie,不可通過js的
document.cookieAPI訪問,可以避免XSS攻擊。 默認(rèn)是 true。 - SameSite: 允許服務(wù)器要求某個(gè)cookie在跨站請求時(shí)不會(huì)被發(fā)送,從而可以阻止跨站請求偽造攻擊(CSRF)??蛇x值:
- None:瀏覽器會(huì)在同站請求、跨站請求下繼續(xù)發(fā)送cookies,不區(qū)分大小寫。
- Strict: 瀏覽器將只在訪問相同站點(diǎn)時(shí)發(fā)送cookie。
- Lax: 在新版本瀏覽器中,為默認(rèn)選項(xiàng)。大多數(shù)情況不發(fā)送第三方 Cookie,只有當(dāng)用戶從外部站點(diǎn)導(dǎo)航到URL時(shí)才會(huì)發(fā)送,包括鏈接(a標(biāo)簽)、預(yù)加載(link)、get表單。
- overwrite: 一個(gè)布爾值,表示是否覆蓋以前設(shè)置的同名的 cookie (默認(rèn)是 false). 如果是 true, 在同一個(gè)請求中設(shè)置相同名稱的所有 Cookie(不管路徑或域)是否在設(shè)置此Cookie 時(shí)從 Set-Cookie 標(biāo)頭中過濾掉。
設(shè)置中文cookie
通過buffer轉(zhuǎn)成base64存進(jìn)去,取出來是再轉(zhuǎn)回中文。
// 轉(zhuǎn)base64
Buffer.from('張三').toString('base64');
// 轉(zhuǎn)回中文
Buffer.from(buf, 'base64').toString();
session
- seeion簡單介紹
session是另一種記錄客戶狀態(tài)的機(jī)制,不同的是cookie保存在客戶端瀏覽器中,而session保存在服務(wù)器上。
前面說過,cookie 是存放在客戶端,不是很安全,用戶可以自己手動(dòng)把cookie種在客戶端以欺騙服務(wù)器。而session是存儲(chǔ)在服務(wù)端的,所以對于較重要的數(shù)據(jù)存儲(chǔ)在session。
缺點(diǎn): session會(huì)在一定時(shí)間內(nèi)保存在服務(wù)器上。當(dāng)訪問增多,會(huì)比較占用你服務(wù)器的性能。
- session 的工作機(jī)制
當(dāng)瀏覽器第一次請求服務(wù)器時(shí),服務(wù)器端會(huì)創(chuàng)建一個(gè)session對象,生成一個(gè)類似于key-value的鍵值對, 然后將key(cookie)下發(fā)到客戶端,當(dāng)客戶端再訪問時(shí),攜帶key(cookie),找到對應(yīng)的session(value)。 生產(chǎn)中用戶的信息都保存在session中。
- koa-session 的使用
npm install koa-session
const Koa = require('koa');
const session = require('koa-session');
const app = new Koa();
app.keys = ['some secret hurr'];
const CONFIG = {
key: 'koa:sess', // 返給瀏覽器 cookie 的key 默認(rèn)是 'kao:sess'
maxAge: 86400000, // cookie的過期時(shí)間 maxAge in ms (default is 1 days)
autoCommit: true, // (boolean) 自動(dòng)給客戶端下發(fā)cookie 并設(shè)置session
overwrite: true, // 是否可以覆蓋之前同名的cookie (默認(rèn)default true)
httpOnly: true, // cookie是否只有服務(wù)器端可以訪問 httpOnly or not (default true)
signed: true, // 簽名默認(rèn)true
rolling: false, // 在每次響應(yīng)時(shí)強(qiáng)制設(shè)置session標(biāo)識(shí)符cookie,到期時(shí)被重置設(shè)置過期倒計(jì)時(shí)。(默認(rèn)為false)
renew: false, // 當(dāng)session快過期時(shí)更新session,這樣就可以始終保持用戶登錄 默認(rèn)是false
};
以上配置選項(xiàng)常用的就是key、maxAge、httpOver。
renew應(yīng)用:比如我們登錄賬號(hào)寫一篇博客,寫了一半cookie過期了,當(dāng)我們提交的時(shí)候就會(huì)退出登錄,體驗(yàn)很不好,而且寫好的博客丟失。
九、重定向
301和302重定向狀態(tài)碼區(qū)別。302為臨時(shí)重定向,301永久重定向。Koa中默認(rèn)為302。詳細(xì)信息查看這篇博客 301和302重定向介紹
字符串 “back” 是特別提供Referrer支持的,當(dāng)Referrer不存在時(shí),使用 alt 或“/”。
ctx.redirect('back');
ctx.redirect('back', '/login');
ctx.redirect('/login');
要更改 “302” 的默認(rèn)狀態(tài),只需在該調(diào)用之前或之后分配狀態(tài)。要變更主體請?jiān)诖苏{(diào)用之后:
ctx.status = 301;
ctx.redirect('/cart');
ctx.body = 'Redirecting to shopping cart';
十、跨域請求
解決跨域的方式有很多種,個(gè)人認(rèn)為最好的方案是在服務(wù)器端設(shè)置支持跨域。
原生設(shè)置方式:
下面詳細(xì)說明在koa2中設(shè)置具體的請求頭信息:
app.use(async (ctx, next) => {
// 允許所有域名請求
ctx.set("Access-Control-Allow-Origin", "*")
// 只允許 http://localhost:8080 域名的請求
// ctx.set("Access-Control-Allow-Origin", "http://localhost:8080");
// 設(shè)置允許的跨域請求方式
ctx.set("Access-Control-Allow-Methods", "OPTIONS, GET, PUT, POST, DELETE")
// 字段是必需的。值一個(gè)逗號(hào)分隔的字符串,表示服務(wù)器所支持的所有頭信息字段.
ctx.set("Access-Control-Allow-Headers", "x-requested-with, accept, origin, content-type")
// 服務(wù)器收到請求以后,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,確認(rèn)允許跨源請求,即可做出響應(yīng)。
// Content-Type表示具體請求中的媒體類型信息
ctx.set("Content-Type", "application/json;charset=utf-8")
// 該字段可選。它的值是一個(gè)布爾值,表示是否允許發(fā)送Cookie。默認(rèn)情況下,Cookie不包括在CORS請求之中。 當(dāng)設(shè)置成允許請求攜帶憑證cookie時(shí),需要保證"Access-Control-Allow-Origin"是服務(wù)器有的域名,而不能是"*";
ctx.set("Access-Control-Allow-Credentials", true);
// 該字段可選,用來指定本次預(yù)檢請求的有效期,單位為秒。
// 當(dāng)請求方法是PUT或DELETE等特殊方法或者Content-Type字段的類型是application/json時(shí),服務(wù)器會(huì)提前發(fā)送一次請求進(jìn)行驗(yàn)證
// 下面的的設(shè)置只本次驗(yàn)證的有效時(shí)間,即在該時(shí)間段內(nèi)服務(wù)端可以不用進(jìn)行驗(yàn)證
ctx.set("Access-Control-Max-Age", 300);
/*
CORS請求時(shí),XMLHttpRequest對象的getResponseHeader()方法只能拿到6個(gè)基本字段:
Cache-Control、
Content-Language、
Content-Type、
Expires、
Last-Modified、
Pragma。
*/
// 需要獲取其他字段時(shí),使用Access-Control-Expose-Headers,
// getResponseHeader('myData')可以返回我們所需的值
ctx.set("Access-Control-Expose-Headers", "myData")
await next()
})
使用中間件方式
在koa2中,解決跨域請求還可使用中間件koa2-cors
- 安裝:
npm i koa2-cors -S
- 注冊中間件
const cors = require('koa2-cors')
app.use(cors())
十一、node發(fā)送郵件
node 發(fā)送郵件可以使用nodemailer三方模塊。
安裝:
npm i nodemailer
const nodemailer = require("nodemailer")
async function main() {
let transporter = nodemailer.createTransport({
host: "smtp.qq.com",
port: 465,
secure: true, // true for 465, false for other ports
auth: {
user: 'test@qq.com',
pass: 'akphfubplzqdbdfh'
}
})
let info = await transporter.sendMail({
from: 'test@qq.com', // sender address
to: "bar@qq.com", // 接收地址 多個(gè)郵箱使用 ','分割
subject: "Hello 老胖", // 郵件主題
text: "胖子蹲啊胖子蹲胖子蹲完瘦子蹲", // plain text body
// html: "<b>嗯好</b>" // html body
})
console.log("Message sent: %s", info.messageId)
console.log("Preview URL: %s", nodemailer.getTestMessageUrl(info))
}
main().catch(console.error)
使用說明:
創(chuàng)建transporter配置信息
在node_modules>nodemailer>lib>well_known>services.json中查看對應(yīng)的配置信息。配置發(fā)件郵箱相應(yīng)的host、port及secure。-
獲取發(fā)件箱密碼(類似于授權(quán),獲取key),以qq郵箱為例
qq郵箱>設(shè)置>賬戶>POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服務(wù),點(diǎn)擊開啟獲取pass授權(quán).png -
配置user和pass
- user即發(fā)件地址
- pass就是剛才授權(quán)獲取的密碼
-
sendMail配置信息
- from 發(fā)件地址
- to 收件地址 如果多個(gè)收件地址,用
,分割即可 - subject 郵件主題
- text 文件內(nèi)容
- html html內(nèi)容
注意:text和html只能配置一個(gè)
更多詳細(xì)配置信息及功能參照官網(wǎng)
