普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月22日首页

你不知道的 TypeScript:模板字符串类型

2026年1月21日 22:49

大部分前端应该都或多或少地使用过 TypeScript 开发,但是此文将带你探索少有人了解的 TS 领域:模板字符串类型。

这样的类型操作你见过吗?

type World = 'World'
type Greeting = `Hello ${World}` // "Hello World"

type UserName = 'cookie'
type UserNameCapitalize = Capitalize<UserName> // "Cookie"

type ButtonVariant = `btn-${'primary' | 'secondary'}-${'sm' | 'lg'}`
// "btn-primary-sm" | "btn-primary-lg" | "btn-secondary-sm" | "btn-secondary-lg"

看起来不可思议,但是这些都是 TypeScript 模板字符串类型的能力。

模板字符串类型(Template Literal Types) 是 TypeScript 4.1 引入的高级特性。它建立在字符串字面量类型的基础上,允许你通过类似 JavaScript 模板字符串的语法,动态地组合、操作和生成新的字符串类型。

接下来,我将从字符串字面量类型开始,逐步讲解到模板字符串类型的初级到高级的用法。

一、基础:什么是字符串字面量类型?

1. 定义

字符串字面量类型是指将一个具体的字符串值作为一种类型

// 普通的 string 类型
let s1: string = 'hello'
s1 = 'world' // 正确

// 字符串字面量类型
let s2: 'hello' = 'hello'
s2 = 'world' // 报错:不能将类型"world"分配给类型"hello"

2. 联合类型 (Union Types)

字符串字面量类型最常见的用法是配合联合类型使用,限制变量只能是某几个特定字符串之一。

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
function request(method: HttpMethod, url: string) {
  // ...
}
request('GET', url) // 正确
request('LET', url) // 报错:类型"LET"的参数不能赋给类型"HttpMethod"的参数。

二、进阶:模板字符串类型

1. 基础拼接

就像 JavaScript 中的模板字符串 ${var} 一样,TypeScript 类型也可以通过反引号 ``${} 配合实现字符串类型的插值。

// 字符串拼接
type World = 'World'
type Greeting = `Hello ${World}` // 类型 "Hello World"

// 多个插值
type Protocol = 'https'
type Domain = 'example.com'
type URL = `${Protocol}://${Domain}` // 类型 "https://example.com"

// 与其他类型结合
type Version = 1 | 2 | 3
type APIVersion = `v${Version}` // "v1" | "v2" | "v3"

在这里提醒一下大家,一定不要把类型和值搞混,这里操作的是类型,不要把它当做值去操作。一些错误示例:

console.log(World) // 报错:"World"仅表示类型,但在此处却作为值使用。
type Greeting = "Hello" + World // 报错:"World"仅表示类型,但在此处却作为值使用。

2. 联合类型的自动分发(笛卡尔积)

在模板字符串中,把多个联合类型组合在一起时,TypeScript 会自动生成所有可能的组合,也就是按笛卡尔积(Cartesian Product)组合。

type Space = 'sm' | 'md' | 'lg'
type Side = 't' | 'b' | 'l' | 'r'
type PaddingClass = `p-${Side}-${Space}`
// "p-t-sm" | "p-t-md" | "p-t-lg" | "p-b-sm" | "p-b-md" | "p-b-lg" | "p-l-sm" | "p-l-md" | "p-l-lg" | "p-r-sm" | "p-r-md" | "p-r-lg"

// 实际使用
function addPadding(el: HTMLElement, className: PaddingClass) {
  el.classList.add(className)
}
const div = document.createElement('div')
addPadding(div, 'p-t-sm') // 正确
addPadding(div, 'p-x-xx') // 类型错误

性能提示:当联合类型的成员数量较多时,组合后的类型数量会呈指数级增长(例如 3 个联合类型各有 10 个成员,组合后会有 1000 种可能),这可能会导致 TypeScript 编译器性能下降或类型检查变慢。

3. 内置工具类型

为了性能和方便,TypeScript 编译器内置了四个特殊的工具类型。它们不是通过 TS 代码实现的,而是直接由编译器处理。

  • Uppercase<S> 将字符串中的每个字符转换为大写
  • Lowercase<S> 将字符串中的每个字符转换为小写
  • Capitalize<S> 将字符串的第一个字符转换为大写
  • Uncapitalize<S> 将字符串的第一个字符转换为小写

下面是使用示例:

// Uppercase:全部转大写
type Color = 'red'
type UpperColor = Uppercase<Color> // "RED"

// Lowercase:全部转小写
type MixedCase = 'TypeScript'
type LowerCase = Lowercase<MixedCase> // "typescript"

// Capitalize:首字母大写
type Name = 'cookie'
type CapName = Capitalize<Name> // "Cookie"

// Uncapitalize:首字母小写
type Components = 'Button' | 'Input' | 'Modal'
type UncapComponents = Uncapitalize<Components> // "button" | "input" | "modal"

结合模板字符串使用:

// 生成事件处理器名称
type Events = 'click' | 'change' | 'input'
type EventHandlers = `on${Capitalize<Events>}`
// "onClick" | "onChange" | "onInput"

// 生成 CSS 类名
type Size = 'sm' | 'MD' | 'Lg'
type SizeClass = `size-${Lowercase<Size>}`
// "size-sm" | "size-md" | "size-lg"

三、 高阶:模式匹配与 infer

掌握了基础的模板字符串拼接后,接下来我们进入更强大的领域——模式匹配。要在类型中解析字符串(例如提取参数、去掉空格),我们需要结合 extendsinfer

1. 什么是 infer

infer 属于 TS 的高阶用法,它需要配合条件语句一起使用:

A extends B ? C : D

含义是:如果类型 A 可以赋值给类型 B,则结果为 C,否则为 D。

infer 的作用是在条件类型的 extends 子句(也就是 B 语句)中 声明一个待推断的类型变量。可以把它理解为"占位符",让 TypeScript 帮你从某个复杂类型中"提取"出一部分。当类型匹配成功时,infer 声明的类型变量会被推断为匹配到的具体类型。

举一些实用的例子:

// 获取 Promise 返回值的类型
type UnpackPromise<T> = T extends Promise<infer R> ? R : T
type P1 = UnpackPromise<Promise<string>> // string

// 获取数组中元素类型
type GetArrayType<T> = T extends (infer U)[] ? U : never
type A1 = GetArrayType<number[]> // number

// 获取函数的返回值类型
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type R1 = GetReturnType<() => boolean> // boolean

2. 在模板字符串类型中使用 infer

在模板字符串中 infer 的使用方法同上,也需要配合条件语句,区别是我们需要通过 ${infer T} 把它插入到模板字符串类型中,通过模式匹配来提取字符串的特定部分。

比如提取出字符串类型空格后面的部分。

type GetSurname<S> = S extends `${infer First} ${infer Last}` ? Last : never

type T1 = GetSurname<'Hello Cookie'> // "Cookie"
type T2 = GetSurname<'Cookie'> // never (因为没有空格,匹配失败)

3. 贪婪与非贪婪匹配

多个 infer 连用,TypeScript 使用 “非贪婪” 匹配策略,也就是会遵循 “从左到右,尽可能匹配最小单位” 的原则。

比如,当使用 ${infer A}${infer B} 时:

  • A(第一个 infer):匹配第一个字符(最短匹配)
  • B(第二个 infer):匹配剩余所有字符(由于 A 已经匹配了第一个字符,B 必须匹配剩余的全部内容)

举例说明:

// 三个连续的 infer 会依次匹配:A 匹配第一个字符,B 匹配第二个字符,C 匹配剩余所有字符
type Split<S> = S extends `${infer A}${infer B}${infer C}` ? [A, B, C] : []
type Res = Split<'Hello'> // ["H", "e", "llo"]

// 有占位符的情况,也只会匹配到第一个
type GetExt<S> = S extends `${infer Name}.${infer Ext}` ? Ext : never
type E2 = GetExt<'ts.config.json'> 
// "config.json"(Name 匹配到第一个点之前,Ext 获取之后的所有内容)

4. infer 与联合类型联动

当模式中包含联合类型时,TypeScript 会尝试匹配联合类型中的任意一个成员:

// 提取指定几种扩展类型的文件名
type ImageExt = 'jpg' | 'png' | 'gif' | 'webp'
type ExtractRawName<FileName> = FileName extends `${infer Name}.${ImageExt}`
  ? Name
  : never

type E1 = ExtractRawName<'photo.jpg'> // "photo"
type E2 = ExtractRawName<'logo.png'> // "logo"
type E3 = ExtractRawName<'document.pdf'> // never (pdf 不在 ImageExt 联合类型中)

TypeScript 会对联合类型中的每一个成员分别执行 infer 逻辑,最后再把结果重新组合成一个新的联合类型,在模板字符串中也一样。

type IconNames = 'icon-home' | 'icon-user' | 'icon-settings' | 'logo-main'

// 提取所有以 "icon-" 开头的图标名称
type ExtractIcon<T> = T extends `icon-${infer Name}` ? Name : never

type PureNames = ExtractIcon<IconNames>
// 结果: "home" | "user" | "settings"
// 注意: "logo-main" 匹配失败,返回 never,在联合类型中 never 会被自动过滤掉

5. 递归类型 (Recursive Types)

在上一节的基础上,我们再结合递归,可以更加灵活的处理字符串,接下来以 TrimLeft 举例:

目标:去除字符串左边的空格。

type Space = ' ' | '\n' | '\t' // 联合类型 包含三种空白字符

// 如果第一个字符是空白字符,就取除了第一个空白字符的剩余字符串,然后递归处理
// 否则直接取整个字符
type TrimLeft<S extends string> = S extends `${Space}${infer R}`
  ? TrimLeft<R> // 递归调用
  : S // 终止

type T = TrimLeft<'  cookie'> // "cookie"

如果你在纠结为什么不是 \s 而是 ' '?这是因为 TypeScript 的模板字符串类型不支持正则表达式语法。这里的 ' ''\n''\t' 都是具体的字符串字面量类型,而 \s 是正则表达式的特殊语法,在类型系统中没有意义。

四、 映射类型与模板字符串的结合

TypeScript 4.1 不仅引入了模板字符串类型,还支持在映射类型中使用 as 重命名键名。

1. as 语法

type MappedType<T> = {
  [K in keyof T as NewKeyType<K>]: T[K]
}

在之前《面试官:请实现 TS 中的 Pick 和 Omit》一文中,在 Omit 的实现中就用到了 as 来剔除一些类型:

type MyOmit<T, K> = {
  [P in keyof T as P extends K ? never : P]: T[P];
};

type Todo = {
  title: string
  description: string
  completed: boolean
}
type TodoWithoutDescription = MyOmit<Todo, 'description'>
/*
type TodoWithoutDescription = {
  title: string
  completed: boolean
}
*/

2. 在模板字符串中使用

示例 1:添加前缀/后缀

type AddPrefix<T, Prefix extends string> = {
  [K in keyof T as `${Prefix}${string & K}`]: T[K]
}

interface User {
  name: string
  age: number
}

type PrefixedUser = AddPrefix<User, 'user_'>
// { user_name: string; user_age: number }

为什么要 string & K

因为 K 的类型是 keyof T,可能是 string | number | symbol。用交叉类型 & 将其约束为 string

示例 2:生成 Getter/Setter

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (val: T[K]) => void
}

interface State {
  count: number
  name: string
}

type StateGetters = Getters<State>
// { getCount: () => number; getName: () => string }

type StateSetters = Setters<State>
// { setCount: (val: number) => void; setName: (val: string) => void }

示例 3:移除特定前缀

type RemovePrefix<T, Prefix extends string> = {
  [K in keyof T as K extends `${Prefix}${infer Rest}` ? Rest : K]: T[K]
}

interface ApiResponse {
  api_name: string
  api_token: string
  userId: number
}

type CleanResponse = RemovePrefix<ApiResponse, 'api_'>
// { name: string; token: string; userId: number }

// 注意:传入空字符串作为前缀时,由于空字符串会匹配所有键名,但实际上不会移除任何内容
type CleanResponse1 = RemovePrefix<ApiResponse, ''>
// { api_name: string; api_token: string; userId: number }

五、 总结

写这篇文章因为我在刷 TypeScript 类型体操 时,遇到了第一个模板字符串类型的题目 TrimLeft 搜了半天没有发现现成的文章,干脆自己写一个。

如果你也对 TypeScript 类型体操感兴趣,欢迎一起来刷!💪🏻💪🏻💪🏻

昨天 — 2026年1月21日首页

追觅科技成央视春晚智能科技生态战略合作伙伴

2026年1月21日 22:07
1月21日,追觅科技正式成为中央广播电视总台2026年春节联欢晚会智能科技生态战略合作伙伴。据了解,追觅科技致力于将尖端科技用于生活普惠,构建起覆盖智能出行、智能清洁、智能家电、智能厨电、智能影音及户外生活等在内的全场景高端智能生活产品生态,为全球超4200万家庭、上亿用户构建全场景智能科技生态。

GDAL 实现影像裁剪

作者 GIS之路
2026年1月21日 21:08

^ 关注我,带你一起学GIS ^

前言

GDAL作为地理空间数据处理的核心工具,提供了多种影像裁剪方式,可以方便的提取目标区域遥感影像数据,为数据处理和分析提供高效服务。

在地理空间数据处理中,影像裁剪是基础且高频的操作,其核心目标是从整幅遥感影像或栅格数据中提取指定地理范围的子集,以降低数据体量、聚焦研究区域,满足专题分析、地图制作、数据共享等多样化业务需求。

由于本文由一些前置知识,在正式开始之前,需要你掌握一定的Python开发基础和GDAL的基本概念。在之前的文章中讲解了如何使用GDAL或者ogr2ogr工具将txt以及csv文本数据转换为Shp格式,可以作为基础入门学习。

本篇教程在之前一系列文章的基础上讲解如何使用GDAL 实现影像裁剪

如果你还没有看过,建议从以上内容开始。

1. 开发环境

本文使用如下开发环境,以供参考。

时间:2026年

系统:Windows 11

Python:3.11.11

GDAL:3.11.1

2. 数据准备

俗话说巧妇难为无米之炊,数据就是软件的基石,没有数据,再美好的设想都是空中楼阁。因此,第一步需要下载遥感影像数据。

但是,影像数据在哪里下载呢?别着急,本文都给你整理好了。

数据下载可参考文章:GIS 影像数据源介绍

如下,这是我在【地理空间数据云】平台下载的landsat8遥感影像。

3. 导入依赖

GeoTIFF作为一种栅格数据格式,可以使用GDAL直接进行处理,以实现影像数据的裁剪操作。

在影像裁剪开始之前,需要检查数据路径是否正确,所以导入os模块。

from osgeo import gdal
import os

4. 影像裁剪

定义一个方法image_clip(output_file,input_file,clip_file)用于实现影像数据裁剪。

本研究采用矢量范围提取影像区域,实现栅格数据按掩膜裁剪功能。

"""
说明:GDAL 影像裁剪
参数:
    -output_file:输出裁剪后的影像
    -input_files:输入需要裁剪的的影像
    -clip_file:用于影像裁剪的矢量文件
"""
def image_clip(output_file,input_file,clip_file):

在数据裁剪之前,使用方法checkFilePath检查数据路径。

# 检查文件是否存在
checkFilePath(input_file)
"""
说明:检查文件路径是否正常
参数:
-filePath:文件数据路径
"""
def checkFilePath(filePath):
    if os.path.exists(filePath):
        print(f"{filePath} 文件数据路径存在")
    else:
        print(f"{filePath} 文件数据路径不存在,请检查!")

使用gdal.WarpOptions选项定义裁剪参数,其中参数cutlineDSName为用于提取范围的矢量数据源,cropToCutline为布尔类型,表示是否使用裁剪线作为输出边界,dstNodata为设置NoData值。

# 定义裁剪参数
options = gdal.WarpOptions(
    cutlineDSName=clip_file,
    cropToCutline=True,
    dstNodata=0
)

这个方法参数非常多,感兴趣的同学可以到官网查看。

https://gdal.org/en/stable/api/python/utilities.html#osgeo.gdal.WarpOptions

调用gdal对象方法Warp进行影像裁剪,该函数第一个参数destNameOrDestDS 为输出数据集名称或者数据源,第二个参数srcDSOrSrcDSTab为源数据,第三个参数options为可选项描述,用于定义影像裁剪信息。

gdal.Warp(output_file,input_file,options=options)

main函数中调用合并方法。

if __name__ == "__main__":

    input_file"E:\ArcGIS\band_432.tif"

    output_file"E:\ArcGIS\clip_result.tif"

    clip_file"E:\data\target.shp"

    image_clip(output_file,input_file,clip_file)

注:GDAL某些参数是真难记,难写啊,比如WarpOptions对象。

GIS之路

图片效果

OpenLayers示例数据下载,请在公众号后台回复:vector

全国信息化工程师-GIS 应用水平考试资料,请在公众号后台回复:GIS考试

GIS之路 公众号已经接入了智能 助手,可以在对话框进行提问,也可以直接搜索历史文章进行查看。

都看到这了,不要忘记点赞、收藏 + 关注

本号不定时更新有关 GIS开发 相关内容,欢迎关注 


    

GeoTools 开发合集(全)

OpenLayers 开发合集

GDAL 实现影像合并

小小声说一下GDAL的官方API接口

《云南省加快构建现代化产业体系推进产业强省建设行动计划》发布

ArcGIS Pro 添加底图的方式

为什么每次打开 ArcGIS Pro 页面加载都如此缓慢?

ArcGIS 波段合成操作

自然资源部党组关于苗泽等4名同志职务任免的通知

GDAL 创建矢量图层的两种方式

GDAL 实现矢量数据转换处理(全)

GDAL 实现投影转换

GDAL 实现矢量合并

国产版的Google Earth,吉林一号卫星App“共生地球”来了

2026年全国自然资源工作会议召开

GDAL 实现矢量裁剪

GDAL 实现空间分析

vite.config.js 8 大核心模块,一文吃透

作者 yyt_
2026年1月21日 20:46

一、Vite 是什么?—— 面向未来的前端构建工具

Vite(法语意为“快”)是由 Vue 作者尤雨溪创建的新型前端构建工具。它利用浏览器原生支持 ES 模块(ESM)的能力,在开发环境下实现了极快的冷启动和热更新;而在生产环境中,则通过预构建依赖和 Rollup 打包输出高性能代码。

vite.config.js 作为 Vite 工程的核心配置文件,定义了整个项目的运行规则、编译逻辑和部署方案,是连接 Vite 核心能力与项目实际需求的桥梁,一份完善的 vite.config.js 能够让前端工程化流程更高效、更规范。

二、核心概念对比:Vite vs Webpack

虽然 Vite 和 Webpack 都用于构建前端应用,但它们的设计哲学完全不同,核心概念的差异直接决定了两者的使用体验和性能表现:

概念 Webpack Vite
Entry(入口) 显式配置 entry 字段,从 JS 入口开始递归解析依赖 默认以 index.html 为入口,开发时按需加载 ESM 模块,生产环境可显式配置 HTML 入口
Chunk(代码块) 构建阶段静态分析生成 chunks 开发时无需生成 chunk,生产构建依托 Rollup 实现动态代码拆分
Loader(转换器) 使用 loader 处理非 JS 资源(如 babel-loader, sass-loader) 无明确 Loader 概念,通过插件机制 + 内置转换器处理特殊资源,更灵活高效
Plugin(插件) 插件监听生命周期钩子扩展功能 插件系统强大,支持开发、构建双模式介入,兼容部分 Rollup 插件
Output(输出) 输出 bundle 到指定目录,需额外配置优化 生产环境输出优化后的静态资源,内置多种打包优化策略,配置更简洁

关键区别:开发与生产环境的差异化处理

Webpack:开发和生产环境均走完整的打包流程,所有模块需提前编译合并为 bundle 文件,项目体积越大,启动和更新速度越慢。

Vite:

  1. 开发环境:基于浏览器 ESM 直接运行,不进行全量打包,仅对浏览器请求的模块进行即时编译,响应速度极快。
  2. 生产环境:使用 Rollup 进行完整打包,产出经过代码压缩、树摇优化、资源分类的静态资源,兼顾性能与兼容性。

这正是 Vite 能够实现“秒级启动”的根本原因,既保证了开发体验,又满足了生产环境的部署要求。

三、vite.config.js 核心模块配置实战

vite.config.js 采用模块化导出方式,支持根据环境动态返回配置,下面将按照功能模块拆解配置逻辑,详细说明各部分的配置目的和实现方式。

模块一:环境初始化与多环境配置

这是配置文件的前置步骤,核心是获取当前环境变量,实现不同环境下的差异化配置,依赖 Vite 内置的 loadEnv 方法。

配置逻辑

  1. 接收 Vite 传入的 mode 参数,该参数对应启动/构建命令中的环境(如 development、production)。
  2. 通过 loadEnv 加载对应环境的配置文件(如 .env.development、.env.production)。
  3. 定义不同环境下的公共路径、输出目录等核心配置,实现环境隔离。

代码实现

import { defineConfig, loadEnv } from 'vite';
import path from 'path';
import rimraf from 'rimraf';

// 生成时间戳,用于生产环境版本隔离
function createFileDate () {
  const today = new Date();
  const y = today.getFullYear();
  const m = today.getMonth() + 1 > 9 ? today.getMonth() + 1 : '0' + (today.getMonth() + 1);
  const d = today.getDate() > 9 ? today.getDate() : '0' + today.getDate();
  const h = today.getHours() > 9 ? today.getHours() : '0' + today.getHours();
  const M = today.getMinutes() > 9 ? today.getMinutes() : '0' + today.getMinutes();
  return y + '' + m + '' + d + '' + h + '' + M;
}

export default ({ mode }) => {
  // 第一步:加载环境变量,指定环境配置文件所在目录
  const env = loadEnv(mode, path.join(process.cwd(), './env'));
  const fileDateDir = createFileDate();
  
  // 第二步:定义多环境核心配置项
  // 生产环境 CDN 公共路径
  const prodPublicPath = `https://yyt.com/resources/ph7/${fileDateDir}/`;
  // 测试环境本地公共路径
  const testPublicPath = '/ph7/';
  
  // 第三步:生产环境前置清理旧构建产物
  if (mode === 'production') {
    rimraf(path.join(process.cwd(), './dist'), (err) => {
      if (err) console.error('清理 dist 目录失败:', err);
    });
  }

  // 返回最终配置
  return defineConfig({
    // 配置公共基础路径,根据环境切换
    base: mode === 'production' ? prodPublicPath : testPublicPath,
    // 其他核心配置...
  });
};

配置说明

  1. loadEnv 第一个参数为环境模式,第二个参数为环境配置文件目录,会自动加载该目录下 .env.${mode} 格式的文件。
  2. 生产环境构建前通过 rimraf 清理旧的 dist 目录,避免旧资源残留导致部署问题。
  3. 公共路径 base 用于配置打包后资源的根路径,生产环境配置 CDN 地址,测试环境配置本地子路径,解决资源 404 问题。

模块二:生产构建配置(build)

该模块是 vite.config.js 的核心之一,用于定义生产环境打包的输出规则、优化策略,所有配置均放在 build 字段下,依托 Rollup 实现打包能力。

配置逻辑

  1. 配置差异化输出目录,实现生产环境版本隔离。
  2. 开启输出目录自动清空,避免手动清理遗漏。
  3. 配置 Rollup 打包参数,包括入口、代码块输出规则、静态资源分类输出规则。
  4. 配置插件实现打包后资源自动拷贝,满足本地部署需求。
  5. 配置 SourceMap 生成规则,兼顾调试与安全。

代码实现

return defineConfig({
  // 其他配置...
  build: {
    // 1. 差异化输出目录:生产环境带时间戳,测试环境简易目录
    outDir: mode === 'production' ? `dist/cdn/${fileDateDir}` : 'dist',
    // 2. 打包前自动清空 outDir 对应的目录
    emptyOutDir: true,
    // 3. 是否生成 SourceMap:开发环境生成,生产环境关闭(安全+减小体积)
    sourcemap: mode === 'development',
    // 4. Rollup 打包详细配置
    rollupOptions: {
      // 配置打包入口:指定 index.html 作为入口文件
      input: {
        main: path.resolve(__dirname, 'index.html')
      },
      // 配置输出规则
      output: {
        // 入口代码块输出规则:输出到 assets/js 目录,添加 hash 后缀
        entryFileNames: 'assets/js/[name]-[hash].js',
        // 公共/异步代码块输出规则:与入口代码块统一目录
        chunkFileNames: 'assets/js/[name]-[hash].js',
        // 静态资源分类输出规则:按文件类型拆分目录
        assetFileNames: ({ name }) => {
          if (name.endsWith('.css')) {
            return 'assets/css/[name]-[hash][extname]';
          }
          if (name.endsWith('.html')) {
            return 'assets/html/[name]-[hash][extname]';
          }
          if (
            name.endsWith('.png') ||
            name.endsWith('.jpg') ||
            name.endsWith('.jpeg') ||
            name.endsWith('.svg')
          ) {
            return 'assets/img/[name]-[hash][extname]';
          }
          if (
            name.endsWith('.xls') ||
            name.endsWith('.xlsx') ||
            name.endsWith('.csv') ||
            name.endsWith('.pdf')
          ) {
            return 'assets/files/[name]-[hash][extname]';
          }
          if (
            name.endsWith('.ttf') ||
            name.endsWith('.eot') ||
            name.endsWith('.woff') ||
            name.endsWith('.otf')
          ) {
            return 'assets/fonts/[name]-[hash][extname]';
          }
          // 默认输出目录
          return 'assets/[name]-[hash][extname]';
        }
      },
      // 5. Rollup 插件配置:打包后资源拷贝
      plugins: [
        copy({
          targets: [
            {
              src: [
                `dist/cdn/${fileDateDir}/json`,
                `dist/cdn/${fileDateDir}/locales`,
                `dist/cdn/${fileDateDir}/index.html`
              ],
              dest: 'dist/local'
            }
          ],
          // 打包完成后执行拷贝
          hook: 'writeBundle',
          // 扁平化目录结构,避免多级嵌套
          flatten: true
        })
      ]
    }
  },
});

配置说明

  1. outDir 定义打包输出目录,生产环境使用带时间戳的目录名,实现版本隔离,防止旧资源缓存导致线上问题。
  2. Vite 生产打包(vite build)时,默认会给静态资源文件(CSS/图片/字体等)添加内容哈希,规则是:文件名格式:[name].[hash].[ext](比如 app.8a3b2.js)。对于普通 JS 文件,需通过配置 entryFileNames/chunkFileNames 手动添加 hash。
  3. assetFileNames 实现静态资源分类,将 CSS、图片、办公文件、字体分别放入对应目录,便于部署和运维排查。
  4. rollupOptions.plugins 中配置 rollup-plugin-copy,在打包完成后将核心资源拷贝到 dist/local,满足本地测试部署需求。

模块三:静态资源扩展配置(assetsInclude)

Vite 有默认支持的静态资源类型,对于一些特殊格式的文件(如 xlsx、pdf),需要通过 assetsInclude 扩展识别,确保打包时能正确处理这些资源。

配置逻辑

  1. 以数组形式列出需要扩展的静态资源后缀。
  2. 配置在顶层字段中,全局生效。

代码实现

return defineConfig({
  // 其他配置...
  // 扩展静态资源类型识别
  assetsInclude: [
    '**/*.xlsx',
    '**/*.xls',
    '**/*.csv',
    '**/*.pdf',
    '**/*.png',
    '**/*.jpg',
    '**/*.svg'
  ],
});

配置说明

  1. 通配符 **/ 表示匹配所有目录下的对应文件。
  2. 配置后,这些特殊格式文件可以通过 import 引入,打包时会按照 build.rollupOptions.output 中的规则输出到对应目录。

模块四:插件配置(plugins)

插件是 Vite 扩展功能的核心载体,通过配置不同插件,可以实现 Vue 解析、JSX 支持、HTML 优化等功能,所有插件配置在 plugins 数组中,按需求引入并初始化。

配置逻辑

  1. 安装所需插件(如 @vitejs/plugin-vue)。
  2. 在配置文件中导入插件。
  3. plugins 数组中初始化插件,传入必要的配置参数。

代码实现

import vue from '@vitejs/plugin-vue';
import vueJsx from '@vitejs/plugin-vue-jsx';
import { createHtmlPlugin } from 'vite-plugin-html';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';

return defineConfig({
  // 其他配置...
  plugins: [
    // 1. 解析 Vue 单文件组件(.vue),Vue 项目必备
    vue({
      template: {
        transformAssetUrls: {
          video: ['src', 'poster'],
          source: ['src'],
          img: ['src'],
          image: ['xlink:href', 'href'],
          use: ['xlink:href', 'href'],
          a: ['downloadHref']
        }
      }
    }),
    // 2. 支持 Vue JSX/TSX 语法解析
    vueJsx({}),
    // 3. HTML 优化插件:压缩 HTML、动态注入数据
    createHtmlPlugin({
      minify: true,
      entry: 'src/main.js',
      inject: {
        data: {}
      }
    }),
    // 4. SVG 图标管理插件:生成 SVG Sprite,实现图标复用
    createSvgIconsPlugin({
      iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
      symbolId: 'g-icon-[name]'
    })
  ],
});

配置说明

  1. @vitejs/plugin-vue:Vue 项目核心插件,用于解析 .vue 单文件组件,transformAssetUrls 配置用于修正模板中资源的路径解析。
  2. @vitejs/plugin-vue-jsx:支持 JSX/TSX 语法,满足个性化编码需求,无需额外配置即可使用。
  3. vite-plugin-html:生产环境自动压缩 HTML,支持动态注入数据到 HTML 中,提升页面加载性能。
  4. vite-plugin-svg-icons:将指定目录下的 SVG 图标生成 Sprite,在项目中可通过 <svg><use xlink:href="#g-icon-xxx"></use></svg> 复用,避免图标重复引入。

模块五:路径别名配置(resolve.alias)

在大型项目中,相对路径(如 ../../../src/components/Modal)会降低开发效率和代码可维护性,通过 resolve.alias 配置路径别名,可简化模块引入路径。

配置逻辑

  1. 借助 path.resolve 解析绝对路径。
  2. resolve.alias 中定义别名与对应目录的映射关系。

代码实现

return defineConfig({
  // 其他配置...
  resolve: {
    // 优先解析 browser 字段和 module 字段
    mainFields: ['browser', 'module'],
    // 路径别名配置
    alias: {
      '@': path.resolve('./src'), // 映射 src 目录
      '@LC': path.resolve('../lib-components/src'), // 映射外部组件库目录
      '#': path.resolve('./types'), // 映射类型定义目录
      'td-print': path.resolve('./node_modules/td-print/index.js') // 映射特定模块
    }
  },
});

配置说明

  1. 配置后,可使用 @/components/Modal 替代 ../../../src/components/Modal,简化路径书写。
  2. 别名不仅支持目录映射,还支持单个文件映射(如 td-print):
    • td-print 是自定义模块别名,对应项目中 ./node_modules/td-print/index.js(前端打印相关第三方库);
    • 不配置别名时需写完整路径 import print from './node_modules/td-print/index.js',配置后可直接 import print from 'td-print',简化导入、提升可读性。
  3. path.resolve 用于生成绝对路径,避免不同操作系统下的路径分隔符问题。
  4. 补充:路径别名需配合编辑器配置(如 tsconfig.json/jsconfig.jsoncompilerOptions.paths),实现代码提示和跳转。

模块六:CSS 预处理器配置(css)

Vite 内置支持 SCSS、Less 等 CSS 预处理器,只需安装对应的依赖,再通过 css.preprocessorOptions 配置预处理器参数,即可正常使用。

配置逻辑

  1. 安装 SCSS 依赖(sass,注意不是 node-sass)。
  2. css.preprocessorOptions.scss 中配置编译参数、抑制弃用警告等。

代码实现

return defineConfig({
  // 其他配置...
  css: {
    preprocessorOptions: {
      scss: {
        // 使用现代编译器 API,提升兼容性和编译性能
        api: 'modern-compiler',
        // 抑制 import 相关的弃用警告,保持构建日志整洁
        silenceDeprecations: ['import']
      }
    }
  },
});

配置说明

  1. 使用 SCSS 前需安装依赖:npm install sass --save-dev
  2. api: 'modern-compiler' 指定使用现代编译器 API,替代旧的 node-sass 编译器,提升编译速度和兼容性。
  3. silenceDeprecations 用于抑制不必要的弃用警告,避免构建日志被冗余信息覆盖。

模块七:本地开发服务器配置(server)

该模块用于配置本地开发服务器的相关参数,包括端口、跨域代理、主机访问权限等,核心是通过 proxy 配置解决本地开发的接口跨域问题。

配置逻辑

  1. 配置服务器端口和主机访问权限。
  2. 通过 proxy 配置接口转发规则,将前端请求转发到后端服务。
  3. 配置 changeOrigin 实现跨域模拟,配置 rewrite 修正请求路径。

代码实现

return defineConfig({
  // 其他配置...
  server: {
    // 配置本地开发服务器端口
    port: 8387,
    // 允许外部设备访问(如手机、同一局域网的其他电脑)
    host: true,
    // 跨域代理配置
    proxy: {
      // 匹配以 /charm 开头的请求
      '/charm': {
        // 后端服务目标地址
        target: 'http://10.1.11.11:58***/',
        // 开启跨域模拟:修改请求头中的 Origin 为目标地址
        changeOrigin: true,
        // 路径重写:此处保持原路径不变,可根据需求修改
        rewrite: (path) => path.replace(/^\/charm/, '/charm')
      },
      // 可配置多个代理规则
      '/g-filestore': {
        target: 'http://10.5.11.11:8***/',
        changeOrigin: true
      }
    }
  },
});

配置说明

  1. port 配置本地开发端口,避免与其他服务端口冲突。
  2. host: true 允许外部设备访问,方便在手机上调试移动端页面。
  3. proxy 中的 target 为后端服务地址,changeOrigin: true 是解决跨域的核心,通过修改请求头的 Origin 模拟同源请求。
  4. rewrite 用于修正请求路径,若前端请求路径与后端接口路径不一致,可通过该配置进行调整。

模块八:全局常量注入配置(define)

通过 define 可以在项目中注入全局常量,这些常量会在打包时被静态替换,无需手动引入即可在代码中直接使用,适用于埋点、版本号、CDN 路径拼接等场景。

配置逻辑

  1. define 中定义全局常量,注意字符串类型需要使用 JSON.stringify 包裹。
  2. 在项目代码中直接访问该常量。

代码实现

return defineConfig({
  // 其他配置...
  define: {
    // 注入时间戳全局常量,用于版本标识
    'import.meta.env.VITE_APP_LOCAL_HASH': JSON.stringify(fileDateDir)
  },
});

配置说明

  1. define 中的键名建议遵循 import.meta.env.XXX 格式,与 Vite 内置环境变量格式保持一致。
  2. 字符串类型必须使用 JSON.stringify 包裹,否则打包时会被当作变量解析,导致报错。
  3. 在项目代码中可直接使用:console.log(import.meta.env.VITE_APP_LOCAL_HASH),打包后会被静态替换为对应的时间戳字符串。

四、Vite 的构建流程

尽管 Vite 采用了与 Webpack 不同的底层机制,但它依然遵循清晰的构建流程,分为开发环境和生产环境两个阶段:

1. 开发环境构建流程

  1. 初始化参数:解析 vite.config.js,合并命令行参数,加载环境变量和插件。
  2. 启动开发服务器:创建 HTTP 服务,监听指定端口,开启 WebSocket 通信(用于热更新)。
  3. 确定入口:加载根目录下的 index.html,自动修正其中的资源路径和公共基础路径。
  4. 按需编译模块:浏览器请求某个模块时,Vite 实时对该模块进行编译(如 Vue SFC 解析、TS 转 JS),修正依赖路径后返回给浏览器。
  5. 热更新(HMR):监听项目文件变化,仅重新编译修改的单个模块,通过 WebSocket 向浏览器推送更新通知,浏览器直接替换对应模块,无需全页刷新。
  6. 接口代理:根据 server.proxy 配置,将前端请求转发到后端服务,解决跨域问题。

2. 生产环境构建流程

  1. 环境准备:解析 mode 参数,加载对应环境变量,清理旧的打包产物。
  2. 初始化编译器:合并 vite.config.js 中的 build 配置,初始化 Rollup 编译器,注册所有插件。
  3. 解析入口与依赖:以 index.html 为入口,递归分析所有模块的依赖关系,构建完整的依赖图谱。
  4. 模块编译与优化:对所有模块进行编译转换,执行 Tree Shaking 剔除死代码,进行代码压缩和混淆。
  5. 组装与输出资源:将模块组装为入口代码块、公共代码块,按照配置的输出规则将静态资源写入指定目录。
  6. 后续自动化操作:执行插件的 writeBundle 钩子(如资源拷贝),生成最终的打包产物,完成构建。

五、Vite 的核心优势与适用场景

核心优势

  1. 极速启动:利用浏览器原生 ESM,无需全量打包,开发环境启动速度远超传统打包工具。
  2. 快速热更新:仅更新修改的单个模块,热更新响应无延迟,大幅提升开发效率。
  3. 丰富的插件生态:支持 Vue、React、TypeScript 等主流技术栈,兼容部分 Rollup 插件,扩展能力强。
  4. 开箱即用:内置 TypeScript、JSX、CSS 预处理器等支持,无需额外复杂配置。
  5. 高度可配置:vite.config.js 提供完善的配置项,可满足各类项目的工程化需求。
  6. 优化的生产打包:基于 Rollup 实现,产出的静态资源体积小、性能优,满足生产环境部署要求。

适用场景

  1. 新一代 SPA/MPA 项目开发。
  2. 前端组件库开发。
  3. 内部中后台系统、管理平台开发。
  4. 需要快速迭代的原型项目。
  5. 注重开发者体验的团队和项目。

六、总结

vite.config.js 作为 Vite 项目的核心配置文件,涵盖了环境配置、打包输出、插件扩展、本地开发等多个模块,一份完善的配置能够让前端工程化流程更规范、更高效。

Vite 凭借“开发环境按需编译、生产环境 Rollup 打包”的差异化策略,既解决了传统打包工具的性能瓶颈,又满足了生产环境的部署要求,是现代前端开发的优质选择。掌握 vite.config.js 的配置逻辑,能够充分发挥 Vite 的核心优势,助力项目高效开发与部署。

《实时渲染》第2章-图形渲染管线-2.3几何处理

作者 charlee44
2026年1月21日 20:42

实时渲染

2. 图形渲染管线

2.3 几何处理

GPU上的几何处理阶段负责大多数每个三角形和每个顶点的操作。该阶段进一步分为以下功能阶段:顶点着色、投影、裁剪和屏幕映射(如图2.3)。

图2.3. 几何处理阶段分为一系列功能阶段

2.3.1 顶点着色

顶点着色有两个主要任务,即计算顶点的位置和评估程序员可能喜欢的顶点输出数据,例如法线坐标和纹理坐标。传统上,对象的大部分阴影是通过将灯光应用到每个顶点的位置和法线并仅将结果颜色存储在顶点来计算的。然后将这些颜色插入整个三角形。出于这个原因,这个可编程的顶点处理单元被命名为顶点着色器[1049]。随着现代GPU的出现,以及每个像素发生的部分或全部着色,这个顶点着色阶段更加通用,并且可能根本不会求取任何着色方程的值,其工作主要取决于程序员的意图。顶点着色器现在是一个更通用的单元,专门用于设置与每个顶点关联的数据。例如,顶点着色器可以使用第4.4节第4.5节中的方法为对象设置动画。

我们首先描述如何计算顶点位置,一组始终需要的坐标。在被屏幕显示的过程中,模型被转换成几个不同的空间或坐标系。最初,模型驻留在自己的模型空间中,这仅意味着它根本没有被转换。每个模型都可以与模型变换相关联,以便可以对其进行定位和定向。可以将多个模型转换与单个模型相关联。这允许同一模型的多个副本(称为实例)在同一场景中具有不同的位置、方向和大小,而无需复制基本几何体。

模型变换所变换的是模型的顶点和法线。对象的坐标称为模型坐标,在对这些坐标应用模型变换后,模型被称为位于世界坐标或世界空间中。世界空间是唯一的,模型经过各自的模型变换后,所有的模型都存在于同一个空间中。

如前所述,模型只有被相机(或观察者)看到才能渲染。相机在世界空间中有一个位置和一个方向,用于放置和瞄准相机。为了便于投影和剪辑,相机和所有模型都使用视图变换进行了变换。视图变换的目的是将相机放置在原点并瞄准它,使其看向负z轴的方向,y轴指向上方,x轴指向右侧。我们使用-z轴约定;一些文章也会使用向下看+z轴的约定。区别主要是语义上的,因为一个和另一个之间的转换很简单。应用视图变换后的实际位置和方向取决于底层应用程序编程接口 (API)。如此划定的空间称为相机空间,或更常见的是,视图空间或眼睛空间。视图变换影响相机和模型的方式示例如图2.4所示。模型变换和视图变换都可以用4×4矩阵来实现,这是第4章的主题。但是,重要的是要意识到可以以程序员喜欢的任何方式计算顶点的位置和法线。

图2.4 在左图中,自上而下的视图显示了在+z轴向上的坐标系中,按照用户希望的方式定位和定向的相机。视图变换重新定向了坐标系,使相机位于原点,沿其负z轴看,相机的+y轴向上,如右图所示。这样做是为了使裁剪和投影操作更简单、更快捷。浅蓝色区域是视锥体。在这里,假设透视图,因为视锥体是一个平截头体。类似的技术适用于任何类型的投影。

接下来,我们将描述顶点着色的第二种类型的输出。要生成逼真的场景,仅渲染对象的形状和位置是不够的,还必须对它们的外观进行建模。该描述包括每个物体的材质,以及任何光源照射在物体上的效果。材料和灯光可以通过多种方式建模,从简单的颜色到物理描述的精细表示。

这种确定光对材料效果的操作称为着色。它涉及计算对象上不同点的着色方程。通常,其中一些计算在模型顶点的几何处理期间执行,而其他计算可能在逐像素处理期间执行。各种材质数据可以存储在每个顶点,例如点的位置、法线、颜色或求取着色方程值所需的任何其他数字信息。然后,顶点着色结果(可以是颜色、矢量、纹理坐标以及任何其他类型的着色数据)被发送到光栅化和像素处理阶段进行插值并用于计算表面的着色。

GPU顶点着色器形式的顶点着色在本书中进行了更深入的讨论,尤其是在第3章第5章中。

作为顶点着色的一部分,渲染系统先进行投影,然后进行裁剪,将视图体换为单位立方体,其极值点位于(1,1,1)(-1,-1,-1)(1,1,1)(1,1,1)之间。可以使用不同的范围来定义相同的体积,例如,(0z1)(0 ≤ z ≤ 1)。单位立方体称为正规化视图体。投影是在GPU上由顶点着色器首先完成的。常用的投影方法有两种,即正射投影(也称平行投影)和透视投影,如图2.5。事实上,正射投影只是一种平行投影。也可以使用其他几种投影方式(特别是在建筑领域),例如斜投影和轴测投影。老式街机游戏Zaxxon就是以后者命名的。

图2.5. 左侧是正射投影或平行投影;右边是透视投影。

请注意,投影表示为矩阵(第4.7节),因此它有时可能与几何变换的其余部分连接。

正交观察的视图体通常是一个矩形框,正交投影将这个视图体变换为单位立方体。正交投影的主要特点是平行线在变换后保持平行。这种转换是平移和缩放的组合。

透视投影有点复杂。在这种类型的投影中,物体离相机越远,投影后看起来越小。另外,平行线可能会聚在地平线上。因此,透视变换模仿了我们感知物体大小的方式。在几何上,称为截头锥体的视图体是一个具有矩形底面的截棱锥。截头锥体也转化为单位立方体。正交变换和透视变换都可以用4×4矩阵构造(第4章),并且在任一变换之后,模型都被称为在裁剪坐标中。这些实际上是齐次坐标,在第4章中讨论过,因此这发生在除以w之前。GPU的顶点着色器必须始终输出这种类型的坐标,以便下一个功能阶段(裁剪)正常工作。

尽管这些矩阵将一个几何体转换为另一个几何体,但它们被称为投影,因为在显示之后,z坐标不存储在生成的图像中,而是存储在z缓冲区中,如第2.5节所述。通过这种方式,模型从三维投影到两维。

2.3.2 可选的顶点处理

每个管线都有刚刚描述的顶点处理。完成此处理后,可以在GPU上进行几个可选阶段,按顺序是:曲面细分、几何着色和流输出。它们的使用取决于硬件的能力——并非所有 GPU 都有它们——以及程序员的愿望。它们相互独立,一般不常用。 将在第3章中详细介绍每一个。

第一个可选阶段是曲面细分。假设你有一个弹跳球对象。如果用一组三角形表示它,则可能会遇到质量或性能问题。您的球在5米外可能看起来不错,但近距离观察单个三角形,三角形的轮廓,就会变得清晰可见。如果你用更多的三角形制作球来提高质量,当球很远并且只覆盖屏幕上的几个像素时,你可能会浪费大量的处理时间和内存。通过曲面细分,可以使用适当数量的三角形生成曲面。

我们已经讨论了一些三角形,但在管线中的这一点上,我们只处理了顶点。这些可用于表示点、线、三角形或其他对象。顶点可用于描述曲面,例如球。这样的表面可以由一组面片指定,每个面片由一组顶点组成。曲面细分阶段由一系列阶段本身组成——外包着色器(hull shader)、曲面细分器(tessellator)和域着色器(domain shader)——将这些面片顶点集转换为(通常)更大的顶点集,然后用于制作新的三角形集。场景的相机可用于确定生成了多少个三角形:面片很近时很多,远处时很少。

下一个可选阶段是几何着色器。该着色器早于曲面细分着色器,因此在GPU上更常见。它类似于曲面细分着色器,因为它接受各种类型的图元并可以生成新的顶点。这是一个更简单的阶段,因为此创建的范围有限,输出图元的类型也更有限。几何着色器有多种用途,其中最流行的一种是粒子生成。想象一下模拟烟花爆炸。每个火球都可以用一个点来表示,一个顶点。几何着色器可以将每个点变成面向观察者并覆盖多个像素的正方形(由两个三角形组成),从而为我们提供更令人信服的图元进行着色。

最后一个可选阶段称为流输出。这个阶段让我们使用GPU作为几何引擎。与将我们处理过的顶点沿着管道的其余部分发送到屏幕上不同,此时我们可以选择将这些顶点输出到数组中以供进一步处理。这些数据可以由CPU或GPU本身在以后的过程中使用。此阶段通常用于粒子模拟,例如我们的烟花示例。

这三个阶段按此顺序执行——曲面细分、几何着色和流输出——每个阶段都是可选的。无论使用哪个(如果有)选项,如果我们继续沿着管道向下走,我们就会得到一组具有齐次坐标的顶点,这些顶点将被检查相机是否能看到它们。

2.3.3 裁剪

只有全部或部分在视图体内部的图元需要传递到光栅化阶段(以及随后的像素处理阶段),然后在屏幕上绘制它们。完全位于视图体内部的图元将按原样传递到下一个阶段。完全在视图体积之外的基元不会被进一步传递,因为它们没有被渲染。部分位于视图体内部的图元需要裁剪。例如,一条直线,在视图体外部有一个顶点,在视图体积内部有一个顶点,此时应该根据视图体对其进行裁剪;以便外部的顶点被位于该线和视图体之间的交点处的新顶点替换。投影矩阵的使用意味着变换后的图元被裁剪到单位立方体上。在裁剪之前进行视图变换和投影的好处是可以使裁剪问题保持一致;图元总是针对单位立方体进行裁剪。

裁剪过程如图2.6所示。除了视图体积的六个剪裁平面之外,用户还可以定义额外的剪裁平面来明显地剪裁对象。第818页的图19.1中显示了显示这种可视化类型的图像,称为剖视(sectioning)。

图2.6. 只需要单位立方体内部的图元(对应视锥体内部的图元)继续处理。因此,单位立方体外面的图元被丢弃,而完全在里面的图元被保留。与单位立方体相交的图元被裁剪在单位立方体上,从而产生新的顶点并丢弃旧的顶点。

裁剪步骤使用投影产生的4值齐次坐标进行裁剪。值通常不会跨透视空间中的三角形进行线性插值。需要第四个坐标,以便在使用透视投影时正确插入和裁剪数据。最后,执行透视除法,将生成的三角形的位置放入三维标准化设备坐标中。如前所述,此视图体积范围从(1,1,1)(-1,-1,-1)(1,1,1)(1,1,1)。几何阶段的最后一步是从这个空间转换到窗口坐标。

2.3.4 屏幕映射

只有视图体内部的(裁剪的)图元被传递到屏幕映射阶段,进入这个阶段时坐标仍然是三维的。每个图元的x和y坐标被转换为屏幕坐标。屏幕坐标与z坐标一起也称为窗口坐标。假设场景应该被渲染到一个最小位置在(x1,y1)(x_1,y_1),最大位置在(x2,y2)(x_2 ,y_2)处的窗口(其中x1<x2x_1 < x_2y1<y2y_1 < y_2)。屏幕映射先是平移,然后是缩放操作。新的x和y坐标称为屏幕坐标。z坐标(OpenGL的[1,+1][−1,+1]和DirectX的[0,1][0,1])也被映射到[z1,z2][z_1,z_2],其中z1=0z_1=0z2=1z_2=1作为默认值。但是,这些可以通过API进行更改。窗口坐标连同这个重新映射的z值被传递到光栅化阶段。屏幕映射过程如图2.7所示。

图2.7. 投影变换后的图元位于单位立方体中,屏幕映射程序负责在屏幕上找到坐标。

接下来,我们描述整数和浮点值如何与像素(和纹理坐标)相关。给定像素的水平数组并使用笛卡尔坐标,最左边像素的左边缘在浮点坐标中为0.0。OpenGL一直使用这种方案,DirectX10及其后续版本也使用它。该像素的中心为0.5。因此,一系列像素 [0,9] 覆盖了 [0.0,10.0) 的跨度。转换很简单:

d=floor(c)(2.1)d = floor(c) \tag{2.1}
c=d+0.5(2.2)c = d + 0.5 \tag{2.2}

其中dd是像素的离散(整数)索引,cc是像素内的连续(浮点)值。

虽然所有API的像素位置值都从左到右增加,但在OpenGL和DirectX1之间的某些情况下,顶部和底部边缘的零位置不一致。OpenGL始终偏爱笛卡尔系统,将左下角视为最低值元素,而DirectX有时根据上下文将左上角定义为该元素。每个人都有一个逻辑,在他们不同的地方不存在正确的答案。例如,(0,0)(0,0)在 OpenGL中位于图像的左下角,而在DirectX中位于左上角。在从一个API迁移到另一个API时,必须考虑到这种差异。

Footnotes

  1. “Direct3D”是DirectX的三维图形API组件。DirectX包括其他API元素,例如输入和音频控件。我们不区分在指定特定版本时编写“DirectX”和在讨论此特定API时编写“Direct3D”,而是通过始终编写“DirectX”来遵循常见用法。

恒帅股份:实控人及其一致行动人拟合计减持公司不超2.83%股份

2026年1月21日 20:36
36氪获悉,恒帅股份公告,公司实控人俞国梅计划以集中竞价和大宗交易方式减持公司股份不超过242万股(占公司总股本的2.16%);其一致行动人宁波玉米计划以集中竞价交易方式减持公司股份不超过75.48万股(占公司总股本的0.67%)。上述减持主体合计减持股份不超2.83%。

沙特人工智能企业Humain募资最高12亿美元

2026年1月21日 20:29
沙特阿拉伯国家基础设施基金与该国人工智能企业Humain于周三宣布达成一项最高12亿美元的融资协议,旨在助力沙特本土人工智能及数字基础设施的扩张建设。一份声明显示,该协议就建设最高250兆瓦算力的人工智能数据中心制定了不具约束力的融资条款,以服务Humain的客户,相关合作于瑞士达沃斯正式对外公布。(新浪财经)

都2026年,React源码还值不值得读 ❓❓❓

作者 Moment
2026年1月21日 20:26

随着前端技术生态的不断演进,React 作为目前最流行的前端框架之一,已经走过了十多个年头。在 2026 年这个时间节点,很多开发者都在思考一个问题:React 源码还值不值得深入阅读?

这个问题的答案并不是简单的"是"或"否",而需要从多个维度进行分析。本文将从实际价值、学习成本、技术趋势等角度,为你提供一个全面的分析。

为什么曾经值得读 React 源码?

在讨论"现在是否值得"之前,我们先回顾一下为什么 React 源码曾经被认为是值得学习的经典。

React 引入了很多开创性的概念:虚拟 DOM(Virtual DOM)虽然现在已不是新概念,但在当时是一个突破;组件化思想确立了声明式 UI 开发范式;单向数据流成为状态管理的最佳实践;Fiber 架构实现了时间切片和可中断渲染的创新。

React 的代码库以其高质量著称:清晰的代码组织和架构设计、完善的注释和文档、严格的类型检查(使用 Flow,后来迁移到 TypeScript)、丰富的测试覆盖。

阅读 React 源码可以学到大型开源项目的组织方式、性能优化的思路和技巧、复杂状态管理的实现方式,以及设计模式和架构模式的实际应用。

2026 年的技术环境变化

到了 2026 年,React 已经非常成熟:API 已经相对稳定,重大变更减少;核心概念已经被广泛理解和应用;生态系统完善,最佳实践明确。

前端技术栈变得更加多样化:Vue 3、Svelte、Solid.js 等框架各有优势;服务端渲染框架(Next.js、Remix、Astro)的兴起;Web Components 的标准化;编译时优化成为趋势(如 React Compiler)。

学习资源也变得更加丰富:大量的教程、视频、文章;官方文档的完善;社区经验的沉淀;AI 辅助学习工具的普及。

2026 年读 React 源码的利弊分析

仍然值得读的理由

虽然你可以通过文档和教程学会如何使用 React,但阅读源码能让你真正理解 React 的工作原理,而不是表面的 API 使用;理解为什么某些 API 是这样设计的;理解性能优化的底层原理(如 Diff 算法、Fiber 调度)。

当你遇到框架层面的问题时,源码知识能帮助你快速定位问题根源、找到绕过框架限制的方法、为框架贡献代码或参与讨论。

从职业发展角度来看,深入理解 React 源码可以展现技术深度、学习大型项目的架构设计思路、培养阅读复杂代码的能力。

面试仍然是读源码的重要驱动力。打开牛客网等面试平台,你会发现 React 源码相关的面经依然大量存在。大厂面试中,React 源码问题几乎是必问项:Fiber 架构的实现原理、Hooks 的工作机制、Diff 算法的优化策略、事件系统的合成事件机制、调度器的优先级调度原理等等。如果你只能回答 API 的使用,而无法解释底层的实现原理,在面试中很难获得竞争优势。不仅仅是中高级前端开发者的面试,就连秋招和实习面试中也经常出现 React 源码相关的问题,深入理解 React 源码几乎成了标配要求。

在 AI 工具日益普及的 2026 年,一个现实是:即使你不懂 React 原理,也能通过 AI 辅助工具(如 GitHub Copilot、claude code、Cursor 等)完成日常开发工作。AI 可以帮助你生成代码、解决 bug、优化性能。但如果你想要找到一份好的工作,特别是如果你的技术栈是 React,那么最好还是深入理解 React 原理。原因很简单:AI 可以帮你写代码,但不能帮你通过技术面试;AI 可以解决具体问题,但不能替代你对框架的深度理解。在竞争激烈的就业市场中,能够解释 React 底层原理的开发者,明显比只会使用 API 的开发者更有优势。

以下是从牛客网面经中截取的实际面试题目,可以看到 React 原理类问题确实频繁出现:

牛客网 React 面经截图 1

牛客网 React 面经截图 2

React 19 引入了很多新特性,值得深入研究:并发渲染(Concurrent Rendering)、自动批处理(Automatic Batching)、Suspense 的完整实现、Transition API、React Compiler、Actions 和 Form 的改进等。

React 源码是学习设计模式的绝佳教材:观察者模式(事件系统)、策略模式(调度器)、适配器模式(各种 renderer)、工厂模式(组件创建)。

可能不值得读的理由

React 源码库庞大(超过 10 万行代码),需要大量的时间和精力投入,对于大多数应用开发场景,可能"用不到"这么深的知识。

如果你已经是经验丰富的 React 开发者,读源码的边际收益可能不大,很多概念已经通过其他方式学习到了,实际工作中很少需要深入到框架实现层面。

如果项目使用了其他框架(Vue、Svelte 等),React 源码的学习价值相对降低;如果转向了服务端渲染或边缘计算,客户端框架源码的价值降低。

现在有更多高质量的学习资源(视频教程、互动式课程),可以通过构建简化版 React 来学习核心概念,通过 TypeScript 类型定义也能理解很多设计。

如何判断你是否应该读 React 源码?

适合读源码的情况包括:你已经熟练使用 React 进行开发(至少 1-2 年经验);你遇到了框架层面的问题,需要深入理解才能解决;你想提升技术深度,为职业发展做准备;你即将参加面试,无论是秋招、实习还是社招,特别是大厂面试,React 源码问题几乎是必考点;你对框架设计感兴趣,想要学习架构设计;你想要为 React 贡献代码,或参与相关技术讨论;你是前端技术专家或架构师,需要全面的技术理解。

不太适合读源码的情况包括:React 新手应该先掌握基础使用,再考虑读源码;时间有限的情况下,如果项目压力大,先保证业务能力;如果职业方向不在前端框架,如果转向全栈、后端或移动端,优先级应该调整;如果只做业务开发,工作中不需要深入到框架层面,可能收益不大。

如果决定读,应该如何读?

如果你决定要读 React 源码,不要试图一次性读完所有代码,应该按模块学习。

下面这张图是我整理的 React 源码整体阅读流程,用一条清晰的路径把从 JSX 到 Fiber、再到 Scheduler 和 commit 阶段串联在一起。建议你先整体看一遍这张图,对 React 内部的执行链路有个全局认知,再按图中的顺序去对应阅读源码里的关键部分。

20260121200944

结合这张流程图,可以按下面的顺序来读 React 源码:

  1. 从使用入口出发:先看 packages/react 中与 JSX 相关的实现(createElement、jsx 等),再看 packages/react-dom 中的 createRoot、render,弄清楚一次渲染是如何被发起的。

  2. 理解 Fiber 数据结构:在 packages/react-reconciler 中阅读 FiberNode 的定义、createFiber 等代码,搞清楚 Fiber 上都挂了哪些信息,以及它和更新队列的关系。

  3. 看渲染阶段的工作循环:继续在 react-reconciler 里看 workLoopConcurrent、performUnitOfWork、beginWork、completeWork,对应流程图中「渲染阶段」这部分。

  4. 单独啃 Scheduler:切到 packages/scheduler,理解任务队列、优先级和时间切片机制,这一块对应流程图中间的调度器节点。

  5. 回到 commit 阶段:再回到 react-reconciler,看 commitRoot、commitMutationEffects、commitLayoutEffects,弄清楚 DOM 实际在什么时候、以什么顺序被更新。

  6. 深入 Hooks 实现:重点阅读 useState、useReducer、useEffect 等 Hook 对应的实现文件,结合 Hooks 链表的那部分流程图,理解 hooks 链表如何在 current、workInProgress 之间切换。

  7. 最后再看 Context、Suspense、并发特性等模块,把前面打下的基础扩展成完整的 React 内部心智模型。

如果你时间不多,建议优先把 1-6 跑通:入口能串起来、Fiber 能看懂、workLoop 能跟住、Scheduler 有概念、commit 知道在干嘛、Hooks 能解释清楚,基本就已经具备“读懂 React 源码”的主干能力。

工具和方法上,建议先把 React 源码仓库拉到本地,在源码里定位关键函数和关键数据结构。

同时搭配 React DevTools 观察组件树和状态变化。真正调试运行时路径时,浏览器里执行的是构建后的 bundle,所以需要开启 Source Map,把断点、调用栈从 bundle 映射回你本地的源码文件,这样你在 DevTools 里跟代码时看到的就是 React 源码而不是编译产物。

遇到数据结构或边界条件不确定,再对照官方文档和 TypeScript 类型定义去校验。

最后,边读边做一个“最小可运行”的简化版 React,再用少量测试用例验证自己的推导,会比纯读更容易形成稳定的心智模型。

2026 年的新视角:React Compiler 与未来

到了 2026 年,React 可能已经集成了一些新特性,值得关注。

如果 React Compiler 已经集成到核心,这将是值得深入学习的新内容:编译时优化的思路、静态分析和代码转换、性能优化的新范式。

React 19 的并发特性已经稳定,这些实现值得深入研究:时间切片(Time Slicing)、优先级调度、可中断渲染。

了解 React 如何与新技术整合也很重要:React Server Components、与 WebAssembly 的交互、与 Web Workers 的集成。

替代学习路径

如果你觉得读完整源码成本太高,可以考虑这些替代方案。

通过实现一个简化版的 React(如 1000 行左右的代码),你可以学习到核心概念:Virtual DOM、组件系统、Diff 算法、Hooks 基础实现。

只读最核心的部分:Reconciler 的核心逻辑、Hooks 的实现、Scheduler 的调度算法。

React 的 TypeScript 类型定义本身就是很好的文档,可以帮你理解 API 设计思路、数据流、组件生命周期。

关注技术博客和视频:React 团队的官方博客、技术社区的文章、深度解析视频。

结论与建议

在 2026 年,React 源码仍然值得读,但不再是"必读项"。

对于大多数开发者,建议优先掌握 React 的使用和最佳实践,通过文档、教程和项目经验提升能力,只在遇到特定问题或想提升深度时,有针对性地阅读相关源码。

对于有追求的开发者,如果你有时间和兴趣,阅读源码绝对是有价值的投资。建议采用"选择性深度阅读"的方式,重点学习核心模块。结合实践项目,通过构建简化版来加深理解。

对于技术专家和架构师,深入理解 React 源码是必要的。这不仅能帮你做出更好的技术决策,还能提升架构设计能力。

技术学习是一个持续的过程,不应该有"一劳永逸"的想法。是否读 React 源码,应该基于你的当前水平、职业目标、项目需求、时间资源。

在 2026 年,我们有更多的学习选择。React 源码仍然是宝贵的学习资源,但它不再是唯一的选择。选择最适合你当前情况的路径,比盲目追求"读完源码"更重要。

无论你是否选择深入阅读 React 源码,保持学习的心态和对技术的热情,比掌握任何特定的技术栈都更重要。技术会变化,但学习能力是永恒的。

如果你读到这里,说明你对 React 源码已经有不小的兴趣,或者正在认真考虑要不要系统地学一遍。最近我刚刚完成了 React 源码的系统学习,并整理成了一个专栏,用大量示意图和流程图来讲清楚 Fiber 架构、Hooks 的内部实现、调度器以及事件系统等关键部分。

2148a5531fb6563f813d90e0dd467838

3c465d2e2e4b34ff7d851ecb64f30482

60956e3d526aca95969b8a72bad33364

如果你想用更直观的方式把这些知识真正吃透,欢迎加我微信 yunmz777 私聊,一起交流源码学习的思路和实践经验。

红松CEO李乔:红松社区愿意向新东方开放所有社区资源

2026年1月21日 20:24
36氪获悉,中国银发文娱赛道领军企业红松创始人、CEO李乔表示,欢迎新东方加入银发文娱赛道,红松社区愿意向新东方开放所有社区资源。有媒体报道,“北京新东方退休俱乐部”账号近日在社交平台上线,其主要面向北京50-75岁退休人群推出兴趣课程与本地社交活动。

苏州“十五五”规划建议:推进人工智能驱动的新型科研范式、技术研发、工程实现、产品落地一体化协同发展

2026年1月21日 20:20
36氪获悉,中共苏州市委关于制定苏州市国民经济和社会发展第十五个五年规划的建议发布,其中提到,高水平建设“人工智能+”城市。深入推进数字苏州建设,抢抓国家新一代人工智能创新发展试验区和人工智能赋能新型工业化先导区建设机遇,全面实施“人工智能+”行动,建设国家人工智能应用中试基地(制造领域),以人工智能赋能制造业全面转型升级。推进人工智能驱动的新型科研范式、技术研发、工程实现、产品落地一体化协同发展,积极部署人工智能芯片、量子等创新应用技术,研发推广垂类模型。构建人工智能“芯片—算法—平台—应用”全链条产业生态,培育人工智能优势特色产业集群。

苏州“十五五”规划建议:推动具身智能等产业加快形成新的经济增长点

2026年1月21日 20:13
36氪获悉,中共苏州市委关于制定苏州市国民经济和社会发展第十五个五年规划的建议发布,其中提到,培育壮大未来产业。主动谋划未来制造、未来信息、未来材料、未来能源、未来空间、未来健康等产业。探索多元技术路线、典型应用场景、可行商业模式,推动具身智能、生物制造、细胞和基因技术、量子科技、氢能和核聚变能、脑机接口、前沿新材料、原子级制造等产业加快形成新的经济增长点。大力发展创业投资,推动形成未来产业多元投入、风险共担的发展格局。争创国家级未来产业先导区。
❌
❌