Thành phần liên kết dữ liệu trên giao diện người dùng với IndexedDB

Raymond Camden
Raymond Camden

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 hướng dẫn hữu ích của MDN về chủ đề này. Bài viết này giả định rằng 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 trước đây, hy vọng bản minh hoạ trong bài viết này sẽ giúp bạn hiểu rõ 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 mạng nội bộ đơn giản, chứng minh tính khả thi 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 hơn, linh hoạt hơn, cơ sở dữ liệu nhân viên được sao chép vào máy của khách hàng và lưu trữ bằng IndexedDB. Bản minh hoạ chỉ cung cấp một tính năng tìm kiếm theo kiểu tự động hoàn thành và hiển thị một bản ghi nhân viên, nhưng điều thú vị là khi dữ liệu này có sẵn trên ứng dụng, chúng ta cũng có thể sử dụng dữ liệu đó theo nhiều cách khác. Sau đâ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.

  1. Chúng ta phải thiết lập và khởi chạy một thực thể của IndexedDB. Hầu hết các bước đều đơn giản, nhưng việc làm cho tính năng này hoạt động trên cả Chrome và Firefox lại có một chút khó khăn.
  2. Chúng ta cần xem liệu có dữ liệu nào không, và nếu không, hãy tải dữ liệu đó xuống. Thông thường, việc này sẽ được thực hiện thông qua các lệnh gọi AJAX. Để 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ả. Ứng dụng cần nhận biết 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 đó. Quá trình này chỉ cần thực hiện 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 thực hiện 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 của giao diện người dùng.
  3. Khi ứng dụng đã sẵn sàng, chúng ta có thể sử dụng chế độ đ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ù chế độ đ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 chế độ này có một API cho phép mọi nguồn dữ liệu. Chúng tôi sẽ minh hoạ cách sử dụng tính năng 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, đúng không? Có 3 khía cạnh chính của giao diện người dùng này mà chúng ta cần quan tâm. Đầu tiên là trường "name" (tên) sẽ được dùng cho tính năng tự động hoàn thành. Nút này được tải ở trạng thái tắt và sẽ được bật sau thông qua JavaScript. Khoảng thời gian bên cạnh được dùng trong quá trình khởi tạo ban đầu để cung cấp thông tin cập nhật cho người dùng. Cuối cùng, div có mã displayEmployee sẽ được dùng khi bạn chọn một nhân viên trong danh sách đề xuất tự động.

Bây giờ, hãy xem JavaScript. Có rất nhiều thông tin cần nắm bắt nên chúng ta sẽ tìm hiểu từng bước. Mã hoàn chỉnh sẽ được cung cấp ở phần cuối để bạn có thể xem toàn bộ.

Trước tiên, có một số vấn đề về tiền tố mà chúng ta phải lo ngại 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 sẵn sàng của tài liệu jQuery:

$(document).ready(function() {
  console.log("Startup...");
  ...
});

Bản minh hoạ của chúng tôi sử dụng Handlebars.js để hiển thị thông tin chi tiết về nhân viên. Mẫu này chưa được sử dụng ngay, nhưng chúng ta có thể tiếp tục và biên dịch mẫu ngay bây giờ để không phải bận tâm về nó nữa. Chúng ta có một khối tập lệnh được thiết lập dưới dạng loại được Handlebars nhận dạng. Mặc dù không quá phức tạp, nhưng cách này 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 của chúng ta 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. Đầu tiên, chúng ta 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 đọc và ghi dữ liệu, nhưng trước khi làm việc đó, 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à "employee". Khi indexedDB được mở lần đầu tiên hoặc khi bạn thay đổi phiên bản trong mã, sự kiện onupgradeneeded sẽ được chạy. Chúng ta có thể sử dụng thuộc tính 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 kho lưu trữ đối tượng) để xem mảng này có chứa employee hay không. Nếu không, chúng ta chỉ cần làm cho nó như vậ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 khoá có tên là searchkey. Điều này sẽ được giải thích sau.

Sự kiện onungradeneeded sẽ tự động chạy vào 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 sau này, trình xử lý onsuccess sẽ chạy. Chúng ta đã xác định một trình xử lý lỗi đơn giản (và không đẹp mắt) rồi gọi handleSeed.

Vì vậy, trước khi tiếp tục, hãy cùng xem nhanh điều gì đang xảy ra ở đây. Chúng ta mở cơ sở dữ liệu. Chúng ta kiểm tra xem kho lưu trữ đối tượng có tồn tại hay không. Nếu không, chúng ta sẽ tạo nó. 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 gieo mầm dữ liệu trong bản minh hoạ.

Cho tôi một số dữ liệu!

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 Mạng nội bộ 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 các lô bản ghi. Bạn có thể hình dung một dịch vụ đơn giản hỗ trợ số lượng bắt đầu và trả về 100 người cùng một lúc. Quá trình này có thể chạy không đồng bộ trong nền trong khi người dùng đang làm việc khác.

Trong 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 số lượng người dùng thấp hơn một mức nhất định, chúng ta sẽ chỉ cần tạo người dùng giả. Nếu không, chúng ta sẽ coi như đã hoàn tất phần dữ liệu gốc 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 thao tác được liên kết với nhau, vì vậy, hãy chia nhỏ 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ả thao tác dữ liệu với IndexedDB đều yêu cầu một số loại giao dịch.

objectStore("employee");

Lấy kho lưu trữ đố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 là 0, thì chúng ta sẽ bắt đầu quá trình gieo hạt.

Chúng ta sử dụng div trạng thái đã đề cập trước đó để gửi cho người dùng thông báo rằng chúng ta sẽ bắt đầu lấy dữ liệu. Do tính chất không đồng bộ của IndexedDB, chúng tôi đã thiết lập một biến đơn giản, done, để theo dõi các lượt bổ sung. Chúng ta lặp lại và chèn người giả. 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 là đủ để xác định một người. Nhưng 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 tạo một bản sao của trường lastname vào một thuộc tính mới, searchkey. Nếu bạn còn nhớ, đây là khoá mà chúng ta đã nói là nên được tạo dưới dạng chỉ mục cho dữ liệu.

// 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 nội dung 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ợ giả định).

Để thực hiện việc bổ sung cơ sở dữ liệu theo cách hiệu quả, bạn nên sử dụng lại giao dịch cho tất cả các lượt ghi theo lô. Nếu bạn tạo một giao dịch mới cho mỗi lần ghi, thì 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ẽ khiến hiệu suất của bạn trở nên tồi tệ khi thêm nhiều mục (hãy nghĩ đến "1 phút để ghi 1000 đối tượng" – thật tồi tệ).

Sau khi hoàn tất việc gieo hạt, 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

Giờ là phần thú vị – kết nối với trình bổ trợ Tự động hoàn thành của giao diện người dùng jQuery. Như với 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à cải thiện phần tử đó bằng cách gọi một phương thức khởi tạo trên phần tử đó. Chúng tôi đã tóm tắt toàn bộ quy trình thành một hàm có tên là setupAutoComplete. Bây giờ, hãy xem đoạn 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ế độ điều khiển 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ể tuỳ chỉnh để đáp ứng mọi nhu cầu có thể – ngay cả dữ liệu IndexedDB của chúng tôi. API này cung cấp cho bạn yêu cầu (về cơ bản là nội dung được 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 được dùng để hiển thị một nhân viên riêng lẻ và nếu một nhân viên đã được tải lên trước đó, thì div này sẽ xoá nhân viên đó. Giờ thì 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ỉ đơn giản là 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 dải 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 dải trên. Cũng xin lưu ý rằng chúng tôi viết thường cụm từ này để khớp với dữ liệu viết thường mà chúng tôi đã nhập.

Sau khi hoàn tất, chúng ta có thể mở một con trỏ (hãy coi việc này giống như chạy một truy vấn cơ sở dữ liệu) và lặp lại các kết quả. Chế độ điều khiển 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 yêu cầu tối thiểu một khoá giá trị. Chúng ta đặt giá trị thành một phiên bản tên được định dạng đẹp mắt. Chúng tôi cũng trả về toàn bộ đối tượng người. Bạn sẽ biết lý do ngay sau đây. Đầu tiên, đây là ảnh chụp màn hình của tính năng tự động hoàn thành đang hoạt động. Chúng tôi đang sử dụng giao diện Vader cho giao diện người dùng jQuery.

Chỉ riêng việc này là đủ để trả về kết quả của các kết quả khớp IndexedDB cho tính năng tự động hoàn thành. Nhưng chúng ta cũng muốn hỗ trợ việc hiển thị chế độ xem chi tiết của trận đấu khi một trận đấu được chọn. Chúng tôi đã 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 đó.