2
0
mirror of https://github.com/tenrok/vue-meta.git synced 2026-06-19 22:20:33 +03:00

feat: enable onload callbacks (#414)

* refactor(examples): run ssr example from server

* chore: switch to babel for build

buble complains too much

* feat: enable loaded callbacks

feat: add skip option

* examples: add async-callback browser example

* examples: fix server

* examples(ssr): add reactive script with callback

* fix: also skip on ssr

* chore: remove unused var

* feat: only add mutationobserver if DOM is still loading

feat: disconnect mutation observer once DOM has loaded

* examples: pass vmid to loadCallback instead of el

* feat: also support load callbacks for link/style tags

* test: add unit tests for load

* test: add load e2e test

* chore: fix lint

* chore: remove unused files

* test: fix e2e load callback test

* test: fix attempt

* examples: ie9 compatiblity

destructuring doesnt work in ie9

* fix: add onload attribute on ssr

dont rely on mutationobserver

* chore: lint ci conf

* refactor: remove loadCallbackAttribute config option

test: fix coverage for load

* test: improve coverage

* fix: only use console when it exists (for ie9)

* chore: fix coverage
This commit is contained in:
Pim
2019-07-24 10:18:40 +02:00
committed by GitHub
parent 05163a77a8
commit fc71e1f1c4
49 changed files with 963 additions and 632 deletions
@@ -11,9 +11,11 @@
<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="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>
<li><a href="async-callback">Async Callback</a></li>
</ul>
</body>
</html>
+23
View File
@@ -0,0 +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'
}
},
'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'
}
})
+23
View File
@@ -0,0 +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'
}
},
'phone': '010-692-6593 x09125',
'website': 'anastasia.net',
'company': {
'name': 'Deckow-Crist',
'catchPhrase': 'Proactive didactic contingency',
'bs': 'synergize scalable supply-chains'
}
})
+23
View File
@@ -0,0 +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'
}
},
'phone': '1-463-123-4447',
'website': 'ramiro.info',
'company': {
'name': 'Romaguera-Jacobson',
'catchPhrase': 'Face to face bifurcated interface',
'bs': 'e-enable strategic applications'
}
})
+23
View File
@@ -0,0 +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'
}
},
'phone': '493-170-9623 x156',
'website': 'kale.biz',
'company': {
'name': 'Robel-Corkery',
'catchPhrase': 'Multi-tiered zero tolerance productivity',
'bs': 'transition cutting-edge web services'
}
})
+88
View File
@@ -0,0 +1,88 @@
import Vue from 'vue'
import VueMeta from 'vue-meta'
Vue.use(VueMeta)
window.users = []
new Vue({
metaInfo () {
return {
title: 'Async Callback',
titleTemplate: '%s | Vue Meta Examples',
script: [
{
skip: this.count < 2,
vmid: 'potatoes',
src: '/user-3.js',
async: true,
callback: this.updateCounter
},
{
skip: this.count < 1,
vmid: 'vegetables',
src: '/user-2.js',
async: true,
callback: this.updateCounter
},
{
vmid: 'meat',
src: '/user-1.js',
async: true,
callback: el => this.loadCallback(el.getAttribute('data-vmid'))
},
...this.scripts
]
}
},
data () {
return {
count: 0,
scripts: [],
users: window.users
}
},
watch: {
count (val) {
if (val === 3) {
this.addScript()
}
}
},
methods: {
updateCounter () {
this.count++
},
addScript () {
this.scripts.push({
src: '/user-4.js',
callback: () => {
this.updateCounter()
}
})
},
loadCallback (vmid) {
if (vmid === 'meat') {
this.updateCounter()
}
}
},
template: `
<div id="app">
<h1>Async Callback</h1>
<p>{{ count }} scripts loaded</p>
<div>
<h2>Users</h2>
<ul>
<li
v-for="user in users"
:key="user.id"
>
<strong>{{ user.id }}</strong>: {{ user.name }}
</li>
</ul>
</div>
</div>
`
}).$mount('#app')
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>Async Callback Title</title>
<link rel="stylesheet" href="/global.css">
</head>
<body>
<a href="/">&larr; Examples index</a>
<div id="app"></div>
<script src="/__build__/async-callback.js"></script>
</body>
</html>
+2 -2
View File
@@ -11,10 +11,10 @@ Vue.component('child', {
default: ''
}
},
render(h) {
render (h) {
return h('h3', null, this.page)
},
metaInfo() {
metaInfo () {
return {
title: this.page
}
+2 -2
View File
@@ -11,11 +11,11 @@ Vue.component('foo', {
})
new Vue({
data() {
data () {
return { showFoo: false }
},
methods: {
show() {
show () {
this.showFoo = !this.showFoo
}
},
+7 -7
View File
@@ -6,7 +6,7 @@ Vue.use(VueMeta)
// index.html contains a manual SSR render
const app1 = new Vue({
metaInfo() {
metaInfo () {
return {
title: 'App 1 title',
bodyAttrs: {
@@ -14,15 +14,15 @@ const app1 = new Vue({
},
meta: [
{ name: 'description', content: 'Hello from app 1', vmid: 'test' },
{ name: 'og:description', content: this.ogContent }
{ name: 'og:description', content: this.ogContent }
],
script: [
{ innerHTML: 'var appId=1.1', body: true },
{ innerHTML: 'var appId=1.2', vmid: 'app-id-body' },
{ innerHTML: 'var appId=1.2', vmid: 'app-id-body' }
]
}
},
data() {
data () {
return {
ogContent: 'Hello from ssr app'
}
@@ -44,7 +44,7 @@ const app2 = new Vue({
],
script: [
{ innerHTML: 'var appId=2.1', body: true },
{ innerHTML: 'var appId=2.2', vmid: 'app-id-body', body: true },
{ innerHTML: 'var appId=2.2', vmid: 'app-id-body', body: true }
]
}),
template: `
@@ -60,7 +60,6 @@ const app3 = new Vue({
`
}).$mount('#app3')
setTimeout(() => {
console.log('trigger app 1')
app1.$data.ogContent = 'Hello from app 1'
@@ -75,8 +74,9 @@ 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()
new App().$mount()
}, 10000)
+18 -1
View File
@@ -6,11 +6,13 @@ import rewrite from 'express-urlrewrite'
import webpack from 'webpack'
import webpackDevMiddleware from 'webpack-dev-middleware'
import WebpackConfig from './webpack.config'
import { renderPage } from './ssr/server'
const app = express()
app.use(webpackDevMiddleware(webpack(WebpackConfig), {
publicPath: '/__build__/',
writeToDisk: false,
stats: {
colors: true,
chunks: false
@@ -21,12 +23,27 @@ fs.readdirSync(__dirname)
.filter(file => file !== 'ssr')
.forEach((file) => {
if (fs.statSync(path.join(__dirname, file)).isDirectory()) {
app.use(rewrite('/' + file + '/*', '/' + file + '/index.html'))
app.use(rewrite(`/${file}/*`, `/${file}/index.html`))
}
})
app.use(express.static(path.join(__dirname, '_static')))
app.use(express.static(__dirname))
app.use(async (req, res, next) => {
if (!req.url.startsWith('/ssr')) {
next()
}
try {
const html = await renderPage()
res.send(html)
} catch (e) {
consola.error('SSR Oops:', e)
next()
}
})
const host = process.env.HOST || 'localhost'
const port = process.env.PORT || 3000
+90
View File
@@ -0,0 +1,90 @@
import Vue from 'vue'
import VueMeta from '../../'
Vue.use(VueMeta, {
tagIDKeyName: 'hid'
})
export default function createApp () {
return new Vue({
components: {
Hello: {
template: '<p>Hello World</p>',
metaInfo: {
title: 'Hello World',
meta: [
{
hid: 'description',
name: 'description',
content: 'The description'
}
]
}
}
},
metaInfo () {
return {
title: 'Boring Title',
htmlAttrs: { amp: true },
meta: [
{
hid: 'description',
name: 'description',
content: 'Say something'
}
],
script: [
{
hid: 'ldjson-schema',
type: 'application/ld+json',
innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }'
}, {
type: 'application/ld+json',
innerHTML: '{ "body": "yes" }',
body: true
}, {
hid: 'my-async-script-with-load-callback',
src: '/user-1.js',
body: true,
defer: true,
callback: this.loadCallback
}, {
skip: this.count < 1,
src: '/user-2.js',
body: true,
callback: this.loadCallback
}
],
__dangerouslyDisableSanitizersByTagID: {
'ldjson-schema': ['innerHTML']
}
}
},
data () {
return {
count: 0,
users: process.server ? [] : window.users
}
},
methods: {
loadCallback () {
this.count++
}
},
template: `
<div id="app">
<hello/>
<p>{{ count }} users loaded</p>
<ul>
<li
v-for="user in users"
:key="user.id"
>
{{ user.id }}: {{ user.name }}
</li>
</ul>
</div>`
})
}
-55
View File
@@ -1,55 +0,0 @@
import Vue from 'vue'
export default async function createApp() {
// the dynamic import is for this example only
const vueMetaModule = process.env.NODE_ENV === 'development' ? '../../' : 'vue-meta'
const VueMeta = await import(vueMetaModule).then(m => m.default || m)
Vue.use(VueMeta, {
tagIDKeyName: 'hid'
})
return new Vue({
components: {
Hello: {
template: '<p>Hello</p>',
metaInfo: {
title: 'Coucou',
meta: [
{
hid: 'description',
name: 'description',
content: 'Coucou'
}
]
}
}
},
template: '<hello/>',
metaInfo: {
title: 'Hello',
htmlAttrs: { amp: true },
meta: [
{
hid: 'description',
name: 'description',
content: 'Hello World'
}
],
script: [
{
hid: 'ldjson-schema',
type: 'application/ld+json',
innerHTML: '{ "@context": "http://www.schema.org", "@type": "Organization" }'
}, {
type: 'application/ld+json',
innerHTML: '{ "body": "yes" }',
body: true
}
],
__dangerouslyDisableSanitizersByTagID: {
'ldjson-schema': ['innerHTML']
}
}
})
}
+9 -3
View File
@@ -1,16 +1,22 @@
<!doctype html>
<html data-vue-meta-server-rendered {{ htmlAttrs.text() }}>
<html {{ htmlAttrs.text(true) }}>
<head {{ headAttrs.text() }}>
{{ meta.text() }}
{{ title.text() }}
{{ meta.text() }}
<link rel="stylesheet" href="/global.css">
{{ link.text() }}
{{ style.text() }}
{{ webpackAssets }}
{{ script.text() }}
{{ noscript.text() }}
</head>
<body {{ bodyAttrs.text() }}>
{{ script.text({ pbody: true }) }}
{{ noscript.text({ pbody: true }) }}
<a href="/">&larr; Examples index</a>
{{ app }}
<script src="/__build__/ssr.js"></script>
{{ script.text({ body: true }) }}
{{ noscript.text({ body: true }) }}
</body>
+5
View File
@@ -0,0 +1,5 @@
import createApp from './App'
window.users = []
createApp().$mount('#app')
-3
View File
@@ -1,3 +0,0 @@
import createApp from './app'
createApp().$mount('#app')
-36
View File
@@ -1,36 +0,0 @@
import path from 'path'
import fs from 'fs-extra'
import template from 'lodash/template'
import { createRenderer } from 'vue-server-renderer'
import consola from 'consola'
import createApp from './server-entry'
const renderer = createRenderer()
async function createPage() {
const templateFile = path.resolve(__dirname, 'app.template.html')
const templateContent = await fs.readFile(templateFile, { encoding: 'utf8' })
// see: https://lodash.com/docs#template
const compiled = template(templateContent, { interpolate: /{{([\s\S]+?)}}/g })
const webpackAssets = '<link rel="stylesheet" href="../global.css">'
const serverApp = await createApp()
const appHtml = await renderer.renderToString(serverApp)
const pageHtml = compiled({
app: appHtml,
webpackAssets,
...serverApp.$meta().inject()
})
return pageHtml
}
consola.info(`Creating ssr page`)
createPage()
.then((pageHtml) => {
consola.info(`Done, page:`)
consola.log(pageHtml)
})
.catch(e => consola.error(e))
-3
View File
@@ -1,3 +0,0 @@
import createApp from './app'
export default createApp
+27
View File
@@ -0,0 +1,27 @@
import path from 'path'
import fs from 'fs-extra'
import template from 'lodash/template'
import { createRenderer } from 'vue-server-renderer'
import createApp from './App'
const renderer = createRenderer({ runInNewContext: false })
const templateFile = path.resolve(__dirname, 'app.template.html')
const templateContent = fs.readFileSync(templateFile, { encoding: 'utf8' })
// see: https://lodash.com/docs#template
const compiled = template(templateContent, { interpolate: /{{([\s\S]+?)}}/g })
process.server = true
export async function renderPage () {
const app = await createApp()
const appHtml = await renderer.renderToString(app)
const pageHtml = compiled({
app: appHtml,
...app.$meta().inject()
})
return pageHtml
}
+6 -6
View File
@@ -15,21 +15,21 @@ const ChildComponent = {
<h3>You're looking at the <strong>{{ page }}</strong> page</h3>
<p>Has metaInfo been updated? {{ metaUpdated }}</p>
</div>`,
metaInfo() {
metaInfo () {
return {
title: `${this.page} - ${this.date && this.date.toTimeString()}`,
afterNavigation() {
afterNavigation () {
metaUpdated = 'yes'
}
}
},
data() {
data () {
return {
date: null,
metaUpdated
}
},
mounted() {
mounted () {
setInterval(() => {
this.date = new Date()
}, 1000)
@@ -39,10 +39,10 @@ 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/nuxt/vue-meta/issues/9 for more info.
function view(page) {
function view (page) {
return {
name: `section-${page}`,
render(h) {
render (h) {
return h(ChildComponent, {
props: { page }
})
+7 -7
View File
@@ -36,33 +36,33 @@ export default new Vuex.Store({
// GETTERS
getters: {
isLoading(state) {
isLoading (state) {
return state.isLoading
},
post(state) {
post (state) {
return state.post
},
publishedPosts(state) {
publishedPosts (state) {
return state.posts.filter(post => post.published)
},
publishedPostsCount(state, getters) {
publishedPostsCount (state, getters) {
return getters.publishedPosts.length
}
},
// MUTATIONS
mutations: {
loadingState(state, { isLoading }) {
loadingState (state, { isLoading }) {
state.isLoading = isLoading
},
getPost(state, { slug }) {
getPost (state, { slug }) {
state.post = state.posts.find(post => post.slug === slug)
}
},
// ACTIONS
actions: {
getPost({ commit }, payload) {
getPost ({ commit }, payload) {
commit('loadingState', { isLoading: true })
setTimeout(() => {
commit('getPost', payload)
+5 -5
View File
@@ -36,27 +36,27 @@ export default new Vuex.Store({
// GETTERS
getters: {
post(state) {
post (state) {
return state.post
},
publishedPosts(state) {
publishedPosts (state) {
return state.posts.filter(post => post.published)
},
publishedPostsCount(state, getters) {
publishedPostsCount (state, getters) {
return getters.publishedPosts.length
}
},
// MUTATIONS
mutations: {
getPost(state, { slug }) {
getPost (state, { slug }) {
state.post = state.posts.find(post => post.slug === slug)
}
},
// ACTIONS
actions: {
getPost({ commit }, payload) {
getPost ({ commit }, payload) {
commit('getPost', payload)
}
}
+24 -5
View File
@@ -10,12 +10,16 @@ export default {
devtool: 'inline-source-map',
mode: 'development',
entry: fs.readdirSync(__dirname)
.filter(entry => entry !== 'ssr')
.reduce((entries, dir) => {
const fullDir = path.join(__dirname, dir)
const entry = path.join(fullDir, 'app.js')
if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
entries[dir] = entry
if (dir === 'ssr') {
entries[dir] = path.join(fullDir, 'browser.js')
} else {
const entry = path.join(fullDir, 'app.js')
if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
entries[dir] = entry
}
}
return entries
}, {}),
@@ -27,7 +31,22 @@ export default {
},
module: {
rules: [
{ test: /\.js$/, exclude: /node_modules/, use: 'babel-loader' },
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: '2',
targets: { ie: 9, safari: '5.1' }
}]
]
}
}
},
{ test: /\.vue$/, use: 'vue-loader' }
]
},