Prof. Dr.-Ing. Oliver Radfelder
Informatik / Wirtschaftsinformatik
Hochschule Bremerhaven
Kurz und Knapp: Javascript

Loggen Sie sich auf hopper ein. Gehen Sie in Ihr Webverzeichnis /var/www/html/IHR-ACCOUNT/ und öffnen Sie den Editor vim mit dem Dateinamen javascriptexample.html.

$ vim javascriptexample.html
      
und geben Sie langsam und mit korrekter Formatierung den folgenden HTML-Code ein:
<!doctype html>
<html>
  <head>
    <title>Javascript-Seite</title>
    <meta charset='utf-8'>
  <script>
  //hier steht der Javascript Code ...
  </script>
  </head>
  <body>
  <h1 id='myheader'>Javascript-Seite</h1>
  </body>
</html>
      

Dann gehen Sie mit Ihrem Browser auf https://informatik.hs-bremerhaven.de/IHR-ACCOUNT/javascriptexample.html

Für die folgenden Beispiele gilt, dass der gezeigte Code innerhalb des Script-Tags steht. Javascript-Code innerhalb eines solchen Tags wird während des Ladens ausgeführt. Öffnen Sie in Ihrem Browser (mit F12) die Konsole - sie dient als einfaches Ausgabeinstrument.

Tragen Sie nun innerhalb des script-Tags eine Zeile Code ein:

  console.log("hello world")
      

Laden Sie (mit Strg-F5) die Seite nun neu und Sie sehen, dass in dem Konsolen-Fenster hello world! ausgegeben wird. Die Anweisung console.log(...) entspricht also in etwa echo in PHP oder der Bash oder System.out.println() in Java. Das vordefinierte Objekt console ist in jeder Javascript-Umgebung verfügbar und log(...) ist eine Methode des Objektes, der Logmeldungen übergeben werden können. Die Ausgabe ist vor allem für Debugging-Zwecke hilfreich - in realen Anwendungen wird man mit Javascript das Dokument selbst verändern. Dazu später mehr.

In Javascript gibt es - wie üblich - Variable, Schleifen, Bedingungen, Arrays und Funktionen:

let zahl = 7
let name = 'oliver'
let einarray = ['eins','zwei','drei']
for (let i=0; i<einarray.length; ++i) {
  console.log('element '+ i + ': ' + einarray[i])
}
function counter(max){
  let j=0
  while(j<max){
    j++
    if ( j==2 ) {
      console.log("bei zwei")
    }
  }
}
counter(5)
      

Arrays - die in Javascript dynamisch sind - und Objektstrukturen werden mit Literalen erzeugt:


// Arrays
let arr = ['eins','zwei','drei']
arr[3] = 'vier'
arr.push('fuenf')  // hinten anfuegen
arr.pop() // hinten entfernen
arr.unshift('null')  // vorne anfuegen
arr.shift()  // vorne entfernen
for(let i=0;i<arr.length;++i)
   console.log(arr[i])
// Objekte
let obj = {}
obj.vorname = 'Peter'
obj.nachname = 'Petersen'

      

Eine Besonderheit gibt es allerdings: In Javascript sind Funktionen Objekte erster Ordnung. Das bedeutet, sie lassen sich beispielsweise Variablen zuweisen und später über den Variablennamen aufrufen:

function meineFunktion(arg){
  console.log("in einer Funtion mit Argument: "+arg)
}

meineFunktion("normal")   //normaler Aufruf

let fkt = meineFunktion   //Zuweisung an Variable
fkt("indirekt")

      

Damit lassen sich recht gut typische Callback Strukturen aufbauen:

function meineFunktion(fkt){
  fkt("einWert")
}

function meinCallback(val){
  console.log(val)
}

meineFunktion(meinCallback)  //Achtung: keine Klammern ...

//auch als *anonyme* Funktion mitten im Funktionsaufruf ...
meineFunktion(function(val) { 
                console.log(val)
              })
      

Das mag zunächst ungewohnt und abstrakt aussehen - allerdings arbeitet praktisch jeder Javascript-Code mit diesem Mechanismus. So lässt sich beispielsweise eine Funktion definieren, die einmal pro Sekunde aufgerufen wird, indem sie an die (vordefinierte) Funktion setInterval übergeben wurde:

function gibDatumAus(){
  console.log("Es ist: " + Date.now())
}

window.setInterval(gibDatumAus,1000)

      

Das Objekt window ist ebenfalls in einer Browser-Umgebung immer verfügbar.

In Javascript wird stark Event-basiert programmiert: das bedeutet, dass die Umgebung (der Browser) bestimmte Ereignisse verwaltet. Tritt das Ereignis ein, wird der dafür hinterlegte Code ausgeführt. So hängt man sich meist recht früh an das Ereignis "das-Html-Dokument-wurde-vollständig-geladen" an, indem die Objekteigenschaft onload des window-Objektes mit einer Funktionsreferenz belegt wird. Eigenschaften von Objekten werden ebenfalls über die bekannte Punktnotation angesprochen. So wird die Eigenschaft onload mit einer Funktionsreferenz belegt:

let start = Date.now()

function fertigGeladen(event){
  console.log("Das Dokument ist fertig geladen "+event)
  let jetzt = Date.now()
  console.log("... es hat "+ ( jetzt-start ) +"ms benötigt")
}

window.onload=fertigGeladen
      

Was uns zu dem dritten vordefinierten Objekt bringt: document. Dieses Objekt lässt uns auf das dargestellte Dokument und seine Baumstruktur zugreifen; und zwar zumeist mit dem Aufruf von getElementById(...). Vielleicht ist Ihnen aufgefallen, dass das h1-Element in dem obigen HTML-Dokument noch ein Attribut namens id mit dem Wert myheader hat. Damit können wir uns das Element holen und dessen Eigenschaft innerHTML dann mit einem neuen Wert belegen:

function setHeader() {
  let myheader = document.getElementById("myheader")
  myheader.innerHTML = "Es ist: "+Date.now()
} 
function fertigGeladen(event){
  window.setInterval(setHeader,1000)
}

window.onload=fertigGeladen
      

Der obige Ausschnitt ist schon recht knackig - überlegen Sie einmal, weshalb hier der setInterval-Aufruf erst in dem onload-Callback auftritt.

Mit diesen Mitteln lässt sich bereits ein minimaler Ajax-Aufruf durchführen - die Grundlage des Web 2.0 schlechthin. Einzig das new könnte noch unbekannt sein: Damit werden bestimmte Objekte in Javascript erzeugt, die dann wie in Java oder anderen OO-Sprachen Eigenschaften haben, an die sich über die Punkt-Notation herankommen lässt. Wann Sie new benötigen und wann Sie lediglich eine Funktion aufrufen, die ein Objekt zurückliefert, ergibt sich mit der Zeit und mit der Gewohnheit.

let req = new XMLHttpRequest()
function angekommen(){
  if ( req.readyState == 4 && req.status == 200 ) {
    console.log(req.responseText)
  }
}
req.onreadystatechange = angekommen
req.open("GET","eintext.txt")
req.send()
      

In dem obigen Code wird zunächst ein XMLHttpRequest-Objekt erzeugt und der Variablen req zugewiesen. Dann wird die Funktion angekommen definiert, die aufgerufen werden soll, wenn die Antwort des HTTP-Requests angekommen ist. Damit das auch geschieht, wird die eben definierte Funktion an die Objekteigenschaft onreadystatechange des req-Objektes gebunden. Im vorletzten Schritt wird noch der Request geöffnet mit Angabe, ob GET oder POST genutzt werden soll und es wird das angeforderte Dokument übergeben (denken Sie daran, wir befinden uns immer noch in der HTTP-Welt). Nun ist alles vorbereitet und der Request kann mit send() abgeschickt werden.

Der obige Code funktioniert als Minimalbeispiel, hat aber noch ein gewichtiges Problem: Ajax steht für Asynchronous Javascript And XML. Deutlich wird das, wenn Sie nach dem send-Aufruf direkt einen console.log()-Aufruf einfügen und feststellen, dass die Ausgabe vor der Antwort erscheint. Das ist auch gut und richtig und sollte so sein. Allerdings haben Sie jetzt das Problem, dass Sie nicht zwei parallele Ajax-Aufrufe durchführen können, weil die Variable req global ist. Das ließe sich auch anders lösen, aber Javascript hat dafür eine weitere Anlehnung aus der funktionalen Programmierung zu bieten: Closures. Wenn Sie innerhalb einer Funktion eine Funktion definieren, wird der momentane Zustand der in der äusseren Funktion definierten lokalen Variablen festgehalten.

function meineFunktion(arg){
  let aeussereVariable=arg
  function oncePerSecond(){
    console.log(aeussereVariable)
  }
  window.setInterval(oncePerSecond,1000)
}

meineFunktion("eins")
meineFunktion("zwei")
      

Wie man sieht, wird die Funktion als Referenz an setInterval in der Funktion meineFunktion übergeben. Danach ist die Abarbeitung des Funktionskörpers beendet. Allerdings kann später noch auf die (vormals) belegte Variable aeussereVariable zugegriffen werden. Wer viel Java und C++ gewohnt ist, für den oder die ist das ein ungewöhnliches Konzept - und sicherlich beim ersten Ansehen noch nicht in all der Mächtigkeit zu erfassen. Je mehr man aber damit programmiert, desto gewohnter wird es, so zu denken - und Javascript versteht möglicherweise erst richtig, wer sich das auch in eigenem Code zu Nutze macht.

Jedenfalls ist das der Grund, weshalb die meisten Ajax-Beispiele, die Sie im Web finden, eher so aussehen:

function load(doc,logStatement) {
  let req = new XMLHttpRequest()
  req.onreadystatechange = function() {
    if (req.readyState == 4 && req.status == 200) {
     console.log(logStatement+":"+req.responseText)
    }
  }
  req.open("GET", doc )
  req.send()
}
load("eintext.txt","eintext.txt kam an")
load("einanderer.txt","kam auch an")
      

Nun haben wir das Problem nicht mehr - wir haben keine globale Variable und wir können beliebig viele Dokumente asynchron anfordern - das als zweites Argument übergebene Logstatement wird immer das richtige sein. (Wiederholen Sie das Laden mit STRG-F5 mehrmals und schauen Sie, welches zuerst ankommt.)

Und wenn Sie jetzt mutig sind und versuchen wollen zu verstehen, ob Sie verstanden haben, übergeben Sie doch mal eine Funktion als zweites Argument, die dann aufgerufen wird, wenn der Response angekommen ist. Das ist dann wirklich modernes Event-basiertes, funktionales Programmieren (und im Kern etwa 60 Jahre alt ...)

Das Dokument, das Sie mit einem Ajax-Request anfordern, kann natürlich auch ein php-Request sein. Probieren Sie es aus.

Ein GET-Request darf bekanntlich gecached werden - für unsere Anwendungen nicht unbedingt förderlich... Wenn Sie ein Neuladen mit jedem Request erzwingen wollen, müssen Sie dafür sorgen, dass die URL jedesmal anders aussieht. Ein nicht unübliches Verfahren ist, nach dem Fragezeichen die aktuelle Zeit (in Millisekunden) als Parameter mitzugeben:

  ...
  req.open("GET", doc+"?time="+Date.now() )
  ...
      

Alternativ zu dem Mechanismus mit onreadystatechange gibt es noch die etwas modernere Variante mit onload und gegebenenfalls mit onerror, wobei diese Funktion nur einmalig bei erfolgreichem Response aufgerufen wird, was sich etwas kürzer liest:

function load(doc,logStatement) {
  let req = new XMLHttpRequest()
  req.onload = function(e) {
    let xhr = e.target
    console.log(logStatement+":"+xhr.status)
  }
}
  req.open("GET", doc )
  req.send()
}
load("eintext.txt","eintext.txt kam an")
      

Dazu sieht man, dass statt das XMLHttpRequest aus dem äußeren Context zu nehmen, es etwas klarer ist, es sich innerhalb der onload-Funktion von dem übergebenen Event-Objekt zu holen. Das geht ebenfalls bei der onreadystatechange-Variante.

Wenn Sie Daten zwischen Ihrer php- oder java-Anwendung und Javascript übergeben wollen, nutzen Sie in Javascript die Funktionen JSON.stringify(obj) und JSON.parse('...'). Auf php-Seite nutzen Sie json_encode($obj) und json_decode('...'). So können Sie Objektstrukturen (z.B. assoziative Arrays in php) vom Server per Ajax holen und als Javascript-Objekte nutzen.

Websockets

Um Websockets zu nutzen, brauchen Sie auf der Serverseite einen Websocket-Server, der auf einem eigenen Port lauscht. Bei uns ist websocketd installiert, den Sie mit:

websocketd --port 3000 meinskript.sh
      

starten können. Der Port 3000 wird als Webpfad über https://informatik.hs-bremerhaven.de/docker-IHRACCOUNT-websocket/ erreicht. Das Skript wird für jeden Connect aufgerufen und liest von Stdin Nachrichten und schreibt sie über Stdout raus.

Auf der HTML-Seite brauchen Sie den entsprechenden Javascript-Code:

let ws = new WebSocket(
'wss://informatik.hs-bremerhaven.de/docker-demo-websocket/')
console.log("try...")
ws.onopen=function (e) {
  console.log('connected')
  e.target.send("eins")
}
ws.onmessage=function (e) {
  document.getElementById("myheader").innerHTML=e.data
}
ws.onclose=function(e){
  console.log("closed")
}
      

Jetzt haben Sie - ähnlich einer TCP-Verbindung - die Möglichkeit, auf beiden Seiten jederzeit zu schreiben oder zu lesen.

Grafik mit dem Canvas

Mit dem Canvas-Element haben Sie in der Kombination von html und Javascript heutzutage die Möglichkeit, Grafiken und Animationen zu erstellen. Zunächst benötigen Sie in dem HTML-Code ein <cavas>-Element, das Sie mit einer ID versehen und sich wieder mit getElementById holen können.

let canvas=document.getElementById('mycanvas')
let context=canvas.getContext('2d')
context.clearRect(0,0,canvas.width,canvas.height)
context.fillStyle='rgb(200,0,0)'
context.fillRect(10,10,100,100)
context.strokeStyle='rgb(0,255,0)'
context.strokeRect(5,5,105,105)
context.lineWidth=3
context.beginPath()
context.moveTo(0,0)
context.lineTo(100,100)
context.stroke()
context.font = '48px serif'
context.fillText('Hello World',10,50)

      

Mit dem Funktionsaufruf requestAnimationFrame(fct) wird ein Neuzeichnen des Browsers angefordert. Übergeben wird eine Funktionsreferenz und der Browser wird versuchen, mit mindestens 60 fps die Bildrate animationsfreundlich zu halten.

function render(){
  let context ... // wie oben
  requestAnimationFrame(render)
}
window.onload=function(){
  // init
  requestAnimationFrame(render)
}

      

Für Weiterführendes schauen Sie unten in die Links.

Sonstiges:

Javascript ist auch auf dem Server möglich - wenn auch seltener zu sehen als im Browser. Bei uns in der Infrastruktur ist die Javascript-Engine node natürlich auf hopper und in Ihren Dockern installiert. Schreiben Sie doch einmal die folgende kleine Schleife in eine Datei namens demo.js in Ihrem Home-Verzeichnis ...

for (let i=0; i<10; ++i){
                 console.log("ich bin bei:"+i)
                 }
      

... und lassen Sie sie mit:

$ node demo.js
      

... laufen. Oder Sie nutzen Ihr Wissen, dass in dem Shebang auch ein Interpreter stehen kann, machen die Datei mit chmod ausführbar und rufen Sie direkt mit ./demo.js auf.

#!/usr/bin/node
for (let i=0; i<10; ++i){
                 console.log("ich bin bei:"+i)
                 }
      

An einigen Stellen werden Sie etwas modernere Varianten finden. Beispielsweise können Sie statt getElementById und getElementsByClassName in aktuellen Browsern querySelector und querySelectorAll nutzen, um vermittels css-Selektoren Elemente zu suchen:

let element = document.querySelector("#myspecialid")
let elementList = document.querySelectorAll('.myimageclass')
//in dem div mit der class mycontent ein input mit dem Namen login:
let special = document.querySelector("div.mycontent input[name='login']")

      

Es gibt einige spezifische APIs. Beispielsweise das DeviceOrientation-API:

function orientation_change(e) {
    let alpha = Math.round(e.alpha/100)*100
    let beta  = Math.round(e.beta/100)*100
    let gamma = Math.round(e.gamma/100)*100
}
window.onload = function() {
    window.addEventListener("deviceorientation", orientation_change, false)
}
      

Mit nvm können Sie sich unterschiedliche Versionen von node und npm installieren:

wget https://raw.githubusercontent.com/creationix/nvm/master/install.sh 
bash install.sh
source ~/.bashrc
nvm ls-remote
nvm install node
nvm install node --lts
nvm ls
node --version
nvm use --lts
...
nvm use default
mkdir ~/.npm-global
# in .bashrc
export NPM_CONFIG_PREFIX=~/.npm-global
      

Wenn Sie dieses Tutorial durchgearbeitet haben, schauen Sie doch mal bei Ulrike Erb auf der Webseite vorbei: dort gibt es ein wunderbares Leaflet-Tutorial.

Weiterführendes