Створюємо проект c OAuth та NoSQL за $0,00

Вже дуже давно мені хотілося спробувати створити проект, який би представляв собою справжні JavaScript Application, а саме товстий клієнт, без backend і свого хостингу, на основі open source і якого-небудь BaaS/DaaS. До того ж я остаточно втомився від jsperf.com, від цих безглуздих двох кроків, від відсутності хоч якогось редактора коду і нормального пошуку і від постійної втрати своїх тестів, а історія з капчой, яка не завжди спрацьовує, остаточно доконала мене. Я нарешті викроїв час, щоб здійснити давно задумане і вбити двох зайців, реалізувавши альтернативу jsperf.



Отже, насамперед вимоги до проекту:

  • компактний і зрозумілий інтерфейс, без кроків і капчі;
  • нормальний редактор коду з підсвічуванням, а не просто textarea;
  • збереження бенчмарку, щоб потім його можна було легко знайти, а також видалення (всяке буває);
  • можливість скачати бенчмарк і запустити його локально або через Node.js;
  • додати до «Обраного»;
  • інші ніштяки.
А тепер найцікавіше: де зберігати вихідні коди тестів? А результати?

Де зберігати вихідні?
Якщо серед вас є постійні користувачі jsperf, то вони пам'ятають недавню історію, коли він був повністю недоступний саме через зберігання коду та результатів прогону тестів. Так що задача зводилася до одного: як зробити так, щоб нічого не зберігати у себе, а перекласти це на якийсь сервіс, а краще на юзера? Відповідь напрошувався сам собою: ідеальне місце для зберігання исходников — GitHub, точніше Gist. Він є практично у кожного розробника, і це вирішує відразу кілька поставлених завдань:

  • зберігання;
  • нормальний пошук (по унікальному тегу);
  • уподобання;
  • бонусом: історія зміни тестів (diff) і fork'в.
У GitHub є чудовий REST API, це, напевно, одна з еталонних реалізацій, також є API для роботи з Gist. Питання залишалося за малим: як зберегти Gist від імені користувача?

OAuth
Для авторизації GitHub пропонує використовувати OAuth, це нескладно, але вимагає мінімального backend'а. Тут можна було піти кількома шляхами:

  1. Знайти який-небудь безкоштовний хостинг або BaaS і розгорнути там одне з open source рішень для роботи з GitHub.
  2. Скористатися сервісом OAuth.io, у якого є прийнятний free-план.
Я вибрав OAuth.io, як дуже простий і швидкий спосіб для початку роботи, до того ж при необхідності від сервісу можна безболісно позбутися. Плюс у нього є непогана аналітика, простенька ліба для роботи API і купа провайдерів під будь-сервіс, в тому числі і GitHub. А самий кайф, що для початку роботи вам навіть не потрібно проходити нудну реєстрацію, — просто натискаєте «Sign in with GitHub» і додаєте ключі від вашого застосування.

GitHub API
Наступний крок — це написання обгортки для роботи з GitHub API. І тут є невеликий нюанс: я дуже хотів зайвий раз не смикати OAuth.io, щоб не виходити за ліміти free-плану. Як виявилося, GitHub дозволяє звертатися до API неавторизованим, але такі виклики жорстко лімітуються, тому метод отримання Gist має досить нетривіальну логіку:

  1. Перевіряємо Runtime cache, якщо є дані, віддаємо.
  2. Якщо
    localStorage
    є дані про юзере, вважаємо, що він вже авторизований, викликаємо отримання сертифіката через OAuth.io і робимо запит до API. Якщо авторизація не пройшла, відправляємо запит неавторизованим і сподіваємося, що ліміти ще не вичерпані.
  3. Якщо
    localStorage
    нічого немає, робимо запит як неавторизований, у разі помилки намагаємося авторизуватися через OAuth.io і повторити запит вже як авторизований.
Переводимо це код, посипаємо Promise + fetch і отримуємо ось такий метод:

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;
});
}


Де зберігати результати?
Якщо з исходниками я визначилася швидко, то ось з результатами, не все так просто. Не буду томити, просто перерахую рішення, які я знав на той момент:

  • Parse.com — BaaS, є досвід використання, зручний JS SDK;
  • MongoLab.com — DaaS, мікроскопічний досвід використання, потрібно JS-велосипед для роботи;
  • Firebase.com — DaaS+, досвіду не було, є JavaScript SDK і дещо ще ;).
Так як проект був експериментальним, вибір припав на Firebase: крім JavaScript API, він пропонував в два рази більше місця, ніж на тому ж MongoLab, — цілий гігабайт.

Насправді був ще один варіант: localStorage/IndexedDB + WebRTC. Ідея полягала в наступному: результати прогонів зберігаємо в localStorage і якщо в онлайні є ще хто-небудь, то синхронізуємо дані :).

Отже, Firebase. Використовувати його до неподобства просто, документація не бреше: https://www.firebase.com/docs/web/quickstart.html.

// Створюємо екземпляр 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 — це чудовий інструмент для інтеграції редактора коду в вашу програму. Щоб його використовувати:

// Створюємо інстанси
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 і Benchmark.js, графіки малюю за допомогою Google Visualization, так що результати прогону і графіки виглядають точно так само, як на jsperf.

Шарінг
Одна з особливостей — це шарінг тіста, зараз підтримуються тільки Twitter і Facebook.

Twitter

Тут особливо нічого розповідати: відкриваємо 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);
}

Facebook

Ось тут цікавіше, хотілося не просто посилання постити, а відразу графік в стрічку. У Google Visualization є метод отримання dataURI, а у FB — Graph API, залишилося їх подружити:
Обв'язка над Facebook SDK
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,
версія: '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/.

І публікуємо:

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
});
});
}

Плани на майбутнє

  • Підтримка ES6.
  • Підключення сторонніх ліб для тесту.
  • Коментарі до бенчмарк (підтримка Markdown).
  • Перегляд ревізій і fork'ів.
Повний список використовуваних бібліотек і полифилов



Підсумок
Як бачите, зараз можна збирати якісний прототип з допомогою готових рішень і безкоштовних платформ, витрачаючи на це лише свій час. А адже це не просто економія грошей — це і економія часу на вибір хостингу, встановлення, налаштування необхідного софта і подальше адміністрування. Всі ці проблеми зникають, коли ви використовуєте будь BaaS або DaaS — вони дають вам готове рішення без головного болю. Плюс, якщо проект зросте і зміцніє, ви завжди можете перейти на відповідний платний тариф або підняти точно такий же стек на своєму хостингу.

Сторінка проекту: http://jsbench.github.io/
Вихідний код і завдання: https://github.com/jsbench/jsbench.github.io/

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.