Giới thiệu
IndexedDB là một cách hiệu quả để lưu trữ dữ liệu ở phía máy khách. Nếu chưa xem, bạn nên đọc các hướng dẫn hữu ích của MND về chủ đề này. Bài viết này giả định bạn đã có một số kiến thức cơ bản về các API và tính năng. Ngay cả khi bạn chưa từng thấy IndexedDB, hy vọng bản minh hoạ trong bài viết này sẽ giúp bạn biết được những việc có thể làm với IndexedDB.
Bản minh hoạ của chúng tôi là một ứng dụng Intranet đơn giản để chứng minh khái niệm cho một công ty. Ứng dụng này sẽ cho phép nhân viên tìm kiếm các nhân viên khác. Để mang lại trải nghiệm nhanh chóng và linh hoạt hơn, cơ sở dữ liệu nhân viên được sao chép vào máy của ứng dụng và lưu trữ bằng IndexedDB. Bản minh hoạ chỉ cung cấp tính năng tìm kiếm và hiển thị theo kiểu tự động hoàn thành của một bản ghi nhân viên, nhưng điều thú vị là sau khi dữ liệu này có trên ứng dụng, chúng ta cũng có thể sử dụng dữ liệu này theo nhiều cách khác. Dưới đây là bản tóm tắt cơ bản về những việc ứng dụng của chúng ta cần làm.
- Chúng ta phải thiết lập và khởi chạy một thực thể của IndexedDB. Đối với hầu hết các trường hợp, việc này khá đơn giản, nhưng việc làm cho tính năng này hoạt động trong cả Chrome và Firefox lại hơi phức tạp.
- Chúng ta cần xem liệu có dữ liệu nào hay không, nếu không có thì hãy tải dữ liệu xuống. Hiện tại, việc này thường được thực hiện thông qua các lệnh gọi AJAX. Đối với bản minh hoạ, chúng tôi đã tạo một lớp tiện ích đơn giản để nhanh chóng tạo dữ liệu giả mạo. Ứng dụng sẽ cần nhận ra thời điểm tạo dữ liệu này và ngăn người dùng sử dụng dữ liệu cho đến thời điểm đó. Đây là thao tác một lần. Lần tiếp theo người dùng chạy ứng dụng, ứng dụng sẽ không cần phải trải qua quy trình này. Bản minh hoạ nâng cao hơn sẽ xử lý các thao tác đồng bộ hoá giữa ứng dụng và máy chủ, nhưng bản minh hoạ này tập trung nhiều hơn vào các khía cạnh giao diện người dùng.
- Khi ứng dụng đã sẵn sàng, chúng ta có thể sử dụng thành phần điều khiển Tự động hoàn thành của giao diện người dùng jQuery để đồng bộ hoá với IndexedDB. Mặc dù thành phần điều khiển Tự động hoàn thành cho phép các danh sách và mảng dữ liệu cơ bản, nhưng thành phần này có một API cho phép mọi nguồn dữ liệu. Chúng ta sẽ minh hoạ cách sử dụng API này để kết nối với dữ liệu IndexedDB.
Bắt đầu
Bản minh hoạ này có nhiều phần, vì vậy, để bắt đầu một cách đơn giản, hãy xem phần HTML.
<form>
<p>
<label for="name">Name:</label> <input id="name" disabled> <span id="status"></span>
</p>
</form>
<div id="displayEmployee"></div>
Không nhiều, phải không? Có ba khía cạnh chính mà chúng ta quan tâm đến giao diện người dùng này. Trước tiên là trường "name" (tên) sẽ được dùng để tự động hoàn thành. Trình tải này sẽ tải ở trạng thái tắt và sẽ được bật sau thông qua JavaScript. Span bên cạnh được dùng trong hạt ban đầu để cung cấp thông tin cập nhật cho người dùng. Cuối cùng, div có id displayEmployee sẽ được sử dụng khi bạn chọn một nhân viên trong tính năng tự động đề xuất.
Bây giờ, hãy cùng xem JavaScript. Có rất nhiều nội dung cần tìm hiểu ở đây nên chúng ta sẽ thực hiện từng bước. Mã đầy đủ sẽ có ở phần cuối để bạn có thể xem toàn bộ mã.
Trước tiên, chúng ta cần quan tâm đến một số vấn đề về tiền tố trong số các trình duyệt hỗ trợ IndexedDB. Dưới đây là một số mã trong tài liệu của Mozilla đã được sửa đổi để cung cấp các bí danh đơn giản cho các thành phần IndexedDB cốt lõi mà ứng dụng của chúng ta cần.
window.indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
Tiếp theo, một vài biến toàn cục mà chúng ta sẽ sử dụng trong suốt bản minh hoạ:
var db;
var template;
Bây giờ, chúng ta sẽ bắt đầu với khối jQuery document ready:
$(document).ready(function() {
console.log("Startup...");
...
});
Bản minh hoạ của chúng ta sử dụng Handlebars.js để hiển thị thông tin chi tiết về nhân viên. Chúng ta sẽ sử dụng phần này sau, nhưng hiện tại, chúng ta có thể biên dịch mẫu và loại bỏ phần này. Chúng ta đã thiết lập một khối tập lệnh dưới dạng loại được Handlebars nhận dạng. Mã này không quá phức tạp nhưng giúp bạn dễ dàng hiển thị HTML động hơn.
<h2>, </h2>
Department: <br/>
Email: <a href='mailto:'></a>
Sau đó, mã này được biên dịch lại trong JavaScript như sau:
//Create our template
var source = $("#employeeTemplate").html();
template = Handlebars.compile(source);
Bây giờ, hãy bắt đầu làm việc với IndexedDB. Trước tiên, chúng ta sẽ mở ứng dụng.
var openRequest = indexedDB.open("employees", 1);
Việc mở kết nối với IndexedDB cho phép chúng ta truy cập để đọc và ghi dữ liệu, nhưng trước khi làm như vậy, chúng ta phải đảm bảo rằng mình có một objectStore. ObjectStore giống như một bảng cơ sở dữ liệu. Một IndexedDB có thể có nhiều objectStore, mỗi objectStore chứa một tập hợp các đối tượng có liên quan. Bản minh hoạ của chúng tôi rất đơn giản và chỉ cần một objectStore mà chúng tôi gọi là "nhân viên". Khi indexedDB được mở lần đầu tiên hoặc khi bạn thay đổi phiên bản trong mã, một sự kiện onupgradeneeded sẽ chạy. Chúng ta có thể sử dụng thông tin này để thiết lập objectStore.
// Handle setup.
openRequest.onupgradeneeded = function(e) {
console.log("running onupgradeneeded");
var thisDb = e.target.result;
// Create Employee
if(!thisDb.objectStoreNames.contains("employee")) {
console.log("I need to make the employee objectstore");
var objectStore = thisDb.createObjectStore("employee", {keyPath: "id", autoIncrement: true});
objectStore.createIndex("searchkey", "searchkey", {unique: false});
}
};
openRequest.onsuccess = function(e) {
db = e.target.result;
db.onerror = function(e) {
alert("Sorry, an unforseen error was thrown.");
console.log("***ERROR***");
console.dir(e.target);
};
handleSeed();
};
Trong khối trình xử lý sự kiện onupgradeneeded
, chúng ta kiểm tra objectStoreNames, một mảng các kho đối tượng, để xem liệu mảng này có chứa employee hay không. Nếu không, chúng ta chỉ cần thực hiện việc này. Lệnh gọi createIndex rất quan trọng. Chúng ta phải cho IndexedDB biết những phương thức nào (ngoài khoá) mà chúng ta sẽ sử dụng để truy xuất dữ liệu. Chúng ta sẽ sử dụng một biến có tên là searchkey. Chúng tôi sẽ giải thích điều này trong phần sau.
Sự kiện onungradeneeded
sẽ tự động chạy trong lần đầu tiên chúng ta chạy tập lệnh. Sau khi được thực thi hoặc bỏ qua trong các lần chạy trong tương lai, trình xử lý onsuccess
sẽ được chạy. Chúng ta đã xác định trình xử lý lỗi đơn giản (và xấu xí), sau đó gọi handleSeed
.
Vì vậy, trước khi tiếp tục, hãy cùng xem nhanh những gì đang diễn ra ở đây. Chúng ta mở cơ sở dữ liệu. Chúng ta kiểm tra xem kho đối tượng có tồn tại hay không. Nếu không, chúng ta sẽ tạo một thư mục. Cuối cùng, chúng ta gọi một hàm có tên là handleSeed. Bây giờ, hãy chuyển sang phần tạo dữ liệu của bản minh hoạ.
Gimme Some Data!
Như đã đề cập trong phần giới thiệu của bài viết này, bản minh hoạ này đang tạo lại một ứng dụng kiểu Intranet cần lưu trữ bản sao của tất cả nhân viên đã biết. Thông thường, việc này sẽ liên quan đến việc tạo một API dựa trên máy chủ có thể trả về số lượng nhân viên và cung cấp cách để chúng ta truy xuất hàng loạt bản ghi. Bạn có thể tưởng tượng một dịch vụ đơn giản hỗ trợ tính năng bắt đầu đếm và trả về 100 người mỗi lần. Quá trình này có thể chạy không đồng bộ ở chế độ nền trong khi người dùng đang làm việc khác.
Đối với bản minh hoạ, chúng ta sẽ làm một việc đơn giản. Chúng ta sẽ thấy số lượng đối tượng (nếu có) trong IndexedDB. Nếu dưới một số lượng nhất định, chúng ta sẽ chỉ tạo người dùng giả. Nếu không, chúng ta được coi là đã hoàn tất phần hạt giống và có thể bật phần tự động hoàn thành của bản minh hoạ. Hãy xem handleSeed.
function handleSeed() {
// This is how we handle the initial data seed. Normally this would be via AJAX.
db.transaction(["employee"], "readonly").objectStore("employee").count().onsuccess = function(e) {
var count = e.target.result;
if (count == 0) {
console.log("Need to generate fake data - stand by please...");
$("#status").text("Please stand by, loading in our initial data.");
var done = 0;
var employees = db.transaction(["employee"], "readwrite").objectStore("employee");
// Generate 1k people
for (var i = 0; i < 1000; i++) {
var person = generateFakePerson();
// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();
resp = employees.add(person);
resp.onsuccess = function(e) {
done++;
if (done == 1000) {
$("#name").removeAttr("disabled");
$("#status").text("");
setupAutoComplete();
} else if (done % 100 == 0) {
$("#status").text("Approximately "+Math.floor(done/10) +"% done.");
}
}
}
} else {
$("#name").removeAttr("disabled");
setupAutoComplete();
}
};
}
Dòng đầu tiên hơi phức tạp vì chúng ta có nhiều toán tử được nối với nhau, vì vậy, hãy phân tích dòng này:
db.transaction(["employee"], "readonly");
Thao tác này sẽ tạo một giao dịch chỉ có thể đọc mới. Tất cả các thao tác dữ liệu với IndexedDB đều yêu cầu một loại giao dịch.
objectStore("employee");
Lấy cửa hàng đối tượng nhân viên.
count()
Chạy API đếm – như bạn có thể đoán – thực hiện việc đếm.
onsuccess = function(e) {
Và khi hoàn tất, hãy thực thi lệnh gọi lại này. Bên trong lệnh gọi lại, chúng ta có thể nhận được giá trị kết quả là số lượng đối tượng. Nếu số lượng bằng 0, chúng ta sẽ bắt đầu quá trình tạo hạt giống.
Chúng ta sử dụng div trạng thái đã đề cập trước đó để thông báo cho người dùng rằng chúng ta sẽ bắt đầu nhận dữ liệu. Do bản chất không đồng bộ của IndexedDB, chúng ta đã thiết lập một biến đơn giản là done để theo dõi các mục bổ sung. Chúng ta lặp lại và chèn những người giả mạo. Nguồn của hàm đó có trong tệp tải xuống, nhưng hàm này trả về một đối tượng có dạng như sau:
{
firstname: "Random Name",
lastname: "Some Random Last Name",
department: "One of 8 random departments",
email: "first letter of firstname+lastname@fakecorp.com"
}
Chỉ riêng thông tin này thôi cũng đủ để xác định một người. Tuy nhiên, chúng tôi có một yêu cầu đặc biệt để có thể tìm kiếm dữ liệu của mình. IndexedDB không cung cấp cách tra cứu các mục theo cách không phân biệt chữ hoa chữ thường. Do đó, chúng ta sẽ sao chép trường lastname vào một thuộc tính mới là searchkey. Nếu bạn còn nhớ, đây là khoá mà chúng ta đã nói là cần được tạo dưới dạng chỉ mục cho dữ liệu của chúng ta.
// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();
Vì đây là nội dung sửa đổi dành riêng cho ứng dụng, nên việc này được thực hiện ở đây thay vì trên máy chủ phụ trợ (hoặc trong trường hợp của chúng ta là máy chủ phụ trợ ảo).
Để thực hiện việc thêm cơ sở dữ liệu một cách hiệu quả, bạn nên sử dụng lại giao dịch cho tất cả các hoạt động ghi hàng loạt. Nếu bạn tạo một giao dịch mới cho mỗi lần ghi, trình duyệt có thể gây ra một lần ghi ổ đĩa cho mỗi giao dịch và điều đó sẽ làm hiệu suất của bạn giảm sút khi thêm nhiều mục (hãy nghĩ đến "1 phút để ghi 1000 đối tượng" – rất tệ).
Sau khi tạo hạt giống, phần tiếp theo của ứng dụng sẽ được kích hoạt – setupAutoComplete.
Tạo tính năng Tự động hoàn thành
Bây giờ, chúng ta sẽ đến phần thú vị – kết nối với trình bổ trợ Tự động hoàn thành trên giao diện người dùng jQuery. Giống như hầu hết giao diện người dùng jQuery, chúng ta bắt đầu bằng một phần tử HTML cơ bản và nâng cao phần tử đó bằng cách gọi một phương thức hàm khởi tạo trên phần tử đó. Chúng ta đã trừu tượng hoá toàn bộ quy trình thành một hàm có tên là setupAutoComplete. Bây giờ, hãy xem mã đó.
function setupAutoComplete() {
//Create the autocomplete
$("#name").autocomplete({
source: function(request, response) {
console.log("Going to look for "+request.term);
$("#displayEmployee").hide();
var transaction = db.transaction(["employee"], "readonly");
var result = [];
transaction.oncomplete = function(event) {
response(result);
};
// TODO: Handle the error and return to it jQuery UI
var objectStore = transaction.objectStore("employee");
// Credit: http://stackoverflow.com/a/8961462/52160
var range = IDBKeyRange.bound(request.term.toLowerCase(), request.term.toLowerCase() + "z");
var index = objectStore.index("searchkey");
index.openCursor(range).onsuccess = function(event) {
var cursor = event.target.result;
if(cursor) {
result.push({
value: cursor.value.lastname + ", " + cursor.value.firstname,
person: cursor.value
});
cursor.continue();
}
};
},
minLength: 2,
select: function(event, ui) {
$("#displayEmployee").show().html(template(ui.item.person));
}
});
}
Phần phức tạp nhất của mã này là việc tạo thuộc tính nguồn. Chế độ kiểm soát Tự động hoàn thành của giao diện người dùng jQuery cho phép bạn xác định một thuộc tính nguồn có thể được tuỳ chỉnh để đáp ứng mọi nhu cầu có thể có – ngay cả dữ liệu IndexedDB của chúng tôi. API cung cấp cho bạn yêu cầu (về cơ bản là nội dung bạn đã nhập vào trường biểu mẫu) và lệnh gọi lại phản hồi. Bạn chịu trách nhiệm gửi một mảng kết quả trở lại lệnh gọi lại đó.
Việc đầu tiên chúng ta làm là ẩn div displayEmployee. Div này dùng để hiển thị một nhân viên riêng lẻ và xoá nhân viên đó nếu trước đó đã tải. Bây giờ, chúng ta có thể bắt đầu tìm kiếm.
Chúng ta bắt đầu bằng cách tạo một giao dịch chỉ có thể đọc, một mảng có tên là result và một trình xử lý oncomplete chỉ cần truyền kết quả đến thành phần điều khiển tự động hoàn thành.
Để tìm các mục khớp với dữ liệu đầu vào, hãy sử dụng mẹo của người dùng StackOverflow Fong-Wan Chau: Chúng ta sử dụng phạm vi chỉ mục dựa trên dữ liệu đầu vào làm ranh giới cuối thấp hơn và dữ liệu đầu vào cộng với chữ cái z làm ranh giới phạm vi trên. Xin lưu ý rằng chúng ta viết hoa chữ cái đầu tiên của cụm từ để khớp với dữ liệu viết hoa chữ cái đầu tiên mà chúng ta đã nhập.
Sau khi hoàn tất, chúng ta có thể mở một con trỏ (coi như đang chạy một truy vấn cơ sở dữ liệu) và lặp lại các kết quả. Tính năng tự động hoàn thành của giao diện người dùng jQuery cho phép bạn trả về bất kỳ loại dữ liệu nào bạn muốn, nhưng tối thiểu phải có một khoá giá trị. Chúng ta đặt giá trị thành phiên bản tên được định dạng đẹp mắt. Chúng ta cũng trả về toàn bộ người. Bạn sẽ thấy lý do trong giây lát. Trước tiên, đây là ảnh chụp màn hình về tính năng tự động hoàn thành đang hoạt động. Chúng ta sẽ sử dụng giao diện Vader cho giao diện người dùng jQuery.
Chỉ riêng điều này cũng đủ để trả về kết quả của các kết quả trùng khớp IndexedDB cho tính năng tự động hoàn thành. Tuy nhiên, chúng ta cũng muốn hỗ trợ hiển thị chế độ xem chi tiết của trận đấu khi người dùng chọn một trận đấu. Chúng ta đã chỉ định một trình xử lý lựa chọn khi tạo tính năng tự động hoàn thành sử dụng mẫu Handlebars từ trước.