koa 和 session的關係
session 基於cookie ,個人比較喜歡session,但是 koa確實比較輕量
koa
koa需要安裝
- koajs是基於Node.js平臺的web開發框架
- Koa 應用程式是一個包含一組中介軟體函式的物件,它是按照類似堆疊的方式組織和執行的。用法和express類似,但是相對輕量
- Koa 中介軟體以更傳統的方式級聯
- koa是個類
- app是監聽函式
- app有兩個方法 listen use
- koa不整合路由,沒有get, 需要用到koa-router中介軟體
- 封裝了req,res => ctx,還封裝了request,response
- ctx.body === res.end,當時前面可以重複使用,且取最後的值,當所有中介軟體執行完後 會將ctx.body中的內容 取出來 res.end()
- ctx.body === ctx.request.body === ctx.res.end,也就是說ctx會代理ctx.reques,不建議使用原生的方法
- ctx.body可以返回物件,檔案
- ctx.res.setHeader == ctx.response.set == ctx.set
let Koa = require('koa');
let app = new Koa();
let path = require('path');
// ctx中還包含了 request response
let fs = require('fs');
app.use( (ctx,next)=> {
// ctx.request上 封裝了請求的屬性 會被代理到ctx
ctx.set('Content-Type','application/json');
ctx.body = fs.createReadStream(path.resolve(__dirname,'./package.json'));
});
app.listen(3000);
複製程式碼
express中介軟體和koa中介軟體的區別
同步的時候其實是一樣的,只不過非同步會有不同,express不會等待下一個next的完成而koa會
koa中介軟體實現
let Koa = require('koa');
let app = new Koa();
//next前面要麼跟return,要麼跟await否則不知道會不會影后後面的非同步出現問題
app.use(async (ctx,next)=> {
console.log(1);
await next();
console.log(2);
});
function log(){
return new Promise((resolve,reject)=>{
setTimeout(()=> {
resolve('123');
})
})
}
app.use(async (ctx,next)=> {
console.log(3);
let r = await log();
console.log(r);
next();
console.log(4);
});
app.use( (ctx,next)=> {
console.log(5);
next();
console.log(6);
});
// 當所有中介軟體執行完後 會將ctx.body中的內容 取出來 res.end()
app.listen(3000);
複製程式碼
結果就跟同步一樣輸出135642 對於上述問題在express能不能用await解決呢
express中介軟體實現
let express = require('express');
app = express();
function log(){
return new Promise((resolve,reject)=>{
setTimeout(()=> {
resolve('123');
})
})
}
app.use(async (req,res,next)=> {
console.log(1);
await next();
console.log(2);
});
app.use(async (req,res,next)=> {
console.log(3);
let r = await log();
console.log(r);
next();
console.log(4);
});
app.use( (req,res,next)=> {
console.log(5);
next();
console.log(6);
});
app.listen(3000);
複製程式碼
輸出132 123 64,因為在執行到第二個next的時候發現需要等待,他就不會等待,會直接執行下一步next
koa的中介軟體會在內部處理next將其變成中介軟體,那麼我們如何讓express像koa一樣呢?
function app(){
}
function log(){
return new Promise((resolve,reject)=>{
setTimeout(()=> {
resolve('123');
})
})
}
app.routes = [];
app.use = function(cb){
app.routes.push(cb)
}
app.use( async(next)=> {
console.log(1);
await next();
console.log(2);
})
app.use(async (next)=> {
console.log(3);
let r = await log();
console.log(r);
next();
console.log(4);
})
app.use((next)=> {
console.log(5);
console.log(6);
})
let index = 0;
function next(){
if(index === app.routes.lenth) return;
//在原來內部實現方法執行的時候return
return app.routes[index++](next)
}
next();
複製程式碼
在原來內部實現方法執行的時候return,第一個函式中如果等待的是promise那麼會等待這個promise執行完之後在執行,如果返回的是undefined就會跳過,不會等待下一個人執行完之後在執行
利用這個我們寫一個檔案上傳的例子
檔案上傳 ~ koa
之前我們檔案上傳,看怎麼解析請求體,以前我們解析請求體可能是json或者a=b&c=d,這次我們用表單格式
let Koa = require('koa');
// app是監聽函式
let app = new Koa();
let path = require('path');
let fs = require('fs');
app.use(async (ctx,next)=> {
if(ctx.path == '/user' && ctx.method == 'GET'){
ctx.body = `
<form method="POST">
<input name='username' type="text" autoComplete='off'>
<input name='password' type="text" autoComplete='off'>
<input type="submit">
</form>
`
}
await next()
});
function bodyParser(ctx){
return new Promise((resolve,reject)=>{
let buffers = [];
ctx.req.on('data',function(data){
buffers.push(data);
})
ctx.req.on('end',function(){
resolve(Buffer.concat(buffers).toString());
})
})
}
app.use(async (ctx,next)=> {
if(ctx.path == '/user' && ctx.method == 'POST'){
ctx.body = await bodyParser(ctx);
}
next()
});
app.listen(3000);
複製程式碼
我們看到處理data用的buffer,koa本身對這些並沒有封裝,當然我們同樣可以使用中介軟體
koa的中介軟體
koa-bodyparser
...
let bodyParser = require('koa-bodyparser');
app.use(bodyParser()); // 會把請求體的結果放到 req.request.body
...
app.use(async (ctx, next) => {
if (ctx.path === '/user' && ctx.method === 'POST') {
ctx.body = ctx.request.body;
}
next();
});
app.listen(3000)
複製程式碼
koa-bodypaser中介軟體實現
根據上述koa-bodypaser替代部分我們可以大致推測出其實現返回的是promise,但是由於返回的結果在ctx.request.body上,所以會在promise外在包一層(ctx, next)
koa自己實現中介軟體 寫一個函式返回async函式,內部處理好內容,繼續執行即可
function bodyParser() {
return async (ctx,next)=>{
await new Promise((resolve, reject)=>{
let buffers = [];
ctx.req.on('data',function (data) {
buffers.push(data);
})
ctx.req.on('end',function () {
let result = Buffer.concat(buffers);
ctx.request.body = result.toString();
resolve();
})
});
await next();
}
}
複製程式碼
但是bodyparser有個缺點,不支援上傳檔案,比如上傳圖片格式,傳遞方式是二進位制,就不能用tostring轉化了,而且檔案上傳的格式是enctype="multipart/form-data"
這種格式請求後返回的樣子如圖:
如果傳的是檔案,請求體Content-Type
會是: multipart/form-data; boundary=----WebKitFormBoundarywAZ6ljeDoXBrZps6
boundary的內容和請求題的第一行是一樣的
我們如何解析這種格式呢?
let Koa = require('koa');
let app = new Koa();
let fs = require('fs');
Buffer.prototype.split = function (sep) {
let arr = [];
let index = 0;
let len = Buffer.from(sep).length;
let offset = 0;
while (-1 !== (offset = this.indexOf(sep,index))) {
arr.push(this.slice(index,offset));
index = offset + len;
}
arr.push(this.slice(index));
return arr;
}
function bodyParser() {
return async (ctx,next)=>{
await new Promise((resolve, reject)=>{
let buffers = [];
ctx.req.on('data',function (data) {
buffers.push(data);
})
ctx.req.on('end',function () {
let result = Buffer.concat(buffers);
let value = ctx.get('Content-Type');
let boundary = value.split('=')[1];
if(boundary){ // 提交檔案的格式是檔案型別 multipart/form-data
boundary = '--' + boundary; // 分界線
// 將內容 用分界線進行分割 buffer.split()
let arr = result.split(boundary); // []
arr = arr.slice(1,-1);//取出的陣列包括前面的的空格後面的--不要
let obj = {};
arr.forEach(line=>{ // 拆分每一行
let [head,content] = line.split('\r\n\r\n');
// 看一下頭中是否有filename屬性
head = head.toString();
if(head.includes('filename')){ //檔案有filename
// 檔案 content是檔案的內容
let filename = head.match(/filename="(\w.+)"/m);
filename = filename[1].split('.');
filename = Math.random() + '.' + filename[filename.length-1];//檔名唯一
let c = line.slice(head.length+4,-2);
fs.writeFileSync(filename, c ); //寫入檔名字和內容
obj['filename'] = filename;
}else{//普通文字
let key = head.match(/name="(\w+)"/m);//m是多行
key = key[1];
let value = content.toString().slice(0,-2);//內容後面的換行回撤也關掉/r/n
obj[key] = value
}
});
ctx.request.body = obj;
}else{
ctx.request.body = result.toString();
}
resolve();
})
});
await next();
}
}
app.use(bodyParser()); // 會把請求體的結果放到 req.request.body
app.use(async (ctx, next) => {
if (ctx.path === '/user' && ctx.method === 'GET') {
ctx.body = `
<form method="post" enctype="multipart/form-data">
...
</form>
`
}
await next();
});
...
app.listen(3000)
複製程式碼
koa 中的cookie
一般我們的cookie不加密,因為它本身容易被劫持,其次加密之後,可能出來的結果會比原油字串長很多,產生流量消耗,
koa中的cookie是內建的,express也是設定cookie但是例如加{signed:true}這些東西是有cookie-parser提供的
這個過程我們需要安裝koa
koa-router
koa-views
koa-session
koa-static
cookie使用
let Koa = require('koa');
let app = new Koa();
let Router = require('koa-router');
let router = new Router();
app.use(router.routes())
//告訴客戶端服務端支援的方法
app.use(router.allowedMethods()) //405
app.keys = ['hello'];
router.get('/write',(ctx,next)=>{
ctx.cookies.set('name','zdl',{
dimain:'localhist',
path:'/',
maxAge:10*1000,
httpOnly:false,
overwrite:true,
signed:true //用這個屬性必須加app.key
})
ctx.body = 'write ok'
})
router.get('/read',(ctx,next)=>{
ctx.body = ctx.cookies.get('name',{sugned:true}) || 'not fond'
})
app.listen(3000);
複製程式碼
koa-session
實現計數訪問
- session配置是基於cookie的,配置的引數是cookie的引數,其需要簽名
- 用了這個中介軟體可以在ctx上增加session屬性
let Koa = require('koa');
let app = new Koa();
let Router = require('koa-router');
let router = new Router();
let session = require('koa-session');
app.keys = ['hello'];
app.use(session({dimain:'localhost'},app));
router.get('/cross',(ctx,next)=>{
let n = ctx.session.n || 0;
ctx.session.n = ++n;
ctx.body = ctx.session.n;
})
app.use(router.routes())
app.use(router.allowedMethods()) //405
app.listen(3000);
複製程式碼
實現登入許可權管理
基於cookie 和express的類似,這裡我們就不做介紹了,請參考許可權處理 - 用redis實現分散式session~ (cookie && session )
三個路由
- 顯示登入頁面,
- 點選登入 種植cookie
- 客戶端傳送請求驗證是否登入
- 簽名的目的不是加密,只是防止服務端串改,總體來說cookie還是不安全的
koa-session.js
let Koa = require('koa');
let app = new Koa();
let Router = require('koa-router');
let router = new Router();
let fs = require('fs');
let path = require('path');
router.get('/',(ctx,next)=>{
ctx.set('Content-Type','text/html');
ctx.body = fs.createReadStream(path.join(__dirname,'index.html'))
})
router.get('/login',(ctx,next)=>{
ctx.cookies.set('isLogin',true);
ctx.body = {'login':true}
})
router.get('/valiate',(ctx,next)=>{
console.log('hello')
let isLogin = ctx.cookies.get('isLogin');
console.log(isLogin)
ctx.body = isLogin;
})
app.use(router.routes());
app.listen(3000);
複製程式碼
index.html
...
<body>
<div>
<button id='login'>登入</button>
<button id='valiadate'>驗證登入</button>
</div>
<script>
login.addEventListener('click',function(){
let xhr = new XMLHttpRequest();
xhr.open('get','/login',true);
xhr.send();
})
valiadate.addEventListener('click',function(){
let xhr = new XMLHttpRequest();
xhr.open('get','/valiate',true);
xhr.onload = function(){
alert(xhr.response)
}
xhr.send();
})
</script>
</body>
複製程式碼
模版渲染 koa-views ejs
ejs使用
將上述html檔案以ejs的模式渲染 koa-express.js
let Koa = require('koa');
let app = new Koa();
let Router = require('koa-router');
let router = new Router();
let fs = require('fs');
let path = require('path');
let views = require('koa-views');
app.use(views(__dirname, {//以當前路徑作為查詢範圍
map:{html:'ejs'}//設定預設字尾
}));
router.get('/',async (ctx,next)=>{
// 如果不寫return 這個函式執行完就結束了 模板還沒有被渲染,ctx.body = ''
// 如果使用return會等待這個返回的promise執行完後才把當前的promise完成
return ctx.render('ejs.html',{title:'zdl'});
})
app.use(router.routes());
app.listen(3000);
複製程式碼
ejs.html
...
<body>
hello <%=title%>
</body>
複製程式碼
koa實現靜態服務 koa-static
let Koa = require('koa');
let app = new Koa()
let Router = require('koa-router');
let router = new Router;
// let static = require('koa-static');
let fs = require('fs');
let util = require('util');
let path = require('path');
let stat = util.promisify(fs.stat);
let mime = require('mime');
function static(p){
return async (ctx,next) => {
let execFile ;
execFile = path.join(p, ctx.path); // 是一個絕對路徑
try{
let statObj = await stat(execFile);
if(statObj.isDirectory()){
let execFile = path.join(p, 'index.html');
ctx.set('Content-Type', 'text/html');
ctx.body = fs.createReadStream(execFile);
}else{
ctx.set('Content-Type', mime.getType(execFile));
ctx.body = fs.createReadStream(execFile);
}
}catch(e){
// 如果檔案找不到呼叫下一個中介軟體(要加return),下一個中介軟體可能會有非同步操作,希望下一個中介軟體的結果獲取完後再讓當前的promise執行完成
//await也可以,只是return明確表示後面沒有可執行程式碼了
return next();
}
}
}
app.use(static(path.join(__dirname,'public')));
function fn(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{resolve('hello world')},3000)
})
}
router.get('/test',async(ctx,next)=>{
ctx.body = await fn();
})
app.use(router.routes());
app.listen(3000)
複製程式碼
test.html是和當前js一個目錄,但是index.html在public資料夾中,public和當前js在同級目錄
手動實現koa
實現個簡單的koa,包括樣子和錯誤訊息監控,我們先寫一個測試用例,將其基本功能展現,在koa裡面有個lib資料夾,裡面有4個js檔案,下面我們根據功能逐個實現一下這四個檔案
- application.js 應用是他的核心檔案,裡面核心 程式碼是
http.creactServer
- context.js檔案表示上下文,封裝了request和respons
- request.js 裡面有很多和新方法,類似於protype.definePropoty
- respons.js
case.js
let Koa = require('koa');
let app = new Koa();
app.use((ctx, next) => {
//res.end = 'hello'
//ctx.req = ctx.request.req = req
console.log(ctx.req.url);
console.log(ctx.request.req.url);
console.log(ctx.request.url);
console.log(ctx.url);
//ctx 會代理 ctx.request屬性
//資料劫持,基本通過set get實現
console.log(ctx.req.path);
console.log(ctx.request.req.path);
console.log(ctx.request.path);
console.log(ctx.path);
ctx.body = 'hello';
//throw Error('出錯啦')
//ctx.body = {hi:'hello'}
//ctx.body = fs.createReadStream(path.join(__dirname,'package.json'))
})
app.use((ctx,next) => {
ctx.body = 'hello'
})
app.listen(3000)
複製程式碼
先將case.js改成原始的,最後,在通過上下問串在一起
application.js
//框架的核心就是http服務
let http = require('http');
let EventEmitter = require('events');//錯誤監聽事件用的,釋出訂閱
let context = require('./context');
let request = require('./request');
let response = require('./response');
class Koa extends EventEmitter{
constructor(){
super();//繼承專用
//將全域性屬性放到例項上
this.context = context;
this.request = request;
this.response = response;
this.middlewares = [];
}
//koa的和新方法1
use(fn){//函式保留下來,儲存在app裡面,因為可以重複呼叫,所以存的肯定是陣列
this.middlewares.push = fn;
}
//通過req,res創造出Context物件
createContext(req,res){
// 建立ctx物件 request和response是自己封裝的
//Object.creat建立的不會有鏈的關係,新屬性會放到ctx不會放到原始上
let ctx = Object.create(this.context);
//ctx上有reqest,req,response,res屬性
//this.request需要在request.js處理
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
return ctx;
}
// composeFn是組合後的promise
compose(middlewares,ctx){
//目的將第一個函式執行,包裝成promise返回去
function dispatch(index) {
if (index === middlewares.length) return; Promise.resolve();
let fn = middlewares[index];//取第0個
//取出來後讓函式執行,在執行下一個
return Promise.resolve(fn(ctx,()=>dispatch(index+1)))
}
//返回第一個執行完的promise
return dispatch(0);
}
// 通過req和res產生一個ctx物件
handleRequest(req,res){
let ctx = this.createContext(req,res);
//如果沒給ctx.body,我們設定個預設值只要設定了,就改成200
//但是在response.js裡改
res.statusCode = 404;
//koa對函式做了非同步處理,所以conpose是組合後的promise
//然後執行每一個函式,等函式都執行完之後把包取出來,返回函式;
let composeFn = this.conpose(this.middleware,ctx)
composeFn.then(()=>{
let body = ctx.body;
if (body instanceof stream) {
body.pipe(res);
}else if(typeof body === 'object'){
res.end(JSON.stringify(body));
}else if(typeof body === 'string' || Buffer.isBuffer(body)){
res.end(body);
}else{//沒有寫就是not found
res.end('Not Found');
}
}).catch(err=>{ // 如果其中一個promise出錯了就發射錯誤事件即可
this.emit('error',err);
res.statusCode = 500;
res.end('Internal Server Error');
})
}
//koa的核心方法二
listen(){
//fn = (req,res) => {...})
//本身fn裡面有req,res,然而在ctx裡面,我們在fn外面在套一層函式
let server = http.createServer(this.handleRequest.bind(this));
server.listen(...arguments)
}
}
module.exports = Koa;
複製程式碼
this.request沒有url,path等屬性,我們需要在此檔案處理 request.js
let url = require('url');
let request = {
//ctx.req = ctx.request.req = req;
//本身沒有req屬性,但在aplication.js,呼叫url的是ctx.request,ctx.request上有req的屬性,故可以通過ctx.request.url = ctx.request.req.url
get url(){
return this.req.url
},
//處理path
get path(){
return url.parse(this.req.url).pathname
},
get query() {
return url.parse(this.req.url).query
}
...
}
module.exports = request;
複製程式碼
response.js
let response = {
set body(value){
this.res.statusCode = 200;
this._body = value;
},
get body(){
return this._body
}
//這樣取值只能通過ctx.response.body
//我們希望ctx.body = ctx.response.body
//所以需要在context.js檔案代理
//我們同時需要在ctx.body 的時候設定到ctx.request
//同樣取context.js做設定的代理
}
module.exports = response;
複製程式碼
context代理
context
//ctx.path 取的是 ctx.request.path 為鏈讓其互不影響,我們在此用代理的方式
let proto = {};
// ctx.path = ctx.request.path //設定獲取方式預設屬性
//定義獲取器
//defineGetter('request','path');
function defineGetter(property,name) {
proto.__defineGetter__(name,function () {
//ctx.request.path
return this[property][name];
})
}
//ctx = require('context')
//ctx.body = 'hello' 設定的是 ctx.response.body ='hello'
function defineSetter(property, name) {
proto.__defineSetter__(name,function (value) {
this[property][name] = value;
})
}
defineGetter('request','path');
defineGetter('request','url');
defineGetter('response','body');
defineSetter('response','body');
module.exports = proto;
複製程式碼
application
let http = require('http');
let EventEmitter = require('events');//錯誤監聽事件用的
let context = require('./context');
let request = require('./request');
let response = require('./response');
let stream = require('stream');
class Koa extends EventEmitter{
constructor(){
super();
this.context = context;
this.request = request;
this.response = response;
this.middlewares = []
}
use(fn){//函式保留下來
this.middlewares.push(fn);
}
compose(middlewares,ctx){
function dispatch(index) {
if (index === middlewares.length) return Promise.resolve()
let fn = middlewares[index];
return Promise.resolve(fn(ctx,()=>dispatch(index+1)))
}
return dispatch(0);
}
createContext(req,res){
// 建立ctx物件 request和response是自己封裝的
let ctx = Object.create(this.context);
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
return ctx;
}
handleRequest(req,res){
// 通過req和res產生一個ctx物件
let ctx = this.createContext(req,res);
// composeFn是組合後的promise
res.statusCode = 404;
let composeFn = this.compose(this.middlewares, ctx)
composeFn.then(()=>{
let body = ctx.body;
if (body instanceof stream) {
body.pipe(res);
}else if(typeof body === 'object'){
res.end(JSON.stringify(body));
}else if(typeof body === 'string' || Buffer.isBuffer(body)){
res.end(body);
}else{
res.end('Not Found');
}
}).catch(err=>{ // 如果其中一個promise出錯了就發射錯誤事件即可
this.emit('error',err);
res.statusCode = 500;
res.end('Internal Server Error');
})
}
listen(){
let server = http.createServer(this.handleRequest.bind(this));
server.listen(...arguments)
}
}
module.exports = Koa;
複製程式碼