Nhật ký công việc cho nhiệm vụ "Thêm đa ngôn ngữ"
Next.js i18n Domain Routing trong môi trường phát triển nội bộ: Lý do tôi phải chạy dev-server ở cổng 80
Khi thiết lập môi trường cục bộ cho Next.js, tôi đã gặp phải một hạn chế khá khó chịu của tính năng domain-based i18n routing được tích hợp sẵn.
Thoạt nhìn, bài toán rất đơn giản: ứng dụng sử dụng các tên miền khác nhau cho các ngôn ngữ (locales) khác nhau và bạn muốn tái tạo lại sơ đồ đó trên máy cục bộ. Ví dụ:
const nextConfig: NextConfig = {
i18n: {
locales: LOCALE_CODES,
defaultLocale: 'en',
localeDetection: false,
domains: [
{
domain: 'vietnamguru-v3.localhost',
defaultLocale: 'ru',
locales: ['ru'],
http: true,
},
],
},
}
Next.js chính thức hỗ trợ cấu hình này. Trường http: true tồn tại phần lớn để kiểm thử cục bộ các locale domain qua HTTP thay vì HTTPS.
Vấn đề bắt đầu phát sinh từ số cổng (port).
Không thể chỉ định đúng cổng dev trong i18n.domains
Dev server thông thường của Next.js chạy trên cổng 3000:
http://vietnamguru-v3.localhost:3000
Sẽ rất hợp lý nếu viết thế này:
domains: [
{
domain: 'vietnamguru-v3.localhost:3000',
defaultLocale: 'ru',
locales: ['ru'],
http: true,
},
]
Nhưng i18n.domains trong Next.js được thiết kế dành riêng cho các tên miền (domain), chứ không phải cho các origin tùy ý.
Kiểu dữ liệu DomainLocale hiện tại trông như thế này:
export interface DomainLocale {
defaultLocale: string
domain: string
http?: true
locales?: readonly string[]
}
Nó không có port, origin, hay bất kỳ trường devPort nào cả.
Bản thân điều này sẽ không quá tệ nếu Next chỉ sử dụng tên miền để nhận diện locale. Tuy nhiên, domain routing còn ảnh hưởng đến cả việc tạo liên kết (link generation).
<Link href="/place"> bất ngờ trở thành liên kết tuyệt đối (absolute link)
Trong ứng dụng có một liên kết hoàn toàn bình thường:
<Link href="/place">
Что посетить
</Link>
Nếu không có domain-based i18n, bạn sẽ mong đợi một đoạn HTML đại loại thế này:
<a href="/place">Что посетить</a>
Và trình duyệt sẽ tự nhiên mở ra:
http://vietnamguru-v3.localhost:3000/place
Nghĩa là origin hiện tại, bao gồm cả cổng, được giữ nguyên tự động.
Nhưng khi bật domain routing, Next.js biết rằng một locale cụ thể thuộc về một tên miền cụ thể. Do đó, nó có thể tạo ra một URL locale-domain tuyệt đối cho thẻ Link.
Kết quả là ta nhận được:
<a href="http://vietnamguru-v3.localhost/place">
Что посетить
</a>
Và tại đây, :3000 đã bị mất.
Điều này đặc biệt khó chịu bởi vì trong mã nguồn JSX hoàn toàn không có URL tuyệt đối nào cả:
<Link href="/place">
Chính tầng routing của Next.js đã biến nó thành tuyệt đối.
Tình trạng nghịch lý xuất hiện:
Trang hiện tại:
http://vietnamguru-v3.localhost:3000/foo
JSX:
<Link href="/place">
Href được tạo ra:
http://vietnamguru-v3.localhost/place
Trình duyệt hoàn toàn có lý khi hiểu URL sau cùng là HTTP trên cổng chuẩn 80.
Không thể đơn giản bảo Next.js: "Hãy giữ các liên kết nội bộ ở dạng tương đối"
Đây có lẽ là hạn chế lớn nhất.
Trong cấu hình domain i18n, không có tùy chọn kiểu như:
relativeLinks: true
hay:
absoluteLocaleLinks: false
Không có cách nào để thiết lập:
port: 3000
và không có dev-origin riêng biệt:
origin: 'http://vietnamguru-v3.localhost:3000'
Nói cách khác, mô hình cấu hình thực chất giả định rằng locale domain khả dụng trên cổng chuẩn của giao thức tương ứng.
Đối với môi trường production, điều này hoàn toàn bình thường:
https://example.com
https://example.fr
Đối với môi trường phát triển cục bộ:
http://example.localhost:3000
— lại là một vấn đề.
Tại sao http: true không giải quyết được vấn đề
Thoạt đầu, tên trường mang lại hy vọng:
{
domain: 'vietnamguru-v3.localhost',
http: true,
}
Nhưng nó chỉ chịu trách nhiệm chọn giao thức:
https://
hoặc:
http://
Tức là Next nhận đủ thông tin để xây dựng:
http://vietnamguru-v3.localhost/place
Nhưng thông tin rằng máy chủ cục bộ đang chạy ở cổng 3000 đơn giản là không tồn tại trong mô hình này.
Phương án dùng reverse proxy
Về mặt kiến trúc, giải pháp sạch sẽ nhất là đặt một reverse proxy cục bộ trước Next:
http://vietnamguru-v3.localhost
|
v
localhost:3000
Ví dụ, thông qua nginx, Caddy hoặc một proxy cục bộ khác.
Khi đó, Next vẫn tiếp tục tạo ra:
http://vietnamguru-v3.localhost/place
và URL này thực sự hoạt động.
Nhưng đối với môi trường dev hiện tại của tôi, đây là hạ tầng dư thừa chỉ để lách qua một hạn chế của framework.
Do đó, giải pháp tạm thời hóa ra lại đơn giản hơn: chạy trực tiếp Next.js trên cổng 80.
Chạy Next.js trên cổng 80
Việc chạy thì cực kỳ đơn giản:
PORT=80 npm run dev
Sau đó:
http://vietnamguru-v3.localhost
thực sự trở thành địa chỉ của dev server, và các liên kết tuyệt đối do Next.js tạo ra bắt đầu hoạt động chính xác.
Nhưng vấn đề tiếp theo lại xuất hiện.
Người dùng thông thường không thể lắng nghe cổng 80
Trên Linux, các cổng dưới 1024 theo truyền thống là các cổng đặc quyền (privileged ports).
Do đó, lệnh thông thường:
PORT=80 npm run dev
có thể kết thúc bằng lỗi quyền truy cập khi Node.js cố gắng bind vào cổng 80.
Ý nghĩ hiển nhiên đầu tiên:
sudo npm run dev
Nhưng đây vốn dĩ là một lựa chọn tồi, và trong trường hợp của tôi, nó gần như không khả thi: Node/npm không được cài đặt toàn cục trong môi trường hệ thống.
Ví dụ, nếu Node được quản lý bởi một trình quản lý phiên bản (version manager) của người dùng, lệnh npm hiển thị trên shell hiện tại không nhất thiết tồn tại trong môi trường của sudo.
Ta rơi vào tình huống kinh điển:
npm run dev
thì chạy được, còn:
sudo npm run dev
— thì không, hoặc nó khởi chạy một môi trường Node hoàn toàn khác.
Và việc chạy toàn bộ dev server dưới quyền root chỉ vì muốn mở một cổng duy nhất là điều không hề mong muốn.
Dùng CAP_NET_BIND_SERVICE thay vì chạy Node dưới quyền root
Trên Linux, có một capability tên là CAP_NET_BIND_SERVICE phục vụ mục đích này.
Nó cho phép một tệp thực thi (executable) cụ thể mở các cổng mạng đặc quyền mà không cần phải chạy toàn bộ tiến trình dưới quyền root.
Đối với tệp thực thi Node hiện tại:
which node
ta có thể cấp capability:
sudo setcap 'cap_net_bind_service=+ep' $(which node)
Sau đó, ta có thể tiếp tục chạy Node dưới quyền người dùng thông thường:
PORT=80 npm run dev
và tiến trình sẽ có thể lắng nghe trên cổng 80.
Kết quả là sơ đồ cục bộ trở thành như sau:
vietnamguru-v3.localhost
|
| :80
v
Next.js dev
trong khi Next.js domain routing tạo ra:
http://vietnamguru-v3.localhost/place
giữ cho URL khớp với địa chỉ thực tế của ứng dụng.
Tại sao đây vẫn chỉ là một giải pháp tạm thời (workaround)
Việc cấp CAP_NET_BIND_SERVICE trực tiếp cho node không phải là một giải pháp tổng quát hoàn hảo.
Capability này được gán cho tệp thực thi Node.js, chứ không phải cho một dự án Next.js cụ thể nào. Do đó, bất kỳ tiến trình nào chạy thông qua tệp nhị phân Node cụ thể đó từ môi trường này đều có khả năng bind vào các cổng đặc quyền.
Hơn nữa, nếu Node được cài đặt thông qua version manager và phiên bản Node bị chuyển đổi hoặc cài đặt lại, đường dẫn đến tệp thực thi có thể thay đổi. Khi đó, capability sẽ phải được gán lại cho tệp nhị phân mới.
Bạn có thể kiểm tra các capability hiện tại bằng lệnh, ví dụ:
getcap $(which node)
Kết quả mong đợi:
/path/to/node cap_net_bind_service=ep
Khi cần thiết, ta có thể gỡ bỏ capability:
sudo setcap -r $(which node)
Đó là lý do tại sao đây chỉ đơn thuần là một giải pháp tạm thời thuận tiện trên máy cục bộ, chứ không phải là cấu hình nên áp dụng một cách bừa bãi cho mọi môi trường phát triển.
Tổng kết
Vấn đề hóa ra không nằm ở DNS, /etc/hosts, React hay hành vi của trình duyệt.
Nó phát sinh từ sự kết hợp của một số đặc điểm trong domain-based i18n của Next.js:
i18n.domainsmô tả hostname nhưng không cung cấp thiết lập cổng riêng.http: truecho phép chọn HTTP thay vì HTTPS, nhưng không cấu hình cổng dev.- Khi sử dụng domain routing, Next.js có thể chuyển đổi thẻ
<Link href="/...">thông thường thành liên kết tuyệt đối trỏ tới locale domain. - Trong cấu hình không có nút chuyển đổi riêng để giữ các liên kết nội bộ đó ở dạng tương đối.
- Do đó, cổng dev mặc định
3000của Next không tương thích tốt với việc giả lập domain-based locale routing trên máy cục bộ.
Trong trường hợp của tôi, giải pháp tạm thời thu được là:
sudo setcap 'cap_net_bind_service=+ep' $(which node)
PORT=80 npm run dev
Sau đó, hostname cục bộ có thể được sử dụng mà không cần chỉ định cổng tường minh:
http://vietnamguru-v3.localhost
và các liên kết locale-domain tuyệt đối của Next.js trùng khớp với origin thực tế.
Nó hoạt động. Nhưng lại để lại khá nhiều hệ lụy về mặt hạ tầng cho một thứ mà ở cấp độ ứng dụng trông chỉ giống như thế này:
<Link href="/place">
Nguồn tham khảo
Tài liệu chính thức của Next.js về Pages Router xác nhận việc hỗ trợ sẵn domain routing, cấu trúc của domains và mục đích của http: true dành riêng cho việc kiểm thử HTTP cục bộ.
Kiểu dữ liệu DomainLocale hiện tại trong vercel/next.js chứa các trường domain, defaultLocale, locales và http, nhưng không có cài đặt cổng riêng biệt trong đó.
Trong tài liệu về thành phần Link, các chuyển hướng nội bộ thông thường vẫn được định nghĩa bằng đường dẫn tương đối (/, /about, /blog/...), tức là không cần phải viết URL locale-domain tuyệt đối vào mã JSX của ứng dụng.