feat(vue-app): support app/router.scrollBehavior.js and deprecate scrollBehavior (#6055)

This commit is contained in:
Sébastien Chopin 2019-07-24 13:35:50 +02:00 committed by Pooya Parsa
parent ac00f7a627
commit f7cb3dae0f
13 changed files with 138 additions and 112 deletions

View File

@ -405,42 +405,47 @@ export default class Builder {
} }
async resolveCustomTemplates (templateContext) { async resolveCustomTemplates (templateContext) {
// Resolve template files // Sanitize custom template files
this.options.build.templates = this.options.build.templates.map((t) => {
const src = t.src || t
return Object.assign(
{
src: r(this.options.srcDir, src),
dst: t.dst || path.basename(src),
custom: true
},
typeof t === 'object' ? t : undefined
)
})
const customTemplateFiles = this.options.build.templates.map( const customTemplateFiles = this.options.build.templates.map(
t => t.dst || path.basename(t.src || t) t => t.dst || path.basename(t.src || t)
) )
const templatePaths = uniq([
// Modules & user provided templates
// first custom to keep their index
...customTemplateFiles,
// @nuxt/vue-app templates
...templateContext.templateFiles
])
const templateFiles = await Promise.all(templateContext.templateFiles.map(async (file) => { templateContext.templateFiles = await Promise.all(templatePaths.map(async (file) => {
// Skip if custom file was already provided in build.templates[] // Use custom file if provided in build.templates[]
if (customTemplateFiles.includes(file)) { const customTemplateIndex = customTemplateFiles.indexOf(file)
return const customTemplate = customTemplateIndex !== -1 ? this.options.build.templates[customTemplateIndex] : null
} let src = customTemplate ? (customTemplate.src || customTemplate) : r(this.template.dir, file)
// Allow override templates using a file with same name in ${srcDir}/app // Allow override templates using a file with same name in ${srcDir}/app
const customPath = r(this.options.srcDir, 'app', file) const customPath = r(this.options.srcDir, this.options.dir.app, file)
const customFileExists = await fsExtra.exists(customPath) const customFileExists = await fsExtra.exists(customPath)
src = customFileExists ? customPath : src
return { return {
src: customFileExists ? customPath : r(this.template.dir, file), src,
dst: file, dst: file,
custom: customFileExists custom: Boolean(customFileExists || customTemplate),
options: (customTemplate && customTemplate.options) || {}
} }
})) }))
templateContext.templateFiles = templateFiles
.filter(Boolean)
// Add custom template files
.concat(
this.options.build.templates.map((t) => {
return Object.assign(
{
src: r(this.options.srcDir, t.src || t),
dst: t.dst || path.basename(t.src || t),
custom: true
},
typeof t === 'object' ? t : undefined
)
})
)
} }
async resolveLoadingIndicator ({ templateFiles }) { async resolveLoadingIndicator ({ templateFiles }) {
@ -628,6 +633,9 @@ export default class Builder {
} }
this.createFileWatcher(customPatterns, ['change'], refreshFiles, this.assignWatcher('custom')) this.createFileWatcher(customPatterns, ['change'], refreshFiles, this.assignWatcher('custom'))
// Watch for app/ files
this.createFileWatcher([r(this.options.srcDir, this.options.dir.app)], ['add', 'change', 'unlink'], refreshFiles, this.assignWatcher('app'))
} }
getServerMiddlewarePaths () { getServerMiddlewarePaths () {

View File

@ -3,7 +3,10 @@ export const createNuxt = () => ({
globalName: 'global_name', globalName: 'global_name',
globals: [], globals: [],
build: {}, build: {},
router: {} router: {},
dir: {
app: 'app'
}
}, },
ready: jest.fn(), ready: jest.fn(),
hook: jest.fn(), hook: jest.fn(),

View File

@ -209,12 +209,12 @@ describe('builder: builder generate', () => {
await builder.resolveCustomTemplates(templateContext) await builder.resolveCustomTemplates(templateContext)
expect(templateContext.templateFiles).toEqual([ expect(templateContext.templateFiles).toEqual([
{ custom: true, dst: 'router.js', src: 'r(/var/nuxt/src, app, router.js)' }, { custom: true, dst: 'foo.js', src: 'r(/var/nuxt/src, app, foo.js)', options: {} },
{ custom: undefined, dst: 'store.js', src: 'r(/var/nuxt/templates, store.js)' }, { custom: true, dst: 'bar.js', src: '/var/nuxt/templates/bar.js', options: {} },
{ custom: undefined, dst: 'middleware.js', src: 'r(/var/nuxt/templates, middleware.js)' }, { custom: true, dst: 'baz.js', src: '/var/nuxt/templates/baz.js', options: {} },
{ custom: true, dst: 'foo.js', src: 'r(/var/nuxt/src, /var/nuxt/templates/foo.js)' }, { custom: false, dst: 'router.js', src: 'r(/var/nuxt/templates, router.js)', options: {} },
{ custom: true, dst: 'bar.js', src: '/var/nuxt/templates/bar.js' }, { custom: false, dst: 'store.js', src: 'r(/var/nuxt/templates, store.js)', options: {} },
{ custom: true, dst: 'baz.js', src: '/var/nuxt/templates/baz.js' } { custom: false, dst: 'middleware.js', src: 'r(/var/nuxt/templates, middleware.js)', options: {} }
]) ])
}) })

View File

@ -189,9 +189,9 @@ describe('builder: builder watch', () => {
'/var/nuxt/src/style' '/var/nuxt/src/style'
] ]
expect(builder.createFileWatcher).toBeCalledTimes(2) expect(builder.createFileWatcher).toBeCalledTimes(3)
expect(builder.createFileWatcher).toBeCalledWith(patterns, ['change'], expect.any(Function), expect.any(Function)) expect(builder.createFileWatcher).toBeCalledWith(patterns, ['change'], expect.any(Function), expect.any(Function))
expect(builder.assignWatcher).toBeCalledTimes(2) expect(builder.assignWatcher).toBeCalledTimes(3)
}) })
test('should invoke chokidar to create watcher', () => { test('should invoke chokidar to create watcher', () => {

View File

@ -35,6 +35,7 @@ export default () => ({
], ],
dir: { dir: {
assets: 'assets', assets: 'assets',
app: 'app',
layouts: 'layouts', layouts: 'layouts',
middleware: 'middleware', middleware: 'middleware',
pages: 'pages', pages: 'pages',

View File

@ -35,6 +35,12 @@ export function getNuxtConfig (_options) {
options._routerBaseSpecified = true options._routerBaseSpecified = true
} }
// TODO: Remove for Nuxt 3
// router.scrollBehavior -> app/router.scrollBehavior.js
if (options.router && typeof options.router.scrollBehavior !== 'undefined') {
consola.warn('`router.scrollBehavior` property is deprecated in favor of using `~/app/router.scrollBehavior.js` file, learn more: https://nuxtjs.org/api/configuration-router#scrollbehavior')
}
// TODO: Remove for Nuxt 3 // TODO: Remove for Nuxt 3
// transition -> pageTransition // transition -> pageTransition
if (typeof options.transition !== 'undefined') { if (typeof options.transition !== 'undefined') {

View File

@ -151,6 +151,7 @@ Object {
"dev": false, "dev": false,
"devModules": Array [], "devModules": Array [],
"dir": Object { "dir": Object {
"app": "app",
"assets": "assets", "assets": "assets",
"layouts": "layouts", "layouts": "layouts",
"middleware": "middleware", "middleware": "middleware",

View File

@ -131,6 +131,7 @@ Object {
"dev": false, "dev": false,
"devModules": Array [], "devModules": Array [],
"dir": Object { "dir": Object {
"app": "app",
"assets": "assets", "assets": "assets",
"layouts": "layouts", "layouts": "layouts",
"middleware": "middleware", "middleware": "middleware",
@ -457,6 +458,7 @@ Object {
"dev": false, "dev": false,
"devModules": Array [], "devModules": Array [],
"dir": Object { "dir": Object {
"app": "app",
"assets": "assets", "assets": "assets",
"layouts": "layouts", "layouts": "layouts",
"middleware": "middleware", "middleware": "middleware",

View File

@ -26,9 +26,9 @@ describe('util: serialize', () => {
test('should serialize normal function', () => { test('should serialize normal function', () => {
const obj = { const obj = {
fn () {} fn: function () {} // eslint-disable-line object-shorthand
} }
expect(serializeFunction(obj.fn)).toEqual('function() {}') expect(serializeFunction(obj.fn)).toEqual('function () {}')
}) })
test('should serialize shorthand function', () => { test('should serialize shorthand function', () => {

View File

@ -10,6 +10,7 @@ export const template = {
'index.js', 'index.js',
'middleware.js', 'middleware.js',
'router.js', 'router.js',
'router.scrollBehavior.js',
'server.js', 'server.js',
'utils.js', 'utils.js',
'empty.js', 'empty.js',

View File

@ -1,6 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import Router from 'vue-router' import Router from 'vue-router'
import { interopDefault } from './utils'<%= isTest ? '// eslint-disable-line no-unused-vars' : '' %> import { interopDefault } from './utils'<%= isTest ? '// eslint-disable-line no-unused-vars' : '' %>
import scrollBehavior from './router.scrollBehavior.js'
<% function recursiveRoutes(routes, tab, components, indentCount) { <% function recursiveRoutes(routes, tab, components, indentCount) {
let res = '' let res = ''
@ -83,81 +84,6 @@ const _routes = recursiveRoutes(router.routes, ' ', _components, 2)
Vue.use(Router) Vue.use(Router)
<% if (router.scrollBehavior) { %>
const scrollBehavior = <%= serializeFunction(router.scrollBehavior) %>
<% } else { %>
if (process.client) {
if ('scrollRestoration' in window.history) {
window.history.scrollRestoration = 'manual'
// reset scrollRestoration to auto when leaving page, allowing page reload
// and back-navigation from other pages to use the browser to restore the
// scrolling position.
window.addEventListener('beforeunload', () => {
window.history.scrollRestoration = 'auto'
})
// Setting scrollRestoration to manual again when returning to this page.
window.addEventListener('load', () => {
window.history.scrollRestoration = 'manual'
})
}
}
const scrollBehavior = function (to, from, savedPosition) {
// if the returned position is falsy or an empty object,
// will retain current scroll position.
let position = false
// if no children detected and scrollToTop is not explicitly disabled
if (
to.matched.length < 2 &&
to.matched.every(r => r.components.default.options.scrollToTop !== false)
) {
// scroll to the top of the page
position = { x: 0, y: 0 }
} else if (to.matched.some(r => r.components.default.options.scrollToTop)) {
// if one of the children has scrollToTop option set to true
position = { x: 0, y: 0 }
}
// savedPosition is only available for popstate navigations (back button)
if (savedPosition) {
position = savedPosition
}
const nuxt = window.<%= globals.nuxt %>
// triggerScroll is only fired when a new component is loaded
if (to.path === from.path && to.hash !== from.hash) {
nuxt.$nextTick(() => nuxt.$emit('triggerScroll'))
}
return new Promise((resolve) => {
// wait for the out transition to complete (if necessary)
nuxt.$once('triggerScroll', () => {
// coords will be used if no selector is provided,
// or if the selector didn't match any element.
if (to.hash) {
let hash = to.hash
// CSS.escape() is not supported with IE and Edge.
if (typeof window.CSS !== 'undefined' && typeof window.CSS.escape !== 'undefined') {
hash = '#' + window.CSS.escape(hash.substr(1))
}
try {
if (document.querySelector(hash)) {
// scroll to anchor by returning the selector
position = { selector: hash }
}
} catch (e) {
console.warn('Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).')
}
}
resolve(position)
})
})
}
<% } %>
export function createRouter() { export function createRouter() {
return new Router({ return new Router({
mode: '<%= router.mode %>', mode: '<%= router.mode %>',

View File

@ -0,0 +1,77 @@
<% if (router.scrollBehavior) { %>
export default <%= serializeFunction(router.scrollBehavior) %>
<% } else { %>import { getMatchedComponents } from './utils'
if (process.client) {
if ('scrollRestoration' in window.history) {
window.history.scrollRestoration = 'manual'
// reset scrollRestoration to auto when leaving page, allowing page reload
// and back-navigation from other pages to use the browser to restore the
// scrolling position.
window.addEventListener('beforeunload', () => {
window.history.scrollRestoration = 'auto'
})
// Setting scrollRestoration to manual again when returning to this page.
window.addEventListener('load', () => {
window.history.scrollRestoration = 'manual'
})
}
}
export default function (to, from, savedPosition) {
// if the returned position is falsy or an empty object,
// will retain current scroll position.
let position = false
// if no children detected and scrollToTop is not explicitly disabled
const Pages = getMatchedComponents(to)
if (
Pages.length < 2 &&
Pages.every(Page => Page.options.scrollToTop !== false)
) {
// scroll to the top of the page
position = { x: 0, y: 0 }
} else if (Pages.some(Page => Page.options.scrollToTop)) {
// if one of the children has scrollToTop option set to true
position = { x: 0, y: 0 }
}
// savedPosition is only available for popstate navigations (back button)
if (savedPosition) {
position = savedPosition
}
const nuxt = window.<%= globals.nuxt %>
// triggerScroll is only fired when a new component is loaded
if (to.path === from.path && to.hash !== from.hash) {
nuxt.$nextTick(() => nuxt.$emit('triggerScroll'))
}
return new Promise((resolve) => {
// wait for the out transition to complete (if necessary)
nuxt.$once('triggerScroll', () => {
// coords will be used if no selector is provided,
// or if the selector didn't match any element.
if (to.hash) {
let hash = to.hash
// CSS.escape() is not supported with IE and Edge.
if (typeof window.CSS !== 'undefined' && typeof window.CSS.escape !== 'undefined') {
hash = '#' + window.CSS.escape(hash.substr(1))
}
try {
if (document.querySelector(hash)) {
// scroll to anchor by returning the selector
position = { selector: hash }
}
} catch (e) {
console.warn('Failed to save scroll position. Please add CSS.escape() polyfill (https://github.com/mathiasbynens/CSS.escape).')
}
}
resolve(position)
})
})
}
<% } %>

View File

@ -14,9 +14,10 @@ const hooks = [
describe('with-config', () => { describe('with-config', () => {
buildFixture('with-config', () => { buildFixture('with-config', () => {
expect(consola.warn).toHaveBeenCalledTimes(6) expect(consola.warn).toHaveBeenCalledTimes(7)
expect(consola.fatal).toHaveBeenCalledTimes(0) expect(consola.fatal).toHaveBeenCalledTimes(0)
expect(consola.warn.mock.calls).toMatchObject([ expect(consola.warn.mock.calls).toMatchObject([
['`router.scrollBehavior` property is deprecated in favor of using `~/app/router.scrollBehavior.js` file, learn more: https://nuxtjs.org/api/configuration-router#scrollbehavior'],
['Unknown mode: unknown. Falling back to universal'], ['Unknown mode: unknown. Falling back to universal'],
['Invalid plugin mode (server/client/all): \'abc\'. Falling back to \'all\''], ['Invalid plugin mode (server/client/all): \'abc\'. Falling back to \'all\''],
[{ [{