Удобная вставка многострочных шаблонных литералов в код на JavaScript

в 0:38, , рубрики: Code Style, ES6, javascript, node.js, template literals, template strings, Совершенный код

Описание проблемы

Появившиеся в ES6 шаблонные литералы (или шаблонные строки — template literals, template strings) помимо долгожданной интерполяции переменных и выражений принесли возможность вставки многострочного текста без дополнительных ухищрений, усложняющих вид кода.

Однако то, что красиво смотрится в разнообразных примерах на эту тему, в реальном коде порой облекается в новый вид безобразия.

Впрочем, проблемы видны, даже если присмотреться к примерам. Возьмём замечательную статью об этом нововведении из известной серии «ES6 In Depth».

Видите досадные «оспинки»? Лёгкие перекосы в симметрии и стройности?

Маленький пример

var text = (
`foo
bar
baz`)

Большой пример

var html = `<article>
  <header>
    <h1>${title}</h1>
  </header>
  <section>
    <div>${teaser}</div>
    <div>${body}</div>
  </section>
  <footer>
    <ul>
      ${tags.map(tag => `<li>${tag}</li>`).join('n      ')}
    </ul>
  </footer>
</article>`

Возьмём какой-нибудь простой случай и посмотрим на проблемы внимательнее.

const a = 1;
const b = 2;

console.log(
`a = ${a}.
b = ${b}.`
);

1. Первая кавычка искажает стройность текста, портит выравнивание строк.
2. Из-за смеси элементов литерала и кода, автоматически кажется, будто кавычки попадют в вывод. Приходится дополнительно абстрагироваться от них, чтобы представить, как будет выглядеть окончательный результат.
3. Строчки литерала оказываются вровень с вызовом функции, нарушается привычная структура отступов.

Можно сделать так:

const a = 1;
const b = 2;

console.log(`
  a = ${a}.
  b = ${b}.
`);

Это решает упомянутые проблемы: выравнивание строчек сохраняется, элементы кода и литерала разнесены, привычная структура отступов улучшает читабельность, ярче разделяет функцию и аргументы.

Но теперь у нас появляются лишние переводы строк и пробелы. Иногда с этим можно смириться, но на универсальное решение не тянет.

Усугубим наш пример введением дополнительных блоков и отступов.

const verbose = true;

if (verbose) {
  console.log(
`const a is ${a}.
const b is ${b}.`
  );
} else {
  console.log(
`a = ${a}.
b = ${b}.`
  );
}

Ужасно. Теперь литерал вообще выпирает слева, разрушая структуру блоков.

Можно исправить описанным выше способом:

if (verbose) {
  console.log(`
    const a is ${a}.
    const b is ${b}.
  `);
} else {
  console.log(`
    a = ${a}.
    b = ${b}.
  `);
}

Стало ещё больше «служебных» пробелов. А если придётся вставлять литерал на ещё более глубоком уровне вложенности? Всё это быстро выйдет из-под контроля.

Присваивания переменным или вызовы console.log можно заменить на функции записи в файлы, дилемма останется той же — или нечитабельная каша, или лишние пробелы и переводы строк:

fs.writeFileSync('log.txt',
`a = ${a}.
b = ${b}.`,
'ascii');

или

fs.writeFileSync('log.txt', `
  a = ${a}.
  b = ${b}.
`, 'ascii');

Я нашёл для себя выход, которым решил поделиться. Не столько потому, что рискнул посчитать его общеполезным, сколько для того, чтобы затеять обсуждение: вполне вероятно, что уже найдены другие выходы, и все желающие смогут их распространить.

Возможное решение

Оно кроется в области того же самого нововведения, а именно в функционале под названием «tagged templates». В уже упомянутой статье есть раздел, посвящённый этому механизму и «разжёвывающий» алгоритм его работы до значительной наглядности: «Demystifying Tagged Templates».

Приведённые автором «костяки» функций, обрабатывающих шаблонные литералы, натолкнули меня на мысль использовать нечто подобное и для удаления всех служебных пробелов и переводов строки из многострочных литералов. Получилась такая функция:

//remove auxiliary code spaces in template strings

function xs(strings, ...expressions) {
  const xLF = /^n|n$/g;
  const xSP = /n +/g;

  if (!expressions.length) {
    return strings[0].replace(xSP, 'n').replace(xLF, '');
  } else {
    return strings.reduce((acc, str, i) => {
      return (
        (i === 1? acc.replace(xSP, 'n') : acc) +
        expressions[i - 1] +
        str.replace(xSP, 'n')
      );
    }).replace(xLF, '');
  }
}

Или вариант, пригодный для Node.js на то время, пока rest parameters остаются под флагом:

//remove auxiliary code spaces in template strings

function xs(strings) {
  const expressions = Array.from(arguments).slice(1);
  
  const xLF = /^n|n$/g;
  const xSP = /n +/g;

  if (!expressions.length) {
    return strings[0].replace(xSP, 'n').replace(xLF, '');
  } else {
    return strings.reduce((acc, str, i) => {
      return (
        (i === 1? acc.replace(xSP, 'n') : acc) +
        expressions[i - 1] +
        str.replace(xSP, 'n')
      );
    }).replace(xLF, '');
  }
}

Как можно заметить, функция убирает по одному начальному и конечному переводу строки из конечного результата, а также удаляет все начальные пробелы в строчках (не затрагивая интерполируемые переменные и выражения).

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

const a = 1;
const b = 2;

console.log(xs`
  a = ${a}.
  b = ${b}.
`);

const verbose = true;

if (verbose) {
  console.log(xs`
    const a is ${a}.
    const b is ${b}.
  `);
} else {
  console.log(xs`
    a = ${a}.
    b = ${b}.
  `);
}

Теперь и код стал более ясным, и в вывод не попадает ничего лишнего.

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

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

Автор: vmb

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js