整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          前端必學桌面開發:Electron+React開發桌

          前端必學桌面開發:Electron+React開發桌面應用(1W多字超詳細)

          篇教程是使用Electron + React18進行開發,這里主要講electron的使用。首先我們需要一個react的項目環境,react的項目搭建及開發教程可以參考我的react專欄里的文章:react相關技術這里都有

          前言

          Electron是一個開源的跨平臺桌面應用程序開發框架,允許開發者使用前端 Web 技術(HTML、CSS 和 JavaScript)來構建桌面應用程序 背景和起源 Electron 最初由 GitHub 公司開發,最早用于構建 GitHub Desktop。隨著其成功,Electron 逐漸成為一個受歡迎的開發框架,許多知名應用程序如 Visual Studio Code、Slack、WhatsApp 等也使用 Electron 構建。 基本原理 Electron 使用 Chromium 渲染引擎來顯示 Web 內容,同時結合 Node.js 來提供對操作系統的訪問和控制。這使得開發者能夠使用 Web 技術來構建桌面應用程序,同時還能夠利用底層操作系統的功能。 主要特點

          1. 跨平臺: Electron 應用程序可以在多個操作系統(如 Windows、macOS、Linux)上運行,因為它們在不同平臺上共享相同的核心代碼。
          2. 前端技術: 開發者可以使用熟悉的前端技術,如 HTML、CSS 和 JavaScript,來構建用戶界面。
          3. Node.js 集成: 通過 Node.js,開發者可以在應用程序中使用 JavaScript 來處理文件系統、網絡通信、操作系統 API 等等。
          4. 自定義性: 開發者可以通過使用原生的 Web 技術和樣式,創建非常定制化的用戶界面。
          5. 社區支持: 有一個活躍的社區,提供了大量的插件和庫,以幫助開發者構建更強大、更高效的應用程序。

          核心組件

          1. 主進程(Main Process): 這是應用程序的主要控制中心,運行 Node.js 環境,負責管理和控制所有的渲染進程和窗口。
          2. 渲染進程(Renderer Process): 每個 Electron 窗口對應一個獨立的渲染進程,它們運行在 Chromium 渲染引擎中,負責顯示用戶界面。
          3. 主窗口(Main Window): 主窗口是應用程序的主界面,通常是一個 Chromium 窗口,用來顯示 Web 內容。
          4. 系統托盤圖標(Tray): 允許在桌面右下角顯示小圖標,提供應用程序的快速訪問和交互。

          開發流程

          1. 使用 HTML、CSS 和 JavaScript 創建用戶界面。
          2. 在主進程中使用 Node.js 進行應用程序的邏輯控制。
          3. 通過與底層操作系統 API 進行交互,實現文件操作、網絡通信等功能。
          4. 使用 Electron 打包工具將應用程序打包成特定平臺的可執行文件。

          核心架構圖解


          Electron安裝

          安裝electron

          首先,我們需要在一個常規的React項目中,安裝electron,為了使我們功能代碼部分和electron窗口部分更清晰,我們可以在項目的根目錄新建一個desktop文件夾,專門用來存放electron部分代碼和資源。目錄結構大概如圖所示:

          我們cd desktopdesktop文件夾下,執行npm init -y初始化包管理器,然后安裝electron相關包: electron:electron核心包 cross-env:cross-env 是一個用于設置跨平臺環境變量的工具。它可以在 Windows、Linux 和 macOS 等操作系統上提供一致的環境變量設置方式,使得在不同平臺上運行腳本時能夠保持一致的行為。 electron-builder:electron-builder 是一個用于打包、構建和部署 Electron 應用程序的強大工具

          powershell
          復制代碼npm i electron cross-env electron-builder

          Electron App生命周期,創建窗口,應用運行

          App生命周期

          在electron應用的運行過程中存在著自己的生命周期,在不同的生命周期中我們可以做對應的事情,下面介紹一些常用的生命周期,electron的生命周期通過electron中的app實例監聽,我們在desktop目錄下新建一個index.js文件,作為electron的入口文件,并在其中監聽應用的各個生命周期 ready 觸發時機:當 Electron 初始化完成并且應用程序準備好創建瀏覽器窗口時。 作用:通常用于初始化應用程序的主要界面和一些基礎設施。 示例:在 ready 事件中創建主窗口和初始化托盤。 certificate-error 觸發時機:當在加載網頁時發生證書錯誤時。 作用:可以在這個事件中攔截證書錯誤并決定是否繼續加載頁面。 示例:在證書錯誤時阻止默認行為并返回 true 以繼續加載頁面。 before-quit 觸發時機:當用戶嘗試退出應用程序時,通常是通過關閉所有窗口或者點擊關閉按鈕。 作用:在應用程序退出之前執行一些清理操作。 示例:可以在這個事件中執行一些清理或保存操作。 window-all-closed 觸發時機:所有應用程序窗口都被關閉時。 作用:在此事件中通常用于在應用程序完全退出之前保留某些功能。 示例:在 macOS 下通常會保留菜單欄。 activate 觸發時機:在點擊 Dock 圖標(macOS)或者任務欄圖標(Windows)時。 作用:通常用于在所有窗口都已關閉的情況下,重新創建主窗口。 示例:在 macOS 下,當點擊 Dock 圖標時,可以重新創建主窗口。 quit 觸發時機:應用程序即將退出時。 作用:在應用程序退出之前執行最后的清理操作。 示例:在這個事件中可以銷毀托盤或其他資源。 will-quit 觸發時機:在應用程序即將退出時,但在 quit 事件之前。 作用:在應用程序退出之前執行一些清理或保存操作。 示例:在這個事件中可以執行一些清理或保存操作。 will-finish-launching 觸發時機:在應用程序即將完成啟動時。 作用:可以在此事件中執行一些在應用程序完全啟動之前需要完成的操作。 示例:在這個事件中可以初始化一些啟動時需要的資源。

          javascript復制代碼const { app }=require('electron')
          const { createMainWindow }=require('./windows/mainWindow')
          
          app.on('ready', ()=> {
              createMainWindow()
          })
          app.on('certificate-error', (event, webContents, url, error, certificate, callback)=> {
              event.preventDefault()
              callback(true)
          })
          app.on('before-quit', ()=> {
              console.log('app before-quit')
          })
          app.on('window-all-closed', function () {
              console.log('window-all-closed')
          })
          app.on('activate', function () {
              console.log('activate')
          })
          app.on('quit', function () {
              console.log('quit')
              getTray() && getTray().destroy()
          })
          app.on('will-quit', function () {
              console.log('will-quit')
          })
          app.on('will-finish-launching', function () {
              console.log('will-finish-launching')
          })

          創建窗口

          我們在desktop文件夾中創建一個windows文件夾,里面存放每個窗口的相關代碼(我們項目中通常不止一個窗口),我們在windows文件夾中創建一個mainWindow.js文件,用于創建一個簡單的窗口

          javascript復制代碼// 在主進程中.
          const { BrowserWindow }=require('electron')
          const path=require('path')
          
          const win=new BrowserWindow({ width: 800, height: 600 })
          
          // Load a remote URL
          win.loadURL('http://localhost:8000/')
          
          // Or load a local HTML file
          win.loadFile(path.resolve(__dirname, '../../../build/index.html'))

          其中loadURL用于加載一個服務器地址,運行后將會在窗口中顯示該地址的內容,我們這里的http://localhost:8000/是代碼運行的本地環境地址 loadFile是加載一個靜態文件,該文件就是渲染層代碼打包后的入口文件

          應用運行

          由于創建窗口需要在app.on('ready', ()=> {})中,因此我們可以把創建窗口封裝成一個函數并導出,在app.on('ready', ()=> {})中執行,例如: 封裝mainWindow.js

          javascript復制代碼const { BrowserWindow, ipcMain }=require('electron')
          const path=require('path')
          
          const isDevelopment=process.env.NODE_ENV==='development'
          let mainWindow=null
          
          function createMainWindow() {
              mainWindow=new BrowserWindow({
                  width: 1160,
                  height: 752,
                  minHeight: 632,
                  minWidth: 960,
                  show: false,
                  frame: false,
                  title: 'Harbour',
                  webPreferences: {
                      nodeIntegration: true,
                      preload: path.resolve(__dirname, '../utils/contextBridge.js')
                  },
                  icon: path.resolve(__dirname, '../assets/logo.png')
              })
          
              if (isDevelopment) {
                  mainWindow.loadURL('http://localhost:8000/')
              } else {
                  const entryPath=path.resolve(__dirname, '../../build/index.html')
                  mainWindow.loadFile(entryPath)
              }
          
              mainWindow.once('ready-to-show', ()=> {
                  mainWindow.show()
              })
          }
          
          module.exports={ createMainWindow }
          代碼解析:我們這里使用process.env.NODE_ENV的值判斷當前的運行環境,這里的運行環境需要說明一下,當我們使用下面配置的npm run dev-electron運行時,該值為"development",當我們將渲染層代碼打包后,使用npm run prod-electron運行時,該值為"production",然而當我們使用electron-builder打包出來的安裝包運行時,該值不存在為undefined。因此只有當該值是"development"時我們才加載一個我們渲染層啟動的服務地址,其他兩種情況下我們都需要加載我們渲染層打包后的入口文件即:build目錄下的index.html`
          `我們在窗口觸發"ready-to-show"時顯示窗口是為了使加載時的白屏時間不被用戶看到

          index.js導入并在app.on('ready', ()=> {})中執行

          javascript復制代碼const { app }=require('electron')
          const { createMainWindow }=require('./windows/mainWindow')
          
          app.on('ready', ()=> {
              createMainWindow()
          })

          此時我們就可以運行electron,我們在package.json中配置運行命令

          json復制代碼{
              "scripts": {
                  "dev-electron": "cross-env NODE_ENV=development electron main/index.js",
                  "prod-electron": "cross-env NODE_ENV=production electron main/index.js",
              }
          }

          執行命令,啟動開發環境

          powershell
          復制代碼npm run dev-electron 

          運行成功,出現如下窗口(窗口內部內容可自行定義)

          使用npm run prod-electron命令可以啟動生產環境,該生產環境指的是渲染層的功能代碼使用webpack打包后的代碼,使其渲染到窗口中。真正的生產環境應該是下面介紹的使用electron-builder打包后的應用程序,此時process.env.NODE_ENV為undefined

          Electron應用打包

          上面我們啟動electron的應用都是使用的node_modules中的electron包,我們想要得到一個真正可以安裝的安裝包,還需要使用第三方打包工具進行打包,上面有提到過,我們將使用electron-builder打包成可安裝的安裝包。上面我們已經安裝了electron-builder,下面我們需要在package.json中配置build屬性來自定義安裝配置。(限于自身設備問題,這里只介紹在Windows系統的打包配置,electron可以打包成各種安裝包,使其可以在mac,Linux系統上運行,其他系統的配置可自行查閱資料。)下面我們介紹一下配置內容和各個配置含義。 package.json完整配置

          json復制代碼{
            "name": "desktop",
            "productName": "Harbour",
            "version": "1.0.0",
            "description": "",
            "main": "main/index.js",
            "scripts": {
              "dev-electron": "cross-env NODE_ENV=development electron main/index.js",
              "prod-electron": "cross-env NODE_ENV=production electron main/index.js",
              "build-electron-win64": "electron-builder -w --x64"
            },
            "build": {
              "productName": "Harbour",
              "appId": "harbour.electron.app",
              "files": [
                "build/**/*",
                "main/**/*"
              ],
              "directories": {
                "output": "dist"
              },
              "nsis": {
                "oneClick": false,
                "allowElevation": true,
                "allowToChangeInstallationDirectory": true,
                "installerIcon": "./main/assets/logo.ico",
                "uninstallerIcon": "./main/assets/logo.ico",
                "installerHeaderIcon": "./main/assets/logo.png",
                "createDesktopShortcut": true,
                "createStartMenuShortcut": true,
                "shortcutName": "Harbour"
              },
              "win": {
                "icon": "./main/assets/logo.ico",
                "artifactName": "${productName}-${version}-${os}-${arch}.${ext}",
                "target": "nsis"
              },
              "electronDist": "./electron"
            },
            "keywords": [],
            "author": "",
            "license": "ISC",
            "devDependencies": {
              "cross-env": "^7.0.3",
              "electron": "^26.1.0",
              "electron-builder": "^24.6.3"
            }
          }
          配置解釋
          1. productName:指定了您的應用程序的產品名稱,通常用于構建過程中生成的安裝程序文件名等地方。
          2. appId:指定了您的應用程序的唯一標識符,這個值在打包和部署時會用到。
          3. files:指定打包時所需打包的文件
          4. directories.output:指定了輸出目錄的路徑,即構建后的文件將會保存在 dist 目錄中。
          5. nsis
          6. :指定了 NSIS(Nullsoft Scriptable Install System)打包的相關配置。
            1. oneClick:指定是否啟用一鍵安裝模式。
            2. allowElevation:是否允許提升權限進行安裝。
            3. allowToChangeInstallationDirectory:是否允許用戶更改安裝目錄。
            4. installerIcon:安裝程序的圖標文件路徑。
            5. uninstallerIcon:卸載程序的圖標文件路徑。
            6. installerHeaderIcon:安裝程序的頭部圖標文件路徑。
            7. createDesktopShortcut:是否在桌面上創建快捷方式。
            8. createStartMenuShortcut:是否在開始菜單中創建快捷方式。
            9. shortcutName:創建的快捷方式的名稱。
          7. win
          8. :指定了 Windows 平臺的配置。
            1. icon:指定應用程序的圖標文件路徑。
            2. artifactName:定義生成的構建文件的命名規則模板。
            3. target:指定構建的目標平臺,這里是 NSIS。
          9. electronDist:指定了預先下載的 Electron 包的路徑。

          特別注意 這里有幾個需要特別注意的點:

          1. 首先我們用的logo.ico文件尺寸大小至少是256*256
          2. 由于打包時需要使用electron的相關包文件,為了提高打包速度,我們一般會提前下載與我們node_modules相同版本的.zip包,然后打包時使用electronDist指定打包用的文件目錄,可以縮減打包時間
          3. 自定義artifactName,該名稱就是打包后我們可安裝的.exe可執行文件的名稱
          4. electron-builder打包原理實際上是將package.json同目錄的所有文件進行整體打包輸出,如下圖所示,在package.json同級目錄下有一些文件夾我們是不需要進行打包的,其中dist下是我們上次打包輸出的內容,electron是我們預下載的打包所需的.zip包,node_modules下面是我們開發時所用的依賴包,這些都不需要打包進去。因此我們需要指定我們打包時所需要打包的文件夾,此時就需要用到package.json里面build配置中的files屬性,如上配置,我們只需要將build目錄下的文件和main下面的文件打包即可。
          5. 這里的build目錄下是渲染層的代碼,main下面都是我們主進程的代碼

          打包后的內容

          dist目錄下就是打包生成的內容,其中第一個紅框的Harbour.exe是可直接執行的文件,無需安裝第二個紅框中的.exe可執行文件就是可安裝的文件,在文件夾中,雙擊即可進入安裝流程。

          Electron常用API詳解

          使用BrowserWindow創建窗口

          創建窗口常用配置Option

          在我們創建窗口時可以配置很多自定義配置,下面是一些常用配置及解析:

          1. width 和 height:用于設置窗口的初始寬度和高度。
          2. x 和 y:控制窗口的初始位置,以屏幕坐標為基準。
          3. fullscreen:布爾值,指定窗口是否以全屏模式啟動。
          4. resizable:布爾值,控制用戶是否可以調整窗口大小。
          5. minWidth 和 minHeight:指定窗口的最小寬度和最小高度。
          6. maxWidth 和 maxHeight:指定窗口的最大寬度和最大高度。
          7. frame:布爾值,指定是否顯示窗口的外部框架(包括標題欄和控制按鈕)。
          8. title:用于設置窗口的標題。
          9. icon:指定窗口的圖標文件路徑。
          10. backgroundColor:用于設置窗口的背景顏色。
          11. webPreferences:用于配置窗口的 Web 集成選項,例如啟用 Node.js、預加載腳本等。
          12. nodeIntegration:指定是否在渲染進程中啟用 Node.js 集成,允許在渲染進程中使用 Node.js API。
          13. contextIsolation:啟用上下文隔離,將渲染進程的環境與主進程隔離開來,以提高安全性。
          14. preload:指定一個預加載的 JavaScript 文件的路徑,該文件在渲染進程運行之前加載。
          15. devTools:指定是否允許在窗口中打開開發者工具。
          16. webSecurity:指定是否啟用同源策略,限制頁面對其他源的請求。
          17. alwaysOnTop:布爾值,控制窗口是否始終保持在頂部。
          18. fullscreenable:布爾值,指定窗口是否可以進入全屏模式。
          19. show:布爾值,指定創建窗口后是否立即顯示。
          20. transparent:布爾值,指定窗口是否支持透明度。
          21. parent 和 modal:用于實現模態窗口的行為。
          22. closable:布爾值,指定用戶是否可以關閉窗口。
          23. focusable:布爾值,指定窗口是否可以獲得焦點。
          24. minimizable 和 maximizable:控制窗口是否可以最小化和最大化。
          25. skipTaskbar:布爾值,控制窗口是否在任務欄中顯示。
          javascript復制代碼const { BrowserWindow }=require('electron');
          
          const mainWindow=new BrowserWindow({
              width: 800,
              height: 600,
              x: 100,
              y: 100,
              fullscreen: false,
              resizable: true,
              minWidth: 400,
              minHeight: 300,
              frame: true,
              title: 'My Electron App',
              icon: '/path/to/icon.png',
              backgroundColor: '#ffffff',
              webPreferences: {
                  nodeIntegration: true,
                  contextIsolation: false,
                  preload: 'path/to/preload.js',
                  devTools: true,
                  webSecurity: true
              },
              alwaysOnTop: false,
              fullscreenable: true,
              show: true,
              transparent: false,
              closable: true
          });
          
          mainWindow.loadFile('index.html');

          窗口常用的實例事件

          窗口有很多實例事件,使用window.on來監聽,可以在這些事件觸發時做一切操作例如下面是一些常用的實例事件: close 觸發時機:窗口即將關閉時觸發,但實際關閉前。 作用:允許執行一些在窗口關閉前的清理操作,或者阻止窗口關閉。 closed 觸發時機:窗口已經關閉時觸發。 作用:通常用于釋放資源或執行一些在窗口關閉后的最終操作。 resize 觸發時機:窗口大小發生變化時觸發。 作用:允許在窗口大小變化時執行一些操作。 move 觸發時機:窗口位置發生變化時觸發。 作用:允許在窗口位置變化時執行一些操作。 focus 觸發時機:窗口獲得焦點時觸發。 作用:允許在窗口獲得焦點時執行一些操作。 blur 觸發時機:窗口失去焦點時觸發。 作用:允許在窗口失去焦點時執行一些操作。 minimize 觸發時機:窗口被最小化時觸發。 作用:允許在窗口最小化時執行一些操作。 maximize 觸發時機:窗口被最大化時觸發。 作用:允許在窗口最大化時執行一些操作。 unmaximize 觸發時機:窗口從最大化狀態恢復時觸發。 作用:允許在窗口從最大化狀態恢復時執行一些操作。 ready-to-show 觸發時機:當窗口完成初始化并且準備好顯示時觸發。 作用:允許在窗口已準備好顯示之后執行一些操作。這通常在窗口加載內容后并準備好顯示時觸發,用于控制窗口的顯示時機。 show 觸發時機:當窗口被顯示時觸發。 作用:允許在窗口顯示時執行一些操作。 hide 觸發時機:當窗口被隱藏時觸發。 作用:允許在窗口隱藏時執行一些操作。 enter-full-screen 觸發時機:當窗口進入全屏模式時觸發。 作用:允許在窗口進入全屏模式時執行一些操作。 leave-full-screen 觸發時機:當窗口離開全屏模式時觸發。 作用:允許在窗口離開全屏模式時執行一些操作。

          javascript復制代碼// main.js
          
          const { app, BrowserWindow }=require('electron');
          let mainWindow;
          
          function createMainWindow() {
            mainWindow=new BrowserWindow({
              width: 800,
              height: 600,
              webPreferences: {
                nodeIntegration: true
              }
            });
          
            // 加載你的 HTML 文件
            mainWindow.loadFile('index.html');
          
            // 事件: 關閉
            mainWindow.on('close', (event)=> {
              // 允許或阻止窗口關閉
              // event.preventDefault();
              // 執行清理操作
            });
          
            // 事件: 關閉后
            mainWindow.on('closed', ()=> {
              // 釋放資源或執行最終操作
              mainWindow=null;
            });
          
            // 事件: 調整大小
            mainWindow.on('resize', ()=> {
              // 在窗口調整大小時執行操作
            });
          
            // 事件: 移動
            mainWindow.on('move', ()=> {
              // 在窗口移動時執行操作
            });
          
            // 事件: 獲得焦點
            mainWindow.on('focus', ()=> {
              // 在窗口獲得焦點時執行操作
            });
          
            // 事件: 失去焦點
            mainWindow.on('blur', ()=> {
              // 在窗口失去焦點時執行操作
            });
          
            // 事件: 最小化
            mainWindow.on('minimize', ()=> {
              // 在窗口最小化時執行操作
            });
          
            // 事件: 最大化
            mainWindow.on('maximize', ()=> {
              // 在窗口最大化時執行操作
            });
          
            // 事件: 還原
            mainWindow.on('unmaximize', ()=> {
              // 在窗口從最大化狀態還原時執行操作
            });
          
            // 事件: 準備好顯示
            mainWindow.on('ready-to-show', ()=> {
              // 在窗口準備好顯示后執行操作
              mainWindow.show();
            });
          
            // 事件: 顯示
            mainWindow.on('show', ()=> {
              // 在窗口顯示時執行操作
            });
          
            // 事件: 隱藏
            mainWindow.on('hide', ()=> {
              // 在窗口隱藏時執行操作
            });
          
            // 事件: 進入全屏模式
            mainWindow.on('enter-full-screen', ()=> {
              // 在窗口進入全屏模式時執行操作
            });
          
            // 事件: 離開全屏模式
            mainWindow.on('leave-full-screen', ()=> {
              // 在窗口離開全屏模式時執行操作
            });
          }

          窗口常用的實例屬性

          窗口自身存在很多的實例屬性,可以使我們獲取到窗口的一些當前狀態。下面是一些常用的實例屬性。

          1. win.id - 窗口的唯一ID。
          2. win.webContents- 包含窗口網頁內容的BrowserWindowProxy對象。
          3. win.devToolsWebContents - 用于開發者工具窗口的webContents。
          4. win.minimizable - 是否允許最小化窗口,默認為true。
          5. win.maximizable - 是否允許最大化窗口,默認為true。
          6. win.fullScreenable - 是否允許全屏窗口,默認為true。
          7. win.resizable - 是否允許改變窗口大小,默認為true。
          8. win.closable - 是否允許關閉窗口,默認為true。
          9. win.movable - 是否允許移動窗口,默認為true。
          10. win.alwaysOnTop - 是否永遠置頂,默認為false。
          11. win.modal - 是否為模態窗口,默認為false。
          12. win.title - 窗口標題。
          13. win.defaultWidth/Height - 窗口默認寬高。
          14. win.width/height - 窗口當前寬高。
          15. win.x/y- 窗口左上角坐標。

          窗口常用的實例方法

          1. win.loadURL(url)- 加載指定URL到窗口中,通常用于加載本地文件或遠程網頁。
          2. win.webContents.send(channel, ...args)- 在窗口之間發送異步消息。channel 是一個字符串,用于標識消息的類型,...args 是要傳遞的參數。
          3. win.show()- 顯示窗口,通常與 hide() 方法配合使用。
          4. win.hide()- 隱藏窗口。
          5. win.close()- 關閉窗口
          6. win.minimize()- 最小化窗口
          7. win.maximize()- 最大化窗口
          8. win.restore()- 恢復窗口大小和位置。
          9. win.setSize(width, height[, animate])- 設置窗口的寬度和高度。
          10. win.setPosition(x, y[, animate])- 設置窗口的位置。
          11. win.getTitle()- 獲取窗口的標題。
          12. win.setTitle(title)- 設置窗口的標題。
          13. win.setMenu(menu)- 設置窗口的菜單。
          14. win.setResizable(resizable)- 設置窗口是否可以改變大小。
          15. win.setAlwaysOnTop(flag[, level[, relativeLevel]])- 將窗口置頂。
          16. win.setMenu(null)- 隱藏窗口的菜單欄。
          17. win.setProgressBar(progress)- 設置窗口的任務欄進度條。
          18. win.focus()- 將窗口置于前臺并獲得焦點。
          19. win.isVisible()- 返回窗口是否可見。
          20. win.isFullScreen()- 返回窗口是否全屏。
          21. win.isMaximized()- 返回窗口是否最大化。
          22. win.webContents.executeJavaScript(code[, userGesture])- 在窗口的渲染進程中執行一段 JavaScript 代碼。
          23. win.openDevTools([options])- 打開開發者工具。

          創建右下角托盤

          對于一個桌面應用來說,右下角的系統托盤必不可少,electron應用的系統托盤使用tray這個api實現,下面是封裝的專門處理系統托盤的文件 systemTray.js

          javascript復制代碼const { app, Tray, Menu }=require('electron')
          const path=require('path')
          const { getMainWindow, mainWindowIsExist }=require('./windows/mainWindow')
          
          let tray=null
          const iconPath=path.resolve(__dirname, './assets/logo.png')
          
          function initTray() {
              tray=new Tray(iconPath)
          
              const contextMenu=Menu.buildFromTemplate([
                  {
                      label: '打開應用', click: ()=> {
                          mainWindowIsExist() && getMainWindow().show()
                      }
                  },
                  { label: '退出應用', click: ()=> { app.quit() } },
              ])
              
              tray.setToolTip('Harbour') // 設置鼠標懸停時顯示的提示信息
              tray.setContextMenu(contextMenu)
              
              tray.on('click', ()=> {
                  mainWindowIsExist() && getMainWindow().show()
              })
          }
          
          function getTray() {
              return tray
          }
          
          module.exports={ initTray, getTray }
          代碼解析
          1. iconPath獲取托盤圖標路徑,這里注意一定要使用path.resolve生產絕對路徑否則打包成安裝包后會無法找到該文件導致報錯
          2. Menu.buildFromTemplate是electron的一個方法,用來創建一個菜單,菜單的label是顯示的內容,click是點擊后觸發的事件
          3. tray.setToolTip('Harbour')是用來設置鼠標懸停時顯示的提示信息
          4. tray.setContextMenu(contextMenu)將使用Menu.buildFromTemplate創建出的菜單設置為托盤菜單
          5. tray.on('click', ()=> {}) 當點擊托盤的時候觸發的事件,我們這里是將mainWindowshow出來

          初始化系統托盤 系統托盤的初始化需要在app.on('ready')之后,因此我們將初始化系統托盤的方法封裝好導出,在app.on('ready')中執行

          javascript復制代碼const { app }=require('electron')
          const { createMainWindow }=require('./windows/mainWindow')
          const { initTray, getTray }=require('./systemTray')
          
          app.on('ready', ()=> {
              createMainWindow()
              initTray()
          })

          應用層和主進程通信(ipcMain,ipcRender)

          應用層和主進程之間的通信流程是:

          1. 應用層使用ipcRender.send方法將事件及數據傳遞到主進程
          2. 主進程使用ipcMain.on或者ipcMain.once方法監聽事件并獲取數據
          3. 主進程使用ipcMain.removeListener移除事件監聽或者ipcMain.removeAllListeners移除所有事件監聽
          4. 主進程使用窗口實例的webContents.send方法將事件和數據傳遞到應用層
          5. 應用層使用ipcRender.on或者ipcRender.once監聽事件并獲取數據
          6. 應用層使用ipcRenderer.removeListener移除事件監聽或者ipcRenderer.removeAllListeners移除所有事件監聽

          圖解如下


          將ipcRender,process注入到應用層

          我們知道ipcMain和ipcRender都是electron的Api,要想在應用層使用ipcRender就需要先將其注入到應用層,在electron中使用contextBridge.exposeInMainWorld方法將electron的Api注入到應用層,注入之后我們就可以在應用層的window上訪問注入的屬性。我們這里將ipcRender和process兩個屬性注入到應用層,分別用來實現通信和判斷當前運行環境。 封裝contextBridge.js文件

          javascript復制代碼const { contextBridge, ipcRenderer }=require('electron')
          
          /**
           * contextBridge.exposeInMainWorld的作用就是將主進程的某些API注入到渲染進程,
           * 供渲染進程使用(主進程并非所有的API或對象都能注入給渲染進程,需要參考文檔)
           * ipcRenderer 渲染進程通過window.ipcRenderer調用
           */
          contextBridge.exposeInMainWorld('ipcRenderer', {
              send: (channel, ...args)=> {
                  if (args?.length > 0) {
                      ipcRenderer.send(channel, ...args)
                  } else {
                      ipcRenderer.send(channel)
                  }
              },
              on: (channel, func)=> {
                  ipcRenderer.on(channel, func)
              },
              once: (channel, func)=> {
                  ipcRenderer.once(channel, func)
              },
              removeListener: (channel, func)=> {
                  ipcRenderer.removeListener(channel, func)
              },
              sendSync: (channel, ...args)=> {
                  if (args?.length > 0) {
                      return ipcRenderer.sendSync(channel, ...args)
                  } else {
                      return ipcRenderer.sendSync(channel)
                  }
              },
              invoke: (channel, ...args)=> {
                  try {
                      return ipcRenderer.invoke(channel, ...args)
                  } catch (error) {
                      console.error(`Error invoking API: ${channel}`, error)
                  }
              },
          })
          
          contextBridge.exposeInMainWorld('process', {
              NODE_ENV: process.env.NODE_ENV
          })

          這里我們將ipcRender的send,on,once,removeListener,sendSync,invoke方法及process.env.NODE_ENV注入到應用層,后續可在應用層進行使用 注意,該方法需要在應用層渲染時執行,因此我們剛好可以用到創建窗口中的option.webPreferences.preload來加載該文件,后續有案例代碼。

          應用層封裝注入的Api

          我們將ipcRender和process注入到應用層后,為了后期的維護我們可以將所有的方法再次進行封裝,放在一個統一的文件中, 封裝desktopUtils.ts

          typescript復制代碼declare global {
              interface Window {
                  ipcRenderer: {
                      send: (...args: any[])=> void,
                      on: (channel: string, listener: (...args: any[])=> void)=> void,
                      once: (channel: string, listener: (...args: any[])=> void)=> void,
                      removeListener: (channel: string, listener: (...args: any[])=> void)=> void,
                      sendSync: (...args: any[])=> any,
                      invoke: (...args: any[])=> Promise<any>,
                  },
                  process: {
                      NODE_ENV: 'development' | 'production'
                  }
              }
          }
          
          type ArgsType=string | number | boolean | { [key: string]: any } | any[]
          
          export const isDesktop=()=> {
              return !!window.ipcRenderer
          }
          
          export const getProcessNodeEnv=()=> {
              return window?.process.NODE_ENV
          }
          
          export const ipcRendererSend=(eventName: string, ...args: ArgsType[])=> {
              window.ipcRenderer?.send(eventName, ...args)
          }
          
          export const ipcRendererSendSync=(eventName: string, ...args: ArgsType[])=> {
              return window.ipcRenderer?.sendSync(eventName, ...args)
          }
          
          export const ipcRendererInvoke=(eventName: string, ...args: ArgsType[])=> {
              try {
                  return window.ipcRenderer?.invoke(eventName, ...args)
              } catch (error) {
                  console.error(`Error invoking IPC: ${eventName}`, error)
                  return null
              }
          }
          
          export const ipcRendererOn=(eventName: string, listener: (...args: ArgsType[])=> void)=> {
              window.ipcRenderer?.on(eventName, listener)
          }
          
          export const ipcRendererOnce=(eventName: string, listener: (...args: ArgsType[])=> void)=> {
              window.ipcRenderer?.once(eventName, listener)
          }
          
          export const ipcRendererRemoveListener=(eventName: string, listener: (...args: ArgsType[])=> void)=> {
              window.ipcRenderer?.removeListener(eventName, listener)
          }

          這里的isDesktop是用來判斷當前是否是桌面端的,因為很多時候我們使用electron開發的桌面端應用需要兼容web端,由于應用層代碼幾乎相同,我們只需要在一些情況下特別處理桌面端的邏輯即可。由于web端的window上一定沒有ipcRender這個屬性,因此可以根據window.ipcRenderer來判斷 getProcessNodeEnv是用來獲取當前桌面端的運行環境的,這里可以返回當前是開發環境還是生產環境,如果是web端的話,直接用process.env.NODE_ENV即可判斷

          應用層發送事件到主進程

          我們封裝好了方法之后就可以進行使用了,我們做一個簡單的案例,應用的header,有最小化,最大化,關閉,和恢復按鈕,在點擊時使用ipcRendererSend方法將事件傳給主進程并進行相應操作。 由于我們需要一個狀態判斷顯示最大化按鈕還是恢復按鈕因此需要監聽主進程在執行最大化和恢復之后傳回的事件和當前狀態。 desktopHeader.tsx

          tsx復制代碼import React, { memo, useState, useEffect } from 'react'
          import './desktopHeader.less'
          import SvgIcon from '@components/svgIcon'
          import {
              ipcRendererSend,
              ipcRendererOn,
              ipcRendererRemoveListener
          } from '@common/desktopUtils'
          import logoImage from '@assets/logo.png'
          
          function DesktopHeader() {
          
              const [windowIsMax, setWindowIsMax]=useState(false)
          
              useEffect(()=> {
                  const handleSetIsMax=(event: any, isMax: boolean)=> {
                      setWindowIsMax(isMax)
                  }
                  ipcRendererOn('mainWindowIsMax', handleSetIsMax)
                  return ()=> {
                      ipcRendererRemoveListener('mainWindowIsMax', handleSetIsMax)
                  }
              }, [])
          
              const handleWindow=(eventName: string)=> {
                  ipcRendererSend(`mainWindow-${eventName}`)
              }
          
              return (
                  <div className="desktop-header">
                      <div className="header-logo-box">
                          <img src={logoImage} alt="" />
                          <span>Harbour</span>
                      </div>
                      <div className="header-handle-box">
                          <div className="handle-icon-box" onClick={handleWindow.bind(this, 'min')}>
                              <SvgIcon
                                  svgName="min-icon"
                                  needPointer
                                  iconColor="#737780"
                                  iconSize={24}
                              />
                          </div>
                          {windowIsMax ? (
                              <div className="handle-icon-box" onClick={handleWindow.bind(this, 'restore')}>
                                  <SvgIcon
                                      svgName="restore-icon"
                                      needPointer
                                      iconColor="#737780"
                                      iconSize={24}
                                  />
                              </div>
                          ) : (
                              <div className="handle-icon-box" onClick={handleWindow.bind(this, 'max')}>
                                  <SvgIcon
                                      svgName="max-icon"
                                      needPointer
                                      iconColor="#737780"
                                      iconSize={24}
                                  />
                              </div>
                          )}
                          <div className="handle-icon-box handle-close-icon" onClick={handleWindow.bind(this, 'close')}>
                              <SvgIcon
                                  svgName="close-icon"
                                  needPointer
                                  hasHover
                                  iconColor="#737780"
                                  hoverColor="#fff"
                                  iconSize={24}
                              />
                          </div>
                      </div>
                  </div>
              )
          }
          
          export default memo(DesktopHeader)

          主進程接收事件并向應用層發送事件

          在主進程中我們使用ipcMain.on來監聽事件,并進行相應操作,并在相應操作之后在需要的時候發送事件到應用層。下面我們封裝mainWindow.js里面包含創建窗口方法,獲取窗口實例方法,獲取窗口是否存在方法及事件監聽方法 mainWindow.js

          javascript復制代碼const { BrowserWindow, ipcMain }=require('electron')
          const path=require('path')
          
          const isProduction=process.env.NODE_ENV==='development'
          let mainWindow=null
          
          function createMainWindow() {
              mainWindow=new BrowserWindow({
                  width: 1160,
                  height: 752,
                  minHeight: 632,
                  minWidth: 960,
                  show: false,
                  frame: false,
                  title: 'Harbour',
                  webPreferences: {
                      nodeIntegration: true,
                      preload: path.resolve(__dirname, '../utils/contextBridge.js')
                  },
                  icon: path.resolve(__dirname, '../assets/logo.png')
              })
          
              if (isDevelopment) {
                  mainWindow.loadURL('http://localhost:8000/')
              } else {
                  const entryPath=path.resolve(__dirname, '../../build/index.html')
                  mainWindow.loadFile(entryPath)
              }
          
              mainWindow.once('ready-to-show', ()=> {
                  mainWindow.show()
              })
          
              mainWindowListenEvents()
          }
          
          function mainWindowListenEvents() {
              ipcMain.on('mainWindow-min', ()=> {
                  mainWindowIsExist() && mainWindow.minimize()
              })
          
              ipcMain.on('mainWindow-max', ()=> {
                  if (mainWindowIsExist()) {
                      mainWindow.maximize()
                      mainWindow.webContents.send('mainWindowIsMax', true)
                  }
              })
          
              ipcMain.on('mainWindow-restore', ()=> {
                  if (mainWindowIsExist()) {
                      mainWindow.unmaximize()
                      mainWindow.webContents.send('mainWindowIsMax', false)
                  }
              })
          
              ipcMain.on('mainWindow-close', ()=> {
                  mainWindowIsExist() && mainWindow.hide()
              })
          
              ipcMain.on('mainWindow-open-devtool', ()=> {
                  mainWindowIsExist() && mainWindow.webContents.openDevTools()
              })
          }
          
          function mainWindowIsExist() {
              return mainWindow && !mainWindow.isDestroyed()
          }
          
          function getMainWindow() {
              return mainWindow
          }
          
          module.exports={
              getMainWindow,
              createMainWindow,
              mainWindowIsExist
          }

          窗口之間通信

          要實現窗口之間的通信,我們實際上就是使用應用層和主進程之間的通信,由于主進程可以接收到任意窗口發過來的事件,因此我們想實現窗口之間的通信,只需要在主進程中進行轉發就好,下面是圖解。

          javascript復制代碼// 主窗口
          ipcRendererSend('sendToSecond', '123')
          
          // 主進程
          ipcMain.on('sendToSecond', (e, data)=> {
            secondWindow.webContents.send('sendToSecond', data)
          })
          
          // 第二窗口
          ipcRendererOn('sendToSecond', handler)

          使用clipboard操作剪切板

          clipboard 是 Electron 提供的模塊之一,用于在應用程序中進行剪貼板操作。它允許你讀取和寫入系統剪貼板中的文本、圖像等數據。以下是一些常用的 clipboard 模塊方法 clipboard.writeText(text[, type]) 將文本寫入剪貼板。 text:要寫入剪貼板的文本內容。 type(可選):可指定數據類型,默認為 clipboard。可以是 selection(用于選區)或 clipboard(用于剪貼板)。

          javascript復制代碼const { clipboard }=require('electron')
          
          clipboard.writeText('Hello, World!')

          clipboard.readText([type]) 從剪貼板中讀取文本內容。 type(可選):可指定數據類型,默認為 clipboard。可以是 selection(用于選區)或 clipboard(用于剪貼板)。

          javascript復制代碼const { clipboard }=require('electron')
          
          const text=clipboard.readText()
          console.log(text)

          clipboard.writeHTML(markup[, type]) 將 HTML 內容寫入剪貼板。 markup:要寫入剪貼板的 HTML 內容。 type(可選):可指定數據類型,默認為 clipboard。可以是 selection(用于選區)或 clipboard(用于剪貼板)。

          javascript復制代碼const { clipboard }=require('electron')
          
          const html='<div><h1>Hello, World!</h1></div>'
          clipboard.writeHTML(html)

          clipboard.readHTML([type]) 從剪貼板中讀取 HTML 內容。 type(可選):可指定數據類型,默認為 clipboard。可以是 selection(用于選區)或 clipboard(用于剪貼板)。

          javascript復制代碼const { clipboard }=require('electron')
          
          const html=clipboard.readHTML()
          console.log(html)

          clipboard.writeImage(image[, type]) 將圖像寫入剪貼板。 image:要寫入剪貼板的圖像,可以是一個 nativeImage 對象或者一個文件路徑。 type(可選):可指定數據類型,默認為 clipboard。可以是 selection(用于選區)或 clipboard(用于剪貼板)。

          javascript復制代碼const { clipboard, nativeImage }=require('electron')
          const image=nativeImage.createFromPath('/path/to/image.png')
          
          clipboard.writeImage(image)

          clipboard.readImage([type]) 從剪貼板中讀取圖像。 type(可選):可指定數據類型,默認為 clipboard。可以是 selection(用于選區)或 clipboard(用于剪貼板)。

          javascript復制代碼const { clipboard }=require('electron')
          
          const image=clipboard.readImage()

          clipboard.clear([type]) 清空剪貼板內容。 type(可選):可指定數據類型,默認為 clipboard。可以是 selection(用于選區)或 clipboard(用于剪貼板)。

          javascript復制代碼const { clipboard }=require('electron')
          
          clipboard.clear()

          使用dialog創建常用對話框

          dialog 是 Electron 提供的模塊之一,用于在桌面應用程序中創建對話框,以便與用戶進行交互。它可以用于打開文件、保存文件、顯示警告、錯誤等信息,以及進行用戶輸入的獲取等操作。以下是一些常用的 dialog 模塊方法

          打開文件對話框

          dialog.showOpenDialog([browserWindow, ]options)打開一個文件選擇對話框,允許用戶選擇一個或多個文件。

          1. browserWindow(可選):父窗口的引用。如果不傳遞此參數,對話框將會成為一個模態窗口。
          2. options
          3. :配置對象,可以包含以下屬性:
            1. defaultPath:字符串,指定對話框的默認路徑。
            2. filters:數組,定義文件類型過濾器。
            3. properties:數組,包含 openFile、openDirectory、multiSelections 等屬性,決定對話框的行為。
          javascript復制代碼const { dialog }=require('electron');
          
          const options={
            title: '選擇文件',
            defaultPath: '/path/to/default/folder',
            filters: [
              { name: 'Text Files', extensions: ['txt', 'text'] },
              { name: 'All Files', extensions: ['*'] }
            ],
            properties: ['openFile', 'multiSelections']
          };
          
          dialog.showOpenDialog(null, options).then(result=> {
            console.log(result.filePaths)
          }).catch(err=> {
            console.log(err)
          })

          保存文件對話框

          dialog.showSaveDialog([browserWindow, ]options)打開一個文件保存對話框,允許用戶選擇保存的路徑和文件名。

          1. browserWindow(可選):父窗口的引用。
          2. options
          3. :配置對象,可以包含以下屬性:
            1. defaultPath:字符串,指定對話框的默認路徑。
            2. filters:數組,定義文件類型過濾器。
          javascript復制代碼const { dialog }=require('electron');
          
          const options={
            title: '保存文件',
            defaultPath: '/path/to/default/folder',
            filters: [
              { name: 'Text Files', extensions: ['txt', 'text'] },
              { name: 'All Files', extensions: ['*'] }
            ]
          };
          
          dialog.showSaveDialog(null, options).then(result=> {
            console.log(result.filePath);
          }).catch(err=> {
            console.log(err);
          });

          普通消息對話框

          dialog.showMessageBox([browserWindow, ]options)顯示一個消息框,通常用于警告或者通知用戶。

          1. browserWindow(可選):父窗口的引用。
          2. options
          3. :配置對象,可以包含以下屬性:
            1. type:可以是 'none'、'info'、'error'、'question'、'warning',決定消息框的類型。
            2. title:字符串,消息框的標題。
            3. message:字符串,要顯示的消息文本。
            4. buttons:數組,包含消息框的按鈕,例如 ['Yes', 'No', 'Cancel']。
            5. defaultId:數字,指定默認選擇的按鈕索引。
          javascript復制代碼const { dialog }=require('electron');
          
          const options={
            type: 'info',
            title: '信息',
            message: '這是一個信息框。',
            buttons: ['OK']
          };
          
          dialog.showMessageBox(null, options).then(result=> {
            console.log(result.response);
          }).catch(err=> {
            console.log(err);
          });

          錯誤消息對話框

          dialog.showErrorBox(title, content)顯示一個錯誤框,用于顯示錯誤信息。

          1. title:字符串,對話框標題。
          2. content:字符串,要顯示的錯誤內容。
          javascript復制代碼const { dialog }=require('electron');
          
          dialog.showErrorBox('發生錯誤', '這是一個錯誤框的示例。');

          使用globalShortcut注冊全局快捷鍵

          globalShortcut 是 Electron 提供的模塊之一,用于注冊和響應全局鍵盤快捷鍵。這允許你在你的 Electron 應用程序中創建全局快捷鍵,以執行特定操作或觸發事件。以下是一些常用的 globalShortcut 模塊方法 globalShortcut.register(accelerator, callback)注冊全局快捷鍵。

          1. accelerator:字符串,表示要注冊的快捷鍵,如 "CmdOrCtrl+X"。
          2. callback:當快捷鍵被觸發時要執行的回調函數。
          javascript復制代碼const { globalShortcut }=require('electron');
          
          globalShortcut.register('CmdOrCtrl+X', ()=> {
            // 執行某些操作
          });

          globalShortcut.isRegistered(accelerator)檢查是否已經注冊了指定的全局快捷鍵。 accelerator:要檢查的快捷鍵。

          javascript復制代碼const { globalShortcut }=require('electron');
          
          const isRegistered=globalShortcut.isRegistered('CmdOrCtrl+X');
          if (isRegistered) {
            console.log('已注冊');
          } else {
            console.log('未注冊');
          }

          globalShortcut.unregister(accelerator)注銷已注冊的全局快捷鍵。 accelerator:要注銷的快捷鍵。

          javascript復制代碼const { globalShortcut }=require('electron');
          
          globalShortcut.unregister('CmdOrCtrl+X');

          globalShortcut.unregisterAll()注銷所有已注冊的全局快捷鍵。

          javascript復制代碼const { globalShortcut }=require('electron');
          
          globalShortcut.unregisterAll();

          globalShortcut.getRegisteredKeys()獲取當前已注冊的全局快捷鍵的列表。

          javascript復制代碼const { globalShortcut }=require('electron');
          
          const registeredKeys=globalShortcut.getRegisteredKeys();
          console.log(registeredKeys);

          使用Menu創建原生應用菜單和上下文菜單

          Menu 是 Electron 中用于創建和管理應用程序菜單的模塊。它允許你在應用程序的菜單欄、上下文菜單等位置定義菜單項,以便用戶可以通過點擊菜單項執行特定的操作。以下是一些常用的 Menu 模塊方法和屬性

          創建菜單

          創建頂級菜單
          javascript復制代碼const { Menu }=require('electron');
          
          const template=[
            {
              label: 'File',
              submenu: [
                { role: 'openFile' },
                { role: 'saveFile' },
                { role: 'quit' }
              ]
            },
            {
              label: 'Edit',
              submenu: [
                { role: 'copy' },
                { role: 'paste' }
              ]
            }
          ];
          
          const menu=Menu.buildFromTemplate(template);
          Menu.setApplicationMenu(menu)
          創建上下文菜單
          javascript復制代碼const { Menu }=require('electron');
          
          const template=[
            { role: 'cut' },
            { role: 'copy' },
            { role: 'paste' }
          ];
          
          const contextMenu=Menu.buildFromTemplate(template);

          模板數組菜單和應用程序菜單

          Menu.buildFromTemplate(template)從模板數組創建一個菜單。 template:一個包含菜單項的數組。每個菜單項都是一個對象,包含 label、click 等屬性。 Menu.setApplicationMenu(menu)設置應用程序菜單,通常用于頂級菜單欄。 menu:要設置為應用程序菜單的 Menu 對象。

          菜單屬性項

          1. label:字符串,菜單項顯示的文本。
          2. accelerator:字符串,可以是組合鍵,例如 'CmdOrCtrl+X'。
          3. click:函數,當菜單項被點擊時要執行的回調函數。
          4. role:內置角色,例如 'copy'、'paste',會觸發預定義的操作。
          5. submenu:一個子菜單,包含一個子菜單的 Menu 對象。
          javascript復制代碼const template=[
            {
              label: 'File',
              submenu: [
                { label: 'Open', accelerator: 'CmdOrCtrl+O', click: ()=> { /* 打開文件 */ } },
                { role: 'save' },
                { type: 'separator' }, // 分隔線
                { role: 'quit' }
              ]
            },
            {
              label: 'Edit',
              submenu: [
                { role: 'copy' },
                { role: 'cut' },
                { role: 'paste' },
                {
                  label: 'Select All',
                  accelerator: 'CmdOrCtrl+A',
                  click: ()=> { /* 選擇所有內容 */ }
                }
              ]
            }
          ];

          菜單常用方法

          1. menu.append(menuItem)在菜單的末尾追加一個菜單項。
          2. menu.insert(position, menuItem)在指定位置插入一個菜單項。
          3. menu.getMenuItemById(id)根據菜單項的 id 獲取菜單項。
          4. menu.popup([options])
          5. 在指定位置彈出菜單。
            1. options:一個對象,可以包含 x 和 y 屬性,表示彈出菜單的位置。

          使用nativeImage 處理圖像

          nativeImage 是 Electron 提供的模塊之一,用于處理圖像。它可以加載圖像文件、從屏幕截取圖像、創建空白圖像等。nativeImage 支持跨平臺,可以在主進程和渲染進程中使用。

          1. nativeImage.createEmpty()創建一個空白圖像對象。
          javascript復制代碼const { nativeImage }=require('electron');
          
          const emptyImage=nativeImage.createEmpty();
          1. nativeImage.createFromPath(path[, options])
          2. 從文件路徑創建圖像對象。
            1. path:字符串,圖像文件的路徑。
            2. options(可選):一個包含 width 和 height 屬性的對象,可以指定圖像的寬度和高度。
          javascript復制代碼const { nativeImage }=require('electron');
          
          const imagePath='/path/to/image.png';
          const image=nativeImage.createFromPath(imagePath);
          1. nativeImage.createFromBuffer(buffer[, options])
          2. 從緩沖區創建圖像對象。
            1. buffer:一個包含圖像數據的 Buffer 對象。
            2. options(可選):一個包含 width 和 height 屬性的對象,可以指定圖像的寬度和高度。
          javascript復制代碼const { nativeImage }=require('electron');
          
          const fs=require('fs');
          const buffer=fs.readFileSync('/path/to/image.png');
          const image=nativeImage.createFromBuffer(buffer);
          1. nativeImage.createFromDataURL(dataURL)從數據 URL 創建圖像對象。
          2. dataURL:一個包含圖像數據的數據 URL。
          javascript復制代碼const { nativeImage }=require('electron');
          
          const dataURL='data:image/png;base64,iVBORw0KGg...';
          const image=nativeImage.createFromDataURL(dataURL);
          1. nativeImage.createFromNamedImage(imageName[, hslShift])
          2. 從系統的命名圖像創建圖像對象。
            1. imageName:一個包含系統命名圖像的字符串,例如 'NSStopProgressTemplate'。
            2. hslShift(可選):一個包含 h, s, l 屬性的對象,用于對圖像進行顏色調整。
          javascript復制代碼const { nativeImage }=require('electron');
          
          const image=nativeImage.createFromNamedImage('NSStopProgressTemplate', { h: 0, s: 0, l: 0 });
          1. nativeImage.createThumbnailFromPath(path, size[, callback])
          2. 從文件路徑創建縮略圖。
            1. path:字符串,圖像文件的路徑。
            2. size:一個包含 width 和 height 屬性的對象,指定縮略圖的寬度和高度。
            3. callback(可選):一個回調函數,用于接收縮略圖的 nativeImage 對象。
          javascript復制代碼const { nativeImage }=require('electron');
          
          const imagePath='/path/to/image.png';
          const size={ width: 100, height: 100 };
          
          nativeImage.createThumbnailFromPath(imagePath, size, (thumbnail)=> {
            console.log(thumbnail);
          });
          1. nativeImage.isMacTemplateImage(image)
          2. 檢查圖像是否是 macOS 模板圖像。
            1. image:要檢查的 nativeImage 對象。
          javascript復制代碼const { nativeImage }=require('electron');
          
          const imagePath='/path/to/image.png';
          const image=nativeImage.createFromPath(imagePath);
          
          console.log(nativeImage.isMacTemplateImage(image)); // true or false
          1. nativeImage.toDataURL([options])
          2. 將圖像轉換為數據 URL。
            1. options(可選):一個包含 scaleFactor 屬性的對象,用于指定縮放因子。
          javascript復制代碼const { nativeImage }=require('electron');
          
          const imagePath='/path/to/image.png';
          const image=nativeImage.createFromPath(imagePath);
          
          const dataURL=image.toDataURL({ scaleFactor: 2.0 });

          使用screen獲取屏幕信息

          screen 是 Electron 提供的模塊之一,用于獲取有關屏幕和顯示器的信息,以及執行與屏幕相關的操作。以下是一些常用的 screen 模塊方法和屬性:

          1. screen.getPrimaryDisplay():獲取主顯示器的信息,包括位置、大小等。
          2. screen.getAllDisplays():返回所有連接的顯示器的信息,以數組形式返回。
          3. screen.getDisplayNearestPoint(point):返回距離指定點最近的顯示器的信息。
          4. screen.getDisplayMatching(rect):返回與指定矩形相交的顯示器的信息。
          5. screen.getCursorScreenPoint():返回鼠標指針當前所在的屏幕坐標。
          6. screen.getMenuBarHeight(): 返回菜單欄的高度。
          7. screen.getPrimaryDisplay().workArea:返回主顯示器工作區信息,即屏幕去除菜單欄等系統區域的區域。
          8. screen.getPrimaryDisplay().bounds:返回主顯示器整體信息,包括整個顯示器的坐標和尺寸。
          9. screen.getPrimaryDisplay().size:返回主顯示器的分辨率,即屏幕的寬度和高度。
          10. screen.on(event, callback):監聽屏幕相關事件,例如 display-added、display-removed 等,允許你在顯示器變化時進行相應的操作。

          應用層預留接口打開devtool

          在我們開發過程中,需要經常用的控制臺,而我們在開發時直接打開控制臺又有些不友好。還有生產環境中我們有時也需要打開控制臺定位一些問題,但是生產環境又不能那么輕易讓用戶能打開控制臺,因此我們可以在開發環境和生產環境分別預留一個接口打開控制臺,生產環境的方式要復雜一些實現思路 我們可以在應用層監聽鍵盤事件,當在開發環境中,按下ctrl + F12時,我們就打開控制臺。 而在生產環境中我們需要設計的復雜一些,可以在代碼中放一個不顯示的輸入框(設置寬高邊框均為0,并且固定定位就好),當按下特殊組合鍵時,聚焦輸入框,并輸入openDevtool之后打開控制臺,我這里設置的組合鍵為ctrl + win + alt + F12 代碼如下

          javascript復制代碼import React, { useEffect, useRef, useCallback, ChangeEvent } from 'react'
          import DesktopHeader from '@components/desktopHeader'
          import './app.less'
          import {
              isDesktop,
              getProcessNodeEnv,
              ipcRendererSend
          } from '@common/desktopUtils'
          import electronImg from '@assets/electronImg.png'
          
          function App() {
          
              const openDevtoolInput=useRef<HTMLInputElement>(null)
              const isDevelopment=useRef(getProcessNodeEnv()==='development')
          
              const openDevtool=useCallback(()=> {
                  ipcRendererSend('mainWindow-open-devtool')
              }, [])
          
              const openDevtoolInputChange=(event: ChangeEvent<HTMLInputElement>)=> {
                  const { value }=event.target
                  if (value==='openDevtool') {
                      openDevtool()
                  }
              }
          
              useEffect(()=> {
                  document.addEventListener('keydown', (e)=> {
                      const { ctrlKey, metaKey, altKey, key }=e
                      // 開發環境使用ctrl + F12打開控制臺
                      if (isDevelopment.current && ctrlKey && key==='F12') {
                          openDevtool()
                      }
                      // 開發環境使用ctrl + win + alt + F12,然后鍵入'open devtool'打開控制臺
                      if (!isDevelopment.current && ctrlKey && metaKey && altKey && key==='F12') {
                          if (openDevtoolInput.current) {
                              openDevtoolInput.current.focus()
                          }
                      }
                  })
              }, [openDevtool])
          
              return (
                  <div id="electron-app">
                      {!isDevelopment.current && (
                          <input
                              className="open-devtool-input"
                              ref={openDevtoolInput}
                              type="text"
                              onChange={openDevtoolInputChange}
                              onBlur={(e)=> { e.target.value='' }}
                          />
                      )}
                      {isDesktop() && <DesktopHeader />}
                      <div className={isDesktop() ? 'desktop-app-content' : 'app-content'}>
                          <div className="electron-img">
                              <img src={electronImg} alt="" />
                          </div>
                      </div>
                  </div>
              )
          }
          
          export default App
          在主進程監聽該事件并打開控制臺
          javascript復制代碼ipcMain.on('mainWindow-open-devtool', ()=> {
              mainWindowIsExist() && mainWindow.webContents.openDevTools()
          })

          作者:Harbour 鏈接:https://juejin.cn/post/7277799192961925172 來源:稀土掘金 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

          語:有時候在電腦上網的時候看見自己喜歡的網站或特別的頁面,自己已經點擊電腦屏幕上方的星號收藏這個頁面了,但有時候看收藏夾又被密密麻麻的內容弄得頭昏眼花,這時,我的處理方法是把收藏的頁面導出來,新建文件夾分類整理好,這樣下次想看什么內容就不用只看收藏夾里小小的字,而是在文件夾中能夠清楚的,按照自己的習慣,分好類別的,整潔的頁面了。下面是具體操作,根據提示的1、2、3······進行操作,就可以了。

          首先打開瀏覽器。找到自己喜歡的頁面,我打開了自己的收藏夾,找到自己喜歡的頁面。然后點擊文字跳轉到具體的那個喜歡的頁面。

          然后點擊屏幕上方,瀏覽器的三個點“···”。

          然后點擊“更多工具”。

          然后點擊“將頁面另存為”。

          選擇文件夾,把頁面保存下來。

          最后打開電腦你保存文件在什么地方,就能在該地方找到保存的喜歡頁面,該頁面在文件夾里保存的形式為“html”文件。

          公司去年對 CDN 資源服務器進行了遷移,由原來的通過 FTP 方式的文件存儲改為了使用 S3 協議上傳的對象存儲,部門內 @柴俊堃 同學開發了一個命令行腳本工具 RapidTrans(睿傳),使用睿傳可以很方便將本地目錄下的資源上傳到 S3 中。

          睿傳運行時接收兩個主要參數,一個為待上傳的本地路徑,一個為上傳到 CDN 后的路徑,我們可以在項目的 package.json 中去配置 scripts執行上傳。

          npm run rapid-trans -- -s "/home/demo/work/mall2016/release/列表頁" -p "2016/m/list"

          用了一段時間后覺得如果選擇本地路徑的時候可以通過可視化的文件選擇器的方式選擇就太好了,團隊一直在做客戶端方向技術的儲備,所以為了更方便團隊的使用產生了將睿傳封裝成 GUI 的跨平臺客戶端的想法。

          客戶端界面

          功能分析

          • 桌面客戶端,支持 WindowsMac 系統
          • 本地路徑可以通過文件對話框或拖拽的方式進行選擇
          • CDN 路徑可以通過輸入框的方式輸入
          • 上傳成功后將當前選擇的本地路徑和 CDN 的映射關系存儲,下次再選擇到當前目錄的話直接使用之前 CDN 的路徑地址,無需再次輸入
          • S3 參數配置化
          • 自動升級
          • 覆蓋上傳

          技術選型

          • Electron
          • Vue
          • LowDB

          Electron 簡介

          Electron 是由 Github 開發,基于 Chromium 和 Node.js, 讓你可以使用 HTML, CSS 和 JavaScript 構建跨平臺桌面應用的開源框架。

          Electron 可以讓你使用純 JavaScript 調用豐富的原生(操作系統) APIs 來創造桌面應用。 你可以把它看作一個專注于桌面應用的 Node. js 的變體,而不是 Web 服務器。

          簡單點說,用 Electron 可以讓我們在網頁中使用 Node.js 的 API 和調用系統 API。

          Vue + Electron 環境搭建

          使用 vue-cli 腳手架和 electron-vue模板進行搭建,此處需要注意,由于 electron-vue 模板不支持 vue-cli@3.0,所以要使用 2.0 版本。

          # 安裝 vue-cli@2.0,若已安裝則無需重復安裝
          npm install -g vue-cli
          vue init simulatedgreg/electron-vue s3_upload_tool
          
          # 安裝依賴并運行
          cd s3_upload_tool
          npm install
          npm run dev

          目錄結構

          ├─ .electron-vue
          │  ├─ webpack.main.config.js
          │  ├─ webpack.renderer.config.js
          │  └─ webpack.web.config.js
          ├─ build
          │  └─ icons/
          ├─ dist
          │  ├─ electron/
          │  └─ web/
          ├─ node_modules/
          ├─ src
          │  ├─ main
          │  │  ├─ index.dev.js
          │  │  └─ index.js
          │  ├─ renderer
          │  │  ├─ components/
          │  │  ├─ router/
          │  │  ├─ store/
          │  │  ├─ App.vue
          │  │  └─ main.js
          │  └─ index.ejs
          ├─ static/
          ├─ .babelrc
          ├─ .eslintignore
          ├─ .eslintrc.js
          ├─ .gitignore
          ├─ package.json
          └─ README.md

          應用的目錄結構和平常我們用 Vue 做 WEB 端時生成的結構基本差異不大,所以本文我只介紹下與 Web 不同的幾個目錄。

          .electron-vue

          該目錄下包含 3 個獨立的 Webpack 配置文件

          • .electron-vue/webpack.main.config.js 針對于 Electron 的 main 主進程打包的配置,配置比較簡單,主要就是將 src/main/index.js 通過 babel-loader 打包,并且生成 commonjs2模塊規范。
          • .electron-vue/webpack.renderer.config.js 針對于 Electron 的 renderer 渲染進程打包的配置,此配置主要用來打包 Vue 的應用程序,這個配置就和平常我們做 Web 端時 Webpack 的配置基本一樣,處理 Vue、Sass、Image、Html等。
          • .electron-vue/webpack.web.config.js 為瀏覽器構建 render渲染進程的配置,主要針對于發布到 Web 的情況。

          src/main

          主進程代碼存放位置,涉及到調取 Node API 、調用原生系統功能的代碼。

          src/renderer

          渲染進程代碼存放位置,和平常的 Vue 項目基本一樣。

          主進程與渲染進程

          在 Electron 中有兩個進程,分別為主進程渲染進程,主進程負責 GUI 部分,渲染進程負責頁面的展示。

          主進程

          • 主進程通常是在 package.jsonmain字段的腳本進程。
          • 一個 Electron 應用只有一個主進程。
          • 主進程一般用來處理 App 生命周期、系統事件的處理、系統原生GUI。

          main.js

          const { app, BrowserWindow }=require('electron')
          
          function createWindow () {   
            // 創建瀏覽器窗口
            let win=new BrowserWindow({ width: 800, height: 600 })
          
            // 然后加載 app 的 index.html.
            win.loadFile('index.html')
          }
          
          app.on('ready', createWindow)

          渲染進程

          index.html

          <!DOCTYPE html>
          <html>
            <head>
              <meta charset="UTF-8">
              <title>Hello World!</title>
            </head>
            <body>
              <h1>Hello World!</h1>
            </body>
          </html>

          主進程使用 BrowserWindow 實例創建頁面。 每個BrowserWindow實例都在自己的渲染進程里運行頁面。 當一個 BrowserWindow 實例被銷毀后,相應的渲染進程也會被終止。

          主進程和渲染進程通訊

          進程間通信(IPC,Interprocess communication)是一組編程接口,讓開發者能夠協調不同的進程,使之能在一個操作系統里同時運行,并相互傳遞、交換信息。

          Electron 使用 IPC 的機制,由主進程來創建應用,渲染進程來負責繪制頁面,而兩個進程之間是無法直接通信的。

          渲染進程通過ipcRenderer向主進程發送消息,主進程通過 ipcMain監聽事件,當事件響應時對消息進行處理。

          主進程監聽事件的回調函數中會存在 event 對象及arg 對象。arg 對象為渲染進程傳遞過來的參數。

          如果主進程執行的是同步方法,回復同步信息時,需要設置event.returnValue,如果執行的是異步方法回復時需要使用 event.sender.send向渲染進程發送消息。

          下面代碼為渲染進程主動向主進程發送消息,在主進程接收后回復渲染進程。

          // 主進程
          const { ipcMain }=require('electron')
          ipcMain.on('asynchronous-message', (event, arg)=> {
            console.log(arg) // prints "ping"
            event.sender.send('asynchronous-reply', 'pong')
          })
          
          ipcMain.on('synchronous-message', (event, arg)=> {
            console.log(arg) // prints "ping"
            event.returnValue='pong'
          })
          
          // 渲染器進程
          const { ipcRenderer }=require('electron')
          console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"
          
          ipcRenderer.on('asynchronous-reply', (event, arg)=> {
            console.log(arg) // prints "pong"
          })
          ipcRenderer.send('asynchronous-message', 'ping')

          有時候我們也需要由主進程主動向渲染進程發送消息,面對這種情況我們可以在主進程中通過 BrowserWindow對象的 webContets.send方法向渲染進程發送消息。

          // 主進程
          
          const { app, BrowserWindow }=require('electron')
          
          function createWindow () {   
            let win=new BrowserWindow({ width: 800, height: 600 })
            win.loadFile('index.html')
            
            // 向渲染進程發送消息
            win.webContents.send('main-process-message', 'ping')
          }
          
          app.on('ready', createWindow)
          
          // 渲染器進程
          
          const { ipcRenderer }=require('electron')
          // 監聽主進程發送的消息
          ipcRenderer.on('main-process-message', (event, arg)=> {
            console.log(arg) // prints "ping"
          })

          持久化存儲

          在桌面端應用中一些用戶設置通常需要進行存持久化存儲,方便以后使用的時候獲取。 我們做 Web 時候通常是使用像 MySQLMongodb等數據庫進行持久化存儲, 但是當用戶安裝桌面軟件時候不可能讓用戶在本地安裝這類數據庫,所以我們需要一個輕量級的本地化數據庫。

          lowdb 是一個基于 Lodash API 的輕量級本地 JSON 數據庫,支持 Node.jsbrowserElectron

          在我們要開發的工具中,用戶的 S3 配置,已上傳文件的 CDN目錄等信息是需要進行持久化存儲的,所有我們采用的 lowdb進行數據的存儲。

          使用也是非常的簡單,數據的讀寫和平常使用 Lodash差不多。

          安裝

          npm install lowdb -save

          數據存儲路徑

          Electron 提供了獲取系統目錄的方法,可以很方便的進行一些系統目錄的獲取。

          const { app, remote }=require('electron')
          
          app.getPath('home'); // 獲取用戶的 home 文件夾(主目錄)路徑
          app.getPath('userData'); // 獲取當前用戶的應用數據文件夾路徑
          app.getPath('appData'); // 獲取應用程序設置文件的文件夾路徑,默認是 appData 文件夾附加應用的名稱
          app.getPath('temp'); // 獲取臨時文件夾路徑
          app.getPath('documents'); // 獲取用戶文檔目錄的路徑
          app.getPath('downloads'); // 獲取用戶下載目錄的路徑
          app.getPath('music'); // 獲取用戶音樂目錄的路徑
          app.getPath('pictures'); // 獲取用戶圖片目錄的路徑
          app.getPath('videos'); // 獲取用戶視頻目錄的路徑
          app.getPath('logs'); // 獲取應用程序的日志文件夾路徑
          app.getPath('desktop'); // 獲取系統桌面路徑

          數據庫配置

          'use strict'
          
          const DataStore=require('lowdb')
          const FileSync=require('lowdb/adapters/FileSync')
          const path=require('path')
          const fs=require('fs-extra')
          const { app, remote }=require('electron')
          
          const APP=process.type==='renderer' ? remote.app : app
          
          const STORE_PATH=APP.getPath('userData') // 將數據庫存放在當前用戶的應用數據文件夾
          
          if (process.type !=='renderer') {
            if (!fs.pathExistsSync(STORE_PATH)) {
              fs.mkdirpSync(STORE_PATH)
            }
          }
          
          const adapter=new FileSync(path.join(STORE_PATH, '/data.json'))
          
          const db=DataStore(adapter)
          
          // 初始化默認數據
          db.defaults({
            project: [], // 存儲已上傳項目的 CDN 配置信息
            settings: {
              ftp: '', // ftp 用戶配置
              s3: '', // s3 用戶配置
            }
          }).write()
          
          module.exports=db
          

          后臺執行命令行程序

          由于睿傳是一個命令行工具,并沒有對外提供 Node.js API,所以用戶點擊上傳按鈕時候需要通過 Electron在后臺運行命令行程序,并且將命令行運行的日志實時渲染到應用的日志界面中,所以在這里利用 Node.jschild_process子進程的方式來處理。

          'use strict'
          import { ipcMain } from 'electron'
          import { exec } from 'child_process'
          import path from 'path'
          import fixPath from 'fix-path'
          import { logError, logInfo, logExit } from './log'
          const cmdPath=path.resolve(__static, 'lib/rapid_trans') // 睿傳路徑
          let workerProcess
          ipcMain.on('upload', (e, {dirPath, cdnPath, isCover})=> {
            runUpload(dirPath, cdnPath, isCover)
          })
          
          function runUpload (dirPath, cdnPath, isCover) {
            let cmdStr=`node src/rapid-trans.js -s "${dirPath}" -p "${cdnPath}" -q`
            if (isCover) {
              cmdStr +=' -f'
            }
            fixPath()
            logInfo('==================開始上傳==================\n')
            workerProcess=exec(cmdStr, {
              cwd: cmdPath
            })
            workerProcess.stdout.on('data', function (data) {
              logInfo(data)
            })
          
            workerProcess.stderr.on('data', function (data) {
              logError(data)
            })
          
            workerProcess.on('close', function (code) {
              logExit(code)
              logInfo('==================上傳結束==================\n')
            })
          }
          
          // log.js
          'use strict'
          const win=global.mainWindow
          export function logInfo (msg) {
            win.webContents.send('logInfo', msg)
          }
          
          export function logError (msg) {
            win.webContents.send('logError', msg)
          }
          
          export function logExit (msg) {
            win.webContents.send('logExit', msg)
          }
          
          export default {
            logError,
            logExit,
            logInfo
          }

          應用打包

          應用開發完成后需要進行打包,我們可以使用 electron-builder 將應用打包成 Windows、Mac 平臺的應用。

          在執行npm run build之前需要在 package.json進行打包配置的編輯。

          {
            "build": {
              "productName": "S3上傳工具",  // 應用名稱,最終生成的可執行文件的名稱
              "appId": "com.autohome.s3", // 應用 APP.ID
              "directories": {
                "output": "build" // 打包后的輸出目錄
              },
              "asar": false, // 關閉 asar 格式
              "publish": [
                {
                  "provider": "generic", // 服務器提供商
                  "url": "http://xxx.com:8003/oss" // 更新服務器地址
                }
              ],
              "releaseInfo": {
                "releaseNotes": "新版更新" // 更新說明
              },
              "files": [
                "dist/electron/**/*",
                {
                  "from": "dist/electron/static/lib/rapid_trans/node_modules",
                  "to": "dist/electron/static/lib/rapid_trans/node_modules"
                } // 將睿傳的依賴打包進應用
              ],
              // 平臺的一些配置
              "dmg": {
                "contents": [
                  {
                    "x": 410,
                    "y": 150,
                    "type": "link",
                    "path": "/Applications"
                  },
                  {
                    "x": 130,
                    "y": 150,
                    "type": "file"
                  }
                ]
              },
              // 應用圖標
              "mac": {
                "icon": "build/icons/icon.icns"
              },
              "win": {
                "icon": "build/icons/icon.ico"
              },
              "linux": {
                "icon": "build/icons"
              }
            }
          }

          應用更新提示

          由于軟件不進行 App Store 的上架,只在團隊內部使用沒有配置證書,不配置證書的話 Mac 中無法進行自動更新安裝,所以我們在檢測到用戶的當前版本不是最新版本的時候是采用的彈層提示的方式讓用戶自己下載。

          使用 electron-updater 打包的應用自動更新非常方便,將打包后 build 目錄下的 latest-mac.yml文件上傳至package.json 中配置的 publish.url 目錄下,并且在主進程文件中監聽 update-availabl事件。

          // 主進程 main.js
          import { autoUpdater } from 'electron-updater'
          // 關閉自動下載
          autoUpdater.autoDownload=false
          
          // 應用可更新
          autoUpdater.on('update-available', (info)=> {
            // 通知渲染進程應用需要更新
            mainWindow.webContents.send('updater', info)
          })
          
          app.on('ready', ()=> {
            if (process.env.NODE_ENV==='production') autoUpdater.checkForUpdates()
          })
          
          // 渲染進程 updater.js
          import { ipcRenderer, shell } from 'electron'
          import { MessageBox } from 'element-ui'
          
          ipcRenderer.on('updater', (e, info)=> {
            MessageBox.alert(info.releaseNotes, `請升級${info.version}版本`, {
              confirmButtonText: '立即升級',
              showClose: false,
              closeOnClickModal: false,
              dangerouslyUseHTMLString: true,
              callback (action) {
                if (action==='confirm') {
                  // 在用戶的默認瀏覽器中打開存放應用安裝包的網絡地址
                  shell.openExternal('http://10.168.0.49/songjinda/s3_tool/download/')
                  return false
                }
              }
            })
          })
          


          來源:微信公眾號:之家技術

          出處:https://mp.weixin.qq.com/s/ttRU7VtgGknzEmeEkObK-g


          主站蜘蛛池模板: 国产在线观看精品一区二区三区91| 亚洲AV无码一区二区三区鸳鸯影院| 99久久无码一区人妻a黑| 亚洲AV无码一区二区三区电影| 国产在线观看一区二区三区四区| 色妞色视频一区二区三区四区| 精品一区二区三区高清免费观看| 日韩欧美一区二区三区免费观看| 国产精品亚洲高清一区二区| 亚洲AV香蕉一区区二区三区| 国产精品无码一区二区三级| 一区二区三区杨幂在线观看| 精品国产免费观看一区| 日韩免费一区二区三区在线 | 国产一区二区精品久久凹凸 | 一区二区手机视频| 在线电影一区二区| 蜜桃AV抽搐高潮一区二区| 无码人妻一区二区三区免费n鬼沢 无码人妻一区二区三区免费看 | 伊人久久精品一区二区三区| 麻豆一区二区在我观看| 亚洲一区精品中文字幕| 亚洲一区二区三区高清| 久久精品午夜一区二区福利 | 精品人无码一区二区三区| 国产成人无码精品一区二区三区 | 久久精品一区二区三区AV| 国产日韩精品一区二区三区在线| 亚洲中文字幕丝袜制服一区 | 久久人妻内射无码一区三区| 精品一区二区三区中文字幕| 无码精品视频一区二区三区| 久久se精品动漫一区二区三区| 亚洲伦理一区二区| 欧洲无码一区二区三区在线观看| 日韩精品无码一区二区三区 | 搡老熟女老女人一区二区| 射精专区一区二区朝鲜| 亚洲日韩一区二区三区| 另类ts人妖一区二区三区| 欧洲精品一区二区三区在线观看 |