Nextjs全解
Contents
0 准备
0.1 创建项目
- 利用yarn或npm创建
|
|
- 使用typescript:
|
|
- 根据提示输入文件名后,输入
yarn dev
启动项目。
0.2 Link快速导航
- 用法:用Link标签包裹a标签
|
|
- 优点:
- 页面不会刷新,而是利用AJAX请求新页面内容
- 不会请求重复的HTML、CSS、JS(可以打开控制台/network)
- 自动在页面插入新内容、删除旧内容
- 因为省了很多请求和解析过程,所以速度极快
- 借鉴了Rails Turbolinks、pjax等技术
0.3 同构代码
- 同一份代码运行在Node控制台和Chrome两端
- 不是所有的代码都会运行,有些需要用户触发,有些API(window)不能两端共用
1 全局配置
1.1 自定义<head>
-
Head标签可以改变各页面的
title
及meta:viewport
1 2 3 4 5 6
import Head from 'next/head' ... <Head> <title>我的next页面</title> <meta name="description" content="Generated by create next app" /> </Head>
1.2 _app.js
-
在
page/_app.js
中统一编写各页面的全局配置,包括全局css
以及全局<head>
等。 -
页面切换时app组件不会销毁,只是其中的组件会销毁,所以可以在这里保存一些全局状态
-
全局css只能在这里引入,其他组件只能编写局部css
-
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// 全局css import '../styles/globals.css' import Head from 'next/head' function MyApp({ Component, pageProps }) { return ( <> <Head> <title> 我的博客 </title> <meta name="description" content="Generated by create next app" /> </Head> <Component {...pageProps} /> </> ) } export default MyApp
-
引入css的路径是基于本文件的相对路径,因此写起来有些麻烦。可以通过配置
jsconfig.json | tsconfig.json
来重新规定项目引用根目录1 2 3 4 5
{ "compilerOptions": { "baseUrl": "." } }
引用时,可以简写为
import 'styles/global.css'
1.3 局部css
-
styled-jsx
-
写法为:
-
1 2 3 4 5 6 7 8
<div className='example'> First Post </div> <style jsx>{` .example { color: blue } `}</style>
-
-
CSS Modules
-
在styles文件夹下新建
Example.module.css
,在文件中引入及使用方式为: -
1 2 3
import style from 'styles/Example.module.css' ... <div className={ style.example }>First Post</div>
-
-
配置
scss
-
直接安装依赖即可
-
1
yarn add sass
-
1.4 静态资源
-
配置
webpack
-
需要在
next.config.js
中配置file-loader
:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
module.exports = { reactStrictMode: true, webpack: (config, options) => { config.module.rules.push({ test: /\.(png|jpg|jpeg|gif|svg)$/, use: [ { loader: 'file/loader', options: { // 指定输出名称:‘文件名.哈希值.后缀’ name: '[name].[contenthash].[ext]', // 硬盘中的输出路径 outputPath: 'static', // 网站中的加载路径 publicPath: '/_next/static' } }, ], }) return config }, }
-
在根目录下新建
assets/image
,用于存放图片类型的静态资源 -
yarn add --dev file-loader
安装依赖,重启项目后,即可引用图片格式的静态资源了
-
-
新版next中无需配置
webpack
,如需加载静态图片资源,使用<Image>
插件即可1 2 3 4
import png from 'assets/image/1.png' import Image from 'next/image' ... <Image alt='' src={ png } />
1.5 TypeScript支持
-
安装相关依赖
1
yarn add --dev typescript @types/node @types/react
-
新版本可以直接在创建时就配置typescript支持
2 Next.js API示例
-
Next.js的api位于
pages/api
中,下面以路径/api/v1/post
、响应体json为例,开发获取全部博客列表
的接口-
在api下新建v1文件夹(表示第一版接口),创建
posts.tsx
。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next"; // 将getPosts方法放在lib目录下,表示自己封装的工具函数 import { getPosts } from 'lib/posts' // 在指定类型时也可以给整个方法指定为NextApiHandler类型 // const Posts:NextApiHandler = (req, res) => { const Posts = async (req: NextApiRequest, res: NextApiResponse) => { const postsList = await getPosts() res.statusCode = 200 res.setHeader('Content-Type', 'application/json') res.write(JSON.stringify(postsList)) res.end() } export default Posts
-
lib/posts.tsx
:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
import fs, { promises as fsPromise } from 'fs' import path from 'path' import matter from 'gray-matter' // 获取博客列表 const getPosts = async () => { // 获取当前工作目录(current working dir),即项目的绝对路径 // 不同操作系统,路径的连接方式不同,所以使用path.join自动拼接路径 const markdownDir = path.join(process.cwd(), 'markdown') const fileNameList = await fsPromise.readdir(markdownDir) // 分别读取每个文件 const posts = fileNameList.map(fileName => { const fullPath = path.join(markdownDir, fileName) // 处理文件名的后缀 const id = fileName.replace('.md', '') // 每个文件的文本数据 const text = fs.readFileSync(fullPath, 'utf-8') // 交给matter的库去处理,matter用来解析md格式的头部文件 const { data: { title }, content } = matter(text) return { id, title } }) return posts } export { getPosts }
-
3 Next.js页面渲染方式
- Next.js渲染页面主要有三种方式:客户端渲染
(BSR)
、静态页面生成(SSG)
和服务端渲染(SSR)
- 分别对应:
- 客户端渲染——用JS、Vue、 React创建HTML
- SSG——页面静态化,把PHP提前渲染成HTML
- SSR——PHP、 Python、 Ruby、 Java后台的基本功能
- 区别:不过Next.js的预渲染可以与前端React无缝对接(SSG)
3.1 客户端渲染
-
只在浏览器上执行的渲染,一般前端开发都是以这种形式,即由浏览器将前端请求到的数据渲染到页面中。
-
以上文posts接口为例,在页面中加载请求的posts数据。新建
pages/posts/index.tsx
:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
// 安装axios并引入:yarn add axios @types/axios import axios from "axios"; import { NextPage } from "next"; import { useEffect, useState } from "react"; // 声明Post数据类型 type Post = { id: string title: string } const PostsIndex: NextPage = () => { // post数据 const [posts, setPosts] = useState<Post[]>([]) // 是否加载中 const [isLoading, setIsLoading] = useState(true) // 数据是否为空 const [isEmpty, setIsEmpty] = useState(false) useEffect(() => { // 请求数据 axios.get('/api/v1/post').then((res: any) => { setIsLoading(false) setPosts(res.data) if (res.data.length === 0) { setIsEmpty(true) } }, () => { setIsLoading(false) }) }, []) return ( <div> <h1>文章列表</h1> {isLoading ? <div>正在加载。。。</div> : isEmpty ? <div>无数据</div> : posts.map(post => { return ( <div key={post.id}> {post.id} </div>) })} </div> ) } export default PostsIndex
访问
localhost:3000/posts
: -
封装hooks技巧
- 在
lib
目录下新建hooks文件夹,创建usePosts.tsx
- 复制全部hooks有关的代码及类型声明到新文件
- 将usePosts中的hooks全部return出来
- 源文件就可以通过usePosts函数获取到全部需要的参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
// usePosts.tsx import axios from "axios" import { useEffect, useState } from "react" type Post = { id: string title: string } export const usePosts = () => { const [posts, setPosts] = useState<Post[]>([]) const [isLoading, setIsLoading] = useState(true) const [isEmpty, setIsEmpty] = useState(false) useEffect(() => { axios.get('/api/v1/post').then((res: any) => { setIsLoading(false) setPosts(res.data) if (res.data.length === 0) { setIsEmpty(true) } }, () => { setIsLoading(false) }) }, []) return { posts, setPosts, isLoading, setIsLoading, isEmpty, setIsEmpty } } // pages/posts/index.tsx import { NextPage } from "next"; import { usePosts } from 'lib/hooks/usePosts' type post = { id: string title: string } const PostsIndex: NextPage = () => { const { isLoading, isEmpty, posts } = usePosts() return ( <div> <h1>文章列表</h1> {isLoading ? <div>正在加载。。。</div> : isEmpty ? <div>无数据</div> : posts.map((post: post) => { return ( <div key={post.id}> {post.id} </div>) })} </div> ) } export default PostsIndex
- 在
-
文章列表完全是由前端渲染的,我们称之为客户端渲染
-
客户端渲染缺点:
- 白屏:在AJAX得到响应之前,页面中只有白屏或Loading;
- 对
SEO(SearchEngineOptimization)
不友好:搜索引擎访问页面,看不到posts数据,因为搜索引擎默认不会执行JS,只能看到HTML。对于本页面来说,只能看到“文章列表”四个字。
-
静态内容和动态内容(选看)
- 一般来说,静态内容(
<div>文章列表</div>
)是写在代码中的,动态内容(posts
)是来自数据库的 - 静态内容是由服务端与客户端分别渲染一次
Next.js
分别调用了react的renderToString()
方法,由后端将前端代码转为字符串再发送给前端;之后调用hydrate()方法在前端将后端传过来的代码与前端React实例进行整合,会保留HTML且只进行事件绑定,从而让用户有一个非常高性能的首次加载体验。- 也就是说后端渲染HTML,前端添加事件绑定
- 前端也会渲染一次,但不是放在页面上,而是用来确保前后端渲染结果一致
- 一般来说,静态内容(
3.2 静态页面生成
-
背景:
- 当某个页面(例如首页)与用户数据无关时,即每位用户看到的页面是一样的,这种情况下仍需要每位用户的浏览器来渲染
- 所以可以在后端渲染好,然后发给每个用户
- 这样之前的N次渲染就变成了1次渲染,即N次客户端渲染变成了1次静态页面生成
- 这个过程叫做动态内容静态化
-
Next.js中后端获取数据的方式——
getStaticProps
-
将getStaticProps声明在每个page导出的函数旁边即可
-
写法:
1 2 3 4
export const getStaticProps = async () => { const posts = await getPosts() return {props: {posts: posts}} }
-
示例:修改posts页面为SSG渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
import { GetStaticProps, NextPage } from "next"; import { getPosts } from "lib/posts"; type Props = { posts: Posts[] } const PostsIndex: NextPage<Props> = (props) => { const { posts } = props return ( <div> <h1>文章列表</h1> {posts.map(post => { return (<div key={post.id}>{post.id}</div>) })} </div> ) } export default PostsIndex export const getStaticProps: GetStaticProps = async () => { const posts = await getPosts() return { props: { posts: posts } } }
打开页面源码,可以发现后端拿到数据后,将数据直接放入了
<script type="application/json">
标签内部
-
-
通过同构,前端也可以不用AJAX就能拿到数据了,这就是同构的好处:后端数据可以直接传给前端,然后前端
JSON.parse
一下就能得到了数据(next框架已经帮我们做了parse)。 -
PHP/java/Python 能不能做?
- 思路是一样的,他们也能做,但是他们不支持jsx,不好与React无缝对接,而且这些语言的对象不能直接提供给JS用,需要类型转换。
-
SSG静态化的时机:
- 开发环境:在开发环境每次请求都会运行一次
getStaticProps
,这是为了方便修改代码时重新运行。 - 生产环境:
getStaticProps
只在build时运行一次,这样可以提供一份HTML给所有用户下载。
- 开发环境:在开发环境每次请求都会运行一次
-
总结
- 动态内容静态化
- 如果动态内容与用户无关,那么可以提前静态化
- 通过
getStaticProps
可以获取数据 - 静态内容+数据(本地获取)就得到了完整页面,代替了之前的静态内容+动态内容(AJAX获取)
- 时机
- 静态化是在
yarn build
的时候实现的
- 静态化是在
- 优点
- 生产环境中直接给出完整页面
- 首屏不会白屏
- 搜索引擎能看到页面内容,方便SEO(注意SEO只识别有意义的标签,例如
h1
,a
等,不要使用div
)
- 动态内容静态化
3.3 服务端渲染
-
背景
- 在页面内容与用户相关时,较难提前静态化。因为我们需要在用户请求时才可以获取到用户信息,然后通过用户信息再去数据库拿数据
- 比如微博首页的信息流
- 因此,由服务端拿到用户信息后渲染页面发送给前端,这种方案,解决了传统的客户端渲染白屏的问题,而且访问速度也大大提升
-
而
getStaticProps()
只是在项目build阶段执行的,无法获取用户的请求数据 -
Next.js服务端获取数据的方式——
getServerSideProps
-
用法:
1 2 3 4
export const getServerSideProps: GetServerSideProps = async (context) => { ...// context对象包含用户请求与响应等一切信息,类型为NextPageContext return {props: {result: result}} }
-
运行时机:每次请求到来之后运行
-
示例:获取用户浏览器数据并展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
import type { GetServerSideProps, NextPage } from 'next' // 解析浏览器与系统信息的js库 import { UAParser } from 'ua-parser-js' type Props = { browser: { name: string } } const Home: NextPage<Props> = (props) => { const { browser } = props return ( <h1>你的浏览器是:{browser.name}</h1> ) } export default Home export const getServerSideProps: GetServerSideProps = async (context) => { const ua = context.req.headers['user-agent'] // 利用ua-parser-js库解析出客户端浏览器信息 const browser = new UAParser(ua).getBrowser() return { props: { browser } } }
运行结果:
3.4 Next.js渲染页面方案总结
- 静态内容
- 直接输出HTML
- 动态内容(Brower Side Render)
- 通过AJAX请求,渲染成HTML
- 动态内容静态化(Static Site Generation)
- 通过
getStaticProps
获取用户无关内容
- 通过
- 用户相关动态内容静态化(Server Side Render)
- 通过
getServerSideProps
获取请求数据
- 通过
3.5 三种渲染方式的选择
附 博客主页跳转
实现点击文章名称,跳转至文章页面的功能。
- 添加Link实现跳转
|
|
-
基于约定式路由命名规范,在
pages/page/posts
目录下创建[id].tsx
文件- 既声明了路由
/post/:id
,又是post/:id
的页面实现程序
- 既声明了路由
-
在
[id].tsx
中:-
实现
PostShow
,从props
接收post
数据1 2 3 4 5 6 7 8 9 10 11 12 13
type Props = { post: Posts } const postShow: NextPage<Props> = (props) => { const { post } = props return ( <> <h1>{post.title}</h1> <div>{post.content}</div> </> ) } export default postShow
-
实现
getStaticPaths
,返回静态id列表1 2 3 4 5 6 7
export const getStaticPaths: GetStaticPaths = async () => { const idList = await getPostIdList() return { paths: idList.map(i => { return { params: { id: i } } }), fallback: false } }
fallback:false
表示是否兜底,如果是false,表示如果请求的id不在getStaticPaths的结果中,则直接返回404页面;如果是true,表示id找不到的情况下,仍然尝试渲染页面(例如SSR的形式)。注意,id找不到不代表该id不存在,因为大型项目无法将全部页面静态化,只静态化了一部分id对应的页面。 -
实现
getStaticProps
,从第一个参数接收params.id
,返回静态post数据,并在build阶段生成各自的html文件1 2 3 4 5 6 7 8 9
export const getStaticProps: GetStaticProps = async (staticContext) => { const id = staticContext.params?.id const post = await getPost(id) return { props: { post: post } } }
-
源码如下
pages/posts/index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
import { GetStaticProps, NextPage } from "next"; import { getPosts } from "lib/posts" import Link from "next/link"; type Props = { posts: Posts[] } const PostsIndex: NextPage<Props> = (props) => { const { posts } = props return ( <div> <h1>文章列表</h1> {posts.map(post => { return ( <> <Link href={`/posts/${post.id}`} key={post.id}> <a>{post.id}</a> </Link> <hr /> </>) })} </div> ) } export default PostsIndex export const getStaticProps: GetStaticProps = async () => { const posts = await getPosts() return { props: { posts: posts } } }
pages/posts/[id].tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
import { getPost, getPostIdList } from 'lib/posts'; import { GetStaticPaths, GetStaticProps, NextPage } from 'next'; type Props = { post: Posts } const postShow: NextPage<Props> = (props) => { const { post } = props return ( <> <h1>{post.title}</h1> <div>{post.content}</div> </> ) } export default postShow export const getStaticPaths: GetStaticPaths = async () => { const idList = await getPostIdList() return { paths: idList.map(i => { return { params: { id: i } } }), // fallback: false } } export const getStaticProps: GetStaticProps = async (staticContext) => { const id = staticContext.params?.id const post = await getPost(id) return { props: { post: post } } }
lib/posts.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
import fs, { promises as fsPromise } from 'fs' import path from 'path' import matter from 'gray-matter' const markdownDir = path.join(process.cwd(), 'markdown') // 获取博客列表 const getPosts = async () => { const fileNameList = await fsPromise.readdir(markdownDir) // 分别读取每个文件 const posts = fileNameList.map(fileName => { const fullPath = path.join(markdownDir, fileName) // 处理文件名的后缀 const id = fileName.replace('.md', '') // 每个文件的文本数据 const text = fs.readFileSync(fullPath, 'utf-8') // 交给matter的库去处理 const { data: { title }, content } = matter(text) return { id, title } }) return posts } export { getPosts } // 获取博客数据 const getPost = async (id: string[] | string | undefined) => { const fullPath = path.join(markdownDir, id + '.md') const text = fs.readFileSync(fullPath, 'utf-8') const { data: { title }, content } = matter(text) return { id, title, content } } export { getPost } // 获取博客id数组 const getPostIdList = async () => { const fileNameList = await fsPromise.readdir(markdownDir) return fileNameList.map(i => i.replace('.md', '')) } export { getPostIdList }
-
Author gsemir
LastMod 2021-11-18