Инициализировать приложение Vue после изменения состояния аутентификации Firebase

У меня есть небольшая загадка в моем приложении Vue / vuex / vue-router + Firebase. Код начальной загрузки должен инициализировать Firebase, а также зарегистрированного пользователя (если есть) в Vuex:

new Vue({
  el: '#app', router, store, template: '<App/>', components: { App },
  beforeCreate() {
    firebase.initializeApp(firebaseConfig)
      // Because the code below is async, the app gets created before user is returned...
      .auth().onAuthStateChanged(user => {
        this.$store.commit('syncAuthUser', user) // this executes too late...
      })
  }
})

В моих маршрутах у меня есть

{ path: '/home', name: 'Home', component: Home, meta: { requiresAuth: true } }

а также

router.beforeEach((to, from, next) => {
  let authUser = store.getters.authUser // this is not set just yet...

  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!authUser) {
      next('signin') // /home will always hit here...
    }
    next()
  }
  else {
    next()
  }
})

Проблема

Как упоминалось в комментариях, приложение слишком поздно получает аутентифицированного пользователя. Итак, когда вы перейдете к /home во время входа в систему, приложение запустит beforeEachcheck в маршрутах ... И пользовательский экземпляр пока не будет доступен, поэтому он будет думать, что вы не вошли в систему, когда в на самом деле вы являетесь (это видно из моих вычисленных свойств, которые были правильно обновлены через несколько мгновений).

Вопрос

Итак, как мне изменить свой код, чтобы приложение Vue инициализировалось только после того, как Firebase вернет пользователя и после того, как этот пользователь будет передан в хранилище Vuex? Пожалуйста, дайте мне знать, если я делаю что-то совершенно не так.

Попытки

  1. Я пробовал использовать подход «отказаться от подписки»; однако его проблема в том, что onAuthStateChanged сработает только один раз. Итак, если я помещу туда свой критический this.$store.commit('syncAuthUser', user), то этот код будет выполняться только при создании приложения, а не при выходе пользователя, например.
  2. Я также попробовал более подходящий подход здесь . Я действительно работал на себя, но мне было плохо, завертывая onAuthStateChanged в другой Promise, наверняка должен быть лучший способ; плюс, кажется неправильным помещать логин инициализации пользователя auth в файл маршрутов.

person Alex    schedule 20.07.2017    source источник
comment
Привет, я тоже столкнулся с этой проблемой с Firebase. Я использую Angular, но у меня такая же проблема. В конце концов, я решил, что надежный способ - дождаться в компоненте аутентификации пользователя. И да, здесь лучше всего использовать наблюдаемые.   -  person Michelangelo    schedule 04.09.2019


Ответы (2)


Самый простой способ - дождаться состояния аутентификации перед монтированием приложения:

const app = new Vue({
  router,
  store,
  render: h => h(App),
});

firebase.auth().onAuthStateChanged(user => {
  store.commit('authStateChanged', user);
  app.$mount('#app');
});

Если вы хотите показать счетчик перед монтированием, поместите его в #app:

<div id="app"><div class="loader">Loading...</div></div>

Фактически, это сделает ваши действия более чистыми, а приложение более надежным, если вы извлечете логику состояния аутентификации в authStateChanged фиксацию, как указано выше, и обработаете здесь перенаправление маршрута. Поскольку это будет обрабатываться, если состояние аутентификации изменится, пока вы находитесь на маршруте, в то время как router.beforeEach будет проверять, когда вы собираетесь по нему.

firebase.auth().onAuthStateChanged(user => {
  store.commit('auth/authStateChanged', user);

  const isAuthed = Boolean(user);
  const { meta } = router.history.current;

  if (isAuthed && meta.isPublicOnly) { // e.g. /login + /signup
    router.push('/dashboard');
  } else if (!isAuthed && meta.isPrivate) {
    router.push('/login');
  }

  app.$mount('#app');
});

Обратите внимание, что перед этим выполняется первый router.beforeEach обратный вызов, поэтому его следует игнорировать, пока не будет определено состояние аутентификации. Поскольку мы обрабатываем первое перенаправление в onAuthStateChanged, это не имеет значения.

state: {
  isInited: false,
  isAuthed: false,
},
mutations: {
  authStateChanged(state, user) {
    state.isAuthed = Boolean(user);
    state.isInited = true;
  }
}

router.beforeEach((to, from, next) => {
  const { isInited, isAuthed } = store.state.user;

  if (isInited) {
    if (!isAuthed && to.matched.some(record => record.meta.isPrivate)) {
      return next('/login');
    } else if (isAuthed && to.matched.some(record => record.meta.isPublicOnly)) {
      return next('/dashboard');
    }
  }

  next();
});

Вы, вероятно, можете вообще предотвратить загрузку маршрута до тех пор, пока isInited не станет истинным.

person Dominic    schedule 04.09.2019
comment
Что делать, если в вашем приложении есть, например, зона для участников и не для членов, но общие компоненты. Если вы не вошли в систему, он будет бесконечно возвращать null и никогда не загрузит приложение. Нет никакой разницы между отсутствием входа в систему и ожиданием ответа об успешном входе в систему. - person Michelangelo; 04.09.2019
comment
Не уверен, что вы имеете в виду, если вы не вошли в систему, onAuthStateChanged по-прежнему будет вызываться с user, установленным на null, поэтому ваше приложение все равно будет загружаться, но с состоянием вашего магазина, говорящим, что пользователь не авторизован? - person Dominic; 04.09.2019
comment
null всегда будет исходным генерируемым значением наблюдаемого, даже когда вы входите в систему. Наблюдаемый испускает новое значение при изменении состояния, но к тому времени ваше приложение уже может быть смонтировано или нет. Надеюсь, я прояснил свой вопрос. - person Michelangelo; 04.09.2019
comment
Однако это не так, если вы вошли в систему, первый обратный вызов будет иметь объект пользователя, я бы предположил, что это ваша наблюдаемая реализация - person Dominic; 04.09.2019
comment
Хорошо, мне просто интересно. Мне было трудно реализовать состояние аутентификации высокого уровня для некоторых компонентов. Например: я запустил функцию, когда компонент завершил загрузку, и у меня возникла проблема с синхронизацией. Иногда он действительно получал объект пользователя, а иногда просто возвращал null, поэтому мне приходилось ждать в компоненте объекта пользователя. Это приложение Angular, но я изо всех сил пытался получить абстрактное решение на более высоком уровне, например, в службе или в чем-то еще. Может быть, с этим вкладом я смогу взглянуть, найду ли я лучшее решение. Возможно, теперь, когда я думаю об этом, это может быть Angular. - person Michelangelo; 05.09.2019
comment
Да, просто проведите небольшой эксперимент и поместите firebase.auth().onAuthStateChanged вне angular или чего-то еще и посмотрите, что имеет обратный вызов с console.log. У меня так работает в нескольких приложениях - person Dominic; 05.09.2019
comment
app.$mount('#app'); вызывается намеренно после каждого изменения авторизации? - person Boern; 02.06.2020
comment
Прошло некоторое время с тех пор, как я коснулся представления, но я считаю, что это то же самое, что и ReactDOM.render, который можно вызывать несколько раз (это эквивалентно вызову рендеринга из верхней части дерева, и Vue будет разумно повторно отображать только то, что необходимо ). При этом у вас может быть простой var, чтобы сделать это один раз, так как я думаю, что это не нужно, т.е. if (!mounted) { app.$mount(..); mounted = true } - person Dominic; 03.06.2020

ПРИМЕР

./App.vue

created: function() {
  auth.onAuthStateChanged(user => {

    this.$store.dispatch("SET_USER", user)

    user.getIdToken().then( token => {
      this.$store.dispatch("SET_TOKEN", token)
    })
}

./store.js (мутации)

export default {
  SET_USER(state, user) {
    state.authenticated = !!user
    state.user = {
      email: user ? user.email : null,
      displayName: user ? user.displayName : null,
      photoURL: user ? user.photoURL : null,
    }
  }
  SET_TOKEN(state, payload) {
    state.token = payload
  }
}

ОБЗОР

Жизненный цикл

Этот пример работает с использованием ловушки жизненного цикла created: Vue. Внутри created: мы используем auth.onAuthStateChanged для запуска прослушивателя событий. Ваше приложение теперь ожидает изменений авторизации от firebase.

auth.onAuthStateChanged(user => { ...listening for user objects... })

Объект user возвращается всякий раз, когда Firebase замечает изменение состояния аутентификации. Используйте его свойства и методы для аутентификации и авторизации.

Свойство: user.emailVerfied

Метод: user.getIdToken()

TL;DR

При использовании Firebase onAuthStateChanged необходимо реализовать объекты Observable и Subscriptions. Наблюдаемый RxJS является асинхронным, как promise, и поможет вам дождаться ответа аутентификации от Firebase.

См. первый аргумент nextOrObserver из документация:

onAuthStateChanged(nextOrObserver, error, completed) returns function()

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

person Chadd    schedule 17.11.2017
comment
Старайтесь избегать слишком большого количества ссылок в ваших ответах - если они умрут, ваш ответ станет неясным. - person Mike W; 17.11.2017
comment
@jmwatts Хороший замечание, перенесены дополнительные ссылки на комментарии: два отличных примера использования Observables в Vue: Vue w / RxJS Vue с vue-rx - person Chadd; 27.11.2017