Vue: как использовать несколько шаблонов в SPA

Vue: как использовать несколько шаблонов в SPA

Нередки ситуации, когда для разных страниц сайта используются различные шаблоны. Простейший пример: один шаблон для страницы авторизации и другой - для всего остального. Рассмотрим пару способов реализации такого функционала в одностраничных приложениях Vue.

Будем исходить из того, что у нас уже создано Vue-приложение с помощью vue-cli (включающее vuex и vue-router). Если Вы не знаете как это сделать, рекомендую озакомиться с этой документацией, либо посмотреть это короткое видео. Для стилизации используем materialize-css. Т.е. необходимо выполнить команду:

npm install materialize-css

 

Способ 1 - vuex и метаданные маршрутов

Первым делом в каталоге src создаём подкаталог layouts, после чего структура проекта примет примерно такой вид:

Cледующий шаг - войдём в подкаталог и реализуем шаблон по умолчанию и шаблон для входа в приложение.

Код AuthLayout.vue:

<template>
  <div class="auth-wrapper">
    <slot/>
  </div>
</template>

<script>
export default {}
</script>

<style lang="scss" scoped>
.auth-wrapper {
  width: 100%;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background-image: linear-gradient(to bottom right, #004d40, #00bfa5);
}
</style>

 

и DefaultLayout.vue:

<template>
  <div>
    <nav class="teal accent-4">
      <div class="nav-wrapper container">
        <router-link class="brand-logo" to="/">MultiLayouts</router-link>|
        <ul id="nav-mobile" class="right hide-on-med-and-down">
          <li>
            <router-link to="/">Home</router-link>|
          </li>
          <li>
            <router-link class="waves-effect grey darken-1 btn" to="/login">Sign in</router-link>
          </li>
        </ul>
      </div>
    </nav>
    <div class="container">
      <slot/>
    </div>
  </div>
</template>

<script>
export default {
  name: 'DefaultLayout'
}
</script>

<style lang="scss">
#nav {
  padding: 30px;
  a {
    font-weight: bold;
    color: #2c3e50;
    &.router-link-exact-active {
      color: #42b983;
    }
  }
}
</style>

 

Изменим код App.vue, добавив динамическое переключение компонентов. При этом поступим следующим образом - будем использовать шаблон, который указан в свойстве meta.layout, если же такое свойство не указано - значит, нам нужен шаблон по умолчанию:

<template>
  <div id="app">
    <component :is="layout">
      <router-view/>
    </component>
  </div>
</template>

<script>
export default {
  computed: {
    layout() {
      return this.$route.meta.layout || 'default-layout'
    }
  }
}
</script>

 

Также в каталог views добавим пару простейших компонентов. Home.vue:

<template>
  <div class="home">
    <h1>Home page</h1>
  </div>
</template>

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

 

и Login.vue c более-менее простой формой:

<template>
  <div class="card auth-form">
    <div class="card-content">
      <div class="card-title">Sign in</div>
      <form action="#">
        <div class="input-field">
          <i class="material-icons prefix">email</i>
          <input id="email" type="email" class="validate">
          <label for="email">Email</label>
        </div>

        <div class="input-field">
          <i class="material-icons prefix">vpn_key</i>
          <input id="password" type="password" class="validate">
          <label for="password">Password</label>
        </div>

        <div class="btn-group">
          <router-link class="btn waves-effect waves-light grey" to="/">
            Back
            <i class="material-icons left">keyboard_backspace</i>
          </router-link>
          <button class="btn waves-effect waves-light" type="submit">
            Submit
            <i class="material-icons right">send</i>
          </button>
        </div>
      </form>
    </div>
  </div>
</template>

<script>
export default {}
</script>

<style lang="scss" scoped>
.auth-form {
  min-width: 400px;

  .input-field {
    margin-bottom: 2rem;
  }

  .card-title {
    margin-bottom: 2rem;
    text-align: center;
  }

  .btn-group {
    display: flex;
    justify-content: space-between;
  }
}
</style>

 

Основа есть. Теперь надо заставить это всё работать. Хранить текущее значение шаблона будем во Vuex. Код файла store.js:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    layout: 'default-layout'
  },
  mutations: {
    setLayout(state, payload) {
      state.layout = payload
    }
  },
  actions: {},
  getters: {
    layout(state) {
      return state.layout
    }
  }
})

 

Прописываем маршруты (router.js):

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('@/views/Home.vue')
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('@/views/auth/Login.vue'),
      meta: {
        layout: 'auth-layout'
      }
    }
  ]
})

 

И собираем всё в main.js:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

import 'materialize-css/dist/css/materialize.css'

import DefaultLayout from './layouts/DefaultLayout.vue'
import AuthLayout from './layouts/AuthLayout.vue'

Vue.component('default-layout', DefaultLayout)
Vue.component('auth-layout', AuthLayout)

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

 

Готово!

 

Способ 2 - пакет vue-extend-layout

Надо заметить, что струтура и файлы будут практически теми же, за исключением того, что vuex для переключения шаблонов не понадобится. К пакету прилагается неплохая документация, и, откровенно говоря, нижеследующее будет лишь её небольшой вариацией. Итак, устанавливаем расширение:

npm install vue-extend-layout --save

 

Сосредоточимся на отличиях. App.vue теперь выглядит вот так:

<template>
  <div id="app">
    <vue-extend-layouts/>
  </div>
</template>

<script>
import VueExtendLayouts from 'vue-extend-layout'

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

 

И в шаблонах нет слотов (приведу код auth.vue):

<template>
  <div class="auth-wrapper">
    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'auth'
}
</script>

<style lang="scss" scoped>
.auth-wrapper {
  width: 100%;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background-image: linear-gradient(to bottom right, #004d40, #00bfa5);
}
</style>

 

Как уже говорилось, обойдёмся без хранилища. Что касается роутов - свойство мета указываем для каждого из них (router.js):

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: () => import('./views/Home'),
      meta: {
        layout: 'default'
      }
    },
    {
      path: '/login',
      name: 'login',
      component: () => import('./views/Login.vue'),
      meta: {
        layout: 'auth'
      }
    }
  ]
})

 

Страницы-компоненты Home.vue и Login.vue ничем не отличаются от описанных в предыдущем варианте, поэтому дублировать код не имеет смысла. И напоследок main.js:

import Vue from 'vue'
import App from './App.vue'
import router from './router'

import 'materialize-css/dist/css/materialize.css'

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

 

Вот, собственно, и всё. Успехов!