騰訊課堂小程序開發(fā)實(shí)踐與思考
2022-11-28 加入收藏
本文由 InfoQ 整理自騰訊 CSIG 在線教育部前端高級(jí)開發(fā)工程師陳天忱在 GMTC 全球大前端技術(shù)大會(huì)(深圳站)2021 的分享《騰訊課堂小程序開發(fā)實(shí)踐》。
你好,我是陳天忱,來自騰訊 CSIG 在線教育部。我所在的團(tuán)隊(duì)主要負(fù)責(zé)騰訊課堂平臺(tái)的開發(fā)和維護(hù),我從加入團(tuán)隊(duì)以來就圍繞著小程序做了很多探索和優(yōu)化,目前也是騰訊課堂小程序的負(fù)責(zé)人。
我本次分享的內(nèi)容分為五個(gè)部分,首先我們從整體的角度來看一下騰訊課堂小程序的技術(shù)演進(jìn)過程,接著會(huì)分別從開發(fā)體驗(yàn)、性能優(yōu)化以及監(jiān)控體系三個(gè)角度分享一些實(shí)踐和經(jīng)驗(yàn),最后進(jìn)行一下總結(jié)。
在我剛進(jìn)入團(tuán)隊(duì)的時(shí)候,騰訊課堂小程序的工具鏈還處在比較原始的階段。除了在編碼層面利用了 web 比較成熟的 scss、postcss、lint、typescript 結(jié)合 gulp 做一些語法層面的編譯以外,在測(cè)試、構(gòu)建 npm、上傳、設(shè)置體驗(yàn)版、發(fā)布等階段都是依賴的小程序開發(fā)者工具和管理后臺(tái),人工手動(dòng)操作來完成的。
石器時(shí)代
在這個(gè)階段,大部分都是簡(jiǎn)單地利用一些現(xiàn)有的工具,我們稱之為騰訊課堂小程序的石器時(shí)代。
這個(gè)階段存在幾個(gè)明顯的問題:
構(gòu)建和上傳依賴人工操作,有可能會(huì)因?yàn)榱鞒滩僮魇д`而導(dǎo)致現(xiàn)網(wǎng)事故;
由于發(fā)布流程的不規(guī)范,需求并行時(shí)經(jīng)常會(huì)出現(xiàn)發(fā)布撞車的情況,導(dǎo)致體驗(yàn)版相互覆蓋造成預(yù)發(fā)布驗(yàn)證成本。
為了解決人工操作帶來的隱患,我們從零開始基于小程序提供的命令行工具打造了小程序 CI。讓源碼編譯、構(gòu)建 npm、上傳、生成開發(fā)版 / 體驗(yàn)版二維碼、自動(dòng)化測(cè)試等流程在 CI 流水中自動(dòng)流轉(zhuǎn)。
同時(shí)我們也將小程序 CI 與企業(yè)微信打通,將小程序的構(gòu)建進(jìn)度和小程序二維碼實(shí)時(shí)同步過去,而且也支持通過企業(yè)微信主動(dòng)觸發(fā)小程序構(gòu)建,解決了在測(cè)試過程中因?yàn)殚_發(fā)版二維碼過期而中斷測(cè)試的問題。
為了解決發(fā)布流程不規(guī)范的問題,我們將小程序的發(fā)布也接入到了業(yè)務(wù)發(fā)布平臺(tái)。在發(fā)布平臺(tái)上進(jìn)行 CheckList、CodeReview、發(fā)布評(píng)審、發(fā)布環(huán)境管理、發(fā)布靜態(tài)資源等流程的流轉(zhuǎn),確保需求發(fā)布的質(zhì)量、合規(guī)和有序。
解決了開發(fā)流程中的問題之后,我們將更多的精力放到了小程序的研發(fā)效能與性能上。開發(fā)和構(gòu)建階段我們打造跨端的公共模塊,通過 kbone 進(jìn)行同構(gòu)開發(fā),利用云開發(fā)來輔助首屏性能優(yōu)化,以及代替部分后臺(tái)的開發(fā),在構(gòu)建方面將構(gòu)建工具從 gulp 遷移到 webpack,能對(duì)構(gòu)建常務(wù)進(jìn)行更細(xì)致的優(yōu)化。
在發(fā)布之后,通過完善監(jiān)控告警,將發(fā)布質(zhì)量做到可視化的體現(xiàn),并能夠?qū)Τ霈F(xiàn)的問題得到及時(shí)的接收和感知,減少用戶的反饋。
到這里我們可以看到整個(gè)技術(shù)演進(jìn)的過程,它涵蓋了小程序開發(fā)、構(gòu)建、測(cè)試、部署發(fā)布以及監(jiān)控,形成了小程序的 DevOps 開發(fā)模式,這其中具體是怎么做的呢?
首先是開發(fā)階段,我們想要形成一個(gè)可以稱之為“爽”的開發(fā)體驗(yàn),并不僅僅是指 coding 階段,還需要覆蓋到測(cè)試以及發(fā)布階段:
編碼階段——業(yè)務(wù)邏輯跨端可復(fù)用
測(cè)試階段——改動(dòng)持續(xù)集成、測(cè)試與開發(fā)解藕
發(fā)布階段——規(guī)范化、流程化、可追溯
為此我們分別通過打造跨端可復(fù)用的公共模塊、小程序 CI、統(tǒng)一業(yè)務(wù)發(fā)布平臺(tái)來提升開發(fā)體驗(yàn)。
這里從一個(gè)實(shí)際場(chǎng)景出發(fā),我們之前有這樣一個(gè)需求:產(chǎn)品希望在各端的課程詳情頁有一個(gè)提示當(dāng)前此機(jī)構(gòu)正在直播的課程,可以引導(dǎo)用戶跳轉(zhuǎn)到直播間聽老師講解課程的細(xì)節(jié)。
梳理一下這個(gè)需求的流程,發(fā)現(xiàn)其實(shí)還是挺簡(jiǎn)單的:
詳情頁渲染完成后 -> 調(diào)用接口拉取直播間數(shù)據(jù) -> 渲染引導(dǎo)模塊 -> 用戶點(diǎn)擊跳轉(zhuǎn)直播
可以看到,業(yè)務(wù)的主邏輯在各端都是一樣的,但如果去看這些邏輯的細(xì)節(jié)就會(huì)發(fā)現(xiàn)其實(shí)各端需要的是不一樣的實(shí)現(xiàn),比如發(fā)起請(qǐng)求的 api 在瀏覽器和在小程序中是不一樣的;提示的疲勞度控制需要用到本地的緩存能力,瀏覽器和小程序的 api 也是不一樣的;然后在業(yè)務(wù)上,直播間在三端的頁面地址也是不一樣的。
通過 ifelse 或者 switch 的方式,在運(yùn)行時(shí)判斷當(dāng)前的執(zhí)行環(huán)境,然后調(diào)用不同的分支邏輯當(dāng)然是能夠?qū)崿F(xiàn)需求的,但是這種方式會(huì)讓一個(gè)端同時(shí)存在三端的邏輯,這樣的邏輯多了之后,會(huì)造成比較明顯的代碼冗余,而在小程序端由于有 2M 的包大小限制,對(duì)于代碼冗余是比較敏感的。
要解決代碼冗余的問題,大家會(huì)很自然地想到構(gòu)建時(shí)注入一個(gè)環(huán)境變量,通過 tree-shaking 的能力在不同端構(gòu)建出對(duì)應(yīng)端所需要用到的邏輯。但這個(gè)方案對(duì)于構(gòu)建工具有著一定的要求,而在實(shí)際的工作場(chǎng)景中,新老項(xiàng)目往往由于歷史的原因,不僅僅是源碼,在構(gòu)建上的技術(shù)棧也是有很多歷史包袱的,比如 gulp、fis、webpack 等,如果要全部統(tǒng)一起來成本和風(fēng)險(xiǎn)都會(huì)比較大。
所以我們需要一個(gè)能夠跨端復(fù)用,按需打包,而且不依賴項(xiàng)目構(gòu)建體系的公共模塊。
我們基于 git submodule 的方式從組件、業(yè)務(wù)、工具三個(gè)維出發(fā),每個(gè)維度根據(jù)具體的邏輯按照?qǐng)?zhí)行環(huán)境將其拆分成同構(gòu)目錄 (isomorph)、瀏覽器目錄 (lib)、小程序目錄 (wx),各個(gè)項(xiàng)目將 lib 目錄或者 wx 目錄作為引用的入口,而入口文件會(huì)繼承或者透?jìng)鲗?dǎo)出 isomorph 目錄下的邏輯,對(duì)環(huán)境有依賴的特殊邏輯則在 lib 目錄和 wx 目錄下分別實(shí)現(xiàn)。
在開發(fā)階段通過路徑別名來統(tǒng)一引用路徑,例如小程序的項(xiàng)目中設(shè)置tsconfig.json的paths為"ke-modules/*": "submodules/ke-modules/*/wx",這樣就可以統(tǒng)一業(yè)務(wù)層面的代碼邏輯。在構(gòu)建階段submodule會(huì)通過ts單獨(dú)編譯成js,如果是h5和PC的項(xiàng)目就只會(huì)將isomorph和lib目錄構(gòu)建到產(chǎn)物中,小程序的項(xiàng)目就只將isomorph和wx目錄構(gòu)建到產(chǎn)物中。
這樣就確保了在保證兼容性的基礎(chǔ)上不會(huì)產(chǎn)生冗余的代碼,以此來滿足我們之前提出的幾個(gè)需求。
我們搭建小程序的 CI/CD 的起因,是由于開發(fā)者工具中很多人工操作帶來的一系列問題,比如:
在構(gòu)建過程中,很容易漏掉構(gòu)建 npm 依賴
在上傳時(shí)的版本信息和版本號(hào)也不規(guī)范
不同需求的體驗(yàn)版需要管理后臺(tái)切換,需求并行非常不友好
開發(fā)版二維碼需要開發(fā)者實(shí)時(shí)提供,影響測(cè)試進(jìn)度要解決以上這些問題就需要從自動(dòng)化和流程控制來入手。
我們先是基于小程序官方提供的一個(gè)命令行工具進(jìn)行了封裝和擴(kuò)展,支持小程序的 npm 構(gòu)建、上傳、獲取二維碼、自動(dòng)獲取版本號(hào)、版本信息等功能,并作為小程序 CI 流水線中的核心插件。
CI 流水線支持通過 git hook、OpenAPI、手動(dòng)的方式觸發(fā)執(zhí)行。在流水線的流轉(zhuǎn)執(zhí)行中,完成代碼拉取、分支檢查、版本號(hào)迭代及版本信息更新、小程序代碼包上傳、開發(fā) / 體驗(yàn)版二維碼獲取,同時(shí)歸檔小程序產(chǎn)物、sourcemap 等文件便于對(duì)性能和錯(cuò)誤的分析。
同時(shí)通過 CI 的插件與企業(yè)微信機(jī)器人打通,將構(gòu)建進(jìn)度以及構(gòu)建后的小程序二維碼同步到企業(yè)微信中,同時(shí)也支持通過 @企業(yè)微信機(jī)器人以 openAPI 的形式主動(dòng)觸發(fā)流水線執(zhí)行,讓產(chǎn)品和測(cè)試都可以實(shí)時(shí)獲取最新的小程序二維碼進(jìn)行測(cè)試和體驗(yàn)。
在發(fā)布階段,與 web 項(xiàng)目一樣接入統(tǒng)一的業(yè)務(wù)發(fā)布平臺(tái),在發(fā)布平臺(tái)上對(duì)發(fā)布流程進(jìn)行規(guī)范,確保發(fā)布之前的 CheckList、CodeReview、發(fā)布評(píng)審等流程正確執(zhí)行。發(fā)布開始對(duì)發(fā)布環(huán)境進(jìn)行管理,設(shè)置門禁,確保同一時(shí)間只會(huì)有一個(gè)需求處在發(fā)布流程中,避免多需求并行出現(xiàn)的發(fā)布混亂問題,發(fā)布完成并現(xiàn)網(wǎng)觀察沒有問題之后再將環(huán)境釋放給下一個(gè)需求。
建設(shè)了這樣一套 CI/CD 的流程之后,之前遇到的問題就都得到了解決。
通過在構(gòu)建過程中獲取依賴的 npm 信息來判斷是否需要更新及構(gòu)建 npm,并自動(dòng)執(zhí)行;
上傳時(shí)根據(jù) Angular 的 git commit 規(guī)范,自動(dòng)迭代 major、minor、patch 的版本號(hào),更新 changelog;
CI 使用機(jī)器人賬號(hào)上傳小程序,通過業(yè)務(wù)發(fā)布平臺(tái)對(duì)小程序的發(fā)布環(huán)境進(jìn)行管理,避免發(fā)布沖突;
提供企業(yè)微信觸發(fā)小程序構(gòu)建的能力,測(cè)試和產(chǎn)品可實(shí)時(shí)出發(fā)構(gòu)建并獲取最新二維碼。
這是我們?cè)?CI/CD 上面的一些實(shí)踐經(jīng)驗(yàn),以及在開發(fā)體驗(yàn)上面的一些處理方案。
小程序的啟動(dòng)方式分為冷啟動(dòng)和熱啟動(dòng),而小程序的性能瓶頸大部分也都集中在冷啟動(dòng)這一階段。
冷啟動(dòng)階段分為如上幾個(gè)步驟,其中環(huán)境初始化對(duì)于開發(fā)者來說是個(gè)黑盒,目前還無法介入,而下載代碼包和加載代碼包的耗時(shí),主要與小程序代碼包的體積正相關(guān),數(shù)據(jù)拉取需要開發(fā)者對(duì)請(qǐng)求時(shí)機(jī)進(jìn)行優(yōu)化,頁面渲染則需要優(yōu)化渲染策略。
業(yè)務(wù)代碼的體積優(yōu)化需要通過構(gòu)建來解決,以一個(gè)項(xiàng)目的常規(guī)結(jié)構(gòu)來看,我們一般會(huì)將一些有可能復(fù)用的模塊放置到公共模塊中。如下圖所示,引用關(guān)系如果只進(jìn)行編譯的話,根據(jù)小程序的規(guī)則,公共模塊和組件的大小都會(huì)被計(jì)算到主包中,我們希望通過構(gòu)建來優(yōu)化產(chǎn)物結(jié)構(gòu),避免主包太大的問題。
再者,隨著需求的迭代,可能某一個(gè)組件的引用就丟失了,這種情況在小程序的規(guī)則下,依然會(huì)被計(jì)算在主包里面,可以看下面這張圖。我們希望能通過構(gòu)建將未使用的模塊或者組件進(jìn)行過濾。
另外,如果某一個(gè)分包與主包引用了同一個(gè)模塊,這時(shí)候?qū)⑦@個(gè)模塊計(jì)算到主包中是 OK 的,但如果這個(gè)分包是一個(gè)獨(dú)立分包的情況下,再去引用主包的模塊,是有可能報(bào)錯(cuò)的。上面這種情況需要通過構(gòu)建的方式將模塊復(fù)制一份放到獨(dú)立分包下面才能保證小程序的正確執(zhí)行。
我們面對(duì)的上面三個(gè)問題有一個(gè)核心思路是需要在構(gòu)建的過程中,針對(duì)小程序的規(guī)則進(jìn)行依賴分析。下面是我們對(duì)比目前市面上比較成熟的構(gòu)建工具,從四個(gè)維度進(jìn)行了分析:
根據(jù)對(duì)比的結(jié)果,webpack 對(duì)于需求的支持還是比較成熟的,我們最終決定選擇 webpack 作為小程序的構(gòu)建工具,但是 webpack 也不支持小程序的組件,這一點(diǎn)就需要我們自己進(jìn)行支持了。
以 app.js 作為入口文件,根據(jù)小程序的配置規(guī)則找到對(duì)應(yīng)的 json 文件,逐層遞歸就可以將整個(gè)小程序所使用到的頁面和組件分析出來,并將所有的頁面和組件都作為 webpack 的 entry,就可以獲取到小程序中 js 模塊的引用信息了。
通過 plugin 對(duì)引用信息根據(jù)一定策略進(jìn)行計(jì)算 chunk:
例如,某一個(gè)頁面引用了一個(gè)模塊,先判斷模塊是否在分包內(nèi),如果在分包內(nèi)則按照常規(guī)方案打包;不在分包內(nèi)則判斷引用它的分包是否為獨(dú)立分包。如果是獨(dú)立分包則復(fù)制一份 (新建一個(gè) chunk);是普通分包則收集是否被多個(gè)分包引用。若不是,則將模塊移動(dòng)到分包下 (新建一個(gè) chunk,并將原來的刪除)。
計(jì)算完 chunk 之后就可以通過 webpack 的 load 去處理另外的資源文件,包括 css、image、font,提取靜態(tài)資源文件,替換引用路徑。
處理完成后的效果也相當(dāng)明顯,我們的主包從 1900 多 kb 優(yōu)化到了 900 多 kb,優(yōu)化幅度達(dá)到 50%,總包的一些體積也優(yōu)化了 27%。
優(yōu)化的體積主要來自以下三個(gè)方面:
模塊下沉到了分包
對(duì)未使用到的組件和模塊進(jìn)行了過濾
靜態(tài)資源文件上到 CDN
我們的優(yōu)化在實(shí)際的啟動(dòng)耗時(shí)上也有比較顯著的效果,主包下載耗時(shí)優(yōu)化了 43%,js 的注入耗時(shí)優(yōu)化了 18%。
另一方面,當(dāng)小程序需要通過 npm 的形式使用一個(gè)比較復(fù)雜的 SDK 時(shí),由于小程序的 npm 包需要單獨(dú)構(gòu)建一次,無法做到編譯時(shí)按需打包,這也會(huì)遇到體積較大的問題。
在我們的實(shí)際業(yè)務(wù)場(chǎng)景中就有這樣的問題,騰訊課堂作為在線教育業(yè)務(wù),有個(gè)核心能力是直播互動(dòng),就是用戶在線上上課的過程中聊天、舉手、連麥、抽獎(jiǎng)等形式的交互行為。
為了讓這個(gè)核心能力能夠達(dá)到跨端跨業(yè)務(wù)的復(fù)用效果,我們團(tuán)隊(duì)開發(fā)了一個(gè)直播互動(dòng)的 SDK,對(duì)外拋出簡(jiǎn)單的 API,內(nèi)部設(shè)計(jì)了接口層、適配層、通道層、策略層,結(jié)構(gòu)非常清晰,使用起來也很方便,初始化后開發(fā)只需要監(jiān)聽或請(qǐng)求對(duì)應(yīng)的命令字即可,無需關(guān)心內(nèi)部的轉(zhuǎn)化,并且能夠利用 ts 的類型推斷能力直接拿到通道返回的數(shù)據(jù)類型。
但是當(dāng)我們?cè)谛〕绦蚨诉M(jìn)行接入時(shí),遇到了幾個(gè)問題:
為了支持跨端跨業(yè)務(wù),SDK 內(nèi)置了所有功能的邏輯,在小程序端使用會(huì)造成大量的包體積浪費(fèi)
針對(duì) web 設(shè)計(jì),不兼容小程序;單獨(dú)維護(hù)一個(gè)小程序的版本成本比較大
不同項(xiàng)目和業(yè)務(wù)對(duì) SDK 的迭代會(huì)導(dǎo)致版本管理混亂
要解決上面這些問題,我們必須對(duì) SDK 進(jìn)行升級(jí)改造。
我們改造的方案是進(jìn)行插件化處理,將接入層(業(yè)務(wù)層)和適配層作為 SDK 的內(nèi)核抽離出來,并添加了 pluginAdaptor 對(duì)插件進(jìn)行適配管理,將策略層和通道連接層的邏輯進(jìn)行抽象處理,制訂好對(duì)應(yīng)的規(guī)范,根據(jù)抽象類和業(yè)務(wù)需求實(shí)現(xiàn)對(duì)應(yīng)的策略插件和通道插件。
在業(yè)務(wù)中的改造非常簡(jiǎn)單,只需要初始化之前注冊(cè)當(dāng)前場(chǎng)景和功能所需要的插件,后續(xù)在使用上與之前完全一致,業(yè)務(wù)的改造成本非常低。
兼容性上通過 rollup 打包,在構(gòu)建時(shí)注入不同的環(huán)境變量,輸出對(duì)應(yīng)端所需要用到的 bundle。
插件化改造之后,好處就顯而易見了:
按需引入,運(yùn)行時(shí)它的體積是最小的,改造前后 SDK 運(yùn)行時(shí)體積從 384KB 減少到 42KB,優(yōu)化了近 90%
多包結(jié)構(gòu)迭代比較清晰,內(nèi)核和抽象通道也很穩(wěn)定,各個(gè)插件可以進(jìn)行單獨(dú)的版本迭代
跨端復(fù)用能力得到了擴(kuò)展,統(tǒng)一維護(hù)
請(qǐng)求的優(yōu)化也是小程序性能優(yōu)化中很重要的一環(huán),在冷啟動(dòng)和頁面跳轉(zhuǎn)的過程中,我們分別對(duì)請(qǐng)求時(shí)機(jī)以及弱網(wǎng)阻塞兩種情況進(jìn)行了優(yōu)化。
請(qǐng)求時(shí)機(jī)上,可以利用小程序的全局 app 實(shí)例將數(shù)據(jù)請(qǐng)求的時(shí)機(jī)提前到頁面加載之前,進(jìn)一步利用小程序的數(shù)據(jù)預(yù)加載能力,將首屏數(shù)據(jù)的請(qǐng)求時(shí)機(jī)提前到啟動(dòng)小程序時(shí):
頁面加載前發(fā)起請(qǐng)求的流程如下,在 onLaunch 或者頁面跳轉(zhuǎn)時(shí)就直接發(fā)起下一個(gè)頁面的請(qǐng)求,并將請(qǐng)求的 Promise 掛載在 app 實(shí)例上,當(dāng)頁面加載完成出發(fā) onLoad 的時(shí)候則直接通過 app 上的 Promise 返回進(jìn)行渲染,根據(jù)我們的統(tǒng)計(jì)平均可以優(yōu)化 100ms 的耗時(shí),而且相對(duì)靜態(tài)的數(shù)據(jù)可以通過本地緩存的方式,在二次加載此頁面時(shí)通過緩存數(shù)據(jù)渲染,達(dá)到秒開的效果。
而數(shù)據(jù)預(yù)拉取則類似于 web 的服務(wù)端渲染,在啟動(dòng)小程序時(shí)通過云函數(shù)根據(jù)啟動(dòng)參數(shù)調(diào)用業(yè)務(wù)后臺(tái)的服務(wù)獲取數(shù)據(jù)并返回給小程序,小程序啟動(dòng)后就可以直接使用預(yù)拉取的數(shù)據(jù)進(jìn)行渲染,預(yù)拉取成功可以平均優(yōu)化 90% 的首屏數(shù)據(jù)請(qǐng)求耗時(shí)。
除了上面常規(guī)情況的請(qǐng)求優(yōu)化,我們還注意到小程序有一個(gè)網(wǎng)絡(luò)使用限制,最大的并發(fā)限制是 10 個(gè),這就會(huì)造成隱患。因?yàn)樵谛〕绦蚣虞d和用戶交互的過程中會(huì)產(chǎn)生很多的上報(bào)請(qǐng)求,例如 PV 上報(bào)、錯(cuò)誤日志等,在弱網(wǎng)的情況下,很容易出現(xiàn)上報(bào)請(qǐng)求響應(yīng)慢而阻塞了業(yè)務(wù)請(qǐng)求的發(fā)送導(dǎo)致超時(shí),而我們也確實(shí)收到了類似情況的反饋。
為了優(yōu)化弱網(wǎng)情況下存在的隱患,我們對(duì)請(qǐng)求隊(duì)列進(jìn)行了優(yōu)化,通過設(shè)置請(qǐng)求池與等待隊(duì)列,并劫持 wx.request,在發(fā)送請(qǐng)求時(shí)對(duì)請(qǐng)求的 url 進(jìn)行優(yōu)先級(jí)排序,將業(yè)務(wù)請(qǐng)求設(shè)置為高優(yōu)先級(jí)的請(qǐng)求,上報(bào)請(qǐng)求的優(yōu)先級(jí)降低。當(dāng)請(qǐng)求通道相對(duì)緊張時(shí)會(huì)將高優(yōu)先級(jí)的請(qǐng)求優(yōu)先發(fā)送,低優(yōu)先級(jí)的請(qǐng)求在請(qǐng)求通道空閑時(shí)再進(jìn)行補(bǔ)發(fā)。
在實(shí)際運(yùn)行過程中的邏輯如下圖:
視圖渲染和更新可以優(yōu)化的方向在于,將非首屏和非核心模塊數(shù)據(jù)延后更新,因?yàn)?span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"> setData 更新視圖數(shù)據(jù)太大會(huì)增加通信和解析的時(shí)間。以我們最復(fù)雜的課程詳情頁為例,因?yàn)槟K比較多,導(dǎo)致頁面比較長,一般會(huì)有 5~7 屏的內(nèi)容。如果需要等到頁面的所有數(shù)據(jù)全部出完再開始更新試圖,那么白屏?xí)r間和視圖更新時(shí)間都會(huì)比較長,比較合理的渲染策略應(yīng)該是首屏優(yōu)先,分步渲染。
但由于小程序的雙線程模式,通過 setData 的方式更新視圖是同步更新邏輯層數(shù)據(jù),異步更新視圖層數(shù)據(jù),所以并不能簡(jiǎn)單地在處理完一部分?jǐn)?shù)據(jù)后調(diào)用 setData 再繼續(xù)處理其余的數(shù)據(jù),甚至通過 Promise 也做不到分步渲染,而使用 setData 的回調(diào)或者 setTimeout 的方式又會(huì)出現(xiàn)邏輯嵌套的問題,降低代碼的可讀性和可維護(hù)性。
針對(duì)這個(gè)問題,我們的解決方案是基于 setTimeout 根據(jù) Promise 的表示封裝了一個(gè) PromiseMacro 的類,這樣我們就可以向使用 Promise 一樣通過 then 方法將小程序的渲染拆分成多個(gè)步驟達(dá)到漸進(jìn)式渲染的效果。
通過分步渲染的方式,可以將我們首屏渲染的起始時(shí)間從 230ms 提前到 90ms,達(dá)到減少用戶等待焦慮,提升用戶體驗(yàn)的效果。
一個(gè)產(chǎn)品的質(zhì)量不僅僅是靠好的產(chǎn)品設(shè)計(jì)和代碼質(zhì)量,還有很大一部分需要通過收集操作和性能日志,為技術(shù)優(yōu)化提供方案,而對(duì)現(xiàn)網(wǎng)報(bào)錯(cuò)的比例及數(shù)量進(jìn)行監(jiān)控,能讓我們及時(shí)響應(yīng)并修復(fù)。之前我們的小程序上報(bào)也依賴了好幾個(gè)上報(bào)系統(tǒng)來完成:
通過 BadJS 來收集前端報(bào)錯(cuò)和操作日志
通過 Wang 進(jìn)行測(cè)速上報(bào)
通過 Monitor 進(jìn)行打點(diǎn)監(jiān)控告警
通過 Tdw 進(jìn)行產(chǎn)品需求上報(bào)
通過不同的系統(tǒng)進(jìn)行上報(bào)會(huì)存在一些問題:
依賴的 SDK 比較多,每一個(gè) SDK 的 API 都不一致,學(xué)習(xí)和維護(hù)的成本會(huì)比較高,這一點(diǎn)對(duì)于新人來說尤其明顯;
每個(gè) SDK 上報(bào)的數(shù)據(jù)結(jié)構(gòu)也不一樣,想要查找對(duì)應(yīng)的數(shù)據(jù),就必須去對(duì)應(yīng)的統(tǒng)計(jì)平臺(tái)進(jìn)行搜索。
解決這些問題的首要任務(wù)就是需要對(duì)這些上報(bào)的 SDK 進(jìn)行整合,根據(jù)產(chǎn)品和業(yè)務(wù)需求將日志、測(cè)速、監(jiān)控、上報(bào)收歸到一個(gè) SDK 里面,并統(tǒng)一上報(bào)的數(shù)據(jù)結(jié)構(gòu),部分功能會(huì)在 SDK 中進(jìn)行備份轉(zhuǎn)發(fā),保證原上報(bào)系統(tǒng)的功能也能得到利用。
再結(jié)合小程序提供的幾個(gè) API 就可以在日志收集的同時(shí)對(duì)用戶進(jìn)行多維度的統(tǒng)計(jì):
在統(tǒng)一上報(bào)數(shù)據(jù)結(jié)構(gòu)的前提下,就可以自定義制定多維度的統(tǒng)一看板,降低質(zhì)量監(jiān)控的成本。
以上是這次分享的主要內(nèi)容,我們簡(jiǎn)單做一個(gè)總結(jié)。
隨著我們課堂小程序的技術(shù)演進(jìn),圍繞小程序逐漸形成了 DevOps 的開發(fā)模式:開發(fā)階段,我們打造了兼容小程序端的公共模塊,提升了小程序 30%~40% 的研發(fā)效率,同時(shí)在 CI/CD 建設(shè)方面,減少了人工操作的風(fēng)險(xiǎn),充分利用工程化自動(dòng)化來解放開發(fā)的生產(chǎn)力,而且小程序 CI 建設(shè)的很通用化,公司內(nèi)部有 140+ 的項(xiàng)目接入。
在性能優(yōu)化方面,我們通過在體積、請(qǐng)求、渲染方面的優(yōu)化,將冷啟動(dòng)下的首屏性能優(yōu)化了 1.5s,達(dá)到了 42.7% 的優(yōu)化比例。
在監(jiān)控告警方面,通過小程序的 API 再結(jié)合收攏的上報(bào) SDK,可以讓統(tǒng)計(jì)力度更細(xì),而且可視化可定制。
接下來小程序還有一些非常好用的特性可以加以利用,我們比較關(guān)注的是分包異步化和自動(dòng)化測(cè)試。
異步化打包策略
分包異步化可以極大地縮小首屏包的大小,目前分包異步化的特性已經(jīng)適配了 2.11.2 的基礎(chǔ)庫版本,兼容性的問題也已經(jīng)得到了解決;接入分包異步化的能力,可以嘗試在小程序構(gòu)建打包時(shí)將一個(gè)頁面拆分成首屏包 + 異步邏輯包 + 異步組件包的形式,結(jié)合分包預(yù)加載功能,將首屏的代碼包下載耗時(shí)和加載耗時(shí)降至最低。
基于錄制回放的自動(dòng)化測(cè)試
小程序團(tuán)隊(duì)很早就支持了小程序的自動(dòng)化測(cè)試,但是在 UI 自動(dòng)化測(cè)試方面,測(cè)試用例的維護(hù)成本是最大的痛點(diǎn),我們嘗試在本地錄制操作流程,按照一定的約定轉(zhuǎn)換成測(cè)試用例,可以極大地降低測(cè)試用例的維護(hù)成本,提高代碼質(zhì)量。