Handlebars

spider-silk-handlebars는 코어에 번들된 jte 대신 Handlebars.java로 렌더링한다. Tomcat이 별도 모듈인 것과 같은 이유로 별도 모듈이다. 한 엔진에 대한 종속은 아티팩트 이름에 담기고, 코어는 템플릿 의존성이 셋이 아니라 하나인 웹 계층으로 남는다.

설치

dependencies {
    implementation('net.benelog.spidersilk:spider-silk-handlebars:1.1.0') {
        exclude group: 'gg.jte' // core's jte, unused here
    }
}

jte 제외는 선택 사항이다. 쓰지 않는 엔진을 클래스패스에서 치울 뿐이며, 그대로 두면 templates(renderer)가 건네는 것을 무엇이든 받으므로 둘이 나란히 동작한다.

렌더링

new App()
        .templates(new HandlebarsTemplates("hbs"))
        .get("/decks/{deckId}", req ->
                WebResponse.template("deck", Model.of("deck", service.deck(req.pathParamLong("deckId")))));

이는 classpath:/hbs/deck.hbs를 렌더링한다. 모델의 키가 템플릿이 읽는 이름이므로, {{deck.title}}은 Model.of("deck", …​)의 deck 항목에 닿는다.

{{name}}은 HTML 이스케이프되고 {{{name}}}은 되지 않는데, 이는 Handlebars 자체의 기본값이며 여기서 그것을 위해 설정할 것이 없는 이유다.

직접 정하는 루트나 접미사

app.templates(new HandlebarsTemplates("hbs") // classpath:/hbs
        .suffix(".html")); // .../deck.html

접미사는 덧붙일 뿐 검사하지 않으므로, 확장자를 아직 달고 있는 이름은 접미사가 두 번 붙은 채로 조회된다. 클래스패스 로더는 자체 확장자를 덧붙이지 않으므로, 작동하는 것은 suffix 하나뿐이다.

헬퍼와 캐시

다른 생성자는 설정된 Handlebars를 받으며, 헬퍼, 다른 이스케이프 전략, 파셜 로더는 여기에 들어간다:

Handlebars handlebars = new Handlebars(new ClassPathTemplateLoader("/hbs", ""))
        .with(new ConcurrentMapTemplateCache())
        .registerHelper("upper", (value, options) -> value.toString().toUpperCase());

app.templates(new HandlebarsTemplates(handlebars));

위처럼 로더에 빈 접미사를 전달하지 않으면, HandlebarsTemplates가 이미 덧붙인 접미사 위에 .hbs를 또 덧붙인다.

new HandlebarsTemplates("hbs")는 컴파일된 템플릿을 ConcurrentMapTemplateCache에 캐시한다. Handlebars 자체의 기본값은 렌더링할 때마다 컴파일하는 것으로, 프로덕션에서는 요청마다 템플릿을 파싱하게 된다. 다르게 선택하려면 직접 만든 Handlebars를 건넨다.

리플렉션

Handlebars는 {{deck.title}}을 먼저 Map에서 찾지만, 레코드나 빈에 대해서는 리플렉션으로 돌아간다. 이는 코어가 아니라 모듈의 리플렉션이다. 규칙이 적용되는 범위는 "리플렉션 없음"의 범위를 참고한다. 모델 값을 레코드나 빈이 아니라 맵으로 두면 그 리플렉션마저 피할 수 있다.