1.1 配置Electron开发环境

问题

electron在安装时卡在reify:@types/node: timing reifyNode:node_modules/global-agent Completed in 200ms的解决办法

1.2 创建 BrowserWindow

使用nodemon监控文件变化

每次这个main.js有修改的时候呢,都要这个关闭现在的命令呢,再重启一遍。应该经常会遇到这个问题,所以呢,需要下一个辅助工具来完成,让我们这个工作更轻松一些。就可以使用这个 nodemon 。它可以监控文件的这个变化,然后自动的运行这个命令,这样就省去我们手动的操作。不再需要关闭,再重启可以说是非常方便啊

1
nodemon --watch main.js --exec \"electron\"

写一个父子窗口

1.3 进程中的通信

安装Devtron插件

这个插件已经停止更新,不推荐使用

1.4 使用IPC进行通信

使用Electron的ipc模块在不同进程间进行通信。首先,安装了devtron插件,然后在主进程和渲染进程中使用ipc模块进行通信。具体来说,当点击按钮时,将事件发送到主进程,主进程再回话,渲染进程将消息输出到web界面上。在渲染进程中使用ipc renderer,通过send方法将事件发送到主进程。在主进程中使用ipc main监听事件,通过on方法添加事件监听器。当接收到来自渲染进程的消息时,主进程会打印出消息内容。IPC是Inter-Process Communication的缩写,用于在不同进程之间进行数据交换和事件传递。

1.5 使用remote实现跨进程访问

除了通过IPC发送事件的方式,渲染进程中有一个名为remote的模块,可以提供一种更简单的方法完成这个任务。在代码中,可以通过require.electron来使用remote模块,取出主进程中的特定API进行使用。例如,在渲染进程中,可以通过remote模块取出main process特有的browser window API,实现新建窗口的操作。此外,Electron框架允许开发者使用HTML、CSS和JavaScript等Web技术来构建跨平台的桌面应用程序。

2.1 npx是什么

优点:

  • 1.避免安装全局模块
  • 2.调用项目内部安装的模块

3.1 需求分析

3.2 将ui拆分成组件

4.1 配置开发环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const { app, BrowserWindow } = require('electron');

let mainWindow;

app.on('ready', async () => {
// 动态导入 electron-is-dev
const isDev = await import('electron-is-dev');

mainWindow = new BrowserWindow({
width: 1024,
height: 680,
webPreferences: {
nodeIntegration: true,
}
});

const urlLocation = isDev.default ? 'http://localhost:3000' : 'dummy';
mainWindow.loadURL(urlLocation);
});

为什么需要动态 import

由于 electron-is-dev 是一个 ES 模块,它不能使用 CommonJS 的 require 语句。要在 CommonJS 模块中导入 ES 模块,必须使用动态 import,因为它可以在运行时导入模块,并且支持异步操作。

关键点:

  1. 异步函数: createWindow 被定义为异步函数,以便可以使用 await
  2. 动态导入: 使用 await import('electron-is-dev') 导入 ES 模块。
  3. 模块的 default 属性: 动态导入返回一个模块对象,其中 default 属性是模块的默认导出。

通过这种方式,可以在 CommonJS 环境中使用 ESM 模块,同时保持代码的同步执行逻辑。这解决了兼容性问题,并且不需要将整个项目转换为 ESM。

4.2 配置开发环境

安装concurrently

安装wait-on

安装cross-env

4.3 选择样式库 bootstrap

4-4 选择图标库 fontawesome

4-5 第一个组件 FileSearch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import React, {useState, useEffect, useRef} from 'react'
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import {faSearch,faTimes} from '@fortawesome/free-solid-svg-icons'

const FileSearch = ({ title, onFileSearch }) => {
const [inputActive, setInputActive] = useState(false)
const [value, setValue] = useState('')
let node = useRef(null)
const closeSearch =(e) =>{
e.preventDefault()
setInputActive(false)
setValue('')
}
useEffect(() => {
const handleInputEvent = (event) =>{
const {keyCode} = event
if (keyCode === 13 && inputActive) {
onFileSearch(value)
}else if(keyCode === 27 && inputActive) {
closeSearch(event)
}
}
document.addEventListener('keyup', handleInputEvent)
return () =>{
document.removeEventListener('keyup', handleInputEvent)
}
});
useEffect(()=>{
if (inputActive) {
node.current.focus()
}
},[inputActive])
return (
<div className="alert alert-primary d-flex justify-content-between align-items-center">
{
!inputActive &&
<>
<span>{title}</span>
<button
type="button"
className="icon-button"
onClick={() => setInputActive(true)}
>
<FontAwesomeIcon title="搜索" size="lg" icon={faSearch} />
</button>
</>}
{
inputActive &&
<>
<input
className="form-control"
value={value}
ref={node}
onChange={e => setValue(e.target.value)}
/>
<button
type="button"
className="icon-button"
onClick={closeSearch}
>
<FontAwesomeIcon title="关闭" size="lg" icon={faTimes} />
</button>
</>
}
</div>
)
}
export default FileSearch

4.6 使用PropTypes检查属性类型

  • FileSearch.propTypes 使用 PropTypes 进行类型检查和属性验证。它指定了 title 必须是一个字符串,onFileSearch 必须是一个函数,并且是必需的(即,如果没有传入 onFileSearch,会在开发模式下产生警告)。
  • FileSearch.defaultProps 设置了 FileSearch 组件在未接收到 title 属性时的默认值,这里设置为 '我的云文档'

这种方式有助于确保组件在使用时,能够获得正确的属性类型,并提供了默认值以防止未定义属性值的错误。

4.7 第一个自定义Hook 键盘事件useKeyPress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { useState, useEffect} from "react";
const useKeyPress = (targetKeyCode) => {
const [keyPressed,setKeyPressed] = useState(false);

const keyDownHandler = ({keyCode}) => {
if(keyCode ===targetKeyCode){
setKeyPressed(true);
}
}
const keyUpHandler = ({keyCode}) => {
if(keyCode === targetKeyCode){
setKeyPressed(false);
}
}

useEffect(()=>{
document.addEventListener('keydown', keyDownHandler);
document.addEventListener('keyup', keyUpHandler);
return ()=>{
document.removeEventListener('keydown', keyDownHandler);
document.removeEventListener('keyup', keyUpHandler);
}
},[])

return keyPressed
}

export default useKeyPress;

4.8 第二个组件 FileList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import React, {useState, useEffect} from 'react'
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import {faEdit, faTimes, faTrash} from '@fortawesome/free-solid-svg-icons'
import {faMarkdown} from '@fortawesome/free-brands-svg-icons'
import PropTypes from "prop-types";
import useKeyPress from "../hooks/useKeyPress";

const FileList = ({ files,onFileClick,onSaveEdit,onFileDelete}) => {
const [editStatus, setEditStatus] = useState(false);
const [value, setValue] = useState('');
const enterPressed = useKeyPress(13)
const escPressed = useKeyPress(27)
const closeSearch =() =>{
setEditStatus(false)
setValue('')
}

useEffect(() => {
if(enterPressed && editStatus ){
const editItem = files.find(item => item.id === editStatus)
onSaveEdit(editItem.id,value)
setEditStatus(false);
setValue('')
}
if(escPressed && editStatus ){
closeSearch()
}
// const handleInputEvent = (event) =>{
// const {keyCode} = event
// if (keyCode === 13 && editStatus) {
//
// }else if(keyCode === 27 && editStatus) {
// closeSearch(event)
// }
// }
// document.addEventListener('keyup', handleInputEvent)
// return () =>{
// document.removeEventListener('keyup', handleInputEvent)
// }
});
return(
<ul className="list-group list-group-flush file-list">
{
files.map(file => (
<li className="list-group-item bg-light row d-flex align-items-center file-item"
key={file.id}>
{
(file.id !== editStatus) &&
<>
<span className="col-2">
<FontAwesomeIcon size="lg" icon={faMarkdown}/>
</span>
<span className="col-8 c-link"
onClick={()=>onFileClick(file.id)}>{file.title}
</span>
<button
type="button"
className="icon-button col-1"
onClick={() => {setEditStatus(file.id);setValue(file.title)}}>
<FontAwesomeIcon title="编辑" size="lg" icon={faEdit}/>
</button>
<button
type="button"
className="icon-button col-1"
onClick={() => onFileDelete(file.id)}>
<FontAwesomeIcon title="删除" size="lg" icon={faTrash}/>
</button>
</>
}
{
(file.id === editStatus) &&
<>
<input
className="form-control col-10"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button
type="button"
className="icon-button col-2"
onClick={closeSearch}
>
<FontAwesomeIcon title="关闭" size="lg" icon={faTimes}/>
</button>
</>
}
</li>
))
}
</ul>
)
}

FileList.propTypes = {
files: PropTypes.array,
onFileClick: PropTypes.func,
onFileDelete: PropTypes.func,
onSaveEdit: PropTypes.func,
}

export default FileList;

5.1 第三个组件 TabList

需求分析

一种思路:

将状态作为属性传入给组件

另一种思路:

将状态定义在文件属性上,传入文件给组件

5.2 第三个组件 TabList 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import React from 'react'
import PropTypes from "prop-types";
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import {faTimes} from '@fortawesome/free-solid-svg-icons'
import classNames from "classnames";
import './TabList.scss'

const TabList = ({ files,activeId,unSaveIds,onTabClick,onCloseTab}) => {
return (
<ul className="nav nav-pills tablist-component">
{files.map(file => {
const withUnsavedMark = unSaveIds.includes(file.id)
const fClassName = classNames({
'nav-link':true,
'active': file.id === activeId,
'withUnsaved': withUnsavedMark
})
return(
<li className="nav-item" key={file.id}>
<a href="#"
className={fClassName}
onClick={(e) => {
e.preventDefault();
onTabClick(file.id)
}}>
{file.title}
<span
className="ml-2 close-icon" onClick={(e)=>{e.stopPropagation();onCloseTab(file.id)}}>
<FontAwesomeIcon icon={faTimes}/>
</span>
{withUnsavedMark && <span className="rounded-circle unsaved-icon ml-2"></span>}
</a>
</li>
)
})}
</ul>
)
}


TabList.propTypes = {
files: PropTypes.array,
activeId: PropTypes.string,
unSaveIds: PropTypes.array,
onTabClick: PropTypes.func,
onCloseTab: PropTypes.func,
}

TabList.defaultProps = {
unSaveIds: [],
}
export default TabList

中间遇到了bootstrap版本问题,教程使用的是b4版本,而我安装的是b5版本,所以重新安装b4,样式问题得到解决。

5.3 Markdown编辑器的基本需求

引入easyMDE编辑器

6.1 分析设计State结构

6.2 分析应用数据流

6.3