我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
文章首发于我的个人博客:xeblog.cn/articles/10…
系列文章:
前言
嘿嘿~ 如果你是第一次发现这个插件或是对这个插件感兴趣,可以去看看这篇文章了解下这个插件的经历😊:是的,我写了一个摸鱼插件!
前不久,插件新增了一个 浏览器
功能,现在可以直接在 IDEA
中浏览网页了。
这时有人就要说了:“是Chrome不好用,还是你真的有啥‘大毛病’?”
可能就是有啥毛病吧。
开始!
以防 API 不兼容,请使用 2021.2 及以上版本的 IDEA !
插件安装
添加插件库 Plugins > 设置按钮 > Manage Plugin Repositories...
http://plugins.xeblog.cn
搜索 “xechat” 安装
插件主界面
启动浏览器
使用命令 #open 1
开启浏览器
咱们来访问一下掘金社区:https://juejin.cn
右上边地址栏输入一下网站地址后回车
看看文章,学习一下“新技术”
逛逛沸点,提升一下“软技能”
使用说明
按键说明
✕
:关闭浏览器
♨
:跳到主页
←
:后退一页
→
:前进一页
⟳
:刷新当前页
调整窗口大小
S
:小
M
:中
UA设置
缩放
输入数值后回车。
负值缩小,正值放大。
浏览器实现原理
该浏览器是基于 IntelliJ SDK
内置的 JBCefBrowser
实现的,其核心是 JCEF
,参考:plugins.jetbrains.com/docs/intell…
抽象
浏览器功能接口
浏览器基本功能的定义。
package cn.xeblog.plugin.tools.browser.core;
import java.awt.*;
/**
* @author anlingyi
* @date 2022/8/15 2:08 PM
*/
public interface BrowserService {
/**
* 获取浏览器UI组件
*
* @return
*/
Component getUI();
/**
* 加载URL
*
* @param url
*/
void loadURL(String url);
/**
* 后退
*/
void goBack();
/**
* 前进
*/
void goForward();
/**
* 重新加载当前页面
*/
void reload();
/**
* 浏览器关闭
*/
void close();
/**
* 设置用户代理
*
* @param userAgent
*/
void setUserAgent(UserAgent userAgent);
/**
* 添加浏览器事件监听
*
* @param listener
*/
void addEventListener(BrowserEventListener listener);
}
浏览器事件监听接口
目前只用到了两个事件:
- 浏览器地址变更事件:在浏览器地址变更之后,用于获取变更之后的地址。
- 浏览器关闭之前事件:在浏览器关闭之前释放一些资源。
package cn.xeblog.plugin.tools.browser.core;
/**
* @author anlingyi
* @date 2022/8/15 2:24 PM
*/
public interface BrowserEventListener {
/**
* 浏览器地址变更
*
* @param url
*/
default void onAddressChange(String url) {
}
/**
* 浏览器关闭之前
*/
default void onBeforeClose() {
}
}
UA定义
通过枚举类定义目前可支持的 UserAgent
设置
package cn.xeblog.plugin.tools.browser.core;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author anlingyi
* @date 2022/8/15 5:15 PM
*/
@AllArgsConstructor
public enum UserAgent {
IPHONE("iPhone") {
@Override
public String getValue() {
return "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1";
}
},
ANDROID("Android") {
@Override
public String getValue() {
return "Mozilla/5.0 (Linux; Android 8.0.0; SM-G955U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Mobile Safari/537.36";
}
},
IPAD("iPad") {
@Override
public String getValue() {
return "Mozilla/5.0 (iPad; CPU OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/87.0.4280.77 Mobile/15E148 Safari/604.1";
}
},
WINDOWS("Windows") {
@Override
public String getValue() {
return "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36";
}
},
MACOS("MacOS") {
@Override
public String getValue() {
return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36";
}
},
NATIVE("本机") {
@Override
public String getValue() {
return null;
}
};
@Getter
private String name;
public abstract String getValue();
public static UserAgent getUserAgent(String name) {
for (UserAgent userAgent : values()) {
if (userAgent.getName().equals(name)) {
return userAgent;
}
}
return IPHONE;
}
}
实现
浏览器功能实现
基本原理:先创建一个 JBCefBrowser
对象,通过这个对象可以获取到 CefBrowser
对象,浏览器大部分功能都是通过 CefBrowser
来完成的,再通过 CefBrowser
获取 CefClient
,CefClient
可用于监听一些事件,像是浏览器地址变更事件、浏览器关闭事件、浏览器请求事件(可设置UA)等。
package cn.xeblog.plugin.tools.browser.core;
import cn.hutool.core.util.StrUtil;
import com.intellij.ui.jcef.JBCefBrowser;
import org.cef.CefClient;
import org.cef.browser.CefBrowser;
import org.cef.browser.CefFrame;
import org.cef.handler.*;
import org.cef.misc.BoolRef;
import org.cef.network.CefRequest;
import java.awt.*;
/**
* @author anlingyi
* @date 2022/8/15 2:06 PM
*/
public class JcefBrowserService implements BrowserService {
private final JBCefBrowser jbCefBrowser;
private final CefBrowser cefBrowser;
private final CefClient client;
private UserAgent userAgent;
private BrowserEventListener eventListener;
public JcefBrowserService(String url) {
this.jbCefBrowser = new JBCefBrowser(url);
this.cefBrowser = this.jbCefBrowser.getCefBrowser();
this.client = this.cefBrowser.getClient();
this.userAgent = UserAgent.IPHONE;
this.eventListener = new BrowserEventListener() {
};
initAddEvent();
}
private void initAddEvent() {
this.client.removeRequestHandler();
this.client.addRequestHandler(new CefRequestHandlerAdapter() {
@Override
public CefResourceRequestHandler getResourceRequestHandler(CefBrowser browser, CefFrame frame, CefRequest request, boolean isNavigation, boolean isDownload, String requestInitiator, BoolRef disableDefaultHandling) {
return new CefResourceRequestHandlerAdapter() {
@Override
public boolean onBeforeResourceLoad(CefBrowser browser, CefFrame frame, CefRequest request) {
String ua = userAgent.getValue();
if (ua != null) {
request.setHeaderByName("User-Agent", ua, true);
}
return false;
}
};
}
});
this.client.addDisplayHandler(new CefDisplayHandlerAdapter() {
@Override
public void onAddressChange(CefBrowser browser, CefFrame frame, String url) {
if (!StrUtil.startWith(url, "http")) {
return;
}
eventListener.onAddressChange(url);
}
});
this.client.removeLifeSpanHandler();
this.client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
@Override
public boolean onBeforePopup(CefBrowser browser, CefFrame frame, String target_url, String target_frame_name) {
if (StrUtil.endWithAnyIgnoreCase(target_url, "jpg", "png", "gif", "svg", "pdf", "bmp", "webp")) {
return false;
}
loadURL(target_url);
return true;
}
@Override
public void onBeforeClose(CefBrowser browser) {
close();
eventListener.onBeforeClose();
}
});
}
@Override
public Component getUI() {
return this.jbCefBrowser.getComponent();
}
@Override
public void loadURL(String url) {
this.jbCefBrowser.loadURL(url);
}
@Override
public void goBack() {
if (this.cefBrowser.canGoBack()) {
this.cefBrowser.goBack();
}
}
@Override
public void goForward() {
if (this.cefBrowser.canGoForward()) {
this.cefBrowser.goForward();
}
}
@Override
public void reload() {
this.cefBrowser.reload();
}
@Override
public void close() {
this.client.dispose();
this.jbCefBrowser.dispose();
}
@Override
public void setUserAgent(UserAgent userAgent) {
if (userAgent == null) {
return;
}
this.userAgent = userAgent;
}
@Override
public void addEventListener(BrowserEventListener listener) {
if (listener == null) {
return;
}
this.eventListener = listener;
}
}
浏览器UI实现
界面这一块是通过 Swing
实现的,主要是浏览器的一些基本控制按钮。
package cn.xeblog.plugin.tools.browser.ui;
import cn.hutool.core.util.StrUtil;
import cn.xeblog.plugin.enums.Command;
import cn.xeblog.plugin.tools.browser.core.BrowserEventListener;
import cn.xeblog.plugin.tools.browser.core.BrowserService;
import cn.xeblog.plugin.tools.browser.core.JcefBrowserService;
import cn.xeblog.plugin.tools.browser.core.UserAgent;
import com.intellij.openapi.ui.ComboBox;
import lombok.Getter;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
/**
* @author anlingyi
* @date 2022/8/14 11:48 AM
*/
public class BrowserUI extends JPanel {
private final static String HOME_PAGE = "https://cn.bing.com";
private BrowserService browserService;
private String lastUrl;
private WindowMode windowMode;
private UserAgent userAgent;
private Component browserUI;
private JTextField urlField;
private enum WindowMode {
SMALL("S", 200, 250),
MEDIUM("M", 400, 300);
@Getter
String name;
@Getter
int width;
@Getter
int height;
WindowMode(String name, int width, int height) {
this.name = name;
this.width = width;
this.height = height;
}
public static WindowMode getMode(String name) {
for (WindowMode mode : values()) {
if (mode.getName().equals(name)) {
return mode;
}
}
return WindowMode.SMALL;
}
}
public BrowserUI() {
this.windowMode = WindowMode.SMALL;
this.userAgent = UserAgent.IPHONE;
initPanel();
}
private void initPanel() {
removeAll();
String url = HOME_PAGE;
if (lastUrl != null) {
url = lastUrl;
}
if (this.browserService != null) {
this.browserService.close();
}
this.browserService = new JcefBrowserService(url);
this.browserService.setUserAgent(userAgent);
browserUI = browserService.getUI();
urlField = new JTextField(url);
resize();
Dimension buttonDimension = new Dimension(50, 25);
Box hbox = Box.createHorizontalBox();
JButton exitButton = new JButton("✕");
exitButton.setToolTipText("退出");
exitButton.setPreferredSize(buttonDimension);
exitButton.addActionListener(l -> Command.OVER.exec());
hbox.add(exitButton);
JButton homeButton = new JButton("♨");
homeButton.setToolTipText("主页");
homeButton.setPreferredSize(buttonDimension);
homeButton.addActionListener(l -> browserService.loadURL(HOME_PAGE));
hbox.add(homeButton);
JButton backButton = new JButton("←");
backButton.setToolTipText("后退");
backButton.setPreferredSize(buttonDimension);
backButton.addActionListener(l -> browserService.goBack());
hbox.add(backButton);
JButton forwardButton = new JButton("→");
forwardButton.setToolTipText("前进");
forwardButton.setPreferredSize(buttonDimension);
forwardButton.addActionListener(l -> browserService.goForward());
hbox.add(forwardButton);
JButton refreshButton = new JButton("⟳");
refreshButton.setToolTipText("刷新");
refreshButton.setPreferredSize(buttonDimension);
refreshButton.addActionListener(l -> browserService.reload());
hbox.add(refreshButton);
hbox.add(urlField);
JPanel urlPanel = new JPanel();
urlPanel.add(hbox);
Box h2Box = Box.createHorizontalBox();
h2Box.add(new JLabel("Window:"));
ComboBox windowModeBox = new ComboBox();
windowModeBox.setPreferredSize(buttonDimension);
for (WindowMode mode : WindowMode.values()) {
windowModeBox.addItem(mode.getName());
}
windowModeBox.setSelectedItem(windowMode.getName());
windowModeBox.addItemListener(l -> {
windowMode = WindowMode.getMode(l.getItem().toString());
resize();
updateUI();
});
h2Box.add(windowModeBox);
h2Box.add(Box.createHorizontalStrut(5));
h2Box.add(new JLabel("UA:"));
ComboBox uaBox = new ComboBox();
uaBox.setPreferredSize(new Dimension(100, 30));
for (UserAgent userAgent : UserAgent.values()) {
uaBox.addItem(userAgent.getName());
}
uaBox.setSelectedItem(userAgent.getName());
uaBox.addItemListener(l -> {
userAgent = UserAgent.getUserAgent(l.getItem().toString());
browserService.setUserAgent(userAgent);
browserService.reload();
});
h2Box.add(uaBox);
JPanel bottomPanel = new JPanel();
bottomPanel.add(h2Box);
setLayout(new BorderLayout());
add(urlPanel, BorderLayout.NORTH);
add(browserUI, BorderLayout.CENTER);
add(bottomPanel, BorderLayout.SOUTH);
add(Box.createHorizontalStrut(10), BorderLayout.EAST);
updateUI();
urlField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
String url = urlField.getText();
if (!StrUtil.startWithAny(url, "https://", "http://")) {
url = "https://" + url;
}
browserService.loadURL(url);
}
}
});
this.browserService.addEventListener(new BrowserEventListener() {
@Override
public void onAddressChange(String url) {
lastUrl = url;
urlField.setText(url);
}
@Override
public void onBeforeClose() {
SwingUtilities.invokeLater(() -> initPanel());
}
});
}
private void resize() {
int width = this.windowMode.getWidth();
int height = this.windowMode.getHeight();
urlField.setPreferredSize(new Dimension(width * 2 / 3, 30));
urlField.updateUI();
Dimension browserDimension = new Dimension(width, height);
browserUI.setMinimumSize(null);
browserUI.setPreferredSize(null);
if (windowMode == WindowMode.SMALL) {
browserUI.setPreferredSize(browserDimension);
} else {
browserUI.setMinimumSize(browserDimension);
}
}
public void close() {
this.browserService.close();
}
}
浏览器入口
package cn.xeblog.plugin.tools.browser;
import cn.xeblog.plugin.annotation.DoTool;
import cn.xeblog.plugin.tools.AbstractTool;
import cn.xeblog.plugin.tools.Tools;
import cn.xeblog.plugin.tools.browser.ui.BrowserUI;
import java.awt.*;
/**
* @author anlingyi
* @date 2022/8/14 11:12 AM
*/
@DoTool(Tools.BROWSER)
public class Browser extends AbstractTool {
private BrowserUI browserUI;
@Override
protected void init() {
this.browserUI = new BrowserUI();
mainPanel.setLayout(new BorderLayout());
mainPanel.add(this.browserUI, BorderLayout.CENTER);
}
@Override
public void over() {
super.over();
if (browserUI != null) {
this.browserUI.close();
}
}
}