на главную ] 

Google Closure Library.
Краткий обзор инструментов с примерами их использования.

Евгений Борисов

четверг, 7 марта 2013 г.

 
 

В этой статье речь пойдёт о Google Closure Library (GCL) [1]. Это модульная, кросс-браузерная библиотека программирования для разработки web-приложений на JavaScript, с помощью которой построены популярные интернет сервисы Gmail, YouTube и др.

Она содержит структуры данных, средства для управления DOM и коммуникации с сервером (AJAX), набор компонентов пользовательского интерфейса, элементов управления и визуальных эффектов, а так же компоненты для отладки и модульного тестирования. Примеры использования компонентов GCL можно посмотреть в [2], отметим, что эти примеры для наглядности приводятся в не оптимизированном виде.

 
 

1. Hello World

Для начала скачаем файлы библиотеки.

# mkdir ~/closure
# cd ~/closure
# svn checkout http://closure-library.googlecode.com/svn/trunk/ closure-library

Хотя в наборе инструментов имеется специальный оптимизирующий компилятор, о котором мы расскажем в следующих разделах, библиотеку можно использовать и без его применения, для этого в HTML необходимо указать ссылку на файлы библиотеки, приведём простой пример.

index.html:
<!doctype html>
<html>
<head>
   <meta charset="utf-8" />
   <script src="../closure-library/closure/goog/base.js"></script>
   <script type="text/javascript">
	  goog.require('goog.dom');
   </script>
</head>
<body>
   <div id="hello"></div>
   <script type="text/javascript">
	  goog.dom.getElement('hello').innerHTML = '<b>Hello World!</b>';
   </script>
</body>
</html>

Здесь мы не будем подробно рассматривать компоненты GCL, их описание можно найти в [3],[4].

 
 

2. Применение Google Closure Compiler

Важной частью GCL есть Google Closure Compiler (GCC) [5] - оптимизирующий компилятор, использующий специальные комментарии JSDoc [9] в тексте программы. GCC из исходного кода на JavaScript генерирует оптимизированный код на JavaScript, во время работы он проверяет корректность исходников, собирает код необходимых частей GCL и включает его в результирующий код. На выходе получаем оптимизированный скрипт, которому для работы уже не требуются файлы GCL.

GCC написан на Java и для его (GCC) применения используются скрипты на языке Phyton, т.е. для работы понадобиться JRE [6] и Phyton2 [7].

Качаем GCC:

# cd ~/closure
# wget -c http://closure-compiler.googlecode.com/files/compiler-latest.zip
# unzip compiler-latest.zip

$ java -jar compiler.jar --version
Closure Compiler (http://code.google.com/closure/compiler)
Version: 20121212 (revision 2388)
Built on: 2012/12/12 17:42   

Немного перепишем первый пример, выделим код JavaScript в отдельный файл:

start.js:
goog.provide('myproject.start');
goog.require('goog.dom');
myproject.start = function() {
  var newDiv = goog.dom.createDom('h1', {'style': 'background-color:#EEE'}, 'Hello world!');
  goog.dom.appendChild(document.body, newDiv);
};
goog.exportSymbol('myproject.start', myproject.start);

Компилируем JavaScript:

# python2 ../closure/library/closure/bin/build/closurebuilder.py \
	 --root=../closure/library \
	 --root=. \
	 --namespace= "myproject.start" \
	 --output_mode=compiled \
	 --compiler_jar=../closure/compiler/compiler.jar \
	 --compiler_flags="--compilation_level=ADVANCED_OPTIMIZATIONS" \
	 --compiler_flags="--jscomp_error=visibility" \
	 --compiler_flags="--jscomp_warning=visibility" \
	 --compiler_flags="--warning_level=VERBOSE" \
	 > start-compiled.js

в результате получаем скрипт start-compiled.js, который вставляем в HTML.

index.html:
<!DOCTYPE html>
<html>
<head>
   <meta charset="utf-8"/>
   <script src="start-compiled.js"></script>
</head>
<body>
   <script>myproject.start();</script>
</body>
</html>

Отметим, что скрипту start-compiled.js для работы уже не требуются файлы GCL.

 
 

3. Шаблоны

GCL имеет свою систему шаблонов для генерации HTML. Для работы с шаблонами нам понадобиться компилятор шаблонов Soy [8], который генерирует код JavaScript из файлов-шаблонов.

Качаем Soy:

# cd ~/closure
# wget -c http://closure-templates.googlecode.com/files/closure-templates-for-javascript-latest.zip
# unzip closure-templates-for-javascript-latest.zip

Пример шаблона с аннотациями JSDoc[9]:

hello.soy:
{namespace example.templates}
/**
* @param greeting
* @param year
*/
{template .welcome}
<h1 id="greeting">{$greeting}</h1>
The year is {$year}.
{/template}

Пример кода JavaScript, использующего soy-шаблон:

start.js:
goog.provide('example.start');

goog.require('goog.dom');
goog.require('example.templates');

/**
* @param {string} message
*/
example.start= function(message) {
   var data = { greeting: message, year: new Date().getFullYear() };
   var html = example.templates.welcome(data);
   goog.dom.getElement('hello').innerHTML = html;
};

goog.exportSymbol('example.start', example.start);

Генерируем JavaScript из шаблона hello.soy:

# java -jar  ~/closure/templates/SoyToJsSrcCompiler.jar \
	  --outputPathFormat hello.soy.js \
	  --shouldGenerateJsdoc \
	  --shouldProvideRequireSoyNamespaces \
	  hello.soy

Компилируем JavaScript с шаблонами:

# python2 ../closure/library/closure/bin/build/closurebuilder.py \
	 --root=../closure/templates \ 
	 --root=../closure/library \
	 --root=. \
	 --namespace= "example.start" \
	 --output_mode=compiled \
	 --compiler_jar=../closure/compiler/compiler.jar \
	 --compiler_flags="--compilation_level=ADVANCED_OPTIMIZATIONS" \
	 --compiler_flags="--jscomp_error=visibility" \
	 --compiler_flags="--jscomp_warning=visibility" \
	 --compiler_flags="--warning_level=VERBOSE" \
	 > start-compiled.js

Вставляем скрипт в HTML.

index.html:
<!doctype html>
<html>
<head>
   <meta charset="utf-8" />
</head>
<body>
   <div id="hello"></div>
   <script src="start-compiled.js"></script>
   <script> example.start('Hello World!'); </script>
</body>
</html>

 
 

4. Локализация и интернационализация

В этом разделе поговорим об адаптации к языковым и культурным особенностям региона (регионов), отличного от того, в котором разрабатывался продукт [14].

К решению этой технической задачи есть два подхода - локализация и интернационализация.

Локализация (L10n) это "статическая" адаптация программы к данным языковым особенностям. Она выполняется на этапе компиляции и жестко задаёт язык пользовательского интерфейса программы.

Интернационализация (i18n), в отличии от локализации, это "динамическая" (т.е. runtime) адаптация программы. Система изначально проектируется так, что бы пользователь в процессе работы с программой мог выбирать язык и форматы вывода информации.

В GCL есть два пакета для локализации и интернационализации - goog.locale и goog.i18n [10, 11], при этом goog.locale на настоящий момент помечен как устаревший (deprecated).

 
 

4.1 Локализация

При использовании метода статической локализации компилируется несколько экземпляров скрипта для разных языков. Для работы системы выбирается один из них в соответствии с текущими установками языка.

Для смены локализации приложения требуется перекомпиляция с другим словарём. Словарь в формате XLLIF [12] генерируется специальной утилитой SoyMsgExtractor.

Скачаем её:

# cd ~/closure
# wget -c http://closure-templates.googlecode.com/files/closure-templates-msg-extractor-latest.zip
# unzip closure-templates-msg-extractor-latest.zip

Для перевода словаря, сгенерированного SoyMsgExtractor, можно воспользоваться специальным инструментом - Open Language Tools XLIFF Translation Editor [12].

Схема статической локализации с использованием шаблонов soy выглядит следующим образом.

  1. создаём шаблон

    Текстовые метки, требующие перевода в локализованных версиях приложения (надписи на кнопках и др.), помещаем в специальные операторные скобки
    {msg descr="примечание для переводчика"} текст {/msg}.

    template.soy:
    {namespace myproject.templates}
    /**
    * display template
    */
    {template .messages}
    <b>{msg desc="label 1"}это сообщение{/msg} </b> 
    <i>{msg desc="label 2"}на русском{/msg} </i>
    <u>{msg desc="label 3"}языке{/msg} </u>
    <p/>
    {/template}
    

    все метки пишем на одном языке (русском).

  2. Генерируем начальный словарь:

    запускаем сборщик текстовых меток SoyMsgExtractor.

    # java -jar SoyMsgExtractor.jar
       --sourceLocaleString 'ru' \
       --targetLocaleString 'en' \
       --outputFile messages_ru.xlf \
       *.soy
    

    Он генерирует словарь messages_ru.xlf в формате XLLIF (в данном случае на русско-английский).

  3. переводим messages_ru.xlf с помощью XLIFF Translation Editor [13].

    # cp  messages_ru.xlf messages_en.xlf
    # xliff-editor/translation.sh messages_en.xlf
    

  4. генерируем код из template.soy с использованием файла локализации

    # java -jar SoyToJsSrcCompiler.jar \
       --shouldGenerateJsdoc \
       --shouldProvideRequireSoyNamespaces \
       --locales ru \
       --messageFilePathFormat messages_ru.xlf  \
       --outputPathFormat template_ru.soy.js  \
       *.soy 
    

  5. Выбираем скомпилированный локализованный шаблон template_ru.soy.js и компилируем вместе с ним основной код.

    На этапе компиляции задаём параметр goog.LOCALE.

    # python2 closurebuilder.py \
       --root=templates \
       --root=library \
       --root=. \
       --namespace=mynamespace \
       --output_mode=compiled \
       --compiler_jar=compiler.jar \
       --compiler_flags="--compilation_level=ADVANCED_OPTIMIZATIONS" \
       --compiler_flags="--jscomp_error=visibility" \
       --compiler_flags="--jscomp_warning=visibility" \
       --compiler_flags="--define='goog.LOCALE=\"ru\"'" \
       --compiler_flags="--warning_level=VERBOSE" \
       > app-ru-compiled.js
    

  6. Повторяем два предыдущих шага для всех необходимых языков.

 
 

4.2 Интернационализация

Теперь рассмотрим второй способ - смена языка в процессе выполнения (runtime).

В этом случае Soy, при генерации кода JavaScript из шаблона, подставляет вызов функции goog.getMsg в местах вывода сообщений. Для включения этого режима Soy используется флаг --shouldGenerateGoogMsgDefs.

# java -jar SoyToJsSrcCompiler.jar  \
   --shouldGenerateGoogMsgDefs  --bidiGlobalDir 1 \ 
   --shouldGenerateJsdoc \
   --shouldProvideRequireSoyNamespaces \
   --outputPathFormat templates.soy.js \
   *.soy

Для переключения языка необходимо определить свои словари и переопределить функцию goog.getMsg, которая осуществляет замену набор сообщений по умолчанию на локализованные сообщения, например так:

myproject.i18n.msg.ru_RU = {
   'this is':'это',
   'localized':'переведенное',
   'message' : 'сообщение',
   'Hello':'Привет'
};

myproject.i18n.msg.current = myproject.i18n.msg.ru_RU;

goog.getMsg = function(str, opt_values) {
  str = myproject.i18n.msg.current[str] || str;
  var values = opt_values || {};
  for (var key in values) {
    var value = ('' + values[key]).replace(/\$/g, '$$$$');
    str = str.replace(new RegExp('\\{\\$' + key + '\\}', 'gi'), value);
  }
  return str;
};

К сожалению тут есть затруднения, в режиме компилятора ADVANCED_OPTIMIZATIONS переопределение goog.getMsg не срабатывает и упорно вызывается оригинальная версия функции.

 
 

5. Компоненты для отладки

GCL содержит в себе специальные компоненты для тестирования и отладки приложений - goog.testing и goog.debug [3], [10]. В этом разделе мы приведём несколько примеров их использования.

 
 

5.1 тесты

Пример использования тестов goog.testing.jsunit:

index.html:
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <script src="../library/closure/goog/base.js"></script>
  <script>
   goog.require('goog.testing.jsunit');
   goog.require('goog.dom');
   goog.require('goog.dom.NodeType');

   var testHtmlEscaping = function() {
	  var el = goog.dom.getElement('test');
	  assertEquals('The <div id="test"> element should only have 5 child node', 5, el.childNodes.length);
   };
   </script>
</head>
<body>
   <div id="test"> test <br/> passed <br/> example</div> <p/>
</body>
</html>

Это тест проверяет количество дочерних элементов у <div id="test"/>

 
 

5.2 вывод отладочной информации

Пример использования goog.debug.Logger.

start.js:
goog.provide('myproject.start');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.events.EventTarget');
goog.require('goog.debug.ErrorHandler');

goog.require('goog.debug');
goog.require('goog.debug.Logger');
goog.require('goog.debug.DivConsole');


myproject.start = function() {
   // Set up a logger.
   var logger = goog.debug.Logger.getLogger('myproject');  
   goog.debug.LogManager.getRoot().setLevel(goog.debug.Logger.Level.ALL);
   var logconsole = new goog.debug.DivConsole(goog.dom.getElement('log'));
   logconsole.setCapturing(true);

   logger.log(goog.debug.Logger.Level.INFO,'hello');
   goog.events.listen( goog.dom.getElement('mousearea'), 
	  [goog.events.EventType.MOUSEMOVE,goog.events.EventType.CLICK] ,
	  function(e) { 
		 logger.log(goog.debug.Logger.Level.INFO, e.type + ' = ' + e.clientX + ':' + e.clientY ); 
	  } );

   goog.events.listen(window, 'unload', function() { goog.events.removeAll(); });
}
goog.exportSymbol('myproject.start', myproject.start);

Этот скрипт перехватывает сообщения мыши и сообщает об этом в соответствующей части окна.

index.html:

<html>
<head>
<meta charset="utf-8"/>
<style>
.goog-debug-panel .logdiv { position: relative; height:200px; overflow: scroll; overflow-x: hidden; overflow-y: scroll; }
#mousearea {width:500px;height:200px; text-align:center; background-color:#ccc; border:1px solid #000;}
#mousearea span { position:relative;top:45%;}
</style>
<script src='start-compiled.js'></script>
</head>
<body onload='myproject.start();'>
   <div id='mousearea'><span>test area</span></div> <p/>
   <fieldset class="goog-debug-panel"> <legend>Event Log</legend> <div id="log"></div> </fieldset> <p/>
</body> </html>

В нижней части окна создаётся область, где отображаются сообщения отладчика.

 
 

5.3 Профилирование скрипта

Пример использования goog.debug.Trace.

start.js:
goog.provide('myproject.start');

goog.require('goog.dom');
goog.require('goog.debug.Trace');

myproject.mindless_hard_work = function() { 
   // do nothing
   var e=0;
   for(var i = 1; i < 100000; i++ ) { // dzzz
	  e += goog.math.toDegrees(i) /  goog.math.toRadians(i);
	  e += goog.math.modulo(e, i);
   }
   return e;
}

myproject.start = function() {
   goog.debug.Trace.reset(0);
   // Start a trace.
   var tracer = goog.debug.Trace.startTracer('use expensive resource');
   goog.debug.Trace.addComment('resource used');

   var res=myproject.mindless_hard_work();

   // trace checkpoint
   goog.debug.Trace.addComment('resource returned');

   goog.dom.getElement('res').innerHTML = ''+res;

   goog.debug.Trace.stopTracer(tracer);
   goog.dom.getElement('tracer').innerHTML = goog.string.htmlEscape(goog.debug.Trace.getFormattedTrace());
}
// Ensures the symbol will be visible after compiler renaming.
goog.exportSymbol('myproject.start', myproject.start);

В этом скрипте использован компоненент goog.debug.Trace, который показывает время затраченое на исполнения фрагментов этого скрипта.

index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<script src='start-compiled.js'></script>
</head>
<body onload='myproject.start();'>
<h1>optimized</h1> [<a href='index_src.html'>source</a>]<p/>
   <div id='res'> </div><p/>
   <fieldset> <legend>Tracer Log</legend> <pre id='tracer'> </pre> </fieldset> 
</body> </html>

 

Литература

  1. Google Closure Library

  2. Google Closure Demos

  3. Michael Bolin Closure: The Definitive Guide - O’Reilly, 2010

  4. Антон Шевчук Google Closure руководство для начинающих.

  5. Google Closure Compiler

  6. Java.com

  7. Python.org

  8. Google Closure Templates

  9. Annotating JavaScript for the Google Closure Compiler

  10. Google Closure Library API Documentation

  11. Google Closure Templates Translation

  12. XLIFF

  13. Open Language Tools XLIFF Translation Editor

  14. Wikipedia :Internationalization and localization

При использовании материалов этого сайта, пожалуйста вставляйте в свой текст ссылку на мою статью.