Middleware feature 🔥

This commit is contained in:
Sébastien Chopin 2017-02-03 15:09:27 +01:00
parent 84b8a0de3f
commit 17650c25e0
26 changed files with 296 additions and 116 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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
})
}
}

View File

@ -6,6 +6,5 @@ module.exports = {
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', content: 'Auth Routes example' }
]
},
loading: { color: '#3B8070' }
}
}

View File

@ -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",

View File

@ -8,11 +8,6 @@
<script>
export default {
// we use fetch() because we do not need to set data to this component
fetch ({ store, redirect }) {
if (!store.state.authUser) {
return redirect('/')
}
}
middleware: 'auth'
}
</script>

View File

@ -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

View File

@ -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
}

View File

@ -0,0 +1,40 @@
<template>
<ul>
<li v-for="visit in visits"><i>{{ visit.date | hours }}</i> - {{ visit.path }}</li>
</ul>
</template>
<script>
export default {
computed: {
visits () {
return this.$store.state.visits.slice().reverse()
}
},
filters: {
hours (date) {
return date.split('T')[1].split('.')[0]
}
}
}
</script>
<style scoped>
ul {
position: fixed;
top: 20px;
right: 20px;
margin: 0;
padding: 0;
height: 100%;
overflow: auto;
list-style-type: none;
}
ul li {
padding: 2px 0;
}
ul li i {
color: gray;
font-style: normal;
}
</style>

View File

@ -0,0 +1,14 @@
<template>
<div>
<nuxt/>
<visits/>
</div>
</template>
<script>
import Visits from '~components/Visits'
export default {
components: { Visits }
}
</script>

View File

@ -0,0 +1,3 @@
export default function (context) {
context.userAgent = context.isServer ? context.req.headers['user-agent'] : navigator.userAgent
}

View File

@ -0,0 +1,3 @@
export default function ({ store, route }) {
store.commit('ADD_VISIT', route.path)
}

View File

@ -0,0 +1,5 @@
module.exports = {
router: {
middleware: ['visits', 'user-agent']
}
}

View File

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

View File

@ -0,0 +1,25 @@
<template>
<div>
<h1>{{ $route.params.slug || 'Home' }}</h1>
<pre>{{ userAgent }}</pre>
<ul>
<li><nuxt-link to="/">Home</nuxt-link></li>
<li v-for="slug in slugs"><nuxt-link :to="{ name: 'slug', params: { slug } }">{{ slug }}</nuxt-link></li>
</ul>
</div>
</template>
<script>
export default {
data ({ store, route, userAgent }) {
return {
userAgent,
slugs: [
'foo',
'bar',
'baz'
]
}
}
}
</script>

View File

@ -0,0 +1,12 @@
export const state = {
visits: []
}
export const mutations = {
ADD_VISIT (state, path) {
state.visits.push({
path,
date: new Date().toJSON()
})
}
}

View File

@ -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)

20
lib/app/middleware.js Normal file
View File

@ -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 {}<% } %>

View File

@ -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
}

View File

@ -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)

View File

@ -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) {

View File

@ -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: {

View File

@ -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)

View File

@ -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 }

View File

@ -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, '/')