- PVSM.RU - https://www.pvsm.ru -

Снова об Electron или рисуем музыку ВК

image

Добра всем!
Electron — эта такая забавная штука, про которую мало статей на хабре(сходу нашел только habrahabr.ru/post/272075 [1] и habrahabr.ru/post/278951 [2]). Давно хотел написать что-нибудь такое-эдакое, вот руки и дошли — заодно и одним велосипедом в мире станет больше.

Итак, если вкратце: electron — это такой гибрид node.js и chromium'а. Зачем? Очень разнообразный диапазон применений — мощное GUI(html/js/css), нехилая расширяемость(в том числе с возможностью использования других языков вроде C++ или C#), всякие приятности вроде jQuery и т.д. В-общем, удобная штука для разработки и дистрибуции standalone кроссплатформенных приложений.
Теперь о приложении. Оно реализует базовый пример расширения функционала стороннего сайта, базовые принципы работы с Raphael.js [3](графическая библиотека для отрисовки/анимации svg), Dancer.js [4](библиотека для визуализации звука, в данном случае — получения audio waveform).

Начнем со структуры проекта.

image

Package.json — это описание проекта с указанием точки входа(в нашем случае это index.js). Остальные файлы я обычно сую в папку views — названную так больше по привычке, нежели с желанием указать на ее назначение.

Теперь рассмотрим точку входа. На данный момент я сталкивался с 3 подходами на тему кооперации нодовского контекста и контента страницы: preload скрипт+нормальная навигация по страницам, iframe либо webview.
Webview мне, откровенно говоря, не нравится за счет отсутствия возможности прямой манипуляции контентом из родительского окна(нужно либо использовать IPC [5], либо глобальные объекты через remote.getGlobal(передается, что характерно, не ссылка на объект, а его, скажем прямо, хреновенький клон. К примеру, манипулировать содержимым window не выйдет). Впрочем, в нем же есть всякие вкусности вроде подмены referer, useragent, preload-скриптов и прочих штучек-дрючек. Отлично подходит, если не нужно работать с контентом страницы и
Iframe уже получше — есть прямой доступ к контенту(даже кроссдоменому, если вырубить web-security), но иногда бывают подставы в стиле проверки window.top. Думаете, здесь есть что-нибудь вроде nwfaketop из node-webkit? Эх, если бы. Я, к сожалению, нормального способа так и не нашел. Отлично подходит для большинства случаев, в которых нет проверки window.top и прочих радостей жизни. К сожалению, у vk они есть.
Третий способ, в общем-то, пожалуй самый простой, хотя и у него есть свои ограничения — к примеру, скрипты в страницах, в которых есть проверка на module/define/exports(типа jQuery или Raphael) радостно дохнут при виде нодовских кусков, а посему приходится вырубать node-integration и использовать нодовский контекст только в preload-скриптах. Впрочем, безопасность этого тоже требует, так что не слишком сильно и огорчаемся.

Index.js у меня, по большей части стандартный, из доков, за парой исключений:

"use strict";
(function () {
  var app = require('app');
  var BrowserWindow = require('browser-window');
  
  app.commandLine.appendSwitch('disable-web-security');
  app.commandLine.appendSwitch('web-security');
  app.commandLine.appendSwitch('allow-displaying-insecure-content');
  app.commandLine.appendSwitch('ignore-certificate-errors');

  var mainWindow = null;
  app.on('window-all-closed', function() {
    if (process.platform != 'darwin')
      app.quit();
  });

  app.on('ready', function() {
    mainWindow = new BrowserWindow({
        width: 800, 
        height: 600,
        'web-preferences': {
            'web-security': false
        },
        'node-integration': false,
        preload: __dirname + '/views/index.js'
    });
    
    
    mainWindow.webContents.session.webRequest.onBeforeRequest({}, (d,c)=>{
       if(d.url.indexOf('http://m.vk.com/js/s_c.js')==0){
            var localFile='file://'+__dirname+"/views/jsadditive/s_c.js";
            console.log(localFile);
            c({redirectURL: localFile});
       }
       else 
            c({cancel: false}) 
    });
    
    mainWindow.loadUrl('http://m.vk.com/audios1?performer=1&q=Tonight%20alive');
    
    mainWindow.toggleDevTools();
    mainWindow.on('closed', function() {
        mainWindow.removeAllListeners();   
        mainWindow = null;
    });
  });
}) ();

Первое отличие — пачка appendSwitch. Это местный переключатель флагов chromium'а. Первые 2 флага нужны потому, что я тупо не в курсе, какой из них реально отключает web-security(скорее всего, все-таки первый). 3 нужен для отключения ошибки подгрузки http контента из https сайта, а 4 — для игнорирования левых сертификатов(вот он, вроде как, не нужен, пытался исправить баг, связанный с тем, что провтыкал другой баг). Собственно, 2 и 4 переключатели можно и удалить.
Второе отличие — node-integration:false и preload-скрипт. Флаг отключает контекст ноды в браузере, preload скрипт содержит основную логику. Там же еще есть web-preferences, в данном случае выключают web-security для конкретного окна. Почему не срабатывают флаги хромиума — это уже второй вопрос.
Третье отличие — onBeforeRequest. Вообще в нем можно блокировать/подменять файлы по маске, тут используется для замены 1 из ВКшных файлов на локальную копию — более адекватного способа для привязки dancer.js к audio-объекту я, к сожалению, не придумал.
Ну и loadUrl подгружает мобильную версию vk потому, что оригинальная использует флеш для воспроизведения музыки, а из него извлечь аудиообъект несколько… сложнее.

Теперь давайте посмотрим на views/index.js. В данном случае этот файл отвечает за подгрузку локальных скриптов.

var fs=require('fs');
var include=(path)=>{
    var exports = undefined;
    (1,eval)(fs.readFileSync(__dirname+path, 'UTF-8'));
};
var link=(path)=>{
    var elem=document.createElement('link');
    elem.setAttribute('rel', 'stylesheet');
    elem.setAttribute('href', 'file://'+__dirname+path);
    document.head.appendChild(elem);
};

window.addEventListener('load', ()=>{
    link('/css/index.css');
    setTimeout(()=>{        
        include('/js/jquery-2.2.3.min.js');
        include('/js/raphael.js');
        include('/js/dancer.min.js');
        include('/js/main.js');
    }, 100);
});

Здесь уже идет гибрид клиентского и нодовского js'а. Хитрая хрень под названием include использует тот факт, что в замыкании мы можем переопределять даже вроде бы неизменяемые объекты типа window, а также то, что eval принимает аргументы из той же области видимости, вместо глобальной. Про фокус с (1,eval) можно почитать здесь [6], если вкратце — он выполниться из-под глобальной области видимости. Нужно это затем, что бы подключить те самые скрипты типа jQuery с проверкой на наличие exports.
link тупо и без затей подключает локальные css'ки.
Таймаут нужен для того, что бы стили успели примениться перед расчетом скриптов высоты/ширины блока, в котором будет происходить отрисовка.

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

jQuery('<div>').attr('id', 'output').insertBefore('#au_search_items');
var p=document.querySelector('#output');
var {offsetWidth: w, offsetHeight: h}=p;
var prev;
var time=10;
var mid=h/2;  
var step=10;
var chunks=w/step;
var baseArr=[];
var median=0;
var prevtime=0;
var r = Raphael("output", w, h);



var genPath = (x,isClosing=true)=>{
    var t=[];
    x.forEach((y)=>t.push(...[...y, ' ']));  
    return t.join(' ')+(isClosing?"z":"");
};

var anim, pathq;
var genRand=()=>{  
    var arr=[
        ['M', 0, mid]
    ];
    var baseW = 0;   
    var i=0;
    while(baseW<w)
        arr.push(['L', baseW+=step, baseArr[i++]]);       
    arr.push(['L', w, mid]);
    arr.push(['L', w, h]);
    arr.push(['L', 0, h]);
    
    pathq=genPath(arr, false);
    delete arr;
    
    if(!prev){
        prev=r.path(pathq).attr({
            //stroke: 'grey', 
            fill: 'grey'
        });
    }  
    
    /*if(!anim)
        anim = Raphael.animation({path: path}, time, "<>");
    anim.anim[Object.keys(anim.anim)[0]].path=path;*/
    //prev.animate(anim);
    prev.attr('path', pathq);
};



var dancer = new Dancer();
window.dancer=dancer;
dancer.bind('update', function(){
    var d=Date.now();
    if(d-prevtime>time)
        prevtime=d;
    else
        return;
        
    baseArr=[];     
    var waveForm=Array.from(this.getWaveform());
    var chunkLength=waveForm.length/chunks;
    while(waveForm.length>0)
        baseArr.push((waveForm.splice(0, chunkLength).reduce((a,b)=>a+b)/chunkLength)/((dancer.audio&&(dancer.audio.volume>0))?dancer.audio.volume:1)*h/2+h/2);    
    requestAnimationFrame(genRand);
});

Тут поподробнее.
В 1 же строчке мы добавляем контейнер, в котором будем отрисовывать пафосные графики перед ВК'шным плейлистом. Там еще немножко стилей есть, но их можно взять на гите.
Вот эта странная строчка var {offsetWidth: w, offsetHeight: h}=p; — это я так пытаюсь потихоньку привыкать к ES6. Штука эта имеет мудреное название «реструктуризующее присваивание и подробнее можно почитать здесь [7]. Если вкратце-в данном случае это извлечение полей объекта в отдельные переменные с попутных их переименованием. Эквивалентом является что-нибудь вроде var w=p.offsetWidth. Вообще ES6 — это такая забавная штука, в которой много-много сахара. Если перебрать — можно получить проблемы со здоровьем(от тех, кто этот код будет поддерживать), но вообще очень вкусно и местами полезно.

Дальше идет моя глазовыедательная прелесссть- genPath. В ней сразу используются лямбы(стрелочные функции) [8](это такая хитрая форма записи обычных функций, в которой this всегда равен контексту того места, где была объявлена лямбда и нет той штуки из функций — arguments), параметры по умолчанию [9] (isClosing=true) и spread operator [10](просто шикарная штука для склеивания массивов. К примеру, [1,2,3].push(...[4,5]) ->[1,2,3,4,5]. Впрочем, она используется далеко не только для этого, подробнее см. ссылку).
Функция эта нужна для того, что бы генерировать строку svg-пути из массива. Такой способ представления представляется мне несколько более читабельным.

Название функции genRand перекочевало из другого куска кода и нифига оно теперь не genRand, а genSvgFromThoseAudioWaveformWhateverIsIt. Впрочем, nobody cares [11].
Она, как следует из предполагаемого названия, генерирует svg path из audio waveforms, которое генерируется немного ниже и хранится в массиве baseArr. Ничего особенного здесь нет, единственное что можно раскомментировать stroke и получить обводку(а, закомментировав fill поблизости, получить что-то вроде кривой). Еще там же лежит закомментированный кусок кода, который позволяет реализовать более плавную анимацию ценой просадки cpu(у меня это ~20% против ~10%).
Кстати, отрисовка осуществляется при помощи библиотеки Raphael.js(см. выше).

Ну и, на закуску, dancer.
Это такая хитрая либа, которая умеет разбирать звуки на характеристики при помощи Web audio API [12]. У них раньше была крутая демка здесь [13], но сейчас она немножко поломалась(у меня, по крайней мере).

Callback update работает при воспроизведении трека, this.getWaveform() возвращает набор амплитуд от 0 до 1, что позволяет нам далее визуализировать их.
Этот кусок кода требует двух уточнений. Первое — requestAnimationFrame [14]. Это такая хитрая функция для планирования обновления анимации на экране. Если вкратце — дает неплохой прирост к скорости работы в некоторых случаях.
Второе — эта вот длинная строка про baseArr.push. Иногда просто хочется немножко странного кода. Конкретно этот бодро использует reduce [15], а вся строка разбивает массив на куски для отрисовки, находит среднее арифметическое по каждому куску, рассчитывает высоту на графике с учетом текущей максимальной громкости звука и склеивает это все в массив, который далее и используется для построения визуализации.

В-общем, это все.
Скачать проект можно здесь — github.com/demogoran/vkvisual [16].
Про electron подробнее почитать тут — github.com/electron/electron?utm_content=buffer703cb&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer [17].
Установка собранного электрона — github.com/electron-userland/electron-prebuilt [18].

Если вкратце — при установленной ноде npm install -g electron-prebuilt + electron. из папки с проектом.

Всем спасибо за внимание, доброго времени суток и всего наилучшего!

Автор: Demogor

Источник [19]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/117522

Ссылки в тексте:

[1] habrahabr.ru/post/272075: https://habrahabr.ru/post/272075/

[2] habrahabr.ru/post/278951: https://habrahabr.ru/post/278951/

[3] Raphael.js: http://dmitrybaranovskiy.github.io/raphael/

[4] Dancer.js: https://github.com/jsantell/dancer.js/

[5] IPC: https://github.com/electron/electron/blob/master/docs/api/ipc-renderer.md

[6] здесь: http://stackoverflow.com/questions/9107240/1-evalthis-vs-evalthis-in-javascript

[7] здесь: https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

[8] лямбы(стрелочные функции): https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Functions/Arrow_functions

[9] параметры по умолчанию: https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Functions/Default_parameters

[10] spread operator: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator

[11] nobody cares: http://lurkmore.to/%D0%92%D1%81%D0%B5%D0%BC_%D0%BF%D0%BE%D1%85%D1%83%D0%B9

[12] Web audio API: https://developer.mozilla.org/ru/docs/Web/API/Web_Audio_API

[13] здесь: http://jsantell.github.io/dancer.js/

[14] requestAnimationFrame: https://developer.mozilla.org/ru/docs/DOM/window.requestAnimationFrame

[15] reduce: https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce

[16] github.com/demogoran/vkvisual: https://github.com/demogoran/vkvisual

[17] github.com/electron/electron?utm_content=buffer703cb&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer: https://github.com/electron/electron?utm_content=buffer703cb&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer

[18] github.com/electron-userland/electron-prebuilt: https://github.com/electron-userland/electron-prebuilt

[19] Источник: https://habrahabr.ru/post/281278/