HTML5-Tutorium: JavaScript: Hello World Vue 06

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg

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 v06, ... v06a, ... v06b

Anwendungsfälle (Use Cases)

Die Version des fünften Teils des Vue-Tutoriums wird so geändert, dass die Sprache zur Laufzeit geändert werden kann.

Zunächst wird jedoch eine Navigationskomponente eingeführt. Dies hat den Zweck, das Routing an einem Beispiel zu erklären.

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 v05b     # Wechsle in den Branch v05b
git checkout -b v06  # Klone v05b in einen neuen Branch v06
npm i
npm i vue-router

Views

Anstelle von Sections werden Views verwendet. Dazu werden die Section-Komponenten in den Ordner View verschoben und umbenannt:

src/view/ViewHello.vue
src/view/ViewHome.vue     // an Stelle von ViewForm.vue

src/view/ViewError404.vue // neu

Die SCSS-Dateien müssen natürlich auch entsprechend verschoben und abgepasst werden.

ViewForm.vue wird in ViewHome.vue unbenannt, da diese Seite als Startseite verwendet wird.

Zusätzlich wird eine Seite ViewError404.vue angelegt, die immer dann ausgeliefert wird, wenn der Benutzer auf eine URL zugreift, der keine View-Datei zugeordnet ist.

Die SCSS-Dateien sollten analog umbenannt und unter den neuen Namen importiert werden. Für ViewError404.vue muss natürlich auch eine SCSS-Datei angelegt werden. Diese verweist allerdings nur auf _View.scss.

<!-- src/view/ViewError404.vue -->

<script setup>
  import router    from '@/router'
  import storeI18n from '@/store/StoreI18n'

  const phrases = storeI18n().phrases
</script>

<template>
  <h1>404</h1>
  <p>{{phrases.pageNotFound.replace('$1', router.currentRoute.value.path)}}</p>
</template>

<style scoped lang="scss">
  @import '/css/view/ViewError404';
</style>

Router-Plugin

Um mit Vue Multipage-Anwendungen realisieren zu können, benötigt man einRouter-Plugin. Dieses muss zunächst händisch auf Basis von vue-router implementiert werden.

// src/router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import ViewHome                           from '@/view/ViewHome.vue'

const
  routes =
    [ { path: '/',
        name: 'home',
        component: ViewHome,
      },
      { path: '/hello',
        name: 'hello',
        component: () => import('@/view/ViewHello.vue'),
      },
      { path:      '/:pathMatch(.*)',
        component: () => import('@/view/ViewError404.vue'),
      }
    ],

  router =
    createRouter({ history: createWebHistory(), routes })

export default router

Nun muss man das neu erstellte Plugin in die App integrieren.

// src/main.js

import { createPinia } from 'pinia'
import { createApp }   from 'vue'
import getJson         from '@/service/getJson'
import router          from './router'  // './router/index.js'
import App             from './App.vue'
const
  pinia   = createPinia(),
  app     = createApp(App),
  init    = async () =>
            { const
                config = await getJson('/json/config.json')
 
              app
                .provide('config', config)
                .use(pinia)
                .use(router)
                .mount('#app') // app is shown to the user
            }

window.addEventListener('load', init)

index.html

Da das main-Tag nun innerhalb von App.vue verwendet werden soll, wird es aus der Datei index.html entfernt.

...
  <body id="app">
  </body>
...

App.vue

Nun kann in die App eine Navigation eingebaut werden.

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

<script setup>
  import AppNav       from '@/components/AppNav.vue'
  import storeSession from '@/store/StoreSession'
  import storeI18n    from '@/store/StoreI18n'
  
  const session = storeSession()

  storeI18n(); // activate initialization of storeI18n
  session.initialize();
</script>

<template>
  <p      v-if="!session.isInitialized">Loading ...</p>
  <AppNav v-if="session.isInitialized"/>
  <main   v-if="session.isInitialized">
    <router-view/>
  </main>
</template>

<style lang="scss">
  @import '/css/body';
</style>

Die zugehörige Navigationskomponente muss auch noch implementiert werden.

<!-- /src/components/AppNav.vue -->

<script setup>
</script>

<template>
  <nav>
    <ul>
      <li><router-link to="/">Home</router-link></li>
      <li><router-link to="/hello">Hello</router-link></li>
    </ul>
  </nav>
</template>

<style scoped lang="scss">
  @import '/css/components/AppNav';
</style>
/* src/css/_config.scss */
...
$background-color:  #C5EFFC;
$link-color:        #0027d4;
...
$nav-dist:          0.5em;


/* src/css/AppNav.scss */

@import 'config';

nav
{ top:      0;
  width:    100%;
  position: fixed;

  ul
  { list-style-type: none;
    margin: 0 $nav-dist;
    padding: 0;

    li
    { display: inline;
      a { padding: 0 $nav-dist 0.2em; }
    }

    a
    { text-decoration: none;
      color:           $link-color;
      font-weight:     bold;
    }

    a:hover, .router-link-exact-active
    { text-decoration: underline;
      color:           $link-color;
      font-weight:     bold;
    }
  }

  label, input
  { margin: 0;
  }
}

Navigation

Die Navigation muss in allen Komponenten, in den Seiten gewechselt werden, angepasst werden.

Das ist in der App.vue der Fall (siehe zuvor) sowie in der ViewHome.vue.

<!-- /src/vie/ViewHome.vue  -->

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

  import storeGreeting from '@/store/StoreGreeting'
  import router        from '@/router'

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

Die Section-Verwaltung der ehemaligen Single-Page-Anwendung kann und sollte gelöscht werden:

  • config.js
  • storeSession.js
  • ViewHome.vue
  • ViewHello.vue

Die Datei HelloWorld.vue kann (samt SCSS-Datei) ganz gelöscht werden.

Hello World Vue 06a

Aufgabe: Die Ausgabesprache soll vom Benutzer zur Laufzeit geändert werden können. Die Sprachen sollen durch Bilder (Flaggen) symbolisiert werden.

git checkout v06      # Wechsle in den Branch v06
git checkout -b v06a  # Klone v06 in einen neuen Branch v06a

config.json

Um die vorhandenen Sprachen durchblättern zu können, wird ein einfacher Automat verwendet.

// src/public/json/config.json

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

StoreI18n.js: Integration von nextLanguage

Der Automat nextLanguage wird in den StoreI18n integriert. Wenn die Methode changeLang ohne Argument aufgerufen wird, soll der Automat die aktuelle Sprache lang durch die Nachfolgersprache ersetzen. Dazu sollte zunächst die zunächst Konstante nextLanguage definiert werden.

...
lang =
  ref(null),

nextLanguage =
  reactive(config.nextLanguage),

...

Diese neue Konstante wird verwendet, um die Sprache mittels changeLang zu definieren, sofern kein Argument übergeben wurde.

changeLang =
  async (p_lang = null) =>
        { lang.value = p_lang ?? nextLanguage[lang.value];
          await getI18n(lang.value);
          document.getElementsByTagName('html')[0].lang = lang.value;
        }

initialize = 
  async resolve => 
  { await changeLang(config.defaultLanguage); 
    resolve(); 
  }

Bilder von Flaggen

Zur Visualisierung der gewählten Sprache werden Bilder von Flaggen eingesetzt. Wir kopieren die Bilder im SVG-Format aus dem NPM-Paket "https://github.com/lipis/flag-icons".

npm i -D flag-icons

Im Ordner "node_modules/flag-icons/flags/4x3" finden Sie Flaggen zahlreicher Länder im 4-zu-3-Format. Kopieren Sie die Flaggen der Länder Deutschland, Großbritanien und Frankreich in den Ordner public/img.

In die Datei config.js wird der Pfad, unter dem die Bilder zu finden sind, eingefügt.

//   public/config.js
...
  "apiRoot":  "/json/i18n_$1.json",
  "imgPath":  "/img/$1.svg",
  "XapiRoot": "/api/$1",
  "XimgPath": "/api/$1/img",
...

FormButtonIcon

Eine neue Komponente wird benötigt: FormButtonIcon Dabei handelt es sich um einen Button, der ein Icon anzeigt. Das Bild kann zur Laufzeit geändert werden.

<script setup>
  import { computed, inject } from 'vue'

  const
    props       = defineProps
                  ({ image: { type: String },
                     width: { type: String, default: '3em'},
                  }),
    config      = inject('config'),
    emit        = defineEmits(['click']),
    click       = () => emit('click'),
    require     = v_url => import.meta.url == null
                           ? v_url
                           : new URL(v_url, import.meta.url).href,
    imgPath     = config.imgPath,
    buttonImage = computed(() => require(imgPath.replace('$1', props.image)))
</script>

<template>
  <input type="image" :src="buttonImage" :style="{width}" @click="click" />
</template>

<style scoped lang="scss">
  @import '/css/components/form/FormButtonIcon.scss';
</style>
/* src/css/components/FormButtonIcon.scss */

@import 'FormButton';

input
{ width:       $button_icon_width !important;
  margin-left: $nav-dist          !important;
}

Fügen Sie in src/css/_config.scss noch

$button-icon-width: 1em;

ein.

Erweiterung von AppNav.vue

Der Sprachwahl-Button muss noch in die Navigation eingebaut werden. In diesem Rahmen wird auch gleich die Navigation etwas internationalisiert.

Ergänzen Sie in den Dictionaries der Dateien public/json/i18n_de.json und public/json/i18n_en.json folgende Attribute:

    ...         ...,
    "navHome":  "Start",
    "navHello": "Hallo"

bzw.

    ...         ...,
    "navHome":  "Home",
    "navHello": "Hello"

Damit können Sie das Navigationsmenü internationalisieren (einschließlich Auswahl der Sprache per Button).

<!-- src/components/AppNav -->

<script setup>
  import FormButtonIcon from '@/components/form/FormButtonIcon.vue'
  import storeI18n      from '@/store/StoreI18n'
  const i18n = storeI18n()
</script>

<template>
  <nav>
    <ul>
      <li><router-link to="/">{{i18n.dictionary.navHome}}</router-link></li>
      <li><router-link to="/hello">{{i18n.dictionary.navHello}}</router-link></li>
      <li><FormButtonIcon :image="i18n.lang" @click="i18n.changeLang()"/></li>
    </ul>
  </nav>
</template>

<style scoped lang="scss">
  @import '/css/components/AppNav';
</style>

Hello World Vue 06b

Aufgabe: Defaultmäßig soll dem Benutzer die App in der Sprache abgezeigt werden, die der Benutzer im Browser voreingestellt hat. Sollte die voreingestellte Sprache nicht unterstütz werden, wird die App in der Defaultsprache angezeigt.

Legen Sie zunächst wieder einen neuen Branch an.

git checkout v06a     # Wechsle in den Branch v06a
git checkout -b v06b  # Klone v06a in einen neuen Branch v06b

Das Vorgehen ist im Prinzip ganz einfach.

Mittels des NPM-Packets "navigator-languages" ermittelt man die vom Browser unterstützten Sprachen. In der Konfigurationsdatei listet man die Sprachen auf, die von der App unterstütz werden. Besser wäre es natürlich, diese Liste vom Backend zu laden, damit die App mit bekommt, wenn neue Sprachen zur Verfügung stehen. Am aufwändigsten ist es, diese beiden Array zu matchen.

Mögliche Erweiterung: Auch die HTML-Pfade sollten internationalisiert werden.

npm i navigator-languages // unterstützt mehrere Browservarianten

Language Matching

Das Language Matching erfolgt zweimal mit zwei verschachtelten Schleifen. Zunächst wird überprüft, ob es exakte Übereinstimmungen gibt. Sollte das nicht der Fall sein, wird überprüft, ob es Matches zwischen den ersten beiden Buchstaben gibt ('de', 'de-AT', 'de-DE' matchen in diesem Schritt alle miteinander).

// src/util/chooseLanguage.js

import navigatorLanguages from 'navigator-languages'

const
  /**
   * @function
   * @param  { Array<string> } p_langs_available 
   *         An array of language strings containing the languages
   *         supported by the app 
   * @param  { Array<string> } [p_langs_required  = navigatorLanguages()]
   *         An array of language strings containing the languages
   *         requested by the client 
   * @return { string } 
   *         The language to be displayed
   */
  chooseLanguage =
  (p_langs_available, p_langs_required = navigatorLanguages()) =>
  { let
      lang = p_langs_available[0],
      i=0, j=0, n=p_langs_required.length, m=p_langs_available.length
  
    outer:
    { while (i<n)
      { const c_lang_required = p_langs_required[i]
        while (j<m)
        { const c_lang_availabe = p_langs_available[j]
          if (c_lang_availabe === c_lang_required)
          // both languages match perfectly
          { lang = c_lang_availabe;
            break outer;
          }
          j++
        }
        i++
      }
      i=0, j=0
      while (i<n)
      { const c_lang_required = p_langs_required[i]
        while (j<m)
        { const c_lang_availabe = p_langs_available[j]
          if (c_lang_availabe.substring(0,2) === c_lang_required.substring(0,2))
          // main part of the languages match
          { lang = c_lang_availabe;
            break outer;
          }
          j++
        }
        i++
      }
    }
  
     return lang
   }

export default chooseLanguage

config.js

Das Attribut defaultLanguage wird durch das Attribut languages ersetzt.

{ "apiRoot":      "/json/i18n_$1.json",
  "imgPath":      "/img/$1.svg",
  "XapiRoot":     "/api/$1",
  "XimgPath":     "/api/$1/img",
  "languages":    ["de", "en"],
  "nextLanguage": {"de": "en", "en": "de"}
}

src/store/StoreI18n

...
import chooseLanguage from '@/util/chooseLanguage'

...
      () =>
      { const
          ...
          initialize = 
             async resolve => 
                   { await changeLang(chooseLanguage(config.languages)); 
                     resolve(); 
                   }
        ...
...

Hello World Vue 06c

Aufgabe: Sorgen Sie dafür, dass die unterstützten Sprachen und der Sprach-Automat vom Backend geladen wird.

git checkout v06b     # Wechsle in den Branch v06b
git checkout -b v06c  # Klone v06b in einen neuen Branch v06c

Splitten Sie die Config-Datei zunächst in zwei Teile: "config.json" wird aus dem Public-Ordner geladen. "config_backend.json" wird normalerweise vom Backend geladen, kann aber zum Entwicklungszeitpunkt auch vom Public-Ordner geladen werden, wenn das Backend noch nicht stabil läuft.

// src/public/json/config.json

{ "configBackend":  "/json/config_backend.json",
  "apiRoot":        "/json/i18n_$1.json",
  "imgPath":        "/img/$1.svg",

  "XconfigBackend": "/api/config",
  "XapiRoot":       "/api/$1",
  "XimgPath":       "/api/$1/img"
}
// src/public/json/config_backend.json

{ "languages":    ["de", "en"],
  "nextLanguage": {"de": "en", "en": "de"}
}

Das Backend teilt dem Frontend mit, welche Sprachen unterstützt werden.

main.js

Laden Sie, nachdem Sie "config" asynchron geladen haben, "configBackend" asynchron und kopieren Sie den Inhalt dieses Objekts in das Config-Objekt. Den Pfad von "configBackend" finden Sie unter "config.configBackend".

...
const
  ...
  init    = async () =>
            { const
                config = await getJson('/json/config.json'),
                configBackend = await getJson(config.configBackend)

              Object.assign(config, configBackend)

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

storeI18n.js

Die Datei "index.html" sollte bei jeder Änderung der Sprache angepasst werden.

...
changeLang =
  async (p_lang = null) =>
        { lang.value = p_lang ?? nextLanguage[lang.value];
          await getI18n(lang.value);
          document.getElementsByTagName('html')[0].lang = lang.value
        }
...


Quellen

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