diff --git a/README.md b/README.md index 3bccddeef5..03167abc4f 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Learn more: https://nuxtjs.org/api/nuxt ## Using nuxt.js as a middleware -You might want to use your own server with you configurations, your API and everything awesome your created with. That's why you can use nuxt.js as a middleware. It's recommended to use it at the end of your middlewares since it will handle the rendering of your web application and won't call next(). +You might want to use your own server with you configurations, your API and everything awesome your created with. That's why you can use nuxt.js as a middleware. It's recommended to use it at the end of your middleware since it will handle the rendering of your web application and won't call next(). ```js app.use(nuxt.render) diff --git a/bin/nuxt-generate b/bin/nuxt-generate index 8feabf3c0c..10b1c4ff70 100755 --- a/bin/nuxt-generate +++ b/bin/nuxt-generate @@ -17,7 +17,7 @@ if (fs.existsSync(nuxtConfigFile)) { if (typeof options.rootDir !== 'string') { options.rootDir = rootDir } -options.dev = false // Force production mode (no webpack middlewares called) +options.dev = false // Force production mode (no webpack middleware called) console.log('[nuxt] Generating...') // eslint-disable-line no-console var nuxt = new Nuxt(options) diff --git a/bin/nuxt-start b/bin/nuxt-start index 0f9aef3577..9190178576 100755 --- a/bin/nuxt-start +++ b/bin/nuxt-start @@ -14,7 +14,7 @@ if (fs.existsSync(nuxtConfigFile)) { if (typeof options.rootDir !== 'string') { options.rootDir = rootDir } -options.dev = false // Force production mode (no webpack middlewares called) +options.dev = false // Force production mode (no webpack middleware called) var nuxt = new Nuxt(options) diff --git a/examples/auth-routes/middleware/auth.js b/examples/auth-routes/middleware/auth.js new file mode 100644 index 0000000000..2aeeafef6b --- /dev/null +++ b/examples/auth-routes/middleware/auth.js @@ -0,0 +1,10 @@ +export default function ({ store, redirect, error }) { + // If user not connected, redirect to / + if (!store.state.authUser) { + // return redirect('/') + error({ + message: 'You are not connected', + statusCode: 403 + }) + } +} diff --git a/examples/auth-routes/nuxt.config.js b/examples/auth-routes/nuxt.config.js index 0434d79bcc..6927fa17d8 100644 --- a/examples/auth-routes/nuxt.config.js +++ b/examples/auth-routes/nuxt.config.js @@ -6,6 +6,5 @@ module.exports = { { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', content: 'Auth Routes example' } ] - }, - loading: { color: '#3B8070' } + } } diff --git a/examples/auth-routes/package.json b/examples/auth-routes/package.json index 87677b2922..3cd143afbf 100644 --- a/examples/auth-routes/package.json +++ b/examples/auth-routes/package.json @@ -2,12 +2,12 @@ "name": "auth-routes", "description": "", "dependencies": { + "axios": "^0.15.3", "body-parser": "^1.15.2", "cross-env": "^3.1.3", "express": "^4.14.0", "express-session": "^1.14.2", - "nuxt": "latest", - "whatwg-fetch": "^2.0.1" + "nuxt": "latest" }, "scripts": { "dev": "node server.js", diff --git a/examples/auth-routes/pages/secret.vue b/examples/auth-routes/pages/secret.vue index 776201a1e7..c04a37363c 100644 --- a/examples/auth-routes/pages/secret.vue +++ b/examples/auth-routes/pages/secret.vue @@ -8,11 +8,6 @@ diff --git a/examples/auth-routes/server.js b/examples/auth-routes/server.js index 5a194c6210..5a2fb970d1 100644 --- a/examples/auth-routes/server.js +++ b/examples/auth-routes/server.js @@ -1,7 +1,9 @@ -const Nuxt = require('nuxt') +const Nuxt = require('../../') const bodyParser = require('body-parser') const session = require('express-session') const app = require('express')() +const host = process.env.HOST || '127.0.0.1' +const port = process.env.PORT || '3000' // Body parser, to access req.body app.use(bodyParser.json()) @@ -20,7 +22,7 @@ app.post('/api/login', function (req, res) { req.session.authUser = { username: 'demo' } return res.json({ username: 'demo' }) } - res.status(401).json({ error: 'Bad credentials' }) + res.status(401).json({ message: 'Bad credentials' }) }) // POST /api/logout to log out the user and remove it from the req.session @@ -29,19 +31,23 @@ app.post('/api/logout', function (req, res) { res.json({ ok: true }) }) -// We instantiate Nuxt.js with the options -const isProd = process.env.NODE_ENV === 'production' +// Import and Set Nuxt.js options let config = require('./nuxt.config.js') -config.dev = !isProd +config.dev = !(process.env.NODE_ENV === 'production') + +// Init Nuxt.js const nuxt = new Nuxt(config) -// No build in production -const promise = (isProd ? Promise.resolve() : nuxt.build()) -promise.then(() => { - app.use(nuxt.render) - app.listen(3000) - console.log('Server is listening on http://localhost:3000') // eslint-disable-line no-console -}) -.catch((error) => { - console.error(error) // eslint-disable-line no-console - process.exit(1) -}) +app.use(nuxt.render) + +// Build only in dev mode +if (config.dev) { + nuxt.build() + .catch((error) => { + console.error(error) // eslint-disable-line no-console + process.exit(1) + }) +} + +// Listen the server +app.listen(port, host) +console.log('Server listening on ' + host + ':' + port) // eslint-disable-line no-console diff --git a/examples/auth-routes/store/index.js b/examples/auth-routes/store/index.js index 6f2a38f1a0..2181cfdf0c 100644 --- a/examples/auth-routes/store/index.js +++ b/examples/auth-routes/store/index.js @@ -1,69 +1,41 @@ -import Vue from 'vue' -import Vuex from 'vuex' +import axios from 'axios' -Vue.use(Vuex) +export const state = { + authUser: null +} -// Polyfill for window.fetch() -require('whatwg-fetch') +export const mutations = { + SET_USER: function (state, user) { + state.authUser = user + } +} -const store = new Vuex.Store({ - - state: { - authUser: null - }, - - mutations: { - SET_USER: function (state, user) { - state.authUser = user +export const actions = { + nuxtServerInit ({ commit }, { req }) { + if (req.session && req.session.authUser) { + commit('SET_USER', req.session.authUser) } }, - - actions: { - - nuxtServerInit ({ commit }, { req }) { - if (req.session && req.session.authUser) { - commit('SET_USER', req.session.authUser) + login ({ commit }, { username, password }) { + return axios.post('/api/login', { + username, + password + }) + .then((res) => { + commit('SET_USER', res.data) + }) + .catch((error) => { + if (error.response.status === 401) { + throw new Error('Bad credentials') } - }, - - login ({ commit }, { username, password }) { - return fetch('/api/login', { - // Send the client cookies to the server - credentials: 'same-origin', - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - username, - password - }) - }) - .then((res) => { - if (res.status === 401) { - throw new Error('Bad credentials') - } else { - return res.json() - } - }) - .then((authUser) => { - commit('SET_USER', authUser) - }) - }, - - logout ({ commit }) { - return fetch('/api/logout', { - // Send the client cookies to the server - credentials: 'same-origin', - method: 'POST' - }) - .then(() => { - commit('SET_USER', null) - }) - } + }) + }, + logout ({ commit }) { + return axios.post('/api/logout') + .then(() => { + commit('SET_USER', null) + }) } -}) - -export default store +} diff --git a/examples/middleware/components/Visits.vue b/examples/middleware/components/Visits.vue new file mode 100644 index 0000000000..0ac86d54d4 --- /dev/null +++ b/examples/middleware/components/Visits.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/examples/middleware/layouts/default.vue b/examples/middleware/layouts/default.vue new file mode 100644 index 0000000000..07f12ba8a8 --- /dev/null +++ b/examples/middleware/layouts/default.vue @@ -0,0 +1,14 @@ + + + diff --git a/examples/middleware/middleware/user-agent.js b/examples/middleware/middleware/user-agent.js new file mode 100644 index 0000000000..097436a73d --- /dev/null +++ b/examples/middleware/middleware/user-agent.js @@ -0,0 +1,3 @@ +export default function (context) { + context.userAgent = context.isServer ? context.req.headers['user-agent'] : navigator.userAgent +} diff --git a/examples/middleware/middleware/visits.js b/examples/middleware/middleware/visits.js new file mode 100644 index 0000000000..3ba5ab253f --- /dev/null +++ b/examples/middleware/middleware/visits.js @@ -0,0 +1,3 @@ +export default function ({ store, route }) { + store.commit('ADD_VISIT', route.path) +} diff --git a/examples/middleware/nuxt.config.js b/examples/middleware/nuxt.config.js new file mode 100644 index 0000000000..1e075eff57 --- /dev/null +++ b/examples/middleware/nuxt.config.js @@ -0,0 +1,5 @@ +module.exports = { + router: { + middleware: ['visits', 'user-agent'] + } +} diff --git a/examples/middleware/package.json b/examples/middleware/package.json new file mode 100644 index 0000000000..7d068bad95 --- /dev/null +++ b/examples/middleware/package.json @@ -0,0 +1,11 @@ +{ + "name": "nuxt-middleware", + "dependencies": { + "nuxt": "latest" + }, + "scripts": { + "dev": "nuxt", + "build": "nuxt build", + "start": "nuxt start" + } +} diff --git a/examples/middleware/pages/_slug.vue b/examples/middleware/pages/_slug.vue new file mode 100644 index 0000000000..9694907931 --- /dev/null +++ b/examples/middleware/pages/_slug.vue @@ -0,0 +1,25 @@ + + + diff --git a/examples/middleware/store/index.js b/examples/middleware/store/index.js new file mode 100644 index 0000000000..12cdd3f02b --- /dev/null +++ b/examples/middleware/store/index.js @@ -0,0 +1,12 @@ +export const state = { + visits: [] +} + +export const mutations = { + ADD_VISIT (state, path) { + state.visits.push({ + path, + date: new Date().toJSON() + }) + } +} diff --git a/lib/app/client.js b/lib/app/client.js index aac145d260..027ee0dc3d 100644 --- a/lib/app/client.js +++ b/lib/app/client.js @@ -1,8 +1,9 @@ 'use strict' import Vue from 'vue' +import middleware from './middleware' import { app, router<%= (store ? ', store' : '') %> } from './index' -import { getMatchedComponents, getMatchedComponentsInstances, flatMapComponents, getContext, promisify, getLocation, compile } from './utils' +import { getMatchedComponents, getMatchedComponentsInstances, flatMapComponents, getContext, promiseSeries, promisify, getLocation, compile } from './utils' const noopData = () => { return {} } const noopFetch = () => {} let _lastPaths = [] @@ -51,16 +52,44 @@ function loadAsyncComponents (to, from, next) { }) } +function callMiddleware (Components, context, layout) { + // Call middleware + let midd = <%= serialize(router.middleware, { isJSON: true }) %> + if (layout.middleware) { + midd = midd.concat(layout.middleware) + } + Components.forEach((Component) => { + if (Component.options.middleware) { + midd = midd.concat(Component.options.middleware) + } + }) + midd = midd.map((name) => { + if (typeof middleware[name] !== 'function') { + this.error({ statusCode: 500, message: 'Unknown middleware ' + name }) + } + return middleware[name] + }) + if (this.$options._nuxt.err) return + return promiseSeries(midd, context) +} + function render (to, from, next) { if (this._hashChanged) return next() + const _next = function (path) { + <%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %> + nextCalled = true + next(path) + } + const context = getContext({ to<%= (store ? ', store' : '') %>, isClient: true, next: _next.bind(this), error: this.error.bind(this) }) let Components = getMatchedComponents(to) this._dateLastError = this.$options._nuxt.dateErr this._hadError = !!this.$options._nuxt.err if (!Components.length) { // Default layout this.setLayout() + .then(callMiddleware.bind(this, Components, context)) .then(() => { - this.error({ statusCode: 404, message: 'This page could not be found.', url: to.path }) + this.error({ statusCode: 404, message: 'This page could not be found.' }) return next() }) return @@ -87,6 +116,7 @@ function render (to, from, next) { let nextCalled = false // Set layout this.setLayout(Components[0].options.layout) + .then(callMiddleware.bind(this, Components, context)) .then(() => { // Pass validation? let isValid = true @@ -99,7 +129,7 @@ function render (to, from, next) { }) }) if (!isValid) { - this.error({ statusCode: 404, message: 'This page could not be found.', url: to.path }) + this.error({ statusCode: 404, message: 'This page could not be found.' }) return next() } return Promise.all(Components.map((Component, i) => { @@ -109,12 +139,6 @@ function render (to, from, next) { return Promise.resolve() } let promises = [] - const _next = function (path) { - <%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %> - nextCalled = true - next(path) - } - const context = getContext({ to<%= (store ? ', store' : '') %>, isClient: true, next: _next.bind(this), error: this.error.bind(this) }) // Validate method if (Component._data && typeof Component._data === 'function') { var promise = promisify(Component._data, context) diff --git a/lib/app/middleware.js b/lib/app/middleware.js new file mode 100644 index 0000000000..3669e0545b --- /dev/null +++ b/lib/app/middleware.js @@ -0,0 +1,20 @@ +<% if (middleware) { %> +let files = require.context('~/middleware', false, /^\.\/.*\.js$/) +let filenames = files.keys() + +function getModule (filename) { + let file = files(filename) + return file.default + ? file.default + : file +} +let middleware = {} + +// Generate the middleware +for (let filename of filenames) { + let name = filename.replace(/^\.\//, '').replace(/\.js$/, '') + middleware[name] = getModule(filename) +} + +export default middleware +<% } else { %>export default {}<% } %> diff --git a/lib/app/server.js b/lib/app/server.js index e8e184226f..9c04c56fdb 100644 --- a/lib/app/server.js +++ b/lib/app/server.js @@ -5,8 +5,9 @@ debug.color = 4 // force blue color import Vue from 'vue' import { stringify } from 'querystring' import { omit } from 'lodash' +import middleware from './middleware' import { app, router<%= (store ? ', store' : '') %> } from './index' -import { getMatchedComponents, getContext, promisify, urlJoin } from './utils' +import { getMatchedComponents, getContext, promiseSeries, promisify, urlJoin } from './utils' const isDev = <%= isDev %> const _app = new Vue(app) @@ -47,6 +48,7 @@ export default context => { context.error = _app.$options._nuxt.error.bind(_app) <%= (isDev ? 'const s = isDev && Date.now()' : '') %> + const ctx = getContext(context) let Components = getMatchedComponents(context.route) <% if (store) { %> let promise = (store._actions && store._actions.nuxtServerInit ? store.dispatch('nuxtServerInit', omit(getContext(context), 'redirect', 'error')) : null) @@ -71,6 +73,26 @@ export default context => { // Set layout return _app.setLayout(Components.length ? Components[0].options.layout : '') }) + .then((layout) => { + // Call middleware + let midd = <%= serialize(router.middleware, { isJSON: true }) %> + if (layout.middleware) { + midd = midd.concat(layout.middleware) + } + Components.forEach((Component) => { + if (Component.options.middleware) { + midd = midd.concat(Component.options.middleware) + } + }) + midd = midd.map((name) => { + if (typeof middleware[name] !== 'function') { + context.nuxt.error = context.error({ statusCode: 500, message: 'Unknown middleware ' + name }) + } + return middleware[name] + }) + if (context.nuxt.error) return + return promiseSeries(midd, ctx) + }) .then(() => { // Call .validate() let isValid = true @@ -94,7 +116,6 @@ export default context => { // Call data & fetch hooks on components matched by the route. return Promise.all(Components.map((Component) => { let promises = [] - const ctx = getContext(context) if (Component.options.data && typeof Component.options.data === 'function') { Component._data = Component.options.data let promise = promisify(Component._data, ctx) @@ -114,7 +135,7 @@ export default context => { }) .then((res) => { if (!Components.length) { - context.nuxt.error = context.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.' }) <%= (store ? 'context.nuxt.state = store.state' : '') %> return _app } diff --git a/lib/app/store.js b/lib/app/store.js index adbc0ec4d2..218eeebebc 100644 --- a/lib/app/store.js +++ b/lib/app/store.js @@ -2,15 +2,8 @@ import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) -let files -let filenames = [] - -try { - files = require.context('~store', false, /^\.\/.*\.js$/) - filenames = files.keys() -} catch (e) { - console.warn('Nuxt.js store:', e.message) -} +let files = require.context('~/store', false, /^\.\/.*\.js$/) +let filenames = files.keys() function getModule (filename) { let file = files(filename) diff --git a/lib/app/utils.js b/lib/app/utils.js index a06d68f393..013b3d4b29 100644 --- a/lib/app/utils.js +++ b/lib/app/utils.js @@ -40,6 +40,7 @@ export function getContext (context) { ctx.query = ctx.route.query || {} ctx.redirect = function (status, path, query) { if (!status) return + ctx._redirected = true // if only 1 or 2 arguments: redirect('/') or redirect('/', { foo: 'bar' }) if (typeof status === 'string' && (typeof path === 'undefined' || typeof path === 'object')) { query = path || {} @@ -57,6 +58,16 @@ export function getContext (context) { return ctx } +export function promiseSeries (promises, context) { + if (!promises.length || context._redirected) { + return Promise.resolve() + } + return promisify(promises[0], context) + .then(() => { + return promiseSeries(promises.slice(1), context) + }) +} + export function promisify (fn, context) { let promise if (fn.length === 2) { diff --git a/lib/build.js b/lib/build.js index b988fd1083..987a6aa088 100644 --- a/lib/build.js +++ b/lib/build.js @@ -120,8 +120,8 @@ export function * build () { function * buildFiles () { if (this.dev) { - debug('Adding webpack middlewares...') - createWebpackMiddlewares.call(this) + debug('Adding webpack middleware...') + createWebpackMiddleware.call(this) webpackWatchAndUpdate.call(this) watchPages.call(this) } else { @@ -148,12 +148,17 @@ function * generateRoutesAndFiles () { this.routes = _.uniq(_.map(files, (file) => { return file.replace(/^pages/, '').replace(/\.vue$/, '').replace(/\/index/g, '').replace(/_/g, ':').replace('', '/').replace(/\/{2,}/g, '/') })) + if (typeof this.options.router.extendRoutes === 'function') { + // let the user extend the routes + this.options.router.extendRoutes(this.routes) + } // Interpret and move template files to .nuxt/ debug('Generating files...') let templatesFiles = [ 'App.vue', 'client.js', 'index.js', + 'middleware.js', 'router.js', 'server.js', 'utils.js', @@ -168,11 +173,13 @@ function * generateRoutesAndFiles () { isDev: this.dev, router: { base: this.options.router.base, + middleware: this.options.router.middleware, linkActiveClass: this.options.router.linkActiveClass, scrollBehavior: this.options.router.scrollBehavior }, env: this.options.env, head: this.options.head, + middleware: this.options.middleware, store: this.options.store, css: this.options.css, plugins: this.options.plugins.map((p) => r(this.srcDir, p)), @@ -294,7 +301,7 @@ function getWebpackServerConfig () { return serverWebpackConfig.call(this) } -function createWebpackMiddlewares () { +function createWebpackMiddleware () { const clientConfig = getWebpackClientConfig.call(this) // setup on the fly compilation + hot-reload clientConfig.entry.app = _.flatten(['webpack-hot-middleware/client?reload=true', clientConfig.entry.app]) @@ -303,7 +310,7 @@ function createWebpackMiddlewares () { new webpack.NoEmitOnErrorsPlugin() ) const clientCompiler = webpack(clientConfig) - // Add the middlewares to the instance context + // Add the middleware to the instance context this.webpackDevMiddleware = pify(require('webpack-dev-middleware')(clientCompiler, { publicPath: clientConfig.output.publicPath, stats: { diff --git a/lib/generate.js b/lib/generate.js index b8bdd05bd6..9f37dfc469 100644 --- a/lib/generate.js +++ b/lib/generate.js @@ -76,6 +76,7 @@ export default function () { console.error(`Could not generate the dynamic route ${route}, please add the mapping params in nuxt.config.js (generate.routeParams).`) // eslint-disable-line no-console return process.exit(1) } + route = route + '?' const toPath = pathToRegexp.compile(route) routes = routes.concat(routeParams.map((params) => { return toPath(params) diff --git a/lib/nuxt.js b/lib/nuxt.js index 1ed5ab7a51..6982b29049 100644 --- a/lib/nuxt.js +++ b/lib/nuxt.js @@ -37,13 +37,16 @@ class Nuxt { }, router: { base: '/', + middleware: [], linkActiveClass: 'nuxt-link-active', extendRoutes: null, scrollBehavior: null }, build: {} } + // Sanitization if (options.loading === true) delete options.loading + if (options.router && typeof options.router.middleware === 'string') options.router.middleware = [ options.router.middleware ] if (typeof options.transition === 'string') options.transition = { name: options.transition } this.options = _.defaultsDeep(options, defaults) // Env variables @@ -54,6 +57,11 @@ class Nuxt { if (fs.existsSync(join(this.srcDir, 'store'))) { this.options.store = true } + // If middleware defined, update middleware option to true + this.options.middleware = false + if (fs.existsSync(join(this.srcDir, 'middleware'))) { + this.options.middleware = true + } // Template this.appTemplate = _.template(fs.readFileSync(resolve(__dirname, 'views', 'app.html'), 'utf8'), { imports: { serialize } diff --git a/lib/render.js b/lib/render.js index 6a9cd02d4b..a759f1345d 100644 --- a/lib/render.js +++ b/lib/render.js @@ -22,11 +22,11 @@ export function render (req, res) { const context = getContext(req, res) return co(function * () { if (self.dev) { - // Call webpack middlewares only in development + // Call webpack middleware only in development yield self.webpackDevMiddleware(req, res) yield self.webpackHotMiddleware(req, res) } - // If base in req.url, remove it for the middlewares and vue-router + // If base in req.url, remove it for the middleware and vue-router if (self.options.router.base !== '/' && req.url.indexOf(self.options.router.base) === 0) { // Compatibility with base url for dev server req.url = req.url.replace(self.options.router.base, '/')