探索 Electron:助力文档操作应用快速落地

Electron是一个开源的桌面应用程序开发框架,它允许开发者使用Web技术(如 HTML、CSS 和 JavaScript)构建跨平台的桌面应用程序,它的出现极大地简化了桌面应用程序的开发流程,让更多的开发者能够利用已有的 Web 开发技能来构建功能强大且跨平台的应用程序,这对于提升开发效率和应用程序的快速交付具有重要意义。

初始化项目

安装框架:今天博主这里用到electron-vite构建器开发桌面端应用,对项目进行一个初始化,这里我们用到该构建器中的react框架开始今天项目的书写,如果想了解vue框架搭建的项目,参考我之前的文章:地址 ,废话不多说直接开始我们今天的项目讲解:

终端执行如下命令安装electron项目:

bash 复制代码
npm create @quick-start/electron@latest

这里输入完项目的名称之后,选择今天要讲解的react模板即可:

根据需求选择是否使用TS,博主写项目一般都选择TS,这里也就选择TS吧:

是否添加electron更新的插件,当然必须选上:

是否下载electron的镜像代理,这里也选上吧:

配置完成之后,切换到对应项目目录,终端执行 npm i 安装好依赖,终端执行 npm run dev,可以看到我们的项目已经跑起来了,初识页面看着也是非常的简约大气,项目也是给我们默认配置好了相关的插件便于代码的书写:

配置UI框架:因为这里我们使用了react框架搭建项目,所以这里的UI组件库的话还是采用常用的antd进行样式的搭建吧,终端执行如下命令进行安装:

javascript 复制代码
npm install antd --save

然后随便引入一个按钮,可以看到我们的组件库已经引入成功了:

配置styled-components:因为这里我们使用了react框架来编写electron项目,所以这里我们使用了styled-components样式库来编写内容样式,详情请参考我之前的文章:地址

安装图标库:这里我们使用一个大家场景的图标库Font Awesome,该图标库内容还是比较丰富全面的,并且支持vue和react框架其地址为:地址 ,因为本项目采用react框架,所以这里就以react安装为例吧,终端执行如下命令进行安装:

javascript 复制代码
# 以下三个库都需要进行安装
npm i --save @fortawesome/fontawesome-svg-core # 图标库核心文件
npm i --save @fortawesome/react-fontawesome@latest # react风格图标
npm i --save @fortawesome/free-solid-svg-icons # solid类型的字体库

安装完图标库之后,直接在相应的文件中引入对应的图标即可,示例代码如下:

javascript 复制代码
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开始相应的样式,当然这里我们也写了一下全局样式,清除一下框架默认的样式,代码如下(条理清晰,一目了然):

javascript 复制代码
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退出输入框的模式,具体代码如下所示:

javascript 复制代码
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函数:

javascript 复制代码
// 自定义监听键盘事件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代码:

最终得到的效果如下所示:

然后如法炮制,在左侧的底部下面再放置两个按钮,新建和导入,如下:

javascript 复制代码
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组件,然后传入相关数据,以及激活标签页,未保存、点击和关闭回调:

然后开始搭建静态页面,如下所示:

javascript 复制代码
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编辑器插件,其官方网址为:地址 ,如下所示:

终端执行如下命令进行安装:

javascript 复制代码
npm install --save react-simplemde-editor easymde

安装完成之后,通过一段简单的示例代码进行演示:

javascript 复制代码
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来判断:

javascript 复制代码
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);

这里根据判断参数渲染内容:

javascript 复制代码
{ 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,代码如下:

javascript 复制代码
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标签页的时候,切换激活状态,以及点击标签页中的关闭图标进行一个关闭操作:

javascript 复制代码
// 依据关键字搜索文件
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中:

javascript 复制代码
// 文件内容更新
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标签内容即可:

javascript 复制代码
// 删除某个文件项
const deleteItem = (id: string) => {
    const newFiles = files.filter(item => item.id !== id);
    setFiles(newFiles);
    // 删除后,关闭可能正在打开的tab
    closeFile(id);
}

重命名文件:重命名文件的话,直接把输入框中输入的内容拿过来,然后计算当前要修改的文件把里面的名称title进行一个替换即可:

javascript 复制代码
// 重命名文件名称
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进行生成唯一标识,终端执行如下命令进行安装:

javascript 复制代码
npm install uuid
javascript 复制代码
// 新建文件
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]);
    }
}

最终呈现的效果如下所示:

相关推荐
neter.asia3 分钟前
vue中如何关闭eslint检测?
前端·javascript·vue.js
~甲壳虫4 分钟前
说说webpack中常见的Plugin?解决了什么问题?
前端·webpack·node.js
天行健PLUS19 分钟前
【经验分享】六西格玛管理培训适合哪些人参加?
经验分享
光影少年23 分钟前
vue2与vue3的全局通信插件,如何实现自定义的插件
前端·javascript·vue.js
As977_24 分钟前
前端学习Day12 CSS盒子的定位(相对定位篇“附练习”)
前端·css·学习
susu108301891126 分钟前
vue3 css的样式如果background没有,如何覆盖有background的样式
前端·css
Ocean☾28 分钟前
前端基础-html-注册界面
前端·算法·html
Rattenking28 分钟前
React 源码学习01 ---- React.Children.map 的实现与应用
javascript·学习·react.js
Dragon Wu30 分钟前
前端 Canvas 绘画 总结
前端
CodeToGym34 分钟前
Webpack性能优化指南:从构建到部署的全方位策略
前端·webpack·性能优化