HTML5中IndexedDB前端本地数据库

一、indexedDB为何替代了Web SQL Database?

        跟小朋友的教育从来没有什么“赢在起跑线”这种说法一样,在前端领域,也不是哪来先出来哪个就在日后引领风骚的。

        HTML5 indexedDB和Web SQL Database都是本地数据库数据存储,Web SQL Database数据库要出来的更早,然并卵。从2010年11月18日W3C宣布舍弃Web SQL database草案开始,就已经注定Web SQL Database数据库是明日黄花。

未来一定是indexedDB的,从目前浏览器的兼容性来看,也表明了这种结果:

        可以看到IE和Firefox并不支持Web SQL Database,基本上可以断定永远也不会支持,规范都不认可,实在没有浪费精力去支持的理由。

        好,现在我们知道了indexedDB取代Web SQL Database大局已定,那大家知道为何Web SQL Database会被舍弃吗?

下面是一段Web SQL Database代码:

database.transaction(function(tx) {tx.executeSql("CREATE TABLE IF NOT EXISTS tasks (id REAL UNIQUE, text TEXT)", []);
}

        可以看到直接把SQL语句弄到JS中了,主流的关系型数据库即视感,这么设计看上去似乎无可厚非,但恰恰这个设计成为了Web SQL Database被舍弃的重要原因之一:一是学习成本高了很多,SQL虽然本身并不复杂,但跨度较大,例如我司玩JS的工程师和玩SQL的工程师中间还隔了个玩php的工程师;二是本身使用很不方便,需要把JS对象转换成关系型的字符串语句,很啰嗦的。

而indexedDB直接JS对象入库,无缝对接。

下表为更详细的indexedDB和Web SQL Database对比内容:

WebSQL

IndexedDB

优点

真正意义上的关系型数据库,类似SQLite(SQLite是遵守ACID的轻型的关系型数据库管理系统)

  • 允许对象的快速索引和搜索,因此在Web应用程序场景中,您可以非常快速地管理数据以及读取/写入数据。
  • 由于是NoSQL数据库,因此我们可以根据实际需求设定我们的JavaScript对象和索引。
  • 在异步模式下工作,每个事务具有适度的粒状锁。这允许您在JavaScript的事件驱动模块内工作。

不足

  • 规范不支持啦
  • 由于使用SQL语言,因此我们需要掌握和转换我们的JavaScript对象为对应的查询语句。
  • 非对象驱动。

如果你的世界观里面只有关系型数据库,恐怕不太容易理解。

位置

包含行和列的表。

包含JavaScript对象和键的存储对象。

查询机制

SQL

Cursor APIs,

Key Range APIs,应用程序代码

事务

锁可以发生在数据库,表,行的“读写”时候。

锁可以发生在数据库版本变更事务,或是存储对象“只读”和“读写”事务时候。

事务提交

事务创建是显式的。默认是回滚,除非我们调用提交。

事务创建是显式的。默认是提交,除非我们调用中止或有一个错误没有被捕获。

STOP!

不能再继续说下去了,估计看完上面对比表中的内容,已经有很多同学已经有些不知所以然了,因为其中涉及到了很多数据库相关的概念,要想轻松理解上面内容以及更轻松掌握indexedDB的相关知识,这些概念还是需要掌握的。

二、务必先了解数据库的一些概念

对于前端同学,数据库概念没必要理解十分精准,只需要知道大概怎么回事就好了,因此,下面的一些解释会其意,勿嚼字。

1. 关系型数据库和非关系型数据库

“关系型数据库”是历史悠久,已经有半个世纪,占据主流江山的数据库模型,估计诸位公司的网站多半都是使用的这种数据库模型。而“非关系型数据库”(NoSQL数据库)则要年轻很多,根据资料显示是Carlo Strozzi在1998年提出来的,20年不到,使用键值对存储数据,且结构不固定,非常类似JavaScript中的纯对象。

var obj = { key1: 'value1', key2: 'value2', key3: 'value3', ... };

“关系型数据库”对一致性要求非常严格,例如要写入100个数据,前99个成功了,结果第100个不合法,此时事务会回滚到最初状态。这样保证事务结束和开始时候的存储数据处于一致状态。非常适合银行这种对数据一致性要求非常高的场景。但是,这种一致性保证是牺牲了一部分性能的。

但是,对于微博,QQ空间这类web2.0应用,一致性却不是显得那么重要,比方说朋友A看我的主页和朋友B看我的主页信息有不一样,没什么大不了的,有个几秒差异很OK的。虽然这类应用对一致性要求不高,但对性能要求却很高,因为读写实在太频繁了。如果使用“关系型数据库”,一致性的好处没怎么收益,反而性能问题比较明显,此时,不保证一致性但性能更优的“非关系型数据库”则更合适。同时,由于“非关系型数据库”的数据结构不固定,非常容易扩展。由于对于社交网站,需求变动那是一日三餐常有的事,添加新字段在所难免,“非关系型数据库”轻松上阵,如果使用“关系型数据库”,多半要数据大变动,要好好琢磨琢磨了。

从气质上讲,“关系型数据库”稳重持久,“非关系型数据库”迅速灵动。

在前端领域,Web SQL Database是“关系型数据库”,indexedDB是“非关系型数据库”。

2. 了解数据库中的事务-transaction

数据库的事务(英文为’transaction’),我们可以理解为对数据库的操作,而且专指一个序列上的操作。举个例子,银行转账,一个账号钱少了然后另一个账号钱多了,这两个操作要么都执行,要么都不执行。像这种操作就可以看成一个事务。

事务的提出主要是为了保证并发情况下保持数据一致性。关系型数据库中的事务具有下面4个基本特征:

  1. 原子性(Atomicity):事务中的所有操作作为一个整体提交或回滚。
  2. 一致性(Consistemcy):事物完成时,数据必须是一致的,以保证数据的无损。
  3. 隔离性(Isolation):对数据进行修改的多个事务是彼此隔离的。
  4. 持久性(Durability):事务完成之后,它对于系统的影响是永久的,该修改即使出现系统故障也将一直保留。

通常专业描述中会抽取这4个基本特性的首字母,统称为“ACID特性”。事务执行过程可以粗浅地理解为:开始事务,巴拉巴拉操作,如果错误,回滚(rollback),如果没问题,提交(commit),结束事务。

3. 了解数据库中的游标-cursor

现实世界中的“游标”相关联的常见事物就是“游标卡尺”,有刻度有区域:

数据库中的游标其实与之有共同之处。内存条本质上就像一把尺子,我们可以想象上面有很多刻度,然后内存大小就是由这些刻度一个一个堆砌起来的。数据库的事务为了保证数据可以回滚,显然需要有一片内存区域放置那些即将受影响是数据,这个内存区域中的虚表就是数据库的“游标”。

和现实世界的“游标卡尺”相映射就是:一个刻度表示一行数据,游标就是尺子上的一片区域,想要获得数据库一行一行的数据,我们可以遍历这个游标就好了。

4. 数据库中的“锁”-lock

数据库中的“锁”是保证数据库数据高并发时候数据一致性的一种机制。举个例子:现有两处火车票售票点,同时读取某发现上海到北京车票余额为5。此时两处售票点同时卖出一张车票,同时修改余额为5-1也就是4写回数据库,这样就造成了实际卖出两张火车票而数据库中的记录却只少了1张。为了避免发生这种状况,就有了锁机制,也就是执行多线程时用于强行限制资源访问。

三、从简单实用的实例开始学习indexedDB

要想系统学习indexedDB相关知识,可以去MDN文档啃API,假以时日就可以成为前端indexedDB方面的专家。但是这种学习方法周期长,过于痛苦,无法立竿见影,因为API实在太多,天天花个把小时,估计也需要数周时间才能全部通透。

一想到投入产出比这么低,于是我决定从实例入手开始学习,能够实现基本的数据库增删改查功能就好,基本上80%+的相关需求都能从容搞定;等日后有机会再慢慢深入。

花了几个晚上折腾,一个具有增删改无查的实例页面终于弄出来了,您可以狠狠地点击这里:HTMl5 indexedDB存储编辑和删除数据demo

我们第一次进去是没有数据的,显示如下:

1. 此时,我们填写表单,创建一条数据,如下:

点击“确定创建”按钮后,就得到如下图所示的结果:

此时我们刷新页面,依然是上图所示的结果。

打开控制台,在Application→indexedDB中,我们可以看到当前存储的数据字段和值等信息:

2. 我们可以直接对创建的数据进行修改,例如修改备注信息:

失焦后直接就修改了本地数据库信息了,我们刷新一下,可以看到备注文案已经变成“是个帅哥~”

3. 我们还可以对数据库数据进行删除,例如点击删除按钮:

结果这条数据库数据就被删掉了,界面重新显示为“暂无数据”。

以上就是demo页面所实现的indexedDB的增加数据,编辑数据和删除数据功能。

如果您当前的需求急用,可以把demo页面上的JS源代码直接复制过去改改;如果你希望对indexedDB相关API及其使用有所了解,就继续往下看。

1. 首先打开indexedDB数据库

indexedDB数据库打开非常简单,语法就是字面意思:

window.indexedDB.open(dbName, version);

dbName就是数据库名称,例如demo中使用的是'project';version表示数据库的版本,根据我的理解,当我们对数据库的字段进行增加或修改时候,需要增加个版本。

通常,打开indexedDB数据库是和一些回调方法一起出现的,代码套路比较固定,基本上如果大家在实际项目中使用的话,都可以使用类似的代码:

// 数据库数据结果
var db;
// 打开数据库
var DBOpenRequest = window.indexedDB.open('project', 1);
// 数据库打开成功后
DBOpenRequest.onsuccess = function(event) {        // 存储数据结果db = DBOpenRequest.result;// 做其他事情...
};// 下面事情执行于:数据库首次创建版本,或者window.indexedDB.open传递的新版本(版本数值要比现在的高)
DBOpenRequest.onupgradeneeded = function(event) {// 通常对主键,字段等进行重定义,具体参见demo
};

其中,最重要就是红色高亮的db变量,我们后面所有的数据库操作都离不开它。

2. 创建indexedDB数据库的主键和字段

在我们开始数据库的增加删除操作之前,首先要把我们数据库的主键和一些字段先建好。是在onupgradeneeded这个回调方法中设置的,这个回调方法执行于数据库首次创建版本或者数据库indexedDB.open()方法中传递的版本号比当前版本号要高。

我们对数据库某一行数据进行增加删除操作,我们是没有必要对数据库的版本号进行修改的。但是对于字段修改就不一样了,比方说原来是5列数据,我们现在改成6列,由于相关设置是在onupgradeneeded回调中,因此,我们需要增加版本号来触发字段修改。demo页面这部分代码如下:

DBOpenRequest.onupgradeneeded = function(event) {var db = event.target.result;// 创建一个数据库存储对象var objectStore = db.createObjectStore(dbName, { keyPath: 'id',autoIncrement: true});// 定义存储对象的数据项objectStore.createIndex('id', 'id', {unique: true    });objectStore.createIndex('name', 'name');objectStore.createIndex('begin', 'begin');objectStore.createIndex('end', 'end');objectStore.createIndex('person', 'person');objectStore.createIndex('remark', 'remark');
};

这里出现了一个比较重要的概念,叫做objectStore,这是indexedDB可以替代Web SQL Database的重要优势所在,我称之为“存储对象”。

objectStore.add()可以向数据库添加数据,objectStore.delete()可以删除数据,objectStore.clear()可以清空数据库,objectStore.put()可以替换数据等还有很多很多操作API。

在这里,我们使用objectStore来创建数据库的主键和普通字段。

db.createObjectStore在创建主键同时返回“存储对象”,本演示中使用自动递增的id字段作为关键路径。createIndex()方法可以用来创建索引,可以理解为创建字段,语法为:

objectStore.createIndex(indexName, keyPath, objectParameters)

其中:

indexName

创建的索引名称,可以使用空名称作为索引。

keyPath

索引使用的关键路径,可以使用空的keyPath, 或者keyPath传为数组keyPath也是可以的。

objectParameters

可选参数。常用参数之一是unique,表示该字段值是否唯一,不能重复。例如,本demo中id是不能重复的,于是有设置:objectStore.createIndex('id', 'id', { unique: true });

3. indexedDB数据库添加数据

字段建好了之后,我们就可以给indexedDB数据库添加数据了。

由于数据库的操作都是基于事务(transaction)来进行,于是,无论是添加编辑还是删除数据库,我们都要先建立一个事务(transaction),然后才能继续下面的操作。语法就是名称:

var transaction = db.transaction(dbName, "readwrite");

dbName就是数据库的名称,于是我们的数据库添加数据操作变得很简单:

// 新建一个事务
var transaction = db.transaction('project', "readwrite");
// 打开存储对象
var objectStore = transaction.objectStore('project');
// 添加到数据对象中
objectStore.add(newItem);

使用一行语句表示就是:

db.transaction('project', "readwrite").objectStore('project').add(newItem);

这里的newItem直接就是一个原生的纯粹的JavaScript对象,在本demo中,newItem数据类似下面这样:

{"name": "第一个项目","begin": "2024-07-16","end": "2054-07-16","person": "樊振东","remark": "恭喜大满贯"
}

可以发现,当我们使用indexedDB数据库添加数据的时候,根本就不用去额外学习SQL语句,原始的JavaScript对象直接无缝对接,,

4. indexedDB数据库的编辑

原理为,先根据id获得对应行的存储对象,方法为objectStore.get(id),然后在原存储对象上进行替换,再使用objectStore.put(record)进行数据库数据替换。

代码示意:

function edit(id, data) {// 新建事务var transaction = db.transaction('project', "readwrite");// 打开已经存储的数据对象var objectStore = transaction.objectStore(project);// 获取存储的对应键的存储对象var objectStoreRequest = objectStore.get(id);// 获取成功后替换当前数据objectStoreRequest.onsuccess = function(event) {// 当前数据var myRecord = objectStoreRequest.result;// 遍历替换for (var key in data) {if (typeof myRecord[key] != 'undefined') {myRecord[key] = data[key];}}// 更新数据库存储数据             objectStore.put(myRecord);};
}

其中,id就是要替换的数据库行的id字段值,因为唯一的主键,可以保证准确和高性能;data则是需要替换的数据对象,例如从“测试测试”修改为“是个帅哥~”,则调用:

edit(1, {remark: '是个帅哥~'
});

就可以了,跟我们平常写JS代码就没有什么区别。

5. indexedDB数据库的删除

和添加操作正好相反,但代码结构却是类似的,一行表示法:

db.transaction('project', "readwrite").objectStore('project').delete(id);

id表示需要删除行id字段对应的值。

6. indexedDB数据库的获取

indexedDB数据库的获取使用Cursor APIs和Key Range APIs。

也就是使用“游标API”和“范围API”。在本文的演示页面中,只使用了“游标API”,直接显示全部数据,对于“范围API”并没有使用,但这里也会简单介绍下。

“游标API”可以让我们一行一行读取数据库数据,代码示意:

var objectStore = db.transaction(dbName).objectStore(dbName);
objectStore.openCursor().onsuccess = function(event) {var cursor = event.target.result;if (cursor) {// cursor.value就是数据对象// 游标没有遍历完,继续cursor.continue();} else {// 如果全部遍历完毕...}
}

可以看到,我们使用存储对象的openCursor()打开游标,在onsuccess回调中就可以遍历我们的游标对象了。其中cursor.value就是完整的数据对象,纯JS对象,就像下面这样:

{"id":"1","name": "第一个项目","begin": "2024-07-16","end": "2054-07-16","person": "樊振东","remark": "恭喜大满贯"
}

我们就可以按照需求随意显示我们的数据了。

“范围API”是和“游标API”一起使用的,看下面这个例子,只打开id从4~10之间的数据,则有:

// 确定打开的游标的主键范围
var keyRangeValue = IDBKeyRange.bound(4, 10);
// 打开对应范围的游标
var objectStore = db.transaction(dbName).objectStore(dbName);
objectStore.openCursor(keyRangeValue).onsuccess = function(event) {var cursor = event.target.result;// ...
}

其中,有bound(), only(), lowerBound()和upperBound()这几个方法,意思就是方法名字面意思,“范围内”,“仅仅是”,“小于某值”和“大于某值”。

方法最后还支持两个布尔值参数,例如:

IDBKeyRange.bound(4, 10, true, true)

则表示范围3~9,也就是为true的时候不能和范围边界相等。

四、indexedDB存储和localStorage存储对比

  • indexedDB存储IE10+支持,localStorage存储IE8+支持,后者兼容性更好;
  • indexedDB存储比较适合键值对较多的数据,我之前不少项目需要存储多个字段,使用的是localStorage存储,结果每次写入和写出都要字符串化和对象化,很麻烦,如果使用indexedDB会轻松很多,因为无需数据转换。
  • indexedDB存储可以在workers中使用,localStorage貌似不可以。这就使得在进行PWA开发的时候,数据存储的技术选型落在了indexedDB存储上面。

总结下就是,如果是浏览器主窗体线程开发,同时存储数据结构简单,例如,就存个true/false,显然localStorage上上选;如果数据结构比较复杂,同时对浏览器兼容性没什么要求,可以考虑使用indexedDB;如果是在Service Workers中开发应用,只能使用indexedDB数据存储.

五、结束语

indexedDB数据库的使用目前可以直接在http协议下使用,这个和cacheStorage缓存存储必须使用https协议不一样。所以就应用场景来讲,indexedDB数据库还是挺广的,考虑到IE10也支持,所以基本可以确定在实际项目中应用是绝对不成问题的。

例如,页面中一些不常变动的结构化数据,我们就可以使用indexedDB数据库存储在本地,有助于增强页面的交互性能。cacheStorage缓存页面,indexedDB数据库缓存数据,两者一结合而就可以实现百分百的离线开发,听上去还是很厉害的。

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

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

相关文章

关于axios同步获取数据的问题

axios同步获取数据 Axios介绍问题代码修改 总结 Axios介绍 Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 X…

Spring Boot 集成 Redisson 实现消息队列

包含组件内容 RedisQueue:消息队列监听标识RedisQueueInit:Redis队列监听器RedisQueueListener:Redis消息队列监听实现RedisQueueService:Redis消息队列服务工具 代码实现 RedisQueue import java.lang.annotation.ElementTyp…

第2章 方法

本书作者起初以为仅靠研究命令行工具和指标就能提高性能。他认为这样不对。他从头到尾读了一遍手册,看懂了缺页故障、上下文切换和其他各种系统指标的定义,但不知道如何处理它们:如何从发现信号到找到解决方案。 他注意到,每当出…

18、公司信贷管理|贷款额度的测算|贷款期限及其定价的设定逻辑!

银行在综合权衡贷款的第一还款来源和第二还款来源、风险和收益的基础上,应明确提出贷与不贷的意见。经调查审查同意的贷款,应提出最终的融资方案。 合理的融资方案既要有利于提升本行的竞争力,又要有利于控制贷款风险。完整的融资方案一般包…

计算机网络 --- 【2】计算机网络的组成、功能

目录 一、计算机网络的组成 1.1 从组成部分看 1.2 从工作方式看 1.3 从逻辑功能看 1.4 总结 二、计算机网络的功能 2.1 数据通信 2.2 资源共享​编辑 2.3 分布式处理 2.4 提高可靠性 2.5 负载均衡 一、计算机网络的组成 1.1 从组成部分看 我们举例分析计算机网络从…

【人工智能学习笔记】4_4 深度学习基础之生成对抗网络

生成对抗网络(Generative Adversarial Network, GAN) 一种深度学习模型,通过判别模型(Discriminative Model)和生成模型(Generative Model)的相互博弈学习,生成接近真实数据的数据分…

​ArcGIS Pro和ArcGIS的10大区别

本文来源:水经注GIS公众号 如果你经常使用ArcGIS 进行制图和分析,那么你一定听说过ArcGIS Pro,这款软件是Esri未来主打的一款桌面GIS软件,那么这款软件和ArcGIS相比有什么不同呢,这里为你列举了两款软件的10大区别&am…

文本分类场景下微调BERT

How to Fine-Tune BERT for Text Classification 论文《How to Fine-Tune BERT for Text Classification?》是2019年发表的一篇论文。这篇文章做了一些实验来分析了如何在文本分类场景下微调BERT,是后面网上讨论如何微调BERT时经常提到的论文。 结论与思路 先来看…

Sqoop 数据迁移

Sqoop 数据迁移 一、Sqoop 概述二、Sqoop 优势三、Sqoop 的架构与工作机制四、Sqoop Import 流程五、Sqoop Export 流程六、Sqoop 安装部署6.1 下载解压6.2 修改 Sqoop 配置文件6.3 配置 Sqoop 环境变量6.4 添加 MySQL 驱动包6.5 测试运行 Sqoop6.5.1 查看Sqoop命令语法6.5.2 测…

【数学建模】2024数学建模国赛经验分享

文章目录 一、关于我二、我的数模历程三、经验总结: 一、关于我 我的CSDN主页:https://gxdxyl.blog.csdn.net/ 2020年7月(大二结束的暑假)开始在CSDN写作: 阿里云博客专家: 接触的领域挺多的&#xff…

【Linux】常用指令(中)(附带基础指令的详细讲解、Linux的一些附加知识)

文章目录 前言1. Linux基础常用指令1.1 通配符 "*"1.2 man指令(重要)1.2.1 man指令的语法 1.3 何为"指令"?(附带知识)1.4 echo指令1.5 cat指令1.6 Linux下一切皆文件!1.6.1 ">" 输出重定向1.6.2…

Qt篇——Qt使用C++获取Windows电脑上所有外接设备的名称、物理端口位置等信息

我之前有发过一篇文章《Qt篇——获取Windows系统上插入的串口设备的物理序号》,文章中主要获取的是插入的USB串口设备的物理序号;而本篇文章则进行拓展,可以获取所有外接设备的相关信息(比如USB摄像头、USB蓝牙、USB网卡、其它一些…

分布式技术概览

文章目录 分布式技术1. 分布式数据库(Distributed Databases)2. 分布式文件系统(Distributed File Systems)3. 分布式哈希表(Distributed Hash Tables, DHTs)4. 分布式缓存(Distributed Caching…

面试必问:Java 类加载过程

java 类加载过程主要分为加载、链接和初始化三个阶段,六个关键步骤:加载、验证、准备、解析、初始化。 加载阶段(Loading) 加载时类加载的第一个过程,在这个阶段,将完成以下三件事情: 1&#…

基于Springboot的鲜花销售网站的设计与实现

项目描述 这是一款基于Springboot的鲜花销售网站的系统 模块描述 鲜花销售系统 1、用户 登录 在线注册 浏览商品 鲜花搜索 订购商品 查询商品详情 水果分类查看 水果加购物车 下单结算 填写收货地址 2、管理员 登录 用户管理 商品管理 订单管理 账户管理 截图

项目经理应该学习pmp还是cspm?

职场竞争激烈,项目管理专业人才在各个行业中的作用越来越凸显出来。在23年之前,我国关于通用项目管理人才的培养更多依赖于国外的PMP认证,缺少自主的认证评价标准和体系。 为了弥补这一空缺,基于国内的项目管理发展需求&#xff…

西门子博途零基础学PLC必会的100个指令

#西门子##PLC##自动化##工业自动化##编程##电工##西门子PLC##工业##制造业##数字化##电气##工程师# 工控人加入PLC工业自动化精英社群 工控人加入PLC工业自动化精英社群

OpenMV——色块追踪

Python知识: 1.给Python的列表赋值: 定义一个元组时就是 元组a (1,2,…) 元组中可以只有一个元素,但是就必须要加一个 “ , ” 如 a (2,) 而列表的定义和元组类似,只是把()换成[]: #那么下面的colour_1 ~ 3属于元组&#xf…

(计算机网络)运输层

一.运输层的作用 运输层:负责将数据统一的交给网络层 实质:进程在通信 TCP(有反馈)UDP(无反馈) 二.复用和分用 三. TCP和UDP的特点和区别 进程号--不是固定的 端口号固定--mysql--3306 端口--通信的终点 …

苹果的“AI茅”之路只走了一半

今年苹果发布会最大的亮点,也许是和华为“撞档”,又或者是替腾讯“发布”新手游,但肯定不是iPhone 16。 9月10日,苹果秋季新品发布会与华为见非凡品牌盛典相继举行,iPhone 16系列也与HUAWEI Mate XT同日发布。 不过&…