├── .gitignore ├── .babelrc ├── .editorconfig ├── package.json ├── LICENSE ├── src ├── index.js ├── lazy-component.js ├── listener.js ├── util.js └── lazy.js ├── README.md └── vue-lazyload.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | npm-debug.log -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["es2015", { "modules": false }] 4 | ], 5 | "plugins": [ 6 | "external-helpers" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | tab_width = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-lazyload", 3 | "version": "1.0.0-rc12", 4 | "description": "Vue module for lazy-loading images in your vue.js applications.", 5 | "main": "vue-lazyload.js", 6 | "scripts": { 7 | "start": "node build" 8 | }, 9 | "dependencies": {}, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/hilongjw/vue-lazyload.git" 13 | }, 14 | "keywords": [ 15 | "vue-lazyload", 16 | "vue", 17 | "lazyload", 18 | "vue-directive" 19 | ], 20 | "author": "Awe ", 21 | "bugs": { 22 | "url": "https://github.com/hilongjw/vue-lazyload/issues" 23 | }, 24 | "license": "MIT", 25 | "devDependencies": { 26 | "babel-cli": "^6.14.0", 27 | "babel-plugin-external-helpers": "^6.22.0", 28 | "babel-polyfill": "^6.13.0", 29 | "babel-preset-es2015": "^6.22.0", 30 | "babel-preset-es2015-rollup": "^1.2.0", 31 | "rollup": "^0.35.10", 32 | "rollup-plugin-babel": "^2.6.1", 33 | "rollup-plugin-uglify": "^1.0.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Awe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Lazy from './lazy' 2 | import LazyComponent from './lazy-component' 3 | import { assign } from './util' 4 | 5 | 6 | export default { 7 | /** 8 | * install function 9 | * @param {Vue} Vue 10 | * @param {object} options lazyload options 11 | */ 12 | install (Vue, options = {}) { 13 | const LazyClass = Lazy(Vue) 14 | const lazy = new LazyClass(options) 15 | const isVueNext = Vue.version.split('.')[0] === '2' 16 | 17 | Vue.prototype.$Lazyload = lazy 18 | 19 | if (options.lazyComponent) { 20 | Vue.component('lazy-component', LazyComponent(lazy)) 21 | } 22 | 23 | if (isVueNext) { 24 | Vue.directive('lazy', { 25 | bind: lazy.add.bind(lazy), 26 | update: lazy.update.bind(lazy), 27 | componentUpdated: lazy.lazyLoadHandler.bind(lazy), 28 | unbind : lazy.remove.bind(lazy) 29 | }) 30 | } else { 31 | Vue.directive('lazy', { 32 | bind: lazy.lazyLoadHandler.bind(lazy), 33 | update (newValue, oldValue) { 34 | assign(this.vm.$refs, this.vm.$els) 35 | lazy.add(this.el, { 36 | modifiers: this.modifiers || {}, 37 | arg: this.arg, 38 | value: newValue, 39 | oldValue: oldValue 40 | }, { 41 | context: this.vm 42 | }) 43 | }, 44 | unbind () { 45 | lazy.remove(this.el) 46 | } 47 | }) 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/lazy-component.js: -------------------------------------------------------------------------------- 1 | import { inBrowser } from './util' 2 | 3 | export default (lazy) => { 4 | return { 5 | props: { 6 | tag: { 7 | type: String, 8 | default: 'div' 9 | } 10 | }, 11 | render (h) { 12 | if (this.show === false) { 13 | return h(this.tag, { 14 | attrs: { 15 | class: 'cov' 16 | } 17 | }) 18 | } 19 | return h(this.tag, { 20 | attrs: { 21 | class: 'cov' 22 | } 23 | }, this.$slots.default) 24 | }, 25 | data () { 26 | return { 27 | state: { 28 | loaded: false 29 | }, 30 | rect: {}, 31 | show: false 32 | } 33 | }, 34 | mounted () { 35 | lazy.addLazyBox(this) 36 | lazy.lazyLoadHandler() 37 | }, 38 | beforeDestroy () { 39 | lazy.removeComponent(this) 40 | }, 41 | methods: { 42 | getRect () { 43 | this.rect = this.$el.getBoundingClientRect() 44 | }, 45 | checkInView () { 46 | this.getRect() 47 | return inBrowser && 48 | (this.rect.top < window.innerHeight * lazy.options.preLoad && this.rect.bottom > 0) && 49 | (this.rect.left < window.innerWidth * lazy.options.preLoad && this.rect.right > 0) 50 | }, 51 | load () { 52 | this.show = true 53 | this.$emit('show', this) 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/listener.js: -------------------------------------------------------------------------------- 1 | import { loadImageAsync } from './util' 2 | 3 | let imageCache = {} 4 | 5 | export default class ReactiveListener { 6 | constructor ({ el, src, error, loading, bindType, $parent, options, elRenderer }) { 7 | this.el = el 8 | this.src = src 9 | this.error = error 10 | this.loading = loading 11 | this.bindType = bindType 12 | this.attempt = 0 13 | 14 | this.naturalHeight = 0 15 | this.naturalWidth = 0 16 | 17 | this.options = options 18 | 19 | this.initState() 20 | 21 | this.performanceData = { 22 | init: Date.now(), 23 | loadStart: null, 24 | loadEnd: null 25 | } 26 | 27 | this.rect = el.getBoundingClientRect() 28 | 29 | this.$parent = $parent 30 | this.elRenderer = elRenderer 31 | } 32 | 33 | initState () { 34 | this.state = { 35 | error: false, 36 | loaded: false, 37 | rendered: false 38 | } 39 | } 40 | 41 | record (event) { 42 | this.performanceData[event] = Date.now() 43 | } 44 | 45 | update ({ src, loading, error }) { 46 | this.src = src 47 | this.loading = loading 48 | this.error = error 49 | this.attempt = 0 50 | this.initState() 51 | } 52 | 53 | getRect () { 54 | this.rect = this.el.getBoundingClientRect() 55 | } 56 | 57 | checkInView () { 58 | this.getRect() 59 | return (this.rect.top < window.innerHeight * this.options.preLoad && this.rect.bottom > 0) && 60 | (this.rect.left < window.innerWidth * this.options.preLoad && this.rect.right > 0) 61 | } 62 | 63 | load () { 64 | if ((this.attempt > this.options.attempt - 1) && this.state.error) { 65 | if (!this.options.silent) console.log('error end') 66 | return 67 | } 68 | 69 | if (this.state.loaded || imageCache[this.src]) { 70 | return this.render('loaded', true) 71 | } 72 | 73 | this.render('loading', false) 74 | 75 | this.attempt++ 76 | 77 | this.record('loadStart') 78 | 79 | loadImageAsync({ 80 | src: this.src 81 | }, data => { 82 | this.src = data.src 83 | this.naturalHeight = data.naturalHeight 84 | this.naturalWidth = data.naturalWidth 85 | this.state.loaded = true 86 | this.state.error = false 87 | this.record('loadEnd') 88 | this.render('loaded', false) 89 | imageCache[this.src] = 1 90 | }, err => { 91 | this.state.error = true 92 | this.state.loaded = false 93 | this.render('error', false) 94 | }) 95 | } 96 | 97 | render (state, cache) { 98 | this.elRenderer(this, state, cache) 99 | } 100 | 101 | performance () { 102 | let state = 'loading' 103 | let time = 0 104 | 105 | if (this.state.loaded) { 106 | state = 'loaded' 107 | time = (this.performanceData.loadEnd - this.performanceData.loadStart) / 1000 108 | } 109 | 110 | if (this.state.error) state = 'error' 111 | 112 | return { 113 | src: this.src, 114 | state, 115 | time 116 | } 117 | } 118 | 119 | destroy () { 120 | this.el = null 121 | this.src = null 122 | this.error = null 123 | this.loading = null 124 | this.bindType = null 125 | this.attempt = 0 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | const inBrowser = typeof window !== 'undefined' 2 | 3 | function remove (arr, item) { 4 | if (!arr.length) return 5 | const index = arr.indexOf(item) 6 | if (index > -1) return arr.splice(index, 1) 7 | } 8 | 9 | function assign (target, source) { 10 | if (!target || !source) return target || {} 11 | if (target instanceof Object) { 12 | for (let key in source) { 13 | target[key] = source[key] 14 | } 15 | } 16 | return target 17 | } 18 | 19 | function some (arr, fn) { 20 | let has = false 21 | for (let i = 0, len = arr.length; i < len; i++) { 22 | if (fn(arr[i])) { 23 | has = true 24 | break 25 | } 26 | } 27 | return has 28 | } 29 | 30 | function getBestSelectionFromSrcset (el, scale) { 31 | if (el.tagName !== 'IMG' || !el.getAttribute('data-srcset')) return 32 | 33 | let options = el.getAttribute('data-srcset') 34 | const result = [] 35 | const container = el.parentNode 36 | const containerWidth = container.offsetWidth * scale 37 | 38 | let spaceIndex 39 | let tmpSrc 40 | let tmpWidth 41 | 42 | options = options.trim().split(',') 43 | 44 | options.map(item => { 45 | item = item.trim() 46 | spaceIndex = item.lastIndexOf(' ') 47 | if (spaceIndex === -1) { 48 | tmpSrc = item 49 | tmpWidth = 999998 50 | } else { 51 | tmpSrc = item.substr(0, spaceIndex) 52 | tmpWidth = parseInt(item.substr(spaceIndex + 1, item.length - spaceIndex - 2), 10) 53 | } 54 | result.push([tmpWidth, tmpSrc]) 55 | }) 56 | 57 | result.sort(function (a, b) { 58 | if (a[0] < b[0]) { 59 | return -1 60 | } 61 | if (a[0] > b[0]) { 62 | return 1 63 | } 64 | if (a[0] === b[0]) { 65 | if (b[1].indexOf('.webp', b[1].length - 5) !== -1) { 66 | return 1 67 | } 68 | if (a[1].indexOf('.webp', a[1].length - 5) !== -1) { 69 | return -1 70 | } 71 | } 72 | return 0 73 | }) 74 | let bestSelectedSrc = '' 75 | let tmpOption 76 | const resultCount = result.length 77 | 78 | for (let i = 0; i < resultCount; i++) { 79 | tmpOption = result[i] 80 | if (tmpOption[0] >= containerWidth) { 81 | bestSelectedSrc = tmpOption[1] 82 | break 83 | } 84 | } 85 | 86 | return bestSelectedSrc 87 | } 88 | 89 | function find (arr, fn) { 90 | let item 91 | for (let i = 0, len = arr.length; i < len; i++) { 92 | if (fn(arr[i])) { 93 | item = arr[i] 94 | break 95 | } 96 | } 97 | return item 98 | } 99 | 100 | const getDPR = (scale = 1) => inBrowser && window.devicePixelRatio || scale 101 | 102 | function supportWebp () { 103 | if (!inBrowser) return false 104 | 105 | let support = true 106 | const d = document 107 | 108 | try { 109 | let el = d.createElement('object') 110 | el.type = 'image/webp' 111 | el.innerHTML = '!' 112 | d.body.appendChild(el) 113 | support = !el.offsetWidth 114 | d.body.removeChild(el) 115 | } catch (err) { 116 | support = false 117 | } 118 | 119 | return support 120 | } 121 | 122 | function throttle (action, delay) { 123 | let timeout = null 124 | let lastRun = 0 125 | return function () { 126 | if (timeout) { 127 | return 128 | } 129 | let elapsed = Date.now() - lastRun 130 | let context = this 131 | let args = arguments 132 | let runCallback = function () { 133 | lastRun = Date.now() 134 | timeout = false 135 | action.apply(context, args) 136 | } 137 | if (elapsed >= delay) { 138 | runCallback() 139 | } 140 | else { 141 | timeout = setTimeout(runCallback, delay) 142 | } 143 | } 144 | } 145 | 146 | function testSupportsPassive () { 147 | if (!inBrowser) return 148 | let support = false 149 | try { 150 | let opts = Object.defineProperty({}, 'passive', { 151 | get: function() { 152 | support = true 153 | } 154 | }) 155 | window.addEventListener("test", null, opts) 156 | } catch (e) {} 157 | return support 158 | } 159 | 160 | const supportsPassive = testSupportsPassive() 161 | 162 | const _ = { 163 | on (el, type, func) { 164 | if (supportsPassive) { 165 | el.addEventListener(type, func, { 166 | passive:true 167 | }) 168 | } else { 169 | el.addEventListener(type, func, false) 170 | } 171 | }, 172 | off (el, type, func) { 173 | el.removeEventListener(type, func) 174 | } 175 | } 176 | 177 | const loadImageAsync = (item, resolve, reject) => { 178 | let image = new Image() 179 | image.src = item.src 180 | 181 | image.onload = function () { 182 | resolve({ 183 | naturalHeight: image.naturalHeight, 184 | naturalWidth: image.naturalWidth, 185 | src: image.src 186 | }) 187 | } 188 | 189 | image.onerror = function (e) { 190 | reject(e) 191 | } 192 | } 193 | 194 | const style = (el, prop) => { 195 | return typeof getComputedStyle !== 'undefined' 196 | ? getComputedStyle(el, null).getPropertyValue(prop) 197 | : el.style[prop] 198 | } 199 | 200 | const overflow = (el) => { 201 | return style(el, 'overflow') + style(el, 'overflow-y') + style(el, 'overflow-x') 202 | } 203 | 204 | const scrollParent = (el) => { 205 | if (!inBrowser) return 206 | if (!(el instanceof HTMLElement)) { 207 | return window 208 | } 209 | 210 | let parent = el 211 | 212 | while (parent) { 213 | if (parent === document.body || parent === document.documentElement) { 214 | break 215 | } 216 | 217 | if (!parent.parentNode) { 218 | break 219 | } 220 | 221 | if (/(scroll|auto)/.test(overflow(parent))) { 222 | return parent 223 | } 224 | 225 | parent = parent.parentNode 226 | } 227 | 228 | return window 229 | } 230 | 231 | function isObject (obj) { 232 | return obj !== null && typeof obj === 'object' 233 | } 234 | 235 | export { 236 | inBrowser, 237 | remove, 238 | some, 239 | find, 240 | assign, 241 | _, 242 | isObject, 243 | throttle, 244 | supportWebp, 245 | getDPR, 246 | scrollParent, 247 | loadImageAsync, 248 | getBestSelectionFromSrcset 249 | } 250 | -------------------------------------------------------------------------------- /src/lazy.js: -------------------------------------------------------------------------------- 1 | import { 2 | remove, 3 | some, 4 | find, 5 | _, 6 | throttle, 7 | supportWebp, 8 | getDPR, 9 | scrollParent, 10 | getBestSelectionFromSrcset, 11 | assign, 12 | isObject 13 | } from './util' 14 | 15 | import ReactiveListener from './listener' 16 | 17 | const DEFAULT_URL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' 18 | const DEFAULT_EVENTS = ['scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove'] 19 | 20 | export default function (Vue) { 21 | return class Lazy { 22 | constructor ({ preLoad, error, loading, attempt, silent, scale, listenEvents, hasbind, filter, adapter }) { 23 | this.ListenerQueue = [] 24 | this.options = { 25 | silent: silent || true, 26 | preLoad: preLoad || 1.3, 27 | error: error || DEFAULT_URL, 28 | loading: loading || DEFAULT_URL, 29 | attempt: attempt || 3, 30 | scale: getDPR(scale), 31 | ListenEvents: listenEvents || DEFAULT_EVENTS, 32 | hasbind: false, 33 | supportWebp: supportWebp(), 34 | filter: filter || {}, 35 | adapter: adapter || {} 36 | } 37 | this.initEvent() 38 | 39 | this.lazyLoadHandler = throttle(() => { 40 | let catIn = false 41 | this.ListenerQueue.forEach(listener => { 42 | if (listener.state.loaded) return 43 | catIn = listener.checkInView() 44 | catIn && listener.load() 45 | }) 46 | }, 200) 47 | } 48 | 49 | config (options = {}) { 50 | assign(this.options, options) 51 | } 52 | 53 | addLazyBox (vm) { 54 | this.ListenerQueue.push(vm) 55 | this.options.hasbind = true 56 | this.initListen(window, true) 57 | } 58 | 59 | add (el, binding, vnode) { 60 | if (some(this.ListenerQueue, item => item.el === el)) { 61 | this.update(el, binding) 62 | return Vue.nextTick(this.lazyLoadHandler) 63 | } 64 | 65 | let { src, loading, error } = this.valueFormatter(binding.value) 66 | 67 | Vue.nextTick(() => { 68 | let tmp = getBestSelectionFromSrcset(el, this.options.scale) 69 | 70 | if (tmp) { 71 | src = tmp 72 | } 73 | 74 | const container = Object.keys(binding.modifiers)[0] 75 | let $parent 76 | 77 | if (container) { 78 | $parent = vnode.context.$refs[container] 79 | // if there is container passed in, try ref first, then fallback to getElementById to support the original usage 80 | $parent = $parent ? $parent.$el || $parent : document.getElementById(container) 81 | } 82 | 83 | if (!$parent) { 84 | $parent = scrollParent(el) 85 | } 86 | 87 | this.ListenerQueue.push(this.listenerFilter(new ReactiveListener({ 88 | bindType: binding.arg, 89 | $parent, 90 | el, 91 | loading, 92 | error, 93 | src, 94 | elRenderer: this.elRenderer.bind(this), 95 | options: this.options 96 | }))) 97 | 98 | if (!this.ListenerQueue.length || this.options.hasbind) return 99 | 100 | this.options.hasbind = true 101 | this.initListen(window, true) 102 | $parent && this.initListen($parent, true) 103 | this.lazyLoadHandler() 104 | Vue.nextTick(() => this.lazyLoadHandler()) 105 | }) 106 | } 107 | 108 | update (el, binding) { 109 | let { src, loading, error } = this.valueFormatter(binding.value) 110 | 111 | const exist = find(this.ListenerQueue, item => item.el === el) 112 | 113 | exist && exist.src !== src && exist.update({ 114 | src, 115 | loading, 116 | error 117 | }) 118 | this.lazyLoadHandler() 119 | Vue.nextTick(() => this.lazyLoadHandler()) 120 | } 121 | 122 | remove (el) { 123 | if (!el) return 124 | const existItem = find(this.ListenerQueue, item => item.el === el) 125 | existItem && remove(this.ListenerQueue, existItem) && existItem.destroy() 126 | this.options.hasbind && !this.ListenerQueue.length && this.initListen(window, false) 127 | } 128 | 129 | removeComponent (vm) { 130 | vm && remove(this.ListenerQueue, vm) 131 | this.options.hasbind && !this.ListenerQueue.length && this.initListen(window, false) 132 | } 133 | 134 | initListen (el, start) { 135 | this.options.hasbind = start 136 | this.options.ListenEvents.forEach((evt) => _[start ? 'on' : 'off'](el, evt, this.lazyLoadHandler)) 137 | } 138 | 139 | initEvent () { 140 | this.Event = { 141 | listeners: { 142 | loading: [], 143 | loaded: [], 144 | error: [] 145 | } 146 | } 147 | 148 | this.$on = (event, func) => { 149 | this.Event.listeners[event].push(func) 150 | } 151 | 152 | this.$once = (event, func) => { 153 | const vm = this 154 | function on () { 155 | vm.$off(event, on) 156 | func.apply(vm, arguments) 157 | } 158 | this.$on(event, on) 159 | } 160 | 161 | this.$off = (event, func) => { 162 | if (!func) { 163 | this.Event.listeners[event] = [] 164 | return 165 | } 166 | remove(this.Event.listeners[event], func) 167 | } 168 | 169 | this.$emit = (event, context, inCache) => { 170 | this.Event.listeners[event].forEach(func => func(context, inCache)) 171 | } 172 | } 173 | 174 | performance () { 175 | let list = [] 176 | 177 | this.ListenerQueue.map(item => { 178 | list.push(item.performance()) 179 | }) 180 | 181 | return list 182 | } 183 | 184 | /** 185 | * set element attribute with image'url and state 186 | * @param {object} lazyload listener object 187 | * @param {string} state will be rendered 188 | * @param {bool} inCache is rendered from cache 189 | * @return 190 | */ 191 | elRenderer (listener, state, cache) { 192 | if (!listener.el) return 193 | const { el, bindType } = listener 194 | 195 | let src 196 | switch (state) { 197 | case 'loading': 198 | src = listener.loading 199 | break 200 | case 'error': 201 | src = listener.error 202 | break 203 | default: 204 | src = listener.src 205 | break 206 | } 207 | 208 | if (bindType) { 209 | el.style[bindType] = 'url(' + src + ')' 210 | } else if (el.getAttribute('src') !== src) { 211 | el.setAttribute('src', src) 212 | } 213 | 214 | el.setAttribute('lazy', state) 215 | 216 | this.$emit(state, listener, cache) 217 | this.options.adapter[state] && this.options.adapter[state](listener, this.options) 218 | } 219 | 220 | listenerFilter (listener) { 221 | if (this.options.filter.webp && this.options.supportWebp) { 222 | listener.src = this.options.filter.webp(listener, this.options) 223 | } 224 | if (this.options.filter.customer) { 225 | listener.src = this.options.filter.customer(listener, this.options) 226 | } 227 | return listener 228 | } 229 | 230 | 231 | /** 232 | * generate loading loaded error image url 233 | * @param {string} image's src 234 | * @return {object} image's loading, loaded, error url 235 | */ 236 | valueFormatter (value) { 237 | let src = value 238 | let loading = this.options.loading 239 | let error = this.options.error 240 | 241 | // value is object 242 | if (isObject(value)) { 243 | if (!value.src && !this.options.silent) console.error('Vue Lazyload warning: miss src with ' + value) 244 | src = value.src 245 | loading = value.loading || this.options.loading 246 | error = value.error || this.options.error 247 | } 248 | return { 249 | src, 250 | loading, 251 | error 252 | } 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue-Lazyload 2 | 3 | Vue module for lazyloading images in your applications. Some of goals of this project worth noting include: 4 | 5 | * Be lightweight, powerful and easy to use 6 | * Work on any image type 7 | * Add loading class while image is loading 8 | * Supports both of Vue 1.0 and Vue 2.0 9 | 10 | 11 | 12 | # Table of Contents 13 | 14 | * [___Demo___](#demo) 15 | * [___Requirements___](#requirements) 16 | * [___Installation___](#installation) 17 | * [___Usage___](#usage) 18 | * [___Constructor Options___](#constructor-options) 19 | * [___Implementation___](#implementation) 20 | * [___Basic___](#basic) 21 | * [___Css state___](#css-state) 22 | * [___Methods___](#methods) 23 | * [__Event hook__](#event-hook) 24 | * [__LazyLoadHandler__](#lazyloadhandler) 25 | * [__Performance__](#performance) 26 | * [___Authors && Contributors___](#authors-&&-Contributors) 27 | * [___License___](#license) 28 | 29 | 30 | # Demo 31 | 32 | [___Demo___](http://hilongjw.github.io/vue-lazyload/) 33 | 34 | # Requirements 35 | 36 | - [Vue.js](https://github.com/vuejs/vue) `1.x` or `2.x` 37 | 38 | 39 | # Installation 40 | 41 | ```bash 42 | 43 | # npm 44 | $ npm install vue-lazyload 45 | 46 | ``` 47 | 48 | # Usage 49 | 50 | main.js 51 | 52 | ```javascript 53 | 54 | import Vue from 'vue' 55 | import App from './App.vue' 56 | import VueLazyload from 'vue-lazyload' 57 | 58 | Vue.use(VueLazyload) 59 | 60 | // or with options 61 | Vue.use(VueLazyload, { 62 | preLoad: 1.3, 63 | error: 'dist/error.png', 64 | loading: 'dist/loading.gif', 65 | attempt: 1 66 | }) 67 | 68 | new Vue({ 69 | el: 'body', 70 | components: { 71 | App 72 | } 73 | }) 74 | ``` 75 | 76 | ## Constructor Options 77 | 78 | |key|description|default|options| 79 | |:---|---|---|---| 80 | | `preLoad`|proportion of pre-loading height|`1.3`|`Number`| 81 | |`error`|src of the image upon load fail|`'data-src'`|`String` 82 | |`loading`|src of the image while loading|`'data-src'`|`String`| 83 | |`attempt`|attempts count|`3`|`Number`| 84 | |`listenEvents`|events that you want vue listen for|`['scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove']`| [Desired Listen Events](#desired-listen-events) | 85 | |`adapter`| dynamically modify the attribute of element |`{ }`| [Element Adapter](#element-adapter) | 86 | |`filter`| the image's src filter |`{ }`| [Image url filter](#image-url-filter) | 87 | |`lazyComponent`| lazyload component | `false` | [Lazy Component](#lazy-component) 88 | 89 | ### Desired Listen Events 90 | 91 | You can configure which events you want vue-lazyload by passing in an array 92 | of listener names. 93 | 94 | ```javascript 95 | Vue.use(VueLazyload, { 96 | preLoad: 1.3, 97 | error: 'dist/error.png', 98 | loading: 'dist/loading.gif', 99 | attempt: 1, 100 | // the default is ['scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend'] 101 | listenEvents: [ 'scroll' ] 102 | }) 103 | ``` 104 | 105 | This is useful if you are having trouble with this plugin resetting itself to loading 106 | when you have certain animations and transitions taking place 107 | 108 | 109 | ### Image url filter 110 | 111 | dynamically modify the src of image 112 | 113 | ```javascript 114 | Vue.use(vueLazy, { 115 | filter: { 116 | webp ({ src }) { 117 | const isCDN = /qiniudn.com/ 118 | if (isCDN.test(src)) { 119 | src += '?imageView2/2/format/webp' 120 | } 121 | return src 122 | }, 123 | someProcess ({ el, src }) { 124 | if (el.getAttribute('large')) { 125 | src += '?large' 126 | } 127 | return src 128 | } 129 | } 130 | }) 131 | ``` 132 | 133 | 134 | ### Element Adapter 135 | 136 | ```javascript 137 | Vue.use(vueLazy, { 138 | adapter: { 139 | loaded ({ bindType, el, naturalHeight, naturalWidth, $parent, src, loading, error, Init }) { 140 | // do something here 141 | // example for call LoadedHandler 142 | LoadedHandler(el) 143 | }, 144 | loading (listender, Init) { 145 | console.log('loading') 146 | }, 147 | error (listender, Init) { 148 | console.log('error') 149 | } 150 | } 151 | }) 152 | ``` 153 | 154 | ### Lazy Component 155 | 156 | ```html 157 | 158 | 159 | 160 | 161 | 172 | ``` 173 | 174 | 175 | ## Implementation 176 | 177 | ### Basic 178 | 179 | vue-lazyload will set this img element's `src` with `imgUrl` string 180 | 181 | ```html 182 | 196 | 197 | 215 | ``` 216 | 217 | ### CSS state 218 | 219 | There are three states while img loading 220 | 221 | `loading` `loaded` `error` 222 | 223 | ```html 224 | 225 | 226 | 227 | ``` 228 | 229 | ```html 230 | 253 | ``` 254 | 255 | ## Methods 256 | 257 | ### Event Hook 258 | 259 | `vm.$Lazyload.$on(event, callback)` 260 | `vm.$Lazyload.$off(event, callback)` 261 | `vm.$Lazyload.$once(event, callback)` 262 | 263 | - `$on` Listen for a custom events `loading`, `loaded`, `error` 264 | - `$once` Listen for a custom event, but only once. The listener will be removed once it triggers for the first time. 265 | - `$off` Remove event listener(s). 266 | 267 | #### `vm.$Lazyload.$on` 268 | 269 | #### Arguments: 270 | 271 | * `{string} event` 272 | * `{Function} callback` 273 | 274 | #### Example 275 | 276 | ```javascript 277 | vm.$Lazyload.$on('loaded', function ({ bindType, el, naturalHeight, naturalWidth, $parent, src, loading, error }, formCache) { 278 | console.log(el, src) 279 | }) 280 | ``` 281 | 282 | #### `vm.$Lazyload.$once` 283 | 284 | #### Arguments: 285 | 286 | * `{string} event` 287 | * `{Function} callback` 288 | 289 | #### Example 290 | 291 | ```javascript 292 | vm.$Lazyload.$once('loaded', function ({ el, src }) { 293 | console.log(el, src) 294 | }) 295 | ``` 296 | 297 | #### `vm.$Lazyload.$off` 298 | 299 | If only the event is provided, remove all listeners for that event 300 | 301 | #### Arguments: 302 | 303 | * `{string} event` 304 | * `{Function} callback` 305 | 306 | #### Example 307 | 308 | ```javascript 309 | function handler ({ el, src }, formCache) { 310 | console.log(el, src) 311 | } 312 | vm.$Lazyload.$on('loaded', handler) 313 | vm.$Lazyload.$off('loaded', handler) 314 | vm.$Lazyload.$off('loaded') 315 | ``` 316 | 317 | ### LazyLoadHandler 318 | 319 | `vm.$Lazyload.lazyLoadHandler` 320 | 321 | Manually trigger lazy loading position calculation 322 | 323 | #### Example 324 | 325 | ```javascript 326 | 327 | this.$Lazyload.lazyLoadHandler() 328 | 329 | ``` 330 | 331 | ### Performance 332 | 333 | ``` 334 | this.$Lazyload.$on('loaded', (listener) { 335 | console.table(this.$Lazyload.performance()) 336 | }) 337 | ``` 338 | 339 | ![performance-demo](http://ww1.sinaimg.cn/large/69402bf8gw1fbo62ocvlaj213k09w78w.jpg) 340 | 341 | # Authors && Contributors 342 | 343 | - [hilongjw](https://github.com/hilongjw) 344 | - [imcvampire](https://github.com/imcvampire) 345 | - [darrynten](https://github.com/darrynten) 346 | - [biluochun](https://github.com/biluochun) 347 | - [whwnow](https://github.com/whwnow) 348 | - [Leopoldthecoder](https://github.com/Leopoldthecoder) 349 | - [michalbcz](https://github.com/michalbcz) 350 | - [blue0728](https://github.com/blue0728) 351 | - [JounQin](https://github.com/JounQin) 352 | - [llissery](https://github.com/llissery) 353 | - [mega667](https://github.com/mega667) 354 | - [RobinCK](https://github.com/RobinCK) 355 | - [GallenHu](https://github.com/GallenHu) 356 | 357 | # License 358 | 359 | [The MIT License](http://opensource.org/licenses/MIT) 360 | -------------------------------------------------------------------------------- /vue-lazyload.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Vue-Lazyload.js v1.0.0-rc12 3 | * (c) 2017 Awe 4 | * Released under the MIT License. 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.VueLazyload=e()}(this,function(){"use strict";function t(t,e){if(t.length){var n=t.indexOf(e);return n>-1?t.splice(n,1):void 0}}function e(t,e){if(!t||!e)return t||{};if(t instanceof Object)for(var n in e)t[n]=e[n];return t}function n(t,e){for(var n=!1,i=0,r=t.length;ie[0])return 1;if(t[0]===e[0]){if(e[1].indexOf(".webp",e[1].length-5)!==-1)return 1;if(t[1].indexOf(".webp",t[1].length-5)!==-1)return-1}return 0});for(var d="",l=void 0,c=i.length,h=0;h=o){d=l[1];break}return d}}function r(t,e){for(var n=void 0,i=0,r=t.length;i=e?a():n=setTimeout(a,e)}}}function a(){if(h){var t=!1;try{var e=Object.defineProperty({},"passive",{get:function(){t=!0}});window.addEventListener("test",null,e)}catch(t){}return t}}function u(t){return null!==t&&"object"===("undefined"==typeof t?"undefined":d(t))}var d="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol?"symbol":typeof t},l=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},c=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:1;return h&&window.devicePixelRatio||t},p=a(),v={on:function(t,e,n){p?t.addEventListener(e,n,{passive:!0}):t.addEventListener(e,n,!1)},off:function(t,e,n){t.removeEventListener(e,n)}},y=function(t,e,n){var i=new Image;i.src=t.src,i.onload=function(){e({naturalHeight:i.naturalHeight,naturalWidth:i.naturalWidth,src:i.src})},i.onerror=function(t){n(t)}},m=function(t,e){return"undefined"!=typeof getComputedStyle?getComputedStyle(t,null).getPropertyValue(e):t.style[e]},g=function(t){return m(t,"overflow")+m(t,"overflow-y")+m(t,"overflow-x")},b=function(t){if(h){if(!(t instanceof HTMLElement))return window;for(var e=t;e&&e!==document.body&&e!==document.documentElement&&e.parentNode;){if(/(scroll|auto)/.test(g(e)))return e;e=e.parentNode}return window}},w={},L=function(){function t(e){var n=e.el,i=e.src,r=e.error,o=e.loading,s=e.bindType,a=e.$parent,u=e.options,d=e.elRenderer;l(this,t),this.el=n,this.src=i,this.error=r,this.loading=o,this.bindType=s,this.attempt=0,this.naturalHeight=0,this.naturalWidth=0,this.options=u,this.initState(),this.performanceData={init:Date.now(),loadStart:null,loadEnd:null},this.rect=n.getBoundingClientRect(),this.$parent=a,this.elRenderer=d}return c(t,[{key:"initState",value:function(){this.state={error:!1,loaded:!1,rendered:!1}}},{key:"record",value:function(t){this.performanceData[t]=Date.now()}},{key:"update",value:function(t){var e=t.src,n=t.loading,i=t.error;this.src=e,this.loading=n,this.error=i,this.attempt=0,this.initState()}},{key:"getRect",value:function(){this.rect=this.el.getBoundingClientRect()}},{key:"checkInView",value:function(){return this.getRect(),this.rect.top0&&this.rect.left0}},{key:"load",value:function(){var t=this;return this.attempt>this.options.attempt-1&&this.state.error?void(this.options.silent||console.log("error end")):this.state.loaded||w[this.src]?this.render("loaded",!0):(this.render("loading",!1),this.attempt++,this.record("loadStart"),void y({src:this.src},function(e){t.src=e.src,t.naturalHeight=e.naturalHeight,t.naturalWidth=e.naturalWidth,t.state.loaded=!0,t.state.error=!1,t.record("loadEnd"),t.render("loaded",!1),w[t.src]=1},function(e){t.state.error=!0,t.state.loaded=!1,t.render("error",!1)}))}},{key:"render",value:function(t,e){this.elRenderer(this,t,e)}},{key:"performance",value:function(){var t="loading",e=0;return this.state.loaded&&(t="loaded",e=(this.performanceData.loadEnd-this.performanceData.loadStart)/1e3),this.state.error&&(t="error"),{src:this.src,state:t,time:e}}},{key:"destroy",value:function(){this.el=null,this.src=null,this.error=null,this.loading=null,this.bindType=null,this.attempt=0}}]),t}(),k="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",A=["scroll","wheel","mousewheel","resize","animationend","transitionend","touchmove"],E=function(a){return function(){function d(t){var e=this,n=t.preLoad,i=t.error,r=t.loading,a=t.attempt,u=t.silent,c=t.scale,h=t.listenEvents,p=(t.hasbind,t.filter),v=t.adapter;l(this,d),this.ListenerQueue=[],this.options={silent:u||!0,preLoad:n||1.3,error:i||k,loading:r||k,attempt:a||3,scale:f(c),ListenEvents:h||A,hasbind:!1,supportWebp:o(),filter:p||{},adapter:v||{}},this.initEvent(),this.lazyLoadHandler=s(function(){var t=!1;e.ListenerQueue.forEach(function(e){e.state.loaded||(t=e.checkInView(),t&&e.load())})},200)}return c(d,[{key:"config",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};e(this.options,t)}},{key:"addLazyBox",value:function(t){this.ListenerQueue.push(t),this.options.hasbind=!0,this.initListen(window,!0)}},{key:"add",value:function(t,e,r){var o=this;if(n(this.ListenerQueue,function(e){return e.el===t}))return this.update(t,e),a.nextTick(this.lazyLoadHandler);var s=this.valueFormatter(e.value),u=s.src,d=s.loading,l=s.error;a.nextTick(function(){var n=i(t,o.options.scale);n&&(u=n);var s=Object.keys(e.modifiers)[0],c=void 0;s&&(c=r.context.$refs[s],c=c?c.$el||c:document.getElementById(s)),c||(c=b(t)),o.ListenerQueue.push(o.listenerFilter(new L({bindType:e.arg,$parent:c,el:t,loading:d,error:l,src:u,elRenderer:o.elRenderer.bind(o),options:o.options}))),o.ListenerQueue.length&&!o.options.hasbind&&(o.options.hasbind=!0,o.initListen(window,!0),c&&o.initListen(c,!0),o.lazyLoadHandler(),a.nextTick(function(){return o.lazyLoadHandler()}))})}},{key:"update",value:function(t,e){var n=this,i=this.valueFormatter(e.value),o=i.src,s=i.loading,u=i.error,d=r(this.ListenerQueue,function(e){return e.el===t});d&&d.src!==o&&d.update({src:o,loading:s,error:u}),this.lazyLoadHandler(),a.nextTick(function(){return n.lazyLoadHandler()})}},{key:"remove",value:function(e){if(e){var n=r(this.ListenerQueue,function(t){return t.el===e});n&&t(this.ListenerQueue,n)&&n.destroy(),this.options.hasbind&&!this.ListenerQueue.length&&this.initListen(window,!1)}}},{key:"removeComponent",value:function(e){e&&t(this.ListenerQueue,e),this.options.hasbind&&!this.ListenerQueue.length&&this.initListen(window,!1)}},{key:"initListen",value:function(t,e){var n=this;this.options.hasbind=e,this.options.ListenEvents.forEach(function(i){return v[e?"on":"off"](t,i,n.lazyLoadHandler)})}},{key:"initEvent",value:function(){var e=this;this.Event={listeners:{loading:[],loaded:[],error:[]}},this.$on=function(t,n){e.Event.listeners[t].push(n)},this.$once=function(t,n){function i(){r.$off(t,i),n.apply(r,arguments)}var r=e;e.$on(t,i)},this.$off=function(n,i){return i?void t(e.Event.listeners[n],i):void(e.Event.listeners[n]=[])},this.$emit=function(t,n,i){e.Event.listeners[t].forEach(function(t){return t(n,i)})}}},{key:"performance",value:function(){var t=[];return this.ListenerQueue.map(function(e){t.push(e.performance())}),t}},{key:"elRenderer",value:function(t,e,n){if(t.el){var i=t.el,r=t.bindType,o=void 0;switch(e){case"loading":o=t.loading;break;case"error":o=t.error;break;default:o=t.src}r?i.style[r]="url("+o+")":i.getAttribute("src")!==o&&i.setAttribute("src",o),i.setAttribute("lazy",e),this.$emit(e,t,n),this.options.adapter[e]&&this.options.adapter[e](t,this.options)}}},{key:"listenerFilter",value:function(t){return this.options.filter.webp&&this.options.supportWebp&&(t.src=this.options.filter.webp(t,this.options)),this.options.filter.customer&&(t.src=this.options.filter.customer(t,this.options)),t}},{key:"valueFormatter",value:function(t){var e=t,n=this.options.loading,i=this.options.error;return u(t)&&(t.src||this.options.silent||console.error("Vue Lazyload warning: miss src with "+t),e=t.src,n=t.loading||this.options.loading,i=t.error||this.options.error),{src:e,loading:n,error:i}}}]),d}()},z=function(t){return{props:{tag:{type:String,default:"div"}},render:function(t){return this.show===!1?t(this.tag,{attrs:{class:"cov"}}):t(this.tag,{attrs:{class:"cov"}},this.$slots.default)},data:function(){return{state:{loaded:!1},rect:{},show:!1}},mounted:function(){t.addLazyBox(this),t.lazyLoadHandler()},beforeDestroy:function(){t.removeComponent(this)},methods:{getRect:function(){this.rect=this.$el.getBoundingClientRect()},checkInView:function(){return this.getRect(),h&&this.rect.top0&&this.rect.left0},load:function(){this.show=!0,this.$emit("show",this)}}}},H={install:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=E(t),r=new i(n),o="2"===t.version.split(".")[0];t.prototype.$Lazyload=r,n.lazyComponent&&t.component("lazy-component",z(r)),o?t.directive("lazy",{bind:r.add.bind(r),update:r.update.bind(r),componentUpdated:r.lazyLoadHandler.bind(r),unbind:r.remove.bind(r)}):t.directive("lazy",{bind:r.lazyLoadHandler.bind(r),update:function(t,n){e(this.vm.$refs,this.vm.$els),r.add(this.el,{modifiers:this.modifiers||{},arg:this.arg,value:t,oldValue:n},{context:this.vm})},unbind:function(){r.remove(this.el)}})}};return H}); --------------------------------------------------------------------------------