# web.view 快速入门指南

## 1. 简介

`web.view` 是 aardio 中用于创建基于 WebView2 (Edge/Chromium 内核) 的浏览器控件的库。WebView2 接口简洁、性能强悍、稳定可靠。

更重要的是 WebView2 是 Win10、Win11 等主流桌面操作系统的自带组件，可生成体积较小的独立 EXE 程序。


## 2. 创建 web.view 控件

首先，我们需要导入必要的库并创建一个窗体:

```aardio
import win.ui;
import web.view;

// 创建主窗口
var winform = win.form(text="web.view 示例");

// 创建 web.view 控件
var wb = web.view(winform);

winform.show();
win.loopMessage();
```

要点:

- 使用 `import web.view` 导入 web.view 库
- 使用 `web.view(winform)` 创建 web.view 控件，参数为宿主窗口（ 可指定主窗体或窗口上的 custom 控件 ）。

web.view 的第二个构造参数可选用一个表对象指定启动参数，示例：

```aardio
var wb = web.view(winform,{ 
	extensions = true; //可选字段，是否允许用 `wb.loadExtension()` 加载浏览器扩展。
	language =  "zh-CN"; //可选字段，用于自定义浏览器界面语言与 Accept-Language 请求头 
	userDataDir = //可选字段，用于自定义用户数据目录，不同目录可隔离会话。
	
	/*
	startArguments 也是可选字段，指定与 Chrome / Edge 浏览器兼容的启动命令行参数。如果 startArguments 是表对象，则参数表中驼峰风格的参数名自动转为连字符格式并添加 -- 前缀。
	*/
	startArguments = { 
		proxyServer = "SOCKS5://代理地址";
		userAgent = "Mozilla/5.0 (Linux; Android 9) AppleWebKit/537.36 Chrome/100.0 Mobile"
	};
})

```

web.view 第三个构造参数以后后续参数指定 startArguments：

```aardio
var wb = web.view(winform,userDataDir,startArguments,...);
```

示例：

```aardio

//指定多个命令行参数时，由 aardio 处理命令行转义并合并为空格分开的单个字符串。
var wb = web.view(winform, ,`--user-agent=Mozilla/5.0`,`--accept-lang=zh-CN`)

//以空格分隔的命令行参数，原始命令行格式自行处理转义
var wb = web.view(winform, ,`--user-agent=Mozilla/5.0 --accept-lang=zh-CN`)
```


## 3. 加载网页内容 <a id="navigate" href="#navigate">&#x23;</a>

`web.view` 控件提供了多种方式加载网页内容:

```aardio
// 加载 URL，不指定  HTTP referrer 请求头
wb.go("https://example.com");

//跳转网址，当前网址设为 HTTP referrer 请求头（引用页面）
wb.location = "https://example.com"

// 加载 HTML 字符串
wb.html = "<html><body><h1>Hello World</h1></body></html>";

// 加载本地 HTML 文件
wb.go("/web/index.html");
```

如果 wb.go 的第一个参数指定了[应用程序根目录下的相对路径（以单个斜杠或反斜杠开始）](../../../builtin/io/path.md#apppath)，并且该路径位于发布后会嵌入 EXE 文件的内嵌资源目录，则我们必须事先引入 aardio 提供的嵌入式 HTTP 服务端，例如：

- `import wsock.tcp.simpleHttpServer` 引入多线程 HTTP 服务端
- `import wsock.tcp.asynHttpServer` 引入单线程异步 HTTP 服务端

aardio 将会将资源路径自动转换为通过嵌入 HTTP 服务端访问的网址，如果 wb.go 指定的是 index.html 的路径，则 index.html 的父目录自动被设置为文档根目录（ documentBase，在网页中通过 `/` 表示该目录 ），并且自动支持 SPA 应用。

如果 wb.go 的第二个参数是一个指定前端开发时的调试端口的数值，则在 aardio 开发环境中启动时会自动连接并等待调试端口可用（端口不可用时 web.view 将显示等待页面 ），示例：

```aardio
wb.go("\web\index.html",37151);
```

在程序发布后，上面的参数 37151 将被忽略。

## 4. 在 aardio 中如何调用 JavaScript <a id="call-javascript" href="#call-javascript">&#x23;</a>


首先在网页中添加下面的 JavaScript 全局函数：

```html
<script> 
//定义全局函数
window.add = function(a,b){
	return a+ b;  
} 
</script>
```

在 aardio 中就可以如下调用上面的 JS 函数：

```aardio
//调用 JS 函数
var result = wb.xcall("add",12,3);

//显示返回值
winform.msgbox(result);
```

wb.xcall 使用 JSON 在 aardio 与 JS 之间转换函数的调用参数与返回值。第一个参数可指定可获取 JS 函数的 JS 表达式。

wb.xcall 会阻塞等待 JS 函数的返回值，如果不需要返回值可改用异步调用的 wb.invoke 调用 JS 函数。wb.xcall 与 wb.invoke 除了是否等待 JS 返回值的区别，其他用法完全一样。

通过 wb.eval 函数可以直接执行 JS 表达式并获取返回值。wb.eval 使用 JSON 将 JS 返回值转换为 aardio 对象。注意 wb.eval 也是阻塞调用函数。

我们也可以通过 wb.doScript 函数执行 JS 代码，并可选指定一个异步非阻塞的回调函数以获取 JS 返回值，示例：

```aardio
wb.doScript("window.location.href",function(result){
	winform.msgbox(result,"result 为 JS 返回的值")
})
```

> 注意： 所有阻塞调用 JS 的函数都 不会卡界面，等待的同时会继续处理窗口消息。

## 5. 使用 wb.external 导出 aardio 对象或函数到 JavaScript <a id="external" href="#external">&#x23;</a>

`web.view` 可以通过 external 导出网页可以直接访问的 aardio 对象，示例:

```aardio
// 导出 aardio 对象到 JavaScript
wb.external = {
    sayHello = function(name){
        winform.msgbox("Hello, " + name);
    }
}

// 网页中调用 aardio 函数
wb.html = /**
<!doctype html>
<html>
<body>
    <button onclick="aardio.sayHello('World')">Click Me</button>
</body>
</html>
**/
```

要点:

- 只有在打开网页或设置网页 HTML 前通过  `wb.external` 导出的 aardio 对象才能在网页中生效。 
- 在 JavaScript 中通过全局变量 `aardio` 访问  `wb.external` 对象。
- 在 JavaScript 中所有的 aardio 对象以及 aardio 对象的方法与属性都被包装为 Promise 对象。
- 在 JavaScript 中调用 wb.external 导出的函数时，参数与返回值都不需要经过 JSON 转换。
除字符串、数值、布尔值、可兼容的数组、buffer 等基础值类型可以直接传值以外，其他对象类型（JavaScript 中的 object 或 aardio 中的表对象 ）自动转换为 COM 代理对象，让我们可以通过 COM 接口间接操作跨语言的原生对象。

	要注意不能将这种 COM 代理对象作为原生 JavaScript 对象使用，
	例如不能用于 JavaScript 图表或表格的数据源对象，而 wb.export 导出的 aardio 函数则无此限制。

- 在 JavaScript 回调 aardio 函数时不应在被回调的 aardio 函数内再通过阻塞调用的 wb.eval 、 wb.xcall 回调 JavaScript 函数，应改用非阻塞的 wb.invoke 、wb.doScript 调用 JS 函数。或者通过 winform.setTimeout 异步调用那些同步阻塞的函数。


**关于 JavaScript 中的 `aardio` 对象：**

> 在 aardio 中运行代码 `wb.external = aardioObject` ，  
会自动调用 `wb.exportHostObject( "external",aardioObject)` 在网页中创建 JavaScript 对象 `chrome.webview.hostObjects.external` ，并且同时执行 `wb.preloadScript("window.aardio = chrome.webview.hostObjects.external;")` 将 JavaScript 的全局变量名 `aardio` 指向 `chrome.webview.hostObjects.external`。
> 
> 在 web.view 在初始化时会自动执行 `window.external ={invoke:s=>window.chrome.webview.postMessage(s)};` 点用了 `window.external` 这个名称，所以在 JavaScript 中需要用 `external` 去访问 aardio 代码中的 `wb.external` (也就是 JavaScript 中的 `chrome.webview.hostObjects.external` ) 。 

web.view 使用 wb.external 或者 wb.exportHostObject 函数导出 aardio 对象到网页都是添加到 JavaScript 对象 chrome.webview.hostObject 中，我们一般都使用 wb.external 而没必要使用 wb.exportHostObject 。chrome.webview.hostObject 的交互基于 COM 接口，来回传参默认都是按 COM 规则。

数据类型转换：

- 字符串、数值、布尔值这些基础类型，以及这些基础类型的数组都相互兼容。
- aardio 中的字符串可以包含二进制数据，但传到 WebView 的 JavaScript 以后只能保留纯文本。
- aardio 中的缓冲区（buffer）类型传到 WebView 的 JavaScript 则为普通数组（ Array ），用这种方法可以直接传二进制数据。如果要转换为其他 JS 类型可用 JavaScript 自行转换。[范例](../../../../example/WebUI/web.view/buffer.html)
- WebView2 网页内的类型化组例如 Uint8Array 传到 aardio 中默认为 COM 对象（ com.IDispatch 对象） ，可以通过下标索引访问数组成员。但是在 COM 对象中默认会隐藏类型化数组的 length 等内置属性，所以在传数组以前可以在 JS 中调用 `Uint8Array.size = Uint8Array.length` 用其他自定义的字段名记录数组长度（ aardio 中可以访问 Uint8Array.size ）。
- WebView2 网页内的时间对象（ Date ） 与 aardio 中的 time 对象相互兼容并可自动转换。[范例](../../../../example/WebUI/web.view/time.html)

	示例：

	```aardio
	import web.view;
	var wb = web.view(winform);
		
	wb.external = {
		testDate = function(jsDateTime){  
	 
			jsDateTime.local() //可选转换为本地时间。
			
			winform.text = jsDateTime;

			return jsDateTime;
		};
	}
	```
	
	* aardio 的时间对象总是转换为 UTC 标准时间传给网页 JavaScript，这不会改变原来的时间对象，如果 aardio 提供的时间对象已经是 UTC 时间则不作转换。
	* 网页 JavaScript 的时间对象传到 aardio 里，aardio 会在格式化串属性里将其标注为 UTC 时间（也就是在 format 属性的第一个字符加上 `!`符号）, 时间对象通过这个标识可自动区分本地时间与 UTC 时间，并且可以必要时自动或手动转换本地时间与标准时间，UTC 格式标注也可以避免重复或不必要的转换操作。


## 6. 使用 wb.export  导出 JavaScript 全局对象。 <a id="export" href="#export">&#x23;</a>

在 aardio 中可以用 wb.export 导出 aardio 对象为 JavaScript 全局对象。

要点: 

- 被导出的 aardio 函数在 JavaScript 中都是 Promise 对象。
- JavaScript 调用这些 aardio 函数时将会使用 JSON 自动转换跨编程语言的参数和返回值。调用参数会被转换为纯 aardio 对象，而 aardio 函数的返回值则会转换为纯 JavaScript 对象。

示例：

```aardio
wb.export({
    alert = function(msg){
		winform.msgbox(msg) 
		
		//要避免重入，例如：不能在 alert 回调中再次调用 alert 函数
		//wb.invoke("alet('test')");
	};
	nativeAdd = function(a,b){ 
		return a + b; 
	}
})
```

上面的参数是一个表，表中每个元素在等号前面的键名为导出到 JavaScript 的全局变量名称，等号后面的值为导出到 JavaScript 的全局对象。

wb.export 导出的函数使用 JSON 协议在 aardio 与 JavaScript 之间自动转换参数和返回值，JS 函数通常无需特别处理就默认支持 JSON 兼容的参数。

注意：

- 只有在打开网页或设置网页 HTML 前通过  `wb.export`导出的 aardio 对象才能在网页中生效。 
- 在 JavaScript 回调 aardio 函数时不应在被回调的 aardio 函数内再通过阻塞调用的 wb.eval 、 wb.xcall 回调 JavaScript 函数，应改用非阻塞的 wb.invoke 、wb.doScript 调用 JS 函数。或者通过 winform.setTimeout 异步调用 wb.eval, wb.xcall。

## 7. 网页调试 <a id="DevTools" href="#DevTools">&#x23;</a>


在网页上右键选择"检查"或按 F12 键可以打开开发者工具 (DevTools)，在控制台可以查看 JS 输出的信息与错误信息。

## 8. 等待网页节点 <a id="waitEle" href="#waitEle">&#x23;</a>

wb.waitEle 可在当前网页内等待指定的 HTML 节点。

用法：

```aardio
wb.waitEle(selector,callback,timeout)
```

- selector 参数指定匹配等待节点的 CSS 选择器。
- callback 为可选参数，可指定 aardio 回调函数，也可以是一个包含  JavaScript 回调脚本的字符串。
	- 如是是回调函数，成功找到指定的节点时第一个回调参数为 CSS 选择器，失败则第一个回调参数为 null ，第二个回调参数为错误信息。
	- 如果指定 JavaScript 回调脚本，则脚本中可用 this 对象访问找到的节点。
	- 如果不指定 callback 参数则同步等待（函数退出代码才会向后执行）,否则异步等待（代码继续向后执行）。

要特别注意，wb.waitEle 仅在单个页在有效，如果打开其他的网页会导致 wb.waitEle 退出等待。

wb.waitEle2 则可以跨网页等待，用法：

```aardio
wb.waitEle2(selector,timeout)
```

selector,timeout 参数的用法与 wb.waitEle 相同。

wb.waitEle2 没有回调参数，只能同步等待。好处是 wb.waitEle2 跨网页有效。如果在等待过程中可能需要打开不同的网页，则应使用支持跨网页的 wb.waitEle2 函数等待节点。

参考链接： [等待并自动化操作网页元素](automation.md#waitEle)

示例：

```aardio
//创建 winform 窗口
import win.ui; 
var winform = win.form(text="窗口标题")

//创建 WebView2 浏览器控件
import web.view;
var wb = web.view(winform);

// 导出 aardio 函数
wb.external = {
	add: function(a, b) {
		return a + b;
	}	
} 

// 指定网页 HTML 代码
wb.html = /******
<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
</head><body> 
<div id="result"></div>

<script> 
(async ()=>{
	
	//调用 aardio 导出的 wb.external.add 函数。
	var num = await aardio.add(1, 2)

	//显示结果
	document.getElementById('result').innerText = num;
})()
</script>
******/;

//在当前页面等待指定节点，改用 wb.waitEle2 则支持跨网页等待
wb.waitEle("#result","this.style.color='red'");

//显示窗口
winform.show();

//启动界面消息循环
win.loopMessage();
```

## 9. 网页加载事件 <a id="onDocumentInit" href="#onDocumentInit">&#x23;</a>

按触发先后顺序列举如下：

- `wb.onDocumentInit` 事件，网页初始化创建文档对象（ 也就是通过 JavaScript 脚本里可以访问 document 对象 ）时触发此事件。
- `wb.onDocumentComplete` 事件，网页 DOM 准备就绪触发此事件，也就是在网页的 document 对象的 DOMContentLoaded 事件触发时执行。
- `wb.onLoad` 事件，此时整个页面及所有依赖资源如样式表和图片都已完成加载，也就是在网页的 window 对象的 load 事件触发时执行。

示例：

```aardio
import win.ui;
/*DSG{{*/
var winform = win.form(text="WebView2")
/*}}*/

import web.view;
var wb = web.view(winform);

//网页文档对象已初始化。
wb.onDocumentInit = function(url){
	// url 为 当前打开的网址 
}

wb.onDocumentComplete = function(url){
	
	//耗时操作可用下面的方法延时执行，不要妨碍网页事件及时返回
	wb.setTimeout( 
		function(){
			//对话框只会阻塞执行，并不会阻塞界面线程的消息循环。
			winform.msgbox("网页已下载")
			
			//如果会随塞消息循环的耗时操作应当放在工作线程里，例如
			
			thread.invoke( 
				function(){
					
					//耗时操作
					sleep(1000)
				}
			)	
		}
	)
}

//打开网页
wb.go("http://www.example.com")

winform.show();
win.loopMessage();
```

## 10. 弹窗事件 wb.onNewWindow <a id="onNewWindow" href="#onNewWindow">&#x23;</a>


wb.onNewWindow 可用于拦截网页弹窗，示例：

```aardio
import win.ui;
/*DSG{{*/
var winform = win.form(text="拦截网页弹窗";right=818;bottom=507)
/*}}*/

import web.view; 
var wb = web.view(winform);

//弹出新窗口触发
wb.onNewWindow = function(url){
	
	//耗时操作应返回异步自动执行的函数（提前结束 onNewWindow）
	return function(){ 
		
		//如果打开的是 file: 前缀网址，例如拖放文件到网页上。
			var filePath = inet.url.getFilePath(url)
			
			if(filePath){
				winform.msgbox(filePath,"本地文件"); 	
			}
			else {
				//用 wb.location 跳转才会指定 HTTP referrer 请求头
				wb.location = url;
			} 
		}
}
	
wb.html = /**
<html><head>
<base target="_blank" />
</head>

<a href="http://www.aardio.com">aardio.com</a>
<button onclick="window.open('http://www.aardio.com')" >aardio.com</button>
**/

winform.show();
win.loopMessage();
```

## 11. 使用 wb.preloadScript 添加网页默认加载执行的 JavaScript。 <a id="preloadScript" href="#preloadScript">&#x23;</a>

wb.preloadScript 函数添加的 JavaScript 在网页初始化时执行，保证在 window.onload 事件前执行，每次添加都会增加 JavaScript 脚本而不会覆盖之前添加的脚本。

下面的示例使用 wb.preloadScript 实现禁止在网页按 F5，Ctrl + R 刷新：

```aardio
import win.ui;
/*DSG{{*/
var winform = win.form(text="WebView2 浏览器控件 - 禁止按 F5，Ctrl + R 刷新")
/*}}*/

import web.view;
var wb = web.view(winform);
 
var initScript = /****

//禁止页面刷新
document.onkeydown = function (e) {
    if (e.key == "F5" || (e.ctrlKey && e.key == "r") ) {
        e.preventDefault(); 
    }
} 
 
//禁止滚轮缩放
document.addEventListener('wheel', function(e) {
    if(e.ctrlKey) {
        e.preventDefault();
    }
}, { passive: false });

****/

//添加网页默认加载执行的 JavaScript
wb.preloadScript(initScript)

//打开网页
wb.go("https://www.example.com")

winform.show();
win.loopMessage();
```

## 12.  CDP（Chrome DevTools Protocol） 接口 <a id="cdp" href="#cdp">&#x23;</a>


调用 CDP 命令的参数格式：

```aardio
var result = wb.cdp(method,params,callback) 
```

- method 参数指定 CDP 命令，例如 "Page.enable"
- params 参数可选用一个表（table）或 JSON 字符串指定 CDP 参数
- callback 参数可选指定一个回调函数，回调参数@1为 CDP 返回的表对象，回调参数@2 为错误代码。如果指定 callback 则 wb.cdp 不等待 CDP 命令执行结束而是异步回调 callback，如果不指定 callback 参数则 wb.cdp 会等待命令执行结束并且成功返回一个表对象（ result ），失败返回 null,错误代码。

订阅 CDP 事件的参数格式：

```aardio
wb.cdpSubscribe(event,callback)
```

调用 wb.cdpSubscribe 函数总是会先取消之前订阅的同名事件。

- event 参数指定要订阅的事件名，例如 "Page.javascriptDialogOpening"。
- callback 指定回调函数，如果不指定此参数（值为 null）则取消订阅。回调函数 callback 的回调参数 @1 是一个表对象。如果在回调函数内部返回一个函数对象，则先结束 CDP 事件，然后异步执行返回的那个函数对象。

下面是一个自动关闭弹框的例子：

```aardio
import win.ui;
/*DSG{{*/
var winform = win.form(text="CDP 事件 - 自动关闭网页上弹出信息框")
/*}}*/

import web.view; 
var wb = web.view(winform);
winform.show();
 
//调用 cdp 命令。 
wb.cdp("Page.enable");

//订阅 CDP 事件
wb.cdpSubscribe("Page.javascriptDialogOpening",function(dlg){
 
	//为避免阻塞导致某些网页出现异常，应返回异步执行的函数关闭弹框。
	return function(){
		
		//自动关闭弹框，CDP 参数为 {accept=true}
		wb.cdp("Page.handleJavaScriptDialog",{accept=true});
		
		//调用 JS 函数打印 dlg 参数：dlg.message 是对话框文本，dlg.type 是对话框类型，dlg.url 是对话框所在页面网址 。
		wb.xcall("(v)=>document.write( JSON.stringify(v) )",dlg); 
	} 
})
 
wb.html = /**
<script type="text/javascript">alert("测试弹框")</script>
**/
win.loopMessage();
```

使用 wb.cdpQuery 获取页面节点，参数格式：

```aardio
var ele = wb.cdpQuery(selector,parent,callback)
```

- 参数 selector 指定 CSS 选择器
- 可选参数 parent 可用于指定父节点或父节点的 nodeId。不指定则以根节点为父节点。
- 可选用参数 callback 指定回调函数，回调参数@1为 CDP 返回的表对象，回调参数@2 为错误代码。

如果不指定 callback 参数则成功返回表对象，wb.cdpQuery 失败返回 null，错误代码。如果指定 callback 参数则不会等待查询节果而是异步回调 callback 函数，wb.cdpQuery 成功返回 true，失败返回 null,错误代码。
 
wb.cdpWaitQuery 函数的用法与 wb.cdpQuery 类似，但会一直等待直到找到节点或者超时：

```aardio
var ele = wb.wb.cdpWaitQuery(selector,parent,timeout)
```

- 参数 selector 指定 CSS 选择器。
- 可选参数 parent 可用于指定父节点或父节点的 nodeId。
- 可选用 timeout 参数指定超时（以毫秒为单位）, 其他与。

wb.cdpWaitQuery 与  wb.waitEle2  相同的是可以跨多页页面等待  CSS 选择器指定的节点出现。

示例：

```aardio
import win.ui;
/*DSG{{*/
var winform = win.form(text="web.view - 调用 CDP 命令修改文件输入框路径")
/*}}*/

import web.view;
var wb = web.view(winform);

wb.html = `<input type="file">`

//获取控件
var fileInput = wb.cdpWaitQuery(`input[type="file"]`);

//设置文件路径
fileInput.files = { io._exepath }; 

//设置文件路径，fileInput 参数指定了 nodeId 与 files 这两个字段的值
wb.cdp("DOM.setFileInputFiles",fileInput) 

winform.show();
win.loopMessage();
```

## 13. 多线程调用 web.view 对象 <a id="thread" href="#thread">&#x23;</a>


请参考： [多线程入门](../../../../guide/language/thread.md)

web.view 对象可通过线程参数传入工作线程， 跨线程调用将回发到界面线程执行。 

```aardio
import win.ui;
/*DSG{{*/
var winform = win.form(text="多线程界面回调") 
/*}}*/

import web.view;
var wb = web.view(winform);

//在网页 JS 脚本中通过全局变量名 aardio 调用 wb.external
wb.external = {
	ping = function(domain){
		
		//创建工作线程
		thread.invoke( 
			function(wb,domain){
				
				import process.popen; 
				
				//创建进程管道
				var prcs = process.popen("ping "+ domain);  
				
				for stdout in prcs.each()  {
					wb.invoke("document.body.insertAdjacentText",'beforeend',stdout); 
				} 
				
			},wb,domain //线程启动参数
		)  
	} 
}

wb.html = /**
<body style="white-space: pre;">
<button 
	onclick="javascript:aardio.ping('www.example.com')"
	>开始干活了</button>
**/

winform.show();
win.loopMessage();
```

