一篇掌握React Server Components

原文链接:www.joshwcomeau.com/react/serve...

本文内容较长,如果有任何理解难点和错误,请告知我~

文章干货很多!

文章干货很多!

文章干货很多!

自从十年前React初次被引进到开发社区,它经历了几次进化。到现在,他们不会因为一些想法太激进而不去做这一次升级:如果他们发现了一个更好的解决方案,他们就会去落地它。

几个月前,React团队推出了React Server Components,是最新的一个重大变化。从这一刻开始,React components可以独立运行在服务端上。

这个功能一经推出大家都有一些困惑,比如:这到底是个什么?它是如何工作的?这个的优势是什么?它如何适配服务端渲染?

针对React Server Components我已经做了很多实验,并且已经解开了我很多的困惑。我不得不承认它比我预想的还要令我兴奋,它真的很cool。所以,我今天的目的就是帮助你弄明白这个功能,回答很多你可能会关心的问题。

SSR快速入门

在掌握React Server Components之前,熟悉一下SSR(server side rendering)是十分有必要的。如果你已经非常熟悉SSR了,那么你可以跳过这一节。

我在2015年开始使用React,大部分的React的设定是客户端渲染策略。用户会接收到一个像这样的HTML文件:

html 复制代码
<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
    <script src="/static/js/bundle.js"></script>
  </body>
</html>

其中bundle.js包含了我们所需要的一切并执行应用,包括React,其他的三方依赖和我们所写的全部代码。

一旦Js文件被下载和解析,React会迅速进入执行,转化我们全部应用的所有node节点,并且会将他们注入到空节点<div id='root'>中。

这种方式带来的一个问题就是:需要更多的时间来处理这些全部的工作。一旦这样去执行,用户就会看到一个白屏的效果。随着时间的推移,这种现象会变得越来越明显,因为每一次新特性的迭代,都会带来一些额外的字节体积到我们的bundle中,进而就会延长用户需要等待的时间。

SSR的设计就是来改善这种体验,相较于发送一个空的Html文件,服务端会渲染我们的应用进而来生成实际的Html。用户最终会接收到一个完全成形的Html文档。

这个Html文件仍然需要包含script标签,因为我们还是需要React运行在客户端,进而来处理每一次的交互。但还是存在一点与浏览器端渲染的差异:这不是从头开始变出了所有DOM节点,而是采用了现有的Html,这个过程被称之为水合作用(hydration)。

引用一下React团队的Dan Abramov的解释称:水合就像用交互性和事件处理程序的"水"浇灌"干燥"的HTML。(Hydration is like watering the "dry" HTML with the "water" of interactivity and event handlers.)

一旦JS bundle被下载下来,React会迅速启动程序,构建虚拟DOM并最终拟合到真实DOM上,绑定事件,触发效果等等。

简而言之,SSR就是通过服务端生成了一个初始的Html,这样用户就不再需要因为Js bundles的下载和解析而白白等待空白页。然后,客户端React从服务器端React中断的地方开始,采用DOM并加入交互性。

总的来说:

当我们谈论服务端渲染的时候,我们可以想象这样的一个流程:

1、用户访问myWebsite.com

2、Node服务接收到这个请求,立即渲染React应用,生成HTML

3、这个刚生成的HTML会被发送到客户端

这是实现服务端渲染的一种可能方式,但不是唯一方式。另一个选择是当我们构建应用的时候生成HTML。

一以贯之的是,React应用需要被编译,将JSX转化为原生的JS,打包我们所有的模块。如果在同一过程中,我们为所有不同的路由"预呈现"了所有HTML,会怎么样?

这通常被称为静态站点生成(SSG)。它是服务器端渲染的一个子变体。

在我看来,服务端渲染整体来说包含了一些不同的渲染策略,他们都有一个共同点:通过服务端运行时使用ReactDOMServer来初始渲染。无论是按需还是在编译时,这种情况在什么时候发生都无关紧要。无论哪种方式,它都是服务器端渲染。

前后端来回跳动的困扰

接下来我们看一下React中的数据请求,正常情况下,有两个独立的应用通过网络进行通信:

  • 客户端React应用
  • 服务端REST API

通过React Query、SWR或者Apollo,客户端可以对后端发起一次网络请求,服务端可以从数据库中抓取数据然后通过网络发送给客户端。

通过一张图看一下这个流程:

上述图中的横坐标单位为时间,不是分钟也不是秒,只是一种时间概念。在实际场景里面,因为影响的因素很多,这里的时间概念可能千差万别。因此这里的图只是给大家对于概念的一个更高维度的理解。

上面这张图展示了客户端渲染的流程,流程开始于客户端收到一个HTML文件,这个文件除了一些Script标签之外没有任何内容。

一旦JS文件被下载和解析,React应用就会启动,创建一批DOM节点并渲染UI。首先,因为我们还没有真实的数据,所以我们只能渲染一些加载状态的骨架(头部,底部和一些通用的layout)。

你应该看过很多这种骨架屏的模式,比如,UberEats的启动页就是一个骨架屏,它在等待实际需要渲染数据的返回。

在获取数据期间,用户只能看到这个加载状态的页面,直到数据返回并重新渲染页面。

那么,我们换个思路来解决这个问题。下面这张图保留着同样的数据请求模式,但是通过服务端渲染取代了客户端渲染:

在这个新的流程中,我们在服务端进行第一次的渲染,这意味着用户不再只是收到一个空白的HTML文件。

相较于空白页面,这种流程确实有一些改善的效果,但是根本上它并不是一种解决问题的有效方式,因为用户访问我们的应用不是看一个加载页面,而是访问我们的真实内容(餐馆、饭店列表,搜索结果,消息等等)。

为了能够真实地提升用户体验,我们在图表里面添加一些性能指标,让我们看看CSR和SSR的性能差别:

上述的每一个小红旗🚩都是一些常用的web性能指标。以下是分析结果:

  • First Paint -- 用户不再需要看着空白屏,基础的布局已经被渲染,但是没有内容。这就称之为FCP(First Contentful Paint)
  • Page Interactive -- React已经下载,我们的应用已经被渲染/水合,交互的元素都能正常的响应。这就称之为TTI(Time To Interactive)
  • Content Paint -- 页面包含了用户关心的内容,应用已经从服务端拉取到了数据并渲染在UI中。这就称之为LCP(Largest Contentful Paint)

通过在服务端做初始渲染,我们可以更快地获得骨架屏,这会让加载的体验更好一些,就好像这是一种流程一样,并且流程已经开始。

在有些情况下,这种优化是十分有意义的,比如有些用户就是在等页面头部的加载,然后可以快速链接到其他地方。

但你不觉得这种流程有点傻么?每当我看着SSR的流程图的时候,我都忍不住地关注到请求是在服务端启动的。相较于第二次的网络请求,我们可不可以尝试在第一次请求的时候拿数据。换言之,可以不可以让流程变成这样:

相较于在客户端和服务端之间来回跳动,我们将数据请求作为初始请求的一部分,给用户发送完全填充的UI。

但是,我们究竟该怎么做呢?

为了能让这个设想成真,我们需要给React提供一串代码并让其只运行在服务端------来做数据请求。但是这不是React提供的一个能力,甚至在SSR,我们所有的组件都在客户端和服务端渲染。

前端生态针对这个问题有很多的解决思路。像是Next.js和Gatsby这样的框架已经通过他们自己的方式让代码单独运行在服务端上。

举例说明,这里是在Next.js中使用的形态(使用传统的页面路由):

jsx 复制代码
import db from 'imaginary-db';
// This code only runs on the server:
export async function getServerSideProps() {
  const link = db.connect('localhost', 'root', 'passw0rd');
  const data = await db.query(link, 'SELECT * FROM products');
  return {
    props: { data },
  };
}
// This code runs on the server + on the client
export default function Homepage({ data }) {
  return (
    <>
      <h1>Trending Products</h1>
      {data.map((item) => (
        <article key={item.id}>
          <h2>{item.title}</h2>
          <p>{item.description}</p>
        </article>
      ))}
    </>
  );
}

我们对这段代码进行一下分析:当服务端接收到一个请求,getServerSideProps函数被执行并返回一个props对象,这个对象被注入到组件里面,组件会先在服务端渲染,然后在客户端进行水合作用。

比较好的是getServerSideProps这个方法不会在客户端重新执行,那是因为这个方法不会被打包进我们的Js bundle中。

这种方式是十分超前的,这确实很好,但是任何事情都是一体两面的,不免也会有一些缺陷:

  • 这种策略只会在路由层面工作,也就是那些在在组件数顶层的组件,不可以在任何组件中使用
  • 每个框架都有自己的实现方式,Next.js有自己的方式,Gatsby有实现方案,Remix也有,没有统一的标准
  • 所有的组件在客户端都会进行水合作用,不管还需不需要

这几年,React团队一直在思考并解决这件事,期望通过一个官方统一的方式来处理这个问题,于是React Server Components诞生了。

React Server Components介绍

在一个更高的层面来看,React Server Components是一种全新的规范,在这个新的世界,我们可以创建只在服务端运行的组件,这允许了我们在组件里进行像数据请求的操作。

下面是一个服务端组件的示例:

tsx 复制代码
import db from 'imaginary-db';

async function Homepage() {
  const link = db.connect('localhost', 'root', 'passw0rd');
  const data = await db.query(link, 'SELECT * FROM products');
  
  return (
    <>
      <h1>Trending Products</h1>
      {data.map((item) => (
        <article key={item.id}>
          <h2>{item.title}</h2>
          <p>{item.description}</p>
        </article>
      ))}
    </>
  );
}

export default Homepage;

作为一位使用react很多年的我来说,这段代码给我的第一印象确实太疯狂了。

"但是...",我扪心自问:"函数式组件不允许异步啊,在渲染的流程中不允许我们产生任何副作用的操作"。

有一个关键点需要注意:服务端组件不会重新渲染,他们只会在服务端执行一次来生成UI,渲染的结果会被锁定并发送给客户端。就React而言,这个结果是不可变的,也永不会变。

这意味着React的API中有很大一部分与服务器组件不兼容。比如,我们不能使用state,因为state可变,但是服务端不会重新渲染;我们不能使用effects,因为effects只会在渲染之后执行,在客户端,服务端组件不会执行这里的。

但是当在某些条件下它还是有些弹性的。例如,传统的react中,我们在使用useEffect的时候,我们会将副作用代码放在callback、事件处理器或者其他中,进而确保每次渲染的时候不会重复执行。但是如果组件只会执行一次,这种副作用代码就可以不写啦。

服务端组件本身是很清晰简单的,但是React Server Components规范还是很复杂的。因为React还有一些常规的组件,他们结合在一起的方式可能会令人十分困惑的。

在这个新的规范中,传统的React组件------也就是我们说的客户端组件,这类组件只会在客户端渲染,但是这个结论又并不严谨,客户端组件会同步是在两端渲染。

我知道这些术语会让人感到困惑,所以这里我做个总结:

  • React Server Components是在这个规范才出现的名称
  • 在这个规范里,我们熟知的标准的React Components被重新称为Client Components,旧物新名
  • 在这个规范里,服务端组件是一种新的组件类型,他们会单独运行在服务端,他们的代码不会被打包进js bundle,所以他们不会进行水合作用和重新渲染

React Server Components VS Server side Rendering

让我们来厘清一下这两个容易混淆的概念:React Server Components不是要取代SSR,你也不必认为这是SSR2.0,我反而认为这是两块独立的部分,结合在一起会非常完美,这两个部分互相完整彼此。

我们仍然需要SSR来生成初始的HTML,React Server Components在顶部构建,并且会在客户端bundle中过滤掉,进而确保这个组件只会在客户端执行

实际上,React Server Components可以脱离SSR使用,但是在实操中,一起使用会会得到更好的结果。React 团队构建了一个最小的没有SSR的RSC demo,你可以看看

上手说明

通常,React有新特性发布以后,通过对已存项目升级React到最新版本就可以使用新特性,一次快速安装npm install react@latest即可。

但是, React Server Components的使用不是酱婶儿的。

在我的理解里,React Server Components需要与React之外一些工作紧紧结合才能使用,像是bundler,服务端和路由等。

在我写这篇文章的当下,只有一种使用React Server Components的方式,那就是Next.js 13.4+,使用他们全新的重新设计的App Router

希望在不久的将来能有更多地基于React的框架集成React Server Components。其实像这么重要的React功能目前只有一种方式在支持是很尴尬的。React的文档中有一个地方列举着支持RSC的介绍,我会不定期地去查看这个页面,看看会不会有一些新的消息同步进来。

客户端组件的说明

在这个新的React Server Components规范里,所有的组件都是默认为服务端组件。针对客户端组件,我们需要选择加入客户端组件。

如何选择?通过申明一个全新的命令来做:

tsx 复制代码
'use client';

import React from 'react';

function Counter() {
  const [count, setCount] = React.useState(0);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Current value: {count}
    </button>
  );
}

export default Counter;

这段代码的顶部单独申明的一行use client,就是用来表明当前的组件为客户端组件,这些组件需要打包进js bundle中,进而可以在客户端实现重新渲染的能力。

通过这种方式来申明我们开发的组件看起来似乎有点古怪,但是这类申明其实是有先例的:use strict命令就是严格是模式(Strict Modes)的一种声明。

我们不会在服务端组件中声明use server来标记服务端组件,那是因为在RSF中默认都是服务端组件。实际上,use server是用在服务端的事件中的,一个完全不同的功能,但是超出了本文的介绍范围。

哪种组件应该被设计为客户端组件呢?

你可能有这个疑问:我该如何定义一个组件为客户端组件还是服务端组件呢?

一个简单规则:如果一个组件可以成为服务端组件,那么它就应该是服务端组件,服务端组件倾向于更简单更容易推理。同时还有一个性能优势:因为服务端组件不会打包到客户端的JS bundler中。RSC的一个潜在的优点就是:可以提升TTI(time to interactive)的指标。

也就是说,我们不必要为了尽可能减少客户端组件作为我们的开发任务,我们不需要优化到最小数量的客户端组件。有一点需要提醒的是:每个React应用程序中的每个React组件都是客户端组件。

当你开始进行RSC的开发,你会发现这种划分更多的时候是凭直觉。有些组件我们因为使用了state或者effects进而必须要在客户端运行,这是时候你需要在组件文件的顶部添加一个命令use client。其他的都可以作为服务端组件。

边界的概念

我在学习RSC的时候遇到的第一个问题是:当props变化的时候组件会怎么样呢?

举个例子,假设有个这样的服务端组件:

tsx 复制代码
function HitCounter({ hits }) {
  return (
    <div>
      Number of hits: {hits}
    </div>
  );
}

假设服务端渲染的初始值hits为0,这个组件将会产出下面的标记:

html 复制代码
<div>
  Number of hits: 0
</div>

那如果hits的值发生了变化呢?假设这是一个state变量,从0变成1,HitCounter会发生重新渲染,但是它不会发生重新渲染,因为这是个服务端组件。

问题是:服务端组件隔离的情况下是没有任何意义的,我们需要缩小视野,以获得更全面的视角来看应用的结构。

我们以下面的这个组件树为例:

如果上述都是服务端组件,那么这就是都有意义的。没有props的变化,因为所有组件都不会触发重新渲染。

但是我们假设Article组件持有state变量hits,为了能使用这个变量,我们需要将这个组件变成客户端组件。

你发现问题了吗?当Article组件重新渲染以后,该组件的所有子组件都需要重新渲染,包括了HitCounterDiscussion组件,那如果这些组件都是服务端组件的情况下,是没有办法触发重新渲染的。

为了避免这种情况,React团队添加了一条规则:客户端组件只能引用客户端组件use client指令意味着HitCounter和Discussion的实例需要转化为客户端组件。

学习RSC规范的过程中,最哇塞的一个时刻是对于创建客户端边界的认知。实际上,下面才是最终实际的效果。

Article组件添加了use client指令,客户端边界就已经创建了。这个范围内的所有组件都会隐式地转化为客户端组件,尽管这些组件都没有添加use client的指令,他们仍然会在客户端进行水合和重新渲染。(当然如果你在其他地方通过服务端组件调用该组件,这个组件还是服务端组件)。

所以,我们不需要为每个运行在客户端组件添加use client指令,实际情况下,我们只需要在创建新的客户端边界的组件上添加即可。

概念的变通之法

当我第一次了解到客户端组件不能引用服务端组件的时候,我感觉这是个十分受限的功能。那万一我要在应用树顶部去使用state,那是不是就说明所有的组件都会变成客户端组件了?

实际情况下有很多这种场景,我们可以通过重新组织应用结构来改变组件的拥有者,通过变通的方式解决这个限制。

这个比较难解释清楚,可以直接看一个例子:

jsx 复制代码
'use client';

import { DARK_COLORS, LIGHT_COLORS } from '@/constants.js';

import Header from './Header';
import MainContent from './MainContent';

function Homepage() {
  const [colorTheme, setColorTheme] = React.useState('light');
  
  const colorVariables = colorTheme === 'light'
    ? LIGHT_COLORS
    : DARK_COLORS;
    
  return (
    <body style={colorVariables}>
      <Header />
      <MainContent />
    </body>
  );
}

在这个设定里面,我们需要使用React state来提供用户切换黑白模式的功能。这就需要将state放在应用树的顶层,进而才能将css变量应用到body标签上。

为了能使用state,我们需要将Homepage定义为一个客户端组件,但因为这是个顶部组件,所以它的所有子组件HeaderMainContent都会隐式地变成客户端组件。

那么,为了解决这个问题,我们提取这个颜色管理任务成为一个组件,并在该组件内实现逻辑:

jsx 复制代码
// /components/ColorProvider.js

'use client';

import { DARK_COLORS, LIGHT_COLORS } from '@/constants.js';

function ColorProvider({ children }) {
  const [colorTheme, setColorTheme] = React.useState('light');
  
  const colorVariables = colorTheme === 'light'
    ? LIGHT_COLORS
    : DARK_COLORS;
    
  return (
    <body style={colorVariables}>
      {children}
    </body>
  );
}

回到Homepage,我们使用这个新组件:

jsx 复制代码
// /components/Homepage.js

import Header from './Header';
import MainContent from './MainContent';
import ColorProvider from './ColorProvider';

function Homepage() {
  return (
    <ColorProvider>
      <Header />
      <MainContent />
    </ColorProvider>
  );
}

我们可以从Homepage中删除use client这个指令,因为这个组件不再使用state,任何其他客户端React特性。这也就意味着HeaderMainContent不再需要隐式地转化为客户端组件。

桥豆麻袋,当我没看见呢?ColorProvider作为一个客户端组件,且作为HeaderMainContent的父组件,无论怎样,还是在应用树的顶层,对吧?

当触发客户端边界的时候,组件的父子关系将不再生效。Homepage是唯一引用和渲染HeaderMainConent,这意味着Homepage决定了这些组件的props是什么。

请记住,我们尝试解决的问题是:服务端组件不能重新渲染,所以他们的Props不能被赋予任何新的值。有了这个设定,Homepage决定了HeaderMainContent的props是什么,但是Homepage又是一个服务端组件,所以没有问题。

这确实有点烧脑,纵使我有了好几年的React的开发经验,我仍然觉得这个十分让人困惑😴。这需要相当多的练习才能形成直觉。

更严谨一点说,use client指令运用在文件或者模块级别,任何在客户端组件文件中引用的模块都是客户端组件。毕竟,当bundler打包我们的代码时,它会遵循这些导入!

如何修改主题?

在我上面的例子中,你可能注意到了没有办法可以更改颜色主题,setColorTheme没有被调用。

我想要通过最小单元来展示我想表达的内容,所以我就遗弃了一些完整的内容,完整的例子则是通过React context来让setter方法在所有子孙节点生效。只要消费这个上下文的组件是个客户端组件,那么一切都会正常运行。

深入了解服务端组件

让我们看得再深入一些,当我们使用服务端组件,那么它的产出是什么?实际上生成了什么呢?

让我们从一个最简单的React应用来看:

jsx 复制代码
function Homepage() {
  return (
    <p>
      Hello world!
    </p>
  );
}

在RSC的规范中,所有的组件默认都是服务端组件,因为我们没有显式地标记组件为客户端组件(或者被统计为客户端边界内的组件),它只会在服务端渲染。

当我们通过浏览器访问这个app的时候,我们会接收到一个HTML文档,其内容如下:

HTML 复制代码
<!DOCTYPE html>
<html>
  <body>
    <p>Hello world!</p>
    
    <script src="/static/js/bundle.js"></script>
    <script>
      self.__next['$Homepage-1'] = {
        type: 'p',
        props: null,
        children: "Hello world!",
      };
    </script>
  </body>
</html>

HTML文档中已经包括了通过React应用已生成的UI,Hello world!这个段落,这要归功于SSR,而不是由RSC来贡献的。

接下来是一个script标签,用来加载我们的js bundle。这个bundle包括了一些依赖,像是React,一些在应用中使用到的客户端组件。Homepage组件是服务端组件,这个组件的代码不会包含在这个bundle里面。

最终,我们还有第二个script标签,里面包含了一些内敛JS:

js 复制代码
self.__next['$Homepage-1'] = {
  type: 'p',
  props: null,
  children: "Hello world!",
};

这一段确实有点意思,本质上,我们这里代码是想告诉React:hey,我知道你没有Homepage组件的代码了,但是不用担心,这里是它最终渲染的内容。

正常情况下,React在客户端进行水合的过程中,它会快速渲染所有组件,构建整个应用的虚拟内容。这个过程不能运用到服务端组件,因为bundle里面没有这部分代码。

因此,我们发送渲染值,即服务器生成的虚拟内容。当React在客户端运行时,它会重新使用这个虚拟描述而不是重新生成它。

这就是为什么上面的ColorProvider案例可以运行的原因。HeaderMainContent的输出通过childrenprop传递给ColorProvider组件,ColorProvider可以重新渲染很多次,但是这个children是静态的,在服务端就被锁定了。

如果您想了解服务器组件是如何序列化和通过网络发送的真实内容,你可以通过RSC Devtools工具查看。

RSC的优势

RSC在React中是第一个官方方式运行纯服务端代码,我前面也提到了,在丰富的React的生态中,运行服务端代码不是一个新鲜事儿,在2016年的Next.js中就已经可以运行服务端代码了。

最大的区别是:我们之前没有一种方式可以让单独服务端代码运行在组件内部。

最明显的优点就是性能,服务端代码不必打包到bundle中,这会减少脚本的下载体积,也会减少水合的组件数量。

说实话,这并不是我最兴奋的事情,大部分的Next.js的应用在TTI方面的时间进行很好了,都很快。

如果你了解HTML的语法规则,大部分的应用在React完成水合之前是可以使用的。链接可以点击,表单可以提交,手风琴可以展开和关闭(使用<details><summary>)。对于大部分的项目,为了React的水合作用等待几秒是可以接受的。

但是我发现了一个更Cool的事情:就功能和包体积大小而言,我们不再需要做这种妥协。

举例说明,大部分技术博客都需要引入一些语法高亮的依赖包,在这篇文档中,我在使用Prism。代码片段是这样的:

一个合理的语法高亮包会包含所有的流行编程语言,那将会有好几MB,占用了太多的bundle体积。结果就是我们需要做出妥协,清除掉不需要的语言和特性功能。

但是,如果我们将语法高亮放在服务端组件中,这样就没有语法高亮代码被打包进bundle中,结论就是:我们不需要任何妥协,进而可以使用所有的功能。

这就是Bright的想法,一个与RSC结合的现代化的语法高亮包。

这才是我对于RSC兴奋的点,那些因为打包进bundle中而带来过高成本的内容,现在没有任何负担地可以跑在服务端,bundle不会增加任何体积,也会带来一个更好的用户体验。

不仅仅在性能和用户体验方面,使用了RSC一段时间以后,感觉使用服务端组件是一件多么舒畅的事情。在这里你不需要关心依赖数组,闭包问题,内存问题,还有其他因为状态变化带来的其他复杂的事情。

最后,一切都还早,RSC目前还在beta版本几个月中。接下来几年的技术进化让我兴奋,社区持续研发新的解决方案,就像Bright,充分利用了这个新规范的优势。成为一名React开发者是一件兴奋的事情!

完整蓝图

RSC是一项令人兴奋的开发方式,但这也只是整个现代化React蓝图中一小块。

当我们将RSC、Suspense和新的流式SSR结构结合,那将是真的令人兴奋的事情。整体的流程就变成了如下:

这个图确实超过了这篇文章介绍的内容,但是如果你有兴趣,可以看这里

相关推荐
崔庆才丨静觅11 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606112 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了12 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅12 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅12 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅13 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment13 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅13 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊13 小时前
jwt介绍
前端
爱敲代码的小鱼13 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax