工具应用场景 -> 针对大量线上资源(css/img/js/font等CDN资源)快速转化为本地路径引用
需要的核心依赖:
思考过程分析:
开始前先分析要操作的目录结构:
有目录,也有文件,也有重名的文件和目录demo,目录里再套目录,目录中,也有配置文件config,也有独立放在根目录的pub.html,所以情况有很多种,但归根结底都是一个流程,那就是:
查找->匹配->下载->替换
好~,那我们开始吧!
第一步:
需要一个正则来匹配要下载的线上资源:以.jpg举例:
https://xxx.CDN1.com/1e68e9094ddd/demo.jpg
那么正则应该这么写:/(http[s]?:)?\/\/[0-9a-zA-Z.]*CDN1.*
\.(jpg|jpeg|gif|png|webp|ico)/g
注意1,资源可能是http的,也可能是https的
注意2,注意标红的问号,是正则的停止匹配的贪婪模式。如果不加的话,会一直匹配到下一个符合正则的url。
第二步:
1,设定从哪里开始搜索(比如从项目里的某一个目录)
2,可以从哪些不同文件类型的文件进行匹配(.html/.css/.php/.vue等)
3,匹配后的资源下载到本地的哪个文件夹下
第三步:对想要匹配的路径内的所有目录进行递归读取
let fsPathSys = (fspath, targetFile) => {
let stat = fs.statSync(fspath);
if (stat.isDirectory()) { //是否是目录
let isDirectory = (err, files) => {
//读取目录里所有文件
if (err) {
console.log("err", err);
return err;
} else {
files.forEach((item, index) => {
if (item === "__MACOSX") {
//删除mac无用文件
execSync("rm -rf " + fspath + "/__MACOSX");
} else {
let nowPath = path.join(fspath, item);
let isNeed = true;
switch(true){
case EXCLUDETARGET.path.length > 0:
EXCLUDETARGET.path.forEach((item,i) => {
//EXCLUDETARGET.path -> 可以指定不想搜索的文件路径或文件夹路径
if (
nowPath ===
path.join(__dirname, LOCMAP.searchPath, item)
) {
isNeed = false;
return;
}
})
case EXCLUDETARGET.keyword.length > 0:
EXCLUDETARGET.keyword.forEach((item,i) => {
//EXCLUDETARGET.keyword -> 可以指定不想搜索带有关键词的文件或者文件夹
if (
nowPath.split(path.sep).join('/').match(new RegExp('\/'+item+'\/','g'))
) {
isNeed = false;
return;
}
})
}
if (!isNeed) return;
let stat = fs.statSync(nowPath);
if (!stat.isDirectory()) {
//如果路径不是目录,说明到底了
if(targetFile.length > 0){
targetFile.forEach((obj, index) => {
if (item.indexOf(obj) != -1) {
readMatchFile(nowPath);
}
});
}else{
readMatchFile(nowPath);
}
} else {
//如果是目录,再执行
fsPathSys(nowPath, targetFile);
}
}
});
}
}
fs.readdir(fspath, isDirectory); //读取路径
} else {
targetFile.forEach((obj, index) => {
readMatchFile(fspath);
});
}
}
/*读取文件流*/
let readMatchFile = (curPath) => {
let readAble = fs.createReadStream(curPath);
let body = "";
readAble.on("data", chunk => {
body += chunk;
});
readAble.on("end", () => {
matchDemo(curPath, body);
});
}
/*读取文件流*/
第四步:对匹配的资源进行建本地目录操作
/*递归创建目录*/
let mkdirsSync = (name) => {
if (fs.existsSync(name)) {
return true;
} else {
if (mkdirsSync(path.dirname(name))) {
fs.mkdirSync(name);
return true;
}
}
}
/*递归创建目录*/
/*匹配替换并开始下载资源*/
let matchDemo = (curPath, body) => {
//读取文件内容,进行内容的替换
let proDestDir = '',targetPath = '',dirname = curPath.split(path.sep).join('/').match(
new RegExp(LOCMAP.searchPath + "\/(.*)")
);
if (body.match(REG) && dirname) {
//匹配到CDN
/*这里是对文件名进行获取,因为下载后的资源要存放在对应读取文件名的目录里,所以要按照文件名进行递归建目录,这样才能保证层级关系清楚,如果在匹配文件时,发现该文件并不在项目目录里,而是单独个体存在在路径下,则用其文件名来做目录名*/
if (dirname[1].indexOf("/") != -1) {
//如果有目录,把目录名作为下载资源的目录名
proDestDir = path.join(LOCMAP.destLocPath, dirname[1]);
targetPath = path.dirname(dirname[1]);
mkdirsSync(path.dirname(proDestDir));
} else {
//没有目录,把文件名作为下载资源的目录名
proDestDir = path.join(LOCMAP.destLocPath, "/", dirname[1].split(".")[0]);
targetPath = dirname[1].split(".")[0];
mkdirsSync(proDestDir);
}
startDownload(proDestDir, body.match(REG)); //文件内的资源下载
let endbody = body.replace(REG, (matched) => {//逐一替换
//进行地址替换
return (
"/static/localres/" +
targetPath +
"/" +
dealFileName(matched)
);
});
//重写替换后的文件
fs.writeFile(curPath, endbody, err => {
if (err) throw err;
});
}
}
/*匹配替换并开始下载资源*/
第五步:把资源下载到刚才新建的目录里
let startDownload = (dir, imageLinks) => {// 存放的目录位置,[下载链接1,下载链接2]
let newDir = dir.indexOf(".") == -1 ? dir : path.dirname(dir);
async.mapSeries(
imageLinks,
(src, callback) => {
console.log(chalk.cyan.bold('下载',src));
if (src.indexOf("http") == -1) src = "http:" + src;
let errType = '错误类型:';
//文件源地址,下载后存放的目标地址
let dest = path.resolve(newDir, dealFileName(src));
request({url:src,timeout: 4000})
.on('error', (err) => {//要在pipe之前
if(err.code == 'ESOCKETTIMEDOUT'){
errType += '超时';
}else{
errType += '其他';
}
errStr += `${errType}\r\n状态码:${err.code}\r\n源文件地址:${src}\r\n替换后的地址:${dest}\r\n\r\n\r\n`;
callback(null, src);
//下载完就要告知程序,可以下载下一个了
})
.pipe(fs.createWriteStream(dest))
.on("close", () => {
//读取到目标,写到最终目录里
callback(null, src);
//下载完就要告知程序,可以下载下一个了
});
}
);
}
第六步:测试
node q.js
代码中的资源被替换了 原文件:
替换后的文件:
运行结果:
大功告成!
总结注意事项:
1. 同一级下,命名为相同名字的目录和文件名,并不会相互覆盖,在建立好目录后,都会追加到相应的目录里(Ep:demo目录和demo.html)
2. 脚本应放在要匹配目录的外层
3. 在一个项目目录里,针对同一个文件名命名的文件,如果发现在匹配时有相同的名字时,会采用获取CDN中的hash值来做区分
近期做的已优化的点:
1. 增加匹配所有文件功能
2. 增加目录排除功能
3. 兼容windows/mac
4. 区分文件命名
5. 增加超时处理
6. 增加错误日志