├── .all-contributorsrc
├── .babelrc.js
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .stylelintrc
├── .travis.yml
├── LICENSE
├── README-zh.md
├── README.md
├── build.sh
├── build
└── rollup.config.js
├── docs
├── autocrop.md
├── basic.md
├── customize.md
├── directive.md
├── extra-query.md
├── faq.md
├── loading.md
├── prefer-https.md
├── svg.md
└── with-v-img-preview.md
├── netlify.sh
├── notify.sh
├── package.json
├── src
├── compute-layout.js
├── directive.js
├── index.js
├── load-script.js
├── provider-config.js
├── reload.png
├── spinner.svg
├── ua.js
├── v-img.d.ts
└── v-img.vue
├── styleguide.config.js
├── styleguide
└── register.js
├── test
├── compute-layout.spec.js
├── provider-config.spec.js
└── ua.spec.js
└── yarn.lock
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "v-img",
3 | "projectOwner": "FEMessage",
4 | "repoType": "github",
5 | "repoHost": "https://github.com",
6 | "files": [
7 | "README.md"
8 | ],
9 | "imageSize": 100,
10 | "commit": false,
11 | "commitConvention": "angular",
12 | "contributors": [
13 | {
14 | "login": "colmugx",
15 | "name": "ColMugX",
16 | "avatar_url": "https://avatars1.githubusercontent.com/u/21327913?v=4",
17 | "profile": "https://colmugx.github.io",
18 | "contributions": [
19 | "code",
20 | "doc",
21 | "test",
22 | "translation"
23 | ]
24 | },
25 | {
26 | "login": "donaldshen",
27 | "name": "Donald Shen",
28 | "avatar_url": "https://avatars3.githubusercontent.com/u/19591950?v=4",
29 | "profile": "https://donaldshen.github.io/portfolio",
30 | "contributions": [
31 | "code",
32 | "test",
33 | "doc",
34 | "review"
35 | ]
36 | },
37 | {
38 | "login": "evillt",
39 | "name": "EVILLT",
40 | "avatar_url": "https://avatars3.githubusercontent.com/u/19513289?v=4",
41 | "profile": "https://evila.me",
42 | "contributions": [
43 | "code",
44 | "test",
45 | "doc"
46 | ]
47 | },
48 | {
49 | "login": "lianghx-319",
50 | "name": "Han",
51 | "avatar_url": "https://avatars2.githubusercontent.com/u/27187946?v=4",
52 | "profile": "https://github.com/lianghx-319",
53 | "contributions": [
54 | "code",
55 | "bug"
56 | ]
57 | },
58 | {
59 | "login": "xrr2016",
60 | "name": "Cold Stone",
61 | "avatar_url": "https://avatars1.githubusercontent.com/u/18013127?v=4",
62 | "profile": "https://coldstone.fun",
63 | "contributions": [
64 | "doc"
65 | ]
66 | },
67 | {
68 | "login": "levy9527",
69 | "name": "levy",
70 | "avatar_url": "https://avatars3.githubusercontent.com/u/9384365?v=4",
71 | "profile": "https://github.com/levy9527/blog",
72 | "contributions": [
73 | "projectManagement",
74 | "ideas"
75 | ]
76 | },
77 | {
78 | "login": "gd4Ark",
79 | "name": "4Ark",
80 | "avatar_url": "https://avatars0.githubusercontent.com/u/27952659?v=4",
81 | "profile": "https://4ark.me",
82 | "contributions": [
83 | "code"
84 | ]
85 | }
86 | ],
87 | "contributorsPerLine": 7,
88 | "skipCi": true
89 | }
90 |
--------------------------------------------------------------------------------
/.babelrc.js:
--------------------------------------------------------------------------------
1 | module.exports = api => {
2 | return {
3 | presets: [['@babel/env', {modules: api.env('test') ? 'commonjs' : false}]],
4 | plugins: [
5 | [
6 | '@babel/transform-runtime',
7 | {
8 | regenerator: true,
9 | }
10 | ]
11 | ]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_style = space
8 | indent_size = 2
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.md]
13 | insert_final_newline = false
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | browser: true,
5 | node: true
6 | },
7 | parserOptions: {
8 | parser: 'babel-eslint'
9 | },
10 | extends: [
11 | 'eslint:recommended',
12 | 'plugin:jest/recommended',
13 | 'plugin:vue/recommended',
14 | 'plugin:prettier/recommended',
15 | 'prettier/vue'
16 | ],
17 | plugins: ['vue', 'prettier'],
18 | rules: {
19 | 'no-console': [
20 | 'error',
21 | {
22 | allow: ['warn', 'error']
23 | }
24 | ],
25 | 'no-debugger': 'error',
26 | 'prettier/prettier': 'error'
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 | dist
7 | docs/build
8 | docs/index.html
9 | docs/*.woff
10 | docs/*.ttf
11 |
12 | # Editor directories and files
13 | .idea
14 | .vscode
15 | *.suo
16 | *.ntvs*
17 | *.njsproj
18 | *.sln
19 | .env
20 |
21 | .npmrc
22 | example
23 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | docs
2 | dist
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | semi: false
2 | singleQuote: true
3 | bracketSpacing: false
4 | trailingComma: 'es5'
5 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard",
3 | "rules": {
4 | "no-empty-source": null
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | branches:
2 | only:
3 | - master
4 | language: node_js
5 | node_js:
6 | - lts/*
7 | git:
8 | depth: 30
9 | install:
10 | - yarn --frozen-lockfile
11 | - yarn test
12 | script:
13 | - ./build.sh
14 | after_script:
15 | - ./notify.sh
16 | cache: yarn
17 | deploy:
18 | - provider: pages
19 | local-dir: docs
20 | github-token: $GITHUB_TOKEN
21 | skip-cleanup: true
22 | keep-history: true
23 | - provider: npm
24 | email: levy9527@qq.com
25 | api_key: $NPM_TOKEN
26 | skip-cleanup: true
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 FEMessage
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 |
--------------------------------------------------------------------------------
/README-zh.md:
--------------------------------------------------------------------------------
1 | # v-img
2 |
3 | [](https://travis-ci.com/FEMessage/v-img)
4 | [](https://www.npmjs.com/package/@femessage/v-img)
5 | [](https://www.npmjs.com/package/@femessage/v-img)
6 | [](https://github.com/FEMessage/v-img/blob/master/LICENSE)
7 | [](https://github.com/FEMessage/v-img/pulls)
8 | [](https://github-tools.github.io/github-release-notes/)
9 |
10 | 取代原生 img 元素,使用 webp!
11 |
12 | ## Table of Contents
13 |
14 | - [Features](#features)
15 | - [Install](#install)
16 | - [Usage](#Usage)
17 | - [Links](#links)
18 | - [Contributing](#contributing)
19 | - [Contributors](#contributors)
20 | - [License](#license)
21 |
22 | ## Features
23 |
24 | - 输入 jpg/png,输出 webp(svg/gif 原样返回,不做转换处理)
25 | - 根据浏览器环境自动选择是否使用 webp
26 | - 支持云服务
27 | - [x] 阿里云
28 | - [x] 华为云
29 | - [x] 七牛
30 | - 支持 SSR
31 |
32 | ## Install
33 |
34 | ```bash
35 | yarn add @femessage/v-img
36 | ```
37 |
38 | [⬆ Back to Top](#table-of-contents)
39 |
40 | ## Usage
41 |
42 | ```vue
43 |
44 | ```
45 |
46 | ### provider
47 |
48 | 设置 `provider` 来使用不同的图片处理方案,`provider` 参数有以下选项:
49 |
50 | - alibaba(默认,同样适用于华为云)
51 | - qiniu
52 | - self
53 | - none
54 |
55 | ---
56 |
57 | - 默认值为 alibaba。这意味着,只需要上传 jpg/png 到阿里云 OSS,使用 v-img 来显示图片,则会使用阿里云的图片处理服务,根据情况自动返回 webp。如果项目中已使用阿里云 OSS 进行图片存储,则可省略设置`provider`
58 |
59 | - 当 `provider=self` 时,也即图片放到自有主机,一般出现在项目私有化部署的情况,此时需为每一份图片自行准备相应的 webp 文件。 例如:
60 |
61 | ```sh
62 | images/
63 | avatar.png # 原有图片
64 | avatar.png.webp # 需要生成的 webp 文件
65 | ```
66 |
67 | 生成 webp 副本的方法可查看[此文](https://www.yuque.com/docs/share/3eaa556c-0780-4018-8ac1-4e217fb0efdb)。
68 |
69 | - 当`provider=none` 时,仅启用图片懒加载功能
70 |
71 | 总结一下就是:
72 |
73 | 1. 当 `provider=alibaba或qiniu` 时,使用云服务商图片处理功能,自动转 webp,默认对图片瘦身
74 | 2. 当 `provider=self` 时,期望用户准备好 webp 文件,判断浏览器环境支持 webp 时,请求 webp 图片
75 | 3. 当 `provider=none` 时,不对 src 做处理
76 |
77 | ### width/height
78 |
79 | 最好设置一下 width 或 height 属性,不需要带单位(100 而不是 100px),以便懒加载功能能更好地运行(有一种情况是,如果图片高度为 0,则极有可能导致一次性加载多张图片)。
80 |
81 | ### lazyload
82 |
83 | 组件借助了[lazysizes](https://github.com/aFarkas/lazysizes)来实现懒加载功能,自动开启。
84 |
85 | [⬆ Back to Top](#table-of-contents)
86 |
87 | ## Links
88 |
89 | - [把图片优化指南做成一个组件:v-img 介绍](https://zhuanlan.zhihu.com/p/99769484)
90 | - [api](https://FEMessage.github.io/v-img/)
91 | - [设计文档](https://www.yuque.com/docs/share/6edaadbb-9260-4b49-90d7-0a8d8d03b1de)
92 | - [webp](https://developers.google.com/speed/webp)
93 | - [alibaba oss guide](https://www.alibabacloud.com/help/doc-detail/47505.html?spm=a2c5t.11065259.1996646101.searchclickresult.2c802d29Uot0hD)
94 | - [qiniu images processing doc](https://developer.qiniu.com/dora/api/1270/the-advanced-treatment-of-images-imagemogr2)
95 | - [如何居中缩放 svg 图片](https://stackoverflow.com/a/11671373)
96 | - [svg 缩放属性详解](https://css-tricks.com/scale-svg/)
97 |
98 | [⬆ Back to Top](#table-of-contents)
99 |
100 | ## Contributing
101 |
102 | For those who are interested in contributing to this project, such as:
103 |
104 | - report a bug
105 | - request new feature
106 | - fix a bug
107 | - implement a new feature
108 |
109 | Please refer to our [contributing guide](https://github.com/FEMessage/.github/blob/master/CONTRIBUTING.md).
110 |
111 | [⬆ Back to Top](#table-of-contents)
112 |
113 | ## Contributors
114 |
115 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
127 |
128 | [⬆ Back to Top](#table-of-contents)
129 |
130 | ## License
131 |
132 | [MIT](./LICENSE)
133 |
134 | [⬆ Back to Top](#table-of-contents)
135 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # v-img
2 |
3 | [](https://travis-ci.com/FEMessage/v-img)
4 | [](https://www.npmjs.com/package/@femessage/v-img)
5 | [](https://www.npmjs.com/package/@femessage/v-img)
6 | [](https://github.com/FEMessage/v-img/blob/master/LICENSE)
7 | [](https://github.com/FEMessage/v-img/pulls)
8 | [](https://github-tools.github.io/github-release-notes/)
9 |
10 | This component aims to replace native img element and use webp!
11 |
12 | [中文文档](./README-zh.md)
13 |
14 | ## Table of Contents
15 |
16 | - [Features](#features)
17 | - [Install](#install)
18 | - [Usage](#Usage)
19 | - [Links](#links)
20 | - [Contributing](#contributing)
21 | - [Contributors](#contributors)
22 | - [License](#license)
23 |
24 | ## Features
25 |
26 | - Input jpg/png, output webp(svg/gif not be processed)
27 | - Automatically check whether your browser support webp and use it
28 | - Support cloud image service
29 | - [x] Alibaba Cloud
30 | - [x] Huawei Cloud
31 | - [x] Qiniu Cloud
32 | - Support SSR
33 |
34 | [⬆ Back to Top](#table-of-contents)
35 |
36 | ## Install
37 |
38 | ```bash
39 | yarn add @femessage/v-img
40 | ```
41 |
42 | [⬆ Back to Top](#table-of-contents)
43 |
44 | ## Usage
45 |
46 | ```vue
47 |
48 | ```
49 |
50 | ### provider
51 |
52 | The component use `provider` to choose image processing strategy, here are available values:
53 |
54 | - alibaba(default value, and it's compatible with Huawei Cloud)
55 | - qiniu
56 | - self
57 | - none
58 |
59 | ---
60 |
61 | - Alibaba OSS services are used by default, so if you host images on Alibaba OSS, `provider` can be omitted, this means jpg/png on Alibaba OSS, you can get webp when using v-img
62 |
63 | - When `provider=self`, means you host images on your server(like Nginx), this needs you need to prepare a webp file for each image, for example:
64 |
65 | ```sh
66 | images/
67 | avatar.png # your original image file
68 | avatar.png.webp # webp file need to be generated
69 | ```
70 |
71 | look at this [article](https://www.yuque.com/docs/share/3eaa556c-0780-4018-8ac1-4e217fb0efdb?translate=en) to see how to use node.js to generate webp from jpg/png
72 |
73 | - When `provider=none`, it only enable lazyload images function
74 |
75 | ### width/height
76 |
77 | You'd better set image's width or height attribute(like 100, not 100px) to make sure lazyload function can work correctly
78 |
79 | ### lazyload
80 |
81 | The `lazyload` function is supported by [lazysizes](https://github.com/aFarkas/lazysizes), and it is auto enabled.
82 |
83 | [⬆ Back to Top](#table-of-contents)
84 |
85 | ## Links
86 |
87 | - [api](https://FEMessage.github.io/v-img/)
88 | - [design doc](https://www.yuque.com/docs/share/6edaadbb-9260-4b49-90d7-0a8d8d03b1de?translate=en)
89 | - [webp](https://developers.google.com/speed/webp)
90 | - [alibaba oss guide](https://www.alibabacloud.com/help/doc-detail/47505.html?spm=a2c5t.11065259.1996646101.searchclickresult.2c802d29Uot0hD)
91 | - [qiniu images processing doc](https://developer.qiniu.com/dora/api/1270/the-advanced-treatment-of-images-imagemogr2)
92 | - [how to scale svg from center](https://stackoverflow.com/a/11671373)
93 | - [more about scaling svg](https://css-tricks.com/scale-svg/)
94 |
95 | [⬆ Back to Top](#table-of-contents)
96 |
97 | ## Contributing
98 |
99 | For those who are interested in contributing to this project, such as:
100 |
101 | - report a bug
102 | - request new feature
103 | - fix a bug
104 | - implement a new feature
105 |
106 | Please refer to our [contributing guide](https://github.com/FEMessage/.github/blob/master/CONTRIBUTING.md).
107 |
108 | [⬆ Back to Top](#table-of-contents)
109 |
110 | ## Contributors
111 |
112 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
113 |
114 |
115 |
116 |
117 |
128 |
129 |
130 |
131 |
132 |
133 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
134 |
135 | [⬆ Back to Top](#table-of-contents)
136 |
137 | ## License
138 |
139 | [MIT](./LICENSE)
140 |
141 | [⬆ Back to Top](#table-of-contents)
142 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | yarn stdver
3 |
4 | yarn build
5 |
--------------------------------------------------------------------------------
/build/rollup.config.js:
--------------------------------------------------------------------------------
1 | // rollup.config.js
2 | import vue from 'rollup-plugin-vue'
3 | import babel from 'rollup-plugin-babel'
4 | import commonjs from 'rollup-plugin-commonjs'
5 | import {terser} from 'rollup-plugin-terser'
6 | import img from 'rollup-plugin-img'
7 | import minimist from 'minimist'
8 |
9 | const argv = minimist(process.argv.slice(2))
10 |
11 | const config = {
12 | input: 'src/index.js',
13 | output: {
14 | name: 'VImg',
15 | exports: 'named'
16 | },
17 | plugins: [
18 | commonjs(),
19 | img(),
20 | vue({
21 | css: true,
22 | compileTemplate: true
23 | }),
24 | babel({
25 | runtimeHelpers: true,
26 | extensions: ['.js', '.jsx', '.es6', '.es', '.mjs', '.vue'],
27 | exclude: 'node_modules/**'
28 | })
29 | ]
30 | }
31 |
32 | // Only minify browser (iife) version
33 | if (argv.format === 'iife') {
34 | config.plugins.push(terser())
35 | }
36 |
37 | export default config
38 |
--------------------------------------------------------------------------------
/docs/autocrop.md:
--------------------------------------------------------------------------------
1 | ## 自动裁剪
2 | 如果传入 width 或 height 属性,会默认按照客户端的 devicePixelRatio(默认是 2) 获取合适大小的图片。节省流量的同时,保证图片最佳显示效果
3 |
4 | ```vue
5 |
6 |
7 |
原图 宽:400 高:267
8 |
9 |
按宽度等比缩放 宽:100 高:等比缩放
10 |
11 |
按高度等比缩放 高:100 宽:等比缩放
12 |
13 |
固定宽高剪裁,宽高:100,居中裁剪,不拉伸图片
14 |
15 |
对背景图片使用自动裁剪
16 |
17 |
18 |
19 |
20 |
29 | ```
30 |
--------------------------------------------------------------------------------
/docs/basic.md:
--------------------------------------------------------------------------------
1 | 不需要任何配置,即可拥有:
2 | - 懒加载
3 | - 优先返回webp
4 | - 默认使用阿里云的图片优化服务
5 |
6 | these features are out of box:
7 | - lazyload
8 | - webp first
9 | - set up alibaba image transform services
10 |
11 | ```vue
12 |
13 |
14 |
👇往下滑动,显示图片
15 |
height: {{height}}
16 |
点击切换src
17 |
18 |
可以选择关闭webp转换
19 |
20 |
21 |
22 |
23 |
37 |
38 |
46 | ```
47 |
--------------------------------------------------------------------------------
/docs/customize.md:
--------------------------------------------------------------------------------
1 | ## 自定义 loading 和 error
2 | load 时可以用类名 `.on-loading`, error 时可以用类名 `.on-error` 覆盖默认的样式
3 |
4 | ```vue
5 |
6 |
7 | 自定义 loading 图案
8 |
13 |
14 |
15 | 自定义 error 的图案
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 | ```
28 |
29 | ### 可在注册组件时全局设置,适合全局修改
30 | ```javascript
31 | import Vue from 'vue'
32 | import VImg from '@femessage/v-img'
33 |
34 | Vue.ues(VImg, {
35 | placeholder: 'https://deepexi-moby.oss-cn-shenzhen.aliyuncs.com/femessage/bean_eater.svg',
36 | error: 'https://deepexi-moby.oss-cn-shenzhen.aliyuncs.com/femessage/iconmonstr-refresh-6.svg'
37 | })
38 | ```
39 |
40 | ### 可在使用组件时,单独给组件设置
41 | 自定义图案的使用逻辑: 组件属性设置 > 全局设置 > 默认设置
42 |
43 |
--------------------------------------------------------------------------------
/docs/directive.md:
--------------------------------------------------------------------------------
1 | 使用指令 `v-img` 可以对 background-image 进行图片优化。
2 |
3 | 与 v-img 一样的参数设置,如 `v-img="{src: '', provider: 'qiniu'}"`
4 |
5 |
6 | use `v-img` directive, can let background-image to use webp。
7 |
8 | `v-img="{src: '', provider: 'qiniu'}"`
9 |
10 | ```vue
11 |
12 |
16 |
17 |
18 |
27 | ```
28 |
--------------------------------------------------------------------------------
/docs/extra-query.md:
--------------------------------------------------------------------------------
1 | 该例子展示了使用阿里云图片参数实现图片处理
2 |
3 | This example shows using alibaba image service to process images.
4 |
5 | ```vue
6 |
7 |
8 |
Before
9 |
10 |
11 |
After
12 |
13 |
14 |
15 |
16 |
25 | ```
26 |
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | ## 在 TypeScript 中指定组件的类型
2 |
3 | ```html
4 |
5 |
6 |
7 |
21 | ```
22 |
--------------------------------------------------------------------------------
/docs/loading.md:
--------------------------------------------------------------------------------
1 | 懒加载时,会先出现 loading 的图片;图片加载失败,会出现 reload 的占位图。
2 | 不同尺寸时的样式如下
3 |
4 | ```vue
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ```
18 |
--------------------------------------------------------------------------------
/docs/prefer-https.md:
--------------------------------------------------------------------------------
1 | if `preferHttps` enable and the provider is not (none | self):
2 | 1. //img-url will transform to https://img-url
3 | 2. http://img-url will get warning on the console
4 |
5 | ```vue
6 |
7 |
8 |
9 |
14 |
15 |
16 |
21 |
22 |
23 |
29 |
30 |
31 |
37 |
38 |
39 |
40 | ```
41 |
--------------------------------------------------------------------------------
/docs/svg.md:
--------------------------------------------------------------------------------
1 | svg 图片不会转换
2 |
3 | svg will not be transformed
4 |
5 | ```vue
6 |
7 |
8 |
9 |
10 |
19 | ```
20 |
--------------------------------------------------------------------------------
/docs/with-v-img-preview.md:
--------------------------------------------------------------------------------
1 | 与 v-img-preview 指令组合使用
2 |
3 | v-img 内部提供了一个未经过裁剪的 url 属性(data-uncropped-src), 因此可以向 v-img-preview 指令传递这个参数
4 |
5 | 由于 v-img 内部不提供 @femessage/img-preview 组件, 因此需要安装并且通过 Vue.use(ImgPreview), 才能使用这个指令
6 |
7 | ```vue
8 |
9 |
15 |
16 |
17 |
26 | ```
--------------------------------------------------------------------------------
/netlify.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo "is netlify: $NETLIFY"
3 | echo "in branch: $BRANCH"
4 | echo "head: $HEAD"
5 |
6 | if [ "$NETLIFY" != "true" ]
7 | then
8 | echo "this script only runs in netlify, bye"
9 | exit 1
10 | fi
11 |
12 | if [ "$BRANCH" != "dev" ] && [ "$HEAD" != "dev" ]
13 | then
14 | yarn doc
15 | else
16 | echo "this script only runs in targeting dev's PR deploy preview, bye"
17 | fi
18 |
--------------------------------------------------------------------------------
/notify.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # https://stackoverflow.com/questions/13872048/bash-script-what-does-bin-bash-mean
3 | echo "1/5: checking TRAVIS_TEST_RESULT"
4 | if [ "$TRAVIS_TEST_RESULT" != "0" ]
5 | then
6 | echo "build not success, bye"
7 | exit 1
8 | fi
9 |
10 | ORG_NAME=$(echo "$TRAVIS_REPO_SLUG" | cut -d '/' -f 1)
11 | REPO_NAME=$(echo "$TRAVIS_REPO_SLUG" | cut -d '/' -f 2)
12 |
13 | echo "2/5: pushing commit and tag to github"
14 | # 该命令很可能报错,但不影响实际进行,因而不能简单地在脚本开头 set -e
15 | git remote add github https://$GITHUB_TOKEN@github.com/$TRAVIS_REPO_SLUG.git > /dev/null 2>&1
16 | git push github HEAD:master --follow-tags
17 |
18 | echo "3/5: generating github release notes"
19 | GREN_GITHUB_TOKEN=$GITHUB_TOKEN yarn release
20 |
21 | # 避免发送错误信息
22 | if [ $? -ne 0 ]
23 | then
24 | echo "gren fails, bye"
25 | exit 1
26 | fi
27 |
28 | echo "4/5: downloading github release info"
29 | url=https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases/latest
30 | resp_tmp_file=resp.tmp
31 |
32 | curl -H "Authorization: token $GITHUB_TOKEN" $url > $resp_tmp_file
33 |
34 | html_url=$(sed -n 5p $resp_tmp_file | sed 's/\"html_url\"://g' | awk -F '"' '{print $2}')
35 | body=$(grep body < $resp_tmp_file | sed 's/\"body\"://g;s/\"//g')
36 | version=$(echo $html_url | awk -F '/' '{print $NF}')
37 |
38 | echo "5/5: notifying with dingtalk bot"
39 | msg='{"msgtype": "markdown", "markdown": {"title": "'$REPO_NAME'更新", "text": "@所有人\n# ['$REPO_NAME'('$version')]('$html_url')\n'$body'"}}'
40 |
41 | curl -X POST https://oapi.dingtalk.com/robot/send\?access_token\=$DINGTALK_ROBOT_TOKEN -H 'Content-Type: application/json' -d "$msg"
42 |
43 | rm $resp_tmp_file
44 |
45 | echo "executing notify.sh successfully"
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@femessage/v-img",
3 | "version": "1.0.0",
4 | "description": "📸 use webp and lazyload images",
5 | "author": "https://github.com/FEMessage",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/FEMessage/v-img.git"
10 | },
11 | "keywords": [
12 | "vue",
13 | "sfc",
14 | "component"
15 | ],
16 | "files": [
17 | "src",
18 | "dist"
19 | ],
20 | "main": "dist/v-img.umd.js",
21 | "module": "dist/v-img.esm.js",
22 | "unpkg": "dist/v-img.min.js",
23 | "browser": {
24 | "./sfc": "src/v-img.vue"
25 | },
26 | "types": "src/v-img.d.ts",
27 | "scripts": {
28 | "dev": "vue-styleguidist server",
29 | "test": "jest --verbose",
30 | "doc": "vue-styleguidist build",
31 | "build": "npm run build:unpkg & npm run build:es & npm run build:umd & npm run doc",
32 | "build:umd": "rollup --config build/rollup.config.js --format umd --file dist/v-img.umd.js",
33 | "build:es": "rollup --config build/rollup.config.js --format es --file dist/v-img.esm.js",
34 | "build:unpkg": "rollup --config build/rollup.config.js --format iife --file dist/v-img.min.js",
35 | "stdver": "standard-version -m '[skip ci] chore(release): v%s'",
36 | "lint": "eslint \"**/*.@(js|vue)\" --fix",
37 | "release": "gren release --override"
38 | },
39 | "dependencies": {},
40 | "devDependencies": {
41 | "@babel/core": "^7.4.3",
42 | "@babel/plugin-transform-runtime": "^7.4.3",
43 | "@babel/preset-env": "^7.4.3",
44 | "@femessage/github-release-notes": "latest",
45 | "@femessage/img-preview": "^1.4.0",
46 | "babel-eslint": "^10.0.3",
47 | "babel-loader": "^8.0.5",
48 | "eslint": "^6.8.0",
49 | "eslint-config-prettier": "^6.9.0",
50 | "eslint-plugin-jest": "^23.6.0",
51 | "eslint-plugin-prettier": "^3.1.2",
52 | "eslint-plugin-vue": "^6.1.2",
53 | "file-loader": "^3.0.1",
54 | "glob": "^7.1.3",
55 | "husky": "1.3.1",
56 | "jest": "^24.8.0",
57 | "less": "^3.9.0",
58 | "less-loader": "^5.0.0",
59 | "lint-staged": "^8.1.0",
60 | "minimist": "^1.2.0",
61 | "prettier": "1.18.2",
62 | "rollup": "^1.9.0",
63 | "rollup-plugin-babel": "^4.3.2",
64 | "rollup-plugin-commonjs": "^9.3.4",
65 | "rollup-plugin-img": "^1.1.0",
66 | "rollup-plugin-terser": "^4.0.4",
67 | "rollup-plugin-vue": "^4.7.2",
68 | "standard-version": "^6.0.1",
69 | "stylelint": "^9.10.0",
70 | "stylelint-config-standard": "^18.2.0",
71 | "vue": "^2.6.10",
72 | "vue-loader": "^15.7.1",
73 | "vue-styleguidist": "^3.16.3",
74 | "vue-template-compiler": "^2.5.16",
75 | "webpack": "^4.29.6"
76 | },
77 | "publishConfig": {
78 | "access": "public"
79 | },
80 | "vue-sfc-cli": "1.12.0",
81 | "engines": {
82 | "node": ">= 4.0.0",
83 | "npm": ">= 3.0.0"
84 | },
85 | "husky": {
86 | "hooks": {
87 | "pre-commit": "lint-staged",
88 | "post-commit": "git update-index --again",
89 | "pre-push": "yarn test"
90 | }
91 | },
92 | "lint-staged": {
93 | "*.@(md|json)": [
94 | "prettier --write",
95 | "git add"
96 | ],
97 | "*.js": [
98 | "eslint --fix",
99 | "prettier --write",
100 | "git add"
101 | ],
102 | "*.vue": [
103 | "eslint --fix",
104 | "prettier --write",
105 | "stylelint --fix",
106 | "git add"
107 | ]
108 | },
109 | "gren": "@femessage/grenrc"
110 | }
111 |
--------------------------------------------------------------------------------
/src/compute-layout.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by levy on 2021/1/13.
3 | */
4 | function isPixel(dimension) {
5 | return ('' + dimension).indexOf('px') > -1
6 | }
7 |
8 | export default function(vm) {
9 | let width, height
10 | if (Number.isInteger(vm.width) || isPixel(vm.width))
11 | width = parseInt(vm.width)
12 | if (Number.isInteger(vm.height) || isPixel(vm.height))
13 | height = parseInt(vm.height)
14 |
15 | if (isNaN(width) && vm.$el) {
16 | let styleWidth = window.getComputedStyle(vm.$el).width
17 | if (isPixel(styleWidth)) width = parseInt(styleWidth)
18 | }
19 | if (isNaN(height) && vm.$el) {
20 | let styleHeight = window.getComputedStyle(vm.$el).height
21 | if (isPixel(styleHeight)) height = parseInt(styleHeight)
22 | }
23 |
24 | // TODO 这里假设 auto 没有返回数值,待验证
25 | if ((isNaN(width) && isNaN(height)) || (!width && !height)) return {}
26 |
27 | let naturalWidth = vm.$el.naturalWidth,
28 | naturalHeight = vm.$el.naturalHeight
29 |
30 | if (width > naturalWidth) width = naturalWidth
31 | if (height > naturalHeight) height = naturalHeight
32 |
33 | // getComputedStyle 有可能会返回 natural[Width/Height],这使得其返回值不可直接使用,还需要最后 resize 一下。
34 | let scaleWidth = (height * vm.$el.naturalWidth) / vm.$el.naturalHeight
35 | let scaleHeight = (width * vm.$el.naturalHeight) / vm.$el.naturalWidth
36 |
37 | if (!scaleWidth || width * scaleHeight <= height * scaleWidth)
38 | return {width, height: scaleHeight}
39 | else if (!scaleHeight || scaleWidth * height <= scaleHeight * width)
40 | return {width: scaleWidth, height}
41 | return {}
42 | }
43 |
--------------------------------------------------------------------------------
/src/directive.js:
--------------------------------------------------------------------------------
1 | import getImageSrc from './provider-config'
2 | import ua from './ua'
3 |
4 | function getSrc(config) {
5 | const {
6 | provider = 'alibaba',
7 | extraQuery,
8 | src,
9 | width,
10 | height,
11 | autocrop = true,
12 | preferHttps = true,
13 | webp = true,
14 | } = config
15 | if (!src) return
16 |
17 | let isSupportWebp = false
18 | if (webp) {
19 | // TODO only simply check in sync way
20 | isSupportWebp =
21 | ua.isSupportWebp(navigator.userAgent) ||
22 | JSON.parse(localStorage.getItem('isSupportWebp')) ||
23 | false
24 | }
25 |
26 | return getImageSrc({
27 | autocrop,
28 | provider,
29 | src,
30 | isSupportWebp,
31 | extraQuery,
32 | width,
33 | height,
34 | preferHttps,
35 | }).$src
36 | }
37 |
38 | export default {
39 | init(el, {value = {}}) {
40 | const size = {
41 | width: el.offsetWidth,
42 | height: el.offsetHeight,
43 | }
44 | const src = getSrc({...size, ...value})
45 | el.classList.add('lazyload')
46 | el.setAttribute('data-bgset', src)
47 | },
48 |
49 | update(el, {value = {}}) {
50 | const src = getSrc(value)
51 | el.style.backgroundImage = `url(${src})`
52 | },
53 | }
54 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // Import vue component
2 | import Component from './v-img.vue'
3 | import background from './directive'
4 | import placeholder from './spinner.svg'
5 | import reload from './reload.png'
6 | import loadScript from './load-script'
7 |
8 | const defaultOptions = {
9 | placeholder,
10 | error: reload,
11 | }
12 |
13 | const lazysizes = 'https://unpkg.com/lazysizes/lazysizes.min.js'
14 | const bgset =
15 | 'https://unpkg.com/lazysizes/plugins/bgset/ls.bgset.min.js'
16 |
17 | // `Vue.use` automatically prevents you from using
18 | // the same plugin more than once,
19 | // so calling it multiple times on the same plugin
20 | // will install the plugin only once
21 | Component.install = (Vue, options = {}) => {
22 | Vue.prototype.$vImg = {...defaultOptions, ...options}
23 |
24 | if (typeof window !== 'undefined' && !window.lazySizes) {
25 | // https://github.com/aFarkas/lazysizes/tree/gh-pages/plugins/bgset
26 | Promise.all([
27 | loadScript({name: 'bgset', url: bgset}),
28 | loadScript({name: 'lazysizes', url: lazysizes}),
29 | ]).catch(console.error)
30 | }
31 |
32 | Vue.component(Component.name, Component)
33 |
34 | Vue.directive('img', {
35 | inserted: background.init,
36 | update: background.update,
37 | })
38 | }
39 |
40 | // To auto-install when vue is found
41 | let GlobalVue = null
42 | if (typeof window !== 'undefined') {
43 | GlobalVue = window.Vue
44 | } else if (typeof global !== 'undefined') {
45 | GlobalVue = global.Vue
46 | }
47 | if (GlobalVue) {
48 | GlobalVue.use(Component)
49 | }
50 |
51 | // To allow use as module (npm/webpack/etc.) export component
52 | export default Component
53 |
54 | // It's possible to expose named exports when writing components that can
55 | // also be used as directives, etc. - eg. import { RollupDemoDirective } from 'rollup-demo';
56 | // export const RollupDemoDirective = component;
57 |
--------------------------------------------------------------------------------
/src/load-script.js:
--------------------------------------------------------------------------------
1 | export default function({name, url}) {
2 | return new Promise((resolve, reject) => {
3 | if (!url) {
4 | reject(new Error('to load script, url cannot be null'))
5 | }
6 | if (document.getElementById(name)) {
7 | resolve(true)
8 | } else {
9 | const node = document.getElementsByTagName('script')[0],
10 | script = document.createElement('script')
11 | script.setAttribute('id', name)
12 | script.setAttribute('src', url)
13 |
14 | script.onload = function() {
15 | resolve(true)
16 | }
17 | script.onerror = function(err) {
18 | script.remove()
19 | reject(err)
20 | }
21 | node.parentNode.insertBefore(script, null)
22 | }
23 | })
24 | }
25 |
--------------------------------------------------------------------------------
/src/provider-config.js:
--------------------------------------------------------------------------------
1 | const png = /\.png(\?.*)?$/
2 | const jpg = /\.jpe?g(\?.*)?$/
3 | const webp = /\.webp(\?.*)?$/
4 | const svg = /\.svg(\?.*)?$/
5 | /**
6 | * https://helpx.adobe.com/hk_en/experience-manager/6-3/assets/using/best-practices-for-optimizing-the-quality-of-your-images.html
7 | * https://sirv.com/help/articles/jpeg-quality-comparison/#use-sirv-to-find-the-perfect-quality
8 | */
9 | export const quality = 80
10 |
11 | function is(types, src) {
12 | return Array.isArray(types) ? types.some(t => t.test(src)) : types.test(src)
13 | }
14 |
15 | const srcProcess = {
16 | CONVERT_WEBP: 'convertWebp',
17 | CROP_IMAGE: 'cropImage',
18 | APPEND_QUERY: 'appendQuery',
19 | }
20 |
21 | const pipe = function(fns) {
22 | return function(item) {
23 | return fns.reduce(function(prev, fn) {
24 | if (typeof fn !== 'function') {
25 | fn = v => v
26 | }
27 | return fn(prev)
28 | }, item)
29 | }
30 | }
31 |
32 | /**
33 | * //img-url => https://img-url
34 | * origin 参数纯粹是为了警告
35 | * @param url
36 | */
37 | function preferHttps(url, origin) {
38 | if (url.startsWith('//')) {
39 | return 'https:' + url
40 | } else if (!url.startsWith('https')) {
41 | console.warn(
42 | 'preferHttps is true, but this img is using http protocol: ' +
43 | (origin || url)
44 | )
45 | }
46 | return url
47 | }
48 |
49 | export const providerConfig = {
50 | alibaba: {
51 | [srcProcess.CONVERT_WEBP](vm) {
52 | const {src, isSupportWebp} = vm
53 | if (!src) return vm
54 |
55 | let query = ''
56 | if (isSupportWebp && is([png, jpg], src)) query += '/format,webp'
57 | /**
58 | * 质量变换仅对jpg、webp有效。(png已被转为webp)
59 | * @see https://help.aliyun.com/document_detail/44705.html?spm=a2c4g.11186623.6.1256.347d69cb9tB4ZR
60 | */
61 | if (is([png, jpg, webp], src)) query += `/quality,Q_${quality}`
62 |
63 | vm.$src = query
64 | return vm
65 | },
66 | // https://help.aliyun.com/document_detail/183902.html?spm=a2c4g.11186623.2.12.738828fbVGaPAf#section-tx1-qtj-ar8
67 | [srcProcess.CROP_IMAGE](vm) {
68 | const {$src = '', width, height, autocrop, src} = vm
69 |
70 | if (!autocrop || is(svg, src) || !src) return vm
71 | if (isNaN(width) && isNaN(height)) return vm
72 |
73 | const DPR = 2
74 | let dpr =
75 | (typeof window !== 'undefined' && window.devicePixelRatio) || DPR
76 | if (dpr === 1) {
77 | dpr = DPR
78 | }
79 | const actions = ['/resize']
80 | const WIDTH = `w_${parseInt(width * dpr)}`
81 | const HEIGHT = `h_${parseInt(height * dpr)}`
82 | const AUTOCROP = `m_fill`
83 |
84 | if (!isNaN(width) && !isNaN(height)) {
85 | actions.push(AUTOCROP)
86 | }
87 |
88 | if (!isNaN(height)) {
89 | actions.push(HEIGHT)
90 | }
91 |
92 | if (!isNaN(width)) {
93 | actions.push(WIDTH)
94 | }
95 |
96 | const resizeQuery = actions.join(',')
97 |
98 | vm.$src = resizeQuery + $src
99 | vm.$resizeQuery = resizeQuery
100 |
101 | return vm
102 | },
103 |
104 | [srcProcess.APPEND_QUERY](vm) {
105 | const {src, extraQuery} = vm
106 | // null 无法通过解构设置默认值的方式改变值,依然会是 null,不如直接返回
107 | if (!src) return vm
108 |
109 | let query = vm.$src || ''
110 | if (extraQuery) query += '/' + extraQuery
111 | if (query) {
112 | query =
113 | src +
114 | (src.indexOf('?') > -1 ? '&' : '?') +
115 | 'x-oss-process=image' +
116 | query
117 | }
118 |
119 | vm.$src = query || src
120 | return vm
121 | },
122 | },
123 | qiniu: {
124 | [srcProcess.CONVERT_WEBP](vm) {
125 | const {src, isSupportWebp} = vm
126 | if (!src) return vm
127 |
128 | let query = vm.$src || ''
129 | // imageMogr2 接口可支持处理的原图片格式有 psd、jpeg、png、gif、webp、tiff、bmp
130 | if (is(svg, src)) {
131 | return vm
132 | }
133 | if (isSupportWebp && is([png, jpg], src)) query += '/format/webp'
134 | query += `/quality/${quality}`
135 |
136 | vm.$src = query
137 | return vm
138 | },
139 |
140 | [srcProcess.APPEND_QUERY](vm) {
141 | const {src, extraQuery} = vm
142 | if (!src) return vm
143 |
144 | let query = vm.$src || ''
145 | if (extraQuery) query += '/' + extraQuery
146 | if (query) {
147 | query = src + (src.indexOf('?') > -1 ? '&' : '?') + 'imageMogr2' + query
148 | }
149 |
150 | vm.$src = query || src
151 | return vm
152 | },
153 | },
154 | self: {
155 | [srcProcess.CONVERT_WEBP](vm) {
156 | const {src, isSupportWebp} = vm
157 | if (!src) return vm
158 |
159 | if (isSupportWebp && is([png, jpg], src)) {
160 | vm.$src =
161 | src.indexOf('?') > -1 ? src.replace('?', '.webp?') : src + '.webp'
162 | } else {
163 | vm.$src = src
164 | }
165 | return vm
166 | },
167 | },
168 | none: {
169 | [srcProcess.CONVERT_WEBP](vm) {
170 | vm.$src = vm.src || ''
171 | return vm
172 | },
173 | },
174 | }
175 |
176 | /**
177 | * 传入配置,根据 src 注入 $src,在 v-img 组件中使用的是 $src
178 | * 但在 APPEND_QUERY 步骤之前,其实 $src 代表的是 query
179 | * @param vm
180 | * @returns 注入了 $src 的 vm
181 | */
182 | export default vm => {
183 | vm.$src = ''
184 | const providerPipe = providerConfig[vm.provider]
185 | const output = pipe([
186 | providerPipe[srcProcess.CONVERT_WEBP],
187 | providerPipe[srcProcess.CROP_IMAGE],
188 | providerPipe[srcProcess.APPEND_QUERY],
189 | ])(vm)
190 | vm.$uncroppedSrc = vm.$src.replace(vm.$resizeQuery, '')
191 |
192 | if (vm.preferHttps && ['self', 'none'].indexOf(vm.provider) == -1) {
193 | vm.$src = preferHttps(vm.$src, vm.src)
194 | }
195 |
196 | return output
197 | }
198 |
--------------------------------------------------------------------------------
/src/reload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FEMessage/v-img/2f1fa01a15565d39a3ccb8296ae4e273907aba5d/src/reload.png
--------------------------------------------------------------------------------
/src/spinner.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/src/ua.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by levy on 2020/12/25.
3 | * https://developers.google.com/speed/webp/faq#how_can_i_detect_browser_support_for_webp
4 | * https://textslashplain.com/2019/05/01/edge-76-vs-edge-18-vs-chrome/
5 | * https://en.wikipedia.org/wiki/History_of_the_Opera_web_browser#Opera_2013
6 | * simple logic: if (chrome >= 79 || firefox >= 65) supports webp
7 | * it is including edge >= 79 && opera >= 66
8 | *
9 | * for more powerful user agent detection, please use: https://github.com/faisalman/ua-parser-js
10 | */
11 | const regex = /(firefox|chrome(?=\/))\/?\s*(\d+)/i
12 | export const minChromeVersion = 79
13 | export const minFirefoxVersion = 65
14 | export const Chrome = 'Chrome'
15 | export const Firefox = 'Firefox'
16 |
17 | function getBrowser(ua) {
18 | const matches = regex.exec(ua) || []
19 | // eslint-disable-next-line no-unused-vars
20 | const [str, browser, version] = matches
21 | return {
22 | browser,
23 | version: parseInt(version)
24 | }
25 | }
26 |
27 | function isSupportWebp(ua) {
28 | const {browser, version} = getBrowser(ua)
29 | return (
30 | (browser === Chrome && version >= minChromeVersion) ||
31 | (browser === Firefox && version >= minFirefoxVersion)
32 | )
33 | }
34 |
35 | export default {
36 | isSupportWebp,
37 | getBrowser
38 | }
39 |
--------------------------------------------------------------------------------
/src/v-img.d.ts:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | declare module '@femessage/v-img' {
4 | class FemessageComponent extends Vue {
5 | static install(vue: typeof Vue): void
6 | }
7 |
8 | type CombinedVueInstance<
9 | Instance extends Vue,
10 | Data,
11 | Methods,
12 | Computed,
13 | Props
14 | > = Data & Methods & Computed & Props & Instance
15 |
16 | type ExtendedVue<
17 | Instance extends Vue,
18 | Data,
19 | Methods,
20 | Computed,
21 | Props
22 | > = VueConstructor<
23 | CombinedVueInstance & Vue
24 | >
25 |
26 | type Combined = Data &
27 | Methods &
28 | Computed &
29 | Props
30 |
31 | type VImgData = {
32 | isSupportWebp: boolean | null
33 | status: number
34 | transparentImg: string
35 | }
36 |
37 | type VImgMethods = {
38 | checkLayout: () => void
39 |
40 | checkSupportWebp: () => Promise
41 |
42 | forceUpdateSrc: () => void
43 |
44 | onLoad: () => void
45 |
46 | onError: () => void
47 |
48 | onClick: () => void
49 | }
50 |
51 | type VImgComputed = {
52 | classname: string
53 |
54 | style: {[key: string]: any}
55 |
56 | imageSrc: any
57 |
58 | loadingImage: string
59 |
60 | reloadImage: string
61 | }
62 |
63 | type VImgProps = {
64 | src: string
65 |
66 | width: string | number
67 |
68 | height: string | number
69 |
70 | hasLoading: boolean
71 |
72 | provider: string
73 |
74 | extraQuery: string
75 |
76 | placeholder: string
77 |
78 | error: string
79 |
80 | autocrop: string
81 | }
82 |
83 | type VImg = Combined
84 |
85 | export interface VImgType extends FemessageComponent, VImg {}
86 |
87 | const VImgConstruction: ExtendedVue<
88 | Vue,
89 | VImgData,
90 | VImgMethods,
91 | VImgComputed,
92 | VImgProps
93 | >
94 |
95 | export default VImgConstruction
96 | }
97 |
--------------------------------------------------------------------------------
/src/v-img.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
241 |
--------------------------------------------------------------------------------
/styleguide.config.js:
--------------------------------------------------------------------------------
1 | const {VueLoaderPlugin} = require('vue-loader')
2 | const path = require('path')
3 | const glob = require('glob')
4 |
5 | const sections = (() => {
6 | const docs = glob
7 | .sync('docs/*.md')
8 | .map(p => ({name: path.basename(p, '.md'), content: p}))
9 | const demos = []
10 | let faq = '' // 约定至多只有一个faq.md
11 | const guides = []
12 | docs.forEach(d => {
13 | if (/^faq$/i.test(d.name)) {
14 | d.name = d.name.toUpperCase()
15 | faq = d
16 | } else if (/^guide-/.test(d.name)) {
17 | guides.push(d)
18 | } else {
19 | demos.push(d)
20 | }
21 | })
22 | return [
23 | {
24 | name: 'Components',
25 | components: 'src/*.vue',
26 | usageMode: 'expand'
27 | },
28 | {
29 | name: 'Demo',
30 | sections: demos,
31 | sectionDepth: 2
32 | },
33 | ...(faq ? [faq] : []),
34 | ...(guides.length
35 | ? [{name: 'Guide', sections: guides, sectionDepth: 2}]
36 | : [])
37 | ]
38 | })()
39 |
40 | module.exports = {
41 | require: ['./styleguide/register.js'],
42 | styleguideDir: 'docs',
43 | pagePerSection: true,
44 | ribbon: {
45 | url: 'https://github.com/FEMessage/v-img'
46 | },
47 | sections,
48 | webpackConfig: {
49 | module: {
50 | rules: [
51 | {
52 | test: /\.vue$/,
53 | loader: 'vue-loader'
54 | },
55 | {
56 | test: /\.js?$/,
57 | exclude: /node_modules/,
58 | loader: 'babel-loader'
59 | },
60 | {
61 | test: /\.css$/,
62 | loaders: ['style-loader', 'css-loader']
63 | },
64 | {
65 | test: /\.less$/,
66 | loaders: ['vue-style-loader', 'css-loader', 'less-loader']
67 | },
68 | {
69 | test: /\.(svg|png)$/,
70 | loader: 'file-loader'
71 | }
72 | ]
73 | },
74 | plugins: [new VueLoaderPlugin()]
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/styleguide/register.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import ImgPreview from '@femessage/img-preview'
3 | import VImg from '../src'
4 |
5 | Vue.use(ImgPreview)
6 | Vue.use(VImg)
7 |
--------------------------------------------------------------------------------
/test/compute-layout.spec.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by levy on 2021/1/13.
3 | */
4 | import computeLayout from '../src/compute-layout'
5 | describe('compute img layout', () => {
6 | let vm
7 | let expected
8 | let natural
9 |
10 | beforeEach(() => {
11 | vm = {
12 | width: '', // 理想值是数字
13 | height: '', // 理想值是数字
14 | $el: {
15 | naturalWidth: 200,
16 | naturalHeight: 200,
17 | style: {
18 | width: '', // 可能是 auto, 可能是 100%,可能是 300px 或其他单位。。。
19 | height: '',
20 | },
21 | },
22 | }
23 | expected = {width: 100, height: 100}
24 | natural = {width: 200, height: 200}
25 | })
26 |
27 | // https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle#notes
28 | // width, height, they are the same as used values
29 | Object.defineProperty(window, 'getComputedStyle', {
30 | value: jest.fn(el => {
31 | let width = el.style.width || el.naturalWidth
32 | let height = el.style.height || el.naturalHeight
33 |
34 | return {
35 | width: Number.isInteger(width) ? width + 'px' : width,
36 | height: Number.isInteger(height) ? height + 'px' : height,
37 | }
38 | }),
39 | })
40 |
41 | // 此时优先使用 width
42 | test('width/height 有数值, style.width/style.height 有px值', () => {
43 | vm.width = 100
44 | vm.height = 100
45 | vm.$el.style.width = '50px'
46 | vm.$el.style.height = '50px'
47 | expect(computeLayout(vm)).toStrictEqual(expected)
48 | })
49 |
50 | test('width/height 有数值', () => {
51 | vm.width = 100
52 | vm.height = 100
53 | expect(computeLayout(vm)).toStrictEqual(expected)
54 |
55 | // 兼容处理不正确的输入值
56 | vm.width = '100px'
57 | expect(computeLayout(vm)).toStrictEqual(expected)
58 | })
59 |
60 | test('width/height 无数值,style.width/style.height 有px值', () => {
61 | vm.$el.style.width = '100px'
62 | vm.$el.style.height = '100px'
63 | expect(computeLayout(vm)).toStrictEqual(expected)
64 | })
65 |
66 | test('width 有数值,height/style.height 无值, height 自动计算', () => {
67 | vm.width = 100
68 | expect(computeLayout(vm)).toStrictEqual(expected)
69 | })
70 |
71 | test('style.width 有px值,height/style.height 无值, height 自动计算', () => {
72 | vm.$el.style.width = '100px'
73 | expect(computeLayout(vm)).toStrictEqual(expected)
74 | })
75 |
76 | test('只有 naturalWidth/naturalHeight', () => {
77 | vm.$el.naturalWidth = 100
78 | vm.$el.naturalHeight = 100
79 | expect(computeLayout(vm)).toStrictEqual(expected)
80 | })
81 |
82 | test('width/height 不是纯数字, 暂不支持', () => {
83 | vm.width = '100%'
84 | vm.height = 'auto'
85 | expect(computeLayout(vm)).toStrictEqual(natural)
86 | })
87 |
88 | test('style.width/style.height 不是px,暂不支持', () => {
89 | vm.$el.style.width = 'auto'
90 | vm.$el.style.height = '100%'
91 | expect(computeLayout(vm)).toStrictEqual({})
92 | })
93 |
94 | test('width/height 设置为 0, 忽略不计', () => {
95 | vm.width = 0
96 | vm.height = 0
97 | expect(computeLayout(vm)).toStrictEqual({})
98 |
99 | vm.width = ''
100 | vm.height = ''
101 | vm.$el.style.width = '0'
102 | vm.$el.style.height = '0'
103 | expect(computeLayout(vm)).toStrictEqual({})
104 | })
105 |
106 | test('设置了不恰当比例的宽高,要防止变形', () => {
107 | vm.$el.style.width = '100px'
108 | vm.$el.style.height = '80px'
109 | expect(computeLayout(vm)).toStrictEqual({width: 80, height: 80})
110 |
111 | vm.width = 100
112 | vm.height = 150
113 | expect(computeLayout(vm)).toStrictEqual(expected)
114 |
115 | vm.width = 300
116 | vm.height = 300
117 | expect(computeLayout(vm)).toStrictEqual(natural)
118 |
119 | vm.width = 100
120 | expect(computeLayout(vm)).toStrictEqual(expected)
121 | })
122 | })
123 |
--------------------------------------------------------------------------------
/test/provider-config.spec.js:
--------------------------------------------------------------------------------
1 | import _getSrc, {quality} from '../src/provider-config'
2 |
3 | const getSrc = val => _getSrc(val).$src
4 | const getUncroppedSrc = val => _getSrc(val).$uncroppedSrc
5 |
6 | describe('alibaba', () => {
7 | const defaultQuery = `?x-oss-process=image/quality,Q_${quality}`
8 | const src = 'http://image-demo.oss-cn-hangzhou.aliyuncs.com/panda.png'
9 | const relative = '//image-demo.oss-cn-hangzhou.aliyuncs.com/panda.png'
10 | const https = 'https://image-demo.oss-cn-hangzhou.aliyuncs.com/panda.png'
11 |
12 | test('preferHttps', () => {
13 | const defaultConfig = {
14 | provider: 'alibaba',
15 | preferHttps: true,
16 | }
17 |
18 | expect(
19 | getSrc({
20 | src: relative,
21 | ...defaultConfig,
22 | })
23 | ).toBe(https + defaultQuery)
24 |
25 | expect(
26 | getSrc({
27 | src: relative,
28 | ...defaultConfig,
29 | preferHttps: false,
30 | })
31 | ).toBe(relative + defaultQuery)
32 |
33 | expect(
34 | getSrc({
35 | src: relative,
36 | ...defaultConfig,
37 | provider: 'none',
38 | })
39 | ).toBe(relative)
40 |
41 | expect(
42 | getSrc({
43 | src,
44 | ...defaultConfig,
45 | })
46 | ).toBe(src + defaultQuery)
47 |
48 | expect(
49 | getSrc({
50 | src: https,
51 | ...defaultConfig,
52 | })
53 | ).toBe(https + defaultQuery)
54 | })
55 |
56 | test('浏览器支持webp', () => {
57 | expect(
58 | getSrc({
59 | provider: 'alibaba',
60 | src,
61 | isSupportWebp: true,
62 | })
63 | ).toBe(`${src}?x-oss-process=image/format,webp/quality,Q_${quality}`)
64 | })
65 | test('浏览器不支持webp', () => {
66 | expect(
67 | getSrc({
68 | provider: 'alibaba',
69 | src,
70 | isSupportWebp: false,
71 | })
72 | ).toBe(`${src}${defaultQuery}`)
73 | })
74 | test('浏览器支持webp,但图片不是(png|jpe?g)', () => {
75 | const webp = src.replace('png', 'webp')
76 | expect(
77 | getSrc({
78 | provider: 'alibaba',
79 | src: webp,
80 | isSupportWebp: true,
81 | })
82 | ).toBe(`${webp}${defaultQuery}`)
83 | })
84 | test('svg不处理,除非有extraQuery', () => {
85 | const svg = src.replace('png', 'svg')
86 | const extraQuery = 'rotate,10'
87 | expect(getSrc({provider: 'alibaba', src: svg})).toBe(svg)
88 | expect(
89 | getSrc({
90 | provider: 'alibaba',
91 | src: svg,
92 | extraQuery,
93 | })
94 | ).toBe(`${svg}?x-oss-process=image/${extraQuery}`)
95 | })
96 | test('带extraQuery的情况', () => {
97 | const extraQuery = 'rotate,10'
98 | expect(
99 | getSrc({
100 | provider: 'alibaba',
101 | src,
102 | isSupportWebp: true,
103 | extraQuery,
104 | })
105 | ).toBe(
106 | `${src}?x-oss-process=image/format,webp/quality,Q_${quality}/${extraQuery}`
107 | )
108 | })
109 |
110 | test('自动裁剪,只传 width', () => {
111 | expect(
112 | getSrc({
113 | provider: 'alibaba',
114 | src,
115 | isSupportWebp: true,
116 | autocrop: true,
117 | width: 100,
118 | })
119 | ).toBe(
120 | `${src}?x-oss-process=image/resize,w_200/format,webp/quality,Q_${quality}`
121 | )
122 | })
123 | test('自动裁剪,只传 height', () => {
124 | expect(
125 | getSrc({
126 | provider: 'alibaba',
127 | src,
128 | isSupportWebp: true,
129 | autocrop: true,
130 | height: 100,
131 | })
132 | ).toBe(
133 | `${src}?x-oss-process=image/resize,h_200/format,webp/quality,Q_${quality}`
134 | )
135 | })
136 | test('自动裁剪,height 和 width 都传', () => {
137 | expect(
138 | getSrc({
139 | provider: 'alibaba',
140 | src,
141 | isSupportWebp: true,
142 | autocrop: true,
143 | height: 100,
144 | width: 100,
145 | })
146 | ).toBe(
147 | `${src}?x-oss-process=image/resize,m_fill,h_200,w_200/format,webp/quality,Q_${quality}`
148 | )
149 | })
150 | test('自动裁剪,height 和 width 都不传', () => {
151 | expect(
152 | getSrc({
153 | provider: 'alibaba',
154 | src,
155 | isSupportWebp: true,
156 | autocrop: true,
157 | })
158 | ).toBe(`${src}?x-oss-process=image/format,webp/quality,Q_${quality}`)
159 | })
160 |
161 | test('获取不经过裁剪的 url', () => {
162 | expect(
163 | getUncroppedSrc({
164 | provider: 'alibaba',
165 | src,
166 | isSupportWebp: true,
167 | autocrop: true,
168 | height: 100,
169 | width: 100,
170 | })
171 | ).toBe(`${src}?x-oss-process=image/format,webp/quality,Q_${quality}`)
172 | })
173 |
174 | test('测试 src 为空', () => {
175 | expect(
176 | getSrc({
177 | provider: 'alibaba',
178 | src: '',
179 | })
180 | ).toBe('')
181 |
182 | expect(
183 | getSrc({
184 | provider: 'alibaba',
185 | src: null,
186 | })
187 | ).toBe('')
188 | })
189 | })
190 |
191 | describe('qiniu', () => {
192 | const src = 'https://odum9helk.qnssl.com/resource/gogopher.jpg'
193 | test('浏览器支持webp', () => {
194 | expect(
195 | getSrc({
196 | provider: 'qiniu',
197 | src,
198 | isSupportWebp: true,
199 | })
200 | ).toBe(`${src}?imageMogr2/format/webp/quality/${quality}`)
201 | })
202 | test('浏览器不支持webp', () => {
203 | expect(
204 | getSrc({
205 | provider: 'qiniu',
206 | src,
207 | isSupportWebp: false,
208 | })
209 | ).toBe(`${src}?imageMogr2/quality/${quality}`)
210 | })
211 | test('浏览器支持webp,但图片不是(png|jpe?g)', () => {
212 | const webp = src.replace('jpg', 'webp')
213 | expect(
214 | getSrc({
215 | provider: 'qiniu',
216 | src: webp,
217 | isSupportWebp: true,
218 | })
219 | ).toBe(`${webp}?imageMogr2/quality/${quality}`)
220 | })
221 | test('svg不处理', () => {
222 | const svg = src.replace('jpg', 'svg')
223 | expect(getSrc({provider: 'qiniu', src: svg})).toBe(svg)
224 | })
225 | test('带extraQuery的情况', () => {
226 | const extraQuery = 'rotate/10'
227 | expect(
228 | getSrc({
229 | provider: 'qiniu',
230 | src,
231 | isSupportWebp: true,
232 | extraQuery,
233 | })
234 | ).toBe(`${src}?imageMogr2/format/webp/quality/${quality}/${extraQuery}`)
235 | })
236 | })
237 |
238 | describe('self', () => {
239 | test('精确的资源路径', () => {
240 | const src = 'https://cumming.com/creampie.png'
241 | expect(
242 | getSrc({
243 | provider: 'self',
244 | src,
245 | isSupportWebp: true,
246 | })
247 | ).toBe(`${src}.webp`)
248 | })
249 |
250 | test('带参数的资源路径', () => {
251 | const src = 'https://cumming.com/creampie.png'
252 | const query = '?format/jpeg'
253 | expect(
254 | getSrc({
255 | provider: 'self',
256 | src: src + query,
257 | isSupportWebp: true,
258 | })
259 | ).toBe(`${src}.webp${query}`)
260 | })
261 |
262 | test('资源路径存在相同后缀名', () => {
263 | const src = 'https://cumming.com/creampie.png.creampie.png'
264 | expect(
265 | getSrc({
266 | provider: 'self',
267 | src,
268 | isSupportWebp: true,
269 | })
270 | ).toBe(`${src}.webp`)
271 | })
272 |
273 | test('使用本地资源路径', () => {
274 | const src = './com/creampie.png.creampie.png'
275 | expect(
276 | getSrc({
277 | provider: 'self',
278 | src,
279 | isSupportWebp: true,
280 | })
281 | ).toBe(`${src}.webp`)
282 | })
283 |
284 | test('默认情况下不转换svg', () => {
285 | const src = './com/creampie.png.creampie.svg'
286 | expect(
287 | getSrc({
288 | provider: 'self',
289 | src,
290 | isSupportWebp: true,
291 | })
292 | ).toBe(src)
293 | })
294 | })
295 |
--------------------------------------------------------------------------------
/test/ua.spec.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by levy on 2020/12/25.
3 | */
4 | import ua from '../src/ua'
5 | import {minChromeVersion, minFirefoxVersion, Chrome, Firefox} from '../src/ua'
6 |
7 | describe('user-agent detection', () => {
8 | const Chrome87 =
9 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'
10 | const Edge =
11 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.74 Safari/537.36 Edg/79.0.309.43'
12 | const Opera =
13 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36 OPR/70.0.3728.178 (Edition Baidu)'
14 | const Firefox51 =
15 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:51.0) Gecko/20100101 Firefox/51.0'
16 | const Android =
17 | 'Mozilla/5.0 (Linux; Android 4.4.4; HUAWEI H891L Build/HuaweiH891L) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36'
18 | const iPhone =
19 | 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'
20 | const Safari =
21 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8'
22 |
23 | test('unknown user agent so assumes it do not support webp', () => {
24 | expect(ua.isSupportWebp(iPhone)).toBe(false)
25 | expect(ua.isSupportWebp(Safari)).toBe(false)
26 | })
27 |
28 | test('isChrome but not support webp', () => {
29 | expect(ua.getBrowser(Android).browser).toBe(Chrome)
30 | expect(ua.getBrowser(Android).version).toBeLessThan(minChromeVersion)
31 | expect(ua.isSupportWebp(Android)).toBe(false)
32 | })
33 |
34 | test('isChrome and support webp', () => {
35 | expect(ua.getBrowser(Chrome87).browser).toBe(Chrome)
36 | expect(ua.getBrowser(Chrome87).version).toBeGreaterThanOrEqual(
37 | minChromeVersion
38 | )
39 | expect(ua.isSupportWebp(Chrome87)).toBe(true)
40 | expect(ua.isSupportWebp(Edge)).toBe(true)
41 | expect(ua.isSupportWebp(Opera)).toBe(true)
42 | })
43 |
44 | test('isFirefox but not support webp', () => {
45 | expect(ua.getBrowser(Firefox51).browser).toBe(Firefox)
46 | expect(ua.getBrowser(Firefox51).version).toBeLessThan(minFirefoxVersion)
47 | expect(ua.isSupportWebp(Firefox51)).toBe(false)
48 | })
49 | })
50 |
--------------------------------------------------------------------------------