离线包具体实现

一:数据存储

这一块的知识只要是针对小程序列表和web资源包资源路径进行存储和查询。具体的详细设计如下:

小程序列表数据存储

小程序接口列表接口返回数据设计

数据存储通过数据库进行存储、数据库表设计如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"list": [{
"minProgramId": "", // 小程序Id
"name": "" // 小程序名称
"content": "", // 更新描述
"version": "", // 小程序版本号
"build": 1, // 小程序build号
"h5Url": "", // h5线上地址
"detail": {
"fullUrl": "", // 全量资源URL地址
"partUrl": "", // 增量资源URL地址
"fullMd5": "", // 全量资源md5值
"partMd5": "" // 增量资源md5值
},
}]

表名:webappinfos

表字段

  • id:小程序ID
  • name:小程序名称
  • version:小程序版本
  • fullMd5:全量包资源md5
  • fullUrl:全量包资源地址URL
  • diffMd5:差量包资源md5
  • diffUrl:差量包资源地址URL
  • localPath:小程序资源包本地存储路径
id name version fullMd5 fullUrl diffMd5 diffUrl localPath
10001 小程序1 1.0.0 Ho…A== ..zip R9…B== ..zip webapps/10001
10002 小程序2 1.0.0 Ho…A== ..zip R9…B== ..zip webapps/10002

web资源包资源路径存储

web资源包文件目录如下:

离线具体方案实现

数据存储通过数据库进行存储、数据库表设计如下:

表名:pathIndexInfos

表字段

  • url:web资源url路径
  • localPath:离线包资源本地路径
  • md5:离线包资源文件MD5值
  • miniProgramId:小程序ID
url localPath md5 miniProgramId
index.html webapps/1001/res/package/index.html 0u…== 1001
images/…/.png webapps/1001/res/package/subPackagesA/static/images/pay/pay_close.png Ho…== 305
style/common.css webapps/1001/res/package/static/style/common.css NE…== 1001
static/js/index.js webapps/1001/res/package/static/js/index.js Ub…== 1001

二:离线包资源更新

离线包资源更新分为全量更新和差量更新

全量更新

全量更新相对来说比较简单、通过小程序列表接口拉取小程序最新版本信息和本地当前运行版本进行对比,如果有新版本需要更新直接通过fullUrl下载最新版本的离线包资源。下载完成后解压替换本地的离线包资源即可。

差量更新

差量更新相对来说情况比较复杂一点,设计到的情况也比较多。具体更新逻辑如下:

假设:

  • A:本地资源旧包
  • B:差量资源包
  • C:最新全量资源包 c_md5(最新全量资源包的MD5值)

B和A通过bsdiff库进行差量合并合并生成新的资源包D

获取D的md5值为d_md5

d_md5和c_md5值进行比较如果d_md5==c_md5将新生成的资源包D替换本地离线包资源

如果d_md5!=c_md5说明差量资源包和本地就资源包合并之后的资源不是服务器上上传的最新资源包此时需要进行全量更新下载全量资源到本地并替换本来离线包

出现无法进行差量更新情况场景分析:

假设此时要更新的小程序版本为: 1.1.2版本

如果用户当前版本为上个版本例如:1.1.1此时进行差量合并更新是OK的。

如果当前用户是上个版本之前的版本(和要更新的版本中间间隔至少一个版本)此时进行差量更新会失败,从而进行全量更新。

三:web资源拦截

web资源拦截主要针对ios的实现进行讲解利用NSURLProtocol拦截WKWebView的请求

具体的实现如下:

注册把http和https请求交给NSURLProtocol处理

监听web相关请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
[NSURLProtocol registerClass:[KKWebViewProtocol class]]; // 注册监听
@interface KKWebViewProtocol : NSURLProtocol
@end
// 判断是否拦截请求

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
return YES;
}
// 开始加载请求资源

+ (void)startLoading {
// 再次获取cacheData
NSURL *url = self.request.URL;
NSData* cacheData = nil;
if ([webViewProtocolDelegate respondsToSelector:@selector(dataForWebViewProtocolWithURL:)]) {
// 拦截请求url 通过url查询是否存在已经缓存在本地的资源 如果存在则直接从本地读取返回NSData对象。如果不存在则返回null
cacheData = [webViewProtocolDelegate dataForWebViewProtocolWithURL:[url absoluteString]];
if (cacheData == nil) {
// 发送网络请求
self.downloadTask = [self.session dataTaskWithRequest:self.request];
[self.downloadTask resume];
}
else{
// 加载本地资源
NSDictionary *dict = [NSDictionary dictionary];
NSHTTPURLResponse* response = [[NSHTTPURLResponse alloc]
initWithURL:url
statusCode:200
HTTPVersion:@"HTTP/2.0"
headerFields:dict];
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
[self.client URLProtocol:self didLoadData:cacheData];
[self.client URLProtocolDidFinishLoading:self];
}
} else {
self.downloadTask = [self.session dataTaskWithRequest:self.request];
[self.downloadTask resume];
}
}

// 停止资源加载
-(void)stopLoading{
[self.downloadTask cancel];
self.downloadTask = nil;
}

+ (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask*)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
// 允许处理服务器的响应,才会继续接收服务器返回的数据
completionHandler(NSURLSessionResponseAllow);
self.cacheData = [NSMutableData data];
}

+ (void)URLSession:(NSURLSession *)session dataTask:(nonnull NSURLSessionDataTask*)dataTask didReceiveData:(nonnull NSData *)data{
[self.client URLProtocol:self didLoadData:data];
[self.cacheData appendData:data];
}

+ (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask*)task didCompleteWithError:(NSError *)error {
// 下载完成之后的处理
if (error) {
[self.client URLProtocol:self didFailWithError:error];
} else {
//将数据存入到本地文件中
[self.cacheData writeToFile:[self filePathWithUrlString:self.request.URL.absoluteString] atomically:YES];
[self.client URLProtocolDidFinishLoading:self];
}
}

WKURLSchemeHandler 拦截

iOS11之后,苹果公司,自己提供一个看似网友们都十分欢喜的新的API:

1
+ (void)setURLSchemeHandler:(nullable id <WKURLSchemeHandler>)urlSchemeHandler forURLScheme:(NSString *)urlScheme API_AVAILABLE(macos(10.13), ios(11.0));

四:如何检测离线包功能生效

完成以上离线包功能的开发、如何去验证APP是加载的离线资源而不是线上资源呢?

  • 真机运行,体验正常加载和离线包加载两种方式进入小程序的速度
  • 代码断点调试或者log打印是否走离线资源加载
  • 抓包分析验证资源加载情况

离线包和非离线包加载的区别是:

  • 离线包不需要加载web资源
  • 非离线包需要请求加载web资源

六:总结

用到的知识点汇总:

  • 资源文件下载
  • zip文件解压
  • 数据库存储
  • bsdiff差分包库
  • 文件md5值校验
  • web请求拦截