平时写文章或写代码的时候,都少不了需要将本地图片转成在线图片链接,大家都是使用什么工具进行转换的呢?相信很多人都有自己的图床工具,今天来给大家介绍一下,怎么基于Gitee和Electron来开发一个便捷的图床工具,支持图片的上传、删除、复制和快速生成markdown链接、快捷键唤起和隐藏面板,粘贴剪切板图片上传等……
原本只是想写一个Chrome插件来实现简单功能,后面发现Chrome插件的局限性太大了,所以最后还是选择使用Electron
来制作一个桌面程序。
存储方面我们可以直接使用gitee来用做图库存储,不需要额外去购买存储服务器。
直接到Gitee官网: Gitee - 基于 Git 的代码托管和研发协作平台 ,点击注册即可。
注册完账号后直接登录,在首页点击右上角的加号可以新建仓库
仓库信息自行填写即可
创建完仓库后我们可以新建一个文件夹用来存储图片:
打开设置里的私人令牌页面
点击生成新令牌,根据提示填写信息即可,注意保存好生成的令牌。
我们可以先搭建一个简单的electron项目:
安装 Node.js:确保你的电脑上已经安装了 Node.js。你可以从 Node.js 官方网站(https://nodejs.org)下载并安装最新版本的 Node.js。
创建项目目录:在你想要创建项目的位置,创建一个新的文件夹作为项目目录。
初始化项目:打开命令行终端,进入到项目目录,并执行以下命令初始化一个新的 npm 项目:
npm init -y
npm install electron
创建主文件:在项目目录中创建一个名为 main.js
的文件,作为 Electron 应用的主文件。
编写主文件代码:在 main.js
文件中编写 Electron 应用的主要逻辑。例如,下面是一个简单的示例:
const { app, BrowserWindow } = require('electron');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
});
win.loadFile('index.html');
}
app.whenReady().then(() => {
createWindow();
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit();
});
创建 HTML 文件:在项目目录中创建一个名为 index.html
的文件,作为 Electron 应用的初始页面。
编写 HTML 文件代码:在 index.html
文件中编写你的应用界面的 HTML 代码。
在 package.json
文件中添加启动命令:打开 package.json
文件,在 "scripts"
部分添加以下内容:
"scripts": {
"start": "electron ."
}
npm start
前面准备工作全都完成后,现在我们就有了一个简单electron项目架子和一个gitee仓库,可以开始来实现相关的功能了。
gitee提供了api文档,我们可以通过gitee的api文档来对我们的仓库进行上传图片和获取图片的操作。
gitee API文档地址:https://gitee.com/api/v5/swagger#/getV5ReposOwnerRepoStargazers?ex=no
这里我将需要使用到的功能写成了一个类:
用户授权码,也就是我们前面生成的私人令牌。
仓库所属空间地址(企业、组织或个人的地址path),如下图:
仓库路径(path),如下图:
图片存放目录地址,如下图:
分支名,默认为master
,我们可以修改成指定分支:
init(config = {}) {
// 设置 Gitee 仓库信息和目录路径
this.username = config.username;
this.repo = config.repo;
this.accessToken = config.accessToken;
this.branchName = config.branchName || "master";
this.apiUrl = "https://gitee.com/api/v5/repos/";
this.dirPath = config.dirPath;
}
将以上配置信息在程序的配置里设置好即可:
根据API文档进行请求即可:
async uploadToGitee(base64Data) {
try {
const formData = new FormData();
formData.append("content", base64Data);
formData.append("access_token", this.accessToken);
formData.append("message", "上传图片");
const timeStamp = new Date().getTime();
Toast.showLoading("正在上传");
const response = await fetch(
`${this.apiUrl}${this.username}/${this.repo}/contents/${this.dirPath}${timeStamp}.jpg`,
{
method: "POST",
body: formData,
}
);
Toast.hide();
if (!response.ok) {
throw new Error("上传图片失败");
}
const data = await response.json();
Toast.showToast("图片上传成功!");
return data.content.download_url;
} catch (error) {
console.error(error);
Toast.showToast("图片上传失败!");
throw error;
}
}
根据API文档进行请求即可:
async getImg() {
try {
const response = await fetch(
`${this.apiUrl}${this.username}/${this.repo}/contents/${this.dirPath}`,
{
headers: {
Authorization: `token ${this.accessToken}`,
},
}
);
if (!response.ok) {
throw new Error("获取图片列表失败");
}
const data = await response.json();
// 筛选出图片文件
const imageFiles = data.filter(
(file) =>
file.type === "file" && file.name.match(/\.(jpg|jpeg|png|gif)$/i)
);
return imageFiles;
} catch (error) {
console.error(error);
throw error;
}
}
根据API文档进行请求即可:
async deleteImg(fileName, sha, cb) {
try {
const response = await fetch(
`${this.apiUrl}${this.username}/${this.repo}/contents/${this.dirPath}/${fileName}?access_token=${this.accessToken}&ref=${this.branchName}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json;charset=utf-8",
},
body: JSON.stringify({
message: "删除图片",
sha,
prune: true,
}),
}
);
if (!response.ok) {
throw new Error("删除图片失败");
}
Toast.showToast("删除成功");
cb && cb();
} catch (error) {
console.error(error);
Toast.showToast("删除失败");
throw error;
}
}
我们可以通过三种方式来上传我们本地的图片
前面有写了一篇实现拖拽或点击上传图片的文章,这里就不详细再赘述了,有兴趣的可以去看看:《文件拖拽上传功能已经烂大街了,你还不会吗?》
平时我们经常会使用到截图,所以我们希望可以直接将截图粘贴到工具中进行上传,这里我们可以通过监听页面上的paste
事件,直接读取剪切板的图片展示到页面上并进行上传。
document.addEventListener("paste", function (e) {
const items = e.clipboardData.items;
for (const item of items) {
if (item.type.indexOf("image") !== -1) {
const blob = item.getAsFile();
showPreview(blob);
}
}
});
一张图片一张图片上传太慢了,所以我们也支持直接选择一个文件夹,将文件夹里的所有图片一次性上传到gitee上,
元素和一个按钮用于触发上传操作。代码如下:<input
type="file"
style="display: none"
id="folderInput"
onchange="uploadImages()"
webkitdirectory
multiple
/>
<button class="upload-btn" onclick="selectFolder()" style="background: #FFD04C;">
选择文件夹上传
button>
webkitdirectory
:告诉浏览器文件选择器应该允许选择文件夹(目录),而不仅仅是单个文件。
multiple
:告诉浏览器文件选择器应该允许选择多个文件或文件夹。
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const base64String = reader.result.split(",")[1];
resolve(base64String);
};
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
function selectFolder() {
const folderInput = document.getElementById("folderInput");
folderInput.click();
}
async function uploadImages() {
const folderInput = document.getElementById("folderInput");
let files = folderInput.files || [];
files = [...files].filter((file) => file.type.startsWith("image/"));
for (let i = 0; i < files.length; i++) {
Toast.showLoading(`上传中,${i}/${files.length}`);
const file = files[i];
const base64Data = await blobToBase64(file);
await gitOperate.uploadToGitee(base64Data,false);
}
Toast.hide();
Toast.showToast(`已全部上传`);
waterfall.init();
}
上传完图片后,我们还希望可以看到之前上传的图片,这里我们需要对图片列表做一个瀑布流展示。
之前我也有写过一个瀑布流组件详细的实现步骤,有兴趣的同学可以看看:《Vue封装一个瀑布流图片容器组件》。
这里我们可以通过原生JavaScrip来快速实现一个,具体代码如下:
class WaterfallContent {
constructor(config = {}) {
this.init(config);
}
init(config = {}) {
this.imgList = config.imgList || [];
this.column = config.column || 8;
this.imgMargin = config.imgMargin || 0.5;
this.domId = config.domId || "waterfall-container";
this.minHeight = [];
this.arr = [];
const ul = document.getElementById(this.domId);
ul.innerHTML = "";
}
async create(imgList = this.imgList, cb) {
this.init();
this.imgList = imgList;
const ul = document.getElementById(this.domId);
ul.innerHTML = "";
let trueWidth = Math.floor(
(100 - this.column * this.imgMargin * 2) / this.column
);
let trueWidthPx = 0;
for (let i = 0; i < this.column; i++) {
let li = document.createElement("li");
li.style.listStyle = "none";
li.style.float = "left";
li.style.width = `${trueWidth}%`;
li.style.margin = `0 ${this.imgMargin}%`;
li.classList.add("git-img");
ul.appendChild(li);
this.arr.push(li);
this.minHeight.push(0);
trueWidthPx = li.offsetWidth;
}
this.loadHandler(trueWidthPx, cb);
}
getBase64(file) {
const reader = new FileReader();
reader.readAsDataURL(file);
return new Promise((resolve) => {
reader.onload = () => {
resolve(reader.result);
};
});
}
getImgPx(img, maxWidth) {
const image = new Image();
image.src = img;
return new Promise((resolve) => {
image.onload = () => {
const width = image.width;
const height = image.height;
image.width = maxWidth;
image.height = image.height * (maxWidth / width);
resolve({ width, height, image });
};
});
}
async loadHandler(trueWidth, cb) {
for (let i = 0; i < this.imgList.length; i++) {
const imgItem = this.imgList[i];
const src = imgItem.download_url;
const res = await this.getImgPx(src, trueWidth);
const { image } = res;
const minHeight = this.minHeight;
const arr = this.arr;
// 高度数组的最小值
const min = Math.min.apply(null, minHeight);
// 高度数组的最小值索引
const minIndex = minHeight.indexOf(min);
// 克隆一份图片
const im = image.cloneNode(true);
im.setAttribute("data-sha", imgItem.sha);
im.onclick = this.imgClick;
// 将图片假如对应最小值索引的容器中
arr[minIndex].appendChild(im);
// 更新最小值索引的容器的高度
minHeight[minIndex] += im.height;
if (i === 0 && cb) {
cb();
}
}
}
}
查看图片的时候我们希望可以对图片进行操作,这里的操作我们通过鼠标右键点击弹出,所以我们可以实现一个自定义鼠标右键菜单栏。
具体代码如下:
class MouseMenu {
constructor(config) {
this.menuClass = config.menuClass || "j-mouse-menu";
this.menuId = config.menuId || "JMouseMenu";
this.contentId = config.contentId || "j-mouse-content";
this.init();
}
init() {
const dom = document.getElementById(this.contentId);
dom.oncontextmenu = (e) => {
const clickItem = e.path[0];
if (clickItem.localName !== "img") return;
const menu = document.getElementById(this.menuId);
this.clickItem = clickItem;
this.hideAllMenu();
// 自定义body元素的鼠标事件处理函数
e = e || window.event;
e.preventDefault();
let scrollTop =
document.documentElement.scrollTop || document.body.scrollTop; // 获取垂直滚动条位置
let scrollLeft =
document.documentElement.scrollLeft || document.body.scrollLeft; // 获取水平滚动条位置
menu.style.display = "block";
let left = e.clientX + scrollLeft;
let top = e.clientY + scrollTop;
if (menu.offsetHeight + top > window.innerHeight) {
top = window.innerHeight - menu.offsetHeight - 5;
}
if (menu.offsetWidth + left > window.innerWidth) {
left = window.innerWidth - menu.offsetWidth - 5;
}
menu.style.left = left + "px";
menu.style.top = top + "px";
document.onclick = () => {
this.hideAllMenu();
};
};
}
hideAllMenu() {
const jMenu = document.getElementsByClassName("j-mouse-menu");
for (const item of jMenu) {
item.style.display = "none";
}
}
}
交互少不了Toast弹窗提示,这里我们使用JavaScrip简单实现一个,具体代码如下:
class ToastC {
constructor(config) {
this.config = config;
this.init();
}
init() {
const body = document.body;
const toastContainer = document.createElement("div");
toastContainer.id = "toastContainer";
const styleObj = {
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
background: "rgba(0, 0, 0, 0.8)",
color: "#ffffff",
fontSize: "16px",
opacity: 0.7,
transition: "opacity 0.3s ease-in-out",
padding: "10px",
"border-radius": "5px",
display: "none",
textAlign: "center",
};
for (const key in styleObj) toastContainer.style[key] = styleObj[key];
body.appendChild(toastContainer);
const loader = document.createElement("div");
loader.id = "toastLoader";
const loaderStyleObj = {
border: "4px solid #f3f3f3",
borderTop: "4px solid #3498db",
borderRadius: "50%",
width: "20px",
height: "20px",
animation: "spin 1s linear infinite",
margin: "0 auto 10px",
display: "none",
};
for (const key in loaderStyleObj) loader.style[key] = loaderStyleObj[key];
toastContainer.appendChild(loader);
const text = document.createElement("div");
text.id = "toastText";
const textStyleObj = {
marginTop: "5px",
};
for (const key in textStyleObj) text.style[key] = textStyleObj[key];
toastContainer.appendChild(text);
const keyframes = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
`;
const style = document.createElement("style");
style.innerHTML = keyframes;
document.head.appendChild(style);
}
showToast(text) {
const textElem = document.getElementById("toastText");
// 设置Toast提示文本
textElem.innerText = text;
// 显示Toast提示
toastContainer.style.display = "block";
// 3秒后隐藏Toast提示
setTimeout(() => this.hide(), 3000);
}
showLoading(text = "加载中...") {
const loader = document.getElementById("toastLoader");
const textElem = document.getElementById("toastText");
// 设置Toast提示文本为加载中
textElem.innerText = text;
// 显示Toast提示和加载动画
loader.style.display = "block";
this.show();
}
show() {
const toastContainer = document.getElementById("toastContainer");
// 显示Toast提示
toastContainer.style.display = "block";
}
hide() {
try {
const toastContainer = document.getElementById("toastContainer");
const loader = document.getElementById("toastLoader");
// 隐藏Toast提示和加载动画
toastContainer.style.display = "none";
loader.style.display = "none";
} catch (err) {}
}
}
我们可以设置快捷键快速唤起和隐藏窗口,在根目录下的main.js文件中注册快捷键,这里我设置的是alt + x
,大家也可以改成自己喜欢的快捷键,具体代码如下:
// 注册快捷键
globalShortcut.register("Alt+X", () => {
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
}
});
直接到 gitee 上下载即可。
下载完源码之后,我们到gitImgBed目录下运行npm i
安装依赖,等待依赖安装完成。
依赖安装完成之后,我们可以在gitImgBed目录下运行npm run build
进行打包
打包完成后我们可以在当前目录下看到一个叫jyeontuGitImgBed-win32-x64
文件夹,打开文件夹,找到里面一个叫jyeontuGitImgBed
的应用程序,双击启动即可
将之前准备工作时间的gitee仓库相关信息填写到配置中。
输入正确信息后保存,便可以上传和查看gitee图床中的图片了。
gitee 地址:https://gitee.com/zheng_yongtao/electron_program
关注公众号『前端也能这么有趣』发送 图床
即可获取源码。
这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 ,平时也喜欢写些东西,既为自己记录 ,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 ,写错的地方望指出,定会认真改进 ,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 。