Next.js 中奇怪的命令式 onScroll 路由行为(仅在服务器上呈现)
Posted
技术标签:
【中文标题】Next.js 中奇怪的命令式 onScroll 路由行为(仅在服务器上呈现)【英文标题】:Strange imperative onScroll routing behavior in Next.js (only rendering on server) 【发布时间】:2018-05-21 22:59:02 【问题描述】:我正在尝试在滚动处理程序中使用 Next.js 命令式路由 api 进行页面导航。当我附加到窗口时,它几乎是完美的,除了滚动位置重置时有一个颜色“闪烁”(如果导航到上一页的顶部,需要继续从新路线的底部向上滚动,并且颜色过渡需要是无缝的,这似乎是不可能的,因为浏览器的毫秒数与 scrollTop 为 0)。代码:
export default class ScrollOMatic extends Component
constructor(props)
super(props)
this.state =
prevRoute: '',
nextRoute: '',
isEndOfScroll: false
binder(this, ['handleScroll'])
componentWillMount()
if (typeof window !== 'undefined')
console.log(window)
componentDidMount()
window.addEventListener('scroll', this.handleScroll)
const
prevRoute,
nextRoute
= this.props.routeData
this.setState(
prevRoute,
nextRoute
)
Router.prefetch('us')
Router.prefetch('work')
Router.prefetch('services')
Router.prefetch('converse')
componentWillUnmount()
window.removeEventListener('scroll', this.handleScroll)
handleScroll(e)
e.preventDefault()
let
scrollTop,
scrollHeight
= e.srcElement.scrollingElement
scrollHeight = scrollHeight / 2
const scrollTiplier = scrollTop / scrollHeight
if (scrollTop === 0)
Router.push(this.state.prevRoute)
window.scrollTo(0, scrollHeight - 1,
duration: 0
)
if (scrollTiplier === 1)
Router.push(this.state.nextRoute)
window.scrollTo(0, 1,
duration: 0
)
render()
return (
<div className = 'scroll-o-matic' > this.props.children </div>
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
所以,我使用了一个具有自己滚动行为的容器,灵感来自 react-horizontal-scroll,这似乎很有希望。但是这个新代码发生了一件奇怪的事情。新路由会在客户端呈现片刻,当从服务器获取新路由时页面会刷新。见 gif 和代码:
// inspired by react-horizontal-scroll
import Motion, spring, presets from 'react-motion'
import raf from 'raf'
import Router from 'next/router'
import fadeColor, binder from '../../lib/_utils'
import setScrollState from '../../lib/_navRules'
export default class ScrollOMatic extends Component
constructor (props)
super(props)
this.state =
prevRoute: '',
nextRoute: '',
animValues: 0
binder(this, ['handleWheel', 'resetMin', 'resetMax', 'navigator', 'canIscroll', 'getLayoutData'])
componentDidMount ()
const prevRoute, nextRoute = this.props.routeData
this.setState(
prevRoute,
nextRoute
)
const prevRouteName = prevRoute === '/' ? 'index' : prevRoute.replace('/', '')
const nextRouteName = prevRoute === '/' ? 'index' : nextRoute.replace('/', '')
Router.prefetch(prevRouteName)
Router.prefetch(nextRouteName)
componentWillReceiveProps (nextProps)
if (this.props.children !== nextProps.children) this.resetMin()
shouldComponentUpdate (nextProps, nextState)
if (true &&
this.calculate.timer !== void 0 &&
this.props.children === nextProps.children &&
this.state.animValues === nextState.animValues)
return false
if (true &&
this.props.children === nextProps.children &&
this.canIscroll() === false)
return false
return true
componentDidUpdate () this.calculate()
getLayoutData ()
const scrollOMatic = DOM.findDOMNode(this.scrollOMatic)
const scrollTray = DOM.findDOMNode(this.scrollTray)
const max = scrollOMatic.scrollHeight
const win = scrollOMatic.offsetHeight
const currentVal = this.state.animValues
const bounds = -(max - win)
const trayTop = scrollTray.offsetTop
const trayOffsetHeight = scrollTray.offsetHeight
const trayScrollHeight = scrollTray.scrollHeight
const scrollOMaticRect = scrollOMatic.getBoundingClientRect()
const scrollOMaticTop = scrollOMaticRect.top
const scrollOMaticHeight = scrollOMaticRect.height
const scrollOMaticOffsetHeight = scrollOMatic.offsetHeight
return
currentVal,
bounds,
scrollTray,
trayTop,
trayOffsetHeight,
trayScrollHeight,
scrollOMatic,
scrollOMaticTop,
scrollOMaticHeight,
scrollOMaticOffsetHeight,
scrollOMaticRect
calculate ()
const layout = this.getLayoutData()
clearTimeout(this.calculate.timer)
this.calculate.timer = setTimeout(() =>
const max = layout.trayScrollHeight
const win = layout.scrollOMaticOffsetHeight
const currentVal = this.state.animValues
const bounds = -(max - win)
if (currentVal >= 1)
this.resetMin()
else if (currentVal <= bounds)
const x = bounds + 1
this.resetMax(x)
)
resetMin () this.setState( animValues: 0 )
resetMax (x) this.setState( animValues: x )
canIscroll ()
const layout = this.getLayoutData()
return layout.trayOffsetTop < layout.scrollOMaticTop ||
layout.trayOffsetHeight > layout.scrollOMaticHeight
handleWheel (e)
e.preventDefault()
const rawData = e.deltaY ? e.deltaY : e.deltaX
const mouseY = Math.floor(rawData)
const animationVal = this.state.animValues
const newAnimationVal = (animationVal + mouseY)
const newAnimationValNeg = (animationVal - mouseY)
if (!this.canIscroll()) return
const layout = this.getLayoutData()
const currentVal, scrollOMaticHeight, trayScrollHeight = layout
const isEndOfPage = -(currentVal - scrollOMaticHeight) + 1 === trayScrollHeight
this.navigator()
const scrolling = () =>
this.state.scrollInverted
? this.setState( animValues: newAnimationValNeg )
: this.setState( animValues: newAnimationVal )
raf(scrolling)
navigator ()
const layout = this.getLayoutData()
const currentVal, scrollOMaticHeight, trayScrollHeight = layout
const shouldBeNextRoute = -(currentVal - scrollOMaticHeight) + 1 >= trayScrollHeight
// const shouldBePrevRoute = this.state.animValues < 0
if (shouldBeNextRoute)
Router.push(this.state.nextRoute)
// if (shouldBePrevRoute)
// Router.push(this.state.prevRoute)
//
render ()
const springConfig = presets.noWobble
return (
<div style= position: 'relative', width: '100vw', height: '100vh' className='scroll-o-matic' onWheel=this.handleWheel
ref=scrollOMatic => this.scrollOMatic = scrollOMatic >
<Motion style= z: spring(this.state.animValues, springConfig) >
( z ) => (
<div className='scroll-tray' ref=(scrollTray) => this.scrollTray = scrollTray
style=
height: '300vh',
width: '100vw',
// top: '-100vh',
transform: `translate3d(0,$z.toFixed(3)px,0)`,
willChange: 'transform',
display: 'inline-flex',
position: 'absolute'
>
this.props.children
</div>
)
</Motion>
<style jsx>`
.scroll-o-matic
background-color: $this.state.currentColor;
`</style>
</div>
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
服务器代码:
const express = require('express')
const next = require('next')
const dev = process.env.NODE_ENV !== 'production'
const app = next( dev )
const port = process.env.PORT || 3000
const handle = app.getRequestHandler()
app.prepare()
.then(() =>
const server = express()
server.use('/static', express.static('static'))
server.get('/', (req, res) =>
return app.render(req, res, '/', req.query)
)
server.get('/us', (req, res) =>
return app.render(req, res, '/us', req.query)
)
server.get('/work', (req, res) =>
return app.render(req, res, '/work', req.query)
)
server.get('/services', (req, res) =>
return app.render(req, res, '/services', req.query)
)
server.get('/converse', (req, res) =>
return app.render(req, res, '/converse', req.query)
)
server.get('*', (req, res) =>
return handle(req, res, '/', req.query)
)
server.listen(port, (err) =>
if (err) throw err
console.log(`> Ready on http://localhost:$port`)
)
)
也许我不明白 Next 的“预取”,但它似乎绝对没有预取任何东西。我主要很困惑为什么会发生服务器端页面加载。即使在 express 中使用通配符请求处理程序,页面也会重新加载(但会重定向到自身?),所以我认为这不是服务器配置的问题。
我错过了什么,互联网的神谕?
【问题讨论】:
【参考方案1】:滚动事件没有触发。因为我的身体在滚动而不是 documentElement。
我刚刚从我的 body 标签中删除了 height: 100%,然后滚动事件开始触发。
//in your CSS don't set height to percent
div
height: 100%
【讨论】:
【参考方案2】:对于任何可能有同样问题的人来说,虽然它很模糊,但问题最终与 Next 的“样式化 JSX”渲染方法有关。
显然,即使在客户端页面加载中,出于某种原因,Styled JSX 也需要半秒左右的时间来加载。您通常不会注意到这一点,除非像整个应用程序的背景颜色这样引人注目的东西在您的 <style jsx>``<style>
标记内。将所有样式放在常规的旧 html 样式 attr 中解决了“flash”问题。
【讨论】:
【参考方案3】:预取选项是在后台下载页面的 JS 代码,因此当您想要进行页面更改时,它已经下载,但仅此而已,如果您希望它预取已经呈现的 HTML 或您需要的数据手动实现。
【讨论】:
以上是关于Next.js 中奇怪的命令式 onScroll 路由行为(仅在服务器上呈现)的主要内容,如果未能解决你的问题,请参考以下文章