Trong phần này, chúng ta sẽ cùng tìm hiểu về khái niệm Template trong Flask.
Để 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:
- Phần 1: Hello, World
- Phần 2: Tìm hiểu về template (bài viết này)
- Phần 3: Tìm hiểu về Web Forms
- Phần 4: Sử dụng cơ sở dữ liệu
- Phần 5: Xử lý đăng nhập
- Phần 6: Hồ sơ cá nhân và ảnh đại diện
- Phần 7: Xử lý lỗi
- Phần 8: Tạo chức năng follower
- Phần 9: Phân trang
- Phần 10: Hỗ trợ email
- Phần 11: Nâng cấp giao diện
- Phần 12: Xử lý thời gian
- Phần 13: Hỗ trợ đa ngôn ngữ
- Phần 14: Sử dụng Ajax
- Phần 15: Tinh chỉnh cấu trúc ứng dụng
- Phần 16: Hỗ trợ tìm kiếm
- Phần 17: Triển khai ứng dụng trên Linux
- Phần 18: Triển khai ứng dụng với Heroku
- Phần 19: Triển khai ứng dụng với Docker
- Phần 20: JavaScript nâng cao
- Phần 21: Thông báo cho người sử dụng
- Phần 22: Tìm hiểu về tác vụ nền
- Phần 23: Xây dựng API
Bạn có thể truy cập mã nguồn cho phần này tại GitHub.
Để ôn lại, chúng ta nhớ rằng sau khi hoàn tất Phần 1, bạn đã có được ứng dụng Web đầu tiên với cấu trúc như sau:
1 2 3 4 5 6 |
myblog\ myenv\ app\ __init__.py routes.py myblog.py |
Và để thực thi chương trình bạn cần thiết lập biến môi trường FLASK_APP=myblog.py
cho phiên làm việc của bạn và sau đó chạy lệnh flask run
. Sau lệnh này, Flask sẽ khởi động một Web server đơn giản và cho phép bạn truy cập ứng dụng bằng cách nhập URL http://localhost:5000/ vào thanh địa chỉ của trình duyệt.
Trong phần này, bạn sẽ tiếp tục phát triển ứng dụng mà bạn đã viết ở phần trước. Chúng ta sẽ học cách để tạo ra các trang Web phức tạp hơn với nhiều thành phần động. Nếu bạn cảm thấy chưa hiểu rõ về ứng dụng hoặc cách phát triển ứng dụng mà chúng ta đã thực hiện trong phần trước, hãy ôn lại trước khi chúng ta bắt đầu.
Template là gì?
Chúng ta muốn trang chủ trong ứng dụng blog của chúng ta sẽ trình bày một thông điệp chào mừng người sử dụng. Để không làm phức tạp vấn đề, chúng ta tạm thời bỏ qua khái niệm người dùng (user). Thay vào đó, chúng ta sẽ giả lập (mock) các đối tượng người dùng bằng dictionary trong Python như sau:
1 |
user = {'username': 'Thai'} |
Tạo ra các đổi tượng giả lập là một kỹ thuật rất hữu ích khi bạn muốn tập trung vào một phần nào đó của ứng dụng và không phải lo đến các thành phần khác chưa được xây dựng xong. Vì chúng ta đang tập trung vào vấn đề kiến thiết trang chủ cho ứng dụng và không muốn phân tâm vì chưa thiết lập hệ thống người sử dụng, chúng ta giả lập đối tượng người dùng.
Hiện thời, hàm hiển thị trong ứng dụng trả về một chuỗi đơn giản. Điều đó chưa đủ, chúng ta muốn nâng cấp chuỗi trả về này thành một trang HTML hoàn chỉnh, ví dụ như sau:
app/routes.py: Trả về chuỗi HTML từ hàm hiển thị
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from app import app @app.route('/') @app.route('/index') def index(): user = {'username': 'Thai'} return ''' <html> <head> <title>Home Page - Myblog</title> </head> <body> <h1>Hello, ''' + user['username'] + '''!</h1> </body> </html>''' |
Nếu bạn còn chưa quen thuộc với HTML, bạn nên đọc thêm về HTML trên Wikipedia.
Hãy cập nhật hàm hiển thị như trên và chạy thử chương trình để thấy kết quả.
Chắc bạn cũng đồng ý là cách trả về chuỗi HTML cho trình duyệt như trên không phải là giải pháp tốt. Bạn hãy thử tưởng tượng xem mã trong hàm hiển thị trên sẽ trở nên phức tạp như thế nào khi ứng dụng hoàn tất và liên tục nhận các bài viết với các nội dung khác nhau từ người dùng. Ứng dụng cũng sẽ phải có nhiều hàm hiển thị hơn để liên kết với các URL khác. Vì vậy, nếu lúc nào đó chúng ta cần thay đổi cách trình bày của trang Web, chúng ta phải cập nhật mã HTML trong từng hàm hiển thị. Rõ ràng đây không phải là cách hay khi ứng dụng của chúng ta trở nên phức tạp và có nhiều nội dung động hơn.
Vì vậy, sẽ tốt hơn nhiều nếu chúng ta chia phần trình bày (presentation) và phần logic của chương trình thành các thành phần tách biệt. Nếu làm được như vậy, bạn có thể nhờ người thiết kế Web để tạo ra các trang Web thật hấp dẫn, còn bạn thì có thể viết mã ứng dụng với Python hoàn toàn độc lập với họ.
Template cho phép chúng ta thực hiện việc phân tách giữa phần trình bày và logic của ứng dụng. Trong Flask, các template được viết trong các file khác nhau và lưu trong thư mục con templates của gói ứng dụng. Do đó, để bắt đầu với template, hãy đảm bảo rằng bạn đang ở trong thư mục myblog và dùng lệnh sau để tạo ra thư mục chứa các template:
1 |
(myenv) $ mkdir app/templates |
Dưới đây là template đầu tiên với chức năng tương tự như đoạn mã HTML được hàm hiển thị index()
trả về ở trên. Bạn hãy lưu lại file này trong thư mục app/templates/index.html:
app/templates/index.html: Template cho trang chủ
1 2 3 4 5 6 7 8 |
<html> <head> <title>{{ title }} - Myblog</title> </head> <body> <h1>Hello, {{ user.username }}!</h1> </body> </html> |
Đây hầu như là một trang Web với mã HTML tiêu chuẩn. Điểm khác nhau duy nhất giữa mã HTML thông thường và đoạn mã trên đây là vài vùng chứa (placeholder) cho các nội dung động nằm giữa hai dấu ngoặc nhọn {{..}}
. Các placeholder này đại diện cho các nội dung có thể được thay đổi vào thời gian chạy (runtime).
Như vậy, vì chúng ta đã chuyển phần trình bày vào một HTML template, hàm hiển thị có thể được đơn giản hóa như sau:
app/routes.py: Dùng hàm render_template()
1 2 3 4 5 6 7 8 |
from flask import render_template from app import app @app.route('/') @app.route('/index') def index(): user = {'username': 'Thai'} return render_template('index.html', title='Home', user=user) |
Trông tốt hơn nhiều phải không? Bạn hãy thử chạy để thấy template làm việc thế nào. Khi trang Web hiển thị trên trình duyệt, bạn cũng nên xem mã HTML được sinh ra và so sánh nó với mã ban đầu của template.
Quá trình chuyển đổi template thành một trang HTML được gọi là quá trình kết xuất hay kiến tạo (render). Để kết xuất template, chúng ta phải tham chiếu đến một hàm trong Flask gọi là render_template()
. Hàm này nhận vào tên file và một danh sách các tham số của template và trả về template đó, nhưng thay thế các vùng chứa (placeholder) bằng các giá trị tương ứng.
Hàm render_template()
sẽ gọi đến Jinja2 – là một chương trình (engine) cho template – có sẵn trong Flask. Jinja2 sẽ thay thế các đoạn mã trong các khối {{ ... }}
bằng các giá trị tương ứng từ các tham số được cung cấp khi gọi hàm render_template()
.
Các lệnh điều kiện
Đến đây, bạn đã thấy được làm thế nào Jinja2 thay thế các placeholder bằng các giá trị thực trong quá trình kết xuất, nhưng đó chỉ là một trong số nhiều tác vụ mà Jinja2 có thể làm với các template. Ví dụ như template cũng hỗ trợ cho các lệnh điều khiển (control statement) nẳm trong các khối {% ... %}
. Sau đây là phần cập nhật của file index.html có sử dụng một lệnh điều kiện:
app/templates/index.html: Lệnh điều kiện trong template
1 2 3 4 5 6 7 8 9 10 11 12 |
<html> <head> {% if title %} <title>{{ title }} - Myblog</title> {% else %} <title>Welcome to Myblog!</title> {% endif %} </head> <body> <h1>Hello, {{ user.username }}!</h1> </body> </html> |
Với sự thay đổi nhỏ này, template của chúng ta thông minh hơn một chút. Nếu hàm hiển thị không truyền một giá trị cho placeholder title
thì nó sẽ dùng một giá trị mặc định thay vì để trống. Bạn có thể thử để xem lệnh điều kiện làm việc thế nào bằng cách xóa tham số title
khi gọi hàm hiển thị render_template()
.
Vòng lặp (Loop)
Một trong những chức năng quan trọng của blog là cho phép người sử dụng có đăng nhập xem những bài viết gần đây, chúng ta sẽ cập nhật mã chương trình để làm điều này.
Chúng ta lại đi đường tắt một lần nữa và sử dụng mẹo để tạo các đối tượng giả lập cho người dùng và các bài viết:
app/routes.py: Các đối tượng giả lập cho bài viết trong hàm hiển thị
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
from flask import render_template from app import app @app.route('/') @app.route('/index') def index(): user = {'username': 'Thai'} posts = [ { 'author': {'username': 'Nguyen'}, 'body': 'Flask de hoc qua phai khong?' }, { 'author': {'username': 'Long'}, 'body': 'Lap trinh Web that thu vi!' } ] return render_template('index.html', title='Home', user=user, posts=posts) |
Chúng ta sẽ dùng list trong Python để tạo cấu trúc dữ liệu cho các bài viết, trong đó mỗi một thành phần của list là một dictionary gồm có các trường author
và body
. Khi chúng ta xây dựng mã thật sự cho các đối tượng này, chúng ta sẽ sử dụng các tên lớp và trường giống như trong các đối tượng giả lập. Nhờ đó, chúng ta không cần phải lo về các lỗi có thể phát sinh với mã thật vì tên đối tượng/trường không đúng.
Chúng ta cần giải quyết một vấn đề ở đây: trong mã template ở trên, chúng ta đã định nghĩa 2 đối tượng “bài viết”. Tuy vậy, trong thực tế, danh sách bài viết có thể có số bài viết bất kỳ. Số lượng bài viết hiển thị phải do hàm hiển thị quyết định chứ không phải template. Một cách khái quát, template chỉ nên nhận số lượng bài viết từ hàm hiển thị và hiển thị chúng chứ không nên quyết định có bao nhiêu bài viết. Làm sao chúng ta có thể làm điều này?
Rất may là Jinja2 có cách giải quyết vấn đề bằng cách dùng vòng lặp for
:
app/templates/index.html: sử dụng vòng lặp for trong template
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<html> <head> {% if title %} <title>{{ title }} - Myblog</title> {% else %} <title>Welcome to Myblog</title> {% endif %} </head> <body> <h1>Hi, {{ user.username }}!</h1> {% for post in posts %} <div><p>{{ post.author.username }} : <b>{{ post.body }}</b></p></div> {% endfor %} </body> </html> |
Tốt hơn nhiều phải không? Template mới này cho phép hiển thị số bài viết tùy ý. Bạn hãy thử chạy chương trình và xem kết quả nhé.
Thừa kế template
Hầu hết các ứng dụng Web đều có một thanh định hướng ở đầu trang bao gồm một số URL thường sử dụng như là URL để cập nhật hồ sơ cá nhân, đăng nhập, rời khỏi … Chúng ta cũng có thể thêm thanh điều hướng vào template index.html với một ít mã HTML. Tuy nhiên, khi ứng dụng của chúng ta có thêm nhiều trang Web nữa thì chúng ta sẽ phải thêm mã này vào các trang mới. Điều này đồng nghĩa với việc chúng ta có vài bản sao của phần mã HTML cho thanh định hướng và phải bảo đảm rằng chúng hoạt động giống nhau. Đây là điều nên tránh trong lập trình cũng như việc viết cùng một đoạn mã trong các hàm/lớp khác nhau.
Jinja2 có một cơ chế cho phép việc kế thừa các template và sẽ giúp chúng ta giải quyết vấn đề này. ĐIểm cốt yếu là bạn sẽ chuyển những đoạn mã có thể được lặp lại trong các template vào một template cơ bản. Và các template có các đoạn mã lặp lại này sẽ kế thừa từ template cơ bản đó. Khi chạy, các template kế thừa này vẫn hiển thị chính xác các nội dung từ các đoạn mã được lặp lại này (vì chúng được thừa kế từ template cơ bản), nhưng khi viết mã, bạn sẽ không cần phải sao chép các đoạn mã này vào các template kế thừa nếu chúng đã có trong template cơ bản. Điều này cũng tương tự như tính kế thừa của các ngôn ngữ lập trình đối tượng.
Để thực hiện việc kế thừa, chúng ta sẽ tạo ra một template mới được gọi là base.html có chứa mã HTML của một thanh định hướng đơn giản và logic cho tựa của trang. Chúng ta sẽ lưu template này vào file app/templates/base.html:
app/templates/base.html: Template cơ bản với mã HTML cho thanh định hướng
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<html> <head> {% if title %} <title>{{ title }} - Myblog</title> {% else %} <title>Welcome to Myblog</title> {% endif %} </head> <body> <div>Myblog: <a href="/index">Home</a></div> <hr> {% block content %}{% endblock %} </body> </html> |
Trong template này, chúng ta dùng lệnh điều khiển block
để định nghĩa nơi mà các template kế thừa sẽ hiển thị nội dung. Tên của các block phải là duy nhất (unique) để các template kế thừa có thể xác định sẽ hiển thị nội dung nào.
Sau khi đã có template cơ bản, chúng ta có thể đơn giản hóa template index.html bằng cách làm cho nó kế thừa từ template base.html:
app/templates/index.html: Kế thừa từ template cơ bản base.html
1 2 3 4 5 6 7 8 |
{% extends "base.html" %} {% block content %} <h1>Hi, {{ user.username }}!</h1> {% for post in posts %} <div><p>{{ post.author.username }}: <b>{{ post.body }}</b></p></div> {% endfor %} {% endblock %} |
Bởi vì template base.html sẽ đảm nhiệm việc trình bày cấu trúc tổng quát của các Web trong ứng dụng, chúng ta có thể xóa hầu hết các đoạn mã từ index.html ngoại trừ phần nội dung. Câu lệnh extends
thiết lập mối liên hệ thừa kế giữa hai template index.html và base.html, nhờ đó Jinja2 biết khi cần phải kết xuất index.html thì nó phải kết xuất template này bên trong base.html. Hai template này có cùng một block với tên gọi là content
cho phép Jinja2 biết khi cần trình bày nội dung bên trong block này thì nó phải dùng mã từ index.html thay vì mã của base.html.
Với cấu trúc mới này, nếu chúng ta cần thêm các template mới vào ứng dụng, chúng ta chỉ cần cho chúng kế thừa (extend) từ template base.html. Nhờ đó, chúng ta có thể bảo đảm rằng mọi trang Web trong ứng dụng của chúng ta có cùng một kiểu hiển thị mà không cần phải viết mã trùng lặp.
Hãy xem kết quả cuối cùng trước khi chúng ta chấm dứt phần này:
Chúng ta sẽ kết thúc ở đây, hẹn gặp lại bạn trong phần tiếp theo.
cảm ơn anh, những bài viết của anh rất hay và chuyên sâu, phong cách viết rõ ràng, hàn lâm, khi đọc em thấy như đang đọc một quyển sách hay vậy, hiện tại e đang thực hành theo loạt bài viết của a về Flask, nếu có gì không rõ mong a hướng dẫn thêm. Ngoài ra nếu a có thời gian rảnh mong rằng a viết thêm một loạt các bài viết khác. thân ái!
Phần 2 này mình bị báo lỗi:
name ‘render_template’ is not defined
Mình phải thêm dòng sau vào file routes.py:
from flask import render_template
và move folder templates vào folder app:
app/
templates/
index.html