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 dev
Link快速导航
用法
把点击改为
- 点击
优点
- 页面不会刷新,用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>
<meta
name="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
```jsx
export 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; }
```jsx
import 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 = 200
res.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 = 200
res.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 } = props
return (
<div>
<h1>文章列表</h1>
{posts.map(p => <div key={p.id}>
{p.id}
</div>)}
</div>
)
}
export default PostsIndex
export 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 } = props
return (
<div>
<h1>你的浏览器是{browser.name}</h1>
</div>
);
}
export default index
export 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 } = props
return (
<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 PostsIndex
export 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 } = props
return (
<div>
<h1>{post.title}</h1>
<article dangerouslySetInnerHTML={{ __html: post.htmlContent }}>
</article>
</div>
)
}
export default postsShow
export 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对应的页面