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