Android中的全量更新、增量更新以及热更新

news2024/11/17 4:27:14

在客户端开发过程中,我们可能会遇到这样一种需求:点击某个按钮弹出一个弹窗,提示我们可以更新到apk的某个版本,或者我们可以通过服务端接口进行强制更新。在这种需求中,我们是不需要通过应用商店来更新我们的apk的,而是直接在apk内部进行版本更新。这次我们就来看看实现这种应用内更新的几种方式。当然,这种玩法只能在国内玩,海外的话会被Googleplay据审的。如果是海外的应用要更新apk,只能在GooglePlay上上传新版本的包。

全量更新

什么是全量更新呢?举个例子,假设现在用户手机上的apk是1.0版本,如果想要升级到2.0版本,全量更新的处理方式则是把2.0版本的apk全部下载下来进行覆盖安装。那么,我们该如果设计一个合理的全量更新方案呢?

  • 服务端
    需要提供一个接口,这个接口返回来的body中包含新版本的包的下载地址以及该包的md5值用于下载完成之后进行校验用
  • 客户端
    访问该服务端接口,下载新版本的包(其实就是字节流的读写),然后进行覆盖安装

做完上面这2点其实就可以实现一个较为完整的全量更新功能。

客户端核心代码如下:

package com.mvp.myapplication.update;

import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
import android.text.TextUtils;
import android.util.Log;

import androidx.core.content.FileProvider;

import com.mvp.myapplication.utils.MD5Util;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class UpdateService extends Service {

    public static final String KEY_MD5 = "MD5";
    public static final String URL = "downloadUrl";


    private boolean startDownload;//开始下载
    public static final String TAG = "UpdateService";
    private DownloadApk downloadApkTask;
    private String downloadUrl;
    private String mMd5;
    private UpdateProgressListener updateProgressListener;
    private LocalBinder localBinder = new LocalBinder();

    public class LocalBinder extends Binder {
        public void setUpdateProgressListener(UpdateProgressListener listener) {
            UpdateService.this.setUpdateProgressListener(listener);
        }
    }

    private void setUpdateProgressListener(UpdateProgressListener listener) {
        this.updateProgressListener = listener;
    }

    /**
     * 获取FileProvider的auth
     */
    private static String getFileProviderAuthority(Context context) {
        try {
            for (ProviderInfo provider : context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_PROVIDERS).providers) {
                if (FileProvider.class.getName().equals(provider.name) && provider.authority.endsWith(".update_app.file_provider")) {
                    return provider.authority;
                }
            }
        } catch (PackageManager.NameNotFoundException ignore) {
        }
        return null;
    }

    private static Intent installIntent(Context context, String path) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.addCategory(Intent.CATEGORY_DEFAULT);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            Uri fileUri = FileProvider.getUriForFile(context, getFileProviderAuthority(context), new File(path));
            intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        } else {
            intent.setDataAndType(Uri.fromFile(new File(path)), "application/vnd.android.package-archive");
        }
        return intent;
    }

    public UpdateService() {
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (!startDownload && intent != null) {
            startDownload = true;
            mMd5 = intent.getStringExtra(KEY_MD5);
            downloadUrl = intent.getStringExtra(URL);

            downloadApkTask = new DownloadApk(this, mMd5);
            downloadApkTask.execute(downloadUrl);

        }
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return localBinder;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        return true;
    }

    @Override
    public void onDestroy() {
        if (downloadApkTask != null) {
            downloadApkTask.cancel(true);
        }
        if (updateProgressListener != null) {
            updateProgressListener = null;
        }
        super.onDestroy();
    }

    private static String getSaveFileName(String downloadUrl) {
        if (downloadUrl == null || TextUtils.isEmpty(downloadUrl)) {
            return System.currentTimeMillis() + ".apk";
        }
        return downloadUrl.substring(downloadUrl.lastIndexOf("/"));
    }

    private static File getDownloadDir(UpdateService service) {
        File downloadDir = null;
        if (Environment.getExternalStorageDirectory().equals(Environment.MEDIA_MOUNTED)) {
            downloadDir = new File(service.getExternalCacheDir(), "update");
        } else {
            downloadDir = new File(service.getCacheDir(), "update");
        }
        if (!downloadDir.exists()) {
            downloadDir.mkdirs();
        }
        return downloadDir;
    }

    private void start() {
        if (updateProgressListener != null) {
            updateProgressListener.start();
        }
    }

    private void update(int progress) {
        if (updateProgressListener != null) {
            updateProgressListener.update(progress);
        }
    }

    private void success(String path) {
        if (updateProgressListener != null) {
            updateProgressListener.success(path);
        }
        Intent i = installIntent(this, path);
        startActivity(i);//自动安装
        stopSelf();
    }

    private void error() {
        if (updateProgressListener != null) {
            updateProgressListener.error();
        }
        stopSelf();
    }

    private static class DownloadApk extends AsyncTask<String, Integer, String> {
        private final String md5;
        private UpdateService updateService;

        public DownloadApk(UpdateService service, String md5) {
            this.updateService = service;
            this.md5 = md5;
        }

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            if (updateService != null) {
                updateService.start();
            }
        }

        @Override
        protected String doInBackground(String... strings) {
            final String downloadUrl = strings[0];

            final File file = new File(UpdateService.getDownloadDir(updateService),
                    UpdateService.getSaveFileName(downloadUrl));

            Log.d(TAG, "download url is " + downloadUrl);
            Log.d(TAG, "download apk cache at " + file.getAbsolutePath());

            File dir = file.getParentFile();
            if (!dir.exists()) {
                dir.mkdirs();
            }

            HttpURLConnection httpConnection = null;
            InputStream is = null;
            FileOutputStream fos = null;
            long updateTotalSize = 0;
            URL url;
            try {
                url = new URL(downloadUrl);
                httpConnection = (HttpURLConnection) url.openConnection();
                httpConnection.setConnectTimeout(20000);
                httpConnection.setReadTimeout(20000);

                Log.d(TAG, "download status code: " + httpConnection.getResponseCode());


                if (httpConnection.getResponseCode() != 200) {
                    return null;
                }

                updateTotalSize = httpConnection.getContentLength();

                if (file.exists()) {
                    if (updateTotalSize == file.length()) {
                        // 下载完成
                        if (TextUtils.isEmpty(md5) || MD5Util.getMD5String(file).toUpperCase().equals(md5.toUpperCase())) {
                            return file.getAbsolutePath();
                        }
                    } else {
                        file.delete();
                    }
                }
                file.createNewFile();
                is = httpConnection.getInputStream();
                fos = new FileOutputStream(file, false);
                byte buffer[] = new byte[4096];

                int readSize = 0;
                long currentSize = 0;

                while ((readSize = is.read(buffer)) > 0) {
                    fos.write(buffer, 0, readSize);
                    currentSize += readSize;
                    publishProgress((int) (currentSize * 100 / updateTotalSize));
                }
                // download success
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            } finally {
                if (httpConnection != null) {
                    httpConnection.disconnect();
                }
                if (is != null) {
                    try {
                        is.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (fos != null) {
                    try {
                        fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            try {
                if (TextUtils.isEmpty(md5) || MD5Util.getMD5String(file).toUpperCase().equals(md5.toUpperCase())) {
                    return file.getAbsolutePath();
                }
            } catch (IOException e) {
                e.printStackTrace();
                return file.getAbsolutePath();
            }
            Log.e(TAG, "md5 invalid");
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            if (updateService != null) {
                updateService.update(values[0]);
            }
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            if (updateService != null) {
                if (s != null) {
                    updateService.success(s);
                } else {
                    updateService.error();
                }
            }
        }
    }

    public static class Builder {
        private String downloadUrl;
        private String md5;
        private ServiceConnection serviceConnection;

        protected Builder(String downloadUrl) {
            this.downloadUrl = downloadUrl;
        }

        public static Builder create(String downloadUrl) {
            if (downloadUrl == null) {
                throw new NullPointerException("downloadUrl == null");
            }
            return new Builder(downloadUrl);
        }

        public String getMd5() {
            return md5;
        }

        public Builder setMd5(String md5) {
            this.md5 = md5;
            return this;
        }

        public Builder build(Context context, UpdateProgressListener listener) {
            if (context == null) {
                throw new NullPointerException("context == null");
            }
            Intent intent = new Intent();
            intent.setClass(context, UpdateService.class);
            intent.putExtra(URL, downloadUrl);
            intent.putExtra(KEY_MD5, md5);

            UpdateProgressListener delegateListener = new UpdateProgressListener() {
                @Override
                public void start() {
                    if (listener != null) {
                        listener.start();
                    }
                }

                @Override
                public void update(int var1) {
                    if (listener != null) {
                        listener.update(var1);
                    }
                }

                @Override
                public void success(String path) {
                    try {
                        context.unbindService(serviceConnection);
                    } catch (Throwable t) {
                        Log.e("UpdateService", "解绑失败" + t.getMessage());
                    }

                    if (listener != null) {
                        listener.success(path);
                    }
                }

                @Override
                public void error() {
                    try {
                        context.unbindService(serviceConnection);
                    } catch (Throwable t) {
                        Log.e("UpdateService", "解绑失败" + t.getMessage());
                    }
                    if (listener != null) {
                        listener.error();
                    }
                }
            };
            serviceConnection = new ServiceConnection() {
                @Override
                public void onServiceConnected(ComponentName name, IBinder service) {
                    LocalBinder binder = (LocalBinder) service;
                    binder.setUpdateProgressListener(delegateListener);
                }

                @Override
                public void onServiceDisconnected(ComponentName name) {
                }
            };
            context.bindService(intent, serviceConnection, Context.BIND_IMPORTANT);
            context.startService(intent);
            return this;
        }
    }

    public interface UpdateProgressListener {
        void start();

        void update(int var);

        void success(String path);

        void error();
    }
}
package com.mvp.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import com.mvp.myapplication.update.UpdateService;

public class MainActivity extends AppCompatActivity {
    private Button btnAllUpdate, btnAddUpdate, btnHotUpdate;

    private String url,md5;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btnAddUpdate = findViewById(R.id.btn_add_update);
        btnAllUpdate = findViewById(R.id.btn_all_update);
        btnHotUpdate = findViewById(R.id.btn_hot_update);

        Log.e("MainActivity","onCreate");

        btnAllUpdate.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                UpdateService.Builder.create(url)
                        .setMd5(md5)
                        .build(MainActivity.this, new UpdateService.UpdateProgressListener() {
                            @Override
                            public void start() {
                                Log.e("MainActivity", "start");
                            }

                            @Override
                            public void update(int var) {
                                Log.e("MainActivity", "update ===> " + var);
                            }

                            @Override
                            public void success(String path) {
                                Log.e("MainActivity", "success ===> " + path);
                            }

                            @Override
                            public void error() {
                                Log.e("MainActivity", "error");
                            }
                        });
            }
        });
    }
}
  • AndroidManifest
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication"
        tools:targetApi="31">
        <service
            android:name=".update.UpdateService"
            android:enabled="true"
            android:exported="true"></service>

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
        <!-- Android7以上需要 -->
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.update_app.file_provider"
            android:exported="false"
            android:grantUriPermissions="true"
            tools:replace="android:authorities">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/update_app_path" />
        </provider>
    </application>

</manifest>
  • update_app_path
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <cache-path
        name="update_app_cache_files"
        path="/update" />
    <external-path
        name="update_app_external_files"
        path="/" />
    <external-cache-path
        name="update_app_external_cache_files"
        path="/update" />
</paths>

热更新

严格意义上来说,个人认为热更新并不是用来进行包体升级,更多的用来进行修复bug的。例如,由于某个程序员的失误,在某个类中抛出了一个空指针异常,导致程序执行到该类后一直崩溃。这种情况下,其实就可以使用热更新来处理。因为,我们并没有大改app中的功能,只是某个类报错了。但个人认为热更新其实也不能解决所有的奔溃问题的,这些黑科技或多或少都是有一些兼容性的问题的,像Tinker就必须要冷启动才能修复,而且受限于Android的版本。
具体是技术实现方式可以参考笔者之前写的一篇博客:Android热修复1以及Android热更新十:自己写一个Android热修复

增量更新

什么是增量更新呢?举了例子,假设我们需要将apk从v1.0升级到v2.0,这时我们可以通过全量更新的方式,下载2.0版本的apk然后进行覆盖安装。但是,一般情况下2.0版本的apk往往包含了1.0版本的功能,理论上我们只需要下载二者的差分包,然后将差分包与1.0版本的包进行合并即可生成一个2.0版本的包。这样做的好处自然就是节约了流量了。像几乎所有的应用商店都使用增量更新的方式来更新apk。那么,我们该我们使用增量更新呢?这个就要借助一个工具:bsdiff
注意:如果想要使用增量更新,那么必须要有一个旧版本的apk,如果用户安装完apk后直接把旧版本的apk删掉了,那么还是老老实实使用全量更新的方式吧。

  • 拆——拆分出差分包

bsdiff oldfile newfile1 patchfile

  • 合——将旧版本的包与差分包进行合并

bspatch oldfile newfile2 patchfile

使用上面两步便可以完成差分包的拆分与合并,新生成的newfile2 与newfile1的md5是一致的。但是上面这两步法我们是在pc端进行的,我们该如何在代码中实现上面的逻辑呢?首先,拆分的逻辑还是在pc端中进行,客户端只需要关注如何合并差分包。
首先,我们需要导入bspatch相关的类
在这里插入图片描述
接着,我们新建一个类用于调用c相关的代码:

package com.mvp.myapplication.utils;

public class BSPatchUtil {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("bspatch");
    }

    /**
     * native方法 使用路径为oldApkPath的apk与路径为patchPath的补丁包,合成新的apk,并存储于newApkPath
     *
     * 返回:0,说明操作成功
     *
     * @param oldApkPath 示例:/sdcard/old.apk
     * @param outputApkPath 示例:/sdcard/output.apk
     * @param patchPath  示例:/sdcard/xx.patch
     * @return
     */
    public static native int bspatch(String oldApkPath, String outputApkPath,
                                   String patchPath);

}

package com.mvp.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import com.mvp.myapplication.update.UpdateService;
import com.mvp.myapplication.utils.BSPatchUtil;

import java.io.File;

public class MainActivity extends AppCompatActivity {
    private Button btnAllUpdate, btnAddUpdate, btnHotUpdate;

    private String url, md5;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btnAddUpdate = findViewById(R.id.btn_add_update);
        btnAllUpdate = findViewById(R.id.btn_all_update);
        btnHotUpdate = findViewById(R.id.btn_hot_update);

        Log.e("MainActivity", "onCreate");

        btnAllUpdate.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                UpdateService.Builder.create(url)
                        .setMd5(md5)
                        .build(MainActivity.this, new UpdateService.UpdateProgressListener() {
                            @Override
                            public void start() {
                                Log.e("MainActivity", "start");
                            }

                            @Override
                            public void update(int var) {
                                Log.e("MainActivity", "update ===> " + var);
                            }

                            @Override
                            public void success(String path) {
                                Log.e("MainActivity", "success ===> " + path);
                            }

                            @Override
                            public void error() {
                                Log.e("MainActivity", "error");
                            }
                        });
            }
        });

        btnAddUpdate.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                genNewApk();
            }
        });
    }

    private void genNewApk() {
        String oldpath = getApplicationInfo().sourceDir;
        String newpath = (this.getCacheDir().getAbsolutePath()+ File.separator
                + "composed_hivebox_apk.apk");

        String patchpath = (this.getCacheDir().getAbsolutePath()+ File.separator
                + "bs_patch");
        Log.e("MainActivity", "oldpath is " + oldpath + "\n newpath is " + newpath + "\n patchpath is " + patchpath);
        BSPatchUtil.bspatch(oldpath, newpath, patchpath);

    }
}

注意:需要修改bspatch.c文件中的Java_com_mvp_myapplication_utils_BSPatchUtil_bspatch方法签名:改为BSPatchUtil 的包名,例如BSPatchUtil对应的路径为com.dxl.testbatch.util.BSPatchUtil,那么native方法签名就是Java_com_mvp_myapplication_utils_BSPatchUtil_bspatch
这样便可以通过jni调用到c层面的代码。

接着,修改build.gradle文件,添加下面圈中的闭包
在这里插入图片描述
最后,我们执行Make Project命令,正常情况下便可以生成如下几个so库
在这里插入图片描述
最后,我们在把so库放入jniLibs文件夹中,然后build下生成apk包
在这里插入图片描述

然后,我们将差分包bs_patch放入手机的data/data目录下,点击按钮就会生成composed_hivebox_apk.apk这个apk包,将其与v2.0的包进行MD5对比,发现是一致的。如此,我们便实现了一个简单的增量更新逻辑。
Demo地址:https://gitee.com/hzulwy/add_-update/tree/master/MyApplication

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/410360.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Hadoop/Java】基于HDFS的Java Web网络云盘

【Hadoop/Java】基于HDFS的Java Web网络云盘 本人BNUZ大学生萌新&#xff0c;水平不足&#xff0c;还请各位多多指教&#xff01; 实验目的 熟悉HDFS Java API的使用&#xff1b;能使用HDFS Java API编写应用程序 实验要求 云盘系统通过互联网为企业和个人提供信息的储存、…

前后端部署+nginx配置

文章目录概要1、脚手架安装2、项目打包部署3、配置nginxEND概要 内容主要包括部署前端项目&#xff0c;nginx安装配置&#xff0c;以及后端项目的打包 1、脚手架安装 vue init webpack 项目运行&#xff08;默认端口8080&#xff09; npm run dev 如果前后端分离项目&…

使用Vue+el-form+form-validate实现管理端登录接口联调前准备工作实战

前言 这是《Vue + SpringBoot前后端分离项目实战》专栏的第7篇博客,感谢你能从成千上万篇博客中打开这一篇,和我一起学习前端开发实战知识,让我们一起开始吧。 目录 前言 一、上节回顾和本节介绍 1. 上节回顾

【可视化开发】echarts配置项——修改tooltip默认样式

修改tooltip默认样式 在可视化开发中我们通常会遇到修改tooltip样式问题&#xff0c;下面分享给大家代码片段和最终呈现效果 tooltip: {//鼠标悬浮框的提示文字trigger: "axis",axisPointer: {// 坐标轴指示器配置项。type: "none", // line 直线指示器 …

Vue3.0ElementPlus<input输入框自动获取焦点>

文章目录前言一、input-focus事件&#xff1f;二、使用步骤1.给input 设置ref 属性2.引入ref和nextTick3.在dialog打开事件中触发前言 记录一下自己最近开发vue3.0的小小问题~~ 最近在做项目时&#xff0c;dialog弹框事件需定位input焦点&#xff0c;方便用户可直接输入。原理…

【vue3】组合式API之setup()介绍与reactive()函数的使用

>&#x1f609;博主&#xff1a;初映CY的前说(前端领域) ,&#x1f4d2;本文核心&#xff1a;setup()概念、 reactive()的使用 【前言】vue3作为vue2的升级版&#xff0c;有着很多的新特性&#xff0c;其中就包括了组合式API&#xff0c;也就是是 Composition API。学习组合…

vue + gojs 实现拖拽 流程图

一、流程图效果 最近一段时间在研究go.js,它是一款前端开发画流程图的一个插件&#xff0c;也是一个难点&#xff0c;要说为什么是难点&#xff0c;首先&#xff0c;它是依赖画布canvas知识开发。其次&#xff0c;要依赖于内部API开发需求&#xff0c;开发项目需求的时候就要花…

js逆向爬取某音乐网站某歌手的歌曲

js逆向爬取某音乐网站某歌手的歌曲一、分析网站1、案例介绍2、寻找列表页Ajax入口&#xff08;1&#xff09;页面展示图。&#xff08;2&#xff09;寻找部分歌曲信息Ajax的token。&#xff08;3&#xff09;寻找歌曲链接&#xff08;4&#xff09;获取歌曲名称和id信息3、寻找…

vue-plugin-hiprint vue hiprint vue使用hiprint打印控件VUE HiPrint HiPrint简单使用

vue-plugin-hiprint vue hiprint vue使用hiprint打印控件VUE HiPrint HiPrint简单使用安装相关依赖安装 vue-plugin-hiprintJQuery安装 打印客户端引入依赖打印 html 内容 - 简单使用根据模版打印 - 简单使用以下内容 和上面demo 没关系 &#xff01;&#xff01;&#xff01;&…

使用videjs+vue2+elementui自定义播放器控件

一、安装项目所需依赖 videojs依赖&#xff1a; npm install --save-dev video.js elementui依赖&#xff08;这个图方便就不按需引入了&#xff09;&#xff1a; npm i element-ui -S 二、main.js修改 增加以下几行&#xff1a; import videojs from video.js import e…

成功解决:下载的谷歌浏览器,打开却是“2345浏览器”,方法亲测有效

今天打开谷歌浏览器使用&#xff0c;浏览器界面显示的2345浏览器&#xff0c;难道谷歌把2345收购了&#xff1f;应该不能&#xff0c;上网查找问题原因才发现&#xff0c;原来的谷歌首页是被劫持了。&#xff08;如果迫切解决问题&#xff0c;直接拉到底找方法&#xff09; 试了…

前端必备的谷歌浏览器JSON可视化插件:JSON-Handle

功能简介: 日常开发过程中,对接后台返回的数据接口时,数据格式常常是各种json格式字符串,在netWork里面查看十分不便,需要在网上找一个json格式化的工具再查看,然后再取数据字段,然后绑定到页面上,十分不便,推荐这么一款前端开发的浏览器插件工具给大家使用。 返回数…

【React-Hooks进阶】useState回调函数的参数 / useEffect发送网络请求/ useRef / useContext

前言 博主主页&#x1f449;&#x1f3fb;蜡笔雏田学代码 专栏链接&#x1f449;&#x1f3fb;React专栏 上篇文章初步学习了Hooks的基础知识 今天来深入学习Hooks的一些扩展知识 感兴趣的小伙伴一起来看看吧~&#x1f91e; 文章目录useState -回调函数的参数使用场景语法语法规…

el-switch

目录 在element ui中el-switch开关组件具有先改变开关值再传值的特点。&#xff08;先改后传&#xff09; 1、触发change事件时 2、绑定disabled属性写三元表达式时 3、解决办法 在element ui中el-switch开关组件具有先改变开关值再传值的特点。&#xff08;先改后传&#xf…

cron表达式 详解

corn表达式是&#xff1a;由若干数字、空格、符号按一定的规则&#xff0c;组成的一组字符串&#xff0c;从而表达时间的信息。 好像和正则表达式有点类似哈&#xff0c;都是一个字符串表示一些信息。Cron 表达式生成器&#xff1a; https://www.smart-tools.cn/cron简介Cron 表…

基于Java+MySQL 实现(Web)日程管理系统【100010222】

基于Java的日程管理系统开发 摘要 日程管理在日常生活中是十分普通的一件事情&#xff0c;人们无论在生活中还是工作中都会有大大小小、各种各样的事情安排&#xff0c;如果仅仅靠纸张或者自己记录这些事情&#xff0c;往往会遗忘。针对这样的痛点&#xff0c;本文提供了日程…

HTML小图标的使用(无需下载图标源码)

我们在浏览一个网页中&#xff0c;会遇到很多有趣的小图标&#xff0c;这些小图标与访问者形成了友好的互动&#xff0c;所以我们在开发中都会适当插入一些生动有趣的图标来吸引访问者。插入图标的网站有很多&#xff0c;我这里以阿里巴巴图标库&#xff08;iconfont-阿里巴巴矢…

Div标签里放img和span标签实现垂直水平居中

正常默认布局 代码实现&#xff1a; <div style"width: 400px; height:400px; background-color:blueviolet"><img style"width: 80px; height: 80px;" src"./picture.png"><span style"color:white;">我是span标…

Vue3 使用MD5加密(清晰明了)

概述 最近在想做个cloud项目,gitee上找了个模板项目&#xff0c;前端使用到vue3 typeScript、Element Plus、Vue Router、Pinia、Axios、i18n、Vite等技术&#xff0c;最近使用到vue3 MD5加密&#xff0c;顺便学习一下&#xff0c;在此总结一下&#xff0c;若有不足之处&…

《EDA前端软件开发工程师面试指南》

2023届EDA领域校招总结&#xff0c;完结撒花&#xff01;&#xff01;&#xff01; 目录 前言 一、EDA公司介绍 二、项目面试 1.自我介绍 2.项目深入 3.专业经验 4.成果和技能 5.对面试官有什么问题 三、C面试 1、高频考点 2、其他知识点 3、算法题 四、逻辑综合面…