openplanning

Lịch sử phát triển của module trong JavaScript

  1. Module?
  2. IIFE
  3. CommonJS
  4. AMD
  5. UMD
  6. ES Modules

1. Module?

Bài viết này giới thiệu với các bạn về lịch sử module trong JavaScript. Rất nhiều đặc tả kỹ thuật và các triển khai module đã được tạo ra cho tới khi JavaScript ES6 (2105) đưa ra một đặc tả module mới được hỗ trợ ở mức độ ngôn ngữ. Vì vậy các đặc tả kỹ thuật cũ đang dần trở thành lỗi thời, mặc dù một vài trong số chúng vẫn còn được sử dụng ở đâu đó, chẳng hạn NodeJS, nhưng chúng chắc chắn sẽ bị loại bỏ trong tương lai gần.
Mục đích của bài viết này là cung cấp một cái nhìn tổng quan và loại bỏ sự bối rối với những người mới bắt đầu với JavaScript Module, tránh việc mất thời gian để học các thư viện đã lỗi thời.
Các chương trình JavaScript bắt đầu khá nhỏ - hầu hết việc sử dụng nó trong những ngày đầu là để thực hiện các tác vụ kịch bản riêng biệt, cung cấp một chút tương tác cho các website của bạn khi cần thiết, vì vậy các tập lệnh lớn nói chung là không cần thiết. Tua đi vài năm và giờ đây chúng ta có các ứng dụng hoàn chỉnh đang được chạy trong các trình duyệt có nhiều JavaScript, cũng như JavaScript đang được sử dụng trong nhiều ngữ cảnh khác nhau, chẳng hạn NodeJs.
Khi JavaScript bắt đầu được sử dụng nhiều hơn trong các ứng dụng, việc quản lý và bảo trì mã trở lên khó khăn hơn. Ngoài ra, việc sử dụng nhiều file JavaScript trong cùng một chương trình sẽ xẩy ra vấn đề xung đột tên biến hoặc tên hàm, và tạo ra các rủi ro lớn. Hãy xem ví dụ đơn giản sau:
file_1a.js
var friendName = "Tom";
function sayHello() {
    console.log(`Hello ${friendName}!`);
}
file_2a.js
sayHello(); // Hello Tom!
friendName = 'Jerry';
sayHello(); // Hello Jerry!
test_a.html
<html>
  <head>
     <script src="file_1a.js"></script>
     <script src="file_2a.js"></script>
     <script>
        var friendName = 'Donald';
        sayHello(); // Hello Donald!
     </script>
  </head>
  <body>
      View the result in the Browser Console Window.
  </body>
</html>
Output:
Hello Tom!
Hello Jerry!
Hello Donald!
Như vậy khái niệm module xuất hiện để giải quyết hai vấn đề quan trọng:
  • Module phải là một không gian đóng kín, tất cả những gì được chia sẻ ra bên ngoài phải bắt nguồn từ một chủ đích rõ ràng.
  • Module phải là cách để chia một ứng dụng lớn thành nhiều phần nhỏ riêng biệt, giúp các lập trình viên dễ dàng hơn cho việc phát triển và bảo trì ứng dụng.

2. IIFE

IIFE (Immediately Invoked Function Expression) (Biểu thức hàm, được thực thi tức thì): Đây là một kỹ thuật sơ khai nhất để module hoá code. Tại thời điểm này, khái niệm lớp chưa tồn tại trong JavaScript. Việc sử dụng IIFE giúp chúng ta thoải mái tạo ra các biến hoặc các hàm trong một không gian đóng kín, để sử dụng một cách nội bộ và không sợ xung đột với bất kỳ nơi nào khác của ứng dụng.
Biểu thức IIFE có cấu trúc giống như thế này:
(function () {
  // Statements
})();
Nhìn có vẻ hack não bởi rất nhiều dấu ngoặc tham gia vào biểu thức, nhưng nếu viết dạng tương đương dưới đây chắc chắn sẽ dễ hiểu hơn:
var myFunc = function()  {
   // Statements
}  
myFunc();
IIFE giúp ẩn các code được xử lý bên trong và chỉ phơi bầy ra những gì mà chúng ta muốn. Ví dụ:
var text = (function () {
    var privateText = "My private TEXT";
    var publicText = "My public TEXT";
    return publicText;
})();

console.log(text); // My public TEXT
Và chúng ta vẫn có thể truy cập vào các biến toàn cục từ bên trong IIFE một cách bình thường:
var myGlobalVariable = 'Hello, I am a global variable :)';

var text = (function () {
    var privateText = "My private TEXT";
    var publicText = "My public TEXT";
    console.log(myGlobalVariable);
    return publicText;
})();

console.log(text); // My public TEXT
Tiếp tục với ý tưởng ở trên, một ví dụ IIFE trả về một đối tượng có cấu trúc như một interface:
file_1b.js
var mySimpleModule = (function() {
   var greeting = 'Hello'; // Private variable!
   var friendName = "Tom";
   var sayHello = function()  {
       console.log(`${greeting} ${friendName}!`);
   }
   var mm = {
       friendName: friendName,
       sayHello: sayHello
   };
   return mm;
})();
file_2b.js
// Call sayHello function of 'mySimpleModule':
mySimpleModule.sayHello(); // Hello Tom!

var friendName = 'Donald';
mySimpleModule.friendName = 'Jerry';
mySimpleModule.sayHello(); // Hello Jerry!

3. CommonJS

CommonJS là một đặc tả được kỹ sư Kevin Dangoor của Mozilla bắt đầu vào tháng 1 năm 2009. Mục đích chính của nó thiết lập các quy ước về hệ sinh thái module cho các môi trường JavaScript khác với môi trường được cung cấp bởi trình duyệt. Bây giờ chúng ta đang đứng ở giai đoạn giữa, giống như hình minh hoạ dưới đây, đúng không?
Thực ra, ban đầu dự án có tên là ServerJS, hướng tới hệ sinh thái JavaScript tại máy chủ. Tháng 8 năm 2009 nó được đổi tên thành CommonJS như một sự thể hiện rằng phạm vi ứng dụng của nó là rộng rãi, bao gồm cả trình duyệt.
Ngay từ khi ra đời, các đặc điểm kỹ thuật module của CommonJS đã được sử dụng cho NodeJS. Và cho tới thời điểm bài viết này được công bố (2021), CommonJS vẫn được sử dụng trong NodeJS. Tuy nhiên, chắc chắn rằng nó sẽ được thay thế bởi ES6 Module trong tương lai gần. Đặc điểm nhận dạng của CommonJS là các hàm require()module.exports().
Các ưu điểm của CommonJS:
  • Đơn giản và dễ dàng sử dụng mà không cần xem tài liệu.
  • Tích hợp sẵn trình quản lý các phụ thuộc (dependency manager). Các module được tải theo một thứ tự thích hợp.
  • Hàm require() có thể được sử dụng ở bất cứ đâu để yêu cầu một module khác.
  • Hỗ trợ sự phụ thuộc vòng tròn (circular dependency).
Các nhược điểm của CommonJS:
  • Tải module một cách đồng bộ, khiến tốc độ tải chậm, không phù hợp với một số mục đích sử dụng nhất định, chẳng hạn các ứng dụng phía trình duyệt.
  • Mỗi file cho mỗi module.
  • Các trình duyệt không hiểu các module theo tiêu chuẩn CommonJS, nó cần một thư viện Module Loader bổ xung hoặc ít nhất nó phải được chuyển đổi mã nguồn bởi một transpiler.
  • Không hỗ trợ các hàm constructor (Theo đặc tả). Tuy nhiên NodeJS hỗ trợ điều này.
  • Khó phân tích đối với các bộ phân tích mã tĩnh.
Các thư viện JavaScript triển khai đặc tả CommonJS:
  • NodeJS (Server side)
  • webpack (Client side)
  • Browserify (Client side)
Ví dụ: Cách khai báo module trong CommonJS:
cjs_file_1.js
function _add(a, b) {
    return a + b;
}
function _subtract(a, b) {
    return a - b;
}
module.exports = {
    add: _add,
    subtract: _subtract,
};
Cách sử dụng:
cjs_file_2.js
// Using require() function to load file "csj_file_1.js", no need .js extension.
var calculator = require("./csj_file_1");
console.log(calculator.add(2, 2)); // 4
console.log(calculator.subtract(2, 2)); // 0
Xem thêm bài viết chuyên sâu hơn về CommonJS:
  • JavaScript CommonJS

4. AMD

AMD (Asynchronous Module Definition) (Định nghĩa module không đồng bộ) được sinh ra từ một nhóm các nhà phát triển không hài lòng với định hướng được CommonJS áp dụng. Trên thực tế, AMD đã tách khỏi CommonJS từ rất sớm trong quá trình phát triển. Sự khác biệt chính giữa AMDCommonJS nằm ở việc nó hỗ trợ tải không đồng bộ module. Giống với CommonJS, AMD chỉ là một đặc tả kỹ thuật.
Với tính năng tải không đồng bộ, các thư viện không phụ thuộc vào nhau để tải có thể được tải cùng một lúc. Điều này đặc biệt quan trọng đối với các trình duyệt, nơi mà thời gian khởi động là điều cần thiết để có trải nghiệm người dùng tốt.
Các ưu điểm của AMD:
  • Tải không đồng bộ (Asynchronous loading), thời gian khởi động tốt hơn.
  • Hỗ trợ và tương thích với các hàm require()module.exports(). Tương tự như CommonJS.
  • Tích hợp sẵn trình quản lý các phụ thuộc (dependency manager).
  • Một module có thể bao gồm nhiều file.
  • Hỗ trợ hàm constructor.
  • Hỗ trợ sự phụ thuộc vòng tròn (circular dependency).
  • Hỗ trợ plugin (các bước tải tùy chỉnh).
Các nhược điểm của AMD:
  • Về mặt cú pháp, phức tạp hơn một chút so với CommonJS.
  • Khó phân tích đối với các bộ phân tích mã tĩnh.
  • Các trình duyệt không hiểu các module theo tiêu chuẩn AMD, nó cần một thư viện Module Loader bổ xung hoặc ít nhất nó phải được chuyển đổi mã nguồn bởi một transpiler.
Ví dụ: Tạo một module AMD:
amd_file_1.js
define("myAmdModule", {
    add: function (a, b) {
        return a + b;
    },
    subtract: function (a, b) {
        return a - b;
    },
});
amd_file_2.js
require(["myAmdModule"], function (myAmdModule) {
    console.log(myAmdModule.add(2, 2)); // 4
    console.log(myAmdModule.subtract(2, 2)); // 0
});
Xem thêm bài viết chuyên sâu hơn về AMD:
  • JavaScript AMD

5. UMD

UMD (Universal Module Definition) (Định nghĩa module phổ quát) là một tập hợp các kỹ thuật được sử dụng để tạo các module có thể được nhập khẩu dưới dạng module IIFE, CommonJS hoặc AMD. Do đó, bây giờ một chương trình có thể nhập khẩu các module của bên thứ ba, bất kể nó đang sử dụng đặc tả module nào.
Ví dụ:
UMD_file_1.js
(function (myUmdModule) {
    if (typeof define === 'function' && define.amd) {
        // Export with AMD
        define("myUmdModule", myUmdModule);
    } else if (typeof module === 'object' && module.exports) {
        // Export with CommonJS
        module.exports = myUmdModule;
    } else {
        // Export with browser global
        window.myUmdModule = myUmdModule;
    }
}({
    add: function (a, b) {
        return a + b;
    },
    subtract: function (a, b) {
        return a - b;
    },
}));
UMD_file_2.js
// Load with AMD
require(["myUmdModule"], function (myUmdModule) {
    console.log(myUmdModule.add(2, 2));
    console.log(myUmdModule.subtract(2, 2));
});
// Load with CommonJS
var myUmdModule = require("./myUmdModule");
console.log(myUmdModule.add(2, 2));
console.log(myUmdModule.subtract(2, 2));

6. ES Modules

ES Modules hay còn gọi là ECMAScript Modules hoặc ES6 Modules hoặc ES2015 Modules. ES Modules chính là giải pháp cho một tương lai tươi sáng của hệ sinh thái module bằng việc chuẩn hóa cú pháp khai báo module, hoạt động trên cả BackendFrontend, và được hỗ trợ bởi JavaScript ở mức độ ngôn ngữ. Tuy nhiên, không phải tất cả người dùng đã chuyển sang sử dụng các trình duyệt hiện đại hỗ trợ ES6, vì vậy nó cần thêm thời gian.

Các hướng dẫn ECMAScript, Javascript

Show More