当前位置: 移动技术网 > IT编程>脚本编程>vue.js > 利用Electron简单撸一个Markdown编辑器的方法

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

2019年07月18日  | 移动技术网IT编程  | 我要评论

下水道的美人鱼观看,三国志关羽列传,2011河南招警

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

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

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

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

环境搭建

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

electron

使用 javascript, html 和 css 构建跨平台的桌面应用

初次使用 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:
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,很肤浅,但至少学到了一些知识:

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

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

解决中文编码问题

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

复制代码 代码如下:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网