应用koa的上传文件,手动实现koa

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;
复制代码
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值