Это волшебство или хитрость?

В прошлом посте мы рассмотрели:

  • Загрузка из WebStorm в GitHub
  • Семантическое управление версиями
  • Обычные коммиты

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

Поведение первой итерации

Первой реализацией была функция eval, результат которой отображался на панели результатов. Ввод строки «hello» приведет к отображению hello. Кроме того, если назначить переменную, а затем поместить только эту переменную в последнюю строку, в области результатов отобразится значение переменной.

Вместо этого было бы более интуитивно использовать console.log, что MDN и сделал в своих интерактивных примерах.

Предыстория второй итерации

В NodeJS можно прочитать поток process.stdout или создать новый регистратор с пользовательскими потоками. Итак, я искал ресурсы о том, как перехватить вывод браузера console.log. Насколько я понял, нет способа получить вывод операторов console в браузере.

Однако можно перехватить аргументы console.log при вызове. У ответа StackOverflow есть решение. Переназначив исходный оператор console.log новой переменной, мы можем назначить нашу собственную пользовательскую функцию console.log. Обратите внимание, что это не перехватывает вывод операторов журнала, а перехватывает только аргументы.

Как это делает MDN?

У MDN, похоже, есть возможность читать вывод консоли, но при дальнейшем рассмотрении вывод интерактивного редактора MDN отличается от вывода консоли браузера. Это означает, что они должны сами обрабатывать форматирование.

Глядя на минимизированный код editor-js MDN, new Function(t)() используется вместо eval(t) для разбора и запуска ввода. Также есть блок try-catch для обработки ошибок.

!function(t) {
  d.classList.add("fade-in");
  try {
    new Function(t)()
  } catch (t) {
    d.textContent = "Error: " + t.message
  }
  d.addEventListener("animationend", function() {
    d.classList.remove("fade-in")
  })
}(e.getDoc().getValue())

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

var e = t("./console-utils")
  , n = console.log
  , r = console.error;
console.error = function(t) {
  e.writeOutput(t),
  r.apply(console, arguments)
},
console.log = function() {
  for (var t = [], r = 0, i = arguments.length; r < i; r++) {
    var o = e.formatOutput(arguments[r]);
    t.push(o)
  }
  var a = t.join(" ");
  e.writeOutput(a),
  n.apply(console, arguments)
}

Этот код хорошо сочетается с ответом StackOverflow. Каждый аргумент повторяется и форматируется, а затем объединяется. Исходные аргументы также передаются обратно исходному console.log.

Охота за исходным кодом

После некоторого поиска я обнаружил Построитель битов MDN (BoB). Это репозиторий, отвечающий за интерактивные примеры в MDN. Mozilla также любезно предоставила репозиторию лицензию MIT.

Например, это оригинальный исходный код для уменьшенного блока кода выше:

module.exports = function() {
    'use strict';
var consoleUtils = require('./console-utils');
    var originalConsoleLogger = console.log; // eslint-disable-line no-console
    var originalConsoleError = console.error;
console.error = function(loggedItem) {
        consoleUtils.writeOutput(loggedItem);
        // do not swallow console.error
        originalConsoleError.apply(console, arguments);
    };
// eslint-disable-next-line no-console
    console.log = function() {
        var formattedList = [];
        for (var i = 0, l = arguments.length; i < l; i++) {
            var formatted = consoleUtils.formatOutput(arguments[i]);
            formattedList.push(formatted);
        }
        var output = formattedList.join(' ');
        consoleUtils.writeOutput(output);
        // do not swallow console.log
        originalConsoleLogger.apply(console, arguments);
    };
};

Для тех, кому интересно, файл, отвечающий за форматирование логов MDN, находится здесь: https://github.com/mdn/bob/blob/master/editor/js/editor-libs/console-utils.js. У него есть некоторые правила, которые отвечают за форматирование строк журнала, когда вывода собственного метода toString() недостаточно.

Глядя на другие варианты

После установки mdn-bob я решил, что библиотека слишком специфична для варианта использования MDN. Например, в библиотеке были стили CSS, которые мне не нужны. Мне нужен был только небольшой фрагмент кода, форматер.

Сам NodeJS имеет родную util библиотеку с методом inspect, которая может форматировать что угодно. После некоторого поиска в NPM портов браузера util.inspect я остановился на object-inspect.

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

Создание второй итерации

Объединив ответ StackOverflow с MDN BoB, я начал с назначения исходных операторов консоли.

const originalConsoleLogger = console.log;
const originalConsoleError = console.error;

Обратите внимание, что поскольку вызывается console.log, а не window.console.log, у нас не должно возникнуть проблем с рендерингом на стороне сервера (SSR).

Я переназначил console.log и console.error своим соответствующим пользовательским функциям. В конечном итоге они должны войти в наш компонент React, поскольку результатом обработки аргументов должно быть обновление состояния компонента.

console.error = function () {
  // handle arguments
  originalConsoleError.apply(console, arguments)
}
console.log = function () {
  // handle arguments
  originalConsoleLogger.apply(console, arguments)
}

Я заметил, что мы можем преобразовать это в синтаксис ES6, используя функции стрелок, а также используя остальные параметры вместо arguments. Этот подход предложен MDN. Чтобы сохранить симметрию между параметрами функции и выполнением исходной консольной функции, я решил использовать .call вместо .apply.

console.error = (...args) => {
  // handle arguments
  originalConsoleError.call(console, ...args)
}
console.log = (...args) => {
  // handle arguments
  originalConsoleLogger.call(console, ...args)
}

Затем аргументы должны быть обработаны библиотекой object-inspect. Вместо использования цикла for я решил использовать Array.reduce. Хотя годы работы с ESLint научили меня не использовать тип any, я думаю, что в данном случае это приемлемо, поскольку objectInspect ожидает any в качестве входных данных. С таким количеством «аргументов» это, несомненно, любимая функция пиратов.

import objectInspect from "object-inspect";
/* ... */
const reduceArgs = (formattedList: any[], arg: any) => [
  ...formattedList,
  objectInspect(arg),
];

const formatArgs = (args: any[]) => args.reduce(reduceArgs, []).join(" ");

Использование функции вместо Eval

Согласно совету MDN, использование Function быстрее и безопаснее, чем eval. Обратите внимание, что использование любого из них небезопасно в большинстве случаев. В этом случае код предоставляется пользователем в собственном браузере и нигде больше не сохраняется и не используется повторно.

try {
  new Function(code)();
} catch (e) {
  console.error(e);
}

Внедрение в React

Все, что нужно, — это поместить каждую функцию в ту же область видимости, что и состояния setResult и setError, и обновить состояние, используя выходные данные formatArgs.

const StringPage = () => {
  const [result, setResult] = React.useState("");
  const [error, setError] = React.useState("");
  const codeRef = React.useRef<HTMLTextAreaElement>(null);
  console.log = (...args: any[]) => {
    const formattedLog = formatArgs(args);
    setResult(formattedLog);
    originalConsoleLogger.call(console, ...args);
  };

  console.error = function (...args: any[]) {
    const formattedError = formatArgs(args);
    setError(formattedError);
    originalConsoleError.call(console, ...args);
  };
  const evaluateCode = () => {
    if (codeRef.current === null) return;
    const code = codeRef.current.value;
    if(code.length < 1) return;
    try {
      new Function(code)();
    } catch (e) {
      console.error(e);
    }
  };
  return (
    <>
      {/* surrounding JSX removed for clarity */}
      {result}
      {error}
    </>
  );

TL; DR

Вывод браузера console не может быть прочитан. Интерактивные примеры MDN переопределяют console.log, форматируют аргументы для веб-страницы, а затем вызывают исходный console.log. Я создал компонент в React, который выполняет то же самое.

Исходно из: