Диаграмма, иллюстрирующая принцип работы шаблонизатора Ejs
На картинке выше примерно изображен простой движок шаблонов (здесь сEJSНапример) принцип был объяснен семью или восемьюдесятью восемью способами. В этой статье будет описано, как работает простой механизм шаблонов? Содержит ключевые этапы реализации и идеи, лежащие в ее основе.
По сути, процедура механизма шаблонов похожа на эту, но эти идеи являются общими.Например, если вы смотрите на исходный код компилятора шаблонов Vue, вы также можете применить эти идеи и методы.
Базовый дизайн API
Мы будем реализовывать упрощенную версию EJS, этот механизм шаблонов поддерживает следующие теги:
-
<% script %>- Выполнение скрипта.Обычно используется для операторов управления, значение не выводится, например<% if (user) { %> <div>some thing</div> <% } %> -
<%= expression %>- Вывести значение выражения, но экранировать HTML:<title>{%= title %}</title> -
<%- expression %>- а также<%= expr %>То же самое, за исключением того, что HTML не экранируется -
<%%а также%%>- Обозначает экранирование меток, например<%%будет выводиться как<% -
<%# 注释 %>- нет вывода контента
Ниже приведен полный пример шаблона, который будет объяснен ниже на основе этого шаблона:
<html>
<head><%= title %></head>
<body>
<%% 转义 %%>
<%# 这里是注释 %>
<%- before %>
<% if (show) { %>
<div>root</div>
<% } %>
</body>
</html>
Базовый дизайн API
Мы помещаем логику, связанную с разбором и рендерингом шаблона, в класс Template, и его основной интерфейс выглядит следующим образом:
export default class Template {
public template: string;
private tokens: string[] = [];
private source: string = "";
private state?: State;
private fn?: Function;
public constructor(template: string) {
this.template = template;
}
/**
* 模板编译
*/
public compile() {
this.parseTemplateText();
this.transformTokens();
this.wrapit();
}
/**
* 渲染方法,由用户指定一个对象来渲染字符串
*/
public render(local: object) { }
/**
* token解析
* 将<% if (codintion) { %>
* 解析为token数组,例如['<%', ' if (condition) { ', '%>']
*/
private parseTemplateText() {}
/**
* 将Token转换为Javascript语句
*/
private transformTokens() {}
/**
* 将上一个步骤转换出来的Javascript语句,封装成一个渲染方法
*/
private wrapit() {}
}
парсинг токена
На первом этапе нам нужно проанализировать все начальные и конечные теги.Наши ожидаемые результаты анализа следующие:
[
"\n<html>\n <head>",
"<%=",
" title ",
"%>",
"</head>\n <body>\n ",
"<%%",
" 转义 ",
"%%>",
"\n ",
"<%#",
" 这里是注释 ",
"%>",
"\n ",
"<%-",
" before ",
"%>",
"\n ",
"<%",
" if (show) { ",
"%>",
"\n <div>root</div>\n ",
"<%",
" } ",
"%>",
"\n </body>\n</html>\n"
]
Поскольку грамматика нашего механизма шаблонов очень проста, нет необходимости разбирать ее в абстрактное синтаксическое дерево (AST) вообще (то есть синтаксический анализ опускается, и выполняется только лексический синтаксический анализ).正则表达式Теги можно извлечь.
Сначала определите регулярное выражение, используемое для соответствия всем нашим поддерживаемым меткам:
// <%% %%> 用于转义
// <% 脚本
// <%= 输出脚本值
// <%- 输出脚本值,unescape
// <%# 注释
// %> 结束标签
const REGEXP = /(<%%|%%>|<%=|<%-|<%#|<%|%>)/;
Используйте регулярные выражения для сопоставления одного за другим и разделения строки Код также очень прост:
parseTemplateText() {
let str = this.template;
const arr = this.tokens;
// 通过exec方法可以获取匹配的位置, 如果匹配失败则返回null
let res = REGEXP.exec(str);
let index;
while (res) {
index = res.index;
// 前置字符串
if (index !== 0) {
arr.push(str.substring(0, index));
str = str.slice(index);
}
arr.push(res[0]);
// 截断字符串,继续匹配
str = str.slice(res[0].length);
res = REGEXP.exec(str);
}
if (str) {
arr.push(str);
}
}
Простая проверка грамматики
Хорошо, когда теги проанализированы, пришло время подготовиться к преобразованию их в функцию «рендеринга».
Сделай это первымПростая проверка грамматики, чтобы проверить, является ли метказакрытие:
const start = "<%"; // 开始标签
const end = "%>"; // 结束标签
const escpStart = "<%%"; // 开始标签转义
const escpEnd = "%%>"; // 结束标签转义
const escpoutStart = "<%="; // 转义的表达式输出
const unescpoutStart = "<%-"; // 不转义的表达式输出
const comtStart = "<%#"; // 注释
if (tok.includes(start) && !tok.includes(escpStart)) {
closing = this.tokens[idx + 2];
if (closing == null || !closing.includes(end)) {
throw new Error(`${tok} 未找到对应的闭合标签`);
}
}
конвертировать
Теперь начните обход токена. Мы можем использовать конечный автомат (FSM) для описания логики перехода.
状态机это указатьМатематическая модель конечного числа состояний и поведения, такого как переходы и действия между этими состояниями.. Проще говоря, конечный автомат состоит из набора состояний, начального состояния, входных данных и функции перехода, которая переходит в следующее состояние на основе входных данных и существующего состояния. Он имеет три характеристики:
- Общее количество штатов ограничено.
- В любой момент времени существует только одно состояние.
- При определенных условиях переход из одного состояния в другое
После небольшого анализа диаграмма перехода состояний нашего шаблонизатора выглядит следующим образом:
Следующие состояния могут быть извлечены из приведенного выше рисунка:
enum State {
EVAL, // 脚本执行
ESCAPED, // 表达式输出
RAW, // 表达式输出不转义
COMMENT, // 注释
LITERAL // 字面量,直接输出
}
Хорошо, теперь начните обход токена:
this.tokens.forEach((tok, idx) => {
// ...
switch (tok) {
/**
* 标签识别
*/
case start:
// 脚本开始
this.state = State.EVAL;
break;
case escpoutStart:
// 转义输出
this.state = State.ESCAPED;
break;
case unescpoutStart:
// 非转义输出
this.state = State.RAW;
break;
case comtStart:
// 注释
this.state = State.COMMENT;
break;
case escpStart:
// 标签转义
this.state = State.LITERAL;
this.source += `;__append('<%');\n`;
break;
case escpEnd:
this.state = State.LITERAL;
this.source += `;__append('%>');\n`;
break;
case end:
// 恢复初始状态
this.state = undefined;
break;
default:
/**
* 转换输出
*/
if (this.state != null) {
switch (this.state) {
case State.EVAL:
// 代码
this.source += `;${tok}\n`;
break;
case State.ESCAPED:
// stripSemi 将多余的分号移除
this.source += `;__append(escapeFn(${stripSemi(tok)}));\n`;
break;
case State.RAW:
this.source += `;__append(${stripSemi(tok)});\n`;
break;
case State.LITERAL:
// 因为我们把字符串放到单引号中,所以transformString将tok中的单引号、换行符、转义符进行转移
this.source += `;__append('${transformString(tok)}');\n`;
break;
case State.COMMENT:
// 什么都不做
break;
}
} else {
// 字面量
this.source += `;__append('${transformString(tok)}');\n`;
}
}
});
После приведенного выше преобразования мы можем получить такой результат:
;__append('\n<html>\n <head>');
;__append(escapeFn( title ));
;__append('</head>\n <body>\n ');
;__append('<%');
;__append(' 转义 ');
;__append('%>');
;__append('\n ');
;__append('\n ');
;__append( before );
;__append('\n ');
; if (show) {
;__append('\n <div>root</div>\n ');
; }
;__append('\n </body>\n</html>\n');
Последний шаг — сгенерировать функцию
Теперь мы оборачиваем результат преобразования в функцию:
wrapit() {
this.source = `\
const __out = [];
const __append = __out.push.bind(__out);
with(local||{}) {
${this.source}
}
return __out.join('');\
`;
this.fn = new Function("local", "escapeFn", this.source);
}
используется здесьwithоператор, чтобы обернуть приведенный выше преобразованный код, чтобы можно было опустить префикс квалификации доступа к локальному объекту.
Метод рендеринга очень прост, напрямую вызовите функцию, обернутую выше:
render(local: object) {
return this.fn.call(null, local, escape);
}
начать бежать
const temp = new Template(`
<html>
<head><%= title %></head>
<body>
<%% 转义 %%>
<%# 这里是注释 %>
<%- before %>
<% if (show) { %>
<div>root</div>
<% } %>
</body>
</html>
`);
temp.compile();
temp.render({ show: true, title: "hello", before: "<div>xx</div>" })
// <html>
// <head>hello</head>
// <body>
// <% 转义 %>
//
// <div>xx</div>
//
// <div>root</div>
//
// </body>
// </html>
Вы можете запустить полный код в CodeSandbox:
Суммировать
Эта статья на самом делеthe-super-tiny-compilerВдохновленный, реализован минималистский механизм шаблонов.На самом деле механизм шаблонов по сути является компилятором.Из вышеизложенного мы видим, что для компиляции механизма шаблонов нужно выполнить три шага:
-
РазобратьРазобрать код шаблона в абстрактное представление. Сложные компиляторы будут иметь
词法解析(Lexical Analysis)а также语法解析(Syntactic Analysis)Лексический анализ, Процесс разбора содержимого шаблона на токены выше можно рассматривать как «лексический анализ», который разбивает исходный код на массив токенов, а токен — это небольшая единица, представляющая независимый «фрагмент грамматики».
разбор, синтаксический анализатор получает массив токенов и переформатирует их в абстрактное синтаксическое дерево (AST), которое можно использовать для описания синтаксических единиц и отношений между единицами. На этапе разбора грамматики могут быть обнаружены грамматические проблемы.
(Источник изображения:Entersland Foods IV AK.com/Ross Bass-part…)
Механизм шаблонов, представленный в этой статье, не нуждается в промежуточном представлении AST, поскольку его синтаксис слишком прост. Конвертируйте напрямую в токены
-
конвертироватьПреобразуйте абстрактное представление предыдущего шага в то, что хочет компилятор. Например, механизм шаблонов выше преобразует его в оператор на соответствующем языке. Сложные компиляторы будут «преобразовывать» на основе AST, то есть «добавлять, удалять, проверять и изменять» AST. Обычно он будет сотрудничать с режимом «Посетители» для обхода/доступа к узлам AST.
-
генерация кодаПреобразуйте преобразованное абстрактное представление в новый код. Например, последний шаг шаблонизатора будет инкапсулирован как функция рендеринга, а сложный компилятор преобразует AST в объектный код.
Вещи, связанные с компилятором, действительно интересны, и позже у меня будет возможность поговорить о том, как писать плагины для Babel.