× Giới thiệu Lịch khai giảng Tin tức Sản phẩm học viên

Mô-đun trong Javascript giới thiệu toàn tập cho người mới

21/06/2022 13:40

Khi ứng dụng của chúng ta phát triển lớn hơn, chúng ta muốn chia nó thành nhiều tệp, được gọi là “mô-đun”. Một mô-đun có thể chứa một lớp hoặc một thư viện các chức năng cho một mục đích cụ thể. Trong một thời gian dài, JavaScript đã tồn tại mà không có cú pháp mô-đun cấp ngôn ngữ. Đó không phải là vấn đề, bởi vì ban đầu các tập lệnh nhỏ và đơn giản, vì vậy không cần thiết.

Khi ứng dụng của chúng ta phát triển lớn hơn, chúng ta muốn chia nó thành nhiều tệp, được gọi là “mô-đun”. Một mô-đun có thể chứa một lớp hoặc một thư viện các chức năng cho một mục đích cụ thể.

Trong một thời gian dài, JavaScript đã tồn tại mà không có cú pháp mô-đun cấp ngôn ngữ. Đó không phải là vấn đề, bởi vì ban đầu các tập lệnh nhỏ và đơn giản, vì vậy không cần thiết.

Nhưng cuối cùng các script ngày càng trở nên phức tạp hơn nên cộng đồng đã phát minh ra nhiều cách để tổ chức code thành các module, các thư viện đặc biệt để tải các module theo yêu cầu.

Để đặt tên cho một số (vì lý do lịch sử):

  • AMD - một trong những hệ thống mô-đun cổ xưa nhất, được triển khai ban đầu bởi thư viện request.js .
  • CommonJS - hệ thống mô-đun được tạo cho máy chủ Node.js.
  • UMD - một hệ thống mô-đun nữa, được đề xuất như một hệ thống phổ thông, tương thích với AMD và CommonJS.

Giờ đây, tất cả những thứ này dần trở thành một phần của lịch sử, nhưng chúng ta vẫn có thể tìm thấy chúng trong các chữ viết cũ.

Hệ thống mô-đun cấp ngôn ngữ xuất hiện trong tiêu chuẩn vào năm 2015, dần dần phát triển kể từ đó và hiện được hỗ trợ bởi tất cả các trình duyệt chính và trong Node.js. Vì vậy, chúng ta sẽ nghiên cứu các mô-đun JavaScript hiện đại từ bây giờ.

Mô-đun là gì?

Một mô-đun chỉ là một tệp. Một tập lệnh là một mô-đun. Đơn giản vậy thôi.

Các mô-đun có thể tải lẫn nhau và sử dụng các chỉ thị đặc biệt exportvà importđể hoán đổi chức năng, hãy gọi các chức năng của một mô-đun này từ một mô-đun khác:

  • exportnhãn từ khóa các biến và hàm có thể truy cập được từ bên ngoài mô-đun hiện tại.
  • importcho phép nhập chức năng từ các mô-đun khác.

Ví dụ: nếu chúng ta có một tệp sayHi.jsxuất một hàm:

// 📁 sayHi.js
export function sayHi(user) {
  alert(`Hello, ${user}!`);
}

… Sau đó, một tệp khác có thể nhập và sử dụng nó:

// 📁 main.js
import {sayHi} from './sayHi.js';

alert(sayHi); // function...
sayHi('John'); // Hello, John!

Lệnh importtải mô-đun theo đường dẫn ./sayHi.jsliên quan đến tệp hiện tại và gán chức năng đã xuất sayHicho biến tương ứng.

Hãy chạy ví dụ trong trình duyệt.

Vì các mô-đun hỗ trợ các từ khóa và tính năng đặc biệt, chúng ta phải cho trình duyệt biết rằng tập lệnh phải được coi như một mô-đun, bằng cách sử dụng thuộc tính <script type="module">.

Như thế này:

Kết quả
say.js
index.html
 
<!doctype html>
<script type="module">
  import {sayHi} from './say.js';

  document.body.innerHTML = sayHi('John');
</script>

Trình duyệt tự động tìm nạp và đánh giá mô-đun đã nhập (và nhập mô-đun nếu cần), rồi chạy tập lệnh.

Mô-đun chỉ hoạt động qua (các) HTTP, không hoạt động cục bộ

Nếu bạn cố gắng mở một trang web cục bộ, thông qua file://giao thức, bạn sẽ thấy rằng các lệnh import/exportkhông hoạt động. Sử dụng máy chủ web cục bộ, chẳng hạn như máy chủ tĩnh hoặc sử dụng khả năng “máy chủ trực tiếp” của trình soạn thảo của bạn, chẳng hạn như Phần mở rộng Máy chủ Trực tiếp VS Code để kiểm tra các mô-đun.

Các tính năng của mô-đun cốt lõi

Điều gì khác biệt trong các mô-đun, so với các tập lệnh "thông thường"?

Có các tính năng cốt lõi, hợp lệ cho cả JavaScript trình duyệt và phía máy chủ.

Luôn "sử dụng strict"

Các mô-đun luôn hoạt động ở chế độ nghiêm ngặt. Ví dụ: gán cho một biến chưa được khai báo sẽ gây ra lỗi.

 
 
<script type="module">
  a = 5; // error
</script>

Phạm vi cấp mô-đun

Mỗi mô-đun có phạm vi cấp cao nhất của riêng nó. Nói cách khác, các biến và hàm cấp cao nhất từ ​​một mô-đun không được nhìn thấy trong các tập lệnh khác.

Trong ví dụ dưới đây, hai tập lệnh được nhập và hello.jscố gắng sử dụng userbiến được khai báo trong user.js. Nó không thành công, vì đó là một mô-đun riêng biệt (bạn sẽ thấy lỗi trong bảng điều khiển):

Kết quả
xin chào.js
user.js
index.html
 
<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>

Mô-đun nên exportnhững gì họ muốn có thể truy cập từ bên ngoài và importnhững gì họ cần.

  • user.jsnên xuất userbiến.
  • hello.jsnên nhập nó từ user.jsmô-đun.

Nói cách khác, với các mô-đun, chúng ta sử dụng nhập / xuất thay vì dựa vào các biến toàn cục.

Đây là biến thể chính xác:

Kết quả
xin chào.js
user.js
index.html
 
import {user} from './user.js';

document.body.innerHTML = user; // John

Trong trình duyệt, nếu chúng ta nói về các trang HTML, phạm vi cấp cao nhất độc lập cũng tồn tại cho mỗi trang <script type="module">.

Đây là hai tập lệnh trên cùng một trang, cả hai type="module". Họ không nhìn thấy các biến cấp cao nhất của nhau:

 
 
<script type="module">
  // The variable is only visible in this module script
  let user = "John";
</script>

<script type="module">
  alert(user); // Error: user is not defined
</script>
Xin lưu ý:

Trong trình duyệt, chúng ta có thể tạo toàn cục cấp cửa sổ có thể thay đổi bằng cách gán rõ ràng nó cho một thuộc windowtính, ví dụ window.user = "John".

Sau đó, tất cả các tập lệnh sẽ nhìn thấy nó, cả khi có type="module"và không có nó.

Điều đó nói rằng, việc tạo ra các biến toàn cầu như vậy là không tốt. Hãy cố gắng tránh chúng.

Mã mô-đun chỉ được đánh giá lần đầu tiên khi được nhập

Nếu cùng một mô-đun được nhập vào nhiều mô-đun khác, thì mã của nó chỉ được thực thi một lần vào lần nhập đầu tiên. Sau đó, xuất khẩu của nó được trao cho tất cả các nhà nhập khẩu khác.

Việc đánh giá một lần có những hậu quả quan trọng mà chúng ta cần lưu ý.

Hãy xem một vài ví dụ.

Đầu tiên, nếu việc thực thi mã mô-đun mang lại các tác dụng phụ, chẳng hạn như hiển thị một tin nhắn, thì việc nhập nó nhiều lần sẽ chỉ kích hoạt nó một lần - lần đầu tiên:

// 📁 alert.js
alert("Module is evaluated!");
// Import the same module from different files

// 📁 1.js
import `./alert.js`; // Module is evaluated!

// 📁 2.js
import `./alert.js`; // (shows nothing)

Lần nhập thứ hai không hiển thị gì, bởi vì mô-đun đã được đánh giá.

Có một quy tắc: mã mô-đun cấp cao nhất nên được sử dụng để khởi tạo, tạo cấu trúc dữ liệu nội bộ dành riêng cho mô-đun. Nếu chúng ta cần tạo một thứ gì đó có thể gọi được nhiều lần - chúng ta nên xuất nó dưới dạng một hàm, giống như chúng ta đã làm với sayHiở trên.

Bây giờ, hãy xem xét một ví dụ sâu hơn.

Giả sử, một mô-đun xuất một đối tượng:

// 📁 admin.js
export let admin = {
  name: "John"
};

Nếu mô-đun này được nhập từ nhiều tệp, mô-đun chỉ được đánh giá lần đầu tiên, adminđối tượng được tạo và sau đó được chuyển cho tất cả các nhà nhập khẩu tiếp theo.

Tất cả các nhà nhập khẩu nhận được chính xác một adminđối tượng duy nhất:

// 📁 1.js
import {admin} from './admin.js';
admin.name = "Pete";

// 📁 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete

// Both 1.js and 2.js reference the same admin object
// Changes made in 1.js are visible in 2.js

Như bạn có thể thấy, khi 1.jsthay đổi thuộc nametính trong nhập admin, sau đó 2.jscó thể thấy mới admin.name.

Đó chính xác là vì mô-đun chỉ được thực thi một lần. Các bản xuất được tạo và sau đó chúng được chia sẻ giữa các nhà nhập khẩu, vì vậy nếu có điều gì đó thay đổi adminđối tượng, các nhà nhập khẩu khác sẽ thấy điều đó.

Hành vi như vậy thực sự rất thuận tiện, bởi vì nó cho phép chúng ta cấu hình các mô-đun.

Nói cách khác, một mô-đun có thể cung cấp một chức năng chung cần thiết lập. Ví dụ: xác thực cần thông tin xác thực. Sau đó, nó có thể xuất một đối tượng cấu hình mong đợi mã bên ngoài để gán cho nó.

Đây là mẫu cổ điển:

  1. Một mô-đun xuất một số phương tiện cấu hình, ví dụ như một đối tượng cấu hình.
  2. Trong lần nhập đầu tiên, chúng ta khởi tạo nó, ghi vào các thuộc tính của nó. Tập lệnh ứng dụng cấp cao nhất có thể làm điều đó.
  3. Nhập khẩu tiếp theo sử dụng mô-đun.

Ví dụ: admin.jsmô-đun có thể cung cấp một số chức năng nhất định (ví dụ: xác thực), nhưng mong đợi thông tin xác thực đi vào configđối tượng từ bên ngoài:

// 📁 admin.js
export let config = { };

export function sayHi() {
  alert(`Ready to serve, ${config.user}!`);
}

Tại đây, admin.jsxuất configđối tượng (ban đầu trống, nhưng cũng có thể có các thuộc tính mặc định).

Sau đó init.js, trong tập lệnh đầu tiên của ứng dụng, chúng ta nhập configtừ nó và đặt config.user:

// 📁 init.js
import {config} from './admin.js';
config.user = "Pete";

… Bây giờ mô-đun đã admin.jsđược cấu hình.

Các nhà nhập khẩu khác có thể gọi nó và nó hiển thị chính xác người dùng hiện tại:

// 📁 another.js
import {sayHi} from './admin.js';

sayHi(); // Ready to serve, Pete!

import.meta

Đối tượng import.metachứa thông tin về mô-đun hiện tại.

Nội dung của nó phụ thuộc vào môi trường. Trong trình duyệt, nó chứa URL của tập lệnh hoặc URL của trang web hiện tại nếu bên trong HTML:

 
 
<script type="module">
  alert(import.meta.url); // script URL
  // for an inline script - the URL of the current HTML-page
</script>

Trong một mô-đun, “cái này” là không xác định

Đó là một tính năng nhỏ, nhưng để hoàn thiện chúng ta nên đề cập đến nó.

Trong một mô-đun, cấp cao nhất thislà không xác định.

So sánh nó với các tập lệnh không phải mô-đun, ở đâu thislà một đối tượng toàn cục:

 
<script>
  alert(this); // window
</script>

<script type="module">
  alert(this); // undefined
</script>

Các tính năng dành riêng cho trình duyệt

Cũng có một số khác biệt về trình duyệt cụ thể của các tập lệnh type="module"so với các tập lệnh thông thường.

Bạn có thể muốn bỏ qua phần này ngay bây giờ nếu bạn đang đọc lần đầu tiên hoặc nếu bạn không sử dụng JavaScript trong trình duyệt.

Tập lệnh mô-đun bị hoãn lại

Các tập lệnh mô-đun luôn được hoãn lại, có tác dụng giống như deferthuộc tính (được mô tả trong chương Scripts: async, defer ), cho cả tập lệnh bên ngoài và nội tuyến.

Nói cách khác:

  • tải xuống các tập lệnh mô-đun bên ngoài <script type="module" src="...">không chặn quá trình xử lý HTML, chúng tải song song với các tài nguyên khác.
  • các tập lệnh mô-đun đợi cho đến khi tài liệu HTML hoàn toàn sẵn sàng (ngay cả khi chúng rất nhỏ và tải nhanh hơn HTML), rồi chạy.
  • thứ tự tương đối của các tập lệnh được duy trì: các tập lệnh đi trước trong tài liệu, thực thi trước.

Như một tác dụng phụ, các tập lệnh mô-đun luôn “nhìn thấy” trang HTML được tải đầy đủ, bao gồm các phần tử HTML bên dưới chúng.

Ví dụ:

 
 
<script type="module">
  alert(typeof button); // object: the script can 'see' the button below
  // as modules are deferred, the script runs after the whole page is loaded
</script>

Compare to regular script below:

<script>
  alert(typeof button); // button is undefined, the script can't see elements below
  // regular scripts run immediately, before the rest of the page is processed
</script>

<button id="button">Button</button>

Xin lưu ý: tập lệnh thứ hai thực sự chạy trước tập lệnh đầu tiên! Vì vậy, chúng ta sẽ xem undefinedtrước, và sau đó object.

Đó là bởi vì các mô-đun được hoãn lại, vì vậy chúng ta đợi tài liệu được xử lý. Tập lệnh thông thường chạy ngay lập tức, vì vậy chúng ta nhìn thấy đầu ra của nó trước tiên.

Khi sử dụng các mô-đun, chúng ta nên biết rằng trang HTML hiển thị khi nó tải và các mô-đun JavaScript chạy sau đó, vì vậy người dùng có thể xem trang trước khi ứng dụng JavaScript sẵn sàng. Một số chức năng có thể chưa hoạt động. Chúng ta nên đặt "chỉ báo tải", hoặc nếu không thì đảm bảo rằng khách truy cập sẽ không bị nhầm lẫn bởi điều đó.