最近的後臺項目裏須要添加新鮮事功能,簡而言之就是一個帶圖片的評論回覆系統,看了好幾個相似的系統後,仍是決定仿照掘金沸點的設計,簡潔並且優雅,整個模塊界面基本和沸點同樣,只是少了一些功能(連接和話題功能沒有作)css
整個系統比較複雜,包含圖片文字上傳組件,emoji表情組件,一級評論,二級回覆,以及二級回覆要能展現圖片,點贊組件,圖片展現組件等。其實主要是後臺比較複雜,如何有效地設計數據庫表結構以及各類增刪評論。本文主要介紹下圖片展現模塊的構思和代碼編寫邏輯,以下圖,下圖是新鮮事中的圖片展現界面(縮略圖),當用戶在發佈新鮮事時上傳了圖片時,下面的新鮮事組件就會展現全部圖片.html
感受這個圖片展現組件邏輯較多且比較複雜,也找不到別人的輪子,所以值得拿出來分析下,下面的分析只會給出關鍵的代碼,不會給出所有代碼,主要太多太複雜,理解原理就可以本身作一個數據庫
首先接到這個任務第一步是仔細查看掘金沸點的圖片展現組件的具體表現和邏輯(能夠去沸點模塊試一試),這一步須要大量測試來挖掘其中的全部邏輯狀況,下面列舉出該組件的一些表現
(1)若是隻有一張圖,則顯示爲大圖,以下圖canvas
(2)若是有多張圖(最多9張),則顯示爲縮略圖,且圖片數量不一樣時圖片排列不一樣數組
(3)當某張圖長寬比超過必定閾值時,圖片右下角顯示長圖標籤bash
(4)點擊任意一張縮略圖,切換到詳情圖展現界面markdown
(54)點擊上面圖片中的查看大圖按鈕進入到全屏的大圖查看組件app
而後點擊屏幕,切換到長圖原始尺片顯示界面(右側出現滾動條),以下圖ide
首先給整個組件命名(新鮮事圖片展現組件),只須要一個prop即圖片數組,包含每張圖片的url
<message-image-viewer :imageList="imageList"></message-image-viewer>
函數
通過上面的功能分析,發現全屏圖片查看這個模塊能夠抽象成一個單獨的組件,這個組件是上面組件的子組件
<!--全屏大圖組件--> <full-screen-viewer :imageList="imageList" :currentImageIndex="currentImageIndex" @close="handleFullScreenViewerClose" v-if="isShowFullScreenViewer"> </full-screen-viewer> 複製代碼
須要2個prop,首先是要展現的圖片數組,其次是當前展現圖片的index,由其父組件<message-image-viewer>
傳入
下面是<message-image-viewer>
的整體結構
<template> <div class="wrapper" ref="wrapper"> <!--縮略圖的div--> <div class="brief-view-wrapper" v-show="!isShowDetail" ref="outerWrapper"> <!--若是是單張圖--> <template v-if="isSingleImage"> <div class="single-img-container" @click="showDetailImage(imageList[0])" :style="{backgroundImage:'url('+imageList[0]+')'}"> <!--用div撐開圖片--> <div class="ratio-holder" :style="{paddingTop: calcSingleImgHeight}"> </div> <span class="long-image" v-if="isLongImage">長圖</span> </div> </template> <!--多張圖--> <template v-else> <!--圖片放在backgroundImage屬性中--> <div class="multiple-img-wrapper" :class="[colsOfMultipleImages]"> <div class="multiple-img-container" v-for="(item,index) in imageList" @click="showDetailImage(item)" :style="{backgroundImage:'url('+item+')'}"> <!--控制圖片的高度和寬度同樣,padding-top基於父元素的寬度--> <div class="ratio-holder" style="padding-top: 100%"> </div> <span class="long-image" v-show="isLongImageList[index]">長圖</span> </div> </div> </template> </div> <!--詳情圖的div--> <div class="detail-view-wrapper" v-show="isShowDetail" ref="detailViewWrapper"> <!--頂部操做欄--> <div class="top-panel"> <div class="panel-item" @click="hideDetailImage"> <i class="iconfont icon-zoomout icon-pos"></i> <span>收起</span> </div> <div class="panel-item" @click="showFullScreenViewer"> <i class="iconfont icon-zoomin icon-pos"></i> <span>查看大圖</span> </div> <div class="panel-item" @click="handleImageRotate(-1)"> <!--inline-block才能旋轉,inline不行--> <i class="iconfont icon-reload icon-pos" style="transform: rotateY(-180deg);display:inline-block;"> </i> <span>向左旋轉</span> </div> <div class="panel-item" @click="handleImageRotate(1)"> <i class="iconfont icon-reload icon-pos"></i> <span>向右旋轉</span> </div> </div> <!--中間圖片展現欄,注意須要設置高度,由於裏面的img是絕對定位--> <div class="detail-img-wrapper" :style="{height:outerDivHeight+'px'}"> <!--加載的logo--> <div class="detail-img-loading"> <circle-loading fillColor="#9C9C9C" v-if="!isDetailImageLoaded"> </circle-loading> </div> <img src="" ref="detailImage" v-show="isDetailImageLoaded" :style="detailImageStyle" class="detail-img"> <!--點擊隱藏詳情圖的div--> <div class="toggle-zoomout" @click="hideDetailImage"> </div> <!--上一張圖片的div--> <div class="prev-img" @click="switchImage(-1)" v-if="currentImageIndex > 0"> </div> <div class="next-img" @click="switchImage(1)" v-if="currentImageIndex<imageList.length-1"> </div> <!--全屏大圖組件--> <full-screen-viewer :imageList="imageList" :currentImageIndex="currentImageIndex" @close="handleFullScreenViewerClose" v-if="isShowFullScreenViewer"> </full-screen-viewer> </div> <!--縮略圖展現欄--> <div class="small-img-wrapper"> <div class="small-img-container" @click="switchSmallImage(index)" :class="{'small-img-active':currentImageIndex === index}" :style="{backgroundImage:'url('+item+')'}" v-for="(item,index) in imageList"> </div> </div> </div> </div> </template> 複製代碼
是否是感受很複雜,其實一開始的結構也是很簡單,慢慢加代碼就變成如今這樣,主要結構圖以下
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倍,且居中顯示,不然按比例顯示
這些規則都是必須的,好比給一張豎直方向很長的圖,那麼其按上述規則顯示爲以下
下面是單圖的顯示邏輯代碼
<!--若是是單張圖--> <template v-if="isSingleImage"> <div class="single-img-container" @click="showDetailImage(imageList[0])" :style="{backgroundImage:'url('+imageList[0]+')'}"> <!--用div撐開圖片--> <div class="ratio-holder" :style="{paddingTop: calcSingleImgHeight}"> </div> <span class="long-image" v-if="isLongImage">長圖</span> </div> </template> 複製代碼
圖片顯示是用的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方法觸發時會從新計算圖片比例。
下面分析多圖狀況下的縮略圖顯示
<!--多張圖--> <template v-else> <!--圖片放在backgroundImage屬性中--> <div class="multiple-img-wrapper" :class="[colsOfMultipleImages]"> <div class="multiple-img-container" v-for="(item,index) in imageList" @click="showDetailImage(item)" :style="{backgroundImage:'url('+item+')'}"> <!--控制圖片的高度和寬度同樣,padding-top基於父元素的寬度--> <div class="ratio-holder" style="padding-top: 100%"> </div> <span class="long-image" v-show="isLongImageList[index]">長圖</span> </div> </div> </template> 複製代碼
多圖狀況下的顯示規則: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%;
}
複製代碼
詳情圖主要涉及到圖片旋轉,稍微麻煩點
(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是絕對定位--> <div class="detail-img-wrapper" :style="{height:outerDivHeight+'px'}"> <!--加載的logo--> <div class="detail-img-loading"> <circle-loading fillColor="#9C9C9C" v-if="!isDetailImageLoaded"> </circle-loading> </div> <!--要展現的圖片--> <img src="" ref="detailImage" v-show="isDetailImageLoaded" :style="detailImageStyle" class="detail-img"> <!--點擊隱藏詳情圖的div--> <div class="toggle-zoomout" @click="hideDetailImage"> </div> <!--上一張圖片的div--> <div class="prev-img" @click="switchImage(-1)" v-if="currentImageIndex > 0"> </div> <div class="next-img" @click="switchImage(1)" v-if="currentImageIndex<imageList.length-1"> </div> <!--全屏大圖組件--> <full-screen-viewer :imageList="imageList" :currentImageIndex="currentImageIndex" @close="handleFullScreenViewerClose" v-if="isShowFullScreenViewer"> </full-screen-viewer> </div> 複製代碼
上面代碼中<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外面,所以只旋轉是不行的,每次旋轉還必須伴隨着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 class="detail-img-wrapper" :style="{height:outerDivHeight+'px'}"> <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{
max-width: 100vw;
max-height: 100vh;
}
複製代碼
最大寬高都是滿屏,由於限制了最大高度,因此圖片的高度不會超出屏幕高度,不會出現滾動條,那麼如今要求點擊圖片後可以查看原始圖片,這時候就只須要設置
.img{
max-height: none;
}
複製代碼
圖片沒有最大高度限制,所以圖片顯示爲原始的高度,此時若是圖片高度超過屏幕高度,出現滾動條,圖片也變寬爲原始的寬度