视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
利用Electron简单撸一个Markdown编辑器的方法
2020-11-27 21:55:22 责编:小采
文档


初次使用 Electron,我们下载回来运行看看:

# 克隆示例项目的仓库
$ git clone https://github.com/electron/electron-quick-start

# 进入这个仓库
$ cd electron-quick-start

# 安装依赖并运行
$ npm install && npm start

VUE

VUE 是当前的前端框架的佼佼者,而且还是我们国人开发的,不得不服。本人也是 VUE 的忠实粉丝,在还没火的 1.0 版本开始,我就使用 VUE 了。

electron-vue

将这两者结合在一起,也就是本文推荐使用的 simulatedgreg/electron-vue

vue init simulatedgreg/electron-vue FanlyMD

安装插件,并运行:

npm installnpm run dev

选择插件

1. Ace Editor

选择一个好的编辑器至关重要:

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 内容,我选择是用插件:markdown-it

npm install markdown-it --save

3. electron-store

既然是编辑器应用,所有很多个性化设置和内容,就有必要存于本地,如编辑器所需要的样式文件、自定义的头部尾部内容等。这里我选择:electron-store

npm install electron-store --save

整合

万事俱备,接下来我们就开始着手实现简单的 Markdown 的编辑和预览功能。

先看 src 文件夹结构:

.
├── 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

整个 APP 主要分成左右两列结构,左侧编辑 Markdown 内容,右侧实时看到效果,而页面视图主要由 Renderer 来渲染完成,所以我们首先在 renderer/components/ 下创建 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>

编辑区

左侧使用插件:require('vue2-ace-editor'),处理实时监听 Editor 输入 Markdown 内容,将内容传出去。

watch: {
 input: function(newContent, oldContent) {
 messageBus.newContentToRender(newContent);
 }
},

其中这里的 messageBus 就是把 vue 和 ipcRenderer 相关逻辑事件放在一起的 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() { }
 }
});

// 监听 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')

编辑器的内容,将实时由 ipcRenderer.send('newContentToRender', newContent); 下发出去,即由 Main 进程的 ipcMain.on('newContentToRender', function(event, content) 事件获取。

一个 Electron 应用只有一个 Main 主进程,很多和本地化东西 (如:本地存储,文件读写等) 更多的交由 Main 进程来处理。

如本案例中,想要实现的第一个功能就是,「可以自定义固定模块,如文章的头部,或者尾部」

我们使用一个插件:electron-store,用于存储头部和尾部内容,创建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 期待您继续关注</strong>
 <img src="http://bimage.coding01.cn/coding01_me.GIF" 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
注:这里只是写死的头部和尾部内容。

有了头尾部内容,和编辑器的 Markdown 内容,我们就可以将这些内容整合,然后输出给我们的右侧 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内容、css 样式和我们的模板 layout1.html 载入。这个就比较简单了,直接看代码:

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 内容,然后使用ejs.render() 来填充模板的各个位置内容。这里,同时也为我们的目标:样式必须是可以自定义的 和封装各种不同情况下,使用不同的头部、尾部、模板、和样式提供了伏笔

当有了内容后,我们还需要把它放到「服务器」上,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 对象,转给我们右侧的 Preview 组件,即通过 mainWindow.webContents.send('newContentToPreview', previewURL);

注:在 Main 和 Renderer 进程间通信,使用的是 ipcMainipcRendereripcMain 无法主动发消息给 ipcRenderer。因为ipcMain只有 .on() 方法没有 .send() 的方法。所以只能用 webContents

预览区

右侧使用的时间上就是一个 iframe 控件,具体做成一个组件 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 监听 newContentToPreview 事件,实时载入 URL 对象。

messageBus.$on('newContentToPreview', (url2preview) => {
 this.reload(url2preview);
});

到此为止,我们基本实现了最基础版的 Markdown 编辑器功能,yarn run dev 运行看看效果:

总结

第一次使用 Electron,很肤浅,但至少学到了一些知识:

  • 每个 Electron 应用只有一个 Main 进程,主要用于和系统打交道和创建应用窗口,在 Main 进程中,利用 ipcMain 监听来自 ipcRenderer的事件,但没有 send 方法,只能利用 BrowserWindow。webContents.send()。
  • 每个页面都有对应的 Renderer 进程,用于渲染页面。当然也有对应的 ipcRenderer 用于接收和发送事件。
  • 在 vue 页面组件中,我们还是借助 vue 的 $on 和 `$emit 传递和接收消息。
  • 接下来一步步完善该应用,目标是满足于自己的需要,然后就是:也许哪天就开源了呢。

    解决中文编码问题

    由于我们使用 iframe,所以需要在 iframe 内嵌的 <html></html> 增加 <meta charset='utf-8'>

    代码如下:

    下载本文
    显示全文
    专题