Это волшебство или хитрость?
В прошлом посте мы рассмотрели:
- Загрузка из 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, который выполняет то же самое.
Исходно из: