6. Web API
6. Web API
- 早期浏览器就提供了的**BOM(Browser Object Model)**对象集合,用于操作浏览器窗口、历史记录、导航、弹窗、屏幕信息等浏览器环境,
BOM主要对象包括window:浏览器窗口的全局对象navigator:浏览器信息与权限screen:屏幕信息location:URL 跳转与刷新history:浏览器当前页面历史栈alert/confirm/prompt:弹窗setTimeout/setInterval:定时器
- 除了这些之外,现代浏览器还提供了不少标准接口工具,主要用于操作浏览器能力、网络、设备和多媒体等
- 网络:
fetch、WebSocket,见异步请求 - 存储:
localStorage、IndexedDB - 多媒体:
AudioContext - 权限/设备:
Notification、Geolocation API、摄像头麦克风 - 浏览器脚本:
Service Worker - 文件处理:
FileReader、Blob、File - 其他:全屏、剪贴板、动画渲染
requestAnimationFrame等
- 网络:
6.1. window
window是浏览器窗口的全局对象,并且所有全局的对象都是window对象下的属性window对象提供了和显示器窗口有关的属性devicePixelRatio:设备像素比,用于判断设备是否为高清设备,一般大于1为高清设备,safari浏览器不支持此属性screen:屏幕信息对象,包含以下属性availableHeight:可用高度,单位为像素,移除了比如windows系统任务栏这样的永久或半永久占用的界面空间availableWidth:可用宽度,单位为像素height:屏幕高度,单位为像素width:屏幕宽度,单位为像素colorDepth:颜色深度,单位为位,比如24位,32位,64位等等,出于隐私考虑,另一个pixelDepth属性和这个属性的值应该相同orientation:屏幕方向,返回一个枚举值,有portrait-primary、portrait-secondary、landscape-primary、landscape-secondary,分别表示竖屏正方向、竖屏反方向、横屏正方向、横屏反方向
screenX/screenY:返回浏览器窗口距离屏幕左侧和屏幕顶部的距离,单位为像素。别名为screenLeft/screenTopmoveTo(x, y)/moveBy(x, y):移动浏览器窗口,参数为x和y坐标,单位为像素x是相对定点的窗口水平移动的像素数。正值向右移动,负值向左移动y是相对定点的窗口垂直移动的像素数。正值向下移动,负值向上移动moveTo(x, y)和moveBy(x, y)函数的区别在于moveTo是相对屏幕左上角的位置进行移动,而moveBy是相对当前窗口位置进行移动
open(url, target, features):打开新窗口,返回值是新窗口的window对象(仅同源时可获取到)url:要打开的页面的URL地址target:加载到的浏览器上下文的名称,默认为_blank_self:加载到当前窗口_blank:加载到新窗口- 其他的,可用于
a标签的target属性的值
features窗口的属性,多个属性之间用逗号隔开,值是以键值对的形式存储的字符串popup:窗口是否显示为精简的弹出窗口,功能由浏览器决定,可能没有地址栏等内容,默认值为false。由于历史原因,在未指定这个值且location/menubar/sesizable/scrollbars/status等属性(已弃用)为false或不存在时,也会显示为精简的弹出窗口width/height:内容区域的宽高,有别名innerWidth/innerHeightleft/top:窗口左上角在操作系统工作区的初始位置,有别名screenX/screenY,如果初始位置在工作区外,会进行修正- 早期
opener可以带来tabnabbing攻击,通过获取opener修改页面location为钓鱼网站 - 现在的浏览器很多已经默认不允许获取
opener了
- 早期
noopener:如果启用此功能,新窗口将无法通过Window.opener访问原始窗口的window对象noreferrer:如果启用此功能,浏览器将省略*[rel]的ref属性ref属性使新页面通过获取原本的referer能够获取到地址中的token敏感等信息- 现在浏览器存在
Referrer-policy策略,非同源只发域名origin地址;存在协议变化https->http不发地址
close():关闭当前窗口
6.1.1. selection API
selection对象是通过window.getSelection()获取到的对象,可以获取当前选中的文本内容,以及选中文本的开始位置和结束位置- 选取区间的概念介绍
- 锚点:选区起始位置
- 焦点:选区结尾位置
- 输入光标:当选区起始位置和结尾位置相同时(直接点击也会产生光标),对于可输入的元素会显示输入光标,其他元素也会产生隐藏的光标
- 选区会使用被选择的节点的共同父节点来描述一个范围,即父节点和一个基于父节点的偏移
- 关于偏移
offset:如果是文本节点,偏移是的文本偏移字数,如果是元素节点,是相对于childNodes数组的偏移索引
- 实例属性
anchorNode:当前页面选中的起始DOM节点,如果没有选中任何文本,则返回nullfocusNode:当前页面选中的结束DOM节点,如果没有选中任何文本,则返回nullanchorOffset:选区锚点在anchorNode中的字符位置,如果没有选中任何文本,则返回0focusOffset:结束焦点在focusNode中的字符位置,如果没有选中任何文本,则返回0isCollapsed:选区是否为折叠状态,起点和终点相同,即没有选中任何文本rangeCount:选区数量,目前仅Firefox支持按住ctrl选择多个片段type:选区类型,有未选择None,选择了一个范围Range和折叠Caret三种direction:选区文本方向,可能是none、backward和forward三种
- 实例方法
addRange(range):添加一个选择区间,选择区间即之后介绍的Range对象removeRange(range):删除一个选择区间removeAllRanges():删除所有选择区间,别名是empty()collapse(node, offset):将区间压缩到一个点node:插入位置所在的DOM节点,如果为null,则相当于清除所有选区offset:插入位置相对节点开头的偏移位置,默认是0
collapseToStart():将选区折叠到起始位置,如果选区内容聚焦且可编辑,插入号会闪烁collapseToEnd():将选区折叠到结束位置,如果选区内容聚焦且可编辑,插入号会闪烁containsNode(node[, partialContainment]):判断选区是否包含某个节点,第二个参数是指在部分选择时是否算包含节点,默认是false也就是部分选择不算被包含deleteFromDocument():删除选区文本或节点内容,实际上是调用选择区间的Range.deleteContents()方法extend(node[, offset]):将选区焦点移动到指定节点的某个偏移位置,offset参数可选,默认是0getRangeAt(index):返回指定索引的选区区间,索引从0开始,因为大多数浏览器不支持多选取,一般索引为0,此方法不支持影子根节点,返回Range对象getComposedRange(option):一个新支持的方法,可以获取到选区中的影子节点,返回StaticRange对象option的选项shadowRoots:列出希望能被返回的影子节点所在的影子根节点列表
modify(alter, direction, granularity):修改光标位置alter:修改方式,有move、extend两种,表示移动光标(选取重合,出现单个光标)和扩展选区direction:移动方向,有向后forward(right)、向前backward(left)四种granularity:一次移动的粒度,具体有- 一次移动单个字符
character - 移动到下一个词(空格或标点分隔)边缘
word - 移动到下一个句子(以标点结尾)边缘
sentence - 移动当前段落(通常是以换行分隔)的字符数
paragraph - 移动一个视觉显示文本行的字符数
line - 移动到当前行边界
lineboundary - 移动到当前句子边界
sentenceboundary,似乎和sentence一样 - 移动到当前段落边界
paragraphboundary - 移动到页面第一个可选中字符前后
documentboundary
- 一次移动单个字符
Firefox不支持sentence、paragraph、sentenceboundary、paragraphboundary、documentboundary
selectAllChildren(node):选择指定节点的所有子节点,之前的选择会被清除setBaseAndExtent(anchorNode, anchorOffset, focusNode, focusOffset):选择或部分选择两个节点和之间的所有内容anchorNode:选区起始节点anchorOffset:选区起始节点的偏移位置focusNode:选区结束节点focusOffset:选区结束节点的偏移位置
toString():返回选区文本内容,相当于getSelection().toString()
- 区间表示对象
AbstractRange对象:选取范围的抽象对象,StaticRange对象和Range对象都直接继承自AbstractRange,包含以下实例属性collapsed:表示选区是否为折叠状态,起点和终点相同,即没有选中任何文本startContainer:选区起始节点startOffset:选区起始节点的偏移位置,如果是文本节点是指字符偏移,如果是元素节点是指子节点childNodes的索引endContainer:选区结束节点endOffset:选区结束节点的偏移位置commonAncestorContainer:选区所有节点的公共父节点
StaticRange对象:内容不会随着DOM树内部的变化而更新,未扩展新属性Range对象:表示文档的片段,可以包含节点和文本节点的部分,添加了许多有用的方法cloneContents():将选择的内容克隆,放入一个新的DocumentFragment对象中,返回此对象- 如果节点元素被部分选择,这种情况下被克隆的只是被选中的内容
- 使用的方式和
Node.cloneNode()相同,这意味着不会克隆事件侦听
cloneRange():通过值复制拷贝一个新的Range对象,二者变更不会相互影响deleteContents():删除被选中的内容extractContents():将选择内容提取出来,返回一个DocumentFragment对象,此对象包含被选内容,但原选内容被删除,类似cloneContents()和extractContents()的组合getBoundingClientRect():返回一个矩形对象,包含选区边界的坐标信息,如果需要所有的矩形对象,可使用getClientRects()方法insertNode(node):在起始节点插入新节点intersectsNode(node):判断节点是否被选区部分包含isPointInRange(node, offset):判断指定节点的偏移位置是否被选区包含selectNode(node):选择指定节点,容纳其中所有内容selectNodeContents(node):选择节点内的所有内容,相比selectNode(node),范围边界的描述是按此节点进行的(startContainer和endContainer是这个节点,而不是父节点)setStart(node, offset):设置选区起始位置,如果起点设置在终点之后,则会导致范围折叠,都设置在指定位置,offset无默认值不可省略setStartAfter(node)和setStartBefore(node):设置选区起始位置,设置在节点之后或之前setEnd(node, offset):设置选区结束位置,如果终点设置在起点之前,则会导致范围折叠,都设置在指定位置setEndAfter(node)和setEndBefore(node):设置选区结束位置,设置在节点之后或之前compareBoundaryPoints(how, otherRange):比较当前对象的边界点和另一个Range对象的边界点关系,返回一个数字,表示边界点关系how:比较方式,有START_TO_START、START_TO_END、END_TO_END、END_TO_START四种,第一个start或end指定的是otherRange的哪个端点,第二个指定的是当前Range的哪个端点- 返回值为
-1表示当前节点的边界点在前(左),1表示当前节点的边界点在后(右),0表示两个边界点位置相同
collapse(toStart):折叠选区,toStart参数可选,默认是false,表示折叠到结束位置comparePoint(node, offset):比较指定节点的偏移位置和当前选区位置关系,返回一个数字- 指定位置在区间起点位置之前,返回
-1 - 指定位置在区间终点位置之后,返回
1 - 指定位置在区间内,返回
0
- 指定位置在区间起点位置之前,返回
surroundContents(newParent):用指定节点包含选定内容,如果区间部分包含非文本节点会抛出异常newParent:应该用来包裹内容的节点,函数提取内容并用区间内容替换newParent的子节点,并在当前区间插入,最后让区间选择这个新节点
createContextualFragment(input):将TrustedHTML对象实例或字符串解析为DocumentFragment对象,并返回此对象,有xss风险- 出于安全考量,可以设置名称
require-trusted-types-for的CSP指令,要求输入一个TrustedHTML对象
- 出于安全考量,可以设置名称
- 选区指南
获取选择内容:如果需要获取到有哪些文本被选择,因为选区的描述是基于父节点的,无法直接了解选择的区间,不过可以使用以下方法直接获取选中内容
window.getSelection().toString():获取选中内容文本range.cloneContents():获取一个拷贝的选区片段,可获取到节点
修改选区
selection.addRange(range):添加一个选区range对象selection.removeAllRanges():删除所有选区selection.selectAllChildren(node):选择指定节点的所有子节点range.setStart(node, offset)等:设置选区开始结束位置selection.modify(alter, direction, granularity):根据方向、指令和粒度修改扩展选区selection.collapse(node, offset)、selection.collapseToStart()、selection.collapseToEnd():折叠选区
判断节点是否在选区中
range.intersectsNode(node):判断节点是否被选区部分包含range.isPointInRange(node, offset):判断指定节点的偏移位置是否被选区包含range.comparePoint(node, offset):比较指定节点的偏移位置和当前选区位置关系,返回一个数字
获取文本选区对应的节点和位置:
- 已知需要选中的文本内容字数索引,返回这个区间的决定字段
[startContainer, startOffset, endContainer, endOffset] - 已知``
function getNodeAndOffset(wrap_dom, start=0, end=0){ const txtList = []; const map = function(children){ [...children].forEach(el => { if (el.nodeName === '#text') { txtList.push(el) } else { map(el.childNodes) } }) } // 递归遍历,提取出所有 #text map(wrap_dom.childNodes); // 计算文本的位置区间 [0,3]、[3, 8]、[8,10] const clips = txtList.reduce((arr,item,index)=>{ const end = item.textContent.length + (arr[index-1]?arr[index-1][2]:0) arr.push([item, end - item.textContent.length, end]) return arr },[]) // 查找满足条件的范围区间 const startNode = clips.find(el => start >= el[1] && start < el[2]); const endNode = clips.find(el => end >= el[1] && end < el[2]); return [startNode[0], start - startNode[1], endNode[0], end - endNode[1]] } function getRangeOffset(wrap_dom){ const txtList = []; const map = function(children){ [...children].forEach(el => { if (el.nodeName === '#text') { txtList.push(el) } else { map(el.childNodes) } }) } // 递归遍历,提取出所有 #text map(wrap_dom.childNodes); // 计算文本的位置区间 [0,3]、[3, 8]、[8,10] const clips = txtList.reduce((arr,item,index)=>{ const end = item.textContent.length + (arr[index-1]?arr[index-1][2]:0) arr.push([item, end - item.textContent.length, end]) return arr },[]) const range = window.getSelection().getRangeAt(0); // 匹配选区与区间的#text,计算出整体偏移量 const startOffset = (clips.find(el => range.startContainer === el[0]))[1] + range.startOffset; const endOffset = (clips.find(el => range.endContainer === el[0]))[1] + range.endOffset; return [startOffset, endOffset] }- 已知需要选中的文本内容字数索引,返回这个区间的决定字段
6.2. navigator
6.2.1. geolocation
geolocation对象是navigator对象中的一个属性,主要用于获取当前位置信息- 此属性需要在
https协议下,才能使用 - 如果是作为嵌入子页面,需要修改请求头,授予特定的第三方来源的权限
Permission-Policy: geolocation=(self suborigin),默认值为self。此外,需要为iframe添加属性allow="geolocation" - 首次获取操作时,会要求用户通过浏览器提示确认授予权限
- 此属性需要在
geolocation相关的变量类型接口GeolocationCoordinates对象,用于表示设备在地球上的位置和海拔高度,以及这些属性的计算精度,所有属性都是double浮点数,如果设备无法提供,值为null- 纬度
latitude - 经度
longitude - 高度
altitude:单位为米 - 精度
accuracy:单位为米 - 高度精度
altitudeAccuracy:单位为米 - 移动速度
speed:单位是米/秒 - 设备朝向
heading:0表示正北方,90表示正东,270表示正西,即数值与正北方的夹角大小有关,按顺时针方向增加
- 纬度
GeolocationPosition对象,示相关设备在给定时间时的位置coords:当前位置的经纬度信息,是GeolocationCoordinates对象timestamp:位置的获取时间,以毫秒为单位的时间戳
GeolocationPositionError对象,用于表示获取位置信息失败的原因code:错误代码,有3种错误代码,1代表被阻止PERMISSION_DENIED,2代表因为错误获取失败POSITION_UNAVAILABLE,3代表获取超时TIMEOUTmessage:用于调试的详细错误信息,规范指出此内容不应该直接显示在用户界面中
geolocation对象的实例方法
getCurrentPosition(success, error, options):异步获取当前位置信息,成功时调用success函数,失败时调用error函数success回调函数唯一参数为GeolocationPosition对象error回调函数唯一参数为GeolocationPositionError对象options:可选参数,用于指定获取位置信息的选项,包括timeout:表示设备返回位置信息所需的最长时间(毫秒),默认值为Infinity,即未获取到信息不返回maximumAge:可接受的缓存时间,默认是0,即不使用缓存位置enableHighAccuracy:是否使用高精度获取位置信息,默认为false,高精度可能导致响应速度变慢和功耗增加
watchPosition(success, error, options):监听设备位置信息变化,每次变化时调用success函数,出现错误时调用error函数,参数介绍和getCurrentPosition一致,函数会返回一个监听IDclearWatch(watchId):取消监听位置信息变化,参数为watchPosition返回的监听ID
6.2.2. clipboard
clipboard对象是navigator对象中的一个属性,用于操作剪贴板,主要方法属性有writeText(text):将文本写入剪贴板readText():从剪贴板读取最新文本,promise异步write(items):将ClipboardItem列表写入剪贴板,可以是非文本数据,比如图片read():从剪贴板读取ClipboardItem列表(剪切板中可能有多种类型的数据,不是包含过往剪切板历史的列表),promise异步,可以是非文本数据type:剪贴板数据类型
对于这些功能,
Firefox于2024年完善支持,之前仅支持写入文本数据
在https协议下,才能使用clipboard对象访问和写入剪切板的内容,且需要用户同意
ClipboardItem是一个可以进行多种类型的剪切板数据操作的新增的对象type:数据类型列表,也就是支持向哪些数据类型转换的列表presentationStyle:剪贴板数据展示样式,值是未指定unspecified、内联式inline或附件attachment,目前支持有限,chrome还不支持- 实例方法
getType(type):返回一个Promise,请求对应类型blob,如果类型未找到则返回错误 - 静态方法
ClipboardItem.supports(type):返回一个Promise,判断当前浏览器是否支持对应类型数据
- 构造方法
new ClipboardItem(data[, option]):构造一个ClipboardItem对象,用来写入剪贴板data:数据对象,可以是字符串或Blob对象,也可以是一个可解析为Blob或字符串的Promise对象option:可选参数presentationStyle:剪贴板数据展示样式
6.3. location
location对象是window对象中的一个属性,用于操作当前页面地址栏的URLlocation对象主要属性有href:当前页面的完整URLhash:当前页的URL的#后锚点部分host/hostname:当前页的URL的主机名/域名部分pathname:当前页的URL的第一个/后路径部分search:当前页的URL的?后参数部分protocol:当前页的URL的协议部分
修改这些属性会改变当前页的
URL,修改除hash外的属性会自动触发页面刷新;修改hash属性会触发hashchange事件- 方法
assign(url):跳转到指定页面replace(url):替换当前页,并跳转到指定页面,不保留历史记录reload():重新加载当前页
6.4. history
history对象是window对象中的一个属性,用于操作浏览器历史记录由于安全的保护,
history对象是一个只能向前看的历史记录栈,从js中无法读取到完整的历史,只能操作堆栈前进后退,核心方法包括history.back():返回上一页history.forward():前进一页history.go(n):跳转到指定页面,相当于前进或后退n步(正数前进、负数后退)
history.back(); history.forward(); history.go(-1); // 返回上一页 history.go(1); // 前进一页Html5开始,单页面应用成为主流,因此添加了新的方法,允许修改历史堆栈后不触发页面刷新history.pushState(state, title, url):添加历史堆栈state:状态对象,用于保存当前页面的状态信息,通常为对象,需要可序列化为jsontitle:标题,通常为空,目前大多数浏览器忽略此参数url:跳转的URL
history.replaceState(state, title, url):替换当前历史堆栈history.state:当前历史堆栈的状态对象
history.pushState({ Login: false }, '', '/page2'); history.replaceState({ Login: true }, '', '/page1');无刷新路由跳转的局限性
浏览器会记录下这个堆栈的修改,一切看起来都很完美,但实际使用中,可能会有如下问题
- 无刷新意味着不会触发
load事件,通过popstate事件可以处理用户的跳转,但处理不了使用pushState和replaceState进行的跳转 - 每次修改会替换子路由,因此每次都需要完整地写入子路由,比如
/path/page1 - 如果通过跳转后的地址进行页面恢复,会导致请求的内容不存在,得到
404,需要后端进行配置,确保这些路径请求能得到页面 - 不能跨域修改堆栈,不然会报错
- 早期浏览器(
IE9及以下)不支持
6.5. 弹窗
6.6. 定时器
setTimeout(fn, delay):延迟执行- 参数包括一个函数和一个延迟时间(
ms) - 返回一个定时器
ID,可以通过clearTimeout(id)取消
const id = setTimeout(() => console.log('Hello'), 1000); clearTimeout(id); // 取消- 参数包括一个函数和一个延迟时间(
setInterval(fn, delay):循环执行- 参数包括一个函数和一个循环间隔时间(
ms) - 返回一个循环器
ID,可以通过clearInterval(id)取消
const id = setInterval(() => console.log('tick'), 1000); clearInterval(id); // 停止周期执行- 参数包括一个函数和一个循环间隔时间(
注意事项
- 定时器不精确:受到浏览器线程调度和系统性能影响
- 最小延迟限制:现代浏览器中有最小延迟限制,通常为
4ms - 页面关闭
beforeunload时需要清除定时器:定时器会继续执行,直到浏览器关闭,造成内存泄漏 setInterval不应该作为长时间精确循环使用,每次执行之后误差都会扩大,应该使用setTimeout嵌套函数
function loop() { console.log('loop'); // 此方法虽然和直接递归很像,但是有区别 // 直接递归会保留当前函数堆栈,最终函数循环调用导致堆栈溢出 // 使用延时会在之后将函数添加到宏任务队列,函数会正常结束 setTimeout(loop, 1000); }
6.7. cookie
cookie是一种存储在浏览器端的小数据文件,最多只有4KB,用于保存用户数据,数据保存在浏览器中,可以设置过期时间创建和读取
- 创建和在
setCookie头中写的内容一致,但不能创建httpOnly的cookie - 读取到的是字符串,需要经过转换获取值
// 设置 document.cookie = "username=Tom; expires=Fri, 31 Dec 2025 23:59:59 GMT; path=/"; // 读取 console.log(document.cookie); // "username=Tom; age=20"- 创建和在
前端使用
cookie的情况相对较少,主要因为获取不便,但也可以使用相关库,比如js-cookie处理- 一般
cookie用于存储服务器凭证等信息,这些信息都设置了HttpOnly,不需要前端处理,前端也无法设置这样的cookie - 获取不如
Storage方便,因此一般需要前端处理的内容应该考虑使用storage存储更方便
- 一般
6.8. Storage
Storage是浏览器存储API的一个接口,用于进行简单的同步的键值对字符串数据存储,主要有两个实现localStorage:存储同源标签之间共享的数据。数据持久保存,即使关闭并重新打开浏览器也会持续存在,直到主动删除sessionStorage:存储给定页面的临时数据。数据会话保存,关闭页面后数据会丢失(刷新不会)
存储的内容
浏览器端的存储(
localStorage/sessionStorage/indexedDB)是完全不可信的环境- 它适合存放“状态类”或“无安全价值”的数据,而绝不能存储或依赖其中的任何安全关键数据(比如登录状态、验证码等),因为前端完全可以获取到这些数据
- 存在大小限制,
localStorage和sessionStorage存储的数据不能超过5MB,更大的数据需要使用indexedDB存储
Storage接口的实例方法,都是同步的setItem(key, value):设置数据- 存储时会发生隐式转换,存储的键和值数据会以字符串形式保存
- 超出容量会引起错误
QuotaExceededError,默认容量一般是5M - 建议使用命名空间前缀,避免和其他工具冲突,也能方便分类理解用处
getItem(key):获取数据- 不存在的数据返回
null
- 不存在的数据返回
removeItem(key):删除数据- 删除不存在的数据不会报错
clear():清空数据,包括某些库添加的值key(index):返回第index个存储键名,用于遍历数据,需要结合length- 插入顺序和索引顺序可能不一致,因为用户代理底层可能仅使用非顺序结构存储
- 如果值不存在,返回
null
length:数据数量
// 属性 Storage.length // 当前存储中键值对的数量 // 方法 Storage.key(0) Storage.setItem('key', 'value') Storage.getItem('key') Storage.removeItem('key') Storage.clear() window.localStorage instanceof Storage // true window.sessionStorage instanceof Storage // trueStorage接口在修改存储时会在其他页面的window上触发storage事件,这是StorageEvent事件实例key:改变的数据键名,如果数据使用clear被删除,此值为nulloldValue:改变前的数据,如果数据是新增的,此值为nullnewValue:改变后的数据,如果数据被删除,此值为nullstorageArea:受改变的数据存储区域,localStorage或sessionStorage的内容url:触发storage事件的页面地址
window.addEventListener('storage', (e) => { console.log(`${e.storageArea} changed:`, e); });sessionStorage仅在当前页面有效,因此不可能在其他页面触发storage事件,仅在本页面的iframe触发(iframe内部也会导致外部触发);localStorage会在同源的所有其他浏览上下文中触发storage事件,包括同源的其他标签页
6.9. IndexedDB
IndexedDB(简称IDB)是浏览器中内置的一个本地持久化数据库,适合存储结构化数据、文件、二进制数据等,比localStorage强大得多IndexedDB是一个异步的基于事务的键值数据库,异步是通过事件回调完成的所有存储、索引和游标操作都与事务关联执行
数据库是按源隔离的,每个源(协议、域名、端口)对应一个
IndexedDB数据库,实际上在存储的时候每个源各一个数据库子目录适合实现离线应用、缓存大量数据、或者用于存储需要前端搜索、索引、版本管理的复杂数据
容量存在上限,虽然没有严格的标准,且可能根据版本变化,通常不小于
250M,如果写入太多数据,可能会触发QuotaExceededError报错- 可以查看最多存储数据量
浏览器 大致上限 特点 Chrome/Edge尽力存储和持久存储模式都最多使用磁盘的 60%左右,每个源最多占全局的20%为了避免用户识别,可能无法达到上限 Firefox尽力存储模式最多使用磁盘 10%或10GB;持久存储最多使用大约磁盘的60%,上限为8TB同样可能不能达到配额 Safari每个源最多占用磁盘的 60%,整体不超过80%如果是非浏览器的 Web应用,最多占用磁盘的20%(每个源不超过15%),如果有相关浏览器缓存,使用和浏览器相同的空间(60%)其他引擎的或移动端的浏览器 一般更小(几十到数百 MB)依设备空间动态变化
使用dexie或idb可以优化
IndexedDB操作。原生语法略显复杂,如果希望能实现同步代码需要在事件中嵌套下一步处理逻辑,很容易写成回调地狱所有的数据操作都依赖对应的对象仓库对象,一个常见的操作流程如下
- 打开数据库,并创建对象存储仓库
- 启动事务并发出请求以获取对象仓库,执行某些数据库作,例如添加或检索数据
- 通过侦听正确类型的事件等待作完成
- 对结果执行某些操作
相关的授权和查看
api查看是尽力存储还是持久存储模式:尽力存储
best-effort和持久化存储persistent会影响浏览器的数据清理策略- 尽力存储
best-effort:这是默认情况下数据的存储方式。只要源低于其配额,设备有足够的存储空间,并且用户不选择通过浏览器的设置删除数据,尽力而为的数据就会持续存在 - 持久化存储
persistent:源可以选择以持久性方式存储其数据。只有在用户选择使用浏览器设置时,才会以这种方式存储的数据被逐出或删除。
// 检查浏览器是否支持持久化存储 if (navigator.storage && navigator.storage.persist) { const isPersisted = await navigator.storage.persisted(); console.log(`当前是否已持久化: ${isPersisted}`); if (!isPersisted) { const granted = await navigator.storage.persist(); console.log(granted ? "已获得持久化权限" : "用户或浏览器拒绝持久化"); } }- 尽力存储
可存储和已使用空间查看
- 使用
navigator.storage.estimate()异步查看当前数据库使用情况和限额,promise返回的对象包含usage和quota属性
- 使用
const quota = await navigator.storage.estimate(); console.log(quota.usage, quota.quota);
6.9.1. 数据库对象
数据库相关操作:
IndexedDB需要分数据库执行操作创建或打开数据库:使用
indexedDB.open(name, version)创建或打开数据库,其中需要提供数据库名称和版本号整数- 方法会返回一个数据库打开请求对象
IDBOpenDBRequest,继承了数据库请求对象IDBRequest,请求对象中有success、error事件处理请求状态,而数据库打开请求对象额外提供了upgradeneeded和blocked事件用于数据库升级 - 首先会检查是否有对应的数据库,版本号是多少
- 如果数据库不存在,则创建数据库,设置版本号(必须正整数)
- 如果数据库存在,比较版本号。如果版本号大于现有版本号,则升级数据库;如果版本号小于现有版本号,会报
VersionError错误
- 方法会返回一个数据库打开请求对象
数据库升级:每个数据库只有在版本号提升时才能修改数据库,创建索引和表
- 在升级时触发
upgradeneeded事件,在其中能修改数据库结构 - 数据库升级是异步的,一次只能升级一个版本,不会并发升级
- 升级时连接对象会依次经历
versionchange(旧连接中触发提示,在其中应该手动关闭连接)、blocked(有连接未关闭,升级被阻塞)、upradeneeded(数据库开始升级,如果版本号变化)、success(数据库升级完成)事件,如果出现问题,会触发error事件 - 在触发
upgradeneeded事件的回调中,得到的是IDBVersionChangeEvent,可以使用event.target.result获取数据库对象db,来自IDBRequest获取到的结果- 旧版本号
event.oldVersion - 新版本号
event.newVersion
- 创建对象仓库:使用
db.createObjectStore(name, {keyPath: string, autoIncrement: boolean})创建对象仓库,也就是存储对象的数据表。第二个参数是可选的配置项,可配置两个属性:keyPath和autoIncrement,分别表示每条记录的关键路径(类似主键名,将通过value.keyPath获取键值)和是否使用自动递增的整数作为键名(默认为false,初始值为1) - 创建索引:使用
objectStore.createIndex(name, keyPath, {unique: boolean, multiEntry: boolean})创建索引,索引名为name,索引的列为keyPath。如果设置为唯一索引,则不允许添加重复的索引值,否则添加报错ConstraintError;如果设置多值索引,也就是会将此列中列表的值拆分为多个索引值,需要使用单个索引项才能匹配上,比如[1, 2, 3]原先需要使用[1, 2, 3]将变为使用单个元素1、2、3都能匹配
const request = indexedDB.open("MyAppDB", 3); request.onupgradeneeded = (event) => { const db = event.target.result; const oldVersion = event.oldVersion; console.log(`数据库从版本 ${oldVersion} 升级到 ${db.version}`); // 注意:oldVersion = 0 表示“首次创建数据库” if (oldVersion < 1) { const userStore = db.createObjectStore("users", { keyPath: "id", autoIncrement: true }); userStore.createIndex("name", "name", { unique: false }); userStore.createIndex("email", "email", { unique: true }); userStore.createIndex("tags", "tags", { multiEntry: true }); } if (oldVersion < 2) { db.createObjectStore("orders", { keyPath: "orderId" }); } if (oldVersion < 3) { const settingStore = db.createObjectStore("settings", { keyPath: "key" }); settingStore.put({ key: "theme", value: "light" }); // 初始化默认数据 } }; request.onsuccess = (event) => { const db = event.target.result; console.log("数据库打开成功", db); }; request.onerror = (event) => { console.error("数据库打开失败", event.target.error); };- 在升级时触发
数据库连接关闭:使用数据库对象
db.close()关闭数据库连接数据库对象
db的事件close:数据库意外关闭时触发,普通的close()关闭不会触发事务versionchange:数据库版本号变化时触发
名称查询
- 删除数据库:使用
indexedDB.deleteDatabase(name)删除对应数据库,方法也会返回一个数据库请求对象IDBRequest,但似乎不会触发成功或报错事件 - 当前源所有数据库名称查询:
indexedDB.databases()可以异步promise返回所有数据库名称和版本号列表 - 当前数据库中对象仓库列表:可以使用
db.objectStoreNames返回当前数据库中的所有对象仓库名称的类数组对象 - 当前对象仓库中的索引列表:
objectStore.indexNames返回当前对象仓库索引名称的类数组对象
- 删除数据库:使用
6.9.2. 请求对象
- 正如之前看到的,实际上很多数据库操作都会返回一个数据库请求对象
IDBRequest,有以下属性request.readyState:请求状态,pending表示操作正在进行,done表示操作正在完成request.result:返回请求的结果。如果请求失败、结果不可用,读取该属性会报错request.error:请求失败时,返回错误对象。request.source:返回请求的来源(比如索引对象或ObjectStore)request.transaction:返回当前请求正在进行的事务,如果不包含事务,返回null
- 相关事件:
success:请求处理成功时触发error:请求处理失败时触发
6.9.3. 事务对象
所有的数据操作(包括存储、索引和游标)都应该在事务中完成
事务创建:使用
db.transaction(storeNames, mode, options)创建事务对象IDBTransaction- 参数
storeNames是字符串或字符串数组,表示对象仓库名称 - 参数
mode是事务模式,可选值有readonly、readwrite,默认为readonly - 定义其他选项的对象,目前有持久性
durability,可以是以下三个字符串文字值之一:"default","strict"(只有验证完所有操作都已经保存时才完成事务),"relaxed"
const transaction = db.transaction(["users", "orders"], "readwrite"); // 获取对象仓库对象 const userStore = transaction.objectStore("users"); const orderStore = transaction.objectStore("orders");- 参数
事务实例中的方法
objectStore(name):获取对象仓库对象objectStoreabort():取消事务commit():提交事务
事务实例带来的事件
abort:事务被取消(回滚)时触发complete:事务正常完成时触发error:事务发生错误时触发,所有的请求报错都会触发此事件- 在某个请求执行出错时会自动触发
abort() - 可以在
error事件中看见之后的操作都因为abort所以直接报错了 - 可以使用
event.preventDefault()阻止事务的取消
- 在某个请求执行出错时会自动触发
事务的生命周期:生命周期文档
active:事务在首次创建时以及从与事务关联的请求调度事件期间处于此状态- 此状态下,事务可以发起新请求
inactive:事务在创建后控制返回到事件循环后(也就是同步代码执行完毕),并且没有正在分派请求时,事务处于此状态- 当事务处于此状态时,不能对事务发出任何请求
committing:一旦与事务关联的所有请求都完成,事务将在尝试提交时进入此状态- 当事务处于此状态时,不能对事务发出任何请求
- 事务会自动提交
finished:一旦事务提交或中止,它就会进入此状态
事务调度:根据创建时提供的事务的作用域,进行事务调度
- 只读事务需要等待之前的访问相同仓库的读写事务执行完成
- 读写事务需要等待之前所有访问相同仓库的事务执行完成
6.9.4. 对象仓库对象
对象仓库是实现数据存储的接口,对象存储中的记录根据其键进行排序。这种排序可以实现快速插入、查找和有序检索。所有的方法都将返回一个数据库请求对象
IDBRequest对象仓库的实例属性:
indexNames: 索引名称列表keyPath: 键路径name: 仓库名称autoIncrement: 是否自增transaction: 返回当前仓库所属事务
增加数据,因为创建对象仓库时可能提供了额外配置选项,指明了关键路径
keyPath或自增,所以key参数可选。如果提供了额外配置项情况下添加key参数,会报错DataErroradd(value, key):添加数据。如果键已经存在,操作将失败,触发约束错误ConstraintError- 可用于实现如果存在就不插入数据,用于如果存在会报错,需要在
error事件中阻止默认行为,避免直接事务回滚
- 可用于实现如果存在就不插入数据,用于如果存在会报错,需要在
put(value, key):添加或更新数据。如果键不存在,则添加数据,如果键已经存在,则更新数据
// add:添加新记录(如果键存在则失败) const user1 = { id: 1, name: 'Alice', age: 25 }; const addRequest = store.add(user1); addRequest.onsuccess = () => console.log('添加成功:', user1); addRequest.onerror = (e) => console.error('添加失败(键已存在):', e.target.error); // put:添加或更新记录(存在则更新,不存在则插入) const user2 = { id: 2, name: 'Bob', age: 30 }; const putRequest = store.put(user2); putRequest.onsuccess = () => console.log('添加或更新成功:', user2);对象数据存储使用结构化算法完成,在Object深拷贝部分有介绍,无法克隆函数、原型链数据和
DOM对象,正则对象的部分属性和所有对象的属性描述符无法正确复制删除数据
delete(key):删除数据,删除时如果键不存在,操作也不会失败clear():清空数据
// 删除单条记录 const delRequest = store.delete(2); delRequest.onsuccess = () => console.log('删除 id=2 成功'); // 清空所有对象仓库数据 // const clearRequest = store.clear(); // clearRequest.onsuccess = () => console.log('所有数据已清空');查询数据,结果都存放在
success时event.target的result属性中,如果没有数据,属性为undefinedget(key):获取当前主键对应的数据,key可以是键值或一个键范围对象IDBKeyRangegetAll(query, count):获取所有数据,参数都是可选的,query是键值或键范围对象,count是最多获取的记录数getAllKeys(query, count):获取所有的主键值openCursor(range, direction):方法用于异步遍历所有数据range参数表明获取的数据范围,一个IDBKeyRange或键direction参数表明遍历方向,可选值有next、prev、nextunique、prevunique,默认为next- 获取数据成功时可以获取到游标
IDBCursor对象const result = event.target.result - 其中
result.value为当前行数据值,result.key为当前行键名;result.source为对应对象仓库或索引对象,result.direction为当前遍历方向,result.primaryKey为当前行主键名 - 执行完成当前行处理后需要使用
result.continue()方法继续遍历下一行数据 - 如果在访问过程中遇到行被删除,被删除行和之后的行都不会访问到
- 如果在访问过程中遇到事务提交会在
success执行过程中报错,同时不会触发error事件
// 获取单条 const getRequest = store.get(1); getRequest.onsuccess = (event) => { console.log('查询结果:', event.target.result); }; // 获取所有 const getAllRequest = store.getAll(); getAllRequest.onsuccess = (event) => { console.log('全部数据:', event.target.result); }; // === 使用 openCursor 遍历所有数据 === const cursorRequest = store.openCursor(); cursorRequest.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { console.log(`键: ${cursor.key}, 值:`, cursor.value); // 在此可对每一行数据进行处理 cursor.continue(); // 继续遍历下一条 } else { console.log('遍历结束'); } };IndexedRange
IDBKeyRange对象用于创建索引范围,可以只包含一个值,也可以指定上限和下限,有以下静态方法方便创建IDBKeyRange.upperBound(value, open):创建一个包含指定上限的索引范围IDBKeyRange.lowerBound(value, open):创建一个包含指定下限的索引范围IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen):创建一个包含指定范围的索引范围IDBKeyRange.only(value):创建一个只包含指定值的索引范围
// All keys ≤ x let r1 = IDBKeyRange.upperBound(x); // All keys < x let r2 = IDBKeyRange.upperBound(x, true); // All keys ≥ y let r3 = IDBKeyRange.lowerBound(y); // All keys > y let r4 = IDBKeyRange.lowerBound(y, true); // All keys ≥ x && ≤ y let r5 = IDBKeyRange.bound(x, y); // All keys > x &&< y let r6 = IDBKeyRange.bound(x, y, true, true); // All keys > x && ≤ y let r7 = IDBKeyRange.bound(x, y, true, false); // All keys ≥ x &&< y let r8 = IDBKeyRange.bound(x, y, false, true); // The key = z let r9 = IDBKeyRange.only(z);创建的对象可以使用
lower和upper属性获取范围边界,并通过lowerOpen和upperOpen属性判断边界是否开区间
其他方法
count():获取对象仓库中记录的数量
const countRequest = store.count(); countRequest.onsuccess = (event) => { console.log('对象仓库的记录数量:', event.target.result); }
6.9.5. 索引对象
- 索引对象可以根据对应索引取值,可以在对象仓库对象的以下方法得到索引对象实例
- 在
upgradeneeded事件回调中使用indexedDB.createIndex()方法创建索引 store.index(name):获取对应属性的索引对象实例,需要索引事先创建过,不然会报错NotFoundError- 在
upgradeneeded事件回调中使用store.deleteIndex()方法删除索引
- 在
- 索引对象实例方法有:
get(key):获取索引属性对应的数据,可能有多个满足项,将返回第一个getAll(query, count):获取索引属性对应的数据openCursor(range, direction):方法用于异步遍历所有的数据count():获取数据的个数getKey(key):获取索引属性对应主键的值
- 实例属性
name:索引名称keyPath:索引列的键名objectStore:索引所属的对象仓库对象multiEntry:索引属性是否为多值unique:索引列值是否唯一
6.10. 获取设备媒体
6.11. 文件处理
6.11.1. 获取文件
在浏览器中为了安全不能随意读取用户电脑上的文件,如果需要读取文件,需要使用
input[type=file]的标签,并且监听change事件- 标签本身的
files属性中包含了本次选择的文件列表,对应FileList接口(不可修改的文件列表,可以使用数组方法,但存在修改限制) - 这个列表的每一项都是一个
File对象,包含文件语义和数据内容等信息
如果觉得原始的
input[type=file]标签太丑了,可以将这个标签隐藏,使用自定义的文件选择器元素,然后在用户点击自定义选择器时,触发input[type=file]的click事件实现文件选择<input type="file" id="file" /> <script> const file = document.getElementById('file').files[0]; console.log(file); </script>- 标签本身的
利用拖动事件,拖动文件上传
- 拖动事件
drop中可以得到被拖动到元素上的文件列表e.dataTransfer.files - 拖动相关的事件需要禁用浏览器默认行为并阻止传播,避免误处理
function dragenter(e) { e.stopPropagation(); e.preventDefault(); } function dragover(e) { e.stopPropagation(); e.preventDefault(); } function drop(e) { e.stopPropagation(); e.preventDefault(); const dt = e.dataTransfer; const files = dt.files; handleFiles(files); }- 拖动事件
6.11.2. Blob 与 File 对象
File对象是Blob对象的子类,继承了Blob对象的所有属性和方法,并且主要新增了文件名name和上次修改时间lastModified属性,了解File对象要先从Blob对象开始Blob对象是用于描述文件内容的数据载体,存储文件的二进制数据,可以方便地进行文件分片和传输type:文件类型,比如image/png和text/plainsize:文件大小,单位字节slice(start, end, type):返回一个新的Blob对象,常用于文件分片- 新对象包含从
start到end的二进制数据,start和end参数都是字节数,都是可选参数,默认包括整个文件,如果使用负数则表示从文件末尾开始计数 type表示新的Blob对象的type,也是可选的
- 新对象包含从
text():返回值为一个Promise对象,用于读取文件内容,转换为文本字符串arrayBuffer():返回值为一个Promise对象,用于读取文件内容,转换为二进制数据,适用于图片等二进制数据- 如果是上传操作,一般不需要从
Blob中读取,如果希望上传二进制流,可以考虑读取ArrayBuffer,是一个可传输对象,传输时对应的content-type为application/octet-stream - 如果是下载操作,读取完成后如果希望处理,需要先转换为
uint8Array,因为ArrayBuffer对象无法操作内容,必须使用这种视图读写,之后就可以使用String.fromCharCode()将二进制数据转换为字符串,再使用btoa()将普通字符串数据转换为base64编码 - 一般情况下不需要使用这个方法处理
Blob数据,可以使用下面介绍的FileReader对象
- 如果是上传操作,一般不需要从
bytes():返回值为一个Promise对象,用于读取文件内容,转换为uint8Array对象stream():返回值为一个ReadableStream对象,用于读取文件内容,返回的是一个可读流
构造一个
Blob对象:new Blob(list[, options])可以动态生成一个Blob对象- 列表中可以有:普通字符串;二进制类型,比如
ArrayBuffer、TypeArray、DataView、其他Blob对象 options中可以设置属性:type:指定Blob对象的type属性,默认为''endings:指定Blob对象中的换行符处理方式,可选值有不处理'transparent'和按系统处理'native',默认为'transparent'
const blob = new Blob(['hello world'], { type: 'text/plain' }); const blob = new Blob([new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100])], { type: 'text/plain' });- 列表中可以有:普通字符串;二进制类型,比如
File对象继承了Blob对象的所有属性方法,并且额外扩展了lastModified:文件最后修改时间,毫秒数name:文件名webkitRelativePath:文件的目录在文件系统中的路径,Firefox安卓端2025刚适配,其他浏览器早已支持
- 构造一个
File对象:new file(fileBits, name[, options])fileBits:和Blob对象构造一样,是一个包含普通字符串、二进制数据、Blob对象的列表name:文件名options:除了Blob构造函数支持的type和endings属性,还可以设置lastModified,默认值是Date.now()
6.11.3. 读取文件内容
如果需要进一步读取文件或
Blob对象内容进行处理,比如预览上传图片内容,需要使用FileReader对象,这个对象专门用来读取文件内容,转换为字符串或base64编码,常用方法属性包括readAsText(blob):读取文件内容文本,将结果保存在result属性中- ``readAsDataURL(blob)
:读取文件内容作为图片base64编码,将结果保存在result`属性中 readAsArrayBuffer(blob):读取文件内容二进制数据作为arrayBuffer,将结果保存在result属性中readyState:读取状态,0-2分别表示未开始、正在读取、读取完成
对应有同步读取的
FileReaderSync对象,方法相同const reader = new FileReader(); // 需要使用 load 事件监听读取完成 reader.onload = (event) => { console.log(reader.result); } // 所有的读取都是异步的,由浏览器的 I/O 线程或其他线程完成 // 一般一个 reader 用于读取一个文件,必须要在一次读取完成后才能再次调用读取的方法 reader.readAsText(file);base64编码
Base64是一种把二进制数据用文本表示出来的编码方式,用于在某些场景安全传输二进制数据(二进制中可能有不可见字符或控制字符)Base64编码使用了大小写字母、数字、+和/共64个字符编码(编码索引按提及顺序,0对应A,63对应/),并添加=进行补位。有一些变种编码会使用-和_代替+和/- 编码要求将原先的每
3个字节二进制数据拆成4个6位序列,每个序列对应一个编码字符,可以看出实际上序列可能性恰好种,因此称为Base64,同时编码之后6位表示变为8位,数据量膨胀约 - 如果原文字节数不是
3的倍数,字节就不会恰好可被6整除,需要在原文尾部添加0,确保所有原始数据都被编码。此时最终Base64编码就不是4的倍数,此时需要用=进行补位,实际上最多尾部只会添加1-2个= Base64编码的字符串格式为data:[mime];base64,...,其中data:[mime]用于指明对应数据的存储格式,之后使用base64指明编码方式,最后的部分...是具体的内容Base64数据可以作为图片、视频、音频的输入源,不依赖真实的地址和服务器,适合预览。同时Base64生成的数据不同于Blob.url,生成的预览数据由浏览器管理,不需要手动释放
6.11.4. 文件上传
- 文件上传是一个常见的场景,将
File对象或Blob对象上传,本质上上传的是原始的二进制数据作为
FormData的一个表项上传:也就是通过formData.append存储之后将整个FormData对象上传const formData = new FormData(); formData.append('file', file); formData.append('name', file.name); fetch('/upload', { method: 'POST', body: formData })直接上传文件:由于名称不会被上传,因此通常需要将名称放入请求头或参数中
fetch('/upload', { method: 'POST', body: file headers: { 'x-name': file.name 'Content-Type': 'application/octet-stream' } })分片上传:当文件较大时,应该考虑分片上传
- 文件分片可以方便实现断点续传,减少网络开销。服务器可以返回接收到的分片信息,然后前端重新传输某些未成功的分片
- 文件很大,应该在上传前验证
hash,如果文件存在,则不需要上传。操作可以在上传开始前完成,同时传输分片数量等其他信息 - 具体实现中一般需要请求3个后端接口,第一个接口进行
hash验证,并通知分片信息,后端返回对应的文件是否存在等信息;第二个接口上传分片,后端需要临时存储;第三个接口将分片合并,并进行校验,如果发现分片未传输完成,需要通知上传
import SparkMD5 from 'spark-md5'; const chunkSize = 50 * 1024 * 1024; const total = Math.ceil(file.size / chunkSize); async function calcFileHash(file, chunkSize = 5 * 1024 * 1024) { const spark = new SparkMD5.ArrayBuffer(); const chunks = Math.ceil(file.size / chunkSize); for (let i = 0; i < chunks; i++) { const chunk = file.slice( i * chunkSize, (i + 1) * chunkSize ); const buffer = await chunk.arrayBuffer(); spark.append(buffer); } return spark.end(); } const hash = await calcFileHash(file); // 预验证 const res = await fetch('/upload/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hash, filename: file.name, size: file.size, total }) }); const { exists, uploadedChunks } = await res.json(); if (!exists) { // 分片上传 let requestList = [] for (let i = 0; i < total; i++) { if (uploadedChunks.includes(i)) continue; const chunk = file.slice( i * chunkSize, (i + 1) * chunkSize ); const fd = new FormData(); fd.append('file', chunk); fd.append('hash', hash); // append 方法只支持字符串和blob输入,会隐式转换为字符串 fd.append('index', i); fd.append('total', total); requestList.push(fetch('/upload/chunk', { method: 'POST', body: fd })); } await Promise.all(requestList); // 之后合并 fetch('/upload/merge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hash }) }); }
6.11.5. 文件预览
文件预览一般用于提供上传前的查看和处理的能力,常见的可直接预览的文件包括图片、视频、音频,像
pdf这样的文件一般也有相应的库可以实现预览和处理常用的预览方法
- 使用
Blob URL:Blob对象可以通过URL.createObjectURL(blob)生成对应的URL,之后将URL作为源地址就可以预览- 创建的
URL仅对当前页面有效 - 需要手动释放,
URL.revokeObjectURL(url) - 具体和见URL对象的使用部分
- 创建的
- 使用
Base64:Base64编码可以用于video、img和audio标签的源地址Base64编码转码耗时较长,大文件应该优先考虑Blob URL
- 直接获取文本:
FileReader对象可以通过readAsText(blob)读取文件并返回文本内容 - 其他浏览器支持文件的预览:通过
iframe可以实现其他浏览器可打开文件的预览- 实际上就是使用
URL.createObjectURL(blob)生成对应的链接,并作为iframe的src
- 实际上就是使用
- 使用第三方库:如
pdf.js,提供了其他文件或增强的预览方法
// 创建 Blob URL const url = URL.createObjectURL(f) img.src = url URL.revokeObjectURL(url) // 创建 Base64 const reader = new FileReader(); reader.onload = function () { img.src = reader.result; } reader.readAsDataURL(f);- 使用
6.11.6. 文件下载
文件下载是另一个常见的场景,一般是通过修改页面地址(新窗口打开)实现下载
后端应该设置
Content-Disposition: attachment;头,也就是默认下载文件,而不是优先预览具体的实现方法
- 通过
a标签:a标签可以用于打开新的页面,最基本的方法就是将a标签的href属性设置为对应的Blob url或真实url地址a标签有一个download属性,用于指定下载的文件名,如果Content-Disposition头包含文件名,则最终名称以服务器返回的文件名为准- 适合各类文件,大文件应该考虑直接使用真实的
url下载,避免Blob格式内存占用过高
- 通过
window.location.href = url:修改页面地址,如果是不支持预览或设置了Content-Disposition: attachment;的地址,会触发下载操作,而不是页面跳转- 文件名称应该添加到
Content-Disposition头
- 文件名称应该添加到
const div = document.querySelector('#download'); div.addEventListener('click', (e) => { // 使用 a 标签 const a = document.createElement('a'); a.href = url; a.download = 'file.txt'; a.click(); // 使用 window.location.href window.location.href = url; })- 通过
6.12. 其他小工具
6.12.1. 全屏切换
6.12.2. URL
URL主要包括:URL对象:创建解析操作URLURLSearchParams对象:操作query查询参数
new URL(url, base):创建解析操作URL,得到一个URL对象url.hostname:网址或主机名url.pathname:子路径url.protocol:协议url.port:端口url.search:query参数url.searchParams:query参数对应的URLSearchParams对象url.hash:锚点#
const u = new URL("https://example.com:8080/path/file?x=1#top"); u.hostname // "example.com" u.port // "8080" u.protocol // "https:" u.pathname // "/path/file" u.search // "?x=1" u.hash // "#top" // 可以修改 url.pathname = "/api"; url.searchParams.set("q", "hello"); // 重新生成 URL console.log(url.toString(), url.href);静态方法:
URL.createObjectURL(blob):创建一个指向blob对象的URL,这个URL可以作为预览内容的地址,地址格式为blob:<域名>/<UUID>- 此地址不会传递给后端,仅当前会话有效
- 一般传入的blob为图片对象,通过
blob地址对应图片,用于显示图片上传预览 - 此方法也可以用于生成
uuid
URL.revokeObjectURL(url):释放一个blob对象对应的URL
const input = document.querySelector('input'); let url; input.addEventListener('change', function (e) { const blob = event.target.files[0]; url = URL.createObjectURL(blob); const img = document.querySelector('img'); // blob 地址作为图片的显示地址 img.src = url; img.style.display = 'block'; }); window.addEventListener('unload', function () { if (url) { URL.revokeObjectURL(url); } })URL.canParse(url, base):判断url是否可以解析成有效的URL,返回true或false(2023才广泛使用)URL.parse(url, base):解析url,返回一个URL对象(2024才广泛使用)
URLSearchParams对象:操作query查询参数,相当于一个参数字典get(name):获取第一个满足的参数值,如果没有满足的参数,返回nullgetAll(name):获取所有满足的参数值列表set(name, value):设置参数值,如果有多个匹配值,将匹配键值的参数全部删除再添加新参数append(name, value):添加参数delete(name):删除参数has(name):判断参数是否存在sort():通过键排序参数size:目前含有的参数个数toString():返回参数字符串- 可迭代,有
keys()、values()、entries()和forEach()方法 toString():返回参数字符串
如果查询中包含多个相同的键值对,现在主流的后端框架返回的是数组,一般都能传递,部分老框架行为可能不同
- 其他的全局处理函数
encodeURI(str):对字符串进行编码,用于编码一个URL,因此最终的; / ? : @ & = + $ , #等URL字符不会被编码encodeURIComponent(str):对字符串进行编码,相比encodeURI,编码的字符更多,和encodeURI相同的是不会编码字符包括A–Z a–z 0–9 - _ . ! ~ * ' ( )btoa(str):将字符串进行Base64编码,包含占用了多个字节的字符的字符串不应该作为输入atob(str):将Base64编码的字符串进行解码
6.12.3. Intl
6.12.4. Performance
6.12.5. crypto
6.12.6. view transition
view transition:页面视图转换API,是在2025年完成基本适配的功能,它能够在不同的页面视图之间创建合适的过渡动画,只需要很少的代码就能实现,可用于同页面内的状态转换或不同页面的前进后退- 工作原理:
- 你告知浏览器需要过渡,浏览器自动捕捉旧视图的快照(静态图像)
- 之后执行你提供的正常的页面更新操作
- 浏览器再次捕捉新视图的快照
- 两个视图快照构建在伪元素
::view-transition中 - 然后在你需要的时候进行过渡,最后浏览器在确认当前帧没有需要进行的视图动画时清理伪元素
- 伪元素
::view-transition伪元素的结构如下,对于一个页面而言,可能有多个
view-transition-group,每个组设定了被捕获的元素区域,默认是root也就是整个页面,在其中有一对被光栅化的新旧页面快照(类似静态图像副本)::view-transition └─ ::view-transition-group(root) ← 默认 root 也可自定义 └─ ::view-transition-image-pair(root) ├─ ::view-transition-old(root) ← 旧视图 └─ ::view-transition-new(root) ← 新视图你可以为这些伪元素添加样式,比如视图切换的过渡效果,但也要注意这些伪元素自带的关键样式
伪元素 功能说明 典型需要修改哪些样式(最常用) 默认 UA样式(关键部分)::view-transition整个过渡的根 overlay,覆盖全页面内容 background-color(偶尔加遮罩)position: fixed; inset: 0;::view-transition-group(name)负责位置、大小、 transform的平滑动画(移动缩放)animation-duration、timing-function、z-index、transform-originposition: absolute; animation-duration: 0.25s;::view-transition-image-pair(name)old+new的容器,控制混合模式isolation: isolate/auto(开启mix-blend-mode)position: absolute; inset: 0; isolation: isolate;::view-transition-old(name)出场( outbound)动画:旧内容淡出、滑出等animation-name、opacity、transformposition: absolute; ... animation-* inherit::view-transition-new(name)入场( inbound)动画:新内容淡入、滑入等同 old同 old自定义区域:默认情况下,快照是通过对整个页面捕获得到的,你可以使用
view-transition-name属性为你需要捕获的区域命名,之后就可以对对应区域的快照进行样式设置/* 给对应区域命名 */ /* 如果是从一个块域到另一个块域的转换, 需要确保转换的视图区域的这个属性值一致 */ .my-transition-region { view-transition-name: my-hero-section; /* 唯一名称 */ } /* 自定义这个区域的动画 */ ::view-transition-group(my-hero-section) { } ::view-transition-old(my-hero-section) { } ::view-transition-new(my-hero-section) { }
开始过渡
你需要为页面设置过渡样式,这可以使用
css完成,也可以通过js实现- 如果需要的样式和鼠标交换有关,很明显使用
js直接添加样式更方便,因为本身就需要计算鼠标位置 - 我们以常见的从按钮开始的水波纹过渡效果为例,使用
clip-path完成,提供js和css添加过渡样式的方法 - 通常情况下,不需要对旧视图进行过渡,也就是说需要禁用旧视图的动画,避免旧视图和新视图同时过渡,产生干扰。新旧视图的过渡带来了两个过渡动画,在动画的中间状态就完成过渡了
js// js添加样式 // 计算鼠标位置和水波纹的半径 const x = e.clientX; const y = e.clientY; const endRadius = Math.hypot( Math.max(x, innerWidth - x), Math.max(y, innerHeight - y) ); // 需要关闭默认的渐变动画效果 // ::view-transition-old(root), // ::view-transition-new(root) { // animation: none; // mix-blend-mode: normal; // display: block; // } // 用 Web Animations API 执行手动圆形过渡 function startRipple() { document.documentElement.animate( { clipPath: [ `circle(0px at ${x}px ${y}px)`, // 从中心开始(半径 0) `circle(${endRadius}px at ${x}px ${y}px)` // 扩散到全屏 ] }, { duration: 600, easing: 'ease-in-out', pseudoElement: '::view-transition-new(root)' // 只动画新视图 } ); } startRipple();css/* css 添加样式 */ /* // 将相关信息挂载在html根元素上,方便伪类访问到 document.documentElement.style.setProperty('--x', `${x}px`); document.documentElement.style.setProperty('--y', `${y}px`); document.documentElement.style.setProperty('--end-radius', `${endRadius}px`); */ @keyframes clip-path-animation { from { clip-path: circle(0 at var(--x) var(--y)); } to { clip-path: circle(var(--end-radius) at var(--x) var(--y)); } } ::view-transition-old(root) { animation: none; } ::view-transition-new(root) { animation: theme-in 0.6s ease-in-out; }- 如果需要的样式和鼠标交换有关,很明显使用
对于
SPA应用内部的切换,你需要使用window.startViewTransition方法来开始过渡- 方法传入一个回调,回调执行前是当前视图状态,回调执行后是新的视图状态
- 返回值是
ViewTransition接口类型,其中包裹了多个个Promise,在伪结点生命周期进行到某个阶段后实现ready:一个Promise,如果伪简单构建完成后,实现,返回voidfinished:一个Promise,如果过渡完成(新视图已经完全可见),返回void。如果更新回调执行报错或在SPA应用使用时返回了一个拒绝reject的Promise,finished会拒绝updateCallbackDone:一个Promise,如果提供的回调执行完成,返回值,如果返回的是拒绝的Promise,那么updateCallbackDone也会拒绝type:过渡类型字符串标识,可以在完成构建前修改它,这对应css样式伪类html:active-view-transition-type(typeStr),默认情况下使用的是html:active-view-transition伪类,如果设置了type,那么优先使用对应的伪类skipTransition():一个实例方法,可以在需要跳过过渡时使用
- 可以使用
await transition.ready等待构建完成,之后进行js手动过渡 - 也可以在样式中预定义过渡逻辑,在调用
window.startViewTransition之后通过伪类动画样式过渡
const transition = window.startViewTransition(() => { // 切换样式 const html = document.documentElement; const current = html.getAttribute('data-theme'); html.setAttribute('data-theme', current === 'light' ? 'dark' : 'light'); }) // 如果需要手动添加过渡样式,需要等待伪类构建完成 await transition.ready; // 立即手动过渡,浏览器新帧还没有创建,伪类不会立即清理 startRipple();对于
MPA应用,过渡切换需要使用@view-transition { navigation: auto; }添加样式标注,表明需要在切换同源页面时进行过渡,之后就可以使用伪类的过渡样式进行过渡了- 一旦进行了同源跳转,会使用新页面的动画样式进行过渡
- 可以在
<head>区域使用<link rel="expect" href="#lead-content" blocking="render" />,过渡会在渲染完成对应区域后再开始 pageswap、pagereval事件,这两个事件目前还没有广泛支持,可见MDN
@view-transition { navigation: auto; } ::view-transition-old(root) { animation: slide-out-to-right 3s ease-in-out; } ::view-transition-new(root) { animation: slide-in-from-left 3s ease-in-out; } @keyframes slide-out-to-right { to { transform: translateX(100%); } } @keyframes slide-in-from-left { from { transform: translateX(-100%); } } /* 过渡对任意页面都能生效,效果是平滑地向右滑动切换 */
