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 qua, bạn nên đọc các hướng dẫn MDN hữu ích 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. Mặc dù bạn chưa từng thấy IndexedDB, nhưng hy vọng bản minh hoạ trong bài viết này sẽ giúp bạn biết được những gì có thể thực hiện được với nó.

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 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 tuyệt vời là sau khi dữ liệu này có sẵn trên máy khách, chúng ta cũng có thể sử dụng dữ liệu đó 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.

  1. Chúng ta phải thiết lập và khởi chạy một thực thể của IndexedDB. Việc này khá đơn giản, nhưng để làm cho tính năng này hoạt động trong cả Chrome và Firefox thì có chút phức tạp.
  2. 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 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.
  3. 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 phương thức này để kết nối với dữ liệu IndexedDB.

Bắt đầu

Chúng ta có nhiều phần trong bản minh hoạ này, vì vậy, để bắt đầu, hãy cùng xem xét 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. Khoảng cách bên cạnh được dùng trong nội dung gốc 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, vì vậy, 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 lo ngại về một số vấn đề liên quan đến 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 tôi mở tệp đó.

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à "emNhân viên". Khi lập chỉ mụcDB đượ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 cần nâng cấp sẽ được 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 đối tượng lưu trữ để xem đối tượng đó có chứa nhân viên 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. Điều này sẽ được giải thích một chút.

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 một trình xử lý lỗi đơn giản (và xấu) rồi 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 tôi sẽ tạo ra ứng dụng đó. 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 thấp hơn một số nhất định, chúng tôi sẽ chỉ tạo người dùng giả mạo. Nếu không, chúng ta sẽ được coi là đã xong với phần 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 liên kết với nhau, vì vậy hãy phân tích nó:

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");

Tải kho đố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 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ế là đủ để xác định một con người. Nhưng chúng tôi có yêu cầu đặc biệt để có thể tìm kiếm dữ liệu của chúng tôi. 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ớ thì đây là khoá mà chúng ta đã nói phải tạo để làm chỉ mục cho dữ liệu.

// Modify our data to add a searchable field
person.searchkey = person.lastname.toLowerCase();

Vì đây là một nội dung sửa đổi dành riêng cho ứng dụng khách nên thao tác này được thực hiện ở đây trái ngược với máy chủ phụ trợ (hoặc trong trường hợp của chúng ta là máy chủ phụ trợ ảo).

Để bổ sung cơ sở dữ liệu một cách hiệu quả, bạn nên sử dụng lại giao dịch này cho tất cả các lượt 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ẽ khiến 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. Tính năng 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 loạt các kết quả về 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, một mảng được gọi là kết quả và một trình xử lý oncomplete chỉ cần chuyển kết quả đến nút đ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. Ngoài ra, xin lưu ý rằng chúng tôi viết thường cụm từ đó để 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ỏ (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.