Статьи

Добавьте веб-консоль на панель инструментов, часть 2

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

Узнайте, как работает консоль

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

  var Console = 
 {
    init: function (canvasName, numCols, numRows)
          {
          },

    очистить: функция ()
           {
           },

    getLine: функция (обратный вызов)
             {
             },

    эхо: функция (сообщение)
          {
          },

    render: function ()
            {
            },

    writeChar: функция (ch)
               {
               },

    прокрутка: функция ()
            {
            }
 } 

Листинг 1: Скелетная структура консоли библиотеки

В листинге 1 показан глобальный объект с именем Console состоящий из семи свойств функции. Первые четыре свойства включают в себя общедоступный API, тогда как последние три свойства следует считать закрытыми и недоступными. Этот список свойств далек от завершения, потому что init(canvasName, numCols, numRows) вводит дополнительные свойства.

Заметка
Я мог бы «спрятать» последние три свойства, введя такие выражения, как Console.writeChar = function(ch) { /* code here */ } внутри init(canvasName, numCols, numRows) . Я решил не делать так, чтобы init(canvasName, numCols, numRows) больше не получал.

Откройте для себя init(canvasName, numCols, numRows)

В листинге 2 представлены init(canvasName, numCols, numRows) .

  init: function (canvasName, numCols, numRows)
       {
          var canvas = document.getElementById (canvasName);  Console.numCols = numCols;  Console.numRows = numRows;  Console.ctx = canvas.getContext ("2d");  Console.ctx.font = "20px / 20px monospace";  Console.ctx.textBaseline = "top";  Console.charWidth = Console.ctx.measureText ("m").   ширина;  Console.charHeight = 20;  canvas.width = Console.charWidth * numCols + 10;  canvas.height = Console.charHeight * numRows + 10;  Console.buffer = document.createElement ("   canvas "); Console.buffer.width = canvas.width; Console.buffer.height = canvas.height; Console.bufferCtx = Console.buffer.getContext (" 2d "   );  Console.bufferCtx.font = "20px / 20px monospace";  Console.bufferCtx.textBaseline = "top";  Console.screen = new Array (numRows);  for (var row = 0; row <numRows; row ++) Console.screen [row] = new Array (numCols);  Console.keyQueue = new Array ();  function keyDown (event) {// Эта функция вызывается всеми браузерами для возврата.  if (event.keyCode == 8) // Backspace?  {Console.keyQueue.push ("b");  // Следующий код необходим Chrome для предотвращения возврата backspace // в историю страниц.  event.preventDefault ();  }} canvas.addEventListener ("   keydown ", keyDown, true); функция keyPress (event) {if (event.keyCode == 8) {// Opera требует следующий код для предотвращения возврата backspace // в историю страниц. event.preventDefault () ; return;} if (event.keyCode == 13) // return? {Console.keyQueue.push ("n"); return;} var ch = (event.keyCode == 0)? event.charCode: event. keyCode; if (ch> = 32 && ch <127) Console.keyQueue.push (String.   fromCharCode (ч));  } canvas.addEventListener ("   keypress ", keyPress, true); canvas.tabIndex = 0; // Поместить холст в порядок табуляции. canvas.focus (); // Назначить фокус холсту клавиатуры. Не работает в // Internet Explorer. Console.cursorOn = true; Console.cursorCounter = 0; Console.cursorCounterMax = 5; Console.line = ""; Console.clear ();} 

Листинг 2: Инициализация консоли библиотеки

Листинг 2 сначала получает ссылку на именованный элемент <canvas> и сохраняет количество столбцов и количество строк в свойствах Console для использования другими функциями. Затем он получает контекст для рисования на этом холсте и инициализирует этот контекст для моноширинного шрифта размером 20 пикселей. Базовая линия текста устанавливается в верхней части шрифта, чтобы координаты символа были относительно его верхнего левого угла.

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

Следующие несколько строк создают и инициализируют буфер для поддержки двойной буферизации. Цель состоит в том, чтобы избежать мерцания, рисуя в буфер и затем копируя содержимое буфера на холст. Различные посты в блоге (например, http://stackoverflow.com/ questions / 2795269 / do-html5- холст-поддержка-двойной буферизация ) предполагают, что современные браузеры поддерживают двойную буферизацию автоматически, тогда как старые браузеры обычно не поддерживают.

Теперь создан двумерный screen массив для хранения символов, отображаемых в консоли. JavaScript реализует двумерный массив как одномерный массив строк из одномерных массивов столбцов. Объект Array используется для создания массива строк, а затем для каждого элемента строки — массива столбцов, ссылка на которые назначена элементу массива строк.

Хотя каждая строка в этой таблице потенциально может хранить разное количество столбцов (которое называется рваным массивом ), я решил зафиксировать количество столбцов в значении, передаваемом конструктору Array . Доступ к элементу в массиве Screen осуществляется через синтаксис Console.screen[ row ][ col ] — индексы строк и столбцов начинаются с нуля.

keyQueue объект Array используется для создания массива keyQueue для хранения символов и специальных нажатий клавиш (например, новой строки). Этот массив ведет себя как очередь, в которой нажатия клавиш добавляются на одном конце и удаляются на другом. Код, который добавляет нажатия клавиш в эту очередь, содержится в паре функций обработки событий, которые зарегистрированы на холсте для реагирования на события нажатия клавиш и нажатия клавиш.

Обработчик события нажатия клавиши реагирует только на клавишу возврата. Я бы предпочел обрабатывать эту клавишу с помощью нажатия клавиши, но этот обработчик событий не вызывается при нажатии клавиши Backspace в контекстах Internet Explorer, Chrome или Safari. После добавления b в очередь при нажатии клавиши выполняется event.preventDefault() чтобы предотвратить замену текущей страницы предыдущей страницей в истории страниц Chrome.

Обработчик события нажатия клавиши также реагирует на клавишу возврата для Firefox и Opera. Он игнорирует этот ключ в этих браузерах (было бы неплохо добавить второй b код в очередь), но выполняет event.preventDefault() для предотвращения замены текущей страницы предыдущей страницей в истории страниц Opera.

Обработчик события нажатия клавиши также реагирует на клавишу ввода / возврата, добавляя символ новой строки в очередь, и реагирует на ключи, чьи коды варьируются от 32 до 126, вызывая функцию fromCharCode() для кода и добавляя эквивалент персонаж в очередь. В Firefox keyCode содержит 0 для символьной клавиши (например, A), и соответствующий код должен быть получен из charCode .

Заметка
Opera не поддерживает charCode , но keyCode различает прописные и строчные буквы в контексте нажатия клавиш.

HTML предоставляет атрибут tabindex и свойство DOM tabIndex для размещения элементов в порядке табуляции. Для этой цели ноль присваивается свойству tabIndex холста. Затем вызывается функция focus() холста, чтобы придать этому элементу фокус клавиатуры. Хотя основное внимание уделяется Firefox, основное внимание не уделяется Internet Explorer — вы должны нажать клавишу Tab один раз или щелкнуть мышью на холсте.

Канва управляет курсором через cursorOn , cursorCounter и cursorCounterMax . Курсор виден, когда true назначен для cursorOn (значение по умолчанию), и курсор остается видимым до cursorCounter пор, пока cursorCounter достигает cursorCounterMax , после чего он сбрасывается в 0. Затем он становится невидимым и остается таковым в течение той же продолжительности.

Есть две последние задачи для init(canvasName, numCols, numRows) для выполнения. Во-первых, он назначает пустую строку свойству line , которое является буфером для хранения символов до нажатия Enter / Return. Во-вторых, он вызывает функцию clear() для очистки консоли и сброса положения курсора в верхнюю левую позицию символа.

Откройте для себя clear()

В листинге 3 представлены clear() .

  очистить: функция ()
        {
           for (var row = 0; row <Console.numRows; row ++)
              для (var col = 0; col <Console.numCols; col ++)
                 Console.screen [row] [col] = "";
           Console.row = 0;
           Console.col = 0;
           Console.render ();
        } 

Листинг 3: Очистка консоли

Листинг 3 очищает консоль, назначая пробел каждому элементу screen массива. (Хотя это и не очень производительно, я подчеркиваю ясность. Я мог бы ускорить код, используя функцию splice() Array .) Затем он сбрасывает текущую позицию курсора (указанную в свойствах col и row Console ) в верхняя левая позиция символа и отображает содержимое screen на холст. (Я обсуждаю render() позже.)

Откройте для себя getLine(callback)

В листинге 4 представлен getLine(callback) .

  getLine: функция (обратный вызов)
          {
             Console.render ();  // обновить курсор

             if (Console.keyQueue.length == 0)
             {
                if (Console.line.length == 0)
                {
                   if (обратный вызов! = не определено)
                      Перезвони();
                }
                вернуть ноль;
             }
             var ch = Console.keyQueue.shift ();
             if (ch == "b") // обрабатывать возврат
             {
                if (Console.line.length! = 0)
                {
                   Console.line = Console.line.substr (0, 
                                                      Console.line.length-1);
                   Console.echo (ч);
                }
                вернуть ноль;
             }
             Console.echo (ч);
             if (ch == "n") // обрабатывать перевод строки
             {
                var temp = Console.line;
                Console.line = "";
                вернуть темп;
             }
             еще
                Console.line + = ch;
             вернуть ноль;
          } 

Листинг 4: Получение строки ввода

В листинге 4 описывается функция опроса, которая постоянно проверяет ввод и обрабатывает этот ввод по одному символу за раз. Первая задача — показать или скрыть курсор, и эта задача выполняется путем вызова Console.render() . Поскольку getLine() постоянно вызывается демо консоли и приложениями оболочки браузера, иллюзия мигающего курсора сохраняется.

Заметка
Частота мигания курсора зависит от значения задержки, переданного в setInterval() . Чем больше значение задержки, тем медленнее курсор мигает.

Следующая задача — определить, присутствуют ли какие-либо символы в очереди. Если очередь пуста, getLine() может вернуться. Однако сначала необходимо вызвать любую функцию обратного вызова, переданную в качестве аргумента, но она может вызывать эту функцию только в том случае, если буфер line пуст (строка ввода не выполняется), чтобы предотвратить искажение строки ввода, как показано при обсуждении оболочка браузера.

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

После getLine() символа в консоль getLine() проверяет текущий символ, чтобы увидеть, является ли он символом новой строки. Если это так, буфер line сбрасывается в пустую строку в ожидании следующей строки ввода, и возвращается его предыдущее содержимое. В противном случае текущий символ добавляется в этот буфер и возвращается значение null поскольку полная строка ввода еще не доступна.

Откройте echo(msg)

В листинге 5 представлено echo(msg) .

  эхо: функция (сообщение)
       {
          для (var i = 0; i <msg.length; i ++)
             Console.writeChar (msg.charAt (i));  Console.render ();  } 

Листинг 5: Вывод строки в консоль

В листинге 5 выводится строка символов для консоли по одному символу за раз, обновляя текущую позицию курсора в процессе. Код использует writeChar(ch) для этой цели, и я вскоре объясню эту функцию. После записи строки вызывается Console.render() для обновления холста содержимым массива screen .

Откройте render()

В листинге 6 представлены render() .

  render: function ()
         {
            Console.bufferCtx.fillStyle = "# 000";  // черный
            Console.bufferCtx.fillRect (0, 0, Console.ctx.canvas.width, 
                                       Console.ctx.canvas.height);
            Console.bufferCtx.fillStyle = "# 0f0";  // зеленый
            var y = 0;
            for (var row = 0; row <Console.numRows; row ++)
            {
               var x = 0;
               для (var col = 0; col <Console.numCols; col ++)
               {
                   var s = Console.screen [row] [col] + "";
                   Console.bufferCtx.fillText (s, x + 5, y + 5);
                   x + = Console.charWidth;
               }
               y + = Console.charHeight;
            }
            if (Console.cursorOn)
               Console.bufferCtx.fillStyle = "# 0f0";  // зеленый
            еще
               Console.bufferCtx.fillStyle = "# 000";  // черный
            Console.bufferCtx.fillText ("_", Console.col * Console.charWidth +   5, Console.row * Консоль.   charHeight + 5);  if (++ Console.cursorCounter == Console.cursorCounterMax) {Console.cursorCounter = 0;  Console.cursorOn =! Console.cursorOn;  } Console.ctx.drawImage (Консоль.   буфер, 0, 0);  } 

Листинг 6: Отображение массива screen в буфер (и многое другое)

Листинг 6 отвечает за отображение массива screen в буфер и обновление курсора. Он рендерится в фоновый буфер и в конечном итоге копирует этот буфер на холст, чтобы избежать мерцания в тех браузерах, где может возникнуть мерцание. Фоновый буфер сначала очищается до черного, чтобы удалить потенциальный мусор из предыдущего рендеринга.

После установки зеленого цвета для рисования render() выполняет итерацию по массиву screen , вызывая функцию fillText() API Canvas для рисования каждого символа. Смещение в пять пикселей добавляется в верхний левый угол символа для поддержки пустой границы, нарисованной вокруг консоли. Символы разделены по горизонтали пикселями charWidth и по вертикали пикселями charHeight .

Следующий раздел кода отвечает за отрисовку курсора. Подходящий стиль заливки выбирается на основе значения cursorOn : зеленый, когда true, и черный, если false. Затем в текущей позиции курсора отображается зеленое или черное подчеркивание. Черное подчеркивание полностью стирает то, что отображалось ранее.

Наконец, устанавливается интервал включения / выключения курсора: курсор виден при пяти вызовах render() и невидим при пяти вызовах этой функции. Поскольку render() вызывается методом getLine() , а getLine() вызывается приложением под управлением setInterval() , наблюдается мигающий курсор.

Откройте для себя writeChar(ch)

В листинге 7 представлены writeChar(ch) .

  writeChar: функция (ch)
            {
               если (ch == "b")
               {
                  if (Console.col == 0 && Console.row == 0)
                     возвращение;  // не могу вернуться за верхний левый угол
                  Console.col--;
                  if (Console.col <0)
                  {
                     Console.col = Console.numCols-1; 
                     Console.row--;
                  }
                  Console.screen [Console.row] [Console.col] = "";  возвращение;  } if (ch == "n") {Console.col = 0;  if (++ Console.row> = Console.numRows) Console.scroll ();  возвращение;  } Console.screen [Console.row] [   Console.col] = ch;  if (++ Console.col> = Console.numCols) {Console.col = 0;  if (++ Console.row> = Console.numRows) Console.scroll ();  }} 

Листинг 7: Запись одного символа в массив screen

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

Откройте scroll

В листинге 8 представлен scroll() .

  прокрутка: функция ()
         {
            Console.row = Console.numRows-1;
            for (var row = 0; row <Console.numRows-1; row ++)
                для (var col = 0; col <Console.numCols; col ++)
                   Console.screen [row] [col] = Console.screen [row + 1] [col];
            для (var col = 0; col <Console.numCols; col ++)
               Console.screen [Console.  numRows-1] [col] = "";  } 

Листинг 8: Прокрутка консоли вверх на один ряд

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

Вывод

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

Заметка
Все файлы, относящиеся к этой статье, находятся в code.zip .