前言

上半年事比较多,好久不更新啦。一直想把小张列传单独分离一个博客,前段时间用Typecho搭了一个博客,Ty主题很多但是没有一个主题可以把我想要的所有功能放到一起,所以继承了Hexo衣钵,没有就魔改!想加一个相册功能,不需要额外管理(不想每传一张图再写一张图片地址到某个地方,主打一个懒)为了实现动态显示,大多人图床依托存储桶或者兰空,所以从这个角度出发,直接调用他们的接口动态获取图片集合。印象里莱昂纳斯和林木木做过相关瀑布流相册,所以借鉴了他们。

参考:

Ty效果:相册 - zhsher

butterfly效果

photo wall06

方案一

方案一仅做思路记录,密钥会暴露在前端,不建议使用

腾讯云

免部署,使用快捷,但密钥暴露在前端,切记遵循 最小权限指引原则 对永久密钥的权限范围进行限制

首先参照对象存储 快速入门-SDK 文档中准备环境,如下

  1. 登录 对象存储控制台 ,获取存储桶名称和地域名称

  2. 遵循最小权限原则,使用子用户的 SecretIdSecretKey。登录 控制台-新建用户 ,选择快速创建,依次添加用户名,编程访问,QcloudCOSReadOnlyAccess(只读权限)

    photo wall01

  3. 配置 CORS 规则,AllowHeader 需配成*或自己域名,操作为 GET,其它默认

    photo wall02

  4. 以Hexo为例,新建页面hexo new page photos,插入如下代码,填写42~64行配置

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<!-- 腾讯云SDK -->
<script src="https://npm.elemecdn.com/cos-js-sdk-v5/dist/cos-js-sdk-v5.min.js"></script>
<!-- 瀑布流排版 https://github.com/raphamorim/waterfall.js -->
<script src="//cdnjs.cloudflare.com/ajax/libs/waterfall.js/1.0.2/waterfall.min.js"></script>
<!-- imgStatus https://github.com/raphamorim/imgStatus -->
<script src="https://npm.elemecdn.com/imgstatus/imgStatus.min.js"></script>
<!-- 图片时间 https://github.com/Tokinx/lately -->
<script src="https://jsd.onmicrosoft.cn/gh/Tokinx/lately/lately.min.js"></script>
<!-- 图片灯箱 https://github.com/Tokinx/ViewImage -->
<script src="https://jsd.onmicrosoft.cn/gh/Tokinx/ViewImage/view-image.min.js"></script>
<style>
/* bottom: 0px适配jasmine主题 */
.gallery-photos a img {margin: 0; border-radius:0;bottom: 0px;}
.gallery-photos{width:100%;margin-top: 10px;}
.gallery-photo{min-height:5rem;width:24.97%;padding:4px;position: relative;}
.gallery-photo a{border-radius:8px;display:block;overflow: hidden;}
.gallery-photo img{display: block;width:100%;animation: fadeIn 1s;cursor: pointer;transition: all .4s ease-in-out !important;}
.gallery-photo span.photo-title,.gallery-photo span.photo-time{max-width: calc(100% - 7px);line-height:1.8;position:absolute;left:4px;font-size:14px;background: rgba(0, 0, 0, 0.3);padding:0px 8px;color: #fff;animation: fadeIn 1s;}
.gallery-photo span.photo-title{bottom:4px;border-radius: 0 8px 0 8px;}
.gallery-photo span.photo-time{top:4px;border-radius: 8px 0 8px 0;}
.gallery-photo:hover img{transform: scale(1.1);}
@media screen and (max-width: 1100px) {.gallery-photo{width:33.3%;}}
@media screen and (max-width: 768px) {
.gallery-photo{width:49.9%;padding:3px}
.gallery-photo span.photo-time{display:none}
.gallery-photo span.photo-title{font-size:12px}
.gallery-photo span.photo-title{left:3px;bottom:3px;}
}
@keyframes fadeIn{0% {opacity: 0;}100%{opacity: 1;}}
</style>
</head>
<body>
<!-- 挂载元素 -->
<div class="gallery-photos page"></div>

<script type="text/javascript">
// start 需要手动配置
// 自适应 填写当前页面路径例如blog.zhsher.cn/photos就填photos
window.onresize = () => {
if (location.pathname == '/photos/') waterfall('.gallery-photos');
};
// 存储桶自定义域名
domain = 'imgl.zhsher.cn'
// SECRETID 和 SECRETKEY 请登录 https://console.cloud.tencent.com/cam/capi 进行查看和管理
var cos = new COS({
SecretId: 'AKIDTmpcATXaOzLRP7ankRs4VkjUzMUN1PYS',
SecretKey: 'iY9VbVO4WpgRwCg4KOI4cz87nJDwWs1z',
});
// 过滤不想展示的图片 存储桶的绝对路径
exclude = []

cos.getBucket({
// 存储桶名称
Bucket: 'zh-13104467',
// 存储桶所在地域
Region: 'ap-nanjing',
// 非必须,文件夹路径,默认全部
Prefix: '',// end 配置结束
}).then(data => {
// 处理拼接图片信息到photo_list filter过滤文件后缀
const photo_list = data.Contents
.filter(item => /\.(jpg|png|webp)$/.test(item.Key) && !exclude.includes(item.Key))
.map(item => ({
img: `https://${domain}/` + item.Key,
title: item.Key.match(/([^/]+)\.\w+$/)[1],
time: item.LastModified.replace(/[TZ]/g, ' ').slice(0, -5)
}));
// 较新时间放在前
// 默认排序 文件夹路径优先时间排序 B/x.webp 2023.10,A/x.webp 2022.10 A排在B前,不是完全按时间顺序,所以不能用photo_list.reverse();
photo_list.sort(function(a, b) {
var timeA = new Date(a.time);
var timeB = new Date(b.time);
return timeB - timeA;
});
// 拼接dom元素 插入到.gallery-photos.page
let html = '';
photo_list.forEach(item => {
html +=
`<div class="gallery-photo"><a href="${item.img}" data-fancybox="gallery" class="fancybox" data-thumb="${item.img}"><img class="photo-img" loading='lazy' decoding="async" src="${item.img}"></a>`;
item.title ? html += `<span class="photo-title">${item.title}</span>` : '';
item.time ? html += `<span class="photo-time">${item.time}</span>` : '';
html += `</div>`;
});
document.querySelector('.gallery-photos.page').innerHTML = html;

// 瀑布流排版
imgStatus.watch('.photo-img', () => {
waterfall('.gallery-photos');
});

// 处理时间为x天前
window.Lately && Lately.init({
target: '.photo-time'
});

// 图片灯箱
window.ViewImage && ViewImage.init('.gallery-photo img');
});
</script>
</body>
</html>

Lsky图床

免部署,使用快捷,但密钥暴露在前端,Lsky无法权限限制,不要使用!!!

  1. 申请token,开源版需要使用命令行。企业版在前端面板中申请

    curl --location --request POST 'http://域名/api/v1/tokens' --form 'email="邮箱"' --form 'password="密码"'
  2. 以Hexo为例,新建页面hexo new page photos,插入如下代码,填写40~54行配置

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<!-- 瀑布流排版 https://github.com/raphamorim/waterfall.js -->
<script src="//cdnjs.cloudflare.com/ajax/libs/waterfall.js/1.0.2/waterfall.min.js"></script>
<!-- imgStatus https://github.com/raphamorim/imgStatus -->
<script src="https://npm.elemecdn.com/imgstatus/imgStatus.min.js"></script>
<!-- 图片时间 https://github.com/Tokinx/lately -->
<script src="https://jsd.onmicrosoft.cn/gh/Tokinx/lately/lately.min.js"></script>
<!-- 图片灯箱 https://github.com/Tokinx/ViewImage -->
<script src="https://jsd.onmicrosoft.cn/gh/Tokinx/ViewImage/view-image.min.js"></script>
<style>
/* bottom: 0px适配jasmine主题可能不适用你的主题,可以删掉 */
.gallery-photos a img {margin: 0; border-radius:0;bottom: 0px;}
.gallery-photos{width:100%;margin-top: 10px;}
.gallery-photo{min-height:5rem;width:24.97%;padding:4px;position: relative;}
.gallery-photo a{border-radius:8px;display:block;overflow: hidden;}
.gallery-photo img{display: block;width:100%;animation: fadeIn 1s;cursor: pointer;transition: all .4s ease-in-out !important;}
.gallery-photo span.photo-title,.gallery-photo span.photo-time{max-width: calc(100% - 7px);line-height:1.8;position:absolute;left:4px;font-size:14px;background: rgba(0, 0, 0, 0.3);padding:0px 8px;color: #fff;animation: fadeIn 1s;}
.gallery-photo span.photo-title{bottom:4px;border-radius: 0 8px 0 8px;}
.gallery-photo span.photo-time{top:4px;border-radius: 8px 0 8px 0;}
.gallery-photo:hover img{transform: scale(1.1);}
@media screen and (max-width: 1100px) {.gallery-photo{width:33.3%;}}
@media screen and (max-width: 768px) {
.gallery-photo{width:49.9%;padding:3px}
.gallery-photo span.photo-time{display:none}
.gallery-photo span.photo-title{font-size:12px}
.gallery-photo span.photo-title{left:3px;bottom:3px;}
}
@keyframes fadeIn{0% {opacity: 0;}100%{opacity: 1;}}
</style>
</head>
<body>
<!-- 挂载元素 -->
<div class="gallery-photos page"></div>

<script type="text/javascript">
// start 需要手动配置
// 自适应 填写相册页面路径例如blog.zhsher.cn/photos就填photos
window.onresize = () => {
if (location.pathname == '/photos/') waterfall('.gallery-photos');
};

// 兰空域名
domain = 'test.zhsher.cn'
var myHeaders = new Headers();
// 兰空token
myHeaders.append("Authorization", "Bearer 1|zsNyAN4hPx0xWONcq8Zu01UpynZWQcTgBg26R6iP");
myHeaders.append("Accept", "application/json");
// 过滤不想展示的图片
exclude = ['2023/06/04/web开发07.webp']
// end 配置结束
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow',
};

fetch(`http://${domain}/api/v1/images`, requestOptions)
.then(response => response.json())
.then(result => {
page_num = result.data.last_page; //获取页码
getPhoto(page_num)
})
.catch(error => console.log('error', error));

function getPhoto(page_num) {
// 需要遍历的页码
const urls = [];
for (var i = 1; i <= page_num; i++) {
urls.push(`http://${domain}/api/v1/images?page=${i}`)
}

const data = {};
const photo_list = []; // 保存每页的图片信息
Promise.all(urls.map(url =>
fetch(url, requestOptions)
.then(response => response.json())
.then(result => {
data[result.data.current_page] = result.data.data
})
.catch(error => console.log('error', error)))).then(() => {
//按页码保存图片信息到photo_list
for (var i = 1; i <= page_num; i++) {
photo_list.push(...data[i].filter(item => !exclude.includes(item.pathname)).map(item => ({
img: item.links.url,
title: item.name.match(/^(.*)\.(webp|png|jpg)$/)[1],
time: item.date
})))
}

// 拼接dom元素 插入到.gallery-photos.page
let html = '';
photo_list.forEach(item => {
html +=
`<div class="gallery-photo"><a href="${item.img}" data-fancybox="gallery" class="fancybox" data-thumb="${item.img}"><img class="photo-img" loading='lazy' decoding="async" src="${item.img}"></a>`;
item.title ? html += `<span class="photo-title">${item.title}</span>` : '';
item.time ? html += `<span class="photo-time">${item.time}</span>` : '';
html += `</div>`;
});
document.querySelector('.gallery-photos.page').innerHTML = html;

// 瀑布流排版
imgStatus.watch('.photo-img', () => {
waterfall('.gallery-photos');
});

// 处理时间为x天前
window.Lately && Lately.init({
target: '.photo-time'
});

// 图片灯箱
window.ViewImage && ViewImage.init('.gallery-photo img');
});
}
</script>
</body>
</html>

方案二

为了解决方案一token暴露的痛点,想用一个后端解决,之前实时文章排参照hexo-circle-of-friends项目用了py,想尝试新东西,因为先有了方案一用了很多JS,所以采用node改写会很方便。主要参照Lenous的微博自建api仿着写的。

区别方案一做了如下修改:

  1. token不再暴露于前端
  2. 两种后端部署方式二选一,支持Vercel/服务器
  3. 减少cdn消耗,不再加载所有照片,对照片做了分页,每页20张图片,点击加载更多在底部追加20张(又是参考朋友圈思路嘿嘿)
  4. COS和Lsky在后端处理成相同Json格式,前端共用一个模板,代码更加简洁清晰
  5. Butterfly主题懒加载图片无法显示

获取变量

按照方案一中腾讯云的1、2步或Lsky图床的第一步获取需要的变量

腾讯云

变量名解释示例
cos_domain存储桶自定义域名lsky.pro
cos_SecretId存储桶SECRETID
cos_SecretKey存储桶SECRETKEY
cos_bucket存储桶名称name-xxxx
cos_region存储桶地域ap-nanjing
cos_prefix文件夹路径,可留空显示全部blog
exclude排除某个图片的绝对路径,可留空'blog/1.webp','blog/2.webp

兰空

变量名解释示例
lsky_domain兰空域名lsky.pro
lsky_token兰空TokenBearer 1 | zsNyAN4hPx0xWONcq8Zu01UpynZWQcTgBg26R6iP
exclude排除某个图片的绝对路径,可留空'blog/1.webp','blog/2.webp

后端-Vercel

  1. 点击按钮开始部署: Deploy with Vercel,直接使用 Github 账号登录即可

  2. 配置变量,Settings->Environment Variables->依次添加变量->Save,以腾讯云为例配置如图

    photo wall03

  3. 保存后重新部署使变量生效,Deployments->点击最上边一行的三个点->Redeploy,如上图所示

  4. 部署完毕后访问测试,腾讯云接口为https://域名/cos,Lsky域名为https://域名/lsky

  5. 绑定自定义域名,Vercel 分配的域名 DNS 被污染了, 绑定自定义域名即可直连。Setting->Domains

    hexo-hot-article 实时文章排行及访客地图04

后端-服务器

打开宝塔面板,在wwwroot下新建一个目录,然后新建 index.jspackage.json 并填写仓库GC-ZF/Cos-Lsky-PhotoWall内对应文件内容。其中indexl.js的8~37行诸如process.env['cos_domain']需要替换为你实际的配置,如果只用COS就只需要对COS修改

photo wall04

建好之后,依次点击 左侧导航栏网站->node项目->添加node项目,然后填写配置,没有node需要安装node

  • 项目目录:刚才创建的目录
  • 项目端口:3000,需要修改在index.js第五行。记得在腾讯云服务器面板的防火墙放行端口
  • 绑定域名:建议申请并配置一下ssl,有的网站不能使用http。不配置域名使用服务器ip:3000
  • 其他:默认即可

photo wall05

前端

以Hexo为例,新建页面hexo new page photos,插入如下代码,填写36~43行配置

<!-- 瀑布流排版 https://github.com/raphamorim/waterfall.js  -->
<script src="//cdnjs.cloudflare.com/ajax/libs/waterfall.js/1.0.2/waterfall.min.js"></script>
<!-- imgStatus https://github.com/raphamorim/imgStatus -->
<script src="https://npm.elemecdn.com/imgstatus/imgStatus.min.js"></script>
<!-- 图片时间 https://github.com/Tokinx/lately -->
<script src="https://jsd.onmicrosoft.cn/gh/Tokinx/lately/lately.min.js"></script>
<!-- 图片灯箱 https://github.com/Tokinx/ViewImage -->
<script src="https://jsd.onmicrosoft.cn/gh/Tokinx/ViewImage/view-image.min.js"></script>
<style>
/* bottom: 0px适配jasmine主题,margin: 0!important;适配butterfly 可能不适用其它主题,可以删掉 */
.gallery-photos a img {margin: 0!important; border-radius:0;bottom: 0px;}
.gallery-photos{width:100%;margin-top: 10px;}
.gallery-photo{min-height:5rem;width:24.97%;padding:4px;position: relative;}
.gallery-photo a{border-radius:8px;display:block;overflow: hidden;}
.gallery-photo img{display: block;width:100%;animation: fadeIn 1s;cursor: pointer;transition: all .4s ease-in-out !important;}
.gallery-photo span.photo-title,.gallery-photo span.photo-time{max-width: calc(100% - 7px);line-height:1.8;position:absolute;left:4px;font-size:14px;background: rgba(0, 0, 0, 0.3);padding:0px 8px;color: #fff;animation: fadeIn 1s;}
.gallery-photo span.photo-title{bottom:4px;border-radius: 0 8px 0 8px;}
.gallery-photo span.photo-time{top:4px;border-radius: 8px 0 8px 0;}
.gallery-photo:hover img{transform: scale(1.1);}
@media screen and (max-width: 1100px) {.gallery-photo{width:33.3%;}}
@media screen and (max-width: 768px) {
.gallery-photo{width:49.9%;padding:3px}
.gallery-photo span.photo-time{display:none}
.gallery-photo span.photo-title{font-size:12px}
.gallery-photo span.photo-title{left:3px;bottom:3px;}
}
@keyframes fadeIn{0% {opacity: 0;}100%{opacity: 1;}}
.uploadmore{width:40%;max-width:810px;height:30px;margin:auto;border-radius:12px;font-weight:700;text-align:center;display:flex;align-items:center;justify-content:center;cursor:pointer;border:1px solid #000;box-shadow:0 8px 16px -4px #2c2d300c;background:#f4f4f4}
/* butterfly和ty的夜间加载按钮 */
[data-theme=dark] .uploadmore,.dark .uploadmore{background: #1D1E3C;}
</style>

<!-- 挂载元素 -->
<div class="gallery-photos page"></div>
<script type="text/javascript">
// start 需要手动配置
// 自适应 填写相册页面路径例如blog.zhsher.cn/photos就填photos
window.onresize = () => {
if (location.pathname == '/photos/') waterfall('.gallery-photos');
};
// vercel配置的api接口 腾讯云为`域名/cos` 兰空为`域名/lsky`
api = 'https://cos-lsky-photo-wall.zhsher.cn/cos'
// end 配置结束
// 插入加载更多按钮
var element = document.createElement('div');
element.classList.add('uploadmore');
element.innerHTML = `加载中...`
document.querySelector('.gallery-photos.page').insertAdjacentElement('afterend', element);
fetch(`${api}`)
.then(response => response.json())
.then(result => {
let page = []; //存放分页
let current_page = 0; // 当前页码
// 将照片分页 每页20张
for (var i = 0; i < result.length / 20; i++) {
page.push(result.slice(i * 20, (i + 1) * 20))
}
// 先加载一页
getPhoto(page[0]);
updateButton(current_page, page.length - 1);
current_page++;
// 点击加载更多
document.querySelector('.uploadmore').onclick = function() {
getPhoto(page[current_page]);
updateButton(current_page, page.length - 1)
current_page++;
}
})
.catch(error => console.log('error', error));
function getPhoto(photo_list) {
// 拼接dom元素 插入到.gallery-photos.page
let html = '';
photo_list.forEach(item => {
html +=
`<div class="gallery-photo"><a href="${item.img}" data-fancybox="gallery" class="fancybox" data-thumb="${item.img}"><img class="photo-img " loading='lazy' decoding="async" src="${item.img}"></a>`;
item.title ? html += `<span class="photo-title">${item.title}</span>` : '';
item.time ? html += `<span class="photo-time">${item.time}</span>` : '';
html += `</div>`;
});
document.querySelector('.gallery-photos.page').innerHTML += html;
// 瀑布流排版
imgStatus.watch('.photo-img', () => {
waterfall('.gallery-photos');
});
// 处理时间为x天前
window.Lately && Lately.init({
target: '.photo-time'
});
// 图片灯箱
window.ViewImage && ViewImage.init('.gallery-photo img');
}
// 更新按钮文字
function updateButton(current_page, page) {
// 第一次先加载一次按钮 只有一页时page=0所以额外判断一次
if (current_page == 0 && page != 0) {
document.querySelector('.uploadmore').innerHTML = `加载更多`
} //全部加载完
else if (current_page == page) {
document.querySelector('.uploadmore').innerHTML = `已加载全部`
}
}
</script>

butterfly额外操作1

如果是butterfly主题,主题自带灯箱,修改前端模板

- // 图片灯箱
- window.ViewImage && ViewImage.init('.gallery-photo img');

butterfly额外操作2

如果是butterfly主题且开启了lazyload,图片可能会显示空白。参照butterfly控制特定图片懒加载 | 安知鱼中方法二,修改前端模板

- <img class="photo-img" loading='lazy' decoding="async" src="${item.img}">;
+ <img class="photo-img no-lazyload" loading='lazy' decoding="async" src="${item.img}">;

修改themes/butterfly/scripts/filters/post_lazyload.js的第13行

- return htmlContent.replace(/(<img.*? src=)/ig, `$1 "${bg}" data-lazy-src=`);
+ return htmlContent.replace(/(<img(?!.*?class[\t]*=[\t]*['"].*?no-lazyload.*?['"]).*? src=)/gi, `$1 "${bg}" data-lazy-src=`);