前言
与阿里的oss不同的是S3不支持通过url参数实现这种类似黑盒的图片处理,而是提供了一种特殊的lambda函数,我虽然不清楚为什么叫做lambda函数,但其本质就是一个拦截器。下面就演示一下利用lambda函数实现图片缩放和质量转换。
cloudfront是什么?这个是一个缓存服务,就是经常听说的cdn
有四个阶段可以做图片处理,下文选择的是在origin request事件中做图片处理。
有几个前置步骤,这里只列出,不做细致演示。
- aws账号注册,注册后需要绑定一张信用卡才能使用。新注册用户有12个月的免费资源使用。从绑定银行卡之日起计算。
- 创建存储桶,这个和阿里OSS一样。
创建桶之后注意需要修改访问权限,否则访问不到。
步骤一:配置cloudfront(cdn)
- 创建地址:https://us-east-1.console.aws.amazon.com/cloudfront/v4/home?region=us-east-1#/distributions
- 注意cloudfront必须配置在佛吉尼亚北部。
这里需要注意一下, 启用了防火墙之后价格是按照请求另外计算的,防火墙不在免费资源范围内
创建完成之后,回到首页
点进去之后,这里就可以看到域名了。尝试使用该域名访问:https://图片中的域名/resized-boke.jpg
现在cloudfront就基本配置完成了。
步骤二: 创建lmabda edge函数
- 创建地址:https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/discover
创建函数时注意选择蓝图(这个一看就是机器翻译的,其实就是模板的意思)。执行角色这个不管选择哪个,后面都要去配置一下角色权限(https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/roles),防止出现执行函数异常的问题,或者日志不打印的问题。
注意一路向下部署完成后,此时版本1已经生效,回到函数页面点击更新函数,编写代码。
步骤三:编写lambda函数代码:
import {GetObjectCommand, S3Client} from '@aws-sdk/client-s3';
import sharp from 'sharp';
import {Buffer} from 'buffer';
import {Readable} from 'stream';
import {parse} from 'querystring';
const bucketName = 'chengzhi-test-resized';
/**
* 多个处理参数使用&符号分隔: /test.png@resize=50x50&width=50&height=50&proportion=50&quality=50
* 图片处理参数类型:
* - 按照像素缩放: @resize=50x50
* - 按照宽度等比缩放: @width=20
* - 按照高度等比缩放: @height=200
* - 按照比例缩放: @proportion=99
* - 质量转换: @quality=50
* @param event
* @param context
* @returns
*/
export const handler = async (event, context) => {
const cf = event.Records[0].cf;
const request = cf.request;
const params = getParams(request.uri);
if (params === undefined) {
console.log("未携带处理参数,不进行处理!");
} else {
let response = '';
const image = params.image;
const objectKey = `${image}`;
const trigger_point = cf.response ? 'RESPONSE' : 'REQUEST';
if (trigger_point === 'REQUEST') {
let buffer = await getObjectBuffer(bucketName, objectKey);
if (buffer === undefined) {
return request;
}
if (params.resize) {
buffer = await resizeImage(buffer, params.resize);
}
if (params.width) {
buffer = await widthImage(buffer, params.width);
}
if (params.height) {
buffer = await heightImage(buffer, params.height);
}
if (params.proportion) {
buffer = await proportionImage(buffer, params.proportion);
}
if (params.quality) {
buffer = await qualityImage(buffer, image.split('.')[1],params.quality);
}
return generateResponse(response, buffer, trigger_point);
}
}
return request;
};
async function getObjectBuffer(bucket_name, objectKey) {
const region = "eu-north-1";
const s3 = new S3Client({
region: region
});
try {
const params = {
Bucket: bucket_name,
Key: objectKey
};
var response = await s3.send(new GetObjectCommand(params));
var stream = response.Body;
if (stream instanceof Readable) {
var content_buffer = Buffer.concat(await stream.toArray());
return content_buffer;
} else {
throw new Error('Unknown object stream type');
}
} catch (error) {
console.log(error);
return;
}
}
async function heightImage(content_buffer, height) {
try {
let pipeline = sharp(content_buffer);
height = Number(height);
let resizeConfig = {
height: height
};
pipeline = pipeline.resize(resizeConfig);
return await pipeline.toBuffer();
} catch (error) {
console.log(error);
return;
}
}
async function widthImage(content_buffer, width) {
try {
let pipeline = sharp(content_buffer);
width = Number(width);
let resizeConfig = {
width: width
};
pipeline = pipeline.resize(resizeConfig);
return await pipeline.toBuffer();
} catch (error) {
console.log(error);
}
}
/**
* 等比例缩放图片
* @param content_buffer
* @param proportion 百分比(0-100)
* @returns {Promise<*>}
*/
async function proportionImage(content_buffer, proportion) {
try {
let pipeline = sharp(content_buffer);
const metadata = await pipeline.metadata();
proportion = Number(proportion);
const percentage = proportion / 100;
let resizeConfig = {
width: Math.round(metadata.width * percentage)
};
pipeline = pipeline.resize(resizeConfig);
return await pipeline.toBuffer();
} catch (error) {
console.log(error);
}
}
/**
* 裁剪图片
* @param content_buffer
* @param size 例如:50x40
* @returns {Promise<*>}
*/
async function resizeImage(content_buffer, size) {
try {
const [width, height] = size.split('x').map(Number);
var output_buffer = await sharp(content_buffer).resize(width,height).toBuffer();
} catch (error) {
console.log(error);
return;
}
return output_buffer;
}
/**
* 图片质量转换
* @param content_buffer
* @param format 图片格式
* @param quality 目标质量(0-100)
* @returns {Promise<*>}
*/
async function qualityImage(content_buffer, format, quality) {
try {
// 传入的quality为字符串
quality = Number(quality);
console.log('quality:', quality)
console.log('format:', format)
let pipeline = sharp(content_buffer);
// 根据格式设置质量
switch (format.toLowerCase()) {
case 'jpeg':
case 'jpg':
pipeline = pipeline.jpeg({
quality: quality,
mozjpeg: true // 使用 mozjpeg 优化
});
break;
case 'png':
pipeline = pipeline.png({
quality: quality,
compressionLevel: 9 // PNG 压缩级别 0-9
});
break;
case 'webp':
pipeline = pipeline.webp({
quality: quality,
lossless: false // 有损压缩
});
break;
case 'avif':
pipeline = pipeline.avif({
quality: quality,
lossless: false
});
break;
}
console.log('质量转换完成!')
var outputBuffer = await pipeline.toBuffer();
console.log('流转换完成!')
} catch (error) {
console.log(error);
return;
}
return outputBuffer;
}
function generateResponse(response, buffer, trigger_point) {
if (trigger_point === 'REQUEST') {
response = {
status: '',
statusDescription: '',
headers: {
'cache-control': [{
key: 'Cache-Control',
value: 'max-age=100'
}],
'content-type': [{
key: 'Content-Type',
value: 'image/png'
}],
'content-encoding': [{
key: 'Content-Encoding',
value: 'base64'
}]
},
body: '',
bodyEncoding: 'base64'
};
}
response.status = '200';
response.body = buffer.toString('base64');
response.bodyEncoding = 'base64';
response.headers['content-type'] = [{
key: 'Content-Type',
value: 'image/png'
}];
response.headers['content-encoding'] = [{
key: 'Content-Encoding',
value: 'base64'
}];
response.statusDescription = trigger_point === 'REQUEST'
? 'Generated by CloudFront Original Request Function'
: 'Generated by CloudFront Original Response Function';
return response;
}
function getParams(image) {
image = image.startsWith('/') ? image.slice(1) : image
if (!image.includes('@')) {
console.log("不包含@符号,不需要进行处理!");
return;
}
var strings = image.split('@');
if (strings.length < 2) {
console.log("@符号位置不正确,不进行处理");
return;
}
var result = parse(strings[1]);
var picture = strings[0];
if (picture === undefined || !picture.match(/\.(jpg|jpeg|png|gif|webp)$/i)) {
console.log("非图片类,不进行处理");
return;
}
if (result !== undefined) {
result.image = picture;
}
console.log("图片处理参数:", JSON.stringify(result))
return result;
}
函数编写完成后,需要将依赖打包,注意因为是要在aws的服务器上运行该代码,所以引入的依赖必须是linux版本的。以下是代码中使用到的两个依赖。
npm install --arch=x64 --platform=linux --target=16x sharp@0.32.6
npm install --arch=x64 --platform=linux --target=16x aws-sdk@2.1450.0
依赖引入完成后,需要打包成zip包,包结构如图,如果zip包超过10M,上传会超时,需要使用S3上传,注意上传代码的S3也必须是弗吉尼亚北部。否则上传会失败。
代码上传完成后,一定要修改函数执行超时时间,否则函数运行1s后,就自动停止了。
步骤四:部署
代码完成后,点击添加触发器,选择cloudfront(注意如果函数不是在弗吉尼亚北部,这个是选择不到的),按照提示填写配置。注意我上面的代码cluodfront事件选择的是源请求。
步骤五: 测试
https://域名/novel1.png@height=200
验证是否生成缓存
https://us-east-1.console.aws.amazon.com/cloudfront/v3/home?region=us-east-1#/popular_urls
查看日志
查看日志的时候需要注意右上角的地区,由于lambda函数是部署在cloudfront上的,而cloudfront是全球分布多个节点,一般本地访问都是优先连接附近的节点,因此日志也会出现在附近的地区节点上,比如,我在上海访问,那么日志会随机落到东京和孟买。如果IAM角色权限未正确配置(https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/roles),不会打印日志。
答疑
1、为什么要在url中使用@?
答: 在测试过程中发现,函数中无法获取到queryString的参数,也就是说无法获取到url问号后面的参数部分,因此采用@然后在程序中做分割处理,这样一来可以解决获取参数的问题, 二来可以解决图片名称相同但尺寸不同的缓存问题。2、为什么要在源请求中做处理?
答: 刚开始也考虑在源响应中做处理,但是发现源响应的response中并没有body,咨询了aws的售前说源响应是没有body的。
3、是否可以兼容ali oss图片处理参数?
答:基于上述内容可以发现,lambda函数使用的是sharp库,这个库中支持的那大概率都能实现,我只是因为只需要用到这几个处理方式。4、为什么使用Node,不适用python?
答: 刚开始尝试使用了python,但是打包上传到服务上时提示,有包冲突,谷歌了一下,说是需要用到aws的一个层,个人觉比较复杂,就更换了node.
参考文档:
https://aws.amazon.com/cn/blogs/china/use-cloudfront-lambdaedge-for-transaction-processing/
https://docs.aws.amazon.com/zh_cn/lambda/latest/dg/getting-started.html#get-started-invoke-manually
https://docs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/lambda-at-the-edge.html
https://docs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/GettingStarted.SimpleDistribution.html
https://docs.aws.amazon.com/zh_cn/lambda/latest/dg/with-s3-tutorial.html
https://docs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/popular-objects-report.html