背景
目前在本地编译时,需要开启一个本地 Server 来托管图片,然后使用自定义插件来替换代码内所有引用相对路径的图片,把相对地址改成本地server路径。
例如:
'/static/index/startWelfare.png' -> 'http://10.70.xxx.xxx:xxxx/index/startWelfare/btn.png'
现在目标是取消采用本地 Server,通过工程化方式把图片全替换成 CDN 地址,提高本地开发效率。
- 对于之前已经发布上线的图片,图片已经上传到CDN,可以直接替换掉。
- 新增的图片,通过脚本上传到团队 CDN,然后代码里自动替换成 CDN 地址。
设计
对于已有图片,维护一个映射文件 (本地图片 -> CDN地址)
这里分2个脚本来实现:
- 增量图片上传CDN
- 扫描components、pages和commonStyle目录,替换图片地址(可以通过映射文件中的记录查找,提高效率)
流程图
如何获取新增图片?
既然需要将新增的图片上传到CDN,首先就需要找出项目中新增的图片。
这里,我是通过 git status 去获取,然后用正则匹配出图片。
但是很快就遇到了问题:新文件夹下新文件 git status 识别不出来
解决方法是在 git status 命令加上 --untracked-files
const { exec } = require('child_process')
const util = require('util')
const execPromise = util.promisify(exec)
exec('git status --untracked-files', (err, std) => {
if (err) {
console.error(`error for exec git status ${err.message}`)
}
const newImgs = std.match(/src\/.*\.(png|jpg|jpeg)/g)
console.log(newImgs)
})
举个例子,假如本次新增了2张图片:gift-logo.png
和 main-header.png
,执行上述脚本后得到newImgs
,
如果没有新增图片,结束。
如果有新增图片,则读取缓存文件,判断新增图片是否已有缓存记录,如果都有缓存记录,说明这些图片已经上传过了,结束。
if (!newImgs?.length) {
return
}
const tempJson = readTempFile()
const needUploadImgs = []
for (const img of newImgs) {
if (!tempJson[img]) {
needUploadImgs.push(img)
}
}
if (!needUploadImgs.length) {
return
}
通过命令上传图片到团队CDN。
上传返回结果里有图片的CDN地址,通过字符串操作获取,最终得到了 uploadMap (映射记录表),然后更新缓存文件。
// ....上传
// ...获取uploadMap
// 筛选出本次新增的图片
if (tempJson) {
for (const img in uploadMap) {
tempJson[img] = uploadMap[img]
}
// 写入缓存文件
writeTempFile(tempJson)
}
脚本执行后在项目根目录出现一个图片映射文件:
扫描源码,替换图片
首先读取缓存文件到内存,然后读取对应目录:
const tempImgJson = readTempFile(); // tempImgJson 就是上图所示的json文件
function readDir(dir) {
fs.readdir(dir, function (err, files) {
if (err) {
console.error(err)
}
files.forEach(function (file) {
replaceFile(path.resolve(dir, file))
})
})
}
对于每个文件或目录,如果是文件则找出文件内符合正则的图片地址,替换成缓存记录中CDN地址。
如果是目录,则递归读取替换。
function replaceFile(filePath) {
if (/*是文件*/) {
fs.readFile(filePath, function (err, data) {
// ......
// 替换src和url中符合
const newData = data
.replace(/:?src="(.*?)"/g, ($0, $1) => {
if ($1.includes('/static/')) {/*...*/}
})
.replace(/(url\((.*?)\))+/g, ($0, _, $2) => {
if ($2.includes('@/static/')) {/*...*/}
})
if (/*发生变动*/) {
// 写入文件
}
})
} else if (/*是目录*/) {
// 递归读目录下的文件
readDir(filePath)
}
}
通过分析代码,其中需要替换的代码集中在 .vue 和 .less 中。
而这两种文件又集中在 src/components 和 src/pages、src/commonStyle ,所以最终只需要替换这三个目录下的文件。
// 替换components目录
const componentsAbsolutePath = path.resolve(__dirname, '../src/components')
readDir(componentsAbsolutePath)
// 替换pages目录
const pagesAbsolutePath = path.resolve(__dirname, '../src/pages')
readDir(pagesAbsolutePath)
// 替换commonStyle目录
const commonStyleAbsolutePath = path.resolve(__dirname, '../src/commonStyle')
readDir(commonStyleAbsolutePath)
脚本执行时机
在 package.json 中新增命令:
"scripts" : {
// ...
"upload": "node ./script/upload"
}
由研发自行判断是否需要上传,在合适阶段调用命令执行图片上传CDN及替换。
总结
新方案除了开发提效(6.6s -> 5.9s,编译时长大致提升10%)。
因为是用的Uniapp,需要先把源码编译成小程序代码,所以时间比直接使用原生小程序开发来说较长。
基于小程序的限制,目前只能发布一个小程序测试版本到小程序开发者平台。
假如有两个需求并行测试,那么其中一个需求只能由前端生成一个本地二维码,测试同学扫码测试。
这种做法结合之前本地Server托管图片的方案,就有一个存在的问题,就是前端必须时刻开启本地Server。
假如研发关掉本地Server或者切换分支(切换后分支的图片目录缺少了测试中分支的图片),那么二维码扫码之后小程序将无法访问到那些图片。
而采用新方案后,舍弃了本地Server,图片全部换成CDN,不管开发阶段还是生产阶段,保证了图片的一致性,一定程度上,也方便了测试。