Русский
Русский
English
Статистика
Реклама

Отдаем корректный код 404 в связке VUE SPA SSR

Есть у меня один сайт, как сейчас говорят, пет-проект. Был написан в далеком 2013 году, что называется "на коленке" без использования каких-то фреймворков. Только php, только хардкор. Но тем не менее, функции свои выполнял, даже обрел некую популярность в узких кругах и был неплохо проиндексирован.

Недавно было решено начисто переписать его на современном стеке. Выбор пал на Laravel и Vue с серверным рендером. Сказано сделано. Сайт переписан, развернут на vps, работает. Но есть одно но. В яндекс-метрике остались тысячи ссылок, которые на текущий момент не актуальны, но эти адреса возвращают код 200 и поисковый бот снова и снова их проверяет. Не хорошо.

Итак, проблема обозначена. Посмотрим на используемый стек технологий, чтобы понять что к чему.

Подготовка

Laravel используется исключительно в качестве API, на сервере висит на localhost:81, а nginx проксирует к нему маршруты /api . Здесь ничего не сделать.

Фронтэнд написан с использованием фреймворка quasar. Это невероятно крутая вещь, которая может собрать вам сайт или приложение под несколько платформ. Я использую платформу SSR. В этом случае квазар собирает весь фронт, плюс генерирует nodejs-сервер на базе express. Этот сервер у меня запущен на localhost:3000 и опять же nginx проксирует к нему все остальные запросы (кроме API).

Чтобы говорить более предметно, давайте создадим простенький проект. Будем считать, что с установкой quasar/cli вы справитесь сами/

quasar create q404

В папке q404 будет создана стартовая заготовка проекта. Можно перейти в нее и запустить сервер разработки.

cd q404quasar dev -m ssr

Не заморачиваясь сильно на этом тестовом проекте, добавим вторую страницу AboutMe:

pages/AboutMe.vue
<script>export default {  name: 'AboutMe',};</script><template>  <q-page padding>    <h1>About me</h1>  </q-page></template>

Соответствующий роут

router/routes.js
const routes = [  {    path     : '/',    component: () => import('layouts/MainLayout'),    children : [      { path: '', component: () => import('pages/Index') },      // Added:      { path: 'about-me', component: () => import('pages/AboutMe') },    ],  },

И заменим главное меню

layouts/MainLayout.vue
const linksData = [  {    title: 'Homepage',    icon : 'code',    link : { path: '/' },  },  {    title: 'About Me',    icon : 'code',    link : { path: '/about-me' },  },  {    title: '404 test',    icon : 'code',    link : { path: '/404' },  },];

Для правильной работы следует еще поменять компонент EssentialLink.vue

EssentialLink.vue
<script>   ...   link: {      type   : Object,      default: null,   },   ...</script><template>  <q-item      clickable      :to="link"  >  ...  </q-item></template>

Теперь все готово. Если сейчас мы запустим dev-сервер и откроем сайт, то увидим, что все работает, а заглянув в исходный код страницы убедимся, что и серверный рендер отрабатывает.

Кроме одной проблемы страница 404 возвращает нам код ответа 200.

Поиск решения

Поиск информации в интернете готовых к использованию решений не дал. В официальном репозитории квазара есть ишью где рекомендуют создать отдельный роут для 404 страницы и редиректить на нее. Это не всегда подходит, мне, например, хотелось бы, чтобы пользователь оставался на той же странице, которую запросил, но с отображением плашки "404 not found", т.е. чтобы url в адресной строке не менялся.

Другой совет заключается в том, чтобы не использовать в приложении роут "*". Да. В этом случае при запросе несуществующей страницы сервер ответит кодом 404, но при навигации внутри приложения у нас теперь не будет красивой плашки 404. Не подходит.

На тостере предлагалось в сервере express делать дополнительные запросы и отправлять при необходимости 404-й код. Но сами понимаете, такое себе решение. В топку.

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

Но как я люблю отвечать заказчикам на их хотелки "для программиста нет ничего невозможного, чего бы он не мог сделать с кодом". Это наша вселенная, мы здесь боги.

Решение

Давайте еще раз сформулируем ТЗ. Мы хотим

  • отдавать 404 по несуществующим адресам (тем, что явно не прописаны в нашем роутере)

  • отдавать 404 по несуществующим эндпойнтам API

  • отдавать 404 при отсутствии запрошенной информации. Т.е. эндпойнт верный, но объекта в базе данных нет.

  • также не хотим отказываться от использования роута "*" на стороне клиента

Решение на самом деле находится на поверхности.

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

src-ssr/index.js
ssr.renderToString({ req, res }, (err, html) => {    if (err) {      if (err.url) {        res.redirect(err.url)      }      else if (err.code === 404) {        // Should reach here only if no "catch-all" route        // is defined in /src/routes        res.status(404).send('404 | Page Not Found')      }      else {        // Render Error Page or        // create a route (/src/routes) for an error page and redirect to it        res.status(500).send('500 | Internal Server Error')        if (ssr.settings.debug) {          console.error(`500 on ${req.url}`)          console.error(err)          console.error(err.stack)        }      }    }    else {      res.send(html)    }})

Комментарий в ветке условия 404 предупреждает нас, что сюда мы попадем, только если не будем использовать роут "*". А также мы можем понять, что если фреймворк не выбрасывает нас сюда, то мы сами можем бросить ошибку с телом {code:404}.

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

Для задействования данной фичи, нужно раскомментировать в файле quasar.conf.js строку

preFetch: true,

В компоненте Error404.vue добавим код

export default {  name: 'Error404',  preFetch({ ssrContext }) {    if (ssrContext) {      return Promise.reject({ code: 404 });    }  },};

Теперь при отображении данного компонента будет выбрасываться ошибка и express сервер сможет поймать её и ответить кодом 404. Причем, ошибка будет выбрасываться только в контексте серверного рендера, на клиенте же, перейдя на не существующий адрес, мы увидим красивую заглушку NotFound.

Первый и четвертый пункты требований мы выполнили.

Теперь займемся обработкой api-вызовов. Подготовим Axios. Создадим инстанс, настроим его и привяжем Vue.

boot/axios.js
import Axios from 'axios';export default ({ Vue, ssrContext, store }) => {  let axiosInstance = Axios.create({    baseURL         : '/api',    timeout         : 0,    responseType    : 'json',    responseEncoding: 'utf8',    headers         : {      'X-Requested-With': 'XMLHttpRequest',      'Accept'          : 'application/json',    },    // Reject only if the status code is greater than or equal to specify here    validateStatus: status => status < 500,  });    // ...  Vue.axios = axiosInstance;}

Здесь все стандартно обозначаем базовый урл, типы ответов, кодировку, заголовки. Функция validateStatus определяет ответы с какими кодами считать ошибкой. Мы будем считать ошибками все коды 5xx. В этом случае сайт будет возвращать код 500 и соответствующее сообщение.

Чтобы централизованно обрабатывать запросы к несуществующим эндпойнтам, добавим в эту конфигурацию перехватчик (interceptor в axios):

//...axiosInstance.interceptors.response.use(response => {  if (response.status >= 400) {    if (ssrContext) {      return Promise.reject({ code: response.status });    } else {      // store.commit('showErrorPage', response.status);    }  }  return response.data;});

К закомментированной строке вернемся позднее. Теперь Axios будет отклонять промис при ответах сервера с ошибками 4xx, и мы будем попадать в соответствующую ветку условия в сервере express чтобы вернуть правильный статус-код.

Для примера модифицируем компонент AboutMe.vue, добавив в него запрос к нашему API. Так как апишки у нас сейчас нет, запрос вернет 404 ошибку.

preFetch() {  return Vue.axios.get('/test.json')    .then(response => {      console.log(response);    });},

Здесь два важных момента. Мы должны обязательно вернуть промис и мы не должны перехватывать ошибку, оставив это на откуп библиотеке Axios. Если нам нужно выполнить для данной страницы несколько запросов, можно обернуть их в Promise.all.

Теперь, если мы перейдем на адрес /about-me, и обновим страницу, то увидим в панели разработчика браузера, что запрос страницы возвращает ответ с кодом 404. То что нужно поисковым системам! Пункт два выполнен.

Однако при внутреннем переходе на данную страницу пользователь никак не информируется о проблеме. Тут можно применить разные решения для отображения плашки 404. Я использовал следующее.

Добавил в стор флаг

showErrorPage: false,

Мутацию

export const showErrorPage = (state, show) => state.showErrorPage = show;

И условие в компонент основной раскладки

<q-page-container>  <Error404 v-if="$store.state.example.showErrorPage"/>  <router-view v-else/></q-page-container>

И возвращаясь к загрузчику Axios, раскомментируем там строку

store.commit('showErrorPage', response.status);

Еще в роутере придется добавить хук beforeEach для сброса этого флага (но только при работе в браузере)

router/index.js
export default function ({ store, ssrContext }) {  const Router = new VueRouter({    scrollBehavior: () => ({ x: 0, y: 0 }),    routes,    mode: process.env.VUE_ROUTER_MODE,    base: process.env.VUE_ROUTER_BASE,  });  if (!ssrContext) {    Router.beforeEach((to, from, next) => {      store.commit('showErrorPage', false);      next();    });  }  return Router;}

На данный момент мы реализовали 3 из 4-х пунктов технического задания.
Что касается третьего пункта

отдавать 404 при отсутствии запрошенной информации. Т.е. эндпойнт верный, но объекта в базе данных нет.

то тут возможны варианты. Если вы делаете свой API, как положено, RESTful, то такой запрос обязан вернуть статус-код 404, что уже вписывается в построенную систему. Если же вы по каким-то причинам возвращаете объекты типа

{  "status": false,  "message": "Object not found"}

то можно добавить дополнительные проверки в перехватчик Axios.

Еще кое-что

Внимательный читатель заметил, что мы в перехватчике отклоняем промис таким образом:

Promise.reject({ code: response.status });

А значит должны немного доработать express-сервер

else if (err.code >=400 && err.code < 500) {  res.status(err.code).send(`${err.code} | ${getStatusMessage(err.code)}`);}

Реализацию функции getStatusMessageрассматривать не будем =)
Таким образом мы получили также возможность корректной обработки любых 4хх кодов ответа от API и в первую очередь нам конечно интересны 401 и 403.

Заключение

Вот, пожалуй, и все, что я хотел написать.

Исходники тестового проекта закинул на гитхаб

Источник: habr.com
К списку статей
Опубликовано: 14.10.2020 12:12:47
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка веб-сайтов

Javascript

Vuejs

Vue

Ssr

Quasar

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru