ミドルウェアとは
Vue Router を使うにあたって、それぞれのルートで何かしら共通の処理を行いたい場合に、ロジックを使いまわせると便利です。使い回しが効くように切り出されたロジックをここではミドルウェアと呼んでいます。Vue Router のライフサイクルに組み込まれたフックを利用してミドルウェア機構を実装していきます。
Router の作成
import Vue from 'vue'
import store from '~/store'
import Meta from 'vue-meta'
import routes from './routes'
import Router from 'vue-router'
import { sync } from 'vuex-router-sync'
Vue.use(Meta)
Vue.use(Router)
// The middleware for every page of the application.
const globalMiddleware = ['locale', 'check-auth']
// Load middleware modules dynamically.
const routeMiddleware = resolveMiddleware(require.context('~/middleware', false, /.*\.js$/))
const router = createRouter()
sync(store, router)
export default router
-
globalMiddleware
- すべてのページに適用されるミドルウェアを設定します。
-
resolveMiddleware
- 後述します
-
createRouter
-
Router
のインスタンス化とフックの登録をおこなう。
createRouterfunction createRouter() { const router = new Router({ scrollBehavior, mode: 'history', routes }) router.beforeEach(beforeEach) // before ガードの登録 router.afterEach(afterEach) // after フックの登録 return router }
-
-
sync
- Vuex のストアで route 情報にアクセスできるようにします。
ナビゲーションガード
ナビゲーションガードとは、ルーターによる遷移の可否をつかさどる仕組みで、特定のコンテンツを認証済みのユーザーだけに公開したりする際に利用します。これをガードと呼びます。
-
router.beforeEach
: グローバル before ガード-
next
関数を受け取り、ナビゲーションを確立するにはnext
関数を呼び出してフックを解決します。 - 前処理を記述します。
beforeEach/** * Global router guard. * * @param {Route} to * @param {Route} from * @param {Function} next */ async function beforeEach(to, from, next) { // Get the matched components and resolve them. const components = await resolveComponents(router.getMatchedComponents({ ...to })) if (components.length === 0) { return next() } // Start the loading bar. if (components[components.length - 1].loading !== false) { router.app.$nextTick(() => router.app.$loading.start()) } // Get the middleware for all the matched components. const middleware = getMiddleware(components) // Call each middleware. callMiddleware(middleware, to, from, (...args) => { // Set the application layout only if "next()" was called with no args. if (args.length === 0) { router.app.setLayout(components[0].layout || '') } next(...args) }) }
-
router.getMatchedComponents
現在のルートまたは提供されたロケーションにマッチしているコンポーネント (インスタンスではなく定義 / コンストラクタ) の配列を返します。
-
resolveComponents
/** * Resolve async components. * * @param {Array} components * @return {Array} */ async function resolveComponents(components) { return await Promise.all( components.map(async component => { return typeof component === 'function' ? await component() : component }) ) }
-
getMiddleware
- 後述します
-
callMiddleware
- 後述します
-
グローバル after フック
グローバル after フックも router.beforeEach
と同様にルーターのライフサイクルの中で呼ばれるメソッドですが、ガードとは意味合いが異なるため単純に「フック」と呼んでいます。
-
router.afterEach
: グローバル after フック-
next
関数を受け取らず、遷移の可否には影響しません。 - 後処理を記述します。
afterEach/** * Global after hook. * * @param {Route} to * @param {Route} from * @param {Function} next */ async function afterEach(to, from, next) { await router.app.$nextTick() router.app.$loading.finish() }
ローディングインジケータを終了させる
vm.$loading.finish
トリガは、確実にDOMが更新された後にコールされるように、インスタンスプロパティvm.$nextTick
のPromise
が解決されるのを待つようにします(async/await
)。 -
ミドルウェア機構の実装
-
callMiddleware
/** * Call each middleware. * * @param {Array} middleware * @param {Route} to * @param {Route} from * @param {Function} next */ function callMiddleware(middleware, to, from, next) { const stack = middleware.reverse() const _next = (...args) => { // Stop if "_next" was called with an argument or the stack is empty. if (args.length > 0 || stack.length === 0) { if (args.length > 0) { router.app.$loading.finish() } return next(...args) } const middleware = stack.pop() if (typeof middleware === 'function') { middleware(to, from, _next) } else if (routeMiddleware[middleware]) { routeMiddleware[middleware](to, from, _next) } else { throw Error(`Undefined middleware [${middleware}]`) } } _next() }
- ミドルウェアは複数指定が可能なため、ミドルウェアのスタックを再帰的に処理している。
- 各ミドルウェアは
_next
をコールして解決する。 -
middleware
がfunction
の場合は、その関数をそのまま実行する。 -
middleware
がstring
の場合はrouteMiddleware
配列から既定のミドルウェアを実行する。
-
getMiddleware
router/index.js/** * Merge the the global middleware with the components middleware. * * @param {Array} components * @return {Array} */ function getMiddleware(components) { const middleware = [...globalMiddleware] components.filter(c => c.middleware).forEach(component => { if (Array.isArray(component.middleware)) { middleware.push(...component.middleware) } else { middleware.push(component.middleware) } }) return middleware }
- コンポーネントからミドルウェアを取り出します。
- コンポーネント側で
middleware
が配列で指定されていた場合、globalMiddleware
とマージされ1次配列にフラット化されます。
- コンポーネント側で
- コンポーネントからミドルウェアを取り出します。
-
scrollBehavior
/** * Scroll Behavior * * @link https://router.vuejs.org/en/advanced/scroll-behavior.html * * @param {Route} to * @param {Route} from * @param {Object|undefined} savedPosition * @return {Object} */ function scrollBehavior(to, from, savedPosition) { if (savedPosition) { return savedPosition } if (to.hash) { return { selector: to.hash } } const [component] = router.getMatchedComponents({ ...to }).slice(-1) if (component && component.scrollToTop === false) { return {} } return { x: 0, y: 0 } }
- 遷移時のスクロールの振る舞いをカスタマイズします。
- コンポーネントに対し
scrollTop: true
が設定されている場合、ポジションをトップへ移動せさます。
-
resolveMiddleware
/** * @param {Object} requireContext * @return {Object} */ function resolveMiddleware(requireContext) { return requireContext .keys() .map(file => [file.replace(/(^.\/)|(\.js$)/g, ''), requireContext(file)]) .reduce((guards, [name, guard]) => ({ ...guards, [name]: guard.default }), {}) }
ミドルウェアの利用
-
middleware
の指定方法string
function
[string|function]
string<script> export default { middleware: 'auth' } </script>
array<script> export default { middleware: ['auth', 'logger'] } </script>
array-and-function<script> const bar = (to, from, next) => { // // do something // next() // important! } export default { middleware: [ 'foo', bar, ] } </script>
※本記事では、Vue.js を単一ファイルコンポーネントで利用することを想定しています。