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 hữu hiệu để lưu trữ dữ liệu ở phía máy khách. Nếu chưa xem xét thì bạn nên đọc hướng dẫn MMDN hữu ích về chủ đề này nếu chưa xem xét. Bài viết này giả định một số kiến thức cơ bản về API và các tính năng. Ngay cả khi bạn chưa từng xem IndexedDB trước đây, hy vọng bản minh họa trong bài viết này sẽ cho bạn ý tưởng về những gì có thể thực hiện với nó.

Bản minh hoạ của chúng tôi là một bằng chứng đơn giản về khái niệm Ứng dụng mạng nội bộ cho một công ty. Ứng dụng này sẽ cho phép nhân viên tìm kiếm nhân viên khác. Để cung cấp trải nghiệm nhanh hơn và nhanh 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à được 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 hồ sơ nhân viên, nhưng điều tuyệt vời là một khi dữ liệu này đã có trên máy khách, chúng tôi cũng có thể sử dụng theo nhiều cách khác. Dưới đây là phác thảo cơ bản về những việc ứng dụng của chúng tôi cần làm.

  1. Chúng ta phải thiết lập và khởi chạy một bản sao của IndexedDB. Đối với hầu hết hoạt động này, việc này rất đơn giản, nhưng làm cho nó hoạt động trong cả Chrome và Firefox lại là một việc khó khăn.
  2. Chúng tôi cần xem liệu chúng tôi có dữ liệu hay không, 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 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 dạng khi 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 chạy ứng dụng, người dùng sẽ không cần thực hiện quá trình này. Bản minh hoạ nâng cao hơn sẽ xử lý hoạt động đồ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 tính năng điều khiển Tự động hoàn thành của giao diện người dùng jQuery để đồng bộ hóa với IndexedDB. Mặc dù chế độ kiểm soát Tự động hoàn thành cho phép sử dụng các danh sách và mảng dữ liệu cơ bản, nhưng tính năng này còn có API để cho phép mọi nguồn dữ liệu. Chúng ta sẽ minh hoạ cách dùng dữ liệu này để kết nối với dữ liệu IndexedDB.

Bắt đầu

Chúng ta có nhiều phần cho bản minh hoạ này, 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 lắm, đúng không? Có 3 khía cạnh chính đối với giao diện người dùng này mà chúng ta quan tâm. Đầu tiên là trường "name" sẽ được dùng để tự động hoàn thành. API này sẽ tải và sẽ được bật sau qua JavaScript. Khoảng 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 nhân viên từ tính năng tự động đề xuất.

Giờ chúng ta hãy xem JavaScript. Có rất nhiều nội dung cần thông báo ở đây, vì vậy chúng tôi sẽ trình bày từng bước một. Mã đầy đủ sẽ hiển thị ở cuối để bạn có thể xem toàn bộ mã đó.

Trước tiên, có một số vấn đề về tiền tố mà chúng ta phải lo lắng trong số các trình duyệt hỗ trợ IndexedDB. Dưới đây là một số mã từ tài liệu Mozilla đã sửa đổi để cung cấp các bí danh đơn giản cho các thành phần IndexedDB chính 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 xuyên 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 của nhân viên. Sau này chúng ta sẽ không sử dụng nội dung đó, nhưng chúng ta có thể tiếp tục biên dịch mẫu ngay bây giờ và hoàn thành bước đó. Chúng ta đã thiết lập một khối tập lệnh dưới dạng một kiểu Thanh điều khiển nhận dạng được. Không quá cầu kỳ nhưng nó giúp hiển thị HTML động dễ dàng hơn.

<h2>, </h2>
Department: <br/>
Email: <a href='mailto:'></a>

Sau đó, đoạn mã này được biên dịch lại trong JavaScript của chúng tôi 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 của chúng tôi. Trước 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 vậy, chúng ta phải đảm bảo chúng ta có một objectStore. Một objectStore giống như một bảng cơ sở dữ liệu. Một IndexedDB có thể có nhiều objectStores, mỗi cửa hàng chứa một tập hợp các đối tượng liên quan. Bản minh hoạ của chúng ta rất đơn giản và chỉ cần một objectStore mà chúng ta gọi là "employee". Khi indexDB đượ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 onupgrade cần thiết sẽ chạy. Chúng ta có thể sử dụng lớp 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 sẽ kiểm tra objectStoreNames (một mảng lưu trữ đối tượng) để xem liệu đối tượng đó có chứa nhân viên hay không. Nếu không, chúng tôi 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á, chúng ta sẽ sử dụng để truy xuất dữ liệu. Chúng ta sẽ sử dụng một khoá tên là khoá tìm kiếm. Điều này sẽ được giải thích ngắn gọn.

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 thực thi hoặc bị 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 được 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 xem nhanh những gì đang xảy ra ở đây. Chúng ta sẽ mở cơ sở dữ liệu. Chúng tôi kiểm tra xem kho lưu trữ đối tượng của mình có tồn tại hay không. Nếu không, chúng tôi sẽ tạo. Cuối cùng, chúng ta gọi một hàm có tên là handleSeed. Bây giờ, hãy chú ý đến phần tạo dữ liệu trong bản minh hoạ.

Hãy cung cấp 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ái tạo 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ả cá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 cho chúng tôi cách truy xuất các lô bản ghi. Hãy tưởng tượng 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. Ứng dụng này có thể chạy không đồng bộ trong nền trong khi người dùng không làm những việc khác.

Đối với bản minh hoạ, chúng ta sẽ làm một cách đơn giản. Chúng ta thấy số lượng đối tượng trong IndexedDB của mình, nếu có. Nếu thấp hơn một con 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 tôi được coi là đã hoàn tất phần nội dung gốc và có thể bật phần tự động hoàn thành của bản minh hoạ. Hãy cùng tìm hiểu về 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 hoạt động liên kết với nhau, vì vậy hãy phân tích chi tiết:

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 yêu cầu một giao dịch nào đó.

objectStore("employee");

Tải kho lưu trữ đối tượng nhân viên.

count()

Chạy count API (như bạn có thể đoán) để thực hiện một lượt đế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, thì chúng ta sẽ bắt đầu quá trình gốc.

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 tính chất không đồng bộ của IndexedDB, nên chúng tôi đã thiết lập một biến đơn giản, đã hoàn tất, biến này sẽ theo dõi các lượt bổ sung. Chúng tôi lặp lại và chèn những người giả mạo. Nguồn của hàm đó có sẵn trong tệp tải xuống, nhưng 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 điều này cũng đủ để định nghĩa một con 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 tìm kiếm các mục theo cách không phân biệt chữ hoa chữ thường. Do đó, chúng tôi sao chép trường họ vào một thuộc tính mới là khoá tìm kiếm. Nếu bạn còn nhớ, đây là khoá mà chúng ta đã yêu cầu nê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à một sửa đổi dành riêng cho ứng dụng, nên nó được thực hiện tại đâ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 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, trình duyệt có thể gây ra việc ghi ổ đĩa cho mỗi giao dịch và điều đó sẽ làm cho hiệu suất của bạn trở nên rất tệ khi thêm nhiều mục (nguy nghĩ "1 phút để viết 1000 đối tượng" - rất tệ).

Sau khi nội dung gốc hoàn tấ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

Phần thú vị này là kết hợp với trình bổ trợ Tự động hoàn thành cho 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 với 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 hàm khởi tạo trên đó. Chúng tôi đã rút gọn toàn bộ quy trình thành một hàm có tên là setupAutoComplete. Hãy cùng xem mã đó ngay bây giờ.

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 tài sản 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 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 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 đó.

Điều đầu tiên chúng ta cần làm là ẩn div displayEmployee. Mục này dùng để hiển thị từng nhân viên và để xoá nhân viên nếu đã tải lên trước đó. Bây giờ, chúng ta có thể bắt đầu tìm kiếm.

Chúng tôi bắt đầu bằng cách tạo một giao dịch chỉ có thể đọc, một mảng được gọi là kết quả và một trình xử lý oncomplete chỉ chuyển kết quả đến tính năng kiểm soát tự động hoàn thành.

Để tìm các mục phù hợp với thông tin đầu vào, hãy cùng sử dụng mẹo của người dùng StackOverflow Fong-Wan Chau: Chúng tôi sử dụng một dải chỉ mục dựa trên dữ liệu đầu vào làm ranh giới dưới và chữ cái z làm ranh giới trên của phạm vi. 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 bằng chữ thường mà chúng tôi đã nhập.

Sau khi thực hiện xong - chúng ta có thể mở con trỏ (hãy coi như chạy truy vấn cơ sở dữ liệu) và lặp lại kết quả. Đ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 phải có khoá giá trị. Chúng ta đặt giá trị cho một phiên bản tên được định dạng tốt. Chúng tôi cũng sẽ trả lại toàn bộ con người. Bạn sẽ thấy lý do sau một giây. Đầu tiên là ảnh chụp màn hình tính năng tự động hoàn thành đang hoạt động. Chúng ta đang 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ả trùng khớp IndexedDB của chúng tôi cho tính năng tự động hoàn thành. Nhưng chúng tôi cũng muốn hỗ trợ hiển thị chế độ xem chi tiết về kết quả trùng khớp khi một kết quả trùng khớp được chọn. Chúng tôi đã chỉ định một trình xử lý chọn lọc khi tạo nội dung tự động hoàn thành sử dụng mẫu Tay cầm ở trước đó.