Next.js 的背景

开发团队是 zeit

  • zeit 团队水平如何。后改名为 Vercel
  • 简言之,一个高中开始编程的、会做平面设计的复旦大学计算机专业毕业生,在微软工作一年后,加入了 zeit 团队
  • 几乎每一个同事都有非常强大的背景
  • Next,js 核心团队四个人平均年龄 20岁
  • 按 star 数,zeit 是 GitHub 组织的 Top 20
  • 全员远程工作

Next.js 的定位

Node.js 全栈框架

  • CSS-inJS
  • 页面预渲染 + SSR(服务端渲染)
  • 前后端同构(代码同时运行在两端)
  • Node.js 10.13 以上
  • 支持 React,与 React 无缝对接
  • 支持 TypeScript

弱项

  • 完全没有提供数据库相关功能,可行搭配 Sequelize 或 TypeORM
  • 完全没有提供测试相关功能,可自行搭配 Jest 或 Cypress
  • 有一个叫做 Blitz.js 的框架在这些方向上努力

Link 快速导航

用法

优点

  • 页面不会刷新,用 AJAX 请求新页面内容
  • 不会请求重复的HTML、CSS、JS
  • 自动在页面插入新内容、删除旧内容
  • 因为省了很多请求和解析过程,所以速度极快

吐槽

  • 借鉴了 Rails Turbolinks、pjax 等技术

传统导航,访问 page2 时是浏览器请求

相比传统导航,访问 page2 时是 page1 用AJAX请求页面(在network中可见)

同构代码

一份代码运行在两端(省了一份)

  • 在组件里写一句 console.log(‘执行了’)
  • 你会发现 Node 控制台会输出这句话
  • 同样你会发现 Chrome 控制台也会输出这句话

注意差异

  • 不是所有代码都会运行,有些需要用户触发
  • 不是所有的 API 都能用,比如 window 在 Node 里报错

全局配置

pages/_app.js

  • export default function App 是每个页面的根组件
  • 页面切换时 App 不会销毁,App 里面的组件会销毁
  • 可用 App 保存全局状态

注意

  • 创建 _app.js 之后需要重启服务(yarn dev)

全局 CSS

放在 _app.js 里

  • import ‘../styles/global.css’
  • 因为切换页面时 App 不会销毁
  • 其他地方不能 import global.css
  • 其他地方只能写局部 CSS

插曲

  • 相对引用好烦,能改成 import ‘style/global.css’吗?
  • 文档 Absolute Import 章节(baseUrl: ‘.’)

局部 CSS

官方支持

  • 默认支持 styled-jsx 和 CSS Modules
  • 一般来说,简单需求用前者,复杂需求用后者

React 个人体验

  • styled-jsx 不方便分离 CSS 和 JS
  • CSS Modules 用起来太麻烦
  • styled-components 用起来顺手

静态资源

next 推荐放在 public 里

  • 个人觉得不太好
  • 因为放在 public 里不支持改文件名(如哈希)

所以需要配置自定义 webpack config

  • 创建 next.config.js
  • 栗子🌰:使用 file-loader 或者 next-images
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// file-loader 
module.exports = {
webpack: (config, options) => {
config.module.rules.push({
test: /\.(jpg|png|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[contenthash].[ext]', // 文件名称
outputPath: 'static', // 硬盘路径
publicPath: '_next/static', // 网站路径
}
},
],
})

return config
},
}
1
2
3
4
5
6
7
// next-images
const withImages = require('next-images')
module.exports = withImages({
webpack(config, options) {
return config
}
})

启用 Typescript

创建 tsconfig.json

  • tsc –init 运行后得到 tsconfig.json 或者 touch tsconfig.ts
  • 将 jsconfig.json 里面的配置合并到 tsconfig.json
  • 删除 jsconfig.json

重启服务 yarn dev

  • 会自动改写 tsconfig.json
  • 更改文件名后缀由 .js 改为 .tsx
  • 不需要一次性将所有文件全部改完
  • 在 tsconfig.json 里添加
  • “noImlicitAny”: true (禁用隐式的 any)

Next.js API

目前的页面

  • index 和 posts 都是 HTML
  • 但实际开发中我们需要请求 /user /shops 等 API
  • 但返回的内容是 JSON 格式的字符串
  • 使用 Next.js API
  • 路径为 /api/v1/posts 以便与 /posts 区分开来
  • 默认导出的函数的类型为 NextApiHandler
  • 该代码只运行在 Node.js 里,不运行在浏览器中
  • 栗子🌰:
    1
    2
    3
    4
    5
    6
    7
    8
    const posts: NextApiHandler = async (req, res) => {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json');
    const posts = await getPosts() // 数据库操作
    res.end(JSON.stringify(posts))
    }

    export default posts

API

/api/ 里的文件是 API

  • 一般返回 JSON 格式的字符串
  • 但也不是不能返回 HTML, 比如 res.end(‘<html’></html’>)

API 文件默认导出 NextAPIHandler

  • 这是一个函数类型
  • 第一个参数是请求
  • 第二个参数是对象
  • 因为 Next.js 是基于 Express 的,所以支持 Express 的中间件,下文在分析,官方文档

Next.js 三种渲染方式

客户端渲染

  • 只在浏览器上执行的渲染

静态页面生成(SSG)

  • Static Site Generation,解决白屏问题、SEO问题
  • 无法生成用户相关的内容(所有用户请求的结果都一样)

服务端渲染(SSR)

  • 解决白屏问题、SEO 问题
  • 可以生成用户相关内容(不同用户结果不同)

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

旧瓶装新酒

三种渲染方式分别对应

  • 客户端渲染 – 用 JS、Vue、React 创建 HTML
  • SSG – 页面静态化,把 PHP 提前渲染成HTML
  • SSR – PHP、Python、Ruby、Java 后台的基本功能

与传统的后端不同点

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

客户端渲染的缺点

白屏

  • 在 AJAX 得到相应之前,页面中 Loading

SEO 不友好

  • 搜索引擎访问页面,看不到 AJAX 得到的数据
  • 因为搜索引擎默认不会执行 JS,只能看到 HTML

静态内容 VS 动态内容

上图的静态内容

  • 是服务渲染的,还是客户端渲染的?
  • 渲染了几次?一次还是两次?

参考 React SSR 的官方文档

  • 推荐在后端 renderToString() 在前端 hydrate()
  • hydrate() 混合,会保留 HTML 并附上事件监听
  • 也就是说后端渲染 HTML,前端添加监听
  • 前端也会渲染一次,用以确保前后端渲染结果一致(如何看出渲染了两次,当使用 styled-conponents 时会有报错)

推论

  • 所有页面至少有一个标签是静态内容,由服务端渲染

静态页面生成(SSG)

背景

  • 你有没有想过,其实每个人看到的文章列表都是一样的
  • 那么为什么还需要在每个人的浏览器渲染一次
  • 为什么不在后端渲染好,然后发给每个人
  • N 次渲染变成了 1 次渲染
  • N 次客户端渲染变成了1 次静态页面生成
  • 这个过程叫做动态内容静态化

getStaticProps 获取 posts

声明位置

  • 每个 page 不是默认导出一个函数么?
  • 把 getStaticProps 声明在这个函数旁边即可
  • 栗子🌰:
1
2
3
4
5
6
7
8
export const getStaticProps: GetStaticProps = async () => {
const posts = await getPosts()
return {
props: {
posts,
}
}
}

必须按照这个格式,不能变(命名和返回值{ props: {…} })

打开控制台我们可以清楚的看见,原来我们需要通过 AJAX 的内容,直接被打包进 HTML 里面了,这样浏览器不需要用 AJAX 就可以直接拿到数据了!

这就是同构 SSR 的好处:后端数据可以传给前端

前端 JSON.parse 一下就能够得到了 posts(现在 Next.js 帮你做了)

难道 PHP / Java / Python 就做不到么

  • 其实也可以做到,思路一样
  • 但是他们不支持 JSX,很难与 Reactr 无缝对接
  • 而且他们的对象不能直接提供 JS 用(他们又有 int 之类的类型),需要类型转换

静态化的时机

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

解读打包文件

  • λ (Server) server-side renders at runtime (uses getInitialProps or getServerSideProps)
  • ○ (Static) automatically rendered as static HTML (uses no initial props)
  • ● (SSG) automatically generated as static HTML + JSON (uses getStaticProps)

动态内容静态化

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

优点

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

getServerSideProps

用户相关动态内容

就难提前静态化

  • 需要在用户请求时,获取用户信息,然后通过用户信息去数据库拿数据
  • 如果因要做,就要给每个用户都提前创建一个页面(占内存,麻烦)
  • 但还有时候这些数据更新极快,无法提前静态化
  • 比如微博首页的信息流

所以

  • 要么客户端渲染,下拉更新(1)
  • 要么服务端渲染,下拉更新(2)
  • 但这次的服务端渲染不能用 getStaticProps
  • 因为 getStaticProps 是在 build 时执行的
  • 所以要用 getServerSideProps

运行时机

  • 无论是开发环境还是生产环境
  • 都是在请求到来之后运行 getServerSideProps
  • 与 getStaticProps 区别,build 时运行一次

参数

  • context,类型为 NextPageContext
  • context.req / context.res 可以获取请求和响应
  • 一般只需要用到 context.req
  • 栗子🌰:
1
2
3
4
5
6
7
8
export const getServerSideProps: GetServerSideProps = async (context) => {
const ua = context.req.headers["user-agent"]
return {
props: {
ua,
}
}
}

必须按照这个格式,不能变(命名和返回值{ props: {…} })

这个栗子展示了用户访问的浏览器,这些信息我们不可能提前(在用户请求之前)知道

总结

静态内容

  • 直接输出 HTML,没有术语

动态内容

  • 术语:客户端渲染,通过 AJAX 请求,渲染成 HTML

动态内容静态化

  • 术语:SSG,通过 getStaticProps 获取用户无关内容

用户相关动态内容静态化

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

流程图

有动态内容吗?没有什么都不用做,自动渲染为 HTML
动态内容跟客户端相关?相关就只能用客户端渲染(BSR)
动态内容跟请求/用户相关吗?相关就只能用服务端渲染(SSR)或 BSR
其他情况可以用 SSG 或 BSR

补充:路由的另一个功能

点击列表查看详情功能

  • 简单,不就是加个 Link>a 标签吗
  • href={/post/${id}}
    1
    2
    3
    <Link href="/posts/[id]" as={`/posts/${post.id}`}>
    <a>{post.title}</a>
    </Link>

但是新建的文件叫做什么

  • pages/posts/[id].tsx
  • 没错,文件名就是 [id].tsx,(约定)

/pages/posts/[id].tsx 的作用

  • 既声明了路由 /posts/:id
  • 又是 /posts/:id 的页面实现程序
  • 妙啊

实现

  • 用 getServerSideProps 渲染列表页面
  • 详情页用 getStaticProps,从第一个参数接受 params.id
  • 用 getStaticPaths 返回 id 列表