一、前言
京東到家小程序最初只有微信小程序,隨著業(yè)務(wù)的發(fā)展,同樣的功能需要支持容器越來越多,包括支付寶小程序、京東小程序、到家APP、京東APP等,然而每個端分開實現(xiàn)要面臨研發(fā)成本高、不一致等問題。
為了提高研發(fā)效率,經(jīng)過技術(shù)選型采用了taro3+原生混合開發(fā)模式,本文主要講解我們是如何基于taro框架,進行多端能力的探索和性能優(yōu)化。
二、多端能力的探索
1.到家小程序基于taro3的架構(gòu)流程圖
?框架分層解釋
1.配置層:主要包含編譯配置、路由配置、分包加載、拓展口子。
(資料圖片)
2.視圖層:主要完成App生命周期初始化、頁面初始化、注入宿主事件、解析配置為頁面和組件綁定事件和屬性。
3.組件庫:是一個單獨維護的項目,多端組件庫包括業(yè)務(wù)組件和原子組件,可由視圖層根據(jù)配置動態(tài)加載組件。
//渲染主入口 render() { let { configData, isDefault, isLoading } = this.state; const pageInfo = { ...this.pageInfoValue, ...this._pageInfo } return ( <MyContext.Provider value={pageInfo}> <View className={style.bg} > {//動態(tài)渲染模板組件 configData && configData.map((item, key) => { return this.renderComponent(item, key); }) } </View> {isLoading && <Loading></Loading>} </MyContext.Provider> ); } //渲染組件 注入下發(fā)配置事件和屬性 renderComponent(item, key) { const AsyncComponent = BussinesComponent[item.templateName]; if (AsyncComponent) { return ( <AsyncComponent key={key} dataSource={item.data} {...item.config} pageEvent={pageEvent} ></AsyncComponent> ); } else { return null; } }
4.邏輯層:包括業(yè)務(wù)處理邏輯,請求、異常、狀態(tài)、性能、公共工具類,以及與基礎(chǔ)庫對接的適配能力。
5.基礎(chǔ)庫: 提供基本能力,定位、登錄、請求、埋點等基礎(chǔ)功能,主要是抹平各端基礎(chǔ)功能的差異。
2、基礎(chǔ)庫
1.統(tǒng)一接口,分端實現(xiàn),差異內(nèi)部抹平
關(guān)于基礎(chǔ)庫我們采用分端實現(xiàn)的方式,即統(tǒng)一接口的多端文件。
??基礎(chǔ)庫如何對接在項目里,修改config/index.js,結(jié)合taro提供的MultiPlatformPlugin插件編譯。
const baseLib = "@dj-lib/login" //增加別名,便于后續(xù)基礎(chǔ)庫調(diào)整切換 alias: { "@djmp": path.resolve(__dirname, "..", `./node_modules/${baseLib}/build`), }, //修改webpack配置,h5和mini都要修改 webpackChain(chain, webpack) { chain.resolve.plugin("MultiPlatformPlugin") .tap(args => { args[2]["include"] = [`${baseLib}`] return args }) }
業(yè)務(wù)里使用方式
import { goToLogin } from "@djmp/login/index";goToLogin()
2.高復用
基礎(chǔ)庫不應(yīng)該耦合框架,那么基礎(chǔ)庫應(yīng)該如何設(shè)計,使其既能滿足taro項目又能滿足原生項目使用呢?
npm基礎(chǔ)庫 在taro經(jīng)過編譯后生成為 vendors文件
npm基礎(chǔ)庫 在小程序原生項目npm構(gòu)建后 生成miniprogram_npm
??
一樣的基礎(chǔ)庫經(jīng)過編譯后會存在2種形態(tài),多占了一份空間呢。
我們對小程序包體積大小是比較敏感的,為了節(jié)約空間,那么如何讓taro使用小程序的miniprogram_npm呢?
先簡單說一下思路,更改 webpack 的配置項,通過externals 配置處理公共方法和公共模塊的引入,保留這些引入的語句,并將引入方式設(shè)置成 commonjs 相對路徑的方式,詳細代碼如下所示。
const config = { // ... mini: { // ... webpackChain (chain) { chain.merge({ externals: [ (context, request, callback) => { const externalDirs = ["@djmp/login"] const externalDir = externalDirs.find(dir => request.startsWith(dir)) if (process.env.NODE_ENV === "production" && externalDir) { const res = request.replace(externalDir, `../../../../${externalDir.substr(1)}`) return callback(null, `commonjs ${res}`) } callback() }, ], }) } // ... } // ...}
3、組件庫
想要實現(xiàn)跨端組件,難點有三個
第一:如何在多個技術(shù)棧中找到最恰當?shù)哪テ椒桨福煌姆桨笗е?開發(fā)適配的成本不同,而人效提升才是我們最終想要實現(xiàn)的目的;
第二:如何在一碼多端實現(xiàn)組件之后,確保沒有對各個組件的性能產(chǎn)生影響
第三:如何在各項目中進行跨端組件的使用
基于以上,在我們已經(jīng)確定的以Taro為基礎(chǔ)開發(fā)框架的前提下,我們進行了整體跨端組件方案實現(xiàn)的規(guī)劃設(shè)計:
??在組件層面,劃分為三層:UI基礎(chǔ)組件和業(yè)務(wù)組件 為最底層;容器組件是中間層,最上層是業(yè)務(wù)模板組件;我們首先從UI基礎(chǔ)組件與業(yè)務(wù)組件入手,進行方案的最終確認;
調(diào)研過程中,UI組件和業(yè)務(wù)組件主要從API、樣式、邏輯三個方面去調(diào)研跨端的復用率:
??經(jīng)過以上調(diào)研得出結(jié)論:API層面仍需要使用各自技術(shù)棧進行實踐,通過屬性一致的方式進行API層面的磨平;樣式上,基礎(chǔ)都使用Sass語法,通過babel工具在轉(zhuǎn)化過程中生成各端可識別的樣式形式;邏輯上基本是平移,不需要做改動;所以當我們想做跨端組件時,我們最大工作量在于:API的磨平和樣式的跨端寫法的探索;
例:圖片組件的磨平:
??
基于以上,跨端組件的復用方案經(jīng)過調(diào)研是可行的,但是接下來,我們該如何保證轉(zhuǎn)化后的組件能夠和原生組件的性能媲美呢?我們的跨端組件又該如何在各個項目中使用呢?
在這個過程中,我們主要調(diào)研對比兩種方案:
第一:直接利用Taro提供的跨端編輯功能進行轉(zhuǎn)換,轉(zhuǎn)換編譯成RN . 微信小程序 以及H5;
第二:通過babel進行編譯,直接轉(zhuǎn)換成RN原生代碼,微信小程序原生代碼,以及H5原生代碼
對比方向 | 原碼大小 | 編譯成本 | 生成的組件性能 |
Taro直接編譯 | 大(攜帶了Taro環(huán)境) | 中(Taro直接提供,但需要各端調(diào)試) | 與原生相同 |
通過babel轉(zhuǎn)義 | ?。ㄖ挥挟斍敖M件的源碼代碼) | 中(需要開發(fā)Babel轉(zhuǎn)義組件) | 與原生相同 |
經(jīng)過以上幾組對比,我們最終選用了babel轉(zhuǎn)義的方式。在項目中使用時,發(fā)布到Npm服務(wù)器上,供各個項目進行使用。
方案落地與未來規(guī)劃:
在確認整體的方案方向之后,我們進行了項目的落地,首先搭建了跨端組件庫的運行項目:能夠支持預覽京東小程序、微信小程序以及H5的組件生成的頁面;以下是整個組件從生成到發(fā)布到對應(yīng)項目的全部流程。
??目前已經(jīng)完成了個5種UI組件的實現(xiàn),4種業(yè)務(wù)組件;其中優(yōu)惠券模塊已經(jīng)落地在到家小程序項目中,并已經(jīng)沉淀了跨端組件的設(shè)計規(guī)則和方案。未來一年中,會繼續(xù)跨端組件的實現(xiàn)與落地,從UI、業(yè)務(wù)層到復雜容器以及復雜頁面中。
4、工程化構(gòu)建
1.構(gòu)建微信小程序
因為存在多個taro項目由不同業(yè)務(wù)負責,需要將taro聚合編譯后的產(chǎn)物,和微信原生聚合在一起,才能構(gòu)成完整的小程序項目。
下面是設(shè)計的構(gòu)建流程。
??
為了使其自動化,減少人工操作,在迪迦發(fā)布后臺(到家自研的小程序發(fā)布后臺)創(chuàng)建依賴任務(wù)即可,完成整體構(gòu)建并上傳。
??
其中執(zhí)行【依賴任務(wù)】這個環(huán)節(jié)會進行,taro項目聚合編譯,并將產(chǎn)物合并到原生項目。
??迪迦發(fā)布后臺
2.構(gòu)建京東小程序
yarn deploy:jd 版本號 描述
//集成CI上傳工具 jd-miniprogram-ciconst { upload, preview } = require("jd-miniprogram-ci")const path = require("path")const privateKey = "xxxxx"http://要上傳的目錄-正式const projectPath = path.resolve(__dirname, "../../", `dist/jddist`)//要上傳的目錄-本地調(diào)試const projectPathDev = path.resolve(__dirname, "../../", `dist/jddevdist`)const version = process.argv[2] const desc = process.argv[3]//預覽版preview({ privateKey: privateKey, projectPath: projectPathDev, base64: false,})//體驗版upload({ privateKey: privateKey, projectPath: projectPath, uv: version, desc: desc, base64: false,})
3.構(gòu)建發(fā)布h5
yarn deploy:h5
h5的應(yīng)用通常采用 cdn資源 +html入口 這種模式。先發(fā)布cdn資源進行預熱,在發(fā)布html入口進行上線。
主要進行3個操作
1.編譯出h5dist產(chǎn)物,即html+靜態(tài)資源
2.靜態(tài)資源,利用集成 @jd/upload-oss-tools 工具上傳到 cdn。
3.觸發(fā)【行云部署編排】發(fā)布html文件入口
關(guān)于cdn: 我們集成了cdn上傳工具,輔助快速上線。
//集成 @jd/upload-oss-tools上傳工具const UploadOssPlugin = require("@jd/upload-oss-tools");const accessKey = new Buffer.from("xxx", "base64").toString()const secretKey = new Buffer.from("xxx", "base64").toString()module.exports = function (localFullPath, folder) { return new Promise((resolve) => { console.log("localFullPath", localFullPath) console.log("folder", folder) // 初始化上傳應(yīng)用 let _ploadOssPlugin = new UploadOssPlugin({ localFullPath: localFullPath, // 被上傳的本地絕對路徑,自行配置 access: accessKey, // http://oss.jd.com/user/glist 生成的 access key secret: secretKey, // http://oss.jd.com/user/glist 生成的 secret key site: "storage.jd.local", cover: true, // 是否覆蓋遠程空間文件 默認true printCdnFile: true, // 是否手動刷新cdn文件 默認false bucket: "wxconfig", // 空間名字 僅能由小寫字母、數(shù)字、點號(.)、中劃線(-)組成 folder: folder, // 空間文件夾名稱 非必填(1、默認創(chuàng)建當前文件所在的文件夾,2、屏蔽字段或傳undefined則按照localFullPath的路徑一層層創(chuàng)建文件夾) ignoreRegexp: "", // 排除的文件規(guī)則,直接寫正則不加雙引號,無規(guī)則時空字符串。正則字符串,匹配到的文件和文件夾都會忽略 timeout: "", // 上傳請求超時的毫秒數(shù) 單位毫秒,默認30秒 uploadStart: function (files) { }, // 文件開始上傳回調(diào)函數(shù),返回文件列表參數(shù) uploadProgress: function (progress) { }, // 文件上傳過程回調(diào)函數(shù),返回文件上傳進度 uploadEnd: (res) =>{ console.log("上傳完成") resolve() }, // 文件上傳完畢回調(diào)函數(shù),返回 {上傳文件數(shù)組、上傳文件的總數(shù),成功數(shù)量,失敗數(shù)量,未上傳數(shù)量 }); _ploadOssPlugin.upload(); })}
三、性能優(yōu)化
性能優(yōu)化是一個亙古不變的話題,總結(jié)來說優(yōu)化方向:包下載階段、js注入階段、請求階段、渲染階段。
以下主要介紹在下載階段如何優(yōu)化包體積,請求階段如何提高請求效率。
(一)體積優(yōu)化
相信使用過taro3的同學,都有個同樣的體會,就是編譯出來的產(chǎn)物過大,主包可能超2M!
1.主包是否開啟
優(yōu)化主包的體積大小 :optimizeMainPackage。
像下面這樣簡單配置之后,可以避免主包沒有引入的 module 不被提取到commonChunks中,該功能會在打包時分析 module 和 chunk 的依賴關(guān)系,篩選出主包沒有引用到的 module 把它提取到分包內(nèi)。
module.exports = { // ... mini: { // ... optimizeMainPackage: { enable: true, }, },}
2.使用壓縮插件 terser-webpack-plugin
//使用壓縮插件 webpackChain(chain, webpack) { chain.merge({ plugin: { install: { plugin: require("terser-webpack-plugin"), args: [{ terserOptions: { compress: true, // 默認使用terser壓縮 keep_classnames: true, // 不改變class名稱 keep_fnames: true // 不改變函數(shù)名稱 } }] } } }) }
3.把公共文件提取到分包。
mini.addChunkPages:為某些頁面單獨指定需要引用的公共文件。
例如在使用小程序分包的時候,為了減少主包大小,分包的頁面希望引入自己的公共文件,而不希望直接放在主包內(nèi)。那么我們首先可以通過 webpackChain 配置 來單獨抽離分包的公共文件,然后通過 mini.addChunkPages 為分包頁面配置引入分包的公共文件,其使用方式如下:
mini.addChunkPages 配置為一個函數(shù),接受兩個參數(shù)
?pages 參數(shù)為 Map 類型,用于為頁面添加公共文件
?pagesNames 參數(shù)為當前應(yīng)用的所有頁面標識列表,可以通過打印的方式進行查看頁面的標識
例如,為 pages/index/index 頁面添加 eating 和 morning 兩個抽離的公共文件:
mini: { // ... addChunkPages(pages: Map<string, string[]>, pagesNames: string[]) { pages.set("pages/index/index", ["eating", "morning"]) }, },
4.代碼分析
如果以上方式,還達不到我們想要的效果,那么我們只能靜下心來分析下taro的打包邏輯。
??可以執(zhí)行 npm run dev 模式查看產(chǎn)物里的 xxx.LICENSE.txt文件,里面羅列打包了哪些文件,需要自行分析去除冗余。
以下以vendors.LICENSE.txt 為例
???runtime.js: webpack 運行時入口 ,只有2k,沒有優(yōu)化空間。
?taro.js: node_modules 中 Taro 相關(guān)依賴,112k,可以魔改源碼,否則沒有優(yōu)化空間。
?vendors.js: node_modules 除 Taro 外的公共依賴,查看vendors.js.LICENSE.txt文件分析包括哪些文件
?common.js: 項目中業(yè)務(wù)代碼公共邏輯,查看common.js.LICENSE.txt文件分析包括哪些文件
?app.js app生命周期中依賴的文件。查看app.js.LICENSE.txt文件分析包括哪些文件
?app.wxss 公共樣式文件 ,看業(yè)務(wù)需求優(yōu)化,去除非必要的全局樣式。
?base.wxml 取決于使用組件的方式,可優(yōu)化空間較小。
(二)網(wǎng)絡(luò)請求優(yōu)化:
相信大家的業(yè)務(wù)里有多種類型的請求,業(yè)務(wù)類、埋點類、行為分析、監(jiān)控、其他sdk封裝的請求。然而在不同的宿主環(huán)境有不同的并發(fā)限制,比如,微信小程序請求并發(fā)限制 10個,京東等小程序限制為5個。
如下圖,以微信小程序為例,在請求過多時,業(yè)務(wù)與埋點類的請求爭搶請求資源,造成業(yè)務(wù)請求排隊,導致頁面展示滯后,弱網(wǎng)情況甚至造成卡頓。
??那么基于以上問題,如何平衡業(yè)務(wù)請求和非業(yè)務(wù)請求呢?
這里我們有2個方案:
1.動態(tài)調(diào)度方案 https://www.cnblogs.com/rsapaper/p/15047813.html
思路就行將請求分為高優(yōu)和低優(yōu)請求,當發(fā)生阻塞時,將高優(yōu)請求放入請求隊列,低優(yōu)進入等待隊列。
??請求分發(fā)器 QueueRequest:對新的請求進行分發(fā)。
?加入等待隊列:正在進行的請求數(shù)超過設(shè)置的 threshold,且請求為低優(yōu)先級時;
?加入請求池:請求為高優(yōu)先級,或并發(fā)數(shù)未達到 threshold。
等待隊列 WaitingQueue:維護需要延時發(fā)送的請求等待隊列。在請求池空閑或請求超過最長等待時間時,補發(fā)等待請求。
請求池 RequestPool:發(fā)送請求并維護所有正在進行的請求的狀態(tài)。對外暴露正在進行的請求數(shù)量,并在有請求完成時通知等待隊列嘗試補發(fā)。
2.虛擬請求池方案
該思路是將微信的10個請求資源,分成3個請求池,業(yè)務(wù)請求:埋點類:其他請求的比例為6:2:2。比例可以自行調(diào)整。
這樣各類型請求都在自己的請求池,不存在爭搶其他請求池資源,保障了業(yè)務(wù)不被其他請求阻塞。
??實現(xiàn)方式
??方案對比
優(yōu)缺點 | 動態(tài)調(diào)度(方案一) | 虛擬請求池(方案二) |
拓展性 | 低 | 高 |
成本(開發(fā)、測試、維護) | 高 | 低 |
請求效率 | 低 | 高 |
2個方案都可以完成請求資源的分配,但結(jié)合業(yè)務(wù)實際采用的是虛擬請求方案,經(jīng)測試在弱網(wǎng)情況下,請求效率可以提升15%.?
四、總結(jié)和展望
未來一定是一碼多端的方向,所以我們未來在基礎(chǔ)建設(shè)上會投入更多的精力,包括框架層升級優(yōu)化、基礎(chǔ)庫建設(shè)、組件庫建設(shè)、工程化建設(shè)快速部署多端。
在性能優(yōu)化上我們還可以探索的方向有京東小程序分包預加載、分包異步化、京東容器flutter渲染、騰訊skyLine渲染引擎等。
在團隊溝通協(xié)作上會與Taro團隊、京東小程序容器團隊、nut-ui、拼拼等團隊進行學習溝通, 也希望能與大家合作共建。
五、結(jié)束語
京東小程序開放平臺是京東自研平臺,提供豐富的開放能力和底層的引擎支持,目前有開發(fā)者工具、轉(zhuǎn)化工具、可視化拖拽等多種開發(fā)工具可供內(nèi)部研發(fā)同事使用,提升開發(fā)質(zhì)量同時快速實現(xiàn)業(yè)務(wù)功能的上線。內(nèi)部已有京東支付、京東讀書、京東居家等業(yè)務(wù)使用京東小程序作為技術(shù)框架開展其業(yè)務(wù)。
參考:
https://www.cnblogs.com/rsapaper/p/15047813.html
https://taro-docs.jd.com/docs/next/config-detail#minioptimizemainpackage
https://taro-docs.jd.com/docs/next/dynamic-import
https://zhuanlan.zhihu.com/p/396763942