大前端公共知識(shí)雜談
近年來,隨著移動(dòng)化聯(lián)網(wǎng)浪潮的洶涌而來與瀏覽器性能的提升,iOS、Android、Web 等前端開發(fā)技術(shù)各領(lǐng)風(fēng)騷,大前端的概念也日漸成為某種共識(shí)。其中特別是 Web 開發(fā)的領(lǐng)域,以單頁應(yīng)用為代表的富客戶端應(yīng)用迅速流行,各種框架理念爭(zhēng)妍斗艷,百花競(jìng)放。而 Web 技術(shù)的蓬勃發(fā)展也催生了一系列跨端混合開發(fā)技術(shù),希望能夠結(jié)合 Web 的開發(fā)便捷性與原生應(yīng)用的高性能性;其中以 Cordova、PWA 為代表的方向致力于為 Web 應(yīng)用盡可能添加原生體驗(yàn),而以 NativeScript、ReactNative、Weex 為代表的利用 Web 技術(shù)或者理念開發(fā)原生應(yīng)用。
平心而論,無論哪一種開發(fā)領(lǐng)域或者技術(shù),他們本質(zhì)上都是進(jìn)行圖形用戶界面(GUI)應(yīng)用程序的開發(fā),面對(duì)的問題、思考的方式、架構(gòu)的設(shè)計(jì)很大程度上仍然可以回溯到當(dāng)年以 MFC、Swing、WPF 為主導(dǎo)的桌面應(yīng)用程序開發(fā)時(shí)代,其術(shù)不同而道相似。
任何的前端開發(fā)學(xué)習(xí)中,我們都需要掌握基本的編程語言語法與接口;譬如在 Android 開發(fā)中使用的 Java 或者 Kotlin,在 iOS 開發(fā)中使用的 Objective-C 或者 Swift,在 Web 開發(fā)中使用的 JavaScript、HTML 與 CSS 等。編程語言的學(xué)習(xí)中我們往往關(guān)注于語法基礎(chǔ)、數(shù)據(jù)結(jié)構(gòu)、功能調(diào)用、泛型編程、元編程等內(nèi)容,譬如如何聲明表達(dá)式、如何理解作用域與閉包、如何進(jìn)行基本的流程控制與異常處理、如何實(shí)踐面向?qū)ο缶幊?、如何進(jìn)行網(wǎng)絡(luò)請(qǐng)求通信等等。
接下來我們就需要了解如何構(gòu)建基礎(chǔ)的界面,譬如利用 HTML 與 CSS 繪制簡(jiǎn)單 Web 頁面、利用代碼創(chuàng)建并使用簡(jiǎn)單的 Activity、利用 StoryBoard 快速構(gòu)建界面原型等等。然后我們需要去學(xué)習(xí)使用常見的系統(tǒng)功能,譬如如何進(jìn)行網(wǎng)絡(luò)交互,如何訪問遠(yuǎn)端的 RESTful 接口以獲取需要的數(shù)據(jù)、如何讀取本地文件或者利用 SharedPreference、localStorage、CoreData 來存取數(shù)據(jù)、如何進(jìn)行組件間或者應(yīng)用間信息交互等內(nèi)容。
到這里我們已經(jīng)能夠進(jìn)行基礎(chǔ)的界面開發(fā),并且為其增添必要的特性,不過在真實(shí)的項(xiàng)目中我們往往還會(huì)用到很多的組件或者插件,iOS 或者 Android 中為我們提供了豐富的 SDK,譬如 UITableView 或者 RecycleView 可以幫助我們快速構(gòu)建高性能列表組件,Android 5.0 之后默認(rèn)的 Material Design 也是非常優(yōu)秀的界面樣式設(shè)計(jì)指南;而 Web 開發(fā)中我們往往需要引入第三方模式庫,譬如著名的 BootStrap、React Material UI、Vue element 都為我們提供了很多預(yù)置的樣式組件,react-virtualized 也為我們提供了高性能的類似于 ListView 這樣的部分項(xiàng)渲染機(jī)制。
然后我們需要將應(yīng)用真實(shí)地發(fā)布給用戶使用,我們需要考慮很多工程實(shí)踐的問題,譬如如何進(jìn)行測(cè)試與調(diào)試、如何進(jìn)行性能優(yōu)化并且在生產(chǎn)環(huán)境下完成應(yīng)用狀態(tài)跟蹤、熱更新等操作、如何統(tǒng)一開發(fā)團(tuán)隊(duì)的代碼風(fēng)格與約定等等;這里 Web 因?yàn)槠涮匦远詭Я藷岣碌墓δ?,而?Android 或者 iOS 我們則可以利用插件化技術(shù)或者 JSPatch 來實(shí)現(xiàn)熱更新。
Java 與 Swift 都是強(qiáng)類型語言,其能夠在編譯階段幫開發(fā)者排查問題減少潛在風(fēng)險(xiǎn);而我們也可以使用 TypeScript 或者 Flow 為 JavaScript 添加靜態(tài)類型檢測(cè)的特性,在 VSCode 等現(xiàn)代編輯器中同樣可以達(dá)到類似于 Android Studio、XCode 中的即時(shí)檢查與提示的功能。
最后,隨著應(yīng)用功能的增加、代碼庫的擴(kuò)展,我們需要考慮整體的應(yīng)用架構(gòu)與工程化的問題;在應(yīng)用架構(gòu)中我們往往需要考慮模塊化、組件化以及狀態(tài)管理等多個(gè)方面,選擇合適的 MVC、MVP、MVVM、Flux、VIPER 等不同的架構(gòu)模式來引導(dǎo)應(yīng)用中的代碼組織與職責(zé)分割;我們也需要考慮選擇合適的構(gòu)建與部署工具來簡(jiǎn)化或者自動(dòng)化應(yīng)用發(fā)布流程,在 Android 開發(fā)中我們會(huì)選擇 Gradle 及其自帶的多模塊特性來管理依賴與分割代碼,而在 Web 中我們可以使用 Webpack、Rollup 等工具來自動(dòng)處理依賴并且進(jìn)行構(gòu)建,iOS 中我們也可以選擇 CoocaPods。
到這里我們會(huì)發(fā)現(xiàn)雖然具體的代碼實(shí)現(xiàn)、使用的技術(shù)不同,但是 Android、iOS 以及 Web 乃至于 React Native 等開發(fā)中,我們需要解決的問題、能夠用到的架構(gòu)設(shè)計(jì)模式都是可以相互借鑒的。在我們從某個(gè)領(lǐng)域遷移到其他領(lǐng)域時(shí),我們能很方便地知道應(yīng)該學(xué)習(xí)些什么,不同的技術(shù)、工具他們的職責(zé)是什么,應(yīng)該選擇怎樣的架構(gòu)或者設(shè)計(jì)模式。古語云,欲窮千里目,更上一層樓,我們想要真正掌握某種客戶端開發(fā)技術(shù),最好是要了解我們應(yīng)該掌握哪些方面,本文即是對(duì)筆者日前總結(jié)出的泛前端知識(shí)圖譜(Web/iOS/Android/RN) 的簡(jiǎn)要闡釋。
編程語言
編程語言的學(xué)習(xí)是我們進(jìn)入軟件世界的基礎(chǔ)階梯,著名的 Code Complete 一書中提到:Program into Your Language, Not in it. 我們不應(yīng)該將自己的編程思維局限于掌握的語言提供的那些特性或者概念,而是能夠理解這些語法特性背后能提供的抽象功能與原理,從而能夠根據(jù)自己想要達(dá)到的目標(biāo)選擇最合適的編程語言。而從另一個(gè)角度來看,無論哪一門編程語言的學(xué)習(xí)也是具有極大的共性,從嚴(yán)謹(jǐn)而又被詬病過度冗余的 Java 到需要用游標(biāo)卡尺的 Python,從掙扎著一路向前的 JavaScript 到含著金湯匙出生的 Swift、Rust,我們都能夠發(fā)現(xiàn)其中的相通與互相借鑒之處。
語法基礎(chǔ)
任何一門編程語言的學(xué)習(xí)都需要從基本的表達(dá)式(Expression)語法開始學(xué)習(xí),我們需要了解如何去聲明與使用變量、如何為這些變量賦值、如何使用運(yùn)算符進(jìn)行簡(jiǎn)單的變量操作等等。在很多語言之中都有所謂的傳值還是傳引用的思量,譬如 Java 與 JavaScript 本質(zhì)上就是 Pass-by-Value 的語言,只不過會(huì)將復(fù)雜對(duì)象的引用值傳遞給目標(biāo)變量。這個(gè)特性又引發(fā)了所謂淺復(fù)制與深復(fù)制、如何進(jìn)行復(fù)合類型深拷貝等等需要注意的技術(shù)點(diǎn)。
除此之外,作用域與閉包也是很多語言學(xué)習(xí)中重點(diǎn)討論的內(nèi)容,在 JavaScript 與 Python 的學(xué)習(xí)中我們就會(huì)經(jīng)常討論如何利用閉包來保存外部變量,或者在循環(huán)中避免閉包帶來的意外變量值。表達(dá)式是一門編程語言語法基礎(chǔ)的重要組成部分,接下來我們就需要去學(xué)習(xí)流程控制與異常處理、函數(shù)定義與調(diào)用、類與對(duì)象、輸入輸出流、模塊等內(nèi)容。流程控制的典型代表就是分支選擇與循環(huán),譬如不同的語言都為我們提供了基礎(chǔ) for 循環(huán)或者更方便地 for-in 循環(huán),而在 JavaScript 中我們還可以使用 forEach 與 for-of 循環(huán),Java 8 之后我們也可以基于 Stream API 中的 forEach 編寫聲明式地循環(huán)執(zhí)行體,而 Python 中的列表推導(dǎo)也可以看做便捷的循環(huán)實(shí)現(xiàn)方式。
異常處理也是各個(gè)編程語言的重要組成部分,合理的異常處理有助于增強(qiáng)應(yīng)用的魯棒性;不過很多時(shí)候會(huì)出現(xiàn)濫用異常的情況,我們只是一層一層地拋出而并未真正地去處理或者利用這些異常。Java 中將異常分為了受控異常與不受控異常這兩類,雖然 JavaScript 等語言中并未在數(shù)據(jù)類型中有所區(qū)分,但是卻可以引入這種分類方式來進(jìn)行不同的異常處理;有時(shí)候 Let it Crash 也是不錯(cuò)的設(shè)計(jì)模式。
Eric Elliott 曾在博文中提及,軟件開發(fā)實(shí)際就是 Function Composition 與 DataStructure Design;函數(shù)或者方法是軟件系統(tǒng)的重要基石與組成。我們需要了解如何去定義函數(shù),包括匿名函數(shù)以及 Lambda 表達(dá)式等;盡管 Java 中的 Lambda 表達(dá)式是對(duì)于 FunctionalInterface 的實(shí)現(xiàn),但是鑒于其表現(xiàn)形式我們也可以將其劃歸到函數(shù)這個(gè)知識(shí)類別中。
接下來我們需要了解如何定義與傳入函數(shù)參數(shù),在 C 這樣的語言中我們會(huì)去關(guān)心指針傳遞的不同姿勢(shì);而在 JavaScript 中我們常常會(huì)關(guān)心如何設(shè)置默認(rèn)參數(shù),無論是使用對(duì)象解構(gòu)還是可選參數(shù),都各有利弊。Objective-C 與 Swift 中提供的外部參數(shù)就是不錯(cuò)的函數(shù)自描述,Java 或 Python 中提供的不定參數(shù)也能夠幫我們更靈活地定義參數(shù),在 JavaScript 中我們則可以通過擴(kuò)展操作符實(shí)現(xiàn)類似的效果。
然后我們就需要去考慮如何調(diào)用函數(shù),最典型就是就是 JavaScript 中函數(shù)調(diào)用的四種方式,我們還需要去關(guān)心調(diào)用時(shí)函數(shù)內(nèi)部的 this 指針指向。而裝飾器或者注解能幫我們更好地組織代碼,以類似于高階函數(shù)的方式如洋蔥圈般一層一層地剝離與抽象業(yè)務(wù)邏輯。最后在函數(shù)這部分我們還需要關(guān)心下迭代器與生成器,它們是不錯(cuò)的異步實(shí)現(xiàn)模式或者流數(shù)據(jù)構(gòu)建工具。
近幾年隨著前端富客戶端應(yīng)用的迅猛發(fā)展與服務(wù)端并發(fā)編程的深入應(yīng)用,函數(shù)式編程以及 Haskell 這樣的函數(shù)式編程語言也是引領(lǐng)風(fēng)騷。盡管面向?qū)ο缶幊桃灿兄芏嗥渌蝗嗽嵅〉牡胤剑窃诖笮蛷?fù)雜業(yè)務(wù)邏輯的應(yīng)用開發(fā)中我們還是會(huì)傾向使用面向?qū)ο缶幊痰姆妒?這就要求我們對(duì)于類與對(duì)象的基本語法有所掌握。
我們首先要去了解如何定義類,定義類的屬性、方法以及使用訪問修飾符等方式進(jìn)行訪問控制。其次我們需要了解如何從類中實(shí)例化出對(duì)象,如何在具體的語言中實(shí)踐單例模式等。然后我們就需要去了解面向?qū)ο蟮睦^承與多態(tài)的特性,應(yīng)該如何實(shí)現(xiàn)類繼承,子類與父類在靜態(tài)屬性、靜態(tài)方法、類屬性、構(gòu)造函數(shù)上的調(diào)用順序是怎樣的;以及如何利用純虛函數(shù)、抽象類、接口、協(xié)議這些不同的關(guān)鍵字在具體語言中實(shí)現(xiàn)多態(tài)與約定。最后我們還需要去關(guān)注下語言是否支持內(nèi)部類,譬如 Java 就分為了靜態(tài)內(nèi)部類、成員內(nèi)部類、局部?jī)?nèi)部類與匿名內(nèi)部類這四種不同的分類。在整個(gè)語法基礎(chǔ)部分的最后,我們還需要去了解下輸入輸出流與模塊化相關(guān)的知識(shí),譬如 Java 9 中即將推出 JPMS 模塊化系統(tǒng),而 JavaScript 的模塊化標(biāo)準(zhǔn)則歷經(jīng)了 CommonJS、AMD、UMD、ES6 Modules 等多輪變遷。
數(shù)據(jù)結(jié)構(gòu)與功能
語法基礎(chǔ)是我們掌握某門編程語言的敲門磚,而學(xué)習(xí)內(nèi)建的數(shù)據(jù)結(jié)構(gòu)與功能語法則是能夠用該語言進(jìn)行實(shí)際應(yīng)用開發(fā)的重要前提。在數(shù)據(jù)結(jié)構(gòu)的學(xué)習(xí)中我們首先要對(duì)該語言內(nèi)建的數(shù)據(jù)類型有所概覽,我們要了解如何進(jìn)行常見的類型與值判斷以及類型間轉(zhuǎn)換;譬如如何進(jìn)行引用與值的等價(jià)性判斷、如何進(jìn)行動(dòng)態(tài)類型檢查、如何對(duì)復(fù)合對(duì)象的常用屬性進(jìn)行判斷等等。
很多編程語言中會(huì)將數(shù)據(jù)類型劃分為原始類型(Primitive)與復(fù)合類型(Composite),不過這里為了保證通用性還是將學(xué)習(xí)復(fù)雜度較低的數(shù)據(jù)類型劃歸到基本類型中。常見的基本類型囊括了數(shù)值類型、空類型、布爾類型、可選類型(Optional)以及枚舉類型(Enum)等等。學(xué)習(xí)數(shù)值類型的時(shí)候我們還需要了解如何進(jìn)行隨機(jī)數(shù)生成、如何進(jìn)行常見的科學(xué)計(jì)算,這也是基礎(chǔ)的數(shù)值理論算法的重要組成。JavaScript 中提供了 undefined 與 null 兩個(gè)關(guān)鍵字,二者都可以認(rèn)為是空類型不過又有所區(qū)別;而可選類型則能夠幫我們更好地處理可能為空地對(duì)象,避免很多的運(yùn)行時(shí)錯(cuò)誤。
接下來我們就要將目光投注于字符串類型上,我們需要了解如何創(chuàng)建、刪除、復(fù)制、替換某個(gè)字符串或者其他內(nèi)容;很多語言也提供了模板字符串或者格式化字符串的方式來創(chuàng)建新字符串。我們還需要知道如何對(duì)字符串進(jìn)行索引遍歷,如何對(duì)字符串進(jìn)行常見的類型編碼以及如何實(shí)踐模式匹配。模式匹配中最直接的方式就是使用正則表達(dá)式,這也是我們應(yīng)用開發(fā)中經(jīng)常會(huì)使用到的技術(shù)點(diǎn)。除此之外我們還需要關(guān)注字符串校驗(yàn)、以及如何進(jìn)行高效模糊搜索等等內(nèi)容;我們也可以學(xué)習(xí)使用 KMP、Sunday 等常見的模式匹配算法來處理搜索問題。
然后我們需要學(xué)習(xí)常見的時(shí)間與日期處理方式,了解如何時(shí)間戳、時(shí)區(qū)、RFC2822、ISO8601 這些基礎(chǔ)的時(shí)間與日期相關(guān)的概念,了解如何從時(shí)間戳或者時(shí)間字符串中解析出當(dāng)前編程語言支持的時(shí)間與日期對(duì)象。我們還需要了解時(shí)區(qū)轉(zhuǎn)換、時(shí)間比較以及如何格式化地展示時(shí)間等內(nèi)容,有時(shí)候我們還需要利用日歷等對(duì)象進(jìn)行事件的增減以及偏移計(jì)算。
接下來就是非常重要的集合類型,無論哪種編程語言都會(huì)提供類似于 Array、List、Set、Dictionary、Map 等相關(guān)的數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn),而我們也就需要去了解這些常見集合類型中的增刪復(fù)替以及索引遍歷這些基礎(chǔ)操作以及每個(gè)集合的特點(diǎn);譬如對(duì)于序列類型我們要能熟練使用 map、reduce、filter、sort 這些常見的變換進(jìn)行序列變換與生成。進(jìn)階而言的話我們可以多了解下這些數(shù)據(jù)結(jié)構(gòu)的底層算法實(shí)現(xiàn),譬如 Java 8 中對(duì)于 HashMap 的鏈表/紅黑樹實(shí)現(xiàn),或者 V8 中是如何利用 Hidden Class 進(jìn)行快速索引的。接下來的話我們可以對(duì)于像 Java 中 SteamAPI 或者各種語言的 Immutable 對(duì)象的實(shí)現(xiàn)方式有所了解,還有就是常見的 JSON、XML、CSV 這些類型的序列化與反序列化操作庫也是實(shí)際開發(fā)中經(jīng)常用到的。
接下來我們就需要對(duì)語言提供的常用外部功能相關(guān)的 API 或者語法有所掌握,主要也是分為存儲(chǔ)、網(wǎng)絡(luò)與系統(tǒng)進(jìn)程這三個(gè)部分。在存儲(chǔ)部分我們需要掌握如何與 MySQL、Redis、Mongodb 等關(guān)系型或者非關(guān)系型數(shù)據(jù)庫進(jìn)行數(shù)據(jù)交互,掌握如何對(duì)文件系統(tǒng)進(jìn)行如文件尋址、文件監(jiān)控等操作,并且還需要能夠使用一些譬如 Java 堆外存儲(chǔ)這樣的應(yīng)用內(nèi)緩存來存放數(shù)據(jù)。而網(wǎng)絡(luò)部分我們應(yīng)該掌握如何利用 HTTP 客戶端進(jìn)行網(wǎng)絡(luò)交互、如何使用相對(duì)底層的 Socket 套接字建立 TCP 連接、或者使用語言內(nèi)置的一些遠(yuǎn)程調(diào)用框架與遠(yuǎn)端服務(wù)進(jìn)行交互。
最后我們需要對(duì)如何利用語言進(jìn)行系統(tǒng)進(jìn)程操作有所了解,本部分筆者認(rèn)為最重要的當(dāng)屬并發(fā)編程相關(guān)知識(shí)。在而今服務(wù)器性能不斷提升、處理的數(shù)據(jù)量越來越多的情況下,我們不可避免地需要使用并發(fā)操作來提高應(yīng)用吞吐量。并發(fā)編程領(lǐng)域我們應(yīng)該去學(xué)習(xí)如何使用線程、線程池或者協(xié)程來實(shí)現(xiàn)并發(fā),如何利用鎖、事務(wù)等方式進(jìn)行并發(fā)控制并保持?jǐn)?shù)據(jù)一致性,如何使用回調(diào)、Promise、Generator、Async/Await 等異步編程模式。除此之外,我們還需要對(duì)切面編程、系統(tǒng)調(diào)用以及本地跨語言調(diào)用有所了解。
工程實(shí)踐與進(jìn)階
編程語言初學(xué)階段的最后我們需要了解下工程實(shí)踐以及一些偏原理與底層實(shí)現(xiàn)的進(jìn)階內(nèi)容。首先開發(fā)者應(yīng)當(dāng)對(duì)具體編程語言中如何實(shí)現(xiàn) S.O.L.I.D 編程原則與數(shù)十種設(shè)計(jì)模式有所了解,當(dāng)然也不能邯鄲學(xué)步只求形似,而是能夠根據(jù)業(yè)務(wù)功能需求靈活地選擇適用的范式。而在團(tuán)隊(duì)開發(fā)中我們往往還需要統(tǒng)一團(tuán)隊(duì)內(nèi)的樣式指南,包括代碼風(fēng)格約定中常見的命名約定、文檔與注釋約定、項(xiàng)目與模塊的目錄架構(gòu)以及語法檢查規(guī)范等。接下來我們還需要對(duì)語言或者常用開發(fā)工具的調(diào)試方式有所了解,掌握基本的單步調(diào)試等技巧,并且能夠?yàn)榇a編寫合適的單元測(cè)試用例。
工程實(shí)踐方面的最后則是要求我們對(duì)代碼性能優(yōu)化所有了解,盡量避免反模式。進(jìn)階內(nèi)容的話則相對(duì)更加地抽象或者需要花費(fèi)更多的精力去學(xué)習(xí),其中包括泛型編程、元編程、函數(shù)式編程、響應(yīng)式編程、內(nèi)存管理、數(shù)據(jù)結(jié)構(gòu)與算法等幾個(gè)部分。泛型編程與元編程中的反射、代碼生成、依賴注入等還是屬于語言本身提供的語法特性之一,而函數(shù)式編程與響應(yīng)式編程則偏向于實(shí)際應(yīng)用開發(fā)中有所偏愛的開發(fā)范式。即使 Java 這樣純粹的面向?qū)ο蟮恼Z言,當(dāng)我們借鑒純函數(shù)、不可變對(duì)象、高階函數(shù)、Monad 等函數(shù)式編程中常見的名詞時(shí),也能為代碼優(yōu)化開辟新的思路。響應(yīng)式編程是非常不錯(cuò)的異步編程范式,這里我們還需要注意下并發(fā)編程與異步編程之間的差異。而內(nèi)存管理則有助于我們理解編程語言運(yùn)行地底層機(jī)制,譬如對(duì)于 JVM 或者 V8 的內(nèi)存結(jié)構(gòu)、內(nèi)存分配、垃圾回收機(jī)制有所了解的話能夠反過來有助于我們編寫高性能地應(yīng)用程序,并且對(duì)于線上應(yīng)用錯(cuò)誤的調(diào)試也能更加得心應(yīng)手。
界面基礎(chǔ)
用戶界面是前端應(yīng)用程序的核心組成部分,而我們涉足前端開發(fā)的第一步往往也就是從簡(jiǎn)單的界面搭建開始。我們可能是在 Android 中編寫簡(jiǎn)單的基于 XML 布局的 Activity,在 iOS 中利用 StoryBoard 快速構(gòu)建導(dǎo)航界面,或者在 Web 中使用某個(gè)框架實(shí)現(xiàn) TODOList。而界面開發(fā)最基礎(chǔ)的部分就是布局與定位,無論在何端開發(fā)中我們往往都會(huì)使用相對(duì)布局、絕對(duì)布局、彈性布局、網(wǎng)格布局等布局方式;并且面向多尺寸的屏幕我們往往也需要進(jìn)行響應(yīng)式布局的考慮,從橫豎屏響應(yīng)式切換到不同分辨率下的布局與尺寸的調(diào)整,都是為了給予用戶較好的使用體驗(yàn)。而了解了布局與定位之后,我們往往就需要來學(xué)習(xí)如何使用基本的界面容器,譬如常見的滾動(dòng)視圖、導(dǎo)航視圖、頁卡視圖與伸縮視圖。
Android 與 iOS 往往也為我們對(duì)這些基本容器進(jìn)行了較好地封裝,而 Web 中則往往需要我們自己動(dòng)手去實(shí)現(xiàn)相應(yīng)功能。譬如在滾動(dòng)視圖中,我們需要去提供常見的滾動(dòng)事件控制,典型的有如何在不同環(huán)境下保證平滑滾動(dòng)體驗(yàn)、如何設(shè)置優(yōu)美的滾動(dòng)條、如何設(shè)置滾動(dòng)監(jiān)聽等等。除此之外,我們往往還需要針對(duì)列表或者長(zhǎng)閱讀界面封裝一些高級(jí)事件響應(yīng),譬如上拉加載、下拉刷新以及無限滾動(dòng)時(shí)需要的滾動(dòng)觸發(fā)規(guī)則實(shí)現(xiàn)。
作為最常見的用戶交互方式之一,無論是在移動(dòng)端還是桌面端,我們也都需要實(shí)現(xiàn)一些優(yōu)美的動(dòng)畫;譬如視差滾動(dòng)就可以給用戶帶來不一樣的視覺感受,而像 Swiper 這樣的整頁滾動(dòng)則是很好地產(chǎn)品展示或者講演頁的交互方式。在基礎(chǔ)的界面容器使用中我們已經(jīng)接觸了一些用戶交互的監(jiān)聽與響應(yīng)的實(shí)現(xiàn),接下來我們則是需要深入全面地了解用戶交互相關(guān)內(nèi)容。最基礎(chǔ)的我們需要了解常用事件與手勢(shì)操作,了解如何進(jìn)行事件監(jiān)聽與綁定、如何捕獲事件并且進(jìn)行分發(fā)、如何進(jìn)行縮放、拖拽、搖晃等復(fù)雜手勢(shì)動(dòng)作地監(jiān)聽與識(shí)別、如何響應(yīng)鍵盤事件并且進(jìn)行響應(yīng)處理。
除此之外,筆者將音視頻錄制與播放,指紋、計(jì)步器等傳感器的使用,本地通知與遠(yuǎn)程推送等內(nèi)容也都?xì)w納于了用戶交互這個(gè)分類下。在 Android 與 iOS 開發(fā)中相信對(duì)于這些 API 的使用并不會(huì)陌生,而隨著 HTML5 的流行以及現(xiàn)代瀏覽器的發(fā)展,相信未來 Web 應(yīng)用也會(huì)越來越多地添加這些與系統(tǒng)層面進(jìn)行交互地功能。我們?cè)诒静糠诌€需要了解下動(dòng)畫與變換、繪圖及數(shù)據(jù)可視化等相關(guān)內(nèi)容。常見的動(dòng)畫引擎包含了屬性控制與幀動(dòng)畫兩種方式,前者更趨向于命令式編程而后者則適用于聲明式編程;除了了解這些基礎(chǔ)的語法,我們還需要對(duì)常用的動(dòng)畫進(jìn)行收集與匯總,以便在項(xiàng)目開發(fā)中能夠靈活應(yīng)用。而隨著大數(shù)據(jù)時(shí)代的到來,數(shù)據(jù)可視化相關(guān)應(yīng)用也成了前端開發(fā)常見的任務(wù)之一。
在這個(gè)部分,我們需要對(duì) SVG、Canvas、WebGL 等相關(guān)繪畫基礎(chǔ)有所了解,能夠運(yùn)用 D3.js 或者其他類似的庫進(jìn)行簡(jiǎn)單圖形繪制。并且我們要能夠利用 ECharts 等優(yōu)秀的外部繪圖庫進(jìn)行散點(diǎn)圖、折線圖、流程圖等常見類型圖表進(jìn)行繪制。最后,地圖以及相關(guān)技術(shù)也是我們需要去了解的,作為開發(fā)者我們要能夠基于百度地圖等第三方 API 或者 SDK 開發(fā)導(dǎo)航、地理位置信息可視化等相關(guān)的功能。
系統(tǒng)功能
與界面基礎(chǔ)相對(duì)的就是常見的系統(tǒng)功能以及 API 的使用語法,其主要分為系統(tǒng)與進(jìn)程、數(shù)據(jù)存儲(chǔ)以及網(wǎng)絡(luò)交互這三個(gè)部分。
進(jìn)程與存儲(chǔ)
在開發(fā)多界面應(yīng)用程序或者利用 Service、ServiceWorker 等方式啟動(dòng)后臺(tái)線程時(shí),我們就需要考慮如何進(jìn)行組件間通信;譬如在 Android 開發(fā)中我們可以利用 Otto 等庫以消息總線的方式在 Activity、Fragment、Service 等組件之間傳遞消息。而在 Android 或者 iOS 開發(fā)中我們也常常需要考慮并發(fā)編程,可能會(huì)涉及到如何利用 Thread、GCD 等方式實(shí)現(xiàn)多線程并行、如何利用 RxJava 等響應(yīng)式擴(kuò)展優(yōu)化異步編程模型、如何利用鎖等同步方式進(jìn)行并發(fā)控制等等內(nèi)容。有時(shí)候我們也需要去更多地了解系統(tǒng)服務(wù)相關(guān)的內(nèi)容,特別是在 Android 或者桌面應(yīng)用程序開發(fā)時(shí),我們需要考慮如何實(shí)現(xiàn)守護(hù)進(jìn)程以協(xié)調(diào)并且保障各個(gè)組件的正常運(yùn)行。
在系統(tǒng)與進(jìn)程部分的最后,我們還需要去接觸些系統(tǒng)輔助相關(guān)的功能實(shí)現(xiàn),譬如如何進(jìn)行運(yùn)行環(huán)境檢測(cè)、如何利用 DeepLink 進(jìn)行 APP 之間跳轉(zhuǎn)、如何進(jìn)行應(yīng)用的權(quán)限管理等等。接下來我們討論下數(shù)據(jù)存儲(chǔ)部分應(yīng)該掌握哪些內(nèi)容,最簡(jiǎn)單的就是類似于 SharedPreference、NSUserDefaults、localStorage 這樣的鍵值類存儲(chǔ);復(fù)雜一點(diǎn)的情況我們可能會(huì)利用到 SQLite 或者 IndexedDB 這樣的簡(jiǎn)化關(guān)系型或者文檔型數(shù)據(jù)庫,有時(shí)候 Realm 這樣的第三方解決方案也是不錯(cuò)的選擇。很多時(shí)候我們還需要了解如何控制緩存或者剪貼板中的內(nèi)容,以及如何對(duì)文件系統(tǒng)進(jìn)行基本的操作,譬如讀寫配置文件與資源文件、瀏覽列舉文件系統(tǒng)中的文件并且根據(jù)不同的文件類型選用不同的處理方式。
網(wǎng)絡(luò)交互
而網(wǎng)絡(luò)交互部分更多地關(guān)注如何與服務(wù)端或者第三方系統(tǒng)進(jìn)行交互,實(shí)際上對(duì)于如何在需求動(dòng)態(tài)變化的情況下較好地協(xié)調(diào)服務(wù)端與客戶端對(duì)于接口的定義是很多項(xiàng)目開發(fā)的痛點(diǎn)。不過從基礎(chǔ)使用的角度,我們首先需要了解如何利用網(wǎng)絡(luò)客戶端進(jìn)行基于 HTTP 或 HTTPS 的網(wǎng)絡(luò)請(qǐng)求。這部分我們需要了解如何構(gòu)造、分析、編碼 URI,如何管理請(qǐng)求頭、設(shè)置請(qǐng)求方法與請(qǐng)求參數(shù),如何同步、異步或者并發(fā)地執(zhí)行請(qǐng)求,如何進(jìn)行響應(yīng)解析,如何進(jìn)行復(fù)雜的請(qǐng)求管理等等內(nèi)容。
除了這些,我們還要能夠利用基礎(chǔ)的 Socket 進(jìn)行通信,這樣有助于我們理解通信網(wǎng)絡(luò)與 TCP/IP 實(shí)現(xiàn)原理;我們往往還需要關(guān)心如何利用 WebSocket 等技術(shù)實(shí)現(xiàn)推送與長(zhǎng)連接功能,如何進(jìn)行遠(yuǎn)程與本地方法調(diào)用等等。除了這三個(gè)偏功能實(shí)現(xiàn)的知識(shí)點(diǎn),我們還可以嘗試去了解下系統(tǒng)的底層設(shè)計(jì)原理。譬如在 Android 開發(fā)中我們可以嘗試去了解 Dalvik 虛擬機(jī)的工作原理,使用 Xposed 或類似工具進(jìn)行系統(tǒng)層面的一些操作;對(duì)于 Web 開發(fā)而言我們可以去更多地關(guān)注瀏覽器工作原理,了解現(xiàn)代瀏覽器的運(yùn)行機(jī)制等等內(nèi)容。
界面插件
在掌握了如何構(gòu)建基本的界面并且為應(yīng)用添加必須的功能之后,我們就需要去嘗試進(jìn)行應(yīng)用項(xiàng)目開發(fā)。每個(gè)應(yīng)用可以按照用戶交互地邏輯切分為多個(gè)獨(dú)立界面,而每個(gè)界面的開發(fā)中我們往往又需要編寫導(dǎo)航、菜單、列表、表單等等可重復(fù)使用的界面插件。實(shí)際上前端開發(fā)中最核心的工作之一就是界面插件的開發(fā),好的開發(fā)者能夠在項(xiàng)目開發(fā)中沉淀出可復(fù)用的界面插件庫;這類可復(fù)用的界面插件往往會(huì)獨(dú)立于具體的業(yè)務(wù)邏輯,其分類自然也應(yīng)按照顯示或者交互邏輯本身,而不應(yīng)該受制于不同的業(yè)務(wù)場(chǎng)景。筆者習(xí)慣地會(huì)將界面插件區(qū)分為指示器(Indicator)、輸入器(Picker)、列表與表單(TableGrid)、對(duì)話框(Dialog)、畫廊(Galley)、WebView 等幾個(gè)部分。
指示器與輸入器
指示器與輸入器算是兩個(gè)寬泛的界面插件分類,最常見的指示器當(dāng)屬文本顯示類別的插件,譬如標(biāo)簽。標(biāo)簽多用于表單中的輸入域描述、用戶引導(dǎo)等場(chǎng)景,而除了文字標(biāo)簽之外我們也會(huì)使用圖標(biāo)或者所謂的 Tags。除此之外我們還會(huì)關(guān)注于 MarkDown 等富文本的展示、如何針對(duì)不同屏幕對(duì)頁面進(jìn)行排版與字體設(shè)置、如何針對(duì)不同地區(qū)的用戶進(jìn)行國際化切換、如何為文本添加合適的動(dòng)畫等等方面。
在應(yīng)用開發(fā)中我們也會(huì)添加專門的介紹或引導(dǎo)頁,一方面引導(dǎo)用戶使用,另一方面也可以進(jìn)行后臺(tái)資源請(qǐng)求與處理;譬如我們往往會(huì)在應(yīng)用啟動(dòng)時(shí)設(shè)置閃屏頁(Splash),記得最早在 Uber 見到以短視頻為背景的閃屏頁很有耳目一新的感覺。除此之外,我們常見的指示器還包括了進(jìn)度指示與時(shí)間指示這兩種。在進(jìn)行數(shù)據(jù)請(qǐng)求或者數(shù)據(jù)處理等需要用戶等待的場(chǎng)景中,我們往往會(huì)給用戶以進(jìn)度條方式地友好反饋,這種進(jìn)度條就是典型地進(jìn)度指示。常用的進(jìn)度條設(shè)計(jì)有線性進(jìn)度條、圓形進(jìn)度條或者固定在頁首或者頁尾的進(jìn)度條,有些設(shè)計(jì)中我們也會(huì)以背景投射地方式反饋當(dāng)前進(jìn)度,這種方式可能更具有視覺沖擊力。而除了進(jìn)度條之外,無限循環(huán)的加載效果、分頁器或者步驟跟蹤顯示器也是常見的進(jìn)度指示的表現(xiàn)形式之一。而所謂的時(shí)間指示即譬如界面上放置的擬物時(shí)鐘或者電子時(shí)鐘、常見于社交媒體上的時(shí)間軸或者日歷效果以及倒計(jì)數(shù)效果等。
而輸入器的典型代表則為按鈕與文本輸入,譬如我們除了常見的 Primary、Secondary 按鈕之外,我們可能還會(huì)用到懸浮按鈕、可擴(kuò)展的按鈕或者在喜歡與點(diǎn)贊時(shí)用到的具有一定動(dòng)畫效果的按鈕。而文本輸入系列的插件中,除了常見的文本框或者富文本編輯器,有時(shí)我們也需要去編寫具有自動(dòng)補(bǔ)全或者類似于密碼、勾選之類的特殊格式的輸入框。選擇器也是我們常用到的輸入器之一,譬如開關(guān)、單選按鈕、勾選按鈕、分段輸入以及常用于兩個(gè)列表互選的左右穿梭器等等。除了這些,搜索、菜單、解鎖界面也是歸屬于輸入器這個(gè)類別中。
列表、畫廊與對(duì)話框
在這兩個(gè)大類之外算得上最常用的插件的當(dāng)屬列表、網(wǎng)格與表單這個(gè)系列的控件;基本上每個(gè)應(yīng)用都會(huì)包含列表或者網(wǎng)格布局,對(duì)于海量數(shù)據(jù)的列表渲染也是前端常見的挑戰(zhàn)之一。Android 中內(nèi)置的 RecycleView 與 iOS 中內(nèi)置的 UITableView 都為我們提供了不錯(cuò)的懶加載、局部渲染的功能,而 Web 中我們往往需要自己定制或者尋求第三方庫的幫助。對(duì)于列表的交互也是常見問題之一,除了允許用戶正常的點(diǎn)擊,我們還需要添加左滑右滑時(shí)的反饋、可伸縮或者允許排序、拖拽的方式進(jìn)行交互,有時(shí)候還需要為了列表項(xiàng)添加進(jìn)出時(shí)的轉(zhuǎn)場(chǎng)動(dòng)畫,以這種微互動(dòng)增加整個(gè)界面的友好性。
最后我們來聊聊畫廊與對(duì)話框,畫廊最典型的插件就是提供圖片或者視頻預(yù)覽的走馬燈效果的輪播插件,筆者也是將圖片加載、呈現(xiàn)、處理相關(guān)的插件劃分到了畫廊這一系列插件中。而在端開發(fā)中我們常常需要對(duì)相冊(cè)或者緩存中的圖片進(jìn)行瀏覽,或者將圖片以瀑布流的方式呈現(xiàn)給用戶,這種性質(zhì)的插件也應(yīng)歸屬到畫廊這一類中。對(duì)話框的分類則稍顯的有些生硬,譬如 ActionSheet、HUD 是系統(tǒng)提供的消息提示性質(zhì)的插件,這種彈出與顯示層自然會(huì)劃歸到對(duì)話框這個(gè)系列的組件中。而在 Web 中我們常常需要自定義的模態(tài)對(duì)話框、覆蓋層也屬于對(duì)話框系列,有時(shí)候我們還需要考慮如何為對(duì)話框提供拖拽支持,或者在對(duì)話框顯示和消失之際添加轉(zhuǎn)場(chǎng)動(dòng)畫。
工程化與應(yīng)用架構(gòu)
前面我們討論了開發(fā)某個(gè)前端應(yīng)用所需要的必備技能,而在需要持續(xù)交付的團(tuán)隊(duì)項(xiàng)目開發(fā)中,我們還需要考慮很多工程實(shí)踐相關(guān)的方法與技巧。命令式編程到聲明式編程的變化,將更多地功能性工作交于框架處理,而開發(fā)人員更加地專注于業(yè)務(wù)邏輯的實(shí)現(xiàn)。
工程實(shí)踐
代碼調(diào)試是每個(gè)程序員都掌握的技能,不過如何較好地調(diào)試代碼以快速定位錯(cuò)誤所在卻并不是那么容易。在開發(fā)中我們常常需要熱加載、增量編譯等相關(guān)技術(shù)來避免過長(zhǎng)的等待,而單步調(diào)試則能夠幫助我們梳理代碼邏輯、循序漸進(jìn)地發(fā)現(xiàn)問題所在。可能 iOS、Android 的開發(fā)人員更習(xí)慣使用單步調(diào)試,而在 Web 或者 Node.js 開發(fā)中我們也應(yīng)適當(dāng)?shù)囟嗍褂?Chrome 等工具進(jìn)行代碼的單步調(diào)試;有時(shí)候單步調(diào)試也是不錯(cuò)的瀏覽分析第三方源代碼庫的方式。
另一方面,日志無論在開發(fā)環(huán)境還是生產(chǎn)環(huán)境中都能夠幫我們記錄應(yīng)用運(yùn)行狀態(tài)等信息。接下來我們還要了解應(yīng)用開發(fā)周期中不同階段使用的單元測(cè)試、集成測(cè)試以及端到端測(cè)試的具體的實(shí)現(xiàn)方式,在團(tuán)隊(duì)協(xié)同開發(fā)中統(tǒng)一代碼風(fēng)格與約定,能夠利用多種方式對(duì)應(yīng)用進(jìn)行性能優(yōu)化,以及在發(fā)布到生產(chǎn)環(huán)境之后能夠混淆加密、進(jìn)行應(yīng)用更新以及應(yīng)用狀態(tài)跟蹤。
應(yīng)用架構(gòu)
所謂架構(gòu)二字,核心即是對(duì)于對(duì)于富客戶端的代碼組織/職責(zé)劃分,從具體的代碼分割的角度,即是功能的模塊化、界面的組件化、應(yīng)用狀態(tài)管理這三個(gè)方面??v覽這十年內(nèi)的架構(gòu)模式變遷,大概可以分為 MV 與 Unidirectional 兩大類,而 Clean Architecture 則是以嚴(yán)格的層次劃分獨(dú)辟蹊徑。從筆者的認(rèn)知來看,從 MVC 到 MVP 的變遷完成了對(duì)于 View 與 Model 的解耦合,改進(jìn)了職責(zé)分配與可測(cè)試性。而從 MVP 到 MVVM,添加了 View 與 ViewModel 之間的數(shù)據(jù)綁定,使得 View 完全的無狀態(tài)化。
最后,整個(gè)從 MV 到 Unidirectional 的變遷即是采用了消息隊(duì)列式的數(shù)據(jù)流驅(qū)動(dòng)的架構(gòu),并且以 Redux 為代表的方案將原本 MV* 中碎片化的狀態(tài)管理變?yōu)榱私y(tǒng)一的狀態(tài)管理,保證了狀態(tài)的有序性與可回溯性。 實(shí)際上從 MVC、MVP 到 MVVM,一直圍繞的核心問題就是如何分割 ViewLogic 與 View,即如何將負(fù)責(zé)界面展示的代碼與負(fù)責(zé)業(yè)務(wù)邏輯的代碼進(jìn)行分割。所謂分久必合,合久必分,從筆者自我審視的角度,發(fā)現(xiàn)很有趣的一點(diǎn)。
Android 與iOS中都是從早期的用代碼進(jìn)行組件添加與布局到專門的 XML/Nib/StoryBoard 文件進(jìn)行布局,Android 中的 Annotation/DataBinding、iOS 中的 IBOutlet 更加地保證了 View 與 ViewLogic 的分割(這一點(diǎn)也是從元素操作到以數(shù)據(jù)流驅(qū)動(dòng)的變遷,我們不需要再去編寫大量的 findViewById。而Web的趨勢(shì)正好有點(diǎn)相反,無論是 WebComponent 還是 ReactiveComponent 都是將 ViewLogic 與 View 置于一起,特別是 JSX 的語法將 JavaScript 與 HTML 混搭,頗有幾分當(dāng)年 PHP/JSP 與 HTML 混搭的風(fēng)味。
從代碼組織的角度來看,項(xiàng)目的構(gòu)建工具與依賴管理工具會(huì)深刻地影響到代碼組織,這一點(diǎn)在功能的模塊化中尤其顯著。譬如筆者對(duì)于 Android/Java 構(gòu)建工具的使用變遷經(jīng)歷了從 Eclipse 到 Maven 再到 Gradle,筆者會(huì)將不同功能邏輯的代碼封裝到不同的相對(duì)獨(dú)立的子項(xiàng)目中,這樣就保證了子項(xiàng)目與主項(xiàng)目之間的一定隔離,方便了測(cè)試與代碼維護(hù)。同樣的,在 Web 開發(fā)中從 AMD/CMD 規(guī)范到標(biāo)準(zhǔn)的 ES6 模塊與 Webpack 編譯打包,也使得代碼能夠按照功能盡可能地解耦分割與避免冗余編碼。而另一方面,依賴管理工具也極大地方便我們使用第三方的代碼與發(fā)布自定義的依賴項(xiàng),譬如 Web 中的 NPM 與 Bower,iOS 中的 CocoaPods 都是十分優(yōu)秀的依賴發(fā)布與管理工具,使我們不需要去關(guān)心第三方依賴的具體實(shí)現(xiàn)細(xì)節(jié)即能夠透明地引入使用。
因此選擇合適的項(xiàng)目構(gòu)建工具與依賴管理工具也是好的GUI架構(gòu)模式的重要因素之一。不過從應(yīng)用程序架構(gòu)的角度看,無論我們使用怎樣的構(gòu)建工具,都可以實(shí)現(xiàn)或者遵循某種架構(gòu)模式,筆者認(rèn)為二者之間也并沒有必然的因果關(guān)系。而組件即是應(yīng)用中用戶交互界面的部分組成,組件可以通過組合封裝成更高級(jí)的組件。組件可以被放入層次化的結(jié)構(gòu)中,即可以是其他組件的父組件也可以是其他組件的子組件。根據(jù)上述的組件定義,筆者認(rèn)為像 Activity 或者UIViewController 都不能算是組件,而像 ListView 或者 UITableView 可以看做典型的組件。 我們強(qiáng)調(diào)的是界面組件的Composable&Reusable,即可組合性與可重用性。
當(dāng)我們一開始接觸到 Android 或者 iOS 時(shí),因?yàn)楸旧? SDK 的完善度與規(guī)范度較高,我們能夠很多使用封裝程度較高的組件;凡事都有雙面性,這種較高程度的封裝與規(guī)范統(tǒng)一的 API 方便了我們的開發(fā),但是也限制了我們自定義的能力。同樣的,因?yàn)?SDK 的限制,真正意義上可復(fù)用/組合的組件也是不多,譬如你不能將兩個(gè) ListView 再組合成一個(gè)新的ListView。在 React 中有所謂的 controller-view 的概念,即意味著某個(gè) React 組件同時(shí)擔(dān)負(fù)起 MVC 中 Controller 與 View 的責(zé)任,也就是 JSX 這種將負(fù)責(zé) ViewLogic 的 JavaScript 代碼與負(fù)責(zé)模板的 HTML 混編的方式。
界面的組件化還包括一個(gè)重要的點(diǎn)就是路由,譬如 Android 中的 AndRouter、iOS中的 JLRoutes 都是集中式路由的解決方案,不過集中式路由在 Android 或者 iOS 中并沒有大規(guī)模推廣。iOS 中的 StoryBoard 倒是類似于一種集中式路由的方案,不過更偏向于以 UI 設(shè)計(jì)為核心。筆者認(rèn)為這一點(diǎn)可能是因?yàn)?Android 或者 iOS 本身所有的代碼都是存放于客戶端本身,而 Web 中較傳統(tǒng)的多頁應(yīng)用方式還需要用戶跳轉(zhuǎn)頁面重新加載,而后在單頁流行之后即不存在頁面級(jí)別的跳轉(zhuǎn),因此在 Web 單頁應(yīng)用中集中式路由較為流行而 Android、iOS 中反而不流行。
所謂可變的與不可預(yù)測(cè)的狀態(tài)時(shí)軟件開發(fā)中的萬惡之源,我們盡可能地希望組件的無狀態(tài)性,那么整個(gè)應(yīng)用中的狀態(tài)管理應(yīng)該盡量地放置在所謂 High-Order Component 或者 Smart Component 中。在 React 以及 Flux 的概念流行之后,Stateless Component 的概念深入人心,不過其實(shí)對(duì)于 MVVM 中的 View,也是無狀態(tài)的 View。通過雙向數(shù)據(jù)綁定將界面上的某個(gè)元素與 ViewModel 中的變量相關(guān)聯(lián),筆者認(rèn)為很類似于 HOC 模式中的 Container 與 Component 之間的關(guān)聯(lián)。隨著應(yīng)用的界面與功能的擴(kuò)展,狀態(tài)管理會(huì)變得愈發(fā)混亂。
【本文是51CTO專欄作者“張梓雄 ”的原創(chuàng)文章,如需轉(zhuǎn)載請(qǐng)通過51CTO與作者聯(lián)系】