mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-23 14:15:13 +00:00
feat: Add dynamic component injection + example
This commit is contained in:
parent
97e873bb6b
commit
e4e9149b54
@ -54,11 +54,6 @@ const nuxtConfigFile = resolve(rootDir, argv['config-file'])
|
|||||||
const nuxtConfig = loadNuxtConfig()
|
const nuxtConfig = loadNuxtConfig()
|
||||||
_.defaultsDeep(nuxtConfig, { watchers: { chokidar: { ignoreInitial: true } } })
|
_.defaultsDeep(nuxtConfig, { watchers: { chokidar: { ignoreInitial: true } } })
|
||||||
|
|
||||||
// Fail if an error happened
|
|
||||||
process.on('unhandledRejection', function (err) {
|
|
||||||
throw err
|
|
||||||
})
|
|
||||||
|
|
||||||
// Start dev
|
// Start dev
|
||||||
let dev = startDev()
|
let dev = startDev()
|
||||||
|
|
||||||
|
7
examples/dynamic-components/articles/article-1.vue
Normal file
7
examples/dynamic-components/articles/article-1.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Article 1</h1>
|
||||||
|
<p>Hello I am the first article :)</p>
|
||||||
|
<nuxt-link to="/">Home</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
7
examples/dynamic-components/articles/article-2.vue
Normal file
7
examples/dynamic-components/articles/article-2.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Article 2</h1>
|
||||||
|
<p>Hello I am the second article!</p>
|
||||||
|
<nuxt-link to="/">Home</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
11
examples/dynamic-components/package.json
Normal file
11
examples/dynamic-components/package.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "dynamic-component-nuxt",
|
||||||
|
"dependencies": {
|
||||||
|
"nuxt": "latest"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nuxt",
|
||||||
|
"build": "nuxt build",
|
||||||
|
"start": "nuxt"
|
||||||
|
}
|
||||||
|
}
|
16
examples/dynamic-components/pages/_slug.vue
Normal file
16
examples/dynamic-components/pages/_slug.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<BlogArticle/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
async asyncData({ params, error }) {
|
||||||
|
const slug = params.slug || 'hello'
|
||||||
|
try {
|
||||||
|
this.components.BlogArticle = await import(`~/articles/${slug}.vue`)
|
||||||
|
} catch (e) {
|
||||||
|
error({ statusCode: 404, message: 'Article not found' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
9
examples/dynamic-components/pages/index.vue
Normal file
9
examples/dynamic-components/pages/index.vue
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>Articles</h1>
|
||||||
|
<ul>
|
||||||
|
<li><nuxt-link to="/article-1">Article #1</nuxt-link></li>
|
||||||
|
<li><nuxt-link to="/article-2">Article #2</nuxt-link></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -1,12 +1,14 @@
|
|||||||
const { join } = require('path')
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
head: {
|
||||||
|
titleTemplate: '%s - Nuxt.js',
|
||||||
|
meta: [
|
||||||
|
{ charset: 'utf-8' },
|
||||||
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
|
{ hid: 'description', name: 'description', content: 'Meta description' }
|
||||||
|
]
|
||||||
|
},
|
||||||
css: [
|
css: [
|
||||||
'hover.css/css/hover-min.css',
|
|
||||||
'bulma/bulma.sass',
|
'bulma/bulma.sass',
|
||||||
join(__dirname, 'css/main.css')
|
'~assets/main.css'
|
||||||
],
|
]
|
||||||
build: {
|
|
||||||
extractCSS: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -160,9 +160,11 @@ async function render (to, from, next) {
|
|||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
let promises = []
|
let promises = []
|
||||||
|
// Create this context for asyncData & fetch (used for dynamic component injection)
|
||||||
|
const _this = { components: {} }
|
||||||
// asyncData method
|
// asyncData method
|
||||||
if (Component.options.asyncData && typeof Component.options.asyncData === 'function') {
|
if (Component.options.asyncData && typeof Component.options.asyncData === 'function') {
|
||||||
var promise = promisify(Component.options.asyncData, context)
|
var promise = promisify(Component.options.asyncData.bind(_this), context)
|
||||||
promise.then((asyncDataResult) => {
|
promise.then((asyncDataResult) => {
|
||||||
applyAsyncData(Component, asyncDataResult)
|
applyAsyncData(Component, asyncDataResult)
|
||||||
<%= (loading ? 'this.$loading.increase && this.$loading.increase(30)' : '') %>
|
<%= (loading ? 'this.$loading.increase && this.$loading.increase(30)' : '') %>
|
||||||
@ -170,12 +172,17 @@ async function render (to, from, next) {
|
|||||||
promises.push(promise)
|
promises.push(promise)
|
||||||
}
|
}
|
||||||
if (Component.options.fetch) {
|
if (Component.options.fetch) {
|
||||||
var p = Component.options.fetch(context)
|
var p = Component.options.fetch.call(_this, context)
|
||||||
if (!p || (!(p instanceof Promise) && (typeof p.then !== 'function'))) { p = Promise.resolve(p) }
|
if (!p || (!(p instanceof Promise) && (typeof p.then !== 'function'))) { p = Promise.resolve(p) }
|
||||||
<%= (loading ? 'p.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %>
|
<%= (loading ? 'p.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %>
|
||||||
promises.push(p)
|
promises.push(p)
|
||||||
}
|
}
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
|
.then(() => {
|
||||||
|
Object.keys(_this.components).forEach((name) => {
|
||||||
|
Component.options.components[name] = _this.components[name]
|
||||||
|
})
|
||||||
|
})
|
||||||
}))
|
}))
|
||||||
_lastPaths = Components.map((Component, i) => compile(to.matched[i].path)(to.params))
|
_lastPaths = Components.map((Component, i) => compile(to.matched[i].path)(to.params))
|
||||||
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
|
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
|
||||||
@ -332,9 +339,7 @@ function addHotReload ($component, depth) {
|
|||||||
|
|
||||||
// Load vue app
|
// Load vue app
|
||||||
const NUXT = window.__NUXT__ || {}
|
const NUXT = window.__NUXT__ || {}
|
||||||
if (!NUXT) {
|
NUXT.components = window.__COMPONENTS__ || null
|
||||||
throw new Error('[nuxt.js] cannot find the global variable __NUXT__, make sure the server is working.')
|
|
||||||
}
|
|
||||||
// Get matched components
|
// Get matched components
|
||||||
const resolveComponents = function (router) {
|
const resolveComponents = function (router) {
|
||||||
const path = getLocation(router.options.base)
|
const path = getLocation(router.options.base)
|
||||||
@ -345,6 +350,10 @@ const resolveComponents = function (router) {
|
|||||||
Component = sanitizeComponent(Component)
|
Component = sanitizeComponent(Component)
|
||||||
if (NUXT.serverRendered) {
|
if (NUXT.serverRendered) {
|
||||||
applyAsyncData(Component, NUXT.data[index])
|
applyAsyncData(Component, NUXT.data[index])
|
||||||
|
if (NUXT.components) {
|
||||||
|
Component.options.components = Object.assign(Component.options.components, NUXT.components[index])
|
||||||
|
}
|
||||||
|
Component._Ctor = Component
|
||||||
}
|
}
|
||||||
match.components[key] = Component
|
match.components[key] = Component
|
||||||
resolve(Component)
|
resolve(Component)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import clone from 'clone'
|
||||||
import { stringify } from 'querystring'
|
import { stringify } from 'querystring'
|
||||||
import { omit } from 'lodash'
|
import { omit } from 'lodash'
|
||||||
import middleware from './middleware'
|
import middleware from './middleware'
|
||||||
@ -43,6 +44,9 @@ export default async (context) => {
|
|||||||
<%= (store ? 'context.store = store' : '') %>
|
<%= (store ? 'context.store = store' : '') %>
|
||||||
// Add route to the context
|
// Add route to the context
|
||||||
context.route = router.currentRoute
|
context.route = router.currentRoute
|
||||||
|
// Components array (for dynamic components)
|
||||||
|
context.hasDynamicComponents = false
|
||||||
|
context.components = []
|
||||||
// Nuxt object
|
// Nuxt object
|
||||||
context.nuxt = { layout: 'default', data: [], error: null<%= (store ? ', state: null' : '') %>, serverRendered: true }
|
context.nuxt = { layout: 'default', data: [], error: null<%= (store ? ', state: null' : '') %>, serverRendered: true }
|
||||||
// Add meta infos
|
// Add meta infos
|
||||||
@ -133,8 +137,11 @@ export default async (context) => {
|
|||||||
// Call asyncData & fetch hooks on components matched by the route.
|
// Call asyncData & fetch hooks on components matched by the route.
|
||||||
let asyncDatas = await Promise.all(Components.map((Component) => {
|
let asyncDatas = await Promise.all(Components.map((Component) => {
|
||||||
let promises = []
|
let promises = []
|
||||||
|
// Create this context for asyncData & fetch (used for dynamic component injection)
|
||||||
|
const _this = { components: {} }
|
||||||
|
// Call asyncData
|
||||||
if (Component.options.asyncData && typeof Component.options.asyncData === 'function') {
|
if (Component.options.asyncData && typeof Component.options.asyncData === 'function') {
|
||||||
let promise = promisify(Component.options.asyncData, ctx)
|
let promise = promisify(Component.options.asyncData.bind(_this), ctx)
|
||||||
// Call asyncData(context)
|
// Call asyncData(context)
|
||||||
promise.then((asyncDataResult) => {
|
promise.then((asyncDataResult) => {
|
||||||
applyAsyncData(Component, asyncDataResult)
|
applyAsyncData(Component, asyncDataResult)
|
||||||
@ -142,10 +149,26 @@ export default async (context) => {
|
|||||||
})
|
})
|
||||||
promises.push(promise)
|
promises.push(promise)
|
||||||
} else promises.push(null)
|
} else promises.push(null)
|
||||||
// call fetch(context)
|
// Call fetch(context)
|
||||||
if (Component.options.fetch) promises.push(Component.options.fetch(ctx))
|
if (Component.options.fetch) promises.push(Component.options.fetch.call(_this, ctx))
|
||||||
else promises.push(null)
|
else promises.push(null)
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
|
.then((data) => {
|
||||||
|
// If not dyanmic component, return data directly
|
||||||
|
if (Object.keys(_this.components).length === 0) return data
|
||||||
|
// Tell renderer that dynamic components has been added
|
||||||
|
context.hasDynamicComponents = true
|
||||||
|
// Add Component on server side (clone of it)
|
||||||
|
Component.options.components = {
|
||||||
|
...Component.options.components,
|
||||||
|
...clone(_this.components) // Clone it to avoid vue to overwrite references
|
||||||
|
}
|
||||||
|
// Add components into __NUXT__ for client-side hydration
|
||||||
|
// We clone it since vue-server-renderer will update the component definition
|
||||||
|
context.components.push(sanitizeDynamicComponents(_this.components))
|
||||||
|
// Return data to server-render them
|
||||||
|
return data
|
||||||
|
})
|
||||||
}))
|
}))
|
||||||
// If no Components found, returns 404
|
// If no Components found, returns 404
|
||||||
if (!Components.length) {
|
if (!Components.length) {
|
||||||
@ -172,10 +195,24 @@ export default async (context) => {
|
|||||||
await _app.loadLayout(layout)
|
await _app.loadLayout(layout)
|
||||||
_app.setLayout(layout)
|
_app.setLayout(layout)
|
||||||
return _app
|
return _app
|
||||||
// if (typeof error === 'string') {
|
|
||||||
// error = { statusCode: 500, message: error }
|
|
||||||
// }
|
|
||||||
// context.nuxt.error = context.error(error)
|
|
||||||
// <%= (store ? 'context.nuxt.state = store.state' : '') %>
|
|
||||||
// return _app
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeDynamicComponents(components) {
|
||||||
|
Object.keys(components).forEach((name) => {
|
||||||
|
const component = components[name]
|
||||||
|
// Remove SSR register hookd
|
||||||
|
if (Array.isArray(component.beforeCreate)) {
|
||||||
|
component.beforeCreate = component.beforeCreate.filter((fn) => fn !== component._ssrRegister)
|
||||||
|
if (!component.beforeCreate.length) delete component.beforeCreate
|
||||||
|
}
|
||||||
|
// Remove SSR & informations properties
|
||||||
|
delete component._ssrRegister
|
||||||
|
delete component.__file
|
||||||
|
if (component.staticRenderFns && !component.staticRenderFns.length) {
|
||||||
|
delete component.staticRenderFns
|
||||||
|
}
|
||||||
|
// Add Component to NUXT.components[i][name]
|
||||||
|
components[name] = component
|
||||||
|
})
|
||||||
|
return clone(components)
|
||||||
|
}
|
@ -182,10 +182,6 @@ export default class Renderer extends Tapable {
|
|||||||
// Add webpack middleware only for development
|
// Add webpack middleware only for development
|
||||||
if (this.options.dev) {
|
if (this.options.dev) {
|
||||||
this.useMiddleware(async (req, res, next) => {
|
this.useMiddleware(async (req, res, next) => {
|
||||||
if (req.url.includes('.hot-update.json')) {
|
|
||||||
res.statusCode = 404
|
|
||||||
return res.end()
|
|
||||||
}
|
|
||||||
if (this.webpackDevMiddleware) {
|
if (this.webpackDevMiddleware) {
|
||||||
await this.webpackDevMiddleware(req, res)
|
await this.webpackDevMiddleware(req, res)
|
||||||
}
|
}
|
||||||
@ -328,9 +324,13 @@ export default class Renderer extends Tapable {
|
|||||||
resourceHints = context.renderResourceHints()
|
resourceHints = context.renderResourceHints()
|
||||||
HEAD += resourceHints
|
HEAD += resourceHints
|
||||||
}
|
}
|
||||||
|
|
||||||
HEAD += context.renderStyles()
|
HEAD += context.renderStyles()
|
||||||
APP += `<script type="text/javascript">window.__NUXT__=${serialize(context.nuxt, { isJSON: true })}</script>`
|
APP += `<script type="text/javascript">`
|
||||||
|
APP += `window.__NUXT__=${serialize(context.nuxt, { isJSON: true })};`
|
||||||
|
if (context.hasDynamicComponents) {
|
||||||
|
APP += `window.__COMPONENTS__=${serialize(context.components)};`
|
||||||
|
}
|
||||||
|
APP += `</script>`
|
||||||
APP += context.renderScripts()
|
APP += context.renderScripts()
|
||||||
|
|
||||||
const html = this.resources.appTemplate({
|
const html = this.resources.appTemplate({
|
||||||
|
@ -74,6 +74,7 @@
|
|||||||
"babel-preset-vue-app": "^1.2.0",
|
"babel-preset-vue-app": "^1.2.0",
|
||||||
"chalk": "^1.1.3",
|
"chalk": "^1.1.3",
|
||||||
"chokidar": "^1.7.0",
|
"chokidar": "^1.7.0",
|
||||||
|
"clone": "^2.1.1",
|
||||||
"compression": "^1.6.2",
|
"compression": "^1.6.2",
|
||||||
"connect": "^3.6.2",
|
"connect": "^3.6.2",
|
||||||
"css-loader": "^0.28.4",
|
"css-loader": "^0.28.4",
|
||||||
|
@ -1506,6 +1506,10 @@ clone@^1.0.2:
|
|||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
|
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
|
||||||
|
|
||||||
|
clone@^2.1.1:
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb"
|
||||||
|
|
||||||
cmd-shim@^2.0.2:
|
cmd-shim@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb"
|
resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb"
|
||||||
|
Loading…
Reference in New Issue
Block a user