Nuxt3 切换主题色且不闪屏该怎么做?

Nuxt3 支持服务端渲染给用户带来了更好的首屏体验,但是给只熟悉单页应用开发的前端同学也带来了一定的学习成本。为了更好的 SEO,我也是选择用 Nuxt3 来重构我的 类型小屋 这个用 Vue3 写的项目,然而开发过程中遇到了很多问题,这篇文章先讲一个【切换主题色】功能遇到的问题。

后面应该还会写更多文章来讲下关于 Nuxt3 实战上的一些问题,绝对干货,有兴趣的朋友可以关注我。

服务端渲染基本概念

理解服务端渲染的概念是很重要的,不然面对不同的需求场景,你可能会走很多弯路,简单来说,服务端渲染意味着 网页上面呈现的内容在服务端就已经生成好了

举个例子,如果我们是一个用 Vue3 写的单页应用,我们首次请求到的根 html 可能是这样的:

html 复制代码
<html>
  <head>
    ...
    <script src="/main.js"></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

很简单,就一个根元素(id 为 app),以及一个加载其他元素的 main.js 文件,这个文件里面会去初始化 Vue 实例,然后挂载到根元素上。

而服务端渲染的流程是,我们的服务器收到请求后,会在服务端解析 Vue 写的代码,然后把生成的 html 字符串返回给客户端,客户端拿到 html 字符串后,会直接渲染到页面上,比如我们首次请求到的 html 可能是这样的:

html 复制代码
<html>
  <head>...</head>
  <body>
    <h1>Hello World</h1>
    <main>I have already mounted on serve-render</main>
  </body>
</html>

服务端渲染的 html 字符串中已经包含了我们需要的内容,直接渲染到页面上。不过随着项目的复杂度上升,你肯定也会在客户端加载一些其他的 js 的,大家别弄混了。

window 对象丢失

在服务端渲染过程中,在解析 vue 代码过程中去获取 window 对象,会发现 window 对象是不存在的,因为 window 这种毕竟是浏览器的概念,在服务端渲染过程中,没有浏览器的执行环境。

所以如果你在 Nuxt3 中写以下代码是会报错的:

ts 复制代码
<script lang="ts" setup>
console.log(window.localStorage.setItem('key', 'value'))
</script>

主题色切换思路

以下是我做主题色切换的思路。

定义 css 变量

style/theme.css 文件:

css 复制代码
body {
  --color-text: #000;
  --color-bg: #fff;
}

body[theme='dark'] {
  --color-text: #fff;
  --color-bg: #0d1117;
}

当在 body 元素上加上 theme="dark" 这个属性后,我们的主题色就会切换到暗色主题。

不过我们得先把这个主题文件加到 nuxt.config.ts 配置文件中:

ts 复制代码
export default defineNuxtConfig({
  css: ['@/style/theme.css'],
})

引入 pinia

我们需要一个全局的状态管理,好在 Nuxt3 提供了 @pinia/nuxt 这个同时支持服务端和客户端的状态管理模块。我们先来安装一下:

bash 复制代码
yarn add @pinia/nuxt pinia pinia-plugin-persistedstate

pinia-plugin-persistedstate 这个包是用于持久化的。

安装完成后配置 nuxt.config.ts 文件:

ts 复制代码
export default defineNuxtConfig({
  css: ['@/style/theme.css'],
  modules: [
    '@pinia/nuxt',
  ],
})

写入 store

在根目录下创建 store/index.ts 文件,写入以下代码:

ts 复制代码
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import useGlobalStore from './theme'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

export { useProblemStore }
export default pinia

继续创建 store/theme.ts 文件,写入以下代码:

ts 复制代码
import { defineStore } from 'pinia'

export type ThemeType = 'system' | 'light' | 'dark'

interface GlobalState {
  fakeTheme: ThemeType
  theme: Exclude<ThemeType, 'system'>
}

const useThemeStroe = defineStore('global', {
  state: (): GlobalState => ({
    fakeTheme: 'light',
    theme: 'light',
  }),
  actions: {
    setRealTheme(theme: Exclude<ThemeType, 'system'>) {
      this.theme = theme
      if (theme === 'light') {
        document.body.removeAttribute('theme')
      } else {
        document.body.setAttribute('theme', 'dark')
      }
    },
    setTheme(theme: ThemeType) {
      window.localStorage.setItem('theme', theme)
      this.fakeTheme = theme
      if (theme === 'system') {
        const themeMedia = window.matchMedia('(prefers-color-scheme: dark)')
        this.setRealTheme(themeMedia.matches ? 'dark' : 'light')
        themeMedia.addEventListener('change', (evt) => {
          const t = evt.matches ? 'dark' : 'light'
          this.setRealTheme(t)
        })
      } else {
        this.setRealTheme(theme)
      }
    },
  },
  persist: true,
})

export default useThemeStroe

然后我们随便在 /pages 目录下随便创建一个页面,就叫 /pages/index.vue 吧,写入以下代码:

ts 复制代码
<template>
  <h1 class="title">
    I am index page
  </h1>
  <button @click="themeStroe.setTheme('system')">跟随系统</button>
  <button @click="themeStroe.setTheme('dark')">暗色</button>
  <button @click="themeStroe.setTheme('light')"> 亮色</button>
</template>

<script lang="ts" setup>
import { useThemeStroe } from '@/store'

const themeStroe = useThemeStroe()
</script>

<style scoped>
.title {
  background-color: var(--color-bg);
  color: var(--color-text);
}
</style>

OK,可以看到引入了 css 变量,点击不同的主题色按钮,就可以切换颜色了。

注意看的话,切换不同主题色,body 元素会有一个 theme="dark" 属性的添加与删除,如果有这个属性,就会走暗色的 css 颜色变量,从而实现主题色切换。

读写本地主题色

store/theme.ts 文件中,我们改变主题色时,会通过以下代码把用户设置的主题色存到本地,以达到持久化目的:

ts 复制代码
window.localStorage.setItem('theme', theme)

在用户下次进入页面时,我们在通过以下代码读取到这个值:

ts 复制代码
window.localStorage.getItem('theme')

我们直接在咱们的 /pages/index.vue 加入以下代码:

ts 复制代码
<script lang="ts" setup>
import { useThemeStroe } from '@/store'
import type { ThemeType } from '@/store/theme'

const themeStroe = useThemeStroe()

themeStroe.setTheme((window.localStorage.getItem('theme') as ThemeType) || 'light')
</script>

好家伙,一看页面,又报错了:

原因上面我们已经说过了,那好,我放到客户端渲染完成总行了吧,我们尝试着用写:

ts 复制代码
// onMounted 回调只会在客户端渲染执行
onMounted(() => {
  themeStroe.setTheme((window.localStorage.getItem('theme') as ThemeType) || 'light')
})

再去页面,你先点击【暗色】按钮,然后多刷新几遍观察下:

是不是出现了短暂亮色再变成暗色的生硬过渡,这对于用户的体验来说是很差劲的,我这里只是简单一行文字,如果你的网站是全局的主题变换,闪屏会更加明显与难看。

而这就是我们要解决的问题。

防止闪屏

服务端渲染拿不到 window 对象,从而无法访问 window.matchMediawindow.localStorage 这些方法,也就无法给 body 元素添加和删除属性。

那有没有一个阶段是,服务端渲染好传给了客户端,但是客户端还未渲染完成,却又拥有浏览器环境。

很幸运,Nuxt3 提供了一个强大的 hook 供我们使用,它就是我们常拿来进行 SEO 优化的 useHead

直接上我们最重要的代码:

ts 复制代码
<script lang="ts" setup>
// 其他代码保持不变,onMounted 也要留着,给客户端渲染用

useHead({
  script: [
    {
      // @ts-ignore
      body: true,
      children: `
      const theme = window.localStorage.getItem('theme') || 'light';
      if (theme === 'system') {
        const themeMedia = window.matchMedia('(prefers-color-scheme: dark)')
        if (themeMedia.matches) {
          document.body.setAttribute('theme', 'dark')
        } else {
          document.body.removeAttribute('theme')
        }
      } else {
        if (theme === 'dark') {
          document.body.setAttribute('theme', 'dark')
        } else {
          document.body.removeAttribute('theme')
        }
      }`,
    },
  ],
})
</script>

这段代码的作用是,插入一个脚本,这个脚本的功能和我们 store/theme.ts 的类似,拿到用户配置的主题变量,根据这个变量去决定在 body 元素上添加或删除 theme="dark" 属性。

需要注意的是 body: true 这个配置,目的是把脚本挂在到 body 元素下,而不是 head 元素下,我也是找到官方的这个 issue 才发现有这个用法的:

github.com/nuxt/nuxt/i...

然而 Nuxt3 的类型定义似乎没把这个加上,必须加个 @ts-ignore 防止报错,实际功能是有的:

这时候你再去页面,不管先点击【跟随系统】还是【暗色】按钮,重复刷新,你会发现已经没有闪屏了,完美!

demo 代码已上传,大家有兴趣可以看看:

点我查看 demo

最后

Nuxt3 的开发模式和 Vue3 还是有很多不同的,主要是服务端渲染这个过程和我们以往的心智模型不太一样,所以也是在重构过程中踩了很多坑,后面慢慢出文章,希望能帮助到有需要的朋友。

为什么要写这个?说真的,我翻遍全网,没有一篇是讲这个的。

相关推荐
机智的奎哥2 分钟前
微信小程序实现长按录音,点击播放等功能,CSS实现语音录制动画效果
前端·javascript·css·微信·微信小程序·小程序
XDU小迷弟39 分钟前
第30天:PHP应用&组件框架&前端模版渲染&三方插件&富文本编辑器&CVE审计
开发语言·前端·网络安全·php
明月看潮生41 分钟前
青少年编程与数学 02-006 前端开发框架VUE 09课题、计算属性
前端·javascript·vue.js·青少年编程·编程与数学
轩轩9902181 小时前
关于vue.js组件开发
vue.js
布兰妮甜1 小时前
Three.js - 打开Web 3D世界的大门
前端·javascript·3d·动画·three.js
再学一点就睡1 小时前
一文搞懂 Vue 组件通信,附详细代码案例
vue.js
小皮虾1 小时前
几行代码封装,让小程序云函数变为真正云函数,开发体验直接起飞
前端·javascript·微信小程序
Traced back1 小时前
在vue3项目中利用自定义ref实现防抖
前端·javascript·vue.js
木易66丶2 小时前
Vue中el-tree结合vuedraggable实现跨组件元素拖拽
前端·笔记
黑客KKKing2 小时前
网络安全-web应用程序发展历程(基础篇)
前端·安全·web安全