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