Hiển thị các bài đăng có nhãn Thymeleaf. Hiển thị tất cả bài đăng
Hiển thị các bài đăng có nhãn Thymeleaf. Hiển thị tất cả bài đăng

26 tháng 8, 2017

Tùy biến Thymeleaf

Gần đây, tôi có dịp được tham gia một dự án Java có sử dụng đến Thymeleaf, một trong số các công việc mà đội phát triển chúng tôi thực hiện đó là phải tùy biến các thẻ hoặc thuộc tính của template khi triển khai dự án.

1. Tại sao phải tùy biến thư viện Thymeleaf?


Thymeleaf là một thư viện rất mở, hầu hết các tính năng hướng tới người dùng của nó đều không được trực tiếp phát triển bên trong các thành phần cốt lõi, mà chỉ là đóng gói và thành phần hóa các tính năng này thành bộ tính năng gọi là dialect.

Thư viện sẽ cung cấp cho người dùng hai dialect có thể trực tiếp sử dụng đó là: StandardSpringStandard, nhưng bạn cũng có thể dễ dàng mở rộng, tạo ra thêm các dialect của riêng mình. Hãy cùng xem qua một số lí do có thể khiến bạn phải thực hiện việc này:

Kịch bản 1: thêm các tính năng không tồn tại trong các dialect mặc định

Giả sử, ứng dụng của bạn sử dụng dialectSpringStandard, và cần hiển thị cho người dùng cuối một thông báo có màu nền là màu xanh lam hoặc màu đỏ dựa vào role của người dùng đã đăng nhập vào hệ thống (ví dụ, admin hoặc non-admin), trong khoảng thời gian từ thứ hai đến thứ bảy hàng tuần, nếu vào chủ nhật sẽ luôn là màu xanh lá. Bạn có thể làm được điều này bằng cách thực hiện tính toán với các biểu thức điều kiện trong html template, nhưng nếu có quá nhiều điều kiện sẽ khiến code của bạn trở lên rất khó đọc, và khó bảo trì về sau.

Giải pháp: hãy tạo ra một attribute gọi là alertClass, sử dụng Java code để tính toán giá trị của attribute này và trả về CSS class mong muốn, đóng gói code này bên trong dialect có tên là MyOwnDialect, thêm dialect này vào trong template engine với prefix là th (giống như SpringStandard) và bây giờ bạn sẽ có thể sử dụng th:alertClass="${user.role}".

Kịch bản 2: view-layer components

Giả sử, công ty của bạn sử dụng Thymeleaf cho rất nhiều dự án khác nhau, và bạn mong muốn tạo ra một repository cho tất cả các chức năng phổ biến, được sử dụng lại rất nhiều lần trong một số dự án (ví dụ, các tag và/hoặc các attribute) để không phải copy-paste những đoạn code tương tự nhau từ dự án này qua các dự án kế tiếp. Bạn mong muốn tạo ra các view-layers component tương tự như các taglib trong công nghệ JSP.

Giải pháp: tạo một Thymeleaf dialect cho mỗi bộ các chức năng có liên quan với nhau, và thêm các dialect này vào ứng dụng của bạn nếu điều đó cần thiết.

Kịch bản 3: Tự tạo một template riêng

Hãy tưởng tượng rằng, bạn có một trang web cộng đồng cho phép người dùng có thể tạo ra các mẫu thiết kế của riêng họ để hiển thị nội dung. Nhưng bạn không hề muốn người dùng có thể thực hiện được toàn bộ công việc trong template của họ, thậm chí là hạn chế một vài tính năng của Standard dialect (ví dụ, các biểu thức OGNL). Vì vậy, bạn cần phải cung cấp cho người dùng khả năng thêm vào template của họ một số tính năng trong tầm kiểm soát của bạn (ví dụ như, hiển thị ảnh cá nhân, nhập nội dung văn bản, ...).

Giải pháp: Bạn cần tạo ra một Thymeleaf dialect có các thẻ hoặc thuộc tính mà bạn cho phép người dùng có thể sử dụng, giống như <mysite:profilePhoto></mysite:profilePhoto> hoặc có thể là <mysite:blogentries fromDate="23/4/2011" />. Sau đó, hãy cho phép người dùng tạo ra các template riêng có thể sử dụng các tính năng này và chỉ cho Thymeleaf cách thực hiện chúng, đảm bảo rằng không một ai có thể thực hiện được những việc mà bạn không cung cấp.

2. Dialect và Processor


2.1 Dialect


Ngoài các thuộc tính th:x hoặc các thẻ <th:y> đã được cung cấp sẵn bởi Thymeleaf với các tính năng mặc định, thì bạn hoàn toàn có thể tự tạo ra các bộ thuộc tính hoặc thẻ của riêng mình bằng tên bạn muốn và sử dụng chúng để xử lý các template của bạn.

Các Dialect là những đối tượng được implement từ interface org.thymeleaf.dialect.IDialect:

public interface IDialect {

    public String getName();

}

Yêu cầu cốt lõi duy nhất của một dialect là nó phải có tên để có thể xác định được. Tuy nhiên, cách thực hiện chỉ implement duy nhất IDialect rất ít khi được lựa chọn, thay vào đó, chúng ta thường sẽ implement từ một hoặc một số các interface con của IDialect, việc này phụ thuộc vào những gì mà Thymeleaf engine cung cấp:

  • IProcessorDialect: dialect cung cấp các processor.
  • IPreProcessorDialect: dialect cung cấp các pre-processor.
  • IPostProcessorDialect: dialect cung cấp các post-processor.
  • IExpressionObjectDialect: dialect cung cấp các object biểu thức.
  • IExecutionAttributeDialect: dialect cung cấp các thuộc tính xử lý.

Processor dialects: IProcessorDialect

IProcessorDialect interface:

public interface IProcessorDialect extends IDialect {

    public String getPrefix();
    public int getDialectProcessorPrecedence();
    public Set<IProcessor> getProcessors(final String dialectPrefix);

}

Các Processor là những đối tượng phụ trách hầu hết các xử lý logic trong các Thymeleaf template, đây có thể được coi là thành phần quan trọng nhất trong Thymeleaf:

Trong diaclect này chỉ có ba mục được định nghĩa:

  • prefix: đây là phần tiền tố hoặc namespace sẽ được áp dụng mặc định cho các phần tử hoặc các thuộc tính phù hợp với các processor của dialect này. Do đó, một dialect có tiền tố là th giống với Standard Dialect sẽ có thể xác định được các processor phù hợp với các thuộc tính như th:text, th:if, hoặc th:whatever (hoặc bạn cũng có thể sử dụng cú pháp của HTML5: data-th-text, data-th-if, và data-th-whatever). Tiền tố có thể được thiết lập trong cấu hình template engine và chúng có thể nhận giá trị null nếu như bạn muốn các processor có thể được thực thi trên các tag/attribute không cố định.
  • dialect precedence: là độ ưu tiên khi sắp xếp các processor trong các dialect
  • processors: các processor được cung cấp bởi dialect. Chú ý là phương thức getProcessors(...) nhận dialectPrefix làm tham số đầu vào trong trường hợp dialect này đã được cấu hình trong Template engine với tiền tố khác mặc định. Hầu hết các trường hợp thì ```IProcessor sẽ cần thông tin này khi khởi tạo.

Pre-processor dialects: IPreProcessorDialect

Pre-processorpost-processor khác với các processor đó là thay vì chỉ thực thi trên một sự kiện hoặc mẫu sự kiện đơn lẻ (một fragment của một template), thì chúng sẽ được áp dụng lên toàn bộ quá trình xử lý template như là một bước bổ sung trong chuỗi xử lý của engine. Do vậy, chúng sẽ được thực thi theo API hoàn toàn khác so với các processor, sẽ hướng sự kiện nhiều hơn, và được xác định bởi interface tầng thấp là ITemplateHandler.

Trong trường hợp có sử dụng các pre-processor, chúng sẽ được áp dụng trước khi Thymeleaf engine bắt đầu xử lý các processor cho một template chỉ định.

IPreProcessorDialect interface:

public interface IPreProcessorDialect extends IDialect {

    public int getDialectPreProcessorPrecedence();
    public Set<IPreProcessor> getPreProcessors();

}

Interface này tương tự với IProcessorDialect, nhưng thiếu prefix vì nó không cần thiết cho các pre-processor (chúng sẽ xử lý tất cả sự kiện xảy ra, không riêng bất kì sự kiện cụ thể nào cả)

Post-processor dialects: IPostProcessorDialect

Như đã nêu ở trên, post-processor là một bước bổ sung vào dây chuyền thực thi template, nhưng lần này chúng sẽ được thực thi sau khi Thymeleaf engine đã áp dụng tất cả các processor cần thiết. Điều này nghĩa là post-processor sẽ được áp dụng ngay trước khi template có kết quả (và do đó có thể chỉnh sửa được kết quả trả về).

IPostProcessorDialect interface:

public interface IPostProcessorDialect extends IDialect {

    public int getDialectPostProcessorPrecedence();
    public Set<IPostProcessor> getPostProcessors();

}

IPostProcessorDialect có cấu trúc tương tự với IPreProcessorDialect.

Expression Object dialects: IExpressionObjectDialect

Những dialect implement interface này cung cấp thêm các đối tượng expression object hoặc các expression utility object, các đối tượng này có thể được sử dụng trong các biểu thức ở bất kì nơi nào của một template, ví dụ, Standard Dialect mặc định có cung cấp một số đối tượng sau #strings, #numbers, #dates, #list,...

Còn đây là interface IExpressionObjectDialect:

public interface IExpressionObjectDialect extends IDialect {

    public IExpressionObjectFactory getExpressionObjectFactory();

}

Như chúng ta thấy, interface này chỉ có một phương thức duy nhất và nó không hề trả về chính các đối tượng expression, mà lại là một factory. Lí do là một vài đối tượng expression sẽ phải cần dữ liệu từ ngữ cảnh xử lý để có thể build được, do đó, nó sẽ không thể tự build cho đến khi chúng ta thực sự đang trong quá trình xử lý template... Bên cạnh đó, hầu hết các biểu thức đều không cần dùng đến các đối tượng expression, vì vậy sẽ tốt hơn nếu các đối tượng này chỉ được build khi thực sự cần thiết đối với các biểu thức cụ thể (và cũng chỉ build những gì thật cần thiết).

Đây là interface IExpressionObjectFactory:

public interface IExpressionObjectFactory {

    public Map<String,ExpressionObjectDefinition> getObjectDefinitions();

    public Object buildObject(final IProcessingContext processingContext, final String expressionObjectName);

}

Dialect xử lý thuộc tính: IExecutionAttributeDialect

Các Dialect implement interface này được cung cấp khả năng xử lý các thuộc tính.

Ví dụ, Standard Dialect implement interface này để cung cấp cho mọi processor:

  • Thymeleaf Standard Expression parser: các Standard Expression trong bất kì thuộc tính đều có thể được phân tích và thực thi.
  • Variable Expression Evaluator: các biểu thức ${...} được thực thi trong OGNL hoặc SpringEL (nếu tích hợp với Spring module).
  • Conversion Service: thực hiện các tính toán chuyển đổi trong biểu thức ${{...}}.

Lưu ý rằng, những đối tượng này không có sẵn trong ngữ cảnh, vì vậy, chúng không thể được sử dụng từ các biểu thức template. Tính khả dụng của chúng bị giới hạn trong việc triển khai mở rộng, như là các processor, pre-processor,...

Interface này đơn giản chỉ là:

public interface IExecutionAttributeDialect extends IDialect {

    public Map<String,Object> getExecutionAttributes();

}

2.2. Processor


Processor là những đối tượng implement interface org.thymeleaf.processor.IProcessor, và chúng chứa các logic thật sự để áp dụng cho các phần khác nhau của một template.

Interface này có cấu trúc như sau:

public interface IProcessor {

    public TemplateMode getTemplateMode();
    public int getPrecedence();

}

Giống với các dialect, đây là một interface rất đơn giản, chỉ xác định mode của templateprocessor có thể được sử dụng và độ ưu tiên của nó.

Nhưng có một vài loại processor tương ứng với mỗi loại sự kiện có thể xảy ra:

  • Template start/end
  • Element Tags
  • Texts
  • Comments
  • CDATA Sections
  • DOCTYPE Clauses
  • XML Declarations
  • Processing Instructions

Tùy biến Thymeleaf


(by @dangquando)