Анализ строкового шаблона

JavaScript ECMAScript 6 DOM

Автор: Цуй Цзин

предисловие

Хотя существуют различные интерфейсные фреймворки для повышения эффективности разработки, в некоторых случаях компоненты, реализованные на собственном JavaScript, также незаменимы. Например, в нашем проекте нам нужно предоставить общий платежный компонент для бизнес-стороны, но стек технологий, используемый бизнес-стороной, может бытьVue,Reactи т. д., даже собственный JavaScript. Тогда для достижения универсальности и обеспечения ремонтопригодности компонентов необходимо реализовать нативный компонент JavaScript.

Левое изображение ниже показывает общий вид нашего компонента Panel, а правое изображение показывает общую структуру каталогов нашего проекта:

Разбиваем компонент на.html,.js,.cssТри файла, такие как компонент Panel, содержат три файла: panel.html, panel.js и panel.css, так что представления, логика и стили могут быть разобраны для удобства обслуживания. Чтобы повысить гибкость компонента, заголовок в нашей панели, текст кнопки, а также количество и содержимое промежуточных элементов контролируются данными конфигурации, чтобы мы могли динамически отображать компоненты в соответствии с данные конфигурации. В этом процессе, чтобы сделать поток данных и событий более понятным, ссылаясь на дизайн Vue, мы ввели концепцию центра обработки данных, и данные, требуемые компонентами, хранятся в центре обработки данных единообразно. Изменение данных центра обработки данных приведет к обновлению компонента, а процесс обновления заключается в повторном отображении представления в соответствии с другими данными.

Panel.html — это то, что мы часто называем «шаблоном строки», а процесс его преобразования в исполняемый код JavaScript — это то, что делает «механизм шаблонов». Есть много шаблонизаторов на выбор, и они, как правило, предоставляют богатые функции. Но во многих случаях мы можем иметь дело с простым шаблоном, без слишком сложной логики, тогда нам достаточно простого строкового шаблона.

Несколько методов строковых шаблонов и простые принципы

В основном делятся на следующие категории:

  1. Просто и грубо - регулярная замена

    Самый простой и грубый способ — использовать строки напрямую для регулярной замены. Но не могу обрабатывать операторы цикла и если/иначе судить об этом.

    А. Определите способ записи строковой переменной, например, используя<%%>пакет

    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'
    })
    
  2. Простой и элегантный — синтаксис шаблона 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>
    `
    

    в строке шаблона${}Произвольные выражения могут быть записаны в , но таким же образом, если / иначе не могут быть обработаны суждения и операторы цикла.

  3. Простой механизм шаблонов

    Во многих случаях при рендеринге 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:

    1. <%%>Если это логический оператор (if/else/for/switch/case/break), то содержимое в середине напрямую преобразуется в код JavaScript. через регулярные выражения/(^( )?(var|if|for|else|switch|case|break|;))(.*)?/gОтфильтруйте логические выражения для обработки.
    2. <% xxx %>Если не логическое утверждение, то мы заменилиhtml.push(xxx)утверждение
    3. <%%>Для содержимого, отличного от , мы заменяем его на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Это обозначение используется в синтаксисе шаблона и также обычно называется интерполяционным выражением. Давайте посмотрим на простоймеханизм шаблонов синтаксиса усовреализация.

  4. шаблонизатор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'
      }
    })
    
    • Разбор шаблона

      Механизм шаблонов сначала анализирует шаблон. Процесс парсинга шаблона усов примерно таков:

      1. Обычная совпадающая часть, псевдокод выглядит следующим образом:
      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)
      }
      
      1. Затем, пройдяtokens, будет непрерывнымtextМассивы типов объединяются.

      2. траверс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, которые необходимо отобразить, нам нужно обратить внимание на производительность механизма шаблонов при рендеринге.

Наконец, взяв наш проект в качестве примера, компонент, который мы хотим реализовать, является легким компонентом (в основном плавающий интерфейс и два полноэкранных интерфейса на уровне страницы).В то же время взаимодействие с пользователем также очень простое, а компонент не будет выполнять частые повторные рендеры. Но нас будет очень беспокоить общий размер компонента, и есть особая вещь, которая нам нужна для поддержки нескольких языков в копирайтинге компонента. Так что в итоге остановились на третьем варианте, описанном выше.

Справочная документация