Sản phẩm
Cloudflare Workers nâng cấp hệ thống module: Tương thích Node.js mạnh mẽ hơn
(giờ Việt Nam)
Tóm tắt AI
Cloudflare Workers hiện hỗ trợ Node.js mặc định, nâng giới hạn ứng dụng lên 64MB và cải tiến hệ thống registry với cơ chế biên dịch lười (lazy compilation) cùng bộ nhớ đệm dùng chung, giúp tối ưu hiệu suất và trải nghiệm lập trình.
Bản dịch AI

Chúng tôi đã viết lại module registry trong workerd, thành phần mã nguồn mở cốt lõi của Workers runtime, để giúp nó hoạt động nhanh hơn, tuân thủ các tiêu chuẩn tốt hơn và đồng bộ hơn với module registry của Node.js.
Trong vài năm qua, chúng tôi đã liên tục bổ sung hỗ trợ cho ngày càng nhiều API runtime của Node.js. Workers runtime hiện hỗ trợ mọi API ổn định từ Node.js mà bạn có thể muốn sử dụng trong môi trường serverless, và các API này hiện được bật theo mặc định, cho phép bạn triển khai các ứng dụng Node.js lớn hơn lên Cloudflare (hiện lên tới 64 MiB trên tất cả các gói — chúng tôi đã loại bỏ giới hạn về kích thước bundle nén).
Tuy nhiên, chỉ tương thích API thôi là chưa đủ: các ứng dụng Node.js còn phụ thuộc vào cách runtime phân giải, tải và lưu vào bộ nhớ đệm (cache) các module. ESM, CommonJS và WebAssembly đều là các loại module mà bạn có thể import trong mã nguồn Worker của mình. Hệ thống bên trong runtime xử lý tất cả những việc này được gọi là module registry.
Bạn có thể bắt đầu sử dụng nó ngay hôm nay bằng cách bật cờ tương thích new_module_registry trong Worker của mình.
Khi bạn bật cờ tương thích new_module_registry:
Để tìm hiểu sâu hơn về cách module registry mới này tương tác với các API module của V8, chúng tôi đã thêm tài liệu tham khảo vào workerd, trong đó phân tích chi tiết mọi thứ. Nhưng đối với hầu hết những người xây dựng ứng dụng trên Workers, bạn sẽ muốn hiểu cách những thay đổi này cải thiện khả năng tương thích và hỗ trợ công việc của bạn. Để làm điều đó, chúng ta sẽ đi sâu vào từng thay đổi trong các phần dưới đây.
Cách Workers runtime tải mã nguồn bạn cung cấp
Khi bạn triển khai một Worker lên Cloudflare, wrangler hoặc Vite sẽ "đóng gói" (bundle) tất cả mã nguồn của Worker từ nhiều tệp và phụ thuộc thành một hoặc nhiều module, sau đó được tải lên Cloudflare khi bạn chạy lệnh wrangler deploy.
Theo mặc định, Wrangler đóng gói gần như toàn bộ mã nguồn này thành một tệp script module duy nhất. Nó chạy esbuild ở phía sau, xử lý và chèn (inline) các import tương đối và các lệnh gọi require cho hầu hết các phụ thuộc npm vào tệp đó. Các câu lệnh import và require được thay thế bằng các hàm thông thường như một phần của quá trình này. Đến khi bundle đó đến được Workers runtime (workerd), thường không còn nhiều đồ thị module (module graph) để Workers runtime phải xử lý nữa. Hầu hết các module khác nhau đã được đóng gói vào một tệp. Chúng tôi đã thấy các script này phát triển lên tới hàng trăm nghìn dòng.
Tại sao cần phải đóng gói nhiều module thành một tệp duy nhất trước khi tải mã nguồn phía server lên Cloudflare? Về mặt kỹ thuật, việc tải lên nhiều module, thậm chí các module thuộc các loại khác nhau, đã khả thi trong Workers runtime từ nhiều năm nay. Tuy nhiên, runtime trước đây không phân giải các module theo cách nhất quán với tất cả các runtime khác. Ví dụ, nếu mã nguồn hoặc các phụ thuộc của bạn sử dụng import.meta.resolve để phân giải đường dẫn đến một module khác, mã đó sẽ thất bại vì import.meta.resolve không được hỗ trợ.
Khi bạn sử dụng plugin Vite của Cloudflare, Vite 8 sẽ đóng gói mã nguồn của bạn bằng Rolldown thay vì Wrangler sử dụng esbuild. Rolldown phân giải các import và phụ thuộc npm, chuyển đổi CommonJS sang ESM khi cần thiết, và xuất ra một module đầu vào cùng với bất kỳ chunk bổ sung nào được tạo thông qua phân tách mã (code splitting), chẳng hạn như dynamic import. Kết quả là, Workers runtime nhận được một đồ thị module nhỏ hơn, do trình build tạo ra thay vì đồ thị nguồn gốc của ứng dụng.
Việc triển khai module registry mới trong Workers runtime mở ra cơ hội cho các trình đóng gói (bundler) như Rolldown thực hiện ít thao tác chuyển đổi hơn và dựa vào runtime nhiều hơn để xử lý việc phân giải module.
Khi bạn import một API Node.js trong worker, theo mặc định, bạn đang import một module được tích hợp sẵn trong workerd. Nó không được đóng gói vào mã nguồn của bạn dưới dạng polyfill. Các module Wasm, văn bản và nhị phân cũng được cung cấp cho Workers runtime dưới dạng các tệp riêng biệt. Chúng được tham chiếu bằng định danh (specifier) thay vì bị chèn trực tiếp. Và nếu bạn triển khai với --no-bundle, hoặc công cụ của bạn tải lên một Worker dưới dạng nhiều module trực tiếp, toàn bộ đồ thị module sẽ xuất hiện tại thời điểm runtime chính xác như cách bạn đã viết.
Trong tất cả các trường hợp này, cần có một cơ chế nhận định danh, xác định xem nó thực sự trỏ đến mã nguồn nào, biên dịch nó và cung cấp cho V8 một đối tượng module mà nó có thể liên kết và chạy. Trong workerd, đó là công việc của module registry.
Tại sao lại cần một bản triển khai mới?
Registry ban đầu phân giải các định danh dưới dạng đường dẫn kiểu hệ thống tệp, không phải URL. Nghe có vẻ là một sự khác biệt nhỏ, nhưng nó loại trừ rất nhiều thứ: không có cách nào sạch sẽ để triển khai import.meta.url, các import tương đối không tuân theo các quy tắc phân giải giống như new URL, và các giao thức như node: và cloudflare: được xử lý như các tiền tố chuỗi đặc biệt thay vì, thực sự là, các giao thức.
Nó cũng biên dịch toàn bộ bundle Worker của bạn ngay từ đầu, bất kể một module nhất định có được import hay không, và nó giữ một bản sao riêng biệt, cá nhân cho mỗi V8 isolate. Cloudflare chạy nhiều bản sao V8 isolate của cùng một Worker để phân bổ tải trên các nhân CPU, vì vậy trên thực tế, điều đó có nghĩa là biên dịch chính xác cùng một nguồn nhiều lần, đồng thời giữ nhiều bản sao của nguồn trong bộ nhớ.
Không có điều nào trong số này thực sự là một lỗi, nhưng nó gây khó khăn cho việc phát triển bản triển khai mà không gây ra các thay đổi phá vỡ (breaking changes). Registry mới bắt đầu từ URL làm định dạng định danh và coi tính lười biếng (laziness) và chia sẻ bộ nhớ đệm là những thứ cần được thiết kế ngay từ ngày đầu. Bản triển khai registry hiện tại sẽ không biến mất. Hiện tại, các Worker đã triển khai sẽ tiếp tục hoạt động như trước đây.
import.meta
API import.meta cung cấp thông tin về module, chẳng hạn như URL của module và liệu đó có phải là module điểm nhập chính (main entry point) hay không:
Nó in ra một cái gì đó giống như file:///bundle/index.js, main: true.
import.meta.main chỉ là true đối với module được cấu hình làm điểm nhập của Worker; mọi module khác đều nhận giá trị false.
import.meta.resolve phân giải một định danh dựa trên module hiện tại mà không cần import nó:
Đây là một phép biến đổi chuỗi thuần túy, giống như trong Node.js và trình duyệt: nó không kiểm tra xem URL được phân giải có tương ứng với một module thực tế hay không, và nó sẽ ném ra một TypeError đối với một định danh không thể phân tích cú pháp thành URL, thay vì trả về null. Một chi tiết đáng lưu ý nếu bạn nhìn kỹ vào đầu ra: nó chuẩn hóa mã hóa phần trăm (percent-encoding) giống như cách new URL thực hiện, nghĩa là nó thu gọn các đường dẫn như./a/../b.js, nhưng nó không giải mã các ký tự đã được mã hóa phần trăm trước đó. import.meta.resolve('%66oo.js') phân giải thành file:///bundle/%66oo.js, chứ không phải file:///bundle/foo.js.
Các định danh là URL
Các import tương đối hiện phân giải theo cùng cách mà new URL(specifier, base) thực hiện, vì đó chính xác là những gì đang xảy ra ở phía sau. Các URL đầy đủ cũng hoạt động như các định danh, không chỉ các đường dẫn tương đối:
Hệ quả thú vị hơn là những gì xảy ra với các chuỗi truy vấn (query string) và phân đoạn (fragment). Theo các quy tắc định danh module giống như trình duyệt sử dụng, một định danh với chuỗi truy vấn hoặc phân đoạn khác nhau được coi là một thực thể module thực sự khác biệt, ngay cả khi nó trỏ đến cùng một nguồn cơ bản:
./counter.js?a và./counter.js?b tải cùng một nguồn, nhưng chúng được đánh giá riêng biệt, mỗi cái có import.meta.url riêng và mỗi cái có bản sao trạng thái cấp cao nhất (top-level state) riêng. Việc import lại cùng một định danh với cùng một chuỗi truy vấn vẫn trả về cùng một thực thể, vì vậy đây không phải là cách để buộc đánh giá lại mỗi khi import.
Các thuộc tính import (import attributes) được xác thực chính xác
Bản triển khai module registry ban đầu lờ đi các thuộc tính import vi phạm đặc tả. Các bản triển khai được mong đợi sẽ ném ra một ngoại lệ khi bất kỳ thuộc tính import nào mà nó không hiểu được sử dụng.
json là loại thuộc tính import duy nhất được bật hiện tại, vì đây là loại duy nhất trong các đề xuất TC39 liên quan đã đạt đến Giai đoạn 4. text và bytes được nhận diện vì chúng theo dõi các đề xuất Import Text và Import Bytes, nhưng chúng bị từ chối với một lỗi cụ thể thay vì bị lờ đi hoặc coi là cú pháp không được hỗ trợ:
Bất kỳ khóa thuộc tính nào khác ngoài type hiện cũng là một lỗi nghiêm trọng, thay vì bị lờ đi:
Và nếu loại (type) bạn chỉ định không khớp với những gì module thực sự là:
require(esm) tuân theo các quy tắc của Node.js
Nếu bạn require một thứ gì đó hóa ra là một ES module, dù là trực tiếp bên trong một module CommonJS hay thông qua require('node:module').createRequire, registry sẽ tuân theo hành vi require(esm) của Node.js:
Có một hạn chế đi kèm với điều này: nếu module bạn đang require, hoặc bất kỳ thứ gì trong đồ thị module của nó, có top-level await, require sẽ ném ra lỗi thay vì chặn hoặc trả về một thứ gì đó chưa hoàn thiện:
Điều này khớp với hạn chế ERR_REQUIRE_ASYNC_MODULE của chính Node.js: require phải trả về đồng bộ, và không có giá trị hợp lý nào để trả về cho một module chưa hoàn thành đánh giá. Hãy sử dụng import cho bất kỳ thứ gì bất đồng bộ (async). Kiểm tra này cũng áp dụng bất kể thứ tự import: một module không trở nên có thể require chỉ vì một thứ gì đó đã import và đánh giá đầy đủ nó trước đó.
Nếu bạn đang require đầu ra từ một bundler có trước khi Node.js hỗ trợ require(esm) và đặt một export __cjsUnwrapDefault có giá trị truthy làm dấu hiệu, thì nó sẽ được ưu tiên hơn cả hai quy tắc trên và trả về export mặc định. Điều đó chỉ tồn tại để các bundle đã xây dựng trước đó vẫn tiếp tục hoạt động.
Các lỗi nhất quán và sử dụng đúng lớp (class)
Bài viết được AI dịch và tổng hợp tự động từ Cloudflare Blog. Liên kết bài gốc ở phía trên. AIHOT.vn luôn dẫn nguồn đầy đủ — nếu bạn thấy điểm cần chỉnh sửa, hãy gửi ý kiến tại trang phản hồi.