Next.js背景
开发团队是zeit
- zeit团队水平,知乎,后改名为Vercel
- 高中开始编程,会做平面设计的复旦大学计算机专业毕业生,在微软工作一年后加入zeit团队
- Next.js核心团队四个人平均年龄20岁
- 按star数,zeit是gitHub组织的Top20
- 全员远程工作
Next.js定位
Node.js全栈框架
- CSS-in-JS
- 页面预渲染+SSR(服务器渲染)
- 前后端同构(代码同时运行在两端)
- Node.js10.13以上
- React
- TypeScript
弱项
- 完全没有提供数据库相关功能,可自行搭配Squelize或TypeORM
- 完全没有测试相关功能,自行搭配Jest或者Cypress
- Blitz.js框架在往这些方向努力
创建Next项目
$ npm init next-app nextjs-blog-1$ cd nextjs-blog-1$ yarn devLink快速导航
用法
把点击改为
- 点击
优点
- 页面不会刷新,用AJAX请求新页面内容
- 不会请求重复的HTML,CSS,JS
- 自动在页面插入新内容,删除旧内容
- 因为省了很多请求和解析过程,所以速度极快
其他
差异
- 不是所有代码都会运行,有的需要用户触发
- 不是所有的API都能用,比如window在Node里报错
全局配置
自定义
使用 <Head>
pages/_app.js
- export default function App是每个页面的根组件
- 页面切换时App不会销毁,App里面的组件会销毁
- 可用App保存全局状态
```jsx
import Head from “next/head”;
function MyApp({ Component, pageProps }) {
return (
); }
<Head><title>我的博客</title><metaname="viewport"content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"></meta></Head><Component {...pageProps} />
export default MyApp;
<a name="hTniS"></a>### 全局CSS_app.js<br />`import './styles/global.css'`<br />切页面App不会销毁<br />其他组件内不引入CSS,只写局部CSS<a name="QdGUa"></a>### 局部CSS- style-jsx```jsxexport default function Index() {return (<div><Link href="/posts/first-post"><a> 第一篇文章! </a></Link><style jsx>{`a {color: green;}`}</style></div>);}
- CSS Modules ```jsx .wrapper { background: #3498db; }
.link { background: gray; }
```jsximport styles from 'styles/first-post.module.css'export default function X() {return (<><div className={styles.wrapper}>First Post<hr />回到首页<Link href="/"><a className={styles.link}>点击</a></Link></div></>);}
绝对引用
jsconfig.json
{"compilerOptions":{"baseUrl":"."}}
import 'styles/global.css'
使用SCSS
$ yarn add sass
.css改为.scss
静态资源
$ yarn add --dev file-loader
next.config.js
module.exports = {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},}
next-images
TypeScript
$ yarn global add typescript$ tsc --init 得到tsconfig.json
合并jsconfig.json的内容,然后删掉
打开 “noImplicitAny”: true,$ yarn add --dev typescript @types/react @types/node
.js改为.tsx
API
简单请求
./pages/api/v1/posts.tsx
import { NextApiHandler } from "next";const Posts: NextApiHandler = (req, res) => {res.statusCode = 200res.setHeader('Content-Type', 'application/json')res.write(JSON.stringify({name: 'Gouson'}))res.end()}export default Posts

实现posts API
./markdown/
写两个md文件
---title: 我的第一篇博客date: 2021-7-17---a a abbb是怎么回事呢?a a a相信大家都很熟悉,但是a a abbb是怎么回事呢,下面就让小编带大家一起了解吧。a a abbb,其实就是ccc,大家可能会很惊讶a a a怎么会bbb呢?但事实就是这样,小编也感到非常惊讶。这就是关于a a abbb的事情了,大家有什么想法呢,欢迎在评论区告诉小编一起讨论哦!
$ yarn add gray-matter
./lib/posts.tsx
import fs, { promises as fsPromise } from 'fs'import path from "path";import matter from 'gray-matter'const getPosts = async () => {const markdownDIR = path.join(process.cwd(), 'markdown')const fileNames = await fsPromise.readdir(markdownDIR)const posts = fileNames.map(filename => {const fullPath = path.join(markdownDIR, filename)const id = filename.replace(/\.md$/g, '')const text = fs.readFileSync(fullPath, 'utf8')const { data: { title, date }, content } = matter(text)return { id, title, date }})return posts}export default getPosts
./pages/api/v1/posts.tsx
import getPosts from "lib/posts";import { NextApiHandler } from "next";const Posts: NextApiHandler =async (req, res) => {const posts= await getPosts()console.log(posts)res.statusCode = 200res.setHeader('Content-Type', 'application/json')res.write(JSON.stringify(posts))res.end()}export default Posts
小结
- /api/里的文件是API
- 一般返回JSON格式的字符串
- 也可以返回HTML
- API文件默认导出NextApiHandler
- 这是一个函数类型
- 第一个参数是请求
- 第二个参数是对象
三种渲染方式
客户端渲染
只在浏览器上执行的渲染
用JS、vue、react创建HTML
$ yarn add axios$ yarn add --dev @types/axios
/pages/posts/index.tsx
import { NextPage } from 'next'import axios from 'axios'import { useEffect, useState } from 'react';type Post = {id: string;date: string;title: string;}const PostsIndex: NextPage = () => {const [posts, setPosts] = useState<Post[]>([])const [isLoading, setIsLoading] = useState(true)const [isEmpty, setIsEmpty] = useState(false)useEffect(() => {axios.get('/api/v1/posts').then(response => {setPosts(response.data)setIsLoading(false)if (response.data.length === 0) {setIsEmpty(true)}}, () => {setIsLoading(false)})}, []);return (<div><h1>文章列表</h1>{isLoading ? <div>加载中</div> :isEmpty ? <div>暂无文章</div> :posts.map(p =><div key={p.id}>{p.id}</div>)}</div>)}export default PostsIndex
缺点
- 白屏
- 在AJAX得到响应前,页面中后loading
SEO不友好
React SSR
- 推荐在后端 renderToString()在前端hydrate()
- hydrate()混合,会保留HTML并附上事件监听
- 也就是后端渲染HTML,前端添加监听
- 前端也会渲染一次,以确保前后端渲染一致
静态页面生成(SSG)
Static Site Generation,解决白屏问题、SEO问题
无法生成用户相关内容(所有用户的请求都一样)
页面静态化,把PHP提前渲染成HTML
每个page在默认导出的函数旁边加上export getStaticProps,写法是固定的
- 导出函数的第一个参数就是props
/pages/posts/index.tsx
import { NextPage } from 'next'import getPosts from '../../lib/posts';type Props = {posts: Post[]}const PostsIndex: NextPage<Props> = (props) => {const { posts } = propsreturn (<div><h1>文章列表</h1>{posts.map(p => <div key={p.id}>{p.id}</div>)}</div>)}export default PostsIndexexport const getStaticProps = async () => {const posts = await getPosts()return {props: {posts: JSON.parse(JSON.stringify(posts))}}}
同构
- 现在前端不用AJAX也能拿到posts了
- 这就是同构SSR的好处,后端数据可以直接传给前端
- 前端JSON.parse一下就能得到posts
静态化的时机
环境

- 解读
- λ(Server) SSR不能自动创建HTML
- ◯(Static)自动创建HTML(没用props)
- ⚫(SSG)自动创建HTML和JSON(用到了props)
- 三种文件类型
- posts.html含有静态内容,用于用户直接访问
- posts.js也含有静态内容,用于快速导航(与HTML相对应)
- post.json含有数据,跟post.js结合得到页面
小结
- 动态内容静态化
- 如果动态内容和用户午关
- 通过getStaticProps可以获取数据
- 静态内容+数据(本地获取)就得到了完整页面
- 代替了之前的静态内容+动态内容(AJAX获取)
- 时机
- 静态化在 yarn build的时候实现
优点
有的比较难提前静态化,比如微博。需要在用户请求时,获取用户id,去数据库拿数据
- 这时候想要在服务器渲染就不能用getStaticProps,因为是在build阶段执行
- 要用getServerSideProps(context:NextPageContext)
$ yarn add ua-parser-js$ yarn add --dev @types/ua-parser-js
/pages/index.tsx
import { GetServerSideProps, NextPage } from "next";import { UAParser } from "ua-parser-js";type Props = {browser: {name: string,version: string,major: string}}const index: NextPage<Props> = (props) => {const { browser } = propsreturn (<div><h1>你的浏览器是{browser.name}</h1></div>);}export default indexexport const getServerSideProps: GetServerSideProps = async (constext) => {const ua = constext.req.headers['user-agent']const { browser } = new UAParser(ua).getResult()return {props: {browser}}}
getServerSideProps
- 运行时机
- 无论是开发环境还是生产环境
- 都是在请求到来后运行getServerSideProps
参数
静态内容
- 直接输出HTML,没有术语
- 动态内容
- 术语:客户端渲染,通过AJAX请求
- 动态内容静态化
- 术语:SSG,通过getStaticProps获取用户无关内容
用户相关动态内容静态化
有动态内容吗?没有什么都不用做,自动渲染为HTML
- 动态内容和客户端相关吗?相关就只能用客户端渲染(BSR)
- 动态内容跟请求/用户相关吗?相关就只能用服务端渲染(SSR)或BSR
- 其他情况可以用SSG或SSR或BSR
升级版SSG
/posts/index.tsx
import { NextPage } from 'next'import Link from 'next/link'import getPosts from '../../lib/posts';type Post = {id: string;date: string;title: string;}type Props = {posts: Post[]}const PostsIndex: NextPage<Props> = (props) => {const { posts } = propsreturn (<div><h1>文章列表</h1>{posts.map(post => <div key={post.id}><Link href="/posts/[id]" as={`/posts/${post.id}`}><a>{post.title}</a></Link></div>)}</div>)}export default PostsIndexexport const getStaticProps = async () => {const posts = await getPosts()return {props: {posts: JSON.parse(JSON.stringify(posts))}}}
/lib/posts.tsx
import fs, { promises as fsPromise } from 'fs'import path from "path";import matter from 'gray-matter'import marked from 'marked'const markdownDIR = path.join(process.cwd(), 'markdown')const getPosts = async () => {const fileNames = await fsPromise.readdir(markdownDIR)const posts = fileNames.map(filename => {const fullPath = path.join(markdownDIR, filename)const id = filename.replace(/\.md$/g, '')const text = fs.readFileSync(fullPath, 'utf8')const { data: { title, date }, content } = matter(text)return { id, title, date }})return posts}export const getPost = async (id: string) => {const fullPath = path.join(markdownDIR, id + '.md')const text = fs.readFileSync(fullPath, 'utf8')const { data: { title, date }, content } = matter(text)const htmlContent = marked(content)return JSON.parse(JSON.stringify({ id, title, date, content, htmlContent }))}export const getPostIds = async () => {const fileNames = await fsPromise.readdir(markdownDIR)return fileNames.map(fileName => fileName.replace(/\.md$/g, ''))}export default getPosts
/pages/posts/[id].tsx
import { getPost } from 'lib/posts'import React from 'react'import { NextPage } from 'next';import { getPostIds } from '../../lib/posts';type Post = {id: string;date: string;title: string;content: string,htmlContent: string}type Props = {post: Post}const postsShow: NextPage<Props> = (props) => {const { post } = propsreturn (<div><h1>{post.title}</h1><article dangerouslySetInnerHTML={{ __html: post.htmlContent }}></article></div>)}export default postsShowexport const getStaticPaths = async () => {const idList = await getPostIds()return {paths: idList.map(id => ({ params: { id: id } })),fallback: false}}export const getStaticProps = async (staticContext: any) => {const post = await getPost(staticContext.params.id)return {props: {post: post}}}
[id].tsx
- 步骤
- 实现PostShow,从props接收post数据
- 实现getStaticProps,从第一个参数接受params.id
- 实现getStaticPaths,返回id列表
优化
是否自动兜底
- false表示如果请求的id不再getStaticPaths的结果里,则直接返回404页面
- true表示自动兜底,id找不到依然渲染页面
- 注意id不在结果里不代表id不存在,比如大型项目无法将所有产品页面都静态化,只静态化id对应的页面

