前言

Web Assembly 是由 Mozilla、Google、Microsoft、Apple 共同提出對瀏覽器支援更低階層次的編譯執行,是可以透過各種程式語言編譯成 wasm(web 組合語言),然後在瀏覽器執行,號稱可以比 Javascript 執行速度來得快,被軟體開發的各界熱烈關注。

本篇將介紹 Web Assembly 是如何運作,以及一個可以從 Java 轉譯為 Web Assembly 的編譯器: TeaVM,然後再介紹專門給 TeaVM 的 Flavour Framework (就像 Angular.js, React.js, Vue.js 那種程式語言框架),製作出一個 Todo Application。

Web Assembly 執行原理

Web Assembly 其實就是讓不同語言編譯的二進制程式可以執行在瀏覽器上,但是目前(2018年),瀏覽器還無法直接執行 Web Assembly,必須要透過 Web Assembly API 代為執行,詳情可以參考 Mozilla(https://developer.mozilla.org/en-US/docs/WebAssembly/Concepts)。

官方對於 Web Assembly 的執行過程作介紹,把 C/C++ 程式放到瀏覽器上執行的流程是:

.wasm 其實是一個標準的 Web Assembly 格式,你甚至可以用這個格式的內容透過 API 直接在瀏覽器執行,詳情請參考 wat2wasm 或是 官方文件

來看看 C# 的一個 Web Assembly 框架 Blazor 是如何運作 Web Assembly 的:

如果瀏覽器發生一些事件來觸發程式,最後再改變 DOM,Blazor 的流程是:

程式語言將要操作 DOM 的程式碼包成 Render Tree 送出去 Javascript 來改變 DOM。

以上用這樣的流程說明,可以幫助更好的理解 Web Assembly 如何執行。

TeaVM

本篇介紹 TeaVM 進行 Web Assembly 開發,他可以編譯 Java, Kotlin, Scala,其寫程式的思維也必須稍微進行轉換。

TeaVM 的工作就是把 Java Bytecode(JVM) 轉出來到 Javascript, Web Assembly 使用,而且 TeaVM 團隊還開發了一個叫 Flavour 的 Framework,使用上很像 Angular.js,稍後就會做介紹。

TeaVM 是用 Maven 來產生一個專案,然後匯入專案到 IDE 中進行開發,然後在執行程式時, TeaVM 會透過轉換器把 Java bytecode 進行轉換,然後在瀏覽器中看到結果。

執行 Hello World

請先安裝 Maven, Net Beans(EE+Glassfish) 到開發環境,然後使用指令:

mvn -DarchetypeCatalog=local -DarchetypeGroupId=org.teavm -DarchetypeArtifactId=teavm-maven-webapp -DarchetypeVersion=0.5.1 archetype:generate  

如果執行後問你參數要填寫什麼,照著上面參數填。 執行參數後,就會產生一個專案目錄,請注意,專案目錄的名稱及 DarchetypeArtifactId 名稱就代表了這個 application 的路徑,所以如果名稱是 test,網址路徑就是: http://localhost:8080/test。

接著,用 Net Beans 開啟舊專案,然後可以看到在 org.teavm 這個 Package 可以看到 Client.java 這個主程式,下面是我改過的範例:

package org.teavm;

import org.teavm.jso.dom.events.Event;  
import org.teavm.jso.dom.events.EventListener;  
import org.teavm.jso.JSObject;  
import org.teavm.jso.browser.Window;  
import org.teavm.jso.dom.html.HTMLDocument;  
import org.teavm.jso.dom.html.HTMLElement;  
import org.teavm.jso.dom.xml.Element;

public class Client {


    public static void main(String[] args) {

        HTMLDocument document = HTMLDocument.current();
        HTMLElement div = document.createElement("h1");
        div.appendChild(document.createTextNode("Hello World by TeaVM1"));
        document.getBody().appendChild(div);

        final Window window = Window.current();
        //找到 window 這個頂層 DOM 物件

        Element element = window.getDocument()
            .getElementsByTagName("h1")
            .get(0); //選擇同類多個元素,其中第一筆。

        //下面: 針對某個元素新增事件監聽器
        HTMLElement ew = (HTMLElement)element;
        ew.addEventListener("click", new EventListener() {
            @Override
            public void handleEvent(Event evt) {
                window.alert((JSObject) evt);
            }
        });

        //會顯示在 F12 Console 裡面
        System.out.println("Only print this message in web console.");

    }

}

*接下來如果更動到 .html 等資源檔案,都必須要 Clean & Build,最後才執行。

Flavour Framework

安裝 Flavour Framework

輸入指令:

mvn archetype:generate -DarchetypeGroupId=org.teavm.flavour -DarchetypeArtifactId=teavm-flavour-application -DarchetypeVersion=0.1.0  

網址的路徑會根據 Maven 的專案目錄名稱一致。

關於 Flavour Framework 的 Template 和雙向溝通

Flavour 或純 TeaVM 在開始運作的時候,會以 Web Pages 的目錄內容為主,並且會從一個 Java 程式進入點(main)開始執行 Java WASM 程式。

首先,從 index.html 中設定一個元素的 id 名稱成為 Java WASM 綁定的點,然後跑起程式(和 React.js 很像),一開始的範例就包含了其他模板的注入,在一開始我們就有 client.html 這個檔案:

client.html 我修改成這樣的範例:

<div>  
  <label>Please, enter your name</label>:
  <input class="username"
         type="text" 
         html:value="userName"  
         event:keyup="changeUserName()"
    />
</div>

<div>  
  Hello, <i><html:text value="userName"/></i>
</div>  

上述宣告為 html:value 的屬性,裡面的值 userName 將會參考到 Java 程式中的變數,稍後會出現;event:keyup 則是在鍵盤點擊後去觸發 Java 中的 Method。

在這些標籤中, html:value, event:keyup 都是 Flavour Template 的特殊用法,所有的使用方式,可以參考官方文件 Standard Components ,不過我覺得文件寫得很不齊全,更多 template 使用的功能可以參考 Dependencies -> teavm-flavour-template-0.1.0.jar 中的定義。

要對 inde.html 和 Java 程式做雙向溝通,請看到目錄結構 org.teavm.flavour 的 Client.java 其中我修改的範例是:

package org.teavm.flavour;

import org.teavm.jso.dom.xml.Element;  
import org.teavm.flavour.templates.BindTemplate;  
import org.teavm.flavour.templates.ModifierTarget;  
import org.teavm.flavour.widgets.ApplicationTemplate;  
import org.teavm.jso.JSBody;  
import org.teavm.jso.browser.Window;  
import org.teavm.jso.dom.html.HTMLCollection;  
import org.teavm.jso.dom.html.HTMLDocument;  
import org.teavm.jso.dom.html.HTMLElement;  
import org.teavm.jso.dom.html.HTMLInputElement;  
import org.teavm.jso.dom.xml.NodeList;

//定義 Client 這個 Class 使用到的模板,需定義,client.html 才能使用到這些變數或方法
@BindTemplate("templates/client.html")
public class Client extends ApplicationTemplate{  
    final Window window = Window.current();

    //欲操作變數
    private String userName = "";

    public static void main(String[] args) {
        Client client = new Client();
        client.bind("application-content");
    }

    //動詞+欲操作變數(第一個字小寫)
    // get 會自動被 template 使用
    public String getUserName() {
        return userName;
    }

    //動詞+欲操作變數(第一個字小寫)
    //set 會自動被 template 使用
    public void setUserName(String userName) {
        this.userName = userName;
    }

    //在 Java 中調用 Javascript 語法
    @JSBody(params = { "message" }, script = "console.log(message)")
    public static native void log(String message);

    //在 Java 中調用 Javascript 語法並且回傳 DOM 元素樹給我們操作
    //在 TeaVM 我找不到 getElementsByClassName 這種用法,可以這麼做:
    @JSBody(params = { "el" }, script = "return document.getElementsByClassName(el);")
    public static native HTMLElement[] getThatClass(String el);

    //在 template 中我定義了 event:keyup 會找到這個變數
    public void changeUserName() {
        System.out.println("Change...");

        log("test");

        //把 getThatClass 的內容做轉型
        HTMLInputElement e = (HTMLInputElement)getThatClass("username")[0];
        System.out.println(e.getValue());

        //比 setUserName 更及時的更改
        userName = e.getValue();
    }
}

程式中, psvm縮寫(String args[]) 就是 Java WASM 程式進入點,一開始就實作了自身的 Class -> Client,並且和 index.html 的 id: application-content 做綁定。

宣告一個欲操作的變數 userName,如果在 index.html 中使用到 template 的功能,就會自動對這個變數操作。

另外可以看到如何在 Java 中調用 Javascript 並且可以做雙向值傳遞溝通,詳情或更多做法可以參考官方文件: Interacting with JavaScript

程式進入點

在 index.html 中的 <body onload="main()"> 是整個 wasm 程式被載入的進入點,然後, Java 中的 public static void main(String args[])必須要在 org.teavm.flavour 底下,且名稱要是 Client.java ,Class 可不需要實作任何繼承或介面,如果把上一個範例改名叫做 User.java ,則 Client.java 程式就會變成:

package org.teavm.flavour;

import org.teavm.flavour.templates.Templates;  
public class Client {  
    public static void main(String[] args) {
        Templates.bind(new User(), "application-content");
    }
}

Java Router 模式及骨架

Flavour 可以提供實現 Router 的模式,定義是建立 Interface,建立一個名為 RouterDefinition.java 的 Interface:

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package org.teavm.flavour;

import org.teavm.flavour.routing.Path;  
import org.teavm.flavour.routing.PathParameter;  
import org.teavm.flavour.routing.PathSet;  
import org.teavm.flavour.routing.Route;

/**
 *
 * @author lab-student
 */
@PathSet
public interface RouterDefinition extends Route{  
    @Path("/")
    void index();


    @Path("/hello/{name}")
    void sayHello(@PathParameter("name") String username);
}

接著,在 Client.java 引用這個 Router,並且把 Client 自身當作是整個 Rotuer 骨架:

package org.teavm.flavour;

import org.teavm.flavour.routing.Route;  
import org.teavm.flavour.templates.BindTemplate;  
import org.teavm.flavour.templates.Templates;  
import org.teavm.flavour.widgets.ApplicationTemplate;  
import org.teavm.flavour.widgets.RouteBinder;

/**
 *
 * @author lab-student
 */
@BindTemplate("templates/basepage.html")
public class Client extends ApplicationTemplate implements RouterDefinition{  
    public static void main(String[] args) {

        //自己就是路由器的起始點
        Client client = new Client();
        new RouteBinder()
                .withDefault(RouterDefinition.class, r -> r.index())
                .add(client)
                .update();

        client.bind("application-content");
    }

    //index 的路由器程式
    @Override
    public void index() {
        //設定程式(含 Template)
        setView(new IndexView());
    }

    //包含 Parameter 的路由器程式
    @Override
    public void sayHello(String username) {
        setView(new SayHello(username));
    }
}

其中需要建立 IndexView.java, templates/IndexView.html, SayHello.java, templates/SayHello.html 檔案。

IndexView.java:

package org.teavm.flavour;

import org.teavm.flavour.templates.BindTemplate;  
import org.teavm.flavour.widgets.ApplicationTemplate;

@BindTemplate("templates/IndexView.html")
public class IndexView extends ApplicationTemplate{  
    private String name;

    public String getName(){
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

IndexView.html:

<div>What's your name?</div>

<div>  
  <input type="text" html:change="name"/>
  <button type="button">GO</button>
</div>

<div>  
  Hello, <i><html:text value="name"/></i>
</div>  

SayHello.java:

package org.teavm.flavour;

import org.teavm.flavour.templates.BindTemplate;  
import org.teavm.flavour.widgets.ApplicationTemplate;

@BindTemplate("templates/SayHello.html")
public class SayHello extends ApplicationTemplate{

    private String username = "";

    //URL 的 Parameter 參數是透過 Constructor 的參數來傳遞的
    public SayHello(String username){
        this.username = username;
    }

    public String getUsername(){
        return this.username;
    }

}

templates/SayHello.java:

<div>  
    Hello, <html:text value="username" />
</div>  

執行後,可以看到 /#/ 這個首頁路由就會是 IndexView 的程式,另外在 /#/hello/xxxxx 就會出現:

Hello, xxxxx  

Servlet 後端程式和 TeaVM 並存

如果你的 TeaVM 專案是跑在 GlassFish/Tomcat 上,只要在 Dependencies 加上: javax.servlet:javax.servlet-api:3.1.0 就可以寫 Servlet 程式,兩者之間可以互相溝通。

Todo Apps

樣式模板使用 w3schools - todolist 範例 ,程式碼於 Gist: https://gist.github.com/hpcslag/4f2a685032f96e665f04410e74cea554

總結

我曾是熟悉使用 Javascript/Typescript 並搭配 React.js/Angular.js/Vue.js 等框架來寫前端 Apps 的開發者,在使用 Web Assembly 時,不免其實會用到很多以前開發 JS 的經驗在操作 DOM 以及寫很多前端邏輯,我認為對於新手來說,寫 Web Assembly 難處在於如何操作 DOM 及事件、值之上。

本文章使用的 Web Assembly 是 TeaVM,因為 Java 寫起來非常嚴謹,所以架構的部分還是需要很多 Java 的基礎知識才能夠寫起來,加上 TeaVM 的文件寫的零散又很亂,很不容易讀懂整個使用的方式,像是型別轉換、可以使用的 API 等...etc,甚至還要自己到 .jar 裡面翻看看官方是怎麼寫的,反而在使用上很不方便,希望未來可以改善。

用 JS/TS Client Side Rendering 或 Web Assembly 來撰寫前端程式,它們都是透過瀏覽器進行渲染,但是 Web Assembly 反而提供了更底層的硬體資源來加速程式運作,有如範例:

參考自: o-alexandrov-GitHub issue

事實上,挑選工具寫 Web Assembly 程式時,需要特別注意 Browser Console 的錯誤訊息是否是混淆代碼,挑選工具時必須觀察套件中是否有良好的除錯模式及架構的風格,舉例 Flavour 框架來看,在除錯方面 TeaVM 官方有提供 Chrome 專有的除錯外掛: TeaVM debug agent,整體而言,只要用到瀏覽器和 WASM 互相端傳值,就一定需要瀏覽器和開發端兩邊除錯,有點小麻煩。

在 Browser Console 模式下,其實出錯時,不要跳過那些混淆代碼,仔細看那就是可以看的 .java 檔案:

最後請注意 Java 跑這些程式,每次更新一定要 Clean&Build 一次,在瀏覽器也盡量關閉快取。

Reference

https://developer.mozilla.org/en-US/docs/WebAssembly/Concepts
https://www.c-sharpcorner.com/article/blazor-running-c-sharp-on-browser-using-web-assembly/
https://github.com/vuejs/vue/issues/8193
http://webassembly.org.cn/getting-started/developers-guide/
https://github.com/WebAssembly-cn/weekly/wiki
https://github.com/konsoletyper/teavm-flavour
https://github.com/konsoletyper/teavm/blob/fd7ff3d538d4bf2da2cf0b8969991239b5e68f14/s.../Server.java