1# 使用Web组件的下载能力 2<!--Kit: ArkWeb--> 3<!--Subsystem: Web--> 4<!--Owner: @aohui--> 5<!--Designer: @yaomingliu--> 6<!--Tester: @ghiker--> 7<!--Adviser: @HelloCrease--> 8 9当需要通过Web页面进行文件下载时,可以通过此方式调用Web接口。 10 11## 监听页面触发的下载 12 13通过[setDownloadDelegate()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#setdownloaddelegate11)向Web组件注册一个DownloadDelegate来监听页面触发的下载任务。资源由Web组件来下载,Web组件会通过DownloadDelegate将下载的进度通知给应用。 14 15下面的示例中,在应用的rawfile中创建index.html以及download.html。应用启动后会创建一个Web组件并加载index.html,点击setDownloadDelegate按钮向Web组件注册一个DownloadDelegate,点击页面里的下载按钮的时候会触发一个下载任务,在DownloadDelegate中可以监听到下载的进度。 16 17默认路径在应用沙箱的web目录内,用户无法查看。如果希望用户能够查看,需要将下载路径修改到有访问权限的目录,比如Download目录,请参考[使用Web组件发起一个下载任务](#使用web组件发起一个下载任务)。 18 19```ts 20// xxx.ets 21import { webview } from '@kit.ArkWeb'; 22import { BusinessError } from '@kit.BasicServicesKit'; 23 24@Entry 25@Component 26struct WebComponent { 27 controller: webview.WebviewController = new webview.WebviewController(); 28 delegate: webview.WebDownloadDelegate = new webview.WebDownloadDelegate(); 29 30 build() { 31 Column() { 32 Button('setDownloadDelegate') 33 .onClick(() => { 34 try { 35 this.delegate.onBeforeDownload((webDownloadItem: webview.WebDownloadItem) => { 36 console.log("will start a download."); 37 // 传入一个下载路径,并开始下载。 38 // 如果传入一个不存在的路径,则会下载到默认/data/storage/el2/base/cache/web/目录。 39 webDownloadItem.start("/data/storage/el2/base/cache/web/" + webDownloadItem.getSuggestedFileName()); 40 }) 41 this.delegate.onDownloadUpdated((webDownloadItem: webview.WebDownloadItem) => { 42 // 下载任务的唯一标识。 43 console.log("download update guid: " + webDownloadItem.getGuid()); 44 // 下载的进度。 45 console.log("download update percent complete: " + webDownloadItem.getPercentComplete()); 46 // 当前的下载速度。 47 console.log("download update speed: " + webDownloadItem.getCurrentSpeed()) 48 }) 49 this.delegate.onDownloadFailed((webDownloadItem: webview.WebDownloadItem) => { 50 console.error("download failed guid: " + webDownloadItem.getGuid()); 51 // 下载任务失败的错误码。 52 console.error("download failed last error code: " + webDownloadItem.getLastErrorCode()); 53 }) 54 this.delegate.onDownloadFinish((webDownloadItem: webview.WebDownloadItem) => { 55 console.log("download finish guid: " + webDownloadItem.getGuid()); 56 }) 57 this.controller.setDownloadDelegate(this.delegate); 58 } catch (error) { 59 console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); 60 } 61 }) 62 Web({ src: $rawfile('index.html'), controller: this.controller }) 63 } 64 } 65} 66``` 67 68加载的html文件。 69```html 70<!-- index.html --> 71<!DOCTYPE html> 72<html> 73<body> 74// 点击视频右下方菜单的下载按钮会触发下载任务。 75<video controls="controls" width="800px" height="580px" 76 src="http://vjs.zencdn.net/v/oceans.mp4" 77 type="video/mp4"> 78</video> 79<a href='data:text/html,%3Ch1%3EHello%2C%20World%21%3C%2Fh1%3E' download='download.html'>下载download.html</a> 80</body> 81</html> 82``` 83 84待下载的html文件。 85```html 86<!-- download.html --> 87<!DOCTYPE html> 88<html> 89<body> 90<h1>download test</h1> 91</body> 92</html> 93``` 94 95## 使用Web组件发起一个下载任务 96 97使用[startDownload()](../reference/apis-arkweb/arkts-apis-webview-WebviewController.md#startdownload11)接口发起一个下载。 98Web组件发起的下载会根据当前显示的url以及Web组件默认的Referrer Policy来计算referrer。 99 100 在下面的示例中,先点击setDownloadDelegate按钮向Web注册一个监听类,然后点击startDownload主动发起了一个下载, 101 该下载任务也会通过设置的DownloadDelegate来通知app下载的进度。 102 103```ts 104// xxx.ets 105import { webview } from '@kit.ArkWeb'; 106import { BusinessError } from '@kit.BasicServicesKit'; 107 108@Entry 109@Component 110struct WebComponent { 111 controller: webview.WebviewController = new webview.WebviewController(); 112 delegate: webview.WebDownloadDelegate = new webview.WebDownloadDelegate(); 113 114 build() { 115 Column() { 116 Button('setDownloadDelegate') 117 .onClick(() => { 118 try { 119 this.delegate.onBeforeDownload((webDownloadItem: webview.WebDownloadItem) => { 120 console.log("will start a download."); 121 // 传入一个下载路径,并开始下载。 122 // 如果传入一个不存在的路径,则会下载到默认/data/storage/el2/base/cache/web/目录。 123 webDownloadItem.start("/data/storage/el2/base/cache/web/" + webDownloadItem.getSuggestedFileName()); 124 }) 125 this.delegate.onDownloadUpdated((webDownloadItem: webview.WebDownloadItem) => { 126 console.log("download update guid: " + webDownloadItem.getGuid()); 127 }) 128 this.delegate.onDownloadFailed((webDownloadItem: webview.WebDownloadItem) => { 129 console.error("download failed guid: " + webDownloadItem.getGuid()); 130 }) 131 this.delegate.onDownloadFinish((webDownloadItem: webview.WebDownloadItem) => { 132 console.log("download finish guid: " + webDownloadItem.getGuid()); 133 }) 134 this.controller.setDownloadDelegate(this.delegate); 135 } catch (error) { 136 console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); 137 } 138 }) 139 Button('startDownload') 140 .onClick(() => { 141 try { 142 // 这里指定下载地址为 https://www.example.com/,Web组件会发起一个下载任务将该页面下载下来。 143 // 开发者需要替换为自己想要下载的内容的地址。 144 this.controller.startDownload('https://www.example.com/'); 145 } catch (error) { 146 console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); 147 } 148 }) 149 Web({ src: 'www.example.com', controller: this.controller }) 150 } 151 } 152} 153``` 154 155使用[DocumentViewPicker()](../reference/apis-core-file-kit/js-apis-file-picker.md#documentviewpicker)获取当前示例的默认下载目录,将该目录设置为下载目录。 156```ts 157// xxx.ets 158import { webview } from '@kit.ArkWeb'; 159import { BusinessError } from '@kit.BasicServicesKit'; 160import { picker, fileUri } from '@kit.CoreFileKit'; 161@Entry 162@Component 163struct WebComponent { 164 controller: webview.WebviewController = new webview.WebviewController(); 165 delegate: webview.WebDownloadDelegate = new webview.WebDownloadDelegate(); 166 167 build() { 168 Column() { 169 Button('setDownloadDelegate') 170 .onClick(() => { 171 try { 172 this.delegate.onBeforeDownload((webDownloadItem: webview.WebDownloadItem) => { 173 console.log("will start a download."); 174 // 使用DocumentViewPicker()获取当前示例的默认下载目录,将该目录设置为下载目录 175 getDownloadPathFromPicker().then((downloadPath) => { 176 webDownloadItem.start(downloadPath + '/' + webDownloadItem.getSuggestedFileName()); 177 }); 178 179 }) 180 this.delegate.onDownloadUpdated((webDownloadItem: webview.WebDownloadItem) => { 181 // 下载任务的唯一标识。 182 console.log("download update guid: " + webDownloadItem.getGuid()); 183 // 下载的进度。 184 console.log("download update percent complete: " + webDownloadItem.getPercentComplete()); 185 // 当前的下载速度。 186 console.log("download update speed: " + webDownloadItem.getCurrentSpeed()) 187 }) 188 this.delegate.onDownloadFailed((webDownloadItem: webview.WebDownloadItem) => { 189 console.error("download failed guid: " + webDownloadItem.getGuid()); 190 // 下载任务失败的错误码。 191 console.error("download failed last error code: " + webDownloadItem.getLastErrorCode()); 192 }) 193 this.delegate.onDownloadFinish((webDownloadItem: webview.WebDownloadItem) => { 194 console.log("download finish guid: " + webDownloadItem.getGuid()); 195 }) 196 this.controller.setDownloadDelegate(this.delegate); 197 } catch (error) { 198 console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); 199 } 200 }) 201 Web({ src: $rawfile('index.html'), controller: this.controller }) 202 } 203 } 204 205} 206function getDownloadPathFromPicker(): Promise<string> { 207 return new Promise<string>(resolve => { 208 try { 209 const documentSaveOptions = new picker.DocumentSaveOptions(); 210 documentSaveOptions.pickerMode = picker.DocumentPickerMode.DOWNLOAD 211 const documentPicker = new picker.DocumentViewPicker(); 212 documentPicker.save(documentSaveOptions).then(async (documentSaveResult: Array<string>) => { 213 if (documentSaveResult.length <= 0) { 214 resolve(''); 215 return; 216 } 217 const uriString = documentSaveResult[0]; 218 if (!uriString) { 219 resolve(''); 220 return; 221 } 222 const uri = new fileUri.FileUri(uriString); 223 resolve(uri.path); 224 }).catch((err: BusinessError) => { 225 console.error(`ErrorCode: ${err.code}, Message: ${err.message}`); 226 resolve(''); 227 }); 228 } catch (error) { 229 resolve(''); 230 } 231 }) 232} 233``` 234 235> **说明:** 236> 237>Web组件的下载功能要求应用通过调用[WebDownloadItem.start](../reference/apis-arkweb/arkts-apis-webview-WebDownloadItem.md#start11)来指定下载文件的保存路径。 238> 239>值得注意的是,WebDownloadItem.start并非启动下载,下载过程实际上在用户点击页面链接时即已开始。WebDownloadItem.start的作用是将已经下载到临时文件的部分移动到指定目标路径,后续未完成的下载的内容将直接保存到指定目标路径,临时目录位于`/data/storage/el2/base/cache/web/Temp/`。如果决定取消当前下载,应调用[WebDownloadItem.cancel](../reference/apis-arkweb/arkts-apis-webview-WebDownloadItem.md#cancel11),此时临时文件将被删除。 240> 241>如果不希望在WebDownloadItem.start之前将文件下载到临时目录,可以通过WebDownloadItem.cancel中断下载,后续可通过[WebDownloadManager.resumeDownload](../reference/apis-arkweb/arkts-apis-webview-WebDownloadManager.md#resumedownload11)恢复中断的下载。 242 243## 使用Web组件恢复进程退出时未下载完成的任务 244在Web组件启动时,可通过[resumeDownload()](../reference/apis-arkweb/arkts-apis-webview-WebDownloadManager.md#resumedownload11)接口恢复未完成的下载任务。 245 246在以下示例中,通过“record”按钮将当前下载任务保存至持久化文件中,应用重启后,可借助“recovery”按钮恢复持久化的下载任务。示例代码实现了将当前下载任务持久化保存至文件的功能,若需保存多个下载任务,应用可根据需求调整持久化的时机与方式。 247```ts 248// xxx.ets 249import { webview } from '@kit.ArkWeb'; 250import { BusinessError } from '@kit.BasicServicesKit'; 251import { downloadUtil, fileName, filePath } from './downloadUtil'; // downloadUtil.ets 见下文 252 253@Entry 254@Component 255struct WebComponent { 256 controller: webview.WebviewController = new webview.WebviewController(); 257 delegate: webview.WebDownloadDelegate = new webview.WebDownloadDelegate(); 258 download: webview.WebDownloadItem = new webview.WebDownloadItem(); 259 // 用于记录失败的下载任务。 260 failedData: Uint8Array = new Uint8Array(); 261 262 aboutToAppear(): void { 263 downloadUtil.init(this.getUIContext()); 264 } 265 266 build() { 267 Column() { 268 Button('setDownloadDelegate') 269 .onClick(() => { 270 try { 271 this.delegate.onBeforeDownload((webDownloadItem: webview.WebDownloadItem) => { 272 console.log("will start a download."); 273 // 传入一个下载路径,并开始下载。 274 // 如果传入一个不存在的路径,则会下载到默认/data/storage/el2/base/cache/web/目录。 275 webDownloadItem.start("/data/storage/el2/base/cache/web/" + webDownloadItem.getSuggestedFileName()); 276 }) 277 this.delegate.onDownloadUpdated((webDownloadItem: webview.WebDownloadItem) => { 278 console.log("download update percent complete: " + webDownloadItem.getPercentComplete()); 279 this.download = webDownloadItem; 280 }) 281 this.delegate.onDownloadFailed((webDownloadItem: webview.WebDownloadItem) => { 282 console.error("download failed guid: " + webDownloadItem.getGuid()); 283 // 序列化失败的下载任务到一个字节数组。 284 this.failedData = webDownloadItem.serialize(); 285 }) 286 this.delegate.onDownloadFinish((webDownloadItem: webview.WebDownloadItem) => { 287 console.log("download finish guid: " + webDownloadItem.getGuid()); 288 }) 289 this.controller.setDownloadDelegate(this.delegate); 290 webview.WebDownloadManager.setDownloadDelegate(this.delegate); 291 } catch (error) { 292 console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); 293 } 294 }) 295 Button('startDownload') 296 .onClick(() => { 297 try { 298 // 这里指定下载地址为 https://www.example.com/,Web组件会发起一个下载任务将该页面下载下来。 299 // 开发者需要替换为自己想要下载的内容的地址。 300 this.controller.startDownload('https://www.example.com/'); 301 } catch (error) { 302 console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); 303 } 304 }) 305 // 将当前的下载任务信息序列化保存,用于后续恢复下载任务。 306 // 当前用例仅展示下载一个任务的场景,多任务场景请按需扩展。 307 Button('record') 308 .onClick(() => { 309 try { 310 // 保存当前下载数据到持久化文档中。 311 downloadUtil.saveDownloadInfo(downloadUtil.uint8ArrayToStr(this.download.serialize())); 312 } catch (error) { 313 console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); 314 } 315 }) 316 // 从序列化的下载任务信息中,恢复下载任务。 317 // 按钮触发时必须保证WebDownloadManager.setDownloadDelegate设置完成。 318 Button('recovery') 319 .onClick(() => { 320 try { 321 // 当前默认持久化文件存在,用户根据实际情况增加判断。 322 let webDownloadItem = 323 webview.WebDownloadItem.deserialize(downloadUtil.strToUint8Array(downloadUtil.readFileSync(filePath, fileName))); 324 webview.WebDownloadManager.resumeDownload(webDownloadItem); 325 } catch (error) { 326 console.error(`ErrorCode: ${(error as BusinessError).code}, Message: ${(error as BusinessError).message}`); 327 } 328 }) 329 330 Web({ src: 'www.example.com', controller: this.controller }) 331 } 332 } 333} 334``` 335 336下载任务信息持久化工具类文件。 337```ts 338// downloadUtil.ets 339import { util } from '@kit.ArkTS'; 340import fileStream from '@ohos.file.fs'; 341 342const helper = new util.Base64Helper(); 343 344export let filePath : string; 345export const fileName = 'demoFile.txt'; 346export namespace downloadUtil { 347 348 export function init(context: UIContext): void { 349 filePath = context.getHostContext()!.filesDir; 350 } 351 352 export function uint8ArrayToStr(uint8Array: Uint8Array): string { 353 return helper.encodeToStringSync(uint8Array); 354 } 355 356 export function strToUint8Array(str: string): Uint8Array { 357 return helper.decodeSync(str); 358 } 359 360 export function saveDownloadInfo(downloadInfo: string): void { 361 if (!fileExists(filePath)) { 362 mkDirectorySync(filePath); 363 } 364 365 writeToFileSync(filePath, fileName, downloadInfo); 366 } 367 368 export function fileExists(filePath: string): boolean { 369 try { 370 return fileStream.accessSync(filePath); 371 } catch (error) { 372 return false; 373 } 374 } 375 376 export function mkDirectorySync(directoryPath: string, recursion?: boolean): void { 377 try { 378 fileStream.mkdirSync(directoryPath, recursion ?? false); 379 } catch (error) { 380 console.error(`mk dir error. err message: ${error.message}, err code: ${error.code}`); 381 } 382 } 383 384 export function writeToFileSync(dir: string, fileName: string, msg: string): void { 385 let file = fileStream.openSync(dir + '/' + fileName, fileStream.OpenMode.WRITE_ONLY | fileStream.OpenMode.CREATE); 386 fileStream.writeSync(file.fd, msg); 387 } 388 389 export function readFileSync(dir: string, fileName: string): string { 390 return fileStream.readTextSync(dir + '/' + fileName); 391 } 392 393} 394``` 395