Подпишитесь на мою рассылку сейчас по адресу http://jauyeung.net/subscribe/.

Подпишитесь на меня в Twitter по адресу https://twitter.com/AuMayeung

Слоты - это полезная функция Vue.js, которая позволяет вам разделять различные части компонента в единое целое. Когда ваш компонент разделен на слоты, вы можете повторно использовать компоненты, поместив их в определенные вами слоты. Это также делает ваш код более чистым, поскольку позволяет отделить макет от логики приложения.

Кроме того, если вы используете слоты, вам больше не нужно составлять компоненты с родительскими дочерними отношениями, поскольку вы можете помещать любые компоненты в свои слоты.

Вот простой пример слотов Vue. Вы определяете свой слот в Layout.vue файле:

<template>
  <div class="frame">
    <slot name="frame"></slot>
  </div>
</template>

Затем в другом файле вы можете добавить:

<Layout>
  <template v-slot:frame>
     <img src="an-image.jpg">
   </template>
</Layout>

Чтобы использовать слот в вашем Layout компоненте.

Мы проясним приведенный выше пример, создав пример приложения. Чтобы проиллюстрировать использование слотов в Vue.js, мы создадим адаптивное приложение, которое отображает фрагменты статей из New York Times API и страницу поиска, где пользователи могут вводить ключевое слово для поиска в API.

В макете рабочего стола слева будет список названий элементов, а справа - фрагменты статей. Мобильный макет будет иметь раскрывающийся список для выбора отображаемого раздела и карточек с фрагментами статей под ним.

На странице поиска вверху будет форма поиска, а под ней - фрагменты статей, независимо от размера экрана.

Чтобы начать создание приложения, мы начнем с запуска Vue CLI. Мы бежим:

npx @vue/cli create nyt-app

для создания проекта Vue.js. Когда появится мастер, мы выбираем «Выбрать функции вручную». Затем мы решили включить в наш проект Vue Router и Babel.

Затем мы добавляем наши собственные библиотеки для стилизации и выполнения HTTP-запросов. Мы используем BootstrapVue для стилизации, Axios для выполнения запросов, VueFilterDateFormat для форматирования дат и Vee-Validate для проверки формы.

Для установки всех библиотек запускаем:

npm i axios bootstrap-vue vee-validate vue-filter-date-format

После того, как все библиотеки установлены, мы можем приступить к созданию нашего приложения.

Сначала мы используем слоты для создания макетов наших страниц. Создайте BaseLayout.vue в папке components и добавьте:

<template>
  <div>
    <div class="row">
      <div class="col-md-3 d-none d-lg-block d-xl-none d-xl-block">
        <slot name="left"></slot>
      </div>
      <div class="col">
        <div class="d-block d-sm-none d-none d-sm-block d-md-block d-lg-none">
          <slot name="section-dropdown"></slot>
        </div>
        <slot name="right"></slot>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: "BaseLayout"
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

В этом файле мы используем слоты Vue для создания адаптивного макета для домашней страницы. В этом файле есть слоты left, right и section-dropdown. Слот left отображается только при большом экране, поскольку мы добавили классы d-none d-lg-block d-xl-none d-xl-block в слот left. Слот section-dropdown отображается только на маленьких экранах, поскольку мы добавили к нему классы d-block d-sm-none d-none d-sm-block d-md-block d-lg-none. Эти классы являются адаптивными служебными классами из Bootstrap.

Полный список адаптивных служебных классов находится на https://getbootstrap.com/docs/4.0/utilities/display/.

Затем создайте файл SearchLayout.vue в папке components и добавьте:

<template>
  <div class="row">
    <div class="col-12">
      <slot name="top"></slot>
    </div>
    <div class="col-12">
      <slot name="bottom"></slot>
    </div>
  </div>
</template>
<script>
export default {
  name: "SearchLayout"
};
</script>

чтобы создать еще один макет для нашей страницы поиска. У нас есть слоты top и bottom, занимающие всю ширину экрана.

Затем мы создаем папку mixins и в ней создаем файл requestsMixin.js и добавляем:

const axios = require("axios");
const APIURL = "https://api.nytimes.com/svc";
export const requestsMixin = {
  methods: {
    getArticles(section) {
      return axios.get(
        `${APIURL}/topstories/v2/${section}.json?api-key=${process.env.VUE_APP_API_KEY}`
      );
    },
searchArticles(keyword) {
      return axios.get(
        `${APIURL}/search/v2/articlesearch.json?api-key=${process.env.VUE_APP_API_KEY}&q=${keyword}`
      );
    }
  }
};

создать миксин для выполнения HTTP-запросов к New York Times API. process.env.VUE_APP_API_KEY - это ключ API для New York Times API, и мы получаем его из файла .env в корневой папке проекта, где ключ переменной среды - VUE_APP_API_KEY.

Затем в Home.vue замените существующий код на:

<template>
  <div class="page">
    <h1 class="text-center">Home</h1>
    <BaseLayout>
      <template v-slot:left>
        <b-nav vertical pills>
          <b-nav-item
            v-for="s in sections"
            :key="s"
            :active="s == selectedSection"
            @click="selectedSection = s; getAllArticles()"
          >{{s}}</b-nav-item>
        </b-nav>
      </template>
      <template v-slot:section-dropdown>
        <b-form-select
          v-model="selectedSection"
          :options="sections"
          @change="getAllArticles()"
          id="section-dropdown"
        ></b-form-select>
      </template>
      <template v-slot:right>
        <b-card
          v-for="(a, index) in articles"
          :key="index"
          :title="a.title"
          :img-src="(Array.isArray(a.multimedia) && a.multimedia.length > 0 && a.multimedia[a.multimedia.length-1].url) || ''"
          img-bottom
        >
          <b-card-text>
            <p>{{a.byline}}</p>
            <p>Published on: {{new Date(a.published_date) | dateFormat('YYYY.MM.DD hh:mm a')}}</p>
            <p>{{a.abstract}}</p>
          </b-card-text>
          <b-button :href="a.short_url" variant="primary" target="_blank">Go</b-button>
        </b-card>
      </template>
    </BaseLayout>
  </div>
</template>
<script>
// @ is an alias to /src
import BaseLayout from "@/components/BaseLayout.vue";
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  name: "home",
  components: {
    BaseLayout
  },
  mixins: [requestsMixin],
  data() {
    return {
      sections: `arts, automobiles, books, business, fashion,
      food, health, home, insider, magazine, movies, national,
      nyregion, obituaries, opinion, politics, realestate, science,
      sports, sundayreview, technology, theater,
      tmagazine, travel, upshot, world`
        .split(",")
        .map(s => s.trim()),
      selectedSection: "arts",
      articles: []
    };
  },
  beforeMount() {
    this.getAllArticles();
  },
  methods: {
    async getAllArticles() {
      const response = await this.getArticles(this.selectedSection);
      this.articles = response.data.results;
    },
    setSection(ev) {
      this.getAllArticles();
    }
  }
};
</script>
<style scoped>
#section-dropdown {
  margin-bottom: 10px;
}
</style>

Мы используем слоты, определенные в BaseLayout.vue в этом файле. В слот left мы помещаем туда список имен разделов, чтобы отображать список слева, когда у нас есть экран размером с рабочий стол.

В слот section-dropdown мы поместили раскрывающийся список, который отображается только на мобильных экранах, как определено в BaseLayout.

Затем в слот right мы помещаем карточки начальной загрузки для отображения фрагментов статей, как определено в BaseLayout.

Мы помещаем все содержимое слота внутрь BaseLayout и используем v-slot вне элементов, которые мы хотим поместить в слоты, чтобы элементы отображались в назначенном слоте.

В разделе script мы получаем статьи по разделам, определяя функцию getAllArticles из requestsMixin.

Затем создайте файл Search.vue и добавьте:

<template>
  <div class="page">
    <h1 class="text-center">Search</h1>
    <SearchLayout>
      <template v-slot:top>
        <ValidationObserver ref="observer" v-slot="{ invalid }">
          <b-form @submit.prevent="onSubmit" novalidate id="form">
            <b-form-group label="Keyword" label-for="keyword">
              <ValidationProvider name="keyword" rules="required" v-slot="{ errors }">
                <b-form-input
                  :state="errors.length == 0"
                  v-model="form.keyword"
                  type="text"
                  required
                  placeholder="Keyword"
                  name="keyword"
                ></b-form-input>
                <b-form-invalid-feedback :state="errors.length == 0">Keyword is required</b-form-invalid-feedback>
              </ValidationProvider>
            </b-form-group>
            <b-button type="submit" variant="primary">Search</b-button>
          </b-form>
        </ValidationObserver>
      </template>
<template v-slot:bottom>
        <b-card v-for="(a, index) in articles" :key="index" :title="a.headline.main">
          <b-card-text>
            <p>By: {{a.byline.original}}</p>
            <p>Published on: {{new Date(a.pub_date) | dateFormat('YYYY.MM.DD hh:mm a')}}</p>
            <p>{{a.abstract}}</p>
          </b-card-text>
          <b-button :href="a.web_url" variant="primary" target="_blank">Go</b-button>
        </b-card>
      </template>
    </SearchLayout>
  </div>
</template>
<script>
// @ is an alias to /src
import SearchLayout from "@/components/SearchLayout.vue";
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  name: "home",
  components: {
    SearchLayout
  },
  mixins: [requestsMixin],
  data() {
    return {
      articles: [],
      form: {}
    };
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      const response = await this.searchArticles(this.form.keyword);
      this.articles = response.data.response.docs;
    }
  }
};
</script>
<style scoped>
</style>

Это очень похоже на Home.vue. Мы помещаем форму поиска в слот top, помещая ее внутрь SearchLayour, и помещаем содержимое нашего слота для слота top, помещая нашу форму внутрь элемента <template v-slot:top>.

Мы используем ValidationObserver для проверки всей формы и ValidationProvider для проверки ввода keyword. Оба они предоставлены Vee-Validate.

После нажатия кнопки «Поиск» мы вызываем this.$refs.observer.validate(); для проверки формы. Мы получили this.$refs.observer, так как ValidationObserver мы вынесли за пределы формы.

Затем, как только проверка формы завершится успешно, this.$refs.observer.validate() разрешив true, мы вызываем searchArticles из requestsMixin для поиска статей.

В слот bottom кладем карточки для вывода результатов поиска по статье. Он работает так же, как и другие слоты.

Далее в App.vue мы помещаем:

<template>
  <div>
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="#">New York Times App</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item to="/" :active="path == '/'">Home</b-nav-item>
          <b-nav-item to="/search" :active="path == '/search'">Search</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <router-view />
  </div>
</template>
<script>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>
<style>
.page {
  padding: 20px;
}
</style>

чтобы мы добавили сюда BootstrapVue b-navbar и наблюдали за изменением маршрута, чтобы мы могли установить опору active на ссылку страницы, на которой в данный момент находится пользователь.

Затем мы меняем код main.js на:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import VueFilterDateFormat from "vue-filter-date-format";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
Vue.use(VueFilterDateFormat);
Vue.use(BootstrapVue);
extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.config.productionTip = false;
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

Мы импортируем все используемые здесь пакеты для всего приложения, такие как BootstrapVue, Vee-Validate, а также виджеты для выбора календаря и даты и времени.

Стили также импортируются сюда, поэтому мы можем видеть их во всем приложении.

Затем в router.js замените существующий код на:

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import Search from "./views/Search.vue";
Vue.use(Router);
export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    },
    {
      path: "/search",
      name: "search",
      component: Search
    }
  ]
});

чтобы установить маршруты для нашего приложения, чтобы, когда пользователи вводят указанный URL-адрес или щелкают ссылку с ним, они могут видеть нашу страницу.

Наконец, мы заменяем код в index.html на:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title>New York Times App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-slots-tutorial-app doesn't work properly without
        JavaScript enabled. Please enable it to continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

чтобы изменить название приложения.

Наконец, мы запускаем наше приложение, запустив npm run serve в папке проекта нашего приложения, чтобы запустить наше приложение.

После этого вы должны увидеть: