数据预取js方案

收藏
我的收藏
基础库3.22版本开始支持此能力

使用场景

数据预取能够在小程序冷启动时提前发起请求,并缓存请求内容,在真实请求时使用缓存数据,减少网络请求的时间。在一些发起网络请求较晚的场景,增加数据预取,可以提升页面完全展现的速度
以西瓜视频小程序为例,在接入数据预取后,线上 FMP 从 1173ms 优化到 818ms,整体优化 300+ms,速度提升了 30%,效果显著(为了更好的对比效果,下图为 0.5 倍速的启动对比)。
执行时机
方案影响
预取只是一个提取执行请求的附加优化方案,预取逻辑和小程序启动逻辑是并行启动的,在正常接入数据预取并且能够命中缓存的情况下,不会对服务器造成额外负担。因此需要注意预取的请求和小程序实际的请求一致否则会引入额外请求。

方案对比

配置方案
本方案(js方案)
原理
通过json配置,客户端解析配置处理
独立js环境,开发者通过js控制要执行的逻辑
能力
    json配置限制,只支持几个固定数据源参数并行请求,复杂参数以及串行场景受限。
    支持能力少,只支持接口预取
    所有的逻辑均为开发者控制,灵活性高。
    支持能力多,支持图片请求等更多能力
接入成本
略高,整体链路对开发者黑盒,不便调试
低,便于调试
适用场景
参数简单,n个独立请求的简单场景。
配置方案不满足的复杂场景

代码接入

添加入口文件

注意:prelaunch.js 不支持 require 别的文件或者被 require。

原生小程序

根目录新建 prelaunch.js / prelaunch.ts文件。
说明
当使用prelaunch.ts时需要额外在app.json文件中增加如下内容
"prelaunch": { "entry": "prelaunch.ts" }

Taro

    1.简单场景,prelaunch.js 为单独文件场景,使用taro提供的copy配置将 prelaunch.js copy到小程序产物下。https://docs.taro.zone/docs/config-detail#copy
    2.复杂场景,需要为prelaunch.js进行二次编译等。可将二次编译产物的输出路径设置为小程序产物跟目录的prelaunch.js 或者参考简单场景进行二次copy。

uni-app

只支持 cli 模式项目,将 prelaunch.js copy 到小程序产物目录下可参考如下方案。
    1.使用uni-app 提供的官方能力,参考自定义静态文件的实现copy文件 https://zh.uniapp.dcloud.io/collocation/vue-config.html / https://zh.uniapp.dcloud.io/collocation/vite-config.html
    2.自定义npm script。如下为一个参考示例
项目根目录 srcipts copyPrelaunch.js src prelaunch.js copyPrelaunch.js 文件 const fs = require('fs'); // 具体路径自定义 const path = require('path'); fs.copyFileSync(path.join(__dirname, '../src/prelaunch.js'), path.join(__dirname, '../dist/build/mp-toutiao/prelaunch.js')); package.json文件 "scripts": { "genPrelaunch": "node ./scripts/copyPrelaunch.js", "build:mp-toutiao": "... && npm run genPrelaunch", }

在 prelaunch 中监听页面的启动

在 prelaunch.js 中使用 registerOnPrelaunch 方法监听指定页面的启动
注意:registerOnPrelaunch 的回调在小程序启动生命周期只会触发一次,当页面由于跳转/重定向等方式二次打开则不会再次触发
registerOnPrelaunch(path: string, callback: (params: { path: string, query:string}) => { }) registerOnPrelaunch('app', callback); // 任意页面首次启动都会执行 registerOnPrelaunch(path, callback); // 特定页面首次启动都会执行

添加预取逻辑

registerOnPrelaunch('xxx',callback: (params: {path: string, query: string})=>{ // path 启动页路径 // query 启动页参数 tt.request({ url:url, id: 'id_test', // 唯一id,必须与消费时相同,自定义一个字符串即可,用于标识是一个需要复用的请求, // id和url相同的两个请求会复用 xxx,其余request参数 })});

消费触发的预取缓存

tt.request({ id: 'id_test', // 必须与触发时相同, usePrefetchCache: true// 必须为true xxx,其余request参数 })

调试&预览

真机

    4.2.3以下版本需要关闭局域网快速预览,使用真机预览
    目前只支持vconsole 查看调试信息,可打开vconsole查看【Prelaunch】即可。

开发者工具

开发者工具4.2.3 & 基础库3.22.0 及以上版本支持
    Console 搜索Prelaunch
    调试:搜索tt-prelaunch.js文件

验证预取结果

request结果,通过查看request方法的回调返回值。
图片预取目前暂不支持结果查询,完善中

预取环境中支持的API

类型
对外API
说明(未特殊说明与官方文档一致)
网络
tt.request
请求预取
新增 id 参数作为请求唯一标识, url (不带query)+ id相同即视为同一个请求。
预取结果情况,在request方法的结果中查看如下返回值信息
// 是否使用预取缓存 isPrefetch: boolean // 【预取结果描述】 // 其中 -1 为默认值; // 1~8 为未匹配原因; // 9 为没有任何缓存; // 22,23 为已发起预取,但返回结果失败或返回结果为空; // 100 为命中缓存; // 101 为命中请求中缓存,并成功; // 102 为命中请求中缓存,请求失败,为网络错误。 prefetchDetail:number // 数据预取的结果的详细信息 prefetchInfo: string | object
tt.preloadImages
图片预取
用于提前缓存指定urls对应的图片资源
// 参数: parmas.urls:array[] // 示例: preloadImages({urls: ['https://xxx']})
存储
tt.getStorage
tt.getStorageSync
tt.setStorageSync
tt.setStorage
tt.removeStorage
tt.clearStorage
tt.getStorageInfo
tt.getStorageInfoSync
信息
tt.getNetworkType
tt.getConnectedWifi
基础库版本>= 3.19.0 会拉起官方隐私授权弹窗
tt.getSystemInfo
tt.getSystemInfoSync
tt.getExtConfig
tt.getExtConfigSync
tt.getLaunchOptionsSync
tt.getEnvInfoSync
基础库 >= 3.22.0时支持,需要通过caniuse判断
授权相关
基础库版本>= 3.19.0 会拉起官方能力隐私联合授权弹窗
login不会拉起app登录面板
tt.getSetting
其他
tt.canIUse
预加载环境里可用的api

预取逻辑 接入必看

    id和url不带query部分相同时认为是同一个请求。id参数决定了请求是否能匹配,usePrefetchCache参数决定是否要使用缓存,作用与数据预取配置方案的数据预取_小程序_抖音开放平台匹配规则相同
    非预取发起的两个请求url如果不带query部分和 + id参数都相同时,也认为是同一个请求,如果传了usePrefetchCache,第二次请求会命中第一次的缓存。
    只传入id,usePrefetchCache不传入或者未false时,会更新缓存,缓存key未id+url不带query。
🌰 现在有tt.request http://a/?b=1 和 tt.request http://a/?b=2 两个请求(不区分是否是预取环境)
    如果调用tt.request时请求时传入的id参数相同,那么第二个请求不会发起网络请求,会直接使用http://a/?b=1的接口
    如果想让tt.request http://a/?b=2重新发起请求。要么
    不传id/usePrefetchCache不为true
    id参数生成逻辑把b这个query的值带上
    缓存有效期为5min,app重启后销毁。
    只预取LCP前的关键请求,否则性能可能会恶劣
    需要保证prelaunch.js未执行会或者执行失败时,逻辑能正常运行
    尽量减少prelaunch.js的体积,建议小于10k。

FAQ

    1.预取请求 参数 是 精准匹配,还是模糊匹配,具体指的是什么?除了指定id值相等外,url的请求参数(文档中的xxx,其余参数同request)是否也必须完全相同才能匹配到预取的数据?
    a.精准匹配,id + 去掉query的 url 是匹配的key。其他参数不影响匹配。
    2.预取命不中后, 系统会自动切换到正常请求吗?小程序还需要做额外操作吗?
    a.会正常请求,不需要额外操作
    3.跟以前的文档相比, 预取执行的时机变了。 以前是启动前,现在是启动时? 这时候会不会由于时序问题,造成命不中。具体指的是:预取策略是不是应用的较晚,以至于页面加载的时候获取数据还没有加载好?(理想场景是:当用户看到挂载锚点时触发预取)
    a.数据预取配置方案预取时机也是启动时。
    b.预取未完成时也能复用。
    c.预取后发起时,预取会复用逻辑侧重新发起的请求,不会有额外请求。
    4.registerOnPrelaunch的回调只有页面路由以及query,是没有启动场景值的,有些局限性的东西做不了
    5.数据预取的缓存过期时间
    a.5min,如果想要不命中, 需要控制id的生成逻辑或者usePrefetchCache参数不传或者为false
    6.业务接口参数需要 md5 签名
使用三方框架开发的小程序, prelaunch.js 因为不支持原生打包,所以普通开发者如果使用到crypto-js时不知道如何处理。这里暂时提供一段精简后的crypto-js代码(只包含md5 / HmacMD5,core)。
var t=t||function(t,n){var r;if("undefined"!=typeof window&&window.crypto&&(r=window.crypto),"undefined"!=typeof self&&self.crypto&&(r=self.crypto),"undefined"!=typeof globalThis&&globalThis.crypto&&(r=globalThis.crypto),!r&&"undefined"!=typeof window&&window.msCrypto&&(r=window.msCrypto),!r&&"undefined"!=typeof global&&global.crypto&&(r=global.crypto),!r&&"function"==typeof require)try{r=require("crypto")}catch(t){}var e=function(){if(r){if("function"==typeof r.getRandomValues)try{return r.getRandomValues(new Uint32Array(1))[0]}catch(t){}if("function"==typeof r.randomBytes)try{return r.randomBytes(4).readInt32LE()}catch(t){}}throw new Error("Native crypto module could not be used to get secure random number.")},i=Object.create||function(){function t(){}return function(n){var r;return t.prototype=n,r=new t,t.prototype=null,r}}(),o={},s=o.lib={},a=s.Base={extend:function(t){var n=i(this);return t&&n.mixIn(t),n.hasOwnProperty("init")&&this.init!==n.init||(n.init=function(){n.$super.init.apply(this,arguments)}),n.init.prototype=n,n.$super=this,n},create:function(){var t=this.extend();return t.init.apply(t,arguments),t},init:function(){},mixIn:function(t){for(var n in t)t.hasOwnProperty(n)&&(this[n]=t[n]);t.hasOwnProperty("toString")&&(this.toString=t.toString)},clone:function(){return this.init.prototype.extend(this)}},c=s.WordArray=a.extend({init:function(t,n){t=this.words=t||[],this.sigBytes=null!=n?n:4*t.length},toString:function(t){return(t||f).stringify(this)},concat:function(t){var n=this.words,r=t.words,e=this.sigBytes,i=t.sigBytes;if(this.clamp(),e%4)for(var o=0;o<i;o++){var s=r[o>>>2]>>>24-o%4*8&255;n[e+o>>>2]|=s<<24-(e+o)%4*8}else for(var a=0;a<i;a+=4)n[e+a>>>2]=r[a>>>2];return this.sigBytes+=i,this},clamp:function(){var n=this.words,r=this.sigBytes;n[r>>>2]&=4294967295<<32-r%4*8,n.length=t.ceil(r/4)},clone:function(){var t=a.clone.call(this);return t.words=this.words.slice(0),t},random:function(t){for(var n=[],r=0;r<t;r+=4)n.push(e());return new c.init(n,t)}}),u=o.enc={},f=u.Hex={stringify:function(t){for(var n=t.words,r=t.sigBytes,e=[],i=0;i<r;i++){var o=n[i>>>2]>>>24-i%4*8&255;e.push((o>>>4).toString(16)),e.push((15&o).toString(16))}return e.join("")},parse:function(t){for(var n=t.length,r=[],e=0;e<n;e+=2)r[e>>>3]|=parseInt(t.substr(e,2),16)<<24-e%8*4;return new c.init(r,n/2)}},h=u.Latin1={stringify:function(t){for(var n=t.words,r=t.sigBytes,e=[],i=0;i<r;i++){var o=n[i>>>2]>>>24-i%4*8&255;e.push(String.fromCharCode(o))}return e.join("")},parse:function(t){for(var n=t.length,r=[],e=0;e<n;e++)r[e>>>2]|=(255&t.charCodeAt(e))<<24-e%4*8;return new c.init(r,n)}},l=u.Utf8={stringify:function(t){try{return decodeURIComponent(escape(h.stringify(t)))}catch(t){throw new Error("Malformed UTF-8 data")}},parse:function(t){return h.parse(unescape(encodeURIComponent(t)))}},d=s.BufferedBlockAlgorithm=a.extend({reset:function(){this._data=new c.init,this._nDataBytes=0},_append:function(t){"string"==typeof t&&(t=l.parse(t)),this._data.concat(t),this._nDataBytes+=t.sigBytes},_process:function(n){var r,e=this._data,i=e.words,o=e.sigBytes,s=this.blockSize,a=o/(4*s),u=(a=n?t.ceil(a):t.max((0|a)-this._minBufferSize,0))*s,f=t.min(4*u,o);if(u){for(var h=0;h<u;h+=s)this._doProcessBlock(i,h);r=i.splice(0,u),e.sigBytes-=f}return new c.init(r,f)},clone:function(){var t=a.clone.call(this);return t._data=this._data.clone(),t},_minBufferSize:0}),p=(s.Hasher=d.extend({cfg:a.extend(),init:function(t){this.cfg=this.cfg.extend(t),this.reset()},reset:function(){d.reset.call(this),this._doReset()},update:function(t){return this._append(t),this._process(),this},finalize:function(t){return t&&this._append(t),this._doFinalize()},blockSize:16,_createHelper:function(t){return function(n,r){return new t.init(r).finalize(n)}},_createHmacHelper:function(t){return function(n,r){return new p.HMAC.init(t,r).finalize(n)}}}),o.algo={});return o}(Math);!function(n){var r=t,e=r.lib,i=e.WordArray,o=e.Hasher,s=r.algo,a=[];!function(){for(var t=0;t<64;t++)a[t]=4294967296*n.abs(n.sin(t+1))|0}();var c=s.MD5=o.extend({_doReset:function(){this._hash=new i.init([1732584193,4023233417,2562383102,271733878])},_doProcessBlock:function(t,n){for(var r=0;r<16;r++){var e=n+r,i=t[e];t[e]=16711935&(i<<8|i>>>24)|4278255360&(i<<24|i>>>8)}var o=this._hash.words,s=t[n+0],c=t[n+1],d=t[n+2],p=t[n+3],y=t[n+4],g=t[n+5],w=t[n+6],v=t[n+7],_=t[n+8],m=t[n+9],B=t[n+10],b=t[n+11],x=t[n+12],H=t[n+13],S=t[n+14],z=t[n+15],C=o[0],M=o[1],A=o[2],D=o[3];C=u(C,M,A,D,s,7,a[0]),D=u(D,C,M,A,c,12,a[1]),A=u(A,D,C,M,d,17,a[2]),M=u(M,A,D,C,p,22,a[3]),C=u(C,M,A,D,y,7,a[4]),D=u(D,C,M,A,g,12,a[5]),A=u(A,D,C,M,w,17,a[6]),M=u(M,A,D,C,v,22,a[7]),C=u(C,M,A,D,_,7,a[8]),D=u(D,C,M,A,m,12,a[9]),A=u(A,D,C,M,B,17,a[10]),M=u(M,A,D,C,b,22,a[11]),C=u(C,M,A,D,x,7,a[12]),D=u(D,C,M,A,H,12,a[13]),A=u(A,D,C,M,S,17,a[14]),C=f(C,M=u(M,A,D,C,z,22,a[15]),A,D,c,5,a[16]),D=f(D,C,M,A,w,9,a[17]),A=f(A,D,C,M,b,14,a[18]),M=f(M,A,D,C,s,20,a[19]),C=f(C,M,A,D,g,5,a[20]),D=f(D,C,M,A,B,9,a[21]),A=f(A,D,C,M,z,14,a[22]),M=f(M,A,D,C,y,20,a[23]),C=f(C,M,A,D,m,5,a[24]),D=f(D,C,M,A,S,9,a[25]),A=f(A,D,C,M,p,14,a[26]),M=f(M,A,D,C,_,20,a[27]),C=f(C,M,A,D,H,5,a[28]),D=f(D,C,M,A,d,9,a[29]),A=f(A,D,C,M,v,14,a[30]),C=h(C,M=f(M,A,D,C,x,20,a[31]),A,D,g,4,a[32]),D=h(D,C,M,A,_,11,a[33]),A=h(A,D,C,M,b,16,a[34]),M=h(M,A,D,C,S,23,a[35]),C=h(C,M,A,D,c,4,a[36]),D=h(D,C,M,A,y,11,a[37]),A=h(A,D,C,M,v,16,a[38]),M=h(M,A,D,C,B,23,a[39]),C=h(C,M,A,D,H,4,a[40]),D=h(D,C,M,A,s,11,a[41]),A=h(A,D,C,M,p,16,a[42]),M=h(M,A,D,C,w,23,a[43]),C=h(C,M,A,D,m,4,a[44]),D=h(D,C,M,A,x,11,a[45]),A=h(A,D,C,M,z,16,a[46]),C=l(C,M=h(M,A,D,C,d,23,a[47]),A,D,s,6,a[48]),D=l(D,C,M,A,v,10,a[49]),A=l(A,D,C,M,S,15,a[50]),M=l(M,A,D,C,g,21,a[51]),C=l(C,M,A,D,x,6,a[52]),D=l(D,C,M,A,p,10,a[53]),A=l(A,D,C,M,B,15,a[54]),M=l(M,A,D,C,c,21,a[55]),C=l(C,M,A,D,_,6,a[56]),D=l(D,C,M,A,z,10,a[57]),A=l(A,D,C,M,w,15,a[58]),M=l(M,A,D,C,H,21,a[59]),C=l(C,M,A,D,y,6,a[60]),D=l(D,C,M,A,b,10,a[61]),A=l(A,D,C,M,d,15,a[62]),M=l(M,A,D,C,m,21,a[63]),o[0]=o[0]+C|0,o[1]=o[1]+M|0,o[2]=o[2]+A|0,o[3]=o[3]+D|0},_doFinalize:function(){var t=this._data,r=t.words,e=8*this._nDataBytes,i=8*t.sigBytes;r[i>>>5]|=128<<24-i%32;var o=n.floor(e/4294967296),s=e;r[15+(i+64>>>9<<4)]=16711935&(o<<8|o>>>24)|4278255360&(o<<24|o>>>8),r[14+(i+64>>>9<<4)]=16711935&(s<<8|s>>>24)|4278255360&(s<<24|s>>>8),t.sigBytes=4*(r.length+1),this._process();for(var a=this._hash,c=a.words,u=0;u<4;u++){var f=c[u];c[u]=16711935&(f<<8|f>>>24)|4278255360&(f<<24|f>>>8)}return a},clone:function(){var t=o.clone.call(this);return t._hash=this._hash.clone(),t}});function u(t,n,r,e,i,o,s){var a=t+(n&r|~n&e)+i+s;return(a<<o|a>>>32-o)+n}function f(t,n,r,e,i,o,s){var a=t+(n&e|r&~e)+i+s;return(a<<o|a>>>32-o)+n}function h(t,n,r,e,i,o,s){var a=t+(n^r^e)+i+s;return(a<<o|a>>>32-o)+n}function l(t,n,r,e,i,o,s){var a=t+(r^(n|~e))+i+s;return(a<<o|a>>>32-o)+n}r.MD5=o._createHelper(c),r.HmacMD5=o._createHmacHelper(c)}(Math); var CryptoJS = t; /** * * @param {string} data * @returns {string} */ function md5(data) { return CryptoJS.enc.Hex.stringify(CryptoJS.MD5(data)); } /** * * @param {string} data * @param {string} key * @returns {string} */ function HmacMd5(data, key) { return CryptoJS.enc.Hex.stringify(CryptoJS.HmacMD5(data, key)); }

完整示例代码

// prelaunch.js registerOnPrelaunch('app', function() { tt.request({ xxx }) }) // 监听index页面,只有index页面冷启动才会执行 registerOnPrelaunch('pages/index/index', function () { console.log('request---'); tt.request({ url: "xxx", id: 'xxx', success: function success(res) { console.log('tt.request', 'success'); tt.preloadImages({ urls: res.xximgs }); }, fail: function fail(e) { console.log('request error', e); } }); } // pages/index/index.js 逻辑代码 tt.request({ url: "xxx", // 如果prelaunch.js发起了url和id相同的请求,此时调用会直接复用之前发起的请求。 id: 'xxx', usePrefetchCache: true, // 标识是否要消费预期的请求,默认为false success: function success(res) { console.log('tt.request', 'success'); }, fail: function fail(e) { console.log('request error', e); } });