Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
網(wǎng)頁展現(xiàn)的更快,官方說法叫做首屏繪制,F(xiàn)irst Paint 或者簡(jiǎn)稱 FP,直白的說法叫做白屏?xí)r間,就是從輸入 URL 到真的看到內(nèi)容(不必可交互,那個(gè)叫 TTI, Time to Interactive)之間經(jīng)歷的時(shí)間。當(dāng)然這個(gè)時(shí)間越短越好。
但這里要注意,和首屏相關(guān)的除了 FP 還有兩個(gè)指標(biāo),分別稱為 FCP (First Contentful Paint,頁面有效內(nèi)容的繪制) 和 FMP (First Meaningful Paint,頁面有意義的內(nèi)容繪制)。雖然這幾個(gè)概念可能會(huì)讓我們繞暈,但我們只需要了解一點(diǎn):首屏?xí)r間 FP 并不要求內(nèi)容是真實(shí)的,有效的,有意義的,可交互的。換言之,隨便 給用戶看點(diǎn)啥都行。
這就是本文標(biāo)題的玄機(jī)了:“看起來”。是的,只是看起來更快,實(shí)際上還是那樣。所以本文并不討論性能優(yōu)化,討論的是一個(gè)投機(jī)取巧的小伎倆,但的確能夠?qū)崒?shí)在在的提升體驗(yàn)。打個(gè)比方,性能優(yōu)化是修煉內(nèi)功,提升你本身的各項(xiàng)機(jī)能;而本文接下來要討論的是一些招式,能讓你在第一時(shí)間就唬住對(duì)手。
這所謂的招式就是我接下來要談的內(nèi)容,學(xué)名骨架屏,也叫 Skeleton。你可能沒聽過這個(gè)名字,但你不可能沒見過它。
骨架屏長什么樣
這種應(yīng)該是最常見的形式,使用各種形狀的灰色矩形來模擬圖片和文字。有些 APP 也會(huì)使用圓形,但重點(diǎn)都是和實(shí)際內(nèi)容結(jié)構(gòu)近似,不能差距太大。
如果追求效果,還可以在色塊表面添加動(dòng)畫(如波紋),顯示出一種動(dòng)態(tài)的效果,算是致敬 Loading 了。
在圖片居多的站點(diǎn),這將會(huì)是一個(gè)很好的體驗(yàn),因?yàn)閳D片通常加載較慢。如上圖演示中的占位圖片采用了低像素的圖片,即大體配色和變化是和實(shí)際內(nèi)容一致的。
如果無法生成這樣的低像素圖片,稍微降級(jí)的方案是通過算法獲取圖片的主體顏色,使用純色塊占位。
再退一級(jí),還可以使用全站相同的站位圖片,或者直接一個(gè)統(tǒng)一顏色的色塊。雖說效果肯定不如上面兩種,但也聊勝于無。
骨架屏完全是自定義的,想做成什么樣全憑你的想象。你想做圓形的,三角形的,立體的都可以,但“占位”決定了它的特性:它不能太復(fù)雜,必須第一時(shí)間,最快展現(xiàn)出來。
骨架屏有哪些優(yōu)勢(shì)
大體來說,骨架屏的優(yōu)勢(shì)在于:
1、在頁面加載初期預(yù)先渲染內(nèi)容,提升感官上的體驗(yàn)。
2、一般情況骨架屏和實(shí)際內(nèi)容的結(jié)構(gòu)是類似的,因此之后的切換不會(huì)過于突兀。這點(diǎn)和傳統(tǒng)的 Loading 動(dòng)圖不同,可以認(rèn)為是其升級(jí)版。
3、只需要簡(jiǎn)單的 CSS 支持 (涉及圖片懶加載可能還需要 JS ),不要求 HTTPS 協(xié)議,沒有額外的學(xué)習(xí)和維護(hù)成本。
4、如果頁面采用組件化開發(fā),每個(gè)組件可以根據(jù)自身狀態(tài)定義自身的骨架屏及其切換時(shí)機(jī),同時(shí)維持了組件之間的獨(dú)立性。
骨架屏能用在哪里
現(xiàn)在的 WEB 站點(diǎn),大致有兩種渲染模式:
前端渲染
由于最近幾年 Angular/React/Vue 的相繼推出和流行,前端渲染開始占據(jù)主導(dǎo)。這種模式的應(yīng)用也叫單頁應(yīng)用(SPA, Single Page Application)。
前端渲染的模式是服務(wù)器(多為靜態(tài)服務(wù)器)返回一個(gè)固定的 HTML。通常這個(gè) HTML 包含一個(gè)空的容器節(jié)點(diǎn),沒有其他內(nèi)容。之后內(nèi)部包含的 JS 包含路由管理,頁面渲染,頁面切換,綁定事件等等邏輯,所以稱之為前端渲染。
因?yàn)榍岸艘芾淼氖虑楹芏?,所?JS 通常很大很復(fù)雜,執(zhí)行起來也要花較多的時(shí)間。在 JS 渲染出實(shí)際內(nèi)容之前,骨架屏就是一個(gè)很好的替補(bǔ)隊(duì)員。
后端渲染
在這波前端渲染流行之前,早期的傳統(tǒng)網(wǎng)站采用的模式叫做后端渲染,即服務(wù)器直接返回網(wǎng)站的 HTML 頁面,已經(jīng)包含首頁的全部(或絕大部分) DOM 元素。其中包含的 JS 的作用大多是綁定事件,定義用戶交互后的行為等。少量會(huì)額外添加/修改一些 DOM,但無礙大局。
此外,前端渲染的模式存在 SEO 不友好的問題,因?yàn)樗祷氐?HTML 是一個(gè)空的容器。如果搜索引擎沒有執(zhí)行 JS 的能力(稱為 Deep Render),那它就不知道你的站點(diǎn)究竟是什么內(nèi)容,自然也就無法把站點(diǎn)排到搜索結(jié)果中去。這對(duì)于絕大部分站點(diǎn)來說是不可接受的,于是前端框架又相繼推出了服務(wù)端渲染(簡(jiǎn)稱 SSR, Server Side Rendering)模式。這個(gè)模式和傳統(tǒng)網(wǎng)站很接近,在于返回的 HTML 也是包含所有的 DOM,而非前端渲染。而前端 JS 除了綁定事件之外,還會(huì)多做一個(gè)事情叫做“激活”(hydration),這里就不再贅述了。
不論是傳統(tǒng)模式還是 SSR,只要是后端渲染,就不需要骨架屏。因?yàn)轫撁娴膬?nèi)容直接存在于 HTML,所以并沒有骨架屏出場(chǎng)的余地。
骨架屏怎么用
討論了一波背景,我們來看如何使用。首先先無視具體的實(shí)現(xiàn)細(xì)節(jié),先看思路。
實(shí)現(xiàn)思路
大體分為幾個(gè)步驟:
<html> <head> <style> .skeleton-wrapper { // styles } </style> <!-- 聲明 meta 或者引入其他 CSS --> </head> <body> <div id="app"> <div class="skeleton-wrapper"> <img src="data:image/svg+xml;base64,XXXXXX"> </div> </div> <!-- 引用 JS --> </body> </html>
let app = new Vue({...}) let container = document.querySelector('#app') if (container) { container.innerHTML = '' } app.$mount(container)
僅此兩步,并不牽涉多么復(fù)雜的機(jī)制和高端的 API,因此非常容易應(yīng)用,趕快用起來!
示例
我編寫了一個(gè)示例,用于快速展現(xiàn)骨架屏的效果,代碼在此。
代碼的三個(gè)文件各司其職,配合上面的實(shí)現(xiàn)思路,應(yīng)該還是很好理解的??梢栽?這里 查看效果。
因?yàn)檫@個(gè)示例的邏輯太過簡(jiǎn)單,而實(shí)際的前端渲染框架復(fù)雜得多,包含的功能也不單純是渲染,還有狀態(tài)管理,路由管理,虛擬 DOM 等等,所以文件大小和執(zhí)行時(shí)間都更大更長。我們?cè)诓榭蠢拥臅r(shí)候,把網(wǎng)絡(luò)調(diào)成 "Fast 3G" 或者 "Slow 3G" 能夠稍微真實(shí)一些。
但匪夷所思的是,對(duì)著這個(gè)地址刷新試幾次,我也基本看不到骨架屏(骨架屏的內(nèi)容是一個(gè)居中的藍(lán)色方形圖片,外加一條白色橫線反復(fù)側(cè)滑的高亮動(dòng)畫)。是我們的實(shí)現(xiàn)思路有問題嗎?
瀏覽器的奧秘:減少重排
為了排除肉眼的遺漏和干擾,我們用 Chrome Dev Tools 的 Performance 工具來記錄剛才發(fā)生了什么,截圖如下:(截圖時(shí)的網(wǎng)絡(luò)設(shè)置為 "Fast 3G")
我們可以很明顯地看到 3 個(gè)時(shí)間點(diǎn):
1、HTML 加載完成了。瀏覽器在解析 HTML 的同時(shí),發(fā)現(xiàn)了它需要引用的 2 個(gè)外部資源 index.js 和 index.css,于是發(fā)送網(wǎng)絡(luò)請(qǐng)求去獲取。
2、獲取成功后,執(zhí)行 JS 并注冊(cè) CSS 的規(guī)則。
3、JS 一執(zhí)行,很自然的渲染出了實(shí)際的內(nèi)容,并應(yīng)用了樣式規(guī)則(隨機(jī)顏色的橫條)。
我們的骨架屏呢?按照預(yù)想,骨架屏應(yīng)該出現(xiàn)在 1 和 2 之間,也就是在獲取 JS 和 CSS 的同時(shí),就應(yīng)該渲染骨架屏了。這也是我們當(dāng)時(shí)把骨架屏的 HTML 注入到 index.html, 還把 CSS 從 index.css 中分離出來的良苦用心,然而瀏覽器并不買賬。
這其實(shí)和瀏覽器的渲染順序有關(guān)。
相信大家都整理過行李箱。我們?cè)谡硇欣钕鋾r(shí),會(huì)根據(jù)每個(gè)行李的大小合理安排,大的和小的配合,填滿一層再放上面一層。現(xiàn)在突然有人跑來跟你說,你的電腦不用帶了,你要多帶兩件衣服,你不能帶那么多瓶礦泉水。除了想打他之外,為了重新整理行李箱,必然需要把整理好的行李拿出來再重新放。在瀏覽器中這個(gè)過程叫做重排 (reflow),而那個(gè)餿主意就是新加載的 CSS。顯而易見,重排的開銷是很大的。
熟能生巧,箱子理多了,就能想出解決辦法。既然每個(gè) CSS 文件加載都可能觸發(fā)重繪,那我能不能等所有 CSS 加載完了一起渲染呢?正是基于這一點(diǎn),瀏覽器會(huì)等 HTML 中所有的 CSS 都加載完,注冊(cè)完,一起應(yīng)用樣式,力求一次排列完成工作,不要反復(fù)重排??雌饋頌g覽器的設(shè)計(jì)者經(jīng)常出差,因?yàn)檫@是一個(gè)很正確的優(yōu)化思路,但應(yīng)用在骨架屏上就出了問題。
我們?yōu)榱吮M早展現(xiàn)骨架屏,把骨架屏的樣式從 index.css 分離出來。但瀏覽器不知道,它以為骨架屏的 HTML 還依賴 index.css,所以必須等它加載完。而它加載完之后,render.js 也差不多加載完開始執(zhí)行了,于是骨架屏的 HTML 又被替換了,自然就看不到了。而且在等待 JS, CSS 加載的時(shí)候依然是個(gè)白屏,骨架屏的效果大打折扣。
所以我們要做的是告訴瀏覽器,你放心大膽的先畫骨架屏,它和后面的 index.css 是無關(guān)的。那怎么告訴它呢?
告訴瀏覽器先渲染骨架屏
我們?cè)谝?CSS 時(shí),會(huì)使用 <link rel="stylesheet" href="xxxx> 這樣的語法。但實(shí)際上,瀏覽器還提供了其他一些機(jī)制確保(后續(xù))頁面的性能,我們稱之為 preload,中文叫預(yù)加載。具體來說,使用 <link rel="preload" href="xxxx">,提前把后續(xù)要使用的資源先聲明一下。在瀏覽器空閑的時(shí)候會(huì)提前加載并放入緩存。之后再使用就可以節(jié)省一個(gè)網(wǎng)絡(luò)請(qǐng)求。
這看似無關(guān)的技術(shù),在這里將起到很大的作用,因?yàn)?預(yù)加載的資源是不會(huì)影響當(dāng)前頁面的。
我們可以通過這種方式,告訴瀏覽器:先不要管 index.css,直接畫骨架屏。之后 index.css加載回來之后,再應(yīng)用這個(gè)樣式。具體來說代碼如下:
<link rel="preload" href="index.css" as="style" onload="this.rel='stylesheet'">
方法的核心是通過改變 rel 可以讓瀏覽器重新界定 <link> 標(biāo)簽的角色,從預(yù)加載變成當(dāng)頁樣式。(另外也有文章采用修改 media 的方法,但瀏覽器支持度較低,這里不作展開了。我把文章列在最后了)這樣的話,瀏覽器在 CSS 尚未獲取完成時(shí),會(huì)先渲染骨架屏(因?yàn)榇藭r(shí)的 CSS 還是 preload,也就是后續(xù)使用的,并不妨礙當(dāng)前頁面)。而當(dāng) CSS 加載完成并修改了自己的 rel之后,瀏覽器重新應(yīng)用樣式,目的達(dá)成。
不得不考慮的注意點(diǎn)
事實(shí)上,并不是把 rel="stylesheet" 改成 rel="preload" 就完事兒了。在真正應(yīng)用到生產(chǎn)環(huán)境之前,我們還有很多事情要考慮。
兼容性考慮
首先,在 <link> 內(nèi)部我們使用了 onload,也就是使用了 JS。為了應(yīng)對(duì)用戶的瀏覽器沒有開啟腳本功能的情況,我們需要添加一個(gè) fallback。(不過這點(diǎn)對(duì)于單頁應(yīng)用來說可能也無所謂,因?yàn)槿绻麤]有腳本,那頁面實(shí)際內(nèi)容也渲染不出來的)
<noscript><link rel="stylesheet" href="index.css"></noscript>
其次,rel="preload" 并不是沒有兼容性問題。對(duì)于不支持 preload 的瀏覽器,我們可以添加一些 polyfill 代碼(來使所有瀏覽器獲得一致的效果。
<script> /*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */ (function(){ ... }()); </script>
polyfill 的壓縮代碼可以參見 Lavas 的 SPA 模板第 29 行。
加載順序
不同于傳統(tǒng)頁面,我們的實(shí)際 DOM 是通過 render.js 生成的。所以如果 JS 先于 CSS 執(zhí)行,那將會(huì)發(fā)生跳動(dòng)。(因?yàn)橄蠕秩玖藢?shí)際內(nèi)容卻沒有樣式,而后樣式加載,頁面出現(xiàn)很明顯的變化)所以這里我們需要嚴(yán)格控制 CSS 早于渲染。
<link rel="preload" href="index.css" as="style" onload="this.rel='stylesheet';window.STYLE_READY=true;window.mountApp && window.mountApp()">
JS 對(duì)外暴露一個(gè) mountApp 方法用于渲染頁面(其實(shí)是模擬 Vue 的 mount)
// render.js function mountApp() { // 方法內(nèi)部就是把實(shí)際內(nèi)容添加到 <body> 上面 } // 本來直接調(diào)用方法完成渲染 // mountApp() // 改成掛到 window 由 CSS 來調(diào)用 window.mountApp = mountApp() // 如果 JS 晚于 CSS 加載完成,那直接執(zhí)行渲染。 if (window.STYLE_READY) { mountApp() }
如果 CSS 更快加載完成,那么通過設(shè)置 window.STYLE_READY 允許 JS 加載完成后直接執(zhí)行;而如果 JS 更快,則先不自己執(zhí)行,而是把機(jī)會(huì)留給 CSS 的 onload。
清空 onload
loadCSS 的開發(fā)者提出,某些瀏覽器會(huì)在 rel 改變時(shí)重新出發(fā) onload,導(dǎo)致后面的邏輯走了兩次。為了消除這個(gè)影響,我們?cè)僭?onload 里面添加一句 this.onload=null。
最終的 CSS 引用方式
<link rel="preload" href="index.css" as="style" onload="this.onload=null;this.rel='stylesheet';window.STYLE_READY=true;window.mountApp && window.mountApp()"> <!-- 為了方便閱讀,折行重復(fù)一遍 --> <!-- this.onload=null --> <!-- this.rel='stylesheet' --> <!-- window.STYLE_READY=true --> <!-- window.mountApp && window.mountApp() -->
修改后的效果
修改后的代碼在 這里,訪問地址在 這里。(為了簡(jiǎn)便,我省去了處理兼容性的代碼,即 <noscript> 和 preload polyfill)
Performance 截圖如下:(依然采用了 "Fast 3G" 的網(wǎng)絡(luò)設(shè)置)
這次在 render.js 和 index.css 還在加載的時(shí)候頁面已經(jīng)呈現(xiàn)出骨架屏的內(nèi)容,實(shí)際肉眼也可以觀測(cè)到。在截圖的情況下,骨架屏的展現(xiàn)大約持續(xù)了 300ms,占據(jù)整個(gè)網(wǎng)絡(luò)請(qǐng)求的大約一半時(shí)間。
至于說為什么不是 HTML 加載完成立馬展現(xiàn)骨架屏,而是還要等大約 300ms 才展現(xiàn),從圖上看是瀏覽器 ParseHTML 所花費(fèi)的時(shí)間,可能在 Dev Tools 打開的情況下計(jì)算資源有限,不過可優(yōu)化空間已經(jīng)不大。(可能簡(jiǎn)化骨架屏的結(jié)構(gòu)能起一些作用吧)
多骨架屏的支持
一般來說一個(gè)站點(diǎn)的所有頁面不太可能是同一種展示類型。例如說首頁和內(nèi)部頁面就展示風(fēng)格而言會(huì)很有區(qū)別,另外例如列表頁和搜索頁比較接近(可能都有列表展示),但和詳情頁(可能是商品,服務(wù),個(gè)人信息,博客文章等等)就會(huì)很不相同。但單頁應(yīng)用的 index.html 只有一個(gè),所有的變化都源自前端渲染框架在容器節(jié)點(diǎn)內(nèi)部進(jìn)行改變。所以直接將骨架屏注入到 index.html中會(huì)導(dǎo)致所有的頁面都用同一個(gè)骨架屏,那就很難達(dá)成“和實(shí)際內(nèi)容結(jié)構(gòu)類似”的目標(biāo)了,骨架屏就退化為 Loading 了。
為了要支持多種骨架屏,我們需要在 index.html 里面進(jìn)行判斷邏輯(獨(dú)立于主體 JS 之外),具體來說:
1、把所有種類的骨架屏的 HTML 和樣式全部寫入 index.html
2、在 index.html 底下新增內(nèi)聯(lián)的腳本 <script>,根據(jù)當(dāng)前路由判斷應(yīng)該展示哪一個(gè)骨架屏
這樣會(huì)導(dǎo)致 index.html 體積變大一點(diǎn),但整體感覺依然是收益大于付出,我認(rèn)為是值得的。
后記
這個(gè)優(yōu)化點(diǎn)最早由我的前同事 xiaop 同學(xué) 在開發(fā) Lavas 的 SPA 模板中發(fā)現(xiàn)并完成的,Issue 記錄在此。我在他的基礎(chǔ)上,做了一個(gè)分離 Lavas 和 Vue 環(huán)境并且更直白的例子,讓截圖也盡可能易于理解,方便閱讀。在此非常感謝他的工作!
另外骨架屏的編寫我全部采用的是純粹的手寫 HTML 和 CSS,不止展現(xiàn)邏輯,包括開發(fā)流程也是獨(dú)立于單頁應(yīng)用其他常規(guī)頁面的。當(dāng)然這可能給開發(fā)者帶來一點(diǎn)不便,所以這時(shí)候需要推出 xiaop 同學(xué)的利器:vue-skeleton-webpack-plugin https://github.com/lavas-project/vue-skeleton-webpack-plugin。它的作用是把骨架屏本身也當(dāng)成一個(gè) Vue 組件,配上單獨(dú)的路由規(guī)則來統(tǒng)一在 Vue 項(xiàng)目中的開發(fā)體驗(yàn),最后使用 webpack 在打包構(gòu)建的時(shí)候加以區(qū)分并注入,對(duì)于使用 Vue + webpack 開發(fā)的同學(xué)來說可以一試。
參考文章
轉(zhuǎn)自作者作者:小蘑菇小哥https://zhuanlan.zhihu.com/p/48601348
素的顯示與隱藏
使用CSS讓元素不可見的方法很多,剪裁、定位到屏幕外、透明度變化等都是可以的。雖然它們都是肉眼看不見,但背后卻在多個(gè)維度上都有差別
下面是總結(jié)的一些比較好的隱藏實(shí)踐,大家一起來根據(jù)實(shí)際開發(fā)場(chǎng)景來選擇合適的使用
比較好的隱藏實(shí)踐
不占空間,資源可以加載,DOM可訪問 使用display:none
不占空間,隱藏顯示時(shí)有transition效果
占空間,不能點(diǎn)擊 visibility: hidden
不占空間,不能點(diǎn)擊,鍵盤能訪問 clip裁切
占空間,不能點(diǎn)擊,鍵盤能訪問 relative
占空間,可以點(diǎn)擊 opacity
隱藏文字 使用text-indent
根據(jù)實(shí)際的隱藏場(chǎng)景選擇合適的隱藏方法,這里就不再多說了,接著往下看吧
display與元素的顯隱
我們都知道display如果值為none,則該元素以及所有后代元素都隱藏,反之如果值是非none的情況,則都為顯示了
display可以說是web顯隱交互中出場(chǎng)頻率最高的一種隱藏方式,是真正意義上的隱藏,干凈利落,不留痕跡
none做到了無法點(diǎn)擊、無法使用屏幕閱讀器等輔助設(shè)備訪問,不占空間,其實(shí)不僅僅是這樣,更應(yīng)該知道的是
me: 我有酒,那么別說你沒有故事
我知道display:none你才不是一個(gè)沒有故事的女同學(xué)
display: none的元素的background-image圖片根據(jù)不同瀏覽器的情況加載情況不一
在Firefox瀏覽器下,display:none的background-image圖片不加載,包括父元素display:none也是如此在Chrome和Safari瀏覽器,則根據(jù)父元素是否是否為none來影響圖片加載情況,父元素帶有display:none,圖片不加載。
父元素不帶有display:none,而自身有背景圖元素帶的話,那也照樣加載
3.在IE瀏覽器下,無論怎么搞都會(huì)請(qǐng)求圖片資源,就是這么任性
因此,在實(shí)際開發(fā)的時(shí)候,例如頭圖輪播切換效果
那些默認(rèn)需要隱藏的圖片作為背景圖藏在display:none元素的子元素上,這樣的細(xì)小改動(dòng)就可以明顯提升頁面的加載體驗(yàn),也是非常實(shí)用的小技巧
whatever
上面說的興致盎然,但實(shí)際中不可能全部都是背景圖去加載圖片資源的
還有另外一個(gè)好朋友,img元素,然并卵的是,上面說了一大堆加載不加載的情況,對(duì)img來說沒個(gè)鳥用,人家不管你none不none的,依舊帶著勇闖天涯的氣概去請(qǐng)求著資源
活久見
都說display:none做事最純粹,最干凈,不能被點(diǎn)擊,觸碰到,然而下面這種情況又是什么鬼?
出來解釋解釋,我們都是文明人是絕對(duì)不會(huì)動(dòng)武的!
隱藏的按鈕會(huì)觸發(fā)click,觸發(fā)表單提交,此現(xiàn)象出現(xiàn)在時(shí)髦的瀏覽器中(IE9+,現(xiàn)代標(biāo)準(zhǔn)瀏覽器中)
既然有這種例外情況那加了display:none的意義又是什么呢?
很多都是純天然的
HTML中有很多標(biāo)簽和屬性天然自帶display:none
HTML5中新增了hidden這個(gè)布爾屬性,可以讓元素天生隱藏起來
既然說到了visibility了,那么就趕緊邀請(qǐng)visibility閃亮登場(chǎng)吧
visibility與元素的顯隱
visibility要為自己正名,不僅僅是保留空間這么簡(jiǎn)單
看點(diǎn)多多:
繼承性(最有意思的一個(gè)特點(diǎn),不是我說的)
2. 與css計(jì)數(shù)器
visibility:hidden雖然讓元素不可見了,但是不影響其計(jì)數(shù)效果,不會(huì)重新計(jì)算結(jié)果
3. 與transition
設(shè)置了visibility:hidden的元素,可以很好的展現(xiàn)transition過渡效果
這是因?yàn)閠ransition支持的css屬性中有visibility(果然是兄弟),而并沒有display屬性
4.與JS
visibility:hidden除了對(duì)transition友好外,對(duì)js來說也很友好
在實(shí)際開發(fā)中,需要對(duì)隱藏元素進(jìn)行尺寸和位置的獲取,來實(shí)現(xiàn)布局精確定位的交互
此時(shí),就建議使用visibility:hidden
好了以上內(nèi)容要告一段落了,我們繼續(xù)開始新的征程吧,哈哈
用戶界面樣式
用戶界面樣式指的是CSS世界中用來幫助用戶進(jìn)行界面交互的一些CSS樣式,主要有outline和cursor等屬性
和border形似的outline屬性
outline表示元素的輪廓,語法也和border一樣,分為寬度、類型和顏色三個(gè)值
樣式表示上相同,但是設(shè)計(jì)的初衷卻是不太相同的,這一點(diǎn)天地日月可鑒
outline是一個(gè)和用戶體驗(yàn)密切相關(guān)的屬性,與focus狀態(tài)以及鍵盤訪問密切相關(guān)
對(duì)于按鈕或鏈接,通常的鍵盤操作是:Tab鍵按次序不斷focus控件元素(鏈接、按鈕、輸入框等表單元素),或者focus設(shè)置了tabindex的普通元素,然后按Shift+Tab是反向訪問
重點(diǎn)來了!
默認(rèn)狀態(tài)下,對(duì)于處于focus狀態(tài)的元素,瀏覽器會(huì)通過發(fā)光or虛框的形式進(jìn)行區(qū)分和提示,這是友好的用戶體驗(yàn),很有必要,不然用戶很難知道自己當(dāng)前聚焦在了哪個(gè)元素上面,會(huì)迷失自我
元素如果聚焦到了a鏈接上,按下回車鍵就會(huì)跳轉(zhuǎn)到相應(yīng)鏈接,以上的交互都是基于鍵盤訪問的,這就是為什么outline和鍵盤訪問如此親密了
不專業(yè)的行為
很多時(shí)候直接在reset樣式的時(shí)候,寫成如下形式是非常不可取的
這樣直接一竿子打死一群鴨子的做法是不對(duì)的,更多的時(shí)候是因?yàn)闉g覽器內(nèi)置的focus效果和設(shè)計(jì)風(fēng)格格格不入,才需要重置,而且要使用專門的類名
最后再強(qiáng)調(diào)一遍:萬萬不可在全局設(shè)置outline: 0 none;
這樣的操作會(huì)造成鍵盤訪問的時(shí)候用戶找不到當(dāng)前焦點(diǎn),容易產(chǎn)生困擾的,為了大家好,收斂一下吧
下面來點(diǎn)干貨: 在實(shí)際開發(fā)中,有時(shí)候需要讓普通元素代替表單控件元素有outline效果
舉個(gè)栗子:submit按鈕來完成UI設(shè)計(jì)是非常麻煩的,所以使用label元素來移花接木,通過for屬性和這些原生的表單控件相關(guān)聯(lián)
真正的不占據(jù)空間的outline及其應(yīng)用
outline是一個(gè)真正意義上不占任何空間的屬性,Amazing
頭像剪裁的矩形鏤空效果
先來看個(gè)效果圖
上圖就是矩形鏤空效果,那么下面直接上代碼,滿滿的干貨
用一個(gè)大大的outline來實(shí)現(xiàn)周圍半透明的黑色遮罩,因?yàn)閛utline無論設(shè)置多么多么大,都不會(huì)占據(jù)空間影響布局,至于超出的部分,直接給父元素設(shè)置一個(gè)overflow:hidden就搞定了 注意:
自動(dòng)填滿屏幕剩余空間的應(yīng)用技巧
開發(fā)中很多時(shí)候,由于頁面內(nèi)容不夠多,導(dǎo)致底部footer會(huì)出現(xiàn)尷尬的剩余空間,解決方法往往也有很多種,在此我們還是依然利用outline的功能來完美實(shí)現(xiàn)一下
關(guān)鍵的css就是設(shè)置一個(gè)超大輪廓范圍的outline屬性,如給個(gè)9999px,保證無論屏幕多高,輪廓顏色都能覆蓋
值得注意的是,outline無法指定方位,它是直接向四周發(fā)散的,所以需要配合clip剪裁來進(jìn)行處理,以左邊和上邊為邊界進(jìn)行裁剪
光標(biāo)屬性
光標(biāo)屬性cursor我們真的是最熟悉的陌生人啊
為什么這么說呢,因?yàn)樵诒姸嗟膶傩灾得媲埃覀兯坪踔挥玫搅藀ointer(手形)(最常用的,沒有之一),move(移動(dòng)),default(系統(tǒng)默認(rèn))這幾樣
在cursor的世界里,遠(yuǎn)比我們想象的要豐富很多,下面按照功能特性來對(duì)其進(jìn)行分類吧
琳瑯滿目的cursor屬性值
友情不友情的小提示:☆(表示常用)
鏈接和狀態(tài)
cursor: progress; 進(jìn)行中
選擇
拖拽都是CSS3新增的光標(biāo)類型
以上內(nèi)容就介紹完了用戶界面樣式的全部?jī)?nèi)容了,還有最后一章的冷知識(shí),大家不要方,繼續(xù)看下去,了解一下,了解一下,了解一下
流向的改變
說出來你可能不信,direction可以改變水平流向,盡管知道或者使用過的人少之又少,但并不妨礙它的發(fā)光發(fā)熱
而且屬性簡(jiǎn)單好記,值少,兼容極好ie6支持,可以來挖掘一下它的神奇功效
direction
僅僅兩個(gè)值:
direction: rtl;
當(dāng)然看到這里你可能會(huì)感覺,這些說起來都沒什么鳥用,因?yàn)榇笳惺遣惠p易放出的,而真正有用的地方在于改變網(wǎng)頁布局的時(shí)候
direction屬性默認(rèn)有一個(gè)特性
可以改變替換元素(img,input,textarea,select)或inline-block/inline-table元素的水平呈現(xiàn)順序
舉個(gè)例子:顛倒順序
再舉個(gè)例子:
比如制作彈窗組件的時(shí)候,確認(rèn)和取消按鈕有的時(shí)候會(huì)根據(jù)用戶的使用行為會(huì)顯示在不同的位置
下面來看看這種特性的表現(xiàn)在實(shí)際開發(fā)中的作用
windows用戶看到的樣子
好了,direction的話題就告一段落,接下來介紹最后一個(gè)知識(shí)了,堅(jiān)持住,快休息了
writing-mode
改變CSS世界縱橫規(guī)則的writing-mode,如此強(qiáng)大的功能,居然沒有被大家發(fā)掘和廣發(fā)應(yīng)用起來,實(shí)屬遺憾了,話不多說,往下看
writing-mode作用及真正需要關(guān)注的屬性值
writing-mode可以改變排版,變成垂直流,如下圖所示
在使用語法上,也是需要記兩套的,一套是IE私有屬性,一套是CSS3規(guī)范屬性
CSS3語法:
IE語法:
針對(duì)實(shí)戰(zhàn)版來整理一份writing-mode是這樣的
對(duì)于垂直排版來說,實(shí)際開發(fā)是很少會(huì)遇到的,不過還是要說說writing-mode帶來的改變
水平方向也能margin合并
我們都知道兩個(gè)相鄰的元素垂直的margin會(huì)合并,當(dāng)元素變?yōu)榇怪绷鞯臅r(shí)候,水平的margin也會(huì)合并
普通塊元素可以使用margin: auto實(shí)現(xiàn)垂直居中
text-align:center實(shí)現(xiàn)圖片垂直居中(同上實(shí)現(xiàn)的效果)
實(shí)現(xiàn)全兼容的icon fonts圖標(biāo)旋轉(zhuǎn)效果
老IE下讓小圖標(biāo)旋轉(zhuǎn)很麻煩,writing-mode把文檔變成垂直流的時(shí)候,英文、數(shù)字和字符號(hào)都天然的轉(zhuǎn)了90°
@font-face的兼容性很好IE5.5就支持了,所以就算是IE6和IE7也沒問題
好了,這就是《CSS世界》里最后三章的全部?jī)?nèi)容了,終于寫完了,哈哈,希望大家有收獲一些冷知識(shí)。
簡(jiǎn)單說兩句
做個(gè)個(gè)人的小總結(jié)吧:
css有很多奇妙的地方,在某些特性當(dāng)初被設(shè)計(jì)出來的時(shí)候可能只是為了某些圖文排版而生
但是我們可以利用它們帶來的特性發(fā)揮自己的創(chuàng)造力,實(shí)現(xiàn)其他很多意想不到的效果,因此,上面所講述的所有知識(shí)點(diǎn),盡管很多內(nèi)容都有點(diǎn)奇技淫巧以悅婦孺的過程
但這也給我們開發(fā)的過程中,提供了一些很出奇的妙招,值得我們好好學(xué)習(xí)領(lǐng)悟
感謝個(gè)位的觀看了,再見了,哈哈
者開源了一個(gè)Web思維導(dǎo)圖mind-map,最近在優(yōu)化背景圖片效果的時(shí)候遇到了一個(gè)問題,頁面上展示時(shí)背景圖片是通過css使用background-image渲染的,而導(dǎo)出的時(shí)候?qū)嶋H上是繪制到canvas上導(dǎo)出的,那么就會(huì)有個(gè)問題,css的背景圖片支持比較豐富的效果,比如通過background-size設(shè)置大小,通過background-position設(shè)置位置,通過background-repeat設(shè)置重復(fù),但是canvas筆者只找到一個(gè)createPattern()方法,且只支持設(shè)置重復(fù)效果,那么如何在canvas里模擬一定的css背景效果呢,不要走開,接下來一起來試試。
首先要說明的是不會(huì)去完美完整100%模擬css的所有效果,因?yàn)?span style="color: #1E6BB8; --tt-darkmode-color: #1E6BB8;">css太強(qiáng)大了,屬性值組合很靈活,且種類非常多,其中單位就很多種,所有只會(huì)模擬一些常見的情況,單位也只考慮px和%。
讀完本文,你還可以順便復(fù)習(xí)一下canvas的drawImage方法,以及css背景設(shè)置的幾個(gè)屬性的用法。
總的來說,我們會(huì)使用canvas的drawImage()方法來繪制背景圖片,先來大致看一下這個(gè)方法,這個(gè)方法接收的參數(shù)比較多:
只有三個(gè)參數(shù)是必填的。
核心邏輯就是加載圖片,然后使用drawImage方法繪制圖片,無非是根據(jù)各種css的屬性和值來計(jì)算drawImage的參數(shù),所以可以寫出下面的函數(shù)基本框架:
const drawBackgroundImageToCanvas = (
ctx,// canvas繪圖上下文
width,// canvas寬度
height,// canvas高度
img,// 圖片url
{ backgroundSize, backgroundPosition, backgroundRepeat }// css樣式,只模擬這三種
) => {
// canvas的寬高比
let canvasRatio = width / height
// 加載圖片
let image = new Image()
image.src = img
image.onload = () => {
// 圖片的寬高及寬高比
let imgWidth = image.width
let imgHeight = image.height
let imageRatio = imgWidth / imgHeight
// 繪制圖片
// drawImage方法的參數(shù)值
let drawOpt = {
sx: 0,
sy: 0,
swidth: imgWidth,// 默認(rèn)繪制完整圖片
sheight: imgHeight,
x: 0,
y: 0,
width: imgWidth,// 默認(rèn)不縮放圖片
height: imgHeight
}
// 根據(jù)css屬性和值計(jì)算...
// 繪制圖片
ctx.drawImage(image, drawOpt.sx, drawOpt.sy, drawOpt.swidth, drawOpt.sheight, drawOpt.x, drawOpt.y, drawOpt.width, drawOpt.height)
}
}
接下來看幾個(gè)工具函數(shù)。
// 將以空格分隔的字符串值轉(zhuǎn)換成成數(shù)字/單位/值數(shù)組
const getNumberValueFromStr = value => {
let arr = String(value).split(/\s+/)
return arr.map(item => {
if (/^[\d.]+/.test(item)) {
// 數(shù)字+單位
let res = /^([\d.]+)(.*)$/.exec(item)
return [Number(res[1]), res[2]]
} else {
// 單個(gè)值
return item
}
})
}
css的屬性值為字符串或數(shù)字類型,比如100px 100% auto,不方便直接使用,所以轉(zhuǎn)換成[[100, 'px'], [100, '%'], 'auto']形式。
// 縮放寬度
const zoomWidth = (ratio, height) => {
// w / height = ratio
return ratio * height
}
// 縮放高度
const zoomHeight = (ratio, width) => {
// width / h = ratio
return width / ratio
}
根據(jù)原比例和新的寬度或高度,計(jì)算縮放后的寬度或高度。
默認(rèn)background-repeat的值為repeat,我們先不考慮重復(fù)的情況,所以先把它設(shè)置成no-repeat。
background-size 屬性用于設(shè)置背景圖片的大小,可以接受四種類型的值,依次來模擬一下。
設(shè)置背景圖片的高度和寬度。第一個(gè)值設(shè)置寬度,第二個(gè)值設(shè)置高度。如果只給出一個(gè)值,第二個(gè)默認(rèn)為 auto(自動(dòng))。
css樣式如下:
.cssBox {
background-image: url('/1.jpg');
background-repeat: no-repeat;
background-size: 300px;
}
只設(shè)置一個(gè)值,那么代表背景圖片顯示的實(shí)際寬度,高度沒有設(shè)置,那么會(huì)根據(jù)圖片的長寬比自動(dòng)縮放,效果如下:
在canvas中模擬很簡(jiǎn)單,需要傳給drawImage方法四個(gè)參數(shù):img、x、y、width、height,img代表圖片,x、y代表在畫布上放置圖片的位置,沒有特殊設(shè)置,顯然就是0、0,width、height代表將圖片縮放到指定大小,如果background-size只傳了一個(gè)值,那么width直接設(shè)置成這個(gè)值,而height則根據(jù)圖片的長寬比進(jìn)行計(jì)算,如果傳了兩個(gè)值,那么分別把兩個(gè)值傳給width、height即可,另外需要對(duì)值為auto的進(jìn)行一下處理,實(shí)現(xiàn)如下:
drawBackgroundImageToCanvas(ctx, width, height, this.img, {
backgroundSize: '300px'
})
const drawBackgroundImageToCanvas = () =>{
// ...
image.onload = () => {
// ...
// 模擬background-size
handleBackgroundSize({
backgroundSize,
drawOpt,
imageRatio
})
// ...
}
}
// 模擬background-size
const handleBackgroundSize = ({ backgroundSize, drawOpt, imageRatio }) => {
if (backgroundSize) {
// 將值轉(zhuǎn)換成數(shù)組
let backgroundSizeValueArr = getNumberValueFromStr(backgroundSize)
// 兩個(gè)值都為auto,那就相當(dāng)于不設(shè)置
if (backgroundSizeValueArr[0] === 'auto' && backgroundSizeValueArr[1] === 'auto') {
return
}
// 圖片寬度
let newNumberWidth = -1
if (backgroundSizeValueArr[0]) {
if (Array.isArray(backgroundSizeValueArr[0])) {
// 數(shù)字+單位類型
drawOpt.width = backgroundSizeValueArr[0][0]
newNumberWidth = backgroundSizeValueArr[0][0]
} else if (backgroundSizeValueArr[0] === 'auto') {
// auto類型,那么根據(jù)設(shè)置的新高度以圖片原寬高比進(jìn)行自適應(yīng)
if (backgroundSizeValueArr[1]) {
drawOpt.width = zoomWidth(imageRatio, backgroundSizeValueArr[1][0])
}
}
}
// 設(shè)置了圖片高度
if (backgroundSizeValueArr[1] && Array.isArray(backgroundSizeValueArr[1])) {
// 數(shù)字+單位類型
drawOpt.height = backgroundSizeValueArr[1][0]
} else if (newNumberWidth !== -1) {
// 沒有設(shè)置圖片高度或者設(shè)置為auto,那么根據(jù)設(shè)置的新寬度以圖片原寬高比進(jìn)行自適應(yīng)
drawOpt.height = zoomHeight(imageRatio, newNumberWidth)
}
}
}
效果如下:
設(shè)置兩個(gè)值的效果:
background-size: 300px 400px;
將計(jì)算相對(duì)于背景定位區(qū)域的百分比。第一個(gè)值設(shè)置寬度百分比,第二個(gè)值設(shè)置的高度百分比。如果只給出一個(gè)值,第二個(gè)默認(rèn)為auto(自動(dòng))。比如設(shè)置了50% 80%,意思是將圖片縮放到背景區(qū)域的50%寬度和80%高度。
css樣式如下:
.cssBox {
background-image: url('/1.jpg');
background-repeat: no-repeat;
background-size: 50% 80%;
}
實(shí)現(xiàn)也很簡(jiǎn)單,在前面的基礎(chǔ)上判斷一下單位是否是%,是的話就按照canvas的寬高來計(jì)算圖片要顯示的寬高,第二值沒有設(shè)置或者為auto,跟之前一樣也是根據(jù)圖片的寬高比來自適應(yīng)。
drawBackgroundImageToCanvas(ctx, width, height, this.img, {
backgroundSize: '50% 80%'
})
handleBackgroundSize({
backgroundSize,
drawOpt,
imageRatio,
canvasWidth: width,// 傳參新增canvas的寬高
canvasHeight: height
})
// 模擬background-size
const handleBackgroundSize = ({ backgroundSize, drawOpt, imageRatio, canvasWidth, canvasHeight }) => {
if (backgroundSize) {
// ...
// 圖片寬度
let newNumberWidth = -1
if (backgroundSizeValueArr[0]) {
if (Array.isArray(backgroundSizeValueArr[0])) {
// 數(shù)字+單位類型
if (backgroundSizeValueArr[0][1] === '%') {
// %單位,則圖片顯示的高度為畫布的百分之多少
drawOpt.width = backgroundSizeValueArr[0][0] / 100 * canvasWidth
newNumberWidth = drawOpt.width
} else {
// 其他都認(rèn)為是px單位
drawOpt.width = backgroundSizeValueArr[0][0]
newNumberWidth = backgroundSizeValueArr[0][0]
}
} else if (backgroundSizeValueArr[0] === 'auto') {
// auto類型,那么根據(jù)設(shè)置的新高度以圖片原寬高比進(jìn)行自適應(yīng)
if (backgroundSizeValueArr[1]) {
if (backgroundSizeValueArr[1][1] === '%') {
// 高度為%單位
drawOpt.width = zoomWidth(imageRatio, backgroundSizeValueArr[1][0] / 100 * canvasHeight)
} else {
// 其他都認(rèn)為是px單位
drawOpt.width = zoomWidth(imageRatio, backgroundSizeValueArr[1][0])
}
}
}
}
// 設(shè)置了圖片高度
if (backgroundSizeValueArr[1] && Array.isArray(backgroundSizeValueArr[1])) {
// 數(shù)字+單位類型
if (backgroundSizeValueArr[1][1] === '%') {
// 高度為%單位
drawOpt.height = backgroundSizeValueArr[1][0] / 100 * canvasHeight
} else {
// 其他都認(rèn)為是px單位
drawOpt.height = backgroundSizeValueArr[1][0]
}
} else if (newNumberWidth !== -1) {
// 沒有設(shè)置圖片高度或者設(shè)置為auto,那么根據(jù)設(shè)置的新寬度以圖片原寬高比進(jìn)行自適應(yīng)
drawOpt.height = zoomHeight(imageRatio, newNumberWidth)
}
}
}
效果如下:
background-size設(shè)置為cover代表圖片會(huì)保持原來的寬高比,并且縮放成將完全覆蓋背景定位區(qū)域的最小大小,注意,圖片不會(huì)變形。
css樣式如下:
.cssBox {
background-image: url('/3.jpeg');
background-repeat: no-repeat;
background-size: cover;
}
這個(gè)實(shí)現(xiàn)也很簡(jiǎn)單,根據(jù)圖片的寬高比和canvas的寬高比判斷,到底是縮放圖片的寬度和canvas的寬度一致,還是縮放圖片的高度和canvas的高度一致。
drawBackgroundImageToCanvas(ctx, width, height, this.img, {
backgroundSize: 'cover'
})
handleBackgroundSize({
backgroundSize,
drawOpt,
imageRatio,
canvasWidth: width,
canvasHeight: height,
canvasRatio// 參數(shù)增加canvas的寬高比
})
const handleBackgroundSize = ({
backgroundSize,
drawOpt,
imageRatio,
canvasWidth,
canvasHeight,
canvasRatio
}) => {
// ...
// 值為cover
if (backgroundSizeValueArr[0] === 'cover') {
if (imageRatio > canvasRatio) {
// 圖片的寬高比大于canvas的寬高比,那么圖片高度縮放到和canvas的高度一致,寬度自適應(yīng)
drawOpt.height = canvasHeight
drawOpt.width = zoomWidth(imageRatio, canvasHeight)
} else {
// 否則圖片寬度縮放到和canvas的寬度一致,高度自適應(yīng)
drawOpt.width = canvasWidth
drawOpt.height = zoomHeight(imageRatio, canvasWidth)
}
return
}
// ...
}
效果如下:
background-size設(shè)置為contain類型表示圖片還是會(huì)保持原有的寬高比,并且縮放成適合背景定位區(qū)域的最大大小,也就是圖片會(huì)顯示完整,但是不一定會(huì)鋪滿背景的水平和垂直兩個(gè)方向,在某個(gè)方向可能會(huì)有留白。
css樣式:
.cssBox {
background-image: url('/1.jpg');
background-repeat: no-repeat;
background-size: contain;
}
實(shí)現(xiàn)剛好和cover類型的實(shí)現(xiàn)反過來即可,如果圖片的寬高比大于canvas的寬高比,為了讓圖片顯示完全,讓圖片的寬度和canvas的寬度一致,高度自適應(yīng)。
const handleBackgroundSize = () => {
// ...
// 值為contain
if (backgroundSizeValueArr[0] === 'contain') {
if (imageRatio > canvasRatio) {
// 圖片的寬高比大于canvas的寬高比,那么圖片寬度縮放到和canvas的寬度一致,高度自適應(yīng)
drawOpt.width = canvasWidth
drawOpt.height = zoomHeight(imageRatio, canvasWidth)
} else {
// 否則圖片高度縮放到和canvas的高度一致,寬度自適應(yīng)
drawOpt.height = canvasHeight
drawOpt.width = zoomWidth(imageRatio, canvasHeight)
}
return
}
}
效果如下:
到這里對(duì)background-size的模擬就結(jié)束了,接下來看看background-position。
先看不設(shè)置background-size的情況。
background-position屬性用于設(shè)置背景圖像的起始位置,默認(rèn)值為0% 0%,它也支持幾種不同類型的值,一一來看。
第一個(gè)值設(shè)置水平位置,第二個(gè)值設(shè)置垂直位置。左上角是0%0%,右下角是100%100%,如果只設(shè)置了一個(gè)值,第二個(gè)默認(rèn)為50%,比如設(shè)置為50% 60%,意思是將圖片的50% 60%位置和背景區(qū)域的50% 60%位置進(jìn)行對(duì)齊,又比如50% 50%,代表圖片中心點(diǎn)和背景區(qū)域中心點(diǎn)重合。
css樣式:
.cssBox {
background-image: url('/2.jpg');
background-repeat: no-repeat;
background-position: 50% 50%;
}
實(shí)現(xiàn)上我們只需要用到drawImage方法的img、x、y三個(gè)參數(shù),圖片的寬高不會(huì)進(jìn)行縮放,根據(jù)比例分別算出在canvas和圖片上對(duì)應(yīng)的距離,他們的差值即為圖片在canvas上顯示的位置。
drawBackgroundImageToCanvas(ctx, width, height, this.img, {
backgroundPosition: '50% 50%'
})
const drawBackgroundImageToCanvas = () => {
// ...
// 模擬background-position
handleBackgroundPosition({
backgroundPosition,
drawOpt,
imgWidth,
imgHeight,
canvasWidth: width,
canvasHeight: height
})
// ...
}
// 模擬background-position
const handleBackgroundPosition = ({
backgroundPosition,
drawOpt,
imgWidth,
imgHeight,
canvasWidth,
canvasHeight
}) => {
if (backgroundPosition) {
// 將值轉(zhuǎn)換成數(shù)組
let backgroundPositionValueArr = getNumberValueFromStr(backgroundPosition)
if (Array.isArray(backgroundPositionValueArr[0])) {
if (backgroundPositionValueArr.length === 1) {
// 如果只設(shè)置了一個(gè)值,第二個(gè)默認(rèn)為50%
backgroundPositionValueArr.push([50, '%'])
}
// 水平位置
if (backgroundPositionValueArr[0][1] === '%') {
// 單位為%
let canvasX = (backgroundPositionValueArr[0][0] / 100) * canvasWidth
let imgX = (backgroundPositionValueArr[0][0] / 100) * imgWidth
// 計(jì)算差值
drawOpt.x = canvasX - imgX
}
// 垂直位置
if (backgroundPositionValueArr[1][1] === '%') {
// 單位為%
let canvasY = (backgroundPositionValueArr[1][0] / 100) * canvasHeight
let imgY = (backgroundPositionValueArr[1][0] / 100) * imgHeight
// 計(jì)算差值
drawOpt.y = canvasY - imgY
}
}
}
}
效果如下:
第一個(gè)值代表水平位置,第二個(gè)值代表垂直位置。左上角是0 0。單位可以是px或任何其他css單位,當(dāng)然,我們只考慮px。如果僅指定了一個(gè)值,其他值將是50%。所以你可以混合使用%和px。
css樣式:
.cssBox {
background-image: url('/2.jpg');
background-repeat: no-repeat;
background-position: 50px 150px;
}
這個(gè)實(shí)現(xiàn)更簡(jiǎn)單,直接把值傳給drawImage的x、y參數(shù)即可。
drawBackgroundImageToCanvas(ctx, width, height, this.img, {
backgroundPosition: '50px 150px'
})
// 模擬background-position
const handleBackgroundPosition = ({}) => {
// ...
// 水平位置
if (backgroundPositionValueArr[0][1] === '%') {
// ...
} else {
// 其他單位默認(rèn)都為px
drawOpt.x = backgroundPositionValueArr[0][0]
}
// 垂直位置
if (backgroundPositionValueArr[1][1] === '%') {
// ...
} else {
// 其他單位默認(rèn)都為px
drawOpt.y = backgroundPositionValueArr[1][0]
}
}
也就是通過left、top之類的關(guān)鍵詞進(jìn)行組合,比如:left top、center center、center bottom等。可以看做是特殊的%值,所以我們只要寫一個(gè)映射將這些關(guān)鍵詞對(duì)應(yīng)上百分比數(shù)值即可。
.cssBox {
background-image: url('/2.jpg');
background-repeat: no-repeat;
background-position: right bottom;
}
drawBackgroundImageToCanvas(ctx, width, height, this.img, {
backgroundPosition: 'right bottom'
})
// 關(guān)鍵詞到百分比值的映射
const keyWordToPercentageMap = {
left: 0,
top: 0,
center: 50,
bottom: 100,
right: 100
}
const handleBackgroundPosition = ({}) => {
// ...
// 將關(guān)鍵詞轉(zhuǎn)為百分比
backgroundPositionValueArr = backgroundPositionValueArr.map(item => {
if (typeof item === 'string') {
return keyWordToPercentageMap[item] !== undefined
? [keyWordToPercentageMap[item], '%']
: item
}
return item
})
// ...
}
最后我們來看看和background-size組合使用會(huì)發(fā)生什么情況。
.cssBox {
background-image: url('/2.jpg');
background-repeat: no-repeat;
background-size: cover;
background-position: right bottom;
}
drawBackgroundImageToCanvas(ctx, width, height, this.img, {
backgroundSize: 'cover',
backgroundPosition: 'right bottom'
})
結(jié)果如下:
不一致,這是為啥呢,我們來梳理一下,首先在處理background-size會(huì)計(jì)算出drawImage參數(shù)中的width、height,也就是圖片在canvas中顯示的寬高,而在處理background-position時(shí)會(huì)用到圖片的寬高,但是我們傳的還是圖片的原始寬高,這樣計(jì)算出來當(dāng)然是有問題的,修改一下:
// 模擬background-position
handleBackgroundPosition({
backgroundPosition,
drawOpt,
imgWidth: drawOpt.width,// 改為傳計(jì)算后的圖片的顯示寬高
imgHeight: drawOpt.height,
imageRatio,
canvasWidth: width,
canvasHeight: height,
canvasRatio
})
現(xiàn)在再來看看效果:
background-repeat屬性用于設(shè)置如何平鋪對(duì)象的background-image屬性,默認(rèn)值為repeat,也就是當(dāng)圖片比背景區(qū)域小時(shí)默認(rèn)會(huì)向垂直和水平方向重復(fù),另外還有幾個(gè)可選值:
接下來我們實(shí)現(xiàn)一下這幾種情況。
首先判斷圖片的寬高是否都比背景區(qū)域大,是的話就不需要平鋪,也就不用處理,另外值為no-repeat也不需要做處理:
// 模擬background-repeat
handleBackgroundRepeat({
backgroundRepeat,
drawOpt,
imgWidth: drawOpt.width,
imgHeight: drawOpt.height,
imageRatio,
canvasWidth: width,
canvasHeight: height,
canvasRatio
})
可以看到這里我們傳的圖片的寬高也是經(jīng)background-size計(jì)算后的圖片顯示寬高。
// 模擬background-repeat
const handleBackgroundRepeat = ({
backgroundRepeat,
drawOpt,
imgWidth,
imgHeight,
canvasWidth,
canvasHeight,
}) => {
if (backgroundRepeat) {
// 將值轉(zhuǎn)換成數(shù)組
let backgroundRepeatValueArr = getNumberValueFromStr(backgroundRepeat)
// 不處理
if (backgroundRepeatValueArr[0] === 'no-repeat' || (imgWidth >= canvasWidth && imgHeight >= canvasHeight)) {
return
}
}
}
接下來增加對(duì)repeat-x的支持,當(dāng)canvas的寬度大于圖片的寬度,那么水平平鋪進(jìn)行繪制,繪制會(huì)重復(fù)調(diào)用drawImage方法,所以還需要再傳遞ctx、image參數(shù)給handleBackgroundRepeat方法,另外如果handleBackgroundRepeat方法里進(jìn)行了繪制,原來的繪制方法就不用再調(diào)用了:
// 模擬background-repeat
// 如果在handleBackgroundRepeat里進(jìn)行了繪制,那么會(huì)返回true
let notNeedDraw = handleBackgroundRepeat({
ctx,
image,
...
})
if (!notNeedDraw) {
drawImage(ctx, image, drawOpt)
}
// 根據(jù)參數(shù)繪制圖片
const drawImage = (ctx, image, drawOpt) => {
ctx.drawImage(
image,
drawOpt.sx,
drawOpt.sy,
drawOpt.swidth,
drawOpt.sheight,
drawOpt.x,
drawOpt.y,
drawOpt.width,
drawOpt.height
)
}
將繪制的方法提取成了一個(gè)方法,方便復(fù)用。
const handleBackgroundRepeat = ({}) => {
// ...
// 水平平鋪
if (backgroundRepeatValueArr[0] === 'repeat-x') {
if (canvasWidth > imgWidth) {
let x = 0
while (x < canvasWidth) {
drawImage(ctx, image, {
...drawOpt,
x
})
x += imgWidth
}
return true
}
}
// ...
}
每次更新圖片的放置位置x參數(shù),直到超出canvas的寬度。
對(duì)repeat-y的處理也是類似的:
const handleBackgroundRepeat = ({}) => {
// ...
// 垂直平鋪
if (backgroundRepeatValueArr[0] === 'repeat-y') {
if (canvasHeight > imgHeight) {
let y = 0
while (y < canvasHeight) {
drawImage(ctx, image, {
...drawOpt,
y
})
y += imgHeight
}
return true
}
}
// ...
}
最后就是repeat值,也就是水平和垂直都進(jìn)行重復(fù):
const handleBackgroundRepeat = ({}) => {
// ...
// 平鋪
if (backgroundRepeatValueArr[0] === 'repeat') {
let x = 0
while (x < canvasWidth) {
if (canvasHeight > imgHeight) {
let y = 0
while (y < canvasHeight) {
drawImage(ctx, image, {
...drawOpt,
x,
y
})
y += imgHeight
}
}
x += imgWidth
}
return true
}
}
從左到右,一列一列進(jìn)行繪制,水平方向繪制到x超出canvas的寬度為止,垂直方向繪制到y超出canvas的高度為止。
最后同樣看一下和前兩個(gè)屬性的組合情況。
css樣式:
.cssBox {
background-image: url('/4.png');
background-repeat: repeat;
background-size: 50%;
background-position: 50% 50%;
}
效果如下:
圖片大小是正確的,但是位置不正確,css的做法應(yīng)該是先根據(jù)background-position的值定位一張圖片,然后再向四周進(jìn)行平鋪,而我們顯然忽略了這種情況,每次都從0 0位置開始繪制。
知道了原理,解決也很簡(jiǎn)單,在handleBackgroundPosition方法中已經(jīng)計(jì)算出了x、y,也就是沒有平鋪前第一張圖片的放置位置:
我們只要計(jì)算出左邊和上邊還能平鋪多少張圖片,把水平和垂直方向上第一張圖片的位置計(jì)算出來,作為后續(xù)循環(huán)的x、y的初始值即可。
const handleBackgroundRepeat = ({}) => {
// 保存在handleBackgroundPosition中計(jì)算出來的x、y
let ox = drawOpt.x
let oy = drawOpt.y
// 計(jì)算ox和oy能平鋪的圖片數(shù)量
let oxRepeatNum = Math.ceil(ox / imgWidth)
let oyRepeatNum = Math.ceil(oy / imgHeight)
// 計(jì)算ox和oy第一張圖片的位置
let oxRepeatX = ox - oxRepeatNum * imgWidth
let oxRepeatY = oy - oyRepeatNum * imgHeight
// 將oxRepeatX和oxRepeatY作為后續(xù)循環(huán)的x、y的初始值
// ...
// 平鋪
if (backgroundRepeatValueArr[0] === 'repeat') {
let x = oxRepeatX
while (x < canvasWidth) {
if (canvasHeight > imgHeight) {
let y = oxRepeatY
// ...
}
}
}
}
本文簡(jiǎn)單實(shí)現(xiàn)了一下在canvas中模擬css的background-size、background-position、background-repeat三個(gè)屬性的部分效果,完整源碼在https://github.com/wanglin2/simulateCSSBackgroundInCanvas。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。