思路来自:https://developers.weixin.qq.com/community/develop/article/doc/000c6e6f7f0ca83c2069e656351813
具体的优化方向和思路可以参考上面的文章,主要就是利用从触发路由到下一个页面onLoad的这段时间来做数据请求,实际上就是提前即将加载页面的数据请求,从而提升页面展现速度。
下面是优化的实践方案(这个方案因为是用在旧项目上,所以设计成嵌入式的,对现有业务逻辑没什么影响):
前提
下面的优化方案有两个前提
- 路由方法封装(项目中要有统一的路由钩子,至少需要优化的页面跳转要有)
- 数据model层,我们称为service(在实践之前我们也是没有这一层的,可以对需要优化的页面添加service)
实践
- 首先我们是这样获取页面需要的数据的
DetailService.getData(this.options).then((result) => {
// 渲染逻辑
})
- 看下这个DetailService,有一个getData方法,实际上就是获取后端数据,然后返回一个Promise。(当然getData里可以做任何事情,包括取缓存等,可以结合缓存优化)
import BaseService from '../base';
class DetailService extends BaseService {
// 预取服务,对应serviceMap中key
preGetName = 'DetailService';
/**
* service数据体
* @param {*} query
*/
getData (query = {}) {
// 这里返回一个Promise
return request({
url: '/v1.0/data',
data: rdata
});
}
}
// 单例service
export default new DetailService();
- 到上面一步,跟我们的数据预取都没有什么关系,无非就是抽离了获取数据的方法而已,预取逻辑在BaseService中,里面做的事情就是在getData之前判断是否有预取缓存,有则直接使用,没有就发起真正的请求
import serviceMap from './main';
/**
* service基类
*/
class BaseService {
constructor () {
setTimeout(() => {
// 装饰模式包装预取
const childGetData = this.getData.bind(this);
this.getData = (query = {}, path = '') => {
const data = this.checkData(path);
if (data) { return data; }
return childGetData(query);
};
}, 0);
}
/**
* 检查预取
* @param {*} path
*/
checkData (path) {
const name = this.preGetName;
path = path || getCurrentPages().slice(-1)[0].route;
return serviceMap && serviceMap[path] && serviceMap[path][name] && serviceMap[path][name].cacheRes;
}
}
export default BaseService;
- 上面一步有一个serviceMap,用于存储每个页面需要预取的请求(这里只应该是影响页面首屏渲染的关键请求),其中为防止内存泄漏和不必要的缓存影响,使缓存结果为单次消费即销毁的模式
const serviceMap = {
// 页面路径对应的主渲染service,可以为多个
'pages/index/index': {
DetailService: {
// 要求登录的service不能用于首屏预取
isRequireLogin: false,
method: DetailService
}
}
};
/**
* 添加一次性消费的缓存属性
*/
(function cacheService () {
for (const key in serviceMap) {
if (serviceMap.hasOwnProperty(key)) {
const pageService = serviceMap[key];
for (const k in pageService) {
if (pageService.hasOwnProperty(k)) {
const service = pageService[k];
Object.defineProperty(service, 'cacheRes', {
get () {
let val = this.v || null;
this.v = null;
return val;
},
set (val) {
this.v = val;
}
});
}
}
}
}
})();
export default serviceMap;
- 数据预取钩子
/**
* 预执行页面主service
* 预取钩子
* @param {*} url 页面path,必选(不传query则可以带参数)
* @param {*} query 页面参数,可选(不传会尝试从path中解析)
*/
const preRunPageService = (url, query) => {
let path = url;
if (!query) {
({path, query = {}} = parseUrl(url));
}
const page = path.replace(/^\//, '');
for (const key in serviceMap[page]) {
if (serviceMap[page].hasOwnProperty(key)) {
const service = serviceMap[page][key];
if (service.isRequireLogin) {
// 登录后才执行预取,否则什么也不做
Login(() => {
service.cacheRes = service.method.getData(query, page);
});
} else {
service.cacheRes = service.method.getData(query, page);
}
}
}
};
export default preRunPageService;
好了,以上就完成了绝大部分的封装工作,之后我们只需要将预取钩子添加到任何想要预取数据的地方就可以了,例如路由钩子中或者App onLaunch中:
function goNextPage (path) {
preRunPageService(path)
wx.navigateTo(...)
}
任何一个页面需要预取数据优化时只需要写一个service继承BaseService(实现model层的优势不止于此),并添加路由映射到serviceMap即可。
结语
以上是对数据预取思路的实践封装,对页面加载性能确实有一定的提升,由于是嵌入式的,项目中的可维护性也能接受。
但是任何优化都要结合实际情况来定,比如我们本来就有抽离model层(service)的想法,并且我们的页面大多都有1~2个主渲染请求,但是因为是旧项目,页面众多,因此最终决定只对高PV页面以及筛选出来合适的页面做上述优化。
任何意见建议,欢迎交流~
有demo么?
挺不错的想法