người hướng dẫn. [00. 01] Nếu bạn bắt đầu viết html mà máy chủ web của bạn trả về dưới dạng một chuỗi trong Python, thì bạn sẽ bắt đầu ghét cuộc sống và lựa chọn cuộc sống của bạn khá nhanh chóng
[00. 08] Một trong những điều chúng ta có thể làm trong Flask là tạo thư mục này có tên là Mẫu. Trong thư mục Mẫu đó, chúng ta có thể tạo một tệp html như Xin chào. html
[00. 20] Điều này cho phép chúng tôi sử dụng trình chỉnh sửa của mình hoặc bất kỳ html nào được cung cấp cho chúng tôi và chỉnh sửa tệp html đó với đầy đủ lợi thế của tô sáng cú pháp và tự động hoàn thành, tất cả những nội dung tuyệt vời mà bạn nhận được
[00. 31] Html này sẽ được hiển thị dưới dạng mẫu Jinja2, có nghĩa là chúng ta có thể tận dụng tối đa tất cả các tính năng của Jinja2. Ví dụ: các câu lệnh điều kiện để kiểm tra xem một biến được gọi tên có tồn tại không
[00. 45] Nếu tên tham số tồn tại, chúng tôi có thể hiển thị giá trị của tham số đó trong html của mẫu của chúng tôi. Nếu tham số tên không có ở đó, chúng tôi có thể thực hiện một hành động hoàn toàn khác và sau đó chúng tôi có thể đóng logic điều kiện của mình trong mẫu
[01. 03] Quay lại logic ứng dụng của chúng tôi. Tôi sẽ tạo một trình trang trí tuyến đường mới đáp ứng /hello. không có tham số ở cuối của nó
[01. 15] Để phù hợp với tên tham số mà chúng tôi sử dụng trong mẫu Jinja của mình, tôi sẽ đổi tên người dùng thành tên tham số tại đây. Bây giờ, thay vì trả về chuỗi, tôi sẽ trả về mẫu kết xuất và cung cấp cho nó tên của tệp mẫu để trả về cũng như bất kỳ tham số nào mà mẫu sẽ có quyền truy cập vào
[01. 36] Để điều đó hoạt động, từ Flask, chúng ta cần nhập chức năng mẫu kết xuất. Nếu tôi lưu nó và sau đó đi đến xin chào. không có tham số bổ sung nào được đính kèm, điều đó sẽ nổ tung vì chúng ta đã gọi hàm hello nhưng không bao gồm tham số bắt buộc của tên
[01. 56] Chúng ta có thể giải quyết vấn đề đó bằng cách đặt tên giá trị mặc định là none. Bây giờ, khi chúng tôi gọi lại, chúng tôi nhận được "Xin chào, người không tên", được hiển thị bởi mẫu Jinja trong Xin chào của chúng tôi. tệp html
[02. 11] Nếu tôi gọi nó bằng cách thêm /will vào cuối, nó sẽ nhận ra rằng đó là một tham số tên và phản hồi bằng "Xin chào, Will. "
Flask là một khung web Python đang ngày càng phổ biến trong thế giới webdev. Sau khi đọc rất nhiều điều tuyệt vời về Flask, tôi quyết định tự mình dùng thử. Cá nhân tôi thấy việc thử nghiệm một khung mới rất khó khăn, bởi vì bạn phải tìm một dự án đủ phức tạp để tiết lộ những điều kỳ quặc của khung, nhưng không quá khó để lấy đi niềm vui của dự án. May mắn thay, trang web hỗ trợ PHP/Wordpress của tôi đã hoàn thành vai trò này khá tốt - trang web chỉ bao gồm nội dung tĩnh, trang liên hệ và blog. Nếu tôi không thể chuyển đổi một trang web đơn giản như vậy sang Flask, tôi sẽ biết ngay rằng tôi và Flask sẽ không thể tạo thành một nhóm ăn ý
cảnh báo spoiler. Bạn đang đọc nội dung này trên một trang web do Flask cung cấp. Vui lòng kiểm tra nguồn của trang web này trên Github
Bắt đầu
Nhiệm vụ đầu tiên trong tầm tay chỉ đơn giản là làm cho các trang chủ, về, dịch vụ và công việc hiển thị chính xác. Mặc dù nhiệm vụ chủ yếu bao gồm sao chép và dán, nhưng tôi đã có thể áp dụng ngay hầu hết các khái niệm từ hướng dẫn khởi động nhanh Flask. Cụ thể, tôi phải học
- Làm thế nào để tôi thực sự khởi động máy chủ?
- Các mẫu của tôi đi đâu?
- Tài sản tĩnh của tôi đi đâu?
- Làm cách nào để truy xuất nội dung của tôi từ bên trong các mẫu?
- Làm cách nào để định tuyến các yêu cầu?
Chắc chắn rồi, việc hiển thị các tài liệu HTML thuần túy khá đơn giản. Theo HTML thuần túy, ý tôi là không có bao gồm cho đầu trang, chân trang, v.v. Do đó, bước hợp lý tiếp theo là lấy các tài liệu HTML thuần túy này và KHÔ các vùng lặp lại
Đau ngày càng tăng. đã sẵn sàng?
Đến từ PHP, tôi đã quen với việc chỉ
{% extends "layout.html" %} {% block body %} Body content goes here! {% endblock body %}2-nhập nội dung tôi muốn và hoàn thành nó. Jinja2, công cụ tạo khuôn mẫu mặc định của Flask, hiện không hỗ trợ bao gồm. [Ghi chú. Hàm
{% extends "layout.html" %} {% block body %} Body content goes here! {% endblock body %}2. Tuy nhiên, tôi không thể lấy hàm bao gồm để trả về html không thoát. Có lẽ chức năng này đã bị hỏng trong một bản dựng gần đây]. Tuy nhiên, Jinja có. Để chứng minh, giả sử chúng ta có một mẫu gốc
{% extends "layout.html" %} {% block body %} Body content goes here! {% endblock body %}4
{% nguyên %}
My Website{% block body %}{% endblock body %}
{% rút lại %}
Và một mẫu con
{% extends "layout.html" %} {% block body %} Body content goes here! {% endblock body %}5 kế thừa mẫu gốc và điền vào các khối của nó {% raw %}
{% extends "layout.html" %} {% block body %} Body content goes here! {% endblock body %}
{% rút lại %}
Nếu chúng tôi gọi vào ngày
{% extends "layout.html" %} {% block body %} Body content goes here! {% endblock body %}5, đây là những gì chúng tôi sẽ nhận được
My WebsiteBody content goes here!
Tôi thích mẫu kế thừa mẫu của Jinja, nhưng việc thiếu câu lệnh bao gồm có thể dễ dàng dẫn đến các mẫu gốc khổng lồ
Rất may, tôi đã tìm thấy một cách giải quyết đầy đủ cho việc thiếu câu lệnh bao gồm. Jinja cho phép bạn xác định, về cơ bản là các chức năng được sử dụng trong các mẫu để hiển thị nội dung nào đó. Tôi đã xác định một macro có tên là
{% extends "layout.html" %} {% block body %} Body content goes here! {% endblock body %}7 chỉ trả về HTML cho điều hướng. Tôi đã làm điều tương tự cho
{% extends "layout.html" %} {% block body %} Body content goes here! {% endblock body %}8,
{% extends "layout.html" %} {% block body %} Body content goes here! {% endblock body %}9 và bất kỳ phần nào khác được lặp lại trên toàn bộ trang web. Lấy chân trang của chúng tôi, ví dụ
{% nguyên %}
{% macro footer[] %}{% endmacro %}hi@vertstudios.com | @vertstudios
{% rút lại %}
Sau đó, từ
My Website0, tôi có thể gọi macro chân trang. Flask yêu cầu sử dụng bộ lọcBody content goes here!
My Website1 nếu bạn muốn hiển thị chuỗi trả về. Do đó, macroBody content goes here!
My Website2 có thể được gọi từ macroBody content goes here!
My Website3 mà không cần chỉ địnhBody content goes here!
My Website4. Nhưng vìBody content goes here!
My Website0 là thứ đang được hiển thị trực tiếp, nên chúng tôi cần chỉ địnhBody content goes here!
My Website1 mỗi khi chúng tôi gọi macroBody content goes here!
Vì vậy,
My Website0 cuối cùng trông giống nhưBody content goes here!
{% nguyên %}
{% from "sections" import meta_and_css, nav, javascripts, footer %} {{ meta_and_css[g, title]|safe }}{{ nav[g]|safe }}{{ javascripts[g]|safe }}{% block body %}{% endblock body %}{{ footer[]|safe }}
{% rút lại %}
Điều này không lý tưởng lắm, nhưng nó đã hoạt động;
Là 'Dũng cảm'
Flask không phải lúc nào cũng có giải pháp vượt trội cho vấn đề này. Trong thực tế, nó hiếm khi làm. Tuy nhiên, Flask đã cung cấp cho tôi tất cả các công cụ cần thiết để tạo ra bất cứ thứ gì tôi muốn. Theo một cách nào đó, tôi cảm thấy rằng chính Flask đang nói với tôi rằng "Joseph, bạn đủ thông minh để tự triển khai chức năng X. dũng cảm một lần. "
Đôi khi phát minh lại bánh xe là một bài tập bổ ích
Biểu mẫu liên hệ
Phương pháp được đề xuất để xử lý các biểu mẫu trong Flask là tiện ích mở rộng Flask-WTF. Flask-WTF có nhiều , nhưng nó không bao gồm xác thực số điện thoại. Không có gì to tát, bất kỳ thư viện biểu mẫu nào đáng giá đều có phương tiện để xác định các chức năng xác thực tùy chỉnh. WTForms trên thực tế có một cơ chế để triển khai các trình xác thực tùy chỉnh, nhưng tôi không thấy các ví dụ của chúng rất bắt mắt
def length[min=-1, max=-1]: message = 'Must be between %d and %d characters long.' % [min, max] def _length[form, field]: l = field.data and len[field.data] or 0 if l < min or max != -1 and l > max: raise ValidationError[message] return _length class MyForm[Form]: name = TextField['Name', [Required[], length[max=50]]]
Do đó, tôi đã sử dụng Flask-WTF để thu thập dữ liệu từ các trường biểu mẫu, nhưng tôi đã triển khai mô-đun xác thực của riêng mình, việc này chỉ mất khoảng một giờ
Việc gửi email thực tế được xử lý dễ dàng với mô-đun
My Website8 tích hợp sẵn của Python và GmailBody content goes here!
Blog
Tạo một blog đã trở thành "Xin chào, Thế giới" của các khung ứng dụng web. Mặc dù phần mềm viết blog Python tồn tại, nhưng có vẻ như sẽ mất nhiều thời gian hơn để tích hợp một nền tảng viết blog hiện có vào dự án Flask của tôi hơn là chỉ triển khai nền tảng viết blog của riêng tôi
Trong khoảng một năm qua, tôi đã mong mỏi có một nền tảng viết blog dựa trên hệ thống tệp. Viết bài của tôi trong VIM + Markdown thú vị hơn nhiều so với viết HTML trong VIM, chuyển HTML sang Wordpress, xem trước HTML, đảm bảo định dạng đúng và các bước khó chịu khác
Có một vài điều cụ thể tôi cần phải hoàn thành với blog
- Khả năng tạo nguồn cấp dữ liệu RSS
- URL của các bài đăng trên blog phải giống như trước đây
- phân trang
Không cần tạo nguồn cấp dữ liệu RSS, điều duy nhất tôi cần làm là định tuyến
My Website9 tới một hàm cố gắng hiển thị mẫu có tênBody content goes here!
{% macro footer[] %}0 và trả về 404 nếu không tìm thấy mẫu đó. Tuy nhiên, yêu cầu RSS buộc tôi phải suy nghĩ về cách lưu trữ dữ liệu meta cho mỗi bài đăng{% endmacro %}hi@vertstudios.com | @vertstudios
Tôi quyết định chọn một cấu trúc khá đơn giản, mặc dù không đẹp hay thông minh nhưng cũng đủ tốt cho tôi. Tôi tạo một thư mục trong
{% macro footer[] %}1 có tên là{% endmacro %}hi@vertstudios.com | @vertstudios
{% macro footer[] %}2. Trong thư mục đó, tôi tạo hai tệp.{% endmacro %}hi@vertstudios.com | @vertstudios
{% macro footer[] %}3 và{% endmacro %}hi@vertstudios.com | @vertstudios
{% macro footer[] %}4.{% endmacro %}hi@vertstudios.com | @vertstudios
{% macro footer[] %}3 chỉ chứa một vài biến cho siêu dữ liệu [ngày đăng, đoạn trích, v.v.] và nội dung. html có nội dung bài đăng{% endmacro %}hi@vertstudios.com | @vertstudios
Để tránh phải phân tích tất cả các tệp siêu dữ liệu để tìm ra thứ tự của các bài đăng cho nguồn cấp RSS, tôi chỉ cần thêm tên thư mục vào đầu tệp có tên là
{% macro footer[] %}6. Một lần nữa, không phải là thứ đẹp nhất trên thế giới nhưng đó là một hệ thống mà tôi thấy dễ hiểu và dễ làm việc{% endmacro %}hi@vertstudios.com | @vertstudios
Nguồn cấp dữ liệu RSS và phân trang blog được xử lý trong cùng một bước. Tôi chạy tập lệnh có tên là
{% macro footer[] %}7 để tạo tệp rss xml và tệp phân trang. Sử dụng các tệp tĩnh được tạo bởi tập lệnh{% endmacro %}hi@vertstudios.com | @vertstudios
{% macro footer[] %}8 [đọc tập lệnh{% endmacro %}hi@vertstudios.com | @vertstudios
{% macro footer[] %}6 đã đề cập trước đó] để phân trang có ý nghĩa vì cách phân trang chỉ thay đổi bất cứ khi nào tôi cập nhật/tạo một câu chuyện{% endmacro %}hi@vertstudios.com | @vertstudios
Nhập bài đăng trên blog của tôi
Để nhập các bài đăng Wordpress của tôi vào cấu trúc mới này, tất cả những gì tôi phải làm là phân tích cú pháp tệp xml xuất của Wordpress bằng PyQuery và tạo các tệp có chức năng thư viện chuẩn. Tôi không bận tâm chuyển đổi các bài đăng từ Wordpress sang Markdown, vì vậy tất cả các bài đăng cũ bạn xem qua trong nguồn sẽ có HTML bình thường
Rắc rối về cấu trúc
Một trong những rắc rối lớn nhất mà tôi gặp phải trong suốt quá trình phát triển trang web là làm thế nào để nhập mọi thứ vào các tệp cụ thể. Câu trả lời StackOverflow này mô tả các bước cần thiết để nhập mô-đun trong thư mục cha/anh chị em khi chạy tập lệnh trực tiếp. Do đó, bất kỳ tập lệnh nào cần quyền truy cập vào đối tượng
{% from "sections" import meta_and_css, nav, javascripts, footer %} {{ meta_and_css[g, title]|safe }}0 [chứa nhiều phương thức và thuộc tính quan trọng về phiên bản ứng dụng], tôi phải chạy{{ nav[g]|safe }}{{ javascripts[g]|safe }}{% block body %}{% endblock body %}{{ footer[]|safe }}
{% from "sections" import meta_and_css, nav, javascripts, footer %} {{ meta_and_css[g, title]|safe }}1 hoặc chỉ cần di chuyển{{ nav[g]|safe }}{{ javascripts[g]|safe }}{% block body %}{% endblock body %}{{ footer[]|safe }}
{% macro footer[] %}7 vào cùng thư mục với{% endmacro %}hi@vertstudios.com | @vertstudios
{% from "sections" import meta_and_css, nav, javascripts, footer %} {{ meta_and_css[g, title]|safe }}3, điều này sẽ cho phép tôi . Tôi đã chọn cái sau vì sự lười biếng tuyệt đối, mặc dù tôi không đặc biệt hạnh phúc khi phải đưa ra lựa chọn như vậy. Tôi nên lưu ý rằng đây không phải là sự cố cụ thể của Flask mà là sự cố Python{{ nav[g]|safe }}{{ javascripts[g]|safe }}{% block body %}{% endblock body %}{{ footer[]|safe }}
Môi trường phát triển vs sản xuất
Trong quá trình phát triển, tất cả nội dung được cung cấp từ các tệp tĩnh. Trong sản xuất, tất cả tài sản được phục vụ từ S3. Môi trường được đặt bởi một env-var có tên là
{% from "sections" import meta_and_css, nav, javascripts, footer %} {{ meta_and_css[g, title]|safe }}5. Tôi cần một cách để cho các mẫu của mình biết "Này, bạn đang ở trong môi trường sản xuất, bạn nên liên kết với các tệp S3 thay vì các tệp tĩnh. ". Tôi quyết định tận dụng đối tượng{{ nav[g]|safe }}{{ javascripts[g]|safe }}{% block body %}{% endblock body %}{{ footer[]|safe }}
{% from "sections" import meta_and_css, nav, javascripts, footer %} {{ meta_and_css[g, title]|safe }}6 của Flask, một đối tượng toàn cục tồn tại trong bối cảnh yêu cầu. Bạn có thể tự do điền vào đó bất cứ thứ gì bạn muốn. Tôi đã tạo và sử dụng tài sản{{ nav[g]|safe }}{{ javascripts[g]|safe }}{% block body %}{% endblock body %}{{ footer[]|safe }}
{% from "sections" import meta_and_css, nav, javascripts, footer %} {{ meta_and_css[g, title]|safe }}7{{ nav[g]|safe }}{{ javascripts[g]|safe }}{% block body %}{% endblock body %}{{ footer[]|safe }}
Mặc dù điều này hoàn toàn phù hợp với nhu cầu của tôi, nhưng nó khiến các mẫu của tôi trông kém đẹp hơn một chút. Bất cứ khi nào tôi muốn gọi một macro tìm kiếm thuộc tính
{% from "sections" import meta_and_css, nav, javascripts, footer %} {{ meta_and_css[g, title]|safe }}7, tôi phải chuyển{{ nav[g]|safe }}{{ javascripts[g]|safe }}{% block body %}{% endblock body %}{{ footer[]|safe }}
{% from "sections" import meta_and_css, nav, javascripts, footer %} {{ meta_and_css[g, title]|safe }}6 cho nó. Ví dụ: đây là định nghĩa của macro{{ nav[g]|safe }}{{ javascripts[g]|safe }}{% block body %}{% endblock body %}{{ footer[]|safe }}
def length[min=-1, max=-1]: message = 'Must be between %d and %d characters long.' % [min, max] def _length[form, field]: l = field.data and len[field.data] or 0 if l < min or max != -1 and l > max: raise ValidationError[message] return _length class MyForm[Form]: name = TextField['Name', [Required[], length[max=50]]]0, một chức năng được sử dụng để hiển thị hình ảnh trên trang web
{% nguyên %}
{% macro img[g, file, alt="", class=""] %} {% endmacro %}
{% rút lại %}
Một ví dụ gọi đến macro. {% nguyên %}
{{img[g, "pointer.png", "", "pointer"]|safe }}
{% rút lại %}
Nó dài dòng hơn những gì tôi thực sự thích, nhưng nó hoàn thành công việc
Phát trực tiếp với Nginx và uWSGI
Bây giờ dự án của tôi đã hoàn thành khá nhiều, đã đến lúc đưa các tệp lên VPS Linode của tôi và giới thiệu dự án của tôi với mọi người. Tôi quyết định sử dụng Nginx với uWSGI, đơn giản vì cặp đôi này đã là chủ đề của nhiều hướng dẫn. Bản thân Flask có tài liệu về uWSGI. Tuy nhiên, tôi thấy các phần của tài liệu không đầy đủ
Từ tài liệu
# Given a flask application in myapp.py, use the following command: $ uwsgi -s /tmp/uwsgi.sock --module myapp --callable app
Nếu bạn muốn đưa một dự án lên một máy chủ trực tiếp, không có cách nào bạn phải làm nhiều việc như vậy. uWSGI có các tệp cấu hình ini mà tôi đã tận dụng tối đa. Bây giờ, thay vì thực hiện một lệnh dài dòng từ trình bao, tôi có thể thực hiện một cách đơn giản
$ uwsgi uwsgi.ini
def length[min=-1, max=-1]: message = 'Must be between %d and %d characters long.' % [min, max] def _length[form, field]: l = field.data and len[field.data] or 0 if l < min or max != -1 and l > max: raise ValidationError[message] return _length class MyForm[Form]: name = TextField['Name', [Required[], length[max=50]]]1 của tôi như sau
{% extends "layout.html" %} {% block body %} Body content goes here! {% endblock body %}0
Bây giờ cấu hình uWSGI của tôi nằm dưới sự kiểm soát phiên bản với phần còn lại của dự án của tôi
Đây là một lời giải thích cho các thông số
- dự án. Tên mô-đun mà bạn gọi là
def length[min=-1, max=-1]: message = 'Must be between %d and %d characters long.' % [min, max] def _length[form, field]: l = field.data and len[field.data] or 0 if l < min or max != -1 and l > max: raise ValidationError[message] return _length class MyForm[Form]: name = TextField['Name', [Required[], length[max=50]]]
2 - quỷ hóa. Nếu bạn muốn daemon hóa. Vì lý do nào đó, bạn phải chỉ định đường dẫn tệp nhật ký nếu không uWSGI sẽ đẩy mọi thứ xuống
def length[min=-1, max=-1]: message = 'Must be between %d and %d characters long.' % [min, max] def _length[form, field]: l = field.data and len[field.data] or 0 if l < min or max != -1 and l > max: raise ValidationError[message] return _length class MyForm[Form]: name = TextField['Name', [Required[], length[max=50]]]
3.def length[min=-1, max=-1]: message = 'Must be between %d and %d characters long.' % [min, max] def _length[form, field]: l = field.data and len[field.data] or 0 if l < min or max != -1 and l > max: raise ValidationError[message] return _length class MyForm[Form]: name = TextField['Name', [Required[], length[max=50]]]
4 không hiệu quả với tôi - bậc thầy. Boolean cho biết nếu bạn muốn có một quy trình tổng thể. Các quy trình tổng thể giúp quản lý uwsgi dễ dàng hơn nhiều
- chdir. Thay đổi thư mục. Về cơ bản, cho phép bạn giữ các tham số như "dự án" và virtualenv so với đường dẫn được cung cấp cho chdir
- ổ cắm. Đường dẫn ổ cắm Unix hoặc thông tin ổ cắm TCP
- wsgi. %[mô-đun chứa ứng dụng]. ứng dụng
- virtualenv. Đường dẫn đến virtualenv cho dự án
- pidfile. Tệp pid quy trình chính sẽ được ghi ở đâu
- cảm ứng tải lại. Chạm vào tệp này sẽ khiến các quy trình tải lại [thuận tiện. ]
- quy trình. Bạn muốn chạy bao nhiêu quy trình [không bao gồm quy trình chính]
- procname-tiền tố. Tiền tố tên quy trình. Hữu ích cho
def length[min=-1, max=-1]: message = 'Must be between %d and %d characters long.' % [min, max] def _length[form, field]: l = field.data and len[field.data] or 0 if l < min or max != -1 and l > max: raise ValidationError[message] return _length class MyForm[Form]: name = TextField['Name', [Required[], length[max=50]]]
5
Bây giờ, đối với các phần cấu hình nginx của tôi có liên quan đến uWSGI
{% extends "layout.html" %} {% block body %} Body content goes here! {% endblock body %}1
Những việc CẦN LÀM còn lại
Tôi cảm thấy mình đã làm đủ để xứng đáng loại bỏ trang PHP/Wordpress của mình và đặt trang Flask vào vị trí của nó. Mặc dù vậy, vẫn còn khá nhiều việc phải làm
Tôi dự định xuất các nhận xét Wordpress của mình sang Disqus. Tôi thích tương tác với những người đọc bài viết của tôi và tôi muốn giữ lại những nhận xét mà tôi đã nhận được
Tôi đã xoay sở để phá vỡ một số liên kết. Đặc biệt, tôi đã phá vỡ các liên kết đến các tệp php mà tôi đã ngu ngốc sử dụng để giới thiệu một số plugin jQuery của mình
Using prettify for syntax highlighting means I have to still use
tags in my documents, since the script requires a class of "prettyprint" on those tags. I'll either adjust prettyprint js and styles myself, find an alternative syntax highlighter, or do some processing of the markup to append the "prettyprint" class whenever it finds the
tag.
Vẫn cần thiết lập triển khai git. Tôi hiện chỉ lấy từ repo bitbucket riêng trên máy chủ của mình
Tôi cần tự động hóa quy trình xây dựng của mình. Một tập lệnh bash duy nhất sẽ nối tất cả các tệp CSS/JS của tôi, chạy YUICompressor và tải các tệp lên S3
Tôi thực sự cần triển khai thẻ mô tả meta cho bài đăng của mình. nhưng điều đó sẽ yêu cầu tôi phải thực sự viết chúng
Số lần hiển thị tổng thể
Nhìn chung, tôi khá hài lòng với Flask. Phần lớn, nó không theo cách của bạn và cho phép bạn xây dựng mọi thứ theo cách bạn muốn xây dựng chúng. Do đó, cấu trúc dự án của bạn có thể cẩu thả [đặc biệt là khi mới học Flask], nhưng đảm bảo bạn sẽ hiểu cách thức và lý do mọi thứ hoạt động