本文同步在个人博客shymean.com上,欢迎关注
iPic 是一个很赞的应用,可以快速将图片上传到图床上。由于非会员只能使用免费的新浪图床,因为最近新浪图床防盗链和图片有效期的缘故,因此决定自己实现一个图片快速上传的应用。
大致对比了一下Flutter Desktop
、PyQT
和Electron
等框架,最后决定使用Electron
,花了两三个晚上实现了将剪切板的图片快速上传到七牛上(非广告~)。
本文将回顾整个开发流程,并记录第一次正儿八经开发Electron
的经验。
项目完整代码已放在github上。
electron-forge是一个用来开发、打包和发布 Electron 的脚手架,首先安装electron
和electron-forge
# 全局安装 npm install -g electron npm install -g electron-forge # 初始化项目 electron-forge init oPic # ...初始化的时间可能会有点长 复制代码
electron-forge 为我们生成了基本的项目模板。
如果是开发类似于 GUI 应用,可以修改src/index.html
里面相关的视图文件,体验使用 Web 技术开发桌面应用。如果引入了 Vue、React 等框架,也可以在开发环境下直接将file://
文件替换为webpack-dev-server
服务 URL
// mainWindow.loadURL(`file://${__dirname}/index.html`); mainWindow.loadURL(`http://localhost:8080`); 复制代码
由于本次开发目标是工具类应用,很少涉及到 UI 层面的开发,梳理一下整个工具的需求
点击顶部任务栏应用图标,展示剪贴板内的图片(如复制图片文件、截图等),下面是 iPic 的工作页面
点击待上传图片,后台将图片上传到图床,自动将图片 URL 填充到剪贴板
更新已上传图片列表,点击已上传图片,会重新复制该图片的 URL
整个需求比较简单,主要需要去Electron 文档查下面几个接口
大概就这些,开始写代码啦
查看系统托盘 API,构造一个Tray
实例
import { Tray } from "electron"; // 创建顶部图标 const createTray = app => { // upload@3x是展示在托盘的图标 const icon16 = path.resolve(__dirname, "../assets/upload@3x.png"); const tray = new Tray(icon16); // 监听图标点击事件,打开选项菜单 tray.on("click", () => { const config = configUtil.getConfig(); const template = [ { label: "待上传", type: "normal", enabled: false }, { label: "", type: "separator" }, { label: "已上传", type: "normal", enabled: false }, { label: "", type: "separator" } ]; // todo 构建图片选项 // 创建contextMenu并弹出 const contextMenu = Menu.buildFromTemplate(template); tray.popUpContextMenu(contextMenu); }); }; 复制代码
参考
clipboard.readImage
获取剪贴板内的图片readImage
获取的是一个 NativeImage 包装对象,需要查看它与原始图片文件之间的转换import { clipboard } from "electron"; const uploadList = []; // 将已上传的图片保存在内存中 const clipboardImageList = []; // 保存最近未上传的图片 // 根据剪切板图片创建menuItem const createClipboardImageItem = () => { const clipboardImage = clipboard.readImage(); // 剪切板如果有数据,则保存到clipboardImageList中 if (clipboardImage && !clipboardImage.isEmpty()) { const radio = clipboardImage.getAspectRatio(); // 创建一个用于在菜单栏展示的图标 const img = clipboardImage.resize({ width: 100, height: radio / 100 }); // 将图片暂存在clipboardImageList中 addToImageList(clipboardImageList, { img, raw: clipboardImage }, 1); } return clipboardImageList.map((row, index) => { const { raw, img } = row; // 点击菜单选项时执行upload const upload = () => { const buffer = raw.toPNG(); // 调用uploadBufferImage方法上传图片 uploadBufferImage(buffer).then(url => { // 更新列表 addToImageList(uploadList, { img, url }); removeFromClipboardList(img); // 自动复制url copyUrl(url); Util.showNotify(`上传到七牛成功,链接${url}已经复制到剪切板`); }); }; // 返回菜单栏配置 return { label: (index + 1).toString(), icon: row.img, // 缩小版的图片传给icon配置项,这样就可在菜单栏展示了 type: "normal", click: upload }; }); }; // 同理,创建已上传的图片记录 const createUploadItem = () => uploadList.map(({ img, url }, index) => { const handler = () => { const text = copyUrl(url); Util.showNotify(`链接${text}已经复制到剪切板`); }; return { label: (index + 1).toString(), icon: img, type: "normal", click: handler }; }); 复制代码
希望应用足够轻量,因此在数据存储方便并没有使用诸如nedb等工具,而是直接简单粗暴地保存在本地文件。
当点击菜单栏的配置项时,将弹出一个配置窗口填写配置项,确定时将数据保存在本地文件中。
let settingWindow; const openSettingWindow = () => { settingWindow = new BrowserWindow({ width: 600, height: 400 }); // 使用electron渲染一个页面 const url = `file://${path.resolve(__dirname, "./setting.html")}`; settingWindow.loadURL(url); // settingWindow.webContents.openDevTools(); settingWindow.on("closed", () => { settingWindow = null; }); }; const template = [ // ...增加一个菜单选项 { label: "偏好设置", type: "normal", click: openSettingWindow } ]; 复制代码
在setting.html
中,实现一个表单提交的页面,
然后通过封装的本地存储工具configUtil
读取和保存配置
const defaultConfig = { autoMarkdown: true, upload: { // 七牛图床配置 qiNiu: { accessKey: "", secretKey: "", bucket: "", // 仓库名 host: "" // 资源域名 } } }; const configFile = "../config.json"; // 获取配置 function getConfig() { try { return require(configFile); } catch (e) { return defaultConfig; } } // 保存配置 function saveConfig(config) { const fileName = path.resolve(__dirname, configFile); return fs.writeFile(fileName, JSON.stringify(config)); } 复制代码
除了图床配置,configUtil
还可以可以保存应用偏好设置,在设计上也需要支持后续其他图床的扩展(虽然我目前用七牛就够啦~)
回到前面的uploadBufferImage
方法,由于没有找到直接上传 Electron NativeImage 的方法,因此这里的实现思路是:
首先读取七牛配置,然后将 buffer 写入本地临时文件,接着通过 qiniu SDK 将文件上传到服务器上,最后删除临时文件就 OK 了
function qiNiuUpload(img) { try { const { upload: uploadConfig } = configUtil.getConfig(); const upload = createUploadQiNiu(uploadConfig.qiNiu); return upload(img); } catch (e) { console.log("缺少config.json配置文件"); return Promise.reject(e); } } // 上传二进制文件 async function uploadBufferImage(buffer) { // 写入临时图片 const fileName = `${Date.now()}_${Math.floor(Math.random() * 1000)}`; const filePath = path.resolve(__dirname, `../tmp/${fileName}.png`); await fs.writeFile(filePath, buffer); // 创建临时本地文件 const url = await qiNiuUpload(filePath); // 上传到七牛 await fs.unlinkSync(filePath); // 删除临时文件 return url; } 复制代码
下面这个createUploadQiNiu
是封装qiniuSDK 的方法,三年前的代码了[/捂脸],凑活着用
const qiniu = require("qiniu"); const path = require("path"); const createUploadQiNiu = opts => { const { accessKey, secretKey, bucket, host } = opts; return filePath => { const key = `oPic/${path.basename(filePath)}`; // 设置上传策略 const putPolicy = new qiniu.rs.PutPolicy({ scope: `${bucket}:${key}` }); // 根据密钥创建鉴权对象mac,获取上传token const mac = new qiniu.auth.digest.Mac(accessKey, secretKey); const uploadToken = putPolicy.uploadToken(mac); // 配置对象 const config = new qiniu.conf.Config(); // 上传机房,z2是华南 config.zone = qiniu.zone.Zone_z2; // 扩展参数,主要是用于文件分片上传使用的,这里可以忽略 const putExtra = new qiniu.form_up.PutExtra(); // 实例化上传对象 const formUploader = new qiniu.form_up.FormUploader(config); return new Promise((resolve, reject) => { formUploader.putFile( uploadToken, key, filePath, putExtra, (respErr, respBody, respInfo) => { if (respErr) { reject(respErr); } if (respInfo && respInfo.statusCode === 200) { // 拼接服务器路径 const filename = host + key; resolve(filename); } else { reject("respInfo is error"); } } ); }); }; }; 复制代码
前面提到,希望在图片上传之前对文件进行压缩,目前用过最好的图片压缩还是TinyPNG,不过貌似没开源压缩算法,目前一个月只能调用500次API,所以试了下imagemin,看起来效果也能接受,就它啦。
const imageMin = require("imagemin"); const imageJPEG = require("imagemin-jpegtran"); const imagePNG = require("imagemin-pngquant"); // 图片压缩 function compressImage(filePath, destination) { return imageMin([filePath], { destination, plugins: [ imageJPEG(), imagePNG({ quality: [0.6, 0.8] }) ] }); } 复制代码
然后再上传前进行压缩即可
await fs.writeFile(filePath, buffer); // 创建临时本地文件 // 图片压缩为同名图片 if (needCompress) { await compressImage(filePath, folder); } const url = await qiNiuUpload(filePath); // 上传到七牛 复制代码
功能开发完毕后,使用electron-forge
打包就可以啦,会在项目根目录下输出out
文件夹
npm run package npm run make 复制代码
此外,Electron 打包的应用是在是太大了,上面这点代码打包出来居然有 150M,不知道是不是我的姿势不对,有空研究下
至此,就完成了一个简易版的图片上传应用,目前基本能实现日常的需求了(PS:现在终于不用担心博客的图片被新浪图床吞掉了~),也算是完成了自己的一个挂念。
整个项目已放在github上,由于开发时间有点短,加上之前也基本没用过 Electron,所以代码写的有点烂~ 还有一些可以迭代的地方,比如
有时间再处理,折腾其他东西去了。