利用Electron简单撸一个Markdown编辑器的方法


Posted in Javascript onJune 10, 2019

Markdown 是我们每一位开发者的必备技能,在写 Markdown 过程中,总是寻找了各种各样的编辑器,但每种编辑器都只能满足某一方面的需要,却不能都满足于日常写作的各种需求。

所以萌生出自己动手试试,利用 Electron 折腾一个 Markdown 编辑器出来。

下面罗列出我所理想的 Markdown 编辑器的痛点需求:

  • 必须要有图床功能,而且还可以直接上传到自己的图片后台,如七牛;
  • 样式必须是可以自定义的;
  • 导出的 HTML 内容可以直接粘贴到公众号编辑器里,直接发布,而不会出现格式的问题;
  • 可以自定义固定模块,如文章的头部,或者尾部。
  • 可以自定义功能,如:自动载入随机图片,丰富我们的文章内容。
  • 必须是跨平台的。
  • 其它。

环境搭建

使用 Electron 作为跨平台开发框架,是目前最理想的选择,再者说,如:VS Code、Atom 等大佬级别的应用也是基于 Electron 开发的。

Electron

使用 JavaScript, HTML 和 CSS 构建跨平台的桌面应用

https://electronjs.org/

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

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

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

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

利用Electron简单撸一个Markdown编辑器的方法

VUE

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

electron-vue

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

vue init simulatedgreg/electron-vue FanlyMD

利用Electron简单撸一个Markdown编辑器的方法

安装插件,并运行:

npm installnpm run dev

利用Electron简单撸一个Markdown编辑器的方法

选择插件

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" 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
注:这里只是写死的头部和尾部内容。

有了头尾部内容,和编辑器的 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简单撸一个Markdown编辑器的方法

总结

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

  • 每个 Electron 应用只有一个 Main 进程,主要用于和系统打交道和创建应用窗口,在 Main 进程中,利用 ipcMain 监听来自 ipcRenderer的事件,但没有 send 方法,只能利用 BrowserWindow。webContents.send()。
  • 每个页面都有对应的 Renderer 进程,用于渲染页面。当然也有对应的 ipcRenderer 用于接收和发送事件。
  • 在 vue 页面组件中,我们还是借助 vue 的 $on 和 `$emit 传递和接收消息。

接下来一步步完善该应用,目标是满足于自己的需要,然后就是:也许哪天就开源了呢。

解决中文编码问题

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JS 统计时间
Mar 09 Javascript
JavaScript脚本语言在网页中的简单应用
May 13 Javascript
两个比较有用的Javascript工具函数代码
Feb 17 Javascript
Js中的onblur和onfocus事件应用介绍
Aug 27 Javascript
JavaScript获取DOM元素的11种方法总结
Apr 25 Javascript
jQuery插件scroll实现无缝滚动效果
Apr 27 Javascript
jQuery密码强度检测插件passwordStrength用法实例分析
Oct 30 Javascript
WdatePicker.js时间日期插件的使用方法
Jul 26 Javascript
select标签设置默认选中的选项方法
Mar 02 Javascript
vue构建动态表单的方法示例
Sep 22 Javascript
关于js陀螺仪的理解分析
Apr 11 Javascript
vue 实现通过vuex 存储值 在不同界面使用
Nov 11 Javascript
js实现类似iphone的网页滑屏解锁功能示例【附源码下载】
Jun 10 #Javascript
基于jquery实现的tab选项卡功能示例【附源码下载】
Jun 10 #jQuery
对node通过fs模块判断文件是否是文件夹的实例讲解
Jun 10 #Javascript
Javascript数组方法reduce的妙用之处分享
Jun 10 #Javascript
利用node 判断打开的是文件 还是 文件夹的实例
Jun 10 #Javascript
javascript function(函数类型)使用与注意事项小结
Jun 10 #Javascript
浅谈ECMAScript 中的Array类型
Jun 10 #Javascript
You might like
饭制《星际争霸》Mod:优化游戏机制 增加新单位
2017/07/02 星际争霸
DC最新动画电影:《战争之子》为何内容偏激,毁了一个不错的漫画
2020/04/09 欧美动漫
兼容ie6浏览器的php下载文件代码分享
2014/07/14 PHP
PHP date函数常用时间处理方法
2015/05/11 PHP
PHP实现的简单sha1加密功能示例
2017/08/27 PHP
Laravel学习基础之migrate的使用教程
2017/10/11 PHP
获取DOM对象的几种扩展及简写
2006/10/09 Javascript
js word表格动态添加代码
2010/06/07 Javascript
js函数获取html中className所在的内容并去除标签
2013/09/08 Javascript
深入理解javascript的执行顺序
2014/04/04 Javascript
angularjs中的单元测试实例
2014/12/06 Javascript
jquery任意位置浮动固定层插件用法实例
2015/05/29 Javascript
javascript实现点击提交按钮后显示loading的方法
2015/07/03 Javascript
JavaScript实现基于十进制的四舍五入实例
2015/07/17 Javascript
swtich/if...else的替代语句
2015/08/16 Javascript
学习JavaScript设计模式之享元模式
2016/01/18 Javascript
JavaScript表单验证开发
2016/11/23 Javascript
JS 循环li添加点击事件 (闭包的应用)
2016/12/10 Javascript
layer.close()关闭进度条和Iframe窗的方法
2018/08/17 Javascript
Element UI 自定义正则表达式验证方法
2018/09/04 Javascript
微信小程序使用canvas自适应屏幕画海报并保存图片功能
2019/07/25 Javascript
layui导出所有数据的例子
2019/09/10 Javascript
通过GASP让vue实现动态效果实例代码详解
2019/11/24 Javascript
koa-passport实现本地验证的方法示例
2020/02/20 Javascript
Djang中静态文件配置方法
2015/07/30 Python
Python实现动态添加属性和方法操作示例
2018/07/25 Python
django 外键model的互相读取方法
2018/12/15 Python
Python读取xlsx文件的实现方法
2019/07/04 Python
python并发编程多进程 模拟抢票实现过程
2019/08/20 Python
Python列表删除元素del、pop()和remove()的区别小结
2019/09/11 Python
从pandas一个单元格的字符串中提取字符串方式
2019/12/17 Python
如何使用Python调整图像大小
2020/09/26 Python
如何在pycharm中安装第三方包
2020/10/27 Python
Python可以用来做什么
2020/11/23 Python
办公室文员岗位职责
2015/02/04 职场文书
php 防护xss,PHP的防御XSS注入的终极解决方案
2021/04/01 PHP