Add pages/_app.vue possibility + example

This commit is contained in:
Sébastien Chopin 2016-11-21 14:14:35 +01:00
parent cb20e417fa
commit edd0227e74
19 changed files with 159 additions and 105 deletions

View File

@ -0,0 +1,42 @@
# Extending the main app
> Nuxt.js allows you to extend the main application by adding a `pages/_app.vue` file
## The default app
The default source code of the main app is:
```html
<template>
<nuxt-container>
<nuxt/>
</nuxt-container>
</template>
```
## The `pages/_app.vue` file
### 🎬 [Example video](https://www.youtube.com/watch?v=wBhia7uBxDA)
You have to make sure to add the `<nuxt-container>` and `<nuxt>` components when extending the app.
It is important that the code you add stays inside `<nuxt-container>`.
Example:
```html
<template>
<nuxt-container>
<div>My navigation bar here</div>
<nuxt/>
</nuxt-container>
</template>
```
## Demo
```bash
npm install
npm start
```
Go to [http://localhost:3000](http://localhost:3000) and navigate trough the app. Notice how the logo at the top right stays between the pages, even on the error page: [http://localhost:3000/404](http://localhost:3000/404)

View File

@ -0,0 +1,11 @@
{
"name": "nuxt-extend-app",
"dependencies": {
"nuxt": "latest"
},
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start"
}
}

View File

@ -0,0 +1,15 @@
<template>
<nuxt-container>
<img src="logo.png"/>
<nuxt/>
</nuxt-container>
</template>
<style scoped>
img {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
</style>

View File

@ -0,0 +1,16 @@
<template>
<div>
<p>Hi from {{ name }}</p>
<router-link to="/">Home page</router-link>
</div>
</template>
<script>
export default {
data ({ req }) {
return {
name: req ? 'server' : 'client'
}
}
}
</script>

View File

@ -0,0 +1,6 @@
<template>
<div class="container">
<h1>Welcome!</h1>
<router-link to="/about">About page</router-link>
</div>
</template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -3,7 +3,7 @@
</template>
<script>
// https://cards-dev.twitter.com/validator
// Test on: https://cards-dev.twitter.com/validator
export default {
data: () => ({
href: 'https://twitter.com/intent/tweet?url=' + encodeURIComponent('https://head-elements.now.sh/about')

View File

@ -1,5 +0,0 @@
<template>
<nuxt-container>
<nuxt></nuxt>
</nuxt-container>
</template>

View File

@ -1,49 +1,5 @@
<template>
<div id="__nuxt">
<% if (loading) { %><nuxt-loading ref="loading"></nuxt-loading><% } %>
<router-view v-if="!err"></router-view>
<nuxt-error v-if="err" :error="err"></nuxt-error>
</div>
<nuxt-container>
<nuxt/>
</nuxt-container>
</template>
<script>
import NuxtError from '<%= components.ErrorPage %>'
<% if (loading) { %>import NuxtLoading from '<%= (typeof loading === "string" ? loading : "./components/nuxt-loading.vue") %>'<% } %>
export default {
data () {
return {
err: null
}
},
<% if (loading) { %>
created () {
this.$loading = {} // for NUXT.serverRendered = false
},
mounted () {
this.$loading = this.$refs.loading
},
<% } %>
methods: {
error (err) {
err = err || null
this.err = err || null
<% if (loading) { %>
if (this.err && this.$loading) {
if (this.$loading.fail) this.$loading.fail()
if (this.$loading.finish) this.$loading.finish()
}
<% } %>
return this.err
}
},
components: {
NuxtError<%= (loading ? ',\n\t\tNuxtLoading' : '') %>
},
head: <%= JSON.stringify(head) %>
}
</script>
<% css.forEach(function (c) { %>
<style src="<%= (typeof c === 'string' ? c : c.src) %>" lang="<%= (c.lang ? c.lang : 'css') %>"></style>
<% }) %>

View File

@ -112,8 +112,9 @@ function render (to, ___, next) {
// Special hot reload with data(context)
function hotReloadAPI (_app) {
var _forceUpdate = _app.$forceUpdate.bind(_app)
_app.$forceUpdate = function () {
const $nuxt = _app.$nuxt
var _forceUpdate = $nuxt.$forceUpdate.bind($nuxt)
$nuxt.$forceUpdate = function () {
let Component = getMatchedComponents(router.currentRoute)[0]
if (!Component) return _forceUpdate()
if (typeof Component === 'object' && !Component.options) {
@ -127,7 +128,7 @@ function hotReloadAPI (_app) {
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
router.push(path)
}
const context = getContext({ route: router.currentRoute<%= (store ? ', store' : '') %>, isClient: true, next: next.bind(this), error: _app.error.bind(_app) })
const context = getContext({ route: router.currentRoute<%= (store ? ', store' : '') %>, isClient: true, next: next.bind(this), error: _app.error })
// Check if data has been updated
const originalDataFn = (Component._data || noopData).toString().replace(/\s/g, '')
const newDataFn = (Component._Ctor.options.data || noopData).toString().replace(/\s/g, '')
@ -206,17 +207,27 @@ const resolveComponents = flatMapComponents(router.match(path), (Component, _, m
Promise.all(resolveComponents)
.then((Components) => {
const _app = new Vue(app)
const mountApp = () => _app.$mount('#__nuxt')
const onNuxtReady = window.onNuxtReady || function () {}
if (NUXT.error) _app.error(NUXT.error)
const mountApp = () => {
_app.$mount('#__nuxt')
<% if (loading) { %>
// Special loading bar
_app.$loading = _app.$nuxt.$loading
<% } %>
// Hot reloading
if (module.hot) hotReloadAPI(_app)
// Useful for jsdom testing or plugins (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading)
if (typeof window.onNuxtReady === 'function') {
window.onNuxtReady(_app)
}
}
_app.error = _app.$options._nuxt.error.bind(_app)
_app.$loading = {} // to avoid error while _app.$nuxt does not exist
if (NUXT.error) _app.error(NUXT.error)
// Add router hooks
router.beforeEach(loadAsyncComponents.bind(_app))
router.beforeEach(render.bind(_app))
if (NUXT.serverRendered) {
mountApp()
// Call window.onModulesLoaded for jsdom testing (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading)
onNuxtReady(_app)
return
}
render.call(_app, router.currentRoute, router.currentRoute, function (path) {
@ -226,13 +237,11 @@ Promise.all(resolveComponents)
if (mounted) return
mounted = true
mountApp()
onNuxtReady(_app)
})
router.push(path)
return
}
mountApp()
onNuxtReady(_app)
})
})
.catch((err) => {

View File

@ -13,6 +13,7 @@
<script>
export default {
name: 'nuxt-error',
props: ['error'],
head () {
return {

View File

@ -12,6 +12,7 @@
<script>
export default {
name: 'nuxt-error',
props: ['error'],
head () {
return {

View File

@ -8,9 +8,10 @@
</template>
<script>
const Vue = require('vue')
import Vue from 'vue'
export default {
name: 'nuxt-loading',
data () {
return {
percent: 0,

View File

@ -1,57 +1,49 @@
<template>
<div>
<% if (loading) { %><nuxt-loading ref="loading"></nuxt-loading><% } %>
<transition>
<router-view v-if="!err"></router-view>
<nuxt-error v-if="err" :error="err"></nuxt-error>
<transition mode="out-in">
<router-view v-if="!nuxt.err"></router-view>
<nuxt-error v-if="nuxt.err" :error="nuxt.err"></nuxt-error>
</transition>
</div>
</template>
<script>
import Vue from 'vue'
import NuxtError from '<%= components.ErrorPage %>'
<% if (loading) { %>import NuxtLoading from '<%= (typeof loading === "string" ? loading : "./nuxt-loading.vue") %>'<% } %>
export default {
name: 'nuxt',
data () {
return {
err: null
}
beforeCreate () {
Vue.util.defineReactive(this, 'nuxt', this.$root.$options._nuxt)
},
<% if (loading) { %>
created () {
if (this.$root.$nuxt) {
return console.error('Only one instance of Nuxt.js is possible, make sure to use <nuxt></nuxt> only once.')
}
// Add $nuxt in the root instance
this.$root.$nuxt = this
// add vm.$nuxt to child instances
// Add this.$nuxt in child instances
Vue.prototype.$nuxt = this
// Add this.$root.$nuxt
this.$root.$nuxt = this
// add to window so we can listen when ready
if (typeof window !== 'undefined') {
window.$nuxt = this
}
// for NUXT.serverRendered = false
this.$loading = {}
},
<% if (loading) { %>
mounted () {
this.$loading = this.$refs.loading
},
<% } %>
watch: {
'nuxt.err': 'errorChanged'
},
methods: {
error (err) {
err = err || null
this.err = err || null
<% if (loading) { %>
if (this.err && this.$loading) {
errorChanged () {
if (this.nuxt.err && this.$loading) {
if (this.$loading.fail) this.$loading.fail()
if (this.$loading.finish) this.$loading.finish()
}
<% } %>
return this.err
}
},
<% } %>
components: {
NuxtError<%= (loading ? ',\n\t\tNuxtLoading' : '') %>
}

View File

@ -1,18 +1,19 @@
'use strict'
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import Meta from 'vue-meta/lib/vue-meta.js' // require the ES2015 lib
import router from './router.js'
<% if (store) { %>import store from '~store/index.js'<% } %>
import NuxtContainer from './components/nuxt-container.vue'
import Nuxt from './components/nuxt.vue'
import App from '<%= appPath %>'
// Component: <nuxt-container>
Vue.component(NuxtContainer.name, NuxtContainer)
// Component: <nuxt>
Vue.component(Nuxt.name, Nuxt)
// vue-meta configuration
Vue.use(Meta, {
keyName: 'head', // the component option name that vue-meta looks for meta info on.
attribute: 'n-head', // the attribute name vue-meta adds to the tags it observes
@ -20,17 +21,25 @@ Vue.use(Meta, {
tagIDKeyName: 'hid' // the property name that vue-meta uses to determine whether to overwrite or append a tag
})
// Includes external plugins
<% plugins.forEach(function (pluginPath) { %>
require('<%= pluginPath %>')
<% }) %>
import App from '<%= appPath %>'
// create the app instance.
// root instance
// here we inject the router and store to all child components,
// making them available everywhere as `this.$router` and `this.$store`.
const app = {
router,
<%= (store ? 'store,' : '') %>
_nuxt: {
err: null,
error (err) {
err = err || null
this.$options._nuxt.err = err;
return err
}
},
...App
}

View File

@ -10,7 +10,7 @@ import { getMatchedComponents, getContext, promisify, urlJoin } from './utils'
const isDev = <%= isDev %>
const _app = new Vue(app)
// Fix issue from vue-router Abstract mode with base (use for server-side rendering)
// Fix issue from vue-router Abstract mode with base (used for server-side rendering)
router.history.base = '<%= router.base %>'
// This exported function will be called by `bundleRenderer`.
@ -46,7 +46,7 @@ export default context => {
// Add meta infos
context.meta = _app.$meta()
// Error function
context.error = _app.error.bind(_app)
context.error = _app.$options._nuxt.error.bind(_app)
<%= (isDev ? 'const s = isDev && Date.now()' : '') %>
let Components = getMatchedComponents(context.route)
@ -88,7 +88,7 @@ export default context => {
})
.then((res) => {
if (!Components.length) {
context.nuxt.error = _app.error({ statusCode: 404, message: 'This page could not be found.', url: context.route.path })
context.nuxt.error = context.error({ statusCode: 404, message: 'This page could not be found.', url: context.route.path })
<%= (store ? 'context.nuxt.state = store.state' : '') %>
return _app
}
@ -97,13 +97,13 @@ export default context => {
<% } %>
// datas are the first row of each
context.nuxt.data = res.map((tab) => tab[0])
context.nuxt.error = _app.err
context.nuxt.error = _app.$options._nuxt.err
<%= (store ? '// Add the state from the vuex store' : '') %>
<%= (store ? 'context.nuxt.state = store.state' : '') %>
return _app
})
.catch(function (error) {
context.nuxt.error = _app.error(error)
context.nuxt.error = context.error(error)
<%= (store ? 'context.nuxt.state = store.state' : '') %>
return _app
})

View File

@ -25,6 +25,7 @@ export function getContext (context) {
route: (context.to ? context.to : context.route),
error: context.error
}
const next = context.next
ctx.params = ctx.route.params || {}
ctx.query = ctx.route.query || {}
ctx.redirect = function (status, path, query) {
@ -35,7 +36,7 @@ export function getContext (context) {
path = status
status = 302
}
context.next({
next({
path: path,
query: query,
status: status
@ -51,7 +52,7 @@ export function promisify (fn, context) {
if (fn.length === 2) {
// fn(context, callback)
promise = new Promise((resolve) => {
fn(getContext(context), function (err, data) {
fn(context, function (err, data) {
if (err) {
context.error(err)
}
@ -60,7 +61,7 @@ export function promisify (fn, context) {
})
})
} else {
promise = fn(getContext(context))
promise = fn(context)
}
if (!(promise instanceof Promise)) {
promise = Promise.resolve(promise)

View File

@ -27,10 +27,10 @@ const wp = function (p) {
}
const r = function () {
let args = Array.from(arguments)
args = args.map(normalize)
if (_.last(args).includes('~')) {
return wp(_.last(args))
}
args = args.map(normalize)
return wp(resolve.apply(null, args))
}
@ -184,7 +184,6 @@ function * generateRoutesAndFiles () {
templateVars.loading = templateVars.loading + '.vue'
}
// Format routes for the lib/app/router.js template
// TODO: check .children
templateVars.router.routes.forEach((route) => {
route._component = route.component
route._name = '_' + hash(route._component)
@ -308,7 +307,7 @@ function createRenderer (bundle) {
function watchPages () {
const patterns = [ r(this.dir, 'pages/*.vue'), r(this.dir, 'pages/**/*.vue') ]
const options = {
ignored: '**/_*.vue',
// ignored: '**/_*.vue',
ignoreInitial: true
}
const refreshFiles = _.debounce(() => {

View File

@ -1,6 +1,6 @@
{
"name": "nuxt",
"version": "0.5.3",
"version": "0.6.0",
"description": "A minimalistic framework for server-rendered Vue.js applications (inspired by Next.js)",
"main": "index.js",
"license": "MIT",