☰
微信小程序拍照授权与保存到相册完整链路
2026/9/30 16:01:12 网站建设 项目流程

微信小程序里的拍照功能看着简单,真落地时几乎每个人都会在授权和保存这两步上摔一跤。用户点了拍照没反应、授权弹窗只出现一次之后再怎么点都静默失败、拍完了图片存不进相册只丢回一句fail auth deny,这些现象背后都是同一套权限模型和文件系统规则在起作用。这篇就把"拍照—授权—保存到本地"这条链路从头到尾拆开讲:camera组件和wx.chooseMedia到底该怎么选,scope.camera和scope.writePhotosAlbum的授权时机怎么安排,被用户拒绝之后怎么优雅地把人送进设置页重新打开,拍到的临时文件怎么落进手机相册——以及另一种同样高频的"保存到本地",也就是落到小程序自己的用户文件目录wx.env.USER_DATA_PATH里。不管你是刚上手小程序的新人,还是做过几个项目但一直靠拷代码过日子的老手,这里面的参数取舍、错误码排查和真机差异清单,应该都能直接抄走。

1. 先搞清楚"拍照"这件事被拆成了几块

1.1 拍照、授权、保存是三件互不依赖的事

大部分新手写拍照功能,习惯把所有逻辑塞进一个函数:点按钮 → 调相机 → 拿路径 → 存相册。看起来很紧凑,实际上一旦失败,你根本不知道该重试哪一步。正确的拆法是把它看成三个独立的动作,每个动作都有自己独立的失败原因和恢复手段。

第一步是取图,也就是拿到一张图片的路径。这一步的产物永远是一个临时路径,注意是"临时",不是"永久"。小程序的临时文件只在本次启动的生命周期内有效,用户杀掉微信再进来,这个路径大概率就指不到了。第二步是授权,它不是拍照本身的步骤,而是访问系统能力(摄像头、相册写入)的门票,门票好不好使决定了后面两步能不能走通。第三步是落盘,把临时文件搬到稳定存储里——搬到系统相册是一种,搬到小程序用户目录是另一种。

拆开之后,你的代码就能做到:拍照失败可以单独重试,保存失败不影响已经拍到的图,授权被拒时给用户一条明确的补救路径,而不是弹个"操作失败"就完事。我在项目里通常会把这个流程封装成三个返回 Promise 的函数,页面层只负责编排,不碰任何errMsg字符串判断,这样后面微信改个错误文案也不用满项目搜索替换。

1.2 两条实现路线的取舍:camera 组件还是 wx.chooseMedia

小程序拍照有两条路,选错了后面全是坑,先看对比。

对比维度camera 组件wx.chooseMedia
界面来源自己写,完全自定义微信统一相机界面
开发成本高,要处理层级、暂停、错误低,一个 API 搞定
授权要求需要scope.camera,要自己处理由微信内部处理,调用方不感知
灵活度极高,可叠人脸框、扫码、实时帧低,只能拿最终图
主要坑点原生组件层级最高,普通view盖不住,得用cover-view参数少,但真机行为有差异
典型场景证件识别、人脸核身、实时滤镜头像上传、打卡留痕、报修拍照

如果你的需求只是"拍一张图然后上传",用wx.chooseMedia就够了,省下来的时间够你多做两个需求。只有当你要在取景框上叠加东西——比如把身份证的框画出来、实时提示"请把脸放进去"、或者需要读取每一帧做识别——才值得上camera组件。这里有个很多人不知道的细节:camera组件在部分机型上被压到后台再回来会黑屏,需要监听bindstop然后手动重建组件,而wx.chooseMedia完全没有这个问题,因为它走的是系统相机。

2. 授权链路:什么时候要,什么时候不要

2.1 scope 权限的三种状态和"只弹一次"的机制

小程序把用户对系统能力的授权结果存在authSetting里,对我们最相关的是scope.camera(摄像头)和scope.writePhotosAlbum(相册写入)。每个 scope 只有三种状态:未询问过(字段不存在)、已同意(值为true)、已拒绝(值为false)。

关键在于:同一个 scope,弹窗一辈子只会弹一次。用户第一次点拍照,微信弹出"允许访问摄像头",如果他点了拒绝,你之后再调多少次wx.authorize或者相关 API,都不会再弹窗,只会直接走fail回调。这就是大量"为什么我这儿好好的,用户那儿点不动"问题的根源——不是你的代码有问题,是用户在那一次弹窗里点了拒绝,然后世界就安静了。

所以正确的做法不是"调 API 之前先wx.authorize一遍",而是先查状态,再决定策略。查询用wx.getSetting:

function getAuthState(scope) { return new Promise((resolve) => { wx.getSetting({ success(res) { const val = res.authSetting[scope] // undefined = 没问过 true = 已同意 false = 已拒绝 resolve(val) }, fail() { resolve(undefined) } }) }) }

拿到undefined,说明可以放心直接调 API,让微信去弹窗;拿到true,直接干活,别多嘴;拿到false,才是需要走"引导去设置页"分支的时候。这三种情况一定要分开处理,写成if (res.authSetting[scope])会把undefined和false混在一起,结果就是首次使用的用户被无端弹一个"请去设置页开启权限",体验直接崩掉。

2.2 被拒绝之后,怎么把用户拉回来

用户拒绝之后,唯一的翻盘机会是把人引到小程序的设置页,让他手动打开开关。这里有两个入口,用途不同,很多人会混着用。

一是button组件的open-type="openSetting",这是最稳的方式,因为它天然满足"必须由用户点击触发"的要求:

<button open-type="openSetting" bindopensetting="onSettingBack">去开启权限</button>

二是wx.openSetting,这个 API 在较新版本里同样要求由用户点击事件的同步调用链里触发,如果你写在setTimeout里或者网络回调里,会直接报"openSetting:fail can only be invoked by user TAP gesture"。

我的习惯是:页面上放一个自定义的权限提示弹层,里面嵌那个open-type="openSetting"的按钮,用户点完之后在bindopensetting里读res.authSetting,如果权限开了就自动继续刚才被打断的操作。这样用户的操作路径是连贯的——点拍照、被拦、点去设置、点允许、回来照片就出来了,中间不需要他再点一次拍照按钮。这个细节看着小,但转化率差得很明显,尤其是上了年纪的用户,你不帮他续上,他大概率就放弃了。

注意:open-type="openSetting"只能打开当前小程序的权限设置页,不能引导去系统层面的设置,别在文案里写"请到手机设置里打开"。

2.3 隐私协议适配,别让拍照在真机上莫名失效

这是这两年最容易踩的新坑。按照现行规范,小程序在调用摄像头、相册这类涉及用户隐私的接口之前,必须在管理后台的"用户隐私保护指引"里声明对应的收集项,并且在前端完成一次隐私授权。没做这一步的典型表现是:开发工具里一切正常,真机上保存相册直接返回fail privacy permission is not authorized。

前端需要处理的是"是否需要弹出隐私协议"这个判断,用wx.getPrivacySetting:

wx.getPrivacySetting({ success(res) { if (res.needAuthorization) { // 需要展示隐私协议,让用户点同意 this.setData({ showPrivacy: true, privacyName: res.privacyContractName }) } else { // 用户已同意过,或者当前不需要授权,直接继续业务 this.startPhotoFlow() } } })

弹层里的同意按钮必须用特定写法才能生效:

<button open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="onAgreePrivacy">同意</button>

后台那边要记得勾选"摄像头"和"相册(仅写入)权限"两项,只勾一半的情况很常见——声明了摄像头忘了相册,结果拍得好好的,一保存就失败。这个声明项和你代码里实际调用的接口必须对得上,多声明了审核会问,少声明了功能会挂。我一般会先把可能用到的接口列一遍:拍照、从相册选图、保存到相册、扫一扫,全部对照后台声明核一遍再提审。

3. 拍照环节的实现细节与参数取舍

3.1 camera 组件路线的关键配置

走自绘路线的话,页面结构基本长这样:

<camera device-position="{{position}}" flash="{{flash}}" resolution="high" frame-size="medium" binderror="onCameraError" bindinitdone="onCameraReady" style="width: 100%; height: 750rpx;" /> <cover-view class="toolbar"> <cover-view class="btn-switch" bindtap="onSwitchCamera">翻转</cover-view> <cover-view class="btn-shoot" bindtap="onShoot">拍照</cover-view> </cover-view>

几个必须知道的点:camera是原生组件,层级永远最高,普通view覆盖不上去,工具条只能用cover-view+cover-image;device-position支持back和front,切换时组件会短暂重建,期间takePhoto会失败,所以要在bindinitdone之后再放开快门按钮。拍照本身通过CameraContext完成:

const ctx = wx.createCameraContext() function shoot(ctx) { return new Promise((resolve, reject) => { ctx.takePhoto({ quality: 'high', // low | normal | high | original success: (res) => resolve(res.tempImagePath), fail: reject }) }) }

quality选original会拿到未压缩的原始图,体积通常是high的三到五倍,一张能到 5MB 以上,除非你要做证件识别,否则别用。上传前再做一次压缩更划算,这个后面细说。

3.2 wx.chooseMedia 路线的推荐写法

大多数场景用这一个函数就结束了,但参数得配对:

function takePhoto(options = {}) { const { count = 1, sourceType = ['camera'], // 只要 camera 就强制拍照,不给相册入口 sizeType = ['compressed'] // 基础库 2.21.0 起支持 } = options return new Promise((resolve, reject) => { wx.chooseMedia({ count, mediaType: ['image'], sourceType, sizeType, camera: 'back', success(res) { resolve(res.tempFiles.map(f => f.tempFilePath)) }, fail(err) { // 用户主动取消不是错误,别弹提示 if (/cancel/i.test(err.errMsg)) return resolve([]) reject(err) } }) }) }

sourceType只写['camera']会直接唤起相机、不给相册选项,适合"必须现场拍"的场景(打卡、查勘、巡检);写成['album', 'camera']则会出现一个选择面板。sizeType建议默认给compressed,微信会做一次压缩,肉眼几乎看不出差别,但体积能降一半以上。还有一点容易被忽略:用户取消要当成正常返回处理,返回空数组,而不是 reject,否则用户点一下"取消"你就弹一个错误提示,非常打扰。

3.3 拍完之后必踩的三个坑:太大、方向不对、路径失效

第一是体积。压缩后的图常见在 200KB 到 1MB 之间,如果你要一次传九张,那就是接近 10MB。建议在拿到路径后再压一次,wx.compressImage支持按质量和目标宽高两个维度压:

function compress(src, quality = 80) { return new Promise((resolve) => { wx.compressImage({ src, quality, success: (res) => resolve(res.tempFilePath), fail: () => resolve(src) // 压缩失败就用原图,别让流程断掉 }) }) }

第二是方向。部分安卓机型(尤其是早年的一些三星、小米设备)拍出来的照片带 EXIF 方向标记,image组件会自动校正显示,但你把图丢进canvas去画水印或者做拼图时,就会发现它横过来了。解决办法是先用wx.getImageInfo读出orientation字段,再在canvas里按方向做旋转和镜像补偿,这一步不做,安卓端水印十有八九是歪的。

第三是路径时效。前面说过,临时文件只在本次启动内有效。如果你的业务流程是"用户拍完先放一放,等填完表单再提交",那一定要在拿到路径的第一时间把它搬到用户目录,否则用户中途切出去刷了会视频再回来,微信可能已经回收了那份临时文件,提交时报file not found。这个坑我在真实项目里遇到过两次,都是用户投诉"照片传上去是空白",查了半天才发现是临时文件过期。

4. 保存到手机相册的完整链路

4.1 标准保存流程的五个阶段

保存到系统相册用wx.saveImageToPhotosAlbum,它看着只有一个参数,实际上你的代码要处理五个阶段:拿到可靠路径→检查相册授权状态→调用保存接口→区分错误类型→给用户明确反馈。完整的封装长这样:

function saveToAlbum(filePath) { return new Promise((resolve, reject) => { wx.saveImageToPhotosAlbum({ filePath, success: () => resolve(true), fail: (err) => reject(err) }) }) }

有个细节很多人不知道:当scope.writePhotosAlbum是"未询问"状态时,你不需要提前调wx.authorize,saveImageToPhotosAlbum自己会把授权弹窗带出来。提前authorize反而容易导致重复弹窗或者时序错乱。所以在页面里的推荐流程是:

async onTapSave() { const path = this.data.imgPath if (!path) return try { await saveToAlbum(path) wx.showToast({ title: '已保存到相册', icon: 'success' }) } catch (err) { const msg = err.errMsg || '' if (/auth deny|auth denied|authorize/i.test(msg)) { // 曾经拒绝过,展示自定义引导层 this.setData({ showAuthTip: true }) return } if (/cancel/i.test(msg)) return // 用户点了取消,静默处理 if (/file not found/i.test(msg)) { wx.showToast({ title: '图片已失效,请重新拍摄', icon: 'none' }) return } if (/privacy/i.test(msg)) { wx.showToast({ title: '请先同意隐私协议', icon: 'none' }) return } wx.showToast({ title: '保存失败,请重试', icon: 'none' }) } }

这段代码的价值在于:同样是失败,用户看到的提示完全不同。用户点了取消,你不吭声;权限被拒,你给他一条去设置的路;文件失效,你告诉他重新拍;其他情况兜底一个通用提示。这就是前面说的"拆开"带来的好处,如果所有错误都塞在一坨代码里,最后只能统一弹一个"保存失败",用户完全不知道该怎么办。

4.2 错误码速查与逐条排查

errMsg 关键字真实含义处理方式
auth deny/auth denied用户曾拒绝相册写入权限展示引导层,按钮用open-type="openSetting"
fail cancel用户在系统弹窗里点了取消静默,不要弹任何提示
fail file not found临时文件已被回收或路径非法校验路径存在,提示重新拍摄
fail invalid file type传了非图片文件,或视频路径检查传入的是不是图片临时路径
fail privacy permission is not authorized隐私协议未授权或后台未声明补后台声明 + 前端隐私弹窗
fail system deny系统层面禁用了相册权限只能引导用户去系统设置,小程序侧无解
fail dest path is invalid目标路径格式问题(多见于自定义场景)不要手拼路径,用 API 返回的原始路径
fail:the permission value is offline verifying权限校验中,多见于首次弹窗后立即调用延迟 100~300ms 重试一次

最后一条值得单独说:在部分安卓机型上,用户在系统弹窗里点了允许之后,小程序的权限状态更新有个极短的延迟,你立刻调保存会拿到"校验中"的失败。稳妥的做法是失败后延迟重试一次,setTimeout200ms 再试,成功率能明显提升。

4.3 保存成功之后还该做的事

保存成功之后有两个动作值得加上。一是给一个非阻塞的成功反馈,wx.showToast就够了,别用showModal打断用户。二是如果你做的是"拍照打卡"类功能,保存到相册只是副产品,真正要做的上传应该在这之后自动触发——但顺序不能反,先本地保存再上传,因为上传可能失败,本地那张图是用户唯一确定拿到手的东西。

还有个小细节:iOS 上保存到相册后,如果在系统相册里立刻查看,会看到照片按拍摄时间排序,而不是保存时间。这是系统行为,不用管,但客服肯定会被问,提前准备好话术。

5. 另一种"保存到本地":落到小程序的用户文件目录

5.1 wx.env.USER_DATA_PATH 到底是什么

产品经理说"保存到本地"时,很可能指的是把图片存在小程序自己的目录里,下次打开还能看到,而不是存到系统相册。这时候要用的是wx.env.USER_DATA_PATH,它指向小程序的本地用户文件目录,通常是wxfile://usr这种形式。特点很鲜明:不受临时文件回收机制影响,只要小程序不被删除,文件就在;只有你自己的小程序能读写,其他小程序和系统相册都看不到;总容量有上限,官方文档给的数字是 200MB,超了写入就会失败。

和它并列的还有两个目录:本地临时文件目录(各种 API 返回的tempFilePath就在这儿,随时可能被清)和本地缓存文件目录(wx.saveFile时代的产物,官方已经把它标记为废弃)。所以现在做持久化,认准USER_DATA_PATH就对了,这也是为什么网上搜相关内容会频繁看到它。

5.2 从临时路径搬运到用户目录的两种方式

第一种是拷贝,用FileSystemManager.copyFile,源路径可以直接是临时文件:

const fs = wx.getFileSystemManager() const DIR = `${wx.env.USER_DATA_PATH}/photos` function ensureDir() { try { fs.accessSync(DIR) } catch (e) { fs.mkdirSync(DIR, true) // 第二个参数 true 表示递归创建 } } function persistFile(tempFilePath, name) { ensureDir() const dest = `${DIR}/${name || Date.now() + '.jpg'}` return new Promise((resolve, reject) => { fs.copyFile({ srcPath: tempFilePath, destPath: dest, success: () => resolve(dest), fail: reject }) }) }

第二种是直接写入,适用于你从接口拿到的是 base64 或者 ArrayBuffer 的情况:

function saveBinary(buffer, name) { ensureDir() const dest = `${DIR}/${name}` return new Promise((resolve, reject) => { fs.writeFile({ filePath: dest, data: buffer, encoding: 'binary', success: () => resolve(dest), fail: reject }) }) }

这里有个必须注意的点:存进USER_DATA_PATH的路径不能直接丢给image组件显示——实际上是可以的,image组件支持wxfile://开头的本地路径。但如果你要把它传给<web-view>或者上传接口,就得走wx.uploadFile或者先转成临时路径。另外,USER_DATA_PATH里的文件在开发者工具的"清除缓存"操作后会被清空,测试的时候别以为是代码写错了。

5.3 容量、清理与升级时的数据迁移

因为总容量只有 200MB,做图片缓存一定要有清理策略,否则用户用几个月之后就会开始写入失败。我的做法是给目录里的文件加一个"访问时间索引",存在Storage里,每次打开页面时检查:超过 7 天没被访问的,用fs.unlinkSync删掉;总占用超过 150MB 时,按时间从旧到新删到 100MB 以下。

function cleanUp(maxAge = 7 * 24 * 3600 * 1000) { const now = Date.now() let files = [] try { files = fs.readdirSync(DIR) } catch (e) { return } files.forEach((name) => { const full = `${DIR}/${name}` try { const stat = fs.statSync(full) if (now - stat.lastModifiedTime * 1000 > maxAge) { fs.unlinkSync(full) } } catch (e) { // 单个文件失败不影响整体清理 } }) }

还要考虑版本升级的问题:如果新版本改了存储结构(比如从平铺改成按日期分目录),一定要在onLaunch里写一段兼容逻辑,检测旧结构并按需迁移。小程序没有"用户主动升级"的概念,用户打开就是最新版本,你不做兼容,老用户一进来数据就找不到了。另外提一句,USER_DATA_PATH里的数据在用户换手机、清微信缓存之后都会消失,别把它当成可靠的服务端存储,重要数据该传还是得传回去。

6. 一套可以直接抄的完整实现

6.1 权限与拍照的工具函数

把前面所有逻辑收拢到一个utils/media.js:

const ALBUM = 'scope.writePhotosAlbum' export function getAuthState(scope) { return new Promise((resolve) => { wx.getSetting({ success: (res) => resolve(res.authSetting[scope]), fail: () => resolve(undefined) }) }) } export function takePhoto({ count = 1, sourceType = ['camera'] } = {}) { return new Promise((resolve, reject) => { wx.chooseMedia({ count, mediaType: ['image'], sourceType, sizeType: ['compressed'], camera: 'back', success: (res) => resolve(res.tempFiles.map((f) => f.tempFilePath)), fail: (err) => (/cancel/i.test(err.errMsg) ? resolve([]) : reject(err)) }) }) } export function saveToAlbum(filePath) { return new Promise((resolve, reject) => { wx.saveImageToPhotosAlbum({ filePath, success: () => resolve(true), fail: reject }) }) } export function checkPrivacy() { return new Promise((resolve) => { if (!wx.getPrivacySetting) return resolve(true) wx.getPrivacySetting({ success: (res) => resolve(!res.needAuthorization), fail: () => resolve(true) }) }) }

工具层只做单一职责的事,不做任何 UI 决策,页面层拿到的就是干净的 Promise 和原始错误对象。这个分层在需求变更时特别值钱——哪天产品说"保存前先加个水印",你只需要改工具层的一个函数,页面一行不动。

6.2 页面结构与交互代码

<view class="page"> <image wx:if="{{imgPath}}" src="{{imgPath}}" mode="widthFix" class="preview" /> <view wx:else class="placeholder">还没有照片</view> <view class="actions"> <button size="default" bindtap="onShoot">拍照</button> <button size="default" bindtap="onPick">从相册选</button> <button size="default" bindtap="onSave" disabled="{{!imgPath}}">保存到相册</button> <button size="default" bindtap="onKeepLocal" disabled="{{!imgPath}}">存到小程序本地</button> </view> <view wx:if="{{showAuthTip}}" class="mask"> <view class="tip-box"> <view class="tip-title">需要相册权限</view> <view class="tip-desc">开启后才能把照片保存到你的手机相册</view> <button open-type="openSetting" bindopensetting="onSettingBack">去开启</button> <view class="tip-cancel" bindtap="onCloseTip">暂不开启</view> </view> </view> </view>

页面逻辑:

import { takePhoto, saveToAlbum, checkPrivacy } from '../../utils/media' import { persistFile } from '../../utils/fs' Page({ data: { imgPath: '', showAuthTip: false }, async onShoot() { const ok = await checkPrivacy() if (!ok) return this.setData({ showPrivacy: true }) const paths = await takePhoto({ sourceType: ['camera'] }) if (paths.length) this.setData({ imgPath: paths[0] }) }, async onPick() { const paths = await takePhoto({ sourceType: ['album'] }) if (paths.length) this.setData({ imgPath: paths[0] }) }, async onSave() { try { await saveToAlbum(this.data.imgPath) wx.showToast({ title: '已保存到相册', icon: 'success' }) } catch (err) { const msg = err.errMsg || '' if (/auth deny|auth denied|authorize/i.test(msg)) { return this.setData({ showAuthTip: true }) } if (/cancel/i.test(msg)) return wx.showToast({ title: '保存失败,请重试', icon: 'none' }) } }, async onKeepLocal() { try { const saved = await persistFile(this.data.imgPath) this.setData({ imgPath: saved }) wx.showToast({ title: '已存入小程序本地', icon: 'success' }) } catch (e) { wx.showToast({ title: '本地存储失败', icon: 'none' }) } }, onSettingBack(res) { const granted = res.detail.authSetting['scope.writePhotosAlbum'] this.setData({ showAuthTip: false }) if (granted) this.onSave() // 授权成功后自动续上刚才的操作 }, onCloseTip() { this.setData({ showAuthTip: false }) } })

重点看onSettingBack里那句自动续接——这是整套流程里体验最好的一个细节。用户点"去开启"、在设置页打开开关、返回小程序,照片直接保存成功,整个过程他只需要点两次,不需要回到页面重新点一遍保存按钮。

6.3 多张连拍与上传的并发处理

如果业务是连拍多张然后批量上传,注意两个限制:一是chooseMedia的count上限是 9;二是小程序的网络请求有并发上限,一般是 10 个同时。九张图如果一次性Promise.all发出去,加上页面里其他请求,很容易撞上限流。

我的做法是写一个简单并发池,把并发数卡在 3:

async function uploadBatch(paths, url) { const results = [] const queue = [...paths] const workers = Array.from({ length: 3 }, async () => { while (queue.length) { const path = queue.shift() try { const res = await new Promise((resolve, reject) => { wx.uploadFile({ url, filePath: path, name: 'file', success: resolve, fail: reject }) }) results.push({ path, ok: true, data: res.data }) } catch (e) { results.push({ path, ok: false, error: e }) } } }) await Promise.all(workers) return results }

这样做的额外好处是失败可以单独重试——results里标了ok: false的,用户点一下"重试失败项"就只传那几张,不用九张全部重来。

7. 实战踩坑记录与常见问题速查

7.1 开发者工具和真机的差异清单

现象开发者工具真机应对
保存到相册提示"已保存到开发工具"真正写入系统相册保存功能必须在真机上验收
权限弹窗模拟弹窗,可一键重置真实系统弹窗,只能弹一次测试时提前在设置里重置权限
隐私协议可手动开关模拟严格按后台声明走提审前用真机完整跑一遍
USER_DATA_PATH点清缓存即清空除非删小程序否则保留清理逻辑要用真机验证
图片方向基本不会出现异常部分安卓机型 EXIF 方向异常canvas 处理前先读 orientation

7.2 十个高频问题与对应解法

  1. 点了拍照完全没反应:先看scope.camera是不是false,是的话用户曾拒绝过,走设置页引导。
  2. 保存提示成功但相册里没有:多半是在开发者工具里测的,真机再验一次;真机仍然没有,检查是不是存到了小程序内部目录。
  3. 首次弹窗后立刻保存失败:权限校验延迟,延迟 200ms 重试一次。
  4. 安卓上照片横着:EXIF 方向问题,canvas 处理前读orientation做补偿。
  5. 保存报file not found:临时文件被回收了,拿到路径后第一时间落盘。
  6. 真机报隐私未授权:后台没声明相册写入,或前端没接隐私弹窗。
  7. camera组件被弹窗遮不住:用cover-view,普通view无效。
  8. 连拍九张上传只成功几张:并发超限,用并发池控制在 3 到 5。
  9. 切换前后摄后拍照失败:组件重建未完成,等bindinitdone再放开快门。
  10. 本地存储用了几个月后写入失败:容量到 200MB 上限了,补清理逻辑。

8. 参数调优和后续还能怎么扩

8.1 画质、闪光灯和前后摄的取舍

camera组件的resolution有low、medium、high三档,flash有auto、on、off、torch。经验值是:做文字识别或者票据存档,resolution拉到high并且用torch常亮补光,识别率能明显提升;做头像或者社交分享,medium加上flash: 'auto'就够,图小上传快。takePhoto的quality和resolution是两个独立维度,一个管最终输出质量,一个管预览和取帧质量,别搞混。

还有一个常被忽略的优化:预览尺寸和展示尺寸对齐。camera的宽高比如果不是按设备屏幕比例设置,安卓上很容易出现画面被拉伸,尤其是横屏页面。稳妥的做法是用wx.getSystemInfoSync拿到屏幕宽高,按比例算出一个接近 4:3 或 16:9 的展示区域,宁可留黑边也别拉伸。

8.2 还能往哪些方向扩

基础功能跑通之后,往下做通常有三个方向。一是加水印,用canvas把图片和文字合成一张新图再保存,注意canvas的尺寸要按wx.getImageInfo拿到的原始宽高设置,不然会被缩放模糊。二是本地图片墙,把USER_DATA_PATH里的文件列表读出来展示,配上删除和预览,适合做"我的证件照""我的打卡记录"这类功能。三是压缩后上传 + 失败重传,把上传队列持久化到Storage,即使小程序被杀掉,下次打开还能把没传完的图续上,这个在弱网环境下特别有用。

最后分享一个小技巧:所有和媒体相关的操作,我都会在开发阶段加一个隐藏的调试面板,把当前authSetting的完整状态、最近一次错误的errMsg、以及临时文件的实际字节数打在上面。上线前把它用一个变量关掉,真机上出问题时让测试同学截个图,比来回问"你那边具体情况是什么样的"高效太多。这套东西做一次,后面的项目直接复制过去用就行。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询