Micro View of Allium

Micro View of Allium

Jeango

Jeango 2/3/2021, 3:20:07 AM

👉 Route 路由配置

路由和页面生成,还有数据获取是整体,整体的学习目标:

  • 了解路由的四种基本形式,常规路由,和三种动态路由形式 [slug][...slug] 还有 [[...slug]]
  • 通过使用 getStaticPaths()该当和动态路由进行静态生成。
  • 实现 getStaticProps() 函数以获取数据生成页面时依赖的数据。
  • 使用 remark 模块实现 markdown 文档解析。
  • 链接到动态路由的页面。

前面解析过,Next.js 的路由是基于文件路径的生成的,一个普通的文件路径信息就是一条路由,即页面文件具有什么样的路径信息就生成什么样的路由。

Next.js 支持具有动态路由的页面,具有动态路由的页面路径中使用方括号。例如,如果你创建了一个命名为 pages/posts/[id].js 的页面文件,那么就可以通过 posts/1posts/2 等类似的 URL 地址进行访问,访问时会将参数传入并且由 Router 对象解析出来。

动态路由 Dynamic Routes,当页面文件名或目录名中用方括号命名,就是动态路由,这表示给生成的路由设置了参数。

以下示范,如何在请求页面时使用 Router 对象获取 URL 传递的数据:

import { useRouter } from 'next/router'

const Post = () => {
  const router = useRouter()
  const { id } = router.query

  return <p>Post: {id}</p>
}

export default Post

在设置 Link 组件属性时,href 属性一般也包含 [id] 这个符号,还要完整包含目录路径结构的其它部分。在 Next.js 10 新特性中,使用 as 属性是一个可选项,也可以用来覆盖 href 属性。注意,as 属性比 href 属性更优先,这个值也就是在客户端的状态,它会回传到服务器以匹配页面文件

参考 dynamic-routing 示范工程的 \pages\post\[id]\index.js

import { useRouter } from 'next/router'
import Link from 'next/link'
import Header from '../../../components/header'

const Post = () => {
  const router = useRouter()
  const { id } = router.query

  return (
    <>
      <Header />
      <h1>Post: {id}</h1>
      <ul>
        <li>
          <Link href="/post/[id]/[comment]" as={`/post/${id}/first-comment`}>
            <a>First comment</a>
          </Link>
        </li>
        <li>
          <Link href="/post/[id]/[comment]" as={`/post/${id}/second-comment`}>
            <a>Second comment</a>
          </Link>
        </li>
      </ul>
    </>
  )
}

export default Post

Dynamic route segments,要使用页面文件路径匹配上动态路由片段,可以在目录、文件名字上使用方括号,参考以下文件名与生成的路由对应关系:

  • pages/blog/[slug].js ---> /blog/:slug --> (/blog/hello-world)
  • pages/[username]/settings.js ---> /:username/settings --> (/foo/settings)
  • pages/post/[...all].js ---> /post/* --> (/post/2020/id/title)

注意事项,当多个路由匹配时,预定义路由优先,即非动态路由比动态路由优先匹配执行,按以下优先顺序执行匹配:

  • Predefined Routes
  • Dynamic Routes
  • Catch-all Routes
  • Optional Catch-all Routes

在同一级目录,Catch-all Routes 和 Optional Catch-all Routes 不能同时设置。

例如:

pages/post/create.js --> 匹配 /post/create pages/post/[pid].js --> 匹配 /post/1, /post/ab, /post/cd ... 但是不能匹配 /post/create pages/post/[...slug].js --> 匹配 /post/1/2, /post/a/b/c ... 但不能匹配 /post/create, /post/abc pages/post/[[...opt]].js --> 匹配前面几种情况之外的路由,还可以匹配 /post

那些由 Automatic Static Optimization 自动静态优化过的页面会在不依赖路由参数的情况下水合 Hydrated,即 query 是 {} 空对象,没有数据。然后服务端会触发一个更新,让应用提供 query 数据。

自动静态优化是通过检测页面是否有导出 getServerSideProps()getInitialProps() 函数决定是否进行优化的。

使用动态路由就需要实现 getStaticPaths() 函数,以返回有效路由匹配列表。因为具体要实现什么路由路径的页面只有给出定义,Next.js 才知道哪么路径的页面是有效的。只有命中列表的才算是有效页面路径,Next.js 才会去执行页面生成。如果,想对没有命中的路由路径进行其它处理,那么就需要使用 getStaticPaths() 函数反的参数来设置,其中的 fallback 参数是设置如何处理未命中路由的,参考 getStaticPaths() 静态化路径生成函数。

如果不为动态路由实现 getServerSideProps()getInitialProps() 函数,则它们表现和一般的预定义路由没有什么不同。并且请求页面时,也收不到参数的,这是因为这种情况下 Next.js 使用的是静态构建模式。

作为测试,可以准备以下这组 Link 组件,分别用于不同路由规则的测试:

🚩<Link href="/route/normal"><a>Normal Link</a></Link>
🚩<Link href="/route/normal" as="/route/normal?more=yes"><a>Normal As</a></Link>

🚩<Link href="/route/dynamic"><a>Dynamic Link</a></Link>
🚩<Link href="/route/dynamic" as="/route/more?more=yes"><a>Dynamic More</a></Link>

🚩<Link href="/route/catch/all"><a>Catch All</a></Link>
🚩<Link href="/route/catch/GitHub?more=yes"><a>Catch GitHub</a></Link>

另外准备几个页面,命名以使用不同的路由规则,这里以 /route/[[...catchopt]].tsx 作为示范:

import Layout from '../../components/layout'
import {useRouter} from 'next/router'

export default function Catchopt(props:any){
    const Router = useRouter()
    let json = JSON.stringify(Router.query);
    let fallback = Router.isFallback && "FALLBACK";
    console.log(`------ [[...catchopt]].tsx`, fallback, props, Router);
    return (
    <Layout>
    <h3 className="card">Optional Catch-all Routes Test</h3>
    {Router.isFallback?(
        <h1 className="card">Router.isFallback</h1>
    ):(
        <pre className="card">{json}</pre>
    )}
    </Layout>
    )
}

export async function getStaticProps(context: any) {
    console.log("++++++ Optional Catch-all getStaticProps", context);
    return {
        props: {
            data: JSON.stringify(context)
        }
    }
}

export async function getStaticPaths(context:any) {
    console.log("++++++ Optional Catch-all getStaticPaths", context);
    const res = await fetch(`https://github.com/manifest.json`)
    const data = await res.json()
    let locale = context.locale ?? 'zh-CN';
    let paths = [
        {locale, params:{ catchopt: ['catch', 'all'] }},
        {locale, params:{ catchopt: ['catch', data.name] }},
        {locale, params:{ catchopt: false }}, 
    ]
    return { paths, fallback: false }
}

这种 Optional Catch-all 路由是可选形式的 Catch-all,可以在 params 设置 null, [], undefined 或者 false 都可以,表示匹配路由文件所在的一级目录,这就是可选的灵活性。就这里,/route/[[...catchopt]].tsx 页面而言,设置为 null 匹配的是 /route 这个路径的页面,对应生成的文件就是 /route.html

从构建过程中输出的目录结构也可以看到,各个路由路径对应的输出:

Page                                                           Size     First Load JS
┌ ● /                                                          2.29 kB        73.7 kB
├   /_app                                                      0 B            63.6 kB
├ ○ /404                                                       3.46 kB          67 kB
├ ○ /authors/me                                                2.86 kB        66.4 kB
├ ○ /posts/build                                               981 B          72.4 kB
├ ● /posts/posts                                               1.92 kB        65.5 kB
├ λ /posts/tutorial-assets                                     3.66 kB        82.9 kB
├ ● /route/[[...catchopt]]                                     458 B          68.4 kB
├   ├ /en/route/catch/all
├   ├ /en/route/catch/GitHub
├   └ /en/route
├ ● /route/[dynamic]                                           411 B          68.3 kB
├   ├ /en/route/dynamic
├   ├ /en/route/pass
├   ├ /en/route/sideway
├   └ /en/route/more
├ ○ /route/normal                                              404 B          68.3 kB
└ ● /tutorial/[...slug]                                        685 B          79.9 kB
    ├ /zh-CN/tutorial/tutorial-typescript
    ├ /zh-CN/tutorial/tutorial-styling
    ├ /zh-CN/tutorial/tutorial-start
    └ [+5 more paths]

在导出 getStaticProps()getStaticPaths() 函数时,就是在向 Next.js 表明,页面需要使用动态的数据,渲染生成时需要执行它们,参考输出信息:

++++++ Optional Catch-all getStaticPaths { locales: null, defaultLocale: null }
++++++ Optional Catch-all getStaticProps {
  params: { catchopt: [ 'catch', 'GitHub' ] },
  locales: undefined,
  locale: undefined,
  defaultLocale: undefined
}
------ [[...catchopt]].tsx false { data: '{"params":{"catchopt":["catch","GitHub"]}}' } ServerRouter {
  route: '/route/[[...catchopt]]',
  pathname: '/route/[[...catchopt]]',
  query: { catchopt: [ 'catch', 'GitHub' ] },
  asPath: '/route/catch/GitHub',
  basePath: '',
  events: undefined,
  isFallback: false,
  locale: undefined,
  isReady: false,
  locales: undefined,
  defaultLocale: undefined,
  domainLocales: undefined
}

不难发现,getStaticProps() 函数返回的 props 就是组件的输入参数。而它的输入的 context 中的 params 包含动态路由参数信息,比如页面 [id].js 会收到类似 { id: ... } 这样的路由参数,应该结合 getStaticPaths() 考虑。

可以尝试注解掉 getStaticProps()getStaticPaths(),比较一下组件在执行时获取到的参数,会发现原本来自客户端的查询字符串没有了。

如果要在项目中使用本地化信息,请在配置文件中设置:

// 👉 /next.config.js 配置脚本
module.exports = {
  i18n: {
    locales: [ 'en', 'zh-CN' ],
    defaultLocale: 'en',
  },
}

这样,运行后可以从 getStaticProps()getStaticPaths() 函数的传入参数中获取:

{ locales: [ 'en', 'zh-CN' ], locale: 'zh-CN', defaultLocale: 'en' }

其中 locale 是通过浏览器标头检查出来的,而且只在首页中执行检查,假设浏览发送了以下标头内容:

Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,pt;q=0.7,zh-TW;q=0.6,ja;q=0.5

设置本地化信息后,路由路径会自动处理,添加区域前缀。但是对于动态路由,需要相应调整,否则匹配不正确。

Link 组件可以使用 Node URL 对象,所以它又可以按以下方式给 href 属性传入一个对象结构:

import Link from 'next/link'

export default function BlogLink() {
  return (
    <Link
      href={{
        pathname: '/blog/[post]/[comment]',
        query: { post: 'post-1', comment: 'comment-1' },
      }}
    >
      <a>Valid link</a>
    </Link>
  )
}

动态路由的使用一般就是表示要使用动态数据,只要导出数据获取 API,就会在构建期的数据获取行为。除非使用浅路由,Shallow Routing 它可以导致不执行本应在构建期的执行的 getServerSideProps, getStaticProps, getInitialProps 等数据获取方法。

Dynamic Routes 还可以扩展捕获所有路径,在方括号中使用三个省略点就可以,如pages/post/[...slug].js 就可以匹配 /post/a, /post/a/b, /post/a/b/c 等等。

可选扩展方式使用双重方括号,匹配范围多一层,如 pages/post/[[...slug]].js 还可以多匹配 /post 这一情况。

而 Linkg 属性相应设置的 query 对象如下:

{ } // GET `/post` (empty object)
{ "slug": ["a"] } // `GET /post/a` (single-element array)
{ "slug": ["a", "b"] } // `GET /post/a/b` (multi-element array)

启用浅路由只需要设置路由选项的 shallow 为 true:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

// Current URL is '/'
function Page() {
  const router = useRouter()

  useEffect(() => {
    // Always do navigations after the first render
    router.push('/?counter=10', undefined, { shallow: true })
  }, [])

  useEffect(() => {
    // The counter changed!
  }, [router.query.counter])
}

export default Page

当 URL 更新时,如 /?counter=10 页面并不会替代,只有路由状态改变才会,可以通过类组件的 componentDidUpdate() 生命周期函数来观察:

componentDidUpdate(prevProps) {
  const { pathname, query } = this.props.router
  // verify props have changed to avoid an infinite loop
  if (query.counter !== prevProps.router.query.counter) {
    // fetch data based on the new query
  }
}

通常 next/link 已经满足大部分的路由需求,但还是有时候要在客户端进行导航,这时可以使用 Router 对象:

import { useRouter } from 'next/router'

function ReadMore() {
  const router = useRouter()

  return (
    <span onClick={() => router.push('/about')}>Click here to read more</span>
  )
}

export default ReadMore