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

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Keine Bearbeitungszusammenfassung
Zeile 37: Zeile 37:
// src/json/config.json
// src/json/config.json


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


Zeile 108: Zeile 108:


<source lang="ecmascript">
<source lang="ecmascript">
section.init('question')
section.init('form')
</source>
</source>


Zeile 208: Zeile 208:
</source>
</source>


=== <code>SectionQuestion.vue</code> ===
=== <code>SectionForm.vue</code> ===


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">
<source lang="html">
<!-- src/section/SectionQuestion.vue -->
<!-- src/section/SectionForm.vue -->


<script setup>
<script setup>
Zeile 313: Zeile 313:
// public/json/config.json
// public/json/config.json


{ "startSection":    "question",
{ "startSection":    "form",
   "apiRoot":        "/json/i18n_$1.json",
   "apiRoot":        "/json/i18n_$1.json",
   "XapiRoot":        "/api/$1",
   "XapiRoot":        "/api/$1",
Zeile 543: Zeile 543:
// public/json/config.json
// public/json/config.json


{ "startSection":    "question",
{ "startSection":    "form",
   "XapiRoot":        "/json/i18n_$1.json",
   "XapiRoot":        "/json/i18n_$1.json",
   "apiRoot":        "/api/$1",
   "apiRoot":        "/api/$1",

Version vom 23. April 2024, 17:19 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

Vorlage:InBearbeitung

Musterlösung
Git-Repository, git checkout v05 (JavaScript)
Git-Repository (TypeScript), git checkout v05

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

Konfigurationsdatei

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":
  { "stranger":       "Fremder",
    "askName":        "Wie heißen Sie?",
    "welcome":        "Willkommen zu Web-Programmierung!",
    "buttonReset":    "Reset",
    "buttonSayHello": "Begrüßung"
  }
}
// src/json/i18n_en.json

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

  "i18n":
  { "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 code>StoreSection.js mit Hilfe einer JSON-Datei

Die JSON-Datei config.json wird von StoreSection.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'

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 Konstante "section" 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 HelloWorld.vue. Entfernen Sie nun die zugehörige Zeile:

section.init('form')

Sie können nun auch noch die Funktion "init" und das zugehörige Interface aus der Datei StoreSection.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 StoreSectiong.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.

// src/store/StoreGreeting.js

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

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),

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

    name = ref(''),

    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 storeGreeting from '@/store/StoreGreeting'
 
  const
    greeting   = storeGreeting(),
    dictionary = greeting.dictionary
</script>

<template>
  <section id="section_hello">
    <h1>{{greeting.hello}}</h1>
    <p>{{dictionary.welcome}}</p>
  </section>
</template>

<style scoped lang="scss">
  @import '/css/section/SectionHello';
</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 storeGreeting from '@/store/StoreGreeting'
  import storeSection  from '@/store/StoreSection'

  const
    greeting = storeGreeting(),
    i18n     = greeting.i18n,
    section  = storeSection(),
    sayHello = () => { section.change('hello') }
</script>

<template>
  ...
</template>

<style scoped lang="scss">
  ...
</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

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

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

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.

// src/service/getJson.js

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": "en"
}
// 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))

StoreSection.js

// src/store/StoreSection.js

...
import { ..., inject } from 'vue'

const store... =
defineStore
( '...',
  () =>
  { const
      config = inject('config'),
...

StoreI18n.js

// src/store/StoreI18n.js

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

const
  storeI18n =
    defineStore
    ( 'i18n',

      () =>
      { const
          config =
            inject('config'),

          lang =
            ref(config.defaultLanguage),

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

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

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

        return { lang, phrases, dictionary, changeLang }
      }
    )

export default storeI18n

StoreGreeting.js

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

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

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 --id lang"

JSON-Server-Daten

// db.json

{ "v1":
  [ { "lang": "de",

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

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

    { "lang": "en",

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

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

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 den Server und testen Sie ihn im Browser:

npm run server 

http://localhost:4000/v1
http://localhost:4000/v1/de
http://localhost:4000/v1/en

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"
}

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

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

wk_express_hello_world (v01)

Kopieren Sie die Datei db.json aus der Version v05a in den Ordner src. Den Inhalt dieser Datei geben wir nun mit Hilfe eines Express-Servers aus.

src/server.js

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 tunning on ${SERVER}`)
}

init()

VLC-REST-Client

Installieren Sie im VLS die Extension REST Client.

Testen Sie dann Ihren Express-Server, indem Sie 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/de HTTP/1.1

###
GET http://localhost:4000/v2/en HTTP/1.1

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)