Nextjs 初探

2,515 阅读11分钟

前言

背景请参考 Nodejs 服务端框架调研 内容。从数据上来看极其优秀的 Nextjs,是否适合做 BFF,甚至是做其他类型的服务端?

先给答案吧:不符合笔者的需求。最主要的原因是它跟 React 绑定太深了,我们的项目既有 React,又有 Vue,从这点来说基本上就已经不符合需求了。而且如果单从教程来看,绝大多数篇幅都是在介绍前后端分离基础下的一些偏前端的功能,有关后端服务的内容只有 API Routes 的部分提到了一些,而且少得可怜。由此就可以看出,Nextjs 的定位其实更偏前端,有点 React + Serverless 的意思。

不过,Nextjs 仍然是一个非常好用的 React 全栈框架,可以简单粗暴的理解为:Nextjs = CRA + React-Router + Nodejs Server。即使不需要服务端的功能,也可以拿 Nextjs 当 React 的脚手架来使用,而且非常方便。既然文章都开了,我们就来探索一下 Nextjs 的设计理念,看是否能够获得一些借鉴和启发。

话说 Nextjs 的教程写的是真详细,从 React 讲起,即使零基础的同学也能看得懂,而且原理和步骤都非常详细,完全可以把它当做 React 的入门使用教程了。即使是会用 React 的同学,再看一遍也会有新的收获,真的良心。

简介

Nextjs 官网 的介绍:

The React Framework for Production
Next.js gives you the best developer experience with all the features you need for production: hybrid static & server rendering, TypeScript support, smart bundling, route pre-fetching, and more. No config needed.
谷歌翻译:
Next.js 为您提供生产所需的所有功能的最佳开发人员体验:混合静态和服务器渲染、TypeScript 支持、智能捆绑、路由预取等。无需配置。

嗯……好像说了什么,又好像什么都没说……还是来看一下它的功能吧:

Next.js aims to have best-in-class developer experience and many built-in features, such as:

  • An intuitive page-based routing system (with support for dynamic routes)
  • Pre-rendering, both static generation (SSG) and server-side rendering (SSR) are supported on a per-page basis
  • Automatic code splitting for faster page loads
  • Client-side routing with optimized prefetching
  • Built-in CSS and Sass support, and support for any CSS-in-JS library
  • Development environment with Fast Refresh support
  • API routes to build API endpoints with Serverless Functions
  • Fully extendable

Next.js is used in tens of thousands of production-facing websites and web applications, including many of the world's largest brands.

就不逐字翻译了,大概就是对标了上面说过的几大块功能:

  • React-Router:直观的基于页面的路由系统;客户端路由;
  • CRA:SSG;自动实现页面级别的 Code Split;内置的 Sass、CSS-in-JS;开发模式下的热更新;
  • Nodejs Server:SSR;API Serverless 路由功能;

这下算是有些概念了,再查看一下它的 package.json 文件,依赖了 React 全家桶、Express、Webpack 以及各种工程化的工具,可以算是一个「大杂烩」了,看来 Nextjs 的野心不小啊,貌似是想做一个大而全的工具。我们接下来主要来探究一下它如何把这些功能整合到一起的,是如何设计的,应该对我们的日常开发有所启发。

分析

工程化

跳过 JS 和 React 的基本概念介绍,教程有一个 How Next.js Works 模块,多是跟工程化相关的内容。开篇就提到:

  • In the development stage, Next.js optimizes for the developer and their experience building the application. It comes with features that aim to improve the Developer Experience such the TypeScript and ESLint integration, Fast Refresh, and more.
  • In the production stage, Next.js optimizes for the end-users, and their experience using the application. It aims to transform the code to make it performant and accessible.
  • 开发阶段的目标用户是开发者,目的是优化他们开发应用的体验,比如 TS、ESlint、快速热更新等。
  • 生产阶段的目标用户是终端用户,目的是优化他们的使用体验,比如将源代码转化的更高效更易用。

这可以说是前端工程化的最基本认知了,但也得说总结的很好,有了这两条作为纲领,方向就清晰了很多。

之后的小节内容又分为:Compiling(编译)、Minifying(压缩)、Bundling(打包)、Code Splitting(代码拆分)、Build Time vs. Runtime(构建时 vs. 运行时)、Client and Server(客户端和服务端)、Rendering(渲染)、CDNs and the Edge(CDN)。

整个链路拆分的非常清晰,专业。这个链路无论对于学习还是模块划分,都有非常大的借鉴意义。虽然每块的内容有多有少,但是最关键的还是这个路径本身,它支撑了整个 Nextjs。

脚手架

接下来就是代码实战部分了,我们可以发现,每个小节前面都会有一个帮你马上开始的命令,如下:

npx create-next-app nextjs-blog --use-npm --example "https://github.com/vercel/next-learn/tree/master/basics/learn-starter"

首先,命令跟 CRA 很像,可以说是一模一样。然后我们主要关注 --example 后面的参数,显然这是一个 Github 地址,托管的肯定是代码。我们点进去看一下,发现就是完成前一步教程之后,项目应该的样子,相当于教程进度的「快照」。

所以像 CRA、CNA 这类的脚手架,就是靠这种「笨方法」实现的多种模板的支持,这个方法可以借鉴,尤其是写工具类的同学们。

路由

静态路由

路由可以分为实现跳转,两方面来看。

Nextjs 路由的实现,采用了约定式的设计。既读取 /pages/ 目录下的内容自动生成路由,所以目录和文件的名字不能瞎起。

另外,/public/ 目录下的静态资源,也可以算得上是一种路由。所以千万要注意 publicpages 下面的内容命名冲突的问题。如果不知道这点,是很有可能造成迷惑的。

至于这样设计的原因,笔者以为,Nextjs 和 Nuxtjs 这种支持 SSR 的框架,需要同时考虑前端和后端路由方案的实现。而二者的实现,起码在代码上还是有很大差异的。所以用「目录结构」作为媒介,来统一前后端路由的实现,不失为一个好的选择。

Nextjs 路由的使用,提供了 Link 组件,即 next/linkLink 标签实际上就是实现了客户端跳转,让跳转更加的丝滑,不需要向后端发送额外的请求。但是如果直接使用 a 标签,跳转时就会出现短时间白屏的情况了,这就证明此时的页面请求走了服务端,我们通常都不会太采用。

值得一提的是,Link 标签内可以包裹 a 标签,但此时的 a 标签已经「废」了,不会走服务端跳转了,失效了,而是只剩下了 Link 的跳转方式。此时加个 a 标签 只是为了能够调整超链接的样式,仅此而已。

动态路由

教程中的 Dynamic Routes,涉及到 getStaticPathsgetStaticProps,实际上更偏服务端的应用,是在编译时候执行的方法,属于约定的固定用法。我们可以先不用太关注,主要关注动态路由是怎么匹配的。这个在教程里没有,只能看 文档 了,内容倒也不复杂。主要有三种类型,下面直接展示一下三种类型共存时的优先级(越排在前面优先级越高),大家就一目了然了:

  1. Predefinedpages/post/create.js - Will match /post/create
  2. Dynamic Routespages/post/[pid].js - Will match /post/1/post/abc, etc. But not /post/create
  3. Catch All Routespages/post/[...slug].js - Will match /post/1/2/post/a/b/c, etc. But not /post/create/post/abc

组件当中可以使用 next/router 提供的 useRouter 来获取 URL 参数,也比较好理解。

import { useRouter } from 'next/router'

const Post = () => {
  const router = useRouter()
  const { pid } = router.query

  return <p>Post: {pid}</p>
}

export default Post

所以,Nextjs 就是用特殊文件名的方式来实现动态路由的,貌似有的框架用的是 _ 开头的文件名,这个就看框架的约定了。

代码拆分

Nextjs 自动以页面维度(pages 目录下的内容)进行了代码拆分,而且是懒加载。最关键的,如果发现当前页面有 Link 标签,会在空闲的时候自动预加载 Link 的目标页,这样可以让跳转的体验更加的顺滑,还可以降低首次访问的等待时间,这个做法还是很极致的。

看了下 Nextjs 的依赖,果然有 Webpack,这就不奇怪了,用 Webpack 实现代码拆分还是比较方便的,Nextjs 只是帮我们省了配置这步工作,倒也算是方便了。

特殊标签

用 CRA 或者自己用 Webpack 构建的项目,是会有一个 html 文件的。但是 Nextjs 没有,直接就是 /pages/index.js 作为入口。

这就有问题了,假设我们需要用 umd 的形式引用一个第三方 js,或者简单的设置 titlemata 标签,要怎么做呢?

Nextjs 的解决方法是提供 next/headnext/script 组件(标签)来解决问题。前者的内部可以像普通 head 标签一样写 link、meta、title 等标签;后者使用跟普通 script 标签 也差不多,还多了 onXXX 的几个回调,具体可以看 文档

但是,这样理论上每个页面都需要写,于是在教程里就有了 /components/layout.js,在里面一次性写上这些内容,然后每个 pages 下的组件都需要用 layout 包裹。虽然相比普通项目的直接修改 html 复杂了很多,但是也可以理解。并且通常的项目里都会有这样一个 layout 组件的,所以这样做还算是可以接受。

但是 html 标签怎么办呢?我们通常会这样写 <html lang="en">。Nextjs 的解决方法如下:

If you want to customize the <html> tag, for example to add the lang attribute, you can do so by creating a pages/_document.js file. Learn more in the custom Document documentation.

好吧,为了消灭这个入口 html,需要打的补丁还真不少。但是消灭 html 的必要性是什么呢?暂时笔者还没有答案,希望知道的同学给提个醒。

值得一提的还有 next/image 标签,为图片提供了懒加载、自适应等功能,看起来还是很方便的。

样式

样式隔离

import styles from './layout.module.css';

export default function Layout({ children }) {
  return <div className={styles.container}>{children}</div>;
}

从教程里给的 案例 可以看出,Nextjs 是内置了 CSS Modules 的。约定,只要是 xxx.module.css 就自动是 CSS Modules 的处理方式,使用方式也不用多说了,我们重点来看下全局样式的处理。

全局样式

从教程给的 案例 来看,需要新建一个特殊的 /pages/_app.js 文件,然后引入一个 CSS 就可以了。

// `pages/_app.js`
import '../styles/global.css';

export default function App({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

关键就在这个 _app.js 了,一看名字就知道,又是一个特殊文件,它的 文档 是这样描述它的功能的:

Next.js uses the App component to initialize pages. You can override it and control the page initialization and:

  • Persist layouts between page changes
  • Keeping state when navigating pages
  • Custom error handling using componentDidCatch
  • Inject additional data into pages
  • Add global CSS

感觉上面说的 head 和 script 可以写在这里啊,不需要写在 layout 里了啊……好吧,这里我必须得吐槽一下了。

总之,Nextjs 利用内置的 CSS Module 来解决样式冲突的问题。全局样式就得利用一个特殊的 _app.js 文件来处理了。嗯……好吧,又是一个约定。

预渲染

Nextjs 的预渲染有两种方式,SSG 和 SSR,概念如下:

  • Static Generation (SSG) is the pre-rendering method that generates the HTML at build time. The pre-rendered HTML is then reused on each request.
  • Server-side Rendering (SSR) is the pre-rendering method that generates the HTML on each request.

简单来说,SSG 只需要在 build 阶段一次性获取到需要的数据,然后直接生成静态 html 文件即可,可以很好的解决前后端分离造成的一些问题。需要使用 getStaticProps 方法在 build 阶段获取需要的数据,该方法的逻辑不会在浏览器端执行。SSG 在性能上要优于 SSR,但是数据实时性上就不行了。主要的应用场景如下:

  • Marketing pages
  • Blog posts
  • E-commerce product listings
  • Help and documentation

SSR,每次渲染页面都需要服务端去读取数据,然后在服务端动态生成 html,类似于后端 View 模板的形式,也可以解决前后端分离的问题。与之匹配的方法是 getServerSideProps,该方法只会在服务端运行,而不会在 build 和浏览器端运行。教程里面没有过多介绍,可以查看 文档 了解更多。

与 SSG 和 SSR 相对的,是 CSR,即 Client-side Rendering。也就是我们常说的前后端分离,用 JS 生成页面的方式,这块就不展开了。

总结

BFF 的定义是服务前端的后端,如果单从这个定义来看,Nextjs 其实很符合。它有大量的功能都是在为前端服务,SSG、SSR、Pre-Rendering 等。

可惜笔者的需求,更偏「后端」一点,比如接口聚合,简单逻辑等。虽然用 Nextjs 也能够实现,但是还是那个关键点,它跟 React 绑定太深了。而且如果用不到上述说的那些功能,Nextjs 就有点「大材小用」了。

另外,可以从文档中看出,使用 Nextjs,还是需要一定的上手成本的,有较多的约定和概念需要掌握。在笔者看来,这些约定都是为了同时适配前后端而设计的。所以,笔者以为,如果只是实现一些偏后端的功能,框架的学习成本应该可以更小。所以接下来笔者打算尝试一下 Nestjs