Ориентация главы
xtemplate стандартизует связку «данные + шаблон → текст»: разделение компиляции и рендеринга — xrtTemplateCompile разбирает шаблон в неизменяемое дерево узлов (с подсчётом ссылок, тот же жизненный цикл, что у регулярных выражений главы 30), а xrtTemplateRender рендерит владеющий текст из дерева значений (xvalue главы 31). Три префикса интерполяции закрывают три частые потребности: {$path} прямой вывод, {%path:fmt} форматирование чисел (строка формата — синтаксис главы 26), {&path:fmt} форматирование времени; управляющие конструкции (циклы/условия) и вызовы функций разворачиваются в обзорном образце. Стандартное употребление шаблонов конфигурации, заголовков отчётов и текстов уведомлений: компиляция на старте, высокочастотный рендеринг — стоимость разбора платится один раз. Глава разделяет с регулярными выражениями главы 30 одну ментальную модель двухслойного жизненного цикла: освоив одно, второе берёте бесплатно.
35.1引入
Потребность системы уведомлений (этот модуль есть почти у каждой онлайн-системы): «Уважаемый {имя пользователя}, ваш заказ {номер} отправлен {время}.» — три поля, три типа (строка, число, время), плюс варианты по каналам (короткая версия для SMS, длинная для почты). Ручная сборка через xrtFormat — брать каждое поле, форматировать, сшивать; с ростом числа полей получается лапша; построитель главы 25 лучше, но «шаблон» всё ещё рассыпан по коду — смена одной формулировки означает правку кода, перекомпиляцию, передеплой.
Шаблонный движок выносит «форму» из кода: шаблон — это данные (файл, строка в конфигурации); поменяли формулировку — поменяли шаблон: без релиза и без простоя; рендеринг — чистая функция: один шаблон с одними данными всегда даёт один вывод, тестируемо и кешируемо. Разделение компиляции и рендеринга добавляет звено производительности (в этом его сущностный отрыв от «замены строк»): разбор шаблона (постройка дерева узлов, валидация путей) выполняется один раз при компиляции, а рендеринг делает только «взять + отформатировать + вывести» — стоимость горячего пути (рендеринг уведомления на каждый запрос) практически не зависит от сложности шаблона.
35.2概念
Разделение компиляции и рендеринга (сперва модель, потом функции)
Скомпилированный объект неизменяем и разделяем — многопоточный рендеринг одного шаблона требует лишь собственных деревьев значений у каждого потока (рендеринг читает шаблон, не меняя). Это полностью изоморфно двухслойной модели регулярных выражений главы 30: тяжёлая операция (разбор) один раз, лёгкая (рендеринг) высокочастотно.
Три префикса интерполяции (по одному символу смысла на каждый)
| Префикс | Синтаксис | Семантика |
|---|---|---|
| Прямой вывод | {$user.name} | строка как есть; путь навигирует дерево значений по точкам |
| Форматирование чисел | {%count:,d} | после двоеточия строка формата главы 26 (тысячи, система счисления, проценты) |
| Форматирование времени | {&updated:%F} | значение времени рендерится по строке формата (дата ISO и т.п.) |
Синтаксис пути a.b.c навигирует дерево значений (точечный доступ во вложенные объекты — как в большинстве шаблонных языков): при рендеринге значение берётся по пути; промах (путь не существует или тип не тот) обрабатывается по конфигурации рендеринга (ошибка или пустая строка — выбор явный). Типизированное форматирование — водораздел между шаблоном и «заменой строк» (данные хранят тип, показ хранит гибкость: одно и то же числовое поле в SMS-шаблоне компактным форматом, в шаблоне отчёта с разделителями тысяч — решает шаблон): форматирование чисел и времени объявляется в шаблоне, а не препформатируется в строки в коде — данные остаются типизированными, показ остаётся гибким.
Управляющие конструкции и функции (форма подстраивается под данные)
Шаблон — не только интерполяция: циклы (обход массива дерева значений, по переменной цикла на элемент) и условия (ветвление по истинности значения) делают возможным «форма данных определяет структуру вывода» — отчёты построчно, уведомления по наличию вложений. Вызовы функций (форма call обзорного образца) регистрируют переиспользуемую логику форматирования как вызываемые из шаблона единицы — «логика показа» вроде смены регистра и форм множественного числа остаётся на стороне шаблона. Граничная дисциплина конструкций и функций: шаблон принимает решения показа, а не считает бизнес — сложное выражение в шаблоне сигналит, что ему пора обратно в слой подготовки данных.
Позиционирование ошибок (линия обороны компиляции)
Ошибки компиляции несут номер строки (обзорный образец проверяет error-location line=1) — синтаксическая ошибка в шаблоне вскрывается при компиляции, а не выплёвывается покалеченным текстом. Ошибки рендеринга (промах пути) также докладываются структурно — система ошибок главы 4 действует как обычно.
35.3示例
Полная программа: компиляция, дерево значений и три интерполяции
Из образца репозитория examples/template/core/main.c:
*/ #include <stdio.h> #include <xrt.h> int main(void) { xvalue* pData = xrtValueObject(); /* 渲染数据:一个对象 */ xtemplate* pTemplate; str sOutput; xtime Time; */ if ( (pData == NULL) || !xrtDateTime( 2026, 4, 2, 12, 30, 0, 0, &Time ) || !xrtValueObjectSetNew( pData, XRT_STR_LITERAL("name"), xrtValueString(XRT_STR_LITERAL("xrt")) ) || !xrtValueObjectSetNew( pData, XRT_STR_LITERAL("requests"), xrtValueInt(42) ) || !xrtValueObjectSetNew( pData, XRT_STR_LITERAL("created"), xrtValueTime(Time) ) ) { xrtValueRelease(pData); return 1; } /* 编译:一次解析成不可变对象;模板语法错在此步报错。 */ pTemplate = xrtTemplateCompile( XRT_STR_LITERAL( "{$name} handled {%requests:,d} requests on {&created:%F}" ) ); if ( pTemplate == NULL ) { xrtValueRelease(pData); return 2; } /* 渲染:NULL 配置 = 默认预算(防失控输出的上限仍生效)。 */ sOutput = xrtTemplateRender(pTemplate, pData, NULL); if ( sOutput == NULL ) { xrtTemplateRelease(pTemplate); xrtValueRelease(pData); return 3; } puts(sOutput); /* 三个拥有式资源逆序释放。 */ xrtFree(sOutput); xrtTemplateRelease(pTemplate); xrtValueRelease(pData); return 0; }
Что только что произошло. (1) Сторона данных строится деревом значений главы 31: ObjectNew + SetNew (поля типов String/Int/Time) — шаблон ест деревья значений, так что продукты разбора JSON/XLON (главы 32/33) подключаются напрямую, ноль преобразований. (2) {$name} выводит строку как есть; {%count:,d} прогоняет число через форматирование тысяч главы 26 (42 без разделителей тоже легально); {&day:%F} рендерит время датой ISO — три префикса по одному заходу каждый. (3) Render возвращает владеющий текст, освобождение xrtFree; скомпилированный объект возвращается Release-ом. Подставили другое дерево значений — тот же код выдаёт другое уведомление: разделение шаблона и данных здесь нагляднее всего.
Полная программа: управляющие конструкции и циклы
Из examples/template/control/main.c — цикл по списку и условные ветви:
*/ #include <stdio.h> #include <xrt.h> int main(void) { xvalue* pRoot = xrtValueObject(); xvalue* pUsers = xrtValueArray(); xtemplate* pTemplate; str sOutput; */ if ( (pRoot == NULL) || (pUsers == NULL) || !xrtValueArrayAppendNew( pUsers, xrtValueString(XRT_STR_LITERAL("Alice")) ) || !xrtValueArrayAppendNew( pUsers, xrtValueString(XRT_STR_LITERAL("Bob")) ) || !xrtValueObjectSetTake( pRoot, XRT_STR_LITERAL("users"), &pUsers ) ) { xrtValueRelease(pUsers); xrtValueRelease(pRoot); return 1; } /* 一条模板串演示全部控制语句(见文件头语法速查)。 */ pTemplate = xrtTemplateCompile(XRT_STR_LITERAL( "{#if:users}Users: {#foreach:users}" "{?loop.first::, }{$loop.value}{#end}{#else}No users{#end}; " "range: {#for:1:5}" "{#if:loop.value = 2}{#continue}{#end}" "{#if:loop.value = 4}{#break}{#end}" "{%loop.value}{#end}" )); if ( pTemplate == NULL ) { xrtValueRelease(pRoot); return 2; } sOutput = xrtTemplateRender(pTemplate, pRoot, NULL); if ( sOutput == NULL ) { xrtTemplateRelease(pTemplate); xrtValueRelease(pRoot); return 3; } puts(sOutput); xrtFree(sOutput); xrtTemplateRelease(pTemplate); xrtValueRelease(pRoot); return 0; }
Что только что произошло. (1) Цикл обходит массив пользователей в дереве значений — каждый элемент входит в переменную цикла, сегмент шаблона рендерится повторно; вывод через запятую определён фрагментом шаблона внутри тела цикла. (2) Условия и диапазоны: range: 13 показывает вывод условно-агрегатных конструкций — управляющие конструкции позволяют шаблону подстраиваться под форму данных, а не данным гнуться под фиксированный шаблон. (3) Обзорный образец оставлен для продвинутого чтения: он покрывает загрузку шаблона из файла, потоковый вывод рендеринга, вызовы функций и реестр — четыре группы способностей, полная форма шаблонного движка в реальном проекте; четыре строки ok соответствуют им по одной.
35.4契约
- Двухслойная модель: скомпилированный объект неизменяем, разделяется подсчётом ссылок; рендеринг читает шаблон и идёт высокочастотно параллельно (у каждого потока своё дерево значений).
- Три префикса:
{$}прямой вывод /{%:fmt}строка формата чисел главы 26 /{&:fmt}строка формата времени. - Данные — это дерево: вход рендеринга — xvalue; продукты JSON/XLON подключаются без преобразований.
- Позиционирование ошибок: ошибки компиляции с номером строки; ошибки рендеринга (путь/тип) — структурный отчёт.
- Граничная дисциплина: шаблон решает вопросы показа, а не бизнеса — сложные выражения переезжают в слой подготовки данных.
- Владеющий продукт: вывод рендеринга —
str, освобождениеxrtFree; потоковый рендеринг подключается прямо к sink.
От примеров к инженерии: три хозяина шаблонов
Генерация уведомлений/сообщений (самое частое): глобальные шаблоны компилируются на старте, по приходу события строится дерево значений, рендеринг — и в путь; хорошая форма ловушки 1 и есть её каркас; в многоязычных сценариях компилируется по шаблону на язык, выбор — по языку пользователя. Отчёты и скелеты страниц: строки данных рендерятся циклом, заголовки ветвятся условием — главная площадка управляющих конструкций; большие отчёты идут потоковым рендерингом прямо в sink (форма streamed обзорного образца) — миллион строк не собирает целую страницу в памяти. Конфигурируемые тексты UI: тексты-шаблоны лежат в конфигурации (правка текста без релиза), поля берутся из дерева конфигурации — шаблон и данные становятся горячо-обновляемыми активами. Общий путь эволюции трёх хозяев: шаблон из константы кода → файл конфигурации → горячо-обновляемый актив; каждый шаг опирается на двухслойный дизайн «скомпилированный объект заменяем, рендеринг без состояния».
Карта разделения труда с JSON/XLON/регулярными выражениями
Четыре модуля второй половины тома 4 (плюс дерево значений) часто выходят на одну сцену; карта разделения не даёт перепутать. JSON/XLON (главы 32/33): структурированные данные входят и выходят — разбор в дерево значений; сериализация дерева обратно. Дерево значений (глава 31): промежуточное представление — общая валюта всех модулей. Регулярные выражения (глава 30): извлекают структуру из неструктурированного текста (поля журналов, строки конфигурации) → порождают дерево значений. Шаблоны (эта глава): порождают текст из дерева значений (уведомления, отчёты) → продуцируют строки. Извлечение и генерация разнонаправленны и зеркальны — композиционная глава 36 сцепит «извлечь → дерево значений → сгенерировать шаблоном» в полный конвейер. На карте каждый модуль делает ровно одно дело; композиция идёт через дерево значений как промежуточную валюту.
Взгляд на дизайн: шаблон — тоже интерфейс
Между шаблоном и кодом рендеринга есть неявный интерфейс: множество путей и типов, на которые шаблон ссылается. Интерфейс сменился (поле переименовано, тип из int стал string), а шаблон не поспел — рендеринг упадёт в рантайме. Три привычки делают неявное явным: дымовой тест рендеринга (минимальное дерево + рендер всех префиксов с утверждённым выводом) — в CI; файлы шаблонов — в том же репозитории и ревью, что код; в чек-лист смены полей добавляется графа «затронутые шаблоны». Композиционная глава 36 развернёт эту идею «интерфейса» в контракт данных полного конвейера.
Типичные ошибки
Симптом: аномальный CPU на высокочастотном пути уведомлений/отчётов; горячая точка в xrtTemplateCompile — полный изоморф ловушки 1 регулярных выражений главы 30.
Причина: компиляция (разбор в дерево узлов) — тяжёлая операция; рендеринг — лёгкая. Тяжёлая попала в цикл.
str render(const xvalue* pData, xstrview Tpl) { xtemplate* T = xrtTemplateCompile(Tpl); /* компиляция на каждый рендеринг */ str s = xrtTemplateRender(T, pData, NULL, NULL); xrtTemplateRelease(T); return s; }
/* компиляция один раз на старте; смена шаблона - через "перекомпиляция + атомарная замена" */ static xtemplate* gTpl; str render(const xvalue* pData) { return xrtTemplateRender(gTpl, pData, NULL, NULL); /* только лёгкий путь */ }
Симптом: шаблоны растут и всё больше походят на ещё один язык программирования; смена бизнес-правил требует правки шаблонов; тесты шаблонов не покрывают логические ветви.
Причина: шаблонный движок дал выразительность, и команда перенесла в него подготовку данных — граница между «решениями показа» и «бизнес-вычислениями» пала.
<!-- 模板里做业务计算:满减规则、运费、税费全在模板表达式里 -->
Total: {%cart.subtotal - discount(cart.promo) + shipping(cart.weight) + tax(...):,.2f}<!-- 模板只展示;计算在代码层完成、结果进值树 -->
Total: {%order.total:,.2f}
(order.total 由代码层按业务规则算好再构造值树)Практические задания
Постройте дерево значений с полями строка/число/время; шаблоном отрендерите каждое своим префиксом; подставьте другой набор данных и перерендерьте, проверяя переиспользование шаблона.
Дерево значений с массивом пользователей (имя + число заказов у каждого); шаблон циклом рендерит сводную строку «каждому {name}({count})»; для пустого массива выводится «никто не заказывал» — условие и цикл в связке.
JSON-конфигурация (глава 32) определяет метаданные отчёта (заголовок, столбцы, пути данных); шаблон по конфигурации динамически рендерит шапку и строки — количество и имена столбцов целиком из конфигурации, без жёсткого кодирования. Критерии приёмки: один шаблон с двумя разными конфигурациями рендерит две разные таблицы; отсутствие столбца даёт ошибку с путём; компиляция происходит ровно один раз (проверка на нескольких рендерах).
Итоги
| Тема | Кратко |
|---|---|
| Двухслойная модель | компиляция неизменяема и разделяема (Retain/Release) / рендеринг только читает, высокочастотно и параллельно |
| Три префикса | {$path} прямой вывод / {%path:fmt} формат чисел / {&path:fmt} формат времени |
| Данные | дерево значений напрямую — продукты JSON/XLON без преобразований; дерево значений — общая валюта модулей |
| Управление | циклы по массивам / условные ветви / вызовы функций (логика показа — шаблону, бизнес-вычисления — коду) |
| Ошибки | компиляция — с номером строки; рендеринг — структурно (путь/тип) |
| Дисциплина производительности | компиляция один раз, глобальное разделение; на горячем пути — только рендеринг; та же двухслойная модель, что у регулярных выражений |
| Граница | решения показа — шаблону, бизнес-вычисления — слою кода |
| Три хозяина | генерация уведомлений / страницы отчётов (потоковый рендеринг) / тексты на конфигурации — эволюция на двухслойном дизайне |
| Карта разделения | JSON/XLON водят структуру, дерево значений — валюта, регулярные выражения извлекают, шаблоны порождают |
| Интерфейсный взгляд | пути и типы, на которые ссылается шаблон, — неявный интерфейс; дымовой тест рендеринга в CI защищает от дрейфа |
📖 Справочники API в этой главе:template (51 записей)value (159 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки