Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions apps/vite-vue3-project/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,29 +1,52 @@
<script setup lang="ts">
import Picture from "@kwai-explore/picture.vue";
import examplePic from "./assets/example.jpg?preset=modern";
import examplePicPng from "./assets/eve-bg-0108.png?preset=modern";
import { ref } from "vue";

const src = ref(examplePicPng);

function onLoad(...args: any[]) {
console.log("load", ...args);
}
function onClick(e: Event) {
console.log(e);
src.value = examplePic; // 点击切换图片
}
</script>

<template>
<div>vue3 demo</div>
<Picture
:src="examplePic"
placeholder="color"
class="picture"
@load="onLoad"
@click="onClick"
/>
<div>vue3 Picture demo</div>
<!--
root-attrs 会被加到组件根元素 picture 元素
而直接加的props,全部会被透传到 img 元素上
-->
<div class="box">
<Picture
:src
placeholder="color"
@load="onLoad"
@click="onClick"
:root-attrs="{
class: 'diy-picture',
id: 'diy-picture',
}"
class="inner-img-class"
id="inner-img-id"
/>
</div>
</template>

<style scoped>
.picture {
border: 5px solid red;
<style scoped lang="scss">
/* .box {
width: 200px;
height: 100px;
} */
.diy-picture {
border: 4px solid yellow;
}
:deep(.inner-img-class) {
border: 4px solid red;
width: 200px;
height: 100px;
overflow: hidden;
Expand Down
Binary file added apps/vite-vue3-project/src/assets/eve-bg-0108.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 57 additions & 4 deletions packages/picture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,9 @@ type PictureProp = {

## 样式问题

组件内部是<picture><img /></picture>,其中img直接继承父元素的所有css属性,这是为了避免使用:deep()才能给img设置样式
组件内部是<picture><img /></picture>,其中给Picture传入的Props会被透传到 img 元素上,但是在 `<style scoped>` 中设置的样式会被vue添加的hash给拦截,无法对img生效

使用时通过class设置样式时,会同时对这两个标签生效,组件内部已经对background、border、margin、padding等样式进行了过滤。

如果使用其他属性时,样式出现了和预期不符合的问题,请尝试直接使用:deep(img),或者使用行内样式(会直接透传到img),使样式只对img标签失效。
所以推荐使用 `:deep(.Picture组件class名) {}` 添加样式,如果有需要为picture标签添加属性的需求,可以使用 rootAttrs props 传入。具体可以看demo。


### 建议添加 eslint 规则
Expand All @@ -111,3 +109,58 @@ type PictureProp = {
## 兼容性

vue >= 3.3

# Demo

```html
<script setup lang="ts">
import Picture from "@kwai-explore/picture.vue";
import examplePic from "./assets/example.jpg?preset=modern";

function onLoad(...args: any[]) {
console.log("load", ...args);
}
function onClick(e: Event) {
console.log(e);
}
</script>

<template>
<div>vue3 demo</div>
<!--
root-attrs 会被加到组件根元素 picture 元素
而直接加的props,全部会被透传到 img 元素上
-->
<div class="box">
<Picture
:src="examplePic"
placeholder="color"
@load="onLoad"
@click="onClick"
:root-attrs="{
class: 'diy-picture',
id: 'diy-picture',
}"
class="inner-img-class"
id="inner-img-id"
/>
</div>
</template>

<style scoped lang="scss">
/* .box {
width: 200px;
height: 100px;
} */
.diy-picture {
border: 4px solid yellow;
}
:deep(.inner-img-class) {
border: 4px solid red;
width: 200px;
height: 100px;
overflow: hidden;
object-fit: cover;
}
</style>
```
70 changes: 15 additions & 55 deletions packages/picture/src/components/Picture.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import type { PictureProp } from './Picture.vue.d.ts';

const bgColors = ['#A7D2CB', '#874C62', '#C98474', '#F2D388'];
Expand All @@ -14,6 +14,11 @@ const props = withDefaults(defineProps<PictureProp>(), {
const emit = defineEmits<{ (event: 'load', ev: Event): void }>();
defineOptions({ inheritAttrs: false });

// ios设备上,会同时加载最佳兼容和兜底图
const isSafari = /iphone|ipad|ipod/i.test(
navigator?.userAgent.toLowerCase() || '',
);

/** 插件会生成多种格式的图片,放入source中,picture标签会选择最优图像显示 */
const sources = computed<{ srcset?: string; type?: string }[]>(() =>
'fallback' in props.src
Expand All @@ -28,62 +33,27 @@ const sources = computed<{ srcset?: string; type?: string }[]>(() =>
);

/** 返回图片对象里面主要图片,放入img中,作为兜底图像 */
const lastSource = computed(() => {
const res = 'fallback' in props.src ? props.src.fallback : props.src.img;
assertNotNil(res);
return res;
});
const lastSource = computed(() =>
'fallback' in props.src ? props.src.fallback : props.src.img,
);

const safariSrc = ref();
const isSafari = getBrowserName() === 'Safari';
// 测试过url变化时加载的图片符合预期
const safariSrc = ref('');
onMounted(() => {
safariSrc.value = lastSource.value.src;
watch(lastSource, (value) => (safariSrc.value = value.src), {
immediate: true,
});
});

const loaded = ref(false);
function handleLoad(ev: Event) {
emit('load', ev);
loaded.value = true;
}

/**
* 获取浏览器名称
* @see https://codepedia.info/detect-browser-in-javascript
* @returns 浏览器名称
*/
function getBrowserName() {
if (typeof navigator === 'undefined') {
return 'other';
}
const agent = navigator.userAgent.toLowerCase();
switch (
true // case agent.indexOf("edge") > -1: return "MS Edge";
) {
// case agent.indexOf("edg/") > -1: return "Edge ( chromium based)";
// case agent.indexOf("opr") > -1 && !!window.opr: return "Opera";
// case agent.indexOf("chrome") > -1 && !!window.chrome!: return "Chrome";
case agent.includes('chrome'):
return 'Chrome';
// case agent.indexOf("trident") > -1: return "MS IE";
case agent.includes('firefox'):
return 'Mozilla Firefox';
case agent.includes('safari'):
return 'Safari';
default:
return 'other';
}
}

function assertNotNil<T>(v: T, message?: string): asserts v is NonNullable<T> {
if (v == null) {
throw new Error(message ?? 'Must not be null or undefined');
}
}
</script>

<template>
<picture v-bind:class="$attrs.class">
<picture v-bind="rootAttrs">
<source v-for="(attrs, index) in sources" :key="index" v-bind="attrs" />
<img
v-bind="{ ...lastSource, ...$attrs }"
Expand All @@ -95,22 +65,12 @@ function assertNotNil<T>(v: T, message?: string): asserts v is NonNullable<T> {
</picture>
</template>

<style scoped>
<style scoped lang="scss">
picture {
display: inline-block;
box-sizing: border-box;

img {
all: inherit;
/* object-fit: cover; */
/* 不继承的属性 */
vertical-align: top;
width: 100%;
height: 100%;
background: none;
border: none;
margin: 0;
padding: 0;
}

.placeholder-player {
Expand Down
19 changes: 11 additions & 8 deletions packages/picture/src/components/Picture.vue.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import type { ImgHTMLAttributes } from 'vue';

/** Picture Props 类型 */
export type PictureProp = {
src: PictureOption;
/** color 会展示一个渐变色块的 loading 效果,加上 fade-in 的加载成功的渐变 */
placeholder?: 'empty' | 'color';
};
import type { ImgHTMLAttributes, HTMLAttributes } from 'vue';

/**
* 主要推荐使用的类型,由 vite-imagetools 生成
Expand Down Expand Up @@ -33,6 +26,16 @@ export type FallbackPictureOption = {

export type PictureOption = ImagetoolsPictureOption | FallbackPictureOption;

/** Picture Props 类型 */
export type PictureProp = {
/** @see https://npm.corp.kuaishou.com/-/web/detail/@kwai-explore/picture.vue */
src: PictureOption;
/** color 会展示一个渐变色块的 loading 效果,加上 fade-in 的加载成功的渐变 */
placeholder?: 'empty' | 'color';
/** PictureRootAttrs,由于使用时加的属性会被注入到img元素上,所以这里提供一个给根元素加属性的api */
rootAttrs?: HTMLAttributes;
};

declare const PictureComponent: new () => {
$props: PictureProp & Omit<ImgHTMLAttributes, 'src'>;
};
Expand Down