作者:yucj,《報導者》前端工程師
最近我在《報導者》協助製作了三個全版簡報式(fullpage slideshow)報導:
有許多讀者很好奇這種形式的網頁是怎麼製作的,有沒有什麼眉角?在此我把製作這三個專題前端工程部分的經驗,統整如下,跟大家分享。
內容目錄:
1. 怎麼做 fullpage slideshow?
各種框架應該都會有現成的 fullpage slideshow 框架,例如 jQuery 有 fullPage.js,React 有 Spectacle(這個根本是 React 版 PowerPoint)。
但以我們的情況,考量到專案其實不需要太複雜的功能,且首要的要求是輕量、快速開發、高度客製化轉場效果,因此最後決定,與其花時間去 survey 和研究套件,不如直接用已經熟悉的 React 手刻一個比較快,不確定性也較低。
1–1. Viewport Component
整個概念簡單來說是用一個寬、高都會 fit 瀏覽器可見範圍大小(window.innerHeight
, window.innerWidth
)的 div
作為 viewport。頁數也存在這一層的 React component state,所有的換頁動作都在這邊監聽處理,而 viewport 底下的 children 再根據傳下來的頁數作不同的反應。
也就是說,要先寫一個 viewport component ,它有以下幾個功能:
- 在這個 component 的
state
裡面存現在的頁數 - 把頁數和 invoke
this.setState
來換頁的 function 作為 props 傳給底下的所有 children - 設定 fullpage slide 需要的 css style
- 設定相關的 event listener 和 handling callback function(包括 click、keydown、wheel 等)
Code 如下:
1–2. Children 可以拿到頁數了,但怎麼安排各頁內容?
我目前試過兩種寫法:
第一種:用頁面來分
原理大概像這樣:
「廢墟裡的少年:15歲起我這樣養活自己 」和「台灣勞工職業病圖譜」就是用這種寫法。
這種寫法好處是在最後輸出的 DOM 結構上,每張投影片有點像繪圖軟體中的圖層或群組,在管理 z-index
的順序上可以用整張投影片為單位管理。
大部分的情況下,不管是排版或是資料更新,這樣的寫法都比較好管理。
第二種:用功能來分
這次的「衛星圖看六輕:石化帝國是怎麼煉成的」,嘗試使用另外一個寫法:
這種寫法好處是可以一個一個功能快速獨立開發,資料也可以切開不用相互干擾,這樣不用動太多腦就可以快快寫。
壞處則是:
- 因為資料也一樣照功能切開,就要個別指定
index
。如果偷懶把index
寫死的話,投影片順序改就....會改到哭。但這應該可以透過設計資料的架構來解決。 - 比較大的問題是,如果每一頁的物件沒有在離開該頁的時候被 unmount,或是改變 z-index(但要小心改的時機會影響轉場動畫),就會全部疊在頁面上 XDDD 使用者想反白複製文字的時候就會覺得奇怪怎麼複製到其他頁的文字。正規的解法是不顯示的物件就該要 unmount。但因為我用 anime.js + react transition group 還沒想到好的 unmount 寫法(後述),所以暫時性解法是設
user-select: none;
讓 user 不能反白選取,直接解決可能提出問題的人(誤。
1–3. 不管是哪個寫法,都記得最後要設定 viewport 底下 components 的 shouldComponentUpdate()
每次 page index 改變的時候,每一頁或每個功能的 component props 都會變動,所以只用 React.PureComponent
的話,還是會一直 invoke render
method。
建議最後功能底定時,可以調校一下,在靠近上層的地方避免不必要的 render
運算。
2. 怎麼堆疊圖層,又要做到 RWD
2–1. 圖層類型的選擇
以製作六輕地圖為例,當我們把地圖的衛星底圖和上面的物件拆成不同圖層的時候,要怎麼疊才能疊好?
實務上有幾個方案可以選擇:
底圖:(1) jpg (2) Google Earth 或其他線上衛星圖 API
物件:(1) 跟底圖一起包進 HTML5 Canvas (2) svg (3) png
由於這次設計和開發時間只有約兩週左右,我們內部也並沒有手刻 HTML5 Canvas 的經驗,於是我們就捨棄開發上不確定性比較高的 Canvas 和外部 API,只考慮比較單純的靜態圖片。
原本我是希望嘗試物件用 svg,因為這有兩個優點:
- 我們專案中都是用
babel-plugin-inline-react-svg
把 SVG 檔轉成 React compoenent 來使用,裡面的東西可以分別作為 DOM element 透過 Javascript 和 CSS 操作動態,就可以分別對檔案中的<path>
或任何物件指定動畫。(像 anime.js 提供的範例) - 字跟圖在呈現上都會比較銳利,而且文字不必跟著圖縮放,可以透過 CSS 指定
font-size
。
不過,在我嘗試了一下我們設計師出的版型怎麼做到 RWD 後,發現必須要用到像 background-size: cover;
的效果,才能在各種裝置上都有比較好的觀感。
而如果要用一個優雅的寫法來讓 <svg>
可以做到跟 background-size: cover;
一樣的效果,甚至要可以對應我設定的不同 background-position
,會花比較多時間來測試,不確定是否來得及趕上時程。
所以,最後選擇把設計師出的 svg 都轉成 png 檔來用 (XD), 這樣我只要把圖一張一張疊上去而已,開發上單純很多。
這邊要注意的是,在請設計師出素材的時候,每個圖層的圖片要出同樣的大小比例(例如在 sketch 裡就是匯出整個 artboard),這樣對前端來說會很方便對位置。
2–2. 工程師和設計師維持同步很重要
在版面設計時,就要請設計師留意,不同裝置的版面盡量不要會更改元素所在的階層或順序。只要沒有去更動階層或順序,大部分的 RWD 設計應該都可以透過 CSS 達成。
另外,我們設計和工程部分的流程是:
- 設計師先出 mockup 圖和提供素材的 sample。我們一般會請設計師出 mobile、tablet、desktop 三種版型。
- 前端工程師根據 mockup 思考這些版面怎麼達成 RWD (不會等 mockup 畫完才開始想,在設計師 sketch 時就已經可以直接討論怎麼樣比較可行,我們沒有正式的 wireframe 階段)
- 把確定的素材規格給設計師出素材,修圖、裁切先出一兩張測試就好,讓工程師快速部分的 prototype 確認,避免溝通上的誤解會讓設計師多作白工。
小公司比較彈性,所以只要雙方都能夠不會害怕多互相確認,就可以減少許多不必要的時間浪費~
一般較有規模的流程可以參考這篇文章,可以看到我們的簡化很多。
2–3 .全版型的圖片裁切
我是參考 Google Searching For Syria 的規格,整理出它的 RWD 背景圖 size 如下:
這要先提供給設計師作裁切素材時的參考。
3. 動畫、轉場換頁效果的技術選擇
報導者最近的多媒體專題製作都是用 Babel + Webpack 打包成靜態網頁,再上傳到 Google Cloud Storage。詳細的 code 可以看我們 static-fe-boilerplate
的 example
資料夾。
在前端使用 React + styled-compoenents 的基礎上,目前我們有用過以下幾種選擇:
- CSS +
styled-components
+react-transition-group
velocity-react
(with dependencyreact-transition-group
)anime.js
+react-transition-group
3–1. CSS + styled-components
+ react-transition-group
「廢墟裡的少年」(Source Code、@twreporter/react-components/fullpage-slides、@twreporter/react-components/cinemagraph)是用這個方案。
基本上就是用純 CSS 的 animation
、transition
去作動畫。
這個方案的優點:
- 用到的 library 最少,減少 bundle size 而且不用學太多 API
缺點:
- 很難控管動畫的播放控制,只有當 DOM element 移除再進入的時候才能再播放動畫,也沒有辦法中止或暫停
- 如果想要串接同一元件不同的動畫,或是不同元件的動畫,必須要自己想一些方法手動安排,例如去聽 animationEnd 的事件,或是用變數來安排秒數。
- 不同的進入(從上一頁進入或從下一頁回來)和移出動畫不好管理
- 網路上有關於 CSS 和 Javascript 哪個用來作動畫 performance 比較好 的爭論,雖然大部分測試結果是 js library 比較順或兩者差不多,但瀏覽器改版速度那麼快,可能還是要找最接近的使用情境來自己做測試比較準。
簡言之,在 styled-component
的幫助下,可以透過 Javascript 來操作 CSS,已經比純寫 CSS 或 SASS 的方式方便很多,也更好跟 React 結合。但還是較適合動畫比較單純、不需要串接的情境。
純 CSS 動畫的優缺點也可以參考這篇 2016 年的比較文章 。
3-2. velocity-react
「台灣勞工職業病圖譜:新興的風險與隱藏的黑數」(Source Code)是採用這個方案。
優點:
- 已經把 velocity 跟 React 還有
react-transition-group
都打包好,可以直接用很方便 - 可以用的 API 相當完整
缺點:
- 把 bundle-size 變大了~ 用 Webpack Bundle Analyzer 看,velocity-animate 15.57kb + velocity-react 4.08 kb,增加約 20 kb(gzipped)。
- 被包住的 element 一開始的 style 會被強制跳到給定動畫的最後一格狀態。但當我沒有要 loop 動畫,不想要初始狀態跟最終狀態一樣的時候,目前還想不到看起來比較乾淨的解法改變這個設定。
3-3. anime.js
+ react-transition-group
「衛星圖看六輕:石化帝國是怎麼煉成的」(Source Code)是採用這個方案。
優點:
- 增加的 bundle-size 比 velocity 小很多,只有 5.7 kb(gzipped)。
- 動畫控制的設計相當好用,而且 Document 的範例可以很快速的看到有哪些效果可以用,也很好懂。
缺點:
- Document 對 API 的用法不盡完善,有些 API 是我自己試出來可以這樣寫但文件上找不到 XD
- 並沒有官方的整合 React 方式,我現在的寫法是用
ref
callback 拿到 DOM element 再將target
傳給animejs
做操作,但這樣其實就脫離 React 透過 virtual DOM 的管理,直接去操作變更 DOM Tree,可能會有不好的後果(? - 還沒有想到好的方法去協調安排「
react-transition-group
unmount、mount element」、「ref 拿到 DOM」、「DOM 註冊到 animejs」的順序流程。照我目前 code 的寫法,如果我打開react-transition-group
的unmountOnExit
,動畫會太早被呼叫以致於失敗。所以暫時的很爛的解法是所有的東西都不 unmount,只改 opacity 和 visibility。 - 只要不是 numeric 的 property 就不能直接用 anime.js 操作
3–4. 動畫技術選擇小結:
目前我要寫動畫,大概會是以下三種選項:
- 兩個狀態AB之間互換,且 A to B 和 B to A 時間相同 → CSS transition
- 超過兩個狀態或時間不同,但還算簡單 → CSS animation
- 複雜的動畫:還在研究,目前沒有試到完美的選項,傾向使用
anime.js
+react-transition-group
來管理,但就有前述的問題。這部分之後應該會再特別研究整理一次。
以 fullpage slideshow 來說,就是比較複雜的動畫。
4. 轉場換頁的設計
一個互動式 slideshow,在設計某一頁的轉場時,總共會有四轉轉場的情況要處理:
- 從上一頁進入這頁
- 從下一頁回到這頁
- 從這頁離開到下一頁
- 從這頁回到上一頁
而通常會有三種寫動畫處理轉場的方式:
- 只寫 enter from previous page 和 exit to next page 的動畫(也就是順向播放流程),而反向的動畫就直接 reverse(透過 anime.js 或 velocity),通常用在較複雜的改變位置、大小的轉場。
- 只區分 enter 和 leave 的動畫,通常用在淡入、淡出效果,或是簡單的位移轉場。
- 針對四個情況都個別設計轉場動畫。
整理如下表:
因為報導者並沒有專職的 motion designer,所以轉場效果都是由設計師和前端工程師討論製作。
5. 踩雷:版面高度會比預期的要小
以 iPhone 為例,當我們把 scrollY
固定在 0
(網頁最上方)的時候,上方會固定出現網址列,而下方則是一直會有 iOS Safari 自己的工具列:
因為這兩條 bar 都會往內吃掉原本瀏覽器的空間,所以在排版和設計時就要特別注意大小和可以放的字數上限。
這要提前告知設計師與記者,方便他們安排內容,避免來回修改多費工。
有你,才有《報導者》,邀您用行動支持!
優質深度報導必須投入優秀記者、足夠時間與大量資源⋯⋯我們需要細水長流的小額贊助,才能走更長遠的路。竭誠歡迎認同《報導者》理念的朋友贊助支持我們!
官網:https://www.twreporter.org/
臉書:https://www.facebook.com/twreporter/