Ściągamy Vue CLI i odpalamy, patrzymy jak wygląda „prawdziwy” projekt w w Vue.js. Do dzieła.

Ok, zakładam, że ściągnąć Vue CLI na tym poziomie już sobie potrafimy i takich rzeczy już tłumaczyć nie będziemy. Odpalamy nowy projekt.

Teraz patrzymy na main.js:

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

createApp(App).mount('#app')

Niby wszystko fajnie, taki odpowiednik Vue.createApp, które robiliśmy jak raczkowaliśmy we frontendzie. Tylko gdzie tu komponenty? Cóż, app.component tworzy komponent o zasięgu globalnym, tutaj takich nie ma.

Ok, idziemy do App.vue:

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <HelloWorld msg="Welcome to Your Vue.js App"/>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Mamy tutaj klasyczny dla nowoczesnego Vue (i każdego Vue poza raczkowaniem z „Vue.createApp”) podział:

  • tag template – markup komponentu/apki
  • tag script – logika JS
  • tag style – CSSy

Tutaj w script mamy ściągnięcie komponentu HelloWorld (także w components jest określony) oraz export default (poznamy jeszcze script setup, to nie będziemy musieli nic eksportować).

Ok, swoją drogą templatka apki nie jest ograniczona typowym ograniczeniem komponentów, czyli jeden top-level element. Tak jak nasze #app, też nie było.

Idziemy do komponentu HelloWorld:

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <p>
      For a guide and recipes on how to configure / customize this project,<br>
      check out the
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
    </p>
    //(...)
  </div>
</template>

<script>

export default {
  name: 'HelloWorld',
  props: {
    msg: String
  }
}

</script>


<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

Templatka komponentów ma ograniczenie jeden top-level element. Komponenty nie mają data, tylko propsy przekazywane z góry. Poza template, w script mamy nazwę komponentu i przyjmowane propsy.

Tag style ma atrybut scoped, aby ograniczyć te style do komponentu, aby się nie gryzły przypadkiem z tak samo nazwanymi stylami w innym komponencie.

Swoją drogą odpalmy naszą apkę:

npm run serve;

Kliknijmy h3 i podejrzyjmy style:

h3[data-v-469af010] {
  margin: 40px 0 0;
}

Tak wygląda scoped style po stronie klienta. Komponent HelloWorld i każde jego dziecko ma ustawiony atrybut „data-v-469af010”. W ten sposób działają scoped styles.

Swoją drogą, zdziwiło mnie to, HTML5 wspiera coś takiego, jak scoped style tags (czyli tag style zagnieżdżony wewnątrz tagu HTML o ograniczonym do niego zasięgu), choć to niszowa rzecz, myślałem, że z tego korzysta Vue.

Cóż, Vue pewnie istniało zanim to wprowadzili do HTMLa a sposób w jaki podchodzą do scoped styles się sprawdza.

Ok, jedna rzecz do zapamiętania – script setup:

<script setup>
const props = defineProps(['foo'])

</script>

Czyli taki bardziej Reactowy i mniej „export defaultowy” sposób podejścia do tagu script. Dokumentacja jest pełna przykładów z tym podejściem, ono jest dominujące, będziemy musieli się z nim polubić.

Pozwolę sobie wrzucić Modal.vue utworzony przez Laravel Breeze:

<script setup>
import { computed, onMounted, onUnmounted, watch } from 'vue';

const props = defineProps({
    show: {
        type: Boolean,
        default: false,
    },
    maxWidth: {
        type: String,
        default: '2xl',
    },
    closeable: {
        type: Boolean,
        default: true,
    },
});

const emit = defineEmits(['close']);

watch(
    () => props.show,
    () => {
        if (props.show) {
            document.body.style.overflow = 'hidden';
        } else {
            document.body.style.overflow = null;
        }
    }
);

const close = () => {
    if (props.closeable) {
        emit('close');
    }
};

const closeOnEscape = (e) => {
    if (e.key === 'Escape' && props.show) {
        close();
    }
};

onMounted(() => document.addEventListener('keydown', closeOnEscape));

onUnmounted(() => {
    document.removeEventListener('keydown', closeOnEscape);
    document.body.style.overflow = null;
});

const maxWidthClass = computed(() => {
    return {
        sm: 'sm:max-w-sm',
        md: 'sm:max-w-md',
        lg: 'sm:max-w-lg',
        xl: 'sm:max-w-xl',
        '2xl': 'sm:max-w-2xl',
    }[props.maxWidth];
});
</script>

Tak wyglądają nowoczesne komponenty. Nie musimy tego rozumieć od ręki, ale jest trochę inaczej, niż w naszych poprzednich zabawach. Mimo wszystko mam nadzieję, że co nieco ogarniamy.