Автор: Цуй Цзин
предисловие
Хотя существуют различные интерфейсные фреймворки для повышения эффективности разработки, в некоторых случаях компоненты, реализованные на собственном JavaScript, также незаменимы. Например, в нашем проекте нам нужно предоставить общий платежный компонент для бизнес-стороны, но стек технологий, используемый бизнес-стороной, может бытьVue,Reactи т. д., даже собственный JavaScript. Тогда для достижения универсальности и обеспечения ремонтопригодности компонентов необходимо реализовать нативный компонент JavaScript.
Левое изображение ниже показывает общий вид нашего компонента Panel, а правое изображение показывает общую структуру каталогов нашего проекта:
Разбиваем компонент на.html,.js,.cssТри файла, такие как компонент Panel, содержат три файла: panel.html, panel.js и panel.css, так что представления, логика и стили могут быть разобраны для удобства обслуживания. Чтобы повысить гибкость компонента, заголовок в нашей панели, текст кнопки, а также количество и содержимое промежуточных элементов контролируются данными конфигурации, чтобы мы могли динамически отображать компоненты в соответствии с данные конфигурации. В этом процессе, чтобы сделать поток данных и событий более понятным, ссылаясь на дизайн Vue, мы ввели концепцию центра обработки данных, и данные, требуемые компонентами, хранятся в центре обработки данных единообразно. Изменение данных центра обработки данных приведет к обновлению компонента, а процесс обновления заключается в повторном отображении представления в соответствии с другими данными.
Panel.html — это то, что мы часто называем «шаблоном строки», а процесс его преобразования в исполняемый код JavaScript — это то, что делает «механизм шаблонов». Есть много шаблонизаторов на выбор, и они, как правило, предоставляют богатые функции. Но во многих случаях мы можем иметь дело с простым шаблоном, без слишком сложной логики, тогда нам достаточно простого строкового шаблона.
Несколько методов строковых шаблонов и простые принципы
В основном делятся на следующие категории:
-
Просто и грубо - регулярная замена
Самый простой и грубый способ — использовать строки напрямую для регулярной замены. Но не могу обрабатывать операторы цикла и если/иначе судить об этом.
А. Определите способ записи строковой переменной, например, используя
<%%>пакетconst template = ( '<div class="toast_wrap">' + '<div class="msg"><%text%></div>' + '<div class="tips_icon <%iconClass%>"></div>' + '</div>' )б. Затем путем обычного сопоставления найти все
<%%>, чтобы заменить переменную внутриfunction templateEngine(source, data) { if (!data) { return source } return source.replace(/<%([^%>]+)?%>/g, function (match, key) { return data[key] ? data[key] : '' }) } templateEngine(template, { text: 'hello', iconClass: 'warn' }) -
Простой и элегантный — синтаксис шаблона ES6
Используйте синтаксис ES6 встрока шаблона, приведенную выше глобальную замену через регулярные выражения, мы можем просто написать
const data = { text: 'hello', iconClass: 'warn' } const template = ` <div class="toast_wrap"> <div class="msg">${data.text}</div> <div class="tips_icon ${data.iconClass}"></div> </div> `в строке шаблона
${}Произвольные выражения могут быть записаны в , но таким же образом, если / иначе не могут быть обработаны суждения и операторы цикла. -
Простой механизм шаблонов
Во многих случаях при рендеринге HTML-шаблонов, особенно при рендеринге элементов ul, необходим цикл for. Затем вам нужно добавить операторы логической обработки на основе приведенной выше простой логики.
Например, у нас есть следующий шаблон:
var template = ( 'I hava some menu lists:' + '<% if (lists) { %>' + '<ul>' + '<% for (var index in lists) { %>' + '<li><% lists[i].text %></li>' + '<% } %>' + '</ul>' + '<% } else { %>' + '<p>list is empty</p>' + '<% } %>' )Интуитивно мы надеемся, что шаблон можно преобразовать в следующее:
'I hava some menu lists:' if (lists) { '<ul>' for (var index in lists) { '<li>' lists[i].text '</li>' } '</ul>' } else { '<p>list is empty</p>' }Чтобы получить окончательный шаблон, мы запихиваем разбросанные HTML-фрагменты в массив
html, наконец прошелhtml.join('')Сращивание в окончательный шаблон.const html = [] html.push('I hava some menu lists:') if (lists) { html.push('<ul>') for (var index in lists) { html.push('<li>') html.push(lists[i].text) html.push('</li>') } html.push('</ul>') } else { html.push('<p>list is empty</p>') } return html.join('')Таким образом, у нас есть исполняемый код JavaScript. Для сравнения легко увидеть, что есть несколько преобразований из шаблона в код JavaScript:
-
<%%>Если это логический оператор (if/else/for/switch/case/break), то содержимое в середине напрямую преобразуется в код JavaScript. через регулярные выражения/(^( )?(var|if|for|else|switch|case|break|;))(.*)?/gОтфильтруйте логические выражения для обработки. -
<% xxx %>Если не логическое утверждение, то мы заменилиhtml.push(xxx)утверждение -
<%%>Для содержимого, отличного от , мы заменяем его наhtml.push(字符串)
const re = /<%(.+?)%>/g const reExp = /(^( )?(var|if|for|else|switch|case|break|;))(.*)?/g let code = 'var r=[];\n' let cursor = 0 let result let match const add = (line, js) => { if (js) { // 处理 `<%%>` 中的内容, code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n' } else { // 处理 `<%%>` 外的内容 code += line !== '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '' } return add } while (match = re.exec(template)) { // 循环找出所有的 <%%> add(template.slice(cursor, match.index))(match[1], true) cursor = match.index + match[0].length } // 处理最后一个<%%>之后的内容 add(template.substr(cursor, template.length - cursor)) // 最后返回 code = (code + 'return r.join(""); }').replace(/[\r\t\n]/g, ' ')На данный момент у нас есть «текстовая» версия кода JavaScript, использующая
new Function«Текстовый» код можно превратить в настоящий исполняемый код.Осталось последнее — передать параметры и выполнить функцию.
Способ 1. Вы можете единообразно инкапсулировать все параметры шаблона в объект (данные), а затем использовать команду «Применить» для привязки функции.
thisк этому объекту. Итак, в шаблоне мы можем передатьthis.xxПолучить данные.new Function(code).apply(data)Способ 2: Всегда пишите
this.Это будет немного хлопотно. Вы можете обернуть функцию вwith(obj)чтобы запустить его, а затем передать данные, используемые шаблоном, в функцию в качестве параметра obj. Таким образом, переменные можно использовать непосредственно в шаблоне, как при написании шаблона в предыдущем примере.let code = 'with (obj) { ...' ... new Function('obj', code).apply(data, [data])Но следует отметить,
withСама грамматика имеет некоторые недостатки.На данный момент у нас есть простой механизм шаблонов.
Исходя из этого, можно выполнить некоторую упаковку для расширения функции. Например, вы можете добавитьмногоязычный подход i18n. Таким образом, языковая копия может быть отделена от шаблона, и после глобальной настройки языка ее можно будет использовать непосредственно в последующем рендеринге.
Основная идея: обернуть переданные данные в шаблон и добавить к ним функцию $i18n. Затем, когда мы пишем в шаблоне
<p><%$i18n("something")%></p>, будет проанализировано какpush($i18n("something"))Конкретный код выглядит следующим образом:
// template-engine.js import parse from './parse' // 前面实现的简单的模板引擎 class TemplateEngine { constructor() { this.localeContent = {} } // 参数 parentEl, tpl, data = {} 或者 tpl, data = {} renderI18nTpl(tpl, data) { const html = this.render(tpl, data) const el = createDom(`<div>${html}</div>`) const childrenNode = children(el) // 多个元素则用<div></div>包裹起来,单个元素则直接返回 const dom = childrenNode.length > 1 ? el : childrenNode[0] return dom } setGlobalContent(content) { this.localeContent = content } // 在传入模板的数据中多增加一个$i18n的函数。 render(tpl, data = {}) { return parse(tpl, { ...data, $i18n: (key) => { return this.i18n(key) } }) } i18n(key) { if (!this.localeContent) { return '' } return this.localeContent[key] } } export default new TemplateEngine()пройти через
setGlobalContentметод установки глобальной копии. Затем в шаблоне вы можете передать<%$i18n("contentKey")%>использовать напрямуюimport TemplateEngine from './template-engine' const content = { something: 'zh-CN' } TemplateEngine.setGlobalContent(content) const template = '<p><%$i18n("something")%></p>' const divDom = TemplateEngine.renderI18nTpl(template)Используйте '<%%>' для обертывания логических блоков и переменных в представленном нами методе, и есть более распространенный способ - использование двойных фигурных скобок
{{}}, также известный как тег усов. существуетVue, Angularтак же какАпплет WeChatЭто обозначение используется в синтаксисе шаблона и также обычно называется интерполяционным выражением. Давайте посмотрим на простоймеханизм шаблонов синтаксиса усовреализация.%%> -
-
шаблонизаторmustache.jsпринцип
Благодаря методу 3 нам немного легче понять другие принципы механизма шаблонов. Давайте рассмотрим принципы работы с усами, широко используемого облегченного шаблона.
Простой пример выглядит следующим образом:
var source = ` <div class="entry"> {{#author}} <h1>{{name.first}}</h1> {{/author}} </div> ` var rendered = Mustache.render(source, { author: true, name: { first: 'ana' } })-
Разбор шаблона
Механизм шаблонов сначала анализирует шаблон. Процесс парсинга шаблона усов примерно таков:
- Обычная совпадающая часть, псевдокод выглядит следующим образом:
tokens = [] while (!剩余要处理的模板字符串是否为空) { value = scanner.scanUntil(openingTagRe); value = 模板字符串中第一个 {{ 之前所有的内容 if (value) { 处理value,按字符拆分,存入tokens中。例如 <div class="entry"> tokens = [ {'text', "<", 0, 1}, {'text', "d"< 1, 2}, ... ] } if (!匹配{{) break; type = 匹配开始符 {{ 之后的第一个字符,得到类型,如{{#tag}},{{/tag}}, {{tag}}, {{>tag}}等 value = 匹配结束符之前的内容 }},value中的内容则是 tag 匹配结束符 }} token = [ type, value, start, end ] tokens.push(token) }-
Затем, пройдя
tokens, будет непрерывнымtextМассивы типов объединяются. -
траверс
tokens,иметь дело сsectionтипа (т.е. в шаблоне{{#tag}}{{/tag}},{{^tag}}{{/tag}}).sectionОни появляются парами в шаблоне и должны бытьsectionВложенность, и, наконец, соответствует типу вложенности нашего шаблона.
-
оказывать
После парсинга шаблона происходит его рендеринг: по поступающим данным получается конечная HTML-строка. Общий процесс рендеринга выглядит следующим образом:
Сначала сохраните данные отображаемого шаблона в переменную
contextсередина. Так как в шаблоне переменные представлены в виде строк, например'name.first'. Первый проход при получении.делить'name'а также'first'затем пройтиtrueValue = context['name']['first']установить значение. Для повышения производительности можно добавитьcacheСохраните полученный результат на этот раз,cache['name.first'] = trueValueдля следующего использования.Основной процесс рендеринга состоит в том, чтобы пройти
tokens, получить тип и переменную (value) истинного значения, затем выполнить рендеринг в соответствии с типом и значением и, наконец, сшить полученные результаты вместе, чтобы получить окончательный результат.
-
Найдите правильный шаблонизатор
Среди множества шаблонизаторов, как заблокировать нужный? Вот несколько направлений, которые можно рассмотреть, надеясь помочь вам выбрать:
-
Функция
Выбирая инструмент, главное посмотреть, сможет ли он удовлетворить наши потребности. Например, поддерживать ли переменные, логические выражения, поддерживать ли подшаблоны, экранировать ли HTML-теги и т. д. В таблице ниже представлено простое сравнение нескольких шаблонизаторов.
Помимо основных функций, разные шаблонизаторы также предоставляют свои уникальные функции, например, artTemplate поддерживает точки останова на файлах шаблонов, что удобно для отладки при использовании, и есть некоторые вспомогательные методы, handlesbars также предоставляет рантайм-версию, которая можно использовать для шаблонов.Предварительная компиляция, логические выражения ejs пишутся так же, как в JavaScript, и т. д. здесь не будут перечислены.
-
размер
Для облегченного компонента мы уделим особое внимание конечному размеру компонента. Многофункциональный механизм шаблонов означает больший объем, поэтому нам необходимо провести определенные измерения с точки зрения функций и размера. artTemplate и doT маленькие, всего несколько КБ после сжатия, тогда как handlebars больше, а версия 4.0.11 все еще имеет 70+ КБ после сжатия.
(Примечание: часть данных на приведенном выше рисунке исходит из размера min.js на https://cdnjs.com/, а часть — из размера на git. Размер не соответствует размеру gzip)
-
представление
Если есть много частых обновлений DOM или большое количество DOM, которые необходимо отобразить, нам нужно обратить внимание на производительность механизма шаблонов при рендеринге.
Наконец, взяв наш проект в качестве примера, компонент, который мы хотим реализовать, является легким компонентом (в основном плавающий интерфейс и два полноэкранных интерфейса на уровне страницы).В то же время взаимодействие с пользователем также очень простое, а компонент не будет выполнять частые повторные рендеры. Но нас будет очень беспокоить общий размер компонента, и есть особая вещь, которая нам нужна для поддержки нескольких языков в копирайтинге компонента. Так что в итоге остановились на третьем варианте, описанном выше.