集成式SSR框架 - Next.js

785 阅读13分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

提示:项目实战类文章资源无法上传,仅供参考

集成式SSR框架 - Next.js

NextJS介绍

react服务器端渲染应用框架,用于构建SEO有好的SPA / 单页应用,解决SEO不友好和首屏页面加载慢的问题

  • 支持两种渲染方式,静态生成(预先生成静态页面)和服务器端渲染(上一节方式)
  • 基于页面的路由系统(文件名即访问地址),路由0配置,无需手动配置
  • 自动代码拆分,不同页面生成不同文件,优化加载速度
  • 支持静态导出,可将应用导出为静态的网站
  • 内置CSS-in-JS库,styled-jsx,也可使用自己的库
  • 方案成熟,可用于生产环境,使用广泛
  • 用用部署简单,拥有专属的部署环境 - Vercel,也可部署在其他环境

Nwxt.js项目创建

  1. 创建项目:npm init next-app 项目名
    • 命令会帮助我们自动安装create-next-app然后调用,保证我们创建的版本是最新的
    • 选择模版使用默认模版即可
  2. 运行项目:npm run dev
  3. 浏览器访问:http://localhost:3000/

基于路由的页面系统

创建页面

  • 页面是被放置在pages文件夹中的React组件
  • 组件需要被默认导出 - default
  • 组件中不需要手动引入react
  • 页面地址即为文件地址,相互对应

对应关系举例:

页面路径 - 项目中访问地址 - 浏览器中
pages/index.js/index 或 /
Pages/list.js/list
Pages/post/my.js/post/my

路由跳转 - Link组件

  • Link组件默认使用JS进行页面跳转,即SPA(单页)形式跳转
  • 如果浏览器禁用了JS,则使用链接跳转(普通a标签跳转)
  • Link组件标签上,不应该添加除了href之外的其他属性,其他属性书写在Link内部a标签中
  • Link组件通过预取/加载(生产中)功能自动优化应用程序以获取最佳性能
  • Link组件内部必须包裹a标签,否则会报警告
// Link组件从next/link中获取
import Link from 'next/link'

// Link标签上只允许书写href属性,属性值即为路由跳转地址
<Link href='/list'>
  // 其他属性书写在a标签上
  <a title='list pages'> list pages </a> 
</Link>
// 引入Link组件
import Link from 'next/link'

export default function Home() {
  return (
    <>
      Home内容
      {/* Link组件有且只有href一个属性 */}
      <Link href='/list'>
        {/* Link内部必有a标签 */}
        <a>跳转到list</a>
      </Link>
    </>
  )
}

静态资源访问

应用程序中public目录存放静态资源,当需要使用静态资源时,直接使用 / 即可表示public目录

例如:

资源路径访问地址
public/images/1.jpg/images/1.jpg
public/css/base.css/css/base.css
export default function Home() {
  return (
    <>
      {/* 使用静态文件,文件存放在public/favicon.ico */}
      <img src='/favicon.ico'/>
    </>
  )
}

修改元数据 - head

  • 元数据 - hade标签中包含的内容
  • Next可以让每一个页面拥有自己的title、css等
  • Next中使用Head组件可修改当前页面元数据
// Head组件从next/head中获取
import Head from 'next/head'

// 直接使用Head在组件,Head组件内部直接书写自定义元数据即可
<>
  <Head>
    <title>自定义title</title>
    ........
  </Head>
  ........
</>

// 最终程序会把自定义的元数据渲染到页面中
// 引入Head修改/定制元数据
import Head from 'next/head'

function List() {
  return (
    <>
      {/* 元数据 */}
      <Head>
        {/* 设置自己的页面的title */}
        <title>List</title>
      </Head>
      <div>List内容</div>
    </>
  )
}

export default List

添加样式

使用元文件引入

直接在Head标签内添加link标签引入样式也可以成功添加样式

Next内置的styled-jsx

在Next中内置了styled-jsx,它是一个CSS-in-JS库,允许直接在React内部书写css样式,样式也仅在该组件内部生效

  1. 直接给需要设置样式的标签添加类名 - className,其他选择器应该也可以
  2. 之后在组件内部书写<style jsx>{模版字符串}<.style>标签,将需要书写的样式全部写在模版字符串内即可
......
<>
  <Link href='/list'>
    <a title='list pages' className='demo'> list pages </a> 
  </Link>

  <style jsx>{`
    .demo {
    	color: red
    }
   `}</style>
</>
......
import Link from 'next/link'
import Head from 'next/head'

export default function Home() {
  return (
    <>
      <Head>
        <title>Home</title>
      </Head>
      <p>Home内容</p>
      <Link href='/list'>
      	{/* 设置类名 */}
        <a className='a'>跳转到list</a>
      </Link>
      {/* 使用styled-jsx书写样式 */}
      <style jsx>{`
        .a {
          color: red;
        }
      `}</style>
    </>
  )
}

CSS模块导入

  • 通过使用CSS模块功能,语序将组件的CSS样式单独书写在一个.css文件中
  • CSS模块约定样式文件的名称必须为组件名.module.css
/* index.module.css,样式文件,文件名称必须以.module.css结尾 */
.box {
  width: 100px;
  height: 100px;
  backage-color: red
}
// 直接引入样式文件
import Styles from './index.module.css'

// 组件使用时直接使用选择器即可,style.box表示设置类名为box
<div clasName={style.box}></div>
import Link from 'next/link'
import Head from 'next/head'
// 通过import引入外部样式文件
import style from '../styles/index.module.css'

export default function Home() {
  return (
    <>
      <Head>
        <title>Home</title>
      </Head>
      <p>Home内容</p>
      <Link href='/list'>
        {/* 直接使用引入的样式文件设置类名 */}
        <a className={style.a}>跳转到list</a>
      </Link>
    </>
  )
}

全局样式文件导入

  1. 在pages目录下新建_app.js文件并书写固定代码,注意文件名是固定的,不可更改
  2. 在项目根目录下创建style目录,并在其中创建global.css,也可以在任意位置创建global.css,命名也可随意
  3. _app.js中通过import引入global.css
  4. 重启开发服务器,必须重启才能生效
// pages/_app.js

// 引入全局css样式文件
import '../style/global.css'

// 固定写法
export default function App({Component, pageProps}) {
  return <Component {...pageProps}/>
}

在创建Next项目的时候,会自动创建_app.js文件、style目录global.css文件

import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

预渲染

预渲染指数据和HTML的拼接在服务器提前完成

  • 预渲染可以使SEO更加友好
  • 会带来更好的用户体验,无需运行JS即可查看应用程序UI界面

预渲染两种形式区别在于生成HTML的时机不同

  • 静态生成:在构建时生成HTML,以后请求时把体检构建好的HTML直接响应给客户端
  • 服务器端渲染:请求时生成HTML,每个请求都会重新生成HTML,然后响应给客户端

渲染方式选择

  • Next允许开发者给每个页面选择不同的渲染方式,不同渲染方式也有不同特点,应根据场景进行渲染
  • 但建议大多页面使用静态生成
    • 静态生成一次构建可以反复使用,访问速度开,适用于营销页面、博客文章、电子商务产品列表、帮助、文档等
    • 服务器端渲染访问速度不如静态生成快,但是因为可以自行组合HTML结构,更加灵活,所以比较适用于数据频繁更新的页面或者说内容随着请求会有变化的页面

静态生成

要区分是否是静态生成可以查看打包后.next/server/pages目录中有没有对应的HTML文件,如果有,则证明这个页面是静态生成的

  • 无数据静态生成:组件不需要在其他地方获取数据,默认直接进行静态生成
  • 有数据静态生成:如果组件需要在其他地方获取数据,在构建时Next会预先获取组件需要的数据,然后再对组件进行静态生成

有数据静态生成 - getStaticProps

  • getStaticProps的所用是获取组件静态生成所需要的数据,并通过props的方式传递给组件
  • getStaticProps方法是一个异步函数,需要在组件内部进行导出
  • 在开发模式下,getStaticProps改为在每个请求上运行,意味着刷新页面就会执行一次
  • getStaticProps是运行在node环境下的,所以可以调用node相关接口代码
import React from 'react'
// 引入Head修改/定制元数据
import Head from 'next/head'
// Node读取文件API
import { readFile } from 'fs'
// 将读取文件API进行包装,让她返回Promis对象
import { promisify } from 'util'
// 路径拼接API
import { join } from 'path'

// 包装读取API
const read = promisify(readFile)

// 直接接收到静态构建所需数据
function List(props) {
  return (
    <>
      {/* 元数据 */}
      <Head>
        {/* 设置自己的页面的title */}
        <title>List</title>
      </Head>
      <div>List内容</div>
      {/* 直接使用数据 */}
      <p>{props.data}</p>
    </>
  )
}

export default List

// 获取静态构建需要的数据,异步函数
export async function getStaticProps() {
  // 我们在这里直接读取pages/_app.js文件内容,实际工作中也可以是一个接口请求
  const data = await read(join(process.cwd(), 'pages', '_app.js'), 'utf-8')
  // 打印看一下
  console.log(data)
  // 最终返回出去
  return {
    // 这个props可以在list组件上直接使用
    props: {
      data,
    },
  }
}

有数据的静生成,会在整个项目构建的时候把getStaticProps传递过去,进行数据获取,数据获取后再交给组件进行静态生成,所以可以发现上面的console.log(data)在实际生产环境下无论怎么刷新页面也不会执行,而在开发模式下每次刷新都会发现它执行了,而且还是在node环境下执行的,也就是说,其实我们每次刷新页面都会进行一次重新构建

服务器端渲染

  • 服务器端渲染需要在组件中导出getServerSideProps方法
  • 和上面类似,这也是一个异步函数,最终也要返回props
  • getServerSideProps方法会携带一个参数context - 上下文对象,这个参数的query属性中包含客户端此次请求参数
  • getServerSideProps和上面的getStaticProps用法相同,只不过是多了一个参数而已,我们可以利用这个参数做一些操作
import React from 'react'
// 引入Head修改/定制元数据
import Head from 'next/head'
// Node读取文件API
import { readFile } from 'fs'
// 将读取文件API进行包装,让她返回Promis对象
import { promisify } from 'util'
// 路径拼接API
import { join } from 'path'

// 包装读取API
const read = promisify(readFile)

// 直接接收到静态构建所需数据
function List(props) {
  return (
    <>
      {/* 元数据 */}
      <Head>
        {/* 设置自己的页面的title */}
        <title>List</title>
      </Head>
      <div>List内容</div>
      {/* 直接使用数据 */}
      <p>{props.data}</p>
    </>
  )
}

export default List

// 进行服务器端渲染
export async function getServerSideProps(context) {
  // 我们在这里直接读取pages/_app.js文件内容,实际工作中也可以是一个接口请求
  const data = await read(join(process.cwd(), 'pages', '_app.js'), 'utf-8')
  // 打印看一下
  console.log(data, context.query)
  // 最终返回出去
  return {
    // 这个props可以在list组件上直接使用
    props: {
      data,
    },
  }
}

**服务器端渲染会在每次发出请求的时候进行促渲染,并且打包时不会在pages目录下生成HTML文件,**所以当刷新页面的时候console.log(data, context.query)每次都会执行,并在终端打印出来

无论是静态生成还是服务器端渲染,上面引入的nodeJS接口不会打包到最终目录中

基于动态路由的静态生成

需求:查看不同文章时,路由会有所不同,此时使用静态生成

  • 基于参数为页面组件生成HTML页面,有多少参数就生成多少个HTML页面
  • 在构建时,获取用户可以访问的所有路由参数,再根据路由参数获取具体数据,然后再进行静态生成

实现步骤

  • 创建基于动态路由的页面组件(模版组件),命名的时候文件名称外面加上[],比如[id].js
  • 在组件内部导出一个异步函数getStaticPaths,用来获取所有用户可以访问的路由参数
  • 在导出一个getStaticProps(静态生成)异步函数,用于根据路由参数获取具体的数据
  • 当获取到全部路由信息 - getStaticPaths后会遍历全部路由,然后每次遍历路由都会触发一次静态生成 - getStaticProps
  • 注意:getStaticPaths和getStaticProps永远不会在客户端运行,也不会打包进客户端,所以这两个方法里面就可以随意书写服务端代码,例如文件操作、读取数据库等
// 用于获取关于此id全部的路由参数
export async function getStaticPaths() {
  // 注意:这里id必须使用字符串,否则会报错
  return {
    // 全部可能的路由信息,这里直接写死,工作中可以从接口获取
    paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
    fallback: false,
  }
}

// 上面paths会被遍历,每次遍历都会调用这个方法进行静态生成
// params即为路由信息
export async function getStaticProps({ params }) {
  const id = params.id
  // 创建data接收数据
  let data
  // 这里使用switch写死数据,工作中可以根据路由信息进行异步请求数据等工作
  switch (id) {
    case '1':
      data = { id: '1', title: '第一篇文章标题' }
      break
    case '2':
      data = { id: '2', title: '第二篇文章标题' }
      break
    default:
      data = {}
  }
  // 最终把数据返回出去
  return {
    props: {
      data,
    },
  }
}

// 组件接收数据并使用数据
function Post(props) {
  return (
    <>
      <p>{props.data.id}</p>
      <p>{props.data.title}</p>
    </>
  )
}

export default Post

总结:创建模版组件 => 获取全部可能使用到的路由参数 => 根据每个路由参数获取对应数据进行静态生成

fallback作用

fallback规定当用户访问不在路由参数范围内的路由时展示的内容

  • false(默认值):直接展示404页面
  • true:进行不在范围内的参数的静态生成,就算找不到也不会显示404,显示空白
import { useRouter } from 'next/router'

// 用于获取关于此id全部的路由参数
export async function getStaticPaths() {
  // 注意:这里id必须使用字符串,否则会报错
  return {
    // 全部可能的路由信息,这里直接写死,工作中可以从接口获取
    paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
    // 为true时,访问非1和2会尝试进行静态生成,否则直接显示404页面
    fallback: true,
  }
}

// 上面paths会被遍历,每次遍历都会调用这个方法进行静态生成
// params即为路由信息
export async function getStaticProps({ params }) {
  const id = params.id
  // 创建data接收数据
  let data
  // 这里使用switch写死数据,工作中可以根据路由信息进行异步请求数据等工作
  switch (id) {
    case '1':
      data = { id: '1', title: '第一篇文章标题' }
      break
    case '2':
      data = { id: '2', title: '第二篇文章标题' }
      break
    // id为3是不在上面范围内的
    case '3':
      data = { id: '3', title: '第三篇文章标题' }
      break
    default:
      data = {}
  }
  // 最终把数据返回出去
  return {
    props: {
      data,
    },
  }
}

// 组件接收数据并使用数据
function Post(props) {
  const router = useRouter()
  // 判断是否正在静态生成
  if (router.isFallback) return <div>正在静态生成</div>
  return (
    <>
      <p>{props.data.id}</p>
      <p>{props.data.title}</p>
    </>
  )
}

export default Post

自定义404页面

  • 替换原有的Next404页面
  • 直接在pages目录中创建404.js文件,Next会直接使用这个组件替换掉原本的404页面

API Routes

可以理解为接口,客户端向服务端发送请求获取数据的接口,Next.js应用允许React开发者编写服务器端代码创建数据接口

  • pages/api目录中创建API Routes文件,比如User.js
  • 文件中直接导出请求处理函数,函数有两个参数,req为请求对象,res为相应对象
  • 发起请求:直接使用http://localhost:3000/api/user可以请求`pages/api/user`
  • 注意:当前的API Routes可以接收任何HTTP请求方法。即get、post等都可以请求
  • 不要在getStaticPaths和getStaticProps中访问API Routes,因为这两个函数就在服务器端运行,直接书写服务器端代码即可
//pages/api/user.js 新建文件书写API Routes

export default (req, res) => {
  res.send({ name: '吴签', charge: '强奸罪' })
}

// 也可以模仿next提供的api/hello.js文件书写

总结:说白了,API Routes就是让开发者自己在项目中模拟一个接口,这个借口和后端接口一样可以返回数据