2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-14 18:22:24 +03:00

feat: first work on vue v3 composition metainfo app

This commit is contained in:
pimlie
2020-05-03 19:59:05 +02:00
parent 41c7561bf9
commit 5d0eb1ab60
21 changed files with 551 additions and 182 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules
__build__
dist
+2 -2
View File
@@ -10,8 +10,8 @@
<!-- 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="ssr">SSR</a></li>
<li><a href="multiple-apps">Usage with multiple apps</a></li>
<li><a href="ssr">SSR</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>
View File
View File
+18 -18
View File
@@ -1,23 +1,23 @@
window.users.push({
'id': 1,
'name': 'Leanne Graham',
'username': 'Bret',
'email': 'Sincere@april.biz',
'address': {
'street': 'Kulas Light',
'suite': 'Apt. 556',
'city': 'Gwenborough',
'zipcode': '92998-3874',
'geo': {
'lat': '-37.3159',
'lng': '81.1496'
id: 1,
name: 'Leanne Graham',
username: 'Bret',
email: 'Sincere@april.biz',
address: {
street: 'Kulas Light',
suite: 'Apt. 556',
city: 'Gwenborough',
zipcode: '92998-3874',
geo: {
lat: '-37.3159',
lng: '81.1496'
}
},
'phone': '1-770-736-8031 x56442',
'website': 'hildegard.org',
'company': {
'name': 'Romaguera-Crona',
'catchPhrase': 'Multi-layered client-server neural-net',
'bs': 'harness real-time e-markets'
phone: '1-770-736-8031 x56442',
website: 'hildegard.org',
company: {
name: 'Romaguera-Crona',
catchPhrase: 'Multi-layered client-server neural-net',
bs: 'harness real-time e-markets'
}
})
+18 -18
View File
@@ -1,23 +1,23 @@
window.users.push({
'id': 2,
'name': 'Ervin Howell',
'username': 'Antonette',
'email': 'Shanna@melissa.tv',
'address': {
'street': 'Victor Plains',
'suite': 'Suite 879',
'city': 'Wisokyburgh',
'zipcode': '90566-7771',
'geo': {
'lat': '-43.9509',
'lng': '-34.4618'
id: 2,
name: 'Ervin Howell',
username: 'Antonette',
email: 'Shanna@melissa.tv',
address: {
street: 'Victor Plains',
suite: 'Suite 879',
city: 'Wisokyburgh',
zipcode: '90566-7771',
geo: {
lat: '-43.9509',
lng: '-34.4618'
}
},
'phone': '010-692-6593 x09125',
'website': 'anastasia.net',
'company': {
'name': 'Deckow-Crist',
'catchPhrase': 'Proactive didactic contingency',
'bs': 'synergize scalable supply-chains'
phone: '010-692-6593 x09125',
website: 'anastasia.net',
company: {
name: 'Deckow-Crist',
catchPhrase: 'Proactive didactic contingency',
bs: 'synergize scalable supply-chains'
}
})
+18 -18
View File
@@ -1,23 +1,23 @@
window.users.push({
'id': 3,
'name': 'Clementine Bauch',
'username': 'Samantha',
'email': 'Nathan@yesenia.net',
'address': {
'street': 'Douglas Extension',
'suite': 'Suite 847',
'city': 'McKenziehaven',
'zipcode': '59590-4157',
'geo': {
'lat': '-68.6102',
'lng': '-47.0653'
id: 3,
name: 'Clementine Bauch',
username: 'Samantha',
email: 'Nathan@yesenia.net',
address: {
street: 'Douglas Extension',
suite: 'Suite 847',
city: 'McKenziehaven',
zipcode: '59590-4157',
geo: {
lat: '-68.6102',
lng: '-47.0653'
}
},
'phone': '1-463-123-4447',
'website': 'ramiro.info',
'company': {
'name': 'Romaguera-Jacobson',
'catchPhrase': 'Face to face bifurcated interface',
'bs': 'e-enable strategic applications'
phone: '1-463-123-4447',
website: 'ramiro.info',
company: {
name: 'Romaguera-Jacobson',
catchPhrase: 'Face to face bifurcated interface',
bs: 'e-enable strategic applications'
}
})
+18 -18
View File
@@ -1,23 +1,23 @@
window.users.push({
'id': 4,
'name': 'Patricia Lebsack',
'username': 'Karianne',
'email': 'Julianne.OConner@kory.org',
'address': {
'street': 'Hoeger Mall',
'suite': 'Apt. 692',
'city': 'South Elvis',
'zipcode': '53919-4257',
'geo': {
'lat': '29.4572',
'lng': '-164.2990'
id: 4,
name: 'Patricia Lebsack',
username: 'Karianne',
email: 'Julianne.OConner@kory.org',
address: {
street: 'Hoeger Mall',
suite: 'Apt. 692',
city: 'South Elvis',
zipcode: '53919-4257',
geo: {
lat: '29.4572',
lng: '-164.2990'
}
},
'phone': '493-170-9623 x156',
'website': 'kale.biz',
'company': {
'name': 'Robel-Corkery',
'catchPhrase': 'Multi-tiered zero tolerance productivity',
'bs': 'transition cutting-edge web services'
phone: '493-170-9623 x156',
website: 'kale.biz',
company: {
name: 'Robel-Corkery',
catchPhrase: 'Multi-tiered zero tolerance productivity',
bs: 'transition cutting-edge web services'
}
})
+5 -5
View File
@@ -5,12 +5,12 @@ const {
processIf,
getBaseTransformPreset,
createObjectExpression,
createObjectProperty,
createObjectProperty
} = require('@vue/compiler-core')
const { parse } = require('@vue/compiler-dom')
function headTransform(node, context) {
function headTransform (node, context) {
console.log('NODE', node)
if (node.type === 1 /* NodeTypes.ELEMENT */) {
return () => {
@@ -24,7 +24,7 @@ function headTransform(node, context) {
node.children.length === 1 ? node.children[0] : 'null'
)
//options.properties.push(option)
// options.properties.push(option)
}
}
}
@@ -32,7 +32,7 @@ function headTransform(node, context) {
module.exports = function (source, map) {
// TODO: add options
const ast = parse(source)
//console.log('AST', ast)
// console.log('AST', ast)
const [nodeTransforms, directiveTransforms] = getBaseTransformPreset({
prefixIdentifiers: true
@@ -49,7 +49,7 @@ module.exports = function (source, map) {
const result = generate(ast, { mode: 'module' })
console.log(result.code)
console.log(result.code)
this.callback(null, `
import { computed } from 'vue'
+1 -1
View File
@@ -77,6 +77,6 @@ setTimeout(() => {
setTimeout(() => {
console.log('trigger app 4')
const App = Vue.extend({ template: `<div>app 4</div>` })
const App = Vue.extend({ template: '<div>app 4</div>' })
new App().$mount()
}, 10000)
+57
View File
@@ -0,0 +1,57 @@
<script>
import { h, ref, computed, inject, Teleport } from 'vue'
import { renderMeta } from './render'
export default {
props: {
metainfo: {
type: Object,
required: true
}
},
setup() {
const mapping = inject('__vueMetaConfig')
return {
mapping
}
},
render() {
const targets = {}
for (const key in this.metainfo) {
const config = this.mapping[key] || {}
const vnodes = renderMeta(this, key, this.metainfo[key], config)
let target = (key !== 'base' && this.metainfo[key].target) || config.target || 'head'
if (Array.isArray(vnodes)) {
for (const vnode of vnodes) {
if (vnode.__vm_target) {
target = vnode.__vm_target
delete vnode.__vm_target
}
if (!targets[target]) {
targets[target] = []
}
targets[target].push(vnode)
}
continue
}
if (!targets[target]) {
targets[target] = []
}
targets[target].push(vnodes)
continue
}
console.log('TARGETS', targets)
return Object.keys(targets).map(target => {
return h(Teleport, { to: target }, targets[target])
})
}
}
</script>
+119
View File
@@ -0,0 +1,119 @@
const defaults = {
title: {
contentAttributes: false
},
base: {
contentAttributes: [
'href',
'target'
]
},
meta: {
nameAttribute: 'name',
contentAttributes: [
'content',
'name',
'http-equiv',
'charset'
]
},
link: {
contentAttributes: [
'href',
'crossorigin',
'rel',
'media',
'integrity',
'hreflang',
'type',
'referrerpolicy',
'sizes',
'imagesrcset',
'imagesizes',
'as',
'color'
]
},
style: {
contentAttributes: [
'media'
]
},
script: {
contentAttributes: [
'src',
'type',
'nomodule',
'async',
'defer',
'crossorigin',
'integrity',
'referrerpolicy'
]
},
noscript: {
contentAttributes: false
}
}
const defaultMapping = {
body: {
tag: 'script',
target: 'body'
},
base: {
contentAttribute: 'href'
},
charset: {
tag: 'meta',
nameless: true,
contentAttribute: 'charset'
},
description: {
tag: 'meta'
},
og: {
group: true,
namespacedAttribute: true,
tag: 'meta',
nameAttribute: 'property'
},
twitter: {
group: true,
namespacedAttribute: true,
tag: 'meta'
}
}
export {
defaults,
defaultMapping
}
export function hasConfig (name) {
return !!defaults[name] || !!defaultMapping[name]
}
export function getConfigKey (name, key, config, dontLog) {
if (!dontLog) {
// console.log('getConfigKey', name, key, getConfigKey(name, key, config, true), config)
}
if (config && key in config) {
return config[key]
}
if (Array.isArray(name)) {
for (const _name of name) {
if (_name && _name in defaults) {
return defaults[_name][key]
}
}
}
if (name in defaults) {
return defaults[name][key]
}
return undefined
}
+18 -13
View File
@@ -1,24 +1,30 @@
import { markRaw, reactive, computed, onMounted } from 'vue'
import { markRaw, reactive, onMounted } from 'vue'
import { defaultMapping } from './config'
const apps = {}
let appId = 1
export function createMeta () {
const id = Symbol()
export function createMeta ({ config }) {
const id = Symbol(`vue-meta-${appId++}`)
const Meta = {
id,
install(app) {
install (app) {
let watchersAdded = false
app.provide('__vueMetaConfig', {
...defaultMapping,
...config
})
app.mixin({
created() {
created () {
if (this === this.$root) {
watchersAdded = true
}
if (!this.metaData) {
if (!this.metaData || watchersAdded) {
return
}
@@ -33,7 +39,7 @@ export function createMeta () {
}
}
const __meta = markRaw({
this.__meta = markRaw({
depth
})
console.log('CREATED', this, this.metaData, depth)
@@ -49,15 +55,14 @@ export function createMeta () {
return Meta
}
export function useMeta () {
export function useMeta (rawMetainfo) {
onMounted(vmMounted)
const metaData = reactive([])
console.log(this)
const metainfo = reactive(rawMetainfo)
return metaData
return metainfo
}
function vmMounted() {
function vmMounted () {
console.log('MOUNTED', this, arguments)
}
+154
View File
@@ -0,0 +1,154 @@
import { h } from 'vue'
import { getConfigKey } from './config'
export function renderMeta (ctx, key, data, config) {
console.info('renderMeta', key, data, config)
if (config.group) {
return renderGroup(ctx, key, data, config)
}
return renderTag(ctx, key, data, config)
}
export function renderGroup (ctx, key, data, config) {
console.info('renderGroup', key, data, config)
if (Array.isArray(data)) {
config.contentAttributes = getConfigKey([key, config.tag], 'contentAttributes', config)
return data.map(_data => renderTag(ctx, key, _data, config))
}
return Object.keys(data).map((childKey) => {
const groupConfig = {
group: key,
data
}
if (config.namespaced || config.namespacedAttribute) {
let namespace
if (config.namespaced) {
namespace = config.namespaced === true ? key : config.namespaced
groupConfig.tagNamespace = namespace
} else {
namespace = config.namespacedAttribute === true ? key : config.namespacedAttribute
groupConfig.fullName = `${namespace}:${childKey}`
groupConfig.slotName = `${namespace}(${childKey})`
}
}
return renderTag(ctx, key, data[childKey], config, groupConfig)
})
}
export function renderTag (ctx, key, data, config = {}, groupConfig = {}) {
if (!config.group && Array.isArray(data)) {
return renderTag(ctx, key, { content: data }, config, groupConfig)
}
const { tag = config.tag || key } = data
const {
slotName = key,
fullName = key
} = groupConfig
let content, hasChilds
if (Array.isArray(data)) {
return data.map((child) => {
return renderTag(ctx, key, child, config, groupConfig)
})
} else if (data.content && Array.isArray(data.content)) {
content = data.content.map((child) => {
if (typeof child === 'string') {
return child
}
return renderTag(ctx, key, child, config, groupConfig)
})
hasChilds = true
} else {
content = data
}
let { attrs: attributes } = data
if (!attributes && typeof data === 'object') {
attributes = {
...data
}
delete attributes.tag
delete attributes.content
delete attributes.target
} else {
attributes = {}
}
if (hasChilds) {
content = getSlotContent(ctx, slotName, content, config, data)
} else {
const contentAttributes = getConfigKey(tag, 'contentAttributes', config)
if (contentAttributes) {
if (!config.nameless) {
const nameAttribute = getConfigKey(tag, 'nameAttribute', config)
if (nameAttribute) {
attributes[nameAttribute] = fullName
}
}
const contentAttribute = config.contentAttribute || contentAttributes[0]
attributes[contentAttribute] = getSlotContent(ctx, slotName, attributes[contentAttribute] || content, config, groupConfig)
content = undefined
} else {
content = getSlotContent(ctx, slotName, content, config, data, true)
}
}
const finalTag = groupConfig.tagNamespace
? `${groupConfig.tagNamespace}:${tag}`
: tag
console.info('FINAL TAG', finalTag)
console.log(' ATTRIBUTES', attributes)
console.log(' CONTENT', content)
// console.log(data, attributes, config)
if (hasChilds) {
for (const child of content) {
if (typeof child === 'string') {
continue
}
if (child.type === finalTag) {
return content
}
break
}
}
const vnode = h(finalTag, attributes, content)
if (data.target) {
vnode.__vm_target = data.target
}
return vnode
}
export function getSlotContent ({ metainfo, $slots }, slotName, content, config, groupConfig) {
if (!$slots[slotName]) {
return content
}
const slotProps = {
content,
metainfo
}
if (groupConfig.group) {
slotProps[groupConfig.group] = groupConfig.data
}
content = $slots[slotName](slotProps)
return content[0].children
}
+5 -6
View File
@@ -1,11 +1,10 @@
import { createSSRApp } from 'vue'
import { createRouter, createMemoryHistory } from 'vue-router'
import VueMeta from '../../'
/*Vue.use(Router)
/* Vue.use(Router)
Vue.use(VueMeta, {
tagIDKeyName: 'hid'
})*/
}) */
export default function createMyApp () {
const Home = {
@@ -116,7 +115,7 @@ export default function createMyApp () {
users: process.server ? [] : window.users
}
},
mounted() {
mounted () {
const { set, remove } = this.$meta().addApp('client-only')
set({
bodyAttrs: { class: 'client-only' }
@@ -149,11 +148,11 @@ export default function createMyApp () {
app.use(router)
/*const { set } = app.$meta().addApp('custom')
/* const { set } = app.$meta().addApp('custom')
set({
bodyAttrs: { class: 'custom-app' },
meta: [{ charset: 'utf-8' }]
})*/
}) */
return { app, router }
}
+4 -4
View File
@@ -1,8 +1,8 @@
import path from 'path'
import fs from 'fs-extra'
import template from 'lodash/template'
const { renderToString } = require('@vue/server-renderer')
import createApp from './App'
const { renderToString } = require('@vue/server-renderer')
const templateFile = path.resolve(__dirname, 'app.template.html')
const templateContent = fs.readFileSync(templateFile, { encoding: 'utf8' })
@@ -18,12 +18,12 @@ export async function renderPage ({ url }) {
await router.push(url.substr(4))
await router.isReady()
/*console.log(router)
/* console.log(router)
const matchedComponents = router.getMatchedComponents()
// no matched routes, reject with 404
if (!matchedComponents.length) {
return reject({ code: 404 })
}*/
} */
const appHtml = await renderToString(app)
@@ -41,7 +41,7 @@ export async function renderPage ({ url }) {
head: () => {},
bodyPrepend: () => {},
bodyAppend: () => {}
//...app.$meta().inject()
// ...app.$meta().inject()
})
return pageHtml
+3 -2
View File
@@ -3,12 +3,13 @@
<p>Has metaInfo been updated due to navigation? {{ metaUpdated }}</p>
</template>
<script>
/*
<head type="template">
<title v-if="title">{{ title }}</title>
<meta v-for="meta in metas" :name="meta.name" :content="meta.content" />
</head>
<script>
*/
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
+103 -74
View File
@@ -1,27 +1,17 @@
import { createApp, defineComponent, reactive, toRefs, h, onMounted } from 'vue'
import VueMeta from 'vue-meta'
import { createApp, defineComponent, reactive, toRefs, h } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import About from './about.vue'
import Metainfo from '../next/Metainfo.vue'
import { createMeta, useMeta } from '../next'
/*Vue.use(VueMeta, {
refreshOnceOnNavigation: true
})*/
const meta = createMeta({
})
import About from './about.vue'
let metaUpdated = 'no'
const ChildComponent = defineComponent({
name: 'child-component',
props: {
page: String
},
template: `
<metainfo>
<title>Another Title</title>
</metainfo>
<div>
<h3>You're looking at the <strong>{{ page }}</strong> page</h3>
<p>Has metaInfo been updated due to navigation? {{ metaUpdated }}</p>
@@ -38,41 +28,20 @@ const ChildComponent = defineComponent({
}
},
created () {
console.log(this)
//console.log(this)
},
setup () {
const metaData = useMeta({
})
const state = reactive({
date: null,
metaUpdated
})
onMounted(function vmMounted() {
console.log('MOUNTED', this, arguments)
})
return {
metaData,
...toRefs(state)
}
},
/*mounted () {
this.interval = setInterval(() => {
this.date = new Date()
}, 1000)
},
destroyed () {
clearInterval(this.interval)
}*/
}
})
// this wrapper function is not a requirement for vue-router,
// just a demonstration that render-function style components also work.
// See https://github.com/nuxt/vue-meta/issues/9 for more info.
function view (page) {
return {
name: `section-${page}`,
@@ -82,6 +51,86 @@ function view (page) {
}
}
const App = {
setup () {
const metainfo = useMeta({
base: { href: '/vue-router', target: '_blank' },
charset: 'utf8',
title: 'My Title',
description: 'The Description',
og: {
title: 'Og Title',
description: 'Bla bla',
image: [
'https://picsum.photos/600/400/?image=80',
'https://picsum.photos/600/400/?image=82'
]
},
twitter: {
title: 'Twitter Title'
},
noscript: [
'<!-- // A code comment -->',
{ tag: 'link', rel: 'stylesheet', href: 'style.css' }
],
otherNoscript: {
tag: 'noscript',
'data-test': 'hello',
content: [
'<!-- // Another code comment -->',
{ tag: 'link', rel: 'stylesheet', href: 'style2.css' }
]
},
body: 'body-script1.js',
script: [
'<!--[if IE]>',
{ src: 'head-script1.js' },
'<![endif]>',
{ src: 'body-script2.js', target: 'body' },
{ src: 'body-script3.js', target: '#put-it-here' }
],
esi: {
content: [{
tag: 'choose',
content: [{
tag: 'when',
test: '$(HTTP_COOKIE{group})=="Advanced"',
content: [{
tag: 'include',
src: 'http://www.example.com/advanced.html'
}]
}]
}]
}
})
setTimeout(() => (metainfo.title = 'My Updated Title'), 2000)
return {
metainfo
}
},
template: `
<metainfo :metainfo="metainfo">
<template v-slot:base="{ content, metainfo }">http://nuxt.dev:3000{{ content }}</template>
<template v-slot:title="{ content, metainfo }">{{ content }} - {{ metainfo.description }} - Hello</template>
<template v-slot:og(title)="{ content, metainfo, og }">
{{ content }} - {{ og.description }} - {{ metainfo.description }} - Hello Again
</template>
</metainfo>
<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>
`
}
const router = createRouter({
history: createWebHistory('/vue-router'),
routes: [
@@ -90,45 +139,27 @@ const router = createRouter({
]
})
const Metadata = {
template: `
<teleport to="head">
<slot />
</teleport>
<teleport to="body">
<slot name="body" />
</teleport>
`
}
const App = {
template: `
<metainfo>
<title>My Title</title>
<meta name="charset" content="utf8" />
<template v-slot:body>
<script>var a = 1</script>
</template>
</metainfo>
<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>
`
}
const meta = createMeta({
config: {
esi: {
group: true,
namespaced: true,
contentAttributes: [
'src',
'test',
'text'
]
}
}
})
const app = createApp(App)
app.component('metainfo', Metadata)
app.component('metainfo', Metainfo)
app.use(router)
app.use(meta)
app.mount('#app')
// old stuff:
/*
const { set, remove } = app.$meta().addApp('custom')
@@ -142,8 +173,6 @@ set({
})
setTimeout(() => remove(), 3000)
*/
app.mount('#app')
/*
const waitFor = time => new Promise(r => setTimeout(r, time || 1000))
const o = {
+1
View File
@@ -14,6 +14,7 @@
<body>
<a href="/">&larr; Examples index</a>
<div id="app"></div>
<div id="put-it-here"></div>
<script src="/__build__/vue-router.js"></script>
</body>
</html>
+2 -2
View File
@@ -4,7 +4,7 @@ import webpack from 'webpack'
import WebpackBar from 'webpackbar'
import { VueLoaderPlugin } from 'vue-loader'
const srcDir = path.join(__dirname, '..', 'src')
// const srcDir = path.join(__dirname, '..', 'src')
export default {
devtool: 'inline-source-map',
@@ -63,7 +63,7 @@ export default {
// is a simple `export * from '@vue/runtime-dom`. However having this
// extra re-export somehow causes webpack to always invalidate the module
// on the first HMR update and causes the page to reload.
'vue': 'vue/dist/vue.esm.js',
vue: 'vue/dist/vue.esm-bundler.js',
'vue-meta': path.resolve(__dirname, './next/')
}
},
+1 -1
View File
@@ -49,7 +49,7 @@
"dev": "cd examples && yarn dev && cd ..",
"docs": "vuepress dev --host 0.0.0.0 --port 3000 docs",
"docs:build": "vuepress build docs",
"lint": "eslint src test",
"lint": "eslint src test examples",
"prerelease": "git checkout master && git pull -r",
"release": "yarn lint && yarn test && standard-version",
"test": "yarn test:unit && yarn test:e2e-ssr && yarn test:e2e-browser",