Страница ошибки vue

Константин Базров

Константин Базров


ведущий разработчик NGRSOFTLAB

С появлением веб-приложений пришла потребность в смене URL-адресов с помощью JS. На помощь пришел History API браузера.

Благодаря этому все основные современные фреймворки позволяют программно управлять маршрутизацией с синхронизацией URL-адреса с представлением приложения.

Для маршрутизации во Vue-приложениях можно создать свою собственную интеграцию с History API, но лучше использовать официальную библиотеку от Vue — Vue-Router.

Базовые вещи

Использование можно начать хоть с установки с CDN:

<script src="https://unpkg.com/vue-router"></script>

Но мы начнем сразу с «правильного» варианта — с Vue Cli:

yarn global add @vue/cli

# ИЛИ

npm i -g @vue/cli

Создадим проект с помощью VUE CLI с базовым шаблоном — Default ([Vue 2] babel, eslint):

vue create vue-router-test-app

Минимальная конфигурация

Добавим роутер:

yarn add vue-router

# или

npm i --save vue-router

Добавим в Main.js минимальную конфигурацию для роутера:

/src/main.js

import Vue from "vue";
import App from "@/App.vue";
import VueRouter from "vue-router";
import HelloWorld from "@/components/HelloWorld";

Vue.use(VueRouter);

const routes = [
  {
    path: "",
    component: HelloWorld,
  },
];

const router = new VueRouter({
  routes,
  mode: "history",
});

Vue.config.productionTip = false;

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

Роуты представляют собой массив, каждый элемент которого — объект, где требуется указать path и component.

Чтобы увидеть изменения надо вывести компонент роутера — routerView, который отвечает за отображение. Для этого изменим App.vue:

/src/App.vue

<template>
  <div id="app">
    <router-view /> 
  </div>
</template>

Теперь, зайдем на http://localhost:8080/. Увидим страницу с маршрутом «/», где отображается компонент HelloWorld.vue, вместо тега router-view, который мы писали в App.vue.

Иерархия путей

Добавим маршрут в main.js (массив routes):

const routes = [
  {
    path: "",
    component: HelloWorld,
  },
  {
    path: "/board",
    component: {
      render: (h) => h("div", ["Board Page"]),
    },
  },
];

Зайдем по адресу http://localhost:8080/board. Увидим вторую страницу с отображением рендер-функции.

Параметры (Props) маршрута

Поправим дочерний маршрут для маршрута /board в main.js. Для дочерних компонентов надо указывать где в родительском компоненте отображать дочерние — компонентом router-view. В нашем случае — это в рендер-функция:

import Board from "@/components/Board";
const routes = [
  {
    path: "",
    component: HelloWorld,
  },
  {
    path: "/board",
    component: {
      render: (h) => h("div", ["Board Page", h("router-view")]),
    },
    children: [
      {
        path: '/board/:id',
        component: Board,
      }
    ]
  },
];

Напомню, что рендер-функция в template-представлении будет выглядеть следующим образом:

<template>
  <div>
    Board Page
    <router-view />
  </div>
</template>

Создадим компонент Board.vue с содержимым:

/src/components/Board.vue

<template>
  <div>Board with prop id: {{ id }}</div>
</template>

<script>
export default {
  computed: {
    id() {
      return this.$route.params.id;
    },
  },
};
</script>

Перейдем по адресу http://localhost:8080/board/21 и увидим родительский и дочерний компоненты Board с передачей параметра id равным 21.

Параметры маршрута доступны в компоненте по this.$route.params.

Если хотим более явно отобразить зависимость компонента от входных параметров, используем настройку props: true при настройке маршрута:

/src/main.js

children: [
  {
    path: '/board/:id',
    component: Board,
    props: true,
  }
]

А в компоненте Board.vue принять id как входной параметр компонента:

/src/components/Board.vue

<template>
  <div>Board with prop id: {{ id }}</div>
</template>

<script>
export default {
  props: {
    id: {
      type: String,
      default: null,
    },
  },
};
</script>

Метаданные (meta) маршрута

/src/main.js

  const routes = [
    {
      path: "",
      component: HelloWorld,
      meta: {
        dataInMeta: "test",
      },
    },
     ....
   ]

Теперь мы можем обратиться к метаданным роута из компонента HelloWorld.vue следующим образом:

this.$route.meta.dataInMeta.

Глубже (nested children)

В дочерние компоненты можно углубляться до бесконечности (до ограничений сервера).

Сделаем дочерний роут для дочернего роута.

/src/main.js

const routes = [
  {
    path: "",
    component: HelloWorld,
  },
  {
    path: "/board",
    component: {
      render: (h) => h("div", ["Board Page", h("router-view")]),
    },
    children: [
      {
        path: "/board/:id",
        component: Board,
        props: true,
        children: [
          {
            path: "child",
            component: {
              render: function(h) {
                return h("div", ["I'm Child with prop", this.propToChild]);
              },
              props: {
                propToChild: {
                  type: Number,
                  required: true,
                  default: null,
                },
              },
            },
          },
        ],
      },
    ],
  },
];

Рендер-функция теперь записана обычной функцией, т.к. нужен контекст компонента.

/src/components/Board.vue

<template>
  <div>
    Board with prop id: {{ id }}
    <router-view :prop-to-child="parseInt(id)" />
  </div>
</template>

<script>
export default {
  props: {
    id: {
      type: String,
      default: null,
    },
  },
};
</script>

Передаем дочернему компоненту дочернего компонента параметры через компонент router-view как обычному компоненту. Звучит сложно, но интуитивно понятно. И так, спускаем пропсы в дочернем — дочернему дочернего:

<router-view :prop-to-child="parseInt(id)" />

Пояснение за Path

Запись вида path: "child" означает, что мы обращаемся к пути родителя и продолжаем его путь: {parent-route}/child

Из дочернего компонента можно сослаться на любой другой уровень роута:

/src/main.js (routes):

children: [
      {
        path: "/first-level",
        ....
      }
    ]

Эта запись обрабатывает страницу с адресом: http://localhost:8080/first-level.

Шире (несколько router-view)

Можно использовать несколько router-view в 1 компоненте. Для этого в конфигурации маршрутов (routes) пишем вместо component — components, который принимает объект, где ключ — атрибут name у router-view. Если указать ключ «default», то такой компонент будет отображаться, если router-view безымянный (без атрибута name).

/src/main.js

const routes = [
  {
    path: "",
    component: HelloWorld,
  },
  {
    path: "/board",
    component: {
      render: (h) => h("div", ["Board Page", h("router-view")]),
    },
    children: [
      {
        path: "/board/:id",
        component: Board,
        props: true,
        children: [
          {
            path: "child",
            components: {
              default: { render: (h) => h("div", ["I'm Default"]) },
              user: { render: (h) => h("div", ["I'm User"]) },
              guest: { render: (h) => h("div", ["I'm Guest"]) },
            },
          },
        ],
      },
    ],
  },
]; 

/components/Board.vue

<template>
  <div>
    Board with prop id: {{ id }}
    <div>
      <label for="is-user">
        Is User?
        <input v-model="isUser" id="is-user" type="checkbox" />
      </label>
      <router-view :prop-to-child="parseInt(id)" />
      <router-view v-if="isUser" name="user" />
      <router-view v-else name="guest" />
    </div>
  </div>
</template>

<script>
export default {
  props: {
    id: {
      type: String,
      default: null,
    },
  },
  data() {
    return {
      isUser: false,
    };
  },
};
</script>

Перейдем по адресу: http://localhost:8080/board/23/child и увидим небольшой интерактив с переключением активных router-view.

Страница ошибки 404

Чтобы создать страницу ошибки, достаточно положить в конец списка маршрутов такую конструкцию:

/src/main.js(routes)

{
  path: "*",
  component: { render: (h) => h("div", ["404! Page Not Found!"]) },
},

Теперь, при переходе по несуществующему пути (например — http://localhost:8080/mistake), будет выведен компонент ошибки.

Лучше писать в таком виде:

/src/main.js

{
  path: "/page-not-found",
  alias: '*',
  component: { render: (h) => h("div", ["404! Page Not Found!"]) },
},

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

Защита маршрутов

Защиту маршрутов осуществляют с использованием метаданных маршрутов и хука beforeEach роутера.

/src/main.js

import Vue from "vue";
import App from "@/App.vue";
import VueRouter from "vue-router";

import HelloWorld from "@/components/HelloWorld";
import Board from "@/components/Board";

Vue.use(VueRouter);

const routes = [
  {
    path: "",
    component: HelloWorld,
  },
  {
    path: "/board",
    component: {
      render: (h) => h("div", ["Board Page", h("router-view")]),
    },
    meta: {
      requiresAuth: true,
    },
    children: [
      {
        path: "/board/:id",
        component: Board,
        props: true,
        children: [
          {
            path: "child",
            components: {
              default: { render: (h) => h("div", ["I'm Default"]) },
              user: { render: (h) => h("div", ["I'm User"]) },
              guest: { render: (h) => h("div", ["I'm Guest"]) },
            },
          },
        ],
      },
    ],
  },
  {
    path: "/auth-required",
    component: { render: (h) => h("div", ["Auth required!"]) },
  },
  {
    path: "/*",
    component: { render: (h) => h("div", ["404! Page Not Found!"]) },
  },
];

const router = new VueRouter({
  routes,
  mode: "history",
});

const isAuthenticated = () => false;

router.beforeEach((to, from, next) => {
  if (to.matched.some((route) => route.meta?.requiresAuth)) {
    if (isAuthenticated()) {
      next();
    } else {
      next("/auth-required");
    }
  } else {
    next();
  }
});

Vue.config.productionTip = false;

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

Теперь, при попытке получить доступ к странице, которая требует авторизации, нас перебросит на страницу /auth-required.

Навигация между маршрутами

Программная навигация

Программная навигация может вызываться из любого места вашего приложения таким образом:

$router.push('/dash/23/child')

Если мы хотим передать параметры, нам нужно использовать другой подход, основанный на использовании имен роутов.

Укажем имя роуту /board/:id:

  ...
   children: [
      {
        path: "/board/:id",
        name: 'board',
        component: Board,
        props: true,
        children: [
   ....

Теперь мы можем передавать параметры:

$router.push({ name: 'board', params: { id: 100500 }})

Получим ошибку «Invalid prop: type check failed for prop «id». Expected String with value «100500», got Number with value 100500».

Причина в том, что url — это всегда тип данных String, а мы передали программно id с типом Number. Исправляется это просто: перечислим возможные типы данных в компоненте.

components/Board.vue

props: {
  id: {
    type: [String, Number],
    default: null,
  },
},

Компонент routerLink

Компонент routerLink позволяет создавать ссылки внутри сайта, которые преобразуются в «нативные» браузерные ссылки (тег <а>):

<router-link to='/dash/23/child'> Link </router-link>

К таким ссылкам автоматически могут добавляться классы:

  • router-link-exact-activeточное совпадение;
  • router-link-active — частичное (активен дочерний компонент указанного в атрибуте to роута).

Чтобы не отображать активный класс родительских, достаточно написать атрибут exact:

<router-link to='/dash/23/child' exact> Link </router-link>

Мы можем переопределить создаваемый элемент:

<router-link tag="button" to='/dash'> Button </router-link>

К сожалению, в таком случае, классы не проставляются.

Также можем передавать объект:

<router-link :to="{ path: '/dash/23' "> Link </router-link>

<router-link :to="{ name: 'board', params: { id: 123 } }"> Link </router-link>

Лучшие практики

Этот раздел мы посвятим рефакторингу того, что мы написали выше.

Создаем структуру папок для роутера:

src/router/router.js
src/router/routes.js

Перенесем в router.js все, что касается настроек роутера:

import Vue from "vue";
import VueRouter from "vue-router";
import routes from "/routes";

Vue.use(VueRouter);

const router = new VueRouter({
  routes,
  mode: "history",
  base: process.env.BASE_URL,
});

const isAuthenticated = () => true;

router.beforeEach((to, from, next) => {
  if (to.matched.some((route) => route.meta?.requiresAuth)) {
    if (isAuthenticated()) {
      next();
    } else {
      next("/auth-required");
    }
  } else {
    next();
  }
});

export default router;

Перенесем routes.js все, что касается настроек маршрутов.

И сразу заменим импорты на динамические.

Если у Вас уже прописано много роутов, ручное изменение может потребовать много времени. Поможет регулярка:

^import (w+) from (".+")$

заменить на

const $1 = () => import(/* webpackChunkName: "$1" */ $2)

Теперь в Chrome Dev Tools во вкладке Network будет видно когда-какой компонент грузится из сети, а раньше все роуты загружались сразу в 1 мега-бандле.

src/router/routes.js

const HelloWorld = () => import(/* webpackChunkName: "HelloWorld" */ "@/components/HelloWorld")
const Board = () => import(/* webpackChunkName: "Board" */ "@/components/Board")

const routes = [
  {
    path: "",
    component: HelloWorld,
  },
  {
    path: "/board",
    component: {
      render: (h) => h("div", ["Board Page", h("router-view")]),
    },
    meta: {
      requiresAuth: true,
    },
    children: [
      {
        path: "/board/:id",
        name: "board",
        component: Board,
        props: true,
        children: [
          {
            path: "child",
            components: {
              default: { render: (h) => h("div", ["I'm Default"]) },
              user: { render: (h) => h("div", ["I'm User"]) },
              guest: { render: (h) => h("div", ["I'm Guest"]) },
            },
          },
        ],
      },
    ],
  },
  {
    path: "/auth-required",
    component: { render: (h) => h("div", ["Auth required!"]) },
  },
  {
    path: "/*",
    component: { render: (h) => h("div", ["404! Page Not Found!"]) },
  },
];

export default routes; 

Продвинутые приемы

Под «продвинутостью» подразумевается «приятность» их использования. К таким приемам можно отнести, например, такие темы как:

  • разбиение прав по уровням доступа;
  • анимацию переходов между страницами;
  • индикацию загрузки при переходе между роутами;
  • изменение тайтлов при переходе между роутами;
  •  плавный скролл по странице при переходе назад;
  • и т.п.

Итак, обо всем по-порядку.

Разбиение прав по уровням доступа

Бывает ситуация, когда у пользователей бывает более двух состояний: не только авторизация, но и другие. Например, платная подписка. С этих пор мы задумываемся про неограниченный уровень разделения прав. Делается это буквально парой десятков строчек кода, но для краткости, удобства и чтобы не изобретать велосипед, мы будем использовать готовую библиотеку. Установим ее:

yarn add vue-router-middleware-plugin

Создадим специальные файлы middleware для проверки прав пользователей:

router/middleware/authMiddleware.js

const isLoggedIn = () => !!window.localStorage.getItem("logged-in")

const authMiddleware = async ({ /* to, from to,*/ redirect }) => {
  if (!isLoggedIn()) {
    redirect({
      name: "login",
    });
  }
};

export default authMiddleware;

router/middleware/guestMiddleware.js

const isLoggedIn = () => !!window.localStorage.getItem("logged-in");

const guestMiddleware = async ({ /* to, from to,*/ redirect }) => {
  if (isLoggedIn()) {
    redirect({ name: "main" });
  }
};

export default guestMiddleware;

router/middleware/subscribersMiddleware.js

const isSubscribed = () => Promise.resolve(!!window.localStorage.getItem("has-license"))

const subscribersMiddleware = async ({ /* to, from, */ redirect }) => {
  if (!await isSubscribed()) {
    console.log("isn't subscribed, redirect to license")
    redirect({ name: 'license' })
  }
}

export default subscribersMiddleware

В последнем листинге, приведен пример асинхронной проверки, что значит — можно обращаться в actions стора и делать запросы на сервер.

Теперь поставим проверку на авторизацию на все роуты, а затем сделаем исключения для некоторых роутов:

/src/router/router.js

import Vue from "vue";
import VueRouter from "vue-router";
import routes from "./routes";
import MiddlewarePlugin from "vue-router-middleware-plugin";
import authMiddleware from "./middleware/authMiddleware";


Vue.use(VueRouter);

const router = new VueRouter({
  routes,
  mode: "history",
  base: process.env.BASE_URL,
});

Vue.use(MiddlewarePlugin, {
  router,
  middleware: [authMiddleware],
});

export default router; 

Теперь разберемся с конкретными маршрутами.

Поработаем над архитектурой нашего приложения, чтобы сделать его более предсказуемым. Сделаем отдельный шаблон Auth.vue и положим его в pages, а компоненты, которые там используются, т.е. в разделе /auth, положим в соответствующий раздел components.

Т.о. получается удобная структура:

pages
--Auth.vue
components
-- auth
---- Login.vue
---- Register.vue
---- Forgot.vue

Создадим вспомогательную функцию для генерации подобных роутов genAuthRoutes.

/src/router/routes.js

import guestMiddleware from "./middleware/guestMiddleware";
import authMiddleware from "./middleware/authMiddleware";
import subscribersMiddleware from "./middleware/subscribersMiddleware";

const MainBoard = () =>
  import(/* webpackChunkName: "MainBoard" */ "@/pages/MainBoard");
const BoardComponent = () =>
  import(
    /* webpackChunkName: "BoardComponent" */ "@/components/board/BoardComponent"
  );

const clearAndUpper = (text) => text.replace(/-/, "").toUpperCase();
const toPascalCase = (text) => text.replace(/(^w|-w)/g, clearAndUpper);

const genAuthRoutes = ({ parent, tabs = [] }) => ({
  path: `/${parent}`,
  name: parent,
  component: () => import(/* webpackChunkName: "auth" */ "@/pages/Auth"),
  redirect: { name: tabs[0] },

  children: tabs.map((tab) => {
    const tabPascalCase = toPascalCase(tab);
    return {
      path: tab,
      name: tab,
      component: () =>
        import(
          /* webpackChunkName: "[request]" */ `@/components/${parent}/${tabPascalCase}`
        ),
      meta: {
        middleware: {
          ignore: [authMiddleware],
          attach: [guestMiddleware],
        },
      },
    };
  }),
}); 
const routes = [
  genAuthRoutes({ parent: "auth", tabs: ["login", "register", "forgot"] }),
  {
    path: "/",
    name: "main",
    component: MainBoard,
    children: [
      {
        path: "/board",
        name: "board",
        component: {
          render: (h) => h("div", ["Board Page", h("router-view")]),
        },
        children: [
          {
            path: "/board/:id",
            name: "board-child",
            component: BoardComponent,
            props: true,
            children: [
              {
                path: "child",
                components: {
                  default: { render: (h) => h("div", ["I'm Default"]) },
                  user: { render: (h) => h("div", ["I'm User"]) },
                  guest: { render: (h) => h("div", ["I'm Guest"]) },
                },
                meta: {
                  middleware: {
                    attach: [subscribersMiddleware],
                  },
                },
              },
            ],
          },
        ],
      },
      {
        path: "/license",
        name: "license",
        component: {
          render: (h) => h("div", ["License Page"]),
        },
      },
    ],
  },
  {
    path: "/auth-required",
    name: "auth-required",
    component: { render: (h) => h("div", ["Auth required!"]) },
    meta: {
      middleware: {
        ignore: [authMiddleware],
      },
    },
  },
  {
    path: "/*",
    component: { render: (h) => h("div", ["404! Page Not Found!"]) },
    meta: {
      middleware: {
        ignore: [authMiddleware],
      },
    },
  },
];

export default routes;

Удаляем глобальную проверку на авторизацию в свойстве ignore и добавляем другую проверку в свойстве attach объекта meta.middleware:

 middleware: {
    ignore: [authMiddleware],
    attach: [guestMiddleware], 
 }

Создадим компоненты

  • src/components/auth/Login.vue;
  • src/components/auth/Register.vue;
  • src/components/auth/Forgot.vue,

с типовым шаблоном:

<template>
  <div>
    Forgot Page
  </div>
</template>

Также отрефакторим страницу Board, назовем его MainBoard

/src/pages/MainBoard.vue

<template>
  <div>
    <h1>Main Board Page</h1>
    <router-view />
  </div>
</template>

Соответственно, добавляем компоненты в соответствующую категорию в components:

/src/components/board/BoardComponent.vue

<template>
  <div>
    Board with prop id: {{ id }}
    <div>
      <label for="is-user">
        Is User?
        <input v-model="isUser" id="is-user" type="checkbox" />
      </label>
      <router-view :prop-to-child="parseInt(id)" />
      <router-view v-if="isUser" name="user" />
      <router-view v-else name="guest" />
    </div>
  </div>
</template>

<script>
export default {
  props: {
    id: {
      type: [String, Number],
      default: null,
    },
  },
  data() {
    return {
      isUser: false,
    };
  },
};
</script>

Осталось отрефакторить главный компонент — App.vue:

/src/App.vue

<template>
  <div id="app">
    <div class="links">
      <router-link :to="{ name: 'register' }">Register</router-link>
      <router-link :to="{ name: 'login' }">Login</router-link>
      <router-link :to="{ name: 'forgot' }">Forgot</router-link>
      <template v-if="loggedIn">
        <router-link :to="{ name: 'license' }">License</router-link>
        <router-link :to="{ name: 'board' }">Board</router-link>
        <router-link :to="{ name: 'board-child', params: { id: 33 } }"
          >Board:33</router-link
        >
        <router-link :to="{ path: '/board/33/child' }"
          >Board:33/child</router-link
        >
        <router-link :to="{ path: '/404' }">404</router-link>
      </template>

      <label for="logged-in"
        >Logged In
        <input type="checkbox" id="logged-in" v-model="loggedIn" />
      </label>
      <label for="has-license"
        >Has License
        <input type="checkbox" id="has-license" v-model="hasLicense" />
      </label>
    </div>

    <router-view />
  </div>
</template>

<script>
export default {
  data() {
    return {
      loggedIn: !!window.localStorage.getItem("logged-in"),
      hasLicense: !!window.localStorage.getItem("has-license"),
    };
  },
  watch: {
    loggedIn(e) {
      window.localStorage.setItem("logged-in", e ? true : "");
    },
    hasLicense(e) {
      window.localStorage.setItem("has-license", e ? true : "");
    },
  },
};
</script>

<style scoped>
.links > * {
  margin: 1em;
}
</style>

Теперь, снимем отметку с «Logged In» и попробуем перейти по маршруту http://localhost:8080/board. Нас незамедлительно переадресует на страницу «auth-required».

Поставим отметку на «Logged In», снимем с «Has License» и перейдем по маршруту http://localhost:8080/board/33/child. Нас перенесет на страницу license, однако, если снять отметку с «Logged In» и обновить страницу, то мы снова перейдем на страницу «auth-required».

Теперь проверим, можно ли зайти на страницу авторизации, когда пользователь уже прошел авторизацию. Поставим отметку «Logged In» и перейдем по адресу http://localhost:8080/auth/register. Нас перебросит на главную страницу.

Анимация переходов между страницами

Это просто. Оборачиваем главный RouterView компонентом анимации transition и добавляем стили:

src/App.vue

<template>
 ....
  <transition name="fade">
   <router-view />
  </transition>
 ...
</template>
<style scoped>
  ...
.fade-enter-active,
.fade-leave-active {
  transition-property: opacity;
  transition-duration: 0.25s;
}

.fade-enter-active {
  transition-delay: 0.25s;
}

.fade-enter,
.fade-leave-active {
  opacity: 0;
}
<style>

Индикация загрузки при переходе между роутами

Это тоже просто. Ставим библиотеку nprogress:

yarn add nprogress

Добавляем в router.js:

/src/router/router.js

import NProgress from 'nprogress'
import 'nprogress/nprogress.css';
router.beforeResolve((to, from, next) => {
  if (to.name) {
      // Запустить отображение загрузки
      NProgress.start()
  }
  next()
})

router.afterEach(() => {
  // Завершить отображение загрузки
  NProgress.done()
})

Изменение тайтлов при переходе между роутами

И это тоже просто.

Заполняем meta.title маршрутам и ставим document.title каждой странице в хуке beforeEach:

/src/router/router.js

...
router.beforeEach(async (to, from, next) => {
  const { title } = to.meta;
  const brand = "NGRSoftlab";
  document.title = `${title ? title + " | " : ""}${brand}`;
  next();
});
...

Плавный скролл по странице при переходе вперед/назад

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

/src/router/router.js

import VueScrollTo from "vue-scrollto";
const router = new VueRouter({
  routes,
  mode: "history",
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) {
      VueScrollTo.scrollTo("#app", 500, { offset: savedPosition.y });
      return savedPosition;
    } else {
      VueScrollTo.scrollTo("#app");
    }
  },
});

In the routes declaration, I like to add this:

[
  ...  
  { path: '/404', component: NotFound },  
  { path: '*', redirect: '/404' },  
  ...  
]

Which will imply that if the user is navigated to a path which does not match any routes, it will be redirected to the «404» route, which will contain the «not found» message.

The reason I’ve separated it into 2 routes is so that you can also programmatically direct the user to the 404 route in such a case when some data you need does not resolve.

For instance, if you were creating a blog, you might have this route:

{ path: '/posts/:slug', component: BlogPost }

Which will resolve, even if the provided slug does not actually retrieve any blog post. To handle this, when your application determines that a post was not found, do

return this.$router.push('/404')

or

return router.push('/404')

if you are not in the context of a Vue component.

One thing to bear in mind though is that the correct way to handle a not found response isn’t just to serve an error page — you should try to serve an actual HTTP 404 response to the browser. You won’t need to do this if the user is already inside a single-page-application, but if the browser hits that example blog post as its initial request, the server should really return a 404 code.

Cover image for Create a custom 404 page for your Vue 2 app

In this tutorial, you’ll learn how to add a custom 404 page to a Vue app (generated using the Vue CLI) with a basic Vue router configuration.

For this tutorial, I will be using a Vue router starter app generated using the Vue CLI. Here’s how the project file tree might look:

Vue CLI Router Filetree

Right now, all we need to focus on are src/router/index.js and the components of the src/views folder.

This is how src/router/index.js should somewhat look:

import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';

Vue.use(VueRouter)

  const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
  routes
})

export default router

Enter fullscreen mode

Exit fullscreen mode

1) Visit the home page of the Vue app. /

What do you see?

Vue app homepage

2) Visit the about page of the Vue app. /about

What do you see?

Vue app about

3) Visit a random url of the app. Like /hi/someurl/404

What do you see?

Vue app homepage

(I customised my Vue app a lot, so it looks a whole lot different from the starter Vue router app, kindly excuse me for that 😅)

What do we notice from the above 3 scenarios?

If we visit a URL that exists, it correctly renders the component associated with that route. But if the URL does not exist, it just redirects it to the homepage, instead of showing some sort of error or a default 404 page. You might also have noticed that the URL of the default Vue app has /#/ suffixed to the URL.

Vue app URL

We can fix all of these issues.

For the redirecting-to-homepage-when-it-doesn’t-exist case, we can create a custom 404 page, by specifying a wildcard route after all the other routes. First, we will have to create a 404 component.

In src/views folder, create a file named NotFound.vue. Add some basic text and images that makes it look like a 404 page.

<template>
  <center>
    <h1>Not Found!</h1>
    <p>
      <a href="/">Go home?</a>
    </p>
  </center>
</template>

<script>

  export default {
    name: 'NotFound'
  }

</script>

<style scoped>

  center {
    margin: 15vw;
  }

  h1 {
    color: var(--border);
    font-size: 2em;
  }

</style>

Enter fullscreen mode

Exit fullscreen mode

Once you have created NotFound.vue, in src/router/index.js add a wildcard route pointing towards the NotFound.vue component.

import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';
import NotFound from '../views/NotFound.vue';

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '*',
    name: 'Not Found',
    component: NotFound
  }
]

const router = new VueRouter({
  routes
})

export default router

Enter fullscreen mode

Exit fullscreen mode

But we need to do one more thing, only then can we «successfully» create a 404 page.

The weird URL.

The «weird» URL is because the Vue router uses hash mode for routing by default. It uses the URL hash to simulate a full URL so that the page won’t be reloaded when the URL changes.

We can prevent the Vue router from doing this by enabling History mode.

const router = new VueRouter({
  mode: 'history',
  routes
});

Enter fullscreen mode

Exit fullscreen mode

The final src/router/index.js:

import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';
import NotFound from '../views/NotFound.vue';

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '*',
    name: 'Not Found',
    component: NotFound
  }
]

const router = new VueRouter({
  mode: 'history',
  routes
})

export default router

Enter fullscreen mode

Exit fullscreen mode

And now, our URL looks normal!

Vue Normal URL

And that’s it! We have a fully functional 404 page now! Hope you enjoyed this tutorial!

In this article, we will look at how to implement a catch-all route to handle the page not found error and display a custom 404 page in the Vue application.

In the router configuration, we can specify a path /:notFound and also define a NotFound component to display the 404 custom page.

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      // ...
    },
    {
      path: "/:notFound",
      component: NotFound,
    },
  ],
});

The /:notFound will match any string in the URL and display the NotFound component. Let’s implement it and see it in action.

We will begin by creating a new application using Vue CLI.

❯ vue create catch-all-demo

Once, the installation is complete, we can navigate to the folder and run the server to load the application.

❯ cd catch-all-demo
❯ npm run serve

This will run our server on port 8080 by default http://localhost:8080/

Next, we will install the vue-router package required for routing. We can install it using CDN, npm or yarn.

npm:

npm install vue-router@4

yarn:

yarn add vue-router@4

Once the package is installed, we can integrate it with our Vue application.

Let’s create two components for our demo purpose.

❯ touch src/components/MyComponent.vue
❯ touch src/components/NotFound.vue

We will open the files and add some template code.

<!-- MyComponent.vue -->

<template>
  <h2>MyComponent</h2>
</template>

NotFound.vue

<!-- NotFound.vue -->

<template>
  <h2>404 Not found. The page you are looking for does not exist.</h2>
</template>

In the main.js we can define two routes, one route will be to display the MyComponent and the second route will be the catch-all route which will display the NotFound component.

// main.js

import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import App from "./App.vue";

import MyComponent from "./components/MyComponent";
import NotFound from "./components/NotFound";

const app = createApp(App);

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: "/my-component",
      component: MyComponent,
    },
    {
      path: "/:notFound",
      component: NotFound,
    },
  ],
});

app.use(router);
app.mount("#app");

Here, we have defined the catch-all route /:notFound which will catch any string that does not match the predefined route paths and display the NotFound component.

In the App.vue let’s add the router view.

<!-- App.vue -->
<template>
  <router-view></router-view>
</template>

<script>
export default {
  name: "App",
};
</script>

With this let’s open up the URL http://localhost:8080/my-component

catch-all-404-routes-vue-1

As expected, the Mycomponent component gets displayed. Now let’s try to add some random path in the URL like http://localhost:8080/xxx or http://localhost:8080/something

catch-all-404-routes-vue-2

catch-all-404-routes-vue-3

As you can see, our catch route is working and is able to display the custom 404 content.

If you are reading this post, chances are that you have been trying to implement a catch-all route into Vue 3, or upgrading one from Vue 2, and not being able to succeed. This article is going to provide you with the details you need to upgrade your application and successfully set up a route that would catch all endpoints.

TLTR;

Vue-router 4 (the version of vue-router that works with Vue 3) introduced a breaking change that require you to define a star or catch-all route using a parameter with a custom regex. An example for a 404 route would be “{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound }“.

What is a catch all route

A catch all route, also known as a wildcard, is a route that is specifically set to redirect users to a specific page in the case in which the inserted URL is not existing. This wildcard is very commonly used for a 404 page.

Regex in catch all route

Routes are usually defined by creating rules on the specific URL path. So for example creating a route for a path of /user would be caught when the user enters website.com/user, but it won’t if the user enters website.com/my-user or any other word that is not precisely the one set in the path rule.

For the case of a wildcard, or catch all route the rule is usually set using a regular expression. A regular expression is:

regular expression (shortened as regex or regexp; sometimes referred to as rational expression) is a sequence of characters that specifies a search pattern in text

https://en.wikipedia.org/wiki/Regular_expression

For the case of a catch all, the router rule would be defined by using a regex that would catch all character. This would be defined using the two symbols .*. These two symbols means respectively:

  • . – indicates any character
  • * – mean “0 or more instances of the preceding regex token”

How do you define a catch all in Vue 3

Now that we have learned what a catch all actually is, it is time to see how to use this in practice in our Vue 3 application. As I mentioned at the very start of this post, the syntax required to define this route in Vue 3 has changes from Vue 2 due to a breaking change.

The main change in Vue 3 require us to setup the Regex using a parameter with custom regex.

What are parameters with custom regex

Before we move forward it is important to really understand what a parameters with custom regex are in Vue router. Many people, included me, would jump to the definition of the catch all rule, without actually understanding what a parameter with custom regex is and its usage.

As the name explain, this feature merges two important options of routes: parameters and regex.

Parameters

In the site expressjs.com parameters in route are expressed as:

Route parameters are named URL segments that are used to capture the values specified at their position in the URL

https://expressjs.com

So for example a route set with the following path /:orderId would capture any value inserted after the forward slash and provide it to the server. Please note that the name used for the parameter is completely arbitrary and we could have used whatever we want as it does not affect the URL accessed, as shown below:

// Routes
{ path: '/:arbitraryParameter' }

// Correct URL
mywebsite.com/123
mywebsite.com/simone
mywebsite.com/simone123

Regex

The next features offered by the parameter with custom regex is the ability to define a regex for our route. So for example, we could enhance the example above by making sure that the route is just hit if the orderId is made of all numbers:

{ path: '/:orderId(\d+)' }

Using a bunch of custom regex to define paths is not really advised, as custom regex can be very fragile and hard to maintain. To avoid complicated routes, it is suggested to created nested routes to easily differentiate between routes without the need of a custom regex.

{ path: '/order/:orderId' }, // accessed from mywebsite.com/order/123
{ path: '/user/:userId' } // accessed from mywebsite.com/user/simone

The wildcard (catch all) scenario

Even if above we have indicated that regex should to not be used, there are some special cases where the use of regex can create helpful feature, for example when creating a catch all or 404 route.

As we mentioned above, this route is used in the case in which the user has miss-typed a route and did not enter anything that exist on the site, and returns a 404 (not found) page.

In this scenario, the use of a regex is beneficial as it helps us to define a rule that would catch URL entered, by creating a very generic regex.

To define a catch all rule in vue-router 4 we would enter the following code:

{ path: '/:pathMatch(.*)', name: 'not-found', component: NotFound }

As we can see, the catch all route is defined using the parameter with custom regex that we have recently introduced. The above rule will work with any URL that cannon be found in our defined rules, so for example mywebsite.com/fake/url and mywebsite.com/404 will redirect correctly to the NotFound component, but unfortunately it wont work if we try to call our route by its name.

How to create a catch all route that can be called by its name

There are cases in which the developers may want to redirect an user to the 404 page dynamically. This is usually the result of a bad request or an action that has resulted in the wrong outcomes. When defining a programmatic navigation, it is usually beneficial to do so using the name of routes, as it makes the code more readable. For example in our case the code to programmatically navigate to a catch all route would look like this:

router.push({ name: 'not-found'});

As we quickly mentioned previously, the above programmatic navigation will not work with out defined route. This is because the parameter that we have defined in our not-found route is currently required and it cannot be omitted.

To be able to navigate to this route with the use of its name (and without having to pass extra parameter) we need to make our route parameter optional.

Paramethers can be made optional in 2 different ways:

  • ? Mark a parameter as optional and cannot be repeated
  • * Mark a parameter as optional but repeatable.

In our case we would like to receive a list of all parameters sent as part of the URL and we would therefore use the *. Our route will now look like this:

{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound }

With the above code, we are not only able to catch wrong URL, but also able to dynamically access the route.

Conclusion

Creating a catch all route is quite simple, and searching on the internet usually provided the answer that we all wanted, but it is important sometimes to investigate things further and try to understand how things actually work and the main reason behind specific features.

In my case fully understand the catch all, has helped me to fully comprehendthe parameter with custom regex and “optional” options.

I hope this post has been helpful to you as it has been to me, and please feel free to leave a comment to help me improve this post for future readers.

Related

Понравилась статья? Поделить с друзьями:
  • Страница ошибки squid
  • Страница ошибки html css
  • Страница ошибки bootstrap
  • Страница ошибки 404 пример
  • Страница ошибки 404 ошибка 404