Sử dụng ES Modules trên trình duyệt

Posted on February 25th, 2019

Hiện tại, ES Modules đã được support bởi khá nhiều các trình duyệt. Điều đó nghĩa là bạn có thể sử dụng ES Modules trực tiếp trên trình duyệt mà không cần phải cài đặt Node.js cùng với các công cụ như Browserify, RequireJS, Webpack,...

Sau đây, mình sẽ tìm hiểu về ES Modules và cách sử dụng nó trực tiếp trên trình duyệt. Mời bạn theo dõi bài viết!

Cơ bản về ES Modules

ES Modules (hay còn gọi là "JavaScript Modules", "JS Modules" hay "ECMAScript modules") là một tính năng mới của trình duyệt cho phép bạn làm việc với modules. Nhờ vậy, bạn có thể chia nhỏ chương trình ra thành các modules, với mỗi module có một chức năng riêng biệt.

Trong một module, bạn có thể sử dụng từ khoá export để export bất kỳ kiểu dữ liệu nào như: biến số với var, let hay const, class, function,... Sau đó, bạn sử dụng từ khoá import để sử dụng chúng ở một file khác.

Sử dụng ES Modules có một số lợi ích như:

  • Giúp module hoá chương trình. Qua đó, việc xây dựng, kiểm thử và bảo trì code sẽ tốt hơn.
  • Hỗ trợ dynamic import() giúp download modules khi cần thiết. Nhờ vậy, thời gian load trang sẽ giảm xuống.

Nói vậy thì cách sử dụng ES Modules có gì khác với JavaScript thông thường?

ES Modules với JavaScript thông thường

Bởi vì, ES Modules là một tính năng mới (từ ES6) dành cho các trình duyệt hiện đại. Do đó, nó luôn luôn được sử dụng ở chế độ Strict mode.

Tiếp theo, comment code theo kiểu HTML không được support ở modules, mặc dù nó vẫn hợp lệ ở JS thông thường. Ví dụ:

/*
* Sử dụng comment kiểu HTML trong JavaScript thông thường,
* không sai, nhưng không nên dùng.
*/
const x = 42; <!-- TODO: Rename x to y.

// Cách sử dụng comment chuẩn
const x = 42; // TODO: Rename x to y.

Modules có phạm vi "lexical top-level". Nghĩa là khi bạn chạy var foo = 42; trong modules, JS sẽ không tạo ra một biến ở global với tên foo. Hay nói cách khác là window.foo sẽ trả về undefined.

Cuối cùng, từ khoá exportimport chỉ sử dụng được ở modules mà không gọi được ở JavaScript thông thường.

Với những sự khác nhau trên, rõ ràng trình duyệt cần phải phân biệt được: đâu là JavaScript cho ES Modules và đâu là JavaScript thông thường để có thể xử lý cho đúng.

Vì vậy, phần dưới đây sẽ trình bày cách sử dụng ES Modules trên trình duyệt.

Cách sử dụng ES Modules trên trình duyệt

Để khai báo một script là ES Modules, bạn phải thêm attribute cho nó là: type="module".

<script type="module" src="main.js"></script>

Nghĩa là với những trình duyệt hiện đại, nó sẽ hiểu được thuộc tính này. Và trình duyệt sẽ xử lý file main.js theo kiểu của module.

Nói vậy, đối với những trình duyệt cũ, không support ES modules thì sao?

Để chương trình có thể chạy được ở cả trình duyệt cũ và mới thì mình phải định nghĩa thêm như này:

<script type="module" src="main.js"></script>
<script nomodule src="fallback.js"></script>

Khi đó, đối với trình duyệt mới (support modules): nó sẽ hiểu thuộc tính type="module" và bỏ qua script với thuộc tính nomodule. Hay nói cách khác, chương trình sẽ nhận main.js và bỏ qua fallback.js.

Với trình duyệt cũ (không support modules): nó không hiểu thuộc tính type="module" mà chỉ hiểu thuộc tính type="text/javascript" hoặc trường hợp không khai báo thuộc tính type thì mặc định vẫn là type="text/javascript". Do đó, chương trình sẽ bỏ qua main.js và chỉ nhận fallback.js.

Chú ý: 2 file main.jsfallback.js có ý nghĩa tương đương nhau, chỉ khác nhau về cú pháp, cách viết.

Ví dụ đơn giản về chương trình sử dụng ES Modules

Mình sẽ demo một chương trình cực kỳ đơn giản sử dụng Modules với các file là: index.html, main.jslib.js.

index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <script type="module" src="./main.js"></script>
  </body>
</html>

main.js:

import { repeat, shout } from "./lib.js";

console.log("main.js");
console.log(repeat("hello"));
console.log(shout("Modules in action"));

lib.js:

export const repeat = string => `${string} ${string}`;

export function shout(string) {
  return `${string.toUpperCase()}!`;
}

Trong file, index.html mình khai báo script với type="module" và nội dung file là main.js. Tức file main.js đóng vai trò là ES Modules. Trong file này, mình có sử dụng từ khoá import để import nội dung từ một file modules khác lib.js. Dĩ nhiên, file lib.js cũng là một ES Modules. Trong file này, mình sử dụng từ khoá export để export ra 2 hàm cho main.js sử dụng.

Kết quả hiện thị ra console là:

/*
 * => main.js
 * => hello hello
 * => MODULES IN ACTION
 */

Cú pháp cơ bản của ES Modules

Khi sử dụng modules, mình phải quan tâm đến cả phần export (tạo modules) và import (sử dụng modules). Có 2 kiểu export modules:

  • named (theo tên): dùng để export nhiều giá trị trong một modules. Và khi import, bắt buộc phải sử dụng đúng tên đã export.
  • default (mặc định): mỗi module chỉ cho phép export default một giá trị.

Export, import theo tên

Export theo tên

Bạn có thể export bất kỳ kiểu dữ liệu nào với từ khoá export đặt trước khai báo biến:

export let x = 1;
export var y = "a";
export const z = { x: 1, y: 2 };
export function add(a, b) {
  return a + b;
}
export class Utils {
  print(text) {
    console.log(text);
  }
}

Hoặc gộp chúng vào thành 1 object với một từ khoá export:

let x = 1;
var y = "a";
const z = { x: 1, y: 2 };
function add(a, b) {
  return a + b;
}
class Utils {
  print(text) {
    console.log(text);
  }
}

export { x, y, z, add, Utils };

Hoặc cũng có thể đổi tên chúng khi export:

export { x as publicX, y as publicY, z as publicZ, add as sum, Utils as Tools };

Import theo tên

Mình phải import theo đúng tên đã export:

import { x, y, z, add, Utils } from "./lib.js";

console.log(x); // 1
console.log(y); // a
console.log(z); // {x: 1, y: 2}
console.log(add(1, 2)); // 3
console.log(new Utils().print("hi")); // hi

Bên trên, mình import hết tất cả các giá trị. Tuy nhiên, bạn có thể chỉ import những thứ cần để sử dụng:

import { x, y, z } from "./lib.js";

console.log(x); // 1
console.log(y); // a
console.log(z); // {x: 1, y: 2}

Ngoải ra, để tránh trường hợp trùng tên với biến khác, bạn có thể import và đồng thời đặt tên mới cho biến:

import { x as newX, y as newY, z as newZ } from "./lib.js";

console.log(newX); // 1
console.log(newY); // a
console.log(newZ); // {x: 1, y: 2}

Hoặc import tất cả các giá trị ứng với một object với tên mới:

import * as myModule from "./lib.js";

console.log(myModule.x); // 1
console.log(myModule.y); // a
console.log(myModule.z); // {x: 1, y: 2}
console.log(myModule.add(1, 2)); // 3
console.log(new myModule.Utils().print("hi")); // hi

Chú ý: Trong trường hợp này, module export và import luôn luôn là một object.

Export, import default

Khác với trường hợp trên, trường hợp này luôn export và import trực tiếp với bất kỳ kiểu giá trị nào. Nghĩa là bạn export function thì khi import cũng là function,...

Vì khi export default, mỗi file sẽ chỉ cho phép export default 1 giá trị, nên mình sẽ lấy ví dụ đồng thời cho cả export và import luôn.

Export, import default biến

// export ở file lib.js
let x = 1;
export default x;

// import ở file main.js
import variable from "./lib.js";
console.log(variable); // 1

Khi bạn import, giá trị của variable sẽ tương ứng với giá trị export default. Do đó, variable tương ứng với x, nên có giá trị là 1.

Export, import default function

// export ở file lib.js
export default function(a, b) {
  return a + b;
}

// hoặc
function add(a, b) {
  return a + b;
}
export default add;

// import ở file main.js
import func from './lib.js';
console.log(func(1, 2));         // 3

Export, import default class

// export ở file lib.js
export default class Utils {
  print(text) {
    console.log(text);
  }
}

// hoặc
class Utils {
  print(text) {
    console.log(text);
  }
}
export default Utils;

// import ở file main.js
import Tools from './lib.js';
console.log(new Tools().print("hi")); // hi

Kết hợp export, import theo tên và default

Bên trên, mình ví dụ tách biệt 2 kiểu export, import. Nhưng thực tế, bạn có thể sử dụng kết hợp chúng:

export let x = 1;
export var y = "a";
export const z = { x: 1, y: 2 };
export function add(a, b) {
  return a + b;
}
export default class Utils {
  print(text) {
    console.log(text);
  }
}

Bên trên, mình chỉ export default Utils, còn lại là export thông thường. Khi đó, mình sẽ import như sau:

import Tools, { x, y, z, add } from "./lib.js";

console.log(x); // 1
console.log(y); // a
console.log(z); // {x: 1, y: 2}
console.log(add(1, 2)); // 3
console.log(new Tools().print("hi")); // hi

Dynamic import

Thực tế, cách import bên trên là static. Nghĩa là đoạn code liên quan đến module luôn luôn load ngay từ thời điểm đầu. Tuy nhiên, bạn cũng có thể import theo kiểu dynamic. Tức là bạn sẽ chỉ load script khi nào cần thiết.

Ví dụ export:

export let x = 1;
export var y = "a";
export const z = { x: 1, y: 2 };
export function add(a, b) {
  return a + b;
}
export class Utils {
  print(text) {
    console.log(text);
  }
}

Khi đó, cách dynamic import là:

setTimeout(async () => {
  let { x, y, z, add, Utils } = await import("./lib.js");

  console.log(x); // 1
  console.log(y); // a
  console.log(z); // {x: 1, y: 2}
  console.log(add(1, 2)); // 3
  console.log(new Utils().print("hi")); // hi
}, 2000);

Chú ý: hàm setTimeout trên chỉ là ví dụ để thể hiện việc delay khi load module.

Với cách dynamic import này, import sẽ trả về một Promise. Do đó, bạn có thể sử dụng kết hợp với async/await như trên.

Lời kết

Như vậy là mình đã trình bày xong những kiến thức cơ bản về ES Modules, cũng như cách sử dụng nó ngay trên trình duyệt.

Theo bạn, việc sử dụng ES Modules trực tiếp trên trình duyệt có thực sự cần thiết? Để lại quan điểm của bạn xuống phần bình luận nhé!

Xin chào và hẹn gặp lại, thân ái!

Tham khảo:


★ Nếu bạn thấy bài viết này hay thì hãy theo dõi mình trên Facebook để nhận được thông báo khi có bài viết mới nhất nhé: