利用Electron簡(jiǎn)單擼一個(gè)Markdown編輯器的方法
Markdown 是我們每一位開發(fā)者的必備技能,在寫 Markdown 過程中,總是尋找了各種各樣的編輯器,但每種編輯器都只能滿足某一方面的需要,卻不能都滿足于日常寫作的各種需求。
所以萌生出自己動(dòng)手試試,利用 Electron 折騰一個(gè) Markdown 編輯器出來。
下面羅列出我所理想的 Markdown 編輯器的痛點(diǎn)需求:
- 必須要有圖床功能,而且還可以直接上傳到自己的圖片后臺(tái),如七牛;
- 樣式必須是可以自定義的;
- 導(dǎo)出的 HTML 內(nèi)容可以直接粘貼到公眾號(hào)編輯器里,直接發(fā)布,而不會(huì)出現(xiàn)格式的問題;
- 可以自定義固定模塊,如文章的頭部,或者尾部。
- 可以自定義功能,如:自動(dòng)載入隨機(jī)圖片,豐富我們的文章內(nèi)容。
- 必須是跨平臺(tái)的。
- 其它。
環(huán)境搭建
使用 Electron 作為跨平臺(tái)開發(fā)框架,是目前最理想的選擇,再者說,如:VS Code、Atom 等大佬級(jí)別的應(yīng)用也是基于 Electron 開發(fā)的。
Electron
使用 JavaScript, HTML 和 CSS 構(gòu)建跨平臺(tái)的桌面應(yīng)用
初次使用 Electron,我們下載回來運(yùn)行看看:
# 克隆示例項(xiàng)目的倉(cāng)庫(kù) $ git clone https://github.com/electron/electron-quick-start # 進(jìn)入這個(gè)倉(cāng)庫(kù) $ cd electron-quick-start # 安裝依賴并運(yùn)行 $ npm install && npm start

VUE
VUE 是當(dāng)前的前端框架的佼佼者,而且還是我們國(guó)人開發(fā)的,不得不服。本人也是 VUE 的忠實(shí)粉絲,在還沒火的 1.0 版本開始,我就使用 VUE 了。
electron-vue
將這兩者結(jié)合在一起,也就是本文推薦使用的 simulatedgreg/electron-vue:
vue init simulatedgreg/electron-vue FanlyMD

安裝插件,并運(yùn)行:
npm installnpm run dev

選擇插件
1. Ace Editor
選擇一個(gè)好的編輯器至關(guān)重要:
chairuosen/vue2-ace-editor: https://github.com/chairuosen/vue2-ace-editor
npm install buefy vue2-ace-editor vue-material-design-icons --save
2. markdown-it
能夠快速的解析 Markdown 內(nèi)容,我選擇是用插件:markdown-it
npm install markdown-it --save
3. electron-store
既然是編輯器應(yīng)用,所有很多個(gè)性化設(shè)置和內(nèi)容,就有必要存于本地,如編輯器所需要的樣式文件、自定義的頭部尾部?jī)?nèi)容等。這里我選擇:electron-store
npm install electron-store --save
整合
萬事俱備,接下來我們就開始著手實(shí)現(xiàn)簡(jiǎn)單的 Markdown 的編輯和預(yù)覽功能。
先看 src 文件夾結(jié)構(gòu):
. ├── README.md ├── app-screenshot.jpg ├── appveyor.yml ├── build │ └── icons │ ├── 256x256.png │ ├── icon.icns │ └── icon.ico ├── dist │ ├── electron │ │ └── main.js │ └── web ├── package.json ├── src │ ├── index.ejs │ ├── main │ │ ├── index.dev.js │ │ ├── index.js │ │ ├── mainMenu.js │ │ ├── preview-server.js │ │ └── renderer.js │ ├── renderer │ │ ├── App.vue │ │ ├── assets │ │ │ ├── css │ │ │ │ └── coding01.css │ │ │ └── logo.png │ │ ├── components │ │ │ ├── EditorPage.vue │ │ │ └── Preview.vue │ │ └── main.js │ └── store │ ├── content.js │ └── store.js ├── static └── yarn.lock
整個(gè) APP 主要分成左右兩列結(jié)構(gòu),左側(cè)編輯 Markdown 內(nèi)容,右側(cè)實(shí)時(shí)看到效果,而頁面視圖主要由 Renderer 來渲染完成,所以我們首先在 renderer/components/ 下創(chuàng)建 vue 頁面:EditorPage.vue:
<div id="wrapper">
<div id="editor" class="columns is-gapless is-mobile">
<editor
id="aceeditor"
ref="aceeditor"
class="column"
v-model="input"
@init="editorInit"
lang="markdown"
theme="twilight"
width="500px"
height="100%"></editor>
<preview
id="previewor"
class="column"
ref="previewor"></preview>
</div>
</div>
編輯區(qū)
左側(cè)使用插件:require('vue2-ace-editor'),處理實(shí)時(shí)監(jiān)聽 Editor 輸入 Markdown 內(nèi)容,將內(nèi)容傳出去。
watch: {
input: function(newContent, oldContent) {
messageBus.newContentToRender(newContent);
}
},
其中這里的 messageBus 就是把 vue 和 ipcRenderer 相關(guān)邏輯事件放在一起的 main.js:
import Vue from 'vue';
import App from './App';
import 'buefy/dist/buefy.css';
import util from 'util';
import { ipcRenderer } from 'electron';
if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
Vue.config.productionTip = false
export const messageBus = new Vue({
methods: {
newContentToRender(newContent) {
ipcRenderer.send('newContentToRender', newContent);
},
saveCurrentFile() { }
}
});
// 監(jiān)聽 newContentToPreview,將 url2preview 傳遞給 vue 的newContentToPreview 事件
// 即,傳給 Preview 組件獲取
ipcRenderer.on('newContentToPreview', (event, url2preview) => {
console.log(`ipcRenderer.on newContentToPreview ${util.inspect(event)} ${url2preview}`);
messageBus.$emit('newContentToPreview', url2preview);
});
/* eslint-disable no-new */
new Vue({
components: { App },
template: '<App/>'
}).$mount('#app')
編輯器的內(nèi)容,將實(shí)時(shí)由 ipcRenderer.send('newContentToRender', newContent); 下發(fā)出去,即由 Main 進(jìn)程的 ipcMain.on('newContentToRender', function(event, content) 事件獲取。
一個(gè) Electron 應(yīng)用只有一個(gè) Main 主進(jìn)程,很多和本地化東西 (如:本地存儲(chǔ),文件讀寫等) 更多的交由 Main 進(jìn)程來處理。
如本案例中,想要實(shí)現(xiàn)的第一個(gè)功能就是,「可以自定義固定模塊,如文章的頭部,或者尾部」
我們使用一個(gè)插件:electron-store,用于存儲(chǔ)頭部和尾部?jī)?nèi)容,創(chuàng)建Class:
import {
app
} from 'electron'
import path from 'path'
import fs from 'fs'
import EStore from 'electron-store'
class Content {
constructor() {
this.estore = new EStore()
this.estore.set('headercontent', `<img src="http://bimage.coding01.cn/logo.jpeg" class="logo">
<section class="textword"><span class="text">本文 <span id="word">111</span>字,需要 <span id="time"></span> 1分鐘</span></section>`)
this.estore.set('footercontent', `<hr>
<strong>coding01 期待您繼續(xù)關(guān)注</strong>
<img src="http://bimage.coding01.cn/coding01_me.GIF" alt="qrcode">`)
}
// This will just return the property on the `data` object
get(key, val) {
return this.estore.get('windowBounds', val)
}
// ...and this will set it
set(key, val) {
this.estore.set(key, val)
}
getContent(content) {
return this.headerContent + content + this.footerContent
}
getHeaderContent() {
return this.estore.get('headercontent', '')
}
getFooterContent() {
return this.estore.get('footercontent', '')
}
}
// expose the class
export default Content
注:這里只是寫死的頭部和尾部?jī)?nèi)容。
有了頭尾部?jī)?nèi)容,和編輯器的 Markdown 內(nèi)容,我們就可以將這些內(nèi)容整合,然后輸出給我們的右側(cè) Preview 組件了。
ipcMain.on('newContentToRender', function(event, content) {
const rendered = renderContent(headerContent, footerContent, content, cssContent, 'layout1.html');
const previewURL = newContent(rendered);
mainWindow.webContents.send('newContentToPreview', previewURL);
});
其中,renderContent(headerContent, footerContent, content, cssContent, 'layout1.html') 方法就是將我們的頭部、尾部、Markdown內(nèi)容、css 樣式和我們的模板 layout1.html 載入。這個(gè)就比較簡(jiǎn)單了,直接看代碼:
import mdit from 'markdown-it';
import ejs from 'ejs';
const mditConfig = {
html: true, // Enable html tags in source
xhtmlOut: true, // Use '/' to close single tags (<br />)
breaks: false, // Convert '\n' in paragraphs into <br>
// langPrefix: 'language-', // CSS language prefix for fenced blocks
linkify: true, // Autoconvert url-like texts to links
typographer: false, // Enable smartypants and other sweet transforms
// Highlighter function. Should return escaped html,
// or '' if input not changed
highlight: function (/*str, , lang*/) { return ''; }
};
const md = mdit(mditConfig);
const layouts = [];
export function renderContent(headerContent, footerContent, content, cssContent, layoutFile) {
const text = md.render(content);
const layout = layouts[layoutFile];
const rendered = ejs.render(layout, {
title: 'Page Title',
content: text,
cssContent: cssContent,
headerContent: headerContent,
footerContent: footerContent,
});
return rendered;
}
layouts['layout1.html'] = `
<html>
<head>
<meta charset='utf-8'>
<title><%= title %></title>
<style>
<%- cssContent %>
</style>
</head>
<body>
<div class="markdown-body">
<section class="body_header">
<%- headerContent %>
</section>
<div id="content">
<%- content %>
</div>
<section class="body_footer">
<%- footerContent %>
</section>
</div>
</body>
</html>
`;
這里,使用插件 markdown-it 來解析 Markdown 內(nèi)容,然后使用ejs.render() 來填充模板的各個(gè)位置內(nèi)容。這里,同時(shí)也為我們的目標(biāo):樣式必須是可以自定義的 和封裝各種不同情況下,使用不同的頭部、尾部、模板、和樣式提供了伏筆
當(dāng)有了內(nèi)容后,我們還需要把它放到「服務(wù)器」上,const previewURL = newContent(rendered);
import http from 'http';
import url from 'url';
var server;
var content;
export function createServer() {
if (server) throw new Error("Server already started");
server = http.createServer(requestHandler);
server.listen(0, "127.0.0.1");
}
export function newContent(text) {
content = text;
return genurl('content');
}
export function currentContent() {
return content;
}
function genurl(pathname) {
const url2preview = url.format({
protocol: 'http',
hostname: server.address().address,
port: server.address().port,
pathname: pathname
});
return url2preview;
}
function requestHandler(req, res) {
try {
res.writeHead(200, {
'Content-Type': 'text/html',
'Content-Length': content.length
});
res.end(content);
} catch(err) {
res.writeHead(500, {
'Content-Type': 'text/plain'
});
res.end(err.stack);
}
}
最終得到 URL 對(duì)象,轉(zhuǎn)給我們右側(cè)的 Preview 組件,即通過 mainWindow.webContents.send('newContentToPreview', previewURL);
注:在 Main 和 Renderer 進(jìn)程間通信,使用的是ipcMain和ipcRenderer。ipcMain無法主動(dòng)發(fā)消息給ipcRenderer。因?yàn)?code>ipcMain只有.on()方法沒有.send()的方法。所以只能用webContents。
預(yù)覽區(qū)
右側(cè)使用的時(shí)間上就是一個(gè) iframe 控件,具體做成一個(gè)組件 Preview:
<template>
<iframe src=""/>
</template>
<script>
import { messageBus } from '../main.js';
export default {
methods: {
reload(previewSrcURL) {
this.$el.src = previewSrcURL;
}
},
created: function() {
messageBus.$on('newContentToPreview', (url2preview) => {
console.log(`newContentToPreview ${url2preview}`);
this.reload(url2preview);
});
}
}
</script>
<style scoped>
iframe { height: 100%; }
</style>
在 Preview 組件我們使用 vue 的 $on 監(jiān)聽 newContentToPreview 事件,實(shí)時(shí)載入 URL 對(duì)象。
messageBus.$on('newContentToPreview', (url2preview) => {
this.reload(url2preview);
});
到此為止,我們基本實(shí)現(xiàn)了最基礎(chǔ)版的 Markdown 編輯器功能,yarn run dev 運(yùn)行看看效果:

總結(jié)
第一次使用 Electron,很膚淺,但至少學(xué)到了一些知識(shí):
- 每個(gè) Electron 應(yīng)用只有一個(gè) Main 進(jìn)程,主要用于和系統(tǒng)打交道和創(chuàng)建應(yīng)用窗口,在 Main 進(jìn)程中,利用 ipcMain 監(jiān)聽來自 ipcRenderer的事件,但沒有 send 方法,只能利用 BrowserWindow。webContents.send()。
- 每個(gè)頁面都有對(duì)應(yīng)的 Renderer 進(jìn)程,用于渲染頁面。當(dāng)然也有對(duì)應(yīng)的 ipcRenderer 用于接收和發(fā)送事件。
- 在 vue 頁面組件中,我們還是借助 vue 的 $on 和 `$emit 傳遞和接收消息。
接下來一步步完善該應(yīng)用,目標(biāo)是滿足于自己的需要,然后就是:也許哪天就開源了呢。
解決中文編碼問題
由于我們使用 iframe,所以需要在 iframe 內(nèi)嵌的 <html></html> 增加 <meta charset='utf-8'>
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
淺談Vue CLI 3結(jié)合Lerna進(jìn)行UI框架設(shè)計(jì)
這篇文章主要介紹了淺談Vue CLI 3結(jié)合Lerna進(jìn)行UI框架設(shè)計(jì),在此之前先簡(jiǎn)單介紹一下Element的構(gòu)建流程,以便對(duì)比新的UI框架設(shè)計(jì)。小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2019-04-04
vue中實(shí)現(xiàn)路由跳轉(zhuǎn)的三種方式超詳細(xì)教程
這篇文章主要介紹了vue中實(shí)現(xiàn)路由跳轉(zhuǎn)的三種方式超詳細(xì)教程,其中聲明式router-link實(shí)現(xiàn)跳轉(zhuǎn)最簡(jiǎn)單的方法,可用組件router-link來替代a標(biāo)簽,每種方式給大家講解的非常詳細(xì)需要的朋友可以參考下2022-11-11
vue-router中scrollBehavior的巧妙用法
本文給大家介紹vue-router中scrollBehavior的妙用,文中給大家提到了兩種解決方案,需要的朋友可以參考下2018-07-07
vue實(shí)現(xiàn)微信瀏覽器左上角返回按鈕攔截功能
這篇文章主要介紹了vue實(shí)現(xiàn)微信瀏覽器左上角返回按鈕攔截功能,本文通過實(shí)例代碼相結(jié)合的形式給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-01-01
vue form表單post請(qǐng)求結(jié)合Servlet實(shí)現(xiàn)文件上傳功能
這篇文章主要介紹了vue form表單post請(qǐng)求結(jié)合Servlet實(shí)現(xiàn)文件上傳功能,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-01-01
vue實(shí)現(xiàn)無縫滾動(dòng)的示例詳解
這篇文章主要為大家詳細(xì)介紹了vue非組件如何實(shí)現(xiàn)列表的無縫滾動(dòng)效果,文中的示例代碼簡(jiǎn)潔易懂,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-09-09
Vue3 composition API實(shí)現(xiàn)邏輯復(fù)用的方法
本文主要介紹了Vue3 composition API實(shí)現(xiàn)邏輯復(fù)用的方法,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08

