electron实现软件(热)更新(附带示例源码)
热更新指的是:electron 程序已经开启,在不关闭的情况下执行更新,需要我们把远程的app.asar文件下载到本地执行替换,然而 在electron应用程序开启状态是无法直接下载app.asar文件的,下载会检查出app.asar文件被占用,所以我们需要在本地将app.asar文件反编译,编译出一个app文件夹,里面有项目所需的所有源码文件,这时通过vue或react打包的渲染进程代码是经过压缩的,但是主进程代码会直接暴露,所以刚好,我们可以将主进程代码做压缩混淆,然后生成一个
app.zip
压缩包,上传至服务器,然后下载这个压缩包,解压,将其编译成app.asar
文件替换到resources目录中,从而实现electron软件的热更新。
- 主进程(nodejs)侧代码:
const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')
const fs = require("fs");
const http = require("http");
const asar = require('asar');
const AdmZip = require('adm-zip');
const fsExtra = require('fs-extra');const mainData = require("./mainData");
const txtConsole = require("./txtConsole");//当前环境
const production = mainData?.production;//Electron 安装根目录
const rootPath = production === 'dev' ? path.resolve('./public') : path.dirname(app.getPath('exe'));
mainData.rootPath = rootPath;// 软件更新配置信息(目前需要手动修改~~~)
const winUpdateConfig = {currentVersion: null, //当前版本updateVersionFilePath: 'http://103.117.121.53:8002/latest', //远程版本信息路径updateFilePath: 'http://103.117.121.53:8002/app.zip', //远程包路径localUpdateVersionFilePath: production === 'dev' ? `${rootPath}/latest` : `${rootPath}/resources/latest`, //本地版本信息路径localUpdateFilePath: production === 'dev' ? rootPath : `${rootPath}/resources`, //本地包路径updateSteps: [{id: 1, desc: '开始下载并解压更新文件,请勿重启!', active: 'active'},{id: 2, desc: '下载并解压完成, 开始覆盖安装!', active: 'wait'},{id: 3, desc: '更新完毕, 即将重启,请稍候!(第3步完成后也可以手动重启)', active: 'wait'},], //更新步骤 active:正在进行 wait:等待 success:执行成功 error: 执行失败
};let versionInfo = ''; //获取最新版本信息
let locallatest = ''; //本地版本号function appInit() {txtConsole.log('初始化');try {locallatest = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath, 'utf-8');locallatest = JSON.parse(locallatest);//设置当前版本信息winUpdateConfig.currentVersion = locallatest?.version;txtConsole.log('已设置当前版本信息', locallatest?.version);//删除日志txtConsole.clearLog();} catch (err) {txtConsole.log(err);}
}//创建主窗口
function createWindow() {const mainWindow = new BrowserWindow({width: 800,height: 600,webPreferences: {// preload: path.join(__dirname, 'preload.js'),nodeIntegration: true,contextIsolation: false,webSecurity: false, //禁用同源策略}});mainWindow.loadFile('index.html').then();// 打开开发者工具 控制台if (mainData.winControl === 'dev') {mainWindow.webContents.openDevTools();}// 检查更新ipcMain.on('window-version', function (event) {try {txtConsole.log('检查更新', versionInfo);if (!versionInfo) {!event?.sender?.isDestroyed() &&event?.sender?.send('window-version-err-msg', '更新文件读取失败');return;}const v = {...(versionInfo || {})};//最新版本号let firNewVersion = versionInfo?.version?.split('.')?.[0]; //第一位let secNewVersion = versionInfo?.version?.split('.')?.[1]; //第二位let thiNewVersion = versionInfo?.version?.split('.')?.[2]; //第三位//当前版本号let firOldVersion = versionInfo?.currentVersion?.split('.')?.[0]; //第一位let secOldVersion = versionInfo?.currentVersion?.split('.')?.[1]; //第二位let thiOldVersion = versionInfo?.currentVersion?.split('.')?.[2]; //第三位//按位比较是否需要更新if (Number(firNewVersion || 10000) > Number(firOldVersion || 10000)) {v['versionVisible'] = true;}else if (Number(secNewVersion || 10000) > Number(secOldVersion || 10000)) {v['versionVisible'] = true;}else if (Number(thiNewVersion || 10000) > Number(thiOldVersion || 10000)) {v['versionVisible'] = true;}else {v['versionVisible'] = false;v['currentVersion'] = versionInfo?.version;}if (!v['versionVisible']) {let latest = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath, 'utf-8');latest = JSON.parse(latest);latest.version = versionInfo?.version || latest?.version;latest.currentVersion = versionInfo?.version || latest?.version;fs.writeFileSync(winUpdateConfig.localUpdateVersionFilePath, JSON.stringify(latest));txtConsole.log('hot: ', latest.version);}txtConsole.log('versionVisible=> ', v['versionVisible']);!event?.sender?.isDestroyed() && event?.sender?.send('window-version-msg', v);} catch (err) {!event?.sender?.isDestroyed() &&event?.sender?.send('window-version-err-msg', '更新文件读取失败');txtConsole.log('检查更新err:', err);}});// 下载更新文件ipcMain.on('window-download-newfile', function (event) {txtConsole.log('开始下载并解压更新文件 热更新');event?.sender?.send('window-download-newfile-msg', winUpdateConfig.updateSteps);const file = fs.createWriteStream(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'),);let downloadedBytes = 0;let totalBytes = 0;http.get(winUpdateConfig.updateFilePath, (response) => {totalBytes = parseInt(response?.headers['content-length'], 10);let prevTimestamp = Date.now();response?.on('data', (chunk) => {downloadedBytes += chunk.length;const timestamp = Date.now();const timeDiff = timestamp - prevTimestamp;// 每1.5秒钟更新一次进度if (timeDiff >= 1500) {const progress = ((downloadedBytes / totalBytes) * 100).toFixed(2);txtConsole.log(`下载进度:${progress}% `, totalBytes);prevTimestamp = timestamp;event?.sender?.send('window-download-progress-msg', Math.min(Number(progress), 80));}});response?.pipe(file);}).on('error', (err) => {txtConsole.log(`下载错误: ${err.message}`);event?.sender?.send('window-download-newfile-err-msg', '更新文件下载失败');});file?.on('finish', function () {event.sender.send('window-download-progress-msg', 90);winUpdateConfig.updateSteps[0]['active'] = 'success';winUpdateConfig.updateSteps[1]['active'] = 'active';event?.sender?.send('window-download-newfile-msg', winUpdateConfig.updateSteps);// 文件已经完全写入磁盘,开始解压try {const zip = new AdmZip(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'), void 0);zip.extractAllTo(winUpdateConfig.localUpdateFilePath, true, void 0, void 0);} catch (err) {txtConsole.log('解压异常 error: ', err);!event?.sender?.isDestroyed() &&event?.sender?.send('window-download-newfile-err-msg', '解压异常');return;}winUpdateConfig.updateSteps[1]['active'] = 'success';winUpdateConfig.updateSteps[2]['active'] = 'active';event?.sender?.send('window-download-newfile-msg', winUpdateConfig.updateSteps);event?.sender?.send('window-download-progress-msg', 95);const sourceDir = path.join(winUpdateConfig.localUpdateFilePath, 'apps');const destPath = path.join(winUpdateConfig.localUpdateFilePath, 'app.asar');asar.createPackage(sourceDir, destPath).then(() => {if (fs.existsSync(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'))) {fs.unlinkSync(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'));}txtConsole.log('更新完毕');event.sender.send('window-download-progress-msg', 100);winUpdateConfig.updateSteps[2]['active'] = 'success';event?.sender?.send('window-download-newfile-msg',winUpdateConfig.updateSteps,'success',);//设置当前版本信息try {let latest = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath, 'utf-8');latest = JSON.parse(latest);latest.version = versionInfo.version;fs.writeFileSync(winUpdateConfig.localUpdateVersionFilePath, JSON.stringify(latest));txtConsole.log('更新后已设置当前版本信息', latest?.version);//删除apps文件夹 防止执行文件夹内的代码deleteFolderRecursive(sourceDir);} catch (err) {txtConsole.log(err);}}).catch((err) => {txtConsole.log('创建asar文件失败: ', err);event.sender.send('window-download-newfile-err-msg', 'asar文件创建失败');});});file?.on('error', function (err) {txtConsole.log('更新asar=>Error: ', err);event.sender.send('window-download-newfile-err-msg', err);});});
}//检查更新
function checkUpdate(callback) {txtConsole.log('检查更新');http.get(winUpdateConfig.updateVersionFilePath, (res) => {res.on('data', (chunk) => {versionInfo += chunk;});res.on('end', () => {try {if (versionInfo && versionInfo?.indexOf('404 Not Found') < 0) {versionInfo = JSON.parse(versionInfo);winUpdateConfig.updateFilePath = versionInfo.updateFilePath;//热更最新信息let asarVersionInfo = {newVersionDesc: versionInfo.newVersionDesc,currentVersion: winUpdateConfig.currentVersion,};versionInfo.currentVersion = winUpdateConfig.currentVersion;let writeNewVersonInfo;//不存在则创建latest文件if (!fs.existsSync(winUpdateConfig.localUpdateVersionFilePath)) {writeNewVersonInfo = versionInfo;txtConsole.log('latest文件重新创建成功');}else {let currentVersion = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath,'utf8',);currentVersion = JSON.parse(currentVersion);currentVersion['updateFilePath'] = '';//只覆盖热更版本信息writeNewVersonInfo = {...currentVersion, ...asarVersionInfo};}//将整理好的配置文件信息写入fs.writeFileSync(winUpdateConfig.localUpdateVersionFilePath,JSON.stringify(writeNewVersonInfo),);// txtConsole.log('已将新的更新配置文件信息写入:', JSON.stringify(writeNewVersonInfo));txtConsole.log(`更新检查完毕:最新版本:${versionInfo.version}, 当前版本:${asarVersionInfo.currentVersion}`,);txtConsole.log('-------------------------------------------------------');callback?.(null, versionInfo);}else {txtConsole.log('更新配置文件读取失败');callback?.('更新配置文件读取失败');}} catch (err) {txtConsole.log('更新配置文件覆写失败');callback?.('更新配置文件覆写失败');}});}).on('error', (error) => {txtConsole.log(`更新配置文件下载失败: ${error.message}`);callback?.('更新配置文件下载失败');});
}//删除更新文件
function deleteFolderRecursive(folderPath) {if (fs.existsSync(folderPath)) {fs.readdirSync(folderPath).forEach((file) => {const curPath = path.join(folderPath, file);if (fs.lstatSync(curPath).isDirectory()) {// 递归删除子文件夹deleteFolderRecursive(curPath);}else {// 删除文件fs.unlinkSync(curPath);}});// 删除子文件夹后删除文件夹本身fs.rmdirSync(folderPath);}
}app.whenReady().then(() => {//初始化appInit();//检查更新checkUpdate(async (check, versionInfo = {}) => {if (check) {txtConsole.log('检查更新执行失败');}else {txtConsole.log('检查更新执行成功');}createWindow();app.on('activate', function () {if (BrowserWindow.getAllWindows().length === 0) createWindow()})});
})app.on('window-all-closed', function () {if (process.platform !== 'darwin') app.quit()
})
- 渲染进程侧代码(以原生为例):
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1.0"/><title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
<button onclick="onCheckUpdate()">检查更新</button>
<button onclick="onUpdateVersion()">测试更新</button><div class="updateInfo"></div>
<div class="descInfo"></div><script>const {ipcRenderer} = require("electron");const onVersion = {updateDsec: [],process: 0,};//检查更新function onCheckUpdate() {ipcRenderer?.send('window-version');ipcRenderer?.once('window-version-msg', (_, bool) => {document.querySelector('.updateInfo').innerHTML = JSON.stringify(bool);});ipcRenderer?.once('window-version-err-msg', (_, err) => {document.querySelector('.updateInfo').innerHTML = err;});}//测试更新function onUpdateVersion() {onVersion.updateDsec = [];ipcRenderer?.send('window-download-newfile');//监听下载版本信息ipcRenderer?.on('window-download-newfile-msg', (event, updateDsec, status) => {if (!event.handel) {event.handel = true;onVersion.isStartUpdate = true;onVersion.updateDsec = updateDsec;document.querySelector('.descInfo').innerHTML = updateDsec.map(item => `<span style="color:${item.active === 'success' ? 'green' : 'orangered'}">${item.desc}</span>`).join('</br>');if (status === 'success') {ipcRenderer?.send('window-restart-app');}onVersion.visible = true;}});//监听更新包下载进度ipcRenderer?.on('window-download-progress-msg', (event, process) => {if (!event.handel) {event.handel = true;document.querySelector('.updateInfo').innerHTML = String("完成进度:" + process + '%');}});//监听下载版本错误信息ipcRenderer?.once('window-download-newfile-err-msg', (event, res) => {console.log(res)});}
</script>
</body>
</html>
- 轮子(实现压缩混淆反编译):
//生成 反编译app.asar 并生成压缩包
const asar = require('asar');
const path = require('path');
const fs = require('fs');
const fsExtra = require('fs-extra');
const zlib = require('zlib');
const archiver = require('archiver');
const uglify = require('uglify-js');
const moment = require('moment');
const {exec} = require('child_process');
const JavaScriptObfuscator = require('javascript-obfuscator');const mainData = require('./mainData');const startTime = moment().unix(); //秒级时间戳const rootPath = path.resolve(__dirname); // 获取项目根路径
const asarPath = './build/win-ia32-unpacked/resources/app.asar'; // 获取 app.asar 文件路径
const sourceDir = './apps'; // 要压缩的文件夹路径
const asarAppPath = './apps/apps'; // asar反编译文件的存放路径
const buildPath = './build'; //electron 打包后的build文件夹
const destFile = './app.zip'; // 压缩后的文件路径
const publicLogPath = './public/log.txt';
const publicAsarPath = './public/app.asar';// 配置环境路径为项目根路径
const env = Object.assign({}, process.env, {PATH: rootPath + ';' + process.env.PATH,npm_config_prefix: 'C:\\Program Files\\nodejs\\npm', // 这里是你的 npm 安装路径
});const Console = {log(p1 = '', p2 = '', p3 = '', p4 = '', p5 = '') {console.log(`${moment().format('HH:mm:ss')} | ${p1}${p2}${p3}${p4}${p5}`);},
};//压缩主进程 main.js 相关代码
function zipMainJS() {try {const dir = path.resolve(asarAppPath, 'main.js');const dirJs = fs.readFileSync(dir, 'utf8');//压缩代码 mangle: true,const result = uglify.minify(dirJs, {mangle: {toplevel: true,},});// 混淆代码const obfuscationResult = JavaScriptObfuscator.obfuscate(result.code, {compact: true,controlFlowFlattening: true,controlFlowFlatteningThreshold: 0.75,numbersToExpressions: true,simplify: true,shuffleStringArray: true,splitStrings: true,stringArrayThreshold: 0.75,});fs.writeFileSync(dir, obfuscationResult.getObfuscatedCode());return true;} catch (err) {Console.log(err);return false;}
}//添加开始执行 app.asar反编译逻辑
async function init() {//执行app.asar 反编译、压缩、混淆Console.log('正在执行app.asar 反编译、压缩、混淆');// 将 app.asar 解压缩到指定文件夹中asar.extractAll(asarPath, asarAppPath);Console.log('正在压缩app文件夹到项目根目录');//压缩main.js相关代码let zipRes = zipMainJS();if (!zipRes) {Console.log('!!!压缩main.js主进程代码失败!');return;}Console.log('主进程相关代码压缩完毕');//再次生成 app.asarasar.createPackage(asarAppPath, asarPath).then(() => {Console.log('已再次生成 app.asar 文件(代码压缩后的asar文件)');onAppZip();});
}function onAppZip() {// 创建一个可写流,将压缩后的文件写入到目标文件中const destStream = fs.createWriteStream(destFile);// 创建一个 archiver 实例const archive = archiver('zip', {zlib: {level: zlib.constants.Z_BEST_COMPRESSION},});// 将可写流传递给 archiver 实例archive.pipe(destStream);// 将要压缩的文件夹添加到 archiver 实例中archive.directory(sourceDir, false, null);// 完成压缩并关闭可写流archive.finalize();// 监听可写流的 'close' 事件,表示压缩完成destStream.on('close', () => {Console.log(`压缩完毕,压缩包路径:【${path.resolve(__dirname, destFile)}】`);Console.log('共用时:' + (moment().unix() - startTime) + '秒');});
}try {if (mainData?.production === 'dev') {throw "请将环境切换为生产环境 mainData.js => 【const production = 'pro';】";}if (mainData?.winControl === 'dev') {throw '请关闭主窗口调试控制台!' + 'winControl';}fsExtra.removeSync(buildPath);Console.log('已删除build文件夹内容');fsExtra.removeSync(publicLogPath);Console.log('已删除public/log.txt');fsExtra.removeSync(publicAsarPath);Console.log('已删除public/app.asar');fsExtra.removeSync(asarAppPath);Console.log('已删除apps');//执行 打包命令Console.log('正在执行【npm run packager32】命令');exec('npm run packager32', env, (error, stdout, stderr) => {if (error) {Console.log(`执行出错: ${error}`);return;}stderr && Console.log('【npm run packager32】 stderr=>', stderr);//生成app.init();});} catch (err) {Console.log(err);
}
示例Demo: https://github.com/qglovehy/electron-updater.git
相关文章:
electron实现软件(热)更新(附带示例源码)
热更新指的是:electron 程序已经开启,在不关闭的情况下执行更新,需要我们把远程的app.asar文件下载到本地执行替换,然而 在electron应用程序开启状态是无法直接下载app.asar文件的,下载会检查出app.asar文件被占用&…...

飞天使-k8s知识点12-kubernetes散装知识点1-架构有状态资源对象分类
文章目录 k8s架构图有状态和无状态服务 资源和对象对象规约和状态 资源的对象-资源的分类元数据型与集群型资源命名空间 k8s架构图 有状态和无状态服务 区分有状态和无状态服务有利于维护yaml文件 因为配置不同资源和对象 命令行yaml来定义对象对象规约和状态 规约 spec 描述…...

mhz_c1f
信息收集 探测到存活主机的IP地址为 192.168.101.32 # nmap -sT --min-rate 10000 -p- 192.168.101.32 -oN port.nmap Starting Nmap 7.94 ( https://nmap.org ) at 2024-02-03 13:41 CST Nmap scan report for 192.168.101.32 Host is up (0.0020s latency). Not shown: 6553…...

Excel——高级筛选匹配条件提取数据
一、筛选多条件 Q:筛选多个条件,并将筛选出的内容复制到其他区域 点击任意一个单元格 点击【数据】——【筛选】——【高级筛选】 选择【将筛选结果复制到其他位置】——在【列表区域】 鼠标选择对应的区域位置,条件区域一定要单独写出来&a…...

Python初学者学习记录——python基础综合案例:数据可视化——动态柱状图
一、案例效果 通过pyecharts可以实现数据的动态显示,直观的感受1960~2019年世界各国GDP的变化趋势 二、通过Bar构建基础柱状图 反转x轴和y轴 标签数值在右侧 from pyecharts.charts import Bar from pyecharts.options import LabelOpts# 构建柱状图对象 bar Bar()…...

1.27马尔科夫链,抽样蒙特卡洛模拟(逆转化方法,接受拒绝矩阵),马尔科夫链蒙特卡洛MCMC,隐马尔科夫(HMM(V算法剪枝优化),NLP)
马尔科夫链 蒙特卡洛法模拟 抽样,逆转换方法 就是说由系统自带的随机函数RANDOM,通过下面这个方法,可以变为对应的随机模拟函数 就是说要实现蒙特卡洛模拟,是要先有一个概率表达式,然后基于这个概率表达式࿰…...

MC34063异常发热分析
问题描述: 工程现场反馈若干电源转换模块损坏,没有输出。拿到问题模块后,查看有一个MC34063周围的PCB有比较明显的高温痕迹,配套的电感也有明显的高温过热痕迹。 问题调查: MC34063的电路非常经典(虽然自…...

获取真实 IP 地址(一):判断是否使用 CDN(附链接)
一、介绍 CDN,全称为内容分发网络(Content Delivery Network),是一种网络架构,旨在提高用户对于网络上内容的访问速度和性能。CDN通过在全球各地部署分布式服务器节点来存储和分发静态和动态内容,从而减少…...

跨越财务困境,聚道云软件连接器如何助力企业轻松实现数字化转型?
客户介绍 某家庭服务科技有限公司是一家专注于提供高品质家庭服务的综合性企业。公司以“让家庭生活更美好”为使命,致力于为每一位客户提供专业、细致、周到的家庭服务。作为一家具有社会责任感的企业,该公司积极履行企业公民义务,关注家庭…...

Python接口自动化测试框架运行原理及流程
这篇文章主要介绍了Python接口自动化测试框架运行原理及流程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 本文总结分享介绍接口测试框架开发,环境使用python3selenium3unittestddtrequests测试框…...
strtok的使用
Strtok:原型char *strtok(char s[], const char *delim); s为要分解的字符,delim为分隔符字符(如果传入字符串,则传入的字符串中每个字符均为分割符)。首次调用时,s指向要分解的字符串,之后再次…...

0206作业
TCP(传输控制协议)和 UDP(用户数据报协议)是两种常用的网络传输协议。它们之间的主要区别在于: 可靠性:TCP 是一种可靠的传输协议,它提供了数据传输的确认、重传和排序功能。如果数据在传输过程…...
数据结构-栈
1.容器 容器用于容纳元素集合,并对元素集合进行管理和维护. 传统意义上的管理和维护就是:增,删,改,查. 我们分析每种类型容器时,主要分析其增,删,改ÿ…...

CentOS7搭建k8s-v1.28.6集群详情
文章目录 1.灌装集群节点操作系统1.1 设置hosts1.2 设置nameserver1.3 关闭防火墙1.4 关闭Selinux1.5 关闭Swap分区1.6 时间同步1.7 调整内核参数1.8 系统内核升级 2.安装Docker2.1 卸载旧Docker2.2 配置Docker软件源2.3 安装Docker 3.部署Kubernets集群3.1 设置 K8s 软件源3.2…...

Android实现底部导航栏方法(Navigation篇)
Navigation实现底部导航栏 前言导入和基本使用导入基础使用创建nav文件编辑Nav文件添加页面(代码版)添加页面(图解版) 创建导航动作 action创建action(代码版)创建action(图解版) 编…...
python 爬虫篇(1)---->re正则的详细讲解(附带演示代码)
re正则的详细讲解 文章目录 re正则的详细讲解前言4.re正则表达式(1)e正则的匹配模式(2) re.search 的使用(3)re.findall()的使用(4)re.sub()的使用结语前言 大家好,今天我将开始更新python爬虫篇,陆续更新几种解析数据的方法,例如 re正则表达式beautifulsoup xpath lxml 等等,…...

(超详细)10-YOLOV5改进-替换CIou为Wise-IoU
yolov5中box_iou其默认用的是CIoU,其中代码还带有GIoU,DIoU,文件路径:utils/metrics.py,函数名为:bbox_iou 将下面代码放到metrics.py文件里面,原来的bbox_iou函数删掉 class WIoU_Scale: mon…...

Java-并发高频面试题-2
接着之前的Java-并发高频面试题 7. synchronized的实现原理是怎么样的? 首先我们要知道synchronized它是解决线程安全问题的一种方式,而具体是怎么解决的呢?主要是通过加锁的方式来解决 在底层实现上来看 是通过 monitorenter、monitorexit…...
Windows安装Redis
安装Redis是一个比较简单的过程,以下是在Windows上安装Redis的基本步骤: 下载Redis:首先,你需要从Redis官方网站(https://redis.io/download)下载适合Windows的Redis安装包。你可以选择稳定版本或者开发版本…...

Nicn的刷题日常之 有序序列判断
目录 1.题目描述 描述 输入描述: 输出描述: 示例1 示例2 示例3 2.解题 1.题目描述 描述 输入一个整数序列,判断是否是有序序列,有序,指序列中的整数从小到大排序或者从大到小排序(相同元素也视为有序)。 数据…...
[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?
🧠 智能合约中的数据是如何在区块链中保持一致的? 为什么所有区块链节点都能得出相同结果?合约调用这么复杂,状态真能保持一致吗?本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里…...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...

解决Ubuntu22.04 VMware失败的问题 ubuntu入门之二十八
现象1 打开VMware失败 Ubuntu升级之后打开VMware上报需要安装vmmon和vmnet,点击确认后如下提示 最终上报fail 解决方法 内核升级导致,需要在新内核下重新下载编译安装 查看版本 $ vmware -v VMware Workstation 17.5.1 build-23298084$ lsb_release…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...

Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...

Unity | AmplifyShaderEditor插件基础(第七集:平面波动shader)
目录 一、👋🏻前言 二、😈sinx波动的基本原理 三、😈波动起来 1.sinx节点介绍 2.vertexPosition 3.集成Vector3 a.节点Append b.连起来 4.波动起来 a.波动的原理 b.时间节点 c.sinx的处理 四、🌊波动优化…...

Git 3天2K星标:Datawhale 的 Happy-LLM 项目介绍(附教程)
引言 在人工智能飞速发展的今天,大语言模型(Large Language Models, LLMs)已成为技术领域的焦点。从智能写作到代码生成,LLM 的应用场景不断扩展,深刻改变了我们的工作和生活方式。然而,理解这些模型的内部…...
适应性Java用于现代 API:REST、GraphQL 和事件驱动
在快速发展的软件开发领域,REST、GraphQL 和事件驱动架构等新的 API 标准对于构建可扩展、高效的系统至关重要。Java 在现代 API 方面以其在企业应用中的稳定性而闻名,不断适应这些现代范式的需求。随着不断发展的生态系统,Java 在现代 API 方…...

DeepSeek源码深度解析 × 华为仓颉语言编程精粹——从MoE架构到全场景开发生态
前言 在人工智能技术飞速发展的今天,深度学习与大模型技术已成为推动行业变革的核心驱动力,而高效、灵活的开发工具与编程语言则为技术创新提供了重要支撑。本书以两大前沿技术领域为核心,系统性地呈现了两部深度技术著作的精华:…...