最新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
    
  • 接下来将看到如下提示:根据自己的习惯进行选择,这里我全选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>
}

执行命令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>
    }
    

    接下来我们将在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>
    }
    

    当我们访问对应路由//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>
      )
    }
    

    为了演示这个效果,我们单独封装了个简单的共享组件,当然后面也会详细说明在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>
      )
    }
    

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

    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>
      )
    }
    

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

    export default function Page() {
      return <h1>settings</h1>
    }
    

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

    嵌套布局,在文件夹(例如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>
    }
    
  • 修改<head>

    app目录中,您可以使用内置的SEO支持修改<head> 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 '...'
    }
    

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

III. 链接和导航

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

  • 使用Link组件
  • 使用useRouterHook
  • <Link>组件

    <Link> 是一个内置组件,用于扩展 HTML <a> 标记以提供路由之间的预取和客户端导航。这是在 Next.js 中的路由之间导航的主要方式。

    可以通过从next/link 导入<Link>并将 href传递给组件来使用它,

    使用例子,如前面添加的src/components/index.tsxhref属性传入跳转的对应路由,此外还可以以对象方式传入,Link组件具体使用

    '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>
      )
    }
    
  • useRouter()勾子

    此钩子只能在客户端组件中使用,并从next/navigation导入。

    'use client'
     
    import { useRouter } from 'next/navigation'
     
    export default function Page() {
      const router = useRouter()
     
      return (
        <button type="button" onClick={() => router.push('/dashboard')}>
          Dashboard
        </button>
      )
    }
    

    有关useRouter方法的完整列表,请参阅API参考。

IV. 路由分组

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

  • 路由分组的作用

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

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

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

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

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

    在这里插入图片描述

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

    在这里插入图片描述

  • 创建多个根布局

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

    在这里插入图片描述

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>
}
路由示例网址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,
  }))
}

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

  • 首先引入我们需要使用的样式文件,在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;
    }
    
  • 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.
      
    • 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.
      
  • 安装三个包

    npm i gray-matter remark remark-html
    
  • 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
      }
    }
    
  • 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>
      )
    }
    

    接下来,访问: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>
        </>
      )
    }
    

    接下来,访问: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 <>加载中...</>
    }
    

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

    在这里插入图片描述

  • 流式处理相关,见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>
  )
}

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

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 定义自己的 <html><body> 标签

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>
  )
}
处理服务器错误

如果在服务器组件中抛出错误,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>
  )
}

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

src/app/@team/page.tsx

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

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>
      }
      
    • 新建default.js

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

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

    @analytics目录下

    • 新建page.tsx

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

      export default function Page() {
        return (
          <h1>
            Parallel Route Analytics <span style={{ color: 'yellow' }}>Default</span>
          </h1>
        )
      }
      
  • 然后更新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']} />
          </>
        )
      }
      
      
    • 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>
        )
      }
      
  • 新建src/app/default.tsx

    export default function Page() {
      return (
        <h1>
          App <span style={{ color: 'blue' }}>Default</span>{' '}
        </h1>
      )
    }
    

完成好的目录结构如下

在这里插入图片描述

启动项目访问: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

新建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;
}

新增两个组件在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>
      )
    }
    
  • 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>
        </>
      )
    }
    

更新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>
  )
}

更新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>
  )
}

更新src/app/default.tsx

// app default
export default function Page() {
  return null
}

新建文件夹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>
      )
    }
    

新建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>
  )
}

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

最终的效果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/119346.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

克鲁斯卡尔算法

连通图中寻找最小生成树的常用算法有 2 种&#xff0c;分别是普里姆算法和克鲁斯卡尔算法。本节&#xff0c;我们将带您详细了解克鲁斯卡尔算法。 和普里姆算法类似&#xff0c;克鲁斯卡尔算法的实现过程也采用了贪心的策略&#xff1a;对于具有 n 个顶点的图&#xff0c;将图中…

竞赛选题 深度学习手势识别 - yolo python opencv cnn 机器视觉

文章目录 0 前言1 课题背景2 卷积神经网络2.1卷积层2.2 池化层2.3 激活函数2.4 全连接层2.5 使用tensorflow中keras模块实现卷积神经网络 3 YOLOV53.1 网络架构图3.2 输入端3.3 基准网络3.4 Neck网络3.5 Head输出层 4 数据集准备4.1 数据标注简介4.2 数据保存 5 模型训练5.1 修…

【仙逆】尸阴宗秘密揭露,王林差点被夺舍,修仙恐怖消息曝光

Hello,小伙伴们&#xff0c;我是小郑继续为大家深度解析国漫资讯。 深度爆料&#xff0c;《仙逆》国漫第九话最新剧情&#xff0c;尸阴宗表面上令人敬畏&#xff0c;但背后却隐藏着不为人知的秘密。这个宗门暗地里为受伤或死亡的强大修真者提供夺舍容器&#xff0c;帮助他们获…

定时发圈怎么设置?

微信本身是不能定时发送朋友圈的。微信公众号可以定时发送&#xff0c;微博可以定时发送&#xff0c;那微信可不可以也定时发送呢&#xff1f;当然可以&#xff0c;只要用这个方法&#xff0c;微信也能实现定时发朋友圈&#xff0c;不用再守着时间发朋友圈了。

持续集成交付CICD:安装Jenkins Slave(从节点)

目录 一、实验 1.安装Jenkins Slave&#xff08;从节点&#xff09; 二、问题 1.salve节点启动jenkins报错 2.终止命令行后jenkins从节点状态不在线 一、实验 1.安装Jenkins Slave&#xff08;从节点&#xff09; &#xff08;1&#xff09;查看jenkins版本 Version 2.…

基于SSM的酒店客房管理系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

Maven-构建工具

一、背景 开发者编写完成源码&#xff0c;还需要进行编译、测试、打包、部署等一系列操作。在一些小型项目中&#xff0c;还可能通过手动方式进行以上操作。但是在大型项目中&#xff0c;难以确定以上操作的顺序&#xff0c;而且会耗费更高的时间成本。 1.构建工具 构建工具…

必看!玩转Salesforce沙盒的5个实用技巧

定期刷新沙盒对于尝试最新版本的功能&#xff0c;以及防止在生产组织的环境中缺乏测试而导致开发工作回滚至关重要。 为了确保沙盒设置在刷新后顺利进行&#xff0c;需要考虑几个因素。首先&#xff0c;确保有完善的文档化流程。文档应分为Conga、DocuSign、数据&#xff08;C…

双通道 H 桥电机驱动芯片AT8833,软硬件兼容替代DRV8833,应用玩具、打印机等应用

上期小编给大家分享了单通道 H 桥电机驱动芯片&#xff0c;现在来讲一讲双通道的驱动芯片。 双通道 H 桥电机驱动芯片能通过控制电机的正反转、速度和停止等功能&#xff0c;实现对电机的精确控制。下面介绍双通道H桥电机驱动芯片的工作原理和特点。 一、工作原理 双通道 H 桥电…

【题解】2023 DTS算法竞赛集训 第1次

比赛地址&#xff1a;https://www.luogu.com.cn/contest/143650 P1319 压缩技术 https://www.luogu.com.cn/problem/P1319 简单的签到模拟题 #include <iostream>//c标准库 using namespace std; int main(){int a,n,t0,i0,b,s0;//t判断有没有回车&#xff0c;i判断输…

初识rust

调试下rust 的执行流程 参考&#xff1a; 认识 Cargo - Rust语言圣经(Rust Course) 新建一个hello world 程序&#xff1a; fn main() {println!("Hello, world!"); }用IDA 打开exe&#xff0c;并加载符号&#xff1a; 根据字符串找到主程序入口&#xff1a; 双击…

MySQL进阶之性能优化与调优技巧

数据库开发-MySQL 1. 多表查询1.1 概述1.1.2 介绍1.1.3 分类 1.2 内连接1.3 外连接1.4 子查询1.4.1 介绍1.4.2 标量子查询1.4.3 列子查询1.4.4 行子查询1.4.5 表子查询 2. 事务2.1 介绍2.2 操作2.3 四大特性 3. 索引3.1 介绍3.2 结构3.3 语法 1. 多表查询 1.1 概述 1.1.2 介绍…

云计算的大模型之争,亚马逊云科技落后了?

文丨智能相对论 作者丨沈浪 “OpenAI使用了Azure的智能云服务”——在过去的半年&#xff0c;这几乎成为了微软智能云最好的广告词。 正所谓“水涨船高”&#xff0c;凭借OpenAI旗下的ChatGPT在全球范围内爆发&#xff0c;微软趁势拉了一波自家的云计算业务。2023年二季度&a…

个人和企业如何做跨境电商?用API电商接口教你选平台选品决胜跨境电商

当下是跨境电商快速发展的阶段&#xff0c;在未来将会朝向成熟系统化的方向发展&#xff0c;对于跨境电商从业者来说既是机遇&#xff0c;也是挑战。那么&#xff0c;个人做跨境电商的核心要素是什么&#xff1f;又该如何去做&#xff1f;对此&#xff0c;小编总结以下四大核心…

【C/C++】什么是POD(Plain Old Data)类型

2023年11月6日&#xff0c;周一下午 目录 POD类型的定义标量类型POD类型的特点POD类型的例子整数类型&#xff1a;C 风格的结构体&#xff1a;数组&#xff1a;C 风格的字符串&#xff1a;std::array:使用 memcpy 对 POD 类型进行复制把POD类型存储到文件中&#xff0c;并从文…

3.Netty中Channel通道概述

Selector 模型 Java NIO 是基于 Selector 模型来实现非阻塞的 I/O。Netty 底层是基于 Java NIO 实现的&#xff0c;因此也使用了 Selector 模型。 Selector 模型解决了传统的阻塞 I/O 编程一个客户端一个线程的问题。Selector 提供了一种机制&#xff0c;用于监视一个或多个 …

Java2 - 数据结构

5 数据类型 5.1 整数类型 在Java中&#xff0c;数据类型用于定义变量或表达式可以存储的数据的类型。Java的数据类型可分为两大类&#xff1a;基本数据类型和引用数据类型。 byte&#xff0c;字节 【1字节】表示范围&#xff1a;-128 ~ 127 即&#xff1a;-2^7 ~ 2^7 -1 sho…

第二章:人工智能深度学习教程-深度学习简介

深度学习是基于人工神经网络的机器学习的一个分支。它能够学习数据中的复杂模式和关系。在深度学习中&#xff0c;我们不需要显式地对所有内容进行编程。近年来&#xff0c;由于处理能力的进步和大型数据集的可用性&#xff0c;它变得越来越流行。因为它基于人工神经网络&#…

mac装不了python3.7.6

今天发现一个很奇怪的问题 但是我一换成 conda create -n DCA python3.8.12就是成功的 这个就很奇怪

【ElasticSearch系列-04】ElasticSearch的聚合查询操作

ElasticSearch系列整体栏目 内容链接地址【一】ElasticSearch下载和安装https://zhenghuisheng.blog.csdn.net/article/details/129260827【二】ElasticSearch概念和基本操作https://blog.csdn.net/zhenghuishengq/article/details/134121631【三】ElasticSearch的高级查询Quer…