为 Hexo + Fluid 博客添加承载相册的页面

本文最后更新于:1 年前

Reference


TODO

  • 我想要的相册 tab 是 https://gishai.top/blog/photos/ 所展示的这样,Fluid 博主在博客[1]分享出来的是书签形式的,不是这种标签形式的。我还没有实现出来。
  • mp4 文件宽高信息获取。
  • 相册在页面中显示的顺序是无序的,需要修改为有序的。

版本信息


完整的文件目录结构

因为涉及的文件比较多,把完整的文件目录结构看一下 只展开相对于 Hexo 的增量部分,然后再逐步实现细节:


创建相册的页面

比如我需要创建 photo 页面,执行 Hexo 命令:

1
hexo new page "photo"

会创建 source/photo,里面有一个文件 index.md
motor 页面、skiing 页面和 photo 页面同理。

index.md 文件的默认内容由 scaffolds/page.md 决定。

修改 index.md 关键是 layout: photo,这个 photo 和 photo 页面不是一回事,设置时没注意所以重名了

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
---
title: 摄影
subtitle: ''
excerpt: ''
banner_img: /img/my_background.jpg
math: false
mermaid: false
category_bar: false
comments: false
lazyload: true
hide: false
date: 2022-11-19 09:04:03
layout: photo
index_img:
tags:
categories:
updated:
---

<style>
.ImageGrid {
width: 100%;
max-width: 1040px;
margin: 0 auto;
text-align: center;
}
.card {
overflow: hidden;
transition: .3s ease-in-out;
border-radius: 8px;
background-color: #efefef;
padding: 1.4px;
}
.ImageInCard img {
padding: 0;
border-radius: 8px;
width:100%;
height:100%;
}
@media (prefers-color-scheme: dark) {
.card {background-color: #333;}
}
</style>

<div id="imageTab"></div>
<div class="ImageGrid"></div>

_config.fluid.yml 文件的 menu 里添加页面的访问入口:

1
{ key: "摄影", link: "/photo/", icon: "iconfont icon-my-camera" }

准备图片

处理图片

先把图片在 NAS 中用 Qfiling 处理一遍,处理后生成原图和缩略图。
缩略图里的文件名和扩展名都和原图里的一致。
因为预览时设置了缩略图的最大宽度为 240,按照竖向图片高宽比 2:1 计算,宽度、高度均不超 480,长宽比不变即可。

这里有一个坑:
视频文件生成不了缩略图,只能自己截图(phototool.js 只处理了 png 格式,所以图片只能是 png 文件,且宽高数据需要和视频一致,否则加载时会有问题),然后把视频和截图的 png 一起放到对应的文件夹里,让 Qfiling 处理,处理后的文件需要删除缩略图里的视频和 png,把原图里的 png 作为缩略图使用,毕竟视频的宽高一般都不太大。

原图缩略图里分别有3个文件夹 motorphotoskiing 对应3个页面,每个页面里可以放多个相册。

图片可以放在 source 内对应页面的文件夹里,也可以放在图床。
放在 source 内的文件会在执行 hexo g 命令时放进 public,最终会被上传到 GitHub 如果使用了 Github Pages

上传图片到图床

我的图片是放在腾讯的图床,所以可以在 Hexo 的根目录创建文件夹 images
把处理后的原图缩略图复制到 images

然后上传图片到图床,上传时不能按照页面上传,要按照相册上传,否则后面 image.json 还得处理:

安装 image-size

image-size 的功能是获取图片的宽高信息不支持获取非图片格式的宽高,比如视频

执行命令:

1
npm i image-size

使用 phototool.js 生成 image.json

image.json 的作用是建立图片和相册、页面的索引。

NexT 博主的页面是平铺的,没有相册概念,所以 phototool.js[2] 生成的 json 是一层的:

1
2
3
4
5
6
[
"4032.3024 IMG_0391.webp",
"12500.3874 IMG_0404.webp",
"4032.3024 IMG_0416.webp",
"4032.3024 IMG_0424.webp",
]

Fluid 博主的页面是两层的,使用 create.js 生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[
{
"name": "广州一游",
"children": [
"1080.1440 圣心大教堂.jpg",
"1080.1440 广州塔顶夜晚景色.jpg",
"1080.1440 广州塔顶夜晚景色2.jpg",
"1080.1440 晚上广州塔.jpg",
"1080.1443 白天广州塔.jpg"
]
},
{
"name": "澳门游玩",
"children": [
"1080.1443 夜晚澳门巴黎铁塔.jpg",
"1443.1080 微信图片_20210108213615.jpg",
"1443.1080 微信图片_20210108213635.jpg",
"1443.1080 微信图片_20210108213645.jpg",
"1443.1080 微信图片_20210108213707.jpg",
"1080.1443 白天澳门巴黎铁塔.jpg"
]
}
]

但是这个 create.js 文件博主没有分享出来,我参考 phototool.jsindex.js[3] 写了一个新的 phototool.js,除了实现 create.js 的功能,还实现了:

  1. 在每个图片后面添加了一个值,作为图片对应的缩略图。
  2. 支持了获取 image-size 不支持的格式的宽高信息 除了 mp4 文件还可以随意添加
    具体实现方式就是如果文件是这些不支持的文件格式,就使用对应的缩略图中的 png 的宽高信息。
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
"use strict";

const sizeOf = require('image-size');
const path = require('path');
const { readdir, readdirSync, writeFileSync, stat } = require('fs');

var dimensions;

const imagesSrcPath = 'images/原图/'; // hexo/images/原图/
const imagesThumbPath = 'images/缩略图/'; // hexo/images/缩略图/
const sourcePath = 'source/'; // hexo/source/
const pageArr = readdirSync(imagesSrcPath); // 读取出所有页面

for (var i in pageArr) {
// console.log("Page: " + pageArr[i]);

const pageSrcPath = imagesSrcPath + pageArr[i] + '/'; // hexo/images/原图/photo/
const pageThumbPath = imagesThumbPath + pageArr[i] + '/'; // hexo/images/原图/photo/
const output = sourcePath + pageArr[i] + '/' + 'image.json'; // hexo/source/photo/image.json
readdir(pageSrcPath, function (err, filesInPage) { // 读取出所有相册
if (err) {
console.log("Error: readdir(pageSrcPath, function (err, filesInPage)");
return;
}

let imagesInPage = [];
(function iterator(j) {
if (j == filesInPage.length) {
return;
}

const albumSrcPath = pageSrcPath + filesInPage[j]+ '/';
const albumThumbPath = pageThumbPath + filesInPage[j]+ '/';
stat(albumSrcPath, function (err, file) { // stat + isDirectory(): 过滤掉 images.json 文件
if (err) {
console.log("Error: stat(albumSrcPath, function (err, file)");
return;
}

if (file.isDirectory()) {
// console.log("Dir: " + filesInPage[j]); // 2022.09.04~2022.09.10-陕西

let imagesInAlbum = {};
imagesInAlbum.name = filesInPage[j];
let children = [];

readdir(albumSrcPath, function (err, imageFile) { // 读取出所有图片
if (err) {
console.log("Error: readdir(albumSrcPath, function (err, imageFile)");
return;
}

(function iterator(k) {
if (k == imageFile.length) {
imagesInAlbum.children = children;
console.log(JSON.stringify(imagesInAlbum));
imagesInPage.push(imagesInAlbum);
// writeFileSync(output, JSON.stringify(imagesInPage));
writeFileSync(output, JSON.stringify(imagesInPage, null, "\t"));
return;
}

// console.log("File: " + imageFile[k]); // 5大唐芙蓉城(edited).jpg

if (path.extname(imageFile[k]) !== ".mp4") {
dimensions = sizeOf(albumSrcPath + imageFile[k]);
console.log(dimensions.width, dimensions.height, imageFile[k], imageFile[k]);
children.push(dimensions.width + '.' + dimensions.height + ' ' + imageFile[k] + ' ' + imageFile[k]);
} else {
dimensions = sizeOf(albumThumbPath + path.basename(imageFile[k], ".mp4") + ".png");
console.log(dimensions.width, dimensions.height, imageFile[k], path.basename(imageFile[k], ".mp4") + ".png");
children.push(dimensions.width + '.' + dimensions.height + ' ' + imageFile[k] + ' ' + path.basename(imageFile[k], ".mp4") + ".png");
}

iterator(k + 1);
}(0));
});
}
})
iterator(j + 1);
}(0));
});
}

生成的 json 格式:

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
[
{
"name": "2022.09.04~2022.09.10-陕西",
"children": [
"4016.6016 5大唐芙蓉城(edited).jpg 5大唐芙蓉城(edited).jpg",
"6016.4016 7钟楼(edited).jpg 7钟楼(edited).jpg"
]
},
{
"name": "2022.10.01-北京-石景山游乐园",
"children": [
"5509.3782 DSC_5662(edited).jpg DSC_5662(edited).jpg",
"5512.3784 DSC_5664(edited).jpg DSC_5664(edited).jpg",
"5515.3786 DSC_5667(edited).jpg DSC_5667(edited).jpg",
"5542.3804 DSC_5671(edited).jpg DSC_5671(edited).jpg",
"5554.3813 DSC_5680(edited).jpg DSC_5680(edited).jpg"
]
},
{
"name": "2022.10.07-北京-首钢园",
"children": [
"4016.6016 DSC_6355(edited).jpg DSC_6355(edited).jpg"
]
}
]

手动执行命令 验证用,后续会自动执行,会在 source 内对应页面的文件夹内生成 image.json

1
node scripts/phototool.js

放在 scripts 内的 js 文件在执行 Hexo 部分命令 比如 hexo s -d 时会被自动执行。

至此,图片的准备工作就做完了。


加载图片到页面

创建 gallery.js

参考的是 Fluid 博主的 photoWall.js,代码的主要功能是:

  1. 读取 iamge.json 文件内的 json 数据:
    • name 的值加载到 index.mdimageTab 上。
    • children 的图片加载到 index.mdImageGrid 上。
  2. 图片加载使用了 lazyload[4],src 设置为使用数据 uri 图像作为占位符,以减少请求量。

我做了部分修改,重命名为 gallery.js,修改如下:

  1. 使用了之前上传到图床的缩略图,这样显示的的图片会很清晰,而且支持了视频。
    photoWall.js 给 lazyload 设置的 data-src 是原图,在图片加载后会显示特别糊的缩略图,估计是代码生成的,但是生成的图片质量远不如上传的缩略图;而且不支持生成视频缩略图。
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
var imgDataPath = "image.json"; // 图片所在相册、高宽、名称
var imgPath = "https://weichao-io-1257283924.cos.ap-beijing.myqcloud.com/qldownload/%E5%8D%9A%E5%AE%A2/"; // 原图访问路径
var imgThumbPath = "https://weichao-io-1257283924.cos.ap-beijing.myqcloud.com/qldownload/%E5%8D%9A%E5%AE%A2/%E7%BC%A9%E7%95%A5%E5%9B%BE/"; // 缩略图访问路径

var windowWidth =
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth;
if (windowWidth < 768) {
var imageWidth = 145; //图片显示宽度(手机端)
} else {
var imageWidth = 250; //图片显示宽度
}

const photo = {
page: 1,
init: function () {
var that = this;
$.getJSON(imgDataPath, function (data) {
that.render(that.page, data);
//that.scroll(data);
that.eventListen(data);
});
},
constructHtml(options) {
const {
imageWidth,
imageX,
imageY,
name,
imgPath,
imgThumbPath,
imgName,
imgNameWithPattern,
imgThumbNameWithPattern,
} = options;
const htmlEle = `<div class="card lozad" style="width:${imageWidth}px">
<div class="ImageInCard" style="height:${
(imageWidth * imageY) / imageX
}px">
<a data-fancybox="gallery" href="${imgPath}${name}/${imgNameWithPattern}"
data-caption="${imgName}" title="${imgName}">
<img class="lazyload" data-src="${imgThumbPath}${name}/${imgThumbNameWithPattern}"
src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
onload="lzld(this)"
lazyload="auto">
</a>
</div>
</div>`;
return htmlEle;
},
render: function (page, data = []) {
this.data = data;
if (!data.length) return;
var html,
imgNameWithPattern,
imgThumbNameWithPattern,
imgName,
imageSize,
imageX,
imageY,
li = "";

let liHtml = "";
let contentHtml = "";

data.forEach((item, index) => {
const activeClass = index === 0 ? "active" : "";
liHtml += `<li class="nav-item" role="presentation">
<a class="nav-link ${activeClass} photo-tab" id="home-tab" photo-uuid="${item.name}" data-toggle="tab" href="#${item.name}" role="tab" aria-controls="${item.name}" aria-selected="true">${item.name}</a>
</li>`;
});
const [initData = {}] = data;
const { children = [],name } = initData;
// 解析 children
children.forEach((item, index) => {
imgNameWithPattern = item.split(" ")[1];
imgThumbNameWithPattern = item.split(" ")[2];
imgName = imgNameWithPattern.split(".")[0];
imageSize = item.split(" ")[0];
imageX = imageSize.split(".")[0];
imageY = imageSize.split(".")[1];
let imgOptions = {
imageWidth,
imageX,
imageY,
name,
imgName,
imgPath,
imgThumbPath,
imgNameWithPattern,
imgThumbNameWithPattern,
};
li += this.constructHtml(imgOptions);
});
contentHtml += ` <div class="tab-pane fade show active" role="tabpanel" aria-labelledby="home-tab">${li}</div>`;

const ulHtml = `<ul class="nav nav-tabs" id="myTab" role="tablist">${liHtml}</ul>`;
const tabContent = `<div class="tab-content" id="myTabContent">${contentHtml}</div>`;

$("#imageTab").append(ulHtml);
$(".ImageGrid").append(tabContent);
this.minigrid();
},
eventListen: function (data) {
let self = this;
var html,
imgNameWithPattern,
imgThumbNameWithPattern,
imgName,
imageSize,
imageX,
imageY,
li = "";
$('a[data-toggle="tab"]').on("shown.bs.tab", function (e) {
$(".ImageGrid").empty();
const selectId = $(e.target).attr("photo-uuid");
const selectedData = data.find((data) => data.name === selectId) || {};
const { children,name } = selectedData;
let li = "";
// 解析 children
children.forEach((item, index) => {
imgNameWithPattern = item.split(" ")[1];
imgThumbNameWithPattern = item.split(" ")[2];
imgName = imgNameWithPattern.split(".")[0];
imageSize = item.split(" ")[0];
imageX = imageSize.split(".")[0];
imageY = imageSize.split(".")[1];
let imgOptions = {
imageWidth,
imageX,
imageY,
name,
imgName,
imgPath,
imgThumbPath,
imgNameWithPattern,
imgThumbNameWithPattern,
};
li += self.constructHtml(imgOptions);
});
$(".ImageGrid").append(li);
self.minigrid();
});
},
minigrid: function () {
var grid = new Minigrid({
container: ".ImageGrid",
item: ".card",
gutter: 12,
});
grid.mount();
$(window).resize(function () {
grid.mount();
});
},
};
photo.init();

执行 gallery.js

这里用到了 Hexo 注入器[5],使用的是 Fluid 博主的代码 injector.js,稍作修改。
layout: photo 时会导入下面的 js 与 css,这里的 layout 就是 index.md 里的 layout,其中包括 gallery.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const { root: siteRoot = "/" } = hexo.config;
// layout 为 photo的时候导入这些js与css
hexo.extend.injector.register(
"body_end",
`
<link rel="stylesheet" href="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.css">
<script src="//cdn.jsdelivr.net/npm/[email protected]/dist/minigrid.min.js"></script>
<script src="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/lazyloadjs/3.2.2/lazyload.js"></script>
<script defer src="${siteRoot}js/gallery.js"></script>
`,
"photo"
);

参考

  1. hexo的fluid主题添加瀑布流懒加载相册功能:https://gishai.top/blog/posts/798ba833.html
  2. Hexo NexT 博客增加瀑布流相册页面:https://pinlyu.com/posts/31/
  3. leirock/image-size-generator/index.js:https://github.com/leirock/image-size-generator/blob/main/index.js
  4. vvo/lazyload:https://github.com/vvo/lazyload
  5. Hexo 注入代码:https://hexo.fluid-dev.com/docs/advance/#hexo-%E6%B3%A8%E5%85%A5%E4%BB%A3%E7%A0%81

为 Hexo + Fluid 博客添加承载相册的页面
https://weichao.io/09dacc5ba02c/
作者
魏超
发布于
2022年11月20日
更新于
2023年2月14日
许可协议