test-capacitor-openharmony 8.0.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +1657 -0
  2. package/package.json +30 -0
package/README.md ADDED
@@ -0,0 +1,1657 @@
1
+ # capacitor-openharmony框架说明
2
+
3
+ <div align="left">
4
+
5
+ capacitor-openharmony是capacitor的鸿蒙化版本,所有接口兼容Capacitor的Android和iOS版本
6
+
7
+ ## 本文档说明
8
+
9
+ 本文档仅说明capacitor-openharmony框架部分的使用手册、开发说明,集成步骤等。
10
+
11
+ ## 开发说明
12
+
13
+ capacitor-openharmony是capacitor的鸿蒙化版本,并支持ArkTS侧和C/C++侧自定义插件研发,框架采用C/C++研发,底层使用自研Socket TCP/IP通讯,封装了HTTP/HTTPS协议通讯解决各种跨域访问问题,无需配置web服务端,同时结合webview的通讯协议栈,大大提高应用层网络请求效率。
14
+
15
+ ## 附加说明
16
+
17
+ capacitor-openharmony使用多页面视图研发,同时兼容Android和iOS原有的单页面视图,原有项目可以轻松移植;另外在复杂项目中,可以使用capacitor-openharmony的多页面视图功能,创建多个webview协同工作。
18
+
19
+ ## 开发背景
20
+
21
+ capacitor官方网站:[https://capacitorjs.com/],是移动端跨平台的新框架,大量厂商直接或间接采用此框架开发APP;但是目前不支持HarmonyOS Next版本,开发者将原Android和iOS项目移植到HarmonyOS Next版,无法适配,为此我们研发了capacitor-openharmony,遵守capacitor官方标准,原有项目无需投入任何研发轻松移植到鸿蒙系统;新开发的项目,一次研发就适用于Android、iOS和HarmonyOS三大平台,也节省了大量的时间和人力成本。
22
+ 本框架为cordova-openharmony框架的升级版本,沿用了部分cordova-openharmony框架的能力,在插件和通信部分针对Capacitor做了优化,部分功能仍沿用cordova-openharmony框架。
23
+
24
+ ---
25
+
26
+ ## capacitor-openharmony源码集成使用说明
27
+
28
+ ### 1. 创建项目
29
+
30
+ 打开DevEco创建项目,选择Empty Ability进入下一步(next),填写必要信息,点击完成(finish),工程创建完成。
31
+
32
+ ### 2. 集成源码
33
+
34
+ 下载本工程,放入主工程文件夹中,此时在DevEco中已经可以看到capacitor的模块工程了。
35
+
36
+ ### 3. 引入依赖
37
+
38
+ 在根目录的oh-package.json中引入依赖:
39
+ ```json
40
+ {
41
+ "dependencies": {
42
+ "capacitor-openharmony": "file:./capacitor"
43
+ }
44
+ }
45
+ ```
46
+
47
+ 然后再修改entry/build-profile.json5(项目级)配置文件,在modules模块中增加:
48
+
49
+ ```json5
50
+ {
51
+ "name": "capacitor",
52
+ "srcPath": "./capacitor",
53
+ }
54
+ ```
55
+
56
+ 以上三步操作后,已经在主工程中集成了capacitor的源码了。
57
+
58
+ ### 4. 项目移植
59
+
60
+ **前端工程打包**
61
+
62
+ 为提升项目部署灵活性,需将前端工程的资源引用及路由跳转逻辑从“绝对根路径依赖”调整为"相对路径引用":
63
+
64
+ 调整页面基准路径:
65
+
66
+ 1. 在项目根目录的 index.html 文件中,将 <base> 标签的 href 属性由默认的绝对根路径 / 修改为相对路径 ./,作为页面所有相对路径资源的基准锚点;
67
+ 2. 配置打包输出路径:如在Vue工程中,将 vue.config.js 配置文件中,将控制 Webpack 静态资源打包路径的核心属性 publicPath 由默认的 / 调整为 ./,确保打包后 JS、CSS、图片等静态资源均采用相对路径引用。
68
+
69
+ **Android项目移植:**
70
+
71
+ 复制原有Android studio的工程assets目录下面的所有文件到鸿蒙工程entry/src/main/resources/rawfile目录下,原Android工程的assets目录包含config.xml(如果有)、 capacitor.config.json(必须)、 capacitor.plugins.json(必须)和dist目录,将dist文件夹名修改为www,www目录包含index.html(必须)、cordova.js(如果有)、cordova_plugins.js(如果有)、[native-bridge.js](https://github.com/ionic-team/capacitor/blob/8.0.0/android/capacitor/src/main/assets/native-bridge.js)(手动引入)、css目录、js目录等,如果要指定加载页面,不使用默认页面,请查看高级功能部分说明。复制成功后,仍需要安装Android包含的鸿蒙版插件。
72
+
73
+ 其中 [native-bridge.js](https://github.com/ionic-team/capacitor/blob/8.0.0/android/capacitor/src/main/assets/native-bridge.js) 复用安卓侧js代码,手动放置于如rawfile/www/ 目录之下,和index.html目录同级。
74
+
75
+ **iOS项目移植:**
76
+
77
+ 第一步:复制原有IOS App目录下的public文件夹到鸿蒙工程entry/src/main/resources/rawfile目录下,将public文件夹名修改为www,文件包含:index.html(必须)、cordova.js(如果有)、cordova_plugins.js(如果有)、[native-bridge.js](https://github.com/ionic-team/capacitor/blob/8.0.0/android/capacitor/src/main/assets/native-bridge.js)(手动引入)、css目录、js目录等
78
+
79
+ 第二步:Xcode工程的配置文件在App目录下,Xcode工程的该文件不能直接被capacitor-openharmony使用,需要进行转换,该文件主要记录的是框架配置信息、插件的名称和初始化的类,因为鸿蒙版是根据android的配置文件进行插件初始化的,因此需要将Xcode工程配置文件转为安卓的配置文件,请将Xcode工程使用node加入安卓平台,系统会自动生成android版的config.xml(如果有)、 capacitor.config.json(必须)、 capacitor.plugins.json(必须)。然后将文件复制到鸿蒙版工程的entry/src/main/resources/rawfile下。复制成功后,仍需要安装iOS包含的鸿蒙版插件。
80
+
81
+ 其中 [native-bridge.js](https://github.com/ionic-team/capacitor/blob/8.0.0/android/capacitor/src/main/assets/native-bridge.js) 复用安卓侧js代码,手动放置于如rawfile/www/ 目录之下,和index.html目录同级。
82
+
83
+ **新建项目:**
84
+
85
+ 如果您没有Android和iOS项目,需要使用Capacitor的框架,创建Android项目,创建成功后,再按照Android项目移植方法操作即可。
86
+
87
+ **添加鸿蒙配置:**
88
+
89
+ 在capacitor.config.json中添加harmony属性,在此可配置自定义配置。
90
+
91
+ ```json
92
+ {
93
+ "appId": "appId",
94
+ "appName": "appName",
95
+ "webDir": "www",
96
+ "harmony": {
97
+
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### 5. 修改Index.ets文件
103
+
104
+ 打开鸿蒙工程文件entry/src/main/etx/pages/Index.ets文件,修改代码如下(可以直接全部拷贝和复制到Index.ets文件中):
105
+
106
+ ```typescript
107
+ import { MainPage, pageBackPress, pageHideEvent, pageShowEvent, PluginEntry, MainPageOnBackPress} from 'capacitor-openharmony';
108
+ //import { TestPlugin } from "../plugins/TestPlugin" //自定义插件TestPlugin,根据实际情况导入自己的自定义插件
109
+ @Entry
110
+ @Component
111
+ struct Index {
112
+ //ArkTs侧的自定义插件:配置插件名称和对象,请查看自定义查看开发部分
113
+ cordovaPlugs:Array< PluginEntry> = [];
114
+ mainPageOnBackPress:MainPageOnBackPress = new MainPageOnBackPress();
115
+ /*
116
+ cordovaPlugs:Array< PluginEntry> =
117
+ [
118
+ {
119
+ pluginName: 'TestPlugin', //插件名称
120
+ pluginObject:new TestPlugin() //实例化插件对象供框架调用
121
+ }
122
+ ];
123
+ */
124
+ onPageShow(){
125
+ pageShowEvent(); //页面显示通知框架
126
+ }
127
+ onBackPress() {
128
+ return this.mainPageOnBackPress.backPress();
129
+ }
130
+ onPageHide() {
131
+ pageHideEvent(); //页面隐藏通知框架
132
+ }
133
+ build() {
134
+ RelativeContainer() {
135
+ //默认加载rawfile/www/index.html
136
+ //如果要指定加载页面参考高级功能部分
137
+ MainPage({isWebDebug:false,cordovaPlugs:this.cordovaPlugs});
138
+ }
139
+ .height('100%')
140
+ .width('100%')
141
+ }
142
+ }
143
+ ```
144
+
145
+ ### 6. 修改EntryAbility.ets文件
146
+
147
+ 打开鸿蒙工程文件/entry/src/main/ets/entryAbility/EntryAbility.ets文件,修改onCreate函数如下:
148
+
149
+ ```typescript
150
+ import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
151
+ import { hilog } from '@kit.PerformanceAnalysisKit';
152
+ import { window } from '@kit.ArkUI';
153
+ import { webview } from '@kit.ArkWeb'; //引入webview
154
+
155
+ ... //省略部分代码
156
+ onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
157
+ hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
158
+ webview.WebviewController.initializeWebEngine();//webview引擎初始化
159
+ }
160
+ ```
161
+
162
+ ### 7. 完成
163
+
164
+ 做以上代码修改后,鸿蒙的移植已经完毕,可以使用模拟器或者真机进行编译和测试了。
165
+
166
+ ---
167
+
168
+ ## 高级用法,区别于Android和iOS
169
+
170
+ ### 1. MainPage传入indexPage参数设置自定义启动路径,支持rawfile、resfile和沙箱路径
171
+
172
+ ```typescript
173
+ /*
174
+ *indexPage:默认启动首页,举例如下:
175
+ * "/www/index.html":rawfile目录下的文件
176
+ * "/data/storage/el2/base/files/www/index.html":使用虚拟域名www.example.com加载沙箱路径下的文件,
177
+ * "https://cn.bing.com":加载在线网页,必须指定https或者http
178
+ * "file:///data/storage/el2/base/files/www/index.html":file协议加载el2级别沙箱路径文件
179
+ * "file:///data/storage/el1/bundle/entry/resources/resfile/www/index.html":file协议加载el1级别沙箱路径文件
180
+ * "file://"+getContext().resourceDir+"/www/index.html":file协议加载el1级别沙箱路径文件
181
+ * capacitor支持使用虚拟域名www.example.com加载本地文件,也支持使用file协议加载本地文件
182
+ * 改变this.indexPage的值,webview会重新加载页面
183
+ */
184
+ //省略其他代码
185
+ MainPage({indexPage:"/www/index.html"});
186
+ //省略其他代码
187
+ ```
188
+
189
+ ### 2. 注册自定义用户scheme,用于capacitor内部拦截scheme的请求
190
+
191
+ ```typescript
192
+ import { RegisterCustomSchemes } from 'capacitor-openharmony';
193
+ onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
194
+ hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
195
+ RegisterCustomSchemes("cmp"); //注册自定义scheme
196
+ webview.WebviewController.initializeWebEngine();//webview引擎初始化
197
+ }
198
+ ```
199
+
200
+ ```typescript
201
+ //customSchemes:自定义scheme,多个scheme用","分隔
202
+ //省略其他代码
203
+ MainPage({customSchemes:"cmp,xmp,xxx"});
204
+ //省略其他代码
205
+ ```
206
+
207
+ ### 3. 拦截自定义的scheme,在webview端拦截并处理,也可以拦截http(s)请求处理
208
+
209
+ ```typescript
210
+ /*
211
+ *拦截请求函数,根据需要拦截相应请求,一般用于自定义scheme,如果存在自定义scheme的必须使用此函数拦截处理
212
+ *使用此函数拦截自己的scheme进行处理,也可以在MainPage生命周期回调函数中拦截处理,二选一,不能同时拦截处理。
213
+ *拦截后处理有两种方式,推荐使用第一种方式
214
+ * 1,capacitor webview内核处理,返回null,capacitor可以处理替换所有资源,例如在线资源,本地资源,js、img、css等
215
+ * 2,自己处理,返回WebResourceRequest
216
+ *说明如下:
217
+ * 1,子组件的回调函数不能使用this指针,如果要使用this,请参考parentPage参数
218
+ * 2,采用第一种方式,写法简单,且效率高,推荐第一种方式
219
+ */
220
+ onInterceptWebRequest(request: WebResourceRequest, webTag:string):ESObject {
221
+ let url = request.getRequestUrl();
222
+ //capacitor webview内核处理替换
223
+ if (url == "cmp://v1.1.1/temp/test2.png") {
224
+ /*
225
+ *替换资源说明如下:
226
+ *本地资源请使用https://www.example.com的虚拟域名作为访问本地资源的标记
227
+ *详细了解www.example.com内置虚拟域名规则,查看最后面的常见问题说明
228
+ *被替换和替换内容可以是图片、css、js等
229
+ *替换资源举例如下:
230
+ *1,沙箱路径
231
+ * https://www.example.com/data/storage/el2/base/files/test.png
232
+ *2,rawfile目录的下的资源文件
233
+ * https://www.example.com/www/test.png
234
+ *3,网络在线资源
235
+ * https://www.chuzhitong.com/images/logo.png
236
+ *4,cdvfile协议的沙箱路径的文件,绝对路径
237
+ * cdvfile:///data/storage/el2/base/files/test.png
238
+ *此函数是通知capacitor webview内核,后续加载页面实施资源替换
239
+ */
240
+ SetResourceReplace(webTag, url, "https://www.chuzhitong.com/images/logo.png");
241
+ }
242
+
243
+ //自己处理资源返回webview
244
+ if(url == "https://www.ext.com/v1.1.1/temp/test3.png") {
245
+ let response = new WebResourceResponse();
246
+ response.setResponseData($rawfile("www/picture/bao.png"));
247
+ response.setResponseEncoding('utf-8');
248
+ response.setResponseMimeType("image/png");
249
+ response.setResponseCode(200);
250
+ response.setReasonMessage('OK');
251
+ response.setResponseIsReady(true);
252
+ return response;
253
+ }
254
+ return null;
255
+ }
256
+
257
+ //省略其他代码
258
+ /*
259
+ *onInterceptWebRequest 返回null放行,返回具体的WebResourceResponse
260
+ * 参考https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/web-cross-origin-V5 说明
261
+ */
262
+ MainPage({onInterceptWebRequest:this.onInterceptWebRequest});
263
+ //省略其他代码
264
+ ```
265
+
266
+ ### 4. 在原生层,动态设置webview属性
267
+
268
+ ```typescript
269
+ /*
270
+ *在原生层页面加载后,网页面中注入新的js,也可以在mainPage的生命周期页面加载完毕后注入js
271
+ */
272
+ onSetCordovaWebAttribute(cordovaWebView:CordovaWebView) {
273
+ if(cordovaWebView) {
274
+ //获取webview属性变量,用于动态修改webview属性,具体参考如下连接,页面加载完成后触发
275
+ //鸿蒙并不支持WebAttribute组件属性的动态设置,但是可以设置部分属性,不支持的属性会抛出"Method not implemented."、"is not callable"等异常信息
276
+ //https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5
277
+ //https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-universal-attributes-attribute-modifier#attributemodifier
278
+ cordovaWebView!.getWebAttribute()?.height('50%');
279
+ //获取webview的控制变量,用于实现具体的功能,示例代码实现在webviw执行js或者注入新的js,具体参考如下连接
280
+ //https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-basic-components-web-V5
281
+ cordovaWebView!.getWebviewController().runJavaScript("alert(1);");
282
+ }
283
+ //省略其他代码
284
+ MainPage({onSetCordovaWebAttribute:this.onSetCordovaWebAttribute});
285
+ //省略其他代码
286
+ ```
287
+
288
+ ### 5. 多webview界面,即多页面视图,自定义webId,使用自定义插件各webview之间通讯,可用于平板等大屏幕研发需求
289
+
290
+ ```typescript
291
+ //省略其他代码
292
+ //webId:自定义webId,用于多webview,各webview之间通讯,webId确保唯一,参考自定义插件研发示例代码
293
+ MainPage({ webId:"123456"})
294
+ //省略其他代码
295
+ ```
296
+
297
+ ### 6. 动态创建组件,在webview和NodeController相结合实现动态创建和显示组件时,切记一定要设定webId参数,避免重复创建webview
298
+
299
+ ```typescript
300
+ //动态创建MainPage的示例代码,主要用于原生界面和webview界面显示在同一个视图里面的混合式研发
301
+ //如果要传入其他参数,参考此文档详细了解
302
+ //https://developer.huawei.com/consumer/cn/doc/best-practices-V5/bpta-ui-dynamic-operations-V5
303
+ @Builder
304
+ function buildMainPage() {
305
+ Column() {
306
+ //直接加载在线网站
307
+ MainPage({webId:"123456", indexPage:"https://cn.bing.com", cordovaPlugs:[
308
+ {
309
+ pluginName: 'TestPlugin', //插件名称
310
+ pluginObject:new TestPlugin() //实例化插件对象
311
+ }
312
+ ]});
313
+ }.width("100%").height("100%")
314
+ }
315
+
316
+ class TextNodeController extends NodeController {
317
+ private textNode: BuilderNode<[]> | null = null;
318
+
319
+ constructor() {
320
+ super();
321
+ }
322
+
323
+ makeNode(context: UIContext): FrameNode | null {
324
+ // 创建BuilderNode实例
325
+ this.textNode = new BuilderNode(context);
326
+ this.textNode.build(wrapBuilder<[]>(buildMainPage));
327
+ // 返回需要显示的节点
328
+ return this.textNode.getFrameNode();
329
+ }
330
+ }
331
+ ```
332
+
333
+ ```typescript
334
+ //省略其他代码
335
+ private textNodeController = new TextNodeController();
336
+
337
+ //省略其他代码
338
+ RelativeContainer() {
339
+ if (this.isShow) {
340
+ NodeContainer(this.textNodeController)
341
+ .width('100%')
342
+ .height("100%")
343
+ .backgroundColor('#FFF0F0F0')
344
+ }
345
+ }
346
+ .height('30%')
347
+ .width('100%')
348
+
349
+ Button("显示和隐藏web").onClick(()=>{
350
+ this.isShow = false;
351
+ })
352
+
353
+ ```
354
+
355
+ ### 7,W3C WEB授权webview权限,例如webview调起摄像头和麦克风
356
+
357
+ ```typescript
358
+ //Web组件可以通过W3C标准协议授权回调函数,例如拉起摄像头和麦克风,示例如下
359
+ onPermissionRequest(event:OnPermissionRequestEvent,parentPage?:object){
360
+ let page = parentPage as Index;//page为当前页面对象,相当于当前页面的this指针,使用该对象,必须将this指针传入到mainPage中
361
+ if (event) {
362
+ //拉起摄像头和麦克风,为确保用户拒绝后能二次拉起授权,需要多个授权时,单独分开授权
363
+ //单独分开授权会多次弹出窗口,仅供参考,也可以一次授权多个权限,但是用户拒绝后,无法拉起二次授权窗口
364
+ //授权摄像头和麦克风,弹窗授权
365
+ //const yourPermissions: Array< Permissions> = ['ohos.permission.CAMERA', 'ohos.permission.MICROPHONE'];
366
+ //授权加速度和陀螺仪,无弹窗用户无感知
367
+ const yourPermissions: Array< Permissions> = ['ohos.permission.ACCELEROMETER', 'ohos.permission.GYROSCOPE'];
368
+ for (let i = 0; i < yourPermissions.length; i++) {
369
+ let confirmPermissions: Array< Permissions> = [yourPermissions[i]];
370
+ let atManager = abilityAccessCtrl.createAtManager();
371
+ atManager.requestPermissionsFromUser(getContext(this), confirmPermissions).then((data) => {
372
+ let grantStatus: Array< number> = data.authResults;
373
+ if (grantStatus[0] != 0) {
374
+ // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
375
+ atManager.requestPermissionOnSetting(getContext() as common.UIAbilityContext, confirmPermissions)
376
+ .then((data: Array< abilityAccessCtrl.GrantStatus>) => {
377
+ if (data.length > 0 && data[0] == 0 ) {
378
+ event.request.grant(event.request.getAccessibleResource());
379
+ }
380
+ console.info('data:' + JSON.stringify(data));
381
+ })
382
+ .catch((err: BusinessError) => {
383
+ console.error('data:' + JSON.stringify(err));
384
+ return;
385
+ });
386
+ } else{
387
+ event.request.grant(event.request.getAccessibleResource());
388
+ }
389
+ }).catch((error: BusinessError) => {
390
+ console.error(`Failed to request permissions from user. Code is ${error.code}, message is ${error.message}`);
391
+ })
392
+ }
393
+ }
394
+ }
395
+
396
+ /*
397
+ *onPermissionRequest:web组件W3C标准拉起授权的回调函数
398
+ * 参考连接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/web-rtc-V5?catalogVersion=V5
399
+ */
400
+ MainPage({parentPage:this, onPermissionRequest:this.onPermissionRequest,});
401
+ ```
402
+
403
+ ### 8. 父组件感知MainPage子组件的所有生命周期,在不同的周期执行相应的操作
404
+
405
+ ```typescript
406
+ //MainPage的生命周期的各回调函数,根据业务需要设置单个或多个生命周期回调函数添加业务功能
407
+ //生命周期的说明参考:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/web-event-sequence-V5
408
+ mainPageCycle?:MainPageCycle;
409
+ aboutToAppear() {
410
+ this.mainPageCycle = new MainPageCycle()
411
+ .setOnAboutToAppear((webviewController: webview.WebviewController,parentPage?:object)=>{
412
+ //page为当前页面对象,相当于当前页面的this指针,使用该对象,必须将this指针通过parentPage参数传入mainPage中
413
+ let page = parentPage as Index;
414
+ console.log("exec onAboutToAppear");
415
+ })
416
+ .setOnControllerAttached((webviewController: webview.WebviewController,parentPage?:object)=>{
417
+ console.log("exec onControllerAttached");
418
+ })
419
+ .setOnLoadIntercept((webResourceRequest: WebResourceRequest,parentPage?:object):boolean=>{
420
+ console.log("exec onLoadIntercept");
421
+ return false;
422
+ })
423
+ .setOnOverrideUrlLoading((webResourceRequest: WebResourceRequest,parentPage?:object):boolean=>{
424
+ console.log("exec onOverrideUrlLoading");
425
+ return false;
426
+ })
427
+ .setOnInterceptRequest((request: WebResourceRequest, webTag:string,parentPage?:object):WebResourceResponse|null=>{
428
+ console.log("exec setOnInterceptRequest");
429
+ return null;
430
+ })
431
+ .setOnPageBegin((url:string,parentPage?:object):void=>{
432
+ console.log("exec onPageBegin");
433
+ })
434
+ .setOnProgressChange((newProgress: number,parentPage?:object):void=>{
435
+ console.log("exec onProgressChange");
436
+ })
437
+ .setOnPageEnd((url:string, webviewController: webview.WebviewController,parentPage?:object):void=>{
438
+ console.log("exec onPageEnd");
439
+ })
440
+ .setOnPageVisible((url:string,parentPage?:object):void=>{
441
+ console.log("exec onPageVisible");
442
+ })
443
+ .setOnRenderExited((renderExitReason:RenderExitReason,parentPage?:object):void=>{
444
+ console.log("exec onRenderExited");
445
+ })
446
+ .setOnDisAppear((parentPage?:object):void=>{
447
+ console.log("exec onDisAppear");
448
+ });
449
+ }
450
+
451
+ ```
452
+
453
+ ```typescript
454
+ //省略其他代码
455
+ /*
456
+ *lifeCycle:传入生命周期对象,让父组件感知MainPage的生命周期,进行相应业务处理
457
+ *parentPage:传入this,就是webview父组件对象,也就是当前组件的对象,可以在插件里面调用
458
+ */
459
+ MainPage({lifeCycle:this.mainPageCycle, parentPage:this})
460
+ //省略其他代码
461
+ ```
462
+
463
+ ### 9. 在同一个Page中加载多个webview,实现本地、在线页面混合研发
464
+
465
+ ```typescript
466
+ build() {
467
+ Column() {
468
+ RelativeContainer() {
469
+ MainPage({indexPage:"/www/index.html"});
470
+ }
471
+ .height('30%')
472
+ .width('100%')
473
+ RelativeContainer() {
474
+ MainPage({indexPage:"https://developer.huawei.com"});
475
+ }
476
+ .height('30%')
477
+ .width('100%')
478
+ }
479
+ }
480
+ ```
481
+
482
+ ### 10. 加载不包含cordova.js和capacitor.js页面,父组件控制webview的返回键,或者自己控制路由
483
+
484
+ ```typescript
485
+ /*
486
+ *控制mainPage的页面返回,需将此对象传入MainPage
487
+ *如果加载的页面不包含cordova.js和capacitor.js,使用pageBackPress无法通知capacitor返回,必须使用此对象控制页面返回
488
+ *也可以通过此对象控制webview的路由
489
+ */
490
+ mainPageOnBackPress:MainPageOnBackPress = new MainPageOnBackPress();
491
+
492
+ onBackPress() {
493
+ pageBackPress();
494
+ /*
495
+ *如果加载的页面没有包含cordova.js和capacitor.js,例如加载https://cn.bing.com,
496
+ * 返回值
497
+ * true:已经到了页面等层
498
+ * false:返回了上一页
499
+ */
500
+ //return this.mainPageOnBackPress.backPress();
501
+ return true;
502
+ }
503
+ //backPress:传入控制webview路由的对象,加载的页面不包含cordova.js和capacitor.js时控制webview路由,需要传
504
+ mainPage({ backPress: this.mainPageOnBackPress })
505
+ ```
506
+
507
+ ### 11. MainPage的路由开关控制,便于MainPage嵌套使用,路由子原生页面内再嵌套使用MainPage
508
+
509
+ ```typescript
510
+ /*
511
+ *isNavPath:true使用MainPage组件内的路由,默认是true,false:不使用MainPage内的路由,
512
+ * 特别是MainPage嵌套使用时,父组件要打开路由,子组件关闭路由,否则会路由冲突
513
+ */
514
+ MainPage({isNavPath:false});
515
+ ```
516
+
517
+ ### 12. 自定义cookie,传入cookie键值对
518
+
519
+ ```typescript
520
+ //手动添加cookie,在发送POST或者Get请求时携带cookie,https的session cookie无需手动设置,cordova会自动处理
521
+ //http的session cookie参考最后的https的cookie说明
522
+ this.cookies.set("https://mem.tongecn.com", ["key1=value1; path=/; Domain=.tongecn.com", "key2=value2"]);
523
+
524
+ /*
525
+ *cookies:如果ArkTs侧有自定义的cookie,可以通过此参数传入
526
+ * 一般情况下cookie都是cordova自动处理的,无需ArkTS侧手动设置,不过ArkTS侧通过此参数可以手动设置cookie
527
+ * 如果您的请求是采用的http协议非https,分为跨域请求和非跨域请求,请查看最后的常见问题说明
528
+ */
529
+ MainPage({cookies:this.cookies});
530
+ ```
531
+
532
+ ### 13. 自定义webview字体大小缩放百分比,支持适老化,屏蔽跟随系统字体大小变化
533
+
534
+ ```typescript
535
+ /*
536
+ *textZoomRatio:webview字体放大缩小百分比,默认是100保持默认
537
+ * 设置webview不跟随系统字体大小、并且屏蔽跟随显示大小缩放后
538
+ * 可以通过此参数统一设置webview字体大小变化百分比,避免页面错乱
539
+ * 也可以通过鸿蒙Device插件增加的字体大小百分比接口函数,在js侧设置,参考Device插件
540
+ * 参考常见问题屏蔽跟随系统字体大小和屏蔽跟随显示大小缩放
541
+ */
542
+ MainPage({textZoomRatio:110});
543
+ ```
544
+
545
+ ### 14. 同层渲染,以及同层渲染组件和插件结合的使用的方法
546
+
547
+ ```typescript
548
+ //同层渲染示例代码,H5页面增加一个原生的TextInput组件
549
+ @Observed
550
+ declare class Params{
551
+ elementId: string
552
+ textOne: string
553
+ textTwo: string
554
+ width: number
555
+ height: number
556
+ onTextChange?: (value: string) => void;
557
+ }
558
+
559
+ @Component
560
+ struct TextInputComponent {
561
+ @Prop params: Params
562
+ @State bkColor: Color = Color.Blue
563
+
564
+ build() {
565
+ Column() {
566
+ TextInput({text: '', placeholder: 'please input your word...'})
567
+ .placeholderColor(Color.Gray)
568
+ .id(this.params?.elementId)
569
+ .placeholderFont({size: 13, weight: 400})
570
+ .caretColor(Color.Gray)
571
+ .width(this.params?.width)
572
+ .height(this.params?.height)
573
+ .fontSize(14)
574
+ .fontColor(Color.Black)
575
+ .onChange((value:string)=>{
576
+ if (this.params.onTextChange) {
577
+ this.params.onTextChange(value); // 触发回调
578
+ }
579
+ })
580
+ }
581
+ //自定义组件中的最外层容器组件宽高应该为同层标签的宽高
582
+ .width(this.params.width)
583
+ .height(this.params.height)
584
+ }
585
+ }
586
+
587
+ @Builder
588
+ function TextInputBuilder(params:Params) {
589
+ TextInputComponent({params: params})
590
+ .width(params.width)
591
+ .height(params.height)
592
+ .backgroundColor(Color.White)
593
+ }
594
+
595
+ class MyNodeController extends NodeController {
596
+ private rootNode: BuilderNode<[Params]> | undefined | null;
597
+ private embedId_: string = "";
598
+ private surfaceId_: string = "";
599
+ private renderType_: NodeRenderType = NodeRenderType.RENDER_TYPE_DISPLAY;
600
+ private width_: number = 0;
601
+ private height_: number = 0;
602
+ private type_: string = "";
603
+ private isDestroy_: boolean = false;
604
+
605
+ setRenderOption(params: ESObject) {
606
+ this.surfaceId_ = params.surfaceId;
607
+ this.renderType_ = params.renderType;
608
+ this.embedId_ = params.embedId;
609
+ this.width_ = params.width;
610
+ this.height_ = params.height;
611
+ this.type_ = params.type;
612
+ }
613
+
614
+ // 必须要重写的方法,用于构建节点数、返回节点数挂载在对应NodeContainer中。
615
+ // 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新。
616
+ makeNode(uiContext: UIContext): FrameNode | null {
617
+ if (this.isDestroy_) { // rootNode为null
618
+ return null;
619
+ }
620
+ if (!this.rootNode) {// rootNode 为undefined时
621
+ this.rootNode = new BuilderNode(uiContext, { surfaceId: this.surfaceId_, type: this.renderType_ });
622
+ if(this.rootNode) {
623
+ this.rootNode.build(wrapBuilder(TextInputBuilder),
624
+ {textOne: "myTextInput", width: this.width_, height: this.height_, onTextChange:(value:string)=>{
625
+ //TextInput值该表后,通知js侧,这里只是列举了一个简单的例子,以实际情况执行js代码
626
+ let jsFun:string = "setValue('"+value+"')";
627
+ try {
628
+ this.cordovaWebView?.getWebviewController().runJavaScript(jsFun);
629
+ } catch (error) {
630
+ console.log(error);
631
+ }
632
+ }})
633
+ return this.rootNode.getFrameNode();
634
+ }else{
635
+ return null;
636
+ }
637
+ }
638
+ return this.rootNode.getFrameNode();
639
+ }
640
+
641
+ updateNode(arg: Object): void {
642
+ this.rootNode?.update(arg);
643
+ }
644
+
645
+ getEmbedId(): string {
646
+ return this.embedId_;
647
+ }
648
+
649
+ setDestroy(isDestroy: boolean): void {
650
+ this.isDestroy_ = isDestroy;
651
+ if (this.isDestroy_) {
652
+ this.rootNode = null;
653
+ }
654
+ }
655
+
656
+ postEvent(event: TouchEvent | undefined): boolean {
657
+ return this.rootNode?.postTouchEvent(event) as boolean
658
+ }
659
+ }
660
+ ```
661
+
662
+ ```typescript
663
+ @Entry
664
+ @Component
665
+ export struct Index {
666
+ //省略其他代码
667
+ public nodeControllerMap: Map<string, MyNodeController> = new Map();
668
+ @State componentIdArr: Array<string> = [];
669
+ @State widthMap: Map<string, number> = new Map();
670
+ @State heightMap: Map<string, number> = new Map();
671
+ @State positionMap: Map<string, Edges> = new Map();
672
+ @State edges: Edges = {};
673
+ @State textValue:string = "hello";
674
+ /*
675
+ *同层渲染生命周期回调函数
676
+ */
677
+ onNativeEmbedLifecycleChange(embed: NativeEmbedDataInfo,cordovaWebView:CordovaWebView,parentPage?:object) {
678
+ let page = parentPage as Index;//page为当前页面对象,相当于当前页面的this指针,使用该对象,必须将this指针传入到mainPage中
679
+ console.log("NativeEmbed surfaceId" + embed.surfaceId);
680
+ // 如果使用embed.info.id作为映射nodeController的key,请在h5页面显式指定id
681
+ const componentId = embed.info?.id?.toString() as string
682
+ if (embed.status == NativeEmbedStatus.CREATE) {
683
+ console.log("NativeEmbed create" + JSON.stringify(embed.info));
684
+ // 创建节点控制器、设置参数并rebuild
685
+ let nodeController = new MyNodeController()
686
+ // embed.info.width和embed.info.height单位是px格式,需要转换成ets侧的默认单位vp
687
+ nodeController.setRenderOption({surfaceId : embed.surfaceId as string,
688
+ type : embed.info?.type as string,
689
+ renderType : NodeRenderType.RENDER_TYPE_TEXTURE,
690
+ embedId : embed.embedId as string,
691
+ width : cordovaWebView.getUIContext().px2vp(embed.info?.width),
692
+ height : cordovaWebView.getUIContext().px2vp(embed.info?.height),
693
+ cordovaWebView:cordovaWebView,
694
+ textValue:page.textValue
695
+ })
696
+ page.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`}
697
+ nodeController.setDestroy(false);
698
+ //根据web传入的embed的id属性作为key,将nodeController存入Map
699
+ page.nodeControllerMap.set(componentId, nodeController);
700
+ page.widthMap.set(componentId, cordovaWebView.getUIContext().px2vp(embed.info?.width));
701
+ page.heightMap.set(componentId, cordovaWebView.getUIContext().px2vp(embed.info?.height));
702
+ page.positionMap.set(componentId, page.edges);
703
+ // 将web传入的embed的id属性存入@State状态数组变量中,用于动态创建nodeContainer节点容器,需要将push动作放在set之后
704
+ page.componentIdArr.push(componentId)
705
+ } else if (embed.status == NativeEmbedStatus.UPDATE) {
706
+ let nodeController = page.nodeControllerMap.get(componentId);
707
+ console.log("NativeEmbed update" + JSON.stringify(embed));
708
+ page.edges = {left: `${embed.info?.position?.x as number}px`, top: `${embed.info?.position?.y as number}px`}
709
+ page.positionMap.set(componentId, page.edges);
710
+ page.widthMap.set(componentId, cordovaWebView.getUIContext().px2vp(embed.info?.width));
711
+ page.heightMap.set(componentId, cordovaWebView.getUIContext().px2vp(embed.info?.height));
712
+ nodeController?.updateNode({page:page, textOne: 'update', width: cordovaWebView.getUIContext().px2vp(embed.info?.width), height: cordovaWebView.getUIContext().px2vp(embed.info?.height), text:page.textValue, onTextChange:page.onTextChangeCallBack} as ESObject);
713
+ } else if (embed.status == NativeEmbedStatus.DESTROY) {
714
+ console.log("NativeEmbed destroy" + JSON.stringify(embed));
715
+ let nodeController = page.nodeControllerMap.get(componentId);
716
+ nodeController?.setDestroy(true)
717
+ page.nodeControllerMap.clear();
718
+ page.positionMap.delete(componentId);
719
+ page.widthMap.delete(componentId);
720
+ page.heightMap.delete(componentId);
721
+ page.componentIdArr.filter((value: string) => value != componentId)
722
+ } else {
723
+ console.log("NativeEmbed status" + embed.status);
724
+ }
725
+ }
726
+
727
+ onNativeEmbedGestureEvent(touch: NativeEmbedTouchInfo,cordovaWebView:CordovaWebView,parentPage?:object) {
728
+ let page = parentPage as Index;//page为当前页面对象,相当于当前页面的this指针,使用该对象,必须将this指针传入到mainPage中
729
+ console.log("NativeEmbed onNativeEmbedGestureEvent" + JSON.stringify(touch.touchEvent));
730
+ page.componentIdArr.forEach((componentId: string) => {
731
+ let nodeController = page.nodeControllerMap.get(componentId);
732
+ // 将获取到的同层区域的事件发送到该区域embedId对应的nodeController上
733
+ if(nodeController?.getEmbedId() == touch.embedId) {
734
+ let ret = nodeController?.postEvent(touch.touchEvent)
735
+ if(ret) {
736
+ console.log("onNativeEmbedGestureEvent success " + componentId);
737
+ } else {
738
+ console.log("onNativeEmbedGestureEvent fail " + componentId);
739
+ }
740
+ if(touch.result) {
741
+ // 通知Web组件手势事件消费结果
742
+ touch.result.setGestureEventResult(ret);
743
+ }
744
+ }
745
+ })
746
+ }
747
+
748
+ /*
749
+ *同层渲染的TextInput文本改变后回调该函数
750
+ *可以通过自定义插件获取改变后的值
751
+ */
752
+ onTextChangeCallBack(page:Index, value:string) {
753
+ page.textValue = value;
754
+ }
755
+
756
+ getTextValue():string {
757
+ return this.textValue;
758
+ }
759
+
760
+ /*
761
+ *设置同层渲染TextInput的显示文本
762
+ *可以通过自定义插件设置TextInput的显示文本
763
+ */
764
+ setNativeValue(id:string, value:string){
765
+ this.textValue = value;
766
+ let nodeController = this.nodeControllerMap.get(id);
767
+ nodeController?.updateNode({page:this, textOne: 'update', width: this.widthMap.get(id), height: this.heightMap.get(id), text:this.textValue, onTextChange:this.onTextChangeCallBack} as ESObject)
768
+ }
769
+
770
+ RelativeContainer() {
771
+ //同层渲染
772
+ ForEach(this.componentIdArr, (componentId: string) => {
773
+ NodeContainer(this.nodeControllerMap.get(componentId))
774
+ .position(this.positionMap.get(componentId))
775
+ .width(this.widthMap.get(componentId))
776
+ .height(this.heightMap.get(componentId))
777
+ }, (embedId: string) => embedId)
778
+ /*
779
+ *nativeEmbedHtmlTag:注册同层渲染标签
780
+ * 默认是:<embed>的标签 ,如果要注册object,请传入object,同层渲染只支持这两个标签,可以直接保持默认
781
+ *nativeEmbedHtmlType:注册同层选择标签类型
782
+ * 默认是:native类型,如要要传入其他类型,请随意取名字
783
+ *onNativeEmbedLifecycleChange:同层渲染元素生命周期函数
784
+ *onNativeEmbedGestureEvent:同层渲染手势回调函数
785
+ * 同层渲染参考连接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/web-same-layer
786
+ */
787
+ MainPage({
788
+ parentPage: this,
789
+ onNativeEmbedLifecycleChange: this.onNativeEmbedLifecycleChange,
790
+ onNativeEmbedGestureEvent: this.onNativeEmbedGestureEvent
791
+ });
792
+ }
793
+ }
794
+
795
+ ```
796
+
797
+ ### 15, 键盘避让模式
798
+
799
+ ```typescript
800
+ //webKeyboardAvoidMode:避让键盘模式,默认:WebKeyboardAvoidMode.RESIZE_VISUAL
801
+ mainPage({webKeyboardAvoidMode:WebKeyboardAvoidMode.RESIZE_VISUAL})
802
+ ```
803
+
804
+ ### 16, 自定义http头
805
+ ```typescript
806
+ /*
807
+ *customHttpHeaders:自定义http头
808
+ * 前端withCredentials为true时添加自定义http头,没有自定义http头不用添加
809
+ * 参考常见问题的跨域说明
810
+ *isAllowCredentials:默认是false
811
+ * 前端请求设置withCredentials为true,要传入参数isAllowCredentials:true
812
+ * 参考常见问题的跨域说明
813
+ */
814
+ mainPage({customHttpHeaders:"X-AUTH", isAllowCredentials:true})
815
+ ```
816
+ ### 17, 传入当前页面对象parentPage,在mainPage的生命周期函数中可以引用当前页面的变量
817
+
818
+ ```typescript
819
+ //parentPage:传入this,就是webview父组件对象,也就是当前组件的对象,可以在插件里面调用
820
+ mainPage({parentPage:this})
821
+
822
+ ```
823
+
824
+ ## 热更新
825
+
826
+ capacitor原框架官方不支持热更新,鸿蒙化框架基于Cordova官方热更新插件功能,改造成适配capacitor框架,框架自带,无需安装。
827
+
828
+ ### 前置条件
829
+
830
+ 在服务器上放置两个文件(通过cordova-hot-code-push-cli命令生成,参考:https://www.npmjs.com/package/cordova-hot-code-push-cli):
831
+ 安装依赖
832
+ #### 安装 CLI 工具(全局)
833
+ npm install -g cordova-hot-code-push-cli
834
+
835
+ #### 在项目根目录执行
836
+ chcp init
837
+ #### 根据提示输入更新服务器地址(如 https://www.example.com/chcp)
838
+
839
+ #### 构建本地 Web 资源,生成 chcp.json 与 chcp.manifest
840
+ chcp build
841
+
842
+ 在chcp目录下:
843
+ chcp.json:定义版本、更新内容 URL 等。
844
+ chcp.manifest:列出所有文件及其哈希值(用于校验)。
845
+ www目录:放置结构与工程中rawfile/www中的更新文件,需要与原结构保持一致。
846
+
847
+
848
+ ### 基本配置步骤
849
+
850
+ 1、修改 capacitor.config.json,增加如下配置
851
+ ```typescript
852
+ {
853
+ "plugins": {
854
+ "chcp": {
855
+ "auto-download": true,
856
+ "auto-install": true,
857
+ "config-file": "http://www.example.com/chcp/chcp.json"
858
+ }
859
+ }
860
+ }
861
+ ```
862
+
863
+ 2、chcp.json配置文件示例
864
+ 该文件在本地rawfile/www目录下存放一份,然后在服务器存储一份
865
+ - **release:** 版本号,chcp会判断该版本号和本地版本号比较,判断是否要更新
866
+ - **content_url:** 更新文件的存储位置
867
+ - **其他参数:** 其他参数chcp暂不使用
868
+ ```typescript
869
+ {
870
+ "name": "capacitor",
871
+ "autogenerated": true,
872
+ "update": "now",
873
+ "min_native_interface": 1,
874
+ "content_url": "http://www.example.com/chcp/www",
875
+ "release": "2025.03.05-16.47.30"
876
+ }
877
+ ```
878
+
879
+ 3、chcp.manifest配置文件
880
+ 该文件在本地rawfile/www目录下存放一份,然后在服务器存储一份,服务端存储在chcp.json项目目录内
881
+ ```typescript
882
+ [
883
+ {
884
+ "file": "assets/icon/favicon.png",
885
+ "hash": "988be98f12b400c41a22b59b82cfeab1"
886
+ }
887
+ ]
888
+ ```
889
+
890
+ 4、js代码部分
891
+ 在本地工程中调用以下代码,实现热更新功能
892
+ ```javascript
893
+ function chcpUpdate() {
894
+ //配置新的更新地址,如果不传option,更新地址使用www/chcp.json配置的地址
895
+ window.Capacitor.Plugins.HotCodePushPlugin.fetchUpdate({
896
+ "config-file":"http://www.example.com/chcp/chcp.json" // 服务端配置信息
897
+ }).then(result => {
898
+ if (result.action == 'chcp_updateIsReadyToInstall') {
899
+ console.log('插件有更新'));
900
+ //检测到更新,更新成功后会自动重启app,每次更新间隔周期需大于1分钟
901
+ //如果要进行测试,修改www/chcp.json的release版本号,修改www/chcp.manifest文件内,其中文件对应的md5值
902
+ window.Capacitor.Plugins.HotCodePushPlugin.installUpdate().then(result2 => {
903
+ console.log('更新完成'));
904
+ });
905
+ }
906
+ else {
907
+ console.log('插件无更新');
908
+ }
909
+ });
910
+ }
911
+ ```
912
+
913
+ 5、更新流程
914
+ 应用启动 → 检查服务器 chcp.json → 对比版本 → 下载差异文件 → 安装更新
915
+
916
+ ## 自定义ArKTS插件研发
917
+
918
+ 自定义ArkTS插件研发复用了cordova-openharmony能力进行实现,自定义插件接口遵守cordova sdk官方规范,以自定义插件TestPlugin、为例:
919
+
920
+ ### (1)新建ArkTs文件
921
+
922
+ 新建ArkTs文件,取名字为TestPlugin,示例代码如下,具体功能参考示例代码注释说明。
923
+
924
+ ```typescript
925
+ import { CordovaPlugin,CordovaInterface, CallbackContext} from 'capacitor-openharmony';
926
+ import { CordovaWebView, MessageStatus, PluginResult} from 'capacitor-openharmony';
927
+ import { PromptAction } from '@kit.ArkUI';
928
+ import { common, Want } from '@kit.AbilityKit';
929
+ import { BusinessError } from '@kit.BasicServicesKit';
930
+ import { Index as Page } from '../pages/Index';
931
+
932
+ export class TestPlugin extends CordovaPlugin {
933
+ protected cordovaInterface?: CordovaInterface;
934
+ protected cordovaWebView?: CordovaWebView;
935
+
936
+ //插件初始化函数,初始化函数在页面显示前调用,因此在初始化中不能进行UI的相关操作。
937
+ initialize(cordovaInterface: CordovaInterface, cordovaWebView:CordovaWebView):void {
938
+ this.cordovaInterface = cordovaInterface;
939
+ this.cordovaWebView = cordovaWebView;
940
+ return;
941
+ }
942
+
943
+ execute(action: string, args: ESObject[], callbackContext: CallbackContext):boolean {
944
+ if(action == "eayHello") {
945
+ return this.eayHello(args, callbackContext);
946
+ }
947
+
948
+ //获取preferences的配置
949
+ if(action == "getPreferences") {
950
+ let preferences = this.preferences!.getAll();
951
+ let jsonArray:Array< object> = new Array< object>();
952
+ preferences.forEach((value,key) => {
953
+ let pre:object = new Object();
954
+ pre["name"] = key;
955
+ pre["value"] = value;
956
+ jsonArray.push(pre);
957
+ });
958
+ callbackContext.successByJson(jsonArray);
959
+ }
960
+
961
+ if(action == "openSystemBrowser") {
962
+ return this.openSystemBrowser(args, callbackContext);
963
+ }
964
+
965
+ if(action == "openOtherPage") {
966
+ //系统路由功能,webview是根页面,跳转到原生的其他页面,具体使用参考如下连接
967
+ //https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/arkts-navigation-navigation-V5
968
+ let pathStack:NavPathStack = this.cordovaInterface!.getPageStack();
969
+ pathStack.pushPathByName("TestPage", "{test:10}");
970
+ }
971
+
972
+ if(action == "otherFunction") {
973
+ //获取webview属性变量,用于动态修改webview属性,具体参考如下连接
974
+ //https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/js-apis-webview-V5
975
+ this.cordovaWebView!.getWebAttribute()?.height('50%');
976
+ //获取webview的控制变量,用于实现具体的功能,示例代码实现在webviw执行js,具体参考如下连接
977
+ //https://developer.huawei.com/consumer/cn/doc/harmonyos-references-V5/ts-basic-components-web-V5
978
+ this.cordovaWebView!.getWebviewController().runJavaScript("alert(1);");
979
+ //多次执行js侧回调函数,例如在显示执行进度时,需要多次调用
980
+ let pluginResult:PluginResult = PluginResult.createByString(MessageStatus.OK, "success");
981
+ pluginResult.setKeepCallback(true);
982
+ callbackContext.sendPluginResult(pluginResult);
983
+ let pluginResult2:PluginResult = PluginResult.createByString(MessageStatus.OK, "success2");
984
+ callbackContext.sendPluginResult(pluginResult2);
985
+
986
+ //多次调用也可以采用如下写法
987
+ //callbackContext.successByString("success1", true);//第一次调用
988
+ //callbackContext.successByString("success2", true);//第二次调用
989
+ //callbackContext.successByString("success3");//最后一次调用
990
+ }
991
+
992
+ if(action == "resetPageInfo") {
993
+ return this.resetPageInfo(args, callbackContext);
994
+ }
995
+
996
+ if(action == "otherWebviewController") {
997
+ return this.otherWebviewController(args, callbackContext);
998
+ }
999
+ return true;
1000
+ }
1001
+
1002
+ eayHello(args: ESObject[], callbackContext: CallbackContext):boolean {
1003
+ //获取UI上下文,用于原生UI交互
1004
+ let uiContext:UIContext = this.cordovaWebView!.getUIContext();
1005
+ let promptAction: PromptAction = uiContext?.getPromptAction();
1006
+ try {
1007
+ //弹出系统原生窗口
1008
+ promptAction.showDialog({
1009
+ title: 'Title',
1010
+ message: 'eay hello',
1011
+ buttons: [
1012
+ {
1013
+ text: '确定',
1014
+ color: '#000000'
1015
+ }
1016
+ ]
1017
+ }, (err, data) => {
1018
+ if (err) {
1019
+ return;
1020
+ }
1021
+ //执行成功通知js侧回调函数,通知函数有多个具体查看CallbackContext封装函数
1022
+ callbackContext.success();
1023
+ });
1024
+ } catch (error) {
1025
+ }
1026
+ return true;
1027
+ }
1028
+
1029
+ openSystemBrowser(args: ESObject[], callbackContext: CallbackContext):boolean {
1030
+ if(args.length > 0) {
1031
+ let url:string = args[0];
1032
+ //获取UIAbilityContext
1033
+ let context = getContext(this) as common.UIAbilityContext
1034
+ let wantInfo: Want = {
1035
+ action: 'ohos.want.action.viewData',
1036
+ entities: ['entity.system.browsable'],
1037
+ uri: url
1038
+ }
1039
+ //跳转一个新的ability
1040
+ context.startAbility(wantInfo).then(() => {
1041
+ console.log('[跳转至外部浏览器] - success')
1042
+ }).catch((err: BusinessError) => {
1043
+ console.error('[跳转至外部浏览器] - Failed to startAbility. Code: ' + err.code + 'message:' + err.message);
1044
+ })
1045
+ }
1046
+ return true;
1047
+ }
1048
+
1049
+ //获取父组件对象(定义为Page)通过父组件调用相关方法或设置属性
1050
+ resetPageInfo(args: ESObject[], callbackContext: CallbackContext):boolean {
1051
+ if(this.cordovaInterface) {
1052
+ if(this.cordovaInterface.getPage()) {
1053
+ let page:Page = this.cordovaInterface!.getPage() as Page;
1054
+ page.indexPage = "/www2/index.html"; //加载其他页面
1055
+ page.DoTest();//调用父组件方法
1056
+ }
1057
+ }
1058
+ callbackContext.success();
1059
+ return true;
1060
+ }
1061
+ /*
1062
+ * 多Webview模式,一个webview和其他webview通讯,在多页面情况下使用,单页面视图的APP不需要
1063
+ * 1,查询其他webview的插件
1064
+ * 2,设置其他webview的属性
1065
+ * 3,其他webview注入js
1066
+ * 4, 在其他webview打开原生界面
1067
+ * 5,在其他webview控制路由
1068
+ * 6,可以灵活使用,需要技术支持联系开发者
1069
+ */
1070
+ otherWebviewController(args: ESObject[], callbackContext: CallbackContext):boolean {
1071
+ if(args.length > 0) {
1072
+ let webId: string = args[0];
1073
+ let cmd:string = args[1];
1074
+
1075
+ //打印所有webId
1076
+ if(cmd == "printWebId" && this.mapWebIdToWebTag) {
1077
+ this.mapWebIdToWebTag.forEach((value, key) => {
1078
+ let pluginResult:PluginResult = PluginResult.createByString(MessageStatus.OK, key!);
1079
+ pluginResult.setKeepCallback(true);
1080
+ callbackContext.sendPluginResult(pluginResult);
1081
+ });
1082
+ }
1083
+
1084
+ //打印webId对应的webview自带的所有自定义ArkTS插件
1085
+ if(cmd == "printPlugins" && this.mapWebIdToWebTag) {
1086
+ if(this.mapWebIdToWebTag.hasKey(webId)) {
1087
+ let webTag:string = this.mapWebIdToWebTag.get(webId);
1088
+ if(this.mapWebIdToCustomPlugins?.hasKey(webTag)) {
1089
+ this.mapWebIdToCustomPlugins.get(webTag).forEach((value, key) => {
1090
+ let pluginResult: PluginResult = PluginResult.createByString(MessageStatus.OK, key!);
1091
+ pluginResult.setKeepCallback(true);
1092
+ callbackContext.sendPluginResult(pluginResult);
1093
+ });
1094
+ }
1095
+ }
1096
+ }
1097
+
1098
+ //指定webId对应webview,设置属性
1099
+ if(cmd == "setAttr" && this.mapWebIdToWebView) {
1100
+ if(this.mapWebIdToWebView.hasKey(webId)) {
1101
+ this.mapWebIdToWebView.get(webId).getWebAttribute()?.height('20%');
1102
+ }
1103
+ callbackContext.success();
1104
+ }
1105
+
1106
+ //指定webId对应webview执行注入新的js代码
1107
+ if(cmd == "injectJs" && this.mapWebIdToWebView) {
1108
+ if(this.mapWebIdToWebView.hasKey(webId)) {
1109
+ this.mapWebIdToWebView.get(webId).getWebviewController().runJavaScript("alert(1);");
1110
+ }
1111
+ callbackContext.successByString("OK");
1112
+ }
1113
+
1114
+ //指定webId对应的webview打开原生界面,并使用指定webId的webview的路由
1115
+ if(cmd == "openPage" && this.mapWebIdToInterface) {
1116
+ if(this.mapWebIdToInterface.hasKey(webId)) {
1117
+ let pathStack:NavPathStack = this.mapWebIdToInterface.get(webId).getPageStack();
1118
+ pathStack.pushPathByName("TestPage", "{test:10}");
1119
+ }
1120
+ callbackContext.success();
1121
+ }
1122
+
1123
+ //指定webId对应的webview弹窗
1124
+ if(cmd == "openAlert" && this.mapWebIdToWebView) {
1125
+ if(this.mapWebIdToWebView.hasKey(webId)) {
1126
+ this.mapWebIdToWebView.get(webId).getWebviewController().runJavaScript("alert(1);");
1127
+ let cordovaWebView:CordovaWebView = this.mapWebIdToWebView.get(webId);
1128
+ //获取UI上下文,用于原生UI交互
1129
+ let uiContext:UIContext = cordovaWebView!.getUIContext();
1130
+ let promptAction: PromptAction = uiContext?.getPromptAction();
1131
+ try {
1132
+ //弹出系统原生窗口
1133
+ promptAction.showDialog({
1134
+ title: 'Title',
1135
+ message: 'eay hello',
1136
+ buttons: [
1137
+ {
1138
+ text: '确定',
1139
+ color: '#000000'
1140
+ }
1141
+ ]
1142
+ }, (err, data) => {
1143
+ if (err) {
1144
+ return;
1145
+ }
1146
+ //执行成功通知js侧回调函数,通知函数有多个具体查看CallbackContext封装函数
1147
+ callbackContext.success();
1148
+ });
1149
+ } catch (error) {
1150
+ }
1151
+ return true;
1152
+ }
1153
+ callbackContext.success();
1154
+ }
1155
+
1156
+ }
1157
+ return true;
1158
+ }
1159
+
1160
+ /*
1161
+ *同层渲染
1162
+ *JS侧设置原生插件属性
1163
+ */
1164
+ setNativeValue(args: ESObject[], callbackContext: CallbackContext):boolean {
1165
+ let id: string = args[0];
1166
+ let value: string = args[1];
1167
+ if(this.cordovaInterface) {
1168
+ if(this.cordovaInterface.getPage()) {
1169
+ let page:Page = this.cordovaInterface!.getPage() as Page;
1170
+ page.setNativeValue(id, value);
1171
+ }
1172
+ }
1173
+ callbackContext.success();
1174
+ return true;
1175
+ }
1176
+
1177
+ /*
1178
+ *同层渲染
1179
+ *JS侧获取原生组件属性
1180
+ */
1181
+ getNativeValue(args: ESObject[], callbackContext: CallbackContext):boolean {
1182
+ if(this.cordovaInterface) {
1183
+ if(this.cordovaInterface.getPage()) {
1184
+ let page:Page = this.cordovaInterface!.getPage() as Page;
1185
+ callbackContext.successByString(page.textValue);
1186
+ }
1187
+ }
1188
+ return true;
1189
+ }
1190
+ }
1191
+ ```
1192
+
1193
+ ### (2)插件的配置
1194
+
1195
+ ArkTs侧插件写好以后,在entry/src/main/ets/pages/index.ets文件中配置,支持多页面视图,各个视图拥有自己的插件,以及各视图之间通讯:
1196
+
1197
+ ```typescript
1198
+ import { MainPage, pageBackPress, pageHideEvent, pageShowEvent, PluginEntry} from 'capacitor-openharmony';
1199
+ import { TestPlugin } from "../plugins/TestPlugin"//引入插件
1200
+ struct Index {
1201
+ /*
1202
+ *ArkTs侧的自定义插件键值对:插件名称和实现对象,自定义插件开发,请查看自定义查看开发部分
1203
+ *如果一个插件传入多个MainPage,务必单独定义对象传入,不可多MainPage使用一个对象,否则会使窗口操作串联
1204
+ */
1205
+ cordovaPlugs:Array< PluginEntry> =
1206
+ [
1207
+ {
1208
+ pluginName: 'TestPlugin', //插件名称
1209
+ pluginObject:new TestPlugin() //实例化插件对象
1210
+ }
1211
+ ];
1212
+
1213
+ cordovaPlugs2:Array< PluginEntry> =
1214
+ [
1215
+ {
1216
+ pluginName: 'TestPlugin', //插件名称
1217
+ pluginObject:new TestPlugin() //实例化插件对象
1218
+ }
1219
+ ];
1220
+
1221
+ //省略其他代码
1222
+
1223
+ build() {
1224
+ RelativeContainer() {
1225
+ //isWebDebug:DevTools工具调试开关,cordovaPlugs:自定义插件列表,启动首页index.html
1226
+ MainPage({isWebDebug:false,cordovaPlugs:this.cordovaPlugs});
1227
+ }
1228
+ .height('50%')
1229
+ .width('100%')
1230
+
1231
+ RelativeContainer() {
1232
+ //isWebDebug:DevTools工具调试开关,cordovaPlugs:自定义插件列表,指定加载rawfile资源目录下文件
1233
+ MainPage({isWebDebug:false,indexPage:"/www2/index.html", cordovaPlugs:this.cordovaPlugs2});
1234
+ }
1235
+ .height('50%')
1236
+ .width('100%')
1237
+ }
1238
+ }
1239
+ ```
1240
+
1241
+ ### (3)JS侧插件调用
1242
+
1243
+ js侧插件调用完全遵守cordova官方调用规范:
1244
+
1245
+ ** 直接调用,无需做任何配置,代码如下:**
1246
+
1247
+ ```javascript
1248
+ cordova.exec(function(result){
1249
+ console.log(result);
1250
+ },function(error){
1251
+ console.log(error);
1252
+ },"TestPlugin", "openOtherPage", [{name:'name1'},{name:'name2'}]);
1253
+ ```
1254
+
1255
+ ### (3)自定义插件实现原理简述
1256
+
1257
+ 由于HarmonyOS提供ArkTS和C/C++ API,capacitor sdk是使用C/C++研发,自定义插件是跨语言调用,调用顺序为:js侧->C/C++侧->ArkTs侧,回调是相反顺序,不过ArkTS侧的插件也可以直接调用JS侧。自定义插件的研发根据具体实现的功能,可以选择使用ArkTS开发,也可选择C/C++开发。
1258
+
1259
+ ---
1260
+
1261
+ ## 自定义C++插件研发
1262
+
1263
+ 研发自定义C++侧插件,您可以参考已移植的capacitor官方插件,编写C++侧插件。
1264
+
1265
+ ### (1)开发步骤
1266
+
1267
+ 1. 在源码集成的capacitor工程中,在源码的CPP目录内新建一个插件目录,保存您的自定义插件;
1268
+ 2. 在新目录中新建一个class,该class要继承Plugin类,同时新建对应插件的CMakeLists.txt;
1269
+ 3. 在您的CPP文件中,添加REGISTER_CAP_PLUGIN()注册您的插件名称,如CapacitorPlugin;用于实例化您的插件对象;
1270
+ 4. 在您的CPP文件中,添加REGISTER_PLUGIN_METHOD()注册您的插件方法,如PluginHello;用于实现您的插件功能;
1271
+ 5. 如果您的插件中需要调用ArkTS侧的代码,需要调用executeArkTs(同步)或者executeArkTsAsync(异步)执行ArkTS侧代码,参数说明参考Plugin类注释说明,ArkTs实现文件需要在capacitor模块的build-profile.json5下的 buildOption -> arkOptions -> runtimeOnly -> sources下导入该文件。
1272
+ 6. 如果您的ArkTS侧需要把执行结果通知到C++侧的插件,在ArkTS侧需要调用onArkTsResult函数通知C++侧,C++侧的插件也要注册和实现onArkTsResult这个函数;
1273
+ 7. 在完成您的插件研发后需要将您的cpp文件添加到CMakeLists.txt中,完成编译;
1274
+
1275
+ ### (2)配置
1276
+
1277
+ 在rawfile/capacitor.plugins.json文件中,添加插件名和c++插件实现类名
1278
+
1279
+ ```json
1280
+ [
1281
+ {
1282
+ "pkg": "@capacitor/CapacitorPlugin",
1283
+ "classpath": "CapacitorPlugin"
1284
+ }
1285
+ ]
1286
+ ```
1287
+
1288
+ ### (3)JS调用
1289
+
1290
+ ```javascript
1291
+ const result = await window.Capacitor.Plugins.CapacitorPlugin.PluginHello({
1292
+ message: 'Exec PluginHello'
1293
+ });
1294
+ ```
1295
+
1296
+ ---
1297
+
1298
+ ## Web加载性能优化
1299
+
1300
+ ### 1. 预启动web和预渲染
1301
+
1302
+ 在应用启动后,在EntryAbility代码中,后台启动web引擎,并在后台渲染页面,进入page页面后,页面秒开,关闭页面后,页面进入后台,不会销毁web,下次打开仍可秒开;需提醒的是,在使用Capacitor的页面预渲染时,会初始化capacitor插件,有可能会出现在用户没有同意隐私政策前,初始化插件会访问系统资源。
1303
+
1304
+ 该功能需要对mainPage的组件进行二次封装,自己可以根据需要修改代码,如需技术支持请联系本开发者,提供封装方法和源码如下:
1305
+
1306
+ 参考连接:[https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-web-develop-optimization](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-web-develop-optimization)
1307
+
1308
+ **(1)在pages中新建ArkTs文件,命名为WebBuilder.ets,复制以下代码:**
1309
+
1310
+ ```typescript
1311
+ import { MainPage, MainPageCycle, PluginEntry } from 'capacitor-openharmony';
1312
+ import { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI';
1313
+ import { webview } from '@kit.ArkWeb';
1314
+ import { TestPlugin } from '../plugins/TestPlugin';
1315
+
1316
+ //根据需要扩展参数,参数参考MainPage的参数,高级功能中对mainPage参数有说明
1317
+ class DataParameters{
1318
+ url?: string;
1319
+ mainPageCycle?:MainPageCycle;
1320
+ mainPagePageNodeController?:MainPagePageNodeController;
1321
+ cordovaPlugs?:Array< PluginEntry>;
1322
+ }
1323
+
1324
+ @Builder
1325
+ function buildMainPage(data:DataParameters) {
1326
+ Column() {
1327
+ MainPage({indexPage:data.url, lifeCycle:data.mainPageCycle, parentPage:data.mainPagePageNodeController,cordovaPlugs:data.cordovaPlugs});
1328
+ }.width("100%").height("100%")
1329
+ }
1330
+
1331
+ let wrap = wrapBuilder< DataParameters[]>(buildMainPage);
1332
+
1333
+ class MainPagePageNodeController extends NodeController {
1334
+ private rootNode: BuilderNode< DataParameters[]> | null = null;
1335
+ private root: FrameNode | null = null;
1336
+ private cordovaPlugs:Array< PluginEntry> = [
1337
+ {
1338
+ pluginName: 'TestPlugin', //插件名称
1339
+ pluginObject:new TestPlugin() //实例化插件对象
1340
+ }
1341
+ ];
1342
+ private mainPageCycle:MainPageCycle = new MainPageCycle().setOnAboutToAppear((webviewController: webview.WebviewController,parentPage?:object)=>{
1343
+ let page = parentPage as MainPagePageNodeController;//page为当前页面对象,相当于当前页面的this指针,使用该对象,必须将this指针传入到mainPage中
1344
+ console.log("exec onAboutToAppear");
1345
+ });
1346
+
1347
+ constructor() {
1348
+ super();
1349
+ }
1350
+
1351
+ makeNode(uiContext: UIContext): FrameNode | null {
1352
+ if (this.rootNode != null) {
1353
+ const parent = this.rootNode.getFrameNode()?.getParent();
1354
+ if (parent) {
1355
+ console.info(JSON.stringify(parent.getInspectorInfo()));
1356
+ parent.removeChild(this.rootNode.getFrameNode());
1357
+ this.root = null;
1358
+ }
1359
+ this.root = new FrameNode(uiContext);
1360
+ this.root.appendChild(this.rootNode.getFrameNode());
1361
+ return this.root;
1362
+ }
1363
+ return null;
1364
+ }
1365
+
1366
+ initWeb(url:string, uiContext:UIContext) {
1367
+ if(this.rootNode != null) {
1368
+ return;
1369
+ }
1370
+ this.rootNode = new BuilderNode(uiContext);
1371
+ //可以根据不同的页面传入不同的参数,单页面视图不存在这种情况,需要技术支持联系本开发者
1372
+ if(url === "/www3/index.html") {
1373
+ this.rootNode.build(wrap, {url:url, mainPageCycle:this.mainPageCycle,mainPagePageNodeController:this, cordovaPlugs:this.cordovaPlugs});
1374
+ } else {
1375
+ this.rootNode.build(wrap, {url:url});
1376
+ }
1377
+ }
1378
+ }
1379
+
1380
+ let NodeMap:Map< string, MainPagePageNodeController | undefined> = new Map();
1381
+
1382
+ export const createNWeb = (url: string, uiContext: UIContext) : MainPagePageNodeController | undefined => {
1383
+ let baseNode = new MainPagePageNodeController();
1384
+ baseNode.initWeb(url, uiContext);
1385
+ NodeMap.set(url, baseNode);
1386
+ return baseNode;
1387
+ }
1388
+
1389
+ export const getNWeb = (url : string, uiContext:UIContext) : MainPagePageNodeController | undefined => {
1390
+ if(NodeMap.has(url)) {
1391
+ return NodeMap.get(url);
1392
+ } else {
1393
+ return createNWeb(url, uiContext);
1394
+ }
1395
+ }
1396
+ ```
1397
+
1398
+ **(2)修改EntryAbility.ets,添加预启动web和预渲染代码:**
1399
+
1400
+ ```typescript
1401
+ //省略了其他代码
1402
+ onWindowStageCreate(windowStage: window.WindowStage): void {
1403
+ windowStage.loadContent('pages/Splash', (err) => {
1404
+ //启动预启动web和预渲染,多页面视图可以预选设置和初始化
1405
+ createNWeb('/www3/index.html', windowStage.getMainWindowSync().getUIContext());
1406
+ createNWeb('/www3/index2.html', windowStage.getMainWindowSync().getUIContext());
1407
+ createNWeb('/www3/index3.html', windowStage.getMainWindowSync().getUIContext());
1408
+ });
1409
+ }
1410
+ ```
1411
+
1412
+ **(3)修改Index.ets启动capacitor封装的mainPage页面,此时秒开,效率和传统打开mainPage相比大大提高:**
1413
+
1414
+ ```typescript
1415
+ build() {
1416
+ Column() {
1417
+ RelativeContainer() {
1418
+ NodeContainer(getNWeb('/www3/index.html', this.getUIContext()))
1419
+ .height('100%')
1420
+ .width('100%')
1421
+ }
1422
+ .height('100%')
1423
+ .width('100%')
1424
+ }
1425
+ ```
1426
+
1427
+ ### 2. 资源拦截替换的JavaScript生成字节码缓存(Code Cache)
1428
+
1429
+ 使用Capacitor框架,根据Apache Capacitor的标准,所有页面和JS文件都在本地,鸿蒙Capacitor内部已经使用了拦截和替换功能,如果您加载的是在线资源或者JS文件,并且强制使用了Capacitor协议栈(通过capacitor.config.json配置或者SetCordovaProtocolUrl函数设置),Capacitor框架也进行了资源缓存,如果您加载的是在线页面,使用webview的协议栈,可以结合MainPage提供的生命周期函数onInterceptWebRequest进行拦截,对于在线的js文件,也可以直接打包到本地的沙箱目录下,通过Capacitor提供的SetResourceReplace函数进行拦截替换,以提供加载页面速度。示例代码如下:
1430
+
1431
+ 参考连接:[https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-web-develop-optimization#section172031338172719](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-web-develop-optimization#section172031338172719)
1432
+
1433
+ ```typescript
1434
+ //省略有其他代码,以下是js预编译示例代码
1435
+ configs: Array< Config> = [
1436
+ {
1437
+ url: 'https://www.tongecn.com/example.js',
1438
+ localPath: 'example.js',//文件在rawfile目录下
1439
+ options: {
1440
+ responseHeaders: [
1441
+ { headerKey: 'E-Tag', headerValue: 'xxx' },
1442
+ { headerKey: 'Last-Modified', headerValue: 'Web, 21 Mar 2024 10:38:41 GMT' }
1443
+ ]
1444
+ }
1445
+ }
1446
+ ]
1447
+
1448
+ mainPageCycle = new MainPageCycle().setOnControllerAttached((webviewController: webview.WebviewController,parentPage?:object)=>{
1449
+ console.log("exec onControllerAttached");
1450
+ for (const config of this.configs) {
1451
+ let content = await this.getUIContext().getHostContext()?.resourceManager.getRawFileContentSync(config.localPath);
1452
+ try {
1453
+ this.controller.precompileJavaScript(config.url, content, config.options)
1454
+ .then((errCode: number) => {
1455
+ console.log('precompile successfully!' );
1456
+ }).catch((errCode: number) => {
1457
+ console.error('precompile failed.' + errCode);
1458
+ })
1459
+ } catch (err) {
1460
+ console.error('precompile failed!.' + err.code + err.message);
1461
+ }
1462
+ }
1463
+ })
1464
+
1465
+ //省略其他代码,以下是拦截替换
1466
+ onInterceptWebRequest(request: WebResourceRequest, webTag:string):ESObject {
1467
+ // webview内核处理替换
1468
+ if(url == "https://www.tongecn.com/v1.1.1/temp/test3.js") {
1469
+ //替换本地沙箱路径
1470
+ SetResourceReplace(webTag, url, "https://localhost/data/storage/el2/base/files/test.js");
1471
+ //替换本地rawfile文件
1472
+ //SetResourceReplace(webTag, url, "https://www.example.com/test.js");
1473
+ }
1474
+ return null;
1475
+ }
1476
+
1477
+ //省略有其他代码
1478
+ MainPage({isWebDebug:true, indexPage:"https://www.tongecn.com", lifeCycle:data.mainPageCycle, parentPage:this,onInterceptWebRequest:this.onInterceptWebRequest});
1479
+ ```
1480
+
1481
+ ---
1482
+
1483
+ ## 常见问题
1484
+
1485
+ ### 1. 鸿蒙返回键不起作用
1486
+
1487
+ 鸿蒙返回键不起作用,就是手势事件,从左往右快速滑动,app不返回上一页面,或者到了顶层页面不退出应用。
1488
+
1489
+ 不同的框架有不同的处理方式,如果不管使用的是什么框架,只在capacitor层处理的,需要监听返回键事件,代码如下:
1490
+
1491
+ ```javascript
1492
+ document.addEventListener("deviceready", onDeviceReady, false);
1493
+ function onDeviceReady() {
1494
+ document.addEventListener("backbutton", onBackKeyDown, false);
1495
+ }
1496
+
1497
+ function onBackKeyDown() {
1498
+ //自己处理返回
1499
+ }
1500
+ ```
1501
+
1502
+ 如果采用ionic angularjs框架,可以采用如下代码:
1503
+
1504
+ ```javascript
1505
+ function showConfirm() {
1506
+ //处理退出应用的逻辑
1507
+ }
1508
+ $ionicPlatform.registerBackButtonAction(function (e) {
1509
+ // Is there a page to go back to?
1510
+ if ($location.path() == '/tab/message') { //到了顶层页面,/tab/message是顶层页面的路由,这里只是举个例子,实际情况根据您的项目设置
1511
+ showConfirm();
1512
+ return false
1513
+ } else if ($ionicHistory.backView()) {
1514
+ // Go back in history
1515
+ $ionicHistory.goBack(); //自己处理返回
1516
+ } else {
1517
+ // This is the last page: Show confirmation popup
1518
+ showConfirm();
1519
+ return false;
1520
+ }
1521
+ e.preventDefault();
1522
+ return false;
1523
+ }, 101);
1524
+ ```
1525
+
1526
+ **说明**:无论采用什么框架都可以在capacitor层通过监听backbutton返回事件自己处理。
1527
+
1528
+ 如果加载的页面不包含相关js,需要传入控制webview路由MainPageOnBackPress对象控制返回
1529
+
1530
+ ### 2. 如何访问沙箱资源文件
1531
+
1532
+ 采用`cdvfile://`访问沙箱文件,以downloadImage.png为例:
1533
+
1534
+ ```
1535
+ cdvfile:///data/storage/el2/base/files/chuzhitong/downloadedImage.png
1536
+ ```
1537
+
1538
+ 如果是`file://`作为MainPage的入口页,也可以使用`file://`协议访问本地文件,沙箱资源文件可以是图片(png,jpg,svg等)、js、html等,请参考最后的file://协议说明
1539
+
1540
+ ### 3. HTTP协议的cookie说明
1541
+
1542
+ 如果您使用http协议非https协议,请参考如下cookie说明:
1543
+
1544
+ **(1)同源请求:**
1545
+
1546
+ 例如您是直接在MainPage传入网址例如传入`http://www.tongecn.com`,capacitor会自动处理cookie,无需手动处理
1547
+
1548
+ **(2)跨域请求:**
1549
+
1550
+ 例如您加载的文件在沙箱路径或者rawfile目录下的文件,在html文件中使用的http发送的GET/POST请求,此时需要再在capacitor.config.json里面配置http请求的域名,以便以capacitor为http处理cookie,配置如下:
1551
+
1552
+ 在capacitor.config.json 静态配置,静态配置所有请求,包括img、css、js等有capacitor处理:
1553
+
1554
+ ```json
1555
+ {
1556
+ "harmony": {
1557
+ "cordova-protocol-force": ["***.***.com"]
1558
+ }
1559
+ }
1560
+ ```
1561
+
1562
+ 在ArkTs侧运行态动态设置http的cookie,http的请GET/POST自动携带cookie,capacitor不出来静态资源,静态资源有webview处理:
1563
+
1564
+ ```typescript
1565
+ //在ArkTs侧运行态动态设置http的cookie,http的请GET/POST自动携带cookie,capacitor不出来静态资源,静态资源有webview处理
1566
+ aboutToAppear() {
1567
+ SetCordovaProtocolUrl("***.****.com");
1568
+ }
1569
+ ```
1570
+
1571
+ **(3)HTTPS协议:**
1572
+
1573
+ 您发送的请求是https协议,非http协议,capacitor会自动处理cookie,无需手动处理
1574
+
1575
+ **(4)手动设置cookie:**
1576
+
1577
+ 如果您要在ArkTs侧运行态手动设置cookie,请参考不常用的高级功能部分
1578
+
1579
+ ### 4. 虚拟域名www.example.com、自定义域名、localhost、file协议和cdvfile协议的详细说明
1580
+
1581
+ 加载rawfile目录下的页面时,通过DevTools工具测试时或者在日志log中会看到`https://www.example.com`的域名,可能会感到疑虑或者惊慌,接下来详细介绍一下,为什么使用此域名:
1582
+
1583
+ - 鸿蒙无法使用file协议直接加载rawfile目录的文件,因此使用www.example.com虚拟域名代替file协议,因此您看到`https://www.example.com`就理解为`file://`即可
1584
+ - 在capacitor.config.json里面harmony属性添加`"Hostname": "app.com"`使用自定义域名加载本地文件
1585
+ - 使用`cdvfile://`协议加载沙箱目录文件
1586
+
1587
+ **说明**:如果您的h5程序中有使用file://协议,在MainPage中就必须使用file://协议进入首页,否则file协议无法加载本地文件,capacitor完全支持file://协议加载文件,无论是从资源文件夹加载还是从沙箱路径加载鸿蒙capacitor完全支持
1588
+
1589
+ ### 5. 屏蔽跟随系统字体大小
1590
+
1591
+ - 在app.json5中增加configuration选项以屏蔽跟随系统字体大小,具体配置方法参考:[https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/app-configuration-file](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/app-configuration-file)
1592
+ - 在EntryAbility的onWindowStageCreate函数中增加`windowStage.setDefaultDensityEnabled(true);`屏蔽跟随显示大小缩放,参考:[https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-window#setdefaultdensityenabled12](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-window#setdefaultdensityenabled12)
1593
+
1594
+ ### 6. 跨域错误
1595
+
1596
+ capacitor已经解决了所有的跨域访问,同时会自动携带cookie,并兼容所有的自定义http头,无需做任何配置,但是前端withCredentials设置为true时,需要相应的配置解决跨域。
1597
+
1598
+ 在前端发送POST、GET请求,withCredentials为true时,同时您的服务器依赖于自定义http头例如X-Auth-Token Test-Type,mainPage要传入相应的参数如下:
1599
+
1600
+ ```typescript
1601
+ mainPage({customHttpHeaders:"X-Auth-Token,Test-Type",isAllowCredentials:true})
1602
+ ```
1603
+
1604
+ **如果没有设置会报跨域错误:**
1605
+
1606
+ ```
1607
+ Access to XMLHttpRequest at '*****' from origin '****' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
1608
+ ```
1609
+
1610
+ **解决方法:**在mainPage增加参数isAllowCredentials:true
1611
+
1612
+ ```typescript
1613
+ mainPage({isAllowCredentials:true})
1614
+ ```
1615
+
1616
+ ```
1617
+ Access to XMLHttpRequest at '*****' from origin '*****' has been blocked by CORS policy: Request header field ***** is not allowed by Access-Control-Allow-Headers in preflight response
1618
+ ```
1619
+
1620
+ **解决方法:**在mainPage增加自定义头,例如自定义http头X-Auth-Token Test-Type
1621
+
1622
+ ```typescript
1623
+ mainPage({customHttpHeaders:"X-Auth-Token,Test-Type", isAllowCredentials:true})
1624
+ ```
1625
+
1626
+ ### 7. iframe跨域设置cookie
1627
+
1628
+ 如果您使用的iframe加载了第三方页面,第三方页面直接使用js设置cookie,并不是通过http头Set-Cookie设置cookie的,js设置cookie,一定要加上`SameSite=None;Secure`,否则iframe会出现页面无法显示问题,因为请求http头不会自动携带cookie,设置cookie示例如下:
1629
+
1630
+ ```javascript
1631
+ document.cookie = 'token=467d1510-xxxx-xxxx-xxxx-73852620effa1; path=/; SameSite=None; Secure';
1632
+ ```
1633
+
1634
+ 如果第三方页面无法更改,请使用内置浏览器打开页面,或者直接使用a标签打开页面,a标签在鸿蒙的capacitor中会自动触发内置浏览器,Android和iOS不具备该功能。示例代码如下:
1635
+
1636
+ ```javascript
1637
+ //内置浏览器打开,可以配置相关参数,需集成内置浏览器插件
1638
+ window.open("https://www.*****.com/index.html", "title=测试标题");
1639
+ ```
1640
+
1641
+ ```html
1642
+ <!--a标签打开,会自动触发内置浏览器-->
1643
+ <a href="https://www.*****.com/index.html" target="_blank">打开链接</a>
1644
+ ```
1645
+
1646
+ ### 8. capacitor内部缓存时长设置
1647
+
1648
+ 默认请求下使用capacitor的协议栈访问网络,静态资源缓存一天,即24 * 60 * 60秒钟,如果您想自己配置缓存时长在capacitor.config.json里面添加如下配置:
1649
+
1650
+ ```json
1651
+ {
1652
+ "harmony": {
1653
+ "cordova-cache-duration":60
1654
+ }
1655
+ }
1656
+
1657
+ ```