使用開發(fā)記事本
是一個(gè)跨平臺(tái)的框架,可以用網(wǎng)頁(yè)語(yǔ)言來開發(fā)客戶端程序,雖然說每一個(gè)應(yīng)用都是一個(gè),但畢竟也是方便了我們這些前端開發(fā)者做自己的客戶端軟件的夢(mèng)想。
這里我為了能使用到最新版本的,并沒有選擇用-vue去作為項(xiàng)目的基礎(chǔ)模版,而是在上著了一個(gè)加了功能的模版易語(yǔ)言圖標(biāo)替換,輸入以下命令開始:
git clone git@github.com:szwacz/electron-boilerplate.git life-memory
一、測(cè)試模版是否可用
下載好之后,第一步就是將的版本更新到v7.1.7看看是否可以正常運(yùn)行,這里為了避免因?yàn)榫W(wǎng)絡(luò)問題導(dǎo)致下載失敗就直接用cnpm進(jìn)行安裝了。
經(jīng)過測(cè)試,這個(gè)模版的運(yùn)行及打包均沒有問題,可以正常執(zhí)行(Mac環(huán)境),這下子可以安心地去開發(fā)了。
二、為應(yīng)用添加菜單
模版中,應(yīng)用的菜單并不符合記事本的要求,因此需要調(diào)整一下。菜單的定義位于src/menu目錄下,我們要做兩件事,一是為Mac系統(tǒng)的菜單騰出第一個(gè)位置,二是補(bǔ)充自己需要的菜單項(xiàng)。下面是我的菜單項(xiàng)定義:
// file_menu_template.js
?
import { dialog } from 'electron'
import log from 'electron-log'
?
export const fileMenuTemplate = {
label: '文件',
submenu: [
// {
// label: '新建',
// accelerator: 'CmdOrCtrl+N'
// },
{
label: '打開',
accelerator: 'CmdOrCtrl+O',
click: openFile
},
{
type: 'separator'
},
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
click: saveFile
},
{
label: '另存為',
accelerator: 'CmdOrCtrl+Shift+S',
click: saveAsFile
}
]
}
?
export const macAppMenuTemplate = {
label: '生活記',
submenu: [
{
label: '退出',
role: 'quit'
}
]
}
?
/**
* 打開文件
* @param {MenuItem} menuItem 菜單項(xiàng)
* @param {BrowserWindow} browserWindow 渲染進(jìn)程窗口
* @param {Event} event 事件

*/
function openFile(menuItem, browserWindow, event) {
dialog.showOpenDialog(browserWindow, {
title: '打開文件',
filters: [
{ name: 'Markdown 文件', extensions: ['md', 'markdown'] },
{ name: '文本文件', extensions: ['txt'] }
],
properties: ['openFile']
}).then(dialogRes => {
if (!dialogRes.canceled) {
// 向當(dāng)前獲取焦點(diǎn)的窗口發(fā)送事件
if (browserWindow) {
browserWindow.webContents.send('lm-open-file', dialogRes.filePaths)
}
}
}).catch(e => {
log.error(e)
})
}
?
/**
* 保存文件
* @param {MenuItem} menuItem 菜單項(xiàng)
* @param {BrowserWindow} browserWindow 渲染進(jìn)程窗口
* @param {Event} event 事件
*/
function saveFile(menuItem, browserWindow, event) {
if (browserWindow) {
browserWindow.webContents.send('lm-save-file')
}
}
?
/**
* 另存為
* @param {MenuItem} menuItem 菜單項(xiàng)
* @param {BrowserWindow} browserWindow 渲染進(jìn)程窗口
* @param {Event} event 事件
*/
function saveAsFile(menuItem, browserWindow, event) {
if (browserWindow) {
browserWindow.webContents.send('lm-save-as-file')
}
}
?
因?yàn)榈谝粋€(gè)位置暫時(shí)也不需要添加其他內(nèi)容,所以我就沒有將其拆分出去,而是和文件菜單放在一個(gè)文件里了。
寫這篇文檔時(shí)項(xiàng)目已經(jīng)完成,所以這個(gè)文檔的代碼中會(huì)包含一些現(xiàn)在用不到的代碼,見諒見諒~
// edit_menu_template.js
?
export const editMenuTemplate = {
label: "編輯",
submenu: [
{ label: "撤銷", accelerator: "CmdOrCtrl+Z", click: undo },
{ label: "重做", accelerator: "Shift+CmdOrCtrl+Z", click: redo },
{ type: "separator" },
{ label: "剪切", accelerator: "CmdOrCtrl+X", selector: "cut:" },
{ label: "復(fù)制", accelerator: "CmdOrCtrl+C", selector: "copy:" },
{ label: "粘貼", accelerator: "CmdOrCtrl+V", selector: "paste:" },
{ label: "全選", accelerator: "CmdOrCtrl+A", click: selectAll }
]
};
?

/**
* 選擇全部
* @param {MenuItem} menuItem 菜單項(xiàng)
* @param {BrowserWindow} browserWindow 渲染進(jìn)程窗口
* @param {Event} event 事件
*/
function selectAll(menuItem, browserWindow, event) {
if (browserWindow) {
browserWindow.webContents.send('lm-select-all')
}
}
?
/**
* 撤銷
* @param {MenuItem} menuItem 菜單項(xiàng)
* @param {BrowserWindow} browserWindow 渲染進(jìn)程窗口
* @param {Event} event 事件
*/
function undo(menuItem, browserWindow, event) {
if (browserWindow) {
browserWindow.webContents.send('lm-undo')
}
}
?
/**
* 重做
* @param {MenuItem} menuItem 菜單項(xiàng)
* @param {BrowserWindow} browserWindow 渲染進(jìn)程窗口
* @param {Event} event 事件
*/
function redo(menuItem, browserWindow, event) {
if (browserWindow) {
browserWindow.webContents.send('lm-redo')
}
}
?
編輯菜單里面基本上都是對(duì)文檔內(nèi)容的快捷操作。
// help_menu_template.js
?
import { app, shell } from "electron";
import jetpack from "fs-jetpack";
?
const appDir = jetpack.cwd(app.getAppPath());
const manifest = appDir.read("package.json", "json");
?
export const helpMenuTemplate = {
label: '幫助',
submenu: [
{
label: '學(xué)習(xí)Markdown語(yǔ)法',
click: function (item, focusedWindow) {
// 打開外部文檔
shell.openExternal('https://www.runoob.com/markdown/md-tutorial.html')
}
},
// {
// label: '幫助'
// },
{
label: '關(guān)于',
submenu: [
{

label: '版本 v' + manifest.version,
enabled: false
}
// {
// label: '更新記錄'
// }
]
}
]
}
?
幫助菜單中則是將應(yīng)用的版本號(hào)顯示出來,另外還有一個(gè)開發(fā)時(shí)顯示的菜單,那個(gè)菜單只需要去掉退出應(yīng)用的菜單項(xiàng)即可。
菜單定義之后,在.js中,我們需要將新增的菜單定義加入,并稍微修改一下邏輯,讓Mac系統(tǒng)下的菜單列表前面增加一個(gè)占位的菜單。
import { devMenuTemplate } from "./menu/dev_menu_template";
import { editMenuTemplate } from "./menu/edit_menu_template";
import {
macAppMenuTemplate,
fileMenuTemplate
} from "./menu/file_menu_template";
import { helpMenuTemplate } from "./menu/help_menu_template";
?
const setApplicationMenu = () => {
const menus = [fileMenuTemplate, editMenuTemplate, helpMenuTemplate];
if (process.platform === "darwin") {
menus.unshift(macAppMenuTemplate);
}
if (env.name === "development") {
menus.push(devMenuTemplate);
}
Menu.setApplicationMenu(Menu.buildFromTemplate(menus));
};
此時(shí)運(yùn)行程序,我們定義的菜單就會(huì)如期顯示出來,接下來要讓程序?qū)τ脩酎c(diǎn)擊菜單項(xiàng)做出響應(yīng),則需要在菜單定義中定義click函數(shù),普通的菜單點(diǎn)擊,我們只需要將事件發(fā)送到當(dāng)前聚焦的窗口,讓它去處理這個(gè)事件即可。
不過這里就涉及到主線程主動(dòng)向渲染進(jìn)程發(fā)送消息的知識(shí)了,在上面的代碼中我們也可以看到,我們需要拿到的實(shí)例,然后獲取到它的對(duì)象,然后就可以向其發(fā)送消息了。而渲染進(jìn)程要接受消息,則是通過去獲取,這一點(diǎn)官方文檔已經(jīng)講得很詳細(xì)了,我就不再細(xì)說了。
還有一種更為復(fù)雜的情況,以打開文件為例,當(dāng)用戶點(diǎn)擊打開時(shí),程序應(yīng)該彈出窗口詢問用戶要打開哪個(gè)文件。而對(duì)話框只能由主線程來操作,當(dāng)前菜單點(diǎn)擊的處理線程正是主線程,你不可能說把事件傳給渲染進(jìn)程,再讓渲染進(jìn)程把打開對(duì)話框的事件傳給你。所以最好還是直接就在這里彈出對(duì)話框,將用戶選擇的文件交給渲染進(jìn)程處理就好了。這段代碼在上面的菜單定義中也有提及。
三、為應(yīng)用添加日志記錄功能
軟件開發(fā)過程中,不可避免會(huì)遇到bug,而當(dāng)bug到達(dá)用戶那里時(shí),身為開發(fā)者的你是不好去調(diào)試的。所以日志記錄就顯得尤為關(guān)鍵,還好生態(tài)中有比較好用的-log可以使用。我的用法比較簡(jiǎn)單,就在主線程中修改了日志記錄的格式,后面因?yàn)槿止蚕硪粋€(gè)實(shí)例,所以其他地方就不用去修改配置了,直接引入這個(gè)包即可。
import log from "electron-log";
?
// 修改日志記錄的格式
log.transports.console.format =
"[{h}:{i}:{s}.{ms}] [{level} {processType}] ? {text}";
log.transports.file.format =
"[{y}-{m}-dnrv3ppvb {h}:{i}:{s}.{ms}] [{level}] {text}";
log.debug("path of user data: ", app.getPath("userData"));
在啟動(dòng)時(shí)我還去打印了一下用戶數(shù)據(jù)的存放位置,方便以后排查問題。
四、其他關(guān)鍵點(diǎn)記錄
其實(shí)我的最終目標(biāo)不是開發(fā)一個(gè)簡(jiǎn)單的記事本,所以我在項(xiàng)目中引入了這個(gè)插件,引入的時(shí)候還是遇到了一些問題的,下面是我的解決方案:
首先在js文件中實(shí)例化:
import CodeMirror from "codemirror/lib/codemirror";
import "codemirror/mode/markdown/markdown";

?
this.editor = CodeMirror.fromTextArea(
document.getElementById(textareaId),
editorOptions
);
到這一步還是正常的,可當(dāng)我要引入它的CSS文件時(shí),它就報(bào)錯(cuò)了,我也不知道為什么。但最后想出了一個(gè)解決辦法,就是把css文件在html中引入:
?
<link rel="stylesheet" href="../node_modules/codemirror/lib/codemirror.css">
這樣做之后基本上就沒什么問題了,關(guān)于記事本的代碼其實(shí)很簡(jiǎn)單,邏輯也不復(fù)雜,就不貼出來獻(xiàn)丑了。
另外就是涉及到文件讀寫時(shí),node的文件系統(tǒng)讀寫結(jié)果是通過回調(diào)函數(shù)來獲取的,我覺得用起來很不爽,就寫了一個(gè)工具類把它包裝了一下,讓它返回對(duì)象。然后我用的時(shí)候就可以愉快的用async/await了~
最后一點(diǎn)就是我比較喜歡用scss去寫樣式文件,所以需要自己配置一下scss的編譯方式,首先需要安裝sass:
cnpm i sass node-sass sass-loader --save-dev
安裝之后,找到build/.base..js,在rules中添加:
{
test: /\.scss$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader'
},
{
loader: 'sass-loader'
}
]
}
這樣就可以解析scss文件了。
五、打包相關(guān)1、圖標(biāo)
應(yīng)用做好之后,我們需要為其準(zhǔn)備一個(gè)精美的圖標(biāo),而在不同的系統(tǒng)上使用的圖標(biāo)類型是不同的,因此我們?cè)诘玫揭粡?png的圖標(biāo)之后,還需要為平臺(tái)生成.ico格式的圖標(biāo),此時(shí)我們可以在/.en//這個(gè)網(wǎng)站去轉(zhuǎn)換。
若要為Mac平臺(tái)生成.icns的圖標(biāo)則沒有那么簡(jiǎn)單,因?yàn)閕cns格式并不是一個(gè)圖標(biāo),而是包含不同分辨率圖標(biāo)的集合,我們需要一個(gè)一個(gè)的生成然后再去轉(zhuǎn)換。
我就填了這個(gè)坑,我在剛剛的網(wǎng)站把轉(zhuǎn)換好的一張icns格式的圖片放到項(xiàng)目中打包,結(jié)果打包后的應(yīng)用是沒有圖標(biāo)的!
轉(zhuǎn)換icns并沒有網(wǎng)站可以幫我們做,我們只能在mac電腦中敲命令來做。具體可以參考這個(gè)博客,需要注意的是剛開始創(chuàng)建的目錄,后面的.不能省,前面的名字可以隨便起。
當(dāng)圖標(biāo)準(zhǔn)備完畢之后,把它們重命名替換掉項(xiàng)目中目錄下的圖標(biāo)文件即可。
2、為程序關(guān)聯(lián)文件格式
我希望我的程序在安裝過后可以在用戶想要打開文本文件時(shí)可以用我的程序來打開,可在Mac系統(tǒng)上,你會(huì)發(fā)現(xiàn)大部分應(yīng)用處于灰色狀態(tài)(如果你的應(yīng)用不做處理,也會(huì)是這個(gè)樣子的)
為了這個(gè)小功能我谷歌了好久好久都沒找到解決方案,不過最后偶然間看到了-的配置文檔,里面描述了如何配置文件關(guān)聯(lián)易語(yǔ)言圖標(biāo)替換,我只能說,,真NM簡(jiǎn)單。。
-的配置一般會(huì)放在.json中,恰好我用的這個(gè)模版里面的打包工具就是它,我們只需要在build下面加上下面的配置即可關(guān)聯(lián)自己定義的文件格式了。
"fileAssociations": [
{
"ext": "md",
"name": "Markdown 文件",

"role": "Editor"
},
{
"ext": "markdown",
"name": "Markdown 文件",
"role": "Editor"
},
{
"ext": "txt",
"name": "文本文件",
"role": "Editor"
}
],
關(guān)聯(lián)之后,程序只是虛有其表,因?yàn)槲覀儾]有真正去處理傳來的文件,所以下一步就是接收文件路徑。在這一步自己也爬了一個(gè)又一個(gè)坑,都是血和淚的教訓(xùn)啊。。。
在程序啟動(dòng)時(shí)接收文件路徑參數(shù),乍一想這個(gè)問題,就應(yīng)該是通過進(jìn)程對(duì)象就可以取到了。可當(dāng)時(shí)我在Mac電腦上開發(fā),不論怎么搞都沒辦法獲取到路徑參數(shù),谷歌也找不到答案。又是在萬(wàn)念俱灰之時(shí),我去看了看官方文檔,,,我只想說MMP
原來,Mac系統(tǒng)是要監(jiān)聽app的open-file事件的,而則是通過進(jìn)程對(duì)象來獲取文件路徑。
這樣可以獲取到文件路徑了,但最終這個(gè)路徑是要交給渲染進(jìn)程去處理的,而在程序剛啟動(dòng)時(shí)渲染進(jìn)程甚至還沒有創(chuàng)建出來!此時(shí),就需要在主進(jìn)程中先定義一個(gè)變量保存一下接收的這個(gè)路徑了,等待渲染進(jìn)程加載完成后再把這個(gè)路徑傳給它,所以,我的整個(gè)處理邏輯如下:
// background.js
?
// 外部文件路徑
let preFilePath = ''
?
app.on('will-finish-launching', () => {
log.debug('will-finish-launching')
?
// 打開文件事件(MacOS有效)
app.on("open-file", (e, filePath) => {
log.debug("open-file: ", filePath);
?
const fw = BrowserWindow.getFocusedWindow();
if (fw) {
fw.webContents.send("lm-open-file", [filePath]);
} else {
preFilePath = filePath
}
});
?
// 檢查進(jìn)程是否含有參數(shù)(Windows有效)
if (process.platform ==='win32' && process.argv.length >= 2) {
log.debug('process argv:', process.argv)
?
// windows系統(tǒng)當(dāng)沒有路徑參數(shù)時(shí)這個(gè)位置默認(rèn)有個(gè).,需要加以判斷
preFilePath = process.argv[1] === '.' ? '' : process.argv[1]
}
})
?
mainWindow.once('ready-to-show', () => {
log.debug('ready-to-show')
mainWindow.show()
?
// 檢查是否存在需要直接打開的文件,有的話就直接打開
if (preFilePath) {
mainWindow.webContents.send('lm-open-file', [preFilePath])
}
})
其中,在app的will--事件中才開始監(jiān)聽文件打開事件,也是官方文檔上面建議的:
這樣做了之后,整個(gè)記事本應(yīng)用才顯得完整起來。
其實(shí),上面的很多做法不僅限于記事本中使用,希望我寫的文章能對(duì)大家有所幫助!
下面放上我寫的記事本的截圖,來證明我做到過!