• 最新Next 14快速上手基础部分


    最新Next 14快速上手基础部分

    最新的NEXT快速上手文档,2023.10.27 英文官网同步,版本Next14.0.0
    本项目案例:GitHub地址,可以根据git回滚代码到对应知识,若有错误,欢迎指正!

    一、介绍

    1.什么是Next.js

    ​ Next.js是一个用于构建全栈Web应用程序的React框架。你可以使用React组件来构建用户界面,使用Next.js来实现额外的功能和优化。
    ​ 在引擎盖下,Next.js还抽象并自动配置React所需的工具,如捆绑,编译等。这使您可以专注于构建应用程序,而不是花费时间进行配置。
    ​ 无论您是个人开发人员还是大型团队的一员,Next.js都可以帮助您构建交互式,动态和快速的React应用程序

    2. 主要特点

    Next.js的特点主要包括:

    特点描述
    路由基于文件系统的路由器,构建在服务器组件之上,支持布局、嵌套路由、加载状态、错误处理等。
    渲染使用客户端和服务器组件进行客户端和服务器端渲染。通过Next.js在服务器上使用静态和动态渲染进一步优化。Edge和Node.js运行时上的流媒体。
    数据获取简化了服务器组件中的数据获取,并扩展了fetch API,用于请求存储,数据缓存和重新验证。
    样式支持您首选的样式化方法,包括CSS Modules、Tailwind CSS和CSS-in-JS
    优化项图像、字体和脚本优化,以改善应用程序的核心Web关键点和用户体验。
    TypeScript支持改进了对TypeScript的支持,具有更好的类型检查和更有效的编译,以及自定义TypeScript插件和类型检查器。

    二、安装Next

    Node版本要求在18.17以上,建议使用nvm切换

    1. 安装步骤

    • 打开终端(官网建议使用create-next-app创建Next应用)

      npx create-next-app@latest
      
      • 1
    • 接下来将看到如下提示:根据自己的习惯进行选择,这里我全选Yes,最后回车

      What is your project named? my-next-app
      Would you like to use TypeScript? No / Yes
      Would you like to use ESLint? No / Yes
      Would you like to use Tailwind CSS? No / Yes
      Would you like to use src/ directory? No / Yes
      Would you like to use App Router? (recommended) No / Yes
      Would you like to customize the default import alias (@/)? No / Yes
      What import alias would you like configured? @/

      注意:选择使用项目根目录中的src目录将应用程序代码与配置文件分开。这和我选择的方式是一致的

    2. 项目结构

    下面将介绍我们主要关注的几个目录

    • 顶级目录文件夹

      public服务的静态资产
      src应用程序源文件夹,在这个文件夹下编写应用代码
    • src文件夹

      • src文件夹中的app目录就是我们选择的App Router,在app文件夹创建文件夹及相关文件将对应相应的路由,后面将详细说明
      • src下,按照习惯,
        • 创建components文件夹,用于放置自定义的组件
        • 创建styles文件夹,用于放置样式文件,当前使用的是CSS in JS方式
        • 创建lib文件夹,用于放置自定义的方法工具等
          ······

    三、构建应用程序

    推荐使用路由器==(App Router )方式==

    Next.js使用基于文件系统的路由器App Router概览
    路由目录对应的文件规则:

    文件名(后缀.js .jsx .tsx)描述
    layout路由及其子路由的共享UI
    page路由的唯一UI并使路由可公开访问
    loading路由加载及其子路由加载的UI
    not-found找不到路由及其子路由的UI
    errorError UI for a segment and its children段及其子段的错误UI
    global-error全局错误UI,在app(根)目录下
    route服务器端API端点
    template专门的重新渲染布局UI
    default并行路由的回退UI

    I. 定义路由

    Next.js使用基于文件系统的路由器,其中文件夹用于定义路由。

    每个文件夹代表一个映射到URL段的路由段。要创建嵌套路由,可以将文件夹嵌套在彼此内部。
    在这里插入图片描述
    特殊的page.js文件用于使路由可公开访问。(主要后缀js本文用tsx)

    例如,要创建第一个页面,在src/app目录下添加page.tsx文件,并导出React组件:

    export default function Page() {
      return <h1>Hello, Next.js!</h1>
    }
    
    • 1
    • 2
    • 3

    执行命令npm run dev,访问:http://localhost:3000/,页面如下:
    在这里插入图片描述

    II. 页面和布局

    Next.js 13中的App Router引入了新的文件约定,可以轻松创建页面共享布局模板。本篇将指导您如何在Next.js应用程序中使用这些特殊文件。

    • 页面

      页面是路由所特有的UI。可以通过从page.tsx文件导出组件来定义页面。使用嵌套文件夹定义路由page.js文件以使路由可公开访问

      上一节,我们已经在src/app下添加了page.tsx文件作为首页,我们更新这个文件:

      // `app/page.tsx` is the UI for the `/` URL
      export default function Page() {
        return <h1>Hello, Home page!</h1>
      }
      
      • 1
      • 2
      • 3
      • 4

      接下来我们将在src/app下添加dashboard目录,并且在这个新增目录下添加page.tsx

      // `app/dashboard/page.tsx` is the UI for the `/dashboard` URL
      export default function Page() {
        return <h1>Hello, Dashboard Page!</h1>
      }
      
      • 1
      • 2
      • 3
      • 4

      当我们访问对应路由//dashboard的时候,就会分别展示对应的page/tsx中的UI,对应目录和路由如下:在这里插入图片描述

      总结:要使路由可公开访问,需要使用page.js文件。

    • 布局

      布局是在多个页面之间共享的UI。在导航时,布局将保留状态,保持交互性,并且不会重新呈现。布局也可以==嵌套==

      我们可以通过从layout.js文件默认(default)导出React组件来定义布局。该组件应该接受一个children prop,该prop将在呈现过程中填充子布局(如果存在)或子页面。
      最顶层的布局称为根布局,即app目录下的layout.tsx。该文件是必须存在的,且在应用程序中的所有页面之间共享。根布局必须包含htmlbody标签。
      app/layout.tsx根布局如下(也可以自定义):

      export default function RootLayout({
        children,
      }: {
        children: React.ReactNode
      }) {
        return (
          <html lang="en">
            <body>{children}</body>
          </html>
        )
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

      为了演示这个效果,我们单独封装了个简单的共享组件,当然后面也会详细说明在Next中的路由跳转。

      首先,在src/components下新建文件夹links并在目录下创建文件index.tsx

      'use client'
      
      import { usePathname } from 'next/navigation'
      import Link from 'next/link'
      type Props = {
        linkList: string[]
      }
      export function Links({ linkList }: Props) {
        const pathname = usePathname()
      
        return (
          <nav>
            <ul style={{ display: 'flex', listStyle: 'none' }}>
              {linkList.map((link:string) => {
                return (
                  <li key={link} style={{ margin: '0 20px' }}>
                    <Link className={`${pathname === link ? 'active' : ''}`} href={link === 'home' ? '/' : '/' + link}>
                      {link?.toUpperCase()}
                    </Link>
                  </li>
                )
              })}
            </ul>
          </nav>
        )
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26

      接下来,我们将按如下目录创建文件:
      在这里插入图片描述

      src/app/dashboard下创建layout.tsx文件:

      import { Links } from '../../components/links'
      
      export default function DashboardLayout({ children }: { children: React.ReactNode }) {
        return (
          <section>
            {/* Include shared UI here e.g. a header or sidebar */}
            <Links linkList={['dashboard', 'dashboard/settings']} />
            {children}
          </section>
        )
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11

      src/app/dashboard/settings下创建page.tsx文件:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

      export default function Page() {
        return <h1>settings</h1>
      }
      
      • 1
      • 2
      • 3

      效果如下:
      在这里插入图片描述

      嵌套布局,在文件夹(例如app/dashboard/layout.js)中定义的布局适用于特定的路由(例如acme.com/dashboard),并在这些路由处于活动状态时进行渲染。默认情况下,文件层次结构中的布局是嵌套的,这意味着它们通过其children属性包装子布局。
      在这里插入图片描述

    • 模板Templates(目前先简单了解)

      模板类似于布局,因为它们包装每个子布局或页面。与跨路径持久化并保持状态的布局不同,模板在导航上为其每个子项创建一个新实例。这意味着,当用户在共享模板的路由之间导航时,将挂载组件的新实例,重新创建DOM元素,不保留状态,并重新同步效果。

      在某些情况下,您可能需要这些特定的行为,而模板将是比布局更合适的选择。例如:

      • 依赖于useEffect(例如记录页面浏览量)和useState(例如每页反馈表单)的功能。
      • 更改默认框架行为。例如,布局内的Suspense Bouncement仅在首次加载布局时显示回退,而在切换页面时不显示。对于模板,回退显示在每个导航中。

      模板可以通过从template.js文件导出默认的React组件来定义。该组件应接受childrenprop。如src/app/template.tsx

      export default function Template({ children }: { children: React.ReactNode }) {
        return <div>{children}</div>
      }
      
      • 1
      • 2
      • 3
    • 修改

      app目录中,您可以使用内置的SEO支持修改 HTML元素,例如titlemeta

      元数据即html文件中head标签下的内容,可以在layout.tsxpage.tsx中导出metadata对象或generateMetadata函数来定义,如src/app/page.tsx

      import { Metadata } from 'next'
       
      export const metadata: Metadata = {
        title: 'Next.js',
      }
       
      export default function Page() {
        return '...'
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

      然后访问路由/时,标签页的名就会变为Next.js

    III. 链接和导航

    在Next.js中有两种方法可以在路由之间导航:

    • 使用Link组件
    • 使用useRouterHook

    IV. 路由分组

    app 目录中,嵌套文件夹通常映射到 URL 路径。但是,您可以将文件夹标记为路由组,以防止该文件夹包含在路由的 URL 路径中。这允许您将路由段和项目文件组织到逻辑组中,而不会影响 URL 路径结构

    • 路由分组的作用

      • 将路线组织成组,例如按站点部分、意图或团队。

      • 在同一路线段级别启用嵌套布局

        • 在同一区段中创建多个嵌套布局,包括多个根布局
        • 将布局添加到公共段中的路由子集
    • 路由分组的使用:

      可以通过将文件夹名称括在括号中来创建路由组: (folderName)

    • 在不影响 URL 路径的情况下组织路由
      要在不影响 URL 的情况下组织路由,请创建一个组以将相关路由保持在一起。括号中的文件夹将从 URL 中省略(例如或 (marketing) (shop) )。

      在这里插入图片描述

      同时,即使路由内部 (marketing)(shop) 共享相同的 URL 层次结构,您也可以通过在文件夹内添加 layout.js 文件来为每个组创建不同的布局

      在这里插入图片描述

    • 创建多个根布局

      要创建多个根布局,请移除顶级文件,然后在每个路由组内添加一个 layout.js layout.js 文件。这对于将应用程序划分为具有完全不同的 UI 或体验的部分非常有用。 需要将 和 标记添加到每个根布局中

      在这里插入图片描述

    V. 动态路由

    如果您提前不知道确切的路由名称,并且想要根据动态数据创建路由,则可以使用在请求时填充或在构建时预呈现的动态路由。

    可以通过将文件夹的名称括在方括号中来创建动态路由[folderName] 。例如, [id][slug]

    动态路由作为 params prop传递给 、 layout routepagegenerateMetadata 函数。

    例如src/app/blog/[id]/page.tsx

    export default function Page({ params }: { params: { id: string } }) {
      return <div>My Post: {params.id}</div>
    }
    
    • 1
    • 2
    • 3
    路由示例网址params
    app/blog/[id]/page.js/blog/a{ id: 'a' }
    app/blog/[id]/page.js/blog/b{ id: 'b' }
    app/blog/[id]/page.js/blog/c{ id: 'c' }

    generateStaticParams 函数可与动态路由段结合使用,以在构建时静态生成路由,而不是在请求时按需生成路由。
    例如src/app/blog/[id]/page.tsx

    export async function generateStaticParams() {
      const posts = await fetch('https://.../posts').then((res) => res.json())
     
      return posts.map((post) => ({
        id: post.id,
      }))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    最简单的动态路由案例(博客)实现,步骤:

    • 首先引入我们需要使用的样式文件,在src/styles/utils.module.css中写入代码:

      .heading2Xl {
        font-size: 2.5rem;
        line-height: 1.2;
        font-weight: 800;
        letter-spacing: -0.05rem;
        margin: 1rem 0;
      }
      
      .headingXl {
        font-size: 2rem;
        line-height: 1.3;
        font-weight: 800;
        letter-spacing: -0.05rem;
        margin: 1rem 0;
      }
      
      .headingLg {
        font-size: 1.5rem;
        line-height: 1.4;
        margin: 1rem 0;
      }
      
      .headingMd {
        font-size: 1.2rem;
        line-height: 1.5;
      }
      
      .borderCircle {
        border-radius: 9999px;
      }
      
      .colorInherit {
        color: inherit;
      }
      
      .padding1px {
        padding-top: 1px;
      }
      
      .list {
        list-style: none;
        padding: 0;
        margin: 0;
      }
      
      .listItem {
        margin: 0 0 1.25rem;
      }
      
      .lightText {
        color: #999;
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
    • src/posts文件夹下,准备两个Markdown文件

      • pre-rendering.md

        ---
        title: 'Two Forms of Pre-rendering'
        date: '2020-01-01'
        ---
        
        Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
        
        - **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
        - **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
        
        Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
      • ssg-ssr.md

        ---
        title: 'When to Use Static Generation v.s. Server-side Rendering'
        date: '2020-01-02'
        ---
        
        We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
        
        You can use Static Generation for many types of pages, including:
        
        - Marketing pages
        - Blog posts
        - E-commerce product listings
        - Help and documentation
        
        You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
        
        On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.
        
        In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
    • 安装三个包

      npm i gray-matter remark remark-html
      
      • 1
    • src/lib/posts.ts中,编写要用到的代码

      import fs from 'fs'
      import path from 'path'
      import matter from 'gray-matter'
      import { remark } from 'remark'
      import html from 'remark-html'
      
      const postsDirectory = path.join(process.cwd(), 'src/posts')
      
      // 获取排序后的blog列表
      export function getSortedPostsData() {
        // Get file names under /posts
        const fileNames = fs.readdirSync(postsDirectory)
        const allPostsData = fileNames.map(fileName => {
          // Remove ".md" from file name to get id
          const id = fileName.replace(/\.md$/, '')
      
          // Read markdown file as string
          const fullPath = path.join(postsDirectory, fileName)
          const fileContents = fs.readFileSync(fullPath, 'utf8')
      
          // Use gray-matter to parse the post metadata section
          const matterResult = matter(fileContents)
      
          // Combine the data with the id
          return {
            id,
            ...matterResult.data
          }
        })
      
        return new Promise(function (resolve, reject) {
          //做一些异步操作
          setTimeout(function () {
            resolve(
              // Sort posts by date
              allPostsData.sort((a: any, b: any) => {
                if (a.date < b.date) {
                  return 1
                } else {
                  return -1
                }
              })
            )
          }, 1000)
        })
      }
      
      // 获取所有动态路由
      export function getAllPostIds() {
        const fileNames = fs.readdirSync(postsDirectory)
      
        // Returns an array that looks like this:
        // [
        //   {
        //     params: {
        //       id: 'ssg-ssr'
        //     }
        //   },
        //   {
        //     params: {
        //       id: 'pre-rendering'
        //     }
        //   }
        // ]
      
        return new Promise(function (resolve, reject) {
          //做一些异步操作
          setTimeout(function () {
            resolve(
              fileNames.map(fileName => {
                return {
                  params: {
                    id: fileName.replace(/\.md$/, '')
                  }
                }
              })
            )
          }, 1000)
        })
      }
      
      // 根据blog的ID获取博客内容
      export async function getPostData(id: string) {
        const fullPath = path.join(postsDirectory, `${id}.md`)
        const fileContents = fs.readFileSync(fullPath, 'utf8')
      
        // Use gray-matter to parse the post metadata section
        const matterResult = matter(fileContents)
        console.log('matterResult', matterResult)
      
        // Use remark to convert markdown into HTML string
        const processedContent = await remark().use(html).process(matterResult.content)
        const contentHtml = processedContent.toString()
      
        // Combine the data with the id and contentHtml
        return {
          id,
          contentHtml,
          ...matterResult.data
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
    • src/app/blogs目录下新建page.tsx文件

      import { getSortedPostsData } from '@/lib/posts'
      import utilStyles from '../../styles/utils.module.css'
      import Link from 'next/link'
      export default async function Page() {
        // 获取按日期排序好的博客大纲
        const allPostsData: any = await getSortedPostsData()
        // console.log('allPostsData', allPostsData)
        return (
          <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
            <h2 className={utilStyles.headingLg}>Blogs</h2>
            <ul className={utilStyles.list}>
              {/* 渲染博客列表 */}
              {allPostsData.map(({ id, date, title }: { id: string; date: string; title: string }) => (
                <li className={utilStyles.listItem} key={id}>
                  <Link href={`/blogs/${id}`}>{title}</Link>
                  <br />
                  <small className={utilStyles.lightText}>{date}</small>
                </li>
              ))}
            </ul>
          </section>
        )
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23

      接下来,访问:http://localhost:3000/blogs

      你将看到如下页面

      在这里插入图片描述

    • src/app/blogs/[id]目录下新建page.tsx文件

      import Head from 'next/head'
      import { getAllPostIds, getPostData } from '../../../lib/posts'
      import utilStyles from '../../../styles/utils.module.css'
      import Link from 'next/link'
      
      // params中的属性对应文件夹[id]
      type pathProps = [{ params: { id: string } }]
      // generateStaticParams函数可以与动态路由段结合使用,以便在构建时静态生成路由,而不是在请求时按需生成路由。
      // 若是无generateStaticParams函数不影响动态路由使用
      // 静态生成的params参数数组,用于构建动态路由
      export async function generateStaticParams() {
        const paths = (await getAllPostIds()) as pathProps
        console.log('paths', paths)
        return paths
      }
      
      type pageParams = {
        params: {
          // 此处id对应动态路由文件夹 [id], 若是[slug]文件夹应该是 slug:string
          id: string
        }
        // 此处的searchParams对应浏览器的query参数,即?username=xzq&age=18这种
        searchParams: {}
      }
      // 页面(默认导出),根据对应的动态路由渲染页面
      export default async function Page({ params }: pageParams) {
        const postData: any = await getPostData(params.id)
        return (
          <>
            <Head>
              <title>{postData.id}</title>
            </Head>
            <article>
              <h1 className={utilStyles.headingXl}>{postData?.id}</h1>
              <div className={utilStyles.lightText}>{postData?.date}</div>
              <div dangerouslySetInnerHTML={{ __html: postData?.contentHtml }} />
            </article>
            <Link style={{ position: 'absolute', marginTop: 100 }} href={`/blogs`}>
              back blogs
            </Link>
          </>
        )
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43

      接下来,访问:http://localhost:3000/blogs/ssg-ssr

      你就看到如下页面:

      在这里插入图片描述

    • 最终案例效果如下:

      在这里插入图片描述

    此外,还有两种动态路由,详情见Next官网

    VI. 加载UI

    特殊文件 loading.js 可帮助您使用 React Suspense 创建有意义的加载 UI。使用此约定,您可以在加载路由段的内容时显示来自服务器的即时加载状态。渲染完成后,新内容将自动交换。

    • 立即加载状态

      即时加载状态是导航时立即显示的回调 UI。您可以预渲染加载指示器,例如骨架和微调器,或者未来屏幕的一小部分但有意义的部分,例如封面照片、标题等。这有助于用户了解应用正在响应,并提供更好的用户体验。

      通过在文件夹中添加 loading.js 文件来创建加载状态。

      在这里插入图片描述

      src/app/dashboard下新建文件loading.tsx

      export default function Loading() {
        // You can add any UI inside Loading, including a Skeleton.
        return <>加载中...</>
      }
      
      • 1
      • 2
      • 3
      • 4

      在同一个文件夹中, loading.js 将嵌套在 layout.js .它会自动将 page.js 文件和下面的任何子项包装在 边界中。

      在这里插入图片描述

    • 流式处理相关,见Next官网

    VII. 错误处理

    普通错误

    文件 error.js 约定允许您正常处理嵌套路由中的意外运行时错误。

    • 自动将路由段及其嵌套子级包装在 React 错误边界中。
    • 使用文件系统层次结构创建针对特定段定制的错误 UI,以调整粒度。
    • 将错误隔离到受影响的段,同时保持应用程序的其余部分正常运行。
    • 添加功能以尝试从错误中恢复,而无需重新加载整个页面。

    通过在路由段中添加 error.js 文件并导出 React 组件来创建错误 UI:

    在这里插入图片描述

    src/app/dashboard下新建文件error.tsx

    'use client' // Error components must be Client Components
     
    import { useEffect } from 'react'
     
    export default function Error({
      error,
      reset,
    }: {
      error: Error & { digest?: string }
      reset: () => void
    }) {
      useEffect(() => {
        // Log the error to an error reporting service
        console.error(error)
      }, [error])
     
      return (
        <div>
          <h2>Something went wrong!</h2>
          <button
            onClick={
              // Attempt to recover by trying to re-render the segment
              () => reset()
            }
          >
            Try again
          </button>
        </div>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    注意:错误处理组件必须是一个客户端组件

    error.tsx的工作原理

    在这里插入图片描述

    • error.js 自动创建一个 React 错误边界,用于包装嵌套的子段或 page.js 组件。
    • error.js 文件导出的 React 组件用作回退组件。
    • 如果在错误边界内引发错误,则会包含该错误,并呈现回退组件。
    • 当回退错误组件处于活动状态时,错误边界上方的布局将保持其状态并保持交互性,并且错误组件可以显示从错误中恢复的功能
    处理嵌套路由错误

    通过特殊文件创建的 React 组件呈现在特定的嵌套层次结构中。

    例如,具有两个包含 layout.jserror.js 文件的段的嵌套路由在以下简化的组件层次结构中呈现:

    在这里插入图片描述

    嵌套组件层次结构对嵌套路由中的 error.js 文件行为有影响:

    • 错误冒泡到最近的父错误边界。这意味着 error.js 文件将处理其所有嵌套子段的错误。通过将文件放置在 error.js 路由的嵌套文件夹中的不同级别,可以实现或多或少的粒度错误 UI。
    • 错误处理error.js 不会处理同一段中 layout.js 组件中引发的错误,因为错误边界error.js 嵌套在该布局layout.js 的中。
    处理布局中的错误

    error.js 边界不会捕获抛出 layout.js 的错误或 template.js 同一段的组件。这种有意的层次结构使在发生错误时在同级路由(如导航)之间共享的重要 UI 可见且正常运行。

    要处理特定布局或模板中的错误,请将 error.js 文件放在布局父段中。

    处理根布局中的错误

    根错误边界 app/error.js 不会捕获根布局app/layout.js 或模板 app/template.js 组件中引发的错误。

    要处理根布局或模板中的错误,请使用命名的 error.js 变体:global-error.js

    与根错误边界error.js不同, global-error.js 错误边界包装整个应用程序,其回退组件在活动时替换根 error.js 布局。因此,重要的是要注意必须 global-error.js 定义自己的 标签

    global-error.js 是最精细的错误 UI,可被视为整个应用程序的“全部捕获”错误处理。它不太可能经常触发,因为根组件通常不太动态,其他 error.js 边界将捕获大多数错误。

    即使定义了 , global-error.js 仍建议定义一个根,其回退组件将在根 error.js 布局中呈现,其中包括全局共享的 UI 和品牌。

    src/app/global-error.tsx如下

    'use client'
     
    export default function GlobalError({
      error,
      reset,
    }: {
      error: Error & { digest?: string }
      reset: () => void
    }) {
      return (
        <html>
          <body>
            <h2>Something went wrong!</h2>
            <button onClick={() => reset()}>Try again</button>
          </body>
        </html>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    处理服务器错误

    如果在服务器组件中抛出错误,Next.js 会将一个 Error 对象(在生产中去除敏感错误信息)转发到最近的 error.js 文件作为 error prop。

    保护敏感错误信息,在生产过程中,转发到客户端的 Error 对象仅包含泛型 messagedigest 属性。这是一种安全预防措施,可避免将错误中包含的潜在敏感详细信息泄露给客户端。

    该属性包含有关错误的通用消息,该 message digest 属性包含自动生成的错误哈希,可用于匹配服务器端日志中的相应错误。

    在开发过程中,转发到客户端 Error 的对象将被序列化,并包含原始错误的 , message 以便于调试。

    VIII. 并行路由

    并行路由允许您在同一布局中同时或有条件地呈现一个或多个页面。对于应用的高度动态部分(例如社交网站上的仪表板和源),并行路由可用于实现复杂的路由模式。

    例如,您可以同时呈现团队和分析页面。

    在这里插入图片描述

    并行路由允许您为每个路由定义独立的错误和加载状态,因为它们正在独立流式传输

    在这里插入图片描述

    并行路由还允许您根据特定条件(如身份验证状态)有条件地呈现槽。这将在同一 URL 上启用完全分离的代码。

    在这里插入图片描述

    并行路由使用规则

    并行路由是使用命名槽创建的。插槽是按照 @folder 约定定义的,并作为props传递到同一级别的布局

    槽不是路由段,不会影响 URL 结构。可在路由 /members 访问文件路径 /@team/members

    例如,以下文件结构定义了两个显式插槽: @analytics@team

    在这里插入图片描述

    上面的文件夹结构意味着 app/layout.js 组件中现在接受 @analytics@team 插槽 props,并且可以将它们与 children 并行渲染:

    src/app/layout.tsx

    export default function RootLayout({
      children,
      team,
      analytics
    }: {
      children: React.ReactNode
      team: React.ReactNode
      analytics: React.ReactNode
    }) {
      return (
        <html lang="en">
          <body>
            <>
              {children}
              {team}
              {analytics}
            </>
          </body>
        </html>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    提示: children 道具是一个隐式插槽,不需要映射到文件夹。这意味着 app/page.js 等效于 app/@children/page.js

    src/app/@team/page.tsx

    export default function Page() {
      return <h1>Parallel Route Team </h1>
    }
    
    • 1
    • 2
    • 3

    src/app/@analytics/page.tsx类似

    最终我们访问:http://localhost:3000/

    在这里插入图片描述

    未被匹配的路由

    ==默认情况下,==插槽内渲染的内容将与当前 URL 匹配

    对于不匹配的插槽,Next.js 呈现的内容因路由技术和文件夹结构而异。

    default.tsx

    您可以定义一个文件default.tsx ,以便在 Next.js 无法根据当前 URL 恢复槽的活动状态时渲染

    • 在导航时,Next.js 将呈现槽以前处于活动状态的状态,即使它与当前 URL 不匹配。
    • 重新加载时,Next.js 将首先尝试渲染不匹配的插槽的default.tsx 的文件。如果default.tsx不存在,则呈现 404

    用于不匹配路由的 404 有助于确保您不会意外渲染不应并行渲染的路由。

    useSelectedLayoutSegment(s)

    useSelectedLayoutSegmentuseSelectedLayoutSegments 接受 parallelRoutesKey,这允许您读取该插槽内的活动路由段,不包括并行路由内部的路由。

    为了更好的演示,并行路由的以上情况,我们实现了一个案例来更好的解释,尤其是在未被匹配路由的情况下,以及重新加载时**default.tsx**的效果。(注意本操作,基于前面的项目)

    • 首先新建两个并行路由目录src/app/@teamsrc/app/@analytics

      @team目录下

      • 新建page.tsx

        export default function Page() {
          return <h1>Parallel Route Team </h1>
        }
        
        • 1
        • 2
        • 3
      • 新建default.js

        export default function Page() {
          return (
            <h1>
              Parallel Route Team <span style={{ color: 'red' }}>Default</span>
            </h1>
          )
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
      • 新建目录settings,在这个目录下新建page.tsx

        export default function Page() {
          return <h1>Parallel Route Team Settings </h1>
        }
        
        • 1
        • 2
        • 3

      @analytics目录下

      • 新建page.tsx

        export default function Page() {
          return <h1>Parallel Route Analytics </h1>
        }
        
        • 1
        • 2
        • 3
      • 新建default.js

        export default function Page() {
          return (
            <h1>
              Parallel Route Analytics <span style={{ color: 'yellow' }}>Default</span>
            </h1>
          )
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
    • 然后更新src/app/page.tsxsrc/app/layout.tsx

      • src/app/page.tsx

        import { Links } from '@/components/links'
        import { Metadata } from 'next'
        
        export const metadata: Metadata = {
          title: 'Next.js'
        }
        
        // `app/page.tsx` is the UI for the `/` URL
        export default function Page() {
          return (
            <>
              <Links linkList={['dashboard', 'settings']} />
            </>
          )
        }
        
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
      • src/app/layout.tsx

        'use client'
        import Link from 'next/link'
        import { useSelectedLayoutSegment, useSelectedLayoutSegments } from 'next/navigation'
        
        export default function RootLayout({
          children,
          team,
          analytics
        }: {
          children: React.ReactNode
          team: React.ReactNode
          analytics: React.ReactNode
        }) {
          const allSegments = useSelectedLayoutSegments()
          console.log('allSegments', allSegments)
          return (
            <html lang="en">
              <body>
                <>
                  {children}
                  {team}
                  {analytics}
                  <Link style={{ position: 'absolute', marginTop: 100 }} href={`/`}>
                    back index
                  </Link>
                </>
              </body>
            </html>
          )
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
        • 24
        • 25
        • 26
        • 27
        • 28
        • 29
        • 30
    • 新建src/app/default.tsx

      export default function Page() {
        return (
          <h1>
            App <span style={{ color: 'blue' }}>Default</span>{' '}
          </h1>
        )
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

    完成好的目录结构如下

    在这里插入图片描述

    启动项目访问:http://localhost:3000/,注意观察路由、重新加载页面已经控制台信息,结合上面提到的情况

    最终效果如下

    在这里插入图片描述

    可以看到,从首页导航进入/dashboard,页面渲染还是原来的并行路由,接着重新加载(刷新)页面,两个并行路由,分别渲染了对应的default.tsx;当从首页导航进入/settings时,其实这里访问的是/@team/settings的文件,页面渲染还是原来的并行路由,接着重新加载(刷新)页面,重新加载页面,因为当前路由是在/settings所以第一个并行路由渲染的是src/app/@team/settings/page.tsx,第二个并行路由则渲染自己的default.tsx,此外原来路由对应渲染的children,路由发生变化时,重新加载,也会使用自身的default.tsx

    注意:前面说到的404页面,在当前项目下,当你将/@team/default.tsx删除后,进入/dashboard,刷新页面,因为此时有一个并行路由找不到对应的default.tsx,所以会渲染404页面,这个可以选择去尝试一下

    另有关登录Modal模态框及条件路由的相关使用见Next官网

    IX. 拦截路由

    拦截路由允许您从当前布局中应用程序的另一部分加载路由。当您希望在用户不切换到其他上下文的情况下显示路由的内容时,此路由范式非常有用。

    例如,单击源中的照片时,可以以Modal模态框显示照片,覆盖源。在这种情况下,Next.js 会截获 /photo/123 路由,屏蔽 URL,并将其覆盖 /feed 在 上。

    在这里插入图片描述

    但是,当通过单击可共享的 URL 或刷新页面导航到照片时,应呈现整个照片页面而不是模式。不应发生路由拦截。

    在这里插入图片描述

    拦截路由的定义方式

    拦截路由可以使用指定规则定义,该规则类似于相对路径 (..) 约定 ../ ,但适用于路由。

    可以使用:

    • (.) 匹配同一级别的路由段
    • (..) 匹配上一级的路由段
    • (..)(..) 匹配上两级的路由
    • (...) 匹配根 app 目录中的路由段

    接下来,我们实现一个Modal框的小案例(建议将前面的记录用commit提交,后续方便回滚查看),我们将用到动态路由、并行路由和拦截路由的相关知识

    首先,添加src/lib/photos.ts,代码如下

    import { StaticImageData } from 'next/image'
    import imgTemp from '@/assets/images/opengraph-image.jpg'
    export type Photo = {
      id: string
      name: string
      href: string
      username: string
      imageSrc: StaticImageData
    }
    
    const photos: Photo[] = [
      {
        id: '1',
        name: 'Kevin Canlas',
        href: 'https://wallhaven.cc/w/gp1j9l',
        imageSrc: imgTemp,
        username: '@kvncnls'
      },
      {
        id: '2',
        name: 'Pedro Duarte',
        username: '@peduarte',
        href: 'https://wallhaven.cc/w/gp1j9l',
        imageSrc: imgTemp
      },
      {
        id: '3',
        name: 'Ahmad Awais',
        username: '@MrAhmadAwais',
        href: 'https://wallhaven.cc/w/gp1j9l',
        imageSrc: imgTemp
      },
      {
        id: '4',
        name: 'Leandro Soengas',
        username: '@lsoengas',
        href: 'https://wallhaven.cc/w/gp1j9l',
        imageSrc: imgTemp
      },
      {
        id: '5',
        name: 'Samina',
        username: '@saminacodes',
        href: 'https://wallhaven.cc/w/gp1j9l',
        imageSrc: imgTemp
      },
      {
        id: '6',
        name: 'lafond.eth',
        username: '@laf0nd',
        href: 'https://wallhaven.cc/w/gp1j9l',
        imageSrc: imgTemp
      },
      {
        id: '7',
        name: '山岸和利💛',
        username: '@ykzts',
        href: 'https://wallhaven.cc/w/gp1j9l',
        imageSrc: imgTemp
      },
      {
        id: '8',
        name: 'Altngelo',
        username: '@AfterDarkAngelo',
        href: 'https://wallhaven.cc/w/gp1j9l',
        imageSrc: imgTemp
      },
      {
        id: '9',
        name: 'Matias Baldanza',
        href: 'https://twitter.com/matiasbaldanza/status/1404834163203715073',
        username: '@matiasbaldanza',
        imageSrc: imgTemp
      }
    ]
    
    export default photos
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78

    新建src/assets/images文件夹,并放入一张自己喜欢的图片,我们这里命名为opengraph-image.jpg,此外我们将src/styles移动到src/assets中完善项目文件结构,其中的globals.css如下

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    html,
    body {
      padding: 0;
      margin: 0;
      font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans,
        Helvetica Neue, sans-serif;
      line-height: 1.6;
      font-size: 18px;
    }
    
    * {
      box-sizing: border-box;
    }
    
    a {
      color: #0070f3;
      text-decoration: none;
    }
    
    a:hover {
      text-decoration: underline;
    }
    
    img {
      max-width: 100%;
      display: block;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    新增两个组件在src/components

    • modal/Modal.tsx

      'use client'
      import { useCallback, useRef, useEffect, MouseEventHandler } from 'react'
      import { useRouter } from 'next/navigation'
      
      export default function Modal({ children }: { children: React.ReactNode }) {
        const overlay = useRef(null)
        const wrapper = useRef(null)
        const router = useRouter()
      
        const onDismiss = useCallback(() => {
          router.back()
        }, [router])
      
        const onClick: MouseEventHandler = useCallback(
          e => {
            if (e.target === overlay.current || e.target === wrapper.current) {
              if (onDismiss) onDismiss()
            }
          },
          [onDismiss, overlay, wrapper]
        )
      
        const onKeyDown = useCallback(
          (e: KeyboardEvent) => {
            if (e.key === 'Escape') onDismiss()
          },
          [onDismiss]
        )
      
        useEffect(() => {
          document.addEventListener('keydown', onKeyDown)
          return () => document.removeEventListener('keydown', onKeyDown)
        }, [onKeyDown])
      
        return (
          <div ref={overlay} className="fixed z-10 left-0 right-0 top-0 bottom-0 mx-auto bg-black/60" onClick={onClick}>
            <div
              ref={wrapper}
              className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full sm:w-10/12 md:w-8/12 lg:w-1/3 p-6"
            >
              {children}
            </div>
          </div>
        )
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
    • frame/Frame.tsx

      import Image from 'next/image'
      import { Photo } from '../../lib/photos'
      
      export default function Frame({ photo }: { photo: Photo }) {
        return (
          <>
            <Image
              alt=""
              src={photo.imageSrc}
              height={600}
              width={600}
              className="w-full object-cover aspect-square col-span-2"
            />
      
            <div className="bg-white p-4 px-6">
              <h3>{photo.name}</h3>
              <p>Taken by {photo.username}</p>
            </div>
          </>
        )
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21

    更新src/app/page.tsx

    import Link from 'next/link'
    import swagPhotos from '../lib/photos'
    import Image from 'next/image'
    
    export default function Home() {
      const photos = swagPhotos
    
      return (
        <main className="container mx-auto">
          <h1 className="text-center text-4xl font-bold m-10">Parallel routing and route interception achieve Modal</h1>
          <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 auto-rows-max	 gap-6 m-10">
            {photos.map(({ id, imageSrc }: { id: string; imageSrc: any }) => (
              <Link key={id} href={`/photos/${id}`}>
                <Image alt="" src={imageSrc} height={500} width={500} className="w-full object-cover aspect-square" />
              </Link>
            ))}
          </div>
        </main>
      )
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    更新src/app/layout.tsx

    'use client'
    import '@/assets/styles/globals.css'
    import Link from 'next/link'
    
    import { useSelectedLayoutSegment, useSelectedLayoutSegments } from 'next/navigation'
    
    export default function RootLayout({
      children,
      team,
      analytics,
      modal
    }: {
      children: React.ReactNode
      team: React.ReactNode
      analytics: React.ReactNode
      modal: React.ReactNode
    }) {
      const allSegments = useSelectedLayoutSegments()
      console.log('allSegments', allSegments)
      return (
        <html lang="en">
          <body>
            <>
              {children}
              {modal}
              <Link style={{ position: 'absolute', marginTop: 100 }} href={`/`}>
                back index
              </Link>
            </>
          </body>
        </html>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    更新src/app/default.tsx

    // app default
    export default function Page() {
      return null
    }
    
    • 1
    • 2
    • 3
    • 4

    新建文件夹src/app/@modal

    • 该文件夹下新建default.tsx,和app/default.tsx返回null防止在并行路由刷新页面404的情况

    • 该文件夹下新建(.)photos/[id]/page.tsx

      import Frame from '../../../../components/frame/Frame'
      import Modal from '../../../../components/modal/Modal'
      import swagPhotos, { Photo } from '../../../../lib/photos'
      
      export default function PhotoModal({ params: { id: photoId } }: { params: { id: string } }) {
        const photos = swagPhotos
        const photo: Photo = photos.find(p => p.id === photoId)!
      
        return (
          <Modal>
            <Frame photo={photo} />
          </Modal>
        )
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14

    新建src/app/photos/[id]/page.tsx,该动态路由的作用是当对应的路由被拦截后,刷新页面展示到这个路由页面

    import Frame from '../../../components/frame/Frame'
    import swagPhotos, { Photo } from '../../../lib/photos'
    
    export default function PhotoPage({ params: { id } }: { params: { id: string } }) {
      const photo: Photo = swagPhotos.find(p => p.id === id)!
    
      return (
        <div className="container mx-auto my-10">
          <div className="w-1/2 mx-auto border border-gray-700">
            <Frame photo={photo} />
          </div>
        </div>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    新建src/app/photos/default.tsx,和前面的一样返回null就行

    最终的效果

    在这里插入图片描述

  • 相关阅读:
    数据治理-数据管理组织的结构
    SpringBoot Application事件监听的实现方案(动态写入yml)
    Web服务器讲解(Tomcat)
    SpringBoot配置文件
    基于python的网络爬虫搜索引擎的设计
    Acwing 907. 区间覆盖
    【科普分享】linux服务器文件挂载技术介绍——mount
    【HDU No. 2874】 城市之间的联系 Connections between cities
    已知中序遍历数组和先序遍历数组,返回后序遗历数组
    15.利用webpack搭建server本地服务
  • 原文地址:https://blog.csdn.net/weixin_44959506/article/details/134263042