基于Java和Bytemd用120行代碼實現(xiàn)一個桌面版Markdown編輯器
前提
某一天點開掘金的寫作界面的時候,發(fā)現(xiàn)了內(nèi)置Markdown編輯器有一個Github的圖標,點進去就是一個開源的Markdown編輯器項目bytemd(https://github.com/bytedance/bytemd):

這是一個NodeJs項目,由字節(jié)跳動提供。聯(lián)想到之前業(yè)余的時候做過一些Swing或者JavaFx的Demo,記得JavaFx中有一個組件WebView已經(jīng)支持Html5、CSS3和ES5,這個組件作為一個嵌入式瀏覽器,可以輕松地渲染一個URL里面的文本內(nèi)容或者直接渲染一個原始的Html字符串。另外,由于原生的JavaFx的視覺效果比較丑,可以考慮引入Swing配合IntelliJ IDEA的主題提供更好的視覺效果。本文的代碼基于JDK11開發(fā)。
引入依賴
很多人吐槽過Swing組件的視覺效果比較差,原因有幾個:
技術(shù)小眾,現(xiàn)在有更好的組件進行混合開發(fā)和跨平臺開發(fā) 基于上一點原因,導致很少人會去開發(fā) Swing組件的UI,其實Swing的每個組件都可以重新實現(xiàn)UI的表現(xiàn)效果compose-jb(JetBrains)組件很晚才發(fā)布出來,剛好碰上Swing官方停止維護,后面應該更加少人會使用Swing做GUI開發(fā)
使用Swing并且成功了的方案最知名的就是JetBrains全家桶。目前來看,為了解決這個"丑"的問題,現(xiàn)在有比較簡單的處理方案:
方案一:使用 compose-jb(名字有點不好聽,官方倉庫為https://github.com/JetBrains/compose-jb)開發(fā),這個是JetBrains系列的通用組件,基于Swing做二次封裝,「不過必須使用語言Kotlin,有點強買強賣的嫌疑」,這列貼兩個官方的圖參考一下:


方案二: FormDev(之前推出過Swing布局器的開發(fā)商,官網(wǎng)https://www.formdev.com/flatlaf)提供的FlatLaf(Flat Look and Feel),提供了Light Dark IntelliJ and Darcula themes,而且依賴少,使用起來十分簡單,個人認為當前這個是Swing UI組件視覺效果首選

引入FlatLaf和OpenFx的依賴:
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf-intellij-themes</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>11.0.2</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
<version>11.0.2</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
<version>11.0.2</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-base</artifactId>
<version>11.0.2</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>11.0.2</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>11.0.2</version>
</dependency>
布局和實現(xiàn)
布局的實現(xiàn)比較簡單:

最終的H5文本渲染在WebView組件中(JFXPanel是JavaFx => Swing的適配器,WebView是JavaFx的組件,但是這里使用的外層容器都是Swing組件),具體的編碼實現(xiàn)如下:
public class MarkdownEditor {
private static final int W = 1200;
private static final int H = 1000;
private static final String TITLE = "markdown editor";
public static String CONTENT = "<!DOCTYPE html>\n" +
"<html lang=\"en\">\n" +
"<head>\n" +
" <meta charset=\"UTF-8\"/>\n" +
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"/>\n" +
" <title>ByteMD example</title>\n" +
" <link rel=\"stylesheet\" href=\"https://unpkg.com/bytemd/dist/index.min.css\"/>\n" +
" <link rel=\"stylesheet\" href=\"https://unpkg.com/github-markdown-css\"/>\n" +
" <script src=\"https://unpkg.com/bytemd\"></script>\n" +
" <script src=\"https://unpkg.com/@bytemd/plugin-gfm\"></script>\n" +
" <script src=\"https://unpkg.com/@bytemd/plugin-highlight\"></script>\n" +
" <style>\n" +
" .bytemd {\n" +
" height: calc(100vh - 50px);\n" +
" }\n" +
"\n" +
" .footer {\n" +
" width: 100%;\n" +
" height: 30px;\n" +
" left: 0;\n" +
" position: absolute;\n" +
" bottom: 0;\n" +
" text-align: center;\n" +
" }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
"<div class=\"footer\">\n" +
" <a href=\"https://github.com/bytedance/bytemd\">bytemd</a>\n" +
"</div>\n" +
"<script>\n" +
" const plugins = [bytemdPluginGfm(), bytemdPluginHighlight()];\n" +
" const editor = new bytemd.Editor({\n" +
" target: document.body,\n" +
" props: {\n" +
" value: '# heading\\n\\nparagraph\\n\\n> blockquote',\n" +
" plugins,\n" +
" },\n" +
" });\n" +
" editor.$on('change', (e) => {\n" +
" editor.$set({value: e.detail.value});\n" +
" });\n" +
"</script>\n" +
"</body>\n" +
"</html>";
static {
// 初始化主題
try {
UIManager.setLookAndFeel(FlatIntelliJLaf.class.getName());
} catch (Exception e) {
throw new IllegalStateException("theme init error", e);
}
}
private static JFrame buildFrame(int w, int h, LayoutManager layoutManager) {
JFrame frame = new JFrame();
frame.setLayout(layoutManager);
frame.setTitle(TITLE);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(w, h);
Toolkit toolkit = Toolkit.getDefaultToolkit();
int x = (int) (toolkit.getScreenSize().getWidth() - frame.getWidth()) / 2;
int y = (int) (toolkit.getScreenSize().getHeight() - frame.getHeight()) / 2;
frame.setLocation(x, y);
return frame;
}
private static void initAndDisplay() {
// 構(gòu)建窗體
JFrame frame = buildFrame(W, H, new BorderLayout());
JFXPanel panel = new JFXPanel();
Platform.runLater(() -> {
panel.setSize(W, H);
initWebView(panel, CONTENT);
frame.getContentPane().add(panel);
});
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(MarkdownEditor::initAndDisplay);
}
private static void initWebView(JFXPanel fxPanel, String content) {
StackPane root = new StackPane();
Scene scene = new Scene(root);
WebView webView = new WebView();
WebEngine webEngine = webView.getEngine();
webEngine.setJavaScriptEnabled(true);
webEngine.loadContent(content);
root.getChildren().add(webView);
fxPanel.setScene(scene);
}
}
H5文本來源于bytemd的原生JS實現(xiàn)例子:

所有代碼加上注釋大概120多行。使用JDK11運行,結(jié)果如下:

目前有2個沒有解決的問題(也有可能是):
JS的動作觸發(fā)有輕微延遲WebView組件初始化比較慢
小結(jié)
Oracle JDK官方已經(jīng)宣布不再維護Swing項目,按照一般的尿性后面有可能從JDK中移除,也許是因為它體現(xiàn)不了自身的價值(低情商:不賺錢)。Swing的開發(fā)中布局是比較反人類的,一般可能一個Swing項目布局會耗費90%以上的時間,原生組件的UI設計看上去比較"丑",沒有豐富的擴展組件和活躍的社區(qū),加上現(xiàn)在有其他更好的跨平臺開發(fā)方案如Qt、React Native和Flutter等等,Swing被遺忘是一個既定的結(jié)局。往后除了一枝獨秀的JetBrains,Swing的結(jié)局就是成為少數(shù)人業(yè)務的愛好,成為JDK GUI編程愛好者的收藏品。
Demo源碼:
local-markdown-editor( https://gitee.com/throwableDoge/local-markdown-editor)
(本文完 e-a-20210815 c-1-d)
