简介:这份面向具备 Vue 基础、希望为移动端 Web 应用补齐原生能力的开发者,聚焦 Vue 与 Cordova 的混合开发集成,围绕定位、手机振动、拍照/相册取图、二维码扫描等常见场景,帮助读者理解跨平台项目中插件接入、权限处理与兼容性适配的整体思路。压缩包约14.48MB,文件总数上游标注为0,类型明细暂未提供,内部构成无法据此展开;从主题看,内容应服务于上述原生功能的集成参考与问题排查。目前已有534人学习或下载,说明该主题在 Vue 移动化开发中具有一定关注度。读者可从中获得插件选型与调用链路的参考、不同平台权限请求的注意事项,以及真机联调和实时刷新场景下的排错方向;适合需要把 Web 项目快速延伸到 Android/iOS 原生能力的初中级前端人员查漏补缺。
1. Vue 接 Cordova 原生能力:先把桥接时序和权限想明白
Vue 项目跑进 Cordova 壳里,获取地理位置、手机振动、调取手机图片、扫描二维码这几件事,听起来只是几个 API 调用,真到真机上最先卡住的往往不是写代码,而是 deviceready 时序和系统权限。这份资源把 Vue 页面、Cordova 插件安装、原生能力调用和真机排查串成一条线,适合已经能跑 Vue 项目、但一接原生就遇到白屏、权限弹窗不出现、扫码黑屏的人。我拆的时候重点看三处:构建产物是否落进壳、调用是否发生在桥接就绪之后、权限和销毁是否闭环。定位、拍照、扫一扫这些能力单独拎出来都不复杂,难的是它们在 Vue 生命周期里互相打架,先把这条主线理顺,后面每加一个插件都省事。
2. 工程缝合:Vue 构建产物如何落进 Cordova 壳与插件调用链
Vue 和 Cordova 放在一起,最容易乱的是目录边界。我的习惯是让 Vue 工程独立开发,Cordova 工程只负责壳、平台配置和插件,构建时把 Vue 的产物输出到 Cordova 的www目录。这样开发阶段还能用 Vue 的热更新,真机阶段又不会把node_modules和源码一起塞进 WebView。常见做法是建两个平级目录,名字按项目习惯改,但边界要清楚:
project/ vue-app/ src/ vue.config.js cordova-app/ config.xml www/ platforms/ plugins/这个结构里,vue-app只写页面和业务逻辑,cordova-app只保留壳工程。www是构建产物落点,不要在里面手写业务代码,否则下一次构建就被覆盖。platforms和plugins是 Cordova 生成和安装插件后维护的目录,不要把它们当源码目录改,尤其是 Android 清单和 iOS 描述文件,手改一次,下一次cordova prepare就可能被冲掉。
2.1 构建输出:别让 index.html 在 file:// 下找不到资源
Vue 默认按根路径加载静态资源,放到 Cordova 的 WebView 里经常变成白屏,控制台能看到/static/js/...404。原因不是包没打进去,而是 WebView 加载的是本地文件,根路径/指向了文件系统根,不是www目录。我一般会在 Vue 配置里把publicPath改成相对路径,把outputDir指到壳工程的www:
// vue.config.js const path = require('path') module.exports = { // 让 index.html 用相对路径加载 static 下的资源 publicPath: './', // 构建产物直接落到 Cordova 壳的 www outputDir: path.resolve(__dirname, '../cordova-app/www'), assetsDir: 'static', productionSourceMap: false }这段逻辑的关键是publicPath: './',它让打包后的index.html用./static/...去找资源,而不是/static/...。outputDir用绝对路径更稳,避免从不同目录执行构建时落错位置。assetsDir只是把静态资源收进static,方便和 Cordova 自己的文件区分。productionSourceMap关掉能减少产物体积,真机排查阶段如果确实需要看源码映射,再临时打开,不要长期留在发布包里。
提示:每次构建前清空
www可以避免旧文件干扰,但不要删config.xml,它在壳工程根目录,不在www里。
构建完成后,Cordova 侧执行一次同步,让平台工程拿到最新页面。这个步骤看起来机械,但漏掉就会遇到“代码改了、真机没变”的假故障:
# 在 cordova-app 目录执行 cordova preparecordova prepare会把www和config.xml的变化同步到各平台工程。它不负责安装插件,也不负责重新编译原生包;如果只改 Vue 页面,通常重新跑一次真机包就够了;如果改了权限或插件,后面还要重新构建。
2.2 deviceready 之前调插件,为什么会必翻车
Vue 的mount很快,Cordova 的桥接注入没那么快。页面一挂载就去调navigator.camera或navigator.geolocation,浏览器里可能正常,真机上却报undefined或返回一个不完整的对象。原因是 Cordova 的原生桥接要等deviceready事件之后才稳定,插件对象、权限接口、原生回调通道都在这个时间点之后才准备好。我的做法不是在每个页面里散着监听,而是在入口统一等一次:
// main.js import { createApp } from 'vue' import App from './App.vue' function waitForCordova(timeout = 8000) { return new Promise((resolve) => { // 浏览器环境没有 Cordova 注入,直接放行方便调试 if (!window.cordova) { return resolve('browser') } let done = false const finish = (source) => { if (done) return done = true resolve(source) } document.addEventListener('deviceready', () => finish('device'), { once: true }) // 超时兜底:不让页面永远停在白屏 setTimeout(() => finish('timeout'), timeout) }) } waitForCordova().then((source) => { console.log('Cordova ready source:', source) createApp(App).mount('#app') })这段逻辑把 Vue 挂载放到桥接就绪之后。window.cordova不存在时直接resolve('browser'),是为了让浏览器调试不被卡住;真机上即使window.cordova已经存在,也要等deviceready,所以别用“对象存在”代替事件。timeout设成 8000 毫秒是常见折中,太短会误判,太长会让白屏时间不可接受。once: true保证监听只执行一次,避免热更新或重复挂载时触发多次。
注意:超时兜底不等于桥接一定就绪。业务代码里调用插件前仍然要判断navigator.camera、QRScanner这些对象是否存在,不存在就给出可读提示,不要直接把异常抛到控制台。
2.3 插件安装与权限声明的最小闭环
插件不要一次全装。定位、振动、相机、扫码四个能力一起上,出问题很难判断是哪一个插件的权限或初始化顺序导致的。我一般按功能分批装,每装一个就在真机上验一次:
# 在 cordova-app 目录按功能分批安装 cordova plugin add cordova-plugin-geolocation cordova plugin add cordova-plugin-vibration cordova plugin add cordova-plugin-camera cordova plugin add cordova-plugin-file cordova plugin add cordova-plugin-qrscannercordova-plugin-geolocation负责系统定位,cordova-plugin-vibration负责振动,cordova-plugin-camera负责拍照和相册,cordova-plugin-file用于把拍照返回的本地路径读成可预览内容,cordova-plugin-qrscanner负责二维码扫描。装完不要只跑浏览器,必须重新构建真机包,因为权限和原生代码是在平台工程里生效的。
权限声明尽量写在config.xml里,让每次prepare都能同步到平台工程。Android 的定位、相机权限,iOS 的相机、相册、定位描述,缺一个都可能在真机上静默失败:
<!-- config.xml --> <platform name="android"> <config-file target="AndroidManifest.xml" parent="/manifest"> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.CAMERA" /> </config-file> </platform> <platform name="ios"> <config-file target="*-Info.plist" parent="NSCameraUsageDescription"> <string>用于拍照和扫描二维码</string> </config-file> <config-file target="*-Info.plist" parent="NSPhotoLibraryUsageDescription"> <string>用于从相册选择图片</string> </config-file> <config-file target="*-Info.plist" parent="NSLocationWhenInUseUsageDescription"> <string>用于获取当前位置</string> </config-file> </platform>Android 侧ACCESS_FINE_LOCATION是精确位置,插件通常还会合并粗略位置权限;相机权限也要确认在最终清单里存在。iOS 侧的描述字符串不能空着,系统弹窗会直接拒绝,用户看不到任何解释。target="*-Info.plist"用来匹配平台工程里的描述文件,改完config.xml后执行cordova prepare,不要手动去platforms下改生成文件,那条路看起来快,后面同步一次就白改。
3. 定位与振动:权限、超时和节奏参数怎么落到 Vue 组件
定位和振动经常被当成“一行 API”的能力,实际在真机上,定位受权限、GPS 状态、室内环境和超时参数影响,振动受平台限制和页面生命周期影响。Vue 组件里直接写navigator.geolocation.getCurrentPosition也能跑,但错误处理会散在页面里,用户点一次没反应,你连是权限拒绝还是超时都分不清。我的做法是先把原生调用包成返回 Promise 的小函数,再在 composable 里管 loading、错误和振动反馈。
3.1 定位调用链:高精度、超时和错误码要一起看
定位参数里最容易被忽略的是timeout和maximumAge。enableHighAccuracy: true会倾向使用 GPS,精度高但室内容易超时;timeout太短会让用户还没走到窗边就失败;maximumAge设 0 表示不吃缓存,每次都要新位置,适合签到类场景,不适合频繁刷新。下面这个封装把常用参数收在一起,并把错误码原样带出来:
// native/location.js export function getCurrentPosition(options = {}) { const defaultOptions = { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } return new Promise((resolve, reject) => { if (!navigator.geolocation) { return reject({ code: -1, message: '当前环境不支持定位' }) } navigator.geolocation.getCurrentPosition( (position) => { resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude, accuracy: position.coords.accuracy, timestamp: position.timestamp }) }, (error) => { reject({ code: error.code, message: error.message }) }, { ...defaultOptions, ...options } ) }) }这段逻辑把成功结果裁剪成业务真正需要的字段,纬度、经度、精度和时间戳,避免把整个Position对象透传到组件里。参数上,enableHighAccuracy控制是否请求高精度,timeout是毫秒,maximumAge是允许的缓存年龄。错误码里,1通常是用户拒绝,2是位置不可用,3是超时;页面提示要按错误码区分,不要统一弹“定位失败”,否则用户不知道去设置里开权限还是走到窗边。
我一般会做一次降级:高精度 8 秒失败后,再用低精度 15 秒试一次。室内场景下高精度容易超时,低精度走网络定位反而更容易出结果。降级不是掩盖错误,而是把“可接受的位置”先拿到,再在业务层标注精度。
3.2 振动节奏:数组参数能用,但别把它当关键反馈
振动 API 看起来简单,navigator.vibrate(200)是振 200 毫秒,navigator.vibrate([200, 100, 200])是振、停、振的节奏。Cordova 的振动插件注入后,Android 上节奏数组通常能按预期走,iOS 上不要指望完全按拍子执行,系统限制会让节奏变形。所以振动只适合做轻量反馈,比如定位成功轻振一下、扫码失败短振两下,不要用它传递关键业务状态:
// native/vibration.js export function vibrate(pattern = 200) { if (!('vibrate' in navigator)) { return false } return navigator.vibrate(pattern) } export function stopVibrate() { if ('vibrate' in navigator) { navigator.vibrate(0) } }这段逻辑先判断vibrate是否存在,避免在不支持的环境里直接报错。pattern可以是数字,也可以是数组,单位都是毫秒;vibrate(0)用来取消当前振动。参数上,短反馈用 50 到 100 毫秒,失败提示可以用[120, 80, 120],但别连续长振,用户会以为手机出问题。页面切走时调一次stopVibrate,能避免振动在后台继续。
3.3 用 composable 收口:让页面只关心“定位成功没成功”
定位和振动经常一起出现:点按钮、开始定位、成功轻振、失败短振。把这些状态散在页面里,后面加拍照和扫码会越来越乱。我一般写一个 composable,把 loading、坐标、错误和振动反馈收进去:
// composables/useLocationAndHaptic.js import { ref } from 'vue' import { getCurrentPosition } from '../native/location' import { vibrate } from '../native/vibration' export function useLocationAndHaptic() { const loading = ref(false) const coords = ref(null) const error = ref(null) async function locate(options) { loading.value = true error.value = null try { coords.value = await getCurrentPosition(options) vibrate(80) } catch (e) { error.value = e vibrate([120, 80, 120]) } finally { loading.value = false } } return { loading, coords, error, locate } }这段逻辑把“请求定位、成功反馈、失败反馈、收尾”放在一个函数里,页面只需要调用locate()并绑定loading和error。options透传给底层,页面可以按场景改超时,比如签到用 12 秒,附近门店用 6 秒。参数上注意不要把maximumAge设得太大,否则用户移动后拿到的还是旧位置;如果业务允许缓存,设 5000 到 10000 毫秒比较常见。
定位权限被拒绝后,不要反复自动请求,系统可能不再弹窗。我的处理是第一次拒绝后展示手动设置提示,用户再次点击时再尝试,避免把用户逼到死角。振动在这时只做一次短反馈,不要连续振,体验会显得很急躁。
4. 拍照与扫一扫:Camera 参数、图片回显和扫码生命周期
拍照、相册和扫一扫是三个容易互相牵连的能力。相机插件返回的是本地路径,不是可以直接长期使用的 URL;扫码插件会占用摄像头预览,页面切走不释放就会黑屏;图片太大还会把 WebView 内存顶上去。我的顺序是先把拍照参数定稳,再解决图片回显,最后把扫码的prepare、show、scan、destroy串成完整生命周期。
4.1 相机参数:质量、来源和目标路径怎么选
navigator.camera.getPicture的成功回调拿到的是图片路径或 Base64,具体取决于destinationType。我一般用FILE_URI,因为 Base64 大图会直接进 JS 内存,连续拍几张就容易卡。quality设 80 左右是常见折中,再高对业务帮助不大,包体和上传时间却会涨。下面这个封装把拍照和相册两个来源分开:
// native/camera.js export function takePhoto(source = 'CAMERA') { const options = { quality: 80, destinationType: Camera.DestinationType.FILE_URI, sourceType: source === 'PHOTOLIBRARY' ? Camera.PictureSourceType.PHOTOLIBRARY : Camera.PictureSourceType.CAMERA, encodingType: Camera.EncodingType.JPEG, mediaType: Camera.MediaType.PICTURE, correctOrientation: true, saveToPhotoAlbum: false } return new Promise((resolve, reject) => { if (!navigator.camera) { return reject(new Error('相机插件未就绪')) } navigator.camera.getPicture(resolve, reject, options) }) }这段逻辑把来源选择收在source参数里,传CAMERA走拍照,传PHOTOLIBRARY走相册。quality是 0 到 100 的 JPEG 质量,destinationType用FILE_URI避免大图 Base64;correctOrientation处理部分设备拍照后方向旋转的问题;saveToPhotoAlbum默认关掉,避免用户拍一次业务图片就在系统相册多一份。mediaType限制为图片,防止选到视频。
allowEdit这类裁剪参数在不同平台上表现不一致,业务没有强裁剪需求就别开,开了反而容易出现“裁完路径变了”的问题。targetWidth和targetHeight可以压缩尺寸,但会改变比例,需要和上传接口的约束一起看。
4.2 图片回显:FILE_URI 不要直接塞进 img
拍照拿到file://或content://路径后,直接写进<img src>在部分 WebView 上会空白。原因不是路径一定错,而是 WebView 对本地文件的安全策略和路径协议处理不一致。稳妥做法是用cordova-plugin-file的resolveLocalFileSystemURL拿到文件 entry,再读成 DataURL 做预览:
// composables/useCameraPreview.js import { ref } from 'vue' export function useCameraPreview() { const previewUrl = ref('') function loadPreview(fileUri) { if (!fileUri) return window.resolveLocalFileSystemURL( fileUri, (entry) => { entry.file((file) => { const reader = new FileReader() reader.onloadend = () => { previewUrl.value = reader.result } reader.readAsDataURL(file) }) }, (err) => { console.error('读取本地图片失败', err) } ) } return { previewUrl, loadPreview } }这段逻辑把本地路径转成File,再用FileReader读成 DataURL。fileUri来自相机回调,entry.file拿到文件对象,readAsDataURL的结果可以直接给<img>用。参数上注意预览图只用于展示,真正上传时仍然用原始fileUri或重新读取文件,不要把 DataURL 当长期存储。图片特别大时,读完要释放引用,页面切走时把previewUrl置空,减少内存占用。
如果业务需要上传,常见做法是用FileTransfer或表单方式把文件路径交给原生层,不要在前端把大图转成 Base64 再拼 JSON。Base64 会让请求体膨胀,弱网下很容易超时。
4.3 扫一扫:prepare、show、scan、destroy 少一步都不行
二维码扫描插件的调用顺序比相机更讲究。先prepare检查相机权限,再show让预览层显示,scan注册扫描回调,扫到或取消后hide隐藏预览,页面卸载时destroy释放摄像头。少一个destroy,下次进页面就可能黑屏;少一个hide,WebView 会一直停在预览层上:
// composables/useQrScanner.js import { onBeforeUnmount, ref } from 'vue' export function useQrScanner() { const scanning = ref(false) const result = ref('') function prepare() { return new Promise((resolve, reject) => { QRScanner.prepare((err, status) => { if (err) return reject(err) if (!status.authorized) return reject(new Error('相机未授权')) resolve(status) }) }) } async function start() { await prepare() scanning.value = true QRScanner.show() QRScanner.scan((err, text) => { scanning.value = false QRScanner.hide() if (err) { result.value = '' return } result.value = text }) } function stop() { scanning.value = false QRScanner.hide() QRScanner.destroy() } onBeforeUnmount(stop) return { scanning, result, start, stop } }这段逻辑把权限检查、预览显示、扫描回调和释放串起来。prepare回调里的status.authorized表示相机是否授权,未授权时要给用户明确提示;show负责显示预览,scan的回调里先hide再处理结果;destroy放在页面卸载和手动停止里,释放摄像头。参数上,扫描结果text是二维码里的原始字符串,业务层要自己判断是 URL、编号还是 JSON,不要直接信任内容。
扫码页面的背景处理也要注意。插件预览层在 WebView 下面,页面背景不透明时就会看到黑屏或白屏,而不是相机画面。常见做法是给扫码页容器加透明背景,退出时恢复,避免影响其他页面。
5. 避坑排查:权限、时序、黑屏和图片路径的五个翻车现场
Cordova 的坑大多不是语法错误,而是环境差异。浏览器里能跑,真机上不行;Android 上正常,iOS 上弹窗都不出现;第一次扫码成功,第二次黑屏。下面五个现场是我拆这类项目时最常遇到的,按“现象、原因、解决”记,排查时少走弯路。
5.1 权限弹窗不出现:先看插件清单和平台描述
现象是点定位或拍照没有任何系统弹窗,控制台也没有明显报错,浏览器里却正常。原因通常是插件没装进当前平台工程,或者 iOS 的描述字符串缺失,系统直接拒绝而不弹窗。排查时先看插件清单,再重新同步平台:
# 查看已安装插件 cordova plugin ls # 修改 config.xml 后重新同步 cordova preparecordova plugin ls能确认插件是否真的装在壳工程里,cordova prepare会把config.xml的权限和描述同步到平台工程。解决上,Android 检查最终清单里是否有定位和相机权限,iOS 检查Info.plist里是否有相机、相册、定位描述。描述字符串要写清楚用途,不要空着,也不要写无关内容。权限问题不要靠反复卸载重装碰运气,先看清单,再看系统设置里应用权限是否被手动关掉。
5.2 deviceready 竞态:偶发 undefined 不是玄学
现象是大部分时候能拍照,偶尔报navigator.camera is undefined,或者定位回调一直不返回。原因是 Vue 挂载早于 Cordova 桥接就绪,页面刚进来就调插件,插件对象还没注入。解决不是加setTimeout猜时间,而是统一等deviceready,并在调用前做存在性判断:
// 调用前做一次防御,避免桥接未就绪时直接崩 export function ensurePlugin(name, target) { if (!target) { throw new Error(`${name} 未就绪,请确认 deviceready 后再调用`) } return target } // 使用示例 ensurePlugin('camera', navigator.camera)这段逻辑把“插件是否存在”变成可读错误,页面能提示用户重试,而不是抛一个底层异常。name用来标记是哪个插件,target是实际对象。解决竞态的根本还是在入口等deviceready,防御性判断只是兜底。真机上如果仍然偶发,检查是否有页面在deviceready之前就发起了异步请求,回调回来时桥接状态已经变了。
5.3 扫码黑屏:预览层和页面背景打架
现象是调用扫码后页面全黑,没有相机画面,或者第一次能扫,第二次进来黑屏。原因常见的有两个:扫码页背景不透明,遮住了插件预览层;或者上一次扫描结束后没有hide、destroy,摄像头被占用。解决上,扫码页容器要透明,退出时恢复背景,并且每次结束都释放:
/* 只在扫码页生效,避免影响其他页面 */ .qr-scan-page, .qr-scan-page #app { background: transparent !important; }// 进入扫码页时加类,离开时移除 document.body.classList.add('qr-scan-page') // 退出扫码时 document.body.classList.remove('qr-scan-page') QRScanner.hide() QRScanner.destroy()这段样式只对扫码页容器生效,background: transparent让 WebView 背景透出下面的相机预览。JS 里加类、移除类要成对出现,页面卸载时也要移除,否则其他页面会变成透明背景。hide负责隐藏预览,destroy负责释放摄像头,两个都做,第二次进入才不会黑屏。
5.4 拍照后图片不显示:content:// 路径别硬塞
现象是拍照成功,回调里也有路径,但<img>是空白,或者只在部分机型上显示。原因是content://路径不能被 WebView 直接当图片 URL 用,file://路径也可能被安全策略拦。解决是用文件插件把路径转成可读文件,再生成预览:
// 读取失败时至少把原始路径打出来,方便判断是路径问题还是文件问题 window.resolveLocalFileSystemURL( fileUri, (entry) => { console.log('文件 entry 读取成功', entry.nativeURL) }, (err) => { console.error('文件读取失败', fileUri, err) } )这段排查代码不直接修复显示,但能把“路径是否存在、协议是否支持”分开。fileUri是相机回调返回的原始路径,entry.nativeURL是文件插件解析后的本地 URL。解决上优先走resolveLocalFileSystemURL加FileReader,不要在前端拼file://前缀碰运气。如果业务只需要上传,不要求预览,可以把路径直接交给上传层,减少前端读文件的内存开销。
5.5 真机与浏览器差异:定位超时先看权限和 GPS
现象是浏览器里定位很快,真机上一直超时,或者精度差得离谱。原因是浏览器可能走网络定位,Cordova 走系统定位,权限、GPS 开关、室内环境都会影响结果。排查时把错误码打出来,不要只打印“失败”:
navigator.geolocation.getCurrentPosition( (pos) => console.log('定位成功', pos.coords), (err) => console.error('定位失败', { code: err.code, message: err.message }), { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } )这段代码把错误码和消息一起打出来,code为 1 时先查权限,为 2 时查 GPS 和网络,为 3 时再考虑放宽超时。解决上不要一味加大timeout,先确认系统设置里应用有定位权限,再确认设备定位开关打开。室内场景可以降级到低精度,或者提示用户到窗边重试。真机排查阶段保留日志,发布前再收敛提示文案。
6. 进阶收口:把四个原生能力封装成可验收的 Vue 插件
四个能力分别调通之后,最后一步是收口。我的习惯是建一个native目录,定位、振动、相机、扫码各一个文件,再通过 Vue 插件挂到全局属性上,页面只依赖统一入口。这样后面换插件或加降级策略,不用逐个页面改:
// native/index.js import { getCurrentPosition } from './location' import { vibrate } from './vibration' import { takePhoto } from './camera' import { useQrScanner } from './qr' export const native = { location: { getCurrentPosition }, haptic: { vibrate }, camera: { takePhoto }, qr: { useQrScanner } } export default { install(app) { app.config.globalProperties.$native = native } }这段逻辑把能力按域分组,location、haptic、camera、qr各自独立,页面通过$native.camera.takePhoto()调用。install是 Vue 插件入口,挂到globalProperties后模板和选项式 API 都能用;组合式 API 里也可以直接 import,不必强依赖全局属性。错误码建议统一成{ code, message, source },source标记是定位、相机还是扫码,方便日志聚合。
验收不要只看“能点开”,要按场景过一遍。下面这张表是我真机验收时必走的清单:
| 验收项 | 操作 | 通过标准 |
|---|---|---|
| 桥接时序 | 冷启动直接进功能页 | 不出现插件 undefined,页面不白屏 |
| 定位权限 | 首次点击定位 | 出现系统弹窗,拒绝后有可读提示 |
| 定位超时 | 室内高精度模式 | 超时后能降级或提示,不无限转圈 |
| 振动反馈 | 定位成功和失败各一次 | 有短反馈,页面切走后停止 |
| 拍照 | 拍照后预览 | 图片方向正确,不空白 |
| 相册 | 从相册选择 | 路径可读,预览正常 |
| 扫码 | 连续进入退出三次 | 每次都有预览,退出后不黑屏 |
| 权限关闭 | 系统设置里关相机后扫码 | 提示明确,不崩溃 |
这张表里“连续进入退出三次”是我加的血泪经验,扫码插件只测一次很容易放过释放问题。权限关闭场景也要测,用户拒绝权限后页面不能卡死。真机验收时把日志打开,发布前再关掉,别让调试日志跟着包走。
从那以后我每次接 Cordova 原生能力,都先把 deviceready、权限、销毁这三个动作写成检查项再动业务代码。希望帮到你。
本文还有配套的精品资源,点击获取