, w którym będzie wyświetlona lista dostępnych pokoi czatu. Element
Kolejny plik, który trzeba dodać, zawiera style CSS używane w aplikacji. W katalogu public/stylesheets utwórz plik o nazwie style.css, a następnie umieść w nim kod CSS przedstawiony w listingu 2.6. Listing 2.6. Kod CSS używany przez aplikację body { padding: 50px; font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; } a { color: #00B7FF; } #content { Aplikacja będzie miała szerokość 800 pikseli i zostanie wyśrodkowana poziomo. width: 800px; margin-left: auto; margin-right: auto; } #room { Reguła CSS dla elementu, w którym wyświetlana jest nazwa aktualnego pokoju czatu. background-color: #ddd; margin-bottom: 1em; } #messages { Element wiadomości ma szerokość 690 pikseli i wysokość 300 pikseli. width: 690px; height: 300px; overflow: auto; Element
, w którym będą wyświetlone wiadomości czatu.
Kolejny plik, który trzeba dodać, zawiera style CSS używane w aplikacji. W katalogu public/stylesheets utwórz plik o nazwie style.css, a następnie umieść w nim kod CSS przedstawiony w listingu 2.6. Listing 2.6. Kod CSS używany przez aplikację body { padding: 50px; font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; } a { color: #00B7FF; } #content { Aplikacja będzie miała szerokość 800 pikseli i zostanie wyśrodkowana poziomo. width: 800px; margin-left: auto; margin-right: auto; } #room { Reguła CSS dla elementu, w którym wyświetlana jest nazwa aktualnego pokoju czatu. background-color: #ddd; margin-bottom: 1em; } #messages { Element wiadomości ma szerokość 690 pikseli i wysokość 300 pikseli. width: 690px; height: 300px; overflow: auto; Element
wyświetlający wiadomości czatu będzie mógł być przewijany, gdy wiadomości całkowicie go wypełnią. background-color: #eee; margin-bottom: 1em; margin-right: 10px; }
Po dodaniu plików HTML i CSS możesz uruchomić aplikację w przeglądarce internetowej. Na obecnym etapie prac powinna wyglądać jak na rysunku 2.9. Aplikacja oczywiście nie oferuje jeszcze pełnej funkcjonalności, ale pliki
statyczne są udostępniane, a podstawowy układ graficzny prawidłowo generowany. Przechodzimy więc teraz do przygotowania kodu działającego po stronie serwera i odpowiedzialnego za obsługę wiadomości.
2.4. Obsługa wiadomości czatu za pomocą biblioteki Socket.IO Z trzech wymagań stawianych budowanym aplikacjom omówiliśmy dotąd pierwsze, czyli udostępnianie plików statycznych. Przechodzimy teraz do drugiego — obsługi komunikacji między przeglądarką internetową i serwerem. Nowoczesne przeglądarki internetowe mają możliwość użycia technologii WebSocket do obsługi komunikacji między przeglądarką i serwerem. (Dokładne informacje dotyczące obsługi WebSocket w przeglądarkach internetowych znajdziesz na stronie http://socket.io/#browser-support).
Rysunek 2.9. Aplikacja czatu na obecnym etapie prac
WebSocket zapewnia warstwę abstrakcji dla siebie oraz dla innych mechanizmów transportu, zarówno dla Node, jak i kodu JavaScript działającego po stronie klienta. Biblioteka Socket.IO automatycznie zapewnia rozwiązania awaryjne, jeśli obsługa WebSocket nie została zaimplementowana w
przeglądarce internetowej. We wszystkich przypadkach używane jest to samo API. W tym podrozdziale: pokrótce poznasz bibliotekę Socket.IO oraz zdefiniujesz funkcje Socket.IO niezbędne po stronie serwera; dodasz kod odpowiedzialny za konfigurację serwera Socket.IO; dodasz kod odpowiedzialny za obsługę różnych zdarzeń czatu. Biblioteka Socket.IO standardowo oferuje wirtualne kanały, więc zamiast rozgłaszać każdą wiadomość do wszystkich połączonych użytkowników, można ją przekazać jedynie do tych, którzy są subskrybentami danego kanału. Dzięki tej funkcji implementacja pokoi czatu w budowanej tutaj aplikacji staje się naprawdę łatwym zadaniem, o czym się wkrótce przekonasz. Biblioteka Socket.IO to również doskonały przykład użyteczności emiterów zdarzeń. Wspomniany emiter zdarzeń to w zasadzie użyteczny wzorzec organizacji logiki asynchronicznej. W tym rozdziale poznasz kod pewnych emiterów zdarzeń, ale tym tematem dokładnie zajmiemy się w następnym rozdziale. Emiter zdarzeń Pod względem koncepcji emiter zdarzeń jest powiązany z pewnego rodzaju zasobem, może wysyłać i otrzymywać wiadomości do oraz z zasobu. Wspomnianym zasobem może być połączenie ze zdalnym serwerem lub coś znacznie bardziej abstrakcyjnego, na przykład postać w grze. Projekt Johnny-Five (https://github.com/rwaldron/johnny-five) wykorzystuje Node w aplikacjach robotów i używa emiterów zdarzeń do kontrolowania mikrokontrolerów Arduino.
W pierwszej kolejności trzeba uruchomić serwer i przygotować logikę odpowiedzialną za nawiązywanie połączenia. Następnie przystąpimy do zdefiniowania funkcji wymaganych po stronie serwera.
2.4.1. Konfiguracja serwera Socket.IO Na początku w pliku server.js należy umieścić dwa podane poniżej wiersze kodu. Pierwszy powoduje wczytanie funkcji ze wskazanego modułu Node dostarczającego logikę potrzebną do obsługi po stronie serwera funkcji czatu związanych z biblioteką Socket.IO. Wskazany moduł zdefiniujemy za chwilę. Natomiast drugi wiersz uruchamia serwer i dostarcza funkcje Socket.IO przy założeniu, że mamy już zdefiniowany serwer HTTP, co pozwala na współdzielenie tego samego portu TCP/IP: var chatServer = require('./lib/chat_server'); chatServer.listen(server);
Teraz trzeba utworzyć plik chat_server.js w podkatalogu lib. Na początku w
wymienionym pliku umieść poniższe deklaracje zmiennych. Te deklaracje pozwolą na użycie biblioteki Socket.IO, a także inicjalizują kilka zmiennych przeznaczonych do definiowania stanu czatu: var socketio = require('socket.io'); var io; var guestNumber = 1; var nickNames = {}; var namesUsed = []; var currentRoom = {};
Utworzenie logiki połączenia Kolejnym krokiem jest dodanie przedstawionej w listingu 2.7 logiki odpowiedzialnej za zdefiniowanie funkcji listen serwera czatu. Wymieniona funkcja jest wywoływana w pliku server.js. Powoduje uruchomienie serwera Socket.IO, ogranicza ilość danych wyświetlanych w konsoli przez bibliotekę Socket.IO, a także definiuje sposób obsługi wszystkich połączeń przychodzących. Listing 2.7. Logika odpowiedzialna za uruchomienie serwera Socket.IO exports.listen = function(server) { io = socketio.listen(server); Uruchomienie serwera Socket.IO i umożliwienie mu współpracy z istniejącym serwerem HTTP. io.set('log level', 1); io.sockets.on('connection', function (socket) { Zdefiniowanie sposobu obsługi połączenia użytkownika. guestNumber = assignGuestName(socket, guestNumber, nickNames, namesUsed); Przypisanie użytkownikowi nazwy gościa podczas nawiązywania połączenia. joinRoom(socket, 'Lobby'); Umieszczenie użytkownika w pokoju Lobby, gdy próbuje on nawiązać połączenie. handleMessageBroadcasting(socket, nickNames); Obsługa wiadomości użytkownika, prób zmiany nazwy użytkownika, a także tworzenia lub zmiany pokoju czatu. handleNameChangeAttempts(socket, nickNames, namesUsed); handleRoomJoining(socket); socket.on('rooms', function() { Wyświetlenie użytkownika wraz z listą pokoi, w których prowadzi czat. socket.emit('rooms', io.sockets.manager.rooms); }); handleClientDisconnection(socket, nickNames, namesUsed); Zdefiniowanie logiki wykonywanej podczas rozłączania użytkownika. }); };
Jak możesz zauważyć, logika obsługi połączenia wywołuje wiele funkcji
pomocniczych, które teraz trzeba będzie zdefiniować w pliku chat_server.js. Po przygotowaniu logiki odpowiedzialnej za nawiązywanie połączenia trzeba zdefiniować kilka funkcji pomocniczych, które obsługują inne funkcjonalności oferowane przez aplikację.
2.4.2. Obsługa zdarzeń oraz scenariuszy w aplikacji Aplikacja czatu musi zapewnić obsługę wymienionych poniżej zdarzeń i rodzajów scenariuszy: przypisanie nazwy gościa, żądanie zmiany pokoju, żądanie zmiany nazwy użytkownika, wysyłanie wiadomości czatu, tworzenie pokoju, rozłączanie użytkownika. Do obsługi wymienionych funkcji dodatkowych funkcji pomocniczych.
konieczne
jest
przygotowanie
kilku
Przypisanie nazwy gościa Pierwsza funkcja pomocnicza, którą trzeba dodać, nosi nazwę assignGuestName() i jest odpowiedzialna za obsługę nadawania nazwy nowemu użytkownikowi. Kiedy użytkownik po raz pierwszy nawiązuje połączenie z serwerem czatu, zostaje umieszczony w pokoju Lobby. Jednocześnie następuje wywołanie funkcji assignGuestName() i przypisanie mu nazwy odróżniającej nowego użytkownika od pozostałych. Nazwa każdego gościa to w zasadzie słowo Gość, po którym znajduje się liczba o wartości zwiększanej po nawiązaniu połączenia przez każdego kolejnego użytkownika. Nazwa gościa jest przechowywana w zmiennej nickNames powiązanej z wewnętrznym identyfikatorem gniazda. Ponadto nazwa zostaje dodana do namesUsed, czyli zmiennej zawierającej użyte dotąd nazwy użytkowników. Implementacja funkcji assignGuestName() została przedstawiona w listingu 2.8, dodaj ją do pliku lib/chat_server.js. Listing 2.8. Przypisanie nazwy gościa function assignGuestName(socket, guestNumber, nickNames, namesUsed) { var name = 'Gość' + guestNumber; Wygenerowanie nowej nazwy gościa .
nickNames[socket.id] = name; Powiązanie nazwy gościa z identyfikatorem połączenia klienta. socket.emit('nameResult', { Podanie użytkownikowi wygenerowanej dla niego nazwy. success: true, name: name }); namesUsed.push(name); Zwróć uwagę na użycie nazwy gościa. return guestNumber + 1; Inkrementacja licznika używanego podczas generowania nazw gości. }
Dołączanie do pokoju Druga funkcja pomocnicza, którą trzeba dodać do pliku chat_server.js, nosi nazwę joinRoom(). Kod wymienionej funkcji przedstawiono w listingu 2.9. Jest ona odpowiedzialna za obsługę logiki związanej z dołączaniem użytkownika do pokoju czatu. Listing 2.9. Logika obsługująca dołączanie do pokoju function joinRoom(socket, room) { Dołączenie uczestnika do pokoju. socket.join(room); currentRoom[socket.id] = room; Zauważ, że użytkownik znajduje się w pokoju. socket.emit('joinResult', {room: room}); Poinformowanie uczestnika, że znajduje się we wskazanym pokoju. socket.broadcast.to(room).emit('message', { Poinformowanie pozostałych uczestników w pokoju o dołączeniu nowego. text: nickNames[socket.id] + ' dołączył do pokoju ' + room + '.' }); var usersInRoom = io.sockets.clients( room); uczestnicy znajdują się w danym pokoju.
Ustalenie, czy jeszcze inni
if (usersInRoom.length > 1) { Jeżeli w pokoju są inni uczestnicy, aplikacja wyświetla ich liczbę. var usersInRoomSummary = 'Lista użytkowników w pokoju ' + room + ': '; for (var index in usersInRoom) { var userSocketId = usersInRoom[index].id; if (userSocketId != socket.id) { if (index > 0) { usersInRoomSummary += ', '; } usersInRoomSummary += nickNames[userSocketId]; } }
usersInRoomSummary += '.'; socket.emit('message', {text: usersInRoomSummary}); Przekazanie nowemu uczestnikowi podsumowania o innych uczestnikach znajdujących się w pokoju. } }
Dzięki bibliotece Socket.IO dołączenie uczestnika do pokoju czatu jest prostą operacją i wymaga jedynie wywołania metody join obiektu socket. Następnie aplikacja podaje informacje szczegółowe uczestnikowi oraz pozostałym uczestnikom znajdującym się w tym samym pokoju. Aplikacja podaje liczbę osób znajdujących się w pokoju czatu, a także informuje pozostałych uczestników w pokoju o dołączeniu nowego.
Obsługa żądań zmiany nazwy użytkownika Jeżeli każdy uczestnik zachowa przydzieloną mu nazwę gościa, wtedy trudno będzie połapać się, kto jest kim. Dlatego też aplikacja czatu pozwala użytkownikowi na zmianę jego nazwy. Jak pokazano na rysunku 2.10, zmiana nazwy powoduje wykonanie przez przeglądarkę internetową użytkownika żądania za pomocą Socket.IO, a następnie otrzymanie odpowiedzi wskazującej na sukces lub niepowodzenie operacji.
Rysunek 2.10. Żądanie zmiany nazwy użytkownika i odpowiedź negatywna
W pliku lib/chat_server.js umieść kod przedstawiony w listingu 2.10 zawierający definicję funkcji odpowiedzialnej za obsługę żądań zmiany nazwy użytkownika. Z perspektywy aplikacji użytkownik nie może zmienić nazwy na rozpoczynającą się od słowa Gość lub będącej już w użyciu. Listing 2.10. Logika odpowiedzialna za obsługę zmiany nazwy użytkownika function handleNameChangeAttempts(socket, nickNames, namesUsed) { socket.on('nameAttempt', function(name) { Dodanie funkcji nasłuchującej zdarzeń
nameAttempt. if (name.indexOf('Gość') == 0) { Niedozwolone jest użycie nazwy rozpoczynającej się od słowa Gość. socket.emit('nameResult', { success: false, message: 'Nazwa użytkownika nie może rozpoczynać się od słowa "Gość".' }); } else { if (namesUsed.indexOf(name) == -1) { Jeżeli nazwa nie jest jeszcze zarejestrowana, wtedy należy ją zarejestrować. var previousName = nickNames[socket.id]; var previousNameIndex = namesUsed.indexOf(previousName); namesUsed.push(name); Usunięcie poprzedniej nazwy użytkownika i tym samym udostępnienie jej innym klientom. nickNames[socket.id] = name; delete namesUsed[previousNameIndex]; socket.emit('nameResult', { success: true, name: name }); socket.broadcast.to(currentRoom[socket.id]).emit('message', { text: previousName + ' zmienił nazwę na ' + name + '.' }); } else { socket.emit('nameResult', { Wygenerowanie błędu, jeśli wybrana nazwa jest już używana przez innego użytkownika. success: false, message: 'Ta nazwa jest używana przez innego użytkownika.' }); } } }); }
Wysyłanie wiadomości czatu Kiedy zadbaliśmy już o nazwy użytkowników, przechodzimy do dodania kolejnej funkcji. Odpowiada ona za obsługę sposobu wysyłania wiadomości czatu. Na rysunku 2.11 pokazano podstawę działania tego procesu: użytkownik emituje zdarzenie wskazujące pokój, do którego ma zostać wysłana wiadomość, oraz jej tekst. Następnie serwer przekazuje wiadomość do wszystkich uczestników
czatu znajdujących się w danym pokoju.
Rysunek 2.11. Wysyłanie wiadomości czatu
Poniższy kod umieść w pliku lib/chat_server.js. Do przekazywania wiadomości jest używana funkcja broadcast() biblioteki Socket.IO: function handleMessageBroadcasting(socket) { socket.on('message', function (message) { socket.broadcast.to(message.room).emit('message', { text: nickNames[socket.id] + ': ' + message.text }); }); }
Tworzenie pokoju Kolejnym krokiem jest dodanie funkcji pozwalającej użytkownikowi na dołączenie do istniejącego pokoju czatu lub utworzenie nowego. Na rysunku 2.12 pokazano interakcje zachodzące wówczas między użytkownikiem i serwerem.
Rysunek 2.12. Przejście do innego pokoju czatu
Poniższy kod umieść w pliku lib/chat_server.js, umożliwiając tym samym zmianę pokoju czatu. Zwróć uwagę na użycie metody leave() biblioteki Socket.IO: function handleRoomJoining(socket) { socket.on('join', function(room) { socket.leave(currentRoom[socket.id]); joinRoom(socket, room.newRoom); }); }
Obsługa rozłączenia użytkownika Do pliku lib/chat_server.js konieczne jest dodanie poniższego fragmentu kodu odpowiedzialnego za usunięcie nazwy użytkownika ze zmiennych nickNames i namesUsed, gdy użytkownik kończy pracę z aplikacją czatu: function handleClientDisconnection(socket) { socket.on('disconnect', function() { var nameIndex = namesUsed.indexOf(nickNames[socket.id]); delete namesUsed[nameIndex]; delete nickNames[socket.id]; }); }
W ten sposób zakończyliśmy tworzenie komponentów działających po stronie serwera. Teraz możemy powrócić do kontynuowania prac nad logiką działającą po stronie klienta.
2.5. Użycie kodu JavaScript działającego po
stronie klienta do utworzenia interfejsu użytkownika aplikacji Po dodaniu działającej po stronie serwera logiki Socket.IO do obsługi wiadomości czatu pora dodać kod JavaScript działający po stronie klienta i potrzebny do prowadzenia komunikacji z serwerem. Wspomniany kod musi zapewnić następujące możliwości: wysyłanie do serwera wiadomości oraz żądań zmiany pokoju lub nazwy użytkownika; wyświetlanie wiadomości pochodzących od innych uczestników czatu oraz listy dostępnych pokoi. Rozpoczniemy do implementacji pierwszej z wymienionych powyżej funkcji.
2.5.1. Przekazywanie do serwera wiadomości oraz żądań zmiany pokoju lub nazwy użytkownika Pierwszym fragmentem kodu JavaScript działającego po stronie klienta jest prototyp obiektu JavaScript, który będzie przetwarzał polecenia czatu, wysyłał wiadomości oraz żądania zmiany pokoju lub nazwy użytkownika. W katalogu public/javascripts utwórz plik o nazwie chat.js i umieść w nim poniższy fragment kodu. W języku JavaScript służy on do zdefiniowania „klasy” pobierającej podczas tworzenia pojedynczy argument w postaci gniazda Socket.IO: var Chat = function(socket) { this.socket = socket; };
Następnie dodaj poniższą funkcję odpowiedzialną za wysyłanie wiadomości: Chat.prototype.sendMessage = function(room, text) { var message = { room: room, text: text }; this.socket.emit('message', message); };
A teraz dodaj funkcję przeznaczoną do obsługi zmiany pokoju: Chat.prototype.changeRoom = function(room) {
this.socket.emit('join', { newRoom: room }); };
Na końcu dodaj funkcję przedstawioną w listingu 2.11 i przeznaczoną do przetwarzania poleceń czatu. Rozpoznawane są dwa polecenia: join pozwalające na dołączenie się do pokoju lub utworzenie nowego oraz nick pozwalające na zmianę nazwy użytkownika. Listing 2.11. Funkcja przetwarzająca polecenia czatu Chat.prototype.processCommand = function(command) { var words = command.split(' '); var command = words[0] .substring(1, words[0].length) .toLowerCase(); Przetworzenie polecenia z listy słów. var message = false; switch(command) { case 'join': words.shift(); var room = words.join(' '); this.changeRoom(room); Obsługa operacji zmiany pokoju lub utworzenia nowego. break; case 'nick': words.shift(); var name = words.join(' '); this.socket.emit('nameAttempt', name); Obsługa operacji zmiany nazwy użytkownika. break; default: message = 'Nieznane polecenie.'; Jeżeli polecenie nie zostanie rozpoznane, wtedy nastąpi wygenerowanie błędu. break; }; return message; };
2.5.2. Wyświetlenie w interfejsie użytkownika wiadomości i listy dostępnych pokoi
W tym punkcie zajmiemy się dodaniem logiki odpowiedzialnej za bezpośrednią współpracę z opartym na przeglądarce interfejsem użytkownika za pomocą biblioteki jQuery. Pierwsza funkcja, nad którą będziemy pracować, służy do wyświetlania danych tekstowych. Z perspektywy zapewnienia bezpieczeństwa w aplikacji sieciowej rozróżniamy dwa rodzaje danych tekstowych. Pierwszy to zaufane dane tekstowe, na które składają się dane pochodzące z aplikacji. Drugi to niezaufane dane tekstowe, które pochodzą od użytkownika lub powstały na podstawie danych podanych przez użytkownika. Dane tekstowe pochodzące od użytkownika są uznawane za niezaufane, ponieważ złośliwy użytkownik może celowo umieścić w nich z na cz niki "}; console.log(ejs.render(template, context));
Powyższy fragment kodu powoduje wygenerowanie następujących danych wyjściowych:
Jeżeli ufasz danym używanym w szablonie i nie chcesz zmieniać znaczenia znaków w wartościach kontekstu w szablonie EJS, wtedy w znaczniku szablonu można użyć <% zamiast <%=, jak przedstawiono w poniższym fragmencie kodu: var ejs = require('ejs'); var template = '<%- message %>';
var context = { message: "" }; console.log(ejs.render(template, context));
Warto w tym miejscu dodać, że jeśli nie lubisz znaków używanych przez EJS do wskazywania znaczników, możesz je zmodyfikować w następujący sposób: var ejs = require('ejs'); ejs.open = '{{:' ejs.close = '}}:' var template = '{{= message }}'; var context = {message: 'Witaj, szablonie!'}; console.log(ejs.render(template, context));
Skoro poznałeś podstawy EJS, możemy teraz przejść do innych zadań, dzięki którym zarządzanie warstwą prezentacyjną dla danych stanie się łatwiejsze.
11.2.2. Praca z danymi szablonu za pomocą filtrów EJS Silnik EJS obsługuje filtry, czyli funkcję pozwalającą na łatwe przeprowadzanie transformacji lekkich danych. W celu wskazania użycia filtru do znaczników otwierających EJS dodaje się dwukropek, na przykład: <%=: — ten znacznik otwierający jest używany wraz ze zneutralizowanymi danymi wyjściowymi EJS, w których zastosowano filtr. <%-: — ten znacznik otwierający jest używany wraz z niezmodyfikowanymi danymi wyjściowymi, w których zastosowano filtr. Filtry mogą być również łączone, co oznacza możliwość umieszczenia wielu filtrów w pojedynczym znaczniku EJS i wyświetlenia efektu zastosowania wszystkich filtrów (to rozwiązanie podobne do koncepcji „potokowania” w systemach UNIX). W kolejnych kilku podpunktach przedstawiono kilka filtrów, które są użyteczne w najczęściej spotykanych sytuacjach.
Filtry obsługujące wybór Filtry EJS są umieszczane w znacznikach EJS. Jako przykład do opisania użyteczności filtrów może posłużyć aplikacja informująca użytkowników o obejrzanych przez nich filmach. W takim przypadku najważniejszym elementem informacji może być tytuł ostatnio obejrzanego filmu. Znacznik EJS w szablonie przedstawionym w poniższym przykładzie sprawia, że wyświetlany jest tytuł
ostatniego filmu z tablicy. Odbywa się to przez użycie filtru wyświetlenie tylko ostatniego elementu tablicy:
last
powodującego
var ejs = require('ejs'); var template = '<%=: movies | last %>'; var context = {'movies': [ 'Bambi', 'Babe: świnka w mieście', 'Wkraczając w pustkę' ]}; console.log(ejs.render(template, context));
Warto pamiętać, że first to również prawidłowy filtr. Jeżeli chcesz pobrać określony element z listy, wówczas można użyć filtru get. Znacznik EJS <%=: movies | get:1 %> wyświetli drugi element tablicy movies (pierwszy element ma indeks 0). Filtru get można również użyć do wyświetlenia właściwości, jeśli wartość kontekstu jest obiektem, a nie tablicą.
Filtry przeznaczone do zmiany wielkości znaków Filtry EJS można wykorzystać także do zmiany wielkości znaków. Znacznik EJS w poniższym fragmencie kodu zawiera filtr, który spowoduje zmianę wielkości pierwszej litery w wartości kontekstu. W omawianym przykładzie nastąpi zmiana wyświetlanej wartości z bartek na Bartek: var ejs = require('ejs'); var template = '<%=: name | capitalize %>'; var context = {name: 'bartek'}; console.log(ejs.render(template, context));
Jeżeli wartość kontekstu chcesz wyświetlić w całości wielkimi literami, wtedy użyj filtru upcase. Z kolei użycie filtru downcase spowoduje wyświetlenie wartości małymi literami.
Filtry przeznaczone do pracy z tekstem Tekst może być dzielony i modyfikowany przez filtry EJS. Za ich pomocą można skracać tekst, dodawać przyrostek lub przedrostek, a nawet zastępować fragmenty tekstu. Skrócenie tekstu do określonej liczby znaków pomaga w uniknięciu długich ciągów tekstowych sprawiających problemy w układach HTML. Na przykład przedstawiony poniżej fragment kodu powoduje skrócenie tytułu do 20 znaków i wyświetlenie Królewna Śnieżka i si: var ejs = require('ejs'); var template = '<%=: title | truncate:20 %>';
var context = {title: 'Królewna Śnieżka i siedmiu krasnoludków'}; console.log(ejs.render(template, context));
Jeżeli chcesz skrócić tekst do określonej liczby słów, również możesz to zrobić za pomocą filtru EJS. W powyższym fragmencie kodu wystarczy zastąpić znacznik EJS następującym: <%=: title | truncate_words:2 %>, a wartość kontekstu zostanie skrócona do dwóch słów. Dane wyjściowe będą miały postać Królewna Śnieżka. Filtr replace używa w tle String.prototype.replace(wzorzec), a więc akceptuje ciąg tekstowy lub wyrażenie regularne. Przedstawiony poniżej fragment kodu pokazuje przykład automatycznego tworzenia skrótu słowa za pomocą filtru EJS: var ejs = require('ejs'); var template = "<%=: weight | replace:'kilogramów','kg' %>"; var context = {weight: '40 kilogramów'}; console.log(ejs.render(template, context));
Dołączenie tekstu odbywa się za pomocą filtru append, na przykład append: 'dowolny tekst'. Tekst można też poprzedzić innym za pomocą filtru prepend, na przykład prepend: 'przedrostek'.
Filtry przeprowadzające sortowanie Filtry EJS umożliwiają także sortowanie. Powracając do wcześniejszego przykładu wyświetlania tytułów filmów, filtr EJS można wykorzystać do posortowania filmów według tytułów, a następnie wyświetlić pierwszy wedle kolejności alfabetycznej, jak to zilustrowano na rysunku 11.3.
Rysunek 11.3. Schemat pokazujący użycie filtrów EJS do przetwarzania tablic tekstu
Rozwiązanie pokazane na rysunku 11.3 zostało zaimplementowane w poniższym fragmencie kodu: var ejs = require('ejs'); var template = '<%=: movies | sort | first %>'; var context = {'movies': [
'Bambi', 'Babe: świnka w mieście', 'Wkraczając w pustkę' ]}; console.log(ejs.render(template, context));
Jeżeli chcesz sortować tablicę składającą się z obiektów, ale sortowanie przeprowadzić przez porównywanie właściwości obiektu, wtedy możesz wykorzystać filtry: var ejs = require('ejs'); var template = "<%=: movies | sort_by:'name' | first | get:'name' %>"; var context = {'movies': [ {name: 'Babe: świnka w mieście'}, {name: 'Bambi'}, {name: 'Wkraczając w pustkę'} ]}; console.log(ejs.render(template, context));
Zwróć uwagę na użycie get: 'name' na końcu łańcucha filtrów. Wymieniony filtr jest używany, ponieważ wynikiem operacji sortowania jest obiekt i trzeba wskazać właściwość obiektu, która ma zostać wyświetlona.
Filtr map Filtr EJS map pozwala na wskazanie właściwości obiektu, na której mają operować kolejne filtry. W poprzednim przykładzie użyto łańcucha filtrów za pomocą map. Nie musisz wskazywać właściwości za pomocą filtru sort_by, a wyświetlanej właściwości za pomocą filtru get. Alternatywą jest użycie filtru map do utworzenia tablicy na podstawie właściwości obiektu. Tak przygotowany znacznik EJS będzie miał postać: <%=: movies | map:'name' | sort | first %>.
Tworzenie własnych filtrów Wprawdzie silnik EJS jest dostarczany z filtrami spełniającymi większość najczęściej spotykanych wymagań, jednak czasami może wystąpić konieczność wykroczenia poza możliwości oferowane przez EJS. Potrzebujesz na przykład filtru zaokrąglającego liczbę do wskazanego miejsca po przecinku dziesiętnym. Niestety nie znajdziesz wbudowanego filtru EJS służącego do tego celu. Na szczęście silnik EJS ułatwia tworzenie własnych filtrów, jak przedstawiono w listingu 11.5. Listing 11.5. Definiowanie własnych filtrów EJS var ejs = require('ejs'); var template = '<%=: price * 1.145 | round:2 %>'; var context = {price: 21};
ejs.filters.round = function(number, decimalPlaces) { Zdefiniowanie funkcji dla obiektu ejs.filters. number = isNaN(number) ? 0 : number; Pierwszy argument funkcji to dane wejściowe, kontekst lub wynik działania poprzedniego filtru. decimalPlaces = !decimalPlaces ? 0 : decimalPlaces; var multiple = Math.pow(10, decimalPlaces); return Math.round(number * multiple) / multiple; }; console.log(ejs.render(template, context));
Jak się przekonasz, filtry EJS zapewniają doskonały sposób zmniejszenia ilości logiki niezbędnej w celu przygotowania danych do wyświetlenia. Transformacji danych nie trzeba przeprowadzać ręcznie przed wygenerowaniem szablonu, ponieważ silnik EJS oferuje elegancki, wbudowany mechanizm do takiego celu.
11.2.3. Integracja EJS w aplikacji Ponieważ przechowywanie plików szablonów wraz z kodem aplikacji jest niewygodne i prowadzi do zaśmiecania kodu, wyjaśnimy teraz, jak użyć API systemu plików Node do odczytywania szablonów z oddzielnych plików. Przejdź do katalogu roboczego i utwórz plik o nazwie app.js zawierający kod przedstawiony w listingu 11.6. Listing 11.6. Przechowywanie kodu szablonu w oddzielnym pliku var ejs = require('ejs'); var fs = require('fs'); var http = require('http'); var filename = './template/students.ejs'; Zwróć uwagę na położenie pliku szablonu. var students = [ Dane przekazywane silnikowi szablonów. {name: 'Jan Kowalski', age: 23}, {name: 'Kasia Nowak', age: 25}, {name: 'Bartek Malinowski', age: 37} ]; var server = http.createServer(function(req, res) { Utworzenie serwera HTTP. if (req.url == '/') { fs.readFile(filename, function(err, data) { Odczyt szablonu z pliku. var template = data.toString(); var context = {students: students}; var output = ejs.render(template, context); Wygenerowanie szablonu. res.setHeader('Content-type', 'text/html'); res.end(output); Wysłanie odpowiedzi HTTP.
}); } else { res.statusCode = 404; res.end('Nie znaleziono'); } }); server.listen(8000);
Następnie utwórz przeznaczony do przechowywania szablonów podkatalog o na z wie template. W podkatalogu szablonów utwórz teraz plik o nazwie students.ejs, aby struktura katalogów aplikacji wyglądała jak na rysunku 11.4. W pliku szablonu students.ejs umieść kod przedstawiony w listingu 11.7.
Rysunek 11.4. Struktura katalogów tworzonej aplikacji Listing 11.7. Szablon EJS generujący tablicę studentów <% if (students.length) { %>
Buforowanie szablonów EJS Silnik EJS obsługuje opcjonalne buforowanie w pamięci funkcji szablonu. Oznacza to, że po przetworzeniu szablonu przechowywana będzie funkcja utworzona na skutek wspomnianego przetwarzania. Wygenerowanie buforowanego szablonu będzie szybsze, ponieważ można pominąć krok przetwarzania. Jeżeli przeprowadzasz początkowe prace programistyczne nad aplikacją sieciową Node i chcesz, aby wszelkie zmiany wprowadzane w plikach szablonów były odzwierciedlane natychmiast, nie włączaj buforowania. Natomiast jeśli wdrażasz aplikację w środowisku produkcyjnym, wówczas włączenie buforowania przebiega szybko i przynosi znaczne korzyści dla aplikacji. Buforowanie jest warunkowo włączane za pomocą zmiennej środowiskowej NODE_ENV.
Aby wypróbować buforowanie, wywołanie przykładzie zmień na następujące:
funkcji render() w poprzednim
var cache = process.env.NODE_ENV === 'production'; var output = ejs.render( template, {students: students, cache: cache, filename: filename} );
Pamiętaj, że opcja filename niekoniecznie musi wskazywać plik — możesz użyć unikatowej wartości identyfikującej szablon, który ma zostać wygenerowany. Skoro już wiesz, jak zintegrować silnik szablonów EJS z aplikacją Node, zobacz, jak szablony EJS wykorzystać w inny sposób, czyli w przeglądarkach internetowych.
11.2.4. Użycie EJS w aplikacjach działających po stronie klienta Wcześniej przedstawiono przykład aplikacji Node używającej EJS, teraz dowiesz się, jak używać szablonów EJS w przeglądarce internetowej. Aby użyć EJS po stronie klienta, należy najpierw pobrać silnik EJS do katalogu roboczego, co można zrobić za pomocą poniższych poleceń: cd /twój/katalog/roboczy curl https://raw.github.com/visionmedia/ejs/master/ejs.js -o ejs.js
Po pobraniu pliku ejs.js można go używać w kodzie działającym po stronie klienta. W listingu 11.8 przedstawiono prostą aplikację EJS działającą po stronie klienta. Listing 11.8. Użycie silnika EJS w celu dodania obsługi szablonów po stronie klientaPrzykład EJS Miejsce zarezerwowane na wygenerowane dane wyjściowe szablonu.
W ten sposób dowiedziałeś się, jak używać w pełni wyposażonego silnika szablonów w Node. Teraz przejdziemy do silnika Hogan, który celowo ogranicza ilość funkcji dostępnych dla kodu szablonu.
11.3. Użycie języka szablonów Mustache wraz z silnikiem Hogan Hogan.js (https://github.com/twitter/hogan.js) to silnik szablonów utworzony dla serwisu Twitter na jego potrzeby związane z szablonami. Hogan jest implementacją Mustache (http://mustache.github.io/), czyli popularnego standardu języka szablonów utworzonego przez Chrisa Wanstratha. Mustache stosuje minimalistyczne podejście w zakresie szablonów. W przeciwieństwie do EJS standard Mustache celowo nie zawiera logiki warunkowej ani żadnych wbudowanych możliwości filtrowania, poza zmianą znaczenia znaków specjalnych w treści jako sposobem na ochronę przed atakami typu XSS. Według filozofii twórcy Mustache kod szablonu powinien być maksymalnie uproszczony. W tym podrozdziale dowiesz się: Jak utworzyć i zaimplementować szablony Mustache w aplikacji. Jakie są znaczniki szablonu oferowane przez standard Mustache. Jak zorganizować szablony za pomocą „partials”. Jak dopasować Hogan do swoich potrzeb za pomocą własnych ograniczników i innych opcji. Zapoznaj się więc z alternatywnym podejściem, jakie Hogan stosuje w zakresie szablonów.
11.3.1. Tworzenie szablonu W celu użycia standardu Hogan w aplikacji lub wypróbowania przykładów przedstawionych w tym podrozdziale konieczne jest zainstalowanie silnika Hogan w katalogu aplikacji. Można to zrobić za pomocą poniższego polecenia wydanego z poziomu powłoki: npm install hogan.js
Poniżej przedstawiono prosty przykład aplikacji Node używającej Hogan do wygenerowania prostego szablonu za pomocą kontekstu. Po uruchomieniu aplikacji zostanie wyświetlony ciąg tekstowy Witaj, szablonie!: var hogan = require('hogan.js'); var template = '{{message}}'; var context = {message: 'Witaj, szablonie!'}; var template = hogan.compile(template); console.log(template.render(context));
Teraz już wiesz, jak przetwarzać szablony Mustache za pomocą silnika Hogan. Przystępujemy więc do poznania znaczników obsługiwanych przez Mustache.
11.3.2. Znaczniki Mustache Pod względem koncepcyjnym znaczniki Mustache są podobne do znaczników EJS. Znaczniki Mustache działają w charakterze miejsc zarezerwowanych dla wartości zmiennym, wskazują na potrzebę iteracji oraz pozwalają na zwiększenie funkcjonalności Mustache i dodanie komentarzy do szablonów.
Wyświetlanie prostych wartości W celu wyświetlenia wartości kontekstu w szablonie Mustache nazwę wartości należy umieścić w podwójnym nawiasie. W społeczności Mustache nawiasy są określane mianem „mustaches”. Jeżeli chcesz wyświetlić wartość elementu name kontekstu, powinieneś użyć znacznika {{name}}. Podobnie jak jest w przypadku większości silników szablonów, Hogan domyślnie neutralizuje znaki specjalne w treści, aby uniknąć ataków typu XSS. W celu wyświetlenia niezmienionej wartości w Hogan konieczne jest dodanie trzeciej pary nawiasów lub poprzedzenie znakiem ampersand nazwy elementu kontekstu. W przypadku poprzedniego przykładu elementu name jego niezmodyfikowaną wartość można wyświetlić, używając znacznika {{{name}}} lub {{&name}}. Jeżeli w szablonie Mustache chcesz umieścić komentarz, możesz użyć następującego formatu: {{! To jest komentarz }}.
Sekcje: iteracja przez wiele wartości
Wprawdzie Hogan nie pozwala na umieszczanie logiki w szablonach, ale oferuje elegancki sposób iteracji przez wiele wartości w elemencie kontekstu — za pomocą tak zwanych sekcji Mustache. Na przykład przedstawiony poniżej kontekst zawiera element wraz z tablicą wartości: var context = { students: [ { name: 'Jan Kowalski', age: 21 }, { name: 'Roman Nowak', age: 26 } ] };
Jeżeli chcesz utworzyć szablon wyświetlający każdego studenta w oddzielnym akapicie HTML i generujący dane wyjściowe podobne do poniższych, takie zadanie możesz łatwo wykonać za pomocą szablonu Hogan:
Przedstawiony poniżej szablon wygeneruje oczekiwany kod HTML: {{#students}}
Sekcje odwrócone: domyślny kod HTML, gdy wartość nie istnieje A gdyby wartość elementu students w danych kontekstu nie była tablicą? Jeżeli wartość będzie na przykład pojedynczym obiektem, wtedy szablon ją wyświetli. Sekcje nie zostaną jednak wyświetlone, jeśli wartość elementu jest niezdefiniowana, wynosi false lub stanowi pustą tablicę. Jeżeli szablon ma wygenerować komunikat informujący o nieistniejącej wartości dla sekcji, wtedy można wykorzystać oferowane przez Hogan tak zwane sekcje odwrócone. Po dodaniu poniższego fragmentu kodu do poprzedniego szablonu w przypadku braku studenta w danych kontekstu nastąpi wyświetlenie odpowiedniego komunikatu: {{^students}}
Sekcja lambda: własna funkcjonalność w blokach sekcji Aby umożliwić programistom zwiększenie funkcjonalności Mustache, standard pozwala na definiowanie znaczników sekcji przetwarzających treść szablonu za
pomocą wywołania funkcji, a nie iteracji przez rozwiązanie jest nazywane sekcją lambda.
tablice.
Tego rodzaju
W listingu 11.9 przedstawiono przykład użycia sekcji lambda. Kod powoduje dodanie obsługi składni Markdown podczas generowania szablonu. Zwróć uwagę, że w przykładzie użyto modułu github-flavored-markdown, który trzeba zainstalować przez wydanie z poziomu powłoki polecenia npm install githubflavored-markdown. Listing 11.9. Użycie funkcji lambda w szablonie Hogan var hogan = require('hogan.js'); var md = require('github-flavored-markdown'); Wymagany jest analizator składni Markdown. var template = '{{#markdown}}' Szablon Mustache zawiera także składnię Markdown. + '**Imię i nazwisko**: {{name}}' + '{{/markdown}}'; var context = { name: 'Roman Nowak', markdown: function() { return function(text) { Kontekst szablonu zawiera sekcję lambda przeznaczoną do przetwarzania składni Markdown w szablonie. return md.parse(text); }; } }; var template = hogan.compile(template); console.log(template.render(context));
W listingu 11.9 kod **Imię
w szablonie zostanie wygenerowany jako Imię i nazwisko po przetworzeniu przez analizator Markdown wywoływany przez logikę sekcji lambda. i nazwisko**
Sekcje lambda pozwalają na łatwą implementację w szablonach rozwiązań takich jak buforowanie i mechanizmy translacji.
Partials: wielokrotne użycie szablonów w innych szablonach Podczas tworzenia szablonów zwykle chcesz uniknąć niepotrzebnego powtarzania tego samego kodu w wielu szablonach. Jednym z możliwych rozwiązań jest utworzenie tak zwanych partials. To szablony używane jako bloki budulcowe i umieszczane w innych szablonach. Kolejnym zastosowaniem dla partials jest podział skomplikowanego szablonu na prostsze szablony. W listingu 11.10 przedstawiono przykład użycia partials do oddzielenia od głównego szablonu kodu szablonu wykorzystywanego do wyświetlania danych
studentów. Listing 11.10. Użycie partials w szablonie Hogan var hogan = require('hogan.js'); Kod szablonu używanego jako partials. var studentTemplate = '
11.3.3. Dostosowanie szablonu Hogan do własnych potrzeb Szablony Hogan są proste w użyciu, więc po poznaniu składni znaczników powinieneś móc bez problemów z nich korzystać. Podczas pracy może wystąpić potrzeba wprowadzenia kilku zmian. Jeżeli nie lubisz nawiasów w stylu Mustache, wówczas możesz zmienić ograniczniki stosowane w szablonach Hogan. Odbywa się to przez przekazanie metodz ie compile() opcji pozwalającej na zmianę ograniczników. Poniższy przykład pokazuje, jak skompilować Hogan wraz z obsługą ograniczników w stylu EJS: hogan.compile(text, {delimiters: '<% %>'});
Jeżeli chcesz użyć znaczników sekcji nierozpoczynających się od znaku # po nawiasie otwierającym, wtedy możesz użyć innej opcji metody compile() — o nazwie sectionTags. Na przykład inny format znaczników można zastosować dla
znaczników sesji wykorzystujących funkcje lambda. W listingu 11.11 przedstawiono zmodyfikowany przykład z listingu 11.9. Nowa wersja używa znaku podkreślenia w celu odróżnienia znacznika sekcji markdown od kolejnych znaczników sekcji, które przeprowadzają iterację, a nie korzystają z funkcji lambda. Listing 11.11. Użycie własnych znaczników sekcji w szablonie Hogan var hogan = require('hogan.js'); var md = require('github-flavored-markdown'); Markdown.
Wymagany jest analizator składni
var template = '{{_markdown}}' Własny znacznik użyty w szablonie. + '**Imię i nazwisko**: {{name}}' + '{{/markdown}}'; var context = { name: 'Roman Nowak', _markdown: function(text) { Funkcja lambda dla własnego znacznika. return md.parse(text); } }; var template = hogan.compile( template, Zdefiniowanie własnych znaczników otwierających i zamykających. {sectionTags: [{o: '_markdown', c: 'markdown'}]} ); console.log(template.render(context));
Podczas użycia szablonów Hogan nie musisz zmieniać żadnych opcji w celu włączenia buforowania. Wspomniane buforowanie jest wbudowane w funkcję compile() i włączone domyślnie. W ten sposób poznałeś dwa całkiem proste silniki szablonów dostępne dla Node. Teraz przechodzimy do silnika szablonów Jade, który w zupełnie inny sposób niż EJS i Hogan obsługuje znaczniki prezentacyjne.
11.4. Szablony Jade Jade (http://jade-lang.com/) oferuje alternatywny sposób określania kodu HTML. Podstawowa różnica między Jade i większością głównych silników szablonów to użycie wcięć, które mają znaczenie. Podczas tworzenia szablonu w Jade stosowane są wcięcia wskazujące zagnieżdżanie znaczników HTML. Znaczniki HTML nie muszą być wyraźnie zamykane, co eliminuje problem przypadkowego, przedwczesnego zamknięcia
znacznika lub pozostawienia niezamkniętego znacznika. Stosowanie wcięć powoduje również, że szablony są wizualnie bardziej przejrzyste i łatwiejsze w obsłudze. Aby przekonać się, jak działają szablony Jade, najpierw spójrz na poniższy kod HTML, który za chwilę wygenerujemy za pomocą szablonu:Witamy
Powyższy kod HTML można przedstawić za pomocą następującego szablonu Jade: html head title Witamy body div.content#main strong "Witaj, świecie!"
Jade, podobnie jak EJS, pozwala na osadzanie kodu JavaScript, a szablony Jade mogą być używane po stronie zarówno serwera, jak i klienta. Jade oferuje także funkcje dodatkowe, jak obsługa dziedziczenia szablonów i polecenie mixin. Wspomniane polecenie mixin pozwala na definiowanie wielokrotnego użytku miniszablonów przedstawiających kod HTML dla najczęściej występujących elementów, takich jak listy i pola wyboru. Pod względem koncepcji mixins przypomina partials w Hogan.js, czyli w omówionym w poprzednim podrozdziale silniku szablonów. Dziedziczenie szablonów ułatwia organizację w wielu plikach szablonów Jade niezbędnych do wygenerowania pojedynczej strony HTML. Więcej informacji na ten temat znajdziesz w dalszej części rozdziału. Instalacja Jade w katalogu aplikacji Node wymaga wydania poniższego polecenia z poziomu powłoki: npm install jade
Instalacja Jade wraz z opcją globalną -g również jest użyteczna, ponieważ daje dostęp do polecenia jade pozwalającego na szybkie wygenerowanie szablonu do
postaci kodu HTML. Przedstawione poniżej polecenie spowoduje, że plik template/sidebar.jade zostanie wygenerowany jako sidebar.html w katalogu template. Polecenie jade pozwala na łatwe eksperymentowanie ze składnią Jade: jade template/sidebar.jade
W tym punkcie: Poznasz podstawy Jade, takie jak wskazywanie nazw klas, atrybutów i rozwinięcie bloków. Dowiesz się, jak dodać logikę do szablonu Jade za pomocą wbudowanych słów kluczowych. Dowiesz się, jak zorganizować szablony za pomocą dziedziczenia, bloków i poleceń mixin. Na początek zajmiemy się składnią i podstawowym sposobem użycia Jade.
11.4.1. Podstawy szablonów Jade Jade używa takich samych nazw znaczników jak HTML, ale pozwala na pominięcie znaków < i > — zamiast nich można zastosować wcięcia oznaczające zagnieżdżanie znaczników. Znacznik może mieć przypisaną jedną lub więcej klas CSS przez dodanie .. Element
Po dodaniu plików HTML i CSS możesz uruchomić aplikację w przeglądarce internetowej. Na obecnym etapie prac powinna wyglądać jak na rysunku 2.9. Aplikacja oczywiście nie oferuje jeszcze pełnej funkcjonalności, ale pliki
statyczne są udostępniane, a podstawowy układ graficzny prawidłowo generowany. Przechodzimy więc teraz do przygotowania kodu działającego po stronie serwera i odpowiedzialnego za obsługę wiadomości.
2.4. Obsługa wiadomości czatu za pomocą biblioteki Socket.IO Z trzech wymagań stawianych budowanym aplikacjom omówiliśmy dotąd pierwsze, czyli udostępnianie plików statycznych. Przechodzimy teraz do drugiego — obsługi komunikacji między przeglądarką internetową i serwerem. Nowoczesne przeglądarki internetowe mają możliwość użycia technologii WebSocket do obsługi komunikacji między przeglądarką i serwerem. (Dokładne informacje dotyczące obsługi WebSocket w przeglądarkach internetowych znajdziesz na stronie http://socket.io/#browser-support).
Rysunek 2.9. Aplikacja czatu na obecnym etapie prac
WebSocket zapewnia warstwę abstrakcji dla siebie oraz dla innych mechanizmów transportu, zarówno dla Node, jak i kodu JavaScript działającego po stronie klienta. Biblioteka Socket.IO automatycznie zapewnia rozwiązania awaryjne, jeśli obsługa WebSocket nie została zaimplementowana w
przeglądarce internetowej. We wszystkich przypadkach używane jest to samo API. W tym podrozdziale: pokrótce poznasz bibliotekę Socket.IO oraz zdefiniujesz funkcje Socket.IO niezbędne po stronie serwera; dodasz kod odpowiedzialny za konfigurację serwera Socket.IO; dodasz kod odpowiedzialny za obsługę różnych zdarzeń czatu. Biblioteka Socket.IO standardowo oferuje wirtualne kanały, więc zamiast rozgłaszać każdą wiadomość do wszystkich połączonych użytkowników, można ją przekazać jedynie do tych, którzy są subskrybentami danego kanału. Dzięki tej funkcji implementacja pokoi czatu w budowanej tutaj aplikacji staje się naprawdę łatwym zadaniem, o czym się wkrótce przekonasz. Biblioteka Socket.IO to również doskonały przykład użyteczności emiterów zdarzeń. Wspomniany emiter zdarzeń to w zasadzie użyteczny wzorzec organizacji logiki asynchronicznej. W tym rozdziale poznasz kod pewnych emiterów zdarzeń, ale tym tematem dokładnie zajmiemy się w następnym rozdziale. Emiter zdarzeń Pod względem koncepcji emiter zdarzeń jest powiązany z pewnego rodzaju zasobem, może wysyłać i otrzymywać wiadomości do oraz z zasobu. Wspomnianym zasobem może być połączenie ze zdalnym serwerem lub coś znacznie bardziej abstrakcyjnego, na przykład postać w grze. Projekt Johnny-Five (https://github.com/rwaldron/johnny-five) wykorzystuje Node w aplikacjach robotów i używa emiterów zdarzeń do kontrolowania mikrokontrolerów Arduino.
W pierwszej kolejności trzeba uruchomić serwer i przygotować logikę odpowiedzialną za nawiązywanie połączenia. Następnie przystąpimy do zdefiniowania funkcji wymaganych po stronie serwera.
2.4.1. Konfiguracja serwera Socket.IO Na początku w pliku server.js należy umieścić dwa podane poniżej wiersze kodu. Pierwszy powoduje wczytanie funkcji ze wskazanego modułu Node dostarczającego logikę potrzebną do obsługi po stronie serwera funkcji czatu związanych z biblioteką Socket.IO. Wskazany moduł zdefiniujemy za chwilę. Natomiast drugi wiersz uruchamia serwer i dostarcza funkcje Socket.IO przy założeniu, że mamy już zdefiniowany serwer HTTP, co pozwala na współdzielenie tego samego portu TCP/IP: var chatServer = require('./lib/chat_server'); chatServer.listen(server);
Teraz trzeba utworzyć plik chat_server.js w podkatalogu lib. Na początku w
wymienionym pliku umieść poniższe deklaracje zmiennych. Te deklaracje pozwolą na użycie biblioteki Socket.IO, a także inicjalizują kilka zmiennych przeznaczonych do definiowania stanu czatu: var socketio = require('socket.io'); var io; var guestNumber = 1; var nickNames = {}; var namesUsed = []; var currentRoom = {};
Utworzenie logiki połączenia Kolejnym krokiem jest dodanie przedstawionej w listingu 2.7 logiki odpowiedzialnej za zdefiniowanie funkcji listen serwera czatu. Wymieniona funkcja jest wywoływana w pliku server.js. Powoduje uruchomienie serwera Socket.IO, ogranicza ilość danych wyświetlanych w konsoli przez bibliotekę Socket.IO, a także definiuje sposób obsługi wszystkich połączeń przychodzących. Listing 2.7. Logika odpowiedzialna za uruchomienie serwera Socket.IO exports.listen = function(server) { io = socketio.listen(server); Uruchomienie serwera Socket.IO i umożliwienie mu współpracy z istniejącym serwerem HTTP. io.set('log level', 1); io.sockets.on('connection', function (socket) { Zdefiniowanie sposobu obsługi połączenia użytkownika. guestNumber = assignGuestName(socket, guestNumber, nickNames, namesUsed); Przypisanie użytkownikowi nazwy gościa podczas nawiązywania połączenia. joinRoom(socket, 'Lobby'); Umieszczenie użytkownika w pokoju Lobby, gdy próbuje on nawiązać połączenie. handleMessageBroadcasting(socket, nickNames); Obsługa wiadomości użytkownika, prób zmiany nazwy użytkownika, a także tworzenia lub zmiany pokoju czatu. handleNameChangeAttempts(socket, nickNames, namesUsed); handleRoomJoining(socket); socket.on('rooms', function() { Wyświetlenie użytkownika wraz z listą pokoi, w których prowadzi czat. socket.emit('rooms', io.sockets.manager.rooms); }); handleClientDisconnection(socket, nickNames, namesUsed); Zdefiniowanie logiki wykonywanej podczas rozłączania użytkownika. }); };
Jak możesz zauważyć, logika obsługi połączenia wywołuje wiele funkcji
pomocniczych, które teraz trzeba będzie zdefiniować w pliku chat_server.js. Po przygotowaniu logiki odpowiedzialnej za nawiązywanie połączenia trzeba zdefiniować kilka funkcji pomocniczych, które obsługują inne funkcjonalności oferowane przez aplikację.
2.4.2. Obsługa zdarzeń oraz scenariuszy w aplikacji Aplikacja czatu musi zapewnić obsługę wymienionych poniżej zdarzeń i rodzajów scenariuszy: przypisanie nazwy gościa, żądanie zmiany pokoju, żądanie zmiany nazwy użytkownika, wysyłanie wiadomości czatu, tworzenie pokoju, rozłączanie użytkownika. Do obsługi wymienionych funkcji dodatkowych funkcji pomocniczych.
konieczne
jest
przygotowanie
kilku
Przypisanie nazwy gościa Pierwsza funkcja pomocnicza, którą trzeba dodać, nosi nazwę assignGuestName() i jest odpowiedzialna za obsługę nadawania nazwy nowemu użytkownikowi. Kiedy użytkownik po raz pierwszy nawiązuje połączenie z serwerem czatu, zostaje umieszczony w pokoju Lobby. Jednocześnie następuje wywołanie funkcji assignGuestName() i przypisanie mu nazwy odróżniającej nowego użytkownika od pozostałych. Nazwa każdego gościa to w zasadzie słowo Gość, po którym znajduje się liczba o wartości zwiększanej po nawiązaniu połączenia przez każdego kolejnego użytkownika. Nazwa gościa jest przechowywana w zmiennej nickNames powiązanej z wewnętrznym identyfikatorem gniazda. Ponadto nazwa zostaje dodana do namesUsed, czyli zmiennej zawierającej użyte dotąd nazwy użytkowników. Implementacja funkcji assignGuestName() została przedstawiona w listingu 2.8, dodaj ją do pliku lib/chat_server.js. Listing 2.8. Przypisanie nazwy gościa function assignGuestName(socket, guestNumber, nickNames, namesUsed) { var name = 'Gość' + guestNumber; Wygenerowanie nowej nazwy gościa .
nickNames[socket.id] = name; Powiązanie nazwy gościa z identyfikatorem połączenia klienta. socket.emit('nameResult', { Podanie użytkownikowi wygenerowanej dla niego nazwy. success: true, name: name }); namesUsed.push(name); Zwróć uwagę na użycie nazwy gościa. return guestNumber + 1; Inkrementacja licznika używanego podczas generowania nazw gości. }
Dołączanie do pokoju Druga funkcja pomocnicza, którą trzeba dodać do pliku chat_server.js, nosi nazwę joinRoom(). Kod wymienionej funkcji przedstawiono w listingu 2.9. Jest ona odpowiedzialna za obsługę logiki związanej z dołączaniem użytkownika do pokoju czatu. Listing 2.9. Logika obsługująca dołączanie do pokoju function joinRoom(socket, room) { Dołączenie uczestnika do pokoju. socket.join(room); currentRoom[socket.id] = room; Zauważ, że użytkownik znajduje się w pokoju. socket.emit('joinResult', {room: room}); Poinformowanie uczestnika, że znajduje się we wskazanym pokoju. socket.broadcast.to(room).emit('message', { Poinformowanie pozostałych uczestników w pokoju o dołączeniu nowego. text: nickNames[socket.id] + ' dołączył do pokoju ' + room + '.' }); var usersInRoom = io.sockets.clients( room); uczestnicy znajdują się w danym pokoju.
Ustalenie, czy jeszcze inni
if (usersInRoom.length > 1) { Jeżeli w pokoju są inni uczestnicy, aplikacja wyświetla ich liczbę. var usersInRoomSummary = 'Lista użytkowników w pokoju ' + room + ': '; for (var index in usersInRoom) { var userSocketId = usersInRoom[index].id; if (userSocketId != socket.id) { if (index > 0) { usersInRoomSummary += ', '; } usersInRoomSummary += nickNames[userSocketId]; } }
usersInRoomSummary += '.'; socket.emit('message', {text: usersInRoomSummary}); Przekazanie nowemu uczestnikowi podsumowania o innych uczestnikach znajdujących się w pokoju. } }
Dzięki bibliotece Socket.IO dołączenie uczestnika do pokoju czatu jest prostą operacją i wymaga jedynie wywołania metody join obiektu socket. Następnie aplikacja podaje informacje szczegółowe uczestnikowi oraz pozostałym uczestnikom znajdującym się w tym samym pokoju. Aplikacja podaje liczbę osób znajdujących się w pokoju czatu, a także informuje pozostałych uczestników w pokoju o dołączeniu nowego.
Obsługa żądań zmiany nazwy użytkownika Jeżeli każdy uczestnik zachowa przydzieloną mu nazwę gościa, wtedy trudno będzie połapać się, kto jest kim. Dlatego też aplikacja czatu pozwala użytkownikowi na zmianę jego nazwy. Jak pokazano na rysunku 2.10, zmiana nazwy powoduje wykonanie przez przeglądarkę internetową użytkownika żądania za pomocą Socket.IO, a następnie otrzymanie odpowiedzi wskazującej na sukces lub niepowodzenie operacji.
Rysunek 2.10. Żądanie zmiany nazwy użytkownika i odpowiedź negatywna
W pliku lib/chat_server.js umieść kod przedstawiony w listingu 2.10 zawierający definicję funkcji odpowiedzialnej za obsługę żądań zmiany nazwy użytkownika. Z perspektywy aplikacji użytkownik nie może zmienić nazwy na rozpoczynającą się od słowa Gość lub będącej już w użyciu. Listing 2.10. Logika odpowiedzialna za obsługę zmiany nazwy użytkownika function handleNameChangeAttempts(socket, nickNames, namesUsed) { socket.on('nameAttempt', function(name) { Dodanie funkcji nasłuchującej zdarzeń
nameAttempt. if (name.indexOf('Gość') == 0) { Niedozwolone jest użycie nazwy rozpoczynającej się od słowa Gość. socket.emit('nameResult', { success: false, message: 'Nazwa użytkownika nie może rozpoczynać się od słowa "Gość".' }); } else { if (namesUsed.indexOf(name) == -1) { Jeżeli nazwa nie jest jeszcze zarejestrowana, wtedy należy ją zarejestrować. var previousName = nickNames[socket.id]; var previousNameIndex = namesUsed.indexOf(previousName); namesUsed.push(name); Usunięcie poprzedniej nazwy użytkownika i tym samym udostępnienie jej innym klientom. nickNames[socket.id] = name; delete namesUsed[previousNameIndex]; socket.emit('nameResult', { success: true, name: name }); socket.broadcast.to(currentRoom[socket.id]).emit('message', { text: previousName + ' zmienił nazwę na ' + name + '.' }); } else { socket.emit('nameResult', { Wygenerowanie błędu, jeśli wybrana nazwa jest już używana przez innego użytkownika. success: false, message: 'Ta nazwa jest używana przez innego użytkownika.' }); } } }); }
Wysyłanie wiadomości czatu Kiedy zadbaliśmy już o nazwy użytkowników, przechodzimy do dodania kolejnej funkcji. Odpowiada ona za obsługę sposobu wysyłania wiadomości czatu. Na rysunku 2.11 pokazano podstawę działania tego procesu: użytkownik emituje zdarzenie wskazujące pokój, do którego ma zostać wysłana wiadomość, oraz jej tekst. Następnie serwer przekazuje wiadomość do wszystkich uczestników
czatu znajdujących się w danym pokoju.
Rysunek 2.11. Wysyłanie wiadomości czatu
Poniższy kod umieść w pliku lib/chat_server.js. Do przekazywania wiadomości jest używana funkcja broadcast() biblioteki Socket.IO: function handleMessageBroadcasting(socket) { socket.on('message', function (message) { socket.broadcast.to(message.room).emit('message', { text: nickNames[socket.id] + ': ' + message.text }); }); }
Tworzenie pokoju Kolejnym krokiem jest dodanie funkcji pozwalającej użytkownikowi na dołączenie do istniejącego pokoju czatu lub utworzenie nowego. Na rysunku 2.12 pokazano interakcje zachodzące wówczas między użytkownikiem i serwerem.
Rysunek 2.12. Przejście do innego pokoju czatu
Poniższy kod umieść w pliku lib/chat_server.js, umożliwiając tym samym zmianę pokoju czatu. Zwróć uwagę na użycie metody leave() biblioteki Socket.IO: function handleRoomJoining(socket) { socket.on('join', function(room) { socket.leave(currentRoom[socket.id]); joinRoom(socket, room.newRoom); }); }
Obsługa rozłączenia użytkownika Do pliku lib/chat_server.js konieczne jest dodanie poniższego fragmentu kodu odpowiedzialnego za usunięcie nazwy użytkownika ze zmiennych nickNames i namesUsed, gdy użytkownik kończy pracę z aplikacją czatu: function handleClientDisconnection(socket) { socket.on('disconnect', function() { var nameIndex = namesUsed.indexOf(nickNames[socket.id]); delete namesUsed[nameIndex]; delete nickNames[socket.id]; }); }
W ten sposób zakończyliśmy tworzenie komponentów działających po stronie serwera. Teraz możemy powrócić do kontynuowania prac nad logiką działającą po stronie klienta.
2.5. Użycie kodu JavaScript działającego po
stronie klienta do utworzenia interfejsu użytkownika aplikacji Po dodaniu działającej po stronie serwera logiki Socket.IO do obsługi wiadomości czatu pora dodać kod JavaScript działający po stronie klienta i potrzebny do prowadzenia komunikacji z serwerem. Wspomniany kod musi zapewnić następujące możliwości: wysyłanie do serwera wiadomości oraz żądań zmiany pokoju lub nazwy użytkownika; wyświetlanie wiadomości pochodzących od innych uczestników czatu oraz listy dostępnych pokoi. Rozpoczniemy do implementacji pierwszej z wymienionych powyżej funkcji.
2.5.1. Przekazywanie do serwera wiadomości oraz żądań zmiany pokoju lub nazwy użytkownika Pierwszym fragmentem kodu JavaScript działającego po stronie klienta jest prototyp obiektu JavaScript, który będzie przetwarzał polecenia czatu, wysyłał wiadomości oraz żądania zmiany pokoju lub nazwy użytkownika. W katalogu public/javascripts utwórz plik o nazwie chat.js i umieść w nim poniższy fragment kodu. W języku JavaScript służy on do zdefiniowania „klasy” pobierającej podczas tworzenia pojedynczy argument w postaci gniazda Socket.IO: var Chat = function(socket) { this.socket = socket; };
Następnie dodaj poniższą funkcję odpowiedzialną za wysyłanie wiadomości: Chat.prototype.sendMessage = function(room, text) { var message = { room: room, text: text }; this.socket.emit('message', message); };
A teraz dodaj funkcję przeznaczoną do obsługi zmiany pokoju: Chat.prototype.changeRoom = function(room) {
this.socket.emit('join', { newRoom: room }); };
Na końcu dodaj funkcję przedstawioną w listingu 2.11 i przeznaczoną do przetwarzania poleceń czatu. Rozpoznawane są dwa polecenia: join pozwalające na dołączenie się do pokoju lub utworzenie nowego oraz nick pozwalające na zmianę nazwy użytkownika. Listing 2.11. Funkcja przetwarzająca polecenia czatu Chat.prototype.processCommand = function(command) { var words = command.split(' '); var command = words[0] .substring(1, words[0].length) .toLowerCase(); Przetworzenie polecenia z listy słów. var message = false; switch(command) { case 'join': words.shift(); var room = words.join(' '); this.changeRoom(room); Obsługa operacji zmiany pokoju lub utworzenia nowego. break; case 'nick': words.shift(); var name = words.join(' '); this.socket.emit('nameAttempt', name); Obsługa operacji zmiany nazwy użytkownika. break; default: message = 'Nieznane polecenie.'; Jeżeli polecenie nie zostanie rozpoznane, wtedy nastąpi wygenerowanie błędu. break; }; return message; };
2.5.2. Wyświetlenie w interfejsie użytkownika wiadomości i listy dostępnych pokoi
W tym punkcie zajmiemy się dodaniem logiki odpowiedzialnej za bezpośrednią współpracę z opartym na przeglądarce interfejsem użytkownika za pomocą biblioteki jQuery. Pierwsza funkcja, nad którą będziemy pracować, służy do wyświetlania danych tekstowych. Z perspektywy zapewnienia bezpieczeństwa w aplikacji sieciowej rozróżniamy dwa rodzaje danych tekstowych. Pierwszy to zaufane dane tekstowe, na które składają się dane pochodzące z aplikacji. Drugi to niezaufane dane tekstowe, które pochodzą od użytkownika lub powstały na podstawie danych podanych przez użytkownika. Dane tekstowe pochodzące od użytkownika są uznawane za niezaufane, ponieważ złośliwy użytkownik może celowo umieścić w nich z na cz niki "}; console.log(ejs.render(template, context));
Powyższy fragment kodu powoduje wygenerowanie następujących danych wyjściowych:
Jeżeli ufasz danym używanym w szablonie i nie chcesz zmieniać znaczenia znaków w wartościach kontekstu w szablonie EJS, wtedy w znaczniku szablonu można użyć <% zamiast <%=, jak przedstawiono w poniższym fragmencie kodu: var ejs = require('ejs'); var template = '<%- message %>';
var context = { message: "" }; console.log(ejs.render(template, context));
Warto w tym miejscu dodać, że jeśli nie lubisz znaków używanych przez EJS do wskazywania znaczników, możesz je zmodyfikować w następujący sposób: var ejs = require('ejs'); ejs.open = '{{:' ejs.close = '}}:' var template = '{{= message }}'; var context = {message: 'Witaj, szablonie!'}; console.log(ejs.render(template, context));
Skoro poznałeś podstawy EJS, możemy teraz przejść do innych zadań, dzięki którym zarządzanie warstwą prezentacyjną dla danych stanie się łatwiejsze.
11.2.2. Praca z danymi szablonu za pomocą filtrów EJS Silnik EJS obsługuje filtry, czyli funkcję pozwalającą na łatwe przeprowadzanie transformacji lekkich danych. W celu wskazania użycia filtru do znaczników otwierających EJS dodaje się dwukropek, na przykład: <%=: — ten znacznik otwierający jest używany wraz ze zneutralizowanymi danymi wyjściowymi EJS, w których zastosowano filtr. <%-: — ten znacznik otwierający jest używany wraz z niezmodyfikowanymi danymi wyjściowymi, w których zastosowano filtr. Filtry mogą być również łączone, co oznacza możliwość umieszczenia wielu filtrów w pojedynczym znaczniku EJS i wyświetlenia efektu zastosowania wszystkich filtrów (to rozwiązanie podobne do koncepcji „potokowania” w systemach UNIX). W kolejnych kilku podpunktach przedstawiono kilka filtrów, które są użyteczne w najczęściej spotykanych sytuacjach.
Filtry obsługujące wybór Filtry EJS są umieszczane w znacznikach EJS. Jako przykład do opisania użyteczności filtrów może posłużyć aplikacja informująca użytkowników o obejrzanych przez nich filmach. W takim przypadku najważniejszym elementem informacji może być tytuł ostatnio obejrzanego filmu. Znacznik EJS w szablonie przedstawionym w poniższym przykładzie sprawia, że wyświetlany jest tytuł
ostatniego filmu z tablicy. Odbywa się to przez użycie filtru wyświetlenie tylko ostatniego elementu tablicy:
last
powodującego
var ejs = require('ejs'); var template = '<%=: movies | last %>'; var context = {'movies': [ 'Bambi', 'Babe: świnka w mieście', 'Wkraczając w pustkę' ]}; console.log(ejs.render(template, context));
Warto pamiętać, że first to również prawidłowy filtr. Jeżeli chcesz pobrać określony element z listy, wówczas można użyć filtru get. Znacznik EJS <%=: movies | get:1 %> wyświetli drugi element tablicy movies (pierwszy element ma indeks 0). Filtru get można również użyć do wyświetlenia właściwości, jeśli wartość kontekstu jest obiektem, a nie tablicą.
Filtry przeznaczone do zmiany wielkości znaków Filtry EJS można wykorzystać także do zmiany wielkości znaków. Znacznik EJS w poniższym fragmencie kodu zawiera filtr, który spowoduje zmianę wielkości pierwszej litery w wartości kontekstu. W omawianym przykładzie nastąpi zmiana wyświetlanej wartości z bartek na Bartek: var ejs = require('ejs'); var template = '<%=: name | capitalize %>'; var context = {name: 'bartek'}; console.log(ejs.render(template, context));
Jeżeli wartość kontekstu chcesz wyświetlić w całości wielkimi literami, wtedy użyj filtru upcase. Z kolei użycie filtru downcase spowoduje wyświetlenie wartości małymi literami.
Filtry przeznaczone do pracy z tekstem Tekst może być dzielony i modyfikowany przez filtry EJS. Za ich pomocą można skracać tekst, dodawać przyrostek lub przedrostek, a nawet zastępować fragmenty tekstu. Skrócenie tekstu do określonej liczby znaków pomaga w uniknięciu długich ciągów tekstowych sprawiających problemy w układach HTML. Na przykład przedstawiony poniżej fragment kodu powoduje skrócenie tytułu do 20 znaków i wyświetlenie Królewna Śnieżka i si: var ejs = require('ejs'); var template = '<%=: title | truncate:20 %>';
var context = {title: 'Królewna Śnieżka i siedmiu krasnoludków'}; console.log(ejs.render(template, context));
Jeżeli chcesz skrócić tekst do określonej liczby słów, również możesz to zrobić za pomocą filtru EJS. W powyższym fragmencie kodu wystarczy zastąpić znacznik EJS następującym: <%=: title | truncate_words:2 %>, a wartość kontekstu zostanie skrócona do dwóch słów. Dane wyjściowe będą miały postać Królewna Śnieżka. Filtr replace używa w tle String.prototype.replace(wzorzec), a więc akceptuje ciąg tekstowy lub wyrażenie regularne. Przedstawiony poniżej fragment kodu pokazuje przykład automatycznego tworzenia skrótu słowa za pomocą filtru EJS: var ejs = require('ejs'); var template = "<%=: weight | replace:'kilogramów','kg' %>"; var context = {weight: '40 kilogramów'}; console.log(ejs.render(template, context));
Dołączenie tekstu odbywa się za pomocą filtru append, na przykład append: 'dowolny tekst'. Tekst można też poprzedzić innym za pomocą filtru prepend, na przykład prepend: 'przedrostek'.
Filtry przeprowadzające sortowanie Filtry EJS umożliwiają także sortowanie. Powracając do wcześniejszego przykładu wyświetlania tytułów filmów, filtr EJS można wykorzystać do posortowania filmów według tytułów, a następnie wyświetlić pierwszy wedle kolejności alfabetycznej, jak to zilustrowano na rysunku 11.3.
Rysunek 11.3. Schemat pokazujący użycie filtrów EJS do przetwarzania tablic tekstu
Rozwiązanie pokazane na rysunku 11.3 zostało zaimplementowane w poniższym fragmencie kodu: var ejs = require('ejs'); var template = '<%=: movies | sort | first %>'; var context = {'movies': [
'Bambi', 'Babe: świnka w mieście', 'Wkraczając w pustkę' ]}; console.log(ejs.render(template, context));
Jeżeli chcesz sortować tablicę składającą się z obiektów, ale sortowanie przeprowadzić przez porównywanie właściwości obiektu, wtedy możesz wykorzystać filtry: var ejs = require('ejs'); var template = "<%=: movies | sort_by:'name' | first | get:'name' %>"; var context = {'movies': [ {name: 'Babe: świnka w mieście'}, {name: 'Bambi'}, {name: 'Wkraczając w pustkę'} ]}; console.log(ejs.render(template, context));
Zwróć uwagę na użycie get: 'name' na końcu łańcucha filtrów. Wymieniony filtr jest używany, ponieważ wynikiem operacji sortowania jest obiekt i trzeba wskazać właściwość obiektu, która ma zostać wyświetlona.
Filtr map Filtr EJS map pozwala na wskazanie właściwości obiektu, na której mają operować kolejne filtry. W poprzednim przykładzie użyto łańcucha filtrów za pomocą map. Nie musisz wskazywać właściwości za pomocą filtru sort_by, a wyświetlanej właściwości za pomocą filtru get. Alternatywą jest użycie filtru map do utworzenia tablicy na podstawie właściwości obiektu. Tak przygotowany znacznik EJS będzie miał postać: <%=: movies | map:'name' | sort | first %>.
Tworzenie własnych filtrów Wprawdzie silnik EJS jest dostarczany z filtrami spełniającymi większość najczęściej spotykanych wymagań, jednak czasami może wystąpić konieczność wykroczenia poza możliwości oferowane przez EJS. Potrzebujesz na przykład filtru zaokrąglającego liczbę do wskazanego miejsca po przecinku dziesiętnym. Niestety nie znajdziesz wbudowanego filtru EJS służącego do tego celu. Na szczęście silnik EJS ułatwia tworzenie własnych filtrów, jak przedstawiono w listingu 11.5. Listing 11.5. Definiowanie własnych filtrów EJS var ejs = require('ejs'); var template = '<%=: price * 1.145 | round:2 %>'; var context = {price: 21};
ejs.filters.round = function(number, decimalPlaces) { Zdefiniowanie funkcji dla obiektu ejs.filters. number = isNaN(number) ? 0 : number; Pierwszy argument funkcji to dane wejściowe, kontekst lub wynik działania poprzedniego filtru. decimalPlaces = !decimalPlaces ? 0 : decimalPlaces; var multiple = Math.pow(10, decimalPlaces); return Math.round(number * multiple) / multiple; }; console.log(ejs.render(template, context));
Jak się przekonasz, filtry EJS zapewniają doskonały sposób zmniejszenia ilości logiki niezbędnej w celu przygotowania danych do wyświetlenia. Transformacji danych nie trzeba przeprowadzać ręcznie przed wygenerowaniem szablonu, ponieważ silnik EJS oferuje elegancki, wbudowany mechanizm do takiego celu.
11.2.3. Integracja EJS w aplikacji Ponieważ przechowywanie plików szablonów wraz z kodem aplikacji jest niewygodne i prowadzi do zaśmiecania kodu, wyjaśnimy teraz, jak użyć API systemu plików Node do odczytywania szablonów z oddzielnych plików. Przejdź do katalogu roboczego i utwórz plik o nazwie app.js zawierający kod przedstawiony w listingu 11.6. Listing 11.6. Przechowywanie kodu szablonu w oddzielnym pliku var ejs = require('ejs'); var fs = require('fs'); var http = require('http'); var filename = './template/students.ejs'; Zwróć uwagę na położenie pliku szablonu. var students = [ Dane przekazywane silnikowi szablonów. {name: 'Jan Kowalski', age: 23}, {name: 'Kasia Nowak', age: 25}, {name: 'Bartek Malinowski', age: 37} ]; var server = http.createServer(function(req, res) { Utworzenie serwera HTTP. if (req.url == '/') { fs.readFile(filename, function(err, data) { Odczyt szablonu z pliku. var template = data.toString(); var context = {students: students}; var output = ejs.render(template, context); Wygenerowanie szablonu. res.setHeader('Content-type', 'text/html'); res.end(output); Wysłanie odpowiedzi HTTP.
}); } else { res.statusCode = 404; res.end('Nie znaleziono'); } }); server.listen(8000);
Następnie utwórz przeznaczony do przechowywania szablonów podkatalog o na z wie template. W podkatalogu szablonów utwórz teraz plik o nazwie students.ejs, aby struktura katalogów aplikacji wyglądała jak na rysunku 11.4. W pliku szablonu students.ejs umieść kod przedstawiony w listingu 11.7.
Rysunek 11.4. Struktura katalogów tworzonej aplikacji Listing 11.7. Szablon EJS generujący tablicę studentów <% if (students.length) { %>
- <% students.forEach(function(student) { %>
- <%= student.name %> (<%= student.age %>) <% }) %>
Buforowanie szablonów EJS Silnik EJS obsługuje opcjonalne buforowanie w pamięci funkcji szablonu. Oznacza to, że po przetworzeniu szablonu przechowywana będzie funkcja utworzona na skutek wspomnianego przetwarzania. Wygenerowanie buforowanego szablonu będzie szybsze, ponieważ można pominąć krok przetwarzania. Jeżeli przeprowadzasz początkowe prace programistyczne nad aplikacją sieciową Node i chcesz, aby wszelkie zmiany wprowadzane w plikach szablonów były odzwierciedlane natychmiast, nie włączaj buforowania. Natomiast jeśli wdrażasz aplikację w środowisku produkcyjnym, wówczas włączenie buforowania przebiega szybko i przynosi znaczne korzyści dla aplikacji. Buforowanie jest warunkowo włączane za pomocą zmiennej środowiskowej NODE_ENV.
Aby wypróbować buforowanie, wywołanie przykładzie zmień na następujące:
funkcji render() w poprzednim
var cache = process.env.NODE_ENV === 'production'; var output = ejs.render( template, {students: students, cache: cache, filename: filename} );
Pamiętaj, że opcja filename niekoniecznie musi wskazywać plik — możesz użyć unikatowej wartości identyfikującej szablon, który ma zostać wygenerowany. Skoro już wiesz, jak zintegrować silnik szablonów EJS z aplikacją Node, zobacz, jak szablony EJS wykorzystać w inny sposób, czyli w przeglądarkach internetowych.
11.2.4. Użycie EJS w aplikacjach działających po stronie klienta Wcześniej przedstawiono przykład aplikacji Node używającej EJS, teraz dowiesz się, jak używać szablonów EJS w przeglądarce internetowej. Aby użyć EJS po stronie klienta, należy najpierw pobrać silnik EJS do katalogu roboczego, co można zrobić za pomocą poniższych poleceń: cd /twój/katalog/roboczy curl https://raw.github.com/visionmedia/ejs/master/ejs.js -o ejs.js
Po pobraniu pliku ejs.js można go używać w kodzie działającym po stronie klienta. W listingu 11.8 przedstawiono prostą aplikację EJS działającą po stronie klienta. Listing 11.8. Użycie silnika EJS w celu dodania obsługi szablonów po stronie klienta
W ten sposób dowiedziałeś się, jak używać w pełni wyposażonego silnika szablonów w Node. Teraz przejdziemy do silnika Hogan, który celowo ogranicza ilość funkcji dostępnych dla kodu szablonu.
11.3. Użycie języka szablonów Mustache wraz z silnikiem Hogan Hogan.js (https://github.com/twitter/hogan.js) to silnik szablonów utworzony dla serwisu Twitter na jego potrzeby związane z szablonami. Hogan jest implementacją Mustache (http://mustache.github.io/), czyli popularnego standardu języka szablonów utworzonego przez Chrisa Wanstratha. Mustache stosuje minimalistyczne podejście w zakresie szablonów. W przeciwieństwie do EJS standard Mustache celowo nie zawiera logiki warunkowej ani żadnych wbudowanych możliwości filtrowania, poza zmianą znaczenia znaków specjalnych w treści jako sposobem na ochronę przed atakami typu XSS. Według filozofii twórcy Mustache kod szablonu powinien być maksymalnie uproszczony. W tym podrozdziale dowiesz się: Jak utworzyć i zaimplementować szablony Mustache w aplikacji. Jakie są znaczniki szablonu oferowane przez standard Mustache. Jak zorganizować szablony za pomocą „partials”. Jak dopasować Hogan do swoich potrzeb za pomocą własnych ograniczników i innych opcji. Zapoznaj się więc z alternatywnym podejściem, jakie Hogan stosuje w zakresie szablonów.
11.3.1. Tworzenie szablonu W celu użycia standardu Hogan w aplikacji lub wypróbowania przykładów przedstawionych w tym podrozdziale konieczne jest zainstalowanie silnika Hogan w katalogu aplikacji. Można to zrobić za pomocą poniższego polecenia wydanego z poziomu powłoki: npm install hogan.js
Poniżej przedstawiono prosty przykład aplikacji Node używającej Hogan do wygenerowania prostego szablonu za pomocą kontekstu. Po uruchomieniu aplikacji zostanie wyświetlony ciąg tekstowy Witaj, szablonie!: var hogan = require('hogan.js'); var template = '{{message}}'; var context = {message: 'Witaj, szablonie!'}; var template = hogan.compile(template); console.log(template.render(context));
Teraz już wiesz, jak przetwarzać szablony Mustache za pomocą silnika Hogan. Przystępujemy więc do poznania znaczników obsługiwanych przez Mustache.
11.3.2. Znaczniki Mustache Pod względem koncepcyjnym znaczniki Mustache są podobne do znaczników EJS. Znaczniki Mustache działają w charakterze miejsc zarezerwowanych dla wartości zmiennym, wskazują na potrzebę iteracji oraz pozwalają na zwiększenie funkcjonalności Mustache i dodanie komentarzy do szablonów.
Wyświetlanie prostych wartości W celu wyświetlenia wartości kontekstu w szablonie Mustache nazwę wartości należy umieścić w podwójnym nawiasie. W społeczności Mustache nawiasy są określane mianem „mustaches”. Jeżeli chcesz wyświetlić wartość elementu name kontekstu, powinieneś użyć znacznika {{name}}. Podobnie jak jest w przypadku większości silników szablonów, Hogan domyślnie neutralizuje znaki specjalne w treści, aby uniknąć ataków typu XSS. W celu wyświetlenia niezmienionej wartości w Hogan konieczne jest dodanie trzeciej pary nawiasów lub poprzedzenie znakiem ampersand nazwy elementu kontekstu. W przypadku poprzedniego przykładu elementu name jego niezmodyfikowaną wartość można wyświetlić, używając znacznika {{{name}}} lub {{&name}}. Jeżeli w szablonie Mustache chcesz umieścić komentarz, możesz użyć następującego formatu: {{! To jest komentarz }}.
Sekcje: iteracja przez wiele wartości
Wprawdzie Hogan nie pozwala na umieszczanie logiki w szablonach, ale oferuje elegancki sposób iteracji przez wiele wartości w elemencie kontekstu — za pomocą tak zwanych sekcji Mustache. Na przykład przedstawiony poniżej kontekst zawiera element wraz z tablicą wartości: var context = { students: [ { name: 'Jan Kowalski', age: 21 }, { name: 'Roman Nowak', age: 26 } ] };
Jeżeli chcesz utworzyć szablon wyświetlający każdego studenta w oddzielnym akapicie HTML i generujący dane wyjściowe podobne do poniższych, takie zadanie możesz łatwo wykonać za pomocą szablonu Hogan:
Imię i nazwisko: Jan Kowalski, wiek: 21 lat
Imię i nazwisko: Roman Nowak, wiek: 26 lat
Przedstawiony poniżej szablon wygeneruje oczekiwany kod HTML: {{#students}}
Imię i nazwisko: {{name}}, wiek: {{age}} lat
{{/students}}Sekcje odwrócone: domyślny kod HTML, gdy wartość nie istnieje A gdyby wartość elementu students w danych kontekstu nie była tablicą? Jeżeli wartość będzie na przykład pojedynczym obiektem, wtedy szablon ją wyświetli. Sekcje nie zostaną jednak wyświetlone, jeśli wartość elementu jest niezdefiniowana, wynosi false lub stanowi pustą tablicę. Jeżeli szablon ma wygenerować komunikat informujący o nieistniejącej wartości dla sekcji, wtedy można wykorzystać oferowane przez Hogan tak zwane sekcje odwrócone. Po dodaniu poniższego fragmentu kodu do poprzedniego szablonu w przypadku braku studenta w danych kontekstu nastąpi wyświetlenie odpowiedniego komunikatu: {{^students}}
Nie znaleziono studentów.
{{/students}}Sekcja lambda: własna funkcjonalność w blokach sekcji Aby umożliwić programistom zwiększenie funkcjonalności Mustache, standard pozwala na definiowanie znaczników sekcji przetwarzających treść szablonu za
pomocą wywołania funkcji, a nie iteracji przez rozwiązanie jest nazywane sekcją lambda.
tablice.
Tego rodzaju
W listingu 11.9 przedstawiono przykład użycia sekcji lambda. Kod powoduje dodanie obsługi składni Markdown podczas generowania szablonu. Zwróć uwagę, że w przykładzie użyto modułu github-flavored-markdown, który trzeba zainstalować przez wydanie z poziomu powłoki polecenia npm install githubflavored-markdown. Listing 11.9. Użycie funkcji lambda w szablonie Hogan var hogan = require('hogan.js'); var md = require('github-flavored-markdown'); Wymagany jest analizator składni Markdown. var template = '{{#markdown}}' Szablon Mustache zawiera także składnię Markdown. + '**Imię i nazwisko**: {{name}}' + '{{/markdown}}'; var context = { name: 'Roman Nowak', markdown: function() { return function(text) { Kontekst szablonu zawiera sekcję lambda przeznaczoną do przetwarzania składni Markdown w szablonie. return md.parse(text); }; } }; var template = hogan.compile(template); console.log(template.render(context));
W listingu 11.9 kod **Imię
w szablonie zostanie wygenerowany jako Imię i nazwisko po przetworzeniu przez analizator Markdown wywoływany przez logikę sekcji lambda. i nazwisko**
Sekcje lambda pozwalają na łatwą implementację w szablonach rozwiązań takich jak buforowanie i mechanizmy translacji.
Partials: wielokrotne użycie szablonów w innych szablonach Podczas tworzenia szablonów zwykle chcesz uniknąć niepotrzebnego powtarzania tego samego kodu w wielu szablonach. Jednym z możliwych rozwiązań jest utworzenie tak zwanych partials. To szablony używane jako bloki budulcowe i umieszczane w innych szablonach. Kolejnym zastosowaniem dla partials jest podział skomplikowanego szablonu na prostsze szablony. W listingu 11.10 przedstawiono przykład użycia partials do oddzielenia od głównego szablonu kodu szablonu wykorzystywanego do wyświetlania danych
studentów. Listing 11.10. Użycie partials w szablonie Hogan var hogan = require('hogan.js'); Kod szablonu używanego jako partials. var studentTemplate = '
Imię i nazwisko: {{name}}, ' + 'wiek: {{age}} lat
'; var mainTemplate = '{{#students}}' Kod szablonu głównego. + '{{>student}}' + '{{/students}}'; var context = { students: [{ name: 'Jan Kowalski', age: 21 },{ name :'Roman Nowak', age: 26 }] }; var template = hogan.compile(mainTemplate); Połączenie szablonów głównego i partials. var partial = hogan.compile(studentTemplate); var html = template.render(context, {student: partial}); Wygenerowanie szablonu głównego i partials. console.log(html);11.3.3. Dostosowanie szablonu Hogan do własnych potrzeb Szablony Hogan są proste w użyciu, więc po poznaniu składni znaczników powinieneś móc bez problemów z nich korzystać. Podczas pracy może wystąpić potrzeba wprowadzenia kilku zmian. Jeżeli nie lubisz nawiasów w stylu Mustache, wówczas możesz zmienić ograniczniki stosowane w szablonach Hogan. Odbywa się to przez przekazanie metodz ie compile() opcji pozwalającej na zmianę ograniczników. Poniższy przykład pokazuje, jak skompilować Hogan wraz z obsługą ograniczników w stylu EJS: hogan.compile(text, {delimiters: '<% %>'});
Jeżeli chcesz użyć znaczników sekcji nierozpoczynających się od znaku # po nawiasie otwierającym, wtedy możesz użyć innej opcji metody compile() — o nazwie sectionTags. Na przykład inny format znaczników można zastosować dla
znaczników sesji wykorzystujących funkcje lambda. W listingu 11.11 przedstawiono zmodyfikowany przykład z listingu 11.9. Nowa wersja używa znaku podkreślenia w celu odróżnienia znacznika sekcji markdown od kolejnych znaczników sekcji, które przeprowadzają iterację, a nie korzystają z funkcji lambda. Listing 11.11. Użycie własnych znaczników sekcji w szablonie Hogan var hogan = require('hogan.js'); var md = require('github-flavored-markdown'); Markdown.
Wymagany jest analizator składni
var template = '{{_markdown}}' Własny znacznik użyty w szablonie. + '**Imię i nazwisko**: {{name}}' + '{{/markdown}}'; var context = { name: 'Roman Nowak', _markdown: function(text) { Funkcja lambda dla własnego znacznika. return md.parse(text); } }; var template = hogan.compile( template, Zdefiniowanie własnych znaczników otwierających i zamykających. {sectionTags: [{o: '_markdown', c: 'markdown'}]} ); console.log(template.render(context));
Podczas użycia szablonów Hogan nie musisz zmieniać żadnych opcji w celu włączenia buforowania. Wspomniane buforowanie jest wbudowane w funkcję compile() i włączone domyślnie. W ten sposób poznałeś dwa całkiem proste silniki szablonów dostępne dla Node. Teraz przechodzimy do silnika szablonów Jade, który w zupełnie inny sposób niż EJS i Hogan obsługuje znaczniki prezentacyjne.
11.4. Szablony Jade Jade (http://jade-lang.com/) oferuje alternatywny sposób określania kodu HTML. Podstawowa różnica między Jade i większością głównych silników szablonów to użycie wcięć, które mają znaczenie. Podczas tworzenia szablonu w Jade stosowane są wcięcia wskazujące zagnieżdżanie znaczników HTML. Znaczniki HTML nie muszą być wyraźnie zamykane, co eliminuje problem przypadkowego, przedwczesnego zamknięcia
znacznika lub pozostawienia niezamkniętego znacznika. Stosowanie wcięć powoduje również, że szablony są wizualnie bardziej przejrzyste i łatwiejsze w obsłudze. Aby przekonać się, jak działają szablony Jade, najpierw spójrz na poniższy kod HTML, który za chwilę wygenerujemy za pomocą szablonu:
"Witaj, świecie!"
Powyższy kod HTML można przedstawić za pomocą następującego szablonu Jade: html head title Witamy body div.content#main strong "Witaj, świecie!"
Jade, podobnie jak EJS, pozwala na osadzanie kodu JavaScript, a szablony Jade mogą być używane po stronie zarówno serwera, jak i klienta. Jade oferuje także funkcje dodatkowe, jak obsługa dziedziczenia szablonów i polecenie mixin. Wspomniane polecenie mixin pozwala na definiowanie wielokrotnego użytku miniszablonów przedstawiających kod HTML dla najczęściej występujących elementów, takich jak listy i pola wyboru. Pod względem koncepcji mixins przypomina partials w Hogan.js, czyli w omówionym w poprzednim podrozdziale silniku szablonów. Dziedziczenie szablonów ułatwia organizację w wielu plikach szablonów Jade niezbędnych do wygenerowania pojedynczej strony HTML. Więcej informacji na ten temat znajdziesz w dalszej części rozdziału. Instalacja Jade w katalogu aplikacji Node wymaga wydania poniższego polecenia z poziomu powłoki: npm install jade
Instalacja Jade wraz z opcją globalną -g również jest użyteczna, ponieważ daje dostęp do polecenia jade pozwalającego na szybkie wygenerowanie szablonu do
postaci kodu HTML. Przedstawione poniżej polecenie spowoduje, że plik template/sidebar.jade zostanie wygenerowany jako sidebar.html w katalogu template. Polecenie jade pozwala na łatwe eksperymentowanie ze składnią Jade: jade template/sidebar.jade
W tym punkcie: Poznasz podstawy Jade, takie jak wskazywanie nazw klas, atrybutów i rozwinięcie bloków. Dowiesz się, jak dodać logikę do szablonu Jade za pomocą wbudowanych słów kluczowych. Dowiesz się, jak zorganizować szablony za pomocą dziedziczenia, bloków i poleceń mixin. Na początek zajmiemy się składnią i podstawowym sposobem użycia Jade.
11.4.1. Podstawy szablonów Jade Jade używa takich samych nazw znaczników jak HTML, ale pozwala na pominięcie znaków < i > — zamiast nich można zastosować wcięcia oznaczające zagnieżdżanie znaczników. Znacznik może mieć przypisaną jedną lub więcej klas CSS przez dodanie .
wraz z przypisanymi klasami content i sidebar można przedstawić w następujący sposób: div.content.sidebar
Identyfikatory CSS znaczników są przypisywane przez dodanie #. Na przykład dodanie identyfikatora CSS o nazwie featured_content do poprzedniego przykładu odbywa się następująco: div.content.sidebar#featured_content Skrót dla znacznika
Identyfikatory CSS znaczników są przypisywane przez dodanie #
Ponieważ znacznik
jest powszechnie używany w kodzie HTML, Jade oferuje możliwość wskazania go za pomocą skrótu. Przedstawione poniżej wywołanie powoduje wygenerowanie takiego samego kodu HTML jak w poprzednim przykładzie: .content.sidebar#featured_content
Skoro już wiesz, jak wskazywać znaczniki HTML oraz przypisywać im identyfikatory i klasy CSS, zobacz, jak wskazać atrybuty HTML.
Podawanie atrybutów znacznika Atrybut znacznika można podać przez jego umieszczenie w nawiasie okrągłym,
a specyfikacje poszczególnych atrybutów należy rozdzielić przecinkami. Dlatego też że pomocą poniższego kodu Jade możesz wskazać łącze, które będzie otwierane w nowej karcie: a(href='http://nodejs.org', target='_blank')
Ponieważ specyfikacja atrybutów znaczników może oznaczać wiele wierszy kodu Jade, silnik szablonów oferuje na tym polu pewną elastyczność. Przedstawiony poniżej przykład to prawidłowy kod Jade będący odpowiednikiem poprzedniego przykładu: a(href='http://nodejs.org', target='_blank')
Istnieje również możliwość podania atrybutów niewymagających wartości. W kolejnym przykładzie Jade pokazano specyfikację formularza HTML zawierającego element select wraz z opcją wskazującą na wybrany element: strong Wybierz ulubioną potrawę: form select option(value='Ser') Ser option(value='Tofu', selected) Tofu
Podanie treści znacznika W poprzednim fragmencie kodu pokazano przykład treści znacznika (Wybierz ulubioną potrawę) po znaczniku strong: Ser po pierwszym znaczniku option i Tofu po drugim. To standardowy sposób podawania treści znacznika w Jade, ale nie jedyny. Wprawdzie tego rodzaju styl doskonale sprawdza się w przypadku niewielkiej treści, ale może spowodować, że wiersze szablonu Jade staną się bardzo długie, jeśli treść znacznika będzie obszerna. Na szczęście, jak pokazano w poniższym przykładzie, treść znacznika można w Jade podawać także za pomocą znaku |: textarea | To jest pewien tekst domyślny, | który powinien zostać | wyświetlony użytkownikowi.
Jeżeli znacznik HTML, na przykład style lub script, akceptuje jedynie tekst (to znaczny nie zezwala na zagnieżdżanie elementów HTML), wówczas znaki | można zupełnie pominąć, jak przedstawiono w poniższym przykładzie: style h1 { font-size: 6em; color: #9DFF0C;
}
Istnienie dwóch oddzielnych sposobów podawania dłuższej i krótszej treści znaczników pomaga w zachowaniu eleganckiego wyglądu znaczników Jade. Ponadto Jade obsługuje alternatywny sposób wyrażania zagnieżdżeń, nazywany rozwinięciem bloku.
Zachowanie organizacji dzięki rozwinięciu bloku Standardowo zagnieżdżenia są wyrażane w Jade przez zastosowanie wcięć, ale czasami wcięcia mogą doprowadzić do powstawania dużej ilości pustego miejsca. Jako przykład poniżej przedstawiono szablon Jade używający wcięć do zdefiniowania prostej listy łączy: ul li a(href='http://nodejs.org/') Strona główna Node.js li a(href='http://npmjs.org/') Strona główna NPM li a(href='http://nodebits.org/') Blog Nodebits
Znacznie bardziej zwięzłym sposobem wyrażenia poprzedniego przykładu jest użycie oferowanego przez Jade mechanizmu rozwinięcia bloku. W takim przypadku po znaczniku umieszcza się dwukropek oznaczający zagnieżdżenie. Przedstawiony poniżej kod Jade generuje takie same dane wyjściowe jak poprzedni listing, ale składa się z czterech wierszy zamiast z siedmiu: ul li: a(href='http://nodejs.org/') Strona główna Node.js li: a(href='http://npmjs.org/') Strona główna NPM li: a(href='http://nodebits.org/') Blog Nodebits
Teraz już wiersz, jak przedstawić znaczniki za pomocą Jade. Przechodzimy więc do zagadnienia integracji Jade z aplikacją sieciową.
Umieszczanie danych w szablonach Jade Do silnika Jade dane są przekazywane w taki sam podstawowy sposób jak do EJS. Szablon zostaje w pierwszej kolejności skompilowany na postać funkcji, która następnie będzie wywoływana dla kontekstu w celu wygenerowania danych wyjściowych HTML. Przykład przedstawiono poniżej: var jade = require('jade'); var template = 'strong #{message}'; var context = {message: 'Witaj, szablonie!'}; var fn = jade.compile(template);
console.log(fn(context));
W poprzednim przykładzie kod #{message} w szablonie wskazywał miejsce zarezerwowane, które zostanie zastąpione przez wartość pochodzącą z kontekstu. Wartości kontekstu można również używać w celu dostarczania wartości atrybutów. Przedstawiony poniżej kod spowoduje wygenerowanie znacznika : var jade = require('jade'); var template = 'a(href = url)'; var context = {url: 'http://google.pl'}; var fn = jade.compile(template); console.log(fn(context));
W ten sposób dowiedziałeś się, jak kod HTML można podać za pomocą Jade, a także jak dostarczać szablonom Jade danych aplikacji. Przechodzimy więc do wykorzystania logiki w Jade.
11.4.2. Logika w szablonach Jade Po dostarczeniu szablonowi Jade danych aplikacji potrzebna jest logika, która będzie mogła przetworzyć wspomniane dane. Jade pozwala na bezpośrednie osadzanie wierszy kodu JavaScript w szablonach, co stanowi doskonały sposób na zdefiniowanie logiki. Powszechnie stosuje się konstrukcje takie jak instrukcje if, pętle for i deklaracje var. Zanim przejdziemy do szczegółów, spójrz na przykład szablonu Jade generującego listę kontaktową. W ten sposób możesz się przekonać, jak używać logiki Jade w aplikacji: h3.contacts-header Moja lista kontaktów if contacts.length each contact in contacts - var fullName = contact.firstName + ' ' + contact.lastName .contact-box p fullName if contact.isEditable p: a(href='/edit/+contact.id) Edycja rekordu p case contact.status when 'Aktywny' strong Użytkownik jest aktywny w systemie when 'Nieaktywny'
em Użytkownik jest nieaktywny w systemie when 'Oczekujący' | Oczekiwanie na akceptację zaproszenia else p Twoja lista kontaktów jest obecnie pusta
Najpierw opiszemy różne sposoby, na jakie Jade obsługuje dane wyjściowe podczas przetwarzania osadzonego kodu JavaScript.
Użycie JavaScript w szablonach Jade Poprzedzenie wiersza kodu JavaScript prefiksem - powoduje wykonanie go bez umieszczenia jakiejkolwiek wartości zwrotnej tego kodu w danych wyjściowych szablonu. Z kolei poprzedzenie logiki JavaScript znakiem = spowoduje dołączenie wartości zwrotnej kodu, przy czym znaki specjalne zostaną zneutralizowane w celu ochrony przed atakami typu XSS. Jeżeli kod JavaScript generuje dane wyjściowe, które nie powinny być modyfikowane, należy zastosować prefiks !=. Podsumowanie prefiksów przedstawiono w tabeli 11.1. Tabela 11.1. Prefiksy stosowane wraz z kodem JavaScript osadzonym w szablonie Jade Prefiks Opis danych wyjściowych
=
Dane wyjściowe zostaną zneutralizowane (w przypadku niezaufanych lub nieprzewidywalnych wartości to rodzaj zabezpieczenia przed atakami XSS).
!=
Dane wyjściowe pozostają niezmodyfikowane (w przypadku zaufanych lub przewidywalnych wartości).
-
Brak danych wyjściowych.
Jade zawiera wiele najczęściej używanych poleceń warunkowych i iteracji, które mogą być zapisywane bez prefiksów: if, else if, else, case, when, default, until, while, each i unless. Istnieje również możliwość definiowania zmiennych w Jade. Poniższe dwa odpowiadające sobie polecenia pokazują, jak można przypisać wartość w Jade: - var count = 0 count = 0
Pozbawione prefiksu polecenie nie generuje danych wyjściowych, podobnie jak w przypadku użycia prefiksu -, co wcześniej omówiono.
Iteracja przez obiekty i tablice Wartości przekazywane w kontekście są dostępne dla kodu JavaScript w szablonie Jade. W kolejnym przykładzie odczytujemy szablon Jade z pliku, a następnie przekazujemy szablonowi kontekst zawierający tablicę z kilkoma komunikatami, które mają być wyświetlone: var jade = require('jade');
var fs = require('fs'); var template = fs.readFileSync('./template.jade'); var context = { messages: [ 'Logowanie zakończone powodzeniem.', 'Witamy ponownie!' ]}; var fn = jade.compile(template); console.log(fn(context));
Szablon Jade przedstawia się następująco: - messages.forEach(function(message) { p= message - })
Ostateczne dane wyjściowe HTML mają postać przedstawioną poniżej:
Jade obsługuje iterację w formie innej niż stosowana w JavaScript: za pomocą polecenia each. Wspomniane polecenie each pozwala na łatwą iterację przez tablicę i właściwości obiektu. Przedstawiony poniżej kod jest odpowiednikiem poprzedniego przykładu, ale używa polecenia each: each message in messages p= message
Iterację przez właściwości obiektu można przeprowadzić, stosując niewielką zmianę polecenia, na przykład: each value, key in post div strong #{key} p value
Warunkowe wygenerowanie kodu szablonu Czasami w zależności od wartości danych szablon musi „podejmować decyzje” co do sposobu ich wyświetlania. Kolejny przykład ilustruje tego rodzaju sytuację, w której mniej więcej co drugi raz skrypt generuje dane w postaci kodu HTML: - var n = Math.round(Math.random() * 1) + 1 - if (n == 1) { script alert('Wygrałeś!'); -}
Polecenia warunkowe mogą być w Jade zapisane także w alternatywnej, nieco bardziej przejrzystej formie: - var n = Math.round(Math.random() * 1) + 1 if n == 1 script alert('Wygrałeś!');
Jeżeli tworzysz negację warunku, na przykład if w Jade słowa kluczowego unless:
(n != 1),
wtedy powinieneś użyć
- var n = Math.round(Math.random() * 1) + 1 unless n == 1 script alert('Wygrałeś!');
Użycie poleceń case w Jade Jade obsługuje również inną niż w JavaScript formę konstrukcji warunkowej podobnej do switch: polecenie case. Wymienione polecenie pozwala na wskazanie danych wyjściowych na podstawie różnych scenariuszy. Poniższy przykład szablonu pokazuje, jak polecenie case można wykorzystać do wyświetlenia na trzy różne sposoby wyników operacji wyszukiwania w blogu. Jeżeli operacja nic nie znajdzie, użytkownikowi jest wyświetlany odpowiedni komunikat. W przypadku znalezienia pojedynczego wpisu bloga zostanie on wyświetlony. Jeżeli znalezionych będzie więcej wpisów, polecenie each zostanie użyte do iteracji przez posty i wyświetlenia ich tytułów. case results.length when 0 p Nie znaleziono szukanego wyrażenia. when 1 p= results[0].content default each result in results p= result.title
11.4.3. Organizacja szablonów Jade Po zdefiniowaniu szablonów warto wiedzieć, jak można je organizować. Podobnie jak w przypadku logiki aplikacji nie chcesz, aby pliki szablonów osiągnęły wielkie rozmiary. Pod względem koncepcyjnym pojedynczy plik szablonu powinien odpowiadać blokowi budulcowemu, na przykład stronie, paskowi bocznemu lub treści postu bloga.
W tym punkcie poznasz kilka mechanizmów pozwalających na współpracę różnych plików szablonów w celu wygenerowania treści. Są to: Strukturyzacja wielu szablonów za pomocą dziedziczenia szablonów. Implementacja układu za pomocą poprzedzania blokiem lub dołączania bloku. Dołączanie szablonów. Wielokrotne użycie logiki szablonu za pomocą poleceń mixin. Rozpoczynamy od zapoznania Cię z dziedziczeniem szablonów w Jade.
Strukturyzacja wielu szablonów za pomocą ich dziedziczenia Dziedziczenie szablonu to jeden ze sposobów strukturyzacji wielu szablonów. W tej koncepcji szablony są traktowane jak klasy w programowaniu zorientowanym obiektowo. Jeden szablon może rozszerzać inny, który z kolei będzie rozszerzał kolejny. Można użyć dowolnej liczby poziomów dziedziczenia, o ile ma to sens. Poniżej przedstawiono prosty przykład dziedziczenia szablonu w celu dostarczenia podstawowego opakowania HTML, które można wykorzystać jako opakowanie na treść strony. W katalogu roboczym utwórz nowy podkatalog o nazwie template przeznaczony na plik szablonu Jade. Szablon strony będzie umieszczony w pliku o nazwie layout.jade zawierającym następujący kod: html head block title body block content
Szablon layout.jade zawiera podstawową definicję strony HTML oraz dwa bloki. Wspomniane bloki są podczas dziedziczenia szablonów używane do zdefiniowania miejsca, w którym szablon dziedziczący powinien umieścić treść. W layout.jade mamy blok title pozwalający szablonowi dziedziczącemu na ustawienie tytułu oraz blok content pozwalający na wyświetlenie treści strony. Następnie w katalogu szablonów utwórz plik o nazwie page.jade przeznaczony do wypełniania bloków title i content: extends layout block title title Messages block content
each message in messages p= message
Do aplikacji dodaj logikę przedstawioną w listingu 11.12 (to zmodyfikowana wersja wcześniejszego przykładu zaprezentowanego w tym podrozdziale), która wyświetla wynik przetworzenia szablonu, pokazując tym samym dziedziczenie w akcji. Listing 11.12. Dziedziczenie szablonów w akcji var jade = require('jade'); var fs = require('fs'); var templateFile = './template/page.jade'; var iterTemplate = fs.readFileSync(templateFile); var context = { messages: [ 'Logowanie zakończone powodzeniem.', 'Witamy ponownie!' ]}; var iterFn = jade.compile( iterTemplate, {filename: templateFile} ); console.log(iterFn(context));
Teraz przechodzimy do innej funkcji poprzedzenia blokiem lub dołączenia bloku.
dziedziczenia
szablonów,
czyli
Implementacja układu za pomocą poprzedzenia blokiem lub dołączenia bloku W poprzednim przykładzie bloki w szablonie layout.jade nie zawierały treści, co powodowało, że ustawienie treści w szablonie page.jade było stosunkowo proste. Jeśli jednak blok w szablonie dziedziczącym zawiera treść, wówczas będzie ona uwzględniona, a nie zastąpiona przez szablon dziedziczący. Odbywa się to przez poprzedzenie blokiem lub dołączenie bloku. W ten sposób można zdefiniować stałą treść i dodawać do niej nową, zamiast całkowicie zastępować ją nową treścią. Przedstawiony poniżej szablon layout.jade zawiera dodatkowy blok o nazwie scripts, w którym znajduje się stała treść — znacznik script wczytujący popularną bibliotekę JavaScript jQuery. html head block title block scripts
script(src='//ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.js') body block content
Jeżeli chcesz, aby szablon page.jade dodatkowo wczytywał bibliotekę jQuery UI, możesz to osiągnąć, używając go w sposób przedstawiony w listingu 11.13. Listing 11.13. Użycie block append w celu wczytania dodatkowego pliku JavaScript extends layout Ten szablon rozszerza szablon layout. baseUrl = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/" block title title Messages block style Zdefiniowanie bloku style. link(rel="stylesheet", href= baseUrl+"themes/flick/jquery-ui.css") block append scripts Dołączenie tego bloku script do zdefiniowanego w szablonie layout. script(src= baseUrl+"jquery-ui.js") block content count = 0 each message in messages - count = count + 1 script $(function() { $("#message_#{count}").dialog({ height: 140, modal: true }); }); != '
Dziedziczenie szablonów to nie jedyny sposób na integrację wielu szablonów. Istnieje również możliwość użycia polecenia Jade o nazwie include.
Dołączanie szablonu Inne narzędzie przeznaczone do organizacji szablonów Jade to polecenie include. Wymienione polecenie powoduje dołączenie zawartości innego szablonu. Jeżeli w użytym we wcześniejszym przykładzie szablonie layout.jade umieścisz wiersz include footer, to otrzymasz następujący szablon: html head block title
block style block scripts script(src='//ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.js') body block content include footer
Jak zilustrowano na rysunku 11.5, podczas generowania danych wyjściowych szablonu layout.jade dołączona będzie również zawartość szablonu o nazwie footer.jade.
Rysunek 11.5. Oferowane przez Jade polecenie include pozwala na łatwe umieszczanie zawartości jednego szablonu w innym
Takie rozwiązanie można zastosować na przykład w celu dodania informacji o witrynie lub zaprojektowania elementów dla layout.jade. Istnieje również możliwość dołączania plików innych niż szablony Jade, wystarczy tylko podać rozszerzenie pliku (na przykład include twitter_widget.html).
Wielokrotne użycie logiki szablonu za pomocą polecenia mixin Wprawdzie oferowane przez Jade polecenie include jest odpowiednie do dołączania wcześniej utworzonych fragmentów kodu, ale nie będzie idealnym rozwiązaniem podczas tworzenia biblioteki wielokrotnego użytku funkcji, które można wykorzystać na różnych stronach lub w oddzielnych aplikacjach. Do tego celu Jade udostępnia polecenie mixin, które pozwala na zdefiniowanie wielokrotnego użycia fragmentów kodu Jade. Polecenie
mixin
jest analogiczne do funkcji w JavaScript. Podobnie jak funkcja
polecenie mixin może pobierać argumenty, które z kolei można wykorzystać do wygenerowania kodu Jade. Przyjmujemy założenie, że aplikacja obsługuje strukturę danych podobną do przedstawionej poniżej: var students = [ {name: 'Jan Kowalski', age: 23}, {name: 'Kasia Nowak', age: 25}, {name: 'Bartek Malinowski', age: 37} ];
Jeżeli chcesz zdefiniować rozwiązanie pozwalające na wygenerowanie listy HTML na podstawie danej właściwości każdego obiektu, wtedy możesz przygotować następujące polecenie mixin: mixin list_object_property(objects, property) ul each object in objects li= object[property]
Następnie wystarczy użyć polecenia użyj poniższego wiersza kodu Jade:
mixin
do wyświetlenia danych. Do tego celu
mixin list_object_property(students, 'name')
Dzięki wykorzystaniu dziedziczenia szablonów i poleceń include i mixin można bardzo łatwo wielokrotnie używać znaczników prezentacyjnych, a tym samym nie pozwolić na nadmierne zwiększenie plików szablonów.
11.5. Podsumowanie W ten sposób poznałeś trzy popularne silniki szablonów HTML. Możesz więc wykorzystać wybraną technikę szablonów do organizacji logiki aplikacji i warstwy prezentacyjnej. Społeczność Node opracowała wiele silników szablonów. Jeżeli żaden z trzech omówionych w rozdziale nie odpowiada Ci z jakiegokolwiek powodu, zawsze możesz wypróbować inne: https://www.npmjs.org/browse/keyword/template. Na przykład silnik szablonów Handlebars.js (https://github.com/wycats/handlebars.js/) rozszerza Mustache i dodaje kolejne funkcje, takie jak znaczniki warunkowe i globalnie dostępne funkcje lambda. Dustjs (https://github.com/akdubya/dustjs) stawia na wydajność i funkcje, na przykład strumieniowanie. Listę silników szablonów dla Node znajdziesz w projekcie consolidate.js (https://github.com/visionmedia/consolidate.js), który zapewnia API pozwalające na abstrakcję użycia silników szablonów i ułatwia
stosowanie wielu silników w aplikacji. Jeśli jednak myśl o nauce składni stosowanej w różnych szablonach jest dla Ciebie zniechęcająca, silnik o nazwie Plates (https://github.com/flatiron/plates) pozwoli Ci na pozostanie przy kodzie HTML i wykorzystanie logiki silnika do mapowania danych aplikacji na identyfikatory i klasy CSS w kodzie znaczników. Jeżeli sposób, w jaki Jade obsługuje rozdzielenie warstwy prezentacyjnej i logiki aplikacji, wydaje Ci się kuszący, zainteresuj się Stylusem (https://github.com/LearnBoost/stylus), czyli projektem stosującym podobne podejście podczas tworzenia stylów CSS. Zapoznałeś się z wiedzą niezbędną do tworzenia profesjonalnych aplikacji sieciowych. W następnym rozdziale zajmiemy się wdrażaniem, czyli udostępnieniem aplikacji całemu światu.
Część III Co dalej? W ostatniej części książki przeczytasz, jak można używać Node w inny sposób niż do tworzenia aplikacji sieciowych oraz jak za pomocą biblioteki Socket.IO dodawać do aplikacji sieciowej komponenty działające w czasie rzeczywistym. Ponadto dowiesz się, jak wykorzystywać Node do tworzenia serwerów innych niż HTTP TCP/IP, a nawet w narzędziach działających z poziomu powłoki. Oprócz nowych sposobów użycia Node poznasz również funkcjonowanie ekosystemu społeczności Node, odkryjesz miejsca, w których można uzyskać pomoc, a także dowiesz się, jak własnymi projektami podzielić się ze społecznością Node za pomocą repozytorium Node Package Manager.
Rozdział 12. Wdrażanie aplikacji Node i zapewnienie bezawaryjnego działania W tym rozdziale: • Wybór hostingu dla aplikacji Node. • Wdrażanie typowej aplikacji. • Zapewnienie działania aplikacji i maksymalizacja jej wydajności.
Opracowanie aplikacji sieciowej to jedno, a umieszczenie jej w środowisku produkcyjnym to zupełnie co innego. W przypadku każdej platformy sieciowej istnieją pewne wskazówki i sztuczki pomagające w zwiększeniu stabilności i maksymalizacji wydajności. Node nie jest tutaj wyjątkiem. Kiedy stajesz przed koniecznością wdrożenia aplikacji sieciowej, w pierwszej kolejności zastanawiasz się, jakie masz możliwości w zakresie hostingu. Konieczne jest również rozważenie, jak monitorować aplikację i zapewnić jej działanie. Być może zastanawiasz się, co można jeszcze zrobić, aby aplikacja działała z maksymalną szybkością. W tym rozdziale dowiesz się, jak przeprowadzić wdrożenie aplikacji sieciowej Node. Na początek sprawdzimy, jakie mamy możliwości w zakresie hostingu aplikacji Node.
12.1. Hosting aplikacji Node Większość programistów aplikacji sieciowych zna aplikacje oparte na PHP. Kiedy serwer Apache zapewniający obsługę PHP otrzyma żądanie HTTP, ścieżkę żądanego adresu URL mapuje na konkretny plik, a PHP wykonuje zawartość wspomnianego pliku. Taka funkcjonalność ułatwia wdrażanie aplikacji PHP — pliki PHP wystarczy umieścić w określonej lokalizacji systemu plików, a staną się dostępne dla przeglądarek internetowych. Cechą hostingu PHP jest nie tylko łatwe wdrożenie, ale również niska cena, ponieważ serwery są bardzo często współdzielone przez wielu użytkowników. Wdrożenie aplikacji Node za pomocą usług hostingu w chmurze przygotowanych pod kątem Node i oferowanych przez firmy takie jak Joyent, Heroku, Nodejitsu, VMware i Microsoft nie jest trudne. Wspomniane usługi hostingu w chmurze przygotowane z myślą o Node są warte uwagi, jeśli chcesz uniknąć problemów związanych z administracją własnym serwerem, a jednocześnie chcesz korzystać z zalet diagnostyki opracowanej specjalnie dla Node. Przykładem
może być oferowana przez system operacyjny SmartOS firmy Joyent możliwość sprawdzenia, która logika w aplikacji Node działa najwolniej. Witryna Cloud9 została zbudowana za pomocą Node.js, a nawet oferuje oparte na przeglądarce internetowej zintegrowane środowisko programistyczne (IDE), w którym można klonować projekty z serwisu GitHub, pracować nad nimi w przeglądarce internetowej, a następnie wdrażać do wielu usług hostingu w chmurze przygotowanych specjalnie dla Node (patrz tabela 12.1). Tabela 12.1. Usługi IDE i hostingu w chmurze przygotowane pod kątem Node Nazwa
Witryna internetowa
Heroku
https://www.heroku.com/
Nodejitsu
https://www.nodejitsu.com/
VMware’s Cloud Foundry
http://www.gopivotal.com/platform-as-a-service/pivotal-cf
Microsoft Azure SDK for Node.js
http://azure.microsoft.com/en-us/develop/nodejs/
Cloud9 IDE
https://c9.io/
Alternatywą dla hostingu w chmurze przygotowanego z myślą o Node jest uruchomienie własnego serwera. Linux to popularny wybór dla serwerów Node. Oferuje znacznie większą elastyczność niż wspomniany hosting w chmurze przygotowany specjalnie dla Node, ponieważ pozwala na łatwą instalację potrzebnych aplikacji, na przykład baz danych. W przypadku hostingu w chmurze do dyspozycji jest na ogół ograniczona liczba aplikacji. Administrowanie serwerem działającym pod kontrolą systemu Linux wymaga doświadczenia. Jeżeli zdecydujesz się na użycie własnego serwera, będziesz musiał nieco poczytać o wybranej dystrybucji systemu Linux i upewnić się o posiadaniu odpowiedniej wiedzy z zakresu jego konfiguracji i obsługi. VirtualBox. Jeżeli zagadnienia związane z administracją serwerem są dla Ciebie nowością, zawsze możesz eksperymentować za pomocą oprogramowania wirtualizacji, na przykład takiego jak VirtualBox (https://www.virtualbox.org/), które pozwala na uruchamianie komputerów wirtualnych działających pod kontrolą danego systemu, na przykład Linux. W przypadku użycia wirtualizacji nie ma żadnego znaczenia, pod kontrolą jakiego systemu działa fizyczny komputer. Jeżeli temat różnych opcji serwera nie jest Ci obcy, możesz jedynie przejrzeć informacje aż do podrozdziału 12.2, w którym przystąpimy do omawiania podstaw wdrażania aplikacji Node. Najpierw przekonajmy się, jakie mamy dostępne opcje: serwery dedykowane,
serwery VPS, ogólnego przeznaczenia serwery chmury. Przeanalizujemy teraz niektóre opcje dostępne podczas wyboru hostingu dla aplikacji Node.
12.1.1. Serwery dedykowane i VPS Serwer może być fizyczny i wówczas jest określany mianem dedykowanego lub też wirtualny. Serwery wirtualne działają wewnątrz fizycznych i współdzielą zasoby serwera fizycznego: procesor, pamięć RAM i przestrzeń na dysku twardym. Serwery wirtualne emulują fizyczne i można nimi administrować w dokładnie taki sam sposób. W pojedynczym serwerze fizycznym może działać wiele serwerów wirtualnych. Serwery dedykowane są zwykle dużo droższe od wirtualnych, a ich przygotowanie najczęściej wymaga nieco dłuższego czasu, ponieważ może wystąpić konieczność zamówienia komponentów, złożenia całości i przeprowadzenia konfiguracji. Natomiast serwery VPS (ang. Virtual Private Server) mogą być przygotowane bardzo szybko, ponieważ są tworzone w istniejących serwerach fizycznych. VPS to dobre rozwiązanie w zakresie hostingu dla aplikacji sieciowych, jeśli nie przewidujesz szybkiego wzrostu stopnia ich użycia. Rozwiązanie oparte na VPS jest tanie, a ponadto pozwala na łatwe dodanie zasobów, takich jak przestrzeń na dysku lub pamięć RAM, gdy wystąpi potrzeba. Odpowiednia technologia jest już opracowana, a wiele firm, na przykład Linode (https://www.linode.com/) i Prgmr (http://prgmr.com/xen/), bardzo ułatwia rozpoczęcie pracy z VPS. Podobnie jak serwery wirtualne, także serwery VPS zwykle nie mogą być tworzone na żądanie. Najczęściej nie nadążają za szybko zwiększającym się poziomem zużycia zasobów, ponieważ to wymaga możliwości szybkiego dodawania kolejnych serwerów bez konieczności interwencji ze strony człowieka. Aby spełnić wspomniane wymagania, trzeba skorzystać z hostingu w chmurze.
12.1.2. Hosting w chmurze Serwer chmury jest podobny do VPS pod tym względem, że stanowi wirtualną emulację serwera dedykowanego. Jednak w porównaniu z serwerami dedykowanymi i VPS ma przewagę w postaci w pełni zautomatyzowanego zarządzania. Serwery chmury mogą być tworzone, uruchamiane, zatrzymywane i usuwane za pomocą zdalnego interfejsu lub API.
Kto będzie potrzebował tego rodzaju rozwiązania? Przypuśćmy, że założyłeś firmę posiadającą oparte na Node korporacyjne oprogramowanie intranet. Chcesz umożliwić klientom zarejestrowanie się do oferowanej usługi i krótko po tym uzyskanie dostępu do ich serwerów działających w Twoim oprogramowaniu. Wprawdzie możesz zatrudnić personel techniczny do konfiguracji i wdrażania serwerów klientów przez całą dobę, ale jeśli nie posiadasz własnego centrum danych, wspomniany personel nadal będzie musiał koordynować działania z dostawcą serwerów dedykowanych lub VPS, aby na czas zapewnić odpowiednie zasoby. Dzięki użyciu serwerów chmury operacje zarządzania serwerem można przeprowadzać za pomocą instrukcji wysyłanych przez API do dostawcy chmury i tym samym uzyskiwać dostęp do nowych serwerów, gdy zajdzie potrzeba. Ten poziom automatyzacji pozwala na szybkie dostarczanie usług klientom bez konieczności podejmowania interwencji ze strony człowieka. Na rysunku 12.1 zilustrowano przykład użycia hostingu w chmurze do tworzenia i usuwania serwerów aplikacji.
Rysunek 12.1. Tworzenie, uruchamianie, zatrzymywanie i usuwanie serwerów w chmurze może być w pełni zautomatyzowane
Wadą użycia serwerów w chmurze jest ich wyższa cena niż w przypadku VPS, a także konieczność posiadania pewnej wiedzy w zakresie konkretnej platformy w chmurze.
Amazon Web Services Najstarszą i najpopularniejszą platformą chmury jest Amazon Web Services (http://aws.amazon.com/). AWS składa się z różnych usług związanych z hostingiem, na przykład dostarczania wiadomości e-mail, CDN (ang. Content Delivery Networks) itd. Oferowana przez Amazon usługa Elastic Compute Cloud (EC2) to jedna z centralnych usług AWS, pozwalająca na tworzenie serwerów w chmurze, gdy zachodzi potrzeba. Wirtualne serwery EC2 są nazywane egzemplarzami i mogą być zarządzane z poziomu powłoki lub za pomocą konsoli opartej na przeglądarce internetowej, jak pokazano na rysunku 12.2. Ponieważ poznanie sposobu zarządzania AWS z poziomu powłoki wymaga nieco czasu, użytkownikom dopiero zaczynającym pracę z EC2 zaleca się użycie konsoli graficznej.
Rysunek 12.2. Konsola AWS jest przeznaczona do zarządzania serwerami chmury Amazon; dla nowych użytkowników to narzędzie łatwiejsze w użyciu niż powłoka
Na szczęście z powodu rozpowszechnienia AWS bardzo łatwo można znaleźć w internecie pomoc oraz samouczki dotyczące tej konsoli. Przykładem może być oferowany przez Amazon samouczek „Getting Started with Amazon EC2 Linux Instances” (http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EC2_GetStarted.html).
Rackspace Cloud Znacznie prostszą i łatwiejszą w użyciu platformą chmury jest Rackspace Cloud
(http://www.rackspace.com/cloud/). Wspomniana łatwość może wydawać się kusząca, ale Rackspace Cloud oferuje znacznie mniejszą gamę funkcji i produktów powiązanych z chmurą niż AWS, a interfejs graficzny przeznaczony do zarządzania usługą jest, delikatnie mówiąc, niewygodny. Serwerami Rackspace Cloud można zarządzać za pomocą interfejsu graficznego lub utworzonych przez społeczność narzędzi działających z poziomu powłoki. W tabeli 12.2 podsumowano omówione w tym podrozdziale opcje, które znajdziesz w zakresie hostingu aplikacji Node. Po ogólnym przedstawieniu usług, które można wykorzystać do hostingu aplikacji Node, teraz przechodzimy do tematu faktycznego wdrożenia aplikacji Node w serwerze. Tabela 12.2. Podsumowanie opcji dotyczących hostingu Możliwość wzrostu ruchu sieciowego
Opcja hostingu
Względny koszt
Wolna
serwer dedykowany
średni
Liniowa
serwer VPS
mały
Nieprzewidywalna
chmura
wysoki
12.2. Podstawy wdrożenia Przyjmujemy założenie, że utworzyłeś aplikację sieciową, którą chcesz się pochwalić światu. Ewentualnie opracowałeś aplikację komercyjną i musisz ją przetestować przed umieszczeniem w środowisku produkcyjnym. Prawdopodobnie zaczniesz od prostego wdrożenia, a następnie wykonasz nieco dodatkowej pracy w celu maksymalizacji wydajności i zapewnienia niezawodnego działania aplikacji. W tym podrozdziale przeanalizujemy proste, tymczasowe wdrożenie Git, a także omówimy sposoby zapewnienia niezawodnego działania aplikacji za pomocą Forever. Tymczasowe wdrożenie istnieje tylko do chwili ponownego uruchomienia, ale jednocześnie charakteryzuje się szybką konfiguracją.
12.2.1. Wdrożenie z repozytorium Git Poniżej przedstawiono podstawowe wdrożenie za pomocą repozytorium Git, co pozwoli Ci na poznanie podstawowych etapów procesu. Wdrożenie odbywa się przez wykonanie wymienionych poniżej kroków: 1. Nawiązanie połączenia z serwerem za pomocą SSH. 2. Instalacja Node i narzędzi kontroli wersji (na przykład Git lub Subversion) w serwerze, o ile zachodzi potrzeba.
3. Pobranie z repozytorium kontroli wersji plików aplikacji, między innymi skryptów Node, obrazów, arkuszy stylów CSS, a następnie umieszczenie ich w serwerze. 4. Uruchomienie aplikacji. Poniżej znajduje się przykład uruchomienia aplikacji po pobraniu jej plików z repozytorium Git: git clone https://github.com/Marak/hellonode.git cd hellonode node server.js
Podobnie jak PHP, także Node nie działa jako zadanie w tle. Dlatego też przedstawione tutaj podstawowe wdrożenie wymaga zachowania otwartego połączenia SSH. Po zamknięciu połączenia nastąpi natychmiastowe zakończenie działania aplikacji. Na szczęście bardzo łatwo można zapewnić nieustanne działanie aplikacji za pomocą prostego narzędzia. Wdrożenie automatyczne. Wdrożenie aplikacji Node można zautomatyzować na wiele sposobów. Jednym z nich jest użycie narzędzia takiego jak Fleet (https://github.com/substack/fleet), które pozwala na wdrożenie jednego lub więcej serwerów za pomocą git push. Znacznie bardziej tradycyjnym podejściem jest użycie Capistrano, co zostało dokładnie omówione w poście Evana Tahlera zatytułowanym „Deploying node.js applications with Capistrano” (http://blog.evantahler.com/blog/deployingnode-js-applications-with-capistrano.html).
12.2.2. Zapewnienie działania aplikacji Node Przyjmujemy założenie, że utworzyłeś osobisty blog za pomocą aplikacji Cloud9 Nog (https://github.com/c9/nog), a teraz chcesz ją wdrożyć i mieć gwarancje jej działania nawet po zamknięciu połączenia SSH. W społeczności Node najpopularniejszym narzędziem do tego celu jest Nodejitsu Forever (https://github.com/nodejitsu/forever). Zapewnia ono działanie aplikacji po zamknięciu połączenia SSH, a ponadto ponownie ją uruchamia, jeśli uległa awarii. Koncepcję sposobu działania Forever zilustrowano na rysunku 12.3.
Rysunek 12.3. Narzędzie Forever pomaga w zapewnieniu działania aplikacji, nawet jeśli ulegnie ona awarii
Forever można zainstalować globalnie za pomocą polecenia
sudo.
Polecenie sudo. Bardzo często podczas instalacji globalnej (to znaczy z użyciem opcji -g) modułu menedżera npm polecenie npm trzeba poprzedzić p ol ecen i em sudo (http://www.sudo.ws/), aby menedżer npm działał z uprawnieniami superużytkownika. Po pierwszym wydaniu polecenia sudo konieczne jest podanie hasła użytkownika. Następnie wykonane zostanie polecenie znajdujące się po sudo. Poniższe polecenie powoduje globalną instalację Forever: sudo npm install -g forever
Po instalacji możesz użyć narzędzia Forever do uruchomienia bloga i zapewnienia jego działania. W tym celu wystarczy wydać poniższe polecenie: forever start server.js
Jeżeli z jakiegokolwiek powodu będziesz chciał zatrzymać działanie aplikacji bloga, wystarczy użyć polecenia stop narzędzia Forever: forever stop server.js
Forever pozwala również na pobranie listy aplikacji zarządzanych przez to narzędzie. Do tego celu służy polecenie list: forever list
Inna użyteczna możliwość narzędzia Forever to opcjonalne ponowne uruchomienie aplikacji po zmianie któregokolwiek z jej plików źródłowych. To uwalnia programistę od konieczności ręcznego ponownego uruchamiania aplikacji za każdym razem, gdy zostanie dodana nowa funkcja lub usunięty błąd. Aby uruchomić narzędzie Forever we wspomnianym trybie, należy użyć opcji -w: forever -w start server.js
Wprawdzie Forever to niezwykle użyteczne narzędzie przeznaczone do
wdrażania aplikacji, ale na potrzeby długoterminowych wdrożeń możesz potrzebować narzędzia wyposażonego w nieco więcej funkcji i możliwości. W kolejnym podrozdziale poznasz wybrane przemysłowe rozwiązania w zakresie monitorowania oraz dowiesz się, jak można zmaksymalizować wydajność działania aplikacji.
12.3. Maksymalizacja wydajności i czasu bezawaryjnego działania aplikacji Kiedy aplikacja Node jest gotowa do udostępnienia użytkownikom, na pewno chcesz się upewnić o jej uruchamianiu i zatrzymywaniu wraz z serwerem oraz o automatycznym ponownym uruchamianiu jej po awarii serwera. Bardzo łatwo zapomnieć o zatrzymaniu aplikacji przed ponownym uruchomieniem serwera lub o jej uruchomieniu tuż po ponownym uruchomieniu serwera. Konieczne jest również upewnienie się o podjęciu wszelkich kroków zapewniających maksymalną wydajność działania aplikacji. Jeśli aplikacja działa w serwerze wyposażonym w procesor czterordzeniowy, wtedy sensowne jest wykorzystanie przez nią więcej niż tylko jednego rdzenia. Gdy używany jest tylko jeden rdzeń, a ilość ruchu sieciowego obsługiwanego przez aplikację gwałtownie wzrośnie, wówczas jeden rdzeń może nie mieć wystarczających możliwości do obsługi całego ruchu, a aplikacja nie będzie natychmiast odpowiadała na działania użytkownika. Oprócz wykorzystania wszystkich rdzeni procesora należy też unikać użycia Node do udostępniania plików statycznych w witrynach produkcyjnych obsługujących duży ruch sieciowy. Node opracowano dla aplikacji interaktywnych, takich jak aplikacje sieciowe i protokoły TCP/IP. Dlatego też nie potrafi udostępniać plików statycznych w tak efektywny sposób jak oprogramowanie przeznaczone tylko do tego celu. Do udostępniania plików statycznych należy wykorzystać technologie, które się w tym specjalizują, takie jak Nginx (http://nginx.org/en/). Ewentualnie wszystkie pliki statyczne można umieścić w systemie CDN, na przykład Amazon S3 (http://aws.amazon.com/s3/), a następnie odwoływać się do tych plików z poziomu aplikacji Node. W tym podrozdziale zostaną przedstawione pewne funkcje pomagające w poprawie wydajności i zapewnieniu niezawodnego działania aplikacji: Użycie Upstart do zapewnienia działania aplikacji i jej ponownego uruchamiania, na przykład po awarii. Użycie API klastra Node do wykorzystania możliwości oferowanych przez procesory wielordzeniowe. Obsługa plików statycznych w aplikacji Node za pomocą Nginx.
Rozpoczynamy od oferującego potężne możliwości i jednocześnie łatwego w użyciu narzędzia Upstart, które zapewnia niezawodne działanie aplikacji.
12.3.1. Zapewnienie działania aplikacji za pomocą Upstart Przyjmujemy założenie, że jesteś zadowolony z opracowanej aplikacji i chcesz ją udostępnić innym. Ponadto chcesz mieć absolutną pewność, że po ponownym uruchomieniu serwera nie zapomnisz ponownie uruchomić aplikacji. Poza tym po ewentualnej awarii aplikacja powinna zostać automatycznie ponownie uruchomiona, a awaria — odnotowana w dzienniku zdarzeń. Powinieneś też otrzymać informację o incydencie, co pozwoli Ci na zdiagnozowanie i usunięcie problemów. Upstart (http://upstart.ubuntu.com/) to projekt zapewniający eleganckie rozwiązanie w zakresie zarządzania uruchamianiem i zatrzymywaniem dowolnej aplikacji systemu Linux, w tym także aplikacji Node. Najnowsze wydania dystrybucji Ubuntu i CentOS obsługują użycie Upstart. Jeżeli narzędzie Upstart nie jest jeszcze zainstalowane w Ubuntu, instalacja zostanie przeprowadzona po wydaniu poniższego polecenia: sudo apt-get install upstart
Jeżeli narzędzie Upstart nie jest jeszcze zainstalowane w systemie CentOS, instalacja zostanie przeprowadzona po wydaniu poniższego polecenia: sudo yum install upstart
Po zainstalowaniu Upstart konieczne jest dodanie pliku konfiguracyjnego Upstart dla każdej aplikacji. Wspomniane pliki są tworzone w katalogu /etc/init i powinny mieć nazwę w stylu nazwa_aplikacji.conf. Pliki konfiguracyjne nie wymagają uprawnień do ich wykonywania. Poniższe polecenie powoduje utworzenie pustego pliku konfiguracyjnego Upstart dla przykładowej aplikacji przedstawionej w tym rozdziale: sudo touch /etc/init/hellonode.conf
W utworzonym pliku konfiguracyjnym umieść kod przedstawiony w listingu 12.1. Przygotowana konfiguracja spowoduje uruchomienie aplikacji wraz z serwerem i zakończenie jej działania wraz z zamknięciem serwera. Sekcja exec to kod wykonywany przez Upstart. Listing 12.1. Typowy plik konfiguracyjny Upstart author "Jak Kowalski" Podanie imienia i nazwiska twórcy aplikacji. description "hellonode" Podanie nazwy aplikacji lub jej opisu. setuid "nonrootuser" Aplikacja będzie uruchomiona przez użytkownika innego niż
root. start on (local-filesystems and net-device-up IFACE=eth0) Uruchomienie aplikacji podczas startu systemu, gdy system plików i sieć będą już dostępne. stop on shutdown Zakończenie działania aplikacji podczas zamykania systemu. respawn Ponowne uruchomienie aplikacji po jej awarii. console log Komunikaty standardowego wejścia i błędów będą zapisywane w pliku /var/log/upstart/nazwa_aplikacji.log. env NODE_ENV=production Zdefiniowanie wszelkich zmiennych środowiskowych niezbędnych dla aplikacji. exec /usr/bin/node /ścieżka/dostępu/do/serwera.js Podanie polecenia uruchamiającego aplikację.
Przedstawiony plik konfiguracyjny zapewnia działanie procesu po ponownym uruchomieniu serwera, a nawet po wystąpieniu awarii samej aplikacji. Wszystkie dane wyjściowe wygenerowane przez aplikację zostaną umieszczone w pliku /var/log/upstart/hellonode.log, a narzędzie Upstart automatycznie zajmie się rotacją dzienników zdarzeń. Po utworzeniu pliku konfiguracyjnego Upstart aplikację można uruchomić przez wydanie poniższego polecenia: sudo service hellonode
Jeżeli uruchomienie aplikacji zakończyło się powodzeniem, otrzymasz komunikat podobny do poniższego: hellonode start/running, process 6770
Narzędzie Upstart oferuje bogate możliwości w zakresie konfiguracji. Warto zapoznać się z dostępnym w internecie podręcznikiem (http://upstart.ubuntu.com/cookbook/), w którym wymieniono wszystkie opcje. Narzędzie Upstart i ponowne uruchamianie Po użyciu opcji respawn narzędzie Upstart będzie nieustannie i automatycznie uruchamiać aplikację po jej awarii, o ile aplikacja nie ulegnie awarii dziesięciokrotnie w ciągu pięciu sekund. Wspomniany limit można zmienić za pomocą opcji respawn limit liczba przedział_czasu, gdzie liczba oznacza ilość uruchomień w podanym przedziale_czasu wyrażonym w sekundach. Na przykład ustawienie limitu dwudziestu uruchomień w ciągu pięciu sekund wymaga użycia poniższych opcji: respawn respawn limit 20 5 Jeżeli aplikacja zostanie ponownie uruchomiona 10 razy w ciągu 5 sekund (domyślny limit), zwykle oznacza to problem w kodzie lub konfiguracji i prawdopodobnie nigdy nie zostanie ona prawidłowo uruchomiona. Po osiągnięciu limitu narzędzie Upstart nie będzie ponownie próbowało uruchomić aplikacji, co ma na celu zachowanie zasobów dla innych procesów. Dobrym rozwiązaniem jest sprawdzanie stanu aplikacji poza narzędziem Upstart, aby jej programistom dostarczać odpowiednie komunikaty, na przykład w postaci wiadomości e-mail. Operacja może polegać na przejściu do witryny i sprawdzeniu, czy użytkownik otrzymuje prawidłową odpowiedź. Możesz wykorzystać własne metody lub użyć jednego z dostępnych narzędzi przeznaczonych do tego celu, na przykład Monit (http://mmonit.com/monit/) lub Zabbix (http://www.zabbix.com/).
Skoro już wiesz, jak zapewnić działanie aplikacji niezależnie od awarii lub ponownego uruchomienia serwera, kolejnym logicznym krokiem jest osiągnięcie maksymalnej wydajności działania. Na tym polu pomocne może okazać się API klastra Node.
12.3.2. API klastra — wykorzystanie zalety w postaci wielu rdzeni Większość nowoczesnych procesorów zawiera kilka rdzeni, ale Node podczas działania używa tylko jednego z nich. Jeżeli umieściłeś aplikację Node w serwerze i chcesz w maksymalnym stopniu wykorzystać jej możliwości sprzętowe, jednym z rozwiązań może być ręczne uruchomienie wielu egzemplarzy aplikacji działających na różnych portach TCP/IP. Następnie za pomocą mechanizmu równoważenia obciążenia trzeba rozkładać ruch sieciowy między poszczególne egzemplarze. Przygotowanie tego rodzaju rozwiązania jest pracochłonne. Aby ułatwić wykorzystanie wielu rdzeni przez pojedynczą aplikację, do Node dodano API klastra. Wymienione API ułatwia aplikacji jednoczesne działanie wielu „procesów roboczych” w poszczególnych rdzeniach, wykonujących to samo zadanie i udzielających odpowiedzi na tym samym porcie. Na rysunku 12.4 pokazano sposób działania aplikacji wykorzystującej API klastra w procesorze czterordzeniowym.
Rysunek 12.4. Utworzenie trzech dodatkowych procesów roboczych w procesorze czterordzeniowym
Kod przedstawiony w listingu 12.2 automatycznie tworzy procesy robocze dla każdego dodatkowego rdzenia w procesorze. Listing 12.2. Demonstracja użycia API klastra Node var cluster = require('cluster'); var http = require('http'); var numCPUs = require('os').cpus().length; Ustalenie liczby rdzeni w procesorze
serwera. if (cluster.isMaster) { for (var i = 0; i < numCPUs; i++) { Utworzenie procesu roboczego dla każdego z nich. cluster.fork(); } cluster.on('exit', function(worker, code, signal) { console.log('Proces roboczy ' + worker.process.pid + ' zakończył działanie.'); }); } else { http.Server(function(req, res) { Zdefiniowanie zadania wykonywanego przez poszczególne procesy robocze. res.writeHead(200); res.end('Jestem procesem roboczym działającym w procesie ' + process.pid); }).listen(8000); }
Ponieważ procesy główny i robocze to oddzielne procesy systemu operacyjnego (to konieczne, aby mogły działać w oddzielnych rdzeniach), nie mogą współdzielić informacji o stanie za pomocą zmiennych globalnych. Na szczęście API klastra Node zapewnia rozwiązanie pozwalające na komunikację między procesem głównym i roboczymi. W listingu 12.3 przedstawiono przykład aplikacji, w której zachodzi komunikacja między procesami głównym i roboczymi. Liczba wszystkich żądań jest przechowywana przez proces główny, natomiast poszczególne procesy robocze informują o obsłudze każdego żądania. Listing 12.3. Demonstracja użycia API klastra Node var cluster = require('cluster'); var http = require('http'); var numCPUs = require('os').cpus().length; var workers = {}; var requests = 0; if (cluster.isMaster) { for (var i = 0; i < numCPUs; i++) { workers[i] = cluster.fork(); (function (i) { workers[i].on('message', function(message) { Nasłuchiwanie informacji z procesu roboczego. if (message.cmd == 'incrementRequestTotal') { requests++; Zwiększenie liczby wszystkich żądań.
for (var j = 0; j < numCPUs; j++) { workers[j].send({ Wysłanie wszystkim procesom roboczym informacji o całkowitej liczbie żądań. cmd:
'updateOfRequestTotal',
requests: requests }); } } }); })(i); Użycie domknięcia w celu zachowania wartości procesu roboczego. } cluster.on('exit', function(worker, code, signal) { console.log('Proces roboczy ' + worker.process.pid + ' zakończył działanie.'); }); } else { process.on('message', function(message) { Nasłuchiwanie informacji z procesu głównego. if (message.cmd == 'updateOfRequestTotal') { requests = message.requests; Uaktualnienie licznika żądań za pomocą komunikatu procesu głównego. } }); http.Server(function(req, res) { res.writeHead(200); res.end('Proces roboczy w procesie ' + process.pid + ' twierdzi, że klaster udzielił odpowiedzi na ' + requests + ' żądań.'); process.send({cmd: 'incrementRequestTotal'}); Poinformowanie procesu głównego o konieczności zwiększenia licznika żądań. }).listen(8000); }
Użycie API klastra Node to prosty sposób pozwalający na tworzenie aplikacji wykorzystujących pełnię możliwości nowoczesnego sprzętu komputerowego.
12.3.3. Proxy i hosting plików statycznych Wprawdzie Node to efektywne rozwiązanie w zakresie udostępniania dynamicznej treści sieciowej, ale nie sprawdza się już tak efektywnie podczas udostępniania plików statycznych, takich jak obrazy, style CSS lub skrypty
JavaScript działające po stronie klienta. Udostępnianie plików statycznych przez HTTP to specjalny rodzaj zadania, do realizacji którego opracowano odpowiednie oprogramowanie. Wspomniane oprogramowanie jest od lat używane do tego rodzaju operacji i zostało specjalnie zoptymalizowane pod ich kątem. Na szczęście w Node bardzo łatwo można przeprowadzić konfigurację Nginx (http://nginx.org/en/) — to dostępny jako oprogramowanie open source serwer WWW, który został zoptymalizowany do udostępniania plików statycznych. W typowej konfiguracji Nginx/Node serwer Nginx początkowo obsługuje każde żądanie sieciowe i te żądania, które nie dotyczą plików statycznych, są przekazywane do Node. Tego rodzaju konfigurację zilustrowano na rysunku 12.5.
Rysunek 12.5. Nginx można użyć jako proxy do szybkiego przekazywania zasobów statycznych z powrotem do klientów sieciowych
Rozwiązanie pokazane na rysunku 12.5 zostało zaimplementowane w kodzie przedstawionym w listingu 12.4. Ten listing to sekcja http pliku konfiguracyjnego Nginx. Wspomniany plik konfiguracyjny Nginx jest w systemie Linux przechowywany zgodnie z konwencją w katalogu /etc (/etc/nginx/nginx.conf). Dzięki użyciu Nginx do obsługi statycznych zasobów aplikacji sieciowych gwarantujesz, że Node będzie wykorzystywane do zadań, w realizacji których sprawdza się najlepiej. Listing 12.4. Plik konfiguracyjny, który używa Nginx jako proxy dla Node.js i udostępnia pliki statyczne http { upstream my_node_app { server 127.0.0.1:8000; Adres IP i numer portu aplikacji Node.
} server { listen 80; Port, na którym proxy będzie otrzymywało żądania. server_name localhost domain.com; access_log /var/log/nginx/my_node_app.log; location ~ /static/ { Obsługa żądań plików dla adresów URL rozpoczynających się od /static/. root /home/node/my_node_app; if (!-f $request_filename) { return 404; } } location / { Zdefiniowanie ścieżki adresu URL, na który proxy będzie udzielać odpowiedzi. proxy_pass http://my_node_app; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-NginX-Proxy true; } } }
12.4. Podsumowanie W tym rozdziale przedstawiono wiele opcji w zakresie hostingu aplikacji Node, między innymi rozwiązania opracowane pod kątem Node, serwery dedykowane, serwery VPS oraz hosting w chmurze. Każda opcja sprawdza się w innych zastosowaniach. Gdy osiągniesz gotowość do wdrożenia aplikacji Node dla ograniczonego kręgu odbiorców, proces wdrożenia będziesz mógł przeprowadzić szybko za pomocą narzędzia Forever. Z kolei w długoterminowych wdrożeniach dobrym rozwiązaniem może być zautomatyzowanie operacji uruchamiania i zatrzymywania działania aplikacji za pomocą narzędzia Upstart. Abyś mógł w maksymalnym stopniu wykorzystać zasoby sprzętowe serwera, do dyspozycji masz API klastra Node pozwalające na jednoczesne uruchamianie egzemplarzy aplikacji w wielu rdzeniach. Jeżeli aplikacja sieciowa musi udostępniać zasoby statyczne, takie jak obrazy lub dokumenty PDF, wówczas warto skonfigurować serwer Nginx i zastosować go w charakterze proxy dla
aplikacji Node. W ten sposób poznałeś tajniki aplikacji sieciowych Node. Warto więc dowiedzieć się, co jeszcze można osiągnąć za pomocą Node. W kolejnym rozdziale przyjrzymy się innym zastosowaniom Node: począwszy od tworzenia narzędzi działających w powłoce aż po narzędzia pozwalające na pobieranie danych z witryn internetowych.
Rozdział 13. Nie tylko serwery WWW W tym rozdziale: • Użycie biblioteki Socket.IO do prowadzenia w czasie rzeczywistym komunikacji między przeglądarkami internetowymi. • Implementacja sieci TCP/IP. • Użycie API Node do współpracy z systemem operacyjnym. • Opracowanie narzędzi działających w powłoce i posługiwanie się nimi.
Asynchroniczna natura Node pozwala na wykonywanie intensywnych operacji wejścia--wyjścia, co mogłoby być niemożliwe lub nieefektywne w środowisku synchronicznym. W tej książce omówiono aplikacje HTTP i być może chcesz zapytać: a co z innymi rodzajami aplikacji? Do czego Node może być jeszcze użyteczne? Powinieneś wiedzieć, że framework Node nie jest przeznaczony jedynie dla HTTP, ale służy do wszelkiego rodzaju ogólnych zadań wymagających przeprowadzania operacji wejścia-wyjścia. Oznacza to, że za pomocą Node możesz utworzyć praktycznie dowolny rodzaj aplikacji, na przykład program działający na poziomie powłoki, skrypt przeznaczony do administracji systemem, a także działające w czasie rzeczywistym aplikacje sieciowe. W tym rozdziale dowiesz się, jak tworzyć działające w czasie rzeczywistym serwery WWW wykraczające poza tradycyjny model serwera HTTP. Ponadto poznasz także inne API Node, które można wykorzystać do tworzenia innych rodzajów aplikacji, takich jak serwery TCP lub programy powłoki. Rozpoczynamy od omówienia biblioteki Socket.IO pozwalającej na prowadzenie w czasie rzeczywistym komunikacji między przeglądarkami internetowymi i serwerem.
13.1. Biblioteka Socket.IO Biblioteka Socket.IO (http://socket.io/) to bez wątpienia najbardziej znany moduł w społeczności Node. Programiści zainteresowani tworzeniem aplikacji sieciowych działających w czasie rzeczywistym, którzy nie znają Node, wcześniej lub później napotykają Socket.IO, a następnie odkrywają Node. Biblioteka Socket.IO pozwala na tworzenie działających w czasie rzeczywistym aplikacji sieciowych opartych na dwukierunkowej komunikacji między serwerem i klientem. W najprostszej postaci biblioteka Socket.IO ma API bardzo podobne do WebSocket API ( http://www.websocket.org/) oraz kilka wbudowanych rozwiązań zapasowych dla starszych przeglądarek internetowych, które nie
obsługują nowych funkcji. Socket.IO zapewnia wygodne API przeznaczone do rozgłaszania, wysyłania wiadomości itd. Oferowane funkcje powodują, że Socket.IO to bardzo popularna biblioteka dla gier działających w przeglądarkach internetowych, aplikacji czatu i aplikacji wykorzystujących strumienie. HTTP to protokół bezstanowy, co oznacza, że klient może wykonać tylko pojedyncze, krótkie żądania do serwera, który z kolei nie ma możliwości rozróżniania połączonych z nim użytkowników. Wspomniane ograniczenie doprowadziło do opracowania protokołu WebSocket, który pozwala przeglądarkom internetowym na utrzymanie połączenia typu pełny dupleks z serwerem — obie strony mogą wówczas jednocześnie wysyłać i otrzymywać dane. API WebSocket otwiera zupełnie nowe możliwości przed aplikacjami sieciowymi opartymi na prowadzonej w czasie rzeczywistym komunikacji między klientem i serwerem. Problem z protokołem WebSocket polega na tym, że prace nad nim nie zostały jeszcze ukończone. Wprawdzie pewne przeglądarki internetowe oferują już obsługę WebSocket, ale nadal wiele starszych wersji, przede wszystkim Internet Explorer, nie obsługuje WebSocket. Biblioteka Socket.IO rozwiązuje ten problem przez wykorzystanie WebSocket w przeglądarkach internetowych obsługujących tę technologię oraz zastosowanie pewnych rozwiązań zapasowych przeznaczonych dla przeglądarek pozbawionych obsługi WebSocket. Dzięki temu zachowania naśladujące WebSocket są dostępne nawet w starszych przeglądarkach internetowych. W tym podrozdziale utworzymy dwie przykładowe aplikacje oparte na bibliotece Socket.IO: Minimalną aplikację Socket.IO, która czas serwera przekazuje połączonym z nim klientom. Aplikację Socekt.IO odświeżającą stronę po modyfikacji pliku CSS. Po utworzeniu wymienionych przykładowych aplikacji poznasz jeszcze kilka innych sposobów użycia biblioteki Socket.IO, co odbędzie się podczas modyfikacji utworzonej w rozdziale 4. aplikacji wyświetlającej postęp operacji przekazywania plików do serwera. Zaczynamy jednak od podstaw.
13.1.1. Tworzenie minimalnej aplikacji Socket.IO Przyjmujemy założenie, że chcemy utworzyć niewielką aplikację sieciową, która nieustannie będzie wyświetlała w przeglądarce internetowej aktualny czas UTC serwera. Tego rodzaju aplikacja będzie służyła do sprawdzania różnicy między czasem serwera i klientów. Zastanów się teraz, jak taką aplikację można
utworzyć za pomocą modułu http i dotąd poznanych frameworków. Wprawdzie istnieje możliwość opracowania pewnego rozwiązania opartego na sztuczkach takich jak użycie puli, ale biblioteka Socket.IO oferuje estetyczny interfejs do realizacji tego rodzaju zadań. Implementacja wspomnianej aplikacji z wykorzystaniem Socket.IO jest niezwykle prosta. Aby utworzyć aplikację, w pierwszej kolejności należy zainstalować Socket.IO za pomocą menedżera npm: npm install socket.io
W listingu 13.1 przedstawiono kod działający po stronie serwera. Umieść go w pliku o nazwie clock-server.js; kod będzie można wypróbować po przygotowaniu kodu działającego po stronie klienta. Listing 13.1. Serwer Socket.IO nieustannie przekazujący klientowi aktualny czas var app = require('http').createServer(handler); var io = require('socket.io').listen(app); Uaktualnienie zwykłego serwera HTTP do serwera Socket.IO. var fs = require('fs'); var html = fs.readFileSync('index.html', 'utf8'); function handler (req, res) { Kod serwera HTTP zawsze udostępnia plik index.html. res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Length', Buffer.byteLength(html, 'utf8')); res.end(html); } function tick () { var now = new Date().toUTCString(); Pobranie bieżącego czasu w formacie UTC. io.sockets.send(now); Wysłanie aktualnego czasu do wszystkich połączonych gniazd. } setInterval(tick, 1000); Wywołanie funkcji tick() co sekundę. app.listen(8080);
Jak możesz się przekonać, biblioteka Socket.IO minimalizuje ilość dodatkowego kodu, który trzeba umieścić w podstawowym serwerze HTTP. Tylko dwa wiersze kodu są potrzebne do użycia zmiennej io (to zmienna egzemplarza serwera Socket.IO) w celu przesyłania w czasie rzeczywistym komunikatów między serwerem i klientami. W omawianym przykładzie funkcja tick() jest wywoływana co sekundę i przekazuje aktualny czas serwera wszystkim połączonym z nim klientom. Kod serwera w pierwszej kolejności wczytuje do pamięci plik index.html, który teraz zaimplementujemy. Kod działający po stronie klienta przedstawiono w listingu 13.2.
Listing 13.2. Klient Socket.IO wyświetlający czas otrzymany z serwera Aktualny czas w serwerze:
Wypróbowanie aplikacji Teraz można już wypróbować serwer. Uruchom go za pomocą wywołania node clock-server.js, a zobaczysz komunikat info - socket.io started. Oznacza to, że biblioteka Socket.IO została skonfigurowana i jest gotowa do otrzymywania połączeń. Uruchom więc przeglądarkę internetową i przejdź pod adres URL http://localhost:8080/. Powinieneś otrzymać wynik podobny do pokazanego na rysunku 13.1. Czas będzie uaktualniany co sekundę na podstawie komunikatu otrzymywanego z serwera. Uruchom śmiało inną przeglądarkę internetową, przejdź na ten sam adres URL i przekonaj się, że zmiana wartości czasu następuje jednocześnie w obu przeglądarkach.
Rysunek 13.1. Serwer czasu uruchomiony w oknie terminala i połączony z nim klient widoczny w przeglądarce internetowej
W ten sposób za pomocą biblioteki Socket.IO i zaledwie kilku wierszy kodu przygotowałeś prowadzoną w czasie rzeczywistym komunikację między serwerem i klientami. Inne rodzaje komunikatów obsługiwanych przez Socket.IO. Wysyłanie komunikatu do wszystkich połączonych gniazd to tylko jedno z rozwiązań udostępnianych przez Socket.IO i pozwalających na współpracę z połączonymi użytkownikami. Komunikaty można wysyłać także do poszczególnych gniazd, rozgłaszać je do wszystkich gniazd poza jednym wskazanym, a także można wysyłać zmienne (opcjonalne) komunikaty itd. Więcej informacji na ten temat znajdziesz w dokumentacji biblioteki Socket.IO dostępnej na stronie http://socket.io/#how-to-use. Skoro przekonałeś się, jak proste rozwiązania można stosować za pomocą biblioteki Socket.IO, teraz możemy przejść do innego przykładu, pokazującego użyteczność zdarzeń wysyłanych przez serwer.
13.1.2. Użycie biblioteki Socket.IO do odświeżenia strony i stylów CSS Oto jak w skrócie internetowych:
wygląda
typowy
sposób
pracy
projektanta
stron
1. Otworzenie strony internetowej w wielu przeglądarkach. 2. Wyszukanie elementów wymagających modyfikacji stylów. 3. Wprowadzenie odpowiednich zmian w jednym lub większej liczbie arkuszy stylów. 4. Ręczne odświeżenie strony we wszystkich przeglądarkach internetowych. 5. Powrót do kroku 2. Jedną z możliwości usprawnienia pracy jest automatyzacja kroku 4., w którym projektant musi ręcznie przejść do każdej przeglądarki internetowej i kliknąć przycisk odświeżający stronę. To zadanie jest szczególnie czasochłonne, gdy podczas pracy stronę trzeba przetestować w wielu przeglądarkach internetowych w różnych komputerach i urządzeniach mobilnych. Czy istnieje możliwość całkowitej eliminacji ręcznego odświeżania strony? Wyobraź sobie, że po zapisaniu arkusza stylów w edytorze tekstów wszystkie przeglądarki internetowe, w których jest ona wyświetlana, automatycznie odświeżają stronę i odzwierciedlają tym samym zmiany wprowadzone w CSS. To byłaby ogromna oszczędność czasu dla projektantów stron internetowych. Biblioteka Socket.IO w połączeniu z funkcjami Node fs.watchFile() i fs.watch()
umożliwia przygotowanie wspomnianego rozwiązania, na dodatek w zaledwie niewielu wierszach kodu. W
omawianym przykładzie użyjemy funkcji fs.watchFile() zamiast nowszej fs.watch(), ponieważ chcemy zachować gwarancję, że kod będzie działał dokładnie tak samo na wszystkich platformach. Dokładne omówienie sposobu działania funkcji fs.watch() znajdzie się w dalszej części rozdziału. fs.watchFile() kontra fs.watch(). Node.js oferuje API przeznaczone do obserwacji plików. Pierwsze z nich to funkcja fs.watchFile() (http://nodejs.org/api/fs.html#fs_fs_watchfile_filename_options_listener), która wykorzystuje sporą ilość zasobów, ale jest niezawodna i działa na wszystkich platformach. Drugie to funkcja fs.watch() (http://nodejs.org/api/fs.html#fs_fs_watch_filename_options_listener), która jest wysoce zoptymalizowana dla poszczególnych platform, ale jej zachowanie jest odmienne na pewnych platformach. Dokładne omówienie tej funkcji znajdziesz w punkcie 13.3.2. W omawianym przykładzie połączymy użycie frameworka Express i biblioteki Socket.IO. Oba wymienione komponenty doskonale ze sobą współpracują, podobnie jak użyty w poprzednim przykładzie http.Server. Najpierw zapoznamy się z kodem działającym po stronie serwera. Kod przedstawiony w listingu 13.3 umieść w pliku o nazwie watch-server.js, jeśli będziesz chciał ostatecznie uruchomić tworzoną tutaj aplikację. Listing 13.3. Serwer utworzony za pomocą Express/Socket.IO powoduje wywołanie zdarzenia po wykryciu zmiany w pliku var fs = require('fs'); var url = require('url'); var http = require('http'); var path = require('path'); var express = require('express'); var app = express(); Utworzenie aplikacji serwera Express. var server = http.createServer(app); var io = require('socket.io').listen(server); Opakowanie serwera HTTP w celu utworzenia egzemplarza serwera Socket.IO. var root = __dirname; Użycie metody pośredniczącej do monitorowania plików wskazanych przez metodę pośredniczącą static(). app.use(function (req, res, next) { Zarejestrowanie zdarzenia statycznego emitowanego przez metodę pośredniczącą static().
var file = url.parse(req.url).pathname;
var mode = 'stylesheet'; if (file[file.length - 1] == '/') { file += 'index.html'; mode = 'reload'; } createWatcher(file, mode); Określenie nazwy pliku i wywołanie createWatcher(). next(); }); app.use(express.static(root)); Konfiguracja serwera jako prostego serwera pliku statycznego. var watchers = {}; Zachowanie listy aktywnych plików, które są monitorowane. function createWatcher (file, event) { var absolute = path.join(root, file); if (watchers[absolute]) { return; } fs.watchFile(absolute, function (curr, prev) { Rozpoczęcie monitorowania pliku pod kątem zmian. if (curr.mtime !== prev.mtime) { Sprawdzenie, czy zmianie uległ mtime (czas ostatniej modyfikacji). Jeżeli tak, należy wywołać zdarzenie Socket.IO. io.sockets.emit(event, file); } }); watchers[absolute] = true; Oznaczenie pliku jako monitorowanego. } server.listen(8080);
Na tym etapie masz w pełni funkcjonalny serwer plików statycznych przygotowany do wywoływania u klientów zdarzeń reload i stylesheet za pomocą biblioteki Socket.IO. Teraz zajmiemy się kodem działającym po stronie klienta. Kod przedstawiony w listingu 13.4 umieść w pliku index.html, aby został udostępniony po uruchomieniu serwera i podaniu ścieżki dostępu do katalogu głównego. Listing 13.4. Kod działający po stronie klienta odpowiedzialny za ponowne wczytanie stylów po otrzymaniu zdarzenia z serweraDynamiczne ponowne wczytywanie arkuszy stylów CSS za pomocą Socket.IO
Wypróbowanie aplikacji Zanim aplikacja będzie działała, konieczne jest utworzenie kilku plików CSS (header.css i styles.css), ponieważ plik index.html wczytuje wymienione arkusze
stylów podczas generowania jego zawartości. Po przygotowaniu kodu działającego po stronie serwera, pliku index.html i arkuszy stylów używanych przez przeglądarkę internetową można wypróbować aplikację. Uruchom więc serwer: $ node watch-server.js
Po uruchomieniu serwera otwórz przeglądarkę internetową i przejdź na stronę http://localhost:8080, a zobaczysz udostępnioną wygenerowaną prostą stronę HTML. Teraz spróbuj zmodyfikować jeden z plików CSS (na przykład zmień kolor tła dla znacznika ). Przekonasz się, że arkusz stylów zostanie ponownie wczytany w przeglądarce internetowej bez odświeżenia samej strony. Spróbuj wyświetlić podaną stronę jednocześnie w wielu przeglądarkach internetowych. W omawianym przykładzie reload i stylesheet to własne zdefiniowane zdarzenia aplikacji — one nie są częścią API biblioteki Socket.IO. Na podstawie omówionego przykładu przekonałeś się, że obiekt socket działa w charakterze dwukierunkowego EventEmitter, którego można użyć do emisji zdarzeń transferowanych później przez Socket.IO do klienta.
13.1.3. Inne zastosowania dla biblioteki Socket.IO Jak zapewne wiesz, protokół HTTP nie został opracowany w celu zapewnienia jakiegokolwiek rodzaju komunikacji w czasie rzeczywistym. Jednak dzięki zastosowaniu technologii takich jak WebSocket i modułów takich jak biblioteka Socket.IO wspomniane ograniczenie zostało pokonane. W ten sposób otworzono drogę pozwalającą na opracowywanie wielu nowych rodzajów aplikacji sieciowych działających w przeglądarkach internetowych, co wcześniej było niemożliwe. W rozdziale 4. dowiedziałeś się, że biblioteka Socket.IO będzie doskonałym rozwiązaniem do przekazywania przeglądarce internetowej zdarzeń informujących o postępie podczas transferu pliku do serwera. Istnieje możliwość użycia także własnego zdarzenia progress: form.on('progress', function(bytesReceived, bytesExpected) { Uaktualniona wersja przykładu z punktu 4.4.3. var percent = Math.floor(bytesReceived / bytesExpected * 100); socket.emit('progress', { percent: percent }); Przekazanie za pomocą Socket.IO informacji wyrażonych w procentach. });
Aby przedstawione rozwiązanie działało, konieczne jest uzyskanie dostępu do e gz e mpla r z a socket dopasowującego przeglądarkę internetową, która przekazuje plik do serwera. To wykracza poza zakres tematyczny niniejszej
książki, ale zasoby dostępne w internecie mogą okazać się tutaj pomocne. (Początkujący powinni zapoznać się z artykułem Daniela Bauliga zatytułowanym „socket.io and Express: tying it all together”, który opublikowano na blogu blinzeln pod adresem http://www.danielbaulig.de/socket-ioexpress/). Biblioteka Socket.IO zmieniła zasady gry. Jak wcześniej wspomniano, programiści zainteresowani tworzeniem działających w czasie rzeczywistym aplikacji sieciowych bardzo często słyszeli o tej bibliotece, jeszcze zanim dowiedzieli się o istnieniu Node.js — to potwierdzenie, jak wpływowa i ważna jest biblioteka Socket.IO. Informacje dotyczące biblioteki nieustannie pojawiają się w społecznościach zajmujących się grami sieciowymi, jest ona używana do tworzenia bardziej kreatywnych gier i aplikacji, niż można to sobie wyobrazić. Wspomniana biblioteka to również bardzo popularny wybór w aplikacjach tworzonych w technologiach uznawanych za konkurencyjne dla Node, na przykład Node Knockout (http://nodeknockout.com/). Do jakich zapierających dech w piersiach celów ją wykorzystasz?
13.2. Dokładniejsze omówienie sieci TCP/IP Node to technologia doskonale przystosowana dla aplikacji działających w sieci, ponieważ na ogół używanie ich oznacza dużą ilość operacji wejścia-wyjścia. Poza serwerami HTTP, o których sporo się dowiedziałeś z tej książki, Node obsługuje praktycznie każdy rodzaj sieci opartej na TCP. Zatem platforma Node jest odpowiednia do utworzenia na przykład serwera poczty elektronicznej, plików lub proxy, a ponadto może być używana przez klientów dla wymienionego rodzaju usług. Technologia dostarcza kilku narzędzi pomagających w tworzeniu aplikacji o wysokiej jakości i zapewniających dużą wydajność w zakresie operacji wejścia-wyjścia. Więcej informacji na ten temat znajdziesz w tym podrozdziale. Pewne protokoły sieciowe wymagają wartości odczytywanych na poziomie bajtów — znaków, liczb całkowitych, liczb zmiennoprzecinkowych i innych typów danych obejmujących dane binarne. Jednak JavaScript nie oferuje żadnych rodzimych binarnych typów danych, z którymi można pracować. Najbliższe rozwiązanie to zastosowanie szalonych sztuczek wobec ciągów tekstowych. W Node zastosowano luźną implementację własnego typu danych Buffer, który działa jako fragment danych binarnych o stałej wielkości. To pozwala na uzyskanie dostępu na poziomie bajtów, co jest wymagane do implementacji innych protokołów. W tym podrozdziale zostaną poruszone wymienione poniżej zagadnienia: praca z buforami i danymi binarnymi, utworzenie serwera TCP,
utworzenie klienta TCP. Na początek przekonajmy się, jak Node współpracuje z danymi binarnymi.
13.2.1. Praca z buforami i danymi binarnymi to specjalny typ danych, który Node dostarcza programistom. Działa na zasadzie pojemnika dla niezmodyfikowanych danych binarnych o stałej wielkości. Bufor można potraktować jako odpowiednik funkcji języka C o nazwie malloc() lub słowa kluczowego new w C++. Bufory to bardzo szybkie i lekkie obiekty, są stosowane w podstawowym API Node. Na przykład domyślnie znajdują się w wartości zwrotnej zdarzeń data wszystkich klas Stream. Buffer
Node globalnie udostępnia konstruktor Buffer, zachęcając tym samym programistę do jego użycia jako rozszerzenia zwykłych typów JavaScript. Z programistycznego punktu widzenia bufory można traktować podobnie jak tablice. Różnice polegają na braku możliwości zmiany wielkości buforów, a ponadto bufory mogą przechowywać jedynie wartości w postaci liczb całkowitych z zakresu od 0 do 255. W ten sposób są idealnym rozwiązaniem do przechowywania danych binarnych praktycznie wszystkiego. Ponieważ bufory działają z niezmodyfikowanymi bajtami, można je wykorzystać do implementacji na niskim poziomie dowolnego protokołu.
Dane tekstowe kontra binarne Przyjmujemy założenie, że w pamięci chcesz przechowywać liczbę 121 234 869, używając do tego typu Buffer. Domyślnie Node przyjmuje założenie, że programista chce pracować z danymi tekstowymi w buforach. Dlatego też przekazanie ciągu tekstowego "121234869" konstruktorowi Buffer spowoduje alokację nowego obiektu Buffer wraz z zapisaną wartością w postaci ciągu tekstowego: var b = new Buffer("121234869"); console.log(b.length); 9 console.log(b);
W omawianym przykładzie wartością zwrotną jest dziewięciobajtowy obiekt Buffer. Wynika to z zapisu ciągu tekstowego w obiekcie Buffer za pomocą domyślnego, czytelnego dla człowieka kodowania tekstowego (UTF-8), w którym ciąg tekstowy zawiera po jednym bajcie dla każdego znaku. Node zawiera także funkcje pomocnicze przeznaczone do odczytu i zapisu danych binarnych (czytelnych dla komputera) w postaci liczb całkowitych. Są
one niezbędne do implementacji protokołów maszynowych, które wysyłają niezmodyfikowane typy danych (na przykład liczby całkowite, zmiennoprzecinkowe, o podwójnej precyzji itd.). Ponieważ w omawianym przykładzie chcemy przechowywać wartość liczbową, znacznie efektywniejszym rozwiązaniem będzie wykorzystanie funkcji pomocniczej o nazwie writeInt32LE() do zapisu liczby 121 234 869 jako czytelnych dla komputera danych binarnych (przyjęto założenie o użyciu kolejności bajtów little endian) w czterobajtowym obiekcie Buffer. Istnieją jeszcze inne odmiany funkcji pomocniczych Buffer: writeInt16LE() dla mniejszych liczb całkowitych; writeUInt32LE() dla wartości bez znaku; writeInt32BE() dla wartości stosującej kolejność bajtów big endian. Dostępnych funkcji jest znacznie więcej, a jeśli chcesz poznać wszystkie, to więcej informacji znajdziesz na stronie dokumentacji API obiektu Buffer (http://nodejs.org/docs/latest/api/buffer.html). W poniższym fragmencie kodu liczba została zapisana za pomocą binarnej funkcji pomocniczej writeInt32LE(): var b = new Buffer(4); b.writeInt32LE(121234869, 0); console.log(b.length); 4 console.log(b);
Przechowywanie w pamięci wartości w postaci binarnej liczby całkowitej zamiast ciągu tekstowego oznacza zmniejszenie wielkości danych o połowę — z 9 bajtów do 4. Na rysunku 13.2 pokazano schemat obu buforów i różnice między protokołem czytelnym dla człowieka (tekst) i czytelnym dla komputera (dane binarne).
Rysunek 13.2. Różnica na poziomie bajtów między liczbą 121 234 869 przedstawioną w postaci ciągu tekstowego a binarną liczbą całkowitą przedstawioną z zastosowaniem kolejności bajtów little endian
Niezależnie od używanego protokołu oferowana przez Node klasa w stanie obsłużyć poprawnie sposób przedstawienia danych.
Buffer
będzie
Kolejność bajtów. Pojęcie kolejności bajtów odnosi się do ich kolejności w sekwencji wielobajtowej. Kiedy bajty są w kolejności little endian, najmniej znaczący bajt (ang. Least Significant Byte, LSB) jest przechowywany jako pierwszy, a sekwencja bajtów jest odczytywana od prawej do lewej strony. Natomiast kolejność big endian oznacza, że jako pierwszy jest przechowywany najbardziej znaczący bajt (ang. Most Significant Byte, MSB), a sekwencja bajtów jest odczytywana od lewej do prawej strony. Node.js oferuje funkcje pomocnicze dla typów danych obsługujących obie kolejności, czyli little endian i big endian. Teraz możemy już wykorzystać wspomniane obiekty utworzenie serwera TCP i rozpoczęcie z nim pracy.
Buffer
w praktyce przez
13.2.2. Tworzenie serwera TCP Podstawowe API Node działa na niskim poziomie i udostępnia jedynie niezbędną infrastrukturę dla modułów zbudowanych na jego podstawie. Doskonałym przykładem jest tutaj moduł http, utworzony na podstawie modułu net i przeznaczony do implementacji protokołu HTTP. Inne protokoły, na przykład SMTP dla poczty elektronicznej lub FTP dla transferu plików, muszą być zaimplementowane również na bazie modułu net, ponieważ podstawowe API
Node nie implementuje żadnych protokołów działających na wysokim poziomie.
Zapis danych Moduł net oferuje interfejs niezmodyfikowanego gniazda TCP/IP do użycia w aplikacji. API przeznaczone do tworzenia serwera TCP jest bardzo podobne do używanego w trakcie tworzenia serwera HTTP: wywołujesz net.createServer() i podajesz nazwę funkcji wywołania zwrotnego, która będzie wykonana po każdym połączeniu. Podstawowa różnica związana z tworzeniem serwera TCP polega na tym, że funkcja wywołania zwrotnego pobiera tylko jeden argument (zwykle o nazwie socket) będący obiektem Socket. Natomiast podczas tworzenia serwera HTTP używane są argumenty req i res. Klasa Socket. Wymieniona klasa jest używana zarówno przez klienty, jak i serwer podczas stosowania modułu net w Node. To jest podklasa klasy Stream umożliwiająca odczyt (readable) i zapis (writeable). Oznacza to, że emituje zdarzenia data podczas odczytu danych wejściowych z gniazda, a także ma funkcje write() i end() przeznaczone do wysyłania danych wyjściowych. Spójrz teraz na obiekt net.Server oczekujący na połączenia, a następnie wykonujący funkcję wywołania zwrotnego. W omawianym przypadku logika zdefiniowana w funkcji wywołania zwrotnego powoduje po prostu przekazanie do gniazda komunikatu Witaj, świecie! i eleganckie zamknięcie połączenia: var net = require('net'); net.createServer(function (socket) { socket.write('Witaj, świecie!\r\n'); socket.end(); }).listen(1337); console.log('Serwer nasłuchiwanie na porcie 1337');
Uruchom serwer w celu przeprowadzenia pewnych testów: $ node server.js Serwer nasłuchiwanie na porcie 1337
Jeżeli za pomocą przeglądarki internetowej spróbujesz nawiązać połączenie z serwerem, próba zakończy się niepowodzeniem, ponieważ serwer nie potrafi się komunikować, używając protokołu HTTP, a jedynie przez niezmodyfikowany TCP. W celu nawiązania połączenia z serwerem i wyświetlenia komunikatu konieczne jest użycie odpowiedniego klienta TCP, na przykład w postaci polecenia netcat: $ netcat localhost 1337 Witaj, świecie
Doskonale! Teraz możemy wypróbować polecenie
telnet:
$ telnet localhost 1337 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Witaj, świecie! Connection closed by foreign host.
Polecenie telnet jest najczęściej używane w trybie interaktywnym, a więc oprócz komunikatu Witaj, świecie! wyświetla jeszcze inne, wygenerowane przez polecenie. Zgodnie z oczekiwaniami komunikat serwera jest wyświetlany tuż przed zamknięciem połączenia. Jak możesz się przekonać, zapis danych do gniazda jest całkiem łatwym zadaniem. Używasz wywołania write() oraz na końcu end(). Podczas przygotowywania odpowiedzi dla klienta omówione API celowo odpowiada API obiektu HTTP res.
Odczyt danych Serwery bardzo często stosują paradygmat żądanie-odpowiedź, w którym klient nawiązuje połączenie i natychmiast wysyła pewne żądanie. Serwer odczytuje żądanie, przetwarza odpowiedź, a następnie przekazuje ją do gniazda. Dokładnie w taki sposób działa protokół HTTP, a także większość innych protokołów sieciowych. Dlatego też bardzo ważne jest zrozumienie, jak dane są odczytywane. Jeżeli pamiętasz, jak można odczytać dane żądania z obiektu HTTP req, to odczyt danych z gniazda TCP nie powinien sprawić Ci żadnego problemu. Przy zachowaniu zgodności z możliwym do odczytu interfejsem Stream Twoje zadanie sprowadza się do nasłuchiwania zdarzeń data zawierających dane wejściowe, które zostały odczytane z gniazda: socket.on('data', function (data) { console.log('got "data"', data); });
Domyślnie nie jest stosowane żadne kodowanie znaków wobec socket, a więc argument data będzie egzemplarzem Buffer. Zwykle takie rozwiązanie jest oczekiwane (i dlatego zostało zdefiniowane jako domyślne). Jednak czasami znacznie wygodniejsze może się okazać wywołanie funkcji setEncoding() i nakazanie zdekodowania argumentu data na postać ciągu tekstowego, a nie bufora. Istnieje również możliwość nasłuchiwania zdarzenia end i tym samym ustalenia, że klient zamknął połączenie z gniazdem i nie należy już wysyłać kolejnych danych: socket.on('end', function () {
console.log('Gniazdo zostało zamknięte'); });
Bardzo łatwo można utworzyć klienta TCP, który będzie wyszukiwał ciąg tekstowy wersji danego serwera SSH przez po prostu oczekiwanie na pierwsze zdarzenie data: var net = require('net'); var socket = net.connect({ host: process.argv[2], port: 22 }); socket.setEncoding('utf8'); socket.once('data', function (chunk) { console.log('Wersja serwera SSH: %j', chunk.trim()); socket.end(); });
Teraz wypróbuj aplikację. Warto pamiętać o jednym: w tym uproszczonym przykładzie przyjęto założenie, że cały ciąg tekstowy zawierający informacje o wersji serwera zostanie dostarczony w jednym fragmencie. W większości przypadków takie rozwiązanie sprawdza się doskonale, ale dobry program powinien buforować dane wejściowe aż do napotkania znaku \n. Sprawdźmy, jakiej wersji serwera SSH używa serwis GitHub: $ node client.js github.com Wersja serwera SSH: "SSH-2.0-OpenSSH_5.5p1 Debian-6+squeeze1+github8"
Połączenie dwóch strumieni za pomocą socket.pipe()
Użycie funkcji pipe() (http://nodejs.org/api/stream.html#stream_readable_stream_pipe_destination_o w połączeniu z odczytywalnymi lub zapisywalnymi fragmentami obiektu Socket to również dobry pomysł. Jeżeli chcesz utworzyć prosty serwer TCP wyświetlający klientowi wszystko to, co zostało przekazane do serwera, wystarczy tylko pojedynczy wiersz kodu w funkcji wywołania zwrotnego: socket.pipe(socket);
Powyższy przykład pokazuje, że wystarczy tylko jeden wiersz kodu do zaimplementowania protokołu IETF Echo (http://tools.ietf.org/rfc/rfc862.txt). Co ważniejsze jednak, przykład pokazuje możliwość użycia funkcji pipe() w celu przekazywania danych do oraz z obiektu socket. Oczywiście z reguły stosowane będą znacznie pożyteczniejsze egzemplarze strumieni, na przykład systemu plików lub gzip.
Obsługa nieeleganckiego zamknięcia połączenia Ostatnią rzeczą, którą trzeba uwzględnić w serwerze TCP, jest konieczność przewidywania klientów zamykających połączenie, ale nie zamykających prawidłowo gniazda. W przypadku polecenia netcat tego rodzaju sytuacja
wystąpi po naciśnięciu klawiszy Ctrl+C w celu zamknięcia procesu zamiast naciśnięcia Ctrl+D w celu eleganckiego zamknięcia połączenia. Aby wykryć wspomnianą sytuację, należy nasłuchiwać zdarzeń close: socket.on('close', function () { console.log('Klient został rozłączony'); });
Jeżeli po zamknięciu połączenia trzeba będzie zamknąć jeszcze gniazdo, wtedy odpowiednie operacje należy przeprowadzać z poziomu zdarzenia close, a nie end, ponieważ drugie z wymienionych nie zostanie wywołane w przypadku nieeleganckiego zamknięcia połączenia.
Zebranie wszystkiego w całość Teraz zbierzemy wszystkie zdarzenia i utworzymy prosty serwer typu echo, który wyświetla w terminalu komunikaty po wystąpieniu różnych zdarzeń. Kod serwera został przedstawiony w listingu 13.5. Listing 13.5. Prosty serwer TCP, który klientowi wyświetla z powrotem wszystkie otrzymane od niego dane var net = require('net'); net.createServer(function (socket) { console.log('Nawiązano połączenie z gniazdem!'); socket.on('data', function (data) { Zdarzenie data może wystąpić wielokrotnie. console.log('Zdarzenie "data"', data); }); socket.on('end', function () { console.log('Zdarzenie "end"'); Zdarzenie end może wystąpić tylko raz dla gniazda. }); socket.on('close', function () { Zdarzenie close również może wystąpić tylko raz dla gniazda. console.log('Zdarzenie "close"'); }); socket.on('error', function (e) { Zdefiniowanie obsługi błędów, aby uniknąć niezgłoszonych wyjątków. console.log('Zdarzenie "error"', e); }); socket.pipe(socket); }).listen(1337);
Uruchom serwer i nawiąż z nim połączenie za pomocą polecenia netcat lub telnet, a następnie wypróbuj serwer. Kiedy w aplikacji klienta zaczniesz
wpisywać znaki z klawiatury, powinieneś widzieć wywołania console.log() dotyczące zdarzeń, o których komunikaty są przekazywane do standardowego wyjścia serwera. Skoro potrafisz już tworzyć działające na niskim poziomie serwery TCP w Node, to prawdopodobnie zastanawiasz się, jak w Node przygotować program klienta przeznaczony do interakcji ze wspomnianymi serwerami. Teraz zajmiemy się więc programem klienta.
13.2.3. Tworzenie klienta TCP Node to nie tylko oprogramowanie przeznaczone do tworzenia serwerów. Utworzenie programu klienta sieciowego w Node również jest łatwe i użyteczne. Podczas tworzenia niezmodyfikowanych połączeń z serwerem TCP kluczowe znaczenie ma funkcja net.connect(). Wymieniona funkcja akceptuje wartości host i port, a zwraca egzemplarz socket. Obiekt socket zwrócony przez net.connect() jest odłączony od serwera, a więc najczęściej należy nasłuchiwać zdarzeń connect przed rozpoczęciem wykonywania jakiejkolwiek operacji z gniazdem: var net = require('net'); var socket = net.connect({ port: 1337, host: 'localhost' }); socket.on('connect', function () { // Rozpoczęcie tworzenia "żądania". socket.write('HELO local.domain.name\r\n'); ... });
Kiedy egzemplarz socket nawiąże połączenie z serwerem, wtedy zaczyna zachowywać się jak egzemplarze socket, z którymi masz do czynienia w funkcji wywołania zwrotnego net.Server. Przystępujemy teraz do przygotowania prostej replikacji polecenia netcat. Kod tworzonego klienta przedstawiono w listingu 13.6. Ogólnie rzecz biorąc, program nawiązuje połączenie ze wskazanym zdalnym serwerem i potokuje standardowe wejście z programu do gniazda, a następnie potokuje odpowiedź gniazda do standardowego wyjścia programu. Listing 13.6. Podstawowa replika polecenia netcat przygotowana za pomocą Node var net = require('net'); var host = process.argv[2]; var port = Number(process.argv[3]);ó Przetworzenie argumentów host i port podanych w powłoce. var socket = net.connect(port, host); Utworzenie egzemplarza socket i rozpoczęcie
procedury nawiązywania połączenia z serwerem. socket.on('connect', function () { Obsługa zdarzenia connect po nawiązaniu połączenia z serwerem. process.stdin.pipe(socket); Potokowanie standardowego wejścia procesu do gniazda. socket.pipe(process.stdout); Potokowanie danych gniazda do standardowego wyjścia procesu. process.stdin.resume(); Wywołanie resume() dla danych wejściowych, aby rozpocząć odczyt danych. }); socket.on('end', function () { Wstrzymanie danych ze standardowego wejścia po wystąpieniu zdarzenia end. process.stdin.pause(); });
Przygotowanego klienta można wykorzystać do nawiązywania połączeń z utworzonymi wcześniej serwerami TCP. Jeżeli jesteś fanem „Gwiezdnych wojen”, spróbuj uruchomić przedstawioną replikację polecenia netcat wraz z poniższym argumentem, aby otrzymać efekt specjalny: $ node netcat.js towel.blinkenlights.nl 23
Usiądź wygodnie i ciesz się danymi wyjściowymi, które pokazano na rysunku 13.3. Zasłużyłeś na przerwę.
Rysunek 13.3. Nawiązanie połączenia z serwerem ASCII Star Wars za pomocą skryptu netcat.js
To już wszystko w zakresie tworzenia za pomocą Node działających na niskim
poziomie serwerów TCP i klientów. Moduł net oferuje proste, choć obszerne API, natomiast klasa Socket stosuje zgodnie z oczekiwaniami odczytywalny i zapisywalny interfejs Stream. W zasadzie moduł net to próbka podstawowych możliwości oferowanych przez Node. Zmienimy narzędzia raz jeszcze i przyjrzymy się, w jaki sposób podstawowe API Node pozwala na współpracę ze środowiskiem procesu oraz na zbieranie informacji dotyczących środowiska uruchomieniowego i systemu operacyjnego.
13.3. Narzędzia przeznaczone do pracy z systemem operacyjnym Bardzo często zachodzi potrzeba współpracy ze środowiskiem, w którym działa Node. Może się to wiązać na przykład z koniecznością sprawdzenia zmiennych środowiskowych w celu włączenia rejestracji danych podczas procesu debugowania. Kolejny przykład to implementacja sterownika dżojstika w systemie Linux przy użyciu działających na niskim poziomie funkcji modułu fs przeznaczonych do pracy z /dev/js0 (plik urządzenia dla dżojstika). Jeszcze inny przykład to konieczność uruchomienia procesu potomnego, takiego jak php, do kompilacji starszego skryptu PHP. Wszystkie wymienione rodzaje operacji wymagają użycia pewnego podstawowego API Node. Oto komponenty, które zostaną omówione w tym podrozdziale: Obiekt globalny o nazwie process. Zawiera informacje o bieżącym procesie, takie jak użyte argumenty oraz aktualnie ustawione zmienne środowiskowe. Moduł fs. Zawiera działające na wysokim poziomie klasy ReadStream i WriteStream, które powinieneś już znać. Ponadto obejmuje działające na niskim poziomie funkcje, które zostaną omówione w tym podrozdziale. Moduł child_process. Zawiera działające zarówno na wysokim, jak i niskim poziomie interfejsy przeznaczone do tworzenia procesów potomnych oraz obsługi specjalnego sposobu tworzenia egzemplarzy node wraz z dwukierunkowym kanałem przekazywania informacji. Obiekt process jest jednym z API najczęściej wykorzystywanych przez programy i dlatego zapoznamy się z nim na początku.
13.3.1. Obiekt process, czyli globalny wzorzec
Singleton Każdy proces Node posiada globalny obiekt process współdzielony przez wszystkie moduły. Wymieniony obiekt zawiera użyteczne informacje o procesie i kontekście, w którym działa. Na przykład argumenty podane Node podczas uruchamiania aktualnego skryptu są dostępne jako process.argv, natomiast zmienne środowiskowe można pobrać i ustawić za pomocą process.env. Jednak najbardziej interesującą cechą obiektu process jest to, że stanowi on egzemplarz EventEmitter i emituje zdarzenia specjalne, takie jak exit i uncaughtException. Obiekt process ma wiele możliwości, a pewne API nieomówione w tym punkcie zostanie przedstawione w dalszej części rozdziału. W tym punkcie koncentrujemy się na następujących zagadnieniach: Użycie process.env do pobierania i ustawiania zmiennych środowiskowych. Nasłuchiwanie zdarzeń specjalnych emitowanych przez obiekt process, na przykład exit i uncaughtException. Nasłuchiwanie emitowanych przez obiekt process zdarzeń sygnałów, na przykład SIGUSR2 i SIGKILL.
Użycie process.env do pobierania i ustawiania zmiennych środowiskowych Zmienne środowiskowe to doskonałe rozwiązanie pozwalające na zmianę sposobu działania programu lub modułu. Tego rodzaju zmienne można wykorzystać na przykład do skonfigurowania serwera i wskazania portu, na którym ma on nasłuchiwać. Z kolei system operacyjny może ustawić zmienną TMPDIR w celu określenia lokalizacji przeznaczonej na generowane przez program pliki tymczasowe, które później mogą być usunięte. Zmienne środowiskowe. Być może nie znasz jeszcze zastosowania zmiennych środowiskowych, powinieneś jednak wiedzieć, że to pary typu klucz--wartość, które mogą być używane przez dowolny proces do zmiany sposobu jego zachowania. Wszystkie systemy operacyjne używają zmiennej środowiskowej PATH do zdefiniowania listy ścieżek dostępu sprawdzanych podczas wyszukiwania programu po jego nazwie (na przykład pełna ścieżka dostępu polecenia ls to /bin/ls). Przyjmujemy założenie, że chcesz włączyć rejestrację danych w trybie debugowania podczas tworzenia modułu lub usuwania z niego błędów. Wspomniana rejestracja ma być niedostępna w trakcie zwykłego użycia modułu, ponieważ może irytować jego użytkowników. Doskonałym rozwiązaniem będzie użycie zmiennych środowiskowych. Można ustalić, czy została ustawiona
z mi e nna DEBUG. Odbywa się przedstawiono w listingu 13.7.
to
przez
sprawdzenie
process.env.DEBUG,
jak
Listing 13.7. Zdefiniowanie funkcji debug() na podstawie zmiennej środowiskowej DEBUG var debug; if (process.env.DEBUG) { Zdefiniowanie działania funkcji na podstawie wartości process.env.DEBUG. debug = function (data) { console.error(data); Gdy zmienna DEBUG jest ustawiona, funkcja debug() będzie przekazywać argument do standardowego wyjścia błędów. }; } else { debug = function () {}; Gdy zmienna DEBUG nie jest ustawiona, funkcja debug() będzie pusta i nie wykona żadnej operacji. } debug('To jest wywołanie debugujące'); Wywołanie funkcji debug() w różnych miejscach kodu. console.log('Witaj, świecie!'); debug('To jest inne wywołanie debugujące');
Jeżeli przedstawiony skrypt zostanie uruchomiony w zwykły sposób (bez ustawienia zmiennej środowiskowej process.env.DEBUG), wówczas wywołanie debug() nie da żadnego efektu, ponieważ wywoływana jest pusta funkcja: $ node debug-mode.js Witaj, świecie!
Aby przetestować tryb debugowania, konieczne jest ustawienie zmiennej środowiskowej process.env.DEBUG. Najłatwiejszym sposobem jest dołączenie DEBUG=1 do polecenia uruchamiającego egzemplarz Node. W trybie debugowania wywołanie funkcji debug() spowoduje nie tylko wygenerowanie standardowych danych wyjściowych, ale również umieszczenie odpowiedniego komunikatu w konsoli. To bardzo użyteczne rozwiązanie pozwalające na zbieranie danych diagnostycznych podczas rozwiązywania problemów związanych z kodem: $ DEBUG=1 node debug-mode.js To jest wywołanie debugujące Witaj, świecie! To jest inne wywołanie debugujące
Opracowany przez T.J. Holowaychuka moduł debug (https://github.com/visionmedia/debug) hermetyzuje dokładnie tę samą funkcjonalność, a ponadto zawiera wiele funkcji dodatkowych. Jeżeli lubisz przedstawioną tutaj technikę debugowania, zdecydowanie powinieneś zapoznać się z modułem debug.
Zdarzenia specjalne emitowane przez obiekt proces Istnieją dwa zdarzenia specjalne emitowane przez obiekt process: exit. To zdarzenie jest emitowane tuż przed zakończeniem działania przez proces. uncaughtException. To zdarzenie jest emitowane za każdym razem, gdy wystąpi nieobsłużony błąd. Zdarzenie exit ma istotne znaczenie dla każdej aplikacji, która musi wykonać jakiekolwiek operacje przed zakończeniem działania programu, na przykład usunąć obiekt lub umieścić w konsoli ostateczny komunikat. Warto pamiętać, że zdarzenie exit jest emitowane już po zatrzymaniu pętli zdarzeń, a więc nie masz możliwości uruchomienia jakiegokolwiek zadania asynchronicznego w trakcie zdarzenia exit. Kod wyjścia jest przekazywany jako pierwszy argument, kod 0 oznacza sukces. Utworzymy teraz skrypt, który nasłuchuje zdarzenia komunikat Koniec pracy...:
exit,
a następnie generuje
process.on('exit', function (code) { console.log('Koniec pracy...'); });
Inne zdarzenie specjalne emitowane przez obiekt process to uncaughtException. W perfekcyjnym programie nigdy nie wystąpią nieobsłużone wyjątki, ale w rzeczywistości lepiej być przygotowanym na taką ewentualność, niż później żałować. Jedynym argumentem przekazywanym do zdarzenia uncaughtException jest nieprzechwycony obiekt Error. Kiedy nie ma innych obiektów nasłuchujących zdarzeń „błędu”, wszelkie nieprzechwycone błędy doprowadzą do awarii procesu (to jest zachowanie domyślne w większości aplikacji). Jednak istnienie co najmniej jednego obiektu nasłuchującego daje możliwość podjęcia dowolnego działania po przechwyceniu błędu. Node nie zakończy automatycznie działania, choć takie rozwiązanie jest konieczne we własnych wywołaniach zwrotnych. Dokumentacja Node.js wyraźnie ostrzega, że każde użycie zdarzenia uncaughtException powinno zawierać wywołanie process.exit() w wywołaniu zwrotnym, ponieważ w przeciwnym razie aplikacja pozostanie w niezdefiniowanym stanie, co jest złym rozwiązaniem. Przygotujemy teraz obiekt nasłuchujący zdarzeń uncaughtException, a następnie zgłosimy nieprzechwycony błąd, aby zobaczyć, jak to wygląda w praktyce: process.on('uncaughtException', function (err) { console.error('Wystąpił nieprzechwycony wyjątek:', err.message);
process.exit(1); }); throw new Error('Nieprzechwycony wyjątek');
Po wystąpieniu nieoczekiwanego błędu kod będzie w stanie go przechwycić i przeprowadzić odpowiednie operacje przed zakończeniem działania procesu.
Przechwytywanie sygnałów wysyłanych procesowi W systemie UNIX wprowadzono koncepcję sygnałów, które stanowią podstawową formę komunikacji międzyprocesowej (ang. Interprocess Communication, IPC). Wspomniane sygnały są bardzo proste i pozwalają na użycie jedynie pewnego stałego zestawu nazw, a ponadto są przekazywane bez argumentów. Node posiada zdefiniowane domyślne zachowanie dla kilku wymienionych poniżej sygnałów: SIGINT. Sygnał wysyłany przez powłokę po naciśnięciu klawiszy Ctrl+C. Domyślne zachowanie Node powoduje zakończenie działania procesu. To zachowanie można zmienić za pomocą pojedynczego nasłuchującego sygnału SIGINT w obiekcie process. SIGUSR1. Po otrzymaniu tego sygnału Node „wejdzie” do wbudowanego debugera. SIGWINCH. Ten sygnał jest wysyłany przez powłokę po zmianie wielkości okna terminalu. Node zeruje wartości process.stdout.rows i process.stdout.columns i emituje zdarzenie resize po otrzymaniu omawianego sygnału. Są to trzy sygnały domyślne obsługiwane przez Node. Istnieje możliwość nasłuchiwania w obiekcie process dowolnego z omówionych sygnałów i wykonywania funkcji wywołania zwrotnego. Przyjmujemy założenie, że utworzyłeś serwer, ale po naciśnięciu klawiszy Ctrl+C następuje zakończenie jego działania. Nie jest to eleganckie, a dodatkowo wszelkie połączenia oczekujące zostaną po prostu usunięte. Rozwiązaniem jest przechwycenie sygnału SIGINT, wstrzymanie akceptowania nowych połączeń, umożliwienie zakończenia istniejących i dopiero wówczas zakończenie działania procesu. Odbywa się to przez nasłuchiwanie process.on('SIGINT', ...). Nazwa emitowanego zdarzenia jest taka sama jak nazwa sygnału: process.on('SIGINT', function () { console.log('Przechwycono naciśnięcie Ctrl+C!'); server.close();
});
Teraz po naciśnięciu kombinacji Ctrl+C na klawiaturze sygnał SIGINT zostanie wysłany z powłoki do procesu Node i zamiast natychmiastowego zakończenia działania procesu spowoduje wykonanie zdefiniowanego wywołania zwrotnego. Ponieważ domyślne zachowanie w większości aplikacji to zakończenie działania procesu, zwykle dobrym pomysłem jest zrobienie tego samego we własnej procedurze obsługi sygnału SIGINT po przeprowadzeniu wszelkich niezbędnych operacji. W omawianym przykładzie wystarczające jest wstrzymanie akceptacji nowych połączeń przez serwer. Pomimo braku prawidłowych sygnałów takie rozwiązanie działa również w Windows, ponieważ Node obsługuje odpowiednie akcje Windows i symuluje sztuczne sygnały w Node. Tę samą technikę można zastosować do przechwycenia dowolnego sygnału systemu UNIX wysyłanego do procesu Node. Lista sygnałów UNIX została wymieniona w artykule Wikipedii na stronie http://en.wikipedia.org/wiki/Unix_signal#POSIX_signals. Niestety, ogólnie rzecz biorąc, poza kilkoma symulowanymi (SIGINT, SIGBREAK, SIGHUP i SIGWINCH) sygnały nie działają w systemie Windows.
13.3.2. Użycie modułu filesystem M o duł fs zapewnia funkcje niezbędne do współpracy z systemem plików komputera, w którym uruchomiono Node. Większość funkcji to rodzaj mapowania typu „jeden do jednego” dla ich odpowiedników w języku C, ale istnieją również działające na wysokim poziomie abstrakcje, takie jak fs.readFile(), fs.writeFile(), klasy fs.ReadStream i fs.WriteStream, które zostały opracowane na bazie open(), read(), write() i close(). Niemal wszystkie funkcje działające na niskim poziomie są używane identycznie jak ich odpowiedniki w języku C. W rzeczywistości większość dokumentacji Node odsyła Cię do odpowiednich stron podręcznika systemowego man wyjaśniających działanie dopasowanych funkcji C. Funkcje działające na niskim poziomie można bardzo łatwo zidentyfikować, ponieważ zawsze mają synchroniczny odpowiednik. Na przykład fs.stat() i fs.statSync() to działające na niskim poziomie odpowiedniki funkcji języka C o nazwie stat(). Funkcje synchroniczne w Node.js. Jak już wiesz, API Node to przede wszystkim funkcje asynchroniczne, które nigdy nie blokują pętli zdarzeń. Dlaczego więc dołączone są synchroniczne wersje funkcji systemu plików? Odpowiedź jest prosta: funkcja require() w Node jest synchroniczna i została zaimplementowana za pomocą funkcji modułu fs, a więc synchroniczne odpowiedniki są niezbędne. Funkcje synchroniczne w Node powinny być używane jedynie podczas uruchamiania lub początkowego wczytania modułu, a nigdy później.
Teraz zapoznamy Cię z przykładami pracy z systemem plików.
Przenoszenie pliku Na pozór prostym i dość często przeprowadzanym zadaniem podczas pracy z systemem plików jest przenoszenie pliku między katalogami. Na platformie UNIX do tego celu używa się polecenia mv, natomiast w Windows — polecenia move. Wydaje się, że przeprowadzenie tej operacji w Node powinno być proste. Cóż, jeśli przejrzysz dokumentację modułu fs (http://nodejs.org/api/fs.html), to zauważysz brak funkcji w stylu fs.move(). Jednak istnieje funkcja fs.rename(), która tak naprawdę wykonuje to samo zadanie. Doskonale! Nie tak szybko… Funkcja fs.rename() jest mapowana bezpośrednio na funkcję C o nazwie rename(). Jednak wadą wymienionej funkcji jest to, że nie działa między dwoma różnymi urządzeniami fizycznymi (na przykład między dwoma dyskami twardymi). Dlatego też poniższy fragment kodu nie działa zgodnie z oczekiwaniami i spowoduje zgłoszenie błędu typu EXDEV: fs.rename('C:\\hello.txt', 'D:\\hello.txt', function (err) { // err.code === 'EXDEV' });
Co można zrobić w takiej sytuacji? Nadal można utworzyć nowy plik na dysku D:\, odczytać zawartość pliku z dysku C:\ i skopiować ją przez sieć. Można przygotować zoptymalizowaną funkcję move() wywołującą fs.rename(), gdy istnieje taka możliwość, i kopiującą plik między urządzeniami przy użyciu fs.ReadStream i fs.WriteStream. Przykładową implementację funkcji move() przedstawiono w listingu 13.8. Listing 13.8. Funkcja move(), która zmienia nazwę pliku (o ile to możliwe) lub go kopiuje var fs = require('fs'); module.exports = function move (oldPath, newPath, callback) { fs.rename(oldPath, newPath, function (err) { Wywołanie fs.rename() w nadziei, że zadziała prawidłowo. if (err) { if (err.code === 'EXDEV') { W przypadku wystąpienia błędu EXDEV trzeba zastosować kopiowanie. copy(); } else { callback(err); Niepowodzenie wywołania i zgłoszenie tego faktu wywołującemu, jeśli wystąpi inny rodzaj błędu. } return; }
callback(); Jeżeli funkcja fs.rename() zadziałała prawidłowo, w tym momencie zadanie jest wykonane. }); function copy() { var readStream = fs.createReadStream(oldPath); Odczyt pliku źródłowego i jego potokowanie do pliku docelowego. var writeStream = fs.createWriteStream(newPath); readStream.on('error', callback); writeStream.on('error', callback); readStream.on('close', function () { fs.unlink(oldPath, callback); Usunięcie pliku źródłowego po jego wcześniejszym skopiowaniu. }); readStream.pipe(writeStream); } }
Moduł można przetestować bezpośrednio w interfejsie REPL Node, na przykład: $ node > var move = require('./copy') > move('copy.js', 'copy.js.bak', function (err) { if (err) throw err })
Zwróć uwagę, że funkcja copy() działa jedynie z plikami, a nie z katalogami. Aby działała również z katalogami, konieczne jest sprawdzenie w pierwszej kolejności, czy podana ścieżka dostępu prowadzi do katalogu. Jeśli tak, wówczas wywoływane są funkcje fs.readdir() i fs.mkdir(). Obsługę katalogów przez funkcję copy() możesz teraz zaimplementować samodzielnie. Kody błędów modułu fs. Moduł fs zwraca standardowe nazwy systemu UNIX dla kodów błędów systemu plików (http://www.gnu.org/software/libc/manual/html_node/Error-Codes.html), a więc znajomość wspomnianych nazw jest wymagana. Te nazwy są normalizowane przez bibliotekę libuv nawet w Windows, a więc aplikacja musi sprawdzać za każdym razem tylko jeden kod błędu. Zgodnie z informacjami zamieszczonymi w dokumentacji GNU, błąd EXDEV występuje podczas wykrycia „próby utworzenia nieprawidłowego dowiązania między systemami plików”.
Monitorowanie katalogu lub pliku pod kątem zmian Funkcja fs.watchFile() jest dostępna od dawna. Na pewnych platformach jej działanie jest kosztowne, ponieważ stosuje próbkowanie w celu sprawdzenia, czy plik został zmieniony. Oznacza to wywołanie stat() dla pliku, odczekanie krótkiego zakresu czasu, następnie ponowne wywołanie stat(). Cały proces jest przeprowadzany nieustannie w pętli. Po wykryciu zmiany w pliku następuje
wywołanie zdefiniowanej funkcji. Przyjmujemy założenie, że tworzony jest moduł rejestrujący zmiany wprowadzane w pliku dziennika zdarzeń system.log. Konieczne jest zdefiniowanie funkcji wywołania zwrotnego, która będzie wykonywana po wykryciu modyfikacji wymienionego pliku: var fs = require('fs'); fs.watchFile('/var/log/system.log', function (curr, prev) { if (curr.mtime.getTime() !== prev.mtime.getTime()) { console.log('Plik "system.log" został zmodyfikowany'); } });
Zmienne curr i prev to bieżący i poprzedni obiekt fs.Stat — powinny mieć różne znaczniki czasu dla tego samego dołączonego do nich pliku. W omawianym przykładzie porównywane są wartości mtime, ponieważ moduł ma informować jedynie o modyfikacji pliku, a nie o uzyskaniu do niego dostępu. F unk c j a fs.watch() została wprowadzona w Node v0.6. Jak wcześniej wspomniano, jest bardziej zoptymalizowana niż fs.watchFile(), ponieważ podczas monitorowania plików używa rodzimego dla platformy API powiadomień o zmianie pliku. Dlatego też wymieniona funkcja może monitorować katalog pod kątem zmian w dowolnym ze znajdujących się w nim plików. W praktyce funkcja fs.watch() jest mniej niezawodna niż fs.watchFile(), co wynika z różnic między stosowanymi przez poszczególne platformy mechanizmami monitorowania plików. Na przykład parametr filename nie jest zgłaszany w systemie OS X podczas monitorowania katalogu, a firma Apple może to zmienić w kolejnych wydaniach systemu OS X. W dokumentacji Node znajdziesz listę tego rodzaju zastrzeżeń (http://nodejs.org/api/fs.html#fs_caveats).
Użycie opracowanych przez społeczność modułów fstream i filed Jak się przekonałeś, moduł fs — podobnie jak całe podstawowe API Node — działa jedynie na niskim poziomie. Oznacza to sporo miejsca na innowacje i tworzenie abstrakcji na jego podstawie. Kolekcja aktywnych modułów Node w repozytorium npm zwiększa się każdego dnia i jak możesz się domyślać, istnieją również pewne doskonałe rozwiązania zbudowane w oparciu o moduł fs. Na przykład opracowany przez Isaaca Schluetera moduł fstream (https://github.com/isaacs/fstream) to jeden z podstawowych komponentów samego menedżera npm. Wymieniony moduł jest interesujący, ponieważ zaistniał jako część menedżera npm, a następnie został wyodrębniony do postaci samodzielnego modułu, ponieważ jego funkcjonalność ogólnego przeznaczenia okazała się użyteczna dla wielu różnego rodzaju narzędzi powłoki i skryptów
przeznaczonych dla administratorów systemów. Jedną z fantastycznych funkcji m o d u ł u fstream jest bezproblemowa obsługa uprawnień i dowiązań symbolicznych, czym moduł zajmuje się domyślnie podczas kopiowania plików i katalogów. Dzięki modułowi fstream można wykonać operację odpowiadającą wywołaniu cp rp katalog_źródłowy katalog_docelowy (rekurencyjne kopiowanie katalogu wraz z zawartością, a także transfer właściciela i uprawnień) przez po prostu potokowanie egzemplarza Reader do egzemplarza Writer. W przedstawionym poniżej fragmencie kodu wykorzystano również możliwości modułu fstream w zakresie filtrowania w celu warunkowego wykluczania plików na podstawie funkcji wywołania zwrotnego: fstream .Reader("ścieżka/do/katalogu") .pipe(fstream.Writer({ path: "ścieżka/do/innego/katalogu", filter: isValid ) // Sprawdzenie, czy plik jest przeznaczony do zapisania // oraz czy ewentualnie może być nadpisany. function isValid () { // Zignorowanie plików tymczasowych edytorów tekstu, na przykład TextMate. return this.path[this.path.length - 1] !== '~'; }
Opracowany przez Mikeala Rogersa moduł filed (https://github.com/mikeal/filed) to inny ważny moduł, ponieważ Mikeal jest również autorem niezwykle popularnego modułu request. Wymienione moduły spopularyzowały nowy rodzaj kontroli przepływu egzemplarzy Stream: nasłuchiwanie zdarzenia pipe i podjęcie odpowiedniego działania na podstawie potokowanych danych (lub miejsca ich potokowania). Aby poznać potężne możliwości związane z przedstawionym podejściem, spójrz, jak za pomocą modułu filed i jednego wiersza kodu można zmienić zwykły serwer HTTP na w pełni wyposażony serwer plików statycznych: http.createServer(function (req, res) { req.pipe(filed('ścieżka/do/plików/statycznych')).pipe(res); });
Powyższy kod zajmuje się wysłaniem nagłówka Content-Length wraz z odpowiednimi nagłówkami dotyczącymi buforowania. W przypadku gdy przeglądarka internetowa posiada buforowaną wersję pliku, na żądanie HTTP moduł filed udziela odpowiedzi o kodzie 304 (niezmodyfikowany), pomijając kroki związane z otworzeniem i odczytem pliku z dysku. To są te rodzaje optymalizacji, które działają ze zdarzeniem pipe, ponieważ egzemplarz filed ma
dostęp do obiektów req i res żądania HTTP. W ten sposób przedstawiliśmy dwa przykłady opracowanych przez społeczność dobrych modułów rozszerzających moduł bazowy fs o nowe możliwości i udostępniających piękne API. Warto pamiętać, że modułów jest znacznie więcej. Polecenie npm search do doskonały sposób na wyszukiwanie opublikowanych modułów przeznaczonych do wykonywania określonego zadania. Przyjmujemy założenie, że chcesz znaleźć jeszcze inny moduł ułatwiający proces kopiowania plików z jednej lokalizacji do drugiej. Wydanie polecenia npm search copy powinno przynieść wiele użytecznych wyników. Kiedy znajdziesz opublikowany moduł wyglądający interesująco, zawsze możesz wydać polecenie npm info nazwa-modułu, aby uzyskać więcej informacji na temat modułu, między innymi jego opis, adres strony domowej i opublikowane wersje. Pamiętaj o jednym: w przypadku danego zadania istnieje duże prawdopodobieństwo, że ktoś już próbował rozwiązać problem za pomocą modułu npm, i dlatego zawsze przed przystąpieniem do tworzenia własnego kodu od podstaw sprawdź, czy nie istnieje odpowiedni moduł w npm.
13.3.3. Tworzenie procesów zewnętrznych Node oferuje moduł child_process przeznaczony do tworzenia procesów potomnych w ramach serwera Node lub skryptu. Istnieją dwa API przeznaczone do wymienionego celu: działające na wysokim poziomie exec() oraz działające na niskim poziomie spawn(). W zależności od potrzeb każde z wymienionych API może być odpowiednie. Ponadto mamy jeszcze udostępniany przez samo Node specjalny sposób tworzenia procesu potomnego za pomocą wbudowanego kanału IPC o nazwie fork(). Wszystkie te funkcje są przeznaczone do użycia w różnych przypadkach: cp.exec() — działające na wysokim poziomie API przeznaczone do tworzenia poleceń i buforowania wyniku operacji w wywołaniu zwrotnym. cp.spawn() — działające na niskim poziomie API przeznaczone do tworzenia pojedynczych poleceń w obiekcie ChildProcess. cp.form() — specjalny sposób tworzenia dodatkowego procesu Node za pomocą wbudowanego kanału IPC. Powyższe API przeanalizujemy po kolei. Wady i zalety procesów potomnych. Istnieją wady i zalety użycia procesów potomnych. Oczywistą wadą jest konieczność instalacji w komputerze użytkownika uruchamianego programu, co czyni go zależnym od aplikacji.
Alternatywne rozwiązanie polega na użyciu języka JavaScript do wykonania zadań przeznaczonych dla procesu potomnego. Dobrym przykładem jest tutaj polecenie npm, które początkowo używało systemowego polecenia tar do rozpakowywania pakietów Node. Takie rozwiązanie powodowało problemy wynikające z niezgodności między wersjami polecenia tar, a ponadto rzadko się zdarzało, aby w systemie Windows było zainstalowane polecenie tar. Wymienione czynniki spowodowały opracowanie narzędzia node-tar (https://github.com/isaacs/node-tar), które całkowicie utworzono w języku JavaScript. Nie są więc używane żadne procesy potomne. Z drugiej strony, użycie aplikacji zewnętrznej pozwala programiście na wykorzystanie użytecznego programu utworzonego w zupełnie innym języku programowania. Na przykład gm (http://aheckmann.github.io/gm/) to moduł używający potężnych bibliotek GraphicMagick i ImageMagick do przeprowadzania wszelkich operacji na obrazach oraz ich konwersji w aplikacji Node.
Buforowanie za pomocą cp.exec() wyników działania polecenia Działające na wysokim poziomie API cp.exec() jest użyteczne podczas wywoływania polecenia — programistę interesuje jedynie wynik końcowy, a nie kwestie związane z dostępem do danych z poziomu potomnych strumieni standardowego wejścia, gdy się pojawiają. To API pozwala na podanie pełnych sekwencji poleceń, między innymi zawierających wiele procesów potokowanych jeden do drugiego. Jednym z dobrych przykładów użycia API cp.exec() jest akceptowanie przeznaczonych do wykonania poleceń użytkownika. Przyjmujemy założenie o tworzeniu bota IRC. Polecenie ma zostać wykonane, gdy użytkownik wprowadzi tekst zaczynający się od kropki. Na przykład jeśli jako wiadomość IRC użytkownik wpisze .ls, wówczas nastąpi wykonanie polecenia ls i wyświetlenie danych wyjściowych w pokoju IRC. Jak pokazano w listingu 13.9, konieczne jest ustawienie pewnej opcji timeout, aby nigdy nie kończące się procesy były automatycznie zamykane po upłynięciu pewnego czasu. Listing 13.9. Użycie cp.exec() do uruchamiania za pomocą IRC bot poleceń podawanych przez użytkownika var cp = require('child_process'); room.on('message', function (user, message) { Obiekt room przedstawia połączenie z pokojem IRC (z pewnego teoretycznego modułu IRC). if (message[0] === '.') { Zdarzenie message jest emitowane przez każdą wiadomość IRC wysyłaną do pokoju. var command = message.substring(1); cp.exec(command, { timeout: 15000 }, Sprawdzenie, czy treść wiadomości rozpoczyna się od kropki.
function (err, stdout, stderr) { if (err) { Utworzenie procesu potomnego i buforowanie w wywołaniu zwrotnym wyniku przez Node. Czas upływu ważności wynosi 15 sekund. room.say( 'Błąd podczas wykonywania polecenia "' + command + '": ' + err.message ); room.say(stderr); } else { room.say('Wykonywanie polecenia zakończone: ' + command); room.say(stdout); } } ); } });
Repozytorium npm zawiera jeszcze inne dobre moduły implementujące protokół IRC. Jeżeli faktycznie chcesz utworzyć bota IRC, powinieneś skorzystać z jednego z istniejących modułów (do popularnych zaliczają się irc i irc-js). Kiedy zachodzi potrzeba buforowania danych wyjściowych polecenia, ale chciałbyś użyć Node do automatycznego neutralizowania argumentów, wówczas możesz skorzystać z funkcji execFile(). Wymieniona funkcja pobiera cztery argumenty zamiast trzech. Przekazujesz jej pliki wykonywalne przeznaczone do uruchomienia oraz tablicę argumentów, z którymi mają być wykonane. Takie rozwiązanie jest użyteczne podczas przyrostowego tworzenia argumentów przeznaczonych do użycia przez proces potomny: cp.execFile('ls', [ '-l', process.cwd() ], function (err, stdout, stderr) { if (err) throw err; console.error(stdout); });
Tworzenie poleceń za pomocą interfejsu Stream i cp.spawn() Oferowane przez Node i działające na niskim poziomie API przeznaczone do tworzenia procesów potomnych to cp.spawn(). Ta funkcja różni się od cp.exec(), ponieważ zwraca obiekt ChildProcess, z którym można współpracować. Zamiast przekazywać cp.spawn() pojedynczą funkcję wywołania zwrotnego, gdy proces potomny zakończy działanie, cp.spawn() pozwala na interakcje z poszczególnymi strumieniami standardowego wejścia procesu potomnego. Najprostszy sposób użycia
cp.spawn()
przedstawia się następująco:
var child = cp.spawn('ls', [ '-l' ]); // Standardowe wyjście to zwykły egzemplarz Stream, który emituje zdarzenia 'data', // 'end' itd. child.stdout.pipe(fs.createWriteStream('ls-result.txt')); child.on('exit', function (code, signal) { // Zdarzenie emitowane po zakończeniu działania przez proces potomny. });
Pierwszy argument wskazuje program do uruchomienia. To może być pojedyncza nazwa programu, która zostanie wyszukana w lokalizacjach wskazanych przez aktualną wartość zmiennej środowiskowej PATH lub też bezwzględna ścieżka dostępu do programu. Drugim argumentem jest tablica ciągów tekstowych będących argumentami, z którymi zostanie uruchomiony proces. Domyślnie obiekt ChildProcess zawiera trzy wbudowane egzemplarze Stream, z którymi tworzone skrypty będą współpracowały: child.stdin — to zapisywalny obiekt Stream, który przedstawia standardowe wejście procesu potomnego. child.stdout — to odczytywalny obiekt Stream, który przedstawia standardowe wyjście procesu potomnego. child.stderr — to odczytywalny obiekt Stream, który przedstawia standardowe wyjście błędów procesu potomnego. Wymienione strumienie można wykorzystać w dowolny sposób, na przykład potokować je do pliku lub gniazda bądź też do innego rodzaju zapisywalnego strumienia. Jeśli chcesz, możesz je nawet zupełnie zignorować. Inne interesujące zdarzenie występujące w obiekcie ChildProcess to exit, które jest wywoływane po zakończeniu działania procesu i powiązanych z nim obiektów strumieni. Dobrym przykładem modułu zapewniającym abstrakcję użycia cp.spawn() w postaci użytecznej funkcjonalności jest node-cgi (https://github.com/TooTallNate/node-cgi). Pozwala on na ponowne użycie starych skryptów CGI (ang. Common Gateway Interface) w serwerach HTTP Node. CGI był standardem udzielania odpowiedzi na żądania HTTP przez wywoływanie skryptów CGI jako procesów potomnych serwera HTTP wraz ze specjalnymi zmiennymi środowiskowymi, które opisywały żądanie. Jako przykład poniżej przedstawiono skrypt CGI używający sh (powłoki) jako interfejsu CGI: #!/bin/sh echo "Status: 200" echo "Content-Type: text/plain"
echo echo "Witaj, $QUERY_STRING"
Jeżeli skrypt zostanie zapisany pod nazwą hello.cgi (nie zapomnij o wydaniu polecenia chmod +x hello.cgi, aby plik był wykonywalny), wówczas łatwo można wywołać skrypt jako logikę udzielania odpowiedzi na żądania HTTP. W serwerze HTTP wystarczy do tego celu pojedynczy wiersz kodu: var http = require('http'); var cgi = require('cgi'); var server = http.createServer( cgi('hello.cgi') ); server.listen(3000);
Po przygotowaniu serwera, kiedy żądanie HTTP dotrze do tego serwera, moduł node-cgi obsłuży je, wykonując dwa zadania: Za pomocą cp.spawn() uruchomienie skryptu hello.cgi jako nowego procesu potomnego. Przekazanie nowemu procesowi informacji o bieżącym żądaniu HTTP przy wykorzystaniu własnego zbioru zmiennych środowiskowych. S k r y p t hello.cgi używa jednej charakterystycznej dla CGI zmiennej środowiskowej QUERY_STRING, która zawiera ciąg tekstowy zapytania z adresu URL żądania. Ten ciąg tekstowy będzie wykorzystany w odpowiedzi, która zostanie umieszczona w danych wyjściowych skryptu. Jeżeli uruchomisz przygotowany serwer i wykonasz do niego żądanie HTTP za pomocą polecenia curl, wtedy otrzymasz wynik podobny do poniższego: $ curl http://localhost:3000/?Natalia Witaj, Natalia
Istnieje wiele użytecznych możliwości wykorzystania procesów potomnych w Node, a omówiony moduł node-cgi to tylko jedna z nich. Kiedy przygotowanego serwera lub aplikacji zaczniesz używać do celów, w których zostały opracowane, wówczas na pewnym etapie niewątpliwie znajdziesz odpowiednie zastosowanie dla procesu potomnego.
Rozkład obciążenia za pomocą cp.fork() Ostatnie API oferowane przez moduł child_process to specjalny sposób tworzenia nowych procesów Node za pomocą wbudowanego w Node kanału IPC. Ponieważ zawsze będzie tworzony nowy proces samego Node, pierwszym argumentem przekazywanym wywołaniu cp.fork() jest ścieżka dostępu do modułu Node.js przeznaczonego do wykonania. Podobnie jak jest w przypadku
cp.spawn(),
także
cp.fork()
zwraca obiekt ChildPro
cess.
Podstawowa różnica polega na dodaniu API przez kanał IPC: proces potomny ma funkcję child.send(message), a skrypt wywoływany przez fork() może nasłuchiwać zdarzeń process.on('message'). Przyjmujemy założenie, że w Node tworzymy serwer HTTP przeznaczony do obliczenia ciągu Fibonacciego. Tego rodzaju serwer możesz spróbować utworzyć w Node za pomocą kodu przedstawionego w listingu 13.10. Listing 13.10. Nieoptymalna implementacja ciągu Fibonacciego w Node.js var http = require('http'); function fib (n) { Obliczenie liczby w ciągu Fibonacciego. if (n < 2) { return 1; } else { return fib(n - 2) + fib(n - 1); } } var server = http.createServer(function (req, res) { var num = parseInt(req.url.substring(1), 10); res.writeHead(200); res.end(fib(num) + "\n"); }); server.listen(8000);
Jeżeli uruchomisz serwer za pomocą node fibonacci-native.js i wykonasz żądanie HTTP do http://localhost:8000, wówczas serwer będzie działał zgodnie z oczekiwaniami i obliczy ciąg Fibonacciego dla danej liczby. Taka operacja będzie jednak kosztowna i znacznie obciąży procesor. Ponieważ serwer Node jest jednowątkowy i będzie zajęty obliczaniem wyniku, inne żądania HTTP nie będą mogły być w tym czasie obsługiwane. Ponadto wykorzystany zostanie tylko jeden rdzeń procesora, a inne prawdopodobnie pozostaną nieaktywne. Z wymienionych powodów przedstawione rozwiązanie jest złe. Lepsze rozwiązanie polega na utworzeniu nowego procesu Node podczas każdego żądania HTTP i zleceniu procesowi potomnemu przeprowadzenia kosztownych obliczeń i podania ich wyniku. Wywołanie cp.fork() oferuje czysty interfejs do tego celu. Rozwiązanie będzie się składało z dwóch następujących plików: fibonacci-server.js — to skrypt serwera. fibonacci-calc.js — to skrypt procesu przeprowadzającego obliczenia.
Poniżej przedstawiono kod serwera: var http = require('http'); var cp = require('child_process'); var server = http.createServer(function(req, res) { var child = cp.fork(__filename, [ req.url.substring(1) ]); child.on('message', function(m) { res.end(m.result + '\n'); }); }); server.listen(8000);
Serwer używa wywołania cp.fork() do umieszczenia w oddzielnym procesie Node logiki odpowiedzialnej za obliczenie ciągu Fibonacciego. Wynik obliczeń zostanie przekazany procesowi nadrzędnemu za pomocą process.send(), jak przedstawiono w poniższym skrypcie fibonacci-calc.js: function fib(n) { if (n < 2) { return 1; } else { return fib(n - 2) + fib(n - 1); } } var input = parseInt(process.argv[2], 10); process.send({ result: fib(input) });
Teraz możesz uruchomić serwer za pomocą node wykonać żądanie HTTP do http://localhost:8000.
fibonacci-server.js
i ponownie
To jest doskonały przykład pokazujący, jak podział różnych komponentów aplikacji na wiele procesów może przynieść ogromne korzyści. Rozwiązanie oparte na cp.fork() zapewnia funkcje child.send() i child.on('message') przeznaczone do wysyłania i odbierania komunikatów z procesu potomnego. W ramach samego procesu potomnego dysponujesz funkcjami process.send() i process.on('message') przeznaczonymi do wysyłania i odbierania komunikatów z procesu nadrzędnego. Używaj wymienionych funkcji! Przechodzimy teraz do tematu, jakim jest opracowywanie w Node narzędzi działających w powłoce.
13.4. Tworzenie narzędzi powłoki
Inne zadanie często realizowane przez skrypty Node to tworzenie narzędzi działających w powłoce. Jak dotąd powinieneś znać już największe narzędzie powłoki zbudowane za pomocą Node — menedżer pakietów Node Package Manager, czyli polecenie npm. Ponieważ wymienione narzędzie jest menedżerem pakietów, przeprowadza wiele operacji systemu plików, tworzy nowe procesy potomne, a wszystko odbywa się za pomocą Node i jego asynchronicznego API. W ten sposób menedżer może instalować pakiety równolegle zamiast szeregowo, co przyśpiesza cały proces. Ponadto skoro w Node można utworzyć tak skomplikowane narzędzie powłoki, oznacza to, że za pomocą Node można zbudować dowolne narzędzie powłoki. W większości programów powłoki występuje potrzeba wykonywania tych samych zadań związanych z procesami, na przykład przetwarzania argumentów powłoki, odczytu danych ze standardowego wejścia, a także zapisu do standardowego wyjścia oraz obsługi błędów. W tym podrozdziale poznasz zadania najczęściej wykonywane podczas tworzenia programu powłoki, takie jak: Przetwarzanie argumentów powłoki. Praca ze strumieniami danych wejściowych i wyjściowych. Dodanie koloru do danych wyjściowych za pomocą skryptu ansi.js. Aby móc rozpocząć tworzenie wspaniałych programów powłoki, należy posiadać umiejętność odczytu argumentów, z którymi został uruchomiony program. Tym zagadnieniem zajmiemy się na kolejnych stronach.
13.4.1. Przetwarzanie argumentów podanych w powłoce Przetwarzanie argumentów to łatwy i prosty proces. Node oferuje właściwość process.argv będącą tablicą ciągów tekstowych, które są argumentami użytymi podczas wywołania Node. Pierwszym elementem tablicy jest plik wykonywalny Node, natomiast drugi to nazwa skryptu. Przetwarzanie i podejmowanie działań na podstawie wspomnianych argumentów wymaga przeprowadzenia iteracji przez elementy tablicy oraz sprawdzenia każdego argumentu. Aby to zademonstrować, utworzymy teraz prosty skrypt o nazwie args.js, którego zadaniem jest wyświetlenie zawartości tablicy process.argv. W większości przypadków dwa pierwsze elementy tablicy nie będą Cię interesowały i dlatego usuwamy je z danych wyjściowych za pomocą wywołania slice(): var args = process.argv.slice(2);
console.log(args);
Po wywołaniu skryptu bez argumentów tablica będzie pusta, ponieważ żadne argumenty dodatkowe nie zostały przekazane skryptowi: $ node args.js []
Jednak wywołanie skryptu z argumentami jan i kowalski powoduje, że tablica zawiera oczekiwane wartości w postaci ciągów tekstowych: $ node args.js jan kowalski [ 'jan', 'kowalski' ]
Ujęcie w nawias argumentów zawierających spacje powoduje — podobnie jak w każdej aplikacji powłoki — połączenie ich w pojedynczy argument. To nie jest funkcja Node, ale używanej powłoki (najczęściej bash na platformie UNIX i cmd.exe w Windows): $ node args.js "tobi jest zwierzakiem" [ 'tobi jest zwierzakiem' ]
Zgodnie z konwencją platformy UNIX każdy program powłoki powinien obsługiwać opcje -h i --help przez wyświetlenie informacji o sposobie użycia programu, a następnie zakończenie jego działania. W listingu 13.11 przedstawiono przykład użycia Array#forEach() do iteracji przez argumenty programu oraz przetworzenia ich za pomocą wywołania zwrotnego. Użycie wymienionych wcześniej opcji powoduje wyświetlenie informacji o sposobie użycia programu. Listing 13.11. Przetworzenie tablicy process.argv za pomocą Array#forEach() i bloku switch var args = process.argv.slice(2); Usunięcie z danych wyjściowych dwóch pierwszych elementów tablicy, którymi najczęściej nie jesteśmy zainteresowani. args.forEach(function (arg) { Iteracja przez argumenty i wyszukiwanie opcji -h lub -help. switch (arg) { case '-h': case '--help': printHelp(); break; Jeśli zachodzi potrzeba, umieść tutaj dodatkowe opcje. } }); function printHelp () { Wyświetlenie komunikatu o sposobie użycia programu, a następnie zakończenie jego działania. console.log(' Użycie:'); console.log(' $ WspaniałyProgram '); console.log(' Przykład:');
console.log(' $ WspaniałyProgram --wspaniała-opcja jeszcze-niezbyt.wspaniałe'); process.exit(0); }
B l o k switch można bardzo łatwo rozbudować o możliwość przetwarzania dodatkowych opcji. Moduły opracowane przez społeczność, na przykład commander.js, nopt, optimist i nomnom (wymieniono tutaj jedynie kilka dostępnych), przetwarzają opcje na własne sposoby. Dlatego też warto wiedzieć, że użycie bloku switch to nie jest jedyny możliwy sposób przetwarzania argumentów. Podobnie jak w wielu innych przypadkach, tak i tu nie istnieje jeden prawidłowy sposób wykonania danego zadania w programowaniu. Inne zadanie, z którym każdy program powłoki musi sobie radzić, to odczyt danych wejściowych ze standardowego wejścia i zapis strukturyzowanych danych wyjściowych do standardowego wyjścia. Przekonajmy się, jak można to zrobić w Node.
13.4.2. Praca ze standardowym wejściem i wyjściem Programy na platformie UNIX są najczęściej małe, samodzielne i skoncentrowane na wykonywaniu pojedynczego zadania. Poszczególne programy są łączone za pomocą potoków i wynik działania jednego jest przekazywany do kolejnego programu, aż do zakończenia łańcucha poleceń. Na przykład za pomocą standardowych poleceń systemu UNIX można pobrać listę unikatowych autorów z dowolnego repozytorium Git. Wymaga to połączenia poleceń git log, sort i uniq w następujący sposób: $ git log --format='%aN' | sort | uniq Mike Cantelon Nathan Rajlich TJ Holowaychuk
Wymienione polecenia działają jednocześnie, dane wyjściowe jednego procesu są używane jako dane wejściowe kolejnego i tak aż do końca łańcucha poleceń. Aby zaoferować mechanizm podobny do potokowania, Node dostarcza dwa obiekty Stream, które można wykorzystać w budowanych programach powłoki: process.stdin — obiekt ReadStream przeznaczony do odczytu danych wejściowych. process.stdout — obiekt WriteStream przeznaczony do zapisu danych wyjściowych. Sposób działania wymienionych obiektów jest podobny jak omówionych
wcześniej interfejsów strumieni.
Zapis danych wyjściowych za pomocą process.stdout Zapisywalnego strumienia process.stdout używasz za każdym razem, gdy wywoływana jest funkcja console.log(). Wewnętrznie wymieniona funkcja w y w o ł u j e process.stdout.write() po sformatowaniu argumentów danych wejściowych. Jednak funkcje console są przeznaczone bardziej do debugowania i analizowania obiektów. Kiedy zachodzi potrzeba zapisu w standardowym wyjściu strukturyzowanych danych, wtedy można bezpośrednio wywołać process.stdout.write(). Przyjmujemy założenie, że program nawiązuje połączenie z adresem HTTP URL, a następnie odpowiedź przekazuje do standardowego wyjścia. W takim kontekście doskonale sprawdza się Stream#pipe(), jak przedstawiono w poniższym fragmencie kodu: var http = require('http'); var url = require('url'); var target = url.parse(process.argv[2]); var req = http.get(target, function (res) { res.pipe(process.stdout); });
Doskonale! Niezwykle minimalna replika polecenia curl utworzona za pomocą jedynie sześciu wierszy kodu. Nie tak źle, prawda? Teraz zajmiemy się process.stdin.
Odczyt danych wejściowych za pomocą process.stdin Zanim będzie można odczytać dane ze standardowego wejścia, konieczne jest wywołanie process.stdin.resume() w celu wskazania, że skrypt jest zainteresowany danymi pochodzącymi ze standardowego wejścia. Działa ono podobnie jak każdy inny odczytywalny strumień, emituje zdarzenia data po otrzymaniu danych wyjściowych innego procesu oraz kiedy użytkownik naciśnie dowolny klawisz w oknie terminalu. W listingu 13.12 przedstawiono kod programu powłoki, który prosi użytkownika o podanie wieku. Na podstawie otrzymanych informacji program podejmuje decyzję o dalszym sposobie działania. Listing 13.12. Program, który prosi użytkownika o podanie wieku var requiredAge = 18; Ustawienie granicy wieku. process.stdout.write('Proszę podać wiek: '); Przygotowanie pytania dla użytkownika. process.stdin.setEncoding('utf8'); Określenie, że standardowe wejście ma emitować ciągi tekstowe UTF-8 zamiast buforów. process.stdin.on('data', function (data) {
var age = parseInt(data, 10); Przetworzenie danych na postać liczby. if (isNaN(age)) { Jeżeli wiek podany przez użytkownika jest mniejszy niż 18 lat, należy wyświetlić komunikat informujący go, aby powrócił do programu za kilka lat. console.log('%s nie jest poprawną liczbą!', data); } else if (age < requiredAge) { Jeżeli użytkownik nie podał prawidłowej liczby, należy wyświetlić odpowiedni komunikat. console.log('Musisz mieć co najmniej %d lat, aby uzyskać dostęp do programu. ' + 'Wróć za %d lat', requiredAge, requiredAge - age); } else { enterTheSecretDungeon(); Po spełnieniu wcześniejszych warunków można kontynuować wykonywanie programu. } process.stdin.pause(); Oczekiwanie na pojedyncze zdarzenie data przed zamknięciem standardowego wejścia. }); process.stdin.resume(); Wywołanie funkcji resume() w celu rozpoczęcia odczytu, ponieważ process.stdin na początku znajduje się w stanie wstrzymania. function enterTheSecretDungeon () { console.log('Witamy w Programie :)'); }
Rejestracja danych diagnostycznych za pomocą process.stderr W każdym procesie Node istnieje również zapisywalny strumień process.stderr, który działa dokładnie tak samo jak process.stdout, ale dane przekazuje do standardowego wyjścia błędów. Ponieważ jest ono zarezerwowane zwykle dla debugowania, a nie dla wysyłania strukturalnych danych lub potokowania, ogólnie rzecz biorąc, będziesz używał funkcji console.error(), zamiast uzyskiwać bezpośredni dostęp do process.stderr. Po poznaniu standardowych strumieni wejścia-wyjścia w Node dysponujesz wiedzą niezbędną przy tworzeniu programów powłoki. Przystępujemy więc do zbudowania czegoś nieco bardziej kolorowego.
13.4.3. Dodanie koloru do danych wyjściowych Wiele narzędzi powłoki używa kolorowego tekstu w celu ułatwienia użytkownikom odróżniania informacji na ekranie. Node również stosuje tę funkcjonalność w interfejsie REPL, podobnie jak menedżer npm na różnych poziomach rejestracji danych. Użycie koloru to przydatny dodatek, z którego łatwo może skorzystać praktycznie każdy program powłoki. Dodanie koloru to
łatwe zadanie, zwłaszcza w przypadku użycia jednego z modułów opracowanych przez społeczność.
Tworzenie i zapis znaków sterujących ANSI Kolory w terminalu są generowane za pomocą znaków sterujących ANSI (ang. American National Standards Institute). Wspomniane znaki sterujące to sekwencje zwykłego tekstu przekazywane do standardowego wyjścia i mające znaczenie specjalne dla terminalu — mogą zmienić kolor tekstu, położenie kursora, odtworzyć dźwięk itd. Zaczniemy od prostego przykładu. Aby skrypt wyświetlił słowo witaj w kolorze zielonym, można użyć pojedynczego wywołania console.log(): console.log('\033[32mwitaj\033[39m');
Jeżeli przyjrzysz się dokładnie powyższemu wywołaniu, słowo witaj dostrzeżesz w środku ciągu tekstowego, wśród dziwnych znaków wokół. W pierwszej chwili to może wydawać się dziwne, ale tak naprawdę jest to prosty mechanizm. Na rysunku 13.4 pokazano podział ciągu tekstowego witaj w kolorze zielonym na trzy oddzielne części.
Rysunek 13.4. Wyświetlenie słowa „witaj” w kolorze zielonym za pomocą znaków sterujących ANSI
Istnieje znacznie więcej znaków sterujących rozpoznawanych przez terminale, ale większość programistów ma ważniejsze rzeczy do zrobienia niż zapamiętywanie ich. Na szczęście społeczność Node okazuje się pomocna nawet w takiej sytuacji — opracowała wiele modułów, między innymi colors.js, clicolor.js i ansi.js, dzięki którym użycie koloru w programie powłoki jest niezwykle łatwe. Kody ANSI w Windows. Technicznie rzecz biorąc, system Windows i jego wiersz poleceń (cmd.exe) nie obsługują znaków sterujących ANSI. Na szczęście dla nas Node interpretuje znaki sterujące w Windows, gdy skrypt przekazuje je do standardowego wyjścia, a następnie wywołuje odpowiednie funkcje Windows zapewniające możliwość osiągnięcia tego samego wyniku. Warto o tym wiedzieć, choć jednocześnie nie musisz się nad tym zastanawiać podczas tworzenia aplikacji Node.
Formatowanie koloru tekstu za pomocą ansi.js Spójrz na moduł ansi.js (https://github.com/TooTallNate/ansi.js), który można zainstalować za pomocą npm install ansi. Wymieniony moduł jest dobrym rozwiązaniem, ponieważ to bardzo cienka warstwa na górze zwykłych znaków sterujących ANSI, oferująca ogromną elastyczność w porównaniu z innymi modułami dodającymi kolor (działają one z pojedynczym ciągiem tekstowym). Za pomocą modułu ansi.js można zdefiniować tryby (na przykład „pogrubienie”) dla strumieni. Będą one stosowane aż do wywołania reset(). Warto w tym miejscu dodać, że ansi.js to pierwszy moduł obsługujący 256 kolorów terminala. Potrafi również konwertować kody kolorów CSS (takie jak #FF0000) na kody kolorów ANSI. M o duł ansi.js działa wraz z koncepcją kursora, który tak naprawdę jest opakowaniem dla egzemplarza zapisywalnego strumienia i zawiera wiele wygodnych funkcji przeznaczonych do umieszczania kodów ANSI w strumieniu, a ponadto obsługuje łączenie w łańcuchy. W celu wyświetlenia słowa witaj w kolorze zielonym za pomocą modułu ansi.js można użyć poniższego fragmentu kodu: var ansi = require('ansi'); var cursor = ansi(process.stdout); cursor .fg.green() .write('witaj') .fg.reset() .write('\n');
Jak możesz się przekonać, w celu użycia modułu ansi.js najpierw trzeba utworzyć egzemplarz cursor na podstawie zapisywalnego strumienia. Ponieważ interesuje nas zastosowanie koloru w danych wyjściowych programu, można przekazać process.stdout jako zapisywalny strumień używany przez egzemplarz coursor. Po uzyskaniu egzemplarza coursor można już wywołać dowolną z dostarczonych metod przeznaczonych do zmiany sposobu generowania w terminalu tekstu danych wyjściowych. W omawianym przykładzie wynik odpowiada wcześniej przedstawionemu wywołaniu console.log(): cursor.fg.green() — ustawienie koloru tekstu na zielony. cursor.write('witaj') — wyświetlenie w terminalu słowa witaj w kolorze zielonym. cursor.fg.reset() — przywrócenie domyślnego koloru tekstu. cursor.write('\n') — umieszczenie znaku nowego wiersza.
Programowe dopasowanie danych wyjściowych za estetyczny interfejs przeznaczony do zmiany kolorów.
pomocą
cursor
oferuje
Formatowanie koloru tła za pomocą ansi.js Moduł ansi.js pozwala również na zmianę koloru tła. Aby ustawić kolor tła zamiast tekstu, w wywołaniu należy fragment fg zastąpić przez bg. Na przykład ustawienie tła w kolorze czerwonym wymaga wywołania cursor.bg.red(). Przygotujemy teraz mały program wyświetlający w terminalu kolorowe informacje dotyczące tytułu i autorów niniejszej książki, jak pokazano na rysunku 13.5. Jak pokazano na rysunku, kod przeznaczony do wyświetlenia danych wyjściowych w kolorze jest nieco rozległy, ale łatwy. Każda funkcja jest mapowana bezpośrednio na znak sterujący umieszczany w strumieniu. Kod przedstawiony w listingu 13.13 składa się z dwóch wierszy inicjalizacyjnych oraz naprawdę długiego łańcucha wywołań funkcji, które ostatecznie umieszczają znaki sterujące koloru i ciągi tekstowe w pro cess.stdout. Kody pozwalające na zdefiniowanie koloru to tylko jedna z kluczowych funkcji modułu ansi.js. Nie przedstawiliśmy tutaj możliwości w zakresie umieszczania kursora, odtwarzania dźwięku bądź też ukrywania i wyświetlania kursora. Więcej informacji na ten temat i przykłady znajdziesz w dokumentacji modułu ansi.js.
Rysunek 13.5. Wynik uruchomienia skryptu ansi-title.js — tytuł książki i jej autorzy to informacje wyświetlone w różnych kolorach Listing 13.13. Prosty program, który wyświetla tytuł książki i jej autorów w różnych kolorach var ansi = require('ansi'); var cursor = ansi(process.stdout); cursor .reset() .write(' ')
.bold() .underline() .bg.white() .fg.black() .write('Node.js w akcji') .fg.reset() .bg.reset() .resetUnderline() .resetBold() .write(' \n') .fg.green() .write(' by:\n') .fg.cyan() .write('
Mike Cantelon\n')
.fg.magenta() .write('
TJ Holowaychuk\n')
.fg.yellow() .write('
Nathan Rajlich\n')
.reset()
13.5. Podsumowanie Node zaprojektowano przede wszystkim do wykonywania zadań związanych z operacjami wejścia-wyjścia, takimi jak tworzenie serwera HTTP. Jednak Node doskonale sprawdza się także w wielu innych zadaniach, między innymi podczas tworzenia programów powłoki dla serwera aplikacji, programu klienta nawiązującego połączenie z serwerem ASCII Star Wars, programu pobierającego i wyświetlającego dane statystyczne z serwerów giełdowych itd. Możliwości zastosowania są ograniczone jedynie Twoją wyobraźnią. Spójrz na menedżera npm i node-gyp — to dwa przykłady skomplikowanych narzędzi powłoki utworzonych za pomocą Node. To jednocześnie doskonałe przykłady, na podstawie których można się uczyć. W tym rozdziale wspomniano o kilku modułach opracowanych przez społeczność, które mogą pomóc podczas prac nad aplikacjami. W kolejnym rozdziale skoncentrujemy się na tym, jak wyszukiwać wspaniałe moduły opracowane przez społeczność Node. Dowiesz się również, jak podzielić się samodzielnie opracowanymi modułami w celu zebrania komentarzy i wprowadzenia usprawnień w modułach. Aspekt społecznościowy to naprawdę ekscytujący obszar!
Rozdział 14. Ekosystem Node W tym rozdziale: • Wyszukiwanie w internecie pomocy związanej z Node. • Współpraca nad Node poprzez serwis GitHub. • Publikowanie własnej pracy za pomocą Node Package Manager.
Aby w pełni móc wykorzystać możliwości programowania w Node, należy wiedzieć, gdzie szukać pomocy oraz jak dzielić się ze społecznością Node samodzielnie opracowanym kodem. Prace nad Node i powiązanymi projektami odbywają się podobnie jak w większości społeczności oprogramowania open source, czyli na zasadzie współpracy przez internet wielu osób. Programiści współpracują ze sobą, przekazując sobie i przeglądając kod, tworząc dokumentację projektu i zgłaszając błędy. Kiedy programiści są gotowi do wydania nowej wersji Node, zostaje ona opublikowana w oficjalnej witrynie Node. Po utworzeniu przez osoby trzecie modułu wartego wydania można go umieścić w repozytorium npm, dzięki czemu staje się łatwy do instalacji przez innych użytkowników. Zasoby dostępne w internecie zawierają wszelkie informacje niezbędne do rozpoczęcia pracy z Node oraz z projektami powiązanymi. Na rysunku 14.1 pokazano, jak używać zasobów internetowych podczas prac programistycznych związanych z Node, dystrybucją i zapewnieniem pomocy technicznej. Zanim zaczniesz współpracę z innymi, będziesz prawdopodobnie szukał pomocy technicznej. Dlatego też w pierwszej kolejności dowiedz się, gdzie w internecie znajdziesz pomoc, gdy będzie Ci ona potrzebna.
Rysunek 14.1. Projekty związane z Node są tworzone wspólnie za pośrednictwem serwisu GitHub. Następnie są publikowane w repozytorium npm, a dokumentacja i pomoc techniczna są dostępne za pomocą zasobów internetowych
14.1. Dostępne w internecie zasoby dla programistów Node Ponieważ świat Node nieustannie się zmienia, najnowsze informacje dotyczące tej technologii znajdziesz w internecie. Do dyspozycji masz wiele witryn internetowych, grup dyskusyjnych, czatów i innych zasobów, w których można znaleźć potrzebne informacje.
14.1.1. Node i odniesienia do modułów W tabeli 14.1 wymieniono pewne znajdujące się w internecie zasoby związane z Node. Najużyteczniejsze z nich to strony internetowe zawierające omówienie API Node (strona domowa Node.js), a także informacje o opracowanych przez firmy trzecie modułach (strona domowa menedżera npm). Tabela 14.1. Użyteczne zasoby w internecie dotyczące Node Z asób
Adres URL
Strona główna Node.js
http://nodejs.org/
Aktualna dokumentacja API Node
http://nodejs.org/api/
Blog Node
http://blog.nodejs.org/
Oferty pracy związane z Node
http://jobs.nodejs.org/a/jobs/find-jobs
Strona główna menedżera Node Package Manager (npm)
https://www.npmjs.org/
Jeśli próbujesz coś zaimplementować za pomocą Node lub dowolnego z wbudowanych modułów, to strona domowa Node stanowi nieoceniony zasób. Ta pokazana na rysunku 14.2 witryna internetowa zawiera pełną dokumentację frameworka Node i oferowanego przez niego API. Tutaj zawsze znajdziesz dokumentację dotyczącą najnowszej wersji Node. Oficjalny blog również zawiera informacje o nowościach wprowadzonych w Node oraz ogólnie komunikaty ważne dla społeczności Node. Strona domowa zawiera również oferty pracy.
Rysunek 14.2. Oprócz odnośników prowadzących do użytecznych zasobów powiązanych z Node witryna nodejs.org oferuje także dokumentację API dla każdej wydanej wersji Node
Jeśli szukasz funkcji opracowanych przez firmy trzecie, repozytorium npm to
pierwsze miejsce, które powinieneś odwiedzić. Pozwala na użycie słów kluczowych do przeszukiwania tysięcy modułów oferowanych przez npm. Jeżeli znajdziesz moduł, który chcesz sprawdzić, kliknij jego nazwę, co spowoduje wyświetlenie strony z informacjami szczegółowymi o module. Wspomniana strona zawiera między innymi łącze prowadzące do strony domowej projektu modułu oraz wszelkie użyteczne informacje o nim, na przykład wskazujące, jakie inne moduły zależą od danego, zależności danego modułu od innych, wersje zgodnych modułów oraz informacje o licencji. Wymienione wcześniej witryny internetowe nie dostarczają odpowiedzi na wszystkie pytania, które mogą się pojawić w zakresie użycia Node lub modułów opracowanych przez firmy trzecie. Zapoznaj się więc z innymi doskonałymi miejscami, w których można poprosić o pomoc.
14.1.2. Grupy Google Grupy Google zostały utworzone dla Node, menedżera npm oraz pewnych popularnych modułów i frameworków, między innymi Express, node-mogodb-native i Mongoose. Grupy Google to doskonałe miejsce do zadawania trudnych lub obszernych pytań. Na przykład jeśli masz problem ze znalezieniem sposobu na usunięcie dokumentów MongoDB za pomocą modułu node-mogodb-native, wówczas odpowiednie pytanie można zadać na grupie poświęconej wymienionemu modułowi (https://groups.google.com/forum/?fromgroups&hl=pl#!forum/nodemongodb-native) i przekonać się, czy ktokolwiek inny napotkał podobny problem. Jeżeli ktoś już spotkał się z podobnym problemem, Twoim kolejnym krokiem powinno być dołączenie do grupy Google i zadanie pytania. Możesz tworzyć długie posty, co jest przydatne w skomplikowanych pytaniach, ponieważ pozwala na dokładne wyjaśnienie istoty problemu. Nie istnieje centralna lista wymieniająca wszystkie grupy Google powiązane z Node. Możesz je znaleźć podane w dokumentacji projektu, choć najczęściej będziesz musiał sam wyszukać je w sieci. W tym celu w wyszukiwarce internetowej wystarczy podać wyrażenie „nazwa-modułu node.js grupa google” i sprawdzić, czy istnieje grupa dla wskazanego modułu. Wadą użycia grup Google jest często konieczność czekania kilka godzin lub nawet dni na uzyskanie odpowiedzi, w zależności od parametrów grupy Google. W przypadku prostych pytań, gdy szybko potrzebujesz odpowiedzi, powinieneś rozważyć skorzystanie z czatu internetowego, na którym z reguły szybko znajdziesz potrzebne informacje.
14.1.3. IRC
IRC (ang. Internet Relay Chat) utworzono w roku 1988 i choć niektórzy mogą uważać IRC za archaiczne rozwiązanie, ono nadal funkcjonuje i jest aktywnie wykorzystywane — to najlepszy sposób na szybkie uzyskanie odpowiedzi na pytania dotyczące oprogramowania open source. Pokoje IRC są nazywane kanałami, istnieją dla wielu modułów Node. Wprawdzie nigdzie nie znajdziesz listy kanałów IRC poświęconych Node, ale w dokumentacji modułu czasami umieszczane są informacje o poświęconym mu kanale IRC, o ile taki istnieje. Aby otrzymać odpowiedź na pytanie zadane na czacie, należy nawiązać połączenie z siecią IRC (http://chatzilla.hacksrus.com/faq/#connect), przejść do odpowiedniego kanału, a następnie po prostu zadać pytanie. Ze względu na szacunek wobec uczestników czatu przed zadaniem pytania warto sprawdzić, czy podobne pytanie nie pojawiło się wcześniej i czy nie została na nie już udzielona odpowiedź. Jeżeli jesteś początkującym użytkownikiem IRC, najłatwiejszym sposobem nawiązania połączenia będzie użycie klienta opartego na przeglądarce internetowej. Freenode, sieć IRC, w której znajduje się większość kanałów IRC poświeconych Node, oferuje klienta sieciowego pod adresem http://webchat.freenode.net. Aby przyłączyć się do czatu, w formularzu połączenia podaj nazwę użytkownika. Nie trzeba się rejestrować i można podać dowolną nazwę użytkownika. (Jeżeli ktokolwiek używa wybranej przez Ciebie nazwy użytkownika, na jej końcu zostanie umieszczony znak podkreślenia, aby umożliwić rozróżnianie użytkowników). Po kliknięciu przycisku Connect dołączysz do kanału. Po prawej stronie znajduje się pasek boczny, w którym wymieniono innych uczestników czatu.
14.1.4. Zgłaszanie problemów w serwisie GitHub Jeżeli projekt znajduje się w serwisie GitHub, wtedy innym miejscem pozwalającym na szukanie rozwiązania dla powstałych problemów jest strona zgłaszania problemów w projekcie GitHub. Aby przejść na tę stronę, należy najpierw wyświetlić stronę główną projektu w serwisie GitHub, a następnie kliknąć kartę Issues. Możesz skorzystać z pola wyszukiwania i spróbować odszukać inne problemy podobne do napotkanego przez Ciebie. Przykładową stronę przeznaczoną do zgłaszania problemów pokazano na rysunku 14.3.
Rysunek 14.3. W przypadku projektów umieszczonych w serwisie GitHub zgłoszenie błędu może być pomocne, jeśli sądzisz, że odkryłeś problem w kodzie projektu
Jeżeli nie znajdujesz informacji pomagających w rozwiązaniu problemu i uważasz, że może on być wynikiem błędu w kodzie projektu, kliknij przycisk New Issue na wyświetlonej stronie i dokładnie opisz problem. Po utworzeniu nowego zgłoszenia osoby zajmujące się projektem będą mogły odpowiedzieć na tej samej stronie i rozwiązać problem lub zadać dodatkowe pytania w celu lepszego zdiagnozowania problemu. Strona zgłaszania problemów w GitHub to nie forum pomocy technicznej. W zależności od projektu zadawanie na wspomnianej stronie ogólnych pytań z zakresu pomocy technicznej może być uznawane za nieodpowiednie. Dzieje się tak najczęściej, gdy dla projektu przygotowano inne formy udzielania pomocy technicznej, na przykład za pośrednictwem grup Google. Dobrym podejściem jest zapoznanie się z plikiem README projektu i sprawdzenie, czy istnieją jakiekolwiek wskazówki dotyczące zadania pytań z zakresu ogólnej pomocy technicznej. Teraz już wiesz, jak zgłaszać problemy związane z projektami GitHub. Przechodzimy więc do niezwiązanej z pomocą techniczną roli serwisu GitHub będącego miejscem współpracy większości osób zajmujących się rozwijaniem frameworka Node.
14.2. Serwis GitHub
Serwis GitHub to środek ciężkości świata oprogramowania open source i jednocześnie miejsce o znaczeniu krytycznym dla programistów Node. GitHub oferuje obsługę repozytoriów Git, czyli potężnego systemu kontroli wersji (ang. Version Control System , VCS). Serwis zawiera interfejs działający w przeglądarce internetowej i pozwalający na łatwe przeglądanie repozytoriów Git. Projekty typu open source mogą być umieszczone bezpłatnie w serwisie GitHub. Git. System kontroli wersji Git stał się najpopularniejszym wyborem dla projektów typu open source. To rozproszony system kontroli wersji, którego w przeciwieństwie do Subversion i innych tego rodzaju systemów można używać bez połączenia sieciowego z serwerem. Git powstał w roku 2005, zainspirowany przez własnościowy VCS o nazwie BitKeeper. Właściciel serwisu BitKeeper pozwolił na bezpłatne korzystanie z serwisu zespołowi pracującemu nad jądrem systemu Linux. Ten przywilej cofnął jednak, gdy nabrał podejrzeń, że członkowie zespołu próbują ustalić wewnętrzny sposób działania serwisu BitKeeper. Linus Torvalds, czyli twórca systemu Linux, postanowił utworzyć alternatywny system kontroli wersji, o podobnej funkcjonalności. Kilka miesięcy później system Git był już używany przez zespół pracujący nad jądrem systemu Linux. Poza hostingiem repozytoriów Git serwis GitHub oferuje stronę zgłaszania błędów, wiki, a także możliwość hostingu strony internetowej poświęconej projektowi. Ponieważ większość projektów Node w repozytorium npm jest przechowywana w serwisie GitHub, umiejętność posługiwania się tym serwisem pozwala na wykorzystanie pełni możliwości podczas programowania w Node. GitHub zapewnia wygodny sposób przeglądania kodu, sprawdzania nierozwiązanych błędów, a jeśli zachodzi potrzeba — także zgłaszania poprawek i opracowywania dokumentacji. Innym sposobem użycia GitHub jest obserwacja projektu. Obserwacja projektu oznacza otrzymywanie powiadomień o wprowadzeniu zmian w projekcie. Liczba osób obserwujących dany projekt jest często używana do określenia jego ogólnej popularności. Serwis GitHub może oferować potężne możliwości, ale jak z niego korzystać? Tego dowiesz się w kolejnym punkcie.
14.2.1. Rozpoczęcie pracy z GitHub Kiedy wpadniesz na pomysł utworzenia projektu lub opracowania modułu opartego na Node, wówczas warto utworzyć konto w serwisie GitHub (jeśli jeszcze tego nie zrobiłeś), aby zapewnić sobie łatwy dostęp do hostingu repozytoriów Git. Po utworzeniu konta i przeprowadzeniu jego konfiguracji będziesz mógł zacząć dodawać projekty, o czym dowiesz się w kolejnym
punkcie. Ponieważ GitHub wymaga systemu kontroli wersji Git, konieczne jest skonfigurowanie go przed rozpoczęciem pracy z GitHub. Na szczęście w serwisie GitHub znajdują się strony pomocy objaśniające prawidłową konfigurację Git na platformie Mac, Windows i Linux (https://help.github.com/articles/set-up-git). Po skonfigurowaniu Git możesz utworzyć konto GiHub, rejestrując się w witrynie internetowej serwisu i dostarczając klucz SSH. Wspomniany klucz SSH jest niezbędny do zapewnienia bezpiecznej współpracy z serwisem GitHub. W tym punkcie zostanie dokładnie omówiony proces konfiguracji. Zwróć uwagę, że omówione tutaj kroki trzeba wykonać tylko jednokrotnie, a nie podczas każdego dodawania projektu do GitHub.
Konfiguracja Git i rejestracja GitHub Aby można było używać serwisu GitHub, konieczne jest skonfigurowanie systemu kontroli wersji Git. Imię i nazwisko oraz adres e-mail podajesz za pomocą dwóch wymienionych poniżej poleceń: git config --global user.name "Jan Kowalski" git config --global user.email [email protected]
Teraz możesz przystąpić do rejestracji konta w GitHub. Przejdź na stronę rejestracji (https://github.com/join), wypełnij formularz i kliknij przycisk Create an account.
Dostarczenie GitHub klucza publicznego SSH Po rejestracji trzeba umieścić w serwisie GitHub klucz publiczny SSH (https://help.github.com/articles/generating-ssh-keys). Wymieniony klucz będzie używany do uwierzytelniania transakcji Git. Wykonaj wymienione poniżej kroki: 1. W przeglądarce internetowej przejdź na stronę https://github.com/settings/ssh. 2. Kliknij przycisk Add SSH Key. Na tym etapie dalsze postępowanie zależy od używanego systemu operacyjnego. GitHub wykryje Twój system operacyjny i wyświetli odpowiednie instrukcje.
14.2.2. Dodanie projektu do GitHub Po przygotowaniu konta w serwisie GitHub można przystąpić do dodawania projektów i przekazywania kodu do repozytoriów. W tym celu najpierw utwórz repozytorium GitHub dla projektu, co zostanie wkrótce omówione. Następnie w komputerze lokalnym utwórz repozytorium
Git, w którym będziesz umieszczał kod przed jego przekazaniem do GitHub. Cały proces został pokazany na rysunku 14.4. Pliki projektu można przejrzeć również za pomocą oferowanego przez GitHub interfejsu sieciowego.
Utworzenie repozytorium GitHub Utworzenie kroków:
repozytorium Git
wymaga
wykonania
wymienionych poniżej
1. W przeglądarce internetowej przejdź do serwisu GitHub i zaloguj się. 2. Przejdź na stronę https://github.com/new.
Rysunek 14.4. Kroki niezbędne podczas dodawania projektu Node do GitHub
3. Wypełnij wyświetlony formularz opisujący repozytorium i kliknij przycisk Create Repository. 4. GitHub utworzy puste repozytorium Git oraz stronę zgłaszania błędów dla projektu. 5. GitHub wyświetli kroki, jakie trzeba wykonać, aby użyć systemu kontroli wersji Git do umieszczenia kodu w nowym repozytorium. Warto rozumieć działanie poszczególnych kroków i dlatego zostaną tutaj omówione, a przykłady przedstawią podstawy użycia Git.
Konfiguracja pustego repozytorium Git W celu dodania przykładowego projektu do serwisu GitHub najpierw utworzymy prosty moduł dla Node. W omawianym przykładzie będzie to moduł zawierający logikę przeznaczoną do skracania adresów URL. Modułowi nadajemy nazwę node-elf. Rozpoczynamy więc od utworzenia katalogu tymczasowego dla projektu, wydając wymienione poniżej polecenia: mkdir -p ~/tmp/node-elf cd ~/tmp/node-elf
Aby użyć katalogu jako repozytorium Git, należy wydać poniższe polecenie, które spowoduje utworzenie katalogu o nazwie .git zawierającego metadane repozytorium: git init
Dodanie plików do repozytorium Git Po przygotowaniu pustego repozytorium możemy dodać do niego pewne pliki. W omawianym przykładzie to będzie plik zawierający logikę odpowiedzialną za skracanie adresów URL. Kod przedstawiony w listingu 14.1 umieść w pliku o nazwie index.js w utworzonym wcześniej katalogu. Listing 14.1. Moduł Node służący do skracania adresów URL exports.initPathData = function(pathData) { Funkcja inicjalizacyjna jest wywoływana niejawnie przez funkcje shorten() i expand(). pathData = (pathData) ? pathData : {}; pathData.count = (pathData.count) ? pathData.count : 0; pathData.map
= (pathData.map) ? pathData.map : {};
} exports.shorten = function(pathData, path) { Funkcja akceptuje ciąg tekstowy „ścieżki” i zwraca skrócony adres URL mapowany na tę ścieżkę. exports.initPathData(pathData); pathData.count++; pathData.map[pathData.count] = path; return pathData.count.toString(36); } exports.expand = function(pathData, shortened) { Funkcja akceptuje poprzednio skrócony adres URL i zwraca adres URL w pełnej postaci. exports.initPathData(pathData); var pathIndex = parseInt(shortened, 36); return pathData.map[pathIndex]; }
Teraz trzeba poinformować Git, że chcemy umieścić nowy plik w repozytorium. Polecenie add w systemie Git działa inaczej niż w pozostałych systemach kontroli wersji. Zamiast dodawać pliki do repozytorium, polecenie dodaje je do tak zwanego staging area w Git. Wymieniony obszar można uznawać za listę wskazującą na pliki nowo dodane, zmodyfikowane lub te, które mają zostać umieszczone w kolejnej wersji repozytorium: git add index.js
System Git teraz „wie”, że powinien monitorować wskazany plik. Jeśli chcesz, do staging area możesz dodać jeszcze inne pliki, ale na obecnym etapie wystarczy tylko jeden wymieniony. Aby nakazać systemowi Git utworzenie nowej wersji repozytorium zawierającego zmodyfikowane pliki umieszczone w staging area, należy wydać polecenie commit. W przypadku innych VCS polecenie commit może akceptować opcję -m pozwalającą na dodanie opisu zmian wprowadzonych w nowej wersji repozytorium: git commit -m "Dodano funkcję skracania adresów URL."
Repozytorium znajdujące się w komputerze lokalnym zawiera teraz nową wersję repozytorium. Wyświetlenie listy zmian repozytorium następuje po wydaniu poniższego polecenia: git log
Przekazanie repozytorium Git do serwisu GitHub Jeżeli na tym etapie komputer lokalny zostanie zniszczony, wówczas stracisz całą dotychczas wykonaną pracę. W celu zabezpieczenia się przed nieprzewidywalnymi kataklizmami i wykorzystania pełni możliwości interfejsu sieciowego GitHub zmiany wprowadzone w lokalnym repozytorium Git należy przekazać do serwisu GitHub. Wcześniej trzeba jednak poinformować Git, gdzie ma być przekazywany kod. To wymaga utworzenia zdalnego repozytorium Git. Tego rodzaju repozytoria są określane mianem zdalnych. Poniższe polecenie pokazuje, jak dodać zdalne repozytorium GitHub. Podaj odpowiednią nazwę użytkownika i zwróć uwagę, że node-elf.git wskazuje nazwę projektu: git remote add origin [email protected]:nazwa-użytkownika/node-elf.git
Po dodaniu zdalnego repozytorium zmiany wprowadzone w kodzie można wysłać do serwisu GitHub. W terminologii Git przekazywanie zmian do repozytorium nosi nazwę push. W poniższym poleceniu nakazujesz Git przekazanie zmian do zdalnego repozytorium origin zdefiniowanego w poprzednim poleceniu. Każde repozytorium Git może mieć jedno lub więcej odgałęzień, które pod względem koncepcji są oddzielnymi obszarami roboczymi w repozytorium. Zmiany wprowadzone w kodzie chcesz przekazać do gałęzi master:
git push -u origin master
W poleceniu push opcja -u informuje Git, że wskazane zdalne repozytorium to upstream i gałąź. Zdalne repozytorium upstream jest domyślnie używanym zdalnym repozytorium. Po pierwszym przekazaniu zmian za pomocą opcji -u kolejne operacje przekazania kodu możesz przeprowadzać za pomocą poniższego polecenia, które jest prostsze i tym samym łatwiejsze do zapamiętania: git push
Jeżeli przejdziesz do serwisu GitHub i odświeżysz stronę repozytorium, zobaczysz nowo dodany plik. Utworzenie i hosting modułu w GitHub to łatwy sposób na umożliwienie wielokrotnego wykorzystania danego modułu. Na przykład jeżeli będziesz chciał go użyć w innym projekcie, wówczas wystarczy wydać przedstawione poniżej polecenia: mkdir ~/tmp/my_project/node_modules cd ~/tmp/my_project/node_modules git clone https://github.com/mcantelon/node-elf.git elf cd ..
Polecenie require('elf') w kodzie projektu zapewnia później dostęp do modułu. Zwróć uwagę, że podczas klonowania repozytorium używasz ostatniego argumentu powłoki w celu nadania nazwy katalogowi, w którym ma zostać umieszczony sklonowany kod. Teraz już wiesz, jak dodawać projekty do serwisu GitHub, jak utworzyć repozytorium w GitHub, jak utworzyć i dodać pliki do repozytorium Git w komputerze lokalnym oraz jak przekazać wprowadzone zmiany do zdalnego repozytorium. W internecie znajdziesz zasoby, dzięki którym będziesz potrafił wykorzystać system kontroli wersji w jeszcze większym stopniu. Jeżeli szukasz dokładnych informacji z zakresu użycia systemu Git, przeczytaj książkę „Pro Git” jednego z założycieli serwisu GitHub, czyli Scotta Chacona. Można ją kupić lub przeczytać bezpłatnie w internecie (http://git-scm.com/book/pl/). Jeżeli wolisz podejście bardziej praktyczne, zapoznaj się z dokumentacją dostępną w oficjalnej witrynie systemu Git (http://git-scm.com/documentation), gdzie wymieniono listę wielu samouczków, dzięki którym rozpoczniesz pracę z Git.
14.2.3. Współpraca przez serwis GitHub Skoro już wiesz, jak zupełnie od początku utworzyć repozytorium GitHub, zobacz, jak można je wykorzystać do współpracy z innymi. Przyjmujemy założenie, że używasz modułu opracowanego przez firmę trzecią i
natknąłeś się na błąd. Możesz przejrzeć kod źródłowy modułu i spróbować znaleźć sposób na usunięcie błędu. Następnie wysyłasz wiadomość e-mail do autora modułu, opisujesz znaleziony błąd i dołączasz pliki zawierające przygotowane poprawki. Takie podejście wymaga od autora wykonania pewnej żmudnej pracy. Autor porównuje otrzymane pliki z najnowszą wersją kodu, a następnie umieszcza w niej poprawki otrzymane od Ciebie. Jeżeli autor korzysta z serwisu GitHub, wówczas możesz sklonować repozytorium projektu autora, wprowadzić pewne zmiany, a następnie przez GitHub poinformować autora o wprowadzeniu poprawki usuwającej znaleziony błąd. GitHub pozwoli autorowi na wyświetlenie różnic między oryginalnym kodem i przygotowanym przez Ciebie. Jeżeli poprawka zostanie zaakceptowana, przygotowane przez Ciebie zmiany zostaną za pomocą pojedynczego kliknięcia myszą umieszczone w najnowszej wersji kodu projektu przygotowanego przez autora modułu. W żargonie GitHub duplikowanie repozytorium nosi nazwę rozwidlenia (ang. forking). Rozwidlenie projektu pozwala na wprowadzenie dowolnych zmian we własnej kopii projektu bez obaw o uszkodzenie oryginalnego repozytorium. Nie musisz mieć zgody autora repozytorium na utworzenie rozwidlenia: każdy może utworzyć rozwidlenie projektu i wprowadzone przez siebie zmiany przekazać autorowi. Autor wcale nie musi zaakceptować otrzymanych zmian, ale nawet wówczas zachowujesz własną wersję z wprowadzonymi zmianami, którą możesz samodzielnie obsługiwać i rozwijać. Jeżeli rozwidlona wersja projektu zyska popularność, inni mogą tworzyć jego rozwidlenia i Tobie oferować wprowadzane przez siebie usprawnienia. Po wprowadzeniu zmian w rozwidleniu projektu możesz je przekazać autorowi oryginalnego projektu poprzez tak zwane żądanie przekazania zmian (ang. pull request), które jest wiadomością skierowaną do autora repozytorium i zawierającą propozycję wprowadzenia zmian. W żargonie GitHub pull oznacza import zmian z rozwidlenia i połączenie ich z kodem w innym rozwidleniu. Na rysunku 14.5 pokazano przykład typowej współpracy za pomocą GitHub. Przeanalizujemy teraz przykład rozwidlenia repozytorium GitHub na potrzeby współpracy nad projektem. Proces został pokazany na rysunku 14.6. Rozwidlenie rozpoczyna proces współpracy przed powieleniem repozytorium GitHub na własnym koncie (co nosi nazwę rozwidlenia) — patrz krok A. Następnie rozwidlone repozytorium zostaje sklonowane do komputera lokalnego (B), w którym po wprowadzeniu zmian są one zatwierdzane (C). Kolejnym krokiem jest przekazanie wprowadzonych zmian z powrotem do serwisu GitHub (D) i wysłanie żądania przekazania zmian autorowi oryginalnego repozytorium z prośbą o rozważenie ich uwzględnienia (E). Jeżeli autor będzie chciał umieścić zmiany w oryginalnym repozytorium, wtedy zaakceptuje wspomniane żądanie przekazania zmian.
Rysunek 14.5. Typowa współpraca przez serwis GitHub
Rysunek 14.6. Proces współpracy nad repozytorium GitHub przez utworzenie rozwidlenia
Przyjmujemy założenie, że chcesz utworzyć rozwidlenie repozytorium node-elf przygotowanego wcześniej w rozdziale, a następnie dodać kod odpowiedzialny za eksport wersji modułu. W ten sposób użytkownicy modułu zyskają pewność, że używają jego odpowiedniej wersji. Przede wszystkim zaloguj się w serwisie GitHub i przejdź na stronę główną r epoz ytor ium https://github.com/mcantelon/node-elf. Znajdziesz na niej
przycisk Fork duplikujący repozytorium. Strona wynikowa będzie podobna do strony oryginalnego repozytorium, ale z komunikatem w stylu forked from mcantelon/node-elf wyświetlanym pod nazwą repozytorium. Po utworzeniu rozwidlenia kolejne kroki to utworzenie kopii repozytorium w komputerze lokalnym, wprowadzenie zmian, a następnie przekazanie ich z powrotem do GitHub. Poniższe polecenia pokazują, jak to zrobić w przypadku repozytorium node-elf: mkdir -p ~/tmp/forktest cd ~/tmp/forktest git clone [email protected]:chickentown/node-elf.git cd node-elf echo "exports.version = '0.0.2';" >> index.js git add index.js git commit -m "Dodano specyfikację wersji modułu." git push origin master
Po wprowadzeniu zmian kliknij przycisk Pull Request na stronie rozwidlenia repozytorium, podaj temat i treść wiadomości, opisując dokonane zmiany. Następnie kliknij przycisk Send Pull Request. Na rysunku 14.7 pokazano przykład typowej treści wiadomości.
Rysunek 14.7. Szczegóły dotyczące żądania przekazania zmian do repozytorium GitHub
Żądanie przekazania kodu zostanie umieszczone na stronie problemów oryginalnego repozytorium. Opiekun oryginalnego repozytorium może po przejrzeniu proponowanych zmian wprowadzić je w repozytorium, klikając przycisk Merge Pull Request, a następnie podać informacje związane z zatwierdzanymi zmianami i kliknąć Confirm Merge. W ten sposób zgłoszenie będzie automatycznie zamknięte. Po współpracy z innymi i utworzeniu wspaniałego modułu kolejnym krokiem jest udostępnienie go społeczności. Najlepszym rozwiązaniem jest wtedy dodanie modułu do repozytorium npm.
14.3. Przekazanie własnego modułu do repozytorium npm
Przypuśćmy, że od pewnego czasu pracowałeś nad modułem przeznaczonym do skracania adresów URL. Teraz uważasz, że moduł w obecnej wersji może być użyteczny dla innych programistów Node. Aby go opublikować, na grupie Google związanej z Node możesz umieścić opis funkcjonalności modułu. W ten sposób jednak ograniczysz liczbę osób, do których dotrzesz. Ponadto nie będziesz miał łatwego sposobu na poinformowanie osób, które zaczną korzystać z Twojego modułu, o wydaniu jego nowszej wersji. Rozwiązaniem problemów związanych z udostępnianiem modułu i jego uaktualnianiem jest publikacja w repozytorium npm. Za pomocą repozytorium npm można bardzo łatwo zdefiniować zależności projektu i pozwolić na ich automatyczną instalację wraz z modułem. Jeżeli utworzyłeś moduł przeznaczony do przechowywania komentarzy dotyczących treści (na przykład postów bloga), możesz wówczas dołączyć jako zależność moduł obsługujący bazę danych MongoDB przeznaczoną na komentarze. W przypadku modułu dostarczającego narzędzie działające z poziomu powłoki zależnością może być moduł pomocniczy przeznaczony do przetwarzania argumentów powłoki. Jak dotąd w książce używaliśmy repozytorium npm do instalacji wszystkiego, począwszy od testowanych frameworków, aż po sterowniki baz danych. Jednak niczego jeszcze nie opublikowaliśmy. W kolejnych punktach przeczytasz, jak wykonać wymienione poniżej kroki w celu publikacji własnej pracy w repozytorium npm. 1. Przygotowanie pakietu. 2. Przygotowanie specyfikacji pakietu. 3. Testowanie pakietu. 4. Publikacja pakietu. Zaczynamy od przygotowania pakietu.
14.3.1. Przygotowanie pakietu Każdy moduł Node, który masz zamiar udostępnić światu, powinien składać się z odpowiednich zasobów, takich jak dokumentacja, przykłady, testy i powiązane z modułem narzędzia działające w powłoce. Ponadto moduł powinien zawierać p l i k README wraz z wystarczającą ilością informacji, aby można było umożliwić użytkownikom szybkie rozpoczęcie pracy z modułem. Katalog modułu powinien być zorganizowany za pomocą podkatalogów. W tabeli 14.2 przedstawiono zgodne z konwencją podkatalogi projektu Node — bin, docs, example, lib i test — oraz ich przeznaczenie. Tabela 14.2. Zgodne z konwencją podkatalogi projektu Node Katalog
Opis
bin
Skrypty powłoki.
docs
Dokumentacja.
example
Przykład użycia aplikacji.
lib
Podstawowa funkcjonalność aplikacji.
test
Skrypty testowe i powiązane z tym zasoby.
Po przygotowaniu odpowiedniej struktury dla pakietu należy go przygotować do publikacji w repozytorium npm poprzez utworzenie specyfikacji pakietu.
14.3.2. Przygotowanie specyfikacji pakietu Kiedy publikujesz pakiet w repozytorium npm, konieczne jest dołączenie czytelnego dla komputera pliku specyfikacji pakietu. Wspomniany plik w formacie JSON nosi nazwę package.json i zawiera informacje o module, takie jak nazwa, opis, wersja, zależności, a także inne cechy charakterystyczne. Nodejitsu oferuje użyteczną stronę internetową (http://package.json.nodejitsu.com/) pokazującą przykładowy plik package.json i objaśniającą poszczególne fragmenty pliku po umieszczeniu nad nimi kursora myszy. W pliku package.json tylko nazwa i wersja to dane obowiązkowe. Pozostałe informacje są opcjonalne, ale jeśli niektóre z nich zostaną podane, to znacznie zwiększą użyteczność modułu. Dzięki zdefiniowaniu charakterystyki bin menedżer npm będzie „wiedział”, które pliki pakietu są narzędziami powłoki, i globalnie je udostępni. Przykładowa specyfikacja pakietu może przedstawiać się następująco: { "name": "elf" , "version": "0.0.1" , "description": "Toy URL shortener" , "author": "Mike Cantelon" , "main": "index" , "engines": { "node": "0.4.x" } }
Więcej dokładnych informacji o dostępnych otrzymasz po wydaniu poniższego polecenia:
opcjach
pliku package.json
npm help json
Ponieważ ręczne wygenerowanie pliku JSON jest tylko nieco bardziej zabawne od ręcznego tworzenia pliku XML, zapoznamy się z pewnymi narzędziami
ułatwiającymi to zadanie. Jednym z nich jest ngen, czyli dostępny w repozytorium npm pakiet, który dodaje polecenie powłoki o nazwie ngen. Po zadaniu kilku pytań narzędzie wygeneruje plik package.json. Ponadto generuje ono także wiele innych plików, które standardowo są umieszczane w pakietach npm, na przykład Readme.md. Instalacja narzędzia ngen następuje po wydaniu poniższego polecenia: npm install -g ngen
Po zainstalowaniu narzędzia ngen uzyskujesz globalne polecenie ngen, które wywołane w katalogu głównym projektu wyświetli kilka pytań, a następnie wygeneruje plik package.json oraz pozostałe, najczęściej stosowane w pakietach Node. Niepotrzebnie wygenerowane pliki można usunąć. Wygenerowany zostaje między innymi plik .gitignore wskazujący pliki i katalogi, które nie powinny być dodawane do repozytorium Git projektu podczas jego publikacji w npm. Ponadto generowany jest plik .npmignore pełniący podobną rolę jak .gitignore i wskazujący pliki, które mają być zignorowane podczas publikacji pakietu w npm. Poniżej przedstawiono przykładowe dane wyjściowe po wydaniu polecenia
ngen:
Project name: elf Enter your name: Mike Cantelon Enter your email: [email protected] Project description: URL shortening library create : /Users/mike/programming/js/shorten/node_modules/.gitignore create : /Users/mike/programming/js/shorten/node_modules/.npmignore create : /Users/mike/programming/js/shorten/node_modules/History.md create : /Users/mike/programming/js/shorten/node_modules/index.js ...
Wygenerowanie pliku package.json to najtrudniejsze zadanie podczas publikacji modułu w repozytorium npm. Po zakończeniu tego kroku jesteś gotowy do opublikowania modułu.
14.3.3. Testowanie i publikowanie pakietu Opublikowanie modułu w repozytorium npm obejmuje trzy wymienione poniżej kroki, które zostaną omówione w tym punkcie: 1. Przetestowanie instalacji lokalnej pakietu. 2. Dodanie użytkownika
npm,
o ile jeszcze tego nie zrobiłeś.
3. Publikacja modułu w repozytorium npm.
Testowanie instalacji pakietu Aby przetestować lokalną instalację pakietu, należy użyć polecenia
link
menedżera npm i wydać je z poziomu katalogu głównego modułu. To polecenie spowoduje globalne udostępnienie modułu w komputerze i Node będzie mogło z niego korzystać tak jak w przypadku pakietu konwencjonalnie zainstalowanego przez npm. sudo npm link
Po globalnym udostępnieniu pakietu można go zainstalować w oddzielnym katalogu testowym, używając poniższego polecenia link wraz z nazwą pakietu: npm link elf
Po instalacji pakietu szybki test polega na użyciu funkcji require() w interfejsie REPL Node, jak przedstawiono w poniższym wierszu kodu. Wynikiem są zmienne lub funkcje dostarczane przez moduł: node > require('elf'); { version: '0.0.1', initPathData: [Function], shorten: [Function], expand: [Function] }
Jeżeli pakiet przeszedł test i zakończyłeś już nad nim prace, wtedy z poziomu katalogu głównego modułu należy wydać polecenie unlink menedżera npm: sudo npm unlink
Teraz moduł nie będzie już dłużej dostępny globalnie w komputerze. Po zakończeniu procesu publikacji pakietu w repozytorium npm będziesz mógł go zainstalować w standardowy sposób, czyli przez wydanie polecenia install menedżera npm. Po przetestowaniu pakietu kolejnym krokiem jest utworzenie konta npm przeznaczonego do publikacji pakietów, o ile takiego konta nie utworzyłeś już wcześniej.
Dodanie użytkownika npm Poniższe polecenie powoduje utworzenie Twojego przeznaczonego do publikacji pakietu w repozytorium npm:
własnego
konta
npm adduser
Zostaniesz poproszony o podanie nazwy użytkownika, adresu e-mail i hasła do konta. Jeżeli operacja tworzenia konta zakończy się powodzeniem, na ekranie nie będzie wyświetlony żaden komunikat błędu.
Publikacja w repozytorium npm Kolejnym krokiem jest publikacja modułu. Wydanie poniższego polecenia powoduje opublikowanie modułu:
npm publish
Na ekranie zostanie wyświetlony komunikat Sending authorization over an insecure channel (wysyłanie danych uwierzytelniających przez niezabezpieczony kanał), ale jeśli nie zobaczysz żadnych dodatkowych komunikatów błędu, będzie to oznaczało, że publikacja modułu zakończyła się powodzeniem. Sukces publikacji modułu możesz potwierdzić za pomocą polecenia view menedżera npm: npm view elf description
Jeżeli chcesz, możesz dołączyć jedno lub więcej prywatnych repozytoriów jako zależności pakietu. Tego rodzaju sytuacja występuje, gdy być może masz moduł z użytecznymi funkcjami pomocniczymi, których chcesz używać, ale nie chcesz udostępniać ich publicznie w repozytorium npm. Aby dodać zależność w postaci prywatnego repozytorium, w miejscu przeznaczonym do umieszczania nazw zależności modułu podaj dowolną nazwę inną niż pozostałe zależności. Tam, gdzie normalnie umieszcza się wersję pakietu, podaj adres URL repozytorium Git. W przedstawionym poniżej przykładzie będącym fragmentem pliku package.json ostatnia zależność przedstawia repozytorium prywatne: "dependencies" : { "optimist" : ">=0.1.3", "iniparser" : ">=1.0.1", "mingy": ">=0.1.2", "elf": "git://github.com/mcantelon/node-elf.git" },
Pamiętaj, że wszelkie moduły prywatne również powinny zawierać pliki package.json. Aby upewnić się, że tego rodzaju moduły nie zostaną przypadkowo opublikowane, w pliku package.json przypisz właściwości private wartość true: "private": true,
W ten sposób już wiesz, jak skonfigurować, przetestować i opublikować własny moduł w repozytorium npm.
14.4. Podsumowanie Podobnie jak jest w przypadku większości projektów open source, które osiągnęły sukces, także Node charakteryzuje się aktywną społecznością w internecie. Oznacza to dostępność ogromnej ilości zasobów internetowych, a także możliwość szybkiego znalezienia odpowiedzi na nurtujące Cię pytania. Do wspomnianych zasobów zaliczamy między innymi grupy Google, kanały IRC i strony zgłaszania błędów w GitHub.
Poza miejscem zgłaszania błędów serwis GitHub oferuje również możliwość hostingu repozytorium Git i przeglądania kodu repozytorium Git za pomocą przeglądarki internetowej. Dzięki GitHub inni programiści mogą łatwo tworzyć rozwidlenia Twojego kodu open source, aby wprowadzać w nim poprawki, dodawać nowe funkcje lub w ogóle skierować projekt na nowe tory. Zmiany wprowadzone w kodzie można bardzo łatwo przekazywać z powrotem do oryginalnego repozytorium. Od chwili udostępnienia użytkownikom projektu Node mogą oni przekazywać własne moduły do repozytorium Node Package Manager. Dzięki publikacji modułu w npm inni użytkownicy łatwiej go znajdą. A jeśli Twój projekt jest modułem, umieszczenie go w repozytorium npm znacznie ułatwi instalację modułu. Teraz już wiesz, gdzie szukać pomocy, jak współpracować z innymi przez internet oraz jak dzielić się owocami swojej pracy. Node istnieje dzięki aktywnej i zaangażowanej w prace nad nim społeczności. Zachęcamy Cię, abyś stał się aktywnym członkiem wspomnianej społeczności Node!
Dodatek A Instalacja Node i dodatki opracowane przez społeczność Node można bardzo łatwo zainstalować w większości systemów operacyjnych za pomocą konwencjonalnych aplikacji instalatorów lub też z użyciem wiersza poleceń. Drugie z wymienionych rozwiązań jest bardzo łatwe do wykonania w systemach OS X i Linux, ale niezalecane w Windows. Aby pomóc Ci w rozpoczęciu pracy, w tym dodatku dokładnie omówiono instalację Node w systemach operacyjnych OS X, Windows i Linux. Na końcu dodatku dowiesz się, jak używać menedżera pakietów Node (npm) do wyszukiwania i instalacji użytecznych dodatków.
A.1. Instalacja w systemie OS X Instalacja Node w systemie OS X jest bardzo prosta. Pokazany na rysunku A.1 oficjalny instalator jest dostępny na stronie http://nodejs.org/download/ i pozwala na łatwą instalację skompilowanej wersji Node i menedżera npm. Jeżeli jednak wolisz przeprowadzić instalację ze źródeł, możesz skorzystać z narzędzia o nazwie Homebrew (http://brew.sh/), które automatyzuje proces instalacji ze źródeł. Ewentualnie przeprowadź ręczną instalację ze źródeł. Warto w tym miejscu wspomnieć, że instalacja Node ze źródeł w systemie OS X wymaga posiadania zainstalowanych w systemie narzędzi programistycznych OS X. Xcode. Jeżeli nie masz jeszcze zainstalowanego środowiska programistycznego Xcode, możesz je pobrać ze strony Apple (http://developer.apple.com/downloads/). Uzyskanie dostępu do wymienionej strony wymaga przeprowadzenia bezpłatnej rejestracji konta programisty na platformie Apple. Xcode to całkiem duża aplikacja (około 4 GB). Alternatywnym rozwiązaniem oferowanym przez Apple jest pakiet Command Line Tools for Xcode, który można pobrać z tej samej strony. Wymieniony pakiet zawiera minimalną ilość narzędzi potrzebnych do kompilacji Node oraz innych projektów oprogramowania typu open source.
Rysunek A.1. Oficjalny instalator Node dla systemu OS X
Aby szybko sprawdzić, czy masz zainstalowaną w systemie aplikację Xcode, uruchom narzędzie Terminal, a następnie wydaj polecenie xcodebuild. Jeżeli masz zainstalowaną aplikację Xcode, otrzymasz komunikat błędu informujący, że w katalogu bieżącym nie znaleziono projektu Xcode. Podczas instalacji konieczne może być wydawanie poleceń z poziomu powłoki. Do tego celu wykorzystaj narzędzie Terminal, które standardowo znajduje się w katalogu Programy/Narzędzia. Jeżeli przeprowadzasz kompilację ze źródeł, zajrzyj do podrozdziału A.4, w którym znajdziesz omówienie kroków niezbędnych do wykonania.
A.1.1. Instalacja za pomocą Homebrew W systemie OS X Node można bardzo łatwo zainstalować za pomocą Homebrew, czyli menedżera pakietów przeznaczonego do instalacji oprogramowania typu open source. Pierwszym krokiem jest instalacja samego menedżera Homebrew, co wymaga wydania poniższego polecenia w powłoce: ruby -e "$(curl -fsSkL raw.github.com/mxcl/homebrew/go)"
Gdy menedżer Homebrew znajduje się już w systemie, instalację Node możesz przeprowadzić, wydając polecenie: brew install node
Podczas kompilacji kodu przez Homebrew w oknie narzędzia Terminal będzie wyświetlana duża ilość tekstu, który możesz przewijać. Wspomniany tekst zawiera informacje związane z procesem kompilacji; możesz je spokojnie zignorować.
A.2. Instalacja w systemie Windows W systemie Windows instalację Node można przeprowadzić za pomocą oficjalnego instalatora dostępnego na stronie http://nodejs.org/download/. Po instalacji Node i menedżera npm będziesz mógł uruchamiać z poziomu wiersza poleceń Windows. Rozwiązanie alternatywne polega na instalacji Node przez przeprowadzenie kompilacji ze źródeł. Takie podejście jest znacznie bardziej skomplikowane i wymaga użycia projektu o nazwie Cygwin, który dostarcza środowisko zgodne z systemem UNIX. Niemal na pewno będziesz chciał uniknąć użycia Node za pomocą projektu Cygwin, o ile nie próbujesz używać modułów, które w Windows nie działają w innej konfiguracji lub wymagają kompilacji (przykładem mogą być tutaj moduły sterowników pewnych baz danych). Aby przeprowadzić instalację Cygwin, przejdź na stronę http://cygwin.com/install.html, a następnie pobierz plik setup.exe. Dwukrotne kliknięcie pobranego pliku powoduje uruchomienie kreatora instalacji. Naciskaj przycisk Dalej, zatwierdzając tym samym opcje domyślne, aż do chwili dotarcia do kroku o nazwie Choose a Download Site. Wybierz dowolną witrynę z listy, a następnie kliknij przycisk Dalej. Jeżeli kreator wyświetli komunikat ostrzeżenia, naciśnij przycisk OK, aby kontynuować proces instalacji. Kreator powinien teraz wyświetlić pokazane na rysunku A.2 okno wyboru pakietów Cygwin.
Rysunek A.2. Okno wyboru pakietów Cygwin pozwala na wskazanie oprogramowania open source, które zostanie zainstalowane w systemie
Korzystając z wymienionego okna, wybierz pakiety, które mają znaleźć się w tworzonym środowisku zgodnym z systemem UNIX. Listę pakietów potrzebnych do pracy z Node przedstawiono w tabeli A.1. Tabela A.1. Pakiety Cygwin niezbędne do uruchomienia Node Kategoria
Pakiet
devel
gcc4-g++
devel
git
devel
make
devel
openssl-devel
devel
pkg-config
devel
zlib-devel
net
inetutils
python
python
web
wget
Po wybraniu wymaganych pakietów kliknij przycisk Dalej.
W kolejnym kroku zostanie wyświetlona lista pakietów będących zależnościami dla wybranych wcześniej. Ponieważ je również musisz zainstalować, kliknij pr z ycisk Dalej, aby zaakceptować wskazane pakiety. Cygwin rozpocznie pobieranie potrzebnych pakietów. Kiedy pobieranie zakończy się, naciśnij przycisk Zakończ. Uruchom Cygwin, klikając ikonę umieszczoną na pulpicie lub w menu Start. Na ekranie zostanie wyświetlony wiersz poleceń. Teraz możesz już przystąpić do kompilacji Node (wymagane kroki zostały omówione w podrozdziale A.4).
A.3. Instalacja w systemie Linux Instalacja Node w systemie Linux zwykle jest bardzo prosta. W tym dodatku przedstawiono instalację na podstawie kodu źródłowego w dwóch popularnych dystrybucjach: Ubuntu i CentOS. Node jest dostępne także za pomocą menedżerów pakietów w wielu innych dystrybucjach. Informacje dotyczące instalacji znajdziesz w serwisie GitHub: https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager.
A.3.1. Przygotowania do instalacji w Ubuntu Przed rozpoczęciem instalacji Node w Ubuntu konieczna jest instalacja wymaganych pakietów. W systemie Ubuntu 11.04 lub nowszym wystarczy wydać poniższe polecenie: sudo apt-get install build-essential libssl-dev
sudo — polecenie sudo jest używane do wykonania innego polecenia z uprawnieniami superużytkownika (nazywanego również root). Polecenie sudo jest bardzo często stosowane podczas instalacji oprogramowania, ponieważ pliki muszą być umieszczane w chronionych obszarach systemu plików, a użytkownik root ma dostęp do każdego pliku w systemie niezależnie od uprawnień zdefiniowanych dla pliku.
A.3.2. Przygotowania do instalacji w CentOS Przed instalacją Node w CentOS konieczna jest instalacja wymaganych pakietów. W systemie CentOS 5 lub nowszym wystarczy wydać poniższe polecenia: sudo yum groupinstall 'Development Tools' sudo yum install openssl-devel
Po zainstalowaniu wymaganych pakietów można przystąpić do kompilacji Node.
A.4. Kompilacja Node Kroki niezbędne do przeprowadzenia kompilacji Node są takie same we wszystkich systemach operacyjnych. W powłoce należy wydać poniższe polecenie powodujące utworzenie katalogu tymczasowego, do którego pobierzemy kod źródłowy Node: mkdir tmp
Kolejnym krokiem jest przejście do utworzonego wcześniej katalogu: cd tmp
Teraz wydaj poniższe polecenie, aby pobrać kod źródłowy Node: curl -O http://nodejs.org/dist/node-latest.tar.gz
Na ekranie będą wyświetlane informacje o postępie podczas pobierania pliku. Po pobraniu pliku w powłoce zostanie wyświetlony znak zachęty. Wydanie poniższego polecenia powoduje rozpakowanie pobranego archiwum: tar zxvf node-latest.tar.gz
W trakcie rozpakowywania na ekranie będzie wyświetlona duża ilość danych wyjściowych operacji, a następnie ponownie zobaczysz znak zachęty. Poniższe polecenie powoduje wyświetlenie listy plików znajdujących się w katalogu bieżącym, między innymi nazwy rozpakowanego przed chwilą katalogu: ls
Wydanie poniższego polecenia powoduje przejście do wspomnianego katalogu: cd node-v*
W tym momencie znajdujesz się w katalogu zawierającym kod źródłowy Node. Wydanie poniższego polecenia powoduje uruchomienie skryptu konfiguracyjnego, który przygotowuje instalację oprogramowania dla używanego przez Ciebie systemu operacyjnego: ./configure
Kompilacja Node jest prosta i wymaga wydania poniższego polecenia: make
Proces kompilacji może wymagać nieco czasu, a więc zachowaj cierpliwość. W trakcie kompilacji na ekranie będzie wyświetlana ogromna ilość komunikatów. Wspomniane komunikaty zawierają informacje związane z procesem kompilacji i możesz je spokojnie zignorować. Niedoskonałość Cygwin. Jeżeli uruchomiłeś Cygwin w systemie Windows 7 lub Vista, w trakcie kompilacji mogą wystąpić błędy. Wynikają one z problemów związanych z Cygwin, a nie Node. Rozwiązaniem jest zamknięcie wiersza poleceń Cygwin, a następnie uruchomienie aplikacji wiersza poleceń
ash.exe (znajdziesz ją w katalogu Cygwin, najczęściej c:\cygwin\bin\ash.exe). W uruchomionym wierszu poleceń wydaj polecenie /bin/rebaseall -v. Po wykonaniu polecenia uruchom ponownie komputer. To powinno rozwiązać problemy związane z Cygwin. Na tym etapie już prawie zakończyliśmy instalację. Kiedy przestaną pojawiać się nowe komunikaty i ponownie zobaczysz znak zachęty, wydaj ostatnie polecenie w procesie instalacji Node: sudo make install
Po wykonaniu powyższego polecenia wydanie poniższego spowoduje wyświetlenie numeru wersji Node. Dzięki temu można potwierdzić, że instalacja zakończyła się powodzeniem. node -v
W ten sposób zainstalowałeś Node w systemie!
A.5. Używanie menedżera pakietów Node Po zainstalowaniu Node zyskasz możliwość użycia wbudowanych modułów zapewniających dostęp do API przeznaczonego do wykonywania zadań związanych z siecią, pracy z systemem plików oraz innych operacji najczęściej wymaganych w aplikacjach. Wbudowane moduły Node są określane mianem podstawowych modułów Node. Wprawdzie oferują one wiele użytecznych funkcji, ale prawdopodobnie będziesz chciał korzystać także z funkcji opracowanych przez społeczność. Na rysunku A.3 pokazano koncepcję relacji między modułami podstawowymi Node i dodatkowymi. W zależności od używanego języka programowania idea opracowywanych przez społeczność repozytoriów zawierających moduły z funkcjami dodatkowymi może być Ci znana lub nieznana. Wspomniane repozytoria przypominają biblioteki i stanowią użyteczne bloki budulcowe aplikacji pomagające w realizacji zadań, których nie można w łatwy sposób wykonać za pomocą standardowego języka. Repozytoria są najczęściej modułowe: zamiast od razu pobierać całą bibliotekę, wystarczy pobrać jedynie niezbędne dodatki. Społeczność Node opracowała własne narzędzie przeznaczone do zarządzania dodatkami: menedżer npm (ang. Node Package Manager). W tym podrozdziale dowiesz się, jak używać menedżera npm do wyszukiwania dodatków, przeglądania dokumentacji dodatków, a także analizy kodu źródłowego dodatków.
Rysunek A.3. Stos Node składa się z funkcji dostępnych globalnie, modułów podstawowych oraz opracowanych przez społeczność
W moim systemie nie ma menedżera npm Jeżeli zainstalowałeś Node, to menedżer npm również został zainstalowany. Możesz się o tym przekonać, wydając polecenie npm w powłoce i sprawdzając, jaka będzie odpowiedź powłoki. Jeśli faktycznie nie masz menedżera npm w systemie, jego instalacja wymaga wydania poniższych poleceń: cd /tmp git clone git://github.com/isaacs/npm.git cd npm sudo make install
Po zainstalowaniu menedżera npm wydaj poniższe polecenie, aby upewnić się, że działa (polecenie nakazuje wyświetlenie numeru wersji menedżera): npm -v
Jeżeli menedżer jest zainstalowany prawidłowo, powinieneś zobaczyć dane
wyjściowe (numer wersji) podobne do poniższych: 1.0.3
W przypadku jakichkolwiek problemów z instalacją npm najlepszym rozwiązaniem jest przejście na stronę projektu npm w serwisie GitHub (https://github.com/npm/npm), na której znajdziesz aktualne informacje dotyczące instalacji menedżera.
A.5.1. Wyszukiwanie pakietów Menedżer npm oferuje wygodne rozwiązanie w zakresie uzyskiwania dostępu do dodatków Node opracowanych przez społeczność. Wspomniane dodatki są nazywane pakietami i są przechowywane w repozytorium. Dla użytkowników PHP, Ruby i Perla menedżer npm można określić jako odpowiedniki narzędzi PEAR, Gem i CPAN. Menedżer npm jest niezwykle wygodnym narzędziem. Za jego pomocą można pobierać i instalować pakiety, wydając tylko jedno polecenie. Bardzo łatwo można również wyszukiwać pakiety, przeglądać ich dokumentację, zapoznawać się z kodem źródłowym pakietu, a nawet publikować własne, udostępniając je tym samym całej społeczności Node. Do wyszukiwania pakietów znajdujących się w repozytorium służy polecenie search. Na przykład jeśli chcesz znaleźć generator XML, wtedy możesz wydać poniższe, proste polecenie: npm search xml generator
Pierwsze użycie menedżera npm do operacji wyszukiwania będzie trwało nieco dłużej, ponieważ konieczne jest pobranie informacji o repozytorium. Jednak kolejne operacje wyszukiwania będą przeprowadzane już znacznie szybciej. Alternatywnym rozwiązaniem dla polecenia wydawanego w powłoce jest interfejs wyszukiwania dostępny przez przeglądarkę internetową. Wystarczy przejść na witrynę https://www.npmjs.org/. Podana witryna (pokazana na rysunku A.4) dostarcza również dane statystyczne dotyczące pakietów, między innymi całkowitą liczbę pakietów, pakiety będące najczęściej zależnościami dla innych pakietów, a także ostatnio uaktualnione.
Rysunek A.4. Witryna npmjs.org podaje użyteczne dane statystyczne dotyczące pakietów
Interfejs w postaci wymienionej witryny internetowej pozwala także na przeglądanie poszczególnych pakietów, wyświetla użyteczne informacje o nich, takie jak zależności, oraz podaje położenie repozytorium kontroli wersji dla pakietu.
A.5.2. Instalacja pakietu Po znalezieniu interesującego Cię pakietu istnieją dwa podstawowe sposoby jego instalacji: lokalnie lub globalnie. Instalacja lokalna powoduje umieszczenie pobranego modułu w katalogu o nazwie node_modules, znajdującym się w bieżącym katalogu roboczym. Jeżeli wymieniony katalog nie istnieje, wtedy menedżer npm utworzy go. Poniżej przedstawiono przykład instalacji lokalnej pakietu o nazwie
express:
npm install express
W systemach operacyjnych innych niż Windows instalacja globalna powoduje umieszczenie pobranego modułu w katalogu o nazwie /usr/local. Wymieniony katalog jest tradycyjnie używany przez systemy z rodziny UNIX do przechowywania aplikacji instalowanych przez użytkownika. Z kolei w systemach Windows dla modułów instalowanych globalnie jest przeznaczony katalog Appdata\Roaming\npm. Poniżej przedstawiono przykład instalacji globalnej pakietu o nazwie
express:
npm install -g express
Jeżeli nie masz wystarczających uprawnień do przeprowadzenia instalacji globalnej, to na początku polecenia powinieneś umieścić sudo, na przykład: sudo npm install -g express
Po zainstalowaniu pakietu kolejnym krokiem jest ustalenie sposobu jego działania. Na szczęście dzięki menedżerowi npm jest to łatwe zadanie.
A.5.3. Przeglądanie dokumentacji i kodu pakietu Menedżer npm oferuje wygodny sposób przeglądania dokumentacji pakietu, o ile taka jest dostępna. Polecenie docs powoduje otworzenie przeglądarki internetowej, w której zostaje wyświetlona dokumentacja wskazanego pakietu. Poniżej przedstawiono przykład wyświetlania dokumentacji pakietu o nazwie express: npm docs express
Dokumentację można wyświetlić, nawet jeśli pakiet nie został zainstalowany. Jeżeli dokumentacja pakietu jest niekompletna lub niejasna, wtedy często użytecznym rozwiązaniem jest przejrzenie jego kodu źródłowego. Menedżer npm ułatwia także i to zadanie. Powoduje utworzenie podpowłoki, w której katalogiem najwyższego poziomu jest bieżący katalog roboczy plików kodu źródłowego pakietu. Poniżej przedstawiono przykład przeglądania plików kodu źródłowego lokalnie zainstalowanego pakietu express: npm explore express
Aby przeglądać kod źródłowy pakietu zainstalowanego globalnie, wystarczy po prostu dodać opcję -g po poleceniu npm, na przykład: npm -g explore express
Analiza kodu źródłowego pakietu do również doskonały sposób nauki. Dzięki przeglądaniu kodu źródłowego Node bardzo często poznajesz nowe techniki programowania oraz organizacji kodu.
Dodatek B Debugowanie Node Podczas prac programistycznych, a zwłaszcza w trakcie poznawania nowego języka lub frameworka, narzędzia i techniki debugowania mogą być niezwykle użyteczne. W tym dodatku poznasz sposoby ustalenia, co tak naprawdę dzieje się w aplikacji Node.
B.1. Analiza kodu za pomocą JSHint Błędy związane ze składnią lub zasięgiem są poważnym problemem w trakcie programowania. Podczas próby określenia podstawowej przyczyny problemu pierwszą linią obrony jest przejrzenie kodu. Jeśli jednak przejrzysz kod źródłowy i od razu nie wychwycisz błędu, kolejną możliwością wartą wypróbowania jest uruchomienie narzędzia sprawdzającego kod źródłowy pod kątem błędów. JSHint to jedno z tego rodzaju narzędzi. Potrafi poinformować o błędach zarówno poważnych, takich jak wywołania funkcji niezdefiniowanych w kodzie, jak i bardziej błahych, na przykład niestosowanie się do konwencji JavaScript w zakresie wielkości znaków w konstruktorach klas. Nawet jeśli nigdy nie korzystałeś z narzędzia JSHint, przeczytanie o wyszukiwanych przez nie błędach pozwala na zdobycie cennej wiedzy o czyhających pułapkach. JSHint to projekt oparty na JSLint, czyli dostępnym od dekady kodzie źródłowym JavaScript narzędzia analizy. Jednak w przeciwieństwie do JSHint wspomniane JSLint to narzędzie nie oferujące zbyt dużych możliwości w zakresie konfiguracji. W opinii wielu osób narzędzie JSLint jest zbyt rygorystyczne pod względem stylistyki. Z kolei JSHint pozwala na wskazanie, co chcesz sprawdzić, a co ma zostać zignorowane. Na przykład średniki są z technicznego punktu widzenia wymagane przez interpretery JavaScript, ale większość interpreterów stosuje zautomatyzowane wstawianie średników (ang. Automated Semicolon Insertion, ASI) i umieszcza je tam, gdzie ich zabrakło. Z tego powodu niektórzy programiści celowo pomijają je w kodzie źródłowym, aby zwiększyć jego przejrzystość, a sam kod działa bez zastrzeżeń. Narzędzie JSLint będzie uznawało brak średników za błąd, natomiast JSHint można skonfigurować w taki sposób, aby ignorowało ten „błąd” i sprawdzało kod pod kątem innych poważnych błędów. Instalacja JSHint udostępnia polecenie powłoki o nazwie jshint, które sprawdza kod źródłowy. Narzędzie JSHint powinno być zainstalowane globalnie za pomocą menedżera npm przez wydanie poniższego polecenia:
npm install -g jshint
Po instalacji JHint można sprawdzić pliki JavaScript przez wydanie polecenia podobnego do poniższego: jshint aplikacja.js
W większości przypadków będziesz chciał utworzyć plik konfiguracyjny dla JSHint wskazujący to, co powinno być sprawdzone. Jednym z możliwych rozwiązań jest skopiowanie do komputera lokalnego domyślnego pliku konfiguracyjnego dostępnego w serwisie GitHub (https://github.com/jshint/node-jshint/blob/master/.jshintrc), a następnie jego modyfikacja. Jeżeli przygotowany plik konfiguracyjny nazwiesz .jshintrc i umieścisz w katalogu aplikacji lub w dowolnym katalogu nadrzędnym dla aplikacji, to narzędzie automatycznie znajdzie ten plik i go użyje. Alternatywne rozwiązanie polega na użyciu opcji config i wskazaniu położenia pliku konfiguracyjnego. Poniższy przykład wywołania JSHint nakazuje narzędziu użycie pliku konfiguracyjnego o niestandardowej nazwie: jshint aplikacja.js --config /home/michal/jshint.json
Więcej informacji na temat konkretnych opcji konfiguracyjnych znajdziesz na stronie http://www.jshint.com/docs/#options.
B.2. Dane wyjściowe debugowania Gdy kod źródłowy wydaje się prawidłowy, ale aplikacja nadal działa w sposób inny od oczekiwanego, wówczas warto wyświetlić dane wyjściowe procesu debugowania, aby jeszcze dokładniej dowiedzieć się, co tak naprawdę dzieje się z aplikacją.
B.2.1. Debugowanie za pomocą modułu console Node zawiera wbudowany moduł console, który oferuje funkcje użyteczne podczas debugowania i wyświetlania danych wyjściowych w konsoli.
Wyświetlanie informacji o stanie aplikacji Funkcja console.log() jest używana do wyświetlania w standardowym wyjściu danych wyjściowych zawierających informacje o stanie aplikacji. Inna nazwa tej funkcji to console.info(). Istnieje możliwość podania funkcji argumentów w stylu printf() (http://pl.wikipedia.org/wiki/Printf): console.log('Counter: %d', counter);
Podobnie działają funkcje
console.warn()
i console.error(), które są przeznaczone
do wyświetlania ostrzeżeń i błędów. Jedyna różnica polega na tym, że dane są kierowane do standardowego wyjścia błędów zamiast do standardowego wyjścia. Dzięki temu można je przekierować (jeśli występuje taka potrzeba) do pliku dziennika zdarzeń, jak przedstawiono w poniższym przykładzie: node server.js 2> error.log
F unk c j a console.dir() wyświetla zawartość wskazanego pokazano przykładowe dane wyjściowe wymienionej funkcji:
obiektu.
Poniżej
{ name: 'Jan Kowalski', interests: [ 'sport', 'polityka', 'muzyka', 'teatr' ] }
Dane wyjściowe dotyczące pomiaru czasu Moduł console zawiera dwie funkcje, które gdy są używane razem, pozwalają na pomiar czasu wykonywania fragmentów kodu. Jednocześnie można mierzyć więcej niż tylko jeden aspekt. Aby rozpocząć pomiar czasu, poniższy wiersz kodu należy umieścić w miejscu, w którym ma się rozpocząć pomiar: console.time('danyKomponent');
W celu zakończenia pomiaru i podania czasu, jaki upłynął od rozpoczęcia pomiaru, w miejscu zakończenia pomiaru trzeba umieścić wiersz kodu: console.timeEnd('danyKomponent');
Powyższy kod wyświetli zmierzony czas.
Wyświetlanie stosu wywołań Stos wywołań dostarcza informacje o funkcjach wywoływanych przed dotarciem do wskazanego punktu w logice aplikacji. Kiedy w trakcie działania programu Node wystąpi błąd, wówczas może zostać wyświetlony stos wywołań zawierający informacje o tym, co w logice aplikacji doprowadziło do wystąpienia błędu. W dowolnym punkcie aplikacji można wyświetlić stos wywołań bez konieczności zatrzymywania działania aplikacji. W tym celu wystarczy jedynie wywołać funkcję console.trace(). Wspomniane poniższych:
wywołanie
spowoduje
wyświetlenie
Trace: at lastFunction (/Users/mike/tmp/app.js:12:11) at secondFunction (/Users/mike/tmp/app.js:8:3) at firstFunction (/Users/mike/tmp/app.js:4:3) at Object. (/Users/mike/tmp/app.js:15:3) ...
danych
podobnych
do
Pamiętaj, że informacje znajdujące się na stosie wywołań są wyświetlane w odwrotnej kolejności chronologicznej.
B.2.2. Użycie modułu debug do zarządzania danymi wyjściowymi procesu debugowania Dane wyjściowe debugowania są użyteczne, ale jeśli akurat nie będziesz aktywnie szukał problemu, wtedy mogą wprowadzać jedynie dodatkowe zamieszanie. Idealnym rozwiązaniem jest możliwość włączania i wyłączania wyświetlania danych wyjściowych debugowania. Jednym ze sposobów włączania i wyłączania danych wyjściowych debugowania jest użycie zmiennej środowiskowej. Opracowany przez T.J. Holowaychuka moduł debug oferuje użyteczne narzędzie do tego celu. Zarządzanie wyświetlaniem danych wyjściowych debugowania następuje za pomocą zmiennej środowiskowej DEBUG. Więcej informacji na ten temat znajdziesz w rozdziale 13.
B.3. Debuger wbudowany w Node Gdy potrzeby w zakresie debugowania wykraczają poza proste dane wyjściowe procesu debugowania, Node oferuje wbudowany debuger działający z poziomu powłoki. Wywołanie debugera następuje przez uruchomienie aplikacji wraz ze słowem kluczowym debug, na przykład: node debug server.js
Po uruchomieniu aplikacji w ten sposób na ekranie zobaczysz kilka pierwszych wierszy aplikacji oraz znak zachęty debugera, jak pokazano na rysunku B.1.
Rysunek B.1. Uruchomienie debugera wbudowanego w Node
Komunikat break in server.js:1 oznacza, że debuger zatrzymał wykonywanie programu przed wykonaniem pierwszego wiesza kodu.
B.3.1. Nawigacja po debugerze Po wyświetleniu znaku zachęty debugera można kontrolować działanie aplikacji. Polecenie next (lub po prostu n) powoduje wykonanie kolejnego wiesza kodu. Z kolei polecenie cont (lub c) powoduje wykonywanie aplikacji, dopóki nie zostanie przerwane. Zatrzymanie debugera może nastąpić przez zakończenie działania aplikacji lub po dojściu do tak zwanego punktu kontrolnego. Wspomniane punkty kontrolne to punkty, w których debuger ma wstrzymać działanie aplikacji, aby można było przeanalizować jej stan. Jednym ze sposobów dodania punktu kontrolnego jest dodanie w aplikacji wiersza, w którym ma zostać umieszczony punkt kontrolny. Wspomniany wiersz powinien zawierać polecenie debugger;, jak przedstawiono w listingu B.1. Wiersz debugger; nie ma żadnego negatywnego wpływu na normalne działanie aplikacji, a więc można go bez obaw pozostawić w kodzie źródłowym programu. Listing B.1. Programowe dodanie punktu kontrolnego var http = require('http'); function handleRequest(req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Witaj, świecie\n'); } http.createServer(function (req, res) { debugger; Dodanie punktu kontrolnego do kodu. handleRequest(req, res); }).listen(1337, '127.0.0.1'); console.log('Serwer jest dostępny pod adresem http://127.0.0.1:1337/');
Jeżeli w trybie debugowania uruchomisz program przedstawiony w listingu B.1, pierwsze wstrzymanie działania nastąpi w wierszu pierwszym. Po wydaniu polecenia cont w debugerze działanie aplikacji będzie kontynuowane do czasu utworzenia serwera i oczekiwania na połączenie. Po nawiązaniu połączenia przez przejście na stronę http://127.0.0.1:1337 w przeglądarce internetowej przekonasz się, że działanie programu zostało wstrzymane w wierszu debugger;. Wydanie polecenia next spowoduje wykonanie kolejnego wiersza kodu. Bieżącym wierszem stanie się wywołanie handleRequest(). Jeżeli znów wydasz polecenie next, aby wykonać kolejny wiersz kodu, debuger nie zejdzie do kolejnego wiersza funkcji handleRequest(). Wydanie polecenia step powoduje przejście do funkcji handleRequest() i pozwala na rozwiązanie wszelkich związanych z nią problemów. Gdy zmienisz zdanie dotyczące debugowania funkcji handleRequest(), wydaj polecenie out (lub o), co spowoduje opuszczenie funkcji.
Punkty kontrolne nie muszą być definiowane jedynie w kodzie źródłowym, można je ustawić także z poziomu debugera. Aby w debugerze ustawić punkt kontrolny w bieżącym wierszu, należy wydać polecenie setBreakpoint() (lub sb()). Istnieje także możliwość ustawienia punktu kontrolnego we wskazanym wierszu (sb(wiersz)) lub w konkretnej funkcji (sb('fn()')). Kiedy
chcesz
usunąć punkt kontrolny, do dyspozycji masz funkcję clearBreakpoint() (cb()). Wymieniona funkcja pobiera takie same argumenty jak setBreakpoint(), ale stosuje je w przeciwnym celu.
B.3.2. Analiza i zmiana stanu w debugerze Jeżeli chcesz monitorować określone wartości w aplikacji, możesz dodać tak zwanych wartowników. Informują oni o wartości zmiennej, gdy poruszasz się po kodzie. Na przykład podczas debugowania kodu przedstawionego w listingu B.1 możesz wydać polecenie watch ("req.headers['user-agent']"). W każdym kroku będziesz widział, jaka przeglądarka internetowa wykonała żądanie. Wyświetlenie listy wartowników następuje po wydaniu polecenia watchers. Aby usunąć wartownika, należy użyć polecenia unwatch, na przykład unwatch("req .headers['user- agent']"). Jeżeli na jakimkolwiek etapie procesu debugowania będziesz chciał w pełni przeanalizować lub zmienić stan, wówczas możesz użyć polecenia repl, aby przejść do interfejsu REPL. W ten sposób zyskujesz możliwość podania dowolnego wyrażenia JavaScript i zostanie ono obliczone. Opuszczenie interfejsu REPL i powrót do debugera następuje po naciśnięciu klawiszy Ctrl+C. Po zakończeniu debugowania opuszczenie debugera następuje po dwukrotnym naciśnięciu klawiszy Ctrl+C, naciśnięciu Ctrl+D lub po wydaniu polecenia .exit. Tutaj przedstawiono jedynie podstawy użycia debugera. Więcej informacji na temat tego, co można zrobić za pomocą debugera, znajdziesz na stronie http://nodejs.org/api/debugger.html.
B.4. Inspektor Node Inspektor Node to alternatywa dla wbudowanego debugera Node. Jest używany przez przeglądarki internetowe zbudowane w oparciu o silnik WebKit, na przykład Chrome i Safari, a nie dostępny jako interfejs z poziomu powłoki.
B.4.1. Uruchomienie inspektora Node Zanim rozpoczniesz debugowanie, powinieneś zainstalować inspektora Node
globalnie za pomocą przedstawionego poniżej polecenia. Po instalacji w systemie będzie dostępne polecenie node-inspector: npm install -g node-inspector
W celu rozpoczęcia debugowania aplikacji Node uruchom ją z opcją powłoce:
--debug-brk
w
node --debug-brk server.js
Użycie opcji --debug-brk powoduje, że proces debugowania wstawi punkt kontrolny przed pierwszym wierszem aplikacji. Jeżeli jest to niepożądane, wtedy zamiast wymienionej można użyć opcji --debug. Po uruchomieniu aplikacji należy uruchomić inspektora Node: node-inspector
Inspektor Node jest interesujący, ponieważ używa tego samego kodu co inspektor sieciowy WebKit. Zostaje jednak umieszczony w silniku JavaScript Node, więc programiści sieciowi powinni czuć się jak w domu podczas jego użycia. Po uruchomieniu inspektora Node przejdź na stronę http://127.0.0.1:8080/debug?port=5858 w przeglądarce internetowej opartej na silniku WebKit, a zobaczysz inspektora. Jeżeli uruchomiłeś go za pomocą opcji --debug-brk, inspektor Node będzie natychmiast wyświetlał pierwszy skrypt aplikacji, jak pokazano na rysunku B.2. W przypadku użycia opcji --debug będziesz miał możliwość wyboru skryptu, co na rysunku B.2 jest wskazywane przez nazwę step.js. W ten sposób możesz wybrać skrypt, który będzie debugowany. Czerwona strzałka pokazana po lewej stronie kodu wskazuje wiersz, który zostanie wykonany jako kolejny.
B.4.2. Nawigacja po inspektorze Node W celu przejścia do kolejnego wywołania funkcji w aplikacji kliknij przycisk wyglądający jak mała zakręcona strzałka. Inspektor Node, podobnie jak debuger Node działający w powłoce, pozwala na wejście do funkcji. Kiedy czerwona strzałka po lewej stronie wiersza kodu wskazuje wywołanie funkcji, wejście do funkcji następuje po kliknięciu małej strzałki skierowanej w dół i wskazującej kółko. Opuszczenie funkcji następuje po kliknięciu przycisku ze strzałką skierowaną w górę. Jeżeli używasz modułów Node wbudowanych lub utworzonych przez społeczność, debuger spowoduje przejście do pliku skryptu modułu, gdy będziesz poruszał się po aplikacji. Nie przejmuj się, w pewnym momencie powrócisz do kodu aplikacji.
Rysunek B.2. Inspektor Node
Aby dodać punkt kontrolny podczas użycia inspektora Node, wystarczy kliknąć numer wiersza po lewej stronie dowolnego wiersza skryptu. Usunięcie wszystkich punktów kontrolnych następuje po kliknięciu przycisku znajdującego się po prawej stronie przycisku opuszczenia funkcji (przekreślona strzałka). Inspektor Node ma również interesującą funkcję, jaką jest możliwość zmiany kodu podczas działania aplikacji. Jeżeli chcesz zmienić wiersz kodu, po prostu dwukrotnie go kliknij, przeprowadź edycję, a następnie kliknij w dowolnym miejscu poza zmodyfikowanym wierszem kodu.
B.4.3. Przeglądanie stanu w inspektorze Node Podczas debugowania aplikacji jej stan można sprawdzać za pomocą rozwijanych paneli znajdujących się pod przyciskami, jak pokazano na rysunku B.3. Wspomniane panele pozwalają na przejrzenie stosu wywołań, zmiennych w zasięgu aktualnie wykonywanego kodu. Modyfikacja zmiennej jest możliwa po jej dwukrotnym kliknięciu i zmianie wartości. Podobnie jak jest w przypadku debugera wbudowanego w Node i działającego w powłoce, istnieje możliwość dodania wartowników wyświetlających wartości podczas poruszania się po aplikacji. Więcej informacji na temat maksymalnego wykorzystania możliwości oferowanych przez inspektora Node znajdziesz na stronie projektu w serwisie GitHub (https://github.com/node-inspector/node-inspector/).
Rysunek B.3. Przeglądanie stanu aplikacji za pomocą inspektora Node
Jeśli masz wątpliwości, odśwież stronę. Jeżeli podczas użycia inspektora Node zauważysz dziwne zachowanie, może pomóc odświeżenie strony w przeglądarce internetowej. Jeżeli to rozwiązanie nie działa, spróbuj ponownie uruchomić zarówno aplikację, jak i inspektora Node.
Dodatek C Rozszerzenie i konfiguracja frameworka Express Framework Express standardowo oferuje wiele użytecznych funkcji, ale jego rozszerzenie i dostrojenie konfiguracji może uprościć programowanie i zapewnić jeszcze większe możliwości.
C.1. Rozszerzenie frameworka Express Na początek przekonajmy się, jak można rozszerzyć framework Express. W tym podrozdziale dowiesz się: • n Jak utworzyć własne silniki szablonów? • n Jak wykorzystać zalety silników szablonów utworzonych przez społeczność? • n Jak usprawnić aplikacje za pomocą modułów rozszerzających frameworka Express?
C.1.1. Rejestracja szablonów silników Silnik może standardowo zapewniać obsługę frameworka Express przez eksport metody __express. Jednak nie każdy silnik szablonów oferuje tego rodzaju możliwość. Ponadto być może będziesz chciał utworzyć własny silnik. Framework Express jest na to przygotowany i oferuje metodę app.engine(). W tym punkcie dowiesz się, jak utworzyć mały silnik szablonów zapewniający zastępowanie zmiennej, co pozwala na obsługę treści dynamicznej. Metoda app.engine() mapuje rozszerzenie nazwy pliku na funkcję wywołania zwrotnego, aby framework Express „wiedział”, jak należy użyć danego pliku. W listingu C.1 przedstawiono kod, w którym plik z rozszerzeniem .md jest przekazywany, aby wywołanie takie jak res.render('myview.md') stosowało wskazaną funkcję wywołania zwrotnego do wygenerowania pliku. Przedstawiona abstrakcja pozwala na użycie z frameworkiem Express praktycznie każdego silnika szablonów. W omawianym silniku szablonów nawiasy są używane wokół zmiennych lokalnych, aby w ten sposób zapewnić obsługę dynamicznych danych wejściowych. Na przykład {name} spowoduje wyświetlenie wartości name za każdym razem, gdy pojawi się w szablonie. Listing C.1. Obsługa rozszerzenia .md
var express = require('express'); var http = require('http'); var md = require('github-flavored-markdown').parse; Wymagana jest implementacja Markdown. var fs = require('fs'); var app = express(); Mapowanie tego wywołania zwrotnego na pliki z rozszerzeniem .md. app.engine('md', function(path, options, fn){ fs.readFile(path, 'utf8', function(err, str){ Odczyt zawartości pliku i umieszczenie jej w ciągu tekstowym. if (err) return fn(err); try { Przekazanie frameworkowi Express obsługi błędów. var html = md(str); Konwersja ciągu tekstowego w formacie Markdown na kod HTML. html = html.replace(/\{([^}]+)\}/g, function(_, name){ Zastąpienie wartości w nawiasach. return options[name] || ''; Wartość domyślna to '' (pusty ciąg tekstowy). }); fn(null, html); Przekazanie frameworkowi Express wygenerowanego kodu HTML. } catch (err) { fn(err); Przechwycenie wszystkich zgłoszonych błędów. } }); });
Silnik szablonów przedstawiony w listingu C.1 pozwala na tworzenie dynamicznych widoków za pomocą składni Markdown. Jeśli na przykład chcesz powitać użytkownika, możesz użyć następującego kodu: # {name} Witaj, {name}! Cieszymy się, że będziesz używał aplikacji {appName}.
C.1.2. Szablony i projekt consolidate.js Projekt consolidate.js został przygotowany specjalnie dla frameworka Express 3.x i oferuje pojedyncze, ujednolicone API dla wielu silników szablonów w Node. Oznacza to, że standardowo Express 3.x pozwala na użycie ponad 14 różnych silników szablonów. Jeżeli pracujesz nad biblioteką używającą szablonów, wówczas możesz skorzystać z ich szerokiej gamy oferowanej przez consolidate.js. Na przykład Swig to silnik szablonów zainspirowany przez Django. Do zdefiniowania logiki korzysta ze znaczników osadzonych w kodzie HTML, jak
przedstawiono poniżej:
Prawdopodobnie w zależności od silnika szablonów i edytora tekstów z funkcją podświetlania składni wolisz korzystać z silników opartych na stylu HTML. Wówczas pliki mają rozszerzenie .html, a nie pochodzące od nazwy silnika, na przykład .swig. Z pomocą przychodzi wówczas metoda app.engine() frameworka Express. Po jej wywołaniu wygenerowany przez framework Express plik .html będzie używać wskazanego silnika szablonów, na przykład Swig: var cons = require('consolidate'); app.engine('html', cons.swig);
Silnik szablonów EJS również będzie mapowany na pliki .html, ponieważ też używa osadzonych znaczników:
Pewne silniki szablonów korzystają z zupełnie odmiennej składni, a więc nie ma sensu mapować ich na pliki .html. Dobrym przykładem jest silnik Jade posiadający własny język deklaracyjny. Jade można mapować za pomocą poniższego wywołania: var cons = require('consolidate'); app.engine('jade', cons.jade);
Więcej informacji szczegółowych oraz listę obsługiwanych silników szablonów znajdziesz na stronie repozytorium projektu consolidate.js pod adresem https://github.com/visionmedia/consolidate.js.
C.1.3. Frameworki i rozszerzenia Express Być może zastanawiasz się, jakie opcje mają programiści korzystający z bardziej strukturalnych frameworków, takich jak Ruby on Rails. Express oferuje kilka możliwości w takiej sytuacji. Społeczność Express opracowała wiele działających na wysokim poziomie frameworków opartych na Express w celu dostarczenia struktury katalogów, a
także funkcji, na przykład kontrolerów w stylu Ruby. Poza wspomnianymi frameworkami Express obsługuje także mnóstwo wtyczek rozszerzających jego możliwości standardowe.
Express-Expose W tycz ka express-expose może być wykorzystana do udostępnienia klientowi obiektów JavaScript znajdujących się po stronie serwera. Na przykład jeśli chcesz udostępnić dane JSON uwierzytelnionego użytkownika, wtedy możesz użyć wywołania res.expose() i dostarczyć kodowi działającemu po stronie klienta obiekt express.user: res.expose(req.user, 'express.user');
Express-Resource Inną doskonałą wtyczką jest express-resource, która jest wykorzystywana do obsługi strukturalnego routingu. Routing można zapewnić na wiele sposobów, ale wszystkie praktycznie sprowadzają się do metody żądania i ścieżki, co Express oferuje standardowo. Na tej podstawie można tworzyć koncepcje działające na wysokim poziomie. W przedstawionym poniżej przykładzie pokazano, jak można zdefiniować akcje przeznaczone do pokazywania, tworzenia i uaktualniania zasobu użytkownika w sposób deklaracyjny. Przede wszystkim w pliku app.js trzeba umieścić następujących wiersz kodu: app.resource('user', require('./controllers/user'));
W listingu C.2 przedstawiono kod modułu kontrolera /controllers/user.js. Listing C.2. Plik user.js exports.new = function(req, res){ res.send('new user'); }; exports.create = function(req, res){ res.send('create user'); }; exports.show = function(req, res){ res.send('show user ' + req.params.user); };
Pełną listę wtyczek, silników szablonów i frameworków zamieszczono w wiki frameworka Express na stronie https://github.com/visionmedia/express/wiki.
C.2. Konfiguracja zaawansowana
Z wcześniejszych rozdziałów książki dowiedziałeś się, jak skonfigurować framework Express za pomocą funkcji app.configure(). Przedstawiono w nich również wiele opcji konfiguracyjnych. W tym podrozdziale poznasz dodatkowe opcje konfiguracyjne, które można wykorzystać do zmiany zachowania domyślnego oraz udostępnienia kolejnych funkcji. W tabeli C.1 wymieniono opcje konfiguracyjne frameworka Express, które nie zostały omówione w rozdziale 8. Tabela C.1. Wbudowane ustawienia Express default engine
Użyty domyślny silnik szablonu
views
Wyświetlenie ścieżki wyszukiwania
json replacer
Funkcja modyfikacji odpowiedzi JSON
json spaces
Liczba spacji użytych do formatowania odpowiedzi JSON
jsnop callback
Obsługa JSONP za pomocą res.json() i res.send()
trust proxy
Zaufanie odwróconemu proxy
view cache
Funkcje buforowania silnika szablonu
Opcja konfiguracyjna views jest całkiem prosta. Używa się jej do wskazania lokalizacji szablonów widoku. Kiedy tworzysz szkielet aplikacji z poziomu powłoki za pomocą polecenia express, opcja konfiguracyjna view automatycznie wskazuje podkatalog views aplikacji. Teraz przechodzimy do nieco bardziej skomplikowanej opcji konfiguracyjnej, czyli json_replacer.
C.2.1. Modyfikacja odpowiedzi JSON Przyjmujemy założenie, że masz obiekt user wraz z właściwościami prywatnymi, takimi jak identyfikator obiektu _id. Domyślnie wywołanie metody res.send(user) spowoduje udzielenie odpowiedzi w formacie JSON w postaci takiej jak {"_id":123,"name":"Tobi"}. Opcja json replacer to ustawienie pobierające funkcję, którą framework Express przekaże JSON.stringify() w trakcie wywołań res.send() i res.json(). Samodzielna aplikacja Express przedstawiona w listingu C.3 pokazuje, jak można wykorzystać wymienioną opcję do pominięcia w odpowiedzi JSON wszystkich właściwości o nazwach rozpoczynających się od znaku podkreślenia. W omawianym przykładzie odpowiedź będzie miała postać {"name":"Tobi"}. Listing C.3. Użycie json_replacer do kontroli i modyfikacji danych JSON var express = require('express'); var app = express();
app.set('json replacer', function(key, value){ if ('_' == key[0]) return; return value; }); var user = { _id: 123, name: 'Tobi' }; app.get('/user', function(req, res){ res.send(user); }); app.listen(3000);
Zwróć uwagę, implementować
że poszczególne obiekty lub prototypy obiektów mogą metodę toJSON(). Wymieniona metoda jest używana przez JSON.stringify() podczas konwersji obiektu na ciąg tekstowy JSON. To jest doskonała alternatywa dla wywołania zwrotnego json_replacer, jeśli wprowadzane zmiany nie dotyczą każdego obiektu. W ten sposób dowiedziałeś się, jak określać dane, które powinny znajdować się w danych wyjściowych JSON. Możemy więc przejść do dostrajania formatowania danych w formacie JSON.
C.2.2. Formatowanie odpowiedzi JSON Opcja konfiguracyjna json spaces wpływa na wywołania JSON.stringify() we frameworku Express. Wymieniona opcja definiuje liczbę spacji używanych podczas formatowania danych JSON jako ciągu tekstowego. Domyślnie
metoda zwraca skompresowane dane JSON, na przykład {"name":"Tobi",å"age":2,"species":"zwierzę"}. Tego rodzaju skompresowane dane JSON są idealne w środowisku produkcyjnym, ponieważ zmniejszają wielkość udzielanej odpowiedzi. Jednak w trakcie prac nad aplikacją nieskompresowane dane wyjściowe są znacznie łatwiejsze w odczycie. O pc j a json spaces ma automatycznie ustawianą wartość 0 w środowisku produkcyjnym oraz 2 w środowisku programistycznym, co powoduje wygenerowanie danych wyjściowych w następującej postaci: { "name": "Tobi", "age": 2, "species": "zwierzę" }
C.2.3. Zaufanie nagłówkom odwrotnego proxy
Domyślnie framework Express w żadnym środowisku „nie ufa” nagłówkom odwrotnego proxy. Wspomniane odwrotne nagłówki proxy wykraczają poza zakres tematyczny tej książki. Jeśli Twoja aplikacja działa za odwrotnym proxy, na przykład Nginx, HAProxy lub Varnish, wtedy powinieneś użyć opcji trust proxy , aby framework Express „wiedział”, że te nagłówki można bezpiecznie sprawdzać.
Spis treści Wstęp Przedmowa Podziękowania Mike Cantelon Marc Harter Nathan Rajlich O książce Mapa drogowa Konwencje zastosowane w kodzie i materiały do pobrania Forum Author Online Część I Podstawy Node Rozdział 1. Witamy w Node.js 1.1. Node jest zbudowane w oparciu o JavaScript 1.2. Asynchroniczna i oparta na zdarzeniach: przeglądarka internetowa 1.3. Asynchroniczny i oparty na zdarzeniach: serwer 1.4. Aplikacje DIRT 1.5. Domyślna aplikacja jest typu DIRT 1.5.1. Prosty przykład aplikacji asynchronicznej 1.5.2. Serwer HTTP 1.5.3. Strumieniowanie danych 1.6. Podsumowanie Rozdział 2. Tworzenie aplikacji wielopokojowego czatu 2.1. Ogólny opis aplikacji 2.2. Wymagania aplikacji i konfiguracja początkowa 2.2.1. Obsługa HTTP i WebSocket 2.2.2. Tworzenie struktury plików aplikacji 2.2.3. Wskazanie zależności 2.2.4. Instalacja zależności 2.3. Udostępnianie plików HTML, CSS i kodu JavaScript działającego po stronie klienta 2.3.1. Tworzenie podstawowego serwera plików statycznych Wysyłanie danych pliku i odpowiedzi w postaci błędów Tworzenie serwera HTTP Uruchomienie serwera HTTP 2.3.2. Dodanie plików HTML i CSS 2.4. Obsługa wiadomości czatu za pomocą biblioteki Socket.IO 2.4.1. Konfiguracja serwera Socket.IO Utworzenie logiki połączenia 2.4.2. Obsługa zdarzeń oraz scenariuszy w aplikacji
Przypisanie nazwy gościa Dołączanie do pokoju Obsługa żądań zmiany nazwy użytkownika Wysyłanie wiadomości czatu Tworzenie pokoju Obsługa rozłączenia użytkownika 2.5. Użycie kodu JavaScript działającego po stronie klienta do utworzenia interfejsu użytkownika aplikacji 2.5.1. Przekazywanie do serwera wiadomości oraz żądań zmiany pokoju lub nazwy użytkownika 2.5.2. Wyświetlenie w interfejsie użytkownika wiadomości i listy dostępnych pokoi 2.6. Podsumowanie Rozdział 3. Podstawy programowania w Node 3.1. Organizacja i wielokrotne użycie kodu Node 3.1.1. Tworzenie modułu 3.1.2. Dostrajanie tworzenia modułu za pomocą module.exports 3.1.3. Wielokrotne użycie modułów za pomocą katalogu node_modules 3.1.4. Zastrzeżenia 3.2. Techniki programowania asynchronicznego 3.2.1. Użycie wywołań zwrotnych do obsługi zdarzeń jednorazowych 3.2.2. Użycie emitera zdarzeń do obsługi powtarzających się zdarzeń Przykład emitera zdarzeń Udzielanie odpowiedzi na zdarzenie, które powinno wystąpić tylko jednokrotnie Tworzenie emitera zdarzeń — przykład oparty na publikacji i subskrypcji Rozbudowa emitera zdarzeń — przykład obserwatora pliku 3.2.3. Wyzwania pojawiające się podczas programowania asynchronicznego 3.3. Sekwencja logiki asynchronicznej 3.3.1. Kiedy stosować szeregową kontrolę przepływu? 3.3.2. Implementacja szeregowej kontroli przepływu 3.3.3. Implementacja równoległej kontroli przepływu 3.3.4. Użycie narzędzi opracowanych przez społeczność 3.4. Podsumowanie Część II Tworzenie aplikacji sieciowych w Node Rozdział 4. Tworzenie aplikacji sieciowej w Node 4.1. Podstawy dotyczące serwera HTTP 4.1.1. Jak przychodzące żądania HTTP są przez Node przedstawiane programiście? 4.1.2. Prosty serwer HTTP odpowiadający komunikatem „Witaj, świecie” 4.1.3. Odczyt nagłówków żądania i zdefiniowanie nagłówków odpowiedzi 4.1.4. Ustawienie kodu stanu odpowiedzi HTTP 4.2. Tworzenie usługi sieciowej RESTful 4.2.1. Tworzenie zasobów za pomocą żądań POST
4.2.2. Pobieranie zasobów za pomocą żądania GET Zdefiniowanie nagłówka Content-Length 4.2.3. Usunięcie zasobu za pomocą żądania DELETE 4.3. Udostępnianie plików statycznych 4.3.1. Tworzenie serwera plików statycznych Optymalizacja transferu danych za pomocą Stream#pipe() 4.3.2. Obsługa błędów serwera 4.3.3. Wyprzedzająca obsługa błędów za pomocą wywołania fs.stat() 4.4. Akceptacja danych wejściowych użytkownika przekazanych za pomocą formularza sieciowego 4.4.1. Obsługa wysłanych pól formularza sieciowego Moduł querystring 4.4.2. Obsługa przekazanych plików za pomocą formidable 4.4.3. Sprawdzanie postępu operacji przekazywania plików 4.5. Zabezpieczanie aplikacji dzięki użyciu protokołu HTTPS 4.6. Podsumowanie Rozdział 5. Przechowywanie danych aplikacji Node 5.1. Niewymagający serwera magazyn danych 5.1.1. Magazyn danych w pamięci 5.1.2. Magazyn danych oparty na plikach Utworzenie logiki początkowej Zdefiniowanie funkcji pomocniczej do pobierania zadań Zdefiniowanie funkcji pomocniczej do przechowywania zadań 5.2. System zarządzania relacyjną bazą danych 5.2.1. MySQL Użycie MySQL do utworzenia aplikacji śledzącej wykonywanie zadań Utworzenie logiki aplikacji Tworzenie funkcji pomocniczych odpowiedzialnych za wysyłanie kodu HTML, tworzenie formularzy sieciowych i pobieranie danych z formularzy Dodanie danych za pomocą MySQL Usuwanie danych MySQL Uaktualnienie danych MySQL Pobieranie danych MySQL Generowanie rekordów MySQL Generowanie formularzy HTML Wypróbowanie aplikacji 5.2.2. PostgreSQL Nawiązanie połączenia z PostgreSQL Wstawienie rekordu do tabeli bazy danych Utworzenie zapytania zwracającego wynik 5.3. Bazy danych typu NoSQL 5.3.1. Redis Nawiązywanie połączenia z serwerem Redis
Praca z danymi bazy Redis Przechowywanie i pobieranie wartości za pomocą tabeli hash Przechowywanie i pobieranie danych za pomocą listy Przechowywanie i pobieranie danych za pomocą zbiorów Dostarczanie danych za pomocą kanałów Maksymalizacja wydajności Node_Redis 5.3.2. MongoDB Nawiązanie połączenia z MongoDB Uzyskanie dostępu do kolekcji MongoDB Wstawienie dokumentu do kolekcji Uaktualnienie danych za pomocą identyfikatora dokumentu Wyszukiwanie dokumentów Usuwanie dokumentów 5.3.3. Mongoose Otworzenie i zamknięcie połączenia Rejestracja schematu Dodanie zadania Wyszukiwanie dokumentu Uaktualnianie dokumentu Usuwanie dokumentu 5.4. Podsumowanie Rozdział 6. Framework Connect 6.1. Konfiguracja aplikacji Connect 6.2. Jak działa metoda pośrednicząca frameworka Connect? 6.2.1. Metody pośredniczące wyświetlające żądanie 6.2.2. Metoda pośrednicząca udzielająca odpowiedzi w postaci komunikatu „Witaj, świecie” 6.3. Dlaczego kolejność metod pośredniczących ma znaczenie? 6.3.1. Kiedy metoda pośrednicząca nie wywołuje next()? 6.3.2. Użycie kolejności metod pośredniczących do przeprowadzenia uwierzytelnienia 6.4. Montowanie metody pośredniczącej i serwera 6.4.1. Metody pośredniczące przeprowadzające uwierzytelnianie 6.4.2. Metoda pośrednicząca wyświetlająca panel administracyjny Przetestowanie całości 6.5. Tworzenie konfigurowalnej metody pośredniczącej 6.5.1. Tworzenie konfigurowalnej metody pośredniczącej logger() 6.5.2. Tworzenie metody pośredniczącej router() 6.5.3. Tworzenie metody pośredniczącej przeznaczonej do przepisywania adresów URL 6.6. Użycie metody pośredniczącej do obsługi błędów 6.6.1. Domyślna obsługa błędów w Connect 6.6.2. Samodzielna obsługa błędów aplikacji
6.6.3. Użycie wielu metod pośredniczących przeznaczonych do obsługi błędów Implementacja metody pośredniczącej hello() Implementacja metody pośredniczącej users() Implementacja metody pośredniczącej pets() Implementacja metody pośredniczącej errorHandler() Implementacja metody pośredniczącej errorPage 6.7. Podsumowanie Rozdział 7. Metody pośredniczące frameworka Connect 7.1. Metody pośredniczące przeznaczone do przetwarzania plików cookie, danych żądań i ciągów tekstowych zapytań 7.1.1. cookieParser() — przetwarzanie plików cookie Podstawowy sposób użycia Zwykłe cookie Podpisane cookie Cookie w formacie JSON Ustawienie cookie wychodzących 7.1.2. bodyParser() — przetwarzanie danych żądania Podstawowy sposób użycia Przetwarzanie danych JSON Przetwarzanie zwykłych danych formularza sieciowego Przetwarzanie wieloczęściowych danych formularza sieciowego 7.1.3. limit() — ograniczenie danych żądania Dlaczego metoda pośrednicząca limit() jest potrzebna? Podstawowy sposób użycia Opakowanie metody pośredniczącej limit() w celu uzyskania większej elastyczności 7.1.4. query() — analizator ciągu tekstowego zapytania Podstawowy sposób użycia 7.2. Metody pośredniczące implementujące podstawowe funkcje wymagane przez aplikację sieciową 7.2.1. logger() — rejestracja informacji o żądaniu Podstawowy sposób użycia Dostosowanie do własnych potrzeb formatu rejestracji danych Opcje metody pośredniczącej logger() — stream, immediate i buffer 7.2.2. favicon() — obsługa ikon favicon Podstawowy sposób użycia 7.2.3. methodOverride() — nieprawdziwe metody HTTP Podstawowy sposób użycia Uzyskanie dostępu do oryginalnej właściwości req.method 7.2.4. vhost() — wirtualny hosting Podstawowy sposób użycia Użycie wielu egzemplarzy vhost() 7.2.5. session() — zarządzanie sesją
Podstawowy sposób użycia Ustawienie czasu wygaśnięcia ważności sesji Praca z danymi sesji Praca z plikami cookie sesji Magazyn danych sesji 7.3. Metody pośredniczące zapewniające bezpieczeństwo aplikacji sieciowej 7.3.1. basicAuth() — uwierzytelnianie podstawowe HTTP Podstawowy sposób użycia Przekazanie funkcji wywołania zwrotnego Przekazanie asynchronicznej funkcji wywołania zwrotnego Przykład z użyciem polecenia curl 7.3.2. csrf() — ochrona przed atakami typu CSRF Podstawowy sposób użycia 7.3.3. errorHandler() — obsługa błędów w trakcie tworzenia aplikacji Podstawowy sposób użycia Komunikat błędu w formacie HTML Komunikat błędu w formacie zwykłego tekstu Komunikat błędu w formacie JSON 7.4. Metody pośredniczące przeznaczone do udostępniania plików statycznych 7.4.1. static() — udostępnianie plików statycznych Podstawowy sposób użycia Użycie metody static() wraz z montowaniem Bezwzględne kontra względne ścieżki dostępu do katalogów Udostępnianie pliku index.html, gdy żądanie dotyczy katalogu 7.4.2. compress() — kompresja plików statycznych Podstawowy sposób użycia Użycie własnej funkcji filtrującej Określenie poziomu kompresji i pamięci 7.4.3. directory() — wyświetlenie katalogów Podstawowy sposób użycia Podstawowy sposób użycia 7.5. Podsumowanie Rozdział 8. Framework Express 8.1. Utworzenie szkieletu aplikacji 8.1.1. Globalna instalacja frameworka Express 8.1.2. Generowanie aplikacji 8.1.3. Poznawanie aplikacji 8.2. Konfiguracja frameworka Express i tworzonej aplikacji 8.2.1. Konfiguracja na podstawie środowiska 8.3. Generowanie widoków aplikacji Express 8.3.1. Konfiguracja systemu widoków Zmiana katalogu wyszukiwania Domyślny silnik szablonów
Buforowanie widoku 8.3.2. Wyszukiwanie widoku 8.3.3. Udostępnianie danych widokom Tworzenie widoku wyświetlającego listę zdjęć Metody udostępniania danych widokom 8.4. Obsługa formularzy i przekazywania plików 8.4.1. Implementacja modelu zdjęcia 8.4.2. Tworzenie formularza przeznaczonego do przekazywania zdjęć Utworzenie formularza Dodanie trasy dla strony przeznaczonej do przekazywania zdjęć Obsługa wysyłania danych formularza 8.4.3. Wyświetlenie listy przekazanych zdjęć 8.5. Obsługa pobierania zasobów 8.5.1. Tworzenie trasy dla pobierania zdjęć 8.5.2. Implementacja trasy pobierania zdjęcia Rozpoczęcie pobierania przez przeglądarkę Ustawienie nazwy pobieranego pliku 8.6. Podsumowanie Rozdział 9. Zaawansowane użycie frameworka Express 9.1. Uwierzytelnianie użytkowników 9.1.1. Zapisywanie i wczytywanie użytkowników Tworzenie pliku package.json Tworzenie modelu User Zapis użytkownika w bazie danych Redis Zabezpieczanie hasła użytkownika Testowanie logiki odpowiedzialnej za zapis użytkownika Pobieranie danych użytkownika Uwierzytelnianie użytkownika 9.1.2. Rejestrowanie nowego użytkownika Dodanie tras rejestracji Tworzenie formularza rejestracji Udzielanie odpowiedzi użytkownikowi Trwałe przechowywanie komunikatów w sesji Implementacja rejestracji użytkownika 9.1.3. Logowanie zarejestrowanych użytkowników Wyświetlenie formularza logowania Uwierzytelnienie logowania Utworzenie menu dla użytkowników uwierzytelnionych i anonimowych 9.1.4. Metoda pośrednicząca przeznaczona do wczytywania użytkownika 9.2. Zaawansowane techniki routingu 9.2.1. Weryfikacja użytkownika podczas przesyłania treści Utworzenie modelu postu Dodanie tras powiązanych z postami
Dodanie strony wyświetlającej listę postów Utworzenie formularza postu Implementacja tworzenia postu 9.2.2. Metoda pośrednicząca charakterystyczna dla trasy Weryfikacja formularza za pomocą metody pośredniczącej przeznaczonej dla trasy Utworzenie elastycznej metody pośredniczącej przeznaczonej do przeprowadzania weryfikacji 9.2.3. Implementacja stronicowania Projektowanie API stronicowania Implementacja metody pośredniczącej przeznaczonej do obsługi stronicowania Użycie stronicowania w trasie Utworzenie szablonu dla łączy mechanizmu stronicowania Dodanie łączy stronicowania w szablonie Włączenie czystych adresów URL dla stronicowania 9.3. Utworzenie publicznego API REST 9.3.1. Projekt API 9.3.2. Dodanie uwierzytelnienia podstawowego 9.3.3. Implementacja routingu Testowanie operacji pobierania danych użytkownika Usunięcie danych wrażliwych użytkownika Dodanie postów Dodanie obsługi wyświetlania listy wpisów 9.3.4. Włączenie negocjacji treści Implementacja negocjacji treści Udzielenie odpowiedzi w postaci XML 9.4. Obsługa błędów 9.4.1. Obsługa błędów 404 Dodanie trasy pozwalającej na udzielenie odpowiedzi informującej o błędzie Tworzenie szablonu dla strony błędu Włączenie metody pośredniczącej 9.4.2. Obsługa błędów Użycie trasy warunkowej do przetestowania stron błędów Implementacja procedury obsługi błędów Utworzenie szablonu strony błędu Włączenie metody pośredniczącej 9.5. Podsumowanie Rozdział 10. Testowanie aplikacji Node 10.1. Testy jednostkowe 10.1.1. Moduł assert Prosty przykład Użycie asercji eqal() do sprawdzenia wartości zmiennej Użycie asercji notEqual() do wyszukiwania problemów w logice
Użycie asercji dodatkowych strictEqual(), notStrictEqual(), deepEqual(), notDeepEqual() Użycie asercji ok() do sprawdzenia, czy wartość zwrotna metody asynchronicznej wynosi true Sprawdzenie, czy zgłaszane komunikaty błędów są poprawne Dodanie logiki przeznaczonej do uruchamiania testów 10.1.2. Framework nodeunit Instalacja nodeunit Testowanie aplikacji Node za pomocą frameworka nodeunit 10.1.3. Mocha Testowanie aplikacji Node za pomocą Mocha Zdefiniowanie konfiguracji i czyszczenie logiki za pomocą zaczepów Mocha Testowanie logiki asynchronicznej 10.1.4. Framework Vows Testowanie logiki aplikacji za pomocą frameworka Vows 10.1.5. Biblioteka should.js Testowanie funkcjonalności modułu za pomocą biblioteki should.js 10.2. Testy akceptacyjne 10.2.1. Tobi Testowanie aplikacji sieciowych za pomocą Tobi 10.2.2. Soda Instalacja frameworka Soda i serwera Selenium Testowanie aplikacji sieciowej za pomocą Soda i Selenium Testowanie aplikacji sieciowej za pomocą Soda i Sauce Labs 10.3. Podsumowanie Rozdział 11. Szablony w aplikacji sieciowej 11.1. Użycie szablonów w celu zachowania przejrzystości kodu 11.1.1. Szablon w akcji Wygenerowanie kodu HTML bez użycia szablonu Wygenerowanie kodu HTML z użyciem szablonu 11.2. Silnik szablonów Embedded JavaScript 11.2.1. Tworzenie szablonu Zmiana znaczenia znaków 11.2.2. Praca z danymi szablonu za pomocą filtrów EJS Filtry obsługujące wybór Filtry przeznaczone do zmiany wielkości znaków Filtry przeznaczone do pracy z tekstem Filtry przeprowadzające sortowanie Filtr map Tworzenie własnych filtrów 11.2.3. Integracja EJS w aplikacji Buforowanie szablonów EJS 11.2.4. Użycie EJS w aplikacjach działających po stronie klienta
11.3. Użycie języka szablonów Mustache wraz z silnikiem Hogan 11.3.1. Tworzenie szablonu 11.3.2. Znaczniki Mustache Wyświetlanie prostych wartości Sekcje: iteracja przez wiele wartości Sekcje odwrócone: domyślny kod HTML, gdy wartość nie istnieje Sekcja lambda: własna funkcjonalność w blokach sekcji Partials: wielokrotne użycie szablonów w innych szablonach 11.3.3. Dostosowanie szablonu Hogan do własnych potrzeb 11.4. Szablony Jade 11.4.1. Podstawy szablonów Jade Podawanie atrybutów znacznika Podanie treści znacznika Zachowanie organizacji dzięki rozwinięciu bloku Umieszczanie danych w szablonach Jade 11.4.2. Logika w szablonach Jade Użycie JavaScript w szablonach Jade Iteracja przez obiekty i tablice Warunkowe wygenerowanie kodu szablonu Użycie poleceń case w Jade 11.4.3. Organizacja szablonów Jade Strukturyzacja wielu szablonów za pomocą ich dziedziczenia Implementacja układu za pomocą poprzedzenia blokiem lub dołączenia bloku Dołączanie szablonu Wielokrotne użycie logiki szablonu za pomocą polecenia mixin 11.5. Podsumowanie Część III Co dalej? Rozdział 12. Wdrażanie aplikacji Node i zapewnienie bezawaryjnego działania 12.1. Hosting aplikacji Node 12.1.1. Serwery dedykowane i VPS 12.1.2. Hosting w chmurze Amazon Web Services Rackspace Cloud 12.2. Podstawy wdrożenia 12.2.1. Wdrożenie z repozytorium Git 12.2.2. Zapewnienie działania aplikacji Node 12.3. Maksymalizacja wydajności i czasu bezawaryjnego działania aplikacji 12.3.1. Zapewnienie działania aplikacji za pomocą Upstart 12.3.2. API klastra — wykorzystanie zalety w postaci wielu rdzeni 12.3.3. Proxy i hosting plików statycznych 12.4. Podsumowanie Rozdział 13. Nie tylko serwery WWW 13.1. Biblioteka Socket.IO
13.1.1. Tworzenie minimalnej aplikacji Socket.IO Wypróbowanie aplikacji 13.1.2. Użycie biblioteki Socket.IO do odświeżenia strony i stylów CSS Wypróbowanie aplikacji 13.1.3. Inne zastosowania dla biblioteki Socket.IO 13.2. Dokładniejsze omówienie sieci TCP/IP 13.2.1. Praca z buforami i danymi binarnymi Dane tekstowe kontra binarne 13.2.2. Tworzenie serwera TCP Zapis danych Odczyt danych Połączenie dwóch strumieni za pomocą socket.pipe() Obsługa nieeleganckiego zamknięcia połączenia Zebranie wszystkiego w całość 13.2.3. Tworzenie klienta TCP 13.3. Narzędzia przeznaczone do pracy z systemem operacyjnym 13.3.1. Obiekt process, czyli globalny wzorzec Singleton Użycie process.env do pobierania i ustawiania zmiennych środowiskowych Zdarzenia specjalne emitowane przez obiekt proces Przechwytywanie sygnałów wysyłanych procesowi 13.3.2. Użycie modułu filesystem Przenoszenie pliku Monitorowanie katalogu lub pliku pod kątem zmian Użycie opracowanych przez społeczność modułów fstream i filed 13.3.3. Tworzenie procesów zewnętrznych Buforowanie za pomocą cp.exec() wyników działania polecenia Tworzenie poleceń za pomocą interfejsu Stream i cp.spawn() Rozkład obciążenia za pomocą cp.fork() 13.4. Tworzenie narzędzi powłoki 13.4.1. Przetwarzanie argumentów podanych w powłoce 13.4.2. Praca ze standardowym wejściem i wyjściem Zapis danych wyjściowych za pomocą process.stdout Odczyt danych wejściowych za pomocą process.stdin Rejestracja danych diagnostycznych za pomocą process.stderr 13.4.3. Dodanie koloru do danych wyjściowych Tworzenie i zapis znaków sterujących ANSI Formatowanie koloru tekstu za pomocą ansi.js Formatowanie koloru tła za pomocą ansi.js 13.5. Podsumowanie Rozdział 14. Ekosystem Node 14.1. Dostępne w internecie zasoby dla programistów Node 14.1.1. Node i odniesienia do modułów 14.1.2. Grupy Google
14.1.3. IRC 14.1.4. Zgłaszanie problemów w serwisie GitHub 14.2. Serwis GitHub 14.2.1. Rozpoczęcie pracy z GitHub Konfiguracja Git i rejestracja GitHub Dostarczenie GitHub klucza publicznego SSH 14.2.2. Dodanie projektu do GitHub Utworzenie repozytorium GitHub Konfiguracja pustego repozytorium Git Dodanie plików do repozytorium Git Przekazanie repozytorium Git do serwisu GitHub 14.2.3. Współpraca przez serwis GitHub 14.3. Przekazanie własnego modułu do repozytorium npm 14.3.1. Przygotowanie pakietu 14.3.2. Przygotowanie specyfikacji pakietu 14.3.3. Testowanie i publikowanie pakietu Testowanie instalacji pakietu Dodanie użytkownika npm Publikacja w repozytorium npm 14.4. Podsumowanie Dodatek A Instalacja Node i dodatki opracowane przez społeczność A.1. Instalacja w systemie OS X A.1.1. Instalacja za pomocą Homebrew A.2. Instalacja w systemie Windows A.3. Instalacja w systemie Linux A.3.1. Przygotowania do instalacji w Ubuntu A.3.2. Przygotowania do instalacji w CentOS A.4. Kompilacja Node A.5. Używanie menedżera pakietów Node A.5.1. Wyszukiwanie pakietów A.5.2. Instalacja pakietu A.5.3. Przeglądanie dokumentacji i kodu pakietu Dodatek B Debugowanie Node B.1. Analiza kodu za pomocą JSHint B.2. Dane wyjściowe debugowania B.2.1. Debugowanie za pomocą modułu console Wyświetlanie informacji o stanie aplikacji Dane wyjściowe dotyczące pomiaru czasu Wyświetlanie stosu wywołań B.2.2. Użycie modułu debug do zarządzania danymi wyjściowymi procesu debugowania B.3. Debuger wbudowany w Node B.3.1. Nawigacja po debugerze
B.3.2. Analiza i zmiana stanu w debugerze B.4. Inspektor Node B.4.1. Uruchomienie inspektora Node B.4.2. Nawigacja po inspektorze Node B.4.3. Przeglądanie stanu w inspektorze Node Dodatek C Rozszerzenie i konfiguracja frameworka Express C.1. Rozszerzenie frameworka Express C.1.1. Rejestracja szablonów silników C.1.2. Szablony i projekt consolidate.js C.1.3. Frameworki i rozszerzenia Express Express-Expose Express-Resource C.2. Konfiguracja zaawansowana C.2.1. Modyfikacja odpowiedzi JSON C.2.2. Formatowanie odpowiedzi JSON C.2.3. Zaufanie nagłówkom odwrotnego proxy
Skoro już wiesz, jak wskazywać znaczniki HTML oraz przypisywać im identyfikatory i klasy CSS, zobacz, jak wskazać atrybuty HTML.
Podawanie atrybutów znacznika Atrybut znacznika można podać przez jego umieszczenie w nawiasie okrągłym,
a specyfikacje poszczególnych atrybutów należy rozdzielić przecinkami. Dlatego też że pomocą poniższego kodu Jade możesz wskazać łącze, które będzie otwierane w nowej karcie: a(href='http://nodejs.org', target='_blank')
Ponieważ specyfikacja atrybutów znaczników może oznaczać wiele wierszy kodu Jade, silnik szablonów oferuje na tym polu pewną elastyczność. Przedstawiony poniżej przykład to prawidłowy kod Jade będący odpowiednikiem poprzedniego przykładu: a(href='http://nodejs.org', target='_blank')
Istnieje również możliwość podania atrybutów niewymagających wartości. W kolejnym przykładzie Jade pokazano specyfikację formularza HTML zawierającego element select wraz z opcją wskazującą na wybrany element: strong Wybierz ulubioną potrawę: form select option(value='Ser') Ser option(value='Tofu', selected) Tofu
Podanie treści znacznika W poprzednim fragmencie kodu pokazano przykład treści znacznika (Wybierz ulubioną potrawę) po znaczniku strong: Ser po pierwszym znaczniku option i Tofu po drugim. To standardowy sposób podawania treści znacznika w Jade, ale nie jedyny. Wprawdzie tego rodzaju styl doskonale sprawdza się w przypadku niewielkiej treści, ale może spowodować, że wiersze szablonu Jade staną się bardzo długie, jeśli treść znacznika będzie obszerna. Na szczęście, jak pokazano w poniższym przykładzie, treść znacznika można w Jade podawać także za pomocą znaku |: textarea | To jest pewien tekst domyślny, | który powinien zostać | wyświetlony użytkownikowi.
Jeżeli znacznik HTML, na przykład style lub script, akceptuje jedynie tekst (to znaczny nie zezwala na zagnieżdżanie elementów HTML), wówczas znaki | można zupełnie pominąć, jak przedstawiono w poniższym przykładzie: style h1 { font-size: 6em; color: #9DFF0C;
}
Istnienie dwóch oddzielnych sposobów podawania dłuższej i krótszej treści znaczników pomaga w zachowaniu eleganckiego wyglądu znaczników Jade. Ponadto Jade obsługuje alternatywny sposób wyrażania zagnieżdżeń, nazywany rozwinięciem bloku.
Zachowanie organizacji dzięki rozwinięciu bloku Standardowo zagnieżdżenia są wyrażane w Jade przez zastosowanie wcięć, ale czasami wcięcia mogą doprowadzić do powstawania dużej ilości pustego miejsca. Jako przykład poniżej przedstawiono szablon Jade używający wcięć do zdefiniowania prostej listy łączy: ul li a(href='http://nodejs.org/') Strona główna Node.js li a(href='http://npmjs.org/') Strona główna NPM li a(href='http://nodebits.org/') Blog Nodebits
Znacznie bardziej zwięzłym sposobem wyrażenia poprzedniego przykładu jest użycie oferowanego przez Jade mechanizmu rozwinięcia bloku. W takim przypadku po znaczniku umieszcza się dwukropek oznaczający zagnieżdżenie. Przedstawiony poniżej kod Jade generuje takie same dane wyjściowe jak poprzedni listing, ale składa się z czterech wierszy zamiast z siedmiu: ul li: a(href='http://nodejs.org/') Strona główna Node.js li: a(href='http://npmjs.org/') Strona główna NPM li: a(href='http://nodebits.org/') Blog Nodebits
Teraz już wiersz, jak przedstawić znaczniki za pomocą Jade. Przechodzimy więc do zagadnienia integracji Jade z aplikacją sieciową.
Umieszczanie danych w szablonach Jade Do silnika Jade dane są przekazywane w taki sam podstawowy sposób jak do EJS. Szablon zostaje w pierwszej kolejności skompilowany na postać funkcji, która następnie będzie wywoływana dla kontekstu w celu wygenerowania danych wyjściowych HTML. Przykład przedstawiono poniżej: var jade = require('jade'); var template = 'strong #{message}'; var context = {message: 'Witaj, szablonie!'}; var fn = jade.compile(template);
console.log(fn(context));
W poprzednim przykładzie kod #{message} w szablonie wskazywał miejsce zarezerwowane, które zostanie zastąpione przez wartość pochodzącą z kontekstu. Wartości kontekstu można również używać w celu dostarczania wartości atrybutów. Przedstawiony poniżej kod spowoduje wygenerowanie znacznika : var jade = require('jade'); var template = 'a(href = url)'; var context = {url: 'http://google.pl'}; var fn = jade.compile(template); console.log(fn(context));
W ten sposób dowiedziałeś się, jak kod HTML można podać za pomocą Jade, a także jak dostarczać szablonom Jade danych aplikacji. Przechodzimy więc do wykorzystania logiki w Jade.
11.4.2. Logika w szablonach Jade Po dostarczeniu szablonowi Jade danych aplikacji potrzebna jest logika, która będzie mogła przetworzyć wspomniane dane. Jade pozwala na bezpośrednie osadzanie wierszy kodu JavaScript w szablonach, co stanowi doskonały sposób na zdefiniowanie logiki. Powszechnie stosuje się konstrukcje takie jak instrukcje if, pętle for i deklaracje var. Zanim przejdziemy do szczegółów, spójrz na przykład szablonu Jade generującego listę kontaktową. W ten sposób możesz się przekonać, jak używać logiki Jade w aplikacji: h3.contacts-header Moja lista kontaktów if contacts.length each contact in contacts - var fullName = contact.firstName + ' ' + contact.lastName .contact-box p fullName if contact.isEditable p: a(href='/edit/+contact.id) Edycja rekordu p case contact.status when 'Aktywny' strong Użytkownik jest aktywny w systemie when 'Nieaktywny'
em Użytkownik jest nieaktywny w systemie when 'Oczekujący' | Oczekiwanie na akceptację zaproszenia else p Twoja lista kontaktów jest obecnie pusta
Najpierw opiszemy różne sposoby, na jakie Jade obsługuje dane wyjściowe podczas przetwarzania osadzonego kodu JavaScript.
Użycie JavaScript w szablonach Jade Poprzedzenie wiersza kodu JavaScript prefiksem - powoduje wykonanie go bez umieszczenia jakiejkolwiek wartości zwrotnej tego kodu w danych wyjściowych szablonu. Z kolei poprzedzenie logiki JavaScript znakiem = spowoduje dołączenie wartości zwrotnej kodu, przy czym znaki specjalne zostaną zneutralizowane w celu ochrony przed atakami typu XSS. Jeżeli kod JavaScript generuje dane wyjściowe, które nie powinny być modyfikowane, należy zastosować prefiks !=. Podsumowanie prefiksów przedstawiono w tabeli 11.1. Tabela 11.1. Prefiksy stosowane wraz z kodem JavaScript osadzonym w szablonie Jade Prefiks Opis danych wyjściowych
=
Dane wyjściowe zostaną zneutralizowane (w przypadku niezaufanych lub nieprzewidywalnych wartości to rodzaj zabezpieczenia przed atakami XSS).
!=
Dane wyjściowe pozostają niezmodyfikowane (w przypadku zaufanych lub przewidywalnych wartości).
-
Brak danych wyjściowych.
Jade zawiera wiele najczęściej używanych poleceń warunkowych i iteracji, które mogą być zapisywane bez prefiksów: if, else if, else, case, when, default, until, while, each i unless. Istnieje również możliwość definiowania zmiennych w Jade. Poniższe dwa odpowiadające sobie polecenia pokazują, jak można przypisać wartość w Jade: - var count = 0 count = 0
Pozbawione prefiksu polecenie nie generuje danych wyjściowych, podobnie jak w przypadku użycia prefiksu -, co wcześniej omówiono.
Iteracja przez obiekty i tablice Wartości przekazywane w kontekście są dostępne dla kodu JavaScript w szablonie Jade. W kolejnym przykładzie odczytujemy szablon Jade z pliku, a następnie przekazujemy szablonowi kontekst zawierający tablicę z kilkoma komunikatami, które mają być wyświetlone: var jade = require('jade');
var fs = require('fs'); var template = fs.readFileSync('./template.jade'); var context = { messages: [ 'Logowanie zakończone powodzeniem.', 'Witamy ponownie!' ]}; var fn = jade.compile(template); console.log(fn(context));
Szablon Jade przedstawia się następująco: - messages.forEach(function(message) { p= message - })
Ostateczne dane wyjściowe HTML mają postać przedstawioną poniżej:
Logowanie zakończone powodzeniem.
Witamy ponownie!
Jade obsługuje iterację w formie innej niż stosowana w JavaScript: za pomocą polecenia each. Wspomniane polecenie each pozwala na łatwą iterację przez tablicę i właściwości obiektu. Przedstawiony poniżej kod jest odpowiednikiem poprzedniego przykładu, ale używa polecenia each: each message in messages p= message
Iterację przez właściwości obiektu można przeprowadzić, stosując niewielką zmianę polecenia, na przykład: each value, key in post div strong #{key} p value
Warunkowe wygenerowanie kodu szablonu Czasami w zależności od wartości danych szablon musi „podejmować decyzje” co do sposobu ich wyświetlania. Kolejny przykład ilustruje tego rodzaju sytuację, w której mniej więcej co drugi raz skrypt generuje dane w postaci kodu HTML: - var n = Math.round(Math.random() * 1) + 1 - if (n == 1) { script alert('Wygrałeś!'); -}
Polecenia warunkowe mogą być w Jade zapisane także w alternatywnej, nieco bardziej przejrzystej formie: - var n = Math.round(Math.random() * 1) + 1 if n == 1 script alert('Wygrałeś!');
Jeżeli tworzysz negację warunku, na przykład if w Jade słowa kluczowego unless:
(n != 1),
wtedy powinieneś użyć
- var n = Math.round(Math.random() * 1) + 1 unless n == 1 script alert('Wygrałeś!');
Użycie poleceń case w Jade Jade obsługuje również inną niż w JavaScript formę konstrukcji warunkowej podobnej do switch: polecenie case. Wymienione polecenie pozwala na wskazanie danych wyjściowych na podstawie różnych scenariuszy. Poniższy przykład szablonu pokazuje, jak polecenie case można wykorzystać do wyświetlenia na trzy różne sposoby wyników operacji wyszukiwania w blogu. Jeżeli operacja nic nie znajdzie, użytkownikowi jest wyświetlany odpowiedni komunikat. W przypadku znalezienia pojedynczego wpisu bloga zostanie on wyświetlony. Jeżeli znalezionych będzie więcej wpisów, polecenie each zostanie użyte do iteracji przez posty i wyświetlenia ich tytułów. case results.length when 0 p Nie znaleziono szukanego wyrażenia. when 1 p= results[0].content default each result in results p= result.title
11.4.3. Organizacja szablonów Jade Po zdefiniowaniu szablonów warto wiedzieć, jak można je organizować. Podobnie jak w przypadku logiki aplikacji nie chcesz, aby pliki szablonów osiągnęły wielkie rozmiary. Pod względem koncepcyjnym pojedynczy plik szablonu powinien odpowiadać blokowi budulcowemu, na przykład stronie, paskowi bocznemu lub treści postu bloga.
W tym punkcie poznasz kilka mechanizmów pozwalających na współpracę różnych plików szablonów w celu wygenerowania treści. Są to: Strukturyzacja wielu szablonów za pomocą dziedziczenia szablonów. Implementacja układu za pomocą poprzedzania blokiem lub dołączania bloku. Dołączanie szablonów. Wielokrotne użycie logiki szablonu za pomocą poleceń mixin. Rozpoczynamy od zapoznania Cię z dziedziczeniem szablonów w Jade.
Strukturyzacja wielu szablonów za pomocą ich dziedziczenia Dziedziczenie szablonu to jeden ze sposobów strukturyzacji wielu szablonów. W tej koncepcji szablony są traktowane jak klasy w programowaniu zorientowanym obiektowo. Jeden szablon może rozszerzać inny, który z kolei będzie rozszerzał kolejny. Można użyć dowolnej liczby poziomów dziedziczenia, o ile ma to sens. Poniżej przedstawiono prosty przykład dziedziczenia szablonu w celu dostarczenia podstawowego opakowania HTML, które można wykorzystać jako opakowanie na treść strony. W katalogu roboczym utwórz nowy podkatalog o nazwie template przeznaczony na plik szablonu Jade. Szablon strony będzie umieszczony w pliku o nazwie layout.jade zawierającym następujący kod: html head block title body block content
Szablon layout.jade zawiera podstawową definicję strony HTML oraz dwa bloki. Wspomniane bloki są podczas dziedziczenia szablonów używane do zdefiniowania miejsca, w którym szablon dziedziczący powinien umieścić treść. W layout.jade mamy blok title pozwalający szablonowi dziedziczącemu na ustawienie tytułu oraz blok content pozwalający na wyświetlenie treści strony. Następnie w katalogu szablonów utwórz plik o nazwie page.jade przeznaczony do wypełniania bloków title i content: extends layout block title title Messages block content
each message in messages p= message
Do aplikacji dodaj logikę przedstawioną w listingu 11.12 (to zmodyfikowana wersja wcześniejszego przykładu zaprezentowanego w tym podrozdziale), która wyświetla wynik przetworzenia szablonu, pokazując tym samym dziedziczenie w akcji. Listing 11.12. Dziedziczenie szablonów w akcji var jade = require('jade'); var fs = require('fs'); var templateFile = './template/page.jade'; var iterTemplate = fs.readFileSync(templateFile); var context = { messages: [ 'Logowanie zakończone powodzeniem.', 'Witamy ponownie!' ]}; var iterFn = jade.compile( iterTemplate, {filename: templateFile} ); console.log(iterFn(context));
Teraz przechodzimy do innej funkcji poprzedzenia blokiem lub dołączenia bloku.
dziedziczenia
szablonów,
czyli
Implementacja układu za pomocą poprzedzenia blokiem lub dołączenia bloku W poprzednim przykładzie bloki w szablonie layout.jade nie zawierały treści, co powodowało, że ustawienie treści w szablonie page.jade było stosunkowo proste. Jeśli jednak blok w szablonie dziedziczącym zawiera treść, wówczas będzie ona uwzględniona, a nie zastąpiona przez szablon dziedziczący. Odbywa się to przez poprzedzenie blokiem lub dołączenie bloku. W ten sposób można zdefiniować stałą treść i dodawać do niej nową, zamiast całkowicie zastępować ją nową treścią. Przedstawiony poniżej szablon layout.jade zawiera dodatkowy blok o nazwie scripts, w którym znajduje się stała treść — znacznik script wczytujący popularną bibliotekę JavaScript jQuery. html head block title block scripts
script(src='//ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.js') body block content
Jeżeli chcesz, aby szablon page.jade dodatkowo wczytywał bibliotekę jQuery UI, możesz to osiągnąć, używając go w sposób przedstawiony w listingu 11.13. Listing 11.13. Użycie block append w celu wczytania dodatkowego pliku JavaScript extends layout Ten szablon rozszerza szablon layout. baseUrl = "http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/" block title title Messages block style Zdefiniowanie bloku style. link(rel="stylesheet", href= baseUrl+"themes/flick/jquery-ui.css") block append scripts Dołączenie tego bloku script do zdefiniowanego w szablonie layout. script(src= baseUrl+"jquery-ui.js") block content count = 0 each message in messages - count = count + 1 script $(function() { $("#message_#{count}").dialog({ height: 140, modal: true }); }); != '
' + message + '
'Dziedziczenie szablonów to nie jedyny sposób na integrację wielu szablonów. Istnieje również możliwość użycia polecenia Jade o nazwie include.
Dołączanie szablonu Inne narzędzie przeznaczone do organizacji szablonów Jade to polecenie include. Wymienione polecenie powoduje dołączenie zawartości innego szablonu. Jeżeli w użytym we wcześniejszym przykładzie szablonie layout.jade umieścisz wiersz include footer, to otrzymasz następujący szablon: html head block title
block style block scripts script(src='//ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.js') body block content include footer
Jak zilustrowano na rysunku 11.5, podczas generowania danych wyjściowych szablonu layout.jade dołączona będzie również zawartość szablonu o nazwie footer.jade.
Rysunek 11.5. Oferowane przez Jade polecenie include pozwala na łatwe umieszczanie zawartości jednego szablonu w innym
Takie rozwiązanie można zastosować na przykład w celu dodania informacji o witrynie lub zaprojektowania elementów dla layout.jade. Istnieje również możliwość dołączania plików innych niż szablony Jade, wystarczy tylko podać rozszerzenie pliku (na przykład include twitter_widget.html).
Wielokrotne użycie logiki szablonu za pomocą polecenia mixin Wprawdzie oferowane przez Jade polecenie include jest odpowiednie do dołączania wcześniej utworzonych fragmentów kodu, ale nie będzie idealnym rozwiązaniem podczas tworzenia biblioteki wielokrotnego użytku funkcji, które można wykorzystać na różnych stronach lub w oddzielnych aplikacjach. Do tego celu Jade udostępnia polecenie mixin, które pozwala na zdefiniowanie wielokrotnego użycia fragmentów kodu Jade. Polecenie
mixin
jest analogiczne do funkcji w JavaScript. Podobnie jak funkcja
polecenie mixin może pobierać argumenty, które z kolei można wykorzystać do wygenerowania kodu Jade. Przyjmujemy założenie, że aplikacja obsługuje strukturę danych podobną do przedstawionej poniżej: var students = [ {name: 'Jan Kowalski', age: 23}, {name: 'Kasia Nowak', age: 25}, {name: 'Bartek Malinowski', age: 37} ];
Jeżeli chcesz zdefiniować rozwiązanie pozwalające na wygenerowanie listy HTML na podstawie danej właściwości każdego obiektu, wtedy możesz przygotować następujące polecenie mixin: mixin list_object_property(objects, property) ul each object in objects li= object[property]
Następnie wystarczy użyć polecenia użyj poniższego wiersza kodu Jade:
mixin
do wyświetlenia danych. Do tego celu
mixin list_object_property(students, 'name')
Dzięki wykorzystaniu dziedziczenia szablonów i poleceń include i mixin można bardzo łatwo wielokrotnie używać znaczników prezentacyjnych, a tym samym nie pozwolić na nadmierne zwiększenie plików szablonów.
11.5. Podsumowanie W ten sposób poznałeś trzy popularne silniki szablonów HTML. Możesz więc wykorzystać wybraną technikę szablonów do organizacji logiki aplikacji i warstwy prezentacyjnej. Społeczność Node opracowała wiele silników szablonów. Jeżeli żaden z trzech omówionych w rozdziale nie odpowiada Ci z jakiegokolwiek powodu, zawsze możesz wypróbować inne: https://www.npmjs.org/browse/keyword/template. Na przykład silnik szablonów Handlebars.js (https://github.com/wycats/handlebars.js/) rozszerza Mustache i dodaje kolejne funkcje, takie jak znaczniki warunkowe i globalnie dostępne funkcje lambda. Dustjs (https://github.com/akdubya/dustjs) stawia na wydajność i funkcje, na przykład strumieniowanie. Listę silników szablonów dla Node znajdziesz w projekcie consolidate.js (https://github.com/visionmedia/consolidate.js), który zapewnia API pozwalające na abstrakcję użycia silników szablonów i ułatwia
stosowanie wielu silników w aplikacji. Jeśli jednak myśl o nauce składni stosowanej w różnych szablonach jest dla Ciebie zniechęcająca, silnik o nazwie Plates (https://github.com/flatiron/plates) pozwoli Ci na pozostanie przy kodzie HTML i wykorzystanie logiki silnika do mapowania danych aplikacji na identyfikatory i klasy CSS w kodzie znaczników. Jeżeli sposób, w jaki Jade obsługuje rozdzielenie warstwy prezentacyjnej i logiki aplikacji, wydaje Ci się kuszący, zainteresuj się Stylusem (https://github.com/LearnBoost/stylus), czyli projektem stosującym podobne podejście podczas tworzenia stylów CSS. Zapoznałeś się z wiedzą niezbędną do tworzenia profesjonalnych aplikacji sieciowych. W następnym rozdziale zajmiemy się wdrażaniem, czyli udostępnieniem aplikacji całemu światu.
Część III Co dalej? W ostatniej części książki przeczytasz, jak można używać Node w inny sposób niż do tworzenia aplikacji sieciowych oraz jak za pomocą biblioteki Socket.IO dodawać do aplikacji sieciowej komponenty działające w czasie rzeczywistym. Ponadto dowiesz się, jak wykorzystywać Node do tworzenia serwerów innych niż HTTP TCP/IP, a nawet w narzędziach działających z poziomu powłoki. Oprócz nowych sposobów użycia Node poznasz również funkcjonowanie ekosystemu społeczności Node, odkryjesz miejsca, w których można uzyskać pomoc, a także dowiesz się, jak własnymi projektami podzielić się ze społecznością Node za pomocą repozytorium Node Package Manager.
Rozdział 12. Wdrażanie aplikacji Node i zapewnienie bezawaryjnego działania W tym rozdziale: • Wybór hostingu dla aplikacji Node. • Wdrażanie typowej aplikacji. • Zapewnienie działania aplikacji i maksymalizacja jej wydajności.
Opracowanie aplikacji sieciowej to jedno, a umieszczenie jej w środowisku produkcyjnym to zupełnie co innego. W przypadku każdej platformy sieciowej istnieją pewne wskazówki i sztuczki pomagające w zwiększeniu stabilności i maksymalizacji wydajności. Node nie jest tutaj wyjątkiem. Kiedy stajesz przed koniecznością wdrożenia aplikacji sieciowej, w pierwszej kolejności zastanawiasz się, jakie masz możliwości w zakresie hostingu. Konieczne jest również rozważenie, jak monitorować aplikację i zapewnić jej działanie. Być może zastanawiasz się, co można jeszcze zrobić, aby aplikacja działała z maksymalną szybkością. W tym rozdziale dowiesz się, jak przeprowadzić wdrożenie aplikacji sieciowej Node. Na początek sprawdzimy, jakie mamy możliwości w zakresie hostingu aplikacji Node.
12.1. Hosting aplikacji Node Większość programistów aplikacji sieciowych zna aplikacje oparte na PHP. Kiedy serwer Apache zapewniający obsługę PHP otrzyma żądanie HTTP, ścieżkę żądanego adresu URL mapuje na konkretny plik, a PHP wykonuje zawartość wspomnianego pliku. Taka funkcjonalność ułatwia wdrażanie aplikacji PHP — pliki PHP wystarczy umieścić w określonej lokalizacji systemu plików, a staną się dostępne dla przeglądarek internetowych. Cechą hostingu PHP jest nie tylko łatwe wdrożenie, ale również niska cena, ponieważ serwery są bardzo często współdzielone przez wielu użytkowników. Wdrożenie aplikacji Node za pomocą usług hostingu w chmurze przygotowanych pod kątem Node i oferowanych przez firmy takie jak Joyent, Heroku, Nodejitsu, VMware i Microsoft nie jest trudne. Wspomniane usługi hostingu w chmurze przygotowane z myślą o Node są warte uwagi, jeśli chcesz uniknąć problemów związanych z administracją własnym serwerem, a jednocześnie chcesz korzystać z zalet diagnostyki opracowanej specjalnie dla Node. Przykładem
może być oferowana przez system operacyjny SmartOS firmy Joyent możliwość sprawdzenia, która logika w aplikacji Node działa najwolniej. Witryna Cloud9 została zbudowana za pomocą Node.js, a nawet oferuje oparte na przeglądarce internetowej zintegrowane środowisko programistyczne (IDE), w którym można klonować projekty z serwisu GitHub, pracować nad nimi w przeglądarce internetowej, a następnie wdrażać do wielu usług hostingu w chmurze przygotowanych specjalnie dla Node (patrz tabela 12.1). Tabela 12.1. Usługi IDE i hostingu w chmurze przygotowane pod kątem Node Nazwa
Witryna internetowa
Heroku
https://www.heroku.com/
Nodejitsu
https://www.nodejitsu.com/
VMware’s Cloud Foundry
http://www.gopivotal.com/platform-as-a-service/pivotal-cf
Microsoft Azure SDK for Node.js
http://azure.microsoft.com/en-us/develop/nodejs/
Cloud9 IDE
https://c9.io/
Alternatywą dla hostingu w chmurze przygotowanego z myślą o Node jest uruchomienie własnego serwera. Linux to popularny wybór dla serwerów Node. Oferuje znacznie większą elastyczność niż wspomniany hosting w chmurze przygotowany specjalnie dla Node, ponieważ pozwala na łatwą instalację potrzebnych aplikacji, na przykład baz danych. W przypadku hostingu w chmurze do dyspozycji jest na ogół ograniczona liczba aplikacji. Administrowanie serwerem działającym pod kontrolą systemu Linux wymaga doświadczenia. Jeżeli zdecydujesz się na użycie własnego serwera, będziesz musiał nieco poczytać o wybranej dystrybucji systemu Linux i upewnić się o posiadaniu odpowiedniej wiedzy z zakresu jego konfiguracji i obsługi. VirtualBox. Jeżeli zagadnienia związane z administracją serwerem są dla Ciebie nowością, zawsze możesz eksperymentować za pomocą oprogramowania wirtualizacji, na przykład takiego jak VirtualBox (https://www.virtualbox.org/), które pozwala na uruchamianie komputerów wirtualnych działających pod kontrolą danego systemu, na przykład Linux. W przypadku użycia wirtualizacji nie ma żadnego znaczenia, pod kontrolą jakiego systemu działa fizyczny komputer. Jeżeli temat różnych opcji serwera nie jest Ci obcy, możesz jedynie przejrzeć informacje aż do podrozdziału 12.2, w którym przystąpimy do omawiania podstaw wdrażania aplikacji Node. Najpierw przekonajmy się, jakie mamy dostępne opcje: serwery dedykowane,
serwery VPS, ogólnego przeznaczenia serwery chmury. Przeanalizujemy teraz niektóre opcje dostępne podczas wyboru hostingu dla aplikacji Node.
12.1.1. Serwery dedykowane i VPS Serwer może być fizyczny i wówczas jest określany mianem dedykowanego lub też wirtualny. Serwery wirtualne działają wewnątrz fizycznych i współdzielą zasoby serwera fizycznego: procesor, pamięć RAM i przestrzeń na dysku twardym. Serwery wirtualne emulują fizyczne i można nimi administrować w dokładnie taki sam sposób. W pojedynczym serwerze fizycznym może działać wiele serwerów wirtualnych. Serwery dedykowane są zwykle dużo droższe od wirtualnych, a ich przygotowanie najczęściej wymaga nieco dłuższego czasu, ponieważ może wystąpić konieczność zamówienia komponentów, złożenia całości i przeprowadzenia konfiguracji. Natomiast serwery VPS (ang. Virtual Private Server) mogą być przygotowane bardzo szybko, ponieważ są tworzone w istniejących serwerach fizycznych. VPS to dobre rozwiązanie w zakresie hostingu dla aplikacji sieciowych, jeśli nie przewidujesz szybkiego wzrostu stopnia ich użycia. Rozwiązanie oparte na VPS jest tanie, a ponadto pozwala na łatwe dodanie zasobów, takich jak przestrzeń na dysku lub pamięć RAM, gdy wystąpi potrzeba. Odpowiednia technologia jest już opracowana, a wiele firm, na przykład Linode (https://www.linode.com/) i Prgmr (http://prgmr.com/xen/), bardzo ułatwia rozpoczęcie pracy z VPS. Podobnie jak serwery wirtualne, także serwery VPS zwykle nie mogą być tworzone na żądanie. Najczęściej nie nadążają za szybko zwiększającym się poziomem zużycia zasobów, ponieważ to wymaga możliwości szybkiego dodawania kolejnych serwerów bez konieczności interwencji ze strony człowieka. Aby spełnić wspomniane wymagania, trzeba skorzystać z hostingu w chmurze.
12.1.2. Hosting w chmurze Serwer chmury jest podobny do VPS pod tym względem, że stanowi wirtualną emulację serwera dedykowanego. Jednak w porównaniu z serwerami dedykowanymi i VPS ma przewagę w postaci w pełni zautomatyzowanego zarządzania. Serwery chmury mogą być tworzone, uruchamiane, zatrzymywane i usuwane za pomocą zdalnego interfejsu lub API.
Kto będzie potrzebował tego rodzaju rozwiązania? Przypuśćmy, że założyłeś firmę posiadającą oparte na Node korporacyjne oprogramowanie intranet. Chcesz umożliwić klientom zarejestrowanie się do oferowanej usługi i krótko po tym uzyskanie dostępu do ich serwerów działających w Twoim oprogramowaniu. Wprawdzie możesz zatrudnić personel techniczny do konfiguracji i wdrażania serwerów klientów przez całą dobę, ale jeśli nie posiadasz własnego centrum danych, wspomniany personel nadal będzie musiał koordynować działania z dostawcą serwerów dedykowanych lub VPS, aby na czas zapewnić odpowiednie zasoby. Dzięki użyciu serwerów chmury operacje zarządzania serwerem można przeprowadzać za pomocą instrukcji wysyłanych przez API do dostawcy chmury i tym samym uzyskiwać dostęp do nowych serwerów, gdy zajdzie potrzeba. Ten poziom automatyzacji pozwala na szybkie dostarczanie usług klientom bez konieczności podejmowania interwencji ze strony człowieka. Na rysunku 12.1 zilustrowano przykład użycia hostingu w chmurze do tworzenia i usuwania serwerów aplikacji.
Rysunek 12.1. Tworzenie, uruchamianie, zatrzymywanie i usuwanie serwerów w chmurze może być w pełni zautomatyzowane
Wadą użycia serwerów w chmurze jest ich wyższa cena niż w przypadku VPS, a także konieczność posiadania pewnej wiedzy w zakresie konkretnej platformy w chmurze.
Amazon Web Services Najstarszą i najpopularniejszą platformą chmury jest Amazon Web Services (http://aws.amazon.com/). AWS składa się z różnych usług związanych z hostingiem, na przykład dostarczania wiadomości e-mail, CDN (ang. Content Delivery Networks) itd. Oferowana przez Amazon usługa Elastic Compute Cloud (EC2) to jedna z centralnych usług AWS, pozwalająca na tworzenie serwerów w chmurze, gdy zachodzi potrzeba. Wirtualne serwery EC2 są nazywane egzemplarzami i mogą być zarządzane z poziomu powłoki lub za pomocą konsoli opartej na przeglądarce internetowej, jak pokazano na rysunku 12.2. Ponieważ poznanie sposobu zarządzania AWS z poziomu powłoki wymaga nieco czasu, użytkownikom dopiero zaczynającym pracę z EC2 zaleca się użycie konsoli graficznej.
Rysunek 12.2. Konsola AWS jest przeznaczona do zarządzania serwerami chmury Amazon; dla nowych użytkowników to narzędzie łatwiejsze w użyciu niż powłoka
Na szczęście z powodu rozpowszechnienia AWS bardzo łatwo można znaleźć w internecie pomoc oraz samouczki dotyczące tej konsoli. Przykładem może być oferowany przez Amazon samouczek „Getting Started with Amazon EC2 Linux Instances” (http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EC2_GetStarted.html).
Rackspace Cloud Znacznie prostszą i łatwiejszą w użyciu platformą chmury jest Rackspace Cloud
(http://www.rackspace.com/cloud/). Wspomniana łatwość może wydawać się kusząca, ale Rackspace Cloud oferuje znacznie mniejszą gamę funkcji i produktów powiązanych z chmurą niż AWS, a interfejs graficzny przeznaczony do zarządzania usługą jest, delikatnie mówiąc, niewygodny. Serwerami Rackspace Cloud można zarządzać za pomocą interfejsu graficznego lub utworzonych przez społeczność narzędzi działających z poziomu powłoki. W tabeli 12.2 podsumowano omówione w tym podrozdziale opcje, które znajdziesz w zakresie hostingu aplikacji Node. Po ogólnym przedstawieniu usług, które można wykorzystać do hostingu aplikacji Node, teraz przechodzimy do tematu faktycznego wdrożenia aplikacji Node w serwerze. Tabela 12.2. Podsumowanie opcji dotyczących hostingu Możliwość wzrostu ruchu sieciowego
Opcja hostingu
Względny koszt
Wolna
serwer dedykowany
średni
Liniowa
serwer VPS
mały
Nieprzewidywalna
chmura
wysoki
12.2. Podstawy wdrożenia Przyjmujemy założenie, że utworzyłeś aplikację sieciową, którą chcesz się pochwalić światu. Ewentualnie opracowałeś aplikację komercyjną i musisz ją przetestować przed umieszczeniem w środowisku produkcyjnym. Prawdopodobnie zaczniesz od prostego wdrożenia, a następnie wykonasz nieco dodatkowej pracy w celu maksymalizacji wydajności i zapewnienia niezawodnego działania aplikacji. W tym podrozdziale przeanalizujemy proste, tymczasowe wdrożenie Git, a także omówimy sposoby zapewnienia niezawodnego działania aplikacji za pomocą Forever. Tymczasowe wdrożenie istnieje tylko do chwili ponownego uruchomienia, ale jednocześnie charakteryzuje się szybką konfiguracją.
12.2.1. Wdrożenie z repozytorium Git Poniżej przedstawiono podstawowe wdrożenie za pomocą repozytorium Git, co pozwoli Ci na poznanie podstawowych etapów procesu. Wdrożenie odbywa się przez wykonanie wymienionych poniżej kroków: 1. Nawiązanie połączenia z serwerem za pomocą SSH. 2. Instalacja Node i narzędzi kontroli wersji (na przykład Git lub Subversion) w serwerze, o ile zachodzi potrzeba.
3. Pobranie z repozytorium kontroli wersji plików aplikacji, między innymi skryptów Node, obrazów, arkuszy stylów CSS, a następnie umieszczenie ich w serwerze. 4. Uruchomienie aplikacji. Poniżej znajduje się przykład uruchomienia aplikacji po pobraniu jej plików z repozytorium Git: git clone https://github.com/Marak/hellonode.git cd hellonode node server.js
Podobnie jak PHP, także Node nie działa jako zadanie w tle. Dlatego też przedstawione tutaj podstawowe wdrożenie wymaga zachowania otwartego połączenia SSH. Po zamknięciu połączenia nastąpi natychmiastowe zakończenie działania aplikacji. Na szczęście bardzo łatwo można zapewnić nieustanne działanie aplikacji za pomocą prostego narzędzia. Wdrożenie automatyczne. Wdrożenie aplikacji Node można zautomatyzować na wiele sposobów. Jednym z nich jest użycie narzędzia takiego jak Fleet (https://github.com/substack/fleet), które pozwala na wdrożenie jednego lub więcej serwerów za pomocą git push. Znacznie bardziej tradycyjnym podejściem jest użycie Capistrano, co zostało dokładnie omówione w poście Evana Tahlera zatytułowanym „Deploying node.js applications with Capistrano” (http://blog.evantahler.com/blog/deployingnode-js-applications-with-capistrano.html).
12.2.2. Zapewnienie działania aplikacji Node Przyjmujemy założenie, że utworzyłeś osobisty blog za pomocą aplikacji Cloud9 Nog (https://github.com/c9/nog), a teraz chcesz ją wdrożyć i mieć gwarancje jej działania nawet po zamknięciu połączenia SSH. W społeczności Node najpopularniejszym narzędziem do tego celu jest Nodejitsu Forever (https://github.com/nodejitsu/forever). Zapewnia ono działanie aplikacji po zamknięciu połączenia SSH, a ponadto ponownie ją uruchamia, jeśli uległa awarii. Koncepcję sposobu działania Forever zilustrowano na rysunku 12.3.
Rysunek 12.3. Narzędzie Forever pomaga w zapewnieniu działania aplikacji, nawet jeśli ulegnie ona awarii
Forever można zainstalować globalnie za pomocą polecenia
sudo.
Polecenie sudo. Bardzo często podczas instalacji globalnej (to znaczy z użyciem opcji -g) modułu menedżera npm polecenie npm trzeba poprzedzić p ol ecen i em sudo (http://www.sudo.ws/), aby menedżer npm działał z uprawnieniami superużytkownika. Po pierwszym wydaniu polecenia sudo konieczne jest podanie hasła użytkownika. Następnie wykonane zostanie polecenie znajdujące się po sudo. Poniższe polecenie powoduje globalną instalację Forever: sudo npm install -g forever
Po instalacji możesz użyć narzędzia Forever do uruchomienia bloga i zapewnienia jego działania. W tym celu wystarczy wydać poniższe polecenie: forever start server.js
Jeżeli z jakiegokolwiek powodu będziesz chciał zatrzymać działanie aplikacji bloga, wystarczy użyć polecenia stop narzędzia Forever: forever stop server.js
Forever pozwala również na pobranie listy aplikacji zarządzanych przez to narzędzie. Do tego celu służy polecenie list: forever list
Inna użyteczna możliwość narzędzia Forever to opcjonalne ponowne uruchomienie aplikacji po zmianie któregokolwiek z jej plików źródłowych. To uwalnia programistę od konieczności ręcznego ponownego uruchamiania aplikacji za każdym razem, gdy zostanie dodana nowa funkcja lub usunięty błąd. Aby uruchomić narzędzie Forever we wspomnianym trybie, należy użyć opcji -w: forever -w start server.js
Wprawdzie Forever to niezwykle użyteczne narzędzie przeznaczone do
wdrażania aplikacji, ale na potrzeby długoterminowych wdrożeń możesz potrzebować narzędzia wyposażonego w nieco więcej funkcji i możliwości. W kolejnym podrozdziale poznasz wybrane przemysłowe rozwiązania w zakresie monitorowania oraz dowiesz się, jak można zmaksymalizować wydajność działania aplikacji.
12.3. Maksymalizacja wydajności i czasu bezawaryjnego działania aplikacji Kiedy aplikacja Node jest gotowa do udostępnienia użytkownikom, na pewno chcesz się upewnić o jej uruchamianiu i zatrzymywaniu wraz z serwerem oraz o automatycznym ponownym uruchamianiu jej po awarii serwera. Bardzo łatwo zapomnieć o zatrzymaniu aplikacji przed ponownym uruchomieniem serwera lub o jej uruchomieniu tuż po ponownym uruchomieniu serwera. Konieczne jest również upewnienie się o podjęciu wszelkich kroków zapewniających maksymalną wydajność działania aplikacji. Jeśli aplikacja działa w serwerze wyposażonym w procesor czterordzeniowy, wtedy sensowne jest wykorzystanie przez nią więcej niż tylko jednego rdzenia. Gdy używany jest tylko jeden rdzeń, a ilość ruchu sieciowego obsługiwanego przez aplikację gwałtownie wzrośnie, wówczas jeden rdzeń może nie mieć wystarczających możliwości do obsługi całego ruchu, a aplikacja nie będzie natychmiast odpowiadała na działania użytkownika. Oprócz wykorzystania wszystkich rdzeni procesora należy też unikać użycia Node do udostępniania plików statycznych w witrynach produkcyjnych obsługujących duży ruch sieciowy. Node opracowano dla aplikacji interaktywnych, takich jak aplikacje sieciowe i protokoły TCP/IP. Dlatego też nie potrafi udostępniać plików statycznych w tak efektywny sposób jak oprogramowanie przeznaczone tylko do tego celu. Do udostępniania plików statycznych należy wykorzystać technologie, które się w tym specjalizują, takie jak Nginx (http://nginx.org/en/). Ewentualnie wszystkie pliki statyczne można umieścić w systemie CDN, na przykład Amazon S3 (http://aws.amazon.com/s3/), a następnie odwoływać się do tych plików z poziomu aplikacji Node. W tym podrozdziale zostaną przedstawione pewne funkcje pomagające w poprawie wydajności i zapewnieniu niezawodnego działania aplikacji: Użycie Upstart do zapewnienia działania aplikacji i jej ponownego uruchamiania, na przykład po awarii. Użycie API klastra Node do wykorzystania możliwości oferowanych przez procesory wielordzeniowe. Obsługa plików statycznych w aplikacji Node za pomocą Nginx.
Rozpoczynamy od oferującego potężne możliwości i jednocześnie łatwego w użyciu narzędzia Upstart, które zapewnia niezawodne działanie aplikacji.
12.3.1. Zapewnienie działania aplikacji za pomocą Upstart Przyjmujemy założenie, że jesteś zadowolony z opracowanej aplikacji i chcesz ją udostępnić innym. Ponadto chcesz mieć absolutną pewność, że po ponownym uruchomieniu serwera nie zapomnisz ponownie uruchomić aplikacji. Poza tym po ewentualnej awarii aplikacja powinna zostać automatycznie ponownie uruchomiona, a awaria — odnotowana w dzienniku zdarzeń. Powinieneś też otrzymać informację o incydencie, co pozwoli Ci na zdiagnozowanie i usunięcie problemów. Upstart (http://upstart.ubuntu.com/) to projekt zapewniający eleganckie rozwiązanie w zakresie zarządzania uruchamianiem i zatrzymywaniem dowolnej aplikacji systemu Linux, w tym także aplikacji Node. Najnowsze wydania dystrybucji Ubuntu i CentOS obsługują użycie Upstart. Jeżeli narzędzie Upstart nie jest jeszcze zainstalowane w Ubuntu, instalacja zostanie przeprowadzona po wydaniu poniższego polecenia: sudo apt-get install upstart
Jeżeli narzędzie Upstart nie jest jeszcze zainstalowane w systemie CentOS, instalacja zostanie przeprowadzona po wydaniu poniższego polecenia: sudo yum install upstart
Po zainstalowaniu Upstart konieczne jest dodanie pliku konfiguracyjnego Upstart dla każdej aplikacji. Wspomniane pliki są tworzone w katalogu /etc/init i powinny mieć nazwę w stylu nazwa_aplikacji.conf. Pliki konfiguracyjne nie wymagają uprawnień do ich wykonywania. Poniższe polecenie powoduje utworzenie pustego pliku konfiguracyjnego Upstart dla przykładowej aplikacji przedstawionej w tym rozdziale: sudo touch /etc/init/hellonode.conf
W utworzonym pliku konfiguracyjnym umieść kod przedstawiony w listingu 12.1. Przygotowana konfiguracja spowoduje uruchomienie aplikacji wraz z serwerem i zakończenie jej działania wraz z zamknięciem serwera. Sekcja exec to kod wykonywany przez Upstart. Listing 12.1. Typowy plik konfiguracyjny Upstart author "Jak Kowalski" Podanie imienia i nazwiska twórcy aplikacji. description "hellonode" Podanie nazwy aplikacji lub jej opisu. setuid "nonrootuser" Aplikacja będzie uruchomiona przez użytkownika innego niż
root. start on (local-filesystems and net-device-up IFACE=eth0) Uruchomienie aplikacji podczas startu systemu, gdy system plików i sieć będą już dostępne. stop on shutdown Zakończenie działania aplikacji podczas zamykania systemu. respawn Ponowne uruchomienie aplikacji po jej awarii. console log Komunikaty standardowego wejścia i błędów będą zapisywane w pliku /var/log/upstart/nazwa_aplikacji.log. env NODE_ENV=production Zdefiniowanie wszelkich zmiennych środowiskowych niezbędnych dla aplikacji. exec /usr/bin/node /ścieżka/dostępu/do/serwera.js Podanie polecenia uruchamiającego aplikację.
Przedstawiony plik konfiguracyjny zapewnia działanie procesu po ponownym uruchomieniu serwera, a nawet po wystąpieniu awarii samej aplikacji. Wszystkie dane wyjściowe wygenerowane przez aplikację zostaną umieszczone w pliku /var/log/upstart/hellonode.log, a narzędzie Upstart automatycznie zajmie się rotacją dzienników zdarzeń. Po utworzeniu pliku konfiguracyjnego Upstart aplikację można uruchomić przez wydanie poniższego polecenia: sudo service hellonode
Jeżeli uruchomienie aplikacji zakończyło się powodzeniem, otrzymasz komunikat podobny do poniższego: hellonode start/running, process 6770
Narzędzie Upstart oferuje bogate możliwości w zakresie konfiguracji. Warto zapoznać się z dostępnym w internecie podręcznikiem (http://upstart.ubuntu.com/cookbook/), w którym wymieniono wszystkie opcje. Narzędzie Upstart i ponowne uruchamianie Po użyciu opcji respawn narzędzie Upstart będzie nieustannie i automatycznie uruchamiać aplikację po jej awarii, o ile aplikacja nie ulegnie awarii dziesięciokrotnie w ciągu pięciu sekund. Wspomniany limit można zmienić za pomocą opcji respawn limit liczba przedział_czasu, gdzie liczba oznacza ilość uruchomień w podanym przedziale_czasu wyrażonym w sekundach. Na przykład ustawienie limitu dwudziestu uruchomień w ciągu pięciu sekund wymaga użycia poniższych opcji: respawn respawn limit 20 5 Jeżeli aplikacja zostanie ponownie uruchomiona 10 razy w ciągu 5 sekund (domyślny limit), zwykle oznacza to problem w kodzie lub konfiguracji i prawdopodobnie nigdy nie zostanie ona prawidłowo uruchomiona. Po osiągnięciu limitu narzędzie Upstart nie będzie ponownie próbowało uruchomić aplikacji, co ma na celu zachowanie zasobów dla innych procesów. Dobrym rozwiązaniem jest sprawdzanie stanu aplikacji poza narzędziem Upstart, aby jej programistom dostarczać odpowiednie komunikaty, na przykład w postaci wiadomości e-mail. Operacja może polegać na przejściu do witryny i sprawdzeniu, czy użytkownik otrzymuje prawidłową odpowiedź. Możesz wykorzystać własne metody lub użyć jednego z dostępnych narzędzi przeznaczonych do tego celu, na przykład Monit (http://mmonit.com/monit/) lub Zabbix (http://www.zabbix.com/).
Skoro już wiesz, jak zapewnić działanie aplikacji niezależnie od awarii lub ponownego uruchomienia serwera, kolejnym logicznym krokiem jest osiągnięcie maksymalnej wydajności działania. Na tym polu pomocne może okazać się API klastra Node.
12.3.2. API klastra — wykorzystanie zalety w postaci wielu rdzeni Większość nowoczesnych procesorów zawiera kilka rdzeni, ale Node podczas działania używa tylko jednego z nich. Jeżeli umieściłeś aplikację Node w serwerze i chcesz w maksymalnym stopniu wykorzystać jej możliwości sprzętowe, jednym z rozwiązań może być ręczne uruchomienie wielu egzemplarzy aplikacji działających na różnych portach TCP/IP. Następnie za pomocą mechanizmu równoważenia obciążenia trzeba rozkładać ruch sieciowy między poszczególne egzemplarze. Przygotowanie tego rodzaju rozwiązania jest pracochłonne. Aby ułatwić wykorzystanie wielu rdzeni przez pojedynczą aplikację, do Node dodano API klastra. Wymienione API ułatwia aplikacji jednoczesne działanie wielu „procesów roboczych” w poszczególnych rdzeniach, wykonujących to samo zadanie i udzielających odpowiedzi na tym samym porcie. Na rysunku 12.4 pokazano sposób działania aplikacji wykorzystującej API klastra w procesorze czterordzeniowym.
Rysunek 12.4. Utworzenie trzech dodatkowych procesów roboczych w procesorze czterordzeniowym
Kod przedstawiony w listingu 12.2 automatycznie tworzy procesy robocze dla każdego dodatkowego rdzenia w procesorze. Listing 12.2. Demonstracja użycia API klastra Node var cluster = require('cluster'); var http = require('http'); var numCPUs = require('os').cpus().length; Ustalenie liczby rdzeni w procesorze
serwera. if (cluster.isMaster) { for (var i = 0; i < numCPUs; i++) { Utworzenie procesu roboczego dla każdego z nich. cluster.fork(); } cluster.on('exit', function(worker, code, signal) { console.log('Proces roboczy ' + worker.process.pid + ' zakończył działanie.'); }); } else { http.Server(function(req, res) { Zdefiniowanie zadania wykonywanego przez poszczególne procesy robocze. res.writeHead(200); res.end('Jestem procesem roboczym działającym w procesie ' + process.pid); }).listen(8000); }
Ponieważ procesy główny i robocze to oddzielne procesy systemu operacyjnego (to konieczne, aby mogły działać w oddzielnych rdzeniach), nie mogą współdzielić informacji o stanie za pomocą zmiennych globalnych. Na szczęście API klastra Node zapewnia rozwiązanie pozwalające na komunikację między procesem głównym i roboczymi. W listingu 12.3 przedstawiono przykład aplikacji, w której zachodzi komunikacja między procesami głównym i roboczymi. Liczba wszystkich żądań jest przechowywana przez proces główny, natomiast poszczególne procesy robocze informują o obsłudze każdego żądania. Listing 12.3. Demonstracja użycia API klastra Node var cluster = require('cluster'); var http = require('http'); var numCPUs = require('os').cpus().length; var workers = {}; var requests = 0; if (cluster.isMaster) { for (var i = 0; i < numCPUs; i++) { workers[i] = cluster.fork(); (function (i) { workers[i].on('message', function(message) { Nasłuchiwanie informacji z procesu roboczego. if (message.cmd == 'incrementRequestTotal') { requests++; Zwiększenie liczby wszystkich żądań.
for (var j = 0; j < numCPUs; j++) { workers[j].send({ Wysłanie wszystkim procesom roboczym informacji o całkowitej liczbie żądań. cmd:
'updateOfRequestTotal',
requests: requests }); } } }); })(i); Użycie domknięcia w celu zachowania wartości procesu roboczego. } cluster.on('exit', function(worker, code, signal) { console.log('Proces roboczy ' + worker.process.pid + ' zakończył działanie.'); }); } else { process.on('message', function(message) { Nasłuchiwanie informacji z procesu głównego. if (message.cmd == 'updateOfRequestTotal') { requests = message.requests; Uaktualnienie licznika żądań za pomocą komunikatu procesu głównego. } }); http.Server(function(req, res) { res.writeHead(200); res.end('Proces roboczy w procesie ' + process.pid + ' twierdzi, że klaster udzielił odpowiedzi na ' + requests + ' żądań.'); process.send({cmd: 'incrementRequestTotal'}); Poinformowanie procesu głównego o konieczności zwiększenia licznika żądań. }).listen(8000); }
Użycie API klastra Node to prosty sposób pozwalający na tworzenie aplikacji wykorzystujących pełnię możliwości nowoczesnego sprzętu komputerowego.
12.3.3. Proxy i hosting plików statycznych Wprawdzie Node to efektywne rozwiązanie w zakresie udostępniania dynamicznej treści sieciowej, ale nie sprawdza się już tak efektywnie podczas udostępniania plików statycznych, takich jak obrazy, style CSS lub skrypty
JavaScript działające po stronie klienta. Udostępnianie plików statycznych przez HTTP to specjalny rodzaj zadania, do realizacji którego opracowano odpowiednie oprogramowanie. Wspomniane oprogramowanie jest od lat używane do tego rodzaju operacji i zostało specjalnie zoptymalizowane pod ich kątem. Na szczęście w Node bardzo łatwo można przeprowadzić konfigurację Nginx (http://nginx.org/en/) — to dostępny jako oprogramowanie open source serwer WWW, który został zoptymalizowany do udostępniania plików statycznych. W typowej konfiguracji Nginx/Node serwer Nginx początkowo obsługuje każde żądanie sieciowe i te żądania, które nie dotyczą plików statycznych, są przekazywane do Node. Tego rodzaju konfigurację zilustrowano na rysunku 12.5.
Rysunek 12.5. Nginx można użyć jako proxy do szybkiego przekazywania zasobów statycznych z powrotem do klientów sieciowych
Rozwiązanie pokazane na rysunku 12.5 zostało zaimplementowane w kodzie przedstawionym w listingu 12.4. Ten listing to sekcja http pliku konfiguracyjnego Nginx. Wspomniany plik konfiguracyjny Nginx jest w systemie Linux przechowywany zgodnie z konwencją w katalogu /etc (/etc/nginx/nginx.conf). Dzięki użyciu Nginx do obsługi statycznych zasobów aplikacji sieciowych gwarantujesz, że Node będzie wykorzystywane do zadań, w realizacji których sprawdza się najlepiej. Listing 12.4. Plik konfiguracyjny, który używa Nginx jako proxy dla Node.js i udostępnia pliki statyczne http { upstream my_node_app { server 127.0.0.1:8000; Adres IP i numer portu aplikacji Node.
} server { listen 80; Port, na którym proxy będzie otrzymywało żądania. server_name localhost domain.com; access_log /var/log/nginx/my_node_app.log; location ~ /static/ { Obsługa żądań plików dla adresów URL rozpoczynających się od /static/. root /home/node/my_node_app; if (!-f $request_filename) { return 404; } } location / { Zdefiniowanie ścieżki adresu URL, na który proxy będzie udzielać odpowiedzi. proxy_pass http://my_node_app; proxy_redirect off; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-NginX-Proxy true; } } }
12.4. Podsumowanie W tym rozdziale przedstawiono wiele opcji w zakresie hostingu aplikacji Node, między innymi rozwiązania opracowane pod kątem Node, serwery dedykowane, serwery VPS oraz hosting w chmurze. Każda opcja sprawdza się w innych zastosowaniach. Gdy osiągniesz gotowość do wdrożenia aplikacji Node dla ograniczonego kręgu odbiorców, proces wdrożenia będziesz mógł przeprowadzić szybko za pomocą narzędzia Forever. Z kolei w długoterminowych wdrożeniach dobrym rozwiązaniem może być zautomatyzowanie operacji uruchamiania i zatrzymywania działania aplikacji za pomocą narzędzia Upstart. Abyś mógł w maksymalnym stopniu wykorzystać zasoby sprzętowe serwera, do dyspozycji masz API klastra Node pozwalające na jednoczesne uruchamianie egzemplarzy aplikacji w wielu rdzeniach. Jeżeli aplikacja sieciowa musi udostępniać zasoby statyczne, takie jak obrazy lub dokumenty PDF, wówczas warto skonfigurować serwer Nginx i zastosować go w charakterze proxy dla
aplikacji Node. W ten sposób poznałeś tajniki aplikacji sieciowych Node. Warto więc dowiedzieć się, co jeszcze można osiągnąć za pomocą Node. W kolejnym rozdziale przyjrzymy się innym zastosowaniom Node: począwszy od tworzenia narzędzi działających w powłoce aż po narzędzia pozwalające na pobieranie danych z witryn internetowych.
Rozdział 13. Nie tylko serwery WWW W tym rozdziale: • Użycie biblioteki Socket.IO do prowadzenia w czasie rzeczywistym komunikacji między przeglądarkami internetowymi. • Implementacja sieci TCP/IP. • Użycie API Node do współpracy z systemem operacyjnym. • Opracowanie narzędzi działających w powłoce i posługiwanie się nimi.
Asynchroniczna natura Node pozwala na wykonywanie intensywnych operacji wejścia--wyjścia, co mogłoby być niemożliwe lub nieefektywne w środowisku synchronicznym. W tej książce omówiono aplikacje HTTP i być może chcesz zapytać: a co z innymi rodzajami aplikacji? Do czego Node może być jeszcze użyteczne? Powinieneś wiedzieć, że framework Node nie jest przeznaczony jedynie dla HTTP, ale służy do wszelkiego rodzaju ogólnych zadań wymagających przeprowadzania operacji wejścia-wyjścia. Oznacza to, że za pomocą Node możesz utworzyć praktycznie dowolny rodzaj aplikacji, na przykład program działający na poziomie powłoki, skrypt przeznaczony do administracji systemem, a także działające w czasie rzeczywistym aplikacje sieciowe. W tym rozdziale dowiesz się, jak tworzyć działające w czasie rzeczywistym serwery WWW wykraczające poza tradycyjny model serwera HTTP. Ponadto poznasz także inne API Node, które można wykorzystać do tworzenia innych rodzajów aplikacji, takich jak serwery TCP lub programy powłoki. Rozpoczynamy od omówienia biblioteki Socket.IO pozwalającej na prowadzenie w czasie rzeczywistym komunikacji między przeglądarkami internetowymi i serwerem.
13.1. Biblioteka Socket.IO Biblioteka Socket.IO (http://socket.io/) to bez wątpienia najbardziej znany moduł w społeczności Node. Programiści zainteresowani tworzeniem aplikacji sieciowych działających w czasie rzeczywistym, którzy nie znają Node, wcześniej lub później napotykają Socket.IO, a następnie odkrywają Node. Biblioteka Socket.IO pozwala na tworzenie działających w czasie rzeczywistym aplikacji sieciowych opartych na dwukierunkowej komunikacji między serwerem i klientem. W najprostszej postaci biblioteka Socket.IO ma API bardzo podobne do WebSocket API ( http://www.websocket.org/) oraz kilka wbudowanych rozwiązań zapasowych dla starszych przeglądarek internetowych, które nie
obsługują nowych funkcji. Socket.IO zapewnia wygodne API przeznaczone do rozgłaszania, wysyłania wiadomości itd. Oferowane funkcje powodują, że Socket.IO to bardzo popularna biblioteka dla gier działających w przeglądarkach internetowych, aplikacji czatu i aplikacji wykorzystujących strumienie. HTTP to protokół bezstanowy, co oznacza, że klient może wykonać tylko pojedyncze, krótkie żądania do serwera, który z kolei nie ma możliwości rozróżniania połączonych z nim użytkowników. Wspomniane ograniczenie doprowadziło do opracowania protokołu WebSocket, który pozwala przeglądarkom internetowym na utrzymanie połączenia typu pełny dupleks z serwerem — obie strony mogą wówczas jednocześnie wysyłać i otrzymywać dane. API WebSocket otwiera zupełnie nowe możliwości przed aplikacjami sieciowymi opartymi na prowadzonej w czasie rzeczywistym komunikacji między klientem i serwerem. Problem z protokołem WebSocket polega na tym, że prace nad nim nie zostały jeszcze ukończone. Wprawdzie pewne przeglądarki internetowe oferują już obsługę WebSocket, ale nadal wiele starszych wersji, przede wszystkim Internet Explorer, nie obsługuje WebSocket. Biblioteka Socket.IO rozwiązuje ten problem przez wykorzystanie WebSocket w przeglądarkach internetowych obsługujących tę technologię oraz zastosowanie pewnych rozwiązań zapasowych przeznaczonych dla przeglądarek pozbawionych obsługi WebSocket. Dzięki temu zachowania naśladujące WebSocket są dostępne nawet w starszych przeglądarkach internetowych. W tym podrozdziale utworzymy dwie przykładowe aplikacje oparte na bibliotece Socket.IO: Minimalną aplikację Socket.IO, która czas serwera przekazuje połączonym z nim klientom. Aplikację Socekt.IO odświeżającą stronę po modyfikacji pliku CSS. Po utworzeniu wymienionych przykładowych aplikacji poznasz jeszcze kilka innych sposobów użycia biblioteki Socket.IO, co odbędzie się podczas modyfikacji utworzonej w rozdziale 4. aplikacji wyświetlającej postęp operacji przekazywania plików do serwera. Zaczynamy jednak od podstaw.
13.1.1. Tworzenie minimalnej aplikacji Socket.IO Przyjmujemy założenie, że chcemy utworzyć niewielką aplikację sieciową, która nieustannie będzie wyświetlała w przeglądarce internetowej aktualny czas UTC serwera. Tego rodzaju aplikacja będzie służyła do sprawdzania różnicy między czasem serwera i klientów. Zastanów się teraz, jak taką aplikację można
utworzyć za pomocą modułu http i dotąd poznanych frameworków. Wprawdzie istnieje możliwość opracowania pewnego rozwiązania opartego na sztuczkach takich jak użycie puli, ale biblioteka Socket.IO oferuje estetyczny interfejs do realizacji tego rodzaju zadań. Implementacja wspomnianej aplikacji z wykorzystaniem Socket.IO jest niezwykle prosta. Aby utworzyć aplikację, w pierwszej kolejności należy zainstalować Socket.IO za pomocą menedżera npm: npm install socket.io
W listingu 13.1 przedstawiono kod działający po stronie serwera. Umieść go w pliku o nazwie clock-server.js; kod będzie można wypróbować po przygotowaniu kodu działającego po stronie klienta. Listing 13.1. Serwer Socket.IO nieustannie przekazujący klientowi aktualny czas var app = require('http').createServer(handler); var io = require('socket.io').listen(app); Uaktualnienie zwykłego serwera HTTP do serwera Socket.IO. var fs = require('fs'); var html = fs.readFileSync('index.html', 'utf8'); function handler (req, res) { Kod serwera HTTP zawsze udostępnia plik index.html. res.setHeader('Content-Type', 'text/html'); res.setHeader('Content-Length', Buffer.byteLength(html, 'utf8')); res.end(html); } function tick () { var now = new Date().toUTCString(); Pobranie bieżącego czasu w formacie UTC. io.sockets.send(now); Wysłanie aktualnego czasu do wszystkich połączonych gniazd. } setInterval(tick, 1000); Wywołanie funkcji tick() co sekundę. app.listen(8080);
Jak możesz się przekonać, biblioteka Socket.IO minimalizuje ilość dodatkowego kodu, który trzeba umieścić w podstawowym serwerze HTTP. Tylko dwa wiersze kodu są potrzebne do użycia zmiennej io (to zmienna egzemplarza serwera Socket.IO) w celu przesyłania w czasie rzeczywistym komunikatów między serwerem i klientami. W omawianym przykładzie funkcja tick() jest wywoływana co sekundę i przekazuje aktualny czas serwera wszystkim połączonym z nim klientom. Kod serwera w pierwszej kolejności wczytuje do pamięci plik index.html, który teraz zaimplementujemy. Kod działający po stronie klienta przedstawiono w listingu 13.2.
Listing 13.2. Klient Socket.IO wyświetlający czas otrzymany z serwera Aktualny czas w serwerze:
Wypróbowanie aplikacji Teraz można już wypróbować serwer. Uruchom go za pomocą wywołania node clock-server.js, a zobaczysz komunikat info - socket.io started. Oznacza to, że biblioteka Socket.IO została skonfigurowana i jest gotowa do otrzymywania połączeń. Uruchom więc przeglądarkę internetową i przejdź pod adres URL http://localhost:8080/. Powinieneś otrzymać wynik podobny do pokazanego na rysunku 13.1. Czas będzie uaktualniany co sekundę na podstawie komunikatu otrzymywanego z serwera. Uruchom śmiało inną przeglądarkę internetową, przejdź na ten sam adres URL i przekonaj się, że zmiana wartości czasu następuje jednocześnie w obu przeglądarkach.
Rysunek 13.1. Serwer czasu uruchomiony w oknie terminala i połączony z nim klient widoczny w przeglądarce internetowej
W ten sposób za pomocą biblioteki Socket.IO i zaledwie kilku wierszy kodu przygotowałeś prowadzoną w czasie rzeczywistym komunikację między serwerem i klientami. Inne rodzaje komunikatów obsługiwanych przez Socket.IO. Wysyłanie komunikatu do wszystkich połączonych gniazd to tylko jedno z rozwiązań udostępnianych przez Socket.IO i pozwalających na współpracę z połączonymi użytkownikami. Komunikaty można wysyłać także do poszczególnych gniazd, rozgłaszać je do wszystkich gniazd poza jednym wskazanym, a także można wysyłać zmienne (opcjonalne) komunikaty itd. Więcej informacji na ten temat znajdziesz w dokumentacji biblioteki Socket.IO dostępnej na stronie http://socket.io/#how-to-use. Skoro przekonałeś się, jak proste rozwiązania można stosować za pomocą biblioteki Socket.IO, teraz możemy przejść do innego przykładu, pokazującego użyteczność zdarzeń wysyłanych przez serwer.
13.1.2. Użycie biblioteki Socket.IO do odświeżenia strony i stylów CSS Oto jak w skrócie internetowych:
wygląda
typowy
sposób
pracy
projektanta
stron
1. Otworzenie strony internetowej w wielu przeglądarkach. 2. Wyszukanie elementów wymagających modyfikacji stylów. 3. Wprowadzenie odpowiednich zmian w jednym lub większej liczbie arkuszy stylów. 4. Ręczne odświeżenie strony we wszystkich przeglądarkach internetowych. 5. Powrót do kroku 2. Jedną z możliwości usprawnienia pracy jest automatyzacja kroku 4., w którym projektant musi ręcznie przejść do każdej przeglądarki internetowej i kliknąć przycisk odświeżający stronę. To zadanie jest szczególnie czasochłonne, gdy podczas pracy stronę trzeba przetestować w wielu przeglądarkach internetowych w różnych komputerach i urządzeniach mobilnych. Czy istnieje możliwość całkowitej eliminacji ręcznego odświeżania strony? Wyobraź sobie, że po zapisaniu arkusza stylów w edytorze tekstów wszystkie przeglądarki internetowe, w których jest ona wyświetlana, automatycznie odświeżają stronę i odzwierciedlają tym samym zmiany wprowadzone w CSS. To byłaby ogromna oszczędność czasu dla projektantów stron internetowych. Biblioteka Socket.IO w połączeniu z funkcjami Node fs.watchFile() i fs.watch()
umożliwia przygotowanie wspomnianego rozwiązania, na dodatek w zaledwie niewielu wierszach kodu. W
omawianym przykładzie użyjemy funkcji fs.watchFile() zamiast nowszej fs.watch(), ponieważ chcemy zachować gwarancję, że kod będzie działał dokładnie tak samo na wszystkich platformach. Dokładne omówienie sposobu działania funkcji fs.watch() znajdzie się w dalszej części rozdziału. fs.watchFile() kontra fs.watch(). Node.js oferuje API przeznaczone do obserwacji plików. Pierwsze z nich to funkcja fs.watchFile() (http://nodejs.org/api/fs.html#fs_fs_watchfile_filename_options_listener), która wykorzystuje sporą ilość zasobów, ale jest niezawodna i działa na wszystkich platformach. Drugie to funkcja fs.watch() (http://nodejs.org/api/fs.html#fs_fs_watch_filename_options_listener), która jest wysoce zoptymalizowana dla poszczególnych platform, ale jej zachowanie jest odmienne na pewnych platformach. Dokładne omówienie tej funkcji znajdziesz w punkcie 13.3.2. W omawianym przykładzie połączymy użycie frameworka Express i biblioteki Socket.IO. Oba wymienione komponenty doskonale ze sobą współpracują, podobnie jak użyty w poprzednim przykładzie http.Server. Najpierw zapoznamy się z kodem działającym po stronie serwera. Kod przedstawiony w listingu 13.3 umieść w pliku o nazwie watch-server.js, jeśli będziesz chciał ostatecznie uruchomić tworzoną tutaj aplikację. Listing 13.3. Serwer utworzony za pomocą Express/Socket.IO powoduje wywołanie zdarzenia po wykryciu zmiany w pliku var fs = require('fs'); var url = require('url'); var http = require('http'); var path = require('path'); var express = require('express'); var app = express(); Utworzenie aplikacji serwera Express. var server = http.createServer(app); var io = require('socket.io').listen(server); Opakowanie serwera HTTP w celu utworzenia egzemplarza serwera Socket.IO. var root = __dirname; Użycie metody pośredniczącej do monitorowania plików wskazanych przez metodę pośredniczącą static(). app.use(function (req, res, next) { Zarejestrowanie zdarzenia statycznego emitowanego przez metodę pośredniczącą static().
var file = url.parse(req.url).pathname;
var mode = 'stylesheet'; if (file[file.length - 1] == '/') { file += 'index.html'; mode = 'reload'; } createWatcher(file, mode); Określenie nazwy pliku i wywołanie createWatcher(). next(); }); app.use(express.static(root)); Konfiguracja serwera jako prostego serwera pliku statycznego. var watchers = {}; Zachowanie listy aktywnych plików, które są monitorowane. function createWatcher (file, event) { var absolute = path.join(root, file); if (watchers[absolute]) { return; } fs.watchFile(absolute, function (curr, prev) { Rozpoczęcie monitorowania pliku pod kątem zmian. if (curr.mtime !== prev.mtime) { Sprawdzenie, czy zmianie uległ mtime (czas ostatniej modyfikacji). Jeżeli tak, należy wywołać zdarzenie Socket.IO. io.sockets.emit(event, file); } }); watchers[absolute] = true; Oznaczenie pliku jako monitorowanego. } server.listen(8080);
Na tym etapie masz w pełni funkcjonalny serwer plików statycznych przygotowany do wywoływania u klientów zdarzeń reload i stylesheet za pomocą biblioteki Socket.IO. Teraz zajmiemy się kodem działającym po stronie klienta. Kod przedstawiony w listingu 13.4 umieść w pliku index.html, aby został udostępniony po uruchomieniu serwera i podaniu ścieżki dostępu do katalogu głównego. Listing 13.4. Kod działający po stronie klienta odpowiedzialny za ponowne wczytanie stylów po otrzymaniu zdarzenia z serwera
To jest nasza wspaniała strona internetowa!
Jeżeli ten plik (index.html
) zostanie zmodyfikowany, wówczas za pomocą biblioteki Socket.IO serwer wyśle komunikat do przeglądarki internetowej nakazujący odświeżenie strony.
Jeżeli którykolwiek arkusz stylów (header.css
lub styles.css
) zostanie zmodyfikowany, wówczas za pomocą biblioteki Socket.IO serwer wyśle komunikat do przeglądarki internetowej nakazujący dynamiczne wczytanie stylów CSS bez odświeżenia strony.
Wypróbowanie aplikacji Zanim aplikacja będzie działała, konieczne jest utworzenie kilku plików CSS (header.css i styles.css), ponieważ plik index.html wczytuje wymienione arkusze
stylów podczas generowania jego zawartości. Po przygotowaniu kodu działającego po stronie serwera, pliku index.html i arkuszy stylów używanych przez przeglądarkę internetową można wypróbować aplikację. Uruchom więc serwer: $ node watch-server.js
Po uruchomieniu serwera otwórz przeglądarkę internetową i przejdź na stronę http://localhost:8080, a zobaczysz udostępnioną wygenerowaną prostą stronę HTML. Teraz spróbuj zmodyfikować jeden z plików CSS (na przykład zmień kolor tła dla znacznika ). Przekonasz się, że arkusz stylów zostanie ponownie wczytany w przeglądarce internetowej bez odświeżenia samej strony. Spróbuj wyświetlić podaną stronę jednocześnie w wielu przeglądarkach internetowych. W omawianym przykładzie reload i stylesheet to własne zdefiniowane zdarzenia aplikacji — one nie są częścią API biblioteki Socket.IO. Na podstawie omówionego przykładu przekonałeś się, że obiekt socket działa w charakterze dwukierunkowego EventEmitter, którego można użyć do emisji zdarzeń transferowanych później przez Socket.IO do klienta.
13.1.3. Inne zastosowania dla biblioteki Socket.IO Jak zapewne wiesz, protokół HTTP nie został opracowany w celu zapewnienia jakiegokolwiek rodzaju komunikacji w czasie rzeczywistym. Jednak dzięki zastosowaniu technologii takich jak WebSocket i modułów takich jak biblioteka Socket.IO wspomniane ograniczenie zostało pokonane. W ten sposób otworzono drogę pozwalającą na opracowywanie wielu nowych rodzajów aplikacji sieciowych działających w przeglądarkach internetowych, co wcześniej było niemożliwe. W rozdziale 4. dowiedziałeś się, że biblioteka Socket.IO będzie doskonałym rozwiązaniem do przekazywania przeglądarce internetowej zdarzeń informujących o postępie podczas transferu pliku do serwera. Istnieje możliwość użycia także własnego zdarzenia progress: form.on('progress', function(bytesReceived, bytesExpected) { Uaktualniona wersja przykładu z punktu 4.4.3. var percent = Math.floor(bytesReceived / bytesExpected * 100); socket.emit('progress', { percent: percent }); Przekazanie za pomocą Socket.IO informacji wyrażonych w procentach. });
Aby przedstawione rozwiązanie działało, konieczne jest uzyskanie dostępu do e gz e mpla r z a socket dopasowującego przeglądarkę internetową, która przekazuje plik do serwera. To wykracza poza zakres tematyczny niniejszej
książki, ale zasoby dostępne w internecie mogą okazać się tutaj pomocne. (Początkujący powinni zapoznać się z artykułem Daniela Bauliga zatytułowanym „socket.io and Express: tying it all together”, który opublikowano na blogu blinzeln pod adresem http://www.danielbaulig.de/socket-ioexpress/). Biblioteka Socket.IO zmieniła zasady gry. Jak wcześniej wspomniano, programiści zainteresowani tworzeniem działających w czasie rzeczywistym aplikacji sieciowych bardzo często słyszeli o tej bibliotece, jeszcze zanim dowiedzieli się o istnieniu Node.js — to potwierdzenie, jak wpływowa i ważna jest biblioteka Socket.IO. Informacje dotyczące biblioteki nieustannie pojawiają się w społecznościach zajmujących się grami sieciowymi, jest ona używana do tworzenia bardziej kreatywnych gier i aplikacji, niż można to sobie wyobrazić. Wspomniana biblioteka to również bardzo popularny wybór w aplikacjach tworzonych w technologiach uznawanych za konkurencyjne dla Node, na przykład Node Knockout (http://nodeknockout.com/). Do jakich zapierających dech w piersiach celów ją wykorzystasz?
13.2. Dokładniejsze omówienie sieci TCP/IP Node to technologia doskonale przystosowana dla aplikacji działających w sieci, ponieważ na ogół używanie ich oznacza dużą ilość operacji wejścia-wyjścia. Poza serwerami HTTP, o których sporo się dowiedziałeś z tej książki, Node obsługuje praktycznie każdy rodzaj sieci opartej na TCP. Zatem platforma Node jest odpowiednia do utworzenia na przykład serwera poczty elektronicznej, plików lub proxy, a ponadto może być używana przez klientów dla wymienionego rodzaju usług. Technologia dostarcza kilku narzędzi pomagających w tworzeniu aplikacji o wysokiej jakości i zapewniających dużą wydajność w zakresie operacji wejścia-wyjścia. Więcej informacji na ten temat znajdziesz w tym podrozdziale. Pewne protokoły sieciowe wymagają wartości odczytywanych na poziomie bajtów — znaków, liczb całkowitych, liczb zmiennoprzecinkowych i innych typów danych obejmujących dane binarne. Jednak JavaScript nie oferuje żadnych rodzimych binarnych typów danych, z którymi można pracować. Najbliższe rozwiązanie to zastosowanie szalonych sztuczek wobec ciągów tekstowych. W Node zastosowano luźną implementację własnego typu danych Buffer, który działa jako fragment danych binarnych o stałej wielkości. To pozwala na uzyskanie dostępu na poziomie bajtów, co jest wymagane do implementacji innych protokołów. W tym podrozdziale zostaną poruszone wymienione poniżej zagadnienia: praca z buforami i danymi binarnymi, utworzenie serwera TCP,
utworzenie klienta TCP. Na początek przekonajmy się, jak Node współpracuje z danymi binarnymi.
13.2.1. Praca z buforami i danymi binarnymi to specjalny typ danych, który Node dostarcza programistom. Działa na zasadzie pojemnika dla niezmodyfikowanych danych binarnych o stałej wielkości. Bufor można potraktować jako odpowiednik funkcji języka C o nazwie malloc() lub słowa kluczowego new w C++. Bufory to bardzo szybkie i lekkie obiekty, są stosowane w podstawowym API Node. Na przykład domyślnie znajdują się w wartości zwrotnej zdarzeń data wszystkich klas Stream. Buffer
Node globalnie udostępnia konstruktor Buffer, zachęcając tym samym programistę do jego użycia jako rozszerzenia zwykłych typów JavaScript. Z programistycznego punktu widzenia bufory można traktować podobnie jak tablice. Różnice polegają na braku możliwości zmiany wielkości buforów, a ponadto bufory mogą przechowywać jedynie wartości w postaci liczb całkowitych z zakresu od 0 do 255. W ten sposób są idealnym rozwiązaniem do przechowywania danych binarnych praktycznie wszystkiego. Ponieważ bufory działają z niezmodyfikowanymi bajtami, można je wykorzystać do implementacji na niskim poziomie dowolnego protokołu.
Dane tekstowe kontra binarne Przyjmujemy założenie, że w pamięci chcesz przechowywać liczbę 121 234 869, używając do tego typu Buffer. Domyślnie Node przyjmuje założenie, że programista chce pracować z danymi tekstowymi w buforach. Dlatego też przekazanie ciągu tekstowego "121234869" konstruktorowi Buffer spowoduje alokację nowego obiektu Buffer wraz z zapisaną wartością w postaci ciągu tekstowego: var b = new Buffer("121234869"); console.log(b.length); 9 console.log(b);
W omawianym przykładzie wartością zwrotną jest dziewięciobajtowy obiekt Buffer. Wynika to z zapisu ciągu tekstowego w obiekcie Buffer za pomocą domyślnego, czytelnego dla człowieka kodowania tekstowego (UTF-8), w którym ciąg tekstowy zawiera po jednym bajcie dla każdego znaku. Node zawiera także funkcje pomocnicze przeznaczone do odczytu i zapisu danych binarnych (czytelnych dla komputera) w postaci liczb całkowitych. Są
one niezbędne do implementacji protokołów maszynowych, które wysyłają niezmodyfikowane typy danych (na przykład liczby całkowite, zmiennoprzecinkowe, o podwójnej precyzji itd.). Ponieważ w omawianym przykładzie chcemy przechowywać wartość liczbową, znacznie efektywniejszym rozwiązaniem będzie wykorzystanie funkcji pomocniczej o nazwie writeInt32LE() do zapisu liczby 121 234 869 jako czytelnych dla komputera danych binarnych (przyjęto założenie o użyciu kolejności bajtów little endian) w czterobajtowym obiekcie Buffer. Istnieją jeszcze inne odmiany funkcji pomocniczych Buffer: writeInt16LE() dla mniejszych liczb całkowitych; writeUInt32LE() dla wartości bez znaku; writeInt32BE() dla wartości stosującej kolejność bajtów big endian. Dostępnych funkcji jest znacznie więcej, a jeśli chcesz poznać wszystkie, to więcej informacji znajdziesz na stronie dokumentacji API obiektu Buffer (http://nodejs.org/docs/latest/api/buffer.html). W poniższym fragmencie kodu liczba została zapisana za pomocą binarnej funkcji pomocniczej writeInt32LE(): var b = new Buffer(4); b.writeInt32LE(121234869, 0); console.log(b.length); 4 console.log(b);
Przechowywanie w pamięci wartości w postaci binarnej liczby całkowitej zamiast ciągu tekstowego oznacza zmniejszenie wielkości danych o połowę — z 9 bajtów do 4. Na rysunku 13.2 pokazano schemat obu buforów i różnice między protokołem czytelnym dla człowieka (tekst) i czytelnym dla komputera (dane binarne).
Rysunek 13.2. Różnica na poziomie bajtów między liczbą 121 234 869 przedstawioną w postaci ciągu tekstowego a binarną liczbą całkowitą przedstawioną z zastosowaniem kolejności bajtów little endian
Niezależnie od używanego protokołu oferowana przez Node klasa w stanie obsłużyć poprawnie sposób przedstawienia danych.
Buffer
będzie
Kolejność bajtów. Pojęcie kolejności bajtów odnosi się do ich kolejności w sekwencji wielobajtowej. Kiedy bajty są w kolejności little endian, najmniej znaczący bajt (ang. Least Significant Byte, LSB) jest przechowywany jako pierwszy, a sekwencja bajtów jest odczytywana od prawej do lewej strony. Natomiast kolejność big endian oznacza, że jako pierwszy jest przechowywany najbardziej znaczący bajt (ang. Most Significant Byte, MSB), a sekwencja bajtów jest odczytywana od lewej do prawej strony. Node.js oferuje funkcje pomocnicze dla typów danych obsługujących obie kolejności, czyli little endian i big endian. Teraz możemy już wykorzystać wspomniane obiekty utworzenie serwera TCP i rozpoczęcie z nim pracy.
Buffer
w praktyce przez
13.2.2. Tworzenie serwera TCP Podstawowe API Node działa na niskim poziomie i udostępnia jedynie niezbędną infrastrukturę dla modułów zbudowanych na jego podstawie. Doskonałym przykładem jest tutaj moduł http, utworzony na podstawie modułu net i przeznaczony do implementacji protokołu HTTP. Inne protokoły, na przykład SMTP dla poczty elektronicznej lub FTP dla transferu plików, muszą być zaimplementowane również na bazie modułu net, ponieważ podstawowe API
Node nie implementuje żadnych protokołów działających na wysokim poziomie.
Zapis danych Moduł net oferuje interfejs niezmodyfikowanego gniazda TCP/IP do użycia w aplikacji. API przeznaczone do tworzenia serwera TCP jest bardzo podobne do używanego w trakcie tworzenia serwera HTTP: wywołujesz net.createServer() i podajesz nazwę funkcji wywołania zwrotnego, która będzie wykonana po każdym połączeniu. Podstawowa różnica związana z tworzeniem serwera TCP polega na tym, że funkcja wywołania zwrotnego pobiera tylko jeden argument (zwykle o nazwie socket) będący obiektem Socket. Natomiast podczas tworzenia serwera HTTP używane są argumenty req i res. Klasa Socket. Wymieniona klasa jest używana zarówno przez klienty, jak i serwer podczas stosowania modułu net w Node. To jest podklasa klasy Stream umożliwiająca odczyt (readable) i zapis (writeable). Oznacza to, że emituje zdarzenia data podczas odczytu danych wejściowych z gniazda, a także ma funkcje write() i end() przeznaczone do wysyłania danych wyjściowych. Spójrz teraz na obiekt net.Server oczekujący na połączenia, a następnie wykonujący funkcję wywołania zwrotnego. W omawianym przypadku logika zdefiniowana w funkcji wywołania zwrotnego powoduje po prostu przekazanie do gniazda komunikatu Witaj, świecie! i eleganckie zamknięcie połączenia: var net = require('net'); net.createServer(function (socket) { socket.write('Witaj, świecie!\r\n'); socket.end(); }).listen(1337); console.log('Serwer nasłuchiwanie na porcie 1337');
Uruchom serwer w celu przeprowadzenia pewnych testów: $ node server.js Serwer nasłuchiwanie na porcie 1337
Jeżeli za pomocą przeglądarki internetowej spróbujesz nawiązać połączenie z serwerem, próba zakończy się niepowodzeniem, ponieważ serwer nie potrafi się komunikować, używając protokołu HTTP, a jedynie przez niezmodyfikowany TCP. W celu nawiązania połączenia z serwerem i wyświetlenia komunikatu konieczne jest użycie odpowiedniego klienta TCP, na przykład w postaci polecenia netcat: $ netcat localhost 1337 Witaj, świecie
Doskonale! Teraz możemy wypróbować polecenie
telnet:
$ telnet localhost 1337 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Witaj, świecie! Connection closed by foreign host.
Polecenie telnet jest najczęściej używane w trybie interaktywnym, a więc oprócz komunikatu Witaj, świecie! wyświetla jeszcze inne, wygenerowane przez polecenie. Zgodnie z oczekiwaniami komunikat serwera jest wyświetlany tuż przed zamknięciem połączenia. Jak możesz się przekonać, zapis danych do gniazda jest całkiem łatwym zadaniem. Używasz wywołania write() oraz na końcu end(). Podczas przygotowywania odpowiedzi dla klienta omówione API celowo odpowiada API obiektu HTTP res.
Odczyt danych Serwery bardzo często stosują paradygmat żądanie-odpowiedź, w którym klient nawiązuje połączenie i natychmiast wysyła pewne żądanie. Serwer odczytuje żądanie, przetwarza odpowiedź, a następnie przekazuje ją do gniazda. Dokładnie w taki sposób działa protokół HTTP, a także większość innych protokołów sieciowych. Dlatego też bardzo ważne jest zrozumienie, jak dane są odczytywane. Jeżeli pamiętasz, jak można odczytać dane żądania z obiektu HTTP req, to odczyt danych z gniazda TCP nie powinien sprawić Ci żadnego problemu. Przy zachowaniu zgodności z możliwym do odczytu interfejsem Stream Twoje zadanie sprowadza się do nasłuchiwania zdarzeń data zawierających dane wejściowe, które zostały odczytane z gniazda: socket.on('data', function (data) { console.log('got "data"', data); });
Domyślnie nie jest stosowane żadne kodowanie znaków wobec socket, a więc argument data będzie egzemplarzem Buffer. Zwykle takie rozwiązanie jest oczekiwane (i dlatego zostało zdefiniowane jako domyślne). Jednak czasami znacznie wygodniejsze może się okazać wywołanie funkcji setEncoding() i nakazanie zdekodowania argumentu data na postać ciągu tekstowego, a nie bufora. Istnieje również możliwość nasłuchiwania zdarzenia end i tym samym ustalenia, że klient zamknął połączenie z gniazdem i nie należy już wysyłać kolejnych danych: socket.on('end', function () {
console.log('Gniazdo zostało zamknięte'); });
Bardzo łatwo można utworzyć klienta TCP, który będzie wyszukiwał ciąg tekstowy wersji danego serwera SSH przez po prostu oczekiwanie na pierwsze zdarzenie data: var net = require('net'); var socket = net.connect({ host: process.argv[2], port: 22 }); socket.setEncoding('utf8'); socket.once('data', function (chunk) { console.log('Wersja serwera SSH: %j', chunk.trim()); socket.end(); });
Teraz wypróbuj aplikację. Warto pamiętać o jednym: w tym uproszczonym przykładzie przyjęto założenie, że cały ciąg tekstowy zawierający informacje o wersji serwera zostanie dostarczony w jednym fragmencie. W większości przypadków takie rozwiązanie sprawdza się doskonale, ale dobry program powinien buforować dane wejściowe aż do napotkania znaku \n. Sprawdźmy, jakiej wersji serwera SSH używa serwis GitHub: $ node client.js github.com Wersja serwera SSH: "SSH-2.0-OpenSSH_5.5p1 Debian-6+squeeze1+github8"
Połączenie dwóch strumieni za pomocą socket.pipe()
Użycie funkcji pipe() (http://nodejs.org/api/stream.html#stream_readable_stream_pipe_destination_o w połączeniu z odczytywalnymi lub zapisywalnymi fragmentami obiektu Socket to również dobry pomysł. Jeżeli chcesz utworzyć prosty serwer TCP wyświetlający klientowi wszystko to, co zostało przekazane do serwera, wystarczy tylko pojedynczy wiersz kodu w funkcji wywołania zwrotnego: socket.pipe(socket);
Powyższy przykład pokazuje, że wystarczy tylko jeden wiersz kodu do zaimplementowania protokołu IETF Echo (http://tools.ietf.org/rfc/rfc862.txt). Co ważniejsze jednak, przykład pokazuje możliwość użycia funkcji pipe() w celu przekazywania danych do oraz z obiektu socket. Oczywiście z reguły stosowane będą znacznie pożyteczniejsze egzemplarze strumieni, na przykład systemu plików lub gzip.
Obsługa nieeleganckiego zamknięcia połączenia Ostatnią rzeczą, którą trzeba uwzględnić w serwerze TCP, jest konieczność przewidywania klientów zamykających połączenie, ale nie zamykających prawidłowo gniazda. W przypadku polecenia netcat tego rodzaju sytuacja
wystąpi po naciśnięciu klawiszy Ctrl+C w celu zamknięcia procesu zamiast naciśnięcia Ctrl+D w celu eleganckiego zamknięcia połączenia. Aby wykryć wspomnianą sytuację, należy nasłuchiwać zdarzeń close: socket.on('close', function () { console.log('Klient został rozłączony'); });
Jeżeli po zamknięciu połączenia trzeba będzie zamknąć jeszcze gniazdo, wtedy odpowiednie operacje należy przeprowadzać z poziomu zdarzenia close, a nie end, ponieważ drugie z wymienionych nie zostanie wywołane w przypadku nieeleganckiego zamknięcia połączenia.
Zebranie wszystkiego w całość Teraz zbierzemy wszystkie zdarzenia i utworzymy prosty serwer typu echo, który wyświetla w terminalu komunikaty po wystąpieniu różnych zdarzeń. Kod serwera został przedstawiony w listingu 13.5. Listing 13.5. Prosty serwer TCP, który klientowi wyświetla z powrotem wszystkie otrzymane od niego dane var net = require('net'); net.createServer(function (socket) { console.log('Nawiązano połączenie z gniazdem!'); socket.on('data', function (data) { Zdarzenie data może wystąpić wielokrotnie. console.log('Zdarzenie "data"', data); }); socket.on('end', function () { console.log('Zdarzenie "end"'); Zdarzenie end może wystąpić tylko raz dla gniazda. }); socket.on('close', function () { Zdarzenie close również może wystąpić tylko raz dla gniazda. console.log('Zdarzenie "close"'); }); socket.on('error', function (e) { Zdefiniowanie obsługi błędów, aby uniknąć niezgłoszonych wyjątków. console.log('Zdarzenie "error"', e); }); socket.pipe(socket); }).listen(1337);
Uruchom serwer i nawiąż z nim połączenie za pomocą polecenia netcat lub telnet, a następnie wypróbuj serwer. Kiedy w aplikacji klienta zaczniesz
wpisywać znaki z klawiatury, powinieneś widzieć wywołania console.log() dotyczące zdarzeń, o których komunikaty są przekazywane do standardowego wyjścia serwera. Skoro potrafisz już tworzyć działające na niskim poziomie serwery TCP w Node, to prawdopodobnie zastanawiasz się, jak w Node przygotować program klienta przeznaczony do interakcji ze wspomnianymi serwerami. Teraz zajmiemy się więc programem klienta.
13.2.3. Tworzenie klienta TCP Node to nie tylko oprogramowanie przeznaczone do tworzenia serwerów. Utworzenie programu klienta sieciowego w Node również jest łatwe i użyteczne. Podczas tworzenia niezmodyfikowanych połączeń z serwerem TCP kluczowe znaczenie ma funkcja net.connect(). Wymieniona funkcja akceptuje wartości host i port, a zwraca egzemplarz socket. Obiekt socket zwrócony przez net.connect() jest odłączony od serwera, a więc najczęściej należy nasłuchiwać zdarzeń connect przed rozpoczęciem wykonywania jakiejkolwiek operacji z gniazdem: var net = require('net'); var socket = net.connect({ port: 1337, host: 'localhost' }); socket.on('connect', function () { // Rozpoczęcie tworzenia "żądania". socket.write('HELO local.domain.name\r\n'); ... });
Kiedy egzemplarz socket nawiąże połączenie z serwerem, wtedy zaczyna zachowywać się jak egzemplarze socket, z którymi masz do czynienia w funkcji wywołania zwrotnego net.Server. Przystępujemy teraz do przygotowania prostej replikacji polecenia netcat. Kod tworzonego klienta przedstawiono w listingu 13.6. Ogólnie rzecz biorąc, program nawiązuje połączenie ze wskazanym zdalnym serwerem i potokuje standardowe wejście z programu do gniazda, a następnie potokuje odpowiedź gniazda do standardowego wyjścia programu. Listing 13.6. Podstawowa replika polecenia netcat przygotowana za pomocą Node var net = require('net'); var host = process.argv[2]; var port = Number(process.argv[3]);ó Przetworzenie argumentów host i port podanych w powłoce. var socket = net.connect(port, host); Utworzenie egzemplarza socket i rozpoczęcie
procedury nawiązywania połączenia z serwerem. socket.on('connect', function () { Obsługa zdarzenia connect po nawiązaniu połączenia z serwerem. process.stdin.pipe(socket); Potokowanie standardowego wejścia procesu do gniazda. socket.pipe(process.stdout); Potokowanie danych gniazda do standardowego wyjścia procesu. process.stdin.resume(); Wywołanie resume() dla danych wejściowych, aby rozpocząć odczyt danych. }); socket.on('end', function () { Wstrzymanie danych ze standardowego wejścia po wystąpieniu zdarzenia end. process.stdin.pause(); });
Przygotowanego klienta można wykorzystać do nawiązywania połączeń z utworzonymi wcześniej serwerami TCP. Jeżeli jesteś fanem „Gwiezdnych wojen”, spróbuj uruchomić przedstawioną replikację polecenia netcat wraz z poniższym argumentem, aby otrzymać efekt specjalny: $ node netcat.js towel.blinkenlights.nl 23
Usiądź wygodnie i ciesz się danymi wyjściowymi, które pokazano na rysunku 13.3. Zasłużyłeś na przerwę.
Rysunek 13.3. Nawiązanie połączenia z serwerem ASCII Star Wars za pomocą skryptu netcat.js
To już wszystko w zakresie tworzenia za pomocą Node działających na niskim
poziomie serwerów TCP i klientów. Moduł net oferuje proste, choć obszerne API, natomiast klasa Socket stosuje zgodnie z oczekiwaniami odczytywalny i zapisywalny interfejs Stream. W zasadzie moduł net to próbka podstawowych możliwości oferowanych przez Node. Zmienimy narzędzia raz jeszcze i przyjrzymy się, w jaki sposób podstawowe API Node pozwala na współpracę ze środowiskiem procesu oraz na zbieranie informacji dotyczących środowiska uruchomieniowego i systemu operacyjnego.
13.3. Narzędzia przeznaczone do pracy z systemem operacyjnym Bardzo często zachodzi potrzeba współpracy ze środowiskiem, w którym działa Node. Może się to wiązać na przykład z koniecznością sprawdzenia zmiennych środowiskowych w celu włączenia rejestracji danych podczas procesu debugowania. Kolejny przykład to implementacja sterownika dżojstika w systemie Linux przy użyciu działających na niskim poziomie funkcji modułu fs przeznaczonych do pracy z /dev/js0 (plik urządzenia dla dżojstika). Jeszcze inny przykład to konieczność uruchomienia procesu potomnego, takiego jak php, do kompilacji starszego skryptu PHP. Wszystkie wymienione rodzaje operacji wymagają użycia pewnego podstawowego API Node. Oto komponenty, które zostaną omówione w tym podrozdziale: Obiekt globalny o nazwie process. Zawiera informacje o bieżącym procesie, takie jak użyte argumenty oraz aktualnie ustawione zmienne środowiskowe. Moduł fs. Zawiera działające na wysokim poziomie klasy ReadStream i WriteStream, które powinieneś już znać. Ponadto obejmuje działające na niskim poziomie funkcje, które zostaną omówione w tym podrozdziale. Moduł child_process. Zawiera działające zarówno na wysokim, jak i niskim poziomie interfejsy przeznaczone do tworzenia procesów potomnych oraz obsługi specjalnego sposobu tworzenia egzemplarzy node wraz z dwukierunkowym kanałem przekazywania informacji. Obiekt process jest jednym z API najczęściej wykorzystywanych przez programy i dlatego zapoznamy się z nim na początku.
13.3.1. Obiekt process, czyli globalny wzorzec
Singleton Każdy proces Node posiada globalny obiekt process współdzielony przez wszystkie moduły. Wymieniony obiekt zawiera użyteczne informacje o procesie i kontekście, w którym działa. Na przykład argumenty podane Node podczas uruchamiania aktualnego skryptu są dostępne jako process.argv, natomiast zmienne środowiskowe można pobrać i ustawić za pomocą process.env. Jednak najbardziej interesującą cechą obiektu process jest to, że stanowi on egzemplarz EventEmitter i emituje zdarzenia specjalne, takie jak exit i uncaughtException. Obiekt process ma wiele możliwości, a pewne API nieomówione w tym punkcie zostanie przedstawione w dalszej części rozdziału. W tym punkcie koncentrujemy się na następujących zagadnieniach: Użycie process.env do pobierania i ustawiania zmiennych środowiskowych. Nasłuchiwanie zdarzeń specjalnych emitowanych przez obiekt process, na przykład exit i uncaughtException. Nasłuchiwanie emitowanych przez obiekt process zdarzeń sygnałów, na przykład SIGUSR2 i SIGKILL.
Użycie process.env do pobierania i ustawiania zmiennych środowiskowych Zmienne środowiskowe to doskonałe rozwiązanie pozwalające na zmianę sposobu działania programu lub modułu. Tego rodzaju zmienne można wykorzystać na przykład do skonfigurowania serwera i wskazania portu, na którym ma on nasłuchiwać. Z kolei system operacyjny może ustawić zmienną TMPDIR w celu określenia lokalizacji przeznaczonej na generowane przez program pliki tymczasowe, które później mogą być usunięte. Zmienne środowiskowe. Być może nie znasz jeszcze zastosowania zmiennych środowiskowych, powinieneś jednak wiedzieć, że to pary typu klucz--wartość, które mogą być używane przez dowolny proces do zmiany sposobu jego zachowania. Wszystkie systemy operacyjne używają zmiennej środowiskowej PATH do zdefiniowania listy ścieżek dostępu sprawdzanych podczas wyszukiwania programu po jego nazwie (na przykład pełna ścieżka dostępu polecenia ls to /bin/ls). Przyjmujemy założenie, że chcesz włączyć rejestrację danych w trybie debugowania podczas tworzenia modułu lub usuwania z niego błędów. Wspomniana rejestracja ma być niedostępna w trakcie zwykłego użycia modułu, ponieważ może irytować jego użytkowników. Doskonałym rozwiązaniem będzie użycie zmiennych środowiskowych. Można ustalić, czy została ustawiona
z mi e nna DEBUG. Odbywa się przedstawiono w listingu 13.7.
to
przez
sprawdzenie
process.env.DEBUG,
jak
Listing 13.7. Zdefiniowanie funkcji debug() na podstawie zmiennej środowiskowej DEBUG var debug; if (process.env.DEBUG) { Zdefiniowanie działania funkcji na podstawie wartości process.env.DEBUG. debug = function (data) { console.error(data); Gdy zmienna DEBUG jest ustawiona, funkcja debug() będzie przekazywać argument do standardowego wyjścia błędów. }; } else { debug = function () {}; Gdy zmienna DEBUG nie jest ustawiona, funkcja debug() będzie pusta i nie wykona żadnej operacji. } debug('To jest wywołanie debugujące'); Wywołanie funkcji debug() w różnych miejscach kodu. console.log('Witaj, świecie!'); debug('To jest inne wywołanie debugujące');
Jeżeli przedstawiony skrypt zostanie uruchomiony w zwykły sposób (bez ustawienia zmiennej środowiskowej process.env.DEBUG), wówczas wywołanie debug() nie da żadnego efektu, ponieważ wywoływana jest pusta funkcja: $ node debug-mode.js Witaj, świecie!
Aby przetestować tryb debugowania, konieczne jest ustawienie zmiennej środowiskowej process.env.DEBUG. Najłatwiejszym sposobem jest dołączenie DEBUG=1 do polecenia uruchamiającego egzemplarz Node. W trybie debugowania wywołanie funkcji debug() spowoduje nie tylko wygenerowanie standardowych danych wyjściowych, ale również umieszczenie odpowiedniego komunikatu w konsoli. To bardzo użyteczne rozwiązanie pozwalające na zbieranie danych diagnostycznych podczas rozwiązywania problemów związanych z kodem: $ DEBUG=1 node debug-mode.js To jest wywołanie debugujące Witaj, świecie! To jest inne wywołanie debugujące
Opracowany przez T.J. Holowaychuka moduł debug (https://github.com/visionmedia/debug) hermetyzuje dokładnie tę samą funkcjonalność, a ponadto zawiera wiele funkcji dodatkowych. Jeżeli lubisz przedstawioną tutaj technikę debugowania, zdecydowanie powinieneś zapoznać się z modułem debug.
Zdarzenia specjalne emitowane przez obiekt proces Istnieją dwa zdarzenia specjalne emitowane przez obiekt process: exit. To zdarzenie jest emitowane tuż przed zakończeniem działania przez proces. uncaughtException. To zdarzenie jest emitowane za każdym razem, gdy wystąpi nieobsłużony błąd. Zdarzenie exit ma istotne znaczenie dla każdej aplikacji, która musi wykonać jakiekolwiek operacje przed zakończeniem działania programu, na przykład usunąć obiekt lub umieścić w konsoli ostateczny komunikat. Warto pamiętać, że zdarzenie exit jest emitowane już po zatrzymaniu pętli zdarzeń, a więc nie masz możliwości uruchomienia jakiegokolwiek zadania asynchronicznego w trakcie zdarzenia exit. Kod wyjścia jest przekazywany jako pierwszy argument, kod 0 oznacza sukces. Utworzymy teraz skrypt, który nasłuchuje zdarzenia komunikat Koniec pracy...:
exit,
a następnie generuje
process.on('exit', function (code) { console.log('Koniec pracy...'); });
Inne zdarzenie specjalne emitowane przez obiekt process to uncaughtException. W perfekcyjnym programie nigdy nie wystąpią nieobsłużone wyjątki, ale w rzeczywistości lepiej być przygotowanym na taką ewentualność, niż później żałować. Jedynym argumentem przekazywanym do zdarzenia uncaughtException jest nieprzechwycony obiekt Error. Kiedy nie ma innych obiektów nasłuchujących zdarzeń „błędu”, wszelkie nieprzechwycone błędy doprowadzą do awarii procesu (to jest zachowanie domyślne w większości aplikacji). Jednak istnienie co najmniej jednego obiektu nasłuchującego daje możliwość podjęcia dowolnego działania po przechwyceniu błędu. Node nie zakończy automatycznie działania, choć takie rozwiązanie jest konieczne we własnych wywołaniach zwrotnych. Dokumentacja Node.js wyraźnie ostrzega, że każde użycie zdarzenia uncaughtException powinno zawierać wywołanie process.exit() w wywołaniu zwrotnym, ponieważ w przeciwnym razie aplikacja pozostanie w niezdefiniowanym stanie, co jest złym rozwiązaniem. Przygotujemy teraz obiekt nasłuchujący zdarzeń uncaughtException, a następnie zgłosimy nieprzechwycony błąd, aby zobaczyć, jak to wygląda w praktyce: process.on('uncaughtException', function (err) { console.error('Wystąpił nieprzechwycony wyjątek:', err.message);
process.exit(1); }); throw new Error('Nieprzechwycony wyjątek');
Po wystąpieniu nieoczekiwanego błędu kod będzie w stanie go przechwycić i przeprowadzić odpowiednie operacje przed zakończeniem działania procesu.
Przechwytywanie sygnałów wysyłanych procesowi W systemie UNIX wprowadzono koncepcję sygnałów, które stanowią podstawową formę komunikacji międzyprocesowej (ang. Interprocess Communication, IPC). Wspomniane sygnały są bardzo proste i pozwalają na użycie jedynie pewnego stałego zestawu nazw, a ponadto są przekazywane bez argumentów. Node posiada zdefiniowane domyślne zachowanie dla kilku wymienionych poniżej sygnałów: SIGINT. Sygnał wysyłany przez powłokę po naciśnięciu klawiszy Ctrl+C. Domyślne zachowanie Node powoduje zakończenie działania procesu. To zachowanie można zmienić za pomocą pojedynczego nasłuchującego sygnału SIGINT w obiekcie process. SIGUSR1. Po otrzymaniu tego sygnału Node „wejdzie” do wbudowanego debugera. SIGWINCH. Ten sygnał jest wysyłany przez powłokę po zmianie wielkości okna terminalu. Node zeruje wartości process.stdout.rows i process.stdout.columns i emituje zdarzenie resize po otrzymaniu omawianego sygnału. Są to trzy sygnały domyślne obsługiwane przez Node. Istnieje możliwość nasłuchiwania w obiekcie process dowolnego z omówionych sygnałów i wykonywania funkcji wywołania zwrotnego. Przyjmujemy założenie, że utworzyłeś serwer, ale po naciśnięciu klawiszy Ctrl+C następuje zakończenie jego działania. Nie jest to eleganckie, a dodatkowo wszelkie połączenia oczekujące zostaną po prostu usunięte. Rozwiązaniem jest przechwycenie sygnału SIGINT, wstrzymanie akceptowania nowych połączeń, umożliwienie zakończenia istniejących i dopiero wówczas zakończenie działania procesu. Odbywa się to przez nasłuchiwanie process.on('SIGINT', ...). Nazwa emitowanego zdarzenia jest taka sama jak nazwa sygnału: process.on('SIGINT', function () { console.log('Przechwycono naciśnięcie Ctrl+C!'); server.close();
});
Teraz po naciśnięciu kombinacji Ctrl+C na klawiaturze sygnał SIGINT zostanie wysłany z powłoki do procesu Node i zamiast natychmiastowego zakończenia działania procesu spowoduje wykonanie zdefiniowanego wywołania zwrotnego. Ponieważ domyślne zachowanie w większości aplikacji to zakończenie działania procesu, zwykle dobrym pomysłem jest zrobienie tego samego we własnej procedurze obsługi sygnału SIGINT po przeprowadzeniu wszelkich niezbędnych operacji. W omawianym przykładzie wystarczające jest wstrzymanie akceptacji nowych połączeń przez serwer. Pomimo braku prawidłowych sygnałów takie rozwiązanie działa również w Windows, ponieważ Node obsługuje odpowiednie akcje Windows i symuluje sztuczne sygnały w Node. Tę samą technikę można zastosować do przechwycenia dowolnego sygnału systemu UNIX wysyłanego do procesu Node. Lista sygnałów UNIX została wymieniona w artykule Wikipedii na stronie http://en.wikipedia.org/wiki/Unix_signal#POSIX_signals. Niestety, ogólnie rzecz biorąc, poza kilkoma symulowanymi (SIGINT, SIGBREAK, SIGHUP i SIGWINCH) sygnały nie działają w systemie Windows.
13.3.2. Użycie modułu filesystem M o duł fs zapewnia funkcje niezbędne do współpracy z systemem plików komputera, w którym uruchomiono Node. Większość funkcji to rodzaj mapowania typu „jeden do jednego” dla ich odpowiedników w języku C, ale istnieją również działające na wysokim poziomie abstrakcje, takie jak fs.readFile(), fs.writeFile(), klasy fs.ReadStream i fs.WriteStream, które zostały opracowane na bazie open(), read(), write() i close(). Niemal wszystkie funkcje działające na niskim poziomie są używane identycznie jak ich odpowiedniki w języku C. W rzeczywistości większość dokumentacji Node odsyła Cię do odpowiednich stron podręcznika systemowego man wyjaśniających działanie dopasowanych funkcji C. Funkcje działające na niskim poziomie można bardzo łatwo zidentyfikować, ponieważ zawsze mają synchroniczny odpowiednik. Na przykład fs.stat() i fs.statSync() to działające na niskim poziomie odpowiedniki funkcji języka C o nazwie stat(). Funkcje synchroniczne w Node.js. Jak już wiesz, API Node to przede wszystkim funkcje asynchroniczne, które nigdy nie blokują pętli zdarzeń. Dlaczego więc dołączone są synchroniczne wersje funkcji systemu plików? Odpowiedź jest prosta: funkcja require() w Node jest synchroniczna i została zaimplementowana za pomocą funkcji modułu fs, a więc synchroniczne odpowiedniki są niezbędne. Funkcje synchroniczne w Node powinny być używane jedynie podczas uruchamiania lub początkowego wczytania modułu, a nigdy później.
Teraz zapoznamy Cię z przykładami pracy z systemem plików.
Przenoszenie pliku Na pozór prostym i dość często przeprowadzanym zadaniem podczas pracy z systemem plików jest przenoszenie pliku między katalogami. Na platformie UNIX do tego celu używa się polecenia mv, natomiast w Windows — polecenia move. Wydaje się, że przeprowadzenie tej operacji w Node powinno być proste. Cóż, jeśli przejrzysz dokumentację modułu fs (http://nodejs.org/api/fs.html), to zauważysz brak funkcji w stylu fs.move(). Jednak istnieje funkcja fs.rename(), która tak naprawdę wykonuje to samo zadanie. Doskonale! Nie tak szybko… Funkcja fs.rename() jest mapowana bezpośrednio na funkcję C o nazwie rename(). Jednak wadą wymienionej funkcji jest to, że nie działa między dwoma różnymi urządzeniami fizycznymi (na przykład między dwoma dyskami twardymi). Dlatego też poniższy fragment kodu nie działa zgodnie z oczekiwaniami i spowoduje zgłoszenie błędu typu EXDEV: fs.rename('C:\\hello.txt', 'D:\\hello.txt', function (err) { // err.code === 'EXDEV' });
Co można zrobić w takiej sytuacji? Nadal można utworzyć nowy plik na dysku D:\, odczytać zawartość pliku z dysku C:\ i skopiować ją przez sieć. Można przygotować zoptymalizowaną funkcję move() wywołującą fs.rename(), gdy istnieje taka możliwość, i kopiującą plik między urządzeniami przy użyciu fs.ReadStream i fs.WriteStream. Przykładową implementację funkcji move() przedstawiono w listingu 13.8. Listing 13.8. Funkcja move(), która zmienia nazwę pliku (o ile to możliwe) lub go kopiuje var fs = require('fs'); module.exports = function move (oldPath, newPath, callback) { fs.rename(oldPath, newPath, function (err) { Wywołanie fs.rename() w nadziei, że zadziała prawidłowo. if (err) { if (err.code === 'EXDEV') { W przypadku wystąpienia błędu EXDEV trzeba zastosować kopiowanie. copy(); } else { callback(err); Niepowodzenie wywołania i zgłoszenie tego faktu wywołującemu, jeśli wystąpi inny rodzaj błędu. } return; }
callback(); Jeżeli funkcja fs.rename() zadziałała prawidłowo, w tym momencie zadanie jest wykonane. }); function copy() { var readStream = fs.createReadStream(oldPath); Odczyt pliku źródłowego i jego potokowanie do pliku docelowego. var writeStream = fs.createWriteStream(newPath); readStream.on('error', callback); writeStream.on('error', callback); readStream.on('close', function () { fs.unlink(oldPath, callback); Usunięcie pliku źródłowego po jego wcześniejszym skopiowaniu. }); readStream.pipe(writeStream); } }
Moduł można przetestować bezpośrednio w interfejsie REPL Node, na przykład: $ node > var move = require('./copy') > move('copy.js', 'copy.js.bak', function (err) { if (err) throw err })
Zwróć uwagę, że funkcja copy() działa jedynie z plikami, a nie z katalogami. Aby działała również z katalogami, konieczne jest sprawdzenie w pierwszej kolejności, czy podana ścieżka dostępu prowadzi do katalogu. Jeśli tak, wówczas wywoływane są funkcje fs.readdir() i fs.mkdir(). Obsługę katalogów przez funkcję copy() możesz teraz zaimplementować samodzielnie. Kody błędów modułu fs. Moduł fs zwraca standardowe nazwy systemu UNIX dla kodów błędów systemu plików (http://www.gnu.org/software/libc/manual/html_node/Error-Codes.html), a więc znajomość wspomnianych nazw jest wymagana. Te nazwy są normalizowane przez bibliotekę libuv nawet w Windows, a więc aplikacja musi sprawdzać za każdym razem tylko jeden kod błędu. Zgodnie z informacjami zamieszczonymi w dokumentacji GNU, błąd EXDEV występuje podczas wykrycia „próby utworzenia nieprawidłowego dowiązania między systemami plików”.
Monitorowanie katalogu lub pliku pod kątem zmian Funkcja fs.watchFile() jest dostępna od dawna. Na pewnych platformach jej działanie jest kosztowne, ponieważ stosuje próbkowanie w celu sprawdzenia, czy plik został zmieniony. Oznacza to wywołanie stat() dla pliku, odczekanie krótkiego zakresu czasu, następnie ponowne wywołanie stat(). Cały proces jest przeprowadzany nieustannie w pętli. Po wykryciu zmiany w pliku następuje
wywołanie zdefiniowanej funkcji. Przyjmujemy założenie, że tworzony jest moduł rejestrujący zmiany wprowadzane w pliku dziennika zdarzeń system.log. Konieczne jest zdefiniowanie funkcji wywołania zwrotnego, która będzie wykonywana po wykryciu modyfikacji wymienionego pliku: var fs = require('fs'); fs.watchFile('/var/log/system.log', function (curr, prev) { if (curr.mtime.getTime() !== prev.mtime.getTime()) { console.log('Plik "system.log" został zmodyfikowany'); } });
Zmienne curr i prev to bieżący i poprzedni obiekt fs.Stat — powinny mieć różne znaczniki czasu dla tego samego dołączonego do nich pliku. W omawianym przykładzie porównywane są wartości mtime, ponieważ moduł ma informować jedynie o modyfikacji pliku, a nie o uzyskaniu do niego dostępu. F unk c j a fs.watch() została wprowadzona w Node v0.6. Jak wcześniej wspomniano, jest bardziej zoptymalizowana niż fs.watchFile(), ponieważ podczas monitorowania plików używa rodzimego dla platformy API powiadomień o zmianie pliku. Dlatego też wymieniona funkcja może monitorować katalog pod kątem zmian w dowolnym ze znajdujących się w nim plików. W praktyce funkcja fs.watch() jest mniej niezawodna niż fs.watchFile(), co wynika z różnic między stosowanymi przez poszczególne platformy mechanizmami monitorowania plików. Na przykład parametr filename nie jest zgłaszany w systemie OS X podczas monitorowania katalogu, a firma Apple może to zmienić w kolejnych wydaniach systemu OS X. W dokumentacji Node znajdziesz listę tego rodzaju zastrzeżeń (http://nodejs.org/api/fs.html#fs_caveats).
Użycie opracowanych przez społeczność modułów fstream i filed Jak się przekonałeś, moduł fs — podobnie jak całe podstawowe API Node — działa jedynie na niskim poziomie. Oznacza to sporo miejsca na innowacje i tworzenie abstrakcji na jego podstawie. Kolekcja aktywnych modułów Node w repozytorium npm zwiększa się każdego dnia i jak możesz się domyślać, istnieją również pewne doskonałe rozwiązania zbudowane w oparciu o moduł fs. Na przykład opracowany przez Isaaca Schluetera moduł fstream (https://github.com/isaacs/fstream) to jeden z podstawowych komponentów samego menedżera npm. Wymieniony moduł jest interesujący, ponieważ zaistniał jako część menedżera npm, a następnie został wyodrębniony do postaci samodzielnego modułu, ponieważ jego funkcjonalność ogólnego przeznaczenia okazała się użyteczna dla wielu różnego rodzaju narzędzi powłoki i skryptów
przeznaczonych dla administratorów systemów. Jedną z fantastycznych funkcji m o d u ł u fstream jest bezproblemowa obsługa uprawnień i dowiązań symbolicznych, czym moduł zajmuje się domyślnie podczas kopiowania plików i katalogów. Dzięki modułowi fstream można wykonać operację odpowiadającą wywołaniu cp rp katalog_źródłowy katalog_docelowy (rekurencyjne kopiowanie katalogu wraz z zawartością, a także transfer właściciela i uprawnień) przez po prostu potokowanie egzemplarza Reader do egzemplarza Writer. W przedstawionym poniżej fragmencie kodu wykorzystano również możliwości modułu fstream w zakresie filtrowania w celu warunkowego wykluczania plików na podstawie funkcji wywołania zwrotnego: fstream .Reader("ścieżka/do/katalogu") .pipe(fstream.Writer({ path: "ścieżka/do/innego/katalogu", filter: isValid ) // Sprawdzenie, czy plik jest przeznaczony do zapisania // oraz czy ewentualnie może być nadpisany. function isValid () { // Zignorowanie plików tymczasowych edytorów tekstu, na przykład TextMate. return this.path[this.path.length - 1] !== '~'; }
Opracowany przez Mikeala Rogersa moduł filed (https://github.com/mikeal/filed) to inny ważny moduł, ponieważ Mikeal jest również autorem niezwykle popularnego modułu request. Wymienione moduły spopularyzowały nowy rodzaj kontroli przepływu egzemplarzy Stream: nasłuchiwanie zdarzenia pipe i podjęcie odpowiedniego działania na podstawie potokowanych danych (lub miejsca ich potokowania). Aby poznać potężne możliwości związane z przedstawionym podejściem, spójrz, jak za pomocą modułu filed i jednego wiersza kodu można zmienić zwykły serwer HTTP na w pełni wyposażony serwer plików statycznych: http.createServer(function (req, res) { req.pipe(filed('ścieżka/do/plików/statycznych')).pipe(res); });
Powyższy kod zajmuje się wysłaniem nagłówka Content-Length wraz z odpowiednimi nagłówkami dotyczącymi buforowania. W przypadku gdy przeglądarka internetowa posiada buforowaną wersję pliku, na żądanie HTTP moduł filed udziela odpowiedzi o kodzie 304 (niezmodyfikowany), pomijając kroki związane z otworzeniem i odczytem pliku z dysku. To są te rodzaje optymalizacji, które działają ze zdarzeniem pipe, ponieważ egzemplarz filed ma
dostęp do obiektów req i res żądania HTTP. W ten sposób przedstawiliśmy dwa przykłady opracowanych przez społeczność dobrych modułów rozszerzających moduł bazowy fs o nowe możliwości i udostępniających piękne API. Warto pamiętać, że modułów jest znacznie więcej. Polecenie npm search do doskonały sposób na wyszukiwanie opublikowanych modułów przeznaczonych do wykonywania określonego zadania. Przyjmujemy założenie, że chcesz znaleźć jeszcze inny moduł ułatwiający proces kopiowania plików z jednej lokalizacji do drugiej. Wydanie polecenia npm search copy powinno przynieść wiele użytecznych wyników. Kiedy znajdziesz opublikowany moduł wyglądający interesująco, zawsze możesz wydać polecenie npm info nazwa-modułu, aby uzyskać więcej informacji na temat modułu, między innymi jego opis, adres strony domowej i opublikowane wersje. Pamiętaj o jednym: w przypadku danego zadania istnieje duże prawdopodobieństwo, że ktoś już próbował rozwiązać problem za pomocą modułu npm, i dlatego zawsze przed przystąpieniem do tworzenia własnego kodu od podstaw sprawdź, czy nie istnieje odpowiedni moduł w npm.
13.3.3. Tworzenie procesów zewnętrznych Node oferuje moduł child_process przeznaczony do tworzenia procesów potomnych w ramach serwera Node lub skryptu. Istnieją dwa API przeznaczone do wymienionego celu: działające na wysokim poziomie exec() oraz działające na niskim poziomie spawn(). W zależności od potrzeb każde z wymienionych API może być odpowiednie. Ponadto mamy jeszcze udostępniany przez samo Node specjalny sposób tworzenia procesu potomnego za pomocą wbudowanego kanału IPC o nazwie fork(). Wszystkie te funkcje są przeznaczone do użycia w różnych przypadkach: cp.exec() — działające na wysokim poziomie API przeznaczone do tworzenia poleceń i buforowania wyniku operacji w wywołaniu zwrotnym. cp.spawn() — działające na niskim poziomie API przeznaczone do tworzenia pojedynczych poleceń w obiekcie ChildProcess. cp.form() — specjalny sposób tworzenia dodatkowego procesu Node za pomocą wbudowanego kanału IPC. Powyższe API przeanalizujemy po kolei. Wady i zalety procesów potomnych. Istnieją wady i zalety użycia procesów potomnych. Oczywistą wadą jest konieczność instalacji w komputerze użytkownika uruchamianego programu, co czyni go zależnym od aplikacji.
Alternatywne rozwiązanie polega na użyciu języka JavaScript do wykonania zadań przeznaczonych dla procesu potomnego. Dobrym przykładem jest tutaj polecenie npm, które początkowo używało systemowego polecenia tar do rozpakowywania pakietów Node. Takie rozwiązanie powodowało problemy wynikające z niezgodności między wersjami polecenia tar, a ponadto rzadko się zdarzało, aby w systemie Windows było zainstalowane polecenie tar. Wymienione czynniki spowodowały opracowanie narzędzia node-tar (https://github.com/isaacs/node-tar), które całkowicie utworzono w języku JavaScript. Nie są więc używane żadne procesy potomne. Z drugiej strony, użycie aplikacji zewnętrznej pozwala programiście na wykorzystanie użytecznego programu utworzonego w zupełnie innym języku programowania. Na przykład gm (http://aheckmann.github.io/gm/) to moduł używający potężnych bibliotek GraphicMagick i ImageMagick do przeprowadzania wszelkich operacji na obrazach oraz ich konwersji w aplikacji Node.
Buforowanie za pomocą cp.exec() wyników działania polecenia Działające na wysokim poziomie API cp.exec() jest użyteczne podczas wywoływania polecenia — programistę interesuje jedynie wynik końcowy, a nie kwestie związane z dostępem do danych z poziomu potomnych strumieni standardowego wejścia, gdy się pojawiają. To API pozwala na podanie pełnych sekwencji poleceń, między innymi zawierających wiele procesów potokowanych jeden do drugiego. Jednym z dobrych przykładów użycia API cp.exec() jest akceptowanie przeznaczonych do wykonania poleceń użytkownika. Przyjmujemy założenie o tworzeniu bota IRC. Polecenie ma zostać wykonane, gdy użytkownik wprowadzi tekst zaczynający się od kropki. Na przykład jeśli jako wiadomość IRC użytkownik wpisze .ls, wówczas nastąpi wykonanie polecenia ls i wyświetlenie danych wyjściowych w pokoju IRC. Jak pokazano w listingu 13.9, konieczne jest ustawienie pewnej opcji timeout, aby nigdy nie kończące się procesy były automatycznie zamykane po upłynięciu pewnego czasu. Listing 13.9. Użycie cp.exec() do uruchamiania za pomocą IRC bot poleceń podawanych przez użytkownika var cp = require('child_process'); room.on('message', function (user, message) { Obiekt room przedstawia połączenie z pokojem IRC (z pewnego teoretycznego modułu IRC). if (message[0] === '.') { Zdarzenie message jest emitowane przez każdą wiadomość IRC wysyłaną do pokoju. var command = message.substring(1); cp.exec(command, { timeout: 15000 }, Sprawdzenie, czy treść wiadomości rozpoczyna się od kropki.
function (err, stdout, stderr) { if (err) { Utworzenie procesu potomnego i buforowanie w wywołaniu zwrotnym wyniku przez Node. Czas upływu ważności wynosi 15 sekund. room.say( 'Błąd podczas wykonywania polecenia "' + command + '": ' + err.message ); room.say(stderr); } else { room.say('Wykonywanie polecenia zakończone: ' + command); room.say(stdout); } } ); } });
Repozytorium npm zawiera jeszcze inne dobre moduły implementujące protokół IRC. Jeżeli faktycznie chcesz utworzyć bota IRC, powinieneś skorzystać z jednego z istniejących modułów (do popularnych zaliczają się irc i irc-js). Kiedy zachodzi potrzeba buforowania danych wyjściowych polecenia, ale chciałbyś użyć Node do automatycznego neutralizowania argumentów, wówczas możesz skorzystać z funkcji execFile(). Wymieniona funkcja pobiera cztery argumenty zamiast trzech. Przekazujesz jej pliki wykonywalne przeznaczone do uruchomienia oraz tablicę argumentów, z którymi mają być wykonane. Takie rozwiązanie jest użyteczne podczas przyrostowego tworzenia argumentów przeznaczonych do użycia przez proces potomny: cp.execFile('ls', [ '-l', process.cwd() ], function (err, stdout, stderr) { if (err) throw err; console.error(stdout); });
Tworzenie poleceń za pomocą interfejsu Stream i cp.spawn() Oferowane przez Node i działające na niskim poziomie API przeznaczone do tworzenia procesów potomnych to cp.spawn(). Ta funkcja różni się od cp.exec(), ponieważ zwraca obiekt ChildProcess, z którym można współpracować. Zamiast przekazywać cp.spawn() pojedynczą funkcję wywołania zwrotnego, gdy proces potomny zakończy działanie, cp.spawn() pozwala na interakcje z poszczególnymi strumieniami standardowego wejścia procesu potomnego. Najprostszy sposób użycia
cp.spawn()
przedstawia się następująco:
var child = cp.spawn('ls', [ '-l' ]); // Standardowe wyjście to zwykły egzemplarz Stream, który emituje zdarzenia 'data', // 'end' itd. child.stdout.pipe(fs.createWriteStream('ls-result.txt')); child.on('exit', function (code, signal) { // Zdarzenie emitowane po zakończeniu działania przez proces potomny. });
Pierwszy argument wskazuje program do uruchomienia. To może być pojedyncza nazwa programu, która zostanie wyszukana w lokalizacjach wskazanych przez aktualną wartość zmiennej środowiskowej PATH lub też bezwzględna ścieżka dostępu do programu. Drugim argumentem jest tablica ciągów tekstowych będących argumentami, z którymi zostanie uruchomiony proces. Domyślnie obiekt ChildProcess zawiera trzy wbudowane egzemplarze Stream, z którymi tworzone skrypty będą współpracowały: child.stdin — to zapisywalny obiekt Stream, który przedstawia standardowe wejście procesu potomnego. child.stdout — to odczytywalny obiekt Stream, który przedstawia standardowe wyjście procesu potomnego. child.stderr — to odczytywalny obiekt Stream, który przedstawia standardowe wyjście błędów procesu potomnego. Wymienione strumienie można wykorzystać w dowolny sposób, na przykład potokować je do pliku lub gniazda bądź też do innego rodzaju zapisywalnego strumienia. Jeśli chcesz, możesz je nawet zupełnie zignorować. Inne interesujące zdarzenie występujące w obiekcie ChildProcess to exit, które jest wywoływane po zakończeniu działania procesu i powiązanych z nim obiektów strumieni. Dobrym przykładem modułu zapewniającym abstrakcję użycia cp.spawn() w postaci użytecznej funkcjonalności jest node-cgi (https://github.com/TooTallNate/node-cgi). Pozwala on na ponowne użycie starych skryptów CGI (ang. Common Gateway Interface) w serwerach HTTP Node. CGI był standardem udzielania odpowiedzi na żądania HTTP przez wywoływanie skryptów CGI jako procesów potomnych serwera HTTP wraz ze specjalnymi zmiennymi środowiskowymi, które opisywały żądanie. Jako przykład poniżej przedstawiono skrypt CGI używający sh (powłoki) jako interfejsu CGI: #!/bin/sh echo "Status: 200" echo "Content-Type: text/plain"
echo echo "Witaj, $QUERY_STRING"
Jeżeli skrypt zostanie zapisany pod nazwą hello.cgi (nie zapomnij o wydaniu polecenia chmod +x hello.cgi, aby plik był wykonywalny), wówczas łatwo można wywołać skrypt jako logikę udzielania odpowiedzi na żądania HTTP. W serwerze HTTP wystarczy do tego celu pojedynczy wiersz kodu: var http = require('http'); var cgi = require('cgi'); var server = http.createServer( cgi('hello.cgi') ); server.listen(3000);
Po przygotowaniu serwera, kiedy żądanie HTTP dotrze do tego serwera, moduł node-cgi obsłuży je, wykonując dwa zadania: Za pomocą cp.spawn() uruchomienie skryptu hello.cgi jako nowego procesu potomnego. Przekazanie nowemu procesowi informacji o bieżącym żądaniu HTTP przy wykorzystaniu własnego zbioru zmiennych środowiskowych. S k r y p t hello.cgi używa jednej charakterystycznej dla CGI zmiennej środowiskowej QUERY_STRING, która zawiera ciąg tekstowy zapytania z adresu URL żądania. Ten ciąg tekstowy będzie wykorzystany w odpowiedzi, która zostanie umieszczona w danych wyjściowych skryptu. Jeżeli uruchomisz przygotowany serwer i wykonasz do niego żądanie HTTP za pomocą polecenia curl, wtedy otrzymasz wynik podobny do poniższego: $ curl http://localhost:3000/?Natalia Witaj, Natalia
Istnieje wiele użytecznych możliwości wykorzystania procesów potomnych w Node, a omówiony moduł node-cgi to tylko jedna z nich. Kiedy przygotowanego serwera lub aplikacji zaczniesz używać do celów, w których zostały opracowane, wówczas na pewnym etapie niewątpliwie znajdziesz odpowiednie zastosowanie dla procesu potomnego.
Rozkład obciążenia za pomocą cp.fork() Ostatnie API oferowane przez moduł child_process to specjalny sposób tworzenia nowych procesów Node za pomocą wbudowanego w Node kanału IPC. Ponieważ zawsze będzie tworzony nowy proces samego Node, pierwszym argumentem przekazywanym wywołaniu cp.fork() jest ścieżka dostępu do modułu Node.js przeznaczonego do wykonania. Podobnie jak jest w przypadku
cp.spawn(),
także
cp.fork()
zwraca obiekt ChildPro
cess.
Podstawowa różnica polega na dodaniu API przez kanał IPC: proces potomny ma funkcję child.send(message), a skrypt wywoływany przez fork() może nasłuchiwać zdarzeń process.on('message'). Przyjmujemy założenie, że w Node tworzymy serwer HTTP przeznaczony do obliczenia ciągu Fibonacciego. Tego rodzaju serwer możesz spróbować utworzyć w Node za pomocą kodu przedstawionego w listingu 13.10. Listing 13.10. Nieoptymalna implementacja ciągu Fibonacciego w Node.js var http = require('http'); function fib (n) { Obliczenie liczby w ciągu Fibonacciego. if (n < 2) { return 1; } else { return fib(n - 2) + fib(n - 1); } } var server = http.createServer(function (req, res) { var num = parseInt(req.url.substring(1), 10); res.writeHead(200); res.end(fib(num) + "\n"); }); server.listen(8000);
Jeżeli uruchomisz serwer za pomocą node fibonacci-native.js i wykonasz żądanie HTTP do http://localhost:8000, wówczas serwer będzie działał zgodnie z oczekiwaniami i obliczy ciąg Fibonacciego dla danej liczby. Taka operacja będzie jednak kosztowna i znacznie obciąży procesor. Ponieważ serwer Node jest jednowątkowy i będzie zajęty obliczaniem wyniku, inne żądania HTTP nie będą mogły być w tym czasie obsługiwane. Ponadto wykorzystany zostanie tylko jeden rdzeń procesora, a inne prawdopodobnie pozostaną nieaktywne. Z wymienionych powodów przedstawione rozwiązanie jest złe. Lepsze rozwiązanie polega na utworzeniu nowego procesu Node podczas każdego żądania HTTP i zleceniu procesowi potomnemu przeprowadzenia kosztownych obliczeń i podania ich wyniku. Wywołanie cp.fork() oferuje czysty interfejs do tego celu. Rozwiązanie będzie się składało z dwóch następujących plików: fibonacci-server.js — to skrypt serwera. fibonacci-calc.js — to skrypt procesu przeprowadzającego obliczenia.
Poniżej przedstawiono kod serwera: var http = require('http'); var cp = require('child_process'); var server = http.createServer(function(req, res) { var child = cp.fork(__filename, [ req.url.substring(1) ]); child.on('message', function(m) { res.end(m.result + '\n'); }); }); server.listen(8000);
Serwer używa wywołania cp.fork() do umieszczenia w oddzielnym procesie Node logiki odpowiedzialnej za obliczenie ciągu Fibonacciego. Wynik obliczeń zostanie przekazany procesowi nadrzędnemu za pomocą process.send(), jak przedstawiono w poniższym skrypcie fibonacci-calc.js: function fib(n) { if (n < 2) { return 1; } else { return fib(n - 2) + fib(n - 1); } } var input = parseInt(process.argv[2], 10); process.send({ result: fib(input) });
Teraz możesz uruchomić serwer za pomocą node wykonać żądanie HTTP do http://localhost:8000.
fibonacci-server.js
i ponownie
To jest doskonały przykład pokazujący, jak podział różnych komponentów aplikacji na wiele procesów może przynieść ogromne korzyści. Rozwiązanie oparte na cp.fork() zapewnia funkcje child.send() i child.on('message') przeznaczone do wysyłania i odbierania komunikatów z procesu potomnego. W ramach samego procesu potomnego dysponujesz funkcjami process.send() i process.on('message') przeznaczonymi do wysyłania i odbierania komunikatów z procesu nadrzędnego. Używaj wymienionych funkcji! Przechodzimy teraz do tematu, jakim jest opracowywanie w Node narzędzi działających w powłoce.
13.4. Tworzenie narzędzi powłoki
Inne zadanie często realizowane przez skrypty Node to tworzenie narzędzi działających w powłoce. Jak dotąd powinieneś znać już największe narzędzie powłoki zbudowane za pomocą Node — menedżer pakietów Node Package Manager, czyli polecenie npm. Ponieważ wymienione narzędzie jest menedżerem pakietów, przeprowadza wiele operacji systemu plików, tworzy nowe procesy potomne, a wszystko odbywa się za pomocą Node i jego asynchronicznego API. W ten sposób menedżer może instalować pakiety równolegle zamiast szeregowo, co przyśpiesza cały proces. Ponadto skoro w Node można utworzyć tak skomplikowane narzędzie powłoki, oznacza to, że za pomocą Node można zbudować dowolne narzędzie powłoki. W większości programów powłoki występuje potrzeba wykonywania tych samych zadań związanych z procesami, na przykład przetwarzania argumentów powłoki, odczytu danych ze standardowego wejścia, a także zapisu do standardowego wyjścia oraz obsługi błędów. W tym podrozdziale poznasz zadania najczęściej wykonywane podczas tworzenia programu powłoki, takie jak: Przetwarzanie argumentów powłoki. Praca ze strumieniami danych wejściowych i wyjściowych. Dodanie koloru do danych wyjściowych za pomocą skryptu ansi.js. Aby móc rozpocząć tworzenie wspaniałych programów powłoki, należy posiadać umiejętność odczytu argumentów, z którymi został uruchomiony program. Tym zagadnieniem zajmiemy się na kolejnych stronach.
13.4.1. Przetwarzanie argumentów podanych w powłoce Przetwarzanie argumentów to łatwy i prosty proces. Node oferuje właściwość process.argv będącą tablicą ciągów tekstowych, które są argumentami użytymi podczas wywołania Node. Pierwszym elementem tablicy jest plik wykonywalny Node, natomiast drugi to nazwa skryptu. Przetwarzanie i podejmowanie działań na podstawie wspomnianych argumentów wymaga przeprowadzenia iteracji przez elementy tablicy oraz sprawdzenia każdego argumentu. Aby to zademonstrować, utworzymy teraz prosty skrypt o nazwie args.js, którego zadaniem jest wyświetlenie zawartości tablicy process.argv. W większości przypadków dwa pierwsze elementy tablicy nie będą Cię interesowały i dlatego usuwamy je z danych wyjściowych za pomocą wywołania slice(): var args = process.argv.slice(2);
console.log(args);
Po wywołaniu skryptu bez argumentów tablica będzie pusta, ponieważ żadne argumenty dodatkowe nie zostały przekazane skryptowi: $ node args.js []
Jednak wywołanie skryptu z argumentami jan i kowalski powoduje, że tablica zawiera oczekiwane wartości w postaci ciągów tekstowych: $ node args.js jan kowalski [ 'jan', 'kowalski' ]
Ujęcie w nawias argumentów zawierających spacje powoduje — podobnie jak w każdej aplikacji powłoki — połączenie ich w pojedynczy argument. To nie jest funkcja Node, ale używanej powłoki (najczęściej bash na platformie UNIX i cmd.exe w Windows): $ node args.js "tobi jest zwierzakiem" [ 'tobi jest zwierzakiem' ]
Zgodnie z konwencją platformy UNIX każdy program powłoki powinien obsługiwać opcje -h i --help przez wyświetlenie informacji o sposobie użycia programu, a następnie zakończenie jego działania. W listingu 13.11 przedstawiono przykład użycia Array#forEach() do iteracji przez argumenty programu oraz przetworzenia ich za pomocą wywołania zwrotnego. Użycie wymienionych wcześniej opcji powoduje wyświetlenie informacji o sposobie użycia programu. Listing 13.11. Przetworzenie tablicy process.argv za pomocą Array#forEach() i bloku switch var args = process.argv.slice(2); Usunięcie z danych wyjściowych dwóch pierwszych elementów tablicy, którymi najczęściej nie jesteśmy zainteresowani. args.forEach(function (arg) { Iteracja przez argumenty i wyszukiwanie opcji -h lub -help. switch (arg) { case '-h': case '--help': printHelp(); break; Jeśli zachodzi potrzeba, umieść tutaj dodatkowe opcje. } }); function printHelp () { Wyświetlenie komunikatu o sposobie użycia programu, a następnie zakończenie jego działania. console.log(' Użycie:'); console.log(' $ WspaniałyProgram
console.log(' $ WspaniałyProgram --wspaniała-opcja jeszcze-niezbyt.wspaniałe'); process.exit(0); }
B l o k switch można bardzo łatwo rozbudować o możliwość przetwarzania dodatkowych opcji. Moduły opracowane przez społeczność, na przykład commander.js, nopt, optimist i nomnom (wymieniono tutaj jedynie kilka dostępnych), przetwarzają opcje na własne sposoby. Dlatego też warto wiedzieć, że użycie bloku switch to nie jest jedyny możliwy sposób przetwarzania argumentów. Podobnie jak w wielu innych przypadkach, tak i tu nie istnieje jeden prawidłowy sposób wykonania danego zadania w programowaniu. Inne zadanie, z którym każdy program powłoki musi sobie radzić, to odczyt danych wejściowych ze standardowego wejścia i zapis strukturyzowanych danych wyjściowych do standardowego wyjścia. Przekonajmy się, jak można to zrobić w Node.
13.4.2. Praca ze standardowym wejściem i wyjściem Programy na platformie UNIX są najczęściej małe, samodzielne i skoncentrowane na wykonywaniu pojedynczego zadania. Poszczególne programy są łączone za pomocą potoków i wynik działania jednego jest przekazywany do kolejnego programu, aż do zakończenia łańcucha poleceń. Na przykład za pomocą standardowych poleceń systemu UNIX można pobrać listę unikatowych autorów z dowolnego repozytorium Git. Wymaga to połączenia poleceń git log, sort i uniq w następujący sposób: $ git log --format='%aN' | sort | uniq Mike Cantelon Nathan Rajlich TJ Holowaychuk
Wymienione polecenia działają jednocześnie, dane wyjściowe jednego procesu są używane jako dane wejściowe kolejnego i tak aż do końca łańcucha poleceń. Aby zaoferować mechanizm podobny do potokowania, Node dostarcza dwa obiekty Stream, które można wykorzystać w budowanych programach powłoki: process.stdin — obiekt ReadStream przeznaczony do odczytu danych wejściowych. process.stdout — obiekt WriteStream przeznaczony do zapisu danych wyjściowych. Sposób działania wymienionych obiektów jest podobny jak omówionych
wcześniej interfejsów strumieni.
Zapis danych wyjściowych za pomocą process.stdout Zapisywalnego strumienia process.stdout używasz za każdym razem, gdy wywoływana jest funkcja console.log(). Wewnętrznie wymieniona funkcja w y w o ł u j e process.stdout.write() po sformatowaniu argumentów danych wejściowych. Jednak funkcje console są przeznaczone bardziej do debugowania i analizowania obiektów. Kiedy zachodzi potrzeba zapisu w standardowym wyjściu strukturyzowanych danych, wtedy można bezpośrednio wywołać process.stdout.write(). Przyjmujemy założenie, że program nawiązuje połączenie z adresem HTTP URL, a następnie odpowiedź przekazuje do standardowego wyjścia. W takim kontekście doskonale sprawdza się Stream#pipe(), jak przedstawiono w poniższym fragmencie kodu: var http = require('http'); var url = require('url'); var target = url.parse(process.argv[2]); var req = http.get(target, function (res) { res.pipe(process.stdout); });
Doskonale! Niezwykle minimalna replika polecenia curl utworzona za pomocą jedynie sześciu wierszy kodu. Nie tak źle, prawda? Teraz zajmiemy się process.stdin.
Odczyt danych wejściowych za pomocą process.stdin Zanim będzie można odczytać dane ze standardowego wejścia, konieczne jest wywołanie process.stdin.resume() w celu wskazania, że skrypt jest zainteresowany danymi pochodzącymi ze standardowego wejścia. Działa ono podobnie jak każdy inny odczytywalny strumień, emituje zdarzenia data po otrzymaniu danych wyjściowych innego procesu oraz kiedy użytkownik naciśnie dowolny klawisz w oknie terminalu. W listingu 13.12 przedstawiono kod programu powłoki, który prosi użytkownika o podanie wieku. Na podstawie otrzymanych informacji program podejmuje decyzję o dalszym sposobie działania. Listing 13.12. Program, który prosi użytkownika o podanie wieku var requiredAge = 18; Ustawienie granicy wieku. process.stdout.write('Proszę podać wiek: '); Przygotowanie pytania dla użytkownika. process.stdin.setEncoding('utf8'); Określenie, że standardowe wejście ma emitować ciągi tekstowe UTF-8 zamiast buforów. process.stdin.on('data', function (data) {
var age = parseInt(data, 10); Przetworzenie danych na postać liczby. if (isNaN(age)) { Jeżeli wiek podany przez użytkownika jest mniejszy niż 18 lat, należy wyświetlić komunikat informujący go, aby powrócił do programu za kilka lat. console.log('%s nie jest poprawną liczbą!', data); } else if (age < requiredAge) { Jeżeli użytkownik nie podał prawidłowej liczby, należy wyświetlić odpowiedni komunikat. console.log('Musisz mieć co najmniej %d lat, aby uzyskać dostęp do programu. ' + 'Wróć za %d lat', requiredAge, requiredAge - age); } else { enterTheSecretDungeon(); Po spełnieniu wcześniejszych warunków można kontynuować wykonywanie programu. } process.stdin.pause(); Oczekiwanie na pojedyncze zdarzenie data przed zamknięciem standardowego wejścia. }); process.stdin.resume(); Wywołanie funkcji resume() w celu rozpoczęcia odczytu, ponieważ process.stdin na początku znajduje się w stanie wstrzymania. function enterTheSecretDungeon () { console.log('Witamy w Programie :)'); }
Rejestracja danych diagnostycznych za pomocą process.stderr W każdym procesie Node istnieje również zapisywalny strumień process.stderr, który działa dokładnie tak samo jak process.stdout, ale dane przekazuje do standardowego wyjścia błędów. Ponieważ jest ono zarezerwowane zwykle dla debugowania, a nie dla wysyłania strukturalnych danych lub potokowania, ogólnie rzecz biorąc, będziesz używał funkcji console.error(), zamiast uzyskiwać bezpośredni dostęp do process.stderr. Po poznaniu standardowych strumieni wejścia-wyjścia w Node dysponujesz wiedzą niezbędną przy tworzeniu programów powłoki. Przystępujemy więc do zbudowania czegoś nieco bardziej kolorowego.
13.4.3. Dodanie koloru do danych wyjściowych Wiele narzędzi powłoki używa kolorowego tekstu w celu ułatwienia użytkownikom odróżniania informacji na ekranie. Node również stosuje tę funkcjonalność w interfejsie REPL, podobnie jak menedżer npm na różnych poziomach rejestracji danych. Użycie koloru to przydatny dodatek, z którego łatwo może skorzystać praktycznie każdy program powłoki. Dodanie koloru to
łatwe zadanie, zwłaszcza w przypadku użycia jednego z modułów opracowanych przez społeczność.
Tworzenie i zapis znaków sterujących ANSI Kolory w terminalu są generowane za pomocą znaków sterujących ANSI (ang. American National Standards Institute). Wspomniane znaki sterujące to sekwencje zwykłego tekstu przekazywane do standardowego wyjścia i mające znaczenie specjalne dla terminalu — mogą zmienić kolor tekstu, położenie kursora, odtworzyć dźwięk itd. Zaczniemy od prostego przykładu. Aby skrypt wyświetlił słowo witaj w kolorze zielonym, można użyć pojedynczego wywołania console.log(): console.log('\033[32mwitaj\033[39m');
Jeżeli przyjrzysz się dokładnie powyższemu wywołaniu, słowo witaj dostrzeżesz w środku ciągu tekstowego, wśród dziwnych znaków wokół. W pierwszej chwili to może wydawać się dziwne, ale tak naprawdę jest to prosty mechanizm. Na rysunku 13.4 pokazano podział ciągu tekstowego witaj w kolorze zielonym na trzy oddzielne części.
Rysunek 13.4. Wyświetlenie słowa „witaj” w kolorze zielonym za pomocą znaków sterujących ANSI
Istnieje znacznie więcej znaków sterujących rozpoznawanych przez terminale, ale większość programistów ma ważniejsze rzeczy do zrobienia niż zapamiętywanie ich. Na szczęście społeczność Node okazuje się pomocna nawet w takiej sytuacji — opracowała wiele modułów, między innymi colors.js, clicolor.js i ansi.js, dzięki którym użycie koloru w programie powłoki jest niezwykle łatwe. Kody ANSI w Windows. Technicznie rzecz biorąc, system Windows i jego wiersz poleceń (cmd.exe) nie obsługują znaków sterujących ANSI. Na szczęście dla nas Node interpretuje znaki sterujące w Windows, gdy skrypt przekazuje je do standardowego wyjścia, a następnie wywołuje odpowiednie funkcje Windows zapewniające możliwość osiągnięcia tego samego wyniku. Warto o tym wiedzieć, choć jednocześnie nie musisz się nad tym zastanawiać podczas tworzenia aplikacji Node.
Formatowanie koloru tekstu za pomocą ansi.js Spójrz na moduł ansi.js (https://github.com/TooTallNate/ansi.js), który można zainstalować za pomocą npm install ansi. Wymieniony moduł jest dobrym rozwiązaniem, ponieważ to bardzo cienka warstwa na górze zwykłych znaków sterujących ANSI, oferująca ogromną elastyczność w porównaniu z innymi modułami dodającymi kolor (działają one z pojedynczym ciągiem tekstowym). Za pomocą modułu ansi.js można zdefiniować tryby (na przykład „pogrubienie”) dla strumieni. Będą one stosowane aż do wywołania reset(). Warto w tym miejscu dodać, że ansi.js to pierwszy moduł obsługujący 256 kolorów terminala. Potrafi również konwertować kody kolorów CSS (takie jak #FF0000) na kody kolorów ANSI. M o duł ansi.js działa wraz z koncepcją kursora, który tak naprawdę jest opakowaniem dla egzemplarza zapisywalnego strumienia i zawiera wiele wygodnych funkcji przeznaczonych do umieszczania kodów ANSI w strumieniu, a ponadto obsługuje łączenie w łańcuchy. W celu wyświetlenia słowa witaj w kolorze zielonym za pomocą modułu ansi.js można użyć poniższego fragmentu kodu: var ansi = require('ansi'); var cursor = ansi(process.stdout); cursor .fg.green() .write('witaj') .fg.reset() .write('\n');
Jak możesz się przekonać, w celu użycia modułu ansi.js najpierw trzeba utworzyć egzemplarz cursor na podstawie zapisywalnego strumienia. Ponieważ interesuje nas zastosowanie koloru w danych wyjściowych programu, można przekazać process.stdout jako zapisywalny strumień używany przez egzemplarz coursor. Po uzyskaniu egzemplarza coursor można już wywołać dowolną z dostarczonych metod przeznaczonych do zmiany sposobu generowania w terminalu tekstu danych wyjściowych. W omawianym przykładzie wynik odpowiada wcześniej przedstawionemu wywołaniu console.log(): cursor.fg.green() — ustawienie koloru tekstu na zielony. cursor.write('witaj') — wyświetlenie w terminalu słowa witaj w kolorze zielonym. cursor.fg.reset() — przywrócenie domyślnego koloru tekstu. cursor.write('\n') — umieszczenie znaku nowego wiersza.
Programowe dopasowanie danych wyjściowych za estetyczny interfejs przeznaczony do zmiany kolorów.
pomocą
cursor
oferuje
Formatowanie koloru tła za pomocą ansi.js Moduł ansi.js pozwala również na zmianę koloru tła. Aby ustawić kolor tła zamiast tekstu, w wywołaniu należy fragment fg zastąpić przez bg. Na przykład ustawienie tła w kolorze czerwonym wymaga wywołania cursor.bg.red(). Przygotujemy teraz mały program wyświetlający w terminalu kolorowe informacje dotyczące tytułu i autorów niniejszej książki, jak pokazano na rysunku 13.5. Jak pokazano na rysunku, kod przeznaczony do wyświetlenia danych wyjściowych w kolorze jest nieco rozległy, ale łatwy. Każda funkcja jest mapowana bezpośrednio na znak sterujący umieszczany w strumieniu. Kod przedstawiony w listingu 13.13 składa się z dwóch wierszy inicjalizacyjnych oraz naprawdę długiego łańcucha wywołań funkcji, które ostatecznie umieszczają znaki sterujące koloru i ciągi tekstowe w pro cess.stdout. Kody pozwalające na zdefiniowanie koloru to tylko jedna z kluczowych funkcji modułu ansi.js. Nie przedstawiliśmy tutaj możliwości w zakresie umieszczania kursora, odtwarzania dźwięku bądź też ukrywania i wyświetlania kursora. Więcej informacji na ten temat i przykłady znajdziesz w dokumentacji modułu ansi.js.
Rysunek 13.5. Wynik uruchomienia skryptu ansi-title.js — tytuł książki i jej autorzy to informacje wyświetlone w różnych kolorach Listing 13.13. Prosty program, który wyświetla tytuł książki i jej autorów w różnych kolorach var ansi = require('ansi'); var cursor = ansi(process.stdout); cursor .reset() .write(' ')
.bold() .underline() .bg.white() .fg.black() .write('Node.js w akcji') .fg.reset() .bg.reset() .resetUnderline() .resetBold() .write(' \n') .fg.green() .write(' by:\n') .fg.cyan() .write('
Mike Cantelon\n')
.fg.magenta() .write('
TJ Holowaychuk\n')
.fg.yellow() .write('
Nathan Rajlich\n')
.reset()
13.5. Podsumowanie Node zaprojektowano przede wszystkim do wykonywania zadań związanych z operacjami wejścia-wyjścia, takimi jak tworzenie serwera HTTP. Jednak Node doskonale sprawdza się także w wielu innych zadaniach, między innymi podczas tworzenia programów powłoki dla serwera aplikacji, programu klienta nawiązującego połączenie z serwerem ASCII Star Wars, programu pobierającego i wyświetlającego dane statystyczne z serwerów giełdowych itd. Możliwości zastosowania są ograniczone jedynie Twoją wyobraźnią. Spójrz na menedżera npm i node-gyp — to dwa przykłady skomplikowanych narzędzi powłoki utworzonych za pomocą Node. To jednocześnie doskonałe przykłady, na podstawie których można się uczyć. W tym rozdziale wspomniano o kilku modułach opracowanych przez społeczność, które mogą pomóc podczas prac nad aplikacjami. W kolejnym rozdziale skoncentrujemy się na tym, jak wyszukiwać wspaniałe moduły opracowane przez społeczność Node. Dowiesz się również, jak podzielić się samodzielnie opracowanymi modułami w celu zebrania komentarzy i wprowadzenia usprawnień w modułach. Aspekt społecznościowy to naprawdę ekscytujący obszar!
Rozdział 14. Ekosystem Node W tym rozdziale: • Wyszukiwanie w internecie pomocy związanej z Node. • Współpraca nad Node poprzez serwis GitHub. • Publikowanie własnej pracy za pomocą Node Package Manager.
Aby w pełni móc wykorzystać możliwości programowania w Node, należy wiedzieć, gdzie szukać pomocy oraz jak dzielić się ze społecznością Node samodzielnie opracowanym kodem. Prace nad Node i powiązanymi projektami odbywają się podobnie jak w większości społeczności oprogramowania open source, czyli na zasadzie współpracy przez internet wielu osób. Programiści współpracują ze sobą, przekazując sobie i przeglądając kod, tworząc dokumentację projektu i zgłaszając błędy. Kiedy programiści są gotowi do wydania nowej wersji Node, zostaje ona opublikowana w oficjalnej witrynie Node. Po utworzeniu przez osoby trzecie modułu wartego wydania można go umieścić w repozytorium npm, dzięki czemu staje się łatwy do instalacji przez innych użytkowników. Zasoby dostępne w internecie zawierają wszelkie informacje niezbędne do rozpoczęcia pracy z Node oraz z projektami powiązanymi. Na rysunku 14.1 pokazano, jak używać zasobów internetowych podczas prac programistycznych związanych z Node, dystrybucją i zapewnieniem pomocy technicznej. Zanim zaczniesz współpracę z innymi, będziesz prawdopodobnie szukał pomocy technicznej. Dlatego też w pierwszej kolejności dowiedz się, gdzie w internecie znajdziesz pomoc, gdy będzie Ci ona potrzebna.
Rysunek 14.1. Projekty związane z Node są tworzone wspólnie za pośrednictwem serwisu GitHub. Następnie są publikowane w repozytorium npm, a dokumentacja i pomoc techniczna są dostępne za pomocą zasobów internetowych
14.1. Dostępne w internecie zasoby dla programistów Node Ponieważ świat Node nieustannie się zmienia, najnowsze informacje dotyczące tej technologii znajdziesz w internecie. Do dyspozycji masz wiele witryn internetowych, grup dyskusyjnych, czatów i innych zasobów, w których można znaleźć potrzebne informacje.
14.1.1. Node i odniesienia do modułów W tabeli 14.1 wymieniono pewne znajdujące się w internecie zasoby związane z Node. Najużyteczniejsze z nich to strony internetowe zawierające omówienie API Node (strona domowa Node.js), a także informacje o opracowanych przez firmy trzecie modułach (strona domowa menedżera npm). Tabela 14.1. Użyteczne zasoby w internecie dotyczące Node Z asób
Adres URL
Strona główna Node.js
http://nodejs.org/
Aktualna dokumentacja API Node
http://nodejs.org/api/
Blog Node
http://blog.nodejs.org/
Oferty pracy związane z Node
http://jobs.nodejs.org/a/jobs/find-jobs
Strona główna menedżera Node Package Manager (npm)
https://www.npmjs.org/
Jeśli próbujesz coś zaimplementować za pomocą Node lub dowolnego z wbudowanych modułów, to strona domowa Node stanowi nieoceniony zasób. Ta pokazana na rysunku 14.2 witryna internetowa zawiera pełną dokumentację frameworka Node i oferowanego przez niego API. Tutaj zawsze znajdziesz dokumentację dotyczącą najnowszej wersji Node. Oficjalny blog również zawiera informacje o nowościach wprowadzonych w Node oraz ogólnie komunikaty ważne dla społeczności Node. Strona domowa zawiera również oferty pracy.
Rysunek 14.2. Oprócz odnośników prowadzących do użytecznych zasobów powiązanych z Node witryna nodejs.org oferuje także dokumentację API dla każdej wydanej wersji Node
Jeśli szukasz funkcji opracowanych przez firmy trzecie, repozytorium npm to
pierwsze miejsce, które powinieneś odwiedzić. Pozwala na użycie słów kluczowych do przeszukiwania tysięcy modułów oferowanych przez npm. Jeżeli znajdziesz moduł, który chcesz sprawdzić, kliknij jego nazwę, co spowoduje wyświetlenie strony z informacjami szczegółowymi o module. Wspomniana strona zawiera między innymi łącze prowadzące do strony domowej projektu modułu oraz wszelkie użyteczne informacje o nim, na przykład wskazujące, jakie inne moduły zależą od danego, zależności danego modułu od innych, wersje zgodnych modułów oraz informacje o licencji. Wymienione wcześniej witryny internetowe nie dostarczają odpowiedzi na wszystkie pytania, które mogą się pojawić w zakresie użycia Node lub modułów opracowanych przez firmy trzecie. Zapoznaj się więc z innymi doskonałymi miejscami, w których można poprosić o pomoc.
14.1.2. Grupy Google Grupy Google zostały utworzone dla Node, menedżera npm oraz pewnych popularnych modułów i frameworków, między innymi Express, node-mogodb-native i Mongoose. Grupy Google to doskonałe miejsce do zadawania trudnych lub obszernych pytań. Na przykład jeśli masz problem ze znalezieniem sposobu na usunięcie dokumentów MongoDB za pomocą modułu node-mogodb-native, wówczas odpowiednie pytanie można zadać na grupie poświęconej wymienionemu modułowi (https://groups.google.com/forum/?fromgroups&hl=pl#!forum/nodemongodb-native) i przekonać się, czy ktokolwiek inny napotkał podobny problem. Jeżeli ktoś już spotkał się z podobnym problemem, Twoim kolejnym krokiem powinno być dołączenie do grupy Google i zadanie pytania. Możesz tworzyć długie posty, co jest przydatne w skomplikowanych pytaniach, ponieważ pozwala na dokładne wyjaśnienie istoty problemu. Nie istnieje centralna lista wymieniająca wszystkie grupy Google powiązane z Node. Możesz je znaleźć podane w dokumentacji projektu, choć najczęściej będziesz musiał sam wyszukać je w sieci. W tym celu w wyszukiwarce internetowej wystarczy podać wyrażenie „nazwa-modułu node.js grupa google” i sprawdzić, czy istnieje grupa dla wskazanego modułu. Wadą użycia grup Google jest często konieczność czekania kilka godzin lub nawet dni na uzyskanie odpowiedzi, w zależności od parametrów grupy Google. W przypadku prostych pytań, gdy szybko potrzebujesz odpowiedzi, powinieneś rozważyć skorzystanie z czatu internetowego, na którym z reguły szybko znajdziesz potrzebne informacje.
14.1.3. IRC
IRC (ang. Internet Relay Chat) utworzono w roku 1988 i choć niektórzy mogą uważać IRC za archaiczne rozwiązanie, ono nadal funkcjonuje i jest aktywnie wykorzystywane — to najlepszy sposób na szybkie uzyskanie odpowiedzi na pytania dotyczące oprogramowania open source. Pokoje IRC są nazywane kanałami, istnieją dla wielu modułów Node. Wprawdzie nigdzie nie znajdziesz listy kanałów IRC poświęconych Node, ale w dokumentacji modułu czasami umieszczane są informacje o poświęconym mu kanale IRC, o ile taki istnieje. Aby otrzymać odpowiedź na pytanie zadane na czacie, należy nawiązać połączenie z siecią IRC (http://chatzilla.hacksrus.com/faq/#connect), przejść do odpowiedniego kanału, a następnie po prostu zadać pytanie. Ze względu na szacunek wobec uczestników czatu przed zadaniem pytania warto sprawdzić, czy podobne pytanie nie pojawiło się wcześniej i czy nie została na nie już udzielona odpowiedź. Jeżeli jesteś początkującym użytkownikiem IRC, najłatwiejszym sposobem nawiązania połączenia będzie użycie klienta opartego na przeglądarce internetowej. Freenode, sieć IRC, w której znajduje się większość kanałów IRC poświeconych Node, oferuje klienta sieciowego pod adresem http://webchat.freenode.net. Aby przyłączyć się do czatu, w formularzu połączenia podaj nazwę użytkownika. Nie trzeba się rejestrować i można podać dowolną nazwę użytkownika. (Jeżeli ktokolwiek używa wybranej przez Ciebie nazwy użytkownika, na jej końcu zostanie umieszczony znak podkreślenia, aby umożliwić rozróżnianie użytkowników). Po kliknięciu przycisku Connect dołączysz do kanału. Po prawej stronie znajduje się pasek boczny, w którym wymieniono innych uczestników czatu.
14.1.4. Zgłaszanie problemów w serwisie GitHub Jeżeli projekt znajduje się w serwisie GitHub, wtedy innym miejscem pozwalającym na szukanie rozwiązania dla powstałych problemów jest strona zgłaszania problemów w projekcie GitHub. Aby przejść na tę stronę, należy najpierw wyświetlić stronę główną projektu w serwisie GitHub, a następnie kliknąć kartę Issues. Możesz skorzystać z pola wyszukiwania i spróbować odszukać inne problemy podobne do napotkanego przez Ciebie. Przykładową stronę przeznaczoną do zgłaszania problemów pokazano na rysunku 14.3.
Rysunek 14.3. W przypadku projektów umieszczonych w serwisie GitHub zgłoszenie błędu może być pomocne, jeśli sądzisz, że odkryłeś problem w kodzie projektu
Jeżeli nie znajdujesz informacji pomagających w rozwiązaniu problemu i uważasz, że może on być wynikiem błędu w kodzie projektu, kliknij przycisk New Issue na wyświetlonej stronie i dokładnie opisz problem. Po utworzeniu nowego zgłoszenia osoby zajmujące się projektem będą mogły odpowiedzieć na tej samej stronie i rozwiązać problem lub zadać dodatkowe pytania w celu lepszego zdiagnozowania problemu. Strona zgłaszania problemów w GitHub to nie forum pomocy technicznej. W zależności od projektu zadawanie na wspomnianej stronie ogólnych pytań z zakresu pomocy technicznej może być uznawane za nieodpowiednie. Dzieje się tak najczęściej, gdy dla projektu przygotowano inne formy udzielania pomocy technicznej, na przykład za pośrednictwem grup Google. Dobrym podejściem jest zapoznanie się z plikiem README projektu i sprawdzenie, czy istnieją jakiekolwiek wskazówki dotyczące zadania pytań z zakresu ogólnej pomocy technicznej. Teraz już wiesz, jak zgłaszać problemy związane z projektami GitHub. Przechodzimy więc do niezwiązanej z pomocą techniczną roli serwisu GitHub będącego miejscem współpracy większości osób zajmujących się rozwijaniem frameworka Node.
14.2. Serwis GitHub
Serwis GitHub to środek ciężkości świata oprogramowania open source i jednocześnie miejsce o znaczeniu krytycznym dla programistów Node. GitHub oferuje obsługę repozytoriów Git, czyli potężnego systemu kontroli wersji (ang. Version Control System , VCS). Serwis zawiera interfejs działający w przeglądarce internetowej i pozwalający na łatwe przeglądanie repozytoriów Git. Projekty typu open source mogą być umieszczone bezpłatnie w serwisie GitHub. Git. System kontroli wersji Git stał się najpopularniejszym wyborem dla projektów typu open source. To rozproszony system kontroli wersji, którego w przeciwieństwie do Subversion i innych tego rodzaju systemów można używać bez połączenia sieciowego z serwerem. Git powstał w roku 2005, zainspirowany przez własnościowy VCS o nazwie BitKeeper. Właściciel serwisu BitKeeper pozwolił na bezpłatne korzystanie z serwisu zespołowi pracującemu nad jądrem systemu Linux. Ten przywilej cofnął jednak, gdy nabrał podejrzeń, że członkowie zespołu próbują ustalić wewnętrzny sposób działania serwisu BitKeeper. Linus Torvalds, czyli twórca systemu Linux, postanowił utworzyć alternatywny system kontroli wersji, o podobnej funkcjonalności. Kilka miesięcy później system Git był już używany przez zespół pracujący nad jądrem systemu Linux. Poza hostingiem repozytoriów Git serwis GitHub oferuje stronę zgłaszania błędów, wiki, a także możliwość hostingu strony internetowej poświęconej projektowi. Ponieważ większość projektów Node w repozytorium npm jest przechowywana w serwisie GitHub, umiejętność posługiwania się tym serwisem pozwala na wykorzystanie pełni możliwości podczas programowania w Node. GitHub zapewnia wygodny sposób przeglądania kodu, sprawdzania nierozwiązanych błędów, a jeśli zachodzi potrzeba — także zgłaszania poprawek i opracowywania dokumentacji. Innym sposobem użycia GitHub jest obserwacja projektu. Obserwacja projektu oznacza otrzymywanie powiadomień o wprowadzeniu zmian w projekcie. Liczba osób obserwujących dany projekt jest często używana do określenia jego ogólnej popularności. Serwis GitHub może oferować potężne możliwości, ale jak z niego korzystać? Tego dowiesz się w kolejnym punkcie.
14.2.1. Rozpoczęcie pracy z GitHub Kiedy wpadniesz na pomysł utworzenia projektu lub opracowania modułu opartego na Node, wówczas warto utworzyć konto w serwisie GitHub (jeśli jeszcze tego nie zrobiłeś), aby zapewnić sobie łatwy dostęp do hostingu repozytoriów Git. Po utworzeniu konta i przeprowadzeniu jego konfiguracji będziesz mógł zacząć dodawać projekty, o czym dowiesz się w kolejnym
punkcie. Ponieważ GitHub wymaga systemu kontroli wersji Git, konieczne jest skonfigurowanie go przed rozpoczęciem pracy z GitHub. Na szczęście w serwisie GitHub znajdują się strony pomocy objaśniające prawidłową konfigurację Git na platformie Mac, Windows i Linux (https://help.github.com/articles/set-up-git). Po skonfigurowaniu Git możesz utworzyć konto GiHub, rejestrując się w witrynie internetowej serwisu i dostarczając klucz SSH. Wspomniany klucz SSH jest niezbędny do zapewnienia bezpiecznej współpracy z serwisem GitHub. W tym punkcie zostanie dokładnie omówiony proces konfiguracji. Zwróć uwagę, że omówione tutaj kroki trzeba wykonać tylko jednokrotnie, a nie podczas każdego dodawania projektu do GitHub.
Konfiguracja Git i rejestracja GitHub Aby można było używać serwisu GitHub, konieczne jest skonfigurowanie systemu kontroli wersji Git. Imię i nazwisko oraz adres e-mail podajesz za pomocą dwóch wymienionych poniżej poleceń: git config --global user.name "Jan Kowalski" git config --global user.email [email protected]
Teraz możesz przystąpić do rejestracji konta w GitHub. Przejdź na stronę rejestracji (https://github.com/join), wypełnij formularz i kliknij przycisk Create an account.
Dostarczenie GitHub klucza publicznego SSH Po rejestracji trzeba umieścić w serwisie GitHub klucz publiczny SSH (https://help.github.com/articles/generating-ssh-keys). Wymieniony klucz będzie używany do uwierzytelniania transakcji Git. Wykonaj wymienione poniżej kroki: 1. W przeglądarce internetowej przejdź na stronę https://github.com/settings/ssh. 2. Kliknij przycisk Add SSH Key. Na tym etapie dalsze postępowanie zależy od używanego systemu operacyjnego. GitHub wykryje Twój system operacyjny i wyświetli odpowiednie instrukcje.
14.2.2. Dodanie projektu do GitHub Po przygotowaniu konta w serwisie GitHub można przystąpić do dodawania projektów i przekazywania kodu do repozytoriów. W tym celu najpierw utwórz repozytorium GitHub dla projektu, co zostanie wkrótce omówione. Następnie w komputerze lokalnym utwórz repozytorium
Git, w którym będziesz umieszczał kod przed jego przekazaniem do GitHub. Cały proces został pokazany na rysunku 14.4. Pliki projektu można przejrzeć również za pomocą oferowanego przez GitHub interfejsu sieciowego.
Utworzenie repozytorium GitHub Utworzenie kroków:
repozytorium Git
wymaga
wykonania
wymienionych poniżej
1. W przeglądarce internetowej przejdź do serwisu GitHub i zaloguj się. 2. Przejdź na stronę https://github.com/new.
Rysunek 14.4. Kroki niezbędne podczas dodawania projektu Node do GitHub
3. Wypełnij wyświetlony formularz opisujący repozytorium i kliknij przycisk Create Repository. 4. GitHub utworzy puste repozytorium Git oraz stronę zgłaszania błędów dla projektu. 5. GitHub wyświetli kroki, jakie trzeba wykonać, aby użyć systemu kontroli wersji Git do umieszczenia kodu w nowym repozytorium. Warto rozumieć działanie poszczególnych kroków i dlatego zostaną tutaj omówione, a przykłady przedstawią podstawy użycia Git.
Konfiguracja pustego repozytorium Git W celu dodania przykładowego projektu do serwisu GitHub najpierw utworzymy prosty moduł dla Node. W omawianym przykładzie będzie to moduł zawierający logikę przeznaczoną do skracania adresów URL. Modułowi nadajemy nazwę node-elf. Rozpoczynamy więc od utworzenia katalogu tymczasowego dla projektu, wydając wymienione poniżej polecenia: mkdir -p ~/tmp/node-elf cd ~/tmp/node-elf
Aby użyć katalogu jako repozytorium Git, należy wydać poniższe polecenie, które spowoduje utworzenie katalogu o nazwie .git zawierającego metadane repozytorium: git init
Dodanie plików do repozytorium Git Po przygotowaniu pustego repozytorium możemy dodać do niego pewne pliki. W omawianym przykładzie to będzie plik zawierający logikę odpowiedzialną za skracanie adresów URL. Kod przedstawiony w listingu 14.1 umieść w pliku o nazwie index.js w utworzonym wcześniej katalogu. Listing 14.1. Moduł Node służący do skracania adresów URL exports.initPathData = function(pathData) { Funkcja inicjalizacyjna jest wywoływana niejawnie przez funkcje shorten() i expand(). pathData = (pathData) ? pathData : {}; pathData.count = (pathData.count) ? pathData.count : 0; pathData.map
= (pathData.map) ? pathData.map : {};
} exports.shorten = function(pathData, path) { Funkcja akceptuje ciąg tekstowy „ścieżki” i zwraca skrócony adres URL mapowany na tę ścieżkę. exports.initPathData(pathData); pathData.count++; pathData.map[pathData.count] = path; return pathData.count.toString(36); } exports.expand = function(pathData, shortened) { Funkcja akceptuje poprzednio skrócony adres URL i zwraca adres URL w pełnej postaci. exports.initPathData(pathData); var pathIndex = parseInt(shortened, 36); return pathData.map[pathIndex]; }
Teraz trzeba poinformować Git, że chcemy umieścić nowy plik w repozytorium. Polecenie add w systemie Git działa inaczej niż w pozostałych systemach kontroli wersji. Zamiast dodawać pliki do repozytorium, polecenie dodaje je do tak zwanego staging area w Git. Wymieniony obszar można uznawać za listę wskazującą na pliki nowo dodane, zmodyfikowane lub te, które mają zostać umieszczone w kolejnej wersji repozytorium: git add index.js
System Git teraz „wie”, że powinien monitorować wskazany plik. Jeśli chcesz, do staging area możesz dodać jeszcze inne pliki, ale na obecnym etapie wystarczy tylko jeden wymieniony. Aby nakazać systemowi Git utworzenie nowej wersji repozytorium zawierającego zmodyfikowane pliki umieszczone w staging area, należy wydać polecenie commit. W przypadku innych VCS polecenie commit może akceptować opcję -m pozwalającą na dodanie opisu zmian wprowadzonych w nowej wersji repozytorium: git commit -m "Dodano funkcję skracania adresów URL."
Repozytorium znajdujące się w komputerze lokalnym zawiera teraz nową wersję repozytorium. Wyświetlenie listy zmian repozytorium następuje po wydaniu poniższego polecenia: git log
Przekazanie repozytorium Git do serwisu GitHub Jeżeli na tym etapie komputer lokalny zostanie zniszczony, wówczas stracisz całą dotychczas wykonaną pracę. W celu zabezpieczenia się przed nieprzewidywalnymi kataklizmami i wykorzystania pełni możliwości interfejsu sieciowego GitHub zmiany wprowadzone w lokalnym repozytorium Git należy przekazać do serwisu GitHub. Wcześniej trzeba jednak poinformować Git, gdzie ma być przekazywany kod. To wymaga utworzenia zdalnego repozytorium Git. Tego rodzaju repozytoria są określane mianem zdalnych. Poniższe polecenie pokazuje, jak dodać zdalne repozytorium GitHub. Podaj odpowiednią nazwę użytkownika i zwróć uwagę, że node-elf.git wskazuje nazwę projektu: git remote add origin [email protected]:nazwa-użytkownika/node-elf.git
Po dodaniu zdalnego repozytorium zmiany wprowadzone w kodzie można wysłać do serwisu GitHub. W terminologii Git przekazywanie zmian do repozytorium nosi nazwę push. W poniższym poleceniu nakazujesz Git przekazanie zmian do zdalnego repozytorium origin zdefiniowanego w poprzednim poleceniu. Każde repozytorium Git może mieć jedno lub więcej odgałęzień, które pod względem koncepcji są oddzielnymi obszarami roboczymi w repozytorium. Zmiany wprowadzone w kodzie chcesz przekazać do gałęzi master:
git push -u origin master
W poleceniu push opcja -u informuje Git, że wskazane zdalne repozytorium to upstream i gałąź. Zdalne repozytorium upstream jest domyślnie używanym zdalnym repozytorium. Po pierwszym przekazaniu zmian za pomocą opcji -u kolejne operacje przekazania kodu możesz przeprowadzać za pomocą poniższego polecenia, które jest prostsze i tym samym łatwiejsze do zapamiętania: git push
Jeżeli przejdziesz do serwisu GitHub i odświeżysz stronę repozytorium, zobaczysz nowo dodany plik. Utworzenie i hosting modułu w GitHub to łatwy sposób na umożliwienie wielokrotnego wykorzystania danego modułu. Na przykład jeżeli będziesz chciał go użyć w innym projekcie, wówczas wystarczy wydać przedstawione poniżej polecenia: mkdir ~/tmp/my_project/node_modules cd ~/tmp/my_project/node_modules git clone https://github.com/mcantelon/node-elf.git elf cd ..
Polecenie require('elf') w kodzie projektu zapewnia później dostęp do modułu. Zwróć uwagę, że podczas klonowania repozytorium używasz ostatniego argumentu powłoki w celu nadania nazwy katalogowi, w którym ma zostać umieszczony sklonowany kod. Teraz już wiesz, jak dodawać projekty do serwisu GitHub, jak utworzyć repozytorium w GitHub, jak utworzyć i dodać pliki do repozytorium Git w komputerze lokalnym oraz jak przekazać wprowadzone zmiany do zdalnego repozytorium. W internecie znajdziesz zasoby, dzięki którym będziesz potrafił wykorzystać system kontroli wersji w jeszcze większym stopniu. Jeżeli szukasz dokładnych informacji z zakresu użycia systemu Git, przeczytaj książkę „Pro Git” jednego z założycieli serwisu GitHub, czyli Scotta Chacona. Można ją kupić lub przeczytać bezpłatnie w internecie (http://git-scm.com/book/pl/). Jeżeli wolisz podejście bardziej praktyczne, zapoznaj się z dokumentacją dostępną w oficjalnej witrynie systemu Git (http://git-scm.com/documentation), gdzie wymieniono listę wielu samouczków, dzięki którym rozpoczniesz pracę z Git.
14.2.3. Współpraca przez serwis GitHub Skoro już wiesz, jak zupełnie od początku utworzyć repozytorium GitHub, zobacz, jak można je wykorzystać do współpracy z innymi. Przyjmujemy założenie, że używasz modułu opracowanego przez firmę trzecią i
natknąłeś się na błąd. Możesz przejrzeć kod źródłowy modułu i spróbować znaleźć sposób na usunięcie błędu. Następnie wysyłasz wiadomość e-mail do autora modułu, opisujesz znaleziony błąd i dołączasz pliki zawierające przygotowane poprawki. Takie podejście wymaga od autora wykonania pewnej żmudnej pracy. Autor porównuje otrzymane pliki z najnowszą wersją kodu, a następnie umieszcza w niej poprawki otrzymane od Ciebie. Jeżeli autor korzysta z serwisu GitHub, wówczas możesz sklonować repozytorium projektu autora, wprowadzić pewne zmiany, a następnie przez GitHub poinformować autora o wprowadzeniu poprawki usuwającej znaleziony błąd. GitHub pozwoli autorowi na wyświetlenie różnic między oryginalnym kodem i przygotowanym przez Ciebie. Jeżeli poprawka zostanie zaakceptowana, przygotowane przez Ciebie zmiany zostaną za pomocą pojedynczego kliknięcia myszą umieszczone w najnowszej wersji kodu projektu przygotowanego przez autora modułu. W żargonie GitHub duplikowanie repozytorium nosi nazwę rozwidlenia (ang. forking). Rozwidlenie projektu pozwala na wprowadzenie dowolnych zmian we własnej kopii projektu bez obaw o uszkodzenie oryginalnego repozytorium. Nie musisz mieć zgody autora repozytorium na utworzenie rozwidlenia: każdy może utworzyć rozwidlenie projektu i wprowadzone przez siebie zmiany przekazać autorowi. Autor wcale nie musi zaakceptować otrzymanych zmian, ale nawet wówczas zachowujesz własną wersję z wprowadzonymi zmianami, którą możesz samodzielnie obsługiwać i rozwijać. Jeżeli rozwidlona wersja projektu zyska popularność, inni mogą tworzyć jego rozwidlenia i Tobie oferować wprowadzane przez siebie usprawnienia. Po wprowadzeniu zmian w rozwidleniu projektu możesz je przekazać autorowi oryginalnego projektu poprzez tak zwane żądanie przekazania zmian (ang. pull request), które jest wiadomością skierowaną do autora repozytorium i zawierającą propozycję wprowadzenia zmian. W żargonie GitHub pull oznacza import zmian z rozwidlenia i połączenie ich z kodem w innym rozwidleniu. Na rysunku 14.5 pokazano przykład typowej współpracy za pomocą GitHub. Przeanalizujemy teraz przykład rozwidlenia repozytorium GitHub na potrzeby współpracy nad projektem. Proces został pokazany na rysunku 14.6. Rozwidlenie rozpoczyna proces współpracy przed powieleniem repozytorium GitHub na własnym koncie (co nosi nazwę rozwidlenia) — patrz krok A. Następnie rozwidlone repozytorium zostaje sklonowane do komputera lokalnego (B), w którym po wprowadzeniu zmian są one zatwierdzane (C). Kolejnym krokiem jest przekazanie wprowadzonych zmian z powrotem do serwisu GitHub (D) i wysłanie żądania przekazania zmian autorowi oryginalnego repozytorium z prośbą o rozważenie ich uwzględnienia (E). Jeżeli autor będzie chciał umieścić zmiany w oryginalnym repozytorium, wtedy zaakceptuje wspomniane żądanie przekazania zmian.
Rysunek 14.5. Typowa współpraca przez serwis GitHub
Rysunek 14.6. Proces współpracy nad repozytorium GitHub przez utworzenie rozwidlenia
Przyjmujemy założenie, że chcesz utworzyć rozwidlenie repozytorium node-elf przygotowanego wcześniej w rozdziale, a następnie dodać kod odpowiedzialny za eksport wersji modułu. W ten sposób użytkownicy modułu zyskają pewność, że używają jego odpowiedniej wersji. Przede wszystkim zaloguj się w serwisie GitHub i przejdź na stronę główną r epoz ytor ium https://github.com/mcantelon/node-elf. Znajdziesz na niej
przycisk Fork duplikujący repozytorium. Strona wynikowa będzie podobna do strony oryginalnego repozytorium, ale z komunikatem w stylu forked from mcantelon/node-elf wyświetlanym pod nazwą repozytorium. Po utworzeniu rozwidlenia kolejne kroki to utworzenie kopii repozytorium w komputerze lokalnym, wprowadzenie zmian, a następnie przekazanie ich z powrotem do GitHub. Poniższe polecenia pokazują, jak to zrobić w przypadku repozytorium node-elf: mkdir -p ~/tmp/forktest cd ~/tmp/forktest git clone [email protected]:chickentown/node-elf.git cd node-elf echo "exports.version = '0.0.2';" >> index.js git add index.js git commit -m "Dodano specyfikację wersji modułu." git push origin master
Po wprowadzeniu zmian kliknij przycisk Pull Request na stronie rozwidlenia repozytorium, podaj temat i treść wiadomości, opisując dokonane zmiany. Następnie kliknij przycisk Send Pull Request. Na rysunku 14.7 pokazano przykład typowej treści wiadomości.
Rysunek 14.7. Szczegóły dotyczące żądania przekazania zmian do repozytorium GitHub
Żądanie przekazania kodu zostanie umieszczone na stronie problemów oryginalnego repozytorium. Opiekun oryginalnego repozytorium może po przejrzeniu proponowanych zmian wprowadzić je w repozytorium, klikając przycisk Merge Pull Request, a następnie podać informacje związane z zatwierdzanymi zmianami i kliknąć Confirm Merge. W ten sposób zgłoszenie będzie automatycznie zamknięte. Po współpracy z innymi i utworzeniu wspaniałego modułu kolejnym krokiem jest udostępnienie go społeczności. Najlepszym rozwiązaniem jest wtedy dodanie modułu do repozytorium npm.
14.3. Przekazanie własnego modułu do repozytorium npm
Przypuśćmy, że od pewnego czasu pracowałeś nad modułem przeznaczonym do skracania adresów URL. Teraz uważasz, że moduł w obecnej wersji może być użyteczny dla innych programistów Node. Aby go opublikować, na grupie Google związanej z Node możesz umieścić opis funkcjonalności modułu. W ten sposób jednak ograniczysz liczbę osób, do których dotrzesz. Ponadto nie będziesz miał łatwego sposobu na poinformowanie osób, które zaczną korzystać z Twojego modułu, o wydaniu jego nowszej wersji. Rozwiązaniem problemów związanych z udostępnianiem modułu i jego uaktualnianiem jest publikacja w repozytorium npm. Za pomocą repozytorium npm można bardzo łatwo zdefiniować zależności projektu i pozwolić na ich automatyczną instalację wraz z modułem. Jeżeli utworzyłeś moduł przeznaczony do przechowywania komentarzy dotyczących treści (na przykład postów bloga), możesz wówczas dołączyć jako zależność moduł obsługujący bazę danych MongoDB przeznaczoną na komentarze. W przypadku modułu dostarczającego narzędzie działające z poziomu powłoki zależnością może być moduł pomocniczy przeznaczony do przetwarzania argumentów powłoki. Jak dotąd w książce używaliśmy repozytorium npm do instalacji wszystkiego, począwszy od testowanych frameworków, aż po sterowniki baz danych. Jednak niczego jeszcze nie opublikowaliśmy. W kolejnych punktach przeczytasz, jak wykonać wymienione poniżej kroki w celu publikacji własnej pracy w repozytorium npm. 1. Przygotowanie pakietu. 2. Przygotowanie specyfikacji pakietu. 3. Testowanie pakietu. 4. Publikacja pakietu. Zaczynamy od przygotowania pakietu.
14.3.1. Przygotowanie pakietu Każdy moduł Node, który masz zamiar udostępnić światu, powinien składać się z odpowiednich zasobów, takich jak dokumentacja, przykłady, testy i powiązane z modułem narzędzia działające w powłoce. Ponadto moduł powinien zawierać p l i k README wraz z wystarczającą ilością informacji, aby można było umożliwić użytkownikom szybkie rozpoczęcie pracy z modułem. Katalog modułu powinien być zorganizowany za pomocą podkatalogów. W tabeli 14.2 przedstawiono zgodne z konwencją podkatalogi projektu Node — bin, docs, example, lib i test — oraz ich przeznaczenie. Tabela 14.2. Zgodne z konwencją podkatalogi projektu Node Katalog
Opis
bin
Skrypty powłoki.
docs
Dokumentacja.
example
Przykład użycia aplikacji.
lib
Podstawowa funkcjonalność aplikacji.
test
Skrypty testowe i powiązane z tym zasoby.
Po przygotowaniu odpowiedniej struktury dla pakietu należy go przygotować do publikacji w repozytorium npm poprzez utworzenie specyfikacji pakietu.
14.3.2. Przygotowanie specyfikacji pakietu Kiedy publikujesz pakiet w repozytorium npm, konieczne jest dołączenie czytelnego dla komputera pliku specyfikacji pakietu. Wspomniany plik w formacie JSON nosi nazwę package.json i zawiera informacje o module, takie jak nazwa, opis, wersja, zależności, a także inne cechy charakterystyczne. Nodejitsu oferuje użyteczną stronę internetową (http://package.json.nodejitsu.com/) pokazującą przykładowy plik package.json i objaśniającą poszczególne fragmenty pliku po umieszczeniu nad nimi kursora myszy. W pliku package.json tylko nazwa i wersja to dane obowiązkowe. Pozostałe informacje są opcjonalne, ale jeśli niektóre z nich zostaną podane, to znacznie zwiększą użyteczność modułu. Dzięki zdefiniowaniu charakterystyki bin menedżer npm będzie „wiedział”, które pliki pakietu są narzędziami powłoki, i globalnie je udostępni. Przykładowa specyfikacja pakietu może przedstawiać się następująco: { "name": "elf" , "version": "0.0.1" , "description": "Toy URL shortener" , "author": "Mike Cantelon
Więcej dokładnych informacji o dostępnych otrzymasz po wydaniu poniższego polecenia:
opcjach
pliku package.json
npm help json
Ponieważ ręczne wygenerowanie pliku JSON jest tylko nieco bardziej zabawne od ręcznego tworzenia pliku XML, zapoznamy się z pewnymi narzędziami
ułatwiającymi to zadanie. Jednym z nich jest ngen, czyli dostępny w repozytorium npm pakiet, który dodaje polecenie powłoki o nazwie ngen. Po zadaniu kilku pytań narzędzie wygeneruje plik package.json. Ponadto generuje ono także wiele innych plików, które standardowo są umieszczane w pakietach npm, na przykład Readme.md. Instalacja narzędzia ngen następuje po wydaniu poniższego polecenia: npm install -g ngen
Po zainstalowaniu narzędzia ngen uzyskujesz globalne polecenie ngen, które wywołane w katalogu głównym projektu wyświetli kilka pytań, a następnie wygeneruje plik package.json oraz pozostałe, najczęściej stosowane w pakietach Node. Niepotrzebnie wygenerowane pliki można usunąć. Wygenerowany zostaje między innymi plik .gitignore wskazujący pliki i katalogi, które nie powinny być dodawane do repozytorium Git projektu podczas jego publikacji w npm. Ponadto generowany jest plik .npmignore pełniący podobną rolę jak .gitignore i wskazujący pliki, które mają być zignorowane podczas publikacji pakietu w npm. Poniżej przedstawiono przykładowe dane wyjściowe po wydaniu polecenia
ngen:
Project name: elf Enter your name: Mike Cantelon Enter your email: [email protected] Project description: URL shortening library create : /Users/mike/programming/js/shorten/node_modules/.gitignore create : /Users/mike/programming/js/shorten/node_modules/.npmignore create : /Users/mike/programming/js/shorten/node_modules/History.md create : /Users/mike/programming/js/shorten/node_modules/index.js ...
Wygenerowanie pliku package.json to najtrudniejsze zadanie podczas publikacji modułu w repozytorium npm. Po zakończeniu tego kroku jesteś gotowy do opublikowania modułu.
14.3.3. Testowanie i publikowanie pakietu Opublikowanie modułu w repozytorium npm obejmuje trzy wymienione poniżej kroki, które zostaną omówione w tym punkcie: 1. Przetestowanie instalacji lokalnej pakietu. 2. Dodanie użytkownika
npm,
o ile jeszcze tego nie zrobiłeś.
3. Publikacja modułu w repozytorium npm.
Testowanie instalacji pakietu Aby przetestować lokalną instalację pakietu, należy użyć polecenia
link
menedżera npm i wydać je z poziomu katalogu głównego modułu. To polecenie spowoduje globalne udostępnienie modułu w komputerze i Node będzie mogło z niego korzystać tak jak w przypadku pakietu konwencjonalnie zainstalowanego przez npm. sudo npm link
Po globalnym udostępnieniu pakietu można go zainstalować w oddzielnym katalogu testowym, używając poniższego polecenia link wraz z nazwą pakietu: npm link elf
Po instalacji pakietu szybki test polega na użyciu funkcji require() w interfejsie REPL Node, jak przedstawiono w poniższym wierszu kodu. Wynikiem są zmienne lub funkcje dostarczane przez moduł: node > require('elf'); { version: '0.0.1', initPathData: [Function], shorten: [Function], expand: [Function] }
Jeżeli pakiet przeszedł test i zakończyłeś już nad nim prace, wtedy z poziomu katalogu głównego modułu należy wydać polecenie unlink menedżera npm: sudo npm unlink
Teraz moduł nie będzie już dłużej dostępny globalnie w komputerze. Po zakończeniu procesu publikacji pakietu w repozytorium npm będziesz mógł go zainstalować w standardowy sposób, czyli przez wydanie polecenia install menedżera npm. Po przetestowaniu pakietu kolejnym krokiem jest utworzenie konta npm przeznaczonego do publikacji pakietów, o ile takiego konta nie utworzyłeś już wcześniej.
Dodanie użytkownika npm Poniższe polecenie powoduje utworzenie Twojego przeznaczonego do publikacji pakietu w repozytorium npm:
własnego
konta
npm adduser
Zostaniesz poproszony o podanie nazwy użytkownika, adresu e-mail i hasła do konta. Jeżeli operacja tworzenia konta zakończy się powodzeniem, na ekranie nie będzie wyświetlony żaden komunikat błędu.
Publikacja w repozytorium npm Kolejnym krokiem jest publikacja modułu. Wydanie poniższego polecenia powoduje opublikowanie modułu:
npm publish
Na ekranie zostanie wyświetlony komunikat Sending authorization over an insecure channel (wysyłanie danych uwierzytelniających przez niezabezpieczony kanał), ale jeśli nie zobaczysz żadnych dodatkowych komunikatów błędu, będzie to oznaczało, że publikacja modułu zakończyła się powodzeniem. Sukces publikacji modułu możesz potwierdzić za pomocą polecenia view menedżera npm: npm view elf description
Jeżeli chcesz, możesz dołączyć jedno lub więcej prywatnych repozytoriów jako zależności pakietu. Tego rodzaju sytuacja występuje, gdy być może masz moduł z użytecznymi funkcjami pomocniczymi, których chcesz używać, ale nie chcesz udostępniać ich publicznie w repozytorium npm. Aby dodać zależność w postaci prywatnego repozytorium, w miejscu przeznaczonym do umieszczania nazw zależności modułu podaj dowolną nazwę inną niż pozostałe zależności. Tam, gdzie normalnie umieszcza się wersję pakietu, podaj adres URL repozytorium Git. W przedstawionym poniżej przykładzie będącym fragmentem pliku package.json ostatnia zależność przedstawia repozytorium prywatne: "dependencies" : { "optimist" : ">=0.1.3", "iniparser" : ">=1.0.1", "mingy": ">=0.1.2", "elf": "git://github.com/mcantelon/node-elf.git" },
Pamiętaj, że wszelkie moduły prywatne również powinny zawierać pliki package.json. Aby upewnić się, że tego rodzaju moduły nie zostaną przypadkowo opublikowane, w pliku package.json przypisz właściwości private wartość true: "private": true,
W ten sposób już wiesz, jak skonfigurować, przetestować i opublikować własny moduł w repozytorium npm.
14.4. Podsumowanie Podobnie jak jest w przypadku większości projektów open source, które osiągnęły sukces, także Node charakteryzuje się aktywną społecznością w internecie. Oznacza to dostępność ogromnej ilości zasobów internetowych, a także możliwość szybkiego znalezienia odpowiedzi na nurtujące Cię pytania. Do wspomnianych zasobów zaliczamy między innymi grupy Google, kanały IRC i strony zgłaszania błędów w GitHub.
Poza miejscem zgłaszania błędów serwis GitHub oferuje również możliwość hostingu repozytorium Git i przeglądania kodu repozytorium Git za pomocą przeglądarki internetowej. Dzięki GitHub inni programiści mogą łatwo tworzyć rozwidlenia Twojego kodu open source, aby wprowadzać w nim poprawki, dodawać nowe funkcje lub w ogóle skierować projekt na nowe tory. Zmiany wprowadzone w kodzie można bardzo łatwo przekazywać z powrotem do oryginalnego repozytorium. Od chwili udostępnienia użytkownikom projektu Node mogą oni przekazywać własne moduły do repozytorium Node Package Manager. Dzięki publikacji modułu w npm inni użytkownicy łatwiej go znajdą. A jeśli Twój projekt jest modułem, umieszczenie go w repozytorium npm znacznie ułatwi instalację modułu. Teraz już wiesz, gdzie szukać pomocy, jak współpracować z innymi przez internet oraz jak dzielić się owocami swojej pracy. Node istnieje dzięki aktywnej i zaangażowanej w prace nad nim społeczności. Zachęcamy Cię, abyś stał się aktywnym członkiem wspomnianej społeczności Node!
Dodatek A Instalacja Node i dodatki opracowane przez społeczność Node można bardzo łatwo zainstalować w większości systemów operacyjnych za pomocą konwencjonalnych aplikacji instalatorów lub też z użyciem wiersza poleceń. Drugie z wymienionych rozwiązań jest bardzo łatwe do wykonania w systemach OS X i Linux, ale niezalecane w Windows. Aby pomóc Ci w rozpoczęciu pracy, w tym dodatku dokładnie omówiono instalację Node w systemach operacyjnych OS X, Windows i Linux. Na końcu dodatku dowiesz się, jak używać menedżera pakietów Node (npm) do wyszukiwania i instalacji użytecznych dodatków.
A.1. Instalacja w systemie OS X Instalacja Node w systemie OS X jest bardzo prosta. Pokazany na rysunku A.1 oficjalny instalator jest dostępny na stronie http://nodejs.org/download/ i pozwala na łatwą instalację skompilowanej wersji Node i menedżera npm. Jeżeli jednak wolisz przeprowadzić instalację ze źródeł, możesz skorzystać z narzędzia o nazwie Homebrew (http://brew.sh/), które automatyzuje proces instalacji ze źródeł. Ewentualnie przeprowadź ręczną instalację ze źródeł. Warto w tym miejscu wspomnieć, że instalacja Node ze źródeł w systemie OS X wymaga posiadania zainstalowanych w systemie narzędzi programistycznych OS X. Xcode. Jeżeli nie masz jeszcze zainstalowanego środowiska programistycznego Xcode, możesz je pobrać ze strony Apple (http://developer.apple.com/downloads/). Uzyskanie dostępu do wymienionej strony wymaga przeprowadzenia bezpłatnej rejestracji konta programisty na platformie Apple. Xcode to całkiem duża aplikacja (około 4 GB). Alternatywnym rozwiązaniem oferowanym przez Apple jest pakiet Command Line Tools for Xcode, który można pobrać z tej samej strony. Wymieniony pakiet zawiera minimalną ilość narzędzi potrzebnych do kompilacji Node oraz innych projektów oprogramowania typu open source.
Rysunek A.1. Oficjalny instalator Node dla systemu OS X
Aby szybko sprawdzić, czy masz zainstalowaną w systemie aplikację Xcode, uruchom narzędzie Terminal, a następnie wydaj polecenie xcodebuild. Jeżeli masz zainstalowaną aplikację Xcode, otrzymasz komunikat błędu informujący, że w katalogu bieżącym nie znaleziono projektu Xcode. Podczas instalacji konieczne może być wydawanie poleceń z poziomu powłoki. Do tego celu wykorzystaj narzędzie Terminal, które standardowo znajduje się w katalogu Programy/Narzędzia. Jeżeli przeprowadzasz kompilację ze źródeł, zajrzyj do podrozdziału A.4, w którym znajdziesz omówienie kroków niezbędnych do wykonania.
A.1.1. Instalacja za pomocą Homebrew W systemie OS X Node można bardzo łatwo zainstalować za pomocą Homebrew, czyli menedżera pakietów przeznaczonego do instalacji oprogramowania typu open source. Pierwszym krokiem jest instalacja samego menedżera Homebrew, co wymaga wydania poniższego polecenia w powłoce: ruby -e "$(curl -fsSkL raw.github.com/mxcl/homebrew/go)"
Gdy menedżer Homebrew znajduje się już w systemie, instalację Node możesz przeprowadzić, wydając polecenie: brew install node
Podczas kompilacji kodu przez Homebrew w oknie narzędzia Terminal będzie wyświetlana duża ilość tekstu, który możesz przewijać. Wspomniany tekst zawiera informacje związane z procesem kompilacji; możesz je spokojnie zignorować.
A.2. Instalacja w systemie Windows W systemie Windows instalację Node można przeprowadzić za pomocą oficjalnego instalatora dostępnego na stronie http://nodejs.org/download/. Po instalacji Node i menedżera npm będziesz mógł uruchamiać z poziomu wiersza poleceń Windows. Rozwiązanie alternatywne polega na instalacji Node przez przeprowadzenie kompilacji ze źródeł. Takie podejście jest znacznie bardziej skomplikowane i wymaga użycia projektu o nazwie Cygwin, który dostarcza środowisko zgodne z systemem UNIX. Niemal na pewno będziesz chciał uniknąć użycia Node za pomocą projektu Cygwin, o ile nie próbujesz używać modułów, które w Windows nie działają w innej konfiguracji lub wymagają kompilacji (przykładem mogą być tutaj moduły sterowników pewnych baz danych). Aby przeprowadzić instalację Cygwin, przejdź na stronę http://cygwin.com/install.html, a następnie pobierz plik setup.exe. Dwukrotne kliknięcie pobranego pliku powoduje uruchomienie kreatora instalacji. Naciskaj przycisk Dalej, zatwierdzając tym samym opcje domyślne, aż do chwili dotarcia do kroku o nazwie Choose a Download Site. Wybierz dowolną witrynę z listy, a następnie kliknij przycisk Dalej. Jeżeli kreator wyświetli komunikat ostrzeżenia, naciśnij przycisk OK, aby kontynuować proces instalacji. Kreator powinien teraz wyświetlić pokazane na rysunku A.2 okno wyboru pakietów Cygwin.
Rysunek A.2. Okno wyboru pakietów Cygwin pozwala na wskazanie oprogramowania open source, które zostanie zainstalowane w systemie
Korzystając z wymienionego okna, wybierz pakiety, które mają znaleźć się w tworzonym środowisku zgodnym z systemem UNIX. Listę pakietów potrzebnych do pracy z Node przedstawiono w tabeli A.1. Tabela A.1. Pakiety Cygwin niezbędne do uruchomienia Node Kategoria
Pakiet
devel
gcc4-g++
devel
git
devel
make
devel
openssl-devel
devel
pkg-config
devel
zlib-devel
net
inetutils
python
python
web
wget
Po wybraniu wymaganych pakietów kliknij przycisk Dalej.
W kolejnym kroku zostanie wyświetlona lista pakietów będących zależnościami dla wybranych wcześniej. Ponieważ je również musisz zainstalować, kliknij pr z ycisk Dalej, aby zaakceptować wskazane pakiety. Cygwin rozpocznie pobieranie potrzebnych pakietów. Kiedy pobieranie zakończy się, naciśnij przycisk Zakończ. Uruchom Cygwin, klikając ikonę umieszczoną na pulpicie lub w menu Start. Na ekranie zostanie wyświetlony wiersz poleceń. Teraz możesz już przystąpić do kompilacji Node (wymagane kroki zostały omówione w podrozdziale A.4).
A.3. Instalacja w systemie Linux Instalacja Node w systemie Linux zwykle jest bardzo prosta. W tym dodatku przedstawiono instalację na podstawie kodu źródłowego w dwóch popularnych dystrybucjach: Ubuntu i CentOS. Node jest dostępne także za pomocą menedżerów pakietów w wielu innych dystrybucjach. Informacje dotyczące instalacji znajdziesz w serwisie GitHub: https://github.com/joyent/node/wiki/Installing-Node.js-via-package-manager.
A.3.1. Przygotowania do instalacji w Ubuntu Przed rozpoczęciem instalacji Node w Ubuntu konieczna jest instalacja wymaganych pakietów. W systemie Ubuntu 11.04 lub nowszym wystarczy wydać poniższe polecenie: sudo apt-get install build-essential libssl-dev
sudo — polecenie sudo jest używane do wykonania innego polecenia z uprawnieniami superużytkownika (nazywanego również root). Polecenie sudo jest bardzo często stosowane podczas instalacji oprogramowania, ponieważ pliki muszą być umieszczane w chronionych obszarach systemu plików, a użytkownik root ma dostęp do każdego pliku w systemie niezależnie od uprawnień zdefiniowanych dla pliku.
A.3.2. Przygotowania do instalacji w CentOS Przed instalacją Node w CentOS konieczna jest instalacja wymaganych pakietów. W systemie CentOS 5 lub nowszym wystarczy wydać poniższe polecenia: sudo yum groupinstall 'Development Tools' sudo yum install openssl-devel
Po zainstalowaniu wymaganych pakietów można przystąpić do kompilacji Node.
A.4. Kompilacja Node Kroki niezbędne do przeprowadzenia kompilacji Node są takie same we wszystkich systemach operacyjnych. W powłoce należy wydać poniższe polecenie powodujące utworzenie katalogu tymczasowego, do którego pobierzemy kod źródłowy Node: mkdir tmp
Kolejnym krokiem jest przejście do utworzonego wcześniej katalogu: cd tmp
Teraz wydaj poniższe polecenie, aby pobrać kod źródłowy Node: curl -O http://nodejs.org/dist/node-latest.tar.gz
Na ekranie będą wyświetlane informacje o postępie podczas pobierania pliku. Po pobraniu pliku w powłoce zostanie wyświetlony znak zachęty. Wydanie poniższego polecenia powoduje rozpakowanie pobranego archiwum: tar zxvf node-latest.tar.gz
W trakcie rozpakowywania na ekranie będzie wyświetlona duża ilość danych wyjściowych operacji, a następnie ponownie zobaczysz znak zachęty. Poniższe polecenie powoduje wyświetlenie listy plików znajdujących się w katalogu bieżącym, między innymi nazwy rozpakowanego przed chwilą katalogu: ls
Wydanie poniższego polecenia powoduje przejście do wspomnianego katalogu: cd node-v*
W tym momencie znajdujesz się w katalogu zawierającym kod źródłowy Node. Wydanie poniższego polecenia powoduje uruchomienie skryptu konfiguracyjnego, który przygotowuje instalację oprogramowania dla używanego przez Ciebie systemu operacyjnego: ./configure
Kompilacja Node jest prosta i wymaga wydania poniższego polecenia: make
Proces kompilacji może wymagać nieco czasu, a więc zachowaj cierpliwość. W trakcie kompilacji na ekranie będzie wyświetlana ogromna ilość komunikatów. Wspomniane komunikaty zawierają informacje związane z procesem kompilacji i możesz je spokojnie zignorować. Niedoskonałość Cygwin. Jeżeli uruchomiłeś Cygwin w systemie Windows 7 lub Vista, w trakcie kompilacji mogą wystąpić błędy. Wynikają one z problemów związanych z Cygwin, a nie Node. Rozwiązaniem jest zamknięcie wiersza poleceń Cygwin, a następnie uruchomienie aplikacji wiersza poleceń
ash.exe (znajdziesz ją w katalogu Cygwin, najczęściej c:\cygwin\bin\ash.exe). W uruchomionym wierszu poleceń wydaj polecenie /bin/rebaseall -v. Po wykonaniu polecenia uruchom ponownie komputer. To powinno rozwiązać problemy związane z Cygwin. Na tym etapie już prawie zakończyliśmy instalację. Kiedy przestaną pojawiać się nowe komunikaty i ponownie zobaczysz znak zachęty, wydaj ostatnie polecenie w procesie instalacji Node: sudo make install
Po wykonaniu powyższego polecenia wydanie poniższego spowoduje wyświetlenie numeru wersji Node. Dzięki temu można potwierdzić, że instalacja zakończyła się powodzeniem. node -v
W ten sposób zainstalowałeś Node w systemie!
A.5. Używanie menedżera pakietów Node Po zainstalowaniu Node zyskasz możliwość użycia wbudowanych modułów zapewniających dostęp do API przeznaczonego do wykonywania zadań związanych z siecią, pracy z systemem plików oraz innych operacji najczęściej wymaganych w aplikacjach. Wbudowane moduły Node są określane mianem podstawowych modułów Node. Wprawdzie oferują one wiele użytecznych funkcji, ale prawdopodobnie będziesz chciał korzystać także z funkcji opracowanych przez społeczność. Na rysunku A.3 pokazano koncepcję relacji między modułami podstawowymi Node i dodatkowymi. W zależności od używanego języka programowania idea opracowywanych przez społeczność repozytoriów zawierających moduły z funkcjami dodatkowymi może być Ci znana lub nieznana. Wspomniane repozytoria przypominają biblioteki i stanowią użyteczne bloki budulcowe aplikacji pomagające w realizacji zadań, których nie można w łatwy sposób wykonać za pomocą standardowego języka. Repozytoria są najczęściej modułowe: zamiast od razu pobierać całą bibliotekę, wystarczy pobrać jedynie niezbędne dodatki. Społeczność Node opracowała własne narzędzie przeznaczone do zarządzania dodatkami: menedżer npm (ang. Node Package Manager). W tym podrozdziale dowiesz się, jak używać menedżera npm do wyszukiwania dodatków, przeglądania dokumentacji dodatków, a także analizy kodu źródłowego dodatków.
Rysunek A.3. Stos Node składa się z funkcji dostępnych globalnie, modułów podstawowych oraz opracowanych przez społeczność
W moim systemie nie ma menedżera npm Jeżeli zainstalowałeś Node, to menedżer npm również został zainstalowany. Możesz się o tym przekonać, wydając polecenie npm w powłoce i sprawdzając, jaka będzie odpowiedź powłoki. Jeśli faktycznie nie masz menedżera npm w systemie, jego instalacja wymaga wydania poniższych poleceń: cd /tmp git clone git://github.com/isaacs/npm.git cd npm sudo make install
Po zainstalowaniu menedżera npm wydaj poniższe polecenie, aby upewnić się, że działa (polecenie nakazuje wyświetlenie numeru wersji menedżera): npm -v
Jeżeli menedżer jest zainstalowany prawidłowo, powinieneś zobaczyć dane
wyjściowe (numer wersji) podobne do poniższych: 1.0.3
W przypadku jakichkolwiek problemów z instalacją npm najlepszym rozwiązaniem jest przejście na stronę projektu npm w serwisie GitHub (https://github.com/npm/npm), na której znajdziesz aktualne informacje dotyczące instalacji menedżera.
A.5.1. Wyszukiwanie pakietów Menedżer npm oferuje wygodne rozwiązanie w zakresie uzyskiwania dostępu do dodatków Node opracowanych przez społeczność. Wspomniane dodatki są nazywane pakietami i są przechowywane w repozytorium. Dla użytkowników PHP, Ruby i Perla menedżer npm można określić jako odpowiedniki narzędzi PEAR, Gem i CPAN. Menedżer npm jest niezwykle wygodnym narzędziem. Za jego pomocą można pobierać i instalować pakiety, wydając tylko jedno polecenie. Bardzo łatwo można również wyszukiwać pakiety, przeglądać ich dokumentację, zapoznawać się z kodem źródłowym pakietu, a nawet publikować własne, udostępniając je tym samym całej społeczności Node. Do wyszukiwania pakietów znajdujących się w repozytorium służy polecenie search. Na przykład jeśli chcesz znaleźć generator XML, wtedy możesz wydać poniższe, proste polecenie: npm search xml generator
Pierwsze użycie menedżera npm do operacji wyszukiwania będzie trwało nieco dłużej, ponieważ konieczne jest pobranie informacji o repozytorium. Jednak kolejne operacje wyszukiwania będą przeprowadzane już znacznie szybciej. Alternatywnym rozwiązaniem dla polecenia wydawanego w powłoce jest interfejs wyszukiwania dostępny przez przeglądarkę internetową. Wystarczy przejść na witrynę https://www.npmjs.org/. Podana witryna (pokazana na rysunku A.4) dostarcza również dane statystyczne dotyczące pakietów, między innymi całkowitą liczbę pakietów, pakiety będące najczęściej zależnościami dla innych pakietów, a także ostatnio uaktualnione.
Rysunek A.4. Witryna npmjs.org podaje użyteczne dane statystyczne dotyczące pakietów
Interfejs w postaci wymienionej witryny internetowej pozwala także na przeglądanie poszczególnych pakietów, wyświetla użyteczne informacje o nich, takie jak zależności, oraz podaje położenie repozytorium kontroli wersji dla pakietu.
A.5.2. Instalacja pakietu Po znalezieniu interesującego Cię pakietu istnieją dwa podstawowe sposoby jego instalacji: lokalnie lub globalnie. Instalacja lokalna powoduje umieszczenie pobranego modułu w katalogu o nazwie node_modules, znajdującym się w bieżącym katalogu roboczym. Jeżeli wymieniony katalog nie istnieje, wtedy menedżer npm utworzy go. Poniżej przedstawiono przykład instalacji lokalnej pakietu o nazwie
express:
npm install express
W systemach operacyjnych innych niż Windows instalacja globalna powoduje umieszczenie pobranego modułu w katalogu o nazwie /usr/local. Wymieniony katalog jest tradycyjnie używany przez systemy z rodziny UNIX do przechowywania aplikacji instalowanych przez użytkownika. Z kolei w systemach Windows dla modułów instalowanych globalnie jest przeznaczony katalog Appdata\Roaming\npm. Poniżej przedstawiono przykład instalacji globalnej pakietu o nazwie
express:
npm install -g express
Jeżeli nie masz wystarczających uprawnień do przeprowadzenia instalacji globalnej, to na początku polecenia powinieneś umieścić sudo, na przykład: sudo npm install -g express
Po zainstalowaniu pakietu kolejnym krokiem jest ustalenie sposobu jego działania. Na szczęście dzięki menedżerowi npm jest to łatwe zadanie.
A.5.3. Przeglądanie dokumentacji i kodu pakietu Menedżer npm oferuje wygodny sposób przeglądania dokumentacji pakietu, o ile taka jest dostępna. Polecenie docs powoduje otworzenie przeglądarki internetowej, w której zostaje wyświetlona dokumentacja wskazanego pakietu. Poniżej przedstawiono przykład wyświetlania dokumentacji pakietu o nazwie express: npm docs express
Dokumentację można wyświetlić, nawet jeśli pakiet nie został zainstalowany. Jeżeli dokumentacja pakietu jest niekompletna lub niejasna, wtedy często użytecznym rozwiązaniem jest przejrzenie jego kodu źródłowego. Menedżer npm ułatwia także i to zadanie. Powoduje utworzenie podpowłoki, w której katalogiem najwyższego poziomu jest bieżący katalog roboczy plików kodu źródłowego pakietu. Poniżej przedstawiono przykład przeglądania plików kodu źródłowego lokalnie zainstalowanego pakietu express: npm explore express
Aby przeglądać kod źródłowy pakietu zainstalowanego globalnie, wystarczy po prostu dodać opcję -g po poleceniu npm, na przykład: npm -g explore express
Analiza kodu źródłowego pakietu do również doskonały sposób nauki. Dzięki przeglądaniu kodu źródłowego Node bardzo często poznajesz nowe techniki programowania oraz organizacji kodu.
Dodatek B Debugowanie Node Podczas prac programistycznych, a zwłaszcza w trakcie poznawania nowego języka lub frameworka, narzędzia i techniki debugowania mogą być niezwykle użyteczne. W tym dodatku poznasz sposoby ustalenia, co tak naprawdę dzieje się w aplikacji Node.
B.1. Analiza kodu za pomocą JSHint Błędy związane ze składnią lub zasięgiem są poważnym problemem w trakcie programowania. Podczas próby określenia podstawowej przyczyny problemu pierwszą linią obrony jest przejrzenie kodu. Jeśli jednak przejrzysz kod źródłowy i od razu nie wychwycisz błędu, kolejną możliwością wartą wypróbowania jest uruchomienie narzędzia sprawdzającego kod źródłowy pod kątem błędów. JSHint to jedno z tego rodzaju narzędzi. Potrafi poinformować o błędach zarówno poważnych, takich jak wywołania funkcji niezdefiniowanych w kodzie, jak i bardziej błahych, na przykład niestosowanie się do konwencji JavaScript w zakresie wielkości znaków w konstruktorach klas. Nawet jeśli nigdy nie korzystałeś z narzędzia JSHint, przeczytanie o wyszukiwanych przez nie błędach pozwala na zdobycie cennej wiedzy o czyhających pułapkach. JSHint to projekt oparty na JSLint, czyli dostępnym od dekady kodzie źródłowym JavaScript narzędzia analizy. Jednak w przeciwieństwie do JSHint wspomniane JSLint to narzędzie nie oferujące zbyt dużych możliwości w zakresie konfiguracji. W opinii wielu osób narzędzie JSLint jest zbyt rygorystyczne pod względem stylistyki. Z kolei JSHint pozwala na wskazanie, co chcesz sprawdzić, a co ma zostać zignorowane. Na przykład średniki są z technicznego punktu widzenia wymagane przez interpretery JavaScript, ale większość interpreterów stosuje zautomatyzowane wstawianie średników (ang. Automated Semicolon Insertion, ASI) i umieszcza je tam, gdzie ich zabrakło. Z tego powodu niektórzy programiści celowo pomijają je w kodzie źródłowym, aby zwiększyć jego przejrzystość, a sam kod działa bez zastrzeżeń. Narzędzie JSLint będzie uznawało brak średników za błąd, natomiast JSHint można skonfigurować w taki sposób, aby ignorowało ten „błąd” i sprawdzało kod pod kątem innych poważnych błędów. Instalacja JSHint udostępnia polecenie powłoki o nazwie jshint, które sprawdza kod źródłowy. Narzędzie JSHint powinno być zainstalowane globalnie za pomocą menedżera npm przez wydanie poniższego polecenia:
npm install -g jshint
Po instalacji JHint można sprawdzić pliki JavaScript przez wydanie polecenia podobnego do poniższego: jshint aplikacja.js
W większości przypadków będziesz chciał utworzyć plik konfiguracyjny dla JSHint wskazujący to, co powinno być sprawdzone. Jednym z możliwych rozwiązań jest skopiowanie do komputera lokalnego domyślnego pliku konfiguracyjnego dostępnego w serwisie GitHub (https://github.com/jshint/node-jshint/blob/master/.jshintrc), a następnie jego modyfikacja. Jeżeli przygotowany plik konfiguracyjny nazwiesz .jshintrc i umieścisz w katalogu aplikacji lub w dowolnym katalogu nadrzędnym dla aplikacji, to narzędzie automatycznie znajdzie ten plik i go użyje. Alternatywne rozwiązanie polega na użyciu opcji config i wskazaniu położenia pliku konfiguracyjnego. Poniższy przykład wywołania JSHint nakazuje narzędziu użycie pliku konfiguracyjnego o niestandardowej nazwie: jshint aplikacja.js --config /home/michal/jshint.json
Więcej informacji na temat konkretnych opcji konfiguracyjnych znajdziesz na stronie http://www.jshint.com/docs/#options.
B.2. Dane wyjściowe debugowania Gdy kod źródłowy wydaje się prawidłowy, ale aplikacja nadal działa w sposób inny od oczekiwanego, wówczas warto wyświetlić dane wyjściowe procesu debugowania, aby jeszcze dokładniej dowiedzieć się, co tak naprawdę dzieje się z aplikacją.
B.2.1. Debugowanie za pomocą modułu console Node zawiera wbudowany moduł console, który oferuje funkcje użyteczne podczas debugowania i wyświetlania danych wyjściowych w konsoli.
Wyświetlanie informacji o stanie aplikacji Funkcja console.log() jest używana do wyświetlania w standardowym wyjściu danych wyjściowych zawierających informacje o stanie aplikacji. Inna nazwa tej funkcji to console.info(). Istnieje możliwość podania funkcji argumentów w stylu printf() (http://pl.wikipedia.org/wiki/Printf): console.log('Counter: %d', counter);
Podobnie działają funkcje
console.warn()
i console.error(), które są przeznaczone
do wyświetlania ostrzeżeń i błędów. Jedyna różnica polega na tym, że dane są kierowane do standardowego wyjścia błędów zamiast do standardowego wyjścia. Dzięki temu można je przekierować (jeśli występuje taka potrzeba) do pliku dziennika zdarzeń, jak przedstawiono w poniższym przykładzie: node server.js 2> error.log
F unk c j a console.dir() wyświetla zawartość wskazanego pokazano przykładowe dane wyjściowe wymienionej funkcji:
obiektu.
Poniżej
{ name: 'Jan Kowalski', interests: [ 'sport', 'polityka', 'muzyka', 'teatr' ] }
Dane wyjściowe dotyczące pomiaru czasu Moduł console zawiera dwie funkcje, które gdy są używane razem, pozwalają na pomiar czasu wykonywania fragmentów kodu. Jednocześnie można mierzyć więcej niż tylko jeden aspekt. Aby rozpocząć pomiar czasu, poniższy wiersz kodu należy umieścić w miejscu, w którym ma się rozpocząć pomiar: console.time('danyKomponent');
W celu zakończenia pomiaru i podania czasu, jaki upłynął od rozpoczęcia pomiaru, w miejscu zakończenia pomiaru trzeba umieścić wiersz kodu: console.timeEnd('danyKomponent');
Powyższy kod wyświetli zmierzony czas.
Wyświetlanie stosu wywołań Stos wywołań dostarcza informacje o funkcjach wywoływanych przed dotarciem do wskazanego punktu w logice aplikacji. Kiedy w trakcie działania programu Node wystąpi błąd, wówczas może zostać wyświetlony stos wywołań zawierający informacje o tym, co w logice aplikacji doprowadziło do wystąpienia błędu. W dowolnym punkcie aplikacji można wyświetlić stos wywołań bez konieczności zatrzymywania działania aplikacji. W tym celu wystarczy jedynie wywołać funkcję console.trace(). Wspomniane poniższych:
wywołanie
spowoduje
wyświetlenie
Trace: at lastFunction (/Users/mike/tmp/app.js:12:11) at secondFunction (/Users/mike/tmp/app.js:8:3) at firstFunction (/Users/mike/tmp/app.js:4:3) at Object.
danych
podobnych
do
Pamiętaj, że informacje znajdujące się na stosie wywołań są wyświetlane w odwrotnej kolejności chronologicznej.
B.2.2. Użycie modułu debug do zarządzania danymi wyjściowymi procesu debugowania Dane wyjściowe debugowania są użyteczne, ale jeśli akurat nie będziesz aktywnie szukał problemu, wtedy mogą wprowadzać jedynie dodatkowe zamieszanie. Idealnym rozwiązaniem jest możliwość włączania i wyłączania wyświetlania danych wyjściowych debugowania. Jednym ze sposobów włączania i wyłączania danych wyjściowych debugowania jest użycie zmiennej środowiskowej. Opracowany przez T.J. Holowaychuka moduł debug oferuje użyteczne narzędzie do tego celu. Zarządzanie wyświetlaniem danych wyjściowych debugowania następuje za pomocą zmiennej środowiskowej DEBUG. Więcej informacji na ten temat znajdziesz w rozdziale 13.
B.3. Debuger wbudowany w Node Gdy potrzeby w zakresie debugowania wykraczają poza proste dane wyjściowe procesu debugowania, Node oferuje wbudowany debuger działający z poziomu powłoki. Wywołanie debugera następuje przez uruchomienie aplikacji wraz ze słowem kluczowym debug, na przykład: node debug server.js
Po uruchomieniu aplikacji w ten sposób na ekranie zobaczysz kilka pierwszych wierszy aplikacji oraz znak zachęty debugera, jak pokazano na rysunku B.1.
Rysunek B.1. Uruchomienie debugera wbudowanego w Node
Komunikat break in server.js:1 oznacza, że debuger zatrzymał wykonywanie programu przed wykonaniem pierwszego wiesza kodu.
B.3.1. Nawigacja po debugerze Po wyświetleniu znaku zachęty debugera można kontrolować działanie aplikacji. Polecenie next (lub po prostu n) powoduje wykonanie kolejnego wiesza kodu. Z kolei polecenie cont (lub c) powoduje wykonywanie aplikacji, dopóki nie zostanie przerwane. Zatrzymanie debugera może nastąpić przez zakończenie działania aplikacji lub po dojściu do tak zwanego punktu kontrolnego. Wspomniane punkty kontrolne to punkty, w których debuger ma wstrzymać działanie aplikacji, aby można było przeanalizować jej stan. Jednym ze sposobów dodania punktu kontrolnego jest dodanie w aplikacji wiersza, w którym ma zostać umieszczony punkt kontrolny. Wspomniany wiersz powinien zawierać polecenie debugger;, jak przedstawiono w listingu B.1. Wiersz debugger; nie ma żadnego negatywnego wpływu na normalne działanie aplikacji, a więc można go bez obaw pozostawić w kodzie źródłowym programu. Listing B.1. Programowe dodanie punktu kontrolnego var http = require('http'); function handleRequest(req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Witaj, świecie\n'); } http.createServer(function (req, res) { debugger; Dodanie punktu kontrolnego do kodu. handleRequest(req, res); }).listen(1337, '127.0.0.1'); console.log('Serwer jest dostępny pod adresem http://127.0.0.1:1337/');
Jeżeli w trybie debugowania uruchomisz program przedstawiony w listingu B.1, pierwsze wstrzymanie działania nastąpi w wierszu pierwszym. Po wydaniu polecenia cont w debugerze działanie aplikacji będzie kontynuowane do czasu utworzenia serwera i oczekiwania na połączenie. Po nawiązaniu połączenia przez przejście na stronę http://127.0.0.1:1337 w przeglądarce internetowej przekonasz się, że działanie programu zostało wstrzymane w wierszu debugger;. Wydanie polecenia next spowoduje wykonanie kolejnego wiersza kodu. Bieżącym wierszem stanie się wywołanie handleRequest(). Jeżeli znów wydasz polecenie next, aby wykonać kolejny wiersz kodu, debuger nie zejdzie do kolejnego wiersza funkcji handleRequest(). Wydanie polecenia step powoduje przejście do funkcji handleRequest() i pozwala na rozwiązanie wszelkich związanych z nią problemów. Gdy zmienisz zdanie dotyczące debugowania funkcji handleRequest(), wydaj polecenie out (lub o), co spowoduje opuszczenie funkcji.
Punkty kontrolne nie muszą być definiowane jedynie w kodzie źródłowym, można je ustawić także z poziomu debugera. Aby w debugerze ustawić punkt kontrolny w bieżącym wierszu, należy wydać polecenie setBreakpoint() (lub sb()). Istnieje także możliwość ustawienia punktu kontrolnego we wskazanym wierszu (sb(wiersz)) lub w konkretnej funkcji (sb('fn()')). Kiedy
chcesz
usunąć punkt kontrolny, do dyspozycji masz funkcję clearBreakpoint() (cb()). Wymieniona funkcja pobiera takie same argumenty jak setBreakpoint(), ale stosuje je w przeciwnym celu.
B.3.2. Analiza i zmiana stanu w debugerze Jeżeli chcesz monitorować określone wartości w aplikacji, możesz dodać tak zwanych wartowników. Informują oni o wartości zmiennej, gdy poruszasz się po kodzie. Na przykład podczas debugowania kodu przedstawionego w listingu B.1 możesz wydać polecenie watch ("req.headers['user-agent']"). W każdym kroku będziesz widział, jaka przeglądarka internetowa wykonała żądanie. Wyświetlenie listy wartowników następuje po wydaniu polecenia watchers. Aby usunąć wartownika, należy użyć polecenia unwatch, na przykład unwatch("req .headers['user- agent']"). Jeżeli na jakimkolwiek etapie procesu debugowania będziesz chciał w pełni przeanalizować lub zmienić stan, wówczas możesz użyć polecenia repl, aby przejść do interfejsu REPL. W ten sposób zyskujesz możliwość podania dowolnego wyrażenia JavaScript i zostanie ono obliczone. Opuszczenie interfejsu REPL i powrót do debugera następuje po naciśnięciu klawiszy Ctrl+C. Po zakończeniu debugowania opuszczenie debugera następuje po dwukrotnym naciśnięciu klawiszy Ctrl+C, naciśnięciu Ctrl+D lub po wydaniu polecenia .exit. Tutaj przedstawiono jedynie podstawy użycia debugera. Więcej informacji na temat tego, co można zrobić za pomocą debugera, znajdziesz na stronie http://nodejs.org/api/debugger.html.
B.4. Inspektor Node Inspektor Node to alternatywa dla wbudowanego debugera Node. Jest używany przez przeglądarki internetowe zbudowane w oparciu o silnik WebKit, na przykład Chrome i Safari, a nie dostępny jako interfejs z poziomu powłoki.
B.4.1. Uruchomienie inspektora Node Zanim rozpoczniesz debugowanie, powinieneś zainstalować inspektora Node
globalnie za pomocą przedstawionego poniżej polecenia. Po instalacji w systemie będzie dostępne polecenie node-inspector: npm install -g node-inspector
W celu rozpoczęcia debugowania aplikacji Node uruchom ją z opcją powłoce:
--debug-brk
w
node --debug-brk server.js
Użycie opcji --debug-brk powoduje, że proces debugowania wstawi punkt kontrolny przed pierwszym wierszem aplikacji. Jeżeli jest to niepożądane, wtedy zamiast wymienionej można użyć opcji --debug. Po uruchomieniu aplikacji należy uruchomić inspektora Node: node-inspector
Inspektor Node jest interesujący, ponieważ używa tego samego kodu co inspektor sieciowy WebKit. Zostaje jednak umieszczony w silniku JavaScript Node, więc programiści sieciowi powinni czuć się jak w domu podczas jego użycia. Po uruchomieniu inspektora Node przejdź na stronę http://127.0.0.1:8080/debug?port=5858 w przeglądarce internetowej opartej na silniku WebKit, a zobaczysz inspektora. Jeżeli uruchomiłeś go za pomocą opcji --debug-brk, inspektor Node będzie natychmiast wyświetlał pierwszy skrypt aplikacji, jak pokazano na rysunku B.2. W przypadku użycia opcji --debug będziesz miał możliwość wyboru skryptu, co na rysunku B.2 jest wskazywane przez nazwę step.js. W ten sposób możesz wybrać skrypt, który będzie debugowany. Czerwona strzałka pokazana po lewej stronie kodu wskazuje wiersz, który zostanie wykonany jako kolejny.
B.4.2. Nawigacja po inspektorze Node W celu przejścia do kolejnego wywołania funkcji w aplikacji kliknij przycisk wyglądający jak mała zakręcona strzałka. Inspektor Node, podobnie jak debuger Node działający w powłoce, pozwala na wejście do funkcji. Kiedy czerwona strzałka po lewej stronie wiersza kodu wskazuje wywołanie funkcji, wejście do funkcji następuje po kliknięciu małej strzałki skierowanej w dół i wskazującej kółko. Opuszczenie funkcji następuje po kliknięciu przycisku ze strzałką skierowaną w górę. Jeżeli używasz modułów Node wbudowanych lub utworzonych przez społeczność, debuger spowoduje przejście do pliku skryptu modułu, gdy będziesz poruszał się po aplikacji. Nie przejmuj się, w pewnym momencie powrócisz do kodu aplikacji.
Rysunek B.2. Inspektor Node
Aby dodać punkt kontrolny podczas użycia inspektora Node, wystarczy kliknąć numer wiersza po lewej stronie dowolnego wiersza skryptu. Usunięcie wszystkich punktów kontrolnych następuje po kliknięciu przycisku znajdującego się po prawej stronie przycisku opuszczenia funkcji (przekreślona strzałka). Inspektor Node ma również interesującą funkcję, jaką jest możliwość zmiany kodu podczas działania aplikacji. Jeżeli chcesz zmienić wiersz kodu, po prostu dwukrotnie go kliknij, przeprowadź edycję, a następnie kliknij w dowolnym miejscu poza zmodyfikowanym wierszem kodu.
B.4.3. Przeglądanie stanu w inspektorze Node Podczas debugowania aplikacji jej stan można sprawdzać za pomocą rozwijanych paneli znajdujących się pod przyciskami, jak pokazano na rysunku B.3. Wspomniane panele pozwalają na przejrzenie stosu wywołań, zmiennych w zasięgu aktualnie wykonywanego kodu. Modyfikacja zmiennej jest możliwa po jej dwukrotnym kliknięciu i zmianie wartości. Podobnie jak jest w przypadku debugera wbudowanego w Node i działającego w powłoce, istnieje możliwość dodania wartowników wyświetlających wartości podczas poruszania się po aplikacji. Więcej informacji na temat maksymalnego wykorzystania możliwości oferowanych przez inspektora Node znajdziesz na stronie projektu w serwisie GitHub (https://github.com/node-inspector/node-inspector/).
Rysunek B.3. Przeglądanie stanu aplikacji za pomocą inspektora Node
Jeśli masz wątpliwości, odśwież stronę. Jeżeli podczas użycia inspektora Node zauważysz dziwne zachowanie, może pomóc odświeżenie strony w przeglądarce internetowej. Jeżeli to rozwiązanie nie działa, spróbuj ponownie uruchomić zarówno aplikację, jak i inspektora Node.
Dodatek C Rozszerzenie i konfiguracja frameworka Express Framework Express standardowo oferuje wiele użytecznych funkcji, ale jego rozszerzenie i dostrojenie konfiguracji może uprościć programowanie i zapewnić jeszcze większe możliwości.
C.1. Rozszerzenie frameworka Express Na początek przekonajmy się, jak można rozszerzyć framework Express. W tym podrozdziale dowiesz się: • n Jak utworzyć własne silniki szablonów? • n Jak wykorzystać zalety silników szablonów utworzonych przez społeczność? • n Jak usprawnić aplikacje za pomocą modułów rozszerzających frameworka Express?
C.1.1. Rejestracja szablonów silników Silnik może standardowo zapewniać obsługę frameworka Express przez eksport metody __express. Jednak nie każdy silnik szablonów oferuje tego rodzaju możliwość. Ponadto być może będziesz chciał utworzyć własny silnik. Framework Express jest na to przygotowany i oferuje metodę app.engine(). W tym punkcie dowiesz się, jak utworzyć mały silnik szablonów zapewniający zastępowanie zmiennej, co pozwala na obsługę treści dynamicznej. Metoda app.engine() mapuje rozszerzenie nazwy pliku na funkcję wywołania zwrotnego, aby framework Express „wiedział”, jak należy użyć danego pliku. W listingu C.1 przedstawiono kod, w którym plik z rozszerzeniem .md jest przekazywany, aby wywołanie takie jak res.render('myview.md') stosowało wskazaną funkcję wywołania zwrotnego do wygenerowania pliku. Przedstawiona abstrakcja pozwala na użycie z frameworkiem Express praktycznie każdego silnika szablonów. W omawianym silniku szablonów nawiasy są używane wokół zmiennych lokalnych, aby w ten sposób zapewnić obsługę dynamicznych danych wejściowych. Na przykład {name} spowoduje wyświetlenie wartości name za każdym razem, gdy pojawi się w szablonie. Listing C.1. Obsługa rozszerzenia .md
var express = require('express'); var http = require('http'); var md = require('github-flavored-markdown').parse; Wymagana jest implementacja Markdown. var fs = require('fs'); var app = express(); Mapowanie tego wywołania zwrotnego na pliki z rozszerzeniem .md. app.engine('md', function(path, options, fn){ fs.readFile(path, 'utf8', function(err, str){ Odczyt zawartości pliku i umieszczenie jej w ciągu tekstowym. if (err) return fn(err); try { Przekazanie frameworkowi Express obsługi błędów. var html = md(str); Konwersja ciągu tekstowego w formacie Markdown na kod HTML. html = html.replace(/\{([^}]+)\}/g, function(_, name){ Zastąpienie wartości w nawiasach. return options[name] || ''; Wartość domyślna to '' (pusty ciąg tekstowy). }); fn(null, html); Przekazanie frameworkowi Express wygenerowanego kodu HTML. } catch (err) { fn(err); Przechwycenie wszystkich zgłoszonych błędów. } }); });
Silnik szablonów przedstawiony w listingu C.1 pozwala na tworzenie dynamicznych widoków za pomocą składni Markdown. Jeśli na przykład chcesz powitać użytkownika, możesz użyć następującego kodu: # {name} Witaj, {name}! Cieszymy się, że będziesz używał aplikacji {appName}.
C.1.2. Szablony i projekt consolidate.js Projekt consolidate.js został przygotowany specjalnie dla frameworka Express 3.x i oferuje pojedyncze, ujednolicone API dla wielu silników szablonów w Node. Oznacza to, że standardowo Express 3.x pozwala na użycie ponad 14 różnych silników szablonów. Jeżeli pracujesz nad biblioteką używającą szablonów, wówczas możesz skorzystać z ich szerokiej gamy oferowanej przez consolidate.js. Na przykład Swig to silnik szablonów zainspirowany przez Django. Do zdefiniowania logiki korzysta ze znaczników osadzonych w kodzie HTML, jak
przedstawiono poniżej:
- {% for pet in pets %}
- {{ pet.name }} {% endfor %}
Prawdopodobnie w zależności od silnika szablonów i edytora tekstów z funkcją podświetlania składni wolisz korzystać z silników opartych na stylu HTML. Wówczas pliki mają rozszerzenie .html, a nie pochodzące od nazwy silnika, na przykład .swig. Z pomocą przychodzi wówczas metoda app.engine() frameworka Express. Po jej wywołaniu wygenerowany przez framework Express plik .html będzie używać wskazanego silnika szablonów, na przykład Swig: var cons = require('consolidate'); app.engine('html', cons.swig);
Silnik szablonów EJS również będzie mapowany na pliki .html, ponieważ też używa osadzonych znaczników:
- <% pets.forEach(function(pet){ %>
- <%= pet.name %> <% }) %>
Pewne silniki szablonów korzystają z zupełnie odmiennej składni, a więc nie ma sensu mapować ich na pliki .html. Dobrym przykładem jest silnik Jade posiadający własny język deklaracyjny. Jade można mapować za pomocą poniższego wywołania: var cons = require('consolidate'); app.engine('jade', cons.jade);
Więcej informacji szczegółowych oraz listę obsługiwanych silników szablonów znajdziesz na stronie repozytorium projektu consolidate.js pod adresem https://github.com/visionmedia/consolidate.js.
C.1.3. Frameworki i rozszerzenia Express Być może zastanawiasz się, jakie opcje mają programiści korzystający z bardziej strukturalnych frameworków, takich jak Ruby on Rails. Express oferuje kilka możliwości w takiej sytuacji. Społeczność Express opracowała wiele działających na wysokim poziomie frameworków opartych na Express w celu dostarczenia struktury katalogów, a
także funkcji, na przykład kontrolerów w stylu Ruby. Poza wspomnianymi frameworkami Express obsługuje także mnóstwo wtyczek rozszerzających jego możliwości standardowe.
Express-Expose W tycz ka express-expose może być wykorzystana do udostępnienia klientowi obiektów JavaScript znajdujących się po stronie serwera. Na przykład jeśli chcesz udostępnić dane JSON uwierzytelnionego użytkownika, wtedy możesz użyć wywołania res.expose() i dostarczyć kodowi działającemu po stronie klienta obiekt express.user: res.expose(req.user, 'express.user');
Express-Resource Inną doskonałą wtyczką jest express-resource, która jest wykorzystywana do obsługi strukturalnego routingu. Routing można zapewnić na wiele sposobów, ale wszystkie praktycznie sprowadzają się do metody żądania i ścieżki, co Express oferuje standardowo. Na tej podstawie można tworzyć koncepcje działające na wysokim poziomie. W przedstawionym poniżej przykładzie pokazano, jak można zdefiniować akcje przeznaczone do pokazywania, tworzenia i uaktualniania zasobu użytkownika w sposób deklaracyjny. Przede wszystkim w pliku app.js trzeba umieścić następujących wiersz kodu: app.resource('user', require('./controllers/user'));
W listingu C.2 przedstawiono kod modułu kontrolera /controllers/user.js. Listing C.2. Plik user.js exports.new = function(req, res){ res.send('new user'); }; exports.create = function(req, res){ res.send('create user'); }; exports.show = function(req, res){ res.send('show user ' + req.params.user); };
Pełną listę wtyczek, silników szablonów i frameworków zamieszczono w wiki frameworka Express na stronie https://github.com/visionmedia/express/wiki.
C.2. Konfiguracja zaawansowana
Z wcześniejszych rozdziałów książki dowiedziałeś się, jak skonfigurować framework Express za pomocą funkcji app.configure(). Przedstawiono w nich również wiele opcji konfiguracyjnych. W tym podrozdziale poznasz dodatkowe opcje konfiguracyjne, które można wykorzystać do zmiany zachowania domyślnego oraz udostępnienia kolejnych funkcji. W tabeli C.1 wymieniono opcje konfiguracyjne frameworka Express, które nie zostały omówione w rozdziale 8. Tabela C.1. Wbudowane ustawienia Express default engine
Użyty domyślny silnik szablonu
views
Wyświetlenie ścieżki wyszukiwania
json replacer
Funkcja modyfikacji odpowiedzi JSON
json spaces
Liczba spacji użytych do formatowania odpowiedzi JSON
jsnop callback
Obsługa JSONP za pomocą res.json() i res.send()
trust proxy
Zaufanie odwróconemu proxy
view cache
Funkcje buforowania silnika szablonu
Opcja konfiguracyjna views jest całkiem prosta. Używa się jej do wskazania lokalizacji szablonów widoku. Kiedy tworzysz szkielet aplikacji z poziomu powłoki za pomocą polecenia express, opcja konfiguracyjna view automatycznie wskazuje podkatalog views aplikacji. Teraz przechodzimy do nieco bardziej skomplikowanej opcji konfiguracyjnej, czyli json_replacer.
C.2.1. Modyfikacja odpowiedzi JSON Przyjmujemy założenie, że masz obiekt user wraz z właściwościami prywatnymi, takimi jak identyfikator obiektu _id. Domyślnie wywołanie metody res.send(user) spowoduje udzielenie odpowiedzi w formacie JSON w postaci takiej jak {"_id":123,"name":"Tobi"}. Opcja json replacer to ustawienie pobierające funkcję, którą framework Express przekaże JSON.stringify() w trakcie wywołań res.send() i res.json(). Samodzielna aplikacja Express przedstawiona w listingu C.3 pokazuje, jak można wykorzystać wymienioną opcję do pominięcia w odpowiedzi JSON wszystkich właściwości o nazwach rozpoczynających się od znaku podkreślenia. W omawianym przykładzie odpowiedź będzie miała postać {"name":"Tobi"}. Listing C.3. Użycie json_replacer do kontroli i modyfikacji danych JSON var express = require('express'); var app = express();
app.set('json replacer', function(key, value){ if ('_' == key[0]) return; return value; }); var user = { _id: 123, name: 'Tobi' }; app.get('/user', function(req, res){ res.send(user); }); app.listen(3000);
Zwróć uwagę, implementować
że poszczególne obiekty lub prototypy obiektów mogą metodę toJSON(). Wymieniona metoda jest używana przez JSON.stringify() podczas konwersji obiektu na ciąg tekstowy JSON. To jest doskonała alternatywa dla wywołania zwrotnego json_replacer, jeśli wprowadzane zmiany nie dotyczą każdego obiektu. W ten sposób dowiedziałeś się, jak określać dane, które powinny znajdować się w danych wyjściowych JSON. Możemy więc przejść do dostrajania formatowania danych w formacie JSON.
C.2.2. Formatowanie odpowiedzi JSON Opcja konfiguracyjna json spaces wpływa na wywołania JSON.stringify() we frameworku Express. Wymieniona opcja definiuje liczbę spacji używanych podczas formatowania danych JSON jako ciągu tekstowego. Domyślnie
metoda zwraca skompresowane dane JSON, na przykład {"name":"Tobi",å"age":2,"species":"zwierzę"}. Tego rodzaju skompresowane dane JSON są idealne w środowisku produkcyjnym, ponieważ zmniejszają wielkość udzielanej odpowiedzi. Jednak w trakcie prac nad aplikacją nieskompresowane dane wyjściowe są znacznie łatwiejsze w odczycie. O pc j a json spaces ma automatycznie ustawianą wartość 0 w środowisku produkcyjnym oraz 2 w środowisku programistycznym, co powoduje wygenerowanie danych wyjściowych w następującej postaci: { "name": "Tobi", "age": 2, "species": "zwierzę" }
C.2.3. Zaufanie nagłówkom odwrotnego proxy
Domyślnie framework Express w żadnym środowisku „nie ufa” nagłówkom odwrotnego proxy. Wspomniane odwrotne nagłówki proxy wykraczają poza zakres tematyczny tej książki. Jeśli Twoja aplikacja działa za odwrotnym proxy, na przykład Nginx, HAProxy lub Varnish, wtedy powinieneś użyć opcji trust proxy , aby framework Express „wiedział”, że te nagłówki można bezpiecznie sprawdzać.
Spis treści Wstęp Przedmowa Podziękowania Mike Cantelon Marc Harter Nathan Rajlich O książce Mapa drogowa Konwencje zastosowane w kodzie i materiały do pobrania Forum Author Online Część I Podstawy Node Rozdział 1. Witamy w Node.js 1.1. Node jest zbudowane w oparciu o JavaScript 1.2. Asynchroniczna i oparta na zdarzeniach: przeglądarka internetowa 1.3. Asynchroniczny i oparty na zdarzeniach: serwer 1.4. Aplikacje DIRT 1.5. Domyślna aplikacja jest typu DIRT 1.5.1. Prosty przykład aplikacji asynchronicznej 1.5.2. Serwer HTTP 1.5.3. Strumieniowanie danych 1.6. Podsumowanie Rozdział 2. Tworzenie aplikacji wielopokojowego czatu 2.1. Ogólny opis aplikacji 2.2. Wymagania aplikacji i konfiguracja początkowa 2.2.1. Obsługa HTTP i WebSocket 2.2.2. Tworzenie struktury plików aplikacji 2.2.3. Wskazanie zależności 2.2.4. Instalacja zależności 2.3. Udostępnianie plików HTML, CSS i kodu JavaScript działającego po stronie klienta 2.3.1. Tworzenie podstawowego serwera plików statycznych Wysyłanie danych pliku i odpowiedzi w postaci błędów Tworzenie serwera HTTP Uruchomienie serwera HTTP 2.3.2. Dodanie plików HTML i CSS 2.4. Obsługa wiadomości czatu za pomocą biblioteki Socket.IO 2.4.1. Konfiguracja serwera Socket.IO Utworzenie logiki połączenia 2.4.2. Obsługa zdarzeń oraz scenariuszy w aplikacji
Przypisanie nazwy gościa Dołączanie do pokoju Obsługa żądań zmiany nazwy użytkownika Wysyłanie wiadomości czatu Tworzenie pokoju Obsługa rozłączenia użytkownika 2.5. Użycie kodu JavaScript działającego po stronie klienta do utworzenia interfejsu użytkownika aplikacji 2.5.1. Przekazywanie do serwera wiadomości oraz żądań zmiany pokoju lub nazwy użytkownika 2.5.2. Wyświetlenie w interfejsie użytkownika wiadomości i listy dostępnych pokoi 2.6. Podsumowanie Rozdział 3. Podstawy programowania w Node 3.1. Organizacja i wielokrotne użycie kodu Node 3.1.1. Tworzenie modułu 3.1.2. Dostrajanie tworzenia modułu za pomocą module.exports 3.1.3. Wielokrotne użycie modułów za pomocą katalogu node_modules 3.1.4. Zastrzeżenia 3.2. Techniki programowania asynchronicznego 3.2.1. Użycie wywołań zwrotnych do obsługi zdarzeń jednorazowych 3.2.2. Użycie emitera zdarzeń do obsługi powtarzających się zdarzeń Przykład emitera zdarzeń Udzielanie odpowiedzi na zdarzenie, które powinno wystąpić tylko jednokrotnie Tworzenie emitera zdarzeń — przykład oparty na publikacji i subskrypcji Rozbudowa emitera zdarzeń — przykład obserwatora pliku 3.2.3. Wyzwania pojawiające się podczas programowania asynchronicznego 3.3. Sekwencja logiki asynchronicznej 3.3.1. Kiedy stosować szeregową kontrolę przepływu? 3.3.2. Implementacja szeregowej kontroli przepływu 3.3.3. Implementacja równoległej kontroli przepływu 3.3.4. Użycie narzędzi opracowanych przez społeczność 3.4. Podsumowanie Część II Tworzenie aplikacji sieciowych w Node Rozdział 4. Tworzenie aplikacji sieciowej w Node 4.1. Podstawy dotyczące serwera HTTP 4.1.1. Jak przychodzące żądania HTTP są przez Node przedstawiane programiście? 4.1.2. Prosty serwer HTTP odpowiadający komunikatem „Witaj, świecie” 4.1.3. Odczyt nagłówków żądania i zdefiniowanie nagłówków odpowiedzi 4.1.4. Ustawienie kodu stanu odpowiedzi HTTP 4.2. Tworzenie usługi sieciowej RESTful 4.2.1. Tworzenie zasobów za pomocą żądań POST
4.2.2. Pobieranie zasobów za pomocą żądania GET Zdefiniowanie nagłówka Content-Length 4.2.3. Usunięcie zasobu za pomocą żądania DELETE 4.3. Udostępnianie plików statycznych 4.3.1. Tworzenie serwera plików statycznych Optymalizacja transferu danych za pomocą Stream#pipe() 4.3.2. Obsługa błędów serwera 4.3.3. Wyprzedzająca obsługa błędów za pomocą wywołania fs.stat() 4.4. Akceptacja danych wejściowych użytkownika przekazanych za pomocą formularza sieciowego 4.4.1. Obsługa wysłanych pól formularza sieciowego Moduł querystring 4.4.2. Obsługa przekazanych plików za pomocą formidable 4.4.3. Sprawdzanie postępu operacji przekazywania plików 4.5. Zabezpieczanie aplikacji dzięki użyciu protokołu HTTPS 4.6. Podsumowanie Rozdział 5. Przechowywanie danych aplikacji Node 5.1. Niewymagający serwera magazyn danych 5.1.1. Magazyn danych w pamięci 5.1.2. Magazyn danych oparty na plikach Utworzenie logiki początkowej Zdefiniowanie funkcji pomocniczej do pobierania zadań Zdefiniowanie funkcji pomocniczej do przechowywania zadań 5.2. System zarządzania relacyjną bazą danych 5.2.1. MySQL Użycie MySQL do utworzenia aplikacji śledzącej wykonywanie zadań Utworzenie logiki aplikacji Tworzenie funkcji pomocniczych odpowiedzialnych za wysyłanie kodu HTML, tworzenie formularzy sieciowych i pobieranie danych z formularzy Dodanie danych za pomocą MySQL Usuwanie danych MySQL Uaktualnienie danych MySQL Pobieranie danych MySQL Generowanie rekordów MySQL Generowanie formularzy HTML Wypróbowanie aplikacji 5.2.2. PostgreSQL Nawiązanie połączenia z PostgreSQL Wstawienie rekordu do tabeli bazy danych Utworzenie zapytania zwracającego wynik 5.3. Bazy danych typu NoSQL 5.3.1. Redis Nawiązywanie połączenia z serwerem Redis
Praca z danymi bazy Redis Przechowywanie i pobieranie wartości za pomocą tabeli hash Przechowywanie i pobieranie danych za pomocą listy Przechowywanie i pobieranie danych za pomocą zbiorów Dostarczanie danych za pomocą kanałów Maksymalizacja wydajności Node_Redis 5.3.2. MongoDB Nawiązanie połączenia z MongoDB Uzyskanie dostępu do kolekcji MongoDB Wstawienie dokumentu do kolekcji Uaktualnienie danych za pomocą identyfikatora dokumentu Wyszukiwanie dokumentów Usuwanie dokumentów 5.3.3. Mongoose Otworzenie i zamknięcie połączenia Rejestracja schematu Dodanie zadania Wyszukiwanie dokumentu Uaktualnianie dokumentu Usuwanie dokumentu 5.4. Podsumowanie Rozdział 6. Framework Connect 6.1. Konfiguracja aplikacji Connect 6.2. Jak działa metoda pośrednicząca frameworka Connect? 6.2.1. Metody pośredniczące wyświetlające żądanie 6.2.2. Metoda pośrednicząca udzielająca odpowiedzi w postaci komunikatu „Witaj, świecie” 6.3. Dlaczego kolejność metod pośredniczących ma znaczenie? 6.3.1. Kiedy metoda pośrednicząca nie wywołuje next()? 6.3.2. Użycie kolejności metod pośredniczących do przeprowadzenia uwierzytelnienia 6.4. Montowanie metody pośredniczącej i serwera 6.4.1. Metody pośredniczące przeprowadzające uwierzytelnianie 6.4.2. Metoda pośrednicząca wyświetlająca panel administracyjny Przetestowanie całości 6.5. Tworzenie konfigurowalnej metody pośredniczącej 6.5.1. Tworzenie konfigurowalnej metody pośredniczącej logger() 6.5.2. Tworzenie metody pośredniczącej router() 6.5.3. Tworzenie metody pośredniczącej przeznaczonej do przepisywania adresów URL 6.6. Użycie metody pośredniczącej do obsługi błędów 6.6.1. Domyślna obsługa błędów w Connect 6.6.2. Samodzielna obsługa błędów aplikacji
6.6.3. Użycie wielu metod pośredniczących przeznaczonych do obsługi błędów Implementacja metody pośredniczącej hello() Implementacja metody pośredniczącej users() Implementacja metody pośredniczącej pets() Implementacja metody pośredniczącej errorHandler() Implementacja metody pośredniczącej errorPage 6.7. Podsumowanie Rozdział 7. Metody pośredniczące frameworka Connect 7.1. Metody pośredniczące przeznaczone do przetwarzania plików cookie, danych żądań i ciągów tekstowych zapytań 7.1.1. cookieParser() — przetwarzanie plików cookie Podstawowy sposób użycia Zwykłe cookie Podpisane cookie Cookie w formacie JSON Ustawienie cookie wychodzących 7.1.2. bodyParser() — przetwarzanie danych żądania Podstawowy sposób użycia Przetwarzanie danych JSON Przetwarzanie zwykłych danych formularza sieciowego Przetwarzanie wieloczęściowych danych formularza sieciowego 7.1.3. limit() — ograniczenie danych żądania Dlaczego metoda pośrednicząca limit() jest potrzebna? Podstawowy sposób użycia Opakowanie metody pośredniczącej limit() w celu uzyskania większej elastyczności 7.1.4. query() — analizator ciągu tekstowego zapytania Podstawowy sposób użycia 7.2. Metody pośredniczące implementujące podstawowe funkcje wymagane przez aplikację sieciową 7.2.1. logger() — rejestracja informacji o żądaniu Podstawowy sposób użycia Dostosowanie do własnych potrzeb formatu rejestracji danych Opcje metody pośredniczącej logger() — stream, immediate i buffer 7.2.2. favicon() — obsługa ikon favicon Podstawowy sposób użycia 7.2.3. methodOverride() — nieprawdziwe metody HTTP Podstawowy sposób użycia Uzyskanie dostępu do oryginalnej właściwości req.method 7.2.4. vhost() — wirtualny hosting Podstawowy sposób użycia Użycie wielu egzemplarzy vhost() 7.2.5. session() — zarządzanie sesją
Podstawowy sposób użycia Ustawienie czasu wygaśnięcia ważności sesji Praca z danymi sesji Praca z plikami cookie sesji Magazyn danych sesji 7.3. Metody pośredniczące zapewniające bezpieczeństwo aplikacji sieciowej 7.3.1. basicAuth() — uwierzytelnianie podstawowe HTTP Podstawowy sposób użycia Przekazanie funkcji wywołania zwrotnego Przekazanie asynchronicznej funkcji wywołania zwrotnego Przykład z użyciem polecenia curl 7.3.2. csrf() — ochrona przed atakami typu CSRF Podstawowy sposób użycia 7.3.3. errorHandler() — obsługa błędów w trakcie tworzenia aplikacji Podstawowy sposób użycia Komunikat błędu w formacie HTML Komunikat błędu w formacie zwykłego tekstu Komunikat błędu w formacie JSON 7.4. Metody pośredniczące przeznaczone do udostępniania plików statycznych 7.4.1. static() — udostępnianie plików statycznych Podstawowy sposób użycia Użycie metody static() wraz z montowaniem Bezwzględne kontra względne ścieżki dostępu do katalogów Udostępnianie pliku index.html, gdy żądanie dotyczy katalogu 7.4.2. compress() — kompresja plików statycznych Podstawowy sposób użycia Użycie własnej funkcji filtrującej Określenie poziomu kompresji i pamięci 7.4.3. directory() — wyświetlenie katalogów Podstawowy sposób użycia Podstawowy sposób użycia 7.5. Podsumowanie Rozdział 8. Framework Express 8.1. Utworzenie szkieletu aplikacji 8.1.1. Globalna instalacja frameworka Express 8.1.2. Generowanie aplikacji 8.1.3. Poznawanie aplikacji 8.2. Konfiguracja frameworka Express i tworzonej aplikacji 8.2.1. Konfiguracja na podstawie środowiska 8.3. Generowanie widoków aplikacji Express 8.3.1. Konfiguracja systemu widoków Zmiana katalogu wyszukiwania Domyślny silnik szablonów
Buforowanie widoku 8.3.2. Wyszukiwanie widoku 8.3.3. Udostępnianie danych widokom Tworzenie widoku wyświetlającego listę zdjęć Metody udostępniania danych widokom 8.4. Obsługa formularzy i przekazywania plików 8.4.1. Implementacja modelu zdjęcia 8.4.2. Tworzenie formularza przeznaczonego do przekazywania zdjęć Utworzenie formularza Dodanie trasy dla strony przeznaczonej do przekazywania zdjęć Obsługa wysyłania danych formularza 8.4.3. Wyświetlenie listy przekazanych zdjęć 8.5. Obsługa pobierania zasobów 8.5.1. Tworzenie trasy dla pobierania zdjęć 8.5.2. Implementacja trasy pobierania zdjęcia Rozpoczęcie pobierania przez przeglądarkę Ustawienie nazwy pobieranego pliku 8.6. Podsumowanie Rozdział 9. Zaawansowane użycie frameworka Express 9.1. Uwierzytelnianie użytkowników 9.1.1. Zapisywanie i wczytywanie użytkowników Tworzenie pliku package.json Tworzenie modelu User Zapis użytkownika w bazie danych Redis Zabezpieczanie hasła użytkownika Testowanie logiki odpowiedzialnej za zapis użytkownika Pobieranie danych użytkownika Uwierzytelnianie użytkownika 9.1.2. Rejestrowanie nowego użytkownika Dodanie tras rejestracji Tworzenie formularza rejestracji Udzielanie odpowiedzi użytkownikowi Trwałe przechowywanie komunikatów w sesji Implementacja rejestracji użytkownika 9.1.3. Logowanie zarejestrowanych użytkowników Wyświetlenie formularza logowania Uwierzytelnienie logowania Utworzenie menu dla użytkowników uwierzytelnionych i anonimowych 9.1.4. Metoda pośrednicząca przeznaczona do wczytywania użytkownika 9.2. Zaawansowane techniki routingu 9.2.1. Weryfikacja użytkownika podczas przesyłania treści Utworzenie modelu postu Dodanie tras powiązanych z postami
Dodanie strony wyświetlającej listę postów Utworzenie formularza postu Implementacja tworzenia postu 9.2.2. Metoda pośrednicząca charakterystyczna dla trasy Weryfikacja formularza za pomocą metody pośredniczącej przeznaczonej dla trasy Utworzenie elastycznej metody pośredniczącej przeznaczonej do przeprowadzania weryfikacji 9.2.3. Implementacja stronicowania Projektowanie API stronicowania Implementacja metody pośredniczącej przeznaczonej do obsługi stronicowania Użycie stronicowania w trasie Utworzenie szablonu dla łączy mechanizmu stronicowania Dodanie łączy stronicowania w szablonie Włączenie czystych adresów URL dla stronicowania 9.3. Utworzenie publicznego API REST 9.3.1. Projekt API 9.3.2. Dodanie uwierzytelnienia podstawowego 9.3.3. Implementacja routingu Testowanie operacji pobierania danych użytkownika Usunięcie danych wrażliwych użytkownika Dodanie postów Dodanie obsługi wyświetlania listy wpisów 9.3.4. Włączenie negocjacji treści Implementacja negocjacji treści Udzielenie odpowiedzi w postaci XML 9.4. Obsługa błędów 9.4.1. Obsługa błędów 404 Dodanie trasy pozwalającej na udzielenie odpowiedzi informującej o błędzie Tworzenie szablonu dla strony błędu Włączenie metody pośredniczącej 9.4.2. Obsługa błędów Użycie trasy warunkowej do przetestowania stron błędów Implementacja procedury obsługi błędów Utworzenie szablonu strony błędu Włączenie metody pośredniczącej 9.5. Podsumowanie Rozdział 10. Testowanie aplikacji Node 10.1. Testy jednostkowe 10.1.1. Moduł assert Prosty przykład Użycie asercji eqal() do sprawdzenia wartości zmiennej Użycie asercji notEqual() do wyszukiwania problemów w logice
Użycie asercji dodatkowych strictEqual(), notStrictEqual(), deepEqual(), notDeepEqual() Użycie asercji ok() do sprawdzenia, czy wartość zwrotna metody asynchronicznej wynosi true Sprawdzenie, czy zgłaszane komunikaty błędów są poprawne Dodanie logiki przeznaczonej do uruchamiania testów 10.1.2. Framework nodeunit Instalacja nodeunit Testowanie aplikacji Node za pomocą frameworka nodeunit 10.1.3. Mocha Testowanie aplikacji Node za pomocą Mocha Zdefiniowanie konfiguracji i czyszczenie logiki za pomocą zaczepów Mocha Testowanie logiki asynchronicznej 10.1.4. Framework Vows Testowanie logiki aplikacji za pomocą frameworka Vows 10.1.5. Biblioteka should.js Testowanie funkcjonalności modułu za pomocą biblioteki should.js 10.2. Testy akceptacyjne 10.2.1. Tobi Testowanie aplikacji sieciowych za pomocą Tobi 10.2.2. Soda Instalacja frameworka Soda i serwera Selenium Testowanie aplikacji sieciowej za pomocą Soda i Selenium Testowanie aplikacji sieciowej za pomocą Soda i Sauce Labs 10.3. Podsumowanie Rozdział 11. Szablony w aplikacji sieciowej 11.1. Użycie szablonów w celu zachowania przejrzystości kodu 11.1.1. Szablon w akcji Wygenerowanie kodu HTML bez użycia szablonu Wygenerowanie kodu HTML z użyciem szablonu 11.2. Silnik szablonów Embedded JavaScript 11.2.1. Tworzenie szablonu Zmiana znaczenia znaków 11.2.2. Praca z danymi szablonu za pomocą filtrów EJS Filtry obsługujące wybór Filtry przeznaczone do zmiany wielkości znaków Filtry przeznaczone do pracy z tekstem Filtry przeprowadzające sortowanie Filtr map Tworzenie własnych filtrów 11.2.3. Integracja EJS w aplikacji Buforowanie szablonów EJS 11.2.4. Użycie EJS w aplikacjach działających po stronie klienta
11.3. Użycie języka szablonów Mustache wraz z silnikiem Hogan 11.3.1. Tworzenie szablonu 11.3.2. Znaczniki Mustache Wyświetlanie prostych wartości Sekcje: iteracja przez wiele wartości Sekcje odwrócone: domyślny kod HTML, gdy wartość nie istnieje Sekcja lambda: własna funkcjonalność w blokach sekcji Partials: wielokrotne użycie szablonów w innych szablonach 11.3.3. Dostosowanie szablonu Hogan do własnych potrzeb 11.4. Szablony Jade 11.4.1. Podstawy szablonów Jade Podawanie atrybutów znacznika Podanie treści znacznika Zachowanie organizacji dzięki rozwinięciu bloku Umieszczanie danych w szablonach Jade 11.4.2. Logika w szablonach Jade Użycie JavaScript w szablonach Jade Iteracja przez obiekty i tablice Warunkowe wygenerowanie kodu szablonu Użycie poleceń case w Jade 11.4.3. Organizacja szablonów Jade Strukturyzacja wielu szablonów za pomocą ich dziedziczenia Implementacja układu za pomocą poprzedzenia blokiem lub dołączenia bloku Dołączanie szablonu Wielokrotne użycie logiki szablonu za pomocą polecenia mixin 11.5. Podsumowanie Część III Co dalej? Rozdział 12. Wdrażanie aplikacji Node i zapewnienie bezawaryjnego działania 12.1. Hosting aplikacji Node 12.1.1. Serwery dedykowane i VPS 12.1.2. Hosting w chmurze Amazon Web Services Rackspace Cloud 12.2. Podstawy wdrożenia 12.2.1. Wdrożenie z repozytorium Git 12.2.2. Zapewnienie działania aplikacji Node 12.3. Maksymalizacja wydajności i czasu bezawaryjnego działania aplikacji 12.3.1. Zapewnienie działania aplikacji za pomocą Upstart 12.3.2. API klastra — wykorzystanie zalety w postaci wielu rdzeni 12.3.3. Proxy i hosting plików statycznych 12.4. Podsumowanie Rozdział 13. Nie tylko serwery WWW 13.1. Biblioteka Socket.IO
13.1.1. Tworzenie minimalnej aplikacji Socket.IO Wypróbowanie aplikacji 13.1.2. Użycie biblioteki Socket.IO do odświeżenia strony i stylów CSS Wypróbowanie aplikacji 13.1.3. Inne zastosowania dla biblioteki Socket.IO 13.2. Dokładniejsze omówienie sieci TCP/IP 13.2.1. Praca z buforami i danymi binarnymi Dane tekstowe kontra binarne 13.2.2. Tworzenie serwera TCP Zapis danych Odczyt danych Połączenie dwóch strumieni za pomocą socket.pipe() Obsługa nieeleganckiego zamknięcia połączenia Zebranie wszystkiego w całość 13.2.3. Tworzenie klienta TCP 13.3. Narzędzia przeznaczone do pracy z systemem operacyjnym 13.3.1. Obiekt process, czyli globalny wzorzec Singleton Użycie process.env do pobierania i ustawiania zmiennych środowiskowych Zdarzenia specjalne emitowane przez obiekt proces Przechwytywanie sygnałów wysyłanych procesowi 13.3.2. Użycie modułu filesystem Przenoszenie pliku Monitorowanie katalogu lub pliku pod kątem zmian Użycie opracowanych przez społeczność modułów fstream i filed 13.3.3. Tworzenie procesów zewnętrznych Buforowanie za pomocą cp.exec() wyników działania polecenia Tworzenie poleceń za pomocą interfejsu Stream i cp.spawn() Rozkład obciążenia za pomocą cp.fork() 13.4. Tworzenie narzędzi powłoki 13.4.1. Przetwarzanie argumentów podanych w powłoce 13.4.2. Praca ze standardowym wejściem i wyjściem Zapis danych wyjściowych za pomocą process.stdout Odczyt danych wejściowych za pomocą process.stdin Rejestracja danych diagnostycznych za pomocą process.stderr 13.4.3. Dodanie koloru do danych wyjściowych Tworzenie i zapis znaków sterujących ANSI Formatowanie koloru tekstu za pomocą ansi.js Formatowanie koloru tła za pomocą ansi.js 13.5. Podsumowanie Rozdział 14. Ekosystem Node 14.1. Dostępne w internecie zasoby dla programistów Node 14.1.1. Node i odniesienia do modułów 14.1.2. Grupy Google
14.1.3. IRC 14.1.4. Zgłaszanie problemów w serwisie GitHub 14.2. Serwis GitHub 14.2.1. Rozpoczęcie pracy z GitHub Konfiguracja Git i rejestracja GitHub Dostarczenie GitHub klucza publicznego SSH 14.2.2. Dodanie projektu do GitHub Utworzenie repozytorium GitHub Konfiguracja pustego repozytorium Git Dodanie plików do repozytorium Git Przekazanie repozytorium Git do serwisu GitHub 14.2.3. Współpraca przez serwis GitHub 14.3. Przekazanie własnego modułu do repozytorium npm 14.3.1. Przygotowanie pakietu 14.3.2. Przygotowanie specyfikacji pakietu 14.3.3. Testowanie i publikowanie pakietu Testowanie instalacji pakietu Dodanie użytkownika npm Publikacja w repozytorium npm 14.4. Podsumowanie Dodatek A Instalacja Node i dodatki opracowane przez społeczność A.1. Instalacja w systemie OS X A.1.1. Instalacja za pomocą Homebrew A.2. Instalacja w systemie Windows A.3. Instalacja w systemie Linux A.3.1. Przygotowania do instalacji w Ubuntu A.3.2. Przygotowania do instalacji w CentOS A.4. Kompilacja Node A.5. Używanie menedżera pakietów Node A.5.1. Wyszukiwanie pakietów A.5.2. Instalacja pakietu A.5.3. Przeglądanie dokumentacji i kodu pakietu Dodatek B Debugowanie Node B.1. Analiza kodu za pomocą JSHint B.2. Dane wyjściowe debugowania B.2.1. Debugowanie za pomocą modułu console Wyświetlanie informacji o stanie aplikacji Dane wyjściowe dotyczące pomiaru czasu Wyświetlanie stosu wywołań B.2.2. Użycie modułu debug do zarządzania danymi wyjściowymi procesu debugowania B.3. Debuger wbudowany w Node B.3.1. Nawigacja po debugerze
B.3.2. Analiza i zmiana stanu w debugerze B.4. Inspektor Node B.4.1. Uruchomienie inspektora Node B.4.2. Nawigacja po inspektorze Node B.4.3. Przeglądanie stanu w inspektorze Node Dodatek C Rozszerzenie i konfiguracja frameworka Express C.1. Rozszerzenie frameworka Express C.1.1. Rejestracja szablonów silników C.1.2. Szablony i projekt consolidate.js C.1.3. Frameworki i rozszerzenia Express Express-Expose Express-Resource C.2. Konfiguracja zaawansowana C.2.1. Modyfikacja odpowiedzi JSON C.2.2. Formatowanie odpowiedzi JSON C.2.3. Zaufanie nagłówkom odwrotnego proxy