Система сборки и зависимостей Google Closure Library
В этой статье мы рассмотрим систему сборки и зависимостей Google Closure Library.
А также увидим, как применить ее к своему коду, даже если вместо Google Closure Library используется совсем другой фреймворк.
Система зависимостей Google Closure Library имеет два основных декларативных метода: goog.provide и goog.require .
В javascript-файлах при помощи goog.provide указывается, какие символы предоставляет этот файл.
Символ - это имя javascript-объекта, который тут же можно использовать. Например:
goog.provide('my.super.dom')
my.super.dom.createElement = function() { ... }
Как видите, здесь мы декларируем, что файл объявляет символ my.super.dom , и goog.provide тут же создает пустой объект, используя для этого код, наподобие следующего:
my = window.my || {}
my.super = my.super || {}
my.super.dom = my.super.dom || {}
Так что существующие объекты и свойства не перезаписываются.
При помощи goog.require указывается, какие символы нужны. Указывать require принято после provide для лучшей читабельности кода.
Например:
goog.provide('goog.Delay');
goog.provide('goog.async.Delay');
goog.require('goog.Disposable');
goog.require('goog.Timer');
Функция goog.require лишь декларирует требование объекта и проверяет его. Она также может подгрузить нужный файл, но только если найдет его во внутренней структуре зависимостей.
Для добавления информации о зависимостях используется функция goog.addDependency .
Эта функция для каждого файла перечисляет список символов, которые он дает (provide ) и которые требует (require ).
Например:
goog.addDependency('/tmp/my/bye.js', ['my.bye'], ['my.main']);
Этот вызов добавляет в структуру зависимостей информацию, что файл bye.js предоставляет символ my.bye и требует my.main .
После такого вызова можно смело использовать goog.require('my.bye') - библиотека подгрузит файл /tmp/my/bye.js .
C другой стороны, если информация не была добавлена при помощи goog.addDependency - вызов goog.require('my.bye') завершится с ошибкой.
Для символов, объявленных в Google Closure Library, зависимости хранятся в корневой директории библиотеки, в файле deps.js .
Для ваших собственных символов и файлов вы должны либо прописать зависимости goog.addDependency самостоятельно, либо использовать для этого скрипт calcdeps.py (см. далее).
Для автоматической сборки и генерации файла зависимостей из ваших модулей используется скрипт calcdeps.py , поставляемый вместе с библиотекой.
Для его запуска под Windows можно использовать ActivePython, под Linux/MacOS - обычный python.
Эта утилита получает три основные опции:
-i file.js
- входной javascript-файл, можно указать несколько
-p path/to/dir
- список путей
-o script/deps/list/compiled
- тип вывода
Например:
calcdeps.py -i hello.js -p /tmp/my -p /tmp/gc/closure-library -o script
В нашем примере /tmp/my - место, где хранятся файлы main.js, bye.js, hello.js , а /tmp/gc/closure-library - каталог с google closure library, которая взята из SVN.
Скрипт calcdeps.py делает следующие простые шаги:
- Рекурсивно проходит по всем директориям из списка
-p , находит все файлы с расширением .js
- Для каждого файла - регулярными выражениями выцепляет списки, что ему надо (
goog.require ) и что он дает (goog.provide ).
- В зависимости от типа вывода:
deps
- Выводит набор директив
goog.addDependency .
В нашем случае это будет длинная простыня:
// This file was autogenerated by calcdeps.py
goog.addDependency('/tmp/my/bye.js', ['my.bye'], ['my.main']);
goog.addDependency('/tmp/my/hello.js', ['my.hello'], ['my.main']);
goog.addDependency('/tmp/my/main.js', ['my.main'], []);
goog.addDependency('/tmp/gc/closure-library/alltests.js', [], []);
goog.addDependency('/tmp/gc/closure-library/closure/goog/base.js', [], []);
goog.addDependency('/tmp/gc/closure-library/closure/goog/deps.js', [], []);
goog.addDependency('/tmp/gc/closure-library/closure/goog/array/array.js', ['goog.array'], []);
// ...
Первые несколько зависимостей взяты из файлов bye.js, main.js, hello.js, а остальные - построены из файлов Google Closure Library.
Свой deps.js в Closure Library
Если вы используете Google Closure Library, то можете загружать новый файл в качестве deps.js вместо стандартного.
Для этого нужно установить флаг:
// до загрузки base.js
goog.global.CLOSURE_NO_DEPS=true
(base.js перестанет подгружать стандартный deps.js ) и загружать deps.js через отдельный тег <script> , либо формировать список зависимостей goog.addDependency каким-либо другим путем.
list
выводит список нужных файлов в порядке зависимостей.
В нашем случае:
/tmp/gc/closure-library/closure/goog/base.js
/tmp/my/main.js
hello.js
Других файлов для сборки не нужно.
script
собирает все входные файлы (-i ) и требуемые для них в один файл.
В нашем случае это будет javascript-файл следующего вида:
// Input 0
содержание файла closure-library/closure/goog/base.js
// Input 1
содержание файла main.js
// Input 2
содержание файла hello.js
compiled
Компилирует файл, полученный в результате сборки. При использовании этой опции обязательно указание флага --compiler_jar с путем до jar-файла с компилятором.
Кроме того, как правило, указываются и флаги компиляции. Чтобы вывести откомпилированный файл в o.js , вызовем calcdeps.py вот так:
calcdeps.py -i hello.js -p /tmp/my -p /tmp/gc/closure-library -o compiled \
--compiler_jar compiler.jar --compiler_flags "--js_output_file o.js"
Кавычки для передачи флагов компилятора должны быть двойными.
Обратите внимание - в результате компиляции не будет директив goog.provide/require . Это потому, что компилятор использует специальный проход для проверки зависимостей и удаления этих директив из итогового файла.
С другой стороны, после компиляции с обычным уровнем оптимизации в файле остается много лишнего. Если добавить экспорт какой-нибудь функции и указать флаг "--js_output_file o.js --compilation_level ADVANCED_OPTIMIZATIONS" , то почти вся base.js исчезнет. Таким образом, overhead от использования системы сборки google будет сведен к минимуму.
Вообще, никто не заставляет использовать Google Closure Library. Для использования системы зависимости хватит base.js .
Так как calcdeps.py основан на регэкспах, условные директивы provide/require не поддерживаются. Утилита выхватит из файла все provide/require без разбора блока и местоположения в файле.
У функции goog.require есть один недостаток. Если встроенные зависимости Google Closure Library она загружает автоматически, то для собственных символов такой возможности не предусмотрено: нужно для каждого символа/файла прописывать goog.addDependency .
Чтобы упростить процесс, мы немного расширим goog.require , добавив автоматический подхват символов.
Исходный вариант:
goog.require = function(rule) {
// if the object already exists we do not need do do anything
// TODO: ...
if (!COMPILED) {
// если объект уже есть - возврат
if (goog.getObjectByName(rule)) {
return;
}
// получить путь к файлу с символом rule
var path = goog.getPathFromDeps_(rule);
if (path) {
// _writeScripts пишет <script> только 1 раз для каждого файла
goog.included_[path] = true;
goog.writeScripts_();
} else {
// файл не нашли
var errorMessage = 'goog.require could not find: ' + rule;
if (goog.global.console) {
goog.global.console['error'](errorMessage);
}
throw Error(errorMessage);
}
}
};
Введем правило - символ a.b.c находится в файле a/b/c.js , и модифицируем функцию goog.require , чтобы она при отсутствии зависимости в списке загружала файл по этому правилу.
А чтобы было еще удобнее - добавим префиксы, которые будут указывать путь к каждому пространству имен. Например:
goog.namespacePrefixes = {
'my': '/js/my'
}
Это будет означать, что goog.require('my.file') загрузит символ из /js/my/file.js .
Вот версия goog.require с соответствующими модификациями:
goog.require = function(rule) {
if (!COMPILED) {
if (goog.getObjectByName(rule)) {
return;
}
var path = goog.getPathFromDeps_(rule);
if (path) {
goog.included_[path] = true;
} else {
path = rule.replace(/\./g,'/')+'.js'
for(var prefix in goog.namespacePrefixes) {
path = path.replace(new RegExp("^"+prefix), goog.namespacePrefixes[prefix])
}
goog.included_[path] = true;
setTimeout(function() {
if (goog.getObjectByName(rule)) {
return
}
var errorMessage = 'goog.require could not find: ' + rule;
if (goog.global.console) {
goog.global.console['error'](errorMessage);
}
throw Error(errorMessage);
}, 0)
}
goog.writeScripts_();
}
};
В функции выше также оставлен контроль ошибок. В отличие от простого require , мы можем узнать, появился ли символ, только после загрузки файла, поэтому проверка обернута в setTimeout(..., 0) . Такой вызов сработает сразу после синхронной загрузки script .
Вы можете пропатчить base.js или, что гораздо лучше, загрузить свой файл с новым require и префиксами, который перезапишет определение в base.js.
После этого goog.require начнет подгружать зависимости аналогично системе зависимостей в dojo toolkit и других фреймворках.
Можно расширить эту функциональность, добавить подгрузку вида goog.require('my.*'), более сложную логику нахождения файла по символу и т.п., но нужно ли?
При сборке нескольких модулей Google Closure Compiler проверяет зависимости require/provide , но при этом многомодульные сборки можно делать и без них, как описано в соответствующей статье.
Для удобства примеры в этой статье собраны в архив gbuild.zip.
Он содержит модифицированный require и иллюстрирует, как использовать систему сборки Google, даже если вы не используете Google Closure Library.
Содержимое:
goog
директория с base.js из Google Closure Library
my
директория с нашими js-файлами, base_require.js содержит модифицированный require
calcdeps.py
стандартный calcdeps.py из Google Closure Library
compiler.jar
стандартный Google Closure Compiler
compile.bat
файл для компиляции hello.js с ADVANCED оптимизациями и зависимостями
hello.html
файл-пример, использующий hello.js
|
Кто-то разбирался как подцепить Google Closure Compiler к Ant'у?
Статья просто супер. Буду рекомендовать ее всем своим знакомым. Спасибо!!!
да, наверно так и есть
Юзаю для этого steal из JavaScriptMVC.
Ихний сборщик нуждается в допиливании, но на выходе - удобная штука.
Спасибо за эту информацию!