# 使用 Swup 实现网页平滑过渡

> [使用 Swup 实现网页平滑过渡](https://www.ftls.xyz/posts/2024-04-04-swup/)
> Penned by [恐咖兵糖](https://www.ftls.xyz/) on 2024-04-04


Swup 用于服务器呈现网站的多功能且可扩展的页面过渡库。

<!--more-->

## 缘起

最近看到 Astro ，感觉不错。试了试，虽然感觉生成速度很慢，但是工具链确实很现代化。比如支持 MDX，生成后进行文件压缩，图片转 WebP，少量 js 实现的页面过渡等等功能。 

其中比较喜欢 [Astro Paper](https://github.com/satnaing/astro-paper) 和 [Fuwari](https://github.com/saicaca/fuwari) 主题，这两个都有类似 SPA 应用效果，Paper 使用的应该是 Astro4 自带的 API。而 Fuwari，使用的是 Swup 实现的。 [Fuwari Demo](https://fuwari.vercel.app/) 

下面进行简单尝试。

## 信息

官网介绍

> Swup 是一个多功能且可扩展的页面过渡库，适用于 SSR 网站。 它管理整个页面加载生命周期，并在当前和下一个之间平滑地制作动画页。此外，它还提供了许多其他生命周期改进，例如缓存、智能预加载、 本机浏览器历史记录和增强的可访问性。无需复杂操作，即可让您的网站感觉像一个 SPA 单页应用程序。

官网 https://swup.js.org/

虽然适用于 SSR 网站，但静态博客也能用。


## 示例

以本博客站点为例， layouts/_default/baseof.html 添加

```html
    <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`。

```html
<main class="main transition-fade" id="swup">
```

css 增加

```css
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 完整代码参考

```html
{{- 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>
```

## 后续

因为我目前使用的 DoIt 主题 JS 写的有点复杂，加上 Swup 会导致部分 JS 失效，就删除了。
Swup.js 效果可见

https://kkbt0.github.io/Hugo-Landscape/
https://kkbt0.github.io/Somnia/
