基於 react + electron 開發及結合爬蟲的應用實踐🎅
前言📝
👉 Electron 是一個可以使用 Web 技術如 JavaScript、HTML 和 CSS 來創建跨平台原生桌面應用的框架。藉助 Electron,我們可以使用純 JavaScript 來調用豐富的原生 APIs。 👈
一個 electron-react 栗子 🤖
1️⃣-Demo 安裝 react 腳手架
- 終端執行命令
npx create-react-app react-electron
自動進行配置安裝 - 進入
react-electron
目錄下執行yarn start
,項目自動運行在 3000 埠
2️⃣-Demo 配置 electron 主進程
- 因為
public
文件夾不會被webpack
打包處理,會直接複製一份到dist
目錄下,所以在public
中新建electron.js
作為主進程 - 在主進程中只需要從 electron 包中結構出 app, BrowserWindow,並監聽 app 的’ready’事件,使用 BrowserWindow 生成實例對象,從而判斷環境進行載入靜態文件 or 埠
const { app, BrowserWindow } = require("electron");
const isDev = process.env.NODE_ENV !== "development";
app.on("ready", () => {
mainWindow = new BrowserWindow();
isDev
? mainWindow.loadURL(`file://${__dirname}\\index.html`)
: mainWindow.loadURL(`//localhost:3000`);
});
3️⃣-Demo 配置 react-cli
需要引入的庫
yarn add electron electron-builder nodemon -D //安裝到生產環境
yarn add concurrently cross-env -S //安裝到開發環境
-
在 package.json 中通過 mian 標明主進程執行目錄,配置 homepage
-
配置
scripts
和build
欄位,在 react 啟動後打開 electron 桌面應用、通過cross-env
添加環境變數、以及在打包時如何進行配置(只進行 win 下打包){ "name": "my-app", "version": "0.1.0", "private": true, "main": "public/electron.js", "homepage": ".", "scripts": { "start": "cross-env NODE_ENV=development concurrently \"yarn run client\" \"wait-on //localhost:3000 && yarn run electron:watch\" ", "build": "yarn run build-client && yarn run build-electron", "client": "set BROWSER=none && react-scripts start", "electron:watch": "nodemon --watch public/electron.js --exec electron .", "electron": "electron .", "build-client": "react-scripts build", "build-electron": "electron-builder build -w", "test": "react-scripts test", "eject": "react-scripts eject" }, "build": { "productName": "electron-demos", "files": ["build/","main.js"], "dmg": { "contents": [ {"x": 110,"y": 150}, {"x": 240,"y": 150,"type": "link", "path": "/Applications"} ] }, "win": { "target": [{"target": "nsis", "arch": ["x64" ]}] }, "directories": { "buildResources": "assets", "output": "release" } }, }
此時我們可以運行yarn start
將之前的react
起始頁通過桌面程式的方式打開,也可以通過執行yarn build
將我們的桌面程式打包生成.exe
文件進行安裝 over。
electron-react 每日壁紙 🧠
既然我們可以利用
react
&electron
構建桌面應用,就可以利用眾多 npm 包去實現一個能用在生活中可以用到的功能,前段時間由於興趣使然,接觸 node 爬蟲比較多,所以我想結合puppeteer
實現每日壁紙的桌面應用
1️⃣-wallpaper 明確需求
- 壁紙進行分類獲取,所有主題的壁紙通過合集的方式保存
- 每天的壁紙按時更新,更新過的壁紙會保存到資料庫中
- 壁紙合集中的壁紙可以通過喜歡功能進行收藏或取消
- 壁紙可以預覽、下載,並可進行一鍵設置
- 在收藏的壁紙中可以開啟是否進行每天自動設置當前壁紙
- 風格簡約,自適應布局
2️⃣-wallpaper 功能實現
1、electron 部分
需要引入的庫
dayjs
判斷和添加日期時electron-store
數據存儲 (如果使用mongodb
資料庫在開發環境正常,但是打包後就會報錯)electron-dl
圖片下載
首先進行BrowserWindow
的初始化配置
mainWindow = new BrowserWindow({
show: false,
width: 900,
height: 700,
minHeight: 700,
minWidth: 310,
frame: false, //無邊框
transparent: false, //透明
alwaysOnTop: false,
hasShadow: false, //陰影
resizable: true,
webPreferences: {
nodeIntegration: true, //是否使用 node
enableRemoteModule: true, //是否有子頁面
contextIsolation: false, //是否禁止 node
nodeIntegrationInSubFrames: true, //否允許在子頁面(iframe)或子窗口(child window)中集成 Node.js
},
});
數據通過electron-store
進行操作,使用方便,引入後操作實例對象調取get
、set
、delete
進行獲取、設置和刪除,但缺點同樣明顯,不能像mongodb
一樣通過mongoose
構建模型進行數據操作
const Store = require("electron-store");
const store = new Store(test);
store.set("test", true); //設置
store.get("test"); //獲取
store.delete("test"); //刪除
需求介面 UI 簡潔,所以通過 electron 中的 ipcMain
和 ipcRenderer
通訊模組結合前端antd/icons
設置應用的最小化按鈕、全螢幕按鈕、恢復按鈕,當點擊最小化時,介面隱藏置系統托盤,托盤點擊控制介面出現和隱藏,托盤圖標右鍵進行關閉
👇👇👇👇👇 更改為
const {
Menu: { buildFromTemplate, setApplicationMenu },
} = require("electron");
setApplicationMenu(buildFromTemplate([])); //取消默認工具欄
ipcMain
和 ipcRenderer
都是 EventEmitter
類的一個實例。而EventEmitter
類由NodeJS
中的events
模組導出,EventEmitter
類是 NodeJS 事件的基礎,實現了事件模型需要的介面, 包括 addListener
,removeListener
, emit
及其它工具方法. 同原生 JavaScript 事件類似, 採用了發布/訂閱(觀察者)的方式, 使用內部 _events
列表來記錄註冊的事件處理器。
const { Tray } = require("electron");
var appTray;
ipcMain.on("max-icon", () => {
//點擊最大化時,主進程響應
mainWindow.isMaximized() ? mainWindow.restore() : mainWindow.maximize();
});
ipcMain.on("mini-icon", () => {
//點擊最小化時
mainWindow.minimize(); //介面最小化
mainWindow.hide(); //隱藏介面
if (!appTray) {
appTray = new Tray(path.join(__dirname, "favicon.ico")); //設置托盤圖標
appTray.setToolTip("one wallpaper💎"); //托盤圖標hover時觸發
appTray.on("click", () =>
//托盤圖標點擊時觸發
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
);
appTray.setContextMenu(
//托盤圖標右擊時觸發
buildFromTemplate([
{
label: "退出",
click: () => app.quit(),
},
])
);
}
});
electron 其餘部分就是利用ipcMain
和 ipcRenderer
通訊,使用electron-store
操作數據儲存、處理並返回前端,當需要設置壁紙時通過electron-dl
進行下載,並返回下載後圖片的絕對路徑給前端,用於設置桌面壁紙
2、前端部分
需要引入的庫
antd
頁面樣式wallpaper
設置壁紙puppeteer
爬蟲node-schedule
定時任務
前端頁面初始化時先通過ipcRenderer
進行資料庫,如果存在則對比資料庫中time
欄位保存的時間與當前時間是否為同一天,都符合則獲取展示,否則調取puppeteer
重新進行最新壁紙頁面的數據爬取,並將爬取的數據save
ormerge
到資料庫,更新time
欄位,點擊對應集合時,進行對應集合的爬取並添加到當前children
欄位進行保存, 數據結構為如下所示
[
{
time:'xxxx-xx-xx'
},
{
href: "當前集合鏈接",
srcmini: "集合縮略圖.jpg",
title: "集合名稱",
children: [
{
like: true, //該壁紙是否添加收藏
href: "壁紙所屬集合鏈接",
maxsrc: "壁紙縮略圖.jpg",
srcmini: "壁紙大圖.jpg",
},
...
],
}
...
];
前端選用的是 react
+antd
進行開發,需要引入的 node 庫時在 utils.js
文件下進行引入處理、並通過 es6 方式進行導出,由於electron
通訊的回調函數在 es6 中並不友好,所以在utils.js
中進行統一的非同步封裝,以xxx-reply
作為響應 ipcRenderer 通訊的標準格式,調用時直接傳入通訊事件名await ipcasync('xxx')
export const { ipcRenderer } = window.require("electron");
export const ipcasync = async (name, obj = null) => {
ipcRenderer.send(name, obj);
return await new Promise(resolve => {
ipcRenderer.on(`${name}-reply`, (event, arg) => resolve(arg));
});
};
爬蟲使用的puppeteer
庫,通過無頭瀏覽器進行爬取,防止網頁動態載入導致獲取不到數據,並可以進行點擊、輸入等模擬用戶真實行為,弊端在於爬取速度較慢,所以會將爬取到的數據保存,避免二次爬取,在爬取壁紙集合時,會根據 electron 獲取到的頁面大小進行匹配壁紙尺寸進行爬取
爬取當前最新壁紙
const getHomePage = async url => {
let urls = "壁紙網站域名/" + url; //url即子域名
const browser = await puppeteer.launch(config);
const page = await browser.newPage();
await page.goto(urls);
await page.waitForSelector(".wrapper", { visible: true });
const arr = await page.$$eval(".main>ul a", el =>
el.map(i => ({
href: "壁紙網站域名/" + i.getAttribute("href"),
srcmini: i.firstChild.getAttribute("src"),
title: i.firstChild.getAttribute("title"),
children: [],
}))
);
browser.close();
return arr;
};
爬取指定集合下壁紙
const getPages = async (url, screen) => {
const browser = await puppeteer.launch(config);
const page = await browser.newPage();
await page.goto(url);
const all = await page.$eval(".wrapper span", el => el.textContent);
const allPage = all.split("/")[1].replace(")", "");
await page.waitForSelector(".wrapper", { visible: true });
const arr = await page.$$eval("#showImg li a", el =>
el.map(i => ({
href: "壁紙網站域名/" + i.getAttribute("href"),
srcmini:
i.firstElementChild.getAttribute("src") ||
i.firstElementChild.getAttribute("srcs"),
}))
);
for (let i = 0; i < arr.length; i++) {
console.log(`總共爬取 ${allPage} 張,當前爬取第 ${i} 張`);
await page.goto(arr[i].href);
await page.waitForSelector(`#tagfbl`, { visible: true });
const hrefItems = await page.evaluate(
el =>
document.querySelector(el)
? document.querySelector(el).getAttribute("href")
: document.querySelector(`a[id="1920x1080"]`)
? document.querySelector(`a[id="1920x1080"]`).getAttribute("href")
: document.querySelector(`#tagfbl a`).getAttribute("href"),
`a[id="${screen}"]`
);
await page.goto("壁紙網站域名/" + hrefItems);
await page.waitForSelector("body img", { visible: true });
const hrefItem = await page.$eval("body img", el => el.src);
arr[i].maxsrc = hrefItem;
}
browser.close();
return arr;
};
3️⃣-wallpaper 展示
功能展示
- 自適應布局 ✔
- 壁紙收藏 ✔
- 壁紙下載 ✔
- 每日更新 ✔
- 動態壁紙 ✖(真不知道怎麼搞,來個大佬指導一下)
4️⃣-wallpaper 總結
至此,謝謝各位在百忙之中點開這篇文章,希望對你們能有所幫助,相信你對 electron 結合 react 開發以及有了大概的認實,總的來說優化的點還有很多,比如 webpack 的打包配置、爬蟲、等等…此項目為了大家能更熟練上手在上手 electron+react 的業務需求,如有問題歡迎各位大佬指正。
- 👋:跳轉github
- 🍑:將 package 文件中的 executablePath 更改為自己Google瀏覽器的目標路徑
求個 star,謝謝大家了