Vue 可視化大屏適配插件之過(guò)程篇
一直以來(lái)都想自己寫一款插件去解決大屏的適配問(wèn)題,最近終于有時(shí)間去完成這件事,特此記錄下過(guò)程中碰到的問(wèn)題。
注冊(cè) vue 指令如何支持類型提示?
文檔說(shuō) vue 插件的 use方法是支持第二個(gè)參數(shù)的,一開(kāi)始打算通過(guò)第二個(gè)參數(shù)做基礎(chǔ)配置。能正確讀取到該參數(shù),可是不知道怎么做類型提示,因?yàn)楣俜蕉x的是 any[], 那我總不能讓使用者去從我的插件里導(dǎo)出類型再去 as 吧?
谷歌了問(wèn)題,翻了 issue , 也找了一些開(kāi)源的插件去看,好像大家都沒(méi)這個(gè)需求。
開(kāi)端就遇到問(wèn)題,搞得我都不是很有動(dòng)力寫下去了。
后來(lái)下班路上在地鐵里猛然想起來(lái)第一個(gè)參數(shù)可以是 object 和 function, 那 function 不是支持傳參嗎?就這樣第一個(gè)問(wèn)題解決掉了。
import?Fit?from?'vue-fit-next'
app
??.use(
????Fit({??//?這里就有了類型提示了
??????width:?3840,
??????height:?2160,
????})
??)
??.mount('#app')
復(fù)制代碼
插件叫什么名字?
如果不能為你的插件提供一個(gè)意義明確且好記的名字,那么這個(gè)插件很可能不會(huì)讓人有特別想使用的欲望。
類似 adapter-screen 這種估計(jì)已經(jīng)有人使用了,而且名字太長(zhǎng)也不好拼。
后來(lái)想到了一個(gè)單詞fit, 因?yàn)檫@個(gè)在寫 css的時(shí)候會(huì)用到,比如object-fit:cover, 查了下翻譯軟件確實(shí)有適配、合身的意思。
叫 vue-fit 吧, 但是到 npm一搜,發(fā)現(xiàn)幾年前就被人占用了。
那不如就叫vue-fit-next 吧。
如何適配?
整體縮放
核心可能還是 scale, 剛開(kāi)始采用的網(wǎng)頁(yè)整體 scale。
基本思路就是用innerWidth和設(shè)計(jì)稿寬度計(jì)算比值,然后高度和寬度中采用比值最小的一個(gè)。
??const?w?=?window.innerWidth?/?defaultFitOptions.width
??const?h?=?window.innerHeight?/?defaultFitOptions.height
??const?scale?=?Math.min(w,?h)?//?寬度與高度的比例取最小的,以確保屏幕可以完全顯示
復(fù)制代碼
這樣能達(dá)到基本效果,但是會(huì)帶來(lái)新的問(wèn)題:
- 如果不使用F11(全屏模式),右邊和下邊會(huì)出現(xiàn)”留白“。
- 不能適配非設(shè)計(jì)稿之外的分辨率。
分塊縮放
把網(wǎng)頁(yè)分成很多小塊,分別對(duì)這些小塊進(jìn)行縮放,就能解決”留白“問(wèn)題了。
比如把一張網(wǎng)頁(yè)分成九塊,再配合位置調(diào)整。那么”留白“就留到了小塊的”邊界處“,在視覺(jué)上就看不出”留白“了。
?????-----------------------------------------
????|????????????|??????????????|?????????????|
????|????left????|????center????|????right????|
????|????????????|??????????????|?????????????|
?????-----------------------------------------
????|????????????|??????????????|?????????????|
????|?leftCenter?|?centerCenter?|?rightCenter?|
????|????????????|??????????????|?????????????|
?????-----------------------------------------
????|????????????|??????????????|?????????????|
????|?leftBottom?|?centerBottom?|?rightBottom?|
????|????????????|???????????????|????????????|
?????-----------------------------------------
復(fù)制代碼
transform變換中心問(wèn)題
如上所示,需要把每個(gè) div 分隔開(kāi),就會(huì)涉及到縮放之后的對(duì)齊問(wèn)題,也就是 transform-origin的值。
默認(rèn)是從元素的正中心進(jìn)行縮放的,我們可以進(jìn)行設(shè)置。
但是不管我們?cè)趺丛O(shè)置 transform-origin 的值, 在屏幕不同位置的元素都有可能出現(xiàn)位置不正確的情況。
比如,設(shè)置 transform-origin: left top之后左上角的元素位置正確了。但是右上角的卻錯(cuò)了,右邊和下邊會(huì)出現(xiàn)”留白“。
因此需要針對(duì)不同位置的元素設(shè)置不同的transform-origin值再加上簡(jiǎn)單的偏移計(jì)算。
這就是為什么需要用戶在寫指令的時(shí)候定義好一個(gè) ”區(qū)域“ 。
如何計(jì)算偏移量
也許你用過(guò) offsetTop 或者 offsetLeft, 但應(yīng)該沒(méi)用過(guò)offsetRight 和offsetBottom吧?
是的,當(dāng)我需要他們兩的時(shí)候才發(fā)現(xiàn)根本沒(méi)這兩屬性。
比如當(dāng)元素靠近右側(cè)時(shí),我需要去計(jì)算物體的offsetRight, 因?yàn)橛脩艉苡锌赡芡ㄟ^(guò)CSS設(shè)置了 right、margin-right等位置屬性,
而這些用戶手動(dòng)設(shè)置的屬性是沒(méi)有參與scale的, 就會(huì)導(dǎo)致位置不準(zhǔn)確。那我需要通過(guò)代碼進(jìn)行”糾偏“。
通過(guò)getComputedStyle這個(gè)方法去獲取元素的css屬性值,就得到了右偏移和下偏移的值,再把這個(gè)值通過(guò)translate的方式偏移。
/**?獲取css?屬性值?*/
export?function?getComputedStyleNumber(el:?HTMLElement,?attr:?string)?{
??return?parseFloat(getComputedStyle(el,?null).getPropertyValue(attr)?||?'0')
}
const?offsetRight?=?(getComputedStyleNumber(el,?'right')?+?getComputedStyleNumber(el,?'margin-right'))
const?offsetBottom?=?(getComputedStyleNumber(el,?'bottom')?+?getComputedStyleNumber(el,?'margin-bottom'))
復(fù)制代碼
動(dòng)畫帶來(lái)的問(wèn)題
動(dòng)畫搶占 transform
插件本身還具備入場(chǎng)和出場(chǎng)動(dòng)畫功能,最開(kāi)始是打算直接用animation.css這個(gè)庫(kù)的。
- 可以保證插件具有很多可選的動(dòng)畫效果。
-
使用簡(jiǎn)單,用戶只需要添加對(duì)應(yīng)的
class屬性就行了,沒(méi)有額外的學(xué)習(xí)成本。
但在結(jié)合的過(guò)程中卻出現(xiàn)了問(wèn)題,animation.css也使用了transform,就會(huì)導(dǎo)致 css 沖突覆蓋。
animate的動(dòng)畫結(jié)束之后就把插件的scale值覆蓋成默認(rèn)值了,這就變得麻煩起來(lái)了。
也想過(guò)做一些迂回:比如加一層 dom專門做動(dòng)畫。但這樣的方式不是我想要的, 會(huì)破環(huán)用戶原始的dom結(jié)構(gòu),那還不如不要?jiǎng)赢嫷墓δ芰恕?/p>
那干脆放棄animate.css吧,自己用js做動(dòng)畫,在位置變換的同時(shí)設(shè)置好scale就能解決這個(gè)問(wèn)題了。
transform 順序問(wèn)題
在做動(dòng)畫的過(guò)程中碰到了另一個(gè)問(wèn)題,關(guān)于書寫順序的。搞得我?guī)锥葢岩扇松?,以為是自己?translate值計(jì)算錯(cuò)了...
/*?錯(cuò)誤?*/
transform:?scale(${scale},?${scale})?translate3d(-100%,?${y}px,?0);
/*?正確?*/
transform:?translate3d(-100%,?${y}px,?0)?scale(${scale},?${scale});
復(fù)制代碼
盡管我還是無(wú)法理解先寫scale和先translate的區(qū)別,但是他符合預(yù)期了。
如何動(dòng)態(tài)更新動(dòng)畫
一開(kāi)始計(jì)算出scale值之后就能生成 keyframes的代碼了,但是如果用戶縮放瀏覽器之后呢,那我也應(yīng)該更新下keyframes的代碼。就用到了CSSStyleSheet這個(gè),他提供了insertRule和deleteRule兩個(gè)方法,幫助我們插入和刪除樣式。
styleSheet?.insertRule(
??`@keyframes?slideInLeft_${nanoId}?{
??????from?{
????????transform:?translate3d(-100%,?${y}px,?0)?scale(${scale},?${scale});
??????visibility:?visible;
????}
????to?{
??????transform:?translate3d(${x}px,?${y}px,?0)?scale(${scale},?${scale});
????}
??}`
)
復(fù)制代碼
通過(guò) styleSheet?.deleteRule(index)傳入樣式規(guī)則的下標(biāo)即可刪除樣式。
?const?rules?=?styleSheet?.cssRules
??if?(rules)?{
????while?(rules.length)
??????styleSheet?.deleteRule(0)
??}
復(fù)制代碼
不得不用的 Transition 組件
這樣就添加好入場(chǎng)動(dòng)畫了,沒(méi)想到的是出場(chǎng)動(dòng)畫出了岔子。
我并不知道vue會(huì)在什么時(shí)候刪除元素,當(dāng)用戶通過(guò)v-if或者:is去管理組件的時(shí)候,元素會(huì)被立即銷毀,出場(chǎng)動(dòng)畫也就沒(méi)機(jī)會(huì)動(dòng)畫了。
那我也不能去要求用戶把這個(gè)控制元素狀態(tài)的變量傳遞給我吧?那豈不是太神經(jīng)啦,你的組件關(guān)我什么事哦?
好在vue的 transition組件提供了對(duì)應(yīng)的鉤子,只能讓用戶在跟組件包裹下Transition組件了,并且把leave的鉤子傳遞給我的插件。
講真,這個(gè)要求應(yīng)該不算過(guò)分吧~ 并不會(huì)對(duì)用戶的代碼產(chǎn)生什么破壞性的影響~
事件
到這里幾乎就沒(méi)什么大問(wèn)題了,給window添加對(duì)應(yīng)的事件就行, 事件這一塊就靠rxjs來(lái)幫我了。
拖動(dòng)
核心是整體拖動(dòng),觸發(fā)對(duì)應(yīng)的事件之后,對(duì)整個(gè)body元素設(shè)置位置更新
/**
?*?按住?space?鍵的同時(shí)鼠標(biāo)點(diǎn)擊界面進(jìn)行拖動(dòng).
?*/
spaceDown$.pipe(
??mergeMap(()?=>?mousedown$.pipe(
????map((event)?=>?{
??????const?{?x,?y?}?=?getTranslateValue(body)
??????return?{
????????x:?event.clientX?-?x,
????????y:?event.clientY?-?y,
??????}
????}),
????mergeMap(({?x,?y?})?=>?mousemove$.pipe(
??????map(ev?=>?({
????????x:?ev.clientX?-?x,
????????y:?ev.clientY?-?y,
??????})),
??????takeUntil(mouseup$),
????)),
????throttleTime(20),
????takeUntil(
??????spaceUp$.pipe(
????????tap(()?=>?body.style.cursor?=?cursor),
??????)),
??)),
).subscribe((value)?=>?{
??bodyTransform$.next(setBodyTransform(value))
})
復(fù)制代碼
縮放
/**
?*??按住?ctrl?鍵的同時(shí)滾動(dòng)鼠標(biāo),實(shí)現(xiàn)整體縮放及位置偏移.
?*/
const?ctrlMousewheel$?=?(seed:?Required<TransformType>)?=>?mousewheel$.pipe(
??filter(isCtrlKey),
??tap(event?=>?event.preventDefault()),
??scan(calcTransform,?seed),
)
復(fù)制代碼
復(fù)原
/**
?*?雙擊?space?鍵進(jìn)行位置復(fù)原.
?*/
spaceDown$.pipe(
??bufferWhen(()?=>?spaceDown$.pipe(debounceTime(250))),
??filter(list?=>?list.length?===?2),
).subscribe(()?=>?{
??bodyTransform$.next(setBodyTransform({?x:?0,?y:?0,?scale:?1?}))
})
復(fù)制代碼
和3D交互的沖突
當(dāng)我們使用鼠標(biāo)進(jìn)行拖動(dòng)或者縮放時(shí),這些操作在對(duì)應(yīng)的3d場(chǎng)景中也會(huì)被觸發(fā),就造成了沖突,因此在出發(fā)我們的事件時(shí)需要隔離掉3D事件。最簡(jiǎn)單的方式就是創(chuàng)建一個(gè)mask放到最外邊。
/**
?*?進(jìn)行拖動(dòng)或者放大時(shí)添加遮罩以免影響3D場(chǎng)景
?*/
merge(
??spaceDown$,
??mousewheel$.pipe(filter(isCtrlKey)),
).pipe(throttleTime(500))
??.subscribe(()?=>?{
????document.body.appendChild(fitMask)
??})
/**
?*?鍵盤抬起時(shí)移除遮罩
?*/
keyup$.subscribe(()?=>?{
??document.querySelector('#fitMask')?.remove()
})
復(fù)制代碼
源碼
至此,這個(gè)插件就算完成了,沒(méi)想到這么小小的一個(gè)功能,卻碰到了辣么多的問(wèn)題。
希望這個(gè)插件能好用吧~
最后,源碼和示例都在 github 上了, 求大佬們來(lái)點(diǎn) star。
-
github [1]
-
同事說(shuō)大屏適配方案能用就行,我偏不! [2]
關(guān)于本文
https://juejin.cn/post/7134985068786745374
