Hướng dẫn lập trình Flask – Phần 15: Tinh chỉnh cấu trúc ứng dụng

flask_tutorial_1

Trong phần này, chúng ta sẽ tiến hành tái cấu trúc ứng dụng của chúng ta theo quy cách phù hợp với các ứng dụng cỡ lớn hơn.

Để giúp cho bạn dễ theo dõi, sau đây là danh sách các bài viết trong loạt bài hướng dẫn này:

Bạn có thể truy cập mã nguồn cho phần này tại GitHub.

Đến thời điểm này, Myblog đã không còn là một ứng dụng nhỏ nữa. Vì vậy, đây là lúc thích hợp để chúng ta thảo luận và tìm hiểu làm thế nào để tiếp tục gia tăng số lượng mã nguồn của ứng dụng mà không làm ảnh hưởng để chất lượng hoặc việc quản lý chúng. Triết lý của Flask là cho phép bạn tổ chức các dự án của bạn theo bất kỳ cách nào bạn thích, vì vậy, chúng ta hoàn toàn có thể thay đổi cấu trúc của các ứng dụng cho phù hợp với kích cỡ của ứng dụng hoặc theo nhu cầu hay kinh nghiệm của bạn.

Trong phần này, chúng ta sẽ tìm hiểu một số cách thiết kế phù hợp với các ứng dụng lớn. Và để minh họa, chúng ta sẽ thực hiện một số thay đổi trong cấu trúc dự án của Myblog. Mục tiêu của chúng ta là làm cho mã nguồn dễ bảo trì và được tổ chức tốt hơn. Và theo đúng tinh thần của Flask, đây chỉ là những đề nghị, bạn có thể xem xét và thay đổi cho phù hợp trong các dự án của riêng bạn.

Các giới hạn của ứng dụng

Hiện giờ, ứng dụng Myblog đang phải đối diện với hai vấn đề cơ bản. Nếu bạn để ý đến cấu trúc của ứng dụng này, bạn sẽ thấy rằng nó được phân chia thành một số hệ thống con, nhưng mã nguồn của các hệ thống này lại bị trộn lẫn với nhau và không có ranh giới rõ ràng. Chúng ta có thể điểm danh một số hệ thống con như sau:

  • Hệ thống xác thực người dùng với một số hàm hiển thị trong app/routes.py, một vài form trong app/forms.py, một vài template trong app/templates và hỗ trợ email trong app/email.py.
  • Hệ thống xử lý lỗi với các hàm xử lý lỗi được định nghĩa trong app/errors.py và các template trong app/templates.
  • Các chứng năng chính của ứng dụng bao gồm hiển thị và đăng các bài viết, hồ sơ cá nhân của user, theo dõi, dịch thuật tức thời các bài viết. Các chức năng này được chia ra trong module và template khác nhau.

Khi đi sâu vào ba hệ thống con này, bạn sẽ thấy một điểm chung trong cấu trúc của chúng. Cho đến giờ, cách tổ chức mã nguồn của chúng ta dựa trên các module cho các chức năng khác nhau trong ứng dụng. Chúng ta tạo ra một module cho các hàm hiển thị, một module khác cho các Web form, một module cho các hàm xử lý lỗi và một module khác cho các chức năng liên quan đến email, một thư mục dành cho các template HTML và cứ như thế. Mặc dù cấu trúc này thích hợp cho các ứng dụng nhỏ, nó trở nên cồng kềnh và khó quản lý khi kích thước của dự án gia tăng.

Một cách có thể giúp bạn nhận định vấn đề này rõ hơn là bạn có thể nghĩ đến khả năng bạn cần tạo ra một dự án mới và cần sử dụng lại tối đa các thành phần và hệ thống con mà bạn đã phát triển trong dự án trước đó. Trong một tình huống như vậy, theo lý thuyết thì một hệ thống như là xác thực người dùng có thể được xử dụng lại trong dự án mới mà không gặp bất kỳ khó khăn nào. Nhưng thực tế là để sử dụng lại nó, bạn sẽ phải xem xét các module khác nhau và chọn lựa phần mã nguồn thích hợp để đưa vào dự án mới. Thật là bất tiện phải không? Sẽ tốt hơn nhiều nếu chúng ta có thể đưa tất cả các file có chứa mã nguồn liên quan đến việc xác thực user vào cùng một nơi. Flask có thể giúp chúng ta giải quyết vấn đề này với tính năng blueprint (bản thiết kế).

Vẫn còn một vấn đề thứ hai ít rõ ràng hơn. Chúng ta tạo ra thực thể ứng dụng Flask bằng cách sử dụng một biến toàn cục trong app/__init__.py. và sau đó tham chiếu đến biến này trong nhiều module khác nhau. Mặc dù bản thân thực thể này không phải là một vấn đề, việc sử dụng nó như là một biến toàn cục có thể gây ra một vài rắc rối trong một số hoàn cảnh, đặc biệt là khi thực hiện việc kiểm tra mã nguồn (testing). Bạn hãy thử tưởng tượng việc phải kiểm tra ứng dụng với một vài cấu hình khác nhau. Bởi vì ứng dụng được định nghĩa bằng một biến toàn cục, chúng ta không thể khởi tạo hai ứng dụng với các tham số cấu hình khác nhau. Một rắc rối khác là tất cả các mã được thực thi trong quá trình kiểm tra đều dùng chung một ứng dụng, một đơn vị kiểm tra có thể thực hiện các thay đổi ảnh hưởng đến các bài kiểm tra chạy sau nó. Trong điều kiện lý tưởng, mỗi bài kiểm tra nên được thực hiện trong một thực thể ứng dụng với các tham số cấu hình hoặc dữ liệu không bị ảnh hưởng bới các mã trong các đơn vị kiểm tra khác.

Trước đây, chúng ta đã dùng một giải pháp tạm thời trong module tests.py để thay đổi tham số cấu hình sau khi khởi tạo chúng trong ứng dụng để các mã kiểm tra sử dụng một cơ sở dữ liệu trong bộ nhớ thay vì cơ sở dữ liệu SQLite mặc định. Chúng ta không có lựa chọn nào khác để thay đổi cấu hình cơ sở dữ liệu bởi vì khi các mã kiểm tra được thực thi, ứng dụng đã được khởi tạo và cấu hình. Trong tình huống này, thay đổi cấu hình sau khi nó đã được khởi tạo trong ứng dụng hoạt động được, nhưng trong các trường hợp khác, cách làm này có thể không hoạt động. Nhưng dù thế nào đi nữa, cách làm này không phải là một phương pháp hay vì nó có thể tạo ra cac lỗi không rõ ràng và khó xác định lý do.

Cách giải quyết tốt hơn là sử dụng một hàm tạo ứng dụng (application factory) thay cho biến toàn cục trong ứng dụng để tạo ra các hàm vào thời gian thực thi (runtime). Đây là một hàm nhận tham số là một đối tượng cấu hình và trả về một thực thể ứng dụng Flask được cấu hình theo các định nghĩa trong ứng dụng này. Nếu mã ứng dụng của chúng ta được sửa đổi để làm việc được với hàm tạo ứng dụng, quá trình viết các mã kiểm tra đòi hỏi các cấu hình khác nhau sẽ dễ dàng hơn rất nhiều vì mỗi một đơn vị kiểm tra có thể tạo ra thực thể ứng dụng của riêng nó.

Trong phần này, chúng ta sẽ tiến hành cải tiến mã ứng dụng hiện có để tạo ra các blueprint cho ba hệ thống con và một hàm tạo ứng dụng như đã đề cập ở trên. Bởi vì chúng ta cần phải cập nhật rất nhiều nơi, chúng ta sẽ không đi vào chi tiết của các thay đổi trong mỗi file. Thay vào đó, chúng ta sẽ đi qua cá bước mà chúng ta cần thực hiện trong quá trình này và bạn có thể download mã nguồn cho phần này từ GitHub để theo dõi toàn bộ các chi tiết.

Blueprint

Một blueprint trong Flask là một cấu trúc luận lý đại diện cho một phần của ứng dụng. Một blueprint có thể bao gồm các thành phần như là định tuyến (route), hàm hiển thị, form, template và các file tĩnh. Nếu bạn tạo ra blueprint của bạn trong một gói Python riêng biệt, bạn sẽ có một bộ phận đóng gói các yếu tố cần thiết liên quan đến một chức năng nhất định trong ứng dụng.

Các thành phần bên trong của một blueprint sẽ không được kích hoạt cho đến khi nó được đăng ký với ứng dụng. Trong quá trình đăng ký, tất cả các thành phần bên trong blueprint sẽ được truyền vào ứng dụng. Bạn có thể hình dung blueprint như là một nơi lưu trữ tạm thời cho các chức năng của ứng dụng để tổ chức mã nguồn tốt hơn.

Blueprint xử lý lỗi

Blueprint đầu tiên mà chúng ta sẽ tạo là blueprint có các thành phần để xử lý lỗi. Cấu trúc của blueprint này như sau:

Về bản chất, tất cả những gì chúng ta làm là di chuyển module app/errors.py vào app/errors/handlers.py và hai template kèm theo vào thư mục app/templates/errors. Nhờ đó các template này được phân biệt với các template còn lại trong ứng dụng. Chúng ta cũng phải thay đổi lời gọi hàm render_template() trong cả hai hàm xử lý lỗi để sử dụng các đường dẫn mới đến các template này. Sau đó, chúng ta thêm mã để tạo blueprint vào module app/errors/__init__.py và mã đăng ký blueprint trong module app/__init__.py sau khi tạo ra thực thể ứng dụng.

Cũng cần lưu ý rằng các blueprint trong Flask có thể được cấu hình để chứa các template và các file tĩnh trong các thư mục khác nhau. Chúng ta sẽ sử dụng một thư mục con trong thư mục templates của ứng dụng để chứa tất cả các file từ các blueprint khác nhau như cấu trúc minh họa ở trên. Nhưng nếu muốn, bạn cũng có thể đưa các template thuộc các blueprint khác nhau vào các thư mục bên trong của từng blueprint – điều này hoàn toàn khả thi. Ví dụ như nếu bạn thêm tham số template_folder='templates' vào constructor Blueprint(), bạn có thể dùng thư mục app/errors/templates để chứa các template chỉ dành cho blueprint error mà thôi.

Cách tạo ra một blueprint cũng tương tự như cách tạo ra một ứng dụng. Bạn cần có một module __init__.py trong thư mục của blueprint như sau:

app/errors/__init__.py: Blueprint xử lý lỗi.

Lớp Blueprint sẽ nhận các tham số là tên của blueprint, tên của module cơ sở (base – thường được đặt là __name__ như trong trường hợp của thực thể ứng dụng Flask) và một vài tham số tùy chọn khác mà chúng ta không cần sử dụng đến trong trường hợp này. Sau khi đối tượng blueprint được tạo ra, chúng ta sẽ tham chiếu đến module handlers.py để đăng ký các hàm xử lý lỗi bên trong module này với blueprint. Tham chiếu được đặt ở cuối để tránh tình trạng tham chiếu vòng (circular dependency).

Trong module handlers.py, thay vì kết nối các hàm xử lý lỗi với ứng dụng thông qua decorator @app.errorhandler, chúng ta sử dụng decorator @bp.app_errorhandler từ blueprint. Dù kết quả đều như nhau, việc sử dụng decorator của blueprint sẽ làm cho blueprint độc lập với ứng dụng và có thể được sử dụng với các ứng dụng khác nhau dễ dàng hơn. Chúng ta cũng thay thế đường dẫn đến các template thông báo lỗi bằng các đường dẫn đến thư mục con errors vừa tạo ra ở trên.

Để hoàn tất quá trình này, chúng ta cần đăng ký blueprint mới này với ứng dụng:

app/__init__.py: Đăng ký blueprint xử lý lỗi với ứng dụng.

Để đăng ký một blueprint, chúng ta sử dụng phương thức register_blueprint() trong ứng dụng Flask. Sau khi một blueprint đã được đăng ký, tất cả các hàm hiển thị, templates, file tĩnh, các hàm xử lý lỗi, … sẽ được kết nối vào ứng dụng. Một lần nữa, chúng ta đặt tham chiếu trước khi gọi hàm app.register_blueprint() để tránh tham chiếu vòng.

Blueprint xác thực người dùng

Quá trình cải tiến chức năng xác thực người dùng để tạo ra blueprint cũng tương tự như đối với các hàm xử lý lỗi. Sau đây là sơ đồ chi tiết của blueprint xác thực người dùng:

Để tạo ra blueprint này, chúng ta phải chuyển toàn bộ các chức năng liên quan vào các module mới trong blueprint, bao gồm các hàm hiển thị, webform và các hàm hỗ trợ như là hàm dùng để gởi token phục hồi mật mã đến user bằng email. Chúng ta cũng chuyển các template liên quan vào trong một thư mục con riêng biệt tương tự như với các template thông báo lỗi.

Khi định nghĩa các định tuyến cho blueprint, chúng ta sử dụng decorator @bp.route thay vì @app.route. Bên cạnh đó, chúng ta cũng phải cập nhật các lời gọi hàm url_for() để tạo ra các URL. Đối với các hàm hiển thị được định nghĩa trực tiếp từ ứng dụng, tham số đầu tiên cho nó sẽ là tên hàm hiển thị. Nhưng đối với các định tuyến được định nghĩa bên trong blueprint, tham số này phải có cả tên của blueprint và tên hàm hiển thị và được ngăn cách bằng dấu chấm. Cụ thể như chúng ta phải thay thế tất cả các lời gọi hàm url_for('login') với url_for('auth.login') và tương tự như vậy cho các trường hợp khác.

Để đăng ký blueprint auth với ứng dụng, chúng ta sử dụng cú pháp khác một chút so với blueprint xử lý lỗi:

app/__init__.py: Đăng ký blueprint xác thực người dùng với ứng dụng.

Khi gọi hàm register_blueprint() trong trường hợp này, chúng ta sử dụng một tham số mới là url_prefix. Tham số này là tùy chọn, nhưng Flask cho phép bạn kết nối một blueprint vào một tiền tố URL, nhờ đó mọi URL trong blueprint này sẽ được bắt đầu với tiền tố này. Điều này đặc biệt hữu ích khi chúng ta muốn phân biệt rõ các URL giữa các blueprint khác nhau trong ứng dụng. Trong trường hợp này, mọi URL từ blueprint xác thực người dùng sẽ được bắt đầu với tiền tố /auth. Ví dụ như URL cho trang đăng nhập sẽ là http://localhost:5000/auth/login. Bởi vì chúng ta sử dụng hàm url_for() để tạo ra các URL, tất cả các URL trong blueprint này sẽ được kết hợp với tiền tố này một cách tự động.

Blueprint cho ứng dụng chính

Blueprint thứ ba gồm có các chức năng chính của ứng dụng. Chúng ta cũng tạo ra blueprint này theo quy trình tương tự như với hai blueprint trước và đặt tên nó là main. Vì vậy tất cả các lời gọi hàm url_for() đến các hàm hiển thị bên trong blueprint này phải bắt đầu với tiền tố main. . Và bởi vì đây là chức năng chính trong ứng dụng, chúng ta sẽ giữ nguyên vị trí của các template trong thư mục dành cho template. Điều này không gây ảnh hưởng vì chúng ta đã chuyển các template trong các blueprint khác vào các thư mục con thích hợp.

Thiết kế hàm tạo ứng dụng

Như đã nói từ đầu, việc sử dụng biến toàn cục cho thực thể ứng dụng gây ra một vài vấn đề, chủ yếu trong quá trình kiểm tra. Trước khi chúng ta bắt đầu sử dụng blueprint, thực thể ứng dụng phải là một biến toàn cục bởi vì tất cả các hàm hiển thị và xử lý lỗi cần được liên kết với các decorator từ thư viện app như là @app.route. Nhưng hiện giờ, chúng ta đã chuyển tất cả các định tuyến và hàm xử lý lỗi vào các blueprint tương ứng, việc sử dụng biến toàn cục không còn cần thiết.

Do đó, chúng ta sẽ thêm một hàm gọi là create_app() để tạo ra thực thể ứng dụng Flask và loại bỏ việc sử dụng biến toàn cục như sau:

app/__init__.py: Hàm tạo ứng dụng.

Như chúng ta đã biết, hầu hết các thư viện mở rộng của Flask được khởi tạo bằng cách tạo ra một thực thể của thư viện đó và truyền thực thể ứng dụng cho chúng dưới dạng một tham số. Nếu ứng dụng không tồn tại dưới dạng một biến toàn cục, quá trình khởi tạo các thư viện mở rộng của Flask có thể được thực hiện bằng một cách khác thông qua hai giai đoạn: Chúng ta vẫn tạo ra các thực thể của các thư viện này ở mức toàn cục như trước, nhưng không có thông số kèm theo. Quá trình này sẽ tạo ra các thực thể của các thư viện mở rộng không kết nối với ứng dụng. Khi thực thể ứng dụng được tạo ra trong hàm tạo ứng dụng, chúng ta phải gọi hàm init_app() từ các thực thể của các thư viện mở rộng để liên kết chúng với thực thể ứng dụng.

Các tác vụ khác trong quá trình khởi tạo không thay đổi, nhưng sẽ được dời vào hàm tạo ứng dụng thay vì trong phạm vi toàn cục. Nguyên tắc này cũng được áp dụng cho quá trình đăng ký các blueprint và cấu hình nhật ký hệ thống (logging). Cần lưu ý rằng chúng ta đã thêm mệnh đề not app.testing vào điều kiện để quyết định bật hay tắt các tác vụ hỗ trợ email và nhật ký hệ thống, nhờ đó quá trình ghi nhật ký sẽ được bỏ qua khi thực hiện các đơn vị kiểm tra (unit test). Biến app.testing sẽ có giá trị True khi thực hiện các đơn vị kiểm tra bởi vì biến môi trường TESTING được gán là True trong cấu hình ứng dụng.

Như vậy chúng ta sẽ sử dụng hàm tạo ứng dụng này như thế nào? Vị trí rõ ràng nhất của hàm này là tại điểm bắt đầu trong file myblog.py, hiện là nơi duy nhất mà ứng dụng tồn tại ở mức toàn cục. Một nơi nữa là test.py mà chúng ta sẽ thảo luận ở phần sau.

Như đã nói ở trên, hầu hết các tham chiếu đến app không còn sau khi chúng ta tạo ra các blueprint, nhưng vẫn còn một vài nơi trong mã nguồn còn sử dụng các tham chiếu này và chúng ta cần khắc phục. Ví dụ như các module app/models.py, app/translate.pyapp/main/routes.py đều có các tham chiếu này. Rất may là các lập trình viên phát triển Flask đã để ý đến vấn đề này và đã giúp các hàm hiển thị có thể truy cập thực thể ứng dụng mà không cần phải tham chiếu đến nó như cách chúng ta đã làm. Biến current_app trong Flask là một biến “ngữ cảnh” (context) đặc biệt mà Flask khởi tạo với ứng dụng trước khi nó gởi một yêu cầu. Trước đây chúng ta đã gặp một biến loại này là g và chúng ta đã dùng nó để lưu các thông tin bản địa hóa. Hai biến này, cùng với biến current_user từ thư viện Flask-Login và một số biến khác mà chúng ta vẫn chưa gặp là các biến đặc biệt bởi vì chúng làm việc như là các biến toàn cục, nhưng lại chỉ có thể truy cập được trong quá trình xử lý các yêu cầu, và chỉ trong các thread (luồng) đang xử lý chúng.

Việc thay thế biến app bằng current_app chấm dứt việc sử dụng biến toàn cục để tham chiếu đến thực thể ứng dụng. Đến đây, chúng ta có thể thay thế toàn bộ các tham chiếu đến app.config bằng current_app.config mà không làm ảnh hưởng đến ứng dụng.

Tuy nhiên vẫn còn một vấn đề với module app/email.py và chúng ta phải sử dụng một mẹo nhỏ để giải quyết:

app/email.py: Truyền thực thể ứng dụng cho một thread khác.

Trong hàm send_email(), thực thể ứng dụng được truyền vào một thread (luồng) chạy ở chế độ nền. Thread này đảm nhận việc gởi email độc lập với ứng dụng. Việc sử dụng current_app trực tiếp trong hàm send_async_email() được thread này gọi đến sẽ không có hiệu quả bởi vì biến current_app là một biến có liên quan đến ngữ cảnh (context-aware) và phụ thuộc vào thread đang xử lý các yêu cầu của client. Trong thread khác, biến này sẽ không được gán giá trị nào. Ngay cả khi chúng ta truyền biến này trực tiếp vào một thread khác dưới dạng một tham số cũng không có hiệu quả vì bản chất của nó là một đối tượng trung gian (proxy object) để phục vụ cho việc truy xuất thực thể ứng dụng. Vì vậy, dù chúng ta truyền current_app cho thread hay sử dụng trực tiếp nó cũng có kết quả như nhau. Cái chúng ta cần là thực thể ứng dụng thật sự nằm trong đối tượng proxy này và truyền nó vào tham số app. Lệnh current_app._get_current_object() sẽ giúp chúng ta làm điều này bằng cách trả về thực thể ứng dụng thực sự bên trong biến current_app, nhờ đó chúng ta có thể truyền nó vào cho thread mới.

Một module khác cũng gây khó khăn là app/cli.py. Nếu bạn còn nhớ, module này giúp chúng ta tạo các lệnh mở rộng cho flask để quản lý việc dịch ngôn ngữ. Biến current_app cũng không hoạt động với module này bởi vì các lệnh này được đăng ký lúc ứng dụng khởi động chứ không phải lúc các yêu cầu đang được xử lý – cũng là thời điểm duy nhất mà current_app được gán các giá trị ngữ cảnh thích hợp. Do đó, để loại bỏ tham chiếu đến app trong module này, chúng ta phải sử dụng một mẹo khác là di chuyển tất cả các lệnh mà chúng ta xây dựng trong module này vào bên trong một hàm gọi là register() và cho hàm này có một tham số là biến app.

app/cli.py: Đăng ký các lệnh tùy biến

Sau đó chúng ta sẽ gọi hàm register() từ myblog.py. Dưới đây là phiên bản hoàn chỉnh của myblog.py sau khi đã được điều chỉnh:

myblog.py: Module chính của ứng dụng sau khi được điều chỉnh.

Cải tiến mã kiểm tra ứng dụng (Unit test)

Như đã nói từ đầu, một trong những lý do quan trọng để chúng ta phải cải tiến mã ứng dụng là giúp cho mã kiểm tra có thể vận hành tốt hơn. Khi thực thi mã kiểm tra, chúng ta cần chắc chắn rằng ứng dụng sẽ được cấu hình thích hợp để không làm ảnh hưởng đến các tài nguyên được sử dụng trong quá trình phát  triển, ví dụ như cơ sở dữ liệu với các dữ liệu nháp.

Trong phiên bản hiện tại của tests.py, chúng ta đang sử dụng một mẹo nhỏ bằng cách cập nhật cấu hình sau khi nó đã được thực thể ứng dụng sử dụng. Đây là một kỹ thuật mạo hiểm vì không phải lúc nào các thay đổi này cũng có tác dụng vì đã quá trễ. Điều chúng ta muốn làm là chỉ định toàn bộ cấu hình kiểm tra trước khi nó được ứng dụng tham chiếu đến.

Vì vậy, chúng ta sẽ cập nhật hàm create_app() để nhận vào một tham số là một lớp cấu hình. Giá trị mặc định của tham số này sẽ là lớp Config được định nghĩa trong config.py. Nhưng chúng ta cũng có thể tạo ra một thực thể ứng dụng với một cấu hình khác một cách đơn giản là truyền một lớp mới vào hàm tạo ứng dụng. Sau đây là một ví dụ về lớp cấu hình để sử dụng cho các mã kiểm tra:

tests.py: Cấu hình kiểm tra

Trong đoạn mã trên, chúng ta tạo ra một lớp con của lớp Config và thay thế cấu hình cho SQLAlchemy để sử dụng cơ sở dữ liệu SQLite trong bộ nhớ thay vì trên đĩa cứng. Chúng ta cũng thêm một thuộc tính mới là TESTING và gán giá trị True cho nó – hiện tại chúng ta không cần đến thuộc tính này, nhưng nó có thể hữu dụng trong trường hợp ứng dụng cần xác định nó có đang thực thi trong chế độ kiểm tra hay không.

Nếu bạn còn nhớ, mã kiểm tra của chúng ta dựa trên các phương thức setUp()tearDown(). Các phương thức này được gọi tự động bởi framework kiểm tra và chịu trách nhiệm để khởi tạo và kết thúc các môi trường thích hợp cho quá trình kiểm tra. Chúng ta có thể sử dụng hai phương thức này để khởi tạo và kết thúc các ứng dụng độc lập với nhau trong mỗi bài kiểm tra:

tests.py: Tạo các ứng dụng cho mỗi bài kiểm tra

Ứng dụng mới sẽ được gán cho self.app, nhưng việc tạo ra một ứng dụng vẫn chưa đủ. Đơn cử như trong trường hợp của lệnh db.create_all() để tạo ra các bảng cho cơ sở dữ liệu. Thực thể db cần tham chiếu đến thực thể ứng dụng vì nó cần sử dụng URI của cơ sở dữ liệu trong app.config. Tuy nhiên khi sử dụng hàm tạo ứng dụng, chúng ta có thể có nhiều ứng dụng khác nhau, vậy làm sao để db có thể sử dụng đúng thực thể self.app mà chúng ta vừa tạo ra?

Đáp án cho câu hỏi này là ngữ cảnh ứng dụng (application context). Để làm rõ khái niệm này, chúng ta nhắc lại một chút về biến current_app mà chúng ta đã sử dụng với vai trò trung gian (proxy) để đại diện cho ứng dụng và giúp cho việc loại trừ việc sử dụng biến toàn cục. Biến này sẽ tìm một ngữ cảnh ứng dụng đang hoạt động trong thread hiện tại, nếu tìm được, nó sẽ sử dụng các thông tin về ứng dụng có trong đó. Nếu không tìm ra ngữ cảnh, sẽ không có cách nào để biết ứng dụng nào đang hoạt động, và trong trường hợp đó, current_app sẽ tạo ra một ngoại lệ (exception). Bạn có thể theo dõi ví dụ minh họa trong cửa sổ lệnh Python dưới đây, và lưu ý rằng cửa sổ lệnh này phải được bắt đầu bằng lệnh python3, bởi vì lệnh flask shell sẽ tự động kích hoạt ngữ cảnh ứng dụng cho bạn:

Chúng ta có thể thấy rõ những gì đang xảy ra. Trước khi gọi các hàm hiển thị, Flask sẽ lưu (push) một ngữ cảnh ứng dụng và qua đó, kích hoạt các biến như current_appg với các giá trị cần thiết. Sau khi một yêu cầu được hoàn tất, ngữ cảnh sẽ được xóa bỏ cùng với các biến kèm theo. Vì vậy, để lệnh db.create_all() có thể làm việc được trong phương thức setUp() của mã kiểm tra, chúng ta sẽ lưu một ngữ cảnh ứng dụng cho thực thể ứng dụng chúng ta vừa tạo ra. Nhờ đó, db.create_all() có thể sử dụng current_app.config để lấy các thông tin cấu hình của cơ sở dữ liệu. Sau đó, trong phương thức tearDown(), chúng ta sẽ xóa (pop) ngữ cảnh này để đưa ứng dụng trở lại trạng thái ban đầu.

Bạn cũng nên biết rằng ngữ cảnh ứng dụng là một trong hai ngữ cảnh được Flask sử dụng. Ngoài ngữ cảnh ứng dụng còn có ngữ cảnh yêu cầu (request context) để chứa các thông tin có liên quan đến yêu cầu từ trình duyệt đến máy chủ. Khi một ngữ cảnh yêu cầu được kích hoạt ngay trước khi một yêu cầu được xử lý, các biến requestsession của Flask cũng như biến current_user của Flask-Login sẽ bắt đầu có hiệu lực.

Các biến môi trường

Trong quá trình xây dựng ứng dụng từ đầu đến giờ, chúng ta sử dụng một số các tham số cấu hình phụ thuộc vào các biến môi trường. Các biến này gồm có khóa bí mật, các thông tin cho máy chủ email, địa chỉ của cơ sở dữ liệu và khóa của dịch vụ phiên dịch của Microsoft. Có lẽ bạn cũng nhận ra rằng điều này tương đối bất tiện vì chúng ta phải thiết lập lại các biến này mỗi khi chúng ta cần mở một cửa sổ lệnh mới.

Trong các ứng dụng có sử dụng nhiều biến môi trường, cách giải quyết phổ biến là chứa các biến này vào một file .env trong thư mục gốc của ứng dụng. Ứng dụng sẽ tham chiếu đến các biến này trong quá trình khởi động, và nhờ đó, chúng ta không phải thiết lập các biến này bằng tay.

Để hỗ trợ cho file .env, chúng ta sẽ sử dụng một gói Python là python-dotenv. Trước hết, chúng ta hãy cài đặt nó:

Bởi vì chúng ta sẽ lấy thông tin về các biến môi trường từ module config.py, chúng ta sẽ tham chiếu đến file .env trước khi tạo ra đối tượng Config để bảo đảm rằng các biến đã được gán các giá trị thích hợp trước khi đối tượng Config được khởi tạo:

config.py: Tham chiếu đến file .env có chứa các biến môi trường.

Từ bây giờ, bạn có thể tạo ra file .env và đặt tất cả các biến môi trường cần thiết cho ứng dụng vào file này. Cần nhớ là bạn không được đưa file này vào hệ thống quản lý mã nguồn (source control) vì nó có thể có các thông tin nhạy cảm như mật mã hay khóa truy cập các dịch vụ tính phí.

Chúng ta có thể đưa hầu hết các thông số cấu hình vào file .env ngoại trừ hai biến môi trường FLASK_APPFLASK_DEBUG vì ứng dụng cần đến hai biến này rất sớm trong quá trình khởi động, trước khi thực thể ứng dụng và đối tượng cấu hình hiện hữu.

Sau đây là một ví dụ về file .env có chứa một khóa bí mật, cấu hình để gởi email sử dụng một phần mềm máy chủ email cục bộ tại cổng 25 và không sử dụng xác thực người dùng, khóa cho các API của dịch vụ dịch thuật của Microsoft và sử dụng cấu hình mặc định của cơ sở dữ liệu:

Requirement.txt

Từ đầu đến giờ chúng ta đã cài đặt khá nhiều các thư viện hỗ trợ (hay gói) của Python trong môi trường ảo. Điều này có thể gây ra một vài rắc rối nếu bạn cần phải tạo ra môi trường cần thiết cho ứng dụng trên một máy khác nhưng lại không nhớ bạn phải cài đặt những gói nào. Biện pháp để giải quyết tình trạng này là tạo ra một file gọi là requirement.txt trong thư mục gốc của ứng dụng và liệt kê tất cả các các thư viện mà ứng dụng cần sử dụng kèm theo phiên bản của chúng trong file này. Việc tạo ra file này tương đối đơn giản:

Lệnh pip3 freeze sẽ tìm và liệt kê toàn bộ các thư viện được cài đặt trong môi trường ảo vào file requirements.txt với định dạng thích hợp. Sau đó, nếu bạn cần tạo môi trường ảo tương tự trên một máy khác, thay vì cài đặt từng thư viện, bạn có thể dùng lệnh sau:

Chúng ta sẽ tạm ngừng ở đây, hẹn gặp bạn trong phần tiếp theo.

Leave a Reply

Your email address will not be published. Required fields are marked *