Capacitor原理浅究
1. 插件
1.1 What Web Can Do?
在开发插件之前,官方推荐打开 https://whatwebcando.today/ ,以查看你的需求是否需要做成插件的形式,比如我们是使用Webview打开H5页面,那么就使用Webview打开这个地址,看目前Web API都可以做哪些工作,不能做哪些工作。
如果已经有 Web API 可以实现我们的需求,那么我们就不需要做无谓的工作。即便此WebAPI仅支持Android使用或者仅支持iOS使用,我们也可以省去一端的开发工作。
1.2 插件开发
参考 https://capacitorjs.com/docs/plugins/tutorial/introduction
使用 npx @capacitor/create-plugin 命令创建插件,输入插件的以下信息:
npm包名、文件夹名、package id、类名、库url、作者、license、插件描述信息。
或者直接以参数形式放在命令后面,官方也给了示例:
npx @capacitor/create-plugin \
--name @capacitor-community/screen-orientation \
--package-id io.ionic.plugins.screenorientation \
--class-name ScreenOrientation \
--repo "https://ionic.io" \
--license "MIT" \
--description "Work with the screen orientation in a common way for iOS, Android, and web"
下面是生成的插件文件夹内容:
├─.git
├─android(Android相关文件)
│ ├─.gradle(gradle依赖文件夹)
│ ├─gradle/wrapper
│ ├─src
│ │ ├─androidTest/java/com/getcapacitor/android
│ │ ├─main
│ │ │ ├─java/com/fawkes/plugins/screenorientation(Android插件代码)
│ │ │ ├─res(资源文件夹)
│ │ │ └─AndroidManifest.xml
│ │ └─test/java/com/getcapacitor
│ ├─.gitignore
│ ├─build.gradle
│ ├─gradle.properties
│ ├─gradlew
│ ├─gradlew.bat
│ ├─progurad-rules.pro
│ └─settings.gradle
├─ios(iOS相关文件)
│ ├─Plugin(iOS插件代码)
│ │ ├─Info.plist
│ │ ├─…
│ ├─Plugin.xcodeproj
│ ├─Plugin.xcworkspace
│ ├─PluginTests
│ └─Podfile
├─src
│ ├─definations.ts(插件API声明)
│ ├─index.ts(注册插件并export)
│ └─web.ts(网页端API实现)
├─.eslintignore
├─.gitignore
├─.prettierignore
├─CapacitorCommunityScreenOrientation.podspec(用于iOS发布到Cocoapod)
├─CONTRIBUTING.md
├─package.json
├─README.md
├─rollup.config.js
└─tsconfig.json
其实和Cordova的文件夹也是有相似之处的,比如android文件夹类似Cordova插件的src/android文件夹,src文件夹对应Cordova插件的www文件夹,只是Capacitor插件使用ts。
开发插件时,以Android为例,可以新建一个Capacitor工程,在app模块中开发相应代码,开发完成后再把文件拷贝到插件对应文件夹,注意修改Java文件中的包名。
完成后,使用npm publish上传到npm仓库,这一点也是和Cordova相同的。
1.3 插件使用
安装插件需要 npm i [插件npm包名] 然后 npx cap sync 即可。
H5中调用插件,以Vue为例,只需要import后调用相关方法即可。
代码如下,引入camera插件并使用:
import { Camera, CameraResultType } from '@capacitor/camera';
const takePicture = async () => {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: true,
resultType: CameraResultType.Uri
});
// image.webPath will contain a path that can be set as an image src.
// You can access the original file using image.path, which can be
// passed to the Filesystem API to read the raw data of the image,
// if desired (or pass resultType: CameraResultType.Base64 to getPhoto)
var imageUrl = image.webPath;
console.log('image', image)
// Can be set to the src of an image now
};
1.4 插件代码
1.4.1 Android
在 Capacitor Android 平台,插件是以 Module 的形式引入项目,其实和 Uniapp 类似,相比于Cordova直接把插件代码放到app Modules下,组件化的形式让代码更加简洁,可维护性更高。
插件类需要继承 com.getcapacitor.Plugin
类,并添加 com.getcapacitor.annotation.CapacitorPlugin
注解,CapacitorPlugin 注解中有三个属性,name、requestCodes、permissions。
插件对js暴露的方法需要加上 @PluginMethod
注解,而方法的参数统一为 PluginCall
。
我们可以对比下Cordova,Cordova的插件方法都是通过一个入口: execute(String action, JSONArray args, CallbackContext callbackContext)
调用,通过 action
判断调用哪个具体的方法,args
是js传递过来的参数,callbackContext
则是回调类,再看Capacitor则是用@PluginMethod
注解代替了 action 参数,PluginCall
则包含了 args 和 callbackContext,相对来说更加简约。
除了 @Capacitor
和 @PluginMethod
之外,代码中还用到了 @PermissionCallback
和 @ActivityCallback
注解,分别用来接收 权限申请 和 Activity结果返回 的回调,使用时直接获取权限或打开Activity时将方法名作为参数传递即可。
下面是官方 Camera 插件的插件类部分代码:
@SuppressLint("InlinedApi")
@CapacitorPlugin(
name = "Camera",
permissions = {
@Permission(strings = { Manifest.permission.CAMERA }, alias = CameraPlugin.CAMERA),
// SDK VERSIONS 32 AND BELOW
@Permission(
strings = { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE },
alias = CameraPlugin.PHOTOS
),
/*
SDK VERSIONS 33 AND ABOVE
This alias is a placeholder and the PHOTOS alias will be updated to use these permissions
so that the end user does not need to explicitly use separate aliases depending
on the SDK version.
*/
@Permission(strings = { Manifest.permission.READ_MEDIA_IMAGES }, alias = CameraPlugin.MEDIA)
}
)
public class CameraPlugin extends Plugin {
// Permission alias constants
static final String CAMERA = "camera";
static final String PHOTOS = "photos";
static final String MEDIA = "media";
// Message constants
...
@PluginMethod
public void getPhoto(PluginCall call) {
...
}
@PluginMethod
public void pickLimitedLibraryPhotos(PluginCall call) {
call.unimplemented("not supported on android");
}
private boolean checkCameraPermissions(PluginCall call) {
...
}
private boolean checkPhotosPermissions(PluginCall call) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
if (getPermissionState(PHOTOS) != PermissionState.GRANTED) {
requestPermissionForAlias(PHOTOS, call, "cameraPermissionsCallback");
return false;
}
} else if (getPermissionState(MEDIA) != PermissionState.GRANTED) {
requestPermissionForAlias(MEDIA, call, "cameraPermissionsCallback");
return false;
}
return true;
}
/**
* Completes the plugin call after a camera permission request
*
* @see #getPhoto(PluginCall)
* @param call the plugin call
*/
@PermissionCallback
private void cameraPermissionsCallback(PluginCall call) {
if (call.getMethodName().equals("pickImages")) {
openPhotos(call, true, true);
} else {
if (settings.getSource() == CameraSource.CAMERA && getPermissionState(CAMERA) != PermissionState.GRANTED) {
Logger.debug(getLogTag(), "User denied camera permission: " + getPermissionState(CAMERA).toString());
call.reject(PERMISSION_DENIED_ERROR_CAMERA);
return;
} else if (settings.getSource() == CameraSource.PHOTOS) {
PermissionState permissionState = (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
? getPermissionState(PHOTOS)
: getPermissionState(MEDIA);
if (permissionState != PermissionState.GRANTED) {
Logger.debug(getLogTag(), "User denied photos permission: " + permissionState.toString());
call.reject(PERMISSION_DENIED_ERROR_PHOTOS);
return;
}
}
doShow(call);
}
}
@Override
protected void requestPermissionForAliases(@NonNull String[] aliases, @NonNull PluginCall call, @NonNull String callbackName) {
// If the SDK version is 33 or higher, use the MEDIA alias permissions instead.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
for (int i = 0; i < aliases.length; i++) {
if (aliases[i].equals(PHOTOS)) {
aliases[i] = MEDIA;
}
}
}
super.requestPermissionForAliases(aliases, call, callbackName);
}
@ActivityCallback
public void processPickedImage(PluginCall call, ActivityResult result) {
...
}
/**
* After processing the image, return the final result back to the caller.
* @param call
* @param bitmap
* @param u
*/
@SuppressWarnings("deprecation")
private void returnResult(PluginCall call, Bitmap bitmap, Uri u) {
...
call.reject(UNABLE_TO_PROCESS_IMAGE);
...
call.resolve(data);
...
}
@Override
@PluginMethod
public void requestPermissions(PluginCall call) {
// If the camera permission is defined in the manifest, then we have to prompt the user
// or else we will get a security exception when trying to present the camera. If, however,
// it is not defined in the manifest then we don't need to prompt and it will just work.
if (isPermissionDeclared(CAMERA)) {
// just request normally
super.requestPermissions(call);
} else {
// the manifest does not define camera permissions, so we need to decide what to do
// first, extract the permissions being requested
JSArray providedPerms = call.getArray("permissions");
List<String> permsList = null;
if (providedPerms != null) {
try {
permsList = providedPerms.toList();
} catch (JSONException e) {}
}
if (permsList != null && permsList.size() == 1 && permsList.contains(CAMERA)) {
// the only thing being asked for was the camera so we can just return the current state
checkPermissions(call);
} else {
// we need to ask about photos so request storage permissions
requestPermissionForAlias(PHOTOS, call, "checkPermissions");
}
}
}
@Override
public Map<String, PermissionState> getPermissionStates() {
Map<String, PermissionState> permissionStates = super.getPermissionStates();
// If Camera is not in the manifest and therefore not required, say the permission is granted
if (!isPermissionDeclared(CAMERA)) {
permissionStates.put(CAMERA, PermissionState.GRANTED);
}
// If the SDK version is 33 or higher, update the PHOTOS state to match the MEDIA state.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && permissionStates.containsKey(MEDIA)) {
permissionStates.put(PHOTOS, permissionStates.get(MEDIA));
}
return permissionStates;
}
@Override
protected Bundle saveInstanceState() {
Bundle bundle = super.saveInstanceState();
if (bundle != null) {
bundle.putString("cameraImageFileSavePath", imageFileSavePath);
}
return bundle;
}
@Override
protected void restoreState(Bundle state) {
String storedImageFileSavePath = state.getString("cameraImageFileSavePath");
if (storedImageFileSavePath != null) {
imageFileSavePath = storedImageFileSavePath;
}
}
}
1.4.2 iOS
在 Capacitor iOS 平台,插件以 Pod 库的形式引入。
下面是 Podfile 文件的内容,可以看到已经默认添加了 StatusBar 等插件,其中 camera 插件是使用 npm i @capacitor/camera && npx cap sync
的方式手动添加的。
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '13.0'
use_frameworks!
# workaround to avoid Xcode caching of Pods that requires
# Product -> Clean Build Folder after new Cordova plugins installed
# Requires CocoaPods 1.6 or newer
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard'
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
end
target 'App' do
capacitor_pods
# Add your Pods here
end
post_install do |installer|
assertDeploymentTarget(installer)
end
还是以 camera 插件为例,看下插件类的代码,可以看到名为 CameraPlugin 的文件有三个,CapacitorPlugin.h、CapacitorPlugin.m、 CapacitorPlugin.swift。
Capacitor.h 定义了两个变量,插件的版本号:
#import <UIKit/UIKit.h>
//! Project version number for Plugin.
FOUNDATION_EXPORT double PluginVersionNumber;
//! Project version string for Plugin.
FOUNDATION_EXPORT const unsigned char PluginVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <Plugin/PublicHeader.h>
CapacitorPlugin.m 中使用Capacitor的宏定义 CAP_PLUGIN
和 CAP_PLUGIN_METHOD
注册插件和插件方法,而其中的 CAPCameraPlugin 和 CAPPluginReturnPromise 之类则是在 Capacitor.swift 中定义的。
#import <Foundation/Foundation.h>
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(CAPCameraPlugin, "Camera",
CAP_PLUGIN_METHOD(getPhoto, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(pickImages, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(checkPermissions, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(requestPermissions, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(pickLimitedLibraryPhotos, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLimitedLibraryPhotos, CAPPluginReturnPromise);
)
Capacitor.swift 中是具体的代码实现,继承了CAPPLugin
,可以看到代码中有一些 @objc
修饰符,@objc 修饰的类名和方法名可以被 Objective-C 调用,对应 Capacitor.m 中的 CAPCameraPlugin 和 CAPPluginReturnPromise 等。
import Foundation
import Capacitor
import Photos
import PhotosUI
@objc(CAPCameraPlugin)
public class CameraPlugin: CAPPlugin {
private var call: CAPPluginCall?
private var settings = CameraSettings()
private let defaultSource = CameraSource.prompt
private let defaultDirection = CameraDirection.rear
private var multiple = false
private var imageCounter = 0
@objc override public func checkPermissions(_ call: CAPPluginCall) {
var result: [String: Any] = [:]
...
call.resolve(result)
}
@objc override public func requestPermissions(_ call: CAPPluginCall) {
...
}
@objc func pickLimitedLibraryPhotos(_ call: CAPPluginCall) {
...
call.unavailable("Not available on iOS 13")
...
}
@objc func getLimitedLibraryPhotos(_ call: CAPPluginCall) {
...
}
@objc func getPhoto(_ call: CAPPluginCall) {
...
}
@objc func pickImages(_ call: CAPPluginCall) {
...
}
// public delegate methods
extension CameraPlugin: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate {
public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
self.call?.reject("User cancelled photos app")
}
public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) {
self.call?.reject("User cancelled photos app")
}
public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
self.call?.reject("User cancelled photos app")
}
public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
...
}
}
@available(iOS 14, *)
extension CameraPlugin: PHPickerViewControllerDelegate {
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
...
}
}
private extension CameraPlugin {
func returnImage(_ processedImage: ProcessedImage, isSaved: Bool) {
...
}
}
1.5 Cordova插件兼容
Capacitor是如何实现兼容Cordova插件的?
Cordova插件安装和Capacitor插件安装一样,npm install xxx && npx cap sync
即可,使用则和使用Cordova时一致,直接调用即可。
Capacitor工程 H5目录下,除了 Capacitor 的 capacitor.config.json
和 capacitor.plugins.json
(此文件仅Android)文件,还是有Cordova相关的 cordova.js
和 cordova_plugins.js
以及 plugins
文件夹。
其中 cordova.js 是经过 Capacitor 改造的,主要是修改了 js 与 native 相互调用的那块代码,改造成Capacitor 的调用形式。
而插件的原生代码,
Android 平台下,是放在 Capacitor 预置的一个名为 capacitor-cordova-android-plugins 的 Module 下,在此 Module 下的 build.gradle 文件中可以看到,引入了 implementation "org.apache.cordova:framework:$cordovaAndroidVersion"
依赖,保证插件中引入Cordova的类也不会报错。
iOS平台中,是放在 Pods
下 Capacitor 预置的一个名为 CapacitorPlugins
的 Target
下。
在调用插件时,插件js中会调用 cordova.js 中的 cordova/exec,经过Capacitor改造后,
Android平台,最终会调用 cordova.js 中的 window.androidBridge.postMessage(JSON.stringify(command));
,而window.androidBridge
是在 capacitor-android
模块下的 MessageHandler.java
文件中添加到 Webview 中的,postMessage
的实现也在此文件中。
iOS 平台,最终会调用 cordova.js 中的 window.webkit.messageHandlers.bridge.postMessage(command);
,而其中的 bridge
是在 WebviewDelegationHandler.swift
文件中定义的。
2. bridge
研究过Capacitor是如何兼容Cordova插件之后,我们再看看Capacitor自己的插件的调用原理是什么样的。
www下capacitor 相关的只有两个文件,capacitor.config.json
和 capacitor.plugins.json
(此文件仅Android有)。
其中 capacitor.config.json 是一些配置信息,比如app id、应用名称、H5资源包在哪个文件夹等,示例:
{
"appId": "io.ionic.starter",
"appName": "TestAAA",
"webDir": "dist",
"server": {
"androidScheme": "https"
}
}
capacitor.plugins.json 则是插件列表,包含插件npm包名和插件类路径,示例:
[
{
"pkg": "@capacitor/app",
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
},
{
"pkg": "@capacitor/camera",
"classpath": "com.capacitorjs.plugins.camera.CameraPlugin"
},
{
"pkg": "@capacitor/haptics",
"classpath": "com.capacitorjs.plugins.haptics.HapticsPlugin"
},
{
"pkg": "@capacitor/keyboard",
"classpath": "com.capacitorjs.plugins.keyboard.KeyboardPlugin"
},
{
"pkg": "@capacitor/status-bar",
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
}
]
2.1 Android
Android 平台,js与native交互的代码放在 capacitor-android
模块中(真实路径为 打包工程根目录/node_moduls/@capacitor/android/capacitor 文件夹下)。
我们可以从 app 模块的 MainActivity 开始,一步步探索,这里只聊一下其中的核心功能点。
2.1.1 BridgeActivity
与Cordova相似,MainActivity 在Cordova中继承了 CordovaActivity,而Capacitor中是继承的 BridgeActivity。
下面是部分代码,可以看到 BridgeActivity 的主要工作就是注册插件。
public class BridgeActivity extends AppCompatActivity {
protected Bridge bridge;
protected boolean keepRunning = true; // config.xml中的设置,默认值为true
protected CapConfig config; // Capacitor配置,有默认配置,也可以从文件读取
protected List<Class<? extends Plugin>> initialPlugins = new ArrayList<>();
protected final Bridge.Builder bridgeBuilder = new Bridge.Builder(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 获取 assets下内容
PluginManager loader = new PluginManager(getAssets());
try {
// 获取 Capacitor 插件
// 读取 capacitor.plugins.json,获取插件的 classpath,添加到插件列表
bridgeBuilder.addPlugins(loader.loadPluginClasses());
} catch (PluginLoadException ex) {
Logger.error("Error loading plugins.", ex);
}
this.load();
}
protected void load() {
Logger.debug("Starting BridgeActivity");
bridge = bridgeBuilder.addPlugins(initialPlugins).setConfig(config).create();
this.keepRunning = bridge.shouldKeepRunning();
this.onNewIntent(getIntent());
}
// 手动注册插件
public void registerPlugin(Class<? extends Plugin> plugin) {
bridgeBuilder.addPlugin(plugin);
}
// 手动批量注册插件
public void registerPlugins(List<Class<? extends Plugin>> plugins) {
bridgeBuilder.addPlugins(plugins);
}
}
2.1.2 Bridge
跟着进入 Bridge.java,这里官方为此类写的注释:
The Bridge class is the main engine of Capacitor. It manages loading and communicating with all Plugins, proxying Native events to Plugins, executing Plugin methods, communicating with the WebView, and a whole lot more.
Bridge类是Capacitor的主要引擎。它负责管理所有插件的加载和通信,将本机事件代理到插件、执行插件方法、与 WebView 通信等等。
Generally, you’ll not use Bridge directly, instead, extend from BridgeActivity to get a WebView instance and proxy native events automatically.
通常,开发时不会直接使用 Bridge,而是扩展 BridgeActivity 以自动获取 WebView 实例并代理本机事件。
Bridge.java代码有一千多行,这里只放一些核心代码:
Builder
Bridge类中有一个 Builder 静态内部类,从2.1 BridgeActivity 中就可以看到,Bridge对象是通过 new Bridge.Builder(this) 初始化了一个 Builder 类,设置一些参数后,使用 create() 构建出来的。
在Builder中,除了 参数、两个构造器 和 一些 set / add 方法,就是 create() 方法。
public Bridge create() {
// Cordova 初始化,获取 config.xml 中的 配置信息 以及 Cordova插件列表
ConfigXmlParser parser = new ConfigXmlParser();
parser.parse(activity.getApplicationContext());
CordovaPreferences preferences = parser.getPreferences();
preferences.setPreferencesBundle(activity.getIntent().getExtras());
List<PluginEntry> pluginEntries = parser.getPluginEntries();
// MockCordovaInterfaceImpl 继承自CordovaInterfaceImpl
MockCordovaInterfaceImpl cordovaInterface = new MockCordovaInterfaceImpl(activity);
if (instanceState != null) {
cordovaInterface.restoreInstanceState(instanceState);
}
WebView webView = this.fragment != null ? fragment.getView().findViewById(R.id.webview) : activity.findViewById(R.id.webview);
MockCordovaWebViewImpl mockWebView = new MockCordovaWebViewImpl(activity.getApplicationContext());
mockWebView.init(cordovaInterface, pluginEntries, preferences, webView);
PluginManager pluginManager = mockWebView.getPluginManager();
cordovaInterface.onCordovaInit(pluginManager);
// Bridge initialization
Bridge bridge = new Bridge(activity, serverPath, fragment, webView, plugins,
pluginInstances, cordovaInterface, pluginManager, preferences, config
);
if (webView instanceof CapacitorWebView) {
CapacitorWebView capacitorWebView = (CapacitorWebView) webView;
capacitorWebView.setBridge(bridge);
}
bridge.setCordovaWebView(mockWebView);
bridge.setWebViewListeners(webViewListeners);
bridge.setRouteProcessor(routeProcessor);
if (instanceState != null) {
bridge.restoreInstanceState(instanceState);
}
return bridge;
}
构造器
Bridge 有两个构造器,不过其中一个已经废弃。
下面的代码包含 构造器 和 构造器中用到的部分参数。
// Our MessageHandler for sending and receiving data to the WebView
private final MessageHandler msgHandler;
// The ThreadHandler for executing plugin calls
private final HandlerThread handlerThread = new HandlerThread("CapacitorPlugins");
// Our Handler for posting plugin calls. Created from the ThreadHandler
private Handler taskHandler = null;
// A map of Plugin Id's to PluginHandle's
private Map<String, PluginHandle> plugins = new HashMap<>();
private Bridge(
AppCompatActivity context, ServerPath serverPath,
Fragment fragment, WebView webView,
List<Class<? extends Plugin>> initialPlugins,
List<Plugin> pluginInstances,
MockCordovaInterfaceImpl cordovaInterface,
PluginManager pluginManager, CordovaPreferences preferences,
CapConfig config
) {
this.app = new App();
... // 构造器中参数赋值 this.xxx = xxx,这里就省略掉了
// Start our plugin execution threads and handlers
handlerThread.start();
taskHandler = new Handler(handlerThread.getLooper());
// Capacitor配置
this.config = config != null ? config : CapConfig.loadDefault(getActivity());
// 获取 config中的loggingEnabled 字段,是否打印日志,默认true
Logger.init(this.config);
// Initialize web view and message handler for it
// initWebView() 是初始化了webview的一些配置,如是否启用JS,是否启用Database、UseAgent、内容url等
this.initWebView();
// 与Cordova的AllowList类似,从config中获取配置,放行哪些地址的请求
this.setAllowedOriginRules();
this.msgHandler = new MessageHandler(this, webView, pluginManager);
// Grab any intent info that our app was launched with
Intent intent = context.getIntent();
this.intentUri = intent.getData();
// Register our core plugins
// 注册插件,包括 核心插件、initialPlugins、pluginInstances,对每个插件调用 registerPlugin(pluginClass) 方法
this.registerAllPlugins();
// 最终是调用 webView.loadUrl(appUrl) 打开 Webview
this.loadWebView();
}
/**
* Register a plugin class
* @param pluginClass a class inheriting from Plugin
*/
public void registerPlugin(Class<? extends Plugin> pluginClass) {
String pluginId = pluginId(pluginClass);
if (pluginId == null) return;
try {
this.plugins.put(pluginId, new PluginHandle(this, pluginClass));
} catch (InvalidPluginException ex) {
logInvalidPluginException(pluginClass);
} catch (PluginLoadException ex) {
logPluginLoadException(pluginClass, ex);
}
}
MessageHandler
从构造器方法中,可以发现,插件的使用应该主要是在 MessageHandler 类中。
可以看到 js 调用 Navtive 还是使用的 @JavascriptInterface 注解,再查看 Capacitor工程 node_modules 下 @capacitor/android/capacitor/src/main/assets/native-bridge.js
,可以看到最终插件的调用都是通过 win.androidBridge.postMessage(JSON.stringify(data));
和 win.webkit.messageHandlers.bridge.postMessage(data);
实现的。
而 Native 调用 js 则跟Cordova不太一样,添加了一个新的方式,使用 JavaScriptReplyProxy 实现。
/**
* MessageHandler handles messages from the WebView, dispatching them
* to plugins.
*/
public class MessageHandler {
private Bridge bridge;
private WebView webView;
private PluginManager cordovaPluginManager;
private JavaScriptReplyProxy javaScriptReplyProxy;
public MessageHandler(Bridge bridge, WebView webView, PluginManager cordovaPluginManager) {
this.bridge = bridge;
this.webView = webView;
this.cordovaPluginManager = cordovaPluginManager;
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && !bridge.getConfig().isUsingLegacyBridge()) {
WebViewCompat.WebMessageListener capListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
if (isMainFrame) {
postMessage(message.getData());
javaScriptReplyProxy = replyProxy;
} else {
Logger.warn("Plugin execution is allowed in Main Frame only");
}
};
try {
WebViewCompat.addWebMessageListener(webView, "androidBridge", bridge.getAllowedOriginRules(), capListener);
} catch (Exception ex) {
webView.addJavascriptInterface(this, "androidBridge");
}
} else {
webView.addJavascriptInterface(this, "androidBridge");
}
}
// 通用的 js调用 Native 的方法,使用 @JavascriptInterface 注解
/**
* The main message handler that will be called from JavaScript
* to send a message to the native bridge.
* @param jsonStr
*/
@JavascriptInterface
@SuppressWarnings("unused")
public void postMessage(String jsonStr) {
try {
JSObject postData = new JSObject(jsonStr);
String type = postData.getString("type");
boolean typeIsNotNull = type != null;
boolean isCordovaPlugin = typeIsNotNull && type.equals("cordova");
boolean isJavaScriptError = typeIsNotNull && type.equals("js.error");
String callbackId = postData.getString("callbackId");
if (isCordovaPlugin) {
String service = postData.getString("service");
String action = postData.getString("action");
String actionArgs = postData.getString("actionArgs");
Logger.verbose(
Logger.tags("Plugin"),
"To native (Cordova plugin): callbackId: " +
callbackId +
", service: " +
service +
", action: " +
action +
", actionArgs: " +
actionArgs
);
this.callCordovaPluginMethod(callbackId, service, action, actionArgs);
} else if (isJavaScriptError) {
Logger.error("JavaScript Error: " + jsonStr);
} else {
String pluginId = postData.getString("pluginId");
String methodName = postData.getString("methodName");
JSObject methodData = postData.getJSObject("options", new JSObject());
Logger.verbose(
Logger.tags("Plugin"),
"To native (Capacitor plugin): callbackId: " + callbackId + ", pluginId: " + pluginId + ", methodName: " + methodName
);
this.callPluginMethod(callbackId, pluginId, methodName, methodData);
}
} catch (Exception ex) {
Logger.error("Post message error:", ex);
}
}
// 根据配置和Webview是否支持, 选择使用 JavaScriptReplyProxy 或者 evaluateJavascript 的方式给 js 发消息
public void sendResponseMessage(PluginCall call, PluginResult successResult, PluginResult errorResult) {
try {
PluginResult data = new PluginResult();
data.put("save", call.isKeptAlive());
data.put("callbackId", call.getCallbackId());
data.put("pluginId", call.getPluginId());
data.put("methodName", call.getMethodName());
boolean pluginResultInError = errorResult != null;
if (pluginResultInError) {
data.put("success", false);
data.put("error", errorResult);
Logger.debug("Sending plugin error: " + data.toString());
} else {
data.put("success", true);
if (successResult != null) {
data.put("data", successResult);
}
}
boolean isValidCallbackId = !call.getCallbackId().equals(PluginCall.CALLBACK_ID_DANGLING);
if (isValidCallbackId) {
if (bridge.getConfig().isUsingLegacyBridge()) {
legacySendResponseMessage(data);
} else if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && javaScriptReplyProxy != null) {
javaScriptReplyProxy.postMessage(data.toString());
} else {
legacySendResponseMessage(data);
}
} else {
bridge.getApp().fireRestoredResult(data);
}
} catch (Exception ex) {
Logger.error("sendResponseMessage: error: " + ex);
}
if (!call.isKeptAlive()) {
call.release(bridge);
}
}
// 使用 evaluateJavascript 的方式给 js 发消息
private void legacySendResponseMessage(PluginResult data) {
final String runScript = "window.Capacitor.fromNative(" + data.toString() + ")";
final WebView webView = this.webView;
webView.post(() -> webView.evaluateJavascript(runScript, null));
}
// 调用 Capacitor 插件
private void callPluginMethod(String callbackId, String pluginId, String methodName, JSObject methodData) {
PluginCall call = new PluginCall(this, pluginId, callbackId, methodName, methodData);
bridge.callPluginMethod(pluginId, methodName, call);
}
// 调用 Cordova 插件
private void callCordovaPluginMethod(String callbackId, String service, String action, String actionArgs) {
bridge.execute(
() -> {
cordovaPluginManager.exec(service, action, callbackId, actionArgs);
}
);
}
}
2.2 iOS
iOS 平台下,桥接代码在 Pods -> Development Pods -> Capacitor
下(真实路径为 打包工程根目录/node_moduls/@capacitor/ios/Capacitor/Capacitor 文件夹下)。
2.2.1 CAPBridgeViewController
我们也是从入口文件开始看,Capacitor创建的打包工程入口文件使用 Main.storyboard
实现,可以看到里面加载的是 CAPBridgeViewController
。
在 loadView()
中,可以看到,我们上面提到的 WebViewDelegationHandler
在这里实例化并作为参数传递到 prepareWebView()
和 CapacitorBridge
中。
CapacitorBridge 稍后再谈,在 prepareWebview()
中把 WKWebview 的 uiDelegate
和 navigationDelegate
设置为了 WebViewDelegationHandler
,这部分也是WKWebview的通用操作。
override public final func loadView() {
// load the configuration and set the logging flag
let configDescriptor = instanceDescriptor()
let configuration = InstanceConfiguration(with: configDescriptor, isDebug: CapacitorBridge.isDevEnvironment)
CAPLog.enableLogging = configuration.loggingEnabled
// get the web view
let assetHandler = WebViewAssetHandler(router: router())
assetHandler.setAssetPath(configuration.appLocation.path)
assetHandler.setServerUrl(configuration.serverURL)
// 之前我们也提到了,在 WebViewDelegationHandler 中定义了 js 与 Native 的交互
// 这个 delegationHandler 也作为参数传递到了 WKWebview 和 CapacitorBridge 中
let delegationHandler = WebViewDelegationHandler()
// 对webview做一些初始化配置
prepareWebView(with: configuration, assetHandler: assetHandler, delegationHandler: delegationHandler)
view = webView
// create the bridge
capacitorBridge = CapacitorBridge(with: configuration,
delegate: self,
cordovaConfiguration: configDescriptor.cordovaConfiguration,
assetHandler: assetHandler,
delegationHandler: delegationHandler)
capacitorDidLoad()
}
extension CAPBridgeViewController {
private func prepareWebView(with configuration: InstanceConfiguration, assetHandler: WebViewAssetHandler, delegationHandler: WebViewDelegationHandler) {
...
// set our delegates
aWebView.uiDelegate = delegationHandler
aWebView.navigationDelegate = delegationHandler
}
}
2.2.2 WebViewDelegationHandler
WebViewDelegationHandler 继承了多个代理类,主要有 WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler
。
WKNavigationDelegate 用于页面加载时的一些生命周期方法;
WKUIDelegate 用于复写js原生的 alert、confirm、prompt 等方法;
WKScriptMessageHandler 则用于接收 window.webkit.messageHandlers.*.postMessage 的消息。
可以看到,在 userContentController didReceive
方法中,根据消息的内容,通过 CapacitorBridge
分别调用Capacitor 插件和 Cordova 插件。
部分代码如下:
import Foundation
import WebKit
// adopting a public protocol in an internal class is by design
// swiftlint:disable lower_acl_than_parent
@objc(CAPWebViewDelegationHandler)
internal class WebViewDelegationHandler: NSObject, WKNavigationDelegate, WKUIDelegate, WKScriptMessageHandler {
weak var bridge: CapacitorBridge?
fileprivate(set) var contentController = WKUserContentController()
enum WebViewLoadingState {
case unloaded
case initialLoad(isOpaque: Bool)
case subsequentLoad
}
fileprivate(set) var webViewLoadingState = WebViewLoadingState.unloaded
private let handlerName = "bridge"
init(bridge: CapacitorBridge? = nil) {
super.init()
self.bridge = bridge
// 这里 add 后,就可以在js中使用 window.webkit.messageHandlers.bridge.postMessage
contentController.add(self, name: handlerName)
}
func cleanUp() {
contentController.removeScriptMessageHandler(forName: handlerName)
}
// MARK: - WKNavigationDelegate
// 判断是否放行请求
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
...
}
// MARK: - WKScriptMessageHandler
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let bridge = bridge else {
return
}
let body = message.body
if let dict = body as? [String: Any] {
let type = dict["type"] as? String ?? ""
if type == "js.error" {
if let error = dict["error"] as? [String: Any] {
logJSError(error)
}
} else if type == "message" {
let pluginId = dict["pluginId"] as? String ?? ""
let method = dict["methodName"] as? String ?? ""
let callbackId = dict["callbackId"] as? String ?? ""
let options = dict["options"] as? [String: Any] ?? [:]
if pluginId != "Console" {
CAPLog.print("⚡️ To Native -> ", pluginId, method, callbackId)
}
bridge.handleJSCall(call: JSCall(options: options, pluginId: pluginId, method: method, callbackId: callbackId))
} else if type == "cordova" {
let pluginId = dict["service"] as? String ?? ""
let method = dict["action"] as? String ?? ""
let callbackId = dict["callbackId"] as? String ?? ""
let args = dict["actionArgs"] as? Array ?? []
let options = ["options": args]
CAPLog.print("To Native Cordova -> ", pluginId, method, callbackId, options)
bridge.handleCordovaJSCall(call: JSCall(options: options, pluginId: pluginId, method: method, callbackId: callbackId))
}
}
}
// MARK: - WKUIDelegate
// 使用 window.alert() 需实现此方法
public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
...
}
// 使用 window.confirm() 需实现此方法
public func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (Bool) -> Void) {
...
}
// 使用 window.prompt() 需实现此方法
public func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping (String?) -> Void) {
...
}
public func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
if let url = navigationAction.request.url {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
return nil
}
}
2.2.3 CapacitorBridge
init
首先看下初始化方法,主要是一些变量的初始化,以及插件等到初始化。
exportCoreJS:将 window.Capacitor、window.WEBVIEW_SERVER_URL、native-bridge.js 注入到 js;
registerPlugins:将Capacitor插件变量和插件方法注入到 js;
setupCordovaCompatibility:将 cordova.js、cordova_plugins.js、插件js,注入到js
init(with configuration: InstanceConfiguration, delegate bridgeDelegate: CAPBridgeDelegate, cordovaConfiguration: CDVConfigParser, assetHandler: WebViewAssetHandler, delegationHandler: WebViewDelegationHandler, autoRegisterPlugins: Bool = true) {
self.bridgeDelegate = bridgeDelegate
self.webViewAssetHandler = assetHandler
self.webViewDelegationHandler = delegationHandler
self.config = configuration
self.cordovaParser = cordovaConfiguration
self.notificationRouter = NotificationRouter()
self.notificationRouter.handleApplicationNotifications = configuration.handleApplicationNotifications
self.autoRegisterPlugins = autoRegisterPlugins
super.init()
self.webViewDelegationHandler.bridge = self
// 将 window.Capacitor、window.WEBVIEW_SERVER_URL、native-bridge.js 注入到 js
exportCoreJS(localUrl: configuration.localURL.absoluteString)
// Capacitor插件注入到js
registerPlugins()
// 将 cordova.js、cordova_plugins.js、插件js,注入到js
setupCordovaCompatibility()
observers.append(NotificationCenter.default.addObserver(forName: type(of: self).tmpVCAppeared.name, object: .none, queue: .none) { [weak self] _ in
self?.tmpWindow = nil
})
self.setupWebDebugging(configuration: configuration)
}
handleJSCall
此方法用来调用Capacitor 插件的接口。
通过 pluginId
定位到指定插件 plugin,通过方法名获取 Selector
,使用plugin.perform(selector, with: pluginCall)
执行插件的方法。
/**
Handle a call from JavaScript. First, find the corresponding plugin, construct a selector,
and perform that selector on the plugin instance.
Quiet the length warning because we don't want to refactor the function at this time.
*/
// swiftlint:disable:next function_body_length
func handleJSCall(call: JSCall) {
let load = {
NSClassFromString(call.pluginId)
.flatMap { $0 as? CAPPlugin.Type }
.flatMap(self.loadPlugin(type:))
}
guard let plugin = plugins[call.pluginId] ?? load() else {
CAPLog.print("⚡️ Error loading plugin \(call.pluginId) for call. Check that the pluginId is correct")
return
}
let selector: Selector
if call.method == "addListener" || call.method == "removeListener" {
selector = NSSelectorFromString(call.method + ":")
} else {
guard let method = plugin.getMethod(named: call.method) else {
CAPLog.print("⚡️ Error calling method \(call.method) on plugin \(call.pluginId): No method found.")
CAPLog.print("⚡️ Ensure plugin method exists and uses @objc in its declaration, and has been defined")
return
}
selector = method.selector
}
// 确认插件可调用此selector
if !plugin.responds(to: selector) {
// we don't want to break up string literals
// swiftlint:disable line_length
CAPLog.print("⚡️ Error: Plugin \(plugin.getId()) does not respond to method call \"\(call.method)\" using selector \"\(selector)\".")
CAPLog.print("⚡️ Ensure plugin method exists, uses @objc in its declaration, and arguments match selector without callbacks in CAP_PLUGIN_METHOD.")
CAPLog.print("⚡️ Learn more: \(docLink(DocLinks.CAPPluginMethodSelector.rawValue))")
// swiftlint:enable line_length
return
}
// Create a plugin call object and handle the success/error callbacks
dispatchQueue.async { [weak self] in
// let startTime = CFAbsoluteTimeGetCurrent()
let pluginCall = CAPPluginCall(callbackId: call.callbackId,
options: JSTypes.coerceDictionaryToJSObject(call.options,
formattingDatesAsStrings: plugin.shouldStringifyDatesInCalls) ?? [:],
success: {(result: CAPPluginCallResult?, pluginCall: CAPPluginCall?) -> Void in
if let result = result {
self?.toJs(result: JSResult(call: call, callResult: result), save: pluginCall?.keepAlive ?? false)
} else {
self?.toJs(result: JSResult(call: call, result: .dictionary([:])), save: pluginCall?.keepAlive ?? false)
}
}, error: {(error: CAPPluginCallError?) -> Void in
if let error = error {
self?.toJsError(error: JSResultError(call: call, callError: error))
} else {
self?.toJsError(error: JSResultError(call: call,
errorMessage: "",
errorDescription: "",
errorCode: nil,
result: .dictionary([:])))
}
})
if let pluginCall = pluginCall {
plugin.perform(selector, with: pluginCall)
if pluginCall.keepAlive {
self?.saveCall(pluginCall)
}
}
// let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
// CAPLog.print("Native call took", timeElapsed)
}
}
handleCordovaJSCall
此方法用来调用Cordova 插件的接口。由于Capacitor和Cordova的插件参数和回调还是有区别的,所以也是分成两个方法,不过原理大致一致。
通过 CordovaPluginManager
和 插件id 获取插件变量,通过方法名获取 selector
,最终也是通过 plugin.perform(selector, with: pluginCall)
执行插件中定义的方法。
func handleCordovaJSCall(call: JSCall) {
// Create a selector to send to the plugin
if let plugin = self.cordovaPluginManager?.getCommandInstance(call.pluginId.lowercased()) {
let selector = NSSelectorFromString("\(call.method):")
if !plugin.responds(to: selector) {
CAPLog.print("Error: Plugin \(plugin.className ?? "") does not respond to method call \(selector).")
CAPLog.print("Ensure plugin method exists and uses @objc in its declaration")
return
}
let arguments: [Any] = call.options["options"] as? [Any] ?? []
let pluginCall = CDVInvokedUrlCommand(arguments: arguments,
callbackId: call.callbackId,
className: plugin.className,
methodName: call.method)
plugin.perform(selector, with: pluginCall)
} else {
CAPLog.print("Error: Cordova Plugin mapping not found")
return
}
}