放棄 Electron,擁抱 WebView2!JavaScript 快速開發獨立 EXE 程式

語言: CN / TW / HK

Electron 不錯,但也不是完美的。

Electron 帶來了很多優秀的桌面軟體,但並不一定總是適合我們的需求。

多個選擇總是好事!

我使用 Electron 遇到的一些麻煩

1、Electron 太大了!

2、每一個 Electron 寫的軟體都要重複地帶一個 Electron …… 升級與分發都不方便。

3、Electron 不方便嵌入其他視窗介面,與其他語言、技術融合不易。

4、並不是所有桌面軟體都需要 Electron 的跨平臺特性。macOS , Linux 的桌面系統市場份額小於被遺忘的 Windows 8 ,如果軟體只是在 Windows 平臺執行,並且需要大量與專用系統 API 互動,跨平臺反而是不必要的負擔。

5、我曾經在 aardio 中封裝了一個 electron 擴充套件庫,然後我在寫這個擴充套件庫的時候,當時看到的還是 remote 真香 …… 然後我為這個擴充套件庫寫了個很大的 JS 檔案就用到了 remote。可是等我寫完沒多久, 就看到 remote 被 Electron 拋棄了,remote 會慢一萬倍 ,各種缺陷 ……

▶  WebView2 的優勢

1、WebView2 基於效能強悍的 Edge(Chromium) 核心。

2、呼叫 WebView2 生成的軟體體積很小。所有基於 WebView2 的軟體可以共享同一個 WebView2 元件。Win11 已經內建 WebView2 元件,其他作業系統也可以快速地自動安裝 WebView2 。

3、WebView2 介面非常簡潔,嵌入其他視窗介面也非常方便。

總結一句話就是:WebView2 簡單、好用、生成軟體體積小。

aardio 標準庫中的 web.view 就是基於 WebView2。WebView2 的介面是如此簡潔,所以我寫的這個庫也只有很少的程式碼。因為 aardio 可以將網頁自動內嵌到獨立 EXE 檔案,就可以非常方便地生成獨立 EXE 程式。

▶  一個最簡單的程式演示

下面我們用 aardio 呼叫 web.view (WebView2)寫一個最簡單的程式:

import win.ui;
/*DSG{{*/
mainForm = win.form(text="WebView2")
mainForm.add(
btnCallJs={cls="button";text="呼叫 JS 函式";left=461;top=395;right=726;bottom=449;note="點這裡呼叫 JavaScript  函式";z=1};
custom={cls="custom";left=17;top=21;right=730;bottom=356;z=2}
)
/*}}*/

//建立瀏覽器元件
import web.view;
var wb = web.view(mainForm.custom);

//匯出本地函式給網頁 JavaScript
wb.external = {
    getComputerName = function(){
        return sys.getComputerName();
    }
}
import sys;

//寫入網頁 HTML
wb.html = /**
<html>
<head>
    <script> 
    (async ()=>{ 
        var n = await aardio.getComputerName();
        alert(n);
    })()
    </script>
</head>
<body>
**/

//響應按鈕事件
mainForm.btnCallJs.oncommand = function(id,event){
    //呼叫 JS 函式
    wb.xcall("document.write","測試")
}

mainForm.show();
win.loopMessage();

對,這就是一個完整程式的原始碼,可以一鍵生成獨立 EXE 檔案。

▶  入門

首先點選 「aardio 主選單 > 新建工程 > 視窗程式 > 空白工程」,然後點選「建立工程」。

如果熟悉網頁前端開發,也可以點選 「 新建工程 > Web 介面 > WebView2 」建立工程。

雙擊工程入口程式碼 main.aardio 開啟主視窗,自「介面控制元件」中拖一個 「呼叫 JS 函式」的按鈕上去,再拖一個 custom 控制元件到窗體上 —— 用來嵌入網頁:

然後切換到程式碼檢視,新增以下程式碼建立網頁瀏覽器:

import web.view;
var wb = web.view(mainForm.custom);

web.view 的第 1 個引數指定要嵌入 WebView2 的視窗物件,該引數可以是 mainForm.custom 這樣的控制元件視窗,也可以是 mainForm 這樣的窗體物件。

下面使用

wb.html = "<html></html>"

就可以寫網頁 HTML 程式碼了。

或者使用

wb.go("網址")

可以開啟指定的網頁。

使用

import wsock.tcp.simpleHttpServer;
wb.go("\res\index.html");

可以開啟資源目錄的網頁,支援SPA 單頁應用。資源目錄可以嵌入 EXE 生成 獨立 EXE 檔案,放心不用多寫其他程式碼。

新增下面的程式碼匯出 external 物件給網頁 JavaScript :

//匯出本地函式給網頁 JavaScript
wb.external = {
    getComputerName = function(){
        return sys.getComputerName();
    }
}

import sys;

在網頁 JavaScript 裡可以呼叫上面匯出的 external 物件,不過在 JavaScript 裡要用 aardio 這個名字表示 external 物件,網頁程式碼如下:

wb.html = /**
<html>
<head>
    <script> 
    (async ()=>{ 
        var n = await aardio.getComputerName();
        alert(n);
    })()
    </script>
</head>
<body>
**/

注意在 aardio 中 /* 註釋 */ 可以作為字串賦值給其他變數,請參考: aardio 程式語言快速入門——語法速覽

要注意所有 aardio 物件在 JavaScript 中都是非同步 Promise 物件。如上在 async 函式體內可以愉快地使用 await 呼叫 aardio 函式 —— 這非常方便。

我們在窗體設計檢視雙擊「呼叫 JS 函式」按鈕,這會切換到程式碼檢視,並自動新增以下回調函式:

mainForm.btnCallJs.oncommand = function(id,event){
    
}

使用者點選按鈕時就會呼叫上面的函式。

小改一下新增 aardio 程式碼呼叫 JavaScript 函式:

//響應按鈕事件
mainForm.btnCallJs.oncommand = function(id,event){
    //呼叫 JS 函式
    wb.xcall("document.write","測試")
}

很簡單,一個程式就寫好了。可以在 aardio 中點選「執行」按鈕直接執行程式碼,也可以點選「釋出」按鈕直接生成 EXE 檔案。

▶  如何將網頁顯示在窗體的指定位置?並且支援自動縮放?

web.view() 建構函式的第 1 個嵌入視窗引數可以是 win.form 物件(獨立視窗),也可以是 custom, static 這樣的普通控制元件物件。例如前面的例子就是將 WebView2 嵌入 custom 控制元件:

import web.view;
var wb = web.view(mainForm.custom);

aardio 中的所有控制元件都可以非常方便的支援自動縮放。只要簡單地在窗體設計器中選定控制元件,然後在「屬性」面板設定「固定邊距」、「自適應大小」這些屬性就可以。

一個更簡單的方法是在窗體設計器上點右鍵,然後在彈出選單中點選「九宮格縮放佈局」—— aardio 將會自動設定所有控制元件的自適應縮放屬性。

至於網頁內容自適應排版很簡單,不需要在 aardio 中編寫程式碼。

▶  使用 wb.export 匯出 aardio 函式到 Javascript

前面我們介紹過使用 external 匯出 aardio 函式到網頁 JavaScript 。我們還可以用 wb.export 匯出 aardio 函式,先看例子:

import web.view;
var wb = web.view(mainForm.custom);

wb.export({
    alert = function(msg){
        winform.msgbox(msg) 
    };
    nativeAdd = function(a,b){ 
        return a + b; 
    }
})

注意:

1、wb.export() 匯出的是 JavaScript 全域性函式。

2、wb.export() 匯出的函式在 JavaScript 中同樣是非同步 Promise 物件。

3、wb.export() 匯出的 Javascript 全域性函式, 使用 JSON 自動轉換呼叫引數和返回值,可以更好的相容只能支援純 aardio 物件 / 純 JavaScript 物件的程式碼。

4、wb.export() 匯出的函式內部禁止呼叫 wb.doScript 或 wb.eval 執行Javascript 。

wb.external 內部是呼叫 wb.exportHostObject() 匯出 aardio 物件,中間不需要經過 JSON 自動轉換。

示例:網頁 JavaScript 呼叫本地 Ping 命令

我經常被問到幾個類似的問題:

1、JavaScript 的非同步函式太麻煩了,怎樣把他搞成同步的,不用 await ,不用 async 。

2、JavaScript 的非同步函式太好用了,怎樣在 aardio 中也這樣搞,如何在 aardio 裡 await 。

其實同步有同步的優勢,非同步有非同步的好處,揚長避短是智慧,倒行逆施最累人。下面我們一起來寫一個在 WebView2 中呼叫本地 Ping 命令的小程式體驗一下。

第一步:建立視窗。

import win.ui;
var winform = win.form(text="Ping")

第二步:基於視窗建立 WebView2 瀏覽器元件。

import web.view;
var wb = web.view(winform);

第三步:使用 external 物件匯出 JavaScript 可以呼叫的本地函式。

import process.popen;
wb.external = {
  ping = function(domain){
    
    var prcs = process.popen("ping "+ domain);
    for( all,out,err in prcs.each() ){
        wb.invoke("document.body.insertAdjacentText",'beforeend',all); 
    }
    
    return "恭喜,事做好了!"
  } 
}

在 JavaScript 裡用 aardio.ping() 就可以直接呼叫上面的 external.ping() 函數了。

第四步:下面在網頁裡寫 JavaScript 來呼叫 aardio 函式。

wb.html = /**
<body style="white-space: pre;"><script>
doSomething = async() => {
    var result = await aardio.ping('www.baidu.com');
    document.body.insertAdjacentText('beforeend',result);
};
</script>
<button  onclick="doSomething()">開始幹活了</ping>
**/

就這麼短短几句,一個簡單的程式就完成了,請看執行效果:

上面程式的完整 aardio 原始碼如下:

//建立視窗
import win.ui;
var winform = win.form(text="Ping")

//嵌入瀏覽器元件
import web.view;
var wb = web.view(winform);

//匯出 aardio 函式到 JavaScript
wb.external = {
    ping = function(domain){
        
        //同步有同步的優勢,揚長避短是智慧,倒行逆施最累人。 
        var prcs = process.popen("ping "+ domain);
        for( all,out,err in prcs.each() ){
            wb.invoke("document.body.insertAdjacentText",'beforeend',all); 
        }
        
        return "恭喜,事做好了!"
    } 
}
import process.popen;

//寫入網頁 HTML
wb.html = /**
<body style="white-space: pre;"><script>
doSomething = async() => {
    
    //非同步有非同步的好處,揚長避短是智慧,倒行逆施最累人。
      var result = await aardio.ping('www.baidu.com');
      document.body.insertAdjacentText('beforeend',result);
};
</script>
<button  onclick="doSomething()">開始幹活了</ping>
**/

//顯示視窗
winform.show();

//啟動介面訊息迴圈
win.loopMessage();

▶  aardio 呼叫 JS 函式

在 aardio 中可以使用 wb.doScript() , wb.eval() , wb.xcall() 等函式呼叫網頁 JavaScript ,下面看一個在 aardio 中呼叫 xterm.js 的簡單例子:

import win.ui;
var winform = win.form(text="xterm")

import web.view;
var wb = web.view(winform);

wb.html = /**
<!DOCTYPE html> 
<head>
  <meta charset="UTF-8">
  <title></title>
  <link rel="stylesheet" href="http://unpkg.com/[email protected]/css/xterm.css">
  <script src="http://unpkg.com/[email protected]/lib/xterm.js"></script>
</head>
<body style="height:100vh;"> 
  <script>
    let term  = new Terminal();
    term.open(document.body);
    term.write('\x1b[31m紅色字型\x1b[37m測試')
  </script>
</body>
</html>
**/

wb.xcall("term.write",'\e[32m綠色字型');

winform.show();
win.loopMessage();

▶  無邊框視窗:用網頁實現視窗標題欄

「無邊框視窗」指的是去掉獨立窗體預設的邊框與標題欄,然後由程式自行定製邊框與標題欄。

aardio 做這事還是很容易的,首頁在窗體屬性中指定「邊框」屬性為 none。

這樣直接執行後顯示的窗體就沒有邊框和標題欄了( 按 Alt + F4 關閉視窗 )。

然後新增下面的程式碼就可以為窗體新增標題欄、標題欄按鈕、陰影邊框、並支援拖動邊框縮放:

import win.ui.simpleWindow;
win.ui.simpleWindow(winform);

win.ui.simpleWindow 的原始碼很簡單,參考其原始碼也可以自己編寫新的庫定製邊框與標題欄。

這裡我們不用上面的方法,而是用網頁實現標題欄。

我們知道網頁繪製一個標題欄與標題欄按鈕很簡單,難點在於怎麼在網頁裡控制視窗。我們先學習幾個專用於無邊框視窗的 aardio 函式:

winform.hitMax() //模擬點選最大化按鈕
winform.hitMin() //模擬點選最小化按鈕
winform.hitClose() //模擬點選關閉按鈕
winform.hitCaption() //拖動標題欄

下面寫個簡單的例子,先看下執行效果:

WebView2 無邊框視窗示例完整原始碼如下:

import win.ui;
/*DSG{{*/
var winform = win.form(text="無邊框視窗";right=759;bottom=469;bgcolor=16777215;border="none")
winform.add()
/*}}*/

import web.view;
var wb = web.view(winform);
 
//匯出為 Javascript 中的 aardio 物件
wb.external = { 
    close = function(){
        winform.close();
    };
    hitCaption = function(){
        winform.hitCaption();
    };
    hitMin = function(){
        winform.hitMin();
    };
    hitMax = function(){
        return winform.hitMax();
    };
}

wb.html = /**
<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <style type="text/css">
    html {
        margin: 0px;
        padding: 0px; 
        background-color: #202020; 
    }
    
    #title-bar {
        height: 32px;    
        padding: 0px;
        margin: 0px;
    }
    
    #title-bar .caption {
        position: fixed;
        top: 0px;
        left: 0px;    
        width: 100%;
        padding-left: 10px;
        color: #ADADAD;
        line-height: 32px;
        font-size: 14px;
        cursor: default;
        user-select:none;
    }
    
    #title-bar .buttons {
        position: fixed;
        top: 1px;
        right: 1px;    
    }
    
    #title-bar button {
        font: 14px Marlett ;
        color: #F5F5F5;
        background-color: transparent;
        border: none;
        height: 28px;
        width: 28px;  
    }
     
    #title-bar button:hover {
        background-color: #FF4500;
    }
    
    #title-bar button:active {
        background-color: #B0451E;
        color: #C5C5C5;
    }
    
    #main {
        padding: 12px;    
        color: #C0C0C0;
    }
     
    </style>
    <script type="text/javascript">  
    
    </script>
</head>
  <body>
    <div id="title-bar" >
      <div class="caption" onmousedown="aardio.hitCaption()">按住這裡呼叫 aardio.hitCaption() 拖動視窗 </div>
      <div class="buttons">
        <button id="min-btn" onclick="aardio.hitMin()">0</button>
        <button id="max-btn"  onclick="aardio.hitMax()">1</button>
        <button id="close-btn" onclick="aardio.close()">r</button>
      </div>
    </div>
    <div id="main">
        1、請指定窗體「邊框」屬性為 none ,建立無邊框視窗。<br />
        2、呼叫 win.ui.shadow(winform) 建立陰影邊框<br />
    </div>
    <script src="default.js"></script>
  </body>
</html>
**/ 

//新增陰影邊框
import win.ui.shadow;
win.ui.shadow(winform);

//設定視窗縮放範圍
import win.ui.minmax;
win.ui.minmax(winform);

//切換最大化、還原按鈕
winform.adjust = function( cx,cy,wParam ) {
    if( wParam == 0x2/*_SIZE_MAXIMIZED*/ ){ 
        wb.doScript(`document.getElementById("max-btn").innerText="2";`)
    }
    elseif( wParam == 0x0/*_SIZE_RESTORED*/ ){
        wb.doScript(`document.getElementById("max-btn").innerText="1";`)
    } 
};
            
winform.show();
win.loopMessage();

以上原始碼來自 aardio 自帶範例 > Web 介面 > web.view :

▶  WebView2 + 前端工程

如果熟悉網頁前端開發,也可以點選 「 新建工程 > Web 介面 > WebView2 」建立工程。

執行建立的範例工程會顯示幫助:

這些熟悉前端的一看就懂,就不多說了。

注意 WebView2 預設工程的「網頁原始碼」這個目錄的「內嵌資源」屬性為 false —— 也就是說釋出後的 EXE 檔案不會包含這個目錄。

而工程中的「網頁」目錄「內嵌資源」屬性為 true —— 也就是說釋出後的 EXE 檔案會包含這個目錄。

「網頁」目錄「本地構建」屬性為 true —— 這指的是該目錄下的檔案會無條件新增到釋出 EXE 檔案中(不必新增到工程 )。

▶  其他瀏覽器元件

aardio 中的瀏覽器元件非常多,用法與 web.view 基本都類似。aardio 甚至可以呼叫作業系統已安裝的 Chrome,Edge 等瀏覽器寫軟體介面。

請參考「 aardio 自帶範例 > Web 介面」: