博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
用Vue实现一个掘金沸点图片展示组件
阅读量:7085 次
发布时间:2019-06-28

本文共 12714 字,大约阅读时间需要 42 分钟。

动机

最近的后台项目里需要添加新鲜事功能,简而言之就是一个带图片的评论回复系统,看了好几个类似的系统后,还是决定仿照掘金沸点的设计,简洁而且优雅,整个模块界面基本和沸点一样,只是少了一些功能(链接和话题功能没有做)

整个系统比较复杂,包含图片文字上传组件,emoji表情组件,一级评论,二级回复,以及二级回复要能展示图片,点赞组件,图片展示组件等。其实主要是后台比较复杂,如何有效地设计数据库表结构以及各种增删评论。本文主要介绍下图片展示模块的构思和代码编写逻辑,如下图,下图是新鲜事中的图片展示界面(缩略图),当用户在发布新鲜事时上传了图片时,下面的新鲜事组件就会展示所有图片.

感觉这个图片展示组件逻辑较多且比较复杂,也找不到别人的轮子,因此值得拿出来分析下,下面的分析只会给出关键的代码,不会给出全部代码,主要太多太复杂,理解原理就能够自己做一个

组件功能分析

首先接到这个任务第一步是仔细查看掘金沸点的图片展示组件的具体表现和逻辑(可以去沸点模块试一试),这一步需要大量测试来挖掘其中的所有逻辑情况,下面列举出该组件的一些表现

(1)如果只有一张图,则显示为大图,如下图

(2)如果有多张图(最多9张),则显示为缩略图,且图片数量不同时图片排列不同

上图是8张图时的排列,当9张图时排列呈现九宫格形式,下图是5张图的排列形式

区别在于每一行的图片个数随着图片总数的变化而变化

(3)当某张图长宽比超过一定阈值时,图片右下角显示长图标签

(4)点击任意一张缩略图,切换到详情图展示界面

上图的详情界面中,主要包含顶部操作栏4个按钮以及底部的缩略图栏,点击任何一张缩略图能跳转到对应图片,然后鼠标在大图左侧时显示上一张的cursor,右侧时显示下一张的cursor并能够切换图片。点击旋转按钮能够旋转图片

(54)点击上面图片中的查看大图按钮进入到全屏的大图查看组件

该组件能够左右切换图片,且如果展示的是长图,首先在视口内完整显示长图,如下图

然后点击屏幕,切换到长图原始尺片显示界面(右侧出现滚动条),如下图

再次点击则切换回视口内长图的完整显示界面

组件结构抽象

首先给整个组件命名(新鲜事图片展示组件),只需要一个prop即图片数组,包含每张图片的url

<message-image-viewer :imageList="imageList"></message-image-viewer>

经过上面的功能分析,发现全屏图片查看这个模块可以抽象成一个单独的组件,这个组件是上面组件的子组件

复制代码

需要2个prop,首先是要展示的图片数组,其次是当前展示图片的index,由其父组件<message-image-viewer>传入

下面是<message-image-viewer>的总体结构

复制代码

是不是感觉很复杂,其实一开始的结构也是很简单,慢慢加代码就变成现在这样,主要结构图如下

主体就是详情图的div和缩略图的div进行切换,通过内部变量
isShowDetail进行切换,然后全屏大图组件在详情图内

缩略图部分分析

下面先分析缩略图部分,缩略图就是上面所说的图片的缩略图展示的模样,由前面分析得知单张图时显示大图,多张图时显示小图,最多9张图。整体结构如下

用一个
isSingleImage变量来控制显示单图还是多图的缩略,它是个计算属性,当prop传入的图片张数小于等于1时为true

//是否是单张图isSingleImage:function(){  return !(this.imageList.length>1)},复制代码

1张图时显示大图,这个逻辑看似简单,其实规则比较复杂,下面是我经过测试得出的单图显示规则:

(1)首先判断应该显示为竖图还是横图,根据原图的长宽比来决定
(2)所有图片宽度width一定,变的只有高度
(3)高度/宽度超过一定值(1.8)显示长图标签,此时显示的图片的高度为宽度的1.45倍,否则按比例显示
(4)如果高度/宽度小于一定值(0.68),则缩略图外框高度高度为宽度的0.68倍,且居中显示,否则按比例显示

这些规则都是必须的,比如给一张竖直方向很长的图,那么其按上述规则显示为如下

上图中第一张图是该长图水平放置,下面的是该长图竖直放置,显示长图标签,他们宽度都是一定值,特别注意第四条,如果该图水平方向上很长,那么必须用该规则让其显示的比例适中,不那么难看。或者给你一张10x10像素的图片,显示出来肯定不能按原比例显示,得适当放大

下面是单图的显示逻辑代码

复制代码

图片显示是用的backgroundImage属性而没有用image标签,感觉简单点,需要设置background-size:cover以及background-position:50%保证图片居中且div内充满图片不留白,当然图片是会被裁剪,注意这里的single-image-container类只设置了固定的宽度200px,其高度由里面的div撑开,给ratio-holder类动态设置padding-top,padding-top的百分比值是取的是基于父元素宽度的百分比,因此宽度一定就可以计算出对应的高度,下面给出由上述规则实现的计算div高度的函数

//单张图的高度计算calcSingleImgHeight: function(){    let self = this;    let image = new Image();    //获取图片的原始尺寸并计算比例    //图片较大的话必须等图片加载完成才能获取尺寸    image.onload = function(){      self.singleImageNaturalWidth = image.naturalWidth;      self.singleImageNaturalHeight = image.naturalHeight;    };    image.src = this.imageList[0];    let ratio = this.singleImageNaturalWidth?this.singleImageNaturalHeight / this.singleImageNaturalWidth : 1;    if(ratio < this.imageMinHeightRatio){      ratio = this.imageMinHeightRatio    }    if(ratio > this.imageMaxHeightRatio){      // 该图是长图        this.isLongImage = true;      ratio = this.imageMaxHeightRatio    }      return ratio*100+'%';},复制代码

这个方法是个计算属性,计算图片的长宽比需要用到naturalWidth和naturalHeight,这2个值是图片的原始宽高,但是必须等到图片加载完成才能获取到,否则就是0,因为是计算属性,所以onload方法触发时会重新计算图片比例。

下面分析多图情况下的缩略图显示

复制代码

多图情况下的显示规则:1-4张为col-4,5,6为col-3, 7,8张为col-4, 9张为col-3,col-n代表n列

下用一个v-for遍历图片数组即可,图片宽度高度相同,因此padding-top为100%,注意图片张数不同时排列情况不同的逻辑是通过colsOfMultipleImages这个计算属性根据图片张数计算出对应的类名

//多图时显示的列数的类colsOfMultipleImages:function(){  let len = this.imageList.length;  let map = {      	1:'col-4',2:'col-4',3:'col-4',4:'col-4',        5:'col-3',6:'col-3',7:'col-4',8:'col-4',        9:'col-3'};  return len===0?'':map[len]},复制代码

其实也就2个类,通过控制其宽度保证图片计时换行排列

.col-3{  width:75%}.col-4{  width:100%;}复制代码

详情图分析

详情图主要涉及到图片旋转,稍微麻烦点

详情图主要分为3个部分,上面是顶部操作栏,中间是图片展示栏,底部时缩略图展示栏,其中中间图片展示栏同样有其对应的图片展示规则,经过分析如下

(1)如果图片的宽度超过外层div的宽度,则宽度为div的宽度,高度按图片比例缩放

(2)如果图片的宽度未超过外层div的宽度,则图片按原尺寸显示,图片水平居中
(3)大图加载时的loading图默认宽高是固定的,宽度为外层div的宽度,高度略小
注意如果加载一张很小的图片,仍然按原比例显示,掘金就是这么做的,如下图

下面主要分析下图片旋转功能的实现,略复杂,首先明确一点这里的图片旋转是通过css的transform的rotate进行旋转的,图片只是视觉上旋转了,本质上没有,如果本质上旋转要用canvas重新画图。

css的旋转看似很简单,比如右转90度,只需要给图片的style动态设置transform:rotate(90deg)即可?其实不然,这样的旋转只会导致图片在原来的位置进行旋转(transform-origin默认为center,图片中心点),且图片的宽高都不变,可以想象下一张很长的图片旋转成水平方向后的情况,明显有问题,因此这里的逻辑需要动态计算图片宽高以及外层div宽高,最终结果如下动态图所示

由上图可见这种旋转其实图片宽高都有变化,不是单纯的css旋转,下面慢慢分析,html结构如下

复制代码

上面代码中<img>标签就是要展示的图片,由于图片需要加载,因此设置一个circle-loading组件用于展示loading效果,当点击缩略图时执行下面的函数。剩下的div都是绝对定位,注意最外层的div动态绑定了高度,这是为了根据图片高度的变化而改变外层div的高度,否则图片会溢出div

//展示详情大图  showDetailImage: function(imgUrl){  	let self = this;  	//设置index        this.currentImageIndex = this.imageList.indexOf(imgUrl);  	//改变状态为大图加载中  	this.isDetailImageLoaded = false;  	//计算大图的原始尺寸        let image = this.$refs.detailImage;        image.onload = function(){          self.isDetailImageLoaded = true;          self.detailImageNaturalWidth = image.naturalWidth;          self.detailImageNaturalHeight = image.naturalHeight;         };        image.src = imgUrl;        this.isShowDetail = true;  },复制代码

这里主要做的事就是在图片加载完成后记录当前图片的原始宽高,供后续旋转使用。下面分析下图片旋转的整个过程,首先注意到img标签添加了一个detail-img类,这个类的内容是

.detail-img{    position: absolute;    left:50%;    top:0;    transform-origin: top left;}复制代码

上面的css表示该图片绝对定位,不占文档空间,并且向右移动50%,变换的基本点设置在图片左上角,这样做下来的图片显示情况就如下图

上图的红点就是图片旋转的基本点,该基本点在外层div的中点处,这么做是为了方便旋转的一系列处理,点击向右旋转按钮,图片按照箭头方向旋转,注意图片此时有一半在div外面,当向右旋转后得到下图

此时图片的方向已经旋转正确,但仍然有一部分在div外面,因此只旋转是不行的,每次旋转还必须伴随着transform:translate进行图片平移操作,让图片重新回到div内部,对于上面的图片旋转,旋转后需要translate(0,-50%),即在x方向上不处理,y方向上移动-50%距离,这里容易弄反,该图片看着是需要水平位移,其实这已经是旋转后的结果了,因此现在的水平位移就是旋转前的垂直方向上的位移,所以translateY是-50%,继续向右旋转的话translateX和translateY的值会发生变化,根据推理可以得出一个数组detailImageTranslateArray,保存这旋转时图片需要位移的百分比,下面数组中从左到右是顺时针旋转,每一项的第一个值是translateX的值,第二个是translateY的值

detailImageTranslateArray:[['-50%','0'],['0','-50%'],['-50%','-100%'],['-100%','-50%']],复制代码

图片初始状态是上述数组的第一个值[-50%,0],因此我们可以根据图片当前的translateX和y的值得到图片旋转后下一个状态的translateX和y的值,然后再将该值绑定到图片的style上即可完成旋转

下面函数就是处理图片旋转的逻辑,点击向左或向右按钮触发下面函数

//处理图片旋转handleImageRotate: function(dir){      	//图片加载完成才能旋转      	if(!this.isDetailImageLoaded)return        // 注意旋转中心是图片的左上角(transform-origin:top left)        let angleDelta = dir === 1?90:-90;        //计算旋转后的角度        this.detailRotateAngel = (this.detailRotateAngel + angleDelta)%360;        //修正translate的值        let currentIndex;        this.detailImageTranslateArray.forEach((item,index)=>{          //找到当前的tranlate值          if(item[0]===this.detailImageTranslateX && item[1]===this.detailImageTranslateY){            currentIndex = index;          }        });        //取下一个值        let nextIndex = currentIndex+dir;        if(nextIndex === this.detailImageTranslateArray.length){          nextIndex = 0;        }else if(nextIndex === -1){          nextIndex = this.detailImageTranslateArray.length - 1;        }        //更新tranlate的值        this.detailImageTranslateX = this.detailImageTranslateArray[nextIndex][0];        this.detailImageTranslateY = this.detailImageTranslateArray[nextIndex][1];        //修正外层div的高度        this.processImageScaleInRotate();},复制代码

根据传入的dir参数决定旋转方向,然后计算出旋转后的角度,再计算出旋转后需要translate的值,因此,图片的旋转由data中3个值决定: detailRotateAngel,detailImageTranslateX ,detailImageTranslateY 分别代表旋转角度,x方向上的位移,y方向上的位移,最后通过计算属性将其绑定到img标签上即可

//大图的style,注意旋转的时候必须重设宽高和translate值  detailImageStyle:function(){    return {      width:this.detailImageWidth+'px',      height:this.detailImageHeight+'px',      //注意顺序:先旋转再移动      transform:'rotate('+this.detailRotateAngel+'deg)' +' '                +'translate('+this.detailImageTranslateX+','+this.detailImageTranslateY+')'    }  },复制代码

上面的计算属性返回了一个style对象,里面的transform由旋转和平移组成,这里要注意先rotate再translate,否则会出问题,经过上面的逻辑,图片已经可以正常旋转,但是有个巨大的问题,这里的图片虽然可以旋转,但是其尺寸没有自适应,比如一张非常长的图由竖直方向变为水平方向后,其宽度必须不能超过最外层div的宽度,因此还要给图片动态绑定width和height,见上述代码

图片的宽高是2个计算属性得来的

//详情大图的高度计算detailImageHeight:function(){    if(!this.isDetailImageLoaded){      //加载时高度固定    	return this.loadingDefaultHeight    }else{      return this.processImageScaleInRotate().height    }},//详情大图的宽度detailImageWidth:function(){    if(!this.isDetailImageLoaded){      //外层div的宽度      let outerDiv = this.$refs.wrapper;      let clientWidth = outerDiv?outerDiv.clientWidth:1;      return clientWidth    }else{      return this.processImageScaleInRotate().width    }},复制代码

这里又分为加载中和非加载状态的计算,如果图片是处于加载中,则高度固定,宽度为外层div的宽度,这也是为了美观而设置的固定值,如果图片加载完成,调用processImageScaleInRotate方法计算宽高,该方法如下

//图片旋转时重新计算详情图片的宽高processImageScaleInRotate:function(){    //获取图片原始宽高    let nw = this.detailImageNaturalWidth,        nh = this.detailImageNaturalHeight;    //根据旋转角度来计算该图是初始状态还是旋转过90度横竖交换的情况    let angel = this.detailRotateAngel;    //图片旋转后的宽高    let imageRotatedWidth,imageRotatedHeight;    let clientWidth = this.$refs.wrapper.clientWidth;    let ratio = nh / nw;    //是否是初始状态    let isInitialState = true;    if(angel === 90 || angel === 270 || angel === -90 || angel === -270){      isInitialState = false;    	//由初始状态旋转一次的情况    	if(nh > clientWidth){        imageRotatedWidth = clientWidth;        imageRotatedHeight = imageRotatedWidth / ratio;      }else{        imageRotatedWidth = nh;        imageRotatedHeight = imageRotatedWidth / ratio;      }    }else{      //旋转一次变为初始状态的情况      isInitialState = true;      if(nw > clientWidth){        imageRotatedWidth = clientWidth;        imageRotatedHeight = imageRotatedWidth * ratio;      }else{        imageRotatedWidth = nw;        imageRotatedHeight = imageRotatedWidth * ratio;      }    }    //注意这里的判断,width和height在旋转状态下容易弄反    return {    	width:isInitialState?imageRotatedWidth:imageRotatedHeight,        height:isInitialState?imageRotatedHeight:imageRotatedWidth    }},复制代码

这个方法较为复杂,我们可以发现无论怎么旋转,图片的[宽,高]这一组数据只可能存在2种状态,初始状态和旋转一次后的状态,这很好理解,拿一张图片操作一下就明白了,这2种状态就是由图片的旋转角度detailRotateAngel决定,当这个角度为90,270,-90,-270度时就是旋转一次后的状态,否则为初始状态,然后分别针对这2种状态计算宽高即可

初始状态的计算:首先获取到图片的原始宽高nw,nh,然后计算其比例ratio,当nw>clientWidth时,表明图片的原始宽度大于外层div的最大宽度,这时图片的宽度就是clientWidth,否则宽度是自己的原始宽度,高度的话根据ratio乘以宽度得出。这样就能保证图片的宽度不会超出div的宽度。

由初始状态旋转一次后的计算:原理同上,只不过这时需要判断的值是nh(原始高度),因为图片宽高置换了

最后return返回width和height即可,上面处理完了图片的宽高计算,还剩一个问题就是外层div的高度计算,因为图片绝对定位,如果外层div的高度不变的话,图片会溢出

复制代码

我们给外层div动态绑定height,outerDivHeight是计算属性

//外层div的高度(随着图片旋转而变化)outerDivHeight: function(){    //根据旋转角度来计算该图是初始状态还是旋转过90度横竖交换的情况    let angel = this.detailRotateAngel;    if(angel === 90 || angel === 270 || angel === -90 || angel === -270) {      //由初始状态旋转一次的情况    	return this.detailImageWidth    }else{    	//初始状态      return this.detailImageHeight    }}复制代码

原理很简单,同上,也是根据图片的状态来计算,至此整个旋转的逻辑就结束了。

是否是长图

这个逻辑其实也很简单,代码如下

//计算每张图是否是长图calcImageIsLongImage: function(){    let self = this;    //计算每张图是否是长图    this.imageList.forEach((item,index)=>{      let image = new Image();      image.onload = function(){        let ratio = image.naturalHeight / image.naturalWidth;        if(ratio > self.longImageLimitRatio){        	//通过$set方法修改数组中的值          self.$set(self.isLongImageList,index,true)        }      };      image.src = item;    })},复制代码

该方法在mounted中调用,遍历prop传入的图片url数组,然后每张图new一个Image,在onload中获取其宽高比,如果大于阈值则设置长图数组isLongImageList中的那一项为true,最终通过span标签绝对定位于图片div中

<span class="long-image" v-show="isLongImageList[index]">长图</span>

全屏大图组件

这个组件比较简单,组件fixed定位,外层div的css如下,宽高满屏,z-index尽量大

position: fixed;  left:0;  top:0;  width:100vw;  height:100vh;  z-index:10000;复制代码

下图中是一张很长的图,长度有4个屏幕高度,初始状态下要求整个屏幕要能够完全显示该图片

这个怎么做呢?只需要给img标签设置如下css即可

.img{  max-width: 100vw;  max-height: 100vh;}复制代码

最大宽高都是满屏,因为限制了最大高度,所以图片的高度不会超出屏幕高度,不会出现滚动条,那么现在要求点击图片后能够查看原始图片,这时候就只需要设置

.img{    max-height: none;}复制代码

图片没有最大高度限制,因此图片显示为原始的高度,此时如果图片高度超过屏幕高度,出现滚动条,图片也变宽为原始的宽度

转载地址:http://xegml.baihongyu.com/

你可能感兴趣的文章
Django 模型 - 模型的定义
查看>>
复习日记-Listener/filter/servlet3.0/动态代理
查看>>
定时任务框架APScheduler学习详解
查看>>
vue中Axios的封装和API接口的管理
查看>>
Win7x64安装了DroidPilot-Win64.exe之后跑不起来 -- 解决办法
查看>>
VS2010 中C++ 和C# 颜色转化
查看>>
idea 新手入门配置
查看>>
JAVA 通过 JACOB 调用 WMI
查看>>
构建工具系列一--Travis-cli
查看>>
日历时间选择控件---3(支持ie、火狐)
查看>>
电梯设计大作业——概要设计
查看>>
CountDownLatch实现多线程并发请求
查看>>
AOJ 739 First Blood
查看>>
java 自带的工具
查看>>
10-08C#基础--进制转换
查看>>
[C++基础]007_char、wchar_t、wcout、setlocale()
查看>>
Java纯POJO类反射到Redis,反射到MySQL
查看>>
Localization native development region 设置属性(转)
查看>>
springboot将项目源代码打包
查看>>
Python必会的单元测试框架 —— unittest
查看>>