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

6,178 阅读8分钟

本文已参与「新人创作礼活动」,一起开启掘金创作之路。

大家好,我是爱吃鱼的桶哥,自从上次发布了一篇基于Github Actions完成Electron自动打包、发布及更新后,我一直在加强对Electron的学习,而学习一门框架,最好的办法就是实战,因此我打算从零开发一款桌面端应用。

平时在工作或学习劳累后,我比较喜欢看看书,尤其是电子书,家里正好还有一个泡面盖子(Kindle),于是我就到GitHub上去搜kindle相关的软件,看看有没有提供相关图书搜索的,搜了一圈发现基本都是不维护了,或是搜索功能也使用不了的,基于这个设想,我准备开发一款能够搜书、一键发送到kindle的桌面端。软件的大致的截图如下:

image.png

默认打开软件是这个界面,用户可以在输入框内输入图书的名称,按回车或者搜索按钮后,如果有对应的数据,就能展示出来。具体如下:

image.png

当点击后面的选择下载格式时,会弹出这本书的封面信息和对应可下载的格式。如下:

image.png

然后我们可以点击想要下载的格式,这里我以epub格式做个示范,点击epub格式的标题,会自动跳转到下载页面进行下载,下载页会展示当前下载图书的进度,当图书下载完成后,会看到当前图书下载的具体位置信息,由于目前软件还在开发中,这期只是第一期,教大家如何完成基本的下载,因此目前样式还没有写完,大家先凑合看,具体如下:

image.png

好了,看到这里,大家对这个桌面端软件应该有一个基本的认识了,接下来我们就开始从0到1开发一款这样属于自己的图书下载软件吧。

首先我们选择的技术栈是ElectronReactTypescript,UI组件库用的是antd,毕竟我们没有专业的设计师为我们提供一个UI,因此用现成是比较好。

在之前的文章中我们选择的基础脚手架是Reactcreate-react-app,这一次我们换一个别人已经搭好的基础脚手架,这样很多基础的内容,类似主进程和子进程直接的通信,数据存储之类的基础功能就无需我们自己来写了,并且我还想尝试一下Vite,基于上述的想法,我在GitHub上面找到了一个脚手架electron-vite-react,它的使用方法也很简单,我们需要初始化一个项目,具体代码如下:

npm create electron-vite

当我们执行完上述命令后,会自动生成一个electron-vite的文件目录,进入到目录中,我们可以看一下文件的基本目录。

简单介绍一下文件目录,方便我们后续的开发。新建好的目录中,包含如下的结构:

├── build                     用于生产构建的资源
|   ├── icon.icns             应用图标(macOS)
|   ├── icon.ico              应用图标
|   ├── installerIcon.ico     安装图标
|   └── uninstallerIcon.ico   卸载图标
|
├── dist                      构建后,根据 packages 目录生成
|   ├── main
|   ├── preload
|   └── renderer
|
├── release                   在生产构建后生成,包含可执行文件
|   └── {version}
|       ├── win-unpacked      包含未打包的应用程序可执行文件
|       └── Setup.exe         应用程序的安装程序
|
├── scripts
|   ├── build.mjs             项目开发脚本 npm run build
|   └── watch.mjs             项目开发脚本 npm run dev
|
├── packages
|   ├── main                  主进程源码
|   |   └── vite.config.ts
|   ├── preload               预加载脚本源码
|   |   └── vite.config.ts
|   └── renderer              渲染进程源码
|       └── vite.config.ts

我们只需要关注的文件目录是packages,因为我们的项目代码全部在这个目录中,其中包括main文件夹,它是主进程渲染相关的代码;preload中主要是将主进程的能力注入到渲染进程中的一个辅助代码,包括主进程与渲染进程直接的通信,本地数据存储等;renderer目录中就是我们的渲染进程相关的代码,包括了基本的React项目的结构,具体的内容大家新建好这个目录后,可以自行查看,相关的代码都比较简单,这里就不做过多介绍了。

在我们的目录中,我一般会添加一个.npmrc这样的文件,里面的具体内容是registry=https://registry.npmmirror.com/,这样做的目的是为了将这个项目的npm源设置为taobao源,能够加快我们的项目安装,如下图:

image.png

我们打开控制台,执行npm i,即可初始化项目,等待初始化结束后,我们可以执行npm run dev,即可看到一个基本的桌面端应用。

image.png

上图中我们可以看到这个应用的基本模样,接下来我们构思一个我们自己的应用大概有哪些内容,有一个大致的需求我们才好开始做,而不是一上来就抓瞎,不知道该如何去写。

在一开始的时候,我给大家展示的图片中就包括四个部分,首先是搜索页面,因为有搜索页面用户才能获取到图书的资源,其次是下载页面,为什么要将搜索和下载分开呢?其实也是为了简单,并且我们要遵循一个原则,一个组件只干一件事,也就是说搜索只做搜索相关的内容,至于下载资源,就交给下载页面来做。然后是发送页面,这个页面主要是用于将图书发送到kindle中的。最后是设置页面,我们只有设置了发送邮件的信息和接受邮件的信息,才能完成图书的发送。

基于上述的内容,我们在packages/renderer/src目录下新建一个pages文件夹,并添加我们相同的文件。我们在pages目录下创建以下四个目录:

├── pages
|   ├── Search             搜索
|      ├── index.tsx             搜索内容页
|      ├── index.scss            样式
|   ├── Download           下载
|      ├── index.tsx             
|      ├── index.scss            
|   ├── Send               发送
|      ├── index.tsx             
|      ├── index.scss            
|   └── Setting            设置
|      ├── index.tsx             
|      ├── index.scss            

基本的目录结构搭建好后,我们需要安装一下react-router-domantd让我们的项目能够有一个基本的搜索页面的样子。

npm i react-router-dom antd -D

我们在main.tsx页面中引入antd的样式,方便我们在项目中使用。

由于我们的应用有多个页面,因此需要用到react-router-dom做路由的管理,在src目录下载,创建一个router目录,进入到router目录,创建一个index.tsx文件,添加相关的路由代码:

...other code

const router: RouterProps[] = [
  {
    path: '/',
    element: <LayoutPage />,
    children: [
      { path: '/search', title: '搜索', element: lazyLoad(<Search />), icon: <PieChartOutlined /> },
      { path: '/download', title: '下载', element: lazyLoad(<Download />), icon: <DesktopOutlined /> },
      { path: '/send', title: '发送', element: lazyLoad(<Send />), icon: <SendOutlined /> },
      { path: '/setting', title: '设置', element: lazyLoad(<Setting />), icon: <SettingOutlined /> },
      { path: '/', element: <Navigate to='/search' replace /> }
    ]
  }
];

路由的基本信息设置完成后,我们需要在main.tsx中添加一下路由的配置信息:

...other code

root.render(
  <StrictMode>
    <HashRouter>
      <App />
    </HashRouter>
  </StrictMode>
)

...other code

主要是将路由的模式设置为Hash模式,之所以要这样改,是因为我们打包后的文件的最终路径其实是xxx/index.html这样的,如果不用hash模式,我们就无法匹配到我们需要的页面。

接下来我们还需要修改一下App.tsx文件,它是我们的应用入口。我们需要删除之前的原始代码,然后修改为以下代码:

import { useRoutes } from "react-router-dom";
import router from "./router";
import styles from '@/styles/app.module.scss';

const App = () => {
  const element = useRoutes(router);
  return (
    <div className={styles.app}>
      <header className={styles.appHeader} />
      {element}
    </div>
  )
}

export default App

完成以上代码修改后,我们再次执行npm run dev,就能看到我们的应用已经修改为自己想要的样子了,并且左侧的目录也可以正常的点击进行切换。

image.png

接下来我们实现一下搜索页面的相关内容,目前还是空白的,不好看,搜索页面的具体代码如下:

...other code 

<Table 
    rowKey={'link'}
    columns={columns({ handleClickDownload })}
    size="middle"
    dataSource={dataSource}
    onChange={handleChangePage}
    loading={loading}
    pagination={{
        size: 'default',
        pageSize: 10,
        current: currentPage,
        total: totalPage,
        showTotal: (total: number) => `总共 ${total} 本书`,
        showSizeChanger: false,
        showQuickJumper: true,
    }}
/>

...other code

跑起来后是这样的:

image.png

现在基本的搜索样式已经有了,接下来我们需要完善一下搜索相关的代码。我在网络商搜索了很久,找到了一个电子书的下载网站,它里面有很多图书可以搜索,但是它的图书资源是不在它网站上的,而是放在了某网盘中,我们需要解析网盘的下载地址,获取到真正的图书路径,方便我们将图书资源下载到本地。

由于搜索图书资源其实就是去爬取别人网站的内容,因此我们将这部分代码放在主进程main中进行。

我们打开main文件夹,在main文件夹中创建一个新的文件search.ts,主要用于搜索相关的内容,在这个文件中,我们需要对指定的网站进行爬取,找到我们需要的文件信息,大致代码如下:

// search.ts
...other code

return new Promise(async (resolve, reject) => {
    try {
        const res = await superagent.get(
            `${searchUrl}/page/${currentPage}/?s=${encodeURI(value)}`
        );
        const $ = cheerio.load(res.text);
        const breadcrumb = $('.breadcrumb').text();
        const page = breadcrumb.match(/\d+/g)[0];
        // console.log('page', page);
        const article = $('#main').find('article');
        const array: SearchType[] = [];
        for (let i = 0; i < article.length; i++) {
            const title = $(article[i]).find('.entry-title').text();
            const link = $(article[i])
                .find('.entry-title')
                .find('a')
                .attr('href');
            array.push({
                title,
                link,
                types: [],
            });
        }

        return resolve({ data: array, page });
    } catch (err) {
        console.error(err);
        return reject(err);
    }
});

...other code

上述代码主要是通过用户的搜索信息,爬取对应网站的页面,并将相关信息展示到渲染层。除了search.ts这个文件,我们还需要新增一个event.ts文件,用于主进程和渲染进程之间的通行,因为我们所有的网络操作都是放在主进程,而渲染进程只是通知主进程去执行,然后将主进程执行后的结果返回给渲染进程,并进行渲染,event.ts的大致代码如下:

...other code

// 搜索图书
ipcMain.on('searchText', async (event, arg) => {
    // console.log('arg', arg);
    try {
        const response = await search(arg);
        // console.log('title', response);
        event.sender.send('searchResult', response);
    } catch (err) {
        console.error('error', err);
    }
});

...other code

我们在渲染进程的Search文件中,给主进程发通知:

// 点击搜索
const handleSearch = () => {
    setLoading(true);
    setCurrentPage(1);
    window.ipcRenderer.send('searchText', { value, currentPage });
};

主进程搜索通知后,会去执行search这个方法,然后将对应的返回内容通过event.sender.send('searchResult', response);发送给渲染进程,渲染进程页面,也就是Search页面中,我们在useEffect勾子中去监听主进程发来的通知:

useEffect(() => {
    // 返回搜索结果
    window.ipcRenderer.on('searchResult', (event, args) => {
        // console.log('args', args);
        setDataSource(args.data);
        setToalaPage(args.page);
        setLoading(false);
    });
}, []);

最后我们还需要在主进程main文件夹的index.ts中引入event.ts文件,重新执行后的界面如下:

image.png

就跟我们一开始展示的界面一致了。

好了,到这里就暂时先告一段落了,后续我们还会继续完善自动下载、发送、设置等相关功能,如果大家对这个应用感兴趣,可以持续关注本文,感谢大家的支持。

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

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