探索 Electron:如何进行网址收藏并无缝收录网页图片内容?

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

今天借助electron实现添加网址的应用功能,这里我们通过electron-vite框架搭建项目,详细的配置请参考我之前的文章:地址 这里不再赘述,接下来开始项目的正式讲解:

目录

头部内容搭建

列表数据传递

列表内容处理

头部搜索处理

列表网站弹框

保存弹框图片

收藏本地图片


头部内容搭建

这里我们在头部添加一个添加按钮和搜索框,用于对数据进行处理,如下所示:

<template>
    <div class="search-container">
        <div class="button" @click="handleAdd">+</div>
        <div class="input">
            <input type="text" placeholder="请输入关键字">
        </div>
    </div>
</template>

效果如下所示:

然后这里我们手写一个弹框的效果,如下我们封装一个dialog组件,然后通过showDialog进行判断显示与隐藏:

<template>
    <div class="dialog" v-if="showDialog">
        <div class="content">
            <div class="input">
                <input type="text" placeholder="请输入网址">
            </div>
            <div class="btns">
                <button>添加</button>
                <button @click="setIsShow(false)">取消</button>
            </div>
        </div>
    </div>
</template>

我们在首页的index父组件中,通过provide和inject进行父组件与其所有子孙组件之间进行跨层级数据传递的高级选项,这对于复杂的应用程序结构或深层级嵌套的组件特别有用,如下我们通过其设置了一个变量和控制变量的函数:

<template>
    <div class="home">
        <searchBar></searchBar>
    </div>
    <Dialog></Dialog>
</template>

<script setup lang="ts">
import { ref, provide } from "vue"
import searchBar from "./components/searchBar.vue"
import Dialog from "./components/dialog.vue"

// 窗口的显示状态
const showDialog = ref(false)
const setIsShow = (isShow: boolean) => {
    showDialog.value = isShow
}
provide("dialog-visible", {
    showDialog,
    setIsShow
})
</script>

然后我们在父组件下的两个子组件进行数据的传递,如下所示:

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

列表数据传递

接下来我们开始设置列表内容,然后在home的父组件下进行引入,如下所示:

<template>
    <div class="list">
        <div class="no-item">暂无数据...</div>
        <div class="item">
            <div class="read-item">
                <img src="" alt="">
                <h2>百度一下</h2>
                <button> x </button>
            </div>
            <div class="read-item">
                <img src="" alt="">
                <h2>百度一下</h2>
                <button> x </button>
            </div>
        </div>
    </div>
</template>

接下来我们在对话框中的添加按钮设置点击事件,然后通过ipcRenderer函数中的invoke函数进行主进程与渲染进程的双向通信,这里我们把对话框中的输入框的内容作为数据传递给主进程:

在真正的项目中,主进程可以有许许多多的与渲染进程互通传递数据的函数,为了方便管理,这里我们把与主进程通信的函数抽离出去,然后再在主进程中进行引入,这里我们抽离出一个获取url资源数据的函数进行设置,如下所示:

const { ipcMain, BrowserWindow } = require('electron')

export const getSource = () => {
    ipcMain.handle('add-url', (_, url) => {
        const win = new BrowserWindow({
            width: 500,
            height: 500,
            show: false,
            webPreferences: {
                offscreen: true, // 开启 offscreen
            },
        })
        win.loadURL(url)
        win.webContents.on('did-finish-load', () => {
            const title = win.getTitle()
            console.log(title)
        })
    })
}

如下我们再主进程中进行调用:

当我们点击对话框中的添加按钮后,上述代码会将百度的标题进行一个获取,如下所示:

如果主进程打印的数据出现乱码的情况,这里只需要对package.json文件中运行的命令进行如下配置即可:

接下来我们在getSource文件中,对渲染进程传递过来的url进行一个数据的抓取,这里我们使用了一个异步的Promise进行一个数据的获取并将其return出去,代码如下所示:

const { ipcMain, BrowserWindow } = require('electron');

export const getSource = () => {
    ipcMain.handle('add-url', async (_, url) => {
        const win = new BrowserWindow({
            width: 500,
            height: 500,
            show: false,
            webPreferences: {
                offscreen: true, // 开启 offscreen
            },
        });

        win.loadURL(url);

        return new Promise((resolve, reject) => {
            win.webContents.on('did-finish-load', async () => {
                try {
                    const title = win.getTitle();
                    // 获取nativeImage
                    const image = await win.webContents.capturePage();
                    const screenShot = image.toDataURL();
                    resolve({
                        title,
                        screenShot,
                        url
                    });
                } catch (error) {
                    reject(error);
                }
                // 关闭窗口,避免内存泄漏
                win.close();
            });

            win.webContents.on('did-fail-load', () => {
                reject(new Error('Failed to load the URL'));
                win.close();
            });
        });
    });
}

然后我们在dialog组件中对渲染进程传递的数据进行一个异步的获取结果:

最终达到的效果如下所示,可以看到我们的数据已经成功获取到了:

列表内容处理

添加内容:为了方便处理,这里我们把渲染进程获取到的数据进行一个pinia仓库数据管理,关于pinia仓库及其持久化的配置可以参考我开局分享的链接,这里不再赘述,具体仓库内容如下所示:

// 网站模块信息仓库
import { defineStore } from "pinia";
import { ref } from "vue"
 
export const useWebSiteStore = defineStore("webSite", () => {
    let webSites = ref<any>([]);

    // 添加网站信息
    const addWebSite = (data) => {
        console.log(data)
        webSites.value = [ data, ...webSites.value ]
    }
    return { 
        webSites, 
        addWebSite 
    }
}, { persist: true }) // 开启持久化

然后我们在对话框中的输入的数据在主进程解析并传递过来之后,这里我们将其存储到仓库当中然后通过一个状态判断当前按钮是否是数据存入的状态,避免用户在数据还没返回来之前,对按钮进行重复点击:

<template>
    <div class="dialog" v-if="showDialog">
        <div class="content">
            <div class="input">
                <input v-model="url" type="text" placeholder="请输入网址">
            </div>
            <div class="btns">
                <button @click="handleAdd" :disabled="isSumbit">添加</button>
                <button @click="setIsShow(false)" :disabled="isSumbit">取消</button>
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref, inject } from 'vue'
const { ipcRenderer } = require('electron')
import { useWebSiteStore } from '@renderer/store/modules/webSite'

let url = ref('https://www.baidu.com')
const webSiteStore = useWebSiteStore()
const isSumbit = ref(false) // 是否提交

const { showDialog, setIsShow } = inject('dialog-visible') as any
const handleAdd = async () => {
    isSumbit.value = true
    let result = await ipcRenderer.invoke('add-url', url.value)
    webSiteStore.addWebSite(result)
    isSumbit.value = false  
    setIsShow(false)
}
</script>

存储完仓库之后,在list组件中我们开始把仓库当中的数据进行一个取出,然后进行一个数据的渲染,如下所示:

<template>
    <div class="list">
        <div class="no-item" v-if="webSiteStore.webSites.length <= 0">暂无数据...</div>
        <div class="item" v-for="(item, index) in webSiteStore.webSites" :key="index">
            <div class="read-item">
                <img :src="item.screenShot" :alt="item.title">
                <h2>{{ item.title }}</h2>
                <button> x </button>
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">
import { useWebSiteStore } from '@renderer/store/modules/webSite'
const webSiteStore = useWebSiteStore()
</script>

最终呈现的效果如下所示,可以看到我们的数据已经抓取渲染成功,并且存储到本地磁盘当中:

删除内容:接下来我们在仓库中编写相应的删除数据的函数,这里通过一个filter进行一个url的过滤

// 删除网站信息
const deleteWebSite = (url) => {
    webSites.value = webSites.value.filter(item => item.url !== url)
}

然后在list组件中传递对应的url即可:

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

过滤重复:如果重复添加同一个网站,这里还需要进行一个消息弹框的提示,这里我们在主进程中抽离一个弹框文件进行提示,这里使用到了electron自带的对话框操作:

然后来到我们的仓库里面,对添加的函数进行一个判断,如果已经存在的网址进行一个弹框的提示

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

网址合法:接下来我们要对输入的网址的合法性进行一个处理,如果用户是随便输入的网址,这里我们也要对其进行一个弹框提示,首先我们先在getSource函数中对数据进行一个处理,如果当前的网址不合法,肯定是获取不了图片元素的,这里我们就对其进行一个判断:

然后在dialog组件中,这里我们对添加按钮进行一个情况的判断:

<script setup lang="ts">
import { ref, inject } from 'vue'
const { ipcRenderer } = require('electron')
import { useWebSiteStore } from '@renderer/store/modules/webSite'

let url = ref('https://www.')
const webSiteStore = useWebSiteStore()
const isSumbit = ref(false) // 是否提交

const { showDialog, setIsShow } = inject('dialog-visible') as any
const handleAdd = async () => {
    isSumbit.value = true
    try {
        if (url.value.startsWith('https://www.')) {
            let result = await ipcRenderer.invoke('add-url', url.value)
            webSiteStore.addWebSite(result)
            isSumbit.value = false  
            setIsShow(false)
        } else {
            ipcRenderer.invoke('onShowMessage', '当前输入不是正确网址!')
            url.value = 'https://www.'
            isSumbit.value = false
        }
    } catch (error) {
        ipcRenderer.invoke('onShowMessage', '无法访问该站点!')
        url.value = 'https://www.'
        isSumbit.value = false
    }
    url.value = 'https://www.'
}
const handleCannel = () => {
    setIsShow(false)
    url.value = 'https://www.'
}
</script>

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

头部搜索处理

接下来我们开始实现在头部搜索框输入内容之后,对网站的标题进行一个模糊搜索,因为头部组件和列表内容组件是兄弟组件,搜索框用户输入的数据列表内容组件是要拿到的,兄弟组件进行通信可以使用事件总线bus,或者使用provide和inject方式,这里就使用后者吧!

在父组件定义相关的数据和处理数据的函数,然后使用provide进行暴露出去:

// 搜索组件的数据
const searcKeyWord = ref('')
const setSearchKeyWord = (key: string) => {
    searcKeyWord.value = key
}
provide("search-key", {
    searcKeyWord,
    setSearchKeyWord
})

在搜索组件中,通过keyup鼠标抬起事件来获取输入框数据,并进行inject注入:

在列表内容组件,通过注入拿到对应的数据,可以渲染到页面上,如下所示:

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

然后这里我们开始对输入框进行一个模糊查询,这里我们使用计算属性进行操作,代码如下:

// 获取关键字网站信息 
const filteredWebSites = computed(() => {
    const keyword = searcKeyWord.value.trim().toLowerCase()
    if (!keyword) {
        return webSiteStore.webSites
    } else {
        return webSiteStore.webSites.filter(item => item.title.toLowerCase().includes(keyword))
    }
})

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

列表网站弹框

接下来我们要实现点击列表中的某个网站之后,会弹出对应网站的链接内容的弹框,这里我们要对其列表中的数据设置对应的点击事件,这里顺便把点击列表内容进行一个样式激活操作的内容做掉,具体代码如下所示:

<template>
    <div class="list">
        <div class="no-item" v-if="webSiteStore.webSites.length <= 0">暂无数据...</div>
        <div class="item" v-for="(item, index) in webSiteStore.webSites" :key="index">
            <div class="read-item" :class="{ selected: currentWebSite === index }" @click="handleClick(index, item.url)">
                <img :src="item.screenShot" :alt="item.title">
                <h2>{{ item.title }}</h2>
                <button @click="webSiteStore.deleteWebSite(item.url)"> x </button>
            </div>
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useWebSiteStore } from '@renderer/store/modules/webSite'
const webSiteStore = useWebSiteStore()

// 当前点击的网站
const currentWebSite = ref<number>(0)
// 点击网站
const handleClick = (index, url) => {
    currentWebSite.value = index
}
</script>

效果如下所示,可以看到我们实现了点击之后,样式激活状态的显示:

然后我们在点击事件上可以使用最简单的window自带的打开网页函数:

// 点击网站
const handleClick = (index, url) => {
    currentWebSite.value = index
    window.open(url, '_blank')
}

这里我们主要使用electron带的弹框效果进行列表内容网址弹框,如下我们在点击事件处通过渲染进程往主进程发送一个url数据:

// 点击网站
const handleClick = (index, url) => {
    currentWebSite.value = index
    // window.open(url, '_blank')
    ipcRenderer.invoke('on-open-window', url)
}

在主进程这里通过BrowserWindow函数再次创建一个新的窗口,为了保持窗口的修改状态,这里我们可以使用第三方插件:electron-window-state 进行操作,安装命令如下所示:

npm install --save electron-window-state

接下来我们借助该插件再次创建窗口,并把创建的窗口的状态保存下来,代码如下所示:

const { ipcMain, BrowserWindow } = require('electron');
const WinState = require('electron-window-state')

export const openWindow = () => {
    ipcMain.handle('on-open-window', (_, url) => {
        // 窗口状态管理
        const winState = new WinState({
            defaultWidth: 800,
            defaultHeight: 600,
            electronStoreOptions: { // 存储窗口状态信息
                name: 'win-state'
            }
        });
        const win = new BrowserWindow({
            width: winState.width, 
            height: winState.height,
            x: winState.x, 
            y: winState.y,
            show: false,
        })
        win.on('ready-to-show', () => {
            win.show()
        })
        win.loadURL(url)
        winState.manage(win) // 窗口状态管理
    })
}

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

保存弹框图片

接下来我们对弹框网站中的一些图片进行一个右键保存的效果实现,这里我们在打开的新窗口中调用webContents对象来监听网页的右键上下文菜单(context-menu)事件,并在触发该事件时尝试执行一个名为saveas的函数,该函数意图是保存与右键点击相关的资源(如图片、链接指向的文件等),如下所示:

接下来我们开始编写对应的 saveas 函数中的内容,这里我们使用got模块,got是一个简化和增强Node.js原生http模块的HTTP客户端,用于发送HTTP请求,目前11版本还支持require导入的写法,这里就安装11版本,命令如下:

npm i got@11 -S

然后我们开始调用got模块发起请求,代码如下所示:

const { Menu } = require('electron');
const got = require('got');

export const saveas = (srcUrl) => {
    if (srcUrl) {
        const contextMenu = Menu.buildFromTemplate([
            {
                label: '图片另存为',
                click() {
                    got.get(srcUrl).then((res: any) => {
                        const chunk = Buffer.from(res.rawBody);
                        console.log(chunk.toString())
                    })
                }
            },
        ])
        contextMenu.popup();
    }
}

然后我们在主进程中可以看到我们打印出了图片的二进制流:

拿到二进制流之后,接下来我们开始对其进行一个处理,这里我们通过如下的安装命令,可以获取到我们获取图片的后缀名:

npm i image-type@4.1.0 -S

然后这里我们通过调用electron的对话框函数,弹出保存图片的对话框,然后通过path获取当前保存图片的路径,然后将其下载到我们规定的路径当中:

const { Menu, dialog } = require('electron');
const path = require('path');
const got = require('got');
const imageType = require('image-type');

export const saveas = (srcUrl) => {
    if (srcUrl) {
        const contextMenu = Menu.buildFromTemplate([
            {
                label: '图片另存为',
                click() {
                    got.get(srcUrl).then(async (res: any) => {
                        const chunk = Buffer.from(res.rawBody);
                        const imgType = imageType(chunk);
                        console.log(imgType.ext)
                        const { filePath, canceled } = await dialog.showSaveDialog({
                            title: '图片另存为',
                            defaultPath: path.resolve(__dirname, '../../src/renderer/src/assets/images'),
                        })
                        if (!canceled) {
                            console.log(filePath)
                        }
                    })
                }
            },
        ])
        contextMenu.popup();
    }
}

接下来我们通过一个随机数来对下载图片进行一个命名操作, 通过安装如下命令来获取随机数:

npm i randomstring -D

生成的随机数然后再拼接我们的后缀名,下载图片的前期准备工作可以说是基本完成了:

接下来我们开始对我们下载的图片进行一个写入操作,可以看到图片被成功写入到文件中了:

收藏本地图片

上文将图片保存在本地之后,接下来我们需要把本地保存的图片,再读取到electron桌面端上面,这里我们在桌面页面的顶部上再存放一个按钮,然后进行路由的跳转,这里的路由配置不再赘述,可以参考开局分享的链接,我们在根组件设置如下代码:

<template>
  <div class="container">
    <div class="header">
      <div class="menu">
        <router-link to="/home" :class="{ active: currentIndex === 0 }" @click="currentIndex = 0">网站收藏</router-link>
        <router-link to="/imageGallery" :class="{ active: currentIndex === 1 }" @click="currentIndex = 1">图片收藏</router-link>
      </div>
      <div class="close" @click="close">关闭</div>
    </div>
    <router-view></router-view>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
const { ipcRenderer } = require('electron')

const currentIndex = ref(0)
// 关闭窗口
const close = () => {
  ipcRenderer.send('close-main-window')
}
</script>

然后我们编写获取本地文件的主进程代码,如下所示:

const { ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');

export const getFilesList = () => {
    ipcMain.handle('on-getfiles-event', (_, msg) => {
        fs.readdir(path.resolve(__dirname, '../../src/renderer/src/assets/images/'), (err, files) => {
            console.log(files)
        })
    })
}

在控制台给我们打印出当前文件目录下的所有文件:

然后这里我们将获取到的文件结果给return出去,然后在渲染进程中拿到对应的数据进行一个打印

获取到图片的资源信息之后,接下来通过v-for对数据进行一个渲染:

<template>
    <div class="imageGallery">
        <div class="img-item" v-for="(item, index) in imgList" :key="index">
            <img :src="`/src/assets/images/${item}`" alt="图片">
        </div>
    </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
const { ipcRenderer } = require('electron')

const imgList = ref([])

// 获取本地图片资源
const getLocalImages = async () => {
    const fileList = await ipcRenderer.invoke('on-getfiles-event')
    imgList.value = fileList
}
onMounted(() => {
    getLocalImages()
})
</script>

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

目前项目就暂时写这么多吧,如果大家有想法的也可以在项目中进行一个二开操作,项目地址分享如下,项目地址分享:地址

  • 58
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亦世凡华、

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值