session 基於cookie ,我的比較喜歡session,可是 koa確實比較輕量javascript
koa須要安裝html
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不會等待下一個next的完成而koa會java
koa中間件實現react
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解決呢web
express中間件實現redis
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的時候發現須要等待,他就不會等待,會直接執行下一步nextexpress
koa的中間件會在內部處理next將其變成中間件,那麼咱們如何讓express像koa同樣呢?json
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就會跳過,不會等待下一我的執行完以後在執行數組
利用這個咱們寫一個文件上傳的例子promise
以前咱們文件上傳,看怎麼解析請求體,之前咱們解析請求體多是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自己對這些並無封裝,固然咱們一樣可使用中間件
...
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替代部分咱們能夠大體推測出其實現返回的是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)
複製代碼
通常咱們的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);
複製代碼
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 )
三個路由
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>
複製代碼
將上述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>
複製代碼
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裏面有個lib文件夾,裏面有4個js文件,下面咱們根據功能逐個實現一下這四個文件
http.creactServer
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
//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;
複製代碼