# 实现H5离线包机制

![](https://img.hacpai.com/bing/20181202.jpg?imageView2/1/w/960/h/540/interlace/1/q/100)

> 当前环境： Xcode10.0 Swift4.2 iOS SDK 12.1

H5 本地包下载： <https://github.com/zColdWater/OfflinePack>

## 前言

> 距离我做完离线包机制已经一周左右了，因为整个离线包含有 H5，App，后台一起协作完成，下面这里用到的技术的总结。

使用的技术点：

1. [**常驻线程**](http://47.99.237.180:8080/articles/2019/11/15/1573804725366.html)
2. [**多线程(Operation)**](http://47.99.237.180:8080/articles/2019/11/15/1573807707104.html)
3. [**JSBridge**](http://47.99.237.180:8080/articles/2019/11/15/1573807336658.html)
4. **文件管理**
5. [**压缩解压缩**](https://github.com/marmelroy/Zip)
6. **文件下载**

整体流程：

1. **H5 发布离线包**
2. **App 下载离线包到本地**
3. **离线包平台\[发版/回滚/禁用]**

因为我们的 Job 是做 `APP` 的，所以我们会着重说 `APP` 相关的流程。

## 流程：

> 下面是我们的离线包，在 App 的生命周期内的运作。

对于 App 来说，总体来说可以分两步

**第一步：** 下载离线包到指定的本地位置，这里我用一张图片说明，我本地下载完的路径是什么样的。

我们去服务器上传的参数和返回的参数是什么呢？ 下面的数据结构展示了 App 是如何与服务器端进行交互和逻辑处理的。

**具体下载流程：** 详细操作

1. 首先 APP 启动的时候会开启常驻线程，每间隔一段时间会去服务器请求一次，来更新新包，下面就是 `入参` 和 `出参`，服务器可以下发不允许使用离线包来停用它。
2. 当无本地包的时候，会启用在线包来进行容错，算是个兜底方案。
3. App 上架的时候的 ipa 中是没有离线包的，都是通过网络下载的，因为防止下载包过大。

上传参数数据格式！

```javascript
{
    "data":{
        "appCode":"SomeAppName", //APP标识 与服务器 商定
        "appVer":"1.2.0", //APP 版本 
        "platform":"ios", //手机平台
        "h5vers":[ //H5版本列表
            {
                "moduleId":"10010", // 业务编号
                "moduleVer":"1.0.4", // 版本
                "isActive": true // 是否启用离线
            },
            {
                "moduleId":"10020",
                "moduleVer":"1.0.2",
                "isActive":1
            },
            {
                "moduleId":"10030",
                "moduleVer":"1.0.8",
                "isActive":3
            }
        ]
    }
}
```

服务器返回数据格式！

```javascript
{
    "resultData":[
        {
            "moduleId":"10010", // H5业务号
            "packageUrl":"https://jsoneditoronline.org/aa.zip", // 完整包地址
            "packageMD5":"adfadfadfadfadfadfadfadf", //完整包哈希值
            "patchUrl":"https://jsoneditoronline.org/aa.zip", // 补丁包地址
            "patchMD5":"adfadfadfadfadfadfadfadf", // 补丁包哈希值
            "mode":1, // 下载模式 是完整包 还是补丁
            "isActive":true // 是否启动 离线包
        },
        {
            "moduleId":"10020",
            "packageUrl":"https://jsoneditoronline.org/aa.zip",
            "packageMD5":"adfadfadfadfadfadfadfadf",
            "patchUrl":"https://jsoneditoronline.org/aa.zip",
            "patchMD5":"adfadfadfadfadfadfadfadf",
            "mode":2,
            "isActive":true
        },
        {
            "moduleId":"10030",
            "packageUrl":"https://jsoneditoronline.org/aa.zip",
            "packageMD5":"adfadfadfadfadfadfadfadf",
            "patchUrl":"https://jsoneditoronline.org/aa.zip",
            "patchMD5":"adfadfadfadfadfadfadfadf",
            "mode":1,
            "isActive":false
        }
    ]
}
```

对 下面图片 说明： 首先会去服务器查询可用的离线包，然后拿到下载链接，将离线 zip 包下载到 tmp 文件夹下的 `FD+UUID` 的文件夹下，下面的目录用业务 `Key` 创建文件名字，里面装着离线 H5 Zip 包。 只不过图中的 已经被移动到 Document 文件目录下里了。

![](https://raw.githubusercontent.com/zColdWater/Resources/master/Images/offlinefile1.png)

![](https://raw.githubusercontent.com/zColdWater/Resources/master/Images/offlinefile2.png)

![](https://raw.githubusercontent.com/zColdWater/Resources/master/Images/offlinefile3.png)

![](https://raw.githubusercontent.com/zColdWater/Resources/master/Images/offlinedownload.png)

**第二步：** 加载 WebView 的时候插入离线包机制

具体来说就是我们加载 `WebView` 的时候判断一下再选择加载，这里我截取一下代码。

```objectivec
- (void)loadUrl: (NSString *)urlString
{
    NSURL *url = [NSURL URLWithString:urlString];

    FDH5OfflineEngine *share = FDH5OfflineEngine.share;
    BOOL isCan = [share isCanAccessOfflineH5WithUrl:url];
    // 是否能加载离线包
    if (isCan) {
        // 将在线的URL转换成离线的URL，这里我其实返回的是一个对象，对象里面包含离线路径和离线路径的根路径
        FDH5OfflineURL *sets = [share onlineUrlConvertToLocalUrlWithUrl:url];
        [self loadFileURL:[sets fileURL] allowingReadAccessToURL:[sets allowingReadAccessToURL]];
    }
    else {
        // 加载在线
        NSURLRequest* request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15];
        [self loadRequest:request];
    }
}
```

它在 App 中的整体流程我总结成了一张图如下：

说明： 当一个业务模块的其中一个方法触发的 scheme 跳转(路由实现)，这个 scheme 是跳转到 `WebViewController` 的，会在加载`WebView` loadURL 的时候做上面的判断再去加载。

![](https://raw.githubusercontent.com/zColdWater/Resources/master/Images/offlineload.png)

## 总结：

我相信这个方案肯定有些许问题，但是毕竟是我和小伙伴一起想出来并且实现的，并且也成功上线，后续迭代一点点迭代。 这里的 iOS 技术点，后面会分专门的文章逐一 Demo 总结。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yongpenglovemimi123.gitbook.io/henry/ios/shi-xian-h5-li-xian-bao-ji-zhi.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
