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 文档查下面几个接口
在 Mac 顶部应用栏展示应用图标,点击弹出选项菜单
获取剪切板图片信息,定制选项菜单栏展示图片
图床配置弹窗 UI 开发,与主进程交互
图片上传,很早之前写了一个img_qiniu_cdn,貌似现在还可以用
图片压缩,本来想使用TinyPng的 API,发现有次数限制~找个其他的库吧
大概就这些,开始写代码啦
开发
顶部应用栏
查看系统托盘 API,构造一个Tray实例
import { Tray } from “electron”;
// 创建顶部图标
const createTray = app => {
// upload@3x是展示在托盘的图标
const icon16 = path.resolve(__dirname, “…/assets/[email protected]”);
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);
});
};
复制代码获取剪贴板图片
参考
剪贴板 API,使用clipboard.readImage获取剪贴板内的图片
NativeImage,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,不知道是不是我的姿势不对,有空研究下
https://www.jianshu.com/p/afb04c02bdc2
https://www.jianshu.com/p/9a4d54616cda
https://www.jianshu.com/p/afe0b866b620
https://www.jianshu.com/p/b9a0efcd4cd5
https://www.jianshu.com/p/b94079b1eda9
https://www.jianshu.com/p/0df3f258e778
https://www.jianshu.com/p/58307ddd4bfd
小结
至此,就完成了一个简易版的图片上传应用,目前基本能实现日常的需求了(PS:现在终于不用担心博客的图片被新浪图床吞掉了~),也算是完成了自己的一个挂念。
整个项目已放在github上,由于开发时间有点短,加上之前也基本没用过 Electron,所以代码写的有点烂~ 还有一些可以迭代的地方,比如
上传进度展示