[Из песочницы] Разработка JavaScript API: 5 принципов написания встраиваемых скриптов

Наверняка, вы сталкивались с принципами (пусть и противоречивыми) о написании модулей и классов на JavaScript. Когда мне понадобилось написать встраиваемый в веб-страницу cкрипт, который предоставляет API для работы определённого сервиса, то я не смог найти достойных рекомендаций о проектировании подобных скриптов.

Итак, вот (довольно очевидные) требования к скрипту, с которыми я столкнулся:  


  • он будет встраиваться в страницы сторонних веб-приложений;
  • он должен выполнять свою работу качественно;
  • он должен загружаться быстро;
  • он не должен (непредсказуемо) влиять на работу веб-приложения;
  •  должен соответствовать требованиям безопасности;
  • … // много чего ещё :)

image

Из реальной практики родились принципы, описанные ниже. Это не полностью уникальные идеи, а скорее сборка лучших практик, которых я видел в чужих решениях, например, в библиотечках google analytics и jquery.

Она нужна. Сначала кажется, что можно просто всё держать в одном файле (можно даже с этого начать), но потом становится ясно, что сборка необходима. Потому что используются сторонние библиотечки. Потому что есть несколько вариантов поставки скрипта. Потому что скрипт может подгружать файлы ресурсов по мере необходимости. И об этом стоит думать сразу, даже когда вы ещё держите весь скрипт в одном файле.

Как собирать? Только конкатенация. Потому что основной скрипт должен загружаться быстро, то есть одним файлом. 

Это не значит, что нужно всё залить в один файл, и надеяться, что всё будет хорошо. Необязательные, дополнительные возможности нужно подгружать лишь тогда, когда клиент библиотечки вызывает соответствующие методы. Но ядро должно загрузиться быстро, хорошо закэшироваться и сразу предоставить клиенту API.

Весь скрипт при этом надо завернуть в один scope. Очевидно? Да.

(function () {
 // Здесь будет твой код
}());

Кстати, чтобы обернуть код в scope с помощью Grunt, используйте options banner и footer:

concat: {
  injectScriptProd: {
    src: [...],
    dest: 'someScript.js',
    options: {
        banner: '(function(){\n',
        footer: '\n}());'
    }
},

Чтобы можно было легко управлять сборками и конфигурациями, мне очень помогло завести одну переменную config, положить её в отдельный файл configDev.js или configProd.js и иметь отдельные сборки скрипта. А вариантов сборок по-другим причинам потребовалось больше двух. В результате, наличие этих простых файлов очень облегчило мне и сборку, и код, и жизнь. При конкатенации просто указываете, из каких файлов собрать скрипт, —  и цельный файл-скрипт готов. 

Плохая практика: иметь замещаемые переменные по всему JavaScript-коду вида: <% serverUrl %>/someApi. Портит читаемость кода, медленнее собирается. И хочется, чтобы grunt watch работал действительно быстро, не правда ли?  

Пример нашего prod config-файла:

var config = {
    server: "https://www.yourserver.com/api/",
    resourcesServer: "https://www.yourserver.com/cdn/",
    envSuffix: "Prod",
    globalName: "yourProjectName"
}; 

// Маленький, да удаленький!

Есть разные способы, но сейчас делаем так:

window[config.globalName] = yourApiVar;

Это позволяет:


  • Тестировать несколько версий библиотечки на странице, причём так, что они друг-другу не мешают.
  • Весь скрипт поместить в один закрытый scope.
  • (Если вдруг понадобится) решать проблемы с совместимостью. Мы ведь будем знать, что управление экземпляром API происходит в коде самого скрипта, а не в коде клиента библиотечки. И поэтому у нас есть полный контроль над всеми экземплярами.

Я знаю, чтобы я здесь ни сказал, в меня полетят гнилые помидоры от людей, которые предпочитает другую систему модулей. Начинаем.

Правильно делать так:

var module = (function () { // for each module have this structure
    var someInnerModuleVar;

    // здесь мог бы быть твой гениальный код

    return {
        publicMethod: publicMethod
   };
}());

А почему именно так? Ответ очевиден: когда вы сконкатенируете код таких модулей, всё будет работать безо всяких библиотечек для модулей.

Если в вашей библиотечке есть хоть какая-то инициализация (а она там есть, даже если вы думаете по-другому), то вынесите её в отдельный метод. Можно даже создать отдельный метод для инициализации в каждом модуле. И вызывать их потом явно и с чётким пониманием, как это работает и в какой последовательности.

Для первого раза, наверное, хватит. Вот структура получившегося модуля:  

(function () {
    config = {}; 
    sharedState = {}; 

    var module = (function () { 
        var someInnerModuleVar;

        // крутой js код

        return {
            publicMethod: publicMethod,
            init: init
        };
    }());

    start();
}());

Если у вас есть идеи, как улучшить шаблон, то буду рад их услышать. Я разрабатываю ПО уже 11 лет, но в основном писал на java. Этот проект, — мой самый интенсивный опыт в JavaScript. Напишите, как сделать лучше, в комментариях. 

Ещё думаю написать про работу с cookies, localStorage, db, network. Напишите, какие темы наиболее интересны.

© Habrahabr.ru