简介面向具备 Vue 基础、希望把 Web 应用延伸到移动端原生能力的前端开发者与跨平台学习者内容围绕 Vue 与 Cordova 的集成展开覆盖地理位置获取、手机振动、调用手机图片或拍照、二维码扫描等常见原生功能并关注 camera、geolocation、vibration、barcode scanner 等插件在 Vue 组件中的调用方式、权限请求及多平台兼容问题。压缩包为 zip 格式整体约 14.48MB当前文件总数为 0类型明细暂无数据实际文件构成需以解压结果为准。已有 534 人学习或下载适合用来了解 Vue 项目接入 Cordova 原生能力的整体思路。其价值在于把分散的原生功能串联到同一套 Vue 工程语境中便于读者对照理解组件组织、插件配置、权限处理与真机验证的关注点也可作为移动端功能原型和跨平台方案选型时的参考材料。1. 从一次真机联调说起Vue 调不动原生能力的常见场景上周帮一个团队处理问题他们的 Vue 移动端页面在手机浏览器里调相机被系统拦、定位偏出几百米、扫一扫干脆找不到入口。这类需求的解法并不复杂用 Cordova 把已有的 Vue 产物包进原生 WebView让拍照、扫一扫、定位、振动这几件事改走原生桥前端业务代码基本不用大改改的只是调用层。它适合两类人手里已有 Vue 移动端项目、被要求快速出安卓和 iOS 安装包的团队以及正在做混合应用、需要把 H5 的能力边界往外推一推的开发者。反过来说如果你的页面只跑在微信里、或者对首屏性能极度敏感这套方案反而会拖慢你。下面按我自己实际接入的顺序讲先看工程怎么组织再逐个拆定位、振动、拍照、扫一扫的参数最后把我在真机上遇到的问题摊开说。2. 构建链与环境Vue 产物怎么接进 Cordova 的壳Cordova 的运行模型很朴素它会起一个原生 WebView加载www/目录下的静态文件然后通过cordova.js在 JS 和原生之间架一条消息通道。所有插件本质上都是这条通道上的一个处理器。理解了这一点工程怎么组织就没什么玄学了谁的产物最终落在www/谁就是被加载的那一方。2.1 两种工程组织方式先选对再动手第一种是以壳为主导先cordova create生成一个完整的 Cordova 工程再把 Vue 项目的源码搬进www/或者它的子目录里开发。第二种是以前端为主导Vue 项目独立开发、独立构建构建完把dist/同步进 Cordova 工程的www/。维度壳主导Vue 放进 www前端主导dist 同步进 www前端热更新需要额外配置 devServer 代理原生支持npm run serve照常构建链Cordova 里再套一层前端工具链前端工具链不变多一步同步CI 友好度一般好构建脚本一条命令跑完多人协作前端同学要装 Cordova 环境只有打包的人需要 Cordova 环境适用场景小项目、一次成型长期迭代、前后端分离团队我一般推荐第二种。原因很实际Cordova 的环境依赖JDK、Android SDK、Gradle对纯前端同学来说门槛不低把这份负担限制在打包环节日常开发体验不受影响。代价是每次真机验证前要跑一遍同步这一步写成 npm script 就能自动化。2.2 创建外壳工程与安装插件的最小命令序列壳工程和 Vue 工程放在同级目录互不干扰。下面这套命令是我每次起新项目都会跑一遍的# 1. 创建壳工程包名按自己项目改 cordova create shell com.example.hybrid HybridShell cd shell # 2. 添加平台android 平台建议显式指定大版本避免 CI 上拉到不兼容的新版本 cordova platform add android cordova platform add ios # 3. 逐个装插件一次装一个装完就跑一遍真机别攒着 cordova plugin add cordova-plugin-geolocation cordova plugin add cordova-plugin-vibration cordova plugin add cordova-plugin-camera cordova plugin add phonegap-plugin-barcodescanner # 4. 确认插件真的落到了 platforms 目录里 cordova plugin listcordova create的三个参数分别是目录名、反向域名包名、显示名。包名一旦确定就不要改后面签名、上架、推送注册全都绑在它上面改一次要重新走一遍流程。cordova platform add建议在 CI 或团队协作时显式锁大版本比如写成android12这种形式。平台上小版本升级经常带进来 Gradle 版本要求的变化某天流水线突然编译不过八成就是这个原因。cordova plugin list是验收入口每个插件后面会带版本号和它挂载的平台装失败的时候这里不会有记录比翻日志快。插件安装顺序也有讲究先装无依赖的振动、定位再装带原生依赖的相机、扫码。相机插件在部分 Android 版本上会引入额外的 support 库先装扫码后装相机会触发一次依赖重排偶尔会失败反过来基本不会。2.3 让构建产物落到 www一条同步脚本Vue 侧只需要改一个配置项这个配置项错了就是白屏没有第二种可能// vue.config.js module.exports { // 关键必须为空字符串或 ./。 // Cordova 用 file:// 协议加载页面写成 / 会被解析成设备文件系统的根目录 // 结果就是 index.html 能打开js/css 全部 404页面白屏。 publicPath: , outputDir: dist, productionSourceMap: false, // 混合应用里通常不需要 PWA 的 service worker容易和 WebView 缓存打架 pwa: { workboxPluginMode: None } }publicPath: 的作用是把所有资源引用改写成相对路径./js/app.js这种形式。用 Vite 的话对应的是base: ./道理完全一样。productionSourceMap: false是我个人的偏好混合应用的包体积本来就敏感sourcemap 关掉能省不少真要看堆栈的时候再临时打开。同步脚本写在 Vue 项目的package.json里用跨平台工具别用cp -rWindows 同事会来感谢你{ scripts: { serve: vue-cli-service serve, build:h5: vue-cli-service build, sync:www: rimraf ../shell/www/* cpx \dist/**/*\ ../shell/www, build:android: npm run build:h5 npm run sync:www cd ../shell cordova build android } }rimraf ../shell/www/*这一步不能省。Cordova 的www/是普通目录构建不会自动清理旧文件上一次带 hash 的app.3f2a.js会一直躺在里面时间久了包体积越来越大还会出现“明明改了代码装上去还是老样子”的奇怪情况。cpx负责把dist/下所有文件平铺复制过去**/*保证子目录也能带上。2.4 deviceready 是唯一可靠的启动信号这一点很多人第一次接入时都会出错。Cordova 的 JS 桥不是页面加载完就绪的原生侧初始化完成时会派发一个deviceready事件早于这个时间点调用任何插件 API拿到的都是 undefined。// src/native/bootstrap.js export function waitForCordova(timeout 8000) { return new Promise((resolve, reject) { // 已经就绪的情况比如热重载或者页面内二次调用 if (window.cordova window.cordova.platformId ! browser) { return resolve(window.cordova) } const timer setTimeout(() { reject(new Error(cordova deviceready timeout)) }, timeout) document.addEventListener(deviceready, () { clearTimeout(timer) resolve(window.cordova) }, { once: true }) }) }{ once: true }保证监听器执行一次就摘掉避免路由来回切的时候挂着几十个监听。timeout参数是为了在浏览器里调试时不至于无限等待8 秒足够慢设备完成初始化。调用方在main.js里这样用import { waitForCordova } from ./native/bootstrap waitForCordova() .then(() { // 只有走到这里才认为原生能力可用 store.commit(SET_NATIVE_READY, true) new Vue({ render: h h(App) }).$mount(#app) }) .catch(() { // 浏览器环境或者初始化失败挂载一个降级版应用 store.commit(SET_NATIVE_READY, false) new Vue({ render: h h(App) }).$mount(#app) })关键在 catch 分支也要把应用挂载起来。很多项目的做法是失败就不挂载结果浏览器里打开一片白调样式都没法调。挂载之后用nativeReady这个状态位控制按钮的禁用和提示文案浏览器里照样能看布局。3. 定位与振动权限链、坐标系和时长这两个能力在插件层面都很轻加起来不到两百行但它们暴露出来的问题最典型定位偏几百米、振动在某些机型上完全没反应。前者是坐标系问题后者是平台差异都跟代码写得对不对无关。3.1 定位插件装完之后先验权限cordova-plugin-geolocation装好之后navigator.geolocation会被替换成原生实现。但它只负责取值不负责帮你申请运行时权限——Android 6.0 以后权限是运行时授予的用户点拒绝之后再调 API 只会静默失败不报错也不回调。我的做法是在调用之前先查一次权限状态。可以用cordova-diagnostic-plugin也可以直接用插件回调里的错误码兜住。下面这段是错误码分支的核心逻辑// src/native/geo.js export function getPosition(options {}) { const defaults { enableHighAccuracy: true, // 强制走 GPS 而不是基站/WiFi 定位 timeout: 10000, // 超时后走 error 回调不会一直挂着 maximumAge: 0 // 0 表示不复用缓存每次都重新取 } const opts { ...defaults, ...options } return new Promise((resolve, reject) { navigator.geolocation.getCurrentPosition( pos resolve({ lng: pos.coords.longitude, lat: pos.coords.latitude, accuracy: pos.coords.accuracy, // 单位米用来判断可信度 timestamp: pos.timestamp }), err { // 1 PERMISSION_DENIED, 2 POSITION_UNAVAILABLE, 3 TIMEOUT const map { 1: 定位权限被拒绝请到系统设置里开启, 2: 暂时拿不到位置室内信号弱时常见, 3: 定位超时建议放宽 timeout 到 15 秒 } reject(new Error(map[err.code] || 定位失败)) }, opts ) }) }enableHighAccuracy打开后会优先用 GPS 芯片代价是首次定位慢、耗电高。如果是打卡、签到这类一次性场景打开它值得如果要做持续轨迹建议改成watchPosition并配合maximumAge: 30000让它复用三十秒内的缓存结果功耗能降一大截。accuracy这个字段值得单独留一下。它表示当前坐标的置信半径单位米。室外开阔地通常在 5 到 20 之间室内可能直接干到 500 以上。业务上如果拿到 accuracy 超过 100 的坐标做距离判断就要打折扣否则用户站在楼里也能“打卡成功”。3.2 WGS84 与 GCJ-02坐标偏几百米的真正原因原生定位插件返回的是 WGS84 坐标也就是国际通用的 GPS 坐标系。而国内地图服务在展示和检索时普遍使用的是一套经过偏移处理的坐标系两者直接混用会偏出几百米这就是很多人说“定位不准”的真实来源——代码没错坐标系统错了。处理方式有两种。第一种是在前端做一次坐标转换把 WGS84 转成地图服务需要的坐标系转换算法公开几十行代码就能实现精度在米级。第二种是把原始坐标直接传给后端由后端统一转换前端只存 WGS84。我倾向第二种。原因是转换逻辑一旦铺到前端每个调用点都要记得转一次漏一处就是一个线上问题而且历史数据里会混着两套坐标后面做轨迹分析时很难清理。统一在后端入口转前端只保证一件事所有坐标字段都标注清楚它是 WGS84。// 记录时必须带上坐标系标记别只存两个数字 const record { lng: pos.lng, lat: pos.lat, coordType: WGS84, // 显式声明避免半年后没人知道这是什么坐标 accuracy: pos.accuracy, capturedAt: pos.timestamp }这个字段看着多余实际能省掉大量排查时间。我见过一个项目历史数据里一半是转换过的一半没转最后只能靠 accuracy 值的分布去猜哪批是原始数据。3.3 振动pattern 数组和 iOS 的上限振动插件提供的 API 就一个// 连续振动 500 毫秒 navigator.vibrate(500) // 节奏振动振 200ms停 100ms再振 200ms停 100ms navigator.vibrate([200, 100, 200, 100]) // 传 0 立即停止 navigator.vibrate(0)参数含义很直白单个数字表示持续毫秒数数组表示“振动-停顿-振动-停顿”交替的毫秒序列。数组的长度建议控制在 6 个以内太长的节奏在低端机上会被截断。这里有一个平台差异需要注意iOS 上navigator.vibrate不受时长控制无论传 100 还是 2000效果都是系统固定的一次短振动数组形式也会被降级成单次。所以如果你的交互设计里“长振动表示警告、短振动表示成功”在 iOS 上就失效了。稳妥的做法是把振动当成辅助反馈主反馈还是靠视觉和声音振动只做锦上添花。另外Android 上振动需要VIBRATE权限插件在安装时会自动写进清单文件一般不用管。但如果你的AndroidManifest.xml是手工维护的有些团队会把它从 platforms 目录提到外面升级插件之后记得核对一下这个权限还在不在。3.4 把这两个能力包成统一的调用层业务代码里不应该到处出现navigator.geolocation和navigator.vibrate。统一收口到一层适配里好处是浏览器降级和错误提示都只写一次// src/native/index.js import { getPosition } from ./geo export const native { async locate() { if (!window.cordova) { // 浏览器环境降级到 Web Geolocation行为和精度会有差异 return getPosition({ enableHighAccuracy: false, timeout: 8000 }) } return getPosition() }, vibrate(pattern 200) { // 浏览器里 navigator.vibrate 存在但多数桌面端无效加个类型判断 if (typeof navigator.vibrate function) { navigator.vibrate(pattern) } } }locate里对浏览器环境放宽了enableHighAccuracy因为桌面浏览器开着高精度反而更容易超时。vibrate里先判断函数是否存在桌面浏览器里这个 API 有时候存在但调用无效所以外面还要包一层业务侧的 try。4. 拍照与扫一扫Camera 和 BarcodeScanner 的参数与回调这两个是混合应用里用得最多的原生能力也是最容易出问题的两个。相机插件的参数有十来个真正需要认真调的是四个扫码插件的坑主要在调用时序上。4.1 getPicture 的四个必调参数// src/native/camera.js export function takePhoto({ source camera, quality 80 } {}) { return new Promise((resolve, reject) { const options { quality, // 0-100JPEG 压缩质量 destinationType: Camera.DestinationType.FILE_URI, // 返回文件路径而不是 base64 sourceType: source album ? Camera.PictureSourceType.PHOTOLIBRARY // 相册 : Camera.PictureSourceType.CAMERA, // 相机 encodingType: Camera.EncodingType.JPEG, mediaType: Camera.MediaType.PICTURE, targetWidth: 1280, // 限制长边直接压掉大图的解码压力 targetHeight: 1280, correctOrientation: true, // 关键按 EXIF 把照片转正 saveToPhotoAlbum: false, // 拍完不留系统相册避免用户相册被塞满 allowEdit: false // 原生裁剪框体验差一般关掉自己做 } navigator.camera.getPicture(resolve, reject, options) }) }destinationType选 FILE_URI 是为了避开 base64 的内存问题。一张 1280 长边的 JPEG 转成 base64 之后字符串长度通常是文件体积的 1.33 倍而字符串在 WebView 里是 UTF-16 存储实际内存占用还要再翻倍。连续拍三张不释放中低端机型就容易挂。correctOrientation: true这个参数我建议永远打开。手机拍照时传感器方向和实际握持方向不一定一致照片会带一个 EXIF 方向标记WebView 里的img不认这个标记竖着拍的照片显示出来是横的。打开之后插件会在原生侧直接旋转像素虽然多花一点处理时间但省掉了一大堆前端旋转的代码。targetWidth和targetHeight同时给出时插件会等比缩放到能放进这个框里不会拉伸变形。1280 这个值是我常用的平衡点清晰度够用于大多数业务场景文件体积通常在一两百 KB上传和预览都不吃力。如果业务是证件照或者票据识别把它提到 1920 甚至 2560但要注意老设备的处理耗时。quality只对 JPEG 生效80 是个经验值。往下降很快就能看出块效应往上提体积涨得比画质明显。4.2 FILE_URI 之后怎么预览和上传拿到file:///storage/...这样的路径后直接在img src里用是没问题的WebView 允许加载本地文件。但要上传就得先读出内容// 把本地文件路径转成 Blob方便走 FormData 上传 export function uriToBlob(fileUri) { return new Promise((resolve, reject) { // resolveLocalFileSystemURL 由 cordova-plugin-file 提供camera 插件会带上它 window.resolveLocalFileSystemURL(fileUri, entry { entry.file(file { const reader new FileReader() reader.onloadend () resolve(new Blob([reader.result], { type: file.type })) reader.onerror reject reader.readAsArrayBuffer(file) }, reject) }, reject) }) }用readAsArrayBuffer而不是readAsDataURL是为了避免再走一遍 base64 编码。得到的 Blob 直接塞进FormData就能上传服务端按普通文件接收和 H5 端的接口完全兼容。resolveLocalFileSystemURL在部分 Android 版本上对content://开头的 URI 支持不好这时候通常要把destinationType换成 NATIVE_URI或者干脆退回 DATA_URL 但把targetWidth压到 800 以内。这个取舍没有通用答案按目标机型的分布决定。4.3 扫码插件的选型与调用时序市面上常用的扫码插件大致两类一类是直接调起一个全屏扫码界面扫完返回结果另一类是把扫码能力嵌在自己的页面里可以自定义遮罩和提示。前者接入快后者体验好但配置复杂。维度全屏扫描类如 barcodeScanner内嵌扫描类如 qrscanner接入成本低一个方法调用中要管生命周期界面定制几乎不能改完全可控支持的码制多一维码二维码都支持主要二维码相机占用扫完自动释放需要手动 show/hide/destroy适合场景后台管理、工具类面向 C 端的主流程如果只是后台工具里加个扫码入口全屏扫描类足够代码就三行cordova.plugins.barcodeScanner.scan( result { if (result.cancelled) { // 用户主动返回不是错误别弹提示 return } // result.text 是码内容result.format 是码制如 QR_CODE handleScan(result.text, result.format) }, err { // 只有真正的错误才会走到这里权限被拒、相机被占用等 showToast(扫码失败 err) }, { preferFrontCamera: false, showFlipCameraButton: true, showTorchButton: true, prompt: 把二维码放进框内, resultDisplayDuration: 0 // 扫完立刻返回不要在原界面停留展示结果 } )resultDisplayDuration: 0这个参数值得单独说。它的默认值会在扫到码之后在扫码界面上停留一小会儿展示识别结果用户会以为卡住了。设成 0 之后扫到即回流转顺畅很多。内嵌扫码类的调用时序要严格按prepare → show → scan走async function startScan() { await QRScanner.prepare() // 申请相机权限必须最先做 QRScanner.show() // 把 WebView 背景切成透明露出下面的相机预览 QRScanner.scan((err, text) { if (err) { QRScanner.hide() QRScanner.destroy() // 释放相机不释放的话下次启动会黑屏 return } handleScan(text) }) }show()这步的原理是把 WebView 的页面背景设为透明让原生的相机预览层透出来所以你的扫码页面必须留出透明的区域否则相机画面会被自己的页面元素挡住。destroy()一定要调相机是独占资源不释放的话第二次进入扫码页面就是一片黑。5. 避坑与排查五个真机上反复出现的问题下面这五条都是我在真机联调阶段实际遇到的问题每一条都按“现象、原因、解决”拆开写。放在一起是因为它们的表象很像都是页面看着没问题但功能不动只有分清根因才能少走弯路。5.1 页面白屏控制台什么都没有现象cordova build出来的安装包装上去启动后一片白连 Vue 的报错都没有插上调试线看控制台也是空的。原因九成是资源路径问题。Cordova 用file://协议加载index.html如果你在构建配置里用了默认的绝对路径script src/js/app.js会被解析成设备文件系统的根目录请求直接 404。第二个可能是路由模式用了 history 模式file://下没有服务端做重写任何非首页路径都打不开。解决构建配置里把publicPathVue CLI或baseVite设成或./路由切到 hash 模式。验证方法很简单构建完成后用浏览器直接打开dist/index.html能正常显示就说明路径没问题因为浏览器打开本地文件同样是file://协议和 WebView 的环境一致。5.2 权限被拒一次之后再也调不起来现象用户第一次点拍照系统弹权限框手滑点了拒绝。之后无论怎么点都没有任何反应也不弹框也不报错。原因Android 的运行时权限有个设计用户拒绝一次之后后续申请不再弹窗直接返回拒绝。相机插件的错误回调在这种情况下的行为并不统一部分版本既不触发成功也不触发失败Promise 会一直挂着。解决在调用前主动查权限状态被拒绝时引导用户去系统设置页。用cordova-diagnostic-plugin可以查到具体状态也可以在业务层维护一个“已拒绝”标记第一次失败后就把按钮改成“去设置里开启权限”并跳转应用详情页。另外给所有原生调用加上超时兜底超过 15 秒没回调就按失败处理至少不会让用户一直等着。5.3 大图把 WebView 内存占满导致崩溃现象拍照预览正常连着拍三四张之后应用闪退或者页面变成灰屏重启应用才恢复。原因用了DestinationType.DATA_URL返回 base64 字符串或者虽然用了 FILE_URI 但在前端把每张图都转成了 base64 存在 Vuex 里。一张 4000×3000 的原图解码成位图大约占 48MB三张就超过多数中端机的单进程内存上限。解决destinationType固定用 FILE_URI并且设置targetWidth和targetHeight限制解码尺寸。预览时用本地路径直接给img不要转 base64。上传用 Blob FormData。如果业务确实需要多图做完一张就释放引用列表里只存路径不存内容。5.4 扫码第一次能用第二次黑屏现象第一次进扫码页面正常返回后再次进入相机预览区是全黑的没有任何报错。原因相机是独占资源上一次的扫码会话没有调hide()和destroy()相机句柄还挂在上一层的 Surface 上新的会话拿不到资源。解决在页面的beforeDestroy或路由离开的守卫里强制释放beforeDestroy() { if (window.QRScanner this.scanStarted) { QRScanner.hide() QRScanner.destroy(() { this.scanStarted false }) } }用全屏扫描类插件的话这个问题不明显因为它自己管生命周期但内嵌类一定要自己管。加一个scanStarted标记避免重复释放导致的异常。5.5 打包后接口全部失败浏览器里却正常现象开发时用npm run serve调接口一切正常打成安装包之后所有请求超时抓包看不到任何数据出去。原因Android 9 以后默认禁止明文 HTTP 请求。浏览器和开发服务器通常跑在 localhost不受这个限制但打包后的应用访问的是真实地址如果接口还是 HTTP请求会被系统直接掐掉日志里只有一行不太显眼的 cleartext 提示。解决首选把接口全部升级到 HTTPS这是根治。如果短期内做不到需要在config.xml里配置网络安全策略允许特定域名走明文别图省事全局放开。配置完之后一定要重新cordova build只改 platforms 目录下的清单文件不会生效下次构建就被覆盖了。6. 进阶把原生回调收敛成一层 Promise 适配插件原生的 API 全是回调风格散在业务代码里既难测也难降级。我一般的做法是在src/native/下加一层薄适配把所有原生能力统一成 Promise业务侧只认这一层。6.1 一个带超时和降级的适配器// src/native/adapter.js const DEFAULT_TIMEOUT 15000 export function promisify(fn, { timeout DEFAULT_TIMEOUT, fallback } {}) { return new Promise((resolve, reject) { let settled false const timer setTimeout(() { if (settled) return settled true // 超时优先走降级实现没有降级就报超时 fallback ? resolve(fallback()) : reject(new Error(native call timeout)) }, timeout) const done (err, res) { if (settled) return settled true clearTimeout(timer) err ? reject(err) : resolve(res) } try { fn(done) } catch (e) { done(e) } }) }这个适配器的价值在于settled这个标记。原生插件在某些异常路径下会回调两次一次成功一次失败没有这个标记的话 Promise 状态会被二次修改后面的逻辑就跟着乱。fallback参数则是给浏览器环境留的出口比如扫码在浏览器里可以降级成手动输入框。用起来是这样import { promisify } from ./adapter export const scanCode () promisify( done cordova.plugins.barcodeScanner.scan( r done(null, r), e done(e) ), { fallback: () ({ text: , cancelled: true }) } )业务代码里就只剩const r await scanCode()不用关心回调签名也不用关心在哪个环境跑。6.2 真机验证清单每次接入新插件或者升级平台版本我都会照着下面这张表跑一遍大概二十分钟能挡掉大部分上线后才发现的问题。检查项验证方式通过标准资源加载断网冷启动首屏正常渲染无 404deviceready首屏打日志8 秒内触发且只触发一次权限首次拒绝卸载重装后点拒绝有明确提示不卡死权限二次调用拒绝后再点一次引导到设置页返回后可用相机内存连续拍 5 张不闪退预览不灰屏扫码重复进入进出扫码页 3 次每次都有画面定位精度室外开阔地取点accuracy 小于 20 米明文请求打包后调接口无 cleartext 拦截日志返回键各页面按物理返回行为符合预期不直接退出表格里最后一项容易被忽略。混合应用里物理返回键的默认行为是退出整个应用而不是路由后退需要额外监听backbutton事件并阻止默认行为这个和路由的配合在 Android 上尤其容易出问题。从那以后我每次接新的原生插件都先把这张表跑一遍再写业务代码宁可前期多花二十分钟也不想在上线前一晚对着真机反复试。这套示例的完整目录结构、插件清单和适配层代码我整理在同一份资源里按第 2 章那几条命令跑一遍就能对上。希望帮到你。本文还有配套的精品资源点击获取