Preface#
Recently, I switched to a MacBook Air and found that it came with a built-in "Notes" app, which I thought was pretty good. I could use it to make daily plans and such. However, the one thing that really bothered me was that it didn't support Markdown! Every time I instinctively tried to use Markdown syntax, it felt awkward, like this:
It just looks weird, and the styling isn't very appealing either. So, why not DIY one myself? Let's get started with Electron!
Setting up the Framework#
I won't go into too much detail about this part. Our main focus is on implementing the Markdown note with support and optimizing its functionality. You can refer to the official documentation here: https://www.electronjs.org/zh/docs/latest/tutorial/quick-start
Implementing the Functionality#
Requirements analysis: Markdown note with support
Based on the requirements we created for ourselves, let's create a simple flowchart:
To control the size, let's not introduce any frameworks and just use Electron to implement the relevant functionality:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Markdown Notepad</title>
<style>
body {
display: flex;
height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
}
#markdown-input,
#markdown-preview {
flex: 1;
padding: 20px;
box-sizing: border-box;
height: 100%;
overflow-y: auto;
}
#markdown-input {
border-right: 1px solid #ddd;
}
#markdown-preview {
padding-left: 40px;
}
</style>
</head>
<body>
<textarea id="markdown-input" placeholder="Enter Markdown here..."></textarea>
<div id="markdown-preview"></div>
</body>
</html>
At this point, we notice that there is always a window bar at the top, which is not very elegant compared to the Notes app on Mac. We can modify the window we created by adjusting main.js (responsible for creating windows and handling system events):
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
let win; // Modify variable name to match its usage in the following code
function createWindow() {
// Create a borderless browser window
win = new BrowserWindow({
width: 320,
height: 320,
frame: false, // Set window to be borderless
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
}
});
By setting the frame
property in BrowserWindow, we can make the window border disappear.
Next, let's move on to the next step, which is to introduce a library for parsing Markdown syntax. Here, we choose Marked. Marked is a powerful JavaScript library that can parse Markdown text into HTML. Its advantages include speed, lightweight, support for GitHub-style Markdown syntax, and custom rendering. These advantages make Marked our preferred choice.
npm install marked
Let's handle the frontend logic in renderer.js:
mdInput.addEventListener('input', function () {
const renderedHtml = marked.parse(mdInput.value);
mdPreview.innerHTML = renderedHtml;
});
OK, let's make a slight modification to the HTML and see if the functionality is implemented correctly:
Great, the functionality is working! It renders successfully!
Further Optimization#
Do you feel that something is still not quite right? Having the left side and the right side like this doesn't look elegant at all. So, let's make some modifications and add buttons to toggle between the editing and preview modes:
<div id="app-container">
<textarea id="markdown-input" placeholder="Enter Markdown here..."></textarea>
<div id="markdown-preview"></div>
<div class="buttons-container">
<button id="toggle-pin" class="toggle-button">📌 Toggle Pin</button>
<button id="toggle-preview" class="toggle-button">👌🏻 Preview</button>
</div>
</div>
We'll still write the frontend logic in renderer.js and handle this logic:
togglePreviewBtn.addEventListener('click', () => {
isPreviewMode = !isPreviewMode; // Toggle preview mode state directly
if (isPreviewMode) {
mdInput.style.display = 'none';
mdPreview.style.display = 'block';
togglePreviewBtn.textContent = '✏️ Edit';
} else {
mdInput.style.display = 'block';
mdPreview.style.display = 'none';
togglePreviewBtn.textContent = '👌🏻 Preview';
}
});
OK, the functionality is implemented. I'm sure attentive readers have noticed that there is now an additional "📌 Toggle Pin" button. Since we want it to function as a sticky note, we naturally want it to have a "pin to top" feature. Here, we can use an important feature of Electron - Inter-Process Communication (IPC).
Inter-Process Communication (IPC) is a key part of building feature-rich desktop applications in Electron. Due to the different responsibilities of the main process and renderer processes in Electron's process model, IPC is the only way to perform many common tasks, such as calling native APIs from the UI or triggering changes to web content from native menus.
Our main idea here is that the "Toggle Pin" feature listens for button clicks in the renderer process, toggles the window's "always on top" state, and sends a message to the main process through Electron's IPC mechanism to perform the actual pinning operation. Here is the actual code:
// main.js
const { app, BrowserWindow, ipcMain } = require('electron');
//.....
// Listen for the "toggle-pin" message from the renderer process
ipcMain.on('toggle-pin', (event, shouldPin) => {
if (win) {
win.setAlwaysOnTop(shouldPin); // Set window to be always on top
}
});
// renderer.js
togglePinBtn.addEventListener('click', () => {
isWindowPinned = !isWindowPinned;
ipcRenderer.send('toggle-pin', isWindowPinned);
togglePinBtn.textContent = isWindowPinned ? '📌 Toggle Pin' : '📄 Just Paper';
});
Let's make some slight style optimizations and run Electron to see the final functionality:
At this point, all the functionality has been implemented.
Packaging and Building#
We will use electron-builder for packaging. Configure it in package.json:
"build": {
"appId": "com.yourdomain.memomark",
"mac": {
"category": "public.app-category.productivity",
"icon": "assets/MemoMark.icns" // Your logo
},
"dmg": {
"title": "MemoMark",
"icon": "assets/MemoMark.icns",
"window": {
"width": 600,
"height": 400
}
},
Note that the logo format for Mac is icns, so be careful not to make a mistake~
Run the packaging and building command:
npm run dist
Packaging successful! After installation, you can use your own developed Electron application locally~
Summary and Some Regrets#
When choosing a framework for building desktop applications, not only the functionality needs to be considered, but also factors such as performance, runtime size, and development experience. Here is a detailed comparison between Electron and Tauri:
Electron | Tauri | |
---|---|---|
Size | The minimum executable file size of Electron is about 50MB, due to its dependency on the complete Chromium and Node.js runtime. | Tauri has a significant advantage in terms of size. The size of a basic Tauri application ranges from a few hundred KB to 1-2MB. |
Speed | The speed of Electron is affected by its large size, as it needs to load the complete Chromium and Node.js runtime. | Tauri, written in Rust, is fast and has a short startup time. |
Security | The security of Electron depends on how it is used. Improper usage may lead to security vulnerabilities, such as directly using Node.js API in the renderer process. | Tauri is designed with security in mind and adopts a strict security model, including prohibiting direct access to Node.js API from the renderer. |
Community | Electron is developed by GitHub and has a large user base and mature community. It has extensive documentation and tutorials. | Tauri is a relatively new project, and its community is rapidly growing, but it is not as mature as Electron at the moment. |
Compatibility | Electron supports Windows, macOS, and Linux. | Tauri also supports Windows, macOS, and Linux. |
Development Experience | Electron supports HTML, CSS, and JavaScript, so frontend developers can quickly get started. | Tauri allows the use of any frontend framework that can be compiled into static HTML, CSS, and JavaScript, making it very flexible for frontend developers. |
Memory Usage | Electron has higher memory usage because each Electron application needs to run its own Chromium and Node.js instances. | Tauri has relatively lower memory usage because it does not depend on heavyweight runtime environments. |
For the Markdown note app I implemented, the size has already reached 237MB (although it could be due to my lack of optimization). If I were to use Tauri, the size might be better controlled, which is the biggest regret of this project.
If you are interested in this project, maybe you can take a look at it here: https://github.com/isolcat/MemoMark. If you can download it, try it out, and provide issues or PRs, that would be even better. I hope this Markdown-supported Electron note-taking app can help you~