服务端渲染 nextjs@14 项目接入经验总结,本文重点介绍基本知识点/常用的知识点/关键知识点

背景

为提高首屏渲染速度减少白屏时间提高用户体验及丰富技术面,开始调研和接入nextjs框架

优势

nextjs是一套成熟的同构框架(一套代码能运行在服务端也能运行在浏览器)对比传统的客户端渲染的核心优势是首屏是带数据的和减少跨域带来的option请求。其它后续操作是一样的。理论上能比客户端渲染看到数据能快个100-200ms具体看实际统计,

服务端渲染大概流程图(图片来源于网络)
image.png

客户端渲染大概流程图
image.png

对比流程图服务端渲染更加简洁。

劣势

有优势就有劣势,经过使用经验发现明显的劣势有

  1. 如果服务端接口时间过长会导致浏览器首屏白屏时间长,而客户端可以渲染骨架/loading/其它填充屏幕了。如果服务器到接口的服务器时间在150ms具有优势
  2. 如果接口有几层依赖也会导致在服务器停留时间过长,类似请求A后拿到依赖再去请求B也会导致情况1的出现
  3. 服务器拿不到浏览器/屏幕尺寸有这个依赖的服务器无法判断
  4. 消耗服务器资源
  5. 等等其它的...

使用

环境 Node.js >= 18.17 nextjs14

安装

npx create-next-app@14
选择 src/ 目录 和 使用 App Router

大致目录结构

...
package.json
public
node_modules
src
|- app
  |- page.tsx
  |- layout.tsx
  |- blog
    |- page.tsx
    |- layout.tsx
  |- docs
    |- page.tsx
    |- layout.tsx
| -services
| -utils
...

大致路由为
注意这是约定路由 需要用page.tsx layout.tsx文件命名
image.png
image.png

内置API

head标签
import Head from 'next/head'
图片标签
import Image from 'next/image'
跳转标签
import Link from 'next/link'
script
import Script from 'next/script'
路由相关
import { useRouter, useSearchParams, useParams, redirect } from 'next/navigation'
请求头
import { headers } from 'next/headers'

服务器组件和客户端组件

服务器组件需要运行在服务器
主要特点有请求数据,服务端环境等

客户端组件运行在浏览器 标识 文件第一行增加 'use client'
主要特点有事件,浏览器环境,react hooks

比较
操作服务器组件客户端组件
请求数据
访问后端资源(直接)
在服务器上保留敏感信息(访问令牌、API密钥等)
保持对服务器的大量依赖性/减少客户端JavaScript
添加交互性和事件侦听器(onClickonChange等)
使用状态和生命周期(useStateuseReduceruseEffect等)
浏览器API
自定义hooks
使用React Class组件

开始填充业务代码

  1. 修改html页面
    文件位置在/src/app/layout.tsx,可以进行标题修改等一系操作

    import Head from "next/head";
    
    export default async function RootLayout(props: any) {
      return (
     <html lang="en">
       <Head>
         <title>页面标题</title>
       </Head>
       <body>{props.children}</body>
     </html>
      );
    }
    
  2. 获取数据

    async function getData() {
      const res = await fetch('https://xxxxx.com/', { cache: 'no-store' })
      if (!res.ok) {
        throw new Error('Failed to fetch data')
      }
     
      return res.json()
    }
     
    export default async function Page() {
      const data = await getData()
     
      return <main>{JSON.stringify(data, null, 2)}</main>
    }
  3. 服务器数据和后面请求的数据衔接
// home.tsx
export default async function Home(p: any) {
  const data = await getData();
  return (
    <main>
      <Link href="/">to home</Link>
      <List list={data} />
    </main>
  );
}

// list.jsx
'use client';

import { useState } from 'react';

export default function List(props: any) {
  const [list, setList] = useState(props.list);
  // 这只是随意写的一个例子
  const getPageData = () => {
    fetch('http://xxxx', { }).then((res) => res.json())
    .then((res) => setList([...list, ...res]));
  };

  return (
    <div>
      {list?.map((val: any) => (
        <div key={val.name}>
          <p>
            {val.name}-{val.price}
          </p>
        </div>
      ))}
      <div onClick={getPageData}>加载更多</div>
    </div>
  );
}
  1. 把浏览器的信息转发到服务端
    这个例子是cookie有需求可以用放其它的

    import { headers } from 'next/headers'
    const getData = async () => {
      const headersList = headers();
      const cookie = headersList.get('Cookie');
      const res = await fetch('https://xxx.com', {
     cache: 'no-store',
     headers: { cookie }
      });
      return res.json()
    };
  2. 处理全局通讯和数据
在/src/app 目录下增加 context.tsx
/src/app/context.tsx
'use client';

import { createContext, useMemo } from 'react';
import { useImmer } from 'use-immer';

export const PropsContext = createContext({});

export function Context({ children, ...other }: any) {
  const [GlobalState, setGlobalState] = useImmer<any>({
    ...other
  });

  const providerValue = useMemo(
    () => ({ GlobalState, setGlobalState }),
    [GlobalState]
  );

  return (
    <PropsContext.Provider value={providerValue}>
      {children}
    </PropsContext.Provider>
  );
}
/src/app/layout.tsx
import React from 'react';
import { headers } from 'next/headers'
import { Context } from './context';

const getData = async () => {
  const headersList = headers();
  const cookie = headersList.get('Cookie');
  const res = await fetch('https://xxx.com', {headers: {
      cookie
    }});
  return res.json()
};

export default async function RootLayout(props: any) {
  const useInfo = await getData();
  return (
    <html lang="en">
      <body>
        <div>header</div>
        <Context useInfo={useInfo}>{props.children}</Context>
        <div>footer</div>
      </body>
    </html>
  );
}
使用
/src/app/blog/page.tsx
'use client';

import { PropsContext } from '@/app/context';
import { useContext } from 'react';

export default function A2() {
  const { GlobalState, setGlobalState } = useContext<any>(PropsContext);

  return (
    <main>
      {JSON.stringify(GlobalState, null, 2)}

      <div
        onClick={() => {
          setGlobalState((s: any) => {
            s.useInfo.name = '修改之后的名称';
          });
        }}
      >
        修改名称
      </div>
    </main>
  );
}
  1. 跳转
    如果没有用户信息需要跳转到登录页

    import { redirect } from 'next/navigation'
     
    async function fetchTeam(id) {
      const res = await fetch('https://...')
      // 具体逻辑根据实际的来
      if (!res.ok) return undefined
      return res.json()
    }
     
    export default async function Profile({ params }) {
      const team = await fetchTeam(params.id)
      if (!team) {
     redirect('/login')
      }
     
      // ...
    }
  2. 优化

如果页面长接口多可以在服务端请求可视区数据下面的数据可以在客户端请求

部署

如果不在根域名下需要在 next.config.js添加
路由名称根据实际来

{
  basePath: '/router'
}

然后在流水线nginx配置路由 /router* 转发到这个应用

如果 basePath 配置的 /router/' 对应nginx配置 /router/*

编写 Dockerfile

由于 FROM nodejs@xx 过不了镜像扫描 镜像里面又没有Node.js >= 18.17的只能使用提供最基础的镜像了

Dockerfile

FROM hub.xxx.com/basics/alpine:3.18.2

RUN apk add nodejs=18.18.2-r0 npm=9.6.6-r0

WORKDIR /app

ADD . .

RUN npm i

RUN npm run build

EXPOSE 3000

CMD ["sh", "-c", "NODE_ENV=$NODE_ENV npm run start"]

参考文档

https://nextjs.org/docs
https://vercel.com/guides/react-context-state-management-nextjs


路边县
31 声望1 粉丝