1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
|
---
title: 客户端存储
slug: Learn/JavaScript/Client-side_web_APIs/Client-side_storage
tags:
- API
- IndexedDB
- JavaScript
- 初学者
- 存储
- 文章
translation_of: Learn/JavaScript/Client-side_web_APIs/Client-side_storage
---
<p>{{LearnSidebar}}</p>
<div>{{PreviousMenu("Learn/JavaScript/Client-side_web_APIs/Video_and_audio_APIs", "Learn/JavaScript/Client-side_web_APIs")}}</div>
<p class="summary">现代web浏览器提供了很多在用户电脑web客户端存放数据的方法 — 只要用户的允许 — 可以在它需要的时候被重新获得。这样能让你存留的数据长时间保存, 保存站点和文档在离线情况下使用, 保留你对其站点的个性化配置等等。本篇文章只解释它们工作的一些很基础的部分。</p>
<table class="learn-box standard-table">
<tbody>
<tr>
<th scope="row">Prerequisites:</th>
<td>JavaScript 基础 (查看 <a href="/en-US/docs/Learn/JavaScript/First_steps">第一步</a>, <a href="/en-US/docs/Learn/JavaScript/Building_blocks">构建的块</a>, <a href="/en-US/docs/Learn/JavaScript/Objects">JavaScript 对象</a>), <a href="/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Introduction"> 基础的客户端 API</a></td>
</tr>
<tr>
<th scope="row">Objective:</th>
<td>学习如何使用客户端存储 API 来存储应用数据。</td>
</tr>
</tbody>
</table>
<h2 id="客户端存储">客户端存储?</h2>
<p>在其他的MDN学习中我们已经讨论过 静态网站(<a href="/en-US/docs/Learn/Server-side/First_steps/Client-Server_overview#Static_sites">static sites</a>) 和动态网站( <a href="/en-US/docs/Learn/Server-side/First_steps/Client-Server_overview#Dynamic_sites">dynamic sites</a>)的区别。 大多数现代的web站点是动态的— 它们在服务端使用各种类型的数据库来存储数据(服务端存储), 之后通过运行服务端( <a href="/en-US/docs/Learn/Server-side">server-side</a>) 代码来重新获取需要的数据,把其数据插入到静态页面的模板中,并且生成出HTML渲染到用户浏览上。</p>
<p>客户端存储以相同的原理工作,但是在使用上有一些不同。它是由 JavaScript APIs 组成的因此允许你在客户端存储数据 (比如在用户的机器上),而且可以在需要的时候重新取得需要的数据。这有很多明显的用处,比如:</p>
<ul>
<li>个性化网站偏好(比如显示一个用户选择的窗口小部件,颜色主题,或者字体)。</li>
<li>保存之前的站点行为 (比如从先前的session中获取购物车中的内容, 记住用户是否之前已经登陆过)。</li>
<li>本地化保存数据和静态资源可以使一个站点更快(至少让资源变少)的下载, 甚至可以在网络失去链接的时候变得暂时可用。</li>
<li>保存web已经生产的文档可以在离线状态下访问。</li>
</ul>
<p>通常客户端和服务端存储是结合在一起使用的。例如,你可以从数据库中下载一个由网络游戏或音乐播放器应用程序使用的音乐文件,将它们存储在客户端数据库中,并按需要播放它们。用户只需下载音乐文件一次——在随后的访问中,它们将从数据库中检索。</p>
<div class="note">
<p><strong>注意:</strong>使用客户端存储 API 可以存储的数据量是有限的(可能是每个API单独的和累积的总量);具体的数量限制取决于浏览器,也可能基于用户设置。有关更多信息,获取更多信息,请参考<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API/Browser_storage_limits_and_eviction_criteria">浏览器存储限制和清理标准</a>。</p>
</div>
<h3 id="传统方法:cookies">传统方法:cookies</h3>
<p>客户端存储的概念已经存在很长一段时间了。从早期的网络时代开始,网站就使用 <a href="/zh-CN/docs/Web/HTTP/Cookies">cookies</a> 来存储信息,以在网站上提供个性化的用户体验。它们是网络上最早最常用的客户端存储形式。<br>
因为在那个年代,有许多问题——无论是从技术上的还是用户体验的角度——都是困扰着 cookies 的问题。这些问题非常重要,以至于当第一次访问一个网站时,欧洲居民会收到消息,告诉他们是否会使用 cookies 来存储关于他们的数据,而这是由一项被称为<a href="/zh-CN/docs/Web/HTTP/Cookies#%E6%AC%A7%E7%9B%9FCookie%E6%8C%87%E4%BB%A4">欧盟 Cookie 条例</a>的欧盟法律导致的。</p>
<p><img alt="" src="https://mdn.mozillademos.org/files/15734/cookies-notice.png" style="display: block; margin: 0 auto;"></p>
<p>由于这些原因,我们不会在本文中教你如何使用cookie。毕竟它过时、存在各种<a href="/zh-CN/docs/Web/HTTP/Cookies#安全">安全问题</a>,而且无法存储复杂数据,而且有更好的、更现代的方法可以在用户的计算机上存储种类更广泛的数据。<br>
cookie的唯一优势是它们得到了非常旧的浏览器的支持,所以如果您的项目需要支持已经过时的浏览器(比如 Internet Explorer 8 或更早的浏览器),cookie可能仍然有用,但是对于大多数项目(很明显不包括本站)来说,您不需要再使用它们了。其实cookie也没什么好说的,<code><a href="/zh-CN/docs/Web/API/Document/cookie">document.cookie</a></code>一把梭就完事了。</p>
<div class="note">
<p>为什么仍然有新创建的站点使用 cookies?这主要是因为开发人员的习惯,使用了仍然使用cookies的旧库,以及存在许多web站点,提供了过时的参考和培训材料来学习如何存储数据。</p>
</div>
<h3 id="新流派:Web_Storage_和_IndexedDB">新流派:Web Storage 和 IndexedDB</h3>
<p>现代浏览器有比使用 cookies 更简单、更有效的存储客户端数据的 API。</p>
<ul>
<li><a href="/en-US/docs/Web/API/Web_Storage_API">Web Storage API</a> 提供了一种非常简单的语法,用于存储和检索较小的、由名称和相应值组成的数据项。当您只需要存储一些简单的数据时,比如用户的名字,用户是否登录,屏幕背景使用了什么颜色等等,这是非常有用的。</li>
<li><a href="/en-US/docs/Web/API/IndexedDB_API">IndexedDB API</a> 为浏览器提供了一个完整的数据库系统来存储复杂的数据。这可以用于存储从完整的用户记录到甚至是复杂的数据类型,如音频或视频文件。</li>
</ul>
<p>您将在下面了解更多关于这些 API 的信息。</p>
<h3 id="未来:Cache_API">未来:Cache API</h3>
<p>一些现代浏览器支持新的 {{domxref("Cache")}} API。这个API是为存储特定HTTP请求的响应文件而设计的,它对于像存储离线网站文件这样的事情非常有用,这样网站就可以在没有网络连接的情况下使用。缓存通常与 <a href="/en-US/docs/Web/API/Service_Worker_API">Service Worker API</a> 组合使用,尽管不一定非要这么做。<br>
Cache 和 Service Workers 的使用是一个高级主题,我们不会在本文中详细讨论它,尽管我们将在下面的 <a href="#离线文件存储">离线文件存储</a> 一节中展示一个简单的例子。</p>
<h2 id="存储简单数据_—_web_storage">存储简单数据 — web storage</h2>
<p><a href="/en-US/docs/Web/API/Web_Storage_API">Web Storage API</a> 非常容易使用 — 你只需存储简单的 键名/键值 对数据 (限制为字符串、数字等类型) 并在需要的时候检索其值。</p>
<h3 id="基本语法">基本语法</h3>
<p>让我们来告诉你怎么做:</p>
<ol>
<li>
<p>第一步,访问 GitHub 上的 <a href="https://mdn.github.io/learning-area/javascript/apis/client-side-storage/web-storage/index.html">web storage blank template</a> (在新标签页打开此<a href="https://mdn.github.io/learning-area/javascript/apis/client-side-storage/web-storage/index.html" rel="noopener"><font>模板</font></a>)。</p>
</li>
<li>
<p>打开你浏览器开发者工具的 JavaScript 控制台。</p>
</li>
<li>
<p>你所有的 web storage 数据都包含在浏览器内两个类似于对象的结构中: {{domxref("Window.sessionStorage", "sessionStorage")}} 和 {{domxref("Window.localStorage", "localStorage")}}。 第一种方法,只要浏览器开着,数据就会一直保存 (关闭浏览器时数据会丢失) ,而第二种会一直保存数据,甚至到浏览器关闭又开启后也是这样。我们将在本文中使用第二种方法,因为它通常更有用。</p>
</li>
<li>
<p>{{domxref("Storage.setItem()")}} 方法允许您在存储中保存一个数据项——它接受两个参数:数据项的名字及其值。试着把它输入到你的JavaScript控制台(如果你愿意的话,可以把它的值改为你自己的名字!)</p>
<pre class="brush: js notranslate">localStorage.setItem('name','Chris');</pre>
</li>
<li>
<p>{{domxref("Storage.getItem()")}} 方法接受一个参数——你想要检索的数据项的名称——并返回数据项的值。现在将这些代码输入到你的JavaScript控制台:</p>
<pre class="brush: js notranslate">var myName = localStorage.getItem('name');
myName</pre>
<p>在输入第二行时,您应该会看到 <code>myName</code>变量现在包含<code>name</code>数据项的值。</p>
</li>
<li>
<p>{{domxref("Storage.removeItem()")}} 方法接受一个参数——你想要删除的数据项的名称——并从 web storage 中删除该数据项。在您的JavaScript控制台中输入以下几行:</p>
<pre class="brush: js notranslate">localStorage.removeItem('name');
var myName = localStorage.getItem('name');
myName</pre>
<p>第三行现在应该返回 <code>null</code> — <code>name</code> 项已经不存在于 web storage 中。</p>
</li>
</ol>
<h3 id="数据会一直存在!">数据会一直存在!</h3>
<p>web storage 的一个关键特性是,数据在不同页面加载时都存在(甚至是当浏览器关闭后,对localStorage的而言)。让我们来看看这个:</p>
<ol>
<li>
<p>再次打开我们的 Web Storage 空白模板,但是这次你要在不同的浏览器中打开这个教程!这样可以更容易处理。</p>
</li>
<li>
<p>在浏览器的 JavaScript 控制台中输入这几行:</p>
<pre class="brush: js notranslate">localStorage.setItem('name','Chris');
var myName = localStorage.getItem('name');
myName</pre>
<p>你应该看到 name 数据项返回。</p>
</li>
<li>
<p>现在关掉浏览器再把它打开。</p>
</li>
<li>
<p>再次输入下面几行:</p>
<pre class="brush: js notranslate">var myName = localStorage.getItem('name');
myName</pre>
<p>你应该看到,尽管浏览器已经关闭,然后再次打开,但仍然可以使用该值。</p>
</li>
</ol>
<h3 id="为每个域名分离储存">为每个域名分离储存</h3>
<p>每个域都有一个单独的数据存储区(每个单独的网址都在浏览器中加载). 你 会看到,如果你加载两个网站(例如google.com和amazon.com)并尝试将某个项目存储在一个网站上,该数据项将无法从另一个网站获取。</p>
<p>这是有道理的 - 你可以想象如果网站能够查看彼此的数据,就会出现安全问题!</p>
<h3 id="更复杂的例子">更复杂的例子</h3>
<p><font>让我们通过编写一个简单的工作示例来应用这些新发现的知识,让你了解如何使用网络存储。</font><font>我们的示例将允许你输入一个名称,然后该页面将刷新,以提供个性化问候。</font><font>这种状态也会页面/浏览器重新加载期间保持,因为这个名称存储在Web Storage 中。</font></p>
<p>你可以在 <a href="https://github.com/mdn/learning-area/blob/master/javascript/apis/client-side-storage/web-storage/personal-greeting.html">personal-greeting.html</a> 中找到示例文件 —— 这包含一个具有标题,内容和页脚,以及用于输入您的姓名的表单的简单网站。</p>
<p><img alt="" src="https://mdn.mozillademos.org/files/15735/web-storage-demo.png" style="display: block; margin: 0 auto;"></p>
<p>让我们来构建示例,以便了解它的工作原理。</p>
<ol>
<li>
<p>首先,在您的计算机上的新目录中创建一个 <a href="https://github.com/mdn/learning-area/blob/master/javascript/apis/client-side-storage/web-storage/personal-greeting.html">personal-greeting.html</a> 文件的副本。</p>
</li>
<li>
<p><font><font>接下来,请注意我们的HTML如何引用一个名为</font></font><code>index.js</code><font><font>的 JavaScript 文件</font></font><font><font>(请参见第40行)。</font><font>我们需要创建它并将 JavaScript 代码写入其中。</font></font><font><font>在与HTML文件相同的目录中</font><font>创建一个</font></font><code>index.js</code><font><font>文件。</font></font> </p>
</li>
<li>
<p><font>我们首先创建对所有需要在此示例中操作的HTML功能的引用 - 我们将它们全部创建为常量,因为这些引用在应用程序的生命周期中不需要更改。</font><font>将以下几行添加到你的 JavaScript 文件中:</font></p>
<pre class="brush: js notranslate">// 创建所需的常量
const rememberDiv = document.querySelector('.remember');
const forgetDiv = document.querySelector('.forget');
const form = document.querySelector('form');
const nameInput = document.querySelector('#entername');
const submitBtn = document.querySelector('#submitname');
const forgetBtn = document.querySelector('#forgetname');
const h1 = document.querySelector('h1');
const personalGreeting = document.querySelector('.personal-greeting');</pre>
</li>
<li>
<p><font>接下来,我们需要包含一个小小的事件监听器,以在按下提交按钮时阻止实际的提交表单动作自身,因为这不是我们想要的行为。</font><font>在您之前的代码下添加此代码段:</font> 在你之前的代码后添加这段代码:</p>
<pre class="brush: js notranslate">// 当按钮按下时阻止表单提交
form.addEventListener('submit', function(e) {
e.preventDefault();
});</pre>
</li>
<li>
<p><font><font>现在我们需要添加一个事件监听器,当单击“Say hello”按钮时,它的处理函数将会运行。</font><font>这些注释详细解释了每一处都做了什么,但实际上我们在这里获取用户输入到文本输入框中的名字并使用</font></font><code>setItem()</code><font><font>将它保存在网络存储中</font></font><font><font>,然后运行一个名为</font></font><code>nameDisplayCheck()</code><font><font>的函数</font></font><font><font>来处理实际的网站文本的更新。将此添加到代码的底部:</font></font> </p>
<pre class="brush: js notranslate">// run function when the 'Say hello' button is clicked
submitBtn.addEventListener('click', function() {
// store the entered name in web storage
localStorage.setItem('name', nameInput.value);
// run nameDisplayCheck() to sort out displaying the
// personalized greetings and updating the form display
nameDisplayCheck();
});</pre>
</li>
<li>
<p><font><font>此时,我们还需要一个事件处理程序,以便在单击“</font></font>Forget<font><font>”按钮时运行一个函数——且仅在单击“Say hello”按钮(两种表单状态来回切换)后才显示。在这个功能中,我们</font></font><font><font>使用</font></font><code>removeItem()</code><font><font>从网络存储中</font><font>删除</font><font>项目</font></font><code>name</code><font><font>,然后再次运行</font></font><code>nameDisplayCheck()</code><font><font>以更新显示。</font><font>将其添加到底部:</font></font> </p>
<pre class="brush: js notranslate">// run function when the 'Forget' button is clicked
forgetBtn.addEventListener('click', function() {
// Remove the stored name from web storage
localStorage.removeItem('name');
// run nameDisplayCheck() to sort out displaying the
// generic greeting again and updating the form display
nameDisplayCheck();
});</pre>
</li>
<li>
<p><font><font>现在是时候定义</font></font><code>nameDisplayCheck()</code><font><font>函数本身了。</font><font>在这里,我们通过使用</font></font><code>localStorage.getItem('name')</code><font><font>作为测试条件</font><font>来检查name 数据项是否已经存储在Web Storage 中</font><font>。</font><font>如果它已被存储,则该调用的返回值为</font></font><code>true</code><font><font>; </font><font>如果没有,它会是</font></font><code>false</code><font><font>。</font><font>如果是</font></font><code>true</code><font><font>,我们会显示个性化问候语,显示表格的“</font></font>forget<font><font>”部分,并隐藏表格的“</font></font>Say hello<font><font>”部分。</font><font>如果是</font></font><code>false</code><font><font>,我们会显示一个通用问候语,并做相反的事。</font><font>再次将下面的代码添到底部:</font></font></p>
<pre class="brush: js notranslate">// define the nameDisplayCheck() function
function nameDisplayCheck() {
// check whether the 'name' data item is stored in web Storage
if(localStorage.getItem('name')) {
// If it is, display personalized greeting
let name = localStorage.getItem('name');
h1.textContent = 'Welcome, ' + name;
personalGreeting.textContent = 'Welcome to our website, ' + name + '! We hope you have fun while you are here.';
// hide the 'remember' part of the form and show the 'forget' part
forgetDiv.style.display = 'block';
rememberDiv.style.display = 'none';
} else {
// if not, display generic greeting
h1.textContent = 'Welcome to our website ';
personalGreeting.textContent = 'Welcome to our website. We hope you have fun while you are here.';
// hide the 'forget' part of the form and show the 'remember' part
forgetDiv.style.display = 'none';
rememberDiv.style.display = 'block';
}
}</pre>
</li>
<li>
<p><font><font>最后但同样重要的是,我们需要在</font></font><font><font>每次加载页面时</font><font>运行</font></font><code>nameDisplayCheck()</code><font><font>函数。</font><font>如果我们不这样做,那么个性化问候不会在页面重新加载后保持。</font><font>将以下代码添加到代码的底部:</font></font></p>
<pre class="brush: js notranslate">document.body.onload = nameDisplayCheck;</pre>
</li>
</ol>
<p><font>你的例子完成了 - 做得好!</font><font>现在剩下的就是保存你的代码并在浏览器中测试你的HTML页面。你可以在这里看到我们的</font><a href="https://mdn.github.io/learning-area/javascript/apis/client-side-storage/web-storage/personal-greeting.html" rel="noopener"><font><font>完成版本并在线运行</font></font></a><font><font>。</font></font></p>
<div class="note">
<p><strong>注意</strong>: 在 <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API">Using the Web Storage API</a> 中还有一个稍微复杂点儿的示例。</p>
</div>
<div class="note">
<p><strong>注意</strong>: 在完成版本的源代码中, <code><script src="index.js" defer></script></code> 一行里, <code>defer</code> 属性指明<font>在页面加载完成之前,</font>{{htmlelement("script")}}<font>元素的内容</font><font>不会执行。</font></p>
</div>
<h2 id="存储复杂数据_—_IndexedDB">存储复杂数据 — IndexedDB</h2>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API"><font><font>IndexedDB API</font></font></a><font><font>(有时简称 IDB )是可以在浏览器中访问的一个完整的数据库系统,在这里,你可以存储复杂的关系数据。其种类不限于像字符串和数字这样的简单值。你可以在一个</font></font>IndexedDB中<font><font>存储视频,图像和许多其他的内容。</font></font></p>
<p><font>但是,这确实是有代价的:使用IndexedDB要比Web Storage API复杂得多。</font><font>在本节中,我们仅仅只能浅尝辄止地一提它的能力,不过我们会给你足够基础知识以帮助你开始。</font></p>
<h3 id="通过一个笔记存储示例演示">通过一个笔记存储示例演示</h3>
<p>在这里,我们将向您介绍一个示例,该示例允许您在浏览器中存储笔记并随时查看和删除它们,在我们进行时,我们将解释IDB的最基本部分并让您自己构建注释。</p>
<p>这个应用看起来像这样:</p>
<p><img alt="" src="https://mdn.mozillademos.org/files/15744/idb-demo.png" style="border-style: solid; border-width: 1px; display: block; margin: 0px auto;"></p>
<p>每个笔记都有一个标题和一些正文,每个都可以单独编辑。我们将在下面通过的JavaScript代码提供详细的注释,以帮助您了解正在发生的事情。</p>
<h3 id="开始">开始</h3>
<p>1、首先,将 <code><a href="https://github.com/mdn/learning-area/blob/master/javascript/apis/client-side-storage/indexeddb/notes/index.html">index.html</a></code>, <code><a href="https://github.com/mdn/learning-area/blob/master/javascript/apis/client-side-storage/indexeddb/notes/style.css">style.css</a></code>, 和 <code><a href="https://github.com/mdn/learning-area/blob/master/javascript/apis/client-side-storage/indexeddb/notes/index-start.js">index-start.js</a></code> 文件的本地副本放入本地计算机上的新目录中。 </p>
<p>2、浏览这些文件。 您将看到HTML非常简单:具有页眉和页脚的网站,以及包含显示注释的位置的主内容区域,以及用于在数据库中输入新注释的表单。 CSS提供了一些简单的样式,使其更清晰。 JavaScript文件包含五个声明的常量,其中包含对将显示注释的 {{htmlelement("ul")}} 元素的引用,标题和正文 {{htmlelement("input")}} 元素,{{htmlelement("form")}}本身,以及{{htmlelement("button")}}。</p>
<p>3、将您的JavaScript文件重命名为 <code>index.js</code> 。 您现在可以开始向其添加代码了。</p>
<h3 id="数据库初始设置">数据库初始设置</h3>
<p>现在让我们来看看为了建立数据库必须首先要做什么。</p>
<ol>
<li>
<p>在常量声明下,加入这几行:</p>
<pre class="brush: js notranslate">// Create an instance of a db object for us to store the open database in
let db;</pre>
<p>这里我们声明了一个叫 <code>db</code> 的变量 — 这将在之后被用来存储一个代表数据库的对象。我们将在几个地方使用它,所以我们为了方便使用而在这里把它声明为全局的。</p>
</li>
<li>
<p>接着,在你的代码最后添加如下代码:</p>
<pre class="brush: js notranslate">window.onload = function() {
};</pre>
<p>我们将把所有的后续代码写在这个 <code>window.onload</code> 事件处理函数内,这个函数将在window的{{event("load")}}事件被触发时调用,为了确保我们没有在应用完整加载前试图使用IndexedDB功能(如果我们不这么做,它会失败)。</p>
</li>
<li>
<p><font><font>在</font></font><code>window.onload</code><font><font>处理程序内,添加以下内容:</font></font></p>
<pre class="brush: js notranslate">// Open our database; it is created if it doesn't already exist
// (see onupgradeneeded below)
let request = window.indexedDB.open('notes', 1);</pre>
<p><font><font>此行创建一个</font></font> <code>request</code> <font><font>变量,目的是打开</font></font> <code>notes</code><font><font>数据库的</font></font> <code>1</code><font><font>版本</font></font><font><font>。</font><font>如果<code>notes</code>数据库不存在,则后续代码将为您创建。</font><font>您将在IndexedDB中经常看到此请求模式。</font><font>数据库操作需要时间。</font><font>您不希望在等待结果时挂起浏览器,因此数据库操作是</font></font><a href="https://developer.mozilla.org/en-US/docs/Glossary/asynchronous" title="异步:异步是指通信环境,其中每个方在方便或可能时而不是立即接收和处理消息。"><font><font>异步的</font></font></a><font><font>,这意味着它们不会立即发生,而是在将来的某个时刻发生,并且在完成后会收到通知。</font></font></p>
<p><font><font>要在IndexedDB中处理此问题,您需要创建一个请求对象(可以随意命名 - </font></font>命名为<code>request</code>,<font><font>可以表明它的用途)。</font><font>然后,在请求完成或者失败时,使用事件处理程序来运行代码,您将在下面看到这些代码。</font></font></p>
<div class="note">
<p><strong><font><font>注意</font></font></strong><font><font>:版本号很重要。</font><font>如果要升级数据库(例如:更改表结构),则必须使用增加的版本号或者</font></font><code>onupgradeneeded</code><font><font>处理程序</font><font>内指定的不同模式</font><font>(请参阅下文)等</font><font>再次运行代码</font><font>。在这个简单教程中,我们不讨论数据库升级。</font></font></p>
</div>
<ol>
<li>
<p><font><font>在之前添加的事件处理程序下方添加以下代码 - 在</font></font><code>window.onload</code><font><font>处理程序内:</font></font></p>
<pre class="brush: js notranslate">// onerror handler signifies that the database didn't open successfully
request.onerror = function() {
console.log('Database failed to open');
};
// onsuccess handler signifies that the database opened successfully
request.onsuccess = function() {
console.log('Database opened successfully');
// Store the opened database object in the db variable. This is used a lot below
db = request.result;
// Run the displayData() function to display the notes already in the IDB
displayData();
};</pre>
<p><font><font>如果系统返回:请求失败,</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBRequest/onerror" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>request.onerror</code></a><font><font>将会运行。</font><font>这将允许你对这个问题做出响应。</font><font>在我们的简单示例中,只是将消息打印到JavaScript控制台。</font></font></p>
<p>如果系统返回:请求成功,<font><font>表明成功打开数据库</font></font> ,<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBRequest/onsuccess" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>request.onsuccess</code></a><font><font>将会运行。如果是这种情况,则表示已打开数据库的对象在</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBRequest/result" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>request.result</code></a><font><font>属性中</font><font>变为可用</font><font>,从而允许我们操作数据库。</font><font>我们将它存储在</font></font><code>db</code><font><font>我们之前创建</font><font>的</font><font>变量中供以后使用。</font><font>我们还运行一个名为</font></font><code>displayData()</code>的自定义函数<font><font>,它把数据库中的数据显示在</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/ul" title="The HTML <ul> 元素 ( 或 HTML 无序列表元素) 代表多项的无序列表,即无数值排序项的集合,且它们在列表中的顺序是没有意义的。通常情况下,无序列表项的头部可以是几种形式,如一个点,一个圆形或方形。头部的风格并不是在页面的HTML描述定义, 但在其相关的CSS 可以用 list-style-type 属性。"><code><ul></code></a><font><font>。</font><font>我们现在运行它,以便在页面加载时显示数据库中已有的注释。</font><font>您将在稍后看到此定义。</font></font></p>
</li>
</ol>
</li>
<li>
<p><font><font>最后,对于本节,我们可能会添加最重要的事件处理程序来设置数据库:</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBOpenDBRequest/onupgradeneeded" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>request.onupgradeneeded</code></a><font><font>。</font><font>如果尚未设置数据库,或者使用比现有存储数据库更大的版本号打开数据库(执行升级时),则运行此处理程序。</font><font>在上一个处理程序下面添加以下代码:</font></font></p>
<pre class="brush: js notranslate">// Setup the database tables if this has not already been done
request.onupgradeneeded = function(e) {
// Grab a reference to the opened database
let db = e.target.result;
// Create an objectStore to store our notes in (basically like a single table)
// including a auto-incrementing key
let objectStore = db.createObjectStore('notes', { keyPath: 'id', autoIncrement:true });
// Define what data items the objectStore will contain
objectStore.createIndex('title', 'title', { unique: false });
objectStore.createIndex('body', 'body', { unique: false });
console.log('Database setup complete');
};</pre>
<p><font><font>这是我们定义数据库的模式(结构)的地方; </font><font>也就是说,它包含的列(或字段)集。</font><font>这里我们首先从</font></font><code>e.target.result</code><font><font>(事件目标的</font></font><code>result</code><font><font>属性)中</font><font>获取对现有数据库的引用,该引用</font><font>是</font></font><code>request</code><font><font>对象。</font><font>这相当于</font><font>处理程序</font></font><code>db = request.result;</code><font><font>内部</font><font>的行</font></font><code>onsuccess</code><font><font>,但我们需要在此单独执行此操作,因为</font></font><code>onupgradeneeded</code><font><font>处理程序(如果需要)将在</font></font><code>onsuccess</code><font><font>处理程序</font><font>之前运行</font><font>,这意味着</font></font><code>db</code><font><font>如果我们不这样做</font><font>,该</font><font>值将不可用。</font></font></p>
<p><font><font>然后</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBDatabase/createObjectStore" title="此方法接受一个参数作为 store 的名称,也接受一个可选的参数对象让你可以定义重要的可选属性。你可以用这个属性唯一的标识此 store 中的每个对象。因为参数是一个标识符,所以 store 中的每一个对象都应有此属性并保证此属性唯一。"><code>IDBDatabase.createObjectStore()</code></a><font><font>,</font><font>我们使用</font><font>在打开的数据库中创建一个新的对象库。</font><font>这相当于传统数据库系统中的单个表。</font><font>我们给它起了名称注释,并且还指定了一个</font></font><code>autoIncrement</code><font><font>名为</font><font>的</font><font>关键字段</font></font><code>id</code><font><font>- 在每个新记录中,这将自动赋予增量值 - 开发人员不需要明确地设置它。</font><font>作为密钥,该</font></font><code>id</code><font><font>字段将用于唯一标识记录,例如删除或显示记录时。</font></font></p>
<p><font><font>我们还使用以下</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBObjectStore/createIndex" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>IDBObjectStore.createIndex()</code></a><font><font>方法</font><font>创建另外两个索引(字段)</font><font>:( </font></font><code>title</code><font><font>每个音符将包含一个标题),以及</font></font><code>body</code><font><font>(包含音符的正文)。</font></font></p>
</li>
</ol>
<p>因此,通过设置这个简单的数据库模式,当我们开始向数据库添加记录时,每个记录都会沿着这些行表示为一个对象:</p>
<pre class="brush: js notranslate"><span class="message-body-wrapper"><span class="message-flex-body"><span class="devtools-monospace message-body"><span class="objectBox objectBox-object"><span class="objectLeftBrace">{
</span><span class="nodeName">title</span><span class="objectEqual">: </span><span class="objectBox objectBox-string">"Buy milk"</span>,
<span class="nodeName">body</span><span class="objectEqual">: </span><span class="objectBox objectBox-string">"Need both cows milk and soya."</span>,
<span class="nodeName">id</span><span class="objectEqual">: </span><span class="objectBox objectBox-number">8</span></span></span></span></span><span class="message-body-wrapper"><span class="message-flex-body"><span class="devtools-monospace message-body"><span class="objectBox objectBox-object"><span class="objectRightBrace">
}</span></span></span></span></span></pre>
<h3 id="添加数据到数据库">添加数据到数据库</h3>
<p><font><font>现在让我们看一下如何将记录添加到数据库中。</font><font>这将使用我们页面上的表单完成。</font></font></p>
<p><font><font>在您之前的事件处理程序下面(但仍在</font></font><code>window.onload</code><font><font>处理程序中),添加以下行,该行设置一个</font></font><code>onsubmit</code><font><font>处理程序,</font><font>该</font><font>处理程序运行</font></font><code>addData()</code><font><font>在提交表单时</font><font>调用的函数</font><font>(当</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/button" title="HTML <button> 元素表示一个可点击的按钮,可以用在表单或文档其它需要使用简单标准按钮的地方。"><code><button></code></a><font><font>按下</font><font>提交时</font><font>导致成功提交表单):</font></font></p>
<pre class="brush: js notranslate">// Create an onsubmit handler so that when the form is submitted the addData() function is run
form.onsubmit = addData;</pre>
<p><font><font>现在让我们定义一下这个</font></font><code>addData()</code><font><font>功能。</font><font>在上一行下面添加:</font></font></p>
<pre class="brush: js notranslate">// Define the addData() function
function addData(e) {
// prevent default - we don't want the form to submit in the conventional way
e.preventDefault();
// grab the values entered into the form fields and store them in an object ready for being inserted into the DB
let newItem = { title: titleInput.value, body: bodyInput.value };
// open a read/write db transaction, ready for adding the data
let transaction = db.transaction(['notes'], 'readwrite');
// call an object store that's already been added to the database
let objectStore = transaction.objectStore('notes');
// Make a request to add our newItem object to the object store
var request = objectStore.add(newItem);
request.onsuccess = function() {
// Clear the form, ready for adding the next entry
titleInput.value = '';
bodyInput.value = '';
};
// Report on the success of the transaction completing, when everything is done
transaction.oncomplete = function() {
console.log('Transaction completed: database modification finished.');
// update the display of data to show the newly added item, by running displayData() again.
displayData();
};
transaction.onerror = function() {
console.log('Transaction not opened due to error');
};
}</pre>
<p><font>这很复杂; </font><font>打破它,我们:</font></p>
<ul>
<li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Event/preventDefault" title="Event 接口的 preventDefault( ) 方法,告诉user agent:如果此事件没有需要显式处理,那么它默认的动作也不要做(因为默认是要做的)。此事件还是继续传播,除非碰到事件侦听器调用stopPropagation() 或stopImmediatePropagation(),才停止传播。"><code>Event.preventDefault()</code></a><font><font>在事件对象上</font><font>运行</font><font>以停止以传统方式实际提交的表单(这将导致页面刷新并破坏体验)。</font></font></li>
<li><font><font>创建一个表示要输入数据库的记录的对象,并使用表单输入中的值填充它。</font><font>请注意,我们不必明确包含一个</font></font><code>id</code><font><font>值 - 正如我们提前详细说明的那样,这是自动填充的。</font></font></li>
<li><font><font>使用该</font><font>方法</font><font>打开</font><font>对象存储</font><font>的</font></font><code>readwrite</code><font><font>事务</font><font>。</font><font>此事务对象允许我们访问对象存储,以便我们可以对其执行某些操作,例如添加新记录。</font></font><code>notes</code><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBDatabase/transaction" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>IDBDatabase.transaction()</code></a></li>
<li><font><font>使用该</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBTransaction/objectStore" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>IDBTransaction.objectStore()</code></a><font><font>方法</font><font>访问对象库</font><font>,将结果保存在 </font></font><code>objectStore</code><font><font> 变量中。</font></font></li>
<li><font><font>使用添加新记录到数据库</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBObjectStore/add" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>IDBObjectStore.add()</code></a><font><font>。</font><font>这创建了一个请求对象,与我们之前看到的方式相同。</font></font></li>
<li><font><font>在生命周期的关键点</font><font>添加一堆事件处理程序</font></font><code>request</code><font><font>以及</font></font><code>transaction</code><font><font>运行代码。</font><font>请求成功后,我们会清除表单输入,以便输入下一个注释。</font><font>交易完成后,我们</font></font><code>displayData()</code><font><font>再次</font><font>运行该</font><font>功能以更新页面上的注释显示。</font></font></li>
</ul>
<h3 id="显示数据">显示数据</h3>
<p><font><font>我们已经</font></font><code>displayData()</code><font><font>在代码中</font><font>引用了</font><font>两次,所以我们可能更好地定义它。</font><font>将其添加到您的代码中,位于上一个函数定义之下:</font></font></p>
<pre class="brush: js notranslate">// Define the displayData() function
function displayData() {
// Here we empty the contents of the list element each time the display is updated
// If you ddn't do this, you'd get duplicates listed each time a new note is added
while (list.firstChild) {
list.removeChild(list.firstChild);
}
// Open our object store and then get a cursor - which iterates through all the
// different data items in the store
let objectStore = db.transaction('notes').objectStore('notes');
objectStore.openCursor().onsuccess = function(e) {
// Get a reference to the cursor
let cursor = e.target.result;
// If there is still another data item to iterate through, keep running this code
if(cursor) {
// Create a list item, h3, and p to put each data item inside when displaying it
// structure the HTML fragment, and append it inside the list
let listItem = document.createElement('li');
let h3 = document.createElement('h3');
let para = document.createElement('p');
listItem.appendChild(h3);
listItem.appendChild(para);
list.appendChild(listItem);
// Put the data from the cursor inside the h3 and para
h3.textContent = cursor.value.title;
para.textContent = cursor.value.body;
// Store the ID of the data item inside an attribute on the listItem, so we know
// which item it corresponds to. This will be useful later when we want to delete items
listItem.setAttribute('data-note-id', cursor.value.id);
// Create a button and place it inside each listItem
let deleteBtn = document.createElement('button');
listItem.appendChild(deleteBtn);
deleteBtn.textContent = 'Delete';
// Set an event handler so that when the button is clicked, the deleteItem()
// function is run
<font face="Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace">deleteBtn.onclick = deleteItem;</font>
// Iterate to the next item in the cursor
cursor.continue();
} else {
// Again, if list item is empty, display a 'No notes stored' message
if(!list.firstChild) {
let listItem = document.createElement('li');
listItem.textContent = 'No notes stored.'
list.appendChild(listItem);
}
// if there are no more cursor items to iterate through, say so
console.log('Notes all displayed');
}
};
}</pre>
<p><font><font>再次,让我们打破这个:</font></font></p>
<ul>
<li><font><font>首先,我们清空</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/ul" title="The HTML <ul> 元素 ( 或 HTML 无序列表元素) 代表多项的无序列表,即无数值排序项的集合,且它们在列表中的顺序是没有意义的。通常情况下,无序列表项的头部可以是几种形式,如一个点,一个圆形或方形。头部的风格并不是在页面的HTML描述定义, 但在其相关的CSS 可以用 list-style-type 属性。"><code><ul></code></a><font><font>元素的内容,然后填充更新的内容。</font><font>如果您不这样做,那么每次更新时都会添加大量重复内容。</font></font></li>
<li><font><font>接下来,我们</font></font><code>notes</code><font><font>使用</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBDatabase/transaction" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>IDBDatabase.transaction()</code></a><font><font>和</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBTransaction/objectStore" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>IDBTransaction.objectStore()</code></a><font><font>我们一样</font><font>得到</font><font>对象存储</font><font>的引用</font></font><code>addData()</code><font><font>,除了这里我们将它们链接在一行中。</font></font></li>
<li><font><font>下一步是使用</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBObjectStore/openCursor" title="要确定添加操作是否已成功完成,请侦听结果的成功事件。"><code>IDBObjectStore.openCursor()</code></a><font><font>方法打开对游标的请求 - 这是一个可用于迭代对象存储中的记录的构造。</font><font>我们将一个</font></font><code>onsuccess</code><font><font>处理程序链接到该行的末尾以使代码更简洁 - 当成功返回游标时,运行处理程序。</font></font></li>
<li><font><font>我们</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBCursor" title="IndexedDB API的IDBCursor接口表示用于遍历或迭代数据库中的多个记录的游标。"><code>IDBCursor</code></a><font><font>使用let </font><font>获取对游标本身(</font><font>对象)</font><font>的引用</font></font><code>cursor = e.target.result</code><font><font>。</font></font></li>
<li><font><font>接下来,我们检查光标是否包含来自数据存储区(</font></font><code>if(cursor){ ... }</code><font><font>)</font><font>的记录</font><font>- 如果是这样,我们创建一个DOM片段,用记录中的数据填充它,然后将其插入页面(</font></font><code><ul></code><font><font>元素</font><font>内部</font><font>)。</font><font>我们还包括一个删除按钮,当单击该按钮时,将通过运行该</font></font><code>deleteItem()</code><font><font>功能</font><font>删除该注释</font><font>,我们将在下一节中查看。</font></font></li>
<li><font><font>在</font></font><code>if</code><font><font>块</font><font>结束时</font><font>,我们使用该</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBCursor/continue" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>IDBCursor.continue()</code></a><font><font>方法将光标前进到数据存储区中的下</font><font>一条</font><font>记录,然后</font></font><code>if</code><font><font>再次</font><font>运行</font><font>块</font><font>的内容</font><font>。</font><font>如果有另一个要迭代的记录,这会导致它被插入到页面中,然后</font></font><code>continue()</code><font><font>再次运行,依此类推。</font></font></li>
<li><font><font>当没有更多记录要迭代时,</font></font><code>cursor</code><font><font>将返回</font></font><code>undefined</code><font><font>,因此</font></font><code>else</code><font><font>块将运行而不是</font></font><code>if</code><font><font>块。</font><font>此块检查是否有任何注释被插入</font></font><code><ul></code><font><font>- 如果没有,它会插入一条消息,说没有存储注释。</font></font></li>
</ul>
<h3 id="删除一条笔记">删除一条笔记</h3>
<p><font><font>如上所述,当按下音符的删除按钮时,音符将被删除。</font><font>这是通过</font></font><code>deleteItem()</code><font><font>函数</font><font>实现的</font><font>,如下所示:</font></font></p>
<pre class="brush: js notranslate">// Define the deleteItem() function
function deleteItem(e) {
// retrieve the name of the task we want to delete. We need
// to convert it to a number before trying it use it with IDB; IDB key
// values are type-sensitive.
let noteId = Number(e.target.parentNode.getAttribute('data-note-id'));
// open a database transaction and delete the task, finding it using the id we retrieved above
let transaction = db.transaction(['notes'], 'readwrite');
let objectStore = transaction.objectStore('notes');
let request = objectStore.delete(noteId);
// report that the data item has been deleted
transaction.oncomplete = function() {
// delete the parent of the button
// which is the list item, so it is no longer displayed
e.target.parentNode.parentNode.removeChild(e.target.parentNode);
console.log('Note ' + noteId + ' deleted.');
// Again, if list item is empty, display a 'No notes stored' message
if(!list.firstChild) {
let listItem = document.createElement('li');
listItem.textContent = 'No notes stored.';
list.appendChild(listItem);
}
};
}</pre>
<ul>
<li><font><font>第一部分可以使用一些解释 - 我们检索要删除</font></font><code>Number(e.target.parentNode.getAttribute('data-note-id'))</code><font><font>的记录的ID - 回想一下记录的ID是在</font><font>第一次显示时</font><font>保存在</font></font><code>data-note-id</code><font><font>属性中的</font></font><code><li></code><font><font>。</font><font>但是,我们需要通过全局内置的</font></font><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number"><font><font>Number()</font></font></a><font><font>对象</font><font>传递属性</font><font>,因为它当前是一个字符串,否则将无法被数据库识别。</font></font></li>
<li><font><font>然后,我们使用我们之前看到的相同模式获取对对象存储的引用,并使用该</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBObjectStore/delete" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>IDBObjectStore.delete()</code></a><font><font>方法从数据库中删除记录,并将ID传递给它。</font></font></li>
<li><font><font>当数据库事务完成后,我们</font></font><code><li></code><font><font>从DOM中</font><font>删除注释</font><font>,然后再次检查以查看它是否</font></font><code><ul></code><font><font>为空,并根据需要插入注释。</font></font></li>
</ul>
<p><font><font>就是这样了!</font><font>你的例子现在应该有效。</font></font></p>
<p><font><font>如果您遇到问题,请随时</font></font><a href="https://mdn.github.io/learning-area/javascript/apis/client-side-storage/indexeddb/notes/" rel="noopener"><font><font>查看我们的实例</font></font></a><font><font>(请参阅</font></font><a href="https://github.com/mdn/learning-area/blob/master/javascript/apis/client-side-storage/indexeddb/notes/index.js" rel="noopener"><font><font>源代码</font></font></a><font><font>)。</font></font></p>
<h3 id="通过_IndexedDB_存储复杂数据">通过 IndexedDB 存储复杂数据</h3>
<p><font><font>如上所述,IndexedDB可用于存储不仅仅是简单的文本字符串。</font><font>您可以存储任何您想要的东西,包括复杂的对象,如视频或图像blob。</font><font>并且它比任何其他类型的数据更难实现。</font></font></p>
<p><font><font>为了演示如何操作,我们编写了另一个名为</font></font><a href="https://github.com/mdn/learning-area/tree/master/javascript/apis/client-side-storage/indexeddb/video-store" rel="noopener"><font><font>IndexedDB视频存储的</font></font></a><font><font>示例</font><font>(请参阅</font></font><a href="https://mdn.github.io/learning-area/javascript/apis/client-side-storage/indexeddb/video-store/" rel="noopener"><font><font>此处也可以在此处运行</font></font></a><font><font>)。</font><font>首次运行示例时,它会从网络下载所有视频,将它们存储在IndexedDB数据库中,然后在UI内部</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/video" title="HTML <video> 元素 用于在HTML或者XHTML文档中嵌入视频内容。"><code><video></code></a><font><font>元素中</font><font>显示视频</font><font>。</font><font>第二次运行它时,它会在数据库中找到视频并从那里获取它们而不是显示它们 - 这使得后续加载更快,占用空间更少。</font></font></p>
<p><font><font>让我们来看看这个例子中最有趣的部分。</font><font>我们不会全部看 - 它的很多内容与上一个示例类似,代码注释得很好。</font></font></p>
<ol>
<li>
<p>对于这个简单的例子,我们已经存储了视频的名称以获取数组opf对象:</p>
<pre class="brush: js notranslate">const videos = [
{ 'name' : 'crystal' },
{ 'name' : 'elf' },
{ 'name' : 'frog' },
{ 'name' : 'monster' },
{ 'name' : 'pig' },
{ 'name' : 'rabbit' }
];</pre>
</li>
<li>
<p><font><font>首先,一旦数据库成功打开,我们就运行一个</font></font><code>init()</code><font><font>函数。</font><font>这会遍历不同的视频名称,尝试加载由</font></font><code>videos</code><font><font>数据库中的</font><font>每个名称标识的记录</font><font>。</font></font></p>
<p><font><font>如果在数据库中找到每个视频(通过查看</font></font><code>request.result</code><font><font>评估</font><font>是否容易检查</font></font><code>true</code><font><font>- 如果记录不存在,那么</font></font><code>undefined</code><font><font>),视频文件(存储为blob)和视频名称将直接传递给</font></font><code>displayVideo()</code><font><font>函数以放置它们在用户界面中。</font><font>如果没有,视频名称将传递给</font></font><code>fetchVideoFromNetwork()</code><font><font>函数...你猜对了 - 从网络中获取视频。</font></font></p>
<pre class="brush: js notranslate">function init() {
// Loop through the video names one by one
for(let i = 0; i < videos.length; i++) {
// Open transaction, get object store, and get() each video by name
let objectStore = db.transaction('videos').objectStore('videos');
let request = objectStore.get(videos[i].name);
request.onsuccess = function() {
// If the result exists in the database (is not undefined)
if(request.result) {
// Grab the videos from IDB and display them using displayVideo()
console.log('taking videos from IDB');
displayVideo(request.result.mp4, request.result.webm, request.result.name);
} else {
// Fetch the videos from the network
fetchVideoFromNetwork(videos[i]);
}
};
}
}</pre>
</li>
<li>
<p><font><font>以下片段是从内部</font></font><code>fetchVideoFromNetwork()</code><font><font>获取的 - 这里我们使用两个单独的</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>WindowOrWorkerGlobalScope.fetch()</code></a><font><font>请求</font><font>获取视频的MP4和WebM版本</font><font>。</font><font>然后,我们使用该</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Blob" title="Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据。File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。"><code>Body.blob()</code></a><font><font>方法将每个响应的主体提取为blob,为我们提供可以在以后存储和显示的视频的对象表示。</font></font></p>
<p><font><font>我们在这里遇到了一个问题 - 这两个请求都是异步的,但我们只想在两个promises都满足时尝试显示或存储视频。</font><font>幸运的是,有一种处理这种问题的内置方法 - </font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/all" title="Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。"><code>Promise.all()</code></a><font><font>。</font><font>这需要一个参数 - 引用您要检查放置在数组中的履行的所有单个承诺 - 并且本身是基于承诺的。</font></font></p>
<p><font><font>当所有这些承诺都履行完毕时,</font></font><code>all()</code><font><font>承诺将通过包含所有个人履行价值的数组来实现。</font><font>在</font></font><code>all()</code><font><font>块中,您可以看到我们</font></font><code>displayVideo()</code><font><font>之前</font><font>调用</font><font>函数,就像在UI中显示视频一样,然后我们也调用</font></font><code>storeVideo()</code><font><font>函数将这些视频存储在数据库中。</font></font></p>
<pre class="brush: js notranslate">let mp4Blob = fetch('videos/' + video.name + '.mp4').then(response =>
response.blob()
);
let webmBlob = fetch('videos/' + video.name + '.webm').then(response =>
response.blob()
);;
// Only run the next code when both promises have fulfilled
Promise.all([mp4Blob, webmBlob]).then(function(values) {
// display the video fetched from the network with displayVideo()
displayVideo(values[0], values[1], video.name);
// store it in the IDB using storeVideo()
storeVideo(values[0], values[1], video.name);
});</pre>
</li>
<li>
<p><font><font>我们</font></font><code>storeVideo()</code><font><font>先</font><font>来看看吧</font><font>。</font><font>这与您在上一个示例中看到的用于向数据库添加数据的模式非常相似 - 我们打开一个</font></font><code>readwrite</code><font><font>事务并获取对象存储引用</font></font><code>videos</code><font><font>,创建一个表示要添加到数据库的记录的对象,然后使用它添加它</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/IDBObjectStore/add" rel="nofollow" title="此页面仍未被本地化, 期待您的翻译!"><code>IDBObjectStore.add()</code></a><font><font>。</font></font></p>
<pre class="brush: js notranslate">function storeVideo(mp4Blob, webmBlob, name) {
// Open transaction, get object store; make it a readwrite so we can write to the IDB
let objectStore = db.transaction(['videos'], 'readwrite').objectStore('videos');
// Create a record to add to the IDB
let record = {
mp4 : mp4Blob,
webm : webmBlob,
name : name
}
// Add the record to the IDB using add()
let request = objectStore.add(record);
...
};</pre>
</li>
<li>
<p><font><font>最后但并非最不重要的是,我们</font></font><code>displayVideo()</code><font><font>创建了在UI中插入视频然后将它们附加到页面所需的DOM元素。</font><font>最有趣的部分如下所示 - 要在</font></font><code><video></code><font><font>元素中</font><font>实际显示我们的视频blob </font><font>,我们需要使用该</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/URL/createObjectURL" title="URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的URL 对象表示指定的 File 对象或 Blob 对象。"><code>URL.createObjectURL()</code></a><font><font>方法</font><font>创建对象URL(指向存储在内存中的视频blob的内部URL)</font><font>。</font><font>完成后,我们可以将对象URL设置为</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/source" title="HTML <source>元素为<picture>,<audio>或<video>元素指定多个媒体资源。 这是一个空元素。 它通常用于以不同浏览器支持的多种格式提供相同的媒体内容。"><code><source></code></a><font><font>元素</font></font><code>src</code><font><font>属性的值,并且它可以正常工作。</font></font></p>
<pre class="brush: js notranslate">function displayVideo(mp4Blob, webmBlob, title) {
// Create object URLs out of the blobs
let mp4URL = URL.createObjectURL(mp4Blob);
let webmURL = URL.createObjectURL(webmBlob);
...
let video = document.createElement('video');
video.controls = true;
let source1 = document.createElement('source');
source1.src = mp4URL;
source1.type = 'video/mp4';
let source2 = document.createElement('source');
source2.src = webmURL;
source2.type = 'video/webm';
...
}</pre>
</li>
</ol>
<h2 id="离线文件存储">离线文件存储</h2>
<p><font>上面的示例已经说明了如何创建一个将大型资产存储在IndexedDB数据库中的应用程序,从而无需多次下载它们。</font><font>这已经是对用户体验的一个很大的改进,但仍然有一件事 - 每次访问网站时仍然需要下载主要的HTML,CSS和JavaScript文件,这意味着当没有网络连接时,它将无法工作。</font></p>
<p><img alt="" src="https://mdn.mozillademos.org/files/15759/ff-offline.png" style="border-style: solid; border-width: 1px; display: block; height: 307px; margin: 0px auto; width: 765px;"></p>
<p><font><font>这就是</font></font><a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API"><font><font>服务工作者</font></font></a><font><font>和密切相关的</font></font><a href="https://developer.mozilla.org/en-US/docs/Web/API/Cache"><font><font>Cache API的</font></font></a><font><font>用武之地。</font></font></p>
<p><font><font>服务工作者是一个JavaScript文件,简单地说,它是在浏览器访问时针对特定来源(网站或某个域的网站的一部分)进行注册的。</font><font>注册后,它可以控制该来源的可用页面。</font><font>它通过坐在加载的页面和网络之间以及拦截针对该来源的网络请求来实现这一点。</font></font></p>
<p><font><font>当它拦截一个请求时,它可以做任何你想做的事情(参见</font></font><a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API#Other_use_case_ideas"><font><font>用例思路</font></font></a><font><font>),但经典的例子是离线保存网络响应,然后提供响应请求而不是来自网络的响应。</font><font>实际上,它允许您使网站完全脱机工作。</font></font></p>
<p><font><font>Cache API是另一种客户端存储机制,略有不同 - 它旨在保存HTTP响应,因此与服务工作者一起工作得非常好。</font></font></p>
<div class="note">
<p><strong><font><font>注意</font></font></strong><font><font>:现在大多数现代浏览器都支持服务工作者和缓存。</font><font>在撰写本文时,Safari仍在忙着实施它,但它应该很快就会存在。</font></font></p>
</div>
<h3 id="一个_service_worker_例子">一个 service worker 例子</h3>
<p><font><font>让我们看一个例子,让你对这可能是什么样子有所了解。</font><font>我们已经创建了另一个版本的视频存储示例,我们在上一节中看到了 - 这个功能完全相同,只是它还通过服务工作者将Cache,CSS和JavaScript保存在Cache API中,允许示例脱机运行!</font></font></p>
<p><font><font>请参阅</font></font><a href="https://mdn.github.io/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/" rel="noopener"><font><font>IndexedDB视频存储,其中服务工作者正在运行</font></font></a><font><font>,并且还可以</font></font><a href="https://github.com/mdn/learning-area/tree/master/javascript/apis/client-side-storage/cache-sw/video-store-offline" rel="noopener"><font><font>查看源代码</font></font></a><font><font>。</font></font></p>
<h4 id="注册服务工作者"><font><font>注册服务工作者</font></font></h4>
<p><font><font>首先要注意的是,在主JavaScript文件中放置了一些额外的代码(请参阅</font></font><a href="https://github.com/mdn/learning-area/blob/master/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.js" rel="noopener"><font><font>index.js</font></font></a><font><font>)。</font><font>首先,我们进行特征检测测试,以查看</font></font><code>serviceWorker</code><font><font>该</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator" title="Navigator 接口表示用户代理的状态和标识。 它允许脚本查询它和注册自己进行一些活动。"><code>Navigator</code></a><font><font>对象中</font><font>是否有该</font><font>成员</font><font>。</font><font>如果返回true,那么我们知道至少支持服务工作者的基础知识。</font><font>在这里,我们使用该</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/ServiceWorkerContainer/register" title="ServiceWorkerContainer 接口的 register() 方法创建或更新一个给定scriptURL的ServiceWorkerRegistration 。"><code>ServiceWorkerContainer.register()</code></a><font><font>方法将</font></font><code>sw.js</code><font><font>文件中</font><font>包含的服务工作者注册到</font><font>它所驻留的源,因此它可以控制与它或子目录相同的目录中的页面。</font><font>当其承诺履行时,服务人员被视为已注册。</font></font></p>
<pre class="brush: js notranslate"> // Register service worker to control making site work offline
if('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js')
.then(function() { console.log('Service Worker Registered'); });
}</pre>
<div class="note">
<p><strong><font><font>注意</font></font></strong><font><font>:</font></font><code>sw.js</code><font><font>文件</font><font>的给定路径</font><font>是相对于站点源的,而不是包含代码的JavaScript文件。</font><font>服务人员在</font></font><code>https://mdn.github.io/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js</code><font><font>。</font><font>原点是</font></font><code>https://mdn.github.io</code><font><font>,因此给定的路径必须是</font></font><code>/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js</code><font><font>。</font><font>如果您想在自己的服务器上托管此示例,则必须相应地更改此示例。</font><font>这是相当令人困惑的,但出于安全原因,它必须以这种方式工作。</font></font></p>
</div>
<h4 id="安装_service_worker">安装 service worker</h4>
<p><font><font>下次访问服务工作者控制下的任何页面时(例如,重新加载示例时),将针对该页面安装服务工作者,这意味着它将开始控制它。</font><font>发生这种情况时,</font></font><code>install</code><font><font>会向服务工作人员发起</font><font>一个</font><font>事件; </font><font>您可以在服务工作者本身内编写代码来响应安装。</font></font></p>
<p><font><font>让我们看一下</font></font><a href="https://github.com/mdn/learning-area/blob/master/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js" rel="noopener"><font><font>sw.js</font></font></a><font><font>文件(服务工作者)</font><font>中的一个例子</font><font>。</font><font>您将看到安装侦听器已注册</font></font><code>self</code><font><font>。</font><font>此</font></font><code>self</code><font><font>关键字是一种从服务工作文件内部引用服务工作者的全局范围的方法。</font></font></p>
<p><font><font>在</font></font><code>install</code><font><font> 处理程序</font><font>内部, </font><font>我们使用</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/ExtendableEvent/waitUntil" title="ExtendableEvent.waitUntil() 方法扩展了事件的生命周期。在服务工作线程中,延长事件的寿命从而阻止浏览器在事件中的异步操作完成之前终止服务工作线程。"><code>ExtendableEvent.waitUntil()</code></a><font><font>事件对象上可用</font><font>的</font><font>方法来表示浏览器不应该完成服务工作者的安装,直到其中的promise成功完成。</font></font></p>
<p><font><font>这是我们在运行中看到Cache API的地方。</font><font>我们使用该</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/CacheStorage/open" title="CacheStorage open() 方法返回一个resolve为匹配 cacheName 的 Cache 对象的 Promise ."><code>CacheStorage.open()</code></a><font><font>方法打开一个可以存储响应的新缓存对象(类似于IndexedDB对象存储)。</font><font>此承诺通过</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Cache" title="Cache 接口为缓存的 Request / Response 对象对提供存储机制,例如,作为ServiceWorker 生命周期的一部分。请注意,Cache 接口像 workers 一样,是暴露在 window 作用域下的。尽管它被定义在 service worker 的标准中, 但是它不必一定要配合 service worker 使用."><code>Cache</code></a><font><font>表示</font></font><code>video-store</code><font><font>缓存</font><font>的</font><font>对象来</font><font>实现</font><font>。</font><font>然后,我们使用该</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Cache/addAll" title="Cache 接口的 addAll() 方法接受一个URL数组,检索它们,并将生成的response对象添加到给定的缓存中。 在检索期间创建的request对象成为存储的response操作的key。"><code>Cache.addAll()</code></a><font><font>方法获取一系列资产并将其响应添加到缓存中。</font></font></p>
<pre class="brush: js notranslate">self.addEventListener('install', function(e) {
e.waitUntil(
caches.open('video-store').then(function(cache) {
return cache.addAll([
'/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/',
'/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.html',
'/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.js',
'/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/style.css'
]);
})
);
});</pre>
<p>这就是现在,安装完成。</p>
<h4 id="响应未来的请求">响应未来的请求</h4>
<p><font><font>在我们的HTML页面上注册并安装了服务工作者,并且所有相关资产都添加到我们的缓存中,我们几乎准备好了。</font><font>还有一件事要做,写一些代码来响应进一步的网络请求。</font></font></p>
<p><font><font>这就是第二位代码的</font></font><code>sw.js</code><font><font>作用。</font><font>我们向服务工作者全局范围添加另一个侦听器,该范围在</font></font><code>fetch</code><font><font>引发事件</font><font>时运行处理函数</font><font>。</font><font>只要浏览器在服务工作者注册的目录中请求资产,就会发生这种情况。</font></font></p>
<p><font><font>在处理程序内部,我们首先记录所请求资产的URL。</font><font>然后,我们使用该</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/FetchEvent/respondWith" title="FetchEvent 接口的 respondWith() 方法旨在包裹代码,这些代码为来自受控页面的request生成自定义的response。这些代码通过返回一个 Response 、 network error 或者 Fetch的方式resolve。"><code>FetchEvent.respondWith()</code></a><font><font>方法</font><font>为请求提供自定义响应</font><font>。</font></font></p>
<p><font><font>在这个块中,我们</font></font><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/CacheStorage/match" title="CacheStorage 接口(可适用于全局性caches) 的 match() 方法检查给定的Request 对象或url字符串是否是一个已存储的 Response 对象的key. 这个方法针对 Response 返回一个 Promise ,如果没有匹配则返回 undefined 。"><code>CacheStorage.match()</code></a><font><font>用来检查是否可以在任何缓存中找到匹配的请求(即匹配URL)。</font><font>如果未找到匹配,或者</font></font><code>undefined</code><font><font>如果</font><font>未找到匹配,则此承诺将满足匹配的响应</font><font>。</font></font></p>
<p><font><font>如果找到匹配项,我们只需将其作为自定义响应返回。</font><font>如果没有,我们</font><font>从网络中</font></font><a href="https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch"><font><font>获取()</font></font></a><font><font>响应并返回该响应。</font></font></p>
<pre class="brush: js notranslate">self.addEventListener('fetch', function(e) {
console.log(e.request.url);
e.respondWith(
caches.match(e.request).then(function(response) {
return response || fetch(e.request);
})
);
});</pre>
<p><font><font>这就是我们简单的服务工作者。</font><font>您可以使用它们进行更多的负载 - 有关详细信息,请参阅</font></font><a href="https://serviceworke.rs/" rel="noopener"><font><font>服务工作者手册</font></font></a><font><font>。</font><font>感谢Paul Kinlan在他的文章中</font></font><a href="https://developers.google.com/web/fundamentals/codelabs/offline/" rel="noopener"><font><font>添加服务工作者和离线到您的Web应用程序</font></font></a><font><font>,这启发了这个简单的例子。</font></font></p>
<h4 id="测试离线示例">测试离线示例</h4>
<p><font><font>要测试我们的</font></font><a href="https://mdn.github.io/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/" rel="noopener"><font><font>服务工作者示例</font></font></a><font><font>,您需要加载它几次以确保它已安装。</font><font>完成后,您可以:</font></font></p>
<ul>
<li><font><font>尝试拔掉网络连接/关闭Wifi。</font></font></li>
<li><font><font>如果您使用的是Firefox,请</font><font>选择</font></font><em><font><font>文件>脱机工作</font></font></em><font><font>。</font></font></li>
<li><font><font>转到devtools,然后选择</font></font><em><font><font>Application> Service Workers</font></font></em><font><font>,</font><font>如果您使用的是Chrome </font><font>,请选中</font></font><em><font><font>Offline</font></font></em><font><font>选中。</font></font></li>
</ul>
<p><font><font>如果再次刷新示例页面,您仍应该看到它加载得很好。</font><font>所有内容都是脱机存储的 - 缓存中的页面资源以及IndexedDB数据库中的视频。</font></font></p>
<h2 id="总结">总结</h2>
<p><font>现在就是这些。</font><font>我们希望你能感觉到我们对客户端存储技术的介绍很有用。</font></p>
<h2 id="相关链接">相关链接</h2>
<ul>
<li><a href="/en-US/docs/Web/API/Web_Storage_API">网页存储 API</a></li>
<li><a href="/en-US/docs/Web/API/IndexedDB_API">IndexedDB API</a></li>
<li><a href="/en-US/docs/Web/HTTP/Cookies">Cookies</a></li>
<li><a href="/en-US/docs/Web/API/Service_Worker_API">Service worker API</a></li>
</ul>
<p>{{PreviousMenu("Learn/JavaScript/Client-side_web_APIs/Video_and_audio_APIs", "Learn/JavaScript/Client-side_web_APIs")}}</p>
<h2 id="在本单元中">在本单元中</h2>
<ul>
<li><a href="/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Introduction">网页端 API介绍 </a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Manipulating_documents">Manipulating documents</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Fetching_data">从服务器获取数据(fetch)</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Third_party_APIs">第三方 API</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Drawing_graphics">Drawing graphics</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Video_and_audio_APIs">视频和音频 API</a></li>
<li><a href="/en-US/docs/Learn/JavaScript/Client-side_web_APIs/Client-side_storage">客户端存储</a></li>
</ul>
|