mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-24 01:30:33 +03:00
refactor code to be more modular & performant
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@ indent_style = space
|
|||||||
indent_size = 2
|
indent_size = 2
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="http://imgh.us/Group_5.svg" alt="vue-meta">
|
<img src="http://imgur.com/258WtHI.png" alt="vue-meta">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h5 align="center">
|
<h5 align="center">
|
||||||
|
|||||||
+5
-1
@@ -85,7 +85,11 @@
|
|||||||
"define",
|
"define",
|
||||||
"describe",
|
"describe",
|
||||||
"it",
|
"it",
|
||||||
"expect"
|
"expect",
|
||||||
|
"before",
|
||||||
|
"beforeEach",
|
||||||
|
"after",
|
||||||
|
"afterEach"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import inject from './inject'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an injector for server-side rendering.
|
||||||
|
* @this {Object} - the Vue instance (a root component)
|
||||||
|
* @return {Object} - injector
|
||||||
|
*/
|
||||||
|
export default function $meta () {
|
||||||
|
// bind inject method to this component
|
||||||
|
return { inject: inject.bind(this) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { VUE_META_ATTRIBUTE } from './constants'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a meta info property to one that can be stringified on the server
|
||||||
|
*
|
||||||
|
* @param {String} type - the type of data to convert
|
||||||
|
* @param {(String|Object|Array<Object>)} data - the data value
|
||||||
|
* @return {Object} - the new injector
|
||||||
|
*/
|
||||||
|
export default function generateServerInjector (type, data) {
|
||||||
|
console.log('server injector called for', type, 'with', data)
|
||||||
|
switch (type) {
|
||||||
|
case 'title':
|
||||||
|
return {
|
||||||
|
toString: () => `<${type} ${VUE_META_ATTRIBUTE}="true">${data}</${type}>`
|
||||||
|
}
|
||||||
|
case 'htmlAttrs': {
|
||||||
|
return {
|
||||||
|
toString () {
|
||||||
|
let attributeStr = ''
|
||||||
|
let watchedAttrs = []
|
||||||
|
for (let attr in data) {
|
||||||
|
if (data.hasOwnProperty(attr)) {
|
||||||
|
watchedAttrs.push(attr)
|
||||||
|
attributeStr += `${typeof data[attr] !== 'undefined' ? `${attr}="${data[attr]}"` : attr} `
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attributeStr += `${VUE_META_ATTRIBUTE}="${watchedAttrs.join(',')}"`
|
||||||
|
return attributeStr.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import deepmerge from 'deepmerge'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the `opts.option` $option value of the given `opts.component`.
|
||||||
|
* If methods are encountered, they will be bound to the component context.
|
||||||
|
* If `opts.deep` is true, will recursively merge all child component
|
||||||
|
* `opts.option` $option values into the returned result.
|
||||||
|
*
|
||||||
|
* @param {Object} opts - options
|
||||||
|
* @param {Object} opts.component - Vue component to fetch option data from
|
||||||
|
* @param {String} opts.option - what option to look for
|
||||||
|
* @param {Boolean} opts.deep - look for data in child components as well?
|
||||||
|
* @param {Object} [result={}] - result so far
|
||||||
|
* @return {Object} - final aggregated result
|
||||||
|
*/
|
||||||
|
export default function getComponentOption (opts, result = {}) {
|
||||||
|
const { component, option, deep } = opts
|
||||||
|
const { $options } = component
|
||||||
|
|
||||||
|
// only collect option data if it exists
|
||||||
|
if ($options[option]) {
|
||||||
|
const data = $options[option]
|
||||||
|
|
||||||
|
// TODO: check data is plain object, throw if not
|
||||||
|
|
||||||
|
// bind context of option methods (if any) to this component
|
||||||
|
for (const key in data) {
|
||||||
|
if (data.hasOwnProperty(key)) {
|
||||||
|
const value = data[key]
|
||||||
|
if (typeof value === 'function') {
|
||||||
|
data[key] = value.bind(component)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge with existing options
|
||||||
|
result = deepmerge(result, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect & aggregate child options if deep = true
|
||||||
|
if (deep) {
|
||||||
|
const { $children } = component
|
||||||
|
for (let i = 0, len = $children.length; i < len; i++) {
|
||||||
|
const component = $children[i]
|
||||||
|
result = getComponentOption({ option, deep, component }, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import getComponentOption from './getComponentOption'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the correct meta info for the given component
|
||||||
|
* (child components will overwrite parent meta info)
|
||||||
|
*
|
||||||
|
* @param {Object} component - the Vue instance to get meta info from
|
||||||
|
* @return {Object} - returned meta info
|
||||||
|
*/
|
||||||
|
export default function getMetaInfo (component) {
|
||||||
|
// collect & aggregate all metaInfo $options
|
||||||
|
const info = getComponentOption({ component, option: 'metaInfo', deep: true })
|
||||||
|
|
||||||
|
// if any info options are a function, coerce them to the result of a call
|
||||||
|
for (let key in info) {
|
||||||
|
if (info.hasOwnProperty(key)) {
|
||||||
|
const value = info[key]
|
||||||
|
if (typeof value === 'function') {
|
||||||
|
info[key] = value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// backup the title chunk
|
||||||
|
if (info.title) {
|
||||||
|
info.titleChunk = info.title
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace title with populated template
|
||||||
|
if (info.titleTemplate && info.titleChunk) {
|
||||||
|
info.title = info.titleTemplate.replace(/%s/g, info.titleChunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
+19
-205
@@ -1,210 +1,24 @@
|
|||||||
import deepMerge from 'deepmerge'
|
import $meta from './$meta'
|
||||||
import { VUE_META_ATTRIBUTE } from './constants'
|
import getMetaInfo from './getMetaInfo'
|
||||||
|
import updateClientMetaInfo from './updateClientMetaInfo'
|
||||||
|
|
||||||
// initialize vue-meta
|
// automatic install
|
||||||
const VueMeta = {}
|
|
||||||
|
|
||||||
// initialize manager
|
|
||||||
const _manager = {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers the plugin with Vue.js
|
|
||||||
* Pass it like so: Vue.use(VueMeta)
|
|
||||||
* @param {Function} Vue - the Vue constructor
|
|
||||||
*/
|
|
||||||
VueMeta.install = function install (Vue) {
|
|
||||||
// if we've already installed, don't do anything
|
|
||||||
if (VueMeta.install.installed) return
|
|
||||||
|
|
||||||
// set installation inspection flag
|
|
||||||
VueMeta.install.installed = true
|
|
||||||
|
|
||||||
// listen for when components mount - when they do,
|
|
||||||
// update the meta info & the DOM
|
|
||||||
Vue.mixin({
|
|
||||||
mounted () {
|
|
||||||
this.$root.$meta().updateMetaInfo()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* returns a cached manager API for use on the server
|
|
||||||
* @return {Object} - manager (The programmatic API for this module)
|
|
||||||
*/
|
|
||||||
Vue.prototype.$meta = function $meta () {
|
|
||||||
_manager.getMetaInfo = _manager.getMetaInfo || Vue.util.bind(getMetaInfo, this)
|
|
||||||
_manager.updateMetaInfo = _manager.updateMetaInfo || updateMetaInfo
|
|
||||||
_manager.inject = _manager.inject || inject
|
|
||||||
return _manager
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts the state of the meta info object such that each item
|
|
||||||
* can be compiled to a tag string on the server
|
|
||||||
* @return {Object} - server meta info with `toString` methods
|
|
||||||
*/
|
|
||||||
function inject () {
|
|
||||||
const info = this.getMetaInfo()
|
|
||||||
const serverMetaInfo = {}
|
|
||||||
for (let key in info) {
|
|
||||||
if (info.hasOwnProperty(key)) {
|
|
||||||
serverMetaInfo[key] = generateServerInjector(key, info[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return serverMetaInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a meta info property to one that can be stringified on the server
|
|
||||||
* @param {String} type - the type of data to convert
|
|
||||||
* @param {(String|Object|Array<Object>)} data - the data value
|
|
||||||
* @return {Object} - the new injector
|
|
||||||
*/
|
|
||||||
function generateServerInjector (type, data) {
|
|
||||||
switch (type) {
|
|
||||||
case 'title':
|
|
||||||
return {
|
|
||||||
toString: () => `<${type} ${VUE_META_ATTRIBUTE}="true">${data}</${type}>`
|
|
||||||
}
|
|
||||||
case 'htmlAttrs': {
|
|
||||||
return {
|
|
||||||
toString () {
|
|
||||||
let attributeStr = ''
|
|
||||||
let watchedAttrs = []
|
|
||||||
for (let attr in data) {
|
|
||||||
if (data.hasOwnProperty(attr)) {
|
|
||||||
watchedAttrs.push(attr)
|
|
||||||
attributeStr += `${typeof data[attr] !== 'undefined' ? `${attr}="${data[attr]}"` : attr} `
|
|
||||||
}
|
|
||||||
}
|
|
||||||
attributeStr += `${VUE_META_ATTRIBUTE}="${watchedAttrs.join(',')}"`
|
|
||||||
return attributeStr.trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates meta info and renders it to the DOM
|
|
||||||
*/
|
|
||||||
function updateMetaInfo () {
|
|
||||||
const newMeta = this.getMetaInfo()
|
|
||||||
if (newMeta.title) {
|
|
||||||
updateTitle(newMeta.title)
|
|
||||||
}
|
|
||||||
if (newMeta.htmlAttrs) {
|
|
||||||
updateHtmlAttrs(newMeta.htmlAttrs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches corresponding meta info for the current component state
|
|
||||||
* @return {Object} - all the meta info for currently matched components
|
|
||||||
*/
|
|
||||||
function getMetaInfo () {
|
|
||||||
const info = getMetaInfoDefinition(Vue, this)
|
|
||||||
if (info.titleTemplate) {
|
|
||||||
info.title = info.titleTemplate.replace('%s', info.title)
|
|
||||||
}
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively traverses each component, checking for a `metaInfo`
|
|
||||||
* option. It then merges all these options into one object, giving
|
|
||||||
* higher priority to deeply nested components.
|
|
||||||
*
|
|
||||||
* NOTE: This function uses Vue.prototype.$children, the results of which
|
|
||||||
* are not gauranted to be in order. For this reason, try to avoid
|
|
||||||
* using the same `metaInfo` property in sibling components.
|
|
||||||
*
|
|
||||||
* @param {Function} Vue - the Vue constructor
|
|
||||||
* @param {Object} $instance - the current instance
|
|
||||||
* @param {Object} [metaInfo={}] - the merged options
|
|
||||||
* @return {Object} metaInfo - the merged options
|
|
||||||
*/
|
|
||||||
function getMetaInfoDefinition (Vue, $instance, metaInfo = {
|
|
||||||
title: '',
|
|
||||||
htmlAttrs: {}
|
|
||||||
}) {
|
|
||||||
// if current instance has a metaInfo option...
|
|
||||||
if ($instance.$options.metaInfo) {
|
|
||||||
const componentMetaInfo = $instance.$options.metaInfo
|
|
||||||
|
|
||||||
// ...convert all function type keys to raw data
|
|
||||||
// (this allows meta info to be inferred from props & data)...
|
|
||||||
for (let key in componentMetaInfo) {
|
|
||||||
if (componentMetaInfo.hasOwnProperty(key)) {
|
|
||||||
const val = componentMetaInfo[key]
|
|
||||||
if (typeof val === 'function') {
|
|
||||||
componentMetaInfo[key] = val.call($instance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...then merge the data into metaInfo
|
|
||||||
metaInfo = deepMerge(metaInfo, componentMetaInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if any children also have a metaInfo option, if so, merge
|
|
||||||
// them into existing data
|
|
||||||
const len = $instance.$children.length
|
|
||||||
if (len) {
|
|
||||||
for (let i = 0; i < len; i++) {
|
|
||||||
metaInfo = getMetaInfoDefinition(Vue, $instance.$children[i], metaInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// meta info is ready for consumption
|
|
||||||
return metaInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* updates the document title
|
|
||||||
* @param {String} title - the new title of the document
|
|
||||||
*/
|
|
||||||
function updateTitle (title) {
|
|
||||||
document.title = title || document.title
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* updates the document's html tag attributes
|
|
||||||
* @param {Object} attrs - the new document html attributes
|
|
||||||
*/
|
|
||||||
function updateHtmlAttrs (attrs) {
|
|
||||||
const tag = document.getElementsByTagName('html')[0]
|
|
||||||
const vueMetaAttrString = tag.getAttribute(VUE_META_ATTRIBUTE)
|
|
||||||
const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []
|
|
||||||
const toRemove = [].concat(vueMetaAttrs)
|
|
||||||
for (let attr in attrs) {
|
|
||||||
if (attrs.hasOwnProperty(attr)) {
|
|
||||||
const val = attrs[attr] || ''
|
|
||||||
tag.setAttribute(attr, val)
|
|
||||||
if (vueMetaAttrs.indexOf(attr) === -1) {
|
|
||||||
vueMetaAttrs.push(attr)
|
|
||||||
}
|
|
||||||
const saveIndex = toRemove.indexOf(attr)
|
|
||||||
if (saveIndex !== -1) {
|
|
||||||
toRemove.splice(saveIndex, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let i = toRemove.length - 1
|
|
||||||
for (; i >= 0; i--) {
|
|
||||||
tag.removeAttribute(toRemove[i])
|
|
||||||
}
|
|
||||||
if (vueMetaAttrs.length === toRemove.length) {
|
|
||||||
tag.removeAttribute(VUE_META_ATTRIBUTE)
|
|
||||||
} else {
|
|
||||||
tag.setAttribute(VUE_META_ATTRIBUTE, vueMetaAttrs.join(','))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// automatic installation when global context
|
|
||||||
if (typeof Vue !== 'undefined') {
|
if (typeof Vue !== 'undefined') {
|
||||||
Vue.use(VueMeta)
|
Vue.use(VueMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default VueMeta
|
/**
|
||||||
|
* Plugin install function.
|
||||||
|
* @param {Function} Vue - the Vue constructor.
|
||||||
|
*/
|
||||||
|
export default function VueMeta (Vue) {
|
||||||
|
// bind the $meta method to this component instance
|
||||||
|
Vue.prototype.$meta = $meta
|
||||||
|
|
||||||
|
// watch for client side updates
|
||||||
|
Vue.mixin({
|
||||||
|
mounted () {
|
||||||
|
updateClientMetaInfo(getMetaInfo(this.$root))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import getMetaInfo from './getMetaInfo'
|
||||||
|
import generateServerInjector from './generateServerInjector'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the state of the meta info object such that each item
|
||||||
|
* can be compiled to a tag string on the server
|
||||||
|
*
|
||||||
|
* @this {Object} - Vue instance - ideally the root component
|
||||||
|
* @return {Object} - server meta info with `toString` methods
|
||||||
|
*/
|
||||||
|
export default function inject () {
|
||||||
|
const info = getMetaInfo(this.$root)
|
||||||
|
const serverMetaInfo = {}
|
||||||
|
for (let key in info) {
|
||||||
|
if (info.hasOwnProperty(key) && key !== 'titleTemplate') {
|
||||||
|
serverMetaInfo[key] = generateServerInjector(key, info[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serverMetaInfo
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import updateTitleTag from './updateTitleTag'
|
||||||
|
import updateHtmlTagAttributes from './updateHtmlTagAttributes'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs client-side updates when new meta info is received
|
||||||
|
*
|
||||||
|
* @param {Object} newInfo - the meta info to update to
|
||||||
|
*/
|
||||||
|
export default function updateClientMetaInfo (newInfo) {
|
||||||
|
if (newInfo.title) {
|
||||||
|
updateTitleTag(newInfo.title)
|
||||||
|
}
|
||||||
|
if (newInfo.htmlAttrs) {
|
||||||
|
updateHtmlTagAttributes(newInfo.htmlAttrs)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { VUE_META_ATTRIBUTE } from './constants'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* updates the document's html tag attributes
|
||||||
|
*
|
||||||
|
* @param {Object} attrs - the new document html attributes
|
||||||
|
*/
|
||||||
|
export default function updateHtmlTagAttributes (attrs) {
|
||||||
|
const tag = document.getElementsByTagName('html')[0]
|
||||||
|
const vueMetaAttrString = tag.getAttribute(VUE_META_ATTRIBUTE)
|
||||||
|
const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []
|
||||||
|
const toRemove = [].concat(vueMetaAttrs)
|
||||||
|
for (let attr in attrs) {
|
||||||
|
if (attrs.hasOwnProperty(attr)) {
|
||||||
|
const val = attrs[attr] || ''
|
||||||
|
tag.setAttribute(attr, val)
|
||||||
|
if (vueMetaAttrs.indexOf(attr) === -1) {
|
||||||
|
vueMetaAttrs.push(attr)
|
||||||
|
}
|
||||||
|
const saveIndex = toRemove.indexOf(attr)
|
||||||
|
if (saveIndex !== -1) {
|
||||||
|
toRemove.splice(saveIndex, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let i = toRemove.length - 1
|
||||||
|
for (; i >= 0; i--) {
|
||||||
|
tag.removeAttribute(toRemove[i])
|
||||||
|
}
|
||||||
|
if (vueMetaAttrs.length === toRemove.length) {
|
||||||
|
tag.removeAttribute(VUE_META_ATTRIBUTE)
|
||||||
|
} else {
|
||||||
|
tag.setAttribute(VUE_META_ATTRIBUTE, vueMetaAttrs.join(','))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* updates the document title
|
||||||
|
*
|
||||||
|
* @param {String} title - the new title of the document
|
||||||
|
*/
|
||||||
|
export default function updateTitleTag (title = document.title) {
|
||||||
|
document.title = title
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user