클라이언트 측 저장소. 이는 웹 저장소, 웹 SQL 데이터베이스, 인덱싱된 데이터베이스, 파일 액세스 등 서로 별개이지만 관련된 여러 API를 포괄하는 일반적인 용어입니다. 각 기술은 일반적으로 데이터가 저장되는 서버가 아닌 사용자의 하드 드라이브에 데이터를 저장하는 고유한 방법을 제공합니다. 이는 주로 다음 두 가지 이유로 수행됩니다. (a) 웹 앱을 오프라인에서 사용할 수 있도록 합니다. (b) 성능을 향상합니다. 클라이언트 측 저장소 사용에 대한 자세한 설명은 HTML5Rocks 기사 "오프라인": 무엇을 의미합니까?를 참조하세요. 내가 왜 신경써야 하는데?"
공통점이 무엇인지 살펴보겠습니다.
공통 기능
클라이언트 기반 저장
사실 "클라이언트 시간 저장"이란 예, 데이터는 로컬 장치의 특정 영역에 데이터를 저장하는 브라우저의 저장 API로 전달됩니다. 이 영역은 개인 기본 설정 및 캐시와 같은 기타 사용자별 정보도 저장하는 곳입니다. 데이터를 저장하는 것 외에도 이러한 API를 사용하여 데이터를 검색하고 경우에 따라 검색 및 일괄 작업을 수행할 수 있습니다.
샌드박스
4가지 스토리지 API는 모두 데이터를 단일 "원본"에 연결합니다.
공간 제한(할당량)
어떤 웹사이트에서든 의심하지 않는 하드 드라이브에 기가바이트의 데이터를 채울 수 있다면 얼마나 혼란스러울지 상상할 수 있습니다. 따라서 브라우저는 저장 용량에 제한을 둡니다. 앱이 한도를 초과하려고 시도하는 경우 일반적으로 브라우저는 사용자에게 증가 확인을 요청하는 대화 상자를 표시합니다. 브라우저는 단일 원본에서 사용할 수 있는 모든 저장소에 동일한 개별 제한을 적용한다고 생각할 수 있지만 대부분의 저장소 메커니즘은 개별적으로 제한됩니다. Quota API가 채택되면 변경될 수 있습니다. 하지만 지금은 브라우저를 원점과 저장 공간의 차원을 갖는 2차원 행렬로 생각하세요. abc.example은 최대 5MB의 웹 저장소와 25MB의 웹 SQL 데이터베이스를 허용할 수 있지만 사용자의 액세스가 거부되었기 때문에 Indexed DataBase를 사용할 수 없습니다. Quota API는 문제를 종합하여 사용 가능한 공간과 사용 중인 공간을 쿼리할 수 있게 해줍니다.
어떤 경우에는 저장용량이 얼마나 사용될지 미리 확인할 수도 있습니다. 예를 들어 사용자가 Chrome App Store에 앱을 설치하면 다음을 포함한 권한을 사전에 수락하라는 메시지가 표시됩니다. 저장 한도. (애플리케이션에 대한) 매니페스트에 "unlimited_storage" 값이 있을 수 있습니다.
데이터베이스 처리(거래)
두 가지 "데이터베이스" 저장 형식이 데이터 처리를 지원합니다. 목적은 일반 관계형 데이터베이스 데이터 처리와 동일합니다. 즉, 데이터베이스의 무결성을 보장하는 것입니다. 데이터베이스 트랜잭션(트랜잭션)은 "경합 조건"을 방지합니다. 이는 두 개의 작업 시퀀스가 데이터베이스에 동시에 적용되어 작업 결과를 예측할 수 없고 데이터베이스의 정확성이 의심스러운 상태가 되는 경우입니다.
동기 및 비동기 모드
대부분의 스토리지 형식은 동기 및 비동기 모드를 지원합니다. 동기 모드는 차단입니다. 이는 js 코드의 다음 줄이 실행되기 전에 저장 작업이 완전히 실행된다는 것을 의미합니다. 비동기 모드에서는 데이터베이스 작업이 완료되기 전에 다음 js 코드가 실행됩니다. 저장 작업은 백그라운드에서 수행됩니다. 작업이 완료되면 애플리케이션은 호출되는 콜백 함수 형태로 알림을 받습니다.
동기 모드를 사용하지 않는 것이 좋습니다. 비교적 간단해 보이지만 작업이 완료되면 페이지 렌더링이 차단되고 경우에 따라 전체 브라우저가 정지될 수도 있습니다. 웹사이트나 앱에서도 이런 일이 발생하는 것을 눈치채셨을 것입니다. 버튼을 클릭하면 모든 것이 사용할 수 없게 됩니다. 그래도 작동이 중단되었나요? 그 결과 갑자기 모든 것이 정상으로 돌아왔습니다.
"localStorage"와 같은 일부 API에는 비동기 모드가 없습니다. 이러한 API를 사용할 때는 성능을 주의 깊게 모니터링하고 문제가 발생할 경우 비동기 API로 전환할 준비를 해야 합니다.
API 개요 및 비교
웹 스토리지
웹 스토리지는 localStorage라는 영구 개체입니다. localStorage.foo = "bar"를 사용하여 값을 저장하고 나중에 localStorage.foo를 사용하여 값을 가져올 수 있습니다. 브라우저를 닫았다가 다시 연 후에도 가능합니다. 창이 닫힐 때 지워진다는 점을 제외하면 동일한 방식으로 작동하는 sessionStorage라는 개체를 사용할 수도 있습니다.
웹 스토리지는 NoSQL 키-값 저장소의 일종입니다.
웹 스토리지의 장점
수년 동안 모든 최신 브라우저에서 사용되었습니다. 그렇습니다. iOS 및 Android 시스템에서도 지원됩니다(IE는 IE8부터 지원합니다).
간단한 API 서명.
동기식 API, 호출이 쉽습니다.
의미 이벤트는 다른 탭과 창의 동기화를 유지합니다.
웹 스토리지의 약점
동기 API(가장 널리 지원되는 모드)를 사용하여 대용량 또는 복잡한 데이터를 저장할 때 성능이 좋지 않습니다.
인덱스가 부족하면 크거나 복잡한 데이터를 검색할 때 성능이 저하됩니다. (검색 작업에는 모든 항목을 수동으로 반복해야 합니다.)
크거나 복잡한 데이터 구조를 수동으로 문자열로 직렬화하거나 문자열에서 역직렬화해야 하기 때문에 성능이 저하됩니다. 주요 브라우저 구현은 문자열만 지원합니다(사양에서는 그렇게 명시하지 않지만).
데이터는 사실상 비정형이기 때문에 데이터의 연속성과 무결성이 보장되어야 합니다.
웹 SQL 데이터베이스
웹 SQL 데이터베이스는 일반적인 SQL 기반 관계형 데이터베이스의 모든 기능과 복잡성을 갖춘 구조화된 데이터베이스입니다. 인덱싱된 데이터베이스는 그 사이 어딘가에 있습니다. 웹 SQL 데이터베이스에는 웹 저장소와 약간 유사한 자유 형식 키-값 쌍이 있지만 이러한 값에서 필드를 인덱싱하는 기능도 있으므로 검색 속도가 훨씬 빨라집니다.
웹 SQL 데이터베이스의 장점
주요 모바일 브라우저(안드로이드 브라우저, 모바일 사파리, 오페라 모바일)와 일부 PC 브라우저(크롬, 사파리, 오페라)에서 지원됩니다.
비동기 API로서 전반적인 성능이 매우 좋습니다. 데이터베이스 상호 작용은 사용자 인터페이스를 잠그지 않습니다. (동기식 API는 WebWorkers에서도 사용할 수 있습니다.)
검색 키를 기반으로 데이터를 색인화할 수 있으므로 검색 성능이 좋습니다.
트랜잭션 데이터베이스 모델을 지원하므로 강력합니다.
엄격한 데이터 구조를 통해 데이터 무결성을 더 쉽게 유지할 수 있습니다.
웹 SQL 데이터베이스의 약점
오래되어 IE나 Firefox에서 지원되지 않으며, 어떤 단계에서 다른 브라우저에서는 단계적으로 폐지될 수 있습니다.
학습 곡선이 가파르고 관계형 데이터베이스 및 SQL에 대한 지식이 필요합니다.
객체 관계형 임피던스 불일치
데이터베이스 스키마가 사전 정의되어야 하고 테이블의 모든 레코드가 동일한 구조와 일치해야 하므로 민첩성이 저하됩니다.
인덱싱된 데이터베이스(IndexedDB)
지금까지 웹 스토리지와 웹 SQL 데이터베이스에는 다양한 장단점이 있음을 살펴보았습니다. 인덱스 데이터베이스는 이 두 가지 초기 API의 경험에서 탄생했으며, 단점을 초래하지 않으면서 두 API의 장점을 결합하려는 시도로 볼 수 있습니다.
인덱싱된 데이터베이스는 개체를 직접 배치할 수 있는 "객체 저장소" 모음입니다. 이 저장소는 SQL 테이블과 약간 유사하지만 이 경우 개체 구조에 대한 제약이 없으므로 미리 정의할 필요가 없습니다. 따라서 이는 여러 데이터베이스와 각 데이터베이스에 여러 저장소가 있는 웹 저장소와 약간 비슷합니다. 그러나 Web Storage와는 달리 중요한 성능 이점도 있습니다. 즉, 스토리지에 인덱스를 생성하여 검색 속도를 높일 수 있는 비동기 인터페이스가 있습니다.
IndexedDB의 장점
전체적으로 비동기 API로서의 성능이 좋습니다. 데이터베이스 상호 작용은 사용자 인터페이스를 잠그지 않습니다. (Sync API는 WebWorkers에서도 사용할 수 있습니다.)
검색 키를 기반으로 데이터를 색인화할 수 있으므로 검색 성능이 좋습니다.
버전 관리를 지원합니다.
트랜잭션 데이터베이스 모델을 지원하므로 강력합니다.
데이터 모델이 단순하기 때문에 학습 곡선도 매우 간단합니다.
훌륭한 브라우저 지원: Chrome, Firefox, 모바일 FF, IE10.
IndexedDB의 약점
API가 매우 복잡하여 중첩된 콜백이 많이 발생합니다.
파일 시스템
위 API는 텍스트 및 구조화된 데이터에 적합하지만 대용량 파일 및 바이너리 콘텐츠의 경우에는 다른 것이 필요합니다. 다행히도 이제 FileSystem API 표준이 있습니다. 이는 각 도메인에 완전한 계층적 파일 시스템을 제공하며 적어도 Chrome에서는 이러한 파일이 사용자 하드 드라이브에 있는 실제 파일입니다. 개별 파일을 읽고 쓰는 측면에서 API는 기존 File API를 기반으로 구축됩니다.
FileSystem(파일 시스템) API는 이미지, 오디오, 비디오, PDF 등에 적합한 대용량 콘텐츠와 바이너리 파일을 저장할 수 있다는 장점이 있습니다
.
비동기 API로서 성능이 좋습니다.
FileSystem API의 약점
아주 초기 표준으로 Chrome과 Opera에서만 지원됩니다.
거래는 지원되지 않습니다.
검색/인덱싱 지원이 내장되어 있지 않습니다.
코드 보기
이 섹션에서는 서로 다른 API가 동일한 문제를 어떻게 해결하는지 비교합니다. 이 예는 시간과 장소에 따라 기분을 기록할 수 있는 "geo-mood" 체크인 시스템입니다. 인터페이스를 사용하면 데이터베이스 유형 간에 전환할 수 있습니다. 물론, 실제 상황에서는 이것이 약간 인위적으로 보일 수도 있고, 데이터베이스 유형이 다른 것보다 확실히 더 합리적일 것이며, 파일 시스템 API는 이 애플리케이션에 적합하지 않을 것입니다! 그러나 시연 목적으로 동일한 결과를 달성하는 다양한 방법을 볼 수 있다면 도움이 됩니다. 또한 가독성을 유지하기 위해 일부 코드 조각이 리팩터링되었습니다.
이제 "geo-mood" 애플리케이션을 사용해 볼 수 있습니다.
데모를 더욱 흥미롭게 만들기 위해 데이터 저장소를 분리하고 표준 객체 지향 설계 기술을 사용합니다. UI 로직은 저장소가 하나라는 것만 알고 있으며, 각 저장소마다 메소드가 동일하므로 저장소가 어떻게 구현되는지 알 필요가 없습니다. 따라서 UI 레이어 코드는 store.setup(), store.count() 등으로 호출할 수 있습니다. 실제로 우리 매장에는 각 저장소 유형별로 하나씩 총 4가지 구현이 있습니다. 애플리케이션이 시작되면 URL을 확인하고 해당 상점이 인스턴스화됩니다.
API 일관성을 유지하기 위해 모든 메소드는 비동기식입니다. 즉, 호출자에게 결과를 반환합니다. Web Storage의 구현도 이와 같으며 기본 구현은 기본입니다.
다음 데모에서는 UI와 위치 지정 로직을 건너뛰고 스토리지 기술에 집중하겠습니다.
스토어 생성
localStorage의 경우 스토리지가 존재하는지 간단히 확인합니다. 존재하지 않는 경우 새 배열을 만들고 체크인 키 아래의 localStorage에 저장합니다. 먼저, 대부분의 브라우저는 문자열 저장만 지원하므로 JSON 개체를 사용하여 구조를 문자열로 직렬화합니다.
if (!localStorage.checkins) localStorage.checkins = JSON.stringify([]);
Web SQL Database의 경우 데이터베이스 구조가 존재하지 않으면 먼저 생성해야 합니다. 다행스럽게도 openDatabase 메소드는 데이터베이스가 존재하지 않는 경우 자동으로 데이터베이스를 생성합니다. 마찬가지로 "존재하지 않는 경우"라는 SQL 문을 사용하면 새 체크인 테이블이 이미 존재하는 경우 이를 덮어쓰지 않게 됩니다. 데이터 구조, 즉 checksins 테이블의 각 컬럼의 이름과 타입을 미리 정의해 주어야 합니다. 각 데이터 행은 체크인을 나타냅니다.
this.db = openDatabase('geomood', '1.0', 'Geo-Mood Checkins', 8192);this.db.transaction(function(tx) { tx.executeSql( "create table if not exists " + "checkins(id integer primary key asc, time integer, latitude float," + "longitude float, mood string)", [], function() { console.log("siucc"); } ); });
인덱싱된 데이터베이스를 시작하려면 데이터베이스 버전 관리 시스템을 활성화해야 하므로 일부 작업이 필요합니다. 데이터베이스에 연결할 때 필요한 버전을 명확히 해야 합니다. 현재 데이터베이스가 이전 버전을 사용 중이거나 아직 생성되지 않은 경우 업그레이드가 완료되면 onsuccess 이벤트가 트리거됩니다. 트리거됩니다. 업그레이드할 필요가 없으면 onsuccess 이벤트가 즉시 트리거됩니다.
또 다른 일은 일치하는 감정을 나중에 빠르게 쿼리할 수 있도록 '기분' 지수를 만드는 것입니다.
var db;var version = 1; window.indexedStore = {}; window.indexedStore.setup = function(handler) { // attempt to open the database var request = indexedDB.open("geomood", version); // upgrade/create the database if needed request.onupgradeneeded = function(event) { var db = request.result; if (event.oldVersion < 1) { // Version 1 is the first version of the database. var checkinsStore = db.createObjectStore("checkins", { keyPath: "time" }); checkinsStore.createIndex("moodIndex", "mood", { unique: false }); } if (event.oldVersion < 2) { // In future versions we'd upgrade our database here. // This will never run here, because we're version 1. } db = request.result; }; request.onsuccess = function(ev) { // assign the database for access outside db = request.result; handler(); db.onerror = function(ev) { console.log("db error", arguments); }; }; };
마지막으로 FileSystem을 시작합니다. 각 체크인 JSON을 별도의 파일로 인코딩하며 "checkins/" 디렉터리에 있습니다. 다시 말하지만 이것은 FileSystem API의 가장 적절한 사용은 아니지만 데모 목적으로는 괜찮습니다.
"checkins/" 디렉터리를 확인하기 위해 전체 파일 시스템에서 핸들을 가져오기 시작합니다. 디렉터리가 존재하지 않으면 getDirectory를 사용하여 디렉터리를 만듭니다.
setup: function(handler) { requestFileSystem( window.PERSISTENT, 1024*1024, function(fs) { fs.root.getDirectory("checkins", {}, // no "create" option, so this is a read op function(dir) { checkinsDir = dir; handler(); }, function() { fs.root.getDirectory( "checkins", {create: true }, function(dir) { checkinsDir = dir; handler(); }, onError ); } ); }, function(e) { console.log("error "+e.code+"initialising - see http:php.cn"); } ); }
체크인 저장(Check-in)
localStorage를 사용하면 체크인 어레이만 꺼내고 마지막에 하나 추가한 후 다시 저장하면 됩니다. 또한 이를 문자열로 저장하려면 JSON 개체 메서드를 사용해야 합니다.
var checkins = JSON.parse(localStorage["checkins"]); checkins.push(checkin); localStorage["checkins"] = JSON.stringify(checkins);
웹 SQL 데이터베이스를 사용하면 모든 것이 트랜잭션에서 발생합니다. 우리는 checksins 테이블에 새로운 행을 생성하려고 합니다. 이것은 "insert" 명령에 모든 체크인 데이터를 넣는 대신 "?" 구문을 사용합니다. 실제 데이터(저장하려는 4개 값)는 두 번째 행에 배치됩니다. "?" 요소는 이러한 값(checkin.time, checkin.latitude 등)으로 대체됩니다. 다음 두 매개변수는 작업이 완료된 후 호출되는 함수로, 각각 성공 및 실패 후에 호출됩니다. 이 애플리케이션에서는 모든 작업에 동일한 공통 오류 처리기를 사용합니다. 이러한 방식으로 성공 콜백 함수는 검색 함수에 전달하는 핸들입니다. 작업이 완료될 때 UI에 알릴 수 있도록 성공 시 핸들이 호출되는지 확인하세요(예: 체크인 수 업데이트) 지금까지).
store.db.transaction(function(tx) { tx.executeSql("insert into checkins " + "(time, latitude, longitude, mood) values (?,?,?,?);", [checkin.time, checkin.latitude, checkin.longitude, checkin.mood], handler, store.onError ); });
저장소가 설정되면 IndexedDB에 저장하는 것은 웹 저장소만큼 쉽고 비동기식으로 작업할 수 있다는 이점도 있습니다.
var transaction = db.transaction("checkins", 'readwrite'); transaction.objectStore("checkins").put(checkin); transaction.oncomplete = handler;
FileSystem API를 사용하여 새 파일을 생성하고 FileWriter API로 채울 수 있는 해당 핸들을 가져옵니다.
fs.root.getFile( "checkins/" + checkin.time, { create: true, exclusive: true }, function(file) { file.createWriter(function(writer) { writer.onerror = fileStore.onError; var bb = new WebKitBlobBuilder; bb.append(JSON.stringify(checkin)); writer.write(bb.getBlob("text/plain")); handler(); }, fileStore.onError); }, fileStore.onError );
일치 검색
다음 기능은 특정 감정과 일치하는 모든 체크인을 찾습니다. 예를 들어 사용자는 최근 언제 어디서 즐거운 시간을 보냈는지 확인할 수 있습니다. localStorage를 사용하여 각 체크인을 수동으로 반복하고 이를 검색 감정과 비교하여 일치 항목 목록을 작성해야 합니다. 더 나은 방법은 실제 객체가 아닌 저장된 데이터의 복제본을 반환하는 것입니다. 검색은 읽기 전용 작업이어야 하므로 일치하는 각 체크인 객체를 작업을 위해 일반 clone() 메서드에 전달합니다.
var allCheckins = JSON.parse(localStorage["checkins"]);var matchingCheckins = []; allCheckins.forEach(function(checkin) { if (checkin.mood == moodQuery) { matchingCheckins.push(clone(checkin)); } }); handler(matchingCheckins);
当然,在 IndexedDB 解决方案使用索引,我们先前在 “mood” 表中创建的索引,称为“moodindex”。我们用一个指针遍历每次签到以匹配查询。注意这个指针模式也可以用于整个存储;因此,使用索引就像我们在商店里的一个窗口前,只能看到匹配的对象(类似于在传统数据库中的“视图”)。
var store = db.transaction("checkins", 'readonly').objectStore("checkins");var request = moodQuery ? store.index("moodIndex").openCursor(new IDBKeyRange.only(moodQuery)) : store.openCursor(); request.onsuccess = function(ev) { var cursor = request.result; if (cursor) { handler(cursor.value); cursor["continue"](); } };
与许多传统的文件系统一样,FileSystem API 没有索引,所以搜索算法(如 Unix中的 “grep” 命令)必须遍历每个文件。我们从 “checkins/” 目录中拿到 Reader API ,通过 readentries() 。对于每个文件,再使用一个 reader,使用 readastext() 方法检查其内容。这些操作都是异步的,我们需要使用 readnext() 将调用连在一起。
checkinsDir.createReader().readEntries(function(files) { var reader, fileCount = 0, checkins = []; var readNextFile = function() { reader = new FileReader(); if (fileCount == files.length) return; reader.onload = function(e) { var checkin = JSON.parse(this.result); if (moodQuery == checkin.mood || !moodQuery) handler(checkin); readNextFile(); }; files[fileCount++].file(function(file) { reader.readAsText(file); }); }; readNextFile(); });
匹配计数
最后,我们需要给所有签到计数。
对localStorage,我们简单的反序列化签到数组,读取其长度。
handler(JSON.parse(localStorage["checkins"]).length);
对 Web SQL Database,可以检索数据库中的每一行(select * from checkins),看结果集的长度。但如果我们知道我们在 SQL 中,有更容易和更快的方式 —— 我们可以执行一个特殊的 select 语句来检索计数。它将返回一行,其中一列包含计数。
store.db.transaction(function(tx) { tx.executeSql("select count(*) from checkins;", [], function(tx, results) { handler(results.rows.item(0)["count(*)"]); }, store.onError); });
不幸的是, IndexedDB 不提供任何计算方法,所以我们只能自己遍历。
var count = 0;var request = db.transaction(["checkins"], 'readonly').objectStore("checkins").openCursor(); request.onsuccess = function(ev) { var cursor = request.result; cursor ? ++count && cursor["continue"]() : handler(count); };
对于文件系统, directory reader 的 readentries() 方法提供一个文件列表,所以我们返回该列表的长度就好。
checkinsDir.createReader().readEntries(function(files) { handler(files.length); });
总结
本文从较高层次的角度,讲述了现代客户端存储技术。你也可以看看 《离线应用概述》(overview on offline apps)这篇文章。