- PVSM.RU - https://www.pvsm.ru -
Уже очень давно мне хотелось попробовать создать проект, который бы представлял собой настоящие JavaScript Application, а именно толстый клиент, без backend и своего
Итак, перво-наперво требования к проекту:
А теперь самое интересное: где хранить исходники тестов? А результаты?
Если среди вас есть постоянные пользователи jsperf [3], то они помнят недавнюю историю, когда он был полностью недоступен именно по причине хранения кода и результатов прогона тестов. Так что задача сводилась к одному: как сделать так, чтобы ничего не хранить у себя, а переложить это на какой-нибудь сервис, а лучше на юзера? Ответ напрашивался сам собой: идеальное место для хранения исходников — GitHub [4], точнее Gist [5]. Он есть практически у каждого разработчика, и это решает сразу несколько поставленных задач:
У GitHub есть чудесный REST API [6], это, наверное, одна из эталонных реализаций, также есть API [7] и для работы с Gist. Вопрос оставался за малым: как сохранить Gist от имени пользователя?
Для авторизации GitHub предлагает использовать OAuth [8], это несложно, но требует минимального backend’а. Тут можно было пойти несколькими путями:
Я выбрал OAuth.io [9], как очень простой и быстрый способ для начала работы, к тому же при необходимости от сервиса можно безболезненно избавиться. Плюс у него есть неплохая аналитика, простенькая либа [10] для работы API и куча провайдеров [11] под любой сервис, в том числе и GitHub. А самый кайф, что для начала работы вам даже не нужно проходить нудную регистрацию, — просто нажимаете «Sign in with GitHub» и добавляете ключи от вашего приложения.
Следующий шаг — это написание обёртки для работы с GitHub API. И тут есть небольшой нюанс: я очень хотел лишний раз не дёргать OAuth.io, чтобы не выходить за лимиты free-плана. Как оказалось, GitHub позволяет обращаться к API неавторизованным, но такие вызовы жёстко лимитируются, поэтому метод получения Gist имеет достаточно нетривиальную логику:
localStorage
есть данные о юзере, считаем, что он уже авторизован, вызываем получение токена через OAuth.io и делаем запрос к API. Если авторизация не прошла, отправляем запрос неавторизованным и надеемся, что лимиты ещё не исчерпаны.localStorage
ничего нет, делаем запрос как неавторизованный, в случае ошибки пытаемся авторизоваться через OAuth.io и повторить запрос уже как авторизованный.Переводим это в код, посыпаем Promise [12] + fetch [13] и получаем вот такой метод:
function findOne(id) {
let promise;
const url = 'gists/' + id;
const _fetch = () => {
return fetch(API_ENDPOINT + url).then(res => {
if (res.status !== API_STATUS_OK) {
throw 'Error: ' + res.status;
}
return res.json();
});
};
if (_gists[id]) {
// Runtime cache
promise = Promise.resolve(_gists[id]);
} else if (github.currentUser) {
// Есть авторизация, запрашиваем Gist через OAuth.io
promise = _call('get', url)['catch'](() => {
// Ошибка, пробуем запросить напрямую у GitHub API
github.setUser(null);
return _fetch();
});
} else {
// Нет авторизации, обращаемся напрямую к GitHub API
promise = _fetch()['catch'](() => {
// Ошибка, пробуем авторизоваться и запросить повторно
return _call('get', url);
});
}
return promise.then(gist => {
// Добавляем в Runtime cache
_gists[gist.id] = gist;
return gist;
});
}
Если с исходниками я определился быстро, то вот с результатами не всё так просто. Не буду томить, просто перечислю решения, которые я знал на тот момент:
Так как проект был экспериментальным, выбор пал на Firebase: кроме JavaScript API, он предлагал в два раза больше места, чем на том же MongoLab, — целый гигабайт.
На самом деле был ещё один вариант: localStorage/IndexedDB + WebRTC [17]. Идея заключалась в следующем: результаты прогонов храним в localStorage и если в онлайне есть ещё кто-нибудь, то синхронизируем данные :).
Итак, Firebase. Использовать его до безобразия просто, документация не врёт: https://www.firebase.com/docs/web/quickstart.html [18].
// Создаём экземпляр Firebase (предварительно создав application, в моём случае это JSBench)
const firebase = new Firebase('https://jsbench.firebaseio.com/');
// Подписываемся на событие изменения «узла» (stats / {gist_id} / {revision_id}):
firebase.child('stats').child(gist.id).child(getGistLastRevisionId(gist)).on('value', (snapshot) => {
const values = snapshot.val();
// Обрабатываем данные
});
// Где-то в коде в какой-то момент добавляем данные
firebase.child('stats').child(gist.id).child(getGistLastRevisionId(gist)).push(data);
Это весь код, который мне пришлось написать для работы с Firebase, но самое классное, что событие обновления «узла» срабатывает всякий раз, когда кто-либо запускает бенчмарки, и вы прямо онлайн, без каких-либо F5 или cmd + r, получаете обновление графиков.
Эх, вот чего не умею, того не умею, поэтому всё выглядит так:
Интерфейс максимально информативен, отображены основные важные параметры, справа от кода теста выводится результат прогона, после завершения теста строчки подсвечиваются соответствующим цветом, а под кодом строятся графики. Setup и Teardown вынесены в «уши» внизу экрана — решение спорное, но подходит для большинства задач. В итоге вся возможная информация умещается на одном экране.
Как можно заметить, в отличие от jsperf, у меня есть подсветка кода, для этого используется Ace.
Ace [19] — это чудесный инструмент для интеграции редактора кода в ваше приложение. Чтобы его использовать:
// Создаём инстанс
const editor = ace.edit(this.el);
// Устанавливаем тему
editor.setTheme('ace/theme/tomorrow');
// Включаем поддержку JavaScript
editor.getSession().setMode('ace/mode/javascript');
// Определяем максимальное и минимальное расширение редактора
editor.setOption('maxLines', 30);
editor.setOption('minLines', 4);
// Включаем автопрокрутку
editor.$blockScrolling = Number.POSITIVE_INFINITY;
// Подписываемся на изменения
editor.on('change', () => {
const value = editor.getValue();
// ...
});
Для прогона тестов используются Platform.js [20] и Benchmark.js [21], графики рисую при помощи Google Visualization [22], так что результаты прогона и графики выглядят точно так же, как на jsperf.
Одна из фич — это шаринг теста, сейчас поддерживаются только Twitter и Facebook.
Тут особо нечего рассказывать: открываем popup с предустановленным текстом, а дальше пользователь сам решает, постить или нет.
function twitter(desc, url, tags) {
const max = 130;
const top = Math.max(Math.round((SCREEN_HEIGHT / 3) - (twttr.height / 2)), 0);
const left = Math.round((SCREEN_WIDTH / 2) - (twttr.width / 2));
const message = desc.substr(0, max - (url.length + tags.length)) + ': ' + url + ' ' + tags;
const params = 'left=' + left + ',top=' + top + ',width=' + twttr.width + ',height=' + twttr.height;
const extras = ',personalbar=0,toolbar=0,scrollbars=1,resizable=1';
window.open(twttr.url + encodeURIComponent(message), 'twitter', params + extras);
}
Вот тут интереснее, хотелось не просто ссылку постить, а сразу график в ленту. У Google Visualization есть метод получения dataURI [23], а у FB — Graph API [24], осталось их подружить:
const facebook = {
appId: 'XXXXXXX',
publichUrl: 'https://graph.facebook.com/me/photos',
init() {
return this._promiseInit || (this._promiseInit = new Promise(resolve => {
window.fbAsyncInit = () => {
const FB = window.FB;
FB.init({
appId: this.appId,
version: 'v2.5',
cookie: true,
oauth: true
});
resolve(FB);
};
// Стандартный код публикации
(function (d, s, id) {
var fjs = d.getElementsByTagName(s)[0], js;
if (d.getElementById(id)) {return;}
js = d.createElement(s);
js.id = id;
js.src = '//connect.facebook.net/en_US/sdk.js';
fjs.parentNode.insertBefore(js, fjs);
})(document, 'script', 'facebook-jssdk');
}));
},
login() {
return this._promiseLogin || (this._promiseLogin = this.init().then(api => {
return new Promise((resolve, reject) => {
api.login((response) => {
if (response.authResponse) {
resolve(response.authResponse.accessToken);
} else {
reject(new Error('Access denied'));
}
}, {
scope: 'publish_actions'
});
});
}));
}
};
Для преобразования dataURI используем https://github.com/blueimp/JavaScript-Canvas-to-Blob/. [25]
И публикуем:
function facebookPublish(dataURI, message) {
return facebook.login().then(token => {
const file = dataURLtoBlob(dataURI);
const formData = new FormData();
formData.append('access_token', token);
formData.append('source', file);
formData.append('message', message);
return fetch(facebook.publishUrl, {
method: 'post',
mode: 'cors',
body: formData
});
});
}
Планы на будущее
Полный список используемых библиотек и полифилов
Как видите, в настоящий момент можно собирать качественный прототип с помощью готовых решений и бесплатных платформ, затрачивая на это только своё время. А ведь это не просто экономия денег — это и экономия времени на выбор
Страница проекта: http://jsbench.github.io/ [35]
Исходный код и задачи: https://github.com/jsbench/jsbench.github.io/ [36]
Автор: Mail.Ru Group
Источник [37]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/106206
Ссылки в тексте:
[1] хостинга: https://www.reg.ru/?rlink=reflink-717
[2] jsperf.com: http://jsperf.com/
[3] jsperf: https://jsperf.com/
[4] GitHub: https://github.com/
[5] Gist: https://gist.github.com/
[6] REST API: https://developer.github.com/v3/
[7] API: https://developer.github.com/v3/gists/
[8] OAuth: https://developer.github.com/v3/gists/#authentication
[9] OAuth.io: https://oauth.io/
[10] либа: http://docs.oauth.io/#install-the-sdk
[11] куча провайдеров: https://oauth.io/providers
[12] Promise: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise
[13] fetch: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
[14] Parse.com: https://parse.com/docs
[15] MongoLab.com: https://mongolab.com/
[16] Firebase.com: https://www.firebase.com/
[17] WebRTC: https://developer.mozilla.org/en-US/docs/Web/Guide/API/WebRTC
[18] https://www.firebase.com/docs/web/quickstart.html: https://www.firebase.com/docs/web/quickstart.html
[19] Ace: https://ace.c9.io/
[20] Platform.js: https://github.com/bestiejs/platform.js/
[21] Benchmark.js: http://benchmarkjs.com/
[22] Google Visualization: https://developers.google.com/chart/interactive/docs/gallery
[23] dataURI: https://developers.google.com/chart/interactive/docs/printing
[24] Graph API: https://developers.facebook.com/docs/graph-api/using-graph-api
[25] https://github.com/blueimp/JavaScript-Canvas-to-Blob/.: https://github.com/blueimp/JavaScript-Canvas-to-Blob/
[26] ES5 polyfill: https://cdnjs.com/libraries/es5-shim
[27] Promise: https://cdnjs.com/libraries/es6-promise
[28] fetch: https://cdnjs.com/libraries/fetch
[29] Ace: https://ace.c9.io/#nav=howto
[30] mode-javascript: http://cdnjs.cloudflare.com/ajax/libs/ace/1.2.0/mode-javascript.js
[31] theme-tomorrow: http://cdnjs.cloudflare.com/ajax/libs/ace/1.2.0/theme-tomorrow.js
[32] Platform: https://cdnjs.com/libraries/platform
[33] не только: https://www.firebase.com/docs/
[34] SweetAlert: http://t4t5.github.io/sweetalert/
[35] http://jsbench.github.io/: http://jsbench.github.io/
[36] https://github.com/jsbench/jsbench.github.io/: https://github.com/jsbench/jsbench.github.io/
[37] Источник: http://habrahabr.ru/post/272911/
Нажмите здесь для печати.