微信小程序開發(fā)總結(jié)與心得

0 前言


最近的工作重心一直在小程序,也開發(fā)了幾個(gè)小程序,對小程序開發(fā)的流程及相關(guān)技術(shù)相對比較熟悉,在開發(fā)過程中也總結(jié)了一些心得經(jīng)驗(yàn)、了解一些小程序文檔上沒有的東西、踩了一些坑。所以想著寫篇文章記錄下來,并借此將小程序開發(fā)的相關(guān)知識進(jìn)行梳理,方便以后參考,也作為自己工作的階段性總結(jié)。同時(shí)也希望可以通過文章,結(jié)識更多朋友,多交流,互相學(xué)習(xí),共同進(jìn)步。另文章若有不對之處,還望指出與不吝賜教。

1 微信小程序基本知識與概念


微信小程序開發(fā),入門算是非常簡單,只要看官文文檔即可小程序簡易教程。如何申請小程序賬號,如何開發(fā)自己第一個(gè)小程序,如何發(fā)布,這一系列hello world操作官方文檔都有手把手教學(xué)。小程序開發(fā)的每個(gè)步驟,提供的能力文檔里都有,個(gè)人覺得,做小程序開發(fā),有事沒事都看下文檔,因?yàn)樾〕绦蚋卤容^快速,同時(shí)一些細(xì)小的能力我們可能會漏掉,所以多看文檔。

1.1 簡單說下目錄結(jié)構(gòu)和app.json


文件目錄結(jié)構(gòu)很靈活

先來看看小程序項(xiàng)目的文件目錄結(jié)構(gòu)

文件目錄結(jié)構(gòu)

除了app.json必須位于根目錄下,其他文件隨意,并且都可以刪。并且頁面文件可以放到如何位置,只要在app.json中的pages中配置了就可以??梢哉f是很靈活。你還可以多個(gè)頁面放在同個(gè)文件夾下(我相信你不會這樣做的,何必糟蹋自己呢)。


image

接下來簡單介紹下各個(gè)文件:

全局配置文件app.json

對于一個(gè)小程序項(xiàng)目而言,最重要的文件是app.json,它也是開發(fā)工具識別一個(gè)文件夾是否為小程序項(xiàng)目的標(biāo)識。當(dāng)使用開發(fā)者工具創(chuàng)建一個(gè)項(xiàng)目是,如果選擇的是空文件夾,它會創(chuàng)建一個(gè)新的項(xiàng)目。如果是一個(gè)有文件的文件夾,它會看該文件夾中是否有app.jon文件,如果有,則它會認(rèn)為是一個(gè)小程序項(xiàng)目,則會打開該項(xiàng)目,如果文件夾中沒有app.json文件,則提示無法創(chuàng)建項(xiàng)目。

image

app.json必須放置于項(xiàng)目的根目錄下,它是小程序項(xiàng)目的全局配置文件。在小程序代碼包準(zhǔn)備完成進(jìn)行啟動后(下文會詳細(xì)介紹小程序從用戶點(diǎn)擊打開小程序到小程序銷毀的整個(gè)過程),會先讀取app.json文件,進(jìn)行小程序的初試化,比如初始化整個(gè)小程序外框樣式,獲取首頁頁面地址等。

其實(shí)小程序就是微信提供的一個(gè)容器,各個(gè)頁面就在這個(gè)容器里加載運(yùn)行銷毀

下面介紹下小程序的全局配置選項(xiàng):

注意:

  • 所有配置項(xiàng)key必須使用雙引號括起來,value值為字符串類型的也必須使用雙引號,不支持單引號
  • 因?yàn)樾〕绦蚬δ艿浅Q杆?,基礎(chǔ)庫版本更新也很快,所以下面的介紹是截止目前的最新版本庫2.4.0
  • pages

    "pages": [
        "pages/index/index",
        "pages/log/log"
    ]

在app.json中,pages選項(xiàng)是必須配置的。該配置項(xiàng)注冊了小程序所有頁面的地址,其中每一項(xiàng)都是頁面的 路徑+文件名 。配置的字符串其實(shí)就是每個(gè)頁面wxml路徑,去掉.wxml后綴。因?yàn)榭蚣軙詣尤ふ衣窂较?json、.js、.wxml、.wxss四個(gè)文件進(jìn)行整合。也就意味著.json、.js、.wxss這三個(gè)文件的文件名必須要和.wxml的一致,否則不生效。所以一個(gè)頁面至少必須得有.wxml文件。

總結(jié):

  • 頁面的.json、.js、.wxss文件必須與.wxml文件同名,否則不生效
  • 每個(gè)頁面都必須pages下注冊,沒有注冊的頁面,如果不訪問,編譯能通過,一旦試圖訪問該頁面則會報(bào)錯(cuò)
  • 可以通過在pages下添加一個(gè)選項(xiàng)快速新建一個(gè)頁面,開發(fā)工具會自動生成對應(yīng)的文件
  • window

  "window":{
    "enablePullDownRefresh": ture,
    "navigationStyle": "custom"
  }

該配置項(xiàng)用于配置小程序的全局外觀樣式,具體請查閱文檔。這里重點(diǎn)提一下兩個(gè)比較實(shí)用的

//去掉默認(rèn)的導(dǎo)航欄,輕松實(shí)現(xiàn)全面屏
"navigationStyle": "custom" , 
//開啟自帶的下拉刷新,減少自己寫樣式
"enablePullDownRefresh": ture, 
  • tabBar

該選項(xiàng)可以讓我們輕松實(shí)現(xiàn)導(dǎo)航欄tab效果,不過有個(gè)不足就是跳轉(zhuǎn)可操作性非常低。就是每個(gè)tab只能跳當(dāng)前小程序頁面,不同跳到其他小程序。如果需要跳到其他小程序,還需自己封裝個(gè)組件。

  • networkTimeout

這是網(wǎng)絡(luò)請求超時(shí)時(shí)間,可以設(shè)置不同類型請求的超時(shí)時(shí)間,比如wx.request、wx.uploadFile等。其實(shí)很多時(shí)候我們都會忽略這個(gè)選項(xiàng),小程序默認(rèn)是60s超時(shí),但我們應(yīng)該手動設(shè)置更低的值,因?yàn)槲覀兊慕涌谝话愣紩?0s內(nèi)完成請求(如果超過10s,那你是時(shí)候優(yōu)化了),所以如果網(wǎng)絡(luò)或者服務(wù)器出問題了,那么會讓用戶等60s,最后還是失敗,這對用戶很不友好,還不如提前告訴用戶,現(xiàn)在出問題了,請稍后再試。

前段時(shí)間由于公司服務(wù)器網(wǎng)關(guān)出現(xiàn)了點(diǎn)小問題,導(dǎo)致有些請求連接不上,出現(xiàn)大量連接超時(shí)。通過之前添加的錯(cuò)誤信息收集插件(這個(gè)是性能優(yōu)化,下文有講到)看到了很多接口返回time-out 60s。讓用戶等了60s還是失敗,這不友好。所以這個(gè)超時(shí)時(shí)間一般設(shè)置15s-30s比較好。

  • debug

是否開啟debug功能,開啟后查看更多的調(diào)試信息,方便定位問題,開發(fā)階段可以考慮開啟

  • functionalPages

這個(gè)是結(jié)合插件使用的,因?yàn)槲⑿判〕绦虿寮泻艽笙拗?,插件里提供的api很有限,wx.login 和 wx.requestPayment 在插件中不能使用,如果需要獲取用戶信息和進(jìn)行支付,就必須通過插件提供的功能也實(shí)現(xiàn)。當(dāng)你的小程序下的插件啟用了插件功能也時(shí),必須設(shè)置該選項(xiàng)為true

小程序插件必須掛載在一個(gè)微信小程序中,一個(gè)小程序也只能開通一個(gè)插件。當(dāng)你小程序開通的插件啟用了插件功能也時(shí),必須設(shè)置該選項(xiàng)為true

  • plugins

    "plugins": {
        "myPlugin": {
            "version": "1.0.0",
            "provider": "wxidxxxxxxxxxxxxxxxx"
        }
    }

當(dāng)小程序使用了插件就必須在這里聲明引入。小程序自身開通的小程序不能在本身應(yīng)用

  • navigateToMiniProgramAppIdList

    "navigateToMiniProgramAppIdList": [
        "wxe5f52902cf4de896"
    ]

之前小程序之間只要是關(guān)聯(lián)了通過公眾號就可以相互跳轉(zhuǎn),如今微信做出了限制,要這個(gè)這里配置好需要跳轉(zhuǎn)的小程序,上限為10個(gè),還必須寫死,不支持配置。所以當(dāng)小程序有跳轉(zhuǎn)到其他小程序,一定要配好這個(gè),否則無法跳轉(zhuǎn)。

  • usingComponents

  "usingComponents": {
    "hello-component": "plugin://myPlugin/hello-component"
  }

使用自定義組件或者插件提供的組件前,必須先在這里聲明

1.2 小程序啟動與生命周期


下面來說說小程序從用戶點(diǎn)擊打開到銷毀的整個(gè)過程。用圖說話更清晰,特地畫了個(gè)流程圖:

image

小程序啟動會有兩種情況,一種是「冷啟動」,一種是「熱啟動」。 假如用戶已經(jīng)打開過某小程序,然后在一定時(shí)間內(nèi)再次打開該小程序,此時(shí)無需重新啟動,只需將后臺態(tài)的小程序切換到前臺,這個(gè)過程就是熱啟動;冷啟動指的是用戶首次打開或小程序被微信主動銷毀后再次打開的情況,此時(shí)小程序需要重新加載啟動。

上面的流程圖包含了所有內(nèi)容,但畢竟文字有限,接下來詳細(xì)說下幾個(gè)點(diǎn)。

  1. 小程序會先檢測本地是否有代碼包,然后先使用本地代碼包進(jìn)行小程序啟動,再異步去檢測遠(yuǎn)端版本。這就是小程序的離線能力,相對于H5,這是優(yōu)點(diǎn),能加快小程序啟動速度。
  2. 當(dāng)本地有小程序代碼包時(shí),會異步去請求遠(yuǎn)端是否有最新版本。有則下載到本地,但該次的啟動還是會用之前的代碼。所以當(dāng)我們發(fā)布了最新的版本,需要用戶兩次冷啟動,才能使用到最新版本。如果想要用戶一次冷啟動就可以使用到最新版本,可以使用小程序提供的版本更新API更新。代碼如下,只要在app.js的onShow函數(shù)加上以下代碼,每次小程序有更新,都會提示用戶更新小程序。不過這個(gè)每次提示更新,一定程度上影響用戶體驗(yàn)。如果結(jié)合后端配置,每次進(jìn)來讀取配置,就可以實(shí)現(xiàn)根據(jù)需要是否進(jìn)行該版本的更新,比如一定需要用戶更新才能使用的,那就使用強(qiáng)制更新。對于一些小版本,就不需要使用這個(gè)強(qiáng)制更新。
    if (wx.canIUse('getUpdateManager')) {
        //檢測是否有版本更新
        var updateManager = wx.getUpdateManager()
        updateManager.onCheckForUpdate(function (res) {
            // 請求完新版本信息的回調(diào),有更新
            if (res.hasUpdate) {
                wx.showLoading({
                    title: '檢測到新版本',
                })
            }
        })
        updateManager.onUpdateReady(function () {
            wx.hideLoading();
            wx.showModal({
                title: '更新提示',
                content: '新版本已經(jīng)準(zhǔn)備好,是否重啟應(yīng)用?',
                success: function (res) {
                    if (res.confirm) {
                        //清楚本地緩存
                        try {
                            wx.clearStorageSync()
                        } catch (e) {
                            // Do something when catch error
                        }
                        // 新的版本已經(jīng)下載好,調(diào)用 applyUpdate 應(yīng)用新版本并重啟
                        updateManager.applyUpdate()
                    }
                }
            })
        })
        updateManager.onUpdateFailed(function () {
            // 新的版本下載失敗
            console.log('新版本下載失敗');
        })
    }

1.3 開發(fā)工具


對于小程序開發(fā)工具,還沒有一款讓開發(fā)者滿意的工具,至少我不滿意,哈哈哈!微信提供的微信開發(fā)者工具。除了編譯器不行外,其他都還行。但由于開發(fā)工具、ios、android三個(gè)平臺運(yùn)行小程序的內(nèi)核不同。所以有時(shí)會出現(xiàn)開發(fā)工具上沒問題,真機(jī)有問題的情況,特別是樣式,可以通過在開發(fā)工具中設(shè)置上傳代碼時(shí)樣式自動補(bǔ)全來解決大多數(shù)問題。另外微信開發(fā)者工具提供了真機(jī)調(diào)試功能,該功能對真機(jī)調(diào)試非常方便

還有就是可以自定義編譯條件

image

可以模擬任意場景值、設(shè)置頁面參數(shù)、模擬更新等?;緷M足了所有的調(diào)試。不過還有一些效果,開發(fā)工具和真機(jī)可能會不同,所以還是需要在真機(jī)上確認(rèn)。

1.4 測試-審核-上線的那些事


服務(wù)器域名request合法域名每個(gè)月只能修改5次。所以不應(yīng)該每次請求一個(gè)新域名就添加一次。在開發(fā)階段,在微信開發(fā)者工具上勾上不校驗(yàn)合法域名,真機(jī)上需要開啟調(diào)試模式,就可以先不配置合法域名的情況下請求任何域名甚至ip地址。待開發(fā)完成了,再一次性配置所有合法域名,在微信開發(fā)者工具上取消不校驗(yàn)合法域名,真機(jī)上關(guān)閉調(diào)試模式,然后開始測試。

使用體驗(yàn)版+線上環(huán)境的接口,這就是和線上環(huán)境一模一樣的,所以在發(fā)布前,使用體驗(yàn)版+線上環(huán)境過一遍。如果沒問題,發(fā)布以后也就沒問題了。

小程序二維碼只要發(fā)布了線上版本調(diào)用生成小程序二維碼接口才能成功返回二維碼。而且二維碼識別是線上版本,所以還未發(fā)布的小程序是無法生成二維碼的。

線上版本有個(gè)版本回退功能,這里有個(gè)坑,就是版本回退以后,退回的版本需要重新審核才能發(fā)布


image

還有設(shè)置體驗(yàn)版時(shí)可以設(shè)置指定路徑和參數(shù),這樣很方便測試

image

2 重點(diǎn)介紹幾個(gè)組件


接下來說說使用頻率比較多,功能強(qiáng)大,但又有比較多坑的幾個(gè)組件

2.1 web-view


web-view的出現(xiàn),讓小程序和H5網(wǎng)頁之前的跳轉(zhuǎn)成為了可能。通過把H5頁面放置到web-view中,可以讓H5頁面在小程序內(nèi)運(yùn)行。同時(shí)在H5頁面中也可以跳轉(zhuǎn)回小程序頁面??梢哉f是帶來了很大的便利,但同時(shí)由于web-view的諸多限制,用起來也不是很舒服。

  1. 需要打開的H5頁面必須在后臺業(yè)務(wù)頁面中配置,這其中還有個(gè)服務(wù)校驗(yàn)。另外H5頁面必須是https協(xié)議,否則無法打開
  2. web-view中無法在頁面中調(diào)起分享,如果需要分享,比如跳回小程序原生頁面
  3. 小程序與web-view里H5通信問題。小程序向web-view傳遞,不敏感信息可以通過頁面url傳遞。如果是敏感信息比如用戶token等,可以讓服務(wù)端重定向,比如請求服務(wù)端一個(gè)地址,讓他把敏感信息寫在cookie中,再重定向到我們的H5頁面。之后H5頁面就可以通過在cookie中拿這些敏感數(shù)據(jù)了,或者h(yuǎn)ttp-only,發(fā)送請求時(shí)直接帶上。
  4. 每次web-view中src值有變化就會重新加載一次頁面。所以個(gè)src拼接參數(shù)時(shí),需要先賦值給個(gè)變量拼接好再一次性setData給web-view的src,防止頁面重復(fù)刷新
  5. 從微信客戶端6.7.2版本開始,navigationStyle: custom對<web-view>組件無效。也就意味著使用web-view時(shí),自帶的導(dǎo)航欄無法去掉。
  6. 因?yàn)閷?dǎo)航欄無法去掉,這里就出現(xiàn)了一個(gè)巨大的坑。實(shí)現(xiàn)全屏效果問題。如果想要實(shí)現(xiàn)H5頁面全屏,就是不滑動,全屏顯示完所有內(nèi)容。這時(shí)如果你使用width:100%;height:100%,你會發(fā)現(xiàn),你頁面底部可能會缺失一段。上圖:
image

因?yàn)閣eb-view是默認(rèn)鋪滿全屏的,也就是web-view寬高和屏幕寬高一樣。然后H5頁面這是高度100%,這是相對web-view的高度,也是屏幕高度。但是關(guān)鍵問題:web-view里H5頁面是從導(dǎo)航欄下開始渲染的。這就導(dǎo)致了H5頁面溢出了屏幕,無法達(dá)到全屏效果。

解決方法

這個(gè)問題我在前段時(shí)間的實(shí)際項(xiàng)目碰到過,我們要做個(gè)H5游戲,要求是全屏,剛開始我也是設(shè)置高度100%。后來發(fā)現(xiàn)底部一塊不見了。我的解決方法比較粗暴,如果有更好的解決方法,歡迎評論交流。
我的解決方法是:通過拼接寬高參數(shù)在H5頁面url上,這個(gè)寬高是在web-view外層計(jì)算好的。H5頁面直接讀取url上的寬高,動態(tài)設(shè)置頁面的寬高。頁面高度的計(jì)算,根據(jù)上圖,很顯然就是屏幕高度減去導(dǎo)航欄高度。寬度都是一樣的,直接是屏幕寬度。

但問題又來了,貌似沒有途徑獲取導(dǎo)航欄高度。而且對于不同機(jī)型的手機(jī),導(dǎo)航欄高度不同。經(jīng)過了對多個(gè)機(jī)型導(dǎo)航欄跟屏幕高度的比較。發(fā)現(xiàn)了一個(gè)規(guī)律,導(dǎo)航欄高度與屏幕高度、屏幕寬高比有一定的關(guān)系。所以根據(jù)多個(gè)機(jī)型就計(jì)算出了這個(gè)比例。這解決了95%以上手機(jī)的適配問題,只有少數(shù)機(jī)型適配不是很好。到基本實(shí)現(xiàn)了全屏效果。具體代碼如下:

onLoad (options) {
    //同步獲取屏幕信息,現(xiàn)在用到的是屏幕寬高
    var res = wx.getSystemInfoSync();
    if (res) {
        var widHeight = res.screenHeight;
        //對于大多數(shù)手機(jī),屏幕高度/屏幕寬度 = 1.78。此時(shí)導(dǎo)航欄占屏幕高度比為0.875
        var raito = 0.875;
        if (res.screenHeight / res.screenWidth > 1.95) {
            //對于全屏手機(jī),這個(gè)占比會更高些
            raito = 0.885;
        } else if (res.screenHeight / res.screenWidth > 1.885) {
            raito = 0.88;
        }
        //做兼容處理,只有微信版本庫高于6.7.2,有導(dǎo)航欄才去兼容,否則可以直接使用高度100%。res.statusBarHeight是手機(jī)頂部狀態(tài)欄高度
        //如果微信版本號大于6.7.2,有導(dǎo)航欄
        if (util.compareVersion(res.version, "6.7.2") > 0) {
            widHeight = Math.round(widHeight * raito) + (res.statusBarHeight || 0);
        }
        this.setDate({
            //將H5頁面寬高拼接在url上,賦值給web-view的src即可加載出H5頁面
            webview_src: util.joinParams(h5_src, {
                "height": widHeight, 
                "width": res.screenWidth
            })
        })
    }
}

2.2 scroll-view


當(dāng)我們要實(shí)現(xiàn)一個(gè)區(qū)域內(nèi)滑動效果時(shí),在H5頁面中我們設(shè)置overflow-y: scroll即可。但在小程序中,沒有該屬性。需要用到scroll-view標(biāo)簽。具體操作實(shí)現(xiàn)我們可以查看文件scroll-view

錨點(diǎn)定位在前端開發(fā)中會經(jīng)常用到,在H5頁面中,我們會在url后面加上#來實(shí)現(xiàn)錨點(diǎn)定位效果。但是在小程序中這樣是不起作用的,因?yàn)樾〕绦騼?nèi)渲染頁面的容易不是一個(gè)瀏覽器,無法實(shí)時(shí)監(jiān)聽Hash值得變化。但是使用scroll-view,我們可以實(shí)現(xiàn)錨點(diǎn)點(diǎn)位效果。主要是使用scroll-into-vie屬性具體實(shí)現(xiàn)我們直接上代碼

scroll-into-view | String | 值應(yīng)為某子元素id(id不能以數(shù)字開頭)。設(shè)置哪個(gè)方向可滾動,則在哪個(gè)方向滾動到該元素

wxml文件

    <!--toView的值動態(tài)變化,當(dāng)toView為luckydraw時(shí),會定位到id為luckydraw的view
    需要注意的是,這里需要設(shè)置高度為屏幕高度-->
    <scroll-view scroll-y scroll-into-view="{{toView}}" 
    scroll-with-animation = "true" style="height: 100%; white-space:nowrap">
        <view id="top"></view>
        <view id="luckydraw"></view>
        <view id="secskill"></view>
    <scroll-view>

2.3 canvas


畫布標(biāo)簽,它是原生組件,所以它必須位于屏幕最上邊,而且是不能隱藏的。所以如果想要使用canvas動態(tài)生成分享照片。那你要設(shè)置她的寬高和屏幕一樣。要不導(dǎo)出為照片時(shí)就會失真。因?yàn)檫@個(gè)原因,所以生成分享照片還是有服務(wù)端實(shí)現(xiàn)吧,照片失真太嚴(yán)重了。

3 formid收集


給用戶發(fā)送消息對一個(gè)小程序是非常重要的,它可以召喚回用戶,導(dǎo)量效果非常明顯。我們可以通過模板消息想小程序用戶發(fā)送消息,但前提是我們得獲取到openid和formid。用戶登錄我們即可即可獲取到用戶openid。而只要用戶有點(diǎn)擊行為,我們即可獲取到formid獲取formid。所以說formid是很重要的。我們可以提前收集好formid,在需要的時(shí)候給用戶推送消息。我們可以個(gè)每個(gè)button都包上form標(biāo)簽,只要有用戶點(diǎn)擊行為都可以收集到formid.

    <form bindsubmit="formSubmit" report-submit='true'>
        <button  formType="submit">點(diǎn)擊</button>
    </form>

我們實(shí)現(xiàn)一個(gè)formid收集系統(tǒng),為了盡量減少冗余代碼和減少對業(yè)務(wù)的影響,我們的設(shè)計(jì)是這樣的

  1. 在整個(gè)頁面的最外層包裹form標(biāo)簽,不是每個(gè)button都包裹一個(gè),這樣只要是頁面中formTpye=submit的button有點(diǎn)擊都能獲取到formid。
  2. formid保存在全局變量數(shù)組中,當(dāng)小程序切換到后臺是一次性發(fā)送。
  3. 對于需要實(shí)時(shí)發(fā)送消息的,不添加值全局?jǐn)?shù)組中,直接保存在頁面變量中。

wxml文件

    <!--在整個(gè)頁面的最外層包裹form標(biāo)簽,這樣就不同對每個(gè)button都包裹一個(gè)form標(biāo)簽,代碼簡潔-->
    <form bindsubmit="formSubmit" report-submit='true'>
        <view>頁面內(nèi)容</view>
        <view>頁面內(nèi)容</view>
        <button  formType="submit">點(diǎn)擊</button>
        <view>頁面內(nèi)容</view>
        <view>
            <button  formType="submit">點(diǎn)擊</button>
        </view>
    </form>

page.js文件

    //每次用戶有點(diǎn)擊,都將formid添加到全局?jǐn)?shù)組中
    formSubmit(e) {
        //需要實(shí)時(shí)發(fā)送的,不添加
        if(e.target.dataset.sendMsg){
            formid =  e.detail.formId;
            return;
        }
        app.appData.formIdArr.push(e.detail.formId);
    }

app.js

    onHide: function () {
        //小程序切到后臺時(shí)上傳formid
        this.submitFormId();
    },

4 性能優(yōu)化相關(guān)


從用戶打開小程序到小程序銷毀,我們可以想想有哪些地方是可以優(yōu)化的。首先是打開速度。小程序打開速度直接影響了用戶留存。在小程序后臺,運(yùn)維中心-監(jiān)控告警下有個(gè)加載性能監(jiān)控?cái)?shù)據(jù),我們可以看到小程序啟動總耗時(shí)、下載耗時(shí)、首次渲染耗等加載相關(guān)的數(shù)據(jù)。而這里的打開速度其實(shí)就是小程序的啟動總耗時(shí)。它包括了代碼包下載、首次渲染,微信內(nèi)環(huán)境初始化等步湊。在這一步,我們能做的就是如何加快代碼包下載速度和減少首次渲染時(shí)間

在小程序呈現(xiàn)給用戶之后,接下來如何提高用戶體驗(yàn),增強(qiáng)小程序健壯性的問題了。每個(gè)程序都有bug。只是我們沒發(fā)現(xiàn)而已,盡管在測試階段,我們進(jìn)行了詳盡的測試。但是在實(shí)際生產(chǎn)環(huán)境,不同的用戶環(huán)境,不同的操作路徑,隨時(shí)會觸發(fā)一些隱藏的bug。這時(shí)如果用戶沒有向我們報(bào)告,我們是無法獲知的。所以有必要給我們的小程序增加錯(cuò)誤信息收集,js腳本錯(cuò)誤,意味著整個(gè)程序掛掉了,無法響應(yīng)用戶操作。所以對于運(yùn)行時(shí)的腳本錯(cuò)誤,我們應(yīng)該上報(bào)。對出現(xiàn)的bug及時(shí)修復(fù),增強(qiáng)程序健壯性,提供用戶體驗(yàn)。

每個(gè)程序都有大量的前后端數(shù)據(jù)交互,這是通過http請求進(jìn)行的。因此,還有一個(gè)錯(cuò)誤信息收集就是接口錯(cuò)誤信息收集。對那些請求狀態(tài)碼非2XX、3XX的,或者請求接口成功了,但是數(shù)據(jù)不是我們預(yù)期的,都可以進(jìn)行信息采集。

通過對小程序運(yùn)行時(shí)腳本和http請求進(jìn)行監(jiān)控,我們就可以實(shí)時(shí)了解我們線上小程序的運(yùn)行狀況,有什么問題可以及時(shí)發(fā)現(xiàn),及時(shí)修復(fù),極高地提高了用戶體驗(yàn)性。

4.1 讓小程序更快


讓小程序快,主要因素有兩個(gè),代碼包下載和首屏渲染。
我們來看一個(gè)數(shù)據(jù):

image

前面狀態(tài)小程序代碼大小是650Kb左右,這是下載耗時(shí)(雖然跟用戶網(wǎng)絡(luò)有關(guān),但這個(gè)是全部用戶平均時(shí)間)是1.3s左右。但是經(jīng)過優(yōu)化,將代碼包降低至200kb左右時(shí)。下載耗時(shí)只有0.6s左右。所以說,代碼包減少500kb,下載耗時(shí)能減少0.5s。這個(gè)數(shù)據(jù)還是非常明顯和。所以說,在不影響業(yè)務(wù)邏輯的情況下,我們小程序代碼包應(yīng)該盡可能地小。那么如何降低代碼包大小呢?以下有幾點(diǎn)可以參考

  1. 因?yàn)槲覀兩蟼鞔a到微信服務(wù)器時(shí),它會將我們的代碼進(jìn)行壓縮的,所以用戶下載的代碼包并不是我們開發(fā)時(shí)的那個(gè)大小。對此,開發(fā)時(shí)也沒必要刪空行、刪注釋這些。在開發(fā)工具項(xiàng)目詳情中可以看到上次上傳大小,這個(gè)大小就是用戶最終使用的大小。如果覺得微信壓縮還不夠好,可以通過第三方工具對我們代碼進(jìn)行一次壓縮再上傳,然后對比效果,有沒有更小。這個(gè)沒有使用過。如果有什么好工具,歡迎推薦。
  2. 將靜態(tài)資源文件防止到我們自己服務(wù)器或者cdn上。一個(gè)小程序,最耗空間的往往是圖片文件。所以我們可以抽離出來,圖片文件可以異步獲取,在小程序啟動以后再去獲取。這樣,代碼包就會小很多。
  3. 使用分包加載。小程序提供了分包加載功能。如果你的小程序很龐大,可以考慮使用分包加載功能,先加載必要功能代碼。這樣就是可以極大降低代碼包大小

接下來是首屏渲染,從上圖的小程序生命周期可以看出,從加載首頁代碼帶首頁完成渲染,這段時(shí)間就是白屏?xí)r間,也就是首次渲染時(shí)間。而小程序在這段時(shí)間內(nèi),主要工作是:加載首頁代碼、創(chuàng)建View和AppService層、初試數(shù)據(jù)傳輸、頁面渲染。在這四個(gè)步驟中,加載首頁代碼,前面已經(jīng)說過;創(chuàng)建View和AppService層,是微信完成的,跟用戶手機(jī)有關(guān),這不是我們可控的。我們能做的就是減少初試數(shù)據(jù)傳輸時(shí)間和頁面渲染時(shí)間。

  1. 我們知道page.js中的data對象在首次渲染時(shí)會通過數(shù)據(jù)管道傳個(gè)視圖層進(jìn)行頁面渲染。所以我們應(yīng)該控制這個(gè)data對象的大小。對于與視圖渲染無關(guān)的數(shù)據(jù),不要放在data里面,可以設(shè)置個(gè)全局變量來保存。
    Page({
        //與頁面渲染有關(guān)的數(shù)據(jù)放這里
        data: {
            goods_list:[]
        },
        //與頁面渲染無關(guān)的數(shù)據(jù)放這里
        _data: {
            timer: null
        }
    })
  1. 頁面渲染速度還跟html的dom結(jié)構(gòu)有關(guān)。這一點(diǎn)的優(yōu)化空間算是非常少了,就是寫高質(zhì)量html代碼,減少dom嵌套,讓頁面渲染速度快一丟丟。

4.2 讓小程序更強(qiáng)


接下來就是給小程序增加錯(cuò)誤信息收集,包括js腳本錯(cuò)誤信息收集和http請求錯(cuò)誤信息收集。前段時(shí)間,在時(shí)間工作開發(fā)中,為了更好的復(fù)用和管理,我把這個(gè)錯(cuò)誤信息收集功能做成了插件。然而做成插件并沒有想象中的那么美好,下面再具說。

腳本錯(cuò)誤收集

對于腳本錯(cuò)誤收集,這個(gè)相對比較簡單,因?yàn)樵赼pp.js中提供了監(jiān)聽錯(cuò)誤的onError函數(shù)

image

只不過錯(cuò)誤信息是包括堆棧等比較詳細(xì)的錯(cuò)誤信息,然后當(dāng)上傳時(shí)我們并不需要這么信息,第一浪費(fèi)寬帶,第二看著累又無用。我們需要的信息是:錯(cuò)誤類型、錯(cuò)誤信息描述、錯(cuò)誤位置。

thirdScriptError
aa is not defined;at pages/index/index page test function
ReferenceError: aa is not defined
    at e.test (http://127.0.0.1:62641/appservice/pages/index/index.js:17:3)
    at e.<anonymous> (http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:31500)
    at e.a (http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:26386)
    at J (http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:20800)
    at Function.<anonymous> (http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:22389)
    at http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:27889
    at http://127.0.0.1:62641/appservice/__dev__/WAService.js:6:16777
    at e.(anonymous function) (http://127.0.0.1:62641/appservice/__dev__/WAService.js:4:3403)
    at e (http://127.0.0.1:62641/appservice/appservice?t=1543326089806:1080:20291)
    at r.registerCallback.t (http://127.0.0.1:62641/appservice/appservice?t=1543326089806:1080:20476)

這是錯(cuò)誤信息字符串,接下來我們對它進(jìn)行截取只需要拿我們想要的信息即可。我們發(fā)現(xiàn)這個(gè)字符串是有規(guī)則的。第一行是錯(cuò)誤類型,第二行是錯(cuò)誤詳情和發(fā)生的位置,并且是";"分好分開。所以我們還是很容易就可以拿到我們想要的信息。

    //格式化錯(cuò)誤信息
    function formateErroMsg(errorMsg){
        //包一層try catch 不要讓信息收集影響了業(yè)務(wù)
        try{
            var detailMsg = '';
            var detailPosition= '';
            var arr = errorMsg.split('\n')
            if (arr.length > 1) {
                //錯(cuò)誤詳情和錯(cuò)誤位置在第二行并用分好隔開
                var detailArr = arr[1].split(';')
                detailMsg = detailArr.length > 0 ? detailArr[0] : '';
                if (detailArr.length > 1) {
                    detailArr.shift()
                    detailPosition = detailArr.join(';') 
                }
            }

            var obj = {
                //錯(cuò)誤類型就是第一行
                error_type: arr.length > 0 ? arr[0] : '',
                error_msg: detailMsg,
                error_position: detailPosition
            };
            return obj
        }catch(e){}
    }

獲取到我們想要的信息,就可以發(fā)送到我們服務(wù)后臺,進(jìn)行數(shù)據(jù)整理和顯示,這個(gè)需要服務(wù)端配合,就不深入講了,我們拿到了數(shù)據(jù),其他都不是事。

http請求錯(cuò)誤信息收集
對于http請求錯(cuò)誤信息收集方式,我們盡量不要暴力埋點(diǎn),每個(gè)請求發(fā)送前發(fā)送后加上我們的埋點(diǎn)。這樣工作量太大,也不易維護(hù)。因此,我們可以從底層出發(fā),攔截wx.request請求。使用Object.definePropert對wx對象的request進(jìn)行重新定義。具體實(shí)現(xiàn)如下

function rewriteRequest(){
    try {
        const originRequest = wx.request;
        Object.defineProperty(wx, 'request', {
            configurable:true,
            enumerable: true,
            writable: true,
            value: function(){
                let options = arguments[0] || {};
                //對于發(fā)送錯(cuò)誤信息的接口不收集,防止死循環(huán)
                var regexp = new RegExp("https://xxxx/error","g");
                if (regexp.test(options.url)) {
                    //這里要執(zhí)行原來的方法
                    return originRequest.call(this, options)
                }
                //這里攔截請求成功或失敗接口,拿到請求后的數(shù)據(jù)
                ["success", "fail"].forEach((methodName) => {
                    let defineMethod = options[methodName];
                    options[methodName] = function(){
                        try{          //在重新定義函數(shù)中執(zhí)行原先的函數(shù),不影響正常邏輯
                            defineMethod && defineMethod.apply(this, arguments);
                            //開始信息收集
                            let statusCode, result, msg;
                            //請求失敗
                            if (methodName == 'fail') {
                                statusCode = 0;
                                result = 'fail';
                                msg = ( arguments[0] && arguments[0].errMsg ) || ""
                            }
                            //請求成功,
                            //收集規(guī)則為:
                            // 1、 statusCode非2xx,3xx
                            // 2、 statusCode是2xx,3xx,但接口返回result不為ok
                            if (methodName == 'success') {
                                let data = arguments[0] || {};
                                statusCode = data.statusCode || "";
                                if (data.statusCode && Number(data.statusCode) >= 200 && Number(data.statusCode) < 400 ) {
                                    let resData = data.data ? (typeof data.data == 'object' ? data.data : JSON.parse(data.data)) : {};
                                    //請求成功,不收集
                                    if (resData.result == 'ok') {
                                        return;
                                    }
                                    result = resData.result || "";
                                    msg = resData.msg || "";
                                }else{
                                    result = "";
                                    msg = data.data || "";
                                }
                            }
                            //過濾掉header中的敏感信息
                            if (options.header) {   
                                options.header.userid && (delete options.header.userid)
                            }
                            //過濾掉data中的敏感信息
                            if (options.data) { 
                                options.data.userid && (delete options.data.userid)
                            }
                            
                            var collectInfo = {
                                "url": options.url || '',   //請求地址
                                "method": options.method || "GET",  //請求方法
                                "request_header": JSON.stringify(options.header || {}), //請求頭部信息
                                "request_data": JSON.stringify(options.data || {}), //請求參數(shù)
                                "resp_code": statusCode + '',   //請求狀態(tài)碼
                                "resp_result": result, //請求返回結(jié)果
                                "resp_msg": msg, //請求返回描述信息
                            }
                            //提交參數(shù)與上一次不同,或者參數(shù)相同,隔了1s
                            if (JSON.stringify(collectInfo) != lastParams.paramStr || (new Date().getTime() - lastParams.timestamp > 1000)) {
                                //上傳錯(cuò)誤信息
                                Post.post_error(_miniapp, 'http', collectInfo)
                                lastParams.paramStr = JSON.stringify(collectInfo);
                                lastParams.timestamp = new Date().getTime()
                            }

                        }catch(e){
                            //console.log(e);
                        }
                    };  
                })
                return originRequest.call(this, options)
            }
        })
    } catch (e) {
        // Do something when catch error
    }
}

在不使用插件的小程序中,我們可以在使用wx.request方法執(zhí)行上面的代碼,對wx.request進(jìn)行攔截,然后其他無需加任何代碼就可以收集http請求了。
上面說了,當(dāng)我們封裝成到插件時(shí),這個(gè)就不管用了,因?yàn)楫?dāng)使用插件時(shí),小程序不允許我們修改全局變量。所以執(zhí)行上面代碼時(shí)會報(bào)錯(cuò)。這時(shí),我們退而求其次,只能是在插件中自己封裝個(gè)方法,這個(gè)方法其實(shí)就是wx.request發(fā)送請求,但是在插件中我們就有可以攔截wx.request了。具體實(shí)現(xiàn)如下:

    function my_request(){
        //只要執(zhí)行一次攔截代碼即可
        !_isInit && rewriteRequest();
        return  wx.request(options)
    }

接下來我們看下后臺數(shù)據(jù)

image
image

持續(xù)監(jiān)控,會幫我們找出很多隱藏的bug

4 總結(jié)


洋洋灑灑寫了這么多,或許有些地方說的不太清楚,慢慢鍛煉吧。然后后面幾點(diǎn)只是挑了重要的講,我相信有過小程序開發(fā)經(jīng)驗(yàn)的朋友應(yīng)該沒問題。然后有時(shí)間再補(bǔ)充和優(yōu)化了。先到此,有緣看到的朋友,歡迎留言交流。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,816評論 25 709
  • 1、通過CocoaPods安裝項(xiàng)目名稱項(xiàng)目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地?cái)?shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,172評論 3 119
  • 用兩張圖告訴你,為什么你的 App 會卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 13,918評論 2 59
  • fluentbit 是一個(gè)C實(shí)現(xiàn)的輕量級日志收集工具,相比 fluentd 資源占用要小,相對的插件數(shù)量上要比 f...
    chronos閱讀 4,684評論 0 3
  • 根據(jù)google的搜尋結(jié)果,正念mindfulness的定義是,不做任何評斷,主動地將注意力集中在當(dāng)下的經(jīng)驗(yàn)上。 ...
    AprilinMel閱讀 3,863評論 6 198

友情鏈接更多精彩內(nèi)容