• Nextjs使用教程


    一.手动创建项目

    建议看这个中文网站文档,这个里面的案例配置都是手动的,也可以往下看我这个博客一步步操作
    1.在目录下执行下面命令,初始化package.json文件

    npm init -y
    

    2.安装react相关包以及next包

    yarn add next react react-dom
    // 或者
    npm install --save next react react-dom
    

    3.在package.json文件的script节点,新增以下内容

      "scripts": {
        ...,
        "dev": "next",  // 开发时运行
        "build": "next build", // 打包时运行
        "start": "next start"  // 打完包启动服务的命令
      }
    

    注意:Next.js 只支持React 16及以上

    4.根目录下新建pages目录(这里面就是所有页面代码了,会根据这个目录的内容自动生成路由)

    5.在pages里面新建index.js,放入以下内容进行测试

    export default function(){
        return <div>我是pages/index下面的内容</div>
    }
    

    6.启动项目测试

    npm run dev
    // 或者
    yarn run dev
    

    7.访问控制台的local地址,显示出如下页面

    在这里插入图片描述
    8.打包

    npm run build
    // 或者
    yarn run build
    

    打完包会在项目下新建生成一个.next文件,在根目录下执行如下命令会和开发时看到的效果一致

    npm run start
    // 或者
    yarn run start
    

    二.快速创建项目

    官网文档

    执行下面创建项目的命令

    npx create-next-app next-create
    

    下面会出现一堆询问的配置信息,这里直接走默认就好了

    定义路由

    这里的页面都是统一放到app文件夹下面每个文件的page.js文件

    例如直接访问localhost:3000则访问的是app/page.js文件

    例如直接访问localhost:3000/user则访问的是app/user/page.js文件

    export default function(){
        // className={"text-red-500"} 使用原子化css标签
        return <div className={"text-red-500"}>我是user/page文件</div>
    }
    

    如果访问的页面有很多网格效果,则去app/globals.css里面把样式都删除,只留前三行即可

    页面与布局

    将app/Layout.js进行改造

    import { Inter } from "next/font/google";
    import "./globals.css";
    
    const inter = Inter({ subsets: ["latin"] });
    
    export const metadata = {
      title: "Create Next App",  // 网站标题
      description: "Generated by create next app", // 描述信息
    };
    
    export default function RootLayout({ children }) {
      return (
        <html lang="en">
          <body className={inter.className}>
            app下面的layout
            {children}</body>
        </html>
      );
    }
    

    新建app/user/Layout.js存入以下内容

    export default function userLayout({ children }) {
      return (
        <section>
            user下面的layout
            {children}
        </section>
      );
    }
    

    访问localhost:3000

    在这里插入图片描述
    访问localhost:3000/user

    在这里插入图片描述
    通过对比可以发现,app下面的就是公共的根样式,下面每个Layout.js都会继承到,然后每个文件夹下都可以定义当前路由页面的样式

    链接和导航

    修改下app/page.js内容如下

    'use client';
    import Link from "next/link";
    import { useRouter } from "next/navigation";
    
    
    export default function Home() {
      const router = useRouter()
      return (
        <>
        <h1 className="text-4xl text-orange-600">Hello Name</h1>
        <br></br>
        <Link href={"/user"}>跳转到user路由</Link>
        <br></br>
        <button onClick={()=>{router.push('/user')}}>点击跳转/user</button>
        </>
      );
    }
    
    

    滚动到新路由的指定位置处,相当于锚点链接

    <Link href="/dashboard#settings">Settings</Link>
     
    // Output
    <a href="/dashboard#settings">Settings</a>
    

    路由组

    项目下新建三个路径文件

    • app/(marketing)/about/page.js
    • app/(marketing)/bolg/page.js
    • app/(marketing)/(shop)/acconut/page.js

    在每个page.js里面随便写点内容,访问以下路径

    • localhost:3000/about
    • localhost:3000/bolg
    • localhost:3000/account

    可以发现都能被访问到,总结规律就是文件夹名字带括号的相当于可以忽略了

    路由组不参与url的设定的

    个人感觉唯一作用是用于设置共同的Layout.js

    创建如下两个Layout.js文件

    • app/(marketing)/Layout.js
    • app/(marketing)/(shop)/Layout.js

    在这两个里面添加如下代码

    export default function userLayout({ children }) {
      return (
        <section>
            marketing下面的layout
            {children}
        </section>
      );
    }
    
    export default function userLayout({ children }) {
      return (
        <section>
            marketing下面shop的layout
            {children}
        </section>
      );
    }
    

    运行后会发现,marking的Layout,js被它里面所有文件所共用,shop里面的Layout.js被shop里面的文件所共用,因为这个案例shop在marking里面的,因此shop里面的文件也共用marking里面的样式,这就是路由组,按照上面传统的方式建路由,需要每个文件单独设置自己的Layout.js,使用路由组可以达到复用性

    动态路由

    在app/user新建[username]文件夹,里面的page.js文件内容如下

    export default function({params}){
        console.log('params',params);
        return <>
        <div>我是user/[username]动态路由{params.username}</div>
        </>
    }
    

    将app/page.js内容修改如下

    'use client';
    import Link from "next/link";
    import { useRouter } from "next/navigation";
    
    
    export default function Home() {
      const router = useRouter()
      return (
        <>
        <h1 className="text-4xl text-orange-600">Hello Name</h1>
        <br></br>
        <Link href={"/user/王二"}>跳转到user路由</Link>
        <br></br>
        <button onClick={()=>{router.push('/user/王五')}}>点击跳转/user</button>
        </>
      );
    }
    

    当点击跳转到路由时,后面的参数就是动态参数,文件名username就是参数名,可以被params.username接收到并显示到页面上

    上面有个弊端就是只支持一级动态参数,如果希望多级的话可以将[username]文件名换成[…username]这样就是可以匹配到后面所有参数,如下地址

    • localhost:3000/user/1/2/3/4/5

    效果图

    在这里插入图片描述
    但是这里建议将[…username]文件名替换为[[…username]],两者区别在于[[…username]]当动态参数为空时也会被匹配到,剩余部分两者功能一致

    Loadding加载和流的处理

    1.在app下面新建Loading.js组件

    export default function(){
        return <div className={"text-2xl text-pink-400"}>Loading...</div>
    }
    

    2.修改app/Layout.js

    import { Inter } from "next/font/google";
    import { Suspense } from "react";
    import Loading from './loading';  // 引入app/loading.js
    import "./globals.css";
    
    const inter = Inter({ subsets: ["latin"] });
    
    export const metadata = {
      title: "Create Next App",  // 网站标题
      description: "Generated by create next app", // 描述信息
    };
    
    export default function RootLayout({ children }) {
      return (
        <html lang="en">
          <body className={inter.className}>
            <Suspense fallback={<Loading></Loading>}> // 2.使用SusPense将页面包裹
            	app下面的layout
            	{children}
            </Suspense>
            </body>
        </html>
      );
    }
    

    子组件代码

    这里使用了async/await模拟了一下异步,这是个细节,因为上面的loadding效果如果要出来的话,页面数据必须要是有异步效果,因为我没注意到这点,费了点时间才搞明白

    export default async function Posts() {
      await new Promise((resolve) => setTimeout(resolve, 2000));
      return <div>1111</div>;
    }
    

    注意:将Lodding放到app/Layout.js里面包裹的话,则针对所有页面生效,如果某个页面有不一样的loading效果的话,则需要在当前文件夹里面的Layout.js去单独引入对应的Loading.js,可以在当前文件夹里面创建个Loading.js,这样的话Loading.js的样式仅仅作用于当前文件夹下的所有页面

    import { Suspense } from "react";
    export default function userLayout({ children }) {
      return (
        <section>
            // 这个loading效果仅作用于当前文件夹下面的所有页面
            <Suspense fallback={<div className={"text-2xl text-pink-400"}>Loading...</div>}>
            user下面的layout
            {children}
          </Suspense>
        </section>
      );
    }
    

    注意:必须是渲染的页面内有异步操作(如async/await)才会有Loading.js效果

    错误处理

    新建app/error.js,放入以下内容

    'use client'
    
    export default function({error,reset}){
        return (
            <div>
                <h2>我是全局的错误样式处理</h2>
                <button onClick={()=>reset()}>重试一下</button>
            </div>
        )
    }
    

    也可以对每个页面单独定义路由样式,只需要在目标页面的文件夹内新建error.js,放入以下内容即可

    例如我在app/user/error.js内加入以下内容

    'use client'
    
    export default function({error,reset}){
        return (
            <div>
                <h2>app/user 页面内有错误啦!!!</h2>
                <button onClick={()=>reset()}>重试一下</button>
            </div>
        )
    }
    

    例如我们在目标的user页面加入一些错误信息

    export default function Posts() {
      console.log('a',a);  // 这里没有a变量,因此这里会报错
      return <div>1111</div>;
    }
    

    当我们在浏览器访问localhost:3000/user就会报出以下错误

    在这里插入图片描述
    当我们访问其他页面有错误信息时,但是没有给那个页面单独定义错误样式,则会触发全局的错误样式

    例如访问: localhost:3000/about

    在这里插入图片描述

    组件化渲染

    并行路线,也就是web端的组件

    在app下面新建@home和@setting文件夹,里面都新建一个page.js文件,在里面写一点页面

    在app/Layout.js里面改为如下页面代码

    import { Inter } from "next/font/google";
    import { Suspense } from "react";
    import Loading from './loading';
    import "./globals.css";
    
    const inter = Inter({ subsets: ["latin"] });
    
    export const metadata = {
      title: "Create Next App",  // 网站标题
      description: "Generated by create next app", // 描述信息
    };
    
    export default function RootLayout({ children,home,setting }) {
      return (
        <html lang="en">
          <body className={inter.className}>
            <Suspense fallback={<Loading></Loading>}>
            app下面的layout
            {home}
            {children}
            {setting}
            </Suspense>
            </body>
        </html>
      );
    }
    
    

    在这里插入图片描述

    可以发现组件效果已经出来了

    但是上面的仅限于在Layout.js里面使用的组件,下面是可以应用到我们页面里面的组件的案例

    在app平级处创建components/frame/index.js,放入以下内容

    import Image from "next/image";
    
    export default function({photo}){
        console.log('photo',photo);
        return <>
        <Image src={photo.src} alt="" width={600} height={600} className={'w-full object-cover aspect-square col-span-2 w-28'}></Image>
        </>
    }
    

    在app里面新建photo/page.js文件,插入以下内容

    import Photo from "@/components/frame"  // 引入组件
    export default function(){
        const photo = {src:'https://take-saas.oss-cn-hangzhou.aliyuncs.com/wechat_applets/coach/bgcimg/bgc-13.png'}
        // 给组件传值
        return <Photo photo={photo}></Photo>
    }
    

    注意:这里可能会报错图片问题(网络图片需要加一下白名单才能正常加载,如下在next.config.mjs里面进行配置)

    /** @type {import('next').NextConfig} */
    const nextConfig = {
        images:{
            domains:['take-saas.oss-cn-hangzhou.aliyuncs.com'] // 这里是存放域名白名单处
        }
    };
    
    export default nextConfig;
    
    

    然后就可以看到图片正常加载了

    在这里插入图片描述

    定义404页面

    在app下面新建not-found.js,放入以下内容

    export default function(){
        return <div className={"text-2xl text-pink-400"}>访问页面不存在...</div>
    }
    

    当页面访问一个不存在的页面路由时,页面显示效果如下

    在这里插入图片描述
    PS:由于博客内容都是自学做的整理,在某一次排查问题时,也就是刚好博客写到这里时,刷到了一个博主的nextjs教程合集,也挺详细的,力推点击进入合集地址

    三.重新创建带src文件目录的项目

    跟刚刚步骤一样,只是选择配置的时候选择使用src目录

    路由

    路由跳转和传参,在src/pages/home/index.js加入以下代码

    import Link from "next/link";
    import Router from "next/router";
    
    function Home({ items, time }) {
      return (
        <div className="Home">
         <Link href={'/home/xiaoji'}>跳转到xiaoji页面</Link>
         <br></br>
         <Link href={'home/xiaoji?name=萧寂'}>跳转到xiaoji页面(带参数)</Link>
         <hr></hr>
         <button onClick={()=>{Router.push('/home/xiaoji')}}>跳转到xiaoji页面</button>
         <br></br>
         <button onClick={()=>{Router.push('/home/xiaoji?name=萧寂')}}>跳转到xiaoji页面(第一种带参数)</button>
         <button onClick={()=>{Router.push({
          pathname:'/home/xiaoji',
          query:{
            name:'萧寂'
          }
         })}}>跳转到xiaoji页面(第二种带参数)</button>
        </div>
      );
    }
    export default Home;
    

    接收参数,在src/pages/home/xiaoji.js加入以下代码

    import { withRouter } from "next/router";
    
    function xiaoji({ router }) {
      return (
        <>
          <div>{router.query.name}</div>
        </>
      );
    }
    
    // 需要withRouter包裹组件可以取到参数信息
    export default withRouter(xiaoji)
    
    

    路由里面的六个钩子事件

    将上面的src/pages/home/index.js代码更改如下,然后查看浏览器控制台的打印语句

    import Link from "next/link";
    import Router from "next/router";
    
    function Home({ items, time }) {
      Router.events.on('routeChangeStart',(args)=>{
        console.log("1.routeChangeStart->路由开始变化,参数为:",args);
      })
      Router.events.on('beforeHistoryChange',(args)=>{
        console.log("2.beforeHistoryChange->浏览器的历史记录状态发生变化(常用于询问用户是否保存页面内容),参数为:",args);
      })
      Router.events.on('routeChangeComplete',(args)=>{
        console.log("3.routeChangeComplete->路由变化结束,参数为:",args);
      })
      Router.events.on('routeChangeError',()=>{
        console.log("4.routeChangeError->路由发生错误,404不算错误");
      })
      Router.events.on('hashChangeStart',(args)=>{
        console.log("5.hashChangeStart->hash值开始发生变化,参数为:",args);
      })
      Router.events.on('hashChangeComplete',(args)=>{
        console.log("6.hashChangeComplete->hash值变化完成,参数为:",args);
      })
    
    
      return (
        <div className="Home">
         <Link href={'/home/xiaoji'}>跳转到xiaoji页面</Link>
         <br></br>
         <Link href={'home/xiaoji?name=萧寂'}>跳转到xiaoji页面(带参数)</Link>
         <hr></hr>
         <button onClick={()=>{Router.push('/home/xiaoji')}}>跳转到xiaoji页面</button>
         <br></br>
         <button onClick={()=>{Router.push('/home/xiaoji?name=萧寂')}}>跳转到xiaoji页面(第一种带参数)</button>
         <br></br>
         <button onClick={()=>{Router.push({
          pathname:'/home/xiaoji',
          query:{
            name:'萧寂'
          }
         })}}>跳转到xiaoji页面(第二种带参数)</button>
         <br></br>
         <Link href={"#xiaoji"}>模拟hash值发生变化</Link>
        </div>
      );
    }
    export default Home;
    

    网络请求发送

    getStaticProps 和 getServerSideProps

    getStaticProps 和 getServerSideProps 是 Next.js 中用于数据获取的两个重要函数,它们在页面渲染时分别起到不同的作用。

    1、getStaticProps:

    用途:主要用于静态生成页面,即在构建时获取数据并将其预先注入到页面中。
    执行环境:只在构建时运行,不会在每次请求时重新运行。
    数据获取:可以在构建时从外部数据源获取数据,并将数据作为 props 传递给页面组件。
    适用情况:适用于不频繁变化的静态内容,如博客文章、产品页面等。
    2、getServerSideProps:

    用途:用于服务器端渲染页面,即在每次请求时动态获取数据并注入到页面中。
    执行环境:在每次请求时在服务器端运行,可以根据每次请求动态获取数据。
    数据获取:可以在每次请求时从外部数据源获取数据,并将数据作为 props 传递给页面组件。
    适用情况:适用于需要在每次请求时动态获取数据的情况,如用户特定数据、实时数据等。

    总的来说,getStaticProps 用于静态生成页面,适用于不频繁变化的内容,而 getServerSideProps 用于服务器端渲染页面,适用于需要每次请求时动态获取数据的情况。根据页面的需求和数据更新频率,您可以选择合适的函数来获取数据并注入到页面中。

    getStaticProps

    在src/pages/home/index.js加入以下内容

    function Home({ items, time }) {
      return (
        <div className="Home">
           <p>timer: {time}</p>
          {items.map((item) => {
            return (
              <h4 style={{ margin: "15px 0" }} key={item.qa_id}>
                {item.name}
              </h4>
            );
          })}
        </div>
      );
    }
    
    export async function getStaticProps() {
      // 在这里调用外部接口,可以写各种服务端代码:连数据库,访问文件系统.....
      const resp = await fetch("https://fts.jd.com/area/get?fid=-1");
      const items = await resp.json();
      console.log('items',items);
      // 此函数必须返回 { props: { 需要携带的数据 } } 对象,Home 组件在构建时将接收到 `items` 参数。
      return {
        props: {
          items,
          time: Date.now(), // 为了方便对比yarn dev和yarn build的区别,加入时间戳
        },
      };
    }
    
    export default Home;
    

    浏览器访问:http://localhost:3000/home
    效果图

    在这里插入图片描述

    getServerSideProps

    把上面的代码替换成下面的,然后还是访问相同的路由地址

    function Home({ items, time }) {
      return (
        <div className="Home">
           <p>timer: {time}</p>
          {items.map((item) => {
            return (
              <h4 style={{ margin: "15px 0" }} key={item.qa_id}>
                {item.name}
              </h4>
            );
          })}
        </div>
      );
    }
    
    export async function getServerSideProps(context) {
      // 在这里调用外部接口,可以写各种服务端代码:连数据库,访问文件系统.....
      const resp = await fetch("https://fts.jd.com/area/get?fid=-1");
      const items = await resp.json();
      // 此函数必须返回 { props: { 需要携带的数据 } } 对象,Home 组件在构建时将接收到 `items` 参数。
      return {
        props: {
          items,
          time: Date.now(), // 为了方便对比yarn dev和yarn build的区别,加入时间戳
        },
      };
    }
    
    export default Home;
    

    这里会发现其实效果是一样的,但是我在props里面返回了个时间戳,我们先运行yarn run build先打包,然后运行yarn run start执行打包后的文件再访问上面的路由地址,观察页面上的时间戳变换,可以发现getStaticProps 方法打包出来的时间戳一直是固定了的,也就是这上面的数据只在初始化就渲染一次,后续不会执行,适合作为静态页面数据获取使用,而getServerSideProps方法打包出来的运行后,时间戳是一直变化的,也就是每次都会从服务器获取数据并渲染

    pages页面路由动态获取

    在src/pages/home/detial.js加入以下内容

    function Detail({ id }) {
        // 获取文章详情信息数据渲染到组件中。
        return <div className="Detail">{id}</div>;
      }
      
      export default Detail;
      
      export async function getStaticPaths() {
        const paths = [
          { params: { id: '1' } },
          { params: { id: '2' } },
        ];
        return { paths, fallback: false };
      }
      
      
      export async function getStaticProps({ params }) {
        const id = params.id;
        // 根据 id 从外部数据源获取数据
        return { props: { id } };
      }
    

    浏览器访问:http://localhost:3000/home/detial/1
    或者
    http://localhost:3000/home/detial/2
    都能进行访问,除此之外都是404,这样就是给动态参数加了限制,限制了哪些参数允许访问此页面,例如做文章详情之类的,需要传入文章id,如果id不匹配则返回404这种,可以根据后端数据内容动态渲染paths里面的id值,代码如下:

    function Detail({ detailData }) {
      // 获取文章详情信息数据渲染到组件中。
      return <div className="Detail">{JSON.stringify(detailData)}</div>;
    }
    
    export default Detail;
    
    export async function getStaticPaths() {
      // 调用api,获取文章列表
      const resp = await fetch("url");
      const items = await resp.json();
      // 遍历文章列表,获取文章id,根据文章id生成所有需要预渲染的静态页面路径
      const paths = items.map((item) => ({
        params: { id: `${item.qa_id}` }, // id的值必须是字符串类型,因为动态路径参数也是字符串类型,要保持一致
      }));
      // { fallback: false } 不在上述预渲染列表范围内的路径id,显示404页面。
      // 改为true就不会受到限制,默认为false
      return { paths, fallback: false };
    }
    
    export async function getStaticProps({ params }) {
    // 根据id进行动态请求数据
      const resp = await fetch(
        `url/${params.id}`
      );
      const detailData = await resp.json();
      return {
        props: {
          detailData,
        },
      };
    }
    

    getStaticPaths 函数说明

    • 仅在构建时运行。它非常适合预渲染需要在构建时使用动态数据的类似路径(如/blog/:id)。当与 getStaticProps 结合使用时,它会从 数据库/接口 等数据结构中生成动态页面,然后 Next 可以对其进行静态服务
    • getStaticPaths 只能和 getStaticProps 配合使用,不能和 getServerSideProps 一起使用

    如果fallback参数为true,则在构建时没有生成的路径不会导致404页面,可以先暂时为用户提供一个临时页面,同时 next 会去后台会运行 getStaticProps 函数,对构建时没有生成的路径页面进行动态预渲染。可以将上面的页面进行如下改造

    import { useRouter } from "next/router";
    
    function Detail({ id }) {
        // 获取文章详情信息数据渲染到组件中。
        const router = useRouter();
        if (router.isFallback) {
          return <div>正在请求,请稍后...</div>; // 临时页面,给用户友好提示
        }
        return <div className="Detail">{id}</div>;
      }
      
      export default Detail;
      
      export async function getStaticPaths() {
        const paths = [
          { params: { id: '1' } },
          { params: { id: '2' } },
        ];
        return { paths, fallback: true };
      }
      
      
      export async function getStaticProps({ params }) {
        const id = params.id;
        // 根据 id 从外部数据源获取数据
        return { props: { id } };
      }  
    

    组件

    在src下面新建components文件夹,新建demo.js文件

    export default function ({children}){
        return <div>{children}</div>
    }
    

    在pages/home/xiaoji.js里面使用

    import Demo from "@/components/demo";
    
    export default function (){
        return <Demo><button>我是萧寂</button></Demo>
    }
    

    效果图

    在这里插入图片描述

    项目结构如下
    在这里插入图片描述

    页面css编写

    行内样式

    function Home({ items, time }) {
      return (
        <>
          <div>我是萧寂</div>
          <div className="Home">我是萧寂</div>
          <style jsx>
            {
              `
              div{color:blue;}
              .Home{color:red;}
              `
            }
          </style>
        </>
      );
    }
    
    export default Home;
    

    效果图

    在这里插入图片描述

    外部样式

    在src新建static文件夹再新建index.css文件,存入以下内容

    div{
        color: blue;
    }
    .Home{
        color: red;
    }
    
    import '../../static/index.css'
    function Home({ items, time }) {
      return (
        <>
          <div>我是萧寂</div>
          <div className="Home">我是萧寂</div>
        </>
      );
    }
    
    export default Home;
    

    效果和上面相同

    自定义页面的Header头,用于优化SEO

    单个页面自定义(建议将Head单独封装成组件,在每个页面引入使用)

    import { parseSetCookie } from "next/dist/compiled/@edge-runtime/cookies";
    import Head from "next/head";
    function Home({ items, time }) {
      return (
        <>
        <Head>
          <title>萧寂牛逼</title>
          <meta charSet="utf-8"></meta>
        </Head>
          <div>我是萧寂</div>
        </>
      );
    }
    
    export default Home;
    
  • 相关阅读:
    Leetcode.19 删链表倒数第 N 个结点(栈/先后指针)
    HTML List label Ordered list Unodered list Definition list
    Vercel部署个人静态之DNS污染劫持问题
    在win系统安装部署svn服务及客户端使用
    优秀的golang开源框架
    Go runtime 调度器精讲(十一):总览全局
    JSTL标准标签库 EL表达式
    【微信小程序】解决canvas组件层级最高问题
    Python面向对象编程【进阶】
    【技术积累】《MongoDB实战》笔记(1)
  • 原文地址:https://blog.csdn.net/weixin_68658847/article/details/139392596