[Next.js 实战项目 | 青训营笔记08]

208 阅读3分钟

前言

这是我参与 [第五届青训营] 伴学笔记创作活动的第8天,上篇讲了node.js服务端运行环境,这回我来讲node.js的框架Next.js,那Next.js是什么呢?为什么会出现它呢?和市面上KoaExpress这些node.js框架有什么区别?让我娓娓道来...

Next.js

内容概述

  • 目标
  • CSR,SSR,SSG,ISR,RSC
  • 什么是 Next.js
  • Next.js 客户端开发
  • Next.js 服务端开发

目标

  • 具备C 端 Web 应用开发的基本能力
  • 了解 Next.js 是什么?
  • 在如何解决SPA常见的白屏问题

node.js 是开发 C 端的应用,而不是用来开发 B 端的应用;而我们现在使用的React/Vue都是SPA,SPA最容易出现首屏白屏等问题,C端同时对SEO和用户体验要求度比较高

SPA 应用的问题

SPA: 单页面应用.它所需的资源(HTMLCSSJS等),在一次请求中就加载完成,不需刷新地动态加载,首屏时间更长

  • 首屏白屏
  • SEO

微信截图_20230207144533.png

微信截图_20230207130645.png

同构框架

  • 首屏加载 SSR
  • 后续交互SPA
  • 优雅降级
  • 更多的渲染方式
    • CSR、SSR、SSG·
    • ISR、RSC

CSR,SSR,SSG,ISR,RSC

CSR

客户端渲染(Client-Side Rendering)。
常见 B 端 WEB 应用开发模式,前后端分离,服务器压力相对更轻,演染工作在客户端进行,服务器直接返回不加工的 HTML 用户在后续访问操作

假如我们要开发一个教学平台,我们需要以下几个流程;

  1. 运用Vue/React 去书写前端模板
  2. 跟后端进行接口对接
  3. 模板向后端进行请求,前后端分离的操作

以上这类操作是在客户端上进行操作的,后端不会关注前端UI 模板页面的HTML进行加工的; Snipaste_2023-02-04_16-16-11.png SPA: 单页面应用.它所需的资源(HTML CSSJS等),在一次请求中就加载完成,不需刷新地动态加载,首屏时间更长

SSR

SSR(Server-Side Rendering)。不是什么新鲜的概念,从原先的 JSP / PHP就已经体现了服务器端渲染。

<?php if ( count( $ _POST ) ): ?>
<?php include WTG INCPATH . '/wechat item template.php' ?>
<div style="...">

<div id-"wechat-post" class="wechat-post" style="...">
<div class="item" id="item-list">
<?php
$order = 1;
foreach ( $ POST['posts'] as $wechat item id ) {
  echo generate_item list( $wechat_item id, $order );
  $order++;
}
?>
</div>
<?php
$order = 1;
foreach ( $_POST['posts'] as $wechat_item_id )
echo generate item html( $wechat_item_id, $order );
$order++;
}
?>
<fieldset style="...">
<section style"...">
<p style="...">如果心中仍有疑问,请查看原文并留下评论噢。(<span style="font-size:0.8em"></span>)
</section>
</fieldset>

代码耦合度高,且模板语言
中混杂编程语言对于一些复杂的功能,维护起来很痛苦
这种模式下 Java,PHP 负责染的逻辑而前端只负责 UI和交互

同构SSR

BFF: Backend For Frontend,服务于前端应用的后端 微信截图_20230204171613.png 微信截图_20230204163718.png

前后端一体化,一套 React 代码在服务器上运行一遍,到达浏览器又运行一遍。前端后端都要参与渲染,而且首次渲染出的 HTML 要一样。

BFFnode服务,通常来说可以直接操作数据库,但是不会;而是直接对下游的几个请求数据,进行拼接汇总,然后交给前端页面的交互和展示;类似于中介。
为什么有这个服务?
因为后端服务很多,也有复用这种,在中间加入中介层的话,接口只需要在乎数据本身,而对于页面需要的的内容的一些处理,可以放在BFF当中,提升大型项目的可维护性,可复用性。

SSG

静态站点生成(Static Site Generation),在构建的时候直接把结果页面输出html到磁盘,每次访问直接把html返回给客户端,相当于一个静态资源 CDN: 建立并覆盖在Internet 之上,由分布在不同区域的边缘节点服务器群组成的分布式网络

在Next.js中,用户必须从想要静态渲染的页面中导出 getStaticProps函数。

export default function Home({ data }) {
  return (
    <main>
      // Use data
    </main>
  );
}
export async function getStaticProps() {
  // Fetch data from external API at build time
  const res = await fetch('https://.../data') 
  const data = await res.json()
  // Will be passed to the page component as props
 return { props: { data } }
}

用户还可以在getStaticProps里面查询数据库。

export async function getStaticProps() {
  // Call function to fetch data from database
  const data = await getDataFromDB()
  return { props: { data } }
}

在Next.js 13中,静态渲染是默认操作,内容被获取和缓存,除非用户关闭了缓存选项。

async function getData() {
  const res = await fetch('https://.../data');
  return res.json();
}
export default async function Home() {
  const data = await getData();
  return (
    <main>
      // Use data
    </main>
  );
}

可从文档进一步了解​​Next.js 13中的静态渲染​​。

SSG 使用getStaticProps ,把我们服务端的数据读取出来,直接生成静态资源html,像一些门户类网站,电商京东,都会为了网页极致性能,都会把网页生成静态资源html,每个商品都是单独的静态文件;访问的时候就完全不需要后端的介入,访问然后nginx就直接打出来,速度和效率是最高的;SSG在数据庞大的时候,就会有性能问题,生成就比较慢,成千上万的文章就得静态标记打印,可能需要好几个小时,甚至一天都完成不了,所以就有ISR的方式

SSG 和 SSR 比较:

相比 SSR,因为不需要每次请求都由服务器端处理,
所以可以大幅减轻服务器端的压力

SSG缺点:

缺陷在于只能用于偏静态的页面,无法生成与用户相关的内容,也就是所有的用户访问的页面都是相同的。

SSR,SSG 的优势 -利于 SEO

浏览器的推广程度,取决于搜索引擎对站点检索的排名,搜索引擎可以理解是一种 爬虫,它会爬取指定页面的 HTML,并根 据用户输入的关键词对页面内容进行排序检索,最后形成我们看到的结果。

C端的应用最大是曝光,B端是不会展示里面的元素,把它解析中,再塞进来

SSRS,SG的优势 -更短的首屏时间

SSR/SSG 只需要请求一个 HTML 文件就能展现出页面,虽然在服务器上会调取接口但服务器之间的通信要远比客户端快,甚至是同一台服务器上的本地接口调取因为不再需要请求大量 js 文件,这就使得SSR/SSG 可以拥有更短的首屏时间

RSC

RSC remote service component, 远程服务组件,如果你有CDN,再基于分布式节点优化的RSC

ISR

ISR(Incremental server rendering),增量静态渲染,针对SSG
有时用户想使用SSG,但又想定期更新内容,这时候增量静态生成(ISR)大有帮助。

ISR让用户可以在构建静态页面后在指定的时间间隔后创建或更新静态页面。这样一来,用户不需要重建整个站点,只需重建需要它的页面。

ISR保留了SSG的优点,又增加了为用户提供最新内容的好处。ISG非常适合站点上那些使用不断变化的数据的页面。比如说,用户可以使用ISR渲染博文,以便在编辑文章或添加新文章后博客保持更新。

若要使用ISR,将revalidate属性添加到页面上的getStaticProps函数中。

export async function getStaticProps() {
    const res = await fetch('http://...//data')
    const data = await res.json()
    return {
     props: {
         data,
     },
     revalidate: 60      
    }
} 

在这里,当请求在 60秒 后到来时,Next.js将尝试重新构建页面。下一个请求将产生带有更新页面的响应。

在Next.js 13中,使用fetch中的revalidate,就像这样:

fetch(https//.../data', {next: {revalidate: 60}})

用户可以将时间间隔设置为最适合其数据的任何时间间隔。

小总结: CSR适用于需要新数据的页面。SSR适用于使用动态数据的页面,但它对SEO较为友好。
SSG适合数据基本上静态的页面,而ISG最适合含有用户想要间隔更新的数据的页面。SSGISR从性能和SEO方面来说都很出色,因为数据预获取,用户还可以缓存数据。

什么是 Next;js

SSR 的实现

React 提供的服务器端渲染API

  • rendertoString
  • hydrateRoot
  • SSR 框架
    • 模板页面渲染的同构
    • 路由同构
    • Header 标签修改
    • 脱水注水
    • 性能优化 or other

痛点/期望:

  1. 基于 React 提供的相关服务器端渲染 API 实现,整个过程实现比较繁琐重复,从零实现对新上手同学很不友好
  2. 迫切需要一个封装好的集合
  3. 来快速上手服务器端渲染

React 也能直接做SSR ,其实是有提供SSR API 的,但是其实这样一个SSR 的过程当中,很多步骤是重复的,而且是对新手不是很友好的;所以就出现了Next.js框架
Next.js 定位就是帮我们更好更快的去开发一个SSR 的项目

SSR 实现 - Demo 仓库

仓库地址:github.com/czm12904337…

需要做大量的重复工作,脱水注水同构等,而不是做页面逻辑,所以就出现了Next.js

Next.js

Nextjs 是一个构建于 Node.js之上的开源 Web 开发框架支持基于 React 的 Web 应用程序功能,例如服务端渲染和生成静态网站。

  1. 上手快,能力集全,而且覆盖了足够多的性能优化和生态
  2. 对于新同学学握前后端一体化的开发模式很友好

初始化

npx create-next-app@latest --typescript

Nextjs 客户端开发

CMS 仓库地址;: github.com/czm12904337…
Demo 仓库地址: github.com/czm12904337…

Nextjs 初始化

npx create-next-app@latest --typescript

微信截图_20230204191017.png next0env.d.ts: 确保 TypeScript 编译器选择Next.js 类型,可以放到.gitignore 中,不需要变更
next.config.js: nextjs 的配置,我们可以补充webpack的一些配置进行

// next.config.js
const path = require('path')

module.exports = {
    reactStrictMode:true,
    swcMinify: true,
    webpack: (config) => {
        config.resolve.alias = {
        ...config.resolve.alias,
        
        // 比如说补充一些别名
        "@": path.resolve(__dirname),
        };
        return config;
    }
}

通常不会通过声明周期的钩子去页面请求,像Vue 的话就是 模板页面watchEffect,React 就是模板页面useEffect里面去写请求;这样去写请求的方式和SSR 去将处理好的数据直接传过来最直观的区别就是你不能通过Network 去看到请求

数据注入

  1. getInitialProps
  2. getServerSideProps
  3. getStaticProps
  4. getStaticPath
import type { AppProps, AppContext } from 'next/app';
import React from 'react';
import App from 'next/app';
import { Layout, ILayoutProps } from '@/components/layout';
import Head from 'next/head';
import axios from 'axios';
import { getIsMobile, getIsSupportWebp, LOCALDOMAIN } from '@/utils';
import { ThemeContextProvider } from '@/stores/theme';
import { UserAgentProvider } from '@/stores/userAgent';
import { LanguageContextProvider } from '@/stores/language';
import './global.scss';

export interface IComponentProps {
  isMobile?: boolean;
  isSupportWebp?: boolean;
}

const MyApp = (data: AppProps & ILayoutProps & IComponentProps): JSX.Element => {
  const { Component, pageProps, navbarData, footerData, isMobile, isSupportWebp } = data;

  return (
    <div>
      <Head>
        <title>{`A Demo for 《SSR 实战:官网开发指南》(${isMobile ? '移动端' : 'pc端'})`}</title>
        <meta name="description" content={`A Demo forSSR 实战官网开发指南》(${isMobile ? '移动端' : 'pc端'})`} />
        <meta name="viewport" content="user-scalable=no" />
        <meta name="viewport" content="initial-scale=1,maximum-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <LanguageContextProvider>
        <ThemeContextProvider>
          <UserAgentProvider>
            <Layout navbarData={navbarData} footerData={footerData}>
              <Component {...pageProps} isMobile={isMobile} isSupportWebp={isSupportWebp} />
            </Layout>
          </UserAgentProvider>
        </ThemeContextProvider>
      </LanguageContextProvider>
    </div>
  );
};

MyApp.getInitialProps = async (context: AppContext): Promise<AppProps & ILayoutProps & IComponentProps> => {
  const pageProps = await App.getInitialProps(context);
  const { data = {} } = await axios.get(`${LOCALDOMAIN}/api/layout`);

  return {
    ...pageProps,
    ...data,
    isMobile: getIsMobile(context),
    isSupportWebp: getIsSupportWebp(context),
  };
};

export default MyApp;

getInitialProps

Article.getInitialProps = async (context) => {
  const { articledId } = await axios.get(`${LOCALDOMAIN}/api/articledInfo`,{
   params: {
      articledId,
   },
  })
  return data
};

  • 在服务器端执行,只能在页面层面进行绑定,采用同构,”首次渲染服务器端渲染路由跳转使用客户端路由
  • 意味着如果使用 router 跳转当前页,会在客户端执行这部分逻辑

My.getInitialPropsserver side 上的方法,server side注水的数据;需要链接server side才能进行出现效果,并且你在此类方法内debugger 是没效果的,也是同样的缘由;
注意
getInitialProps此方法里面走route 是走的是client route客户端路由

getInitialProps是一个老的API,因为有时走的是client route,有时又走server side route,所以写的时候就得写两套,server side一套 client一套,next.js就干脆分成了两个API getServerSideProps,getStaticProps,前者负责SSR,后者负责SSG

getServerSideProps

Article.getInitialProps = async (context) => {
  const { articledId } = await axios.get(`${LOCALDOMAIN}/api/articledInfo`,{
   params: {
      articledId,
   },
  })
  return {props:data}; // 需要拿props包裹
};

注意
getServerSideProps此方法里面走route 是走的是server route服务端路由

getStaticProps

// ssg:
export const getstaticPaths: GetStaticPaths = async () => {
return {
  paths: [{ params: { articleId:"1"} }],
  fallback: false,
}
export const getstaticProps: GetStaticprops = async (context) => {
  const { articleId } = context.params as any;
  const { data } = await axios.get(`${LOCALDOMAIN}/api/articleInfoparams`,{
   params: {
    articleId,
   } 
  });
  return {
    props:data,
  };
};

SSG,在服务器端构建时执行

如果涉及动态路由 (带参数),需要使用getStaticPaths配置所有可能的参数情况

用于固定少量的数据可以使用,但是多量不固定就不建议使用getStaticProps 这个API

CSS Modules

Next.js 支持使用文件命名约定的 CSS 模块。 [name].module.css

微信截图_20230205163534.png

Layout

通过在入口文件导入 layout ,可以实现每个页面公共的页眉页尾

Snipaste_2023-02-05_16-47-53.png 微信截图_20230205165631.png

文件式路由

// ./pages/demo/index.tsx => /demo
// ./pages/demo/[id].tsx =>/demo/:id
// ./pages/demo/[...id].tgx => /demo/a/b/c => id = ["a","b","c"]

预定义路由优先级更高,预定义路由能直接匹配的路由就不会分发给下面的动态路由

Next.js 有一个基于页面概念的基于文件系统的路由器。当一个文件被添加到 pages 目录中时,它会自动作为一个路径可用

BFF层的文件式路由

Snipaste_2023-02-05_18-00-36.png 微信截图_20230205180349.png

BFF,作为服务器构建包,不影响客户端构建 bundle 体积相同的 router 生成方式,不过是作为 API 层访问,而不是 page

BFF层不生产数据,只作为数据的搬运工

路由跳转

next/link 跳转

import Link from 'next/link';
<Link href={item.link} key={index}>
    <div class={styles.card}>
        <h2>{item.label} &rarr;</h2>
        <p>{item.info}</p>
    </div>
</Link>

useRouter跳转

import { useRouter } from 'next/router'

function ActiveLink({ children,href }) {
    const router = useRouter()
    const style = {
        marginRight: 10,
        color: router.asPath === href ? 'red' : 'black',
    }
    
    const handleClick = (e) => {
        e.preventDefault()
        router.push(href)
    }
    
    return (
    <a href={href} onClick={handleClick}  style={style}>
        {children}
    </a>)
}

export default ActiveLink

除了这些外,还可以使用原生方法跳转,不过原生的方法不会进行 Diff 比对渲染性能上 Nextjs 提供的路由跳转会更好

window.open,window.loacation 这种原生的API 没有走路由跟普通打开一个页面没有什么区别,不会做Diff比对,直接走路由,性能比对是比较好的。

header 的修改

<Head>
<title>{`A Demo for 《SSR 实战: 官网开发指南》(${isMobile ? "移动端" : "pc端"
})`}</title>
<meta
  name="description" 
  content={`A Demo forSSR 实战: 官网开发指南》(${
    isMobile ?"移动端":"pc端"
  })`}
/>
<meta name="viewport" content="user-scalable=no" />
<meta name="viewport" content="initial-scale=1,maximum-scale=1"/>
<link rel="icon" href="/favicon.ico" />
</Head>

可用于修改TDK(title,description, keywords)

titledescription, keywordsC 端开发最重要的三个点,方便SEO去进行搜索,从而提高排名

多媒体适配

多媒体适配 --- CSS适配

// 极小分辨率移动端设备
@mixin media-mini-mobile {
  @media screen and (max-width: 25875rem) {
  @content;
}
}
  // 介于极小分辨率和正常分辨率之间的移动端设备
  @mixin media-between-mini-and-normal-mobile {
    @media screen and (min-width: 25.876rem) and (max-width: 47.9375rem) {
      @content;
    }
  }
  // 移动端设备
@mixin media-mobile {
  @media screen and (max-width: 47.9375rem) {
    @content;
  }
}
  // ipad
@mixin media-ipad {
  @media screen and (min-width: 47.9375rem) and (max-width: 75rem){
    @content;
  }
}
@include media-ipad {...
}
@include media-mobile {...
}

这类属于媒体查询的方式去兼容移动端各类手机型号,还可以通过px-to-view 的一些库进行适配

多媒体适配 ---- JS适配

import React, { useState, useEffect, createContext } from 'react';
import { Environment } from '@/constants/enum';

interface IUserAgentContextProps {
  userAgent: Environment;
}

interface IProps {
  children: JSX.Element;
}

export const UserAgentContext = createContext<IUserAgentContextProps>({} as IUserAgentContextProps);

export const UserAgentProvider = ({ children }: IProps): JSX.Element => {
  const [userAgent, setUserAgent] = useState<Environment>(Environment.none); // 服务器渲染初始化渲染未必是预期效果,none缓冲切换视觉)

  // 监听本地缓存来同步不同页面间的主题(当前页面无法监听到,直接在顶部栏进行了类的切换)
  useEffect(() => {
    const checkUserAgent = (): void => {
      const width = document.body.offsetWidth;
      // 用宽度去判断,是为了适配不改机型,仅拉扯屏幕宽度的情况
      if (width < 768) {
        // 手机端
        setUserAgent(Environment.mobile);
      } else if (width >= 768 && width < 1200) {
        // ipad端
        setUserAgent(Environment.ipad);
      } else if (width >= 1200) {
        // pc端
        setUserAgent(Environment.pc);
      } else {
        setUserAgent(Environment.none); // 增加none类型来缓冲默认类型样式切换时的视觉突变
      }
    };
    checkUserAgent();
    window.addEventListener('resize', checkUserAgent); // 监听屏幕宽度变化,及时适配当前页面样式
    return (): void => {
      window.removeEventListener('resize', checkUserAgent);
    };
  }, [typeof document !== 'undefined' && document.body.offsetWidth]);

  return <UserAgentContext.Provider value={{ userAgent }}>{children}</UserAgentContext.Provider>;
};

通过监听resize 来多层判断去给多少值,从而达到一个函数适配多套方案

大图优化 - webp

Snipaste_2023-02-05_20-18-18.png

// ./utils/index.ts
export const getIsSupportWebp = (context: AppContext) => {
    const { header = {} } = context.ctx.req || {}
    return headers.accpect?.includes('image/webp'}
}

通过在线pngwebp会压缩至少3倍 在极速的webp 300kb 渲染时间比png 等格式的渲染时间还要长一点;webp需要整体体积非常小,但是需要更长的解析时间。
慢网的情况下,你拉取时间,和解析时间就比较微不足道了 webp 兼容性:需要浏览器给定Accept: webp 才行。或者自己添加这个

Nextjs 服务端开发

BFF 层开发

import type { NextApiRequest, NextApiResponse } from 'next';
import axios from 'axios';
import { CMSDOMAIN } from '@/utils';
import { IArticleProps } from '../article/[articleId]';

const getArticleInfoData = (req: NextApiRequest, res: NextApiResponse<IArticleProps>): void => {
  const { articleId } = req.query;
  axios.get(`${CMSDOMAIN}/api/article-infos/${articleId}`).then(result => {
    const data = result.data || {};
    res.status(200).json(data);
  });
};

export default getArticleInfoData;
export default (req,res) => {
    if (req.method === 'GET') {
        // do something for the get scene
    } else if (req.method === 'POST') {
        // do something for the post scene
    }
}

通过axios请求方式去拿到文章的数据,然后return,不过这个逻辑是在前端写,后端执行

调试方式

Snipaste_2023-02-05_20-34-50.png 微信截图_20230205203835.png

微信截图_20230205204219.png

三种调试模式都是可以进行调试的,调试的方式和浏览器的devtool 一样;但是呢有一点需要注意的是,我们写的逻辑和请求数据是在BFF层,所以用debugger操作服务端是断点不了的,用console.log也是不能实时的

Strapi 工具库

Strapi - headless CMS 仓库: github.com/strapi/stra… 初始化: npx create-strapi-app my-project --quickstart

qsg-handson-restaurant_2.28faf048.gif

一个接口的生成有以下几个过程!

  1. content-type builder 编辑结构体
  2. content manager 配置数据源,并且发布
  3. settings roles 里选择对应角色并勾选要发布的接口类型
  4. 如果涉及嵌套,在接口后加上 populate=deep 参数 (npm install strapi-plugin-populate-deep--save),没安装加参数 populate=* ,但只能嵌套一层

推荐Strapi 工具库的愿意就是开箱即用,不需要自己去重新构造CMS内容管理系统平台,那将非常的耗时和影响效率,学习成本比较的低,通过拖拽输入相应的数据就可实现数据替换更新