imagekit 是什么?
imageKit 是一款包含图像优化、处理和存储解决方案,专为提升 Web 和移动应用的性能而设计。它对外提供了以下能力:
- 实时图像优化:自动压缩和优化图像,确保最佳加载速度和质量。
- 图像处理:支持动态调整图像大小、裁剪、旋转、滤镜应用等操作。
- 图像存储:提供云存储服务,用户可以上传并管理图像,支持大规模存储和高效管理。
- 全球 CDN 加速:通过分布在全球的 CDN 网络,快速分发图像,减少加载延迟。
- 格式转换:支持图像格式自动转换(例如 WebP),以减少带宽消耗和提高加载效率。
本文主要使用的它的 图像存储 能力,它有 免费计划 与 付费计划。其免费计划提供 5g 的文件存储空间
免费计划上传图片大小限制 20m,视频大小限制 100m
为什么使用 api 而不是 sdk 上传
这里作者的使用场景是 个人博客文章的图片上传 写文章使用 bytemd md 编辑器编辑器会有一个上传文件的回调函数
而 imagekit nextjs sdk 貌似是没有提供直接传入 file 上传的方法
使用 api 的另一个好处是直接将文件上传到 imagekit, 无需上传到项目后端服务然后由后端服务上传到 imagekit
七牛云、阿里云、腾讯云存储上传一般也是这么个逻辑,由服务端签发上传 token 客户端直传对应的 oss 服务,不同的是它们的提供客户端上传的 sdk 并对如大文件上传等操作进行了处理
好处是: 减少上传步骤、减轻服务器的负载
坏处是: 切换 oss 服务时前后端都需要更改
画张图看一下使用 api 文件上传的流程
非常简单就三个 api 接口的组合调用
实现一下
获取文件就没啥好说的了跳过
下边使用到的 NEXT_PUBLIC_IMAGEKIT_PUBLIC_KEY、IMAGEKIT_PRIVATE_KEY 可以在 developer配置中获取到
获取文件 hash
这里把整个文件做了计算,这里计算文件 hash 的原因是需要根据 hash 查询文件是否已经上传
感觉可以优化下取文件的开头部分做计算,比如就只取 500k 进行计算
export async function getFileHash(file: File) {
const arrayBuffer = await file.arrayBuffer()
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
}
根据文件 hash 检查文件是否存在
这里是调用接口实现的但为了私有 key 不被泄露所以使用 nextjs api 进行调用
app/api/imagekit/getFileInfoByHash/route.ts
import { sendJson } from '@/lib/utils'
// 根据 hash 获取图片信息
export async function GET(req: Request) {
const url = new URL(req.url)
const fileHash = url.searchParams.get('hash')
try {
if (!fileHash) {
return sendJson({ code: -1, msg: '文件 hash 不存在' })
}
const query = {
type: 'file',
searchQuery: `"customMetadata.md5" = "${fileHash}"`,
limit: '1'
}
const url = `https://api.imagekit.io/v1/files?${new URLSearchParams(query).toString()}`
const res = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(process.env.IMAGEKIT_PRIVATE_KEY + ':')}`
}
}).then((res) => res.json())
if (!Array.isArray(res) || res.length === 0) {
return sendJson({ data: [] })
}
return sendJson({ data: res })
} catch (error) {
console.error(error)
return sendJson({ code: -1, msg: 'Failed to fetch article' })
}
}
sendJson 是一个统一接口返回结构的参数
这里构建了一个查询,查询文件、使用自定义元数据中的 md5 字段进行查询、查询数量为 1 猛击查看文档
这里需要注意下 Authorization 字段的生成方式(看文档看了半天没看明白)
注意,此处有坑
使用自定义元数据需要在 imagekit 中进行设置 猛击直达
获取上传文件的 token
这里也是调用接口实现的但为了私有 key 不被泄露所以使用 nextjs api 进行调用
文件上传接口分为 v1、v2 版本
v1 使用 publicKey、privateKey、urlEndpoint 生成 key
V2 通过使用 JWT 验证整个有效载荷来增强安全性。
下边是 v2 版本的实现
app/api/imagekit/getToken/route.ts
import { sendJson } from '@/lib/utils'
import jwt from 'jsonwebtoken'
// 添加留言
export async function POST(req: Request) {
try {
const body = await req.json()
const { payload } = body
const privateKey = process.env.IMAGEKIT_PRIVATE_KEY ?? ''
const token = jwt.sign(payload.uploadPayload, privateKey, {
expiresIn: 60, // token 过期时间最大 3600 秒
header: {
alg: 'HS256',
typ: 'JWT',
kid: process.env.NEXT_PUBLIC_IMAGEKIT_PUBLIC_KEY
}
})
return sendJson({ data: token })
} catch (err) {
console.error(err)
return sendJson({ code: -1, msg: '获取上传 token 失败!' })
}
}
这里将调用上传文件的参数、私有 key 加密生成 jwt token 猛击查看文档
客户端获取 toekn
封装下调用方便组合上传逻辑
// 获取文件上传 token
async function getImagekitToken(info: Record<string, string>) {
const res = await fetch('/api/imagekit/getToken', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
payload: {
uploadPayload: info
}
})
}).then((res) => res.json())
return res.code === 0 ? res.data : null
}
封装上传函数
上边的准备工作完成了,现在需要把逻辑组合一下实现文件上传
export async function imagekitUploadFile({ file, fileName }: ImagekitUploadFileOpts) {
const fileHash = await getFileHash(file)
const existingFileInfo = await getFileInfoByFileHash(fileHash)
if (existingFileInfo) {
return { code: 0, data: existingFileInfo }
}
const payload = {
fileName,
customMetadata: JSON.stringify({ md5: fileHash }),
folder: `/blog/${dayjs().format('YYYY-MM-DD')}`
}
const token = await getImagekitToken(payload)
if (!token) return { code: -1, msg: '获取 token 失败' }
const formData = new FormData()
Object.entries({ ...payload, file, token }).forEach(([key, value]) =>
formData.append(key, value as Blob | string)
)
const uploadRes = await fetch('https://upload.imagekit.io/api/v2/files/upload', {
method: 'POST',
body: formData
})
if (!uploadRes.ok) {
return { code: -1, msg: '上传文件失败!' }
}
return {
code: 0,
data: await uploadRes.json(),
msg: '上传成功!'
}
}
自定义元数据是一个 json 字符串
folde 字段会根据 / 划分目录
文件上传接口需要使用 FormData 参数来调用,token 也需要放进参数中
返回数据格式
interface ImagekitUploadFileRes {
fileId: string
name: string
size: string
versionInfo: {
id: string
name: string
}
filePath: string
url: string
fileType: string
height: number
width: number
thumbnailUrl: string
AITags: null | string
}
上传函数调用
async function uploadImages(files: File[]) {
const resultData: Record<'url' | 'alt' | 'title', string>[] = []
for (const item of files) {
const res = await imagekitUploadFile({ file: item, fileName: item.name })
if (res?.code === 0) {
console.log('res', res)
resultData.push({
url: res?.data.url,
alt: item.name,
title: item.name
})
} else {
toast({ variant: 'destructive', title: '警告', description: '图片上传失败,请重试!' })
}
}
return resultData
}
uploadImages 是 bytemd md 编辑器的文件上传回调函数
总结
完整代码可以查看 github 仓库猛击直达