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) {
// 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(
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) => {
// Skip if custom file was already provided in build.templates[]
if (customTemplateFiles.includes(file)) {
return
}
templateContext.templateFiles = await Promise.all(templatePaths.map(async (file) => {
// Use custom file if provided in build.templates[]
const customTemplateIndex = customTemplateFiles.indexOf(file)
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
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)
src = customFileExists ? customPath : src
return {
src: customFileExists ? customPath : r(this.template.dir, file),
src,
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 }) {
@ -628,6 +633,9 @@ export default class Builder {
}
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 () {

View File

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

View File

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

View File

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

View File

@ -35,6 +35,12 @@ export function getNuxtConfig (_options) {
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
// transition -> pageTransition
if (typeof options.transition !== 'undefined') {

View File

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

View File

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

View File

@ -26,9 +26,9 @@ describe('util: serialize', () => {
test('should serialize normal function', () => {
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', () => {

View File

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

View File

@ -1,6 +1,7 @@
import Vue from 'vue'
import Router from 'vue-router'
import { interopDefault } from './utils'<%= isTest ? '// eslint-disable-line no-unused-vars' : '' %>
import scrollBehavior from './router.scrollBehavior.js'
<% function recursiveRoutes(routes, tab, components, indentCount) {
let res = ''
@ -83,81 +84,6 @@ const _routes = recursiveRoutes(router.routes, ' ', _components, 2)
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() {
return new Router({
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', () => {
buildFixture('with-config', () => {
expect(consola.warn).toHaveBeenCalledTimes(6)
expect(consola.warn).toHaveBeenCalledTimes(7)
expect(consola.fatal).toHaveBeenCalledTimes(0)
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'],
['Invalid plugin mode (server/client/all): \'abc\'. Falling back to \'all\''],
[{