Category Archives: NodeJS

NodeJS – Tùy biến giao diện Express

Trong các bài trước chúng ta đã tìm hiểu cách xây dựng ứng dụng với Express, và các ứng dụng này có giao diện phía client (hay frontend) cũng khá bắt mắt, tuy nhiên nếu muốn chúng ta có thể tùy biến để giao diện hiển thị theo ý thích riêng của chúng ta.

Khi tạo một project Express thì file index.ejs (mà chúng ta hay tách thành top.ejsbottom.ejs) có dòng code này tròng thẻ <head>:

<link rel='stylesheet' href='/stylesheets/style.css' />

Đây là dòng code tham chiếu đến file style.css được đặt trong thư mục public/stylesheets. Ngoài thư mục stylesheets thì còn có 2 thư mục khác trong này là imagesjavascripts. Đây là các thư mục chứa các file tài nguyên để sử dụng cho phần frontend.

Chúng ta sẽ thử thay đổi một số thứ trong file style.css của project Notes đã làm trong phần trước để có giao diện như ý muốn.

h1 {
    text-align: center;
    text-decoration: underline;
}

Đoạn code CSS trên rất đơn giản nếu bạn đã từng học CSS. Chỉ là đưa các element h1 ra giữa trình duyệt và thêm dấu gạch dưới phía dưới.

capture

Tất nhiên là ngoài file style.css thì chúng ta cũng có thể tự viết nhiều file khác để việc tùy biến giao diện được linh hoạt hơn, bạn chỉ cần viết các file đó rồi đưa link tham chiếu trong thẻ <link> là được.

Nếu muốn bạn có thể dùng thêm một framework khác rất nổi tiếng đó là Bootstrap. Boostrap được phát triển bởi Twitter, hỗ trợ làm frontend rất mạnh, rất dễ sử dụng, có thể dùng trên nhiều loại trinh duyệt khác nhau.

Để sử dụng Bootstrap thì chúng ta có thể lên trang chủ của Bootstrap tại địa chỉ: http://getbootstrap.com/getting-started/#download

Sau khi tải về thì chúng ta giải nén ra rồi bỏ vào thư mục public, tiếp theo chúng ta chỉ cần khai báo thẻ <link> tham chiếu đến file bootstrap.min.css trong thư mục css là được, Ví dụ mình giải nén ra được thư mục bootstrap-3.3.7-dist thì mình khai báo như sau:

<link href="/bootstrap-3.3.7-dist/css/bootstrap.min.css" rel="stylesheet" media="screen">

Sau đó trong file bottom.ejs chúng ta phải khai báo hai dòng sau trước khi đóng thẻ </body>:

<script src="http://code.jquery.com/jquery.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
</body></html>

Hoặc dùng cách khác mà hầu hết mọi người thường dùng là chèn link CDN trực tiếp trong thẻ <link> luôn:

<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">

<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

Dùng cách này thì bạn không cần phải tốn bộ nhớ lưu trữ Bootstrap, ngoài ra Bootstrap sẽ được tải về nhanh hơn vì có rất nhiều trang web sử dụng Bootstrap bằng CDN, do đó trình duyệt sẽ lưu lại Bootsrap trong bộ nhớ cache, và đến khi gặp trang web của chúng ta cũng sử dụng CDN thì trình duyệt không cần phải tải lại nữa mà dùng thẳng cái đã được lưu trong cache luôn.

Sau khi khai báo Bootstrap chúng ta vẫn có thể khai báo file style.css của riêng chúng ta, như thế chúng ta có thể ghi đè Bootstrap đẻ tùy biến theo ý chúng ta.

Ngoài ra khi sử dụng Bootstrap bạn phải khai báo thêm dòng này ở đầu file top.ejs:

<!DOCTYPE html>

 

NodeJS – Xây dựng ứng dụng MVC với Express

Trong các bài trước chúng ta đã tìm hiểu sơ qua về Express, trong phần này chúng ta sẽ dùng Express để xây dựng một ứng dụng ghi chú đơn giản, sử dụng mô hình MVC.

Mô hình MVC trong Express

Bản thân Express không được xây dựng theo mô hình MVC, nhưng module express-generator dùng để tạo một project Express thì lại tạo cho chúng ta một ứng dụng gần như là giống với MVC bởi vì các project này tồn tại 2 thứ:

  • Thư mục views chứa các file template (file có phần mở rộng là .ejs), các file này được dùng để hiển thị dữ liệu, tức là tương tự với phần Views trong MVC.
  • Thư mục routes được dùng để chuyển hướng các URL đến các hàm xử lý tương ứng, tức là tương tự với Controller trong MVC.

Vậy thì để ứng dụng của chúng ta vận hành theo đúng mô hình MVC thì thành phần còn thiếu là Model. Model có chức năng lưu trữ dữ liệu, thay đổi/cập nhật dữ liệu, hỗ trợ truy vấn dữ liệu. Và dĩ nhiên là code lưu trữ model cũng nên được lưu trong một thư mục riêng tách rời với viewsroutes.

Tạo ứng dụng Notes

Chúng ta sẽ xây dựng ứng dụng Notes (quản lý ghi chú) đơn giản.

Đầu tiên chúng ta tạo project:

C:\NodeJS>express --ejs notes
...

Sau đó cài các module đi kèm:

C:\NodeJS>cd notes
C:\NodeJS\notes>npm install
...

Tạo model

Trong thư mục gốc của project, chúng ta tạo một thư mục có tên models nằm chung với các thư mục views, routes… Trong thư mục models, chúng ta tạo một file có tên notes.js với nội dung sau đây:

var notes = [];
exports.update = exports.create = function(key, title, body) {
    notes[key] = { title: title, body: body };
}

exports.read = function(key) {
    return notes[key];
}

exports.destroy = function(key) {
    delete notes[key];
}

exports.keys = function() {
    return Object.keys(notes);
}

Trong đoạn code trên chúng ta tạo một mảng có tên notes dùng để lưu trữ các ghi chú, mỗi ghi chú bao gồm key (id), title (tiêu đề) và body (nội dung). Trong phần này chúng ta chỉ thực hiện lưu trữ trong bộ nhớ RAM cho đơn giản, tức làm mỗi lần tắt/khởi động server thì các ghi chú sẽ bị xóa, trong các phần sau chúng ta sẽ sử dụng cơ sở dữ liệu để lưu dữ liệu.

Ở đoạn code trên hàm update và hàm create giống nhau vì ở đây chúng ta chưa sử dụng cơ sở dữ liệu, trong các phần sau khi sử dụng cơ sở dữ liệu thì các hàm này sẽ phải tách ra.

Mỗi ghi chú sẽ được quản lý bằng key (khóa hay id).

Tùy chỉnh trang chủ

Đầu tiên chúng ta tạo một file có tên notes.js trong thư mục routes rồi để đó, chúng ta sẽ viết file này sau. Tiếp theo trong file app.js chúng ta thêm dòng sau vào cùng với các dòng require() ở đầu file:

...
var notes = require('./routes/notes');
...

Tiếp theo, cũng như trong các phần trước, chúng ta tách phần trang web ra thành các file top.ejs, bottom.ejs để có thể dùng một cách linh hoạt khi cần.

Trong thư mục views chúng ta tạo các file top.ejs, bottom.ejs như sau:

<html>
<head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
    <h1><%= title %></h1>
    <div class='navbar'>
    <p>
        <a href='/'>Home</a> | <a href='/noteadd'>ADD Note</a>
    </p>
    </div>

Chúng ta in tiêu đề trang chủ và 2 đường link đến trang '/''/noteadd', trang /noteadd sẽ được viết sau.

</body>
</html>

Sau đó chỉnh lại file index.ejs như sau:

<% include top %>
<%
    var keys = notes.keys();
    if(keys) {
        keys.forEach(function(key) {
            var note = notes.read(key);
            %><p><%= key %>:
                <a href="/noteview?key=<%= key %>"><%= note.title %></a>
            </p><%
        });
    }
%>
<% include bottom %>

Trong file index.ejs, chúng ta có dùng đến biến notes, chúng ta dùng biến này để lấy key của từng ghi chú, sau đó hiển thị một đường link đến key đó, biến notes sẽ được gửi đến từ hàm render() nào đó, và đó sẽ là dòng code routing trong file routes/index.js, chúng ta sửa lại file routes/index.js như sau:

var express = require('express');
var router = express.Router();
var notes = require('../models/notes');

/* GET home page. */

router.get('/', function(req, res, next) {
    res.render('index', { title: 'Notes', notes: notes });
});

module.exports = router;

Chúng ta thêm dòng require() đến module models/notes, sau đó truyền vào lời gọi res.render() để file index.ejs có thể sử dụng.

Đến đây chúng ta có thể chạy thử ứng dụng:

C:\NodeJS\notes>npm start

capture

Ở đây chúng ta chỉ thấy trang chủ và 2 đường link trống không, vì chúng ta chưa tạo một ghi chú nào cả. Khi bấm vào ADD Note thì sẽ có lỗi xảy ra vì chúng ta cũng chưa viết code cho trang này.

Tạo ghi chú

Do đó bây chúng ta sẽ thêm phần tạo ghi chú.

Trong file app.js chúng ta thêm dòng sau cùng với các dòng routing khác:

...
app.use('/noteadd', notes.add);
...

Trong file routes/notes.js, chúng ta thêm đoạn code sau:

var notes = require('../models/notes');
exports.add = function(req, res, next) {
    res.render('noteedit', {
        title: "Add a note",
        docreate: true,
        notekey: "",
        note: undefined
    });
}

Đoạn code trên rất đơn giản, chúng ta dùng hàm res.render() để gọi file noteedit.ejs và truyền các tham số title, docreate, notekey, note vào file này rồi trả về cho người dùng. Ở đây tham số docreate có cho biết chúng ta đang cập nhật hay tạo mới một ghi chú, nếu là cập nhật thì tham số note sẽ là một đối tượng nào đó, còn tạo mới thì đối tượng này rỗng (undefined), trong phần cập nhật ghi chú chúng ta sẽ hiểu rõ thêm 2 tham số này,.

Tiếp theo trong thư mục views, chúng ta tạo file noteedit.ejs với nội dung như sau:

<% include top %>
<form method='POST' action='/notesave'>
    <input type='hidden' name='docreate' value='<%= docreate ? "create" : "update"%>'>
    <p>Key: <input type='text' name='notekey' value='<%= note ? notekey : "" %>'></p>
    <p>Title: <input type='text' name='title' value='<%= note ? note.title : "" %>'></p>
    <br/>
    <textarea rows=5 cols=40 name='body'><%= note ? note.body : "" %></textarea>
    <br/>
    <input type='submit' value='Submit' />
</form>
<% include bottom %>

Như chúng ta đã biết, cả phần tạo và cập nhật ghi chú dùng chung một hàm, và dùng chung một template là file noteedit.ejs. Ở đây chúng ta tạo một form có các trường để nhập dữ liệu cho một ghi chú, nếu thao tác là cập nhật thì chúng ta điền các dữ liệu có sẵn vào các trường này, người dùng chỉ việc thay đổi các trường cần thiết, nếu là tạo mới thì các trường này sẽ rỗng.

Form này sẽ gửi đến trang /notesave, và form này dùng phương thức POST để gửi, do đó bây giờ chúng ta phải làm thêm trang này. Trong file app.js chúng ta thêm dòng sau:

...
app.post('/notesave', notes.save);
...

Tiếp theo trong file routes/notes.js chúng ta thêm đoạn code sau để xử lý đương dẫn /notesave:

exports.save = function(req, res, next) {
    if (req.body.docreate === 'create') {
        notes.create(req.body.notekey,
                     req.body.title,
                     req.body.body);
    } else {
        notes.update(req.body.notekey,
                     req.body.title,
                     req.body.body);
    }
    res.redirect('/noteview?key='+req.body.notekey);
}

Đoạn code trên cũng rất đơn giản, chúng ta kiểm tra tham số docreate là gì, nếu là tạo mới (create) thì chúng ta tạo một phần tử mới trong mảng notes bằng phương thức notes.create(), nếu là cập nhật thì chúng ta cập nhật notes.update().

Sau đó chúng ta chuyển hướng trang web đến trang /noteview.

Và bởi vì form truyền lên theo phương thức POST lên dữ liệu sẽ nằm trong thuộc tính req.body được tạo từ module bodyParser. Trong file app.js Express đã tự động thêm module này cho chúng ta trong dòng app.use(bodyParser.json());

Bạn có thể chạy lại project và có thể bấm nút ADD Note để tạo ghi chú mới. Khi bấm vào nút submit thì bạn sẽ được một trang báo lỗi 404, đơn giản là vì chúng ta chưa làm trang /noteview.

capture

Tuy nhiên nếu về lại trang Home thì chúng ta vẫn thấy ghi chú đã được thêm vào và đã hiển thị lên trang chủ.

capture

Xem ghi chú

Bây giờ chúng ta sẽ làm trang /noteview để xem chi tiết một ghi chú.

Trong file app.js chúng ta thêm dòng sau:

...
app.use('/noteview', notes.view);
...

Trong file routes/notes.js chúng ta thêm đoạn code sau:

exports.view = function(req, res, next) {
    var note = undefined;
    if(req.query.key) {
        note = notes.read(req.query.key);
    }
    res.render('noteview', {
        title: note ? note.title : "",
        notekey: req.query.key,
        note: note
    });
}

Đoạn code trên sẽ xử lý đường dẫn URL /noteview, ở đây chúng ta phải kiểm tra trước khi trả về dữ liệu cho trình duyêt, bằng cách kiểm tra xem khóa có rỗng hay không, sau đó trong hàm res.render() chúng ta cũng kiểm tra xem title có rỗng hay không để trả về dữ liệu cho thích hợp. Bởi vì người dùng có thể nhập đường dẫn bằng tay chứ không chỉ có dùng chuột để click vào đường link trên trang web.

Cuối cùng chúng ta tạo một file có tên noteview.ejs trong thư mục views với nội dung như sau:

<% include top %>
<h3><%= note ? note.title : "" %></h3>
<p><%= note ? note.body : "" %></p>
<p>Key: <%= notekey %></p>
<% if (notekey) { %>
    <hr/>
    <p>
        <a href="notedestroy?key=<%= notekey %>">Delete<a/>
        <a href="noteedit?key=<%= notekey %>">Edit</a>
    </p>
<% } %>
<% include bottom %>

Các đoạn code trên sẽ chịu trách nhiệm hiển thị nội dung ghi chú. Ngoài ra còn hiển thị 2 đường link đến trang /notedestroy dùng để xóa ghi chú và /noteedit dùng để chỉnh sửa ghi chú.

capture

Tuy nhiên nếu click vào 2 đường link đó thì chúng ta sẽ được một trang báo lỗi 404, lý do cũng đơn giản là vì chúng ta chưa viết hàm routing cho 2 đường dẫn này.

Chỉnh sửa ghi chú

Trong file app.js chúng ta thêm dòng sau:

...
app.use('/noteedit', notes.edit);
...

Trong file routes/notes.js chúng ta thêm đoạn code sau:

exports.edit = function(req, res, next) {
    var note = undefined;
    if(req.query.key) {
        note = notes.read(req.query.key);
    }
    res.render('noteedit', {
        title: note ? ("Edit " + note.title) : "Add a Note",
        docreate: note ? false : true,
        notekey: req.query.key,
        note: note
    });
}

Như bạn đã biết, cả 2 hàm tạo và chỉnh sửa đều dùng chung một template là file notedit.ejs, do đó ở đây chúng ta không cần phải tạo một file .ejs nào khác. Khác với hàm tạo ghi chú, ở đây chúng ta nhận được một key của một ghi chú có sẵn, chúng ta sẽ truyền dữ liệu của ghi chú này vào các tham số trả về. Đầu tiên chúng ta khai báo một biến note có giá trị undefined, sau đó chúng ta tìm đối tượng ghi chú trong mảng notes dựa theo key, nếu tìm thấy thì truyền vào hàm res.render(), nếu không thì chúng ta truyền các tham số giống như khi tạo mới một ghi chú. Như thế sẽ phòng ngừa được việc người dùng nhập đường dẫn bằng tay lên trình duyệt và đưa đường dẫn sai.

Bây giờ bạn có thể thực hiện cập nhật ghi chú.

Xóa ghi chú

Tương tự với các chức năng trên, đầu tiên chúng ta thêm dòng sau vào file app.js:

...
app.use('/notedestroy', notes.destroy);

Tiếp theo trong file routes/notes.js, chúng ta thêm đoạn code sau:

exports.destroy = function(req, res, next) {
    var note = undefined;
    if(req.query.key) {
        note = notes.read(req.query.key);
    }
    res.render('notedestroy', {
        title: note ? note.title : "",
        notekey: req.query.key,
        note: note
    });
}

Cũng tương tự như các hàm khác, ở đây chúng ta kiểm tra xem dữ liệu gửi lên có hợp lệ hay không rồi trả về trong trang /notedestroy.

Kế tiếp chúng ta tạo file notedestroy.ejs trong thư mục views như sau:

<% include top %>
<form method="POST" action='notedodestroy'>
    <input type='hidden' name='notekey' value='<%= note ? notekey : "" %>'>
    <p>Delete <%= note.title %> ?</p>
    <br/>
    <input type="submit" value="DELETE" />
    <a href="/noteview?key=<%= notekey %>">Cancel</a>
</form>
<% include bottom %>

Ở đây chúng ta hiển thị một form cho người dùng xác nhận việc xóa file, form này sẽ gửi đến đường dẫn /notedodestroy với phương thức POST. Do đó bây giờ chúng ta phải xử lý đường dẫn này.

Đầu tiên chúng ta thêm dòng sau vào file app.js:

...
app.post('/notedodestroy', notes.dodestroy);
...

Tiếp theo chúng ta thêm đoạn code này vào file routes/notes.js:

exports.dodestroy = function(req, res, next) {
   notes.destroy(req.body.notekey);
   res.redirect('/');
}

Đoạn code trên sẽ xóa ghi chú ra khỏi mảng notes rồi chuyển hướng về trang '/'.

Bây giờ chúng ta có thể sử dụng chức năng xóa được rồi.

capture

NodeJS – Tạo REST Service

Trong bài trước chúng ta đã tìm hiểu cách tạo một gói tin HTTP, trong phần này chúng ta sẽ tìm hiểu cách tạo các truy vấn REST và cách viết một REST service.

Bản thân việc gọi các REST service đã là bất đồng bộ, tức là khi chúng ta truy vấn một hàm REST thì REST server sẽ gọi một hàm để xử lý và một hàm callback để gửi kết quả trả về, tất cả đều thông qua giao thức HTTP.

Về REST service

REST là viết tắt của REpresentational State Transfer. Đây là một chuẩn web dựa trên giao thức HTTP. Mục đích chính của REST là hỗ trợ truy cập tài nguyên thông qua giao thức HTTP. REST được giới thiệu lần đầu bởi Roy Fielding vào năm 2000.

Công việc của một REST server chỉ đơn giản là cung cấp quyền truy cập tài nguyên, một REST client sẽ dùng các quyền truy cập đó để lấy tài nguyên, tất cả đều thông qua giao thức HTTP. Các tài nguyên được xác định thông qua URI. REST sử dụng 2 định dạng dữ liệu là JSON và XML, nhưng JSON phổ biến hơn.

Nói một cách đơn giản thì giao thức HTTP ban đầu được tạo ra là để trả các website về cho trình duyệt. Sau này người ta phát minh ra chuẩn REST, HTTP không chỉ được dùng để trả về các nội dung HTML mà có thể là bất cứ thứ gì.

Ví dụ

Chúng ta sẽ viết lại server tính số fibonacci, nhưng lần này thay vì trả về một trang web HTML thì server chỉ trả về một chuỗi JSON.

Mặc dù Express ra đời với mục đích chính là để xây dựng các trang web, nhưng chúng ta cũng có thể dùng chính module Express để viết các hàm REST API. Express cho phép so sánh/tìm kiếm chuỗi trong các đường dẫn URL.

Đoạn code routing một đường dẫn URL tới một hàm xử lý cũng tương tự như khi viết một routing bình thường, ví dụ:

app.get('/user/:id', function(req, res) {
    res.send('user ' + req.params.id);
});

Trong dòng code trên, URL /user/:id có một tham số là :id, Express sẽ tách phần tham số này ra và gán vào trường req.params.id.

Bây giờ chúng ta viết lại một server để tính số fibonacci và trả về kiểu JSON, chúng ta tạo một file có tên fiboserver.js với nội dung như sau:

var math = require('./math');
var express = require('express');
var app = express();
app.get('/fibonacci/:n', function(req, res, next) {
    var result = math.fibonacciLoop(req.params.n);
    res.send({
        n: req.params.n,
        res: result
    });
});
app.listen(3002);
console.log('Fibonacci REST service is listening on port 3002');

Ở đoạn code trên chúng ta dùng 2 module là Expressmath do chúng ta viết trong các phần trước.

app.get('/fibonacci/:n', function(req, res, next) {
    ...
});

Ở đây chúng ta dùng phương thức app.get() thay vì dùng app.use() để xử lý phần routing. Tham số đầu tiên là URL, tham số thứ 2 là hàm callback dùng để gửi kết quả về client.

var result = math.fibonacciLoop(req.params.n);
res.send({
    n: req.params.n,
    res: result
});

Trong hàm callback đó chúng ta tính số fibonacci rồi lưu trong biến result, sau đó chúng ta gửi kết quả về thông qua phương thức res.send(). Tham số của phương thức res.send() là một đối tượng  được tạo theo cú pháp JSON, ở đây đối tượng này gồm có 2 tham số là nres, trong đó n chỉ đơn giản là tham số được gửi lên từ client, còn res là kết quả tính số fibonacci từ module math.

Chúng ta chạy file này, sau đó bạn có thể vào trình duyệt và gõ đường dẫn localhost:3002/fibonacci/<tham số> với tham số là một số nguyên bất kỳ, server sẽ trả về chuỗi JSON kết quả là số fibonacci cho bạn.

capture

Tuy nhiên mục đích chính của REST không phải chỉ là để trả kết quả về cho trình duyệt, bất kỳ chương trình nào có hỗ trợ giao thức HTTP đều có thể gọi các hàm REST API, kể cả smartphone, tablet, smartwatch… Chúng ta sẽ viết một client đơn giản để gửi yêu cầu đến REST service này.

Chúng ta tạo một file có tên fiboclient.js với nội dung như sau:

var http = require('http');
var util = require('util');
[
 "/fibonacci/30",
 "/fibonacci/20",
 "/fibonacci/10",
 "/fibonacci/9",
 "/fibonacci/8",
 "/fibonacci/7",
 "/fibonacci/6",
 "/fibonacci/5",
 "/fibonacci/4",
].forEach(function(path) {
    var req = http.request({
        host: "localhost",
        port: 3002,
        path: path,
        method: 'GET' 
    }, function(res) {
        res.on('data', function(chunk) {
            util.log('BODY :' + chunk);
        });
    });
    req.end();
});

Trong đoạn code trên chúng ta tạo một mảng là danh sách các đường dẫn, sau đó chúng ta lặp qua mảng này, mỗi lần lặp chúng ta gửi một yêu cầu HTTP lên server với đường dẫn tương ứng rồi in kết quả trả về.

var req = http.request({
    host: "localhost",
    port: 3002,
    path: path,
    method: 'GET' 
}, function(res) {
    res.on('data', function(chunk) {
        util.log('BODY :' + chunk);
    });
});

Chúng ta tạo một đối tượng http.ServerResponse thông qua phương thức http.request(). Trong đó chúng ta điền các thông tin cơ bản như host là địa chỉ server, port là cổng, path là đường dẫn, method là phương thức, và một hàm callback để nhận kết quả trả về từ server, hàm này nhận một tham số là res, tham số này lắng nghe sự kiện data, sự kiện này xảy ra khi có dữ liệu gửi về từ server.

Chạy đoạn code trên chúng ta được kết quả như sau:

C:\Users\PhoCode>node fiboclient.js
26 Sep 08:59:32 - BODY :{"n":"30","res":832040}
26 Sep 08:59:32 - BODY :{"n":"20","res":6765}
26 Sep 08:59:32 - BODY :{"n":"10","res":55}
26 Sep 08:59:32 - BODY :{"n":"9","res":34}
26 Sep 08:59:32 - BODY :{"n":"8","res":21}
26 Sep 08:59:32 - BODY :{"n":"7","res":13}
26 Sep 08:59:32 - BODY :{"n":"6","res":8}
26 Sep 08:59:32 - BODY :{"n":"5","res":5}
26 Sep 08:59:32 - BODY :{"n":"4","res":3}

 

NodeJS – Tạo yêu cầu HTTP

Trong Node có lớp http.ClientRequest dùng để lưu những thông tin về một gói tin HTTP được gửi đến các webserver, chúng ta có thể tạo ra hầu như tất cả mọi yêu cầu HTTP, các lớp kiểu này thường được dùng để gửi các yêu cầu tự động. Trong bài này chúng ta sẽ tìm hiểu cách tạo một yêu cầu HTTP từ client, trong bài sau chúng ta sẽ tìm hiểu và xây dựng REST server và dùng các yêu cầu HTTP này để test server đó.

Nếu bạn đã từng dùng các hệ điều hành Linux thì có lẽ bạn sẽ biết đến các trình gửi yêu cầu HTTP là wget hoặc curl. Chúng ta sẽ viết đoạn code mô phỏng trình wget.

Chúng ta tạo một file có tên wget.js với nội dung như sau:

var http = require('http');
var url = require('url');
var util = require('util');
var argUrl = process.argv[2];
var parsedUrl = url.parse(argUrl, true);
var options = {
    host: null,
    port: -1,
    path: null,
    method: 'GET'
};
options.host = parsedUrl.hostname;
options.port = parsedUrl.port;
options.path = parsedUrl.pathname;
if(parsedUrl.search)
    options.path += "?" + parsedUrl.search;
var req = http.request(options, function(res) {
    util.log('STATUS: ' + res.statusCode);
    util.log('HEADERS: ' + util.inspect(res.headers));
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
        util.log('BODY: ' + chunk);
    });
    res.on('error', function(err) {
        util.log('RESPONSE ERROR: ' + err);
    });
});
req.on('error', function(err) {
    util.log('REQUEST ERROR: ' + err);
});
req.end();

Sau đó chúng ta có thể chạy đoạn code trên như sau: node wget.js http://www.google.com

capture

Chúng ta sẽ nhận về được một gói tin trả lời từ server của google. Trong đó dòng đầu tiên là mã trạng thái STATUS, ở đây chúng ta nhận được mã là 302, mã này là mã redirect, nghĩa là chúng ta sẽ được chuyển tới một đường dẫn khác, trong trường HEADERS chúng ta có một vài thông tin khác, trong đó location là đường dẫn mà chúng ta sẽ được chuyển tới, ở đây là http://www.google.com.vn/?gfe_rd=cr&ei=WpDkV-CjEKzY8gf-n4uICw, nếu bạn mở trình duyệt web lên rồi truy cập http://www.google.com thì bạn cũng sẽ được chuyển tới đường dẫn như trên. Phần BODY là nội dung HTML trả về, nếu chúng ta dùng trình duyệt để gửi thì đoạn code HTML này sẽ được “vẽ” trên trình duyệt.

var argUrl = process.argv[2];
var parsedUrl = url.parse(argUrl, true);

Trong đoạn code trên, chúng ta tạo một đối tượng thuộc lớp URL từ phương thức url.parse() là parsedUrl, phương thức này nhận vào đường dẫn mà chúng ta đã nhập (ở trên là http://www.google.com), tham số thứ hai cho biết các tham số đi kèm trong đường dẫn có được mã hóa hay không.

var options = {
    host: null,
    port: -1,
    path: null,
    method: 'GET'
};
options.host = parsedUrl.hostname;
options.port = parsedUrl.port;
options.path = parsedUrl.pathname;

Đối tượng options sẽ lưu một số thông tin trong gói tin yêu cầu được gửi đi, như host là đường dẫn website, port là cổng, pathname là đường dẫn phía sau hostname, ví dụ www.google.com/ thì pathname'/', method là phương thức gửi lên như GET, PUT, POST

if(parsedUrl.search)
     options.path += "?" + parsedUrl.search;

Nếu chúng ta đưa vào đường dẫn có tham số, chẳng hạn http://www.google.com/?query=germany, thì đoạn ?query=germany sẽ được lưu trong thuộc tính search của đối tượng parsedUrl, ở đây chúng ta chèn vào sau thuộc tính path của đối tượng options.

var req = http.request(options, function(res) {
    util.log('STATUS: ' + res.statusCode);
    util.log('HEADERS: ' + util.inspect(res.headers));
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
        util.log('BODY: ' + chunk);
    });
    res.on('error', function(err) {
        util.log('RESPONSE ERROR: ' + err);
    });
});

Phương thức http.request() nhận vào đối tượng options và một hàm callback, hàm này sẽ được gọi khi có gói tin trả về từ server, trong hàm này chúng ta chỉ đơn giản là in nội dung gói tin đó ra màn hình. Tham số res trong hàm callback là một đối tượng thuộc lớp EventEmitter, đối tượng này lắng nghe sự kiện dataerror, sự kiện data xảy ra khi có dữ liệu được trả về, sự kiện error xảy ra khi có lỗi trả về.

req.on('error', function(err) {
    util.log('REQUEST ERROR: ' + err);
});

Phương thức http.request() trả về một đối tượng lớp http.ClientRequest. Bản thân đối tượng này kế thừa từ lớp EventEmitter và sẽ tạo ra sự kiện errordata. Sự kiện data xảy ra khi có dữ liệu đến, còn sự kiện error xảy ra khi có lỗi xảy ra. Ở đây chúng ta chỉ bắt sự kiện error, bạn có thể chạy lại đoạn code trên và đưa vào tham số là www.google.com (bỏ phần http://) thì sẽ có lỗi.

NodeJS – Ví dụ về Express

Chúng ta sẽ viết ứng dụng tính số Fibonacci với Express.

Dãy số Fibonacci là một dãy số có dạng như sau: 0 1 1 2 3 5 8 13 21 34…

Trong đó 2 số đầu tiên luôn luôn là 0 và 1, các số thứ 2 trở đi sẽ có giá trị bằng tổng của 2 số đứng trước nó. Dãy số này được phát minh vào năm 1202 bởi Lenonardo of Pisa.

Đầu tiên chúng ta tạo một project với tên fibonacci (hoặc tên bất kỳ tùy bạn đặt) và thực hiện cài đặt các module như bình thường:

C:\Users\PhoCode>express --ejs fibonacci
...
C:\Users\PhoCode>cd fibonacci
C:\Users\PhoCode>npm install
...

Đầu tiên chúng ta sửa file app.js ở thư mục gốc của project như sau:

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var routes = require('./routes/index');
var users = require('./routes/users');
var fibonacci = require('./routes/fibonacci');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes);
app.use('/users', users);
app.use('/fibonacci', fibonacci.index);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});


// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});

module.exports = app;

Trong đoạn code trên chúng ta chèn thêm 2 dòng là:

var fibonacci = require('./routes/fibonacci');
...
app.use('/fibonacci', fibonacci.index);

Lưu ý là dòng require() nên được đặt cùng với các dòng require() khác cho dễ quản lý, còn dòng app.use() là phần routing, bạn có thể đặt ở đâu cũng được nhưng phải đặt trước đoạn code dùng để báo lỗi 404 này:

// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

Lý do là vì Express so sánh các đường dẫn từ trên xuống dưới, nếu không thấy đường dẫn nào khớp thì sẽ trả về trang 404.

Tiếp theo cũng trong thư mục chứa file app.js, chúng ta tạo một file có tên math.js với nội dung như sau:

var fibonacci = exports.fibonacci = function(n) {
    var fibos = [];
    fibos[0] = 0;
    fibos[1] = 1;
    fibos[2] = 1;
    for(var i = 3 ; i <= n ; i++) 
        fibos[i] = fibos[i - 2] + fibos[i - 1];
    return fibos[n];
}

Đây chỉ là module math chứa hàm tính số Fibonacci viết bằng vòng lặp rất đơn giản.

Tiếp theo chúng ta sẽ tạo các  file có đuôi là .ejs, đây chỉ là các file chứa code HTML để in nội dung lên trang web.

Đầu tiên chúng ta tạo một file có tên top.ejs trong thư mục views với nội dung như sau:

<html>
<head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
    <h1><%= title %></h1>
    <div class='navbar'>
        <p>
            <a href='/'>Home</a>
            |
            <a href='/fibonacci'>Fibonacci calculator</a>
        </p>
    </div>

Cũng trong thư mục views chúng ta tạo file bottom.ejs với nội dung như sau:

</body>
</html>

Tiếp theo chúng ta chỉnh lại file views/index.ejs như sau:

<% include top %>
<p>Welcome to the Math calculator</p>
<% include bottom %>

File index.ejs được dùng để trả về cho đường dẫn '/', các dòng <% include top %><% include bottom %> có tác dụng chép đoạn code trong file top.ejsbottom.ejs vào vị trí tương ứng trong file index.ejs. Đây chỉ là một thủ thuật để chúng ta phân chia các thành phần của một trang web thành các file riêng biệt, sau này khi tạo các trang khác chúng ta cũng chỉ cần thêm các câu lệnh include như thế là được, thay vì phải code lại từ đâu. Các đoạn code nằm trong cặp <%= %> hoặc <% %> là code của riêng Express, được dùng để kết nối với code JavaScript chứ không hoàn toàn là code HTML.

Bây giờ chúng ta tạo file fibonacci.ejs trong thư mục views với nội dung như sau:

<% include top %>
<% if (typeof fiboval !== "undefined") { %>
    <p>Fibonacci's index: <%= fibonum %></p> 
    <p>Value: <%= fiboval %></p>
<% } %>
<p>Enter Fibonacci's index:<p>
<form name='fibonacci' action='/fibonacci' method='get'>
    <input type='text' name='fibonum' />
    <input type='submit' value='Submit' />
</form>
<% include bottom %>

Cuối cùng chúng ta tạo một file có tên fibonacci.js trong thư mục routes với nội dung như sau:

var math = require('../math');
exports.index = function(req, res) {
    if(req.query.fibonum) {
        res.render('fibonacci', {
            title: "Calculate Fibonacci numbers",
            fibonum: req.query.fibonum,
            fiboval: math.fibonacci(req.query.fibonum)
        });
    } else {
        res.render('fibonacci', {
            title: "Calculate Fibonacci numbers",
            fiboval: undefined
        });
    }
};

Thế là xong, chúng ta có thể chạy project (bằng lệnh npm start) để xem kết quả:

capture

Trang localhost:3000 được trả về từ file views/index.ejs.

capture

Khi chúng ta click vào đường link Fibonacci calculator thì sẽ được chuyển đến trang /fibonacci có file trả về là views/fibonacci.ejs. Ở trang này chúng ta có thể nhập số thứ tự và trang web sẽ trả về giá trị Fibonacci đó, chẳng hạn số fibonacci thứ 20 có giá trị là 4181.

Chúng ta sẽ tìm hiểu những gì diễn ra ở server như sau:

Trong file app.js, chúng ta có 3 đường dẫn routing, 2 đường dẫn đầu tiên là '/''/users' do Express tự tạo ra (bạn có thể nhập localhost:3000/users để thấy câu thông báo respond with a resource, đường dẫn thứ 3 là '/fibonacci' do chúng ta tạo ra. Các hàm res.render() (trong file index.jsfibonacci.js) sẽ dựng trang web với các file .ejs tương ứng.

Có nghĩa là khi chúng ta truy cập localhost:3000/, Express sẽ chạy đoạn code trong file routes/index.js:

var express = require('express');
var router = express.Router();

/* GET home page. */

router.get('/', function(req, res, next) {
    res.render('index', { title: 'Express' });
});

module.exports = router;

Dòng res.render('index', { title: 'Express' }); có chức năng trả về nội dung HTML trong file views/index.ejs cho người dùng, ngoài ra hàm res.render() không chỉ gọi file index.ejs không mà còn truyền tham số vào đó nữa, ở đây tham số là title có giá trị là Express. Các tham số này sẽ được dùng trong các file .ejs, file index.ejs lại được dựng từ 2 file là top.ejsbottom.ejs, trong đó chỉ có file top.ejs là sử dụng giá trị trong tham số title (ở dòng code <%= title %>).

Tương tự, khi chúng ta click vào đường link để được chuyển đến trang /fibonacci thì hàm index() trong file routes/fibonacci.js sẽ được gọi, bởi vì trong file app.js chúng ta đã chỉ định đường dẫn /fibonacci sẽ gọi hàm này bằng dòng app.use('/fibonacci', fibonacci.index), hàm res.render() sẽ trả về nội dung HTML trong file fibonacci.ejs cho người dùng, đi kèm là một số tham số là title, fibovalfibonum.

Trong file fibonacci.ejs chúng ta khai báo một form với một textbox và một nút Submit để nhập số thứ tự để tính toán giá trị Fibonacci, textbox ở đây được đặt tên là fibonum, khi người dùng click nút Submit thì trình duyệt của người dùng sẽ gửi một yêu cầu HTTP đến server với đường dẫn /fibonacci kèm theo tham số fibonum, trong file fibonacci.js chúng ta kiểm tra xem tham số đó có được truyền lên hay không, nếu có thì chúng ta tính toán và trả về trong tham số fiboval, nếu không thì trả về giá trị undefined, cũng trong file fibonacci.ejs chúng ta kiểm tra xem giá trị fiboval này có khác undefined hay không, nếu khác thì tức là có một giá trị đã được tính toán và chúng ta cho in ra kèm theo với form luôn.

Ban đầu bạn có thể thấy nó hơi rối rắm nhưng thật ra cũng không có gì khó, bạn chỉ cần ngồi ngẫm nghĩ một tí là sẽ hiểu rõ cách những file này hoạt động cùng nhau như thế nào thôi 🙂

NodeJS – Web Framework Express

Như trong bài trước đã nói, đối tượng http.Server cung cấp các hàm cấp thấp cho phép chúng ta làm việc sâu hơn với các thành phần của server như giao thức, gói tin… tức là chúng ta có thể viết server theo ý của chúng ta một cách linh hoạt hơn, tuy nhiên trên thực tế thì khi làm một ứng dụng web chúng ta chỉ nên chỉ nên chú trọng vào các chức năng của web chứ không nên đi sâu vào phần kỹ thuật làm gì.

Do đó cộng đồng Node đã phát triển một số module có chức năng đảm nhiệm các thành phần đó cho chúng ta rồi, bạn có thể xem danh sách ở đây: https://github.com/nodejs/node/wiki/modules#web-frameworks

Các module được phân loại thành các chức năng chính sau đây:

  • Router: có chức năng điều hướng các yêu cầu HTTP đến các hàm xử lý tương ứng. Đây cũng là tính năng có trong các web framework phổ biến khác như Ruby on Rails, Django, Sinatra…
  • Static file servers: có các hàm trả về các file tài nguyên cho client
  • Framework: hỗ trợ phát triển ứng dụng, hầu hết các framework này đều được phát triển dựa trên các framework phổ biến khác như Django, Rails, WebMachine, CakePHP… trong đó Express là web framework phổ biến nhất của Node.
  • Middleware: các module loại này hoạt động giữa phần nhận gói tin HTTP và phần chuyển gói tin đó đến các hàm xử lý.

Express

Trang chủ của module này có địa chỉ là: http://expressjs.com/

Để có thể sử dụng Express thì trước tiên chúng ta phải cài đặt, bạn mở terminal (cmd trong Windows) lên và gõ 2 dòng lệnh sau:

C:\Users\PhoCode>npm install -g express
...
C:\Users\PhoCode>npm install -g express-generator

Hai dòng lệnh trên cài module expressexpress-generator, trong đó express là web framework chính, còn express-generator là module giúp tạo một project Express mẫu, tức là nó tự tạo cho chúng ta các file và thư mục cần thiết.

C:\Users\PhoCode>express --version
4.13.4

Chúng ta có thể xem phiên bản express mới vừa được cài bằng cách chạy câu lệnh express --version. Hiện phiên bản express mà mình dùng để viết các bài hướng dẫn này là phiên bản 4.13.4.

Tiếp theo chúng ta sẽ tạo một project Express bằng câu lệnh:

C:\User\PhoCode>express --ejs FirstProject
    
    create : FirstProject
    create : FirstProject/package.json
    create : FirstProject/app.js
    create : FirstProject/public
    create : FirstProject/routes
    create : FirstProject/routes/index.js
    create : FirstProject/routes/users/js
    create : FirstProject/public/stylesheets
    create : FirstProject/public/stylesheets/style.css
    create : FirstProject/views
    create : FirstProject/views/index.ejs
    create : FirstProject/views/error.ejs
    create : FirstProject/public/images
    create : FirstProject/public/javascripts
    create : FirstProject/bin
    create : FirstProject/bin/www

    install dependencies:
      > cd FirstProject && npm install
    
    run the app:
      > SET DEBUG=FirstProject:* & npm start

Câu lệnh express --ejs FirstProject sẽ tạo một project có tên là FirstProject, bao gồm một thư mục có tên FirstProject với các file và thư mục như trên.

Một project Express sử dụng một số module ngoài nữa nên chúng ta phải cài thêm một số module vào thư mục này bằng câu lệnh:

C:\Users\PhoCode>cd FirstProject && npm install
...

Câu lệnh trên bao gồm 2 câu lệnh là cd FirstProjectnpm install, câu lệnh cd chỉ là chuyển thư mục hiện tại vào thư mục FirstProject, còn câu lệnh npm install sẽ đọc danh sách các module được sử dụng trong file package.json rồi tạo thư mục node_modules và cài đặt các module đó vào thư mục này.

Sau đó để chạy project này thì chúng ta chạy lệnh sau:

C:\Users\PhoCode\FirstProject>SET DEBUG=FirstProject:* & npm start

> FirstProject@0.0.0 start C:\Users\PhoCode\FirstProject
> node ./bin/www

 FirstProject:server Listening on port 3000 + 0ms

Trong đó câu lệnh SET DEBUG=FirstProject:* cho phép chúng ta xem các hoạt động đang xảy ra trên server, còn npm start sẽ chạy server. Mặc định server sẽ chạy trên cổng 3000. Chúng ta có thể vào trình duyệt và gõ localhost:3000 hoặc 127.0.0.1:3000 để xem kết quả.

Đây chỉ là một project trống không với một vài câu thông báo cơ bản.

capture

Các câu lệnh npm install hay SET DEBUG... có thể khác tùy thuộc vào phiên bản Express khác nhau. Tuy nhiên các câu lệnh này đều có hướng dẫn sau khi bạn chạy lệnh express --ejs để tạo project, bạn xem lại sau khi chạy lệnh tạo project thì màn hình có in ra một số câu thông báo, trong đó các dòng thông báo cuối cùng là các dòng hướng dẫn bạn cài các module và chạy server (phần install dependenciesrun the app).

File app.js

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var routes = require('./routes/index');
var users = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes);
app.use('/users', users);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
    var err = new Error('Not Found');
    err.status = 404;
    next(err);
});

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
        message: err.message,
        error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
       message: err.message,
       error: {}
    });
});


module.exports = app;

File app.js là file chính, có thể coi như là file main trong C++, Java… vậy, file này được tạo ra ở thư mục gốc của project. Chúng ta sẽ tìm hiểu một số câu lệnh trong file này.

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var routes = require('./routes/index');
var users = require('./routes/users');

Như đã nói ở trên, một project tạo từ Express có sử dụng một số module ngoài nữa như serve-favicon, morgan, cookie-parser... mà những module này không được tự động cài khi chúng ta cài Express, do đó chúng ta phải chạy lệnh npm install để npm cài các module còn thiếu. Ngoài ra Express còn tạo 2 module riêng là routes/indexroutes/users, 2 module này có nhiệm vụ xử lý các yêu cầu HTTP tương ứng.

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

Phương thức app.set() thiết lập các thông số cho project. Các dòng trên thiết lập đường dẫn đến các module View và engine được dụng để dựng trang web của project, chúng ta sẽ tìm hiểu về View sau.

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', routes);
app.use('/users', users);

Phương thức app.use() cấu hình các module middleware như favicon, logger, bodyParser...v.v

Ngoài ra phương thức app.use() còn được dùng để chỉ định hàm xử lý cho từng yêu cầu HTTP nữa. Trong đoạn code trên URL '/' sẽ được chuyển đến module routes, còn '/users' sẽ được chuyển đến module users.

Chúng ta sẽ tìm hiểu kỹ hơn trong các bài sau.

NodeJS – HTTP Server

Đối tượng HTTP Server là đối tượng chính trong một ứng dụng Node, mục đích chính của đối tượng này là lắng nghe và giao tiếp với các kết nối HTTP. Mặc dù sau này hầu hết (hoặc chắc chắn) chúng ta sẽ sử dụng một web framework khác là Express để phát triển ứng dụng vì framework này đã đơn giản hóa mọi thứ, chúng ta chỉ cần quan tâm đến việc phát triển các tính năng chứ không cần quan tâm đến mấy thứ cấp dưới như giao thức, gói tin…v.v. nhưng vì Express cũng được xây dựng dựa trên đối tượng HTTP Server nên ít nhất chúng ta cũng nên tìm hiểu qua.

Trong bài cài đặt Node, chúng ta đã viết thử một web server đơn giản như sau:

var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type' : 'text/plain'});
    res.end('Hello, World!\n');
}).listen(8124, '127.0.0.1');
console.log('Server running at http://127.0.0.1:8124');

Hàm http.createServer() sẽ tạo một đối tượng http.Server, và bởi vì đối tượng này kế thừa từ lớp EventEmitter nên chúng ta có thể viết lại đoạn code trên cho dễ nhìn hơn như sau:

var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
    res.writeHead(200, {'Content-Type' : 'text/plain'});
    res.end('Hello, World!\n');
});
server.listen(8124, '127.0.0.1');
console.log('Server running at http://127.0.0.1:8124');

Khi sự kiện request xảy ra (tức là khi có người truy cập vào website), hàm function(req, res) {...} sẽ được gọi, hàm này có 2 tham số là reqres, trong đó req là một đối tượng lớp http.IncomingMessage, còn res là đối tượng lớp http.ServerResponse. Đối tượng req chứa thông tin của gói tin được gửi lên, đối tượng res sẽ chứa những dữ liệu trả về cho trình duyệt. Hàm listen() sẽ chạy server và lắng nghe trên cổng do chúng ta chỉ định, ở đây là cổng 8124.

Chúng ta sẽ sửa lại đoạn code trên một tí để server trả về một số thông tin khác thay vì chỉ in ra dòng chữ Hello, World như trên:

var http = require('http');
var util = require('util');
var url  = require('url');
var os   = require('os');
var server = http.createServer();
server.on('request', function(req, res) {
    var reqUrl = url.parse(req.url, true);
    res.writeHead(200, {'Content-Type': 'text/html'}); 
    var content = "";
 
    if(reqUrl.pathname === '/') {
        content += "<html>";
        content += "<head>";
        content += "<title>Hello, World!</title>";
        content += "</head>";
        content += "<body>";
        content += "<p><a href='/osinfo'>OS Info</a></p>";
        content += "</body>";
        content += "</html>";
    } else if (reqUrl.pathname === '/osinfo') {
        content += "<html>";
        content += "<head>";
        content += "<title>Operating System Info</title>";
        content += "</head>";
        content += "<body>";
        content += "<h1>Operating System Info</h1>";
        content += "<table>";
        content += "<tr><th>TMP Directory</th><td>" + os.tmpDir() + "</td></tr>";
        content += "<tr><th>Host Name</th><td>" + os.hostname() + "</td></tr>";
        content += "<tr><th>OS Type</th><td>" + os.type() + " " + os.platform() + " " + os.arch() + "</td></tr>";
        content += "<tr><th>Memory</th><td>total: " + os.totalmem() + ", free: " + os.freemem() + "</td></tr>"; 
        content += "</table>";
        content += "</body>";
        content += "</html>";
    }
 
    res.end(content);
 
});
server.listen(8124);
console.log('Listening to http://127.0.0.1:8124');

Nếu bạn đã từng làm việc với PHP thì đoạn code trên làm một số công việc tương tự như hàm sysinfo() trong PHP, module os có chức năng cung cấp các thông tin về server, nếu muốn bạn có thể tìm hiểu thêm về module này trên mạng.

Điều quan trọng hơn cần quan tâm trong đoạn code trên là phần routing, tức là phần xử lý từng đường dẫn url riêng biệt. Khi chúng ta gõ 127.0.0.1:8124 lên trình duyệt thì trình duyệt sẽ gửi một gói tin HTTP đến server và server sẽ truyền gói tin đó vào tham số req trong hàm xử lý, tham số này chứa một số thông tin có thể dùng cho việc routing là request.urlrequest.method.

Ở đoan code trên chúng ta dùng trường request.url để routing, đây là một chuỗi chứa đường dẫn phía sau địa chỉ server, ví dụ 127.0.0.1:8124 thì request.url chỉ là '/', nếu 127.0.0.1:8124/osinfo thì request.url'/osinfo'. Ngoài ra chúng ta không dùng tham số này một cách bình thường mà thay vào đó chúng ta tạo một đối tượng lớp URL bằng phương thức url.parse(), sau đó mỗi lần kiểm tra đường dẫn để trả về nội dung thì chúng ta xem trường url.pathname.

Ngoài url ra thì có một tham số chúng ta cũng hay quan tâm tới là request.method, nếu bạn đã từng làm web trước đây thì bạn sẽ biết là một yêu cầu gửi lên server thường có các kiểu method như GET, POST, PUT... ở đây chúng ta chưa đụng tới và sẽ tìm hiểu trong các bài sau.

Ngoài ra cách kiểm tra đường dẫn bằng cách so sánh chuỗi trong đoạn code trên thường không hiệu quả với các ứng dụng lớn, vì các ứng dụng lớn thường có đường dẫn rất phức tạp và có cả tham số nên thường chúng ta sẽ sử dụng các phương pháp khớp mẫu chuỗi để routing, chúng ta sẽ tìm hiểu thêm trong các bài sau.

Chạy đoạn code trên chúng ta có kết quả tương tự như sau:

capture

NodeJS – EventEmitter

EventEmitter là một lớp trong Node, lớp này có chức năng chính là phát sinh sự kiện, vì Node chạy theo hướng lập trình sự kiện nên lớp này là một trong số những lớp cốt lõi của Node, cũng vì thế nên dù EventEmitter không liên quan gì tới web nhưng đóng một vai trò rất quan trọng.

Ví dụ

Chúng ta tạo một file có tên pulser.js với nội dung như sau:

var events = require('events');
var util = require('util');

function Pulser() {
    events.EventEmitter.call(this);
}

util.inherits(Pulser, events.EventEmitter);

Pulser.prototype.start = function() {
    var self = this;
    this.id = setInterval(function() {
    self.emit('pulse');
    }, 1000);
}

var pulser = new Pulser();
pulser.on('pulse', function() {
    console.log('pulse received');
});
pulser.start();

Trong đoạn code trên chúng ta định nghĩa lớp Pulser kế thừa từ lớp EventEmitter.

var events = require('events');
var util = require('util');

Đầu tiên chúng ta cần dùng 2 gói là eventsutil.

function Pulser() {
    events.EventEmitter.call(this);
}
util.inherits(Pulser, events.EventEmitter);

Tiếp theo đoạn code trên chúng ta định nghĩa lớp Pulser bằng hàm khởi tạo Pulser(), dòng events.EventEmitter.call(this)util.inherits(...) có tác dụng sao chép toàn bộ những gì có trong lớp EventEmitter vào lớp Pulser, hay nói cách khác là ở đây chúng ta tiến hành cho lớp Pulser kế thừa từ lớp EventEmitter.

Pulser.prototype.start = function() {
    ...
    self.emit('pulse');
    ...
}

Tiếp theo chúng ta định nghĩa hàm start() cho đối tượng prototype của lớp Pulser, hàm này về cơ bản thì cứ sau 1 giây sẽ phát đi một sự kiện bằng phương thức emit().

Phương thức emit() là phương thức của lớp EventEmitter nhưng chúng ta đã cho kế thừa trong lớp Pulser nên có thể gọi từ lớp Pulser. Phương thức emit() sẽ làm công việc phát sinh một sự kiện để các đối tượng khác có thể lắng nghe và “bắt” sự kiện này, tham số đầu vào của phương thức emit() gồm có 1 chuỗi là tên sự kiện dùng để phân biệt các sự kiện, sau đó là danh sách các tham số, ở đây chúng ta không đưa vào tham số nào, nhưng giả sử nếu muốn bạn có thể truyền bất cứ thứ gì cũng được, ví dụ emit('pulse', 1, 'hello world', 2.7465).

var pulser = new Pulser();
pulser.on('pulse', function() {
    console.log('pulse received');
});
pulser.start();

Tiếp theo chúng ta tạo một đối tượng lớp Pulser(), sau đó chúng ta cho đối tượng này “lắng nghe” sự kiện pulse bằng phương thức on(), phương thức này nhận vào tên sự kiện và hàm sẽ xử lý sự kiện đó, ở đây sự kiện phát đi không mang theo tham số nào, nhưng giả sử như nếu có thì nếu muốn bắt các tham số đó, chúng ta chỉ cần khai báo trong hàm xử lý là được, ví dụ:

pulser.on('pulse', function(arg1, arg2, someArg) {
    console.log('pulse received', arg1, arg2, someArg);
});

Chạy đoạn code trên chúng ta được kết quả như sau:

C:\Users\PhoCode>node pulser.js
pulse received
pulse received
pulse received
pulse received
pulse received
pulse received
pulse received
...

Bạn có thể phát ra bao nhiêu sự kiện cũng được, và mỗi sự kiện có thể có bao nhiêu phương thức lắng nghe cũng được.

NodeJS – Hệ thống quản lý gói npm

Như trong bài hướng dẫn cài đặt, chúng ta đã biết npm là một phần mềm quản lý và phân phối gói dành cho Node, nếu bạn có sử dụng các hệ điều hành Linux thì npm giống mấy thứ như apt-get, rpm, yum, MacPorts...v.v vậy. Mục đích của npm là phân phối các gói Node trên mạng Internet cho tất cả người dùng chỉ với vài dòng lệnh, bạn có thể tìm các gói, tải về và cài đặt rất nhanh chóng.

Bản thân npm cũng là một gói của Node và được viết theo quy tắc chuẩn của CommonJS.

Định dạng của một gói

Gói npm là một thư mục trong đó chứa một file có tên là package.json, file này lưu các thông tin mô tả về gói. Các thông tin này được ghi theo định dạng chuẩn CommonJS Package 1.0. Nếu bạn muốn đi sâu vào tìm hiểu về file package.json của gói npm thì có thể gõ lệnh sau:

C:\Users\PhoCode>npm help json

Thông thường một file package.json có nội dung như sau:

{ 
    name: "packageName",         // tên gói
    version: "1.0",              // số phiên bản
    main: "mainModuleName",      // tên module chính
    modules: {                   // danh sách các module đi kèm
        "mod1" : "lib/mod1",     
        "mod2" : "lib/mod2"
    }
}

Đoạn code trên viết bằng định dạng JSON nên sẽ không có gì lạ nếu bạn đã từng làm việc với JavaScript.

Trong đó có 2 trường quan trọng là nameversion. Nếu bạn có ý định viết một gói và đăng lên hệ thống npm thì trước tiên bạn nên xem tên gói mà mình định đặt có bị trùng hay chưa bằng cách tìm trên http://search.npmjs.org hoặc dùng lệnh tìm kiếm sau:

C:\Users\PhoCode>npm search <tên gói>

Tên module chính trong trường main chỉ định module sẽ được chạy đầu tiên khi chúng ta gọi hàm require('packageName'). Một gói cũng có thể chứa nhiều module khác và được liệt kê trong trường modules.

Một gói cũng có thể sử dụng các gói khác, các gói này được ghi trong trường dependencies, ví dụ:

"dependencies" : {
    "foo" : "1.0.0 - 2.x.x",
    "bar" : ">=1.0.2 < 2.1.2"
}

Ngoài ra còn có trường description (mô tả) và trường keyword (từ khóa) có tác dụng hỗ trợ người khác tìm gói của chúng ta trên hệ thống npm dễ dàng hơn. Trường homepage ghi địa chỉ website của người phát triển gói (hoặc bất cứ địa chỉ nào mà người đó muốn), trường author ghi thông tin của người phát triển, ví dụ:

"description" : "Package nay duoc phat trien boi Pho Code",
"homepage" : "https://phocode.com",
"author" : phocode7@gmail.com

Trường directories lưu danh sách thư mục của gói, ví dụ:

directories : { lib: './lib', bin: './bin' },

Trường scripts lưu danh sách các lệnh của gói, ví dụ install, activate, uninstall, update... Ví dụ chúng ta xem danh sách các lệnh của gói npm bằng lệnh sau:

C:\Users\PhoCode>npm help scripts

Lệnh trên sẽ mở website tài liệu của gói npm.

Tìm một gói

Mặc định khi cài thì các gói sẽ được tải về từ địa chỉ http://npmjs.org. Để cài thì bạn chỉ cần gõ lệnh:

C:\Users\PhoCode>npm install <tên gói>

Nếu bạn không biết tên gói thì có thể tìm trên 2 website là http://npmjs.org hoặc http://search.npmjs.org.

Ngoài ra bạn có thể dùng lệnh search của npm để tìm nữa, ví dụ giả sử chúng ta tìm các gói có liên quan đến từ khóa mp3 thì gõ lệnh sau:

C:\Users\PhoCode>npm search mp3
mediatags Tools extracting for media meta-data tags =coolaj16 uttil m4a aac mp3 id3 jpeg exiv xmp
node3p    An Amazon MP3 downloader for NodeJS       =ncb000gt

Chúng ta tìm được 2 gói là mediatags  và node3p, nếu muốn cài mediatags thì chỉ cần dùng lệnh install:

C:\Users\PhoCode>npm install mediatags

Sau khi cài gói nếu muốn xem tài liệu về gói đó thì chúng ta có thể dùng lệnh view:

C:\Users\PhoCode>npm view zombie

Một số lệnh trong npm

Tiếp theo chúng ta sẽ tìm hiểu một số lệnh như làm thế nào để tải gói về, sử dụng hoặc xóa một gói.

Xem hướng dẫn sử dụng

Lệnh help trong npm cho biết tất cả mọi thứ về gói/lệnh/module cụ thể, ví dụ:

C:\Users\PhoCode>npm help npm

Dòng trên sẽ mở website để xem hướng dẫn về gói npm.

Xem thông tin gói

Lệnh view sẽ in nội dung của file package.json, nếu nội dung quá dài thì chúng ta có thể yêu cầu chỉ hiển thị một trường cụ thể nào đó trong file này. Ví dụ:

C:\Users\PhoCode>npm view google-openid dependencies
{ express: '>= 0.0.1', openid: '>= 0.1.1 <= 0.1.1' }

Lệnh trên sẽ in nội dung trường dependencies trong file package.json của gói google-openid.

Cài một gói

Đơn giản là chỉ cần dùng lệnh npm install với tên gói là được:

C:\Users\PhoCode>npm install openid
openid@2.0.4 node_modules/openid
...

Lưu ý là các gói sẽ được tải về trong thư mục node_modules trong thư mục hiện tại của terminal, tức là đoạn lệnh trên sẽ tải gói openid về thư mục C:\Users\PhoCode\node_modules. Các gói được cài như vậy sẽ chỉ có thể đọc được từ các ứng dụng nằm cùng thư mục hoặc các ứng dụng nằm trong thư mục con.

Nếu muốn ứng dụng nào cũng có thể đọc được thì chúng ta cài gói đó vào thư mục cài đặt Node bằng cách thêm tùy chọn -g vào sau lệnh install:

C:\Users\PhoCode>npm install -g openid

Liệt kê các gói đã được cài đặt

Lệnh npm list sẽ liệt kê danh sách các gói đã được cài đặt trong thư mục hiện tại của terminal, danh sách được hiển thị theo dạng cây, bao gồm cả các module con có trong từng gói. Ví dụ:

C:\Users\PhoCode>npm list
├─┬ mediatags@0.1.0
│ ├─┬ futures@2.3.3
│ │ ├── asyncify@2.1.2
│ │ ├── chainify@2.1.2
│ │ ├── events.node@0.4.9
│ │ ├── forEachAsync@2.2.1
│ │ ├── future@2.3.1
│ │ ├── join@2.3.2
│ │ ├── loop@2.1.2
│ │ └── sequence@2.2.1
│ └─┬ walk@2.3.9
│ └── foreachasync@3.0.0
├─┬ openid@2.0.4
│ └─┬ request@2.74.0
│ ├── aws-sign2@0.6.0
│ ├── aws4@1.4.1
│ ├─┬ bl@1.1.2
│ │ └─┬ readable-stream@2.0.6
│ │ ├── core-util-is@1.0.2
│ │ ├── inherits@2.0.3
│ │ ├── isarray@1.0.0
│ │ ├── process-nextick-args@1.0.7
│ │ ├── string_decoder@0.10.31
│ │ └── util-deprecate@1.0.2
│ ├── caseless@0.11.0
│ ├─┬ combined-stream@1.0.5
...

Nếu không muốn xem theo dạng cây thì chúng ta có thể xem theo dạng đường dẫn thư mục bằng cách thiết lập tham số parseable như sau:

C:\Users\PhoCode>npm set parseable=true
C:\Users\PhoCode>npm list
C:\Users\PhoCode\node_modules\mediatags
C:\Users\PhoCode\node_modules\mediatags\node_modules\futures
C:\Users\PhoCode\node_modules\mediatags\node_modules\futures\node_modules\asyncify
C:\Users\PhoCode\node_modules\mediatags\node_modules\futures\node_modules\chainify
C:\Users\PhoCode\node_modules\mediatags\node_modules\futures\node_modules\events.node
C:\Users\PhoCode\node_modules\mediatags\node_modules\futures\node_modules\forEachAsync
C:\Users\PhoCode\node_modules\mediatags\node_modules\futures\node_modules\future
...

Cập nhật phiên bản mới cho các gói

Để xem danh sách các gói đã cũ (hay trên mạng đã có phiên bản mới) chúng ta dùng lệnh outdated:

C:\Users\PhoCode>npm outdated
less@1.3.3 node_modules/less current=1.3.1
gdata-js@2.0.1 node_modules/gdata-js current=0.0.4
consolidate@0.7.0 node_modules/consolidate current=0.5.0

Lệnh outdated hiển thị danh sách các gói đã cũ bao gồm số phiên bản đang cài và số phiên bản mới nhất. Để cập nhật một gói thì chúng ta dùng lệnh update, ví dụ:

C:\Users\PhoCode>npm update express
connect@1.4.1 ./node_modules/express/node_modules/connect
mime@1.2.2 ./node_modules/express/node_modules/mime
qs@0.1.0 ./node_modules/express/node_modules/qs
express@2.3.6 ./node_modules/express

Xóa một gói

Nếu bạn đang dùng thử một gói mà thấy nó “cùi bắp” quá thì có thể xóa đi bằng lệnh uninstall, ví dụ:

C:\Users\PhoCode>npm uninstall openid
unbuild openid@2.0.4

NodeJS – Module

Module là các thành phần cơ bản để xây dựng nên một ứng dụng Node. Một module chứa các hàm API, tức là chúng ta chỉ biết chính xác hàm đó có tên gì, có tham số gì, dữ liệu trả về là gì chứ không biết chi tiết chúng hoạt động như thế nào.

Trong các bài trước chúng ta đã làm việc với module, bản thân mỗi file JavaScript đã là một module rồi. Trong phần này chúng ta sẽ tìm hiểu sâu hơn.

Định nghĩa một module

Trong bài trước chúng ta có viết một file có tên ls.js, trong file đó có dòng code như sau:

var fs = require('fs');

Trong dòng code trên chúng ta dùng hàm require(), hàm này có chức năng tìm module fs và chép vào trình thông dịch, và chúng ta có thể sử dụng các hàm có trong module đó. Đối tượng fs (biến var fs) chứa code được định nghĩa trong module fs.

Tiếp theo chúng ta xem xét ví dụ sau:

var count = 0;
exports.next = function() {
    return count++;
}

Chúng ta tạo một file có tên simple.js với đoạn code trên, sau đó mở terminal lên và gõ lệnh node, chỉ gõ node để mở trình thông dịch Node lên thôi chứ không chạy file simple.js. Lưu ý nếu thư mục hiện tại trên terminal không phải là thư mục chứa file simple.js thì bạn phải chuyển đường dẫn đến thư mục đó (bằng lệnh cd trên cả Windows và Linux, nếu không biết bạn có thể tìm trên Google cách dùng). Sau đó chúng ta chạy một số lệnh như sau:

C:\Users\PhoCode>node
> var s = require('./simple');
> s.next();
0
> s.next();
1
> s.next();
2
> s.next();
3
>

Trong file simple.js, đối tượng exports được trả về từ hàm require('./simple'). Cũng có nghĩa là cứ mỗi lần chúng ta gọi s.next() thì hàm next() được định nghĩa trong file simple.js sẽ được gọi, ở đây hàm next() trả về giá trị của biến count và tăng biến đó lên 1 đơn vị, do đó các lần gọi tiếp theo giá trị sẽ tăng dần.

Quy tắc ở đây là bất cứ thứ gì từ hàm cho tới đối tượng nào mà được gán vào làm một trường của đối tượng exports thì đều có thể gọi được ở ngoài, nếu không phải là một trường của đối tượng exports thì sẽ không thể gọi tới được, cho dù có require() hay không.

Lưu ý là các đối tượng global được định nghĩa trong một file .js cũng chỉ có thể nhìn thấy ở trong file đó thôi, chứ không thể gọi từ file khác được, nếu muốn gọi từ nơi khác thì chúng ta cũng phải gán vào đối tượng exports.

Các module trong Node

Các module có trong Node thường được viết theo cách viết của module CommonJS (chúng ta sẽ tìm hiểu về CommonJS sau) nhưng không giống hoàn toàn. Trong Node, mỗi module được lưu trong một file, cách lưu các file này cũng rất linh hoạt.

Tên một module chính là tên file .js nhưng không có phần đuôi, tức là chúng ta viết require('./simple') thì Node sẽ tự hiểu là tìm file simple.js.

Ngoài các module là code thì Node cũng hỗ trợ các thư viện đã được dịch ra thành file nhị phân, ở đây các file này có đuôi là .node. Tuy nhiên chúng ta sẽ không đi sâu vào tìm hiểu các module kiểu này, bạn chỉ cần nhớ là các file có đuôi .node là các file module bình thường như các file .js thôi, có điều là chúng đã được dịch thành mã nhị phân.

Ngoài ra còn có các module cốt lõi đã được biên dịch thành file khả thi (có đuôi .exe trong Windows).

Đường dẫn để tìm module của Node có 3 loại là đường dẫn tương đối, đường dẫn tuyệt đối hoặc top-level.

Ví dụ:

  • Đường dẫn tương đối : ./simple.js, ../simple.js
  • Đường dẫn tuyệt đối : C:/simple.js
  • Đường dẫn top-level: simple.js (module nằm chung thư mục)

Module nằm trong một app

Thường thì các module sẽ nằm trong một ứng dụng nào đó hoặc không.

Một ứng dụng thường là một thư mục chứa các file và thư mục con trong đó, các thư mục con này có thể chứa file code, file tài nguyên, hoặc chứa các module khác. Các module biết chúng được đặt ở đâu và khi cần thì có thể gọi đến nhau một cách dễ dàng.

Ví dụ dưới đây là thư mục của một gói rất phổ biến trong Node là gói Express, gói này chứa các file và thư mục con bên trong nó theo cấu trúc cây.

express/
    bin/
    History.md
    index.js
    lib/
        express.js
        http.js
        https.js
        request.js
        response.js
        /router
            collection.js
            index.js
            methods.js
            route.js
        utils.js
        view/
            partial.js
            view.js
        view.js
    LICENSE
    Makefile
    node_modules\
        qs\
    package.json
    Readme.md

Trong gói Express thì module thường được sử dụng nhiều nhất là utils.js. Tùy vào vị trí được đặt ở đâu mà các module khác có câu lệnh gọi khác nhau, ví dụ:

var utils = requrire('./lib/utils');
var utils = requrire('./utils');
var utils = requrire('../utils');

Cách tìm module của Node

Ngoài thư mục của ứng dụng thì các module thường còn được đặt trong thư mục node_modules, khi gọi thì chúng ta chỉ cần dùng tên module là đủ (tức là đường dẫn kiểu top-level), ví dụ:

var express = require('express');

Node sẽ tự động tìm module trong các thư mục node_modules, thư mục này có rất nhiều, Node sẽ tìm thư mục node_modules trong thư mục của file gọi đến module đó, nếu không thấy thì tiếp tục tiềm trong thư mục cha của thư mục chứa file gọi đến module đó và nếu không tìm thấy nữa thì Node sẽ tiếp tục đi dần dần lên các thư mục cha tiếp theo cho đến chạm đến thư mục gốc thì thôi.

Trong ví dụ trước, gói Express có một thư mục tên là node_modules, trong thư mục này chứa một thư mục khác có tên là qs, tức là module qs và module này có thể được gọi tới bởi bất kì file nào trong thư mục express.

var qs = require('qs');

Như vậy tóm lại cách dễ nhất để gọi các module là tạo thư mục node_modules và đặt chúng ta trong thư mục này, thư mục node_modules nên được đặt ở vị trí cao nhất để các module khác có thể thấy được. Ví dụ:

projects/
    drawapp/
        index.js
        lib/
            draw.js
            node_modules/
            svg.js
        node_modules/
            express/
            bin/
            History.md
            index.js
            lib/
            LICENSE
            Makefile
            node_modules/
                qs/
            package.json
            Readme.md
        package.json
    node_modules/

 

Trong ví dụ trên, những module nằm trong thư mục drawapp có thể thấy được các module nằm trong thư mục projects/node_modules, và ngược lại các module trong thư mục node_modules này không thể thấy được module qsqs nằm ở trong cùng trong khi Node chỉ tìm các module bằng cách đi ngược về các thư mục cha.

Tương tự, những module nằm trong thư mục lib/node_modules có thể thấy được từ draw.jssvg.js nhưng không thấy được từ index.js.

Lý do tại sao lại có thể có nhiều thư mục node_modules là vì làm như thế thì Node cho phép chúng ta có thể dùng một module với nhiều phiên bản khác nhau thay vì chỉ dùng một thư mục duy nhất rồi chép tất cả vào gây lộn xộn.

Tìm trong biến NODE_PATH

Ngoài các thư mục node_modules thì Node còn cho phép chúng ta đặt module trong các thư mục được định nghĩa trong biến môi trường NODE_PATH, biến này lưu danh sách các đường dẫn thư mục mà Node có thể tìm trong đó, mỗi thư mục cách nhau bằng dấu chấm phẩy (trong Windows) hoặc dấu 2 chấm (trong Linux).