mirror of
https://github.com/tenrok/vue-meta.git
synced 2026-06-15 10:42:26 +03:00
Merge pull request #324 from pimlie/feat-more-refactor
feat: refresh once on navigation & es build
This commit is contained in:
@@ -1,3 +1,13 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env"]
|
||||
"env": {
|
||||
"test": {
|
||||
"presets": [
|
||||
[ "@babel/env", {
|
||||
"targets": {
|
||||
"node": "current"
|
||||
}
|
||||
}]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ package-lock.json
|
||||
|
||||
# built code
|
||||
lib
|
||||
es
|
||||
|
||||
# examples yarn lock
|
||||
examples/yarn.lock
|
||||
|
||||
@@ -6,12 +6,6 @@
|
||||
Manage page meta info in Vue 2.0 components. SSR + Streaming supported. Inspired by <a href="https://github.com/nfl/react-helmet">react-helmet</a>.
|
||||
</h5>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/feross/standard">
|
||||
<img src="https://cdn.rawgit.com/feross/standard/master/badge.svg" alt="Standard - JavaScript Style">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/nuxt/vue-meta/releases/latest"><img src="https://img.shields.io/github/release/nuxt/vue-meta.svg" alt="github release"></a> <a href="http://npmjs.org/package/vue-meta"><img src="https://img.shields.io/npm/v/vue-meta.svg" alt="npm version"></a> <a href="https://circleci.com/gh/nuxt/vue-meta/"><img src="https://badgen.net/circleci/github/nuxt/vue-meta" alt="Build Status"></a> <a href="https://codecov.io/gh/nuxt/vue-meta"><img src="https://codecov.io/gh/nuxt/vue-meta/branch/master/graph/badge.svg" alt="codecov"></a><br>
|
||||
<a href="https://david-dm.org/nuxt/vue-meta"><img src="https://david-dm.org/nuxt/vue-meta/status.svg" alt="dependencies Status"></a> <a href="https://david-dm.org/nuxt/vue-meta?type=dev"><img src="https://david-dm.org/nuxt/vue-meta/dev-status.svg" alt="devDependencies Status"></a><br>
|
||||
@@ -70,6 +64,8 @@
|
||||
- [`__dangerouslyDisableSanitizers` ([String])](#__dangerouslydisablesanitizers-string)
|
||||
- [`__dangerouslyDisableSanitizersByTagID` ({[String]})](#__dangerouslydisablesanitizersbytagid-string)
|
||||
- [`changed` (Function)](#changed-function)
|
||||
- [`refreshOnceOnNavigation` (Boolean)](#refreshonceonnavigation-boolean)
|
||||
- [`afterNavigation` (Function)](#afternavigation-function)
|
||||
- [How `metaInfo` is Resolved](#how-metainfo-is-resolved)
|
||||
- [Lists of Tags](#lists-of-tags)
|
||||
- [Performance](#performance)
|
||||
@@ -658,6 +654,29 @@ Will be called when the client `metaInfo` updates/changes. Receives the followin
|
||||
}
|
||||
```
|
||||
|
||||
#### `refreshOnceOnNavigation` (Boolean)
|
||||
|
||||
Default `false`. If set to `true` then vue-meta will pause updating `metaInfo` during page navigation and only refresh once when navigation has finished. It does this by adding a global beforeEach and afterEach navigation guard on the vue-router instance.
|
||||
|
||||
#### `afterNavigation` (Function)
|
||||
|
||||
Will be called when the client `metaInfo` has changed after navigation occured. Receives the following parameters:
|
||||
- `newInfo` (Object) - The new state of the `metaInfo` object.
|
||||
|
||||
> :warning: This option only works when `refreshOnceOnNavigation: true`. Please see the [vue-router example](./examples/vue-router)
|
||||
|
||||
`this` context is the component instance `afterNavigation` is defined on.
|
||||
|
||||
```js
|
||||
{
|
||||
metaInfo: {
|
||||
afterNavigation (newInfo) {
|
||||
console.log('Meta info update finished after navigation!')
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### How `metaInfo` is Resolved
|
||||
|
||||
You can define a `metaInfo` property on any component in the tree. Child components that have `metaInfo` will recursively merge their `metaInfo` into the parent context, overwriting any duplicate properties. To better illustrate, consider this component heirarchy:
|
||||
|
||||
Generated
-7010
File diff suppressed because it is too large
Load Diff
@@ -19,21 +19,21 @@
|
||||
},
|
||||
"homepage": "https://github.com/nuxt/vue-meta#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.2.2",
|
||||
"@babel/core": "^7.3.3",
|
||||
"@babel/node": "^7.2.2",
|
||||
"@babel/preset-env": "^7.3.1",
|
||||
"babel-loader": "^8.0.5",
|
||||
"cross-env": "^5.2.0",
|
||||
"express": "^4.16.4",
|
||||
"express-urlrewrite": "^1.2.0",
|
||||
"vue": "^2.6.3",
|
||||
"vue-loader": "^15.6.2",
|
||||
"vue": "^2.6.6",
|
||||
"vue-loader": "^15.6.4",
|
||||
"vue-meta": "^1.5.8",
|
||||
"vue-router": "^3.0.2",
|
||||
"vue-template-compiler": "^2.6.3",
|
||||
"vue-template-compiler": "^2.6.6",
|
||||
"vuex": "^3.1.0",
|
||||
"webpack": "^4.26.1",
|
||||
"webpack-dev-server": "^3.1.10",
|
||||
"webpack": "^4.29.5",
|
||||
"webpack-dev-server": "^3.2.0",
|
||||
"webpackbar": "^3.1.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,30 @@ import VueMeta from 'vue-meta'
|
||||
import Router from 'vue-router'
|
||||
|
||||
Vue.use(Router)
|
||||
Vue.use(VueMeta)
|
||||
Vue.use(VueMeta, {
|
||||
refreshOnceOnNavigation: true
|
||||
})
|
||||
|
||||
let metaUpdated = 'no'
|
||||
const ChildComponent = {
|
||||
name: `child-component`,
|
||||
props: ['page'],
|
||||
template: `<h3>You're looking at the <strong>{{ page }}</strong> page</h3>`,
|
||||
template: `<div>
|
||||
<h3>You're looking at the <strong>{{ page }}</strong> page</h3>
|
||||
<p>Has metaInfo been updated? {{ metaUpdated }}</p>
|
||||
</div>`,
|
||||
metaInfo () {
|
||||
return {
|
||||
title: `${this.page} - ${this.date && this.date.toTimeString()}`
|
||||
title: `${this.page} - ${this.date && this.date.toTimeString()}`,
|
||||
afterNavigation() {
|
||||
metaUpdated = 'yes'
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
date: null
|
||||
date: null,
|
||||
metaUpdated
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
@@ -28,7 +38,7 @@ const ChildComponent = {
|
||||
|
||||
// this wrapper function is not a requirement for vue-router,
|
||||
// just a demonstration that render-function style components also work.
|
||||
// See https://github.com/declandewet/vue-meta/issues/9 for more info.
|
||||
// See https://github.com/nuxt/vue-meta/issues/9 for more info.
|
||||
function view (page) {
|
||||
return {
|
||||
name: `section-${page}`,
|
||||
|
||||
+2
-7
@@ -22,8 +22,7 @@ module.exports = {
|
||||
],
|
||||
|
||||
testPathIgnorePatterns: [
|
||||
'node_modules',
|
||||
'old'
|
||||
'node_modules'
|
||||
],
|
||||
|
||||
transformIgnorePatterns: [
|
||||
@@ -39,9 +38,5 @@ module.exports = {
|
||||
'ts',
|
||||
'js',
|
||||
'json'
|
||||
],
|
||||
|
||||
reporters: [
|
||||
'default'
|
||||
].concat(process.env.JEST_JUNIT_OUTPUT ? ['jest-junit'] : [])
|
||||
]
|
||||
}
|
||||
|
||||
+76
-71
@@ -1,73 +1,7 @@
|
||||
{
|
||||
"name": "vue-meta",
|
||||
"description": "Manage page meta info in Vue 2.0 server-rendered apps",
|
||||
"version": "1.5.8",
|
||||
"author": "Declan de Wet <declandewet@me.com>",
|
||||
"bugs": "https://github.com/nuxt/vue-meta/issues",
|
||||
"scripts": {
|
||||
"build": "rimraf lib && rollup -c scripts/rollup.config.js",
|
||||
"codecov": "codecov",
|
||||
"deploy": "npm version",
|
||||
"dev": "cd examples && npm run dev && cd ..",
|
||||
"lint": "eslint src test",
|
||||
"postdeploy": "git push origin master --follow-tags && npm run release",
|
||||
"postversion": "npm run update-cdn && git add . && git commit -m \":ship: CDN update\"",
|
||||
"predeploy": "git checkout master && git pull -r",
|
||||
"prerelease": "npm run build",
|
||||
"preversion": "npm run toc",
|
||||
"release": "npm publish",
|
||||
"test": "jest",
|
||||
"toc": "doctoc README.md --title '# Table of Contents'",
|
||||
"update-cdn": "babel-node scripts/update-cdn.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": "^3.0.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.uniqueid": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.1.6",
|
||||
"@babel/node": "^7.2.2",
|
||||
"@babel/preset-env": "^7.1.6",
|
||||
"@nuxtjs/eslint-config": "^0.0.1",
|
||||
"@vue/server-test-utils": "^1.0.0-beta.29",
|
||||
"@vue/test-utils": "^1.0.0-beta.29",
|
||||
"babel-core": "^7.0.0-bridge",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^24.1.0",
|
||||
"babel-loader": "^8.0.4",
|
||||
"codecov": "^3.1.0",
|
||||
"doctoc": "^1.4.0",
|
||||
"eslint": "^5.13.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-plugin-import": "^2.16.0",
|
||||
"eslint-plugin-jest": "^22.2.2",
|
||||
"eslint-plugin-node": "^8.0.1",
|
||||
"eslint-plugin-promise": "^4.0.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^5.1.0",
|
||||
"jest": "^24.1.0",
|
||||
"jsdom": "^13.2.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"rimraf": "^2.6.2",
|
||||
"rollup": "^1.0.0",
|
||||
"rollup-plugin-buble": "^0.19.4",
|
||||
"rollup-plugin-commonjs": "^9.2.0",
|
||||
"rollup-plugin-json": "^3.1.0",
|
||||
"rollup-plugin-node-resolve": "^4.0.0",
|
||||
"rollup-plugin-terser": "^4.0.4",
|
||||
"update-section": "^0.3.3",
|
||||
"vue": "^2.6.3",
|
||||
"vue-jest": "^3.0.2",
|
||||
"vue-server-renderer": "^2.6.3",
|
||||
"vue-template-compiler": "^2.6.3"
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"types/index.d.ts",
|
||||
"types/vue.d.ts"
|
||||
],
|
||||
"homepage": "https://github.com/nuxt/vue-meta",
|
||||
"description": "Manage page meta info in Vue 2.0 server-rendered apps",
|
||||
"keywords": [
|
||||
"attribute",
|
||||
"google",
|
||||
@@ -82,13 +16,84 @@
|
||||
"universal",
|
||||
"vue"
|
||||
],
|
||||
"homepage": "https://github.com/nuxt/vue-meta",
|
||||
"bugs": "https://github.com/nuxt/vue-meta/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com/nuxt/vue-meta.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": "Declan de Wet <declandewet@me.com>",
|
||||
"files": [
|
||||
"lib",
|
||||
"es",
|
||||
"types/index.d.ts",
|
||||
"types/vue.d.ts"
|
||||
],
|
||||
"main": "lib/vue-meta.common.js",
|
||||
"web": "lib/vue-meta.js",
|
||||
"module": "src/index.js",
|
||||
"module": "es/index.js",
|
||||
"typings": "types/index.d.ts",
|
||||
"repository": {
|
||||
"url": "git@github.com/nuxt/vue-meta.git",
|
||||
"type": "git"
|
||||
"scripts": {
|
||||
"build": "yarn build:other && yarn build:es",
|
||||
"build:es": "rimraf es && babel src --env-name es --out-dir es --ignore 'src/browser.js'",
|
||||
"build:other": "rimraf lib && rollup -c scripts/rollup.config.js",
|
||||
"codecov": "codecov",
|
||||
"predeploy": "git checkout master && git pull -r",
|
||||
"deploy": "npm version",
|
||||
"postdeploy": "git push origin master --follow-tags && npm run release",
|
||||
"dev": "cd examples && npm run dev && cd ..",
|
||||
"lint": "eslint src test",
|
||||
"prerelease": "npm run build",
|
||||
"release": "npm publish",
|
||||
"test": "jest",
|
||||
"toc": "doctoc README.md --title '# Table of Contents'",
|
||||
"update-cdn": "babel-node scripts/update-cdn.js",
|
||||
"preversion": "npm run toc",
|
||||
"postversion": "npm run update-cdn && git add . && git commit -m \":ship: CDN update\""
|
||||
},
|
||||
"dependencies": {
|
||||
"deepmerge": "^3.2.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.uniqueid": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.2.3",
|
||||
"@babel/core": "^7.3.3",
|
||||
"@babel/node": "^7.2.2",
|
||||
"@babel/preset-env": "^7.3.1",
|
||||
"@nuxtjs/eslint-config": "^0.0.1",
|
||||
"@vue/server-test-utils": "^1.0.0-beta.29",
|
||||
"@vue/test-utils": "^1.0.0-beta.29",
|
||||
"babel-core": "^7.0.0-bridge",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^24.1.0",
|
||||
"babel-loader": "^8.0.5",
|
||||
"codecov": "^3.2.0",
|
||||
"doctoc": "^1.4.0",
|
||||
"eslint": "^5.14.1",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-plugin-import": "^2.16.0",
|
||||
"eslint-plugin-jest": "^22.3.0",
|
||||
"eslint-plugin-node": "^8.0.1",
|
||||
"eslint-plugin-promise": "^4.0.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^5.2.2",
|
||||
"esm": "^3.2.5",
|
||||
"jest": "^24.1.0",
|
||||
"jsdom": "^13.2.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"rimraf": "^2.6.3",
|
||||
"rollup": "^1.2.2",
|
||||
"rollup-plugin-buble": "^0.19.6",
|
||||
"rollup-plugin-commonjs": "^9.2.0",
|
||||
"rollup-plugin-json": "^3.1.0",
|
||||
"rollup-plugin-node-resolve": "^4.0.0",
|
||||
"rollup-plugin-terser": "^4.0.4",
|
||||
"update-section": "^0.3.3",
|
||||
"vue": "^2.6.6",
|
||||
"vue-jest": "^3.0.3",
|
||||
"vue-server-renderer": "^2.6.6",
|
||||
"vue-template-compiler": "^2.6.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ export default [{
|
||||
output: {
|
||||
...baseConfig.output,
|
||||
file: pkg.main,
|
||||
intro: 'var window',
|
||||
format: 'cjs'
|
||||
},
|
||||
external: Object.keys(pkg.dependencies)
|
||||
|
||||
+9
-6
@@ -3,25 +3,28 @@ import createMixin from './shared/mixin'
|
||||
import setOptions from './shared/options'
|
||||
import { isUndefined } from './shared/typeof'
|
||||
import $meta from './client/$meta'
|
||||
import { hasMetaInfo } from './shared/hasMetaInfo'
|
||||
|
||||
/**
|
||||
* Plugin install function.
|
||||
* @param {Function} Vue - the Vue constructor.
|
||||
*/
|
||||
function VueMeta(Vue, options = {}) {
|
||||
function install(Vue, options = {}) {
|
||||
options = setOptions(options)
|
||||
|
||||
Vue.prototype.$meta = $meta(options)
|
||||
|
||||
Vue.mixin(createMixin(options))
|
||||
Vue.mixin(createMixin(Vue, options))
|
||||
}
|
||||
|
||||
VueMeta.version = version
|
||||
|
||||
// automatic install
|
||||
if (!isUndefined(window) && !isUndefined(window.Vue)) {
|
||||
/* istanbul ignore next */
|
||||
Vue.use(VueMeta)
|
||||
install(window.Vue)
|
||||
}
|
||||
|
||||
export default VueMeta
|
||||
export default {
|
||||
version,
|
||||
install,
|
||||
hasMetaInfo
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,6 +1,10 @@
|
||||
import { pause, resume } from '../shared/pausing'
|
||||
import refresh from './refresh'
|
||||
|
||||
export default function _$meta(options = {}) {
|
||||
const _refresh = refresh(options)
|
||||
const inject = () => {}
|
||||
|
||||
/**
|
||||
* Returns an injector for server-side rendering.
|
||||
* @this {Object} - the Vue instance (a root component)
|
||||
@@ -8,8 +12,10 @@ export default function _$meta(options = {}) {
|
||||
*/
|
||||
return function $meta() {
|
||||
return {
|
||||
inject: () => {},
|
||||
refresh: refresh(options).bind(this)
|
||||
refresh: _refresh.bind(this),
|
||||
inject,
|
||||
pause: pause.bind(this),
|
||||
resume: resume.bind(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { isUndefined } from '../shared/typeof'
|
||||
import { hasGlobalWindow } from '../shared/window'
|
||||
|
||||
// fallback to timers if rAF not present
|
||||
const stopUpdate = (!isUndefined(window) ? window.cancelAnimationFrame : null) || clearTimeout
|
||||
const startUpdate = (!isUndefined(window) ? window.requestAnimationFrame : null) || (cb => setTimeout(cb, 0))
|
||||
const stopUpdate = (hasGlobalWindow ? window.cancelAnimationFrame : null) || clearTimeout
|
||||
const startUpdate = (hasGlobalWindow ? window.requestAnimationFrame : null) || (cb => setTimeout(cb, 0))
|
||||
|
||||
/**
|
||||
* Performs a batched update. Uses requestAnimationFrame to prevent
|
||||
@@ -15,7 +15,10 @@ const startUpdate = (!isUndefined(window) ? window.requestAnimationFrame : null)
|
||||
* @return {Number} id - a new ID
|
||||
*/
|
||||
export default function batchUpdate(id, callback) {
|
||||
stopUpdate(id)
|
||||
if (id) {
|
||||
stopUpdate(id)
|
||||
}
|
||||
|
||||
return startUpdate(() => {
|
||||
id = null
|
||||
callback()
|
||||
|
||||
+10
-3
@@ -3,6 +3,14 @@ import { isFunction } from '../shared/typeof'
|
||||
import updateClientMetaInfo from './updateClientMetaInfo'
|
||||
|
||||
export default function _refresh(options = {}) {
|
||||
const escapeSequences = [
|
||||
[/&/g, '\u0026'],
|
||||
[/</g, '\u003c'],
|
||||
[/>/g, '\u003e'],
|
||||
[/"/g, '\u0022'],
|
||||
[/'/g, '\u0027']
|
||||
]
|
||||
|
||||
/**
|
||||
* When called, will update the current meta info with new meta info.
|
||||
* Useful when updating meta info as the result of an asynchronous
|
||||
@@ -14,15 +22,14 @@ export default function _refresh(options = {}) {
|
||||
* @return {Object} - new meta info
|
||||
*/
|
||||
return function refresh() {
|
||||
const metaInfo = getMetaInfo(options, this.$root)
|
||||
const metaInfo = getMetaInfo(options, this.$root, escapeSequences)
|
||||
|
||||
const tags = updateClientMetaInfo(options, metaInfo)
|
||||
|
||||
// emit "event" with new info
|
||||
if (tags && isFunction(metaInfo.changed)) {
|
||||
metaInfo.changed.call(this, metaInfo, tags.addedTags, tags.removedTags)
|
||||
}
|
||||
|
||||
return metaInfo
|
||||
return { vm: this, metaInfo, tags }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import batchUpdate from './batchUpdate'
|
||||
|
||||
// store an id to keep track of DOM updates
|
||||
let batchId = null
|
||||
|
||||
export default function triggerUpdate(vm, hookName) {
|
||||
if (vm.$root._vueMeta.initialized && !vm.$root._vueMeta.paused) {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchId = batchUpdate(batchId, () => {
|
||||
vm.$meta().refresh()
|
||||
batchId = null
|
||||
})
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -2,19 +2,22 @@ import { version } from '../package.json'
|
||||
import createMixin from './shared/mixin'
|
||||
import setOptions from './shared/options'
|
||||
import $meta from './server/$meta'
|
||||
import { hasMetaInfo } from './shared/hasMetaInfo'
|
||||
|
||||
/**
|
||||
* Plugin install function.
|
||||
* @param {Function} Vue - the Vue constructor.
|
||||
*/
|
||||
function VueMeta(Vue, options = {}) {
|
||||
function install(Vue, options = {}) {
|
||||
options = setOptions(options)
|
||||
|
||||
Vue.prototype.$meta = $meta(options)
|
||||
|
||||
Vue.mixin(createMixin(options))
|
||||
Vue.mixin(createMixin(Vue, options))
|
||||
}
|
||||
|
||||
VueMeta.version = version
|
||||
|
||||
export default VueMeta
|
||||
export default {
|
||||
version,
|
||||
install,
|
||||
hasMetaInfo
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,7 +1,11 @@
|
||||
import refresh from '../client/refresh'
|
||||
import { pause, resume } from '../shared/pausing'
|
||||
import inject from './inject'
|
||||
|
||||
export default function _$meta(options = {}) {
|
||||
const _refresh = refresh(options)
|
||||
const _inject = inject(options)
|
||||
|
||||
/**
|
||||
* Returns an injector for server-side rendering.
|
||||
* @this {Object} - the Vue instance (a root component)
|
||||
@@ -9,8 +13,10 @@ export default function _$meta(options = {}) {
|
||||
*/
|
||||
return function $meta() {
|
||||
return {
|
||||
inject: inject(options).bind(this),
|
||||
refresh: refresh(options).bind(this)
|
||||
refresh: _refresh.bind(this),
|
||||
inject: _inject.bind(this),
|
||||
pause: pause.bind(this),
|
||||
resume: resume.bind(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,14 @@ import { metaInfoOptionKeys } from '../shared/constants'
|
||||
import generateServerInjector from './generateServerInjector'
|
||||
|
||||
export default function _inject(options = {}) {
|
||||
const escapeSequences = [
|
||||
[/&/g, '&'],
|
||||
[/</g, '<'],
|
||||
[/>/g, '>'],
|
||||
[/"/g, '"'],
|
||||
[/'/g, ''']
|
||||
]
|
||||
|
||||
/**
|
||||
* Converts the state of the meta info object such that each item
|
||||
* can be compiled to a tag string on the server
|
||||
@@ -10,10 +18,9 @@ export default function _inject(options = {}) {
|
||||
* @this {Object} - Vue instance - ideally the root component
|
||||
* @return {Object} - server meta info with `toString` methods
|
||||
*/
|
||||
|
||||
return function inject() {
|
||||
// get meta info with sensible defaults
|
||||
const metaInfo = getMetaInfo(options, this.$root)
|
||||
const metaInfo = getMetaInfo(options, this.$root, escapeSequences)
|
||||
|
||||
// generate server injectors
|
||||
for (const key in metaInfo) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import isArray from './isArray'
|
||||
import { isObject } from './typeof'
|
||||
|
||||
export function ensureIsArray(arg, key) {
|
||||
if (!key || !isObject(arg)) {
|
||||
return isArray(arg) ? arg : []
|
||||
}
|
||||
|
||||
if (!isArray(arg[key])) {
|
||||
arg[key] = []
|
||||
}
|
||||
return arg
|
||||
}
|
||||
|
||||
export function ensuredPush(object, key, el) {
|
||||
ensureIsArray(object, key)
|
||||
|
||||
object[key].push(el)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import uniqueId from 'lodash.uniqueid'
|
||||
import { isUndefined, isFunction, isObject } from '../shared/typeof'
|
||||
import { isUndefined, isFunction, isObject } from './typeof'
|
||||
import uniqBy from './uniqBy'
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,25 +1,9 @@
|
||||
import deepmerge from 'deepmerge'
|
||||
import isPlainObject from 'lodash.isplainobject'
|
||||
import { isUndefined, isFunction, isString } from '../shared/typeof'
|
||||
import { isFunction, isString } from './typeof'
|
||||
import isArray from './isArray'
|
||||
import getComponentOption from './getComponentOption'
|
||||
|
||||
const escapeHTML = str => isUndefined(window)
|
||||
// server-side escape sequence
|
||||
? String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
// client-side escape sequence
|
||||
: String(str)
|
||||
.replace(/&/g, '\u0026')
|
||||
.replace(/</g, '\u003c')
|
||||
.replace(/>/g, '\u003e')
|
||||
.replace(/"/g, '\u0022')
|
||||
.replace(/'/g, '\u0027')
|
||||
|
||||
const applyTemplate = (component, template, chunk) =>
|
||||
isFunction(template) ? template.call(component, chunk) : template.replace(/%s/g, chunk)
|
||||
|
||||
@@ -30,7 +14,7 @@ const applyTemplate = (component, template, chunk) =>
|
||||
* @param {Object} component - the Vue instance to get meta info from
|
||||
* @return {Object} - returned meta info
|
||||
*/
|
||||
export default function getMetaInfo({ keyName, tagIDKeyName, metaTemplateKeyName, contentKeyName } = {}, component) {
|
||||
export default function getMetaInfo({ keyName, tagIDKeyName, metaTemplateKeyName, contentKeyName } = {}, component, escapeSequences = []) {
|
||||
// set some sane defaults
|
||||
const defaultInfo = {
|
||||
title: '',
|
||||
@@ -139,7 +123,7 @@ export default function getMetaInfo({ keyName, tagIDKeyName, metaTemplateKeyName
|
||||
|
||||
if (!isDisabled) {
|
||||
if (isString(val)) {
|
||||
escaped[key] = escapeHTML(val)
|
||||
escaped[key] = escapeSequences.reduce((val, [v, r]) => val.replace(v, r), val)
|
||||
} else if (isPlainObject(val)) {
|
||||
escaped[key] = escape(val)
|
||||
} else if (isArray(val)) {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function hasMetaInfo(vm = this) {
|
||||
return vm && !!vm._vueMeta
|
||||
}
|
||||
+63
-83
@@ -1,28 +1,36 @@
|
||||
import batchUpdate from '../client/batchUpdate'
|
||||
import { isUndefined, isFunction } from '../shared/typeof'
|
||||
|
||||
export default function createMixin(options) {
|
||||
// store an id to keep track of DOM updates
|
||||
let batchID = null
|
||||
import triggerUpdate from '../client/triggerUpdate'
|
||||
import { isUndefined, isFunction } from './typeof'
|
||||
import { ensuredPush } from './ensure'
|
||||
|
||||
export default function createMixin(Vue, options) {
|
||||
// for which Vue lifecycle hooks should the metaInfo be refreshed
|
||||
const updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']
|
||||
|
||||
const triggerUpdate = (vm) => {
|
||||
if (vm.$root._vueMetaInitialized) {
|
||||
// batch potential DOM updates to prevent extraneous re-rendering
|
||||
batchID = batchUpdate(batchID, () => vm.$meta().refresh())
|
||||
}
|
||||
}
|
||||
|
||||
// watch for client side component updates
|
||||
return {
|
||||
beforeCreate() {
|
||||
Object.defineProperty(this, '_hasMetaInfo', {
|
||||
get() {
|
||||
// Show deprecation warning once when devtools enabled
|
||||
if (Vue.config.devtools && !this.$root._vueMeta.hasMetaInfoDeprecationWarningShown) {
|
||||
console.warn('VueMeta DeprecationWarning: _hasMetaInfo has been deprecated and will be removed in a future version. Please import hasMetaInfo and use hasMetaInfo(vm) instead') // eslint-disable-line no-console
|
||||
this.$root._vueMeta.hasMetaInfoDeprecationWarningShown = true
|
||||
}
|
||||
return !!this._vueMeta
|
||||
}
|
||||
})
|
||||
|
||||
// Add a marker to know if it uses metaInfo
|
||||
// _vnode is used to know that it's attached to a real component
|
||||
// useful if we use some mixin to add some meta tags (like nuxt-i18n)
|
||||
if (!isUndefined(this.$options[options.keyName]) && this.$options[options.keyName] !== null) {
|
||||
this._hasMetaInfo = true
|
||||
if (!this.$root._vueMeta) {
|
||||
this.$root._vueMeta = {}
|
||||
}
|
||||
|
||||
if (!this._vueMeta) {
|
||||
this._vueMeta = true
|
||||
}
|
||||
|
||||
// coerce function-style metaInfo to a computed prop so we can observe
|
||||
// it on creation
|
||||
@@ -36,41 +44,62 @@ export default function createMixin(options) {
|
||||
// if computed $metaInfo exists, watch it for updates & trigger a refresh
|
||||
// when it changes (i.e. automatically handle async actions that affect metaInfo)
|
||||
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
|
||||
this.$options.created = this.$options.created || []
|
||||
this.$options.created.push(() => {
|
||||
this.$watch('$metaInfo', () => triggerUpdate(this))
|
||||
ensuredPush(this.$options, 'created', () => {
|
||||
this.$watch('$metaInfo', function () {
|
||||
triggerUpdate(this, 'watcher')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateOnLifecycleHook.forEach((lifecycleHook) => {
|
||||
this.$options[lifecycleHook] = this.$options[lifecycleHook] || []
|
||||
this.$options[lifecycleHook].push(() => triggerUpdate(this))
|
||||
})
|
||||
|
||||
// force an initial refresh on page load and prevent other lifecycleHooks
|
||||
// to triggerUpdate until this initial refresh is finished
|
||||
// this is to make sure that when a page is opened in an inactive tab which
|
||||
// has throttled rAF/timers we still immeditately set the page title
|
||||
if (isUndefined(this.$root._vueMetaInitialized)) {
|
||||
this.$root._vueMetaInitialized = false
|
||||
if (isUndefined(this.$root._vueMeta.initialized)) {
|
||||
this.$root._vueMeta.initialized = this.$isServer
|
||||
|
||||
this.$root.$options.mounted = this.$root.$options.mounted || []
|
||||
this.$root.$options.mounted.push(() => {
|
||||
if (!this.$root._vueMetaInitialized) {
|
||||
this.$nextTick(function () {
|
||||
this.$root.$meta().refresh()
|
||||
this.$root._vueMetaInitialized = true
|
||||
if (!this.$root._vueMeta.initialized) {
|
||||
const $rootMeta = this.$root.$meta()
|
||||
|
||||
ensuredPush(this.$options, 'mounted', () => {
|
||||
if (!this.$root._vueMeta.initialized) {
|
||||
// refresh meta in nextTick so all child components have loaded
|
||||
this.$nextTick(function () {
|
||||
$rootMeta.refresh()
|
||||
this.$root._vueMeta.initialized = true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// add vue-router navigation guard to prevent multiple updates during navigation
|
||||
// only usefull on the client side
|
||||
if (options.refreshOnceOnNavigation && this.$root.$router) {
|
||||
const $router = this.$root.$router
|
||||
$router.beforeEach((to, from, next) => {
|
||||
$rootMeta.pause()
|
||||
next()
|
||||
})
|
||||
|
||||
$router.afterEach(() => {
|
||||
const { vm, metaInfo } = $rootMeta.resume()
|
||||
if (metaInfo && metaInfo.afterNavigation && isFunction(metaInfo.afterNavigation)) {
|
||||
metaInfo.afterNavigation.call(vm, metaInfo)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// do not trigger refresh on the server side
|
||||
if (!this.$isServer) {
|
||||
// no need to add this hooks on server side
|
||||
updateOnLifecycleHook.forEach((lifecycleHook) => {
|
||||
ensuredPush(this.$options, lifecycleHook, () => triggerUpdate(this, lifecycleHook))
|
||||
})
|
||||
|
||||
// re-render meta data when returning from a child component to parent
|
||||
this.$options.destroyed = this.$options.destroyed || []
|
||||
this.$options.destroyed.push(() => {
|
||||
ensuredPush(this.$options, 'destroyed', () => {
|
||||
// Wait that element is hidden before refreshing meta tags (to support animations)
|
||||
const interval = setInterval(() => {
|
||||
if (this.$el && this.$el.offsetParent !== null) {
|
||||
@@ -83,60 +112,11 @@ export default function createMixin(options) {
|
||||
return
|
||||
}
|
||||
|
||||
triggerUpdate(this)
|
||||
triggerUpdate(this, 'destroyed')
|
||||
}, 50)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Not yet removed
|
||||
created() {
|
||||
// if computed $metaInfo exists, watch it for updates & trigger a refresh
|
||||
// when it changes (i.e. automatically handle async actions that affect metaInfo)
|
||||
// credit for this suggestion goes to [Sébastien Chopin](https://github.com/Atinux)
|
||||
if (!this.$isServer && this.$metaInfo) {
|
||||
this.$watch('$metaInfo', () => triggerUpdate(this))
|
||||
}
|
||||
|
||||
},
|
||||
activated() {
|
||||
if (this._hasMetaInfo) {
|
||||
triggerUpdate(this)
|
||||
}
|
||||
},
|
||||
deactivated() {
|
||||
if (this._hasMetaInfo) {
|
||||
triggerUpdate(this)
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (this._hasMetaInfo) {
|
||||
triggerUpdate(this)
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
// do not trigger refresh on the server side
|
||||
if (this.$isServer) {
|
||||
return
|
||||
}
|
||||
|
||||
// re-render meta data when returning from a child component to parent
|
||||
if (this._hasMetaInfo) {
|
||||
// Wait that element is hidden before refreshing meta tags (to support animations)
|
||||
const interval = setInterval(() => {
|
||||
if (this.$el && this.$el.offsetParent !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
clearInterval(interval)
|
||||
|
||||
if (!this.$parent) {
|
||||
return
|
||||
}
|
||||
|
||||
triggerUpdate(this)
|
||||
}, 50)
|
||||
}
|
||||
}/**/
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -1,4 +1,5 @@
|
||||
import { isObject } from '../shared/typeof'
|
||||
import { isObject, isFunction } from './typeof'
|
||||
|
||||
import {
|
||||
keyName,
|
||||
attribute,
|
||||
@@ -28,5 +29,15 @@ export default function setOptions(options) {
|
||||
}
|
||||
}
|
||||
|
||||
if (options.afterNavigation && !isFunction(options.afterNavigation)) {
|
||||
console.warn(`afterNavigation should be a function, received ${typeof options.afterNavigation} instead`) // eslint-disable-line no-console
|
||||
options.afterNavigation = void 0
|
||||
return options
|
||||
}
|
||||
|
||||
if (options.afterNavigation && !options.refreshOnceOnNavigation) {
|
||||
options.refreshOnceOnNavigation = true
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export function pause(refresh = true) {
|
||||
this.$root._vueMeta.paused = true
|
||||
|
||||
return () => resume(refresh)
|
||||
}
|
||||
|
||||
export function resume(refresh = true) {
|
||||
this.$root._vueMeta.paused = false
|
||||
|
||||
if (refresh) {
|
||||
return this.$root.$meta().refresh()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export function isUndefined(arg) {
|
||||
return typeof arg === 'undefined'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { isUndefined } from './typeof'
|
||||
|
||||
export function hasGlobalWindowFn() {
|
||||
try {
|
||||
return !isUndefined(window)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const hasGlobalWindow = hasGlobalWindowFn()
|
||||
@@ -0,0 +1,37 @@
|
||||
import _getMetaInfo from '../src/shared/getMetaInfo'
|
||||
import { defaultOptions, loadVueMetaPlugin } from './utils'
|
||||
|
||||
const getMetaInfo = (component, escapeSequences) => _getMetaInfo(defaultOptions, component, escapeSequences)
|
||||
|
||||
describe('escaping', () => {
|
||||
let Vue
|
||||
|
||||
beforeAll(() => (Vue = loadVueMetaPlugin()))
|
||||
|
||||
test('special chars are escaped unless disabled', () => {
|
||||
const component = new Vue({
|
||||
metaInfo: {
|
||||
title: 'Hello & Goodbye',
|
||||
script: [{ innerHTML: 'Hello & Goodbye' }],
|
||||
__dangerouslyDisableSanitizers: ['script']
|
||||
}
|
||||
})
|
||||
|
||||
expect(getMetaInfo(component, [[/&/g, '&']])).toEqual({
|
||||
title: 'Hello & Goodbye',
|
||||
titleChunk: 'Hello & Goodbye',
|
||||
titleTemplate: '%s',
|
||||
htmlAttrs: {},
|
||||
headAttrs: {},
|
||||
bodyAttrs: {},
|
||||
meta: [],
|
||||
base: [],
|
||||
link: [],
|
||||
style: [],
|
||||
script: [{ innerHTML: 'Hello & Goodbye' }],
|
||||
noscript: [],
|
||||
__dangerouslyDisableSanitizers: ['script'],
|
||||
__dangerouslyDisableSanitizersByTagID: {}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,9 @@
|
||||
import { mount, defaultOptions, VueMetaBrowserPlugin, loadVueMetaPlugin } from './utils'
|
||||
import triggerUpdate from '../src/client/triggerUpdate'
|
||||
import batchUpdate from '../src/client/batchUpdate'
|
||||
import { mount, defaultOptions, vmTick, VueMetaBrowserPlugin, loadVueMetaPlugin } from './utils'
|
||||
|
||||
jest.mock('../src/client/triggerUpdate')
|
||||
jest.mock('../src/client/batchUpdate')
|
||||
jest.mock('../package.json', () => ({
|
||||
version: 'test-version'
|
||||
}))
|
||||
@@ -7,6 +11,7 @@ jest.mock('../package.json', () => ({
|
||||
describe('plugin', () => {
|
||||
let Vue
|
||||
|
||||
beforeEach(() => jest.clearAllMocks())
|
||||
beforeAll(() => (Vue = loadVueMetaPlugin(true)))
|
||||
|
||||
test('is loaded', () => {
|
||||
@@ -35,4 +40,67 @@ describe('plugin', () => {
|
||||
test('plugin sets package version', () => {
|
||||
expect(VueMetaBrowserPlugin.version).toBe('test-version')
|
||||
})
|
||||
|
||||
test('updates can be paused and resumed', async () => {
|
||||
const _triggerUpdate = jest.requireActual('../src/client/triggerUpdate').default
|
||||
const _batchUpdate = jest.requireActual('../src/client/batchUpdate').default
|
||||
|
||||
const triggerUpdateSpy = triggerUpdate.mockImplementation(_triggerUpdate)
|
||||
const batchUpdateSpy = batchUpdate.mockImplementation(_batchUpdate)
|
||||
|
||||
const Component = Vue.component('test-component', {
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.title
|
||||
}
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
template: '<div>Test</div>'
|
||||
})
|
||||
|
||||
let title = 'first title'
|
||||
const wrapper = mount(Component, {
|
||||
localVue: Vue,
|
||||
propsData: {
|
||||
title
|
||||
}
|
||||
})
|
||||
|
||||
// no batchUpdate on initialization
|
||||
expect(wrapper.vm.$root._vueMeta.initialized).toBe(false)
|
||||
expect(wrapper.vm.$root._vueMeta.paused).toBeFalsy()
|
||||
expect(triggerUpdateSpy).toHaveBeenCalledTimes(1)
|
||||
expect(batchUpdateSpy).not.toHaveBeenCalled()
|
||||
jest.clearAllMocks()
|
||||
await vmTick(wrapper.vm)
|
||||
|
||||
title = 'second title'
|
||||
wrapper.setProps({ title })
|
||||
|
||||
// batchUpdate on normal update
|
||||
expect(wrapper.vm.$root._vueMeta.initialized).toBe(true)
|
||||
expect(wrapper.vm.$root._vueMeta.paused).toBeFalsy()
|
||||
expect(triggerUpdateSpy).toHaveBeenCalledTimes(1)
|
||||
expect(batchUpdateSpy).toHaveBeenCalledTimes(1)
|
||||
jest.clearAllMocks()
|
||||
|
||||
wrapper.vm.$meta().pause()
|
||||
title = 'third title'
|
||||
wrapper.setProps({ title })
|
||||
|
||||
// no batchUpdate when paused
|
||||
expect(wrapper.vm.$root._vueMeta.initialized).toBe(true)
|
||||
expect(wrapper.vm.$root._vueMeta.paused).toBe(true)
|
||||
expect(triggerUpdateSpy).toHaveBeenCalledTimes(1)
|
||||
expect(batchUpdateSpy).not.toHaveBeenCalled()
|
||||
jest.clearAllMocks()
|
||||
|
||||
const { metaInfo } = wrapper.vm.$meta().resume()
|
||||
expect(metaInfo.title).toBe(title)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ jest.mock('../package.json', () => ({
|
||||
describe('plugin', () => {
|
||||
let Vue
|
||||
|
||||
beforeEach(() => jest.clearAllMocks())
|
||||
beforeAll(() => (Vue = loadVueMetaPlugin()))
|
||||
|
||||
test('is loaded', () => {
|
||||
@@ -29,4 +30,43 @@ describe('plugin', () => {
|
||||
test('plugin sets package version', () => {
|
||||
expect(VueMetaServerPlugin.version).toBe('test-version')
|
||||
})
|
||||
|
||||
test('prints deprecation warning once when using _hasMetaInfo', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
const Component = Vue.component('test-component', {
|
||||
template: '<div>Test</div>',
|
||||
[defaultOptions.keyName]: {
|
||||
title: 'Hello World'
|
||||
}
|
||||
})
|
||||
|
||||
Vue.config.devtools = true
|
||||
const { vm } = mount(Component, { localVue: Vue })
|
||||
|
||||
expect(vm._hasMetaInfo).toBe(true)
|
||||
expect(warn).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(vm._hasMetaInfo).toBe(true)
|
||||
expect(warn).toHaveBeenCalledTimes(1)
|
||||
warn.mockRestore()
|
||||
})
|
||||
|
||||
test('can use hasMetaInfo export', () => {
|
||||
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
const Component = Vue.component('test-component', {
|
||||
template: '<div>Test</div>',
|
||||
[defaultOptions.keyName]: {
|
||||
title: 'Hello World'
|
||||
}
|
||||
})
|
||||
|
||||
const { vm } = mount(Component, { localVue: Vue })
|
||||
|
||||
expect(VueMetaServerPlugin.hasMetaInfo(vm)).toBe(true)
|
||||
expect(warn).not.toHaveBeenCalled()
|
||||
|
||||
warn.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -41,3 +41,9 @@ export function loadVueMetaPlugin(browser, options, localVue = getVue()) {
|
||||
|
||||
return localVue
|
||||
}
|
||||
|
||||
export const vmTick = (vm) => {
|
||||
return new Promise((resolve) => {
|
||||
vm.$nextTick(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -46,4 +46,6 @@ export interface MetaInfo {
|
||||
__dangerouslyDisableSanitizersByTagID?: string[]
|
||||
|
||||
changed?: <T extends object>(newInfo: T, addedTags: HTMLElement[], removedTags: HTMLElement[]) => void
|
||||
afterNavigation?: <T extends object>(vm: Vue, newInfo: T) => void
|
||||
refreshOnceOnNavigation?: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user