关于react.js:手把手带你入门-NextJsv95

45次阅读

共计 13200 个字符,预计需要花费 33 分钟才能阅读完成。

前言

Next.js 之前用过一次,这次是从新做个小回顾,当初最新版本曾经到了 9.5.3,有些 API 也同以前有点不同了,网上大部分教程也都是旧版本 v7 的比拟多,故打算写下简略的教程,相对具体的带你入个小门。

库版本

本文案例用的要害库版本如下:

"next": "^9.5.2",
"react": "^16.13.1",

node 版本为 12.18.1(node 版本 >= 10.13 即可)

初始化 Next.js 我的项目

  1. 新建一个文件夹

learn-nextjs-example,先进行初始化

npm i -y
  1. 装置所须要的依赖包
npm i react react-dom next --save
  1. 增加 script 命令

为了是开发输出 npm 命令更快捷,所以把罕用的命令作为快捷命令设置

"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
},
  1. 创立 pages 文件夹,运行第一个页面

pages 文件夹是搁置页面的,这里边的文件会主动生成相应路由。

  • 比方创立 pages/about.js,那么拜访该页面地址就是 http://localhost:3000/about
  • 比方创立 pages/about/about.js,那么拜访地址为http://localhost:3000/about/about

当初咱们创立 pages/index.js,index.js 就是默认代表根门路了

const Home = () => {return <div>Hello Next.js!</div>}

export default Home

此时运行

npm rum dev

关上http://localhost:3000/,能够看到页面胜利运行了,也能够晓得 Next.js 内置 React,因而不必咱们再引入
import React from 'react',就能够间接应用 React 的语法,当然你写了也不会报错,不过没必要。

路由跳转

页面跳转有两种模式,一种是利用标签Link,一种是编程式路由跳转router.push('/')。路由跳转形式还是跟 React 很像的,只不过用的是 next 的包

Link

  1. 先创立两个页面

pages/about.js

const About = () => {return <div>About Page</div>}

export default About

pages/news.js

const News = () => {return <div>News Page</div>}

export default News
  1. 在首页 pages/index.js 编写路由跳转链接
import Link from 'next/link'

const Home = () => {
  return (
    <div>
      <div>Hello Next.js!</div>
      <div>
        <Link href='/about'>
          <a> 对于 </a>
        </Link>
      </div>
      <div>
        <Link href='/news'>
          <a> 新闻 </a>
        </Link>
      </div>
    </div>
  )
}
export default Home

在这里要留神的是 a 标签不必增加 href 元素,增加了也不起作用。
如果 Link 外面换成其余标签如 span,也仍然能胜利跳转到对应页面,能够晓得Link 是给外面的元素增加了点击绑定事件,而不是只会给 a 标签增加 href 而已。

此时 DOM 构造:

还有要留神的是:Link 外面只能有一个根元素,要不然它不知要绑定谁会报错。

编程式路由跳转

这里次要用 useRouter 钩子进行跳转

批改pages/index.js,批改其中一个路由

import Link from 'next/link'
import {useRouter} from 'next/router'

const Home = () => {const router = useRouter()
  const gotoAbout = () => {router.push('/news')
  }
  return (
    <div>
      <div>Hello Next.js!</div>
      <div>
        <Link href='/about'>
          <a> 对于 </a>
        </Link>
      </div>
      <div>
        <button onClick={gotoAbout}> 新闻 </button>
      </div>
    </div>
  )
}
export default Home

路由传参

query 模式传参

在 Next.js 中只能通过 query(?name=jackylin)来传递参数,不能通过 (path:name) 的模式传递参数。
批改pages/index.js

import Link from 'next/link'
import {useRouter} from 'next/router'

const Home = () => {const router = useRouter()
  const gotoAbout = () => {router.push('/news')
  }
  return (
    <div>
      <div>Hello Next.js!</div>
      <div>
        <Link href='/about?name=jackylin'>
          <a> 对于 </a>
        </Link>
      </div>
      <div>
        <button onClick={gotoAbout}> 新闻 </button>
      </div>
    </div>
  )
}
export default Home

也能够变换成这种形式传

<Link href={{pathname: '/about', query: { name: 'jackylin'} }}>

传递了参数 name 后,接下来是接管参数。

关上 pages/about.js 增加代码

import {withRouter} from 'next/router'

const About = ({router}) => {
  return (
    <div>
      <div>About Page</div>
      <div> 接管到参数为:{router.query.name}</div>
    </div>
  )
}

export default withRouter(About)

点击 对于,跳转到 about 页面,页面内容为:

About Page
接管到参数为:jackylin

同时也可看到地址栏带有参数:http://localhost:3000/about?name=jackylin

编程式路由跳转传递参数

批改 pages/index.js 中的 gotoAbout办法

const gotoAbout = () => {
  router.push({
    pathname: '/news',
    query: {info: '学习 Next.js',},
  })
}

pages/news.js

import {withRouter} from 'next/router'

const News = ({router}) => {
  return (
    <div>
      <div>News Page</div>
      <div> 接管到参数为:{router.query.info}</div>
    </div>
  )
}

export default withRouter(News)

路由变动的钩子

这里就不一个个细细开展了,间接代码谈话:

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

const Home = () => {const router = useRouter()
  const gotoAbout = () => {
    router.push({
      pathname: '/news',
      query: {info: '学习 Next.js',},
    })
  }

  useEffect(() => {
    const handleRouteChangeStart = url => {console.log('routeChangeStart 路由开始变动,url:', url)
    }

    const handleRouteChangeComplete = url => {console.log('routeChangeComplete 路由完结变动,url:', url)
    }

    const handleBeforeHistoryChange = url => {console.log('beforeHistoryChange 在扭转浏览器 history 之前触发,url:', url)
    }

    const handleRouteChangeError = (err, url) => {console.log(`routeChangeError 跳转产生谬误: ${err}, url:${url}`)
    }

    const handleHashChangeStart = url => {console.log('hashChangeStart hash 路由模式跳转开始时执行,url:', url)
    }

    const handleHashChangeComplete = url => {console.log('hashChangeComplete hash 路由模式跳转实现时,url:', url)
    }

    router.events.on('routeChangeStart', handleRouteChangeStart)
    router.events.on('routeChangeComplete', handleRouteChangeComplete)
    router.events.on('beforeHistoryChange', handleBeforeHistoryChange)
    router.events.on('routeChangeError', handleRouteChangeError)
    router.events.on('hashChangeStart', handleHashChangeStart)
    router.events.on('hashChangeComplete', handleHashChangeComplete)

    return () => {router.events.off('routeChangeStart', handleRouteChangeStart)
      router.events.off('routeChangeComplete', handleRouteChangeComplete)
      router.events.off('beforeHistoryChange', handleBeforeHistoryChange)
      router.events.off('routeChangeError', handleRouteChangeError)
      router.events.off('hashChangeStart', handleHashChangeStart)
      router.events.off('hashChangeComplete', handleHashChangeComplete)
    }
  }, [])

  return (
    <div>
      <div>Hello Next.js!</div>
      <div>
        <Link href='/about?name=jackylin'>
          <a> 对于 </a>
        </Link>
      </div>
      <div>
        <button onClick={gotoAbout}> 新闻 </button>
      </div>
    </div>
  )
}
export default Home

style-jsx 编写页面 CSS 款式

咱们建设一个头部组件再引入pages/index.js,在根目录下新建components/header.js

const Header = () => {
  return (
    <div>
      <div className='header-bar'>Header</div>
    </div>
  )
}

export default Header

pages/index.js引入 Header 组件

import Header from '../components/header'

const Home = () => {
  return (
    <div>
      <Header />
    </div>
  )
}

接着给 Header 写款式,Next.js 内置反对 style jsx 语法,这是其中一种 CSS-in-JS 的解决方案

const Header = () => {
  return (
    <div>
      <div className='header-bar'>Header</div>
      <style jsx>
        {`
          .header-bar {
            width: 100%;
            height: 50px;
            line-height: 50px;
            background: lightblue;
            text-align: center;
          }
        `}
      </style>
    </div>
  )
}

export default Header

运行款式失效

查看 DOM 构造,会发现 Next.js 会主动退出一个随机类名(jsx-xxxxxxx),这样就能避免 CSS 的全局净化。

增加全局 CSS 文件

新建文件夹 public,用于搁置动态文件如图片和 css 文件等,留神只有名为 public 的目录可能寄存动态资源并对外提供拜访。新建文件public/static/styles/common.css

body {
  margin: 0;
  padding: 0;
  font-size: 16px;
}
  • 自定义 App

Next.js 应用 App 组件来初始化页面,咱们能够笼罩 App 组件来管制页面的初始化。该 App 作用个别如下:

  • 在页面切换之间放弃布局的长久化
  • 切换页面时放弃状态
  • 应用 componentDidCatch 自定义错误处理
  • 向页面注入额定的数据
  • 增加全局 CSS

咱们要做的就是增加全局 CSS 款式,所以须要笼罩原来默认的App,首先要创立pages/_app.js

import '../public/static/styles/common.css'

/**
 * Component 指以后页面,每次路由切换时,Component 都会更新
 * pageProps 是带有初始属性的对象,该初始属性由咱们的某个数据获取办法事后加载到你的页面中,否则它将是一个空对象
 */
export default function MyApp({Component, pageProps}) {return <Component {...pageProps} />
}

重启服务,运行,全局款式增加胜利。

集成 style-components

如果想要我的项目反对 style-components 的话,须要咱们本人去配置。咱们通过自定义 Document 的形式来改写代码,即是指 _document.js 文件,它只有在服务器端渲染的时候才会被调用,次要用来批改服务器端渲染的文档内容,个别用来配合第三方 css-in-js 计划应用。

  • 装置所需库
npm i styled-components --save

编译 styled-components

npm i babel-plugin-styled-components --save-dev

新建文件.babelrc

{"presets": ["next/babel"],
  "plugins": [["styled-components", { "ssr": true}]]
}

新建 _document.js,改写笼罩它原来的写法。这里要留神的是,要肯定要继承 Document,才来改写

import Document from 'next/document'
import {ServerStyleSheet} from 'styled-components'

export default class MyDocument extends Document {static async getInitialProps(ctx) {const sheet = new ServerStyleSheet()
    const originalRenderPage = ctx.renderPage
    try {ctx.renderPage = () =>
        originalRenderPage({enhanceApp: App => props => sheet.collectStyles(<App {...props} />),
        })
      const initialProps = await Document.getInitialProps(ctx)

      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        ),
      }
    } finally {sheet.seal()
    }
  }
}

这其中具体配置意思能够去官网文档找点线索理解:Next.js

接下来咱们应用 styled-components 的形式增加款式,关上pages/about.js

import {withRouter} from 'next/router'
import styled from 'styled-components'

const Title = styled.h1`
  color: green;
`

const About = ({router}) => {
  return (
    <div>
      <Title>About Page</Title>
      <div> 接管到参数为:{router.query.name}</div>
    </div>
  )
}

export default withRouter(About)

运行,如下图:

获取数据的形式

四个 api 都只能在 pages 文件夹内的文件中应用

getStaticProps

当页面内容取决于内部数据

Next.js 举荐咱们尽可能应用动态生成的形式,因为所有页面都能够只构建一次并托管到 CDN 上,这比让服务器依据每个页面申请来渲染页面快得多。

比方下列一些类型的页面:

  • 营销页面
  • 集体博客
  • 产品列表
  • 动态文档

如果一个页面应用了 动态生成(SSG),在构建时将生成此页面对应的 HTML 文件。HTML 文件将在每个页面申请时被重用,还能够被 CDN 缓存。这个跟 hexo,Gatsby 相似的,生成动态文件还有 json 文件。

对于 Gatsby,它是基于 React 的下层框架,具体能够看我这篇博客理解:手把手带你入门 Gatsby

Next.js 会尽可能地主动优化利用并输入动态 HTML,如果你在你的页面组件增加了 getInitialProps 办法才会禁用主动动态优化,getInitialProps办法上面会说到。

比方咱们能够执行命令查看

npm run build

打包默认是 SSG 模式:

● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)

会发现 .next/server/pages/ 下多出了几个 HTML 文件,这些就是动态构建出的 HTML 文件。

新建文件pages/blog.js

const Blog = ({posts}) => {return <div>title: {posts.title}</div>
}

// 此函数在构建时被调用
export async function getStaticProps() {
  // 调用内部 API 获取内容
  const res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
  const posts = await res.json()

  // 在构建时将接管到 `posts` 参数
  return {
    props: {posts,},
  }
}

export default Blog

进入http://localhost:3000/blog,发现页面能失常获取到申请后果:

title: delectus aut autem

getStaticPaths

当页面的门路取决于内部数据

这种形式和下面就有些不同了,下面 http://localhost:3000/blog 拜访门路时咱们曾经写了一个 js 页面文件,但如果咱们想要依据获取的数据动静生成多篇文章门路的话,那就须要用到这个 API,通常用这个 API 会同时配合 getStaticProps 一起用。

新建文件pages/posts/[id].js,你没有看错,文件名就是[id].js

id 标识单篇博文,比方你拜访 http://localhost:3000/posts/1 就展现 id 为 1 的文章。

代码如下:

const Post = ({post}) => {
  return (
    <div>
      <div> 文章 id: {post.id}</div>
      <div> 博客题目: {post.title}</div>
    </div>
  )
}

// 构建路由
export async function getStaticPaths() {
  // 调用内部 API 获取博文列表
  const res = await fetch('https://jsonplaceholder.typicode.com/todos')
  const posts = await res.json()

  // 依据博文列表生成所有须要预渲染的门路
  const paths = posts.map(post => `/posts/${post.id}`)

  // fallback 为 false,示意任何不在 getStaticPaths 的门路的后果将是 404 页面。return {paths, fallback: false}
}

// 获取单个页面博文数据
export async function getStaticProps({params}) {
  // 如果路由是 /posts/1,那么 params.id 就是 1
  const res = await fetch(`https://jsonplaceholder.typicode.com/todos/${params.id}`)
  const post = await res.json()

  // 通过 props 参数向页面传递博文的数据
  return {props: { post} }
}

export default Post

拉取下来的数据全副有 200 条,也就是说动静构建了 200 个路由,当初咱们拜访第 50 篇博文的话,间接输出http://localhost:3000/posts/50

页面显示:

文章 id: 50
博文题目: cupiditate necessitatibus ullam aut quis dolor voluptate

接下来让咱们再次打包,执行npm run build(如果打包产生谬误很可能是网络差和申请链接无关,因为要申请很多页面,如果是网络问题多试几次就行),你会发现这次打包工夫变长了,因为要构建的动态页面多了,咱们申请返回来的数据有 200 条,所以应该生成 200 个动态 HTML 文件和相应的 json 数据文件。

咱们来看 1.json 文件,就是申请后的数据

{"pageProps":{"post":{"userId":1,"id":1,"title":"delectus aut autem","completed":false}},"__N_SSG":true}

getServerSideProps

每次页面申请时从新生成页面的 HTML

如果无奈在用户申请之前预渲染页面,那下面的 ” 动态生成 ” 就不太实用了,或者是页面数据须要频繁的更新,并且页面内容会随着每个申请而变动。这个时候,有两种计划:

  • 将“动态生成”与 客户端渲染 一起应用:你能够跳过页面某些局部的预渲染,而后应用客户端 JavaScript 来填充它们
  • 应用 服务器端渲染:Next.js 针对每个页面的申请进行预渲染。因为 CDN 无奈缓存该页面,因而速度会较慢,然而预渲染的页面将始终是最新的。

因为服务器端渲染会导致性能比“动态生成”慢,因而仅在相对必要时才应用此性能。

新建文件pages/server-blog.js

const Blog = ({posts}) => {return <div>title: {posts.title}</div>
}

// 在每次页面申请时都会运行,而在构建时不运行。// 要设置某个页面应用服务器端渲染,就须要导出 getServerSideProps 函数
export async function getServerSideProps() {
  // 调用内部 API 获取内容
  const res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
  const posts = await res.json()

  // 在构建时将接管到 `posts` 参数
  return {
    props: {posts,},
  }
}

export default Blog

运行npm run build,发现这次并没有生成对应的 html 文件——server-blog.html,因为咱们指定了服务端渲染,申请构建时不会执行,不是动态生成。

getInitialProps

getInitialProps 是在渲染页面之前就会运行的 API。如果该门路下蕴含该申请,则执行该申请,并将所需的数据作为 props 传递给页面。当第一次拜访间接页面,getInitialProps 就在服务器端运行,加载结束后页面给客户端托管,应用客户端的路由跳转,之后页面的 getInitialProps 就在客户端执行了。

举荐:getStaticProps 或 getServerSideProps。如果你应用的是 Next.js 9.3 或更高版本,咱们倡议你应用 getStaticProps 或 getServerSideProps 来代替 getInitialProps。这些新的获取数据的办法使你能够在动态生成(static generation)和服务器端渲染(server-side rendering)之间进行精密管制。

新建文件pages/initial-blog.js

import Link from 'next/link'

const InitialBlog = ({post}) => {
  return (
    <div>
      <h1>getInitialProps Demo Page</h1>
      <div> 获取到的 title: {post.title}</div>
      <Link href='/'>
        <a> 返回首页 </a>
      </Link>
    </div>
  )
}

InitialBlog.getInitialProps = async ctx => {const res = await fetch('https://jsonplaceholder.typicode.com/todos/1')
  const post = await res.json()
  return {post}
}

export default InitialBlog

再批改 pages/index.js 代码,减少一个 Link 标签能够跳转回该页面

<div>
  <Link href='/initial-blog'>
    <a> 进入 Initial Blog</a>
  </Link>
</div>

接着间接地址栏输出 http://localhost:3000/initial-blog,在渲染该页面内容前,先会执行getInitialProps 办法,执行完的后果再传到页面组件,开始渲染页面。因为咱们是第一次拜访页面,首屏则为服务端渲染。

页面内容显示为:

getInitialProps Demo Page
获取到的 title: delectus aut autem

关上浏览器的 Network 面板,找到 Name 值为 initial-blog 的申请,会看到申请响应内容的正是该页面的 HTML。

点击 返回首页 ,再从首页点击 进入 Initial Blog,从新进入该页面,留神的是此时曾经不是咱们浏览器通过地址栏拜访该页面,而是通过前端路由进入该页面。

查看 Network 面板的申请,会看到此时返回的是 getInitialProps 申请回来的数据,而不是 HTML 页面内容,由此可知此时页面曾经交由客户端渲染。这就是所谓 SSR 渲染,首屏交给服务端渲染返回,返回后页面跳转就是客户端渲染了。

自定义 Head

之所以会抉择 Next.js,置信一个十分重要的起因就是有利用 SEO,那么利于爬虫检索的形式,一种就是在页面写好 TDK(title、description、keyword),Next.js 能够自定义 Head 标签

批改components/header.js,在最外层 div 根元素里内增加代码

<Head>
  <title>Next.js 教程 -- JackyLin</title>
  <meta name='viewport' content='initial-scale=1.0, width=device-width' />
</Head>

关上http://localhost:3000/,就会看到页面的头部标签 title 曾经被咱们改了。

LazyLoding 实现模块 / 组件懒加载

懒加载是指须要用到或该加载到某个组件 / 模块的时候,才加载那个组件 / 模块的 js 文件,也叫异步加载。

如果页面文件内容多过大,那么可能呈现首次关上速度慢,这时就须要进行优化了,懒加载就是其中一种形式。

懒加载模块

先装置一个解决日期工夫的库

npm i moment --save

新建pages/time.js

import {useState} from 'react'
import moment from 'moment'

const Time = () => {const getTime = () => {setNowTime(moment().format('YYYY-MM-DD HH:mm:ss'))
  }
  const [nowTime, setNowTime] = useState('')
  return (
    <div>
      <button onClick={getTime}> 获取以后工夫 </button>
      <div> 以后工夫:{nowTime}</div>
    </div>
  )
}

export default Time

假如咱们这个页面内容很多,而后你进入该页面可能不须要点击获取工夫,但 moment 模块仍然加载了,这就有点资源节约。

于是,咱们想当咱们点击按钮的时候,再来加载 moment 模块,即是实现异步加载,而不是页面一进入就加载。

此时点击按钮获取工夫,可看到 Network 面板看到 moment 的代码被打包成了一个 1.js 文件,这样就实现了懒加载了,这样能够缩小了次要 JavaScript 包的大小, 带来了更快的加载速度。

import {useState} from 'react'

const Time = () => {const getTime = async () => {const moment = await import('moment')
    setNowTime(moment.default().format('YYYY-MM-DD HH:mm:ss')) // 留神这里应用 default,不能间接用 moment()}
  const getTime = () => {setNowTime(moment().format('YYYY-MM-DD HH:mm:ss'))
  }
  const [nowTime, setNowTime] = useState('')
  return (
    <div>
      <button onClick={getTime}> 获取以后工夫 </button>
      <div> 以后工夫:{nowTime}</div>
    </div>
  )
}

export default Time

懒加载组件

懒加载组件须要引入 next/dynamic模块

新建组件components/content.js

const Content = () => {return <div>content</div>}

export default Content

引入pages/time.js

import {useState} from 'react'
import dynamic from 'next/dynamic'

// 懒加载自定义组件
const Content = dynamic(() => import('../components/content'))

const Time = () => {const getTime = async () => {const moment = await import('moment')
    setNowTime(moment.default().format('YYYY-MM-DD HH:mm:ss')) // 留神这里应用 default,不能间接用 moment()}
  const [nowTime, setNowTime] = useState('')
  return (
    <div>
      <button onClick={getTime}> 获取以后工夫 </button>
      <div> 以后工夫:{nowTime}</div>
      <Content />
    </div>
  )
}

export default Time

察看 Network 能够看到 Content 组件被新独自打包成一个 js 文件

打包生产环境

执行

npm run build

打包实现后,运行服务器

npm run start

而后查看页面,打包胜利!

参考:

  • Server side rendering Styled-Components with NextJS

  • ps:集体技术博文 Github 仓库,感觉不错的话欢送 star,给我一点激励持续写作吧~

正文完
 0