Alkalmazások fejlesztése

4. előadás

Horváth Győző
Egyetemi adjunktus
1117 Budapest, Pázmány Péter sétány 1/c., 2.420-as szoba
Tel: (1) 372-2500/1816
horvath.gyozo@inf.elte.hu

Eddig

  • JavaScript nyelv
  • Alkalmazás architektúrák (MVC)
  • HTTP
  • Express
  • Statikus fájlok kiszolgálása
  • Sablonok és layoutok

Ezen a héten

  • Dinamikus tartalom megjelenítése
  • Űrlapfeldolgozás
  • Adattárolás
  • Absztrakt modellréteg (ORM)

Dinamikus tartalom megjelenítése

Dinamikus tartalom

  • Azonosítani a HTML állományban a dinamikus részeket
  • Sablonnyelv segítségével definiálni a dinamikus rész megjelenítési logikáját
  • Hívó oldalon a megfelelő adatokat átadni
    • először "beégetve" --> adatstruktúra
    • majd ezt előállítva

Handlebars kifejezések

<h1>{{title}}</h1>
<h1>{{article.title}}</h1>

<!-- Nincs HTML escape-elés -->
{{{foo}}}

Handlebars iterálás

<div class="comments">
  {{#each comments}}
    <div class="comment">
      <h2>{{subject}}</h2>
      {{{body}}}
    </div>
  {{/each}}
</div>

{{#each paragraphs}}
  <p>{{this}}</p>
{{else}}
  <p class="empty">No content</p>
{{/each}}

Handlebars feltételek

{{#if isActive}}
  <img src="star.gif" alt="Active">
{{else}}
  <img src="cry.gif" alt="Inactive">
{{/if}}

{{#if isActive}}
  <img src="star.gif" alt="Active">
{{else if isInactive}}
  <img src="cry.gif" alt="Inactive">
{{/if}}

Adatok átadása Expressben

res.render(pathToView[, data]);

Például

res.render('errors/list', {
    cim: 'Alkalmazások fejlesztése'
});
<h1>{{cim}}</h1>

Feladat

Jelenítsünk meg a hibalista oldalon hibákat dinamikusan!

1. lépés

Dinamikus részek azonosítása.

Mely HTML részek változhatnak az adatoknak megfelelően?

2. lépés

HBS elemekkel jelezni ezeket a részeket!

<div class="alert alert-dismissible alert-success">
    <button type="button" class="close" data-dismiss="alert">×</button>
    Hibajegy sikeresen felvéve
</div>

<div class="page-header">
    <h1>Bejelentett hibáim</h1>
</div>

<table class="table table-striped table-hover ">
    <thead>
        <tr>
            <th>Időpont</th>
            <th>Státusz</th>
            <th>Helyszín</th>
            <th>Leírás</th>
        </tr>   
    </thead>
    <tbody>
        {{#each errors}}
        <tr>
            <td>{{date}}</td>
            <td><span class="label label-{{statusClass}}">{{statusText}}</span></td>
            <td>{{location}}</td>
            <td><span class="badge">{{numberOfMessages}}</span> {{description}}</td>
        </tr>
        {{/each}}
    </tbody>
</table>

<p><a href="/errors/new" class="btn btn-default">Új hiba felvitele</a></p>

3. lépés

Adatok átadása

app.get('/errors/list', function (req, res) {
    res.render('errors/list', {
        errors: [
            {
                date: '2015.09.16.',
                statusClass: 'danger',
                statusText: 'Új',
                location: 'PC6-15',
                description: 'Rossz billentyűzet',
                numberOfMessages: 5
            },
            {
                date: '2015.09.16.',
                statusClass: 'danger',
                statusText: 'Új',
                location: 'PC6-16',
                description: 'Rossz monitor',
                numberOfMessages: 5
            },
        ]
    });
});

Űrlapfeldolgozás

Űrlapkezelés lépései

  1. Az űrlap megjelenítése (pl. GET /form)
  2. Az űrlap kitöltése és adatainak felküldése, tipikusan ugyanarra a végpontra (pl. POST /form)
  3. Az űrlap adatainak ellenőrzése.
  4. Hiba esetén: az űrlap újbóli megjelenítése
  5. Siker esetén: a sikertartalom megjelenítése, tipikusan másik oldalon

Siker megjelenítése

POST-REDIRECT-GET (PRG) minta

A felküldött adatok ellenőrzése és sikeres feldolgozása után a sikeres tartalmat egy átirányítással jelenítjük meg (GET). Ezzel elkerülhető a véletlen oldalfrissítésből fakadó újraküldés.

Hibák megjelenítése

  1. POST üzenet válasza az űrlap hibaüzenetekkel.
  2. PRG mintával feldolgozni
    1. adatok felküldése (POST)
    2. adatok ellenőrzése
    3. hiba esetén átirányítás az űrlapot megjelenítő oldalra (GET), az adatok átadásával

Űrlapadatok megszerzése

GET

req.query.parameter

POST

  • HTTP üzenettörzs
  • req.body.parameter
  • body-parser modul használata
npm install body-parser --save
var bodyParser = require('body-parser');
//...
app.use(bodyParser.urlencoded({ extended: false }));

Űrlapadatok ellenőrzése

  • kézzel
  • függvénykönyvtár segítségével
  • express-validator
  • express-form
  • forms

express-validator

Telepítés

npm install express-validator --save

Leírás

Főbb függvények

  • req.checkBody()
  • req.sanitizeBody()
  • req.validationErrors()

Feladat

Készítsd el az új hiba felvitelére szolgáló oldalt (egyelőre mentés nélkül)!

1. lépés

Az oldalsablon előkészítése a dinamikus tartalom számára.

<div class="page-header">
    <h1>Új hiba bejelentése</h1>
</div>

<form class="form-horizontal" method="post">
    <fieldset>
        <div class="form-group
            {{#if validationErrors.helyszin}}
                has-error
            {{/if}}
        ">
            <label for="helyszin" class="col-lg-2 control-label">Helyszín</label>
            <div class="col-lg-10">
                <input class="form-control" id="helyszin" name="helyszin" placeholder="pl. PC6, Lovarda, 2. emeleti folyosó..." type="text" value="{{data.helyszin}}">
                {{#if validationErrors.helyszin}}
                    <span class="help-block">{{validationErrors.helyszin.msg}}</span>
                {{/if}}
            </div>
        </div>
        <div class="form-group
            {{#if validationErrors.leiras}}
                has-error
            {{/if}}
        ">
            <label for="leiras" class="col-lg-2 control-label">Leírás</label>
            <div class="col-lg-10">
                <textarea class="form-control" rows="3" id="leiras" name="leiras">{{data.leiras}}</textarea>
                <span class="help-block">Meg kell adni a helyszínt, ha pedig konkrét géppel van probléma, akkor azt is.</span>
                {{#if validationErrors.leiras}}
                    <span class="help-block">{{validationErrors.leiras.msg}}</span>
                {{/if}}
            </div>
        </div>
        <div class="form-group">
            <div class="col-lg-10 col-lg-offset-2">
                <button type="reset" class="btn btn-default">Cancel</button>
                <button type="submit" class="btn btn-primary">Submit</button>
            </div>
        </div>
    </fieldset>
</form>             

2. lépés

Űrlap megjelenítése

app.get('/errors/new', function (req, res) {
    res.render('errors/new');
});

3. lépés

Űrlapadatok fogadása

npm install body-parser --save
// modulok importálása
var bodyParser = require('body-parser');

// middleware-ek regisztrálása
app.use(bodyParser.urlencoded({ extended: false }));

// végpont írása
app.post('/errors/new', function (req, res) {
    console.log(req.body);
});

4. lépés

Űrlapadatok ellenőrzése és a hibák megjelenítése

npm install express-validator --save
// modulok importálása
var expressValidator = require('express-validator');

// middleware-ek regisztrálása
app.use(expressValidator());

// végpont
app.post('/errors/new', function (req, res) {
    // adatok ellenőrzése
    req.checkBody('helyszin', 'Hibás helyszín').notEmpty().withMessage('Kötelező megadni!');
    req.sanitizeBody('leiras').escape();
    req.checkBody('leiras', 'Hibás leírás').notEmpty().withMessage('Kötelező megadni!');
    
    var validationErrors = req.validationErrors(true);
    console.log(validationErrors);
    
    if (validationErrors) {
        // űrlap megjelenítése a hibákkal és a felküldött adatokkal
    }
    else {
        // adatok elmentése (ld. később) és a hibalista megjelenítése
    }
});

Adattárolás

Adattárolási lehetőségek

  • Hely szerint
    • memória
    • lemez
    • adatbázis
  • Hozzáférhetőség szerint
    • alkalmazásszintű
    • kliensszintű

Alkalmazásszintű adatok

  • Minden kliens számára egyformán elérhető
  • Memória: az alkalmazás globális változói
  • Lemez, adatbázis hasonló
var a = 0;

app.get('/', function (req, res) {
    a += 1;
    res.render('index', {
        a: a
    });
});

Kliensszintű adatok

  • Munkamenet-kezelés
  • Baj: HTTP protokoll állapotmentes
  • Kliensek megkülönböztetése
    • kliensoldalon tárolni valami egyedi kulcsot (pl. sütiben)
    • szerveroldalon tárolni az adatot (memória, lemez, adatbázis)

Munkamenet-kezelés Expressben

Telepítés (express-session)

npm install express-session --save

Használat

var session = require('express-session');

app.use(session({
    cookie: { maxAge: 60000 },
    secret: 'titkos szoveg',
    resave: false,
    saveUninitialized: false,
}));

app.get('/', function (req, res) {
    // Olvasás
    req.session.parameter
    // Írás
    req.session.parameter = value;
});

Flash adatok

  • Egy kérés erejéig élő adatok a munkamenetben.
  • Tipikusan üzenetek átadására szolgál.
  • connect-flash modul
npm install connect-flash --save
var flash = require('connect-flash');

app.use(flash());

app.get('/', function (req, res) {
    // Olvasás
    req.flash(type);
    // Írás
    req.flash(type, msg);
});

Feladat

Tároljuk el az újonnan felvett hibát a memóriában alkalmazásszinten, és jelenítsük meg!

A sikeres adatfelvitelről szóló üzenet jelenjen meg a listaoldalon!

Írjuk át az űrlapfeldolgozást PRG mintára!

1. lépés

Hibatároló definiálása

//Model layer
var errorContainer = [];

2. lépés

Adatok mentése az új hiba oldalon

// POST /errors/new végpont
errorContainer.push({
    date: (new Date()).toLocaleString(),
    status: 'new',
    location: req.body.helyszin,
    description: req.body.leiras,
    numberOfMessages: 0
});

3. lépés

Hibák megjelenítése a listaoldalon

app.get('/errors/list', function (req, res) {
    res.render('errors/list', {
        errors: errorContainer,
    });
});

4. lépés

A kiíráshoz szükséges adatok megadása (hbs helper vagy dekorátor)

//Viewmodel réteg
var statusTexts = {
    'new': 'Új',
    'assigned': 'Hozzárendelve',
    'ready': 'Kész',
    'rejected': 'Elutasítva',
    'pending': 'Felfüggesztve',
};
var statusClasses = {
    'new': 'danger',
    'assigned': 'info',
    'ready': 'success',
    'rejected': 'default',
    'pending': 'warning',
};

function decorateErrors(errorContainer) {
    return errorContainer.map(function (e) {
        e.statusText = statusTexts[e.status];
        e.statusClass = statusClasses[e.status];
        return e;
    });
}

app.get('/errors/list', function (req, res) {
    res.render('errors/list', {
        errors: decorateErrors(errorContainer),
    });
});

5. lépés

Sikeres üzenet átadása

Új hiba oldal

req.flash('info', 'Hiba sikeresen felvéve!');

Listaoldal

res.render('errors/list', {
    errors: decorateErrors(errorContainer),
    messages: req.flash('info')
});

Listasablon

{{#each messages}}
<div class="alert alert-dismissible alert-success">
    <button type="button" class="close" data-dismiss="alert">×</button>
    {{this}}
</div>
{{/each}}

6. lépés

Hibás űrlapadatok feldolgozása a PRG mintával

POST /errors/new

if (validationErrors) {
    req.flash('validationErrors', validationErrors);
    req.flash('data', req.body);
    res.redirect('/errors/new');   
}

GET /errors/new

var validationErrors = (req.flash('validationErrors') || [{}]).pop();
var data = (req.flash('data') || [{}]).pop();

res.render('errors/new', {
    validationErrors: validationErrors,
    data: data,
}); 

Absztrakt modellréteg (ORM)

Modell

  • Adatok és műveletek
    • Adatok
    • Feldolgozó függvények
    • Adatbázis
    • Adatbázis eléréséhez szükséges műveletek

Modell rétegek

  • Perzisztáló réteg
    • memória, fájl, adatbázis
  • Adatbázis-elérési absztrakciós réteg
    • perzisztálóréteg-független általános műveletek
  • Adatbázis absztrakciós réteg
    • magas szintű műveletek adatok kezelésére
  • Üzleti logikai réteg
    • a feladathoz illeszkedő objektumkapcsolatok és műveletek

ORM

Waterline ORM

Waterline koncepció

  • Gyűjtemények (modelldefiníció)
  • Modellek (lekérdezések által visszaadott objektumok)
  • Adapterek

Telepítés és használat

npm install waterline --save
npm install sails-memory --save
npm install sails-disk --save
npm install sails-postgresql --save
// importált modulok
var Waterline = require('waterline');
var memoryAdapter = require('sails-memory');
var diskAdapter = require('sails-disk');
var postgresqlAdapter = require('sails-postgresql');

// ORM példány
var orm = new Waterline();

// konfiguráció
var config = {
    adapters: {
        memory:     memoryAdapter,
        disk:       diskAdapter,
        postgresql: postgresqlAdapter
    },
    connections: {
        memory: {
            adapter: 'memory'
        },
        disk: {
            adapter: 'disk'
        },
        postgresql: {
            adapter: 'postgresql',
            database: 'tickets',
            host: 'localhost',
            user: 'ubuntu',
            password: 'ubuntu',
        }
    },
    defaults: {
        migrate: 'alter'
    },
};

Modellek definiálása (gyűjtemények)

var PersonCollection = Waterline.Collection.extend({

  // Egyedi azonosító
  identity: 'person',

  // A kapcsolat, amelyek keresztül a perzisztáló réteggel kommunikál
  connection: 'local-postgresql',

  // A modell attribútumai
  attributes: {
    firstName: 'string',
    lastName: 'string',
    age: 'integer',
    birthDate: 'date',
    emailAddress: 'email'
  }
});

Modell betöltése, ORM indítása

// Modell betöltése
orm.loadCollection(collection);

// ORM indítása
orm.initialize(config, function(err, models) {
    if(err) throw err;
    
    // models.collections;
    // models.connections;
});

Kapcsolatok példa

// A user may have many pets
var User = Waterline.Collection.extend({

  identity: 'user',
  connection: 'local-postgresql',

  attributes: {
    firstName: 'string',
    lastName: 'string',

    // Add a reference to Pets
    pets: {
      collection: 'pet',
      via: 'owner'
    }
  }
});

// A pet may only belong to a single user
var Pet = Waterline.Collection.extend({

  identity: 'pet',
  connection: 'local-postgresql',

  attributes: {
    breed: 'string',
    type: 'string',
    name: 'string',

    // Add a reference to User
    owner: {
      model: 'user'
    }
  }
});

Gyűjteményszintű műveletek

  • findOne()
  • find()
  • create()
  • update()
  • destroy()
  • findOrCreate()
  • count()
  • query()
  • exec()
  • populate()

Modellszintű műveletek

  • save()
  • validate()
  • toObject()
  • toJSON()

Feladat

Az adatokat ORM segítségével biztosítsd, a tárolást lemezre végezd!

Próbáld ki a tárolást PostgreSQL adatbázisban!

1. lépés

Általános keret

// importált modulok
var Waterline = require('waterline');
var memoryAdapter = require('sails-memory');
var diskAdapter = require('sails-disk');

// ORM példány
var orm = new Waterline();

// konfiguráció
var config = {
    adapters: {
        memory:     memoryAdapter,
        disk:       diskAdapter,
    },
    connections: {
        default: {
            adapter: 'disk',
        },
        memory: {
            adapter: 'memory'
        },
        disk: {
            adapter: 'disk'
        },
    },
    defaults: {
        migrate: 'alter'
    },
};

// ORM indítása
orm.initialize(config, function(err, models) {
    if(err) throw err;
    
    app.models = models.collections;
    app.connections = models.connections;
    
    // Start Server
    var port = process.env.PORT || 3000;
    app.listen(port, function () {
        console.log('Server is started.');
    });
    
    console.log("ORM is started.");
});

2. lépés

Modell definiálása és betöltése

var errorCollection = Waterline.Collection.extend({
    identity: 'error',
    connection: 'default',
    attributes: {
        date: {
            type: 'datetime',
            defaultsTo: function () { return new Date(); },
            required: true,
        },
        status: {
            type: 'string',
            enum: ['new', 'assigned', 'success', 'rejected', 'pending'],
            required: true,
        },
        location: {
            type: 'string',
            required: true,
        },
        description: {
            type: 'string',
            required: true,
        },
    }
});

orm.loadCollection(errorCollection);

3. lépés

Modell használata (Új hiba mentése)

app.models.error.create({
    status: 'new',
    location: req.body.helyszin,
    description: req.body.leiras
})
.then(function (error) {
    //siker
})
.catch(function (err) {
    //hiba
});

4. lépés

Modell használata (Hibalista)

app.models.error.find().then(function (errors) {
    console.log(errors);
    //megjelenítés
});

5. lépés

PostgreSQL konfigurálása Cloud9-on

# indítás
sudo service postgresql start
# belépés
sudo sudo -u postgres psql
# felhasználó létrehozása az adatbázisban
create user ubuntu password 'ubuntu';
# adatbázis létrehozása
create database tickets owner ubuntu;
# adatbázisok listázása
\list
# kilépés
\q

# adatbázis kiválasztása
\connect tickets
# táblák listázása
\dt

6. lépés

PostgreSQL használata

var postgresqlAdapter = require('sails-postgresql');

var config = {
    adapters: {
        // ...
        postgresql: postgresqlAdapter
    },
    connections: {
        // ...
        postgresql: {
            adapter: 'postgresql',
            database: 'tickets',
            host: 'localhost',
            user: 'ubuntu',
            password: 'ubuntu',
        }
    },
};

var errorCollection = Waterline.Collection.extend({
    connection: 'postgresql',
    // ...
});

Otthoni feladat

Feladat

Tegyél fel egy hivatkozást a listaoldal táblázatába, amelyre kattintva megjelennek az adott hiba részletei.

Ezen az oldalon legyen lehetőség megjegyzést felvenni a hibához!

Ugyanitt a már meglévő megjegyzéseket listázd is ki!

1. lépés

Statikus HTML elkészítése, dinamikussá tétele és bekötése

  • Listaoldalon hivatkozás készítése
  • Statikus HTML: Bootstrap komponensek a Bootswatchból
  • Dinamikussá tétel: HBS
  • Végpont: /errors/:id
  • Paraméter kiolvasása: req.params.id
app.get('/:id', function(req, res) {
    var id = req.params.id;
    /* ... */
});

2. lépés

Az adott hibabejelentés lekérdezése id alapján

app.models.error.findOne({ id: id}).then(function (error) {
    res.render('errors/show', {
        error: error,
    }); 
});

3. lépés

Megjegyzések felvitele (hibakezelés)

  • Űrlap készítése az oldal alján (HBS)
  • POST: Adatok ellenőrzése
  • Hibakezelés PRG-vel
  • Siker esetén PRG-vel az oldalon maradni
  • Sikerről üzenetet megjeleníteni

4. lépés

Megjegyzések felvitele (siker): Adatmodell

// Error modell
var errorCollection = Waterline.Collection.extend({
    /* ... */
    comments: {
        collection: 'comment',
        via: 'error'
    },
}

// Comment modell
var commentCollection = Waterline.Collection.extend({
    identity: 'comment',
    connection: 'default',
    attributes: {
        date: {
            type: 'datetime',
            defaultsTo: function () { return new Date(); },
            required: true,
        },
        text: {
            type: 'string',
            required: true,
        },
        username: {
            type: 'string',
            required: true,
            defaultsTo: 'anonymous (test)',
        },
        
        error: {
            model: 'error',
        },
    }
});

5. lépés

Megjegyzések felvitele (siker): Vezérlő

  • Siker esetén adatok mentése
  • PRG-vel az oldalon maradni
  • Sikerről üzenetet megjeleníteni
app.models.comment.create({
    text: req.body.text,
    error: id
})
.then(function (comment) {
    req.flash('info', 'Megjegyzés sikeresen felvéve!');
    res.redirect('/errors/' + id);
})
.catch(function (err) {
    console.log(err);
});

6. lépés

Megjegyzések listázása

Sablon elkészítése (HBS)

{{#each error.comments}}
<li href="#" class="list-group-item">
    <h4 class="list-group-item-heading">{{username}} <small>{{date}}</small></h4>
    <p class="list-group-item-text">{{text}}</p>
</li>
{{/each}}

Adatok lekérdezése és megjelenítése

app.models.error.findOne({ id: id}).populate('comments').then(function (error) {
    res.render('errors/new', {
        error: error,
        /* ... */
    }); 
});