035.2 Bài 2
Chứng chỉ: |
Web Development Essentials |
---|---|
Phiên bản: |
1.0 |
Chủ đề: |
035 Lập trình Máy chủ Node.js |
Mục tiêu: |
035.1 Khái niệm cơ bản về NodeJS Express |
Bài: |
2 trên 2 |
Giới thiệu
Máy chủ web có các cơ chế rất linh hoạt để tạo phản hồi cho các yêu cầu từ máy khách. Đối với một số yêu cầu, máy chủ web chỉ cần cung cấp một phản hồi tĩnh chưa qua xử lý cũng đã là đủ vì tài nguyên được yêu cầu sẽ giống nhau đối với bất kỳ một máy khách nào. Chẳng hạn như khi một máy khách yêu cầu một hình ảnh mà mọi người đều có thể truy cập được, máy chủ chỉ cần gửi đi tệp chứa hình ảnh đó là đủ.
Nhưng khi các phản hồi được tạo động, chúng có thể sẽ cần được cấu trúc một cách tốt hơn so với các dòng chữ đơn giản được viết trong tệp lệnh máy chủ. Trong những trường hợp như vậy, sẽ thuận tiện hơn nếu máy chủ web có thể tạo một tài liệu hoàn chỉnh mà máy khách có thể phiên dịch và hiển thị được. Trong bối cảnh phát triển ứng dụng web, các tài liệu HTML thường được tạo dưới dạng mẫu và được tách biệt khỏi tệp lệnh máy chủ. Tệp lệnh này sẽ chèn dữ liệu động vào các vị trí được xác định trước trong mẫu thích hợp, sau đó gửi phản hồi được định dạng cho máy khách.
Các ứng dụng web thường tiêu thụ cả tài nguyên tĩnh và tài nguyên động. Một tài liệu HTML ngay cả khi được tạo động vẫn có thể có tham chiếu đến các tài nguyên tĩnh như tệp CSS và các hình ảnh. Để minh hoạ cách Express hỗ trợ xử lý loại nhu cầu này, trước tiên, chúng ta sẽ thiết lập một máy chủ mẫu cung cấp tệp tĩnh, sau đó triển khai các tuyến tạo phản hồi dựa trên mẫu có cấu trúc.
Tệp Tĩnh
Bước đầu tiên là tạo tệp JavaScript chạy dưới dạng máy chủ. Hãy làm theo mô hình tương tự như đã được trình bày trong các bài học trước để tạo một ứng dụng Express đơn giản: trước tiên, hãy tạo một thư mục có tên server
; sau đó cài đặt các thành phần cơ sở bằng lệnh npm
:
$ mkdir server $ cd server/ $ npm init $ npm install express
Tại điểm nhập, chúng ta có thể sử dụng tên tệp bất kỳ, nhưng ở đây, ta sẽ sử dụng tên tệp mặc định là index.js
. Danh sách sau đây cho thấy tệp index.js
cơ bản sẽ được sử dụng làm điểm nhập cho máy chủ:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Bạn không cần phải viết một mã chi tiết để có thể gửi được tệp tĩnh. Express có phần mềm trung gian cho mục đích này và được gọi là express.static
. Nếu máy chủ cần gửi các tệp tĩnh đến máy khách, bạn chỉ cần tải phần mềm trung gian express.static
ở đầu tệp lệnh:
app.use(express.static('public'))
Tham số public
cho biết thư mục lưu trữ các tệp mà máy khách có thể yêu cầu. Các đường dẫn do máy khách yêu cầu không được bao gồm thư mục public
mà chỉ có tên tệp hoặc đường dẫn đến tệp liên quan đến thư mục public
. Ví dụ: để yêu cầu tệp public/layout.css
, máy khách phải gửi yêu cầu tới /layout.css
.
Đầu ra được định dạng
Trong khi việc gửi nội dung tĩnh rất đơn giản, các nội dung được tạo động lại có thể khác nhau một cách đáng kể. Việc tạo phản hồi động với các thông báo ngắn giúp ta dễ dàng kiểm tra các ứng dụng trong giai đoạn phát triển ban đầu. Ví dụ: sau đây là một tuyến thử nghiệm chỉ gửi lại cho máy khách một thông báo mà nó đã gửi bằng phương thức HTTP POST
. Phản hồi chỉ có thể sao chép nội dung thư ở dạng văn bản thuần túy mà không có bất kỳ định dạng nào:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
Một tuyến dạng như vậy là một ví dụ điển hình khi học về Express và cho các mục đích chẩn đoán, trong đó, một phản hồi thô chỉ cần được gửi bằng res.send()
là đủ. Tuy nhiên, một máy chủ hữu ích phải có khả năng tạo ra các phản hồi phức tạp hơn. Bây giờ, chúng ta sẽ cùng phát triển loại tuyến tinh vi hơn này.
Thay vì chỉ gửi lại nội dung của yêu cầu hiện tại, ứng dụng mới của chúng ta sẽ duy trì một danh sách đầy đủ các thông báo được gửi trong các yêu cầu trước đó của từng máy khách và gửi lại danh sách của từng máy khách khi được yêu cầu. Một phản hồi hợp nhất tất cả các thông báo là một tùy chọn, nhưng các chế độ đầu ra được định dạng khác sẽ phù hợp hơn, đặc biệt là khi các phản hồi trở nên phức tạp hơn.
Để nhận và lưu trữ tin nhắn của máy khách được gửi trong phiên hiện tại, trước tiên, chúng ta cần bao gồm cả các mô-đun bổ sung để xử lý cookie và dữ liệu được gửi qua phương thức HTTP POST
. Mục đích duy nhất của máy chủ trong ví dụ sau là ghi nhật ký các thông báo được gửi qua POST
và hiển thị các thông báo đã gửi trước đó khi máy khách đưa ra yêu cầu GET
. Vì vậy, có hai tuyến cho đường dẫn /
. Tuyến đầu tiên đáp ứng các yêu cầu được thực hiện bằng phương thức HTTP POST
và tuyến thứ hai đáp ứng các yêu cầu được thực hiện bằng phương thức HTTP GET
:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.static('public'))
const cookieParser = require('cookie-parser')
app.use(cookieParser())
const { v4: uuidv4 } = require('uuid')
app.use(express.urlencoded({ extended: true }))
// Array to store messages
let messages = []
app.post('/', (req, res) => {
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Locate cookie in the request
let uuid = req.cookies.uuid
// If there is no uuid cookie, create a new one
if ( uuid === undefined )
uuid = uuidv4()
// Add message first in the messages array
messages.unshift({uuid: uuid, message: req.body.message})
// Collect all previous messages for uuid
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
// Update cookie expiration date
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Send back JSON response
res.json(user_entries)
})
app.get('/', (req, res) => {
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Locate cookie in the request
let uuid = req.cookies.uuid
// Client's own messages
let user_entries = []
// If there is no uuid cookie, create a new one
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
// Collect messages for uuid
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
// Update cookie expiration date
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Send back JSON response
res.json(user_entries)
})
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Chúng ta sẽ giữ cấu hình tệp tĩnh ở trên cùng bởi việc cung cấp các tệp tĩnh (chẳng hạn như layout.css
) sẽ sớm trở nên hữu ích. Ngoài phần mềm trung gian cookie-parser
được giới thiệu trong chương trước, ví dụ này cũng bao gồm phần mềm trung gian uuid
được sử dụng để tạo một số nhận dạng duy nhất được truyền dưới dạng cookie cho mỗi máy khách gửi đi thông báo. Nếu chưa được cài đặt trong thư mục máy chủ trong ví dụ, các mô-đun này có thể được cài đặt bằng lệnh npm install cookie-parser uuid
.
Mảng toàn cục được gọi là messages
sẽ lưu trữ các thông báo được gửi bởi tất cả các máy khách. Mỗi mục trong mảng này đều bao gồm một đối tượng có đặc tính uuid
và message
.
Một điều thực sự mới mẻ trong tệp lệnh này là phương thức res.json()
được sử dụng ở cuối hai tuyến để tạo phản hồi ở định dạng JSON với mảng chứa các thông báo được gửi bởi máy khách:
// Send back JSON response
res.json(user_entries)
JSON là một định dạng văn bản thuần túy cho phép ta nhóm một tập hợp dữ liệu thành một cấu trúc duy nhất có tính liên kết - tức nội dung được thể hiện dưới dạng khóa và giá trị. JSON đặc biệt hữu ích khi các phản hồi được xử lý bởi máy khách. Khi sử dụng định dạng này, một đối tượng hoặc một mảng JavaScript có thể dễ dàng được xây dựng lại ở phía máy khách với tất cả các đặc tính và chỉ mục của đối tượng ban đầu trên máy chủ.
Vì đang cấu trúc từng thông báo trong JSON nên chúng ta sẽ từ chối các yêu cầu không chứa application/json
trong tiêu đề accept
của chúng:
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
Yêu cầu được thực hiện bằng lệnh curl
đơn giản để chèn thông báo mới sẽ không được chấp nhận vì theo mặc định, curl
không chỉ định application/json
trong tiêu đề accept
:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt Not Found
Tùy chọn -H "accept: application/json"
sẽ thay đổi tiêu đề của yêu cầu để chỉ định định dạng của phản hồi - lần này sẽ được chấp nhận và trả lời ở định dạng đã chỉ định:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt -H "accept: application/json" ["My first message"]
Việc nhận thông báo bằng cách sử dụng tuyến khác cũng được thực hiện theo cách tương tự, nhưng lần này sẽ sử dụng phương thức HTTP GET
:
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -H "accept: application/json" ["Another message","My first message"]
Mẫu
Phản hồi ở các định dạng như JSON sẽ thuận tiện cho việc giao tiếp giữa các chương trình. Tuy nhiên, mục đích chính của hầu hết các máy chủ ứng dụng web là tạo ra nội dung HTML cho mục đích sử dụng của con người. Việc nhúng mã HTML vào mã JavaScript không phải là một ý tưởng hay bởi việc trộn lẫn các ngôn ngữ trong cùng một tệp khiến chương trình dễ bị lỗi hơn và sẽ gây hại tới công tác bảo trì mã.
Express có thể làm việc với các công cụ mẫu khác nhau để tách riêng HTML cho nội dung động; danh sách đầy đủ có thể được tìm thấy tại Trang web công cụ mẫu Express. Một trong những công cụ mẫu phổ biến nhất là JavaScript nhúng (EJS) cho phép bạn tạo các tệp HTML với các thẻ cụ thể để chèn nội dung động.
Giống như các thành phần Express khác, EJS cần được cài đặt trong thư mục mà máy chủ đang chạy:
$ npm install ejs
Tiếp theo, công cụ EJS phải được đặt làm trình kết xuất mặc định trong tệp lệnh máy chủ (gần đầu tệp index.js
, ttrước các dòng xác định tuyến):
app.set('view engine', 'ejs')
Phản hồi được tạo cùng với mẫu sẽ được gửi đến máy khách bằng hàm res.render()
. Hàm này sẽ nhận dưới dạng tham số là tên tệp mẫu và một đối tượng chứa các giá trị có thể truy cập được từ bên trong mẫu. Các tuyến được sử dụng trong ví dụ trước có thể được viết lại để tạo các phản hồi HTML cũng như JSON:
app.post('/', (req, res) => {
let uuid = req.cookies.uuid
if ( uuid === undefined )
uuid = uuidv4()
messages.unshift({uuid: uuid, message: req.body.message})
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
})
app.get('/', (req, res) => {
let uuid = req.cookies.uuid
let user_entries = []
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
})
Hãy lưu ý rằng định dạng của phản hồi sẽ phụ thuộc vào tiêu đề accept
có trong yêu cầu:
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
Một phản hồi ở định dạng JSON sẽ chỉ được gửi nếu máy khách yêu cầu nó một cách rõ ràng. Nếu không, phản hồi sẽ được tạo từ mẫu index
. Cùng một mảng user_entries
sẽ cung cấp cả đầu ra JSON và mẫu, nhưng đối tượng được sử dụng làm tham số cho mẫu sau cũng có đặc tính title: "My messages"
được sử dụng để làm tiêu đề bên trong mẫu.
Mẫu HTML
Giống như các tệp tĩnh, các tệp chứa mẫu HTML nằm trong thư mục riêng của chúng. Theo mặc định, EJS sẽ giả định các tệp mẫu nằm trong thư mục views/
. Trong ví dụ này, một mẫu có tên index
đã được sử dụng; vì vậy, EJS sẽ tìm kiếm tệp views/index.ejs
. Danh sách sau đây là nội dung của mẫu views/index.ejs
đơn giản có thể được sử dụng với mã ví dụ:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><%= title %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/layout.css">
</head>
<body>
<div id="interface">
<form action="/" method="post">
<p>
<input type="text" name="message">
<input type="submit" value="Submit">
</p>
</form>
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
</div>
</body>
</html>
Thẻ EJS đặc biệt đầu tiên là phần tử <title>
trong phần <head>
:
<%= title %>
Trong quá trình kết xuất, thẻ đặc biệt này sẽ được thay thế bằng giá trị của đặc tính title
của đối tượng được truyền dưới dạng tham số cho hàm res.render()
.
Hầu hết các mẫu đều được tạo thành từ mã HTML thông thường; vì vậy, mẫu sẽ chứa biểu mẫu HTML để gửi thông báo mới. Máy chủ thử nghiệm sẽ phản hồi các phương thức HTTP GET
và POST
cho cùng một đường dẫn /
; do đó, ta có các thuộc tính action="/"
và method="post"
trong thẻ biểu mẫu.
Các phần khác của mẫu là sự kết hợp giữa mã HTML và các thẻ EJS. EJS có các thẻ dành cho các mục đích cụ thể trong mẫu:
<% … %>
-
Chèn kiểm soát luồng. Không có nội dung nào được chèn trực tiếp bởi thẻ này nhưng nó có thể được sử dụng với các cấu trúc JavaScript để chọn, lặp lại hoặc chặn các phần của HTML. Ví dụ: để bắt đầu một vòng lặp:
<% messages.forEach( (message) => { %>
<%# … %>
-
Xác định một chú thích có nội dung bị trình phân tích cú pháp bỏ qua. Không giống như các chú thích được viết bằng HTML, những chú thích này sẽ không được hiển thị với máy khách.
<%= … %>
-
Chèn nội dung thoát của biến. Việc thoát khỏi nội dung không xác định để tránh thực thi mã JavaScript là một yếu tố quan trọng; điều này có thể tạo kẽ hở cho các cuộc tấn công tệp lệnh liên trang (XSS). Ví dụ:
<%= title %>
<%- … %>
-
Chèn nội dung của biến mà không thoát.
Sự kết hợp giữa mã HTML và các thẻ EJS được thể hiện rõ trong đoạn mã nơi các thông báo của khách hàng được hiển thị dưới dạng danh sách HTML:
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
Trong đoạn mã này, thẻ <% … %>
đầu tiên sẽ bắt đầu câu lệnh forEach
lặp qua tất cả các phần tử của mảng message
. Dấu phân cách <%
và %>
cho phép ta kiểm soát các đoạn mã HTML. Một danh mục HTML mới là <li><%= message %></li>
sẽ được tạo cho từng phần tử của message
. Với những thay đổi này, máy chủ sẽ gửi phản hồi bằng HTML khi nhận được yêu cầu như sau:
$ curl http://myserver:8080/ --data message="This time" -c cookies.txt -b cookies.txt <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>My messages</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/layout.css"> </head> <body> <div id="interface"> <form action="/" method="post"> <p> <input type="text" name="message"> <input type="submit" value="Submit"> </p> </form> <ul> <li>This time</li> <li>in HTML</li> </ul> </div> </body> </html>
Sự tách biệt giữa mã để xử lý yêu cầu và mã để trình bày phản hồi sẽ làm cho mã sạch hơn và cho phép nhóm công tác phân chia việc phát triển ứng dụng giữa những thành viên với chuyên môn riêng biệt. Ví dụ: một nhà thiết kế web có thể tập trung vào các tệp mẫu trong views/
và các biểu định kiểu liên quan được cung cấp dưới dạng các tệp tĩnh được lưu trữ trong thư mục public/
trên máy chủ mẫu.
Bài tập Hướng dẫn
-
express.static
nên được định cấu hình như thế nào để máy khách có thể yêu cầu các tệp trong thư mụcassets
? -
Làm cách nào để xác định loại phản hồi được chỉ định trong tiêu đề của yêu cầu trong một tuyến Express?
-
Phương thức nào của tham số tuyến
res
(phản hồi) sẽ tạo phản hồi ở định dạng JSON từ một mảng JavaScript có tên làcontent
?
Bài tập Mở rộng
-
Theo mặc định, các tệp mẫu Express sẽ nằm trong thư mục
views
. Làm cách nào để sửa đổi cài đặt này để các tệp mẫu được lưu trữ trongtemplates
? -
Giả sử một máy khách nhận được phản hồi HTML không có tiêu đề (tức
<title></title>
). Sau khi xác minh mẫu EJS, nhà phát triển sẽ tìm thấy thẻ<title><% title %></title>
trong phầnhead
của tệp. Nguyên nhân của vấn đề này có thể là gì? -
Hãy sử dụng thẻ mẫu EJS để viết thẻ HTML
<h2></h2>
với nội dung của biến JavaScripth2
. Thẻ này chỉ được hiển thị nếu biếnh2
không trống.
Tóm tắt
Bài học này bao gồm các phương thức cơ bản mà Express.js cung cấp để tạo các phản hồi tĩnh và phản hồi động được định dạng. Việc thiết lập máy chủ HTTP cho các tệp tĩnh khá là đơn giản và hệ thống tạo khuôn mẫu EJS sẽ cung cấp một cách dễ dàng để tạo nội dung động từ các tệp HTML. Bài học này đã đi qua các khái niệm và quy trình sau:
-
Sử dụng
express.static
cho phản hồi tệp tĩnh. -
Cách tạo phản hồi để khớp với trường loại nội dung trong tiêu đề yêu cầu.
-
Phản hồi có cấu trúc JSON.
-
Sử dụng các thẻ EJS trong các mẫu dựa trên HTML.
Đáp án Bài tập Hướng dẫn
-
express.static
nên được định cấu hình như thế nào để máy khách có thể yêu cầu các tệp trong thư mụcassets
?Một lệnh gọi
app.use(express.static('assets'))
nên được thêm vào tệp lệnh máy chủ. -
Làm cách nào để xác định loại phản hồi được chỉ định trong tiêu đề của yêu cầu trong một tuyến Express?
Máy khách nên đặt các loại được chấp nhận trong trường tiêu đề
accept
được ánh xạ tới đặc tínhreq.headers.accept
. -
Phương thức nào của tham số tuyến
res
(phản hồi) sẽ tạo phản hồi ở định dạng JSON từ một mảng JavaScript có tên làcontent
?Phương thức
res.json()
:res.json(content)
.
Đáp án Bài tập Mở rộng
-
Theo mặc định, các tệp mẫu Express sẽ nằm trong thư mục
views
. Làm cách nào để sửa đổi cài đặt này để các tệp mẫu được lưu trữ trongtemplates
?Thư mục có thể được xác định trong thiết lập ban đầu của tệp lệnh với
app.set('views', './templates')
. -
Giả sử một máy khách nhận được phản hồi HTML không có tiêu đề (tức
<title></title>
). Sau khi xác minh mẫu EJS, nhà phát triển sẽ tìm thấy thẻ<title><% title %></title>
trong phầnhead
của tệp. Nguyên nhân của vấn đề này có thể là gì?Thẻ
<%= %>
nên được sử dụng để chứa nội dung của một biến như trong<%= title %>
. -
Hãy sử dụng thẻ mẫu EJS để viết thẻ HTML
<h2></h2>
với nội dung của biến JavaScripth2
. Thẻ này chỉ được hiển thị nếu biếnh2
không trống.<% if ( h2 != "" ) { %> <h2><%= h2 %></h2> <% } %>