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

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


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


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

image


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


1. Система сборки


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


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


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


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


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

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


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

2. Переключение между локальной и продакшн конфигурацией


Чтобы можно было легко управлять сборками и конфигурациями, мне очень помогло завести одну переменную 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"
}; 

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

3. Как передать API наружу?


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


window[config.globalName] = yourApiVar;

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


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

4. “Правильная” система модулей


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


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


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

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

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

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


5. Инициализация API


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


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


(function () {
    'use strict';
    var config = {}; 
    var sharedState = {}; 

    var module = (function () { 
        var someInnerModuleVar;

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

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

    start();
}());

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


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

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 35

    +1
    >>> Напишите, какие темы наиболее интересны.

    Типизированные массивы.
      0
      Подумаю… А вам интересно это со стороны производительности, или чтения из сетевых потоков, или по какой-нибудь ещё причине?
        0
        И то и другое.

        С ArrayBuffer в принципе всё понятно, хотелось бы посмотреть на класс DataView на примере.

        Но и ArrayBuffer тоже будет не лишним.
      +2
      Работа с LocalStorage будет очень интересной, попробуй сделать поддержку версионности и устареваня данных. Круто будет записывать обьектыв LocalStorage через свою обертку при наличии коей ненужно будет думать у тебя там скаляр или обьект.

      P.S. Поддерживаю такую реализацию модулей. Она позволяет имплементировать приватность, что тяжело при помощи других методов.
      Спасибо за труды при написанни статьи, коротко и по сути.
        0
        Да, хранение данных на клиенте — важная штука; думаю, чем умнее будут становиться веб-приложения, тем активнее будет использоваться localStorage.
        +9
        С приветом из 2014-го? :)
        В JS сейчас такое безумие творится, что подобная статья смотрится уже устаревшей. Ну и пункт 3 какой-то стремный в сегодняшних реалиях. Gulp многие пытаются на свалку отправить, не говоря уже про Grunt.
        Бекендщикам сейчас конечно сложновато погружаться в JS — много времени потребуется на всю эту «хипстоту» :)
          +1

          Я тоже был удивлён, когда не нашёл подобных статей. Но когда я стартанул с JS, мне очень не хватало такого.


          window[config.globalName] = yourApiVar;

          Это на самом деле классика. Во многих библиотечках используется. И хипстерских, и супер-оптимизированных. Необходимость.


          И что вместо Gulp и Grunt? Прочитал сейчас пару статей, интересно. Рекомендуют чистый npm и прочее.

            +2
            Много чего сейчас есть. Например, определенной (и вполне заслуженной) популярностью пользуется сборка на webpack с использованием npm-скриптов (для действий не покрываемых сборщиком).
            «Накладные расходы» на поддержку нормальной модульности минимальны, призывы «только конкатенация» непонятны.
              0
              Староверы собирают на Apache Ant.
                0
                Я кстати не пытался призвать всех отказываться от Gulp. Некоторая агитация по npm scripts имеется, но думаю Gulp еще поживет. Причины отказа от него несколько надуманы, но для мелких проектов возможно да, нет особого смысла его тянуть.
                0

                Рекомендуют книжку https://www.manning.com/books/third-party-javascript

                0
                Спасибо за статью.

                Было бы интересно почитать по сокетам. По ним довольно много материала. Но так, чтобы коротко и по делу я не видел. Может плохо искал, каюсь.
                  0

                  Хорошая тема, если будет чем поделиться, напишу.

                  0
                  Давно собираюсь с силами написать статью, но руки не доходят, так что делюсь идеей, может заинтересует. Напишите про контексты, в которых может оказаться джисер и в чем их разница —
                  — код в странице открытой с сайта
                  — во фрейме
                  — на странице, открытой локально с диска
                  — в расширении (popup,  background, content) — для хрома(оперы), файрфокса (да, движок хромиум, но обвес там совсем другой),
                  — далее вебворкеры
                  — да, для firefox-а — bootstrap.js в интересном контексте запускается, там не все модули доступны, я например до сторейджа (simple-storage) так и не смог достучаться
                  — pac-файлы тоже, хоть и стандарт есть — везде в разном контексте запускаются (в хроме это не совсем песочница, по крайней мере регексы доступны, хотя по стандарту не должны, а в лисе даже описанные в стандарте функции недоступны)

                  Так, навскидку вроде все распространенное охватил на клиенте. В общем если займетесь — могу даже подсобить.
                    0
                    Я тут заканчиваю небольшой бойлерплейт для создания chrome-расширений на базе webpack с hot module replacement и автоматическим chrome.runtime.reload() при компиляции, есть мысли по завершении статейку написать.
                    Про контексты в расширениях — это довольно значимый пунктик, в частности, доступ к chrome API из injected-скриптов, доступ к js-окружению на странице, контекст применения HMR-обновлений (т.к. это не обычная страница, пришлось немного подпилить механизм hot-апдейтов) и т.д. нюансы.
                    Если будут желающие поучаствовать словом и делом — буду рад.
                      0

                      На самом деле всё началось именно с chrome extension, а потом функциональность перекочевала в injected script, потом во встраиваемый скрипт.


                      "доступ к chrome API из injected-скриптов" — я в расширении реализовал специальный channel — он заворачивает функции и шлёт события и на другой стороне вызывает реализацию этих фунций. Вы сделали похоже?


                      Буду иметь ввиду. Напишите, по крайней мере интересно посмотреть: dmitry@kuoll.com

                        0
                        Да, могу накидать хинтов по портированию этого дела в firefox. Созреете — стукните в личку.
                        0

                        Да, хороший вопрос, вполне достойный отдельной статьи.

                        0
                        Расскажите про удобную работу с куками, ибо это «самое убогое API, которое пролезло в JS». Вы скажете что мол вон, есть localStorage, но бывает когда использование кукисов необходимо.
                          0

                          Да, хороший вопрос. И надо ещё подумать когда cookies, а когда localStorage.

                          +1
                          config = {};
                          sharedState = {};

                          Вы действительно хотите сделать эти 2 переменные видимыми снаружи?
                          Почему не сделать этого явно?
                          Откуда берется функция start, которую вы вызываете?
                            0

                            config и sharedState не видны снаружи, они же ведь объявлены внутри функции.


                            Функция start() — из пункта 5; её задача — явная инициализация всего API.

                              +3
                              >config и sharedState не видны снаружи, они же ведь объявлены внутри функции.

                              Вы ошибаетесь.

                              (function(){
                              config={test:'test'};
                              })();

                              console.log(config);

                                0

                                Исправил.

                                  +1
                                  А если бы код был в strict mode, то проблема бы даже не появилась
                                    0

                                    Это же псевдо-код, там реализации функций нет и прочего.


                                    А так все модули обязательно strict mode. Добавляю к шаблону, чтобы было очевидно.

                              +1
                              Правильная система модулей? ES next import/export, не, не слышал :D
                                0
                                Всё-таки, если стоит вопрос возможности работы библиотеки в среде без поддержки ES2015, UMD – наиболее удобный вариант, т.к. поддерживает AMD, CommonJS, vanilla definition :)
                                https://github.com/umdjs/umd
                                  0
                                  babel + webpack, typescript + webpack и webpack делает umd из коробки
                                    0
                                    В Webpack нельзя выбрать вариант экспорта (см. Variations в репозитории)
                                0
                                Ответ очевиден: когда вы сконкатенируете код таких модулей, всё будет работать безо всяких библиотечек для модулей.

                                Серьезно?

                                  0

                                  Если есть возражения, мы готовы их услышать.

                                    0

                                    Окей, я даже немного растерялся. Webpack, Browserify, Systemjs, Reflow (пусть меня поправят, если я что-то упустил) не требуют никаких "библиотечек для модулей". Только бандл проекта, использующего RequireJS требует библиотеку, имплементирующую AMD, например Almond (это всего 1К оверхеда), и то, даже с этим древним бандлером можно воспользоваться не менее древним AMD Clean. Ваши знания явно немного устарели.

                                  +2
                                  Хотело бы добавить по пункту 4 и 5.

                                  Паттерн «модуль» и паттерн «открытия модуля» однозначно стоит применять. Но на мой взгляд нужно возвращать не инстанс модуля, а конструктор модуля. Это позволит из внешнего кода создать инстанс модуля самостоятельно и пробросить в функцию конструктор модуля нужные параметры или зависимости. Сконфигурировать модуль под место его работы.

                                  Таким образом можно использовать несколько экземпляров одного модуля на странице и каждый настроить на свой вкус.
                                    0

                                    Да, хорошая идея.

                                  Only users with full accounts can post comments. Log in, please.