
做H5混合开发最绕不开的一个坎就是H5页面上传图片。产品经理一句“这里要支持拍照和相册选图还要能一次选多张”背后就是Android WebView和JS之间的权限、URI、回调、压缩这一堆破事。这篇文章把我自己趟过的坑、验证过的方案全部整理出来按两种主流实现方式拆开讲清楚包含完整可复用的代码和参数调优思路适用对象是正在做Android WebView混合开发、需要让H5获取手机相机或相册图片的开发者。1. 需求拆解与方案对比1.1 这个需求到底在解决什么问题表面上看H5页面里放一个按钮点击后调起相机拍照或者调起系统相册选图再把图片传回H5页面展示。但深层的问题至少有三个第一Android WebView默认不处理H5里的文件选择动作你必须自己接管onShowFileChooser第二拍照和选图拿到的不是一个文件路径而是content URI这个URI能不能被WebView读取、能不能传给H5使用是个大坑第三多张图片回传时体积、内存、回调时序都要控制否则轻则卡顿重则OOM。先说结论实现“H5获取手机相机或相册图片”这件事在当前主流Android版本下只有两条路走得通。一条是走WebView对HTML标准input typefile标签的内置支持Android端通过WebChromeClient.onShowFileChooser拦截并接管文件选择另一条是走JSBridge注入把原生相机和相册的能力封装成一个JavaScript接口H5直接调用原生处理完图片后再回调给H5。两条路我都完整实现过下面会分别展开。1.2 两种取图方式的取舍这两条路不是二选一的关系而是看你的项目场景。先说第一条input标签方案对H5代码几乎零侵入H5开发人员只需要写一行input typefile acceptimage/* multiple剩下的全部由Android端处理。这个方案最大的价值是兼容性因为这是WebView自带的文件选择机制H5跑在Android端、iOS端甚至PC浏览器上都能复用同一套前端代码。但它的短板也很明显Android端能拿到的只有最终的URI数组H5端如何预览、如何上传、是否需要压缩都由WebView内部默认行为决定自由度比较低。而且onShowFileChooser这个回调在不同ROM上表现差异很大国内厂商系统经常出现选完图不回调、多次点击无响应这类问题。第二条路JSBridge注入方案正好弥补了前者的问题。Android端可以自定义相机拍照的临时文件路径、相册选择的数量上限、图片压缩质量、回传的字段格式所有逻辑都在原生控制范围内H5端只需要调用一个全局方法并处理回调就行。代价是需要H5和Android两端约定好接口协议并且需要考虑注入接口的安全性。我的建议是如果H5页面比较简单、图片只做上传用优先用input标签方案如果产品对交互有定制需求比如限制最多选9张、需要预览原图、需要压缩后再上传直接上JSBridge方案。2. 环境准备先把地基打好2.1 AndroidManifest权限配置无论你选择哪种方案权限声明都是第一步。这里要区分Android 13前后的差异Android 13开始引入了细粒度的媒体权限不能再像以前那样只声明READ_EXTERNAL_STORAGE就完事。如果你们项目的targetSdk已经升到33以上READ_MEDIA_IMAGES必须加上否则在Android 13以上设备上相册选图会直接没反应。manifest !-- 相机权限 -- uses-permission android:nameandroid.permission.CAMERA / !-- Android 12 及以下读存储权限 -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 / !-- Android 13 读图片权限 -- uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES / /manifest权限申请建议放在进入H5页面之前也就是原生启动WebView所在的Activity时统一处理。实测下来如果权限弹窗和WebView加载同时出现有些ROM会把弹窗覆盖在WebView上用户点了授权后WebView内部状态异常。所以我的习惯是先申请权限全部拿到后再初始化WebView并加载URL。2.2 WebView基础配置WebView基础配置里比较关键的是开启JavaScript、允许内容访问以及设置好WebChromeClient。很多人漏了setAllowContentAccess(true)这个配置决定了WebView能不能访问content://协议的资源而选图回来的URI恰恰大多是content://开头一旦关掉H5的img标签就加载不出图片。WebView webView findViewById(R.id.web_view); WebSettings settings webView.getSettings(); settings.setJavaScriptEnabled(true); settings.setAllowFileAccess(true); settings.setAllowContentAccess(true); settings.setDomStorageEnabled(true); webView.setWebChromeClient(new MyWebChromeClient());另外setDomStorageEnabled(true)建议顺手开掉虽然它和选图没有直接关系但很多H5页面渲染图片时会用到localStorage或sessionStorage开掉能避免一类奇怪的图片无法显示问题。2.3 FileProvider配置如果采用JSBridge方案相机拍照时必须自己创建一个用于接收照片的临时文件然后把这个文件的URI通过MediaStore.EXTRA_OUTPUT传给系统相机。从Android 7.0开始直接传file://协议的URI会直接崩掉必须用FileProvider转换成content://协议。这块配置不复杂但经常有人忘记在Manifest里注册Provider导致拍照后黑屏或者闪退。先在res/xml/file_paths.xml里定义对外暴露的目录路径我习惯把临时图片放到应用专属的cache目录下这样不需要任何存储权限?xml version1.0 encodingutf-8? paths cache-path namecamera_tmp pathcamera/ / /paths然后在Manifest中注册FileProviderprovider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /provider注册完成后代码里通过FileProvider.getUriForFile拿到相机输出目标URI再配合Intent.FLAG_GRANT_READ_URI_PERMISSION和FLAG_GRANT_WRITE_URI_PERMISSION把临时读写权限授予相机应用这是JSBridge方案里拍照能正常返回图片的前提。input标签方案虽然可以用FileChooserParams.createIntent()自动创建带相机的选择器但如果要自定义相机输出的临时路径同样绕不开FileProvider。3. 方式一input标签 onShowFileChooser 实现3.1 H5侧怎么写利用HTML标准能力H5侧代码其实非常简单。关键点在于accept属性要写成image/*它决定了系统弹出的选择器只显示图片类文件multiple属性决定是否允许多选去掉multiple就是单选模式。代码是这样input typefile idimagePicker acceptimage/* multiple styledisplay:none / button onclickdocument.getElementById(imagePicker).click()选择图片/button script document.getElementById(imagePicker).addEventListener(change, function(e) { const files Array.from(e.target.files); console.log(选中的图片数量, files.length); files.forEach(file { // 本地预览 const url URL.createObjectURL(file); // 这里可以继续做上传处理 }); }); /script我的主力项目里H5端就是这套写法从Android 7到Android 14的机型都实测过。input标签方案下WebView会自动把用户选中的图片文件包装成File对象注入到页面里H5拿到的files数组是File实例可以用URL.createObjectURL做本地预览也可以用FormData直接上传到服务器对H5来说和PC浏览器的行为完全一致这是这个方案最舒服的地方。3.2 Android侧接管onShowFileChooser要让上面的H5代码在WebView里真正生效Android端需要在WebChromeClient里重写onShowFileChooser方法。这个方法的含义是H5页面触发了文件选择控件系统请求你提供文件选择能力。你要做的是返回true表示“这个选择我来处理”然后自己启动相册选择器或者系统文件选择器。完整代码public class MyWebChromeClient extends WebChromeClient { private ValueCallbackUri[] mFilePathCallback; private static final int REQUEST_FILE_CHOOSER 0x1001; Override public boolean onShowFileChooser(WebView webView, ValueCallbackUri[] filePathCallback, FileChooserParams fileChooserParams) { // 如果上一次回调还没处理完先置空防止回调阻塞 if (mFilePathCallback ! null) { mFilePathCallback.onReceiveValue(null); } mFilePathCallback filePathCallback; // 用系统的FileChooserParams创建Intent它已经根据accept属性做好了类型过滤 Intent contentSelectionIntent fileChooserParams.createIntent(); contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE); contentSelectionIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); Intent chooserIntent Intent.createChooser(contentSelectionIntent, 选择图片); WebViewActivity.this.startActivityForResult(chooserIntent, REQUEST_FILE_CHOOSER); return true; } }这里的ValueCallbackUri[]就是选图完成后要向WebView回传结果的通道Uri[]的含义是支持返回多个图片的URI。很多人在这里写成了ValueCallbackUri那是Android 5.0以前的老回调格式现在必须用数组泛型否则低版本机型直接不回调。3.3 多图选择与ClipData解析选完图片后结果会通过onActivityResult回到Activity这里有个极易踩坑的点用户选了一张图和选了多张图数据存放的位置不一样。单选时数据在data.getData()里多选时数据在data.getClipData()里是一个ClipData对象需要遍历拿到每一个item的URI。完整的解析逻辑Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode ! REQUEST_FILE_CHOOSER) { super.onActivityResult(requestCode, resultCode, data); return; } if (mFilePathCallback null) { return; } Uri[] results null; if (resultCode RESULT_OK) { if (data ! null) { // 多选场景 if (data.getClipData() ! null) { int count data.getClipData().getItemCount(); results new Uri[count]; for (int i 0; i count; i) { results[i] data.getClipData().getItemAt(i).getUri(); } } // 单选场景 if (data.getData() ! null) { results new Uri[]{data.getData()}; } } } // 把结果回传给WebView mFilePathCallback.onReceiveValue(results); mFilePathCallback null; }这段代码里最关键的是最后一行的mFilePathCallback null千万别漏。如果漏了下次用户再次点击选择图片时onShowFileChooser里判断到旧的callback还没清空先给它回传一个null然后用户这次新选的图片可能就不会正常显示表现为“选完图页面没反应”。从性能角度讲多选图片时WebView内部会读取每个URI对应的图片文件并封装成Blob或File对象供H5使用所以选9张高清原图时WebView的内存占用会明显上升。如果你们的业务场景经常是一次选9张全尺寸大图建议后面直接换成JSBridge方案至少能在原生层先把图压缩一遍再塞给H5。3.4 相机拍照与相册组合选择input标签方案有一点比较坑直接用fileChooserParams.createIntent()创建的选择器只包含文件选择相册/文件管理没有系统相机拍照的入口。用户如果点的是H5里的“拍照”按钮期望弹出来的应该是相机界面但Android系统不会自动帮你把相机加进去。处理思路是把相机Intent和相册Intent组合成一个Intent.createChooser让用户自己选从哪个入口取图Override public boolean onShowFileChooser(WebView webView, ValueCallbackUri[] filePathCallback, FileChooserParams fileChooserParams) { if (mFilePathCallback ! null) { mFilePathCallback.onReceiveValue(null); } mFilePathCallback filePathCallback; // 创建相册选择Intent Intent contentSelectionIntent fileChooserParams.createIntent(); contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE); contentSelectionIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); // 创建相机拍照Intent Intent cameraIntent new Intent(MediaStore.ACTION_IMAGE_CAPTURE); File cameraFile createCameraTempFile(); if (cameraFile ! null) { Uri cameraOutputUri FileProvider.getUriForFile( WebViewActivity.this, getPackageName() .fileprovider, cameraFile); cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, cameraOutputUri); cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); cameraIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); } Intent chooserIntent Intent.createChooser(contentSelectionIntent, 选择图片来源); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[]{cameraIntent}); startActivityForResult(chooserIntent, REQUEST_FILE_CHOOSER); return true; }createCameraTempFile方法就是往FileProvider配置的camera/目录下创建一个带时间戳的jpg文件。要注意的是拍照返回后onActivityResult里data参数是个空Intent或者为null因为拍好的照片已经被系统直接写到了你指定的那个cameraOutputUri里。所以判断用户是否真的拍了照、是否读取这张照片都需要围绕这个cameraOutputUri来处理。我的做法是在onActivityResult里判断如果data null且mCameraTmpUri ! null且临时文件存在就认为用户完成了拍照操作把这个URI作为结果回传给WebView。这块逻辑容易搞混建议加上注释隔两个月再回来看还能一眼看懂。4. 方式二JSBridge注入实现原生能力封装4.1 JSBridge的核心原理如果你的项目情况属于我说的“需要定制交互”那JSBridge方案就是正解。这个名字听上去玄乎拆开看就是两件事第一Android原生把一个Java对象通过addJavascriptInterface注入到WebView的JavaScript执行环境里这样H5页面里就能直接调用这个Java对象上的方法第二Java方法执行完耗时操作后通过runOnUiThread加evaluateJavascript把结果回调给页面上的JavaScript函数。ADD图片选择前Android端需要先定义一个接口协议约定Java方法名、参数格式以及回调函数名。我习惯的协议格式是Java暴露一个chooseImages(maxCount, callbackId)方法第一个参数是最大可选图片数第二个参数是回调标识字符串H5端在调用前注册一个与callbackId同名的全局函数Java处理完后调用这个全局函数并传入JSON字符串。这种设计的好处是把回调逻辑和页面业务解耦H5端如果同时触发了多次选择每个回调都有独立标识不容易串数据。4.2 Android端注入与回调实现注入对象定义如下注意方法上必须加JavascriptInterface注解不加的话从Android 4.2开始页面里调用会一直报“method not found”。这个注解相当于一个安全开关明确告诉系统哪些方法允许网页调用public class NativeImageBridge { private final WeakReferenceWebViewActivity activityRef; public NativeImageBridge(WebViewActivity activity) { this.activityRef new WeakReference(activity); } JavascriptInterface public void chooseImages(int maxCount, String callbackId) { WebViewActivity activity activityRef.get(); if (activity ! null) { activity.openImagePicker(maxCount, callbackId); } } JavascriptInterface public void takePhoto(String callbackId) { WebViewActivity activity activityRef.get(); if (activity ! null) { activity.openCamera(callbackId); } } }注入到WebView的时机建议在页面加载前完成webView.addJavascriptInterface(new NativeImageBridge(this), NativeImage);chooseImages方法里面做的事情和上一节类似启动相册选择Intent但在onActivityResult里拿到URI数组后不能直接把URI字符串通过evaluateJavascript回调给H5。原因在于content://URI的权限只授予了当前Activity直接给到H5H5虽然能显示但如果H5里要重新读取这个URI传给别人很大概率会权限不足报错。比较稳妥的做法是在原生层把每个URI对应的图片读取出来经过压缩后转成Base64字符串再统一封装成JSON串回传。这样H5拿到字符串直接就能用不用关心URI权限啊内容协议这些破事。实际的读取压缩与回调代码如下public void handleImagesSelected(ListUri uriList, String callbackId) { new Thread(() - { JSONArray jsonArray new JSONArray(); try { for (Uri uri : uriList) { String base64 compressAndEncode(uri, 1280, 80); JSONObject obj new JSONObject(); obj.put(base64, data:image/jpeg;base64, base64); jsonArray.put(obj); } } catch (Exception e) { e.printStackTrace(); } String finalJson jsonArray.toString(); runOnUiThread(() - webView.evaluateJavascript( javascript:window. callbackId ( finalJson ), null)); }).start(); }这里的compressAndEncode方法稍后在第5节详细讲。现在的重点是理解这个流程原生选择图片、原生压缩图片、原生把图片转成Base64、原生回调H5函数。中间每一步都是从H5视角来看完全透明的。4.3 H5侧调用与结果接收H5侧调用起来就比较直观了。因为addJavascriptInterface注入的NativeImage对象是全局的页面加载完成后就能直接用。为了处理回调H5端需要在调用前先注册一个和callbackId匹配的全局函数window.NativeImageBridgeCallback {}; function chooseImagesFromNative() { var callbackId cb_ Date.now(); // 先注册回调函数 window[callbackId] function(jsonStr) { var images JSON.parse(jsonStr); images.forEach(function(img) { var imgDom document.createElement(img); imgDom.src img.base64; document.body.appendChild(imgDom); }); delete window[callbackId]; }; // 调用原生方法最多选9张 window.NativeImage.chooseImages(9, callbackId); }这里面有个很关键的细节Android调用evaluateJavascript时传过去的字符串是作为JavaScript代码执行的所以如果直接传window.cb_123(data )这种拼接字符串一旦Base64内容里包含特殊字符就很容易把JS语法搞崩。所以正确做法是像我上面这样原生侧先用JSON.toString()生成标准JSON串然后用window. callbackId ( json )包裹JSON串里无论是普通字符串还是Base64字符都能被正确解析。千万不能自己做字符串拼接去伪造JSON压缩后的Base64字符串长且复杂任何一个字符拼接失误都会让整个回调静默失败。5. 图片处理与参数调优5.1 多图传输的体积控制不管走哪条方案图片体积都是一个绕不开的话题。input标签方案下WebView内部读取原图并封成File对象给H5H5如果直接把原图以multipart/form-data形式上传一张手机拍摄的全尺寸照片轻松奔着5MB以上去了。如果一次选9张体积就是45MB甚至更多移动网络下用户体验非常糟糕。JSBridge方案里图片先经过原生压缩再以Base64字符串传给H5。Base64编码本身会让数据体积膨胀约33%如果你压缩后图片是200KBBase64之后就变成约267KB9张图加起来也有2.4MB左右。所以压缩参数不能随便定我建议根据你们项目的实际上传通道来设置如果是自己的服务器且接受压缩图宽度1280、质量80是视觉和体积的平衡点如果图片要继续做二次裁剪或高清展示宽度可以放到1920但质量要降到70左右。5.2 图片压缩策略在Android端压缩图片建议用BitmapFactory采样加Matrix缩放两步走同时必须处理图片的EXIF旋转方向问题。手机摄像头拍照保存的图片会根据设备方向写入EXIF旋转信息如果无视它直接压缩出来的图在H5页面里会出现90度或180度旋转这是最容易被测试团队提bug的点。完整的压缩方法private String compressAndEncode(Uri uri, int maxWidth, int quality) throws IOException { // 第一次采样读出图片宽高 BitmapFactory.Options opts new BitmapFactory.Options(); opts.inJustDecodeBounds true; BitmapFactory.decodeStream(getContentResolver().openInputStream(uri), null, opts); // 计算采样率让解码出来的bitmap不要太大 int sampleSize 1; while (opts.outWidth / sampleSize maxWidth * 2) { sampleSize * 2; } opts.inJustDecodeBounds false; opts.inSampleSize sampleSize; Bitmap bitmap BitmapFactory.decodeStream(getContentResolver().openInputStream(uri), null, opts); // 读取EXIF旋转信息做方向校正 int rotation getExifRotation(uri); if (rotation ! 0) { Matrix matrix new Matrix(); matrix.postRotate(rotation); Bitmap rotated Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); if (rotated ! bitmap) { bitmap.recycle(); bitmap rotated; } } // 按maxWidth等比缩放 int targetWidth Math.min(bitmap.getWidth(), maxWidth); int targetHeight (int) (bitmap.getHeight() * 1.0f / bitmap.getWidth() * targetWidth); Bitmap scaled Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true); if (scaled ! bitmap) { bitmap.recycle(); bitmap scaled; } // 压缩转Base64 ByteArrayOutputStream baos new ByteArrayOutputStream(); bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos); bitmap.recycle(); return Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP); }这里opts.inSampleSize先用2的整数倍做粗略降采样再把内存里的bitmap精确缩放到目标宽度两者配合能在保证清晰度的前提下大幅降低解码时的内存峰值。一张4000x3000的照片直接解码需要约48MB内存采样4倍降到1000x750后只需要约3MB差距非常明显。值得专门提醒的是在onActivityResult回调线程里做压缩解码时不要在UI线程执行。一张大图从磁盘解码到压缩完成耗时可能达到几百毫秒9张图串行处理就是好几秒撞上UI线程轻则掉帧卡顿重则触发ANR。所以我在handleImagesSelected里用了一个new Thread来跑整个压缩循环只在最后回调WebView时切回UI线程。这是我踩过坑之后固定的写法。5.3 内存回收与异常处理多图处理最怕的就是OOM。除了上面说的采样压缩还要养成随手recycle()的习惯。每一张bitmap用完立刻回收尽量缩短bitmap的存活时间。另外注意Bitmap.createScaledBitmap在传入的bitmap尺寸和目标尺寸一致时会返回原对象所以代码里用if (scaled ! bitmap)判断是否产生了新对象避免重复recycle同一个对象导致崩溃。还有一个容易被忽略的点低端机上同时解码9张图时即使采样了内存峰值依然可能很高。更稳的做法是把串行解码改成“解码一张、压缩一张、回收一张、回传一张”不要让9张bitmap同时存于内存中。我的实际做法是压缩完一张就立刻把Base64放进JSON数组同时立即recycle当前的bitmap再处理下一张。这样整个流程的内存占用始终是“一张图”的量级而不是“九张图”的量级。6. 常见问题与排查技巧6.1 高频问题速查表问题现象可能原因解决办法优先级点击H5选择按钮后没有任何反应权限未申请或WebView未重写onShowFileChooser检查运行时权限检查WebChromeClient是否已设置高选完图片后页面不显示图片mFilePathCallback在onShowFileChooser里被重复赋值确保每次进入时先判断并置空旧回调高只选了一张图但getData()为null部分机型使用ACTION_OPEN_DOCUMENT时结果在ClipData中同时处理getData()和getClipData()两种场景中相机拍照后返回黑屏或图片空白content://URI授权失败或临时文件路径未按FileProvider配置确认cameraIntent设置了FLAG_GRANT_*并声明了FileProvider高H5回调函数一直不执行evaluateJavascript传参JS语法错误改用JSON串包裹方式回传不手动拼接中图片在H5页面里旋转了90度忽略了EXIF方向信息压缩前用ExifInterface读取旋转角并校正高选9张大图后WebView卡死/内存暴涨未做图片采样压缩直接解码原图使用inSampleSize采样解码及时recycle()高某些国产ROM选完图点击完成无响应系统文件选择器返回结果慢或回调在非UI线程确保onReceiveValue在UI线程执行可延迟100ms再回调低6.2 几个容易翻车的细节第一个是onShowFileChooser的返回值。如果这个方法返回了falseWebView会按照自己的默认逻辑继续处理但在国内很多ROM上默认逻辑就是个空转表现就是H5页面里点了按钮什么都没有。请务必确认返回了true。第二个是WebView所在的Activity在选图过程中被系统回收的问题。用户在系统相册里挑图挑了太久后台进程可能被杀死回到Activity时WebView的状态已经变了mFilePathCallback没了或者失效。如果你需要处理这种极端场景可以在onSaveInstanceState里保存当前WebView状态在onRestoreInstanceState里恢复同时因为WebView的callback没法真正序列化所以恢复后主动放弃这次选择操作让用户重新点一次这个兜底逻辑优先保证不崩溃。第三个是Android 11以上的包可见性问题。如果你们要判断手机上是否有可用的相机应用直接用resolveActivity在某些机型上可能查不到需要往Manifest里加queries声明。不过我实测下来单纯用ACTION_IMAGE_CAPTURE调相机的时候影响不大可以不做这层判断。第四个是关于缓存策略。WebView对图片的缓存策略会影响选图后的展示速度建议在WebView初始化时设置settings.setCacheMode(WebSettings.LOAD_DEFAULT)不要设成LOAD_NO_CACHE否则H5每次都要重新加载页面资源配合大量Base64图片会让页面卡顿明显。最后说一个调试技巧如果你发现H5回调一直不执行打开WebView的WebView.setWebContentsDebuggingEnabled(true)然后用Chrome的DevTools连上WebView在Console里手动执行window.NativeImage.chooseImages(9, test_cb)再执行window.test_cb([ok])就能快速定位是原生没回调还是H5函数没注册。这个手段在项目联调阶段特别好用能省下大量来回沟通的时间。结尾我个人在实际开发中的体会是H5获取相机或相册图片这种需求看着简单真正落地时细节多得像筛子。input标签方案做通用承接最快但一旦产品提出“限制数量”“压缩体积”“拍照后还要编辑”这类进阶需求JSBridge才是真正可控的路线。建议你在项目里把这两套代码都沉淀成工具类第一次可能花半天写完后续再有页面接入就是复制粘贴改参数的事。最后再分享一个小技巧压缩后的Base64字符串如果超过500KB建议直接用data:image/jpeg;base64格式传给img标签预览而不要存进localStorage或传给btoa处理否则H5端的内存同样吃不消。先把图片控制在合理体积内再谈后续上传和展示这个顺序千万不要反过来。