完美解决 - 前端发版后浏览器缓存问题(发版后及时拉取最新版本代码)

Posted 卡尔特斯

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了完美解决 - 前端发版后浏览器缓存问题(发版后及时拉取最新版本代码)相关的知识,希望对你有一定的参考价值。

一、简介

  • 开发完发布新版本后,在有些电脑上总需要强刷才能获取到最新版本的内容,太恶心了。

  • 浏览器缓存(Browser Caching)是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。

  • 附:前端缓存详解,看了这篇更容易理解缓存配置的概念,浏览器缓存主要有两类:协商缓存彻底(强)缓存

    例如:programcache-controlexpires 都是前端缓存的关键字段,优先级是 pragma > cache-control > expirespragma 是旧产物,已经逐步抛弃,有些网站为了向下兼容还保留了这个字段。

二、解决方案

1、在 .html 页面加 meta 标签

<meta http-equiv="pragram" content="no-cache">
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="expires" content="0">

2、后端 nginx 配置,让 index.html 不缓存

vue 默认配置,打包后 cssjs 的名字后面都加了哈希值,不会有缓存问题,但是 index.html 在服务器端可能是有缓存的,需要在服务器配置不让缓存 index.html

location = /index.html 
  add_header Cache-Control "no-cache, no-store";

3、使用 Vue 脚手架的情况下:vue.config.js

// 动态版本号
const version = new Date().getTime()
// 配置
module.exports = 
  devServer: ,
  filenameHashing: false, // 打包的时候不使用 hash 值,因为后面自行添加时间戳或者版本号了
  css: 
    // 是否使用 css 分离插件 ExtractTextPlugin
    extract: 
      // 输出编译后的文件名称:【文件名称.时间戳】、【文件名称.版本号.时间戳】...
      filename: `css/[name].$version.css`,   
      chunkFilename: `css/[name].$version.css`
      // filename: `css/[name].$process.env.VUE_APP_VERSION.$version.css`,   
      // chunkFilename: `css/[name].$process.env.VUE_APP_VERSION.$version.css`
    
  ,
  configureWebpack: 
    output:  // 输出编译后的文件名称:【文件名称.时间戳】、【文件名称.版本号.时间戳】...
      filename: `js/[name].$version.js`,
      chunkFilename: `js/[name].$version.js`
      // filename: `js/[name].$process.env.VUE_APP_VERSION.$version.js`,
      // chunkFilename: `js/[name].$process.env.VUE_APP_VERSION.$version.js`
    
  

4、使用 webpack 的情况下:webpack.config.js

const date = new Date()
const version = moment(date).format('YYYYMMDDHHmmssSSS') // 打包时候的版本号
const timestamp = date.getTime() // 时间戳
// 注意需下面这段放到配置导出中
output: 
    path: config.build.assetsRoot,
    filename: utils.assetsPath(`js/[name].[chunkhash:8].$ version .js?_t=$ timestamp `),
    chunkFilename: utils.assetsPath(`js/[name].[chunkhash:8].$ version .js?_t=$ timestamp `)

5、有新版本发布,及时拉取最新版本代码

  • 有时候发布了新版本,用户不刷新或强制刷新,一直不能看不到最新版本代码,则封装了套在切换页面时检查服务器是否有新版本,有新版本则直接强制刷新拉取最新版本代码,这样也解决了缓存问题,切换页面就能及时同步到最新版本代码使用也简单,导入之后两步小操作就能支持

  • 支持 vue、react、原生 ... 项目使用。

  • version.js 下载地址,下载后导入项目任意工具文件夹即可。

  • vue 项目导入方式:在 src 文件夹中使用可以 const version = require('@/utils/version') 这样引入使用,在根目录也就是 src 之外的文件夹则只能 const version = require('./src/utils/version') 这样引入使用。

  • 使用方式,只需要 两个位置导入使用 即可:

    1、「如果是原生开发,第一步可以改为手动创建文件,并每次修改版本号即可」在打包配置文件中(例如:vue.config.js)创建版本文件,因为只需要 build 时才需要创建版本文件,版本文件以时间戳为版本号,所以不需要操心。

    // 在 build 时,每次创建/更新版本文件
    const version = require('./src/utils/version')
    version.create()
    

    2、打包有了版本号,发布上去后,那就需要拉回来校验是否有新版本,所以推荐放到路由守卫里面检查,这个网络要求不高,怕影响跳转体验无感,可以放置在 路由跳转后 的回调中操作,这样更新版本了,切换页面,发现有新版本直接强刷到最新版本。

    import router from '@/router'
    const version = require('@/utils/version')
    // 路由跳转后执行
    router.afterEach((to, from) => 
      // 如果不想每个路由都检查是否有新版本,只需要在特定的页面才需要检查版本,可以在这里做白名单判断
      // 兼容版本,如果是新版本则进行刷新并缓存
      version.getPro()
    )
    // 路由跳转前执行
    router.beforeEach((to, from, next) => 
      next()
    )
    

❤️Android 应用崩溃?嗯?莫慌,稳住!❤️

前言

        从刚开始接触Android开发,第一次发版,遇到程序崩溃,那就一个慌张。好几年过去了,现在的听到程序崩溃?嗯,稍等我看看什么问题,然后该锁定该锁定该解决解决。

        发版前减少bug、崩溃等,发版后遇到bug、崩溃也不要慌张,毕竟 bug不 会因为你的慌张而自动修复对吧?要以最快的速度解决(解决问题同样是能力的体现),并说明问题轻重,看看是直接发版还是坐等下次。同时,吸取教训避免同样问题发生。

        今天咱们就聊聊Android程序闪退。一个应用的崩溃率高低,决定了这个应用的质量。

        为了解决崩溃问题,Android 系统会输出各种相应的 log 日志,当然还各式各样的三方库,大程度上降低了工程师锁定崩溃问题的难度。

        如果要给 crash 日志进行分类,可以分成 2 大类:

  • JVM 异常(Exception)堆栈信息,如下:

  • native 代码崩溃日志,如下:

JVM 异常堆栈信息

Java 中异常(Exception)分两种:

  • 检查异常 checked Exception。

  • 非检查异常 unchecked Exception。

检查异常:就是在代码编译时期,Android Studio 就会提示代码有错误,无法通过编译,比如 InterruptedException。如果我们没有在代码中将这些异常 catch,而是直接抛出,最终也有可能导致程序崩溃。

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

非检查异常:包括 error 和运行时异常(RuntimeException),Android Studio 并不会在编译时期提示这些异常信息,而是在程序运行时期因为代码错误而直接导致程序崩溃,比如 OOM 或者空指针异常(NullPointerException)。

2021-09-13 11:50:27.327 19984-19984/com.scc.demo E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.scc.demo, PID: 19984
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.scc.demo/com.scc.demo.actvitiy.HandlerActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.Button.setOnClickListener(android.view.View$OnClickListener)' on a null object reference
        ...
     Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.Button.setOnClickListener(android.view.View$OnClickListener)' on a null object reference
        at com.scc.demo.actvitiy.HandlerActivity.onCreate(HandlerActivity.java:41)
        at android.app.Activity.performCreate(Activity.java:8000)
        ...

Java 异常

        对于上述两种异常我们都可以使用 UncaughtExceptionHandler 来进行捕获操作,它是 Thread 的一个内部接口,定义如下:

    public interface UncaughtExceptionHandler {
        /**
         * 当给定Thread由于给定的Throwable而终止时调用的方法。
         * 此方法抛出的任何异常都将被 Java 虚拟机忽略。
         * @param t Thread
         * @param e Throwable
         */
        void uncaughtException(Thread t, Throwable e);
    }

        从官方对其介绍能够看出,对于传入的 Thread,如果因为"未捕获"异常而导致被终止,uncaughtException 则会被调用。我们可以借助它来间接捕获程序异常,并进行异常信息的记录工作,或者给出更友好的异常提示信息。

自定义异常处理类

自定义异常处理类

        自定义类实现 UncaughtExceptionHandler 接口,并实现 uncaughtException 方法:

public class SccExceptionHandler implements Thread.UncaughtExceptionHandler {
    private Thread.UncaughtExceptionHandler mDefaultHandler;
    private static SccExceptionHandler sccExceptionHandler;
    private Context mContext;

    public static SccExceptionHandler getInstence() {
        if (sccExceptionHandler == null) {
            synchronized (SccExceptionHandler.class) {
                sccExceptionHandler = new SccExceptionHandler();
            }
        }
        return sccExceptionHandler;
    }

    public void init(Context context) {
        mContext = context;
        //系统默认未捕获异常handler
        //the default uncaught exception handler for all threads
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        //将当前Handler设为系统默认
        Thread.setDefaultUncaughtExceptionHandler(this);

    }

    @Override
    public void uncaughtException(@NonNull @NotNull Thread t, @NonNull @NotNull Throwable e) {
        if (!handlerUncaughtException(e) && mDefaultHandler != null) {
            //注释1:系统处理
            mDefaultHandler.uncaughtException(t, e);
        } else {
            //注释2:自己处理
            Intent intent = new Intent(mContext, ImageViewActivity.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TASK);
            mContext.startActivity(intent);
            //终止进程
            android.os.Process.killProcess(android.os.Process.myPid());
            //终止当前运行的 Java 虚拟机。
            //参数用作状态代码; 按照惯例,非零状态代码表示异常终止。
            System.exit(0);
        }
    }

    //处理程序未捕获异常
    private boolean handlerUncaughtException(Throwable e) {
        //1.收集 crash 现场的相关信息,如当前 App 的版本信息,设备的相关信息以及异常信息。
        //2.日志的记录工作(如保存在本地),等开发人员排查问题或等下次启动APP上传至服务器。
        return true;
        //不想处理 return false;
    }
}

        注释1:在自定义异常处理类中需要持有线程默认异常处理类。这样做的目的是在自定义异常处理类无法处理或者处理异常失败时,还可以将异常交给系统做默认处理。

        注释2:如果自定义异常处理类成功处理异常,需要进行页面跳转,或者将程序进程"杀死"。否则程序会一直卡死在崩溃界面,并弹出无响应对话框。

android.os.Process.myPid():返回此进程的标识符,可与 killProcess 和 sendSignal 一起使用。

android.os.Process.killProcess(android.os.Process.myPid()):使用给定的 PID 终止进程。 请注意,尽管此 API 允许我们根据其 PID 请求终止任何进程,但内核仍会对您实际能够终止的 PID 施加标准限制。 通常这意味着只有运行调用者的包/应用程序的进程和由该应用程序创建的任何其他进程; 共享一个通用 UID 的包也将能够杀死彼此的进程。

使用自定义异常处理类

        SccExceptionHandler 定义好之后,就可以将其初始化,并将主线程注册到 SccExceptionHandler 中。如下:

public class SccApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        SccExceptionHandler.getInstence().init(this);
    }
}

native 异常

        当程序中的 native 代码发生崩溃时,系统会在 /data/tombstones/ 目录下保存一份详细的崩溃日志信息。由于对 native 还不是很熟悉就不误导大家,感兴趣的自己玩玩。

对于程序崩溃信号机制的介绍,可以参考腾讯的这篇文章:Android 平台 Native 代码的崩溃捕获机制及实现

线上崩溃日志获取

        上面介绍的 Java 和 Native 崩溃的捕获都是基于自己能够复现 bug 的前提下。但是对于线上的用户,这种操作方式是不太现实的。

        对于大多数公司来说,针对线上版本,没有必要自己实现一个抓取 log 的平台系统。最快速的实现方式就是集成第三方 SDK。目前比较成熟,采用也比较多的就是腾讯的 Bugly。

Bugly

        Bugly 基本能够满足线上版本捕获 crash 的所有需求,包括 Java 层和 Native 层的 crash 都可以获取相应的日志。并且每天 Bugly 都会邮件通知上一天的崩溃日志,方便测试和开发统计 bug 的分布以及崩溃率。

        接入文档接入文档,文章最后会有简易版接入使用

        异常概括

        崩溃分析

        程序崩溃分析这块我没做调整,这个是bugly自动抓取的。

        错误分析

        具体内容

        这里我用来存放去服务端请求接口时的参数和返回的数据。,下面看看具体效果。

        使用起来相当方便,而且错误还提供解决方案,美滋滋。

xCrash

        xCrash 能为安卓 app 提供捕获 java 崩溃,native 崩溃和 ANR 的能力。不需要 root 权限或任何系统权限。

        xCrash 能在 app 进程崩溃或 ANR 时,在你指定的目录中生成一个 tombstone 文件(格式与安卓系统的 tombstone 文件类似)。

        xCrash 已经在 爱奇艺 的不同平台(手机,平板,电视)的很多安卓 app(包括爱奇艺视频)中被使用了很多年。

xCrash传送门

Sentry

        Sentry 是一项可帮助您实时监控和修复崩溃的服务。 服务器使用 Python,但它包含一个完整的 API,用于在任何应用程序中从任何语言发送事件。

Sentry传送门

        XCrash 和 Sentry,这两者比 Bugly 好的地方就是除了自动拦截界面崩溃事件,还可以主动上报错误信息。

        可以看出 XCrash 的使用更加灵活,工程师的掌控性更高。可以通过设置不同的过滤方式,针对性地上报相应的 crash 日志。并且在捕获到 crash 之后,可以加入自定义的操作,比如本地保存日志或者直接进行网络上传等。

        另外:Sentry 还有一个好处就是可以通过设置过滤,来判断是否上报 crash 日志。这对于 SDK 的开发人员是很有用的。比如一些 SDK 的开发商只是想收集自身 SDK 引入的 crash,对于用户的其他操作导致的 crash 进行过滤,这种情况就可以考虑集成 Sentry。

Bugly 简单使用

        感觉教程乱的可以自己去上文找Bugle文档自己集成,很简单的。

库文件导入

自动集成(推荐)

plugins {
    id 'com.android.application'
}
android {
    compileSdkVersion 30//项目的编译版本
    defaultConfig {
        applicationId "com.scc.demo"//包名
        minSdkVersion 23//最低的兼容的Android系统版本
        targetSdkVersion 30//目标版本,表示你在该Android系统版本已经做过充分的测试
        versionCode 1//版本号
        versionName "1.0.0"//版本名称
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a','x86'
            //运行环境,要上传Google Play必须兼容64位,这里仅兼容ARM架构
            //对于ARM架构,32 位库位于armeabi-v7a 中。64 位等效项是arm64-v8a。
            //对于x86体系结构,查找x86(用于 32 位)和 x86_64(用于 64 位)。
        }
    }
}

dependencies {
    //其中latest.release指代最新Bugly SDK版本号,也可以指定明确的版本号,例如2.1.9
    implementation 'com.tencent.bugly:crashreport:latest.release'
    
    //其中latest.release指代最新Bugly NDK版本号,也可以指定明确的版本号,例如3.0
    //集成Bugly NDK时,需要同时集成Bugly SDK。
    implementation 'com.tencent.bugly:nativecrashreport:latest.release'

}

        注意:自动集成时会自动包含Bugly SO库,建议在Module的build.gradle文件中使用NDK的"abiFilter"配置,设置支持的SO库架构。

        如果在添加"abiFilter"之后Android Studio出现以下提示:

        NDK integration is deprecated in the current plugin. Consider trying the new experimental plugin.

        则在项目根目录的gradle.properties文件中添加:

        android.useDeprecatedNdk=true

初始化

public class SccApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        //70594a1ff8 Bugly新建产品的 App ID
        CrashReport.initCrashReport(getApplicationContext(), "70594a1ff8", false);
    }
}

错误分析

设置

    private void setCrashReport(String url, String name, Map<String, String> params, String message) {
        try {
            if (params != null && !MStringUtils.isNullOrEmpty(url) && !MStringUtils.isNullOrEmpty(name) && !MStringUtils.isNullOrEmpty(params.toString()) && !MStringUtils.isNullOrEmpty(message)) {
                CrashReport.putUserData(AppGlobalUtils.getApplication(), "SccParams", params.toString());
                CrashReport.putUserData(AppGlobalUtils.getApplication(), "Data",   "LoginName-Scc001:" + message);
                CrashReport.postCatchedException(new RuntimeException(name + ":" + url + ":" + message));
            }
        } catch (Exception e) {
        }
    }

调用

        HashMap<String,String> hashMap = new HashMap<>();
        hashMap.put("name","scc001");
        hashMap.put("pass","111111");
        String returnData = "哈哈哈哈哈";
        setCrashReport("loin/register","Main",hashMap,returnData);

效果

错误列表

 错误详情

出错堆栈

跟踪数据

崩溃分析

        这个不用咱自己设置,Bugly自动抓取,下面提供跟错误分析类似功能这里就不多描述了。

 ​

        本文内容到这里就算结束了。希望能帮你快速锁定 bug 并解决,让应用更完美,让你的老板更放心,票票来的更多一些。

以上是关于完美解决 - 前端发版后浏览器缓存问题(发版后及时拉取最新版本代码)的主要内容,如果未能解决你的问题,请参考以下文章

解决index.html缓存问题

发版后报错指数级上升!代码出bug了怎么办?怎样才能少出bug?

❤️Android 应用崩溃?嗯?莫慌,稳住!❤️

fiddler 前端调试

Web项目发布的更新

CMM的千行BUG率是怎么计算的?