koa框架會用也會寫—(koa-bodyparser、koa-better-body)
- koa-session:讓無狀態的http擁有狀態,基於cookie實現的後臺儲存資訊的session
- koa-mysql:封裝了需要用到的SQL語句
- koa-mysql-session:當不想讓session儲存到記憶體,而想讓session儲存到mysql資料庫中時使用
- koa-router:後臺會接受到各種請求的url,路由會根據不同的url來使用不同的處理邏輯。
- koa-view:請求html頁面時,後臺會用模板引擎渲染資料到模板上,然後返回給後臺
- koa-static:請求img、js、css等檔案時,不需要其他邏輯,只需要讀取檔案
- koa-better-body:post上傳檔案時,解析請求體
koa系列文章:
- ofollow,noindex">koa框架會用也會寫—(koa的實現)
- koa框架會用也會寫—(koa-router)
- koa框架會用也會寫—(koa-view、koa-static)
- koa框架會用也會寫—(koa-bodyparser、koa-better-body)
form標籤的enctype屬性
- application/x-www-form-urlencoded:在傳送前編碼所有字元(不設定預設)
- multipart/form-data:不對字元編碼,在使用包含檔案上傳控制元件的表單時,必須使用該值。
- text/plain:空格轉換為 "+" 加號,但不對特殊字元編碼。
上面的文字看起來可能不夠直觀,所以可以自己傳送請求來檢視:
//前臺頁面login.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <link rel="stylesheet" href="/bootstrap/dist/css/bootstrap.css"> </head> <body> <form action="/fileupload" enctype="text/plain" method="POST"> <div class="form-group"> <label for="username" class="control-label">使用者名稱</label> <input type="text" class="form-control" id="username" name="username"> </div> <div class="form-group"> <label for="password" class="control-label">密碼</label> <input type="text" class="form-control" id="password" name="password"> </div> <div class="form-group"> <label for="avatar" class="control-label">頭像</label> <input type="file" multiple class="form-control" id="avatar" name="avatar"> </div> <div class="form-group"> <button type="submit" class="btn btn-danger">登入</button> </div> </form> </body> </html> 複製程式碼
//後臺服務 const Koa = require('koa'); const path = require('path'); const Router = require('koa-router'); const static = require('koa-static'); const session = require('koa-session'); let app = new Koa(); let router = new Router(); app.keys = ['zfpx','jw']; app.use(session({ maxAge:5*1000, }, app)); app.use(static(path.resolve(__dirname))); app.use(static(path.resolve(__dirname,'node_modules'))); router.post('/login',async(ctx,next)=>{ // 獲取使用者的賬號及密碼 ctx.session.user = ctx.request.body ctx.body = ctx.session.user; }); router.get('/home',async (ctx,next)=>{ if (ctx.session.user){ ctx.body = { status:1, username: ctx.session.user.username } }else{ ctx.body = { status: 0, username: null } } }) router.post('/fileupload',async(ctx,next)=>{ console.log('upload') }) app.use(router.routes()); app.listen(3000); 複製程式碼

將login.html的form表單的enctype屬性分別改成text/plain、application/x-www-form-urlencoded、multipart/form-data:
- text/plain的請求體:空格轉換為 "+" 加號,但不對特殊字元編碼
- application/x-www-form-urlencoded的請求體:在傳送前編碼所有字元(不設定預設)
- multipart/form-data:不對字元編碼,在使用包含檔案上傳控制元件的表單時,必須使用該值。
- 一般的表單提交使用的application/x-www-form-urlencoded,比text/plain多了字串進行編碼
- application/x-www-form-urlencoded、text/plain提交檔案時會把只把檔名提交,內容在請求體中看不到。但是multipart/form-data卻可以
koa-bodyparser和koa-better-body的區別
- koa-bodyparser沒有處理檔案上傳的功能,而koa-better-body處理了檔案上傳功能
- koa-bodyparserh會將請求體掛載在ctx.request.body,而koa-better-body將請求體掛載在ctx.request.fields
koa-bodyparser的原理
//利用buffer來快取資料,kao的中介軟體使用async和await function bodyParser() { return async (ctx, next) => { await new Promise((resolve, reject) => { let arr = []; ctx.req.on('data', function (data) { arr.push(data); }); ctx.req.on('end', function () { let r = Buffer.concat(arr).toString(); ctx.request.body = r; resolve(); }) }); await next(); } } 複製程式碼
koa-better-body的原理


從上面的兩張圖可以看到請求頭中contentType中存在一個boundary屬性,而請求體中的資料正是用這個boundary屬性的值隔開的,去除裡面的內容可以用分割的方法。由於檔案是二進位制的,需要分割buffer,但是Buffer的原型上沒有這個方法,所以要擴充套件這個split方法:
Buffer.prototype.split = function (sep) { let len = Buffer.from(sep).length;//分隔符的位元組長度 let pos = 0; let index = 0; let arr = []; //判斷pos位後面是否還存在boundary //擷取boundary前面的內容放在陣列中 while (-1 != (index = this.indexOf(sep,pos))) { arr.push(this.slice(pos,index)); pos= len + index;//每個(**)b的位置 } arr.push(this.slice(pos));//將最後boundary後面的內容放進陣列 return arr; } //b代表boundary,**代表獲取的內容 let buffer = Buffer.form('b**b**b**b--').split('b') console.log(buffer); //[<Buffer >,<Buffer 2a 2a>,<Buffer 2a 2a>,<Buffer 2a 2a>,<Buffer 2d 2d>] 複製程式碼
截取了需要的內容之後就可以做進一步的處理:

- 內容中包含filename的獲取除綠色部分的內容
- 內容中不包含filename的組成ctx.request.fields.xx = xx
function betterBody({uploadDir}) { return async (ctx,next)=>{ await new Promise((resolve,reject)=>{ let arr = []; ctx.req.on('data', function (data) { arr.push(data); }); ctx.req.on('end', function () { if (ctx.get('content-type').includes('multipart')) { let r = Buffer.concat(arr); //請求體中的內容 //獲取的boundary少了--,加上後用於擷取內容 let boundary = '--' + ctx.get('content-type').split('=')[1]; // 去除頭尾取到中間有用的部分 //[<Buffer 2a 2a>,<Buffer 2a 2a>,<Buffer 2a 2a>] let lines = r.split(boundary).slice(1, -1); let fileds = {}; //處理含filename和不含filename的內容 lines.forEach((line) => { let [head, content] = line.split('\r\n\r\n'); head = head.toString(); if (head.includes('filename')) { // 是檔案取出除head+'\r\n\r\n'後的內容 //-2表示去除最後空白行\r\n let content = line.slice(head.length + 4, -2); let uuid = require('uuid/v4'); let fs = require('fs'); let p = uuid(); fs.writeFileSync(path.resolve(__dirname, uploadDir, p), content); fileds['path'] = p; } else { // 不是檔案,掛載引數例如:username let key = head.match(/name="([\s\S]*)"/im)[1]; //-2表示去除最後空白行\r\n let value = content.toString().slice(0, -2); fileds[key] = value; }; }) ctx.request.fields = fileds; resolve(); } }) }) await next() } } 複製程式碼