前端开发
在 React 应用(Vite/CRA/Next.js/Pa…
当在 React 项目中使用 Cloudinary React SDK 做图片/视频展示、变换、上传组件或视频播放器,并遇到导入、环境变量、上传预设或 DOM 报错时使用
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
在 React 应用(Vite/CRA/特定
解决思路
- 项目配置:在项目根目录创建 .env,使用 Vite 前缀 VITE_CLOUDINARY_CLOUD_NAME=my_cloud(必填,必须用真实 cloud name,绝不能用占位符 your_cloud_name,否则 401)和 VITE_CLOUDINARY_UPLOAD_PRESET=my_preset(可选,无签名上传组件需要);修改 .env 后重启 dev server;代码中用 import.meta.env.VITE_* 而非 process.env
- 若重启后浏览器中环境变量仍为空:删除 node_modules/.vite/ 缓存,重启 dev server(npm run dev),硬刷新浏览器(Cmd+Shift+R 或 Ctrl+Shift+F5);仍为空则临时用静态配置文件硬编码 cloud name 做开发,之后再切回环境变量
- 创建可复用实例:新建 src/cloudinary/config.ts,从 @cloudinary/url-gen 导入 Cloudinary,读取 cloudName,若缺失则 throw new Error,导出 export const cld = new Cloudinary({ cloud: { cloudName } }) 和 export const uploadPreset = import.meta.env.VITE_CLOUDINARY_UPLOAD_PRESET || '';其他文件统一 import { cld } from './cloudinary/config' 并调用 cld.image(publicId) / cld.video(publicId)
- 非 Vite 打包器只改环境变量读取方式,其余模式不变:Create React App 用 REACT_APP_ 前缀和 process.env.REACT_APP_CLOUDINARY_CLOUD_NAME;Next.js 客户端用 NEXT_PUBLIC_ 前缀和 process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,服务端可用 process.env.CLOUDINARY_*;Parcel 等其他打包器查其文档的客户端环境变量暴露方式
- 上传预设:在 Cloudinary 控制台 settings/upload/presets 创建无签名(Unsigned)预设,写入 .env 的 VITE_CLOUDINARY_UPLOAD_PRESET,代码中 import { uploadPreset } from './cloudinary/config';无签名上传失败时依次检查:.env 是否设置且名称完全匹配、预设是否存在于 Settings → Upload → Upload presets、预设是否为 Unsigned 模式、是否重启过 dev server
- 安装包:用 npm install <package> 不带版本号安装最新兼容版(如 npm install cloudinary-video-player),package.json 中用 caret 范围(如 "cloudinary-video-player": "^1.0.0"),不要固定精确版本;只用这些包名:@cloudinary/react、@cloudinary/url-gen、cloudinary-video-player(独立播放器)、cloudinary(仅 Node 服务端),不要臆造包名
- 导入模式:import { cld } from './cloudinary/config';import { AdvancedImage, AdvancedVideo } from '@cloudinary/react';import { responsive, lazyload, placeholder } from '@cloudinary/react';变换和叠加只使用 Import reference 表中的精确路径,不要猜测子路径
- @cloudinary/url-gen 精确导入路径:Cloudinary 来自 '@cloudinary/url-gen';fill/scale 来自 '@cloudinary/url-gen/actions/resize';format/quality 来自 '@cloudinary/url-gen/actions/delivery';auto 来自 '@cloudinary/url-gen/qualifiers/format';autoQuality 来自 '@cloudinary/url-gen/qualifiers/quality';blur 来自 '@cloudinary/url-gen/actions/effect';source 来自 '@cloudinary/url-gen/actions/overlay';text/image 来自 '@cloudinary/url-gen/qualifiers/source';Transformation 来自 '@cloudinary/url-gen/transformation/Transformation';Position 来自 '@cloudinary/url-gen/qualifiers/position';compass 来自 '@cloudinary/url-gen/qualifiers/gravity';TextStyle 来自 '@cloudinary/url-gen/qualifiers/textStyle';类型 CloudinaryImage/CloudinaryVideo 来自 '@cloudinary/url-gen'
- 创建图片/视频实例:const img = cld.image(publicId);const video = cld.video(publicId);public ID 用正斜杠表示文件夹(如 'folder/subfolder/image'),区分大小写且不含扩展名;示例资源可能被用户删除,始终处理加载错误并提供回退;可用示例图片 public ID:samples/cloudinary-icon、samples/two-ladies、samples/food/spices、samples/landscapes/beach-boat、samples/bike、samples/landscapes/girl-urban-view、samples/animals/reindeer、samples/food/pot-mussels,示例视频:samples/elephants;单图默认用 samples/cloudinary-icon
- 图片变换:在 image 实例上链式调用,如 cld.image('id').resize(fill().width(800).height(600)).effect(blur(800)).delivery(format(auto())).delivery(quality(autoQuality())),然后 <AdvancedImage cldImg={img} />;format 和 quality 必须分开两次 .delivery() 调用;除非用户指定格式或质量,否则始终以 .delivery(format(auto())).delivery(quality(autoQuality())) 结尾;除非用户指定焦点,否则用 gravity(auto())
- 视频变换:与图片相同模式,cld.video('id').resize(fill().width(800).height(600)).delivery(format(auto())),然后 <AdvancedVideo cldVid={video} />
- 插件模式:用户要求懒加载或响应式图片时,使用 @cloudinary/react 的 responsive()、lazyload()、placeholder() 插件配合 AdvancedImage,不要只用原生 loading="lazy" 或纯 CSS;插件以数组传入 plugins={[responsive(), lazyload(), placeholder()]};推荐顺序:responsive() 第一、placeholder() 第二、lazyload() 第三、accessibility() 最后(如需要);始终加 width 和 height 属性防止布局偏移
- 响应式图片:用 responsive() 插件配合 fill() resize,结合 placeholder() 和 lazyload() 插件,如 const img = cld.image('id').resize(fill().width(800)),<AdvancedImage cldImg={img} plugins={[responsive(), placeholder({ mode: 'blur' }), lazyload()]} width={800} height={600} />
- 图片画廊:用 responsive()、lazyload()、placeholder() 插件配合 AdvancedImage 和 fill() resize,加 width/height;用示例 public ID 列表,假设任何示例都可能不存在,从可靠示例(如 samples/cloudinary-icon)或短列表开始;用 onError 隐藏或移除加载失败的图片(如 setState 过滤掉该 publicId 或隐藏父元素),给用户反馈(如 "Some images could not be loaded. Try uploading your own!")并提供上传功能;用户上传后追加 result.public_id
- 图片叠加(文字或 logo):从 actions/overlay 导入 source,从 qualifiers/source 导入 text 和 image(不要从 actions/overlay 导入 text/image);从 qualifiers/position 导入 Position,从 qualifiers/textStyle 导入 TextStyle,从 qualifiers/gravity 导入 compass,从 transformation/Transformation 导入 Transformation,从 actions/resize 导入 scale;compass() 接受带下划线的字符串如 compass('center')、compass('south_east')、compass('north_west'),不要用 camelCase;叠加图片用 new Transformation() 放在 .transformation() 内:image('logo').transformation(new Transformation().resize(scale().width(100)));文字叠加中 fontWeight 放在 TextStyle 上:new TextStyle('Arial', 60).fontWeight('bold'),textColor 链在 text(...) 之后:text('Hello', new TextStyle('Arial', 60)).textColor('white');Position 链在 source(...) 之后:source(image('logo').transformation(...)).position(new Position().gravity(compass('south_east')).offsetX(20).offsetY(20))
- 上传组件严格初始化模式:在 index.html 加 <script src="https://upload-widget.cloudinary.com/global/all.js" async></script>(必须,不要只靠 React 动态注入);在 useEffect 中用 setInterval(如每 100ms)轮询直到 typeof window.cloudinary?.createUploadWidget === 'function',只有为 true 时才创建组件;设置超时(如 10 秒)停止轮询并显示错误;组件只创建一次存入 ref,传选项 { cloudName, uploadPreset, sources: ['local', 'camera', 'url'], multiple: false };按钮点击监听调用 widgetRef.current?.open(),在 cleanup 中移除监听;不要只检查 window.cloudinary、不要在 onload 中只检查一次、不要跳过 index.html 脚本、不要无超时地永久轮询
- 上传回调:<UploadWidget onUploadSuccess={(result) => { console.log('Public ID:', result.public_id); }} onUploadError={(error) => { console.error('Upload failed:', error); }} />;上传结果包含 public_id、secure_url、width、height 等;存储 public_id 而非完整 URL
- 无签名 vs 有签名上传:默认用无签名上传,除非用户明确要求 "secure" 或 "signed";无签名适合快速原型、低风险应用,预设设为 Unsigned,客户端只需 cloudName 和 uploadPreset,无需后端,但知道预设名的人都能上传;有签名适合生产、需认证用户或控制上传权限,预设设为 Signed,后端用 API secret 生成签名,客户端拿 api_key、uploadPreset 和 uploadSignature 函数,API secret 只留服务端
- 有签名上传黄金规则:绝不暴露或提交 API secret(仅服务端);用 server/.env 并加入 .gitignore 存 API key/secret;API key 可发给客户端,API secret 必须仅服务端;用 uploadSignature 函数(不是 signatureEndpoint);创建组件前先从服务端获取 api_key;组件配置包含 uploadPreset;服务端签名参数包含 upload_preset;服务端用 Cloudinary Node.js SDK v2:import { v2 as cloudinary } from 'cloudinary',不要用 v1
- 视频展示 vs 播放器:展示视频用 AdvancedVideo(@cloudinary/react),它只是带变换的视频显示(原生 HTML5 video,可选 controls),不是完整播放器,用 cld.video() 创建实例并链式变换,<AdvancedVideo cldVid={video} controls autoplay muted />,无需导入 CSS(不要 import '@cloudinary/react/dist/cld-video-player.css',该路径不存在);用户要 "video player" 时用 Cloudinary Video Player(cloudinary-video-player),有样式化 UI、控件、播放列表
- Cloudinary Video Player 关键规则:不要传 React 管理的 <video ref>(会导致 removeChild 错误);用 document.createElement('video') 创建元素,append 到容器 ref,传给 videoPlayer(el, ...);npm install cloudinary-video-player;import { videoPlayer } from 'cloudinary-video-player' 并导入 CSS;用 player.source({ publicId })(对象而非字符串);cleanup 时 player.dispose() 然后 if (el.parentNode) el.parentNode.removeChild(el);始终包含 posterOptions: { transformation: { startOffset: '0' }, posterColor: '#0f0f0f' }
- TypeScript 模式:用 import type { CloudinaryImage, CloudinaryVideo } from '@cloudinary/url-gen';定义 CloudinaryUploadResult 接口;创建 vite-env.d.ts 含 ImportMetaEnv 接口;避免 any,用正确接口或 unknown 加类型守卫;ref 类型 useRef<HTMLVideoElement>(null)、useRef<unknown>(null) 用于组件
- 最佳实践:响应式图片始终用 fill() resize 配合自动 gravity;除非用户指定格式或质量,变换始终以 .delivery(format(auto())).delivery(quality(autoQuality())) 结尾;placeholder() 和 lazyload() 插件一起用;AdvancedImage 始终加 width 和 height;存储上传成功的 public_id 而非完整 URL;视频播放器用命令式元素,在 useLayoutEffect cleanup 中 dispose 并用 if (el.parentNode) el.parentNode.removeChild(el) 移除元素,始终包含 posterOptions;用 TypeScript 获得更好自动补全和错误捕获;类型不可用时优先 unknown 而非 any;用类型守卫做运行时检查;为 Cloudinary API 响应定义接口;创建 vite-env.d.ts 做环境变量类型;ref 用正确的 HTML 元素类型
- 常见错误速查:环境变量 undefined → 清 node_modules/.vite/、重启 dev server、硬刷新;"createUploadWidget is not a function" → 用 setInterval 轮询直到 typeof window.cloudinary?.createUploadWidget === 'function';导入错误 → 只用 Import reference 表中的路径,不猜子路径;上传失败 → 检查预设存在、为 Unsigned(无签名时)、dev server 已重启;视频播放器错误 → 只用命令式元素并包含 posterOptions;叠加问题 → 从 qualifiers/source 导入 text/image,不从 actions/overlay
适用边界 · 注意事项
- 不使用 React(如 React Native 上传需用 upload() 方法和后端 SDK,模式不同)
- 不涉及 Cloudinary 服务(本技能依赖 Cloudinary 账号、cloud name 和上传预设)
- 仅做纯前端展示且不需要 Cloudinary 变换、上传或视频播放器功能时