mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-10 21:42:24 +03:00
refactor: use watchers to track all meta changes
This commit is contained in:
+71
-16
@@ -1,11 +1,18 @@
|
||||
import Vue from 'vue'
|
||||
import VueMeta from 'vue-meta'
|
||||
import Router from 'vue-router'
|
||||
import { createMixin } from './vue-meta-next/mixin'
|
||||
|
||||
Vue.use(Router)
|
||||
Vue.use(VueMeta, {
|
||||
refreshOnceOnNavigation: true
|
||||
/*Vue.use(VueMeta, {
|
||||
refreshOnceOnNavigation: false,
|
||||
//waitOnDestroyed: false,
|
||||
//debounceWait: 0
|
||||
})
|
||||
/**/
|
||||
|
||||
Vue.mixin(createMixin())
|
||||
/**/
|
||||
|
||||
let metaUpdated = 'no'
|
||||
const ChildComponent = {
|
||||
@@ -15,20 +22,25 @@ const ChildComponent = {
|
||||
<h3>You're looking at the <strong>{{ page }}</strong> page</h3>
|
||||
<p>Has metaInfo been updated due to navigation? {{ metaUpdated }}</p>
|
||||
</div>`,
|
||||
metaInfo () {
|
||||
return {
|
||||
title: `${this.page} - ${this.date && this.date.toTimeString()}`,
|
||||
bodyAttrs: {
|
||||
class: 'child-component'
|
||||
},
|
||||
afterNavigation () {
|
||||
metaUpdated = 'yes'
|
||||
}
|
||||
}
|
||||
metaInfo: {
|
||||
title() {
|
||||
return `${this.page} - ${this.date && this.date.toTimeString()}`
|
||||
},
|
||||
/*bodyAttrs: {
|
||||
class: 'child-component'
|
||||
},*/
|
||||
meta: [
|
||||
function () { return { vmid: 'descr', name: 'description', content: `description at ${this && this.date2 && this.date2.toTimeString()}` } },
|
||||
{ name: 'og:description', content: `og:description at ${this && this.date2 && this.date2.toTimeString()}` }
|
||||
],
|
||||
/*afterNavigation () {
|
||||
metaUpdated = 'yes'
|
||||
}*/
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
date: null,
|
||||
date2: null,
|
||||
metaUpdated
|
||||
}
|
||||
},
|
||||
@@ -36,9 +48,14 @@ const ChildComponent = {
|
||||
this.interval = setInterval(() => {
|
||||
this.date = new Date()
|
||||
}, 1000)
|
||||
|
||||
this.interval2 = setInterval(() => {
|
||||
this.date2 = new Date()
|
||||
}, 3000)
|
||||
},
|
||||
destroyed () {
|
||||
clearInterval(this.interval)
|
||||
clearInterval(this.interval2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,23 +82,47 @@ const router = new Router({
|
||||
]
|
||||
})
|
||||
|
||||
const getTime = () => window.performance.now()
|
||||
|
||||
let navtime
|
||||
let navtimes = []
|
||||
|
||||
const App = {
|
||||
router,
|
||||
metaInfo() {
|
||||
return {
|
||||
meta: [
|
||||
{ vmid: 'descr', name: 'description', content: 'JAHASJDKASD' }
|
||||
]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
router.beforeEach((to, from, next) => {
|
||||
navtime = getTime()
|
||||
console.log('before', navtime)
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach((to, from) => {
|
||||
console.log('after', getTime())
|
||||
navtimes.push((getTime()) - navtime)
|
||||
console.log(navtimes.length, navtimes.reduce((acc, v) => (acc + v), 0) / navtimes.length, document.title, '|', top.location.pathname)
|
||||
})
|
||||
},
|
||||
template: `
|
||||
<div id="app">
|
||||
<h1>vue-router</h1>
|
||||
<router-link to="/">Home</router-link>
|
||||
<router-link to="/about">About</router-link>
|
||||
<transition name="page" mode="out-in">
|
||||
<router-view></router-view>
|
||||
</transition>
|
||||
<p>Inspect Element to see the meta info</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
// <transition name="page" mode="out-in">
|
||||
|
||||
const app = new Vue(App)
|
||||
|
||||
/*
|
||||
const { set, remove } = app.$meta().addApp('custom')
|
||||
|
||||
set({
|
||||
@@ -93,8 +134,22 @@ set({
|
||||
]
|
||||
})
|
||||
setTimeout(() => remove(), 3000)
|
||||
|
||||
*/
|
||||
app.$mount('#app')
|
||||
/*
|
||||
let i = 0
|
||||
const interval = setInterval(() => {
|
||||
router.push(top.location.href.includes('about') ? '/' : '/about')
|
||||
i++
|
||||
if (i == 3) {
|
||||
navtimes.shift()
|
||||
navtimes.shift()
|
||||
}
|
||||
if (i >= 50) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 1000)
|
||||
/**/
|
||||
|
||||
/*
|
||||
const waitFor = time => new Promise(r => setTimeout(r, time || 1000))
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
import Vue from 'vue'
|
||||
import {
|
||||
sortReverse,
|
||||
sortMetaData,
|
||||
createMetaInfoForTypeKey,
|
||||
createWatcher
|
||||
} from './util'
|
||||
|
||||
// This object contains the metadata returned by vue components
|
||||
// grouped by vmid. metadata without vmid is not listed here.
|
||||
// The purpose of this object is to be able to re-assign the vmid
|
||||
// when the VueComponent that supplied the vmid with the highest priority
|
||||
// gets destroyed
|
||||
// -> title is a special case, its always treated as if it has a vmid
|
||||
export const allMetaData = {
|
||||
title: [],
|
||||
meta: {}
|
||||
}
|
||||
|
||||
// This object shadows the metadata that is actually rendered
|
||||
// -> title is a special case, its always treated as if it has a vmid
|
||||
// -> for meta (and link, script, etc) the signature here is: {
|
||||
// any: [] // <- an array with all metadata objects without a vmid
|
||||
// [vmid]: {} // <- for each vmid the vmid is used as key with the metadata object as value
|
||||
export const displayedMetaData = Vue.observable({
|
||||
title: undefined,
|
||||
meta: {
|
||||
any: []
|
||||
}
|
||||
})
|
||||
|
||||
// find the meta data with the highest priority, if multiple components exists
|
||||
// with the same priority then the lowest component id is used
|
||||
function evaluateMetaDataToUse(vm, type, key) {
|
||||
// TODO: add error handling
|
||||
if (key) {
|
||||
const allMetaDataForTypeById = allMetaData[type][key]
|
||||
|
||||
if (allMetaDataForTypeById.length > 1) {
|
||||
allMetaDataForTypeById.sort(sortMetaData)
|
||||
}
|
||||
|
||||
// re-use the previous HTMLElement if possible
|
||||
if (displayedMetaData[type][key]) {
|
||||
// we need to copy the HTMLElement and unwatch function if they exist
|
||||
for (const prop of ['_el', '_unwatch']) {
|
||||
allMetaDataForTypeById[0][prop] = displayedMetaData[type][key][prop]
|
||||
}
|
||||
}
|
||||
|
||||
// add watcher
|
||||
Vue.set(displayedMetaData[type], key, allMetaDataForTypeById[0])
|
||||
|
||||
// check if this a new element that wasnt displayed before, if so
|
||||
// add a watcher to create/modify the HTMLElement and store the
|
||||
// returned unwatcher to prevent memory leaks
|
||||
if (!allMetaDataForTypeById[0]._unwatch) {
|
||||
const unwatch = vm.$root.$watch(
|
||||
`$data.metaInfo.${type}.${key}`,
|
||||
createWatcher(type),
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
allMetaDataForTypeById[0]._unwatch = unwatch
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: this should only be called for title? maybe base?
|
||||
|
||||
const allMetaDataForTypeById = allMetaData[type]
|
||||
if (allMetaDataForTypeById.length > 1) {
|
||||
// only sort when there is more then 1 element
|
||||
allMetaDataForTypeById.sort(sortMetaData)
|
||||
}
|
||||
|
||||
Vue.set(displayedMetaData, type, allMetaDataForTypeById[0])
|
||||
}
|
||||
|
||||
// This method is called every time a watcher is triggered on the
|
||||
// classic metaInfo / head property of a SFC
|
||||
export function updateMetaInfo(vm, data) {
|
||||
for (const type in data) {
|
||||
if (typeof data[type] === 'function') {
|
||||
console.warn(`prop ${type} ignored, function within functions are not supported`)
|
||||
continue
|
||||
}
|
||||
updateMetaInfoForType(vm, data[type], type)
|
||||
}
|
||||
}
|
||||
|
||||
// To support changes bya single type (meta, link script) only, this method is
|
||||
// used as the real updater method. It can also call itself
|
||||
// -> nestedIndex: important prop. Its the indicator which groups updates returned
|
||||
// from a meta array element item function. The index is 1-based (not 0-base) so
|
||||
// we can still check for falsy values, eg:
|
||||
// metaInfo: {
|
||||
// meta: [ // <- calls updateMetaInfoForType without nestedIndex
|
||||
// function () { return {} }, // <- calls updateMetaInfoForType without nestedIndex = 1
|
||||
// function () { return {} } // <- calls updateMetaInfoForType without nestedIndex = 2
|
||||
// ],
|
||||
export function updateMetaInfoForType(vm, data, type, nestedIndex) {
|
||||
nestedIndex = nestedIndex || 0
|
||||
|
||||
const _cid = vm._vueMeta.id
|
||||
const allMetaDataForType = allMetaData[type]
|
||||
|
||||
if (type === 'title') {
|
||||
const index = allMetaDataForType.findIndex(metaData => metaData._cid === _cid)
|
||||
|
||||
if (index === -1) {
|
||||
allMetaDataForType.push({ _cid, prio: vm._vueMeta.depth || _cid, content: data })
|
||||
|
||||
evaluateMetaDataToUse(vm, type)
|
||||
} else {
|
||||
allMetaDataForType[index].content = data
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
console.log(`What is this, an unsupported feature perhaps? Expected array for ${type}`)
|
||||
return
|
||||
}
|
||||
|
||||
const vmidsSeen = []
|
||||
|
||||
for (const [idx, metaData] of Object.entries(data)) {
|
||||
if (typeof metaData === 'function') {
|
||||
const unwatch = vm.$watch(
|
||||
metaData,
|
||||
(newValue) => {
|
||||
// ensure its an array
|
||||
if (!Array.isArray(newValue)) {
|
||||
newValue = [newValue]
|
||||
}
|
||||
updateMetaInfoForType(vm, newValue, type, idx + 1)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
vm._vueMeta.unwatch.push(unwatch)
|
||||
continue
|
||||
}
|
||||
|
||||
const { vmid } = metaData
|
||||
if (!vmid) {
|
||||
continue
|
||||
}
|
||||
|
||||
vmidsSeen.push(vmid)
|
||||
|
||||
allMetaDataForType[vmid] = allMetaDataForType[vmid] || []
|
||||
|
||||
// TODO: not sure anymore if we need this
|
||||
const _key = createMetaInfoForTypeKey(vm, vmid, nestedIndex)
|
||||
const prio = metaData.prio || vm._vueMeta.depth || _cid
|
||||
const index = allMetaDataForType[vmid].findIndex(metaData => metaData._key === _key)
|
||||
|
||||
// set this before the splice when vmid was already lsited
|
||||
const newOrPrioChanged = index === -1 || allMetaDataForType[vmid][index].prio !== prio
|
||||
|
||||
// we can assign directly to metaData because its the result of a watcher
|
||||
metaData = Object.assign(metaData, {
|
||||
_cid,
|
||||
_nested: nestedIndex,
|
||||
_key,
|
||||
prio
|
||||
})
|
||||
|
||||
if (index === -1) {
|
||||
allMetaDataForType[vmid].push(metaData)
|
||||
} else {
|
||||
// we are going to assign the metadata object to the eixsiting one in allMetaData
|
||||
// TODO: is this really necessary? cant we just copy el/unwatch again and thats it? think so?
|
||||
for (const prop in allMetaDataForType[vmid][index]) {
|
||||
// we need to keep these props and we know they dont
|
||||
if (['_el', '_unwatch'].includes(prop) || prop in metaData) {
|
||||
continue
|
||||
}
|
||||
|
||||
delete allMetaDataForType[vmid][index][prop]
|
||||
}
|
||||
|
||||
Object.assign(allMetaDataForType[vmid][index], metaData)
|
||||
}
|
||||
|
||||
// only re-evaluate which data to show when its new data or the priority has changed
|
||||
if (newOrPrioChanged) {
|
||||
evaluateMetaDataToUse(vm, type, vmid)
|
||||
}
|
||||
}
|
||||
|
||||
// remove old/previous metadata from this component
|
||||
removeMetaDataForType(vm, _cid, type, ({ vmid, _nested }) => {
|
||||
// dont remove if the metadata has a different nesting index
|
||||
if (nestedIndex !== _nested) {
|
||||
return false
|
||||
}
|
||||
|
||||
// remove if metadata doesnt has vmid or when its vmid wasnt listed in this update
|
||||
return !vmid || !vmidsSeen.includes(vmid)
|
||||
})
|
||||
|
||||
for (const metaData of data) {
|
||||
if (typeof metaData === 'function' || metaData.vmid) {
|
||||
// these types have already been handled above, ignore
|
||||
continue
|
||||
}
|
||||
|
||||
// clone metaData!!
|
||||
displayedMetaData[type].any.push({
|
||||
...metaData,
|
||||
_cid,
|
||||
_nested: nestedIndex
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function removeMetaDataForType(vm, componentId, type, filter) {
|
||||
for (const key in displayedMetaData[type]) {
|
||||
if (key === 'any') {
|
||||
// the any key lists all the metadata without a vmid
|
||||
displayedMetaData[type][key]
|
||||
.map((metaData, index) => {
|
||||
if (metaData._cid !== componentId) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (filter && !filter(metaData)) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return index
|
||||
})
|
||||
.filter(index => index > -1)
|
||||
// reverse sort to make sure array indexes arent changed after splicing
|
||||
.sort(sortReverse)
|
||||
.map(index => {
|
||||
const { _el: el, vmid } = displayedMetaData[type][key][index]
|
||||
if (el) {
|
||||
el.remove()
|
||||
}
|
||||
|
||||
displayedMetaData[type][key].splice(index, 1)
|
||||
|
||||
if (vmid) {
|
||||
evaluateMetaDataToUse(vm, type, vmid)
|
||||
}
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// all other keys are vmid's, check that the currently rendered meta data
|
||||
// belongs to the current component. If so, remove the metadata and
|
||||
// re-assign the rendered vmid to a new value (if possible)
|
||||
const { _cid, _el, _unwatch, vmid } = displayedMetaData[type][key]
|
||||
if (_cid === componentId) {
|
||||
// only remove vmids which we havent seen
|
||||
if (filter && !filter(displayedMetaData[type][key])) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (_el) {
|
||||
_el.remove()
|
||||
}
|
||||
|
||||
if (_unwatch) {
|
||||
_unwatch()
|
||||
}
|
||||
|
||||
displayedMetaData[type][key] = null
|
||||
evaluateMetaDataToUse(vm, type, vmid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove all the meta data from a component once it gets destroyed
|
||||
export function onComponentDestroyed(vm, componentId) {
|
||||
if (!vm.$options.metaInfo) {
|
||||
return
|
||||
}
|
||||
|
||||
vm._vueMeta.unwatch.forEach(unwatch => unwatch())
|
||||
|
||||
componentId = componentId || vm._vueMeta.id
|
||||
|
||||
for (const type in allMetaData) {
|
||||
if (type === 'title') {
|
||||
const reassignInfo = displayedMetaData[type] && displayedMetaData[type]._cid === componentId
|
||||
const index = allMetaData[type].findIndex(({ _cid }) => _cid === componentId)
|
||||
|
||||
if (index > -1) {
|
||||
allMetaData[type].splice(index, 1)
|
||||
|
||||
if (reassignInfo) {
|
||||
evaluateMetaDataToUse(vm, type)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for (const vmid in allMetaData[type]) {
|
||||
const index = allMetaData[type][vmid].findIndex(({ _cid }) => _cid === componentId)
|
||||
if (index > -1) {
|
||||
allMetaData[type][vmid].splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const type in displayedMetaData) {
|
||||
if (type === 'title') {
|
||||
continue
|
||||
}
|
||||
|
||||
removeMetaDataForType(vm, componentId, type)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
allMetaData,
|
||||
displayedMetaData,
|
||||
onComponentDestroyed,
|
||||
updateMetaInfo,
|
||||
updateMetaInfoForType
|
||||
} from './meta'
|
||||
|
||||
import {
|
||||
getComponentDepth,
|
||||
createWatcher
|
||||
} from './util'
|
||||
|
||||
let vueMetaComponentId = 1
|
||||
let watchersAdded = false
|
||||
|
||||
export function createMixin() {
|
||||
return {
|
||||
created() {
|
||||
// add global meta watchers on root only once for the full page
|
||||
// TODO: maybe create our own vue-meta Vue instance for that?
|
||||
// -> new Vue instance takes a coupe of ms
|
||||
if (this === this.$root) {
|
||||
if (watchersAdded) {
|
||||
return
|
||||
}
|
||||
watchersAdded = true
|
||||
|
||||
// for easily debugging
|
||||
this.$data.metaInfo = displayedMetaData
|
||||
this.$data.allMetaInfo = allMetaData
|
||||
|
||||
for (const type in displayedMetaData) {
|
||||
if (type === 'title') {
|
||||
this.$watch(
|
||||
`$data.metaInfo.${type}`,
|
||||
createWatcher(type),
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
for (const key in displayedMetaData[type]) {
|
||||
this.$watch(
|
||||
`$data.metaInfo.${type}.${key}`,
|
||||
createWatcher(type),
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.$options.metaInfo) {
|
||||
this._vueMeta = {
|
||||
id: vueMetaComponentId++,
|
||||
depth: getComponentDepth(this),
|
||||
unwatch: []
|
||||
}
|
||||
|
||||
if (typeof this.$options.metaInfo === 'function') {
|
||||
this.$watch(
|
||||
this.$options.metaInfo,
|
||||
(newValue) => updateMetaInfo(this, newValue),
|
||||
{ immediate: true }
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
for (const type in this.$options.metaInfo) {
|
||||
const metaInfoForType = this.$options.metaInfo[type]
|
||||
|
||||
if (typeof metaInfoForType === 'function') {
|
||||
this.$watch(
|
||||
metaInfoForType,
|
||||
(newValue) => updateMetaInfoForType(this, newValue, type),
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
updateMetaInfoForType(this, metaInfoForType, type)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
onComponentDestroyed(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
export const internalProps = [
|
||||
'prio'
|
||||
]
|
||||
|
||||
export const sortReverse = (a, b) => (b - a)
|
||||
export const sortMetaData = (a, b) => {
|
||||
if (a.prio === b.prio) {
|
||||
return a._cid < b._cid ? -1 : 1
|
||||
}
|
||||
|
||||
return a.prio > b.prio ? -1 : 1
|
||||
}
|
||||
|
||||
export function createMetaInfoForTypeKey(vm, type, vmid, nestedIndex) {
|
||||
return `${vm._vueMeta.id}-${type}${vmid ? '-'.concat(vmid) : ''}${nestedIndex ? '-'.concat(nestedIndex) : ''}`
|
||||
}
|
||||
|
||||
// the depth of a component is the default meta priority, as in
|
||||
// the deeper the component the more important its meta data
|
||||
export function getComponentDepth(vm) {
|
||||
let depth = 0
|
||||
let parent = vm
|
||||
|
||||
while (parent !== vm.$root) {
|
||||
depth++
|
||||
parent = parent.$parent
|
||||
}
|
||||
|
||||
return depth
|
||||
}
|
||||
|
||||
export function updateElement(meta) {
|
||||
const el = meta._el || document.createElement('meta')
|
||||
|
||||
// set non-private props as attribute
|
||||
Object.keys(meta)
|
||||
.filter(key => key[0] !== '_' && !internalProps.includes(key))
|
||||
.forEach(key => {
|
||||
if (!el.hasAttribute(key) || el.getAttribute(key) != meta[key]) { // eslint-disable-line eqeqeq
|
||||
el.setAttribute(key, meta[key])
|
||||
}
|
||||
})
|
||||
|
||||
if (meta._el) {
|
||||
return
|
||||
}
|
||||
|
||||
meta._el = el
|
||||
document.head.appendChild(el)
|
||||
}
|
||||
|
||||
export function createWatcher(type) {
|
||||
return function metaDataWatcher(newValue) {
|
||||
if (type === 'title') {
|
||||
document.title = (newValue && newValue.content) || ''
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(newValue)) {
|
||||
newValue.forEach(updateElement)
|
||||
return
|
||||
}
|
||||
|
||||
updateElement(newValue)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user