Service Work离线体验与性能优化
Service Work离线体验与性能优化
引言
先放个意外事件,万事开头难🤣🤣🤣
原计划是分享离线应用与数据资源缓存的应用实践,结果发现这一技术已被web标准废弃
曾经做过一个PC应用,业务需求要求应用具备容灾机制,能够在无网络情况下离线使用,并在网络恢复后同步数据。这需要利用缓存技术来实现。
传统前端缓存技术
HTTP缓存
- 工作原理:HTTP缓存通过请求头中的过期时间和标识符来判断是否使用缓存。
- 局限性:依赖于服务器配置,不适用于复杂的离线场景。
浏览器缓存
- 主要形式:localStorage、sessionStorage、cookie。
- 用途:存储少量的数据,但不适合大规模数据缓存。
应用缓存–Application Cache
- 全称:Offline Web Application。
- 特点:通过manifest文件标注要缓存的静态文件清单。
- 问题:更新机制复杂,页面更新延迟,已被Web标准废弃
它的缓存内容被存在浏览器的Application Cache中。主要是通过manifest文件来标注要被缓存的静态文件清单。但是在缓存静态文件的同时,也会默认缓存html文件。这导致页面的更新只能通过manifest文件中的版本号来决定。而且,即使我们更新了version,用户的第一次访问还是会访问到老的页面,只有下一次再访问才能访问到新的页面。所以,应用缓存只适合那种常年不变化的静态网站。
数据库缓存
- 选项:WebSQL(已废弃)、IndexedDB。
- IndexedDB:浏览器提供的本地数据库,支持大量数据存储和索引,适合复杂的数据缓存需求。
IndexedDB 就是浏览器提供的本地数据库,它可以被网页脚本创建和操作。IndexedDB 允许储存大量数据,提供查找接口,还能建立索引。这些都是 LocalStorage 所不具备的。就数据库类型而言,IndexedDB 不属于关系型数据库(不支持 SQL 查询语句),更接近 NoSQL 数据库。
因此通过Manifest+indexDB可做到应用可离线使用,数据缓存且不丢失。
- Manifest配置需要缓存的静态资源(打包过后的dist文件内容)
- indexDB对业务数据进行存储
既然manifest这一方案已被废弃。于是转战Service Worker这一更优方案。
Service Worker概述
什么是 Service Worker?
- 定义:一种可编程的网络代理,允许你拦截和处理应用发出的所有网络请求,包括拦截和处理网络请求、管理缓存和处理推送通知等。
- 功能:离线支持、推送通知、缓存管理等。
Service Worker的应用场景
- 离线支持:通过缓存静态资源和动态内容,确保应用在没有网络连接时仍然可以使用
- 缓存管理:提高应用性能,通过缓存减少网络请求次数和加快页面加载速度。
- 推送通知:Service Worker 可以处理推送通知,即使用户没有打开应用也能接收消息。
三、Service Worker的特点
- 独立于主线程: Service Worker 运行在独立的线程中,不会阻塞主页面的执行,是后台运行的脚本。
- 生命周期管理:Service Worker 有安装、激活和更新等生命周期事件,被install后就永远存在,除非被手动卸载。
- 跨域与安全限制:同源策略,必须是https的协议才能使用。
- 不能直接操纵dom:因为Service Worker是个独立于网页运行的脚本。
- 可拦截请求和返回,缓存文件。Service Worker可以通过fetch这个api,来拦截网络和处理网络请求,再配合cacheStorage来实现web页面的缓存管理以及与前端postMessage通信。
Service Worker的生命周期
当一个Service Worker被注册成功后,它将开始它的生命周期,我们对Service Worker的操作一般都是在其生命周期里面进行的。Service Worker的生命周期分为这么几个状态 安装中, 安装后, 激活中, 激活后, 废弃。
- 安装( Installing ): 这个状态发生在 Service Worker 注册之后,表示开始安装,这个状态会触发 install 事件,一般会在install事件的回调里面进行静态资源的离线缓存, 如果这些静态资源缓存失败了,那 Service Worker 安装就会失败,生命周期终止。
- 安装后( Installed ): 当成功捕获缓存到的资源时,Service Worker 会变为这个状态,当此时没有其他的Service Worker 线程在工作时,会立即进入激活状态,如果此时有正在工作的Service Worker 工作线程,则会等待其他的 Service Worker 线程被关闭后才会被激活。可以使用 self.skipWaiting() 方法强制正在等待的servicework工作线程进入激活状态。
- 激活( Activating ): 在这个状态下会触发activate事件,在activate 事件的回调中去清理旧版缓存。
- 激活后( Activated ): 在这个状态下,servicework会取得对整个页面的控制
- 废弃状态 ( redundant ): 这个状态表示一个 Service Worker 的生命周期结束。新版本的 Service Worker 替换了旧版本的 Service Worker会出现这个状态
Service Worker的缓存机制
Service Worker 技术不可或缺的一个方面是 Cache 接口,它是一种完全独立于 HTTP 缓存的缓存机制。可在 Service Worker 作用域和主线程作用域内访问 Cache 接口。
HTTP缓存会受到 HTTP 标头中指定的缓存指令的影响,而 Cache 接口可通过 JavaScript 进行编程。这意味着,网络请求的响应可以基于最适合指定网站的任何逻辑。例如:
- 在第一次请求时将静态资源存储在缓存中,并且仅为每个后续请求从缓存中提供这些资源
- 将网页标记存储在缓存中,但仅在离线场景中提供缓存中的标记。
- 从缓存中为某些资产提供过时的响应,但要在后台通过网络对其进行更新。
- 从网络流式传输部分内容,并将其与缓存中的 App Shell 组合起来,以提升感知性能。
Service Worker的简单实践
- 浏览器兼容性检查
首先,在开始使用 Service Worker 之前,你需要确保用户的浏览器支持这项技术。Service Worker 对象存在于navigator对象下,可以通过以下代码来检查
if ('serviceWorker' in navigator) {// 浏览器支持 Service Worker} else {console.log('Service Worker not supported');
}
- 注册Service Worker
注册是启动 Service Worker 的第一步。通常是在主应用中通过 JavaScript 来完成这个过程,在主线程中调用navigator.serviceWorker.register()方法来注册 Service Worker:
register 方法接受两个参数
第一个参数表示ServiceWork.js相对于origin的路径
第二个参数是 Serivce Worker 的配置项,可选填,其中比较重要的是 scope 属性,用来指定你想让 service worker 控制的内容的目录。 默认值为servicework.js所在的目录。这个属性所表示的路径不能在 service worker 文件的路径之上,默认是 Serivce Worker 文件所在的目录。 成功注册或返回一个promise。
if ('serviceWorker' in navigator) {// 浏览器支持 Service Workerwindow.addEventListener('load', () => {navigator.serviceWorker.register('/service-worker.js').then((registration) => {console.log('Service Worker registered with scope:', registration.scope)}).catch((error) => {console.error('Service Worker registration failed:', error)})
})} else {console.log('Service Worker not supported');
}
此代码在主线程上运行,并执行以下操作:
1、由于用户首次访问网站发生在没有注册的 Service Worker 的情况下, 等到页面完全加载后再注册一个。 这样可以在 Service Worker 预缓存任何内容时避免带宽争用。
2、进行快速检查有助于避免在不支持此功能的浏览器出现错误。
3、当页面完全加载且支持 Service Worker 时,注册 /service-worker.js。
- Service Worker-安装(Installing)
安装事件发生在 Service Worker 首次安装时,每个 Service Worker 仅调用一次 install,并且在更新之前不会再次触发。
self.addEventListener('install', event => {event.waitUntil(caches.open('缓存版本ID').then(cache => {return cache.addAll(['/',...]);}));
});
此代码会创建一个新的 Cache 实例并预缓存资产。
此处重点关注:event.waitUntil事件
event.waitUntil 接受 promise; 并等待该 promise 得到解决。 该 promise 执行两项异步操作:
创建名为 ‘缓存版本ID’ 的新 Cache 实例。
创建缓存后 资源网址数组使用其异步缓存资源预缓存 addAll 方法。
如果传递给 event.waitUntil 的 promise 已拒绝。 如果发生这种情况,Service Worker 会被丢弃。
- Service Worker-激活
如果注册和安装成功, Service Worker 激活,并且其状态变为 ‘activating’ ,你可以在这里执行清理旧版本资源的操作:
self.addEventListener('activate', event => {const cacheWhitelist = ['缓存版本ID'];event.waitUntil(caches.keys().then(cacheNames => {return Promise.all(cacheNames.map(cacheName => {if (cacheWhitelist.indexOf(cacheName) === -1) {return caches.delete(cacheName);}}));}));
});
- Service Worker-捕获 Fetch
通过监听Service Worker的 fetch 事件来拦截网络请求,
调用 event 上的 respondWith() 方法来劫持当前servicework控制域下的 HTTP 请求,该方法会直接返回一个Promise 结果 ,这个结果就会是http请求的响应。上面代码中就一个简单的逻辑,先劫持http请求,然后看看缓存中是否有这个请求的资源,如果有则直接返回,如果没有就去请求服务器上的资源。 event.respondWith 方法只能在 Service Worker 的 fetch 事件中使用。
self.addEventListener('fetch', event => {event.respondWith(caches.match(event.request).then(response => {if (response) {console.log('Serving from cache:', event.request.url);return response;}console.log('Fetching from network:', event.request.url);return fetch(event.request).then(networkResponse => {if (networkResponse && networkResponse.ok) {console.log('Caching new response:', event.request.url);return caches.open('f69905188ac970f1').then(cache => {cache.put(event.request, networkResponse.clone());return networkResponse;});}throw new Error('Network response not ok');}).catch(error => {console.error('Fetch failed:', error);throw error;});}));
});
- 开始:Service Worker监听到fetch事件。
- 缓存中是否存在请求资源:检查缓存中是否有匹配的请求资源。
- 从缓存返回资源:如果缓存中有匹配资源,直接返回该资源。
- 发起网络请求:如果缓存中没有匹配资源,则发起网络请求。
- 网络请求是否成功:检查网络请求是否成功。
- 响应状态是否为OK:检查网络响应的状态码是否为200(OK)。
- 缓存新响应:如果网络请求成功且响应状态为OK,则将响应缓存。
- 抛出错误:如果响应状态不是OK,则抛出错误。
- 捕获错误并抛出:如果网络请求失败,则捕获错误并抛出。
- 结束:流程结束。
Service Worker资源缓存-插件自动生成
通过上述资料可知资料缓存需要配置缓存ID和所需要的缓存文件路径,而每次打包的文件名都是混淆之后的,人工写是非常不实际,所以我们可以通过插件帮我们自动生成Service Worker文件自动插入到dist目录下
配置插件进行自动化生产
在vite项目中,根据Rollup接口提供的writeBundle()钩子函数拿到构建后的文件列表,自动生成service-worker.js
import path from 'path'
import * as fs from 'fs'
import * as crypto from 'crypto'// 定义插件选项类型
interface ManifestPluginOptions {outputPath: stringversion?: stringserviceWorkerFileName?: string
}export default function ServiceWorkerManifestPlugin(options: ManifestPluginOptions) {const { outputPath, version, serviceWorkerFileName } = options// 生成随机版本号const generateRandomVersion = (): string => {return crypto.randomBytes(8).toString('hex')}const manifestVersion = version || generateRandomVersion()// 使用默认的 service-worker.js 文件名,如果没有传入自定义文件名const serviceWorkerPath = `/${serviceWorkerFileName || 'service-worker.js'}`// 递归遍历目录并获取所有文件路径const getAllFiles = (dirPath: string, relativePath: string = ''): string[] => {let files: string[] = []const entries = fs.readdirSync(dirPath, { withFileTypes: true })for (const entry of entries) {const fullPath = path.join(dirPath, entry.name)const relativeFullPath = path.join(relativePath, entry.name)if (entry.isDirectory()) {files = files.concat(getAllFiles(fullPath, relativeFullPath))} else {files.push(`/${relativeFullPath}`)}}return files}// 生成 service-worker.js 文件内容const generateServiceWorkerContent = (cachedFiles: string[], manifestVersion: string): string => {return `
self.addEventListener('install', event => {event.waitUntil(caches.open('${manifestVersion}').then(cache => {return cache.addAll([${cachedFiles.map((file) => `'${file}'`).join(',\n')}]);}));
});//Service Worker监听到fetch事件。
self.addEventListener('fetch', event => {event.respondWith(// 缓存中是否存在请求资源:检查缓存中是否有匹配的请求资源。caches.match(event.request).then(response => {// 从缓存返回资源:如果缓存中有匹配资源,直接返回该资源。if (response) {console.log('Serving from cache:', event.request.url);return response;}console.log('Fetching from network:', event.request.url);// 发起网络请求:如果缓存中没有匹配资源,则发起网络请求。return fetch(event.request).then(networkResponse => {// 缓存新资源:如果网络请求成功,则将新资源缓存。if (networkResponse && networkResponse.ok) {console.log('Caching new response:', event.request.url);// 缓存新响应:如果网络请求成功且响应状态为OK,则将响应缓存。return caches.open('${manifestVersion}').then(cache => {cache.put(event.request, networkResponse.clone());return networkResponse;});}// 如果响应状态不是OK,则抛出错误。throw new Error('Network response not ok');}).catch(error => {console.error('Fetch failed:', error);throw error;});}));
});self.addEventListener('activate', event => {const cacheWhitelist = ['${manifestVersion}'];event.waitUntil(caches.keys().then(cacheNames => {return Promise.all(cacheNames.map(cacheName => {if (cacheWhitelist.indexOf(cacheName) === -1) {return caches.delete(cacheName);}}));}));
});
`}// 修改 index.html 文件const modifyIndexHtml = (indexPath: string, serviceWorkerPath: string): void => {try {let indexContent = fs.readFileSync(indexPath, 'utf-8')// 确保 <html> 标签存在if (indexContent.includes('<html')) {// 添加 Service Worker 注册脚本const serviceWorkerRegistrationScript = `
<script>if ('serviceWorker' in navigator) {// 浏览器支持 Service Workerwindow.addEventListener('load', () => {navigator.serviceWorker.register('${serviceWorkerPath}').then(registration => {console.log('Service Worker registered with scope:', registration.scope);}).catch(error => {console.error('Service Worker registration failed:', error);});});} else {console.log('Service Worker not supported');}
</script>
`// 将脚本插入到 </head> 标签之前indexContent = indexContent.replace('</head>', `${serviceWorkerRegistrationScript}</head>`)fs.writeFileSync(indexPath, indexContent)console.log('index.html modified successfully.')} else {console.warn('index.html does not contain a <html> tag.')}} catch (error) {console.error('Failed to modify index.html:', error)}}return {name: 'manifest-plugin', // 必须的,将会在 warning 和 error 中显示writeBundle() {try {const cachedFiles = getAllFiles(outputPath)// 确保 service-worker.js 也被缓存if (!cachedFiles.includes(serviceWorkerPath)) {cachedFiles.push(serviceWorkerPath)}// 生成 service-worker.js 文件内容const serviceWorkerContent = generateServiceWorkerContent(cachedFiles, manifestVersion)// 写入 service-worker.js 文件const serviceWorkerFilePath = path.join(outputPath, serviceWorkerPath.replace(/^\//, ''))fs.writeFileSync(serviceWorkerFilePath, serviceWorkerContent)console.log('service-worker.js generated successfully.')// 修改 index.html 文件const indexPath = path.join(outputPath, 'index.html')if (fs.existsSync(indexPath)) {modifyIndexHtml(indexPath, serviceWorkerPath)} else {console.warn('index.html not found in the output directory.')}} catch (error) {console.error('Failed to write bundle:', error)}},}
}
Service Worker调试与监控
使用Chrome DevTools
- 查看缓存:在“Application”面板中查看当前注册的Service Workers及其缓存内容。
- 模拟离线:通过DevTools的“Network”面板模拟不同的网络状况,测试应用的离线表现。
- 日志记录:利用console.log()配合DevTools的日志功能追踪Service Worker内部发生的事件及执行过程。
通过Chrome DevTools可看到我们的文件被正确的缓存,且通过Application工具管理我们的Service Workers
相关文章:

Service Work离线体验与性能优化
Service Work离线体验与性能优化 引言 先放个意外事件,万事开头难🤣🤣🤣 原计划是分享离线应用与数据资源缓存的应用实践,结果发现这一技术已被web标准废弃 曾经做过一个PC应用,业务需求要求应用具备容灾…...

Unity 语音转文字 Vosk 离线库
市场有很多语音库,这里介绍Vosk SDK 除了支持untiy外还有原生开发服务器等 目录 安装unity示例demo下载语音训练文件运行demo结尾一键三联 注意事项 有可能debug出来的文本是空的,(确保麦克风正常,且索引正确)分大…...

VSCode连接Github的重重困难及解决方案!
一、背景: 我首先在github创建了一个新的项目,并自动创建了readme文件其次在vscode创建项目并写了两个文件在我想将vscode的项目上传到对应的github上时,错误出现了 二、报错及解决方案: 1.解决方案: 需要在git上配置用…...
《AI赋能鸿蒙Next,打造极致沉浸感游戏》
在游戏开发领域,鸿蒙Next系统与人工智能技术的结合为开发者们带来了前所未有的机遇,使打造更具沉浸感的游戏成为可能。以下将深入探讨如何利用人工智能在鸿蒙Next上开发出令人身临其境的游戏。 利用AI优化游戏角色智能行为 在传统游戏中,非…...
小白:react antd 搭建框架关于 RangePicker DatePicker 时间组件使用记录 2
文章目录 一、 关于 RangePicker 组件返回的moment 方法示例 一、 关于 RangePicker 组件返回的moment 方法示例 moment方法中日后开发有用的方法如下: form.getFieldsValue().date[0].weeksInWeekYear(),form.getFieldsValue().date[0].zoneName(), form.getFiel…...
<C++学习>C++ std 多线程教程
C std 多线程教程 理解多线程的概念 多线程是一种并发编程技术,它允许程序同时运行多个任务。每个线程共享同一进程的资源(如内存),但拥有独立的执行路径。多线程编程在现代 C 中变得更加便捷和安全,标准库提供了强大…...

用 Python 自动化处理日常任务
💖 欢迎来到我的博客! 非常高兴能在这里与您相遇。在这里,您不仅能获得有趣的技术分享,还能感受到轻松愉快的氛围。无论您是编程新手,还是资深开发者,都能在这里找到属于您的知识宝藏,学习和成长…...
《深入浅出HTTPS》读书笔记(28):DSA数字签名
《深入浅出HTTPS》读书笔记(28):DSA数字签名 对称加密算法有很多算法,标准算法是RSA机密算法,数字签名技术也有一个标准DSS(Digital Signature Standard),其标准…...
type 属性的用途和实现方式(图标,表单,数据可视化,自定义组件)
1.图标类型 <uni-icon>组件中,type可以用来指定图标的不同样式。 <uni-icons type"circle" size"30" color"#007aff"></uni-icons> //表示圆形 <uni-icons type"square" size"30" co…...

PSINS工具箱学习(四)捷联惯导更新算法
原始 Markdown文档、Visio流程图、XMind思维导图见:https://github.com/LiZhengXiao99/Navigation-Learning 文章目录 一、捷联惯导更新1、insinit():初始化 ins 结构体2、ethupdate():地球自转角速度和牵连角速度更新3、insupdate():捷联惯导更新1. 速度更新2. 位置更新3.…...

P1Linux和Docker常用终端命令:保姆级图文详解
文章目录 前言1、Docker 常用命令1.1、镜像管理1.2、容器管理1.3、网络管理1.4、数据卷管理1.5、监控和性能管理 2、Linux 常用命令分类2.1、文件和目录管理2.2、用户管理2.3、系统监控和性能2.4、软件包管理2.5、网络管理 前言 亲爱的家人们,创作很不容易…...

Windows重装后NI板卡LabVIEW恢复正常
在重新安装Windows系统后,NI(National Instruments)板卡能够恢复正常工作,通常是由于操作系统的重新配置解决了之前存在的硬件驱动、兼容性或配置问题。操作系统重装后,系统重新加载驱动程序、清理了潜在的冲突或损坏的…...
深度解析统计学四大分布:Z、卡方、t 与 F 的关联与应用
统计学关键分布:Z、卡方、t、F 的关系探秘与应用指南 A/B实验系列相关文章(置顶) 1. A/B实验之置信检验(一):如何避免误判和漏报 2. A/B实验之置信检验(二):置信检验精要…...
zkServer.sh脚本
Apache ZooKeeper 几种常见的方法: 一、使用 zkServer.sh 脚本: 最常见的启动 ZooKeeper 的方式是使用提供的 zkServer.sh 脚本。此脚本可用于管理 ZooKeeper 进程。以下是一些示例命令: 1. 在前台启动 ZooKeeper: ./zkServer.s…...

CV(10)--目标检测
前言 仅记录学习过程,有问题欢迎讨论 目标检测 object detection,就是在给定的图片中精确找到物体所在位置,并标注出物体的类别;输出的是分类类别label物体的外框(x, y, width, height)。 目标检测算法:…...

UML系列之Rational Rose笔记七:状态图
一、新建状态图 依旧是新建statechart diagram; 二、工作台介绍 接着就是一个状态的开始:开始黑点依旧可以从左边进行拖动放置: 这就是状态的开始,和活动图泳道图是一样的;只能有一个开始,但是可以有多个…...
C++单例模式的设计
单例模式(Singleton Pattern)是一种设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来访问该实例。在C中,单例模式通常用于管理全局资源或共享状态。 以下是C中实现单例模式的几种常见方式: 懒…...

基于springboot的自习室预订系统
作者:学姐 开发技术:SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等 文末获取“源码数据库万字文档PPT”,支持远程部署调试、运行安装。 项目包含: 完整源码数据库功能演示视频万字文档PPT 项目编码࿱…...

shell笔记
1.使用 ls -l 及 find 查找某个或者多个目录文件数量时 单个目录: find 目录 -type f|wc -l与 ls -l 目录|grep -v total|wc -l 一致 多个目录:如上结果不一致,因为 ls -l 在算多目录时,会将多目录及空格打出算作额外行 find 更精…...
《鸿蒙Next微内核:解锁人工智能决策树并行计算的加速密码》
在当今人工智能飞速发展的时代,提升运算速度是推动其进步的关键。鸿蒙Next以其独特的微内核特性,为设计决策树的并行计算框架提供了新的思路和契机。 鸿蒙Next微内核特性概述 鸿蒙Next的微内核架构将核心功能模块化,仅保留进程管理、内存管…...
Leetcode 3576. Transform Array to All Equal Elements
Leetcode 3576. Transform Array to All Equal Elements 1. 解题思路2. 代码实现 题目链接:3576. Transform Array to All Equal Elements 1. 解题思路 这一题思路上就是分别考察一下是否能将其转化为全1或者全-1数组即可。 至于每一种情况是否可以达到…...
MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例
一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

边缘计算医疗风险自查APP开发方案
核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...

剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...

如何在网页里填写 PDF 表格?
有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据ÿ…...
Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析
Java求职者面试指南:Spring、Spring Boot、MyBatis框架与计算机基础问题解析 一、第一轮提问(基础概念问题) 1. 请解释Spring框架的核心容器是什么?它在Spring中起到什么作用? Spring框架的核心容器是IoC容器&#…...

Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...

20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...