在 Electron 中创建文本编辑器:第 2 部分 - 编写文件
在上一篇教程中,我们创建了基本结构。我们能够从目录中读取文件,在侧边栏中列出它们的标题,并且能够在屏幕上读取它们的内容。
在本教程中,我们将添加更多交互功能。首先,我们来谈谈菜单。由于我们还没有指定自己的菜单,Electron 会默认提供一个,但./main.js我们可以在其中创建自己的按钮,并让它们执行我们需要的功能。让我们来看一个例子。
const { app, BrowserWindow, Menu } = require('electron')
...
app.on('ready', function(){
devtools = new BrowserWindow()
window = new BrowserWindow({ x: 0, y: 0, width:800, height:600})
window.loadURL(path.join('file://', __dirname, 'static/index.html'))
window.setTitle('Texty')
Menu.setApplicationMenu(Menu.buildFromTemplate([
{
label: app.getName(),
submenu: [
{
label: `Hello`,
click: () => console.log("Hello world")
}
]
}
]))
})
我们首先Menu从 Electron 引入组件。然后,我们用它来创建即将加载的应用程序的菜单。以上只是一个示例。和往常一样,第一个标签用于打开子菜单。因此,我们使用应用程序名称作为标签,然后创建一个Hello按钮,该按钮会输出一条消息。
我们来扩展这个菜单。但是,由于对象可能非常大,我们把菜单放在一个单独的组件中。
// ./main.js
const menu = require('./components/Menu')
app.on('ready', function(){
window = new BrowserWindow({ x: 0, y: 0, width:800, height:600})
...
Menu.setApplicationMenu(menu(window))
})
这就是导航的拆分方式。
让我们创建./components/Menu.js一个能够返回函数的文件。
const {app, Menu } = require('electron')
module.exports = function(win){
return Menu.buildFromTemplate([
{
label: app.getName(),
submenu: [
{ label: `Hello`, click: () => console.log("Hello world") }
]
},
{
label: 'Edit',
submenu: [
{label: 'Undo', role: 'undo' },
{label: 'Redo', role: 'redo' },
{label: 'Cut', role: 'cut' },
{label: 'Copy', role: 'copy' },
{label: 'Paste', role:'paste' },
]
},
{
label: 'Custom Menu',
submenu: [/* We'll add more actions */]
}
])
}
Electron 提供了一系列角色,它们在底层负责繁重的底层工作。点击链接查看所有可用角色。
从现在开始,我们将把所有导航都作为子菜单添加Custom Menu——这样会更有趣!
创建新文档
目前,我们的应用程序能够从磁盘读取文件并显示其内容。(这种方法的缺陷将在文末讨论)
让我们添加添加新文档的功能。
我们首先在导航栏中添加一个按钮。所以请./components/Menu.js添加以下代码:
const { NEW_DOCUMENT_NEEDED } = require('../actions/types')
module.exports = function(window){
...
{
label: 'Custom Menu',
submenu: [
{
label: 'New',
accelerator: 'cmd+N',
click: () => {
window.webContents.send(NEW_DOCUMENT_NEEDED, 'Create new document')
}
}
]
这样会New在菜单上创建一个按钮,accelerator该属性用于给按钮添加快捷方式。然后,点击该按钮后,我们会向应用程序的渲染部分发送一条消息!
我读过的一些教程说这很难理解,但想想Redux,与 store 通信的唯一方法就是监听和分发消息。这里的情况也完全一样。
它./main.js负责后端处理。它使我们能够访问 Electron 的模块(例如菜单、如果需要的话访问网络摄像头等等)。
并非所有内容./static/scripts/*.js都能访问上述功能。这部分代码仅用于操作 DOM。甚至有充分的理由反对将这部分代码用于任何文件系统操作(详见下文)。
回到屋里,./static/scripts/index.js我们会仔细聆听NEW_DOCUMENT_NEEDED。
const { ipcRenderer } = require('electron');
const { NEW_DOCUMENT_NEEDED } = require(path.resolve('actions/types'))
ipcRenderer.on(NEW_DOCUMENT_NEEDED, (event , data) => {
let form = document.getElementById('form')
form.classList.toggle('show')
document.getElementById('title_input').focus()
form.addEventListener('submit', function(e){
e.preventDefault()
// write file here ?
})
})
我们监听NEW_DOCUMENT_NEEDED信号。接收到信号后,我们会显示一个表单(普通的 CSS 类切换按钮)。
表单提交后,我们需要创建一个新文件。
对于这个简单的应用程序,我们只需使用fs.writeFile下面的代码即可// write file here ?。但是,如果这是一个大型项目,我们不希望在渲染端进行任何文件系统操作。如果应用程序非常庞大,即使是./main.js这样的操作也无法处理(显然需要打开一个新窗口,这超出了我们讨论的范围)。不过,主要是为了探索如何实现,我们将允许./main.js写入系统。
const { ipcRenderer } = require('electron');
const { WRITE_NEW_FILE_NEEDED } = require(path.resolve('actions/types'))
...
form.addEventListener('submit', function(e){
e.preventDefault()
// write file here ?
ipcRenderer.send(WRITE_NEW_FILE_NEEDED, {
dir: `./data/${fileName}.md`
})
})
上面我们正在向WRITE_NEW_FILE_NEEDED通道发送一个对象(通道名称可以随意命名)。
接下来,./main.js我们创建文件,然后发送消息:
ipcMain.on(WRITE_NEW_FILE_NEEDED, (event, {dir}) => {
fs.writeFile(dir, `Start editing ${dir}`, function(err){
if(err){ return console.log('error is writing new file') }
window.webContents.send(NEW_FILE_WRITTEN, `Start editing ${dir}`)
});
})
WRITE_NEW_FILE_NEEDED当数据被传输时,思路完全相同:获取dir通过该通道发送的数据,将文件写入该目录,然后发送一条消息,表明写入过程已完成。
最后,回到./statics/scripts/index.js
form.addEventListener('submit', function(e){
e.preventDefault()
let fileName = e.target[0].value
...
ipcRenderer.on(NEW_FILE_WRITTEN, function (event, message) {
handleNewFile(e, `./data/${fileName}.md`, message)
});
})
事情就是这样。
当然,您应该克隆代码库才能了解完整内容。这段handleNewFile代码仅仅隐藏了表单,在应用打开期间处理点击事件,并在页面上显示内容。
const handleNewFile = function(form, dir, content){
let fileName =form.target[0].value
form.target.classList.remove('show')
let elChild = document.createElement('li')
elChild.innerText = fileName
readFileContentOnClick(dir, elChild) // read file on click
form.target[0].value = ''
form.target.parentNode.insertBefore(elChild,form.target.nextSibling);
document.getElementById('content').innerHTML = content;
}
我理解ipcRenderer和ipcMain之间通信的方法是思考redux的基础知识。我们与redux store通信的方式完全相同。
这是我们目前代码的示意图
正如你所见,这两个进程之间的这种交互对于我们目前的需求来说有点过于复杂,但为了不阻塞用户界面,这种做法是必要的。正如我所说,即使这样,在更大的应用程序中也可能不够用。我认为这不是一个功能,而是一个漏洞。
保存更改
最后,对于本系列的这一部分,我们需要保存更改。
遵循 Mac 的设计模式,我希望在文件需要保存时显示一个视觉提示,并在文件保存后移除该提示。从……开始./static/scripts/index.js
document.getElementById('content').onkeyup = e => {
if(!document.title.endsWith("*")){
document.title += ' *'
};
ipcRenderer.send(SAVE_NEEDED, { // alerting ./component/Menu.js
content: e.target.innerHTML,
fileDir
})
}
onkeyup这意味着已经输入了某些内容。如果是这种情况,请在标题中添加星号,然后将其传递SAVE_NEEDED给主进程。主进程需要已输入的信息以及受影响的文件目录。
这次我们不是在监听,./main.js而是在监听./components/Menu.js(这当然也是同一过程的一部分)。
let contentToSave = ''
ipcMain.on(SAVE_NEEDED, (event, content) => {
contentToSave = content
})
module.exports = function(window){
return Menu.buildFromTemplate([
...
{
label: 'Save',
click: () => {
if(contentToSave != ''){
fs.writeFile(contentToSave.fileDir, contentToSave.content, (err) => {
if (err) throw err;
window.webContents.send(SAVED, 'File Saved')
});
}
},
accelerator: 'cmd+S'
}
SAVE_NEEDED我们获取传输的内容。然后,每次选中Save该元素时,我们都会检查该内容是否存在,如果存在,则将其写入文件。文件写入完成后,我们会向渲染部分发送一个警报,其中包含消息File Saved,渲染部分会对其进行处理。./static/scripts/index.js
ipcRenderer.on(SAVED, (event , data) => { // when saved show notification on screen
el = document.createElement("p");
text = document.createTextNode(data);
el.appendChild(text)
el.setAttribute("id", "flash");
document.querySelector('body').prepend(el)
setTimeout(function() { // remove notification after 1 second
document.querySelector('body').removeChild(el);
document.title = document.title.slice(0,-1) // remove asterisk from title
}, 1000);
});
最终结果是:
今天就到这里!
不过,我觉得有必要说明一点显而易见的事实。我打算专注于 Electron 的基础知识。因此,正如你所看到的,我完全没有涉及验证方面的内容。
为了使该产品达到最低生产标准,我们需要做的很多事情中只有几项:
- 检查文件是否已存在。
- 在文件之间移动时处理未保存的文件。
- 将内容转换为 Markdown 格式。
innerText使用而不是innerHTML(正如@simonhaisz在上一篇教程中指出的那样)来存储内容。- 还有许多其他事情,它们甚至可能比上述内容更重要。
然而,这些都不是 Electron 特有的,因此我选择不花时间编写和解释对学习 Electron 没有帮助的代码。
本系列教程还将再推出一个,届时我们将学习如何添加另一个窗口以及如何设置用户偏好。
同时,请查看GitHub上的项目,分支:part2
文章来源:https://dev.to/aurelkurtula/creating-a-text-editor-in-electron-part-2---writing-files-l80
