发布于 2026-01-06 0 阅读
0

在 Electron 中创建文本编辑器:第 2 部分 - 编写文件

在 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")
                }
            ]
        }
    ]))

})
Enter fullscreen mode Exit fullscreen mode

我们首先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))

})
Enter fullscreen mode Exit fullscreen mode

这就是导航的拆分方式。

让我们创建./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 */]
        }

    ])    
}
Enter fullscreen mode Exit fullscreen mode

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')
            }
        }
    ]
Enter fullscreen mode Exit fullscreen mode

这样会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 ?
    })
})
Enter fullscreen mode Exit fullscreen mode

我们监听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`
    })
})
Enter fullscreen mode Exit fullscreen mode

上面我们正在向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}`)
    });
})
Enter fullscreen mode Exit fullscreen mode

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)
    });
})
Enter fullscreen mode Exit fullscreen mode

事情就是这样。

当然,您应该克隆代码库才能了解完整内容。这段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;
}
Enter fullscreen mode Exit fullscreen mode

我理解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
    })
}
Enter fullscreen mode Exit fullscreen mode

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'
        }
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

最终结果是:

今天就到这里!

不过,我觉得有必要说明一点显而易见的事实。我打算专注于 Electron 的基础知识。因此,正如你所看到的,我完全没有涉及验证方面的内容。

为了使该产品达到最低生产标准,我们需要做的很多事情中只有几项:

  • 检查文件是否已存在。
  • 在文件之间移动时处理未保存的文件。
  • 将内容转换为 Markdown 格式。
  • innerText使用而不是innerHTML(正如@simonhaisz在上一篇教程中指出的那样)来存储内容。
  • 还有许多其他事情,它们甚至可能比上述内容更重要。

然而,这些都不是 Electron 特有的,因此我选择不花时间编写和解释对学习 Electron 没有帮助的代码。

本系列教程还将再推出一个,届时我们将学习如何添加另一个窗口以及如何设置用户偏好。

同时,请查看GitHub上的项目,分支:part2

文章来源:https://dev.to/aurelkurtula/creating-a-text-editor-in-electron-part-2---writing-files-l80