Vue+Cordova混合开发:原生能力集成与真机联调避坑
简介:这份面向Web前端与跨平台移动开发者的Vue集成Cordova实践资料,聚焦用Vue构建界面、借Cordova调用设备原生能力,覆盖地理位置获取、手机振动、相机与相册取图、二维码扫描等常见场景,适合希望把Web应用延伸到移动端并补齐原生功能链路的初中级开发者。压缩包约14.48MB,文件总数记录为0,暂未提供可核实的文件类型明细,因此包内结构需以下载后实际内容为准。已有534人学习或下载,具备一定的参考热度。其价值在于把环境与项目接入思路、插件选型与调用方式、权限请求、不同平台兼容性处理以及真机调试与实时刷新等环节串联起来,读者可据此理解Vue与Cordova协作的工程组织方式,并迁移到拍照、定位、扫码等业务模块中,减少从Web页面到原生能力之间的摸索成本。
1. 从 H5 到真机能力:这套 Vue + Cordova 工程解决什么场景
上周被拉去救一个活动页的场,页面本身是 Vue 写的,需求列出来很朴素:扫码入场、拍照上传、定位打卡、点按钮给一下振动反馈。浏览器里跑着一点毛病没有,一上真机就露馅——部分机型定位弹窗点完没反应,相册选完大图内存直接飙,扫码那步纯 H5 根本摸不到原生相机。最后的解法是把 Vue 打包出来的静态资源塞进 Cordova 容器,由插件桥接原生能力,业务代码几乎没动。这套 Vue 集成 Cordova 的组合,适合手上已经有成型 H5 项目、又必须摸到设备硬件的团队:定位、振动、拍照、扫一扫这几件事,走原生插件比自己写 Web API 稳得多。
2. 工程骨架:Vue 单页项目怎么套进 Cordova 容器
2.1 技术选型:为什么保留 Vue,只把 Cordova 当壳
先把边界划清楚。这个项目的复杂度集中在业务页面:表单校验、列表渲染、状态流转,这些 Vue 的响应式和组件生态是最顺手的;而定位、振动、相机、扫码这些属于设备能力,属于另一端。Cordova 的定位恰好是「壳 + 桥」,它不接管 UI,不干预你的构建工具,只做两件事:用系统 WebView 加载你打包出来的www/index.html,以及往页面里注入一个cordova.js,让 JS 侧能通过 exec 桥调到原生实现。
所以技术栈是:Vue 3(Composition API 写业务)+ Vite(构建,产出纯静态资源)+ Cordova(容器与插件管理)。插件侧常用的四个是cordova-plugin-geolocation、cordova-plugin-vibration、cordova-plugin-camera、扫码类插件(barcode-scanner 系)。这几个插件的 API 风格高度一致:成功回调 + 失败回调 + options 对象,封装起来很规整。
为什么不换别的路线,这里有一张我当时的对比表:
| 方案 | 上手成本 | 原生能力覆盖 | 适合的场景 |
|---|---|---|---|
| Vue + Cordova | 低,业务代码基本不动 | 依赖插件生态,常用能力齐全 | 已有 H5 项目,只补设备能力 |
| 跨端编译框架 | 中,需要按框架语法重写页面 | 较全,但受框架抽象层限制 | 需要同时出小程序 / H5 / App |
| 原生 + 内嵌 H5 | 高,要维护两套工程 | 最全 | 原生交互占比很高 |
选第一行的理由很实际:页面已经写完了,重写一遍不划算,而这次要的能力都是「标准件」,插件市场里都有成熟实现。代价是启动时会多一次 WebView 初始化,首屏比原生慢个几百毫秒,这个在活动页场景下可以接受。
提示:Cordova 的插件回调是老式 Node 风格,错误对象经常是字符串而不是 Error,封装层里要统一处理,否则上层 catch 到的东西五花八门。
2.2 目录结构:src 之外多出来的 www 和 plugins
跑起来之后目录大概长这样,关键是分清哪些是手写的、哪些是命令生成的:
project-root/ ├── src/ # Vue 业务代码,全部手写 │ ├── views/ │ │ ├── ScanEntry.vue # 扫码入场页 │ │ └── CheckIn.vue # 定位打卡页 │ ├── services/ │ │ ├── native.js # 原生能力统一出口 │ │ ├── geo.js │ │ ├── camera.js │ │ └── haptic.js │ └── main.js ├── public/ │ └── index.html # 需要手动引入 cordova.js ├── config.xml # 应用配置:包名、权限、插件声明 ├── vite.config.js # base 必须是 './' ├── platforms/ # cordova platform add 后生成,别手改 ├── plugins/ # cordova plugin add 后生成,别手改 └── www/ # 构建产物落点,Cordova 从这里加载几个文件的职责要记牢:
| 文件 / 目录 | 谁生成 | 改动频率 | 说明 |
|---|---|---|---|
config.xml | 手动维护 | 中 | 包名、版本、权限、插件声明都在这 |
platforms/ | 命令生成 | 从不手改 | 删了重新 add 就能恢复 |
plugins/ | 命令生成 | 从不手改 | 插件源码,改了下次 install 会被覆盖 |
www/ | 构建产出 | 每次构建 | 里面必须包含cordova.js的引用入口 |
src/services/ | 手写 | 高 | 所有原生调用都收敛到这里 |
把原生调用全部收进services/是我踩过坑之后定下的规矩。早期图省事,直接在页面组件的mounted里写navigator.camera.getPicture(...),结果三个页面各写一遍,参数还不一样,改一个质量参数要翻三个文件。
2.3 从零跑通:建工程、装插件、出真机包
第一步先起 Vue 工程,用 Vite 的 vue 模板,一条命令的事:
# 建 Vue 工程 npm create vite@latest my-app -- --template vue cd my-app npm install第二步,在工程外层建 Cordova 壳。这里我习惯壳和业务代码放在同一个仓库的不同目录,shell/只放 Cordova 相关的东西:
# 全局装 CLI npm install -g cordova # 建壳工程,包名按自己项目改 cordova create shell com.example.demo DemoApp cd shell cordova platform add androidplatform add这一步会拉一堆依赖,第一次跑比较慢。装完之后开始加插件,四个能力对应四个插件,逐个 add:
# 定位 cordova plugin add cordova-plugin-geolocation # 振动 cordova plugin add cordova-plugin-vibration # 拍照 / 相册 cordova plugin add cordova-plugin-camera # 扫码 cordova plugin add cordova-plugin-barcode-scanner第三步是把 Vue 的构建产物直接吐到shell/www,这样省掉一次手工拷贝。改vite.config.js:
// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' export default defineConfig({ plugins: [vue()], // 关键:WebView 用 file:// 加载,绝对路径会 404 base: './', build: { outDir: resolve(__dirname, 'shell/www'), emptyOutDir: true, // 低版本 WebView 不认识太新的语法,压低 target target: 'es2015' } })base: './'和target: 'es2015'这两行是血泪经验。前者不设,打包出来的index.html里资源路径是/assets/xxx.js,WebView 从file://加载时这个斜杠指向文件系统根目录,页面直接白屏;后者不设,部分系统 WebView 版本偏低,解构、可选链这类语法直接报错。
然后处理index.html里的cordova.js。这个文件不在源码里,是 Cordova 在运行时注入的,所以构建工具不能把它当模块处理,只能用相对路径裸引:
<!-- public/index.html --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>DemoApp</title> </head> <body> <div id="app"></div> <!-- 必须放在 body 末尾,且不能用 type="module" --> <script src="cordova.js"></script> <script type="module" src="/src/main.js"></script> </body> </html>最后是构建 + 装机两步:
# 在 Vue 工程根目录 npm run build # 回到壳工程,编译并跑到设备上 cd shell cordova run android --devicecordova run会自动判断有没有连设备,连了就直接装。第一包出来之后,看到页面正常渲染、控制台打印出deviceready,说明骨架通了,接下来才轮到各个能力逐个接。
3. 四个原生能力的调用链路:定位、振动、拍照、扫码逐个拆
3.1 定位:getCurrentPosition 和 watchPosition 别混用
定位的坑在于「一次」和「持续」是两套 API,很多人拿watchPosition去做打卡这种只需要一个点的场景,结果回调连着触发好几次,一次打卡提交了三条记录。
一次性的定位调用是这样:
// src/services/geo.js export function getCurrentPosition(options = {}) { return new Promise((resolve, reject) => { // 插件装好后,navigator.geolocation 会被原生实现接管 navigator.geolocation.getCurrentPosition( (pos) => { resolve({ lat: pos.coords.latitude, // 纬度 lng: pos.coords.longitude, // 经度 accuracy: pos.coords.accuracy, // 精度,单位米,用来判断是否可信 timestamp: pos.timestamp }) }, (err) => { // err.code:1 权限拒绝,2 位置不可用,3 超时 reject(Object.assign(new Error(err.message || '定位失败'), { code: err.code })) }, { enableHighAccuracy: options.highAccuracy ?? true, timeout: options.timeout ?? 10000, maximumAge: options.maximumAge ?? 0 } ) }) }三个参数决定了体验:
| 参数 | 常用取值 | 作用与取舍 |
|---|---|---|
enableHighAccuracy | true | 走 GPS 而非基站,室内会变慢甚至超时;打卡场景建议true,城市列表页建议false |
timeout | 10000 | 超时后走失败回调。设太小室内必然失败,设太大用户干等 |
maximumAge | 0 | 允许使用多久内的缓存位置。打卡必须0,附近门店列表给60000可以秒出 |
持续定位必须成对出现,拿到点之后记得清掉监听,否则页面都销毁了回调还在跑:
// 开 const watchId = navigator.geolocation.watchPosition( (pos) => console.log('轨迹点', pos.coords.latitude, pos.coords.longitude), (err) => console.warn('轨迹失败', err.code), { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 } ) // 页面 onUnmounted 里一定要调 navigator.geolocation.clearWatch(watchId)accuracy这个字段别忽略。尝过好几次精度 2000 米的定位结果,那是基站兜底给的,直接拿去做 500 米范围的打卡校验必然误判,业务侧至少要卡一个accuracy < 100的门槛。
3.2 振动:一行调用也有兼容性差异
振动是四个能力里最简单的,但也是最容易被产品当「玄学」提需求的——「按下去要有一点点反馈感」。实现上做双通道兜底,先试标准 Web API,不行再走插件:
// src/services/haptic.js const DEFAULT_DURATION = 30 export function tap(duration = DEFAULT_DURATION) { // 标准 API,多数 WebView 已经原生支持,没有额外开销 if (typeof navigator.vibrate === 'function') { navigator.vibrate(duration) return true } // Cordova 插件通道 if (window.navigator && window.navigator.notification) { window.navigator.notification.vibrate(duration) return true } return false } // 节奏型振动,比如「错误提示抖两下」 export function buzzPattern() { if (typeof navigator.vibrate === 'function') { navigator.vibrate([0, 60, 80, 60]) // 停 0 / 振 60 / 停 80 / 振 60 return } if (window.navigator && window.navigator.notification) { window.navigator.notification.vibrateWithPattern([0, 60, 80, 60], 0) } }这里有个平台差异必须交代清楚:vibrate(duration)的时长参数在部分平台上不生效,系统只认「振一下」这个动作,不认时长。所以产品如果说「要 200 毫秒的长振」,得提前打招呼说做不到,别等到验收才发现。返回值也别忽略,拿不到振动能力时(比如用户在系统设置里关了触感反馈),业务上要能安静降级,而不是抛错打断流程。
3.3 拍照与相册:返回格式选错,内存直接爆
拍照这块翻车最多。核心就一件事:destinationType决定你拿到的是 base64 字符串还是文件路径。
// src/services/camera.js const Camera = window.Camera // 插件注入的全局对象 export function takePhoto({ source = 'camera', quality = 70, maxWidth = 1280 } = {}) { return new Promise((resolve, reject) => { if (!navigator.camera) { return reject(new Error('camera plugin unavailable')) } navigator.camera.getPicture( (result) => resolve(result), (err) => reject(new Error(typeof err === 'string' ? err : '拍照失败')), { quality, // 0-100,压缩质量,70 是画质和体积的平衡点 destinationType: Camera.DestinationType.FILE_URI, // 返回文件路径,不是 base64 sourceType: source === 'camera' ? Camera.PictureSourceType.CAMERA : Camera.PictureSourceType.PHOTOLIBRARY, targetWidth: maxWidth, // 按宽度等比缩放,控制体积的主要手段 targetHeight: maxWidth, correctOrientation: true, // 必开:否则竖拍照片在某些机型上会横躺 saveToPhotoAlbum: false, // 拍完不往系统相册里塞,避免污染用户相册 encodingType: Camera.EncodingType.JPEG } ) }) }destinationType: DATA_URL会返回一整段 base64,一张 4000×3000 的原图转成 base64 之后是十几兆的字符串,低端机直接 OOM 闪退,这个坑我吃过一次,崩溃日志里什么都看不出来,纯黑匣子。改成FILE_URI之后只拿一个路径,内存曲线立刻平了。
拿到路径之后要上传,得先把它读成二进制。这一步用 Cordova 的文件 API 转:
// 把 file:// 路径读成 ArrayBuffer,再包成 Blob 走 FormData 上传 export function uriToBlob(fileUri) { return new Promise((resolve, reject) => { window.resolveLocalFileSystemURL( fileUri, (entry) => { entry.file( (file) => { const reader = new FileReader() reader.onloadend = () => { // reader.result 是 ArrayBuffer,用它建 Blob 比 base64 省内存 resolve(new Blob([reader.result], { type: 'image/jpeg' })) } reader.onerror = () => reject(new Error('读取图片失败')) reader.readAsArrayBuffer(file) }, () => reject(new Error('获取文件对象失败')) ) }, () => reject(new Error('文件路径无效')) ) }) }上传时用FormData拼一个字段丢给 axios 就行。整条链路是:getPicture拿路径 →resolveLocalFileSystemURL拿 File →FileReader拿 ArrayBuffer → Blob → FormData,中间任何一步都用不上 base64。
3.4 扫一扫:单次扫码和连续扫码是两套写法
扫码用的是 barcode-scanner 系插件,标准调用长这样:
// src/services/scanner.js export function scanOnce(options = {}) { return new Promise((resolve, reject) => { if (!window.cordova || !window.cordova.plugins.barcodeScanner) { return reject(new Error('scanner plugin unavailable')) } cordova.plugins.barcodeScanner.scan( (result) => { if (result.cancelled) { return reject(Object.assign(new Error('用户取消'), { code: 'CANCELLED' })) } resolve({ text: result.text, format: result.format }) }, (err) => reject(new Error(String(err))), { preferFrontCamera: false, // 默认后置,扫码更快 showFlipCameraButton: true, // 允许切换前后摄 showTorchButton: true, // 暗光环境下能开手电 torchOn: false, // 只扫需要的码制,缩小识别范围能明显提速 formats: options.formats || 'QR_CODE,EAN_13,CODE_128', orientation: 'portrait', prompt: '' // 传空字符串,避免插件在取景框下面顶一行英文提示 } ) }) }几个参数值得单独说。formats不传的话插件会尝试识别所有码制,识别时间明显变长,尤其是在画面里有多个码的时候容易扫错;传了具体码制之后,识别率和速度都能上一个台阶。prompt一定要显式传空串,不传的时候插件会塞一句默认英文提示,中文界面里很突兀。
连续扫码(比如仓库盘点,要连着扫几十个)不能靠一次调用解决,得自己套循环。我的做法是在成功回调里把结果 push 进数组,然后用setTimeout隔一小段时间再唤起下一次,间隔太长用户等得烦,太短又会和上一轮的相机释放打架:
export async function scanContinuous(onResult, { interval = 300 } = {}) { const collected = [] let stopped = false while (!stopped) { try { const res = await scanOnce() collected.push(res.text) // 去重、业务校验放在 onResult 里做 onResult && onResult(collected.slice()) } catch (e) { if (e.code === 'CANCELLED') stopped = true // 用户按返回即退出循环 else console.warn('扫一次失败,继续', e.message) } // 关键:给相机一个释放窗口,否则连续唤起会黑屏 await new Promise((r) => setTimeout(r, interval)) } return collected }4. 统一原生服务层:把回调式插件封成 Promise 并有条件降级
4.1 为什么要包一层,以及包哪几件事
插件原生写法有三个问题:一是回调风格,写业务时层层嵌套,异常也没法统一 catch;二是插件对象不是随时可用,得等deviceready;三是 H5 预览环境根本没有这些对象,一调就报 undefined,开发时在浏览器里连页面都打不开。
封装层要解决的就是这四件事:
| 问题 | 处理方式 |
|---|---|
| 回调风格 | 每个能力包成返回 Promise 的函数 |
| 启动时序 | 统一waitReady(),所有调用先 await 它 |
| 错误结构不一 | 错误对象统一挂code字段,业务按 code 分支 |
| 非容器环境 | 走降级实现,返回 mock 数据,不影响开发调试 |
4.2 封装实现:一个统一出口 native.js
先做启动等待,这个必须是最基础的:
// src/services/native.js let readyPromise = null export function isInContainer() { return !!window.cordova } export function waitReady() { if (readyPromise) return readyPromise readyPromise = new Promise((resolve) => { // 已经在容器里且过了启动阶段,直接放行 if (window.__deviceReady) return resolve() if (!isInContainer()) { // 不在容器里(浏览器预览),不阻塞,直接放行走降级 window.__deviceReady = true return resolve() } document.addEventListener( 'deviceready', () => { window.__deviceReady = true resolve() }, { once: true } ) }) return readyPromise }再往上拼一层能力出口,业务只认这个对象:
// src/services/native.js 续 import { getCurrentPosition } from './geo' import { tap as vibrateTap } from './haptic' import { takePhoto, uriToBlob } from './camera' import { scanOnce, scanContinuous } from './scanner' // 浏览器预览用的假数据,只在非容器环境下生效 const mockGeo = () => new Promise((r) => setTimeout(() => r({ lat: 39.9, lng: 116.4, accuracy: 30, mocked: true }), 300)) export const native = { async geo(options) { await waitReady() return isInContainer() ? getCurrentPosition(options) : mockGeo() }, async haptic(duration) { await waitReady() return vibrateTap(duration) }, async photo(options) { await waitReady() if (!isInContainer()) throw new Error('相机能力需要真机环境') return takePhoto(options) }, async photoBlob(options) { const uri = await native.photo(options) return uriToBlob(uri) }, async scan(options) { await waitReady() if (!isInContainer()) throw new Error('扫码能力需要真机环境') return scanOnce(options) }, scanContinuous }页面里就变成一行:
// 使用侧 import { native } from '@/services/native' async function onCheckIn() { try { const pos = await native.geo({ highAccuracy: true, timeout: 12000 }) if (pos.accuracy > 100) return ElMessage.warning('定位精度不足,请到开阔处重试') await submitCheckIn(pos) await native.haptic(30) } catch (e) { // 所有原生错误都收敛到这一个 catch,按 code 分流 if (e.code === 1) ElMessage.error('定位权限未开启,请前往系统设置授权') else ElMessage.error(e.message) } }4.3 降级策略:让浏览器里也能跑起来页面
降级不是可有可无的锦上添花,而是开发效率的刚需。没有降级,前端改一个 CSS 都得打一次真机包。做法是按能力分级:
| 能力 | 浏览器降级 | 原因 |
|---|---|---|
| 定位 | 返回 mock 坐标 +mocked: true标记 | Web 定位 API 在 http 下不可用,mock 更方便 |
| 振动 | 静默返回,不报错 | 缺失不影响流程 |
| 拍照 | 抛错,页面显示「请用真机」占位 | 无合理替代 |
| 扫码 | 抛错,但提供一个手动输入框兜底 | 便于调试后续链路 |
拍照和扫码我给的是「显式失败 + UI 兜底」而不是 mock。因为这两个能力失败后,后面的上传、校验链路还是得能调,所以在扫码页放一个手动输入码值的输入框,在拍照组件里放一个本地文件选择,浏览器里就能把整条业务链路走通。
注意:
mocked这类标记字段一定要透传到业务层并在 UI 上有区分,否则测试同事在浏览器里点出来的「成功」会当成真机也通过了,上了真机才发现定位压根没拿。
5. 避坑与排查:真机联调阶段最容易卡住的几件事
5.1 启动阶段:页面白屏和 deviceready 不触发
现象一:装到真机上打开是纯白页,控制台没任何报错。
原因基本锁定在路径上。Vite 默认base是/,构建出的 HTML 引用的是/assets/index-xxxx.js,WebView 用file:///android_asset/www/index.html加载时,这个前导斜杠会被解析成文件系统根目录,资源 404,页面自然一片白。而且这个 404 在真机上不一定有日志,看起来就像代码没执行。
解决办法是把base改成'./',构建完打开www/index.html肉眼确认一遍资源路径是./assets/...才去打包。我现在的习惯是每次改完构建配置,先在本地用静态服务器起一遍www目录,路径没问题再上真机。
现象二:所有原生调用都报undefined,cordova.js提示 404。
原因有两种可能。第一种是index.html里压根没引cordova.js,或者引了但写成了type="module",构建工具把它当模块解析,找不到文件直接干掉。第二种更隐蔽:构建时emptyOutDir: true把www清空了,Cordova 注入的cordova.js也被一起删掉。
解决办法是cordova.js的引用保持裸<script src="cordova.js"></script>,不加type,放在body末尾。然后用cordova prepare而不是纯构建来刷新www,prepare会把平台相关的注入文件补齐。
现象三:逻辑都写好了,就是不执行,也没有deviceready日志。
原因是你在main.js顶层直接调了原生 API。那时候 WebView 的桥还没建立好,window.cordova是 undefined。解决办法是所有原生调用必须经过waitReady(),绝对不要在模块顶层同步调用。这个在 4.2 的封装里已经解决了,但如果有人绕过封装层直接写,这个坑还会再犯一次。
5.2 运行阶段:权限、内存和插件版本
现象四:定位一直失败,err.code是 1。
原因不是代码,是权限链路没打通。Android 需要在config.xml里声明ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION,只声明前者在高版本系统上会被系统降级成模糊定位;iOS 需要在Info.plist里写NSLocationWhenInUseUsageDescription,这段描述文案缺失的话,系统直接拒绝且不给任何弹窗提示。
解决办法是两边都补齐。Android 侧在config.xml的 platform 节点里加权限声明,iOS 侧在config.xml的edit-config里注入描述文案。另外要提醒产品:用户第一次拒绝之后,系统不会再弹第二次,必须引导去设置页手动开。这个「二次拒绝不可恢复」的行为,很多需求文档里都没写。
<!-- 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.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.CAMERA" /> </config-file> </platform> <platform name="ios"> <edit-config file="*-Info.plist" mode="merge" target="NSCameraUsageDescription"> <string>需要使用相机拍摄凭证照片</string> </edit-config> <edit-config file="*-Info.plist" mode="merge" target="NSLocationWhenInUseUsageDescription"> <string>需要获取位置用于打卡校验</string> </edit-config> </platform>现象五:连拍三五张之后 App 直接闪退,没有任何 JS 报错。
原因是destinationType用了DATA_URL,每张图都以 base64 字符串的形式驻留在 JS 堆里,几张大图叠起来就超过 WebView 的内存上限,被系统直接杀掉进程。这种崩溃最难受的地方是 JS 侧完全无感知,日志里什么都没有。
解决办法是改成FILE_URI,只持有文件路径,上传前再用resolveLocalFileSystemURL+FileReader转 ArrayBuffer。同时targetWidth一定要设,把 4000 宽的原图压到 1280,体积能降一个数量级,上传也快。
现象六:扫码插件调用直接闪退,或者提示方法不存在。
八成是插件没装到当前平台。cordova plugin add只是把插件登记到工程,platform add之后装的插件才会同步进platforms/,如果先 add platform 再 add plugin 顺序是对的,但中途删过platforms目录重新 add,插件就得重新装一遍。另外编译 SDK 版本和插件要求不匹配时,表现是编译期报错或者运行时找不到类。
解决办法是每次动过平台目录,都跑一遍cordova plugin list确认四个插件都在列表里,然后cordova clean再重新构建。装完插件不要直接run,先prepare一次,让注入的 JS 桥文件同步到www。
6. 二次开发:加一个新插件的标准动作
需求再加一个能力的时候,别急着写页面,按固定顺序走一遍能省掉大半返工。
先确认插件只有一份。同一个能力装两个功能重叠的插件,运行时谁覆盖谁是不确定的,桥接对象同名的话会互相顶掉。确认之后按这个顺序落地:
# 1. 装插件(在壳工程目录) cordova plugin add cordova-plugin-network-information # 2. 确认已登记 cordova plugin list # 3. 同步到平台并刷新 www 里的注入文件 cordova prepare然后在封装层加一个能力,规则和前面四个保持一致:先await waitReady(),再判isInContainer(),非容器环境走降级或抛出明确错误。
// 新增:网络状态,用于弱网时禁用拍照上传 export async function getNetworkType() { await waitReady() if (!isInContainer()) return { online: navigator.onLine, type: 'browser' } const type = navigator.connection.type // 插件注入 return { online: type !== Connection.NONE, type } }加完之后必须过一遍验证清单,这几项漏一项就要多打一次包:
| 检查项 | 怎么验 | 不通过的表现 |
|---|---|---|
| 插件已装到当前平台 | cordova plugin list | 调用时报方法不存在 |
| 权限已声明 | 查config.xml和构建后的 Manifest | 调用静默失败或系统直接拒绝 |
| 浏览器降级不报错 | 本地npm run dev走一遍页面 | 开发环境整页崩掉 |
| 真机首次调用成功 | 卸载重装后第一次点按钮 | 只有第二次才成功,说明时序有问题 |
| 用户拒绝后有引导 | 手动拒绝权限再进页面 | 页面卡死无提示 |
这套流程走顺之后,加一个能力的实际编码时间不超过半小时,剩下的时间都花在真机验证上。
我现在改任何 Cordova 相关代码,都会强制自己先卸载重装一遍再点第一次按钮——因为「第二次才成功」这类时序问题,只有全新安装才能暴露出来,热更新是永远测不到的。原生能力这块没有捷径,容器、权限、内存三条线各自的坑都得亲手踩一遍才记得住。希望帮到你。
本文还有配套的精品资源,点击获取