Android 原生功能与 Vue 交互实现全攻略
目录
- 前言
- 2.1 技术选型
- 2.2 环境准备
- 3.1 高层架构图解
- 3.2 双向通信原理
- 4.1 新建 Vue 3 项目
- 4.2 创建与 Android 通信的封装模块
- 4.3 在组件中调用原生接口示例
- 5.1 新建 Android 项目并引入 WebView
- 5.2 配置 WebView 与开启 JavaScript
- 5.3 通过
@JavascriptInterface
暴露原生方法 - 5.4 Android 调用 Vue 的方法(evaluateJavascript)
- 6.1 需求分析
- 6.2 Android 端实现
- 6.3 Vue 端实现
- 6.4 数据流动图解
- 7.1 需求分析
- 7.2 Android 端实现(Location + 权限)
- 7.3 Vue 端实现与展示
- 7.4 通信流程图解
- 常见问题与调试方法
- 总结与最佳实践
前言
随着前端框架的发展,使用 Vue 构建移动端界面已经越来越普及。然而,一旦需要调用 Android 原生功能(如摄像头、定位、传感器、推送通知等),就必须在 Web(Vue)与 Android 之间建立一条“桥梁”,通过双向通信才可实现二者无缝交互。本文将从零开始,手把手讲解如何在 Android 原生项目中嵌入 Vue 应用, 并实现 Vue ↔ 原生 的双向通信。无论你是初学者还是有一定经验的同学,都能通过本文对应的「完整示例」迅速掌握关键点。
技术选型与环境准备
2.1 技术选型
- 前端框架:Vue 3 + Vite
- 后端/中间层:Android 原生
通信方式:
- Vue → Android:调用
window.Android.xxx()
,Android 端通过@JavascriptInterface
注解的方法接收。 - Android → Vue:使用
webView.evaluateJavascript("window.onNativeCallback(...)")
或webView.loadUrl("javascript:...")
等方式触发 Vue 中定义的回调函数。
- Vue → Android:调用
构建工具:
- Vue:Vite(极简、极速热更新)
- Android:Android Studio(建议 2022+ 版本,Gradle plugin 7.x)
2.2 环境准备
Android Studio
- Android Studio Arctic Fox 或更高版本
- 配置好 Java 1.8+ JDK
Node.js + NPM/Yarn
- Node.js 14+
- 全局安装
pnpm
/npm
/yarn
中任意一种包管理工具
Vue CLI (可选)
- 若想使用 Vue CLI 创建,可全局安装
@vue/cli
,不过本文直接采用 Vite 初始化。
- 若想使用 Vue CLI 创建,可全局安装
真机或模拟器
- Android 模拟器(API 21+ 即可)或 真机调试
网络环境
- 建议将 Vue 构建产物放到 Android 项目中的
assets
目录做本地加载,也可通过远程服务器来加载(开发阶段推荐本地)。
- 建议将 Vue 构建产物放到 Android 项目中的
整体架构与通信原理
3.1 高层架构图解
┌──────────────────────────────────────────┐
│ Android 原生项目 │
│ ┌────────────────────────────────────┐ │
│ │ Android Activity │ │
│ │ ┌────────────────────────────────┐ │ │
│ │ │ WebView (容器) │ │ │
│ │ │ ┌───────────────┐ ┌───────────┐ │ │ │
│ │ │ │ index.html │ │ JS 脚本 │ │ │ │
│ │ │ │ (Vue App) │ │ │ │ │ │
│ │ │ └───────────────┘ └───────────┘ │ │ │
│ │ └────────────────────────────────┘ │ │
│ └────────────────────────────────────┘ │
│ │
│ 原生功能调用(摄像头、定位、传感器等) │
│ 双向通信桥梁 │
└──────────────────────────────────────────┘
- Android 项目中,通过
WebView
将 Vue 编译产物(HTML + JS + CSS)加载到移动端。 双向通信:
Vue → Android
- 在 Vue 代码里调用
window.Android.someMethod(...)
,Android 端通过@JavascriptInterface
注解的方法接收请求并执行原生功能。
- 在 Vue 代码里调用
Android → Vue
- Android 原生在异步操作完成(例如获取定位、拍照、扫描二维码)后,通过
webView.evaluateJavascript("window.onNativeCallback(...)", null)
或webView.loadUrl("javascript:window.onNativeCallback('...')")
,将结果回传给 Vue。Vue 在页面里绑定了window.onNativeCallback
函数来处理数据。
- Android 原生在异步操作完成(例如获取定位、拍照、扫描二维码)后,通过
3.2 双向通信原理
Vue → Android
- Vue 端直接访问
window.Android
对象下的方法; - Android 端在
WebView
中注入一个 Java 对象(例如JSBridge
),在该对象上定义若干@JavascriptInterface
的方法; - 当 Vue 端调用
window.Android.openCamera()
时,Android 会收到这次调用并执行相应原生逻辑。
- Vue 端直接访问
Android → Vue
Android 原生代码可调用:
webView.evaluateJavascript("window.onNativeCallback('" + jsonResult + "')", null);
或:
webView.loadUrl("javascript:window.onNativeCallback('" + jsonResult + "')");
- Vue 端在页面全局(例如
main.js
)注册了window.onNativeCallback = function (data) { /* … */ }
,当 Android 推送这段脚本时,Vue 端即可即时收到并处理。
Vue 端:项目初始化与基础封装
4.1 新建 Vue 3 项目
使用 Vite 创建 Vue 3 项目:
npm create vite@latest vue-android-bridge --template vue cd vue-android-bridge npm install
修改
vite.config.js
,确保打包后项目能放到 Android 端assets
目录下。一般无需做特殊配置,默认为:import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [vue()], build: { outDir: 'dist', assetsDir: 'assets', // 如果需要 Base 路径指定为相对路径: base: './' } });
在
package.json
中添加打包脚本(通常已经有):{ "scripts": { "dev": "vite", "build": "vite build" } }
运行并验证:
npm run dev # 浏览器中访问 http://localhost:3000,确认正常。
打包产物:
npm run build # 将会在项目根目录生成 dist/ 文件夹,包含 index.html、assets/...
4.2 创建与 Android 通信的封装模块
为了让在各个 Vue 组件中调用原生接口时更加方便,我们可以统一封装一个 jsbridge.js
文件,让所有调用都通过同一个接口调用原生方法,并处理回调。
在 src/utils/jsbridge.js
中:
// src/utils/jsbridge.js
/**
* 这里约定 Android 端注入的对象名为:Android
* Android 端会在 WebView 加载完成后通过 webView.addJavascriptInterface(new JSBridge(), "Android") 注入。
*
* JSBridge 方法示例:
* - openCamera(): 调用摄像头
* - getDeviceInfo(): 获取设备信息
* - startLocation(): 启动定位
* - stopLocation(): 停止定位
* - …
*/
/** 简单检测 Android 环境 */
function isAndroid() {
return typeof window.Android !== 'undefined';
}
/** 调用 Android 原生方法 */
function callNative(method, ...args) {
if (isAndroid() && typeof window.Android[method] === 'function') {
try {
// 转换参数为字符串,因为 Android 端方法一般接收 String 类型
const strArgs = args.map(arg => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)));
return window.Android[method](...strArgs);
} catch (e) {
console.error(`[JSBridge] 调用原生方法 ${method} 失败`, e);
}
} else {
console.warn(`[JSBridge] 当前环境不支持 Android 原生调用:${method}`);
}
}
/**
* 注册一个全局回调 Map,用于 Android 推送数据到 JS 时进行分发
* key: callbackId
* value: 具体的回调函数
*/
const callbackMap = new Map();
/** 生成唯一 Callback ID */
function genCallbackId() {
return 'cb_' + Date.now() + '_' + Math.floor(Math.random() * 10000);
}
/**
* Vue 端给 Android 调用并携带回调
* @param {String} method 原生方法名
* @param {any[]} args 参数列表
* @param {Function} callback 原生回调 JS 函数
*/
function callNativeWithCallback(method, args = [], callback) {
const callbackId = genCallbackId();
if (typeof callback === 'function') {
callbackMap.set(callbackId, callback);
}
// 将参数列表加上 callbackId 传给原生,约定原生回调时会携带 callbackId
callNative(method, ...args, callbackId);
}
/** 供 Android 调用:当原生通过 evaluateJavascript 调用 window.onNativeCallback 时,这里会执行分发 */
window.onNativeCallback = function (callbackId, jsonResult) {
try {
const result = JSON.parse(jsonResult);
const cb = callbackMap.get(callbackId);
if (cb) {
cb(result);
// 如果只需调用一次,调用完成后删除
callbackMap.delete(callbackId);
}
} catch (e) {
console.error('[JSBridge] 解析回调数据失败', e);
}
};
export default {
callNative,
callNativeWithCallback
};
isAndroid()
:用于检测当前环境是否注入了window.Android
。callNative(method, ...args)
:直接调用无回调的简单原生方法。callNativeWithCallback(method, args, callback)
:带回调的调用,会生成一个callbackId
并缓存回调函数;Android 端执行完原生逻辑后通过window.onNativeCallback(callbackId, JSON.stringify(data))
将结果回传给 JS。
4.3 在组件中调用原生接口示例
假设我们想在 Vue 组件里做如下操作:
- 点击按钮调用 Android 原生的
getDeviceInfo
方法,获取设备型号、系统版本等信息,并在页面中展示。 - 点击按钮调用 Android 原生的
openCamera
方法,拍照后把照片的文件路径传回 JS,然后在页面中显示。
示例组件:src/components/NativeDemo.vue
<template>
<div class="demo">
<h2>Android 原生功能示例</h2>
<div class="section">
<button @click="fetchDeviceInfo">获取设备信息</button>
<div v-if="deviceInfo">
<p><strong>设备信息:</strong></p>
<p>品牌:{{ deviceInfo.brand }}</p>
<p>型号:{{ deviceInfo.model }}</p>
<p>系统版本:{{ deviceInfo.osVersion }}</p>
</div>
</div>
<hr />
<div class="section">
<button @click="takePhoto">拍照并获取照片</button>
<div v-if="photoPath">
<p><strong>照片本地路径:</strong> {{ photoPath }}</p>
<img :src="photoUrl" alt="拍照结果" style="max-width: 300px; margin-top: 8px; border: 1px solid #ccc;" />
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import JSBridge from '@/utils/jsbridge';
const deviceInfo = ref(null);
const photoPath = ref('');
const photoUrl = ref(''); // 兼容 Android File URI
/** 获取设备信息 */
function fetchDeviceInfo() {
JSBridge.callNativeWithCallback('getDeviceInfo', [], (result) => {
// result 形如:{ brand: 'Xiaomi', model: 'Mi 11', osVersion: 'Android 12' }
deviceInfo.value = result;
});
}
/** 拍照 */
function takePhoto() {
JSBridge.callNativeWithCallback('openCamera', [], (result) => {
// result 形如:{ success: true, path: '/storage/emulated/0/DCIM/Camera/xxx.jpg' }
if (result.success) {
photoPath.value = result.path;
// Android 特殊:File:// URI
photoUrl.value = `file://${result.path}`;
} else {
alert('拍照失败:' + result.message);
}
});
}
</script>
<style scoped>
.demo {
padding: 16px;
}
.section {
margin-bottom: 24px;
}
button {
padding: 8px 16px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #66b1ff;
}
</style>
fetchDeviceInfo()
:调用getDeviceInfo
并注册回调。takePhoto()
:调用openCamera
并注册回调,返回的result.path
是 Android 端保存的照片路径,前端用file://
前缀展示。
小结
- Vue 端只关注调用方法名与回调,复用
jsbridge.js
做统一调用。- 回调机制约定:原生收到
callbackId
后,做完逻辑再调用window.onNativeCallback(callbackId, JSON.stringify(result))
。
Android 端:WebView 集成与原生接口暴露
5.1 新建 Android 项目并引入 WebView
创建项目
- 在 Android Studio 中,选择 “New Project” → “Empty Activity”,命名为
VueAndroidBridgeDemo
(包名:com.example.vueandroidbridge
)。 - 语言选择 Java 或 Kotlin,下面示例以 Java 为主(Kotlin 代码在注释中给出对应写法)。
- 最低 SDK 建议选择 Android 5.0(API 21)以上,确保大部分机型兼容。
- 在 Android Studio 中,选择 “New Project” → “Empty Activity”,命名为
布局文件
- 打开
res/layout/activity_main.xml
,用一个全屏的WebView
来承载 Vue 页面:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <WebView android:id="@+id/webview" android:layout_width="match_parent" android:layout_height="match_parent" /> </RelativeLayout>
- 打开
申请权限
在
AndroidManifest.xml
中,根据后续要调用的功能,加入所需权限。例如拍照与读写文件、定位:<!-- AndroidManifest.xml --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.vueandroidbridge"> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <application android:allowBackup="true" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.VueAndroidBridgeDemo"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
5.2 配置 WebView 与开启 JavaScript
在 MainActivity.java
中,我们需要初始化 WebView,加载本地的 index.html
或远程调试链接,并开启 JavaScript 支持、允许文件访问等。
// MainActivity.java
package com.example.vueandroidbridge;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebChromeClient;
import android.webkit.WebViewClient;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
public class MainActivity extends AppCompatActivity {
private WebView webView;
private static final int REQUEST_PERMISSIONS = 1001;
private String[] permissions = new String[]{
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
};
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 1. 动态申请权限(Android 6.0+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSIONS);
}
// 2. 初始化 WebView
webView = findViewById(R.id.webview);
WebSettings ws = webView.getSettings();
ws.setJavaScriptEnabled(true); // 启用 JavaScript
ws.setDomStorageEnabled(true); // 启用 DOM Storage
ws.setAllowFileAccess(true); // 允许文件访问
ws.setAllowContentAccess(true);
ws.setDatabaseEnabled(true);
// 如果调试阶段可开启 WebView 调试
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}
// 3. 解决部分手机点击文件上传不响应的问题
webView.setWebChromeClient(new WebChromeClient());
// 4. 阻止系统浏览器打开链接
webView.setWebViewClient(new WebViewClient());
// 5. 注入 JSBridge 接口对象到 WebView
webView.addJavascriptInterface(new JSBridge(this, webView), "Android");
// 6. 加载本地或远程 URL
// 开发阶段:先加载本地 HTTP 服务器
// webView.loadUrl("http://10.0.2.2:3000/"); // Android 模拟器访问本机 localhost:3000
// 生产阶段:将 dist/ 下的文件放到 assets/www 目录,并加载:
webView.loadUrl("file:///android_asset/www/index.html");
}
/** 权限申请结果回调(如需判断具体权限,可在此处理) */
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
if (requestCode == REQUEST_PERMISSIONS) {
// 这里简单忽略是否全部授权,实际可优化
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
关键配置
ws.setJavaScriptEnabled(true);
—— 启用 JavaScript。webView.addJavascriptInterface(new JSBridge(this, webView), "Android");
—— 将后续自定义的JSBridge
对象注入到 JS 全局的window.Android
。- 加载本地资源:将 Vue 打包产物拷贝到
app/src/main/assets/www/
中,然后loadUrl("file:///android_asset/www/index.html")
。
- 调试时 可以先运行本地 Vue 项目,使用
webView.loadUrl("http://10.0.2.2:3000/");
来实时预览修改效果。
5.3 通过 @JavascriptInterface
暴露原生方法
接下来,我们编写一个名为 JSBridge
的辅助类,用来处理 Vue 端调用的原生方法。示例以 Java 编写:(Kotlin 可参照注释转换)
// JSBridge.java
package com.example.vueandroidbridge;
import android.Manifest;
import android.app.Activity;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import org.json.JSONObject;
import java.io.File;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class JSBridge {
private Activity activity;
private WebView webView;
// 用于拍照
private static final int REQUEST_IMAGE_CAPTURE = 2001;
private Uri photoUri;
private String currentPhotoPath;
private String currentCallbackIdForPhoto;
// 用于定位
private LocationManager locationManager;
private String currentCallbackIdForLocation;
public JSBridge(Activity activity, WebView webView) {
this.activity = activity;
this.webView = webView;
// 初始化定位管理器
locationManager = (LocationManager) activity.getSystemService(Activity.LOCATION_SERVICE);
}
/**
* 获取设备信息
* 约定:Vue 端调用 getDeviceInfo(callbackId),这里直接构造 JSON 并回调给 JS
*/
@JavascriptInterface
public void getDeviceInfo(String callbackId) {
try {
JSONObject result = new JSONObject();
result.put("brand", Build.BRAND);
result.put("model", Build.MODEL);
result.put("osVersion", Build.VERSION.RELEASE);
// 回传给 JS
callbackToJs(callbackId, result.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 打开摄像头,拍照并保存到本地
* 约定:Vue 端调用 openCamera(callbackId)
*/
@JavascriptInterface
public void openCamera(String callbackId) {
// 保存当前 callbackId,拍照后再回调
currentCallbackIdForPhoto = callbackId;
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(activity.getPackageManager()) != null) {
// 创建临时文件
try {
File photoFile = createImageFile();
if (photoFile != null) {
// 7.0+ 需通过 FileProvider 获取 Uri
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
photoUri = FileProvider.getUriForFile(activity,
activity.getPackageName() + ".fileprovider", photoFile);
} else {
photoUri = Uri.fromFile(photoFile);
}
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
activity.startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
}
} catch (Exception ex) {
ex.printStackTrace();
// 拍照失败,立刻回调错误
sendPhotoResult(false, "创建文件失败:" + ex.getMessage());
}
} else {
sendPhotoResult(false, "没有相机应用");
}
}
/** 拍照后回调结果 */
private void sendPhotoResult(boolean success, String message) {
try {
JSONObject result = new JSONObject();
result.put("success", success);
if (success) {
result.put("path", currentPhotoPath);
} else {
result.put("message", message);
}
callbackToJs(currentCallbackIdForPhoto, result.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
/** 处理 onActivityResult,获取拍照结果 */
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_IMAGE_CAPTURE) {
if (resultCode == Activity.RESULT_OK) {
// 照片已保存到 currentPhotoPath
sendPhotoResult(true, "");
} else {
sendPhotoResult(false, "用户取消拍照");
}
}
}
/** 创建图片文件 */
private File createImageFile() throws Exception {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(
imageFileName,
".jpg",
storageDir
);
// 保存文件路径
currentPhotoPath = image.getAbsolutePath();
return image;
}
/**
* 启动定位
* 约定:Vue 调用 startLocation(callbackId)
* 定位信息实时通过回调推送
*/
@JavascriptInterface
public void startLocation(String callbackId) {
currentCallbackIdForLocation = callbackId;
// 检查权限
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
// 越界,需要在 Activity 中动态申请权限,这里仅简单提示
Toast.makeText(activity, "缺少定位权限", Toast.LENGTH_SHORT).show();
return;
}
// 注册监听
try {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
2000, 5, locationListener);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 停止定位
* Vue 调用 stopLocation()
*/
@JavascriptInterface
public void stopLocation() {
locationManager.removeUpdates(locationListener);
}
/** 定位监听 */
private final LocationListener locationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
try {
JSONObject result = new JSONObject();
result.put("latitude", location.getLatitude());
result.put("longitude", location.getLongitude());
result.put("accuracy", location.getAccuracy());
// 实时回调给 JS
callbackToJs(currentCallbackIdForLocation, result.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override public void onStatusChanged(String provider, int status, Bundle extras) {}
@Override public void onProviderEnabled(String provider) {}
@Override public void onProviderDisabled(String provider) {}
};
/**
* 核心:向 JS 端回调的方法
* JavaScript 接收: window.onNativeCallback(callbackId, jsonString)
*/
private void callbackToJs(String callbackId, String jsonString) {
final String script = "window.onNativeCallback('" + callbackId + "', '" + jsonString.replace("'", "\\'") + "')";
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webView.evaluateJavascript(script, null);
} else {
webView.loadUrl("javascript:" + script);
}
}
});
}
}
核心思路
- Vue 端调用
window.Android.openCamera(callbackId)
,Java 植入的JSBridge.openCamera(String callbackId)
收到。 - 在
openCamera
方法内启动相机 Intent,并将callbackId
缓存到currentCallbackIdForPhoto
。 - 拍照完成后,在
onActivityResult
中得到照片保存位置currentPhotoPath
,构造结果 JSON 并调用callbackToJs(callbackId, resultJson)
。 callbackToJs
方法底层通过webView.evaluateJavascript("window.onNativeCallback('cb_123', '{...}')")
将消息推送到 JS。- Vue 端全局注册了
window.onNativeCallback
,它会根据callbackId
从callbackMap
中取得对应的回调函数并执行。
- Vue 端调用
定位示例
- Vue 端调用
window.Android.startLocation(callbackId)
,Java 中开始注册LocationListener
并实时回调:每当有新定位时,就执行callbackToJs(callbackId, resultJson)
,Vue 一旦收到就可以更新界面。 - 如果要停止定位,可调用
window.Android.stopLocation()
,Java 中会移除监听。
- Vue 端调用
5.4 Android 调用 Vue 的方法(evaluateJavascript)
Java 端调用
evaluateJavascript
String script = "window.onNativeCallback('" + callbackId + "', '" + jsonString + "')"; webView.post(() -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { webView.evaluateJavascript(script, null); } else { webView.loadUrl("javascript:" + script); } });
- 注:需要在主线程调用
evaluateJavascript
,因此用webView.post(...)
确保在 UI 线程执行。 - 对于 Android 4.4 以下可用
webView.loadUrl("javascript:...")
兼容。
- 注:需要在主线程调用
Vue 端接收回调
在
jsbridge.js
中定义了全局函数:window.onNativeCallback = function (callbackId, jsonResult) { // 这里把 jsonResult 反序列化,并根据 callbackId 找到对应回调 };
- 只要 Android 端执行了上述 JS,就会触发 Vue 端的回调分发逻辑。
综合示例:获取设备信息与拍照功能
下面整合上文思路,做一个完整可运行的示例:
效果预览:
- 点击「获取设备信息」,在页面上显示品牌、型号、系统版本;
- 点击「拍照并获取照片」,调用摄像头拍摄,拍完后在页面上展示拍摄结果。
6.1 需求分析
Vue 端
- 页面有两个按钮:获取设备信息、拍照。
- 点击按钮时,通过封装的
JSBridge.callNativeWithCallback(...)
发起调用,并注册回调函数。 - 当设备信息回传后,页面更新对应
ref
;当拍照成功后,页面把得到的本地路径展示并渲染<img>
。
Android 端
- 在
JSBridge
中实现getDeviceInfo(callbackId)
与openCamera(callbackId)
两个方法。 getDeviceInfo
直接读取Build
信息并回传;openCamera
启动摄像头 Intent,保存到本地文件;- 在
onActivityResult
中获取结果并回调 Vue。
- 在
6.2 Android 端实现
确保
AndroidManifest.xml
已经声明摄像头和存储读写权限,以及配置FileProvider
:<!-- AndroidManifest.xml --> <application ...> <!-- FileProvider 配置,用于 7.0+ 的文件 Uri 权限 --> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> ... </application>
res/xml/file_paths.xml
:<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <!-- 将 /storage/emulated/0/Android/data/<package>/files/Pictures/ 映射给外部访问 --> <external-files-path name="my_images" path="Pictures/" /> </paths>
MainActivity.java
中添加onActivityResult
转发给JSBridge
:// MainActivity.java(补充部分) public class MainActivity extends AppCompatActivity { private JSBridge jsBridge; @Override protected void onCreate(Bundle savedInstanceState) { ... jsBridge = new JSBridge(this, webView); webView.addJavascriptInterface(jsBridge, "Android"); ... } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); // 转发给 JSBridge 处理拍照回调 jsBridge.onActivityResult(requestCode, resultCode, data); } }
JSBridge.java
代码如上文所示,重点是getDeviceInfo
与openCamera
,以及构造回调。// 见上文 JSBridge.java
6.3 Vue 端实现
项目结构
vue-android-bridge/ ├─ public/ | └─ index.html ├─ src/ | ├─ main.js | ├─ App.vue | ├─ components/ | | └─ NativeDemo.vue | └─ utils/ | └─ jsbridge.js ├─ package.json └─ vite.config.js
src/utils/jsbridge.js
(与前文一致):/** jsbridge.js */ function isAndroid() { return typeof window.Android !== 'undefined'; } function callNative(method, ...args) { if (isAndroid() && typeof window.Android[method] === 'function') { try { const strArgs = args.map(arg => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg))); return window.Android[method](...strArgs); } catch (e) { console.error(`[JSBridge] 调用原生方法 ${method} 失败`, e); } } else { console.warn(`[JSBridge] 环境不支持 Android 原生调用:${method}`); } } const callbackMap = new Map(); function genCallbackId() { return 'cb_' + Date.now() + '_' + Math.floor(Math.random() * 10000); } function callNativeWithCallback(method, args = [], callback) { const callbackId = genCallbackId(); if (typeof callback === 'function') { callbackMap.set(callbackId, callback); } callNative(method, ...args, callbackId); } window.onNativeCallback = function (callbackId, jsonResult) { try { const result = JSON.parse(jsonResult); const cb = callbackMap.get(callbackId); if (cb) { cb(result); callbackMap.delete(callbackId); } } catch (e) { console.error('[JSBridge] 解析回调数据失败', e); } }; export default { callNative, callNativeWithCallback };
src/components/NativeDemo.vue
(已在第 4.3 小节给出):<template> <div class="demo"> <h2>Android 原生功能示例</h2> <div class="section"> <button @click="fetchDeviceInfo">获取设备信息</button> <div v-if="deviceInfo"> <p><strong>设备信息:</strong></p> <p>品牌:{{ deviceInfo.brand }}</p> <p>型号:{{ deviceInfo.model }}</p> <p>系统版本:{{ deviceInfo.osVersion }}</p> </div> </div> <hr /> <div class="section"> <button @click="takePhoto">拍照并获取照片</button> <div v-if="photoPath"> <p><strong>照片路径:</strong> {{ photoPath }}</p> <img :src="photoUrl" alt="拍照结果" style="max-width: 300px; margin-top: 8px; border: 1px solid #ccc;" /> </div> </div> </div> </template> <script setup> import { ref } from 'vue'; import JSBridge from '@/utils/jsbridge'; const deviceInfo = ref(null); const photoPath = ref(''); const photoUrl = ref(''); function fetchDeviceInfo() { JSBridge.callNativeWithCallback('getDeviceInfo', [], (result) => { deviceInfo.value = result; }); } function takePhoto() { JSBridge.callNativeWithCallback('openCamera', [], (result) => { if (result.success) { photoPath.value = result.path; photoUrl.value = `file://${result.path}`; } else { alert('拍照失败:' + result.message); } }); } </script> <style scoped> .demo { padding: 16px; } .section { margin-bottom: 24px; } button { padding: 8px 16px; background: #409eff; color: white; border: none; border-radius: 4px; cursor: pointer; } button:hover { background: #66b1ff; } </style>
src/App.vue
:仅引用NativeDemo
以展示示例。<template> <div id="app"> <NativeDemo /> </div> </template> <script setup> import NativeDemo from './components/NativeDemo.vue'; </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; } </style>
src/main.js
:正常挂载。import { createApp } from 'vue'; import App from './App.vue'; const app = createApp(App); app.mount('#app');
打包并集成到 Android
npm run build
将生成的
dist/
目录整体复制到app/src/main/assets/www/
,保持文件结构不变:app/ └─ src/ └─ main/ └─ assets/ └─ www/ ├─ index.html ├─ assets/...
运行
- 在 Android Studio 中运行应用。应用启动后会加载
file:///android_asset/www/index.html
,Vue 页面显示。 - 点击「获取设备信息」应能看到品牌、型号、系统版本;点击「拍照并获取照片」应打开相机,拍照完毕后页面显示图片。
- 在 Android Studio 中运行应用。应用启动后会加载
6.4 数据流动图解
┌───────────────────────────────────────────────────────────┐
│ 用户点击“拍照” │
└───────────────────────────────────────────────────────────┘
↓ Vue 端
┌───────────────────────────────────────────────────────────┐
│ NativeDemo.vue → JSBridge.callNativeWithCallback("openCamera", [], cbId) │
│ → window.Android.openCamera(cbId) │
└───────────────────────────────────────────────────────────┘
↓ Android WebView
┌───────────────────────────────────────────────────────────┐
│ JSBridge.openCamera(cbId):构建拍照 Intent,并缓存 cbId │
│ 启动相机应用拍照,保存文件到本地路径 currentPhotoPath │
└───────────────────────────────────────────────────────────┘
↓ 拍照完成后 onActivityResult
┌───────────────────────────────────────────────────────────┐
│ JSBridge.onActivityResult → sendPhotoResult(true, path) │
│ → callbackToJs(cbId, JSON.stringify({ success:true, path })) │
│ → webView.evaluateJavascript("window.onNativeCallback(cbId,'{...}')") │
└───────────────────────────────────────────────────────────┘
↓ Vue 端
┌───────────────────────────────────────────────────────────┐
│ window.onNativeCallback(cbId, jsonResult) 被触发 │
│ → 在 jsbridge.js 中,找到 callbackMap.get(cbId),执行回调 │
│ → NativeDemo.vue 中注册的回调被调用, photoPath = result.path │
└───────────────────────────────────────────────────────────┘
↓ Vue 页面
┌───────────────────────────────────────────────────────────┐
│ <img :src="file:///.../xxx.jpg" /> 显示拍摄结果 │
└───────────────────────────────────────────────────────────┘
进阶示例:定位功能与实时回调
在综合示例基础上,再来看一个稍复杂一些的“实时定位”场景:
7.1 需求分析
- 在页面上点击「开始定位」,调用 Android 原生
startLocation
,并实时在 Vue 页面显示经纬度变化。 - 点击「停止定位」,停止原生端的定位监听。
- 定位权限需在 Android 端动态申请。
7.2 Android 端实现(Location + 权限)
确保权限
在
AndroidManifest.xml
中已声明:<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
- 在
MainActivity.java
中动态申请(前文已有申请列表,可复用)。
JSBridge.java 中定位相关方法
// JSBridge.java 中定位相关内容(见第 5.3 节) @JavascriptInterface public void startLocation(String callbackId) { currentCallbackIdForLocation = callbackId; if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { Toast.makeText(activity, "缺少定位权限", Toast.LENGTH_SHORT).show(); return; } try { locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 5, locationListener); } catch (Exception e) { e.printStackTrace(); } } @JavascriptInterface public void stopLocation() { locationManager.removeUpdates(locationListener); } private final LocationListener locationListener = new LocationListener() { @Override public void onLocationChanged(Location location) { try { JSONObject result = new JSONObject(); result.put("latitude", location.getLatitude()); result.put("longitude", location.getLongitude()); result.put("accuracy", location.getAccuracy()); callbackToJs(currentCallbackIdForLocation, result.toString()); } catch (Exception e) { e.printStackTrace(); } } // 其他回调空实现 @Override public void onStatusChanged(String provider, int status, Bundle extras) {} @Override public void onProviderEnabled(String provider) {} @Override public void onProviderDisabled(String provider) {} };
MainActivity.java 动态申请定位权限(可参考第 5.2 段中的权限申请)
- 若用户拒绝权限,需要在 JavaScript 层或页面上给出提示。
7.3 Vue 端实现与展示
新建组件:
src/components/LocationDemo.vue
<template> <div class="location-demo"> <h2>Android 原生定位示例</h2> <div class="controls"> <button @click="startLoc" :disabled="locating">开始定位</button> <button @click="stopLoc" :disabled="!locating">停止定位</button> </div> <div v-if="locating"> <p>实时定位中...</p> <p>经度:{{ longitude }}</p> <p>纬度:{{ latitude }}</p> <p>精度:{{ accuracy }} 米</p> </div> </div> </template> <script setup> import { ref } from 'vue'; import JSBridge from '@/utils/jsbridge'; const locating = ref(false); const latitude = ref(0); const longitude = ref(0); const accuracy = ref(0); // 开始定位 function startLoc() { locating.value = true; JSBridge.callNativeWithCallback('startLocation', [], (result) => { // result = { latitude: 39.9, longitude: 116.4, accuracy: 10.0 } latitude.value = result.latitude; longitude.value = result.longitude; accuracy.value = result.accuracy; }); } // 停止定位 function stopLoc() { locating.value = false; JSBridge.callNative('stopLocation'); } </script> <style scoped> .location-demo { padding: 16px; } .controls { margin-bottom: 16px; } button { padding: 8px 16px; background: #67c23a; color: white; border: none; border-radius: 4px; margin-right: 8px; cursor: pointer; } button:disabled { background: #a0a0a0; cursor: not-allowed; } button:hover:not(:disabled) { background: #85ce61; } </style>
在
App.vue
中引入并展示<template> <div id="app"> <NativeDemo /> <hr /> <LocationDemo /> </div> </template> <script setup> import NativeDemo from './components/NativeDemo.vue'; import LocationDemo from './components/LocationDemo.vue'; </script>
运行效果
- 点击「开始定位」,如果 Android 端已获得定位权限,就会触发
locationListener.onLocationChanged
,不断回调坐标到 JS,页面实时更新。 - 点击「停止定位」,停止原生层的
requestLocationUpdates
。
- 点击「开始定位」,如果 Android 端已获得定位权限,就会触发
7.4 通信流程图解
┌──────────────────────────┐
│ 用户点击“开始定位”按钮 │
└──────────────────────────┘
↓ Vue 端
┌────────────────────────────────────────┐
│ LocationDemo.vue → JSBridge.callNativeWithCallback( │
│ 'startLocation', [], cbId ) │
│ → window.Android.startLocation(cbId) │
└────────────────────────────────────────┘
↓ Android 端
┌────────────────────────────────────────┐
│ JSBridge.startLocation(cbId):检查权限 → 启动 │
│ locationManager.requestLocationUpdates(...) │
└────────────────────────────────────────┘
↓ 设备定位变化,触发 onLocationChanged
┌────────────────────────────────────────┐
│ onLocationChanged(Location loc) → 构造 JSON │
│ → callbackToJs(cbId, jsonString) → │
│ webView.evaluateJavascript("window.onNativeCallback(cbId,'{...}')") │
└────────────────────────────────────────┘
↓ Vue 端
┌────────────────────────────────────────┐
│ window.onNativeCallback(cbId, jsonResult) │
│ → callbackMap.get(cbId)( result ) │
│ → 更新 latitude、longitude、accuracy │
└────────────────────────────────────────┘
↓ 页面实时更新
┌──────────────────────────┐
│ 显示 最新 纬度/经度/精度 │
└──────────────────────────┘
↓ 用户点击“停止定位”
┌────────────────────────────────────────┐
│ LocationDemo.vue → JSBridge.callNative('stopLocation') │
│ → window.Android.stopLocation() │
│ → Android 端 locationManager.removeUpdates(...) │
└────────────────────────────────────────┘
常见问题与调试方法
WebView 不显示 JS 调用
- 确认
webView.getSettings().setJavaScriptEnabled(true)
已经设置。 - 确认注入对象的名称与 JS 侧调用一致:
addJavascriptInterface(jsBridge, "Android")
vswindow.Android.method()
。 - 如果页面打不开本地资源,检查
file:///android_asset/www/index.html
路径是否正确,并确认assets
目录下已经放置好打包文件。
- 确认
@JavascriptInterface
方法未被调用或报错@JavascriptInterface
只对 public 方法生效,需保证方法签名为public void 方法名(String 参数)
。- 如果方法签名与 Vue 端传递不一致(参数个数/类型不匹配),会导致 JS 调用无响应。一般将所有参数都声明为
String
,在方法内部再做JSON.parse(...)
或new JSONObject(...)
。 - 如果多参数情况,Vue 端需按顺序传入多个字符串,Android 方法签名必须与之对应。
evaluateJavascript
无回调或抛异常- 确保在主线程中执行
webView.evaluateJavascript(...)
,可使用runOnUiThread(...)
。 - 对于 Android 4.3 以下版本,只能使用
webView.loadUrl("javascript:...")
。 - 如果回调函数名称书写错误(与 Vue 端定义不一致),JS 侧不会执行。
- 确保在主线程中执行
图片路径无法显示
- Android 7.0+ 需要使用
FileProvider
来获取Uri
,并且在<img>
标签中以src="file://..."
的方式展示。 - 如果
<img>
不显示,检查文件是否真实存在、文件权限是否正确、以及路径是否加了file://
前缀。
- Android 7.0+ 需要使用
定位无回调或坐标不准确
- 确认 Android 端已动态申请并获得定位权限,否则
requestLocationUpdates
会直接抛异常或无回调。 - 如果使用模拟器,需在模拟器设置中开启 GPS 模拟位置或在 Android Studio 的模拟器 Extended Controls → Location 中手动推送经纬度。
- Android 10+ 对后台定位限制更严格,确保有
ACCESS_FINE_LOCATION
权限,以及必要时申请“后台定位”权限。
- 确认 Android 端已动态申请并获得定位权限,否则
跨页面或多 WebView 通信混乱
- 如果项目中有多个
Activity
或Fragment
都有 WebView,需为每个 WebView 单独注入不同的 JSBridge 对象,避免回调混淆。可在构造JSBridge
时传入不同的webView
实例。 - Vue 端可为不同功能定义不同的回调 ID 前缀,方便区分。
- 如果项目中有多个
总结与最佳实践
分离关注点
- Vue 端仅关注业务逻辑(调用
JSBridge
、更新ref
、渲染 UI),不直接操作 Android 原生 API。 - Android 端仅关注原生功能(拍照、定位、传感器等),通过
@JavascriptInterface
方法对外暴露接口。
- Vue 端仅关注业务逻辑(调用
统一回调管理
- 在
jsbridge.js
中维护一个全局的callbackMap
,通过callbackId
做双向映射,避免多次调用冲突。 - 所有回调数据约定采用 JSON 串,保证跨平台兼容。
- 在
权限与生命周期管理
- Android 端要及时申请并检查权限,必要时在
onRequestPermissionsResult
中判断权限是否被拒绝。 - 对于需要生命周期控制的操作(如定位监听、传感器监听),在
Activity.onDestroy()
中做好清理,避免内存泄漏。
- Android 端要及时申请并检查权限,必要时在
优化加载方式
- 开发阶段:可以直接
webView.loadUrl("http://10.0.2.2:3000/")
进行热更新开发。 - 生产阶段:将 Vue 构建产物拷贝到
assets/www/
,以file://android_asset/www/index.html
方式加载,减少网络依赖与加载延迟。
- 开发阶段:可以直接
调试建议
- 打开 WebView 调试:
WebView.setWebContentsDebuggingEnabled(true)
,这样可在 Chrome DevTools 中远程调试 WebView 页面的 JS。 - 在 Vue 端控制台加上适当的
console.log
,在 Chrome DevTools 中可实时查看 JS 调用与回调日志。 - 在 Android Studio Logcat 中过滤关键字(
JSBridge
、onNativeCallback
),查看原生日志和回调过程。
- 打开 WebView 调试:
安全与优化
addJavascriptInterface
会存在安全风险,一定要避免暴露敏感方法,且在 Android 4.2 以下可能存在反射漏洞。强烈建议应用最低 SDK 版本设为 17 及以上,并且注入的JSBridge
方法仅提供最小必要功能。- 对于大型项目,可考虑使用成熟的混合框架(如 Capacitor、Weex、Flutter + Dart 插件)来管理更复杂的原生与 JS 通信,但对于小型项目或自研需求,上述方案已经足够稳定。
通过本文的完整示例与原理剖析,你已经掌握了:
- 如何在 Android 原生项目中集成 Vue(Vite)构建产物;
- 如何在 WebView 中注入原生接口并实现双向通信;
- 如何在 Vue 组件中调用原生方法并在回调中更新 UI;
- 如何在 Android 端获取拍照结果、定位结果并实时推送给 JS。
希望这篇《Android 原生功能与 Vue 交互实现全攻略》能够帮助你在后续项目中快速搭建混合开发框架,轻松集成摄像头、定位、文件、传感器等各种原生能力,打造更加丰富的移动端应用体验!