本文已参与「新人创作礼活动」,一起开启掘金创作之路。
大家好,我是爱吃鱼的桶哥,自从上次发布了一篇基于Github Actions完成Electron自动打包、发布及更新后,我一直在加强对Electron的学习,而学习一门框架,最好的办法就是实战,因此我打算从零开发一款桌面端应用。
平时在工作或学习劳累后,我比较喜欢看看书,尤其是电子书,家里正好还有一个泡面盖子(Kindle),于是我就到GitHub
上去搜kindle相关的软件,看看有没有提供相关图书搜索的,搜了一圈发现基本都是不维护了,或是搜索功能也使用不了的,基于这个设想,我准备开发一款能够搜书、一键发送到kindle的桌面端。软件的大致的截图如下:
默认打开软件是这个界面,用户可以在输入框内输入图书的名称,按回车或者搜索按钮后,如果有对应的数据,就能展示出来。具体如下:
当点击后面的选择下载格式时,会弹出这本书的封面信息和对应可下载的格式。如下:
然后我们可以点击想要下载的格式,这里我以epub格式做个示范,点击epub格式的标题,会自动跳转到下载页面进行下载,下载页会展示当前下载图书的进度,当图书下载完成后,会看到当前图书下载的具体位置信息,由于目前软件还在开发中,这期只是第一期,教大家如何完成基本的下载,因此目前样式还没有写完,大家先凑合看,具体如下:
好了,看到这里,大家对这个桌面端软件应该有一个基本的认识了,接下来我们就开始从0到1开发一款这样属于自己的图书下载软件吧。
首先我们选择的技术栈是Electron
、React
、Typescript
,UI组件库用的是antd
,毕竟我们没有专业的设计师为我们提供一个UI,因此用现成是比较好。
在之前的文章中我们选择的基础脚手架是React
的create-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源,能够加快我们的项目安装,如下图:
我们打开控制台,执行npm i
,即可初始化项目,等待初始化结束后,我们可以执行npm run dev
,即可看到一个基本的桌面端应用。
上图中我们可以看到这个应用的基本模样,接下来我们构思一个我们自己的应用大概有哪些内容,有一个大致的需求我们才好开始做,而不是一上来就抓瞎,不知道该如何去写。
在一开始的时候,我给大家展示的图片中就包括四个部分,首先是搜索页面,因为有搜索页面用户才能获取到图书的资源,其次是下载页面,为什么要将搜索和下载分开呢?其实也是为了简单,并且我们要遵循一个原则,一个组件只干一件事,也就是说搜索只做搜索相关的内容,至于下载资源,就交给下载页面来做。然后是发送页面,这个页面主要是用于将图书发送到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-dom
、antd
让我们的项目能够有一个基本的搜索页面的样子。
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
,就能看到我们的应用已经修改为自己想要的样子了,并且左侧的目录也可以正常的点击进行切换。
接下来我们实现一下搜索页面的相关内容,目前还是空白的,不好看,搜索页面的具体代码如下:
...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
跑起来后是这样的:
现在基本的搜索样式已经有了,接下来我们需要完善一下搜索相关的代码。我在网络商搜索了很久,找到了一个电子书的下载网站,它里面有很多图书可以搜索,但是它的图书资源是不在它网站上的,而是放在了某网盘中,我们需要解析网盘的下载地址,获取到真正的图书路径,方便我们将图书资源下载到本地。
由于搜索图书资源其实就是去爬取别人网站的内容,因此我们将这部分代码放在主进程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
文件,重新执行后的界面如下:
就跟我们一开始展示的界面一致了。
好了,到这里就暂时先告一段落了,后续我们还会继续完善自动下载、发送、设置等相关功能,如果大家对这个应用感兴趣,可以持续关注本文,感谢大家的支持。
本项目的代码仓库:electron-vite-react