Tìm hiểu về module system, CommonJS và require

Trước khi chuẩn ES2015 ra đời, Javascript không hỗ trợ cho các lập trình viên bất kỳ một phương thức tự nhiên nào để tổ chức hệ thống code. Node.js được tạo nên từ Javascript, tuy nhiên những người viết ra Node.js đã đưa thêm vào CommonJs để giải quyết vấn đề về "cấu trúc" hệ thống code viết bởi Javascript. Trong bài viết này chúng ta sẽ tìm hiểu về Node.js module system, cách thức nó hoạt động và cách chúng ta dùng nó để xây dựng nên cấu trúc cho bộ code của mình.

Module system là gì

Modules là các khối cơ bản xây dựng lên cấu trúc của mã code. Module system cho phép bạn tổ chức lại mã code, ẩn đi những thông tin không cần thiết hay chỉ xuất ra những public interface cần thiết trong các module khác thông qua module.exports. Thực hiện lệnh require thực tế là đang load một module khác vào trong mã hiện tại. Dưới đây là một ví dụ cơ bản về một module

// file add.js
function add (a, b) {
  return a + b;
}

module.exports = add;

để sử dụng được module math.js bên trên, chúng ta cần phải load nó vào

// index.js
const add = require('./math');
const total = add(4, 5);
console.log(total);

trong thực tế, khi node.js thực hiện require module add.js, nó sẽ viết lại module đó như sau:

(function (exports, require, module, __filename, __dirname) {
  function add (a, b) {
    return a + b
  }

  module.exports = add
})

bản chất thì các biến mà chúng ta access như exports, require hay module không phải là các biên global, cách mà Node.js viết lại mỗi module như trên cho phép chúng ta sử dụng các biến đó như là các biến global. Lý do của việc viết lại này là nhằm hạn chế việc sử dụng các biến exports, require và module như là các biến global; nó ép chúng ta phải sử dụng các biến này theo dạng scoped variable theo từng module để tránh vấn đề xung đột hoặc ghi đè khi chúng ta cần phải tạo và sử dụng nhiều module hơn.

Cách thức hoạt động của require

Cơ chế load module trong node.js (require) khá đơn giản, ngay tại lần đầu tiên một module được load, Node.js sẽ cache module đó lại, do đó mỗi khi chúng ta thực hiện lời gọi tiếp theo đối với cùng một module, chúng ta sẽ nhận được phiên bản cache của module đó. Điều này sẽ giúp cho việc require module luôn luôn trả về cùng một kết quả cho dù chúng ta có load nó bao nhiêu lần hay bất cứ đâu. Ngoài ra Node.js sẽ phát huy tác dụng tốt nhất nếu phần lớn mã code được viết theo kiểu bất đồng bộ; tuy nhiên require lại là một tiến trình đồng bộ. Do đó nếu không cache lại mỗi module thì việc require sẽ gây tốn thêm nhiều thời gian, ảnh hưởng trực tiếp đến hiệu suất của ứng dụng.

Chúng ta có thể tải một module theo đường dẫn trực tiếp, gián tiếp hoặc từ thư mục node_modules. Nếu module được tải không nằm trong thư mục hiện tại, không phải là đường dẫn trực tiếp thì Node.js sẽ tìm kiếm module đó tại thư mục node_modules; Node.js sẽ duyệt lần lượt từng thư mục trên file system của bạn để tìm module cần thiết. Đầu tiên nó sẽ tìm tại thư mục cha của file đang thực hiện require, sau đó nó sẽ di chuyển đến cha của cha, tiếp theo cho đến khi gặp root của file system. Nếu đến khi gặp root vẫn không tìm thấy module cần thiết, thông báo lỗi sẽ xuất hiện

Đứng sau require: module.js

Việc require được xử lý trong Node.js core thông qua một module khác gọi là module.js. Bạn có thể xem module đó tại đây: Node.js module.js

Có hai hàm quan trọng nhất trong file này đó là _load_compile. Chúng ta sẽ cùng tìm hiểu xem nó là cái gì

The module dealing with module loading in the Node core is called module.js, and can be found in lib/module.js in the Node.js repository.

Module._load

Hàm này trước tiên sẽ kiểm tra xem file/module cần load đã được cache hay chưa, nếu nó đã được cache, module.js sẽ trả về đối tượng exports Nếu module được gọi là native module, nó sẽ thực hiện gọi NativeModule.require() và trả về kết quả của lời gọi này. Nếu module đươc gọi không phải là native module, nó sẽ tạo một module mới cho file được load rồi lưu vào cache. Tiếp theo nó sẽ load nội dung của file trước khi trả về đối tượng exports

// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call `NativeModule.require()` with the filename and return the result.
// 3. Otherwise, create a new module for the file and save it to the cache.
//    Then have it load  the file contents before returning its exports object.

Module._compile

Hàm compile sẽ thực thi nội dung của file trong scope đúng của file đó hoặc trong một sandbox, đồng thời sẽ exposes những biến helper như exports, require và module cho file được gọi. Cách thức hoạt động của require:

Cách tổ chức mã code

Trong mỗi ứng dụng, chúng ta cần tìm ra một phương pháp tốt để đảm bảo tính kết nối và linh hoạt giữa các module được sử dụng trong mã code. Một module phải được focus vào cho một phần nhỏ hoặc một tính năng cụ thể. Mỗi module không nên là các module global hoặc có thể share các tài nguyên giữa các module với nhau. Tốt nhất là các module chỉ nên được giao tiếp với nhau thông qua các lời gọi hàm và có thể cập nhật, thay thế đơn giản bằng cách chỉ sửa trong bản thân mỗi module mà không cần phải mở rộng codebase của bạn

Có gì trong node_modules

Thư mục node_modules là nơi mà Node.js lưu trữ và tìm kiếm các modules. Các module được cài vào thông qua npm một global module được đính kèm cùng Node.js ngay khi cài đặt. Hiện tại thì npm v2 và npm v3 là hai phiên bản được sử dụng phổ biến nhất với Node.js. Hai phiên bản này tuy về cách sử dụng là giống nhau nhưng lại khác nhau khá nhiều về cơ chế lưu trữ các modules. Trong đó:

npm v2

npm v2 cài đặt tất cả dependencies theo style nested. Nghĩa là khi một module cần sử dụng các module khác thì các module đó sẽ nằm trong thư mục node_modules của thư module hiện tại. Vấn đề của npm v2 đó là mỗi module trong node.js thường không đứng một mình mà sẽ sử dụng lại tính năng của một hoặc nhiều module khác, do đó cơ chế nested của npm v2 làm cho cây thư mục node_modules trở nên rất phức tạp và sâu, đôi khi còn gây lãng phí tài nguyên vì nhiều module cùng dùng một module khác nhưng chúng lại không thể share với nhau. Nếu bạn từng code Node.js trên win thì có thể bạn gặp phải tình huống không thể xóa toàn bộ thư mục node_modules vì lý do ngớ ngẩn: 'File path too long'

npm v3

npm v3 thay vì lưu trữ nested, nó cố gắng làm phẳng các dependencies thứ cấp bằng cách cài đặt tất cả các dependencies vào node_modules của project root folder. Điều này cũng có nghĩa là bạn không thể chỉ nhìn vào thư mục node_modules mà có thể xác định được module nào là module trực tiếp mà project bạn cần hay nodule nào là module dependencies của các module khác. Vấn đề sảy ra đối với npm v3 đó là khi app của bạn lớn dần lên, thư mục node_modules tại root cũng vì thế mà nhiều lên, sẽ rất khó nếu bạn đang viết một modules và muốn debug nó hay sẽ rất mỏi mắt khi bạn muốn custom một module có sẵn. Tuy nhiên bạn cũng có thể giải quyết vấn đề này bằng cách cài thủ công dependencies của các module khác bằng các di chuyển đến từng module và chạy nm install

Thực tế thì việc làm phẳng modules trong npm v3 không đơn giản dừng lại ở việc ném tất cả modules cần thiết vào node_modules root. Có rất nhiều package khác nhau sử dụng chung một module khác, chỉ có điều là khác phiên bản. Npm v3 giải quyết vấn đề này bằng cách sử dụng nested module cho các module yêu cầu một module khác phiên bản với một module đã được cài. Các bạn có thể tìm hiểu thêm về npm v3 và những khác biệt với npm v2 tại đây: https://docs.npmjs.com/how-npm-works/npm3

Nguồn và tài nguyên tham khảo: https://docs.npmjs.com/how-npm-works/npm3 https://github.com/nodejs/node/blob/master/lib/module.js https://blog.risingstack.com/node-js-at-scale-module-system-commonjs-require/ https://nodejs.org/api/modules.html

All Rights Reserved