假如你是一个萌新,今天突然决定入坑全栈开发,那么选择什么样的前端和后端框架必然成为面临的第一个难题。你可能听说过React、Vue、Spring、Django、Flask、Gin...等等很多框架,但具体怎么选却毫无头绪。

这篇文章将为你逐层深入剖析,为什么Next.js会成为你的终极版本答案。

在文章开始之前,先回答两个问题:为什么会有前端和后端?又为什么会有全栈?我个人认为全栈不是单纯的全都干,而是要结合具体情况来分析。简单来说,前端和后端分工,能使开发团队只专注各自的领域,使各自领域更加专业,让各岗位能够自由选择技术路线,能够handle更加复杂的应用,并提高开发效率。然而,对于创业者、一些预算能力较差的中小型公司、有快速原型开发的任务、要减少沟通带来的误解和成本,就会更青睐于需要全栈工程师。所以到底深耕前端还是后端应完全取决于你的目标和期待,但不管怎样,尽早理解整个Web应用是如何运转的才更为重要

一、三层架构

与其说前端和后端,不如说是客户端和服务端,客户端负责UI/UX,服务端负责业务逻辑处理,再加上一个数据库,就成了经典的三层架构。网站、小程序、移动端APP还是电脑端APP都遵照这个逻辑,所以单纯说前端并不一定就是特指用JavaScript完成的网站和小程序,也有可能是指开发安卓/iOS端APP,或者PC端/macOS端的APP。

一般一个APP如果是Web应用,需要联网获取数据和业务处理能力,那么它的服务端和数据库就应部署在一个远程的服务器上。它的客户端会一般有两种常见的处理方案,如果是移动端和电脑端的APP,我们一般会把客户端先下载下来,因为它们的体积普遍偏大;而对于网站和小程序来说,客户端则会在我们访问的时候从服务器上传输过来,再由我们的浏览器引擎解析和渲染出来。

如果你触过前端就可能听说过React和Vue,事实上它们都是一种单页应用(Single Page Application, SPA)框架,通常只运行在客户端上,只有一个Html文件,所有的内容都通过浏览器引擎动态地渲染到这一页上,在用户切换页面时不需要完全重新加载,这样的性能在我们刚开始做小项目的时候看起来非常卓越。

二、服务端渲染(Server Side Rendering, SSR)

2.1 为什么SSR?

但是真实情况并非总会像刚开始上手那样轻松,随着你开发的项目逐渐庞大起来,很快你就会发现像SPA框架之类的只运行在客户端上的框架存在着非常明显的劣势:当用户访问站点请求加载前端的时候,庞大的前端代码会被一股脑地发送给用户浏览器,里面复杂的内容会在用户(尤其是第一次)加载时给浏览器造成非常大的负担,产生极差的用户体验,尤其是当用户网络不佳或者使用老一代智能设备的时候更加凸显。

这个时候我们不得不思考,是不是至少要让一部分客户端代码停留在服务器上,等到用户访问的时候再发送,或者干脆在服务端上渲染完再发送过来呢?于是服务端渲染(Server Side Rendering, SSR)的思路应运而生,它的理念就是解决上述问题,并提供更多益处,包括:

  • Bundle Sizes问题:SSR让那些会很大程度影响客户端运行效率的大尺寸的JavaScript Bundle保留在服务端上,渲染或选择性的渲染,然后以多种方式传输给客户端。

  • Data Fetching问题:在开发React或Vue项目的时候,我们会倾向于在客户端加载完成的时候(生命周期中的onMounted部分)在客户端上发起网络请求以获取数据库的数据来渲染页面,但是使用SSR以后我们就可以将这些请求转移到服务端上去做,现将数据渲染到页面上以后再返回给客户端,这大大减少了客户端网络请求的发起量,并且因为服务端相比客户端来说离数据源更近,也大大加快了页面渲染的速度。

  • 网络安全问题:现在敏感数据更安全了,例如Token和API Keys这些和用户信息有关的内容,得益于很多客户端逻辑转移到服务端上来,和减少了客户端行为,你就可以将它们维持在服务端上而不必暴露给客户端。
  • 缓存问题:服务端可以将请求的数据缓存下来并维持起来,这可以有效减少等待请求完成后才渲染的时间,从而大大提高项目的效率。
  • 首次页面渲染(First Contentful Paint, FCP)问题:得益于无需等待客户端完成下载、解析和执行JavaScript代码,服务端会自动生成渲染好的Html并返回客户端让用户立刻看到页面。
  • 搜索引擎优化(Search Engine Optimization, SEO)问题:得益于服务端已经渲染好了Html,它就可以被搜索引擎轻而易举的索引,提升网站在搜索引擎中的排名。同样在SNS(Social Network Sharebility)中,那些社交媒体软件可以直接为你的网站生成可视化的卡片,例如在飞书文档中写文章就可以轻松将单纯的网站链接变为卡片预览效果。

2.2 Next.js是什么?

对于初学者来说,可能连代码还不会写,怎么可能还有时间和精力去学习去为原本的SPA项目配置更加复杂的SSR呢?

所以从现在开始不妨了解一下Next.js。Next.js是在React的基础上建立的,只要你熟悉React的话就可以轻松上手。这个框架的宗旨就是为了让开发者轻松地去全栈开发Web应用,开发者专注于只需要自己的业务逻辑,其余的优化部分直接交给Next.js自动帮你解决,对于开发者来说,就是在写React和JavaScript而已。

Next.js默认就是使用SSR的,它提供了三种SSR策略,分别是Static Rendering、Dynamic Rendering和Streaming,你只需要掌握Next.js就可以轻松使用这些功能,无需学更多。

  • 静态渲染(Static Rendering):相当于静态站点生成(Static Site Generation),页面是在Build(开发环境编译成生产环境,Dev => Prod)时生成的,并且缓存在CDN网络中。在默认情况下Next.js中创建的所有React组件都遵循这个策略。
  • 动态渲染(Dynamic Rendering):在一些情况下,页面的渲染是依赖获取到的数据的,这时就可以转换为动态渲染,在这个策略下,页面是在数据请求时生成的。切换到动态渲染的方法也非常简单,那就是在原本静态渲染的组件中使用动态渲染方法,或不缓存的数据请求。
  • 流处理(Streaming):使用流处理时,页面也是在数据请求时生成的,页面会被分割成许多份,然后根据其优先级或是否已经渲染结束,分批次发给客户端,这可以让用户无需加载完整个页面就可以看到页面上的内容。当一些低优先级UI和低速的数据请求被使用时这个策略会让用户不被这些程序卡住,而直接看到页面的一个预览。

Next.js之所以强大,是因为它可以让你轻松地组合这些策略到一个项目中,你可以根据需求自由决定某个页面有多少客户端组件,有多少服务端组件,并且自由选定组件中的策略,这大大地增加了灵活性与效率。

三、Server Action发动了后端消失术

3.1 Next.js提供了后端要素

现在前端使用什么似乎已经有一个很好的解决方案了,那么后端呢?一个后端工程包含的最基本组件就是它的网络组件,不论使用Java、Python还是Go等任何高级语言去编写业务逻辑,在网络中暴露其接口都是必要的,因为它需要被客户端调用才能够执行业务逻辑,客户端想要在网络中找到这个后端就需要接口,这就是API Endpoint(API端点),而配置API Endpoint通常就需要一个后端库或者框架的支持了。这里的API一般指RESTful API,REST协议定义了许多Http方法供我们去请求这些暴露在网络中的接口,例如GET、POST、PATCH、PUT、DELETE、HEAD、OPTIONS等。

后端工程一般是运行在服务端的,而Next.js在默认情况下也是运行在服务端的,这会让人产生出一种大胆的想法,直接用Next.js写后端,就无需再新学一个后端框架了。

在Next.js 13.4版本以前Next.js是Pages Router模式,在老版本中,Next.js就已经支持通过建立api文件夹生成api/路由来搭建后端服务器以省略传统的搭建过程了,因为“基于文件的路由”(Filebase Route)本身就是一个网络服务,建立api文件夹就会自动建立api/path/,再配合使用RESTful方法就可以对这个路由访问,你可以直接将后端逻辑写在该文件夹下。

3.2 Next.js省略了请求和API端点

然而Next.js 13.4版本发布了App Router稳定版,不同的模式有不同的Filebase Route和API Reference,在这里先不展开讨论了,因为这篇文章并非Next.js教学。

我们今天要介绍的是App Router模式下出现了一种名为Server Action的方式,这种方式的神奇之处在于它无需设定API Endpoint,也无需显式地去做数据请求,可以完全只通过<form>标签的action来访问后端逻辑。

在React或Vue项目中,我们早已倾向于把每个<input>的数据使用state维护,提交表单单独写成一个函数,在里面处理数据校验、数据准备的工作,之后用fetch或者axios之类的库去调用API了,至于action这是在PHP时代以后就早没人用了的,比如下面这段示例React代码:

import React, { useState } from "react";

const page: React.FC = () => {
    const [content, setContent] = useState<string>(""); // 使用state维护数据

    const handleSubmit = async () => {
        // 做一些数据校验和准备工作

        // 发出数据请求
        const res = await fetch("yourapi.com/api/", {
            method: "post",
            body: {
                content,
            },
        });

        // 其他工作,比如检验数据,渲染数据,或者跳转到别的页面
    }

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => setContent(e.target.value);
    
    return (
        <form> <!-- 在form里没有action,甚至没有form都无所谓 -->
            <input type="text" value={content} onChange={handleChange} />
            <button type="button" onClick={handleSubmit}>Submit</ button>
        </form>
    )
}

export default page;

而Next.js团队魔改了action,允许传给它的值除了是字符串(一段API Endpoint)以外,还可以是一个Server Side Function,并且将formData传入这个function,于是你可以把数据校验环节放在这个Function里面,然后直接在function里调用数据库的ORM,并将结果返回,返回后的数据可以在客户端代码里访问,于是Next.js给这个function起了一个名字——Server Action,比如下面这段示例Next.js代码:

import orm from "db"; // 这只是一个示例,表示引入一个ORM

export default function Page() {
    async function createAction(formData: FormData) {
        "use server";

        const content = formData.get("content") as string;
 
        // 直接将原本在后端里才写的业务逻辑写在这里
        // 因为这里不是客户端代码,而是已经在服务端了
        // 例如利用orm和数据库交互
    }
 
    return ( 
        <form action={createAction}>
            <input type="text" name="content" />  <!-- 需要使用name -->
            <button type="submit">Submit</button>
        </form>
    )
}

一个Server Action可以在任何一个服务端组件中被定义和调用,也可以在客户端组件中被调用(但不能被定义),于是按照这个策略,我们的开发模式就可以变成,在客户端中发起一个action,并调用服务端的action,服务端的action完成或失败后将内容返回给客户端,在客户端中拿到并将结果渲染在组件上,下面是一段示例代码:

Server Action端:

// action/content.action.ts
// 为了复用性,server action完全可以单独声明在一个单独的文件中
"use server";

import { z } from 'zod'; // 用来做数据校验
import orm from "db"; // 这只是一个示例,表示引入一个ORM
 
const schema = z.object({
    // 数据校验的规则,具体访问Zod文档去学习
})
 
export default async function createAction(formData: FormData) {
    const content = formData.get("content") as string;

    // 数据校验
    const parsed = schema.parse({
        content,
    });
    if (!parsed.success) return { message: "数据校验失败" };
    
    // 直接将原本在后端里才写的业务逻辑写在这里
    // 因为这里不是客户端代码,而是已经在服务端了
    // 例如利用orm和数据库交互

    return { content, message: "成功" };
}

客户端:

"use client"; // 为了使用state展示错误UI,必须声明为客户端组件

import { useState } from "react";

export default function page() => {
    const [errMsg, setErrMsg] = useState<string>(""); // 错误处理信息

    async function handleCreate(formData: FormData) {
        setErrMsg(""); // 清空错误信息

        // 调用Server Action
        const res = await createAction(formData);

        if (res.message === "数据校验失败") return setErrMsg("校验失败");

        // 其他工作,比如检验数据,渲染数据,或者跳转到别的页面
    }
    
    return (
        <form action={handleCreate}>
            <input type="text" name={content} /> <!-- 需要使用name -->
            <button type="submit">Submit</ button>
            <p>{errMsg}</p> <!-- 展示错误信息 -->
        </form>
    )
}

在这个示例中我们可以看到,既没有类似Fetch发起的数据请求,也没有任何API Endpoint。但是我们虽然没有显式地声明,但是Next.js确实在客户端中发起了一个Fetch,而这就是Next.js的工作了,我们从此变得轻松了许多。

3.3 Server Action的不足与展望

我们可以看到Server Action的强大,它节省了我们非常多的工作。然而,它还有很多不足,例如在执行Action时,我们需要从中获取返回值,并强制使用"use client"去设置错误处理的UI,不论是使用state,还是使用useFormStatus等,都还需要强制使用客户端组件。虽然如此,但是Next.js也在他们的文档中声明了,他们正在探索服务端组件如何获取这些值。我们可以展望,Server Action有这种潜力改变我们已经习惯了的前后端开发模式。

补充:目前Server Action还处在试验阶段,所以为了能够使用这个特性你必须在next.config.js中进行如下声明:

const nextConfig = {
    experimental: {
        serverActions: true,
    },
};

module.exports = nextConfig;

四、结论:使用Next.js意味着什么?

  • 你只需要学一个框架(当然,还是要先学React的)就可以轻松掌握全栈。
  • 你只需要学一个编程语言(JavaScript/TypeScript)就可以轻松掌握全栈。
  • 你无需关心未配置服务端渲染(SSR)所引发的各种问题。
  • 你无需学习一个后端框架来配置必要的网络组件要素,例如API Endpoint。

五、前置学习路线

  1. 永远的第一步,好好学英语!
  2. 学习JavaScript、HTML、CSS。
  3. 熟练掌握JavaScript后学习React.js。
  4. 熟练掌握CSS后学习Tailwindcss,不仅因为这是Next.js标配的UI框架,还因为它本来就非常强大,本人强烈推荐。
  5. 学习TypeScript,选修。
  6. 永远的最后一步,适度学习益脑,过度学习伤身,想想你学这个的目的,和你的期待是什么。

六、关于本人

本人2022级哈尔滨工业大学软件工程专业毕业,现杭州创业做与区块链、数字契约、知识产权等相关内容的探索。这是我的第一篇文章,以后还会分享更多学习心得,以及创业心得,也会在这上面宣传自己的项目,如果你感兴趣欢迎给个赞,关注我,并留言~ 谢谢阅读~

Logo

前往低代码交流专区

更多推荐