Frontend-kehityksen evoluutio

Fronttikehitystä on tullut tehtyä useammalla teknologialla. Miltä näytti/näyttää eri sukupolvien teknologioilla toiminnallisuus, jossa taulukkoon voisi lisätä henkilöiden nimiä sekä sähköpostiosoitteita ja kerätä tiedon arrayhyn tallennusta varten.

esimerkkisovellus

Esittelyssä
JavaScript
jQuery
Knockout
Angular 2+
React
Vue.js

JavaScript

JavaScriptissä UI piti tehdä käsin lisäämällä/poistamalla DOM-puusta elementtejä. Aluksi taulukossa voisi olla vaikka vain otsikkorivi ja alla pari painiketta. Esim. näin

<table>
  <thead>
    <tr><th>First name</th><th>Last name</th><th>Email</th></tr>
  </thead>
  <tbody>
  </tbody>
  <tfoot>
    <tr>
      <td colspan="3"><button onclick="javascript:addRow()">Add</button></td>
    </tr>
  </tfoot>
</table>
<button onclick="javascript:save()">Save</button>

Editoitavan rivin lisääminen taulukkoon on melko työlästä, koska jokainen taulukon solu ja siinä oleva input pitää rakentaa ja lisätä manuaalisesti.

function addRow(){
  var table = document.getElementsByTagName('table')[0];
  var body = table.getElementsByTagName('tbody')[0];
  var row = document.createElement('tr');
  
  var cell1 = document.createElement('td');
  var input1 = document.createElement('input');
  input1.setAttribute('type', 'text');
  cell1.appendChild(input1);
  row.appendChild(cell1);
  
  var cell2 = document.createElement('td');
  var input2 = document.createElement('input');
  input2.setAttribute('type', 'text');
  cell2.appendChild(input2);
  row.appendChild(cell2);
  
  var cell3 = document.createElement('td');
  var input3 = document.createElement('input');
  input3.setAttribute('type', 'email');
  cell3.appendChild(input3);
  row.appendChild(cell3);
    
  body.appendChild(row);
}

Tietojen lukeminen vaatii myös melkoisesti manuaalista pyörittelyä ja html:n rakenteen tuntemista

function save(){
  var table = document.getElementsByTagName('table')[0];
  var body = table.getElementsByTagName('tbody')[0];
  var rows = body.getElementsByTagName('tr');
  var data = [];
  for(var i = 0; i < rows.length; i++){
    var inputs = rows[i].getElementsByTagName('input');
    data.push({
      firstName: inputs[0].value,
      lastName: inputs[1].value,
      email: inputs[2].value
    });
  }
  console.log(data);
}

+ Ei tarvitse kirjastoja
- Paljon (toistuvaa) koodia
- Skripti tiukasti sidottu html:n rakenteeseen
    Sarakkeiden järjestyksen vaihtaminen
    Taulukon vaihtaminen esim. Flexboxiin
    jne. muutokset työläitä ja virhealttiita

jQuery

JQueryssä DOM-elementteihin pääsee käsiksi css:stä lainatuilla valitsimilla. Eikä elementtejäkään tarvitse rakentaa käsin vaan pystyy vaan lisäämään html-pätkän. Nämä olivat huomattavia parannuksia tuottavuuteen silloin ~10v sitten. Eo. html käy myös tähän, mutta skripti lyhenee melkoisesti.

function addRow(){    
  $('table > tbody').append('<tr><td><input type="text" /></td><td><input type="text" /></td><td><input type="email" /></td></tr>');
}
function save(){
  var data=[];
  $('table > tbody > tr').each(function(){
    data.push({
      firstName: $(this).find('input').eq(0).val(),
      lastName: $(this).find('input').eq(1).val(),
      email: $(this).find('input').eq(2).val(),
    });
  });
  console.log(data);
} 

+ Merkittävästi vähemmän koodia kuin pelkkää JavaScriptiä käytettäessä
+ Kuitenkin kohtuullisen kokoinen kirjasto
- Edelleen skripti tiukasti sidottu html:ään

Knockout

Knockout oli itselle ensimmäinen mvvm-patternia käyttävä kirjasto. Samaan sukupolveen taitaa kuulua mm. Backbone ja Ember. Knockout erottelee datan ja sen esitystavan toisistaan. Html:ssä käytetään erityisiä attribuutteja (data-bind), joilla saadaan mm. luupattua (foreach) ja tehtyä kaksisuuntaisia sidontoja (value) käyttöliittymäkomponentin ja datan välille.

<table>
  <thead>
    <tr><th>First name</th><th>Last name</th><th>Email</th></tr>
  </thead>
  <tbody data-bind="foreach: people">
    <tr>
      <td><input type="text" data-bind="value: firstName" /></td>
      <td><input type="text" data-bind="value: lastName" /></td>
      <td><input type="email" data-bind="value: email" /></td>
    </tr>
  </tbody>
  <tfoot>
    <tr><td colspan="3"><button data-bind="click: addRow">Add</button></td>
  </tfoot>
</table>
<button data-bind="click: save">Save</button>

Skripteissä ei tarvitse tietää html:n rakenteesta. Tämä mahdollistaa myös paremmin saman koodin käytön eri paikoissa.

function AppViewModel(){
  var self = this;
  this.people = ko.observableArray([]);
  this.addRow = function(){
    self.people.push(new PersonViewModel());
  }
  this.save = function(){
    var data = [];
    for(var i = 0; i < this.people().length; i++){
      var person = self.people()[i];
      data.push({
        firstName: person.firstName(),
        lastName: person.lastName(),
        email: person.email()
      });
    }
    console.log(data);
  }
}
function PersonViewModel(){
  this.firstName = ko.observable('');
  this.lastName = ko.observable('');
  this.email = ko.observable('');
}

window.addEventListener('load', function () {
  ko.applyBindings(new AppViewModel());
});

+ Skripteissä ei enää tarvitse tietää minkälainen html:n rakenne oli
+ ViewModeleita pystyy käyttämään monessa eri paikassa
+ Kohtuullisen kokoinen kirjasto
- Erikoisattribuuttien opettelu
- Toistuvaa boilerplate-koodia
    ViewModelien kentät pitää kääriä ko.observable -tyyppisesti ja purkaa luettaessa

Angular

Angular (2+) käyttää samantyyppistä lähestymistapaa kuin Knockout erikoisine attribuutteineen ja siksi html onkin melko samanlaista.

<table>
  <thead>
    <tr><th>First name</th><th>Last name</th><th>Email</th></tr>
  </thead>
  <tbody>
   <tr *ngFor="let person of people">
      <td><input type="text" [(ngModel)]="person.firstName" /></td>
      <td><input type="text" [(ngModel)]="person.lastName" /></td>
      <td><input type="email" [(ngModel)]="person.email" /></td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <td colspan="3"><button (click)="addRow()">Add</button></td>
    </tr>
  </tfoot>
</table>
<button (click)="save()">Save</button>

Skripti sen sijaan on kovin erilaista, koska kieli vaihtuu TypeScriptiksi

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  people: any[] = [];
  addRow(): void {
    this.people.push({ });
  }
  save(): void {
    console.log(this.people);
  }
}

+ Komponenteissa ei boilerplate-koodia
+ Aidosti uudelleenkäytettävät komponentit
- Vaatii koodin kääntämisen
- Sisältää paljon sellaista toiminnallisuutta mitä ei ehkä tarvitse kasvattaen näin kokoa
- Erikoisattribuuttien opettelu

React

React oli ensimmäinen uusimman sukupolven kirjasto mihin tutustuin. Kummallista kyllä React huononsi tuottavuutta verrattuna aiemmin käyttämääni Knockoutiin. En tiedä oliko ensimmäinen laatuaan, mutta React mahdollisti sekä UIn että koodin niputtamisen samaan tiedostoon. React käänsi ajatuksen JavaScriptistä html-attribuuteissa toisin päin ja siinä käytetyssä JSX:ssä html (tai oikeastaan xml) onkin laitettu JavaScriptin sekaan.

import React, { Component } from 'react';

class App extends Component {
  constructor(props){
    super(props);
    this.state = {
      people: []
    }
    this.addRow = this.addRow.bind(this);
    this.change = this.change.bind(this);
    this.save = this.save.bind(this);
  }
  addRow() {
    this.setState({ people: [
      ...this.state.people,
      { firstName: '', lastName: '', email: '' }
    ]);
  }
  change(index, field, evt){
    const people = this.state.people.slice();
    people[index][field] = evt.target.value;
    this.setState({ people: people });
  }
  save(){
    console.log(this.state.people);
  }
  render() {
    return (
      <div>
        <table>
          <thead>
            <tr><th>First name</th><th>Last name</th><th>Email</th></tr>
          </thead>
          <tbody>
          {this.state.people.map((person, index) => 
            <tr>
              <td><input type="text" value={person.firstName} onChange={e => this.change(index, 'firstName', e)} /></td>
              <td><input type="text" value={person.lastName} onChange={e => this.change(index, 'lastName', e)} /></td>
              <td><input type="email" value={person.email} onChange={e => this.change(index, 'email', e)} /></td>
            </tr>
          )}
          </tbody>
          <tfoot>
            <tr>
              <td colSpan="3"><button onClick={this.addRow}>Add</button></td>
            </tr>
          </tfoot>
        </table>
        <button onClick={this.save}>Save</button>
      </div>
    );
  }
}

export default App;

+ Aidosti uudelleenkäytettävät komponentit
+ Ei erikoisia attribuutteja muistettavaksi
+ Kompakti kirjasto
- Paljon boilerplate-koodia (esim. onChange)
- Sulkujen mätsääminen renderissä / lopullisen html:n hahmottaminen vaikeaa, jos on isompi komponentti

Vue.js

Yksi kolmesta (muut ovat React ja Angular) käytetyimmästä nykyisessä sukupolvessa. Komponentit voi tehdä yhteen tiedostoon ja html:ssä käytetään erikoisattribuutteja, kuten Knockoutissa ja Angularissa.

<template>
  <div>
    <table>
      <thead>
        <tr><th>First name</th><th>Last name</th><th>Email</th></tr>
      </thead>
      <tbody>
        <tr v-for="person in people">
          <td><input type="text" v-model="person.firstName" /></td>
          <td><input type="text" v-model="person.lastName" /></td>
          <td><input type="email" v-model="person.email" /></td>
        </tr>
      </tbody>
      <tfoot>
        <tr>
          <td colspan="3"><button @click="addRow">Add</button></td>
        </tr>
      </tfoot>
    </table>
    <button @click="save">Save</button>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
  people: any[] = [];
  addRow(): void {
    this.people.push({ });
  }
  save(): void {
    console.log(this.people);
  }
}
</script>

+ Ei boilerplate-koodia
+ Aidosti uudelleenkäytettävät komponentit
+ Kompakti kirjasto
- Erikoisattribuuttien opettelu