We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Nuxt3 支持服务端渲染给用户带来了更好的首屏体验,但是给只熟悉单页应用开发的前端同学也带来了一定的学习成本。为了更好的 SEO,我也是选择用 Nuxt3 来重构我的 类型小屋 这个用 Vue3 写的项目,然而开发过程中遇到了很多问题,这篇文章先讲一个【切换主题色】功能遇到的问题。
后面应该还会写更多文章来讲下关于 Nuxt3 实战上的一些问题,绝对干货,有兴趣的朋友可以关注我。
理解服务端渲染的概念是很重要的,不然面对不同的需求场景,你可能会走很多弯路,简单来说,服务端渲染意味着 网页上面呈现的内容在服务端就已经生成好了。
举个例子,如果我们是一个用 Vue3 写的单页应用,我们首次请求到的根 html 可能是这样的:
<html> <head> ... <script src="/main.js"></script> </head> <body> <div id="app"></div> </body> </html>
很简单,就一个根元素(id 为 app),以及一个加载其他元素的 main.js 文件,这个文件里面会去初始化 Vue 实例,然后挂载到根元素上。
main.js
而服务端渲染的流程是,我们的服务器收到请求后,会在服务端解析 Vue 写的代码,然后把生成的 html 字符串返回给客户端,客户端拿到 html 字符串后,会直接渲染到页面上,比如我们首次请求到的 html 可能是这样的:
<html> <head>...</head> <body> <h1>Hello World</h1> <main>I have already mounted on serve-render</main> </body> </html>
服务端渲染的 html 字符串中已经包含了我们需要的内容,直接渲染到页面上。不过随着项目的复杂度上升,你肯定也会在客户端加载一些其他的 js 的,大家别弄混了。
在服务端渲染过程中,在解析 vue 代码过程中去获取 window 对象,会发现 window 对象是不存在的,因为 window 这种毕竟是浏览器的概念,在服务端渲染过程中,没有浏览器的执行环境。
所以如果你在 Nuxt3 中写以下代码是会报错的:
<script lang="ts" setup> console.log(window.localStorage.setItem('key', 'value')) </script>
以下是我做主题色切换的思路。
style/theme.css 文件:
style/theme.css
body { --color-text: #000; --color-bg: #fff; } body[theme='dark'] { --color-text: #fff; --color-bg: #0d1117; }
当在 body 元素上加上 theme="dark" 这个属性后,我们的主题色就会切换到暗色主题。
body
theme="dark"
不过我们得先把这个主题文件加到 nuxt.config.ts 配置文件中:
nuxt.config.ts
export default defineNuxtConfig({ css: ['@/style/theme.css'], })
我们需要一个全局的状态管理,好在 Nuxt3 提供了 @pinia/nuxt 这个同时支持服务端和客户端的状态管理模块。我们先来安装一下:
@pinia/nuxt
yarn add @pinia/nuxt pinia pinia-plugin-persistedstate
pinia-plugin-persistedstate 这个包是用于持久化的。
pinia-plugin-persistedstate
安装完成后配置 nuxt.config.ts 文件:
export default defineNuxtConfig({ css: ['@/style/theme.css'], modules: [ '@pinia/nuxt', ], })
在根目录下创建 store/index.ts 文件,写入以下代码:
store/index.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 文件,写入以下代码:
store/theme.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 吧,写入以下代码:
/pages
/pages/index.vue
<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 文件中,我们改变主题色时,会通过以下代码把用户设置的主题色存到本地,以达到持久化目的:
window.localStorage.setItem('theme', theme)
在用户下次进入页面时,我们在通过以下代码读取到这个值:
window.localStorage.getItem('theme')
我们直接在咱们的 /pages/index.vue 加入以下代码:
<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>
好家伙,一看页面,又报错了:
原因上面我们已经说过了,那好,我放到客户端渲染完成总行了吧,我们尝试着用写:
// onMounted 回调只会在客户端渲染执行 onMounted(() => { themeStroe.setTheme((window.localStorage.getItem('theme') as ThemeType) || 'light') })
再去页面,你先点击【暗色】按钮,然后多刷新几遍观察下:
是不是出现了短暂亮色再变成暗色的生硬过渡,这对于用户的体验来说是很差劲的,我这里只是简单一行文字,如果你的网站是全局的主题变换,闪屏会更加明显与难看。
而这就是我们要解决的问题。
服务端渲染拿不到 window 对象,从而无法访问 window.matchMedia 和 window.localStorage 这些方法,也就无法给 body 元素添加和删除属性。
window.matchMedia
window.localStorage
那有没有一个阶段是,服务端渲染好传给了客户端,但是客户端还未渲染完成,却又拥有浏览器环境。
很幸运,Nuxt3 提供了一个强大的 hook 供我们使用,它就是我们常拿来进行 SEO 优化的 useHead。
直接上我们最重要的代码:
<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 才发现有这个用法的:
body: true
head
nuxt/nuxt#13069
然而 Nuxt3 的类型定义似乎没把这个加上,必须加个 @ts-ignore 防止报错,实际功能是有的:
@ts-ignore
这时候你再去页面,不管先点击【跟随系统】还是【暗色】按钮,重复刷新,你会发现已经没有闪屏了,完美!
demo 代码已上传,大家有兴趣可以看看:
点我查看 demo
Nuxt3 的开发模式和 Vue3 还是有很多不同的,主要是服务端渲染这个过程和我们以往的心智模型不太一样,所以也是在重构过程中踩了很多坑,后面慢慢出文章,希望能帮助到有需要的朋友。
为什么要写这个?说真的,我翻遍全网,没有一篇是讲这个的。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
Nuxt3 支持服务端渲染给用户带来了更好的首屏体验,但是给只熟悉单页应用开发的前端同学也带来了一定的学习成本。为了更好的 SEO,我也是选择用 Nuxt3 来重构我的 类型小屋 这个用 Vue3 写的项目,然而开发过程中遇到了很多问题,这篇文章先讲一个【切换主题色】功能遇到的问题。
服务端渲染基本概念
理解服务端渲染的概念是很重要的,不然面对不同的需求场景,你可能会走很多弯路,简单来说,服务端渲染意味着 网页上面呈现的内容在服务端就已经生成好了。
举个例子,如果我们是一个用 Vue3 写的单页应用,我们首次请求到的根 html 可能是这样的:
很简单,就一个根元素(id 为 app),以及一个加载其他元素的
main.js
文件,这个文件里面会去初始化 Vue 实例,然后挂载到根元素上。而服务端渲染的流程是,我们的服务器收到请求后,会在服务端解析 Vue 写的代码,然后把生成的 html 字符串返回给客户端,客户端拿到 html 字符串后,会直接渲染到页面上,比如我们首次请求到的 html 可能是这样的:
服务端渲染的 html 字符串中已经包含了我们需要的内容,直接渲染到页面上。不过随着项目的复杂度上升,你肯定也会在客户端加载一些其他的 js 的,大家别弄混了。
window 对象丢失
在服务端渲染过程中,在解析 vue 代码过程中去获取 window 对象,会发现 window 对象是不存在的,因为 window 这种毕竟是浏览器的概念,在服务端渲染过程中,没有浏览器的执行环境。
所以如果你在 Nuxt3 中写以下代码是会报错的:
主题色切换思路
以下是我做主题色切换的思路。
定义 css 变量
style/theme.css
文件:当在
body
元素上加上theme="dark"
这个属性后,我们的主题色就会切换到暗色主题。不过我们得先把这个主题文件加到
nuxt.config.ts
配置文件中:引入 pinia
我们需要一个全局的状态管理,好在 Nuxt3 提供了
@pinia/nuxt
这个同时支持服务端和客户端的状态管理模块。我们先来安装一下:pinia-plugin-persistedstate
这个包是用于持久化的。安装完成后配置
nuxt.config.ts
文件:写入 store
在根目录下创建
store/index.ts
文件,写入以下代码:继续创建
store/theme.ts
文件,写入以下代码:然后我们随便在
/pages
目录下随便创建一个页面,就叫/pages/index.vue
吧,写入以下代码:OK,可以看到引入了 css 变量,点击不同的主题色按钮,就可以切换颜色了。
注意看的话,切换不同主题色,
body
元素会有一个theme="dark"
属性的添加与删除,如果有这个属性,就会走暗色的 css 颜色变量,从而实现主题色切换。读写本地主题色
在
store/theme.ts
文件中,我们改变主题色时,会通过以下代码把用户设置的主题色存到本地,以达到持久化目的:在用户下次进入页面时,我们在通过以下代码读取到这个值:
我们直接在咱们的
/pages/index.vue
加入以下代码:好家伙,一看页面,又报错了:
原因上面我们已经说过了,那好,我放到客户端渲染完成总行了吧,我们尝试着用写:
再去页面,你先点击【暗色】按钮,然后多刷新几遍观察下:
是不是出现了短暂亮色再变成暗色的生硬过渡,这对于用户的体验来说是很差劲的,我这里只是简单一行文字,如果你的网站是全局的主题变换,闪屏会更加明显与难看。
而这就是我们要解决的问题。
防止闪屏
服务端渲染拿不到 window 对象,从而无法访问
window.matchMedia
和window.localStorage
这些方法,也就无法给body
元素添加和删除属性。那有没有一个阶段是,服务端渲染好传给了客户端,但是客户端还未渲染完成,却又拥有浏览器环境。
很幸运,Nuxt3 提供了一个强大的 hook 供我们使用,它就是我们常拿来进行 SEO 优化的 useHead。
直接上我们最重要的代码:
这段代码的作用是,插入一个脚本,这个脚本的功能和我们
store/theme.ts
的类似,拿到用户配置的主题变量,根据这个变量去决定在body
元素上添加或删除theme="dark"
属性。需要注意的是
body: true
这个配置,目的是把脚本挂在到body
元素下,而不是head
元素下,我也是找到官方的这个 issue 才发现有这个用法的:nuxt/nuxt#13069
然而 Nuxt3 的类型定义似乎没把这个加上,必须加个
@ts-ignore
防止报错,实际功能是有的:这时候你再去页面,不管先点击【跟随系统】还是【暗色】按钮,重复刷新,你会发现已经没有闪屏了,完美!
demo 代码已上传,大家有兴趣可以看看:
点我查看 demo
最后
Nuxt3 的开发模式和 Vue3 还是有很多不同的,主要是服务端渲染这个过程和我们以往的心智模型不太一样,所以也是在重构过程中踩了很多坑,后面慢慢出文章,希望能帮助到有需要的朋友。
为什么要写这个?说真的,我翻遍全网,没有一篇是讲这个的。
The text was updated successfully, but these errors were encountered: