前言
由于博客中的图片都是引用图床中的图片,自己搭建图床可能涉及到租赁的服务器到期,服务迁移,或者流量被盗刷等不可抗力因素等,免费图床又可能随时失效,面对以上场景,我一直试图尝试寻找一种免费的解决办法作为备选,使用GitHub图床是其中的一种,但是 Github 仓库大小达到 1G 的时候会有人工审查,如果发现你将 Github 仓库作为图床使用可能会被封禁仓库。最主要的是国内访问速度太慢,之前的免费CDN好像失效了,后来我发现可以使用Vercel搭配自定义域名实现。
实现原理
Vercel可以一键导入Github上的项目,可以利用在GitHub上备份的图片文件搭建一个简单的图床程序,直接从GitHub导入项目,一键部署即可。
注意事项
- 需要在项目根目录上传一个
index.html
入口文件,否则无法访问图片资源。项目结构类似下面
1 2 3 4 5 6 7
| blogpic/ ├── index.html └── public/ └── 2024/ └── 08/ └── 28/ └── xxx.png
|
由于Vercel提供的域名在国内无法访问,需要绑定自定义域名
使用PicGo配置GitHub图床时,将原来的加速CDN修改为自定义域名
1 2 3 4
| https://cdn.jsdelivr.net/gh/Shiguang-coding/blogpic@master
https://img.shiguang666.eu.org
|
优缺点
优点
可以关联GitHub账户,GitHub提交后自动部署,结合PicGo使用非常方便
访问速度还不错,不怕刷流量
方便迁移,采用自定义域名,迁移时直接批量替换图片路径即可
图床文件迁移可参考 Gitee图床被封!如何实现无缝图床转移?
缺点
- 一次上传不能及时显示,如果你要求上传之后及时显示,不建议使用这种方式,这种方式每次都是静态部署网站,平均速度在10s以内。
- 有每日部署频率限制,貌似是100次左右,如果你有大量图片上传的话,建议先上传到github,再批量替换文件链接 ,该方案只作为主图床失效时临时备用方案,GitHub使用PicGo上传频率过高也会影响GitHub贡献地图的数据。
- 需要有一个自己的域名(vercel 部署的自带域名国内无法访问)。
- 因为 Github 存储库的限制,单文件最大 25MB,超出会提示文件过大。
本地预览
Demo见GitHub:https://github.com/Shiguang-coding/vercel-img
让AI帮我写了个简单的程序,实现图床在线预览的功能,具体效果如下:
但是部署到Vercel后无法实现这个效果,理论上是可以的,有了解的小伙伴可以留言指点下。
1、项目根目录创建public
文件夹
将所有图片文件移动到public
目录内
之所以不把public
目录直接上传到GitHub,是因为在使用PicGo时默认导入到剪切板的路径是 设定的自定义域名
+ 设定存储路径
,而预览图片时是不需要加public
这一层目录的,所以会导致上传后剪切板自动带出的图片路径无法访问。
2、在public
目录下创建index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>時光图床</title> <style> body { font-family: Arial, sans-serif; margin: 0; background-color: #f9f9f9; display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; } .container { display: flex; flex-direction: column; align-items: center; } .folder, .image { margin: 10px; padding: 10px; border: 1px solid #ccc; border-radius: 5px; text-align: center; width: 100%; max-width: 300px; background-color: #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); transition: transform 0.2s; } .folder:hover, .image:hover { background-color: #f0f0f0; transform: translateY(-2px); } .image img { max-width: 100%; max-height: 200px; cursor: pointer; border-radius: 5px; } .modal { display: none; position: fixed; z-index: 1; padding-top: 100px; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgb(0,0,0); background-color: rgba(0,0,0,0.9); } .modal-content { margin: auto; display: block; width: 80%; max-width: 700px; border-radius: 5px; } .close { position: absolute; top: 15px; right: 35px; color: #f1f1f1; font-size: 40px; font-weight: bold; transition: 0.3s; } .close:hover, .close:focus { color: #bbb; text-decoration: none; cursor: pointer; } @media (min-width: 600px) { .container { flex-direction: row; flex-wrap: wrap; justify-content: center; } .folder, .image { width: 45%; max-width: none; } } @media (min-width: 900px) { .folder, .image { width: 30%; } } </style> </head> <body> <h1>時光图床</h1> <div class="container" id="container"></div>
<div id="myModal" class="modal"> <span class="close">×</span> <img class="modal-content" id="img01"> <div id="caption"></div> </div>
<script> document.addEventListener('DOMContentLoaded', function() { const container = document.getElementById('container'); const modal = document.getElementById('myModal'); const modalImg = document.getElementById('img01'); const close = document.getElementsByClassName('close')[0];
let currentPath = '';
function loadFolders(folders, path = '') { container.innerHTML = ''; folders.forEach(folder => { if (folder.images && folder.images.length > 0) { folder.images.forEach(image => { const imageDiv = document.createElement('div'); imageDiv.className = 'image'; const img = document.createElement('img'); img.src = `/${image}`; img.alt = image; img.onclick = function() { modal.style.display = 'block'; modalImg.src = this.src; history.pushState({ path: currentPath, image: image }, '', `/${image}`); }; imageDiv.appendChild(img); container.appendChild(imageDiv); }); } else { const folderDiv = document.createElement('div'); folderDiv.className = 'folder'; folderDiv.textContent = folder.name; folderDiv.onclick = function() { currentPath = `${path}${folder.name}/`; history.pushState({ path: currentPath }, '', currentPath); fetch(`/api/files/${currentPath}`) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => loadFolders(data, currentPath)) .catch(error => console.error('Error fetching files:', error)); }; container.appendChild(folderDiv); } }); }
window.addEventListener('popstate', function(event) { if (event.state && event.state.path) { currentPath = event.state.path; fetch(`/api/files/${currentPath}`) .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => loadFolders(data, currentPath)) .catch(error => console.error('Error fetching files:', error)); } if (event.state && event.state.image) { modal.style.display = 'block'; modalImg.src = `/${event.state.image}`; } });
fetch('/api/files') .then(response => { if (!response.ok) { throw new Error('Network response was not ok'); } return response.json(); }) .then(data => { currentPath = ''; history.replaceState({ path: currentPath }, '', currentPath); loadFolders(data); }) .catch(error => console.error('Error fetching files:', error));
close.onclick = function() { modal.style.display = 'none'; history.pushState({ path: currentPath }, '', currentPath); };
window.onclick = function(event) { if (event.target == modal) { modal.style.display = 'none'; history.pushState({ path: currentPath }, '', currentPath); } }; }); </script> </body> </html>
|
3、在项目根目录创建 package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| { "name": "blogpic", "version": "1.0.0", "description": "使用Vercel一键部署实现图床功能", "main": "server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node server.js" }, "keywords": [], "author": "shiguang-coding", "license": "ISC", "dependencies": { "express": "^4.19.2", "fs": "^0.0.1-security", "path": "^0.12.7" } }
|
4、在项目根目录创建 server.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| const express = require('express'); const fs = require('fs'); const path = require('path');
const app = express();
const port = process.env.PORT || 8080;
app.use(express.static('public'));
app.get('/api/files', (req, res) => { const baseDir = path.join(__dirname, 'public'); fs.readdir(baseDir, { withFileTypes: true }, (err, files) => { if (err) { return res.status(500).json({ error: 'Unable to scan directory' }); } const result = []; files.forEach(file => { if (file.isDirectory()) { result.push({ name: file.name, images: [] }); } }); res.json(result); }); });
app.get('/api/files/:folder(*)', (req, res) => { const folderPath = path.join(__dirname, 'public', req.params.folder); fs.readdir(folderPath, { withFileTypes: true }, (err, files) => { if (err) { return res.status(500).json({ error: 'Unable to scan directory' }); } const result = []; files.forEach(file => { if (file.isDirectory()) { result.push({ name: file.name, images: [] }); } else if (file.isFile() && file.name.match(/\.(jpg|jpeg|png|gif)$/i)) { result.push({ name: '', images: [`${req.params.folder}/${file.name}`] }); } }); res.json(result); }); });
app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); });
|
5、安装和运行
前提条件
确保你已经安装了 Node.js
和 npm
。
安装依赖
运行程序
运行后,打开浏览器访问 http://localhost:8080
,即可看到图床应用。
项目结构
1 2 3 4 5
| your-project/ ├── public/ │ └── index.html ├── server.js └── package.json
|
图片测试
下面三张图分别是使用PicGo上传GitHub图床未设定自定义域名,设置 jsdelivr CDN, 和 Vercel部署 的图片,为方便后期查看图床加载速度以及是否失效。
未设定自定义域名:
jsdelivr :
Vercel :
参考
谢谢你,鱼皮!15分钟搭建vercel+github+picgo个人免费图床
使用Github+Vercel搭建图床并通过自定义域名进行加速你的视频或者图片
使用 Github 搭建图床并通过 Vercel 加速访问