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
  • 自动在页面插入新内容,删除旧内容
  • 因为省了很多请求和解析过程,所以速度极快

其他

  • 借鉴了Rails Turbolinks,pjax等技术

    同构代码

    代码运行在两端

  • 在组件里写一句 console.log(‘执行’)

  • Node控制台会输出这句话
  • Chrome控制台也输出这句话

差异

  • 不是所有代码都会运行,有的需要用户触发
  • 不是所有的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 (
    1. <Head>
    2. <title>我的博客</title>
    3. <meta
    4. name="viewport"
    5. content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
    6. ></meta>
    7. </Head>
    8. <Component {...pageProps} />
    ); }

export default MyApp;

  1. <a name="hTniS"></a>
  2. ### 全局CSS
  3. _app.js<br />`import './styles/global.css'`<br />切页面App不会销毁<br />其他组件内不引入CSS,只写局部CSS
  4. <a name="QdGUa"></a>
  5. ### 局部CSS
  6. - style-jsx
  7. ```jsx
  8. export default function Index() {
  9. return (
  10. <div>
  11. <Link href="/posts/first-post">
  12. <a> 第一篇文章! </a>
  13. </Link>
  14. <style jsx>
  15. {`
  16. a {
  17. color: green;
  18. }
  19. `}
  20. </style>
  21. </div>
  22. );
  23. }
  • CSS Modules ```jsx .wrapper { background: #3498db; }

.link { background: gray; }

  1. ```jsx
  2. import styles from 'styles/first-post.module.css'
  3. export default function X() {
  4. return (
  5. <>
  6. <div className={styles.wrapper}>
  7. First Post
  8. <hr />
  9. 回到首页
  10. <Link href="/">
  11. <a className={styles.link}>点击</a>
  12. </Link>
  13. </div>
  14. </>
  15. );
  16. }

绝对引用

jsconfig.json

  1. {
  2. "compilerOptions":{
  3. "baseUrl":"."
  4. }
  5. }

import 'styles/global.css'

使用SCSS

$ yarn add sass
.css改为.scss

静态资源

自定义webpack config

$ yarn add --dev file-loader
next.config.js

  1. module.exports = {
  2. webpack: (config, options) => {
  3. config.module.rules.push({
  4. test: /\.(png|jpg|jpeg|gif|svg)$/,
  5. use: [{
  6. loader: 'file-loader',
  7. options: {
  8. name: '[name].[contenthash].[ext]',
  9. outputPath: 'static', //硬盘路径
  10. publicPath: '/_next/static' //网站路径
  11. }
  12. }],
  13. })
  14. return config
  15. },
  16. }

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

  1. import { NextApiHandler } from "next";
  2. const Posts: NextApiHandler = (req, res) => {
  3. res.statusCode = 200
  4. res.setHeader('Content-Type', 'application/json')
  5. res.write(JSON.stringify({
  6. name: 'Gouson'
  7. }))
  8. res.end()
  9. }
  10. export default Posts

image.png

实现posts API

./markdown/
写两个md文件

  1. ---
  2. title: 我的第一篇博客
  3. date: 2021-7-17
  4. ---
  5. a a abbb是怎么回事呢?a a a相信大家都很熟悉,但是a a abbb是怎么回事呢,下面就让小编带大家一起了解吧。
  6. a a abbb,其实就是ccc,大家可能会很惊讶a a a怎么会bbb呢?但事实就是这样,小编也感到非常惊讶。
  7. 这就是关于a a abbb的事情了,大家有什么想法呢,欢迎在评论区告诉小编一起讨论哦!

$ yarn add gray-matter

./lib/posts.tsx

  1. import fs, { promises as fsPromise } from 'fs'
  2. import path from "path";
  3. import matter from 'gray-matter'
  4. const getPosts = async () => {
  5. const markdownDIR = path.join(process.cwd(), 'markdown')
  6. const fileNames = await fsPromise.readdir(markdownDIR)
  7. const posts = fileNames.map(filename => {
  8. const fullPath = path.join(markdownDIR, filename)
  9. const id = filename.replace(/\.md$/g, '')
  10. const text = fs.readFileSync(fullPath, 'utf8')
  11. const { data: { title, date }, content } = matter(text)
  12. return { id, title, date }
  13. })
  14. return posts
  15. }
  16. export default getPosts

./pages/api/v1/posts.tsx

  1. import getPosts from "lib/posts";
  2. import { NextApiHandler } from "next";
  3. const Posts: NextApiHandler =async (req, res) => {
  4. const posts= await getPosts()
  5. console.log(posts)
  6. res.statusCode = 200
  7. res.setHeader('Content-Type', 'application/json')
  8. res.write(JSON.stringify(posts))
  9. res.end()
  10. }
  11. export default Posts

image.png

小结

  • /api/里的文件是API
    • 一般返回JSON格式的字符串
    • 也可以返回HTML
  • API文件默认导出NextApiHandler
    • 这是一个函数类型
    • 第一个参数是请求
    • 第二个参数是对象

三种渲染方式

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

客户端渲染

只在浏览器上执行的渲染
用JS、vue、react创建HTML

$ yarn add axios
$ yarn add --dev @types/axios
/pages/posts/index.tsx

  1. import { NextPage } from 'next'
  2. import axios from 'axios'
  3. import { useEffect, useState } from 'react';
  4. type Post = {
  5. id: string;
  6. date: string;
  7. title: string;
  8. }
  9. const PostsIndex: NextPage = () => {
  10. const [posts, setPosts] = useState<Post[]>([])
  11. const [isLoading, setIsLoading] = useState(true)
  12. const [isEmpty, setIsEmpty] = useState(false)
  13. useEffect(() => {
  14. axios.get('/api/v1/posts').then(response => {
  15. setPosts(response.data)
  16. setIsLoading(false)
  17. if (response.data.length === 0) {
  18. setIsEmpty(true)
  19. }
  20. }, () => {
  21. setIsLoading(false)
  22. })
  23. }, []);
  24. return (
  25. <div>
  26. <h1>文章列表</h1>
  27. {
  28. isLoading ? <div>加载中</div> :
  29. isEmpty ? <div>暂无文章</div> :
  30. posts.map(p =>
  31. <div key={p.id}>{p.id}</div>
  32. )
  33. }
  34. </div>
  35. )
  36. }
  37. export default PostsIndex

image.png

缺点

  • 白屏
    • 在AJAX得到响应前,页面中后loading
  • SEO不友好

    • 搜索引擎访问页面,看不到Posts数据
    • 因为搜索引擎默认不会执行JS


      静态的部分会在服务器端和客户端渲染两次
  • React SSR

    • 推荐在后端 renderToString()在前端hydrate()
    • hydrate()混合,会保留HTML并附上事件监听
    • 也就是后端渲染HTML,前端添加监听
    • 前端也会渲染一次,以确保前后端渲染一致

      静态页面生成(SSG)

      Static Site Generation,解决白屏问题、SEO问题
      无法生成用户相关内容(所有用户的请求都一样)
      页面静态化,把PHP提前渲染成HTML
  • 每个page在默认导出的函数旁边加上export getStaticProps,写法是固定的

  • 导出函数的第一个参数就是props

/pages/posts/index.tsx

  1. import { NextPage } from 'next'
  2. import getPosts from '../../lib/posts';
  3. type Props = {
  4. posts: Post[]
  5. }
  6. const PostsIndex: NextPage<Props> = (props) => {
  7. const { posts } = props
  8. return (
  9. <div>
  10. <h1>文章列表</h1>
  11. {posts.map(p => <div key={p.id}>
  12. {p.id}
  13. </div>)}
  14. </div>
  15. )
  16. }
  17. export default PostsIndex
  18. export const getStaticProps = async () => {
  19. const posts = await getPosts()
  20. return {
  21. props: {
  22. posts: JSON.parse(JSON.stringify(posts))
  23. }
  24. }
  25. }

image.png

同构

  • 现在前端不用AJAX也能拿到posts了
  • 这就是同构SSR的好处,后端数据可以直接传给前端
  • 前端JSON.parse一下就能得到posts

静态化的时机

  • 环境

    • 在开发环境,每次请求都会运行一次getStaticProps
    • 这是为了方便你修改代码重新运行
    • 生产环境,getStaticProps只在build时运行一次
    • 这样可以提供一份HTML给所有用户下载

      生产环境

    • 关掉yarn dev

    • $ yarn build
    • $ yarn start

image.png

  • 解读
    • λ(Server) SSR不能自动创建HTML
    • ◯(Static)自动创建HTML(没用props)
    • ⚫(SSG)自动创建HTML和JSON(用到了props)
  • 三种文件类型
    • posts.html含有静态内容,用于用户直接访问
    • posts.js也含有静态内容,用于快速导航(与HTML相对应)
    • post.json含有数据,跟post.js结合得到页面

小结

  • 动态内容静态化
    • 如果动态内容和用户午关
    • 通过getStaticProps可以获取数据
    • 静态内容+数据(本地获取)就得到了完整页面
    • 代替了之前的静态内容+动态内容(AJAX获取)
  • 时机
    • 静态化在 yarn build的时候实现
  • 优点

    • 生产环境中直接给出完整页面
    • 首屏不会白屏
    • 搜索引擎能看到页面内容(方便SEO)

      服务端渲染(SSR)

      解决白屏问题、SEO问题
      可以生成用户相关内容(不同用户结果不同)
      PHP、Python、Ruby、Java后台的基本功能
  • 有的比较难提前静态化,比如微博。需要在用户请求时,获取用户id,去数据库拿数据

  • 这时候想要在服务器渲染就不能用getStaticProps,因为是在build阶段执行
  • 要用getServerSideProps(context:NextPageContext)

$ yarn add ua-parser-js
$ yarn add --dev @types/ua-parser-js

/pages/index.tsx

  1. import { GetServerSideProps, NextPage } from "next";
  2. import { UAParser } from "ua-parser-js";
  3. type Props = {
  4. browser: {
  5. name: string,
  6. version: string,
  7. major: string
  8. }
  9. }
  10. const index: NextPage<Props> = (props) => {
  11. const { browser } = props
  12. return (
  13. <div>
  14. <h1>你的浏览器是{browser.name}</h1>
  15. </div>
  16. );
  17. }
  18. export default index
  19. export const getServerSideProps: GetServerSideProps = async (constext) => {
  20. const ua = constext.req.headers['user-agent']
  21. const { browser } = new UAParser(ua).getResult()
  22. return {
  23. props: {
  24. browser
  25. }
  26. }
  27. }

image.png

getServerSideProps

  • 运行时机
    • 无论是开发环境还是生产环境
    • 都是在请求到来后运行getServerSideProps
  • 参数

    • context,类型为NextPageContext
    • context.req/context.res可以获取请求和响应

      三种渲染的总结

  • 静态内容

    • 直接输出HTML,没有术语
  • 动态内容
    • 术语:客户端渲染,通过AJAX请求
  • 动态内容静态化
    • 术语:SSG,通过getStaticProps获取用户无关内容
  • 用户相关动态内容静态化

    • 术语:SSR,通过getServerSideProps获取请求
    • 缺点:无法获取客户端信息,比如比如浏览器窗口大小

      渲染方式的选择

      image.png
  • 有动态内容吗?没有什么都不用做,自动渲染为HTML

  • 动态内容和客户端相关吗?相关就只能用客户端渲染(BSR)
  • 动态内容跟请求/用户相关吗?相关就只能用服务端渲染(SSR)或BSR
  • 其他情况可以用SSG或SSR或BSR

升级版SSG

/posts/index.tsx

  1. import { NextPage } from 'next'
  2. import Link from 'next/link'
  3. import getPosts from '../../lib/posts';
  4. type Post = {
  5. id: string;
  6. date: string;
  7. title: string;
  8. }
  9. type Props = {
  10. posts: Post[]
  11. }
  12. const PostsIndex: NextPage<Props> = (props) => {
  13. const { posts } = props
  14. return (
  15. <div>
  16. <h1>文章列表</h1>
  17. {posts.map(post => <div key={post.id}>
  18. <Link href="/posts/[id]" as={`/posts/${post.id}`}>
  19. <a>{post.title}</a>
  20. </Link>
  21. </div>)}
  22. </div>
  23. )
  24. }
  25. export default PostsIndex
  26. export const getStaticProps = async () => {
  27. const posts = await getPosts()
  28. return {
  29. props: {
  30. posts: JSON.parse(JSON.stringify(posts))
  31. }
  32. }
  33. }

/lib/posts.tsx

  1. import fs, { promises as fsPromise } from 'fs'
  2. import path from "path";
  3. import matter from 'gray-matter'
  4. import marked from 'marked'
  5. const markdownDIR = path.join(process.cwd(), 'markdown')
  6. const getPosts = async () => {
  7. const fileNames = await fsPromise.readdir(markdownDIR)
  8. const posts = fileNames.map(filename => {
  9. const fullPath = path.join(markdownDIR, filename)
  10. const id = filename.replace(/\.md$/g, '')
  11. const text = fs.readFileSync(fullPath, 'utf8')
  12. const { data: { title, date }, content } = matter(text)
  13. return { id, title, date }
  14. })
  15. return posts
  16. }
  17. export const getPost = async (id: string) => {
  18. const fullPath = path.join(markdownDIR, id + '.md')
  19. const text = fs.readFileSync(fullPath, 'utf8')
  20. const { data: { title, date }, content } = matter(text)
  21. const htmlContent = marked(content)
  22. return JSON.parse(JSON.stringify({ id, title, date, content, htmlContent }
  23. ))
  24. }
  25. export const getPostIds = async () => {
  26. const fileNames = await fsPromise.readdir(markdownDIR)
  27. return fileNames.map(fileName => fileName.replace(/\.md$/g, ''))
  28. }
  29. export default getPosts

/pages/posts/[id].tsx

  1. import { getPost } from 'lib/posts'
  2. import React from 'react'
  3. import { NextPage } from 'next';
  4. import { getPostIds } from '../../lib/posts';
  5. type Post = {
  6. id: string;
  7. date: string;
  8. title: string;
  9. content: string,
  10. htmlContent: string
  11. }
  12. type Props = {
  13. post: Post
  14. }
  15. const postsShow: NextPage<Props> = (props) => {
  16. const { post } = props
  17. return (
  18. <div>
  19. <h1>{post.title}</h1>
  20. <article dangerouslySetInnerHTML={{ __html: post.htmlContent }}>
  21. </article>
  22. </div>
  23. )
  24. }
  25. export default postsShow
  26. export const getStaticPaths = async () => {
  27. const idList = await getPostIds()
  28. return {
  29. paths: idList.map(id => ({ params: { id: id } })),
  30. fallback: false
  31. }
  32. }
  33. export const getStaticProps = async (staticContext: any) => {
  34. const post = await getPost(staticContext.params.id)
  35. return {
  36. props: {
  37. post: post
  38. }
  39. }
  40. }

image.png

[id].tsx

  • 步骤
    • 实现PostShow,从props接收post数据
    • 实现getStaticProps,从第一个参数接受params.id
    • 实现getStaticPaths,返回id列表
  • 优化

    • 使用marked得到markdown的HTML内容
    • $ yarn add marked
    • $ yarn add --dev @types/marked

      fallback:false的作用

  • 是否自动兜底

  • false表示如果请求的id不再getStaticPaths的结果里,则直接返回404页面
  • true表示自动兜底,id找不到依然渲染页面
  • 注意id不在结果里不代表id不存在,比如大型项目无法将所有产品页面都静态化,只静态化id对应的页面