Ваша первая чашечка CoffeeScript: Часть 4. Использование CoffeeScript на стороне сервера

Этот цикл статей посвящен изучению популярного языка программирования CoffeeScript, построенного поверх JavaScript. CoffeeScript компилируется в эффективный код JavaScript, соответствующий многими практическим рекомендациям. Этот код JavaScript можно выполнять в Web-браузере или использовать для серверных приложений вместе с такими технологиями, как Node.js. В предыдущих статьях этого цикла мы рассмотрели преимущества CoffeeScript, настроили среду разработки, попробовали многие возможности языка и написали код реального приложения CoffeeScript со стороны клиента. В этой, последней части цикла мы напишем код CoffeeScript для сервера.

Майкл Галпин, инженер по программному обеспечению, Vitria Technology

Майкл Галпин (Michael Galpin) имеет учёную степень по математике в Калифорнийском Технологическом институте. Он является Java-разработчиком с конца 90-х гг. и работает инженером по программному обеспечению в Vitria Technology, в Саннивейл, Калифорния.



20.07.2012

Введение

CoffeeScript, новый язык программирования, построенный поверх JavaScript, обеспечивает рациональный синтаксис, который должен понравиться всем сторонникам Python и Ruby. Он содержит также множество функциональных возможностей, навеянных такими языками программирования, как Haskell и Lisp.

В первой статье этого цикла мы рассказали о преимуществах JavaScript. Мы установили среду разработки и выполнили несколько сценариев. Часть 2 посвящена практическому изучению CoffeeScriptна на примерах математических задач. В Части 3 мы написали код Web-приложения со стороны клиента.

В этой, заключительной статье цикла нам предстоит написать серверный компонент и завершить приложение – и все это на CoffeeScript.

Загрузите исходный код, который будет использоваться в этой статье.


Обращение к Web-сервисам

Web-приложение, написанное в Части 3, выполняет поиск в Google и Twitter по ключевым словам. Для клиентской части приложения мы просто смакетировали результаты, принимаемые от сервера. Для фактической реализации этих функций требуется серверная часть приложения, которая обращается к Web-сервисам, предоставляемым Google и Twitter. Обе компании предлагают очень простые поисковые сервисы. Все, что требуется, ― это обращение к ним с запросами HTTP GET. В листинге 1 показана общая функция для отправки запросов HTTP GET.

Листинг 1. Обращение к Web-ресурсам
http = require "http"

fetchPage = (host, port, path, callback) ->
    options = 
        host: host
        port: port
        path: path
    req = http.get options, (res) ->
        contents = ""
        res.on 'data', (chunk) ->
            contents += "#{chunk}"
        res.on 'end', () ->
            callback(contents)
    req.on "error", (e) ->
        console.log "Erorr: {e.message}"

В верхней части сценария расположен оператор require, который мы кратко рассмотрели в Части 1. Это синтаксис импорта модуля Node.js, или, точнее, его CoffeeScript-версия. "Родной" версией будет var http = require("http"). В этой статье мы будем использовать несколько модулей ядра Node.js. (Подробное рассмотрение работы модулей выходит за рамки этой статьи.) Если вы установили Node.js, то все модули, используемые в этой статье, должны быть доступны (см. Часть 1). Например, в листинге 1 используется модуль http, который обеспечивает несколько полезных классов и функций как для передачи, так и для приема HTTP-запросов.

Затем в листинге определяется функция fetchPage, которая принимает четыре параметра:

  • имя хоста ресурса host;
  • порт ресурса port;
  • путь к ресурсу path;
  • функцию обратного вызова callback.

    Любая функция ввода/вывода в Node.js по своей природе асинхронна, и для обращения к ней по завершении ее работы требуется функция callback. Функция fetchPage принимает функцию callback в качестве своего четвертого параметра. Затем она использует первые три параметра, чтобы сделать запрос HTTP GET с использованием функции get модуля http.

Функция fetchPage принимает также функцию callback, которой передается экземпляр ClientResponse. Объект ClientResponse, определенный в модуле http, реализует интерфейс ReadableStream (основной интерфейс Node.js). Это асинхронный интерфейс, который получает два события: data и end. Его единственная функция используется для регистрации обратного вызова этих событий. Событие данных происходит при получении данных из ресурса, к которому был сделан запрос HTTP GET.

Ресурс может возвратить все данные сразу, но чаще данные передаются частями. При получении каждого фрагмента инициируется событие данных и выполняется обратный вызов. Каждый раз при получении очередного фрагмента он просто добавляется к созданной нами переменной contents. Когда все данные получены, инициируется событие end. Теперь у нас есть все данные, и мы можем возвратить contents в функцию callback, которая была передана функции fetchPage. Определив эту многоцелевую функцию, мы должны создать еще несколько специализированных функций для API поиска Google и Twitter, как показано в листинге 2.

Листинг 2. Функции поиска Google и Twitter
googleSearch = (keyword, callback) ->
    host = "ajax.googleapis.com"
    path = "/ajax/services/search/web?v=1.0&q=#{encodeURI(keyword)}"
    fetchPage host, 80, path, callback

twitterSearch = (keyword, callback) ->
    host = "search.twitter.com"
    path = "/search.json?q=#{encodeURI(keyword)}"
    fetchPage host, 80, path, callback

В листинге 2 определены две функции:

  • googleSearch, которая принимает keyword и функцию callback. Она определяет хост, динамически создает путь с использованием интерполяции строк CoffeeScript, а затем использует fetchPage;
  • twitterSearch, которая очень похожа на googleSearch, но имеет другие значения хоста и пути.

Для обоих значений пути используется интерполяции строк и удобная функция JavaScript в encodeURI для обработки любых пробелов и прочих специальных символов. Теперь, когда у нас есть эти функции поиска, можно создать специализированную функцию сценария комбинированного поиска.


Объединение асинхронных функций

Существует несколько способов комбинирования поиска в Google и Twitter. Можно вызвать googleSearch, а затем в callback инициировать twitterSearch, или наоборот. Однако архитектура асинхронного обратного вызова Node.js позволяет сделать это элегантнее и эффективнее. Комбинированный поиск показан в листинге 3.

Листинг 3. Поиск в Google и Twitter
combinedSearch = (keyword, callback) ->
    data = 
        google : ""
        twitter : ""
    googleSearch keyword, (contents) ->
        contents = JSON.parse contents
        data.google = contents.responseData.results
        if data.twitter != ""
            callback(data)
    twitterSearch keyword, (contents) ->
        contents = JSON.parse contents
        data.twitter = contents.results
        if data.google != ""
            callback(data)

Функция combinedSearch имеет уже знакомое нам свойство: принимать ключевое слово и обратный вызов. Затем она создает структуру данных для комбинированных результатов поиска data. Объект data содержит поля google и twitter, которые инициализируются как пустые строки. На следующем шаге вызывается функция googleSearch. При ее обратном вызове результаты поиска Google анализируются с помощью стандартной функции JSON.parse. Текст JSON, возвращаемый Google, разбирается и помещается в объект JavaScript. Используем его, чтобы задать значение поля data.google. После вызова googleSearch вызовем twitterSearch. Его функция callback очень похожа на функцию для googleSearch.

Важно понимать, что в обоих случаях мы проверяем, присутствуют ли данные другого обратного вызова. Мы не знаем, какой из них закончится первым. Поэтому нужно проверять наличие данных и из Google, и из Twitter. Когда те и другие получены, можно вызвать функцию callback, переданную функции в combinedSearch. Теперь у нас есть функция, которая выполняет поиск в Google и Twitter и выдает комбинированные результаты. Следующая задача заключается в том, чтобы разместить их на Web-странице, созданной в Части 3. Все, что нужно сделать, это написать Web-сервер.


Web-сервера на CoffeeScript

На данный момент у нас есть:

  • Web-страница, способная передавать ключевые слова и показывать результаты поиска;
  • функция, которая может принять ключевое слово и получить результаты поиска от Google и Twitter.

Что соединяет их вместе? Это можно назвать Web-сервером, сервером Web-приложений или даже связующим ПО. Как бы это ни называлось, много кода CoffeeScript для него не потребуется.

Web-сервер должен решать две задачи. Очевидно, что он должен принимать запросы на комбинированный поиск. Кроме того, он должен предоставлять статические ресурсы, созданные в Части 3. Мы создаем Web-приложение, поэтому нужно помнить правило единого источника. Поисковые вызовы должны поступать в то же место, откуда пришла Web-страница. Давайте сначала займемся статическими ресурсами. Листинг 4 иллюстрирует функцию для обслуживания статических ресурсов.

Листинг 4. Обслуживание статических ресурсов
path = require "path"
fs = require "fs"
serveStatic = (uri, response) ->
    fileName = path.join process.cwd(), uri
    path.exists fileName, (exists) ->
        if not exists
            response.writeHead 404, 'Content-Type': 'text/plain'
            response.end "404 Not Found #{uri}!\n"
            return
        fs.readFile fileName, "binary", (err,file) ->
            if err
                response.writeHead 500, 
                            'Content-Type': 'text/plain'
                response.end "Error #{uri}: #{err} \n"
                return
            response.writeHead 200
            response.write file, "binary"
            response.end()

Функция ServeStatic обрабатывает запросы на статические ресурсы в Web-приложении. Нам понадобятся еще два модуля Node.js:

  • path — это просто вспомогательная библиотека для обработки путей к файлам;
  • файловая система, или fs, которая передает все файлы ввода и вывода в Node.js и в основном представляет собой оболочку поверх стандартных функций POSIX.

Функция serveStatic принимает два параметра:

  • uri представляет собой, по существу, относительный путь к статическому файлу, к которому обращается Web-браузер;
  • объект ServerResponse, еще один тип, определенный в модуле http. Среди прочего он создает поток для записи данных туда, откуда сделан запрос HTTP GET к ресурсу.

В serveStatic превратим относительный путь к файлу в абсолютный путь с помощью process.cwd. Объект process ― это глобальный объект системного процесса, в котором работает Node.js. Его метод cwd указывает текущий рабочий каталог. Используем модуль path для объединения текущего рабочего каталога и относительного пути к нужному файлу; результатом будет абсолютный путь. Имея абсолютный путь, можно вновь использовать модуль path, чтобы проверить, существует ли файл. Для проверки используются операции ввода-вывода, так что это асинхронная функция. Передадим ей значение fileName и функцию обратного вызова. Функция обратного вызова выдает логическое значение, давая знать, существует ли этот файл. Если нет, нужно отправить сообщение HTTP 404 "Файл не найден".

Если файл существует, нужно прочитать его содержимое с помощью модуля fs и его асинхронного метода readFile. Передадим ему значение fileName, тип и функцию обратного вызова. Функция обратного вызова получает два параметра:

  • параметр error, указывающий на любые проблемы чтения ресурса из файловой системы. В случае проблемы клиенту возвращается сообщение об ошибке HTTP 500;
  • если никаких проблем нет, выдается сообщение HTTP 200 OK, и клиенту направляется содержимое файла.

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


Динамические ответы и сервер

Пример Web-сервера главным образом обрабатывает запросы к статическим ресурсам и динамические поисковые запросы. Стратегия заключается в том, чтобы использовать для обработки поисковых запросов определенный URL-адрес, а затем передавать другие запросы в функцию serveStatic. Для поисковых запросов будем использовать относительный URL-адрес из /doSearch. Код Web-сервера приведен в листинге 5.

Листинг 5. Web-сервер на CoffeeScript
url = require "url"
server = http.createServer (request, response) ->
    uri = url.parse(request.url)
    if uri.pathname is "/doSearch"
        doSearch uri, response
    else
        serveStatic uri.pathname, response    
server.listen 8080
console.log "Server running at http://127.0.0.1:8080"

Опять же сценарий начинается с загрузки модуля Node.js. Модуль url представляет собой полезную библиотеку для анализа URL-адресов. Следующий шаг заключается в создании Web-сервера с использованием модуля http, загруженного в листинге 1. Используем метод createServer этого модуля, принимающий функцию обратного вызова, которая вызывается при каждом обращении к Web-серверу. Функция обратного вызова принимает два параметра: экземпляр ServerRequest и экземпляр ServerResponse. Оба типа определены в модуле http. При обратном вызове выполним разбор URL-адреса запроса к серверу с помощью метода parse модуля url. Это дает нам объекта URL, и его свойство pathname можно использовать для получения относительного пути. Если pathname/doSearch, вызывается функция doSearch (см. ниже). В противном случае вызывается функция serveStatic из листинга 5. В листинге 5 показано, как работает doSearch.

Листинг 6. Управление запросами
doSearch = (uri, response) ->
    query = uri.query.split "&"
    params = {}
    query.forEach (nv) ->
        nvp = nv.split "="
        params[nvp[0]] = nvp[1]
    keyword = params["q"]
    combinedSearch keyword, (results) ->
        response.writeHead 200, 'Content-Type': 'text/plain'
        response.end JSON.stringify results

Функция doSearch выполняет синтаксический анализ строки запроса URL-адреса, которая находится в свойстве query объекта uri. Для этого строки разбиваются по амперсандам. Затем каждая подстрока разбивается по знакам равенства, чтобы получить пары имя-значение из строки запроса. Каждая из них сохраняется в объекте params. Извлечем параметр "q", чтобы получить ключевое слово для поиска. Передадим его функции combinedSearch из листинга 3. Ей нужно передать функцию обратного вызова. Пример функции обратного вызова просто выдает сообщение HTTP 200 OK и превращает результаты поиска в строку с помощью стандартной функции JSON.stringify.

Вот и все, что требуется для сервера. В следующем разделе показано, как связать это с кодом клиента из Части 3.


Обращение к поисковому серверу

В Части 3 мы создали класс MockSearch, который использовал макет данных для представления результатов поиска. Теперь определим новый класс для реального поиска, который обращается к поисковому серверу. Новый класс поиска показан в листинге 7.

Листинг 7. Класс реального поиска
class CombinedSearch
    search: (keyword, callback) ->
        xhr = new XMLHttpRequest
        xhr.open "GET", "/doSearch?q=#{encodeURI(keyword)}", true
        xhr.onreadystatechange = ->
            if xhr.readyState is 4
                if xhr.status is 200
                    response = JSON.parse xhr.responseText
                    results = 
                        google: response.google.map (result) -> 
                            new GoogleSearchResult result
                        twitter: response.twitter.map (result) -> 
                            new TwitterSearchResult result
                    callback results
        xhr.send null

Класс CombinedSearch содержит один метод поиска с теми же характеристиками, что и у метода поиска MockSearch. Ему требуется ключевое слово и функция обратного вызова. Внутри этой функции:

  • используем XMLHttpRequest, старого друга каждого Web-разработчика, чтобы сделать HTTP-запрос к серверу с использованием пути /doSearch и ключевого слова, переданного функции;
  • получив ответ, проанализируем его с помощью JSON.parse;
  • Создадим объект результатов с полями google и twitter. Для их создания используем классы GoogleSearchResult и TwitterSearchResult из Части 3;
  • результаты просто возвратим в функцию callback.

Теперь нужно использовать ее в методе doSearch на Web-странице вместо MockSearch. В листинге 8 показано, как использовать класс CombinedSearch.

Листинг 8. Использование класса CombinedSearch
@doSearch = ->
    $ = (id) -> document.getElementById(id)
    kw = $("searchQuery").value
    appender = (id, data) ->
        data.forEach (x) -> 
            $(id).innerHTML += "<p>#{x.toHtml()}</p>"
    ms = new CombinedSearch
    ms.search kw, (results) ->
        appender("gr", results.google)
        appender("tr", results.twitter)

Если сравнить листинг 8 с doSearch из Части 3, различий будет немного. Отличается только седьмая строка. Вместо создания экземпляра MockSearch создается экземпляр CombinedSearch. Все остальное то же самое. Мы получаем ключевое слово от Web-страницы, вызываем поиск, а затем добавляем результаты, вызывая метод toHtml каждого объекта SearchResult. На рисунке 1 показано Web-приложение с "живыми" результатами поиска, полученными от сервера.

Рисунок 1. Выполнение примера Web-приложения
Выполнение примера Web-приложения

Чтобы изменения, внесенные в код клиента, вступили в силу, нужно перекомпилировать его с помощью команды coffee -c search.coffee. Для запуска приложения используйте команду coffee search-server.coffee. Затем можно направить браузер по адресу: http://127.0.0.1:8080 и попробовать несколько разных запросов.


Заключение

В этой статье мы закончили Web-приложение, написав серверный компонент в дополнение к коду клиента из Части 3. Теперь, в конце этого цикла статей, у нас есть полное приложение – написанное целиком на CoffeeScript. Мы использовали множество функций Node.js, которые позволяют применять CoffeeScript в качестве серверной технологии.

Node.js часто критикуют за то, что его неблокирующий стиль приводит к нагромождению уровней функций обратного вызова. Они трудно укладываются в голове, а многословный синтаксис JavaScript еще больше затрудняет понимание кода. CoffeeScript не исключает необходимости использования всех этих обратных вызовов, но его элегантный синтаксис несколько упрощает написание и понимание такого кода.


Загрузка

ОписаниеИмяРазмер
Исходный код примера для статьиcs4.zip7 КБ

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Web-архитектура, Open source
ArticleID=826744
ArticleTitle=Ваша первая чашечка CoffeeScript: Часть 4. Использование CoffeeScript на стороне сервера
publish-date=07202012