Why AJAX Isn’t Enough - Tại sao nói chỉ mình AJAX là chưa đủ???

Các cuộc gọi AJAX đã tạo ra 1 bước tiến khổng lồ cho tương tác người dùng trên Web : Chúng ta không còn cần phải tải lại trang (reload page) để đáp ứng với mỗi đầu vào người dùng (user input) - không phải tải lại trang mỗi khi thực hiện xong 1 request nào đó của người dùng. Sử dụng AJAX, chúng ta có thể gọi các thủ tục cụ thể trên máy chủ và cập nhật trang web dựa trên các giá trị trả về và đưa các giá trị này vào ứng dụng thông qua tương tác nhanh (phần giá trị thay đổi sẽ được cập nhật nhanh trên giao diện mà không cần tải lại trang).

Nhưng những cuộc gọi AJAX này không bao gồm các bản cập nhật đến từ máy chủ nên việc đáp ứng được thời gian thực và cộng tác Web là rất cần thiết thì AJAX lại không đáp ứng được (Trong những ứng dụng web cập nhật tỉ số các trận bóng, các cuộc thi đấu hoặc sàn giao dịch chứng khoán... thì yêu cầu thời gian thực và tương tác web lại càng quan trọng hơn). Vì vậy các mô hình nhắn tin khác được thêm vào cùng với AJAX để bổ trợ những khiếm khuyết cho nó là cần thiết. PubSub - “publish and subscribe” là 1 mô hình nhắn tin như thế.

Trong bài viết này chúng ta sẽ cùng nhau tìm hiểu những điểm đạt được - và chưa đạt được của AJAX; đồng thời xem xét 1 cách chính xác cách mà PubSub giải quyết vấn đề cập nhật trực tiếp tới các client khác mà không cần client đó phải làm mới trang, xem xét giải pháp cụ thể - giao thức WAMP tích hợp việc gọi các thủ tục trên server với PubSub thành 1 API đơn - điều mà AJAX chưa làm được như thế nào.

AJAX làm được gì - What AJAX Solved

Trước khi AJAX ra đời, việc tương tác trên các trang web thật là phiền phức và kinh khủng. Bất cứ tương tác nào của người dùng đều yêu cầu tạo ra một phiên bản cập nhật của trang trên máy chủ sau đó gửi cho trình duyệt và hiển thị (render) lại trang này. Trong mô hình này thì các đơn vị cơ bản của tương tác là trang. Các yêu cầu trình duyệt gửi đến máy chủ bất kể là yêu cầu cập nhật nhỏ như thế nào thì kết quả trả về luôn luôn là một trang mới đầy đủ (full page), việc này gây lãng phí cả về lưu lượng đường truyền và tài nguyên máy chủ đồng thời còn gây ức chế cho người dùng vì quá chậm.

AJAX ra đời đã phá bỏ mô hình "ngu ngốc" trên : với AJAX bạn có thể gửi dữ liệu và request lên máy chủ, kết quả nhận được chỉ là kết quả cho sự tương tác kích hoạt bởi request đó và sau đó cập nhật các phần liên quan của trang dựa trên response (phản hồi) này. Với AJAX chúng ta đã chuyển từ 1 lời gọi tổng quát duy nhất (“Give me a new page” - cho tôi 1 trang mới) sang nhiều lời gọi tương tác cụ thể. Với AJAX chúng ta đã tạo các lời gọi thủ tục từ xa (RPC) trên máy chủ.

Ví dụ về 1 ứng dụng web đơn giản - lấy biểu quyết có sử dụng AJAX :

ajax1.png

Người dùng có thể bình chọn cho bất kỳ loại hương vị nào mình yêu thích trong số 3 hình đưa ra và có thể reset - khởi động lại bình chọn. Mỗi 1 click vote sẽ được xử lý bằng AJAX thực hiện việc gì đó ví dụ như đoạn code sau :

var xhr = new XMLHttpRequest();
xhr.open('get', 'send-vote-data.php');

xhr.onreadystatechange = function() {
   if(xhr.readyState === 4) {
      if(xhr.status === 200) {

      // Cập nhật vote count dựa trên kết quả lời gọi
      } else{
         alert('Error: '+xhr.status); // Có lỗi khi gửi request
      }
   }
}

Sau đó chỉ cần thay đổi số lượng bình chọn cho hương vị được bình chọn bởi người dùng thông qua thủ tục trả về của lời gọi AJAX, từ đó thay bằng việc render toàn bộ 1 trang bằng việc cập nhật 1 phần tử DOM duy nhất. Việc này giảm thiểu công việc cho máy chủ rất nhiều và tiết kiệm rất nhiều lưu lượng đường truyền chẳng hạn như trong ví dụ trên chúng ta sẽ nhận về 1 vote count thay vì 1 trang web đầy đủ. Quan trọng nhất nó cho phép cập nhật 1 cách nhanh chóng trên giao diện người dùng, cải thiện đáng kể tạo sự thuận lợi và dễ chịu cho người dùng.

Những tồn tại AJAX chưa giải quyết được - What Remains Unsolved

Ví dụ trên trong thực tế có thể có trường hợp sẽ có rất nhiều người dùng bỏ phiếu song song, kiểm phiếu (vote count) sẽ thay đổi theo tương tác kết hợp của người dùng. Bởi vì cuộc gọi AJAX được kích hoạt bởi sự tương tác của một người dùng cụ thể nào đó và tương tác đó chỉ chỉ kết nối đến máy chủ nên người dùng chỉ nhìn thấy các con số vote hiện thời khi lúc mới tải ứng dụng, các thay đổi về số lượng bình chọn sau đó sẽ không được cập nhật trên giao diện trừ phi tải lại trang.

Điều này xảy ra là do AJAX chỉ cập nhật các trang trong đáp ứng (response) đến người dùng có tương tác. Nó không giải quyết được vấn đề các bản cập nhật đến từ máy chủ. Để giải quyết vấn đề này chúng ta cần một mô hình (mẫu) tin bổ sung mà sẽ gửi thông tin cập nhật tới client mà không cần user phải liên tục gửi request.

ajax2.png

Trong các ứng dụng đa người dùng, phân phối các bản cập nhật là chức năng trọng tâm. Ví dụ như hình trên khi có 1 request cập nhật từ 1 user (mũi tên màu xanh lá cây) thì máy chủ phải phân phối bản cập nhật này tới các user khác (mũi tên xanh dương).

PubSub: Updates From One To Many - Cập nhật từ 1 tới nhiều

Như đã nói ở trên chúng ta cần một mô hình (mẫu) tin bổ sung để xử lý cập nhật tới nhiều client, 1 ví dụ về mô hình đó là PubSub. Tại đây, 1 client sẽ khai báo quan tâm tới 1 chủ đề (“subscribe - đăng ký”) với 1 central broker (môi giới trung tâm). Khi client gửi 1 sự kiện cho 1 chủ đề đến broker (“publish”) thì broker sẽ phân phối sự kiện này tới tất cả các client hiện đang kết nối và đăng ký.

Một lợi thế của mô hình PubSub là các nhà xuất bản (phát hành các cập nhật - publishers) và các thuê bao (đăng ký topic - subscribers) được tách riêng thông qua broker. publisher không cần thiết phải biết thông tin các thuê bao (subscriber) hiện tại tới 1 topic và ngược lại các subscriber cũng không cần biết bất kỳ thông tin nào của các publisher, điều này giúp PubSub dễ dàng thực thi.

PubSub có sẵn nhiều nhiều mô hình triển khai để lựa chọn tùy thuộc vào loại mô hình (framework) back-end và front-end, thư viện và ngôn ngữ bạn sử dụng. Ví dụ như với Node.js hoặc Ruby bạn có thể sử dụng Faye. Nếu bạn không muốn chạy broker riêng thì các dịch vụ web như Pusher sẽ tổ chức các chức năng cho bạn.

Two Messaging Patterns, Two Technologies? - 2 mẫu nhắn tin, 2 công nghệ?

Không quá khó khăn để tìm được 1 công nghệ PubSub phù hợp với nhu cầu của 1 ứng dụng hoặc trang web cụ thể. Nhưng ngay cả đối với những ứng dụng đơn giản như bình chọn phía trên thì cũng vẫn cần sử dụng cả RPC và PubSub để gửi và request dữ liệu cũng như nhận các cập nhật tự động. Với bất kỳ các giải pháp PubSub thuần nào thì bạn cũng đều phải sử dụng 2 công nghệ khác nhau cho việc nhắn tin cho ứng dụng : AJAX và PubSub.

Điều này có 1 số nhược điểm :

  • Bạn cần phải thiết lập 2 ngăn xếp công nghệ (tech stack) có thê là 2 máy chủ và đảm bảo cho chúng cập nhật và chạy (vận hành)
  • Ứng dụng cần kết nối riêng biệt cho cho 2 mô hình nhắn tin, đòi hỏi tài nguyên máy chủ hơn. 2 kết nối này cũng sẽ yêu cầu cơ chế xác thực và ủy quyền của riêng mình làm tăng những thực hiện phức tạp, điều này dễ gây ra sai sót.
  • Trên máy chủ sẽ cần phải tích hợp 2 ngăn xếp công nghệ vào ứng dụng và phối hợp 2 thứ này với nhau.
  • Đối với các nhà phát triển front-end, cũng có các mối quan tâm tương tự như : thiết lập và xử lý 2 kết nối và đối phó với 2 API riêng biệt.

WAMP : RPC và PubSub

WAMP là từ viết tắt của Web Application Messaging Protocol - giao thức nhắn tin cho web ứng dụng, giải quyết các nhược điểm phiền phức trên bằng cách tích hợp RPC và PubSub thành 1 giao thức duy nhất, từ đó bạn sẽ chỉ có 1 thư viện, 1 kết nối và 1 API duy nhất. Nó sẽ xử lý tất cả các tin nhắn giữa front-end của trình duyệt và back-end của ứng dụng.

WAMP là 1 giao thức mở, nó có 1 mã nguồn thực thi JavaScript mở (Autobahn|JS) chạy cả trong trình duyệt và Node.js, cho phép bạn tạo các ứng dụng JavaScript thuần. Các trình thực thi mã nguồn mở còn dùng cho nhiều ngôn ngữ khác, vì vậy bạn có thể sử dụng PHP, Java, Python... cũng như JavaScript trên máy chủ.

ajax3.png

Với WAMP bạn có thể truyền chức năng ứng dụng thông qua nhiều ngôn ngữ

Những ngôn ngữ này không bị giới hạn đến back end - bạn cũng có thể dùng các thư viện của WAMP cho các client bản địa (native client), để web và các native client này sử dụng chung 1 giao thức. Ví dụ như thư viện C++ rất thích hợp để chạy các thành phần (component) WAMP trên các thiết bị nhúng mã nguồn giới hạn - ví dụ như các bộ cảm biến suy nghĩ trong ứng dụng Internet of Things.

Các kết nối WAMP được thiết lập không phải từ trình duyệt tới back-end mà tới 1 router WAMP - thứ để phân phối tin nhắn. Nó đảm nhận vai trò của nhà trung gian (broker) trong PubSub vì vậy máy chủ của bạn chỉ cần thông báo sự kiện tới router, router sẽ phân phối sự kiện đó tới tất cả các thuê bao (subscriber - các client đăng ký topic liên quan). Đối với RPC, front-end phát cuộc gọi cho 1 thủ tục từ xa đến router, tại đây sẽ chuyển tiếp tới back-end đã đăng ký thủ tục; sau đó nó sẽ trả kết quả từ back-end về cho caller (front end gọi thủ tục). Điều này tách riêng 2 đầu front-end và back-end giống như PubSub. Bạn có thể truyền chức năng qua 1 vài cá thể (instance) back-end mà front-end không cần phải biết về sự tồn tại của các instance đó.

Có các tính năng giao thức mở rộng trên đầu các tuyến cơ bản chẳng hạn như chứng thực khách hàng, ủy quyền dựa trên vai trò và chủ đề xuất bản, hạn chế xuất bản(thông báo) tới 1 số client cụ thể nào đó. Router WAMP cung cấp các tập khác nhau của các chức năng tiên tiến này.

Ở phần sau đây chúng ta sẽ xem xét làm thế nào để giải quyết vấn đề cập nhật thời gian thực cho ứng dụng bình chọn ở ví dụ trên sử dụng WAMP và bạn sẽ thấy WAMP xử lý tốt như RPC thế nào.

Cập nhật trực tiếp kết quả bình chọn: Sử dụng WebSockets và WAMP

Chúng ta sẽ có 1 cái nhìn sâu sắc hơn về chức năng nhắn tin theo yêu cầu của các ứng dụng bình chọn và tìm hiểu qua cách thực hiện việc này trong trình duyệt và trên máy chủ. Để đơn giản hóa nhất có thể thì mã back-end cũng sẽ có trong JavaScript và chạy trong 1 tab của trình duyệt.

“back-end trong trình duyệt” là khả thi bởi vì các trình duyệt client có thể đăng ký các thủ tục cho cuộc gọi từ xa giống như bất kỳ WAMP client nào khác. Điều này có nghĩa là chưa kể đến hiệu năng, mã trình duyệt đều có khả năng như mã chạy phía trong như Node.js. Bạn có thể vào đây để xem code demo trên Github cho ứng dụng bình chọn, trong đó cũng có chỉ dẫn chạy ứng dụng và cách sử dụng router WAMP (Crossbar.io).

INCLUDING A WAMP LIBRARY - Include thư viện WAMP

Muốn làm ứng dụng WAMP thì điều đầu tiên cần làm là include 1 thư viện WAMP. Ở đây chúng ta sẽ sử dụng Autobahn|JS.

Để phát triển và test trên trình duyệt thì chỉ cần include nó như sau:

<script src="https://autobahn.s3.amazonaws.com/autobahnjs/latest/autobahn.min.jgz"></script>;

Phiên bản này không cho phép triển khai tới 1 website sản xuất và nó cũng bị giới hạn download từ các trang lưu trữ trên localhost hoặc lưu trữ trên 1 mạng IP nội bộ ví dụ như những IP trong phạm vi 192.168.1.x.

Thiết lập kết nối - ESTABLISHING A CONNECTION

Sau khi include thư viện chúng ta cần thiết lập 1 kết nối tới router WAMP:

var connection = new autobahn.Connection({
   url: "ws://example.com/wamprouter",
   realm: "votesapp"
});

Trong kết nối này đối số đầu tiên là URL của router WAMP (đường dẫn tới router). Ở đây sử dụng lược đồ ws thay vì http vì WAMP sử dụng WebSockets như phương tiện vận chuyển mặc định của nó. WebSockets cung cấp 1 kết nối 2 chiều liên tục cho phép đẩy từ máy chủ mà không có bất kỳ hack nào. Ngoài ra, không có tiêu đề HTTP đi kèm trong mỗi tin nhắn được chuyển giao sẽ giảm thiểu đáng kể chi phí đường truyền. WebSockets được hỗ trợ trong tất cả các trình duyệt hiện đại. Để hỗ trợ các trình duyệt cũ thì bạn xem thêm tại đây.

Đối số thứ 2 là realm, cần chọn 1 "vương quốc - vùng" để gắn kết nối này vào. Các realm này tạo các miền định tuyến riêng biệt trên router - do đó các thông điệp chỉ được định tuyến giữa các kết nối trong cùng 1 realm (có nghĩa là các thông điệp chỉ được truyền qua lại giữa các kết nối trong cùng realm với nhau). Ở đây thì chúng ta đang sử dụng 1 realm cụ thể dành riêng cho bản demo bình chọn, realm này có tên là votesapp.

Các đối tượng connection chúng ta đã tạo cho phép đính kèm 2 callback, 1 callback dùng khi kết nối đã được thiết lập và 1 callback dùng khi thiết lập kết nối đó bị fail hoặc đóng kết nối sau đó.

Việc xử lý onopen dưới đây được gọi sau khi kết nối được thiết lập, nó sẽ nhận được 1 đối tượng session. session này được truyền vào hàm main là hàm thực hiện chức năng của ứng dụng. Đối tượng session này được sử dụng cho các cuộc gọi tin nhắn WAMP.

connection.onopen = function (session, details) {
	main(session);
};

Cuối cùng, để các cuộc goi và thông điệp được truyền đi thì chúng ta càn phải kích hoạt mở kết nối:

connection.open();

Đăng ký và gọi 1 thủ tục

Front-end sẽ submit phiếu bình chọn bằng cách gọi 1 thủ tục ở back-end. Đầu tiên ta hãy định nghĩa hàm xử lý kết quả được submit:


var submitVote = function(args) {
   var flavor = args[0];
   votes[flavor] += 1;

   return votes[flavor];
};

Hàm này thực hiện chức năng tăng kết quả đếm của số lượng bình chọn và trả về kết quả này. Sau đó phải đăng ký hàm này với router WAMP thì mới có thể gọi tới nó:


session.register('com.example.votedemo.vote', submitVote)

Khi đăng ký hàm submitVote này chúng ta chỉ định 1 định danh duy nhất được sử dụng để gọi hàm. Ở đây, WAMP sử dụng các URI thể hiện trong gói chú thích Java (ví dụ như bắt đầu với TLD). Các URI này là rất thiết thực bởi vì chúng là 1 mô hình (pattern) được tổ chức tốt cho phép dễ dàng tách tên miền không gian - namespace.

Đó là các bước đăng ký thủ tục. Bây giờ hàm submitVote có thể được gọi từ bên ngoài bởi bất kỳ WAMP client nào (đã được xác thực) kết nối với cùng 1 realm.

Việc gọi hàm từ front-end được thực hiện như sau:


session.call('com.example.votedemo.vote',[flavor]).then(onVoteSubmitted)

Ở đây, kết quả trả về từ hàm submitVote sẽ được truyền vào bộ xử lý onVoteSubmitted.

Autobahn|JS thực hiện điều này không phải bằng cách sử dụng callback thông thường mà là với promises : session.call ngay lập tức trả về 1 đối tượng sẽ được giải quyết cuối cùng khi trở về cuộc gọi và hàm xử lý sau đó được thực hiện.

Đối với việc sử dụng cơ bản WAMP và Autobahn|JS thì bạn không cần quan tâm hoặc biết bất cứ gì về promises. Như đang sử dụng ở trên thì bạn có thể nghĩ chúng không khác gì so với các ghi chú (notation) trong các callback, còn nếu bạn quan tâm muốn nghiên cứu sâu hơn về nó thì JavaScript Promises là 1 suggest tốt dành cho bạn để bắt đầu tìm hiểu.

Đăng ký và phát hành các cập nhật - SUBSCRIBING AND PUBLISHING UPDATES

Sau đây chúng ta sẽ đi vào tìm hiểu làm thế nào để cập nhật cho các client khác? - điều mà AJAX không làm được.

Để nhận được thông tin cập nhật, client cần nói cho router WAMP biết những thông tin nào mà client này quan tâm bằng cách đăng ký chủ đề (topic), việc đăng ký được front-end thực hiện:


session.subscribe('com.example.votedemo.on_vote', updateVotes);

Chúng ta chỉ submit chủ đề (vẫn là 1 URI) và 1 hàm để thực thi mỗi lần nhận được sự kiện cho topic đó.

Tất cả những gì còn lại cần phải làm là gửi bản cập nhật bình chọn từ máy chủ. Để làm việc này, chỉ cần xây dựng đối tượng cập nhật mà chúng ta muốn gửi và sau đó phát hành (publish) đối tượng này đến cùng topic mà các trình duyệt của chúng ta đăng ký.

Việc publish này cần phải thực hiện bên trong tiến trình xử lý phiếu binhd chọn submitVote :


var submitVote = function(args, kwargs, details) {
   var flavor = args[0];
   votes[flavor] += 1;

   var res = {
      subject: flavor,
      votes: votes[flavor]
   };

   // publish kết quả tới các client đăng ký chủ đề
   session.publish('com.example.votedemo.on_vote', [res]);

   return votes[flavor];
};

Và đó là tất cả, cả việc submit phiếu bầu chọn đến back-end và việc cập nhật kết quả bầu chọn tới tất cả các trình duyệt được kết nối đều được xử lý bởi 1 giao thức duy nhất. Có thể nói không còn cái gì có thể đơn giản và cơ bản hơn sử dụng WAMP.

Lời kết

Mặc dù tiêu đề bài viết là nói về các khiếm khuyết của AJAX và các cách để bổ sung cho khiếm khuyết đó nhưng đồng thời bài này cũng muốn giới thiệu với các bạn về WAMP và những điểm tuyệt vời của nó :

WAMP hợp nhất ứng dụng nhắn tin của bạn - với RPC và PubSub, bạn có thể cover mọi thứ cần thiết cho ứng dụng của bạn. Với WebSockets, điều này được thực hiện bằng cách sử dụng 1 kết nối 2 chiều duy nhất có độ trễ thấp tới máy chủ do đó tiết kiệm tài nguyên server và tiết kiệm lưu lượng đường truyền, làm cho thời gian cho 1 chuyến round-trip rất ngắn. Bởi vì WAMP là 1 giao thức mở và thực thi cho nhiều ngôn ngữ nên bạn có thể chọn 1 option công nghệ back-end, và bạn có thể mở rộng ứng dụng của bạn vượt ra ngoài Web cho các client bản địa.

Với WAMP, dễ dàng viết các ứng dụng web hiện đại, tương tác với trải nghiệm người dùng tuyệt vời và cập nhật trực tuyến từ máy chủ - và mở rộng chúng vượt ra khỏi phạm vi Web.