快應(yīng)用開(kāi)發(fā)優(yōu)化技巧
2018年3月,華為、小米、OPPO等九大手機(jī)廠商共同發(fā)布了快應(yīng)用??鞈?yīng)用標(biāo)準(zhǔn)由主流手機(jī)廠商組成的快應(yīng)用聯(lián)盟共同制定,其擁有傳統(tǒng)app的應(yīng)用體驗(yàn),同時(shí)又具備無(wú)需安裝、即點(diǎn)即用的特點(diǎn)。其實(shí),早在2013年,百度曾推出了輕應(yīng)用,2017年,騰訊又推出了小程序,之后,阿里也推出了支付寶的小程序。業(yè)務(wù)細(xì)節(jié)上,它們各有不同,但大體上定位是類(lèi)似的,如同當(dāng)年CS架構(gòu)轉(zhuǎn)向BS架構(gòu)一樣,這種無(wú)需安裝、即點(diǎn)即用的應(yīng)用形式,正在成為app市場(chǎng)新的趨勢(shì)。
閑言少敘,書(shū)歸正傳。本文的目的,是為了讓大家了解到快應(yīng)用開(kāi)發(fā)常用的優(yōu)化手段,提升對(duì)應(yīng)用代碼整理的組織能力,合理拆分功能模塊,從而使項(xiàng)目更易維護(hù),提高工作效率。至于如何搭建開(kāi)發(fā)環(huán)境,開(kāi)發(fā)流程以及系統(tǒng)api的描述,大家均可在快應(yīng)用的官方文檔中找到。本文將介紹快應(yīng)用開(kāi)發(fā)的優(yōu)化技巧。
快應(yīng)用開(kāi)發(fā)采用前端技術(shù)棧,其優(yōu)化方式主要可從以下四方面進(jìn)行:
- 數(shù)據(jù)共享
- 性能優(yōu)化
- 錯(cuò)誤處理
- 結(jié)構(gòu)優(yōu)化
數(shù)據(jù)共享
在快應(yīng)用開(kāi)發(fā)中,開(kāi)發(fā)者需要了解頁(yè)面與APP之間,頁(yè)面與頁(yè)面之間的數(shù)據(jù)共享方式。其實(shí)不止快應(yīng)用,任何前端工程都需要考慮這個(gè)問(wèn)題,如react開(kāi)發(fā)中,引入redux可方便統(tǒng)一管理數(shù)據(jù)源,在快應(yīng)用開(kāi)發(fā)中,可以通過(guò)使用框架API或使用全局變量global兩種方式實(shí)現(xiàn)數(shù)據(jù)共享。
1. 框架API
開(kāi)發(fā)者可以在頁(yè)面ViewModel中,通過(guò)this.$app.$def獲取APP上定義的數(shù)據(jù)及方法。
但這種方式的缺點(diǎn)在于需要依賴ViewModel實(shí)例,然而,很多全局方法與生命周期無(wú)關(guān)。全局變量global作為獨(dú)立于應(yīng)用生命周期的引用,理應(yīng)成為開(kāi)發(fā)者的***。
2. 全局變量global
在app.ux、${anyPage}.ux中,開(kāi)發(fā)者都可以使用變量global。然而,頁(yè)面與APP之間,頁(yè)面與頁(yè)面之間的變量global并不相同,引用的值指向不同的對(duì)象;雖然不相同,但它們的原型都指向同一個(gè)全局的對(duì)象。因此,開(kāi)發(fā)者可以在這個(gè)全局對(duì)象上定義變量,這樣在任何JS中均可訪問(wèn)。同樣,這種方式也存在缺點(diǎn),比如存在污染全局環(huán)境,在復(fù)雜場(chǎng)景下容易引發(fā)難以復(fù)現(xiàn)的BUG等問(wèn)題。
性能優(yōu)化
1. 更合理的Dom結(jié)構(gòu)
Dom節(jié)點(diǎn)是構(gòu)成頁(yè)面最基本的元素,盡量使用符合語(yǔ)義的標(biāo)簽,同時(shí)減少Dom層級(jí)結(jié)構(gòu)會(huì)對(duì)頁(yè)面的可讀性及性能都會(huì)有明顯提升。
2. 更有效的選擇器
快應(yīng)用的開(kāi)發(fā)框架支持后代選擇器,這對(duì)開(kāi)發(fā)者提供了很大的便利,但與此同時(shí),后代選擇器的性能損耗是相對(duì)較大的,如使用不得當(dāng),會(huì)對(duì)頁(yè)面性能造成較多損耗。
我們首先要知道,CSS選擇器是由右向左解釋的??匆韵逻x擇器
#container > a {font-weight:blod;}
對(duì)于很多剛開(kāi)始了解css的朋友來(lái)說(shuō),通常會(huì)認(rèn)為,該選擇器先找到id為container的元素,然后將字體加粗效果應(yīng)用到直系子元素中的 a 元素上,應(yīng)該是個(gè)高效的選擇器。然而事實(shí)并非如此,瀏覽器首先會(huì)便利頁(yè)面中所有的a元素,然后篩選出父元素id為container的節(jié)點(diǎn)。所以這個(gè)看似高效的選擇器,其實(shí)也是花費(fèi)了不少的開(kāi)銷(xiāo)。
了解到這個(gè)規(guī)則后,對(duì)于后代選擇器的使用,我們建議做到以下幾點(diǎn)優(yōu)化。
- 避免使用組件名稱作為后代選擇的***一項(xiàng)匹配規(guī)則,越是基礎(chǔ)的組件,復(fù)用度就越多,越是要避免。如: .container #doc text { ... };否則每個(gè)text組件渲染時(shí)都會(huì)遍歷匹配一次
- 減少后代選擇的層級(jí)數(shù)量,層級(jí)越深,單次匹配耗時(shí)則成指數(shù)式增長(zhǎng)。
- 后代選擇中***一條匹配規(guī)則的定義名稱盡量唯一,如:.container #doc .doc-item .doc-name-zh{ ... }
3. 圖片優(yōu)化
在前端開(kāi)發(fā)中,圖片通常占據(jù)了較大的空間。較多的圖片資源也意味著較多的http請(qǐng)求,在相同帶寬條件下,下載一個(gè)200k的圖片,一定比下載兩次100k的圖片速度更快。建議可以做幾下幾點(diǎn)優(yōu)化。
- 使用CSS Sprites,其實(shí)就是把網(wǎng)頁(yè)中一些背景圖片整合到一張圖片文件中,再利用CSS的“background-image”,“background-repeat”,“background-position”的組合進(jìn)行背景定位,這種方式***的優(yōu)點(diǎn)就是減少了http請(qǐng)求,大大提高了網(wǎng)頁(yè)性能。話說(shuō)回來(lái),這種方式也有缺點(diǎn),比如開(kāi)發(fā)起來(lái)會(huì)相對(duì)麻煩。
- 對(duì)于頁(yè)面上簡(jiǎn)單的圖標(biāo)文件,可盡量使用css3實(shí)現(xiàn),或矢量圖代替,這樣也能明顯減少圖片占用的空間,提高性能。
- 避免圖片壓縮,如果頁(yè)面中用不到較大的圖片,就完全沒(méi)必要通過(guò)css去壓縮圖片以適應(yīng)需求。直接使用小圖或其他可替代方式更加明智。
4. 簡(jiǎn)化ViewModel的data屬性
數(shù)據(jù)驅(qū)動(dòng)是當(dāng)下流行的前端開(kāi)發(fā)形式,在快應(yīng)用中也是如此。在ViewModel的定義中,data屬性主要承擔(dān)數(shù)據(jù)驅(qū)動(dòng)的數(shù)據(jù)定義,會(huì)對(duì)賦值的data中每個(gè)屬性進(jìn)行遞歸式的定義。因此,屬性定義語(yǔ)義結(jié)構(gòu)越清晰且數(shù)量越少,則質(zhì)量越高。
例如,當(dāng)我們發(fā)出fetch請(qǐng)求,返回結(jié)果中包含了很多數(shù)據(jù),而前端需要顯示的數(shù)據(jù)只是其中很少一部分,則在該頁(yè)面的data中,就只需要定義前端需要顯示的數(shù)據(jù)即可,如下面的示例代碼片段。
// fetch請(qǐng)求返回的數(shù)據(jù),數(shù)據(jù)量大,而data中只需要其中部分?jǐn)?shù)據(jù) const tradeInfoList = [ { "_id" : "5c31aa2a565e9938214da13b", "currentPrice" : 1, "tradePrice" : 1, "userId" : "admin", "tradeAmount" : 600, "stockInfo" : { "code" : "000008", "name" : "股票8", "price" : 1, "des" : "描述8" } }, { // ... } ] export default { data () { return { list: [] } }, onInit () { // 返回頁(yè)面中需要的對(duì)象屬性,過(guò)濾其他屬性 this.list = tradeInfoList.map(item => { userId: item.userId, tradePrice: item.tradePrice }) } }
5. 懶加載
懶加載是一種通用的優(yōu)化手段,傳統(tǒng)H5頁(yè)面中的懶加載,指的是頁(yè)面即將進(jìn)入屏幕可視區(qū)域時(shí),才加載資源,渲染頁(yè)面。這樣,給用戶直觀的感受就是,頁(yè)面加載速度變快了。在快應(yīng)用開(kāi)發(fā)中,可使用指令或事件觸發(fā)來(lái)實(shí)現(xiàn)懶加載。
比如很常見(jiàn)的場(chǎng)景,一個(gè)包含list組件的頁(yè)面,我們開(kāi)始只渲染前十條數(shù)據(jù),當(dāng)頁(yè)面下滑至某位置時(shí),觸發(fā)加載更多來(lái)完成渲染。
錯(cuò)誤處理
前端開(kāi)發(fā)中,一旦程序執(zhí)行出錯(cuò),就會(huì)報(bào)出JS異常彈框。
1. 訪問(wèn)null或undefined的屬性
以上的這種錯(cuò)誤可能是最常見(jiàn)的一種了,在稍微復(fù)雜的業(yè)務(wù)邏輯代碼中,多加判空條件,是避免這種錯(cuò)誤的最簡(jiǎn)易方式。即便某些數(shù)據(jù)在定義時(shí)是必須存在的,但我們無(wú)法完全保證,存在各種原因?qū)е逻@些數(shù)據(jù)為空或undefined。保證代碼的嚴(yán)謹(jǐn)性,是應(yīng)對(duì)不確定性異常的根本方式。當(dāng)然有些稍微復(fù)雜的情況下,需要特殊處理,如后兩種場(chǎng)景。
2. JSON.parse解析出錯(cuò)
這種錯(cuò)誤也十分常見(jiàn)。當(dāng)我們轉(zhuǎn)換一個(gè)JSON字符串時(shí),如果這個(gè)字符串的格式并不是一個(gè)標(biāo)準(zhǔn)的JSON格式,那轉(zhuǎn)換肯定失敗??梢栽贘SON.parse()時(shí),使用try-catch進(jìn)行包裹,以便對(duì)錯(cuò)誤信息進(jìn)行分析,如
當(dāng)然,在每處JSON.parse()的地方都執(zhí)行try-catch會(huì)有些麻煩,更推薦的方式是,在app.ux中提前代理JSON.parse(),使用try-catch包圍,如
export function parseProxy() { const rawParse = JSON.parse JSON.parse = function (str, defaults) { try { return rawParse(str) } catch (err) { console.error( ` JSON解析失敗:$ {str}, $ {err.stack}` )return defaults } } }
3、ViewModel回調(diào)函數(shù)異常場(chǎng)景
用戶打開(kāi)PageA,然后在該頁(yè)面中執(zhí)行接口方法(如fetch請(qǐng)求),然后立即跳轉(zhuǎn)到PageB;此時(shí)接口的回調(diào)函數(shù)返回,但PageA已經(jīng)出棧銷(xiāo)毀,此時(shí),執(zhí)行開(kāi)發(fā)者傳遞的回調(diào)函數(shù)報(bào)錯(cuò)。
這是由于,回調(diào)函數(shù)中訪問(wèn)了一些data數(shù)據(jù)等,而這些ViewModel的數(shù)據(jù)屬性已經(jīng)伴隨著頁(yè)面銷(xiāo)毀而刪除了,所以引起報(bào)錯(cuò)。對(duì)于這種異常,通??刹捎靡韵路绞浇鉀Q。
A. 在回調(diào)函數(shù)執(zhí)行之前,通過(guò)ViewModel對(duì)象的$valid、$visible判斷頁(yè)面狀態(tài)
B. 在Function.prototype上定義方法,關(guān)聯(lián)到每個(gè)回調(diào)函數(shù)綁定ViewModel實(shí)例。
/** * 在Function原型上定義bindPage方法:將回調(diào)函數(shù)綁定到頁(yè)面對(duì)象,頁(yè)面不可見(jiàn)或者銷(xiāo)毀時(shí),不執(zhí)行回調(diào)函數(shù) */ export function bindPageLC () { Function.prototype.bindPage = function (vmInst) { const fn = this return function () { if (!vmInst) { throw new Error(`使用錯(cuò)誤:請(qǐng)傳遞VM對(duì)象`) } if (vmInst.$valid && vmInst.$visible) { return fn(...arguments) } else { console.info(`頁(yè)面不可見(jiàn)或者銷(xiāo)毀時(shí),不執(zhí)行回調(diào)函數(shù)`) } } } }
在${anyPage}.ux中,通過(guò)fn.bindPage(this),在回調(diào)函數(shù)上綁定ViewModel實(shí)例
export default { data () { return {} }, request () { // 調(diào)用bindPage(this)返回:綁定了頁(yè)面對(duì)象的回調(diào)函數(shù),當(dāng)頁(yè)面不可見(jiàn)或者銷(xiāo)毀時(shí),不執(zhí)行回調(diào)函數(shù) fetch.fetch({ success: function(ret) { // 數(shù)據(jù)操作等 }.bindPage(this) }) } }
C. 通常在頁(yè)面發(fā)送請(qǐng)求時(shí),頁(yè)面需要添加loading處理,以防止用戶在此時(shí)進(jìn)行其他操作,當(dāng)然這種方式是從業(yè)務(wù)角度規(guī)避了這個(gè)異常。不過(guò)確是一種很常用的方式??山Y(jié)合方式B以保證代碼嚴(yán)謹(jǐn)性。
結(jié)構(gòu)優(yōu)化
結(jié)構(gòu)優(yōu)化的目的是減小頁(yè)面以及整體rpk包的體積,減少冗余代碼
常用的手段有以下幾項(xiàng):
A. 在app.ux中引入常用的JS庫(kù),并暴露給每個(gè)頁(yè)面使用;可以避免每個(gè)頁(yè)面在打包時(shí)對(duì)JS的重復(fù)定義
B. 項(xiàng)目?jī)?nèi)部的代碼抽象封裝,如封裝常用的工具類(lèi)函數(shù),封裝統(tǒng)一的Fetch請(qǐng)求方法,這些封裝可作為公共方法提供給各個(gè)頁(yè)面,便于維護(hù)的同時(shí),也有效降低了代碼量。
結(jié)尾
優(yōu)化的目的是為了提高代碼的可維護(hù)性以及應(yīng)用性能,可以說(shuō),正是多種多樣的優(yōu)化手段,讓邏輯性極強(qiáng)的代碼變的充滿藝術(shù)性。為了越發(fā)優(yōu)雅地完成編碼,我相信這個(gè)話題會(huì)一直探討下去。