JavaScript - window.scroll({ behavior: 'smooth' }) 在Safari中无法正常工作。

46

正如标题所说,在Chrome上它可以完美地工作。但在Safari中,它只是将页面设置到期望的顶部和左侧位置。这是预期的行为吗?有方法让它能够良好地运行吗?


1
回复有点晚了 - 但是行为不适用于Safari或Edge。您将不得不实现自己的系统或使用库。 - George Daniel
1
这个问题已经在Safari错误跟踪器中得到了官方确认:https://bugs.webkit.org/show_bug.cgi?id=188043 和 https://bugs.webkit.org/show_bug.cgi?id=189907。 - jantimon
9个回答

39

使用 smoothscroll polyfill(适用于所有浏览器的解决方案),易于应用并且轻量级依赖项: https://github.com/iamdustan/smoothscroll

一旦您通过npm或yarn安装了它,请将其添加到您的 主要 .js、.ts 文件中(首先执行的文件)

import smoothscroll from 'smoothscroll-polyfill';
// or if linting/typescript complains
import * as smoothscroll from 'smoothscroll-polyfill';

// kick off the polyfill!
smoothscroll.polyfill();

在React中应该把它放在哪里?它在app.js中不起作用... - Richardson
@Richardson,它应该在那里工作,你遇到了什么错误? - EugenSunic
2
这对我有用,插入到 index.js 文件中。 我总是认为最好将这些配置放在 index.js 中,因为 App.js 是一个组件。 - thismarcoantonio
@Nunchuk 我建议你要么使用你的功能,要么使用我提出的功能。为什么要使用两个实现呢? - EugenSunic
1
@EugenSunic,使用我的功能效果非常好,我唯一缺少的是 Safari 的平滑滚动。我会传达你的建议并使用 CDN。它运行良好。 - Nunchuk
显示剩余3条评论

19

行为选项在 IE/Edge/Safari 并不完全支持,因此您需要自己实现一些东西。我相信 jQuery 已经有了一些东西,但如果您不使用 jQuery,则可以使用纯 JavaScript 实现:

function SmoothVerticalScrolling(e, time, where) {
    var eTop = e.getBoundingClientRect().top;
    var eAmt = eTop / 100;
    var curTime = 0;
    while (curTime <= time) {
        window.setTimeout(SVS_B, curTime, eAmt, where);
        curTime += time / 100;
    }
}

function SVS_B(eAmt, where) {
    if(where == "center" || where == "")
        window.scrollBy(0, eAmt / 2);
    if (where == "top")
        window.scrollBy(0, eAmt);
}

如果您需要水平滚动:

function SmoothHorizontalScrolling(e, time, amount, start) {
    var eAmt = amount / 100;
    var curTime = 0;
    var scrollCounter = 0;
    while (curTime <= time) {
        window.setTimeout(SHS_B, curTime, e, scrollCounter, eAmt, start);
        curTime += time / 100;
        scrollCounter++;
    }
}

function SHS_B(e, sc, eAmt, start) {
    e.scrollLeft = (eAmt * sc) + start;
}

一个示例调用如下:

SmoothVerticalScrolling(myelement, 275, "center");

6
@George Daniel,赞扬你用纯JS解决了问题,不过你可以通过在代码中添加一些行内注释来改进你的答案。 - Oksana Romaniv
2
应该使用Window.requestAnimationFrame()而不是timeouts,因为它具有性能优化并且仅在可见时运行等优点。详情请参阅https://blog.teamtreehouse.com/efficient-animations-with-requestanimationframe。 - Luckylooke
2
谢谢@George Daniel,我写了一个小笔记尝试在这里用注释描述您的函数过程https://codepen.io/gfcf14/pen/qBEMWJe - gfcf14
@Bonsaï,你只需要将我的函数简单地复制粘贴到你的JavaScript库/区域中。然后,只需像示例中所示那样进行函数调用,即可针对目标元素进行操作。 - George Daniel
1
@GeorgeDaniel 我做同样的事情,但是它不起作用。我没有得到任何响应。 - Ulvi
显示剩余2条评论

14

如果您需要更全面的平滑滚动方法列表,请参见我的答案here


window.requestAnimationFrame 可以用来在精确的时间内执行平滑滚动。

为了实现平滑的垂直滚动,可以使用以下函数。请注意,水平滚动可以采用类似的方式进行。

/*
   @param time: the exact amount of time the scrolling will take (in milliseconds)
   @param pos: the y-position to scroll to (in pixels)
*/
function scrollToSmoothly(pos, time) {
    var currentPos = window.pageYOffset;
    var start = null;
    if(time == null) time = 500;
    pos = +pos, time = +time;
    window.requestAnimationFrame(function step(currentTime) {
        start = !start ? currentTime : start;
        var progress = currentTime - start;
        if (currentPos < pos) {
            window.scrollTo(0, ((pos - currentPos) * progress / time) + currentPos);
        } else {
            window.scrollTo(0, currentPos - ((currentPos - pos) * progress / time));
        }
        if (progress < time) {
            window.requestAnimationFrame(step);
        } else {
            window.scrollTo(0, pos);
        }
    });
}

演示:

/*
   @param time: the exact amount of time the scrolling will take (in milliseconds)
   @param pos: the y-position to scroll to (in pixels)
*/
function scrollToSmoothly(pos, time) {
    var currentPos = window.pageYOffset;
    var start = null;
    if(time == null) time = 500;
    pos = +pos, time = +time;
    window.requestAnimationFrame(function step(currentTime) {
        start = !start ? currentTime : start;
        var progress = currentTime - start;
        if (currentPos < pos) {
            window.scrollTo(0, ((pos - currentPos) * progress / time) + currentPos);
        } else {
            window.scrollTo(0, currentPos - ((currentPos - pos) * progress / time));
        }
        if (progress < time) {
            window.requestAnimationFrame(step);
        } else {
            window.scrollTo(0, pos);
        }
    });
}

document.querySelector('button').addEventListener('click', function(e){
  scrollToSmoothly(500, 1500);
});
html, body {
  height: 1000px;
}
<button>Scroll to y-position 500px in 1500ms</button>

对于更复杂的情况,可以使用SmoothScroll.js库,它可以处理垂直和水平的平滑滚动,滚动在其他容器元素内部,不同的缓动行为,相对于当前位置的滚动等等。它还支持大多数没有本地平滑滚动的浏览器。

var easings = document.getElementById("easings");
for(var key in smoothScroll.easing){
    if(smoothScroll.easing.hasOwnProperty(key)){
        var option = document.createElement('option');
        option.text = option.value = key;
        easings.add(option);
    }
}
document.getElementById('to-bottom').addEventListener('click', function(e){
    smoothScroll({yPos: 'end', easing: easings.value, duration: 2000});
});
document.getElementById('to-top').addEventListener('click', function(e){
    smoothScroll({yPos: 'start', easing: easings.value, duration: 2000});
});
<script src="https://cdn.jsdelivr.net/gh/LieutenantPeacock/SmoothScroll@1.2.0/src/smoothscroll.min.js" integrity="sha384-UdJHYJK9eDBy7vML0TvJGlCpvrJhCuOPGTc7tHbA+jHEgCgjWpPbmMvmd/2bzdXU" crossorigin="anonymous"></script>
<!-- Taken from one of the library examples -->
Easing: <select id="easings"></select>
<button id="to-bottom">Scroll To Bottom</button>
<br>
<button id="to-top" style="margin-top: 5000px;">Scroll To Top</button>


谢谢。有没有可能添加缓动效果? - Fred K
@FredK 抱歉回复晚了,但我已经更新了我的答案。 - Unmitigated
谢谢!我通过在window.requestAnimationFrame下添加这行代码var currentPos = window.pageYOffset;实现了更流畅的滚动。 - unx

4

以上所有方法都弥补了Safari不支持行为的不足。

仍然需要检测是否需要使用解决方法。

此小函数将检测浏览器是否支持平滑滚动。在Safari上返回false,在Chrome和Firefox上返回true:

// returns true if browser supports smooth scrolling
const supportsSmoothScrolling = () => {
  const body = document.body;
  const scrollSave = body.style.scrollBehavior;
  body.style.scrollBehavior = 'smooth';
  const hasSmooth = getComputedStyle(body).scrollBehavior === 'smooth';
  body.style.scrollBehavior = scrollSave;
  return hasSmooth;
};

const pre = document.querySelector('pre');

// returns true if browser supports smooth scrolling
const supportsSmoothScrolling = () => {
  const body = document.body;
  const scrollSave = body.style.scrollBehavior;
  body.style.scrollBehavior = 'smooth';
  const hasSmooth = getComputedStyle(body).scrollBehavior === 'smooth';
  body.style.scrollBehavior = scrollSave;
  return hasSmooth;
};

const supported = supportsSmoothScrolling();

pre.innerHTML = `supported:  ${ (supported) ? 'true' : 'false'}`;
<h3>
Testing if 'scrollBehavior smooth' is supported
</h3>
<pre></pre>

更新

根据Safari Technology Preview版本139(Safari 15.4)的测试结果显示,支持scrollBehavior smooth,因此我们可以期待在15.4版本中得到支持。


3

如果您想使用缓动效果,性能最流畅的解决方案是使用requestAnimationFrame:

const requestAnimationFrame = window.requestAnimationFrame ||
          window.mozRequestAnimationFrame ||
          window.webkitRequestAnimationFrame ||
          window.msRequestAnimationFrame;

const step = (timestamp) => {
  window.scrollBy(
    0,
    1, // or whatever INTEGER you want (this controls the speed)
  );

  requestAnimationFrame(step);
};


requestAnimationFrame(step);

如果您想稍后取消滚动,您需要拥有对请求动画帧的引用(在您使用requestAnimationFrame(step)的任何地方都要这样做):

this.myRequestAnimationFrame = requestAnimationFrame(step);

const cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;
cancelAnimationFrame(this.myRequestAnimationFrame);

如果您希望在滚动过程中使用缓动并在滚动操作之间设置时间间隔,该怎么办?

创建一个包含60个元素的数组(requestAnimationFrame通常每秒调用60次。实际上,这取决于浏览器的刷新率,但60是最常见的数字)。我们将以非线性方式填充此数组,然后使用这些数字来控制每个requestAnimationFrame步骤中要滚动的量:

let easingPoints = new Array(60).fill(0)

选择一个缓动函数。假设我们正在使用立方体的ease-out:

function easeCubicOut(t) {
    return --t * t * t + 1;
}

创建一个虚拟数组并通过缓动函数填充它的数据。稍后你会明白为什么我们需要这样做。
    // easing function will take care of decrementing t at each call (too lazy to test it at the moment. If it doesn't, just pass it a decrementing value at each call)
    let t = 60;
    const dummyPoints = new Array(60).fill(0).map(()=> easeCubicOut(t));
    const dummyPointsSum = dummyPoints.reduce((a, el) => {
                                a += el;
                               return a;
                           }, 0);

利用每个虚拟点的比率,映射缓动点 easingPoints 到 dummyPointsSum。
    easingPoints = easingPoints.map((el, i) => {
        return Math.round(MY_SCROLL_DISTANCE * dummyPoints[i] / dummyPointsSum);
    });

在你的滚动函数中,我们将进行一些调整:
     const requestAnimationFrame = window.requestAnimationFrame ||
              window.mozRequestAnimationFrame ||
              window.webkitRequestAnimationFrame ||
              window.msRequestAnimationFrame;

     let i = 0;
     const step = (timestamp) => {
       window.scrollBy(
         0,
         easingPoints[i],
       );


        if (++i === 60) {
                i = 0;
                return setTimeout(() => {
                  this.myRequestAnimationFrame = requestAnimationFrame(step);
                }, YOUR_TIMEOUT_HERE);
        }
      };


      this.myRequestAnimationFrame = requestAnimationFrame(step);

迄今为止我见过的最好的解决方案。 - oldboy

2

使用“缓出”效果的另一种可能解决方案。

受到之前提供的某些答案的启发,

一个关键的区别是使用“pace”而不是指定持续时间,我发现根据固定步速计算每个步骤的长度会在滚动接近目标点时创建平滑的“缓出”效果。

希望下面的代码易于理解。

function smoothScrollTo(destination) {
    //check if browser supports smooth scroll
    if (window.CSS.supports('scroll-behavior', 'smooth')) {
        window.scrollTo({ top: destination, behavior: 'smooth' });
    } else {
        const pace = 200;
        let prevTimestamp = performance.now();
        let currentPos = window.scrollY;
        // @param: timestamp is a "DOMHightResTimeStamp", check on MDN
        function step(timestamp) {
            let remainingDistance = currentPos < destination ? destination - currentPos : currentPos - destination;
            let stepDuration = timestamp - prevTimestamp;
            let numOfSteps = pace / stepDuration;
            let stepLength = remainingDistance / numOfSteps;

            currentPos = currentPos < destination ? currentPos + stepLength : currentPos - stepLength;
            window.scrollTo({ top: currentPos });
            prevTimestamp = timestamp;

            if (Math.floor(remainingDistance) >= 1) window.requestAnimationFrame(step);
        }
        window.requestAnimationFrame(step);
    }
}

经过多年受益于这个伟大的社区后,这是我在SO上的第一次贡献。欢迎提供建设性的批评。


这段文字主要是作者对SO社区的感谢和第一次贡献的介绍,并希望得到建设性的批评。

谢谢。对于我的情况来说,这是最好的解决方案。 - Virto111
谢谢。对于我的情况来说,这是最好的解决方案。 - undefined

2

一个适用于Safari的简单jQuery修复方法:

$('a[href*="#"]').not('[href="#"]').not('[href="#0"]').click(function (t) {
    if (location.pathname.replace(/^\//, "") == this.pathname.replace(/^\//, "") && location.hostname == this.hostname) {
        var e = $(this.hash);
        e = e.length ? e : $("[name=" + this.hash.slice(1) + "]"), e.length && (t.preventDefault(), $("html, body").animate({
            scrollTop: e.offset().top
        }, 600, function () {
            var t = $(e);
            if (t.focus(), t.is(":focus")) return !1;
            t.attr("tabindex", "-1"), t.focus()
        }))
    }
});

@康纳·考林:您的代码(我稍作修改)帮助了我在内容进行延迟加载时,防止内容移动破坏最终滚动偏移位置。非常感谢! - AndreasRu

2
结合 George Danielterrymorse 的答案,以下代码可以在所有浏览器中使用原生JavaScript实现。
由于Chrome和Firefox支持CSS的scroll-behavior: smooth;属性,对于不支持此属性的浏览器,我们可以添加以下代码。
HTML:
<a onclick="scrollToSection(event)" href="#section">
    Redirect On section
</a>
  
<section id="section">
  Section Content
</section>

CSS:
body {
  scroll-behavior: smooth;
}

JavaScript:
function scrollToSection(event) {
  if (supportsSmoothScrolling()) {
    return;
  }
  event.preventDefault();
  const scrollToElem = document.getElementById("section");
  SmoothVerticalScrolling(scrollToElem, 300, "top");
}

function supportsSmoothScrolling() {
  const body = document.body;
  const scrollSave = body.style.scrollBehavior;
  body.style.scrollBehavior = 'smooth';
  const hasSmooth = getComputedStyle(body).scrollBehavior === 'smooth';
  body.style.scrollBehavior = scrollSave;
  return hasSmooth;
};
 
function SmoothVerticalScrolling(element, time, position) {
  var eTop = element.getBoundingClientRect().top;
  var eAmt = eTop / 100;
  var curTime = 0;
  while (curTime <= time) {
    window.setTimeout(SVS_B, curTime, eAmt, position);
    curTime += time / 100;
  }
}

function SVS_B(eAmt, position) {
  if (position == "center" || position == "")
  window.scrollBy(0, eAmt / 2);
  if (position == "top")
  window.scrollBy(0, eAmt);
}

和其他答案一样,这在Safari 14.1.2 (MacOS 11.5.1)上不起作用。 - BSUK
1
@BSUK:我怀疑在这里 const scrollToElem = document.getElementById("section");,在你的情况下scrollToElem是null。请确保你有一个id="section"的元素,我们要滚动到它。或者确保你添加了一个正确的id选择器,这样scrollToElem就不会是null了。 - Aniruddha Shevle
1
非常感谢您的帮助!如果我硬编码元素ID,它现在可以工作,但显然它必须根据单击的任何锚链接动态工作(就像默认的平滑滚动行为)。我想我需要提高我的JavaScript水平并改进这个函数。超级烦人的是Safari不能直接支持这种CSS行为。 - BSUK
一个可能的泛化方法是在链接上添加id并将其传递给函数,如<a onclick="scrollToSection(event, this.id)" href="#section" id="section">。然后,函数将接收它并使用它,而不是硬编码的“section” function scrollToSection(event,id) { ... const scrollToElem = document.getElementById(id)...} - Filipe
@Filipe:你肯定可以让它更加动态。这个答案只是为了在所有浏览器上基本平滑滚动工作! - Aniruddha Shevle
显示剩余2条评论

0
感谢T.Dayya,我已经结合了一些关于该主题的答案,并创建了一个带有扩展函数scrollSmoothIntoView的ts模块。
    export default {}
    
    declare global {
    
        interface Element {
            scrollSmoothIntoView(): void;
        }
    }
    
    Element.prototype.scrollSmoothIntoView = function()
    {
        const t = 45;
        const tstep = 6.425/t;
        const dummyPoints = new Array(t).fill(0).map((t, i) => circ(i * tstep));
        const dummyPointsSum = dummyPoints.reduce((a, el) => { a += el; return a;}, 0);
    
        const _window: any = window;
        const _elem: any = getScrollParent(this);
    
        const scroll_distance: any = (this as any).offsetTop - (!_elem.parentElement ? _window.scrollY : 0);
    
        let easingPoints = new Array(t).fill(0)
        easingPoints = easingPoints.map((el, i) => {
            return Math.round(scroll_distance * dummyPoints[i] / dummyPointsSum);
        });
    
        const requestAnimationFrame = _window.requestAnimationFrame ||
            _window.mozRequestAnimationFrame ||
            _window.webkitRequestAnimationFrame ||
            _window.msRequestAnimationFrame;
    
        let i = 0;    
        const step = (timestamp:any) => {
            _elem.scrollBy(0, easingPoints[i]);
    
            if (++i < t)
                setTimeout(() => { requestAnimationFrame(step) }, 2);
        };
    
        window.requestAnimationFrame(()=>requestAnimationFrame(step));
    }
    
    function getScrollParent(element: any, includeHidden?: any):any {
        var style = getComputedStyle(element);
        var excludeStaticParent = style.position === "absolute";
        var overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
    
        if (style.position === "fixed") return document.body;
        for (var parent = element; (parent = parent.parentElement);) {
            style = getComputedStyle(parent);
            if (excludeStaticParent && style.position === "static") {
                continue;
            }
            if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent;
        }
    
        return document.body;
    }
    
    function circ(t:any) {
        return 1+Math.cos(3+t);
    }

使用 html_element.scrollSmoothIntoView()。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接