A Node.js bemutatása egy blog motoron keresztül

Bemutatkozás

 1 {
 2   'Név': 'Galiba Péter',
 3   'Nick': 'Poetro',
 4   'Leírás':
 5     '1998 óta foglalkozik weboldalak ' +
 6     'készítésével, és JavaScript-tel.' +
 7     '2006 óta saját céggel rendelkezik, ' +
 8     'és azóta a NowPublic.com, majd ' +
 9     '2009 óta az Examiner.com front-end ' +
10     'fejlesztője.',
11   'Specialitások': [
12     'HTML', 'CSS',
13     'PHP', 'Drupal',
14     'JavaScript', 'jQuery', 'Node.js'
15   ],
16   'Weboldalak': [
17     'http://poetro.hu/',
18     'https://github.com/Poetro',
19     'http://weblabor.hu/tagok/1519'
20   ]
21 }

Node.js (http://nodejs.org/)

  • Esemény vezérelt, nem blokkoló I/O rendszer
  • Hasonló rendszerek: Twisted (Python), EventMachine (Ruby)
  • C++-ban és JavaScript-ben van írva
  • A Google V8 motorjára épül (http://code.google.com/p/v8/)
  • CommonJS alapú modul rendszer
    • C++-ban és/vagy JavaScript-ben íródtak
    • Magunk is írhatunk ilyen modulokat
  • „Hivatalos” modul kezelő az npm (http://npmjs.org/)
  • Aktuális verziók:
    • Stabil: v0.2.6 (2010. december 31.)
    • Fejlesztői: v0.3.5 (2011. január 16.)

Telepítés

1 $ mkdir node-latest-install
2 $ cd node-latest-install
3 $ curl http://nodejs.org/dist/node-latest.tar.gz | tar xz --strip-components=1
4 $ ./configure
5 $ make install
  • C++ fordító (gcc)
  • Python 2.4 vagy újabb a fordításhoz
  • OpenSSL (ajánlott)

npm

1 $ curl http://npmjs.org/install.sh | sh

Fontosabb core modulok

  • sys: Loggolást, hibakeresést, futtatást segítő eszközök
  • fs: fájl rendszerhez biztosít hozzáférést
  • http: HTTP kliens és szerver alkalmazásokhoz
  • net: hálózati feladatok ellátásához (socketek, streamek)
  • crypto: titkosítás, kódolás (md5, sha1, rsa, sha256 stb.)
  • path: fájlrendszer elérési utakhoz
  • url: URL kezeléshez
  • querystring: Az URL query részének kezeléséhez

npm

$ curl http://npmjs.org/install.sh | sh
  • listázás: npm ls [kulcsszó]
  • telepítés: npm install modulnév [modulnév…]
  • eltávolítás: npm uninstall modulnév [modulnév…]
  • frissítés: npm update [modulnév…]
  • további lehetőségek: npm help
  • több mint 300 elérhető modul

Feladat

Egy egyszerű blog motor megvalósítása.

  • Bejegyzések létrehozása
  • Bejegyzések módosítása
  • Bejegyzések listázása
  • A felületet angolul hozzuk létre, de legyen lehetőség azt lefordítani

Használt modulok

  • connect: nagy teljesítményű middleware keretrendszer:
    routing, http segéd modulok, session kezelés, fordítók, GET / POST / COOKIE kezelés stb.
  • express: a connect-re épülő keretrendszer, mely egybe fogja a connect szolgáltatásait, és egy Sinatra jellegű webfejlesztői környezetet ad
  • dialect: i18n / l10n keretrendszer
  • sqlite: sqlite adatbázis kezelő motor, és annak Node.js illesztése
  • generic-pool: Object pool pattern egy megvalósítása
  • ejs: Embedded JavaScript sablonmotor

Könyvtár struktúra

app.js
blog.db
dictionaries\
   ├─ en.js
   └─ hu.js
lib\
   ├─ date.js
   ├─ entry.js
   └─ route.js
public\
   └─ css\
      ├─ global.css
      └─ reset.css
views\
   ├─ entries.ejs
   ├─ entry.ejs
   ├─ form─entry.ejs
   ├─ index.ejs
   ├─ layout.ejs
   └─ partials\
      └─ entry.ejs

Kiszolgálónk előkészítése

 1 var express = require('express'),
 2     dialect = require('dialect').dialect,
 3     app = module.exports = express.createServer(),
 4     sqlite = require('sqlite'),
 5     dbPool = require('generic-pool').Pool({
 6       'name'   : 'SQLite Pool',
 7       'create' : function (callback) {
 8         var db = new sqlite.Database();
 9         db.open('blog.db', function (err) {
10           if (err) throw err;
11           callback(db);
12         });
13       },
14       'destroy' : function (db) {
15         db.close(function (error) {
16           if (error) throw error;
17         });
18       },
19       'max' : 1
20     });

A blog bejegyzések adatbázisa

 1 // Előkészítjük az adatbázist.
 2 dbPool.acquire(function (db) {
 3   db.execute(
 4     'CREATE TABLE IF NOT EXISTS entries (' +
 5       'nid INTEGER PRIMARY KEY ASC,' +
 6       'title TEXT,' +
 7       'created INTEGER,' +
 8       'updated INTEGER,' +
 9       'body TEXT' +
10       ')',
11     function (error) {
12       dbPool.release(db);
13       if (error) {
14         throw error;
15       }
16     }
17   );
18 });

További konfiguráció

 1 // Konfiguráció, ami minden környezetben lefut
 2 app.configure(function(){
 3   app.set('views', __dirname + '/views');
 4   app.set('view engine', 'ejs');
 5   app.use(express.bodyDecoder());
 6   app.use(app.router);
 7   app.use(connect.compiler({
 8     src: __dirname + '/public', autocompile: true,
 9     enable: ['less', 'coffeescript']
10   })),
11   app.use(express.staticProvider(__dirname + '/public'));
12 });
13 // Fejlesztői környezet beállításai (ez az alapértelmezett)
14 app.configure('development', function () {
15   app.use(express.errorHandler({ dumpExceptions: true,
16     showStack: true }));
17 });
18 // Éles környezet beállításai (NODE_ENV=production)
19 app.configure('production', function () {
20   app.use(express.errorHandler());
21 });

Routing

 1 app.get(/^\/entries\/(\d+)\/?/, function (request, response) {
 2   var entryId = request.params[0];
 3   entry.load(+entryId, function (error, row) {
 4     if (!row) {
 5       response.send(t(['{entryId} does not exist',
 6         {entryId : ''+entryId, count: 1}]), 404);
 7     }
 8     else {
 9       response.render('entry', {'locals': row});
10     }
11   });
12 });
13 app.get(/^\/entry\/?/, function (request, response) {
14   response.render('form-entry', {locals: {title: '', body: '', created: '', updated: '', nid: ''}});
15 });
16 app.post(/^\/entries\/?/, function (request, response) {
17   var body = request.body;
18   if (body.nid && body.op && body.op === t('Delete')) {
19     entry.destroy(body.nid); response.redirect('entries');
20   }
21   else {
22     entry.create(body); response.redirect('entries/' + (body.nid || ''));
23   }
24 });

Aszinkron műveletek

Node.js esetén általában két módon futtathatunk aszinkron műveletek:

  1. Hagyományos callback függvényekkel

    1 fs.readdir('/path/to/directory', function (error, files) {});
    
  2. Feliratkozunk egy eseményre

    1 request.on('data', function (chunk) { });
    2 // illetve, ami ugyanez:
    3 request.addListener('data', function (chunk) { });
    

EventEmitter objektummal ki tudjuk terjeszteni objektumunkat, ha saját eseményeket akarunk kiváltani, és kezelni

1 var MyObj = function () {}, sys = require('sys'), myobj;
2 sys.inherits(MyObj, require('events').EventEmitter);
3 myobj = new MyObj();
4 myobj.on('event', function () {});
5 myobj.emit('event');

Entry

 1 function Entry(dbPool) {
 2   // Ha nem konstruktorként lett meghívva, hívjuk meg úgy.
 3   if (!(this instanceof Entry)) return new Entry(dbPool);
 4   this.dbPool = dbPool;
 5 }
 6 
 7 Entry.prototype = {
 8   findAll : function (callback) {
 9     var dbPool = this.dbPool;
10     dbPool.acquire(function (db) {
11       db.execute(
12         'SELECT * FROM entries',
13         function (error, rows) {
14           dbPool.release(db);
15           callback && callback(error,
16             (!rows || !rows.length) ? null : rows);
17         }
18       );
19     });
20   },

Entry - frissítés

 1 update : function (attr, callback) {
 2   var dbPool = this.dbPool;
 3   dbPool.acquire(function (db) {
 4     db.execute(
 5       'REPLACE INTO entries ' +
 6       '(nid, title, created, updated, body) VALUES' +
 7       '($nid, $title, $created, $updated, $body)',
 8       {
 9         $nid: +attr.nid || undefined,
10         $title: attr.title,
11         $created: attr.created || Date.now() / 1000 | 0,
12         $updated: Date.now() / 1000 | 0,
13         $body: attr.body
14       },
15       function (error, rows) {
16         dbPool.release(db);
17         callback && callback(error, rows);
18       }
19     );
20   });
21 },

Entry - betöltés

 1 load : function (nid, callback) {
 2   var dbPool = this.dbPool;
 3   dbPool.acquire(function (db) {
 4     db.execute(
 5       'SELECT * FROM entries WHERE nid = ?', [+nid],
 6       function (error, rows) {
 7         dbPool.release(db);
 8         callback && callback(error,
 9           (!rows || !rows.length) ? null : rows[0]);
10       }
11     );
12   });
13 },

Sablonok (EJS)

  • Változók kiírása: <%- változó %>
  • Változók kiírása HTML entitásokkal: <%= változó %>
  • Feltételek:

    1 <% if (true) { %>
    2   Igaz
    3 <% } %>`
    
  • Ciklusok:

    1 <% for (var i=a.length-1; i >= 0; i--) { %>
    2   <%= i %>
    3 <% } %>
    
  • Részelemek (partials):

    1 <%- partial('entry', { collection: entries, as: global }) %>
    

Sablonok (EJS) 2.

 1 <article>
 2   <h1 class="entry-title"><%= title %></h1>
 3   <p>
 4     <time class="published" pubdate="pubdate"
 5       datetime="<%= formatDate('c', created) %>">
 6       <%= t(['Published at {date}',
 7         {date : formatDate('F jS, Y g:i a', created),
 8         count: 1}]) %>
 9     </time> |
10     <time class="updated"
11       datetime="<%= formatDate('c', updated) %>">
12       <%= t(['Updated at {date}',
13         {date : formatDate('F jS, Y g:i a', updated),
14         count: 1}]) %>
15     </time>
16   </p>
17   <div class="entry-content"><%- body %></div>
18   <a href="/entry/<%= nid %>"><%= t('Edit') %></a>
19 </article>

Sablonozó rendszerek

Az EJS-en kívül még számos sablonozó rendszer érhető el Node.js alatt:

  • CoffeeKup (CoffeeScript)
  • Jade
  • jQuery Template
  • Haml
  • Mustache
  • ...

CSS-hez:

  • sass
  • less

Adatbázis kezelők

  • CouchDB
  • Firebird
  • Memcache
  • MongoDB
  • MySQL
  • SQLite
  • Postgres
  • Redis
  • ...

Node.js tulajdonságok

  • Kis memóriahasználat
  • Esemény vezérelt
  • Nem blokkol
  • Egy szálon fut
    • Több szálon futáshoz használhatunk proxy-t
  • Óvatosan kell bánni az aszinkron műveletekkel:
    • Túl sok művelet ne fusson párhuzamosan, mert lehet hogy jobban leterheli a hardvert
    • Nagyon sok kérést tudunk fogadni egy időintervallumban, de nem biztos, hogy ki is tudjuk szolgálni őket ilyen gyorsan
    • Érdemes valamiféle Object pool-t használni, hogy kordában tartsuk a rendszert
    • Lehet korlátozni a maximálisan fogadott HTTP kéréseket, vagy itt is használhatunk pool-t

Node.js tulajdonságok (2)

  • Nincs Apache httpd:
    • mi magunk írjuk a HTTP szervert,
    • kezelni kell minden kérést,
    • például statikus fájlok kiszolgálását
  • Alkalmazásunk daemon-ként (is) fut(hat) (például HTTP / DNS szerver):
    • folyamatosan be van töltve,
    • lehetnek perzisztens változóink, nem kell őket minden kérésnél betölteni
  • Írhatunk teljes értékű parancssoros alkalmazásokat
  • Futtathatunk más programokat
  • Hozzáférünk a fájlrendszerhez, hálózathoz, processzekhez, socketekhez, streamekhez
  • Tudunk kezelni bináris adatokat a Buffer objektumon keresztül

Modulok

  • Egy feladat megoldására már most több modul áll rendelkezésre, és nem biztos, hogy könnyű választani
  • Sebesség:
    • C++ vagy tisztán JavaScript implementációt használunk, nagy lehet a különbség
  • Gyorsan változó API, modulok
  • Jó közösség, segítőkészek, és örülnek a segítségnek
  • Modulok nagy része Github-on van, érdemes figyelni
  • Nagyok a minőségbeli különbsége, mind kód, mind dokumentáció terén

Pár érdekes, illetve hasznos modul:

Kérdések(?)