[NodeJS] Bài 4 – Yêu Cầu Web Tĩnh & Đường Dẫn Thư Mục

Trong bài viết này, chúng ta sẽ cùng thảo luận về yêu cầu web tĩnh và cách làm việc với đường dẫn thư mục trong NodeJS. Đây là hai khái niệm không mới đối với chúng ta, tuy nhiên khi bắt tay vào việc tự xây dựng một phần mềm server cho riêng mình thì

Trong bài viết này, chúng ta sẽ cùng thảo luận về yêu cầu web tĩnh và cách làm việc với đường dẫn thư mục trong NodeJS. Đây là hai khái niệm không mới đối với chúng ta, tuy nhiên khi bắt tay vào việc tự xây dựng một phần mềm server cho riêng mình thì những yếu tố căn bản này lại trở nên đặc biệt quan trọng. Hãy cùng bắt đầu với các yêu cầu web tĩnh.

Các yêu cầu web tĩnh thì khác gì với những yêu cầu không tĩnh?

Ầy… chuyện dài lắm. Nhưng chúng ta cứ bắt đầu đơn giản thôi. Khi nào nói xong thì là hết chuyện. 😄

Đầu tiên thì là thao tác mà chúng ta vẫn làm hàng ngày, đó là truy cập vào các trang web và tìm kiếm tin tức cập nhật hoặc kiến thức. Chúng ta có thể xuất phát với trang chủ của một trang web rồi nhấn vào một liên kết bài viết nào đó. Một yêu cầu mới được tạo ra và gửi tới máy chủ của trang web nọ và liên kết URL trên thanh địa chỉ của trình duyệt web cũng thay đổi, chúng ta nhận được kết quả phản hồi là một trang đơn mới được thay thế vào trang đơn hiện tại.

Có trang web thì chúng ta thấy các liên kết trên thanh địa chỉ có dạng kết thúc là tên một tệp .html, ví dụ như trang blog cá nhân mà bạn đang sử dụng trên Github Pages; Và có những trang web khác thì các liên kết lại ở dạng lủn ngủn không liên quan gì tới tên của một tệp HTML, ví dụ như YouTube – https://www.youtube.com/watch?v=mQLvOJqGrcw.

Khi một yêu cầu được gửi về máy chủ với dạng đầu tiên thì chúng ta có thể gọi là yêu cầu web tĩnh. Trường hợp còn lại thì… không nhất thiết phải có tên gọi để phân biệt, chúng ta cứ biết nó không phải là yêu cầu web tĩnh thôi. 😄

Đối với trường hợp yêu cầu web tĩnh thì thông điệp gửi từ trình duyệt web được hiểu chính xác là tôi muốn xem tệp bai-blog-so-1001.html; Còn trường hợp như của link YouTube kia thì là tôi muốn xem watch một video có mã ký hiệu như thế này mQLvOJqGrcw. Ở đây chúng ta quan tâm tới yêu cầu web tĩnh trước là bởi vì cách thức vận hành đơn giản nhất của một server đó là chúng ta cứ kết nối yêu cầu xem một tệp tĩnh với một tệp dữ liệu có tên trùng khớp đang lưu trữ trong máy chủ. Và chúng ta cần phải thực hiện được điều này đã rồi mới có thể thả diều suy nghĩ tưởng tượng về những logic hoạt động phức tạp hơn được. 😄

Làm thế nào để xem được nội dung yêu cầu mà trình duyệt web gửi tới?

Trong đoạn code server mà chúng ta sử dụng từ đầu cho đến giờ có một điểm bị bỏ quên, đó là tham số request của hàm xử lý yêu cầu handleRequest.

nodejs-blog/server.js

/* Creating a server */const http =require('http');consthandleRequest=function(request, response){var fsPromises =require('fs/promises');var path ='static/index.html';

   fsPromises.readFile(path).then(function(data){
         response.setHeader('content-type','text/html');
         response.statusCode =200;
         response.end(data);}).catch(function(error){throw(error);});};// handleRequestconst server = http.createServer(handleRequest);/* Start running server ... */

Khi có một yêu cầu gửi tới từ trình duyệt web, server sẽ tạo ra một object IncomingMessage trong module http, và truyền vào vị trí tham số request của hàm xử lý handleRequest mà chúng ta đã viết. Và trong tài liệu về module http thì NodeJS có cung cấp cho chúng ta đủ thứ để truy xuất các thông tin liên quan tới yêu cầu được gửi tới – giao thức truyền tải, phương thức đóng gói thông tin, tiêu đề, tên miền, v.v…

Và sau một lượt nhìn ngó cái Table of contents của http thì chúng ta cũng biết được là các object IncomingMessage có một thuộc tính url để mô tả đường dẫn được biểu thị trong liên kết gửi yêu cầu từ trình duyệt web. Bây giờ chúng ta sẽ thử thêm thao tác in đường dẫn ra cửa sổ dòng lệnh mỗi khi có yêu cầu gửi tới server và theo dõi kết quả. Ở đây chúng ta sẽ đặt tạm một biến requestCount để theo dõi số lượt yêu cầu gửi tới.

nodejs-blog/server.js

/* Creating a server */const http =require('http');var requestCount =0;consthandleRequest=function(request, response){
   requestCount +=1;
   console.log(requestCount +': '+ request.url);...

Sau khi khởi động lại server thì chúng ta có thể thử truy cập lại một vài lần với những địa chỉ truy cập giả định tùy ý để xem kết quả tương ứng với các lượt truy cập.

http://127.0.0.1:3000

1: /
2: /asset/style.css
3: /asset/main.js
4: /favicon.ico

http://127.0.0.1:3000/post/an-article.html

5: /post/an-article.html
6: /asset/style.css
7: /asset/main.js
8: /favicon.ico

Ồ… như vậy là mỗi lần chúng ta gửi yêu cầu tới, hàm requestHandler hiện tại đang trả về nội dung của tệp index.html; Và các thẻ <link><script> lần lượt gửi tiếp yêu cầu tới để xin tải thêm tệp style.cssmain.js; Cuối cùng là trình duyệt tự gửi yêu cầu xin tải tệp ảnh favicon.ico để làm cái biểu tượng trên thanh tab bar.

Yêu cầu chính mà chúng ta nhận được là dòng đầu tiên của mỗi lượt nhập địa chỉ mới để truy cập. Đối với trang chủ thì là / và đối với lần tiếp theo là /post/an-article.html. Nếu vậy thì có lẽ là chúng ta cũng đoán ra được rồi, đoạn liên kết urlserver nhận được là tính từ vị trí kết thúc cái tên miền giống như trường hợp của cái link YouTube ở phía trên. 😄

Gửi tệp HTML đáp ứng các yêu cầu web tĩnh

[nodejs-blog]
   |
   +---[static]
   |      |
   |      +---[asset]
   |      |      |
   |      |      +---style.css
   |      |      +---main.js
   |      |
   |      +---[post]
   |      |      |
   |      |      +---an-article.html
   |      |      +---another-article.html
   |      |
   |      +---index.html
   |      +---oops.html
   |
   +---server.js
   +---test.js

Bây giờ chúng ta sẽ giả định các yêu cầu gửi tới cần xử lý –

  1. Nếu là / thì sẽ gửi trả nội dung của trang chủ index.html.
  2. Nếu ở dạng /post/an-article.html thì sẽ gửi trả nội dung của tệp HTML bài viết blog tương ứng là an-article.html.
  3. Trong trường hợp không tìm thấy bài viết tương ứng hoặc dạng yêu cầu khác thì sẽ gửi trả nội dung của trang oops.html.
  4. Nếu có dạng /asset/style.css thì sẽ gửi trả nội dung của tệp CSS tương ứng trong thư mục asset.
  5. Tương tự với dạng /asset/main.js thì sẽ gửi trả nội dung của tệp JavaScript tương ứng trong thư mục asset.
  6. Trong trường hợp không tìm thấy tệp hỗ trợ tương ứng trong thư mục asset thì in ra thông báo ở console của server.

Vậy bây giờ chúng ta cần chuẩn bị thêm nội dung đơn giản cho các tệp – oops.html, an-article.html, another-article.html, style.css, và main.js.

nodejs-blog/static/oops.html

<!doctypehtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Oops ! Not-Found</title><linkrel="stylesheet"href="/asset/style.css"></head><body><h1><h1>Oops ! Not-Found</h1></h1><scriptsrc="/asset/main.js"></script></body></html>

nodejs-blog/static/post/an-article.html

<!doctypehtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>An Article</title><linkrel="stylesheet"href="/asset/style.css"></head><body><h1>An Article</h1><scriptsrc="/asset/main.js"></script></body></html>

nodejs-blog/static/post/another-article.html

<!doctypehtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Another Article</title><linkrel="stylesheet"href="/asset/style.css"></head><body><h1>Another Article</h1><scriptsrc="/asset/main.js"></script></body></html>

nodejs-blog/static/asset/style.css

h1{font-size: 90;line-height: 1.5;text-align: center;}

nodejs-blog/static/asset/main.js

console.log('Client-side JavaScript');

Nếu như chúng ta có thể tìm và gửi trả chính xác các tệp được yêu cầu với dự kiến như trên thì chúng ta sẽ có thể sử dụng blog này giống như cách sử dụng Github Pages cơ bản; Và như vậy là trang blog của bạn sẽ có thể chuyển nội dung sang Glitch dần dần trong thời gian chúng ta tiếp tục học các logic xử lý phức tạp hơn. 😄

Bây giờ chúng ta sẽ xem lại đoạn code server mà chúng ta đã có và đặt một chút suy nghĩ cho hàm xử lý yêu cầu handleRequest.

nodejs-blog/test.js

/* Creating a server */const http =require('http');consthandleRequest=function(request, response){var fsPromises =require('fs/promises');var path ='static/index.html';

   fsPromises.readFile(path).then(function(data){
         response.setHeader('content-type','text/html');
         response.statusCode =200;
         response.end(data);}).catch(function(error){throw(error);});};// handleRequestconst server = http.createServer(handleRequest);/* Start running server */const port =3000;const hostname ='127.0.0.1';constcallback=function(){
   console.log('Server is running at...');
   console.log('http://'+ hostname +':'+ port +'/');};// callback

server.listen(port, hostname, callback);

Như vậy là chúng ta có tất cả các kiểu yêu cầu gửi tới được tập trung tiếp nhận tại handleRequest. Với mỗi đối tượng dữ liệu được yêu cầu là các nội dung bài viết post hay các nội dung hỗ trợ asset thì chúng ta sẽ lại cần phân chia lộ trình xử lý riêng.

Với mỗi kiểu tệp dữ liệu khác nhau thì khi phản hồi lại, chúng ta cũng sẽ phải thiết lập tiêu đề response.setHeader với kiểu nội dung content-type phù hợp để thông báo cho trình duyệt web. Ví dụ như text/html cho nội dung code HTML, text/css cho nội dung code CSS, hoặc text/javascript ….

Lúc này tổng quan logic hoạt động của hàm handleRequest về cơ bản là một cấu trúc điều kiện if dựa trên nội dung của request.url mà chúng ta nhận được. Như vậy chúng ta có thể chuyển tiếp tác vụ xử lý yêu cầu cho các hàm xử lý tác vụ phụ handleTypeRequest ở dạng như thế này –

nodejs-blog/test.js

/* --- */consthandleRequest=function(request, response){if(request.url =='/')handleHomeRequest(request, response);elseif(request.url.startsWith('/post'))handlePostRequest(request, response);elseif(request.url.startsWith('/asset'))handleAssetRequest(request, response);elsehandleOopsRequest(request, response);};// handleRequest/* ... */

Và lúc này chúng ta sẽ có thể di chuyển tất cả phần code xử lý yêu cầu chi tiết ra một tệp route.js bên ngoài ở dạng một module hỗ trợ nhỏ. Ở đây chúng ta cũng sẽ làm quen luôn với cú pháp export của CJS thay cho cú pháp của JavaScript cung cấp mặc định.

nodejs-blog/route.js

consthandleHomeRequest=function(request, response){
   response.end('Homepage');};// handleHomeRequestconsthandlePostRequest=function(request, response){
   response.end('Post');};// handlePostRequestconsthandleAssetRequest=function(request, response){
   response.end('Asset');};// handleAssetRequestconsthandleOopsRequest=function(request, response){
   response.end('Not found');};// handleOopsRequest// Xuất khẩu các thành phần của module// muốn chia sẻ cho code bên ngoài sử dụng
module.exports ={
   handleHomeRequest,
   handlePostRequest,
   handleOopsRequest,
   handleAssetRequest
};// exports

Thêm lệnh require vào test.js để sử dụng các hàm của route.js cung cấp. Ở bước này chúng ta có thể viết lại các hàm xử lý trong handleRequest thành dạng phương thức của object thu được sau khi require.

nodejs-blog/test.js

/* ... */consthandleRequest=function(request, response){var route =require('./route.js');if(request.url =='/')
      route.handleHomeRequest(request, response);/* ... */

Sau đó chúng ta cần khởi động lại server và thử truy cập với các dạng liên kết request.url để xem logic điều hành của handleRequest đã hoạt động chưa.

http://127.0.0.1:3000/

http://127.0.0.1:3000/post/an-article.html

http://127.0.0.1:3000/asset/style.css

http://127.0.0.1:3000/something-else

Sau khi đã chắc chắn logic điều hành của hàm handleRequest hoạt động ổn rồi thì chúng ta bắt đầu viết code chi tiết cho các hàm xử lý tác vụ phụ. Hàm đầu tiên là handleHomeRequest thì chúng ta chỉ việc copy/paste code xử lý cũ của handleRequest là có thể sử dụng được, vì trước đó chúng ta chỉ có thể gửi lại cho người xem duy nhất trang index.html đối với tất cả mọi yêu cầu. 😄

Tuy nhiên thì ở khối .catch, trong trường hợp vì lý do nào đó mà chúng ta ko thể truy xuất và đọc được tệp index.html thì phương án xử lý là thông báo lỗi ra console thay vì throw.

nodejs-blog/route.js

consthandleHomeRequest=function(request, response){var fsPromises =require('fs/promises');
   
   fsPromises.readFile('static'+'/index.html').then(function(data){
         response.setHeader('content-type','text/html');
         response.statusCode =200;
         response.end(data);}).catch(function(error){
         console.error(error);});};// handleHomeRequest/* ... */

http://127.0.0.1:3000/

Như vậy là nội dung của tệp index.html đã được trả về sau khi chúng ta gửi yêu cầu là /. Tuy nhiên tệp style.css vẫn chưa được tải kèm theo, và tệp main.js hiển nhiên cũng vậy. Chúng ta sẽ xử lý hàm handleAssetRequest sau đó để khắc phục điểm này.

Bây giờ theo trình tự là tới hàm handlePostRequest. Về đường dẫn để tìm kiếm tệp thì chúng ta vẫn xuất phát từ thư mục 'static' và chỉ cần thay thế chuỗi 'index.html' bằng request.url. Trong trường hợp không tìm thấy nội dung hay không đọc được tệp thì chúng ta sẽ cần in thông báo lỗi ra console, và gửi lại cho người dùng một trang đơn thông báo không tìm thấy nội dung – tức là chuyển quyền điều khiển tới cho hàm handleOopsRequest. 😄

nodejs-blog/route.js

/* ... */consthandlePostRequest=function(request, response){var fsPromises =require('fs/promises');

   fsPromises.readFile('static'+ request.url).then(function(data){
         response.setHeader('content-type','text/html');
         response.writeHead(200);
         response.end(data);}).catch(function(error){
         console.error(error);handleOopsRequest(request, response);});};// handlePostRequest/* ... */

http://127.0.0.1:3000/post/an-article.html

http://127.0.0.1:3000/post/another-article.html

Tiếp theo là hàm handleAssetRequest để gửi kèm các tệp CSS và JavaScript khi được yêu cầu thêm. Ở đây chúng ta có 2 kiểu nội dung trả về là text/csstext/javascript. Do đó chúng ta cần xây dựng một hàm nhỏ hỗ trợ để kiểm tra loại tệp được yêu cầu từ request.url. Bên cạnh đó thì kiểu nội dung text/html cũng cần gõ lặp lại thủ công lại vào các hàm khác nhiều lần nên chúng ta cũng sẽ tạo ra một biến tham chiếu để tránh khả năng mắc lỗi typo. 😄

nodejs-blog/route.js

const textType ={
   html:'text/html',
   css:'text/css',
   js:'text/javascript',get(url){if(url.endsWith('.html'))return textType.html;elseif(url.endsWith('.css'))return textType.css;elseif(url.endsWith('.js'))return textType.js;elsereturn'';}};// textTypeconsthandleHomeRequest=function(request, response){/* ... */consthandleAssetRequest=function(request, response){var fsPromises =require('fs/promises');

   fsPromises.readFile('static'+ request.url).then(function(data){var contentType = textType.get(request.url);
         response.setHeader('content-type', contentType);
         response.writeHead(200);
         response.end(data);}).catch(function(error){
         console.error(error);});};// handleAssetRequest/* ... */

http://127.0.0.1:3000/

http://127.0.0.1:3000/post/an-article.html

Như vậy là các tệp style.cssmain.js đã được tải kèm trang chủ và các trang bài viết. Bây giờ chúng ta chỉ còn hàm handleOopsRequest để trả về trang đơn thông báo cho người dùng khi không tìm thấy bài viết phù hợp. Ở đây chúng ta sửa lại đường dẫn tìm kiếm tệp và thay request.url bằng đường dẫn tĩnh /oops.html. 😄 Kiểu nội dung trả về là textType.html vì chúng ta đã biết trước kiểu tệp trả về; Đồng thời chúng ta cũng cần sửa lại tín hiệu phản hồi là writeHead(404) thay vì writeHead(200) để biểu thị lỗi truy vấn.

nodejs-blog/route.js

/* ... */consthandleOopsRequest=function(request, response){var fsPromises =require('fs/promises');

   fsPromises.readFile('static'+'/oops.html').then(function(data){
         response.setHeader('content-type', textType.html);
         response.writeHead(404);
         response.end(data);}).catch(function(error){
         console.error(error);});};// handleOopsRequest/* ... */

http://127.0.0.1:3000/something-else

Kết thúc bài viết

Như vậy là tới thời điểm hiện tại, chúng ta đã có thể sử dụng code server đơn giản này để làm blog giống như cách sử dụng Github Pages cơ bản – tức là tạo ra các bài viết bằng các trang đơn được viết bằng code HTML. Đây cũng chính là điểm mà chúng ta chính thức bắt đầu tìm hiểu cách để tự động hóa việc tạo ra các trang đơn HTML từ một nguồn nội dung nhập liệu dễ hơn đứng từ góc độ người sử dụng trang web.

Điều này có nghĩa là chúng ta sẽ có thể tạo nội dung bài viết ở một dạng khác ví dụ như các tệp markdown với cách biểu thị nhanh các nội dung trong khung soạn thảo giống như Viblo đang sử dụng.

Sau đó những nội dung trong các tệp markdown sẽ được biên dịch thành các tệp HTML tương ứng trong thư mục nodejs-blog/static/post, hoặc biên dịch thành code HTML và gửi thẳng cho trình duyệt web. Đối với tác vụ này, chúng ta đã bắt đầu phải nghĩ tới sự trợ giúp của các thư viện JavaScript dùng trong môi trường NodeJS. Ví dụ như tác vụ biên dịch nội dung trong tệp markdown thành code HTML, được hỗ trợ bởi một thư viện phổ biến có tên là marked.

Trong bài tiếp theo, chúng ta sẽ làm quen với cách thức cài đặt và sử dụng các thư viện hỗ trợ từ bên ngoài trong môi trường NodeJS. Bây giờ thì hãy nghỉ giải lao một chút đã. Hẹn gặp lại bạn trong bài viết tiếp theo. 😄

[NodeJS] Bài 5 – NPM – Node Packpage Manager

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ