diff --git a/source/_posts/前端杂烩/照片墙开发记录.md b/source/_posts/前端杂烩/照片墙开发记录.md new file mode 100644 index 0000000..08a68e6 --- /dev/null +++ b/source/_posts/前端杂烩/照片墙开发记录.md @@ -0,0 +1,264 @@ +--- +title: 照片墙开发记录 +date: 2019-05-12 15:17:29 +tags: + - 前端 + - Hexo +categories: + - 前端杂烩 +--- + +依稀记得刚使用hexo的时候, 大部分的照片墙解决方案还是调用**Instagram**的接口 +毕竟这是个很方便上传和管理照片的平台 +如今墙一天比一天高, 这方案也基本不灵了 +现在博客用上了对象存储作为图片库, 照片墙实现起来也可以有另一套办法了 + + +先整理一下思路, 大概是以下几个步骤 +1. 上传图片到对象存储仓库, 这个可以写个脚本跑一遍就可以了 +但是考虑到页面加载这些图片时候的性能问题 +计划是把100张图作为一组, 每组一个目录, 这个目录当中除了这100张图片, 还有一个json文件, 是该组的文件列表 +为动态加载提供方便 +2. 创建自定义页面, 这个hexo本身就支持, 直接在里面写html代码即可 +但是由于hexo本身对于md文件的渲染策略, 每一行都会加上`
` +在普通文章里面没什么, 在这个纯html页面就会影响DOM结构, 修改hexo的渲染策略会影响所有页面 +所以要注意这个md文件正文不能有换行 +3. 照片墙页面的布局, 这个准备采用瀑布流的模式, 跟已被关闭的Google plus ~~(深切缅怀🕯)~~ 一样的布局结构 +4. 分页加载以及滚动加载的一些实现 + +### 上传图片 +根据这次的需要改造一下图片上传的js脚本 +```javascript +const argv = { + rootPath: 'F:\\WallPaper\\', // 本地图片所在位置 + prefix: 'photo-wall', + step: 100 +} + +const listImages = require('./list_images') +// 当前本地存在的所有图片 +const imagesList = listImages(argv.rootPath, argv.prefix) +// console.dir(imagesList) + + +const setting = require('./auth_info.json'), + fs = require('fs'), + path = require('path'), + nos = require('@xgheaven/nos-node-sdk') + +const client = new nos.NosClient(setting) + +_uploadObject(imagesList) + +/** + * 上传文件对象 + * @param {Array} filesList 待上传的文件列表 + * @param {Number} index 索引值 + * @param {Array} group 文件的分组信息 + */ +function _uploadObject(filesList, index=0, groups=[]) { + if(index >= filesList.length) { + groups[groups.length-1].end = index + uploadList(filesList, groups) + return + } + if(!groups.length) { // 对于空对象, 放入第一个分组 + groups.push({start:index}) + } + let objectKey = filesList[index].name.replace(argv.prefix, `${argv.prefix}/${groups.length}`) + let body = fs.createReadStream(path.resolve(argv.rootPath, filesList[index].name)) + filesList[index].name = objectKey + if((index + 1) % argv.step === 0) { + // 到达一个分组的末尾 + groups[groups.length-1].end = index + uploadList(filesList, groups) + groups.push({start:index+1}) + } + client.putObject({objectKey, body}).then(result => { + // eTag是上传后远端校验的md5值, 用于和本地进行比对 + let eTag = result.eTag.replace(/"/g,'') + if(filesList[index].md5 === eTag) { + console.log(`${filesList[index].name} 上传成功, md5:${eTag}`) + } else { + console.warn(`${filesList[index].name} 上传出错, md5值不一致`) + console.warn(`===> 本地文件: ${filesList[index].md5}, 接口返回: ${eTag}`) + } + _uploadObject(filesList, ++index, groups) + }) +} + +/** + * 上传文件列表json + * @param {Array} filesList + * @param {Array} groups + */ +function uploadList(filesList, groups) { + client.putObject({ + objectKey: `${argv.prefix}/${groups.length}/list.json`, + body: Buffer.from(JSON.stringify({ + start: groups[groups.length-1].start, + end: groups[groups.length-1].end, + files: filesList.slice(groups[groups.length-1].start, groups[groups.length-1].end+1) + })) + }).then(result => { + console.log(result.eTag) + }) +} +``` +其中需要对文件名进行替换修改, 改为在该分组内的正确目录 +执行完成后, 对象存储仓库的photo-wall目录下就已经有若干个数字命名的子目录了 +每个子目录里面都有至多100张图片和一个json文件 +比如第一组中json文件的内容为 +```json +{ + "start":0, + "end":99, + "files":[{"name":"xxx.png","md5":"xxxx"}...] +} +``` + +### 创建自定义页面 +在source目录下创建`photo_wall`目录, 其中创建`index.md`文件 +```md +--- +title: 照片墙 +date: 2019-05-12 15:50:10 +pageid: PhotoWall +--- + +
正在加载ԅ( ¯་། ¯ԅ)
+``` +这里写个**pageid**是为了方便在js当中区分自定义页面 +从而执行chunk的动态加载, 避免影响其他页面的加载速度 + +#### 瀑布流布局 +得益于浏览器对多列布局的良好实现, css写起来还是非常简单的 +```scss +#photo-wall { + margin: 0 auto; + column-count: auto; + column-width: 240px; + column-gap: 20px; + // 每一列图片包含层 + .item { + margin-bottom: 20px; + // 防止多列布局,分页媒体和多区域上下文中的意外中断 + break-inside: avoid; + } + // 图片 + .item-img { + width: 100%; + vertical-align: middle; + } +} +#load-top { + color: $color9; + text-align: center; + display: none; +} +``` +列宽固定, 列数不固定, 根据容器的大小自动适配 + +### 滚动加载 +有一些第三方库实现了滚动加载, 但是尝试过之后发现无法与现有的整体布局很好地结合 +于是决定自己实现一下 +**photo-wall.js** +```javascript +import axios from 'axios' + +var groupid = 1, currentIndex = 0, defaultStep = 20, scrollLock = false + +// 滚动区域DOM +const scrollDom = document.getElementById('container') +// 作为底部标记的DOM +const markDom = document.getElementById('footer') +// 加载提示文字 +const loadTip = document.getElementById('load-top') + +function loadMoreItems(step) { + scrollLock = true //加载过程中锁定滚动加载 + loadTip.style.display = 'block' + // 滚动到底部时调用 + axios.get(`${themeConfig.pictureCdn}/photo-wall/${groupid}/list.json`).then(res => { + var itemContainer = document.createElement('div') + var imgItems = '', index = currentIndex + for( ; index + + ` + } + if(index >= res.data.files.length) { // 已到达当前分组列表的末尾 + groupid++ + if(index{ + loadTip.style.display = 'none' + scrollLock = false + }, 2000) + }).catch(res => { // 未加载到文件列表, 代表已经没有更多图片 + scrollLock = true + loadTip.textContent = '没有更多图片啦/(ㄒoㄒ)/~~' + }) +} + +//检测是否具备滚动条加载数据块的条件 +function checkScrollSlide(){ + var scrollH = scrollDom.scrollTop || document.body.scrollTop || document.documentElement.scrollTop + var clientHeight = document.body.clientHeight || document.documentElement.clientHeight + var footerOffetTop = markDom.offsetTop + return scrollH + clientHeight > footerOffetTop +} + +function init() { + var _onscroll = scrollDom.onscroll + var timer = null + scrollDom.onscroll = function () { + // 保留已有的滚动事件回调函数并在新的回调函数中进行调用 + typeof _onscroll === 'function' && _onscroll.apply(this, arguments) + if(scrollLock) return + if(timer) clearTimeout(timer) + timer = setTimeout(()=>{ + if(checkScrollSlide()) { + loadMoreItems(defaultStep) + } + timer = null + }, 200) + } + loadMoreItems(defaultStep) +} +export default { init } +``` +有几点需要注意 +1. 滚动事件需要使用**函数防抖**方式, 防止滚动事件频繁触发导致的性能问题 +2. 对已存在的滚动事件回调函数要注意保留和调用, 避免直接覆盖 +3. 记录当前分页加载所在的位置, 并在当前分组到达末尾的时候切换到下一个分组 +4. 当不存在下一个分组时, ajax获取下一个分组的json文件会返回404, 要在catch当中处理没有更多图片的交互逻辑 +5. 判断是否滚动到容器底部要添加不同浏览器的兼容 + +#### 动态引入photo-wall.js文件 +利用webpack的分块动态引入的功能 +```javascript +if(window.themeConfig.pageid === 'PhotoWall') { + // 自定义的照片墙页面 + import(/* webpackChunkName: "photo-wall" */ './photo-wall').then(PhotoWall => { + PhotoWall.default.init() + }) +} +``` +注释中的`webpackChunkName`是webpack可以读取的分块打包声明 +该引入会被单独打包为一个chunk + +### 效果 +![照片墙](/images/前端杂烩/照片墙.gif) + +感觉还有不少继续优化的空间, 至少图片的宽高可以在json文件里面记录下来 +生成包裹img的div时直接固定高度( 宽度由`column-width`指定 ) +避免加载过程中频繁打乱整体布局 \ No newline at end of file diff --git a/source/images/前端杂烩/照片墙.gif b/source/images/前端杂烩/照片墙.gif new file mode 100644 index 0000000..986a098 Binary files /dev/null and b/source/images/前端杂烩/照片墙.gif differ diff --git a/source/photo_wall/index.md b/source/photo_wall/index.md index e170495..329291e 100644 --- a/source/photo_wall/index.md +++ b/source/photo_wall/index.md @@ -1,6 +1,6 @@ --- title: 照片墙 -date: 2018-5-24 10:05:28 +date: 2019-05-12 15:50:10 pageid: PhotoWall --- diff --git a/themes/yilia/source-src/js/photo-wall.js b/themes/yilia/source-src/js/photo-wall.js index 78cd9d9..90f243b 100644 --- a/themes/yilia/source-src/js/photo-wall.js +++ b/themes/yilia/source-src/js/photo-wall.js @@ -30,10 +30,11 @@ function loadMoreItems(step) { } else { currentIndex = index } + itemContainer.classList.add('item-container') itemContainer.insertAdjacentHTML('beforeend', imgItems) document.getElementById('photo-wall').appendChild(itemContainer) - loadTip.style.display = 'none' setTimeout(()=>{ + loadTip.style.display = 'none' scrollLock = false }, 2000) }).catch(res => { // 未加载到文件列表, 代表已经没有更多图片 @@ -55,6 +56,7 @@ function init() { var _onscroll = scrollDom.onscroll var timer = null scrollDom.onscroll = function () { + // 保留已有的滚动事件回调函数并在新的回调函数中进行调用 typeof _onscroll === 'function' && _onscroll.apply(this, arguments) if(scrollLock) return if(timer) clearTimeout(timer)