这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天
Vue 和 React 等框架在第一次构建的时候只会有一个基本的 HTML 框架和 JS 脚本文件,第二次请求才会构建出完整的页面,导致首屏渲染很慢,虽然之后的速度会变快,然而对于一些用户来说,如果首屏加载慢,那么便会失去耐心,SSR 的出现,大大加快了首屏加载数据,同时对于搜索引擎(SEO)的优化更好。记住,虽然 SSR 有很多优点,但也有缺点,比如对服务器资源消耗很大,所以对于一些后台管理系统等项目,可以使用 Vue 和 React。
NextJS
NextJS 是一个轻量级的 React 服务端渲染应用框架
NextJS 中的路由
NextJS 如何实现路由
相比于 React 中传统的路由,NextJS 实现路由的方式是通过文件夹目录结构实现
NextJS 会将文件夹名或者文件名当做路由的地址,其中 index.jsx 便是路由地址的入口文件名,如果想要创建子路由,那么可以在对应的路由新建文件,文件名代表子路由地址,也可以新建文件夹,在对应文件夹中新建 index.jsx,此时文件夹名代表子路由地址。
路由分为静态路由和动态路由,静态路由的实现很简单只需要新建文件就可以,动态路由则需要在文件夹名加入 []。对于很多参数的路由,还可以使用 [...xxx] 的方式获取参数。
以上图为例解释对应的路由地址:
about/index.jsx --> http://127.0.0.1:3000/about
blog/[...slug].jsx --> http://127.0.0.1:3000/2022/02/02/...
clients/index.jsx --> http://127.0.0.1:3000/clients
clients/[id]/index.jsx --> http://127.0.0.1:3000/clients/2
clients/[id]/[clientprojectid].jsx --> http://127.0.0.1:3000/clients/2/projectA
portfolio/index.jsx --> http://127.0.0.1:3000/portfolio
portfolio/list.jsx --> http://127.0.0.1:3000/portfolio/list
portfolio/[projectid].jsx --> http://127.0.0.1:3000/portfolio/projectA
聪明的你可能已经发现,动态路由和静态路由可能会有冲突的地方,例如 portfolio 中的 list.jsx 和 [projectid].jsx 两个文件,当我访问 http://127.0.0.1:3000/portfolio/list,NextJS 会将 list 当做静态路由还是动态路由的参数?答案是静态路由,这一点 NextJS 处理的还是很好的
NextJS 获取动态路由参数
获取路由参数主要是动态路由,和 React 设计初衷一样,NextJS 也提供了获取路由的钩子函数:
-
引入
useRouterimport { useRouter } from 'next/router' -
获取路由参数
const router = useRouter() console.log(router.query) -
完整实例
// pages/clients/[id]/[clientprojectid].jsx import { useRouter } from "next/router" const SelectedClientProjectPage = () => { const router = useRouter() console.log(router.query) return ( <div> <h1>The Project Page fro a Specific Project for a Selected Client</h1> </div> ) } export default SelectedClientProjectPage如果访问 http://127.0.0.1:3000/clients/max/projectA,那么
router.query便是{ id: 'max', clientprojectid: 'projectA' } -
多个路由参数
// pages/blog/[...slug].jsx import { useRouter } from "next/router" const BlogPostsPage = () => { const router = useRouter() console.log(router.query) return ( <div> <h1>The Blog Posts</h1> </div> ) } export default BlogPostsPage此时访问 http://127.0.0.1:3000/blog/2022/03/03,打印的便是
{ slug: ['2022', '03', '03'] }
路由跳转
可以通过 Link 标签以及 router 对象 中的 push 和 replace 方法进行路由地址的跳转,但是与 React 中的有所不同。
假设我们跳转的路由文件结构为 /client/[id]/[clientprojectid]
第一种: Link标签形式
与 React 相同方式,只是跳转地址的属性为 href
<link href="/clients/max/projectA">跳转</link>
传入对象方式:
<Link href={{
pathname: 'clients/[id]/[clientprojectid]',
query: { id: 'max', clientprojectid: 'projectA' }
}}>跳转</Link>
第二种: useRouter钩子函数形式
先导入 useRouter 方法以及创建实例对象
import { userRouter } from 'next/router'
const xxx = () => {
const router = useRouter()
return <div>Test</div>
}
与 React 相同方式,直接写路由地址:
router.push('/clients/max/projectA')
转入对象方式:
router.push({
pathname: 'clients/[id]/[clientprojectid]',
query: { id: 'max', clientprojectid: 'projectA' }
})
页面数据预加载
浏览器访问网站典型的流程是,浏览器首先获取一个HTML 页面,此页面并没有数据,然后 useEffect 函数调用 fetch 从服务器获取数据,然后使用这些数据设置组件 state,state 因为发生改变,react 使组件函数重新执行一遍,页面因此重新渲染,因此页面实际上有两个渲染周期。
而 SEO (search engine optimization 搜索引擎优化)依据的是第一个渲染周期的页面,此页面数据为空,为了避免这个问题,可以使得预渲染的页面,在第一个 render cycle 就已包含从数据库中获取的数据。
要使得预渲染的页面包含数据,NextJS 提供了两种形式的预渲染:
- static site generation 静态生成 SSG
- server-side rendering 服务端渲染 SSR
| CSR | SSR | SSG | |
|---|---|---|---|
| 运行端 | 浏览器 | 服务器 | 服务器 |
| 静态文件 | 单页面 | 由服务器即时生成 | 多个页面 |
| SEO | 不适合 | 适合 | 适合 |
| 静态文件CDN | 适合 | 不适合 | 适合 |
| 适用场景 | 中后台产品 | 信息展示型网站 | 内容较为固定的资讯类网站 |
getStaticProps
NextJS 提供了 getStaticProps 函数来实现在 Build 期间就能显示数据的方法。
使用步骤:
静态路由
-
书写逻辑
可以在
getStaticProps函数中写请求 API 的逻辑export async function getStaticProps() { /* CODE HERE */ const data = fetch('xxxx') if(!data) { return { redirect: { destination: '/no-data' } } } if(data.length === 0) { return { notFound: true } } return { props: { products: data }, revaliate: 10 } } -
getStaticProps返回值参数参数名 类型 作用 redirect 对象 重定向,可以指定其中的 destination属性,指定路由地址notFound 布尔值 设置为 true 时,会显示 404 页面,默认为 false props 对象 组件的 props 参数,用户可以指定从 API 中获取的数据 revaliate 数字 getStaticProps内部逻辑和静态页面重新执行、生成的时间,例如设置为 10,在 10s 内无论怎么刷新页面去请求某一个数据,也不会出发请求逻辑,10s 过后,如果有新数据,那么会重新生成静态页面。当用户新添加数据时,如果不指定revaliate,虽然页面会显示数据,但这个数据并不会预先生成,检查源代码就能看到,如果指定 revaliate 就可以显示了 -
组件使用
propsconst HomePage = (props) => { console.log(props.data) return ( <div>{props.data}</div> ) }当然这样设置对于某些动态路由组件会报错,甚至会报错,此时需要设置另一个函数:
getStaticPaths,这个以后再讲 -
效果查看
如果执行的命令是
npm run dev,那么revaliate属性不会起作用,我们需要先执行npm run build再执行npm start,查看效果npm run build 效果图,我们可以很清楚看到 Route 与
SSG或者Static之间的对应关系
动态路由
-
动态路由获取参数
在
getStaticProps中的 context 上下文获取动态路由参数// [pid].jsx export async function getStaticProps(context) { const { params } = context const pid = params.pid return { props: { id: pid } } }当然设置上述是不够的,还要写另一个函数
getStaticPaths -
书写
getStaticPaths函数`getStaticPaths 有很多种写法,要配合
getStaticProps和 组件函数fallback 配置项的作用:当用户传入的路径参数不在规定的范围内之后,NextJS 要给用户展示什么:
- 如果 fallback 为 false,则会展示 404 页面
- 如果 fallback 为 true,那么服务器将会根据用户传递的路由参数(即使与 path 配置项的路由不匹配),获取对应的数据,然后将这些数据传递给组件进行静态页面的生成(可以查看有个新建的页面),最后将这个生成好的静态页面传递给客户端用户
- blocking 会一次性加载所有页面(路由)
2.1 第一种写法
在
getStaticPaths函数的返回值中,指定所有的路由参数,一般这样选择。建议加上 fallback
// [pid].jsx export async function getStaticPaths() { return { paths: [ { params: { pid: 'p1' } }, { params: { pid: 'p2' } }, { params: { pid: 'p3' } }, ] } }2.2 第二种写法
这样就访问 p1、p2、p3 也可以访问了
export async function getStaticPaths() { return { fallback: true } }不过如果这样写,那么当用户访问不存在的地址,会直接报错,如果我们不希望这样,可以在
getStaticProps中设置notFound: trueexport async function getStaticProps(context) { const { params } = context const productId = params.pid // 假设有个获取数据的方法 const data = await getData() // 根据路由参数过滤数据 const product = data.products.find(product => product.id === productId) // 关键代码,如果路由参数不对,那么直接返回 404 if(!product) { return { notFound: true } } return { props: { loadedProduct: product } } }同时指定组件函数,因为不太确定各个函数执行时机
const xxx = (props) => { const { loadedProduct } = props if(!loadedProduct) { return <p>loading....</p> } return <p> {loadedProduct} </p> }注意:第二种方法的数据处理方式也适合第一种方法
tips:fallback为false的时候,不需要notFound属性,因为只有从getStaticPaths返回的路径才会被预渲染,当用户访问的路由参数没有在当前函数中返回时,是否显示404,false是显示,true是不显示 。
-
fallback: false如果fallback是false,那么任何路径都不会生成并且变成一个404页面。你可以在你仅有少量的路径去预渲染的时候这样做,这样的话在构建时都是静态页面。当并不经常添加新的页面的时候这样做很有用。但是当你需要然后新的时候的时候,你就需要重新构建。 -
如果
fallback是true,那么getStaticProps的行为会有如下变化:
-
由
getStaticPaths获取的路径会在构建时调用getStaticProps方法渲染成 HTML 文件; -
在构建时未生成的路径不会以 404 页面返回。相反,当请求不存在的页面路径时,Next.js 会渲染一个当前页面的
回退(fallback) 版本。注意,回退(fallback) 版本不会提供给像谷歌这样的爬虫程序,而是以阻塞模式呈现路径。 -
在后台,Next.js 会根据请求路经执行
getStaticProps方法静态生成页面的HTML和JSON。 -
当这些都完成之后,浏览器接收 JSON 数据根据对应的生成路径。这些数据会自动在页面渲染的时候被使用。从用户的角度来看,页面将从备用页面切换到完整页面。
-
在同时,Next.js 将路经添加到预渲染页面列表中。对同一路径的后续请求将渲染已经生成的页面,就像构建时预渲染的其他页面一样。
fallback: true何时最有用? 当你的应用有大量的依赖于数据(depend on data)的静态页面fallback: true,你想要预渲染所有的商品页面,但这样你的构建将花费很长时间。取而代之的是,你可以静态生成一个很小的页面集合,其余部分通过使用fallback: true生成。当有用户请求一个还没有生成的页面时,用户会看到页面中有一个加载指示器。很快的,getStaticProps执行完成,页面会根据请求到的数据渲染。之后同样请求此页面的任何用户将会得到静态预渲染的页面。 这确保了用户在保持快速构建和静态生成的好处的同时始终拥有最快的体验。fallback: true并不会更新已经生成的页面,具体可查看增量静态再生(Incremental Static Regeneration)。fallback: 'blocking'如果fallback是blocking,getStaticPaths未返回的新路径将等待HTML完全生成,与 SSR 是完全相同的,然后被缓存下来以供将来的请求使用,因此每个路径只发生一次。
-
代码示例
import fs from 'fs/promises' import Link from 'next/link' import path from 'path' const index = (props) => { const { products } = props return ( <ul> {products.map(product => <li><Link href={`/${product.id}`} key={product.id}>{product.title}</Link></li>)} </ul> ) } export async function getStaticProps() { console.log('(Re-)Generating...') const filePath = path.join(process.cwd(), 'data', 'dummy-backend.json') const jsonData = await fs.readFile(filePath) const data = JSON.parse(jsonData) if(!data) { return { redirect: { destination: '/no-data' } } } if(data.products.length === 0) { return { notFound: true } } return { props: { products: data.products }, revalidate: 10, } } export default index
getServerSideProps
getServerSideProps 只会在服务器并且只在服务器上运行
和 NodeJS 一样,可以从上下文中获取 req 和 res 对象,也可以像 NextJS 一样获取动态路由参数
// [uid.jsx]
const UserIdPage = (props) => {
return (
<h1>{props.id}</h1>
)
}
export const getServerSideProps = async (context) => {
const { params, req, res } = context
const userId = params.uid
return {
props: {
id: 'userid-' + userId
}
}
}
export default UserIdPage
当然,getServerSideProps 也可以和 getStaticProps 一样返回重定向或者404数据
return {
redirect: {
destination: '/xx',
permanent: false // 只有这一次重定向,并不是永久重定向
}
}
return {
notFound: true
}
Head 标签与 Image 标签
Head
有时我们的网页有很多路由,如果要更换不同路由的网页标题,该如何做?
NextJS 为我们提供了 Head 标签,在 Head 标签内可以书写 meta、title 等标签。
步骤:
-
导入
Head 标签import Head from 'next/head' -
使用
Head 标签在组件函数中添加
const xxx = () => { return ( <Head> <title>本页面标题为xxx</title> <meta name="description" content='NextJS Events' /> </Head> <div> <h1>xxx</h1> </div> ) }如果想要加上整个网页通用的
meta 标签,可以在_app.jsx入口文件中添加,以下是完整代码:注意:
_app.jsx中Head 标签中的title 标签优先级弱于路由级别的title 标签,会被覆盖import '../styles/globals.css' import '../components/layout/layout' import Layout from '../components/layout/layout' import Head from 'next/head' const MyApp = ({ Component, pageProps }) => { return ( <Layout> <Head> <title>Next Events</title> <meta name="description" content='NextJS Events' /> <meta name="viewport" content='initial-scale=1.0, width=device-width' /> </Head> <Component {...pageProps} /> </Layout> ) } export default MyApp如果希望设计页面结构,可以在
_document.jsx入口文件修改,以下是完整代码import Document, { Html, Head, Main, NextScript } from 'next/document' class MyDocument extends Document { static async getInitialProps(ctx) { const initalProps = await Document.getInitialProps(ctx) return initalProps } render() { return ( <Html lang="en"> <Head> {/* Head 标签 */} </Head> <body> <div id="overlays" /> <Main /> <NextScript /> </body> </Html> ) } } export default MyDocument分析以上代码步骤
-
导入所需组件
import Document, { Html, Head, Main, NextScript } from 'next/document' -
书写类标签架构
class MyDocument extends Document { static async getInitialProps(ctx) { const initalProps = await Document.getInitialProps(ctx) return initalProps } render() { return ( <Html> <Head> {/* Head 标签 */} </Head> <body> <Main /> <NextScript /> </body> </Html> ) } } export default MyDocument -
添加属性
return ( <Html lang="en"> <Head></Head> <body> <div id="backdrop"></div> <Main /> <NextScript /> </body> </Html> )
-
Image
默认的 img 标签并不会对图片进行压缩、懒加载等优化,NextJS 提供的 Image 标签可以很好地解决这个问题。
Image 标签的使用:
<Image src="/picURL" alt="鼠标悬浮显示文本" width={500} height={500} />
其中 src、alt 属性与普通的 img 标签一致,width 和 height 需要根据实际情况,例如图片的大小上限、父元素的大小等设置。。
使用前后的变化:
前
后
可以看到首先是图片的 Type 属性由 jpeg 改为 webp,Size 和 Time 都有所减少,尤其是 Time 响应时间。
api router
项目结构:
page 页面中的 api 是一个特殊的文件夹,它代表着 api router(api 路由)
如何使用 api router
-
简单示例
// pages/api/feedback.js const feedbackHandler = (req, res) { res.status(200).json({ message: 'it is working!' }) } export default feedbackHandler如果我们访问 http://127.0.0.1:3000/api/feedbackHandler 就会有
{ “message”: “it is working” }JSON 格式数据返回。发送请求:
GET 请求
// pages/feedback.jsx const feedbackPage = () => { fetch('/api/feedback').then(res => res.json()).then(data => { console.log(data) }) } export default feedbackPagePOST 请求
const feedbackPage = () => { fetch('/api/feedback', { method: 'POST', headers: { "Content-Type": "application/json" }, body: { text: '123456' } }).then(res => res.json()).then(data => { console.log(data) }) } export default feedbackPage -
req 参数
除了 GET 请求,还会有 POST、PUT 请求,以 POST 为例,看 NextJS 如何处理这些请求
// pages/api/feedback const feedbackHandler = (req, res) { if(req.method === 'POST') { const feedbackText = req.body.text } res.status(200).json({ message: 'it is working!' }) } export default feedbackHandlerreq 参数
参数名 参数作用 body 客户端 POST、PUT 请求的 body 数据内容 method 请求方法(具体有 POST、GET、PUT、DELETE ...) query 路由参数,具体见下一个页面
api 动态路由
和普通的 page 页面一样,api 动态路由也可以使用 [] 形式来指定路由参数,同时用 req.query.xxx 来获取动态路由参数
// pages/api/[feedbackId].js
const handler = (req, res) => {
const feedbackId = req.query.feedbackId
res.status(200).json({
feedback: selectedFeedback
})
}
export default handler
NextJS 部署
标准构建 Standard Build
命令:
next build
npm run build
使用此命令构建的 NextJs 应用程序是优化的生产版本,它会生成一个服务端的应用程序,因此需要一个 NodeJS 服务器来运行它,所以不能放在某个静态主机上。NextJS 具有内置的服务端功能,可以在服务器上及时渲染页面、重新验证页面、API路由,这些服务端的特性,需要一个 NodeJS 服务器运行这些代码。
完全静态构建 Full Static Build
命令:
next export
npm run export
会产生 100% 的静态应用程序(只有 HTML、CSS、JavaScript),所以不需要 NodeJS 服务。
如果你的应用依赖于 API Routes 或者 server-side pages或者 revalidations 或者将 fallback 设置为 true 或者 blocking 的页面。所以 next export 只适用于不需要任何服务端代码的页面。
这还意味着当你需要重新更改页面时,需要重新书写代码。对于一些非常简单的博客,比如你每周添加一篇新的文章,可能非常好。
部署步骤
-
一些必要的前置操作
添加页面的 meta 数据、标题、描述;去掉一些不必要的依赖和控制台输出语句;优化代码。不错的是
NextJS内置了延迟加载。 -
检查配置
正确配置环境变量、api 密钥,比如说你的测试数据库地址对于其他用户来说肯定没用,需要换成服务器的数据库地址
-
测试构建
在本地的机器上测试应用程序是否就绪,如果对构建的大小不满意,可以回到第一个步骤优化项目体积
-
最终部署
next.config.js
在项目根目录中创建 next.config.js
在其中书写以下代码:
module.exports = {
env: {
customKey: 'my-value',
mongodb_username: 'xj',
mongodb_password: 'dfa2sf2ad',
mongodb_clustername: 'cluster0',
mongodb_database: 'my-site'
}
}
这样就可以在组件或者其他 js 文件中使用这个 key 值了
// src/page.js
function Page() {
return <h1>The value of customKey is: {process.env.customKey}</h1>
}
export defualt Page
// src/utils/db.js
const connectionString = `mongodb+src://${process.env.mongodb_username}:${process.env.mongodb_password}@${process.env.mongodb_clustername}.ntrwp.mongodb.next:${mongodb_database}`
当然 next.config.js 的内容不止这些,我们还可以默认导出一个函数
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants')
module.exports = (phase) => {
// 如果我们处于开发阶段,则返回这些数据
if(phase === PHASE_DEVELOPMENT_SERVER) {
return {
env: {
customKey: 'my-value',
mongodb_username: 'xj',
mongodb_password: 'dfa2sf2ad',
mongodb_clustername: 'cluster0',
mongodb_database: 'my-site-dev'
}
}
}
return {
env: {
customKey: 'my-value',
mongodb_username: 'xj',
mongodb_password: 'dfa2sf2ad',
mongodb_clustername: 'cluster0',
mongodb_database: 'my-site'
}
}
}
这样就不会影响真正的数据库数据