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

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Zeile 178: Zeile 178:
     greeting = storeGreeting(),
     greeting = storeGreeting(),


     sayHello = () => { section.change('hello') },
     sayHello     = () => { section.change('hello') },
     controllerKey =
     controllerKey = new ControllerKey('Enter', sayHello, 'input_name')
      new ControllerKey('Enter', sayHello, 'input_name')


   section.init('form')
   section.init('form')

Version vom 19. April 2024, 16:31 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 v03 (JavaScript)
Git-Repository (TypeScript), git checkout v03

Stores mit Hilfe von Klassen:
Git-Repository, git checkout v03b (JavaScript)
Git-Repository (TypeScript), git checkout v03b

Anwendungsfälle (Use Cases)

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

Aufgabe

Aufgabe: Speichere den Zustand der Anwendung (Model) in Store-Objekten, um die künftige Modularisierung (Version v04) zu unterstützen.

In diesem Tutorium sollen die Daten in einem Store außerhalb der Komponenten gespeichert werden.

Mögliche Storekonzepte

  • vuex (veraltet, wird nur noch gewartet, angeblich nicht mehr weiterentwickelt, aber Version 4 existiert)
  • Pinia (der Nachfolger von vuex; einfacher zu benutzen; Vue-3- und Vue-2-Paradigmen werden unterstützt)
  • ohne Bibliothek direkt mit ref, reactive und computed aus Vue 3.2 (vgl. Vue 3 — The New Store)

Wir verwenden Pinia.

Pinia

Zitat:

While our hand-rolled state management solution will suffice in simple scenarios, there are many more things to consider in large-scale production applications:

  1. Stronger conventions for team collaboration
  2. Integrating with the Vue DevTools, including timeline, in-component inspection, and time-travel debugging
  3. Hot Module Replacement
  4. Server-Side Rendering support

Pinia is a state management library that implements all of the above. It is maintained by the Vue core team, and works with both Vue 2 and Vue 3.

Existing users may be familiar with Vuex, the previous official state management library for Vue. With Pinia serving the same role in the ecosystem, Vuex is now in maintenance mode. It still works, but will no longer receive new features. It is recommended to use Pinia for new applications.

Pinia started out as an exploration of what the next iteration of Vuex could look like, incorporating many ideas from core team discussions for Vuex 5. Eventually, we realized that Pinia already implements most of what we wanted in Vuex 5, and decided to make it the new recommendation instead.

Compared to Vuex, Pinia provides a simpler API with less ceremony, offers Composition-API-style APIs, and most importantly, has solid type inference support when used with TypeScript.

Erstellen eines neuen Projektzweigs

Erstellen Sie einen neuen Projektzweig (branch) innerhalb von hello_world_vue:

git checkout v02     # Wechsle in den Branch v02
git checkout -b v03  # Klone v02 in einen neuen Branch v03
npm i

Initialisierung von Pinia

Pinia muss als reguläre Dependency installiert werden, da das Paket in den produktiven Code integriert wird.

npm i pinia  // NICHT  npm i -D pinia

Pinia wird als Plugin in die App integriert. Dabei ist wichtig, dass das Pina-Plugin-Objekt vor dem eigentlichen App-Objekt erzeugt wird, da Komponenten der App, die Pinia verwenden, darauf angewiesen sind, dass die Pinia-Erzeugung vor der Komponenten-Initialisierung erfolgt.

// src/main.js

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

const pinia = createPinia() // before createApp(...)

window.addEventListener
( 'load'
  createApp(App).use(pinia).mount('#app')
)

Erstellen der Stores

StoreSection

Im Section-Store wird gespeichert, welche Section der Single-Page-Anwendung dem Benutzer aktuell präsentiert wird.

// src/store/StoreSection.js

import { defineStore } from 'pinia'
import { ref }         from 'vue'
const storeSection =
defineStore
( 'section', // must be unique

  () =>
  { const
      currentSection =
        ref(''),
   
      init =
        p_section =>
        currentSection.value = p_section,

      visability =
        p_section =>
        currentSection.value !== p_section ? 'hidden' : '',

      change =
        init // change macht dasselbe wie init

    return { init, visability, change }
  }
)

export default storeSection

StoreGreeting

Im Greeting-Store werden die Daten zur Begrüßung des Benutzers gespeichert.

// src/store/StoreGreeting.js

import { defineStore }               from 'pinia'
import { reactive, toRef, computed } from 'vue'

const
  storeGreeting =
  defineStore
  ( 'greeting',

    () =>
    { const
      state =
        reactive({ name: '', stranger: 'Stranger', hello: 'Hello, $1!' }),

      init =
        (p_state = {}) =>
        { Object.assign(state, p_state) },

      name = toRef(state, 'name'),

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

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

      hello =
        computed(() => sayHello(state.name))

      return { init, name, helloStranger, hello }
    }
)

export default storeGreeting

Einbinden der Stores in einer Komponente

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

<script setup>
  import { onMounted, onUnmounted } from 'vue'
  import ControllerKey from '@/controller/ControllerKey'
  import storeSection  from '@/store/StoreSection'
  import storeGreeting from '@/store/StoreGreeting'

  const
    section  = storeSection(),
    greeting = storeGreeting(),

    sayHello      = () => { section.change('hello') },
    controllerKey = new ControllerKey('Enter', sayHello, 'input_name')

  section.init('form')
//greeting.init({ hello: 'Hallo, $1!', stranger: 'Fremder'})

  onMounted  (() => controllerKey.add())
  onUnmounted(() => controllerKey.remove())
</script>

Zum Vergleich noch einmal das Skript ohne Store

<!-- src/components/HelloWorld.vue without store -->
<script setup >
  import { ref, computed, onMounted, onUnmounted } from 'vue'
  import ControllerKey from '@/controller/ControllerKey'

  const
    section       = ref('form'),
    helloStranger = ref('Hello, Stranger'),
    name          = ref(''),

    hello         = computed(() => `Hello, ${name.value}!`),
    visability    = p_section =>
                    p_section !== section.value ? 'hidden' : '',
    sayHello      = () => { section.value = 'hello' },
    autofocus     = () => { document.getElementById('input_name').focus() },
    controllerKey = new ControllerKey('Enter', sayHello, 'input_name')


  onMounted  (...)
  onUnmounted(...)
</script>

Anpassung des Templates

Das Template muss angepasst werden. Man muss nun auf die Store-Objekte zugreifen.

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

<template>
  <section id="section_question" v-bind:class="section.visability('question')">
    <h1>{{greeting.helloStranger}}</h1>
    <form>
      <div>
        <label for="input_name">What's your name?</label>
        <input id="input_name" type="text" autofocus
               v-model="greeting.name"
        />
      </div>
      <div>
        <input id="button_reset"  type="reset"  value="Reset"/>
        <input id="button_submit" type="button" value="Say hello"
               v-on:click="sayHello"
        />
      </div>
    </form>
  </section>
  <section id="section_hello" v-bind:class="section.visability('hello')">
    <h1>{{greeting.hello}}</h1>
    <p>Welcome to Web Programming!</p>
  </section>
</template>

Stores als Klassen

Stores können auch mit Hilfe von Klassen definiert werden. Dazu benötigt man zunächst eine Basisklasse, die von jedem Store wiederverwendet werden kann.

Basisklasse Store

// src/common/Store/Store.js

// cmp. https://medium.com/@mario.brendel1990/vue-3-the-new-store-a7569d4a546f
import { reactive } from 'vue'

class Store
{ #initState
  state

  reset()
  { if (this.state == null)
    { this.state = reactive({...this.initState}) }
    else
    { Object.assign(this.state, this.initState) }
  }

  init(p_state)
  { Object.assign(this.initState, p_state);
    this.reset();
  }

  constructor(p_state)
  { this.initState = p_state;
    this.reset();
  }
}

export default Store

Definition der Store-Klasse StoreSection

Nun kann der Section-Store als Klasse realisiert werden.

// src/store/StoreSection.js

import Store from '@/common/store/Store'

class StoreSection extends Store
{ constructor()
  { super({ name: '' }) }

  visability(p_section)
  { return this.state.name !== p_section ? 'hidden' : '' }

  change(p_section)
  { this.state.name = p_section }
}

const storeSection = new StoreSection();

export default storeSection;

Definition der Store-Klasse StoreGreeting

Den Greeting-Store kann man analog definieren.

// src/store/StoreGreeting.js

import Store        from '@/common/store/Store'
import { computed } from 'vue'

class StoreGreeting extends Store
{ constructor(p_init = { hello: 'Hello, $1!', stranger: 'Stranger' })
  { super({ name: '', ...p_init }); }

  get name()
  { return this.state.name; }

  set name(p_name)
  { this.state.name = p_name; }

  #sayHello(p_name)
  { return this.state.hello.replace('$1', p_name); }

  get helloStranger()
  { return computed(() => this.#sayHello(this.state.stranger)).value }

  get hello()
  { return computed(() => this.#sayHello(this.state.name !== ''
                                         ? this.state.name
                                         : this.state.stranger
                                        )
                   ).value
  }
}

const stateGreeting = new StoreGreeting()

export default stateGreeting

Störend ist hier, dass man für jedes Attribut wie z. B. name eine Getter- und eine Setter-Methode definieren muss. Dieses Problem kann man mit einer weiteren Superklasse abmildern.

StoreExtendible

Man definiert die Klasse StoreExtendible, die es ermöglicht, bei der Initialisierung des Stores dynamisch reaktive Getter- und Setter-Methoden hinzuzufügen.

// src/common/Store/StoreExtendible.js

import Store from './Store'

class StoreExtendible extends Store
{ #add(p_properties, p_getter, p_setter)
  { if (typeof p_properties === 'string')
    { Object.defineProperty
      ( this.constructor.prototype,
        p_properties,
        { ...( p_getter ? { get: () => this.state[p_properties] } : {} ),
          ...( p_setter ? { set: (value) =>
                                { if (this.state[p_properties] !== value)
                                  { this.state[p_properties] = value; }
                                },
                         }
                       : {}
             ),
          configurable: false, // the property cannot be redefined
          enumerable:   true
        },
      );
    }
    else
    { p_properties.forEach(e => this.addGetter(e)) }
  }

  addGetter(p_properties)
  { this.#add(p_properties, true, false) }

  addSetter(p_properties)
  { this.#add(p_properties, false, true) }

  addGetterSetter(p_properties)
  { this.#add(p_properties, true, true) }
}

export default StoreExtendible

Verwendung der alternativen Stores in einer Komponente

<!-- src/components/HelloWorld.vue with store class -->

<script setup >
  import { onMounted, onUnmounted } from 'vue'
  import ControllerKey from '@/controller/ControllerKey'
  import section  from '@/store/StoreSectionClass'
//import greeting from '@/store/StoreGreetingClass'
  import greeting from '@/store/StoreGreetingClassExtendible'

  const
    sayHello = () => { section.change('hello') },
    controllerKey =
       new ControllerKey('Enter', sayHello, 'input_name')

  section.init({ name: 'question'})
//greeting.init({ hello: 'Hallo, $1!', stranger: 'Fremder'})

  onMounted  (() => controllerKey.add())
  onUnmounted(() => controllerKey.remove())
</script>

Zum Vergleich: Die Verwendung der zuerst definierten Stores.

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

<script setup>
  import { onMounted, onUnmounted } from 'vue'
  import ControllerKey from '@/controller/ControllerKey'
  import storeSection  from '@/store/StoreSection'
  import storeGreeting from '@/store/StoreGreeting'


  const
    section  = storeSection(),
    greeting = storeGreeting(),

    sayHello = () => { section.change('hello') },
    controllerKey =
       new ControllerKey('Enter', sayHello, 'input_name')

  section.init('question')
//greeting.init({ hello: 'Hallo, $1!', stranger: 'Fremder'})

  onMounted  (() => controllerKey.add())
  onUnmounted(() => controllerKey.remove())
</script>

Fortsetzung des Tutoriums

Sie sollten nun Teil 4 des Vue-Tutoriums bearbeiten. Nun wird die Anwendung modularisiert.

Quellen

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