В последнее время о Node.js много пишут в прессе, высоко превознося за модель асинхронного ввода-вывода, которая освобождает основной поток от ожидания ответов на запросы ввода-вывода и позволяет ему заниматься в этот период другой работой. Главная концепция Node.js состоит в том, что ввод-вывод — операция дорогостоящая, и поэтому предпринята попытка уменьшить эти издержки за счет принудительного введения модели асинхронного ввода-вывода. Я размышлял о том, как эта концепция может быть включена в уже существующую инфраструктуру. Если вы начинаете с нуля, сравнительно легко расписать технологические варианты и сделать выбор. Однако, если цель заключается в обновлении технологии для одной из частей решения, весь фокус в том, чтобы выбрать нечто современное, у которого есть будущее, которое не повлечет за собой массу дополнительных издержек и которое удастся без особых проблем встроить в существующее решение.
Именно это я и собираюсь продемонстрировать в данной статье. Я возьму существующее решение, которое позволяет просматривать документы в хранилище, но требует сигнатуры общего доступа (shared access signature) для их скачивания. В это решение я добавлю простой UI, использующий Node.js. Чтобы упростить эту реализацию, я задействую преимущества некоторых часто применяемых для Node.js инфраструктур. Таким образом, решение будет включать:
- Node.js — базовый механизм;
- Express — инфраструктура в стиле Model-View-Controller (MVC);
- Jade — механизм рендеринга и поддержки шаблонов.
Совместно эти три средства предоставят богатую инфраструктуру для построения UI, во многом аналогичную комбинации ASP.NET MVC 3 и Razor.
Приступаем
Если вы новичок в Node.js, вам лучше начать с изучения материалов, доступных на сайте Microsoft по ссылке windowsazure.com/develop/nodejs. Вам также потребуется установить Node.js SDK for Windows Azure. Кроме того, вы, вероятно, понадобится потратить немного времени на эксперименты с Express expressjs.com и Jade jade-lang.com. Вы обнаружите в этих инструментах некоторые привычные концепции, а также смесь знакомого и незнакомого синтаксиса.
В этом сценарии мои существующие сервисы будут выполнять работу на стороне Windows Azure, а сайт на основе Node.js, размещенный в Windows Azure, будет вызывать эти сервисы, чтобы визуализировать список документов для доступа. В целом, полезно создавать уровень абстракции между клиентом и сервисами. Это изолирует сервисы от любых изменений в интерфейсе, но истинная ценность такого разделения — в дополнительной функциональной гибкости и способе, которым вы можете включать и исключать провайдеров этих сервисов.
В существующем решении, как представлено на рис. 1, целью было выдавать пользователю доступ, только если он аутентифицирован, что приводит к генерации Shared Access Signature (SAS). Идея заключалась в том, чтобы предоставлять доступ для чтения аутентифицированным пользователям, а впоследствии выдавать полный CRUD-доступ (Create, Read, Update, Delete) к конкретному документу на основе ролей и уровня членства в группах. Здесь я сосредоточусь исключительно на разрешениях для чтения.
Увеличить
Рис. 1. Последовательность запросов
User | Пользователь |
Windows Azure Node.js Site | Сайт на основе Node.js в Windows Azure |
Custom Services | Собственные сервисы |
Windows Azure Storage | Windows Azure Storage |
Browse Page | Просмотр страницы |
Render List | Рендеринг списка |
Send Creds | Отправка удостоверений |
Click Link to Fetch Document | Щелчок ссылки для получения документа |
Get Document List Without SAS | Получение списка документов без SAS |
Login | Вход |
Return Access Key | Возврат ключа доступа |
Build List with SAS | Формирование списка с SAS |
Hyperlinked List | Список с гиперссылками |
Get Document List | Получение списка документов |
Get SAS | Получение SAS |
Return Lease | Возврат аренды |
Создание сервисов
Я имитирую сервис аутентификации, возвращающий некий идентификатор. Последующий вызов сервиса возвращает список файлов. Используемый мной контейнер Windows Azure Storage («documents») имеет ограниченные разрешения открытого доступа. Я хочу предоставлять список документов, даже если пользователь не аутентифицирован, но запретить неаутентифицированным пользователям возможность открытия файлов. Две сигнатуры вызова для созданного мной API выглядят так:
http://[host]/admin/GetAccess?user=[user]&pwd=[password]
http://[host]/admin/files?accessId=[authId]
Конечно, вам понадобится более реалистичный сервис аутентификации, который использует SSL и не оперирует строкой запроса; эту часть решения я не буду здесь описывать.
Первым делом надо написать метод для получения SAS (рис. 2) — он понадобится при создании метода, формирующего список документов.
Рис. 2. Метод для получения сигнатуры общего доступа (SAS)
public string GetSharedAccessSignature()
{
string sas = "";
sas = (string) System.Web.HttpContext.Current.Cache.
Get("sas");
// Если SAS нет, создаем его
if (sas == null)
{
// TODO: контейнер "зашит" в код,
// переместить в конфигурацию
CloudBlobContainer container =
blobClient.GetContainerReference("documents");
// Запрашиваем у контейнера передачу SAS
// в только что инициализированной политике
sas = container.GetSharedAccessSignature(
new SharedAccessPolicy()
{
SharedAccessStartTime = DateTime.Now,
SharedAccessExpiryTime =
DateTime.Now.AddMinutes(MaxMinutes),
Permissions = SharedAccessPermissions.Read |
SharedAccessPermissions.List
});
// Добавляем в кеш для повторного использования,
// поскольку это SAS, который не является индивидуальным
// для каждого пользователя
System.Web.HttpContext.Current.Cache.Add("sas", sas, null,
DateTime.Now.AddMinutes(MaxMinutes),
new TimeSpan(0,0,5,0,0), CacheItemPriority.High, null);
}
return sas
}
По большей части это довольно типичный код для Windows Azure Storage, пока дело не доходит до вызова GetSharedAccessSignature. Здесь нет политики общего доступа, поэтому мне нужно передать информацию о том, когда разрешать или запрещать доступ, а также сведения о типе разрешений. Все, что я хочу предоставить через SAS, — это возможность чтения и перечисления файлов. Кроме того, поскольку SAS теоретически будет использоваться любым аутентифицированным пользователем, я добавляю ее в кеш для многократного применения, чтобы избежать конфликтов и уменьшить объем вычислений при генерации ключей доступа.
Интерфейс сервиса будет сконфигурирован как WebMethod:
[OperationContract]
[WebGet(UriTemplate = "Files?accessId={accessId}")]
List<BlobInfo> GetFiles(string accessId);
Обратите внимание на использование собственного класса BlobInfo — и вновь я применяю уровень абстрагирования. У меня есть специфические поля, которые мне нужно возвращать, и IListBlobItem не обязательно представляет их. Поэтому я буду осуществлять маршалинг информации, возвращаемой из IListBlobItems в список своего типа, как показано на рис. 3.
Рис. 3. Реализация GetFiles
public List<BlobInfo> GetFiles(string accessId)
{
List<BlobInfo> blobs = new List<BlobInfo>();
CloudBlobClient sasBlobClient = default(CloudBlobClient);
CloudStorageAccount storageAccount =
CloudStorageAccount.FromConfigurationSetting(
"StorageAccountConnectionString");
string sas = default(string);
if(accessId.Length > 0)
{
// Для имитации просто выполняем элементарную проверку
if(VerifyId(accessId))
{
sas = GetSharedAccessSignature();
// Напрямую создаем BlobClient, используя SAS
sasBlobClient =
new CloudBlobClient(storageAccount.BlobEndpoint,
new StorageCredentialsSharedAccessSignature(sas));
}
}
else
{
sasBlobClient = storageAccount.CreateCloudBlobClient();
}
CloudBlobContainer blobContainer =
sasBlobClient.GetContainerReference("documents");
foreach (IListBlobItem blob in blobContainer.ListBlobs())
{
BlobInfo info = new BlobInfo();
info.Name = blob.Uri.LocalPath;
info.Uri = blob.Uri.AbsoluteUri;
info.Sas = sas;
info.CombinedUri = blob.Uri.AbsoluteUri + sas;
blobs.Add(info);
}
return blobs;
}
Важно отметить, что в коде на рис. 3 я применяю SAS, если пользователь аутентифицирован, чтобы возвращать список, который соответствует политике доступа для контейнера.
При наличии REST-сервиса я могу запустить быстрый тест через окно браузера. После такой настройки интерфейса сервиса можно достаточно легко имитировать аутентификацию, используя какое-нибудь общеизвестное значение, пока не будет написан цикл for, генерирующий список и пока SAS не начнет должным образом работать. VerifyId(string) просто проверяет, есть ли у меня удостоверение, кешированное с ключом, значение которого равно accessId. На рис. 4 показан список, возвращаемый без аутентификации. А поскольку он возвращается сервисом без аутентификации, значение SAS установлено в nil. Таким образом, использовать данные для визуализации списка можно, но предоставить рабочую ссылку пользователю нельзя, так как SAS нет.
Рис. 4. Список без аутентификации
На рис. 5 показан аутентифицированный список, который включает SAS.
Рис. 5. Аутентифицированный список с SAS
Разбор того, что именно возвращает сервис при аутентифицированном вызове, возлагается на клиент Node.js; кроме того, он должен визуализировать гиперссылки с SAS, который записывается в конец URI. Чтобы упростить эту задачу, я предоставил элемент CombinedUri, и клиенту нужно будет обращаться только к этому элементу. Наконец, хотя XML — хорошая штука, я все же работаю в Node.js, а потому имеет смысл изменить атрибуты интерфейса так, чтобы он возвращал JSON. Благодаря этому ответ сервиса можно будет напрямую использовать как объект:
[WebGet(UriTemplate = "Files?accessId={accessId}",
ResponseFormat=WebMessageFormat.Json)]
Вот как примерно выглядит JSON-вывод:
[{"CombinedUri":"https:\/\/footlocker.blob.core.windows.net\
/documents\/AzureKitchen-onesheet.docx?st=2012-03-05T05%3A22%
3A22Z&se=2012-03-05T05%3A27%3A22Z&sr=c&sp=rl&sig=Fh41ZuV2y2z
5ZPHi9OIyGMfFK%2F4zudLU0x5bg25iJas%3D","Name":"\/documents\/
AzureKitchen-onesheet.docx","Sas":"?st=2012-03-05T05%3A22%
3A22Z&se=2012-03-05T05%3A27%3A22Z&sr=c&sp=rl&sig=Fh41ZuV2y2z
5ZPHi9OIyGMfFK%2F4zudLU0x5bg25iJas%3D","Uri":"https:\/\/
footlocker.blob.core.windows.net\/documents\/AzureKitchen-
onesheet.docx"}]
Как отмечалось, в конечном счете здесь нам нужен JSON, так как его можно прямо использовать в Express и Jade.
Node.js UI
Я уже установил Node.js, Express и Jade, так что я готов к созданию UI. Я развернул роли Node.js и запустил их в Visual Studio, но это довольно кропотливый и полностью ручной процесс. Поскольку никаких средств интеграции для этой части Node.js нет, я буду выполнять редактирование с помощью Sublime Text 2, а отладку — через Chrome (как описано в блоге Томаша Янчука [Tomasz Janczuk] по ссылке bit.ly/uvufEM).
Я должен упомянуть о некоторых вспомогательных средствах. Для тех, кто еще не прошел обряд инициации, применяемые мной инфраструктуры предоставляют ряд простых в использовании оболочек, инкапсулирующих определенную функциональность, MVC и механизм рендеринга шаблонов:
- Restler — облегчает REST-вызовы (считайте его упрощенным WebClient);
- Express — универсальная прикладная инфраструктура в стиле MVC;
- Jade — механизм рендеринга шаблонов, аналогичный Razor, который применяется в ASP.NET MVC.
Все эти модули являются компонентами Node.js (как DLL в .NET) и обычно устанавливаются с GitHub через Node Package Manager (NPM). Например, чтобы установить Restler, введите в папке проекта команду «npm install restler». Эта команда установит модуль и добавит ссылку на него в проект.
И еще одно для непосвященных. Вы увидите массу анонимных функций, вложенных в другие функции. Мой совет — просто переформатируйте код так, чтобы можно было видеть вложение в процессе работы с этим кодом, пока вы не привыкнете разбираться в нем без переформатирования. Я старался сделать свои примеры максимально удобными для чтения, а также использовать экранные снимки из Sublime, код на которых выделяется разными цветами (это тоже помогает восприятию кода).
При создании приложения AzureNodeExpress я применял команды New-AzureService и New-AzureWeb¬Role. Я также внес несколько других изменений. В server.js я добавил маршруты к странице Index; аналог в ASP.NET — метод MapRoutes, применяемый в проектах MVC.
Изменения в server.js
Мне нужно сообщить Node.js, какими библиотеками я буду пользоваться; это делается во многом по аналогии с выражениями using в C#. В Node.js такие ссылки задаются присваиванием переменной значения, возвращаемого функцией require(‘[имя_библиотеки]’). После задания ссылок я настраиваю некоторые конфигурационные переменные (например, устанавливаю view engine в «jade»). Особый интерес представляют view engine, router, bodyParser, cookieParser и session.
Я опускаю некоторые более прозаичные элементы, но настроить маршруты необходимо. Для команды Get в своей странице Index я буду просто визуализировать представление:
app.get('/index',
function(req, res){
res.render('index.jade', {title: 'index'});
}
);
Но в случае команды Post я хочу передавать обработку модели индекса (index model). Для этого нужно «связать» определенный метод модели:
app.post('/index', index.GetAccessKey.bind(index));
После этого можно переходить к настройке как представления, так и модели.
Представление: index.jade
В каком-то смысле я делаю скачок от начала к концу, переходя от контроллера к представлению, но при работе в стиле MVC я предпочитаю создавать на первых порах упрощенное представление. Синтаксис Jade в основном соответствует HTML, но в нем не используются квадратные скобки. Весь мой шаблон Jade показан на рис. 6.
Рис. 6. Шаблон Jade
html
head
title Default
body
h1 File List Home Page
br
label Welcome #{username}
form(method='post', action='/index')
label Username:
input(name='username', type='text')
br
label Password:
input(name='password', type='password')
br
button(type='submit') Login
h2 Files
form
table(border="1")
tr
td Name
td Uri
each doc in docList
tr
td #{doc.Name}
td
a(href=#{doc.CombinedUri}) #{doc.Name}
Заметьте, что здесь используются #{[var]} для ссылок на переменные и шаблон table с циклом внутри, который является своего рода сокращенной формой foreach. Я произвольно назвал список перебираемых элементов docList. Это важно, так как на странице index.js, где я прошу Jade визуализировать это представление, мне потребуется передать значение для docList. Остальное понятно и без пояснений, поскольку я создаю UI для разработчика — очень простой и безо всяких украшений.
Модель: index.js
Настроив инфраструктуру исполняющей среды в server.js и шаблон конечного представления в index.jade, остается написать прикладной код, выполняемый в index.js. Вспомните, что я связал app.Post со страницей Index. Эта привязка будет загружаться и запускать прототип, созданный мной в index.js. Для этого я добавлю функции в прототип индекса, как показано на рис. 7. По сути, я создаю именованную функцию (например, GetAccessKey) и определяю анонимную функцию как ее тело. В каждой из этих функций я буду использовать модуль Restler для упрощения необходимых мне REST-вызовов.
Рис. 7. Функции index.js
После связывания Post первой вызывается GetAccessKey, которая просто принимает имя пользователя и пароль, переданные через форму, дописывает их в URI как часть querystring и с помощью Restler выполняет Get. Вспомните, что в Node.js все взаимодействия происходят асинхронно, и это одна из причин обилия вложенных анонимных функций. Храня верность этому шаблону в вызове rest.get, я определяю анонимную функцию, выполняемую по завершении обработки запроса. Без кода обработки ошибок все сводится к следующему:
rest.get (uri).on('complete',
function(result){
accesskey = result;
this.ShowDocs (req, res);}
)
К счастью, такое переформатирование помогает понять, что здесь делается. Как только я получаю ключ от своего сервиса, я добавляю его в конец URI в этом методе, чтобы получить список документов. А теперь порядок вещей начинает отличаться от обычного. В анонимной функции, обрабатывающей данные, которые возвращаются REST-вызовом, для получения списка документов, я запрашиваю Jade визуализировать результаты:
res.render ('index', {title: "Doc List",
layout: false,
docList: result,
username: req.BODY.username});
Ранее я отметил, что в шаблоне я создал переменную с именем docList. Теперь мне нужно убедиться, что я использую именно это имя. Вызов res.render сообщает инфраструктуре Express визуализировать ресурс «index», а затем передать параметры через список пар «имя:значение», разделяемых двоеточиями и запятыми.
Исполняющая среда
Если попытаться перейти к одному из файлов, чтобы скачать его, на странице ничего не появляется. Эта веб-страница не найдена. Возможно, вы ожидали, что Windows Azure Storage сообщит об ошибке, связанной с неавторизованным доступом, но, если вы пытаетесь обратиться к какому-либо закрытому ресурсу, возвращается ошибка «ресурс не существует». Так и задумано, и это поведение предпочтительно, потому что частный ресурс не должен существовать в общедоступном пространстве. Если бы вместо этого возвращалась ошибка 401, она указывала бы на то, что такой ресурс на самом деле есть, и таким образом раскрывала бы сам факт его наличия.
Поскольку я защищаю свое хранилище, прямой доступ запрещен. Однако, как только я запускаю код примера, ситуация несколько меняется. Я публикую приложение командой Publish-AzureService из Windows PowerShell, перехожу к странице и ввожу свои удостоверения; после этого мне предоставляется список ссылок на файлы (рис. 8).
Рис. 8. Ссылки на файлы
Поскольку мой сервис является посредником при вызовах хранилища, я могу перечислять файлы, несмотря на то, что делать это напрямую невозможно. Кроме того, поскольку каждая ссылка дополняется SAS, после ее щелчка мне предлагается открыть или сохранить целевой документ.
Заключение
Если вас интересуют новые технологии для развития вашего приложения Windows Azure и вы следите за тем, что делается в сфере, связанной с Node.js, то Windows Azure — именно то, что вам нужно: она не только обеспечивает хостинг вашего решения, но и предоставляет вам при разработке различные варианты, например клиентскую библиотеку для Node.js, прямой или опосредованный (показанный в этой статье) доступ к REST API. Разработка, безусловно, была бы гораздо эффективнее и проще, если бы Node.js был обеспечен должной инструментальной поддержкой, но я уверен, что в конечном счете мы увидим ту или иную форму интеграции с Visual Studio, если популярность Node.js будет по-прежнему расти.