diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..535c761 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,35 @@ +env: + es6: true + browser: true + +parserOptions: + ecmaVersion: 6 + sourceType: module + +rules: + indent: [ 1, 2, SwitchCase: 1 ] + quotes: [ 1, single ] + linebreak-style: [ 1, unix ] + semi: [ 1, never ] + no-undef: [ 1 ] + no-console: [ 0 ] + no-debug: [ 0 ] + no-unused-vars: [ 1 ] + space-infix-ops: [ 1 ] + no-multi-spaces: [ 1 ] + no-fallthrough: [ 0 ] + comma-dangle: [1, only-multiline] + eol-last: 'error' + +globals: + Vue: true + module: true + require: true + crayfish: true + Promise: true + UParams: true + Geohash: true + hybridAPI: true + UBT: true + +extends: 'eslint:recommended' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d564d61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.log +.cache +.DS_Store +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c4632a --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# vue-img + +> hash2path wrapper for vue 2 + +## 使用方法 + +### 安装插件 + +```JS +// 默认全局配置 +Vue.use(VueImg) + +// 自定义全局配置 +Vue.use(VueImg, { + loading: '', + error: '', + prefix: '', + quality: 100 +}) +``` + +### 使用指令 + +#### 基本用法 + +> 由于 Vue 2 删除了指令中的 params,故采用 object value 的形式传入参数 + +```HTML + + + + + + +
+ + +``` + +### 可读属性 + +VueImg 提供了一些属性,可用于指令以外的场合。你应当视它们为**只读**属性,避免直接修改。 + +```JS +VueImg.cdn // [String] 当前环境的默认 CDN +VueImg.canWebp // [Boolean] 当前环境是否支持 webP +VueImg.getSrc({ ... }) // [Function] 获取图片地址 +``` + +### 参数列表 + +名称 | 描述 | 全局配置 | 指令参数 | getSrc 函数 +--- | --- | --- | --- | --- +hash | [String] 图片哈希(必填)| 不支持 | 支持 | 支持 +width | [Number] 宽度 | 不支持 | 支持 | 支持 +height | [Number] 高度 | 不支持 | 支持 | 支持 +quality | [Number] 图片质量 | 支持 | 支持 | 支持 +prefix | [String] CDN 地址前缀 | 支持 | 支持 | 支持 +suffix | [String] CDN 处理后缀 [?] | 支持 | 支持 | 支持 +loading | [String] 加载中默认图片哈希 | 支持 | 支持 | 不支持 +error | [String] 失败替换图片哈希 | 支持 | 支持 | 不支持 +disableWebp | [Boolean] 禁用 webP | 支持 | 支持 | 支持 + +- `suffix` 参数可用于模糊、旋转等特殊处理,具体请参考[《七牛 CDN 开发者文档》](http://developer.qiniu.com/code/v6/api/kodo-api/image/imagemogr2.html)。 + +## 贡献代码 + +```bash +npm install # 安装依赖 +npm run dev # 构建文件 +npm run test # 单元测试 +``` + +- 提交代码前请确保已通过测试。 +- 更多细节请参考[《饿了么开源项目贡献指南》](https://github.com/ElemeFE/vue-img/blob/master/.github/CONTRIBUTING_zh-cn.md)。 + +## 开源协议 + +MIT diff --git a/build/karma.config.js b/build/karma.config.js new file mode 100644 index 0000000..db449d4 --- /dev/null +++ b/build/karma.config.js @@ -0,0 +1,14 @@ +module.exports = config => { + config.set({ + basePath: '../', + browsers: ['Chrome'], + files: [ + 'http://github.elemecdn.com/vuejs/vue/v2.2.4/dist/vue.js', + 'dist/vue-img.js', + 'build/test/*.test.js' + ], + frameworks: ['mocha', 'chai'], + reporters: ['mocha'], + singleRun: true + }) +} diff --git a/build/rollup.config.js b/build/rollup.config.js new file mode 100644 index 0000000..254e73d --- /dev/null +++ b/build/rollup.config.js @@ -0,0 +1,18 @@ +import eslint from 'rollup-plugin-eslint' +import buble from 'rollup-plugin-buble' + +const argv = process.argv[4] +const config = { + '--es5': { dest: 'vue-img', format: 'umd', plugins: [ eslint(), buble() ] }, + '--es6': { dest: 'vue-img.es6', format: 'es', plugins: [ eslint() ] } +}[argv] + +export default { + entry: 'src/index.js', + dest: `dist/${config.dest}.js`, + + format: `${config.format}`, + moduleName: 'VueImg', + + plugins: config.plugins +} diff --git a/build/test/base.test.js b/build/test/base.test.js new file mode 100644 index 0000000..a5381d7 --- /dev/null +++ b/build/test/base.test.js @@ -0,0 +1,10 @@ +describe('检测依赖', () => { + it('Vue 2 已安装', () => { + expect(Vue).to.exist + expect(Vue.version.split('.')[0]).to.equal('2') + }) + + it('VueImg 已安装', () => { + expect(VueImg).to.be.an('object') + }) +}) diff --git a/build/test/core.test.js b/build/test/core.test.js new file mode 100644 index 0000000..3e97382 --- /dev/null +++ b/build/test/core.test.js @@ -0,0 +1,56 @@ +describe('检测核心函数 getSrc', () => { + const config = { hash: '50f940dbce46148638e03d0778a4c5f8jpeg' } + + it('{ null }', () => { + expect(VueImg.getSrc()) + .to.equal('') + expect(VueImg.getSrc({})) + .to.equal('') + expect(VueImg.getSrc({ hash: 12450 })) + .to.equal('') + expect(VueImg.getSrc({ hash: null })) + .to.equal('') + }) + + it('{ hash }', () => { + expect(VueImg.getSrc(config)) + .to.match(/^http:\/\/fuss10.elemecdn.com\//) + .to.include('/5/0f/940dbce46148638e03d0778a4c5f8jpeg.jpeg') + .to.include('?imageMogr/') + .to.match(/format\/webp\/$/) + }) + + it('{ hash, prefix }', () => { + config.prefix = 'eleme.me' + expect(VueImg.getSrc(config)) + .to.match(/^eleme\.me\//) + }) + + it('{ hash, prefix, suffix }', () => { + config.suffix = 'github' + expect(VueImg.getSrc(config)) + .to.match(/github$/) + }) + + it('{ hash, prefix, suffix, quality }', () => { + config.quality = 75 + expect(VueImg.getSrc(config)) + .to.include('/quality/75/') + }) + + it('{ hash, prefix, suffix, quality, width, height }', () => { + config.width = 100 + expect(VueImg.getSrc(config)) + .to.include('/thumbnail/100x/') + + config.height = 200 + expect(VueImg.getSrc(config)) + .to.include('/!100x200r/gravity/Center/crop/100x200/') + }) + + it('{ disableWebp }', () => { + config.disableWebp = true + expect(VueImg.getSrc(config)) + .to.not.include('webp') + }) +}) diff --git a/build/test/directive.test.js b/build/test/directive.test.js new file mode 100644 index 0000000..25f5739 --- /dev/null +++ b/build/test/directive.test.js @@ -0,0 +1,149 @@ +describe('检测指令', function() { + this.timeout(6000) + + const hash = '50f940dbce46148638e03d0778a4c5f8jpeg' + const loading = '7b73ae0bcb1e68afacbaff7d4b25780bjpeg' + const error = '4f88f93f3797600783990d32e5673ab7jpeg' + const config = { loading } + const re = /^url\(['"]?(.*?)['"]?\)$/ + let vm + + before(done => { + const el = document.createElement('section') + el.id = 'app' + el.innerHTML = '' + document.body.appendChild(el) + + Vue.use(VueImg) + vm = new Vue({ el: '#app', data: { config } }) + setTimeout(done, 1000) + }) + + describe('{ loading }', () => { + before(done => { + Vue.set(vm.config, 'hash', 'xxxxxx') + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const imgSrc = document.querySelector('#app img').src + const divImg = document.querySelector('#app div').style.backgroundImage.match(re)[1] + const src = VueImg.getSrc({ hash: loading }) + + expect(imgSrc).to.equal(src) + expect(divImg).to.equal(src) + }) + }) + + describe('{ error }', () => { + before(done => { + Vue.set(vm.config, 'error', error) + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const imgSrc = document.querySelector('#app img').src + const divImg = document.querySelector('#app div').style.backgroundImage.match(re)[1] + const src = VueImg.getSrc({ hash: error }) + + expect(imgSrc).to.equal(src) + expect(divImg).to.equal(src) + }) + }) + + describe('{ hash }', () => { + before(done => { + Vue.set(vm.config, 'hash', hash) + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const imgSrc = document.querySelector('#app img').src + const divImg = document.querySelector('#app div').style.backgroundImage.match(re)[1] + const src = VueImg.getSrc(config) + + expect(imgSrc).to.equal(src) + expect(divImg).to.equal(src) + }) + }) + + describe('{ hash, suffix }', () => { + before(done => { + Vue.set(vm.config, 'suffix', 'blur/3x5') + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const imgSrc = document.querySelector('#app img').src + const divImg = document.querySelector('#app div').style.backgroundImage.match(re)[1] + const src = VueImg.getSrc(config) + + expect(imgSrc).to.equal(src) + expect(divImg).to.equal(src) + }) + }) + + describe('{ hash, suffix, height }', () => { + before(done => { + Vue.set(vm.config, 'height', 100) + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const imgSrc = document.querySelector('#app img').src + const divImg = document.querySelector('#app div').style.backgroundImage.match(re)[1] + const src = VueImg.getSrc(config) + + expect(imgSrc).to.equal(src) + expect(divImg).to.equal(src) + }) + }) + + describe('{ hash, suffix, width, height }', () => { + before(done => { + Vue.set(vm.config, 'width', 100) + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const imgSrc = document.querySelector('#app img').src + const divImg = document.querySelector('#app div').style.backgroundImage.match(re)[1] + const src = VueImg.getSrc(config) + + expect(imgSrc).to.equal(src) + expect(divImg).to.equal(src) + }) + }) + + describe('{ hash, suffix, width, height, quality }', () => { + before(done => { + Vue.set(vm.config, 'quality', 80) + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const imgSrc = document.querySelector('#app img').src + const divImg = document.querySelector('#app div').style.backgroundImage.match(re)[1] + const src = VueImg.getSrc(config) + + expect(imgSrc).to.equal(src) + expect(divImg).to.equal(src) + }) + }) + + describe('hash', () => { + before(done => { + vm.config = hash + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const imgSrc = document.querySelector('#app img').src + const divImg = document.querySelector('#app div').style.backgroundImage.match(re)[1] + const src = VueImg.getSrc({ hash }) + + expect(imgSrc).to.equal(src) + expect(divImg).to.equal(src) + }) + }) +}) diff --git a/build/test/global.test.js b/build/test/global.test.js new file mode 100644 index 0000000..4ac32eb --- /dev/null +++ b/build/test/global.test.js @@ -0,0 +1,160 @@ +describe('全局配置', function() { + this.timeout(6000) + + const hash = '50f940dbce46148638e03d0778a4c5f8jpeg' + const loading = '7b73ae0bcb1e68afacbaff7d4b25780bjpeg' + const error = '4f88f93f3797600783990d32e5673ab7jpeg' + const createViewModel = ({ option, config }) => { + const el = document.createElement('div') + const id = `vm-${Date.now().toString(16)}` + el.id = id; + el.innerHTML = `` + document.body.appendChild(el) + + VueImg.installed = false // Allow VueImg to be installed again + Vue.use(VueImg, option) + return new Vue({ + el: `#${id}`, + data: { config } + }) + } + + describe('{ loading }', () => { + let vm + + before(done => { + vm = createViewModel({ + option: { loading }, + config: { hash: 'xxxxxxxxxxx' } + }) + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const img = vm.$el.querySelector('img') + expect(img.src) + .to.equal(VueImg.getSrc({ + hash: loading + })) + }) + }) + + describe('{ loading, error }', () => { + let vm + + before(done => { + vm = createViewModel({ + option: { loading, error }, + config: { hash: 'xxxxxxxxxxx' } + }) + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const img = vm.$el.querySelector('img') + expect(img.src) + .to.equal(VueImg.getSrc({ + hash: error + })) + }) + }) + + describe('{ prefix }', () => { + const prefix = 'https://fuss10.elemecdn.com' + let vm + + before(done => { + vm = createViewModel({ + option: { prefix }, + config: { hash } + }) + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const img = vm.$el.querySelector('img') + expect(img.src) + .to.equal(VueImg.getSrc({ + prefix, hash + })) + }) + }) + + describe('{ quality }', () => { + let vm + + before(done => { + vm = createViewModel({ + option: { quality: 80 }, + config: { hash } + }) + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const img = vm.$el.querySelector('img') + expect(img.src) + .to.equal(VueImg.getSrc({ + hash, + quality: 80 + })) + }) + }) + + describe('局部 + 全局 { loading }', () => { + let vm + + before(done => { + vm = createViewModel({ + option: { loading }, + config: { loading: '', hash: 'xxx' } + }) + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const img = vm.$el.querySelector('img') + expect(img.src) + .to.equal('') + }) + }) + + describe('局部 + 全局 { error }', () => { + let vm + + before(done => { + vm = createViewModel({ + option: { error }, + config: { error: '', hash: 'xxx' } + }) + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const img = vm.$el.querySelector('img') + expect(img.src) + .to.equal('') + }) + }) + + describe('局部 + 全局 { quality }', () => { + let vm + + before(done => { + vm = createViewModel({ + option: { quality: 80 }, + config: { quality: 90, hash } + }) + setTimeout(done, 2000) + }) + + it('测试通过', () => { + const img = vm.$el.querySelector('img') + expect(img.src) + .to.equal(VueImg.getSrc({ + hash, + quality: 90 + })) + }) + }) +}) diff --git a/dist/vue-img.es6.js b/dist/vue-img.es6.js new file mode 100644 index 0000000..2164a4d --- /dev/null +++ b/dist/vue-img.es6.js @@ -0,0 +1,162 @@ +const VueImg$1 = Object.create(null); + +// Check webP support +VueImg$1.canWebp = false; +const img = new Image(); +img.onload = () => { VueImg$1.canWebp = true; }; +img.src = 'data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAsAAAABBxAREYiI/gcAAABWUDggGAAAADABAJ0BKgEAAQABABwlpAADcAD+/gbQAA=='; + +// Default cdn prefix +const protocol = location.protocol === 'https:' ? 'https://' : 'http://'; +const env = document.domain.match(/.(alpha|beta).ele(net)?.me$/); +VueImg$1.cdn = protocol + (env ? `fuss${env[0]}` : 'fuss10.elemecdn.com'); + +// Translate hash to path +const hashToPath = hash => hash.replace(/^(\w)(\w\w)(\w{29}(\w*))$/, '/$1/$2/$3.$4'); + +// Get image size +const getSize = (width, height) => { + const thumb = 'thumbnail/'; + const cover = `${width}x${height}`; + + if (width && height) return `${thumb}!${cover}r/gravity/Center/crop/${cover}/` + if (width) return `${thumb}${width}x/` + if (height) return `${thumb}x${height}/` + return '' +}; + +// Get image size +const getSrc = ({ hash, width, height, prefix, suffix, quality, disableWebp } = {}) => { + if (!hash || typeof hash !== 'string') return '' + + const _prefix = typeof prefix === 'string' ? prefix : VueImg$1.cdn; + const _suffix = typeof suffix === 'string' ? suffix : ''; + const _quality = typeof quality === 'number' ? `quality/${quality}/` : ''; + const _format = !disableWebp && VueImg$1.canWebp ? 'format/webp/' : ''; + const params = `${_quality}${_format}${getSize(width, height)}${_suffix}`; + + return _prefix + hashToPath(hash) + (params ? `?imageMogr/${params}` : '') +}; + +const hasProp = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); + +const copyKeys = ({ source, target, keys }) => { + keys.forEach(key => { + if (hasProp(source, key)) { + target[key] = source[key]; + } + }); +}; + +const setAttr = (el, src, tag) => { + if (tag === 'img') { + el.src = src; + } else { + el.style.backgroundImage = `url('${src}')`; + } +}; + +var getImageClass = (opt = {}) => { + class GlobalOptions { + constructor() { + copyKeys({ + source: opt, + target: this, + keys: [ + 'loading', 'error', + 'quality', + 'prefix', 'suffix', + 'disableWebp', + ], + }); + } + + hashToSrc(hash) { + const params = { hash }; + + copyKeys({ + source: this, + target: params, + keys: [ + 'width', 'height', 'quality', + 'prefix', 'suffix', + 'disableWebp', + ], + }); + return getSrc(params) + } + } + + class vImg extends GlobalOptions { + constructor(value) { + const params = value && typeof value === 'object' + ? value + : { hash: value }; + + super(); + copyKeys({ + source: params, + target: this, + keys: [ + 'hash', 'loading', 'error', + 'width', 'height', 'quality', + 'prefix', 'suffix', + 'disableWebp', + ], + }); + } + + toImageSrc() { + return this.hashToSrc(this.hash) + } + + toLoadingSrc() { + return this.hashToSrc(this.loading) + } + + toErrorSrc() { + return this.hashToSrc(this.error) + } + } + + return vImg +}; + +// Vue plugin installer +const install = (Vue, opt) => { + const vImg = getImageClass(opt); + + const update = (el, binding, vnode) => { + const vImgIns = new vImg(binding.value); + const vImgSrc = vImgIns.toImageSrc(); + const vImgErr = vImgIns.toErrorSrc(); + if (!vImgSrc) return + + const img = new Image(); + img.onload = () => { + setAttr(el, vImgSrc, vnode.tag); + }; + if (vImgErr) { + img.onerror = () => { + setAttr(el, vImgErr, vnode.tag); + }; + } + img.src = vImgSrc; + }; + + // Register Vue directive + Vue.directive('img', { + bind(el, binding, vnode) { + const src = new vImg(binding.value).toLoadingSrc(); + if (src) setAttr(el, src, vnode.tag); + update(el, binding, vnode); + }, + + update, + }); +}; + +VueImg$1.getSrc = getSrc; +VueImg$1.install = install; + +export default VueImg$1; diff --git a/dist/vue-img.js b/dist/vue-img.js new file mode 100644 index 0000000..2bdac22 --- /dev/null +++ b/dist/vue-img.js @@ -0,0 +1,189 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.VueImg = factory()); +}(this, (function () { 'use strict'; + +var VueImg$1 = Object.create(null); + +// Check webP support +VueImg$1.canWebp = false; +var img = new Image(); +img.onload = function () { VueImg$1.canWebp = true; }; +img.src = 'data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAsAAAABBxAREYiI/gcAAABWUDggGAAAADABAJ0BKgEAAQABABwlpAADcAD+/gbQAA=='; + +// Default cdn prefix +var protocol = location.protocol === 'https:' ? 'https://' : 'http://'; +var env = document.domain.match(/.(alpha|beta).ele(net)?.me$/); +VueImg$1.cdn = protocol + (env ? ("fuss" + (env[0])) : 'fuss10.elemecdn.com'); + +// Translate hash to path +var hashToPath = function (hash) { return hash.replace(/^(\w)(\w\w)(\w{29}(\w*))$/, '/$1/$2/$3.$4'); }; + +// Get image size +var getSize = function (width, height) { + var thumb = 'thumbnail/'; + var cover = width + "x" + height; + + if (width && height) { return (thumb + "!" + cover + "r/gravity/Center/crop/" + cover + "/") } + if (width) { return ("" + thumb + width + "x/") } + if (height) { return (thumb + "x" + height + "/") } + return '' +}; + +// Get image size +var getSrc = function (ref) { + if ( ref === void 0 ) ref = {}; + var hash = ref.hash; + var width = ref.width; + var height = ref.height; + var prefix = ref.prefix; + var suffix = ref.suffix; + var quality = ref.quality; + var disableWebp = ref.disableWebp; + + if (!hash || typeof hash !== 'string') { return '' } + + var _prefix = typeof prefix === 'string' ? prefix : VueImg$1.cdn; + var _suffix = typeof suffix === 'string' ? suffix : ''; + var _quality = typeof quality === 'number' ? ("quality/" + quality + "/") : ''; + var _format = !disableWebp && VueImg$1.canWebp ? 'format/webp/' : ''; + var params = "" + _quality + _format + (getSize(width, height)) + _suffix; + + return _prefix + hashToPath(hash) + (params ? ("?imageMogr/" + params) : '') +}; + +var hasProp = function (obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }; + +var copyKeys = function (ref) { + var source = ref.source; + var target = ref.target; + var keys = ref.keys; + + keys.forEach(function (key) { + if (hasProp(source, key)) { + target[key] = source[key]; + } + }); +}; + +var setAttr = function (el, src, tag) { + if (tag === 'img') { + el.src = src; + } else { + el.style.backgroundImage = "url('" + src + "')"; + } +}; + +var getImageClass = function (opt) { + if ( opt === void 0 ) opt = {}; + + var GlobalOptions = function GlobalOptions() { + copyKeys({ + source: opt, + target: this, + keys: [ + 'loading', 'error', + 'quality', + 'prefix', 'suffix', + 'disableWebp', + ], + }); + }; + + GlobalOptions.prototype.hashToSrc = function hashToSrc (hash) { + var params = { hash: hash }; + + copyKeys({ + source: this, + target: params, + keys: [ + 'width', 'height', 'quality', + 'prefix', 'suffix', + 'disableWebp', + ], + }); + return getSrc(params) + }; + + var vImg = (function (GlobalOptions) { + function vImg(value) { + var params = value && typeof value === 'object' + ? value + : { hash: value }; + + GlobalOptions.call(this); + copyKeys({ + source: params, + target: this, + keys: [ + 'hash', 'loading', 'error', + 'width', 'height', 'quality', + 'prefix', 'suffix', + 'disableWebp', + ], + }); + } + + if ( GlobalOptions ) vImg.__proto__ = GlobalOptions; + vImg.prototype = Object.create( GlobalOptions && GlobalOptions.prototype ); + vImg.prototype.constructor = vImg; + + vImg.prototype.toImageSrc = function toImageSrc () { + return this.hashToSrc(this.hash) + }; + + vImg.prototype.toLoadingSrc = function toLoadingSrc () { + return this.hashToSrc(this.loading) + }; + + vImg.prototype.toErrorSrc = function toErrorSrc () { + return this.hashToSrc(this.error) + }; + + return vImg; + }(GlobalOptions)); + + return vImg +}; + +// Vue plugin installer +var install = function (Vue, opt) { + var vImg = getImageClass(opt); + + var update = function (el, binding, vnode) { + var vImgIns = new vImg(binding.value); + var vImgSrc = vImgIns.toImageSrc(); + var vImgErr = vImgIns.toErrorSrc(); + if (!vImgSrc) { return } + + var img = new Image(); + img.onload = function () { + setAttr(el, vImgSrc, vnode.tag); + }; + if (vImgErr) { + img.onerror = function () { + setAttr(el, vImgErr, vnode.tag); + }; + } + img.src = vImgSrc; + }; + + // Register Vue directive + Vue.directive('img', { + bind: function bind(el, binding, vnode) { + var src = new vImg(binding.value).toLoadingSrc(); + if (src) { setAttr(el, src, vnode.tag); } + update(el, binding, vnode); + }, + + update: update, + }); +}; + +VueImg$1.getSrc = getSrc; +VueImg$1.install = install; + +return VueImg$1; + +}))); diff --git a/package.json b/package.json new file mode 100644 index 0000000..588661d --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "vue-img", + "version": "2.6.1", + "description": "hash2path wrapper for vue 2", + "main": "dist/vue-img.js", + "scripts": { + "dev": "rollup --config build/rollup.config.js --es5 & rollup --config build/rollup.config.js --es6", + "test": "karma start build/karma.config.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/banricho/vue-img.git" + }, + "author": "banricho", + "license": "MIT", + "devDependencies": { + "chai": "^3.5.0", + "karma": "^1.3.0", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^2.0.0", + "karma-mocha": "^1.2.0", + "karma-mocha-reporter": "^2.2.0", + "mocha": "^3.1.2", + "rollup": "^0.36.3", + "rollup-plugin-buble": "^0.14.0", + "rollup-plugin-eslint": "^3.0.0" + } +} diff --git a/src/base.js b/src/base.js new file mode 100644 index 0000000..af692e8 --- /dev/null +++ b/src/base.js @@ -0,0 +1,14 @@ +const VueImg = Object.create(null) + +// Check webP support +VueImg.canWebp = false +const img = new Image() +img.onload = () => { VueImg.canWebp = true } +img.src = 'data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAsAAAABBxAREYiI/gcAAABWUDggGAAAADABAJ0BKgEAAQABABwlpAADcAD+/gbQAA==' + +// Default cdn prefix +const protocol = location.protocol === 'https:' ? 'https://' : 'http://' +const env = document.domain.match(/.(alpha|beta).ele(net)?.me$/) +VueImg.cdn = protocol + (env ? `fuss${env[0]}` : 'fuss10.elemecdn.com') + +export default VueImg diff --git a/src/class.js b/src/class.js new file mode 100644 index 0000000..ec3f749 --- /dev/null +++ b/src/class.js @@ -0,0 +1,68 @@ +import { copyKeys } from './utils' +import getSrc from './src' + +export default (opt = {}) => { + class GlobalOptions { + constructor() { + copyKeys({ + source: opt, + target: this, + keys: [ + 'loading', 'error', + 'quality', + 'prefix', 'suffix', + 'disableWebp', + ], + }) + } + + hashToSrc(hash) { + const params = { hash } + + copyKeys({ + source: this, + target: params, + keys: [ + 'width', 'height', 'quality', + 'prefix', 'suffix', + 'disableWebp', + ], + }) + return getSrc(params) + } + } + + class vImg extends GlobalOptions { + constructor(value) { + const params = value && typeof value === 'object' + ? value + : { hash: value } + + super() + copyKeys({ + source: params, + target: this, + keys: [ + 'hash', 'loading', 'error', + 'width', 'height', 'quality', + 'prefix', 'suffix', + 'disableWebp', + ], + }) + } + + toImageSrc() { + return this.hashToSrc(this.hash) + } + + toLoadingSrc() { + return this.hashToSrc(this.loading) + } + + toErrorSrc() { + return this.hashToSrc(this.error) + } + } + + return vImg +} diff --git a/src/directive.js b/src/directive.js new file mode 100644 index 0000000..0199646 --- /dev/null +++ b/src/directive.js @@ -0,0 +1,38 @@ +import { setAttr } from './utils' +import getImageClass from './class' + +// Vue plugin installer +const install = (Vue, opt) => { + const vImg = getImageClass(opt) + + const update = (el, binding, vnode) => { + const vImgIns = new vImg(binding.value) + const vImgSrc = vImgIns.toImageSrc() + const vImgErr = vImgIns.toErrorSrc() + if (!vImgSrc) return + + const img = new Image() + img.onload = () => { + setAttr(el, vImgSrc, vnode.tag) + } + if (vImgErr) { + img.onerror = () => { + setAttr(el, vImgErr, vnode.tag) + } + } + img.src = vImgSrc + } + + // Register Vue directive + Vue.directive('img', { + bind(el, binding, vnode) { + const src = new vImg(binding.value).toLoadingSrc() + if (src) setAttr(el, src, vnode.tag) + update(el, binding, vnode) + }, + + update, + }) +} + +export default install diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..c1f33c1 --- /dev/null +++ b/src/index.js @@ -0,0 +1,8 @@ +import VueImg from './base' +import getSrc from './src' +import install from './directive' + +VueImg.getSrc = getSrc +VueImg.install = install + +export default VueImg diff --git a/src/src.js b/src/src.js new file mode 100644 index 0000000..13b4483 --- /dev/null +++ b/src/src.js @@ -0,0 +1,30 @@ +import VueImg from './base' + +// Translate hash to path +const hashToPath = hash => hash.replace(/^(\w)(\w\w)(\w{29}(\w*))$/, '/$1/$2/$3.$4') + +// Get image size +const getSize = (width, height) => { + const thumb = 'thumbnail/' + const cover = `${width}x${height}` + + if (width && height) return `${thumb}!${cover}r/gravity/Center/crop/${cover}/` + if (width) return `${thumb}${width}x/` + if (height) return `${thumb}x${height}/` + return '' +} + +// Get image size +const getSrc = ({ hash, width, height, prefix, suffix, quality, disableWebp } = {}) => { + if (!hash || typeof hash !== 'string') return '' + + const _prefix = typeof prefix === 'string' ? prefix : VueImg.cdn + const _suffix = typeof suffix === 'string' ? suffix : '' + const _quality = typeof quality === 'number' ? `quality/${quality}/` : '' + const _format = !disableWebp && VueImg.canWebp ? 'format/webp/' : '' + const params = `${_quality}${_format}${getSize(width, height)}${_suffix}` + + return _prefix + hashToPath(hash) + (params ? `?imageMogr/${params}` : '') +} + +export default getSrc diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..30a1a1c --- /dev/null +++ b/src/utils.js @@ -0,0 +1,17 @@ +const hasProp = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop) + +export const copyKeys = ({ source, target, keys }) => { + keys.forEach(key => { + if (hasProp(source, key)) { + target[key] = source[key] + } + }) +} + +export const setAttr = (el, src, tag) => { + if (tag === 'img') { + el.src = src + } else { + el.style.backgroundImage = `url('${src}')` + } +}