Develop a simple Markdown editor using Vue and Electron

2021SC@SDUSC

This article does not involve the part of designing a set of Markdown rendering components. Markdown rendering components can be implemented by marked and highlight.js . But this article focuses on how to use Electron and Vue to build a desktop application, so the Markdown rendering component no longer reinvents the wheel, but directly uses mavon-editor .
Therefore, readers who want to learn how to write a Markdown text rendering component do not need to waste too much time in this article.

Electron

Introduction

Electron is an open source framework for developing cross-platform desktop applications using JavaScript, HTML and CSS. It has built-in Chromium kernel and Node , so that the single-page application we wrote can run on the browser-like platform on the desktop. Therefore, we can realize the GUI layout of the desktop application by writing web code, or call the rich native API provided by Node.js through JavaScript.
There are already a lot of applications developed using Electron, the most famous of which is Visual Studio Code, which almost every front-end programmer must have come into contact with .

Install

As we mentioned just now, Electron runs on the basis of Node.js. First, you need to install Node on your PC.
After installing Node.js, we open the terminal and enter in the terminal:

npm install -g electron # 全局安装 electron

After the installation is complete, check the version number of Electron to check whether the installation is successful:

electron -v
v16.0.4

Of course, you can also not install globally, but only for a certain project. Next, I will explain how to use Electron to develop a simple Vue-based Markdown editor.

Create a Vue/Electron project

This article defaults that readers already have a vue foundation at this time, and have installed the vue-cli tool globally in the development environment

Open the terminal and enter the command to create a Vue project:

vue create typark # 创建名为 Typark 的 Vue 项目

cd typark # 进入 Typark 文件夹中

After entering the Typark folder, we can start the Vue project yarn serveby , and we can access it through the browser. But our goal is to get an application that can run independently on the desktop, so naturally it cannot be achieved in this way. Therefore, we need to add Electron dependencies to this project. We open the terminal and type the command in the terminal:

vue add electron-builder

After the installation is complete, we can see all the dependencies installed in the project's package.jsonfile . And there are several startup items under scriptsthe field :

scripts: {
    
    
	// ... ,
    "elect": "electron .",
    "electron:build": "vue-cli-service electron:build",
    "electron:serve": "vue-cli-service electron:serve",
    "postinstall": "electron-builder install-app-deps",
    "postuninstall": "electron-builder install-app-deps"
}

mainThe field of package has also changed background.js. When we go to srcthe folder , we can see this file, which will be used as the entry file of the Electron application.
At this point, our dependencies have been successfully installed, and yarn electron:servewhen , Electron will be used to automatically open the project.
electron

To this end, we have successfully established a vue-based electron project, and we can enter the next stage.

When starting the project, it is very likely that you will encounter such a problem: the following string is always printed in the console:

Launching Electron...
Failed to fetch extension, trying 4 more times
Failed to fetch extension, trying 3 more times
Failed to fetch extension, trying 2 more times
Failed to fetch extension, trying 1 more times

Although in terms of results, the project was successfully launched after all, it would take a long time to start every time.
This is because we are in development mode, electron will be installed once at startup Vue Devtools, and we cannot connect due to some unexplainable reasons in the network. But this tool is not very important, so I will give a solution here:
enter background.jsthe file , find // Install Vue Devtoolsthe comment, and comment out all the installation code below it, like this:

if (isDevelopment && !process.env.IS_TEST) {
     
     
  // Install Vue Devtools
  // try {
     
     
  //   await installExtension(VUEJS_DEVTOOLS)
  // } catch (e) {
     
     
  //   console.error('Vue Devtools failed to install:', e.toString())
  // }
}

In this way, we can skip the installation link and start the application quickly.

background.js

The background.js located in the src folder is the entry file of the electron application, where we can write some Node code to control the window of the entire electron application. Let's focus on . In this asynchronous method, the work done when electron creates a window is written.
async function createWindow()

async function createWindow() {
    
    
  // Create the browser window.
  const win = new BrowserWindow({
    
    
    width: 800,
    height: 600,
    webPreferences: {
    
    

      // Use pluginOptions.nodeIntegration, leave this alone
      // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
      nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
      contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION
    }
  })

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    
    
    // Load the url of the dev server if in development mode
    await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
    if (!process.env.IS_TEST) win.webContents.openDevTools()
  } else {
    
    
    createProtocol('app')
    // Load the index.html when not in development
    win.loadURL('app://./index.html')
  }
}

First, new BrowserWindowinstantiate a browser window class, and pass a json object with window parameters to the window class during instantiation. After the creation is complete, determine whether it is currently in development mode, and pass different urls BrowserWindow.loadURLto , so that the previously initialized browser window loads different pages.
Next, we go back to the top of background.js , and we see that, in addition to the ones used createWindowin , we also introduced the module fromBrowserWindow , through which we can monitor and control the entire electron application. We slide down the code, under the method , there are several code blocks in the form of . Here is to register event listeners for electron applications. for example:electronappcreateWindowapp.on('',()=>{})app.on

app.on('window-all-closed', () => {
    
    
  // On macOS it is common for applications and their menu bar
  // to stay active until the user quits explicitly with Cmd + Q
  if (process.platform !== 'darwin') {
    
    
    app.quit()
  }
})

This piece of code is listening to window-all-closedthis event. When all the windows of the application are closed, if the platform on which the program is running is not on MacOS, then call to app.quit()terminate the running of the program.

In MacOS, even if all windows are closed, the program will generally continue to run until the user explicitly terminates the application through the Cmd + Q key combination

A basic background.js is these contents, and I will continue to introduce some new contents when developing this application later.

windowless application

Do you feel that this window is weak in the screenshot of electron shown to you before?
insert image description here
Do you feel that this default window is very monotonous and has no freshness at all? Do you want to design a window yourself to replace the original window? If you think so, well, I will teach you how to use electron to create a "windowless application".
Let's head over to background.js , down to createWindowthe methods . Let's manually add some configuration to BrowserWindow to achieve the effect of eliminating borders. We add a line of code to it frame: falseto indicate that we don't want to use the system's default window. After adding, we start the project again, and we will find that the original window border has disappeared.
borderless window
Then we can design a unique window and menu bar by ourselves. In order to speed up the development progress, we no longer reinvent the wheel, and directly introduce ElementUI
into the project . For the introduction method, please refer to the official document of ElementUI -Installation . We first clear the content in App.vue and present a blank page. Now we can start writing our top sidebar.

<template>
	<div id="app">
		<header class="head">
			<img src='./assets/logo.png' alt=" " id="windowLogo" />
			<span>Typark</span>
			<button class="windowBtn" id="closeWindowBtn">
				<i class="el-icon-close" />
			</button>
			<button class="windowBtn" id="resizeBtn">
				<i :class="maxSize?'el-icon-copy-document':'el-icon-full-screen'" />
			</button>
			<button class="windowBtn" id="miniSizeBtn">
				<i class="el-icon-minus" />
			</button>
		</header>
	</div>
</template>

<script>
export default {
     
     
	name: "App",
	data() {
     
     
		return {
     
     
			maxSize: false, // 当前窗口是否最大化
		};
	},
};
</script>

<style>
* {
     
     
	margin: 0%;
}

#app {
     
     
	width: 100vw;
	height: 100vh;
	overflow:hidden;
}

/* 应用图标 */
#windowLogo {
     
     
	width: 2.5em;
	height: 2.5em;
	vertical-align: top; /* 这里只要不是 baseline 就行,防止 img 与 span 垂直方向不在同一水平线上 */
}

/* 绘制一个好看一点的滚动条 */
::-webkit-scrollbar {
     
     
	width: 6px;
}

::-webkit-scrollbar-thumb {
     
     
	background-color: #a8a8a8;
	border-radius: 3px;
}

.head {
     
     
	width: 100vw;
	font-size: 12px;
	height: 2.5em;
	line-height: 2.5em;
	background-image: linear-gradient(to right, #b9b9b9 0%, #ffffff 75%); /* 绘制渐变 */
	position: relative;
	z-index: 9999; /* 防止其他 dom 元素覆盖在顶部边栏上方 */
}

.windowBtn {
     
     
	float: right;
	height: 2.25em;
	width: 3em;
	line-height: 2.5em;
	border: none;
	background: rgba(0, 0, 0, 0);
	outline: none;
}

.windowBtn:hover {
     
     
	cursor: pointer;
}

#miniSizeBtn:hover {
     
     
	background-color: #e0e0e0;
}

#resizeBtn:hover {
     
     
	background-color: #00a2ff;
	color: white;
}

#closeWindowBtn:hover {
     
     
	background-color: red;
	color: white;
}
</style>

insert image description here

This way we create a top sidebar for our application. But but but! Now we are surprised to find one thing: we have no way to drag this window, but also inexplicably select the text on the top sidebar, even the img of the logo can be dragged!
insert image description here
It seems that this is an extremely stupid top sidebar, but in fact, we only need to add a few lines of css code to perfectly solve the above problems:

  • The text in the top sidebar will be selected : to .headadduser-select: none
  • The window cannot be dragged : for .headAdd -webkit-app-region: drag, at the same time, in order to prevent users from dragging the window through the three buttons in the upper right corner, we also need to .windowBtnadd-webkit-app-region: no-drag
  • The application logo will be dragged out : .head imgadd-webkit-user-drag: none

After adding the above css code, we changed this "stupid top sidebar" into a "not so stupid top sidebar".
We're also going to make this top sidebar take care of what it needs to do: control the resizing and closing of windows. Here we need to introduce a new electron module - electron.ipcRenderer .

contextIsolation

Next, we need to go to background.js and continue adding new properties to BrowserWindow. In BrowserWindow, we have such a field: webPreferences. We directly modify it to the following form:

webPreferences: {
    
    
	webSecurity: false, // 禁用同源政策
	nodeIntegration: true, // 启用 Node 集成
	contextIsolation: false, // 禁用上下文隔离
}

The contextIsolation attribute is to ensure that our 预加载脚本and Electron's internal logic run to the webpage we load in a separate context, so that the webpage we load cannot directly call Node-related APIs.
We set this attribute to false, indicating that context isolation is not required, so that we can call the electron module in the vue page.

ipcRenderer Communication

Because the window of the electron application is controlled by background.js , we must find a way to communicate with background.js to realize the control window through vue, so electron.ipcRenderer has become our first choice. Let's start by writing a few receivers for background.js .

async function createWindow() {
    
    
	// Create the browser window.
	const win = new BrowserWindow({
    
    ...});
	//接收渲染进程的信息
	const ipc = require('electron').ipcMain;

	// 接到 'min' 信息
	ipc.on('min', function () {
    
    
		win.minimize(); // 窗口最小化
	});

	// 接到 'max' 信息
	ipc.on('max', function () {
    
    
		if (win.isMaximized()) {
    
     // 判断窗口是否最大化
			win.unmaximize(); // 窗口取消最大化
		} else {
    
    
			win.maximize(); // 窗口最大化
		}
	});
	ipc.on('close', function () {
    
    
		win.destroy(); // 摧毁窗口
	})
}

Next, we add several methods to the buttons in the vue page:

<template>
	<div id="app">
		<header class="head">
			/* ... */
			<button class="windowBtn" id="closeWindowBtn" @click="closeWindow">
				<i class="el-icon-close" />
			</button>
			<button class="windowBtn" id="resizeBtn" @click="resizeWindow">
				<i :class="maxSize?'el-icon-copy-document':'el-icon-full-screen'" />
			</button>
			<button class="windowBtn" id="miniSizeBtn" @click="minWindow">
				<i class="el-icon-minus" />
			</button>
		</header>
	</div>
</template>

<script>
const electron = window.require('electron'); // 导入 electron

export default {
     
     
	// ... ,
	methods: {
     
     
		closeWindow() {
     
     
			electron.ipcRenderer.send("close");
		},
		minWindow() {
     
     
			electron.ipcRenderer.send("min"); // 通过 ipcRenderer 发送 min 消息
		},
		resizeWindow() {
     
     
			electron.ipcRenderer.send("max"); // 通过 ipcRenderer 发送 max 消息
		},
	},
}
</script>

Now, our window can successfully realize the function of minimizing, maximizing and closing the window.
Next, we also need to let vue know every window change, so as to update datain maxSize.
Remember win.onwhat ? In fact, it can also monitor the change of window size:

async function createWindow() {
    
    
	const win = new BrowserWindow({
    
     ... });
	// ...
	win.on('resize', () => {
    
    
		win.webContents.send('resize', win.isMaximized())
	})
}

Here we let the window listen to the resize event, and whenever the size changes, we use whether the window is maximized as the load, and win.webContents.sendsend the resize event to the SPA loaded by the win window . In this way, we also need to receive this resize message in the vue page.

created() {
    
    
	// 当 vue 页面创建完成后直接进行事件监听
	electron.ipcRenderer.on("resize", (event, params) => {
    
    
		if (this.maxSize !== params) {
    
    
			this.maxSize = params;
			localStorage.setItem("maxSize", params);
		}
	})
}

In this way, our vue page can obtain the change of window size at any time, and switch the button icon in the upper right corner according to whether the current window is maximized or not.

Introducing Mavon-editor

Although I can implement a simple Markdown text rendering component through marked and highlight.js , the effect is not as good as other mature open source components. Therefore, we directly use a powerful open source Markdown editor based on Vue - mavon-editor here .

yarn add mavon-editor # 为项目添加 mavon-editor 依赖

Next we modify the main.js of the Vue project:

// ...
import mavonEditor from 'mavon-editor' // 引入 mavon-editor 组件
import 'mavon-editor/dist/css/index.css' // 引入 mavon-editor 需要的样式文件

Vue.use(mavonEditor); // Vue 全局注册组件

Now we can use this component in our project. Let's modify the project's GUI now.
We add a body section to the App, which houses the menu bar and our mavon-editor component.

<template>
	<div id="app">
		<header class="head">
			<img src='./assets/logo.png' alt=" " id="windowLogo" />
			<span>Typark{
   
   {filePath?' - ' + filePath.split("/")[filePath.split("/").length - 1]:''}}</span>
			<button class="windowBtn" id="closeWindowBtn" @click="closeWindow">
				<i class="el-icon-close" />
			</button>
			<button class="windowBtn" id="resizeBtn" @click="resizeWindow">
				<i :class="maxSize?'el-icon-copy-document':'el-icon-full-screen'" />
			</button>
			<button class="windowBtn" id="miniSizeBtn" @click="minWindow">
				<i class="el-icon-minus" />
			</button>
		</header>
		<div class="body">
			<!-- 菜单工具栏,现在先不添加功能 -->
			<div class="toolbars">
				<el-dropdown size="mini" trigger="click" placement="bottom-start">
					<button>文件(F)</button>
					<el-dropdown-menu slot="dropdown">
						<el-dropdown-item command="open">打开</el-dropdown-item>
						<el-dropdown-item command="save" :disabled="rawText===''" :divided="true">另存为</el-dropdown-item>
						<el-dropdown-item command="html" :disabled="rawText===''" :divided="true">导出为HTML</el-dropdown-item>
					</el-dropdown-menu>
				</el-dropdown>
			</div>

			<!-- 主体部分,放置 mavon-editor 组件 -->
			<div class="main" v-loading="outputing" element-loading-text="拼命导出中" element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.8)">
				<mavon-editor style="height: 100%; width: 100%;" :toolbars="markdownOption" v-model="rawText" ref="md" />
			</div>

		</div>
	</div>
</template>

<script>
export default {
     
     
	// ... ,
	data() {
     
     
		return {
     
     
			// ... ,
			rawText: "", // markdown 源码
			outputing: false, // 正在导出
			markdownOption: {
     
      // mavon-editor 配置
				bold: true, // 粗体
				italic: true, // 斜体
				header: true, // 标题
				underline: true, // 下划线
				strikethrough: true, // 中划线
				mark: true, // 标记
				superscript: true, // 上角标
				subscript: true, // 下角标
				quote: true, // 引用
				ol: true, // 有序列表
				ul: true, // 无序列表
				link: true, // 链接
				imagelink: true, // 图片链接
				code: true, // code
				table: true, // 表格
				fullscreen: false, // 全屏编辑
				readmodel: false, // 沉浸式阅读
				htmlcode: false, // 展示html源码
				help: true, // 帮助
				/* 1.3.5 */
				undo: true, // 上一步
				redo: true, // 下一步
				trash: true, // 清空
				save: true, // 保存(触发events中的save事件)
				/* 1.4.2 */
				navigation: true, // 导航目录
				/* 2.1.8 */
				alignleft: true, // 左对齐
				aligncenter: true, // 居中
				alignright: true, // 右对齐
				/* 2.2.1 */
				subfield: false, // 单双栏模式
				preview: false, // 预览
			},
		}
	}	
}
</script>

<style>
/* ... */
.body {
     
     
	width: 100%;
	height: calc(100% - 2.5em);
	overflow: hidden;
}

.toolbars {
     
     
	height: 1.5em;
	user-select: none;
}

.toolbars button {
     
     
	height: 1.5em;
	border: none;
}

.toolbars button:hover {
     
     
	background: #e0e0e0;
}

.el-dropdown-menu {
     
     
	user-select: none;
}

.main {
     
     
	-webkit-app-region: no-drag;
	width: 100vw;
	height: calc(100vh - 4em);
	overflow-x: hidden;
	overflow-y: overlay;
}

.markdown-body img {
     
     
	max-height: 100%;
}
</style>

In this way, the interface of our application is drawn. Let's see how it works:
insert image description here
well, the display looks good, now let's add some functionality to it and make it more usable.

Electron operates local files

Let's start by adding some functional interfaces to the app's menu bar.
Let's modify the code of the menu bar first:

<template>
<!-- ... -->
<el-dropdown size="mini" trigger="click" placement="bottom-start" @command="fileCommand">
	<button>文件(F)</button>
	<el-dropdown-menu slot="dropdown">
		<el-dropdown-item command="open">打开</el-dropdown-item>
		<el-dropdown-item command="save" :disabled="rawText===''" :divided="true">另存为</el-dropdown-item>
		<el-dropdown-item command="html" :disabled="rawText===''" :divided="true">导出为HTML</el-dropdown-item>
	</el-dropdown-menu>
</el-dropdown>
<!-- ... -->
</template>

<script>
export default {
     
     
	// ... ,
	methods: {
     
     
		// ... ,
		fileCommand(command) {
     
     
			switch (command) {
     
     
				case "open": {
     
      // 打开 markdown 文件
					electron.ipcRenderer.send("openFile");
					break;
				}
				case "save": {
     
      // 另存为
					if (this.rawText) {
     
     
						electron.ipcRenderer.send("saveNewFile", this.rawText);
					}
					break;
				}
				case "html": {
     
      // 导出 html 文件
					this.outputing = true;
					let filename = "";
					if (this.filePath) {
     
     
						filename =
							this.filePath.split("\\")[
								this.filePath.split("\\").length - 1
							];
						filename = filename.substring(
							0,
							filename.lastIndexOf(".")
						);
					}
					electron.ipcRenderer.send(
						"saveAsHtml",
						filename,
						this.$refs.md.d_render
					);
					break;
				}
			}
		},
	}
}
</script>

Next we will write the listener method for the electron application. Before that we need to add some dependencies to the project:

yarn add fs-extra

open markdown file

This function is used to select a local markdown file, and send the file name and content of the file to the vue page to display the data on the page. In background.js , we add a listening event for opening files to ipc :

import {
    
     app, protocol, BrowserWindow, dialog } from 'electron'
import fs from 'fs-extra'; // 使用fs模块
import path from 'path';

async function createMainWindow() {
    
    
	// ...	
	const ipc = require('electron').ipcMain;
	// ... 
	ipc.on("openFile", () => {
    
    
		dialog.showOpenDialog({
    
     // 通过 dialog 模块显示 “打开文件” 对话框
			properties: ['openFile'], // 参数选择打开文件
			filters: [
				{
    
     name: 'Markdown File', extensions: ['md', 'markdown', 'mmd', 'mkd', 'mdwn', 'mdown', 'mdx', 'mdtxt', 'apib', 'rmarkdown', 'rmd', 'txt', 'text'] }
			], // 文件类型过滤器,只留下 markdown 文件
		}).then((res) => {
    
    
			if (res && res.filePaths && res.filePaths.length > 0) {
    
     // 如果选择了文件
				fs.readFile(res.filePaths[0], "utf8", (err, data) => {
    
     // 通过 fs-extra 读取文件内容
					if (err) {
    
     // 读取失败
						win.webContents.send('openedFile', -1)
					} else {
    
     // 读取成功,将内容发送给 vue 项目
						win.webContents.send('openedFile', 0, res.filePaths[0], data)
					}
				})
			}
		})
	})
}

Now we need to add corresponding listeners to the vue page. Since we need to add more monitoring events in the future, for the convenience of maintenance, we no longer write the functions for creating monitors createdin , but create a method for them separately:

export default {
    
    
	// ... ,
	methods: {
    
    
		// ... ,
		initIpcRenderers() {
    
    
			electron.ipcRenderer.on("resize", (event, params) => {
    
    
				if (this.maxSize !== params) {
    
    
					this.maxSize = params;
					localStorage.setItem("maxSize", params);
				}
			});
			electron.ipcRenderer.on("openedFile", (e, status, path, data) => {
    
    
				if (status === 0) {
    
    
					this.filePath = path;
					this.rawText = data;
					this.initRawText = data;
				} else {
    
    
					console.log("读取失败");
				}
			});
		}
	},
	created() {
    
    
		this.initIpcRenderers();
	}
}

Now, we can open the local markdown file by clicking the File - Open button. At the same time, the opened file name will also be displayed in the title bar.

save as new file

With open files, we also need an API to write to local files.

ipc.on('saveNewFile', (event, data) => {
    
    
	dialog.showSaveDialog({
    
     // 通过 dialog 模块打开 保存文件 对话框
		title: "文件另存为",
		defaultPath: path.join(__dirname, `${
      
      data.replace(/\\|\/|\?|\?|\*|\"|\“|\”|\'|\‘|\’|\<|\>|\{
      
      |\}|\[|\]|\【|\】|\:|\:|\、|\^|\$|\!|\~|\`|\|/g, '').substring(0, 10)}.md`), // 默认文件保存路径
		filters: [
			{
    
     name: 'Markdown File', extensions: ['md', 'markdown', 'mmd', 'mkd', 'mdwn', 'mdown', 'mdx', 'mdtxt', 'apib', 'rmarkdown', 'rmd', 'txt', 'text'] }
		], // 文件类型过滤器,只保留为 markdown 文件
	}).then((res) => {
    
    
		if (res && res.filePath) {
    
    
			fs.writeFile(res.filePath, data, "utf8", (err) => {
    
    
				if (err) {
    
    
					win.webContents.send('savedNewFile', -1);
				} else {
    
     // 写入成功,返回保存路径
					win.webContents.send('savedNewFile', 0, res.filePath);
				}
			})
		}
	})
})

export to HTML

ipc.on('saveAsHtml', (event, filename, data) => {
    
    
	let htmlpath;
	if (filename) {
    
     // 如果当前文件存在,直接用文件名作为 html 的文件名
		htmlpath = path.join(__dirname, filename)
	} else {
    
     // 否则从渲染的 html 文本中提取
		htmlpath = path.join(__dirname, `${
      
      data.replace(/\\|\/|\?|\?|\*|\"|\“|\”|\'|\‘|\’|\<|\>|\{
      
      |\}|\[|\]|\【|\】|\:|\:|\、|\^|\$|\!|\~|\`|\|/g, '').substring(0, 10)}.html`)
	}
	dialog.showSaveDialog({
    
    
		title: "导出为HTML",
		defaultPath: htmlpath,
		filters: [
			{
    
     name: 'HTML', extensions: ['html'] }
		],
	}).then((res) => {
    
    
		if (res) {
    
    
			if (res.canceled) {
    
    
				mainWindow.webContents.send('savedAsHtml', -1);
			} else if (res.filePath) {
    
    
				const title = res.filePath.split('\\')[res.filePath.split('\\').length - 1]
				// 此处写入基本的 html 代码,其中包含了需要的 css 样式文件
				let html = `<!doctype html>\n<html>\n<head>\n<meta charset='UTF-8'><meta name='viewport' content='width=device-width initial-scale=1'>\n<link href="https://cdn.bootcss.com/github-markdown-css/2.10.0/github-markdown.min.css" rel="stylesheet">\n<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/styles/github.min.css" id="md-code-style">\n<title>${
      
      title}</title>\n</head>\n<body>\n<div class="markdown-body">\n${
      
      data}\n</div>\n</body>\n</html>`
				fs.writeFile(res.filePath, html, "utf8", (err) => {
    
    
					if (err) {
    
    
						mainWindow.webContents.send('savedAsHtml', 1, err);
					} else {
    
    
						mainWindow.webContents.send('savedAsHtml', 0);
					}
				})
			}
		}
	})
})

Save file while editing

mavon-editor reserves an interface for us to save files, which can Ctrl + Sbe triggered (or by buttons, but we markdownOptionshide it in ), but the specific logic needs to be implemented by ourselves.
First, we add a hook for the save method to mavon-editor:

<mavon-editor  @save="save" />
save() {
    
    
	if (this.filePath) {
    
     // 如果此时打开的是本地文件,则进行保存操作
		window.electron.ipcRenderer.send(
			"saveFile",
			this.filePath,
			this.rawText
		);
	} else if (this.rawText) {
    
     // 否则进行另存为操作
		window.electron.ipcRenderer.send("saveNewFile", this.rawText);
	}
},

We have written the ipc listener for the save as operation, now we add another listener for saving existing files:

ipc.on('saveFile', (event, path, data) => {
    
    
	fs.writeFile(path, data, "utf8", (err) => {
    
    
		if (err) {
    
    
			win.webContents.send('savedFile', -1);
		} else {
    
    
			win.webContents.send('savedFile', 0);
		}
	})
})

In this way, we have completed a basic editor that can read and write markdown files.

Import local pictures

Special attention needs to be paid to this function here! Readers, if you have the patience to read this far, you might as well try to use mavon-editor to add a local image to your markdown source code, and you will find that the local image cannot be parsed. I won’t go into too much detail here, but briefly explain the solution: this problem is because the XSS defense filters out high-risk attributes. You can add an attribute to the mavon-editor tag to disable XSS to solve this problem. At the same time, when adding a local picture by pasting, since the picture itself does not exist locally, I suggest making a backup of the picture and placing it in the installation directory of the electron application. Next, let's implement this function.:xssOptions="false"

<mavon-editor @imgAdd="imgAdd" />
export default {
    
    
	// ... ,
	methods: {
    
    
		// ... ,
		imgAdd(filename, imgfile) {
    
    
			if (imgfile.path !== "") {
    
    
				this.rawText = this.rawText.replace(
					`![${
      
      imgfile._name}](${
      
      filename})`,
					`![${
      
      imgfile._name}](${
      
      imgfile.path.replace(/\\/g, "/")})`
				);
			} else {
    
    
				electron.ipcRenderer.send(
					"pastePicture",
					imgfile.miniurl.split(",")[1],
					imgfile.type.split("/")[1],
					new Date().valueOf(),
					filename,
					imgfile._name
				);
			}
		},
	}
}
// background.js
ipc.on('pastePicture', (event, imgdata, imgtype, timestamp, filename, tagname) => {
    
    
	let destpath // 定义 目标路径 变量
	if (process.env.NODE_ENV === 'development') {
    
     // 处于开发模式下
		destpath = path.join(__dirname, 'user-images')
	} else {
    
    
		destpath = path.join(__dirname, '../user-images')
	}
	const dirExists = fs.pathExistsSync(destpath) // 判断文件夹是否存在
	if (!dirExists) {
    
    
		fs.mkdirSync(destpath) // 若不存在则创建该文件夹
	}
	let exists = fs.existsSync(path.join(destpath, `typark${
      
      timestamp}.${
      
      imgtype}`))
	while (exists) {
    
    
		exists = fs.existsSync(path.join(destpath, `typark${
      
      ++timestamp}.${
      
      imgtype}`)) // 如果文件重名,时间戳加一,直到不出现重名为止
	}
	fs.writeFile(path.join(destpath, `typark${
      
      timestamp}.${
      
      imgtype}`), Buffer.from(imgdata, 'base64'), (err) => {
    
    
		if (err) {
    
    
			mainWindow.webContents.send('pastedPicture', -1);
		} else {
    
     // 写入成功后返回 markdown 源码中的图片信息以及保存后的图片路径用于替换
			mainWindow.webContents.send('pastedPicture', 0, path.join(destpath, `typark${
      
      timestamp}.${
      
      imgtype}`), filename, tagname);
		}
	})
})

end

To this end, we have completed the construction of a simple markdown text editor. If readers find it helpful to you, you may wish to visit my official warehouse for this application:

There are more perfect functions here, if you like it, please give me a star ^ _ ^ , thank you for reading!

Guess you like

Origin blog.csdn.net/qq_53126706/article/details/121972983