最近買了尤雨溪大大的Live:不吹不黑聊聊前端框架,這場Live讓我的前端思維到了前所未有的高度:當我們身為前端開發萌新,在前端人才金字塔的浮動與掙扎中思考該學什么框架、該如何入門前端、又遇到學習瓶頸怎么辦的時候,正是這些業界大牛們用自己的行動引導著我們,有如尤大所說:多思考場景需求,多看看技術到底做了怎樣的取舍,現在把相關的東西作為筆記整理下來,希望對前端開發有興趣的同學都可以去支持一下尤大
組件可以是函數
想象一下整個應用是一個大的函數,函數里面可以調用別的函數,每一個組件是一個函數,一個組件可以調用其他的函數,整個一個樹狀結構
組件是有分類的
- 純展示型的組件,數據進,DOM出,直觀明了
- 接入型組件,在React場景下的container component,這種組件會跟數據層的service打交道,會包含一些跟服務器或者說數據源打交道的邏輯,container會把數據向下傳遞給展示型組件
- 交互型組件,典型的例子是對于表單組件的封裝和加強,大部分的組件庫都是以交互型組件為主,比如說Element UI,特點是有比較復雜的交互邏輯,但是是比較通用的邏輯,強調組件的復用
- 功能型組件,以Vue的應用場景舉例,路由的router-view組件、transition組件,本身并不渲染任何內容,是一個邏輯型的東西,作為一種擴展或者是抽象機制存在
JSX和模版的對比
JSX本質就是Javascript,它完全獲得了Javascript的靈活度,它最大的價值在于書寫功能型組件的時候比純模版更好,但是模版在展示型和其他的用例上也是不差的,模版會讓你更少的把邏輯放在視圖里面,展示型的組件雖然在邏輯上比較簡單但是在樣式上還是具有一定的復雜度
代碼的拆分(colocation)
把應該放一起的東西放一起,比如說在vue的單文件組件里面我們把模版、樣式和javascript的邏輯都是放在一起的,目前主流框架都是這么做的,如果這概念擴展開來,組件的文檔也是可以和組件的其他東西放一起的,組件化之后和傳統的Separation of concern就有區別了,傳統的Separation of concern是以語言為單位作切分,組件是以組件本身作為一個切分的抽象
變化偵測和渲染機制
- 渲染機制
現代的前端框架里面渲染這塊最重要的是聲明式(Declarative),相比較之下的概念是命令式(Imperative),Imperative最直接的例子是我們使用Jquery時候拿到一個選擇器"直接干",使用命令去進行操作,直截了當,但是很快就會遇到維護性的問題,英文里面有個詞叫做jQuery Spaghetti,Spaghetti的意思是意大利面,這個詞的意思就是說你的代碼寫到后面會像一坨意大利面一樣,維護起來很困難,聲明式的好處就是說我們直接描述說數據和DOM結構之間的映射關系應該是怎么樣的,不需要我們手動去做這些操作
React里面有一個等式 view = render(state)
,這里render就是我們在React里面寫的render函數,vue的模版其實也是編譯成渲染函數的,所以模版和JSX之間的本質是相似的,輸入state,輸出DOM,理想情況下我們就是描述了這樣一種關系,那輸入變了輸出也會跟著變,我們不需要顧慮輸入和輸出之間發生了什么事情,具體到底層實現可以是Virtual DOM,但并不一定得是Virtual DOM,可以是細粒度的綁定
- 變化偵測
用過vue的朋友知道vue的數據是響應式的,vue會把你傳遞的數據進行轉化,轉化過后當你改變一些屬性的值的時候,vue就會進行相應的更新,附上尤大相關的演講PPT,里面有詳細的過程:
ppt
Live提問:
問:一直有一個疑問,以前<div onclick="clickHandler"></div> 被人詬病,為啥 vue 的聲明式寫法就是推崇的?
答:HTML里面這個onclick里面的Javascript的作用域是全局的,當你在vue里面這么寫的時候是有很明確的Javascript作用域的,你的綁定以及你的method所能觸及的影響范圍是設定好的,這個跟全局的Javascript有本質的區別,另外當我們在vue或者Reac里面這么寫的時候你的Javascript邏輯和你的模版或者說你的JSX是在一起的,可以聯想我們之前提到的colocation的概念,所以并不會造成一個維護上的困難,但是如果你在全局的Html里面這樣直接裸寫,你完全不知道你這段Javascript可能會引用到哪里的變量或者是調用的哪里的方法
簡單的總結,變化偵測主要分為兩種:
pull
所謂pull,系統不知道數據什么時候變了,它需要一個信號去告訴它說數據有可能變了,在這個系統才會去進行一次比較暴力的比對,在React里面的表現是Virtual Dom Diff,在Angular里面就是整個臟檢查的流程,能夠這么做的前提是現在Javascript足夠快,雖然有浪費但是性能上也可以接受push
相比之下,vue的響應式數據或者RXJS的數據機制,在數據變動之后立刻就可以知道數據變動了,而且一定程序上我們會知道哪些數據變了,這樣就可以進行相對更細粒度的更新,pull的這種更新是最粗粒度的,所以在大型應用里面我們要幫助系統來減少一些無用功,但是push的形式也有它的缺陷,粒度越細,你的每一個綁定都會需要一個observabel/watcher,這樣會帶來相應的內存以及依賴追蹤的開銷,所以在vue2里面選擇的是一個比較中等粒度的方案,在組件級別是push,每一個組件是一個響應式的watcher,當數據變動時候我們可以對組件進行更新,在每個組件內部則是用Virtual Dom進行比對,push和pull之間的本質區別是在于用偵測成本換取一定程度的自動優化
狀態管理
狀態管理這個概念其實也是在FB提出了Flux之后才搬到臺面上來講,Flux在經歷了初期的混亂競爭之后慢慢的合流到了Redux上,vux在一定程度受到了Redux的影響,狀態管理的本質是從源事件(source event)映射到狀態的遷移和改變,然后在映射到UI的變化,聲明式的渲染已經幫我們解決了從狀態到UI的映射,這一塊,所以狀態管理這些庫他們做的實際上是如何管理將事件源映射到狀態變化的過程,如何將這個映射的過程從視圖組件中剝離出來,如何組織這一部分代碼來提高可維護性,是狀態管理要解決的本質問題
把 Vue 當 Redux 用
把 Vue 當 MobX 用
現在的狀態管理方案還面臨一些其他的共同的尷尬,一個是組件的局部狀態和全局狀態如何區分,現在是局部狀態和全局狀態并沒有很明顯的區分,另一個是全局狀態和服務端數據之間,現有的方案是把服務端抓過來的數據塞到store里面去
路由
路由是只有在大型的單頁應用才會遇到的一個問題,傳統的路由思想是比較有侵入式的,每個路由有自己的數據模型,有自己的模板等,但是當Reac和vue出現之后人們發現把路由和組件解耦是可行的并且還更加靈活,比如Reac直接用不帶路由是完全沒問題的,另一個啟示是,如果從組件出發去思考路由,本質上就變成了把一個url映射到組件樹結構的一個過程,url到組件的映射會有一些小的分歧,我們到底是應該從url出發,還是從這個狀態出發,其實本質是一樣的,因為url就是一個序列化的狀態。
當實際在SPA中去做一個你會發現路由會涉及到許多其他問題,比如說hash模式和history模式如何兼容,重定向,別名,懶加載,然后最復雜的是跳轉,路由之間的跳轉需要提供各種"鉤子",然后這些"鉤子"里面又可能做異步操作,"鉤子"里面也有可能取消這次跳轉,使得這次跳轉無效等等。
整體來說現在主要的路由方案都有點相似,比較有意思的是最新的reat-router4,他推崇的是一種用組件本身來做路由的一種思路,這里很大程度上利用了上述第四大組件"功能型組件",在父組件里面聲明式的渲染其他組件,跟傳統的路由組件方案的區別是"去中心化",他不是把整個路由表寫在一個地方,是分散的寫在各個組件里頭,這樣做的好處是靈活性非常好,但是也有一些問題,首先,集中式的路由表對于理解整個應用的結構是有幫助的,另一方面,去中心化的路由對于跳轉的管理會弱一些,他對于跳轉的管理是直接用組件的生命周期去做的。
web路由和app路由的區別:
目前web路由整體思路上是一樣的,將url映射到組件樹,從一個url跳轉到另一個url,我們把新的url push到歷史的stack里面去了,但是stack前一個位置所對應的位置是被我們丟棄掉的,我們從一個狀態遷移到另一個狀態我們整個應用界面遷移到另一個狀態了,原生應用上的跳轉就像一疊卡片一樣,新的界面會蓋在現有的界面上,當你退回去的時候只是把當前的卡片拿掉,之前的卡片就會出現,web用的路由方案做app會比較別扭。
CSS方案
主流的 CSS 方案
- 跟 JS 完全解耦,靠預處理器和比如 BEM 這樣的規范來保持可維護性,偏傳統
- CSS Modules,依然是 CSS,但是通過編譯來避免 CSS 類名的全局沖突
- 各類 CSS-in-JS 方案,React 社區為代表,比較激進
- Vue 的單文件組件 CSS,或是 Angular 的組件 CSS(寫在裝飾器里面),一種比較折中的方案
比較 CSS 方案時首先要明確場景的問題,如果應用邏輯已經組件化了,是一個比較復雜應用的開發,傳統的 CSS 方式可維護性就有問題了
react-css-in-js
反對css-in-js的文章
傳統 css 的一些問題:
- 作用域
- Critical CSS
- Atomic CSS
- 分發復用
- 跨平臺復用
css-in-js有很多不同的方案,這些方案各自解決了上述的一些問題,但是并不完美:
- CSS Modules,Inline-Styles,vue的單文件組件里面直接加一個scoped都可以解決這個問題
- 所謂的Critical ,比如說我們直出一個頁面,可能我們整個應用有幾十個頁面,但是我們直出的永遠是第一個頁面,如果沒一個頁面都有一個對應的CSS的話,理論上渲染首屏我們只需要首屏的CSS就夠了,這就是所謂的Critical CSS,在服務端渲染尤為重要,解決的辦法是在服務端渲染的時候偵測到渲染要用到哪些CSS,css-in-js和vue2.3+有一個運行時的功能,在編譯過程里面可以把CSS的插入跟組件的生命周期掛鉤,同樣可以起到收集Critical CSS的效果
Live提問:
問:在vue里 使用CSS Modules 會不會比 使用 scoped 好?
答:我個人覺得沒有什么本質的區別,scoped的成本會更低一點,CSS Modules會有一定的運行時的代價,因為需要用動態的class綁定
- Atomic CSS的概念:比如說我們有兩條CSS規則,一條是color:red,一條是color:green,我們寫兩個button的樣式,一個按鈕是紅的,一個按鈕是綠的,原子類的話就會把color:red單獨拆成一個類:A,把color:green單獨拆成一個類:B,然后所有button共享的再拆成一個類:C,然后紅色的button可以說是AC,綠色的button是BC,總而言之就是把盡可能多的共享的一些單獨的規則都拆成一個很小很小的類,這樣出來的最后的結果是你的CSS可壓縮性更好了,可以變得更小,對應于css-in-js里面的Style Chunk
- 分發復用的論點是說css-in-js都是Javascript,所以可以跟普通的Javascript模塊一樣直接發包到npm上去復用,確實Javascript比純CSS更容易去組合復用,但是css你也可以發到npm上然后webpack直接引用,這一點上并不算完全的優勢
- 跨平臺復用:VueX里面就是把靜態的css在parse之后編譯成Javascript,就可以跨平臺復用了
構建工具
構建工具解決的其實是幾方面的問題:
- 任務的自動化
- 開發體驗和效率(新的語言功能,語法糖,hot reload 等等)
- 部署相關的需求
- 編譯時優化
雖然 Vue 本身用 flow,但建議使用 TypeScript 的 flow,主要從開發體驗、生態完善度上考慮
服務端數據通信
長久以來我們傳統的做法都是圍繞Rest,服務端如果暴露的是一個比較標準的Rest API,那我們客戶端就可以直接拿一個fetch直接去抓,或者圍繞Rest來做一個資源的抽象/封裝,特定的應用會遇到比較復雜的場景,一種是,數據直連,數據之間有大量的關聯性,另一類是有實時推送同步的需求,這種情況下傳統的Rest做法會比較痛苦。
Vue.js 服務器端渲染指南
跨平臺渲染
從前端框架的角度去看,跨平臺渲染的本質是在設計框架的時候要讓框架的渲染機制和DOM解耦,這里面有很多種實現方式,并不一定需要Virtual Dom,本質上只要把框架更新時候的一些節點操作封裝起來,你就可以做到跨平臺,一個原生的渲染引擎,比如 React Native 和 VueX本質都是在底層針對每個平臺有一個適配的渲染引擎,只要把渲染引擎暴露的結點操作的 API,跟框架運行時對接一下,就可以實現將框架里面的代碼渲染到原生的目的。這里的解耦很清晰,這也是為什么能看到 NG 可以接 React Native,VueX 可以跑 Vue 文件,VueX 可以跑在 NativeScript 上等等。
新規范
- Web Component
Web Component 和類 React、Angular、Vue 組件化技術誰會成為未來? - WebAssembly
是面向 Web 的通用二進制和文本格式,可以跑在瀏覽器里面。但是在目前的形勢下,WebAssembly 暫時還操作不了 DOM,對于框架的影響暫時比較有限,待觀望
總結
總結一下吧,我們聊了很多東西,可能比較雜,但我希望大家發現其中一些共性的東西:技術方案都是先有問題,再有思路,同時伴隨著取舍。在選擇衡量技術的時候,盡量去思考這個技術背后是在解決什么問題,它做了怎樣的取舍。這樣一方面可以幫助我們更好的理解和使用這些技術,也為以后哪天你遇到業務中的特殊情況,需要自己做方案的時候打好基礎。