Эта идея пришла мне в голову, чтобы помочь с отладкой блестящих приложений R, с которыми вы можете взаимодействовать во время их работы. Вы можете вводить что угодно в консоль R, пока блестящее приложение работает.
Вот я и подумал, а можно ли использовать интерактивную оболочку внутри браузера, написанного на JavaScript? этот пост является результатом того, что я написал эту интерактивную оболочку.
Во-первых, когда мы хотим написать интерпретатор языка R, нам нужна функция eval, которая вернет результат в виде строки. Эта функция выглядит так.
r.eval <- function(code) { paste(capture.output(eval(parse(text = code))), collapse = "\n") }
Теперь внутри JavaScript нам нужно отправить строку, которую мы хотим оценить, из браузера в процесс R.
var index = 0; function exec(str) { var id = index++; Shiny.onInputChange('__EVAL__', {id: id, value: str}); }
Мы используем очень необычное имя события, вы должны выбрать другое, чтобы вас не взломали, если кто-то попытается использовать эту строку в вашем общедоступном приложении. Другое решение — добавить этот код только для определенных пользователей (например, разработчиков или администраторов).
Мы улучшим эту функцию позже, это будет функция, которая возвращает промис с результатом из R.
Теперь нам нужно написатьObservEvent (внутри серверной функции), чтобы получить значение из JavaScript и оценить значение в R и отправить результат обратно в браузер.
observeEvent(input[["__EVAL__"]], { data <- input[["__EVAL__"]] tryCatch({ payload <- list(id = data$id, result = r.eval(data$value)) session$sendCustomMessage("__EVAL__", payload) }, error = function(cond) { error <- paste(capture.output(traceback(cond)), collapse = "\n") payload <- list(id = data$id, error = error) session$sendCustomMessage("__EVAL__", payload) }) })
Внутри R у нас есть все. Далее нам нужен лучший код JavaScript. Если вам не нужен этот код и вам нужно решение, в конце есть закладка, которую можно использовать для создания терминала без написания JavaScript.
Для тех, кто хочет следовать, нам нужно написать лучшую функцию exec. Вот:
var exec = (function() { var handlers = []; if (typeof Shiny !== 'undefined') { Shiny.addCustomMessageHandler("__EVAL__", function(data) { handlers.forEach(function(handler) { handler(data); }); }); } var index = 0; function exec(str, callback) { if (!str.trim()) { // no data return Promise.resolve(); } return new Promise(function(resolve, reject) { var id = index++; handlers.push(function handler(data) { if (data.id === id) { if (data.error) { reject(data.error); } else { resolve(data.result); } } handlers = handlers.filter(function(f) { return f !== handler; }); }); Shiny.onInputChange('__EVAL__', {id: id, value: str}); }); } return exec; })();
Код использует синтаксис ES5, не стесняйтесь реорганизовать его для использования ES6 (ES2015) или любой новой версии JavaScript.
Теперь вы можете в консоли инструментов разработчика JavaScript оценить R-код и получить ответ:
await exec("10 + 10") [1] 20
await верхнего уровня может работать только в инструментах разработчика. если это не работает, вы можете использовать:
exec("10 + 10").then(result => console.log(result));
Теперь все, что нужно сделать, это написать красиво выглядящий терминал, чтобы вы могли вводить команды и получать ответ из блестящего приложения. Я буду использовать свою библиотеку JavaScript jQuery Terminal.
Сначала нам нужно подключить библиотеки:
<script src="https://unpkg.com/jquery.terminal/js/jquery.terminal.min.js"></script> <link href="https://unpkg.com/jquery.terminal/css/jquery.terminal.min.css" rel="stylesheet"/>
Мы также можем захотеть иметь подсветку синтаксиса R, чтобы он выглядел лучше. Нам нужен этот html для включения соответствующих библиотек:
<link href="https://unpkg.com/prismjs/themes/prism-coy.css" rel="stylesheet"/> <script src="https://unpkg.com/prismjs/prism.js"></script> <script src="https://unpkg.com/jquery.terminal/js/prism.js"></script> <script src="https://unpkg.com/prismjs/components/prism-r.min.js"></script>
После добавления зависимостей нам нужно создать терминал. Мы хотим, чтобы терминал располагался внизу экрана, поэтому сначала мы пишем базовую html-структуру.
<div class="shell-wrapper"> <span class="shell-destroy">[x]</span> <div id="r-term"></div> </div>
Теперь нам нужно придать стиль:
.terminal { --size: 1.2; height: 100%; } .shell-wrapper { position: fixed; z-index: 99999; bottom: 0; left: 0; right: 0; height: 150px; } .shell-destroy { position: absolute; right: 10px; color: #ccc; top: 10px; z-index: 10; cursor: pointer; font-family: monospace; }
Теперь заключительная часть — создание терминала с интерактивной оболочкой R.
var term = $('#r-term').terminal(function(cmd, term) { if (!cmd.trim()) { // we ignore empty command but exec will also handle that return; } return exec(cmd).then(function(result) { result = result.trim(); if (result) { term.echo($.terminal.escape_brackets(result)); } }).catch(function(e) { term.error(e); }); }, { greetings: 'R console\n' });
И теперь у вас есть настоящий терминал в браузере, который дает вам настоящую интерактивную сессию R в JavaScript.
Вы также можете использовать небольшой плагин под названием тильда вместо приведенного выше кода, вам нужно только использовать тильду вместо терминала. И у вас будет quake-подобная консоль в вашем приложении, которая открывается, когда вы нажимаете tidla ~ на клавиатуре.
Tidla можно найти на GitHub как один из примеров jQuery Terminal. Добавление Quake как консоль R остается читателю в качестве упражнения.
И как я и обещал вот ссылка на букмарклет, он не очень красивый, но в нем есть все из этого поста. Он загружает все файлы, и вы можете щелкнуть его несколько раз, чтобы уничтожить предыдущий терминал.
ОБНОВЛЕНИЕ:
Пока я интегрировал этот отладчик в свое блестящее приложение, я сделал несколько улучшений, которыми хочу поделиться.
Я создал две функции debugger.init() и debugger(). Init следует вызывать внутри серверной функции, поскольку для этого требуются входные данные и сеанс. И функцию отладчика можно использовать для изменения контекста, когда интерпретатор оценивает выражения, как и browser(), но приложение все еще работает.
А вот и код, который можно поместить в пакет.
#' To use Debugger you need to call debugger.init(input, session) #' inside shiny server function. Then you need complementary JS code #' that is avaiable as bookmarklet. Then in any context you can #' call debugger function to change the context (it's optional). #' If you call that function in constructor you will be able #' to access self and private properties of the object. #' eval to string function that work the same as R REPL #' @param code - string to evaluate #' @param env - enviroment in which to evaluate the code #' @export r.eval <- function(code, env = parent.frame()) { paste( capture.output(eval(parse(text = code), envir = env)), collapse = "\n" ) } #' debugger environment .global <- list2env(list(env = NULL)) #' function to use instead of browser() and put context of debugger in this place #' @export debugger <- function() { .global$env <- parent.frame() } #' Initialization of the debugger #' @param input - shiny input #' @param session - shiny session #' @export debugger.init <- function(input, session, env = parent.frame()) { .global$env <- env shiny::observeEvent(input[["__EVAL__"]], { data <- input[["__EVAL__"]] tryCatch({ payload <- list( id = data$id, result = r.eval(data$value, env = .global$env) ) session$sendCustomMessage("__EVAL__", payload) }, error = function(cond) { error <- paste( capture.output(traceback(cond)), collapse = "\n" ) payload <- list(id = data$id, error = error) session$sendCustomMessage("__EVAL__", payload) }) }) }