J2CL 簡介
1. 概述
J2CL允許我們用 Java 編寫 Web 應用程式並將其編譯為最佳化的 JavaScript,使其成為利用 Java 生態系統並針對現代瀏覽器的強大工具。透過將 J2CL 與 Maven 集成,我們可以簡化開發流程並有效地管理依賴關係。
在本教程中,我們將指導如何使用 Maven 設定 J2CL 專案、自訂 Web 介面以及實作簡單任務管理器應用程式的核心功能。我們將探討如何使用 Java 和 JavaScript 互通性與 RESTful 後端進行互動。
為了創建一個完整的工作範例,我們將使用restful-api.dev ,它公開了用於儲存和檢索資料的免費 REST API。
2. 使用 Maven 設定 J2CL 專案結構
J2CL 是一個 Google 項目,使用Bazel作為其預設建置系統。但是,為了簡化專案設定並使用更熟悉的工作流程,我們將使用Vertispan 開發的 Maven 插件。
2.1. j2cl-archetype-simple
最小的 J2CL 專案需要仔細配置pom.xml
和其他檔案。要開始使用現成的東西,我們可以使用j2cl-archetype-simple
Maven 原型:
mvn archetype:generate -DarchetypeGroupId=com.vertispan.j2cl.archetypes \
-DarchetypeArtifactId=j2cl-archetype-simple \
-DarchetypeVersion=0.22.0 \
-DgroupId=com.baeldung.j2cl.taskmanager \
-DartifactId=j2cl-task-manager \
-Dversion=1.0-SNAPSHOT \
-Dmodule=MyJ2CLApp
這些是生成的文件:
以下是簡要總結:
-
index.html
– 傳送到瀏覽器的頁面 -
j2cl-task-manager.css
– CSS -
MyJ2CLApp.java
–主 Java 類 -
MyJ2CLApp.native.js
– JavaScript 入口點 -
MyJ2CLAppTest.java
– 包含使用@J2clTestInput
註解的測試 -
web.xml
– Java EE(Jakarta EE)的部署描述符。 -
pom.xml
–整合 J2CL、 Elemental2 、 JSInterop和 Jetty Server 進行部署
我們將更詳細地討論這些文件。對於本教程, web.xml
和MyJ2CLAppTest.java
是不需要的,我們可以刪除它們。
2.2. Java 版本
讓我們來關註一下pom.xml
中的這兩行:
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
雖然 J2CL 本身可以處理 Java 11,但 Maven 外掛程式的目前實作強制相容 Java 8。這意味著雖然我們可以使用較新的 JDK(例如 JDK 21)運行轉編譯過程,但我們的程式碼必須嚴格遵守 Java 8 語法和特性。
此外, pom.xml
中的所有依賴項都是基於 Java 11,因此我們不想觸碰任何東西,以免破壞它們。例如,如果我們指定了 Java 的更高版本,那麼原型中包含的 Jetty 版本將不再起作用。
2.3.在瀏覽器中建置並查看結果
這種方法顯著減少了開發週轉時間,因為 Java 程式碼的變更會自動重新編譯並反映在瀏覽器中,從而實現更有效率的工作流程:
- 開啟終端機並執行:
mvn j2cl:watch
- 等待訊息:
“Build Complete: ready for browser refresh”
- 打開另一個終端機並運行:
mvn jetty:run
- 等待訊息:
“Started Jetty Server”
- 讓兩個終端機都保持開啟狀態
- 在瀏覽器中開啟
http://localhost:8080/
這樣,對 Java 程式碼的任何更改都會幾乎立即反映在 JavaScript 程式碼中。如果我們想產生最終優化的 JavaScript 文件,我們可以使用: mvn j2cl:build
這是原型產生的演示頁面。在開發過程中,我們希望保持開發人員工具開啟並且瀏覽器快取被停用:
https://www.baeldung.com/wp-content/uploads/2025/03/J2CL-Demo-archetype.mp4
有關更多信息,**我們可以查閱J2CL Maven 插件目標的文檔**。
2.4.自訂網頁
這是我們的任務管理器的index.html
。為了確保 Maven 目標繼續如預期運作, CSS 和 JavaScript 檔案的路徑必須與原型使用的路徑相同:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
[... Other optional meta tags, eg for mobile devices, Google Translate, robots, etc. ...]
<title>J2CL Task Manager</title>
<link rel="stylesheet" href="css/j2cl-task-manager.css">
</head>
<body>
<h1>Task Manager</h1>
<input type="text" id="taskInput" placeholder="Enter a task" />
<button id="addTask">Add Task</button>
<ul id="taskList"></ul>
<script src='j2cl-task-manager/j2cl-task-manager.js'></script>
</body>
</html>
這是j2cl-task-manager.css
:
body {
background-color: #cefb56;
color: black;
font-family: Calibri, Arial, sans-serif;
}
.deleteButton {
margin-left: 1rem;
}
.errorMessage {
display: block;
width: fit-content;
color: darkred;
background-color: white;
border-radius: 1rem;
border: 1px solid #f5c6cb;
padding: 0.2rem;
margin: 0.5rem 0;
}
沒有什麼需要解釋的,到目前為止,它只是非常簡單的 HTML 和 CSS 程式碼。
3.任務管理器應用程式
從高層次來看,我們的任務管理器允許使用者執行以下操作:
- 透過輸入欄位新增任務並將其儲存在記憶體中
- 透過建立新任務列表或更新現有任務列表,使用遠端 REST API 同步任務
- 如果任務清單不存在,應用程式將建立一個新的任務清單並為其指派一個唯一識別碼 (UUID),然後將其儲存在 URL 中以供將來參考
- 使用 UUID 從 API 檢索任務,允許使用者使用相同的任務清單來恢復會話
- 在本機和伺服器上刪除任務,確保UI和後端之間的資料一致性
在深入研究程式碼之前,讓我們先看一下文件。
3.1.可用類別和註解的文檔
J2CL 是 Google Web Toolkit (GWT) 的演變,因此包含了已模擬在瀏覽器環境中使用的標準 Java 程式庫的子集。受支援的類別和方法的列表可以在官方GWT 模擬參考中找到。
但這只是一個起點。我們也有 Elemental2 來與瀏覽器 DOM 互動並與外部 API 通訊。另一方面,JSInterop 透過註解為我們提供了 Java 和 JavaScript 之間的橋樑。
關於 JSInterop 和 Elemental2 的深入了解,我們可以參考GWTcon 的演講:JsInterop、Elemental2 和 2.8 以外的編碼和JsInterop 規格。
3.2. JavaScript 入口點
這是MyJ2CLApp.native.js
的內容:
setTimeout(function(){
var ep = new MyJ2CLApp();
ep.onModuleLoad()
}, 0);
onModuleLoad()
模擬 GWT 的行為,其中**onModuleLoad()
方法是應用程式的入口點**。 setTimeout(…, 0)
結構通常用於 JavaScript 中,以延遲執行,直到目前執行堆疊被清除。在這種情況下,它可以確保當 Java 類別尚未被 JavaScript 載入時不會出現任何意外。
3.3. MyJ2CLApp
類別的序列圖
透過清楚了解程式碼執行流程,我們可以更理解 Java 方法:
為了簡單起見,我們不解決同時的複雜性,即同時在不同的瀏覽器上使用相同的任務清單。
4. 實施方法
以下是MyJ2CLApp.java
中核心方法的簡要概述,包括將 Java 程式碼與 JavaScript 橋接起來的 JSInterop 註解。大多數邏輯都很簡單:我們操作 DOM、在JsArray
中追蹤任務,並呼叫DomGlobal.fetch(…)
API 與 REST API 進行互動。
不過,作為參考,**我們可以看看MyJ2CLApp.java 的完整程式碼**。
4.1. JSInterop 註釋
我們用@JsType
註解整個類,以便可以從JavaScript程式碼存取它。另一方面, @JsMethod
允許我們將 JavaScript 靜態方法JSON.stringify(…)
用作本機靜態 Java 方法:
@JsType
public class MyJ2CLApp {
// Example of using @JsMethod to wrap JSON.stringify
@JsMethod(namespace = JsPackage.GLOBAL, name = "JSON.stringify")
private static native String jsonStringify(Object obj);
// ...
}
4.2. onModuleLoad()
當應用程式啟動時,我們檢查瀏覽器URL是否包含uuid
參數。如果是的話,我們就從伺服器取得相關任務。我們也連接了Add Task
按鈕:
public void onModuleLoad() {
if (uuid != null) {
fetchTasks();
}
addTaskButton.addEventListener("click", event -> {
String taskText = taskInput.value.trim();
if (!taskText.isEmpty()) {
sendTask(taskText);
taskInput.value = "";
}
});
}
4.3. fetchTasks()
我們使用 GET 請求檢索現有的任務清單。 404
只是意味著尚未儲存任何任務:
private void fetchTasks() {
DomGlobal.fetch(API_URL + "/" + uuid)
.then(response -> {
if (!response.ok && response.status != 404) {
throw new Error("HTTP error " + response.status);
}
return response.status == 404 ? null : response.json();
})
.then(data -> {
if (data != null) {
// ...
// Populate tasks in our JsArray and update the UI
}
return null;
})
.catch_(error -> {
showErrorMessage("Could not fetch tasks. Check console.");
return null;
});
}
4.4. addTaskToUI()
此方法可協助為每個任務新增一個新的<li>
,並附有一個Delete
按鈕:
private void addTaskToUI(String taskText) {
HTMLLIElement taskItem = (HTMLLIElement) DomGlobal.document.createElement("li");
taskItem.textContent = taskText;
HTMLButtonElement deleteButton = (HTMLButtonElement) DomGlobal.document.createElement("button");
deleteButton.textContent = "Delete";
deleteButton.addEventListener("click", e -> {
// ...
// Remove the task from our JsArray, sync with server, then update the UI
});
taskItem.appendChild(deleteButton);
taskList.appendChild(taskItem);
}
4.5. sendTask()
當使用者新增任務時,我們根據uuid
是否已設定來決定是否 POST 一個全新的物件或 PUT 更新的陣列:
private void sendTask(String taskText) {
tasksArray.push(taskText);
if (uuid == null) {
createObjectOnServer()
.then(ignore -> updateTasksOnServer())
.then(ignore -> addTaskToUI(taskText))
// ...
} else {
updateTasksOnServer()
.then(ignore -> addTaskToUI(taskText))
// ...
}
}
4.6. createObjectOnServer()
和updateTasksOnServer()
createObjectOnServer()
執行 POST,讓伺服器產生一個我們儲存在uuid
中的id
:
private Promise<Object> createObjectOnServer() {
// Build JSON body and call the server
JsPropertyMap<Object> jsonBody = JsPropertyMap.of();
jsonBody.set("data", tasksArray);
// ...
return DomGlobal.fetch(API_URL, requestInit)
.then(response -> response.json())
.then(result -> {
uuid = [...]; // Extract from server response
rewriteURLwithUUID(uuid);
return null;
});
}
updateTasksOnServer()
反而執行 PUT 來更新現有記錄:
private Promise<Object> updateTasksOnServer() {
JsPropertyMap<Object> jsonBody = JsPropertyMap.of();
jsonBody.set("id", uuid);
jsonBody.set("data", tasksArray);
// ...
return DomGlobal.fetch(API_URL + "/" + uuid, requestInit)
.then(response -> {
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
return null;
});
}
4.7. showErrorMessage()
這是一個簡單的實用程序,用於顯示錯誤訊息,然後在幾秒鐘後將其刪除:
private void showErrorMessage(String message) {
HTMLDivElement errorDiv = (HTMLDivElement) DomGlobal.document.createElement("div");
errorDiv.textContent = message;
errorDiv.classList.add("errorMessage");
addTaskButton.parentNode.insertBefore(errorDiv, taskList);
DomGlobal.setTimeout((e) -> errorDiv.remove(), 5000);
}
5.瀏覽器結果頁面
第一次測試是在正常條件下進行的,即網路連線正常。我們可以將 URL 新增到書籤,以便始終參考相同的任務清單:
https://www.baeldung.com/wp-content/uploads/2025/03/J2CL-Task-Manager-App-Success.mp4
相反,在第二次測試中,我們斷開網路連線以產生錯誤:
https://www.baeldung.com/wp-content/uploads/2025/03/J2CL-Task-Manager-App-Error.mp4
一切如預期。
6. 結論
在本文中,我們探討如何使用 Maven 設定 J2CL 專案、自訂簡單網頁以及使用 Java 和 JavaScript 實作任務管理器應用程式。我們研究了連接到 RESTful 後端、儲存和檢索任務以及透過 Elemental2 和 JSInterop 使用 DOM 操作動態更新 UI 的基本步驟。
這使得我們無需離開 Java 生態系統即可建立現代 Web 應用程式。我們還了解如何使用 Maven 插件來監視變更、即時重建轉編譯的輸出以及在瀏覽器中快速測試所有內容。
與往常一樣,完整的原始程式碼可在 GitHub 上取得。