Skip to content

Commit

Permalink
feat: add basic support for multiple apps on one page (#373)
Browse files Browse the repository at this point in the history
* feat: add an appId to tags to support multiple apps

* feat: show warning on calling () on non-vuemeta components

* feat: always use appId ssr for server-generated apps

* test: update tests for appId

* chore: update circleci to only run audit for dependencies

* fix: dont set data-vue-meta attribute on title

it has no use on the client as we use document.title there. Which also means the appId listed would be wrong once the title is updated by another app then the ssr app

* chore: remove unused import

* chore: improve not supported message
  • Loading branch information
pimlie authored Jun 6, 2019
1 parent 34c6ad9 commit 024e7c5
Show file tree
Hide file tree
Showing 23 changed files with 240 additions and 60 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
- attach-project
- run:
name: Security Audit
command: yarn audit
command: yarn audit --groups dependencies

test-unit:
executor: node
Expand Down
1 change: 1 addition & 0 deletions examples/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ <h1>Vue Meta Examples</h1>
<li><a href="basic">Basic</a></li>
<li><a href="basic-render">Basic Render</a></li>
<li><a href="keep-alive">Keep alive</a></li>
<li><a href="multiple-apps">Usage with multiple apps</a></li>
<li><a href="vue-router">Usage with vue-router</a></li>
<li><a href="vuex">Usage with vuex</a></li>
<li><a href="vuex-async">Usage with vuex + async actions</a></li>
Expand Down
82 changes: 82 additions & 0 deletions examples/multiple-apps/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Vue from 'vue'
import VueMeta from 'vue-meta'

Vue.use(VueMeta)

// index.html contains a manual SSR render

const app1 = new Vue({
metaInfo() {
return {
title: 'App 1 title',
bodyAttrs: {
class: 'app-1'
},
meta: [
{ name: 'description', content: 'Hello from app 1', vmid: 'test' },
{ name: 'og:description', content: this.ogContent }
],
script: [
{ innerHTML: 'var appId=1.1', body: true },
{ innerHTML: 'var appId=1.2', vmid: 'app-id-body' },
]
}
},
data() {
return {
ogContent: 'Hello from ssr app'
}
},
template: `
<div id="app1"><h1>App 1</h1></div>
`
})

const app2 = new Vue({
metaInfo: () => ({
title: 'App 2 title',
bodyAttrs: {
class: 'app-2'
},
meta: [
{ name: 'description', content: 'Hello from app 2', vmid: 'test' },
{ name: 'og:description', content: 'Hello from app 2' }
],
script: [
{ innerHTML: 'var appId=2.1', body: true },
{ innerHTML: 'var appId=2.2', vmid: 'app-id-body', body: true },
]
}),
template: `
<div id="app2"><h1>App 2</h1></div>
`
}).$mount('#app2')

app1.$mount('#app1')

const app3 = new Vue({
template: `
<div id="app3"><h1>App 3 (empty metaInfo)</h1></div>
`
}).$mount('#app3')


setTimeout(() => {
console.log('trigger app 1')
app1.$data.ogContent = 'Hello from app 1'
}, 2500)

setTimeout(() => {
console.log('trigger app 2')
app2.$meta().refresh()
}, 5000)

setTimeout(() => {
console.log('trigger app 3')
app3.$meta().refresh()
}, 7500)
setTimeout(() => {
console.log('trigger app 4')
const App = Vue.extend({ template: `<div>app 4</div>` })
const app4 = new App().$mount()
}, 10000)
17 changes: 17 additions & 0 deletions examples/multiple-apps/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html data-vue-meta-server-rendered>
<link rel="stylesheet" href="/global.css">
<title data-vue-meta="ssr">App 1 title</title>
<meta data-vue-meta="ssr" name="og:description" content="Hello from app 1">
</html>
<body>
<a href="/">&larr; Examples index</a>
<div id="app1" data-server-rendered="true"><h1>App 1</h1></div>
<hr />
<div id="app2"></div>
<hr />
<div id="app3"></div>
<script src="/__build__/multiple-apps.js"></script>
<script data-vue-meta="ssr" data-body="true">var appId=1.1</script>
</body>
</html>
36 changes: 18 additions & 18 deletions examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"scripts": {
"dev": "cross-env NODE_ENV=development babel-node server.js",
"start": "babel-node server.js",
"ssr": "babel-node ssr"
"ssr": "cross-env NODE_ENV=development babel-node ssr"
},
"repository": {
"type": "git",
Expand All @@ -20,27 +20,27 @@
},
"homepage": "https://github.com/nuxt/vue-meta#readme",
"devDependencies": {
"@babel/core": "^7.3.3",
"@babel/node": "^7.2.2",
"@babel/core": "^7.4.5",
"@babel/node": "^7.4.5",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/preset-env": "^7.3.1",
"babel-loader": "^8.0.5",
"@babel/preset-env": "^7.4.5",
"babel-loader": "^8.0.6",
"babel-plugin-dynamic-import-node": "^2.2.0",
"consola": "^2.5.6",
"consola": "^2.7.1",
"cross-env": "^5.2.0",
"express": "^4.16.4",
"express": "^4.17.1",
"express-urlrewrite": "^1.2.0",
"fs-extra": "^7.0.1",
"fs-extra": "^8.0.1",
"lodash": "^4.17.11",
"vue": "^2.6.6",
"vue-loader": "^15.6.4",
"vue-meta": "^1.5.8",
"vue-router": "^3.0.2",
"vue-server-renderer": "^2.6.8",
"vue-template-compiler": "^2.6.6",
"vuex": "^3.1.0",
"webpack": "^4.29.5",
"webpack-dev-server": "^3.2.0",
"webpackbar": "^3.1.5"
"vue": "^2.6.10",
"vue-loader": "^15.7.0",
"vue-meta": "^1.6.0",
"vue-router": "^3.0.6",
"vue-server-renderer": "^2.6.10",
"vue-template-compiler": "^2.6.10",
"vuex": "^3.1.1",
"webpack": "^4.32.2",
"webpack-dev-server": "^3.5.0",
"webpackbar": "^3.2.0"
}
}
1 change: 0 additions & 1 deletion examples/ssr/app.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Vue from 'vue'
// import VueMeta from 'vue-meta'

export default async function createApp() {
// the dynamic import is for this example only
Expand Down
11 changes: 11 additions & 0 deletions src/client/$meta.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { showWarningNotSupported } from '../shared/constants'
import { getOptions } from '../shared/options'
import { pause, resume } from '../shared/pausing'
import refresh from './refresh'
Expand All @@ -12,6 +13,16 @@ export default function _$meta(options = {}) {
* @return {Object} - injector
*/
return function $meta() {
if (!this.$root._vueMeta) {
return {
getOptions: showWarningNotSupported,
refresh: showWarningNotSupported,
inject: showWarningNotSupported,
pause: showWarningNotSupported,
resume: showWarningNotSupported
}
}

return {
getOptions: () => getOptions(options),
refresh: _refresh.bind(this),
Expand Down
3 changes: 2 additions & 1 deletion src/client/refresh.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export default function _refresh(options = {}) {
return function refresh() {
const metaInfo = getMetaInfo(options, this.$root, clientSequences)

const tags = updateClientMetaInfo(options, metaInfo)
const appId = this.$root._vueMeta.appId
const tags = updateClientMetaInfo(appId, options, metaInfo)
// emit "event" with new info
if (tags && isFunction(metaInfo.changed)) {
metaInfo.changed(metaInfo, tags.addedTags, tags.removedTags)
Expand Down
5 changes: 3 additions & 2 deletions src/client/updateClientMetaInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function getTag(tags, tag) {
*
* @param {Object} newInfo - the meta info to update to
*/
export default function updateClientMetaInfo(options = {}, newInfo) {
export default function updateClientMetaInfo(appId, options = {}, newInfo) {
const { ssrAttribute } = options

// only cache tags for current update
Expand All @@ -25,7 +25,7 @@ export default function updateClientMetaInfo(options = {}, newInfo) {
const htmlTag = getTag(tags, 'html')

// if this is a server render, then dont update
if (htmlTag.hasAttribute(ssrAttribute)) {
if (appId === 'ssr' && htmlTag.hasAttribute(ssrAttribute)) {
// remove the server render attribute so we can update on (next) changes
htmlTag.removeAttribute(ssrAttribute)
return false
Expand Down Expand Up @@ -59,6 +59,7 @@ export default function updateClientMetaInfo(options = {}, newInfo) {
}

const { oldTags, newTags } = updateTag(
appId,
options,
type,
newInfo[type],
Expand Down
9 changes: 5 additions & 4 deletions src/client/updaters/tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { toArray, includes } from '../../utils/array'
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
* @return {Object} - a representation of what tags changed
*/
export default function updateTag({ attribute, tagIDKeyName } = {}, type, tags, headTag, bodyTag) {
const oldHeadTags = toArray(headTag.querySelectorAll(`${type}[${attribute}]`))
const oldBodyTags = toArray(bodyTag.querySelectorAll(`${type}[${attribute}][data-body="true"]`))
export default function updateTag(appId, { attribute, tagIDKeyName } = {}, type, tags, headTag, bodyTag) {
const oldHeadTags = toArray(headTag.querySelectorAll(`${type}[${attribute}="${appId}"], ${type}[data-${tagIDKeyName}]`))
const oldBodyTags = toArray(bodyTag.querySelectorAll(`${type}[${attribute}="${appId}"][data-body="true"], ${type}[data-${tagIDKeyName}][data-body="true"]`))
const dataAttributes = [tagIDKeyName, 'body']
const newTags = []

Expand All @@ -31,7 +31,8 @@ export default function updateTag({ attribute, tagIDKeyName } = {}, type, tags,
if (tags.length) {
tags.forEach((tag) => {
const newElement = document.createElement(type)
newElement.setAttribute(attribute, 'true')

newElement.setAttribute(attribute, appId)

const oldTags = tag.body !== true ? oldHeadTags : oldBodyTags

Expand Down
11 changes: 11 additions & 0 deletions src/server/$meta.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { showWarningNotSupported } from '../shared/constants'
import { getOptions } from '../shared/options'
import { pause, resume } from '../shared/pausing'
import refresh from '../client/refresh'
Expand All @@ -13,6 +14,16 @@ export default function _$meta(options = {}) {
* @return {Object} - injector
*/
return function $meta() {
if (!this.$root._vueMeta) {
return {
getOptions: showWarningNotSupported,
refresh: showWarningNotSupported,
inject: showWarningNotSupported,
pause: showWarningNotSupported,
resume: showWarningNotSupported
}
}

return {
getOptions: () => getOptions(options),
refresh: _refresh.bind(this),
Expand Down
6 changes: 3 additions & 3 deletions src/server/generateServerInjector.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { titleGenerator, attributeGenerator, tagGenerator } from './generators'
* @return {Object} - the new injector
*/

export default function generateServerInjector(options, type, data) {
export default function generateServerInjector(appId, options, type, data) {
if (type === 'title') {
return titleGenerator(options, type, data)
return titleGenerator(appId, options, type, data)
}

if (metaInfoAttributeKeys.includes(type)) {
return attributeGenerator(options, type, data)
}

return tagGenerator(options, type, data)
return tagGenerator(appId, options, type, data)
}
4 changes: 2 additions & 2 deletions src/server/generators/tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { isUndefined } from '../../utils/is-type'
* @param {(Array<Object>|Object)} tags - an array of tag objects or a single object in case of base
* @return {Object} - the tag generator
*/
export default function tagGenerator({ attribute, tagIDKeyName } = {}, type, tags) {
export default function tagGenerator(appId, { attribute, tagIDKeyName } = {}, type, tags) {
return {
text({ body = false } = {}) {
// build a string containing all tags of this type
Expand Down Expand Up @@ -47,7 +47,7 @@ export default function tagGenerator({ attribute, tagIDKeyName } = {}, type, tag
// generate tag exactly without any other redundant attribute
const observeTag = tag.once
? ''
: `${attribute}="true"`
: `${attribute}="${appId}"`

// these tags have no end tag
const hasEndTag = !tagsWithoutEndTag.includes(type)
Expand Down
4 changes: 2 additions & 2 deletions src/server/generators/title.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
* @param {String} data - the title text
* @return {Object} - the title generator
*/
export default function titleGenerator({ attribute } = {}, type, data) {
export default function titleGenerator(appId, { attribute } = {}, type, data) {
return {
text() {
return `<${type} ${attribute}="true">${data}</${type}>`
return `<${type}>${data}</${type}>`
}
}
}
2 changes: 1 addition & 1 deletion src/server/inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function _inject(options = {}) {
// generate server injectors
for (const key in metaInfo) {
if (!metaInfoOptionKeys.includes(key) && metaInfo.hasOwnProperty(key)) {
metaInfo[key] = generateServerInjector(options, key, metaInfo[key])
metaInfo[key] = generateServerInjector('ssr', options, key, metaInfo[key])
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/shared/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,6 @@ export const booleanHtmlAttributes = [
'typemustmatch',
'visible'
]

// eslint-disable-next-line no-console
export const showWarningNotSupported = () => console.warn('This vue app/component has no vue-meta configuration')
13 changes: 12 additions & 1 deletion src/shared/mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { ensuredPush } from '../utils/ensure'
import { hasMetaInfo } from './meta-helpers'
import { addNavGuards } from './nav-guards'

let appId = 1

export default function createMixin(Vue, options) {
// for which Vue lifecycle hooks should the metaInfo be refreshed
const updateOnLifecycleHook = ['activated', 'deactivated', 'beforeMount']
Expand All @@ -27,7 +29,8 @@ export default function createMixin(Vue, options) {
// 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) {
if (!this.$root._vueMeta) {
this.$root._vueMeta = {}
this.$root._vueMeta = { appId }
appId++
}

// to speed up updates we keep track of branches which have a component with vue-meta info defined
Expand Down Expand Up @@ -72,6 +75,14 @@ export default function createMixin(Vue, options) {
this.$root._vueMeta.initialized = this.$isServer

if (!this.$root._vueMeta.initialized) {
ensuredPush(this.$options, 'beforeMount', () => {
// if this Vue-app was server rendered, set the appId to 'ssr'
// only one SSR app per page is supported
if (this.$root.$el && this.$root.$el.hasAttribute('data-server-rendered')) {
this.$root._vueMeta.appId = 'ssr'
}
})

// we use the mounted hook here as on page load
ensuredPush(this.$options, 'mounted', () => {
if (!this.$root._vueMeta.initialized) {
Expand Down
6 changes: 4 additions & 2 deletions test/unit/components.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,17 @@ describe('client', () => {
const wrapper = mount(HelloWorld, { localVue: Vue })

const metaInfo = wrapper.vm.$meta().inject()
expect(metaInfo.title.text()).toEqual('<title data-vue-meta="true">Hello World</title>')
expect(metaInfo.title.text()).toEqual('<title>Hello World</title>')
})

test('doesnt update when ssr attribute is set', () => {
html.setAttribute(defaultOptions.ssrAttribute, 'true')
const wrapper = mount(HelloWorld, { localVue: Vue })

const { tags } = wrapper.vm.$meta().refresh()
expect(tags).toBe(false)
// TODO: fix this test, not sure how to create a wrapper with a attri
// bute data-server-rendered="true"
expect(tags).not.toBe(false)
})

test('changed function is called', async () => {
Expand Down
Loading

0 comments on commit 024e7c5

Please sign in to comment.