web.view
是 aardio 中用于创建基于 WebView2 (Edge/Chromium 内核) 的浏览器控件的库。WebView2 接口简洁、性能强悍、稳定可靠。
更重要的是 WebView2 是 Win10、Win11 等主流桌面操作系统的自带组件,可生成体积较小的独立 EXE 程序。
首先,我们需要导入必要的库并创建一个窗体:
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 的第二个构造参数可选用一个表对象指定启动参数,示例:
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:
var wb = web.view(winform,userDataDir,startArguments,...);
示例:
//指定多个命令行参数时,由 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`)
web.view
控件提供了多种方式加载网页内容:
// 加载 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 的第一个参数指定了应用程序根目录下的相对路径(以单个斜杠或反斜杠开始),并且该路径位于发布后会嵌入 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 将显示等待页面 ),示例:
wb.go("\web\index.html",37151);
在程序发布后,上面的参数 37151 将被忽略。
首先在网页中添加下面的 JavaScript 全局函数:
<script>
//定义全局函数
window.add = function(a,b){
return a+ b;
}
</script>
在 aardio 中就可以如下调用上面的 JS 函数:
//调用 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 返回值,示例:
wb.doScript("window.location.href",function(result){
winform.msgbox(result,"result 为 JS 返回的值")
})
注意: 所有阻塞调用 JS 的函数都 不会卡界面,等待的同时会继续处理窗口消息。
web.view
可以通过 external 导出网页可以直接访问的 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>
**/
要点:
wb.external
导出的 aardio 对象才能在网页中生效。 aardio
访问 wb.external
对象。在 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 异步调用那些同步阻塞的函数。
在 aardio 中可以用 wb.export 导出 aardio 对象为 JavaScript 全局对象。
要点:
示例:
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 兼容的参数。
注意:
wb.export
导出的 aardio 对象才能在网页中生效。 在网页上右键选择"检查"或按 F12 键可以打开开发者工具 (DevTools),在控制台可以查看 JS 输出的信息与错误信息。
wb.waitEle 可在当前网页内等待指定的 HTML 节点。
用法:
wb.waitEle(selector,callback,timeout)
要特别注意,wb.waitEle 仅在单个页在有效,如果打开其他的网页会导致 wb.waitEle 退出等待。
wb.waitEle2 则可以跨网页等待,用法:
wb.waitEle2(selector,timeout)
selector,timeout 参数的用法与 wb.waitEle 相同。
wb.waitEle2 没有回调参数,只能同步等待。好处是 wb.waitEle2 跨网页有效。如果在等待过程中可能需要打开不同的网页,则应使用支持跨网页的 wb.waitEle2 函数等待节点。
//创建 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();
wb.preloadScript 函数添加的 JavaScript 在网页初始化时执行,保证在 window.onload 事件前执行,每次添加都会增加 JavaScript 脚本而不会覆盖之前添加的脚本。
下面的示例使用 wb.preloadScript 实现禁止在网页按 F5,Ctrl + R 刷新:
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();
调用 CDP 命令的参数格式:
var result = wb.cdp(method,params,callback)
订阅 CDP 事件的参数格式:
wb.cdpSubscribe(event,callback)
调用 wb.cdpSubscribe 函数总是会先取消之前订阅的同名事件。
下面是一个自动关闭弹框的例子:
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 获取页面节点,参数格式:
var ele = wb.cdpQuery(selector,parent,callback)
如果不指定 callback 参数则成功返回表对象,wb.cdpQuery 失败返回 null,错误代码。如果指定 callback 参数则不会等待查询节果而是异步回调 callback 函数,wb.cdpQuery 成功返回 true,失败返回 null,错误代码。
wb.cdpWaitQuery 函数的用法与 wb.cdpQuery 类似,但会一直等待直到找到节点或者超时:
var ele = wb.wb.cdpWaitQuery(selector,parent,timeout)
wb.cdpWaitQuery 与 wb.waitEle2 相同的是可以跨多页页面等待 CSS 选择器指定的节点出现。
示例:
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();
请参考: 多线程入门
web.view 对象可通过线程参数传入工作线程, 跨线程调用将回发到界面线程执行。
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();
wb.onNewWindow 可用于拦截网页弹窗,示例:
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();