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

使用 React + Capacitor 构建 Android 混合应用外壳:集成扫码、定位与 NFC 功能实战
PS本文介绍如何使用前端 React 开发 Android 外壳应用以及如何在页面中调用 Android 硬件功能以扫码、定位、NFC 为例。React 构建 Android 外壳一、创建项目npmcreate vitelatest二、添加依赖1、核心插件 CapacitorJS2. 安装插件npmi capacitor/corenpmi-Dcapacitor/cli3、初始化 Capacitornpx 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.json的scripts中之后运行npm run build:cap命令即可构建并同步代码到 Android 项目。build:cap:vite build npx cap sync,四、添加扫码、定位、NFC相关依赖# 按需安装你使用的插件npminstallcapacitor/barcode-scanner# 扫码npminstallcapacitor/geolocation# 定位npminstallcapgo/capacitor-nfc# NFCnpminstallcapacitor/app# 首页或登录页面返回# 安装 sg-capacitor-bridge 插件Capacitor 插件的 iframe postMessage 桥接库。用于 Android 包装应用通过 iframe 加载远程网页应用时让网页应用可以调用父窗口的 Capacitor 原生插件。npmi githttps://gitee.com/cc_nbplus/android-ifream-calls-hardware.git五、前端页面Android 外壳1、capacitor.config.ts 配置importtype{CapacitorConfig}fromcapacitor/cli;constconfig:CapacitorConfig{appId:com.xx.xx.app,// 安卓包名appName:xxx 程序安装后桌面显示的名称,webDir:dist,plugins:{CapacitorHttp:{enabled:true// 跨域}},server:{androidScheme:http// 保证 HTTPS 请求成功}};exportdefaultconfig;2、路由 routersimport { 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.tsximport {RouterProvider} from react-router; import routers from /routers; function App() { return RouterProvider router{routers} / } export default App4.Home.tsximport { 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 useRefHTMLIFrameElement(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.tsimport{useEffect,useRef}fromreact;import{App,typeBackButtonListenerEvent}fromcapacitor/app;import{useLocation,useNavigate}fromreact-router;/** * 用于处理 Capacitor 应用中硬件返回按钮的 React Hook。 * 此优化版本仅注册一次监听器并使用 Capacitor 内置的 canGoBack 状态。 * param exitPaths 返回按钮应触发应用退出确认的路径数组。 * 根据路由配置此数组应包含 /login 和 /home。 */exportconstuseBackButtonHandler(exitPaths[/login,/home]){constlocationuseLocation();constnavigateuseNavigate();// 使用 useRef 存储最新的 location 和 navigate以避免在 useEffect 依赖中包含它们// 从而防止监听器在每次路由变化时都被重新注册。constlastStateuseRef({location,navigate,exitPaths,});// 每次渲染时都更新 ref 中的最新状态useEffect((){lastState.current{location,navigate,exitPaths};});// 这个 useEffect 只在组件挂载时运行一次负责注册和清理监听器useEffect((){// 定义核心处理逻辑consthandleBackButtonasync(event:BackButtonListenerEvent){// 从 ref 中获取最新的状态确保逻辑总是使用当前的数据const{location:currentLocation,navigate:currentNavigate,exitPaths:currentExitPaths,}lastState.current;constisExitPathcurrentExitPaths.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解析后得到监听器实例constlistenerPromiseApp.addListener(backButton,handleBackButton);// 组件卸载时确保移除监听器return(){listenerPromise.then((listener)listener.remove());};},[]);// 空依赖数组 [] 保证这个 effect 只运行一次};components/BackButtonGuard.tsximport { 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:githttp: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本项目的解决方案如下新建android/app/src/main/res/xml/network_security_config.xml文件?xml version1.0 encodingutf-8?network-security-configbase-configcleartextTrafficPermittedtrue//network-security-config在app/src/main/AndroidManifest.xml中引入application...android:networkSecurityConfigxml/network_security_config...某些 Android 设备无法直接调用相机且未获得相应权限解决方法在 AndroidManifest.xml 中添加以下信息uses-permissionandroid:nameandroid.permission.CAMERA/uses-permissionandroid:nameandroid.permission.READ_MEDIA_IMAGES/uses-featureandroid:nameandroid.hardware.cameraandroid:requiredfalse/AndroidManifest.xml 参考示例?xml version1.0 encodingutf-8?manifestxmlns:androidhttp://schemas.android.com/apk/res/androidapplicationandroid:allowBackuptrueandroid:iconmipmap/ic_launcherandroid:labelstring/app_nameandroid:roundIconmipmap/ic_launcher_roundandroid:networkSecurityConfigxml/network_security_configandroid:supportsRtltrueandroid:themestyle/AppThemeactivityandroid:configChangesorientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|densityandroid:name.MainActivityandroid:labelstring/title_activity_mainandroid:themestyle/AppTheme.NoActionBarLaunchandroid:launchModesingleTaskandroid:exportedtrueintent-filteractionandroid:nameandroid.intent.action.MAIN/categoryandroid:nameandroid.intent.category.LAUNCHER//intent-filter/activityproviderandroid:nameandroidx.core.content.FileProviderandroid:authorities${applicationId}.fileproviderandroid:exportedfalseandroid:grantUriPermissionstruemeta-dataandroid:nameandroid.support.FILE_PROVIDER_PATHSandroid:resourcexml/file_paths/meta-data/provider/application!-- Permissions --uses-permissionandroid:nameandroid.permission.INTERNET/uses-permissionandroid:nameandroid.permission.CAMERA/uses-permissionandroid:nameandroid.permission.READ_MEDIA_IMAGES/uses-featureandroid:nameandroid.hardware.cameraandroid:requiredfalse//manifest综上所述使用外壳容器的好处在于打包为 APK 后用户仅需安装一次。后续更新时除非更换应用页面的映射地址否则用户无需重新安装应用只需退出并重新进入程序即可。由于未获得苹果的许可本文仅以Android平台为例进行说明。