本章将深入探讨 UniApp 的高级开发技巧,包括原生插件开发、混合开发模式、第三方 SDK 集成、自定义编译配置等内容,帮助开发者掌握更复杂的开发场景和技术实现。
9.1 原生插件开发
9.1.1 插件开发基础
插件项目结构
my-native-plugin/
├── package.json
├── plugin.xml
├── android/
│ ├── src/
│ │ └── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── plugin/
│ │ │ └── MyPlugin.java
│ │ └── assets/
│ └── build.gradle
├── ios/
│ ├── MyPlugin/
│ │ ├── MyPlugin.h
│ │ ├── MyPlugin.m
│ │ └── MyPluginProxy.h
│ └── MyPlugin.podspec
└── js/
└── index.js
package.json 配置
{
"name": "my-native-plugin",
"version": "1.0.0",
"description": "UniApp 原生插件示例",
"main": "js/index.js",
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"App": {
"app-vue": "y",
"app-nvue": "y"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "y",
"IE": "y",
"Edge": "y",
"Firefox": "y",
"Safari": "y"
},
"小程序": {
"微信": "y",
"阿里": "y",
"百度": "y",
"字节跳动": "y",
"QQ": "y"
},
"快应用": {
"华为": "y",
"联盟": "y"
}
}
}
},
"keywords": [
"uniapp",
"plugin",
"native"
],
"author": "Your Name",
"license": "MIT"
}
plugin.xml 配置
<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://www.phonegap.com/ns/plugins/1.0"
id="my-native-plugin"
version="1.0.0">
<name>MyNativePlugin</name>
<description>UniApp 原生插件示例</description>
<license>MIT</license>
<keywords>uniapp,plugin,native</keywords>
<engines>
<engine name="HBuilder" version=">=3.0.0"/>
</engines>
<!-- JavaScript 接口 -->
<js-module src="js/index.js" name="MyPlugin">
<clobbers target="MyPlugin" />
</js-module>
<!-- Android 配置 -->
<platform name="android">
<config-file target="res/xml/config.xml" parent="/*">
<feature name="MyPlugin">
<param name="android-package" value="com.example.plugin.MyPlugin"/>
</feature>
</config-file>
<source-file src="android/src/main/java/com/example/plugin/MyPlugin.java"
target-dir="src/com/example/plugin"/>
<config-file target="AndroidManifest.xml" parent="/manifest">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</config-file>
</platform>
<!-- iOS 配置 -->
<platform name="ios">
<config-file target="config.xml" parent="/*">
<feature name="MyPlugin">
<param name="ios-package" value="MyPlugin" />
</feature>
</config-file>
<header-file src="ios/MyPlugin/MyPlugin.h" />
<source-file src="ios/MyPlugin/MyPlugin.m" />
<config-file target="*-Info.plist" parent="/*">
<key>NSCameraUsageDescription</key>
<string>需要访问相机权限</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册权限</string>
</config-file>
</platform>
</plugin>
9.1.2 Android 插件开发
Android 插件主类
// android/src/main/java/com/example/plugin/MyPlugin.java
package com.example.plugin;
import android.content.Context;
import android.content.Intent;
import android.hardware.Camera;
import android.os.Build;
import android.util.Log;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.HashMap;
import java.util.Map;
public class MyPlugin extends CordovaPlugin {
private static final String TAG = "MyPlugin";
// 插件方法名称常量
private static final String ACTION_GET_DEVICE_INFO = "getDeviceInfo";
private static final String ACTION_OPEN_CAMERA = "openCamera";
private static final String ACTION_VIBRATE = "vibrate";
private static final String ACTION_SHOW_TOAST = "showToast";
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
Log.d(TAG, "执行方法: " + action);
try {
switch (action) {
case ACTION_GET_DEVICE_INFO:
getDeviceInfo(callbackContext);
return true;
case ACTION_OPEN_CAMERA:
openCamera(callbackContext);
return true;
case ACTION_VIBRATE:
int duration = args.getInt(0);
vibrate(duration, callbackContext);
return true;
case ACTION_SHOW_TOAST:
String message = args.getString(0);
showToast(message, callbackContext);
return true;
default:
callbackContext.error("未知的方法: " + action);
return false;
}
} catch (Exception e) {
Log.e(TAG, "执行方法失败: " + action, e);
callbackContext.error("执行失败: " + e.getMessage());
return false;
}
}
/**
* 获取设备信息
*/
private void getDeviceInfo(CallbackContext callbackContext) {
try {
Context context = cordova.getActivity().getApplicationContext();
JSONObject deviceInfo = new JSONObject();
deviceInfo.put("model", Build.MODEL);
deviceInfo.put("manufacturer", Build.MANUFACTURER);
deviceInfo.put("version", Build.VERSION.RELEASE);
deviceInfo.put("sdk", Build.VERSION.SDK_INT);
deviceInfo.put("brand", Build.BRAND);
deviceInfo.put("device", Build.DEVICE);
deviceInfo.put("board", Build.BOARD);
deviceInfo.put("host", Build.HOST);
deviceInfo.put("fingerprint", Build.FINGERPRINT);
// 获取应用信息
JSONObject appInfo = new JSONObject();
appInfo.put("packageName", context.getPackageName());
appInfo.put("versionName", context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName);
appInfo.put("versionCode", context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode);
deviceInfo.put("app", appInfo);
callbackContext.success(deviceInfo);
} catch (Exception e) {
Log.e(TAG, "获取设备信息失败", e);
callbackContext.error("获取设备信息失败: " + e.getMessage());
}
}
/**
* 打开相机
*/
private void openCamera(CallbackContext callbackContext) {
try {
// 检查相机权限
if (!hasPermission("android.permission.CAMERA")) {
callbackContext.error("没有相机权限");
return;
}
// 检查相机是否可用
if (!isCameraAvailable()) {
callbackContext.error("相机不可用");
return;
}
// 启动相机 Intent
Intent cameraIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
if (cameraIntent.resolveActivity(cordova.getActivity().getPackageManager()) != null) {
cordova.startActivityForResult(this, cameraIntent, 100);
// 保存回调上下文
this.cameraCallbackContext = callbackContext;
} else {
callbackContext.error("没有可用的相机应用");
}
} catch (Exception e) {
Log.e(TAG, "打开相机失败", e);
callbackContext.error("打开相机失败: " + e.getMessage());
}
}
/**
* 震动
*/
private void vibrate(int duration, CallbackContext callbackContext) {
try {
android.os.Vibrator vibrator = (android.os.Vibrator) cordova.getActivity().getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null && vibrator.hasVibrator()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(android.os.VibrationEffect.createOneShot(duration, android.os.VibrationEffect.DEFAULT_AMPLITUDE));
} else {
vibrator.vibrate(duration);
}
callbackContext.success("震动成功");
} else {
callbackContext.error("设备不支持震动");
}
} catch (Exception e) {
Log.e(TAG, "震动失败", e);
callbackContext.error("震动失败: " + e.getMessage());
}
}
/**
* 显示 Toast
*/
private void showToast(String message, CallbackContext callbackContext) {
try {
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
android.widget.Toast.makeText(cordova.getActivity(), message, android.widget.Toast.LENGTH_SHORT).show();
}
});
callbackContext.success("Toast 显示成功");
} catch (Exception e) {
Log.e(TAG, "显示 Toast 失败", e);
callbackContext.error("显示 Toast 失败: " + e.getMessage());
}
}
/**
* 检查权限
*/
private boolean hasPermission(String permission) {
return cordova.hasPermission(permission);
}
/**
* 检查相机是否可用
*/
private boolean isCameraAvailable() {
try {
Camera camera = Camera.open();
if (camera != null) {
camera.release();
return true;
}
} catch (Exception e) {
Log.e(TAG, "检查相机可用性失败", e);
}
return false;
}
// 相机回调上下文
private CallbackContext cameraCallbackContext;
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
if (requestCode == 100 && cameraCallbackContext != null) {
if (resultCode == cordova.getActivity().RESULT_OK) {
cameraCallbackContext.success("相机拍照成功");
} else {
cameraCallbackContext.error("相机拍照取消或失败");
}
cameraCallbackContext = null;
}
}
}
Android Gradle 配置
// android/build.gradle
apply plugin: 'com.android.library'
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
minSdkVersion 19
targetSdkVersion 30
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
// Cordova 依赖
compileOnly 'org.apache.cordova:framework:10.1.1'
}
9.1.3 iOS 插件开发
iOS 插件头文件
// ios/MyPlugin/MyPlugin.h
#import <Cordova/CDVPlugin.h>
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
@interface MyPlugin : CDVPlugin <UIImagePickerControllerDelegate, UINavigationControllerDelegate>
@property (nonatomic, strong) NSString* cameraCallbackId;
// 插件方法声明
- (void)getDeviceInfo:(CDVInvokedUrlCommand*)command;
- (void)openCamera:(CDVInvokedUrlCommand*)command;
- (void)vibrate:(CDVInvokedUrlCommand*)command;
- (void)showToast:(CDVInvokedUrlCommand*)command;
@end
iOS 插件实现文件
// ios/MyPlugin/MyPlugin.m
#import "MyPlugin.h"
#import <AudioToolbox/AudioToolbox.h>
#import <sys/utsname.h>
@implementation MyPlugin
#pragma mark - 获取设备信息
- (void)getDeviceInfo:(CDVInvokedUrlCommand*)command {
@try {
NSMutableDictionary* deviceInfo = [[NSMutableDictionary alloc] init];
// 设备基本信息
UIDevice* device = [UIDevice currentDevice];
[deviceInfo setObject:[device model] forKey:@"model"];
[deviceInfo setObject:[device systemName] forKey:@"platform"];
[deviceInfo setObject:[device systemVersion] forKey:@"version"];
[deviceInfo setObject:[device name] forKey:@"name"];
// 获取设备型号
struct utsname systemInfo;
uname(&systemInfo);
NSString* deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];
[deviceInfo setObject:deviceModel forKey:@"deviceModel"];
// 屏幕信息
CGRect screenBounds = [[UIScreen mainScreen] bounds];
CGFloat screenScale = [[UIScreen mainScreen] scale];
NSMutableDictionary* screenInfo = [[NSMutableDictionary alloc] init];
[screenInfo setObject:@(screenBounds.size.width) forKey:@"width"];
[screenInfo setObject:@(screenBounds.size.height) forKey:@"height"];
[screenInfo setObject:@(screenScale) forKey:@"scale"];
[deviceInfo setObject:screenInfo forKey:@"screen"];
// 应用信息
NSBundle* mainBundle = [NSBundle mainBundle];
NSMutableDictionary* appInfo = [[NSMutableDictionary alloc] init];
[appInfo setObject:[mainBundle bundleIdentifier] forKey:@"bundleId"];
[appInfo setObject:[mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"] forKey:@"version"];
[appInfo setObject:[mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"] forKey:@"build"];
[deviceInfo setObject:appInfo forKey:@"app"];
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:deviceInfo];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
} @catch (NSException* exception) {
NSLog(@"获取设备信息失败: %@", exception.reason);
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:exception.reason];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
}
#pragma mark - 打开相机
- (void)openCamera:(CDVInvokedUrlCommand*)command {
@try {
// 检查相机权限
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
if (authStatus == AVAuthorizationStatusDenied || authStatus == AVAuthorizationStatusRestricted) {
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"没有相机权限"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
return;
}
// 检查相机是否可用
if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"相机不可用"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
return;
}
// 保存回调ID
self.cameraCallbackId = command.callbackId;
// 创建图像选择器
UIImagePickerController* imagePicker = [[UIImagePickerController alloc] init];
imagePicker.sourceType = UIImagePickerControllerSourceTypeCamera;
imagePicker.delegate = self;
imagePicker.allowsEditing = NO;
// 在主线程中显示相机
dispatch_async(dispatch_get_main_queue(), ^{
[self.viewController presentViewController:imagePicker animated:YES completion:nil];
});
} @catch (NSException* exception) {
NSLog(@"打开相机失败: %@", exception.reason);
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:exception.reason];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
}
#pragma mark - 震动
- (void)vibrate:(CDVInvokedUrlCommand*)command {
@try {
NSNumber* duration = [command.arguments objectAtIndex:0];
// iOS 系统震动
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"震动成功"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
} @catch (NSException* exception) {
NSLog(@"震动失败: %@", exception.reason);
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:exception.reason];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
}
#pragma mark - 显示 Toast
- (void)showToast:(CDVInvokedUrlCommand*)command {
@try {
NSString* message = [command.arguments objectAtIndex:0];
dispatch_async(dispatch_get_main_queue(), ^{
// 创建 Alert
UIAlertController* alert = [UIAlertController alertControllerWithTitle:nil
message:message
preferredStyle:UIAlertControllerStyleAlert];
// 显示 Alert
[self.viewController presentViewController:alert animated:YES completion:nil];
// 2秒后自动关闭
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[alert dismissViewControllerAnimated:YES completion:nil];
});
});
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"Toast 显示成功"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
} @catch (NSException* exception) {
NSLog(@"显示 Toast 失败: %@", exception.reason);
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:exception.reason];
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
}
#pragma mark - UIImagePickerControllerDelegate
- (void)imagePickerController:(UIImagePickerController*)picker didFinishPickingMediaWithInfo:(NSDictionary<UIImagePickerControllerInfoKey,id>*)info {
[picker dismissViewControllerAnimated:YES completion:nil];
if (self.cameraCallbackId) {
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:@"相机拍照成功"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:self.cameraCallbackId];
self.cameraCallbackId = nil;
}
}
- (void)imagePickerControllerDidCancel:(UIImagePickerController*)picker {
[picker dismissViewControllerAnimated:YES completion:nil];
if (self.cameraCallbackId) {
CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"相机拍照取消"];
[self.commandDelegate sendPluginResult:pluginResult callbackId:self.cameraCallbackId];
self.cameraCallbackId = nil;
}
}
@end
9.1.4 JavaScript 接口
// js/index.js
class MyNativePlugin {
constructor() {
this.isReady = false;
this.readyCallbacks = [];
// 等待设备就绪
document.addEventListener('deviceready', () => {
this.isReady = true;
this.readyCallbacks.forEach(callback => callback());
this.readyCallbacks = [];
}, false);
}
// 确保设备就绪
ready(callback) {
if (this.isReady) {
callback();
} else {
this.readyCallbacks.push(callback);
}
}
// 执行原生方法
exec(action, args = []) {
return new Promise((resolve, reject) => {
this.ready(() => {
if (typeof cordova !== 'undefined' && cordova.exec) {
cordova.exec(
(result) => resolve(result),
(error) => reject(new Error(error)),
'MyPlugin',
action,
args
);
} else {
reject(new Error('Cordova 环境不可用'));
}
});
});
}
// 获取设备信息
async getDeviceInfo() {
try {
const deviceInfo = await this.exec('getDeviceInfo');
return {
success: true,
data: deviceInfo
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
// 打开相机
async openCamera() {
try {
const result = await this.exec('openCamera');
return {
success: true,
message: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
// 震动
async vibrate(duration = 500) {
try {
const result = await this.exec('vibrate', [duration]);
return {
success: true,
message: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
// 显示 Toast
async showToast(message) {
try {
const result = await this.exec('showToast', [message]);
return {
success: true,
message: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
// 检查平台支持
isSupported() {
return typeof cordova !== 'undefined' &&
(cordova.platformId === 'android' || cordova.platformId === 'ios');
}
// 获取平台信息
getPlatform() {
if (typeof cordova !== 'undefined') {
return cordova.platformId;
}
return 'unknown';
}
}
// 创建插件实例
const myPlugin = new MyNativePlugin();
// 导出插件
if (typeof module !== 'undefined' && module.exports) {
module.exports = myPlugin;
}
if (typeof window !== 'undefined') {
window.MyPlugin = myPlugin;
}
export default myPlugin;
9.2 混合开发模式
9.2.1 原生容器集成
Android 原生容器
// HybridActivity.java
package com.example.hybrid;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.webkit.WebSettings;
import androidx.appcompat.app.AppCompatActivity;
public class HybridActivity extends AppCompatActivity {
private WebView webView;
private HybridBridge hybridBridge;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hybrid);
initWebView();
initBridge();
loadUniApp();
}
private void initWebView() {
webView = findViewById(R.id.webview);
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
// 页面加载完成后注入桥接对象
hybridBridge.injectBridge();
}
});
}
private void initBridge() {
hybridBridge = new HybridBridge(this, webView);
webView.addJavascriptInterface(hybridBridge, "AndroidBridge");
}
private void loadUniApp() {
// 加载 UniApp 页面
String url = "file:///android_asset/www/index.html";
webView.loadUrl(url);
}
@Override
public void onBackPressed() {
if (webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
}
}
@Override
protected void onDestroy() {
if (webView != null) {
webView.destroy();
}
super.onDestroy();
}
}
Android 桥接类
// HybridBridge.java
package com.example.hybrid;
import android.app.Activity;
import android.content.Context;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import android.widget.Toast;
import org.json.JSONObject;
public class HybridBridge {
private Activity activity;
private WebView webView;
public HybridBridge(Activity activity, WebView webView) {
this.activity = activity;
this.webView = webView;
}
@JavascriptInterface
public void showToast(String message) {
activity.runOnUiThread(() -> {
Toast.makeText(activity, message, Toast.LENGTH_SHORT).show();
});
}
@JavascriptInterface
public String getDeviceInfo() {
try {
JSONObject deviceInfo = new JSONObject();
deviceInfo.put("platform", "android");
deviceInfo.put("model", android.os.Build.MODEL);
deviceInfo.put("version", android.os.Build.VERSION.RELEASE);
return deviceInfo.toString();
} catch (Exception e) {
return "{}";
}
}
@JavascriptInterface
public void callNativeMethod(String methodName, String params, String callbackId) {
activity.runOnUiThread(() -> {
try {
// 处理原生方法调用
String result = handleNativeCall(methodName, params);
// 回调 JavaScript
String jsCode = String.format("window.HybridCallback.success('%s', '%s')", callbackId, result);
webView.evaluateJavascript(jsCode, null);
} catch (Exception e) {
String jsCode = String.format("window.HybridCallback.error('%s', '%s')", callbackId, e.getMessage());
webView.evaluateJavascript(jsCode, null);
}
});
}
private String handleNativeCall(String methodName, String params) {
switch (methodName) {
case "vibrate":
return handleVibrate(params);
case "openCamera":
return handleOpenCamera(params);
default:
throw new RuntimeException("未知的方法: " + methodName);
}
}
private String handleVibrate(String params) {
// 实现震动功能
return "震动成功";
}
private String handleOpenCamera(String params) {
// 实现打开相机功能
return "相机打开成功";
}
public void injectBridge() {
String jsCode = ""
+ "window.HybridCallback = {"
+ " success: function(callbackId, result) {"
+ " if (window.hybridCallbacks && window.hybridCallbacks[callbackId]) {"
+ " window.hybridCallbacks[callbackId].success(result);"
+ " delete window.hybridCallbacks[callbackId];"
+ " }"
+ " },"
+ " error: function(callbackId, error) {"
+ " if (window.hybridCallbacks && window.hybridCallbacks[callbackId]) {"
+ " window.hybridCallbacks[callbackId].error(error);"
+ " delete window.hybridCallbacks[callbackId];"
+ " }"
+ " }"
+ "};"
+ "window.hybridCallbacks = {};"
+ "window.HybridBridge = {"
+ " call: function(methodName, params) {"
+ " return new Promise(function(resolve, reject) {"
+ " var callbackId = 'cb_' + Date.now() + '_' + Math.random();"
+ " window.hybridCallbacks[callbackId] = {"
+ " success: resolve,"
+ " error: reject"
+ " };"
+ " AndroidBridge.callNativeMethod(methodName, JSON.stringify(params), callbackId);"
+ " });"
+ " }"
+ "};"
+ "console.log('Hybrid Bridge 注入成功');"
;
webView.evaluateJavascript(jsCode, null);
}
}
9.2.2 iOS 原生容器
iOS 容器视图控制器
// HybridViewController.h
#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>
@interface HybridViewController : UIViewController <WKNavigationDelegate, WKScriptMessageHandler>
@property (nonatomic, strong) WKWebView *webView;
@property (nonatomic, strong) WKUserContentController *userContentController;
@end
// HybridViewController.m
#import "HybridViewController.h"
@implementation HybridViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setupWebView];
[self setupBridge];
[self loadUniApp];
}
- (void)setupWebView {
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 配置用户内容控制器
self.userContentController = [[WKUserContentController alloc] init];
config.userContentController = self.userContentController;
// 允许内联播放
config.allowsInlineMediaPlayback = YES;
// 创建 WebView
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
self.webView.navigationDelegate = self;
self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self.view addSubview:self.webView];
}
- (void)setupBridge {
// 注册消息处理器
[self.userContentController addScriptMessageHandler:self name:@"showToast"];
[self.userContentController addScriptMessageHandler:self name:@"getDeviceInfo"];
[self.userContentController addScriptMessageHandler:self name:@"callNativeMethod"];
}
- (void)loadUniApp {
NSString *htmlPath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html" inDirectory:@"www"];
NSURL *htmlURL = [NSURL fileURLWithPath:htmlPath];
NSURL *baseURL = [htmlURL URLByDeletingLastPathComponent];
NSString *htmlContent = [NSString stringWithContentsOfURL:htmlURL encoding:NSUTF8StringEncoding error:nil];
[self.webView loadHTMLString:htmlContent baseURL:baseURL];
}
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
// 页面加载完成后注入桥接对象
[self injectBridge];
}
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSString *name = message.name;
id body = message.body;
if ([name isEqualToString:@"showToast"]) {
[self handleShowToast:body];
} else if ([name isEqualToString:@"getDeviceInfo"]) {
[self handleGetDeviceInfo:body];
} else if ([name isEqualToString:@"callNativeMethod"]) {
[self handleCallNativeMethod:body];
}
}
- (void)handleShowToast:(id)params {
NSString *message = params[@"message"];
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil
message:message
preferredStyle:UIAlertControllerStyleAlert];
[self presentViewController:alert animated:YES completion:nil];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[alert dismissViewControllerAnimated:YES completion:nil];
});
});
}
- (void)handleGetDeviceInfo:(id)params {
UIDevice *device = [UIDevice currentDevice];
NSDictionary *deviceInfo = @{
@"platform": @"ios",
@"model": [device model],
@"version": [device systemVersion],
@"name": [device name]
};
NSString *callbackId = params[@"callbackId"];
[self callJavaScriptCallback:callbackId withResult:deviceInfo error:nil];
}
- (void)handleCallNativeMethod:(id)params {
NSString *methodName = params[@"methodName"];
NSDictionary *methodParams = params[@"params"];
NSString *callbackId = params[@"callbackId"];
@try {
id result = [self handleNativeCall:methodName withParams:methodParams];
[self callJavaScriptCallback:callbackId withResult:result error:nil];
} @catch (NSException *exception) {
[self callJavaScriptCallback:callbackId withResult:nil error:exception.reason];
}
}
- (id)handleNativeCall:(NSString *)methodName withParams:(NSDictionary *)params {
if ([methodName isEqualToString:@"vibrate"]) {
return [self handleVibrate:params];
} else if ([methodName isEqualToString:@"openCamera"]) {
return [self handleOpenCamera:params];
} else {
@throw [NSException exceptionWithName:@"UnknownMethod" reason:[NSString stringWithFormat:@"未知的方法: %@", methodName] userInfo:nil];
}
}
- (NSString *)handleVibrate:(NSDictionary *)params {
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
return @"震动成功";
}
- (NSString *)handleOpenCamera:(NSDictionary *)params {
// 实现打开相机功能
return @"相机打开成功";
}
- (void)callJavaScriptCallback:(NSString *)callbackId withResult:(id)result error:(NSString *)error {
NSString *jsCode;
if (error) {
jsCode = [NSString stringWithFormat:@"window.HybridCallback.error('%@', '%@')", callbackId, error];
} else {
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:result options:0 error:nil];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
jsCode = [NSString stringWithFormat:@"window.HybridCallback.success('%@', %@)", callbackId, jsonString];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.webView evaluateJavaScript:jsCode completionHandler:nil];
});
}
- (void)injectBridge {
NSString *jsCode = @""
"window.HybridCallback = {"
" success: function(callbackId, result) {"
" if (window.hybridCallbacks && window.hybridCallbacks[callbackId]) {"
" window.hybridCallbacks[callbackId].success(result);"
" delete window.hybridCallbacks[callbackId];"
" }"
" },"
" error: function(callbackId, error) {"
" if (window.hybridCallbacks && window.hybridCallbacks[callbackId]) {"
" window.hybridCallbacks[callbackId].error(error);"
" delete window.hybridCallbacks[callbackId];"
" }"
" }"
"};"
"window.hybridCallbacks = {};"
"window.HybridBridge = {"
" call: function(methodName, params) {"
" return new Promise(function(resolve, reject) {"
" var callbackId = 'cb_' + Date.now() + '_' + Math.random();"
" window.hybridCallbacks[callbackId] = {"
" success: resolve,"
" error: reject"
" };"
" window.webkit.messageHandlers.callNativeMethod.postMessage({"
" methodName: methodName,"
" params: params,"
" callbackId: callbackId"
" });"
" });"
" }"
"};"
"console.log('Hybrid Bridge 注入成功');"
;
[self.webView evaluateJavaScript:jsCode completionHandler:nil];
}
- (void)dealloc {
[self.userContentController removeScriptMessageHandlerForName:@"showToast"];
[self.userContentController removeScriptMessageHandlerForName:@"getDeviceInfo"];
[self.userContentController removeScriptMessageHandlerForName:@"callNativeMethod"];
}
@end
9.2.3 JavaScript 桥接层
// utils/hybridBridge.js
class HybridBridge {
constructor() {
this.isReady = false;
this.readyCallbacks = [];
this.platform = this.detectPlatform();
this.init();
}
// 检测平台
detectPlatform() {
const userAgent = navigator.userAgent;
if (/Android/i.test(userAgent)) {
return 'android';
} else if (/iPhone|iPad|iPod/i.test(userAgent)) {
return 'ios';
} else {
return 'web';
}
}
// 初始化桥接
init() {
if (this.platform === 'web') {
// Web 环境,直接标记为就绪
this.markReady();
return;
}
// 等待原生桥接注入
const checkBridge = () => {
if (this.isBridgeAvailable()) {
this.markReady();
} else {
setTimeout(checkBridge, 100);
}
};
checkBridge();
}
// 检查桥接是否可用
isBridgeAvailable() {
if (this.platform === 'android') {
return typeof window.AndroidBridge !== 'undefined' && typeof window.HybridBridge !== 'undefined';
} else if (this.platform === 'ios') {
return typeof window.webkit !== 'undefined' &&
typeof window.webkit.messageHandlers !== 'undefined' &&
typeof window.HybridBridge !== 'undefined';
}
return false;
}
// 标记为就绪
markReady() {
this.isReady = true;
this.readyCallbacks.forEach(callback => callback());
this.readyCallbacks = [];
}
// 等待就绪
ready(callback) {
if (this.isReady) {
callback();
} else {
this.readyCallbacks.push(callback);
}
}
// 调用原生方法
async callNative(methodName, params = {}) {
return new Promise((resolve, reject) => {
this.ready(async () => {
try {
if (this.platform === 'web') {
// Web 环境模拟
resolve(this.simulateNativeCall(methodName, params));
return;
}
const result = await window.HybridBridge.call(methodName, params);
resolve(result);
} catch (error) {
reject(error);
}
});
});
}
// Web 环境模拟原生调用
simulateNativeCall(methodName, params) {
switch (methodName) {
case 'getDeviceInfo':
return {
platform: 'web',
userAgent: navigator.userAgent,
language: navigator.language,
cookieEnabled: navigator.cookieEnabled
};
case 'showToast':
console.log('Toast:', params.message);
return 'Toast 显示成功(Web 模拟)';
case 'vibrate':
if (navigator.vibrate) {
navigator.vibrate(params.duration || 500);
return '震动成功';
} else {
return '设备不支持震动';
}
default:
throw new Error(`未知的方法: ${methodName}`);
}
}
// 显示 Toast
async showToast(message) {
if (this.platform === 'android' && typeof window.AndroidBridge !== 'undefined') {
window.AndroidBridge.showToast(message);
return 'Toast 显示成功';
} else if (this.platform === 'ios' && typeof window.webkit !== 'undefined') {
window.webkit.messageHandlers.showToast.postMessage({ message });
return 'Toast 显示成功';
} else {
return this.callNative('showToast', { message });
}
}
// 获取设备信息
async getDeviceInfo() {
if (this.platform === 'android' && typeof window.AndroidBridge !== 'undefined') {
const info = window.AndroidBridge.getDeviceInfo();
return JSON.parse(info);
} else if (this.platform === 'ios' && typeof window.webkit !== 'undefined') {
return new Promise((resolve) => {
const callbackId = 'deviceInfo_' + Date.now();
window.hybridCallbacks = window.hybridCallbacks || {};
window.hybridCallbacks[callbackId] = {
success: resolve,
error: resolve
};
window.webkit.messageHandlers.getDeviceInfo.postMessage({ callbackId });
});
} else {
return this.callNative('getDeviceInfo');
}
}
// 震动
async vibrate(duration = 500) {
return this.callNative('vibrate', { duration });
}
// 打开相机
async openCamera() {
return this.callNative('openCamera');
}
// 获取平台信息
getPlatform() {
return this.platform;
}
// 检查是否为原生环境
isNative() {
return this.platform === 'android' || this.platform === 'ios';
}
// 检查是否为 Web 环境
isWeb() {
return this.platform === 'web';
}
}
// 创建全局实例
const hybridBridge = new HybridBridge();
// 导出
export default hybridBridge;
// 全局挂载
if (typeof window !== 'undefined') {
window.HybridBridge = hybridBridge;
}
9.3 第三方 SDK 集成
9.3.1 支付 SDK 集成
微信支付集成
// utils/wechatPay.js
class WechatPay {
constructor(config) {
this.config = {
appId: config.appId,
merchantId: config.merchantId,
apiKey: config.apiKey,
notifyUrl: config.notifyUrl,
...config
};
}
// 发起支付
async pay(orderInfo) {
try {
// 1. 创建预支付订单
const prepayResult = await this.createPrepayOrder(orderInfo);
if (!prepayResult.success) {
throw new Error(prepayResult.message);
}
// 2. 调用支付接口
const payResult = await this.invokePay(prepayResult.data);
return {
success: true,
data: payResult
};
} catch (error) {
console.error('微信支付失败:', error);
return {
success: false,
error: error.message
};
}
}
// 创建预支付订单
async createPrepayOrder(orderInfo) {
const params = {
appid: this.config.appId,
mch_id: this.config.merchantId,
nonce_str: this.generateNonceStr(),
body: orderInfo.description,
out_trade_no: orderInfo.orderNo,
total_fee: Math.round(orderInfo.amount * 100), // 转换为分
spbill_create_ip: '127.0.0.1',
notify_url: this.config.notifyUrl,
trade_type: this.getTradeType()
};
// 生成签名
params.sign = this.generateSign(params);
try {
const response = await uni.request({
url: 'https://api.mch.weixin.qq.com/pay/unifiedorder',
method: 'POST',
header: {
'Content-Type': 'application/xml'
},
data: this.objectToXml(params)
});
const result = this.xmlToObject(response.data);
if (result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS') {
return {
success: true,
data: {
prepayId: result.prepay_id,
codeUrl: result.code_url
}
};
} else {
return {
success: false,
message: result.return_msg || result.err_code_des
};
}
} catch (error) {
return {
success: false,
message: '网络请求失败'
};
}
}
// 调用支付
async invokePay(prepayData) {
const platform = uni.getSystemInfoSync().platform;
if (platform === 'android' || platform === 'ios') {
// App 支付
return this.invokeAppPay(prepayData);
} else {
// H5 支付
return this.invokeH5Pay(prepayData);
}
}
// App 支付
async invokeAppPay(prepayData) {
const payParams = {
appid: this.config.appId,
partnerid: this.config.merchantId,
prepayid: prepayData.prepayId,
package: 'Sign=WXPay',
noncestr: this.generateNonceStr(),
timestamp: Math.floor(Date.now() / 1000).toString()
};
// 生成签名
payParams.sign = this.generateSign(payParams);
return new Promise((resolve, reject) => {
uni.requestPayment({
provider: 'wxpay',
orderInfo: payParams,
success: (res) => {
resolve({
success: true,
message: '支付成功',
data: res
});
},
fail: (err) => {
reject(new Error(err.errMsg || '支付失败'));
}
});
});
}
// H5 支付
async invokeH5Pay(prepayData) {
// H5 支付需要跳转到微信支付页面
const payUrl = `https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=${prepayData.prepayId}&package=ETC`;
// 跳转到支付页面
window.location.href = payUrl;
return {
success: true,
message: '正在跳转到支付页面'
};
}
// 生成签名
generateSign(params) {
// 1. 参数排序
const sortedKeys = Object.keys(params).sort();
// 2. 拼接参数
const stringA = sortedKeys
.filter(key => params[key] !== '' && key !== 'sign')
.map(key => `${key}=${params[key]}`)
.join('&');
// 3. RSA2 签名
return this.rsaSign(stringA, this.config.privateKey);
}
// RSA 签名
rsaSign(data, privateKey) {
// 这里需要引入 RSA 库或使用 crypto-js
// 示例使用 node-rsa
const NodeRSA = require('node-rsa');
const key = new NodeRSA(privateKey);
return key.sign(data, 'base64');
}
// 构建订单字符串
buildOrderString(params) {
return Object.keys(params)
.sort()
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&');
}
// 格式化日期
formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// 验证支付结果
async verifyPayResult(paymentData) {
try {
const bizContent = {
out_trade_no: paymentData.out_trade_no,
trade_no: paymentData.trade_no
};
const params = {
app_id: this.config.appId,
method: 'alipay.trade.query',
charset: this.config.charset,
sign_type: this.config.signType,
timestamp: this.formatDate(new Date()),
version: this.config.version,
biz_content: JSON.stringify(bizContent)
};
params.sign = this.generateSign(params);
const response = await uni.request({
url: 'https://openapi.alipay.com/gateway.do',
method: 'POST',
data: params
});
return {
success: response.data.alipay_trade_query_response.code === '10000',
data: response.data.alipay_trade_query_response
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
}
// 导出支付宝支付类
export default Alipay;
9.3.2 地图 SDK 集成
高德地图集成
// utils/amapSDK.js
class AmapSDK {
constructor(config) {
this.config = {
key: config.key,
version: config.version || '2.0',
plugins: config.plugins || [],
...config
};
this.map = null;
this.isLoaded = false;
this.loadPromise = null;
}
// 加载地图 SDK
async loadSDK() {
if (this.isLoaded) {
return Promise.resolve();
}
if (this.loadPromise) {
return this.loadPromise;
}
this.loadPromise = new Promise((resolve, reject) => {
// 检查是否在 Web 环境
if (typeof window === 'undefined') {
reject(new Error('地图 SDK 只能在 Web 环境中使用'));
return;
}
// 检查是否已经加载
if (window.AMap) {
this.isLoaded = true;
resolve();
return;
}
// 动态加载脚本
const script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = `https://webapi.amap.com/maps?v=${this.config.version}&key=${this.config.key}&plugin=${this.config.plugins.join(',')}`;
script.onload = () => {
this.isLoaded = true;
resolve();
};
script.onerror = () => {
reject(new Error('地图 SDK 加载失败'));
};
document.head.appendChild(script);
});
return this.loadPromise;
}
// 初始化地图
async initMap(containerId, options = {}) {
try {
await this.loadSDK();
const defaultOptions = {
zoom: 10,
center: [116.397428, 39.90923],
viewMode: '3D',
pitch: 0,
rotation: 0,
...options
};
this.map = new AMap.Map(containerId, defaultOptions);
return {
success: true,
map: this.map
};
} catch (error) {
console.error('地图初始化失败:', error);
return {
success: false,
error: error.message
};
}
}
// 添加标记
addMarker(options) {
if (!this.map) {
throw new Error('地图未初始化');
}
const marker = new AMap.Marker({
position: options.position,
title: options.title,
icon: options.icon,
...options
});
this.map.add(marker);
return marker;
}
// 添加信息窗口
addInfoWindow(marker, content) {
if (!this.map) {
throw new Error('地图未初始化');
}
const infoWindow = new AMap.InfoWindow({
content: content,
offset: new AMap.Pixel(0, -30)
});
marker.on('click', () => {
infoWindow.open(this.map, marker.getPosition());
});
return infoWindow;
}
// 获取当前位置
async getCurrentPosition() {
try {
await this.loadSDK();
return new Promise((resolve, reject) => {
AMap.plugin('AMap.Geolocation', () => {
const geolocation = new AMap.Geolocation({
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0,
convert: true,
showButton: true,
buttonPosition: 'LB',
buttonOffset: new AMap.Pixel(10, 20),
showMarker: true,
showCircle: true,
panToLocation: true,
zoomToAccuracy: true
});
geolocation.getCurrentPosition((status, result) => {
if (status === 'complete') {
resolve({
success: true,
position: {
longitude: result.position.lng,
latitude: result.position.lat
},
address: result.formattedAddress,
accuracy: result.accuracy
});
} else {
reject(new Error(result.message));
}
});
});
});
} catch (error) {
return {
success: false,
error: error.message
};
}
}
// 地理编码(地址转坐标)
async geocode(address) {
try {
await this.loadSDK();
return new Promise((resolve, reject) => {
AMap.plugin('AMap.Geocoder', () => {
const geocoder = new AMap.Geocoder({
city: '全国'
});
geocoder.getLocation(address, (status, result) => {
if (status === 'complete' && result.geocodes.length > 0) {
const location = result.geocodes[0].location;
resolve({
success: true,
position: {
longitude: location.lng,
latitude: location.lat
},
formattedAddress: result.geocodes[0].formattedAddress
});
} else {
reject(new Error('地理编码失败'));
}
});
});
});
} catch (error) {
return {
success: false,
error: error.message
};
}
}
// 逆地理编码(坐标转地址)
async reverseGeocode(longitude, latitude) {
try {
await this.loadSDK();
return new Promise((resolve, reject) => {
AMap.plugin('AMap.Geocoder', () => {
const geocoder = new AMap.Geocoder();
const lnglat = new AMap.LngLat(longitude, latitude);
geocoder.getAddress(lnglat, (status, result) => {
if (status === 'complete' && result.regeocode) {
resolve({
success: true,
address: result.regeocode.formattedAddress,
addressComponent: result.regeocode.addressComponent
});
} else {
reject(new Error('逆地理编码失败'));
}
});
});
});
} catch (error) {
return {
success: false,
error: error.message
};
}
}
// 路径规划
async planRoute(start, end, type = 'driving') {
try {
await this.loadSDK();
const routeTypes = {
driving: 'AMap.Driving',
walking: 'AMap.Walking',
transit: 'AMap.Transfer',
riding: 'AMap.Riding'
};
const pluginName = routeTypes[type];
if (!pluginName) {
throw new Error('不支持的路径规划类型');
}
return new Promise((resolve, reject) => {
AMap.plugin(pluginName, () => {
const routeService = new AMap[type.charAt(0).toUpperCase() + type.slice(1)]({
map: this.map,
panel: 'panel'
});
routeService.search(start, end, (status, result) => {
if (status === 'complete') {
resolve({
success: true,
routes: result.routes,
distance: result.routes[0].distance,
duration: result.routes[0].time
});
} else {
reject(new Error('路径规划失败'));
}
});
});
});
} catch (error) {
return {
success: false,
error: error.message
};
}
}
// 销毁地图
destroy() {
if (this.map) {
this.map.destroy();
this.map = null;
}
}
}
// 导出高德地图 SDK
export default AmapSDK;
9.4 自定义编译配置
9.4.1 条件编译
平台条件编译
// utils/platformUtils.js
class PlatformUtils {
// 获取当前平台
static getCurrentPlatform() {
// #ifdef APP-PLUS
return 'app';
// #endif
// #ifdef H5
return 'h5';
// #endif
// #ifdef MP-WEIXIN
return 'mp-weixin';
// #endif
// #ifdef MP-ALIPAY
return 'mp-alipay';
// #endif
// #ifdef MP-BAIDU
return 'mp-baidu';
// #endif
// #ifdef MP-TOUTIAO
return 'mp-toutiao';
// #endif
// #ifdef MP-QQ
return 'mp-qq';
// #endif
return 'unknown';
}
// 检查是否为 App 平台
static isApp() {
// #ifdef APP-PLUS
return true;
// #endif
return false;
}
// 检查是否为 H5 平台
static isH5() {
// #ifdef H5
return true;
// #endif
return false;
}
// 检查是否为小程序平台
static isMiniProgram() {
// #ifdef MP
return true;
// #endif
return false;
}
// 获取平台特定的 API
static getPlatformAPI() {
const platform = this.getCurrentPlatform();
switch (platform) {
case 'app':
return {
// App 特定 API
openLocation: this.openLocationApp,
getLocation: this.getLocationApp,
scanCode: this.scanCodeApp
};
case 'h5':
return {
// H5 特定 API
openLocation: this.openLocationH5,
getLocation: this.getLocationH5,
scanCode: this.scanCodeH5
};
case 'mp-weixin':
return {
// 微信小程序特定 API
openLocation: this.openLocationWX,
getLocation: this.getLocationWX,
scanCode: this.scanCodeWX
};
default:
return {
openLocation: () => Promise.reject(new Error('平台不支持')),
getLocation: () => Promise.reject(new Error('平台不支持')),
scanCode: () => Promise.reject(new Error('平台不支持'))
};
}
}
// App 平台打开位置
static openLocationApp(latitude, longitude, name) {
// #ifdef APP-PLUS
return new Promise((resolve, reject) => {
uni.openLocation({
latitude,
longitude,
name,
success: resolve,
fail: reject
});
});
// #endif
}
// H5 平台打开位置
static openLocationH5(latitude, longitude, name) {
// #ifdef H5
const url = `https://uri.amap.com/marker?position=${longitude},${latitude}&name=${encodeURIComponent(name)}`;
window.open(url, '_blank');
return Promise.resolve();
// #endif
}
// 微信小程序打开位置
static openLocationWX(latitude, longitude, name) {
// #ifdef MP-WEIXIN
return new Promise((resolve, reject) => {
wx.openLocation({
latitude,
longitude,
name,
success: resolve,
fail: reject
});
});
// #endif
}
}
export default PlatformUtils;
环境条件编译
// config/environment.js
class Environment {
constructor() {
this.env = this.getCurrentEnvironment();
this.config = this.getEnvironmentConfig();
}
// 获取当前环境
getCurrentEnvironment() {
// #ifdef NODE_ENV === 'development'
return 'development';
// #endif
// #ifdef NODE_ENV === 'testing'
return 'testing';
// #endif
// #ifdef NODE_ENV === 'production'
return 'production';
// #endif
return 'development';
}
// 获取环境配置
getEnvironmentConfig() {
const configs = {
development: {
apiBaseUrl: 'http://localhost:3000/api',
debug: true,
logLevel: 'debug',
enableMock: true,
enableVConsole: true
},
testing: {
apiBaseUrl: 'https://test-api.example.com/api',
debug: true,
logLevel: 'info',
enableMock: false,
enableVConsole: true
},
production: {
apiBaseUrl: 'https://api.example.com/api',
debug: false,
logLevel: 'error',
enableMock: false,
enableVConsole: false
}
};
return configs[this.env] || configs.development;
}
// 获取 API 基础 URL
getApiBaseUrl() {
return this.config.apiBaseUrl;
}
// 检查是否为开发环境
isDevelopment() {
return this.env === 'development';
}
// 检查是否为生产环境
isProduction() {
return this.env === 'production';
}
// 检查是否启用调试
isDebugEnabled() {
return this.config.debug;
}
// 获取日志级别
getLogLevel() {
return this.config.logLevel;
}
// 初始化环境特定功能
initEnvironmentFeatures() {
// 开发环境特定初始化
// #ifdef NODE_ENV === 'development'
this.initDevelopmentFeatures();
// #endif
// 生产环境特定初始化
// #ifdef NODE_ENV === 'production'
this.initProductionFeatures();
// #endif
}
// 初始化开发环境功能
initDevelopmentFeatures() {
// #ifdef NODE_ENV === 'development'
console.log('开发环境初始化');
// 启用 vConsole
if (this.config.enableVConsole) {
this.enableVConsole();
}
// 启用 Mock 数据
if (this.config.enableMock) {
this.enableMockData();
}
// #endif
}
// 初始化生产环境功能
initProductionFeatures() {
// #ifdef NODE_ENV === 'production'
console.log('生产环境初始化');
// 禁用控制台输出
this.disableConsole();
// 启用错误监控
this.enableErrorMonitoring();
// #endif
}
// 启用 vConsole
enableVConsole() {
// #ifdef H5
import('vconsole').then(VConsole => {
new VConsole.default();
});
// #endif
}
// 启用 Mock 数据
enableMockData() {
// Mock 数据逻辑
console.log('Mock 数据已启用');
}
// 禁用控制台输出
disableConsole() {
// #ifdef NODE_ENV === 'production'
console.log = () => {};
console.warn = () => {};
console.error = () => {};
// #endif
}
// 启用错误监控
enableErrorMonitoring() {
// 错误监控逻辑
window.addEventListener('error', (event) => {
// 上报错误
this.reportError(event.error);
});
}
// 上报错误
reportError(error) {
// 错误上报逻辑
console.error('错误上报:', error);
}
}
// 创建环境实例
const environment = new Environment();
// 初始化环境功能
environment.initEnvironmentFeatures();
export default environment;
9.4.2 自定义构建脚本
Webpack 自定义配置
// vue.config.js
const path = require('path');
const webpack = require('webpack');
module.exports = {
// 基础路径
publicPath: process.env.NODE_ENV === 'production' ? '/app/' : '/',
// 输出目录
outputDir: 'dist',
// 静态资源目录
assetsDir: 'static',
// 生产环境是否生成 sourceMap
productionSourceMap: false,
// 开发服务器配置
devServer: {
port: 8080,
open: true,
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
}
}
},
// 链式操作配置
chainWebpack: config => {
// 设置别名
config.resolve.alias
.set('@', path.resolve(__dirname, 'src'))
.set('@components', path.resolve(__dirname, 'src/components'))
.set('@utils', path.resolve(__dirname, 'src/utils'))
.set('@assets', path.resolve(__dirname, 'src/assets'));
// 优化图片
config.module
.rule('images')
.test(/\.(png|jpe?g|gif|svg)$/)
.use('url-loader')
.loader('url-loader')
.options({
limit: 8192,
name: 'img/[name].[hash:8].[ext]'
});
// 优化字体
config.module
.rule('fonts')
.test(/\.(woff2?|eot|ttf|otf)$/)
.use('url-loader')
.loader('url-loader')
.options({
limit: 8192,
name: 'fonts/[name].[hash:8].[ext]'
});
// 生产环境优化
if (process.env.NODE_ENV === 'production') {
// 代码分割
config.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
vendor: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: 5,
chunks: 'initial',
reuseExistingChunk: true
}
}
});
// 压缩配置
config.optimization.minimizer('terser').tap(args => {
args[0].terserOptions.compress.drop_console = true;
args[0].terserOptions.compress.drop_debugger = true;
return args;
});
}
},
// 配置 webpack
configureWebpack: config => {
// 定义全局变量
config.plugins.push(
new webpack.DefinePlugin({
'process.env.VUE_APP_VERSION': JSON.stringify(process.env.npm_package_version),
'process.env.VUE_APP_BUILD_TIME': JSON.stringify(new Date().toISOString())
})
);
// 开发环境配置
if (process.env.NODE_ENV === 'development') {
config.devtool = 'eval-source-map';
}
// 生产环境配置
if (process.env.NODE_ENV === 'production') {
// 外部化依赖
config.externals = {
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex'
};
// Gzip 压缩
const CompressionPlugin = require('compression-webpack-plugin');
config.plugins.push(
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8
})
);
}
},
// CSS 相关配置
css: {
// 是否提取 CSS
extract: process.env.NODE_ENV === 'production',
// CSS source map
sourceMap: false,
// CSS 预处理器配置
loaderOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
},
less: {
additionalData: `@import "@/styles/variables.less";`
}
}
},
// PWA 配置
pwa: {
name: 'UniApp Demo',
themeColor: '#4DBA87',
msTileColor: '#000000',
appleMobileWebAppCapable: 'yes',
appleMobileWebAppStatusBarStyle: 'black',
// 配置 workbox 插件
workboxPluginMode: 'InjectManifest',
workboxOptions: {
swSrc: 'src/sw.js'
}
}
};
自定义构建脚本
// scripts/build.js
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
class BuildScript {
constructor() {
this.startTime = Date.now();
this.platforms = ['h5', 'mp-weixin', 'mp-alipay', 'app-plus'];
this.environments = ['development', 'testing', 'production'];
}
// 执行构建
async build(options = {}) {
const {
platform = 'h5',
environment = 'production',
clean = true,
analyze = false
} = options;
try {
console.log(chalk.blue('🚀 开始构建...'));
console.log(chalk.gray(`平台: ${platform}`));
console.log(chalk.gray(`环境: ${environment}`));
// 清理输出目录
if (clean) {
await this.cleanOutput(platform);
}
// 设置环境变量
this.setEnvironmentVariables(environment);
// 执行构建命令
await this.executeBuild(platform, environment);
// 分析构建结果
if (analyze) {
await this.analyzeBuild(platform);
}
// 生成构建报告
await this.generateBuildReport(platform, environment);
const duration = Date.now() - this.startTime;
console.log(chalk.green(`✅ 构建完成! 耗时: ${duration}ms`));
} catch (error) {
console.error(chalk.red('❌ 构建失败:'), error.message);
process.exit(1);
}
}
// 清理输出目录
async cleanOutput(platform) {
console.log(chalk.yellow('🧹 清理输出目录...'));
const outputDirs = {
'h5': 'dist/build/h5',
'mp-weixin': 'dist/build/mp-weixin',
'mp-alipay': 'dist/build/mp-alipay',
'app-plus': 'dist/build/app-plus'
};
const outputDir = outputDirs[platform];
if (outputDir && fs.existsSync(outputDir)) {
execSync(`rm -rf ${outputDir}`, { stdio: 'inherit' });
}
}
// 设置环境变量
setEnvironmentVariables(environment) {
process.env.NODE_ENV = environment;
process.env.UNI_PLATFORM = platform;
process.env.VUE_APP_ENV = environment;
process.env.VUE_APP_BUILD_TIME = new Date().toISOString();
}
// 执行构建命令
async executeBuild(platform, environment) {
console.log(chalk.blue('📦 执行构建...'));
const buildCommands = {
'h5': 'uni build --platform h5',
'mp-weixin': 'uni build --platform mp-weixin',
'mp-alipay': 'uni build --platform mp-alipay',
'app-plus': 'uni build --platform app-plus'
};
const command = buildCommands[platform];
if (!command) {
throw new Error(`不支持的平台: ${platform}`);
}
execSync(command, { stdio: 'inherit' });
}
// 分析构建结果
async analyzeBuild(platform) {
console.log(chalk.blue('📊 分析构建结果...'));
// 使用 webpack-bundle-analyzer
if (platform === 'h5') {
execSync('npx webpack-bundle-analyzer dist/build/h5/static/js/*.js', {
stdio: 'inherit'
});
}
}
// 生成构建报告
async generateBuildReport(platform, environment) {
console.log(chalk.blue('📋 生成构建报告...'));
const report = {
platform,
environment,
buildTime: new Date().toISOString(),
duration: Date.now() - this.startTime,
version: process.env.npm_package_version,
nodeVersion: process.version,
files: this.getOutputFiles(platform)
};
const reportPath = `dist/build-report-${platform}-${environment}.json`;
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(chalk.green(`📄 构建报告已生成: ${reportPath}`));
}
// 获取输出文件信息
getOutputFiles(platform) {
const outputDirs = {
'h5': 'dist/build/h5',
'mp-weixin': 'dist/build/mp-weixin',
'mp-alipay': 'dist/build/mp-alipay',
'app-plus': 'dist/build/app-plus'
};
const outputDir = outputDirs[platform];
if (!outputDir || !fs.existsSync(outputDir)) {
return [];
}
const files = [];
const walkDir = (dir) => {
const items = fs.readdirSync(dir);
items.forEach(item => {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
walkDir(fullPath);
} else {
files.push({
path: path.relative(outputDir, fullPath),
size: stat.size,
modified: stat.mtime
});
}
});
};
walkDir(outputDir);
return files;
}
// 构建所有平台
async buildAll(environment = 'production') {
console.log(chalk.blue('🚀 构建所有平台...'));
for (const platform of this.platforms) {
await this.build({ platform, environment });
}
console.log(chalk.green('✅ 所有平台构建完成!'));
}
}
// 命令行接口
if (require.main === module) {
const args = process.argv.slice(2);
const buildScript = new BuildScript();
const options = {};
// 解析命令行参数
args.forEach((arg, index) => {
if (arg === '--platform' && args[index + 1]) {
options.platform = args[index + 1];
}
if (arg === '--env' && args[index + 1]) {
options.environment = args[index + 1];
}
if (arg === '--analyze') {
options.analyze = true;
}
if (arg === '--all') {
options.buildAll = true;
}
});
// 执行构建
if (options.buildAll) {
buildScript.buildAll(options.environment);
} else {
buildScript.build(options);
}
}
module.exports = BuildScript;
9.5 本章总结
本章深入探讨了 UniApp 的进阶开发技巧,涵盖了以下重要内容:
学习要点回顾
原生插件开发
- 插件项目结构和配置
- Android 插件开发(Java)
- iOS 插件开发(Objective-C)
- JavaScript 桥接层实现
混合开发模式
- 原生容器集成
- Android WebView 容器
- iOS WKWebView 容器
- JavaScript 桥接通信
第三方 SDK 集成
- 支付 SDK(微信支付、支付宝支付)
- 地图 SDK(高德地图)
- SDK 封装和抽象
自定义编译配置
- 条件编译技巧
- 环境配置管理
- Webpack 自定义配置
- 自定义构建脚本
实践练习
开发原生插件
- 创建一个相机插件
- 实现文件操作插件
- 开发设备信息插件
集成第三方服务
- 集成推送服务
- 集成统计分析
- 集成社交分享
优化构建流程
- 配置多环境构建
- 实现自动化部署
- 优化打包体积
常见问题解答
Q: 如何选择原生插件开发还是 JavaScript 实现? A: 考虑以下因素: - 功能复杂度:复杂的原生功能建议使用原生插件 - 性能要求:高性能需求选择原生插件 - 平台兼容性:需要跨平台一致性选择 JavaScript - 开发成本:JavaScript 开发成本较低
Q: 混合开发中如何处理原生和 Web 的数据同步? A: 建议方案: - 使用事件机制进行通信 - 建立统一的数据格式 - 实现数据缓存和同步机制 - 处理异步操作和错误情况
Q: 如何优化第三方 SDK 的加载性能? A: 优化策略: - 按需加载 SDK - 使用 CDN 加速 - 实现 SDK 缓存 - 异步初始化
Q: 条件编译如何处理复杂的业务逻辑? A: 最佳实践: - 抽象平台差异到工具类 - 使用策略模式处理平台逻辑 - 保持代码的可读性和维护性 - 充分测试各平台功能
最佳实践建议
插件开发
- 遵循平台开发规范
- 提供完整的错误处理
- 编写详细的文档
- 进行充分的测试
SDK 集成
- 选择稳定可靠的 SDK
- 实现统一的接口抽象
- 处理版本兼容性
- 监控 SDK 性能
构建优化
- 合理配置构建环境
- 实现自动化流程
- 优化构建性能
- 监控构建质量
代码管理
- 使用版本控制
- 实现代码审查
- 建立编码规范
- 持续集成部署
通过本章的学习,你已经掌握了 UniApp 的高级开发技巧,能够处理复杂的开发场景,构建高质量的跨平台应用。这些技能将帮助你在实际项目中解决各种技术挑战,提升开发效率和应用质量。
恭喜你完成了 UniApp 跨平台开发教程的全部学习! 🎉
从基础入门到进阶技巧,你已经系统地掌握了 UniApp 开发的各个方面。现在你可以:
- 熟练使用 UniApp 开发跨平台应用
- 掌握组件开发和复用技巧
- 进行性能优化和调试
- 完成应用的打包发布
- 运用进阶开发技巧解决复杂问题
继续实践和探索,你将成为一名优秀的跨平台开发工程师! const platform = uni.getSystemInfoSync().platform;
if (platform === 'android' || platform === 'ios') {
return 'APP';
} else {
return 'MWEB';
}
}
// 生成随机字符串 generateNonceStr() { return Math.random().toString(36).substr(2, 15); }
// 生成签名 generateSign(params) { // 1. 参数排序 const sortedKeys = Object.keys(params).sort();
// 2. 拼接参数
const stringA = sortedKeys
.filter(key => params[key] !== '' && key !== 'sign')
.map(key => `${key}=${params[key]}`)
.join('&');
// 3. 拼接密钥
const stringSignTemp = `${stringA}&key=${this.config.apiKey}`;
// 4. MD5 加密并转大写
return this.md5(stringSignTemp).toUpperCase();
}
// MD5 加密 md5(str) { // 这里需要引入 MD5 库或使用 crypto-js // 示例使用 crypto-js const CryptoJS = require(‘crypto-js’); return CryptoJS.MD5(str).toString(); }
// 对象转 XML
objectToXml(obj) {
let xml = ‘<${key}><![CDATA[${obj[key]}]]></${key}>
;
}
}
xml += ‘
// XML 转对象 xmlToObject(xml) { const result = {}; const regex = /<(\w+)><![CDATA[([^]]+)]]><\/\1>/g; let match;
while ((match = regex.exec(xml)) !== null) {
result[match[1]] = match[2];
}
return result;
}
// 验证支付结果 async verifyPayResult(paymentData) { try { const params = { appid: this.config.appId, mch_id: this.config.merchantId, transaction_id: paymentData.transaction_id, nonce_str: this.generateNonceStr() };
params.sign = this.generateSign(params);
const response = await uni.request({
url: 'https://api.mch.weixin.qq.com/pay/orderquery',
method: 'POST',
header: {
'Content-Type': 'application/xml'
},
data: this.objectToXml(params)
});
const result = this.xmlToObject(response.data);
return {
success: result.return_code === 'SUCCESS' && result.result_code === 'SUCCESS',
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
} }
// 导出微信支付类 export default WechatPay;
// 使用示例 // const wechatPay = new WechatPay({ // appId: ‘your_app_id’, // merchantId: ‘your_merchant_id’, // apiKey: ‘your_api_key’, // notifyUrl: ‘https://your-domain.com/notify’ // }); // // const payResult = await wechatPay.pay({ // orderNo: ‘ORDER_20231201_001’, // amount: 99.99, // description: ‘商品购买’ // }); “`
支付宝支付集成
”`javascript // utils/alipay.js class Alipay { constructor(config) { this.config = { appId: config.appId, privateKey: config.privateKey, publicKey: config.publicKey, notifyUrl: config.notifyUrl, returnUrl: config.returnUrl, signType: ‘RSA2’, charset: ‘utf-8’, version: ‘1.0’, …config }; }
// 发起支付 async pay(orderInfo) { try { const platform = uni.getSystemInfoSync().platform;
if (platform === 'android' || platform === 'ios') {
return this.appPay(orderInfo);
} else {
return this.webPay(orderInfo);
}
} catch (error) {
console.error('支付宝支付失败:', error);
return {
success: false,
error: error.message
};
}
}
// App 支付 async appPay(orderInfo) { const bizContent = { out_trade_no: orderInfo.orderNo, total_amount: orderInfo.amount.toString(), subject: orderInfo.subject, body: orderInfo.body, product_code: ‘QUICK_MSECURITY_PAY’ };
const params = {
app_id: this.config.appId,
method: 'alipay.trade.app.pay',
charset: this.config.charset,
sign_type: this.config.signType,
timestamp: this.formatDate(new Date()),
version: this.config.version,
notify_url: this.config.notifyUrl,
biz_content: JSON.stringify(bizContent)
};
// 生成签名
params.sign = this.generateSign(params);
// 构建支付参数字符串
const orderString = this.buildOrderString(params);
return new Promise((resolve, reject) => {
uni.requestPayment({
provider: 'alipay',
orderInfo: orderString,
success: (res) => {
resolve({
success: true,
message: '支付成功',
data: res
});
},
fail: (err) => {
reject(new Error(err.errMsg || '支付失败'));
}
});
});
}
// Web 支付 async webPay(orderInfo) { const bizContent = { out_trade_no: orderInfo.orderNo, total_amount: orderInfo.amount.toString(), subject: orderInfo.subject, body: orderInfo.body, product_code: ‘FAST_INSTANT_TRADE_PAY’ };
const params = {
app_id: this.config.appId,
method: 'alipay.trade.page.pay',
charset: this.config.charset,
sign_type: this.config.signType,
timestamp: this.formatDate(new Date()),
version: this.config.version,
notify_url: this.config.notifyUrl,
return_url: this.config.returnUrl,
biz_content: JSON.stringify(bizContent)
};
// 生成签名
params.sign = this.generateSign(params);
// 构建支付 URL
const payUrl = 'https://openapi.alipay.com/gateway.do?' + this.buildOrderString(params);
// 跳转到支付页面
window.location.href = payUrl;
return {
success: true,
message: '正在跳转到支付页面'
};
}