NextJs解决rem布局在client端布局闪烁问题

98 阅读3分钟

NextJs 中,通过script,引入lib-flexible,在client计算rem

背景

Next.js 中,我们通常会在页面中引入一些 client script,比如 Google Analyticslib-flexible 或者一些自定义的脚本。但是,这些脚本通常需要在页面加载后才能执行,否则可能会出现错误。因此,我们需要在页面加载后,动态地引入这些脚本。本本主要是在ssr场景下提供rem布局在client端布局闪烁问题提供一个解决方案。

解决方案

核心思想:通过next/script来实现preload script,然后通过cookie来读取fontSize,实现ssr场景布局闪烁问题

上述方案也有一个问题,第一次加载,无法读取fontSize,希望大家可以先思考一下有什么更好的解决方案,在文末会提供一种解决方案

本次主要有两种写法供大家参考

第一种写法

直接通过 Script 标签引入,然后在script里面去设置cookie就可以了

// client script
(()=> {
  //... 计算rem
  const rem = 16;
  // 设置cookie
  document.cookie = 'fontSize=' + rem+'px; ';
})(window, document)
// app/layout.tsx
import Script from 'next/script'
import { cookies } from 'next/headers'
import type { Metadata } from 'next'
import Layout from '@/components/Layout'
import './globals.css'
export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  const cookieStore = await cookies() // 读取cookie中的fontSize,
  const fontSize = cookieStore.get('fontSize')?.value
  return (
    <html lang="en" style={{ fontSize: fontSize }}>
      <head>
        <Script id="analytics-script" src="flexble.js" />
      </head>
      <body>
        <Layout>{children}</Layout>
      </body>
    </html>
  )
}


到此,这样就可以解决了,只是会出现上面提到的问题,这样布局闪烁总是给人是个bug的感觉,与其解决bug,倒不如隐藏起来,这样大家就看不到了,看不见就当不存在了。

思路: 在第一次没有拿到fontSize的时候,我们可以给html或者body设置一个opacity:0,这样就可以先隐藏起来了,然后在script里面,通过onload时间里面,把html或者bodyopacity设置为1,这样就可以解决上面的问题了。

// client script
(()=> {
  //... 计算rem
  const rem = 16;
  // 设置cookie
  document.cookie = 'fontSize=' + rem+'px; ';
  document.style.opacity = 1
})(window, document)

// app/layout.tsx
import Script from 'next/script'
import { cookies } from 'next/headers'
import type { Metadata } from 'next'
import Layout from '@/components/Layout'
import './globals.css'
import Client from '@/components/Client/index'

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default async function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  const cookieStore = await cookies()
  const fontSize = cookieStore.get('fontSize')?.value
  return (
    <html lang="en" style={{ fontSize: fontSize, opacity: fontSize === undefined ? 0 : 1 }}>
      <head>
        <Script id="analytics-script" src="flexble.js" />
      </head>
      <body>
        <Client scripts={[{ src: 'flexble.js' }]} />
        <Layout>{children}</Layout>
      </body>
    </html>
  )
}

第二种写法

解决方案都是一样的,我这里只是拓展一下,写法大同小异,只是封装了一个DynamicComponentWithScript组件,方便以后使用


// 这里必须通过 'use client' 进行标示是客户端组件,如果没有标示,可以通过dynamic 动态引入,不过也需要将ssr标示为false
'use client'
import Script, { ScriptProps } from 'next/script'
import { type PropsWithChildren } from 'react'

interface Props extends PropsWithChildren {
  scripts: ScriptProps[]
}

function DynamicComponentWithScript({ scripts, children }: Props) {
  return (
    <>
      {scripts.map((script, index) => {
        return <Script {...script} key={index} />
      })}
      {children}
    </>
  )
}


export default DynamicComponentWithScript


写在最后

⚠️ 以上是一种取巧的解决方案,其实也不是一个优雅的或合理的解决方案,因为设置透明度为0 再到1 这个过程中有gap,会拖慢整个白屏时间,所以也不是个优雅的解决方案

但是目前没有想到其他更好的解决方案,如果大家有更好的解决方案,希望可以在评论区讨论