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
+
+### 效果
+
+
+感觉还有不少继续优化的空间, 至少图片的宽高可以在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)