利用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 相关文章推荐
firefox 和 ie 事件处理的细节,研究,再研究 书写同时兼容ie和ff的事件处理代码
Apr 12 Javascript
jquery向.ashx文件post中文乱码问题的解决方法
Mar 28 Javascript
js中文逗号转英文实现
Feb 11 Javascript
js实现字符串的16进制编码不加密
Apr 25 Javascript
Node.js与Sails ~项目结构与Mvc实现及日志机制
Oct 14 Javascript
Js+Ajax,Get和Post在使用上的区别小结
Jun 08 Javascript
bootstrap table实例详解
Jan 06 Javascript
vue.js动态数据绑定学习笔记
May 19 Javascript
javascript实现小型区块链功能
Apr 03 Javascript
微信小程序绑定手机号获取验证码功能
Oct 22 Javascript
Javascript 关于基本类型和引用类型的个人理解
Nov 01 Javascript
微信小程序在text文本实现多种字体样式
Nov 08 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
在php中使用sockets:从新闻组中获取文章
2006/10/09 PHP
分享php分页的功能模块
2015/06/16 PHP
CI框架源码解读之URI.php中_fetch_uri_string()函数用法分析
2016/05/18 PHP
WAF的正确bypass
2017/01/05 PHP
在PHP中实现使用Guzzle执行POST和GET请求
2019/10/15 PHP
js 省地市级联选择
2010/02/07 Javascript
JavaScript闭包 懂不懂由你反正我是懂了
2011/10/21 Javascript
jQuery学习笔记之 Ajax操作篇(一) - 数据加载
2014/06/23 Javascript
JavaScript常用小技巧小结
2014/12/29 Javascript
nodejs中的fiber(纤程)库详解
2015/03/24 NodeJs
jQuery网页右侧广告跟随滚动代码分享
2020/04/20 Javascript
AngularJS实现ajax请求的方法
2016/11/22 Javascript
@ResponseBody 和 @RequestBody 注解的区别
2017/03/08 Javascript
用vue写一个仿简书的轮播图的示例代码
2018/03/13 Javascript
使用 Element UI Table 的 slot-scope方法
2019/10/10 Javascript
Vue实现省市区三级联动
2020/12/27 Vue.js
python类继承用法实例分析
2014/10/10 Python
简单理解Python中的装饰器
2015/07/31 Python
Tensorflow环境搭建的方法步骤
2018/02/07 Python
pandas.DataFrame 根据条件新建列并赋值的方法
2018/04/08 Python
python和pygame实现简单俄罗斯方块游戏
2021/02/19 Python
Python get获取页面cookie代码实例
2018/09/12 Python
利用django+wechat-python-sdk 创建微信服务器接入的方法
2019/02/20 Python
详解Django-channels 实现WebSocket实例
2019/08/22 Python
在Python中等距取出一个数组其中n个数的实现方式
2019/11/27 Python
HTML5+CSS3应用详解
2014/02/24 HTML / CSS
HTML5基于flash实现播放RTMP协议视频的示例代码
2020/12/04 HTML / CSS
英国领先的奢侈品零售商之一:CRUISE
2016/12/02 全球购物
Nuts.com:优质散装,批发坚果、干果和巧克力等
2017/03/21 全球购物
Waterford英国官方网站:世界上最受欢迎的优质水晶品牌
2019/08/17 全球购物
弄虚作假心得体会
2014/09/10 职场文书
音乐教育专业自荐信
2014/09/18 职场文书
租房协议书范例
2014/10/14 职场文书
2016大学生社会实践单位评语
2015/12/01 职场文书
制定企业培训计划的五大要点!
2019/07/10 职场文书
浅谈Redis的keys命令到底有多慢
2021/10/05 Redis