做者:範懷宇(輕芒聯合創始人、CTO)前端
感謝掘金的邀請,可以有機會在這裏跟你們分享一些輕芒作小程序的具體實踐,結合咱們在作輕芒雜誌過程當中的技術選擇,重點和你們聊一些輕芒雜誌交互實現的經驗。小程序
開始以前,先自我介紹一下,我大概 2009 年開始入行,2011 年去了豌豆莢,2016 年末我和王俊煜、崔瑾一塊兒出來作了輕芒。在過去十年的職業生涯中,我經歷過不少「端」,咱們最先在 Windows 上提供豌豆莢的各類服務,而後還有塞班系統、Android、iOS 以及 Web,如今小程序也是很是重要的一個「端」。看整個客戶端的技術發展會發現,從「端」的技術來說是愈來愈薄的。原來作 Windows 開發,你可能面臨不少的光碟,很是厚的 MSDN 的文檔,還有海量的 Windows32 API。而到小程序,已經愈來愈薄,須要瞭解的細節也更少。這是由於,平臺上的框架會變得愈來愈厚,咱們開發者入手一個平臺會變得愈來愈簡單。今天「端」的發展趨勢是,用戶在哪裏,「端」就在哪裏,咱們更多的是來適應「端」,並且不管在哪一個「端」都須要提供最好的產品體驗。後端
輕芒在小程序發佈的第一天就上線了和小程序相關的產品,包括爲用戶推薦高品質內容的輕芒雜誌,咱們也把產品經驗開放給內容創做者使用,經過輕芒小程序+,他們無需代碼就能夠搭建本身的小程序。微信小程序
回到今天的主題:交互。首先要搞清楚什麼是交互,在傳統客戶端的分層邏輯裏咱們會常聽到 MVC,數據、控制、界面,這是一個可能用了二十幾年的分層模型。交互毫無疑問包含內容呈現,把咱們須要給用戶的數據呈現出來。還有一部分很重要的,是你要和用戶互動,交互界面一般不是靜態頁面,用戶會在界面上產生一些行爲,這些行爲經過控制層會反饋到數據層,數據層進行一些變化和更迭,再經過控制層送回給界面層從新進行數據的渲染和呈現,這是整個 MVC 流轉的方式。除了內容的呈現和渲染,還有很重要的一點是要處理好全部和用戶的互動,這塊其實佔了前端很大的開發量,所以,會有的人可能前端開發無聊,有的人則會以爲很是有魅力。作交互其實沒有什麼銀彈。在軟件開發裏咱們一直會須要一個「銀彈」,就是「當我知道那個祕籍以後,我就能夠作得很是輕鬆,什麼事情均可以變得很是瀟灑」,其實並不存在。大量的實踐混雜在細節裏,包括我今天和你們分享的不少東西,聽上去沒有那麼神奇,甚至說有的一點都不工程,但這是真正的實踐。你真正要把一些東西作好,不少都是在細節裏面的。性能優化
首先,我先介紹一下小程序的平臺特色,由於全部的前端開發,都是在一個平臺上進行,你得對這個平臺很是熟悉,就像在一個舞臺上作演出,你須要知道舞臺在哪裏、光在哪裏、聲音在哪裏等等。你須要瞭解這個平臺的特性,才能在後面的實踐中更加自如。微信
跟我經歷過的其餘平臺相比,首先我以爲小程序是個很是純粹的數據驅動的前端平臺。好比,如今去用 Web 或者 Android 開發,若是你須要在界面點一個按鈕來隱藏一個地方,或者點一個按鈕來展開某個地方,你會怎麼作?按剛剛 MVC 模型,你可能不會穿過 Controller 去到 Model,而後改了 Model 再回來,不會走這個路徑。你確定會在前端用一些簡單的腳本或技巧,讓這個東西隱藏起來、再展開,這樣不會影響核心數據模型,並且很便利。框架
但在整個小程序裏其實沒有這一塊東西,你不能操做任何 DOM 節點,這意味着無論你作什麼事情,包括改變界面的任何一點點狀態,都須要經過控制層傳到邏輯層,用 js 通過一些計算,再調用 setData 函數,觸發對 Data 變化的比對,從新對界面進行一次渲染,這是小程序整個的內核驅動模型。這個模型坦白講有不少部分很是重,會限制你作一些特別靈活的實現,但對咱們開發者來說,只有理解這個模型,纔會知道若是去作開發,哪些環節須要特別注意。ide
還有一個你們很是熟悉的點,小程序是傳統的 Web 組件混着一些原生組件,就是用本地語言,好比說 iOS 上的 Objective C 或者是 Android 上 Java 來實現的一個本地組件,這和 Web 上面實現的組件是不同的。小程序採起的技術路線,是所謂的分層渲染,底下是 Web 層,上面是原生層,當我滑動 Web 層時,原生層會接到一些事件,它會嘗試跟 Web 層聯動,但它們其實不是一塊兒動的,中間有一個延遲,這個延遲會帶來不少麻煩事情,尤爲是因爲這個分層是原生層永遠活在 Web 層上面,這也會給交互設計、交互實踐帶來一些麻煩。函數
還有一個特點是單窗口,固然在移動時代其餘平臺也如此,在一個交互頁面,咱們只會跟一個窗口打交道。小程序特別的一點是它完全取消了窗口這個概念,可能咱們都感受不到窗口的存在。若是你們作過 Android,就知道 Android 能夠作懸浮窗,因爲小程序平臺機制的限制,懸浮窗這種交互形態是沒法實現的。性能
以上是我總結的小程序平臺交互設計的顯著特色,這也是咱們後面圍繞小程序平臺作交互設計、交互實踐的一個很重要的起點。
下面跟你們分享一些輕芒的案例,首先講一個你們用得最多的:列表。列表爲何重要?理清楚列表的邏輯對於整個產品後續的實現、對於工程師更好的去理解業務和設計,是很是重要的一個環節。在傳統的通常界面組件裏,列表是最複雜的一個模塊,它會有不少數據的聯動,它的交互、數據傳遞、事件分發都會涉及到不少問題,這也是咱們爲何要妥善思考列表怎麼作。並且在不少場景裏,長列表的性能是不少性能瓶頸的來源,這也是須要額外注意的地方。
什麼叫列表?好比下圖左邊是很是顯然的列表,能夠無限加載、不斷滾動的內容流,咱們認爲每個卡片都是一個列表項。右圖看上去是一個內容詳情頁,在實踐中咱們也把它作成了列表,咱們會把它每一個段落抽象出來,按照列表來渲染。
小程序裏面最大的特點是,它並無一個原生的列表組件。咱們剛剛說 MVC,而 MVC 中最核心的是模型(Model)的設計,就是整個產品中的數據模型究竟是什麼樣的,這是真正影響開發時間的。若是模型設計得好,就像種了一棵樹,樹上長着枝椏,若是你把全部的數據模型抽象的很是漂亮,那樹上的枝椏,也就是 View 和 Controller,實現起來會很是輕鬆。若是你的模型徹底不作抽象,隨便收拾一下就開始作交互開發,那即使你在界面層選了不少漂亮的轉型、用不少 fancy 的框架,你也很難把代碼複雜度降下來,整個產品的複用度也不會高,這也是我爲何會說若是拿一個項目最好從列表入手,由於列表的數據模型一般是最複雜的,列表清楚了,整個產品也就清楚了。
在輕芒裏,好比剛剛那個瀑布流,咱們會抽象成一個 event 對象,固然,咱們不是在全部的小程序開發裏都這麼作,由於在其餘小程序中業務不一樣,抽象模型也可能不同,咱們配套的列表實現也會不同。在輕芒雜誌,由於有很是複雜的卡片類型或者不一樣類型的交互樣式,咱們會把不一樣類型的數據統一抽象成一個 event 對象,每一個 event 會有一個類型,而後會圍繞 event 來設計列表控件。具體的實現,咱們用了微信的模板(Template)來作抽象和封裝,這是由於咱們這個項目作太早了,咱們作的時候沒有任何開源項目,只可以所有本身封裝。
這裏面最重要的實際上是對 event 對象的抽象,也就是產品中最核心的數據模型,若是這個搞清楚了,你會發現後面東西會變得很簡單。在這個代碼中,每個卡片的渲染,可能有兩個不一樣的實現方式,有一些卡片是用模板實現的,好比說 single-card-*,在實現中,咱們會直接用 single-card 加上具體的 type 來實現列表項,好比說 single-card-article、single-card-image、single-card-video 等等。在微信有了自定義組件以後,咱們把一些後來實現的新卡片放成了自定義組件,好比這裏的 universe card,咱們把新的卡片用了新的方式來作。
早期由於咱們用 Template 對界面元素作封裝,這種封裝包含了任何互動的實現。由於微信早期沒有提供任何可封裝機制,如今你也許能夠經過不一樣的開源框架搞定,早期惟一的辦法就是把一些和界面互動相關的函數抽象成一些 Mixin 模塊,而後在頁面配置裏直接整合進去,加強複用度。這個方案咱們如今依然在用,這也是在小程序裏最經常使用的實踐方案之一。
另外一方面,封裝列表也是爲了統一相關聯的界面組件,好比統一的 loading 樣式,統一的空白頁樣式,翻頁的邏輯和多級嵌套等。舉例來講,全部數據的加載,在列表中定義了翻頁方式,就定義了產品的客戶端和服務端的交互方式。在輕芒,全部的服務端翻頁都會使用 Next Url 模式,服務端會告訴客戶端有沒有下一頁,有的話客戶端滾到底就會加載下一頁。這告訴咱們,若是服務端具備統一的 API 模式,客戶端開發會變得很輕鬆,抽象和複用能夠作得更充分。固然,這反向也約束了服務端,須要服務端提供統一的或者整合的 API,這樣落地到公司所有團隊,能夠提高總體的效能。對前端來說,也只有這樣的方案纔是可以極大地簡化開發複雜度。
列表實現中,還有一個和小程序特性特別相關的點,就是如何在列表中嵌入一個原生組件,好比我想在列表裏播視頻,我想在列表裏嵌個地圖、嵌個輸入框,該怎麼辦?若是用微信原生組件的渲染機制,若是嵌一個視頻在內容流裏,它可能會遮住不少你想彈出來的對話框;有可能你想實現頁面快速滾動,視頻會有很是慢的拖影,這都很影響產品體驗。
在輕芒雜誌中,咱們用過很是多的交互方案去嘗試這個東西,最後是團隊一塊兒來解決的。這個解決方案的核心理念是:不要在列表中直接使用原生組件播放視頻,而是用設計的方案去規避,基於分層的方案來實現整個交互。當原生組件出現的時候,整個交互中止,好比看左邊這個視頻,在播放一個嵌入的視頻時,這個視頻好像是「長」在那個卡片上的,其實它是個虛擬的定位,只是大概在那個位置。用戶一旦滾動頁面,視頻馬上收起來再也不播放。可能這聽上去很不「技術」,但實際上這是你對平臺的理解,你知道這是一個系統設計上的硬坑,若是平臺不搞定這點,不管誰來作都會碰到一樣的問題。與其這樣,不如咱們理解平臺以後,和團隊商量,換種方式實現,把事情變得更簡單、更輕鬆。列表實現中還有一些問題,就是如何處理數據和狀態。每一個列表項裏面會包含不少元素,好比列表項裏會有一篇文章的基本信息,同時右下角有個交互按鈕,用戶點擊以後按鈕狀態發生變化,對輕芒來說這就是「馬克」,而這些都不屬於原生數據,會隨着用戶的交互而變化,所以被稱爲狀態。在早期小程序開發中,你們可能不太會區分狀態和數據,會把數據和狀態放到同一個數據模型上,由於實現邏輯很是複雜,在純數據驅動的模式下會碰到很是多問題。
而輕芒的實踐經驗,就是要讓數據和狀態分離。好比拿到數據後,咱們會把數據分紅兩部分,一部分是原生數據,它們不會隨着用戶交互而發生變化,用列表來進行存儲;而另外一部分是狀態,它會隨着用戶交互發生變化,這裏咱們會用字典進行存儲。圖示的代碼中, ui-switch 和 subscribed 的對象都存儲了狀態信息,當用戶對特定元素進行操做後,只須要在控制層改變 switch 對應 id 的某一個值,就能夠對界面進行快速的更新。這看上去是一個很是小的優化,但若是你在全部界面交互中都能把控好這一點,帶來的收益是很是大的。它還有一個特色,是中心化。好比剛剛看到的視頻播放,咱們同時只容許一個視頻播放,用戶點了某個視頻後其餘視頻會中止播放,這時候你只須要在控制層把整個狀態清空,對交互對象的狀態從新設定便可,這都是一些聽上去比較細節,但從實踐來說很是重要的事情。
最後一個問題,是長列表性能優化,與其說是經驗分享,不如說教訓分享。在輕芒雜誌中,咱們會用到大量的長列表,無限滾動的瀑布流、很是長的文章等等。這些都會致使列表變長,帶來卡頓。爲何?從小程序的渲染機制來看,從設定數據到界面渲染呈現,也就是調用 setData 函數到 setData 函數返回,這裏面有兩件可能比較耗時的事情,一個是數據比對:小程序須要找到有哪些數據更新了,這些數據關聯了哪些界面元素。還有一件就是刷新界面:把更新的數據呈現出來,先在虛擬節點上作渲染,從新放到前端來呈現。而小程序最大的問題,在於這兩個環節太「黑盒」了,它告訴開發者的信息很是少,所以優化起來就比較困難。咱們知道輕芒雜誌的渲染都花在這裏了,因此會有卡頓,但究竟是哪裏耗了時間,其實咱們不能知道。
因而咱們作的性能優化方案就是試,不停地作二分,少作一部分邏輯,看看性能有沒有好轉。咱們最先把性能點放在 setData 不能追加上,雖然 setData 能夠修改一個列表中的元素,但它一旦要增刪元素,就要所有替換,這是如今微信整個數據驅動模型的一個核心問題。在這裏,咱們嘗試使用固定的列表項個數,先虛擬一百個列表項,可能只有十個有數據,若是新的元素須要追加,就只用修改而不須要所有替換。最後的結論是,有效果,但沒太大的效果,而開發成本卻增長了很多。後來咱們把性能問題,更多地放在了渲染上,發現一個有效的方案是,讓界面設計變簡單一些,須要渲染的元素變少,性能就行了很多。因此在長列表中,很大的瓶頸仍是在 DOM 渲染上,小程序並無爲列表提供一個元素回收機制,這致使它會完整渲染整個列表,它並不關心這個列表項需不須要在界面上呈現、需不須要給用戶看,這就會形成不少性能問題。
在這裏咱們也沒有太完整的經驗,更多的是教訓。那對於你們來講,若是你能預見到你的產品會有很長列表,你能夠提早考慮是否是要下降列表項的設計複雜度,控制每一個列表項 DOM 節點的數量,減小一些 DOM 節點上綁定的數據和事件等等,這些可能會對產品的性能有所優所。固然總體來看小程序的渲染仍是個黑盒,咱們能用的優化手段也比較少。總結一下,其實最想和你們聊的是交互的開發和業務是密不可分的,須要去和設計、後端、產品,一塊兒去改進,在技術設計中更多的去理解需求。
咱們知道微信小程序是一個單窗口的交互平臺,那什麼是全局窗口呢?好比,上面左邊這個分享卡片,它須要在任何輕芒雜誌的頁面都能被呼出,從而對頁面進行分享。右邊是咱們自定義的 toast,不少時候咱們須要的 toast 和微信小程序官方提供的不同,這時候就須要基於全局窗口來實現。但由於在小程序中沒有全局窗口機制,對咱們而言惟一的辦法就是經過把組件放進每個頁面,隨時能夠呼出使用,模擬全局窗口,咱們把這個稱爲全局組件。在這個部分,我會重點來聊組件的封裝,在小程序中最好的組件方案是怎樣的。
剛剛你們聽了不少第三方的框架,在輕芒,咱們主要還都是基於原生的機制來實現的。常見原生方式有兩種:基於模板(Template) 和基於自定義組件的,這兩種封裝方式咱們都很經常使用。像這個代碼展現的,咱們有的卡片使用了自定義組件來進行封裝,有一些加載的進度條多是模板來封裝的。但若是我把它作成全局組件,讓每一個頁面都包含,這兩種方式都須要我在各個頁面拷貝大量的重複代碼,可能有上百行,一旦須要變動,就會須要同時修改整個小程序裏幾十個頁面。這種方案咱們早期使用過,但發現效果很差,維護起來太麻煩了。因而咱們從新設計了一下,發如今小程序中還有一些更簡單的封裝方案更適合,就是你們今天可能不多用的 include 方案,就至關於直接引入一個別的代碼塊。這是一個很是傳統的一個方案,咱們如今每每會強調組件,強調封裝,要封裝得漂亮,要讓組件邊界變得乾淨,但實際上在這樣的場景裏,咱們打破一些簡單的組件的限制,反而會讓事情變得簡單。
咱們會寫一個全局窗口的文件叫 global.wxml,把全部小程序中會用到的全局組件的數據和實現都放在這裏面,圖上的代碼是疊過的,其實展開仍是比較長的,由於在 include 中不能再使用其餘 Template 進行封裝了。對應的,還配套一些 js 代碼,咱們封裝了 action.js 的文件,裏面會包括各類全局組件的控制,好比模態對話框、輸入對話框、toast、分享卡片等等。在最後使用裏面,每個頁面只要加兩行,一個是 include global,一個 Mixin 這個 action.js,這解決了問題。
這個例子很是小,背後想聊的主要仍是組件封裝的思路。作技術設計,始終要考慮怎麼封裝組件、怎麼複用,由於這個對全部前端開發、服務端開發都是重要的,若是不復用、不去嘗試作一些抽象,那麼久而久之代碼會變得很是散,難以維護、難以閱讀。但怎麼作封裝,怎麼作實踐?除了把組件抽象得很是漂亮外,也能夠嘗試用一些邊界更模糊的封裝方案,能夠顯著的讓代碼變短變簡單,更易於變動和維護。好比,在前面例子中,我要變動某個全局組件的樣式,只要修改 global.xml,而調用該文件的地方是不用改的,用開閉原則來看,它是個很是知足開閉原則的實現策略。
在咱們掉過不少坑,當用戶移動手指去修改選中區域的時候,若是咱們用了微信的條件判斷 wx:if 去改變底色之類的,極可能會讓整個控件樹的結構發生變化,整個交互界面會陷入一個很是可怕的狀態,徹底失去響應,咱們推測這是觸發了微信底層的 bug。因此在這樣的交互中,最好的方式是隻改變 CSS 的,儘量不去改變界面元素的結構。
這裏有個細節,在觸發了調整馬克選中區域以後,咱們再拖動手指的時候,整個界面是不滾動的,直到咱們放開手指以後,整個界面才能夠從新開始滾動。這時候就用到了我剛剛說過的事件傳播機制的流程處理,在小程序中,只要你設定了捕獲函數就必須捕獲事件,不像其餘平臺,能夠經過捕獲函數的返回值來控制是否須要捕獲,而若是不進行捕獲,後續又無法中斷整個頁面的滾動。那怎麼辦?咱們後來用了一些比較 Hack 的方式,咱們去動態的修改綁定的捕獲函數名。當須要捕獲時,就把須要的函數設定上,而中止捕獲時,就把捕獲函數設置爲空白字符串,這樣能夠繞過它的捕獲機制。這時候,就能夠控制界面在何時,能夠滾動頁面,何時只能夠調整選中區域了。
這個馬克交互咱們改版過不少次,早期因爲沒有 query 任何 DOM 元素的機制,很是難以實現。而如今,微信提供了 query DOM 節點的 API,我能夠大概知道 DOM 節點在哪裏,如今咱們會反覆用到 query 函數,瞭解界面狀態,來調整能夠進行的交互。整個馬克的實現過程當中,工程師和設計師有很是多的討論,設計師根據技術限制從新想一些交互方案,工程上不斷地嘗試採起一些相似 Hack 的手段去落地這些方案。若是你但願把交互作得更往前,毫無疑問也會須要這樣進行設計和實現,也會踩到一些平臺的坑,這些經驗也能夠供你們參考。
上面分享的三個案例,不所有是單純的技術,有很多技術外的事情。交互,看上去是一個純前端實現的問題,其實它的實現是整個團隊的事情,包括產品、設計、以及後端的 APIs 設計等等。也是平臺能力和產品設計的交互融合。
另外,在小程序這個平臺上尤爲須要重視數據模型的設計,必定要讓整個數據模型理解好業務,把整個數據模型設計得清晰,什麼是數據、什麼是狀態、哪些應該隔離、哪些應該放在一塊兒,把這些事情區分好,可讓整個前端開發變得很輕鬆。此外,咱們在處理原生組件時也要特別當心,須要規避掉一些設計方案,不然會帶來一系列 Bug。
這就是我今天跟你們分享的內容,這是輕芒的實踐,我相信和你們的具體的業務需求、目標會不徹底一致。但也指望今天的分享能夠給你們一些啓發,若是你們能運用到其中的一些,對咱們來說就足夠了。
謝謝你們。