Редактор Mail.ru. Frontend

Preview:

Citation preview

Редактор Mail.ru

Александр Русаков, Ведущий программист

Редактор

• Просмотр документов

• Превью документов

• Редактирование документов

Задачи

• Просмотр документов

• Превью документов

Как решить задачу «в лоб»?

Этапы обработки документа

Получение модели

документа от сервера

Расчёт документа

Генерация HTML

Модель

{

"sections": [{

"elements": [{

"type": "paragraph",

"elements": [{

"type": "text",

"text": "Test\n",

"font": {

"bold": true

}

}],...

Не подходит для редактирования:

• Нельзя разделить на части

• Нет простого механизма адресации по

вложенной структуре

Модель. Минусы

Модель. Адресация

{

"sections": [{

"elements": [{

"type": "paragraph",

"elements": [{

"type": "text",

"text": "Test \n",

"font": {

"bold": true

}

}],...

Как адресовать символ?

Этапы обработки документа

Получение модели

документа от сервера

Расчёт документа

Генерация HTML

Заливка у слов разной высоты

Картинки «вокруг рамки»

Расчет документа. Зачем?

Браузер width

Chrome 38 285.53…

Firefox 33 283.17…

IE 11 286

Opera 12.16 280

Расчет документа. Получение размера

ctx.font = '28pt Arial';

ctx.measureText('Редактор Mail.ru');

Размеры слов и букв различаются между браузерами

Документ в разных браузерах

рассчитывается по разному

Расчет документа. Минусы

Firefox IE11

Этапы обработки документа

Получение модели

документа от сервера

Расчёт документа

Генерация HTML

HTML

• Отображение текста различается между

браузерами

• Низкая скорость манипуляций с DOM

HTML. Минусы

Исполняем js-код на сервере

НО PhantomJS:

• Медленный

• Нестабильный

Превью. PhantomJS

Формулировка проблем

Использовали Проблема

Модель вложенная Нельзя разделить на части

Сложный механизм адресации

Размер шрифта вычисляется в

браузере

Разный размер букв и слов в

разных браузерах

HTML Разное отображение текста

Медленные манипуляции с DOM

PhantomJS Медленный

Нестабильный

Отказываемся от получения модели от сервера.

От сервера принимаем набор команд.

Модель вложенная. Как разделить на части?

Команды – низкоуровневые операции, изменяющие документ.

Например:

• TextInsert (start : number, text : string)

• TextDelete (start : number, end : number)

• ParagraphUpdate (end : number, props: Object)

• …

Команды

Команды. Пример

TextInsert(0,'П')

TextInsert(1,'р')

TextInsert(2,'у')

TextInsert(3,'д')

Команды. Пример

TextDelete(0, 1)

TextInsert(0,'Т')

Приходят с сервера

Создаются на клиенте на каждое действие пользователя:

• Ввод текста

• Изменение свойств (шрифт, цвет и т.д.)

Команды. Откуда?

Отправляются на сервер:

• По таймауту (автосохранение)

• По нажатию кнопки «Сохранить»

Команды. Куда?

Модель вложенная. Как адресовать символ?

Модель вложенная

Параграф Таблица

Текст ЯчейкаТекст Ячейка

ТаблицаПараграф

ЯчейкаТекст

Новая плоская модель

Модель плоская

Параграф 1 Параграф N

Текст

Таблица 1 Таблица M

Ячейка 1 Ячейка L

Команды формируют модель

{

"text": "Hello, world!\n

}

TextInsert(0,'Hello, world!\n')

Команды формируют модель на клиенте

RunUpdate(0, 4, {

bold: true

})

{

"text": "Hello, world!\n",

"runs": [{

"start": 0,

"end": 4,

"bold": true

}]

}

Команды формируют модель на клиенте

{

"text": "Hello, world!\n",

"runs": [{

"start": 0,

"end": 4,

"bold": true

}],

"paragraphs": [{

"end": 13

}]

}

Paragraph(13)

Модель. «Плоская»

{

"text": "Hello, world!\n",

"runs": [{

"start": 0,

"end": 4,

"bold": true

}],

"paragraphs": [{

"end": 13

}]

}

Свойство text – весь текст

документа.

Адресация по индексу

строки text.

Каждый браузер измеряет по-своему

Расчет документа. Разные размеры букв

Каждый браузер измеряет по-своему

Решение. Присылаем размеры шрифта с сервера

Расчет документа. Разные размеры букв

Каждый браузер измеряет по-своему

Решение. Присылаем размеры шрифта с сервера

В HTML нельзя указать размер слова по ширине

Расчет документа. Разные размеры букв

Каждый браузер измеряет по-своему

Решение. Присылаем размеры шрифта с сервера

В HTML нельзя указать размер слова по ширине

Решение. Canvas может принудительно уместить слово

по ширине

Расчет документа. Разные размеры букв

Шрифтов много хороших и разных…

Какие шрифты нужны?

Расчет документа. Шрифты

Расчет документа. Статистика по шрифтам

Times New Roman Calibri

Tahoma Arial

Courier New Symbol

Wingdings Cambria

Verdana

Расчет документа. Размеры шрифта

{

"widths": {

"a": 1139, ...

},

"ascent": 1921,

"descent": -434,

"factor": 2048

}

C сервера приходят JSON c рассчитанными размерами шрифта

Расчет размеров букв теперь не зависят от браузера!

Расчет документа. Итоговая структура

Документ

Секция

Страница

Строка

Слово

Расчет документа. Итоговая структура

Документ

Секция

Страница

Строка

Слово

Расчет документа. Итоговая структура

Документ

Секция

Страница

Строка

Слово

Расчет документа. Итоговая структура

Документ

Секция

Страница

Строка

Слово

Расчет документа. Слова

Простое слово

Пробельное слово

Табуляция

Разрыв страницы

Перенос строки

Расчет документа. Процесс

Слово большее по высоте сдвигает строку вниз

Расчет документа. Процесс

Если слово длиннее строки, то слово разделяется на две части

Расчет документа. Что-то посложнее

Как рассчитать подобное?

Картинка имеет обтекание текстом – «вокруг рамки»

Расчет документа. Что-то посложнее

Сначала располагаем картинку относительно

левого верхнего угла параграфа

Расчет документа. Что-то посложнее

Пытаем получить прямоугольник для строки – пересечение!

Расчет документа. Что-то посложнее

Строка разбивается на несколько допустимых участков

Расчет документа. Что-то посложнее

Размещаем слова по допустимым участкам

Расчет документа. Что-то посложнее

Итак весь параграф

4 аргумент – максимальная ширина текста

Canvas

ctx.fillText(text, x, y, maxWidth);

Canvas может принудительно уместить слово в

заданный размер

• Полный контроль над расположением элементов и их размерами

• Простая логика на уровне View

• Производительность (client side and server side)

• Используем node.js + node-canvas вместо phantomjs

• C согласия пользователя к репорту прикрепляется скриншот

документа

Canvas. Плюсы

Нет DOM:

• Самостоятельная обработка событий пользователя

• Нельзя заменить содержимое тега, поменять стиль => только

перерисовать

Canvas. Минусы

Используем два <canvas> на странице

Canvas. Больше одного на странице

Один – для текста, второй – для выделения и курсора

ctx.fillRect() должен работать с целыми числами, нужно округлять

Canvas. Округление и целые числа

ctx.fillRect(

round(x),

round(y),

round(w),

round(h)

);

Canvas. Округление и целые числа

y = 10.5;

h = 4.7;

round(y) === 11;

round(h) === 5;

Canvas. Округление и целые числа

y = 10.5;

h = 4.7;

round(y) === 11;

round(h) === 5;

round(y) === 11;

round(y + h) - round(y) === 4;

Заливка находится под текстом, но перед фоном страницы

Строка может вылезать за пределы страницы – нужно обрезать

Canvas. Заливка и обрезка

Рисуем фон под текстом (текст + фон за один проход)

Canvas. Off-screen canvas

ctx.globalCompositeOperation = 'destination-over';

Копируем off-screen canvas в основной с помощью drawImage

var offSreenCanvas = document.createElement('canvas');

ctx.drawImage(offSreenCanvas, ...);

Canvas. Превью и печать

Модуль node-canvas – Canvas API для Node.js

Дает возможность генерировать PDF (печать!)

ctx.addPage(width, height);

Canvas. PDF, первый подход

Использование Off-screen canvas растеризовало текст

Отказываемся от off-sreen canvas в пользу метода clip() для обрезки

ctx.save();

ctx.rect(x, y, w, h);

ctx.clip();

// render

ctx.restore();

Canvas. Почему все стало тормозить?!

save() / restore() действительно сбрасывают clip()

Но:clip() оперирует путем (path)

Путь всегда один

Путь не сбрасывается во время restore()

Canvas. Правильное использование clip()

ctx.save();

ctx.beginPath();

ctx.rect(x, y, w, h);

ctx.clip();

// render

ctx.restore();

Тень вокруг листа.

В превью не нужна тень!

Убрали тень в генерации

превью (Node.js) – время

снизилось в среднем на

50мс

Canvas. Ускоряем превью

Canvas. Дисплеи retina и devicePixelRatio

ctx.scale(devicePixelRatio, devicePixelRatio);

<canvas width="1486" height="434" style="height: 347px; width:1189px;">

</canvas>

Увеличиваем размер Canvas в devicePixeRatio раз и

сжимаем обратно с помощью CSS

В отрисовке увеличиваем масштаб

Результат

Было Стало

Отображение в разных

браузерах

Разное Идентичное

Генерация превью PhantomJS – 600 мс Node.js – 350 мс

Печать Средствами

браузера

Генерация PDF для

печати на сервере

СПАСИБОЗА ВНИМАНИЕa.rusakov@corp.mail.ru

Инструмент Функции

git + phabricator + arcanist Управление и review кода

Grunt Сборка проекта

SASS + autoprefixer Работаем c css

Karma+PhantomJS, instabul Запуск тестов, code coverage

Что мы используем?

Библиотека Функции

Requirejs AMD модули

jQuery Обработка событий + AJAX

Mail.ru FileAPI Загрузка картинок на сервер

Jasmine Unit тесты

Node-canvas Серверная реализация Canvas API

Что мы используем?