Flutter内嵌H5缓存清理方案

1,280 阅读5分钟

背景

在app中内嵌的h5,部署更新h5项目以后,app中获取不到最新的代码资源,页面无法得到及时更新,目前需手动清空app缓存文件,或等待webview应用缓存失效,这样的体验需优化

调研与问题

不进行缓存,每次都请求最新的h5

能实现上述的方案有:

  1. app设置webview强制不缓存,每次都向服务端拉取最新文件
  2. 每次打开webview,url后缀增加时间戳或随机字符,确保不被应用缓存
  3. 运维配置nginx,强制不缓存html

此种处理,可以避免h5发版更新不及时的问题,但每次都会请求最新代码,加载体验不佳

验证发现问题

app内嵌h5,webview缓存会连同index.html入口文件也一致缓存了,导致每次打开的index.html页面都是旧版本,所以就算css,js文件进行了hash,增加时间戳操作也没更新资源

html文件内容增加meta标签设置不缓存,此方法并不通用,在微信浏览器或app内嵌webview中还是无效,应用缓存了整个html文件

<meta http-equiv="cache-control" content="no-cache" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="expires" content="0" />

应用缓存不变,通过版本判断清除缓存

借阅网上已有的方案,结合app及h5项目的环境,考虑可以从线上本地版本差异判断出发

http请求json文件,通过url拼接随机数,避开缓存问题,以获取线上最新版本

app存储h5版本,比对版本号差异,触发执行清除app内缓存的操作

以此实现h5项目发版后,app内打开自动清除缓存需求

处理方案

思路

h5端处理:

需要进行缓存清理的h5项目,在其打包构建脚本中动态生成打包版本信息的json文件,写入生成文件列表中

app端处理:

在打开需要缓存清理的h5项目前,请求h5项目版本信息json文件,获取版本号与app本地存储的版本号进行比对,判断是否需要进行缓存清理操作

H5端进行处理

创建【build-version.js】,用于h5打包构建时动态生成h5项目版本信息

const fs = require('fs')
const path = require('path')
const pkg = require('../package.json')

// 输出路径 - h5打包后的目录
const output = path.resolve(__dirname, '../dist')

if (!fs.existsSync(output)) {
  // 如不存在此路径,则生成路径
  fs.mkdirSync(output)
}

// 获取版本信息字符
function geVersionJson() {
  const buildTime = new Date().getTime()
  const appVersion = pkg.version
  // edgg: 'v1.7.0 - 1672124524686'
  const versionJson = {
    version: `v${appVersion} - ${buildTime}`
  }
  return versionJson
}

try {
  const versionInfo = geVersionJson()
  // 生成version.json文件并写入版本信息
  fs.writeFileSync(path.resolve(output, 'version.json'), JSON.stringify(versionInfo, null, '\t'))
} catch (e) {
  console.log(e)
}

在【package.json】的构建脚本中添加,执行【build-version.js】,以生成【version.json】文件

构建脚本增加以下代码node ./build/build-version.js

{
  "name": "open_account_h5",
  "version": "1.7.2",
  "description": "OpenAccountH5, 盈宝H5开户",
  "scripts": {
    ...
    "build:dev": "yarn && vue-cli-service build --mode development && node ./build/build-version.js",
    "build:test": "yarn && vue-cli-service build --mode staging && node ./build/build-version.js",
    "build:test2": "yarn && vue-cli-service build --mode staging2 && node ./build/build-version.js",
    "build:prod": "yarn && vue-cli-service build --mode production && node ./build/build-version.js",
    ...
  },
}

如进行开发环境打包构建,执行yarn build:dev,运行结束,生成的dist打包目录中则会新增【version.json】文件

此外h5端不需再进行其它额外的处理,此流程只作用于打包项目时,不影响h5现有功能

APP端进行处理

创建h5项目缓存清理类文件【h5_cache_clean】,在其内部实现以下几个功能

  1. getLocalH5VersionByUrl:获取app本地存储的h5项目版本号
  2. setLocalH5VersionByUrl:设置app本地存储的h5项目版本号
  3. reqH5Version: http请求线上最新的h5版本号
  4. doCacheClean:进行webview缓存目录清理操作
  5. checkH5Version:比对app本地及线上h5版本号,判断是否需要进行缓存清理操作,并保存线上最新版本号

其中,主要依靠reqH5Version,获取线上版本信息,reqH5Version内处理方式为,通过http get请求h5项目的version.json文件,并在请求url附加时间戳,以防止缓存干扰

大体代码:


import 'dart:convert';
import 'dart:io';

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:win_bull_app/header.dart';
import 'package:win_bull_app/util/sputil.dart';

// 请求version.json的超时时间
int _timeOut = 1000;
// 本地存储key前缀
String _spKey = 'h5Version-';

class H5CacheClean {
  /// 获取app本地存储的h5项目版本号
  static String? getLocalH5VersionByUrl(String loadUrl) {
    String key = '$_spKey$loadUrl';
    return SP.sp.getString(key);
  }

  /// 设置app本地存储的h5项目版本号
  static Future<bool> setLocalH5VersionByUrl(
      String loadUrl, String version) async {
    String key = '$_spKey$loadUrl';
    return await SP.sp.setString(key, version);
  }

  /// 比对app本地及线上h5版本号,判断是否需要进行缓存清理操作,并保存线上最新版本号
  static Future<void> checkH5Version(String loadUrl) async {
    String? localVersion = getLocalH5VersionByUrl(loadUrl);
    String newVersion = await reqH5Version(loadUrl);

    if (kDebugMode) {
      print('h5链接:$loadUrl');
      print('本地版本:$localVersion');
      print('在线版本:$newVersion');
    }

    // 获取线上版本异常或者空会返回'', 先存储本地
    await setLocalH5VersionByUrl(loadUrl, newVersion);
    /// 如果获取到的本地版本为null(注意不是''), 此时是第一次判断此h5项目
    /// 做一次清理缓存操作
    if (localVersion == null) {
      await doCacheClean();
    } else {
      /// 如果获取到的线上版本为空,则不做缓存清理,可能的情况
      /// 1.请求异常:可能是断网,或者是超时
      /// 2.该h5项目未增加动态打包版本配置
      if (newVersion.isEmpty) {
        return;
      } else {
        // 线上版本不为空
        // 如果跟本地版本不一致,就执行缓存清除操作
        if (newVersion != localVersion) {
          await doCacheClean();
        }
      }
    }
  }

  /// http请求线上最新的h5版本号
  static Future<String> reqH5Version(String loadUrl) async {
    String h5Version = '';
    try {
      String curTime = DateTime.now().millisecondsSinceEpoch.toString();
      String reqUrl = '$loadUrl/version.json?_t=$curTime';
      final baseDio = Dio(BaseOptions(
        receiveTimeout: _timeOut,
        sendTimeout: _timeOut,
        connectTimeout: _timeOut,
      ));

      dynamic res = await baseDio.get<dynamic>(reqUrl);
      var resMap = json.decode(res.toString());
      if (resMap['version'] != null) {
        h5Version = resMap['version'] as String;
      } else {
        h5Version = '';
      }
    } catch (e) {
      h5Version = '';
    }
    return h5Version;
  }

  /// 进行缓存清理操作
  static Future<void> doCacheClean() async {
    try {
      Directory dir = await getTemporaryDirectory();
      List<FileSystemEntity> list = dir.listSync();
      for (var i = 0; i < list.length; i++) {
        var fileStream = list[i];
        String path = fileStream.path.toLowerCase();
        /// 安卓设备webview缓存路径包含【webview】不包含【webkit】
        /// 苹果设备webview缓存路径包含【webkit】不包含【webview】
        if (path.contains('webview') || path.contains('webkit')) {
          if (kDebugMode) {
            print('清理webview缓存:');
            print(fileStream.path);
            print('------------------');
          }
          fileStream.deleteSync(recursive: true);
        }
      }
    } catch (err) {
      if (kDebugMode){
        print(err);
      }
    }
  }
}

调用及验证

app中,需要进行h5缓存清理的项目(不是所有h5项目,对于那些更新不频繁的可不进行此操作?),在其打开webview的方法前,调用

String loadUrl = H5UrlTool.getH5UrlByName('openAccount');
await H5CacheClean.checkH5Version(loadUrl);
// ...打开webview

如版本不一致,则app会调用清除缓存的方法,在打开webview前清空缓存,再打开webview,故实现h5发版清除缓存逻辑;

经验证,二次通过Jenkins构建测试环境,可以实现方案目标需求

image.png

参考: