利用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 相关文章推荐
Javascript中的数学函数
Apr 04 Javascript
简单的Jquery全选功能
Nov 07 Javascript
jQuery+HTML5实现手机摇一摇换衣特效
Jun 05 Javascript
JavaScript清空数组元素的两种方法简单比较
Jul 10 Javascript
jquery制作属于自己的select自定义样式
Nov 23 Javascript
js实现为a标签添加事件的方法(使用闭包循环)
Aug 02 Javascript
JS验证图片格式和大小并预览的简单实例
Oct 11 Javascript
jQuery接受后台传递的List的实例详解
Aug 02 jQuery
微信小程序实现搜索历史功能
Mar 26 Javascript
JS中的算法与数据结构之字典(Dictionary)实例详解
Aug 20 Javascript
JavaScript编写开发动态时钟
Jul 29 Javascript
(开源)微信小程序+mqtt,esp8266温湿度读取
Apr 02 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
zen cart新进商品的随机排序修改方法
2010/09/10 PHP
php中的一些数组排序方法分享
2012/07/20 PHP
PHP计算2点经纬度之间的距离代码
2013/08/12 PHP
PHP生成数组再传给js的方法
2014/08/07 PHP
PHP的Yii框架的常用日志操作总结
2015/12/08 PHP
Composer设置忽略版本匹配的方法
2016/04/27 PHP
Laravel框架控制器的request与response用法示例
2019/09/30 PHP
不同浏览器对回车提交表单的处理办法
2010/02/13 Javascript
幻灯片带网页设计中的20个奇妙应用示例小结
2012/05/27 Javascript
JS Replace()的高级使用方法介绍
2013/06/29 Javascript
css与javascript跨浏览器兼容性总结
2014/09/15 Javascript
JavaScript中的this机制
2016/01/30 Javascript
form+iframe解决跨域上传文件的方法
2016/11/18 Javascript
JS简单实现数组去重的方法示例
2017/03/27 Javascript
Javascript实现信息滚动效果
2017/05/18 Javascript
Vuejs在v-for中,利用index来对第一项添加class的方法
2018/03/03 Javascript
微信小程序自定义底部导航带跳转功能
2018/11/27 Javascript
实现一个Vue自定义指令懒加载的方法示例
2020/06/04 Javascript
通过实例解析javascript Date对象属性及方法
2020/11/04 Javascript
Python中使用装饰器和元编程实现结构体类实例
2015/01/28 Python
python判断windows系统是32位还是64位的方法
2015/05/11 Python
Python中max函数用法实例分析
2015/07/17 Python
python遍历 truple list dictionary的几种方法总结
2016/09/11 Python
python爬虫的工作原理
2017/03/05 Python
Python如何使用BeautifulSoup爬取网页信息
2019/11/26 Python
使用python客户端访问impala的操作方式
2020/03/28 Python
Sperry官网:帆船鞋创始品牌
2016/09/07 全球购物
美国非常受欢迎的Spa品牌:Bliss必列斯
2018/04/10 全球购物
食堂员工工作职责
2013/12/18 职场文书
合作经营协议书
2014/04/17 职场文书
南京青奥会口号
2014/06/12 职场文书
2014年群众路线教育实践活动整改措施
2014/09/24 职场文书
2014年高中班主任工作总结
2014/11/08 职场文书
股东大会通知
2015/04/24 职场文书
男方家长婚礼致辞
2015/07/27 职场文书
openstack云计算keystone组件工作介绍
2022/04/20 Servers