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)
 
(132 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
{{HTML5-Tutorium:JavaScript:HelloWorld:Menü}}
{{HTML5-Tutorium:JavaScript:HelloWorld:Menü}}
{{InBearbeitung}}


'''Musterlösung'''<br/>
'''Musterlösung'''<br/>
[{{Git-Server}}/kowa/wk_hello_world_vue Git-Repository], <code>git checkout v05</code> (JavaScript)<br>
[{{Git-Server}}/kowa/wk_hello_world_vue Git-Repository], <code>git checkout v05</code>, <code>... v05a</code>, <code>... v05b</code>, <code>... v05c</code>, <code>... v05d</code>, <code>... v05e</code>
[{{Git-Server}}/kowa/wk_hello_world_vue_ts Git-Repository (TypeScript)], <code>git checkout v05</code>


== Anwendungsfälle (Use Cases) ==
== Anwendungsfälle (Use Cases) ==
Zeile 22: 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">
<syntaxhighlight 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>
</syntaxhighlight>


== Kofigurationsdateien ==
== Kofigurationsdateien ==


=== Konfigurationsdatei ===
=== Hauptkonfigurationsdatei ===
Die folgende Datei dient zur statischen Konfiguration der Anwendung.  
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.
Der Inhalt wird in die App fest eingebunden und kann in der laufenden Anwendung ohnen einen Rebuild nicht geändert werden.


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


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


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


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


Zeile 53: Zeile 50:


   "dictionary":
   "dictionary":
   { "stranger":       "Fremder",
   { "placeholderName": "Ihr Name",
     "askName":       "Wie heißen Sie?",
    "stranger":       "Fremder",
     "welcome":       "Willkommen zu Web-Programmierung!",
     "askName":         "Wie heißen Sie?",
     "buttonReset":   "Reset",
     "welcome":         "Willkommen zu Web-Programmierung!",
     "buttonSayHello": "Begrüßung"
     "buttonReset":     "Reset",
     "buttonSayHello": "Begrüßung"
   }
   }
}
}
</source>
</syntaxhighlight>


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


Zeile 70: Zeile 68:


   "dictionary":
   "dictionary":
   { "stranger":       "Stranger",
   { "placeholderName": "your name",
     "askName":       "What's your name?",
    "stranger":       "Stranger",
     "welcome":       "Welcome to Web Programing!",
     "askName":         "What's your name?",
     "buttonReset":   "reset",
     "welcome":         "Welcome to Web Programing!",
     "buttonSayHello": "say hello"
     "buttonReset":     "reset",
     "buttonSayHello": "say hello"
   }
   }
}
}
</source>
</syntaxhighlight>


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


== Statische JSON-Dateien (v05) ==
== Statische JSON-Dateien (v05) ==
=== Initialisierung von <code>StoreSection.js</code> mit Hilfe einer JSON-Datei ===
=== Initialisierung von <code>StoreSession.js</code> mit Hilfe einer JSON-Datei ===


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


<source lang="ecmascript">
<syntaxhighlight lang="javascript">
import config from '@/json/config.json'
import config from '@/json/config.json'
</source>
</syntaxhighlight>
 
An dieser Stelle würde ich keine Vite-Alias <code>'/json'</code> definieren, sondern die Schreibweise
<code>'@/json/...'</code> 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 <code>'/json'</code> 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.
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.
Zeile 97: Zeile 99:
Nun kann der Name der Start-Section initialisiert werden:
Nun kann der Name der Start-Section initialisiert werden:


<source lang="ecmascript">
<syntaxhighlight lang="javascript">
const
const
   currentSection = ref(config.startSection),
   section = ref(config.startSection),
...
...
</source>
</syntaxhighlight>
 
Die Konstante "currentSection" enthält das Objekt, das in der JSON-Datei definiert wurde. In diesem Objekt gibt es das Attribut "name", das den Namen der Start-Section enthält.


Die Initialisierung von StoreSection erfolgt bislang in der Datei <code>HelloWorld.vue</code>. Entfernen Sie in dieser Datei den Aufruf Init-Funktion.
Die Initialisierung von StoreSession erfolgt bislang in der Datei <code>HelloWorld.vue</code>mittels der Funition <code>session.init</code>. Entfernen Sie in der Datei <code>StoreSession.js</code> die Init-Funktion und in der Datei <code>HelloWorld.vue</code> den zugehörigen Aufruf der Init-Funktion.


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


=== <code>StoreGreeting.js</code> ===
=== <code>StoreGreeting.js</code> ===
Zeile 113: Zeile 113:
Die Webanwendung soll internationalisiert werden. Dazu muss <code>StoreGreeting.js</code> um Internationalisierungstexte erweitert werden.
Die Webanwendung soll internationalisiert werden. Dazu muss <code>StoreGreeting.js</code> um Internationalisierungstexte erweitert werden.


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


<source lang="ecmascript">
Beachten Sie, dass <code>f_hello</code> gegenüber den früheren Implementierungen einen leeren Namen <code>''</code>
durch den Namen <code>dictionary.stranger</code> ersetzt. (Isabella, danke für den Tipp.)
 
Beachten Sie außerdem, dass <code>f_hello</code> früher <code>sayHello</code> hieß. Allerdings gibt es in <code>SectionHello.vue</code>
eine Funktion mit gleichem Namen, aber anderer Funktionalität. Um Verwechslungen zu vermeiden, habe ich die (Hilfs-)Funktion in  <code>StoreGreeting.js</code> umbenannt.
 
<syntaxhighlight lang="javascript">
// src/store/StoreGreeting.js
// src/store/StoreGreeting.js


Zeile 124: Zeile 130:
import config from '@/json/i18n_de.json'
import config from '@/json/i18n_de.json'


const storeGreeting =
const
defineStore
  storeGreeting =
( 'greeting',
  defineStore
  ( 'greeting',


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


    dictionary =
        dictionary =
      reactive(config.dictionary),
          reactive(config.dictionary),


    sayHello =
        name =
      p_name => phrases.hello.replace('$1', p_name),
          ref(''),


    name = ref(''),
        reset =
          () => { name.value = '' },


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


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


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


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


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


    buttonSayHello =
        buttonReset =
      computed(() => dictionary.buttonSayHello)
          computed(() => dictionary.buttonReset),
    */


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


export default storeGreeting
export default storeGreeting
</source>
</syntaxhighlight>


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.
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.
Zeile 175: Zeile 187:
Die Datei <code>SectionHello.vue</code> muss an die Internationalisierung angepasst werden, indem alle deutschen Wörter und Phrasen ersetzt durch Zugriffe auf den <code>StoreGreeting</code> ersetzt werden.
Die Datei <code>SectionHello.vue</code> muss an die Internationalisierung angepasst werden, indem alle deutschen Wörter und Phrasen ersetzt durch Zugriffe auf den <code>StoreGreeting</code> ersetzt werden.


<source lang="html">
<syntaxhighlight lang="html">
<!-- src/section/SectionHello.vue -->
<!-- src/section/SectionHello.vue -->


<script setup>
<script setup>
   import storeGreeting from '@/store/StoreGreeting'
  import storeSession  from '/store/StoreSession'
   import storeGreeting from '/store/StoreGreeting'
 
   const
   const
    session    = storeSession(),
     greeting  = storeGreeting(),
     greeting  = storeGreeting(),
     dictionary = greeting.dictionary
     dictionary = greeting.dictionary
Zeile 187: Zeile 201:


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


<style scoped lang="scss">
<style scoped>
   @import '/css/section/SectionHello';
   @import '/css/component/section/SectionHello.css';
</style>
</style>
</source>
</syntaxhighlight>


=== <code>SectionForm.vue</code> ===
=== <code>SectionForm.vue</code> ===
Zeile 202: Zeile 217:
Die Datei <code>SectionHello.vue</code> muss auf dieselbe Weise aktualisiert werden. Allerdings benötig man ein paar mehr Konstanten.
Die Datei <code>SectionHello.vue</code> muss auf dieselbe Weise aktualisiert werden. Allerdings benötig man ein paar mehr Konstanten.


<source lang="html">
<syntaxhighlight lang="html">
<!-- src/section/SectionForm.vue -->
<!-- src/section/SectionForm.vue -->


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


   import storeGreeting from '@/store/StoreGreeting'
   import storeSession  from '/store/StoreSession'
   import storeSection  from '@/store/StoreSection'
   import storeGreeting from '/store/StoreGreeting'


   const
   const
     section   = storeSection(),
     session   = storeSession(),
    sayHello  = () => section.currentSection = 'hello',
   
     greeting  = storeGreeting(),
     greeting  = storeGreeting(),
     dictionary = greeting.dictionary
     dictionary = greeting.dictionary,
    reset      = () => { greeting.reset(); session.setFocus() },
    sayHello  = () => session.section = 'hello'
</script>
</script>


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


<style scoped lang="scss">
<style scoped>
   ...
   ...
</style>
</style>
</source>
</syntaxhighlight>


== Dynamisches Laden von JSON-Dateien (v05a) ==
== Dynamisches Laden von JSON-Dateien (v05a) ==
Zeile 247: Zeile 266:
Dazu wird die Version v05 erweitert:
Dazu wird die Version v05 erweitert:


<source lang="ecmascript">
<syntaxhighlight 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>
</syntaxhighlight>


=== Internationalisierungsdateien ===
=== Internationalisierungsdateien ===


Die folgenden Dateien sollen dynamisch zur Laufzeit eingelesen werden.
Der JSON-Ordner <code>src/json</code> wird in den Public-Bereich verschoben.
Beachten Sie, dass sie in den Ordner "public/json" verschoben werden.
Er heißt nun <code>public/json</code>. JSON-Datein, die sich im Public-Bereich befinden müssen dynamisch zur Laufzeit der App geladen werden.
 
<source 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"
  }
}
</source>
 
<source lang="json">
// public/json/i18n_en.json


{ "phrases":
  { "hello": "Hello, $1!"
  },


  "dictionary":
Er können jederzeit analog zu <code>i18n_de.json</code> und <code>i18n_en.json</code> I18n-Dateien für weitere Sprachen definiert werden.
  { "stranger":      "Stranger",
    "askName":        "What's your name?",
    "welcome":        "Welcome to Web Programing!",
    "buttonReset":    "reset",
    "buttonSayHello": "say hello"
  }
}
</source>
 
Er können jederzeit  I18n-Dateien für weitere Sprachen definiert werden.


=== <code>getJson.js</code> ===
=== <code>getJson.js</code> ===
Zeile 297: Zeile 283:
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">
<syntaxhighlight lang="javascript">
// src/service/getJson.js
// 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)
async function getJson(p_url)
Zeile 313: Zeile 306:


export default getJson
export default getJson
</source>
</syntaxhighlight>


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


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


Zeile 325: Zeile 318:
   "defaultLanguage": "de"
   "defaultLanguage": "de"
}
}
</source>
</syntaxhighlight>


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


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


const
const
   pinia   = createPinia(),
   pinia = createPinia(),
   app     = createApp(App),
   app   = createApp(App),
   init   = async () =>
   init = async () =>
            { const
          { const
                config = await getJson('/json/config.json')
              config = await getJson('/json/config.json')
       
 
              config.i18n =
            config.i18n =
                await getJson(config.apiRoot.replace('$1', config.defaultLanguage))
              await getJson(config.apiRoot.replace('$1', config.defaultLanguage))
 
              app
            app
                .provide('config', config)
              .provide('config', config)
                .use(pinia)
              .use(pinia)
                .mount('#app') // app is shown to the user
              .mount('#app') // app is shown to the user
            }
          }


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


=== <code>StoreSection.js</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>
festgelegten Defaultsprache nachladen. Dieses muss mit einem zweiten <code>getJson</code>-Befehl erfolgen, der ausgeführt wird, '''nachdem'''
<code>config.json</code> vollständig geladen wurde.


<code>import config</code> wird durch <code>vue.inject</code> ersetzt.
Besser ist es allerdings einen allgemeineren Initialisierungsmechanismus zu etablieren, mit dem man anhängig von <code>config.json</code> verschiedene Daten vom Backend lädt, um die Anwendung zu initialisieren. Das machen wir später im nächsten Schritt.


<source lang="ecmascript">
=== <code>StoreSession.js</code> und <code>StoreGreeting.js</code> ===
// src/store/StoreSection.js


Wir holen uns nun in den Stores <code>StoreSession.js</code>  und <code>StoreGreeting.js</code>
die Konfigurations-Infomationen aus dem von der Main initialisierten Config-Onjekt.
Auf dieses können wir mit Hilde der Vue-Funktion <code>inject</code> zugreifen.
Der Iport-Befehl zum Laden der Konfigurationsdatei muss jeweils gelöscht werden.
<syntaxhighlight lang="javascript">
// src/store/StoreSession.js
...
import { ref, reactive, inject } from 'vue'
...
  { const
      config    = inject('config'), // contains the app configuration object
      ...
...
...
import { ref, inject } from 'vue'
</syntaxhighlight>
 
In der Datei <code>StoreGreeting.js</code> müssen wir noch
<code>config.i18n.phrases</code> und <code>config.dictionary</code>
durch
<code>config.i18n.phrases</code> und <code>config.dictionary</code>
ersetzen.
 
== Verallgemeinerung des Initialisierungsprozesses (v05b) ==
 
Aufgabe: Der Initialisierungsprozess soll so verallgemeinert werden, dass
jedes Modul (i. Allg. verschiedene Stores) eine Initialisierungsfunktion definieren kann,
die zum Zeitpunkt des App-Starts ausgeführt wird.
 
Als erste Anwendung erstellen wir dann einen Store <code>StoreI18N</code> der die Internationalisierungsdaten per JSON lädt.
 
Dazu wird die Version v05a erweitert:
 
<syntaxhighlight lang="javascript">
git checkout v05a    # Wechsle in den Branch v05a
git checkout -b v05b  # Klone v05a in einen neuen Branch v05b
</syntaxhighlight>


const store... =
=== <code>StoreSession.js</code> ===
 
Der Initialisierungsprozess wird vom Session-Store verwaltet.
Er stellt zu diesem Zweck drei Funktionen zur Verfügung:
 
* <code>isInitialized</code>: Reaktive Variable, die <code>true</code> enthält, sobald der Initialisierungsprozess abgeschlossen wurde.
* <code>addInitializer</code>: Fügt eine Initialisierungsfunktion hinzu, die durch den Initialisierungsprozess ausgeführt werden soll.
* <code>initialize</code>: Startet den Initialisierungsprozess und setzt  <code>isInitialized</code> zum Schluss auf <code>true</code>.
 
Die Konstante <code>initializers</code> enthält alle Initialisierungsfunktionen, die vor Start der Anwendung ausgeführt werden sollen.
Diese werden von anderen Stores mit Hilfe der Funktion <code>addInitializer</code> hinzugefügt. Die Datei <code>App.vue</code> aktiviert die entsprechenden Stores und führt danach die Funktion <code>initialize</code> aus. Mit Hilfe des reaktiven Konstante <code>isInitialized</code> erfährt die <code>App.vue</code>-Komponente, wann alle Stores initialisiert sind und kann dann vom Lade-Bildschirm auf den eigentlichen Startbildschirm umschalten.
 
<syntaxhighlight lang="javascript">
// src/store/StoreSession.js
 
import { defineStore          } from 'pinia'
import { ref, reactive, inject } from 'vue'
 
const storeSession =
defineStore
defineStore
( '...',
( 'session', // must be unique
 
   () =>
   () =>
   { const
   { const
       config = inject('config'), // contains the app configuration object
       ...
 
      initializers  = [],
      isInitialized  = ref(false),
      addInitializer = p_initializer =>
                      initializers.push(new Promise(resolve => p_initializer(resolve))),
      initialize    = () => Promise.all(initializers)
                                    .then(() => isInitialized.value = true)
 
    return { initAutofocus, section, setFocus, addInitializer, initialize, isInitialized }
  }
)
 
export default storeSession
</syntaxhighlight>
 
Und nun kann man das Ganze mal ausprobieren. Solange der Ladevorgang dauert,
wird ein Ladescreen angezeigt. Hier wird der Ladevorgang durch einen Zwei-Sekunde-Timer simuliert. 
 
<syntaxhighlight lang="html">
<!-- src/App.vue -->
 
<script setup>
  import HelloWorld  from '/component/HelloWorld.vue'
  import storeSession from '/store/StoreSession'
 
  const session = storeSession()
 
  // fake initializer, in order to see the loading information for at least two seconds
  session.addInitializer(p_resolve => setTimeout(p_resolve, 2000));
             
  session.initialize();
</script>
 
<template>
  <p          v-if="!session.isInitialized">Loading ...</p>
  <HelloWorld v-if="session.isInitialized"/>
</template>
 
...
...
</source>
</syntaxhighlight>


=== <code>StoreI18n.js</code> ===
=== <code>StoreI18n.js</code> ===


<source lang="ecmascript">
<code>StoreI18n.js</code> übernimmt die I18n-Funktionalität von <code>StoreGreeting.js</code>.
Damit ist die Internationalisierung in einem anwendungsunabhängigen Modul verfügbar.
Das erhöht die Wiederverwendbarkeit dieses Stores.
 
Dazu wird die Version v05b erweitert:
 
<syntaxhighlight lang="javascript">
git checkout v05b    # Wechsle in den Branch v05b
git checkout -b v05c  # Klone v05b in einen neuen Branch v05c
</syntaxhighlight>
 
Für den I18N-Store werden folgende Funktionen ergänzt:
* <code>getI18n</code>: eine Funktion zum Laden der I18n-Daten von einem Backendsystem.
* <code>changeLang</code>: eine Funktion zum asynchronen Ändern der Sprache.
* <code>initialize</code>: eine Funktion zum Initialisieren des Stores.
 
Die Funktion <code>initialize</code> lädt die I18n-Daten für die in der Config-Datei festgelegte
Defaultsprache. Diese Funktion wird im Session-Store als Initialisierungsfunktion registriert.
 
<syntaxhighlight lang="javascript">
// src/store/StoreI18n.js
// src/store/StoreI18n.js


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


const
const
Zeile 391: Zeile 499:
           config =
           config =
             inject('config'),
             inject('config'),
          session =
            storeSession(),


           lang =
           lang =
             ref(config.defaultLanguage),
             ref(null),


           phrases /* Record<string, string> */ =
           phrases /* Record<string, string> */ =
             reactive(config.i18n.phrases),
             reactive({}),


           dictionary /* Record<string, string> */ =
           dictionary /* Record<string, string> */=
             reactive(config.i18n.dictionary),
             reactive({}),


           getI18n =
           getI18n =
Zeile 414: Zeile 525:
                   { lang.value = p_lang ?? config.defaultLanguage;
                   { lang.value = p_lang ?? config.defaultLanguage;
                     await getI18n(lang.value);
                     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 }
         return { lang, phrases, dictionary, changeLang }
Zeile 421: Zeile 541:


export default storeI18n
export default storeI18n
</source>
</syntaxhighlight>
 
<code>changeLang</code> enthält einen kleinen Hack, um die korrekte Sprache in das HTML-Tag der Datei "index.html" einzufügen.
 
<syntaxhighlight lang="javascript">
document.getElementsByTagName('html')[0].lang = lang
</syntaxhighlight>
 
Damit wird im Document Tree der Datei "index.html" jeweils die aktuelle Sprache verwendet:
 
<syntaxhighlight lang="html">
<html lang="de">
<html lang="en">
</syntaxhighlight>
 
Man beachte, dass im Skript-Bereich des Moduls <code>App.vue</code> der Store mittels <code>storeI18n()</code>
erstmals aufgerufen muss, damit er initialisiert wird. Insbesondere wird dabei der Befehl <code>session.addInitializer(initialize)</code>
ausgeführt, das heißt die Initialisierungsfunktion zum Session-Store hinzugefügt.
 
Fügen Sie also den den Aufruf <code>storeI18n()</code> samt zugehörigem Import-Befehl in die
Datei <code>App.vue</code> ein. (Den Fake-Initializer können Sie deaktivieren, wenn Sie wollen. Er wird nicht mehr benötigt.)


=== <code>StoreGreeting.js</code> ===
=== <code>StoreGreeting.js</code> ===


<source lang="ecmascript">
Der Inhalt von <code>StoreGreeting.js</code> vereinfacht sich entsprechend.
Diese Datei nur noch Funktionen zur Verfügung, der in der Phrase <code>phrases.hello</code>
den String <code>$1</code> durch einen Namen ersetzt.
 
<syntaxhighlight lang="javascript">
// src/store/StoreGreeting.js
// src/store/StoreGreeting.js


Zeile 446: Zeile 590:
         i18n.dictionary,
         i18n.dictionary,


       sayHello =
       f_hello =
         p_name => phrases.hello.replace('$1', p_name),
         p_name => phrases.hello.replace('$1', p_name === '' ? dictionary.stranger : p_name),
 
...
...
</source>
</syntaxhighlight>
 
=== Initiaisierung durch <code>main.js</code> und <code>App.vue</code> ===
 
Die Konfiguration in der Datei <code>main.js</code> kann nun vereinfacht werden, da das asynchrone Laden von Initialisierungsdaten in jedem Store selbst geregelt wird.
 
<syntaxhighlight lang="javascript">
// src/main.js
 
import { createPinia } from 'pinia'
import { createApp }  from 'vue'
import getJson        from '/service/getJson'
import App            from './App.vue'


== Benutzung eines Backend-Servers (v05b) ==
const
  pinia = createPinia(),
  app  = createApp(App),
  init  = async () =>
          { app
              .provide('config', await getJson('/json/config.json'))
              .use(pinia)
              .mount('#app') // app is shown to the user
          }
 
window.addEventListener('load', init)
</syntaxhighlight>
 
Die Initialisierung der einzelnen Stores erfolgt in der Hauptdatei <code>App.vue</code>
 
<syntaxhighlight lang="javascript">
<script setup>
  import HelloWorld  from '/component/HelloWorld.vue'
  import storeSession from '/store/StoreSession'
  import storeI18n    from '/store/StoreI18n'
 
  const session = storeSession()
 
  // fake initializer, in order to see the loading information for at least two seconds
  session.addInitializer(p_resolve => setTimeout(p_resolve, 2000));
 
  // Activate initializer of "storeI18n"
  storeI18n(); // First call of storeI18n(): The code of the function is performed
              // All other calls: The reult of the first call is returned (Singleton)
 
  session.initialize();
</script>
 
<template>
  <p          v-if="!session.isInitialized">Loading ...</p>
  <HelloWorld v-if="session.isInitialized"/>
</template>
</syntaxhighlight>
 
== Benutzung eines Backend-Servers (v05c) ==


Aufgabe: Die I18N-Daten sollen von einem Backend-Server gelesen werden.
Aufgabe: Die I18N-Daten sollen von einem Backend-Server gelesen werden.


Dazu wird die Version v05 erweitert:
Dazu wird die Version v05b erweitert:


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


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">
<syntaxhighlight lang="javascript">
npm i -D json-server
npm i -D json-server
</source>
</syntaxhighlight>


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">
<syntaxhighlight lang="json">
"server": "npx json-server --watch db.json --port 4000 --id lang"
"server": "npx json-server --watch db.json --port 4000"
</source>
</syntaxhighlight>


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


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


{ "v1":
{ "v1":
   [ { "id: "de",
   [ { "id": "de",


       "phrases":
       "phrases":
Zeile 487: Zeile 683:


       "dictionary":
       "dictionary":
       { "stranger":      "Fremder",
       { "stranger":      "Fremdling",
         "askName":        "Wie heißen Sie?",
         "askName":        "Wie heißen Sie?",
         "welcome":        "Willkommen zu Web-Programmierung!",
         "welcome":        "Willkommen zu Web-Programmierung!",
Zeile 511: Zeile 707:
   ]
   ]
}
}
</source>
</syntaxhighlight>


Starten Sie nun den Server und testen Sie ihn im Browser:
Testen Sie dann Ihren JSON-Server, indem Sie ihn starten und dann in der Datei <code>test.rest</code>
<source lang="bash">
auf die jeweiligen Send-Request-Anweisungen klicken:
npm run server  
 
<syntaxhighlight lang="bash">
http://localhost:4000/v1
http://localhost:4000/v1/de
npm run server
http://localhost:4000/v1/en
</source>
// 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
</syntaxhighlight>


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


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


...
...


export default defineConfig
export default defineConfig
({plugins: [vue()],
({...
   resolve:
   resolve:
   { ...
   { ...
Zeile 544: Zeile 753:
   },
   },
})
})
</source>
</syntaxhighlight>


Starten Sie nun den Server und testen Sie ihn im Browser:
Starten Sie nun beide Server und testen Sie die Hello-World-Anwendung Browser:
<source lang="bash">
<syntaxhighlight lang="bash">
npm run server  # 1. Terminal
npm run server  # 1. Terminal
npm run dev    # 2. Terminal
npm run dev    # 2. Terminal
Zeile 554: Zeile 763:
http://localhost:5173/api/de
http://localhost:5173/api/de
http://localhost:5173/api/en
http://localhost:5173/api/en
</source>


=== VLC-REST-Client ===
http://localhost:4000/v1
 
http://localhost:4000/v1/de
Installieren Sie im VLS die Extension <code>REST Client</code>.
http://localhost:4000/v1/en
 
</syntaxhighlight>
Testen Sie dann Ihren Express-Server, indem Sie in der Datei <code>test.rest</code>
auf die jeweiligen Send-Request-Anweisungen klicken:
 
<source lang="bash">
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


###
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.
GET http://localhost:4000/v2/en HTTP/1.1
</source>


=== Zugriff auf den REST-Server ===
=== Zugriff auf den REST-Server ===
Zeile 584: Zeile 775:
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">
<syntaxhighlight lang="bash">
// public/json/config.json
// public/json/config.json


Zeile 592: Zeile 783:
   "defaultLanguage": "de"
   "defaultLanguage": "de"
}
}
</source>
</syntaxhighlight>


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.


== wk_express_hello_world (v00) ==
== Benutzung von Express als Backend-Server (v05d) ==


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


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


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


Zeile 621: Zeile 814:
"type": "module",
"type": "module",
...
...
</source>
</syntaxhighlight>


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


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


Zeile 645: Zeile 838:
   </body>
   </body>
</html>
</html>
</source>
</syntaxhighlight>


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


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


Zeile 673: Zeile 866:


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


== wk_express_hello_world (v01) ==
=== Backend-Server mittels Express ===


Kopieren Sie die Datei <code>db.json</code> aus der Version v05a
Erstellen Sie parallel zum Verzeichnis <code>frontend</code> einen Ordner <code>backend</code> und kopieren Sie die Dateien aus dem Ordner <code>express_hello_world</code> nach <code>backend</code>.
in den Ordner <code>src</code>. Den Inhalt dieser Datei geben wir nun
mit Hilfe eines Express-Servers aus.


=== <code>src/server.js</code> ===
Installieren Sie außerdem das NPM-Modul <code>morgan</code>.
 
Kopieren Sie '''das Array''' aus der Datei <code>db.json</code> der Version v05a
in die Datei <code>src/db.json</code> und ersetzen Sie in dieser Datei zweimal <code>"id"</code> durch <code>"lang</code>. Den Inhalt dieser Datei geben wir nun mit Hilfe eines Express-Servers aus.
 
==== <code>src/server.js</code> ====
 
<syntaxhighlight lang="javascript">
/**
* @author    Wolfgang Kowarschick <kowa@hs-augsburg.de>
* @copyright 2016-2025
* @license  MIT
*/


<source lang="ecmascript">
import express from 'express'
import express, { response } from 'express'
import path    from 'path'
import path    from 'path'
import morgan  from 'morgan'


let db;
let db;


const
const
   { PORT    = 4000,
   { PORT    = 4000,  
     SERVER  = `http://localhost:${PORT}`,
     SERVER  = `http://localhost:${PORT}`,
     DB      = `${SERVER}/db.json`
     DB      = `${SERVER}/db.json`
Zeile 697: Zeile 900:
   c_dirname = path.resolve(path.dirname(process.argv[1]))
   c_dirname = path.resolve(path.dirname(process.argv[1]))
;
;
// Middleware for debugging
c_app.use(morgan("combined"));
/*
c_app.use
( function (req, res, next)
  { console.log(req);
    next();
  }
);
*/


c_app
c_app
.get
.get
( '/',
( '/',  
   (req, res) =>
   (req, res) =>  
   { res.sendFile('./index.html', {root: c_dirname }); }
   { res.sendFile('./index.html', {root: c_dirname }); }
)
)
.get
.get
( '/db.json',
( '/db.json',  
   (req, res) =>
   (req, res) =>  
   { res.sendFile('./db.json', {root: c_dirname }); }
   { res.sendFile('./db.json', {root: c_dirname }); }
)
)
.get
.get
( '/v1/:lang',
( '/v1',
   (req, res) =>
  (req, res) => res.json(db)
)
.get
( '/v1/:lang',  
   (req, res) =>  
   { //console.log(req.params.lang);
   { //console.log(req.params.lang);
     const l_i18n = db.filter(i18n => i18n.lang === req.params.lang)[0];
     const l_i18n = db.filter(i18n => i18n.lang === req.params.lang)[0];
     res.json(l_i18n);
     res.json(l_i18n);  
   }
   }
)
)
.get
.get
( '/v1/:lang/:type',
( '/v1/:lang/:type',  
   (req, res) =>
   (req, res) =>  
   { //console.log(req.params.lang, req.params.type);
   { //console.log(req.params.lang, req.params.type);
     const l_i18n = db.filter(i18n => i18n.lang === req.params.lang)[0];
     const l_i18n = db.filter(i18n => i18n.lang === req.params.lang)[0];


     res.json(l_i18n[req.params.type]);
     res.json(l_i18n[req.params.type]);  
   }
   }
);
);
Zeile 729: Zeile 948:
async function init()
async function init()
{ c_app.listen(PORT);
{ c_app.listen(PORT);
   db = await fetch(DB).then(response => response.json())
   db = (await fetch(DB).then(response => response.json())).v1
   console.log(`Server is tunning on ${SERVER}`)
   console.log(`Server is running on ${SERVER}`)
}
}


init()
init()
</source>
</syntaxhighlight>
 
Testen Sie diese Datei mit Hilfe der Datei <code>test.rest</code> aus dem Frontend. Erweitern Sie die Test-Datei sinnvoll.
 
== Benutzung von Strapi als Backend-Servers (v05e) ==
 
Erstellen Sie im Root-Verzeichnis Ihrer App zunächst die Datei <code>create-dotenv.sh</code>:
 
<syntaxhighlight lang="bash">
#!/usr/bin/env bash
 
declare -A strapi=(
  ["dir"]="backend_strapi"
  ["HOST"]=127.0.0.1
  ["PORT"]=1337
  ["APP_KEYS"]="$(openssl rand -hex 64),$(openssl rand -hex 64)"
  ["API_TOKEN_SALT"]=$(openssl rand -hex 64)
  ["ADMIN_JWT_SECRET"]=$(openssl rand -hex 64)
  ["TRANSFER_TOKEN_SALT"]=$(openssl rand -hex 64)
  ["JWT_SECRET"]=$(openssl rand -hex 64)
)
 
create_env_variables(){
  for env in $1
  do
    declare -n dict=$env
    directory=${dict[dir]}
    cd $directory
    echo "Creating $env .env in $directory"
    [ ! -e .env ] || rm .env
    for var in "${!dict[@]}"
    do 
      [ "$var" != "dir" ] && echo "$var=${dict[$var]}" >> .env
    done
    unset -n dict
    cd - >/dev/null
  done
}
 
create_env_variables 'strapi'
 
echo "------------> $0 has finished successfully"
</syntaxhighlight>
 
Installieren Sie Strapi gemäß den Vorgaben aus der Vorlesung (Miro) und
führen Sie dann den Befehl <code>./create-dotenv.sh</code> in der Konsole aus.
Damit wird eine Dummy-Datei <code>strapi/.env</code> erzeugt. Diesen Befehl müssen Sie jedesmal
ausführen, wenn Sie die Strapi-App aus dem Repository laden, da die Datei <code>.env</code>
nicht im Repository gespeichert wird. Die generierte Datei enthält Zufallszahlen,
die für den sicheren Datenaustausch mit dem Frontend benötigt werden.
 
Erstellen Sie nun den Inhalt der Strapi-Datenbank gemäß den Vorgaben aus der Vorlesung (Miro).
 
Um darauf vom Frontend aus zuzugreifen, müssen Sie drei Dateien anpassen:
 
'''Konfiguration'''
<syntaxhighlight lang="javascript">
// /public/json/config.json: Api-Root
 
{ "startSection":    "form",
  "XapiRoot":        "/json/i18n_$1.json",
  "apiRoot":        "/api/$1",
  "defaultLanguage": "de"
}
</syntaxhighlight>
 
'''Proxy'''
<syntaxhighlight 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]=')
      }
    }
  },
</syntaxhighlight>
 
'''Datenextraktion in <code>StoreI18n.js</code>'''
<syntaxhighlight 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];
            Object.assign(phrases,    c_i18n_json.phrases);
            Object.assign(dictionary, c_i18n_json.dictionary);
        },
...
</syntaxhighlight>


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

Aktuelle Version vom 23. Mai 2025, 10:38 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, ... v05d, ... v05e

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:

git checkout v04     # Wechsle in den Branch v04
git checkout -b v05  # Klone v04 in einen neuen Branch v05
npm i

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.

// src/json/config.json

{ "startSection": "form" }

Internationalisierungsdateien

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

// src/json/i18n_de.json

{ "phrases":
  { "hello": "Hallo, $1!"
  },

  "dictionary":
  { "placeholderName": "Ihr Name",
    "stranger":        "Fremder",
    "askName":         "Wie heißen Sie?",
    "welcome":         "Willkommen zu Web-Programmierung!",
    "buttonReset":     "Reset",
    "buttonSayHello":  "Begrüßung"
  }
}
// src/json/i18n_en.json

{ "phrases":
  { "hello": "Hello, $1!"
  },

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

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 Datei 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(''),

        reset =
          () => { name.value = '' },

        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), reset
               /*, 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,
    reset      = () => { greeting.reset(); session.setFocus() },
    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
                       :placeholder="dictionary.placeholderName"
        />
      </div>
      <div>
        <FormButton :label="dictionary.buttonReset"    type="reset"  @click="reset"/>
        <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:

git checkout v05      # Wechsle in den Branch v05
git checkout -b v05a  # Klone v05 in einen neuen Branch v05a

Internationalisierungsdateien

Der JSON-Ordner src/json wird in den Public-Bereich verschoben. Er heißt nun public/json. JSON-Datein, die sich im Public-Bereich befinden müssen dynamisch zur Laufzeit der App geladen werden.


Er können jederzeit analog zu i18n_de.json und i18n_en.json I18n-Dateien für weitere Sprachen definiert werden.

getJson.js

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

// 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

Globale Konfigurationsdatei

// public/json/config.json

{ "startSection":    "form",
  "apiRoot":         "/json/i18n_$1.json",
  "XapiRoot":        "/api/$1",
  "defaultLanguage": "de"
}
// 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)

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.

Der Iport-Befehl zum Laden der Konfigurationsdatei muss jeweils gelöscht werden.

// src/store/StoreSession.js

...
import { ref, reactive, inject } from 'vue'
...
  { const
      config    = inject('config'), // contains the app configuration object
      ...
...

In der Datei StoreGreeting.js müssen wir noch config.i18n.phrases und config.dictionary durch config.i18n.phrases und config.dictionary ersetzen.

Verallgemeinerung des Initialisierungsprozesses (v05b)

Aufgabe: Der Initialisierungsprozess soll so verallgemeinert werden, dass jedes Modul (i. Allg. verschiedene Stores) eine Initialisierungsfunktion definieren kann, die zum Zeitpunkt des App-Starts ausgeführt wird.

Als erste Anwendung erstellen wir dann einen Store StoreI18N der die Internationalisierungsdaten per JSON lädt.

Dazu wird die Version v05a erweitert:

git checkout v05a     # Wechsle in den Branch v05a
git checkout -b v05b  # Klone v05a in einen neuen Branch v05b

StoreSession.js

Der Initialisierungsprozess wird vom Session-Store verwaltet. Er stellt zu diesem Zweck drei Funktionen zur Verfügung:

  • isInitialized: Reaktive Variable, die true enthält, sobald der Initialisierungsprozess abgeschlossen wurde.
  • addInitializer: Fügt eine Initialisierungsfunktion hinzu, die durch den Initialisierungsprozess ausgeführt werden soll.
  • initialize: Startet den Initialisierungsprozess und setzt isInitialized zum Schluss auf true.

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.

// src/store/StoreSession.js

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

const storeSession =
defineStore
( 'session', // must be unique

  () =>
  { const
      ...

      initializers   = [],
      isInitialized  = ref(false),
      addInitializer = p_initializer =>
                       initializers.push(new Promise(resolve => p_initializer(resolve))),
      initialize     = () => Promise.all(initializers)
                                    .then(() => isInitialized.value = true)

    return { initAutofocus, section, setFocus, addInitializer, initialize, isInitialized }
  }
)

export default storeSession

Und nun kann man das Ganze mal ausprobieren. Solange der Ladevorgang dauert, wird ein Ladescreen angezeigt. Hier wird der Ladevorgang durch einen Zwei-Sekunde-Timer simuliert.

<!-- src/App.vue -->

<script setup>
  import HelloWorld   from '/component/HelloWorld.vue'
  import storeSession from '/store/StoreSession'

  const session = storeSession()

  // fake initializer, in order to see the loading information for at least two seconds
  session.addInitializer(p_resolve => setTimeout(p_resolve, 2000));
              
  session.initialize();
</script>

<template>
  <p          v-if="!session.isInitialized">Loading ...</p>
  <HelloWorld v-if="session.isInitialized"/>
</template>

...

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.

Dazu wird die Version v05b erweitert:

git checkout v05b     # Wechsle in den Branch v05b
git checkout -b v05c  # Klone v05b in einen neuen Branch v05c

Für den I18N-Store werden folgende Funktionen ergänzt:

  • getI18n: eine Funktion zum Laden der I18n-Daten von einem Backendsystem.
  • changeLang: eine Funktion zum asynchronen Ändern der Sprache.
  • initialize: eine Funktion zum Initialisieren des Stores.

Die Funktion initialize lädt die I18n-Daten für die in der Config-Datei festgelegte Defaultsprache. Diese Funktion wird im Session-Store als Initialisierungsfunktion registriert.

// 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

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

document.getElementsByTagName('html')[0].lang = lang

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

<html lang="de">
<html lang="en">

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

Fügen Sie also den den Aufruf storeI18n() samt zugehörigem Import-Befehl in die Datei App.vue ein. (Den Fake-Initializer können Sie deaktivieren, wenn Sie wollen. Er wird nicht mehr benötigt.)

StoreGreeting.js

Der Inhalt von StoreGreeting.js vereinfacht sich entsprechend. Diese Datei 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),

...

Initiaisierung durch main.js und App.vue

Die Konfiguration in der Datei main.js kann nun vereinfacht werden, da das asynchrone Laden von Initialisierungsdaten in jedem Store selbst geregelt wird.

// 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 () =>
          { app
              .provide('config', await getJson('/json/config.json'))
              .use(pinia)
              .mount('#app') // app is shown to the user
          }

window.addEventListener('load', init)

Die Initialisierung der einzelnen Stores erfolgt in der Hauptdatei App.vue

<script setup>
  import HelloWorld   from '/component/HelloWorld.vue'
  import storeSession from '/store/StoreSession'
  import storeI18n    from '/store/StoreI18n'

  const session = storeSession()

  // fake initializer, in order to see the loading information for at least two seconds
  session.addInitializer(p_resolve => setTimeout(p_resolve, 2000));
  
  // Activate initializer of "storeI18n"
  storeI18n(); // First call of storeI18n(): The code of the function is performed
               // All other calls: The reult of the first call is returned (Singleton)

  session.initialize();
</script>

<template>
  <p          v-if="!session.isInitialized">Loading ...</p>
  <HelloWorld v-if="session.isInitialized"/>
</template>

Benutzung eines Backend-Servers (v05c)

Aufgabe: Die I18N-Daten sollen von einem Backend-Server gelesen werden.

Dazu wird die Version v05b erweitert:

git checkout v05b     
git checkout -b v05c

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":       "Fremdling",
        "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

Vue-Server als Proxy

// vite.config.common.js

...

export default defineConfig
({...
  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.

Benutzung von Express als Backend-Server (v05d)

express_hello_world

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

<!-- src/index.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport"
          content="width=device-width,
          initial-scale=1,
          shrink-to-fit=no"
    >
    <title>Express Hello World 01</title>
  </head>
  <body>
    <section>
      <h1>Hello, World!</h1>
    </section>
  </body>
</html>

src/server.js

// 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}`);

Backend-Server mittels Express

Erstellen Sie parallel zum Verzeichnis frontend einen Ordner backend und kopieren Sie die Dateien aus dem Ordner express_hello_world nach backend.

Installieren Sie außerdem das NPM-Modul morgan.

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

/**
 * @author    Wolfgang Kowarschick <kowa@hs-augsburg.de>
 * @copyright 2016-2025
 * @license   MIT
 */

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

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]))
;

// Middleware for debugging
c_app.use(morgan("combined"));

/*
c_app.use 
( function (req, res, next) 
  { console.log(req);
    next();
  }
);
*/

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', 
  (req, res) => res.json(db)
)
.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())).v1
  console.log(`Server is running on ${SERVER}`)
}

init()

Testen Sie diese Datei mit Hilfe der Datei test.rest aus dem Frontend. Erweitern Sie die Test-Datei sinnvoll.

Benutzung von Strapi als Backend-Servers (v05e)

Erstellen Sie im Root-Verzeichnis Ihrer App zunächst die Datei create-dotenv.sh:

#!/usr/bin/env bash

declare -A strapi=(
  ["dir"]="backend_strapi"
  ["HOST"]=127.0.0.1
  ["PORT"]=1337
  ["APP_KEYS"]="$(openssl rand -hex 64),$(openssl rand -hex 64)"
  ["API_TOKEN_SALT"]=$(openssl rand -hex 64)
  ["ADMIN_JWT_SECRET"]=$(openssl rand -hex 64)
  ["TRANSFER_TOKEN_SALT"]=$(openssl rand -hex 64)
  ["JWT_SECRET"]=$(openssl rand -hex 64)
)

create_env_variables(){
  for env in $1
  do
    declare -n dict=$env
    directory=${dict[dir]}
    cd $directory
    echo "Creating $env .env in $directory"
    [ ! -e .env ] || rm .env
    for var in "${!dict[@]}"
    do  
      [ "$var" != "dir" ] && echo "$var=${dict[$var]}" >> .env
    done
    unset -n dict
    cd - >/dev/null
  done
}

create_env_variables 'strapi'

echo "------------> $0 has finished successfully"

Installieren Sie Strapi gemäß den Vorgaben aus der Vorlesung (Miro) und führen Sie dann den Befehl ./create-dotenv.sh in der Konsole aus. Damit wird eine Dummy-Datei strapi/.env erzeugt. Diesen Befehl müssen Sie jedesmal ausführen, wenn Sie die Strapi-App aus dem Repository laden, da die Datei .env nicht im Repository gespeichert wird. Die generierte Datei enthält Zufallszahlen, die für den sicheren Datenaustausch mit dem Frontend benötigt werden.

Erstellen Sie nun den Inhalt der Strapi-Datenbank gemäß den Vorgaben aus der Vorlesung (Miro).

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

Konfiguration

// /public/json/config.json: Api-Root

{ "startSection":    "form",
  "XapiRoot":        "/json/i18n_$1.json",
  "apiRoot":         "/api/$1",
  "defaultLanguage": "de"
}

Proxy

// /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]=')
      }
    }
  },

Datenextraktion in StoreI18n.js

...
  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];
            Object.assign(phrases,    c_i18n_json.phrases);
            Object.assign(dictionary, c_i18n_json.dictionary);
         },
...

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)