From e4e9149b5473ae081cec0a10f354da26d53e63bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Sun, 2 Jul 2017 20:47:01 +0200 Subject: [PATCH] feat: Add dynamic component injection + example --- bin/nuxt-dev | 5 -- .../dynamic-components/articles/article-1.vue | 7 +++ .../dynamic-components/articles/article-2.vue | 7 +++ examples/dynamic-components/package.json | 11 ++++ examples/dynamic-components/pages/_slug.vue | 16 ++++++ examples/dynamic-components/pages/index.vue | 9 +++ examples/global-css/nuxt.config.js | 18 +++--- lib/app/client.js | 19 +++++-- lib/app/server.js | 55 ++++++++++++++++--- lib/core/renderer.js | 12 ++-- package.json | 1 + yarn.lock | 4 ++ 12 files changed, 131 insertions(+), 33 deletions(-) create mode 100644 examples/dynamic-components/articles/article-1.vue create mode 100644 examples/dynamic-components/articles/article-2.vue create mode 100644 examples/dynamic-components/package.json create mode 100644 examples/dynamic-components/pages/_slug.vue create mode 100644 examples/dynamic-components/pages/index.vue diff --git a/bin/nuxt-dev b/bin/nuxt-dev index 19e4b9de9f..419fe79a44 100755 --- a/bin/nuxt-dev +++ b/bin/nuxt-dev @@ -54,11 +54,6 @@ const nuxtConfigFile = resolve(rootDir, argv['config-file']) const nuxtConfig = loadNuxtConfig() _.defaultsDeep(nuxtConfig, { watchers: { chokidar: { ignoreInitial: true } } }) -// Fail if an error happened -process.on('unhandledRejection', function (err) { - throw err -}) - // Start dev let dev = startDev() diff --git a/examples/dynamic-components/articles/article-1.vue b/examples/dynamic-components/articles/article-1.vue new file mode 100644 index 0000000000..a957df1a9b --- /dev/null +++ b/examples/dynamic-components/articles/article-1.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/examples/dynamic-components/articles/article-2.vue b/examples/dynamic-components/articles/article-2.vue new file mode 100644 index 0000000000..0c5b848e0e --- /dev/null +++ b/examples/dynamic-components/articles/article-2.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/examples/dynamic-components/package.json b/examples/dynamic-components/package.json new file mode 100644 index 0000000000..ac286a63f4 --- /dev/null +++ b/examples/dynamic-components/package.json @@ -0,0 +1,11 @@ +{ + "name": "dynamic-component-nuxt", + "dependencies": { + "nuxt": "latest" + }, + "scripts": { + "dev": "nuxt", + "build": "nuxt build", + "start": "nuxt" + } +} diff --git a/examples/dynamic-components/pages/_slug.vue b/examples/dynamic-components/pages/_slug.vue new file mode 100644 index 0000000000..328917b95f --- /dev/null +++ b/examples/dynamic-components/pages/_slug.vue @@ -0,0 +1,16 @@ + + + \ No newline at end of file diff --git a/examples/dynamic-components/pages/index.vue b/examples/dynamic-components/pages/index.vue new file mode 100644 index 0000000000..30a3e45f32 --- /dev/null +++ b/examples/dynamic-components/pages/index.vue @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/examples/global-css/nuxt.config.js b/examples/global-css/nuxt.config.js index f5ae0a61d1..17c82e8892 100644 --- a/examples/global-css/nuxt.config.js +++ b/examples/global-css/nuxt.config.js @@ -1,12 +1,14 @@ -const { join } = require('path') - 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: [ - 'hover.css/css/hover-min.css', 'bulma/bulma.sass', - join(__dirname, 'css/main.css') - ], - build: { - extractCSS: true - } + '~assets/main.css' + ] } diff --git a/lib/app/client.js b/lib/app/client.js index 84eb3d1933..bdf1032952 100644 --- a/lib/app/client.js +++ b/lib/app/client.js @@ -160,9 +160,11 @@ async function render (to, from, next) { return Promise.resolve() } let promises = [] + // Create this context for asyncData & fetch (used for dynamic component injection) + const _this = { components: {} } // asyncData method 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) => { applyAsyncData(Component, asyncDataResult) <%= (loading ? 'this.$loading.increase && this.$loading.increase(30)' : '') %> @@ -170,12 +172,17 @@ async function render (to, from, next) { promises.push(promise) } 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) } <%= (loading ? 'p.then(() => this.$loading.increase && this.$loading.increase(30))' : '') %> promises.push(p) } 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)) <%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %> @@ -332,9 +339,7 @@ function addHotReload ($component, depth) { // Load vue app const NUXT = window.__NUXT__ || {} -if (!NUXT) { - throw new Error('[nuxt.js] cannot find the global variable __NUXT__, make sure the server is working.') -} +NUXT.components = window.__COMPONENTS__ || null // Get matched components const resolveComponents = function (router) { const path = getLocation(router.options.base) @@ -345,6 +350,10 @@ const resolveComponents = function (router) { Component = sanitizeComponent(Component) if (NUXT.serverRendered) { 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 resolve(Component) diff --git a/lib/app/server.js b/lib/app/server.js index 326fa3dfd6..dbf770840c 100644 --- a/lib/app/server.js +++ b/lib/app/server.js @@ -1,6 +1,7 @@ 'use strict' import Vue from 'vue' +import clone from 'clone' import { stringify } from 'querystring' import { omit } from 'lodash' import middleware from './middleware' @@ -43,6 +44,9 @@ export default async (context) => { <%= (store ? 'context.store = store' : '') %> // Add route to the context context.route = router.currentRoute + // Components array (for dynamic components) + context.hasDynamicComponents = false + context.components = [] // Nuxt object context.nuxt = { layout: 'default', data: [], error: null<%= (store ? ', state: null' : '') %>, serverRendered: true } // Add meta infos @@ -133,8 +137,11 @@ export default async (context) => { // Call asyncData & fetch hooks on components matched by the route. let asyncDatas = await Promise.all(Components.map((Component) => { 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') { - let promise = promisify(Component.options.asyncData, ctx) + let promise = promisify(Component.options.asyncData.bind(_this), ctx) // Call asyncData(context) promise.then((asyncDataResult) => { applyAsyncData(Component, asyncDataResult) @@ -142,10 +149,26 @@ export default async (context) => { }) promises.push(promise) } else promises.push(null) - // call fetch(context) - if (Component.options.fetch) promises.push(Component.options.fetch(ctx)) + // Call fetch(context) + if (Component.options.fetch) promises.push(Component.options.fetch.call(_this, ctx)) else promises.push(null) 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 (!Components.length) { @@ -172,10 +195,24 @@ export default async (context) => { await _app.loadLayout(layout) _app.setLayout(layout) 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) +} \ No newline at end of file diff --git a/lib/core/renderer.js b/lib/core/renderer.js index 56865b26b9..b2c0b0ba51 100644 --- a/lib/core/renderer.js +++ b/lib/core/renderer.js @@ -182,10 +182,6 @@ export default class Renderer extends Tapable { // Add webpack middleware only for development if (this.options.dev) { this.useMiddleware(async (req, res, next) => { - if (req.url.includes('.hot-update.json')) { - res.statusCode = 404 - return res.end() - } if (this.webpackDevMiddleware) { await this.webpackDevMiddleware(req, res) } @@ -328,9 +324,13 @@ export default class Renderer extends Tapable { resourceHints = context.renderResourceHints() HEAD += resourceHints } - HEAD += context.renderStyles() - APP += `` + APP += `` APP += context.renderScripts() const html = this.resources.appTemplate({ diff --git a/package.json b/package.json index 215af70b32..8c8337a083 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "babel-preset-vue-app": "^1.2.0", "chalk": "^1.1.3", "chokidar": "^1.7.0", + "clone": "^2.1.1", "compression": "^1.6.2", "connect": "^3.6.2", "css-loader": "^0.28.4", diff --git a/yarn.lock b/yarn.lock index e00ea04df6..a9164763c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1506,6 +1506,10 @@ clone@^1.0.2: version "1.0.2" 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: version "2.0.2" resolved "https://registry.yarnpkg.com/cmd-shim/-/cmd-shim-2.0.2.tgz#6fcbda99483a8fd15d7d30a196ca69d688a2efdb"