實際項目開發過程中,數據庫dao層的增刪改查操作,都要接收到前端頁面傳遞過來的參數,然后再進行操作,那么在使用Mybatis數據庫框架進行開發時,底層dao的參數傳遞怎么處理呢?
Mybatis傳遞參數有以下五種方式可以實現,下面以員工操作為例,看看具體的操作方式。
第一種方式使用順序傳遞參數
EmployeeMapper.java文件:
EmployeeMapper.xml文件:
前陣子被一個 base 北京的小廠面到這個問題,我僅知道實現大文件上傳可以通過切片的方式,但是當面試官問我具體實現細節的時候我就沉默了,這個問題本來就應該是大廠問的,心想現在行業都這么卷了嗎,小廠對實習生問這問題,不行,今天必須把這個文件上傳搞明白
中間的實現還是比較復雜的,主要是后端文件和文件夾名字沖突了,最后干脆摒棄文件夾了
手把手教,小白也能放心食用~
建議跟著敲一遍
先看下不做任何處理,大文件上傳會有什么問題吧~
大文件并沒有一個說超過多少 M 就是大文件,我可以說 100M 就是大文件,也可以說 10M 就是大文件
文件若是一次性上傳,耗時會比較久,切片帶來的好處還有個就是你可以得知進度,比如一個文件切成5份,發一份過去就是20%
前端 vue ,后端 node
前端要實現的就是點擊按鈕可以上傳文件
那就寫一個 input 框,input 框的 type 改成 file 類型,它可以自動拿到本地的文件
<div id="app">
<input type="file" @change="handleChange">
<button>上傳</button>
</div>
<script>
const { createApp, ref }=Vue
createApp({
setup() {
const handleChange=(e)=> {
console.log(e);
}
return {
handleChange
}
}
}).mount('#app')
</script>
想要清楚他拿到的文件長什么樣子我們需要給 input 框綁定一個 change 事件
文件就在事件參數中,e.target.files[0]
展開來看里面的信息,里面有個 lastModified,協商緩存中提到過這個字段,表示的是上一次文件修改的時間,里面還有文件的大小 12468172 的 size 大小,單位是字節 Byte ,除兩次 1024 就是 M 了,這里是 11.8M
這個文件是個視頻,視頻是去年女朋友給我拍的,當時參加英語配音大賽,如今已經分手了,其實這個賬號頭像也是她給我拍的,很多人都以為是網圖
其實我們將文件傳輸給后端,就是這個 file 對象
我們將這個 file 對象進行響應式處理存起來,拿到文件后進行切片,也就是點擊上傳時觸發這個函數,那就再寫一個點擊事件 handleUpload
const handleUpload=()=> {
if (!uploadFile.value) return
const chunkList=createChunk(uploadFile.value)
}
切片函數 createChunk 我拿出來寫
const createChunk=(file, size=1 * 1024 * 1024)=> {
const chunkList=[]
let cur=0
while (cur < file.size) {
chunkList.push({ file: file.slice(cur, cur + size) })
cur +=size
}
return chunkList
}
這個函數才是切片的精髓!函數第二個參數表示默認切片大小為 1M , chunkList 用于存放切片, cur 是切的進度, while 循環切,當切完時 cur=file.size 跳出循環
注意這里, file.slice ???, slice 不是數組和字符串身上的方法嗎, file 是個對象啊,別急,我們不防看看這個 file 的原型
文件的原型是一個 Blob 對象,我們再展開 Blob
嚯~原來 slice 是 Blob 身上的啊,我們不妨看下 cdn 上的 blob 介紹
cdn介紹:Blob 對象表示一個不可變、原始數據的類文件對象。它的數據可以按文本或二進制的格式進行讀取,也可以轉換成 ReadableStream 來用于數據操作。
當我們選中一個文件時,瀏覽器默認會幫我們將文件轉成一個 Blob 對象, Blob 上自帶一個 slice 方法,Blob.slice()接受的參數不是下標,而是起始字節,最終字節
并且 slice 最終返回一個Blob對象
好了, 11.8M 的文件按照 1M 來切就是 12 份,最后一份是 0.8 M,打印看看這個 chunkList 數組
完美!
接下來的邏輯就是拿到朝著后端發請求了,把切片都給過去
這里用 axios 發請求,自行 cdn 引入
如果想要實現進度條,就自行封裝 axios ,請求函數 requestUpload ,請求方法寫死 post ,并且axios 人家支持傳入一個 onUploadProgress 函數用于計算上傳進度
const requestUpload=({ url, method='post', data, headers={}, onUploadProgress=(e)=> e })=> {
return new Promise((resolve, reject)=> {
// axios支持在請求中傳入一個回調onUploadProgress,其目的就是為了知道請求的進度
axios[method](url, data, { headers, onUploadProgress })
.then(res=> {
resolve(res)
})
.catch(err=> {
reject(err)
})
})
}
發請求之前其實還需要對 chunkList 進行一個處理,剛才打印 chunkList 時,里面的每一個切片僅僅只有大小信息,沒有其他參數,后端是需要其他信息的,因為網絡原因,切片不可能是按照順序接收的,這里我給 chunkList 再加上下標,還有文件名,切片名,如下
const handleUpload=()=> {
if (!uploadFile.value) return
const chunkList=createChunk(uploadFile.value)
// console.log(chunkList);
// 另外切片需要打上標記,保證后端正確合并
uploadChunkList.value=chunkList.map(({ file }, index)=> {
return {
file,
size: file.size,
percent: 0,
chunkName: `${uploadFile.value.name}-${index}`,
fileName: uploadFile.value.name,
index
}
})
console.log(uploadChunkList.value);
// 發請求 把切片一個一個地給后端
uploadChunks()
}
chunkList 的每一項都是個對象,里面的 file 才是我們需要的,因此進行解構
uploadFile 里面是有 name 屬性的,就是文件名
uploadChunkList 就是封裝好的切片,這個切片比 chunkList多了其他后端需要的信息, uploadChunkList 被 map 賦值后就直接發請求, uploadChunks 待會兒來寫
我們先打印看下切片是否如預期所示,有這些信息
完美!
好了,現在實現函數 uploadChunks 來發請求
發請求并不是直接將封裝好的切片數組 uploadChunkList 交給后端,因為后端并不認識你這個對象格式,我們需要先將其轉換成數據流。
方才的 Blob 在 mdn 的介紹中就說到了, Blob 可以按二進制的格式進行讀取,也可以用ReadableStream數據操作
這里我用原生 js 的表單數據流來傳遞,因此將其轉成表單格式的數據流,它是二進制的
const uploadChunks=()=> {
const formateList=uploadChunkList.value.map(({ file, fileName, index, chunkName })=> {
// 對象需要轉成二進制數據流進行傳輸
const formData=new FormData() // 創建表單格式的數據流
// 將切片轉換成了表單的數據流
formData.append('file', file)
formData.append('fileName', fileName)
formData.append('chunkName', chunkName)
return { formData, index }
})
}
formateList 只拿封裝好的切片數組中的重要信息 file , fileName ,index ,chunkName ,并且在 map 中創建一個二進制表單數據流,將這些信息掛到 formData 中,最終賦值給 formateList
之前封裝好的 uploadChunkList 被轉換成了 formateList ,這個切片數組是表單數據格式,不妨看看長啥樣
居然看不到長啥樣!其實這就是二進制數據,瀏覽器不會給你看的
為何要轉表單格式?
答: Blob 是 js 獨有的,雖是文件類型,但是不便用于傳輸,后端那么多語言不一定有 Blob 。但是表單格式前后端都有,其實最早前后端傳輸就是這個表單格式
好了,把格式問題弄好后,現在對每一個 form 表單格式的切片進行發請求,依舊用 map 遍歷,每一個表單切片都進行調用方才封裝好的請求函數 requestUpload ,這個函數的里面有個進度條回調函數,我也拿出來寫下
const uploadChunks=()=> {
const formateList=uploadChunkList.value.map(({ file, fileName, index, chunkName })=> {
// 對象需要轉成二進制數據流進行傳輸
const formData=new FormData() // 創建表單格式的數據流
// 將切片轉換成了表單的數據流
formData.append('file', file)
formData.append('fileName', fileName)
formData.append('chunkName', chunkName)
return { formData, index }
})
const requestList=formateList.map(({ formData, index })=> {
return requestUpload({
url: 'http://localhost:3000/upload',
data: formData,
onUploadProgress: createProgress(uploadChunkList.value[index]) // 進度條函數拿出來寫
})
})
}
待會兒后端的路由就寫 upload 路徑
之前的 uploadChunkList 已經準備好了 percent ,createProgress 函數就是用于更改這個 percent 的,拿出來寫
const createProgress=(item)=> {
return (e)=> {
// 為何函數需要return出來,因為axios的onUploadProgress就是個函數體
// 并且這個函數體參數e就是進度
item.percent=parseInt(String(e.loaded / e.total) * 100) // axios提供的
}
}
axios 提供好了寫法,直接用即可
好了,目前前端先寫到這里,現在轉入后端,其實前端還有接口要寫,待會兒再補充
后端要實現的是拿到前端傳過來切片進行合并
后端待會兒要安裝依賴,自行npm init -y下
先簡單寫下,把 http 服務跑起來
const http=require('http')
const server=http.createServer((req, res)=> {
if (req.url==='/upload') {
res.end('hello world')
}
})
server.listen(3000, ()=> {
console.log('listening on port 3000');
})
不用看,這里前端拿不到數據,還沒處理跨域呢,前端用 live server 跑在 5501 端口,后端在 3000 端口
這里用 Cors 解決
// 解決跨域
res.setHeader('Access-Control-Allow-Origin', '*') // 允許所有的請求源來跨域
res.setHeader('Access-Control-Allow-Headers', '*') // 允許所有的請求頭來跨域
需要設置兩個,一個 請求源,一個 請求頭
為了保證跨域請求的安全性,比如 csrf 攻擊,這里再寫個預檢請求。跨域請求時,瀏覽器默認會發一種 options 請求,用于向服務端請求許可,以確定實際請求是否安全,通過預檢請求,服務端可以檢查跨域請求的來源,請求的方法,請求的頭部等信息,再來決定是否允許請求
// 請求預檢
if (req.method==='OPTIONS') {
res.status=200
res.end()
return
}
前端請求的方法是 post ,原生 node 想要拿到 post 請求需要用上中間件 body-parser 進行解析,其實這里我們也不需要這個 body-parser ,因為前端傳過來的請求數據不是正常的對象,而是表單數據
為何 post 請求要特殊點,因為 post 支持更多的編碼類型,并且不對數據類型做限制
前端將數據處理成表單數據后發給傳給后端,請求頭中自動會多出一個Content-Type: multipart/form-data;字段,目的就是告訴后端此時你要接收的數據是表單格式
后端想要解析這個格式,需要npm i multiparty,調用 parse 函數,直接把請求體 req 丟給它,它自動幫你解析
多方 - NPM --- multiparty - npm (npmjs.com)
如果直接拿 req.request 你是拿不到的,是 undefined ,畢竟是 二進制 數據,不會給你看的
if (req.url==='/upload') {
const form=new multiparty.Form()
form.parse(req, (err, fields, files)=> {
if (err) {
console.log(err);
return
}
console.log(fields, files);
})
}
fields 和 files 就是被解析出來的數據, fields 是文件名和切片名,files 是切片的詳細數據,打印下看看
看到沒有,切片順序是亂的,因此待會兒不能直接合并
先把切片,文件名,切片名都拿到
const file=files.file[0]
const fileName=fields.fileName[0]
const chunkName=fields.chunkName[0]
這里我存到 server 文件目錄下,方便演示
先提前準備一個路徑UPLOAD_DIR用于存放切片
const UPLOAD_DIR=path.resolve(__dirname, '.', 'chunks') // 準備好地址用來存切片
resolve 的作用是將路徑進行合并, __dirname 是當前文件的絕對路徑,.是下一級,文件夾就叫 chunks
記得提前引入 path 模塊
創建文件夾用 fse 模塊,這個模塊是 fs 模塊的加強版
fs-extra - npm (npmjs.com)
fse 模塊是 fs 模塊的擴展,它增加了異步遞歸的操作、 Promise 支持以及額外的方法
先要判斷UPLOAD_DIR切片目錄是否存在,不存在則創建這個文件夾
if (!fse.existsSync(UPLOAD_DIR)) { // 判斷目錄是否存在
fse.mkdirsSync(UPLOAD_DIR)
}
node 中很多方法都有同步版本,比如這里 mkdirsSync 是同步創建目錄以及父目錄, Sync 就是表示同步,沒有這個就是異步
現在可以運行試試,前端請求是否真實給我們創建一個 chunk 用于存放切片的目錄
完美!
其實這里本來應該多創建一層目錄的,比如比賽現場.mp4視頻應該還包裹一個文件夾,但是這個文件夾名會與待會兒存入的切片名相沖突,我就干脆摒棄了
現在文件目錄已經創建好了,接下來就是將切片寫入這個目錄下
剛才打印的 files ,里面有個 path,細心的小盆友應該發現了,前端傳入的切片,先是被默認放入到 C盤 的 temp 目錄下,這是操作系統給我們做的,我們現在需要將存放在 temp 下的切片挪到這個 chunks 中
fse.moveSync(file.path, `${UPLOAD_DIR}/${chunkName}`)
現在再試試看,是否幫我們把每個切片的目錄給生成好
很奇怪,明明有12個切片,僅僅給我生成了 6 個,并且下標為 5 的切片還丟了
我這里試了很久,不管怎么試,最終都是 6 個,后面才知道原來這是操作系統的限制問題。操作系統在處理文件和目錄時通常會限制每個目錄中的子項數量,這是為確保文件系統的穩定性。既然如此,那肯定有其他辦法,這里我從簡處理,在前端將切片的大小由原來的 1M 改成 2M ,這樣最終就是 6 個切片了
const createChunk=(file, size=2 * 1024 * 1024)=> ……
切片已經從 C盤 挪到我 chunk 中了,接下來要干的就是合并切片,合并之前一定要將順序捋正來,從剛剛的打印就可以看出,切片的順序是亂的,不過我們已經處理好了,因為切片名 chunkName 最后是有個下標的,這個下標和左邊的一部分被-分隔開,因此我們可以用 split 將字符串分割成數組,傳入-就是取到數組第二項,就是下標
后端什么時候合并切片呢?
這里有幾個方案可以實現
普遍方案都是第二個,前端發完切片后發一個合并請求,開干!
后端
if (req.url==='/upload') {
……
} else if (req.url==='/merge') {
……
}
前端
前端剛才已經發完了所有的切片,繼續在下面發一個合并切片的請求 mergeChunks
前端用 map 格式化好 formateList 切片數組后得到的 requestList 就是一個一個的切片請求數組,剛好放入 Promise.all 中實現并發, then 中寫入請求函數 mergeChunks ,完美!
const uploadChunks=()=> {
……
const requestList=formateList.map(({ formData, index })=> {
……
})
Promise.all(requestList).then(mergeChunks())
}
這也就是切片為何速度更快, Promise.all 實現并發請求
面試官當時問我的就是萬一有個切片失敗了怎么辦,沉默許久,現在心中已經有答案了,后端 fse 用 promise 實現了封裝,里面可以捕獲錯誤,如果切片上傳失敗,我可以記錄好這個失敗切片的索引,告訴前端讓其重傳
腦補:面試官追問:一旦斷網就要重傳,如何解決?
斷點續傳,它允許傳輸文件時,若中斷或失敗,可以從上一次中斷的地方繼續傳輸,而非重新上傳。等我學完再出期文章專門講斷電~
合并請求 mergeChunks 如下
const mergeChunks=()=> {
requestUpload({
url: 'http://localhost:3000/merge',
data: JSON.stringify({
fileName: uploadFile.value.name,
size: 2 * 1024 * 1024
})
})
}
后端不是已經有了切片名和切片大小嘛,為何還要再傳一次?
再上一次保險,另外可以防止傳輸過程中不被篡改,這是為了安全性,當然自己寫的時候完全可以不寫
好了,前后端聯調下,若你是前端,你檢查網絡是很難看到 merge 請求的,因為這相當于是提交表單格式數據,瀏覽器向服務端發這個請求,頁面會重定向刷新的,這需要后端在 merge 路由中看是否有打印
好了,現在前端已經發 merge 請求了,后端需要把前端 post 請求的內容拿到,這里寫個函數 resolvePost去解析 post 請求的內容
我將請求體給到 resolvePost ,希望它能解析出 post 參數
const resolvePost=(req)=> {
return new Promise((resolve, reject)=> {
let chunk='' // 參數數據包
req.on('data', (data)=> {
chunk +=data
})
req.on('end', ()=> {
resolve(JSON.parse(chunk))
})
})
}
這里簡單提一嘴,像是這種工具函數用 const 聲明了,最好寫到前面去,沒有聲明提升
監聽請求體的 data 事件來獲取參數數據,請求接收時,觸發 end 事件,最后將 data ,這個 data 是前端 stringify 的 json 對象,需要 parse 回來,因為拿到數據后還需要進行合并,合并是 I/O 操作, node 中的 I/O 是異步宏,這里又是同步,需要 promise 來捋成同步
const { fileName, size }=await resolvePost(req)
前端發合并請求時傳過來的就是切片名和切片大小,這里解構出來
記得在 http.createServer 中的回調形參前寫 async 關鍵字
好,現在去合并切片 mergeFileChunks ,我告訴這個函數路徑 chunk ,以及方才解構出的切片名和切片大小,讓其幫我合并
await mergeFileChunks(UPLOAD_DIR, fileName, size)
現在實現 mergeFileChunks 函數,這個函數同樣需要 return 一個 promise ,這里直接寫個 async 關鍵字,就相當于函數體中 return了一個 promise
既然要合并切片那就需要先讀取
const mergeFileChunks=async(filePath, fileName, size)=> {
// 讀取filePath下所有的切片
const chunks=await fse.readdir(filePath) // 讀文件夾的所有文件
console.log(chunks)
}
readdir 是讀取文件夾中的所有文件, readfile是讀取文件內容
我們打印下看看 readdir 給我們讀成了什么樣子
剛好是個數組,這次讀的順序是對的,但是網絡情況你也不清楚,因此我們需要將其排序
前面已經提到過,用 split 拿到最后的 index,然后 sort
chunks.sort((a, b)=> a.split('-')[1] - b.split('-')[1])
sort 會影響原數組,此時的 chunks 已經是有序的了
現在進行合并
這個 chunks 別看打印出來是個數組,里面每個切片是個字符串,其實這是真實的文件資源,有后綴的
一個形象的比喻,這些切片都是冰塊,我們需要將其融化成水流,然后才能匯聚在一起
合并之前需要將切片轉換成流類型,我再寫個函數 pipeStream 將這些切片轉成流類型,我們需要告訴這個函數切片的路徑以及切片名
const arr=chunks.map((chunkPath, index)=> {
return pipeStream(
path.resolve(filePath, chunkPath),
fse.createWriteStream( // 合并地點
path.resolve(filePath, fileName),
{
start: index * size, // 0 - 2M, 2M - 4M, ……
end: (index + 1) * size
}
)
)
})
這里的 filePath 是UPLOAD_DIR,需要將其與 chunkPath 合并給到 pipeStream ,這個路徑就是合并切片的文件,就在 chunk 下面。第二個參數指定最終合并到哪里去,需要借助createWriteStream來創建一個可寫流,就相當于創建一個杯子,可以倒水進去進行合并,杯子就是合并的地方,這個 杯子 需要接受兩個參數,一個是路徑,還有個對象,對象里面寫入起始位置和終止位置,這就像是給這個水杯打上刻度, 0-2M 一個刻度, 2-4M 一個刻度……
好了,現在去實現 pipeStream ,第一個參數是合并的路徑,所以依舊是 filePath ,也就是UPLOAD_DIR,第二個參數是可寫流
合并寫入的路徑我最終還是寫在 chunk 中
const pipeStream=(filePath, writeStream)=> {
console.log(filePath);
return new Promise((resolve, reject)=> {
})
}
先看下是否幫我們生成了這個文件
沒問題,只不過我們還沒將數據合并進去,這個文件還是空的
現在將所有的切片先讀到,然后監聽 end 事件,把切片都移除掉,讀到的流最終匯入到可寫流中,也就是第二個參數
const pipeStream=(filePath, writeStream)=> {
console.log(filePath);
return new Promise((resolve, reject)=> {
const readStream=fse.createReadStream(filePath)
readStream.on('end', ()=> {
fse.unlinkSync(filePath) // 移除切片
resolve()
})
readStream.pipe(writeStream) // 將切片讀成流匯入到可寫流中
})
}
最后賦值得到的 arr ,都是一個個的 promise 對象,保證每個切片 resolve 即可
await Promise.all(arr)
最后見證奇跡的時刻到了
切片匯總了
在文件資源管理器中打開看看
完美!
前端拿到整個文件后利用文件 Blob 原型上的 slice 方法進行切割,將得到的切片數組 chunkList 添加一些信息,比如文件名和下標,得到 uploadChunkList ,但是 uploadChunkList 想要傳給后端還需要將其轉換成表單數據格式,通過 Promise.all 并發發給后端,傳輸完畢后發送一個合并請求,合并請求帶上文件名和切片大小信息
后端拿到前端傳過來的表單格式數據需要 multiparty 依賴來解析這個表單數據,然后把切片解析出來去存入切片,存入到提前創建好的目錄中,最后將切片按照下標進行排序再來合并切片,合并切片的實現比較復雜,需要創建一個可以寫入流的文件,將每個片段讀成流類型,再寫入到可寫流中
前端index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<title>Document</title>
</head>
<body>
<div id="app">
<input type="file" @change="handleChange">
<button @click="handleUpload">上傳</button>
</div>
<script>
const { createApp, ref }=Vue
createApp({
setup() {
const uploadFile=ref(null) // 文件
const uploadChunkList=ref([])
const handleChange=(e)=> {
if (!e.target.files[0]) return
uploadFile.value=e.target.files[0]
}
const handleUpload=()=> {
if (!uploadFile.value) return
const chunkList=createChunk(uploadFile.value)
// console.log(chunkList);
// 另外切片需要打上標記,保證后端正確合并
uploadChunkList.value=chunkList.map(({ file }, index)=> {
return {
file,
size: file.size,
percent: 0,
chunkName: `${uploadFile.value.name}-${index}`,
fileName: uploadFile.value.name,
index
}
})
console.log(uploadChunkList.value);
// 發請求 把切片一個一個地給后端
uploadChunks()
}
// 上傳切片
const uploadChunks=()=> {
const formateList=uploadChunkList.value.map(({ file, fileName, index, chunkName })=> {
// 對象需要轉成二進制數據流進行傳輸
const formData=new FormData() // 創建表單格式的數據流
// 將切片轉換成了表單的數據流
formData.append('file', file)
formData.append('fileName', fileName)
formData.append('chunkName', chunkName)
return { formData, index }
})
console.log(formateList); // 瀏覽器不給你展示二進制流,但是得清楚確實拿到了
// 發接口請求
const requestList=formateList.map(({ formData, index })=> {
return requestUpload({
url: 'http://localhost:3000/upload',
data: formData,
onUploadProgress: createProgress(uploadChunkList.value[index]) // 進度條函數拿出來寫
})
})
// 合并切片請求
Promise.all(requestList).then(mergeChunks())
}
// 合并切片
const mergeChunks=()=> {
requestUpload({
url: 'http://localhost:3000/merge',
data: JSON.stringify({
fileName: uploadFile.value.name,
size: 2 * 1024 * 1024
})
})
}
// 上傳的進度
const createProgress=(item)=> {
return (e)=> {
// 為何函數需要return出來,因為axios的onUploadProgress就是個函數體
// 并且這個函數體參數e就是進度
item.percent=parseInt(String(e.loaded / e.total) * 100) // axios提供的
}
}
// 為了實現進度條,封裝請求
const requestUpload=({ url, method='post', data, headers={}, onUploadProgress=(e)=> e })=> {
return new Promise((resolve, reject)=> {
// axios支持在請求中傳入一個回調onUploadProgress,其目的就是為了知道請求的進度
axios[method](url, data, { headers, onUploadProgress })
.then(res=> {
resolve(res)
})
.catch(err=> {
reject(err)
})
})
}
const createChunk=(file, size=2 * 1024 * 1024)=> {
const chunkList=[]
let cur=0 // 當前切片
while (cur < file.size) {
chunkList.push({ file: file.slice(cur, cur + size) })
cur +=size
}
return chunkList
}
return {
handleChange,
handleUpload,
createChunk
}
}
}).mount('#app')
</script>
</body>
</html>
后端app.js
const http=require('http')
const multiparty=require('multiparty')
const path=require('path')
const fse=require('fs-extra')
const UPLOAD_DIR=path.resolve(__dirname, '.', 'chunks') // 準備好地址用來存切片
const resolvePost=(req)=> {
return new Promise((resolve, reject)=> {
let chunk='' // 參數數據包
req.on('data', (data)=> {
chunk +=data
})
req.on('end', ()=> {
resolve(JSON.parse(chunk))
})
})
}
const server=http.createServer(async (req, res)=> {
// 解決跨域
res.setHeader('Access-Control-Allow-Origin', '*') // 允許所有的請求源來跨域
res.setHeader('Access-Control-Allow-Headers', '*') // 允許所有的請求頭來跨域
// 請求預檢
if (req.method==='OPTIONS') {
res.status=200
res.end()
return
}
if (req.url==='/upload') {
const form=new multiparty.Form()
form.parse(req, (err, fields, files)=> {
if (err) {
console.log(err);
return
}
const file=files.file[0] // 切片的內容
const fileName=fields.fileName[0]
const chunkName=fields.chunkName[0]
// 拿到切片先存入再合并,存入的目的就是防止順序錯亂
// const chunkDir=path.resolve(UPLOAD_DIR, `${fileName}-chunks`) // 文件名不同,文件目錄就不同
if (!fse.existsSync(UPLOAD_DIR)) { // 判斷目錄是否存在
fse.mkdirsSync(UPLOAD_DIR) // 創建這個文件
}
// 將切片寫入到文件夾中
fse.moveSync(file.path, `${UPLOAD_DIR}/${chunkName}`)
})
} else if (req.url==='/merge') { // 讓前端請求這個地址表明傳輸完成去合并切片
console.log('merge');
const { fileName, size }=await resolvePost(req) // 解析前端傳過來的參數
console.log(fileName, size);
await mergeFileChunks(UPLOAD_DIR, fileName, size)
res.end('合并成功')
}
})
const mergeFileChunks=async(filePath, fileName, size)=> { // 寫個async就相當于new promise
// 讀取filePath下所有的切片
const chunks=await fse.readdir(filePath) // 讀文件夾的所有文件
console.log(chunks);
// 防止切片順序錯亂
chunks.sort((a, b)=> a.split('-')[1] - b.split('-')[1]) // sort會影響原數組,無需賦值
// 合并片段:轉換成流類型
const arr=chunks.map((chunkPath, index)=> {
return pipeStream(
path.resolve(filePath, chunkPath),
fse.createWriteStream( // 合并
path.resolve(filePath, fileName),
{
start: index * size, // 0 - 3M, 3M - 6M, ……
end: (index + 1) * size
}
)
)
})
await Promise.all(arr)
}
const pipeStream=(filePath, writeStream)=> {
console.log(filePath);
return new Promise((resolve, reject)=> {
const readStream=fse.createReadStream(filePath)
readStream.on('end', ()=> {
fse.unlinkSync(filePath) // 移除切片
resolve()
})
readStream.pipe(writeStream) // 將切片讀成流匯入到可寫流中
})
}
server.listen(3000, ()=> {
console.log('listening on port 3000');
})
作者:Dolphin_海豚
鏈接:https://juejin.cn/post/7356817667574136884
提:
用python寫了一個簡單的log分析,主要也就是查詢一些key,value出來,后面也可以根據需求增加。查詢出來后,為了好看,搞個html 表格來顯示。
需要的組件: jinja2 flask 的模板。
先說下設計思路,主要是練習python代碼玩,高手略過
模擬scrapy,搞個管線
每個管線分預處理,分析器,和后處理。預處理的話,可以篩選下數據,分析器提取關鍵信息,然后把結果丟給后處理。html報表就是在后處理生成。
再搞個manger類,管理很多個管線,雖然現在單路pipeLine就完成了,說不定以后還能擴展呢。
我們可以定義預處理,比如過濾一些不關注的關鍵字,或者關注一些特定關鍵字的行
預處理的話,只處理QtiDCT-C關鍵字的日志行。
然后把經過預處理后的數據丟給分析器
主要查詢行數據行里面是否有keyword,然后根據分隔符,和結束符來提取內容
keyword delimiter xxxxxendwith 這樣個模式
獲取最終結果存儲到字典里面 result[keyword]=xxxx。這里會trim,去掉 \r\n.
這樣就有了結果集result.最后丟給posthandler 后處理。完成報表輸出。
后處理主要是用jinja2的模板,然后傳遞參數,生成最終的html文件。
這里的jinja_template.temple, 內容如下
有了模板,就可以在渲染模板的時候提供字典,變量,在模板里面顯示。最終完成報表的輸出。
最終使用
最終在main 方法中,通過-d參數傳入log所在目錄,然后迭代所有的文件,使用input 把文本文件轉換成行數據的list,丟給管線,最后把管線丟給manager,調用process ,完成txt日志的分析,到最后html的生產。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。