手摸手,带你用Electron+React开发一款桌面应用(2)

1,318 阅读6分钟

我正在参加跨端技术专题征文活动,详情查看:juejin.cn/post/710123…

接上回

大家好,我是爱吃鱼的桶哥,上篇文章我们搭建了一个基础的图书搜索界面,这次我们来讲一下图书下载相关的内容,后续我会讲一下图书搜索中具体是数据是怎么获取的,大家有兴趣的可以持续关注我这一系列的文章。

首先我们还是看一下下载页面的具体截图,如下所示:

image.png

基本的样子其实就是添加了一个下载进度,左侧是图书的文件名以及文件大小,右侧是当前的下载进度,当下载完成后,下载进度会变成查看文件的按钮,如下图所示:

image.png

点击它可以看到当前文件的具体目录,如下所示:

image.png

到这里,这期要分享的内容就这么多了,下面咱们开始具体的代码编写。

下载界面

在之前的文章中,我们在pages目录下新建了一个Download文件夹,其中包含了一个index.tsx文件和index.scss文件。首先我们将基本的布局写出来,具体代码如下:

import { FC, useEffect, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Button, Tabs } from 'antd';
import './indes.scss';

const { TabPane } = Tabs;

const Download: FC = () => {
    return (
        <Tabs defaultActiveKey="1">
            <TabPane tab="正在下载" key="1">
                <div className="download-ing">
                    {books.length ? (books || []).map(book => (
                        <div className='download-item' key={book.id}>
                            <div className="progress">
                                <div className="progress-item" style={{ width: book.progress }} />
                            </div>
                            <div className="download-status">
                                <div className="status-left">
                                    <p className='name'>{book.filename}</p>
                                    <p className="size">{book.filesize}</p>
                                </div>
                                <div className="status-right">
                                    {book.done ? <Button type='link'>查看文件</Button> : book.progress}
                                </div>
                            </div>
                        </div>
                    )) : '暂无下载任务'}
                </div>
            </TabPane>
            <TabPane tab="下载完成" key="2">
                Content of Tab Pane 2
            </TabPane>
        </Tabs>
    )
}

我们使用antdTabs组件做一个tab切换,左侧的tab页是当前正在下载的内容,右侧的tab页是已经下载完毕的内容,这样可以让我们清楚的知道当前下载中和已下载完毕的图书,方便我们更好的操作。具体的样式可以查看这里

基本的样式已经有了,那我们怎么知道用户是下载的哪本书呢?

还记得我们从搜索页面跳转到下载页面的时候带了什么参数吗?具体可以看一下之前写的跳转逻辑:

// Search/components/Modal/index.tsx

import { useNavigate } from "react-router-dom";

...other code

const navigate = useNavigate();

useEffect(() => {
    // 获取下载信息
    window.ipcRenderer.on('downloadInfo', (event, args) => {
        console.log('args', args);
        if (args?.code === 200) {
            handleCancel();
            // 跳转页面
            navigate('/download', {
                state: { ...args, bookId: currentBookId.current },
                replace: true
            });
        } else {
            message.error(args?.message);
        }
        setShadow(false);
    });
}, []);

...other code

当我们点击下载某本图书时,我们的渲染进程,也就是Search组件中的Modal组件会给主进程发送获取图书信息的通知,具体代码如下:

// Search/components/Modal/index.tsx

// 点击下载对应格式的图书,需要获取到图书的链接
const handleDownload = useCallback((item: Record<string, any>) => {
    // console.log('item', item);
    const url = getDownloadUrl(item.link);
    const bookId = url[0].indexOf('/f/') > -1 ? url[0].split('/f/')[1] : url[0].split('/file/')[1];
    console.log('url', url, bookId);
    // setGoPage(true);
    currentBookId.current = bookId;
    setShadow(true);
    // // 解析当前图书的下载链接,获取真实的下载地址,并进行下载
    window.ipcRenderer.send('parseBook', {
      bookPass: url[1] || '',
      bookId,
    });
}, []);

这个点击事件中,我们向主进程发送通知,把当前的图书地址和图书id发送给主进程,主进程接收到通知后,会进行相关的解析,具体的代码后续会进行展示。当解析完成,获取到真实的图书下载路径后,我们会给渲染进程发通知,也就是上述useEffect勾子中监听到主进程发来的通知downloadInfo

当我们监听到主进程的downloadInfo通知后,我们就可以执行页面的跳转,通过react-router-dom上面的useNavigate自定义hook方法来进行页面的跳转,并将我们获取到的真实图书下载路径传给download页面,这样当页面跳转到download页面后,会自动开始进行下载,download页面的下载逻辑如下:

//Download/index.tsx

...other code

const Download = () => {
    const location: any = useLocation();
    const [books, setBooks] = useState<DownloadBookProps[]>([]);
    const bookList = useRef<DownloadBookProps[]>([]);
    
    useEffect(() => {
        // 监听页面跳转获取到的数据,给主进程发起下载通知
        if (location?.state && location?.state?.code === 200) {
            // 下载文件
            window.ipcRenderer.send('downloadBookFile', location?.state);
        }
    }, [location?.state]);
    
    useEffect(() => {
        // 监听下载进度
        window.ipcRenderer.on('downloadProgress', (event, args: any) => {
            let allBook: any = [].concat(books as any);
            const filter = allBook.filter((book: DownloadBookProps) => book.id !== args.bookId);
            if (!filter.length) {
                allBook.push({
                    id: args.bookId,
                    progress: args.progress,
                    filename: args.filename,
                    filesize: args.filesize,
                    done: false,
                });
            }
            bookList.current = allBook;
            setBooks(allBook);
        });

        return () => {
            window.ipcRenderer.removeAllListeners('downloadProgress');
        };
    }, []);

    useEffect(() => {
        // 监听文件是否下载完毕
        window.ipcRenderer.on('downloadDone', (event, args) => {
            let allBook: any = [].concat(bookList.current as any);
            allBook.map((book: any) => {
                if (book.id === args.bookId) {
                    book.done = true;
                    book.dirPath = args.path;
                }
                return book;
            });
            setBooks(allBook);
        });

        return () => {
            window.ipcRenderer.removeAllListeners('downloadDone');
        };
    }, []);
    
    // 查看文件位置
    const hanldeViewFile = (path: string) => {
        console.log('文件地址', path);
        window.ipcRenderer.send('openFilePath', path);
    };

...other code
}

上述内容就是渲染进程的相关逻辑了,但是有了这些还远远不够,因为我们的图书下载是需要主进程来帮我们完成的,上述代码中,我们给主进程发了很多通知,让主进程帮我们去处理相关的业务逻辑,接下来我们就写一下主进程相关的代码,让我们的下载页面能够真正的跑起来。

主进程

在上述代码中,我们给主进程发送了downloadBookFile这个通知,通知主进程可以开始下载图书了,并且通过监听主进程,告诉渲染进程当前的下载进度,以及是否下载完毕了,下面我们一起看一下主进程的相关代码:

// packages/main/event.ts

...other code

const fse = require('fs-extra');
const dl = require('download-file-with-progressbar');

// 图书下载
ipcMain.on('downloadBookFile', async (event, arg) => {
    // console.log('arg', arg);
    const { link: url, bookId, name: filename, size: filesize } = arg;
    const currentDay = dayjs().format('YYYY-MM-DD');
    const dirPath = path.join(__dirname, `../../download/`);
    const filePath = path.join(dirPath, currentDay);
    fse.ensureDirSync(filePath);

    // 下载配置项
    const option = {
        filename,
        dir: filePath,
        onDone: () => {
            // console.log('done', info);
            event.sender.send('downloadDone', {
                bookId,
                done: true,
                path: filePath
            });
        },
        onError: (err: any) => {
            console.log('error', err);
        },
        onProgress: (curr: number, total: number) => {
            // console.log('progress', ((curr / total) * 100).toFixed(2) + '%');
            const progress = ((curr / total) * 100).toFixed(2) + '%';
            event.sender.send('downloadProgress', {
                bookId,
                progress,
                filename,
                filesize,
                dirPath,
            });
        },
    };
    dl(url, option);
});

...other code

在上述代码中,主进程接受到渲染进程发来的downloadBookFile通知后,获取到发送过来的参数后,我们拿到下载的具体路径,然后将下载的文件放在download文件夹中,并且根据当天的日期自动再生成一个已当前日期为目录的文件夹,这样方便我们后续的文件查找。

准备工作完成后,就开始下载了,这里我们使用的是download-file-with-progressbar这个库,具体的使用方法大家可以在npm上去搜一下,我们只需要按照它提供的方法进行下载即可,在onProgress方法中能够监听到当前文件的下载进度,我们将下载进度发送给渲染进程,这样用户就能知道当前图书下载的进度了。

最后当图书下载完成后,我们下载进度会变成查看文件按钮,点击它给主进程发通知,打开当前文件夹,具体代码如下:

// packages/main/event.ts

import { ipcMain, shell } from 'electron';

...other code

// 打开文件所在位置
ipcMain.on('openFilePath', async (event, args) => {
    shell.openPath(args);
});

写到这里,下载的业务逻辑也基本差不多了,具体的代码大家可以到我的GitHub上进行查看。

最后

基础的搜索和下载已经讲的差不多了,下一期我们一起来看一下如何将图书发送到Kindle中,但是我们需要注意的是,kindle只支持mobiazw3格式,因此下载图书的时候要看清楚格式进行下载。

好了,本期的分享内容就到这里,如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者,谢谢大家

本项目的代码仓库:electron-vite-react