使用 Swup 实现网页平滑过渡

Swup 用于服务器呈现网站的多功能且可扩展的页面过渡库。

最近看到 Astro ,感觉不错。试了试,虽然感觉生成速度很慢,但是工具链确实很现代化。比如支持 MDX,生成后进行文件压缩,图片转 WebP,少量 js 实现的页面过渡等等功能。

其中比较喜欢 Astro PaperFuwari 主题,这两个都有类似 SPA 应用效果,Paper 使用的应该是 Astro4 自带的 API。而 Fuwari,使用的是 Swup 实现的。 Fuwari Demo

下面进行简单尝试。

官网介绍

Swup 是一个多功能且可扩展的页面过渡库,适用于 SSR 网站。 它管理整个页面加载生命周期,并在当前和下一个之间平滑地制作动画页。此外,它还提供了许多其他生命周期改进,例如缓存、智能预加载、 本机浏览器历史记录和增强的可访问性。无需复杂操作,即可让您的网站感觉像一个 SPA 单页应用程序。

官网 https://swup.js.org/

虽然适用于 SSR 网站,但静态博客也能用。

以本博客站点为例, layouts/_default/baseof.html 添加

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    <script src="https://unpkg.com/swup@4"></script>
    <script src="https://unpkg.com/@swup/scripts-plugin@2"></script>
    <script data-swup-ignore-script> 
        const swup = new Swup({
            plugins: [new SwupScriptsPlugin({
                body: true,
                head: true
            })]
        });
    </script>

这段代码引入了 swup 主要 js 和一个插件。插件主要作用是保证 body ,head js 能够正常运行。(但不保证完全正常)

data-swup-ignore-script 是告诉插件 SwupScriptsPlugin 这段 js 不必重复执行。

除此之外,还需要根据错误,适当修改代码。比如把需要重新执行 js 放到 body ,head 或 main 里。总的来说,增加了页面维护难度。

比如 DoIt 这个主题,我把 {{- partial "assets.html" . -}} 放到 <main></main> 里,评论有时还不会生效。

main 增加 transition-fade class 和 id swup

1
<main class="main transition-fade" id="swup">

css 增加

1
2
3
4
5
6
7
8
html.is-changing .transition-fade {
  transition: opacity 0.1s;
  opacity: 1;
}
/* Define the styles for the unloaded pages */
html.is-animating .transition-fade {
  opacity: 0;
}

忽略控制台报错和一些无法正常工作的脚本还是不错的。按照 4.5.1 版本来看, https://cdn.staticfile.net/swup/4.5.1/Swup.umd.min.js 文件,这个 js 大概 10kb ,未压缩 25kb 左右。插件未压缩不到 3kb 。

http://instantclick.io/ 预加载,访问体验优化。并使用 pushState 和 Ajax (pjax) 替换正文。可能会导致部分 js 失效,增加维护难度。5.9kb
https://instant.page/ 预加载缓存 4.2kb

baseof.html 完整代码参考

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
{{- partial "init.html" . -}}

<!DOCTYPE html>
<html lang="{{ .Site.LanguageCode }}">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="robots" content="noodp" />

    {{- /* Paginate  */ -}}
    {{- /* Paginate in here, To solve the problem of the canonical URL being the same in the pagination */ -}}
    {{- /* see more https://github.com/gohugoio/hugo/issues/4507 */ -}}
    {{- /* see more https://discourse.gohugo.io/t/control-pagination-and-page-collections-from-baseof-html/37643/8 */ -}}
    {{- /* see more https://discourse.gohugo.io/t/determine-if-current-page-is-result-of-pagination/37494/4 */ -}}
    {{- partial "head/paginator.html" . -}}

    <title>
        {{- block "title" . }}{{ .Site.Title }}{{ end -}}
    </title>

    {{- partial "head/meta.html" . -}}
    {{- partial "head/link.html" . -}}
    {{- partial "head/seo.html" . -}}
    
{{- $instantpage := .Scratch.Get "instantpage" | default dict -}}
{{- if $instantpage.enable -}}
    <script src="//instant.page/5.2.0" defer type="module" integrity="sha384-jnZyxPjiipYXnSU0ygqeac2q7CVYMbh84q0uHVRRxEtvFPiQYbXWUorga2aqZJ0z"></script>
{{- end -}}

{{- $instantpage := .Site.Params.page.instantpage.enable -}}
{{- if $instantpage -}}
{{- $js := resources.Get "/lib/instant.page/instantpage.min.js" -}}
  <script
    src="{{ $js.RelPermalink }}"
    defer
    type="module"
  ></script>
{{- end -}}
    <script src="https://cdn.staticfile.net/swup/4.5.1/Swup.umd.min.js"></script>
    <script src="https://unpkg.com/@swup/scripts-plugin@2"></script>
    <script data-swup-ignore-script>
        const swup = new Swup({
            plugins: [new SwupScriptsPlugin({
                body: true,
                head: true
            })]
        });
        document.addEventListener('DOMContentLoaded', () => {
            console.log("Swup Running");
        });
    </script>
</head>

<body header-desktop="{{ .Site.Params.header.desktopMode }}" header-mobile="{{ .Site.Params.header.mobileMode }}">
    {{- /* Check theme isDark before body rendering */ -}}
    {{- $theme := .Site.Params.defaulttheme -}}
    <script data-swup-ignore-script type="text/javascript">
        function setTheme(theme) {
          document.body.setAttribute('theme', theme); 
          document.documentElement.style.setProperty('color-scheme', theme === 'light' ? 'light' : 'dark');
          if (theme === 'light') {
            document.documentElement.classList.remove('tw-dark')
          } else {
            document.documentElement.classList.add('tw-dark')
          }
          window.theme = theme;   
          window.isDark = window.theme !== 'light' 
        }
        function saveTheme(theme) {window.localStorage && localStorage.setItem('theme', theme);}
        function getMeta(metaName) {const metas = document.getElementsByTagName('meta'); for (let i = 0; i < metas.length; i++) if (metas[i].getAttribute('name') === metaName) return metas[i]; return '';}
        if (window.localStorage && localStorage.getItem('theme')) {let theme = localStorage.getItem('theme');theme === 'light' || theme === 'dark' || theme === 'black' ? setTheme(theme) : (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? setTheme('dark') : setTheme('light')); } else { if ('{{ $theme }}' === 'light' || '{{ $theme }}' === 'dark' || '{{ $theme }}' === 'black') setTheme('{{ $theme }}'), saveTheme('{{ $theme }}'); else saveTheme('auto'), window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? setTheme('dark') : setTheme('light');}
        let metaColors = {'light': '#f8f8f8','dark': '#252627','black': '#000000'}
        getMeta('theme-color').content = metaColors[document.body.getAttribute('theme')];
        window.switchThemeEventSet = new Set()
    </script>
    <div id="back-to-top"></div>
    <div id="mask"></div>

    {{- /* Body wrapper */ -}}
    <div class="wrapper">
        {{- partial "header.html" . -}}
        <main class="main transition-fade" id="swup">
            <div class="container">
                {{- block "content" . }}{{ end -}}
            </div>
            {{- /* Load JavaScript scripts and CSS */ -}}
            {{- partial "assets.html" . -}}
        </main>
        {{- partial "footer.html" . -}}
    </div>

    <div id="fixed-buttons" class="print:!tw-hidden">
        {{- /* top button */ -}}
        <a href="#back-to-top" id="back-to-top-button" class="fixed-button" title="{{ T `backToTop` }}">
            {{ partial "plugin/fontawesome.html" (dict "Style" "solid" "Icon" "arrow-up") }}
        </a>

        {{- /* comment button */ -}}
        <a href="#" id="view-comments" class="fixed-button" title="{{ T `viewComments` }}">
            {{ partial "plugin/fontawesome.html" (dict "Style" "solid" "Icon" "comment") }}
        </a>
    </div>


</body>

</html>