这标题怎么一股奇怪的味道

其实这是一个从七月底就开始着手的项目了,因为中间有点事一直拖到了8月底才完成。

起因

没有起因,纯粹是一时兴起想做个网站练练手罢了。正好收藏了不少图,好东西就要分享出来嘛。

技术实现

由于考虑到成本,这个项目所用的所有资源都将做到最低成本。

数据库部分,MongoDB提供了永久免费的500M数据库,虽然都是最低配,但对我来说完全够用了。

前后端部分,Vercel提供了永久免费的云函数以及前端集成部署,还支持自定义域名,这年头这么良心的平台真是少见了。

架构

整个网站除了数据库之外都是我熟悉的Node.js设计,前端仍然采用Nuxt.js,后端是Express,能够直接与Vercel的云函数API实现对接。

在确定了整体架构之后,一个问题来了。这么多的大图片文件,存哪里?

首先排除了直接存MongoDB,虽然数据库的二进制格式可以直接存文件,但对性能来说是个坎,而且免费的500M空间也远远不够用。

然后,我把目光看向了公共图床。在调查了有国内节点的各个大型免费图床平台后,我发现这些图床大多数都有图片大小限制,大部分是5M到10M。然而我收藏的超清壁纸最大的一张有22M,10M以上的图片文件也比较多,所以最终还是放弃了。

不过,由于网站不仅需要壁纸的原图,通常还需要比较小的图片缩略图,所以我决定把所有图片的缩略图放在这些图床上。

最终,对于大文件,我决定把它们放在OneDrive上,然后使用Graph API获取图片直链,当作图片链接来展示在网站上。不过又要去学GraphAPI就是了≡(▔﹏▔)≡

MongoDB数据库设计

之前从来没接触过MongoDB,所有东西都是现学的。

对于当前的网站架构,数据库所需要的信息应该有:图片的图床链接、图片的文件名等。由于图片大部分来自Pixiv所以储存了p站id信息。又因为前端设计需要,所以又存储了图片的长度和宽度。

后来又为了统计网站访问量,又增加了一个集合来收集访问数据。(本来是可以通过第三方平台实现的,结果发现第三方平台的统计代码被我浏览器插件给屏蔽了🤣)

后端设计

后端的接口目前设计的比较少,大致有下面几个:

  • 获取可以分页的图片列表
  • 获取一张图片的详情
  • 获取一张图片的源文件

后来又应要求加了一个随机图片的接口,下面再介绍。

MongoDB分页查询

前端展示图片需要分页,在经过一番学习后找到了一种比较简单的分页查询方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 指定每页的数量
const PER_PAGE_AMOUNT = 30
// page为当前的页码,默认为1
let queryIndex = (page - 1) * PER_PAGE_AMOUNT
let count = await getMaxCount()
await picModel
.find({}, { _id: 0 })
.where('index').gte(queryIndex)
.limit(PER_PAGE_AMOUNT)
.select("index picUrl height width")
.exec((err, docs) => {
// 返回docs
})

其中的where后紧跟的gte就是MongoDB的查询操作符之一。这里的gte代表大于等于。

Graph API对接

Microsoft Graph是巨硬为自家产品统一设计的接口集。要调用接口先要获取一个Access Token,而Access Token又要通过Refresh Token获取,而Refresh Token又要通过应用ID和密钥来生成(一圈下来头都晕了)

不过,还好我找到了一个可以参考的项目:onedrive-vercel-index

这是一个OneDrive网盘索引程序,我的 Revincx Cloud 目前就在使用这个项目。

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const token = await axios.get(`${apiConfig.driveApi}/root${encodePath(authTokenPath)}`, {
headers: { Authorization: `Bearer ${accessToken}` },
params: {
select: '@microsoft.graph.downloadUrl,file',
},
})

// Handle request and check for header 'od-protected-token'
const odProtectedToken = await axios.get(token.data['@microsoft.graph.downloadUrl'])
// console.log(req.headers['od-protected-token'], odProtectedToken.data.trim())

if (!compareHashedToken(req.headers['od-protected-token'] as string, odProtectedToken.data)) {
res.status(401).json({ error: 'Password required for this folder.' })
return
}

前端设计

前端才是最难的部分,前后累计起来搞了一个多星期。

瀑布流布局

由于自己动手实现瀑布流很复杂,所以我直接用了第三方组件:vue-waterfall

虽然直接有组件可以用,但开发的时候还是踩了不少坑。比如瀑布流的LineGap的动态调整问题,不过最后还是勉强解决了。

下面记录以下折磨我时间最长的一个问题:

由于Nuxt也是SPA单页面应用的概念,所以一旦刷新页面或者打开新的窗口,整个App的状态都会被重置。于是问题就来了:在从瀑布流进入到一张图片的详情页面,然后再返回瀑布流时,瀑布流就会重载,然后就会重新再从后端获取数据,这种体验是极其糟糕的,而且也浪费了服务端资源。

于是,我想到了Vue中的Keep-Alive

一开始,我在el-main,也就是页面主容器上添加keep-alive,然后发现没有任何作用。一旦返回瀑布流页面,数据还是会重新加载。

后来我换了条思路,在获取到数据后,通过变量储存起来,下次返回瀑布流时直接拿就可以了。

储存到哪呢?Vuex?那个东西差不多都忘完了嘛,算了,先扔到全局里试试。

浏览器端的全局变量,估计就只有Window对象了。于是,我在获取到数据后,把数据都赋值到了window对象上(事实上这么做是很愚蠢的)。

然后却发现,虽然在进入图片详情页面时window变量中的数据还在,但当我按下浏览器返回键时,window对象中的数据消失了。。

好吧,还是得去复习一遍Vuex,虽然好像也不难的样子。

然而在几天之后,我为了提升导航栏组件的复用性,把导航栏放到了layouts/default.vue中。然后使用<nuxt/>来加载页面内容。这时我灵机一动,在nuxt组件上加了一个keep-alive

奇迹发生了,当我返回瀑布流页面是,页面没有重载!!

然后我试着获取Vuex中的数据,一切正常!!(可能有点夸张,但我当时真的开心死了)

不过还有个小问题,返回主页面时页面滚动位置被重置了,原因是因为我的布局不是根元素在滚动,是主容器。然后我把滚动位置也存到Vuex里,返回时手动加载,也算是解决了。

图片详情页

由于图片详情页要加载原图,这里我使用了双层的设计,在缩略图上通过absolute定位叠上了原图,然后加一个模糊,在加一个加载动画,逼格不就出来了嘛~

然而理想很丰满,现实….

由于绝对定位脱离了文档流,导致两张图一直无法正常的完全重叠。缩略图使用了相对定位,而且有padding,但这个padding对绝对定位的元素是无效的。

在折腾了一段时间后,终于发现其实用CSS的calc函数就可以解决,看来我真的要去复习CSS了,现在写CSS真的是一头雾水。。

总结

由于部分原因,网站源码暂时不会公开,不过欢迎大家来网站看看。网站内容会持续更新的。

传送门:https://dd.al/tcjkA

另外网站有一个随机图片的API,不过目前没啥用。

详情可以去壁纸频道看看:https://dd.al/kxG9j


暑假的坑暂时填完了,下次写博客就不知道是什么时候了。