视频加载失败

访客第一次打开博客时,我想先认真说一句欢迎

1473 字
7 分钟
访客第一次打开博客时,我想先认真说一句欢迎

很多个人博客会在访客进入时弹出提示。有的展示浏览器和操作系统,有的显示 IP、天气和访问次数,也有的连续弹出好几条通知。

我喜欢“有人来时说一句欢迎”这个想法,却不希望它变成开门后的第一道障碍。于是这次做的欢迎提示很轻:只在同一浏览会话第一次打开网站时出现,从右下角进入,停留几秒后自己离开。

首次访问时出现的欢迎提示
首次访问时出现的欢迎提示

欢迎应该出现一次,而不是每页一次#

Firefly 在站内跳转时会替换页面内容。如果每次切换路由都重新执行欢迎逻辑,访客从首页打开文章、再去工具页,会连续看到同一条提示。

我使用 sessionStorage 保存一个会话标记:

const key = "rain_welcome_seen_v1"
if (sessionStorage.getItem(key)) return
sessionStorage.setItem(key, "true")

只要当前标签页没有关闭,它就不会再次出现。重新打开一个新会话时,欢迎提示才会回来。

我没有使用长期的 localStorage,因为那会让看过一次的访客以后永远见不到;也没有使用 Cookie,因为这个状态只和页面体验有关,不需要发给服务器。

先显示,再尝试获取所在地#

提示出现时,会先根据本地时间生成一句问候:

  • 早晨显示“早上好”;
  • 下午显示“下午好”;
  • 深夜则把语气放轻。

随后才在后台请求所在地。如果接口顺利返回,文案会补充为“来自某地的朋友,很高兴遇见你”;如果请求超时、被浏览器拦截或接口暂时不可用,就保留普通欢迎语。

这一步的顺序很重要。第三方接口不应该决定组件能不能出现,更不能拖慢博客正文。请求设置了较短超时,并且使用 AbortController 主动终止。定位只是锦上添花,失败不是页面错误。

动画要让人看见,也要允许离开#

弹窗使用位移、缩放和透明度完成进入动画,时间控制得比较短。右上角保留关闭按钮,不要求访客一定等自动计时结束。

在手机上,它会避开底部悬浮导航;如果系统开启“减少动态效果”,位移动画会被取消,只做轻微淡入淡出。

这些处理看起来比“显示一段文字”复杂,但它们共同决定了提示会不会打扰人:

  • 不遮挡主要按钮;
  • 不抢占页面焦点;
  • 可以手动关闭;
  • 几秒后自动消失;
  • 同一会话不重复;
  • 接口失败不报错。

建立欢迎提示组件#

组件放在:

src/components/features/WelcomeToast.astro

它不是侧栏内容,而是覆盖在页面右下角,所以直接写成全局功能组件:

<welcome-toast
class="welcome-toast"
aria-live="polite"
aria-atomic="true"
data-state="idle"
>
<div class="welcome-toast-icon">✦</div>
<div>
<span class="welcome-toast-kicker">WELCOME</span>
<strong class="welcome-toast-title">欢迎来到朝雨听雨</strong>
<p class="welcome-toast-message">很高兴在这里遇见你。</p>
</div>
<button class="welcome-toast-close" aria-label="关闭欢迎提示">×</button>
</welcome-toast>

aria-live="polite" 会让辅助技术在合适的时机读出提示,又不会强行打断正在阅读的内容。

显示与关闭只依赖 data-state

.welcome-toast {
opacity: 0;
pointer-events: none;
transform: translateY(calc(100% + 2rem)) scale(0.96);
transition:
opacity 320ms ease,
transform 420ms cubic-bezier(0.22, 1, 0.36, 1);
}
.welcome-toast[data-state="visible"] {
opacity: 1;
pointer-events: auto;
transform: translateY(0) scale(1);
}

在窄屏下要给底部导航留出空间:

@media (max-width: 640px) {
.welcome-toast {
right: 1rem;
bottom: calc(5rem + env(safe-area-inset-bottom));
left: 1rem;
width: auto;
}
}

会话判断与定位回退#

初始化时先读 sessionStorage,没有标记才延迟显示:

connectedCallback() {
if (sessionStorage.getItem("rain_welcome_seen_v1")) return
this.querySelector(".welcome-toast-close")
?.addEventListener("click", () => this.close(), { once: true })
this.showTimer = window.setTimeout(() => this.show(), 850)
}

show() 中先写入会话标记并显示普通欢迎语,再发起定位请求:

sessionStorage.setItem("rain_welcome_seen_v1", "true")
this.dataset.state = "visible"
const response = await fetch("https://v2.xxapi.cn/api/ip", {
signal: this.abortController.signal,
})

接口返回结构可能变化,因此不要直接假设地址一定在某个字段。我的处理会依次尝试 data.addressdata.locationdata.city 等候选值。任何异常都会进入 catch,保留“很高兴在这里遇见你”的默认文本。

为了防止请求长时间占用,设置 3.5 秒超时:

this.abortController = new AbortController()
const timeout = window.setTimeout(
() => this.abortController?.abort(),
3500,
)

关闭组件或离开页面时,也要终止请求并清理计时器。

挂载到全局布局#

src/layouts/Layout.astro 顶部导入:

import WelcomeToast from "@components/features/WelcomeToast.astro"

然后在 body 末尾、其他全局效果附近挂载:

<SakuraEffect />
<WelcomeToast />
<FancyboxManager />

测试“只出现一次”时,普通刷新不会再次显示,因为当前标签页的 sessionStorage 仍然存在。可以新开一个标签页,或者在开发者工具的 Application 面板中删除 rain_welcome_seen_v1 后再刷新。

还需要主动测试断网或接口失败的情况。一个合格的欢迎提示应该在没有定位数据时照常出现和关闭,而不是在控制台抛出未处理异常。

个性化不等于收集更多信息#

位置欢迎很容易继续扩展:显示 IP、网络运营商、浏览器、系统版本,甚至根据天气改变文案。

我没有继续加。访客来到博客,是为了看内容,不是为了被告知网站识别出了多少信息。现在只使用接口返回的概略地区来组织一句话,获取不到就立刻退回普通欢迎,并且不保存这些数据。

这也符合我对第二代博客的想法:可以有技术细节,也可以有一点个人气息,但不需要把每一种能力都展示出来。

欢迎提示最终只停留几秒。它不会成为网站里最重要的功能,却能在第一次见面时替我说一句:你好,欢迎来到朝雨听雨。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
访客第一次打开博客时,我想先认真说一句欢迎
http://rainzt.cn/posts/welcome-toast/
作者
Rain
发布于
2026-07-19
许可协议
CC BY-NC-SA 4.0

评论区

文章目录

WELCOME欢迎来到朝雨听雨

很高兴在这里遇见你。