当前位置: 首页 > news >正文

使用 React + Capacitor 构建 Android 混合应用外壳:集成扫码、定位与 NFC 功能实战

PS:本文介绍如何使用前端 React 开发 Android 外壳应用,以及如何在页面中调用 Android 硬件功能,以扫码、定位、NFC 为例。


React 构建 Android 外壳

一、创建项目

npmcreate vite@latest

二、添加依赖

1、核心插件 CapacitorJS

2. 安装插件

npmi @capacitor/corenpmi-D@capacitor/cli

3、初始化 Capacitor

npx cap init

运行后会创建capacitor.config.json文件,该文件记录了项目构建的输出目录(webDir),通常对应 Angular 项目的 www、React 项目的 build、Vue 项目的 public 等目录。

{"appId":"com.sggk.dongte.warehouse.app","appName":"XXXApp安装后显示的名称","webDir":"dist"}

三、创建 Android 项目

安装 Capacitor 核心运行时后,即可添加 Android 平台支持。

npmi @capacitor/android# 创建 Android 项目npx capaddandroid

运行后,会在项目根目录生成一个android文件夹,其中包含了转换后的 Android 项目代码。

同步代码

创建本地项目后,您可以通过运行以下命令将 Web 应用程序同步到本地项目。

npx capsync

[^]:npx cap sync会将您构建的 Web 包(默认位于 Capacitor 配置文件的webDir目录中)复制到您的本地项目,并安装本地项目的依赖项。如果修改了 Capacitor 配置文件,也会同步过去。也就是说,项目初始化后,后续任何配置或代码的更改,都只需运行npx cap sync命令来同步。

将同步命令添加到package.jsonscripts中,之后运行npm run build:cap命令即可构建并同步代码到 Android 项目。

"build:cap":"vite build && npx cap sync",

四、添加扫码、定位、NFC相关依赖

# 按需安装你使用的插件npminstall@capacitor/barcode-scanner# 扫码npminstall@capacitor/geolocation# 定位npminstall@capgo/capacitor-nfc# NFCnpminstall@capacitor/app# 首页或登录页面返回# 安装 sg-capacitor-bridge 插件Capacitor 插件的 iframe + postMessage 桥接库。用于 Android 包装应用通过 iframe 加载远程网页应用时,让网页应用可以调用父窗口的 Capacitor 原生插件。npmi git+https://gitee.com/cc_nbplus/android-ifream-calls-hardware.git

五、前端页面(Android 外壳)

1、capacitor.config.ts 配置

importtype{CapacitorConfig}from'@capacitor/cli';constconfig:CapacitorConfig={appId:'com.xx.xx.app',// 安卓包名appName:'xxx 程序安装后桌面显示的名称',webDir:'dist',plugins:{CapacitorHttp:{enabled:true// 跨域}},server:{androidScheme:'http'// 保证 HTTPS 请求成功}};exportdefaultconfig;

2、路由 routers

import { createHashRouter, Navigate } from "react-router"; import Home from "../pages/Home"; import BackButtonGuard from "../components/BackButtonGuard"; const router = createHashRouter([ { element: <BackButtonGuard />, // 用于首页或登录页再次返回退出程序 children: [ {"path": "/home", element: <Home/>}, {"path": "/", element: <Navigate to={"/home"}/>} ] } ]) export default router;

3. App.tsx

import {RouterProvider} from "react-router"; import routers from "@/routers"; function App() { return <RouterProvider router={routers} /> } export default App

4.Home.tsx

import { useEffect, useRef } from 'react' import { createBridgeHost, builtins } from 'sg-capacitor-bridge' import { CapacitorBarcodeScanner } from '@capacitor/barcode-scanner' const REMOTE_URL = 'http://xxxx' // 服务器上的应用程序网页地址 export default function Home() { const iframeRef = useRef<HTMLIFrameElement>(null) useEffect(() => { const host = createBridgeHost({ getIframe: () => iframeRef.current, }) // 注册扫码插件 host.use(builtins.barcodeScanner.setup(CapacitorBarcodeScanner)) // 需要什么插件,直接注册 // host.use(builtins.xxx.setup(xxx)) return () => host.destroy() }, []) return ( <iframe ref={iframeRef} src={REMOTE_URL} style={{ position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', border: 'none', margin: 0, padding: 0, }} /> ) }

5. 首页或登录页再次返回退出程序

hooks/useBackButtonHandler.ts

import{useEffect,useRef}from"react";import{App,typeBackButtonListenerEvent}from"@capacitor/app";import{useLocation,useNavigate}from"react-router";/** * 用于处理 Capacitor 应用中硬件返回按钮的 React Hook。 * 此优化版本仅注册一次监听器,并使用 Capacitor 内置的 `canGoBack` 状态。 * @param exitPaths 返回按钮应触发应用退出确认的路径数组。 * 根据路由配置,此数组应包含 '/login' 和 '/home'。 */exportconstuseBackButtonHandler=(exitPaths=["/login","/home"])=>{constlocation=useLocation();constnavigate=useNavigate();// 使用 useRef 存储最新的 location 和 navigate,以避免在 useEffect 依赖中包含它们,// 从而防止监听器在每次路由变化时都被重新注册。constlastState=useRef({location,navigate,exitPaths,});// 每次渲染时都更新 ref 中的最新状态useEffect(()=>{lastState.current={location,navigate,exitPaths};});// 这个 useEffect 只在组件挂载时运行一次,负责注册和清理监听器useEffect(()=>{// 定义核心处理逻辑consthandleBackButton=async(event:BackButtonListenerEvent)=>{// 从 ref 中获取最新的状态,确保逻辑总是使用当前的数据const{location:currentLocation,navigate:currentNavigate,exitPaths:currentExitPaths,}=lastState.current;constisExitPath=currentExitPaths.includes(currentLocation.pathname);// 场景合并:// 1. 如果当前页面是指定的退出页 (isExitPath)// 2. 或者,如果 Capacitor 确认已经没有可回退的 WebView 历史 (!event.canGoBack)// 这两种情况下,都应该提示用户退出。if(isExitPath||!event.canGoBack){// 使用 window.confirm 是一个简单的方式,在实际项目中你可能想用一个自定义的UI组件if(window.confirm("确定要退出应用吗?")){awaitApp.exitApp();}}else{// 其他所有情况,执行标准的返回操作currentNavigate(-1);}};// 注册监听器。App.addListener 返回一个 Promise,解析后得到监听器实例constlistenerPromise=App.addListener("backButton",handleBackButton);// 组件卸载时,确保移除监听器return()=>{listenerPromise.then((listener)=>listener.remove());};},[]);// 空依赖数组 [] 保证这个 effect 只运行一次};

components/BackButtonGuard.tsx

import { Outlet } from 'react-router' import { useBackButtonHandler } from '@/hooks/useBackButtonHandler.ts' export default function BackButtonGuard() { useBackButtonHandler(['/login', '/home']) return <Outlet /> }

package.json 参考

"scripts":{"dev":"vite","build":"tsc -b && vite build","build:cap":"vite build && npx cap sync","lint":"eslint .","preview":"vite preview"},"dependencies":{"@capacitor/android":"^8.4.1","@capacitor/app":"^8.1.0","@capacitor/barcode-scanner":"^3.0.2","@capacitor/core":"^8.4.1","@capacitor/geolocation":"^8.2.0","@capgo/capacitor-nfc":"^8.1.5","react":"^19.1.1","react-dom":"^19.1.1","react-router":"^8.0.1","react-router-dom":"^7.18.0","sg-capacitor-bridge":"git+http:xxxx.git"},"devDependencies":{"@capacitor/cli":"^8.4.1","@eslint/js":"^9.36.0","@types/react":"^19.1.13","@types/react-dom":"^19.1.9","@vitejs/plugin-react":"^5.0.3","eslint":"^9.36.0","eslint-plugin-react-hooks":"^5.2.0","eslint-plugin-react-refresh":"^0.4.20","globals":"^16.4.0","typescript":"~5.8.3","typescript-eslint":"^8.44.0","vite":"^7.1.7"}

六、Android 外壳开发结束,打包

使用 Android Studio 开发工具的 Gradle 打包

遇到问题

不能访问 http 资源

Android 项目默认只能访问 https 资源,解决方案可参考解决 Android 28 不能请求 HTTP 接口的问题 #5

本项目的解决方案如下:

  1. 新建android/app/src/main/res/xml/network_security_config.xml文件
<?xml version="1.0" encoding="utf-8"?><network-security-config><base-configcleartextTrafficPermitted="true"/></network-security-config>
  1. app/src/main/AndroidManifest.xml中引入
<application...android:networkSecurityConfig="@xml/network_security_config"...>
某些 Android 设备无法直接调用相机,且未获得相应权限

解决方法:在 AndroidManifest.xml 中添加以下信息

<uses-permissionandroid:name="android.permission.CAMERA"/><uses-permissionandroid:name="android.permission.READ_MEDIA_IMAGES"/><uses-featureandroid:name="android.hardware.camera"android:required="false"/>

AndroidManifest.xml 参考示例

<?xml version="1.0" encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android"><applicationandroid:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:networkSecurityConfig="@xml/network_security_config"android:supportsRtl="true"android:theme="@style/AppTheme"><activityandroid:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"android:name=".MainActivity"android:label="@string/title_activity_main"android:theme="@style/AppTheme.NoActionBarLaunch"android:launchMode="singleTask"android:exported="true"><intent-filter><actionandroid:name="android.intent.action.MAIN"/><categoryandroid:name="android.intent.category.LAUNCHER"/></intent-filter></activity><providerandroid:name="androidx.core.content.FileProvider"android:authorities="${applicationId}.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_paths"></meta-data></provider></application><!-- Permissions --><uses-permissionandroid:name="android.permission.INTERNET"/><uses-permissionandroid:name="android.permission.CAMERA"/><uses-permissionandroid:name="android.permission.READ_MEDIA_IMAGES"/><uses-featureandroid:name="android.hardware.camera"android:required="false"/></manifest>

综上所述,使用外壳(容器)的好处在于:打包为 APK 后,用户仅需安装一次。后续更新时,除非更换应用页面的映射地址,否则用户无需重新安装应用,只需退出并重新进入程序即可。由于未获得苹果的许可,本文仅以Android平台为例进行说明。

http://www.jsqmd.com/news/1103205/

相关文章:

  • Applite:终极Mac软件管理工具完整指南,告别复杂命令行
  • 5分钟搞定Mac Boot Camp驱动部署:Brigadier终极完整指南
  • 【DeepSeek vs ChatGPT终极对决】:20年AI架构师实测12项核心指标,谁才是中国企业级落地首选?
  • 月薪还不到五千的苦逼牛马们,花大几千考PMP,是“人傻钱多”还是“人间清醒”?
  • 保险纠纷典型案例入选国家级报告成果说明
  • 导师喜欢什么样的MBA论文选题?3个标准+10个案例
  • VM虚拟机鼠标键盘没反应求助
  • 从单调到个性:3款蔚蓝档案鼠标主题如何彻底改变你的Windows体验
  • MC6470与TM4C123GH6PMI的运动控制系统设计
  • 大模型落地实战避坑手册(ChatGPT与Gemini选型决策树):从PPO训练兼容性、RAG延迟、多模态支持到GDPR/《生成式AI服务管理暂行办法》适配性全解析
  • 怎样高效构建网盘直链解析服务:NFD云解析实战指南
  • MIC1557与PIC24FJ64GB004的高精度定时方案解析
  • 苹果17视频有美颜功能吗? 苹果17微信美颜设置方法
  • 终极开源工具:跨平台视频格式转换完整解决方案
  • yansongda/pay 支付SDK证书配置全解析:支付宝微信双平台安全接入实战
  • 网盘下载革命:LinkSwift直链下载助手全方位使用指南
  • 遗传算法进阶实战:破解早熟、调参玄学与收敛诊断
  • Applite终极指南:最简单直观的Mac软件管理神器
  • 如何永久保存QQ空间回忆?QZoneExport一键备份解决方案
  • 告别杂乱桌面:用Python+AI自动识别并分类你的所有文件
  • ComfyUI Mixlab Nodes终极指南:如何快速构建AI创意应用
  • 多网盘直链解析引擎架构解析与技术实现指南
  • Cosmos-Reason and Cosmos-Predict2.5 and Cosmos-Transfer2.5
  • ChatGPT vs 文心一言:RAG架构下知识召回准确率暴跌31%的元凶竟是它!(附BERT+Milvus+Prompt Engineering三重优化方案)
  • 【JAVA毕设源码分享】基于springboot的敬老院管理系统的设计与实现(程序+文档+代码讲解+一条龙定制)
  • 做竞品分析用特易还是外贸公社?
  • 撮合引擎 OrderBook 的 100ns 之路:无锁 RingBuffer + 伪共享消除,Go 1.22 下单 op 11ns
  • 模板驱动型文档自动化:重构内容生产流水线
  • DeepSeek V4:用工程契约重塑开源模型发布节奏
  • 5分钟掌握QRazyBox:终极免费二维码修复工具完整指南