NextJs 中,通过script,引入lib-flexible,在client计算rem
背景
在 Next.js 中,我们通常会在页面中引入一些 client script,比如 Google Analytics,lib-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或者body的opacity设置为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,会拖慢整个白屏时间,所以也不是个优雅的解决方案
但是目前没有想到其他更好的解决方案,如果大家有更好的解决方案,希望可以在评论区讨论