Cách viết JavaScript hiện đại: Phần 2: CommonJS module

Nguồn: 旧石器時代のJavaScriptを書いてる各位に告ぐ、現代的なJavaScript超入門 Section2 ~CommonJSモジュールと仲良くなろう~

Bài viết này là phần 2 của loạt bài dịch Cách viết JavaScript hiện đại.

Những người muốn theo dõi từ đầu có thể xem phần 1 ở đây:

Cách viết JavaScript hiện đại: Phần 1: Tổng hợp các điểm mới có thể thực hành ngay

Chuỗi bài viết này sử dụng cho các browser đáp ứng tiêu chuẩn ECMAScript5 nên không dùng được cho IE8 trở xuống.

Bài viết vẫn trung thành với quan điểm giải thích một cách dễ hiểu nhất.

Mục lục

Phần 1: Tổng hợp các điểm mới có thể thực hành ngay

Phần 2: Làm quen với CommonJS module

Phần 3: Master Browserify

Phần 4: Tự động hóa xử lý dùng Gulp

Phần 5: Nhớ cú pháp của ES2015

Lời mở đầu

Trong bài viết, cụm từ CommonJS module được dùng để chỉ module specs và interface (require/exports.○○/module.exports) dùng Node.js độc lập mở rộng CommonJS từ cơ sở là specs của Modules 1.0 của CommonJS.

Nó không có nghĩa là CommonJS của dự án về đặc điểm kỹ thuật tiêu chuẩn phát triển hay specs của CommonJS Modules 1.0.

Khi các bạn nhìn vào code của rất nhiều website, bạn có ngạc nhiên khi thấy các đoạn code sau không?

require…
var hoge = require("hoge");

var piyo = hoge.fuga();
// ...(Xử lý bất kỳ)

module.exports…
function Hoge(){
  // ...(Xử lý bất kỳ)
}

module.exports = Hoge;

Có thể các bạn sẽ ngay lập tức phản ứng như sau:

"Theo tôi biết thì JavaScript không có requiremodule mà?"

"Tôi đã thử copy paste và quả nhiên là không hoạt động trên trình duyệt."

Xin hãy yên tâm, đoạn code này có thể chạy trên trình duyệt. Tuy nhiên, để chạy được thì bạn cần biết vài điều.

Phần trình bày dưới đây sẽ giải thích cụ thể về khái niệm, nguồn gốc và cách để đưa các module này vào trình duyệt.

1. Việc phân chia thành các module

Khi một script lớn đến một mức nào đó, chúng ta có thể tổng hợp các chức năng hay dùng thành các module.

Đến nay, chúng ta vẫn thường thực hiện việc này trên trình duyệt bằng cách tạo file .js, thông qua HTML có thể load nhiều lần.

Ví dụ, khi mọi người dùng jQuery (không dùng CDN), chắc chắn mọi người đã gọi jQuery file bằng script tag.

Ví dụ sử dụng jQuery module

<!DOCTYPE html>
<html lang="ja">
<head>
    <!-- Tóm lược -->
    <script src="./jquery.min.js"></script>
    <script>
        // Có thể sử dụng jQuery vì nó được gọi ra với tư cách là một module bên ngoài.
        console.log(typeof(window.jQuery)); // "function"

        jQuery(function($){
            // Viết xử lý ở đây
        });
    </script>
</head>
<body>
    <!-- Tóm lược -->
</body>
</html>

Tuy nhiên, trong việc dùng module này có một số lỗ hổng chết người. Trong đó, vấn đề lớn nhất là giới hạn của namespace. Chẳng hạn tôi tạo ra 1 module không hề liên quan đến jQuery nhưng lại đặt tên là jQuery.

myjquery.js
!function(){
    window.jQuery = jQuery;
    window.$ = jQuery;

    // Module để chuyển 1 string tiếng Nhật sang query string
    function jQuery(obj){
        var keys = Object.keys(obj);
        if(!keys.length) return "";

        return "?" + keys.map(function(key){
            return encodeURIComponent(key) + "=" + encodeURIComponent(obj[key]);
        }).join("&");
    }
}();

/*
var text = jQuery({
    name: "がお",
    age: 18,
});

console.log(text); // ?name=%E3%81%8C%E3%81%8A&age=18
*/

Bây giờ cùng lúc gọi module này và jQuery mọi người vẫn thường dùng thì sẽ xảy ra vấn đề.

<!DOCTYPE html>
<html lang="ja">
<head>
    <!-- Tóm lược -->
    <script src="./jquery.min.js"></script>
    <script src="./myjquery.js"></script>
    <script>
        // Có thể sử dụng jQuery vì nó được gọi ra với tư cách là một module bên ngoài.
        console.log(typeof(window.jQuery)); // "function"

        // Tuy nhiên, jQuery này không phải là jQuery mà chúng ta vẫn biết.
        jQuery(function($){
            // Dù có viết xử lý ở đây thì nó cũng không được thực thi.
           // jQuery chúng ta tự viết được ưu tiên hơn.
        });
    </script>
</head>
<body>
    <!-- Tóm lược -->
</body>
</html>

Đến đây thì quả là một rắc rối lớn. Do đó, CommonJS module đã được tạo ra. (Hiện nay, CommonJS module không còn được tích cực sử dụng, ngược lại còn đang có xu hướng mất dần đi. Tuy nhiên, đây là câu chuyện của ES2015 module.)

Ở năm 2016 này, đáng tiếc là CommonJS module vẫn còn hữu dụng và vẫn được dùng thường xuyên nên vẫn cần phải biết về cách xử lý các module này.

2. CommonJS module là gì

Về vấn đề này còn rất nhiều tranh cãi, bài này sẽ chỉ ra những điểm chung nhất.

CommonJS module là các module thỏa mãn các điều kiện sau:

2.1. Khi tạo ra module thì dùng module.exports hoặc exports.○○

Thực hiện CommonJS của myjquery.js

/* bỏ cách gán trực tiếp cho window object
window.jQuery = jQuery;
window.$ = jQuery;
*/

// Thay vào đó dùng module.exports
module.exports = jQuery;

// Module để chuyển 1 string tiếng Nhật sang query string
function jQuery(obj){
    var keys = Object.keys(obj);
    if(!keys.length) return "";

    return "?" + keys.map(function(key){
        return encodeURIComponent(key) + "=" + encodeURIComponent(obj[key]);
    }).join("&");
}

2.2. Khi gọi CommonJS module, sử dụng hàm require

Gọi module dùng require

var jQuery = require("jQuery"); // jQuery mọi người thường dùng
var myJQuery = require("./myjquery.js"); // jQuery của bản thân

// Mỗi module được đưa vào 1 hàm
console.log(typeof(jQuery)); // "function"
console.log(typeof(myJQuery)); // "function"

// Theo nguyên tắc của CommonJS, không gán xuống window object nên không bị ảnh hưởng.
console.log(typeof(window.jQuery)); // "undefined"

// jQuery được dùng ở đây là jQuery chúng ta vẫn quen biết!
jQuery(function($){
    // Viết xử lý ở đây});

//Cùng lúc đó chúng ta có thể dùng jQuery của bản thân mình
var text = myJQuery({
    name: "がお",
    age: 18,
});

console.log(text); // ?name=%E3%81%8C%E3%81%8A&age=18

Ở đây tác giả đưa ra việc thực hiện CommonJS module jQuery mà mọi người thường dùng nhưng đừng để ý gì cả.(Module nổi tiếng như jQuery thì đã có các kỹ sư tài ba tạo ra).

Đến đây thì việc đổi một module đã có sẵn thành CommonJS module kết thúc. Tuy nhiên, nếu chỉ đổi thôi thì nó vẫn chưa hoạt động trên trình duyệt.

Đã có Node.js

"Nhưng tại sao lại nhắc đến Node.js khi đang nói về Web?"

Hãy yên tâm. Dù hơi mơ hồ nhưng thực sự có mối quan hệ ở đây.

Thực ra Node.js đã được biết đến trong 5 năm gần đây và dần trở nên phổ biến nên ở đây chỉ giải thích sơ lược.

Node.js là môi trường JavaScript hướng server side hoạt động trong V8 engine.

Node.js

Node.js tuân thủ ECMAScript nên đương nhiên không có DOM API và hoạt động trên console chứ không phải trên trình duyệt. Do đó, có thể nó ko quen thuộc với những người chuyên viết code hướng browser.

Điều tôi thực sự muốn nói ở đây không phải là về Node.js mà chỉ muốn nhấn vào 2 điểm:

Node.js đang ngày càng thịnh hành

Node.js có thể dùng CommonJS module

Tức là Node.js làm cho CommonJS module trở nên phổ biến.

3. Áp dụng CommonJS module vào Web

Đến đây những người tinh ý có thể đã nghĩ ra để dùng CommonJS module trên Web browser thì có thể sử dụng Node.js. Và quả đúng là như vậy.

Về lý thuyết cũng có cách không cần dùng đến Node.js nhưng không quen thuộc với các lập trình viên JS nên không đề cập ở đây.

Thực ra có một số module dùng để đổi sang Web browser nhưng nổi tiếng nhất là Browserify (sẽ giải thích thêm ở phần sau).

Việc sử dụng CommonJS module trên Web browser được tổng hợp trong sơ đồ dưới đây:

CommonJS module.png

Điểm cần chú ý ở đây là dù dùng cho Node.js hay Web browser thì đều dùng chung việc require. Chỉ có điều khi dùng trên Web browser thì có thêm bước xử lý chuyển đổi.

4. Browserify là gì?

Từ phần trên ta có thể hiểu đôi chút về Browserify rồi. Đó là công cụ chuyển đổi để code được viết bằng CommonJS module có thể chạy được trên trình duyệt. (Tuy nhiên, ở bài viết này sẽ không nói về cách dùng Node.js và Browserify để xử lý)

Ví dụ dưới đây minh hoạ cách tạo file input.jsrequire myjquery.js

input.js
var jQuery = require("./myjquery.js");

var text = jQuery({
    name: "がお",
    age: 18,
});

console.log(text); // ?name=%E3%81%8C%E3%81%8A&age=18

Sử dụng Browserify để chuyển đổi phần code này, ta sẽ có đoạn code sau:

output.js
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var jQuery = require("./myjquery.js");

var text = jQuery({
    name: "がお",
    age: 18,
});

console.log(text); // ?name=%E3%81%8C%E3%81%8A&age=18
},{"./myjquery.js":2}],2:[function(require,module,exports){
/* bỏ cách gán trực tiếp cho window object
window.jQuery = jQuery;
window.$ = jQuery;
*/

// Thay vào đó dùng module.exports
module.exports = jQuery;

// Module để chuyển 1 string tiếng Nhật sang query string
function jQuery(obj){
    var keys = Object.keys(obj);
    if(!keys.length) return "";

    return "?" + keys.map(function(key){
        return encodeURIComponent(key) + "=" + encodeURIComponent(obj[key]);
    }).join("&");
}

},{}]},{},[1]);

Đoạn code đã được chuyển sang dạng khá dễ hiểu. Đến đây, như đã nói ở phần 1, hãy thử nhấn F12, mở tool của developer, copy paste đoạn code của output.js vào console window và chạy thử. Đoạn code sẽ chạy không lỗi trên trình duyệt và string "?name=%E3%81%8C%E3%81%8A&age=18" sẽ được xuất ra ở console window.

Và như vậy, nếu dùng Browserify để xử lý chuyển đổi tự động CommonJS module thì nó có thể dùng được trên trình duyệt.

Phần 2 đến đây là kết thúc.

Mặc dù cách chuyển đổi chưa được giới thiệu nhưng qua phần này các bạn có thể hiểu được khái niệm cơ bản của cách dùng module tương đối hiện đại.

Trong phần 3 sẽ giới thiệu về cách dùng Node.jsBrowserify để chuyển đổi.