首頁 > 軟體

elementui下image元件的使用

2022-06-29 14:01:01

elementui-image元件的組成

elementuiimage模組主要是有main.vue,image-viewer.vue兩個檔案組成的,其最外層的main.vue是image元件的主體檔案,image-viewer.vue檔案主要為點選圖片後的大圖預覽。

elementui-image元件的功能

  • 首先理清楚image元件中完成了什麼功能,功能列表如下:

    ​ (1)image元件能夠設定fit的屬性,通過fit來設定圖片展示形式

    ​ (2)圖片懶載入的使用,基於scroll卷軸事件來進行

    ​ (3)圖片的大圖預覽,通過設定preview來開啟,同時需要傳入previewSrcList引數。

    ​ (4)可設定圖片載入失敗的顯示,通過slot來實現

    ​ (5)可設定圖片載入過程的顯示,通過slot來實現

    ​ (6)圖片在大圖預覽下的功能: 縮放,旋轉,等比例調整,圖片切換,基於滑鼠滾輪的縮放,基於鍵盤輸入的縮放,切換,隱藏預覽功能,基於滑鼠按下事件的拖放功能。

elementui-image的功能實現

fit的樣式實現

fit樣式主要是包含5種形式,分別是:fill, none, contain, scale-down,cover

object-fit屬性在有些低版本的瀏覽器上是不被支援的,所以需要自己處理以上的五種顯示形式,下面會進行整理書寫

object-fit屬性用於指定應如何調整 imgvideo 的大小以適合其容器。該屬性告訴內容以多種方式填充容器;例如“保持縱橫比”或“拉伸並佔用儘可能多的空間”。具體的圖片展示效果如下圖:

根據MDN-CSS的解釋,以上5種形式的對應解釋如下

contain:

被替換的內容將被縮放,以在填充元素的內容框時保持其寬高比。 整個物件在填充盒子的同時保留其長寬比,因此如果寬高比與框的寬高比不匹配,該物件將被新增==黑邊==。

cover:

被替換的內容在保持其寬高比的同時填充元素的整個內容框。如果物件的寬高比與內容框不相匹配,該物件將被剪裁以適應內容框。

fill:

被替換的內容正好填充元素的內容框。整個物件將完全填充此框。如果物件的寬高比與內容框不相匹配,那麼該物件將被拉伸以適應內容框。

none:

被替換的內容將保持其原有的尺寸。

scale-down:

內容的尺寸與 nonecontain 中的一個相同,取決於它們兩個之間誰得到的物件尺寸會更小一些。

那麼在image元件中,為了實現fit的動態設定,image元件中是由父元件傳入fit的屬性值,如果不傳入的話,會選擇預設的fill,來對圖片進行處理。

同時由於部分低版本瀏覽器平臺中,是不支援object-fit的,所以image元件對其進行了處理,通過一個computed資料imageStyle來控制image的樣式。同時對於不支援的瀏覽器平臺,也自定義了一份類object-fit的樣式來進行相容。

具體相容程式碼如下:

//通過這段程式碼來判斷是否支援當前瀏覽器,如果不支援就返回false,支援返回true
const isSupportObjectFit = () => document.documentElement.style.objectFit !== undefined;
//定義列舉類,用於對應fit的幾種形式
const ObjectFit = {
    NONE: 'none',
    CONTAIN: 'contain',
    COVER: 'cover',
    FILL: 'fill',
    SCALE_DOWN: 'scale-down'
};
/**
* 相容程式碼,它的實際實現也很簡單,實際就是處理圖片的長寬比,來控制圖片的顯示格式
*/
getImageStyle(fit) {
    const { imageWidth, imageHeight } = this;
    const {
        clientWidth: containerWidth,
        clientHeight: containerHeight
    } = this.$el;

    if (!imageWidth || !imageHeight || !containerWidth || !containerHeight) return {};

    const imageAspectRatio = imageWidth / imageHeight;
    const containerAspectRatio = containerWidth / containerHeight;

    if (fit === ObjectFit.SCALE_DOWN) {
        const isSmaller = imageWidth < containerWidth && imageHeight < containerHeight;
        fit = isSmaller ? ObjectFit.NONE : ObjectFit.CONTAIN;
    }

    switch (fit) {
        case ObjectFit.NONE:
            return { width: 'auto', height: 'auto' };
        case ObjectFit.CONTAIN:
            return (imageAspectRatio < containerAspectRatio) ? { width: 'auto' } : { height: 'auto' };
        case ObjectFit.COVER:
            return (imageAspectRatio < containerAspectRatio) ? { height: 'auto' } : { width: 'auto' };
        default:
            return {};
    }
}
/**
* 圖片的長寬獲取實際是在mounted種獲取到的,通過新建一個Image類,然後監控imageLoad是否成功,如果成功了,就可以讀取圖片的長寬。
*/
loadImage() {
    if (this.$isServer) return;

    // reset status
    this.loading = true;
    this.error = false;

    const img = new Image();
    img.onload = e => this.handleLoad(e, img);
    img.onerror = this.handleError.bind(this);

    // bind html attrs
    // so it can behave consistently
    Object.keys(this.$attrs)
        .forEach((key) => {
        const value = this.$attrs[key];
        img.setAttribute(key, value);
    });
    img.src = this.src;
}
handleLoad(e, img) {
    this.imageWidth = img.width;
    this.imageHeight = img.height;
    this.loading = false;
    this.error = false;
}
//computed屬性imageStyle
imageStyle() {
    const { fit } = this;
    if (!this.$isServer && fit) {
        return isSupportObjectFit()
            ? { 'object-fit': fit }
        : this.getImageStyle(fit);
    }
    return {};
}

通過上述的程式碼,就可以設定圖片的fit屬性了,如果說瀏覽器不支援,也可通過自定義widthheight來實現類fit的樣式。雖然說實現起來的步驟比較多,但是這樣可以為大部分瀏覽器平臺提供很好的適配和相容,是一種很好的處理方式。

圖片懶載入的實現方式

​ 圖片懶載入的實現實際上用了節流器來完成,通過設定延遲器,避免在第一時間載入圖片,等待一個指定的時間後載入圖片,達到圖片懶載入的實現。具體的防抖和節流的實現,這裡就先不贅述了,等到之後出一片博文之後,來在這裡新增一條連結。

​ 同時這裡的圖片懶載入也是基於Scroll的事件監聽進行實現,然後加入throttle之後,設定等待時間為200ms,來實現show的延遲載入。

​ 具體實現程式碼如下:

addLazyLoadListener() {
    //判斷是否是伺服器端渲染,如果是伺服器端渲染就無法使用懶載入,因為伺服器端渲染是把所有標籤全部渲染完成後輸出的
    if (this.$isServer) return;

    const { scrollContainer } = this;
    let _scrollContainer = null;

    if (isHtmlElement(scrollContainer)) {
        _scrollContainer = scrollContainer;
    } else if (isString(scrollContainer)) {
        _scrollContainer = document.querySelector(scrollContainer);
    } else {
        _scrollContainer = getScrollContainer(this.$el);
    }

    if (_scrollContainer) {
        this._scrollContainer = _scrollContainer;
        //這裡來設定節流,等待時間為200ms,這樣就不會實時出發scroll捲動事件。
        this._lazyLoadHandler = throttle(200, this.handleLazyLoad);
        on(_scrollContainer, 'scroll', this._lazyLoadHandler);
        this.handleLazyLoad();
    }
}

​ 實現還是比較簡單的,但是這裡的很多東西感覺很值得學習,無論是節流還是捲動事件的設定。作為現在的新手的我來說是值得學習的,又可以學習使用節流的場景,也可以學習節流與事件合併使用的技巧。

圖片載入失敗和載入中的可設定的實現

這裡的圖片載入失敗和載入中的可設定實現,其實都是基於slot進行實現的,也就是vue中插槽,並且這裡使用了具名插槽的使用。插槽的使用還是比較簡單的,這裡就放一下vue官網對插槽的介紹:vue插槽

圖片預覽的功能模組實現(image-view.vue檔案)

先放一張圖,通過這個圖裡的內容來介紹預覽功能

​ 圖片預覽模組的功能實現就比較好玩了,主要分為上面說的幾個方面,現在就來說一下這些方面:

  • 佈局:

    頁面採用的佈局實際就是蒙版的實現,但是image-view的蒙板實現其實存在一些問題,就是當把元件插在頁面中的時候,如果整個頁面是很長的頁面,那麼在對圖片使用滾輪縮放放大的時候,主頁面也會被影響。整體的頁面佈局採用的還是flex佈局的形式,同時將justify-content設定問centeralign-item也設定為center,這樣就可以保證image的居中顯示了。其他的圖示都為absolute,圖示組依然在absolute的基礎上,繼續設定了flex佈局,同時為了保證每一個圖示在圖示組內均為分佈,justify-content設定為了space-around

    flex佈局牛逼,節省了好多時間,只需要調解基線就完事了。

  • 放縮

    圖片放縮的功能是基於css中transform屬性,transform屬性提供了scale的調整,所以很方便就實現了圖片的放縮,同時在圖片使用transform的時候,會被預設提升到最頂層,這就完美的解決了蒙版遮蓋的問題,一舉兩得。基礎的放縮在image元件的控制是基於兩個放縮按鈕來實現的,所以也是很方便的。

  • 旋轉

    圖片旋轉的功能是基於css中transform屬性,transform屬性提供了rotate的調整,所以很方便就實現了圖片的旋轉。

    在旋轉與放縮的實現中,會設定一個判斷是否是通過普通按鈕操作的判斷,如果是普通的操作就會再加上一個transition來設定切換的時間,讓互動更加友好。但是在滾輪的縮放中,就需要取消transition屬性,因為滾輪的滑動十分迅速,互動十分頻繁,為了達到節流的要求,就需要設定滾輪每一次捲動的步長比普通按鈕的長,且取消transition,防止直接卡死。

  • 等比例調整

    等比例調整功能實現也很簡單,只需要原本設定的max-widthmax-height直接取消掉就可以實現了。

    const Mode = {
        CONTAIN: {
            name: 'contain',
            icon: 'el-icon-full-screen'
        },
        ORIGINAL: {
            name: 'original',
            icon: 'el-icon-c-scale-to-original'
        }
    };
    toggleMode() {
        if (this.loading) return;
    
        const modeNames = Object.keys(Mode);
        const modeValues = Object.values(Mode);
        const index = modeValues.indexOf(this.mode);
        const nextIndex = (index + 1) % modeNames.length;
        this.mode = Mode[modeNames[nextIndex]];
        this.reset();
    }
    

    通過按鈕事件,來切換class即可

  • 圖片切換

    通過鍵盤輸入的左右方向鍵或者頁面中提供的按鈕來進行圖片切換的操作。這裡圖片切換也就是來通過修改修改currentImage以及指向srclist的下標的改變來進行切換。同時這裡還有在載入時的一步操作就是在一開始的獲取點的image的在srclist中的位置來初始化下表。

    //currentImage與viewerZIndex是一個computed屬性,這樣就不需要可以修改這兩個屬性,一旦發生改變就可以觸發
    computed: {
        currentImg() {
            return this.urlList[this.index];
        },
        viewerZIndex() {
            const nextZIndex = PopupManager.nextZIndex();
            return this.zIndex > nextZIndex ? this.zIndex : nextZIndex;
        }
    }
    //具體的實現
    methods: {
        prev() {
            if (this.isFirst && !this.infinite) return;
            const len = this.urlList.length;
            this.index = (this.index - 1 + len) % len;
        },
        next() {
            if (this.isLast && !this.infinite) return;
            const len = this.urlList.length;
            this.index = (this.index + 1) % len;
        }
    }
    
  • 基於滑鼠滾輪的放縮

    基於滑鼠滾輪的放縮就是對當前的頁面新增了一個滑鼠滾輪滑動的事件,同時通過window.requestAnimationFrame來優化滑鼠滾輪滑動的效率,讓其事件的觸發與瀏覽器重新整理頻率相同,優化效能。

    對於滑鼠滾輪的事件,在不同的瀏覽器中擁有不同的事件相應名稱,在image元件中的處理就是判斷isFirefox,具體程式碼如下

    export const isFirefox = function() {
      return !Vue.prototype.$isServer && !!window.navigator.userAgent.match(/firefox/i);
    };
    const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel';
    

    不過這裡個人感覺還不是特別嚴謹,感覺mdn上給出的判斷更加合理,這裡貼出程式碼

    //獲取滑鼠滾輪事件
    const mouseWheel = () =>
      "onwheel" in document.createElement("div")
        ? "wheel" // 各個廠商的高版本瀏覽器都支援"wheel"
        : document.onmousewheel !== undefined
        ? "mousewheel" // Webkit 和 IE一定支援"mousewheel"
        : "DOMMouseScroll"; // 低版本firefox
    

    在自己基於自己的blog開發的image元件中,我才用了上述的寫法, 並做了測試,效果還不錯啦

    這裡讓我主要學習到的是滑鼠滾輪事件的優化,就是上面說的結合window.requestAnimationFrame來使用的方法,真的感覺很棒,這樣每一次滾輪事件的觸發,都結合了當前螢幕的實時重新整理率,讓其效果達到最好。

    具體的程式碼實現貼出:

    this.mouseWheelHandler = rafThrottle((e) => {
        /**
        * NOTE: wheelDelta和detail都是用來判斷滾輪是上滑還是下滑,但是在不同瀏覽器的平臺中的判斷條件不同,所以需要兩個都叫判斷
        * NOTE: 當使用window.requestAnimationFrame的時候,是可以不使用transition的
        */
        const delta = e.wheelDelta ? e.wheelDelta : -e.detail;
        if (delta > 0) {
            this.transform.enableTransition = false;
            this.zoomIn(false);
        } else {
            this.transform.enableTransition = false;
            this.zoomOut(false);
        }
    });
    //on方法是elementui封裝的一個柯里化的addEventListener方法
    on(this.$isServer)(document, mouseWheel(), this.mouseWheelHandler);
    export function rafThrottle(fn) {
        //這裡的locked是限制了當前頁面的requestAnimationFrame事件只能存在一個的鎖。
        let locked = false;
        return function(...args) {
            if (locked) return;
            locked = true;
            window.requestAnimationFrame(_ => {
                fn.apply(this, args);
                locked = false;
            });
        };
    }
    
  • 基於鍵盤輸入的縮放

    基於鍵盤的縮放就是對鍵盤輸入做了監聽,並且對上下左右四個方向鍵以及esc鍵、空格做了處理。直接貼程式碼好了,很簡單

    this._keyDownHandler = e => {
        //防止事件向上傳播,只在當前標籤中使用
        e.stopPropagation();
        const keyCode = e.keyCode;
        switch (keyCode) {
                // ESC
            case 27:
                this.hide();
                break;
                // SPACE
            case 32:
                this.toggleMode();
                break;
                // LEFT_ARROW
            case 37:
                this.prev();
                break;
                // UP_ARROW
            case 38:
                this.handleActions('zoomIn');
                break;
                // RIGHT_ARROW
            case 39:
                this.next();
                break;
                // DOWN_ARROW
            case 40:
                this.handleActions('zoomOut');
                break;
        }
    };
    
  • 隱藏預覽功能

    隱藏預覽主要有三種形式,一種是按下esc鍵,一種是點選蒙版而不是圖片的位置,最後一種就是點選按鈕,其實現都是完全相同,這裡我有點不明白為什麼要把方法作為prop傳過來,然後再執行,為什麼不可以做為emit來處理。實現還是很簡單,就不貼了。

  • 基於滑鼠按下事件的拖放功能

    滑鼠按下事件的拖放功能主要依賴於vue中支援的mousedown事件來實現,同時在mousedown的同時,繫結上mousemove事件,來獲取移動的x,y軸的位置,給到transform上的offsetX與offsetY屬性,用來設定imageStyle的margin-left,margin-top屬性。其實還是比較簡單的。

    具體程式碼實現如下:

    handleMouseDown(e) {
        if (this.loading || e.button !== 0) return;
    
        const { offsetX, offsetY } = this.transform;
        const startX = e.pageX;
        const startY = e.pageY;
        this._dragHandler = rafThrottle(ev => {
            this.transform.offsetX = offsetX + ev.pageX - startX;
            this.transform.offsetY = offsetY + ev.pageY - startY;
        });
        on(document, 'mousemove', this._dragHandler);
        on(document, 'mouseup', ev => {
            off(document, 'mousemove', this._dragHandler);
        });
    	//當事件處理結束後,就會將本事件取消,不會進一步發生
        e.preventDefault();
    }
    

關於elementui管理事件的一些解讀

elementui的事件管理都是通過一個柯里化的方法來進行的,就是src/utils/dom.js下的on和off方法,這兩個方法寫的十分巧妙,可以通過判斷是不是伺服器端渲染的程式碼,返回不同的事件繫結的程式碼,具體實現如下:

export const on = function (isServer) {
    if (!isServer && document.addEventListener) {
        return function (element, event, handle) {
            if (element && event) {
                element.addEventListener(event, handle, false)
            }
        }
    } else {
        return function (element, event, handle) {
            if (element && event) {
                element.attachEvent(`on${event}`, handle)
            }
        }
    }
}

export const off = function (isServer) {
    if (!isServer && document.addEventListener) {
        return function (element, event, handle) {
            if (element && event) {
                element.removeEventListener(event, handle, false)
            }
        }
    } else {
        return function (element, event, handle) {
            if (element && event) {
                element.detachEvent(`on${event}`, handle)
            }
        }
    }
}

雖然這對繫結事件來說會變得十分簡單,但是還是有一個關鍵的問題就是我無法知道我當前事件是否被消除,所以很想知道有沒有很好的事件管理機制來負責處理這些被建立的事件,然後再不需要在使用這些的事件時候,去處理掉這些事件,這也是之後需要我去探索的內容。

總結

element-ui的image元件的原始碼閱讀是我第一個仔細分析的它執行的每一步的一個元件,之前雖然也看過了兩個比較複雜模組的原始碼 table以及 tree的實現,但是卻沒有這次這麼仔細地去分析他的每一個步驟以及頁面的佈局設計。在本次閱讀的過程中,學到了怎麼去優化頻繁互動的處理方法,以及防抖和節流的使用

到此這篇關於elementui下image元件的使用的文章就介紹到這了,更多相關element image元件內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com