應用koa的上傳檔案,手動實現koa

weixin_34357887發表於2018-08-03

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) returnPromise.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;
複製程式碼

相關文章