标签:
应用程序需要数据。对大多数Web应用程序来说,数据在服务器端组织和管理,客户端通过网络请求获取。随着浏览器变得越来越有能力,因此可选择在浏览器存储和操纵应用程序数据。
本文向你介绍名为IndexedDB的浏览器端文档数据库。使用lndexedDB,你可以通过惯于在服务器端数据库几乎相同的方式创建、读取、更新和删除大量的记录。请使用本文中可工作的代码版本去体验,完整的源代码可以通过GitHub库找到。
读到本教程的结尾时,你将熟悉IndexedDB的基本概念以及如何实现一个使用IndexedDB执行完整的CRUD操作的模块化JavaScript应用程序。让我们稍微亲近IndexedDB并开始吧。
什么是IndexedDB
图1:开发者工具查看一个object store
全部的IndexedDB API请参考完整文档
IndexedDB的架构很像在一些流行的服务器端NOSQL数据库实现中的设计典范类型。面向对象数据通过object stores(对象仓库)进行持久化,所有操作基于请求同时在事务范围内执行。事件生命周期使你能够控制数据库的配置,错误通过错误冒泡来使用API管理。
object store是IndexedDB数据库的基础。如果你使用过关系数据库,通常可以将object store等价于一个数据库表。Object stores包括一个或多个索引,在store中按照一对键/值操作,这提供一种快速定位数据的方法。
当你配置一个object store,你必须为store选择一个键。键在store中可以以“in-line”或“out-of-line”的方式存在。in-line键通过在数据对象上引用path来保障它在object store的唯一性。为了说明这一点,想想一个包括电子邮件地址属性Person对象。您可以配置你的store使用in-line键emailAddress,它能保证store(持久化对象中的数据)的唯一性。另外,out-of-line键通过独立于数据的值识别唯一性。在这种情况下,你可以把out-of-line键比作一个整数值,它(整数值)在关系数据库中充当记录的主键。
图1显示了任务数据保存在任务的object store,它使用in-line键。在这个案例中,键对应于对象的ID值。
不同于一些传统的关系数据库的实现,每一个对数据库操作是在一个事务的上下文中执行的。事务范围一次影响一个或多个object stores,你通过传入一个object store名字的数组到创建事务范围的函数来定义。
创建事务的第二个参数是事务模式。当请求一个事务时,必须决定是按照只读还是读写模式请求访问。事务是资源密集型的,所以如果你不需要更改data store中的数据,你只需要以只读模式对object stores集合进行请求访问。
清单2演示了如何使用适当的模式创建一个事务,并在这片文章的 Implementing Database-Specific Code 部分进行了详细讨论。
直到这里,有一个反复出现的主题,您可能已经注意到。对数据库的每次操作,描述为通过一个请求打开数据库,访问一个object store,再继续。IndexedDB API天生是基于请求的,这也是API异步本性指示。对于你在数据库执行的每次操作,你必须首先为这个操作创建一个请求。当请求完成,你可以响应由请求结果产生的事件和错误。
本文实现的代码,演示了如何使用请求打开数据库,创建一个事务,读取object store的内容,写入object store,清空object store。
IndexedDB使用事件生命周期管理数据库的打开和配置操作。图2演示了一个打开的请求在一定的环境下产生upgrade need事件。
图2:IndexedDB打开请求的生命周期
所有与数据库的交互开始于一个打开的请求。试图打开数据库时,您必须传递一个被请求数据库的版本号的整数值。在打开请求时,浏览器对比你传入的用于打开请求的版本号与实际数据库的版本号。如果所请求的版本号高于浏览器中当前的版本号(或者现在没有存在的数据库),upgrade needed事件触发。在uprade need事件期间,你有机会通过添加或移除stores,键和索引来操纵object stores。
如果所请求的数据库版本号和浏览器的当前版本号一致,或者升级过程完成,一个打开的数据库将返回给调用者。
当然,有时候,请求可能不会按预期完成。IndexedDB API通过错误冒泡功能来帮助跟踪和管理错误。如果一个特定的请求遇到错误,你可以尝试在请求对象上处理错误,或者你可以允许错误通过调用栈冒泡向上传递。这个冒泡天性,使得你不需要为每个请求实现特定错误处理操作,而是可以选择只在一个更高级别上添加错误处理,它给你一个机会,保持你的错误处理代码简洁。本文中实现的例子,是在一个高级别处理错误,以便更细粒度操作产生的任何错误冒泡到通用的错误处理逻辑。
也许在开发Web应用程序最重要的问题是:“浏览器是否支持我想要做的?“尽管浏览器对IndexedDB的支持在继续增长,采用率并不是我们所希望的那样普遍。图3显示了caniuse.com网站的报告,支持IndexedDB的为66%多一点点。最新版本的火狐,Chrome,Opera,Safar,iOS Safari,和Android完全支持IndexedDB,Internet Explorer和黑莓部分支持。虽然这个列表的支持者是令人鼓舞的,但它没有告诉整个故事。
图3:浏览器对IndexedDB的支持,来自caniuse.com
只有非常新版本的Safari和iOS Safari 支持IndexedDB。据caniuse.com显示,这只占大约0.01%的全球浏览器使用。IndexedDB不是一个你认为能够理所当然得到支持的现代Web API,但是你将很快会这样认为。
浏览器支持本地数据库并不是从IndexedDB才开始实现,它是在WebSQL实现之后的一种新方法。类似IndexedDB,WebSQL是一个客户端数据库,但它作为一个关系数据库的实现,使用结构化查询语言(SQL)与数据库通信。WebSQL的历史充满了曲折,但底线是没有主流的浏览器厂商对WebSQL继续支持。
如果WebSQL实际上是一个废弃的技术,为什么还要提它呢?有趣的是,WebSQL在浏览器里得到稳固的支持。Chrome, Safari, iOS Safari, and Android 浏览器都支持。另外,并不是这些浏览器的最新版本才提供支持,许多这些最新最好的浏览器之前的版本也可以支持。有趣的是,如果你为WebSQL添加支持来支持IndexedDB,你突然发现,许多浏览器厂商和版本成为支持浏览器内置数据库的某种化身。
因此,如果您的应用程序真正需要一个客户端数据库,你想要达到的最高级别的采用可能,当IndexedDB不可用时,也许您的应用程序可能看起来需要选择使用WebSQL来支持客户端数据架构。虽然文档数据库和关系数据库管理数据有鲜明的差别,但只要你有正确的抽象,就可以使用本地数据库构建一个应用程序。
现在最关键的问题:“IndexedDB是否适合我的应用程序?“像往常一样,答案是肯定的:“视情况而定。“首先当你试图在客户端保存数据时,你会考虑HTML5本地存储。本地存储得到广泛浏览器的支持,有非常易于使用的API。简单有其优势,但其劣势是无法支持复杂的搜索策略,存储大量的数据,并提供事务支持。
IndexedDB是一个数据库。所以,当你想为客户端做出决定,考虑你如何在服务端选择一个持久化介质的数据库。你可能会问自己一些问题来帮助决定客户端数据库是否适合您的应用程序,包括:
如果你对其中的任何问题回答了“是的”,很有可能,IndexedDB是你的应用程序的一个很好的候选。
现在,你已经有机会熟悉了一些的整体概念,下一步是开始实现基于IndexedDB的应用程序。第一个步骤需要统一IndexedDB在不同浏览器的实现。您可以很容易地添加各种厂商特性的选项的检查,同时在window对象上把它们设置为官方对象相同的名称。下面的清单展示了window.indexedDB,window.IDBTransaction,window.IDBKeyRange的最终结果是如何都被更新,它们被设置为相应的浏览器的特定实现。
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction; window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
现在,每个数据库相关的全局对象持有正确的版本,应用程序可以准备使用IndexedDB开始工作。
在本教程中,您将学习如何创建一个使用IndexedDB存储数据的模块化JavaScript应用程序。为了了解应用程序是如何工作的,参考图4,它描述了任务应用程序处于空白状态。从这里您可以为列表添加新任务。图5显示了录入了几个任务到系统的画面。图6显示如何删除一个任务,图7显示了正在编辑任务时的应用程序。
图4:空白的任务应用程序
这个例子从实现这样一个模块开始,它负责从数据库读取数据,插入新的对象,更新现有对象,删除单个对象和提供在一个object store删除所有对象的选项。这个例子实现的代码是通用的数据访问代码,您可以在任何object store上使用。
这个模块是通过一个立即执行函数表达式(IIFE)实现,它使用对象字面量来提供结构。下面的代码是模块的摘要,说明了它的基本结构。
(function (window) { ‘use strict‘; var db = { /* implementation here */ }; window.app = window.app || {}; window.app.db = db; }(window));
用这样的结构,可以使这个应用程序的所有逻辑封装在一个名为app的单对象上。此外,数据库相关的代码在一个叫做db的app子对象上。
这个模块的代码使用IIFE,通过传递window对象来确保模块的适当范围。使用use strict确保这个函数的代码函数是按照(javascript严格模式)严格编译规则。db对象作为与数据库交互的所有函数的主要容器。最后,window对象检查app的实例是否存在,如果存在,模块使用当前实例,如果不存在,则创建一个新对象。一旦app对象成功返回或创建,db对象附加到app对象。
本文的其余部分将代码添加到db对象内(在implementation here会评论),为应用程序提供特定于数据库的逻辑。因此,如你所见本文后面的部分中定义的函数,想想父db对象移动,但所有其他功能都是db对象的成员。完整的数据库模块列表见清单2。
对数据库的每个操作关联着一个先决条件,即有一个打开的数据库。当数据库正在被打开时,通过检查数据库版本来判断数据库是否需要任何更改。下面的代码显示了模块如何跟踪当前版本,object store名、某成员(保存了一旦数据库打开请求完成后的数据库当前实例)。
version: 1, objectStoreName: ‘tasks‘, instance: {},
在这里,数据库打开请求发生时,模块请求版本1数据库。如果数据库不存在,或者版本小于1,upgrade needed事件在打开请求完成前触发。这个模块被设置为只使用一个object store,所以名字直接定义在这里。最后,实例成员被创建,它用于保存一旦打开请求完成后的数据库当前实例。
接下来的操作是实现upgrade needed事件的事件处理程序。在这里,检查当前object store的名字来判断请求的object store名是否存在,如果不存在,创建object store。
upgrade: function (e) { var _db = e.target.result, names = _db.objectStoreNames, name = db.objectStoreName; if (!names.contains(name)) { _db.createObjectStore( name, { keyPath: ‘id‘, autoIncrement: true }); } },
在这个事件处理程序里,通过事件参数e.target.result来访问数据库。当前的object store名称的列表在_db.objectStoreName的字符串数组上。现在,如果object store不存在,它是通过传递object store名称和store的键的定义(自增,关联到数据的ID成员)来创建。
模块的下一个功能是用来捕获错误,错误在模块不同的请求创建时冒泡。
errorHandler: function (error) { window.alert(‘error: ‘ + error.target.code); debugger; },
在这里,errorHandler在一个警告框显示任何错误。这个函数是故意保持简单,对开发友好,当你学习使用IndexedDB,您可以很容易地看到任何错误(当他们发生时)。当你准备在生产环境使用这个模块,您需要在这个函数中实现一些错误处理代码来和你的应用程序的上下文打交道。
现在基础实现了,这一节的其余部分将演示如何实现对数据库执行特定操作。第一个需要检查的函数是open函数。
open: function (callback) { var request = window.indexedDB.open( db.objectStoreName, db.version); request.onerror = db.errorHandler; request.onupgradeneeded = db.upgrade; request.onsuccess = function (e) { db.instance = request.result; db.instance.onerror = db.errorHandler; callback(); }; },
open函数试图打开数据库,然后执行回调函数,告知数据库成功打开可以准备使用。通过访问window.indexedDB调用open函数来创建打开请求。这个函数接受你想打开的object store的名称和你想使用的数据库版本号。
一旦请求的实例可用,第一步要进行的工作是设置错误处理程序和升级函数。记住,当数据库被打开时,如果脚本请求比浏览器里更高版本的数据库(或者如果数据库不存在),升级函数运行。然而,如果请求的数据库版本匹配当前数据库版本同时没有错误,success事件触发。
如果一切成功,打开数据库的实例可以从请求实例的result属性获得,这个实例也缓存到模块的实例属性。然后,onerror事件设置到模块的errorHandler,作为将来任何请求的错误捕捉处理程序。最后,回调被执行来告知调用者,数据库已经打开并且正确地配置,可以使用了。
下一个要实现的函数是helper函数,它返回所请求的object store。
getObjectStore: function (mode) { var txn, store; mode = mode || ‘readonly‘; txn = db.instance.transaction( [db.objectStoreName], mode); store = txn.objectStore( db.objectStoreName); return store; },
在这里,getObjectStore接受mode参数,允许您控制store是以只读还是读写模式请求。对于这个函数,默认mode是只读的。
每个针对object store的操作都是在一个事物的上下文中执行的。事务请求接受一个object store名字的数组。这个函数这次被配置为只使用一个object store,但是如果你需要在事务中操作多个object store,你需要传递多个object store的名字到数组中。事务函数的第二个参数是一个模式。
一旦事务请求可用,您就可以通过传递需要的object store名字来调用objectStore函数以获得object store实例的访问权。这个模块的其余函数使用getObjectStore来获得object store的访问权。
下一个实现的函数是save函数,执行插入或更新操作,它根据传入的数据是否有一个ID值。
save: function (data, callback) { db.open(function () { var store, request, mode = ‘readwrite‘; store = db.getObjectStore(mode), request = data.id ? store.put(data) : store.add(data); request.onsuccess = callback; }); },
save函数的两个参数分别是需要保存的数据对象实例和操作成功后需要执行的回调。读写模式用于将数据写入数据库,它被传入到getObjectStore来获取object store的一个可写实例。然后,检查数据对象的ID成员是否存在。如果存在ID值,数据必须更新,put函数被调用,它创建持久化请求。否则,如果ID不存在,这是新数据,add请求返回。最后,不管put或者add 请求是否执行了,success事件处理程序需要设置在回调函数上,来告诉调用脚本,一切进展顺利。
下一节的代码在清单1所示。getAll函数首先打开数据库和访问object store,它为store和cursor(游标)分别设置值。为数据库游标设置游标变量允许迭代object store中的数据。data变量设置为一个空数组,充当数据的容器,它返回给调用代码。
在store访问数据时,游标遍历数据库中的每条记录,会触发onsuccess事件处理程序。当每条记录访问时,store的数据可以通过e.target.result事件参数得到。虽然实际数据从target.result的value属性中得到,首先需要在试图访问value属性前确保result是一个有效的值。如果result存在,您可以添加result的值到数据数组,然后在result对象上调用continue函数来继续迭代object store。最后,如果没有reuslt了,对store数据的迭代结束,同时数据传递到回调,回调被执行。
现在模块能够从data store获得所有数据,下一个需要实现的函数是负责访问单个记录。
get: function (id, callback) { id = parseInt(id); db.open(function () { var store = db.getObjectStore(), request = store.get(id); request.onsuccess = function (e){ callback(e.target.result); }; }); },
get函数执行的第一步操作是将id参数的值转换为一个整数。取决于函数被调用时,字符串或整数都可能传递给函数。这个实现跳过了对如果所给的字符串不能转换成整数该怎么做的情况的处理。一旦一个id值准备好了,数据库打开了和object store可以访问了。获取访问get请求出现了。请求成功时,通过传入e.target.result来执行回调。它(e.target.result)是通过调用get函数得到的单条记录。
现在保存和选择操作已经出现了,该模块还需要从object store移除数据。
‘delete‘: function (id, callback) { id = parseInt(id); db.open(function () { var mode = ‘readwrite‘, store, request; store = db.getObjectStore(mode); request = store.delete(id); request.onsuccess = callback; }); },
delete函数的名称用单引号,因为delete是JavaScript的保留字。这可以由你来决定。您可以选择命名函数为del或其他名称,但是delete用在这个模块为了API尽可能好的表达。
传递给delete函数的参数是对象的id和一个回调函数。为了保持这个实现简单,delete函数约定id的值为整数。您可以选择创建一个更健壮的实现来处理id值不能解析成整数的错误例子的回调,但为了指导原因,代码示例是故意的。
一旦id值能确保转换成一个整数,数据库被打开,一个可写的object store获得,delete函数传入id值被调用。当请求成功时,将执行回调函数。
在某些情况下,您可能需要删除一个object store的所有的记录。在这种情况下,您访问store同时清除所有内容。
deleteAll: function (callback) { db.open(function () { var mode, store, request; mode = ‘readwrite‘; store = db.getObjectStore(mode); request = store.clear(); request.onsuccess = callback; }); }
这里deleteAll函数负责打开数据库和访问object store的一个可写实例。一旦store可用,一个新的请求通过调用clear函数来创建。一旦clear操作成功,回调函数被执行。
现在所有特定于数据库的代码被封装在app.db模块中,用户界面特定代码可以使用此模块来与数据库交互。用户界面特定代码的完整清单(index.ui.js)可以在清单3中得到,完整的(index.html)页面的HTML源代码可以在清单4中得到。
随着应用程序的需求的增长,你会发现在客户端高效存储大量的数据的优势。IndexedDB是可以在浏览器中直接使用且支持异步事务的文档数据库实现。尽管浏览器的支持可能不能保障,但在合适的情况下,集成IndexedDB的Web应用程序具有强大的客户端数据的访问能力。
在大多数情况下,所有针对IndexedDB编写的代码是天然基于请求和异步的。官方规范有同步API,但是这种IndexedDB只适合web worker的上下文中使用。这篇文章发布时,还没有浏览器实现的同步格式的IndexedDB API。
一定要保证代码在任何函数域外对厂商特定的indexedDB, IDBTransaction, and IDBKeyRange实例进行了规范化且使用了严格模式。这允许您避免浏览器错误,当在strict mode下解析脚本时,它不会允许你对那些对象重新赋值。
你必须确保只传递正整数的版本号给数据库。传递到版本号的小数值会四舍五入。因此,如果您的数据库目前版本1,您试图访问1.2版本,upgrade-needed事件不会触发,因为版本号最终评估是相同的。
立即执行函数表达式(IIFE)有时叫做不同的名字。有时可以看到这样的代码组织方式,它称为self-executing anonymous functions(自执行匿名函数)或self-invoked anonymous functions(自调用匿名函数)。为进一步解释这些名称相关的意图和含义,请阅读Ben Alman的文章Immediately Invoked Function Expression (IIFE) 。
Listing 1: Implementing the getAll function
getAll: function (callback) { db.open(function () { var store = db.getObjectStore(), cursor = store.openCursor(), data = []; cursor.onsuccess = function (e) { var result = e.target.result; if (result && result !== null) { data.push(result.value); result.continue(); } else { callback(data); } }; }); },
// index.db.js ; window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction; window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange; (function(window){ ‘use strict‘; var db = { version: 1, // important: only use whole numbers! objectStoreName: ‘tasks‘, instance: {}, upgrade: function (e) { var _db = e.target.result, names = _db.objectStoreNames, name = db.objectStoreName; if (!names.contains(name)) { _db.createObjectStore( name, { keyPath: ‘id‘, autoIncrement: true }); } }, errorHandler: function (error) { window.alert(‘error: ‘ + error.target.code); debugger; }, open: function (callback) { var request = window.indexedDB.open( db.objectStoreName, db.version); request.onerror = db.errorHandler; request.onupgradeneeded = db.upgrade; request.onsuccess = function (e) { db.instance = request.result; db.instance.onerror = db.errorHandler; callback(); }; }, getObjectStore: function (mode) { var txn, store; mode = mode || ‘readonly‘; txn = db.instance.transaction( [db.objectStoreName], mode); store = txn.objectStore( db.objectStoreName); return store; }, save: function (data, callback) { db.open(function () { var store, request, mode = ‘readwrite‘; store = db.getObjectStore(mode), request = data.id ? store.put(data) : store.add(data); request.onsuccess = callback; }); }, getAll: function (callback) { db.open(function () { var store = db.getObjectStore(), cursor = store.openCursor(), data = []; cursor.onsuccess = function (e) { var result = e.target.result; if (result && result !== null) { data.push(result.value); result.continue(); } else { callback(data); } }; }); }, get: function (id, callback) { id = parseInt(id); db.open(function () { var store = db.getObjectStore(), request = store.get(id); request.onsuccess = function (e){ callback(e.target.result); }; }); }, ‘delete‘: function (id, callback) { id = parseInt(id); db.open(function () { var mode = ‘readwrite‘, store, request; store = db.getObjectStore(mode); request = store.delete(id); request.onsuccess = callback; }); }, deleteAll: function (callback) { db.open(function () { var mode, store, request; mode = ‘readwrite‘; store = db.getObjectStore(mode); request = store.clear(); request.onsuccess = callback; }); } }; window.app = window.app || {}; window.app.db = db; }(window));
// index.ui.js ; (function ($, Modernizr, app) { ‘use strict‘; $(function(){ if(!Modernizr.indexeddb){ $(‘#unsupported-message‘).show(); $(‘#ui-container‘).hide(); return; } var $deleteAllBtn = $(‘#delete-all-btn‘), $titleText = $(‘#title-text‘), $notesText = $(‘#notes-text‘), $idHidden = $(‘#id-hidden‘), $clearButton = $(‘#clear-button‘), $saveButton = $(‘#save-button‘), $listContainer = $(‘#list-container‘), $noteTemplate = $(‘#note-template‘), $emptyNote = $(‘#empty-note‘); var addNoTasksMessage = function(){ $listContainer.append( $emptyNote.html()); }; var bindData = function (data) { $listContainer.html(‘‘); if(data.length === 0){ addNoTasksMessage(); return; } data.forEach(function (note) { var m = $noteTemplate.html(); m = m.replace(/{ID}/g, note.id); m = m.replace(/{TITLE}/g, note.title); $listContainer.append(m); }); }; var clearUI = function(){ $titleText.val(‘‘).focus(); $notesText.val(‘‘); $idHidden.val(‘‘); }; // select individual item $listContainer.on(‘click‘, ‘a[data-id]‘, function (e) { var id, current; e.preventDefault(); current = e.currentTarget; id = $(current).attr(‘data-id‘); app.db.get(id, function (note) { $titleText.val(note.title); $notesText.val(note.text); $idHidden.val(note.id); }); return false; }); // delete item $listContainer.on(‘click‘, ‘i[data-id]‘, function (e) { var id, current; e.preventDefault(); current = e.currentTarget; id = $(current).attr(‘data-id‘); app.db.delete(id, function(){ app.db.getAll(bindData); clearUI(); }); return false; }); $clearButton.click(function(e){ e.preventDefault(); clearUI(); return false; }); $saveButton.click(function (e) { var title = $titleText.val(); if (title.length === 0) { return; } var note = { title: title, text: $notesText.val() }; var id = $idHidden.val(); if(id !== ‘‘){ note.id = parseInt(id); } app.db.save(note, function(){ app.db.getAll(bindData); clearUI(); }); }); $deleteAllBtn.click(function (e) { e.preventDefault(); app.db.deleteAll(function () { $listContainer.html(‘‘); addNoTasksMessage(); clearUI(); }); return false; }); app.db.errorHandler = function (e) { window.alert(‘error: ‘ + e.target.code); debugger; }; app.db.getAll(bindData); }); }(jQuery, Modernizr, window.app));
<!doctype html> <html lang="en-US"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Introduction to IndexedDB</title> <meta name="description" content="Introduction to IndexedDB"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs /font-awesome/4.1.0/css/font-awesome.min.css" > <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs /font-awesome/4.1.0/fonts/FontAwesome.otf" > <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs /font-awesome/4.1.0/fonts/fontawesome-webfont.eot" > <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs /font-awesome/4.1.0/fonts/fontawesome-webfont.svg" > <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs /font-awesome/4.1.0/fonts/fontawesome-webfont.ttf" > <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs /font-awesome/4.1.0/fonts/fontawesome-webfont.woff" > <style> h1 { text-align: center; color:#999; } ul li { font-size: 1.35em; margin-top: 1em; margin-bottom: 1em; } ul li.small { font-style: italic; } footer { margin-top: 25px; border-top: 1px solid #eee; padding-top: 25px; } i[data-id] { cursor: pointer; color: #eee; } i[data-id]:hover { color: #c75a6d; } .push-down { margin-top: 25px; } #save-button { margin-left: 10px; } </style> <script src="//cdnjs.cloudflare.com/ajax/libs/modernizr /2.8.2/modernizr.min.js" ></script> </head> <body class="container"> <h1>Tasks</h1> <div id="unsupported-message" class="alert alert-warning" style="display:none;"> <b>Aww snap!</b> Your browser does not support indexedDB. </div> <div id="ui-container" class="row"> <div class="col-sm-3"> <a href="#" id="delete-all-btn" class="btn-xs"> <i class="fa fa-trash-o"></i> Delete All</a> <hr/> <ul id="list-container" class="list-unstyled"></ul> </div> <div class="col-sm-8 push-down"> <input type="hidden" id="id-hidden" /> <input id="title-text" type="text" class="form-control" tabindex="1" placeholder="title" autofocus /><br /> <textarea id="notes-text" class="form-control" tabindex="2" placeholder="text"></textarea> <div class="pull-right push-down"> <a href="#" id="clear-button" tabindex="4">Clear</a> <button id="save-button" tabindex="3" class="btn btn-default btn-primary"> <i class="fa fa-save"></i> Save</button> </div> </div> </div> <footer class="small text-muted text-center">by <a href="http://craigshoemaker.net" target="_blank">Craig Shoemaker</a> <a href="http://twitter.com/craigshoemaker" target="_blank"> <i class="fa fa-twitter"></i></a> </footer> <script id="note-template" type="text/template"> <li> <i data-id="{ID}" class="fa fa-minus-circle"></i> <a href="#" data-id="{ID}">{TITLE}</a> </li> </script> <script id="empty-note" type="text/template"> <li class="text-muted small">No tasks</li> </script> <script src="//ajax.googleapis.com/ajax/libs /jquery/1.11.1/jquery.min.js"></script> <script src="index.db.js" type="text/javascript"></script> <script src="index.ui.js" type="text/javascript"></script> </body> </html>
标签:
原文地址:http://www.cnblogs.com/xiaohanghang/p/5865258.html