一篇掌握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结构结合,那将是真的令人兴奋的事情。整体的流程就变成了如下:

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

相关推荐
qq_364371721 小时前
Vue 内置组件 keep-alive 中 LRU 缓存淘汰策略和实现
前端·vue.js·缓存
y先森2 小时前
CSS3中的弹性布局之侧轴的对齐方式
前端·css·css3
y先森7 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy7 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189117 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿8 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡9 小时前
commitlint校验git提交信息
前端
虾球xz9 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇9 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒10 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript