由于随着项目业务的增加,导致包越来越大,大概有10M,每次发热更下载都比较久,而且会消耗较多的下载资源,在分析包内容的时候,发现图片占据了约80%的体积,因此提出优化图片的方案。
首先我们先看下包内容构成,以安卓包为例,如下所示
|-- dist 10MB
|-- drawable-hdpi 8K
|-- drawable-mdpi 8.2MB
|-- drawable-xhdpi 8K
|-- drawable-xxhdpi 8K
|-- index.android.bundle 1.8MB
从上图可以看出,图片跟bundle文件大概各占据了80%,优化掉了图片,可以减少80%的体积,相当可观的优化,当然bundle文件也是大头,但是这不是本篇文章要介绍的。
我们用tinypng 进行批量压缩drawable-mdpi(共678张)下的所有图片,压缩完重新打包,结果如下
|—— dist 6.5MB
优化体积为 (10 - 6.5) = 3.5MB,可以看到图片体积减少了一部分,但包还是挺大的。
另外一种思路为将本地图片上传到OSS,然后本地图片替换为网络图片。这样我们就能去掉所有图片的体积, 大约有8M。显然这是一个巨大的改进效果。
而且图片是有缓存的,也就是图片缓存在本地后,app就不会在发起请求,那么就不会消耗OSS资源。
下文会详细介绍如何将本地图片转成OSS图片。
我们需要将本地图片上传至阿里云OSS,假设我们现在有以下目录下的几张图片:
|-- src
|-- images
|-- a.png
|-- b.png
|-- pages
|-- images
|-- c.png
考虑到图片有可能重复的可能,文件名取文件md5值。上传成功后记录文件映射表,映射表为json格式,如下所示
{
"src/images/a.png": "4a8da6930cae6e4cae54d7bae3498fbc.png",
"src/images/b.png": "d21c595c585f11d8bc24bdc5dc0c9b18.png",
"src/pages/images/c.png": "674303925428601900ed2e58fdc7cbb6.png"
}
表key为图片的路径,值为md5值,
假设阿里云地址为https://my.oss-cn-beijing.aliyuncs.com (下文会多次用到这个地址)。
我们随便挑md5表中的一个图片,如src/images/a.png,那么这张图对应的OSS地址如下
https://my.oss-cn-beijing.aliyuncs.com/4a8da6930cae6e4cae54d7bae3498fbc.png
将本地图片代码,替换成引用OSS图片。比如有如下代码
<Image src={require('/src/images/a.png')}/>
然后替换成第一步md5表中的值
<Image src={{uri: "https://my.oss-cn-beijing.aliyuncs.com/4a8da6930cae6e4cae54d7bae3498fbc.png"}}/>
这一步需要用到的技术有Babel插件及AST抽象树。
AST抽象语法树介绍
首先我们的源代码编译成可执行代码,并不是一步生成的。而是源代码先解析成AST抽象语法树,然后再从AST抽象语法树转换成可执行代码。我们先看一下AST语法树长得什么样。
已知源代码如下:
let a = require('/src/images/a.png');
let b = {uri: "https://my.oss-cn-beijing.aliyuncs.com/4a8da6930cae6e4cae54d7bae3498fbc.png" }
会解析成以下语法树
{
"type": "File",
"program": {
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"loc": {
"identifierName": "require"
},
"name": "require"
},
"arguments": [
{
"type": "StringLiteral",
"value": "/src/images/ic_cp.png"
}
]
}
}
]
},
{
"type": "VariableDeclaration",
"declarations": [
{
"id": {
"type": "Identifier",
"name": "b"
},
"init": {
"type": "ObjectExpression",
"properties": [
{
"type": "ObjectProperty",
"key": {
"type": "Identifier",
"name": "uri"
},
"value": {
"type": "StringLiteral",
"value": "https://my.oss-cn-beijing.aliyuncs.com/4a8da6930cae6e4cae54d7bae3498fbc.png"
}
}
]
}
}
]
}
]
}
}
语法树相对简单,就不逐行介绍了,可以使用在线AST语法树对应,从上我们可以看出AST语法树是一个个节点的树,我们可以替换节点以实现代码的替换。
遍历src下的所有图片,然后上传至OSS( 如何上传OSS,参考上传本地文件),代码如下
const klaw = require('klaw');
const fs = require("fs");
const path = require("path");
let OSS = require('ali-oss');
const crypto = require('crypto');
// 记录
let record = {};
// 需要上传的图片
let uploadItems = [];
// 上传下标,按下标逐一上传
let uploadIndex = 0;
let client = new OSS({
// yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
region: 'oss-cn-beijing',
// 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
accessKeyId: '你的keyId',
accessKeySecret: '你的secret',
// 填写Bucket名称。
bucket: '你的bucket'
});
//遍历所有图片
klaw(path.resolve(process.cwd(), "./src"))
.on('data', (item) => {
if (/\.(jpg|jpeg|png)$/.test(item.path)) {
const filePath = item.path;
uploadItems.push(filePath);
}
})
.on('end', async () => {
if (uploadItems.length > 0) {
uploadItem();
}
})
/**
* 上传单个文件
*/
async function uploadItem() {
const filePath = uploadItems[uploadIndex];
let fileExtension = filePath.substring(filePath.lastIndexOf('.'))
// fileKey = /src/images/a.png
const fileKey = filePath.replace(process.cwd(), "");
const buffer = fs.readFileSync(filePath);
const hash = crypto.createHash('md5');
hash.update(buffer);
const md5 = hash.digest('hex');
// recordValue = 4a8da6930cae6e4cae54d7bae3498fbc.png
const recordValue = md5 + fileExtension;
// 填写OSS文件完整路径和本地文件的完整路径。OSS文件完整路径中不能包含Bucket名称。
// 如果本地文件的完整路径中未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
const result = await client.put(recordValue, path.normalize(filePath));
if(result && result.res && result.res.statusCode == 200){
// "/src/images/a.png": "4a8da6930cae6e4cae54d7bae3498fbc.png",
record[fileKey] = recordValue;
}
uploadIndex++;
if (uploadIndex < uploadItems.length) {
uploadItem();
} else {
const uploadRecordPath = path.resolve(process.cwd(), "./upload-image-record.json")
fs.writeFileSync(uploadRecordPath, JSON.stringify(record, null, "\t"));
}
}
上传成功后,会生成上传记录文件upload-image-record.json,如下所示
{
"src/images/a.png": "4a8da6930cae6e4cae54d7bae3498fbc.png",
"src/images/b.png": "d21c595c585f11d8bc24bdc5dc0c9b18.png",
"src/pages/images/c.png": "674303925428601900ed2e58fdc7cbb6.png"
}
这一步实现将本地图片替换成OSS图片,要用到上一步生成的记录表upload-image-record.json,代码如下所示:
const t = require('@babel/types');
const p = require('path');
module.exports = function () {
return {
visitor: {
CallExpression(path, ref = { opts: {} }) {
const node = path.node;
if (node.callee.type === 'Identifier' && node.callee.name === 'require') {
const value = node.arguments[0].value;
if (typeof (value) === 'string' && /\.(jpg|jpeg|png)$/.test(value)) {
/**
* 以假设存在项目,/Users/zhangsan/rn-app/index.js代码为例,/Users/zhangsan/rn-app为项目在我电脑的绝对地址
class HelloWorld extends React.Component {
render() {
return (
<View style={styles.container}>
<Image source={require('./src/images/a.png')}/>
</View>
);
}
}
*/
// upload-image-record.json 即上一步产生的md5表
const uploadRecordPath = p.resolve(process.cwd(), "./upload-image-record.json")
// filename = /Users/zhangsan/rn-app/index.js
let filename = path.hub.file.opts.filename;
// fileDir = /Users/zhangsan/rn-app
let fileDir = filename.substring(0, filename.lastIndexOf("/") + 1);
// imagePath = /Users/zhangsan/rn-app/src/images/ic_bjx.png
let imagePath = pathP.resolve(fileDir, value);
// rootPath = /Users/zhangsan/rn-app
let rootPath = path.hub.file.opts.root;
// imagePath = /src/images/a.png
imagePath = imagePath.replace(rootPath, "");
// foundPath = 4a8da6930cae6e4cae54d7bae3498fbc.png
let foundPath = require(uploadRecordPath)[imagePath];
const uriValue = "https://my.oss-cn-beijing.aliyuncs.com/" + foundPath;
const objectExpression = t.objectExpression([
t.objectProperty(
t.identifier('uri'),
t.stringLiteral(uriValue)
)
]);
// 替换节点,require('/src/images/a.png') 替换成
// {uri: https://my.oss-cn-beijing.aliyuncs.com/4a8da6930cae6e4cae54d7bae3498fbc.png}
path.replaceWith(objectExpression);
}
}
}
}
}
}
至此图片替换的操作就完成了,再看下包大小
包大小 | |
优化前 | 10MB |
优化后 | 2MB |
至此已完成体积优化,当然有些细节没处理,可以参考源码。