Android原生功能与Vue交互实现全攻略‌

Android 原生功能与 Vue 交互实现全攻略


目录

  1. 前言
  2. 技术选型与环境准备

    • 2.1 技术选型
    • 2.2 环境准备
  3. 整体架构与通信原理

    • 3.1 高层架构图解
    • 3.2 双向通信原理
  4. Vue 端:项目初始化与基础封装

    • 4.1 新建 Vue 3 项目
    • 4.2 创建与 Android 通信的封装模块
    • 4.3 在组件中调用原生接口示例
  5. Android 端:WebView 集成与原生接口暴露

    • 5.1 新建 Android 项目并引入 WebView
    • 5.2 配置 WebView 与开启 JavaScript
    • 5.3 通过 @JavascriptInterface 暴露原生方法
    • 5.4 Android 调用 Vue 的方法(evaluateJavascript)
  6. 综合示例:获取设备信息与拍照功能

    • 6.1 需求分析
    • 6.2 Android 端实现
    • 6.3 Vue 端实现
    • 6.4 数据流动图解
  7. 进阶示例:定位功能与实时回调

    • 7.1 需求分析
    • 7.2 Android 端实现(Location + 权限)
    • 7.3 Vue 端实现与展示
    • 7.4 通信流程图解
  8. 常见问题与调试方法
  9. 总结与最佳实践

前言

随着前端框架的发展,使用 Vue 构建移动端界面已经越来越普及。然而,一旦需要调用 Android 原生功能(如摄像头、定位、传感器、推送通知等),就必须在 Web(Vue)与 Android 之间建立一条“桥梁”,通过双向通信才可实现二者无缝交互。本文将从零开始,手把手讲解如何在 Android 原生项目中嵌入 Vue 应用, 并实现 Vue ↔ 原生 的双向通信。无论你是初学者还是有一定经验的同学,都能通过本文对应的「完整示例」迅速掌握关键点。


技术选型与环境准备

2.1 技术选型

  • 前端框架:Vue 3 + Vite
  • 后端/中间层:Android 原生
  • 通信方式

    1. Vue → Android:调用 window.Android.xxx(),Android 端通过 @JavascriptInterface 注解的方法接收。
    2. Android → Vue:使用 webView.evaluateJavascript("window.onNativeCallback(...)")webView.loadUrl("javascript:...") 等方式触发 Vue 中定义的回调函数。
  • 构建工具

    • Vue:Vite(极简、极速热更新)
    • Android:Android Studio(建议 2022+ 版本,Gradle plugin 7.x)

2.2 环境准备

  1. Android Studio

    • Android Studio Arctic Fox 或更高版本
    • 配置好 Java 1.8+ JDK
  2. Node.js + NPM/Yarn

    • Node.js 14+
    • 全局安装 pnpm/npm/yarn 中任意一种包管理工具
  3. Vue CLI (可选)

    • 若想使用 Vue CLI 创建,可全局安装 @vue/cli,不过本文直接采用 Vite 初始化。
  4. 真机或模拟器

    • Android 模拟器(API 21+ 即可)或 真机调试
  5. 网络环境

    • 建议将 Vue 构建产物放到 Android 项目中的 assets 目录做本地加载,也可通过远程服务器来加载(开发阶段推荐本地)。

整体架构与通信原理

3.1 高层架构图解

┌──────────────────────────────────────────┐
│             Android 原生项目              │
│  ┌────────────────────────────────────┐  │
│  │            Android Activity        │  │
│  │ ┌────────────────────────────────┐ │  │
│  │ │         WebView (容器)           │ │  │
│  │ │ ┌───────────────┐  ┌───────────┐ │ │  │
│  │ │ │  index.html   │  │  JS  脚本 │ │ │  │
│  │ │ │ (Vue App)     │  │           │ │ │  │
│  │ │ └───────────────┘  └───────────┘ │ │  │
│  │ └────────────────────────────────┘ │  │
│  └────────────────────────────────────┘  │
│                                          │
│    原生功能调用(摄像头、定位、传感器等)    │
│             双向通信桥梁                    │
└──────────────────────────────────────────┘
  • Android 项目中,通过 WebView 将 Vue 编译产物(HTML + JS + CSS)加载到移动端。
  • 双向通信

    1. Vue → Android

      • 在 Vue 代码里调用 window.Android.someMethod(...),Android 端通过 @JavascriptInterface 注解的方法接收请求并执行原生功能。
    2. Android → Vue

      • Android 原生在异步操作完成(例如获取定位、拍照、扫描二维码)后,通过 webView.evaluateJavascript("window.onNativeCallback(...)", null)webView.loadUrl("javascript:window.onNativeCallback('...')"),将结果回传给 Vue。Vue 在页面里绑定了 window.onNativeCallback 函数来处理数据。

3.2 双向通信原理

  1. Vue → Android

    • Vue 端直接访问 window.Android 对象下的方法;
    • Android 端在 WebView 中注入一个 Java 对象(例如 JSBridge),在该对象上定义若干 @JavascriptInterface 的方法;
    • 当 Vue 端调用 window.Android.openCamera() 时,Android 会收到这次调用并执行相应原生逻辑。
  2. 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 项目

  1. 使用 Vite 创建 Vue 3 项目:

    npm create vite@latest vue-android-bridge --template vue
    cd vue-android-bridge
    npm install
  2. 修改 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: './'
      }
    });
  3. package.json 中添加打包脚本(通常已经有):

    {
      "scripts": {
        "dev": "vite",
        "build": "vite build"
      }
    }
  4. 运行并验证:

    npm run dev
    # 浏览器中访问 http://localhost:3000,确认正常。
  5. 打包产物:

    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 组件里做如下操作:

  1. 点击按钮调用 Android 原生的 getDeviceInfo 方法,获取设备型号、系统版本等信息,并在页面中展示。
  2. 点击按钮调用 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

  1. 创建项目

    • 在 Android Studio 中,选择 “New Project” → “Empty Activity”,命名为 VueAndroidBridgeDemo(包名:com.example.vueandroidbridge)。
    • 语言选择 JavaKotlin,下面示例以 Java 为主(Kotlin 代码在注释中给出对应写法)。
    • 最低 SDK 建议选择 Android 5.0(API 21)以上,确保大部分机型兼容。
  2. 布局文件

    • 打开 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>
  3. 申请权限

    • 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);
        }
    }
}
  • 关键配置

    1. ws.setJavaScriptEnabled(true); —— 启用 JavaScript。
    2. webView.addJavascriptInterface(new JSBridge(this, webView), "Android"); —— 将后续自定义的 JSBridge 对象注入到 JS 全局的 window.Android
    3. 加载本地资源:将 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);
                }
            }
        });
    }
}
  • 核心思路

    1. Vue 端调用 window.Android.openCamera(callbackId),Java 植入的 JSBridge.openCamera(String callbackId) 收到。
    2. openCamera 方法内启动相机 Intent,并将 callbackId 缓存到 currentCallbackIdForPhoto
    3. 拍照完成后,在 onActivityResult 中得到照片保存位置 currentPhotoPath,构造结果 JSON 并调用 callbackToJs(callbackId, resultJson)
    4. callbackToJs 方法底层通过 webView.evaluateJavascript("window.onNativeCallback('cb_123', '{...}')") 将消息推送到 JS。
    5. Vue 端全局注册了 window.onNativeCallback,它会根据 callbackIdcallbackMap 中取得对应的回调函数并执行。
  • 定位示例

    • Vue 端调用 window.Android.startLocation(callbackId),Java 中开始注册 LocationListener 并实时回调:每当有新定位时,就执行 callbackToJs(callbackId, resultJson),Vue 一旦收到就可以更新界面。
    • 如果要停止定位,可调用 window.Android.stopLocation(),Java 中会移除监听。

5.4 Android 调用 Vue 的方法(evaluateJavascript)

  1. 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:...") 兼容。
  2. Vue 端接收回调

    • jsbridge.js 中定义了全局函数:

      window.onNativeCallback = function (callbackId, jsonResult) {
        // 这里把 jsonResult 反序列化,并根据 callbackId 找到对应回调
      };
    • 只要 Android 端执行了上述 JS,就会触发 Vue 端的回调分发逻辑。

综合示例:获取设备信息与拍照功能

下面整合上文思路,做一个完整可运行的示例:

效果预览

  • 点击「获取设备信息」,在页面上显示品牌、型号、系统版本;
  • 点击「拍照并获取照片」,调用摄像头拍摄,拍完后在页面上展示拍摄结果。

6.1 需求分析

  • Vue 端

    1. 页面有两个按钮:获取设备信息、拍照。
    2. 点击按钮时,通过封装的 JSBridge.callNativeWithCallback(...) 发起调用,并注册回调函数。
    3. 当设备信息回传后,页面更新对应 ref;当拍照成功后,页面把得到的本地路径展示并渲染 <img>
  • Android 端

    1. JSBridge 中实现 getDeviceInfo(callbackId)openCamera(callbackId) 两个方法。
    2. getDeviceInfo 直接读取 Build 信息并回传;
    3. openCamera 启动摄像头 Intent,保存到本地文件;
    4. onActivityResult 中获取结果并回调 Vue。

6.2 Android 端实现

  1. 确保 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>
  2. 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);
        }
    }
  3. JSBridge.java 代码如上文所示,重点是 getDeviceInfoopenCamera,以及构造回调。

    // 见上文 JSBridge.java

6.3 Vue 端实现

  1. 项目结构

    vue-android-bridge/
    ├─ public/
    |    └─ index.html
    ├─ src/
    |    ├─ main.js
    |    ├─ App.vue
    |    ├─ components/
    |    |    └─ NativeDemo.vue
    |    └─ utils/
    |         └─ jsbridge.js
    ├─ package.json
    └─ vite.config.js
  2. 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
    };
  3. 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>
  4. 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>
  5. src/main.js:正常挂载。

    import { createApp } from 'vue';
    import App from './App.vue';
    
    const app = createApp(App);
    app.mount('#app');
  6. 打包并集成到 Android

    npm run build
    • 将生成的 dist/ 目录整体复制到 app/src/main/assets/www/,保持文件结构不变:

      app/
      └─ src/
         └─ main/
            └─ assets/
               └─ www/
                  ├─ index.html
                  ├─ assets/...
  7. 运行

    • 在 Android Studio 中运行应用。应用启动后会加载 file:///android_asset/www/index.html,Vue 页面显示。
    • 点击「获取设备信息」应能看到品牌、型号、系统版本;点击「拍照并获取照片」应打开相机,拍照完毕后页面显示图片。

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 + 权限)

  1. 确保权限

    • AndroidManifest.xml 中已声明:

      <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
      <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    • MainActivity.java 中动态申请(前文已有申请列表,可复用)。
  2. 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) {}
    };
  3. MainActivity.java 动态申请定位权限(可参考第 5.2 段中的权限申请)

    • 若用户拒绝权限,需要在 JavaScript 层或页面上给出提示。

7.3 Vue 端实现与展示

  1. 新建组件: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>
  2. 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>
  3. 运行效果

    • 点击「开始定位」,如果 Android 端已获得定位权限,就会触发 locationListener.onLocationChanged,不断回调坐标到 JS,页面实时更新。
    • 点击「停止定位」,停止原生层的 requestLocationUpdates

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(...)           │
└────────────────────────────────────────┘

常见问题与调试方法

  1. WebView 不显示 JS 调用

    • 确认 webView.getSettings().setJavaScriptEnabled(true) 已经设置。
    • 确认注入对象的名称与 JS 侧调用一致:addJavascriptInterface(jsBridge, "Android") vs window.Android.method()
    • 如果页面打不开本地资源,检查 file:///android_asset/www/index.html 路径是否正确,并确认 assets 目录下已经放置好打包文件。
  2. @JavascriptInterface 方法未被调用或报错

    • @JavascriptInterface 只对 public 方法生效,需保证方法签名为 public void 方法名(String 参数)
    • 如果方法签名与 Vue 端传递不一致(参数个数/类型不匹配),会导致 JS 调用无响应。一般将所有参数都声明为 String,在方法内部再做 JSON.parse(...)new JSONObject(...)
    • 如果多参数情况,Vue 端需按顺序传入多个字符串,Android 方法签名必须与之对应。
  3. evaluateJavascript 无回调或抛异常

    • 确保在主线程中执行 webView.evaluateJavascript(...),可使用 runOnUiThread(...)
    • 对于 Android 4.3 以下版本,只能使用 webView.loadUrl("javascript:...")
    • 如果回调函数名称书写错误(与 Vue 端定义不一致),JS 侧不会执行。
  4. 图片路径无法显示

    • Android 7.0+ 需要使用 FileProvider 来获取 Uri,并且在 <img> 标签中以 src="file://..." 的方式展示。
    • 如果 <img> 不显示,检查文件是否真实存在、文件权限是否正确、以及路径是否加了 file:// 前缀。
  5. 定位无回调或坐标不准确

    • 确认 Android 端已动态申请并获得定位权限,否则 requestLocationUpdates 会直接抛异常或无回调。
    • 如果使用模拟器,需在模拟器设置中开启 GPS 模拟位置或在 Android Studio 的模拟器 Extended Controls → Location 中手动推送经纬度。
    • Android 10+ 对后台定位限制更严格,确保有 ACCESS_FINE_LOCATION 权限,以及必要时申请“后台定位”权限。
  6. 跨页面或多 WebView 通信混乱

    • 如果项目中有多个 ActivityFragment 都有 WebView,需为每个 WebView 单独注入不同的 JSBridge 对象,避免回调混淆。可在构造 JSBridge 时传入不同的 webView 实例。
    • Vue 端可为不同功能定义不同的回调 ID 前缀,方便区分。

总结与最佳实践

  1. 分离关注点

    • Vue 端仅关注业务逻辑(调用 JSBridge、更新 ref、渲染 UI),不直接操作 Android 原生 API。
    • Android 端仅关注原生功能(拍照、定位、传感器等),通过 @JavascriptInterface 方法对外暴露接口。
  2. 统一回调管理

    • jsbridge.js 中维护一个全局的 callbackMap,通过 callbackId 做双向映射,避免多次调用冲突。
    • 所有回调数据约定采用 JSON 串,保证跨平台兼容。
  3. 权限与生命周期管理

    • Android 端要及时申请并检查权限,必要时在 onRequestPermissionsResult 中判断权限是否被拒绝。
    • 对于需要生命周期控制的操作(如定位监听、传感器监听),在 Activity.onDestroy() 中做好清理,避免内存泄漏。
  4. 优化加载方式

    • 开发阶段:可以直接 webView.loadUrl("http://10.0.2.2:3000/") 进行热更新开发。
    • 生产阶段:将 Vue 构建产物拷贝到 assets/www/,以 file://android_asset/www/index.html 方式加载,减少网络依赖与加载延迟。
  5. 调试建议

    • 打开 WebView 调试:WebView.setWebContentsDebuggingEnabled(true),这样可在 Chrome DevTools 中远程调试 WebView 页面的 JS。
    • 在 Vue 端控制台加上适当的 console.log,在 Chrome DevTools 中可实时查看 JS 调用与回调日志。
    • 在 Android Studio Logcat 中过滤关键字(JSBridgeonNativeCallback),查看原生日志和回调过程。
  6. 安全与优化

    • addJavascriptInterface 会存在安全风险,一定要避免暴露敏感方法,且在 Android 4.2 以下可能存在反射漏洞。强烈建议应用最低 SDK 版本设为 17 及以上,并且注入的 JSBridge 方法仅提供最小必要功能。
    • 对于大型项目,可考虑使用成熟的混合框架(如 Capacitor、Weex、Flutter + Dart 插件)来管理更复杂的原生与 JS 通信,但对于小型项目或自研需求,上述方案已经足够稳定。

通过本文的完整示例原理剖析,你已经掌握了:

  • 如何在 Android 原生项目中集成 Vue(Vite)构建产物;
  • 如何在 WebView 中注入原生接口并实现双向通信;
  • 如何在 Vue 组件中调用原生方法并在回调中更新 UI;
  • 如何在 Android 端获取拍照结果、定位结果并实时推送给 JS。

希望这篇《Android 原生功能与 Vue 交互实现全攻略》能够帮助你在后续项目中快速搭建混合开发框架,轻松集成摄像头、定位、文件、传感器等各种原生能力,打造更加丰富的移动端应用体验!

最后修改于:2025年05月31日 12:22

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日