乐趣区

关于前端:我的第一个Electron应用

hello,好久不见,最近笔者花了几天工夫入门 Electron,而后做了一个非常简单的利用,本文就来给各位分享一下过程,Electron大佬请随便~

笔者开源了一个 Web 思维导图,尽管借助 showSaveFilePickerapi能够间接操作电脑本地文件,但终归不能离线应用,所以就萌生了做一个客户端的想法,作为一个只会前端的废物,做客户端,Electron显然是最好的抉择,不过毛病也很显著,安装包体积比拟大,如果你对此比拟介意的话能够尝试 tauri。

笔者的需要很简略,能新建、关上本地文件进行编辑,另外能查看最近编辑过的文件列表。

思维导图的编辑页面间接用原来的 Web 版的页面即可,所以只须要新做一个主页。

最终成果如下:

主页:

编辑页:

我的项目引入 Electron

笔者的我的项目是基于 Vue2.x + Vue Cli 开发的一个单页利用,路由用的是 hash 模式,引入 Electron 很简略,也不须要做啥大改变,间接应用 vue-cli-plugin-electron-builder 插件:

vue add electron-builder

而后启动服务:

npm run electron:serve

就会在 Vue 我的项目启动实现后主动帮你启动Electron,接下来就能够欢快的开发了。

主过程

Electron利用须要一个入口文件,用来管制主过程,须要在我的项目的 package.json 文件中的 main 字段指定:

{"main": "background.js"}

主过程中存在一些根本代码,用于管制利用退出:

// background.js
import {app} from 'electron'
const isDevelopment = process.env.NODE_ENV !== 'production'

// 敞开所有窗口后退出
app.on('window-all-closed', () => {
  // 在 macOS 上,应用程序及其菜单栏通常放弃活动状态,直到用户应用 Cmd+ Q 明确退出
  if (process.platform !== 'darwin') {app.quit()
  }
})

// 在开发模式下,应父过程的申请退出。if (isDevelopment) {if (process.platform === 'win32') {
    process.on('message', data => {if (data === 'graceful-exit') {app.quit()
      }
    })
  } else {process.on('SIGTERM', () => {app.quit()
    })
  }
}

而后就是创立和关上利用的窗口:

// background.js
import {app, protocol, BrowserWindow} from 'electron'
import {createProtocol} from 'vue-cli-plugin-electron-builder/lib'

// 注册协定
protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { secure: true, standard: true} }
])

// 在 ready 事件里创立窗口
app.on('ready', async () => {createMainWindow()
})

app.on('activate', () => {
  // 在 macOS 上,当点击 dock 图标且没有其余窗口关上时,通常会在应用程序中从新创立一个窗口。if (BrowserWindow.getAllWindows().length === 0) {createMainWindow()
  }
})

// 创立主页面
let mainWindow = null
async function createMainWindow() {
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    frame: false,
    titleBarStyle: 'hiddenInset',
    webPreferences: {
      webSecurity: false,
      preload: path.join(__dirname, 'preload.js')
    }
  })

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    await mainWindow.loadURL(process.env.WEBPACK_DEV_SERVER_URL + '/#/workbenche')
  } else {createProtocol('app')
    mainWindow.loadURL('app://./index.html/#/workbenche')
  }
}

ready 事件中创立新窗口,默认是关上主页面,开发环境关上本地启动的服务,生产环境间接关上本地文件。

frame设为false,创立的是一个无边框窗口,也就是没有默认的工具栏和控件,只有你的页面区域。

另外能够看到在创立窗口时指定了一个文件preload.js,这个文件是渲染过程和主过程的通信桥梁。

如果你要关上页面调试的控制台,能够调用 openDevTools 办法:

mainWindow.webContents.openDevTools()

渲染过程

通过 BrowserWindow 创立的每个窗口都是一个独自的渲染过程,为了平安,个别不容许渲染过程间接拜访 Node.js 环境,也就是咱们的页面无奈间接调用 Node.jsAPI,然而作为一个客户端,页面显然是须要这种能力的,比方最根本的性能,操作本地文件,这就是preload.js(预加载脚本)文件的作用。

预加载脚本会在渲染器过程加载之前加载,并有权拜访:两个渲染器全局对象 (windowdocument)、Node.js 环境。

能够在预加载脚本中通过 contextBridge.exposeInMainWorld 办法在页面的 window 对象上挂载属性和办法,这样页面就能应用了,具体的应用前面会介绍。

页面控制器和拖拽区域

咱们创立的是无边框页面,然而作为一个客户端页面,页面控制器(最小化、全屏、敞开)和拖拽区域是必不可少的。

拖拽区域

拖拽区域个别放在页面顶部,宽度和页面宽度统一,高度随便,一个 div 即可:

<div class="workbencheHomeHeader"></div>
.workbencheHomeHeader {
    position: relative;
    width: 100%;
    height: 40px;
    background-color: #ebeef1;
    display: flex;
    align-items: center;
    flex-shrink: 0;
}

要让这个一般的 div 能被拖动也很简略,加上如下的款式即可:

.workbencheHomeHeader {
    // ...
    -webkit-app-region: drag;
}

如果这个区域外部的有些元素你不想作为拖拽区域的话,只有在这个元素上加上如下款式:

.innerElement {-webkit-app-region: no-drag;}

控制器

Windows零碎在无边框模式下默认不会显示控制器,然而 Mac 零碎的控制器(红绿灯)是无奈暗藏的,默认会显示在页面的左上方,所以笔者的做法是判断以后零碎,如果是 Windows 则显示一个咱们本人做的控制器,而 Mac 零碎只有在红绿灯区域显示一个占位元素即可。

为了在页面内不便的判断以后的零碎,咱们能够在预加载脚本中注入一个全局变量:

// preload.js
const {contextBridge} = require('electron')

contextBridge.exposeInMainWorld('platform', process.platform)
contextBridge.exposeInMainWorld('IS_ELECTRON', true)

这样咱们就能够在页面中通过 window.platform 获取以后所在的零碎了,另外还注入了一个全局变量 window.IS_ELECTRON 用来给页面判断是否处于 Electron 环境。

Mac零碎的控制器默认在左上角,也就是咱们的拖拽区域内,Windows上的控制器个别是在右上角的,然而笔者间接让 WindowsMac保持一致,一起放在左上角:

<div class="workbencheHomeHeader">
      <MacControl></MacControl>
      <WinControl></WinControl>
</div>
// MacControl.vue
<template>
  <div class="macControl" v-if="IS_MAC"></div>
</template>

<style lang="less" scoped>
.macControl {
  width: 100px;
  height: 100%;
  flex-shrink: 0;
}
</style>
// WinControl.vue
<template>
  <div class="winControl noDrag" v-if="IS_WIN">
    <div class="winControlBtn iconfont iconzuixiaohua" @click="minimize"></div>
    <div
      class="winControlBtn iconfont"
      :class="[isMaximize ?'icon3zuidahua-3':'iconzuidahua']"
      @click="toggleMaximize"
    ></div>
    <div class="winControlBtn iconfont iconguanbi" @click="close"></div>
  </div>
</template>

<script>
export default {data() {
    return {isMaximize: false}
  },
  methods: {// ...}
}
</script>

Windows控制器显然须要调用窗口的相干办法来管制窗口的最小化、敞开等。这就波及到过程间的通信了,具体来说是渲染过程到主过程的通信。

渲染过程到主过程通信

过程间通信须要用到预加载脚本。

咱们能够在预加载脚本中给页面注入一些全局办法,而后在办法中应用过程间通信 (IPC)告诉主过程,拿后面的控制器为例:

// preload.js
const {contextBridge, ipcRenderer} = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {minimize: () => ipcRenderer.send('minimize'),
  maximize: () => ipcRenderer.send('maximize'),
  unmaximize: () => ipcRenderer.send('unmaximize'),
  close: () => ipcRenderer.send('close'),
}

给页面的 window 对象注入了一个值为对象的属性electronAPI,咱们的所有通信办法都会挂在这个对象上,这样咱们的控制器就能够调用相干办法了:

// WinControl.vue
<script>
export default {
  methods: {minimize() {window.electronAPI.minimize()
    },

    toggleMaximize() {if (this.isMaximize) {
        this.isMaximize = false
        window.electronAPI.unmaximize()} else {
        this.isMaximize = true
        window.electronAPI.maximize()}
    },

    close() {window.electronAPI.close()
    }
  }
}
</script>

接下来就是在主过程中接管音讯:

// background.js
import {BrowserWindow, ipcMain} from 'electron'

const bindEvent = () => {;['minimize', 'maximize', 'unmaximize', 'close'].forEach(eventName => {
    ipcMain.on(eventName, event => {
      // 获取发送音讯的 webContents
      const webContents = event.sender
      // 获取给定 webContents 的窗口
      const win = BrowserWindow.fromWebContents(webContents)
      // 调用窗口的办法
      win[item]()})
  })
}

app.on('ready', async () => {createMainWindow()
  bindEvent()// ++ 监听事件})

新建、关上、保留

新建

当点击新建按钮时,会创立一个新的思维导图编辑窗口:

// preload.js
contextBridge.exposeInMainWorld('electronAPI', {create: () => ipcRenderer.send('create')
}
// background.js
import {v4 as uuid} from 'uuid'

ipcMain.on('create', createEditWindow)

const createEditWindow = async (event, id) => {id = id || uuid()
    const win = new BrowserWindow({
      width: 1200,
      height: 800,
      frame: false,
      titleBarStyle: 'hiddenInset',
      webPreferences: {
        webSecurity: false,
        preload: path.join(__dirname, 'preload.js')
      }
    })
    if (process.env.WEBPACK_DEV_SERVER_URL) {
      win.loadURL(process.env.WEBPACK_DEV_SERVER_URL + '/#/workbenche/edit/' + id)
    } else {win.loadURL('app://./index.html/#/workbenche/edit/' + id)
    }
}

编辑页面须要一个惟一的id,而后关上编辑页面窗口即可。

关上

关上是指关上本地的文件,首先笔者自定义了一个文件扩展名 .smm,作为利用反对的文件,实质就是json 格局。

关上本地文件须要应用到 dialog 模块:

// preload.js
contextBridge.exposeInMainWorld('electronAPI', {selectOpenFile: () => ipcRenderer.send('selectOpenFile')
}
// background.js
import {dialog} from 'electron'

// 关上本地文件
ipcMain.on('selectOpenFile', event => {
    const res = dialog.showOpenDialogSync({
        title: '抉择',// 对话框窗口的题目
        filters: [{name: '思维导图', extensions: ['smm'] }]// 指定一个文件类型数组,用于规定用户可见或可选的特定类型范畴
    })
    if (res && res[0]) {openFile(null, res[0])
    }
})

// 关上文件进行编辑
const idToFilePath = {}// 关联 id 和文件门路
const openFile = (event, file) => {let id = uuid()
    idToFilePath[id] = file
    createEditWindow(null, id)
}

指定只能抉择 .smm 文件,抉择实现后会返回抉择文件的门路。

而后调用 openFile 办法关上编辑窗口,同样会生成一个惟一的 id,另外咱们创立了一个对象用来关联idid对应的文件门路,用于后续的保留操作。

页面关上后,页面须要获取文件的数据,作为初始数据渲染到画布,这个须要渲染过程给主过程发信息,并且能接收数据,还是渲染过程到主过程的通信,只不过是双向的。

渲染过程到主过程通信(双向)

同样是应用 ipcRenderer 对象,只不过不是应用 sendon办法,而是应用 invokehandle办法。

// 页面
const getData = async () => {
    try {let data = await window.electronAPI.getFileContent(this.$route.params.id)
    } catch(err) {console.errror(err)
    }
}
// preload.js
contextBridge.exposeInMainWorld('electronAPI', {getFileContent: id => ipcRenderer.invoke('getFileContent', id)
}
// background.js
// 获取文件内容
ipcMain.handle('getFileContent', (event, id) => {return new Promise((resolve, reject) => {let file = idToFilePath[id]
        if (!file) {resolve(null)
            return
        }
        fs.readFile(file, { encoding: 'utf-8'}, (err, data) => {if (err) {reject(err)
            } else {
                resolve({name: path.parse(file).name,
                    content: JSON.parse(data)
                })
            }
        })
    })
})

拖拽文件到页面

除了关上文件抉择对话框抉择文件外,当然也能够间接拖拽文件到页面,这和一般的 web 页面实现逻辑是一样的,也就是应用拖放API

<div
    class="workbencheHomeContainer"
    @drop="onDrop"
    @dragenter="onPreventDefault"
    @dragover="onPreventDefault"
    @dragleave="onPreventDefault"
  >
</div>

<script>
export default {
    // 搁置文件
    onDrop(e) {e.preventDefault()
      e.stopPropagation()

      let df = e.dataTransfer
      let dropFiles = []
      // 从拖拽的文件中过滤出.smm 文件
      if (df.items !== undefined) {for (let i = 0; i < df.items.length; i++) {let item = df.items[i]
          if (item.kind === 'file' && item.webkitGetAsEntry().isFile) {let file = item.getAsFile()
            if (/\.smm$/.test(file.name)) {dropFiles.push(file)
            }
          }
        }
      }
      if (dropFiles.length === 1) {
        // 如果只有一个文件,间接关上编辑
        window.electronAPI.openFile(dropFiles[0].path)
      } else if (dropFiles.length > 1) {
        // 否则增加到最近文件列表
        // ...
      }
    },

    onPreventDefault(e) {e.preventDefault()
      e.stopPropagation()}
}
</script>

如果只拖拽了一个文件,那么间接关上编辑窗口,否则增加到最近的文件列表上。

保留

保留存在两种状况,一是新建还未保留过的状况,这种须要先创立本地文件,再进行保留,第二种就是文件曾经存在了,间接保留到文件即可。

// preload.js
contextBridge.exposeInMainWorld('electronAPI', {save: (id, data, fileName) => ipcRenderer.invoke('save', id, data, fileName)
}
// background.js
ipcMain.handle('save', async (event, id, data, fileName = '未命名') => {
    // 从 idToFilePath 对象中获取 id 对应的文件门路
    // id 没有关联的文件门路,代表文件没有创立,那么先创立文件
    if (!idToFilePath[id]) {
        const res = dialog.showSaveDialogSync({
            title: '保留',
            defaultPath: fileName + '.smm',
            filters: [{name: '思维导图', extensions: ['smm'] }]
        })
        // 创立胜利后返回文件门路
        if (res) {idToFilePath[id] = res
            fs.writeFile(res, data)
        }
    } else {
        // 文件曾经存在,那么间接保留
        fs.writeFile(idToFilePath[id], data)
    }
})

依据 ididToFilePath对象中获取是否存在关联的文件门路,存在的话则代表文件曾经创立了,否则先创立一个文件,并且和 id 关联起来。

拦挡页面敞开事件

当在编辑页面进行了编辑,还未保留的状况下,如果间接点击敞开页面,通常须要进行二次确认,避免误敞开导致数据失落。

因为 Mac 零碎的敞开是应用默认的控制器,所以无奈拦挡敞开办法,只能拦挡敞开事件:

// 页面
window.onbeforeunload = async e => {e.preventDefault()
    e.returnValue = ''
    // 没有未保留内容间接敞开
    if (!this.isUnSave) {window.electronAPI.destroy()
    } else {
        try {
            // 否则询问用户是否敞开
            await this.checkIsClose()
            // 用户抉择敞开会走这里
            window.electronAPI.destroy()} catch (error) {// 用户抉择不敞开会走这里}
    }
}

// 询问是否敞开页面
checkIsClose() {return new Promise((resolve, reject) => {
        this.$confirm('有操作尚未保留,是否确认敞开?', '提醒', {
            confirmButtonText: '确定',
            cancelButtonText: '勾销',
            type: 'warning'
        })
            .then(async () => {resolve()
        })
            .catch(() => {reject()
        })
    })
}

判断以后是否存在未保留的操作,是的话询问用户是否敞开,敞开窗口调用的是 destroy,因为应用close 办法又会被这个事件拦挡,就进入死循环了。

// preload.js
contextBridge.exposeInMainWorld('electronAPI', {destroy: () => ipcRenderer.send('destroy')
}
// background.js
;[..., 'destroy'].forEach(item => {
    ipcMain.on(item, event => {
        const webContents = event.sender
        const win = BrowserWindow.fromWebContents(webContents)
        win[item]()})
})

最近文件

客户端须要存储、更新、删除最近操作的文件记录,存储应用的是 electron-json-storage,APIlocalstorage 的差不多。

创立文件、关上文件、拖入文件、复制文件、删除文件等操作都须要更新最近文件列表,比方后面提到的关上文件:

// background.js
// 关上文件
const openFile = (event, file) => {let id = uuid()
    idToFilePath[id] = file
    saveToRecent(file)// ++ 保留到最近文件
    createEditWindow(null, id)
}

// 保留到最近文件
import storage from 'electron-json-storage'
const RECENT_FILE_LIST = 'recentFileList'
const saveToRecent = file => {return new Promise((resolve, reject) => {let list = getRecent()
    // 如果文件曾经存在,那么先删除
    let index = list.findIndex(item => {return item === file})
    if (index !== -1) {list.splice(index, 1)
    }
    // 再增加,也就是使之变成最近的一个文件
    list.push(file)
    storage.set(RECENT_FILE_LIST, list, err => {if (err) {reject(err)
      } else {resolve()
      }
    })
  })
}

// 获取最近文件列表
const getRecent = () => {let res = storage.getSync(RECENT_FILE_LIST)
  return (Array.isArray(res) ? res : []).filter(item => {return !!item})
}

当然,这个操作只是更新了客户端的存储,还须要告诉页面更新才行,这就波及到主过程到渲染过程的通信了。

主过程到渲染过程通信

还是以后面的关上文件编辑办法为例:

// background.js
// 关上文件
const openFile = (event, file) => {let id = uuid()
    idToFilePath[id] = file
    saveToRecent(file).then(() => {// 保留到最近文件实现后告诉页面刷新
        notifyMainWindowRefreshRecentFileList()// ++})
    createEditWindow(null, id)
}

// 告诉主页面刷新最近文件列表
const notifyMainWindowRefreshRecentFileList = () => {mainWindow.webContents.send('refreshRecentFileList')
}

调用指定窗口的 webContents 对象的 send 办法发送信息,同样须要在预加载脚本中直达:

// preload.js
contextBridge.exposeInMainWorld('electronAPI', {onRefreshRecentFileList: callback => ipcRenderer.on('refreshRecentFileList', callback)
}

而后在页面中调用 onRefreshRecentFileList 办法注册回调:

// 页面
window.electronAPI.onRefreshRecentFileList(() => {this.getRecentFileList()
})

这样预加载脚本中监听到主过程发送的信息后,就会执行传入的回调办法。

页面获取最近文件列表应用后面介绍的渲染过程和主过程的双向通信办法即可。

在文件夹里显示某个文件

这也是一个常见的性能,关上文件所在文件夹,并且定位到文件。

这个性能须要应用到 shell 模块。

// background.js
import {shell} from 'electron'

// 关上文件所在目录,并定位文件
ipcMain.on('openFileInDir', (event, file) => {shell.showItemInFolder(file)
})

应用零碎默认浏览器关上页面

如果间接应用 a 标签关上页面,Electron默认会新开一个窗口显示,当然这个窗口就不被你管制了,所以会显示丑丑的默认控件,通常关上这种非客户端页面的 url 都是应用零碎默认的浏览器关上,实现上,间接应用 open 库即可。

// background.js
import open from 'open'

// 应用默认浏览器关上指定 url
ipcMain.on('openUrl', (event, url) => {open(url)
})

设置利用为文件的默认关上利用

这是一个很重要的性能,比方咱们双击 .txt 文件,默认会关上 txt 编辑器,如果咱们的利用反对关上某种文件,或者自定义了一种类型的文件,比方笔者的 .smm 文件,那么显然在双击这些文件时应该关上咱们的利用,否则还要用户本人去设置默认利用,那体验是十分不好的。

要实现这个性能,首先须要在打包配置里设置,vue-cli-plugin-electron-builder插件应用的显然是 electron-builder,具体的配置字段为fileAssociations

笔者的配置为:

// vue.config.js
module.exports = {
    pluginOptions: {
        electronBuilder: {
            fileAssociations: [
                {
                    ext: 'smm',
                    name: 'mind map file',
                    role: 'Editor',
                    icon: './build/icons/icon.ico'
                }
            ]
        }
    }
}

ext指定反对的文件扩展名,icon用于该种类型文件在文件夹里显示的图标,这样当装置了咱们的利用,反对的文件默认就会显示咱们配置的图标:

以上只解决了文件关联的性能,双击也能关上咱们的利用,然而通常状况下,还须要间接在利用中关上该文件,比方双击 html 文件,要的不是关上浏览器主页,而是间接在浏览器中关上该文件。

这就是须要在利用中反对了,要获取双击关上文件的门路,能够在主过程中监听 will-finish-launching 事件,当应用程序实现根底的启动的时候会触发该事件,而后分平台解决,在 Windows 平台能够间接通过 process.argv 来获取文件门路,在 Mac 零碎上通过监听 open-file 事件来获取:

// background.js

// 存储被双击关上的文件门路
const initOpenFileQueue = []
app.on('will-finish-launching', () => {if (process.platform == 'win32') {
    const argv = process.argv
    if (argv) {
      argv.forEach(filePath => {if (filePath.indexOf('.smm') >= 0) {initOpenFileQueue.push(filePath)
        }
      })
    }
  } else {app.on('open-file', (event, file) => {if (app.isReady() === false) {initOpenFileQueue.push(file)
      } else {// 利用曾经启动了,间接关上文件}
      event.preventDefault()})
  }
})

// 能够在 ready 事件触发后处理 initOpenFileQueue 数据

当然,目前的实现存在一个问题,就是屡次双击文件,会反复关上利用,实践上来说关上一次就够了,这个晓得怎么解决的敌人欢送评论区见~

打包

性能开发完了,最初一步当然是打包了,想要打包出 Windows 利用和 Mac 利用,你至多须要两台电脑,在 Windows 电脑上能够打包出 Windows 利用,在 Mac 零碎上能够打包出 MacLinux利用。

打包应用的是 electron-builder,它有十分多的配置,反对签名和自动更新等性能,笔者并没有深入研究,更多的性能只能各位本人摸索了,上面是笔者参考其余我的项目的打包配置。

打包配置

// vue.config.js
module.exports = {
    pluginOptions: {
        electronBuilder: {
            preload: 'src/electron/preload.js',
            builderOptions: {
                productName: '思路思维导图',
                copyright: 'Copyright © 思路思维导图',
                asar: true,
                // 设置为文件的默认利用
                fileAssociations: [
                    {
                        ext: 'smm',
                        name: 'mind map file',
                        role: 'Editor',
                        icon: './build/icons/icon.ico'
                    }
                ],
                directories: {output: 'dist_electron'},
                mac: {
                    target: [
                        {
                            target: 'dmg',
                            arch: ['x64', 'arm64', 'universal']
                        }
                    ],
                    artifactName: '${productName}-${os}-${version}-${arch}.${ext}',
                    category: 'public.app-category.utilities',
                    darkModeSupport: false
                },
                win: {
                    target: [
                        {
                            target: 'portable',
                            arch: ['x64']
                        },
                        {
                            target: 'nsis',
                            arch: ['x64']
                        }
                    ],
                    publisherName: '思路思维导图',
                    icon: 'build/icons/icon.ico'
                },
                linux: {
                    target: [
                        {
                            target: 'AppImage',
                            arch: ['x64']
                        }
                    ],
                    category: 'Utilities',
                    icon: './build/icon.icns'
                },
                dmg: {icon: 'build/icons/icon.icns'},
                nsis: {
                    oneClick: false,// 勾销一键装置
                    allowToChangeInstallationDirectory: true,// 容许用户抉择装置门路
                    perMachine: true
                }
            }
        }
    }
}

而后在 package.json 文件中增加打包命令:

// package.json
{
    "scripts": {
        "electron:build": "vue-cli-service electron:build -p never",
        "electron:build-all": "vue-cli-service electron:build -p never -mwl",
        "electron:build-mac": "vue-cli-service electron:build -p never -m",
        "electron:build-win": "vue-cli-service electron:build -p never -w",
        "electron:build-linux": "vue-cli-service electron:build -p never -l"
    }
}

第一个命令会主动依据以后零碎打包对应的利用。

打包过程中可能会在下载 electron 的原型包的一步卡住,这个只能多试几次,或者手动下载,具体操作能够百度一下。

利用图标

后面的打包配置中能够看到配置了几种不同格局的图标,也就是咱们的利用图标,Windows零碎用的是 .ico 格局的图片,而 MacLinux零碎用的是 .icns 的图标。

首先你须要筹备一张 1024*1024png图片icon.png

生成 .ico 的图片很简略,网上搜寻一下找一个在线网站转一下就行了,比方这个:png-to-ico。

要生成 .icns 图片,你须要在 Mac 零碎的命令行中执行一些命令:

// 命令行进入图片所在门路

// 创立一个长期文件夹
mkdir temp.iconset

// 在长期文件夹中生成 10 种大小的图片
sips -z 16 16 icon.png --out temp.iconset/icon_16x16.png
sips -z 32 32 icon.png --out temp.iconset/icon_16x16@2x.png
sips -z 32 32 icon.png --out temp.iconset/icon_32x32.png
sips -z 64 64 icon.png --out temp.iconset/icon_32x32@2x.png
sips -z 128 128 icon.png --out temp.iconset/icon_128x128.png
sips -z 256 256 icon.png --out temp.iconset/icon_128x128@2x.png
sips -z 256 256 icon.png --out temp.iconset/icon_256x256.png
sips -z 512 512 icon.png --out temp.iconset/icon_256x256@2x.png
sips -z 512 512 icon.png --out temp.iconset/icon_512x512.png
sips -z 1024 1024 icon.png --out temp.iconset/icon_512x512@2x.png

// 生成.icns 图片
iconutil -c icns temp.iconset -o icon.icns

而后长期文件夹能够删除,不过最好把长期生成的 10 张图片也复制到 icon.png 所在文件,否则在打包 Mac 利用时可能会用到,然而不存在就会报错。

总结

本文分享了一下笔者做的一个简略的利用的细节,因为也是刚入门,所以某些方面可能会存在谬误,或者有更好的实现形式,欢送评论区见。有趣味的敌人也能够下载体验一下~

源码地址:https://github.com/wanglin2/mind-map/tree/electron。

下载地址:https://github.com/wanglin2/mind-map/releases/tag/v0.1.0。

退出移动版