Node-web03_02Next

next深入

使用 TypeScript 开发 Next

1
2
3
4
5
# 全局安装 tsx
yarn global add typescript@3.9.2
# 安装成功后会有两个命令
- tsc
- tsserver
  • 创建 tsconfig.json tsc --init
  • 把 jsconfig.json内容 合并到 tsconfig.json

    1
    2
    3
    4
    5
    {
    "compilerOptions": {
    "baseUrl": "."
    }
    }
  • 安装类型声明文件 yarn add --dev typescript @types/react @types/react-dom @types/node

    • 你就可以用ts开发 react和 node了
  • 重启 yarn dev
  • 结果报错了,找不 xx.png
  • 修改类型声明文件next-env.d.ts
1
2
3
4
declare module "*.png" {
const value: string;
export default value;
}
  • 把js文件都修改为 tsx后缀

强烈建议加上一句配置 tsconfig.json里

1
2
# 禁用隐式 any
noImplicitAny:"true"

Next.js API

如何返回 JSON

  • 新建 pages/api/v1/posts.tsx
1
2
3
4
5
6
7
8
9
10
import { NextApiRequest, NextApiResponse } from "next";

const Posts = (req: NextApiRequest, res: NextApiResponse) => {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.write(JSON.stringify({ name: 'hjx' }))
res.end()
}

export default Posts

你还可以这样写

1
2
3
4
5
6
7
8
9
10
import { NextApiHandler } from "next";

const Posts:NextApiHandler = (req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'application/json')
res.write(JSON.stringify({ name: 'hjx' }))
res.end()
}

export default Posts

读取markdown文件

  • step01 在 markdown/ 目录建立几个文章 ,形如
1
2
3
4
5
6
---
title: hjx的博客
date: 2021-01-30
---

hjx发布的博客 内容1111
  • step02 安装 matter yarn add gray-matter

  • step03 新建功能函数 getPosts lib/posts.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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, 'utf-8');
const { data: { title, date }, content } = matter(text);
return { id, title, date, content }
})
return posts;
}

export default getPosts;
  • step04 新建 pages/api/v1/posts2.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
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

Next.js的三种渲染方式

  • 客户端渲染 BSR
    • 只在浏览器上执行的渲染
  • 静态页面生成 SSG
    • Static Site Generation,解决白屏问题,SEO问题
    • 无法生成用户相关内容(所有用户请求的结果都一样)
  • 服务器渲染 SSR
    • 解决白屏问题,SEO问题
    • 可以生成用户相关的内容(不同用户不同结果)

SSG和 SSR都属于预渲染 Pre-rendering

三种方式实际以前就有

  • BSR —— 客户端渲染 vue/react创建 html
  • SSG —— 页面静态化 把PHP提前渲染成 HTML
  • SSR —— 大部分后台语言java,python 如JSP

不同点

Next.js 的预渲染可以和前端 React无缝对接

客户端渲染 BSR

  • 安装 axios yarn add axios
  • yarn add --dev @types/axios
  • 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
import { NextPage } from "next";
import axios from 'axios';
import { useEffect, useState } from "react";

type Posts = {
id: string;
date: string;
title: string;
content: string;
}
const PostsIndex: NextPage = () => {
const [posts, setPosts] = useState<Posts[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isEmpty, setIsEmpty] = useState(false);
useEffect(() => {
setIsLoading(true)
axios.get('/api/v1/posts2').then(response => {
setTimeout(() => {
setPosts(response.data)
if (response.data.length === 0) {
setIsEmpty(true);
}
setIsLoading(false)
}, 3000);
})
}, [])
return (
<div>
Post Index
{isLoading ? <div>加载中...</div> :
isEmpty ? <div>没有文章!</div> :
posts.map(post => {
return (<div key={post.id}>
{post.id}
</div>)
})
}
</div>
)
}

export default PostsIndex;

优化代码:自定义Hooks

  • hooks/usePosts.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
import { useState, useEffect } from "react";
import axios from 'axios';

type Posts = {
id: string;
date: string;
title: string;
content: string;
}

export const usePosts = () => {
const [posts, setPosts] = useState<Posts[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isEmpty, setIsEmpty] = useState(false);
useEffect(() => {
setIsLoading(true)
axios.get('/api/v1/posts2').then(response => {
setTimeout(() => {
setPosts(response.data)
if (response.data.length === 0) {
setIsEmpty(true);
}
setIsLoading(false)
}, 3000);
})
}, [])
return {
posts,
setPosts,
isLoading,
setIsLoading,
isEmpty,
setIsEmpty
}
}

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
import { usePosts } from "hooks/usePosts";
import { NextPage } from "next";

const PostsIndex: NextPage = () => {
const { isLoading, isEmpty, posts } = usePosts();
return (
<div>
Post Index
{isLoading ? <div>加载中...</div> :
isEmpty ? <div>没有文章!</div> :
posts.map(post => {
return (<div key={post.id}>
{post.id}
</div>)
})
}
</div>
)
}

export default PostsIndex;

客户端渲染的缺点

  • 白屏
    • ajax请求结果之前,显示的是 loading
  • SEO不友好
    • 搜索引擎不友好,看不到 posts数据
    • 因为搜索引擎不会执行JS只会看到HTML

问题分析,上面的渲染方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { usePosts } from "hooks/usePosts";
import { NextPage } from "next";

const PostsIndex: NextPage = () => {
const { isLoading, isEmpty, posts } = usePosts();
return (
<div>
Post Index
{isLoading ? <div>加载中...</div> :
isEmpty ? <div>没有文章!</div> :
posts.map(post => {
return (<div key={post.id}>
{post.id}
</div>)
})
}
</div>
)
}

export default PostsIndex;
  • 是服务器渲染的还是客户端渲染的?
  • 渲染了几次?一次还是两次?
参考 React SSR的官网
  • 推荐 在后端renderToString()在前端 hydrate()
  • hydrate() 混合 ,会保留HTML附上事件监听
  • 后端渲染 html,前端比对一下,有监听,前端添加监听事件
  • 前端也会渲染一次,用以确保前后端渲染结果一致

SSG 页面静态生成

  • 之前的BSR 每个人访问文章列表都是一样的
    • 每次访问 BSR 页面都会发 ajax请求 一样的数据,然后由前端渲染
  • 为什么? 不在后端渲染好,然后发给每个人
  • N次渲染变成了一次
  • N次ajax请求没了
  • N次客户端渲染变成了一次静态页面生成
  • 这个过程 动态内容静态化

  • pages/posts/index_ssg.tsx

    • 新建 getStaticProps 函数,返回 {props:{posts:post}}]}
1
2
3
4
5
6
7
8
9
10
11
//格式要求,函数名称必须是 getStaticProps
// 而且必须 export 导出
export const getStaticProps = async () => {
const posts = await getPosts() // 你的内容
// 返回格式
return {
props: {
posts: JSON.parse(JSON.stringify(posts))
}
}
}
  • pages/posts/index_ssg.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
import { Post } from "hooks/usePosts";
import getPosts from "lib/posts";
import { NextPage } from "next";

interface Props {
posts: Post[]
}
const PostsIndex: NextPage<Props> = (props) => {
// 这个 posts是 前端/后端 都可以拿到的
const { posts } = props;
console.log(posts)
return (
<div>
Post Index(SSG)
{posts.map(p => {
return (
<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))
}
}
}
1
2
// 你会发现页面会有个如下标签把数据封装了
<script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"posts":[{"id":"第一篇博客","title":"hjx的博客","date":"2021-01-30T00:00:00.000Z","content":" \n\nhjx发布的博客 内容1111"},{"id":"第二篇博客","title":"hjx的博客2","date":"2021-01-30T00:00:00.000Z","content":" \n\nhjx发布的博客2 内容22222"}]},"__N_SSG":true},"page":"/posts/index_ssg","query":{},"buildId":"development","nextExport":false,"isFallback":false,"gsp":true}</script>

惊叹的发现

  • 前端不用ajax也能拿到数据 posts
  • 这就是 SSG的好处:后端数据直接给前端
  • 前端 JSON.parse 就可以得到 posts

为啥 java/php/python做不到?

  • 也可以做到,思路一样
  • 但是他们不支持JSX,很难和React无缝对接
  • 而且他们的对象不能直接给JS用,需要类型转换

生产环境静态化示例

  • yarn build
  • 项目根目录 .next/server就是 我们静态化的内容
  • 如果运行 build后的页面 yarn start

解读

1
2
3
λ  (Server) SSR 不能自动创建HTML
○ (Static) 自动创建 HTML
● (SSG) 自动创建 HTML JS JSON

三种文件类型

  • posts.html含有静态内容,用于客户直接访问
  • posts.js 含有静态内容,用于 快速导航
    <Link href="/posts/index_bsr">posts BSR</Link>
    <Link href="/posts/index_ssg">posts SSG</Link>
    
    • 就会预先加载 快速导航里页面对应的js index_bsr.js 和 index_ssg.js
  • posts.json 含有数据, 和 posts.js结合得到页面

为什么不直接把 数据 放入 posts.js 呢?

SSG总结

  • 动态内容静态化
  • 如果动态内容和用户无关,可以提前静态化
  • 通过 getStaticProps 可以获取数据
  • 静态内容(JS) + 数据(JSON) 就得到完整页面
  • 代替了之前 BSR的 静态内容 + 动态内容 (AJAX获取)

时机

静态化是在 yarn build 时候实现的

优点

  • 生产环境直接给出完整页面
  • 首屏不会有白屏
  • 有利于搜索引擎(SEO)