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 等技术
同构代码
一份代码运行在两端(省了一份)
- 在组件里写一句 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
// 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
},
}
// 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 里,不运行在浏览器中
- 栗子:
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(')
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 声明在这个函数旁边即可
- 栗子:
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
- 栗子:
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}
}
{post.title}
但是新建的文件叫做什么
- pages/posts/[id].tsx
- 没错,文件名就是 [id].tsx,(约定)
/pages/posts/[id].tsx 的作用
- 既声明了路由 /posts/:id
- 又是 /posts/:id 的页面实现程序
- 妙啊
实现
- 用 getServerSideProps 渲染列表页面
- 详情页用 getStaticProps,从第一个参数接受 params.id
- 用 getStaticPaths 返回 id 列表