2
0
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:
pimlie
2019-10-25 15:27:32 +02:00
parent 06295bf9fb
commit 9f13ba2afd
4 changed files with 546 additions and 16 deletions
+71 -16
View File
@@ -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))
+319
View File
@@ -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)
}
}
}
+66
View File
@@ -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)
}
}