Eclipse插件开发六:使用Web前端技术开发AI助手页面
之前的过程中,我们都不怎么熟悉Eclipse的哪些API,样式也没发怎么去修改,现在我们要修改为用html的方式来编写.
准备一个AI助手聊天页面的html.css,js代码
效果如下所示。
1.快速demo
1.1.准备前端代码
确保准备的前端代码可以在浏览器正常运行,这里我们直接浏览器访问html的绝对路径
代码目录如下所示
1.1.1.chat.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI助手</title>
<link rel="stylesheet" type="text/css" href="F:\work\com.hutao.search\src\resources\chat.css">
</head>
<body>
<div class="chat-container">
<div class="messages" id="message-container"></div>
<div class="input-container">
<input type="text" class="user-input" id="user-input" placeholder="输入消息...">
<button class="send-button" onclick="handleUserInput()">发送</button>
</div>
</div>
<script src="F:\work\com.hutao.search\src\resources\chat.js"></script>
</body>
</html>
1.1.2.chat.js
const messageContainer = document.getElementById('message-container');
const userInput = document.getElementById('user-input');
function handleUserInput() {
const userMessage = userInput.value.trim();
if (userMessage === '') {
return;
}
appendMessage(userMessage, 'user-message');
// Simulate AI Assistant reply
const systemReply = 'AI助手: 你好呀,勇士!';
appendMessage(systemReply, 'assistant-message');
userInput.value = '';
messageContainer.scrollTop = messageContainer.scrollHeight;
}
function appendMessage(message, messageType) {
const messageElement = document.createElement('div');
messageElement.classList.add('message', messageType);
messageElement.textContent = message;
messageContainer.appendChild(messageElement);
}
1.1.3.chat.css
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f2f2f2;
}
.chat-container {
max-width: 600px;
margin: 20px auto;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.messages {
min-height: 300px;
max-height: 400px;
overflow-y: scroll;
padding: 10px;
}
.message {
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
}
.user-message {
background-color: #DCF8C6;
align-self: flex-end;
}
.assistant-message {
background-color: #E4E4E4;
}
.input-container {
display: flex;
align-items: center;
padding: 10px;
background-color: #f9f9f9;
}
.user-input {
flex: 1;
height: 40px;
margin-right: 10px;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
}
.send-button {
padding: 5px 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
2.改造后端代码ViewPart
将之前的ViewPart 进行改造,这里不在使用java来开发界面。我们用前端的html,css,js.
实现思路,就是将在嵌入浏览器组件 (Browser) 来加载 HTML 和 JavaScript 页面,从而实现你的需求。这种方式可以让你使用熟悉的前端 技术来构建界面。
package com.hutao.search.view;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.part.ViewPart;
public class AiView extends ViewPart {
public static final String ID = "com.hutao.search.ai.plugin.aiview";
@Override
public void createPartControl(Composite parent) {
parent.setLayout(new GridLayout(1, false));
// 创建 Browser 控件
Browser browser = new Browser(parent, SWT.NONE);
browser.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
// 加载 HTML 文件内容
String htmlContent = loadHtmlFile("resources/chat.html");
System.out.println(htmlContent);
// 在 Browser 控件中显示 HTML 内容
browser.setText(htmlContent);
}
/**
* @description:加载html页面
* @author:hutao
* @mail:hutao1@epri.sgcc.com.cn
* @date:2025年2月18日16:01:01
*/
private String loadHtmlFile(String resourcePath) {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath);
if (inputStream == null) {
return "<html><body><h1>无法加载资源</h1></body></html>";
}
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
} catch (IOException e) {
e.printStackTrace();
return "<html><body><h1>无法加载资源</h1></body></html>";
}
return content.toString();
}
@Override
public void setFocus() {
}
}
3.Demo效果展示
左边为我们在正常浏览器里面运行的效果,而右边实在eclipse中运行的效果。不然看出,样式好像丢了一些。也就是将前端的代码,迁移到html中的时候,好像不是完全兼容的。
4.CSS样式丢失的问题
根据上图的对比,不难发现,背景色都丢了,我们通过浏览器查看到这个元素,复制一下这些样式
我们把这部分代码复制出来看看,然后放到我们的html代码中,
<div class="chat-container">
<div class="messages" id="message-container">
<div class="message user-message">xxx</div>
<div class="message assistant-message">AI助手: 你好呀,勇士!</div>
<div class="message user-message">xxx</div>
<div class="message assistant-message">AI助手: 你好呀,勇士!</div>
<div class="message user-message">xxx</div>
<div class="message assistant-message">AI助手: 你好呀,勇士!</div>
<div class="message user-message">xxx</div>
<div class="message assistant-message">AI助手: 你好呀,勇士!</div>
</div>
<div class="input-container">
<input type="text" class="user-input" id="user-input" placeholder="输入消息...">
<button class="send-button" onclick="handleUserInput()">发送</button>
</div>
</div>
如下图所示,通过代码直接写的,样式没问题,通过handleUserInput-》appendMessage-》追加的消息,丢失了背景色样式,
而在浏览器中,我们可以看到,写死的,和通过handleUserInput-》appendMessage-》都是正常的,观察代码,也发先,代码都是一致的,目前我们则怀疑,可能在eclipse的Browser通过我们的handleUserInput-》appendMessage-》追加的消息,可能不能是我们正常显示时候的代码了。
于是我尝试这样解决问题,那我直接用style,不用css来渲染,看看如何?结果样式正常了。
function appendMessage(message, messageType) {
const messageElement = document.createElement('div');
const messageElement = document.createElement('div');
// 设置背景色和字体颜色,直接通过 style 属性应用
if (messageType === 'user-message') {
messageElement.style.backgroundColor = '#DCF8C6'; // 用户消息背景色
messageElement.style.alignSelf = 'flex-end'; // 用户消息右对齐
} else if (messageType === 'assistant-message') {
messageElement.style.backgroundColor = '#E4E4E4'; // AI 助手背景色
}
// 直接应用原本的 CSS 样式
messageElement.style.padding = '10px'; // 设置内边距
messageElement.style.marginBottom = '10px'; // 设置下边距
messageElement.style.borderRadius = '5px'; // 设置圆角
messageElement.style.fontFamily = 'Arial, sans-serif'; // 设置字体
messageElement.style.fontSize = '14px'; // 设置字体大小
messageElement.style.lineHeight = '1.5'; // 设置行高
messageElement.style.maxWidth = '100%'; // 限制宽度(可选)
messageElement.style.display = 'block'; // 确保它是块级元素
messageElement.textContent = message;
messageContainer.appendChild(messageElement);
}
现在基本可以确定,问题出在handleUserInput-》appendMessage-》css渲染出现问题了,我尝试将下面的classList.add(‘message’, messageType)拆成两行,classList.add(‘message’),classList.add(messageType),结果样式可以正常了。
function appendMessage(message, messageType) {
const messageElement = document.createElement('div');
//messageElement.classList.add('message', messageType);
messageElement.classList.add('message');
messageElement.classList.add(messageType);
messageElement.textContent = message;
messageContainer.appendChild(messageElement);
}
现在就有一个问题,怎么去看生成的这些元素样式,毕竟不是在浏览器里面,浏览器我们可以F12查看。
这里这特了一早上,我最后研究了下日志打印来看看为啥上面CSS样式失效。
5.日志打印问题
接着上面的问题,以后出现这样的问题该怎么定位处理?毕竟这个不想浏览器一样,我们可以按F12,可以debug
通过上面我们一系列测试,我们最终锁定:通过handleUserInput-》appendMessage-》追加的消息,丢失了背景色样式,那么如果我们能通过一些有效的方法来看看,我们最终的页面html元素是否为下面这样。
<div class="messages" id="message-container">
<div class="message user-message">aa</div>
<div class="message assistant-message">AI助手: 你好呀,勇士!</div>
</div>
5.1.alert弹框打印信息
alert是原始的js弹框,不用引入任何框架插件即可使用,后面为啥会说,为啥用原始js打印(还有插件引入的问题还没说,这里我引入css,js是使用绝对路径地址)
在合适的位置,使用alert,例如,这里我在调用appendMessage之后,去获取message-container元素,然后弹框
const messageContainer = document.getElementById('message-container');
alert(messageContainer.innerHTML);
此时我们就能发现问题了,我们通过appendMessage生成的最后html和我们预期的不一样,
实际生成的没样式
<div class="message">abc</div>
<div class="message">AI助手: 你好呀,勇士!</div>
预期的生成的有样式
<div class="message user-message">aa</div>
<div class="message assistant-message">AI助手: 你好呀,勇士!</div>
然而,当我们把appendMessage中的classList.add(),拆成两次添加以后,就正常了。
//messageElement.classList.add('message', messageType);
messageElement.classList.add('message');
messageElement.classList.add(messageType);
楼主查阅了很多资料,觉得下面这个理由比较靠谱。
浏览器对标准的 JavaScript API 支持较好,classList.add 方法在现代浏览器中已经是一个标准且稳定的 API。然而,Eclipse 插件的运行环境可能和浏览器有所不同,可能存在对某些 JavaScript 特性支持不完全或者存在兼容性问题。
所以在调用一些前端的API,如果达不到我们的期望的时候,不妨用打印的办法看看。
5.2.使用console.log进行日志输出
如下图所示,只是使用console.log就没那么简单了,你会发现无论怎么打印都不会输出到控制台,毕竟以前我们打印输出,是输出到F12,但是这里不好意思,这里是IDE,不是浏览器,没有F12
下面来说,怎么让console.log进行日志输出
BrowserFunction 是 Eclipse SWT(Standard Widget Toolkit)库中的一个类,用于在 Java 代码和嵌入在 SWT Browser 组件中的 JavaScript 代码之间建立桥梁,允许 JavaScript 代码调用 Java 方法。
public class CustomFunction extends BrowserFunction {
public CustomFunction(Browser browser, String name) {
super(browser, name);
}
@Override
public Object function(Object[] arguments) {
for (Object arg : arguments) {
if (arg != null) {
// 打印到Java控制台
System.out.println(arg.toString());
}
}
return null;
}
}
接着在我们的ViewPart中中的创建part的方法中,创建CustomFunction。
public class AiView extends ViewPart {
public static final String ID = "com.hutao.search.ai.plugin.aiview";
@Override
public void createPartControl(Composite parent) {
parent.setLayout(new GridLayout(1, false));
// 创建 Browser 控件
Browser browser = new Browser(parent, SWT.NONE);
browser.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
// 加载 HTML 文件内容
String htmlContent = loadHtmlFile("resources/chat.html");
System.out.println(htmlContent);
// 在 Browser 控件中显示 HTML 内容
//browser.setText(htmlContent);
browser.setUrl("F:\\work\\com.hutao.search\\src\\resources\\chat.html");
new CustomFunction(browser, "logToJava");
}
}
然后在我们的js文件(需要调用console.log 之前,重写console.log),最好js的第一行代码
console.log = function() {
const messages = Array.prototype.slice.call(arguments);
// 将所有参数连接成一个字符串并调用Java方法
logToJava(messages.join(' '));
};
6.CSS,JS资源引用问题
上面的代码,我用的是绝对路径地址,但是这样明显不对的,这样就没办法迁移了,因此要换成相对地址
<script src="F:\work\com.hutao.search\src\resources\chat.js"></script>
<link rel="stylesheet" type="text/css" href="F:\work\com.hutao.search\src\resources\chat.css">
chat.html和chat.css等都在一个文件夹,因此我们用相对路径
<script src="chat.js"></script>
<link rel="stylesheet" type="text/css" href="chat.css">
修改完以后再看我们的浏览器的,没有受到影响。
但是在看我们的eclipse里面的。样式不仅丢了,点击发送也没任何反应。之前是CSS部分丢了,现在来看是CSS和JS,都全丢了
6.1.问题分析
在 Eclipse SWT 中的 Browser 控件加载 HTML 文件时,相对路径的资源(如 CSS 文件和 JS 文件)通常会出现问题。这是因为 Browser 控件使用的本地 Web 渲染引擎和我们普通的浏览器渲染不是一样的。因此,不能用浏览器去引用CSS,JS的思路去。
我们先不防获取一下这里的CSS,JS,HTML的路径看一下
URL jspath = getClass().getClassLoader().getResource("resources/chat.js");
URL csspath = getClass().getClassLoader().getResource("resources/chat.css");
URL htmlpath = getClass().getClassLoader().getResource("resources/chat.html");
System.out.println(jspath.toString());
System.out.println(csspath.toString());
System.out.println(htmlpath.toString());
如下图所示,如果你没接触到OSGI这类插件式开发的框架,对于下面这个bundleresource的地址,一定是很蒙蔽的。
格式特点:以 bundleresource:// 开头,后面紧跟一串数字(如 729.fwk1177963719),这串数字是 OSGi 框架为每个 Bundle(可理解为一个插件)分配的唯一标识符,用于区分不同的插件。再后面就是资源在 Bundle 内的相对路径,如 resources/chat.js。
用途:这种地址主要用于在 OSGi 环境中定位 Bundle 内部的资源。在 Eclipse 插件开发中,每个插件都是一个 Bundle,插件内的资源(如 HTML、CSS、JavaScript 文件等)可以通过这种地址来引用,确保资源的独立性和隔离性。
6.2.引用方案
6.2.1.1html,css,js物理不分离
即将所有的html,css,js等资源,都写到同一个html文件中,这样就不存在引用问题,代价就是会导致最后html变得很大,如果你开发的插件足够代码多,引用的资源足够多,后期维护变成几百上千行代码的时候,很难维护。这个我就不举例写了。有兴趣自己尝试。
6.2.1.1html,css,jss逻辑不分离
相比于上面的方案,物理不分离,我们在物理上将这些资源文件分离。但是在逻辑上不分离,具体措施就是,开发写代码的时候,html,css,各写个的,但是最后代码执行的时候,将css,js,等注入到html,最后,和上面物理不分离的效果是一样的,区别是,上面的代码在开发的时候就已经是同一个文件了,而这个方法,是开发完毕以后,在代码运行的时候,将代码打包合并到一个html文件中。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.ui.part.ViewPart;
public class AiView extends ViewPart {
public static final String ID = "com.hutao.search.ai.plugin.aiview";
@Override
public void createPartControl(Composite parent) {
parent.setLayout(new GridLayout(1, false));
// 创建 Browser 控件
Browser browser = new Browser(parent, SWT.NONE);
browser.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
// 加载 HTML 文件内容
String htmlContent = loadHtmlFile("resources/chat.html");
// 在 Browser 控件中显示 HTML 内容
browser.setText(htmlContent);
new CustomFunction(browser, "logToJava");
}
/**
* @description:加载html页面
* @author:hutao
* @mail:hutao1@epri.sgcc.com.cn
* @date:2025年2月18日16:01:01
*/
private String loadHtmlFile(String resourcePath) {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath);
if (inputStream == null) {
return "<html><body><h1>无法加载资源</h1></body></html>";
}
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
} catch (IOException e) {
e.printStackTrace();
return "<html><body><h1>无法加载资源</h1></body></html>";
}
// 加载 CSS 文件并内联
String cssContent = loadResourceAsString("resources/chat.css");
String jsContent = loadResourceAsString("resources/chat.js");
// 将 CSS 和 JS 插入到 HTML 内容中
return content.toString()
.replace("<link rel=\"stylesheet\" type=\"text/css\" href=\"chat.css\">","<style>" + cssContent + "</style>")
.replace("<script src=\"chat.js\"></script>","<script>" + jsContent + "</script>");
}
/**
* @description:加载资源文件,转成字符串
* @author:hutao
* @mail:hutao1@epri.sgcc.com.cn
* @date:2025年2月19日11:01:01
*/
private String loadResourceAsString(String resourcePath) {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath);
if (inputStream == null) {
return ""; // 返回空字符串以避免 null
}
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
} catch (IOException e) {
e.printStackTrace();
}
return content.toString();
}
@Override
public void setFocus() {
}
}
至此结束,相信到了这里,你就已经能用前端的开发来开发eclipse的插件了。如果你用的是VUE,也别怕,因为VUE打包最后也是生成HTML,CSS,JS这些代码,只是引用的方式,可能需要做转换