Ngày phát hành: 31 tháng 1 năm 2025
Hãy tưởng tượng bạn đang chạy một blog có đầy đủ chức năng trong trình duyệt của mình – không chỉ giao diện người dùng mà còn cả phần phụ trợ. Không có máy chủ hoặc đám mây nào tham gia – chỉ có bạn, trình duyệt của bạn và… WebAssembly! Bằng cách cho phép các khung phía máy chủ chạy cục bộ, WebAssembly đang làm mờ ranh giới của hoạt động phát triển web cổ điển và mở ra những khả năng mới thú vị. Trong bài đăng này, Vladimir Dementyev (Trưởng bộ phận phụ trách phần phụ trợ tại Evil Martians) chia sẻ tiến trình chuẩn bị Ruby on Rails cho Wasm và trình duyệt:
- Cách đưa Rails vào trình duyệt trong 15 phút.
- Hậu trường của quá trình chuyển đổi Rails sang wasm.
- Tương lai của Rails và Wasm.
"Blog trong 15 phút" nổi tiếng của Ruby on Rails hiện đang chạy ngay trong trình duyệt của bạn
Ruby on Rails là một khung web tập trung vào năng suất của nhà phát triển và việc phân phối nhanh chóng. Đây là công nghệ được các công ty hàng đầu trong ngành sử dụng, chẳng hạn như GitHub và Shopify. Khung này bắt đầu phổ biến từ nhiều năm trước, khi David Heinemeier Hansson (hay còn gọi là DHH) phát hành video "Cách tạo một blog trong 15 phút" nổi tiếng. Vào năm 2005, việc tạo một ứng dụng web hoạt động đầy đủ trong một thời gian ngắn như vậy là điều không thể tưởng tượng được. Cảm giác như ma thuật vậy!
Hôm nay, tôi muốn mang lại cảm giác kỳ diệu này bằng cách tạo một ứng dụng Rails chạy hoàn toàn trong trình duyệt của bạn. Hành trình của bạn bắt đầu bằng cách tạo một ứng dụng Rails cơ bản theo cách thông thường, sau đó đóng gói ứng dụng đó cho Wasm.
Nền tảng: "blog trong 15 phút" trên dòng lệnh
Giả sử bạn đã cài đặt Ruby và Ruby on Rails trên máy, hãy bắt đầu bằng cách tạo một ứng dụng Ruby on Rails mới và tạo một số chức năng (giống như trong video "blog trong 15 phút" ban đầu):
$ rails new --css=tailwind web_dev_blog
create .ruby-version
...
$ cd web_dev_blog
$ bin/rails generate scaffold Post title:string date:date body:text
create db/migrate/20241217183624_create_posts.rb
create app/models/post.rb
...
$ bin/rails db:migrate
== 20241217183624 CreatePosts: migrating ====================
-- create_table(:posts)
-> 0.0017s
== 20241217183624 CreatePosts: migrated (0.0018s) ===========
Ngay cả khi không chạm vào cơ sở mã, giờ đây, bạn có thể chạy ứng dụng và xem ứng dụng hoạt động như thế nào:
$ bin/dev
=> Booting Puma
=> Rails 8.0.1 application starting in development
...
* Listening on http://127.0.0.1:3000
Bây giờ, bạn có thể mở blog của mình tại http://localhost:3000/posts và bắt đầu viết bài đăng!
Bạn đã có một ứng dụng blog rất đơn giản nhưng đầy đủ chức năng được tạo trong vài phút. Đây là một ứng dụng do máy chủ kiểm soát, toàn diện: bạn có một cơ sở dữ liệu (SQLite) để lưu trữ dữ liệu, một máy chủ web để xử lý các yêu cầu HTTP (Puma) và một chương trình Ruby để lưu trữ logic nghiệp vụ, cung cấp giao diện người dùng và xử lý các lượt tương tác của người dùng. Cuối cùng, có một lớp JavaScript mỏng (Turbo) để đơn giản hoá trải nghiệm duyệt web.
Bản minh hoạ chính thức về Rails tiếp tục theo hướng triển khai ứng dụng này trên máy chủ không có hệ điều hành, nhờ đó, ứng dụng này đã sẵn sàng để phát hành chính thức. Hành trình của bạn sẽ tiếp tục theo hướng ngược lại: thay vì đặt ứng dụng ở một nơi xa, bạn sẽ "triển khai" ứng dụng đó trên máy.
Cấp độ tiếp theo: "blog trong 15 phút" trong Wasm
Kể từ khi thêm WebAssembly, trình duyệt không chỉ có thể chạy mã JavaScript mà còn có thể chạy mọi mã có thể biên dịch thành Wasm. Và Ruby cũng không phải là ngoại lệ. Chắc chắn, Rails không chỉ là Ruby, nhưng trước khi tìm hiểu về các điểm khác biệt, hãy tiếp tục bản minh hoạ và wasmify (động từ do thư viện wasmify-rails đặt ra) ứng dụng Rails!
Bạn chỉ cần thực thi một vài lệnh để biên dịch ứng dụng blog thành mô-đun Wasm và chạy ứng dụng đó trong trình duyệt.
Trước tiên, bạn cài đặt thư viện wasmify-rails bằng Bundler (npm
của Ruby) và chạy trình tạo của thư viện đó bằng Rails CLI:
$ bundle add wasmify-rails
$ bin/rails wasmify:install
create config/wasmify.yml
create config/environments/wasm.rb
...
info ✅ The application is prepared for Wasm-ificaiton!
Lệnh wasmify:rails
định cấu hình một môi trường thực thi "wasm" chuyên dụng (ngoài môi trường "phát triển", "kiểm thử" và "phát hành công khai" mặc định) và cài đặt các phần phụ thuộc bắt buộc. Đối với một ứng dụng Rails mới, điều này là đủ để ứng dụng đó sẵn sàng cho Wasm.
Tiếp theo, hãy tạo mô-đun Wasm cốt lõi chứa môi trường thời gian chạy Ruby, thư viện tiêu chuẩn và tất cả phần phụ thuộc của ứng dụng:
$ bin/rails wasmify:build
==> RubyWasm::BuildSource(3.3) -- Building
...
==> RubyWasm::CrossRubyProduct(ruby-3.3-wasm32-unknown-wasip1-full-4aaed4fbda7afe0bdf4e22167afd101e) -- done in 47.37s
INFO: Packaging gem: rake-13.2.1
...
INFO: Packaging gem: wasmify-rails-0.2.0
INFO: Packaging setup.rb: bundle/setup.rb
INFO: Size: 73.77 MB
Bước này có thể mất chút thời gian: bạn phải tạo Ruby từ nguồn để liên kết đúng cách các tiện ích gốc (được viết bằng C) từ thư viện bên thứ ba. Chúng tôi sẽ đề cập đến hạn chế (tạm thời) này ở phần sau của bài đăng.
Mô-đun Wasm được biên dịch chỉ là một nền tảng cho ứng dụng của bạn. Bạn cũng phải đóng gói chính mã ứng dụng và tất cả thành phần (ví dụ: hình ảnh, CSS, JavaScript). Trước khi đóng gói, hãy tạo một ứng dụng trình chạy cơ bản có thể dùng để chạy Rails đã được tạo bằng wasm trong trình duyệt. Để làm việc đó, bạn cũng có một lệnh trình tạo:
$ bin/rails wasmify:pwa
create pwa
create pwa/boot.html
create pwa/boot.js
...
prepend config/wasmify.yml
Lệnh trước tạo một ứng dụng PWA tối thiểu được tạo bằng Vite. Bạn có thể sử dụng ứng dụng này cục bộ để kiểm thử mô-đun Rails Wasm đã biên dịch hoặc triển khai tĩnh để phân phối ứng dụng.
Giờ đây, với trình chạy, bạn chỉ cần đóng gói toàn bộ ứng dụng vào một tệp nhị phân Wasm duy nhất:
$ bin/rails wasmify:pack
...
Packed the application to pwa/app.wasm
Size: 76.2 MB
Vậy là xong! Chạy ứng dụng trình chạy và xem ứng dụng viết blog Rails của bạn chạy hoàn toàn trong trình duyệt:
$ cd pwa/
$ yarn dev
VITE v4.5.5 ready in 290 ms
➜ Local: http://localhost:5173/
Truy cập vào http://localhost:5173, đợi một chút để nút "Launch" (Khởi chạy) hoạt động rồi nhấp vào nút đó. Hãy tận hưởng việc làm việc với ứng dụng Rails chạy cục bộ trong trình duyệt!
Bạn có thấy kỳ diệu khi chạy một ứng dụng phía máy chủ nguyên khối không chỉ trên máy mà còn trong hộp cát của trình duyệt không? Đối với tôi (mặc dù tôi là "phù thủy"), điều này vẫn giống như một điều viển vông. Nhưng không có phép thuật nào ở đây, chỉ có tiến trình của công nghệ.
Bản minh hoạ
Bạn có thể trải nghiệm bản minh hoạ được nhúng trong bài viết hoặc chạy bản minh hoạ trong một cửa sổ độc lập. Hãy xem mã nguồn trên GitHub.
Hậu trường của Rails trên Wasm
Để hiểu rõ hơn về các thách thức (và giải pháp) khi đóng gói ứng dụng phía máy chủ vào mô-đun Wasm, phần còn lại của bài đăng này sẽ giải thích các thành phần thuộc cấu trúc này.
Một ứng dụng web phụ thuộc vào nhiều yếu tố hơn là chỉ một ngôn ngữ lập trình dùng để viết mã ứng dụng. Mỗi thành phần cũng phải được đưa vào môi trường triển khai cục bộ của bạn – trình duyệt. Điều thú vị về bản minh hoạ "blog trong 15 phút" là bạn có thể thực hiện việc này mà không cần viết lại mã ứng dụng. Mã này cũng được dùng để chạy ứng dụng ở chế độ phía máy chủ cổ điển và trong trình duyệt.
Một khung, chẳng hạn như Ruby on Rails, cung cấp cho bạn một giao diện, một bản tóm tắt để giao tiếp với các thành phần cơ sở hạ tầng. Phần sau đây thảo luận về cách bạn có thể sử dụng cấu trúc khung để phân phát các nhu cầu phân phát cục bộ hơi khó hiểu.
Nền tảng: ruby.wasm
Ruby chính thức sẵn sàng cho Wasm vào năm 2022 (kể từ phiên bản 3.2.0), nghĩa là mã nguồn C có thể được biên dịch sang Wasm và đưa máy ảo Ruby đến bất cứ đâu bạn muốn. Dự án ruby.wasm vận chuyển các mô-đun được biên dịch trước và các liên kết JavaScript để chạy Ruby trong trình duyệt (hoặc bất kỳ môi trường thời gian chạy JavaScript nào khác). Dự án ruby:wasm cũng đi kèm với các công cụ bản dựng cho phép Bạn tạo một phiên bản Ruby tuỳ chỉnh có các phần phụ thuộc bổ sung – điều này rất quan trọng đối với các dự án dựa vào thư viện có phần mở rộng C. Có, bạn cũng có thể biên dịch các tiện ích gốc thành Wasm! (Chưa có tiện ích nào, nhưng hầu hết các tiện ích đều có).
Hiện tại, Ruby hỗ trợ đầy đủ Giao diện hệ thống WebAssembly, WASI 0.1. WASI 0.2 bao gồm Mô hình thành phần đã ở trạng thái alpha và chỉ còn vài bước nữa là hoàn tất.Khi WASI 0.2 được hỗ trợ, bạn sẽ không cần phải biên dịch lại toàn bộ ngôn ngữ mỗi khi cần thêm các phần phụ thuộc gốc mới: chúng có thể được tạo thành phần.
Ngoài ra, Mô hình thành phần cũng giúp giảm kích thước gói. Bạn có thể tìm hiểu thêm về quá trình phát triển và tiến trình của ruby.wasm qua buổi trò chuyện Những việc bạn có thể làm với Ruby trên WebAssembly.
Vì vậy, phần Ruby của phương trình Wasm đã được giải quyết. Tuy nhiên, Rails là một khung web cần tất cả các thành phần hiển thị trong sơ đồ trước. Hãy đọc tiếp để tìm hiểu cách đặt các thành phần khác vào trình duyệt và liên kết các thành phần đó với nhau trong Rails.
Kết nối với cơ sở dữ liệu đang chạy trong trình duyệt
SQLite3 đi kèm với phân phối Wasm chính thức và trình bao bọc JavaScript tương ứng, do đó, đã sẵn sàng để nhúng trong trình duyệt. PostgreSQL cho Wasm có sẵn thông qua dự án PGlite. Do đó, bạn chỉ cần tìm hiểu cách kết nối với cơ sở dữ liệu trong trình duyệt từ ứng dụng Rails trên Wasm.
Một thành phần hoặc khung phụ của Rails chịu trách nhiệm lập mô hình dữ liệu và tương tác với cơ sở dữ liệu được gọi là Bản ghi hoạt động (Active Record) (đúng vậy, được đặt theo tên mẫu thiết kế ORM). Active Record tóm tắt hoạt động triển khai cơ sở dữ liệu thực tế bằng SQL từ mã ứng dụng thông qua các bộ chuyển đổi cơ sở dữ liệu. Ngay từ đầu, Rails cung cấp cho bạn các trình chuyển đổi SQLite3, PostgreSQL và MySQL. Tuy nhiên, tất cả đều giả định việc kết nối với các cơ sở dữ liệu thực có sẵn qua mạng. Để khắc phục vấn đề này, bạn có thể viết các trình chuyển đổi của riêng mình để kết nối với cơ sở dữ liệu trên máy, trong trình duyệt!
Dưới đây là cách tạo trình chuyển đổi SQLite3 Wasm và PGlite được triển khai trong dự án Wasmify Rails:
- Lớp bộ chuyển đổi kế thừa từ bộ chuyển đổi tích hợp tương ứng (ví dụ:
class PGliteAdapter < PostgreSQLAdapter
), vì vậy, bạn có thể sử dụng lại logic chuẩn bị truy vấn thực tế và phân tích cú pháp kết quả. - Thay vì kết nối cơ sở dữ liệu cấp thấp, bạn sử dụng đối tượng giao diện bên ngoài nằm trong môi trường thời gian chạy JavaScript – một cầu nối giữa mô-đun Rails Wasm và cơ sở dữ liệu.
Ví dụ: sau đây là cách triển khai cầu nối cho SQLite3 Wasm:
export function registerSQLiteWasmInterface(worker, db, opts = {}) {
const name = opts.name || "sqliteForRails";
worker[name] = {
exec: function (sql) {
let cols = [];
let rows = db.exec(sql, { columnNames: cols, returnValue: "resultRows" });
return {
cols,
rows,
};
},
changes: function () {
return db.changes();
},
};
}
Từ quan điểm của ứng dụng, việc chuyển từ cơ sở dữ liệu thực sang cơ sở dữ liệu trong trình duyệt chỉ là vấn đề về cấu hình:
# config/database.yml
development:
adapter: sqlite3
production:
adapter: sqlite3
wasm:
adapter: sqlite3_wasm
js_interface: "sqliteForRails"
Việc làm việc với cơ sở dữ liệu cục bộ không đòi hỏi nhiều công sức. Tuy nhiên, nếu cần đồng bộ hoá dữ liệu với một số nguồn đáng tin cậy trung tâm, thì bạn có thể gặp phải một thách thức ở cấp cao hơn. Câu hỏi này nằm ngoài phạm vi của bài đăng này (gợi ý: hãy xem bản minh hoạ về Rails trên PGlite và ElectricSQL).
Trình chạy dịch vụ dưới dạng máy chủ web
Một thành phần thiết yếu khác của mọi ứng dụng web là máy chủ web. Người dùng tương tác với các ứng dụng web bằng các yêu cầu HTTP. Do đó, bạn cần có một cách để định tuyến các yêu cầu HTTP được kích hoạt bằng thao tác điều hướng hoặc gửi biểu mẫu đến mô-đun Wasm. May mắn thay, trình duyệt có câu trả lời cho vấn đề đó – trình chạy dịch vụ.
Worker dịch vụ là một loại Worker web đặc biệt đóng vai trò là proxy giữa ứng dụng JavaScript và mạng. Trình bổ trợ này có thể chặn và thao tác các yêu cầu, ví dụ: phân phát dữ liệu đã lưu vào bộ nhớ đệm, chuyển hướng đến các URL khác hoặc… đến các mô-đun Wasm! Dưới đây là bản phác thảo về một dịch vụ đang xử lý các yêu cầu phân phát bằng cách sử dụng ứng dụng Rails chạy trong Wasm:
// The vm variable holds a reference to the Wasm module with a
// Ruby VM initialized
let vm;
// The db variable holds a reference to the in-browser
// database interface
let db;
const initVM = async (progress, opts = {}) => {
if (vm) return vm;
if (!db) {
await initDB(progress);
}
vm = await initRailsVM("/app.wasm");
return vm;
};
const rackHandler = new RackHandler(initVM});
self.addEventListener("fetch", (event) => {
// ...
return event.respondWith(
rackHandler.handle(event.request)
);
});
Thao tác "tìm nạp" được kích hoạt mỗi khi trình duyệt đưa ra yêu cầu. Bạn có thể lấy thông tin yêu cầu (URL, tiêu đề HTTP, nội dung) và tạo đối tượng yêu cầu của riêng mình.
Rails, giống như hầu hết các ứng dụng web Ruby, dựa vào giao diện Rack để xử lý các yêu cầu HTTP. Giao diện của Rack mô tả định dạng của đối tượng yêu cầu và phản hồi cũng như giao diện của trình xử lý HTTP cơ bản (ứng dụng). Bạn có thể biểu thị các thuộc tính này như sau:
request = {
"REQUEST_METHOD" => "GET",
"SCRIPT_NAME" => "",
"SERVER_NAME" => "localhost",
"SERVER_PORT" => "3000",
"PATH_INFO" => "/posts"
}
handler = proc do |env|
[
200,
{"Content-Type" => "text/html"},
["<!doctype html><html><body>Hello Web!</body></html>"]
]
end
handler.call(request) #=> [200, {...}, [...]]
Nếu thấy định dạng yêu cầu quen thuộc, thì có thể bạn đã từng làm việc với CGI.
Đối tượng JavaScript RackHandler
chịu trách nhiệm chuyển đổi các yêu cầu và phản hồi giữa các phạm vi JavaScript và Ruby. Do hầu hết các ứng dụng web Ruby đều sử dụng Rack, nên việc triển khai sẽ trở nên phổ biến chứ không dành riêng cho Rails.
Tuy nhiên, quy trình triển khai thực tế quá dài để đăng tại đây.
Worker là một trong những điểm tích hợp chính của ứng dụng web trong trình duyệt. Không chỉ là một proxy HTTP, mà còn là một lớp lưu vào bộ nhớ đệm và một trình chuyển đổi mạng (tức là bạn có thể tạo một ứng dụng ưu tiên cục bộ hoặc có thể dùng khi không có mạng). Đây cũng là một thành phần có thể giúp bạn phân phát các tệp do người dùng tải lên.
Tiếp tục tải tệp lên trong trình duyệt
Một trong những tính năng bổ sung đầu tiên cần triển khai trong ứng dụng blog mới của bạn có thể là tính năng hỗ trợ tải tệp lên, hoặc cụ thể hơn là đính kèm hình ảnh vào bài đăng. Để làm được điều này, bạn cần có cách lưu trữ và phân phát tệp.
Trong Rails, phần khung chịu trách nhiệm xử lý việc tải tệp lên được gọi là Bộ nhớ tích cực. Bộ nhớ hoạt động cung cấp cho nhà phát triển các giao diện và tính năng trừu tượng để xử lý tệp mà không cần suy nghĩ về cơ chế lưu trữ cấp thấp. Bất kể bạn lưu trữ tệp ở đâu, trên ổ đĩa cứng hay trên đám mây, mã ứng dụng vẫn không biết điều đó.
Tương tự như Active Record, để hỗ trợ cơ chế lưu trữ tuỳ chỉnh, bạn chỉ cần triển khai bộ chuyển đổi dịch vụ lưu trữ tương ứng. Nơi lưu trữ tệp trong trình duyệt?
Cách truyền thống là sử dụng cơ sở dữ liệu. Có, bạn có thể lưu trữ tệp dưới dạng blob trong cơ sở dữ liệu mà không cần thêm thành phần cơ sở hạ tầng nào. Và đã có một trình bổ trợ tạo sẵn cho việc đó trong Rails, đó là Cơ sở dữ liệu lưu trữ động. Tuy nhiên, việc phân phát các tệp được lưu trữ trong cơ sở dữ liệu thông qua ứng dụng Rails chạy trong WebAssembly là không lý tưởng vì liên quan đến các vòng (huỷ) chuyển đổi tuần tự không miễn phí.
Một giải pháp tốt hơn và được tối ưu hoá cho trình duyệt hơn là sử dụng API Hệ thống tệp và xử lý các tệp tải lên và tệp do máy chủ tải lên ngay từ worker dịch vụ. Một ứng cử viên hoàn hảo cho cơ sở hạ tầng như vậy là OPFS (hệ thống tệp riêng tư gốc), một API trình duyệt rất mới chắc chắn sẽ đóng vai trò quan trọng đối với các ứng dụng trong trình duyệt trong tương lai.
Những gì Rails và Wasm có thể đạt được cùng nhau
Tôi khá chắc chắn rằng bạn đã tự hỏi mình câu hỏi này khi bắt đầu đọc bài viết: tại sao phải chạy khung phía máy chủ trong trình duyệt? Ý tưởng về một khung hoặc thư viện phía máy chủ (hoặc phía máy khách) chỉ là một nhãn. Mã tốt và đặc biệt là một mã trừu tượng tốt hoạt động ở mọi nơi. Nhãn không được ngăn bạn khám phá các khả năng mới và đẩy ranh giới của khung (ví dụ: Ruby on Rails) cũng như ranh giới của môi trường thời gian chạy (WebAssembly). Cả hai đều có thể hưởng lợi từ các trường hợp sử dụng không chính thống như vậy.
Ngoài ra, còn có nhiều trường hợp sử dụng thông thường hoặc thực tế.
Trước tiên, việc đưa khung này vào trình duyệt sẽ mở ra vô số cơ hội học hỏi và tạo nguyên mẫu. Hãy tưởng tượng bạn có thể chơi với các thư viện, trình bổ trợ và mẫu ngay trong trình duyệt của mình cùng với những người khác. Stackblitz đã giúp bạn có thể làm được điều này cho các khung JavaScript. Một ví dụ khác là WordPress Playground (Công cụ thử nghiệm WordPress) cho phép bạn thử nghiệm các giao diện WordPress mà không cần rời khỏi trang web. Wasm có thể hỗ trợ điều tương tự cho Ruby và hệ sinh thái của ngôn ngữ này.
Có một trường hợp đặc biệt về việc lập trình trong trình duyệt đặc biệt hữu ích cho các nhà phát triển nguồn mở – phân loại và gỡ lỗi các vấn đề. Xin nhắc lại, StackBlitz đã tạo điều này cho các dự án JavaScript: bạn tạo một tập lệnh tái tạo tối thiểu, trỏ đến đường liên kết trong một Vấn đề trên GitHub và giúp người bảo trì tiết kiệm thời gian tái tạo trường hợp của bạn. Và thực sự, điều này đã bắt đầu xảy ra trong Ruby nhờ dự án RunRuby.dev (dưới đây là một vấn đề mẫu được giải quyết bằng cách tái tạo trong trình duyệt).
Một trường hợp sử dụng khác là ứng dụng có thể hoạt động ngoại tuyến (hoặc nhận biết được khi không có mạng). Các ứng dụng có thể dùng khi không có mạng thường hoạt động bằng mạng, nhưng khi không có kết nối, bạn vẫn có thể sử dụng các ứng dụng đó. Ví dụ: một ứng dụng email cho phép bạn tìm kiếm trong hộp thư đến khi không có mạng. Hoặc một ứng dụng thư viện nhạc có khả năng "Lưu trữ trên thiết bị" để nhạc bạn yêu thích vẫn phát ngay cả khi không có kết nối mạng. Cả hai ví dụ đều phụ thuộc vào dữ liệu được lưu trữ cục bộ, chứ không chỉ sử dụng bộ nhớ đệm như với các PWA cũ.
Cuối cùng, việc xây dựng các ứng dụng cục bộ (hoặc máy tính) bằng Rails cũng rất hợp lý, vì năng suất mà khung này mang lại cho bạn không phụ thuộc vào thời gian chạy. Khung có đầy đủ tính năng phù hợp để xây dựng các ứng dụng có nhiều dữ liệu cá nhân và logic. Ngoài ra, việc sử dụng Wasm làm định dạng phân phối di động cũng là một lựa chọn khả thi.
Đây mới chỉ là bước khởi đầu của hành trình Rails trên Wasm. Bạn có thể tìm hiểu thêm về các thách thức và giải pháp trong sách điện tử Ruby on Rails trên WebAssembly (vốn là một ứng dụng Rails có thể dùng khi không có mạng).