Electron是一个开源的桌面应用程序开发框架,它允许开发者使用Web技术(如 HTML、CSS 和 JavaScript)构建跨平台的桌面应用程序,它的出现极大地简化了桌面应用程序的开发流程,让更多的开发者能够利用已有的 Web 开发技能来构建功能强大且跨平台的应用程序,这对于提升开发效率和应用程序的快速交付具有重要意义。
初始化项目
安装框架:今天博主这里用到electron-vite构建器开发桌面端应用,对项目进行一个初始化,这里我们用到该构建器中的react框架开始今天项目的书写,如果想了解vue框架搭建的项目,参考我之前的文章:地址 ,废话不多说直接开始我们今天的项目讲解:
终端执行如下命令安装electron项目:
npm create @quick-start/electron@latest
这里输入完项目的名称之后,选择今天要讲解的react模板即可:
根据需求选择是否使用TS,博主写项目一般都选择TS,这里也就选择TS吧:
是否添加electron更新的插件,当然必须选上:
是否下载electron的镜像代理,这里也选上吧:
配置完成之后,切换到对应项目目录,终端执行 npm i 安装好依赖,终端执行 npm run dev,可以看到我们的项目已经跑起来了,初识页面看着也是非常的简约大气,项目也是给我们默认配置好了相关的插件便于代码的书写:
配置UI框架:因为这里我们使用了react框架搭建项目,所以这里的UI组件库的话还是采用常用的antd进行样式的搭建吧,终端执行如下命令进行安装:
npm install antd --save
然后随便引入一个按钮,可以看到我们的组件库已经引入成功了:
配置styled-components:因为这里我们使用了react框架来编写electron项目,所以这里我们使用了styled-components样式库来编写内容样式,详情请参考我之前的文章:地址 。
安装图标库:这里我们使用一个大家场景的图标库Font Awesome,该图标库内容还是比较丰富全面的,并且支持vue和react框架其地址为:地址 ,因为本项目采用react框架,所以这里就以react安装为例吧,终端执行如下命令进行安装:
# 以下三个库都需要进行安装
npm i --save @fortawesome/fontawesome-svg-core # 图标库核心文件
npm i --save @fortawesome/react-fontawesome@latest # react风格图标
npm i --save @fortawesome/free-solid-svg-icons # solid类型的字体库
安装完图标库之后,直接在相应的文件中引入对应的图标即可,示例代码如下:
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTimes, faSearch } from '@fortawesome/free-solid-svg-icons'
// 引入对应图标
<FontAwesomeIcon icon={faSearch} /> // 搜索图标
<FontAwesomeIcon icon={faTimes} /> // 关闭图标
左侧菜单栏
在项目初始化完毕 之后,接下来我们开始我们开始对页面开始编写相应的内容了,为了固定我们的界面大小,这里我们先在主进程中把界面的大小固定住,禁止用户缩放,如下:
然后我们在根组件App.tsx中编写相应的内容模块,这里我们分两部分,左侧是菜单栏,右侧是展示的内容区域,这里我们借助样式组件styled-components开始相应的样式,当然这里我们也写了一下全局样式,清除一下框架默认的样式,代码如下(条理清晰,一目了然):
import styled, { createGlobalStyle } from "styled-components";
// 导入公共组件
import SearchFile from "./components/SearchFile";
const App = () => {
return (
<>
<GlobalStyle />
<Container>
<LeftDiv>
<SearchFile title={'我的文档'} onSearch={(value: string)=> {
console.log(value)
}}></SearchFile>
</LeftDiv>
<RightDiv>右侧</RightDiv>
</Container>
</>
);
}
export default App;
// 设置全局样式
const GlobalStyle = createGlobalStyle`
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
`
// 样式组件
const Container = styled.div` // 初始容器
width: 100%;
height: 100vh;
display: flex;
`;
const LeftDiv = styled.div` // 左边容器
width: 30%;
height: 100%;
background-color: #008c8c;
`
const RightDiv = styled.div` // 右边容器
width: 70%;
height: 100%;
background-color: #fff;
`
接下来我们开始编写引入的公共组件SearchFile中的内容,这里我们编写了一个逻辑,默认是文字内容,然后用户点击我们设置的搜索的图标样式之后,则变成输入框,用户可以通过回车传递输入的数据给父组件,然后点击esc退出输入框的模式,具体代码如下所示:
import { useState, useEffect, useRef } from 'react'
import styled from 'styled-components'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTimes, faSearch } from '@fortawesome/free-solid-svg-icons'
const SearchFile = ({ title, onSearch }: { title: string; onSearch: (value: string) => void }) => {
const [ searchActive, setSearchActive ] = useState<boolean>(false)
const [ value, setValue ] = useState<string>('')
// 获取输入框实例
const inputRef = useRef<HTMLInputElement>(null)
// 关闭搜索框
const closeSearch = () => {
setSearchActive(false)
setValue('')
}
// 监听键盘事件
useEffect(() => {
const ListenKeyWord = (e: any) => {
const { keyCode } = e
if ( keyCode === 13 && searchActive ) {
onSearch(value)
}
if ( keyCode === 27 && searchActive ) {
closeSearch()
}
}
document.addEventListener('keyup', ListenKeyWord)
// 组件加载时获取焦点
return () => { // 组件卸载时移除监听事件
document.removeEventListener('keyup', ListenKeyWord)
}
}, [searchActive, value, onSearch])
// 实现输入框聚焦
useEffect(() => {
if (searchActive && inputRef.current) {
inputRef.current.focus()
}
}, [searchActive])
return (
<>
{/* 默认文字显示 */}
{ !searchActive && (
<SearchDiv>
<Span>{ title }</Span>
<Span onClick={()=> { setSearchActive(true) }}>
<FontAwesomeIcon icon={faSearch} />
</Span>
</SearchDiv>
)}
{/* 点击文字则显示搜索框 */}
{ searchActive && (
<SearchDiv>
<Input value={value} ref={inputRef} onChange={(e)=> {
setValue(e.target.value)
}} />
<Span onClick={closeSearch}>
<FontAwesomeIcon icon={faTimes} />
</Span>
</SearchDiv>
)}
</>
)
}
export default SearchFile
// 样式组件
const SearchDiv = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #fff;
`
const Span = styled.span`
color: #fff;
padding: 5px 15px;
font: normal 16px/40px '微软雅黑';
cursor: pointer;
user-select: none;
`
const Input = styled.input.attrs({
type: 'text',
placeholder: '搜索文件'
})`
width: 100%;
height: 25px;
border: none;
outline: none;
border-radius: 1px;
margin-left: 10px;
`
最终呈现的效果如下所示:
接下来开始对左侧的文件菜单栏样式做一个调整,这里我们将其也抽离出一个公共组件,因为文件名称也是要进行输入框来修改,也是要借助监听键盘事件,这里我们将其抽离出一个hooks函数:
// 自定义监听键盘事件hook函数
import { useState, useEffect } from 'react';
export const useKeyHandler = (code: number) => {
const [ keyPressed, setKeyPressed ] = useState<boolean>(false);
const keyDownHandler = (e: any) => { if (e.keyCode == code) setKeyPressed(true) }; // 按下键盘
const keyUpHandler = (e: any) => { if (e.keyCode == code) setKeyPressed(false) } // 抬起键盘
useEffect(() => {
document.addEventListener('keydown', keyDownHandler);
document.addEventListener('keyup', keyUpHandler);
return () => {
document.removeEventListener('keydown', keyDownHandler);
document.removeEventListener('keyup', keyUpHandler);
}
});
return keyPressed;
}
然后接下来我们开始创建文件列表菜单名称,这里我们在根组件中把数据通过props传递给文件列表组件,组件拿到数据后,根据情况进行渲染:
组件拿到数据之后开始对页面进行一个渲染,这里就不再一一赘述了,都是正常的vue代码:
最终得到的效果如下所示:
然后如法炮制,在左侧的底部下面再放置两个按钮,新建和导入,如下:
import styled from 'styled-components'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
interface ButtonItemProps {
title: string,
btnClick?: () => void,
icon: any
}
const ButtonItem = ({ title, btnClick, icon }: ButtonItemProps) => {
return (
<BtnDiv>
<FontAwesomeIcon style={{ marginRight: '10px' }} icon={icon} />
{ title }
</BtnDiv>
)
}
export default ButtonItem
// 样式组件
const BtnDiv = styled.div`
width: 100%;
display: flex;
align-items: center;
justify-content: center;
height: 40px;
cursor: pointer;
font-size: 18px;
user-select: none;
&:hover {
background-color: #15ad7a;
border-radius: 5px 5px 5px 5px;
}
&:active {
background-color: #00fc17;
}
`
最终呈现的效果如下所示:
右侧主内容
接下来开始编写右侧内容区域,首先我们先完成顶部tab标签页的静态内容,这里我们在App根组件中设置TabList组件,然后传入相关数据,以及激活标签页,未保存、点击和关闭回调:
然后开始搭建静态页面,如下所示:
import styled from 'styled-components'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faTimes, faCircle } from '@fortawesome/free-solid-svg-icons'
interface TabListProps {
files: any[]
activeItem: string
unSaveItems: any[]
clickItem: (id: string) => void
closeItem: (id: string) => void
}
const TabList = ({ files, activeItem, unSaveItems, clickItem, closeItem }: TabListProps) => {
return (
<TabDiv>
{ files.map(item => {
let isUnSave = unSaveItems.includes(item.id)
return (
<TabItem
$isActive={activeItem === item.id}
key={item.id}
onClick={(e: any) => { e.preventDefault(); clickItem(item.id) }}
>
<TabItemName>{ item.title }</TabItemName>
<TabItemIcon onClick={(e: any) => { e.stopPropagation(); closeItem(item.id) }}>
<FontAwesomeIcon className='close' icon={faTimes} />
</TabItemIcon>
{ isUnSave && (
<FontAwesomeIcon className='circle' icon={faCircle} />
) }
</TabItem>
)
}) }
</TabDiv>
)
}
最终呈现的效果如下所示:
接下来开始编写右侧主内容中下侧的编辑器内容,这里我们借助一个开源的简易的react编辑器插件,其官方网址为:地址 ,如下所示:
终端执行如下命令进行安装:
npm install --save react-simplemde-editor easymde
安装完成之后,通过一段简单的示例代码进行演示:
import SimpleMDE from "react-simplemde-editor";
import "easymde/dist/easymde.min.css";
<SimpleMDE
value={initFiles[1].body}
onChange={(value: string) => console.log("编辑", value)}
options={{
autofocus: true, // 自动获得焦点
spellChecker: false, // 拼写检查
status: true, // 状态栏
minHeight: "470px", // 最小高度
}}
/>
最终呈现的效果如下所示,可以看到我们的编辑器已经被成功渲染出来了:
这里我们默认都是把上方的tab标签页都给写死了,这里我们先把写死的内容参数去掉,判断如果用户没有点击文件的话,默认右侧的内容区域是不显示内容的,这里我们给一个提示,根据是否有激活的tabid来判断:
const [files, setFiles] = useState<filesTypes[]>(initFiles); // 文件列表
const [activeId, setActiveId] = useState<string>(""); // 当前激活的tab
const [openIds, setOpenIds] = useState<string[]>([]); // 打开的tab
const [unSaveIds, setUnSaveIds] = useState<string[]>([]); // 未保存的tab
// 计算已打开的所有文件信息
const getOpenFiles = openIds.map((id) => {
return files.find((file) => file.id === id);
});
// 计算正在编辑的文件信息
const activeFile = files.find((file) => file.id === activeId);
这里根据判断参数渲染内容:
{ activeFile ? (
<>
<TabList
files={getOpenFiles}
activeItem={activeId}
unSaveItems={unSaveIds}
clickItem={(id: string)=>{ console.log("点击", id)}}
closeItem={(id: string)=>{ console.log("关闭", id)}}
/>
<SimpleMDE
value={activeFile.body}
onChange={(value: string) => console.log("编辑", value)}
options={{
autofocus: true, // 自动获得焦点
spellChecker: false, // 拼写检查
status: true, // 状态栏
minHeight: "470px", // 最小高度
}}
/>
</>
) : (
<AdvertisementDiv>
<AdvertisementImgs src={img} title="csdn博主 '亦世凡华、'" onClick={()=> {
window.open("https://blog.csdn.net/qq_53123067?spm=1000.2115.3001.5343")
}} />
<AdvertisementTitle>当前暂无数据<br/>(PS: 点击上方图片,求一波关注)</AdvertisementTitle>
</AdvertisementDiv>
) }
最终呈现的效果如下所示:
菜单栏操作
接下来对左侧菜单栏中的按钮进行一个交互操作了,主要分为以下几个方向:
搜索文件:点击搜索图标,在输入框输入相关内容,搜索栏下方的文件列表依据关键字进行呈现,这里我们再呈现设置一下左侧菜单栏的显示内容,如果有搜索出内容就显示搜索的内容,否则默认显示files,代码如下:
const [searchFiles, setSearchFiles] = useState<filesTypes[]>(); // 左侧展示搜索列表于默认列表进行区分
// 计算左侧列表需要展示什么样信息
const fileList = (searchFiles && searchFiles.length > 0) ? searchFiles : files;
// 依据关键字搜索文件
const searchFile = (keyword: string) => {
const newFiles = files.filter(item => item.title.includes(keyword));
setSearchFiles(newFiles);
}
tab标签页:接下来我们设置当点击左侧菜单栏中的文件,则打开右侧的tab标签页面,然后当点击tab标签页的时候,切换激活状态,以及点击标签页中的关闭图标进行一个关闭操作:
// 依据关键字搜索文件
const searchFile = (keyword: string) => {
const newFiles = files.filter(item => item.title.includes(keyword));
setSearchFiles(newFiles);
}
// 点击左侧文件显示编辑页面
const openItem = (id: string) => {
setActiveId(id); // 激活tab
// 判断是否已经打开
if (!openIds.includes(id)) {
setOpenIds([...openIds, id]);
}
};
// 点击某个tab选项时切换当前状态
const changeActive = (id: string) => setActiveId(id)
// 关闭某个tab
const closeFile = (id: string) => {
const retOpens = openIds.filter((item) => item !== id); // 过滤掉该tab
setOpenIds(retOpens); // 过滤掉该tab
if (retOpens.length > 0) {
setActiveId(retOpens[0]); // 激活第一个tab
} else {
setActiveId(""); // 如果没有tab了,则清空激活状态
}
};
文件内容更新:然后这里当我们对文件里面的body内容进行修改的时候,tab标签页是呈现修改的圆点状态,然后把修改的内容重新添加到当前修改的文件的body中:
// 文件内容更新
const changeFile = (id: string, value: string) => {
if (!unSaveIds.includes(id)) {
setUnSaveIds([...unSaveIds, id]); // 添加未保存的tab
}
// 某个内容更新后,更新文件列表
const newFiles = files.map((file) => {
if (id === file.id) {
return {...file, body: value}; // 更新文件内容
} else {
return file;
}
});
setFiles(newFiles); // 更新文件列表
};
删除文件:删除文件很简单,直接拿到当前要删除的文件id进行一个过滤,然后顺便关闭可能正在打开的tab标签内容即可:
// 删除某个文件项
const deleteItem = (id: string) => {
const newFiles = files.filter(item => item.id !== id);
setFiles(newFiles);
// 删除后,关闭可能正在打开的tab
closeFile(id);
}
重命名文件:重命名文件的话,直接把输入框中输入的内容拿过来,然后计算当前要修改的文件把里面的名称title进行一个替换即可:
// 重命名文件名称
const renameFile = (id: string, newTitle: string) => {
const newFiles = files.map((file) => {
if (id === file.id) {
return {...file, title: newTitle}; // 更新文件内容
} else {
return file;
}
});
setFiles(newFiles); // 更新文件列表
}
新建文件:新建文件的话需要对每个文件生成特定的id,所以这里我们使用uuid进行生成唯一标识,终端执行如下命令进行安装:
npm install uuid
// 新建文件
const createNewFile = () => {
const newId = uuidv4();
const newFile: any = {
id: newId,
title: "",
body: "## 初始化内容",
createTime: new Date().getTime(),
isNew: true
}
// 避免连续点击新建
if (!files.find((file) => file?.isNew)) {
setFiles([...files, newFile]);
}
}
最终呈现的效果如下所示: