Vue3.0實(shí)現(xiàn)圖片預(yù)覽組件(媒體查看器)功能
前言:
最近項(xiàng)目中有個(gè)場(chǎng)景,一組圖片、視頻、音頻、文件數(shù)據(jù),要求點(diǎn)擊圖片可以放大預(yù)覽,左右可以切換音視頻、文件,支持鼠標(biāo)及各種鍵控制 縮放,左右旋轉(zhuǎn),移動(dòng)等功能,整理了一下,封了個(gè)組件,注釋很全面,每塊地方都有講解,可以直接拿到項(xiàng)目中使用
先看下效果:

關(guān)于傳值:
(必傳)傳入url數(shù)組urlList,傳入圖片所處index,也就是在數(shù)組中的索引
(非必傳)是否支持無(wú)限滾動(dòng)?是否支持ESC鍵退出?是否支持點(diǎn)擊遮罩層退出?是否需要工具欄?
關(guān)于圖片的相關(guān)特效:
定義一個(gè)transform樣式對(duì)象,包含縮放scale、旋轉(zhuǎn)deg、移動(dòng)offsetX|offsetY、動(dòng)畫enableTransition,可在computed計(jì)算中返回由此對(duì)象組成的css樣式對(duì)象,在模板中對(duì)圖片綁定,當(dāng)去觸發(fā)特效相關(guān)各類事件時(shí),改變對(duì)象里某個(gè)值,則會(huì)重新捕獲對(duì)象的改變執(zhí)行computed,去實(shí)時(shí)更新圖片樣式
1. 縮放操作: 可以通過(guò)鼠標(biāo)滾動(dòng)上下滑動(dòng)、鍵盤上下鍵up/down、組件內(nèi)部工具欄按鈕這三種方式去控制此特效,定義一個(gè)縮放比,即每次縮放的程度,可以根據(jù)項(xiàng)目場(chǎng)景自行定義,我這里鍵盤控制、按鈕點(diǎn)擊為1.4,鼠標(biāo)滾輪偏小為1.2, 每次縮放讓初始化的樣式對(duì)象的scale每次乘或除以這個(gè)值即可,當(dāng)然無(wú)限縮小無(wú)限放大肯定不行,需要定義一個(gè)最小最大值控制
2. 旋轉(zhuǎn)操作: 定義一個(gè)旋轉(zhuǎn)常量為90度,順時(shí)針旋轉(zhuǎn)讓初始化樣式對(duì)象deg累加這個(gè)值,逆時(shí)針相反即可
3. 移動(dòng)操作: 顧名思義,也就是在遮罩層內(nèi)可以通過(guò)鼠標(biāo)對(duì)圖片進(jìn)行移動(dòng),在鼠標(biāo)按下事件內(nèi),監(jiān)聽(tīng)鼠標(biāo)移動(dòng)事件,每次移動(dòng)記錄下當(dāng)前鼠標(biāo)位置,計(jì)算offsetX也就是圖片要移動(dòng)的translateX為:移動(dòng)前的距頁(yè)面左側(cè)距離offsetX加上當(dāng)前的鼠標(biāo)位置event.pageX - 移動(dòng)前的鼠標(biāo)位置; 上下移動(dòng)同理
4. 動(dòng)畫過(guò)渡: 根據(jù)特效的觸發(fā)方式?jīng)Q定是否需要過(guò)渡,我這里對(duì)通過(guò)鼠標(biāo)操作縮放時(shí)沒(méi)有定義動(dòng)畫,其余操作方式建議都要加上
關(guān)于圖片初始化展示:
如果想要圖片能夠自適應(yīng)在遮罩層的容器內(nèi),并且保證圖片不變形且寬或高不溢出容器,那么就不應(yīng)該定死寬高或者是不去定義寬高,我這里解決辦法是對(duì)圖片進(jìn)行等比例的縮放,具體算法就不在這里過(guò)多講解了,詳情見(jiàn)文末:附屬,圖片等比例縮放算法(計(jì)算縮放后的寬高)
關(guān)于音視頻展示:
視頻我這里用的是 vue3-video-play 這個(gè)插件,ui和功能各方面整體感覺(jué)很棒,算是對(duì)Vue 3.0支持比較好的一個(gè)吧,詳情可以參考:https://codelife.cc/vue3-video-play/
音頻用的是原生audio,用法很方便,沒(méi)什么可講的,具體看代碼
關(guān)于拋出數(shù)據(jù):
頂部中間一般為圖片當(dāng)前索引index/總長(zhǎng)度,當(dāng)然默認(rèn)的為這樣,拋出index,可以自定義這塊地方插槽的使用,頂部左側(cè)插槽也暴露當(dāng)前index,其實(shí)當(dāng)前組件最需要的數(shù)據(jù)也莫過(guò)于index,暴露方法中也基本都有拋出
附上完整代碼
<template>
<teleport to='body'>
<transition>
<div class="el-image-viewer__wrapper" :tabindex="-1" ref="imageViewer" :style="{ zIndex: computedZIndex }">
<!-- MASK -->
<div class="el-image-viewer__mask" @click.self="isClickToDisappear && hide"></div>
<!-- CLOSE -->
<span class="el-image-viewer__btn el-image-viewer__close" @click="hide">
<el-icon>
<Close />
</el-icon>
</span>
<!-- HEADER -->
<div class="image-viewer-header" v-if="isTools">
<div class="header-left">
<span class="header-file-name">
<slot name="title" :index="index"></slot>
</span>
<slot name="left" :index="index"></slot>
</div>
<div class="header-center">
<slot name="content" :index="index">
{{ index + 1 }} / {{ urlList.length }}
</slot>
</div>
</div>
<!-- ACTIONS -->
<div class="el-image-viewer__btn el-image-viewer__actions"
v-if="isFileType(urlList[index]) === 'img' && isTools">
<div class="el-image-viewer__actions__inner">
<el-icon @click="handleActions('zoomOut')">
<ZoomOut />
</el-icon>
<el-icon @click="handleActions('zoomIn')">
<ZoomIn />
</el-icon>
<el-icon @click="toggleMode">
<component :is="icons.default[mode.icon]"></component>
</el-icon>
<el-icon @click="handleActions('anticlockwise')">
<RefreshLeft />
</el-icon>
<el-icon @click="handleActions('clockwise')">
<RefreshRight />
</el-icon>
</div>
</div>
<!-- ARROW -->
<template v-if="!isSingle">
<span class="el-image-viewer__btn el-image-viewer__prev" @click="prev">
<el-icon>
<ArrowLeftBold />
</el-icon>
</span>
<span class="el-image-viewer__btn el-image-viewer__next" @click="next">
<el-icon>
<ArrowRightBold />
</el-icon>
</span>
</template>
<!-- CANVAS -->
<div id="image-viewer-canvas" class="el-image-viewer__canvas">
<div v-for="(url, i) in urlList" :key="url" style="display: flex" :data-id="url">
<!-- 圖片顯示 -->
<img v-if="i === index && isFileType(url) === 'img'" v-loading="loading" :src="url"
:style="[mediaStyle, { width: 'auto', height: imgHeight }]" class="el-image-viewer__img"
@load="handleImgLoad" @error="handleMediaError" @mousedown="handleMouseDown" />
<!-- 視頻顯示 -->
<videoPlay v-if="(i === index && isFileType(url) === 'video')" :src="url"
class="el-image-viewer__img" @load="handleMediaLoad" @error="handlePlayError(i)"
@mousedown="handleMouseDown">
</videoPlay>
<!-- 音頻顯示 -->
<audio controls="controls" v-if="i === index && isFileType(url) === 'audio'" :src="url"
class="el-image-viewer__img" @load="handleMediaLoad" @error="handleMediaError"></audio>
<!-- 文本文件顯示 -->
<div v-if="i === index && ['file', 'text'].includes(isFileType(url))" class="image-viewer-tips">
<span class="image-unknown-file-type-view"></span>
<p>
我們不能預(yù)覽該文件。<br>
您要先下載文件以查看。
</p>
<div class="image-viewer-download" @click="download(url)">
<span class="icon-download"></span>
下載
</div>
</div>
</div>
</div>
</div>
</transition>
</teleport>
</template>
<script lang="ts">
import 'element-plus/es/components/image-viewer/style/css'
import "vue3-video-play/dist/style.css";
import * as icons from './mediaIcons'
import { videoPlay } from "vue3-video-play/dist/index.es";
import { fileType, transformImgRatio } from "@/utils/dataUtils"; // fileType 判斷文件后綴方法; transformImgRatio 等比例計(jì)算圖片寬高方法
import { isNumber, useEventListener } from '@vueuse/core'
import { PropType } from 'vue';
import { useZIndex } from 'element-plus';
export default defineComponent({
name: 'imageViewerUtil',
props: {
urlList: { // url數(shù)組
type: Array as PropType<string[]>,
default: () => []
},
imgIndex: { // 當(dāng)前文件所處位置,也是在數(shù)組中的索引
type: Number,
default: 0
},
isTools: { // 是否需要工具欄
type: Boolean,
default: true
},
isInfinite: { // 是否支持無(wú)限循環(huán)滾動(dòng)
type: Boolean,
default: true
},
zIndex: { // 層級(jí)
type: Number
},
closeOnPressEscape: { // 是否支持ESC鍵退出
type: Boolean,
default: true
},
isClickToDisappear: { // 是否支持通過(guò)點(diǎn)擊遮罩層關(guān)閉
type: Boolean,
default: false
}
},
emits: ['close', 'download', 'prevIndex', 'nextIndex'],
setup(props, { emit }) {
const global = getCurrentInstance().appContext.config.globalProperties
const { nextZIndex } = useZIndex()
const modes = { // 模式對(duì)象
CONTAIN: {
name: 'contain',
icon: 'IconEpFullScreen',
},
ORIGINAL: {
name: 'original',
icon: 'IconEpScaleToOriginal',
},
}
const mode = shallowRef(modes.ORIGINAL) // 模式
const EVENT_CODE = { // 按鈕對(duì)象
left: 'ArrowLeft', // 37
up: 'ArrowUp', // 38
right: 'ArrowRight', // 39
down: 'ArrowDown', // 40
esc: 'Escape',
space: 'Backspace'
}
const imageViewer = ref<HTMLDivElement>()
const data = reactive({
index: 0, // 圖片索引,也是在數(shù)組中的位置
loading: true, // 處理加載
imgHeight: '', // 處理圖片高
transform: {
scale: 1, // 縮放比
deg: 0, // 旋轉(zhuǎn)角度
offsetX: 0,
offsetY: 0,
enableTransition: false // 是否需要過(guò)渡
}
})
// 是否是火狐
const isFirefox = (): boolean => /firefox/i.test(window.navigator.userAgent)
// 鼠標(biāo)滾輪事件,火狐的不同
const mousewheelEventName = isFirefox() ? 'DOMMouseScroll' : 'mousewheel'
const computedZIndex = computed(() => { // 計(jì)算z-index值
return isNumber(props.zIndex) ? props.zIndex : nextZIndex()
})
const isSingle = computed(() => { // 是否是單張
return props.urlList.length <= 1
})
const isFirst = computed(() => { // 是否是第一張
return data.index === 0
})
const isLast = computed(() => { // 是否是最后一張
return data.index === props.urlList.length - 1
})
const isFileType = computed(() => { // 判斷文件類型
return (url: string) => {
return fileType(url.split('?')[0])
}
})
const mediaStyle = computed(() => { // 圖片css特效對(duì)象
const { scale, deg, offsetX, offsetY, enableTransition } = data.transform
let translateX = offsetX / scale
let translateY = offsetY / scale
switch (deg % 360) {
case 90:
case -270:
;[translateX, translateY] = [translateY, -translateX]
break
case 180:
case -180:
;[translateX, translateY] = [-translateX, -translateY]
break
case 270:
case -90:
;[translateX, translateY] = [-translateY, translateX]
break
}
return {
transform: `scale(${scale}) rotate(${deg}deg) translate(${translateX}px, ${translateY}px)`,
transition: enableTransition ? 'transform .3s' : ''
}
})
const handleImgLoad = (e) => { // 處理加載圖片后的操作
// 計(jì)算圖片等比例縮放后的寬高
const { clientWidth, clientHeight } = document.querySelector('#image-viewer-canvas') // 當(dāng)前遮罩容器
const { width, height } = transformImgRatio(e.target.width, e.target.height, clientWidth, clientHeight - 40) // 計(jì)算等比例縮放后的圖片寬高
data.imgHeight = height + 'px'
data.loading = false
}
const hide = () => { // 關(guān)閉
emit('close')
}
const prev = () => { // 上一張
if (!props.isInfinite && !data.index) return ElMessage({ type: 'info', message: '已經(jīng)是第一張了!' })
data.index = (data.index - 1 + props.urlList.length) % props.urlList.length
resetStyle()
emit('prevIndex', data.index)
}
const next = () => { // 下一張
if (!props.isInfinite && data.index === props.urlList.length - 1) return ElMessage({ type: 'info', message: '已經(jīng)是最后一張了!' })
data.index = (data.index + 1 + props.urlList.length) % props.urlList.length
resetStyle()
emit('nextIndex', data.index)
}
const keydownHandler = (e: event) => { // 鍵盤事件
switch (e.code) {
case EVENT_CODE.esc: // Escape
props.closeOnPressEscape && hide()
break;
case EVENT_CODE.left: // ArrowLeft
prev()
break;
case EVENT_CODE.right: // ArrowRight
next()
break;
case EVENT_CODE.up: // ArrowUp
handleActions('zoomIn')
break;
case EVENT_CODE.down: // ArrowDown
handleActions('zoomOut')
break;
case EVENT_CODE.space: // Backspace
toggleMode()
break
}
e.preventDefault()
}
const mousewheelHandler = (e: WheelEvent | any /* TODO: wheelDelta is deprecated */) => { // 鼠標(biāo)滾輪事件
const delta = e.wheelDelta ? e.wheelDelta : -e.detail // 考慮Firefox
if (delta > 0) { // 向上
handleActions('zoomIn', { zooRate: 1.2, enableTransition: false })
} else { // 向下
handleActions('zoomOut', { zooRate: 1.2, enableTransition: false })
}
}
const handleActions = (action: any, option = {}) => { // 各類指令操作
const { zoomRate, rotateDeg, enableTransition } = { // 定義常規(guī)特效
zoomRate: 1.4,
rotateDeg: 90,
enableTransition: true,
...option,
}
switch (action) {
case 'zoomOut': // 縮小
if (data.transform.scale > 0.2) data.transform.scale = parseFloat((data.transform.scale / zoomRate).toFixed(3))
break;
case 'zoomIn': // 放大
if (data.transform.scale < 6) data.transform.scale = parseFloat((data.transform.scale * zoomRate).toFixed(3))
break;
case 'anticlockwise': // 逆時(shí)針旋轉(zhuǎn)
data.transform.deg -= rotateDeg
break;
case 'clockwise': // 順時(shí)針旋轉(zhuǎn)
data.transform.deg += rotateDeg
break;
}
data.transform.enableTransition = enableTransition
}
const resetStyle = () => { // 左右切換重置transform對(duì)象
data.transform = { scale: 1, deg: 0, enableTransition: false }
}
const handleMediaLoad = (e: event) => { // 加載處理
data.loading = false
}
const handleMediaError = () => { // 圖片 音頻失敗處理
data.loading = false
}
const handlePlayError = (index: number) => { // 視頻失敗處理
props.urlList[index] = props.urlList[index].split('?')[0] + '?v=' + new Date().getTime()
}
const download = (url: string) => { // 文件類型拋出url
emit('download', url)
}
const toggleMode = () => {
if (data.loading) return
const modeNames = Object.keys(modes)
const modeValues = Object.values(modes)
const currentMode = mode.value.name
const index = modeValues.findIndex((i) => i.name === currentMode)
const nextIndex = (index + 1) % modeNames.length
mode.value = modes[modeNames[nextIndex]]
resetStyle()
}
const handleMouseDown = (e: MouseEvent) => { // 處理鼠標(biāo)按下事件
data.transform.enableTransition = false
const { offsetX, offsetY } = data.transform
const startX = e.pageX
const startY = e.pageY
// 拖拽事件
const dragHandler = (ev: MouseEvent) => {
data.transform = {
...data.transform,
offsetX: offsetX + ev.pageX - startX,
offsetY: offsetY + ev.pageY - startY,
}
}
// 添加鼠標(biāo)移動(dòng)事件監(jiān)聽(tīng)
const removeMousemove = useEventListener(document, 'mousemove', dragHandler)
useEventListener(document, 'mouseup', () => {
removeMousemove()
})
e.preventDefault()
}
watch(() => props.imgIndex, () => {
data.index = props.imgIndex
}, { immediate: true })
onMounted(() => {
// 優(yōu)化注冊(cè)事件監(jiān)聽(tīng)
useEventListener(document, 'keydown', keydownHandler)
useEventListener(document, mousewheelEventName, mousewheelHandler)
imageViewer.value?.focus?.()
})
return {
...toRefs(data),
computedZIndex,
mediaStyle,
isFileType,
isSingle,
isFirst,
isLast,
mode,
icons,
hide,
prev,
next,
handleImgLoad,
handleActions,
handleMediaLoad,
handlePlayError,
handleMediaError,
toggleMode,
download,
handleMouseDown
}
}
})
</script>
<style lang="scss" scoped>
:deep(.el-image-viewer__canvas) {
//height: calc(100% - 180px);
align-items: center;
height: calc(100% - 40px);
}
:deep(.el-image-viewer__actions) {
height: 40px;
right: 50px;
top: 0;
left: auto;
z-index: inherit;
transform: none;
padding: 0;
background: none;
border-radius: 0;
}
:deep(.el-image-viewer__actions__divider) {
display: none;
}
:deep(.el-image-viewer__next) {
width: 64px;
height: 100px;
background: transparent;
border-radius: 6px 0px 0px 6px;
opacity: 0.8;
right: 0;
top: 45%;
.el-icon {
color: #999999;
font-size: 50px;
}
}
:deep(.el-image-viewer__prev) {
width: 64px;
height: 100px;
background: transparent;
border-radius: 0px 6px 6px 0px;
opacity: 0.8;
left: 0;
top: 45%;
.el-icon {
color: #999999;
font-size: 50px;
}
}
.el-image-viewer__next:hover,
.el-image-viewer__prev:hover {
background: #000000;
opacity: 1;
}
:deep(.el-image-viewer__mask) {
opacity: 0.8;
}
:deep(.el-image-viewer__close) {
height: 40px;
z-index: 56;
top: 0;
right: 10px;
background: transparent;
}
.image-viewer-carousel {
height: 140px;
background: #000000;
z-index: 50;
padding: 20px;
position: relative;
display: flex;
flex-wrap: wrap;
}
.image-viewer-header {
background-color: #000000;
box-sizing: border-box;
border-spacing: 0;
width: 100%;
height: 40px;
z-index: 55;
position: relative;
z-index: 55;
color: #FFFFFF;
text-align: center;
line-height: 40px;
.header-left {
position: absolute;
}
}
.header-file-icon {
width: 24px;
height: 50px;
display: inline-block;
margin-left: 20px;
background-image: url(../../../../assets/svg/image-icon.svg);
background-repeat: no-repeat;
background-position: 50% 50%;
/*這個(gè)是按從左往右,從上往下的百分比位置進(jìn)行調(diào)整*/
background-size: 100% 60%;
/*按比例縮放*/
}
.header-file-name {
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
vertical-align: top;
margin: 0 20px;
}
.image-viewer-tips {
background-color: rgba(0, 0, 0, 0.8);
color: #fff;
box-sizing: border-box;
display: inline-block;
vertical-align: middle;
text-align: center;
min-width: 490px;
padding: 35px 100px;
line-height: 2em;
border-radius: 5px;
z-index: 56;
}
.image-unknown-file-type-view {
display: inline-block;
width: 96px;
height: 96px;
background-size: contain;
background: url(../../../../assets/svg/file-icon.svg);
background-repeat: no-repeat;
background-position: center center;
background-size: contain;
}
.image-viewer-download {
box-sizing: border-box;
background: #f5f5f5;
border: 1px solid #ccc;
border-radius: 3.01px;
color: #333;
cursor: pointer;
display: inline-block;
font-family: inherit;
font-size: 14px;
font-variant: normal;
font-weight: 400;
height: 2.14285714em;
line-height: 1.42857143;
margin: 0;
padding: 4px 10px;
vertical-align: baseline;
white-space: nowrap;
text-decoration: none;
margin: 20px 10px 0 10px;
.icon-download {
position: relative;
top: 4px;
background: url(../../../../assets/svg/download.svg) no-repeat 0 0;
border: none;
margin: 0;
padding: 0;
text-indent: -999em;
vertical-align: text-bottom;
display: inline-block;
text-align: left;
line-height: 0;
position: relative;
vertical-align: text-top;
height: 16px;
width: 16px;
}
}
</style>附屬
圖片等比例縮放算法(計(jì)算縮放后的寬高)
讓圖片能夠自適應(yīng)父容器的寬高,并且保證圖片不變形不溢出,那么就需要對(duì)圖片進(jìn)行等比例縮放,拿到縮放后的寬高重新賦值即可,具體算法如下:
// 分別傳入圖片寬高、父容器寬高
const transformImgRatio = (imgWidth, imgHeight, containerWidth, containerHeight) => {
let [
// 用于設(shè)定圖片的寬和高
tempWidth,
tempHeight,
] = [
0,
0
]
try {
imgWidth = parseFloat(imgWidth)
imgHeight = parseFloat(imgHeight)
containerWidth = parseFloat(containerWidth)
containerHeight = parseFloat(containerHeight)
} catch (error) {
throw new Error('抱歉,我只接收數(shù)值類型或者可以轉(zhuǎn)成數(shù)值類型的參數(shù)')
}
if (imgWidth > 0 && imgHeight > 0) {
//原圖片寬高比例 大于 指定的寬高比例,這就說(shuō)明了原圖片的寬度必然 > 高度
if (imgWidth / imgHeight >= containerWidth / containerHeight) {
if (imgWidth > containerWidth) {
tempWidth = containerWidth
// 按原圖片的比例進(jìn)行縮放
tempHeight = (imgHeight * containerWidth) / imgWidth
} else {
// 按照?qǐng)D片的大小進(jìn)行縮放
tempWidth = imgWidth
tempHeight = imgHeight
}
} else { // 原圖片的高度必然 > 寬度
if (imgHeight > containerHeight) {
tempHeight = containerHeight
// 按原圖片的比例進(jìn)行縮放
tempWidth = (imgWidth * containerHeight) / imgHeight
} else {
// 按原圖片的大小進(jìn)行縮放
tempWidth = imgWidth
tempHeight = imgHeight
}
}
}
return { width: tempWidth, height: tempHeight }
}到此這篇關(guān)于Vue3.0實(shí)現(xiàn)圖片預(yù)覽組件(媒體查看器)的文章就介紹到這了,更多相關(guān)Vue圖片預(yù)覽組件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- vue?element-plus圖片預(yù)覽實(shí)現(xiàn)方法
- vue中關(guān)于element的el-image 圖片預(yù)覽功能增加一個(gè)下載按鈕(操作方法)
- ant?design?vue?圖片預(yù)覽組件自定義樣式
- Vue使用v-viewer插件實(shí)現(xiàn)圖片預(yù)覽和縮放和旋轉(zhuǎn)等功能(推薦)
- vue實(shí)現(xiàn)圖片預(yù)覽放大以及縮小問(wèn)題
- vue-photo-preview圖片預(yù)覽失效的問(wèn)題及解決
- Vue實(shí)現(xiàn)圖片預(yù)覽效果實(shí)例(放大、縮小、拖拽)
- Vue使用v-viewer實(shí)現(xiàn)圖片預(yù)覽
- vue基于viewer實(shí)現(xiàn)的圖片查看器功能
相關(guān)文章
完美解決iview 的select下拉框選項(xiàng)錯(cuò)位的問(wèn)題
下面小編就為大家分享一篇完美解決iview 的select下拉框選項(xiàng)錯(cuò)位的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03
基于Vant UI框架實(shí)現(xiàn)時(shí)間段選擇器
這篇文章主要為大家詳細(xì)介紹了基于Vant UI框架實(shí)現(xiàn)時(shí)間段選擇器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-12-12
vue router下的html5 history在iis服務(wù)器上的設(shè)置方法
這篇文章主要介紹了vue router下的html5 history在iis服務(wù)器上的設(shè)置方法,需要的朋友參考下吧2017-10-10
Vue動(dòng)態(tài)組件與異步組件實(shí)例詳解
這篇文章主要介紹了Vue動(dòng)態(tài)組件與異步組件,結(jié)合實(shí)例形式分析了動(dòng)態(tài)組件與異步組件相關(guān)概念、功能及使用技巧,需要的朋友可以參考下2019-02-02
vue源碼學(xué)習(xí)之Object.defineProperty 對(duì)數(shù)組監(jiān)聽(tīng)
這篇文章主要介紹了vue源碼學(xué)習(xí)之Object.defineProperty 對(duì)數(shù)組監(jiān)聽(tīng),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05
Vue獲取子組件實(shí)例對(duì)象ref屬性的方法推薦
在Vue中我們可以使用ref屬性來(lái)獲取子組件的實(shí)例對(duì)象,這個(gè)功能非常方便,這篇文章主要給大家介紹了關(guān)于Vue獲取子組件實(shí)例對(duì)象ref屬性的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06
解決VUE 在IE下出現(xiàn)ReferenceError: Promise未定義的問(wèn)題
這篇文章主要介紹了解決VUE 在IE下出現(xiàn)ReferenceError: Promise未定義的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11

