Add callback argument (optional) in data

This commit is contained in:
Sébastien Chopin 2016-11-19 22:16:26 +01:00
parent e48f8250a0
commit ae9b41f321
13 changed files with 202 additions and 29 deletions

View File

@ -2,17 +2,19 @@
## data (context)
> Nuxt.js *supercharges* the `data` method from vue.js to let you handle async operation before setting the real component data.
> Nuxt.js *supercharges* the `data` method from vue.js to let you handle async operation before setting the component data.
`data` is called every time before loading the component (*only if attached to a route*). It can be called from the server-side or before navigating to the corresponding route.
The `data` method receives the context as the first argument, you can use it to fetch some data and return the component data. To make the data method asynchronous, **return a Promise**, nuxt.js will wait for the promise to be resolved before rendering the Component.
The `data` method receives the context as the first argument, you can use it to fetch some data and return the component data. To make the data method asynchronous, Nuxt.js offers you 2 ways, choose the one you're the most familiar with:
1. returning a `Promise`, Nuxt.js will wait for the promise to be resolved before rendering the Component
2. Define a second argument which is a callback method to be called like this: `callback(err, data)`
For example:
Example with returning a `Promise`:
```js
export default {
data ({ params }) {
return axios.get(`http://my-api/posts/${params.id}`)
return axios.get(`https://my-api/posts/${params.id}`)
.then((res) => {
return { title: res.data.title }
})
@ -20,6 +22,18 @@ export default {
}
```
Example with using the `callback` argument:
```js
export default {
data ({ params }, callback) {
axios.get(`https://my-api/posts/${params.id}`)
.then((res) => {
callback(null, { title: res.data.title })
})
}
}
```
And then, you can display the data inside your template:
```html
@ -36,12 +50,49 @@ List of all the available keys in `context`:
|-----|------|--------------|-------------|
| `isClient` | Boolean | Client & Server | Boolean to let you know if you're actually renderer from the client-side |
| `isServer` | Boolean | Client & Server | Boolean to let you know if you're actually renderer from the server-side |
| `isDev` | Boolean | Client & Server | Boolean to let you know if you're in dev mode, can be useful for caching some data in production |
| `route` | [vue-router route](https://router.vuejs.org/en/api/route-object.html) | Client & Server | `vue-router` route instance [see documentation](https://router.vuejs.org/en/api/route-object.html) |
| `store` | [vuex store](http://vuex.vuejs.org/en/api.html#vuexstore-instance-properties) | Client & Server | `Vuex.Store` instance. **Available only if `store: true` is set in `nuxt.config.js`** |
| `params` | Object | Client & Server | Alias of route.params |
| `query` | Object | Client & Server | Alias of route.query |
| `req` | [http.Request](https://nodejs.org/api/http.html#http_class_http_incomingmessage) | Server | Request from the node.js server. If nuxt is used as a middleware, the req object might be different depending of the framework you're using. |
| `res` | [http.Response](https://nodejs.org/api/http.html#http_class_http_serverresponse) | Server | Response from the node.js server. If nuxt is used as a middleware, the res object might be different depending of the framework you're using. |
| `redirect` | Function | Client & Server | Use this method to redirect the user to another route, the status code is used on the server-side, default to 302. `redirect([status,] path [, query])` |
| `error` | Function | Client & Server | Use this method to show the error page: `error(params)`. The `params` should have the fields `statusCode` and `message`. |
## Handling errors
Nuxt.js add the `error(params)` method in the `context`, you can call it to display the error page. `params.statusCode` will be also used to render the proper status code form the server-side.
Example with a `Promise`:
```js
export default {
data ({ params, error }) {
return axios.get(`https://my-api/posts/${params.id}`)
.then((res) => {
return { title: res.data.title }
})
.catch((e) => {
error({ statusCode: 404, message: 'Post not found' })
})
}
}
```
If you're using the `callback` argument, you can call it directly with the error, Nuxt.js will call the `error` method for you:
```js
export default {
data ({ params }, callback) {
axios.get(`https://my-api/posts/${params.id}`)
.then((res) => {
callback(null, { title: res.data.title })
})
.catch((e) => {
callback({ statusCode: 404, message: 'Post not found' })
})
}
}
```
## Demo
@ -50,4 +101,4 @@ npm install
npm start
```
Go to [http://localhost:3000](http://localhost:3000) and navigate inside the app.
Go to [http://localhost:3000](http://localhost:3000) and navigate trough the app.

View File

@ -1,21 +1,20 @@
<template>
<div>
<p>{{ userAgent }}!</p>
<p>{{ userAgent }}</p>
<p><router-link to="/post">See a post (http request / Ajax)</router-link></p>
</div>
</template>
<script>
export default {
data ({ req }) {
return new Promise((resolve) => {
setTimeout(function () {
resolve({
userAgent: (req ? req.headers['user-agent'] : navigator.userAgent)
})
}, 1000)
})
data ({ req }, callback) {
setTimeout(function () {
// callback(err, data)
callback(null, {
userAgent: (req ? req.headers['user-agent'] : navigator.userAgent)
})
}, 100)
}
}
</script>

View File

@ -11,10 +11,16 @@ const axios = require('axios')
export default {
data ({ req }) {
// We can return a Promise instead of calling the callback
return axios.get('https://jsonplaceholder.typicode.com/posts/1')
.then((res) => {
return { post: res.data }
})
},
head () {
return {
title: this.post.title
}
}
}
</script>

5
lib/app/App-new.vue Normal file
View File

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

View File

@ -29,8 +29,9 @@ export default {
err = err || null
this.err = err || null
<% if (loading) { %>
if (this.err && this.$loading && this.$loading.fail) {
this.$loading.fail()
if (this.err && this.$loading) {
if (this.$loading.fail) this.$loading.fail()
if (this.$loading.finish) this.$loading.finish()
}
<% } %>
return this.err

View File

@ -4,7 +4,7 @@ require('es6-object-assign').polyfill()
import 'es6-promise/auto'
import Vue from 'vue'
import { app, router<%= (store ? ', store' : '') %> } from './index'
import { getMatchedComponents, flatMapComponents, getContext, getLocation } from './utils'
import { getMatchedComponents, flatMapComponents, getContext, promisify, getLocation } from './utils'
const noopData = () => { return {} }
const noopFetch = () => {}
@ -78,8 +78,7 @@ function render (to, ___, next) {
}
const context = getContext({ to<%= (store ? ', store' : '') %>, isClient: true, next: _next.bind(this), error: this.error.bind(this) })
if (Component._data && typeof Component._data === 'function') {
var promise = Component._data(context)
if (!(promise instanceof Promise)) promise = Promise.resolve(promise)
var promise = promisify(Component._data, context)
promise.then((data) => {
Component.options.data = () => data || {}
Component._dataFn = Component.options.data.toString().replace(/\s/g, '')
@ -117,6 +116,11 @@ function hotReloadAPI (_app) {
_app.$forceUpdate = function () {
let Component = getMatchedComponents(router.currentRoute)[0]
if (!Component) return _forceUpdate()
if (typeof Component === 'object' && !Component.options) {
// Updated via vue-router resolveAsyncComponents()
Component = Vue.extend(Component)
Component._Ctor = Component
}
<%= (loading ? 'this.$loading.start && this.$loading.start()' : '') %>
let promises = []
const next = function (path) {
@ -129,8 +133,7 @@ function hotReloadAPI (_app) {
const newDataFn = (Component._Ctor.options.data || noopData).toString().replace(/\s/g, '')
if (originalDataFn !== newDataFn) {
Component._data = Component._Ctor.options.data || noopData
let p = Component._data(context)
if (!(p instanceof Promise)) { p = Promise.resolve(p) }
let p = promisify(Component._data, context)
p.then((data) => {
Component.options.data = () => data || {}
Component._dataFn = Component.options.data.toString().replace(/\s/g, '')

View File

@ -0,0 +1,16 @@
<template>
<div id="__nuxt">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'nuxt-container',
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

@ -52,7 +52,7 @@ export default {
this.percent = this.percent + Math.floor(num)
return this
},
decrease () {
decrease (num) {
this.percent = this.percent - Math.floor(num)
return this
},
@ -80,8 +80,6 @@ export default {
},
fail () {
this.canSuccess = false
this.percent = 100
this.hide()
return this
}
}

View File

@ -0,0 +1,59 @@
<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>
</div>
</template>
<script>
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
}
},
<% 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
Vue.prototype.$nuxt = this
// add to window so we can listen when ready
if (typeof window !== 'undefined') {
window.$nuxt = this
}
// for NUXT.serverRendered = false
this.$loading = {}
},
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' : '') %>
}
}
</script>

View File

@ -7,6 +7,12 @@ 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'
Vue.component(NuxtContainer.name, NuxtContainer)
Vue.component(Nuxt.name, Nuxt)
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
@ -18,7 +24,7 @@ Vue.use(Meta, {
require('<%= pluginPath %>')
<% }) %>
import App from './App.vue'
import App from '<%= appPath %>'
// create the app instance.
// here we inject the router and store to all child components,
// making them available everywhere as `this.$router` and `this.$store`.

View File

@ -5,7 +5,7 @@ import Vue from 'vue'
import { stringify } from 'querystring'
import { omit } from 'lodash'
import { app, router<%= (store ? ', store' : '') %> } from './index'
import { getMatchedComponents, getContext, urlJoin } from './utils'
import { getMatchedComponents, getContext, promisify, urlJoin } from './utils'
const isDev = <%= isDev %>
const _app = new Vue(app)
@ -68,10 +68,10 @@ export default context => {
Component._Ctor = Component
Component.extendOptions = Component.options
}
const ctx = getContext(context)
if (Component.options.data && typeof Component.options.data === 'function') {
Component._data = Component.options.data
var promise = Component._data(getContext(context))
if (!(promise instanceof Promise)) promise = Promise.resolve(promise)
let promise = promisify(Component._data, ctx)
promise.then((data) => {
Component.options.data = () => data
Component._Ctor.options.data = Component.options.data
@ -81,7 +81,7 @@ export default context => {
promises.push(null)
}
if (Component.options.fetch) {
promises.push(Component.options.fetch(getContext(context)))
promises.push(Component.options.fetch(ctx))
}
return Promise.all(promises)
}))

View File

@ -20,6 +20,7 @@ export function getContext (context) {
let ctx = {
isServer: !!context.isServer,
isClient: !!context.isClient,
isDev: <%= isDev %>,
<%= (store ? 'store: context.store,' : '') %>
route: (context.to ? context.to : context.route),
error: context.error
@ -45,6 +46,28 @@ export function getContext (context) {
return ctx
}
export function promisify (fn, context) {
let promise
if (fn.length === 2) {
// fn(context, callback)
promise = new Promise((resolve) => {
fn(getContext(context), function (err, data) {
if (err) {
context.error(err)
}
data = data || {}
resolve(data)
})
})
} else {
promise = fn(getContext(context))
}
if (!(promise instanceof Promise)) {
promise = Promise.resolve(promise)
}
return promise
}
// Imported from vue-router
export function getLocation (base) {
var path = window.location.pathname

View File

@ -146,6 +146,8 @@ function * generateRoutesAndFiles () {
'router.js',
'server.js',
'utils.js',
'components/nuxt-container.vue',
'components/nuxt.vue',
'components/nuxt-loading.vue'
]
let templateVars = {
@ -159,6 +161,7 @@ function * generateRoutesAndFiles () {
store: this.options.store,
css: this.options.css,
plugins: this.options.plugins.map((p) => r(this.dir, p)),
appPath: './App.vue',
loading: (typeof this.options.loading === 'string' ? r(this.dir, this.options.loading) : this.options.loading),
components: {
Loading: r(__dirname, '..', 'app', 'components', 'nuxt-loading.vue'),
@ -175,6 +178,9 @@ function * generateRoutesAndFiles () {
route._name = '_' + hash(route._component)
route.component = route._name
})
if (files.includes('pages/_app.vue')) {
templateVars.appPath = r(this.dir, 'pages/_app.vue')
}
if (this.dev && files.includes('pages/_error-debug.vue')) {
templateVars.components.ErrorPage = r(this.dir, 'pages/_error-debug.vue')
}