HTML5-Tutorium: JavaScript: Hello World Vue 05: Unterschied zwischen den Versionen

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Kowa (Diskussion | Beiträge)
Kowa (Diskussion | Beiträge)
Keine Bearbeitungszusammenfassung
Zeile 19: Zeile 19:
Erstellen Sie einen neuen Projektzweig (branch) innerhalb von <code>hello_world_vue</code> und fügen Sie das Package <code>uuid</code> hinzu:
Erstellen Sie einen neuen Projektzweig (branch) innerhalb von <code>hello_world_vue</code> und fügen Sie das Package <code>uuid</code> hinzu:


<source lang="bash">
<syntaxhightlight lang="bash">
git checkout v04    # Wechsle in den Branch v04
git checkout v04    # Wechsle in den Branch v04
git checkout -b v05  # Klone v04 in einen neuen Branch v05
git checkout -b v05  # Klone v04 in einen neuen Branch v05
npm i
npm i
</source>
</syntaxhightlight>


== Kofigurationsdateien ==
== Kofigurationsdateien ==
Zeile 31: Zeile 31:
Der Inhalt wird in die App fest eingebunden und kann in der laufenden Anwendung ohnen einen Rebuild nicht geändert werden.
Der Inhalt wird in die App fest eingebunden und kann in der laufenden Anwendung ohnen einen Rebuild nicht geändert werden.


<source lang="json">
<syntaxhightlight lang="json">
// src/json/config.json
// src/json/config.json


{ "startSection": "form" }
{ "startSection": "form" }
</source>
</syntaxhightlight>


=== Internationalisierungsdateien ===
=== Internationalisierungsdateien ===
Zeile 42: Zeile 42:
Sie dienen der Internationalisierung (internationalization = i..................n = i18n).
Sie dienen der Internationalisierung (internationalization = i..................n = i18n).


<source lang="json">
<syntaxhightlight lang="json">
// src/json/i18n_de.json
// src/json/i18n_de.json


Zeile 57: Zeile 57:
   }
   }
}
}
</source>
</syntaxhightlight>


<source lang="json">
<syntaxhightlight lang="json">
// src/json/i18n_en.json
// src/json/i18n_en.json


Zeile 74: Zeile 74:
   }
   }
}
}
</source>
</syntaxhightlight>


Er können jederzeit I18n-Dateien für weitere Sprachen definiert werden.
Er können jederzeit I18n-Dateien für weitere Sprachen definiert werden.
Zeile 259: Zeile 259:
Dazu wird die Version v05 erweitert:
Dazu wird die Version v05 erweitert:


<source lang="ecmascript">
<syntaxhightlight lang="javascript">
git checkout v05      # Wechsle in den Branch v05
git checkout v05      # Wechsle in den Branch v05
git checkout -b v05a  # Klone v05 in einen neuen Branch v05a
git checkout -b v05a  # Klone v05 in einen neuen Branch v05a
</source>
</syntaxhightlight>


=== Internationalisierungsdateien ===
=== Internationalisierungsdateien ===
Zeile 269: Zeile 269:
Beachten Sie, dass sie in den Ordner "public/json" verschoben werden.
Beachten Sie, dass sie in den Ordner "public/json" verschoben werden.


<source lang="json">
<syntaxhightlight lang="json">
// public/json/i18n_de.json
// public/json/i18n_de.json


Zeile 284: Zeile 284:
   }
   }
}
}
</source>
</syntaxhightlight>


<source lang="json">
<syntaxhightlight lang="json">
// public/json/i18n_en.json
// public/json/i18n_en.json


Zeile 301: Zeile 301:
   }
   }
}
}
</source>
</syntaxhightlight>


Er können jederzeit  I18n-Dateien für weitere Sprachen definiert werden.
Er können jederzeit  I18n-Dateien für weitere Sprachen definiert werden.
Zeile 309: Zeile 309:
Zunächst wird eine Service-Funktion definiert zum asynchronen Laden von JSON-Dateien.
Zunächst wird eine Service-Funktion definiert zum asynchronen Laden von JSON-Dateien.


<source lang="ecmascript">
<syntaxhightlight lang="javascript">
// src/service/getJson.js
// src/service/getJson.js


Zeile 332: Zeile 332:


export default getJson
export default getJson
</source>
</syntaxhightlight>


=== Globale Konfigurationsdatei ===
=== Globale Konfigurationsdatei ===


<source lang="json">
<syntaxhightlight lang="json">
// public/json/config.json
// public/json/config.json


Zeile 344: Zeile 344:
   "defaultLanguage": "de"
   "defaultLanguage": "de"
}
}
</source>
</syntaxhightlight>


<source lang="ecmascript">
<syntaxhightlight lang="javascript">
// src/main.js
// src/main.js


Zeile 371: Zeile 371:


window.addEventListener('load', init)
window.addEventListener('load', init)
</source>
</syntaxhightlight>


In der Datei <code>main.js</code> könnte man die I18n-Daten abhängig von der in der Datei <code>/json/config.json</code>
In der Datei <code>main.js</code> könnte man die I18n-Daten abhängig von der in der Datei <code>/json/config.json</code>
Zeile 416: Zeile 416:
* <code>SectionForm.vue</code>
* <code>SectionForm.vue</code>


<source lang="ecmascript">
<syntaxhightlight lang="javascript">
// src/store/StoreSession.js
// src/store/StoreSession.js


Zeile 447: Zeile 447:
)
)


</source>
</syntaxhightlight>


<source lang="html">
<syntaxhightlight lang="html">
<!-- src/App.vue -->
<!-- src/App.vue -->


Zeile 474: Zeile 474:
</style>
</style>


</source>
</syntaxhightlight>


=== <code>StoreI18n.js</code> ===
=== <code>StoreI18n.js</code> ===
Zeile 492: Zeile 492:
ausgeführt, das heißt die Initialisierungsfunktion zum Session-Store hinzugefügt.
ausgeführt, das heißt die Initialisierungsfunktion zum Session-Store hinzugefügt.


<source lang="ecmascript">
<syntaxhightlight lang="javascript">
// src/store/StoreI18n.js
// src/store/StoreI18n.js


Zeile 551: Zeile 551:


export default storeI18n
export default storeI18n
</source>
</syntaxhightlight>


<code>changeLang</code> enthält einen kleinen Hack, um die korrekte Sprache in das HTML-Tag der Datei "index.html" einzufügen.  
<code>changeLang</code> enthält einen kleinen Hack, um die korrekte Sprache in das HTML-Tag der Datei "index.html" einzufügen.  


<source lang="ecmascript">
<syntaxhightlight lang="javascript">
document.getElementsByTagName('html')[0].lang = lang
document.getElementsByTagName('html')[0].lang = lang
</source>
</syntaxhightlight>


Damit wird im Document Tree der Datei "index.html" jeweils die aktuelle Sprache verwendet:
Damit wird im Document Tree der Datei "index.html" jeweils die aktuelle Sprache verwendet:


<source lang="html">
<syntaxhightlight lang="html">
<html lang="de">
<html lang="de">
<html lang="en">
<html lang="en">
</source>
</syntaxhightlight>


=== <code>StoreGreeting.js</code> ===
=== <code>StoreGreeting.js</code> ===
Zeile 572: Zeile 572:
den String <code>$1</code> durch einen Namen ersetzt.
den String <code>$1</code> durch einen Namen ersetzt.


<source lang="ecmascript">
<syntaxhightlight lang="javascript">
// src/store/StoreGreeting.js
// src/store/StoreGreeting.js


Zeile 597: Zeile 597:


...
...
</source>
</syntaxhightlight>


=== Autofokus-Funktionalität ===
=== Autofokus-Funktionalität ===
Zeile 604: Zeile 604:
müssen die beiden Formular-Komponenten <code>FormTextfield.vue</code> und <code>SectionForm.vue</code> entsprechend angepasst werden.
müssen die beiden Formular-Komponenten <code>FormTextfield.vue</code> und <code>SectionForm.vue</code> entsprechend angepasst werden.


<source lang="ecmascript">
<syntaxhightlight lang="javascript">
<!-- src/components/FormTextfield.vue -->
<!-- src/components/FormTextfield.vue -->
   import { computed, onMounted, onBeforeUnmount } from 'vue'
   import { computed, onMounted, onBeforeUnmount } from 'vue'
Zeile 640: Zeile 640:
   )
   )
...
...
</source>
</syntaxhightlight>




<source lang="html">
<syntaxhightlight lang="html">
<!-- src/components/SectionForm.vue -->
<!-- src/components/SectionForm.vue -->


Zeile 651: Zeile 651:
...
...
</template>
</template>
</source>
</syntaxhightlight>


== Benutzung eines Backend-Servers (v05b) ==
== Benutzung eines Backend-Servers (v05b) ==
Zeile 659: Zeile 659:
Dazu wird die Version v05 erweitert:
Dazu wird die Version v05 erweitert:


<source lang="ecmascript">
<syntaxhightlight lang="javascript">
git checkout v05a    # Wechsle in den Branch v05a
git checkout v05a    # Wechsle in den Branch v05a
git checkout -b v05b  # Klone v05a in einen neuen Branch v05b
git checkout -b v05b  # Klone v05a in einen neuen Branch v05b
</source>
</syntaxhightlight>


Als ersten Backend-Server verwenden wir den JSON-Server, einen Fake-REST-Server.
Als ersten Backend-Server verwenden wir den JSON-Server, einen Fake-REST-Server.


<source lang="ecmascript">
<syntaxhightlight lang="javascript">
npm i -D json-server
npm i -D json-server
</source>
</syntaxhightlight>


Um ihn mittels "npm run server" zu starten, wird folgendes Skript in die Datei package.json eingefügt.
Um ihn mittels "npm run server" zu starten, wird folgendes Skript in die Datei package.json eingefügt.


<source lang="json">
<syntaxhightlight lang="json">
"server": "npx json-server --watch db.json --port 4000"
"server": "npx json-server --watch db.json --port 4000"
</source>
</syntaxhightlight>


=== <code>JSON-Server-Daten</code> ===
=== <code>JSON-Server-Daten</code> ===


<source lang="json">
<syntaxhightlight lang="json">
// db.json
// db.json


Zeile 713: Zeile 713:
   ]
   ]
}
}
</source>
</syntaxhightlight>


Testen Sie dann Ihren JSON-Server, indem Sie ihn starten und dann in der Datei <code>test.rest</code>
Testen Sie dann Ihren JSON-Server, indem Sie ihn starten und dann in der Datei <code>test.rest</code>
auf die jeweiligen Send-Request-Anweisungen klicken:
auf die jeweiligen Send-Request-Anweisungen klicken:
<source lang="bash">
<syntaxhightlight lang="bash">
npm run server
npm run server
Zeile 743: Zeile 743:
GET http://localhost:4000/v1/en/phrases HTTP/1.1
GET http://localhost:4000/v1/en/phrases HTTP/1.1
</source>
</syntaxhightlight>


=== Vue-Server als Proxy ===
=== Vue-Server als Proxy ===


<source lang="ecmascript">
<syntaxhightlight lang="javascript">
// vite.config.js
// vite.config.js


Zeile 767: Zeile 767:
   },
   },
})
})
</source>
</syntaxhightlight>


Starten Sie nun beide Server und testen Sie die Hello-World-Anwendung Browser:
Starten Sie nun beide Server und testen Sie die Hello-World-Anwendung Browser:
<source lang="bash">
<syntaxhightlight lang="bash">
npm run server  # 1. Terminal
npm run server  # 1. Terminal
npm run dev    # 2. Terminal
npm run dev    # 2. Terminal
Zeile 781: Zeile 781:
http://localhost:4000/v1/de
http://localhost:4000/v1/de
http://localhost:4000/v1/en
http://localhost:4000/v1/en
</source>
</syntaxhightlight>


Die Vue-URLs funktionieren im Browser, aber im Gegensatz zu den JSON-Server-URLs nicht in <code>test.rest</code>, da Vue keine echte REST-API zur Verfügung stellt.
Die Vue-URLs funktionieren im Browser, aber im Gegensatz zu den JSON-Server-URLs nicht in <code>test.rest</code>, da Vue keine echte REST-API zur Verfügung stellt.
Zeile 789: Zeile 789:
Um auf den neuen REST-Server zuzugreifen, reicht es,
Um auf den neuen REST-Server zuzugreifen, reicht es,
die URL in der Datei <code>config.json</code> anzupassen:
die URL in der Datei <code>config.json</code> anzupassen:
<source lang="bash">
<syntaxhightlight lang="bash">
// public/json/config.json
// public/json/config.json


Zeile 797: Zeile 797:
   "defaultLanguage": "de"
   "defaultLanguage": "de"
}
}
</source>
</syntaxhightlight>


Die Dateien <code>public/json/i18n_de.json</code> und <code>public/json/i18n_en.json</code> werden nicht mehr benötigt.
Die Dateien <code>public/json/i18n_de.json</code> und <code>public/json/i18n_en.json</code> werden nicht mehr benötigt.
Zeile 803: Zeile 803:
== wk_express_hello_world (v00) ==
== wk_express_hello_world (v00) ==


<source lang="bash">
<syntaxhightlight lang="bash">
mkdir express_hello_world
mkdir express_hello_world
cd express_hello_world
cd express_hello_world
Zeile 812: Zeile 812:
# Dateien erstellen/bearbeiten
# Dateien erstellen/bearbeiten
npm run server
npm run server
</source>
</syntaxhightlight>


=== <code>package.json</code> ===
=== <code>package.json</code> ===


<source lang="json">
<syntaxhightlight lang="json">
// package.json
// package.json


Zeile 826: Zeile 826:
"type": "module",
"type": "module",
...
...
</source>
</syntaxhightlight>


=== <code>src/index.html</code> ===
=== <code>src/index.html</code> ===


<source lang="html">
<syntaxhightlight lang="html">
<!-- src/index.html -->
<!-- src/index.html -->


Zeile 850: Zeile 850:
   </body>
   </body>
</html>
</html>
</source>
</syntaxhightlight>


=== <code>src/server.js</code> ===
=== <code>src/server.js</code> ===


<source lang="ecmascript">
<syntaxhightlight lang="javascript">
// src/server.js
// src/server.js


Zeile 878: Zeile 878:


console.log(`Running on ${SERVER}`);
console.log(`Running on ${SERVER}`);
</source>
</syntaxhightlight>


== wk_express_hello_world (v01) ==
== wk_express_hello_world (v01) ==
Zeile 887: Zeile 887:
=== <code>src/server.js</code> ===
=== <code>src/server.js</code> ===


<source lang="ecmascript">
<syntaxhightlight lang="javascript">
import express, { response } from 'express'
import express, { response } from 'express'
import path    from 'path'
import path    from 'path'
Zeile 938: Zeile 938:


init()
init()
</source>
</syntaxhightlight>


== Benutzung von Strapi als Backend-Servers (v05c) ==
== Benutzung von Strapi als Backend-Servers (v05c) ==
Zeile 947: Zeile 947:


'''Konfiguration'''
'''Konfiguration'''
<source lang="ecmascript">
<syntaxhightlight lang="javascript">
// /public/json/config.json: Api-Root
// /public/json/config.json: Api-Root


Zeile 955: Zeile 955:
   "defaultLanguage": "de"
   "defaultLanguage": "de"
}
}
</source>
</syntaxhightlight>


'''Proxy'''
'''Proxy'''
<source lang="ecmascript">
<syntaxhightlight lang="javascript">
// /vite.config.js: Port 1337 und Rewrite-Regel
// /vite.config.js: Port 1337 und Rewrite-Regel


Zeile 972: Zeile 972:
     }
     }
   },
   },
</source>
</syntaxhightlight>


'''Datenextraktion'''
'''Datenextraktion'''
<source lang="ecmascript">
<syntaxhightlight lang="javascript">
...
...
   getI18n =
   getI18n =
Zeile 988: Zeile 988:
         },
         },
...
...
</source>
</syntaxhightlight>


== Fortsetzung des Tutoriums ==
== Fortsetzung des Tutoriums ==

Version vom 14. April 2025, 09:48 Uhr

Dieser Artikel erfüllt die GlossarWiki-Qualitätsanforderungen nur teilweise:

Korrektheit: 3
(zu größeren Teilen überprüft)
Umfang: 4
(unwichtige Fakten fehlen)
Quellenangaben: 3
(wichtige Quellen vorhanden)
Quellenarten: 5
(ausgezeichnet)
Konformität: 3
(gut)

Vorlesung WebProg

Inhalt | Teil 1 | Teil 2 | Teil 3 | Teil 4 | Teil 5 | Teil 6 | Vue 1 | Vue 2 | Vue 3 | Vue 4 | Vue 5 | Vue 6

Musterlösung
Git-Repository, git checkout v05, ... v05a, ... v05b, ... v05c

Anwendungsfälle (Use Cases)

Gegenüber dem ersten, zweiten und dritten Teil des Vue-Tutoriums ändern sich die die Anwendungsfälle zunächst nicht. Die Anwendung leistet also genau dasselbe wie zuvor.

Zum Schluss wird die Anwendung allerdings erweitert. Die Begrüßung erfolgt in der vom Browser als Favorit angegebenen Sprache, sofern die Begrüßungsdaten für diese Sprache hinterlegt wurden. Anderenfalls erfolgt die Begrüßung ein der Defaultsprache.

Aufgabe

Aufgabe: Konfigurieren und internationalisieren Sie die Anwendung mit Hilfe von dynamischen JSON-Dateien.

Erstellen eines neuen Projektzweigs

Erstellen Sie einen neuen Projektzweig (branch) innerhalb von hello_world_vue und fügen Sie das Package uuid hinzu:

<syntaxhightlight lang="bash"> git checkout v04 # Wechsle in den Branch v04 git checkout -b v05 # Klone v04 in einen neuen Branch v05 npm i </syntaxhightlight>

Kofigurationsdateien

Hauptkonfigurationsdatei

Die folgende Datei dient zur statischen Konfiguration der Anwendung. Der Inhalt wird in die App fest eingebunden und kann in der laufenden Anwendung ohnen einen Rebuild nicht geändert werden.

<syntaxhightlight lang="json"> // src/json/config.json

{ "startSection": "form" } </syntaxhightlight>

Internationalisierungsdateien

Die folgenden Dateien sollen zunächst statisch eingelesen werden. Sie dienen der Internationalisierung (internationalization = i..................n = i18n).

<syntaxhightlight lang="json"> // src/json/i18n_de.json

{ "phrases":

 { "hello": "Hallo, $1!"
 },
 "dictionary":
 { "stranger":       "Fremder",
   "askName":        "Wie heißen Sie?",
   "welcome":        "Willkommen zu Web-Programmierung!",
   "buttonReset":    "Reset",
   "buttonSayHello": "Begrüßung"
 }

} </syntaxhightlight>

<syntaxhightlight lang="json"> // src/json/i18n_en.json

{ "phrases":

 { "hello": "Hello, $1!"
 },
 "dictionary":
 { "stranger":       "Stranger",
   "askName":        "What's your name?",
   "welcome":        "Welcome to Web Programing!",
   "buttonReset":    "reset",
   "buttonSayHello": "say hello"
 }

} </syntaxhightlight>

Er können jederzeit I18n-Dateien für weitere Sprachen definiert werden.

Statische JSON-Dateien (v05)

Initialisierung von StoreSession.js mit Hilfe einer JSON-Datei

Die JSON-Datei config.json wird von StoreSession.js statisch eingelesen, um den zugehörigen Store zu initialisieren. Fügen Sie folgenden Import-Befehl in diese Datei ein:

import config from '@/json/config.json'

An dieser Stelle würde ich keine Vite-Alias '/json' definieren, sondern die Schreibweise '@/json/...' beibehalten. Der Grund ist, dass die JSON-Dateien später dynamisch aus dem Public-Bereich geladen werden sollen und der JSON-Ordner dort unter dem Namen '/json' zu finden sein wird.

Der statische Import von JSON-Dateien ist im ECMAScript-Standard nicht vorgesehen. Eigentlich müssen JSON-Dateien immer asynchron mit Hilfe spezieller AJAX-Anweisungen geladen werden.

Allerdings ist der statische Import in Transcodern wie webpack, vite etc. möglich. Hierbei wird der Import-Befehl bereits zur „Übersetzungszeit“ und nicht erst zur Laufzeit ausgeführt. Das heißt, der Inhalt der JSON-Datei wird in den erzeugten JavaScript-Code direkt eingefügt.

Nun kann der Name der Start-Section initialisiert werden:

const
  section   = ref(config.startSection),
...

Die Initialisierung von StoreSession erfolgt bislang in der Datei HelloWorld.vuemittels der Funition session.init. Entfernen Sie in der Date0 StoreSession.js die Init-Funktion und in der Datei HelloWorld.vue den zugehörigen Aufruf der Init-Funktion.

Sie können nun auch noch die Funktion "init" aus der Datei StoreSession.js entfernen, da die Initialisierung jetzt über JSON erfolgt.

StoreGreeting.js

Die Webanwendung soll internationalisiert werden. Dazu muss StoreGreeting.js um Internationalisierungstexte erweitert werden.

Die Konstante config wird wie in StoreSession.js definiert. In config.i18n sind Internationalisierungstexte enthalten. (Sie wurden in der Datei main.js separat geladen und eingefügt.) Die Datei StoreGreeting.js und die Komponenten, die diesen Store verwenden, müssen entsprechend erweitert werden.

Beachten Sie, dass f_hello gegenüber den früheren Implementierungen einen leeren Namen durch den Namen dictionary.stranger ersetzt. (Isabella, danke für den Tipp.)

Beachten Sie außerdem, dass f_hello früher sayHello hieß. Allerdings gibt es in SectionHello.vue eine Funktion mit gleichem Namen, aber anderer Funktionalität. Um Verwechslungen zu vermeiden, habe ich die (Hilfs-)Funktion in StoreGreeting.js umbenannt.

// src/store/StoreGreeting.js

import { defineStore }                       from 'pinia'
import { reactive, ref, computed, readonly } from 'vue'

import config from '@/json/i18n_de.json'

const
  storeGreeting =
  defineStore
  ( 'greeting',

    () =>
    { const
        phrases =
          reactive(config.phrases),

        dictionary =
          reactive(config.dictionary),

        name =
          ref(''),

        sayHello =
          p_name => phrases.hello.replace('$1', p_name === '' ? dictionary.stranger : p_name),
        //p_name => phrases?.hello?.replace('$1', p_name === '' ? dictionary.stranger : p_name),

        helloStranger =
          computed(() => sayHello(dictionary.stranger)),

        hello =
          computed(() => sayHello(name.value))/*,

        askName =
          computed(() => dictionary.askName),

        welcome =
          computed(() => dictionary.welcome),

        buttonReset =
          computed(() => dictionary.buttonReset),

        buttonSayHello =
          computed(() => dictionary.buttonSayHello)
        */
    
      return { name, helloStranger, hello, dictionary: readonly(dictionary)
               /*, askName, welcome, buttonReset, buttonSayHello */
             }
    }
)

export default storeGreeting

Die Methoden askName, welcome etc. würden die Verwendung des Stores einfacher machen, blähen aber die Datei stark auf, wenn i18n viele Begriffe enthält.

SectionHello.vue

Die Datei SectionHello.vue muss an die Internationalisierung angepasst werden, indem alle deutschen Wörter und Phrasen ersetzt durch Zugriffe auf den StoreGreeting ersetzt werden.

<!-- src/section/SectionHello.vue -->

<script setup>
  import storeSession  from '/store/StoreSession'
  import storeGreeting from '/store/StoreGreeting'

  const
    session    = storeSession(),
    greeting   = storeGreeting(),
    dictionary = greeting.dictionary
</script>

<template>
  <!-- const in code: 'hello' should be passed via a prop -->
  <section v-if="session.section==='hello'">
    <h1>{{greeting.hello}}</h1>
    <p>{{dictionary.welcome}}</p>
  </section>
</template>

<style scoped>
  @import '/css/component/section/SectionHello.css';
</style>

SectionForm.vue

Die Datei SectionHello.vue muss auf dieselbe Weise aktualisiert werden. Allerdings benötig man ein paar mehr Konstanten.

<!-- src/section/SectionForm.vue -->

<script setup>
  import FormButton    from '/component/form/FormButton.vue'
  import FormTextfield from '/component/form/FormTextfield.vue'

  import storeSession  from '/store/StoreSession'
  import storeGreeting from '/store/StoreGreeting'

  const
    session    = storeSession(),
    greeting   = storeGreeting(),
    dictionary = greeting.dictionary,
    sayHello   = () => session.section = 'hello'
</script>

<template>
  <!-- const in code: 'form' should be passed via a prop -->
  <section v-if="session.section==='form'">
    <h1>{{greeting.helloStranger}}</h1>
    <form>
      <div>
        <FormTextfield :label="dictionary.askName"
                       v-model:text="greeting.name"
                       @enter="sayHello"
                       autofocus
        />
      </div>
      <div>
        <FormButton :label="dictionary.buttonReset"    type="reset"  @click="session.setFocus"/>
        <FormButton :label="dictionary.buttonSayHello" type="button" @click="sayHello"/>
      </div>
    </form>
  </section>
</template>

<style scoped>
  ...
</style>

Dynamisches Laden von JSON-Dateien (v05a)

Aufgabe: Konfiguriere und internationalisiere die Anwendung mit Hilfe von dynamischen JSON-Dateien. Dazu wird die Version v05 erweitert:

<syntaxhightlight lang="javascript"> git checkout v05 # Wechsle in den Branch v05 git checkout -b v05a # Klone v05 in einen neuen Branch v05a </syntaxhightlight>

Internationalisierungsdateien

Die folgenden Dateien sollen dynamisch zur Laufzeit eingelesen werden. Beachten Sie, dass sie in den Ordner "public/json" verschoben werden.

<syntaxhightlight lang="json"> // public/json/i18n_de.json

{ "phrases":

 { "hello": "Hallo, $1!"
 },
 "dictionary":
 { "stranger":       "Fremder",
   "askName":        "Wie heißen Sie?",
   "welcome":        "Willkommen zu Web-Programmierung!",
   "buttonReset":    "Reset",
   "buttonSayHello": "Begrüßung"
 }

} </syntaxhightlight>

<syntaxhightlight lang="json"> // public/json/i18n_en.json

{ "phrases":

 { "hello": "Hello, $1!"
 },
 "dictionary":
 { "stranger":       "Stranger",
   "askName":        "What's your name?",
   "welcome":        "Welcome to Web Programing!",
   "buttonReset":    "reset",
   "buttonSayHello": "say hello"
 }

} </syntaxhightlight>

Er können jederzeit I18n-Dateien für weitere Sprachen definiert werden.

getJson.js

Zunächst wird eine Service-Funktion definiert zum asynchronen Laden von JSON-Dateien.

<syntaxhightlight lang="javascript"> // src/service/getJson.js

/**

* @function
* @param  { string } p_url - The URL of a JSON file zo be loaded
* @return { JSON }   The content of the JSON file
* @throws { Error }  If the file does not exists, an error is thrown
*/

async function getJson(p_url) { return fetch(p_url)

   .then
    (response =>
     { if (response.ok)
       { return response.json() }
       else
       { throw new Error(`'${response.url}' not found`) }
     }
    )

}

export default getJson </syntaxhightlight>

Globale Konfigurationsdatei

<syntaxhightlight lang="json"> // public/json/config.json

{ "startSection": "form",

 "apiRoot":         "/json/i18n_$1.json",
 "XapiRoot":        "/api/$1",
 "defaultLanguage": "de"

} </syntaxhightlight>

<syntaxhightlight lang="javascript"> // src/main.js

import { createPinia } from 'pinia' import { createApp } from 'vue' import getJson from '/service/getJson' import App from './App.vue'

const

 pinia   = createPinia(),
 app     = createApp(App),
 init    = async () =>
           { const
               config = await getJson('/json/config.json')
       
            config.i18n =
              await getJson(config.apiRoot.replace('$1', config.defaultLanguage))

             app
               .provide('config', config)
               .use(pinia)
               .mount('#app') // app is shown to the user
           }

window.addEventListener('load', init) </syntaxhightlight>

In der Datei main.js könnte man die I18n-Daten abhängig von der in der Datei /json/config.json festgelegten Defaultsprache nachladen. Dieses muss mit einem zweiten getJson-Befehl erfolgen, der ausgeführt wird, nachdem config.json vollständig geladen wurde.

Besser ist es allerdings einen allgemeineren Initialisierungsmechanismus zu etablieren, mit dem man anhängig von config.json verschiedene Daten vom Backend lädt, um die Anwendung zu initialisieren. Das machen wir später im nächsten Schritt.

StoreSession.js und StoreGreeting.js

Wir holen uns nun in den Stores StoreSession.js und StoreGreeting.js die Konfigurations-Infomationen aus dem von der Main initialisierten Config-Onjekt.

Auf dieses können wir mit Hilde der Vue-Funktion inject zugreifen.

// src/store/StoreSession.js

v05b

StoreSession.js

Wir ersetzen die Datei StoreSession.js durch die Datei StoreSession.js, die alle möglichen Informationen zu aktuellen Session enthält.

  • Name: StoreSession.js => StoreSession.js
  • import config => vue.inject
  • autofocusId enthält das aktuelle Formularfeld, das das der Fokus bei einem Autofokus gesetzt werden soll. Das ist wichtig, weil die Textfelder eine zufällig generierte id erhalten. Der Resetbutton soll beispielsweise nach einem Reset den Fokus auf das Autofokusfeld setzten. Er kennt aber die id nicht, kann sie aber im Session-Store erfagen (sofern das entsprechende Textfeld die id dort hinterlegt).
  • Die größte Änderung ist die Ergänzung einer Initialisierungsfunktionalität.

Die Konstante initializers enthält alle Initialisierungsfunktionen, die vor Start der Anwendung ausgeführt werden sollen. Diese werden von anderen Stores mit Hilfe der Funktion addInitializer hinzugefügt. Die Datei App.vue aktiviert die entsprechenden Stores und führt danach die Funktion initialize aus. Mit Hilfe des reaktiven Konstante isInitialized erfährt die App.vue-Komponente, wann alle Stores initialisiert sind und kann dann vom Lade-Bildschirm auf den eigentlichen Startbildschirm umschalten.

Man muss auch in allen Dateien, die auf StoreSession und StoreSession zugreifen, diese Werte durch storeSession und StoreSession ersezten. Diesen Vorgang nennt man Refactoring. Er wird von modernen IDEs üblicherweise (mal besser. mal schlechter) unterstützt.

  • Session.vue (Nachfolger von StoreSession; siehe nachfolgend)
  • App.vue (neue Version siehe nachfolgend)
  • HelloWorld.vue
  • FormTextField.vue
  • SectionForm.vue

<syntaxhightlight lang="javascript"> // src/store/StoreSession.js

import { defineStore } from 'pinia' import { ref, inject } from 'vue'

const storeSession = defineStore ( 'session',

 () =>
 { const
     config         = inject('config'), // contains the app configuration object
     currentSection = ref(config.startSection),
     
     autofocusId    = ref(null),
     focus          = () => { if (autofocusId.value != null) 
                                document.getElementById(autofocusId.value).focus()
                            },
     initializers   = [],
     isInitialized  = ref(false),
     addInitializer = p_initializer => 
                      initializers.push(new Promise(resolve => p_initializer(resolve))),
     initialize     = () => Promise.all(initializers)
                                   .then(() => isInitialized.value = true)
   return { currentSection, autofocusId, addInitializer, initialize, isInitialized }
 }

)

</syntaxhightlight>

<syntaxhightlight lang="html">

<script setup>

 import HelloWorld   from './components/HelloWorld.vue'
 import storeSession from '@/store/StoreSession'
 import storeI18n    from '@/store/StoreI18n'
 
 const session = storeSession()
 storeI18n(); // activate initialization of storeI18n
 session.addInitializer(p_resolve => setTimeout(p_resolve, 2000));  
             // fake initializer, in order to see the loading information
 session.initialize();

</script>

<template>

Loading ...

 <HelloWorld v-if="session.isInitialized"/>

</template>

<style>

 @import '/css/body';

</style>

</syntaxhightlight>

StoreI18n.js

StoreI18n.js übernimmt die I18n-Funktionalität von StoreGreeting.js. Damit ist die Internationalisierung in einem anwendungsunabhängigen Modul verfügbar. Das erhöht die Wiederverwendbarkeit dieses Stores.

Ergänzt wurden getI18n, eine Funktion zum Laden der I18n-Daten von einem Backendsystem, und changeLang, eine Funktion zum asynchronen Ändern der Sprache.

Außerdem gibt es einen Funktion initialize, die die I18n-Daten für die Defaultsprache lädt. Diese Funktion wird im Session-Store als Initialisierungsfunktion registriert.

Man bachate, dass im Skript-Bereich des Moduls App.vue der Store mittels storeI18n() erstmals aufgerufen und damit initialisiert wird. Insbesodere wird dabei der Befehl session.addInitializer(initialize) ausgeführt, das heißt die Initialisierungsfunktion zum Session-Store hinzugefügt.

<syntaxhightlight lang="javascript"> // src/store/StoreI18n.js

import { defineStore } from 'pinia' import { ref, reactive, inject } from 'vue' import getJson from '@/service/getJson' import storeSession from '@/store/StoreSession'

const

 storeI18n =
   defineStore
   ( 'i18n',
     () =>
     { const
         config =
           inject('config'),
         session =
          storeSession(),
         lang =
           ref(null),
         phrases /* Record<string, string> */ =
           reactive({}),
         dictionary /* Record<string, string> */=
           reactive({}),
         getI18n =
           async p_lang =>
                 { const
                     c_i18n_json =
                       await getJson(config.apiRoot.replace('$1', p_lang ?? lang.value))
                   Object.assign(phrases,    c_i18n_json.phrases);
                   Object.assign(dictionary, c_i18n_json.dictionary);
                 },
         changeLang =
           async (p_lang = null) =>
                 { lang.value = p_lang ?? config.defaultLanguage;
                   await getI18n(lang.value);
                   document.getElementsByTagName('html')[0].lang = lang.value;
                 },
         initialize = 
            async resolve =>
                  { await changeLang(config.defaultLanguage); 
                    resolve();
                  }
       session.addInitializer(initialize);
       return { lang, phrases, dictionary, changeLang }
     }
   )

export default storeI18n </syntaxhightlight>

changeLang enthält einen kleinen Hack, um die korrekte Sprache in das HTML-Tag der Datei "index.html" einzufügen.

<syntaxhightlight lang="javascript"> document.getElementsByTagName('html')[0].lang = lang </syntaxhightlight>

Damit wird im Document Tree der Datei "index.html" jeweils die aktuelle Sprache verwendet:

<syntaxhightlight lang="html"> === StoreGreeting.js === Der StoreGreeting.js von vereinfacht sich entsprechend. Er stellt nur noch Funktionen zur Verfügung, der in der Phrase phrases.hello den String $1 durch einen Namen ersetzt. // src/store/StoreGreeting.js import { defineStore } from 'pinia' import { ref, computed, readonly } from 'vue' import StoreI18n from './StoreI18n' const storeGreeting = defineStore ( 'greeting', () => { const i18n = StoreI18n(), phrases = i18n.phrases, dictionary = i18n.dictionary, f_hello = p_name => phrases.hello.replace('$1', p_name === '' ? dictionary.stranger : p_name), ... === Autofokus-Funktionalität === Damit der Reset-Button den Autofokus nach Löschen der Formularinhalten den Fokus auf das Textfeld setzen kann, müssen die beiden Formular-Komponenten FormTextfield.vue und SectionForm.vue entsprechend angepasst werden. import { computed, onMounted, onBeforeUnmount } from 'vue' ... const ..., session = storeSession(), ... doAutofocus = () => document.getElementById(id).focus(), ... session = storeSession(), reset = () => text.value = '' onMounted ( () => { controllerKey.add(); if (props.autofocus) { window.addEventListener('focus', doAutofocus); doAutofocus(); // If the surrounding form is reset, delete the text value. // This is not done automatically by vue. document.getElementById(id).closest('form').addEventListener('reset', reset); } } ) onBeforeUnmount ( () => { if (props.autofocus) { document.getElementById(id).closest('form').removeEventListener('reset', reset); window.removeEventListener('focus', doAutofocus); } controllerKey.remove() } ) ... == Benutzung eines Backend-Servers (v05b) == Aufgabe: Die I18N-Daten sollen von einem Backend-Server gelesen werden. Dazu wird die Version v05 erweitert: git checkout v05a # Wechsle in den Branch v05a git checkout -b v05b # Klone v05a in einen neuen Branch v05b Als ersten Backend-Server verwenden wir den JSON-Server, einen Fake-REST-Server. npm i -D json-server Um ihn mittels "npm run server" zu starten, wird folgendes Skript in die Datei package.json eingefügt. "server": "npx json-server --watch db.json --port 4000" === JSON-Server-Daten === // db.json { "v1": [ { "id": "de", "phrases": { "hello": "Hallo, $1!" }, "dictionary": { "stranger": "Fremder", "askName": "Wie heißen Sie?", "welcome": "Willkommen zu Web-Programmierung!", "buttonReset": "Reset", "buttonSayHello": "Begrüßung" } }, { "id": "en", "phrases": { "hello": "Hello, $1!" }, "dictionary": { "stranger": "Stranger", "askName": "What's your name?", "welcome": "Welcome to Web Programing!", "buttonReset": "reset", "buttonSayHello": "say hello" } } ] } Testen Sie dann Ihren JSON-Server, indem Sie ihn starten und dann in der Datei test.rest auf die jeweiligen Send-Request-Anweisungen klicken: npm run server // test.rest ### GET http://localhost:4000/ HTTP/1.1 ### GET http://localhost:4000/v1/ HTTP/1.1 ### GET http://localhost:4000/v1/de HTTP/1.1 ### GET http://localhost:4000/v1/en HTTP/1.1 ### GET http://localhost:4000/v1/en/dictionary HTTP/1.1 ### GET http://localhost:4000/v1/en/phrases HTTP/1.1 === Vue-Server als Proxy === // vite.config.js ... export default defineConfig ({plugins: [vue()], resolve: { ... }, server: { proxy: { '/api': { target: 'http://localhost:4000/', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '/v1') } } }, }) Starten Sie nun beide Server und testen Sie die Hello-World-Anwendung Browser: npm run server # 1. Terminal npm run dev # 2. Terminal http://localhost:5173/api http://localhost:5173/api/de http://localhost:5173/api/en http://localhost:4000/v1 http://localhost:4000/v1/de http://localhost:4000/v1/en Die Vue-URLs funktionieren im Browser, aber im Gegensatz zu den JSON-Server-URLs nicht in test.rest, da Vue keine echte REST-API zur Verfügung stellt. === Zugriff auf den REST-Server === Um auf den neuen REST-Server zuzugreifen, reicht es, die URL in der Datei config.json anzupassen: // public/json/config.json { "startSection": "form", "XapiRoot": "/json/i18n_$1.json", "apiRoot": "/api/$1", "defaultLanguage": "de" } Die Dateien public/json/i18n_de.json und public/json/i18n_en.json werden nicht mehr benötigt. == wk_express_hello_world (v00) == mkdir express_hello_world cd express_hello_world npm init -y npm i express dotenv npm i -D nodemon # Dateien erstellen/bearbeiten npm run server === package.json === // package.json ... "scripts": { "server": "nodemon -r dotenv/config src/server.js", "prod": "node -r dotenv/config src/server.js" }, "type": "module", ... === src/index.html === Express Hello World 01

Hello, World!

</syntaxhightlight>

src/server.js

<syntaxhightlight lang="javascript"> // src/server.js

import express from 'express' import path from 'path'

const

 { PORT    = 4000,
   SERVER  = `http://localhost:${PORT}`
 }         = process.env,
 c_app     = express(),
 c_dirname = path.resolve(path.dirname(process.argv[1]))

c_app.get ('/',

(req, res) =>
{ //console.log(c_dirname);
  res.sendFile('./index.html', {root: c_dirname});
}

);

c_app.listen(PORT);

console.log(`Running on ${SERVER}`); </syntaxhightlight>

wk_express_hello_world (v01)

Kopieren Sie das Array aus der Datei db.json der Version v05a in die Datei src/db.json und ersetzen Sie in dieser Datei zweimal "id" durch "lang. Den Inhalt dieser Datei geben wir nun mit Hilfe eines Express-Servers aus.

src/server.js

<syntaxhightlight lang="javascript"> import express, { response } from 'express' import path from 'path'

let db;

const

 { PORT    = 4000,
   SERVER  = `http://localhost:${PORT}`,
   DB      = `${SERVER}/db.json`
 }         = process.env ,
 c_app     = express(),
 c_dirname = path.resolve(path.dirname(process.argv[1]))

c_app .get ( '/',

 (req, res) =>
 { res.sendFile('./index.html', {root: c_dirname }); }

) .get ( '/db.json',

 (req, res) =>
 { res.sendFile('./db.json', {root: c_dirname }); }

) .get ( '/v1/:lang',

 (req, res) =>
 { //console.log(req.params.lang);
   const l_i18n = db.filter(i18n => i18n.lang === req.params.lang)[0];
   res.json(l_i18n);
 }

) .get ( '/v1/:lang/:type',

 (req, res) =>
 { //console.log(req.params.lang, req.params.type);
   const l_i18n = db.filter(i18n => i18n.lang === req.params.lang)[0];
   res.json(l_i18n[req.params.type]);
 }

);

async function init() { c_app.listen(PORT);

 db = await fetch(DB).then(response => response.json())
 console.log(`Server is running on ${SERVER}`)

}

init() </syntaxhightlight>

Benutzung von Strapi als Backend-Servers (v05c)

Erstellen Sie eine Strap-Datenbank gemäß den Vorgaben aus der Vorlesung (Miro).

Um darauf zuzugreifen, müssen Sie drei Dateien anpassen:

Konfiguration <syntaxhightlight lang="javascript"> // /public/json/config.json: Api-Root

{ "startSection": "form",

 "XapiRoot":        "/json/i18n_$1.json",
 "apiRoot":         "/api/$1",
 "defaultLanguage": "de"

} </syntaxhightlight>

Proxy <syntaxhightlight lang="javascript"> // /vite.config.js: Port 1337 und Rewrite-Regel

 ...
 server:
 { proxy:
   { '/api':
     { target:       'http://localhost:1337/',
       changeOrigin: true,
       rewrite:      path => path.replace(/^\/api\//, '/api/i18ns?filters[lang][$eq]=')
     }
   }
 },

</syntaxhightlight>

Datenextraktion <syntaxhightlight lang="javascript"> ...

 getI18n =
   async p_lang =>
         { const
             c_api_data = 
               await getJson(config.apiRoot.replace('$1', p_lang ?? lang.value))
             c_i18n_json =
               c_api_data.data[0].attributes;
           Object.assign(phrases,    c_i18n_json.phrases);
           Object.assign(dictionary, c_i18n_json.dictionary);
        },

... </syntaxhightlight>

Fortsetzung des Tutoriums

Sie sollten nun Teil 6 des Vue-Tutoriums bearbeiten. In diesem Tutorium setzen Sie Routing ein, auch wenn das für die Hello-World-Anwendung nicht sonderlich sinnvoll ist.

Quellen

  1. Kowarschick (WebProg): Wolfgang Kowarschick; Vorlesung „Web-Programmierung“; Hochschule: Hochschule Augsburg; Adresse: Augsburg; Web-Link; 2024; Quellengüte: 3 (Vorlesung)