寫在最前
本次分享一個簡易路徑替換工具沙咏。功能很簡單废膘,重點在于掌握:
- 遞歸遍歷文件夾目錄
- 正則替換目標(biāo)內(nèi)容
- 解壓上傳文件厂庇,返回更新后的壓縮文件
源碼地址:https://github.com/Aaaaaaaty/Blog/tree/master/fsPathSys
效果預(yù)覽
結(jié)果對比圖:
PS:后端支持匹配js、css、img口叙、background-image的url的對應(yīng)路徑并進(jìn)行分別替換,當(dāng)前只是展示方便嗅战,前端只傳遞一個路徑將所有匹配的源路徑替換為目標(biāo)路徑妄田。
整體流程
- 前端上傳壓縮包及需要替換的路徑字段
- 后端解壓縮
- 遞歸文件目錄俺亮,找到.js/.css/.html文件并匹配替換路徑
- 壓縮整體文件,返回到前端
整體來說可能會遇到的難點在于對正則的使用疟呐,以及完成替換后將壓縮的文件夾傳回本地脚曾。以前沒怎么寫過正則正好借此機(jī)會來學(xué)習(xí)一波,同時對于文件夾(注意不是文件傳輸F艟摺)傳輸踩了一下坑本讥。畢竟大部分時間做靜態(tài)服務(wù)器我們是只需要返回單個文件不需要以一個文件夾的形式來返回到前端。
解壓縮zip
在nodejs文檔中發(fā)現(xiàn)原生api貌似只支持gzip的解壓縮鲁冯,故引入了第三方插件unzip來解決拷沸。
let inp = fs.createReadStream(path)
let extract = unzip.Extract({ path: targetPath })
inp.pipe(extract)
extract.on('error', () => {
cons('解壓出錯:' + err);
})
extract.on('close', () => {
cons('解壓完成');
})
這個插件有一點坑的地方在于它沒有說明如何監(jiān)聽'close'、'error'等事件薯演。還是我去看源碼里面發(fā)現(xiàn)要通過上面的形式來調(diào)用才能成功:)
遞歸文件目錄
通過fs模塊的stat方法來判斷當(dāng)前路徑是文件還是文件夾來決定是否繼續(xù)遍歷撞芍。
function fsPathSys(path) { //遍歷路徑
let stat = fs.statSync(path)
if(stat.isDirectory()) {
fs.readdir(path, isDirectory) //讀文件夾
function isDirectory(err, files) {
if(err) {
return err
} else {
files.forEach((item, index) => {
let nowPath = `${path}/${item}`
let stat = fs.statSync(nowPath)
if(!stat.isDirectory()) {
...somthing going on
} else {
fsPathSys(nowPath)
}
})
}
}
}
else {
...
}
}
正則匹配
正則的重點則在于如何匹配到需要的地方,以及替換的順序也需要有所考量涣仿。
本次需要匹配的地方有四個:
- script標(biāo)簽下的src
- link標(biāo)簽下的href
- img標(biāo)簽下的src
- css中background-image下的url
由于目標(biāo)地址前的關(guān)鍵字src勤庐、href可能在不同的標(biāo)簽中,同時最初的想法就是有可能不同類型的文件的存放地址是不同的好港。故采用的匹配原則是先將script愉镰、link、img钧汹、background提取出來丈探,然后再分別匹配src、href拔莱、url關(guān)鍵字碗降。
//body:要替換的文本
let data = [
{
'type': 'script',
'point': targetUrl
},
{
'type': 'link',
'point': targetUrl
},
{
'type': 'img',
'point': targetUrl
},
{
'type': 'background',
'point': targetUrl
}
]
data.forEach((obj, i) => {
if(obj.type === 'script' || obj.type === 'link' || obj.type === 'img') {
let bodyMatch = body.match(new RegExp(`<${obj.type}.*?>`, 'g'))
if(bodyMatch) {
bodyMatch.forEach((item, index) => {
let itemMatch = item.match(/(src|href)\s*=\s*["|'].*?["|']/g)
if(itemMatch) {
itemMatch.forEach((data, i) => {
let matchItem = data.match(/(["|']).*\//g)[0].replace(/\s/g, '').slice(1)
if(!replaceBody[matchItem]) {
replaceBody[matchItem] = obj.point
}
})
}
})
}
} else if(obj.type === 'background') {
let bodyMatch = body.match(/url\(.*?\)/g)
if(bodyMatch) {
bodyMatch.forEach((item, index) => {
let itemMatch = item.match(/\(.*\//g)[0].replace(/\s/g, '').slice(1)
if(!replaceBody[itemMatch]) {
replaceBody[itemMatch] = obj.point
}
})
}
}
})
其中關(guān)于正則的使用可以參考這篇文章JS正則表達(dá)式完整教程(略長) 真的是非常詳細(xì),我就不班門弄斧了塘秦∷显ǎ總的來說上面的代碼得到了一個對象,replaceBody尊剔。這個對象的key是要替換的路徑爪幻,value是替換后的路徑:
細(xì)心的童鞋可能會發(fā)現(xiàn),如果現(xiàn)在直接遍歷這個對象進(jìn)行替換是不是就能大功告成了呢须误?肯定不是的:)因為替換要有先后順序挨稿,不然會有大麻煩。
例如我們將要替換'../css/'以及'./css/'京痢,如果我們先替換后者那么之前的'../css/中的'./css/'也會被換掉從而整體替換失敗這并不是我們想要的結(jié)果奶甘。
目前的做法是將對象中的key排序,長的在前祭椰,之后再進(jìn)行替換臭家。這樣至少不會出現(xiàn)上面所提到的情況疲陕。
Object.keys(replaceBody).sort((a,b) => b.length - a.length) //對對象排序
另外還需要注意一個小點即在替換'.'的時候,由于'.'在正則中表示通配符钉赁。那么此時需要先將所有的'.'替換為'.'再進(jìn)行下面的操作鸭轮。
壓縮整體文件,返回到前端
考慮到現(xiàn)在要傳回前端的是一個文件夾橄霉,故要對其進(jìn)行壓縮。采用開啟子進(jìn)程的方式來編寫shell命令來壓縮文件夾邑蒋。(node的zlib模塊我沒找到怎么來壓縮文件夾姓蜂。。有知道的同學(xué)歡迎分享)
let dirName = `${filePath}.tar.gz`
exec(`tar -zcvf ${dirName} ${filePath}`, (error, stdout, stderr) => {
if (error) {
cons(`exec error: ${error}`);
return;
}
let out = fs.createReadStream(dirName)
res.writeHead(200, {
'Content-type':'application/octet-stream',
'Content-Disposition': 'attachment; filename=' + dirName.match(/ip_.*/)[0]
})
out.pipe(res)
})
這里的重點是將壓縮包用流的形式讀取出來如果不在返回頭加入'Content-Disposition'字段医吊,返回的文件將是那種類似buffer流的形式钱慢,沒有了文件夾層級結(jié)構(gòu)等等。卿堂。查閱了資料才發(fā)現(xiàn)是因為這個頭的緣故束莫。
Content-disposition 是 MIME 協(xié)議的擴(kuò)展,MIME 協(xié)議指示 MIME 用戶代理如何顯示附加的文件草描。
小結(jié)
本次實現(xiàn)這個小工具览绿,使作者正則還有文件在后端的壓縮解壓以及http傳輸中的細(xì)節(jié)有了新的認(rèn)識。源代碼在git上歡迎clone~
參考文獻(xiàn)
最后
慣例po作者的博客穗慕,不定時更新中——
有問題歡迎在issues下交流饿敲。