--- 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-tip { color: $color9; text-align: center; display: none; } ``` 指定列宽, 不指定列数, 根据容器的大小自动适配 > 虽然指定了列宽, 但是列宽也不是固定的, 这个值相当于是个可允许范围内的最小值 ( 多于1列的情况下 ) 比如上面的css当中指定列宽240px, 列间距20px ( 这个值是固定的 ) 如果外部容器的宽度600px, 假如排3列, 那么每一列的宽度就是 ( 600 - 20 * 2 ) / 3 ≈ 186.67 < 240 所以无法容纳3列 假如排2列, 那么每一列的宽度是 ( 600 - 20 ) / 2 = 290 > 240 **实际就会显示为2列**, 每一列的宽度是290px, 列间距20px 换句话说, 就是剩余的宽度会平均分布到各列 ### 滚动加载 有一些第三方库实现了滚动加载, 但是尝试过之后发现无法与现有的整体布局很好地结合 于是决定自己实现一下 **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-tip') 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`指定 ) 避免加载过程中频繁打乱整体布局