Phát triển Web với CherryPy & Jinja2

Bài viết gốc: https://manhhomienbienthuy.bitbucket.io/2015/Aug/30/web-development-with-cherrypy-and-jinja2.html (đã xin phép tác giả 😄)

Trong bài viết này, tôi sẽ giới thiệu một framework để phát triển Web - CherryPy - một framework được viết cho Python. Tôi sẽ không đi sâu vào phân tích và so sánh với các framework khác và các ngôn ngữ khác và tại sao bạn nên sử dụng framework này. Bởi vì mỗi framework đều có những điểm mạnh điểm yếu riêng, không có framework nào là hoàn hảo cả. Mục đích lớn nhất của bài viết là học hỏi và giới thiệu một framework mới, từ đó chúng ta có thêm nhiều lựa chọn hơn khi muốn phát triển Web. Và tất nhiên, tùy vào mục đích của trang Web mà bọn có thể chọn được framework thích hợp nhất cho mình.

Giới thiệu CherryPy và Jinja2

CherryPy

CherryPy là một framework phát triển Web hướng đối tượng theo phong cách pythonic. CherryPy cho phép những người phát triển xây dựng các ứng dụng Web với cách thức hoàn toàn giống như xây dựng các ứng dụng Python hướng đối tượng khác. Điều này sẽ giúp giảm kích thước mã nguồn và thời gian phát triển, nếu như bạn đã có kinh nghiệm với Python.

Những thông tin về CherryPy bạn có thể tìm thấy trên trang chủ http://cherrypy.org/, những tài liệu và nhiều thông tin khác được đưa ra ở trong http://docs.cherrypy.org/en/latest/index.html. Ở những trang đó, bạn có thể thấy những ưu điểm nổi bật của CherryPy và lý do tại sao bạn nên dùng nó. Ở nội dung bài viết này, tôi sẽ không đi sâu vào phân tích những điều này.

Có một đặc điểm của CherryPy khác với rất nhiều các framework khác. CherryPy được xây dựng đơn giản gọn nhẹ, và công việc của nó cũng hết sức đơn giản, đó là nhận request từ người dùng (cụ thể là các trình duyệt Web) và gửi phản hồi. CherryPy không cung cấp bất kỳ một template engine nào để xây dựng các trang HTML. Và CherryPy cũng không cung cấp bất kỳ một ORM (Object Relation Mapper) nào để tương tác với cơ sở dữ liệu. Điều đó có nghĩa là bạn có thể tự do lựa chọn bất cứ một template engine nào, và bất cứ ORM nào được viết cho Python.

Có rất nhiều template engine khác nhau có thể sử dụng với CherryPy, bạn có thể tham khảo ở đây. Với nội dung bài viết này, tôi sẽ sử dụng Jinja2.

Jinja2

Jinja2 là một template engine cho Python với đầy đủ tính năng mà một template engine cần phải có với khả năng hỗ trợ Unicode, I18n, và cả khă năng chạy môi trường sandbox.

Dưới đây là một vài tính năng nổi bật của Jinja2

  • chạy sandbox
  • Có hệ thống escape HTML mạnh mẽ để phòng chống XSS
  • Có thể kế thừa, module hóa template
  • Được xây dựng để tối ưu hóa code Python
  • Tùy chọn cho phép dịch template head-in-time
  • Dễ dàng debug.
  • Cú phát cho phép dễ dàng config

Cài đặt

Để phát triển Web, trước hết bạn phải cài đặt các package cần thiết, cụ thể ở đây là CherryPy và Jinja2. Tôi nghĩ, tốt nhất chúng ta nên thiết lập môi trường ảo cho Python ở trên máy của chúng ta, điều đó sẽ giúp những cài đặt sau đây không ảnh hưởng gì đến hệ thống.

Lưu ý, trong bài viết này, tôi sẽ sử dụng Python 3. Với Python 2, việc cấu hình và mã nguồn sẽ khác đi một chút.

  • Khởi tạo môi trường ảo cho Python
    $ pyvenv venv
Nếu bạn sử dụng Ubuntu 14.04 thì Ubuntu cài đặt sẵn một bản `pyvenv` không đầy đủ, nên bạn không thể dùng lệnh trên được. Tham khảo cách làm ở [đây](http://askubuntu.com/questions/488529/pyvenv-3-4-error-returned-non-zero-exit-status-1) để fix lỗi này.
  • Activate Python trong môi trường ảo
    $ source venv/bin/activate
  • Cài đặt các package cần thiết
    $ pip install CheryPy
    $ pip install Jinja2

Vậy là đủ cho một ứng dụng Web đơn giản. Sau đây chúng ta sẽ đi vào cụ thể việc phát triển Web với CherryPy và Jinja2

Phát triển Web

Cấu trúc file và thư mục

Bởi vì CherryPy được xây dựng để việc phát triển Web giống như phát triển một app Python bình thường, nên không có một quy định cụ thể nào về việc sắp xếp và tổ chức các thư mục. Tuy nhiên, tôi đã tham khảo một project trên bitbucket (https://bitbucket.org/Lawouach/twiseless/src) và thấy đây là một cấu trúc tốt, rất dễ hiểu. Nhiều người phát triển Web bằng CherryPy cũng tham khảo cách tổ chức này. Về cơ bản một project sẽ có các file và thư mục sau:

  • conf: thư mục chứa các file config của app.
  • lib: thư mục chứa các thư viện, tools và cả các models dùng cho app, một phần của nó giống như M trong MVC
  • public: thư mục chứa các tệp tĩnh như css, js, ...
  • template: thư mục chứa các file template, nó chính là phần V trong MVC.
  • webapp: thư mục chưa file chính của app, nó đóng vai trò là phần C trong MVC.
  • server.py: file được gọi chính để khởi tạo server Web.

Nội dung của file server.py như sau:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import cherrypy
import tempfile

import webapp.app

class Server(object):

    def __init__(self):
        self._set_basic_config()
        self._setup()
        self._add_app()

    def _set_basic_config(self):
        self.base_dir = os.path.dirname(os.path.abspath(__file__))
        self.conf_path = os.path.join(self.base_dir, "conf")
        log_dir = os.path.join(self.base_dir, "logs")
        if not os.path.exists(log_dir):
            os.mkdir(log_dir)
        session_dir = os.path.join(tempfile.gettempdir(), "sessions")
        if not os.path.exists(session_dir):
            os.mkdir(session_dir)

    def _setup(self):
        # Update the global settings for the HTTP server and engine
        cherrypy.config.update(os.path.join(self.conf_path, "server.conf"))

    def _add_app(self):
        cherrypy.tree.mount(
            webapp.app.VpyeuBank(),
            "/",
            os.path.join(self.conf_path, "app.conf")
        )

    def run(self):
        engine = cherrypy.engine
        if hasattr(engine, "signal_handler"):
            engine.signal_handler.subscribe()

        if hasattr(engine, "console_control_handler"):
            engine.console_control_handler.subscribe()

        engine.start()
        engine.block()

if __name__ == "__main__":
    Server().run()

server.py sẽ là file chính được gọi khi khởi tạo server Web. Nhiệm vụ của nó là khởi tạo các cấu hình cần thiết của hệ thống để app có thể chạy được. Cụ thể trong trường hợp trên là khởi tạo các thư mục lưu sessions, logs, gọi các app. Các giá trị khởi tạo này hoàn toàn có thể cấu hình được. Và các file cấu hinhf được lưu trong thư mục conf. Trong ví dụ này, tôi lưu 2 file server.conf để lưu cấu hình hệ thống và app.conf để lưu các cấu hình riêng cho app.

Nội dung file server.conf như sau:

[global]
server.socket_host: "0.0.0.0"
server.socket_port: 8080
server.thread_pool: 10
engine.autoreload.on: False
log.error_file: "./logs/error.log"
log.access_file: "./logs/access.log"

File config này được viết theo cú pháp của CherryPy, bạn có thể tham khảo cú pháp đó trong trong Web của CherryPy. Ở đây, tôi đã cấu hình một vài thông số cơ bản như Web server sẽ chạy ở cổng 8080 và các giá trị về thread pool, các file log, ...

Dưới đây là nội dung file app.conf

[/]
tools.staticdir.root: os.path.normpath(os.path.abspath(os.path.curdir))
tools.staticfile.root: os.path.normpath(os.path.abspath(os.path.curdir))
tools.encode.on: True
tools.gzip.on: True
tools.gzip.mime_types: ['text/html', 'text/plain', 'application/json', 'text/javascript', 'application/javascript']
tools.sessions.on: True
tools.sessions.timeout: 60
tools.sessions.storage_type: "file"
tools.sessions.storage_path: os.path.join(tempfile.gettempdir(), "sessions")

[/static]
tools.etags.on: True
tools.staticdir.on: True
tools.staticdir.dir: "public"

Những nội dung cụ thể của file này, tôi sẽ lần lượt giới thiệu sau đây.

Các file tĩnh (css, js,...)

Các file css, js, và file ảnh, ... là các file tĩnh với một ứng dụng Web. Nó sẽ được load cùng với trang Web và không thay đổi nội dung trong suốt quá trình thao tác với trang Web đó. CherryPy cung cấp công cụ riêng để load các file này. Với ví dụ của chúng ta, các file tĩnh đặt trong thư mục public, và tôi muốn CherryPy sẽ load các file này với path tương tự như /static/app.js. Để làm được điều này, chỉ cần cấu hình trong file app.conf là đủ:

[/]
tools.staticdir.root: os.path.normpath(os.path.abspath(os.path.curdir))
tools.staticfile.root: os.path.normpath(os.path.abspath(os.path.curdir))

[/static]
tools.etags.on: True
tools.staticdir.on: True
tools.staticdir.dir: "public"

Với cấu hình trên, mỗi khi Web server muốn load một file tĩnh, ví dụ /static/app.js thì CherryPy sẽ tìm file tương ứng là public/app.js để load ra. Tương tự với các file css và file ảnh.

Khởi tạo app

Trong ví dụ của tôi, app controller sẽ được lập trình trong file webapp/app.py. Trước hết, cần khởi tạo app với việc import các package cần thiết và khởi tạo template engine

# -*- coding: utf-8 -*-

import cherrypy
import jinja2

__all__ = ["VpyeuBank"]

class VpyeuBank(object):

    def __init__(self):
        _tmp_loader = jinja2.FileSystemLoader(searchpath="template")
        self.tmp_env = jinja2.Environment(loader=_tmp_loader)

Như vậy template engine đã được khởi tạo, nó sẽ tìm các file trong thư mục template khi render.

Viết app

@cherrypy.expose
    def index(self):
        template = self.tmp_env.get_template("index.jinja")
        return template.render()

Đây là hàm index, nó sẽ nhận request khi truy cập vào địa URL http://locahost:8080/ và trả về là nội dung đã được render của file index.jinja

Jinja2 không yêu cầu tên file có chứa phần mở rộng như thế nào, nên ở đây tôi sử dụng tên file là *.jinja. Jinja2 cho phép kế thừa và module hóa các template, nên bạn có thể chia các file template thành các file nhỏ và thư mục giống như các template engine khác. Ở ứng dụng của tôi, tôi sẽ sử dụng 1 file layout.jinja để xây dựng nên layout chính của trang Web, và các file khác ứng với các route khác, sẽ kế thừa file layout này và điền nội dung vào phần còn thiếu. Nội dung file layout.jinja như sau:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <!-- phần này sẽ load các file icon, js, css, và cá thẻ meta khác -->

    <title>{% block title %}{% endblock %}
      - Viet Phuong yeu
    </title>
  </head>

  <body>
    <nav>
      <!-- phần này sẽ là navbar -->
    </nav>

    {% block content %}{% endblock %}

    <footer class="page-footer">
      <!-- phần này là footer -->
    </footer>
  </body>
</html>

Trên đây là nội dung của file layout. File này đã định nghĩa trước các block, ví dụ {% block content %}{% endblock %} sẽ là phần nội dung chính cuả trang Web. Sau này khi kế thừa, các file khác sẽ điền nội dung vào block này. Tương tụ với cá block khác. Nếu bạn sử dụng layout khác, bạn hoàn toàn có thể viết lại layout này, và define nhiều block hơn nếu bạn cần.

Nội dung file index.jinja như sau. File này sẽ kế thừa layout và điền nội dung vào content.

{% extends "layout.jinja" %}

{% block title %}Top page{% endblock %}

{% block content %}
<div class="container">
  <!-- phần này là nội dung cần điền -->
</div>
{% endblock %}

Cú pháp của Jinja2 được giới thiệu rất cụ thể ở http://jinja.pocoo.org/docs/dev/templates/. Bạn có thể đọc và tham khảo để sau này sử dụng cho project cuả bạn.

Và với trang Web này, bạn thể sử dụng các CSS framework và các thư viện Javascript để viết ứng dụng Web của mình trở nên sinh động và dễ sử dụng.

Demo

Tôi có 1 demo trang Web viết bằng CherryPy và jinja2: http://vpyeu-bank.herokuapp.com/

Ở demo này, ngoài CherryPy và Jinja2 đã giới thiệu ở trên, tôi sử dụng materializecss framework để trang trí cho trang Web của mình.

Kết luận

CheryPy là một framework rất hay để phát triển một app trên nền Web. Bài viết mới giới thiệu những khái niệm và thao tác cơ bản khi phát triển Web bằng CherryPy và Jinja2.

Trong các bài viết sau, tôi sẽ giới thiệu một số nội dung nâng cao hơn, ví dụ như sử dụng i18n với Jinja2, deploy Web với nginx, v.v...