基於 react + electron 開發及結合爬蟲的應用實踐🎅

前言📝

👉 Electron 是一個可以使用 Web 技術如 JavaScript、HTML 和 CSS 來創建跨平台原生桌面應用的框架。藉助 Electron,我們可以使用純 JavaScript 來調用豐富的原生 APIs。 👈

image-1

一個 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

  • 配置scriptsbuild欄位,在 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。

demo-1

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進行操作,使用方便,引入後操作實例對象調取getsetdelete進行獲取、設置和刪除,但缺點同樣明顯,不能像mongodb一樣通過mongoose構建模型進行數據操作

const Store = require("electron-store");
const store = new Store(test);

store.set("test", true); //設置
store.get("test"); //獲取
store.delete("test"); //刪除

需求介面 UI 簡潔,所以通過 electron 中的 ipcMainipcRenderer 通訊模組結合前端antd/icons設置應用的最小化按鈕、全螢幕按鈕、恢復按鈕,當點擊最小化時,介面隱藏置系統托盤,托盤點擊控制介面出現和隱藏,托盤圖標右鍵進行關閉

title-1
👇👇👇👇👇 更改為
title-2

const {
  Menu: { buildFromTemplate, setApplicationMenu },
} = require("electron");
setApplicationMenu(buildFromTemplate([])); //取消默認工具欄

ipcMainipcRenderer 都是 EventEmitter類的一個實例。而EventEmitter類由NodeJS中的events模組導出,EventEmitter 類是 NodeJS 事件的基礎,實現了事件模型需要的介面, 包括 addListenerremoveListener, 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 其餘部分就是利用ipcMainipcRenderer通訊,使用electron-store操作數據儲存、處理並返回前端,當需要設置壁紙時通過electron-dl進行下載,並返回下載後圖片的絕對路徑給前端,用於設置桌面壁紙

2、前端部分

需要引入的庫

  • antd 頁面樣式
  • wallpaper 設置壁紙
  • puppeteer 爬蟲
  • node-schedule 定時任務

前端頁面初始化時先通過ipcRenderer進行資料庫,如果存在則對比資料庫中time欄位保存的時間與當前時間是否為同一天,都符合則獲取展示,否則調取puppeteer重新進行最新壁紙頁面的數據爬取,並將爬取的數據saveormerge到資料庫,更新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,謝謝大家了