NodeJS系列(11)- Next.js 框架 (四)

发布时间 2023-08-14 18:43:50作者: 垄山小站


在 “NodeJS系列(8)- Next.js 框架 (一)” 里,我们简单介绍了 Next.js 的安装配置,创建了 nextjs-demo 项目,讲解和演示了 Next.js 项目的运行、路由(Routing)、页面布局(Layout)等内容。

在 “NodeJS系列(9)- Next.js 框架 (二)” 里,我们在 nextjs-demo 项目基础上,讲解和演示了 Next.js 项目的国际化 (i18n)、中间件 (Middleware) 等内容。

在 “NodeJS系列(10)- Next.js 框架 (三)” 里,我们在 nextjs-demo 项目基础上,讲解和演示了渲染(Rendering)。

本文继续在 nextjs-demo 项目(Pages Router)基础上,讲解和演示数据获取(Data Fetching)。

NextJS: https://nextjs.org/
NextJS GitHub: https://github.com/vercel/next.js

 

1. 系统环境

    操作系统:CentOS 7.9 (x64)
    NodeJS: 16.20.0
    NPM: 8.19.4
    NVM: 0.39.2
    NextJS: 13.4.12

 

2. 数据获取 (Data Fetching)

    Next.js 中的数据提取允许您根据应用程序的用例以不同的方式呈现内容。其中包括使用服务器端渲染或静态生成进行预渲染,以及使用增量静态再生成在运行时更新或创建内容。

    数据获取相关的几个函数:

       (1) getStaticProps,用于构建时获取一些静态数据,默认情况下只会在构建时执行一次,之后的每次请求都会使用构建时的数据。
       (2) getStaticPaths,从使用动态路由的页面导出名为 getStaticPaths 的函数时,Next.js 将静态预渲染 getStaticPaths 指定的所有路径。
       (3) getServerSideProps,从页面导出名为 getServerSideProps(服务器端渲染)的函数时,Next.js 将使用getServerSideProps 返回的数据在每次请求时预渲染该页面。
       (4) getInitialProps,是一个遗留的API。建议使用 getStaticProps 或 getServerSideProps。

 

3. getStaticProps

    如果从页面导出一个名为 getStaticProps(静态站点生成)的函数,Next.js 将在构建时使用 getStaticProps 返回的 props 预渲染此页面。

    getStaticProps 适用于如下情景:

        (1) 渲染页面所需的数据在用户请求之前的构建时(build)可用
        (2) 数据来自 Headless CMS
        (3) 页面必须预渲染(用于 SEO)并且速度非常快 —— getStaticProps 生成 HTML 和 JSON 文件,这两个文件都可以通过 CDN 缓存以提高性能
        (4) 数据可以被公开缓存(不是特定于用户的)。在某些特定情况下,可以通过使用中间件重写路径来绕过此条件。

    getStaticProps 何时运行(被调用):

        (1) 在 next build 期间,getStaticProps 会被调用
        (2) 当使用 fallback:true 时,getStaticProps 在后台运行
        (3) 当使用 fallback: blocking 时,初始渲染之前调用 getStaticProps
        (4) 当使用 revalidate 时,getStaticProps 在后台运行
        (5) 当使用 revalidate 时,getStaticProps 在后台按需运行

        注:getStaticProps 始终在服务器上运行,而从不在客户端上运行。可以使用此工具 (https://next-code-elimination.vercel.app/) 验证在 getStaticProps 中编写的代码是否已从客户端捆绑包中删除。
            使用 fallback:true,表示如果访问的页面不存在,Next.js 会返回一个 fallback 页面。这个 fallback 页面会在客户端重新生成,并在浏览器中显示。
            使用 fallback: blocking,表示页面的渲染将在所有数据准备就绪后再进行。       
            使用 revalidate,即增量静态再生成,getStaticProps 将在后台运行,同时重新验证陈旧页面,并将新页面提供给浏览器。

    getStaticProps 的限制性:

        (1)getStaticProps 无法访问传入请求(如查询参数或HTTP标头),因为它生成静态 HTML。如果需要访问页面请求,请考虑除了使用 getStaticProps 之外还使用中间件。
        (2)getStaticProps 只能从页面导出。不能从非页面文件、_app、_document 或 _error 导出它。这种限制的原因之一是 React 需要在渲染页面之前拥有所有所需的数据。
        (3)必须将导出 getStaticProps 作为一个独立函数使用 —— 如果将 getStaticProps 添加为页面组件的属性,它将不起作用。
 
    在开发模式(next dev)中,将对每个请求调用 getStaticProps,可以暂时绕过静态生成。例如,可能正在使用无头 CMS,并且希望在草稿发布之前预览草稿。

    1) 使用 getStaticProps 从 external API 获取数据

        示例,创建 src/pages/render/data1.js 文件,代码如下:

            export default ({ repo }) => {
                return repo.stargazers_count
            }

            export const getStaticProps = async () => {
                const res = await fetch('https://api.github.com/repos/vercel/next.js')
                const repo = await res.json()
                return { props: { repo } }
            }

        开发模式运行 nextjs-demo 项目,即运行 npm run dev 命令。
        
        使用浏览器访问 http://localhost:3000/render/data1,显示内容如下:

            Home  Login     # 菜单
            110193
            Footer

    2) 直接编写服务器端代码

        由于 getStaticProps 仅在服务器端运行,因此它永远不会在客户端运行。它甚至不会包含在浏览器的 JS 捆绑包中,因此可以直接编写数据库查询,而无需将其发送到浏览器。

        这意味着,可以直接在 getStaticProps 中编写服务器端代码,而不是从 getStaticProps 获取API路由(它本身从外部源获取数据)。

        示例,API 路由用于从 CMS 获取一些数据。然后直接从 getStaticProps 调用该 API 路由。这会产生额外的调用,从而降低性能。相反,从 CMS 获取数据的逻辑可以通过使用 lib/ 目录来共享。然后可以与 getStaticProps 共享。
           
        创建 src/pages/render/lib/load-posts.js 文件,代码如下:

            // The following function is shared
            // with getStaticProps and API routes
            // from a `lib/` directory
            export const loadPosts = async () => {
                // Call an external API endpoint to get posts
                const res = await fetch('https://.../posts/')
                const data = await res.json()
                
                return data
            }

 

        创建 src/pages/render/blog.js 文件,代码如下:

            // pages/blog.js
            import { loadPosts } from './lib/load-posts'
            
            // This function runs only on the server side
            export const getStaticProps = async () => {
                // Instead of fetching your `/api` route you can call the same
                // function directly in `getStaticProps`
                const posts = await loadPosts()
                
                // Props returned will be passed to the page component
                return { props: { posts } }
            }


        如果不使用 API 路由来获取数据,则可以在 getStaticProps 中直接使用 fetch API 来获取数据。

    3) 静态生成 HTML 和 JSON

        当在构建时预渲染带有 getStaticProps 的页面时,除了页面 HTML 文件外,Next.js 还会生成一个 JSON 文件,其中包含运行 getStaticProps 的结果。

        此 JSON 文件将用于通过 next/link 或 next/router 的客户端路由。当导航到使用 getStaticProps 预渲染的页面时,Next.js 会获取这个 JSON 文件(在构建时预先计算),并将其用作页面组件的道具。这意味着客户端页面转换不会调用 getStaticProps,因为只使用导出的 JSON。

        当使用增量静态生成时,getStaticProps 将在后台执行,以生成客户端导航所需的 JSON。可能会以对同一页面发出多个请求的形式看到这一点,但是,这是有意的,对最终用户性能没有影响。

 

4. getStaticPaths

    如果页面具有动态路由并使用 getStaticProps,则需要定义要静态生成的路径列表。

    从使用动态路由的页面导出名为 getStaticPaths(静态站点生成)的函数时,Next.js 使用 getStaticPaths 指定静态预渲染所有路径。

    getStaticPaths 适用于如下情景:

        (1) 数据来自 Headless CMS
        (2) 数据来自数据库
        (3) 数据来自文件系统
        (4) 数据可以公开缓存(不是特定于用户)
        (5) 页面必须预渲染(用于SEO)并且速度非常快 —— getStaticProps 生成 HTML 和 JSON 文件,这两个文件都可以通过 CDN 缓存以提高性能

        注:每种情景都是基于静态预渲染使用动态路由的页面

    getStaticPaths 何时运行(被调用):

        (1) 在 next build 期间,getStaticProps 需要 getStaticPaths 运行返回路径列表
        (2) 当使用 fallback:true 时,getStaticProps 在后台运行,需要 getStaticPaths 运行返回路径列表
        (3) 当使用 fallback: blocking 时,在初始渲染之前调用 getStaticProps,需要 getStaticPaths 运行返回路径列表

        注:getStaticPaths 将只在生产中的构建(build)过程中运行,在运行时不会调用它。可以使用此工具 (https://next-code-elimination.vercel.app/) 验证在 getStaticPaths 中编写的代码是否已从客户端捆绑包中删除。

    getStaticPaths 的限制性:

        (1) getStaticPaths 必须与 getStaticProps 一起使用
        (2) 不能将 getStaticPaths 与 getServerSideProps 一起使用
        (3) 可以从同样使用 getStaticProps 的动态路由导出 getStaticPaths
        (4) 无法从非页面文件(例如组件文件夹)导出 getStaticPaths
        (5) 必须将 getStaticPaths 导出为独立函数,而不是页面组件的属性

    在开发模式(next dev)中,将对每个请求调用 getStaticPaths。

    示例1,getStaticPaths 允许控制在构建过程中生成哪些页面,而不是通过 fallback 按需生成。创建 src/pages/render/repo/[name].js 文件,代码如下:

        export default ({ repo }) => {
            return repo.stargazers_count
        }

        export const getStaticPaths = async () => {
            return {
                paths: [
                    {
                        params: {
                            name: 'next.js',
                        },
                    }, // See the "paths" section below
                ],
                fallback: true, // false or "blocking"
            }
        }
        
        export const getStaticProps = async () => {
            const res = await fetch('https://api.github.com/repos/vercel/next.js')
            const repo = await res.json()
            return { props: { repo } }
        }

       
    示例2,可以通过返回一个空的路径数组来推迟按需生成所有页面。创建 src/pages/render/posts/[id].js 文件,代码如下:

        export default ({ posts }) => {
            
            ...

        }

        export async function getStaticPaths() {
            // When this is true (in preview environments) don't
            // prerender any static pages
            // (faster builds, but slower initial page load)
            if (process.env.SKIP_BUILD_STATIC_GENERATION) {
                return {
                    paths: [],
                    fallback: 'blocking',
                }
            }
        
            // Call an external API endpoint to get posts
            const res = await fetch('https://.../posts')
            const posts = await res.json()
            
            // Get the paths we want to prerender based on posts
            // In production environments, prerender all pages
            // (slower builds, but faster initial page load)
            const paths = posts.map((post) => ({
                params: { id: post.id },
            }))
            
            // { fallback: false } means other routes should 404
            return { paths, fallback: false }
        }        

        export const getStaticProps = async () => {

            ...

            return { props: { posts } }
        }

 

5. getServerSideProps

    如果从页面导出名为 getServerSideProps(服务器端渲染)的函数,Next.js 将使用 getServerSideProps 返回的数据在每次请求时预渲染该页面。

    只有在需要渲染数据必须在请求时提取的页面时,才应使用 getServerSideProps。如果在请求期间不需要渲染数据,则应考虑在客户端或 getStaticProps 上获取数据。
    
    getServerSideProps 适用于如下情景:

        与 SEO 不相关,也不需要预渲染页面。数据经常更新,需要在请求时获取数据。

    getServerSideProps 何时运行(被调用):

        (1) 当直接请求使用 getServerSideProps 的页面时,getServerSideProps 会在请求时运行,并且此页面将使用返回的 props 进行预渲染
        (2) 当通过 next/link 或 next/router 在客户端页面转换时请求使用 getServerSideProps 页面时,next.js 会向服务器发送一个 API 请求,该请求运行 getServerSideProps

        注:页面请求都会调用 getServerSideProps。getServerSideProps 只在服务器端运行,从不在浏览器上运行。

    getServerSideProps 的限制性:

        (1) 只有在配置了缓存控制标头的情况下才会缓存
        (2) 无法从非页面文件(例如组件文件夹)导出 getServerSideProps
        (3) 必须将 getServerSideProps 导出为独立函数,而不是页面组件的属性

    示例1,在请求时使用 getServerSideProps 获取数据,代码如下:

        export default ({ data }) => {
            // Render data ...

        }
        
        // This gets called on every request
        export const getServerSideProps = async () => {
            // Fetch data from external API
            const res = await fetch(`https://.../data`)
            const data = await res.json()
            
            // Pass data to the page via props
            return { props: { data } }
        }


        注:如果 getServerSideProps 函数内 fetch 的 API 路由 (`https://.../data`) 是本地资源,就是不必要且效率低下的方法,因为它将导致由于服务器上运行 getServerSideProps 和 API 路由而产生额外的请求。

            本地数据库、JSON 文件等资源,最好在 getServerSideProps 函数内直接调用,可以适当降低服务器的负载压力。当然,具体情况具体分析,在安全、效率和开发速度等因素之间,需要开发者自己根据产品需要做好平衡。
                
    示例2,使用服务器端渲染(SSR)进行缓存,代码如下:

        export default ({ data }) => {
            // Render data ...

        }

        // This value is considered fresh for ten seconds (s-maxage=10).
        // If a request is repeated within the next 10 seconds, the previously
        // cached value will still be fresh. If the request is repeated before 59 seconds,
        // the cached value will be stale but still render (stale-while-revalidate=59).
        //
        // In the background, a revalidation request will be made to populate the cache
        // with a fresh value. If you refresh the page, you will see the new value.
        export const getServerSideProps = async ({ req, res }) => {
            res.setHeader(
                'Cache-Control',
                'public, s-maxage=10, stale-while-revalidate=59'
            )

            ...
            
            // Pass data to the page via props
            return { props: { data } }
        }


    可以通过修改配置,在每页的基础上显式设置运行时,例如:

        export const config = {
            runtime: 'nodejs', // or "edge"
        }
        
        export const getServerSideProps = async () => {}


        注:getServerSideProps 可以与 Serverless 和 Edge Runtime 一起使用,并且可以在两者中设置 props。但是,当前在 Edge Runtime 中,无权访问响应对象。这意味着不能在 getServerSideProps 中添加 cookie。要访问响应对象,应该继续使用 Node.js Runtime,这是默认的 Runtime。

    如果在 getServerSideProps 中抛出错误,它将显示 pages/500.js 文件。在开发过程中,不会使用此文件,而是显示 dev 覆盖。

 

6. Web 表单

    Web 表单具有客户端-服务器关系,一般被用于发送由 Web 服务器处理的数据以进行处理和存储。表单本身就是客户端,服务器是任何可以在需要时用于存储、检索和发送数据的存储机制。

    示例,Web 表单提交数据到 /api/login,创建 src/pages/api/login.js 文件,代码如下:

        export default (req, res) => {
            // Get data submitted in request's body.
            const body = req.body

            // Optional logging to see the responses
            // in the command line where next.js app is running.
            console.log('body: ', body)

            if (!body.username || !body.password) {
                // Sends a HTTP bad request error code
                return res.status(400).json({ data: 'Username or password not found' })
            }

            // Found the name.
            // Sends a HTTP success code
            res.status(200).json({ data: `${body.username} ${body.password}` })
        }


    创建 src/pages/login.js 文件,代码如下:

        export default () => {
            // Handles the submit event on form submit.
            const handleSubmit = async (event) => {
                // Stop the form from submitting and refreshing the page.
                event.preventDefault()

                // Get data from the form.
                const data = {
                    username: event.target.username.value,
                    password: event.target.password.value,
                }

                console.log('data: ', data)

                // Send the data to the server in JSON format.
                const JSONdata = JSON.stringify(data)

                // API endpoint where we send form data.
                const endpoint = '/api/login'

                // Form the request for sending data to the server.
                const options = {
                    // The method is POST because we are sending data.
                    method: 'POST',
                    // Tell the server we're sending JSON.
                    headers: {
                    'Content-Type': 'application/json',
                    },
                    // Body of the request is the JSON data we created above.
                    body: JSONdata,
                }

                // Send the form data to our forms API on Vercel and get a response.
                const response = await fetch(endpoint, options)

                console.log('response: ', response)

                // Get the response data from server as JSON.
                // If server returns the name submitted, that means the form works.
                const result = await response.json()
                alert(`Login info: ${result.data}`)
            }

            return (
                <form onSubmit={handleSubmit}>
                <p>&nbsp;</p>

                <p><label for="username">Username: </label>
                <input type="text" id="username" name="username" required value="admin"/></p>

                <p><label for="password">Password: </label>
                <input type="text" id="password" name="password" required value="123456"/></p>

                <p><button type="submit">Submit</button></p>
                </form>
            )  

        }


    开发模式运行 nextjs-demo 项目,使用浏览器访问 http://localhost:3000/login,显示 Login 页面,点击 Submit 按钮,跳出提示对话框。