035.1 Bài 1
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ề Node.js |
Bài: |
1 trên 1 |
Giới thiệu
Node.js là một môi trường thời gian chạy JavaScript chạy mã JavaScript trên máy chủ web – tức web phụ trợ (phía máy chủ) – thay vì sử dụng ngôn ngữ thứ hai như Python hoặc Ruby cho các chương trình phía máy chủ. Ngôn ngữ JavaScript đã được sử dụng trong giao diện người dùng hiện đại của các ứng dụng web; nó tương tác với HTML và CSS của giao diện mà người dùng tương tác với trình duyệt web. Việc sử dụng Node.js song song với JavaScript trong trình duyệt sẽ có thể cho phép chúng ta chỉ cần sử dụng một ngôn ngữ lập trình cho toàn bộ ứng dụng.
Lý do chính cho sự tồn tại của Node.js là cách nó xử lý đồng thời nhiều kết nối ở phía máy chủ. Một trong những cách phổ biến nhất mà máy chủ ứng dụng web xử lý các kết nối là thông qua việc thực thi đa tiến trình. Khi mở một ứng dụng trên máy tính để bàn trong máy tính của mình, một tiến trình sẽ bắt đầu và sử dụng rất nhiều tài nguyên. Hãy thử tưởng tượng nếu hàng nghìn người dùng cùng một lúc cũng làm điều tương tự trong một ứng dụng web lớn.
Node.js sẽ tránh được vấn đề này bằng cách sử dụng một thiết kế có tên là vòng lặp sự kiện. Đây là một vòng lặp nội bộ liên tục kiểm tra các tác vụ đến đang chờ tính toán. Nhờ việc sử dụng rộng rãi JavaScript và tính phổ biến của công nghệ web, Node.js đã được áp dụng rộng rãi trong cả các ứng dụng lớn và nhỏ. Node.js cũng có nhiều đặc điểm khác khiến nó được áp dụng rộng rãi, chẳng hạn như việc xử lý đầu vào/đầu ra (I/O) không đồng bộ và không chặn (những đặc điểm này sẽ được giải thích trong các phần sau của bài học này).
Môi trường Node.js sử dụng một công cụ JavaScript để thông dịch và thực thi mã JavaScript ở phía máy chủ hoặc trên máy tính cá nhân. Trong những điều kiện này, mã JavaScript mà lập trình viên viết sẽ được phân tích cú pháp và biên dịch ngay lập tức để thực thi các lệnh máy do mã JavaScript gốc tạo ra.
Note
|
Càng đi sâu vào bài học về Node.js này, bạn sẽ càng dễ nhận ra rằng JavaScript của Node.js không hoàn toàn giống với JavaScript chạy trên trình duyệt (tuân theo đặc tả ECMAScript), nhưng cũng lại khá tương đồng. |
Bắt đầu
Phần này và các ví dụ sau đây sẽ giả định rằng Node.js đã được cài đặt trên hệ điều hành Linux và người dùng đã có các kỹ năng cơ bản về cách thực thi các lệnh trong cửa sổ dòng lệnh.
Để chạy các ví dụ sau, hãy tạo một thư mục làm việc có tên node_examples
. Sau đó, hãy mở dòng nhắc lệnh và nhập node
. Nếu đã cài đặt được Node.js, nó sẽ hiển thị dấu nhắc >
nơi ta có thể tương tác kiểm tra các lệnh JavaScript. Loại môi trường này được gọi là REPL (“đọc read, đánh giá - evaluate, in - print và lặp - loop”). Hãy nhập nội dung đầu vào sau (hoặc một số câu lệnh JavaScript khác) vào dấu nhắc >
. Nhấn Enter sau mỗi dòng và môi trường REPL sẽ trả về kết quả hành động của nó:
> let array = ['a', 'b', 'c', 'd']; undefined > array.map( (element, index) => (`Element: ${element} at index: ${index}`)); [ 'Element: a at index: 0', 'Element: b at index: 1', 'Element: c at index: 2', 'Element: d at index: 3' ] >
Đoạn mã được viết bằng cú pháp ES6 và cung cấp chức năng ánh xạ để lặp qua mảng và in kết quả bằng các bản mẫu chuỗi. Ta có thể viết bất kỳ lệnh nào hợp lệ. Để thoát khỏi cửa sổ dòng lệnh của Node.js, hãy nhập .exit
và nhớ phải thêm dấu chấm đầu tiên.
Đối với các tệp lệnh và mô-đun dài hơn, sẽ thuận tiện hơn nếu ta sử dụng trình soạn thảo văn bản như VS Code, Emacs hoặc Vim. Ta có thể lưu hai dòng mã vừa hiển thị (với một chút sửa đổi) trong tệp có tên start.js
:
let array = ['a', 'b', 'c', 'd'];
array.map( (element, index) => ( console.log(`Element: ${element} at index: ${index}`)));
Sau đó, bạn có thể chạy tệp lệnh từ vỏ để tạo ra kết quả giống như trước:
$ node ./start.js Element: a at index: 0 Element: b at index: 1 Element: c at index: 2 Element: d at index: 3
Trước khi đi sâu vào một số mã khác, chúng ta sẽ tìm hiểu tổng quan về cách thức hoạt động của Node.js sử dụng môi trường thực thi đơn luồng và vòng lặp sự kiện.
Vòng lặp Sự kiện và Luồng Đơn
Thật khó để biết chương trình Node.js sẽ mất bao nhiêu thời gian để xử lý một yêu cầu. Một số yêu cầu ngắn có thể chỉ lặp qua các biến trong bộ nhớ và trả chúng về, trong khi những yêu cầu khác có thể yêu cầu các hoạt động tốn thời gian hơn như mở tệp trên hệ thống hoặc truy vấn tới cơ sở dữ liệu và chờ kết quả. Node.js xử lý những vấn đề này như thế nào? Vòng lặp sự kiện chính là câu trả lời.
Hãy tưởng tượng một đầu bếp đang làm cùng lúc nhiều nhiệm vụ. Nướng bánh là một công việc đòi hỏi nhiều thời gian để lò làm chín bánh. Người đầu bếp không thể ở đó đợi bánh chín rồi mới bắt đầu đi pha cà phê. Thay vào đó, trong khi lò nướng bánh, đầu bếp sẽ phải pha cà phê và làm các công việc khác song song. Nhưng người đầu bếp cũng luôn phải kiểm tra xem đã đến lúc thích hợp để chuyển trọng tâm sang một nhiệm vụ cụ thể khác (pha cà phê) hay phải lấy bánh ra khỏi lò.
Vòng lặp sự kiện cũng giống như người đầu bếp luôn nhận thức được các hoạt động xung quanh mình. Trong Node.js, một “trình kiểm tra sự kiện” luôn kiểm tra các hoạt động đã hoàn thành hoặc đang chờ được xử lý bởi công cụ JavaScript.
Việc sử dụng một quy trình dài và không đồng bộ như thế này sẽ không chặn các thao tác nhanh khác diễn ra sau đó. Điều này là do cơ chế vòng lặp sự kiện sẽ luôn kiểm tra xem nhiệm vụ dài đó đã được thực hiện chưa, chẳng hạn như thao tác I/O. Nếu chưa, Node.js có thể sẽ tiếp tục xử lý các tác vụ khác. Khi tác vụ nền hoàn tất, kết quả sẽ được trả về và ứng dụng trên Node.js có thể sử dụng hàm kích hoạt (gọi lại) để xử lý tiếp đầu ra.
Vì Node.js tránh sử dụng nhiều luồng giống như các môi trường khác nên nó được gọi là môi trường đơn luồng; cũng vì vậy mà phương pháp tiếp cận không chặn là một yếu tố tối quan trọng. Đây là lý do tại sao Node.js sử dụng vòng lặp sự kiện. Tuy nhiên, đối với các tác vụ chuyên sâu về điện toán, Node.js không phải là một trong những công cụ tốt nhất. Có những ngôn ngữ và môi trường lập trình khác có thể giải quyết những vấn đề này một cách hiệu quả hơn.
Trong các phần sau, chúng ta sẽ xem xét kỹ hơn về các hàm gọi lại. Hiện tại, hãy hiểu rằng các hàm gọi lại chính là các trình kích hoạt được thực thi khi một thao tác được xác định trước được hoàn thành.
Mô-đun
Cách tốt nhất ở đây là chia nhỏ chức năng phức tạp và các đoạn mã mở rộng thành các phần nhỏ hơn. Việc thực hiện mô đun hóa này sẽ giúp ta tổ chức cơ sở mã tốt hơn, tách bạch việc triển khai và tránh các vấn đề kỹ thuật phức tạp. Để đáp ứng những nhu cầu đó, các lập trình viên sẽ đóng gói các khối mã nguồn để sử dụng cho các phần khác ở bên trong hoặc bên ngoài mã nguồn.
Hãy xét ví dụ về chương trình tính thể tích khối cầu. Hãy mở trình soạn thảo văn bản và tạo một tệp có tên volumeCalculator.js
chứa đoạn mã JavaScript sau:
const sphereVol = (radius) => {
return 4 / 3 * Math.PI * radius
}
console.log(`A sphere with radius 3 has a ${sphereVol(3)} volume.`);
console.log(`A sphere with radius 6 has a ${sphereVol(6)} volume.`);
Bây giờ, hãy thực thi tệp bằng Node:
$ node volumeCalculator.js A sphere with radius 3 has a 113.09733552923254 volume. A sphere with radius 6 has a 904.7786842338603 volume.
Ở đây, một hàm đơn giản đã được sử dụng để tính thể tích của một hình cầu dựa trên bán kính của nó. Hãy tưởng tượng rằng chúng ta cũng cần tính thể tích của hình trụ, hình nón, v.v.: chúng ta sẽ nhanh chóng nhận thấy rằng các hàm cụ thể đó phải được thêm vào tệp volumeCalculator.js
. Tệp này có thể trở thành một bộ sưu tập khổng lồ của các hàm. Để tổ chức cấu trúc tốt hơn, chúng ta có thể sử dụng ý tưởng về các mô-đun như các gói mã riêng biệt.
Để làm điều đó, hãy tạo một tệp riêng biệt có tên polyhedrons.js
:
const coneVol = (radius, height) => {
return 1 / 3 * Math.PI * Math.pow(radius, 2) * height;
}
const cylinderVol = (radius, height) => {
return Math.PI * Math.pow(radius, 2) * height;
}
const sphereVol = (radius) => {
return 4 / 3 * Math.PI * Math.pow(radius, 3);
}
module.exports = {
coneVol,
cylinderVol,
sphereVol
}
Bây giờ, trong tệp volumeCalculator.js
, hãy xóa mã cũ và thay thế bằng đoạn mã sau:
const polyhedrons = require('./polyhedrons.js');
console.log(`A sphere with radius 3 has a ${polyhedrons.sphereVol(3)} volume.`);
console.log(`A cylinder with radius 3 and height 5 has a ${polyhedrons.cylinderVol(3, 5)} volume.`);
console.log(`A cone with radius 3 and height 5 has a ${polyhedrons.coneVol(3, 5)} volume.`);
Và sau đó chạy tên tệp trong môi trường Node.js:
$ node volumeCalculator.js A sphere with radius 3 has a 113.09733552923254 volume. A cylinder with radius 3 and height 5 has a 141.3716694115407 volume. A cone with radius 3 and height 5 has a 47.12388980384689 volume.
Trong môi trường Node.js, mọi tệp mã nguồn đều được coi là một mô-đun, nhưng từ “module” trong Node.js sẽ biểu thị một gói mã được bao bọc như trong ví dụ trước. Bằng cách sử dụng các mô-đun, chúng ta đã tách bạch các hàm tính thể tích khỏi tệp chính (volumeCalculator.js
), từ đó giảm kích thước của tệp và giúp việc áp dụng các bài kiểm tra đơn vị trở nên dễ dàng hơn. Đây là một phương pháp hay khi phát triển ứng dụng trong thực tế.
Bây giờ, khi đã biết về cách các mô-đun được sử dụng trong Node.js, chúng ta có thể sử dụng một trong những công cụ quan trọng nhất: Trình quản lý Gói Nút (NPM - Node Package Manager).
Một trong những nhiệm vụ chính của NPM là quản lý, tải xuống và cài đặt các mô-đun bên ngoài vào dự án hoặc bên trong hệ điều hành. Ta có thể khởi tạo kho lưu trữ node bằng lệnh npm init
.
NPM sẽ hỏi các câu hỏi mặc định về tên kho lưu trữ, phiên bản, mô tả, v.v. Bạn có thể bỏ qua các bước này bằng cách sử dụng npm init --yes
và lệnh sẽ tự động tạo tệp package.json
mô tả các thuộc tính của dự án/mô-đun của bạn.
Hãy mở tệp package.json
trong trình soạn thảo văn bản yêu thích và bạn sẽ thấy tệp JSON chứa các thuộc tính như từ khóa, lệnh tệp lệnh để sử dụng với NPM, tên, v.v.
Một trong những thuộc tính đó là các phần phụ thuộc được cài đặt trong kho lưu trữ cục bộ của bạn. NPM sẽ thêm tên và phiên bản của các thành phần phụ thuộc này vào package.json
cùng với package-lock.json
- một tệp khác được NPM sử dụng làm phương án dự phòng trong trường hợp package.json
bị lỗi.
Hãy nhập nội dung sau vào cửa sổ dòng lệnh:
$ npm i dayjs
Cờ i
là lối tắt cho đối số install
. Nếu có kết nối với internet, NPM sẽ tìm kiếm mô-đun có tên dayjs
trong kho lưu trữ từ xa của Node.js, tải mô-đun xuống và cài đặt cục bộ. NPM cũng sẽ thêm phần phụ thuộc này vào các tệp package.json
và package-lock.json
. Bây giờ, bạn có thể thấy rằng có một thư mục có tên node_modules
chứa mô-đun đã cài đặt cùng với các mô-đun khác nếu cần. Thư mục node_modules
chứa mã thực sẽ được sử dụng khi thư viện được nhập và gọi. Tuy nhiên, thư mục này không được lưu trong các hệ thống quản lý phiên bản sử dụng bằng Git vì tệp package.json
sẽ cung cấp tất cả các phần phụ thuộc được sử dụng. Một người dùng khác có thể lấy tệp package.json
và chỉ cần chạy npm install
trong máy của họ và NPM sẽ tạo thư mục node_modules
ở đó với tất cả các phần phụ thuộc trong package.json
, từ đó tránh việc kiểm soát phiên bản cho hàng ngàn tệp có sẵn trên kho lưu trữ NPM.
Bây giờ, mô-đun dayjs
đã được cài đặt trong thư mục cục bộ. Hãy mở bảng điều khiển Node.js và nhập các dòng sau:
const dayjs = require('dayjs');
dayjs().format('YYYY MM-DDTHH:mm:ss')
Mô-đun dayjs
được tải bằng từ khóa require
. Khi gọi một phương thức từ mô-đun, thư viện sẽ lấy ngày giờ hệ thống hiện tại và xuất nó theo định dạng đã được chỉ định:
2020 11-22T11:04:36
Đây là cơ chế tương tự được sử dụng trong ví dụ trước, trong đó thời gian chạy của Node.js sẽ tải hàm của bên thứ ba vào mã.
Chức năng Máy chủ
Vì Node.js kiểm soát phía máy chủ của ứng dụng web nên một trong những nhiệm vụ cốt lõi của nó là xử lý các yêu cầu HTTP.
Dưới đây là tóm tắt về cách máy chủ web xử lý các yêu cầu HTTP đến. Chức năng của máy chủ là lắng nghe các yêu cầu, xác định phản hồi nhanh nhất có thể mà mỗi yêu cầu cần và trả lại phản hồi đó cho người gửi yêu cầu. Ứng dụng này phải nhận một yêu cầu HTTP đến do người dùng kích hoạt, phân tích cú pháp yêu cầu, thực hiện phép tính, tạo phản hồi và gửi lại. Mô-đun HTTP (chẳng hạn như Node.js) được sử dụng vì nó sẽ đơn giản hóa các bước đó, cho phép lập trình viên web tập trung vào chính ứng dụng.
Hãy xem xét ví dụ sau về việc thực hiện chức năng rất cơ bản này:
const http = require('http');
const url = require('url');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
const queryObject = url.parse(req.url,true).query;
let result = parseInt(queryObject.a) + parseInt(queryObject.b);
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(`Result: ${result}\n`);
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
Hãy lưu những nội dung này trong một tệp có tên basic_server.js
và chạy nó thông qua lệnh node
. Cửa sổ dòng lệnh đang chạy Node.js sẽ hiển thị thông báo sau:
Server running at http://127.0.0.1:3000/
Sau đó, hãy truy cập đường dẫn URL sau trên trình duyệt web: http://127.0.0.1:3000/numbers?a=2&b=17
Node.js đang chạy một máy chủ web trong máy tính của bạn và sử dụng hai mô-đun http
và url
. Mô-đun http
sẽ thiết lập một máy chủ HTTP cơ bản, xử lý các yêu cầu web đến và chuyển chúng tới mã ứng dụng đơn giản của chúng ta. Mô-đun URL sẽ phân tích cú pháp các đối số được truyền trong URL, chuyển đổi chúng thành định dạng số nguyên và thực hiện các phép cộng. Sau đó, mô-đun http
sẽ gửi phản hồi dưới dạng văn bản tới trình duyệt web.
Trong một ứng dụng web thực tế, Node.js thường được sử dụng để xử lý và truy xuất dữ liệu, thường là từ cơ sở dữ liệu và trả lại thông tin đã xử lý cho giao diện người dùng để hiển thị. Nhưng ứng dụng cơ bản trong bài học này cho thấy một cách chính xác cách mà Node.js sử dụng các mô-đun để xử lý các yêu cầu web như một máy chủ web.
Bài tập Hướng dẫn
-
Lý do để sử dụng các mô-đun thay vì viết các hàm đơn giản là gì?
-
Tại sao môi trường Node.js lại trở nên phổ biến như vậy? Hãy chỉ ra một đặc điểm cụ thể.
-
Mục đích của tệp
package.json
là gì? -
Tại sao không nên lưu và chia sẻ thư mục
node_modules
?
Bài tập Mở rộng
-
Làm cách nào để có thể thực thi các ứng dụng Node.js trên máy tính của mình?
-
Làm cách nào để có thể phân định các tham số trong URL để phân tích cú pháp bên trong máy chủ?
-
Hãy chỉ ra một trường hợp mà trong đó một tác vụ cụ thể có thể gây trở ngại cho ứng dụng Node.js.
-
Bạn sẽ triển khai một tham số để nhân hoặc tính tổng hai số trong ví dụ về máy chủ như thế nào?
Tóm tắt
Bài học này đã cung cấp một cái nhìn tổng quan về môi trường Node.js, các đặc điểm của nó và cách sử dụng nó để triển khai các chương trình đơn giản. Bài học này bao gồm các khái niệm sau:
-
Node.js là gì và tại sao nó được sử dụng.
-
Cách chạy các chương trình Node.js bằng dòng lệnh.
-
Các vòng lặp sự kiện và luồng đơn.
-
Các mô-đun.
-
Trình quản lý thư viện trong môi trường Node.js (NPM).
-
Chức năng máy chủ.
Đáp án Bài tập Hướng dẫn
-
Lý do để sử dụng các mô-đun thay vì viết các hàm đơn giản là gì?
Bằng cách chọn các mô-đun thay vì các hàm thông thường, lập trình viên sẽ tạo ra một cơ sở mã đơn giản hơn để đọc và bảo trì cũng như để viết các bài kiểm tra tự động.
-
Tại sao môi trường Node.js lại trở nên phổ biến như vậy? Hãy chỉ ra một đặc điểm cụ thể.
Một lý do là tính linh hoạt của ngôn ngữ JavaScript vốn đã được sử dụng rộng rãi trong phần giao diện người dùng của các ứng dụng web. Node.js sẽ cho phép việc sử dụng chỉ một ngôn ngữ lập trình trong toàn bộ hệ thống.
-
Mục đích của tệp
package.json
là gì?Tệp này chứa các siêu dữ liệu cho dự án, chẳng hạn như tên, phiên bản, phần phụ thuộc (thư viện), v.v. Với một tệp
package.json
, những người dùng khác có thể tải xuống và cài đặt chính các thư viện đó cũng như chạy thử nghiệm giống như cách mà người tạo ban đầu đã làm. -
Tại sao không nên lưu và chia sẻ thư mục
node_modules
?Thư mục
node_modules
chứa các phần triển khai của các thư viện có sẵn trong kho lưu trữ từ xa. Vì vậy, cách tốt hơn để chia sẻ các thư viện này là chỉ định chúng trong tệppackage.json
rồi sử dụng NPM để tải xuống các thư viện đó. Phương pháp này đơn giản hơn và ít gặp lỗi hơn vì bạn sẽ không phải theo dõi và duy trì các thư viện cục bộ.
Đáp án Bài tập Mở rộng
-
Làm cách nào để có thể thực thi các ứng dụng Node.js trên máy tính của mình?
Bạn có thể chạy chúng bằng cách nhập
node PATH/FILE_NAME.js
tại dòng lệnh trong cửa sổ dòng lệnh của mình, thay đổiPATH
thành đường dẫn của tệp Node.js và thay đổiFILE_NAME.js
thành tên tệp bạn đã chọn. -
Làm cách nào để có thể phân định các tham số trong URL để phân tích cú pháp bên trong máy chủ?
Ký tự
&
được sử dụng để phân định các tham số đó để chúng có thể được trích xuất và phân tích cú pháp trong mã JavaScript. -
Hãy chỉ ra một trường hợp mà trong đó một tác vụ cụ thể có thể gây trở ngại cho ứng dụng Node.js.
Node.js không phải là môi trường tốt để chạy các quy trình sử dụng nhiều CPU vì nó sử dụng luồng đơn. Một phiên tính toán số có thể làm chậm và khóa toàn bộ ứng dụng. Nếu cần mô phỏng số, tốt hơn là nên sử dụng các công cụ khác.
-
Bạn sẽ triển khai một tham số để nhân hoặc tính tổng hai số trong ví dụ về máy chủ như thế nào?
Sử dụng toán tử bậc ba hoặc điều kiện if-else để kiểm tra tham số bổ sung. Nếu tham số là chuỗi
mult
thì nó sẽ trả về tích của các số, nếu không thì trả về tổng. Thay thế mã cũ bằng đoạn mã bên dưới. Khởi động lại máy chủ trong dòng lệnh bằng cách nhấn kbd:[Ctrl+C] và chạy lại lệnh để khởi động lại máy chủ. Bây giờ, hãy kiểm tra ứng dụng mới bằng cách truy cập URLhttp://127.0.0.1:3000/numbers?a=2&b=17&operation=mult
trong trình duyệt của bạn. Nếu bạn bỏ qua hoặc thay đổi tham số cuối cùng, kết quả sẽ là tổng của các số.let result = queryObject.operation == 'mult' ? parseInt(queryObject.a) * parseInt(queryObject.b) : parseInt(queryObject.a) + parseInt(queryObject.b);