“我们不再需要下载并且安装软件.一个简单的web浏览器和一个可供使用的互联网就足以让我们在任何时间, 任何地点, 还有任何平台上使用任何web应用程序.”
web应用很酷, 但是相对于桌面应用来说, 它们有比较显著的弱点, 不能像原生应用一样存储, 速度较慢.事实上这种情况已经改变了, 就像上面一句话一样, Web应用程序正在逐渐占据用户.Web应用程序在后端支持的情况下已经可以做到类似原生应用的效果, 甚至更好.下面我们看一看前端存储方面的API, IndexedDB和FileSystem.(我顺便加上文件读写操作了)
IndexedDB
用户代理需要在本地存储大量对象, 以满足Web应用程序的离线数据需求.WebStorage 对于存储密钥对及其对应值非常有用.但是, 它不提供密钥的有序检索, 有效地对值进行搜索或存储密钥的重复值.
此规范提供了一个具体的API来执行高级键值数据管理, 这是多数复杂查询处理器的核心.它通过使用事务数据库来存储密钥及其相应的值(每个密钥一个或多个), 并提供以确定性顺序遍历密钥的方法.
这通常通过使用持久的B-树数据结构来实现, 该数据结构被认为对于插入和删除以及对大量数据记录的顺序遍历都是有效的.
API
IDBRequest接口
var request = indexedDB.open('AddressBook', 15); request.onsuccess = function(evt) {...}; request.onerror = function(evt) {...};
request.result,//当请求完成时, 返回结果, 或者undefined请求失败.如果请求仍未处理, 则抛出“ InvalidStateError” DOMException. request.error//当请求完成时, 返回错误(a DOMException);如果请求成功, 则返回null.如果请求仍未处理, 则抛出“ InvalidStateError” DOMException. request.source,//返回IDBObjectStore, IDBIndex或IDBCursor请求, 作出返回, 如果是一个open request则为0. request.transaction,//返回IDBTransaction请求是在内部进行的.如果这是一个open request, 则它在运行时返回upgrade transaction, 否则返回null. request.readyState,//返回"pending"直到请求完成, 然后返回"done".
open request:
- open request 是指使用的数据库连接中或删除中.此时 success 和 error、blocked 和 upgradeneeded 事件可能被触发.
- open request 的 source 总是 null. open request 的 transaction 是 null, 除非 upgradeneeded 事件已经被触发.
- open request 的 get the parent 算法返回 null.
- open request 在连接队列中处理.该队列包含与原点和名称关联的所有open request.添加到连接队列的请求按顺序处理, 并且每个请求必须在处理下一个请求之前运行完成.
- open request 可能在其他连接上被阻塞, 要求这些连接在请求完成之前关闭, 并允许进一步处理请求.
- 一个连接队列不是和事件循环相关的任务队列, 当请求被任何特定的外部处理浏览上下文. 事件传递到完成的 open request 仍然通过, 发出请求的、和事件循环相关的任务队列.
Event接口
interface IDBVersionChangeEvent : Event { readonly attribute unsigned long long oldVersion; readonly attribute unsigned long long? newVersion; }; dictionary IDBVersionChangeEventInit : EventInit { unsigned long long oldVersion = 0; unsigned long long? newVersion = null; };
oldVersion 属性的 getter 返回数据库的先前版本.
newVersion 属性的 getter 返回新版本数据库, 或 null 如果数据库被删除.
事件按照构造事件中的定义构建.
要在指定 oldVersion 和 newVersion 的目标上触发名为 e 的版本更改事件, 需要以下步骤:
- 让 event 成为使用 IDBVersionChangeEvent 创建事件的结果
- event 的 type 属性设置为 e.
- event 的 bubbles 和 cancelable 属性设置为 false.
- event 的 oldVersion 属性设置为 oldVersion.
- event 的 newVersion 属性设置为 newVersion.
- legacyOutputDidListenersThrowFlag 设置为 unset.
- dispatch event 的 taget 为 legacyOutputDidListenersThrowFlag.
- return legacyOutputDidListenersThrowFlag.
该方法的返回值并不总是被使用.
IDBFactory接口
数据库对象通过 IDBFactory 接口上的方法访问.实现此接口的单个 对象存在于支持索引DB操作的全局环境中.
partial interface WindowOrWorkerGlobalScope { [SameObject] readonly attribute IDBFactory indexedDB; };
indexedDB 属性为应用程序提供了访问索引数据库功能的机制.
定义
interface IDBFactory { [NewObject] IDBOpenDBRequest open(DOMString name, optional [EnforceRange] unsigned long long version); [NewObject] IDBOpenDBRequest deleteDatabase(DOMString name); short cmp(any first, any second); };
也就是下面这个意思
request = indexedDB.open(name), //尝试使用当前版本打开指定数据库的连接, 如果数据库不存在, 则尝试重新创建,并且会触发更新事件. request = indexedDB.open(name, version), //尝试使用指定的版本打开指定数据库的连接. //如果数据库已经存在一个较低的版本, 并且存在未响应versionchange事件的打开的连接, 则请求将被阻止, 直到它们全部关闭, 然后才会进行更新. //如果数据库已经存在更高版本, 则请求将失败. request = indexedDB.deleteDatabase(name), //尝试删除指定的数据库.如果数据库已经存在, 并且存在未响应的versionchange事件而打开的连接, 则请求将被阻止, 直到它们全部关闭.如果成功, 返回null. request = indexedDB.cmp(key1, key2), //将两个值作为键进行比较.返回-1如果KEY1之前KEY2, 1个如果KEY2先KEY1, 和0, 如果密钥是相等的. //如果任一输入不是有效的键, 则抛出一个“ DataError” .DOMException
IDBDatabase接口
IDBDatabase 接口表示到数据库的连接.
一个 IDBDatabase 对象不能被垃圾收集如果其相关联的连接的紧密挂起标志未设置, 它有一个或多个事件侦听器寄存器, 其类型为一个abort, error或versionchange.
如果一个 IDBDatabase 对象被垃圾收集, 关联的连接必须关闭.
定义
interface IDBDatabase : EventTarget { readonly attribute DOMString name; readonly attribute unsigned long long version; readonly attribute DOMStringList objectStoreNames; [NewObject] IDBTransaction transaction((DOMString or sequence<DOMString>) storeNames, optional IDBTransactionMode mode = "readonly"); void close(); [NewObject] IDBObjectStore createObjectStore(DOMString name, optional IDBObjectStoreParameters options); void deleteObjectStore(DOMString name); // Event handlers: attribute EventHandler onabort; attribute EventHandler onclose; attribute EventHandler onerror; attribute EventHandler onversionchange; }; dictionary IDBObjectStoreParameters { (DOMString or sequence<DOMString>)? keyPath = null; boolean autoIncrement = false; };
也就是下面这个意思
connection.name, //返回数据库的名称. connection.version, //返回数据库的版本. connection.objectStoreNames, //返回数据库中对象库的名称列表. store = connection.createObjectStore(name [, options]), //用给定的名称和选项 创建一个新的对象存储并返回一个新 IDBObjectStore 对象. //必须在更新事务中使用, 否则抛出“ InvalidStateError” DOMException connection.deleteObjectStore(name), //必须在更新事务中使用,否则抛出“ InvalidStateError” DOMException transaction = connection.transaction(scope [, mode = "readonly"]),//返回具有给定模式( "readonly"或"readwrite")和范围的新事务, 该范围可以是单个对象存储名称或名称数组.. connection.close(), //所有正在运行的事务完成后关闭连接.
IDBObjectStore 接口
IDBObjectStore接口表示一个对象存储句柄。
定义
interface IDBObjectStore { attribute DOMString name; readonly attribute any keyPath; readonly attribute DOMStringList indexNames; [SameObject] readonly attribute IDBTransaction transaction; readonly attribute boolean autoIncrement; [NewObject] IDBRequest put(any value, optional any key); [NewObject] IDBRequest add(any value, optional any key); [NewObject] IDBRequest delete(any query); [NewObject] IDBRequest clear(); [NewObject] IDBRequest get(any query); [NewObject] IDBRequest getKey(any query); [NewObject] IDBRequest getAll(optional any query, optional [EnforceRange] unsigned long count); [NewObject] IDBRequest getAllKeys(optional any query, optional [EnforceRange] unsigned long count); [NewObject] IDBRequest count(optional any query); [NewObject] IDBRequest openCursor(optional any query, optional IDBCursorDirection direction = "next"); [NewObject] IDBRequest openKeyCursor(optional any query, optional IDBCursorDirection direction = "next"); IDBIndex index(DOMString name); [NewObject] IDBIndex createIndex(DOMString name, (DOMString or sequence<DOMString>) keyPath, optional IDBIndexParameters options); void deleteIndex(DOMString name); }; dictionary IDBIndexParameters { boolean unique = false; boolean multiEntry = false; };
也就是下面这个意思
store.name, //返回 store 的名称。 store.name = newName, //将 store 的名称更新为newName。如果未在更新事务中调用,则抛出“ InvalidStateError” DOMException store.keyPath。 //返回 store 的关键路径,如果没有,则返回null。 list.indexNames。 //返回 store 索引名称的列表。 store.transaction。 //返回关联的 transaction. store.autoIncrement, //如果 store 有 keey generator, 则返回true,否则返回false.
//添加或更新记录在 store 给定 value 和 key。如果成功的话,request 的result将是key。 store.put(value [, key]), //如果有,现有的记录与键将被替换,或者同add store.add(value [, key]), //如果一个记录与键已经存在,请求将失败, store.delete(query), //删除 store 内匹配给定定键值的记录,成功则返回undefined。 store.clear(), //删除所有记录,成功则返回undefined。 store.get(query), //匹配给定键或键范围中的检索值的第一个记录。成功返回value,否则是undefined。 store.getKey(query), //同上 store.getAll(query [, count]), //同上 store.getAllKeys(query [, count]), //同上 store.count(query) //如果成功,请求的result将是对应的数量。 store.openKeyCursor([query [, direction = "next"]]), //打开一个光标,按方向排序。如果查询为空,在 store 内的所有记录都会匹配。 //如果成功返回IDBCursorWithValue在第一个匹配指向记录,否则是null。 store.index(name), //返回IDBIndex的索引名为name的store。 store.createIndex(name, keyPath [, options]), //使用给定名称,keyPath和options在store中 创建一个新索引并返回一个新索引,或者抛出一个“ InvalidStateError” DOMException。 store.deleteIndex(name)。 //删除在store里给定的name的index。或者抛出一个“ InvalidStateError” DOMException。
下面我不常用我就。。。。。。
- IDBIndex 接口(https://www.w3.org/TR/IndexedDB/#index-interface)
- IDBKeyRange 接口(https://www.w3.org/TR/IndexedDB/#keyrange)
- IDBCursor 接口(https://www.w3.org/TR/IndexedDB/#cursor-interface)
- IDBTransaction 接口(https://www.w3.org/TR/IndexedDB/#transaction)
实例
coditing.db.init=e=>{ let request=indexedDB.open("coditing.db",getDBVersion()); request.onerror=e=>{ coditing.os.message("coditing.db","error","init","indexedDB.open") } request.onupgradeneeded=e=>{ coditing.db=e.target.result; let db=coditing.db; if (!db.objectStoreNames.contains("user")) { db.createObjectStore("user",{ keyPath: "name",Value: "value" }); } if (!db.objectStoreNames.contains("file")) { db.createObjectStore("file",{ keyPath: "fileHash",Instance: "instance"}); } console.log("coditing.db version changed to " + db.version); }; request.onsuccess=e=>{ coditing.db=e.target.result; let db=coditing.db; /* This Part Define coditing.db methods */ /* *_ Version is for Index */ db.add=async (store,target)=>await new Promise((resolve,reject)=>{ let request=db.transaction(store,"readwrite").objectStore(store).add(target); request.onsuccess=e=>resolve(); request.onerror=e=>reject(3); }); db.get=async (store,KeyPath)=>await new Promise((resolve,reject)=>{ let request=db.transaction(store,"readwrite").objectStore(store).get(KeyPath); request.onsuccess=e=>resolve(e.target.result); request.onerror=e=>reject(2); }); db.getAll=async (store)=>await new Promise((resolve,reject)=>{ let request=db.transaction(store,"readwrite").objectStore(store).getAll(); request.onsuccess=e=>resolve(e.target.result); request.onerror=e=>reject(2); }); db.put=async (store,target)=>await new Promise((resolve,reject)=>{ let request=db.transaction(store,"readwrite").objectStore(store).put(target); request.onsuccess=e=>resolve(); request.onerror=e=>reject(3); }); db.change=async (store,KeyPath,which,newValue)=>await new Promise((resolve,reject)=>{ let request=db.transaction(store,"readwrite").objectStore(store).get(KeyPath); request.onsuccess=e=>{ let target=e.target.result target[which]=newValue; db.put(store,target).then(resolve).catch(reject); } request.onerror=e=>reject(2); }); db.delete=async (store,KeyPath)=>await new Promise((resolve,reject)=>{ let request=db.transaction(store,"readwrite").objectStore(store).delete(KeyPath) request.onsuccess=e=>resolve(); request.onerror=e=>reject(3); }); db.clear=async (store)=>await new Promise((resolve,reject)=>{ if(store) db.transaction(store,"readwrite").objectStore(store).clear(); else { db.close(); let request=indexedDB.deleteDatabase(db.name); request.onsuccess=e=>resolve(); request.onerror=e=>reject(3); } }); /* Define coditing.db methods End */ }; }
安全考虑
DNS欺骗攻击
由于潜在的DNS欺骗攻击,不能保证声称在某个域中的主机确实来自该域。为了缓解这种情况,网页可以使用TLS。使用TLS的页面可以确保只有使用TLS的页面具有将它们标识为来自同一个域的证书才能访问其数据库。
跨目录攻击
不同的作者共享一个主机名,例如托管内容的用户geocities.com,共享一组数据库。
没有限制通过路径名访问的功能。因此,建议共享主机上的作者避免使用这些功能,因为其他作者读取数据并覆盖它们将是微不足道的。
即使路径限制功能可用,通常的DOM脚本安全模型也会使绕过此保护并从任何路径访问数据变得微不足道。
实施风险
实施这些持久性存储功能的两个主要风险是让恶意站点从其他域中读取信息,并让恶意站点编写信息,然后从其他域中读取信息。
让第三方网站读取不应从其域中读取的数据会导致信息泄露。例如,用户的一个域的购物愿望清单可能被另一个域用于定向广告; 或者文字处理站点存储的用户正在进行的工作中的机密文档可以由竞争公司的网站进行检查。
让第三方网站将数据写入其他域的永久存储可能会导致信息欺骗,这同样危险。例如,一个敌对网站可能会将记录添加到用户的愿望清单中; 或恶意站点可以将用户的会话标识符设置为已知ID,然后该恶意站点可以使用该ID来跟踪用户在受害者站点上的行为。
因此,严格遵循本规范中描述的原点模型对用户安全至关重要。
如果使用起源或数据库名称来构建文件系统的持久性路径,则必须适当地转义它们以防止攻击者使用相对路径(例如)从其他源访问信息"../"。
持续存在风险
实际的实现会将数据保存到非易失性存储介质中。数据在被检索时会被存储和反序列化,但序列化格式的细节将是用户特定的。用户代理可能会随时间改变其序列化格式。例如,格式可能会更新以处理新的数据类型或提高性能。为了满足本规范的操作要求,实现必须以某种方式处理较旧的序列化格式。对旧数据的处理不当可能会导致安全问题。除了基本的序列化问题之外,序列化数据还可以编码在较新版本的用户代理中无效的假设。
RegExp类型就是一个实际的例子。该StructuredSerializeForStorage操作允许序列化的RegExp对象。一个典型的用户代理会将一个正则表达式编译成本地机器指令,并假设传入的数据是如何传递的以及返回的结果如何。如果这个内部状态作为存储到数据库的数据的一部分被序列化,那么当内部表示后来被反序列化时可能会出现各种问题。例如,数据传入代码的手段可能已经改变。编译器输出中的安全错误可能已在用户代理更新中被识别和修复,但仍处于序列化内部状态。
用户代理必须适当地识别和处理较旧的数据。一种方法是以序列化格式包含版本标识符,并在遇到较旧的数据时从脚本可见状态重建任何内部状态。
FileSystem
使用Filesystem API 我们可以在一个有层次的文件夹结构体即文件系统中互动和组织.
本地文件系统API包含了俩个不同的版本。异步API,对于一般的应用来说非常有用。同步API,特别为Web Worker设计(已放弃开发)。我们只讨论异步API。
该API分解成各种主题:
- 读取和处理文件:
File
/Blob
,FileList
,FileReader
- 创作和写作:
Blob()
,FileWriter
- 目录和文件系统访问:
DirectoryReader
,FileEntry
/DirectoryEntry
,LocalFileSystem
首先我们需要通过请求一个LocalFileSystem对象来得到HTML5文件系统的访问,
window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;//我写下这篇博客时只有Chrome支持这个API所以要加webkit前缀 window.requestFileSystem(type, size,successCallback[,errorCallback])
下面说明 requestFileSystem 参数的作用
type,//文件存储是否应该持久。可能的值是window.PERSISTENT(1)或者window.TEMPORARY(0)。/* TEMPORARY根据浏览器的判断,可以删除使用的数据(例如,如果需要更多空间)。 PERSISTENT除非用户或应用明确授权并且要求用户向您的应用授予配额,否则无法清除存储。 要使用PERSISTENT存储,您必须获得用户的许可才能存储持久数据。同样的限制不适用于TEMPORARY存储,因为浏览器可能会自行选择临时驱逐存储的数据。 要通过PERSISTENTFileSystem API 使用存储,要使用API navigator.webkitPersistentStorage.requestQuota 请求存储。*/ size,//应用程序需要存储的大小(以字节(bytes)为单位)。 successCallback,//在成功请求文件系统时调用的回调。它的参数是一个FileSystem对象。 errorCallback,//用于处理错误的可选回调或拒绝获取文件系统的请求。它的参数是一个FileError对象。 /*如果您是第一次使用requestFileSystem(),则会为您的应用程序创建新的存储空间。 记住这个文件系统是沙盒的,这意味着一个Web应用程序无法访问另一个应用程序的文件,这一点很重要。 这也意味着您无法将文件读取/写入用户硬盘上的任意文件夹(例如“我的图片”,“我的文档”等)。 文件系统得到这些方法依赖于最初包含的document。所有的document或者web应用来自于同一个最初来源共享一个文件系统。 两个document或者应用来自于不同的来源完全不同并且不可联系。一个文件系统严格被限制访问一个应用,不能访问另外一个应用保存的数据。同时也对于其它的文件独立。*/
演示
//临时内存 var onInitFs=fs=>console.log('Opened file system: ' + fs.name); var errorHandler=e=>console.log(e); window.requestFileSystem(window.TEMPORARY, 5*1024*1024 /*5MB*/, onInitFs, errorHandler); //或者 永久内存 navigator.webkitPersistentStorage.requestQuota(2 * 1024 * 1024,function (grantedBytes) { window.webkitRequestFileSystem(true, grantedBytes,function (fs) { window.fs = fs; },console.log); },console.log);
这里返回的 fs 就是一个 DOMFileSystem 对象
/* DOMFileSystem extends Object, Otherwise: name,//是一个字符串对象http_127.0.0.1_3000:Persistent root;//是一个DirectoryEntry对象
*/
为了方便我打字和你们理解,我直接把 fileSystem系列 的属性列出来。
/* Entry extends Object, Otherwise: name,//file or directory name like "123" fullPath,//path from the root like "/123" filesystem,//file system like "DOMFileSystem {...}" isFile,//true or false isDirectory,//true or false getMetadata(successCallback[,errorCallback]),//successCallback参数是Metadata对象 getParent(successCallback[,errorCallback]),//successCallback参数是一个DirectoryEntry对象,root的返回值为它本身 moveTo(Directory[,newName],successCallback[,errorCallback]),//successCallback参数是移动后的本身 copyTo(Directory[,newName],successCallback[,errorCallback]),//successCallback参数是复制后的本身 remove(successCallback[,errorCallback]),//删除文件或者文件夹 successCallback参数无 不能删除有内容的文件夹 toURL();//转换成URL并且可以在网页中使用 样式filesystem:http://127.0.0.1:3000/persistent/123.txt */ /* FileEntry extends Entry, Otherwise: file(successCallback[,errorCallback]),//successCallback参数是File对象 此文件对象是标准文件 createWriter(successCallback[,errorCallback]);//successCallback参数是FileWriter对象 */ /* DirectoryEntry extends Entry, Otherwise: createReader(),//创建一个DirectoryReader可用于读取此目录中条目的对象。 getDirectory(path,flag,successCallback[,errorCallback]),//successCallback参数是一个DirectoryEntry对象 flag{create:bool,exclusive:bool} exclusive表示之前是否存在 getFile("name",flag,successCallback[,errorCallback]),//successCallback参数是一个File对象
removeRecursively(successCallback[,errorCallback]);//彻底删除文件夹,包括内容####被标记为弃用###chrome仍然可以使用 */ /* DirectoryReader extends Object, Otherwise: readEntries(successCallback[,errorCallback]);//successCallback参数是一个数组。数组中的项目是FileEntry或者DirectoryEntry */ /* Metadata extends Object, Otherwise: modificationTime,//一个带名字空对象,指示条目被修改的日期和时间 例如Wed May 02 2018 00:25:42 GMT+0800 (中国标准时间) {} size;//一个64位无符号整数,以字节为单位指示条目的大小 */
演示
为了方便,下面我用 fs 表示我们申请到的 fileSystem
//很显然,第一件我们需要做的事就是创建一些目录。虽然ROOT目录已经村存在,你不希望把所有的文件都保存在那里。 //文件夹使用DirectoryEntry对象来创建。在下面的例子中我们将在ROOT文件夹中创建一个文件夹:Documents fs.root.getDirectory('Documents', {create: true}, function(dirEntry) { }, errorHandler); //创建子目录与创建任何其他目录完全相同。但是,如果您尝试创建其直接父项不存在的目录,则API会引发错误。解决方案是按顺序创建每个目录,这对于使用异步API非常棘手。 //以下代码通过在其父文件夹创建后递归添加每个子目录,在应用程序的FileSystem的根目录中创建新的层次结构(music/genres/jazz)。 var path = 'music/genres/jazz/'; function createDir(rootDirEntry, folders) { // Throw out './' or '/' and move on to prevent something like '/foo/.//bar'. if (folders[0] == '.' || folders[0] == '') { folders = folders.slice(1); } rootDirEntry.getDirectory(folders[0], {create: true}, function(dirEntry) { // Recursively add the new subfolder (if we still have another to create). if (folders.length) { createDir(dirEntry, folders.slice(1)); } }, errorHandler); }; function onInitFs(fs) { createDir(fs.root, path.split('/')); // fs.root is a DirectoryEntry. } //现在已经有了“music/genres/jazz”,我们可以将其完整路径传递给getDirectory()并创建新的子文件夹或文件。例如: // ###需要注意### //create参数会重新创建文件,即便已经存在此文件 // ###需要注意### window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) { fs.root.getFile('/music/genres/jazz/song.mp3', {create: true}, function(fileEntry) { /*...*/ }, errorHandler); }, errorHandler); //接下来我们需要检查我们的文件系统。我们创建一个DirectoryReader对象,使用ReadEntries()方法来读取目录中的内容。 fs.root.getDirectory('Documents', {}, function(dirEntry){ var dirReader = dirEntry.createReader(); dirReader.readEntries(function(entries) { for(var i = 0; i < entries.length; i++) { var entry = entries[i]; if (entry.isDirectory){ console.log('Directory: ' + entry.fullPath); } else if (entry.isFile){ console.log('File: ' + entry.fullPath); } } }, errorHandler); }, errorHandler);
然后有文件就要操作文件啦
文件操作都是通过Blob的(File对象这里不讲,因为我们已经可以通过FileSystem产生文件了)
/* Blob extends Object, Otherwise: constructor(),//Blob([data],{type,endings}); size,//大小,整型 type,//类型,字符串 Blob.slice([start,[ end ,[contentType]]]);//返回一个新的 Blob对象,包含了源 Blob对象中指定范围内的数据。 //Blob构造器参数type举例:{type: "text/plain;charset=utf-8"}{type : 'application/json'} {type: "application/octet-binary"} //endings,默认值为"transparent",用于指定包含行结束符\n的字符串如何被写入。 它是以下两个值中的一个: // "native",代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者 "transparent",代表会保持blob中保存的结束符不变 */
读写操作使用 FileReader 和 FileWriter(前面说了怎么获得),我写下博客时[W3C File API: Writer(W3C Editor's Draft 07 March 2012)]目前并不适用,
FileReader有接口定义(这个很清楚)(是已经确定的标准)
[Constructor, Exposed=(Window,Worker)] interface FileReader: EventTarget { // async read methods void readAsArrayBuffer(Blob blob); void readAsBinaryString(Blob blob); void readAsText(Blob blob, optional DOMString label); void readAsDataURL(Blob blob); void abort(); // states const unsigned short EMPTY = 0; const unsigned short LOADING = 1; const unsigned short DONE = 2; readonly attribute unsigned short readyState; // File or Blob data readonly attribute (DOMString or ArrayBuffer)? result; readonly attribute DOMException? error; // event handler content attributes attribute EventHandler onloadstart; attribute EventHandler onprogress; attribute EventHandler onload; attribute EventHandler onabort; attribute EventHandler onerror; attribute EventHandler onloadend; };
然后还是我的注解
/* FileReader extends EventTarget, Otherwise: EMPTY:0, WRITING:1, DONE:2, abort(),//中断读取操作,停止读取必须调用 error,//最后一次出错的错误信息 onloadstart,//读取操作开始时触发 onprogress,//读取操作过程中触发 onloadend,//读取操作完成时触发(不管成功还是失败) onload,//读取操作成功时触发 onabort,//读取操作被中断时触发 onerror,//读取操作失败时触发 readyState,//读取工作的状态(DONE、EMPTY、WRITING) result,//读取的内容(DOMString or ArrayBuffer) readAsArrayBuffer(Blob),//参数是File或者Blob 返回ArrayBuffer对象 readAsBinaryString(Blob),//参数是File或者Blob 返回了字符串。。。。??? readAsDataURL(Blob),//参数是File或者Blob 结果是 "data:text/plain;base64,MTIz" 这种东西 readAsText(Blob[, encoding]);//参数是File或者Blob 还有编码默认为"utf-8" 结果是字符串 */ /* FileWriter extends EventTarget, Otherwise: INIT:0, WRITING:1, DONE:2, abort(),//中断写入操作,写完文件必须调用 error,//最后一次出错的错误信息 length,//文件长度(或在无权读取文件信息的情况下返回已写入的长度) onwritestart,//写入操作开始时触发 onprogress,//写入操作过程中触发 onwriteend,//写入操作完成时触发(不管成功还是失败) onwrite,//写入操作成功时触发 onabort,//写入操作被中断时触发 onerror,//写入操作失败时触发 position,//当前写入操作所处的位置 readyState,//保存工作的状态(DONE、INIT、WRITING) seek(offset),//设置position属性为offset,字节偏移 truncate(size),//在size处截断文件 write(Blob);/// 在position处写入blob数据 */
实例
coditing.fs.init=e=>{ window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem; navigator.webkitPersistentStorage.requestQuota(2 * 1024 * 1024,grantedBytes=>{ window.requestFileSystem(true, grantedBytes,requestFileSystemResult=>{ let fs=coditing.fs=requestFileSystemResult; fs.Name="coditing-FS"; fs.resolveURL=window.resolveLocalFileSystemURL || window.webkitResolveLocalFileSystemURL; fs.root.__proto__.removeRecursively_ = fs.root.__proto__.removeRecursively; fs.root.__proto__.removeRecursively=function(successCallback=()=>{},errorCallback=()=>{}){ let solveOne=async(e,i)=>await new Promise((resolve,reject)=>(e[i].isDirectory)?search(e[i]).then(()=>e[i].remove_(()=>resolve(i+1),reject)).catch(reject):e[i].remove_(()=>resolve(i+1),reject)); let search=async Directory=>await new Promise((resolve,reject)=>{ Directory.createReader().readEntries(e=>{ let solveNext=(e,i)=>(i<e.length)?solveOne(e,i).then(i=>solveNext(e,i)).catch(console.log):resolve(); solveNext(e,0); }); }); search(this).then(this.remove_(successCallback,(this==fs.root)?successCallback:errorCallback)).catch(errorCallback); }; fs.root.__proto__.removeRecursively_ = fs.root.__proto__.removeRecursively_ || fs.root.__proto__.removeRecursively; fs.CurrentDirectory=fs.root; fs.root.__proto__.getFile_=fs.root.__proto__.getFile; fs.root.__proto__.getDirectory_=fs.root.__proto__.getDirectory; fs.root.__proto__.getParent_=fs.root.__proto__.getParent; fs.root.__proto__.copyTo_=fs.root.__proto__.copyTo; fs.root.__proto__.moveTo_=fs.root.__proto__.moveTo; fs.root.__proto__.remove_=fs.root.__proto__.remove; fs.root.__proto__.getMetadata_=fs.root.__proto__.getMetadata; //get file or Directory fs.root.__proto__.getFile=async function(target){return await new Promise((resolve,reject)=>this.getFile_(target, {create:false,exclusive:false},resolve,reject))}; fs.getFile=target=>fs.root.__proto__.getFile.call(fs.CurrentDirectory,target); fs.root.__proto__.getDirectory=async function(target){return await new Promise((resolve,reject)=>this.getDirectory_(target, {create:false,exclusive:false},resolve,reject))}; fs.getDirectory=target=>fs.root.__proto__.getDirectory.call(fs.CurrentDirectory,target); fs.root.__proto__.getParent=async function(){return await new Promise((resolve,reject)=>this.getParent_(resolve,reject))}; fs.getParent=target=>fs.root.__proto__.getFile.call(fs.CurrentDirectory,target); fs.root.__proto__.copyTo=async function(target,newName){return await new Promise((resolve,reject)=>(typeof(target)!="string")?this.copyTo_(target,newName,resolve,reject):(this.isDirectory)?(this.getDirectory_(target,{create:false,exclusive:false},e=>this.copyTo_(e,newName,resolve,reject),reject)):(this.getParent_(e=>e.getDirectory_(target,{create:false,exclusive:false},e=>this.copyTo_(e,newName,resolve,reject),reject),reject)))}; fs.root.__proto__.moveTo=async function(target,newName){return await new Promise((resolve,reject)=>(typeof(target)!="string")?this.moveTo_(target,newName,resolve,reject):(this.isDirectory)?(this.getDirectory_(target,{create:false,exclusive:false},e=>this.moveTo_(e,newName,resolve,reject),reject)):(this.getParent_(e=>e.getDirectory_(target,{create:false,exclusive:false},e=>this.moveTo_(e,newName,resolve,reject),reject),reject)))}; fs.root.__proto__.remove=async function(target){return await new Promise((resolve,reject)=>this.removeRecursively(resolve,reject))}; fs.root.__proto__.getMetadata=async function(){return await new Promise((resolve,reject)=>this.getMetadata_(resolve,reject))}; let randomId=Math.random(); fs.root.getFile_(`###TemporaryFileForDefineFunction###${randomId}###`,{create:true},e=>{ e.__proto__.copyTo_=e.__proto__.copyTo; e.__proto__.moveTo_=e.__proto__.moveTo; e.__proto__.remove_=e.__proto__.remove; e.__proto__.getMetadata_=e.__proto__.getMetadata; e.__proto__.copyTo=fs.root.__proto__.copyTo; e.__proto__.moveTo=fs.root.__proto__.moveTo; e.__proto__.remove=async function(target){return await new Promise((resolve,reject)=>this.remove_(resolve,reject))}; e.__proto__.getMetadata=fs.root.__proto__.getMetadata; e.remove(); },console.log); /* This Part Define coditing.fs methods */ //get Memory Usage And Memory Quota fs.queryUsageAndQuota=async()=>await new Promise((resolve,reject)=>navigator.webkitPersistentStorage.queryUsageAndQuota((usage, quota)=>resolve([usage,quota]))); fs.df=fs.queryUsageAndQuota; //show or set current directory in fileSystem fs.root.__proto__.cd=async function(target){return await new Promise((resolve,reject)=>{ if((target=='\\')||(target=='/')) resolve(fs.CurrentDirectory=fs.root); else if((target=="..")||(target=="../")||(target=="..\\")) {this.getParent_(CurrentDirectory=>resolve(fs.CurrentDirectory=CurrentDirectory),reject)} else this.getDirectory_(target, {create:false,exclusive:false},CurrentDirectory=>resolve(fs.CurrentDirectory=CurrentDirectory),reject); })}; fs.cd=target=>fs.root.__proto__.cd.call(fs.CurrentDirectory,target); fs.pwd=async()=>await new Promise((resolve,reject)=>{resolve(fs.CurrentDirectory)}); //list your files or directories in current directory fs.root.__proto__.ls=async function(target){return await new Promise((resolve,reject)=>this.getDirectory_(target, {create:false,exclusive:false},e=>e.createReader().readEntries(resolve,reject),reject))}; fs.ls=fs.dir=target=>fs.root.__proto__.ls.call(fs.CurrentDirectory,target); //show your files or directories in current directory like tree fs.root.__proto__.tree=async function(){return await new Promise((resolve,reject)=>{ let Tree=[[],[],this.name];//Files And Directory And This Directory Name Tree.__proto__.isArray=true; let search=async (TreeDirectory,Directory)=>await new Promise((resolve,reject)=>{ Directory.createReader().readEntries(e=>{ for(let i=0;i<=e.length;i++){ if(i==e.length) return resolve(); if(e[i].isDirectory){ let NextDirectory=[[],[],e[i].name]; TreeDirectory[1].push(NextDirectory); search(NextDirectory,e[i]).then(resolve).catch(reject); }else{ TreeDirectory[0].push(e[i].name); } } },reject)}); search(Tree,this).then(()=>resolve(Tree)).catch(reject); })}; fs.tree=()=>fs.root.__proto__.tree.call(fs.CurrentDirectory); //Create a Directory fs.root.__proto__.md=fs.root.__proto__.mkdir=async function(target){return await new Promise((resolve,reject)=>this.getDirectory_(target, {create:true,exclusive:true},resolve,reject))}; fs.md=fs.mkdir=target=>fs.root.__proto__.md.call(fs.CurrentDirectory,target); //Create a file fs.root.__proto__.touch=async function(target){return await new Promise((resolve,reject)=>this.getFile_(target, {create:true,exclusive:true},resolve,reject))}; fs.touch=target=>fs.root.__proto__.touch.call(fs.CurrentDirectory,target); //copy your file or directory fs.root.__proto__.cp=fs.root.__proto__.copy=async function(source,target){return await new Promise((resolve,reject)=>{ let sourceDelims=source.lastIndexOf('/'); let targetDelims=target.lastIndexOf('/'); let targetName=target.substr(targetDelims+1); let find=(e,target,i)=>(i==e.length)?-1:(e[i].name==target)?i:find(e,target,i+1); this.getDirectory_(source.substr(0,sourceDelims),{create:false,exclusive:false},sp=>{sp.createReader().readEntries(s=>{ this.getDirectory_(target.substr(0,targetDelims),{create:false,exclusive:false},tp=>{tp.createReader().readEntries(t=>{ let si=find(s,source.substr(sourceDelims+1),0); let ti=find(t,targetName,0); (si==-1)?reject(2):(ti==-1)?s[si].copyTo_(tp,targetName,resolve,reject):s[si].copyTo_(t[ti],undefined,resolve,reject); },reject)},reject)},reject)},reject);})}; fs.cp=fs.copy=(source,target)=>fs.root.__proto__.cp.call(fs.CurrentDirectory,source,target); //move and rename file or directory fs.root.__proto__.mv=fs.root.__proto__.move=async function(source,target){return await new Promise((resolve,reject)=>{ let sourceDelims=source.lastIndexOf('/'); let targetDelims=target.lastIndexOf('/'); let targetName=target.substr(targetDelims+1); let find=(e,target,i)=>(i==e.length)?-1:(e[i].name==target)?i:find(e,target,i+1); this.getDirectory_(source.substr(0,sourceDelims),{create:false,exclusive:false},sp=>{sp.createReader().readEntries(s=>{ this.getDirectory_(target.substr(0,targetDelims),{create:false,exclusive:false},tp=>{tp.createReader().readEntries(t=>{ let si=find(s,source.substr(sourceDelims+1),0); let ti=find(t,targetName,0); (si==-1)?reject(2):(ti==-1)?s[si].moveTo_(tp,targetName,resolve,reject):s[si].moveTo_(t[ti],undefined,resolve,reject); },reject)},reject)},reject)},reject);})}; fs.mv=fs.move=(source,target)=>fs.root.__proto__.mv.call(fs.CurrentDirectory,source,target); //delete file or directory fs.root.__proto__.rm=fs.root.__proto__.delete=fs.root.__proto__.del=async function(target){return await new Promise((resolve,reject)=>{ let targetDelims=target.lastIndexOf('/'); let targetName=target.substr(targetDelims+1); let find=(e,target,i)=>(i==e.length)?-1:(e[i].name==target)?i:find(e,target,i+1); this.getDirectory_(target.substr(0,targetDelims),{create:false,exclusive:false},tp=>{tp.createReader().readEntries(t=>{ let ti=find(t,targetName,0); (ti==-1)?reject(2):t[ti].isDirectory?t[ti].removeRecursively_(resolve,reject):t[ti].remove_(resolve,reject); },reject)},reject)})}; fs.rm=fs.delete=fs.del=target=>fs.root.__proto__.rm.call(fs.CurrentDirectory,target); //format fileSystem fs.mkfs=fs.format=async()=>{return await new Promise((resolve,reject)=>{fs.root.removeRecursively(resolve,reject)})}; //find file fs.root.__proto__.find=async function(target){return await new Promise((resolve,reject)=>{ let result=[]; let search=async(Directory)=>await new Promise((resolve,reject)=>{ Directory.createReader().readEntries(e=>{ for(let i=0;i<=e.length;i++){ if(i==e.length) return resolve(); if(e[i].isDirectory){search(e[i]).then(resolve).catch(reject)}; if(e[i].name==target) result.push(e[i]); } },reject)}); search(this).then(()=>resolve(result)).catch(reject); })}; fs.find=target=>fs.root.__proto__.find.call(fs.CurrentDirectory,target); //show a file fs.root.__proto__.cat=fs.root.__proto__.type=async function(target){return await new Promise((resolve,reject)=>{ this.getFile_(target, {create:false,exclusive:false},e=>{e.file(e=>{ let reader = new FileReader(); reader.readAsText(e); reader.onload=e=>resolve(e.target.result); },reject)},reject)})}; fs.cat=fs.type=target=>fs.root.__proto__.cat.call(fs.CurrentDirectory,target); //edit file fs.vim=fs.edit=null; //write file fs.root.__proto__.write=async function(target,data){return await new Promise((resolve,reject)=>{ this.getFile_(target, {create:false,exclusive:false},f=>{f.getMetadata_(d=>{f.createWriter(e=>{ e.seek(d.size); e.write(new Blob([data])); e.onwrite=resolve; e.onwriteend=e.abort; e.onerror=reject; },reject)},reject)},reject)})}; fs.write=(target,data)=>fs.root.__proto__.write.call(fs.CurrentDirectory,target,data); //get entry By URL fs.wget=async URL=>await new Promise((resolve,reject)=>fs.resolveURL(URL,resolve,reject)); }, console.log) },console.log); //coditing.os.message(); /* Define coditing.fs methods End */ }
虽然我们只能在chrome下使用某些API,但是可见未来的发展趋势:Web端将可以做到任何本地端能做到的事。期待更好的明天。
参考文档:
Indexed Database API 2.0(W3C Recommendation, 30 January 2018) ------ https://www.w3.org/TR/file-system-api/W3C File API: Directories and System (W3C Editor's Draft 07 March 2012) ------ https://dev.w3.org/2009/dap/file-system/file-dir-sys.htmlFile API (W3C Working Draft, 26 October 2017) ------ https://www.w3.org/TR/FileAPI/