Skip to content

Commit d9b0ab2

Browse files
committed
feat: add support for setting attributes from multiple apps
chore: improve build size
1 parent 0ab76ee commit d9b0ab2

16 files changed

+250
-76
lines changed

examples/ssr/App.js

+10
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export default function createApp () {
6969
return {
7070
title: 'Boring Title',
7171
htmlAttrs: { amp: true },
72+
bodyAttrs: { class: 'main-app' },
7273
meta: [
7374
{
7475
skip: this.count < 1,
@@ -116,6 +117,14 @@ export default function createApp () {
116117
users: process.server ? [] : window.users
117118
}
118119
},
120+
mounted() {
121+
const { set, remove } = this.$meta().addApp('client-only')
122+
set({
123+
bodyAttrs: { class: 'client-only' }
124+
})
125+
126+
setTimeout(() => remove(), 3000)
127+
},
119128
methods: {
120129
loadCallback () {
121130
this.count++
@@ -140,6 +149,7 @@ export default function createApp () {
140149

141150
const { set } = app.$meta().addApp('custom')
142151
set({
152+
bodyAttrs: { class: 'custom-app' },
143153
meta: [{ charset: 'utf-8' }]
144154
})
145155

examples/vue-router/app.js

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ const ChildComponent = {
1818
metaInfo () {
1919
return {
2020
title: `${this.page} - ${this.date && this.date.toTimeString()}`,
21+
bodyAttrs: {
22+
class: 'child-component'
23+
},
2124
afterNavigation () {
2225
metaUpdated = 'yes'
2326
}
@@ -82,6 +85,9 @@ const app = new Vue(App)
8285
const { set, remove } = app.$meta().addApp('custom')
8386

8487
set({
88+
bodyAttrs: {
89+
class: 'custom-app'
90+
},
8591
meta: [
8692
{ charset: 'utf=8' }
8793
]

scripts/rollup.config.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@ function rollupConfig({
4343
const isBrowserBuild = !config.output || !config.output.format || config.output.format === 'umd' || config.output.file.includes('.browser.')
4444

4545
const replaceConfig = {
46-
exclude: 'node_modules/**',
46+
exclude: 'node_modules/(?!is-mergeable-object)',
4747
delimiters: ['', ''],
4848
values: {
4949
// replaceConfig needs to have some values
5050
'const polyfill = process.env.NODE_ENV === \'test\'': 'const polyfill = true',
5151
'process.env.VERSION': `"${version}"`,
52-
'process.server' : isBrowserBuild ? 'false' : 'true'
52+
'process.server' : isBrowserBuild ? 'false' : 'true',
53+
// remove react stuff from is-mergeable-object
54+
'|| isReactElement(value)': '|| false'
5355
}
5456
}
5557

src/client/load.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ export function addListeners () {
6060
}
6161

6262
export function applyCallbacks (matchElement) {
63-
callbacks.forEach(([query, callback]) => {
63+
callbacks.forEach((args) => {
64+
// do not use destructuring for args, it increases transpiled size
65+
// due to var checks while we are guaranteed the structure of the cb
66+
const query = args[0]
67+
const callback = args[1]
6468
const selector = `${query}[onload="this.__vm_l=1"]`
6569

6670
let elements = []

src/client/updateClientMetaInfo.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export default function updateClientMetaInfo (appId, options = {}, newInfo) {
5656

5757
if (includes(metaInfoAttributeKeys, type)) {
5858
const tagName = type.substr(0, 4)
59-
updateAttribute(options, newInfo[type], getTag(tags, tagName))
59+
updateAttribute(appId, options, type, newInfo[type], getTag(tags, tagName))
6060
continue
6161
}
6262

src/client/updaters/attribute.js

+52-26
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,72 @@
11
import { booleanHtmlAttributes } from '../../shared/constants'
2-
import { toArray, includes } from '../../utils/array'
3-
import { isArray } from '../../utils/is-type'
2+
import { includes } from '../../utils/array'
3+
4+
// keep a local map of attribute values
5+
// instead of adding it to the html
6+
export const attributeMap = {}
47

58
/**
69
* Updates the document's html tag attributes
710
*
811
* @param {Object} attrs - the new document html attributes
912
* @param {HTMLElement} tag - the HTMLElement tag to update with new attrs
1013
*/
11-
export default function updateAttribute ({ attribute } = {}, attrs, tag) {
14+
export default function updateAttribute (appId, { attribute } = {}, type, attrs, tag) {
1215
const vueMetaAttrString = tag.getAttribute(attribute)
13-
const vueMetaAttrs = vueMetaAttrString ? vueMetaAttrString.split(',') : []
14-
const toRemove = toArray(vueMetaAttrs)
16+
if (vueMetaAttrString) {
17+
attributeMap[type] = JSON.parse(decodeURI(vueMetaAttrString))
18+
tag.removeAttribute(attribute)
19+
}
1520

16-
const keepIndexes = []
17-
for (const attr in attrs) {
18-
if (attrs.hasOwnProperty(attr)) {
19-
const value = includes(booleanHtmlAttributes, attr)
20-
? ''
21-
: isArray(attrs[attr]) ? attrs[attr].join(' ') : attrs[attr]
21+
let data = attributeMap[type] || {}
22+
23+
const toUpdate = []
2224

23-
tag.setAttribute(attr, value || '')
25+
// remove attributes from the map
26+
// which have been removed for this appId
27+
for (const attr in data) {
28+
if (data[attr] && appId in data[attr]) {
29+
toUpdate.push(attr)
2430

25-
if (!includes(vueMetaAttrs, attr)) {
26-
vueMetaAttrs.push(attr)
31+
if (!attrs[attr]) {
32+
delete data[attr][appId]
2733
}
34+
}
35+
}
36+
37+
for (const attr in attrs) {
38+
const attrData = data[attr]
39+
40+
if (!attrData || attrData[appId] !== attrs[attr]) {
41+
toUpdate.push(attr)
2842

29-
// filter below wont ever check -1
30-
keepIndexes.push(toRemove.indexOf(attr))
43+
if (attrs[attr]) {
44+
data[attr] = data[attr] || {}
45+
data[attr][appId] = attrs[attr]
46+
} else {
47+
delete data[attr][appId]
48+
}
3149
}
3250
}
3351

34-
const removedAttributesCount = toRemove
35-
.filter((el, index) => !includes(keepIndexes, index))
36-
.reduce((acc, attr) => {
37-
tag.removeAttribute(attr)
38-
return acc + 1
39-
}, 0)
52+
for (const attr of toUpdate) {
53+
const attrData = data[attr]
4054

41-
if (vueMetaAttrs.length === removedAttributesCount) {
42-
tag.removeAttribute(attribute)
43-
} else {
44-
tag.setAttribute(attribute, (vueMetaAttrs.sort()).join(','))
55+
const attrValues = []
56+
for (const appId in attrData) {
57+
Array.prototype.push.apply(attrValues, [].concat(attrData[appId]))
58+
}
59+
60+
if (attrValues.length) {
61+
const attrValue = includes(booleanHtmlAttributes, attr) && attrValues.some(Boolean)
62+
? ''
63+
: attrValues.filter(Boolean).join(' ')
64+
65+
tag.setAttribute(attr, attrValue)
66+
} else {
67+
tag.removeAttribute(attr)
68+
}
4569
}
70+
71+
attributeMap[type] = data
4672
}

src/server/generateServerInjector.js

+20-7
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,30 @@ export default function generateServerInjector (options, metaInfo) {
4747
}
4848

4949
if (metaInfoAttributeKeys.includes(type)) {
50-
let str = attributeGenerator(options, type, serverInjector.data[type], arg)
50+
const attributeData = {}
5151

52-
if (serverInjector.extraData) {
53-
for (const appId in serverInjector.extraData) {
54-
const data = serverInjector.extraData[appId][type]
55-
const extraStr = attributeGenerator(options, type, data, arg)
56-
str = `${str}${extraStr}`
52+
const data = serverInjector.data[type]
53+
if (data) {
54+
for (const attr in data) {
55+
attributeData[attr] = {
56+
[options.ssrAppId]: data[attr]
57+
}
5758
}
5859
}
5960

60-
return str
61+
for (const appId in serverInjector.extraData) {
62+
const data = serverInjector.extraData[appId][type]
63+
if (data) {
64+
for (const attr in data) {
65+
attributeData[attr] = {
66+
...attributeData[attr],
67+
[appId]: data[attr]
68+
}
69+
}
70+
}
71+
}
72+
73+
return attributeGenerator(options, type, attributeData, arg)
6174
}
6275

6376
let str = tagGenerator(options, type, serverInjector.data[type], arg)

src/server/generators/attribute.js

+11-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { booleanHtmlAttributes } from '../../shared/constants'
2-
import { isUndefined, isArray } from '../../utils/is-type'
32

43
/**
54
* Generates tag attributes for use on the server.
@@ -10,22 +9,26 @@ import { isUndefined, isArray } from '../../utils/is-type'
109
*/
1110
export default function attributeGenerator ({ attribute, ssrAttribute } = {}, type, data, addSrrAttribute) {
1211
let attributeStr = ''
13-
const watchedAttrs = []
1412

1513
for (const attr in data) {
16-
if (data.hasOwnProperty(attr)) {
17-
watchedAttrs.push(attr)
14+
const attrData = data[attr]
15+
const attrValues = []
1816

19-
attributeStr += isUndefined(data[attr]) || booleanHtmlAttributes.includes(attr)
20-
? attr
21-
: `${attr}="${isArray(data[attr]) ? data[attr].join(' ') : data[attr]}"`
17+
for (const appId in attrData) {
18+
attrValues.push(...[].concat(attrData[appId]))
19+
}
20+
21+
if (attrValues.length) {
22+
attributeStr += booleanHtmlAttributes.includes(attr) && attrValues.some(Boolean)
23+
? `${attr}`
24+
: `${attr}="${attrValues.join(' ')}"`
2225

2326
attributeStr += ' '
2427
}
2528
}
2629

2730
if (attributeStr) {
28-
attributeStr += `${attribute}="${(watchedAttrs.sort()).join(',')}"`
31+
attributeStr += `${attribute}="${encodeURI(JSON.stringify(data))}"`
2932
}
3033

3134
if (type === 'htmlAttrs' && addSrrAttribute) {

src/shared/additional-app.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import updateClientMetaInfo from '../client/updateClientMetaInfo'
2-
import { removeElementsByAppId } from '../utils/elements'
2+
import { updateAttribute } from '../client/updaters'
3+
import { metaInfoAttributeKeys } from '../shared/constants'
4+
import { getTag, removeElementsByAppId } from '../utils/elements'
35

46
let appsMetaInfo
57

@@ -24,6 +26,12 @@ export function setMetaInfo (vm, appId, options, metaInfo) {
2426

2527
export function removeMetaInfo (vm, appId, options) {
2628
if (vm && vm.$el) {
29+
const tags = {}
30+
for (const type of metaInfoAttributeKeys) {
31+
const tagName = type.substr(0, 4)
32+
updateAttribute(appId, options, type, {}, getTag(tags, tagName))
33+
}
34+
2735
return removeElementsByAppId(options, appId)
2836
}
2937

src/shared/constants.js

+7-8
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,18 @@ export const defaultOptions = {
5757
ssrAppId
5858
}
5959

60+
// The metaInfo property keys which are used to disable escaping
61+
export const disableOptionKeys = [
62+
'__dangerouslyDisableSanitizers',
63+
'__dangerouslyDisableSanitizersByTagID'
64+
]
65+
6066
// List of metaInfo property keys which are configuration options (and dont generate html)
6167
export const metaInfoOptionKeys = [
6268
'titleChunk',
6369
'titleTemplate',
6470
'changed',
65-
'__dangerouslyDisableSanitizers',
66-
'__dangerouslyDisableSanitizersByTagID'
67-
]
68-
69-
// The metaInfo property keys which are used to disable escaping
70-
export const disableOptionKeys = [
71-
'__dangerouslyDisableSanitizers',
72-
'__dangerouslyDisableSanitizersByTagID'
71+
...disableOptionKeys
7372
]
7473

7574
// List of metaInfo property keys which only generates attributes and no tags

src/shared/escaping.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ export function escape (info, options, escapeOptions, escapeKeys) {
3434
continue
3535
}
3636

37-
let [ disableKey ] = disableOptionKeys
37+
// do not use destructuring for disableOptionKeys, it increases transpiled size
38+
// due to var checks while we are guaranteed the structure of the cb
39+
let disableKey = disableOptionKeys[0]
40+
3841
if (escapeOptions[disableKey] && includes(escapeOptions[disableKey], key)) {
3942
// this info[key] doesnt need to escaped if the option is listed in __dangerouslyDisableSanitizers
4043
escaped[key] = value
@@ -81,8 +84,10 @@ export function escape (info, options, escapeOptions, escapeKeys) {
8184
}
8285

8386
export function escapeMetaInfo (options, info, escapeSequences = []) {
87+
// do not use destructuring for seq, it increases transpiled size
88+
// due to var checks while we are guaranteed the structure of the cb
8489
const escapeOptions = {
85-
doEscape: value => escapeSequences.reduce((val, [v, r]) => val.replace(v, r), value)
90+
doEscape: value => escapeSequences.reduce((val, seq) => val.replace(seq[0], seq[1]), value)
8691
}
8792

8893
disableOptionKeys.forEach((disableKey, index) => {

test/unit/components.test.js

+11-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getComponentMetaInfo } from '../../src/shared/getComponentOption'
22
import _getMetaInfo from '../../src/shared/getMetaInfo'
3-
import { mount, createWrapper, loadVueMetaPlugin, vmTick } from '../utils'
3+
import { mount, createWrapper, loadVueMetaPlugin, vmTick, clearClientAttributeMap } from '../utils'
44
import { defaultOptions } from '../../src/shared/constants'
55

66
import GoodbyeWorld from '../components/goodbye-world.vue'
@@ -226,6 +226,13 @@ describe('client', () => {
226226
// this component uses a computed prop to simulate a non-synchronous
227227
// metaInfo update like you would have with a Vuex mutation
228228
const Component = Vue.extend({
229+
metaInfo () {
230+
return {
231+
htmlAttrs: {
232+
theme: this.theme
233+
}
234+
}
235+
},
229236
data () {
230237
return {
231238
hiddenTheme: 'light'
@@ -239,14 +246,7 @@ describe('client', () => {
239246
beforeMount () {
240247
this.hiddenTheme = 'dark'
241248
},
242-
render: h => h('div'),
243-
metaInfo () {
244-
return {
245-
htmlAttrs: {
246-
theme: this.theme
247-
}
248-
}
249-
}
249+
render: h => h('div')
250250
})
251251

252252
const vm = new Component().$mount(el)
@@ -263,6 +263,8 @@ describe('client', () => {
263263
})
264264

265265
test('changes during hydration initialization trigger an update', async () => {
266+
clearClientAttributeMap()
267+
266268
html.setAttribute(defaultOptions.ssrAttribute, 'true')
267269

268270
const el = document.createElement('div')

test/unit/generators.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('generators', () => {
2929
const testInfo = typeTests[action]
3030

3131
// return when no test case available
32-
if (!testCases[action] && !testInfo.test) {
32+
if (!testCases[action]) {
3333
return
3434
}
3535

0 commit comments

Comments
 (0)