> For the complete documentation index, see [llms.txt](https://yongpenglovemimi123.gitbook.io/henry/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yongpenglovemimi123.gitbook.io/henry/ios/shi-xian-h5-li-xian-bao-ji-zhi.md).

# 实现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 总结。
