0%

使用cloudflare搭建个人图床

上次分享了一篇使用 cloudflare 作为个人图床的方案,但是比较粗糙,仅仅是直接使用 cloudflare 的 R2 的控制台上传图片,自己拼接图片地址,属于能用,但并不好用的状态。

这次我们更产品一点,做一个可视的个人图床应用,包括图片上传、删除、查看、搜索。如果需要将其真正用于生产,还需要一个域名,因为 cloudflare 的免费 dev 域名是被墙掉的。

最终成品be like:

图床应用-列表

图床应用-图片列表

图床应用-列表

图床应用-上传图片

使用到的平台包括 cloudflare 和 GitHub。GitHub 用于存储代码。cloudflare 要使用其如下功能:

  • Worker 作为计算单元执行存储、查询逻辑
  • Page 服务作为前端网页托管平台
  • R2 作为图片存储
  • D1 作为数据库

对于个人用户,这些服务几乎都是免费的(每日免费额度对个人用户几乎用不完),。

方案架构

cloudflare个人图床架构

注册并创建服务

这并不是一篇面向小白的文章,所以这里假定你已经熟练掌握 GitHub 的使用了。但如果没有使用过 cloudflare ,先去其官网简单创建一个账号。然后开通以下服务:

创建一个 R2 bucket,随便取个名字,比如 image-storage-demo 。创建一个 D1 数据库取名比如 image-storage-record ,创建一张表叫 images ,字段如下:

接下来创建 worker,创建 worker 时需要稍微注意下,要创建两个,分别用于做页面渲染(前端)和业务逻辑(后端)。虽然worker可以同时做前后端,但为了长期的可维护性,这里我们还是区分下前端和服务端。

worker 取个名字比如 image-storage-worker

创建前端项目

在 GitHub 上创建一个仓库,clone到本地,初始化一个前端项目,前端框架这里为了尝鲜,选择了 SolidJS 。关于这个框架的更多信息,可自行查询相关文档。也可以选择自己喜欢的前端框架比如 Vue、React 等,都一样。

npm create vite@latest my-app -- --template solid-ts 即可创建项目。

为了有一些基础的样式,可以使用 tailwindCSS 作为样式库,直接去 tailwindCSS 的官网找 SolidJS 框架的安装指南照着操作即可,嫌麻烦也可以省略,又不是不能用.jpg。

部署前端页面

简单编写一个图片上传组件:

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
import { createSignal } from 'solid-js'

function ImageUpload () {
const [selectedFile, setSelectedFile] = createSignal(null)

const handleFileChange = (event) => {
const file = event.target.files[0]
setSelectedFile(file)
}

const handleUpload = async () => {
if (!selectedFile()) {
alert('Please select a file first')
return
}

const formData = new FormData()
formData.append('file', selectedFile())

try {
const response = await fetch('https://{上传接口地址}/', {
method: 'POST',
body: formData,
})

if (response.ok) {
const result = await response.json()
console.log('result', result)
window.alert('上传成功')
} else {
console.error('Upload failed. HTTP status:', response.status)
}
} catch (error) {
console.error('Error during upload:', error)
}
}

return (
<div class="max-w-screen-md mx-auto p-4">
<h1 class="text-2xl font-bold mb-4">Image Upload</h1>
<input
type="file"
accept="image/*"
onChange={handleFileChange}
class="mb-4"
/>

<button onClick={handleUpload} class="bg-blue-500 text-white py-2 px-4 rounded">
Upload Image
</button>
</div>
)
}

export default ImageUpload

注意这里上传图片的接口还没写,等下写完 worker 逻辑再加。引入到 app.ts 里,使用它将其展示出来,即可提交代码到 Github。

这时再去 cloudflare worker page 功能下,选关联已有前端项目,关联这个 solidJS 项目,然后就会自动给分配一个域名并部署,如果你有自己的域名,也可以配置自定义域名,即可无需科学访问。

编写服务端逻辑

需要安装 cloudflare 的命令行工具,叫 wrangler ,具体参考其官方文档。这里简单写下流程:先安装依赖 npm install -g wrangler,再登录 wrangler login ,接着把项目搞到本地来写 wrangler init --from-dash image-storage-worker (注意和前面创建 worker 时名字一致),可以额外建立一个 git 仓库存储服务端逻辑代码。

项目到本地后首先编辑 wrangler.toml,加上你的 R2 bucket 和 D1:

1
2
3
4
5
6
7
[[d1_databases]]
binding = "DATABASE" # JS逻辑里使用时的变量名
name = "image-storage-record" # 和前面创建的 D1 数据库名一致

[[r2_buckets]]
binding = 'imageOSS' # JS逻辑里使用时的变量名
bucket_name = 'image-storage-demo' # 和前面创建的 R2 bucket 名一致

编辑src/index.ts,修改如下:

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
const corsHeaders = {
'Access-Control-Allow-Headers': '*', // What headers are allowed. * is wildcard. Instead of using '*', you can specify a list of specific headers that are allowed, such as: Access-Control-Allow-Headers: X-Requested-With, Content-Type, Accept, Authorization.
'Access-Control-Allow-Methods': '*', // Allowed methods. Others could be GET, PUT, DELETE etc.
'Access-Control-Allow-Origin': '*', // This is URLs that are allowed to access the server. * is the wildcard character meaning any URL can.
};

export default {
async fetch(request, env) {
const requestURL = new URL(request.url);
// 查看所有图片
if (request.method === 'GET' && requestURL.pathname === '/query') {
return await handleQueryImage(request, env);
}

// 处理图片上传
if (request.method === 'POST' && requestURL.pathname === '/upload') {
return await handleImageUpload(request, env);
}

return new Response('Invalid request', { status: 400 });
},
};

const handleImageUpload = async (request, env) => {
const { DATABASE, imageOSS } = env;

const formData = await request.formData();
const file = formData.get('file');

if (file) {
const path = `${file.name}`;
const imageFullPath = `https://{这里换成你的R2域名}/${path}`;
await imageOSS.put(path, file);
const createdAt = `${+new Date()}`;

try {
const { success } = await DATABASE.prepare(
`insert into images (imageName, imageUrl, createdAt) values (?, ?, ?)`,
)
.bind(path, imageFullPath, createdAt)
.run();

return new Response(JSON.stringify({ url: imageFullPath, success }), {
headers: {
...corsHeaders,
},
});
} catch (e) {
return new Response(
JSON.stringify({
success: false,
error: JSON.stringify(e),
}),
{
headers: {
...corsHeaders,
},
},
);
}
}
};

const handleQueryImage = async (request, env) => {
const { DATABASE } = env;
const requestURL = new URL(request.url);
const pageNum = Number(requestURL.searchParams.get('pageNum')) || 1;
const pageSize = Number(requestURL.searchParams.get('pageSize')) || 10;
const offset = (pageNum - 1) * pageSize;
const sql = `select * from images order by id DESC LIMIT ${pageSize} OFFSET ${offset}`;

const rows = await DATABASE.batch([
DATABASE.prepare(sql),
DATABASE.prepare(`SELECT COUNT(*) AS total FROM images order by id DESC`),
]);
return new Response(
JSON.stringify({
results: rows[0].results,
total: (rows[1]?.results && rows[1]?.results[0]?.total) || 0,
success: rows[0].success && rows[1].success,
}),
{
headers: {
...corsHeaders,
},
},
);
};

接下来只需要简单的 wrangler deploy 即可部署完成。然后去看下 cloudflare 给你分配的域名,或者是绑定一个自己的自定义域名上去。拿着这个域名我们就可以去连接前后端了。

连接前后端

刚刚前端上传页面留了一个上传地址,填入 https://{刚刚拿到的域名}/upload 即对接好了图片上传,worker收到请求会一边把图片存到 R2 里一边把图片信息记录到 D1 里。 可以尝试下看是否返回了成功。

成功后我们去 cloudflare 的控制台去 D1 里看一眼数据是否入库,再去 R2 里看一眼图片文件是否存在,一切正常后我们再编写一个列表展示页。

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
import { createResource, createSignal, For } from 'solid-js'
import dayjs from 'dayjs'
import copy from 'clipboard-copy'

const fetchPics = async ({ pageNum, pageSize }) =>
(await fetch(`https://{你的worker地址}/query?pageSize=${pageSize}&pageNum=${pageNum}`)).json()

function ImageHome () {
const pageSize = 20

const [pageNo, setPageNo] = createSignal(1)

const fetchOption = () => {
return {
pageNum: pageNo(),
pageSize,
}
}

const [picList] = createResource(fetchOption, fetchPics)

const copyToClipboard = async (text) => {
try {
// 调用 clipboard-copy 库的方法将内容复制到剪贴板
await copy(text)
} catch (error) {
console.error('Error copying to clipboard:', error)
}
}

return (
<div>
<div class="flex p-4 lg:p-10 justify-between lg:pb-0 pb-0 items-center">
<h1 class="p-4 text-2xl">图片列表</h1>

<p>当前第 {pageNo()} 页,共 {picList.loading ? 'loading' : picList().total} 条数据</p>
</div>

<div class="p-4 lg:p-10">
<span>{picList.loading && 'Loading...'}</span>

{!picList.loading && (
<div class="grid 3xl:grid-cols-4 2xl:grid-cols-3 lg:grid-cols-2 sm:grid-cols-1 gap-6 2xl:gap-8 pb-10">
<For each={picList().results}>{(picItem) =>
<div
class="flex flex-col overflow-hidden items-center justify-center bg-slate-200 rounded-xl p-0 flex-wrap md:flex-nowrap">
<div class="flex flex-shrink-0 items-center w-auto h-96 rounded-none mx-auto p-2 justify-center">
<img class="h-full object-contain" src={picItem.imageUrl}/>
</div>

{/*信息区*/}
<div class="w-full pt-2 pl-5">
<div>图片名: {picItem.imageName}</div>
<div>创建时间: {dayjs(+picItem.createdAt).format('YYYY/MM/DD HH:mm:ss')}</div>
<div>完整地址: {picItem.imageUrl}</div>
</div>

{/*操作区*/}
<div class="w-full flex p-3 justify-between">
<div
class="m-2 w-full text-center bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded cursor-pointer"
onClick={() => copyToClipboard(picItem.imageUrl)}>复制
</div>
</div>
</div>
}</For>
</div>
)}
</div>

<div class="fixed bottom-0 left-0 p-4 w-full flex flex-1 mt-8 justify-between bg-white shadow-inner">
{pageNo() > 1 ? (
<p
class="w-22 relative inline-flex justify-center items-center rounded-md border border-gray-300 text-white px-4 py-2 text-sm font-medium bg-blue-500"
onClick={() => (setPageNo((p) => p - 1))}
>上一页</p>
) : <p class="w-20"/>}

<p class="font-medium pt-2">第 {pageNo()} 页</p>

{!picList.loading && pageNo() * pageSize < picList().total ? (
<p
class="w-20 relative inline-flex justify-center items-center rounded-md border border-gray-300 text-white px-4 py-2 text-sm font-medium bg-blue-500"
onClick={() => (setPageNo((p) => p + 1))}
>下一页</p>
) : <p class="w-20"/>}
</div>
</div>
)
}

export default ImageHome

这个组件和图片上传组件可以放在一个页面里,也可以自行查询 SolidJS 关于路由的部分,去分两个页面实现。

本地预览一起正常后,提交代码即可触发自动构建部署,公网访问看看是否一切正常了。

删除图片

就当留个小练习吧,删除逻辑的代码被我隐藏掉了,但是其实很简单,注册个 /delete 路由,接收 id ,编写 sql 即可,请自行实现吧。

结语

好了以上就是全部内容,就可以获得一个10GB的OSS存储空间用于做图床,写博客之类的都可以更方便地贴图了。有问题欢迎留言欢迎交流,如果感兴趣的朋友多,后续可以考虑开源相关代码。